import * as fs from 'fs'; import * as path from 'path'; // eslint-disable-next-line import/no-extraneous-dependencies import * as morph from 'ts-morph'; import { ENT_DIR } from '../../consts'; import { TYPES, toCamel, schemaParamParser, uncapitalize } from './utils'; const { Project, QuoteKind } = morph; const EntDir = path.resolve(ENT_DIR); if (!fs.existsSync(EntDir)) { fs.mkdirSync(EntDir); } class EntitiesGenerator { project = new Project({ tsConfigFilePath: './tsconfig.json', addFilesFromTsConfig: false, manipulationSettings: { quoteKind: QuoteKind.Single, usePrefixAndSuffixTextForRename: false, useTrailingCommas: true, }, }); openapi: Record; schemas: Record; schemaNames: string[]; entities: morph.SourceFile[] = []; constructor(openapi: Record) { this.openapi = openapi; this.schemas = openapi.components.schemas; this.schemaNames = Object.keys(this.schemas); this.generateEntities(); } generateEntities = () => { this.schemaNames.forEach(this.generateEntity); }; generateEntity = (sName: string) => { const { properties, type, oneOf } = this.schemas[sName]; const notAClass = !properties && TYPES[type as keyof typeof TYPES]; if (oneOf) { this.generateOneOf(sName); return; } if (notAClass) { this.generateEnum(sName); } else { this.generateClass(sName); } }; generateEnum = (sName: string) => { const entityFile = this.project.createSourceFile(`${EntDir}/${sName}.ts`); entityFile.addStatements([ '// This file was autogenerated. Please do not change.', '// All changes will be overwrited on commit.', '', ]); const { enum: enumMembers } = this.schemas[sName]; entityFile.addEnum({ name: sName, members: enumMembers.map((e: string) => ({ name: e.toUpperCase(), value: e })), isExported: true, }); this.entities.push(entityFile); }; generateOneOf = (sName: string) => { const entityFile = this.project.createSourceFile(`${EntDir}/${sName}.ts`); entityFile.addStatements([ '// This file was autogenerated. Please do not change.', '// All changes will be overwrited on commit.', '', ]); const importEntities: { type: string, isClass: boolean }[] = []; const entities = this.schemas[sName].oneOf.map((elem: any) => { const [ pType, isArray, isClass, isImport, ] = schemaParamParser(elem, this.openapi); importEntities.push({ type: pType, isClass }); return { type: pType, isArray }; }); entityFile.addTypeAlias({ name: sName, isExported: true, type: entities.map((e: any) => e.isArray ? `I${e.type}[]` : `I${e.type}`).join(' | '), }) // add import importEntities.sort((a, b) => a.type > b.type ? 1 : -1).forEach((ie) => { const { type: pType, isClass } = ie; if (isClass) { entityFile.addImportDeclaration({ moduleSpecifier: `./${pType}`, namedImports: [`I${pType}`], }); } else { entityFile.addImportDeclaration({ moduleSpecifier: `./${pType}`, namedImports: [pType], }); } }); this.entities.push(entityFile); } generateClass = (sName: string) => { const entityFile = this.project.createSourceFile(`${EntDir}/${sName}.ts`); entityFile.addStatements([ '// This file was autogenerated. Please do not change.', '// All changes will be overwrited on commit.', '', ]); const { properties: sProps, required, $ref, additionalProperties } = this.schemas[sName]; if ($ref) { const temp = $ref.split('/'); const importSchemaName = `${temp[temp.length - 1]}`; entityFile.addImportDeclaration({ defaultImport: importSchemaName, moduleSpecifier: `./${importSchemaName}`, namedImports: [`I${importSchemaName}`], }); entityFile.addTypeAlias({ name: `I${sName}`, type: `I${importSchemaName}`, isExported: true, }) entityFile.addStatements(`export default ${importSchemaName};`); this.entities.push(entityFile); return; } const importEntities: { type: string, isClass: boolean }[] = []; const entityInterface = entityFile.addInterface({ name: `I${sName}`, isExported: true, }); const sortedSProps = Object.keys(sProps || {}).sort(); const additionalPropsOnly = additionalProperties && sortedSProps.length === 0; // add server response interface to entityFile sortedSProps.forEach((sPropName) => { const [ pType, isArray, isClass, isImport, isAdditional ] = schemaParamParser(sProps[sPropName], this.openapi); if (isImport) { importEntities.push({ type: pType, isClass }); } const propertyType = isAdditional ? `{ [key: string]: ${isClass ? 'I' : ''}${pType}${isArray ? '[]' : ''} }` : `${isClass ? 'I' : ''}${pType}${isArray ? '[]' : ''}`; entityInterface.addProperty({ name: sPropName, type: propertyType, hasQuestionToken: !( (required && required.includes(sPropName)) || sProps[sPropName].required ), }); }); if (additionalProperties) { const [ pType, isArray, isClass, isImport, isAdditional ] = schemaParamParser(additionalProperties, this.openapi); if (isImport) { importEntities.push({ type: pType, isClass }); } const type = isAdditional ? `{ [key: string]: ${isClass ? 'I' : ''}${pType}${isArray ? '[]' : ''} }` : `${isClass ? 'I' : ''}${pType}${isArray ? '[]' : ''}`; entityInterface.addIndexSignature({ keyName: 'key', keyType: 'string', returnType: additionalPropsOnly ? type : `${type} | undefined`, }); } // add import const imports: { type: string, isClass: boolean }[] = []; const types: string[] = []; importEntities.forEach((i) => { const { type } = i; if (!types.includes(type)) { imports.push(i); types.push(type); } }); imports.sort((a, b) => a.type > b.type ? 1 : -1).forEach((ie) => { const { type: pType, isClass } = ie; if (isClass) { entityFile.addImportDeclaration({ defaultImport: pType, moduleSpecifier: `./${pType}`, namedImports: [`I${pType}`], }); } else { entityFile.addImportDeclaration({ moduleSpecifier: `./${pType}`, namedImports: [pType], }); } }); const entityClass = entityFile.addClass({ name: sName, isDefaultExport: true, }); // addProperties to class; sortedSProps.forEach((sPropName) => { const [pType, isArray, isClass, isImport, isAdditional] = schemaParamParser(sProps[sPropName], this.openapi); const isRequred = (required && required.includes(sPropName)) || sProps[sPropName].required; const propertyType = isAdditional ? `{ [key: string]: ${pType}${isArray ? '[]' : ''}${isRequred ? '' : ' | undefined'} }` : `${pType}${isArray ? '[]' : ''}${isRequred ? '' : ' | undefined'}`; entityClass.addProperty({ name: `_${sPropName}`, isReadonly: true, type: propertyType, }); const getter = entityClass.addGetAccessor({ name: toCamel(sPropName), returnType: propertyType, statements: [`return this._${sPropName};`], }); const { description, example, minItems, maxItems, maxLength, minLength, maximum, minimum } = sProps[sPropName]; if (description || example) { getter.addJsDoc(`${example ? `Description: ${description}` : ''}${example ? `\nExample: ${example}` : ''}`); } if (minItems) { entityClass.addGetAccessor({ isStatic: true, name: `${toCamel(sPropName)}MinItems`, statements: [`return ${minItems};`], }); } if (maxItems) { entityClass.addGetAccessor({ isStatic: true, name: `${toCamel(sPropName)}MaxItems`, statements: [`return ${maxItems};`], }); } if (typeof minLength === 'number') { entityClass.addGetAccessor({ isStatic: true, name: `${toCamel(sPropName)}MinLength`, statements: [`return ${minLength};`], }); } if (maxLength) { entityClass.addGetAccessor({ isStatic: true, name: `${toCamel(sPropName)}MaxLength`, statements: [`return ${maxLength};`], }); } if (typeof minimum === 'number') { entityClass.addGetAccessor({ isStatic: true, name: `${toCamel(sPropName)}MinValue`, statements: [`return ${minimum};`], }); } if (maximum) { entityClass.addGetAccessor({ isStatic: true, name: `${toCamel(sPropName)}MaxValue`, statements: [`return ${maximum};`], }); } if (!(isArray && isClass) && !isClass) { const isEnum = !isClass && isImport; const isRequired = (required && required.includes(sPropName)) || sProps[sPropName].required; const { maxLength, minLength, maximum, minimum } = sProps[sPropName]; const haveValidationFields = maxLength || typeof minLength === 'number' || maximum || typeof minimum === 'number'; if (isRequired || haveValidationFields) { const prop = toCamel(sPropName); const validateField = entityClass.addMethod({ isStatic: true, name: `${prop}Validate`, returnType: `boolean`, parameters: [{ name: prop, type: `${pType}${isArray ? '[]' : ''}${isRequred ? '' : ' | undefined'}`, }], }) validateField.setBodyText((w) => { w.write('return '); const nonRequiredCall = isRequired ? prop : `!${prop} ? true : ${prop}`; if (pType === 'string') { if (isArray) { w.write(`${nonRequiredCall}.reduce((result, p) => result && (typeof p === 'string' && !!p.trim()), true)`); } else { if (typeof minLength === 'number' && maxLength) { w.write(`(${nonRequiredCall}.length >${minLength > 0 ? '=' : ''} ${minLength}) && (${nonRequiredCall}.length <= ${maxLength})`); } if (typeof minLength !== 'number' || !maxLength) { w.write(`${isRequired ? `typeof ${prop} === 'string'` : `!${prop} ? true : typeof ${prop} === 'string'`} && !!${nonRequiredCall}.trim()`); } } } else if (pType === 'number') { if (isArray) { w.write(`${nonRequiredCall}.reduce((result, p) => result && typeof p === 'number', true)`); } else { if (typeof minimum === 'number' && maximum) { w.write(`${isRequired ? `${prop} >= ${minimum} && ${prop} <= ${maximum}` : `!${prop} ? true : ((${prop} >= ${minimum}) && (${prop} <= ${maximum}))`}`); } if (typeof minimum !== 'number' || !maximum) { w.write(`${isRequired ? `typeof ${prop} === 'number'` : `!${prop} ? true : typeof ${prop} === 'number'`}`); } } } else if (pType === 'boolean') { w.write(`${isRequired ? `typeof ${prop} === 'boolean'` : `!${prop} ? true : typeof ${prop} === 'boolean'`}`); } else if (isEnum) { if (isArray){ w.write(`${nonRequiredCall}.reduce((result, p) => result && Object.keys(${pType}).includes(${prop}), true)`); } else { w.write(`${isRequired ? `Object.keys(${pType}).includes(${prop})` : `!${prop} ? true : typeof ${prop} === 'boolean'`}`); } } w.write(';'); }); } } }); if (additionalProperties) { const [ pType, isArray, isClass, isImport, isAdditional ] = schemaParamParser(additionalProperties, this.openapi); const type = `Record`; entityClass.addProperty({ name: additionalPropsOnly ? 'data' : `${uncapitalize(pType)}Data`, isReadonly: true, type: type, }); } // add constructor; const ctor = entityClass.addConstructor({ parameters: [{ name: 'props', type: `I${sName}`, }], }); ctor.setBodyText((w) => { if (additionalProperties) { const [ pType, isArray, isClass, isImport, isAdditional ] = schemaParamParser(additionalProperties, this.openapi); w.writeLine(`this.${additionalPropsOnly ? 'data' : `${uncapitalize(pType)}Data`} = Object.entries(props).reduce>((prev, [key, value]) => {`); if (isClass) { w.writeLine(` prev[key] = new ${pType}(value!);`); } else { w.writeLine(' prev[key] = value!;') } w.writeLine(' return prev;'); w.writeLine('}, {})'); return; } sortedSProps.forEach((sPropName) => { const [ pType, isArray, isClass, , isAdditional ] = schemaParamParser(sProps[sPropName], this.openapi); const req = (required && required.includes(sPropName)) || sProps[sPropName].required; if (!req) { if ((pType === 'boolean' || pType === 'number' || pType ==='string') && !isClass && !isArray) { w.writeLine(`if (typeof props.${sPropName} === '${pType}') {`); } else { w.writeLine(`if (props.${sPropName}) {`); } } if (isAdditional) { if (isArray && isClass) { w.writeLine(`${!req ? ' ' : ''}this._${sPropName} = props.${sPropName}.map((p) => Object.keys(p).reduce((prev, key) => { return { ...prev, [key]: new ${pType}(p[key])}; },{}))`); } else if (isClass) { w.writeLine(`${!req ? ' ' : ''}this._${sPropName} = Object.keys(props.${sPropName}).reduce((prev, key) => { return { ...prev, [key]: new ${pType}(props.${sPropName}[key])}; },{})`); } else { if (pType === 'string' && !isArray) { w.writeLine(`${!req ? ' ' : ''}this._${sPropName} = Object.keys(props.${sPropName}).reduce((prev, key) => { return { ...prev, [key]: props.${sPropName}[key].trim()}; },{})`); } else { w.writeLine(`${!req ? ' ' : ''}this._${sPropName} = Object.keys(props.${sPropName}).reduce((prev, key) => { return { ...prev, [key]: props.${sPropName}[key]}; },{})`); } } } else { if (isArray && isClass) { w.writeLine(`${!req ? ' ' : ''}this._${sPropName} = props.${sPropName}.map((p) => new ${pType}(p));`); } else if (isClass) { w.writeLine(`${!req ? ' ' : ''}this._${sPropName} = new ${pType}(props.${sPropName});`); } else { if (pType === 'string' && !isArray) { w.writeLine(`${!req ? ' ' : ''}this._${sPropName} = props.${sPropName}.trim();`); } else { w.writeLine(`${!req ? ' ' : ''}this._${sPropName} = props.${sPropName};`); } } } if (!req) { w.writeLine('}'); } }); }); // add serialize method; const serialize = entityClass.addMethod({ isStatic: false, name: 'serialize', returnType: `I${sName}`, }); serialize.setBodyText((w) => { if (additionalProperties) { const [ pType, isArray, isClass, isImport, isAdditional ] = schemaParamParser(additionalProperties, this.openapi); w.writeLine(`return Object.entries(this.${additionalPropsOnly ? 'data' : `${uncapitalize(pType)}Data`}).reduce>((prev, [key, value]) => {`); if (isClass) { w.writeLine(` prev[key] = value.serialize();`); } else { w.writeLine(' prev[key] = value;') } w.writeLine(' return prev;'); w.writeLine('}, {})'); return; } w.writeLine(`const data: I${sName} = {`); const unReqFields: string[] = []; sortedSProps.forEach((sPropName) => { const req = (required && required.includes(sPropName)) || sProps[sPropName].required; const [, isArray, isClass, , isAdditional] = schemaParamParser(sProps[sPropName], this.openapi); if (!req) { unReqFields.push(sPropName); return; } if (isAdditional) { if (isArray && isClass) { w.writeLine(` ${sPropName}: this._${sPropName}.map((p) => Object.keys(p).reduce((prev, key) => ({ ...prev, [key]: p[key].serialize() }))),`); } else if (isClass) { w.writeLine(` ${sPropName}: Object.keys(this._${sPropName}).reduce>((prev, key) => ({ ...prev, [key]: this._${sPropName}[key].serialize() }), {}),`); } else { w.writeLine(` ${sPropName}: Object.keys(this._${sPropName}).reduce((prev, key) => ({ ...prev, [key]: this._${sPropName}[key] })),`); } } else { if (isArray && isClass) { w.writeLine(` ${sPropName}: this._${sPropName}.map((p) => p.serialize()),`); } else if (isClass) { w.writeLine(` ${sPropName}: this._${sPropName}.serialize(),`); } else { w.writeLine(` ${sPropName}: this._${sPropName},`); } } }); w.writeLine('};'); unReqFields.forEach((sPropName) => { const [, isArray, isClass, , isAdditional] = schemaParamParser(sProps[sPropName], this.openapi); w.writeLine(`if (typeof this._${sPropName} !== 'undefined') {`); if (isAdditional) { if (isArray && isClass) { w.writeLine(` data.${sPropName} = this._${sPropName}.map((p) => Object.keys(p).reduce((prev, key) => ({ ...prev, [key]: p[key].serialize() }), {}));`); } else if (isClass) { w.writeLine(` data.${sPropName} = Object.keys(this._${sPropName}).reduce((prev, key) => ({ ...prev, [key]: this._${sPropName}[key].serialize() }), {});`); } else { w.writeLine(` data.${sPropName} = Object.keys(this._${sPropName}).reduce((prev, key) => ({ ...prev, [key]: this._${sPropName}[key] }), {});`); } } else { if (isArray && isClass) { w.writeLine(` data.${sPropName} = this._${sPropName}.map((p) => p.serialize());`); } else if (isClass) { w.writeLine(` data.${sPropName} = this._${sPropName}.serialize();`); } else { w.writeLine(` data.${sPropName} = this._${sPropName};`); } } w.writeLine(`}`); }); w.writeLine('return data;'); }); // add validate method const validate = entityClass.addMethod({ isStatic: false, name: 'validate', returnType: `string[]`, }) validate.setBodyText((w) => { if (additionalPropsOnly) { w.writeLine('return []') return; } w.writeLine('const validate = {'); Object.keys(sProps || {}).forEach((sPropName) => { const [pType, isArray, isClass, , isAdditional] = schemaParamParser(sProps[sPropName], this.openapi); const { maxLength, minLength, maximum, minimum } = sProps[sPropName]; const isRequired = (required && required.includes(sPropName)) || sProps[sPropName].required; const nonRequiredCall = isRequired ? `this._${sPropName}` : `!this._${sPropName} ? true : this._${sPropName}`; if (isArray && isClass) { w.writeLine(` ${sPropName}: ${nonRequiredCall}.reduce((result, p) => result && p.validate().length === 0, true),`); } else if (isClass && !isAdditional) { w.writeLine(` ${sPropName}: ${nonRequiredCall}.validate().length === 0,`); } else { if (pType === 'string') { if (isArray) { w.writeLine(` ${sPropName}: ${nonRequiredCall}.reduce((result, p) => result && typeof p === 'string', true),`); } else { if (typeof minLength === 'number' && maxLength) { w.writeLine(` ${sPropName}: (${nonRequiredCall}.length >${minLength > 0 ? '=' : ''} ${minLength}) && (${nonRequiredCall}.length <= ${maxLength}),`); } if (typeof minLength !== 'number' || !maxLength) { w.writeLine(` ${sPropName}: ${isRequired ? `typeof this._${sPropName} === 'string'` : `!this._${sPropName} ? true : typeof this._${sPropName} === 'string'`} && !this._${sPropName} ? true : this._${sPropName},`); } } } else if (pType === 'number') { if (isArray) { w.writeLine(` ${sPropName}: ${nonRequiredCall}.reduce((result, p) => result && typeof p === 'number', true),`); } else { if (typeof minimum === 'number' && maximum) { w.writeLine(` ${sPropName}: ${isRequired ? `this._${sPropName} >= ${minimum} && this._${sPropName} <= ${maximum}` : `!this._${sPropName} ? true : ((this._${sPropName} >= ${minimum}) && (this._${sPropName} <= ${maximum}))`},`); } if (typeof minimum !== 'number' || !maximum) { w.writeLine(` ${sPropName}: ${isRequired ? `typeof this._${sPropName} === 'number'` : `!this._${sPropName} ? true : typeof this._${sPropName} === 'number'`},`); } } } else if (pType === 'boolean') { w.writeLine(` ${sPropName}: ${isRequired ? `typeof this._${sPropName} === 'boolean'` : `!this._${sPropName} ? true : typeof this._${sPropName} === 'boolean'`},`); } } }); w.writeLine('};'); w.writeLine('const isError: string[] = [];') w.writeLine('Object.keys(validate).forEach((key) => {'); w.writeLine(' if (!(validate as any)[key]) {'); w.writeLine(' isError.push(key);'); w.writeLine(' }'); w.writeLine('});'); w.writeLine('return isError;'); }); // add update method; const update = entityClass.addMethod({ isStatic: false, name: 'update', returnType: `${sName}`, }); update.addParameter({ name: 'props', type: additionalPropsOnly ? `I${sName}` : `Partial`, }); update.setBodyText((w) => { w.writeLine(`return new ${sName}({ ...this.serialize(), ...props });`); }); this.entities.push(entityFile); }; save = () => { this.entities.forEach(async (e) => { await e.saveSync(); }); }; } export default EntitiesGenerator;