From 2833d0c3a05a8a0f0083b10a36d2f5834b2e5a8a Mon Sep 17 00:00:00 2001 From: Ivan Tsai Date: Fri, 15 Jul 2022 17:54:03 +0800 Subject: [PATCH 1/4] feat(core): implement pagination of executing function --- packages/core/src/lib/template-engine/nunjucksCompiler.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/core/src/lib/template-engine/nunjucksCompiler.ts b/packages/core/src/lib/template-engine/nunjucksCompiler.ts index f307de7f..9a588403 100644 --- a/packages/core/src/lib/template-engine/nunjucksCompiler.ts +++ b/packages/core/src/lib/template-engine/nunjucksCompiler.ts @@ -20,6 +20,7 @@ import { walkAst, } from './extension-loader'; import { IDataQueryBuilder } from '../data-query'; +import { Pagination } from '@vulcan-sql/core/models'; @injectable() export class NunjucksCompiler implements Compiler { @@ -74,10 +75,12 @@ export class NunjucksCompiler implements Compiler { public async execute( templateName: string, - data: T + data: T, + pagination?: Pagination ): Promise { await this.initializeExtensions(); const builder = await this.renderAndGetMainBuilder(templateName, data); + if (pagination) builder.paginate(pagination); return builder.value(); } From 45b5d97abe1f7f2f31d18dc12752fe56b1fdd6bc Mon Sep 17 00:00:00 2001 From: Ivan Tsai Date: Mon, 18 Jul 2022 15:16:42 +0800 Subject: [PATCH 2/4] refactor(build): change schema parser middlewares to classes instead of pure function Change schema parser middlewares to classes instead of pure function for incoming injections --- .../src/containers/modules/schemaParser.ts | 6 ++ packages/build/src/containers/types.ts | 1 + packages/build/src/lib/schema-parser/index.ts | 2 +- .../middleware/addMissingErrors.ts | 15 ++--- .../middleware/addRequiredValidatorForPath.ts | 9 +-- .../middleware/checkParameter.ts | 15 ++--- .../middleware/checkValidator.ts | 28 ++++++-- .../middleware/fallbackErrors.ts | 9 +-- .../middleware/generateDataType.ts | 13 ++-- .../middleware/generatePathParameters.ts | 9 +-- .../middleware/generateTemplateSource.ts | 9 +-- .../schema-parser/middleware/generateUrl.ts | 9 +-- .../src/lib/schema-parser/middleware/index.ts | 46 +++++++++---- .../schema-parser/middleware/middleware.ts | 10 ++- .../middleware/normalizeDataType.ts | 13 ++-- .../middleware/normalizeFieldIn.ts | 9 +-- .../middleware/setConstraints.ts | 28 ++++++-- .../middleware/transformValidator.ts | 9 +-- .../src/lib/schema-parser/schemaParser.ts | 65 ++++++------------- .../middleware/addMissingErrors.spec.ts | 32 ++++----- .../addRequiredValidatorForPath.spec.ts | 12 +++- .../middleware/checkParameter.spec.ts | 34 ++++------ .../middleware/checkValidator.spec.ts | 11 ++-- .../middleware/fallbackError.spec.ts | 8 ++- .../middleware/generateDataType.spec.ts | 8 ++- .../middleware/generatePathParameters.spec.ts | 8 ++- .../middleware/generateTemplateSource.spec.ts | 8 ++- .../middleware/generateUrl.spec.ts | 14 ++-- .../middleware/normalizeDataType.spec.ts | 8 ++- .../middleware/normalizeInField.spec.ts | 5 +- .../middleware/setConstraints.spec.ts | 7 +- .../middleware/transformValidator.spec.ts | 14 ++-- packages/build/test/spec-generator/schema.ts | 37 ++++++----- 33 files changed, 289 insertions(+), 222 deletions(-) diff --git a/packages/build/src/containers/modules/schemaParser.ts b/packages/build/src/containers/modules/schemaParser.ts index 4946d3a0..faec3c8e 100644 --- a/packages/build/src/containers/modules/schemaParser.ts +++ b/packages/build/src/containers/modules/schemaParser.ts @@ -10,6 +10,7 @@ import { import { ContainerModule, interfaces } from 'inversify'; import { SchemaParserOptions } from '../../options/schemaParser'; import { TYPES } from '../types'; +import { SchemaParserMiddlewares } from '@vulcan-sql/build/schema-parser/middleware'; export const schemaParserModule = (options: ISchemaParserOptions) => new ContainerModule((bind) => { @@ -33,4 +34,9 @@ export const schemaParserModule = (options: ISchemaParserOptions) => // Schema parser bind(TYPES.SchemaParser).to(SchemaParser).inSingletonScope(); + + // Middleware + for (const middleware of SchemaParserMiddlewares) { + bind(TYPES.SchemaParserMiddleware).to(middleware); + } }); diff --git a/packages/build/src/containers/types.ts b/packages/build/src/containers/types.ts index b8ca77d3..b5e2a07f 100644 --- a/packages/build/src/containers/types.ts +++ b/packages/build/src/containers/types.ts @@ -4,4 +4,5 @@ export const TYPES = { SchemaReader: Symbol.for('SchemaReader'), Factory_SchemaReader: Symbol.for('Factory_SchemaReader'), SchemaParser: Symbol.for('SchemaParser'), + SchemaParserMiddleware: Symbol.for('SchemaParserMiddleware'), }; diff --git a/packages/build/src/lib/schema-parser/index.ts b/packages/build/src/lib/schema-parser/index.ts index 24cee285..0e46c3d8 100644 --- a/packages/build/src/lib/schema-parser/index.ts +++ b/packages/build/src/lib/schema-parser/index.ts @@ -1,3 +1,3 @@ export * from './schema-reader'; export * from './schemaParser'; -export { RawAPISchema, SchemaParserMiddleware } from './middleware'; +export * from './middleware'; diff --git a/packages/build/src/lib/schema-parser/middleware/addMissingErrors.ts b/packages/build/src/lib/schema-parser/middleware/addMissingErrors.ts index d733658c..dbf4f860 100644 --- a/packages/build/src/lib/schema-parser/middleware/addMissingErrors.ts +++ b/packages/build/src/lib/schema-parser/middleware/addMissingErrors.ts @@ -1,5 +1,5 @@ -import { AllTemplateMetadata, APISchema } from '@vulcan-sql/core'; -import { SchemaParserMiddleware } from './middleware'; +import { APISchema } from '@vulcan-sql/core'; +import { RawAPISchema, SchemaParserMiddleware } from './middleware'; interface ErrorCode { code: string; @@ -8,13 +8,11 @@ interface ErrorCode { } // Add error code to definition if it is used in query but not defined in schema -export const addMissingErrors = - (allMetadata: AllTemplateMetadata): SchemaParserMiddleware => - async (schemas, next) => { +export class AddMissingErrors extends SchemaParserMiddleware { + public async handle(schemas: RawAPISchema, next: () => Promise) { await next(); const transformedSchemas = schemas as APISchema; - const templateName = transformedSchemas.templateSource; - const metadata = allMetadata[templateName]; + const metadata = schemas.metadata; // Skip validation if no metadata found if (!metadata?.['error.vulcan.com']) return; @@ -27,4 +25,5 @@ export const addMissingErrors = }); } }); - }; + } +} diff --git a/packages/build/src/lib/schema-parser/middleware/addRequiredValidatorForPath.ts b/packages/build/src/lib/schema-parser/middleware/addRequiredValidatorForPath.ts index 69cfe4e5..964b5c07 100644 --- a/packages/build/src/lib/schema-parser/middleware/addRequiredValidatorForPath.ts +++ b/packages/build/src/lib/schema-parser/middleware/addRequiredValidatorForPath.ts @@ -1,9 +1,9 @@ import { FieldInType } from '@vulcan-sql/core'; -import { SchemaParserMiddleware } from './middleware'; +import { RawAPISchema, SchemaParserMiddleware } from './middleware'; // Add the "required" validator when the parameters are in path -export const addRequiredValidatorForPath = - (): SchemaParserMiddleware => async (schemas, next) => { +export class AddRequiredValidatorForPath extends SchemaParserMiddleware { + public async handle(schemas: RawAPISchema, next: () => Promise) { await next(); const requests = schemas.request || []; for (const request of requests) { @@ -20,4 +20,5 @@ export const addRequiredValidatorForPath = }); } } - }; + } +} diff --git a/packages/build/src/lib/schema-parser/middleware/checkParameter.ts b/packages/build/src/lib/schema-parser/middleware/checkParameter.ts index 0ab503be..c8c833bb 100644 --- a/packages/build/src/lib/schema-parser/middleware/checkParameter.ts +++ b/packages/build/src/lib/schema-parser/middleware/checkParameter.ts @@ -1,5 +1,5 @@ -import { AllTemplateMetadata, APISchema } from '@vulcan-sql/core'; -import { SchemaParserMiddleware } from './middleware'; +import { APISchema } from '@vulcan-sql/core'; +import { RawAPISchema, SchemaParserMiddleware } from './middleware'; interface Parameter { name: string; @@ -7,13 +7,11 @@ interface Parameter { columnNo: number; } -export const checkParameter = - (allMetadata: AllTemplateMetadata): SchemaParserMiddleware => - async (schemas, next) => { +export class CheckParameter extends SchemaParserMiddleware { + public async handle(schemas: RawAPISchema, next: () => Promise) { await next(); const transformedSchemas = schemas as APISchema; - const templateName = transformedSchemas.templateSource; - const metadata = allMetadata[templateName]; + const metadata = schemas.metadata; // Skip validation if no metadata found if (!metadata?.['parameter.vulcan.com']) return; @@ -31,4 +29,5 @@ export const checkParameter = ); } }); - }; + } +} diff --git a/packages/build/src/lib/schema-parser/middleware/checkValidator.ts b/packages/build/src/lib/schema-parser/middleware/checkValidator.ts index 18e9aa11..ddaabef8 100644 --- a/packages/build/src/lib/schema-parser/middleware/checkValidator.ts +++ b/packages/build/src/lib/schema-parser/middleware/checkValidator.ts @@ -1,10 +1,23 @@ -import { SchemaParserMiddleware } from './middleware'; +import { RawAPISchema, SchemaParserMiddleware } from './middleware'; import { chain } from 'lodash'; -import { APISchema, IValidatorLoader } from '@vulcan-sql/core'; +import { + APISchema, + IValidatorLoader, + TYPES as CORE_TYPES, +} from '@vulcan-sql/core'; +import { inject } from 'inversify'; -export const checkValidator = - (loader: IValidatorLoader): SchemaParserMiddleware => - async (schemas, next) => { +export class CheckValidator extends SchemaParserMiddleware { + private validatorLoader: IValidatorLoader; + + constructor( + @inject(CORE_TYPES.ValidatorLoader) validatorLoader: IValidatorLoader + ) { + super(); + this.validatorLoader = validatorLoader; + } + + public async handle(schemas: RawAPISchema, next: () => Promise) { await next(); const transformedSchemas = schemas as APISchema; const validators = chain(transformedSchemas.request) @@ -16,9 +29,10 @@ export const checkValidator = throw new Error('Validator name is required'); } - const validator = await loader.load(validatorRequest.name); + const validator = await this.validatorLoader.load(validatorRequest.name); // TODO: indicate the detail of error validator.validateSchema(validatorRequest.args); } - }; + } +} diff --git a/packages/build/src/lib/schema-parser/middleware/fallbackErrors.ts b/packages/build/src/lib/schema-parser/middleware/fallbackErrors.ts index d51cc0bf..f24c0c51 100644 --- a/packages/build/src/lib/schema-parser/middleware/fallbackErrors.ts +++ b/packages/build/src/lib/schema-parser/middleware/fallbackErrors.ts @@ -1,8 +1,9 @@ -import { SchemaParserMiddleware } from './middleware'; +import { RawAPISchema, SchemaParserMiddleware } from './middleware'; -export const fallbackErrors = - (): SchemaParserMiddleware => async (schemas, next) => { +export class FallbackErrors extends SchemaParserMiddleware { + public async handle(schemas: RawAPISchema, next: () => Promise) { if (schemas.errors) return next(); schemas.errors = []; return next(); - }; + } +} diff --git a/packages/build/src/lib/schema-parser/middleware/generateDataType.ts b/packages/build/src/lib/schema-parser/middleware/generateDataType.ts index 56b79f70..9b817881 100644 --- a/packages/build/src/lib/schema-parser/middleware/generateDataType.ts +++ b/packages/build/src/lib/schema-parser/middleware/generateDataType.ts @@ -1,6 +1,10 @@ import { FieldDataType } from '@vulcan-sql/core'; import { DeepPartial } from 'ts-essentials'; -import { RawResponseProperty, SchemaParserMiddleware } from './middleware'; +import { + RawAPISchema, + RawResponseProperty, + SchemaParserMiddleware, +} from './middleware'; const generateResponsePropertyType = ( property: DeepPartial @@ -16,8 +20,8 @@ const generateResponsePropertyType = ( // Fallback to string when type is not defined. // TODO: Guess the type by validators. -export const generateDataType = - (): SchemaParserMiddleware => async (schemas, next) => { +export class GenerateDataType extends SchemaParserMiddleware { + public async handle(schemas: RawAPISchema, next: () => Promise) { await next(); (schemas.request || []).forEach((request) => { if (!request.type) { @@ -27,4 +31,5 @@ export const generateDataType = (schemas.response || []).forEach((property) => generateResponsePropertyType(property) ); - }; + } +} diff --git a/packages/build/src/lib/schema-parser/middleware/generatePathParameters.ts b/packages/build/src/lib/schema-parser/middleware/generatePathParameters.ts index 55d1b787..59ef6aa8 100644 --- a/packages/build/src/lib/schema-parser/middleware/generatePathParameters.ts +++ b/packages/build/src/lib/schema-parser/middleware/generatePathParameters.ts @@ -1,9 +1,9 @@ import { FieldDataType, FieldInType } from '@vulcan-sql/core'; -import { SchemaParserMiddleware } from './middleware'; +import { RawAPISchema, SchemaParserMiddleware } from './middleware'; // /user/{id} => {request: [{fieldName: 'id', fieldIn: 'path' ....}]} -export const generatePathParameters = - (): SchemaParserMiddleware => async (schema, next) => { +export class GeneratePathParameters extends SchemaParserMiddleware { + public async handle(schema: RawAPISchema, next: () => Promise) { await next(); const pattern = /:([^/]+)/g; const pathParameters: string[] = []; @@ -31,4 +31,5 @@ export const generatePathParameters = }) ); schema.request = request; - }; + } +} diff --git a/packages/build/src/lib/schema-parser/middleware/generateTemplateSource.ts b/packages/build/src/lib/schema-parser/middleware/generateTemplateSource.ts index a4204eeb..dbf4a166 100644 --- a/packages/build/src/lib/schema-parser/middleware/generateTemplateSource.ts +++ b/packages/build/src/lib/schema-parser/middleware/generateTemplateSource.ts @@ -1,10 +1,11 @@ -import { SchemaParserMiddleware } from './middleware'; +import { RawAPISchema, SchemaParserMiddleware } from './middleware'; // Use schema sourceName which was generated by schema reader as templateSource when it wan't defined. // It usually be the path file of schema file. -export const generateTemplateSource = - (): SchemaParserMiddleware => async (schemas, next) => { +export class GenerateTemplateSource extends SchemaParserMiddleware { + public async handle(schemas: RawAPISchema, next: () => Promise) { if (schemas.templateSource) return next(); schemas.templateSource = schemas.sourceName; return next(); - }; + } +} diff --git a/packages/build/src/lib/schema-parser/middleware/generateUrl.ts b/packages/build/src/lib/schema-parser/middleware/generateUrl.ts index 35017e82..bef1bdd3 100644 --- a/packages/build/src/lib/schema-parser/middleware/generateUrl.ts +++ b/packages/build/src/lib/schema-parser/middleware/generateUrl.ts @@ -1,7 +1,7 @@ -import { SchemaParserMiddleware } from './middleware'; +import { RawAPISchema, SchemaParserMiddleware } from './middleware'; -export const generateUrl = - (): SchemaParserMiddleware => async (schemas, next) => { +export class GenerateUrl extends SchemaParserMiddleware { + public async handle(schemas: RawAPISchema, next: () => Promise) { if (schemas.urlPath) return next(); let urlPath = schemas.sourceName.toLocaleLowerCase(); @@ -19,4 +19,5 @@ export const generateUrl = schemas.urlPath = urlPath; return next(); - }; + } +} diff --git a/packages/build/src/lib/schema-parser/middleware/index.ts b/packages/build/src/lib/schema-parser/middleware/index.ts index ca344e7b..f11414c3 100644 --- a/packages/build/src/lib/schema-parser/middleware/index.ts +++ b/packages/build/src/lib/schema-parser/middleware/index.ts @@ -1,14 +1,34 @@ +import { ClassType } from '@vulcan-sql/core'; +import { GenerateUrl } from './generateUrl'; +import { CheckValidator } from './checkValidator'; +import { TransformValidator } from './transformValidator'; +import { GenerateTemplateSource } from './generateTemplateSource'; +import { CheckParameter } from './checkParameter'; +import { AddMissingErrors } from './addMissingErrors'; +import { FallbackErrors } from './fallbackErrors'; +import { NormalizeFieldIn } from './normalizeFieldIn'; +import { GenerateDataType } from './generateDataType'; +import { NormalizeDataType } from './normalizeDataType'; +import { GeneratePathParameters } from './generatePathParameters'; +import { AddRequiredValidatorForPath } from './addRequiredValidatorForPath'; +import { SetConstraints } from './setConstraints'; +import { SchemaParserMiddleware } from './middleware'; + export * from './middleware'; -export * from './generateUrl'; -export * from './checkValidator'; -export * from './transformValidator'; -export * from './generateTemplateSource'; -export * from './checkParameter'; -export * from './addMissingErrors'; -export * from './fallbackErrors'; -export * from './normalizeFieldIn'; -export * from './generateDataType'; -export * from './normalizeDataType'; -export * from './generatePathParameters'; -export * from './addRequiredValidatorForPath'; -export * from './setConstraints'; + +// The order of middleware here indicates the order of their execution, the first one will be executed first, and so on. +export const SchemaParserMiddlewares: ClassType[] = [ + GenerateUrl, + CheckValidator, + TransformValidator, + GenerateTemplateSource, + CheckParameter, + AddMissingErrors, + FallbackErrors, + NormalizeFieldIn, + GenerateDataType, + NormalizeDataType, + GeneratePathParameters, + AddRequiredValidatorForPath, + SetConstraints, +]; diff --git a/packages/build/src/lib/schema-parser/middleware/middleware.ts b/packages/build/src/lib/schema-parser/middleware/middleware.ts index df0b021a..09b51c67 100644 --- a/packages/build/src/lib/schema-parser/middleware/middleware.ts +++ b/packages/build/src/lib/schema-parser/middleware/middleware.ts @@ -5,6 +5,7 @@ import { ResponseProperty, ValidatorDefinition, } from '@vulcan-sql/core'; +import { injectable } from 'inversify'; import { DeepPartial } from 'ts-essentials'; export interface RawRequestParameter @@ -22,8 +23,13 @@ export interface RawAPISchema sourceName: string; request?: DeepPartial; response?: DeepPartial; + metadata?: Record; } -export interface SchemaParserMiddleware { - (schema: RawAPISchema, next: () => Promise): Promise; +@injectable() +export abstract class SchemaParserMiddleware { + abstract handle( + schema: RawAPISchema, + next: () => Promise + ): Promise; } diff --git a/packages/build/src/lib/schema-parser/middleware/normalizeDataType.ts b/packages/build/src/lib/schema-parser/middleware/normalizeDataType.ts index 9ea07a84..9a2ed19c 100644 --- a/packages/build/src/lib/schema-parser/middleware/normalizeDataType.ts +++ b/packages/build/src/lib/schema-parser/middleware/normalizeDataType.ts @@ -1,6 +1,10 @@ import { FieldDataType } from '@vulcan-sql/core'; import { DeepPartial } from 'ts-essentials'; -import { RawResponseProperty, SchemaParserMiddleware } from './middleware'; +import { + RawAPISchema, + RawResponseProperty, + SchemaParserMiddleware, +} from './middleware'; const normalizedResponsePropertyType = ( property: DeepPartial @@ -15,8 +19,8 @@ const normalizedResponsePropertyType = ( }; // type: string => FieldIn FieldDataType.STRING -export const normalizeDataType = - (): SchemaParserMiddleware => async (schemas, next) => { +export class NormalizeDataType extends SchemaParserMiddleware { + public async handle(schemas: RawAPISchema, next: () => Promise) { // Request (schemas.request || []).forEach((request) => { if (request.type) { @@ -29,4 +33,5 @@ export const normalizeDataType = ); return next(); - }; + } +} diff --git a/packages/build/src/lib/schema-parser/middleware/normalizeFieldIn.ts b/packages/build/src/lib/schema-parser/middleware/normalizeFieldIn.ts index 6420233d..7b1f79b2 100644 --- a/packages/build/src/lib/schema-parser/middleware/normalizeFieldIn.ts +++ b/packages/build/src/lib/schema-parser/middleware/normalizeFieldIn.ts @@ -1,13 +1,14 @@ import { FieldInType } from '@vulcan-sql/core'; -import { SchemaParserMiddleware } from './middleware'; +import { RawAPISchema, SchemaParserMiddleware } from './middleware'; // FieldIn: query => FieldIn FieldInType.QUERY -export const normalizeFieldIn = - (): SchemaParserMiddleware => async (schemas, next) => { +export class NormalizeFieldIn extends SchemaParserMiddleware { + public async handle(schemas: RawAPISchema, next: () => Promise) { (schemas.request || []).forEach((request) => { if (request.fieldIn) { request.fieldIn = request.fieldIn.toUpperCase() as FieldInType; } }); return next(); - }; + } +} diff --git a/packages/build/src/lib/schema-parser/middleware/setConstraints.ts b/packages/build/src/lib/schema-parser/middleware/setConstraints.ts index bd22a582..6bd65272 100644 --- a/packages/build/src/lib/schema-parser/middleware/setConstraints.ts +++ b/packages/build/src/lib/schema-parser/middleware/setConstraints.ts @@ -1,10 +1,23 @@ -import { APISchema, IValidatorLoader } from '@vulcan-sql/core'; +import { + APISchema, + IValidatorLoader, + TYPES as CORE_TYPES, +} from '@vulcan-sql/core'; +import { inject } from 'inversify'; import { chain } from 'lodash'; -import { SchemaParserMiddleware } from './middleware'; +import { RawAPISchema, SchemaParserMiddleware } from './middleware'; -export const setConstraints = - (loader: IValidatorLoader): SchemaParserMiddleware => - async (rawSchema, next) => { +export class SetConstraints extends SchemaParserMiddleware { + private validatorLoader: IValidatorLoader; + + constructor( + @inject(CORE_TYPES.ValidatorLoader) validatorLoader: IValidatorLoader + ) { + super(); + this.validatorLoader = validatorLoader; + } + + public async handle(rawSchema: RawAPISchema, next: () => Promise) { await next(); const schema = rawSchema as APISchema; @@ -12,7 +25,7 @@ export const setConstraints = // load validator and keep args const validatorsWithArgs = await Promise.all( (request.validators || []).map(async (validator) => ({ - validator: await loader.load(validator.name), + validator: await this.validatorLoader.load(validator.name), args: validator.args, })) ); @@ -37,4 +50,5 @@ export const setConstraints = }) .value(); } - }; + } +} diff --git a/packages/build/src/lib/schema-parser/middleware/transformValidator.ts b/packages/build/src/lib/schema-parser/middleware/transformValidator.ts index e16219f8..37b3fc61 100644 --- a/packages/build/src/lib/schema-parser/middleware/transformValidator.ts +++ b/packages/build/src/lib/schema-parser/middleware/transformValidator.ts @@ -1,8 +1,8 @@ -import { SchemaParserMiddleware } from './middleware'; +import { RawAPISchema, SchemaParserMiddleware } from './middleware'; // Transform validator requests -export const transformValidator = - (): SchemaParserMiddleware => async (schemas, next) => { +export class TransformValidator extends SchemaParserMiddleware { + public async handle(schemas: RawAPISchema, next: () => Promise) { if (!schemas.request) schemas.request = []; for ( let requestIndex = 0; @@ -27,4 +27,5 @@ export const transformValidator = } return next(); - }; + } +} diff --git a/packages/build/src/lib/schema-parser/schemaParser.ts b/packages/build/src/lib/schema-parser/schemaParser.ts index ec333f5b..5c7d4793 100644 --- a/packages/build/src/lib/schema-parser/schemaParser.ts +++ b/packages/build/src/lib/schema-parser/schemaParser.ts @@ -1,30 +1,15 @@ -import { - APISchema, - TemplateMetadata, - TYPES as CORE_TYPES, - IValidatorLoader, -} from '@vulcan-sql/core'; +import { APISchema, AllTemplateMetadata } from '@vulcan-sql/core'; import { SchemaData, SchemaFormat, SchemaReader } from './schema-reader'; import * as yaml from 'js-yaml'; -import { - RawAPISchema, - SchemaParserMiddleware, - generateUrl, - checkValidator, - transformValidator, - generateTemplateSource, - checkParameter, - fallbackErrors, - addMissingErrors, - normalizeFieldIn, - generateDataType, - normalizeDataType, - generatePathParameters, - addRequiredValidatorForPath, - setConstraints, -} from './middleware'; +import { RawAPISchema, SchemaParserMiddleware } from './middleware'; import * as compose from 'koa-compose'; -import { inject, injectable, interfaces } from 'inversify'; +import { + inject, + injectable, + interfaces, + multiInject, + optional, +} from 'inversify'; import { TYPES } from '@vulcan-sql/build/containers'; import { SchemaParserOptions } from '@vulcan-sql/build/options'; @@ -35,44 +20,32 @@ export interface SchemaParseResult { @injectable() export class SchemaParser { private schemaReader: SchemaReader; - private middleware: SchemaParserMiddleware[] = []; + private middleware: SchemaParserMiddleware['handle'][] = []; constructor( @inject(TYPES.Factory_SchemaReader) schemaReaderFactory: interfaces.AutoNamedFactory, @inject(TYPES.SchemaParserOptions) schemaParserOptions: SchemaParserOptions, - @inject(CORE_TYPES.ValidatorLoader) validatorLoader: IValidatorLoader + @multiInject(TYPES.SchemaParserMiddleware) + @optional() + middlewares: SchemaParserMiddleware[] = [] ) { this.schemaReader = schemaReaderFactory(schemaParserOptions.reader); - // Global middleware - this.use(generateUrl()); - this.use(generateTemplateSource()); - this.use(transformValidator()); - this.use(checkValidator(validatorLoader)); - this.use(fallbackErrors()); - this.use(normalizeFieldIn()); - this.use(generateDataType()); - this.use(normalizeDataType()); - this.use(generatePathParameters()); - this.use(addRequiredValidatorForPath()); - this.use(setConstraints(validatorLoader)); + // Load middleware + middlewares.forEach(this.use.bind(this)); } public async parse({ metadata, }: { - metadata?: Record; + metadata?: AllTemplateMetadata; } = {}): Promise { - const middleware = [...this.middleware]; - if (metadata) { - middleware.push(checkParameter(metadata)); - middleware.push(addMissingErrors(metadata)); - } - const execute = compose(middleware); + const execute = compose(this.middleware); const schemas: APISchema[] = []; for await (const schemaData of this.schemaReader.readSchema()) { const schema = await this.parseContent(schemaData); + schema.metadata = metadata?.[schema.templateSource || schema.sourceName]; // execute middleware await execute(schema); schemas.push(schema as APISchema); @@ -81,7 +54,7 @@ export class SchemaParser { } public use(middleware: SchemaParserMiddleware): this { - this.middleware.push(middleware); + this.middleware.push(middleware.handle.bind(middleware)); return this; } diff --git a/packages/build/test/schema-parser/middleware/addMissingErrors.spec.ts b/packages/build/test/schema-parser/middleware/addMissingErrors.spec.ts index deb3cf9a..4f8832b5 100644 --- a/packages/build/test/schema-parser/middleware/addMissingErrors.spec.ts +++ b/packages/build/test/schema-parser/middleware/addMissingErrors.spec.ts @@ -1,6 +1,5 @@ import { RawAPISchema } from '../../../src'; -import { addMissingErrors } from '../../../src/lib/schema-parser/middleware/addMissingErrors'; -import { AllTemplateMetadata } from '@vulcan-sql/core'; +import { AddMissingErrors } from '../../../src/lib/schema-parser/middleware/addMissingErrors'; it('Should add missing error codes', async () => { // Arrange @@ -9,9 +8,7 @@ it('Should add missing error codes', async () => { templateSource: 'some-name', request: [], errors: [], - }; - const metadata: AllTemplateMetadata = { - 'some-name': { + metadata: { 'error.vulcan.com': { errorCodes: [ { @@ -27,8 +24,9 @@ it('Should add missing error codes', async () => { }, }, }; + const addMissingErrors = new AddMissingErrors(); // Act - await addMissingErrors(metadata)(schema, async () => Promise.resolve()); + await addMissingErrors.handle(schema, async () => Promise.resolve()); // Assert expect(schema.errors?.length).toBe(1); }); @@ -45,9 +43,7 @@ it('Existed error codes should be kept', async () => { message: 'ERROR 1 with additional description', }, ], - }; - const metadata: AllTemplateMetadata = { - 'some-name': { + metadata: { parameters: [], 'error.vulcan.com': { errorCodes: [ @@ -64,8 +60,9 @@ it('Existed error codes should be kept', async () => { }, }, }; + const addMissingErrors = new AddMissingErrors(); // Act - await addMissingErrors(metadata)(schema, async () => Promise.resolve()); + await addMissingErrors.handle(schema, async () => Promise.resolve()); // Assert expect(schema.errors?.length).toBe(1); expect(schema.errors).toContainEqual({ @@ -81,17 +78,14 @@ it('Should tolerate empty error data', async () => { templateSource: 'some-name', request: [], errors: [], - }; - const metadata: object = { - 'some-name': { + metadata: { parameters: [], 'error.vulcan.com': null, }, }; + const addMissingErrors = new AddMissingErrors(); // Act - await addMissingErrors(metadata as AllTemplateMetadata)(schema, async () => - Promise.resolve() - ); + await addMissingErrors.handle(schema, async () => Promise.resolve()); // Assert expect(schema.errors?.length).toBe(0); }); @@ -104,11 +98,9 @@ it('Should tolerate empty metadata', async () => { request: [], errors: [], }; - const metadata: object = {}; + const addMissingErrors = new AddMissingErrors(); // Act - await addMissingErrors(metadata as AllTemplateMetadata)(schema, async () => - Promise.resolve() - ); + await addMissingErrors.handle(schema, async () => Promise.resolve()); // Assert expect(schema.errors?.length).toBe(0); }); diff --git a/packages/build/test/schema-parser/middleware/addRequiredValidatorForPath.spec.ts b/packages/build/test/schema-parser/middleware/addRequiredValidatorForPath.spec.ts index 60149427..09c4cc97 100644 --- a/packages/build/test/schema-parser/middleware/addRequiredValidatorForPath.spec.ts +++ b/packages/build/test/schema-parser/middleware/addRequiredValidatorForPath.spec.ts @@ -1,5 +1,5 @@ import { RawAPISchema } from '@vulcan-sql/build/schema-parser'; -import { addRequiredValidatorForPath } from '@vulcan-sql/build/schema-parser/middleware'; +import { AddRequiredValidatorForPath } from '@vulcan-sql/build/schema-parser/middleware/addRequiredValidatorForPath'; import { APISchema, FieldInType } from '@vulcan-sql/core'; it('Should add required validator for parameter in path', async () => { @@ -20,8 +20,11 @@ it('Should add required validator for parameter in path', async () => { }, ], }; + const addRequiredValidatorForPath = new AddRequiredValidatorForPath(); // Act - await addRequiredValidatorForPath()(schema, async () => Promise.resolve()); + await addRequiredValidatorForPath.handle(schema, async () => + Promise.resolve() + ); // Assert expect((schema as APISchema).request?.[0].validators?.[0].name).toEqual( 'required' @@ -52,8 +55,11 @@ it('Should not change the validator if it had already defined', async () => { }, ], }; + const addRequiredValidatorForPath = new AddRequiredValidatorForPath(); // Act - await addRequiredValidatorForPath()(schema, async () => Promise.resolve()); + await addRequiredValidatorForPath.handle(schema, async () => + Promise.resolve() + ); // Assert expect((schema as APISchema).request?.[0].validators?.[0].args.foo).toEqual( 'bar' diff --git a/packages/build/test/schema-parser/middleware/checkParameter.spec.ts b/packages/build/test/schema-parser/middleware/checkParameter.spec.ts index 4c0263b6..096125d6 100644 --- a/packages/build/test/schema-parser/middleware/checkParameter.spec.ts +++ b/packages/build/test/schema-parser/middleware/checkParameter.spec.ts @@ -1,6 +1,5 @@ import { RawAPISchema } from '../../../src'; -import { checkParameter } from '../../../src/lib/schema-parser/middleware/checkParameter'; -import { AllTemplateMetadata } from '@vulcan-sql/core'; +import { CheckParameter } from '../../../src/lib/schema-parser/middleware/checkParameter'; it('Should pass when every parameter has been defined', async () => { // Arrange @@ -15,9 +14,7 @@ it('Should pass when every parameter has been defined', async () => { fieldName: 'param2', }, ], - }; - const metadata: AllTemplateMetadata = { - 'some-name': { + metadata: { parameters: [ { name: 'param1', @@ -31,9 +28,10 @@ it('Should pass when every parameter has been defined', async () => { errors: [], }, }; + const checkParameter = new CheckParameter(); // Act Assert await expect( - checkParameter(metadata)(schema, async () => Promise.resolve()) + checkParameter.handle(schema, async () => Promise.resolve()) ).resolves.not.toThrow(); }); @@ -47,9 +45,7 @@ it(`Should throw when any parameter hasn't be defined`, async () => { fieldName: 'param1', }, ], - }; - const metadata: AllTemplateMetadata = { - 'some-name': { + metadata: { 'parameter.vulcan.com': [ { name: 'param1', @@ -63,9 +59,10 @@ it(`Should throw when any parameter hasn't be defined`, async () => { errors: [], }, }; - // Act Assert + const checkParameter = new CheckParameter(); + // Act, Assert await expect( - checkParameter(metadata)(schema, async () => Promise.resolve()) + checkParameter.handle(schema, async () => Promise.resolve()) ).rejects.toThrow( `Parameter param2.a.sub.property is not found in the schema.` ); @@ -78,18 +75,15 @@ it('Should tolerate empty parameter data', async () => { templateSource: 'some-name', request: [], errors: [], - }; - const metadata: object = { - 'some-name': { + metadata: { 'parameter.vulcan.com': null, errors: [], }, }; + const checkParameter = new CheckParameter(); // Act, Assert await expect( - checkParameter(metadata as AllTemplateMetadata)(schema, async () => - Promise.resolve() - ) + checkParameter.handle(schema, async () => Promise.resolve()) ).resolves.not.toThrow(); }); @@ -101,11 +95,9 @@ it('Should tolerate empty metadata', async () => { request: [], errors: [], }; - const metadata: object = {}; + const checkParameter = new CheckParameter(); // Act, Assert await expect( - checkParameter(metadata as AllTemplateMetadata)(schema, async () => - Promise.resolve() - ) + checkParameter.handle(schema, async () => Promise.resolve()) ).resolves.not.toThrow(); }); diff --git a/packages/build/test/schema-parser/middleware/checkValidator.spec.ts b/packages/build/test/schema-parser/middleware/checkValidator.spec.ts index b76d5521..3a5ae1ac 100644 --- a/packages/build/test/schema-parser/middleware/checkValidator.spec.ts +++ b/packages/build/test/schema-parser/middleware/checkValidator.spec.ts @@ -1,5 +1,5 @@ import { RawAPISchema } from '@vulcan-sql/build/schema-parser'; -import { checkValidator } from '@vulcan-sql/build/schema-parser/middleware/checkValidator'; +import { CheckValidator } from '@vulcan-sql/build/schema-parser/middleware/checkValidator'; import { IValidatorLoader } from '@vulcan-sql/core'; import * as sinon from 'ts-sinon'; @@ -19,10 +19,11 @@ it('Should pass if there is no error', async () => { validateSchema: () => null, validateData: () => null, }); + const checkValidator = new CheckValidator(stubValidatorLoader); // Act Assert await expect( - checkValidator(stubValidatorLoader)(schema, async () => Promise.resolve()) + checkValidator.handle(schema, async () => Promise.resolve()) ).resolves.not.toThrow(); }); @@ -42,10 +43,11 @@ it('Should throw if some validators have no name', async () => { validateSchema: () => null, validateData: () => null, }); + const checkValidator = new CheckValidator(stubValidatorLoader); // Act Assert await expect( - checkValidator(stubValidatorLoader)(schema, async () => Promise.resolve()) + checkValidator.handle(schema, async () => Promise.resolve()) ).rejects.toThrow('Validator name is required'); }); @@ -67,9 +69,10 @@ it('Should throw if the arguments of a validator is invalid', async () => { }, validateData: () => null, }); + const checkValidator = new CheckValidator(stubValidatorLoader); // Act Assert await expect( - checkValidator(stubValidatorLoader)(schema, async () => Promise.resolve()) + checkValidator.handle(schema, async () => Promise.resolve()) ).rejects.toThrow(); }); diff --git a/packages/build/test/schema-parser/middleware/fallbackError.spec.ts b/packages/build/test/schema-parser/middleware/fallbackError.spec.ts index 0291737d..79b69616 100644 --- a/packages/build/test/schema-parser/middleware/fallbackError.spec.ts +++ b/packages/build/test/schema-parser/middleware/fallbackError.spec.ts @@ -1,4 +1,4 @@ -import { fallbackErrors } from '../../../src/lib/schema-parser/middleware'; +import { FallbackErrors } from '../../../src/lib/schema-parser/middleware/fallbackErrors'; import { RawAPISchema } from '../../../src'; it('Should fallback errors to empty array', async () => { @@ -7,8 +7,9 @@ it('Should fallback errors to empty array', async () => { urlPath: '/existed/path', sourceName: 'some-name', }; + const fallbackErrors = new FallbackErrors(); // Act - await fallbackErrors()(schema, async () => Promise.resolve()); + await fallbackErrors.handle(schema, async () => Promise.resolve()); // Assert expect(schema.errors).toEqual([]); }); @@ -20,8 +21,9 @@ it('Should keep original errors value', async () => { sourceName: 'some-name', errors: [{ code: 'ERROR 1', message: 'ERROR 1' }], }; + const fallbackErrors = new FallbackErrors(); // Act - await fallbackErrors()(schema, async () => Promise.resolve()); + await fallbackErrors.handle(schema, async () => Promise.resolve()); // Assert expect(schema.errors?.length).toBe(1); expect(schema.errors).toContainEqual({ code: 'ERROR 1', message: 'ERROR 1' }); diff --git a/packages/build/test/schema-parser/middleware/generateDataType.spec.ts b/packages/build/test/schema-parser/middleware/generateDataType.spec.ts index 1b191a96..23196770 100644 --- a/packages/build/test/schema-parser/middleware/generateDataType.spec.ts +++ b/packages/build/test/schema-parser/middleware/generateDataType.spec.ts @@ -1,5 +1,5 @@ import { RawAPISchema } from '@vulcan-sql/build/schema-parser'; -import { generateDataType } from '@vulcan-sql/build/schema-parser/middleware'; +import { GenerateDataType } from '@vulcan-sql/build/schema-parser/middleware/generateDataType'; import { FieldDataType } from '@vulcan-sql/core'; it('Should generate data type (string) for requests when it was not defined', async () => { @@ -9,8 +9,9 @@ it('Should generate data type (string) for requests when it was not defined', as sourceName: 'some-name', request: [{}], }; + const generateDataType = new GenerateDataType(); // Act - await generateDataType()(schema, async () => Promise.resolve()); + await generateDataType.handle(schema, async () => Promise.resolve()); // Assert expect(schema.request?.[0].type).toEqual(FieldDataType.STRING); }); @@ -23,8 +24,9 @@ it('Should generate data type (string) for responses when it was not defined', a request: [], response: [{}, { type: [{}] as any }], }; + const generateDataType = new GenerateDataType(); // Act - await generateDataType()(schema, async () => Promise.resolve()); + await generateDataType.handle(schema, async () => Promise.resolve()); // Assert expect(schema.response?.[0].type).toEqual(FieldDataType.STRING); expect((schema.response?.[1].type?.[0] as any).type).toEqual( diff --git a/packages/build/test/schema-parser/middleware/generatePathParameters.spec.ts b/packages/build/test/schema-parser/middleware/generatePathParameters.spec.ts index ea77c653..bf18665c 100644 --- a/packages/build/test/schema-parser/middleware/generatePathParameters.spec.ts +++ b/packages/build/test/schema-parser/middleware/generatePathParameters.spec.ts @@ -1,5 +1,5 @@ import { RawAPISchema } from '@vulcan-sql/build/schema-parser'; -import { generatePathParameters } from '@vulcan-sql/build/schema-parser/middleware'; +import { GeneratePathParameters } from '@vulcan-sql/build/schema-parser/middleware/generatePathParameters'; import { FieldDataType, FieldInType } from '@vulcan-sql/core'; it('Should generate path parameters when they were not defined', async () => { @@ -9,8 +9,9 @@ it('Should generate path parameters when they were not defined', async () => { urlPath: 'existed/path/:id/order/:oid', request: [], }; + const generatePathParameters = new GeneratePathParameters(); // Act - await generatePathParameters()(schema, async () => Promise.resolve()); + await generatePathParameters.handle(schema, async () => Promise.resolve()); // Assert expect(schema.request?.[0].fieldName).toEqual('id'); expect(schema.request?.[0].type).toEqual(FieldDataType.STRING); @@ -34,8 +35,9 @@ it('Should keep original parameters when they had been defined', async () => { }, ], }; + const generatePathParameters = new GeneratePathParameters(); // Act - await generatePathParameters()(schema, async () => Promise.resolve()); + await generatePathParameters.handle(schema, async () => Promise.resolve()); // Assert expect(schema.request?.[0].fieldName).toEqual('id'); expect(schema.request?.[0].type).toEqual(FieldDataType.NUMBER); diff --git a/packages/build/test/schema-parser/middleware/generateTemplateSource.spec.ts b/packages/build/test/schema-parser/middleware/generateTemplateSource.spec.ts index 986a35ce..108d55b3 100644 --- a/packages/build/test/schema-parser/middleware/generateTemplateSource.spec.ts +++ b/packages/build/test/schema-parser/middleware/generateTemplateSource.spec.ts @@ -1,5 +1,5 @@ import { RawAPISchema } from '@vulcan-sql/build/schema-parser'; -import { generateTemplateSource } from '@vulcan-sql/build/schema-parser/middleware/generateTemplateSource'; +import { GenerateTemplateSource } from '@vulcan-sql/build/schema-parser/middleware/generateTemplateSource'; it('Should keep templateSource in schema', async () => { // Arrange @@ -7,8 +7,9 @@ it('Should keep templateSource in schema', async () => { templateSource: 'existed/path', sourceName: 'some-name', }; + const generateTemplateSource = new GenerateTemplateSource(); // Act - await generateTemplateSource()(schema, async () => Promise.resolve()); + await generateTemplateSource.handle(schema, async () => Promise.resolve()); // Assert expect(schema['templateSource']).toEqual('existed/path'); }); @@ -18,8 +19,9 @@ it('Should add fallback value (source name) when templateSource is empty', async const schema: RawAPISchema = { sourceName: 'some/name', }; + const generateTemplateSource = new GenerateTemplateSource(); // Act - await generateTemplateSource()(schema, async () => Promise.resolve()); + await generateTemplateSource.handle(schema, async () => Promise.resolve()); // Assert expect(schema['templateSource']).toEqual('some/name'); }); diff --git a/packages/build/test/schema-parser/middleware/generateUrl.spec.ts b/packages/build/test/schema-parser/middleware/generateUrl.spec.ts index ed1782d1..dbc98dac 100644 --- a/packages/build/test/schema-parser/middleware/generateUrl.spec.ts +++ b/packages/build/test/schema-parser/middleware/generateUrl.spec.ts @@ -1,5 +1,5 @@ import { RawAPISchema } from '@vulcan-sql/build/schema-parser'; -import { generateUrl } from '@vulcan-sql/build/schema-parser/middleware/generateUrl'; +import { GenerateUrl } from '@vulcan-sql/build/schema-parser/middleware/generateUrl'; it('Should keep url in schema', async () => { // Arrange @@ -7,8 +7,9 @@ it('Should keep url in schema', async () => { urlPath: '/existed/path', sourceName: 'some-name', }; + const generateUrl = new GenerateUrl(); // Act - await generateUrl()(schema, async () => Promise.resolve()); + await generateUrl.handle(schema, async () => Promise.resolve()); // Assert expect(schema['urlPath']).toEqual('/existed/path'); }); @@ -18,8 +19,9 @@ it('Should add leading slash', async () => { const schema: RawAPISchema = { sourceName: 'some-name', }; + const generateUrl = new GenerateUrl(); // Act - await generateUrl()(schema, async () => Promise.resolve()); + await generateUrl.handle(schema, async () => Promise.resolve()); // Assert expect(schema['urlPath']).toEqual('/some-name'); }); @@ -29,8 +31,9 @@ it('Should remove trailing slash', async () => { const schema: RawAPISchema = { sourceName: '/some-name/', }; + const generateUrl = new GenerateUrl(); // Act - await generateUrl()(schema, async () => Promise.resolve()); + await generateUrl.handle(schema, async () => Promise.resolve()); // Assert expect(schema['urlPath']).toEqual('/some-name'); }); @@ -40,8 +43,9 @@ it('Should replace white space', async () => { const schema: RawAPISchema = { sourceName: 'some name', }; + const generateUrl = new GenerateUrl(); // Act - await generateUrl()(schema, async () => Promise.resolve()); + await generateUrl.handle(schema, async () => Promise.resolve()); // Assert expect(schema['urlPath']).toEqual('/some-name'); }); diff --git a/packages/build/test/schema-parser/middleware/normalizeDataType.spec.ts b/packages/build/test/schema-parser/middleware/normalizeDataType.spec.ts index 02514cd9..74932079 100644 --- a/packages/build/test/schema-parser/middleware/normalizeDataType.spec.ts +++ b/packages/build/test/schema-parser/middleware/normalizeDataType.spec.ts @@ -1,5 +1,5 @@ import { RawAPISchema } from '@vulcan-sql/build/schema-parser'; -import { normalizeDataType } from '@vulcan-sql/build/schema-parser/middleware'; +import { NormalizeDataType } from '@vulcan-sql/build/schema-parser/middleware/normalizeDataType'; import { FieldDataType } from '@vulcan-sql/core'; it('Should normalize data type for requests', async () => { @@ -13,8 +13,9 @@ it('Should normalize data type for requests', async () => { }, ], }; + const normalizeDataType = new NormalizeDataType(); // Act - await normalizeDataType()(schema, async () => Promise.resolve()); + await normalizeDataType.handle(schema, async () => Promise.resolve()); // Assert expect(schema.request?.[0].type).toEqual(FieldDataType.NUMBER); }); @@ -37,8 +38,9 @@ it('Should normalize data type for responses', async () => { }, ], }; + const normalizeDataType = new NormalizeDataType(); // Act - await normalizeDataType()(schema, async () => Promise.resolve()); + await normalizeDataType.handle(schema, async () => Promise.resolve()); // Assert expect(schema.response?.[0].type).toEqual(FieldDataType.NUMBER); expect((schema.response?.[1] as any).type[0].type).toEqual( diff --git a/packages/build/test/schema-parser/middleware/normalizeInField.spec.ts b/packages/build/test/schema-parser/middleware/normalizeInField.spec.ts index ec1f0a84..935dbb80 100644 --- a/packages/build/test/schema-parser/middleware/normalizeInField.spec.ts +++ b/packages/build/test/schema-parser/middleware/normalizeInField.spec.ts @@ -1,5 +1,5 @@ import { RawAPISchema } from '@vulcan-sql/build/schema-parser'; -import { normalizeFieldIn } from '@vulcan-sql/build/schema-parser/middleware'; +import { NormalizeFieldIn } from '@vulcan-sql/build/schema-parser/middleware/normalizeFieldIn'; import { FieldInType } from '@vulcan-sql/core'; it('Should normalize in field', async () => { @@ -13,8 +13,9 @@ it('Should normalize in field', async () => { }, ], }; + const normalizeFieldIn = new NormalizeFieldIn(); // Act - await normalizeFieldIn()(schema, async () => Promise.resolve()); + await normalizeFieldIn.handle(schema, async () => Promise.resolve()); // Assert expect(schema.request?.[0].fieldIn).toEqual(FieldInType.QUERY); }); diff --git a/packages/build/test/schema-parser/middleware/setConstraints.spec.ts b/packages/build/test/schema-parser/middleware/setConstraints.spec.ts index 9a9fe546..7ed9f00c 100644 --- a/packages/build/test/schema-parser/middleware/setConstraints.spec.ts +++ b/packages/build/test/schema-parser/middleware/setConstraints.spec.ts @@ -1,5 +1,5 @@ import { RawAPISchema } from '@vulcan-sql/build/schema-parser'; -import { setConstraints } from '@vulcan-sql/build/schema-parser/middleware'; +import { SetConstraints } from '@vulcan-sql/build/schema-parser/middleware/setConstraints'; import { Constraint, MinValueConstraint, @@ -34,10 +34,9 @@ it('Should set and compose constraints', async () => { }, ], }; + const setConstraints = new SetConstraints(stubValidatorLoader); // Act - await setConstraints(stubValidatorLoader)(schema, async () => - Promise.resolve() - ); + await setConstraints.handle(schema, async () => Promise.resolve()); // Assert expect(schema.request?.[0].constraints?.length).toEqual(2); expect( diff --git a/packages/build/test/schema-parser/middleware/transformValidator.spec.ts b/packages/build/test/schema-parser/middleware/transformValidator.spec.ts index a309ab7c..c8dc625b 100644 --- a/packages/build/test/schema-parser/middleware/transformValidator.spec.ts +++ b/packages/build/test/schema-parser/middleware/transformValidator.spec.ts @@ -1,5 +1,5 @@ import { RawAPISchema } from '@vulcan-sql/build/schema-parser'; -import { transformValidator } from '@vulcan-sql/build/schema-parser/middleware/transformValidator'; +import { TransformValidator } from '@vulcan-sql/build/schema-parser/middleware/transformValidator'; it('Should convert string validator to proper format', async () => { // Arrange @@ -11,8 +11,9 @@ it('Should convert string validator to proper format', async () => { }, ], }; + const transformValidator = new TransformValidator(); // Act - await transformValidator()(schema, async () => Promise.resolve()); + await transformValidator.handle(schema, async () => Promise.resolve()); // Assert expect(schema.request?.[0].validators?.[0]).toEqual({ name: 'validator1', @@ -30,8 +31,9 @@ it('Should add fallback value when a validator has no argument', async () => { }, ], }; + const transformValidator = new TransformValidator(); // Act - await transformValidator()(schema, async () => Promise.resolve()); + await transformValidator.handle(schema, async () => Promise.resolve()); // Assert expect(schema.request?.[0].validators?.[0]).toEqual({ name: 'validator1', @@ -44,8 +46,9 @@ it('Should add fallback value when there is no request', async () => { const schema: RawAPISchema = { sourceName: 'some-name', }; + const transformValidator = new TransformValidator(); // Act - await transformValidator()(schema, async () => Promise.resolve()); + await transformValidator.handle(schema, async () => Promise.resolve()); // Assert expect(schema.request).toEqual([]); }); @@ -56,8 +59,9 @@ it('Should add fallback value when a request has no validator', async () => { sourceName: 'some-name', request: [{ fieldName: 'field1' }], }; + const transformValidator = new TransformValidator(); // Act - await transformValidator()(schema, async () => Promise.resolve()); + await transformValidator.handle(schema, async () => Promise.resolve()); // Assert expect(schema.request?.[0].validators).toEqual([]); }); diff --git a/packages/build/test/spec-generator/schema.ts b/packages/build/test/spec-generator/schema.ts index d09c9a49..bfdf14f2 100644 --- a/packages/build/test/spec-generator/schema.ts +++ b/packages/build/test/spec-generator/schema.ts @@ -3,21 +3,23 @@ import * as glob from 'glob'; import * as path from 'path'; import { promises as fs } from 'fs'; -import { APISchema, Constraint, IValidatorLoader } from '@vulcan-sql/core'; +import { + APISchema, + Constraint, + IValidatorLoader, + TYPES as CORE_TYPES, +} from '@vulcan-sql/core'; import * as jsYaml from 'js-yaml'; import { sortBy } from 'lodash'; -import { IBuildOptions } from '@vulcan-sql/build'; +import { IBuildOptions, TYPES } from '@vulcan-sql/build'; import compose = require('koa-compose'); import { - generateDataType, - normalizeDataType, - normalizeFieldIn, - setConstraints, RawAPISchema, SchemaParserMiddleware, - transformValidator, + SchemaParserMiddlewares, } from '@vulcan-sql/build/schema-parser/middleware'; import * as sinon from 'ts-sinon'; +import { Container } from 'inversify'; const getSchemaPaths = () => new Promise((resolve, reject) => { @@ -95,14 +97,19 @@ export const getSchemas = async () => { schemas.push(jsYaml.load(content) as RawAPISchema); } const loader = getStubLoader(); - // Load some required middleware - const execute = compose([ - normalizeFieldIn(), - transformValidator(), - generateDataType(), - normalizeDataType(), - setConstraints(loader), - ] as SchemaParserMiddleware[]); + const container = new Container(); + container.bind(CORE_TYPES.ValidatorLoader).toConstantValue(loader); + SchemaParserMiddlewares.forEach((middleware) => { + container.bind(TYPES.SchemaParserMiddleware).to(middleware); + }); + + // Load middlewares + + const execute = compose( + container + .getAll(TYPES.SchemaParserMiddleware) + .map((middleware) => middleware.handle.bind(middleware)) + ); for (const schema of schemas) { await execute(schema); } From 4eec83fb3d8e4f8fe8ca57a473be7e7d99fccb2c Mon Sep 17 00:00:00 2001 From: Ivan Tsai Date: Mon, 18 Jul 2022 16:29:14 +0800 Subject: [PATCH 3/4] feat(core): load compiled data right after compiling --- .../core/src/containers/modules/templateEngine.ts | 3 ++- .../lib/template-engine/code-loader/codeLoader.ts | 6 ++++++ .../{ => code-loader}/inMemoryCodeLoader.ts | 3 ++- .../src/lib/template-engine/code-loader/index.ts | 2 ++ packages/core/src/lib/template-engine/compiler.ts | 8 +++++++- packages/core/src/lib/template-engine/index.ts | 2 +- .../src/lib/template-engine/templateEngine.ts | 14 +++++++++++--- .../test/template-engine/templateEngine.spec.ts | 15 +++++++++++++++ 8 files changed, 46 insertions(+), 7 deletions(-) create mode 100644 packages/core/src/lib/template-engine/code-loader/codeLoader.ts rename packages/core/src/lib/template-engine/{ => code-loader}/inMemoryCodeLoader.ts (86%) create mode 100644 packages/core/src/lib/template-engine/code-loader/index.ts diff --git a/packages/core/src/containers/modules/templateEngine.ts b/packages/core/src/containers/modules/templateEngine.ts index c3883f7d..528f5e30 100644 --- a/packages/core/src/containers/modules/templateEngine.ts +++ b/packages/core/src/containers/modules/templateEngine.ts @@ -16,6 +16,7 @@ import { TemplateEngineOptions } from '../../options'; import * as nunjucks from 'nunjucks'; // TODO: fix the path import { bindExtensions } from '@vulcan-sql/core/template-engine/extension-loader'; +import { ICodeLoader } from '@vulcan-sql/core/template-engine/code-loader'; export const templateEngineModule = ( options: ITemplateEngineOptions, @@ -59,7 +60,7 @@ export const templateEngineModule = ( .whenTargetNamed('compileTime'); // Loader - bind(TYPES.CompilerLoader) + bind(TYPES.CompilerLoader) .to(InMemoryCodeLoader) .inSingletonScope(); diff --git a/packages/core/src/lib/template-engine/code-loader/codeLoader.ts b/packages/core/src/lib/template-engine/code-loader/codeLoader.ts new file mode 100644 index 00000000..70a0273b --- /dev/null +++ b/packages/core/src/lib/template-engine/code-loader/codeLoader.ts @@ -0,0 +1,6 @@ +import * as nunjucks from 'nunjucks'; + +export interface ICodeLoader { + setSource(name: string, code: string): void; + getSource(name: string): nunjucks.LoaderSource | null; +} diff --git a/packages/core/src/lib/template-engine/inMemoryCodeLoader.ts b/packages/core/src/lib/template-engine/code-loader/inMemoryCodeLoader.ts similarity index 86% rename from packages/core/src/lib/template-engine/inMemoryCodeLoader.ts rename to packages/core/src/lib/template-engine/code-loader/inMemoryCodeLoader.ts index 4c662828..a0c7fdf0 100644 --- a/packages/core/src/lib/template-engine/inMemoryCodeLoader.ts +++ b/packages/core/src/lib/template-engine/code-loader/inMemoryCodeLoader.ts @@ -1,8 +1,9 @@ import * as nunjucks from 'nunjucks'; import { injectable } from 'inversify'; +import { ICodeLoader } from './codeLoader'; @injectable() -export class InMemoryCodeLoader implements nunjucks.ILoader { +export class InMemoryCodeLoader implements ICodeLoader { private source = new Map(); public setSource(name: string, code: string) { diff --git a/packages/core/src/lib/template-engine/code-loader/index.ts b/packages/core/src/lib/template-engine/code-loader/index.ts new file mode 100644 index 00000000..621ece6a --- /dev/null +++ b/packages/core/src/lib/template-engine/code-loader/index.ts @@ -0,0 +1,2 @@ +export * from './codeLoader'; +export * from './inMemoryCodeLoader'; diff --git a/packages/core/src/lib/template-engine/compiler.ts b/packages/core/src/lib/template-engine/compiler.ts index cae6605d..c7fe0b7b 100644 --- a/packages/core/src/lib/template-engine/compiler.ts +++ b/packages/core/src/lib/template-engine/compiler.ts @@ -1,3 +1,5 @@ +import { Pagination } from '../../models/pagination'; + export interface TemplateLocation { lineNo: number; columnNo: number; @@ -26,5 +28,9 @@ export interface Compiler { * @param template The path or identifier of a template source */ compile(template: string): Promise; - execute(template: string, data: T): Promise; + execute( + templateName: string, + data: T, + pagination?: Pagination + ): Promise; } diff --git a/packages/core/src/lib/template-engine/index.ts b/packages/core/src/lib/template-engine/index.ts index c315bab3..32332221 100644 --- a/packages/core/src/lib/template-engine/index.ts +++ b/packages/core/src/lib/template-engine/index.ts @@ -1,6 +1,6 @@ export * from './templateEngine'; export * from './compiler'; export * from './template-providers'; -export * from './inMemoryCodeLoader'; +export * from './code-loader'; export * from './nunjucksCompiler'; export * from './extension-loader'; diff --git a/packages/core/src/lib/template-engine/templateEngine.ts b/packages/core/src/lib/template-engine/templateEngine.ts index 6aa62f33..9eb4a4ee 100644 --- a/packages/core/src/lib/template-engine/templateEngine.ts +++ b/packages/core/src/lib/template-engine/templateEngine.ts @@ -3,6 +3,8 @@ import { TemplateProvider } from './template-providers'; import { injectable, inject, interfaces } from 'inversify'; import { TYPES } from '@vulcan-sql/core/containers'; import { TemplateEngineOptions } from '../../options'; +import { Pagination } from '@vulcan-sql/core/models'; +import { ICodeLoader } from './code-loader'; export type AllTemplateMetadata = Record; @@ -17,14 +19,17 @@ export interface PreCompiledResult { export class TemplateEngine { private compiler: Compiler; private templateProvider: TemplateProvider; + private compilerLoader: ICodeLoader; constructor( @inject(TYPES.Compiler) compiler: Compiler, @inject(TYPES.Factory_TemplateProvider) templateProviderFactory: interfaces.AutoNamedFactory, - @inject(TYPES.TemplateEngineOptions) options: TemplateEngineOptions + @inject(TYPES.TemplateEngineOptions) options: TemplateEngineOptions, + @inject(TYPES.CompilerLoader) compilerLoader: ICodeLoader ) { this.compiler = compiler; + this.compilerLoader = compilerLoader; this.templateProvider = templateProviderFactory(options.provider); } @@ -36,6 +41,8 @@ export class TemplateEngine { const { compiledData, metadata } = await this.compiler.compile( template.statement ); + // load compileData immediately to the loader + this.compilerLoader.setSource(template.name, compiledData); templateResult[template.name] = compiledData; metadataResult[template.name] = metadata; } @@ -48,8 +55,9 @@ export class TemplateEngine { public async execute( templateName: string, - data: T + data: T, + pagination?: Pagination ): Promise { - return this.compiler.execute(templateName, data); + return this.compiler.execute(templateName, data, pagination); } } diff --git a/packages/core/test/template-engine/templateEngine.spec.ts b/packages/core/test/template-engine/templateEngine.spec.ts index e1bec8c6..fb7fd330 100644 --- a/packages/core/test/template-engine/templateEngine.spec.ts +++ b/packages/core/test/template-engine/templateEngine.spec.ts @@ -2,6 +2,7 @@ import { TemplateEngine, Compiler, TemplateProvider, + ICodeLoader, } from '@vulcan-sql/core/template-engine'; import * as sinon from 'ts-sinon'; import { TYPES } from '@vulcan-sql/core/containers'; @@ -10,11 +11,13 @@ import { Container } from 'inversify'; let container: Container; let stubCompiler: sinon.StubbedInstance; let stubTemplateProvider: sinon.StubbedInstance; +let stubCodeLoader: sinon.StubbedInstance; beforeEach(() => { container = new Container(); stubCompiler = sinon.stubInterface(); stubTemplateProvider = sinon.stubInterface(); + stubCodeLoader = sinon.stubInterface(); container.bind(TYPES.Compiler).toConstantValue(stubCompiler); container @@ -22,6 +25,7 @@ beforeEach(() => { .toConstantValue(() => stubTemplateProvider); container.bind(TYPES.TemplateEngine).to(TemplateEngine).inSingletonScope(); container.bind(TYPES.TemplateEngineOptions).toConstantValue({}); + container.bind(TYPES.CompilerLoader).toConstantValue(stubCodeLoader); stubCompiler.name = 'stub-compiler'; stubCompiler.compile.resolves({ @@ -82,3 +86,14 @@ it('Template engine render function should forward correct data to compiler', as expect(stubCompiler.execute.calledWith('template-name', context)).toBe(true); expect(result).toBe('sql-result'); }); + +it('Template engine should load the compiled code after compiling', async () => { + // Assert + const templateEngine = container.get(TYPES.TemplateEngine); + + // Act + await templateEngine.compile(); + + // Assert + expect(stubCodeLoader.setSource.calledOnce).toBe(true); +}); From caeb4eb0f6a80a3420649e4b7d5b410c87d8dd02 Mon Sep 17 00:00:00 2001 From: Ivan Tsai Date: Mon, 18 Jul 2022 16:47:10 +0800 Subject: [PATCH 4/4] feat(build): add response sampler middleware --- .../src/lib/schema-parser/middleware/index.ts | 2 + .../middleware/responseSampler.ts | 63 +++++++++++ .../middleware/responseSampler.spec.ts | 107 ++++++++++++++++++ packages/build/test/spec-generator/schema.ts | 5 +- packages/core/src/models/artifact.ts | 3 + 5 files changed, 178 insertions(+), 2 deletions(-) create mode 100644 packages/build/src/lib/schema-parser/middleware/responseSampler.ts create mode 100644 packages/build/test/schema-parser/middleware/responseSampler.spec.ts diff --git a/packages/build/src/lib/schema-parser/middleware/index.ts b/packages/build/src/lib/schema-parser/middleware/index.ts index f11414c3..7160bc1b 100644 --- a/packages/build/src/lib/schema-parser/middleware/index.ts +++ b/packages/build/src/lib/schema-parser/middleware/index.ts @@ -13,6 +13,7 @@ import { GeneratePathParameters } from './generatePathParameters'; import { AddRequiredValidatorForPath } from './addRequiredValidatorForPath'; import { SetConstraints } from './setConstraints'; import { SchemaParserMiddleware } from './middleware'; +import { ResponseSampler } from './responseSampler'; export * from './middleware'; @@ -31,4 +32,5 @@ export const SchemaParserMiddlewares: ClassType[] = [ GeneratePathParameters, AddRequiredValidatorForPath, SetConstraints, + ResponseSampler, ]; diff --git a/packages/build/src/lib/schema-parser/middleware/responseSampler.ts b/packages/build/src/lib/schema-parser/middleware/responseSampler.ts new file mode 100644 index 00000000..b853c51d --- /dev/null +++ b/packages/build/src/lib/schema-parser/middleware/responseSampler.ts @@ -0,0 +1,63 @@ +import { inject } from 'inversify'; +import { RawAPISchema, SchemaParserMiddleware } from './middleware'; +import { + APISchema, + FieldDataType, + ResponseProperty, + TemplateEngine, + TYPES as CORE_TYPES, +} from '@vulcan-sql/core'; +import { unionBy } from 'lodash'; + +export class ResponseSampler extends SchemaParserMiddleware { + private templateEngine: TemplateEngine; + + constructor( + @inject(CORE_TYPES.TemplateEngine) templateEngine: TemplateEngine + ) { + super(); + this.templateEngine = templateEngine; + } + + public async handle( + rawSchema: RawAPISchema, + next: () => Promise + ): Promise { + await next(); + const schema = rawSchema as APISchema; + if (!schema.exampleParameter) return; + + const response = await this.templateEngine.execute( + schema.templateSource, + { context: { params: schema.exampleParameter } }, + // We only need the columns of this query, so we set offset/limit both to 0 here. + { + limit: 0, + offset: 0, + } + ); + // TODO: I haven't known the response of queryBuilder.value(), assume that there is a "columns" property that indicates the columns' name and type here. + const columns: { name: string; type: string }[] = response.columns; + const responseColumns = this.normalizeResponseColumns(columns); + schema.response = this.mergeResponse( + schema.response || [], + responseColumns + ); + } + + private normalizeResponseColumns( + columns: { name: string; type: string }[] + ): ResponseProperty[] { + return columns.map((column) => ({ + name: column.name, + type: column.type.toUpperCase() as FieldDataType, + })); + } + + private mergeResponse( + source: ResponseProperty[], + target: ResponseProperty[] + ) { + return unionBy(source, target, (response) => response.name); + } +} diff --git a/packages/build/test/schema-parser/middleware/responseSampler.spec.ts b/packages/build/test/schema-parser/middleware/responseSampler.spec.ts new file mode 100644 index 00000000..7846cc79 --- /dev/null +++ b/packages/build/test/schema-parser/middleware/responseSampler.spec.ts @@ -0,0 +1,107 @@ +import { RawAPISchema } from '@vulcan-sql/build/schema-parser'; +import { ResponseSampler } from '@vulcan-sql/build/schema-parser/middleware/responseSampler'; +import { FieldDataType, TemplateEngine } from '@vulcan-sql/core'; +import * as sinon from 'ts-sinon'; + +it('Should create response definition when example parameter is provided', async () => { + // Arrange + const schema: RawAPISchema = { + templateSource: 'existed/path', + sourceName: 'some-name', + exampleParameter: { + someParam: 123, + }, + }; + const stubTemplateEngine = sinon.stubInterface(); + stubTemplateEngine.execute.resolves({ + columns: [ + { name: 'id', type: 'string' }, + { name: 'age', type: 'number' }, + ], + }); + const responseSampler = new ResponseSampler(stubTemplateEngine); + // Act + await responseSampler.handle(schema, async () => Promise.resolve()); + // Assert + expect(schema.response?.[0].name).toEqual('id'); + expect(schema.response?.[0].type).toEqual(FieldDataType.STRING); + expect(schema.response?.[1].name).toEqual('age'); + expect(schema.response?.[1].type).toEqual(FieldDataType.NUMBER); +}); + +it('Should create response definition when example parameter is a empty object', async () => { + // Arrange + const schema: RawAPISchema = { + templateSource: 'existed/path', + sourceName: 'some-name', + exampleParameter: {}, + }; + const stubTemplateEngine = sinon.stubInterface(); + stubTemplateEngine.execute.resolves({ + columns: [ + { name: 'id', type: 'string' }, + { name: 'age', type: 'number' }, + ], + }); + const responseSampler = new ResponseSampler(stubTemplateEngine); + // Act + await responseSampler.handle(schema, async () => Promise.resolve()); + // Assert + expect(schema.response?.[0].name).toEqual('id'); + expect(schema.response?.[0].type).toEqual(FieldDataType.STRING); + expect(schema.response?.[1].name).toEqual('age'); + expect(schema.response?.[1].type).toEqual(FieldDataType.NUMBER); +}); + +it('Should not create response definition when example parameter is not provided', async () => { + // Arrange + const schema: RawAPISchema = { + templateSource: 'existed/path', + sourceName: 'some-name', + }; + const stubTemplateEngine = sinon.stubInterface(); + stubTemplateEngine.execute.resolves({ + columns: [ + { name: 'id', type: 'string' }, + { name: 'age', type: 'number' }, + ], + }); + const responseSampler = new ResponseSampler(stubTemplateEngine); + // Act + await responseSampler.handle(schema, async () => Promise.resolve()); + // Assert + expect(schema.response).toBeFalsy(); +}); + +it('Should append response definition when there are some existed definitions', async () => { + // Arrange + const schema: RawAPISchema = { + templateSource: 'existed/path', + sourceName: 'some-name', + exampleParameter: {}, + response: [ + { + name: 'name', + type: 'STRING', + }, + ], + }; + const stubTemplateEngine = sinon.stubInterface(); + stubTemplateEngine.execute.resolves({ + columns: [ + { name: 'id', type: 'string' }, + { name: 'age', type: 'number' }, + { name: 'name', type: 'boolean' }, + ], + }); + const responseSampler = new ResponseSampler(stubTemplateEngine); + // Act + await responseSampler.handle(schema, async () => Promise.resolve()); + // Assert + expect(schema.response?.[0].name).toEqual('name'); + expect(schema.response?.[0].type).toEqual(FieldDataType.STRING); + expect(schema.response?.[1].name).toEqual('id'); + expect(schema.response?.[1].type).toEqual(FieldDataType.STRING); + expect(schema.response?.[2].name).toEqual('age'); + expect(schema.response?.[2].type).toEqual(FieldDataType.NUMBER); +}); diff --git a/packages/build/test/spec-generator/schema.ts b/packages/build/test/spec-generator/schema.ts index bfdf14f2..f11cc491 100644 --- a/packages/build/test/spec-generator/schema.ts +++ b/packages/build/test/spec-generator/schema.ts @@ -7,6 +7,7 @@ import { APISchema, Constraint, IValidatorLoader, + TemplateEngine, TYPES as CORE_TYPES, } from '@vulcan-sql/core'; import * as jsYaml from 'js-yaml'; @@ -97,14 +98,14 @@ export const getSchemas = async () => { schemas.push(jsYaml.load(content) as RawAPISchema); } const loader = getStubLoader(); + const templateEngine = sinon.stubInterface(); const container = new Container(); container.bind(CORE_TYPES.ValidatorLoader).toConstantValue(loader); + container.bind(CORE_TYPES.TemplateEngine).toConstantValue(templateEngine); SchemaParserMiddlewares.forEach((middleware) => { container.bind(TYPES.SchemaParserMiddleware).to(middleware); }); - // Load middlewares - const execute = compose( container .getAll(TYPES.SchemaParserMiddleware) diff --git a/packages/core/src/models/artifact.ts b/packages/core/src/models/artifact.ts index 505e435d..b5601d7e 100644 --- a/packages/core/src/models/artifact.ts +++ b/packages/core/src/models/artifact.ts @@ -71,6 +71,8 @@ export interface ErrorInfo { message: string; } +export type ExampleParameter = Record; + export interface APISchema { // graphql operation name operationName: string; @@ -85,6 +87,7 @@ export interface APISchema { // The pagination strategy that do paginate when querying // If not set pagination, then API request not provide the field to do it pagination?: PaginationSchema; + exampleParameter?: ExampleParameter; } export interface BuiltArtifact {