Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion api-playground/APIMATIC-BUILD.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"buildFileVersion": "1.0",
"generatePortal": {
"useHostedPortalScript": false,
"filterByRole": ["9.0.0.cl", "9.2.0.cl", "9.4.0.cl", "9.5.0.cl", "9.6.0.cl", "9.7.0.cl", "9.9.0.cl","9.12.0.cl","10.1.0.cl", "10.3.0.cl", "10.4.0.cl", "10.6.0.cl", "10.7.0.cl", "10.8.0.cl", "10.9.0.cl", "10.10.0.cl", "10.12.0.cl", "10.13.0.cl"],
"filterByRole": ["9.0.0.cl", "9.2.0.cl", "9.4.0.cl", "9.5.0.cl", "9.6.0.cl", "9.7.0.cl", "9.9.0.cl","9.12.0.cl","10.1.0.cl", "10.3.0.cl", "10.4.0.cl", "10.6.0.cl", "10.7.0.cl", "10.8.0.cl", "10.9.0.cl", "10.10.0.cl", "10.12.0.cl", "10.13.0.cl", "10.14.0.cl"],
"pageTitle": "ThoughtSpot Public Rest APIs",
"languageConfig": {
"http": {}
Expand Down Expand Up @@ -84,3 +84,4 @@
}
}


13 changes: 13 additions & 0 deletions api-playground/README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,19 @@
## Background
The Rest API playground is generated using APIMatic's [portal generation API](https://portal-api-docs.apimatic.io/#/http/getting-started/overview-apimatic-portal-generation) and deployed directly on Vercel

## Hiding Fields

To hide specific fields from the API playground UI:

Add configuration to `api-playground-config.json`:
```json
{
"hideApiFields": [
{ "operationId": "APIOperantionId", "fields": ["field.refSchemaField"] }
]
}
```

## Local development
Prepare the build folder

