Pull request: beta client squashed
Merge in DNS/adguard-home from beta-client-2 to master
Squashed commit of the following:
commit b2640cc49a6c5484d730b534dcf5a8013d7fa478
Merge: 659def862 aef4659e9
Author: Eugene Burkov <e.burkov@adguard.com>
Date: Tue Dec 29 19:23:09 2020 +0300
Merge branch 'master' into beta-client-2
commit 659def8626467949c35b7a6a0c99ffafb07b4385
Author: Eugene Burkov <e.burkov@adguard.com>
Date: Tue Dec 29 17:25:14 2020 +0300
all: upgrade github actions node version
commit b4b8cf8dd75672e9155da5d111ac66e8f5ba1535
Author: Vladislav Abdulmyanov <v.abdulmyanov@adguard.com>
Date: Tue Dec 29 16:57:14 2020 +0300
all: beta client squashed
This commit is contained in:
317
client2/scripts/generator/src/generateApis.ts
Normal file
317
client2/scripts/generator/src/generateApis.ts
Normal file
@@ -0,0 +1,317 @@
|
||||
/* eslint-disable no-template-curly-in-string */
|
||||
/* eslint-disable @typescript-eslint/no-unused-expressions */
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import { stringify } from 'qs';
|
||||
// eslint-disable-next-line import/no-extraneous-dependencies
|
||||
import * as morph from 'ts-morph';
|
||||
|
||||
import {
|
||||
API_DIR as API_DIR_CONST,
|
||||
GENERATOR_ENTITY_ALLIAS,
|
||||
} from '../../consts';
|
||||
import { toCamel, capitalize, schemaParamParser } from './utils';
|
||||
|
||||
|
||||
const API_DIR = path.resolve(API_DIR_CONST);
|
||||
if (!fs.existsSync(API_DIR)) {
|
||||
fs.mkdirSync(API_DIR);
|
||||
}
|
||||
|
||||
const { Project, QuoteKind } = morph;
|
||||
|
||||
|
||||
class ApiGenerator {
|
||||
project = new Project({
|
||||
tsConfigFilePath: './tsconfig.json',
|
||||
addFilesFromTsConfig: false,
|
||||
manipulationSettings: {
|
||||
quoteKind: QuoteKind.Single,
|
||||
usePrefixAndSuffixTextForRename: false,
|
||||
useTrailingCommas: true,
|
||||
},
|
||||
});
|
||||
|
||||
openapi: Record<string, any>;
|
||||
|
||||
serverUrl: string;
|
||||
|
||||
paths: any;
|
||||
|
||||
/* interface Controllers {
|
||||
[controller: string]: {
|
||||
[operationId: string]: { parameters - from opneApi, responses - from opneApi, method }
|
||||
}
|
||||
} */
|
||||
controllers: Record<string, any> = {};
|
||||
|
||||
apis: morph.SourceFile[] = [];
|
||||
|
||||
constructor(openapi: Record<string, any>) {
|
||||
this.openapi = openapi;
|
||||
this.paths = openapi.paths;
|
||||
this.serverUrl = openapi.servers[0].url;
|
||||
|
||||
Object.keys(this.paths).forEach((pathKey) => {
|
||||
Object.keys(this.paths[pathKey]).forEach((method) => {
|
||||
const {
|
||||
tags, operationId, parameters, responses, requestBody, security,
|
||||
} = this.paths[pathKey][method];
|
||||
const controller = toCamel((tags ? tags[0] : pathKey.split('/')[1]).replace('-controller', ''));
|
||||
|
||||
if (this.controllers[controller]) {
|
||||
this.controllers[controller][operationId] = {
|
||||
parameters,
|
||||
responses,
|
||||
method,
|
||||
requestBody,
|
||||
security,
|
||||
pathKey: pathKey.replace(/{/g, '${'),
|
||||
};
|
||||
} else {
|
||||
this.controllers[controller] = { [operationId]: {
|
||||
parameters,
|
||||
responses,
|
||||
method,
|
||||
requestBody,
|
||||
security,
|
||||
pathKey: pathKey.replace(/{/g, '${'),
|
||||
} };
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
this.generateApiFiles();
|
||||
}
|
||||
|
||||
generateApiFiles = () => {
|
||||
Object.keys(this.controllers).forEach(this.generateApiFile);
|
||||
};
|
||||
|
||||
generateApiFile = (cName: string) => {
|
||||
const apiFile = this.project.createSourceFile(`${API_DIR}/${cName}.ts`);
|
||||
apiFile.addStatements([
|
||||
'// This file was autogenerated. Please do not change.',
|
||||
'// All changes will be overwrited on commit.',
|
||||
'',
|
||||
]);
|
||||
|
||||
// const schemaProperties = schemas[schemaName].properties;
|
||||
const importEntities: any[] = [];
|
||||
|
||||
// add api class to file
|
||||
const apiClass = apiFile.addClass({
|
||||
name: `${capitalize(cName)}Api`,
|
||||
isDefaultExport: true,
|
||||
});
|
||||
|
||||
// get operations of controller
|
||||
const controllerOperations = this.controllers[cName];
|
||||
const operationList = Object.keys(controllerOperations).sort();
|
||||
// for each operation add fetcher
|
||||
operationList.forEach((operation) => {
|
||||
const {
|
||||
requestBody, responses, parameters, method, pathKey, security,
|
||||
} = controllerOperations[operation];
|
||||
|
||||
const queryParams: any[] = []; // { name, type }
|
||||
const bodyParam: any[] = []; // { name, type }
|
||||
|
||||
let hasResponseBodyType: /* boolean | ReturnType<schemaParamParser> */ false | [string, boolean, boolean, boolean, boolean] = false;
|
||||
let contentType = '';
|
||||
if (parameters) {
|
||||
parameters.forEach((p: any) => {
|
||||
const [
|
||||
pType, isArray, isClass, isImport,
|
||||
] = schemaParamParser(p.schema, this.openapi);
|
||||
|
||||
if (isImport) {
|
||||
importEntities.push({ type: pType, isClass });
|
||||
}
|
||||
if (p.in === 'query') {
|
||||
queryParams.push({
|
||||
name: p.name, type: `${pType}${isArray ? '[]' : ''}`, hasQuestionToken: !p.required });
|
||||
}
|
||||
});
|
||||
}
|
||||
if (queryParams.length > 0) {
|
||||
const imp = apiFile.getImportDeclaration((i) => {
|
||||
return i.getModuleSpecifierValue() === 'qs';
|
||||
}); if (!imp) {
|
||||
apiFile.addImportDeclaration({
|
||||
moduleSpecifier: 'qs',
|
||||
defaultImport: 'qs',
|
||||
});
|
||||
}
|
||||
}
|
||||
if (requestBody) {
|
||||
let content = requestBody.content;
|
||||
const { $ref }: { $ref: string } = requestBody;
|
||||
|
||||
if (!content && $ref) {
|
||||
const name = $ref.split('/').pop() as string;
|
||||
content = this.openapi.components.requestBodies[name].content;
|
||||
}
|
||||
|
||||
[contentType] = Object.keys(content);
|
||||
const data = content[contentType];
|
||||
|
||||
const [
|
||||
pType, isArray, isClass, isImport,
|
||||
] = schemaParamParser(data.schema, this.openapi);
|
||||
|
||||
if (isImport) {
|
||||
importEntities.push({ type: pType, isClass });
|
||||
bodyParam.push({ name: pType.toLowerCase(), type: `${isClass ? 'I' : ''}${pType}${isArray ? '[]' : ''}`, isClass, pType });
|
||||
} else {
|
||||
bodyParam.push({ name: 'data', type: `${pType}${isArray ? '[]' : ''}` });
|
||||
|
||||
}
|
||||
}
|
||||
if (responses['200']) {
|
||||
const { content, headers } = responses['200'];
|
||||
if (content && (content['*/*'] || content['application/json'])) {
|
||||
const { schema, examples } = content['*/*'] || content['application/json'];
|
||||
|
||||
if (!schema) {
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
const propType = schemaParamParser(schema, this.openapi);
|
||||
const [pType, , isClass, isImport] = propType;
|
||||
|
||||
if (isImport) {
|
||||
importEntities.push({ type: pType, isClass });
|
||||
}
|
||||
hasResponseBodyType = propType;
|
||||
}
|
||||
}
|
||||
let returnType = '';
|
||||
if (hasResponseBodyType) {
|
||||
const [pType, isArray, isClass] = hasResponseBodyType as any;
|
||||
let data = `Promise<${isClass ? 'I' : ''}${pType}${isArray ? '[]' : ''}`;
|
||||
returnType = data;
|
||||
} else {
|
||||
returnType = 'Promise<number';
|
||||
}
|
||||
const shouldValidate = bodyParam.filter(b => b.isClass);
|
||||
if (shouldValidate.length > 0) {
|
||||
returnType += ' | string[]';
|
||||
}
|
||||
// append Error to default type return;
|
||||
returnType += ' | Error>';
|
||||
|
||||
const fetcher = apiClass.addMethod({
|
||||
isAsync: true,
|
||||
isStatic: true,
|
||||
name: operation,
|
||||
returnType,
|
||||
});
|
||||
const params = [...queryParams, ...bodyParam].sort((a, b) => (Number(!!a.hasQuestionToken) - Number(!!b.hasQuestionToken)));
|
||||
fetcher.addParameters(params);
|
||||
|
||||
fetcher.setBodyText((w) => {
|
||||
// Add data to URLSearchParams
|
||||
if (contentType === 'text/plain') {
|
||||
bodyParam.forEach((b) => {
|
||||
w.writeLine(`const params = String(${b.name});`);
|
||||
});
|
||||
} else {
|
||||
if (shouldValidate.length > 0) {
|
||||
w.writeLine(`const haveError: string[] = [];`);
|
||||
shouldValidate.forEach((b) => {
|
||||
w.writeLine(`const ${b.name}Valid = new ${b.pType}(${b.name});`);
|
||||
w.writeLine(`haveError.push(...${b.name}Valid.validate());`);
|
||||
});
|
||||
w.writeLine(`if (haveError.length > 0) {`);
|
||||
w.writeLine(` return Promise.resolve(haveError);`)
|
||||
w.writeLine(`}`);
|
||||
}
|
||||
}
|
||||
// Switch return of fetch in case on queryParams
|
||||
if (queryParams.length > 0) {
|
||||
w.writeLine('const queryParams = {');
|
||||
queryParams.forEach((q) => {
|
||||
w.writeLine(` ${q.name}: ${q.name},`);
|
||||
});
|
||||
w.writeLine('}');
|
||||
w.writeLine(`return await fetch(\`${this.serverUrl}${pathKey}?\${qs.stringify(queryParams, { arrayFormat: 'comma' })}\`, {`);
|
||||
} else {
|
||||
w.writeLine(`return await fetch(\`${this.serverUrl}${pathKey}\`, {`);
|
||||
}
|
||||
// Add method
|
||||
w.writeLine(` method: '${method.toUpperCase()}',`);
|
||||
|
||||
// add Fetch options
|
||||
if (contentType && contentType !== 'multipart/form-data') {
|
||||
w.writeLine(' headers: {');
|
||||
w.writeLine(` 'Content-Type': '${contentType}',`);
|
||||
w.writeLine(' },');
|
||||
}
|
||||
if (contentType) {
|
||||
switch (contentType) {
|
||||
case 'text/plain':
|
||||
w.writeLine(' body: params,');
|
||||
break;
|
||||
default:
|
||||
w.writeLine(` body: JSON.stringify(${bodyParam.map((b) => b.isClass ? `${b.name}Valid.serialize()` : b.name).join(', ')}),`);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Handle response
|
||||
if (hasResponseBodyType) {
|
||||
w.writeLine('}).then(async (res) => {');
|
||||
w.writeLine(' if (res.status === 200) {');
|
||||
w.writeLine(' return res.json();');
|
||||
} else {
|
||||
w.writeLine('}).then(async (res) => {');
|
||||
w.writeLine(' if (res.status === 200) {');
|
||||
w.writeLine(' return res.status;');
|
||||
}
|
||||
|
||||
// Handle Error
|
||||
w.writeLine(' } else {');
|
||||
w.writeLine(' return new Error(String(res.status));');
|
||||
w.writeLine(' }');
|
||||
w.writeLine('})');
|
||||
});
|
||||
});
|
||||
|
||||
const imports: any[] = [];
|
||||
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) {
|
||||
apiFile.addImportDeclaration({
|
||||
moduleSpecifier: `${GENERATOR_ENTITY_ALLIAS}${pType}`,
|
||||
defaultImport: pType,
|
||||
namedImports: [`I${pType}`],
|
||||
});
|
||||
} else {
|
||||
apiFile.addImportDeclaration({
|
||||
moduleSpecifier: `${GENERATOR_ENTITY_ALLIAS}${pType}`,
|
||||
namedImports: [pType],
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
this.apis.push(apiFile);
|
||||
};
|
||||
|
||||
save = () => {
|
||||
this.apis.forEach(async (e) => {
|
||||
await e.saveSync();
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
export default ApiGenerator;
|
||||
519
client2/scripts/generator/src/generateEntities.ts
Normal file
519
client2/scripts/generator/src/generateEntities.ts
Normal file
@@ -0,0 +1,519 @@
|
||||
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 } 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<string, any>;
|
||||
|
||||
schemas: Record<string, any>;
|
||||
|
||||
schemaNames: string[];
|
||||
|
||||
entities: morph.SourceFile[] = [];
|
||||
|
||||
constructor(openapi: Record<string, any>) {
|
||||
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 } = this.schemas[sName];
|
||||
|
||||
const importEntities: { type: string, isClass: boolean }[] = [];
|
||||
const entityInterface = entityFile.addInterface({
|
||||
name: `I${sName}`,
|
||||
isExported: true,
|
||||
});
|
||||
const sortedSProps = Object.keys(sProps || {}).sort();
|
||||
// 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
|
||||
),
|
||||
});
|
||||
});
|
||||
|
||||
// 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<boolean>((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<boolean>((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<boolean>((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(';');
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// add constructor;
|
||||
const ctor = entityClass.addConstructor({
|
||||
parameters: [{
|
||||
name: 'props',
|
||||
type: `I${sName}`,
|
||||
}],
|
||||
});
|
||||
ctor.setBodyText((w) => {
|
||||
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) => {
|
||||
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<Record<string, any>>((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) => {
|
||||
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: `Partial<I${sName}>`,
|
||||
});
|
||||
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;
|
||||
74
client2/scripts/generator/src/utils.ts
Normal file
74
client2/scripts/generator/src/utils.ts
Normal file
@@ -0,0 +1,74 @@
|
||||
const toCamel = (s: string) => {
|
||||
return s.replace(/([-_][a-z])/ig, ($1) => {
|
||||
return $1.toUpperCase()
|
||||
.replace('-', '')
|
||||
.replace('_', '');
|
||||
});
|
||||
};
|
||||
const capitalize = (s: string) => {
|
||||
return s[0].toUpperCase() + s.slice(1);
|
||||
};
|
||||
const TYPES = {
|
||||
integer: 'number',
|
||||
float: 'number',
|
||||
number: 'number',
|
||||
string: 'string',
|
||||
boolean: 'boolean',
|
||||
};
|
||||
|
||||
/**
|
||||
* @param schemaProp: valueof shema.properties[key]
|
||||
* @param openApi: openapi object
|
||||
* @returns [propType - basicType or import one, isArray, isClass, isImport]
|
||||
*/
|
||||
const schemaParamParser = (schemaProp: any, openApi: any): [string, boolean, boolean, boolean, boolean] => {
|
||||
let type = '';
|
||||
let isImport = false;
|
||||
let isClass = false;
|
||||
let isArray = false;
|
||||
let isAdditional = false;
|
||||
|
||||
if (schemaProp.$ref || schemaProp.additionalProperties?.$ref) {
|
||||
const temp = (schemaProp.$ref || schemaProp.additionalProperties?.$ref).split('/');
|
||||
|
||||
if (schemaProp.additionalProperties) {
|
||||
isAdditional = true;
|
||||
}
|
||||
|
||||
type = `${temp[temp.length - 1]}`;
|
||||
|
||||
const cl = openApi ? openApi.components.schemas[temp[temp.length - 1]] : {};
|
||||
|
||||
if (cl.type === 'string' && cl.enum) {
|
||||
isImport = true;
|
||||
}
|
||||
|
||||
if (cl.type === 'object' && !cl.oneOf) {
|
||||
isClass = true;
|
||||
isImport = true;
|
||||
} else if (cl.type === 'array') {
|
||||
const temp: any = schemaParamParser(cl.items, openApi);
|
||||
type = `${temp[0]}`;
|
||||
isArray = true;
|
||||
isClass = isClass || temp[2];
|
||||
isImport = isImport || temp[3];
|
||||
}
|
||||
} else if (schemaProp.type === 'array') {
|
||||
const temp: any = schemaParamParser(schemaProp.items, openApi);
|
||||
type = `${temp[0]}`;
|
||||
isArray = true;
|
||||
isClass = isClass || temp[2];
|
||||
isImport = isImport || temp[3];
|
||||
} else {
|
||||
type = (TYPES as Record<any, string>)[schemaProp.type];
|
||||
}
|
||||
if (!type) {
|
||||
// TODO: Fix bug with Error fields.
|
||||
type = 'any';
|
||||
// throw new Error('Failed to find entity type');
|
||||
}
|
||||
|
||||
return [type, isArray, isClass, isImport, isAdditional];
|
||||
};
|
||||
|
||||
export { TYPES, toCamel, capitalize, schemaParamParser };
|
||||
Reference in New Issue
Block a user