Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

HCK-9614: adapt FE to work in the browser #81

Merged
merged 4 commits into from
Jan 23, 2025
Merged
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
7 changes: 7 additions & 0 deletions api/fe.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
const { generateModelScript } = require('../forward_engineering/helpers/generateModelScript');
const { validate } = require('../forward_engineering/helpers/validation/validate');

module.exports = {
generateModelScript,
validate,
};
11 changes: 10 additions & 1 deletion esbuild.package.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ const fs = require('fs');
const path = require('path');
const esbuild = require('esbuild');
const { clean } = require('esbuild-plugin-clean');
const { copy } = require('esbuild-plugin-copy');
const { copyFolderFiles, addReleaseFlag } = require('@hackolade/hck-esbuild-plugins-pack');
const { EXCLUDED_EXTENSIONS, EXCLUDED_FILES, DEFAULT_RELEASE_FOLDER_PATH } = require('./buildConstants');

Expand All @@ -11,21 +12,29 @@ const RELEASE_FOLDER_PATH = path.join(DEFAULT_RELEASE_FOLDER_PATH, `${packageDat
esbuild
.build({
entryPoints: [
path.resolve(__dirname, 'api', 'fe.js'),
path.resolve(__dirname, 'api', 're.js'),
path.resolve(__dirname, 'forward_engineering', 'api.js'),
path.resolve(__dirname, 'reverse_engineering', 'api.js'),
],
bundle: true,
keepNames: true,
platform: 'node',
target: 'node18',
target: 'node16',
outdir: RELEASE_FOLDER_PATH,
minify: true,
logLevel: 'info',
external: ['lodash'],
plugins: [
clean({
patterns: [DEFAULT_RELEASE_FOLDER_PATH],
}),
copy({
assets: {
from: [path.join('node_modules', 'lodash', '**', '*')],
to: [path.join('node_modules', 'lodash')],
},
}),
copyFolderFiles({
fromPath: __dirname,
targetFolderPath: RELEASE_FOLDER_PATH,
Expand Down
254 changes: 4 additions & 250 deletions forward_engineering/api.js
Original file line number Diff line number Diff line change
@@ -1,253 +1,7 @@
const yaml = require('js-yaml');
const get = require('lodash.get');
const validationHelper = require('./helpers/validationHelper');
const getInfo = require('./helpers/infoHelper');
const { getPaths } = require('./helpers/pathHelper');
const getComponents = require('./helpers/componentsHelpers');
const commonHelper = require('./helpers/commonHelper');
const { getServers } = require('./helpers/serversHelper');
const getExtensions = require('./helpers/extensionsHelper');
const handleReferencePath = require('./helpers/handleReferencePath');
const mapJsonSchema = require('../reverse_engineering/helpers/adaptJsonSchema/mapJsonSchema');
const path = require('path');
const versions = require('../package.json').contributes.target.versions;
const { generateModelScript } = require('./helpers/generateModelScript');
const { validate } = require('./helpers/validation/validate');

module.exports = {
generateModelScript(data, logger, cb) {
try {
const {
dbVersion,
externalDocs: modelExternalDocs,
tags: modelTags,
security: modelSecurity,
servers: modelServers,
jsonSchemaDialect,
} = data.modelData[0];
const apiTargetVersion = data?.options?.apiTargetVersion;
const specVersion = apiTargetVersion && versions.includes(apiTargetVersion) ? apiTargetVersion : dbVersion;

const containersIdsFromCallbacks = commonHelper.getContainersIdsForCallbacks(data);

const resolveApiExternalRefs = data.options?.additionalOptions?.find(
option => option.id === 'resolveApiExternalRefs',
)?.value;

const info = getInfo(data.modelData[0]);
const servers = getServers(modelServers);
const externalDefinitions = JSON.parse(data.externalDefinitions || '{}').properties || {};
const containers = handleRefInContainers(data.containers, externalDefinitions, resolveApiExternalRefs);
const { pathContainers, webhookContainers } = separatePathAndWebhooks(containers);
const paths = getPaths(pathContainers, containersIdsFromCallbacks, specVersion);
const webhooks = getPaths(webhookContainers, containersIdsFromCallbacks, specVersion);
const definitions = JSON.parse(data.modelDefinitions) || {};
const definitionsWithHandledReferences = mapJsonSchema(
definitions,
handleRef(externalDefinitions, resolveApiExternalRefs),
);
const components = getComponents({
definitions: definitionsWithHandledReferences,
containers: data.containers,
specVersion,
});
const security = commonHelper.mapSecurity(modelSecurity);
const tags = commonHelper.mapTags(modelTags);
const externalDocs = commonHelper.mapExternalDocs(modelExternalDocs);

const openApiSchema = {
openapi: specVersion,
info,
...(jsonSchemaDialect && { jsonSchemaDialect }),
servers,
paths,
...(webhooks && Object.keys(webhooks).length ? { webhooks } : {}),
components,
security,
tags,
externalDocs,
};
const extensions = getExtensions(data.modelData[0].scopesExtensions);

const resultSchema = Object.assign({}, openApiSchema, extensions);

switch (data.targetScriptOptions.format) {
case 'yaml': {
const schema = yaml.safeDump(resultSchema, { skipInvalid: true });
const schemaWithComments = addCommentsSigns(schema, 'yaml');
cb(null, schemaWithComments);
break;
}
case 'json':
default: {
const schemaString = JSON.stringify(resultSchema, null, 2);
let schema = addCommentsSigns(schemaString, 'json');
if (!get(data, 'options.isCalledFromFETab')) {
schema = removeCommentLines(schema);
}
cb(null, schema);
}
}
} catch (err) {
logger.log('error', { error: err }, 'OpenAPI FE Error');
cb(err);
}
},

validate(data, logger, cb) {
const { script, targetScriptOptions } = data;
try {
const filteredScript = removeCommentLines(script);
let parsedScript = {};

switch (targetScriptOptions.format) {
case 'yaml':
parsedScript = yaml.safeLoad(filteredScript);
break;
case 'json':
default:
parsedScript = JSON.parse(filteredScript);
}

validationHelper
.validate(replaceRelativePathByAbsolute(parsedScript, targetScriptOptions.modelDirectory))
.then(messages => {
cb(null, messages);
})
.catch(err => {
cb(err.message);
});
} catch (e) {
logger.log('error', { error: e }, 'OpenAPI Validation Error');

cb(e.message);
}
},
};

const addCommentsSigns = (string, format) => {
const commentsStart = /hackoladeCommentStart\d+/i;
const commentsEnd = /hackoladeCommentEnd\d+/i;
const innerCommentStart = /hackoladeInnerCommentStart/i;
const innerCommentEnd = /hackoladeInnerCommentEnd/i;
const innerCommentStartYamlArrayItem = /- hackoladeInnerCommentStart/i;

const { result } = string.split('\n').reduce(
({ isCommented, result }, line, index, array) => {
if (commentsStart.test(line) || innerCommentStart.test(line)) {
if (innerCommentStartYamlArrayItem.test(line)) {
const lineBeginsAt = array[index + 1].search(/\S/);
array[index + 1] =
array[index + 1].slice(0, lineBeginsAt) + '- ' + array[index + 1].slice(lineBeginsAt);
}
return { isCommented: true, result: result };
}
if (commentsEnd.test(line)) {
return { isCommented: false, result };
}
if (innerCommentEnd.test(line)) {
if (format === 'json') {
array[index + 1] = '# ' + array[index + 1];
}
return { isCommented: false, result };
}

const isNextLineInnerCommentStart = index + 1 < array.length && innerCommentStart.test(array[index + 1]);
if (
(isCommented || isNextLineInnerCommentStart) &&
!innerCommentStartYamlArrayItem.test(array[index + 1])
) {
result = result + '# ' + line + '\n';
} else {
result = result + line + '\n';
}

return { isCommented, result };
},
{ isCommented: false, result: '' },
);

return result;
};

const removeCommentLines = scriptString => {
const isCommentedLine = /^\s*#\s+/i;

return scriptString
.split('\n')
.filter(line => !isCommentedLine.test(line))
.join('\n')
.replace(/(.*?),\s*(\}|])/g, '$1$2');
};

const replaceRelativePathByAbsolute = (script, modelDirectory) => {
if (!modelDirectory || typeof modelDirectory !== 'string') {
return script;
}
const stringifiedScript = JSON.stringify(script);
const fixedScript = stringifiedScript.replace(/("\$ref":\s*)"(.*?(?<!\\))"/g, (match, refGroup, relativePath) => {
const isAbsolutePath = relativePath.startsWith('file:');
const isInternetLink = relativePath.startsWith('http:') || relativePath.startsWith('https:');
const isModelRef = relativePath.startsWith('#');
if (isAbsolutePath || isInternetLink || isModelRef) {
return match;
}
const absolutePath = path.join(path.dirname(modelDirectory), relativePath).replace(/\\/g, '/');
return `${refGroup}"file://${absolutePath}"`;
});
return JSON.parse(fixedScript);
};

const handleRefInContainers = (containers, externalDefinitions, resolveApiExternalRefs) => {
return containers.map(container => {
try {
const updatedSchemas = Object.keys(container.jsonSchema).reduce((schemas, id) => {
const json = container.jsonSchema[id];
try {
const updatedSchema = mapJsonSchema(
JSON.parse(json),
handleRef(externalDefinitions, resolveApiExternalRefs),
);

return {
...schemas,
[id]: JSON.stringify(updatedSchema),
};
} catch (err) {
return { ...schemas, [id]: json };
}
}, {});

return {
...container,
jsonSchema: updatedSchemas,
};
} catch (err) {
return container;
}
});
};

const handleRef = (externalDefinitions, resolveApiExternalRefs) => field => {
if (!field.$ref) {
return field;
}
const ref = handleReferencePath(externalDefinitions, field, resolveApiExternalRefs);
if (!ref.$ref) {
return ref;
}

return { ...field, ...ref };
};

const separatePathAndWebhooks = containers => {
const pathContainers = [];
const webhookContainers = [];
containers.forEach(container => {
if (container.containerData?.[0]?.webhook) {
webhookContainers.push(container);
} else {
pathContainers.push(container);
}
});

return { pathContainers, webhookContainers };
generateModelScript,
validate,
};
63 changes: 62 additions & 1 deletion forward_engineering/helpers/commentsHelper.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,4 +33,65 @@ function commentDeactivatedItemOuter(item, isActivated, isParentActivated) {
return commentDeactivatedItem(item, isActivated, isParentActivated, commentFlags.outer);
}

module.exports = { commentFlags, commentDeactivatedItemInner, commentDeactivatedItemOuter };
const addCommentsSigns = (string, format) => {
const commentsStart = /hackoladeCommentStart\d+/i;
const commentsEnd = /hackoladeCommentEnd\d+/i;
const innerCommentStart = /hackoladeInnerCommentStart/i;
const innerCommentEnd = /hackoladeInnerCommentEnd/i;
const innerCommentStartYamlArrayItem = /- hackoladeInnerCommentStart/i;

const { result } = string.split('\n').reduce(
({ isCommented, result }, line, index, array) => {
if (commentsStart.test(line) || innerCommentStart.test(line)) {
if (innerCommentStartYamlArrayItem.test(line)) {
const lineBeginsAt = array[index + 1].search(/\S/);
array[index + 1] =
array[index + 1].slice(0, lineBeginsAt) + '- ' + array[index + 1].slice(lineBeginsAt);
}
return { isCommented: true, result: result };
}
if (commentsEnd.test(line)) {
return { isCommented: false, result };
}
if (innerCommentEnd.test(line)) {
if (format === 'json') {
array[index + 1] = '# ' + array[index + 1];
}
return { isCommented: false, result };
}

const isNextLineInnerCommentStart = index + 1 < array.length && innerCommentStart.test(array[index + 1]);
if (
(isCommented || isNextLineInnerCommentStart) &&
!innerCommentStartYamlArrayItem.test(array[index + 1])
) {
result = result + '# ' + line + '\n';
} else {
result = result + line + '\n';
}

return { isCommented, result };
},
{ isCommented: false, result: '' },
);

return result;
};

const removeCommentLines = scriptString => {
const isCommentedLine = /^\s*#\s+/i;

return scriptString
.split('\n')
.filter(line => !isCommentedLine.test(line))
.join('\n')
.replace(/(.*?),\s*(\}|])/g, '$1$2');
};

module.exports = {
commentFlags,
commentDeactivatedItemInner,
commentDeactivatedItemOuter,
addCommentsSigns,
removeCommentLines,
};
Loading