Expand Down
6 changes: 6 additions & 0 deletions api-playground/api-playground-config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"hideApiFields": [
{ "operationId": "getFullAccessToken", "fields": ["user_parameters"] },
{ "operationId": "getObjectAccessToken", "fields": ["user_parameters"] }
]
}
4 changes: 4 additions & 0 deletions api-playground/build.sh
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ mkdir $PWD/build $PWD/spec
# prepare spec
cp ../api-spec/* spec/

if [ -f $PWD/processSpec.js ]; then
npx node processSpec.js
fi

# native zip and unzip unavailable
npx bestzip $PWD/build/portal-input.zip .

Expand Down
171 changes: 171 additions & 0 deletions api-playground/processSpec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
const fs = require('fs');

const SPEC_PATH = './spec/openapiSpecv3-2_0.json';
const CONFIG_PATH = './api-playground-config.json';

function isObject(value) {
return value !== null && typeof value === 'object' && !Array.isArray(value);
}

//Resolves a JSON reference pointer (e.g., "#/components/schemas/MySchema") to the actual object in the spec.
function resolveRef(root, ref) {
if (typeof ref !== 'string' || !ref.startsWith('#/')) return null;
const parts = ref.slice(2).split('/').map((p) => p.replace(/~1/g, '/').replace(/~0/g, '~'));
let node = root;
for (const part of parts) {
if (!isObject(node) || !(part in node)) return null;
node = node[part];
}
return node;
}

//Recursively traverses a schema and removes the specified field.
function hideFieldInSchema(root, schemaNode, fieldPathParts) {
if (!isObject(schemaNode)) return false;

if (schemaNode.$ref && typeof schemaNode.$ref === 'string') {
const target = resolveRef(root, schemaNode.$ref);
if (isObject(target)) {
return hideFieldInSchema(root, target, fieldPathParts);
}
return false;
}

if (isObject(schemaNode.items)) {
if (hideFieldInSchema(root, schemaNode.items, fieldPathParts)) return true;
}

let modified = false;
for (const keyword of ['allOf', 'oneOf', 'anyOf']) {
const variants = schemaNode[keyword];
if (Array.isArray(variants)) {
for (const variant of variants) {
if (hideFieldInSchema(root, variant, fieldPathParts)) modified = true;
}
}
}

if (!isObject(schemaNode.properties)) return modified;

const [current, ...rest] = fieldPathParts;

if (rest.length === 0) {
if (schemaNode.properties[current]) {
delete schemaNode.properties[current];
return true;
}
return modified;
}

const child = schemaNode.properties[current];
if (!child || typeof child !== 'object') return modified;
return hideFieldInSchema(root, child, rest) || modified;
}

//Processes all operations in the spec and hides fields based on the configuration.
function hideApiFields(spec, config) {
const hideApiFieldsConfig = config.hideApiFields || [];

if (hideApiFieldsConfig.length === 0) {
console.log('No API fields specified to hide');
return;
}

const fieldsMap = new Map();
for (const { operationId, fields } of hideApiFieldsConfig) {
if (operationId && fields && fields.length > 0) {
fieldsMap.set(operationId, fields);
}
}

for (const [pathKey, pathItem] of Object.entries(spec.paths)) {
for (const [method, operation] of Object.entries(pathItem)) {
if (!operation.operationId) continue;

const fields = fieldsMap.get(operation.operationId);
if (!fields) continue;

console.log(`\nProcessing operation: ${operation.operationId} (${method.toUpperCase()} ${pathKey})`);

hideFieldsInOperation(spec, operation, fields);

}
}
}

//Hides specified fields from a single operation's parameters and request body.
function hideFieldsInOperation(spec, op, fields) {
const hidden = { parameters: [], requestBody: [] };

if (op.parameters) {
const simpleFields = new Set(fields.filter((f) => !f.includes('.')));
op.parameters = op.parameters.filter((p) => {
if (p.name && simpleFields.has(p.name)) {
hidden.parameters.push(p.name);
return false;
}
return true;
});
}

const requestBody = op.requestBody;

if (requestBody && requestBody.content) {
for (const media of Object.values(requestBody.content)) {
if (media && media.schema) {
for (const field of fields) {
const parts = field.split('.').filter(Boolean);
if (parts.length && hideFieldInSchema(spec, media.schema, parts)) {
hidden.requestBody.push(field);
}
}
}
}
}

return hidden;
}

async function processOpenApiSpec() {
try {
if (!fs.existsSync(CONFIG_PATH)) {
console.error(`Config file not found: ${CONFIG_PATH}`);
process.exit(1);
}

const specContent = fs.readFileSync(SPEC_PATH, 'utf8');
const spec = JSON.parse(specContent);

const configContent = fs.readFileSync(CONFIG_PATH, 'utf8');
const config = JSON.parse(configContent);

if (config.hideApiFields && config.hideApiFields.length > 0) {
console.log('\n=== Hiding API Fields in Spec ===');
hideApiFields(spec, config);
}

let updatedSpecContent = JSON.stringify(spec, null, 2);
if (!updatedSpecContent.endsWith('\n')) {
updatedSpecContent += '\n';
}
fs.writeFileSync(SPEC_PATH, updatedSpecContent, 'utf8');

console.log('\n=== Processing Complete ===');
console.log('Successfully updated OpenAPI specification file');

} catch (error) {
console.error('Error processing files:', error.message);
process.exit(1);
}
}

if (require.main === module) {
processOpenApiSpec();
}

module.exports = {
processOpenApiSpec,
hideApiFields
};


7 changes: 4 additions & 3 deletions api-playground/static/js/embedded.js
Original file line number Diff line number Diff line change
Expand Up @@ -83,17 +83,18 @@ const setPlaygroundConfig = ({ baseUrl, accessToken }) => {
if(isApiMaticPortalReady) {
_setConfig((defaultConfig) => {
return {
...defaultConfig,
showFullCode: false,
// ...defaultConfig,
showFullCode: false,
auth: {
bearerAuth: {
AccessToken: accessToken,
},
},
config: {
...defaultConfig.config,
// ...defaultConfig.config,
"base-url": baseUrl,
},
environment: "production",
};
});
}
Expand Down
2,122 changes: 1,919 additions & 203 deletions api-spec/openapiSpecv3-2_0.json

Large diffs are not rendered by default.

Loading