grapi-cli
Version:
a cli tool to generate loopback 4 applications with extra features like caching & fuzzy search
571 lines • 50.3 kB
JavaScript
import { existsSync } from 'fs';
import { applyPatches, getNpmGlobalDir, removeCodeSection } from '../utils/utils.js';
import { applyPrePatches } from './pre-patches.js';
const cliPath = await getNpmGlobalDir();
if (!existsSync(cliPath))
throw new Error('Loopback\'s CLI is not installed.');
const patches = {
controllerTypeAssignment: {
assignType: {
searchString: 'async promptArtifactCrudVars() {',
replacement: `async promptArtifactCrudVars() {\nif (this.options.controllerType === 'BASIC') { this.artifactInfo.controllerType = ControllerGenerator.BASIC; }
if (this.options.controllerType === 'REST') { this.artifactInfo.controllerType = ControllerGenerator.REST; }`,
path: `${cliPath}/generators/controller/index.js`
}
},
// supportReadonlyProperties: {
// requireRelationUtil: {
// searchString: `const utils = require('../../lib/utils');`,
// replacement: `const utils = require('../../lib/utils');\nconst relationUtils = require('../relation/utils.generator');`,
// path: `${cliPath}/generators/controller/index.js`
// },
// implementReadonly: {
// searchString: `const source = this.templatePath(path.join('src', 'controllers', template));`,
// replacement: `\n if(this.artifactInfo.controllerType === ControllerGenerator.REST) { this.artifactInfo.exclude = [];if (this.artifactInfo.idOmitted) {this.artifactInfo.exclude.push(this.artifactInfo.id);}const project = new relationUtils.AstLoopBackProject();const fileName = path.resolve(this.artifactInfo.modelDir,utils.getModelFileName(this.artifactInfo.modelName),);const modelFile = project.addSourceFileAtPath(fileName);const modelClass = modelFile.getClassOrThrow(this.artifactInfo.modelName);for (const classProperty of modelClass.getInstanceProperties()) { for (const decorator of classProperty.getDecorators()) { \nfor (const decoratorArg of decorator.getArguments()) { \nif (decoratorArg.getProperty) { const readOnlyProperty = decoratorArg.getProperty('readOnly'); \nif (readOnlyProperty) {const readOnlyValue = readOnlyProperty.getInitializerOrThrow().getText();\nif (readOnlyValue === '1' || readOnlyValue === 'true') {this.artifactInfo.exclude.push(classProperty.getName());}}}}}}} const source = this.templatePath(path.join('src', 'controllers', template));`,
// path: `${cliPath}/generators/controller/index.js`
// },
// updateControllerTemplate: {
// searchString: `<%if (idOmitted) {%>exclude: ['<%= id %>'],<% } %>`,
// replacement: `<%if (exclude.length) {%>exclude: [<% exclude.forEach(function(item,index){ %>'<%= item %>', <% }) %>],<% } %>`,
// path: `${cliPath}/generators/controller/templates/src/controllers/controller-rest-template.ts.ejs`
// },
// updateControllerTemplate1: {
// searchString: `<%= modelVariableName %>: <% if (!idOmitted) { -%><%= modelName %><% } else { -%>Omit<<%= modelName %>, '<%= id %>'><% } -%>,`,
// replacement: `<%= modelVariableName %>: <% if (!exclude.length) { -%><%= modelName %><% } else { -%>Omit<<%= modelName %>, '<%= exclude %>'><% } -%>,`,
// path: `${cliPath}/generators/controller/templates/src/controllers/controller-rest-template.ts.ejs`
// },
// updateHasMany: {
// searchString: 'const source = this.templatePath(CONTROLLER_TEMPLATE_PATH_HAS_MANY);',
// replacement: `const project = new relationUtils.AstLoopBackProject();const sourceFile = relationUtils.addFileToProject(project,this.artifactInfo.modelDir,options.sourceModel,);this.artifactInfo.exclude = [this.artifactInfo.targetModelPrimaryKey];const sourceClass = relationUtils.getClassObj(sourceFile,options.sourceModel,);for (const classProperty of sourceClass.getInstanceProperties()) {for (const decorator of classProperty.getDecorators()) {for (const decoratorArg of decorator.getArguments()) {if (decoratorArg.getProperty) {const readOnlyProperty = decoratorArg.getProperty('readOnly');if (readOnlyProperty) {const readOnlyValue = readOnlyProperty.getInitializerOrThrow().getText();if (readOnlyValue === '1' || readOnlyValue === 'true') { this.artifactInfo.exclude.push(classProperty.getName());}}}}}} const source = this.templatePath(CONTROLLER_TEMPLATE_PATH_HAS_MANY);`,
// path: `${cliPath}/generators/relation/has-many-relation.generator.js`
// },
// updateHasManyThrough: {
// searchString: 'const source = this.templatePath(CONTROLLER_TEMPLATE_PATH_HAS_MANY_THROUGH);',
// replacement: `const project = new relationUtils.AstLoopBackProject();const sourceFile = relationUtils.addFileToProject(project,this.artifactInfo.modelDir,options.sourceModel,);this.artifactInfo.exclude = [this.artifactInfo.targetModelPrimaryKey];const sourceClass = relationUtils.getClassObj(sourceFile,options.sourceModel,);for (const classProperty of sourceClass.getInstanceProperties()) {for (const decorator of classProperty.getDecorators()) {for (const decoratorArg of decorator.getArguments()) {if (decoratorArg.getProperty) {const readOnlyProperty = decoratorArg.getProperty('readOnly');if (readOnlyProperty) {const readOnlyValue = readOnlyProperty.getInitializerOrThrow().getText();if (readOnlyValue === '1' || readOnlyValue === 'true') {this.artifactInfo.exclude.push(classProperty.getName());}}}}}} \n const source = this.templatePath(CONTROLLER_TEMPLATE_PATH_HAS_MANY_THROUGH);`,
// path: `${cliPath}/generators/relation/has-many-through-relation.generator.js`
// },
// updateHasOne: {
// searchString: `utils.toFileName(this.artifactInfo.name) + '.controller.ts';`,
// replacement: `utils.toFileName(this.artifactInfo.name) + '.controller.ts';const project = new relationUtils.AstLoopBackProject();const sourceFile = relationUtils.addFileToProject(project,this.artifactInfo.modelDir,options.sourceModel,);this.artifactInfo.exclude = [this.artifactInfo.targetModelPrimaryKey];const sourceClass = relationUtils.getClassObj(sourceFile,options.sourceModel,);for (const classProperty of sourceClass.getInstanceProperties()) {for (const decorator of classProperty.getDecorators()) {for (const decoratorArg of decorator.getArguments()) {if (decoratorArg.getProperty) {const readOnlyProperty = decoratorArg.getProperty('readOnly');if (readOnlyProperty) {const readOnlyValue = readOnlyProperty.getInitializerOrThrow().getText();if (readOnlyValue === '1' || readOnlyValue === 'true') {this.artifactInfo.exclude.push(classProperty.getName());}}}}}}`,
// path: `${cliPath}/generators/relation/has-one-relation.generator.js`
// },
// updateHasOne1: {
// searchString: `const imports = relationUtils.getRequiredImports(targetModel, relationType);`,
// replacement: `const imports = relationUtils.getRequiredImports(targetModel, relationType, sourceModel, );`,
// path: `${cliPath}/generators/relation/has-one-relation.generator.js`
// },
// updateHasOne2: {
// searchString: `dstRepositoryClassName,`,
// replacement: `dstRepositoryClassName,this.artifactInfo.srcModelClass,`,
// path: `${cliPath}/generators/relation/has-one-relation.generator.js`
// },
// updateHasManyThroughTemplate: {
// searchString: `exclude: ['<%= targetModelPrimaryKey %>'],`,
// replacement: `exclude: [<% exclude.forEach(function(item,index){ %>'<%= item %>', <% }) %>],`,
// path: `${cliPath}/generators/relation/templates/controller-relation-template-has-many-through.ts.ejs`
// },
// updateHasManyTemplate: {
// searchString: `exclude: ['<%= targetModelPrimaryKey %>'],`,
// replacement: `exclude: [<% exclude.forEach(function(item,index){ %>'<%= item %>', <% }) %>],`,
// path: `${cliPath}/generators/relation/templates/controller-relation-template-has-many.ts.ejs`
// },
// updateHasManyThroughTemplate1: {
// searchString: `}) <%= targetModelRequestBody %>: Omit<<%= targetModelClassName %>, '<%= targetModelPrimaryKey %>'>,`,
// replacement: `exclude: [<% exclude.forEach(function(item,index){ %>'<%= item %>', <% }) %>],`,
// path: `${cliPath}/generators/relation/templates/controller-relation-template-has-many-through.ts.ejs`
// },
// updateHasManyTemplate1: {
// searchString: `}) <%= targetModelRequestBody %>: Omit<<%= targetModelClassName %>, '<%= targetModelPrimaryKey %>'>,`,
// replacement: `exclude: [<% exclude.forEach(function(item,index){ %>'<%= item %>', <% }) %>],`,
// path: `${cliPath}/generators/relation/templates/controller-relation-template-has-many.ts.ejs`
// },
// updateHasOneTemplate: {
// searchString: '<%= sourceModelClassName %>,',
// replacement: `<%if (sourceModelClassName != targetModelClassName) { %><%= sourceModelClassName %>,<% } %>`,
// path: `${cliPath}/generators/relation/templates/controller-relation-template-has-one.ts.ejs`
// },
// updateHasOneTemplate1: {
// searchString: `exclude: ['<%= targetModelPrimaryKey %>'],`,
// replacement: `exclude: [<% exclude.forEach(function(item,index){ %>'<%= item %>', <% }) %>],`,
// path: `${cliPath}/generators/relation/templates/controller-relation-template-has-one.ts.ejs`
// },
// updateHasOneTemplate2: {
// searchString: `}) <%= targetModelRequestBody %>: Omit<<%= targetModelClassName %>, '<%= targetModelPrimaryKey %>'>,`,
// replacement: `}) <%= targetModelRequestBody %>: Omit<<%= targetModelClassName %>, '<%= exclude %>'>,`,
// path: `${cliPath}/generators/relation/templates/controller-relation-template-has-one.ts.ejs`
// }
// },
addPrefix: {
addOption: {
searchString: 'return super._setupGenerator();',
replacement: `this.option('prefix', {description: g.f('Provide prefix to avoid duplication'),required: false,type: String,});return super._setupGenerator();`,
path: `${cliPath}/generators/openapi/index.js`
},
addPrompt: {
searchString: 'async askForSpecUrlOrPath() {',
replacement: `async askForPrefix() {if (this.shouldExit()) return; const prompts = [{name: 'prefix',message: g.f('Provide prefix to avoid duplication'),when: !this.options.prefix,default: 'openapi',},];const answers = await this.prompt(prompts);if (answers.prefix) {this.options.prefix = answers.prefix;}this.options.prefix = this.options.prefix.replace(/\w+/g,w => w[0].toUpperCase() + w.slice(1).toLowerCase(),);}async askForSpecUrlOrPath() {`,
path: `${cliPath}/generators/openapi/index.js`
},
destructorPrefix: {
searchString: `promoteAnonymousSchemas: this.options['promote-anonymous-schemas'],`,
replacement: `promoteAnonymousSchemas: this.options['promote-anonymous-schemas'],prefix: this.options.prefix,`,
path: `${cliPath}/generators/openapi/index.js`
},
appendPrefix: {
searchString: `const choices = this.controllerSpecs.map(c => {`,
replacement: `this.controllerSpecs = this.controllerSpecs.map(c => {if (c.tag.includes(this.options.prefix)) {let splited = c.tag.split(this.options.prefix);c.tag = splited.join('');} return c;});const choices = this.controllerSpecs.map(c => {`,
path: `${cliPath}/generators/openapi/index.js`
},
appendPrefixToFileName: {
searchString: `c.fileName = getControllerFileName(c.tag || c.className);`,
replacement: `c.fileName = getControllerFileName(c.tag || c.className);c.fileName = this.options.prefix.toLowerCase() + '.' + c.fileName;`,
path: `${cliPath}/generators/openapi/index.js`
},
destructorPrefixFromParam: {
searchString: '{log, validate, promoteAnonymousSchemas} = {},',
replacement: '{log, validate, promoteAnonymousSchemas, prefix} = {},',
path: `${cliPath}/generators/openapi/spec-loader.js`
},
updateSpecification: {
searchString: 'const apiSpec = await loadSpec(url, {log, validate});',
replacement: `const jsonc = require('jsonc');\n let apiSpec = await loadSpec(url, {log, validate});\nconst {components, paths} = apiSpec;\nlet stringifiedApiSpecs = jsonc.stringify(apiSpec);\nstringifiedApiSpecs = stringifiedApiSpecs.replaceAll('WithRelations',\`\$\{prefix\}WithRelations\`,);if (paths) {Object.keys(paths).forEach(eachPath => {if (\!eachPath.includes('{id}') \&\& \!eachPath.includes('count')) {const updatedPath =eachPath.slice(0, 0) + '/' + prefix.toLowerCase() + '/' +eachPath.slice(1);stringifiedApiSpecs = stringifiedApiSpecs.replaceAll(eachPath,updatedPath,);}});}if (components) {const {schemas} = components;if (schemas) {Object.keys(schemas).forEach(item => {if (\!item.startsWith('loopback') \&\& \!item.startsWith('New') \&\& \!item.endsWith('Relations') \&\& \!item.endsWith('Partial') \&\& \!item.includes('Through') \&\& \!item.includes('.') \&\& \!item.includes('Ping')) {stringifiedApiSpecs = stringifiedApiSpecs.replaceAll(item,prefix + item,);}if (item.includes('Ping')) {stringifiedApiSpecs = stringifiedApiSpecs.replaceAll('Ping',prefix + 'Ping');}});}}\napiSpec = jsonc.parse(stringifiedApiSpecs);`,
path: `${cliPath}/generators/openapi/spec-loader.js`
},
},
limitOpenapi: {
addOptions: {
searchString: 'return super._setupGenerator();',
replacement: `\nthis.option('readonly', { description: g.f('Generate only GET endpoints.'), required: false, type: Boolean, }); this.option('exclude', { description: g.f('Exclude endpoints with provided regex.'), required: false, type: String, }); this.option('include', { description: g.f('Only include endpoints with provided regex.'), required: false, type: String, }); return super._setupGenerator();`,
path: `${cliPath}/generators/openapi/index.js`
},
addPromptsForOptions: {
searchString: 'async askForSpecUrlOrPath() {',
replacement: `\nasync askForReadonly() {if (this.shouldExit()) return;const prompts = [{name: 'readonly',message: g.f('Generate only GET endpoints.'),when: false,default: false,},];const answers = await this.prompt(prompts);if (answers.readonly && !this.options.readonly) {this.options.readonly = answers.readonly;}}async askForExclude() {if (this.shouldExit()) return;const prompts = [{name: 'exclude',message: g.f('Exclude endpoints with provided regex.'),when: false,default: false,},];const answers = await this.prompt(prompts);if (answers.exclude && !this.options.exclude) {const excludes = answers.exclude.split(',');this.excludings = [];excludes.forEach(exclude => {this.excludings.push(exclude);});}}async askForInclude() {if (this.shouldExit()) return;const prompts = [{name: 'include',message: g.f('Only include endpoints with provided regex.'),when: false,default: false,},];const answers = await this.prompt(prompts);if (answers.include && !this.options.include) {const includes = answers.include.split(',');this.includings = [];includes.forEach(include => {this.includings.push(include);});}}\nasync askForSpecUrlOrPath() {`,
path: `${cliPath}/generators/openapi/index.js`
},
addImplementationForLimit: {
searchString: 'const result = await loadAndBuildSpec(this.url, {',
replacement: `let includings = []; let excludings = []; if (this.options.exclude) { excludings = this.options.exclude.split(','); } if (this.options.include) { includings = this.options.include.split(','); } if (!this.includings) this.includings = []; includings.forEach(including => { if (including.includes(':')) { const splitedInclude = including.split(':'); const temp = {}; if (splitedInclude[0] === '*') { temp[splitedInclude[1]] = ['GET', 'POST', 'PUT', 'PATCH', 'DELETE']; this.includings.push(temp); } else { temp[splitedInclude[1]] = [splitedInclude[0]]; this.includings.push(temp); } } }); if (!this.excludings) this.excludings = []; excludings.forEach(excluding => { if (excluding.includes(':')) { const splitedExclude = excluding.split(':'); const temp = {}; if (splitedExclude[0] === '*') { temp[splitedExclude[1]] = ['GET', 'POST', 'PUT', 'PATCH', 'DELETE']; this.excludings.push(temp); } else { temp[splitedExclude[1]] = [splitedExclude[0]]; this.excludings.push(temp);}}});\n\t\t\tconst result = await loadAndBuildSpec(this.url, {`,
path: `${cliPath}/generators/openapi/index.js`
},
passOptions: {
searchString: 'log: this.log,',
replacement: `\n\tlog: this.log,\n\treadonly: this.options.readonly,\n\excludings: this.excludings,\n\includings: this.includings,`,
path: `${cliPath}/generators/openapi/index.js`
},
requireOpenapiFilter: {
searchString: `require('@apidevtools/json-schema-ref-parser');`,
replacement: `require('@apidevtools/json-schema-ref-parser');\nconst openapiFilter = require('openapi-filter');`,
path: `${cliPath}/generators/openapi/spec-loader.js`
},
destructLimitOptions: {
searchString: `prefix}`,
replacement: `readonly, excludings, includings, prefix}`,
path: `${cliPath}/generators/openapi/spec-loader.js`
},
applyFilters: {
searchString: `let stringifiedApiSpecs = jsonc.stringify(apiSpec);`,
replacement: `let stringifiedApiSpecs = jsonc.stringify(apiSpec);\napiSpec = filterSpec(JSON.parse(stringifiedApiSpecs), readonly, excludings, includings);\n\t\t\tstringifiedApiSpecs=JSON.stringify(apiSpec);`,
path: `${cliPath}/generators/openapi/spec-loader.js`
},
addHelpingMethods: {
searchString: `module.exports = {`,
replacement: `\nfunction getIndiciesOf(searchStr, str, caseSensitive) {const searchStrLen = searchStr.length;if (searchStrLen === 0) { return []; }; let startIndex = 0, index;const indices = [];if (!caseSensitive) { str = str.toLowerCase(); searchStr = searchStr.toLowerCase();}while ((index = str.indexOf(searchStr, startIndex)) > -1) { indices.push(index); startIndex = index + searchStrLen;}return indices;}\nfunction insertAtIndex(str, substring, index) {return str.slice(0, index) + substring + str.slice(index);}\nfunction applyFilters(specs, options) {const openapiComponent = specs.components;specs = openapiFilter.filter(specs, options);specs.components = openapiComponent;return specs;}\nfunction findIndexes(stringSpecs, regex) {let result;const indices = [];while ((result = regex.exec(stringSpecs))) { indices.push(result.index);}return indices;}\nfunction excludeOrIncludeSpec(specs, filter) {Object.keys(filter).forEach(filterKey => { const regex = new RegExp(filterKey, 'g'); const actions = filter[filterKey]; for (const key in specs.paths) { if (Object.hasOwnProperty.call(specs.paths, key)) { if (findIndexes(key, regex).length) { if (specs.paths[key]) { actions.forEach(action => { action = action.toLowerCase(); if (specs.paths[key][action]) { specs.paths[key][action]['x-filter'] = true; } }); } } } } }); return specs;}\nfunction readonlySpec(specs) {let stringifiedSpecs = JSON.stringify(specs);const excludeOps = ['\"post\":', '\"patch\":', '\"put\":', '\"delete\":'];excludeOps.forEach(operator => { let indices = getIndiciesOf(operator, stringifiedSpecs); let indiciesCount = 0; while (indiciesCount < indices.length) { indices = getIndiciesOf(operator, stringifiedSpecs); const index = indices[indiciesCount]; stringifiedSpecs = insertAtIndex( stringifiedSpecs, '\"x-filter\": true,', index + operator.length + 1, ); indiciesCount++;}});return JSON.parse(stringifiedSpecs);}function filterSpec(specs, readonly, excludings, includings) {const options = { valid: true, info: true, strip: true, flags: ['x-filter'], servers: true, inverse: false,};if (excludings \&\& excludings.length) { excludings.forEach(exclude => { specs = excludeOrIncludeSpec(specs, exclude); }); specs = applyFilters(specs, options);}if (includings \&\& includings.length) { includings.forEach(include => { specs = excludeOrIncludeSpec(specs, include); }); options.inverse = true; specs = applyFilters(specs, options);} if (readonly) { options.inverse = false; specs = applyFilters(readonlySpec(specs), options); } return specs;}\n\nmodule.exports = {`,
path: `${cliPath}/generators/openapi/spec-loader.js`
},
},
hasManyThroughNonPrimaryKey: {
defineVariables: {
searchString: 'const sourceKey = options.sourceKeyOnThrough;',
replacement: 'const sourceKey = options.sourceKeyOnThrough; \nconst customSourceModelKey = options.customSourceModelKey; \n const customTargetModelKey = options.customTargetModelKey;',
path: `${cliPath}/generators/relation/has-many-through-relation.generator.js`
},
variableAssignment: {
searchString: 'const sourceKeyType = options.sourceModelPrimaryKeyType;',
replacement: 'const sourceKeyType = \n options.customSourceModelKeyType || options.sourceModelPrimaryKeyType;',
path: `${cliPath}/generators/relation/has-many-through-relation.generator.js`
},
variableAssignment1: {
searchString: 'const targetKeyType = options.destinationModelPrimaryKeyType;',
replacement: 'const targetKeyType = \noptions.customTargetModelKeyType || \n options.destinationModelPrimaryKeyType;',
path: `${cliPath}/generators/relation/has-many-through-relation.generator.js`
},
emptyCustomReferenceKeys: {
searchString: `let keyTo = '';`,
replacement: `let keyTo = '';let customReferenceKeyFrom = '';let customReferenceKeyTo = '';`,
path: `${cliPath}/generators/relation/has-many-through-relation.generator.js`
},
passKeysToGetHasManyThrough: {
searchString: `getHasManyThrough(`,
replacement: `getHasManyThrough(customSourceModelKey,customTargetModelKey,`,
path: `${cliPath}/generators/relation/has-many-through-relation.generator.js`,
replaceAll: true
},
constructKey: {
searchString: 'const relationDecorator = [',
replacement: `if (customSourceModelKey) { customReferenceKeyFrom = \`customReferenceKeyFrom:'\$\{customSourceModelKey\}',\`; } if (customTargetModelKey) { customReferenceKeyTo = \`customReferenceKeyTo:'\$\{customTargetModelKey\}',\`; }; const relationDecorator = [`,
path: `${cliPath}/generators/relation/has-many-through-relation.generator.js`
},
addReference: {
searchString: 'through: {model: ()',
replacement: `\${customReferenceKeyFrom}\${customReferenceKeyTo}through: {model: ()`,
path: `${cliPath}/generators/relation/has-many-through-relation.generator.js`
},
addOptions: {
searchString: `this.option('destinationModel', {`,
replacement: `this.option('customReferenceKeys', {type: String,required: false,description: g.f('Any Custom Reference Kyes'),});this.option('customSourceModelKey', {type: String,required: false,description: g.f('Custom Source model key'),});this.option('customTargetModelKey', {type: String,required: false,description: g.f('Custom Destination model'),});this.option('destinationModelKey', {type: String,required: false,description: g.f('Destination model key'),}); \n this.option('destinationModel', {`,
path: `${cliPath}/generators/relation/index.js`
},
addPrompt: {
searchString: `async promptForeignKey() {`,
replacement: `async promptCustomReferenceKeys() {if (this.shouldExit()) return false;if (this.options.customReferenceKeys) {this.artifactInfo.customReferenceKeys = this.options.customReferenceKeys;}if (this.options.customSourceModelKey) {this.artifactInfo.customSourceModelKey =this.options.customSourceModelKey;}if (this.options.customTargetModelKey) {this.artifactInfo.customTargetModelKey =this.options.customTargetModelKey;}if (this.artifactInfo.relationType === 'hasManyThrough') {const props = await this.prompt([{type: 'confirm',name: 'customReferenceKeys',message: g.f('Do you have custom reference keys for source and target models?',),when: this.artifactInfo.customReferenceKeys === undefined,default: false,},]);Object.assign(this.artifactInfo, props);if (this.artifactInfo.customReferenceKeys) {const answerSource = await this.prompt([{type: 'input',name: 'customSourceModelKey',message: g.f('What is the name of reference key in source model?'),when: this.artifactInfo.customSourceModelKey === undefined,},]);if (answerSource.customSourceModelKey) {this.artifactInfo.customSourceModelKey =answerSource.customSourceModelKey;}const answerTarget = await this.prompt([{type: 'input',name: 'customTargetModelKey',message: g.f('What is the name of reference key in target model?'),when: this.artifactInfo.customTargetModelKey === undefined,},]);if (answerTarget.customTargetModelKey) {this.artifactInfo.customTargetModelKey =answerTarget.customTargetModelKey;}const customSourceModelKeyType = relationUtils.getModelPropertyType(this.artifactInfo.modelDir,this.artifactInfo.sourceModel,this.artifactInfo.customSourceModelKey,);const customTargetModelKeyType = relationUtils.getModelPropertyType(this.artifactInfo.modelDir,this.artifactInfo.destinationModel,this.artifactInfo.customTargetModelKey,); if (customSourceModelKeyType) {this.artifactInfo.customSourceModelKeyType = customSourceModelKeyType;} else {const answer = await this.prompt([{type: 'list',name: 'customSourceModelKeyType',message: g.f('What is the type of the custom reference key of source model?',),choices: ['number', 'string', 'object'],when: this.artifactInfo.customSourceModelKeyType === undefined,default: 'number',},]);this.artifactInfo.customSourceModelKeyType =answer.customSourceModelKeyType;}if (customTargetModelKeyType) {this.artifactInfo.customTargetModelKeyType = customTargetModelKeyType;} else {const answer = await this.prompt([{type: 'list',name: 'customTargetModelKeyType',message: g.f('What is the type of the custom reference key of source model?',),choices: ['number', 'string', 'object'],when: this.artifactInfo.customTargetModelKeyType === undefined,default: 'number',},]);this.artifactInfo.customTargetModelKeyType =answer.customTargetModelKeyType;}}}} \n async promptForeignKey() {`,
path: `${cliPath}/generators/relation/index.js`
}
},
// specificDSMigration: {
// addImport: {
// searchString: `import {<%= project.applicationName %>} from './application';`,
// replacement: `import {<%= project.applicationName %>} from './application';\nimport { ApplicationConfig } from '@loopback/core';`,
// path: `${cliPath}/generators/app/templates/src/migrate.ts.ejs`
// },
// removeLog: {
// searchString: `console.log('Migrating schemas (%s existing schema)', existingSchema);`,
// replacement: ``,
// path: `${cliPath}/generators/app/templates/src/migrate.ts.ejs`
// },
// declareDS: {
// searchString: `const existingSchema = args.includes('--rebuild') ? 'drop' : 'alter';`,
// replacement: `const existingSchema = args.includes('--rebuild') ? 'drop' : 'alter';\nconst datasourceName = args.find(arg => arg.startsWith('datasource='))?.split('=')[1];`,
// path: `${cliPath}/generators/app/templates/src/migrate.ts.ejs`
// },
// reAddLog: {
// searchString: `args.find(arg => arg.startsWith('datasource='))?.split('=')[1];`,
// replacement: `args.find(arg => arg.startsWith('datasource='))?.split('=')[1];\nconsole.log('Migrating schemas (%s existing schema)', existingSchema);`,
// path: `${cliPath}/generators/app/templates/src/migrate.ts.ejs`
// },
// removeMigrationStatement: {
// searchString: `await app.migrateSchema({existingSchema});`,
// replacement: ``,
// path: `${cliPath}/generators/app/templates/src/migrate.ts.ejs`
// },
// appCreation: {
// searchString: `const app = new <%= project.applicationName %>();`,
// replacement: `const config: ApplicationConfig = {};\nconst app = new <%= project.applicationName %>(config);`,
// path: `${cliPath}/generators/app/templates/src/migrate.ts.ejs`
// },
// removeProcessExit: {
// searchString: `process.exit(0);`,
// replacement: ``,
// path: `${cliPath}/generators/app/templates/src/migrate.ts.ejs`
// },
// declareOptionAndElse: {
// searchString: `await app.boot();`,
// replacement: `await app.boot();\nlet options = {}; if (datasourceName) {console.log('Migrating specific datasource: %s', datasourceName);try {const repositoryBindings = app.find('repositories.*');const models = [];for (const binding of repositoryBindings) {if (binding.key !== 'repositories.RefreshTokenRepository') {const repo: any = await app.get(binding.key);if (repo.dataSource && repo.dataSource.name === datasourceName) {models.push(repo.entityClass.modelName);}}}if (models.length === 0) {console.warn(\`No models found for datasource \${datasourceName}\`);}options = { existingSchema, models };console.log(\`Migration of datasource \${datasourceName} completed successfully.\`);} catch (err) {console.log(err);console.error(\`Error migrating datasource \${datasourceName}:\`, err);process.exit(1);}} else {console.log('Migrating all datasources');options = { existingSchema };}await app.migrateSchema(options);process.exit(0);`,
// path: `${cliPath}/generators/app/templates/src/migrate.ts.ejs`
// },
// },
serviceGenerationFix: {
addQuotesIfHyphen: {
searchString: 'methodParameters[methodName].push(param.name);',
replacement: `const { argName } = buildParameter(param, {});methodParameters[methodName].push(argName);`,
path: `${cliPath}/generators/openapi/spec-helper.js`
}
},
addQuotesToDefaultFn: {
addQutoes: {
searchString: 'if (NON_TS_TYPES.includes(val.tsType)) {',
replacement: `if (val.defaultFn) { val.defaultFn = \`'\${val.defaultFn}'\`; }\nif (NON_TS_TYPES.includes(val.tsType)) {`,
path: `${cliPath}/generators/model/property-definition.js`
}
},
addTypeCheckBeforeJsonParsing: {
addTypeCheckBeforeJsonParsing: {
searchString: 'props[key] = JSON.parse(props[key]);',
replacement: `if (typeof props[key] === 'string') {props[key] = JSON.parse(props[key]);}`,
path: `${cliPath}/generators/datasource/index.js`
}
},
disableStrictModeInModelDiscovery: {
setStrictToFalse: {
searchString: '...discoveredSettings,',
replacement: `...discoveredSettings,strict: false,`,
path: `${cliPath}/generators/discover/import-discovered-model.js`
},
},
addEnumValuesToMySQL: {
declareEnumString: {
searchString: 'templateData.properties[key][\'type\'] = \'String\';',
replacement: `const enumItemString = enumItems.toString().replace(/,\s*$/, '');\ntemplateData.properties[key]['type'] = 'String';`,
path: `${cliPath}/generators/discover/index.js`
},
assignUpdatedMySQL: {
searchString: '`{enum: [${enumItems.toString()}]}`;',
replacement: ' `{enum: [${enumItemString}]}`;const mysqlString = templateData.properties[key][\'mysql\'];templateData.properties[key][\'mysql\'] = mysqlString.replace("\'enum\',",`\'enum\', value: "${enumItemString}",`,);',
path: `${cliPath}/generators/discover/index.js`
}
},
iterateContentProperties: {
replaceLoop: {
searchString: 'for (const m of content) {',
replacement: 'for (const eachContent in content) {',
path: `${cliPath}/generators/openapi/spec-helper.js`
},
replaceAssignment: {
searchString: 'propSchema = content[m].schema;',
replacement: 'propSchema = eachContent.schema;',
path: `${cliPath}/generators/openapi/spec-helper.js`
}
},
removeDuplicationInHasOneTemplate: {
removeDoubles: {
searchString: '<%= targetModelClassName %>,',
replacement: '',
path: `${cliPath}/generators/relation/templates/controller-relation-template-has-one.ts.ejs`,
}
},
supportAllOptionInRepoGenerator: {
checkForAllOption: {
searchString: 'if (this.options.model) {',
replacement: 'if(this.options.all){this.artifactInfo.modelNameList = modelList;}\nif (this.options.model) {',
path: `${cliPath}/generators/repository/index.js`,
},
},
setAllTrueIfAllModelSelected: {
checkForAllOption: {
searchString: 'debug(`props after model list prompt: ${inspect(props)}`);',
replacement: 'if (this.artifactInfo.modelNameList.length === modelList.length) {this.artifactInfo.all = true;} else {this.options.model = this.artifactInfo.modelNameList.toString();}\ndebug(`props after model list prompt: ${inspect(props)}`);',
path: `${cliPath}/generators/repository/index.js`,
},
},
supportMultiModelsInRepoGenerator: {
addSplitingModelsByComma: {
searchString: 'this.options.model = utils.toClassName(this.options.model);',
replacement: 'let models = this.options.model.split(\',\');models = models.map(model => utils.toClassName(model));const modelNameList = [];this.options.model = models.toString();models.forEach(model => {if (modelList \&\& modelList.length > 0 \&\& modelList.includes(model)) {modelNameList.push(model);} else {modelList = [];}});Object.assign(this.artifactInfo, { modelNameList });',
path: `${cliPath}/generators/repository/index.js`,
},
removeSingleModelImplementation: {
searchString: `if\\s*\\(\\s*modelList\\s*&&\\s*modelList\\.length\\s*>\\s*0\\s*&&\\s*modelList\\.includes\\(\\s*this\\.options\\.model\\s*\\)\\s*\\)\\s*\\{\\s*Object\\.assign\\(\\s*this\\.artifactInfo,\\s*\\{modelNameList:\\s*\\[this\\.options\\.model\\]\\}\\);\\s*\\}\\s*else\\s*\\{\\s*modelList\\s*=\\s*\\[\\];\\s*\\}`,
replacement: '',
path: `${cliPath}/generators/repository/index.js`,
isRegex: true
},
},
generateConfigs: {
forAppGenerator: {
searchString: 'if (this.shouldExit()) return result;',
replacement: `if (this.shouldExit()) return result;\nif(\!this.projectInfo.config \&\& this.options['generate-configs']) {delete this.projectInfo['generate-configs'];const configs = { ...this.projectInfo };delete configs.projectType;delete configs.dependencies;this.log(JSON.stringify(configs));process.exit(0)}`,
path: `${cliPath}/generators/app/index.js`,
},
forControllerGenerator: {
searchString: '// renames the file',
replacement: `if(\!this.artifactInfo.config \&\& this.options['generate-configs']) {delete this.artifactInfo['generate-configs'];const configs = { ...this.artifactInfo };this.log(JSON.stringify(configs));process.exit(0)}`,
path: `${cliPath}/generators/controller/index.js`,
},
forDatasourceGenerator: {
searchString: '// From LB3',
replacement: `if(\!this.artifactInfo.config \&\& this.options['generate-configs']) {delete this.artifactInfo['generate-configs'];const configs = { ...this.artifactInfo };this.log(JSON.stringify(configs));process.exit(0)}`,
path: `${cliPath}/generators/datasource/index.js`,
},
forDiscoverGenerator: {
searchString: '// Exit if needed',
replacement: `if (\!this.artifactInfo.config \&\& this.options['generate-configs']) {delete this.artifactInfo['generate-configs'];const configs = { ...this.artifactInfo };if (configs.modelDefinitions.length === this.modelChoices.length) {delete configs.modelDefinitions;configs.all = true;} else {configs.modelDefinitions.forEach(({ name }) => {if (\!configs.models) configs.models = '';configs.models += \`\${name},\`;});configs.models = configs.models.substring(0, configs.models.length - 1);delete configs.modelDefinitions;}delete configs.dataSource;configs.schema = this.options.schema;this.log(JSON.stringify(configs));process.exit(0);}`,
path: `${cliPath}/generators/discover/index.js`,
},
forInterceptorGenerator: {
searchString: '// Setting up data for templates',
replacement: `if(\!this.artifactInfo.config \&\& this.options['generate-configs']) {delete this.artifactInfo['generate-configs'];const configs = { ...this.artifactInfo };this.log(JSON.stringify(configs));process.exit(0)}`,
path: `${cliPath}/generators/interceptor/index.js`,
},
forModelGenerator: {
searchString: 'debug(\'scaffolding\');',
replacement: `if(\!this.artifactInfo.config \&\& this.options['generate-configs']) {delete this.artifactInfo['generate-configs'];const configs = { ...this.artifactInfo };this.log(JSON.stringify(configs));process.exit(0)}\ndebug('scaffolding');`,
path: `${cliPath}/generators/model/index.js`,
},
forOpenapiGenerator: {
searchString: 'this._generateModels();',
replacement: `if(\!this.artifactInfo.config \&\& this.options['generate-configs']) {delete this.artifactInfo['generate-configs'];const configs = { ...this.artifactInfo };configs.url = this.url;this.log(JSON.stringify(configs));process.exit(0)}\nthis._generateModels();`,
path: `${cliPath}/generators/openapi/index.js`,
},
forRestCrudGenerator: {
searchString: 'if (_.isEmpty(this.artifactInfo.modelNameList)) {',
replacement: `if(\!this.artifactInfo.config \&\& this.options['generate-configs']) {delete this.artifactInfo['generate-configs'];const configs = { ...this.artifactInfo };configs.url = this.url;this.log(JSON.stringify(configs));process.exit(0)}\nif (_.isEmpty(this.artifactInfo.modelNameList)) {`,
path: `${cliPath}/generators/rest-crud/index.js`,
},
forServiceGenerator: {
searchString: '// Setting up data for templates',
replacement: 'if(\!this.artifactInfo.config \&\& this.options[\'generate-configs\']) {delete this.artifactInfo[\'generate-configs\'];const configs = {};configs.name = this.artifactInfo.name;configs.serviceType = this.artifactInfo.serviceType;configs.datasource = this.artifactInfo.datasource;this.log(JSON.stringify(configs));process.exit(0);}',
path: `${cliPath}/generators/service/index.js`,
},
forRepositoryGenerator: {
searchString: 'this.artifactInfo.isRepositoryBaseBuiltin = BASE_REPOSITORIES.includes(',
replacement: 'if (\!this.artifactInfo.config && this.options[\'generate-configs\']) {delete this.artifactInfo[\'generate-configs\'];const configs = {};configs.datasource = this.artifactInfo.dataSourceName;configs.model = this.options.model;configs.id = this.options.idProperty;configs.all = this.artifactInfo.all;configs.repositoryBaseClass = this.artifactInfo.repositoryBaseClass;this.log(JSON.stringify(configs));process.exit(0);}\nthis.artifactInfo.isRepositoryBaseBuiltin = BASE_REPOSITORIES.includes(',
path: `${cliPath}/generators/repository/index.js`,
},
forRelationGenerator: {
searchString: 'let relationGenerator;',
replacement: 'if (\!this.artifactInfo.config \&\& this.options[\'generate-configs\']) {delete this.artifactInfo[\'generate-configs\'];const configs = {};configs.relationName = this.artifactInfo.relationName;configs.sourceModel = this.artifactInfo.sourceModel;configs.destinationModel = this.artifactInfo.destinationModel;configs.throughModel = this.artifactInfo.throughModel;configs.foreignKeyName = this.artifactInfo.foreignKeyName;configs.sourceModelPrimaryKey = this.artifactInfo.sourceModelPrimaryKey;configs.destinationModelPrimaryKey = this.artifactInfo.destinationModelPrimaryKey;configs.sourceKeyOnThrough = this.artifactInfo.sourceKeyOnThrough;configs.sourceModelPrimaryKeyType = this.artifactInfo.sourceModelPrimaryKeyType;configs.targetKeyOnThrough = this.artifactInfo.targetKeyOnThrough;configs.destinationModelPrimaryKeyType = this.artifactInfo.destinationModelPrimaryKeyType;configs.relationName = this.artifactInfo.relationName;configs.relationType = this.artifactInfo.relationType;configs.registerInclusionResolver = this.artifactInfo.registerInclusionResolver;this.log(JSON.stringify(configs));process.exit(0);}\nlet relationGenerator;',
path: `${cliPath}/generators/relation/index.js`,
},
},
fixDiscoveryWithMissingModels: {
ignoreUndefinedModel: {
searchString: 'const modelInfo = this.discoveringModels[i];',
replacement: `const modelInfo = this.discoveringModels[i];\nif (\!modelInfo) continue;`,
path: `${cliPath}/generators/discover/index.js`,
},
},
outDirControllers: {
outDirOption: {
searchString: 'return super._setupGenerator();',
replacement: `this.option('outDir', {type: String,description: 'Custom output directory for controller'});\nreturn super._setupGenerator();`,
path: `${cliPath}/generators/controller/index.js`,
},
outDirOptionToArtifact: {
searchString: 'const dest = this.destinationPath(',
replacement: `if (this.options.outDir) {this.artifactInfo.outDir = path.resolve(this.options.outDir);}\nconst dest = this.destinationPath(`,
path: `${cliPath}/generators/controller/index.js`,
},
changeParameter: {
searchString: 'updateIndex(dir',
replacement: `updateIndex(this.artifactInfo.relPath`,
path: `${cliPath}/lib/base-generator.js`,
},
addParameter: {
searchString: 'this.fs)',
replacement: `this.fs, subDir)`,
path: `${cliPath}/lib/base-generator.js`,
},
addSubDir: {
searchString: 'async _updateIndexFile(dir, file) {',
replacement: `async _updateIndexFile(dir, file) {\nconst subDir = path.relative(this.artifactInfo.relPath, this.artifactInfo.outDir);`,
path: `${cliPath}/lib/base-generator.js`,
},
removeExtraPath: {
searchString: 'updateDirRelPath,',
replacement: `'',`,
path: `${cliPath}/lib/base-generator.js`,
},
addParameterToMethod: {
searchString: 'fsApis)',
replacement: `fsApis, subDir = '')`,
path: `${cliPath}/lib/update-index.js`,
},
changeExport: {
searchString: `from './`,
replacement: `from './\${subDir? subDir + '/':''}`,
path: `${cliPath}/lib/update-index.js`,
},
},
sameTableRelationInDiscover: {
setStrictToFalse: {
searchString: 'this.copyTemplatedFiles(',
replacement: `if (templateData && templateData['relationDestinationImports'] && templateData['relationDestinationImports'].includes(templateData['name'])) { templateData['relationDestinationImports'] = templateData['relationDestinationImports'].filter(e => e !== templateData['name']); }\nthis.copyTemplatedFiles(`,
path: `${cliPath}/generators/discover/index.js`
},
},
retainPropertyDetailsAfterCreatingRelation: {
addUtilMethod: {
searchString: 'exports.getPropertyType = function (classObj, propertyName) {',
replacement: `exports.getPropertyDecoratorDetails = function (classObj, propertyName) {propertyName = utils.camelCase(propertyName);\n if(!classObj.getProperty(propertyName)) {return ''}\nreturn classObj.getProperty(propertyName).getDecorator('property').getArguments()[0].getText();};\nexports.getPropertyType = function (classObj, propertyName) {`,
path: `${cliPath}/generators/relation/utils.generator.js`
},
fetchForeignKeyDetails: {
searchString: 'const sourceClass = relationUtils.getClassObj(sourceFile, sourceModel);',
replacement: `const sourceClass = relationUtils.getClassObj(sourceFile, sourceModel);\nconst foreignKeyDetails = relationUtils.getPropertyDecoratorDetails(sourceClass, foreignKeyName);`,
path: `${cliPath}/generators/relation/belongs-to-relation.generator.js`
},
addDetailsToUpdatedProperty: {
searchString: 'relationUtils.addProperty(sourceClass, modelProperty);',
replacement: `if(foreignKeyDetails !== '') {if (foreignKeyDetails.includes('nullable: true') || foreignKeyDetails.includes('nullable: true')) { modelProperty['hasQuestionToken'] = true;} if (!modelProperty['decorators'][0]['arguments'][0].includes(',')) { modelProperty['decorators'][0]['arguments'].push("{}"); } modelProperty['decorators'][0]['arguments'].push(foreignKeyDetails);}\nrelationUtils.addProperty(sourceClass, modelProperty);`,
path: `${cliPath}/generators/relation/belongs-to-relation.generator.js`
}
},
enableRelationsWhileDiscovery: {
declareRelationVariable: {
searchString: 'this.artifactInfo.indexesToBeUpdated =',
replacement: ` const relations = []; const repositoryConfigs = { datasource: '', repositories: new Set(), repositoryBaseClass: 'DefaultCrudRepository' };\nthis.artifactInfo.indexesToBeUpdated =`,
path: `${cliPath}/generators/discover/index.js`
},
constructConfigs: {
searchString: 'if (targetModel) {',
replacement: `if (targetModel) {
const configs = {};
configs['sourceModel'] = templateData.name;
configs['destinationModel'] = targetModel.name;
configs['foreignKeyName'] = relation.foreignKey;
configs['relationType'] = relation.type;
configs['registerInclusionResolver'] = true;
configs['yes'] = true;
relations.push(configs);
repositoryConfigs['datasource'] = this.options.datasource || this.options.dataSource;
repositoryConfigs.repositories.add(templateData.name);
repositoryConfigs.repositories.add(targetModel.name);`,
path: `${cliPath}/generators/discover/index.js`
},
assignConfigsToArtifactInfo: {
searchString: `this.artifactInfo.type = 'Models';`,
replacement: `this.artifactInfo.relationConfigs = relations; this.artifactInfo.repositoryConfigs = repositoryConfigs;this.artifactInfo.type = 'Models';`,
path: `${cliPath}/generators/discover/index.js`
},
executeRepoAndRelationGenerator: {
searchString: `// User Output`,
replacement: `
if (
this.artifactInfo.repositoryConfigs &&
this.artifactInfo.repositoryConfigs.repositories.size
) {
const { repositories, datasource, repositoryBaseClass } = this.artifactInfo.repositoryConfigs;
for (let index = 0; index < [...repositories].length; index++) {
const model = [...repositories][index];
const config = { repositoryBaseClass, datasource, model, name: model }
try {
const { execSync } = require('child_process');
const cmd = "lb4 repository --config='" + JSON.stringify(config) + "' --yes";
execSync(cmd, {
cwd: process.cwd(),
stdio: ['ignore', 'pipe', 'pipe'],
encoding: 'utf8'
});
} catch (error) {
console.log(error);
}
}
} else {
debug('No repository configurations found, skipping repository generation');
}
if (
this.artifactInfo.relationConfigs &&
this.artifactInfo.relationConfigs.length
) {
for (const configs of this.artifactInfo.relationConfigs) {
try {
const { execSync } = require('child_process');
const cmd = "lb4 relation --config='" + JSON.stringify(configs) + "' --yes";
execSync(cmd, {
cwd: process.cwd(),
stdio: ['ignore', 'pipe', 'pipe'],
encoding: 'utf8'
});
} catch (error) {
console.log(error);
}
}
} else {
debug('No relation configurations found, skipping relation generation');
}
`,
path: `${cliPath}/lib/artifact-generator.js`
},
}
};
(async () => {
applyPatches(patches);
applyPrePatches();
await removeCodeSection(`${cliPath}/generators/discover/index.js`, 'Object.assign(templateData.properties[relation.foreignKey], {', 'foreignKey: relation.foreignKey,', 1);
})();
export const globalPatches = async () => {
applyPatches(patches);
applyPrePatches();
await removeCodeSection(`${cliPath}/generators/discover/index.js`, 'Object.assign(templateData.properties[relation.foreignKey], {', 'foreignKey: relation.foreignKey,', 1);
};