Skip to content

Commit

Permalink
HCK-9614: adapt FE to work in the browser (#81)
Browse files Browse the repository at this point in the history
* HCK-9614: adapt FE to work in the browser

* HCK-9614: Small fixes

* HCK-9614: Small fixes

* HCK-9614: remove redundant imports
  • Loading branch information
Nightlngale authored Jan 23, 2025
1 parent ffeddb2 commit 3d93124
Show file tree
Hide file tree
Showing 27 changed files with 556 additions and 369 deletions.
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

0 comments on commit 3d93124

Please sign in to comment.