From 743ef6bf06648661311ae5e0226926e51beb2531 Mon Sep 17 00:00:00 2001 From: Ivan Tsai Date: Tue, 2 Aug 2022 13:51:49 +0800 Subject: [PATCH 01/20] refactor(core): unite extension loaders MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add extension loader and its module. - Update import path from ‘@vulcan-sql/core/containers’ to '@vulcan-sql/core/types' to prevent improper dependencies. - Update extension config from string array to object to receive module aliases. - Update validatorLoader to receive imported classes. - Migrate all core extensions to new style. --- packages/core/src/containers/container.ts | 8 +- .../core/src/containers/modules/extension.ts | 23 +++ packages/core/src/containers/modules/index.ts | 1 + .../src/containers/modules/templateEngine.ts | 12 +- .../src/containers/modules/validatorLoader.ts | 7 +- packages/core/src/containers/types.ts | 6 +- packages/core/src/index.ts | 1 + .../localFilePersistentStore.ts | 2 +- .../artifact-builder/vulcanArtifactBuilder.ts | 2 +- .../lib/extension-loader/extensionLoader.ts | 108 ++++++++++++ .../core/src/lib/extension-loader/index.ts | 1 + .../custom-error/errorTagBuilder.ts | 18 +- .../custom-error/errorTagRunner.ts | 7 +- .../built-in-extensions/index.ts | 6 + .../query-builder/executorBuilder.ts | 6 +- .../query-builder/executorRunner.ts | 3 +- .../query-builder/reqTagBuilder.ts | 23 +-- .../query-builder/reqTagRunner.ts | 16 +- .../sql-helper/uniqueFilterBuilder.ts | 6 +- .../sql-helper/uniqueFilterRunner.ts | 3 +- .../validator/filterChecker.ts | 27 ++- .../validator/parametersChecker.ts | 21 +-- .../template-engine/extension-loader/index.ts | 3 - .../extension-loader/loader.ts | 39 ---- .../extension-loader/models.ts | 166 ------------------ .../helpers.ts | 2 +- .../template-engine/extension-utils/index.ts | 2 + .../extension-utils/interfaces.ts | 27 +++ .../core/src/lib/template-engine/index.ts | 2 +- .../lib/template-engine/nunjucksCompiler.ts | 33 ++-- .../fileTemplateProvider.ts | 2 +- .../src/lib/template-engine/templateEngine.ts | 2 +- .../built-in-validators/dateTypeValidator.ts | 8 +- .../integerTypeValidator.ts | 8 +- .../built-in-validators/requiredValidator.ts | 8 +- .../stringTypeValidator.ts | 9 +- .../built-in-validators/uuidTypeValidator.ts | 8 +- packages/core/src/lib/validators/index.ts | 1 - packages/core/src/lib/validators/validator.ts | 14 -- .../src/lib/validators/validatorLoader.ts | 75 ++------ packages/core/src/models/coreOptions.ts | 5 +- packages/core/src/models/extensions/base.ts | 16 ++ .../core/src/models/extensions/decorators.ts | 16 ++ .../src/models/extensions/filterBuilder.ts | 8 + .../src/models/extensions/filterRunner.ts | 26 +++ packages/core/src/models/extensions/index.ts | 8 + .../src/models/extensions/inputValidator.ts | 17 ++ .../core/src/models/extensions/tagBuilder.ts | 48 +++++ .../core/src/models/extensions/tagRunner.ts | 58 ++++++ .../src/models/extensions/templateEngine.ts | 19 ++ packages/core/src/models/index.ts | 1 + packages/core/src/options/artifactBuilder.ts | 2 +- packages/core/src/options/templateEngine.ts | 2 +- .../localFilePersistentStore.spec.ts | 2 +- .../vulcanArtifactBuilder.spec.ts | 2 +- .../core/test/containers/continer.spec.ts | 2 +- .../extension-loader/extensionLoader.spec.ts | 142 +++++++++++++++ .../test-extension/defaultArray.ts | 11 ++ .../test-extension/defaultObject.ts | 14 ++ .../extension-loader/test-extension/export.ts | 9 + .../test-extension/withoutExtends.ts | 1 + .../template-engine/externalExtension.spec.ts | 8 +- .../template-engine/nunjuckCompiler.spec.ts | 2 +- .../fileTemplateProvider.spec.ts | 2 +- .../fileTemplateProviderError.spec.ts | 2 +- .../template-engine/templateEngine.spec.ts | 2 +- .../core/test/template-engine/testCompiler.ts | 14 +- .../test/template-engine/testExtension.ts | 10 +- .../custom-validators1/index.ts | 27 ++- .../custom-validators2/index.ts | 27 ++- .../custom-validators3/index.ts | 27 ++- .../test/validators/validatorLoader.spec.ts | 121 +++++++------ tsconfig.base.json | 1 + 73 files changed, 848 insertions(+), 520 deletions(-) create mode 100644 packages/core/src/containers/modules/extension.ts create mode 100644 packages/core/src/lib/extension-loader/extensionLoader.ts create mode 100644 packages/core/src/lib/extension-loader/index.ts create mode 100644 packages/core/src/lib/template-engine/built-in-extensions/index.ts delete mode 100644 packages/core/src/lib/template-engine/extension-loader/index.ts delete mode 100644 packages/core/src/lib/template-engine/extension-loader/loader.ts delete mode 100644 packages/core/src/lib/template-engine/extension-loader/models.ts rename packages/core/src/lib/template-engine/{extension-loader => extension-utils}/helpers.ts (98%) create mode 100644 packages/core/src/lib/template-engine/extension-utils/index.ts create mode 100644 packages/core/src/lib/template-engine/extension-utils/interfaces.ts delete mode 100644 packages/core/src/lib/validators/validator.ts create mode 100644 packages/core/src/models/extensions/base.ts create mode 100644 packages/core/src/models/extensions/decorators.ts create mode 100644 packages/core/src/models/extensions/filterBuilder.ts create mode 100644 packages/core/src/models/extensions/filterRunner.ts create mode 100644 packages/core/src/models/extensions/index.ts create mode 100644 packages/core/src/models/extensions/inputValidator.ts create mode 100644 packages/core/src/models/extensions/tagBuilder.ts create mode 100644 packages/core/src/models/extensions/tagRunner.ts create mode 100644 packages/core/src/models/extensions/templateEngine.ts create mode 100644 packages/core/test/extension-loader/extensionLoader.spec.ts create mode 100644 packages/core/test/extension-loader/test-extension/defaultArray.ts create mode 100644 packages/core/test/extension-loader/test-extension/defaultObject.ts create mode 100644 packages/core/test/extension-loader/test-extension/export.ts create mode 100644 packages/core/test/extension-loader/test-extension/withoutExtends.ts diff --git a/packages/core/src/containers/container.ts b/packages/core/src/containers/container.ts index f9b14fa7..3899727b 100644 --- a/packages/core/src/containers/container.ts +++ b/packages/core/src/containers/container.ts @@ -1,5 +1,6 @@ import { ICoreOptions } from '@vulcan-sql/core/models'; import { Container as InversifyContainer } from 'inversify'; +import { extensionModule } from './modules'; import { artifactBuilderModule, executorModule, @@ -18,11 +19,10 @@ export class Container { this.inversifyContainer.load(artifactBuilderModule(options.artifact)); await this.inversifyContainer.loadAsync(executorModule()); await this.inversifyContainer.loadAsync( - templateEngineModule(options.template, options.extensions || []) - ); - await this.inversifyContainer.loadAsync( - validatorLoaderModule(options.extensions) + templateEngineModule(options.template) ); + await this.inversifyContainer.loadAsync(validatorLoaderModule()); + await this.inversifyContainer.loadAsync(extensionModule(options)); } public getInversifyContainer() { diff --git a/packages/core/src/containers/modules/extension.ts b/packages/core/src/containers/modules/extension.ts new file mode 100644 index 00000000..39a11239 --- /dev/null +++ b/packages/core/src/containers/modules/extension.ts @@ -0,0 +1,23 @@ +import { AsyncContainerModule } from 'inversify'; +import { ExtensionLoader } from '../../lib/extension-loader'; +import { ICoreOptions } from '../../models/coreOptions'; +import templateEngineModules from '../../lib/template-engine/built-in-extensions'; +import validatorModule from '../../lib/validators/built-in-validators'; + +export const extensionModule = (options: ICoreOptions) => + new AsyncContainerModule(async (bind) => { + const loader = new ExtensionLoader(options); + // Internal extension modules + + // Template engine (multiple modules) + for (const templateEngineModule of templateEngineModules) { + loader.loadInternalExtensionModule(templateEngineModule); + } + // Validator (single module) + loader.loadInternalExtensionModule(validatorModule); + + // External extension modules + await loader.loadExternalExtensionModules(); + + loader.bindExtensions(bind); + }); diff --git a/packages/core/src/containers/modules/index.ts b/packages/core/src/containers/modules/index.ts index d8a74aec..0d81815b 100644 --- a/packages/core/src/containers/modules/index.ts +++ b/packages/core/src/containers/modules/index.ts @@ -2,3 +2,4 @@ export * from './artifactBuilder'; export * from './executor'; export * from './templateEngine'; export * from './validatorLoader'; +export * from './extension'; diff --git a/packages/core/src/containers/modules/templateEngine.ts b/packages/core/src/containers/modules/templateEngine.ts index 528f5e30..8e0a7e21 100644 --- a/packages/core/src/containers/modules/templateEngine.ts +++ b/packages/core/src/containers/modules/templateEngine.ts @@ -1,4 +1,4 @@ -import { TYPES } from '@vulcan-sql/core/containers'; +import { TYPES } from '@vulcan-sql/core/types'; import { ITemplateEngineOptions, TemplateProviderType, @@ -14,14 +14,9 @@ import { import { AsyncContainerModule, interfaces } from 'inversify'; 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, - extensions: string[] -) => +export const templateEngineModule = (options: ITemplateEngineOptions) => new AsyncContainerModule(async (bind) => { // Options bind( @@ -71,7 +66,4 @@ export const templateEngineModule = ( bind(TYPES.TemplateEngine) .to(TemplateEngine) .inSingletonScope(); - - // Load Extensions - await bindExtensions(bind, extensions); }); diff --git a/packages/core/src/containers/modules/validatorLoader.ts b/packages/core/src/containers/modules/validatorLoader.ts index 26e3a94c..02635c3d 100644 --- a/packages/core/src/containers/modules/validatorLoader.ts +++ b/packages/core/src/containers/modules/validatorLoader.ts @@ -1,14 +1,9 @@ import { AsyncContainerModule } from 'inversify'; import { IValidatorLoader, ValidatorLoader } from '@vulcan-sql/core/validators'; import { TYPES } from '../types'; -import { SourceOfExtensions } from '../../models/coreOptions'; -export const validatorLoaderModule = (extensions?: SourceOfExtensions) => +export const validatorLoaderModule = () => new AsyncContainerModule(async (bind) => { - // SourceOfExtensions - bind(TYPES.SourceOfExtensions).toConstantValue( - extensions || [] - ); // Validator Loader bind(TYPES.ValidatorLoader) .to(ValidatorLoader) diff --git a/packages/core/src/containers/types.ts b/packages/core/src/containers/types.ts index cd573554..c9abeda0 100644 --- a/packages/core/src/containers/types.ts +++ b/packages/core/src/containers/types.ts @@ -25,6 +25,8 @@ export const TYPES = { DataSource: Symbol.for('DataSource'), // Validator ValidatorLoader: Symbol.for('ValidatorLoader'), - // source of extensions - SourceOfExtensions: Symbol.for('SourceOfExtensions'), + // Extensions + ExtensionConfig: Symbol.for('ExtensionConfig'), + Extension_TemplateEngine: Symbol.for('Extension_TemplateEngine'), + Extension_InputValidator: Symbol.for('Extension_InputValidator'), }; diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index a0f04c50..2e1a7a8e 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -4,6 +4,7 @@ export * from './lib/template-engine'; export * from './lib/artifact-builder'; export * from './lib/data-query'; export * from './lib/data-source'; +export * from './lib/extension-loader'; export * from './models'; export * from './containers'; export * from './options'; diff --git a/packages/core/src/lib/artifact-builder/persistent-stores/localFilePersistentStore.ts b/packages/core/src/lib/artifact-builder/persistent-stores/localFilePersistentStore.ts index c1a2fcdd..33800bca 100644 --- a/packages/core/src/lib/artifact-builder/persistent-stores/localFilePersistentStore.ts +++ b/packages/core/src/lib/artifact-builder/persistent-stores/localFilePersistentStore.ts @@ -1,7 +1,7 @@ import { PersistentStore } from './persistentStore'; import { promises as fs } from 'fs'; import { injectable, inject } from 'inversify'; -import { TYPES } from '@vulcan-sql/core/containers'; +import { TYPES } from '@vulcan-sql/core/types'; import { IArtifactBuilderOptions } from '@vulcan-sql/core/models'; @injectable() diff --git a/packages/core/src/lib/artifact-builder/vulcanArtifactBuilder.ts b/packages/core/src/lib/artifact-builder/vulcanArtifactBuilder.ts index e4ac9198..8e402a98 100644 --- a/packages/core/src/lib/artifact-builder/vulcanArtifactBuilder.ts +++ b/packages/core/src/lib/artifact-builder/vulcanArtifactBuilder.ts @@ -2,7 +2,7 @@ import { Artifact, ArtifactBuilder } from './artifactBuilder'; import { PersistentStore } from './persistent-stores'; import { Serializer } from './serializers'; import { inject, injectable, interfaces } from 'inversify'; -import { TYPES } from '@vulcan-sql/core/containers'; +import { TYPES } from '@vulcan-sql/core/types'; import { IArtifactBuilderOptions } from '../../models/artifactBuilderOptions'; @injectable() diff --git a/packages/core/src/lib/extension-loader/extensionLoader.ts b/packages/core/src/lib/extension-loader/extensionLoader.ts new file mode 100644 index 00000000..c1594c7c --- /dev/null +++ b/packages/core/src/lib/extension-loader/extensionLoader.ts @@ -0,0 +1,108 @@ +import { ExtensionBase, ICoreOptions } from '@vulcan-sql/core/models'; +import { interfaces } from 'inversify'; +import { ClassType, defaultImport } from '../utils'; +import { + EXTENSION_NAME_METADATA_KEY, + EXTENSION_TYPE_METADATA_KEY, +} from '../../models/extensions/decorators'; +import 'reflect-metadata'; +import { TYPES } from '../../containers/types'; +import { chain, isArray, values } from 'lodash'; + +type Extension = ClassType; + +export type ExtensionModuleEntry = Extension[] | Record; + +export class ExtensionLoader { + private extensionRegistry = new Map< + symbol, + { name: string; extension: Extension }[] + >(); + private config: ICoreOptions; + private bound = false; + + constructor(config: ICoreOptions) { + this.config = config; + } + + public async loadExternalExtensionModules() { + if (this.bound) + throw new Error( + `We must load all extensions before call bindExtension function` + ); + + const extensionModules = + // {moduleA: 'nameA', moduleB: ['nameB', 'nameC']} + chain(this.config?.extensions || {}) + // [['moduleA', 'nameA'], ['moduleB',['nameB', 'nameC']]] + .toPairs() + // [{alias: 'moduleA', path: 'nameA'}, {alias: 'moduleB', path: 'nameB'}, {alias: 'moduleB', path: 'nameC'}] + .flatMap(([alias, path]) => + (typeof path === 'string' ? [path] : path).map((p) => ({ + alias, + path: p, + })) + ) + .value(); + + for (const module of extensionModules) { + const moduleEntry = ( + await defaultImport(module.path) + )[0]; + const extensions = this.flattenExtensions(moduleEntry); + extensions.forEach((extension) => + this.loadExtension(module.alias, extension) + ); + } + } + + public loadInternalExtensionModule(moduleEntry: ExtensionModuleEntry) { + if (this.bound) + throw new Error( + `We must load all extensions before call bindExtension function` + ); + + const extensions = this.flattenExtensions(moduleEntry); + + for (const extension of extensions) { + const name = Reflect.getMetadata(EXTENSION_NAME_METADATA_KEY, extension); + if (name === undefined) + throw new Error( + `Internal extension must have @VulcanInternalExtension decorator` + ); + this.loadExtension(name, extension); + } + } + + public bindExtensions(bind: interfaces.Bind) { + for (const type of this.extensionRegistry.keys()) { + this.extensionRegistry.get(type)!.forEach(({ name, extension }) => { + bind(type).to(extension); + bind(TYPES.ExtensionConfig) + .toConstantValue(name.length > 0 ? this.config[name] : undefined) + .whenInjectedInto(extension); + }); + } + this.bound = true; + } + + private loadExtension(name: string, extension: Extension) { + const extensionType = Reflect.getMetadata( + EXTENSION_TYPE_METADATA_KEY, + extension + ); + if (!extensionType) + throw new Error( + `Extension must have @VulcanExtension decorator, have you use extend the correct super class?` + ); + if (!this.extensionRegistry.has(extensionType)) + this.extensionRegistry.set(extensionType, []); + + this.extensionRegistry.get(extensionType)!.push({ name, extension }); + } + + private flattenExtensions(moduleEntry: ExtensionModuleEntry): Extension[] { + if (isArray(moduleEntry)) return moduleEntry; + return values(moduleEntry); + } +} diff --git a/packages/core/src/lib/extension-loader/index.ts b/packages/core/src/lib/extension-loader/index.ts new file mode 100644 index 00000000..d5792952 --- /dev/null +++ b/packages/core/src/lib/extension-loader/index.ts @@ -0,0 +1 @@ +export * from './extensionLoader'; diff --git a/packages/core/src/lib/template-engine/built-in-extensions/custom-error/errorTagBuilder.ts b/packages/core/src/lib/template-engine/built-in-extensions/custom-error/errorTagBuilder.ts index 8330d868..6db31988 100644 --- a/packages/core/src/lib/template-engine/built-in-extensions/custom-error/errorTagBuilder.ts +++ b/packages/core/src/lib/template-engine/built-in-extensions/custom-error/errorTagBuilder.ts @@ -1,11 +1,7 @@ -import { - OnAstVisit, - ProvideMetadata, - TagBuilder, -} from '../../extension-loader'; import * as nunjucks from 'nunjucks'; import { chain } from 'lodash'; import { METADATA_NAME } from './constants'; +import { TagBuilder, VulcanInternalExtension } from '@vulcan-sql/core/models'; interface ErrorCode { code: string; @@ -13,13 +9,11 @@ interface ErrorCode { columnNo: number; } -export class ErrorTagBuilder - extends TagBuilder - implements OnAstVisit, ProvideMetadata -{ +@VulcanInternalExtension() +export class ErrorTagBuilder extends TagBuilder { public tags = ['error']; + public override metadataName = METADATA_NAME; private errorCodes: ErrorCode[] = []; - public metadataName = METADATA_NAME; public parse(parser: nunjucks.parser.Parser, nodes: typeof nunjucks.nodes) { // get the tag token @@ -39,7 +33,7 @@ export class ErrorTagBuilder return this.createAsyncExtensionNode(errorMessage, []); } - public onVisit(node: nunjucks.nodes.Node) { + public override onVisit(node: nunjucks.nodes.Node) { if (node instanceof nunjucks.nodes.CallExtension) { if (node.extName !== this.getName()) return; @@ -55,7 +49,7 @@ export class ErrorTagBuilder } } - public getMetadata() { + public override getMetadata() { return { errorCodes: chain(this.errorCodes) .groupBy('code') diff --git a/packages/core/src/lib/template-engine/built-in-extensions/custom-error/errorTagRunner.ts b/packages/core/src/lib/template-engine/built-in-extensions/custom-error/errorTagRunner.ts index e11f9913..41a51d12 100644 --- a/packages/core/src/lib/template-engine/built-in-extensions/custom-error/errorTagRunner.ts +++ b/packages/core/src/lib/template-engine/built-in-extensions/custom-error/errorTagRunner.ts @@ -1,5 +1,10 @@ -import { TagRunner, TagRunnerOptions } from '../../extension-loader'; +import { + TagRunner, + TagRunnerOptions, + VulcanInternalExtension, +} from '@vulcan-sql/core/models'; +@VulcanInternalExtension() export class ErrorTagRunner extends TagRunner { public tags = ['error']; diff --git a/packages/core/src/lib/template-engine/built-in-extensions/index.ts b/packages/core/src/lib/template-engine/built-in-extensions/index.ts new file mode 100644 index 00000000..980f211a --- /dev/null +++ b/packages/core/src/lib/template-engine/built-in-extensions/index.ts @@ -0,0 +1,6 @@ +import CustomError from './custom-error'; +import QueryBuilder from './query-builder'; +import SqlHelper from './sql-helper'; +import Validator from './validator'; + +export default [CustomError, QueryBuilder, SqlHelper, Validator]; diff --git a/packages/core/src/lib/template-engine/built-in-extensions/query-builder/executorBuilder.ts b/packages/core/src/lib/template-engine/built-in-extensions/query-builder/executorBuilder.ts index a05f0877..ff6d81b5 100644 --- a/packages/core/src/lib/template-engine/built-in-extensions/query-builder/executorBuilder.ts +++ b/packages/core/src/lib/template-engine/built-in-extensions/query-builder/executorBuilder.ts @@ -1,6 +1,10 @@ -import { FilterBuilder } from '../../extension-loader'; +import { + FilterBuilder, + VulcanInternalExtension, +} from '@vulcan-sql/core/models'; import { EXECUTE_FILTER_NAME } from './constants'; +@VulcanInternalExtension() export class ExecutorBuilder extends FilterBuilder { public filterName = EXECUTE_FILTER_NAME; } diff --git a/packages/core/src/lib/template-engine/built-in-extensions/query-builder/executorRunner.ts b/packages/core/src/lib/template-engine/built-in-extensions/query-builder/executorRunner.ts index 0b44cf0b..14554ebe 100644 --- a/packages/core/src/lib/template-engine/built-in-extensions/query-builder/executorRunner.ts +++ b/packages/core/src/lib/template-engine/built-in-extensions/query-builder/executorRunner.ts @@ -1,7 +1,8 @@ import { IDataQueryBuilder } from '@vulcan-sql/core/data-query'; -import { FilterRunner } from '../../extension-loader'; +import { FilterRunner, VulcanInternalExtension } from '@vulcan-sql/core/models'; import { EXECUTE_FILTER_NAME } from './constants'; +@VulcanInternalExtension() export class ExecutorRunner extends FilterRunner { public filterName = EXECUTE_FILTER_NAME; diff --git a/packages/core/src/lib/template-engine/built-in-extensions/query-builder/reqTagBuilder.ts b/packages/core/src/lib/template-engine/built-in-extensions/query-builder/reqTagBuilder.ts index e26c8645..70ca48f4 100644 --- a/packages/core/src/lib/template-engine/built-in-extensions/query-builder/reqTagBuilder.ts +++ b/packages/core/src/lib/template-engine/built-in-extensions/query-builder/reqTagBuilder.ts @@ -1,10 +1,4 @@ -import { - OnAstVisit, - ProvideMetadata, - ReplaceChildFunc, - TagBuilder, - visitChildren, -} from '../../extension-loader'; +import { ReplaceChildFunc, visitChildren } from '../../extension-utils'; import * as nunjucks from 'nunjucks'; import { EXECUTE_COMMAND_NAME, @@ -13,18 +7,17 @@ import { METADATA_NAME, REFERENCE_SEARCH_MAX_DEPTH, } from './constants'; +import { TagBuilder, VulcanInternalExtension } from '@vulcan-sql/core/models'; interface DeclarationLocation { lineNo: number; colNo: number; } -export class ReqTagBuilder - extends TagBuilder - implements OnAstVisit, ProvideMetadata -{ +@VulcanInternalExtension() +export class ReqTagBuilder extends TagBuilder { public tags = ['req']; - public metadataName = METADATA_NAME; + public override metadataName = METADATA_NAME; private root?: nunjucks.nodes.Root; private hasMainBuilder = false; private variableList = new Map(); @@ -97,7 +90,7 @@ export class ReqTagBuilder return this.createAsyncExtensionNode(argsNodeToPass, [requestQuery]); } - public onVisit(node: nunjucks.nodes.Node) { + public override onVisit(node: nunjucks.nodes.Node) { // save the root if (node instanceof nunjucks.nodes.Root) { this.root = node; @@ -112,13 +105,13 @@ export class ReqTagBuilder } } - public finish() { + public override finish() { if (!this.hasMainBuilder) { this.wrapOutputWithBuilder(); } } - public getMetadata() { + public override getMetadata() { return { finalBuilderName: FINIAL_BUILDER_NAME, }; diff --git a/packages/core/src/lib/template-engine/built-in-extensions/query-builder/reqTagRunner.ts b/packages/core/src/lib/template-engine/built-in-extensions/query-builder/reqTagRunner.ts index a1b93d7d..3f7694c3 100644 --- a/packages/core/src/lib/template-engine/built-in-extensions/query-builder/reqTagRunner.ts +++ b/packages/core/src/lib/template-engine/built-in-extensions/query-builder/reqTagRunner.ts @@ -1,15 +1,23 @@ -import { TYPES } from '@vulcan-sql/core/containers'; +import { TYPES } from '@vulcan-sql/core/types'; import { IExecutor } from '@vulcan-sql/core/data-query'; import { inject } from 'inversify'; -import { TagRunnerOptions, TagRunner } from '../../extension-loader'; +import { + TagRunner, + TagRunnerOptions, + VulcanInternalExtension, +} from '@vulcan-sql/core/models'; import { FINIAL_BUILDER_NAME } from './constants'; +@VulcanInternalExtension() export class ReqTagRunner extends TagRunner { public tags = ['req']; private executor: IExecutor; - constructor(@inject(TYPES.Executor) executor: IExecutor) { - super(); + constructor( + @inject(TYPES.ExtensionConfig) config: any, + @inject(TYPES.Executor) executor: IExecutor + ) { + super(config); this.executor = executor; } diff --git a/packages/core/src/lib/template-engine/built-in-extensions/sql-helper/uniqueFilterBuilder.ts b/packages/core/src/lib/template-engine/built-in-extensions/sql-helper/uniqueFilterBuilder.ts index 9af90c8b..2153d0e1 100644 --- a/packages/core/src/lib/template-engine/built-in-extensions/sql-helper/uniqueFilterBuilder.ts +++ b/packages/core/src/lib/template-engine/built-in-extensions/sql-helper/uniqueFilterBuilder.ts @@ -1,5 +1,9 @@ -import { FilterBuilder } from '../../extension-loader'; +import { + FilterBuilder, + VulcanInternalExtension, +} from '@vulcan-sql/core/models'; +@VulcanInternalExtension() export class UniqueFilterBuilder extends FilterBuilder { public filterName = 'unique'; } diff --git a/packages/core/src/lib/template-engine/built-in-extensions/sql-helper/uniqueFilterRunner.ts b/packages/core/src/lib/template-engine/built-in-extensions/sql-helper/uniqueFilterRunner.ts index 49a2ee7a..1d4e8873 100644 --- a/packages/core/src/lib/template-engine/built-in-extensions/sql-helper/uniqueFilterRunner.ts +++ b/packages/core/src/lib/template-engine/built-in-extensions/sql-helper/uniqueFilterRunner.ts @@ -1,6 +1,7 @@ +import { FilterRunner, VulcanInternalExtension } from '@vulcan-sql/core/models'; import { uniq, uniqBy } from 'lodash'; -import { FilterRunner } from '../../extension-loader'; +@VulcanInternalExtension() export class UniqueFilterRunner extends FilterRunner { public filterName = 'unique'; public async transform({ diff --git a/packages/core/src/lib/template-engine/built-in-extensions/validator/filterChecker.ts b/packages/core/src/lib/template-engine/built-in-extensions/validator/filterChecker.ts index 0c7f3e74..ece0a7f4 100644 --- a/packages/core/src/lib/template-engine/built-in-extensions/validator/filterChecker.ts +++ b/packages/core/src/lib/template-engine/built-in-extensions/validator/filterChecker.ts @@ -1,31 +1,30 @@ -import { TYPES } from '@vulcan-sql/core/containers'; +import { TYPES } from '@vulcan-sql/core/types'; import { inject, named } from 'inversify'; -import { - CompileTimeExtension, - OnAstVisit, - ProvideMetadata, -} from '../../extension-loader'; import { FILTER_METADATA_NAME } from './constants'; import * as nunjucks from 'nunjucks'; +import { + CompileTimeExtension, + VulcanInternalExtension, +} from '@vulcan-sql/core/models'; -export class FilterChecker - extends CompileTimeExtension - implements OnAstVisit, ProvideMetadata -{ - public metadataName = FILTER_METADATA_NAME; +@VulcanInternalExtension() +export class FilterChecker extends CompileTimeExtension { + public override metadataName = FILTER_METADATA_NAME; private env: nunjucks.Environment; private filters = new Set(); constructor( + @inject(TYPES.ExtensionConfig) + config: any, @inject(TYPES.CompilerEnvironment) @named('compileTime') compileTimeEnv: nunjucks.Environment ) { - super(); + super(config); this.env = compileTimeEnv; } - public onVisit(node: nunjucks.nodes.Node) { + public override onVisit(node: nunjucks.nodes.Node) { if (node instanceof nunjucks.nodes.Filter) { if ( node.name instanceof nunjucks.nodes.Symbol || @@ -39,7 +38,7 @@ export class FilterChecker } } - public getMetadata() { + public override getMetadata() { return Array.from(this.filters); } } diff --git a/packages/core/src/lib/template-engine/built-in-extensions/validator/parametersChecker.ts b/packages/core/src/lib/template-engine/built-in-extensions/validator/parametersChecker.ts index 9e818b56..13b303c3 100644 --- a/packages/core/src/lib/template-engine/built-in-extensions/validator/parametersChecker.ts +++ b/packages/core/src/lib/template-engine/built-in-extensions/validator/parametersChecker.ts @@ -1,10 +1,9 @@ -import { chain } from 'lodash'; -import * as nunjucks from 'nunjucks'; import { CompileTimeExtension, - OnAstVisit, - ProvideMetadata, -} from '../../extension-loader'; + VulcanInternalExtension, +} from '@vulcan-sql/core/models'; +import { chain } from 'lodash'; +import * as nunjucks from 'nunjucks'; import { LOOK_UP_PARAMETER, PARAMETER_METADATA_NAME, @@ -17,14 +16,12 @@ interface Parameter { columnNo: number; } -export class ParametersChecker - extends CompileTimeExtension - implements OnAstVisit, ProvideMetadata -{ - public metadataName = PARAMETER_METADATA_NAME; +@VulcanInternalExtension() +export class ParametersChecker extends CompileTimeExtension { + public override metadataName = PARAMETER_METADATA_NAME; private parameters: Parameter[] = []; - public onVisit(node: nunjucks.nodes.Node): void { + public override onVisit(node: nunjucks.nodes.Node): void { if (node instanceof nunjucks.nodes.LookupVal) { let name = node.val.value; let parent: typeof node.target | null = node.target; @@ -53,7 +50,7 @@ export class ParametersChecker } } - public getMetadata() { + public override getMetadata() { return chain(this.parameters) .groupBy('name') .values() diff --git a/packages/core/src/lib/template-engine/extension-loader/index.ts b/packages/core/src/lib/template-engine/extension-loader/index.ts deleted file mode 100644 index 0d0ec7e6..00000000 --- a/packages/core/src/lib/template-engine/extension-loader/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export * from './models'; -export * from './helpers'; -export * from './loader'; diff --git a/packages/core/src/lib/template-engine/extension-loader/loader.ts b/packages/core/src/lib/template-engine/extension-loader/loader.ts deleted file mode 100644 index 971253f5..00000000 --- a/packages/core/src/lib/template-engine/extension-loader/loader.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { promises as fs } from 'fs'; -import * as path from 'path'; -import { flatten } from 'lodash'; -import { interfaces } from 'inversify'; -import { TYPES } from '@vulcan-sql/core/containers'; - -export const importExtensions = async (folder: string) => { - const extensions = await import(folder); - return extensions.default || []; -}; - -export const bindExtensions = async ( - bind: interfaces.Bind, - externalExtensionNames: string[] -) => { - const builtInExtensionNames = ( - await fs.readdir(path.join(__dirname, '..', 'built-in-extensions')) - ).filter((name) => name !== 'index.ts'); - - const builtInExtensions = flatten( - await Promise.all( - builtInExtensionNames.map((name) => - importExtensions( - path.join(__dirname, '..', 'built-in-extensions', name) - ) - ) - ) - ); - - const externalExtensions = flatten( - await Promise.all( - externalExtensionNames.map((name) => importExtensions(name)) - ) - ); - - [...builtInExtensions, ...externalExtensions].forEach((extension) => { - bind(TYPES.CompilerExtension).to(extension).inSingletonScope(); - }); -}; diff --git a/packages/core/src/lib/template-engine/extension-loader/models.ts b/packages/core/src/lib/template-engine/extension-loader/models.ts deleted file mode 100644 index 7830e87b..00000000 --- a/packages/core/src/lib/template-engine/extension-loader/models.ts +++ /dev/null @@ -1,166 +0,0 @@ -import { sortBy } from 'lodash'; -import * as nunjucks from 'nunjucks'; -import { injectable } from 'inversify'; - -export type TagExtensionContentArgGetter = () => Promise; - -export type TagExtensionArgTypes = string | number | boolean; - -export interface TagRunnerOptions { - context: any; - args: TagExtensionArgTypes[]; - contentArgs: TagExtensionContentArgGetter[]; -} - -export type Extension = RuntimeExtension | CompileTimeExtension; - -@injectable() -export abstract class RuntimeExtension {} - -@injectable() -export abstract class CompileTimeExtension {} - -export abstract class TagBuilder extends CompileTimeExtension { - abstract tags: string[]; - abstract parse( - parser: nunjucks.parser.Parser, - nodes: typeof nunjucks.nodes, - lexer: typeof nunjucks.lexer - ): nunjucks.nodes.Node; - - public set __name(_) { - // ignore it - } - - public get __name() { - return this.getName(); - } - - public getName() { - return sortBy(this.tags).join('_'); - } - - protected createAsyncExtensionNode( - /** - * The arguments of this extension, they'll be rendered and passed to run function. - * It usually contains the configuration of the extension, e.g. {% req variable %} The variable name of req extension. - * Note that these arguments will be pass to run function directly: Literal('123') => "123", so adding Output nodes causes compiling issues. Output("123") => t += "123" - */ - argsNodeList: nunjucks.nodes.NodeList, - /** The content (usually the body) of this extension, they'll be passed to run function as render functions - * It usually contains the Output of your extension, e.g. {% req variable %} select * from user {% endreq %}, the "select * from user" should be put in this field. - * Note that these nodes will be rendered as the output of template: Output("123") => t = ""; t += "123", so adding nodes with no output like Symbol, Literal ... might cause compiling issues. Literal('123') => t = ""; 123 - */ - contentNodes: nunjucks.nodes.Node[] = [] - ) { - return new nunjucks.nodes.CallExtensionAsync( - this.getName(), - '__run', - argsNodeList, - contentNodes - ); - } -} - -export abstract class TagRunner extends RuntimeExtension { - abstract tags: string[]; - abstract run( - options: TagRunnerOptions - ): Promise; - - public __run(...originalArgs: any[]) { - const context = originalArgs[0]; - const callback = originalArgs[originalArgs.length - 1]; - const args = originalArgs - .slice(1, originalArgs.length - 1) - .filter((value) => typeof value !== 'function'); - const contentArgs = originalArgs - .slice(1, originalArgs.length - 1) - .filter((value) => typeof value === 'function') - .map((cb) => () => { - return new Promise((resolve, reject) => { - cb((err: any, result: any) => { - if (err) reject(err); - else resolve(result); - }); - }); - }); - - this.run({ context, args, contentArgs }) - .then((result) => callback(null, result)) - .catch((err) => callback(err, null)); - } - - public set __name(_) { - // ignore it - } - - public get __name() { - return this.getName(); - } - - public getName() { - return sortBy(this.tags).join('_'); - } -} - -export abstract class FilterBuilder extends CompileTimeExtension { - abstract filterName: string; -} - -export abstract class FilterRunner extends RuntimeExtension { - abstract filterName: string; - abstract transform(options: { - value: V; - args: Record; - }): Promise; - - public __transform(value: any, ...args: any[]) { - const callback = args[args.length - 1]; - const otherArgs = args.slice(0, args.length - 1); - this.transform({ - value, - args: otherArgs, - }) - .then((res) => callback(null, res)) - .catch((err) => callback(err, null)); - } -} - -export const implementedOnAstVisit = (source: any): source is OnAstVisit => { - return !!source.onVisit; -}; - -/** - * Visit every nodes after compiling, you can extract metadata from them, or even modify some nodes. - */ -export interface OnAstVisit { - onVisit(node: nunjucks.nodes.Node): void; - finish?: () => void; -} - -export const implementedProvideMetadata = ( - source: any -): source is ProvideMetadata => { - return !!source.metadataName && !!source.getMetadata; -}; - -/** - * Providing metadata after compiling - */ -export interface ProvideMetadata { - metadataName: string; - getMetadata(): any; -} - -export const implementedOnInit = (source: any): source is OnInit => { - return !!source.onInit; -}; - -/** - * Init function will be called before compiling or executing, you can do asynchronous jobs like loading config, read files ...etc. in this function. - * This function will be called only once even if there are multiple templates exist. - */ -export interface OnInit { - onInit(): Promise; -} diff --git a/packages/core/src/lib/template-engine/extension-loader/helpers.ts b/packages/core/src/lib/template-engine/extension-utils/helpers.ts similarity index 98% rename from packages/core/src/lib/template-engine/extension-loader/helpers.ts rename to packages/core/src/lib/template-engine/extension-utils/helpers.ts index ad78e1f1..020dc37f 100644 --- a/packages/core/src/lib/template-engine/extension-loader/helpers.ts +++ b/packages/core/src/lib/template-engine/extension-utils/helpers.ts @@ -1,5 +1,5 @@ import * as nunjucks from 'nunjucks'; -import { OnAstVisit, ProvideMetadata } from './models'; +import { OnAstVisit, ProvideMetadata } from './interfaces'; export const generateMetadata = (providers: ProvideMetadata[]) => { const metadata = providers.reduce((currentMetadata, provider) => { diff --git a/packages/core/src/lib/template-engine/extension-utils/index.ts b/packages/core/src/lib/template-engine/extension-utils/index.ts new file mode 100644 index 00000000..e0d4085a --- /dev/null +++ b/packages/core/src/lib/template-engine/extension-utils/index.ts @@ -0,0 +1,2 @@ +export * from './interfaces'; +export * from './helpers'; diff --git a/packages/core/src/lib/template-engine/extension-utils/interfaces.ts b/packages/core/src/lib/template-engine/extension-utils/interfaces.ts new file mode 100644 index 00000000..12f08b50 --- /dev/null +++ b/packages/core/src/lib/template-engine/extension-utils/interfaces.ts @@ -0,0 +1,27 @@ +import * as nunjucks from 'nunjucks'; + +export const implementedOnAstVisit = (source: any): source is OnAstVisit => { + return !!source.onVisit; +}; + +/** + * Visit every nodes after compiling, you can extract metadata from them, or even modify some nodes. + */ +export interface OnAstVisit { + onVisit(node: nunjucks.nodes.Node): void; + finish?: () => void; +} + +export const implementedProvideMetadata = ( + source: any +): source is ProvideMetadata => { + return !!source.metadataName && !!source.getMetadata; +}; + +/** + * Providing metadata after compiling + */ +export interface ProvideMetadata { + metadataName: string; + getMetadata(): any; +} diff --git a/packages/core/src/lib/template-engine/index.ts b/packages/core/src/lib/template-engine/index.ts index 32332221..18bbcc11 100644 --- a/packages/core/src/lib/template-engine/index.ts +++ b/packages/core/src/lib/template-engine/index.ts @@ -3,4 +3,4 @@ export * from './compiler'; export * from './template-providers'; export * from './code-loader'; export * from './nunjucksCompiler'; -export * from './extension-loader'; +export * from './extension-utils'; diff --git a/packages/core/src/lib/template-engine/nunjucksCompiler.ts b/packages/core/src/lib/template-engine/nunjucksCompiler.ts index 9a588403..8ee1f6fc 100644 --- a/packages/core/src/lib/template-engine/nunjucksCompiler.ts +++ b/packages/core/src/lib/template-engine/nunjucksCompiler.ts @@ -2,40 +2,41 @@ import { Compiler, CompileResult } from './compiler'; import * as nunjucks from 'nunjucks'; import * as transformer from 'nunjucks/src/transformer'; import { inject, injectable, multiInject, named, optional } from 'inversify'; -import { TYPES } from '@vulcan-sql/core/containers'; +import { TYPES } from '@vulcan-sql/core/types'; import { - CompileTimeExtension, - Extension, - FilterBuilder, - FilterRunner, generateMetadata, implementedOnAstVisit, - implementedOnInit, implementedProvideMetadata, OnAstVisit, ProvideMetadata, + walkAst, +} from './extension-utils'; +import { IDataQueryBuilder } from '../data-query'; +import { + Pagination, + TemplateEngineExtension, RuntimeExtension, + CompileTimeExtension, TagBuilder, TagRunner, - walkAst, -} from './extension-loader'; -import { IDataQueryBuilder } from '../data-query'; -import { Pagination } from '@vulcan-sql/core/models'; + FilterBuilder, + FilterRunner, +} from '@vulcan-sql/core/models'; @injectable() export class NunjucksCompiler implements Compiler { public name = 'nunjucks'; private runtimeEnv: nunjucks.Environment; private compileTimeEnv: nunjucks.Environment; - private extensions: Extension[]; + private extensions: TemplateEngineExtension[]; private astVisitors: OnAstVisit[] = []; private metadataProviders: ProvideMetadata[] = []; private extensionsInitialized = false; constructor( - @multiInject(TYPES.CompilerExtension) + @multiInject(TYPES.Extension_TemplateEngine) @optional() - extensions: Extension[] = [], + extensions: TemplateEngineExtension[] = [], @inject(TYPES.CompilerEnvironment) @named('runtime') runtimeEnv: nunjucks.Environment, @@ -84,7 +85,7 @@ export class NunjucksCompiler implements Compiler { return builder.value(); } - public loadExtension(extension: Extension): void { + public loadExtension(extension: TemplateEngineExtension): void { if (extension instanceof RuntimeExtension) { this.loadRuntimeExtensions(extension); } else if (extension instanceof CompileTimeExtension) { @@ -164,9 +165,7 @@ export class NunjucksCompiler implements Compiler { private async initializeExtensions() { if (this.extensionsInitialized) return; for (const extension of this.extensions) { - if (implementedOnInit(extension)) { - await extension.onInit(); - } + if (extension.activate) await extension.activate(); } this.extensionsInitialized = true; } diff --git a/packages/core/src/lib/template-engine/template-providers/fileTemplateProvider.ts b/packages/core/src/lib/template-engine/template-providers/fileTemplateProvider.ts index f1f287c5..6b4948fa 100644 --- a/packages/core/src/lib/template-engine/template-providers/fileTemplateProvider.ts +++ b/packages/core/src/lib/template-engine/template-providers/fileTemplateProvider.ts @@ -3,7 +3,7 @@ import * as glob from 'glob'; import { promises as fs } from 'fs'; import * as path from 'path'; import { inject, injectable } from 'inversify'; -import { TYPES } from '@vulcan-sql/core/containers'; +import { TYPES } from '@vulcan-sql/core/types'; import { ITemplateEngineOptions } from '@vulcan-sql/core/models'; @injectable() diff --git a/packages/core/src/lib/template-engine/templateEngine.ts b/packages/core/src/lib/template-engine/templateEngine.ts index 9eb4a4ee..4c65d8b3 100644 --- a/packages/core/src/lib/template-engine/templateEngine.ts +++ b/packages/core/src/lib/template-engine/templateEngine.ts @@ -1,7 +1,7 @@ import { Compiler, TemplateMetadata } from './compiler'; import { TemplateProvider } from './template-providers'; import { injectable, inject, interfaces } from 'inversify'; -import { TYPES } from '@vulcan-sql/core/containers'; +import { TYPES } from '@vulcan-sql/core/types'; import { TemplateEngineOptions } from '../../options'; import { Pagination } from '@vulcan-sql/core/models'; import { ICodeLoader } from './code-loader'; diff --git a/packages/core/src/lib/validators/built-in-validators/dateTypeValidator.ts b/packages/core/src/lib/validators/built-in-validators/dateTypeValidator.ts index a5dc6820..dfc30fa4 100644 --- a/packages/core/src/lib/validators/built-in-validators/dateTypeValidator.ts +++ b/packages/core/src/lib/validators/built-in-validators/dateTypeValidator.ts @@ -2,7 +2,10 @@ import * as Joi from 'joi'; import { isUndefined } from 'lodash'; import * as dayjs from 'dayjs'; import customParseFormat = require('dayjs/plugin/customParseFormat'); -import { IValidator } from '../validator'; +import { + InputValidator, + VulcanInternalExtension, +} from '@vulcan-sql/core/models'; // Support custom date format -> dayjs.format(...) dayjs.extend(customParseFormat); @@ -13,7 +16,8 @@ export interface DateInputArgs { format?: string; } -export class DateTypeValidator implements IValidator { +@VulcanInternalExtension() +export class DateTypeValidator extends InputValidator { public readonly name = 'date'; // Validator for arguments schema in schema.yaml, should match DateInputArgs private argsValidator = Joi.object({ diff --git a/packages/core/src/lib/validators/built-in-validators/integerTypeValidator.ts b/packages/core/src/lib/validators/built-in-validators/integerTypeValidator.ts index 3038b4b6..2ec748f1 100644 --- a/packages/core/src/lib/validators/built-in-validators/integerTypeValidator.ts +++ b/packages/core/src/lib/validators/built-in-validators/integerTypeValidator.ts @@ -1,6 +1,9 @@ +import { + InputValidator, + VulcanInternalExtension, +} from '@vulcan-sql/core/models'; import * as Joi from 'joi'; import { isUndefined } from 'lodash'; -import { IValidator } from '../validator'; export interface IntInputArgs { // The integer minimum value @@ -13,7 +16,8 @@ export interface IntInputArgs { less?: number; } -export class IntegerTypeValidator implements IValidator { +@VulcanInternalExtension() +export class IntegerTypeValidator extends InputValidator { public readonly name = 'integer'; // Validator for arguments schema in schema.yaml, should match IntInputArgs private argsValidator = Joi.object({ diff --git a/packages/core/src/lib/validators/built-in-validators/requiredValidator.ts b/packages/core/src/lib/validators/built-in-validators/requiredValidator.ts index e3a73d76..5834f247 100644 --- a/packages/core/src/lib/validators/built-in-validators/requiredValidator.ts +++ b/packages/core/src/lib/validators/built-in-validators/requiredValidator.ts @@ -1,5 +1,8 @@ +import { + InputValidator, + VulcanInternalExtension, +} from '@vulcan-sql/core/models'; import * as Joi from 'joi'; -import { IValidator } from '../validator'; export interface RequiredInputArgs { /** @@ -10,7 +13,8 @@ export interface RequiredInputArgs { } // required means disallow undefined as value -export class RequiredValidator implements IValidator { +@VulcanInternalExtension() +export class RequiredValidator extends InputValidator { public readonly name = 'required'; // Validator for arguments schema in schema.yaml, should match RequiredInputArgs private argsValidator = Joi.object({ diff --git a/packages/core/src/lib/validators/built-in-validators/stringTypeValidator.ts b/packages/core/src/lib/validators/built-in-validators/stringTypeValidator.ts index 7df22718..0e5f0500 100644 --- a/packages/core/src/lib/validators/built-in-validators/stringTypeValidator.ts +++ b/packages/core/src/lib/validators/built-in-validators/stringTypeValidator.ts @@ -1,7 +1,9 @@ +import { + InputValidator, + VulcanInternalExtension, +} from '@vulcan-sql/core/models'; import * as Joi from 'joi'; import { isUndefined } from 'lodash'; -import { IValidator } from '../validator'; - export interface StringInputArgs { // The string regex format pattern format?: string; @@ -13,7 +15,8 @@ export interface StringInputArgs { max?: number; } -export class StringTypeValidator implements IValidator { +@VulcanInternalExtension() +export class StringTypeValidator extends InputValidator { public readonly name = 'string'; // Validator for arguments schema in schema.yaml, should match StringInputArgs private argsValidator = Joi.object({ diff --git a/packages/core/src/lib/validators/built-in-validators/uuidTypeValidator.ts b/packages/core/src/lib/validators/built-in-validators/uuidTypeValidator.ts index b10e9f45..41a413c5 100644 --- a/packages/core/src/lib/validators/built-in-validators/uuidTypeValidator.ts +++ b/packages/core/src/lib/validators/built-in-validators/uuidTypeValidator.ts @@ -1,7 +1,10 @@ +import { + InputValidator, + VulcanInternalExtension, +} from '@vulcan-sql/core/models'; import * as Joi from 'joi'; import { GuidVersions } from 'joi'; import { isUndefined } from 'lodash'; -import { IValidator } from '../validator'; type UUIDVersion = 'uuid_v1' | 'uuid_v4' | 'uuid_v5'; @@ -10,7 +13,8 @@ export interface UUIDInputArgs { version?: UUIDVersion; } -export class UUIDTypeValidator implements IValidator { +@VulcanInternalExtension() +export class UUIDTypeValidator extends InputValidator { public readonly name = 'uuid'; // Validator for arguments schema in schema.yaml, should match UUIDInputArgs private argsValidator = Joi.object({ diff --git a/packages/core/src/lib/validators/index.ts b/packages/core/src/lib/validators/index.ts index b54d55d0..de339aa3 100644 --- a/packages/core/src/lib/validators/index.ts +++ b/packages/core/src/lib/validators/index.ts @@ -1,4 +1,3 @@ export * from './built-in-validators'; export * from './validatorLoader'; -export * from './validator'; export * from './constraints'; diff --git a/packages/core/src/lib/validators/validator.ts b/packages/core/src/lib/validators/validator.ts deleted file mode 100644 index d5375474..00000000 --- a/packages/core/src/lib/validators/validator.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { Constraint } from './constraints'; - -// U generic type will be any from RequestParameters -export interface IValidator { - // validator name - readonly name: string; - // validate Schema format - validateSchema(args: T): void; - // validate input value - validateData(value: U, args?: T): void; - // TODO: Find a better way to get constraints. - // Get the constraints of this validator - getConstraints?(args: T): Constraint[]; -} diff --git a/packages/core/src/lib/validators/validatorLoader.ts b/packages/core/src/lib/validators/validatorLoader.ts index ca933114..628f7479 100644 --- a/packages/core/src/lib/validators/validatorLoader.ts +++ b/packages/core/src/lib/validators/validatorLoader.ts @@ -1,78 +1,41 @@ -import { IValidator } from './validator'; -import * as path from 'path'; -import { inject, injectable, optional } from 'inversify'; -import { - defaultImport, - ClassType, - mergedModules, - ModuleProperties, -} from '../utils'; +import { injectable, multiInject, optional } from 'inversify'; import { TYPES } from '../../containers/types'; -import { SourceOfExtensions } from '../../models/coreOptions'; -import { flatten } from 'lodash'; - -export interface ExtensionModule extends ModuleProperties { - ['validators']: ClassType[]; -} +import { InputValidator } from '@vulcan-sql/core/models'; export interface IValidatorLoader { - load(validatorName: string): Promise; + getValidator(validatorName: string): InputValidator; } @injectable() export class ValidatorLoader implements IValidatorLoader { - // only found built-in validators in sub folders - private builtInFolder: string = path.join(__dirname, 'built-in-validators'); - private extensions: Array; + private extensions = new Map(); constructor( - @inject(TYPES.SourceOfExtensions) + @multiInject(TYPES.Extension_InputValidator) @optional() - extensions?: SourceOfExtensions + extensions: InputValidator[] = [] ) { - this.extensions = extensions || []; + this.loadValidators(extensions); } - public async load(validatorName: string) { - // read built-in validators in index.ts, the content is an array middleware class - const builtInClasses = flatten( - await defaultImport[]>(this.builtInFolder) - ); - // if extension path setup, load extension middlewares classes - let extensionClasses: ClassType[] = []; - if (this.extensions) { - // import extension which user customized - const modules = await defaultImport(...this.extensions); - const module = await mergedModules(modules); - extensionClasses = module['validators'] || []; - // check same name validator does exist or not, if exist, throw error. - this.checkSameNameValidator(extensionClasses); - } - - // reverse the array to make the extensions priority higher than built-in validators if has the duplicate name. - const validatorClasses = [...builtInClasses, ...extensionClasses].reverse(); - for (const validatorClass of validatorClasses) { - // create all middlewares by new it - const validator = new validatorClass() as IValidator; - if (validator.name === validatorName) return validator; - } + public getValidator(validatorName: string) { + if (!this.extensions.has(validatorName)) + // throw error if not found + throw new Error( + `The identifier name "${validatorName}" of validator is not defined in built-in validators or extensions configuration` + ); - // throw error if not found - throw new Error( - `The identifier name "${validatorName}" of validator not defined in built-in validators and passed folder path, or the defined validator not export as default.` - ); + return this.extensions.get(validatorName)!; } - private checkSameNameValidator(classes: ClassType[]) { - const map: { [name: string]: IValidator } = {}; - for (const cls of classes) { - const validator = new cls() as IValidator; - if (validator.name in map) { + private loadValidators(validators: InputValidator[]) { + for (const validator of validators) { + if (this.extensions.has(validator.name)) { throw new Error( - `The identifier name "${validator.name}" of validator class ${cls.name} has been defined in other extensions` + `The identifier name "${validator.name}" of validator has been defined in other extensions` ); } - map[validator.name] = validator; + this.extensions.set(validator.name, validator); } } } diff --git a/packages/core/src/models/coreOptions.ts b/packages/core/src/models/coreOptions.ts index 7a64c802..e5a37922 100644 --- a/packages/core/src/models/coreOptions.ts +++ b/packages/core/src/models/coreOptions.ts @@ -1,7 +1,7 @@ import { IArtifactBuilderOptions } from './artifactBuilderOptions'; import { ITemplateEngineOptions } from './templateEngineOptions'; -export type SourceOfExtensions = Array; +export type ExtensionAliases = Record; export interface ICoreOptions { artifact: IArtifactBuilderOptions; @@ -10,5 +10,6 @@ export interface ICoreOptions { * The extensions, could be module name or folder path (which need index.ts) * E.g: [ 'extensionModule1', '/usr/extensions2' ] * */ - extensions?: SourceOfExtensions; + extensions?: ExtensionAliases; + [moduleAlias: string]: any; } diff --git a/packages/core/src/models/extensions/base.ts b/packages/core/src/models/extensions/base.ts new file mode 100644 index 00000000..4bb053f4 --- /dev/null +++ b/packages/core/src/models/extensions/base.ts @@ -0,0 +1,16 @@ +import { TYPES } from '../../containers/types'; +import { inject, injectable } from 'inversify'; + +@injectable() +export abstract class ExtensionBase { + public activate?(): Promise; + private config?: C; + + constructor(@inject(TYPES.ExtensionConfig) config?: C) { + this.config = config; + } + + protected getConfig(): C | undefined { + return this.config; + } +} diff --git a/packages/core/src/models/extensions/decorators.ts b/packages/core/src/models/extensions/decorators.ts new file mode 100644 index 00000000..0da19c74 --- /dev/null +++ b/packages/core/src/models/extensions/decorators.ts @@ -0,0 +1,16 @@ +import 'reflect-metadata'; + +export const EXTENSION_TYPE_METADATA_KEY = Symbol.for('extension-type'); +export const EXTENSION_NAME_METADATA_KEY = Symbol.for('extension-name'); + +export function VulcanExtension(type: symbol): ClassDecorator { + return (target) => { + Reflect.defineMetadata(EXTENSION_TYPE_METADATA_KEY, type, target); + }; +} + +export function VulcanInternalExtension(name?: string): ClassDecorator { + return (target) => { + Reflect.defineMetadata(EXTENSION_NAME_METADATA_KEY, name || '', target); + }; +} diff --git a/packages/core/src/models/extensions/filterBuilder.ts b/packages/core/src/models/extensions/filterBuilder.ts new file mode 100644 index 00000000..aa95eb92 --- /dev/null +++ b/packages/core/src/models/extensions/filterBuilder.ts @@ -0,0 +1,8 @@ +import { TYPES } from '@vulcan-sql/core/types'; +import { VulcanExtension } from './decorators'; +import { CompileTimeExtension } from './templateEngine'; + +@VulcanExtension(TYPES.Extension_TemplateEngine) +export abstract class FilterBuilder extends CompileTimeExtension { + abstract filterName: string; +} diff --git a/packages/core/src/models/extensions/filterRunner.ts b/packages/core/src/models/extensions/filterRunner.ts new file mode 100644 index 00000000..3d8bf6f5 --- /dev/null +++ b/packages/core/src/models/extensions/filterRunner.ts @@ -0,0 +1,26 @@ +import { TYPES } from '@vulcan-sql/core/types'; +import { VulcanExtension } from './decorators'; +import { RuntimeExtension } from './templateEngine'; + +@VulcanExtension(TYPES.Extension_TemplateEngine) +export abstract class FilterRunner< + V = any, + C = any +> extends RuntimeExtension { + abstract filterName: string; + abstract transform(options: { + value: V; + args: Record; + }): Promise; + + public __transform(value: any, ...args: any[]) { + const callback = args[args.length - 1]; + const otherArgs = args.slice(0, args.length - 1); + this.transform({ + value, + args: otherArgs, + }) + .then((res) => callback(null, res)) + .catch((err) => callback(err, null)); + } +} diff --git a/packages/core/src/models/extensions/index.ts b/packages/core/src/models/extensions/index.ts new file mode 100644 index 00000000..3ecf29cd --- /dev/null +++ b/packages/core/src/models/extensions/index.ts @@ -0,0 +1,8 @@ +export * from './base'; +export * from './decorators'; +export * from './tagBuilder'; +export * from './tagRunner'; +export * from './templateEngine'; +export * from './filterBuilder'; +export * from './filterRunner'; +export * from './inputValidator'; diff --git a/packages/core/src/models/extensions/inputValidator.ts b/packages/core/src/models/extensions/inputValidator.ts new file mode 100644 index 00000000..8a314f52 --- /dev/null +++ b/packages/core/src/models/extensions/inputValidator.ts @@ -0,0 +1,17 @@ +import { TYPES } from '@vulcan-sql/core/types'; +import { Constraint } from '@vulcan-sql/core/validators'; +import { ExtensionBase } from './base'; +import { VulcanExtension } from './decorators'; + +@VulcanExtension(TYPES.Extension_InputValidator) +export abstract class InputValidator extends ExtensionBase { + // validator name + abstract readonly name: string; + // validate Schema format + abstract validateSchema(args: T): void; + // validate input value + abstract validateData(value: U, args?: T): void; + // TODO: Find a better way to get constraints. + // Get the constraints of this validator + public getConstraints?(args: T): Constraint[]; +} diff --git a/packages/core/src/models/extensions/tagBuilder.ts b/packages/core/src/models/extensions/tagBuilder.ts new file mode 100644 index 00000000..df449eb3 --- /dev/null +++ b/packages/core/src/models/extensions/tagBuilder.ts @@ -0,0 +1,48 @@ +import { VulcanExtension } from './decorators'; +import { TYPES } from '@vulcan-sql/core/types'; +import { sortBy } from 'lodash'; +import * as nunjucks from 'nunjucks'; +import { CompileTimeExtension } from './templateEngine'; + +@VulcanExtension(TYPES.Extension_TemplateEngine) +export abstract class TagBuilder extends CompileTimeExtension { + abstract tags: string[]; + abstract parse( + parser: nunjucks.parser.Parser, + nodes: typeof nunjucks.nodes, + lexer: typeof nunjucks.lexer + ): nunjucks.nodes.Node; + + public set __name(_) { + // ignore it + } + + public get __name() { + return this.getName(); + } + + public getName() { + return sortBy(this.tags).join('_'); + } + + protected createAsyncExtensionNode( + /** + * The arguments of this extension, they'll be rendered and passed to run function. + * It usually contains the configuration of the extension, e.g. {% req variable %} The variable name of req extension. + * Note that these arguments will be pass to run function directly: Literal('123') => "123", so adding Output nodes causes compiling issues. Output("123") => t += "123" + */ + argsNodeList: nunjucks.nodes.NodeList, + /** The content (usually the body) of this extension, they'll be passed to run function as render functions + * It usually contains the Output of your extension, e.g. {% req variable %} select * from user {% endreq %}, the "select * from user" should be put in this field. + * Note that these nodes will be rendered as the output of template: Output("123") => t = ""; t += "123", so adding nodes with no output like Symbol, Literal ... might cause compiling issues. Literal('123') => t = ""; 123 + */ + contentNodes: nunjucks.nodes.Node[] = [] + ) { + return new nunjucks.nodes.CallExtensionAsync( + this.getName(), + '__run', + argsNodeList, + contentNodes + ); + } +} diff --git a/packages/core/src/models/extensions/tagRunner.ts b/packages/core/src/models/extensions/tagRunner.ts new file mode 100644 index 00000000..2df78cf9 --- /dev/null +++ b/packages/core/src/models/extensions/tagRunner.ts @@ -0,0 +1,58 @@ +import { TYPES } from '@vulcan-sql/core/types'; +import { sortBy } from 'lodash'; +import * as nunjucks from 'nunjucks'; +import { VulcanExtension } from './decorators'; +import { RuntimeExtension } from './templateEngine'; + +export type TagExtensionContentArgGetter = () => Promise; + +export type TagExtensionArgTypes = string | number | boolean; + +export interface TagRunnerOptions { + context: any; + args: TagExtensionArgTypes[]; + contentArgs: TagExtensionContentArgGetter[]; +} + +@VulcanExtension(TYPES.Extension_TemplateEngine) +export abstract class TagRunner extends RuntimeExtension { + abstract tags: string[]; + abstract run( + options: TagRunnerOptions + ): Promise; + + public __run(...originalArgs: any[]) { + const context = originalArgs[0]; + const callback = originalArgs[originalArgs.length - 1]; + const args = originalArgs + .slice(1, originalArgs.length - 1) + .filter((value) => typeof value !== 'function'); + const contentArgs = originalArgs + .slice(1, originalArgs.length - 1) + .filter((value) => typeof value === 'function') + .map((cb) => () => { + return new Promise((resolve, reject) => { + cb((err: any, result: any) => { + if (err) reject(err); + else resolve(result); + }); + }); + }); + + this.run({ context, args, contentArgs }) + .then((result) => callback(null, result)) + .catch((err) => callback(err, null)); + } + + public set __name(_) { + // ignore it + } + + public get __name() { + return this.getName(); + } + + public getName() { + return sortBy(this.tags).join('_'); + } +} diff --git a/packages/core/src/models/extensions/templateEngine.ts b/packages/core/src/models/extensions/templateEngine.ts new file mode 100644 index 00000000..1a90fd86 --- /dev/null +++ b/packages/core/src/models/extensions/templateEngine.ts @@ -0,0 +1,19 @@ +import { ExtensionBase } from './base'; +import { VulcanExtension } from './decorators'; +import * as nunjucks from 'nunjucks'; +import { TYPES } from '@vulcan-sql/core/types'; + +export type TemplateEngineExtension = RuntimeExtension | CompileTimeExtension; + +@VulcanExtension(TYPES.Extension_TemplateEngine) +export abstract class RuntimeExtension extends ExtensionBase {} + +@VulcanExtension(TYPES.Extension_TemplateEngine) +export abstract class CompileTimeExtension extends ExtensionBase { + // AST visitor + public onVisit?(node: nunjucks.nodes.Node): void; + public finish?(): void; + // Metadata provider + public metadataName?: string; + public getMetadata?(): any; +} diff --git a/packages/core/src/models/index.ts b/packages/core/src/models/index.ts index e53e6b52..1cef8867 100644 --- a/packages/core/src/models/index.ts +++ b/packages/core/src/models/index.ts @@ -3,3 +3,4 @@ export * from './pagination'; export * from './artifactBuilderOptions'; export * from './coreOptions'; export * from './templateEngineOptions'; +export * from './extensions'; diff --git a/packages/core/src/options/artifactBuilder.ts b/packages/core/src/options/artifactBuilder.ts index 054b5281..a332b939 100644 --- a/packages/core/src/options/artifactBuilder.ts +++ b/packages/core/src/options/artifactBuilder.ts @@ -1,5 +1,5 @@ import { injectable, inject, optional } from 'inversify'; -import { TYPES } from '@vulcan-sql/core/containers'; +import { TYPES } from '@vulcan-sql/core/types'; import { IArtifactBuilderOptions, PersistentStoreType, diff --git a/packages/core/src/options/templateEngine.ts b/packages/core/src/options/templateEngine.ts index e3f610f0..002eb321 100644 --- a/packages/core/src/options/templateEngine.ts +++ b/packages/core/src/options/templateEngine.ts @@ -1,5 +1,5 @@ import { injectable, inject, optional } from 'inversify'; -import { TYPES } from '@vulcan-sql/core/containers'; +import { TYPES } from '@vulcan-sql/core/types'; import { ITemplateEngineOptions, TemplateProviderType, diff --git a/packages/core/test/artifact-builder/persistent-stores/localFilePersistentStore.spec.ts b/packages/core/test/artifact-builder/persistent-stores/localFilePersistentStore.spec.ts index 509b2649..f0af47db 100644 --- a/packages/core/test/artifact-builder/persistent-stores/localFilePersistentStore.spec.ts +++ b/packages/core/test/artifact-builder/persistent-stores/localFilePersistentStore.spec.ts @@ -3,7 +3,7 @@ import { LocalFilePersistentStore, PersistentStore, } from '@vulcan-sql/core/artifact-builder'; -import { TYPES } from '@vulcan-sql/core/containers'; +import { TYPES } from '@vulcan-sql/core/types'; import { Container } from 'inversify'; let container: Container; diff --git a/packages/core/test/artifact-builder/vulcanArtifactBuilder.spec.ts b/packages/core/test/artifact-builder/vulcanArtifactBuilder.spec.ts index a20e1938..d24c8867 100644 --- a/packages/core/test/artifact-builder/vulcanArtifactBuilder.spec.ts +++ b/packages/core/test/artifact-builder/vulcanArtifactBuilder.spec.ts @@ -5,7 +5,7 @@ import { VulcanArtifactBuilder, } from '@vulcan-sql/core/artifact-builder'; import { Container } from 'inversify'; -import { TYPES } from '@vulcan-sql/core/containers'; +import { TYPES } from '@vulcan-sql/core/types'; import * as sinon from 'ts-sinon'; let container: Container; diff --git a/packages/core/test/containers/continer.spec.ts b/packages/core/test/containers/continer.spec.ts index 4b0c054a..c1dbe2fb 100644 --- a/packages/core/test/containers/continer.spec.ts +++ b/packages/core/test/containers/continer.spec.ts @@ -26,7 +26,7 @@ it('Container should load options and resolve all dependencies', async () => { provider: TemplateProviderType.LocalFile, folderPath: path.resolve(__dirname, 'test-template'), }, - extensions: [], + extensions: {}, }); // Act const templateEngine = container.get(TYPES.TemplateEngine); diff --git a/packages/core/test/extension-loader/extensionLoader.spec.ts b/packages/core/test/extension-loader/extensionLoader.spec.ts new file mode 100644 index 00000000..5b857715 --- /dev/null +++ b/packages/core/test/extension-loader/extensionLoader.spec.ts @@ -0,0 +1,142 @@ +import { + ExtensionLoader, + FilterBuilder, + TYPES, + VulcanInternalExtension, +} from '@vulcan-sql/core'; +import { Container } from 'inversify'; +import * as path from 'path'; + +@VulcanInternalExtension('test1') +class Test1 extends FilterBuilder { + public filterName = 'test1'; + public obtainConfig() { + return this.getConfig(); + } +} + +@VulcanInternalExtension('test2') +class Test2 extends FilterBuilder { + public filterName = 'test2'; + public obtainConfig() { + return this.getConfig(); + } +} + +class Test3 extends FilterBuilder { + public filterName = 'test3'; +} + +it.each([ + ['array', [Test1, Test2]], + ['object', { test1: Test1, test2: Test2 }], +])( + 'Extension loader should load internal extension modules with %s type', + async (_, module: any) => { + // Arrange + const loader = new ExtensionLoader({} as any); + loader.loadInternalExtensionModule(module); + const container = new Container(); + + // Act + loader.bindExtensions(container.bind.bind(container)); + const allExtensions = container.getAll(TYPES.Extension_TemplateEngine); + + // Assert + expect(allExtensions.length).toBe(2); + expect((allExtensions as any)[0].filterName).toBe('test1'); + expect((allExtensions as any)[1].filterName).toBe('test2'); + } +); + +it.each([ + ['default array', 'defaultArray'], + ['default object', 'defaultObject'], + ['export', 'export'], +])( + 'Extension loader should load external extension modules with %s type', + async (_, extPath: any) => { + // Arrange + const loader = new ExtensionLoader({ + extensions: { + test: [path.resolve(__dirname, 'test-extension', extPath)], + }, + } as any); + await loader.loadExternalExtensionModules(); + const container = new Container(); + + // Act + loader.bindExtensions(container.bind.bind(container)); + const allExtensions = container.getAll(TYPES.Extension_TemplateEngine); + // Assert + expect(allExtensions.length).toBe(2); + expect((allExtensions as any)[0].filterName).toBe('test1'); + expect((allExtensions as any)[1].filterName).toBe('test2'); + } +); + +it('Extension loader should throw error when we try to load any extension after bound', async () => { + // Arrange + const loader = new ExtensionLoader({} as any); + const container = new Container(); + loader.bindExtensions(container.bind.bind(container)); + + // Act, Assert + await expect(loader.loadExternalExtensionModules()).rejects.toThrow( + 'We must load all extensions before call bindExtension function' + ); + expect(() => loader.loadInternalExtensionModule([])).toThrow( + 'We must load all extensions before call bindExtension function' + ); +}); + +it('Extension loader should reject external extensions without extending a proper class', async () => { + // Arrange + const loader = new ExtensionLoader({ + extensions: { + test: [path.resolve(__dirname, 'test-extension', 'withoutExtends')], + }, + } as any); + + // Act, Assert + await expect(loader.loadExternalExtensionModules()).rejects.toThrow( + 'Extension must have @VulcanExtension decorator, have you use extend the correct super class?' + ); +}); + +it('Extension loader should reject internal extensions without @VulcanInternalExtension decorator', async () => { + // Arrange + const loader = new ExtensionLoader({} as any); + + // Act, Assert + expect(() => loader.loadInternalExtensionModule([Test3])).toThrow( + 'Internal extension must have @VulcanInternalExtension decorator' + ); +}); + +it('Extension loader should inject correct config to extensions', async () => { + // Arrange + const loader = new ExtensionLoader({ + test1: { + a: 1, + }, + test2: { + b: 2, + }, + } as any); + const container = new Container(); + + // Act + loader.loadInternalExtensionModule([Test1, Test2]); + loader.bindExtensions(container.bind.bind(container)); + const config1 = container + .getAll(TYPES.Extension_TemplateEngine)[0] + .obtainConfig(); + const config2 = container + .getAll(TYPES.Extension_TemplateEngine)[1] + .obtainConfig(); + + // Assert + expect(config1).toEqual({ a: 1 }); + expect(config2).toEqual({ b: 2 }); +}); diff --git a/packages/core/test/extension-loader/test-extension/defaultArray.ts b/packages/core/test/extension-loader/test-extension/defaultArray.ts new file mode 100644 index 00000000..155d40fa --- /dev/null +++ b/packages/core/test/extension-loader/test-extension/defaultArray.ts @@ -0,0 +1,11 @@ +import { FilterBuilder } from '@vulcan-sql/core'; + +class Test1 extends FilterBuilder { + public filterName = 'test1'; +} + +class Test2 extends FilterBuilder { + public filterName = 'test2'; +} + +export default [Test1, Test2]; diff --git a/packages/core/test/extension-loader/test-extension/defaultObject.ts b/packages/core/test/extension-loader/test-extension/defaultObject.ts new file mode 100644 index 00000000..3ff4cbad --- /dev/null +++ b/packages/core/test/extension-loader/test-extension/defaultObject.ts @@ -0,0 +1,14 @@ +import { FilterBuilder } from '@vulcan-sql/core'; + +class Test1 extends FilterBuilder { + public filterName = 'test1'; +} + +class Test2 extends FilterBuilder { + public filterName = 'test2'; +} + +export default { + test1: Test1, + test2: Test2, +}; diff --git a/packages/core/test/extension-loader/test-extension/export.ts b/packages/core/test/extension-loader/test-extension/export.ts new file mode 100644 index 00000000..3c4d01fa --- /dev/null +++ b/packages/core/test/extension-loader/test-extension/export.ts @@ -0,0 +1,9 @@ +import { FilterBuilder } from '@vulcan-sql/core'; + +export class Test1 extends FilterBuilder { + public filterName = 'test1'; +} + +export class Test2 extends FilterBuilder { + public filterName = 'test2'; +} diff --git a/packages/core/test/extension-loader/test-extension/withoutExtends.ts b/packages/core/test/extension-loader/test-extension/withoutExtends.ts new file mode 100644 index 00000000..1e14df54 --- /dev/null +++ b/packages/core/test/extension-loader/test-extension/withoutExtends.ts @@ -0,0 +1 @@ +export class A {} diff --git a/packages/core/test/template-engine/externalExtension.spec.ts b/packages/core/test/template-engine/externalExtension.spec.ts index 3cd096bb..0f3de299 100644 --- a/packages/core/test/template-engine/externalExtension.spec.ts +++ b/packages/core/test/template-engine/externalExtension.spec.ts @@ -1,10 +1,14 @@ import { createTestCompiler } from './testCompiler'; import * as path from 'path'; -it('The init function should be called before executing', async () => { +it('The activate function should be called before executing', async () => { // Arrange const { compiler, loader, getCreatedQueries } = await createTestCompiler({ - extensionNames: [path.join(__dirname, 'testExtension.ts')], + options: { + extensions: { + test: path.resolve(__dirname, 'testExtension.ts'), + }, + }, }); const { compiledData } = await compiler.compile('Hello {{ 123 | test }}!'); diff --git a/packages/core/test/template-engine/nunjuckCompiler.spec.ts b/packages/core/test/template-engine/nunjuckCompiler.spec.ts index f5829d08..72abbcd6 100644 --- a/packages/core/test/template-engine/nunjuckCompiler.spec.ts +++ b/packages/core/test/template-engine/nunjuckCompiler.spec.ts @@ -29,7 +29,7 @@ it('Nunjucks compiler should reject the extension which has no valid super class // Arrange const { compiler } = await createTestCompiler(); // Action, Assert - expect(() => compiler.loadExtension({})).toThrow( + expect(() => compiler.loadExtension({} as any)).toThrow( 'Extension must be of type RuntimeExtension or CompileTimeExtension' ); }); diff --git a/packages/core/test/template-engine/template-provider/fileTemplateProvider.spec.ts b/packages/core/test/template-engine/template-provider/fileTemplateProvider.spec.ts index 48fa63f6..bd2b08f2 100644 --- a/packages/core/test/template-engine/template-provider/fileTemplateProvider.spec.ts +++ b/packages/core/test/template-engine/template-provider/fileTemplateProvider.spec.ts @@ -1,4 +1,4 @@ -import { TYPES } from '@vulcan-sql/core/containers'; +import { TYPES } from '@vulcan-sql/core/types'; import { ITemplateEngineOptions, TemplateProviderType, diff --git a/packages/core/test/template-engine/template-provider/fileTemplateProviderError.spec.ts b/packages/core/test/template-engine/template-provider/fileTemplateProviderError.spec.ts index 67678a96..d56e9200 100644 --- a/packages/core/test/template-engine/template-provider/fileTemplateProviderError.spec.ts +++ b/packages/core/test/template-engine/template-provider/fileTemplateProviderError.spec.ts @@ -1,4 +1,4 @@ -import { TYPES } from '@vulcan-sql/core/containers'; +import { TYPES } from '@vulcan-sql/core/types'; import { ITemplateEngineOptions, TemplateProviderType, diff --git a/packages/core/test/template-engine/templateEngine.spec.ts b/packages/core/test/template-engine/templateEngine.spec.ts index fb7fd330..ef7f84dd 100644 --- a/packages/core/test/template-engine/templateEngine.spec.ts +++ b/packages/core/test/template-engine/templateEngine.spec.ts @@ -5,7 +5,7 @@ import { ICodeLoader, } from '@vulcan-sql/core/template-engine'; import * as sinon from 'ts-sinon'; -import { TYPES } from '@vulcan-sql/core/containers'; +import { TYPES } from '@vulcan-sql/core/types'; import { Container } from 'inversify'; let container: Container; diff --git a/packages/core/test/template-engine/testCompiler.ts b/packages/core/test/template-engine/testCompiler.ts index 36090d2f..1d771b4a 100644 --- a/packages/core/test/template-engine/testCompiler.ts +++ b/packages/core/test/template-engine/testCompiler.ts @@ -1,18 +1,20 @@ -import { TYPES } from '@vulcan-sql/core/containers'; +import { TYPES } from '@vulcan-sql/core/types'; import { InMemoryCodeLoader, NunjucksCompiler, } from '@vulcan-sql/core/template-engine'; -import { bindExtensions } from '@vulcan-sql/core/template-engine/extension-loader'; import { Container } from 'inversify'; import * as sinon from 'ts-sinon'; import * as nunjucks from 'nunjucks'; import { IDataQueryBuilder, IExecutor } from '@vulcan-sql/core/data-query'; +import { extensionModule } from '../../src/containers/modules'; +import { ICoreOptions } from '@vulcan-sql/core'; +import { DeepPartial } from 'ts-essentials'; export const createTestCompiler = async ({ - extensionNames = [], + options = {}, }: { - extensionNames?: string[]; + options?: DeepPartial; } = {}) => { const container = new Container(); const stubQueryBuilder = sinon.stubInterface(); @@ -24,11 +26,11 @@ export const createTestCompiler = async ({ .bind(TYPES.CompilerLoader) .to(InMemoryCodeLoader) .inSingletonScope(); - await bindExtensions(container.bind.bind(container), extensionNames); container.bind(TYPES.Executor).toConstantValue(stubExecutor); - container.bind(TYPES.Compiler).to(NunjucksCompiler).inSingletonScope(); + await container.loadAsync(extensionModule(options as any)); + // Compiler environment container .bind(TYPES.CompilerEnvironment) diff --git a/packages/core/test/template-engine/testExtension.ts b/packages/core/test/template-engine/testExtension.ts index 18a911f7..6d7b7bae 100644 --- a/packages/core/test/template-engine/testExtension.ts +++ b/packages/core/test/template-engine/testExtension.ts @@ -1,14 +1,14 @@ -import { FilterBuilder, FilterRunner, OnInit } from '@vulcan-sql/core'; +import { FilterBuilder, FilterRunner } from '@vulcan-sql/core'; -class TestFilterBuilder extends FilterBuilder { +export class TestFilterBuilder extends FilterBuilder { public filterName = 'test'; } -class TestFilterRunner extends FilterRunner implements OnInit { +export class TestFilterRunner extends FilterRunner { public filterName = 'test'; private initDone = false; - public async onInit(): Promise { + public override async activate(): Promise { this.initDone = true; } @@ -16,5 +16,3 @@ class TestFilterRunner extends FilterRunner implements OnInit { return `${value}-${this.initDone}`; } } - -export default [TestFilterBuilder, TestFilterRunner]; diff --git a/packages/core/test/validators/test-custom-validators/custom-validators1/index.ts b/packages/core/test/validators/test-custom-validators/custom-validators1/index.ts index 9f7d75c6..09b7acc8 100644 --- a/packages/core/test/validators/test-custom-validators/custom-validators1/index.ts +++ b/packages/core/test/validators/test-custom-validators/custom-validators1/index.ts @@ -1,19 +1,14 @@ /* eslint-disable @typescript-eslint/no-empty-function */ -import { IValidator } from '@vulcan-sql/core'; +import { InputValidator } from '@vulcan-sql/core'; // Imitate extension for testing -export default { - validators: [ - class Validator implements IValidator { - name = 'v1-1'; - validateSchema() {} - validateData() {} - }, - class Validator implements IValidator { - name = 'v1-2'; - validateSchema() {} - validateData() {} - }, - ], - middlewares: [], -}; +export class Validator11 extends InputValidator { + name = 'v1-1'; + validateSchema() {} + validateData() {} +} +export class Validator12 extends InputValidator { + name = 'v1-2'; + validateSchema() {} + validateData() {} +} diff --git a/packages/core/test/validators/test-custom-validators/custom-validators2/index.ts b/packages/core/test/validators/test-custom-validators/custom-validators2/index.ts index 2e41a824..d57d4f47 100644 --- a/packages/core/test/validators/test-custom-validators/custom-validators2/index.ts +++ b/packages/core/test/validators/test-custom-validators/custom-validators2/index.ts @@ -1,19 +1,14 @@ /* eslint-disable @typescript-eslint/no-empty-function */ -import { IValidator } from '@vulcan-sql/core'; +import { InputValidator } from '@vulcan-sql/core'; // Imitate extension for testing -export default { - validators: [ - class Validator implements IValidator { - name = 'v2-1'; - validateSchema() {} - validateData() {} - }, - class Validator implements IValidator { - name = 'v2-2'; - validateSchema() {} - validateData() {} - }, - ], - others: [], -}; +export class Validator21 extends InputValidator { + name = 'v2-1'; + validateSchema() {} + validateData() {} +} +export class Validator22 extends InputValidator { + name = 'v2-2'; + validateSchema() {} + validateData() {} +} diff --git a/packages/core/test/validators/test-custom-validators/custom-validators3/index.ts b/packages/core/test/validators/test-custom-validators/custom-validators3/index.ts index 135daf33..d4fadb3c 100644 --- a/packages/core/test/validators/test-custom-validators/custom-validators3/index.ts +++ b/packages/core/test/validators/test-custom-validators/custom-validators3/index.ts @@ -1,18 +1,13 @@ /* eslint-disable @typescript-eslint/no-empty-function */ -import { IValidator } from '@vulcan-sql/core'; +import { InputValidator } from '@vulcan-sql/core'; -export default { - validators: [ - class Validator implements IValidator { - name = 'v3-1'; - validateSchema() {} - validateData() {} - }, - class Validator implements IValidator { - name = 'v1-1'; - validateSchema() {} - validateData() {} - }, - ], - others: [], -}; +export class Validator31 extends InputValidator { + name = 'v3-1'; + validateSchema() {} + validateData() {} +} +export class Validator32 extends InputValidator { + name = 'v1-1'; + validateSchema() {} + validateData() {} +} diff --git a/packages/core/test/validators/validatorLoader.spec.ts b/packages/core/test/validators/validatorLoader.spec.ts index d801cd05..52e9945f 100644 --- a/packages/core/test/validators/validatorLoader.spec.ts +++ b/packages/core/test/validators/validatorLoader.spec.ts @@ -1,17 +1,15 @@ -import { SourceOfExtensions, TYPES } from '@vulcan-sql/core'; +import { TYPES } from '@vulcan-sql/core'; import { IValidatorLoader, ValidatorLoader } from '@vulcan-sql/core/validators'; import { Container } from 'inversify'; import * as path from 'path'; -import faker from '@faker-js/faker'; +import { extensionModule } from '../../src/containers/modules'; describe('Test validator loader for built-in validators', () => { let container: Container; - beforeEach(() => { + beforeEach(async () => { container = new Container(); - container - .bind>(TYPES.SourceOfExtensions) - .toConstantValue([]); + await container.loadAsync(extensionModule({} as any)); container .bind(TYPES.ValidatorLoader) .to(ValidatorLoader) @@ -19,8 +17,11 @@ describe('Test validator loader for built-in validators', () => { }); afterEach(() => { - container.unbindAll(); + container.unbind(TYPES.ValidatorLoader); + // TODO: Found some issues while unloading extension module + // https://github.com/inversify/InversifyJS/issues/1462 }); + it.each([ // built-in validator { name: 'date', expected: 'date' }, @@ -35,7 +36,7 @@ describe('Test validator loader for built-in validators', () => { TYPES.ValidatorLoader ); // Act - const result = await validatorLoader.load(name); + const result = validatorLoader.getValidator(name); // Assert expect(result.name).toEqual(expected); @@ -50,10 +51,10 @@ describe('Test validator loader for built-in validators', () => { TYPES.ValidatorLoader ); // Act - const loadAction = validatorLoader.load(name); + const loadAction = () => validatorLoader.getValidator(name); // Asset - await expect(loadAction).rejects.toThrow(Error); + expect(loadAction).toThrow(Error); } ); }); @@ -61,13 +62,19 @@ describe('Test validator loader for built-in validators', () => { describe('Test validator loader for extension validators with one module', () => { let container: Container; - beforeEach(() => { + beforeEach(async () => { container = new Container(); - container - .bind>(TYPES.SourceOfExtensions) - .toConstantValue([ - path.resolve(__dirname, 'test-custom-validators/custom-validators1'), - ]); + await container.loadAsync( + extensionModule({ + extensions: { + validator1: path.resolve( + __dirname, + 'test-custom-validators/custom-validators1' + ), + }, + } as any) + ); + container .bind(TYPES.ValidatorLoader) .to(ValidatorLoader) @@ -75,7 +82,7 @@ describe('Test validator loader for extension validators with one module', () => }); afterEach(() => { - container.unbindAll(); + container.unbind(TYPES.ValidatorLoader); }); it.each([ // custom validator @@ -89,7 +96,7 @@ describe('Test validator loader for extension validators with one module', () => TYPES.ValidatorLoader ); // Act - const result = await validatorLoader.load(name); + const result = validatorLoader.getValidator(name); // Assert expect(result.name).toEqual(expected); @@ -104,10 +111,10 @@ describe('Test validator loader for extension validators with one module', () => TYPES.ValidatorLoader ); // Act - const loadAction = validatorLoader.load(name); + const loadAction = () => validatorLoader.getValidator(name); // Asset - await expect(loadAction).rejects.toThrow(Error); + expect(loadAction).toThrow(Error); } ); }); @@ -117,30 +124,40 @@ describe('Test validator loader for extension validators in multiple module', () beforeEach(() => { container = new Container(); + container + .bind(TYPES.ValidatorLoader) + .to(ValidatorLoader) + .inSingletonScope(); }); afterEach(() => { - container.unbindAll(); + container.unbind(TYPES.ValidatorLoader); }); it('Should success when loading unique identifier of validators in multiple modules.', async () => { // Arrange - container - .bind>(TYPES.SourceOfExtensions) - .toConstantValue([ - path.resolve(__dirname, 'test-custom-validators/custom-validators1'), - path.resolve(__dirname, 'test-custom-validators/custom-validators2'), - ]); - container - .bind(TYPES.ValidatorLoader) - .to(ValidatorLoader) - .inSingletonScope(); + await container.loadAsync( + extensionModule({ + extensions: { + validator1: [ + path.resolve( + __dirname, + 'test-custom-validators/custom-validators1' + ), + path.resolve( + __dirname, + 'test-custom-validators/custom-validators2' + ), + ], + }, + } as any) + ); const validatorLoader = container.get( TYPES.ValidatorLoader ); // Act - const v1 = await validatorLoader.load('v1-1'); - const v2 = await validatorLoader.load('v2-1'); + const v1 = validatorLoader.getValidator('v1-1'); + const v2 = validatorLoader.getValidator('v2-1'); // Assert expect(v1.name).toBe('v1-1'); expect(v2.name).toBe('v2-1'); @@ -148,26 +165,30 @@ describe('Test validator loader for extension validators in multiple module', () it('Should load failed when found duplicate identifier of validators in multiple modules.', async () => { // Arrange - container - .bind>(TYPES.SourceOfExtensions) - .toConstantValue([ - path.resolve(__dirname, 'test-custom-validators/custom-validators1'), - // the custom-validators3 also contains same identifier v1-1 - path.resolve(__dirname, 'test-custom-validators/custom-validators3'), - ]); - container - .bind(TYPES.ValidatorLoader) - .to(ValidatorLoader) - .inSingletonScope(); - - const validatorLoader = container.get( - TYPES.ValidatorLoader + await container.loadAsync( + extensionModule({ + extensions: { + validator1: [ + path.resolve( + __dirname, + 'test-custom-validators/custom-validators1' + ), + // the custom-validators3 also contains the same identifier "v1-1" + path.resolve( + __dirname, + 'test-custom-validators/custom-validators3' + ), + ], + }, + } as any) ); + // Act - const action = validatorLoader.load(faker.random.word()); + const action = () => container.get(TYPES.ValidatorLoader); + // Assert - expect(action).rejects.toThrow( - 'The identifier name "v1-1" of validator class Validator has been defined in other extensions' + expect(action).toThrow( + 'The identifier name "v1-1" of validator has been defined in other extensions' ); }); }); diff --git a/tsconfig.base.json b/tsconfig.base.json index 7bdaef27..349822bf 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -40,6 +40,7 @@ "packages/core/src/lib/artifact-builder/*" ], "@vulcan-sql/core/containers": ["packages/core/src/containers/index"], + "@vulcan-sql/core/types": ["packages/core/src/containers/types"], "@vulcan-sql/core/data-query": ["packages/core/src/lib/data-query/index"], "@vulcan-sql/core/data-query/*": ["packages/core/src/lib/data-query/*"], "@vulcan-sql/core/data-source": [ From 5c6061ba3fb336be55e0c1513f7d12861cc7380c Mon Sep 17 00:00:00 2001 From: Ivan Tsai Date: Tue, 2 Aug 2022 15:24:06 +0800 Subject: [PATCH 02/20] fix(build): update to fit new validator loaders --- packages/build/src/containers/container.ts | 20 +++-- .../middleware/checkValidator.ts | 4 +- .../middleware/setConstraints.ts | 2 +- packages/build/src/lib/vulcanBuilder.ts | 2 +- packages/build/test/builder/builder.spec.ts | 2 +- .../middleware/checkValidator.spec.ts | 12 +-- .../middleware/setConstraints.spec.ts | 21 +++-- .../test/schema-parser/schemaParser.spec.ts | 4 +- packages/build/test/spec-generator/schema.ts | 87 ++++++++++--------- packages/core/src/containers/container.ts | 8 +- .../src/containers/modules/artifactBuilder.ts | 4 +- .../lib/extension-loader/extensionLoader.ts | 4 +- .../core/test/containers/continer.spec.ts | 1 + .../test/validators/validatorLoader.spec.ts | 6 +- 14 files changed, 97 insertions(+), 80 deletions(-) diff --git a/packages/build/src/containers/container.ts b/packages/build/src/containers/container.ts index e5609dbb..ad711e6a 100644 --- a/packages/build/src/containers/container.ts +++ b/packages/build/src/containers/container.ts @@ -4,22 +4,26 @@ import { IBuildOptions } from '@vulcan-sql/build/models'; import { schemaParserModule } from './modules'; export class Container { - private inversifyContainer = new InversifyContainer(); + private inversifyContainer?: InversifyContainer; + private coreContainer?: CoreContainer; public get(type: symbol) { - return this.inversifyContainer.get(type); + const instance = this.inversifyContainer?.get(type); + if (!instance) + throw new Error(`Cannot resolve ${type.toString()} in container`); + return instance; } public async load(options: IBuildOptions) { - const coreContainer = new CoreContainer(); - await coreContainer.load(options); - this.inversifyContainer.parent = coreContainer.getInversifyContainer(); + this.coreContainer = new CoreContainer(); + await this.coreContainer.load(options); + this.inversifyContainer = this.coreContainer.getInversifyContainer(); this.inversifyContainer.load(schemaParserModule(options.schemaParser)); } - public unload() { - this.inversifyContainer.parent?.unbindAll(); - this.inversifyContainer.unbindAll(); + public async unload() { + await this.coreContainer?.unload(); + await this.inversifyContainer?.unbindAllAsync(); } public getInversifyContainer() { diff --git a/packages/build/src/lib/schema-parser/middleware/checkValidator.ts b/packages/build/src/lib/schema-parser/middleware/checkValidator.ts index ddaabef8..5367c369 100644 --- a/packages/build/src/lib/schema-parser/middleware/checkValidator.ts +++ b/packages/build/src/lib/schema-parser/middleware/checkValidator.ts @@ -29,7 +29,9 @@ export class CheckValidator extends SchemaParserMiddleware { throw new Error('Validator name is required'); } - const validator = await this.validatorLoader.load(validatorRequest.name); + const validator = this.validatorLoader.getValidator( + validatorRequest.name + ); // TODO: indicate the detail of error validator.validateSchema(validatorRequest.args); diff --git a/packages/build/src/lib/schema-parser/middleware/setConstraints.ts b/packages/build/src/lib/schema-parser/middleware/setConstraints.ts index 6bd65272..e6bfa04b 100644 --- a/packages/build/src/lib/schema-parser/middleware/setConstraints.ts +++ b/packages/build/src/lib/schema-parser/middleware/setConstraints.ts @@ -25,7 +25,7 @@ export class SetConstraints extends SchemaParserMiddleware { // load validator and keep args const validatorsWithArgs = await Promise.all( (request.validators || []).map(async (validator) => ({ - validator: await this.validatorLoader.load(validator.name), + validator: this.validatorLoader.getValidator(validator.name), args: validator.args, })) ); diff --git a/packages/build/src/lib/vulcanBuilder.ts b/packages/build/src/lib/vulcanBuilder.ts index 36315f4b..82c3aa45 100644 --- a/packages/build/src/lib/vulcanBuilder.ts +++ b/packages/build/src/lib/vulcanBuilder.ts @@ -24,6 +24,6 @@ export class VulcanBuilder { await artifactBuilder.build({ schemas, templates }); - container.unload(); + await container.unload(); } } diff --git a/packages/build/test/builder/builder.spec.ts b/packages/build/test/builder/builder.spec.ts index 424ee30e..ff9cdd66 100644 --- a/packages/build/test/builder/builder.spec.ts +++ b/packages/build/test/builder/builder.spec.ts @@ -24,7 +24,7 @@ it('Builder.build should work', async () => { provider: TemplateProviderType.LocalFile, folderPath: path.resolve(__dirname, 'source'), }, - extensions: [], + extensions: {}, }; // Act, Assert diff --git a/packages/build/test/schema-parser/middleware/checkValidator.spec.ts b/packages/build/test/schema-parser/middleware/checkValidator.spec.ts index 3a5ae1ac..f60d828f 100644 --- a/packages/build/test/schema-parser/middleware/checkValidator.spec.ts +++ b/packages/build/test/schema-parser/middleware/checkValidator.spec.ts @@ -14,11 +14,11 @@ it('Should pass if there is no error', async () => { ], }; const stubValidatorLoader = sinon.stubInterface(); - stubValidatorLoader.load.resolves({ + stubValidatorLoader.getValidator.returns({ name: 'validator1', validateSchema: () => null, validateData: () => null, - }); + } as any); const checkValidator = new CheckValidator(stubValidatorLoader); // Act Assert @@ -38,11 +38,11 @@ it('Should throw if some validators have no name', async () => { ], }; const stubValidatorLoader = sinon.stubInterface(); - stubValidatorLoader.load.resolves({ + stubValidatorLoader.getValidator.returns({ name: 'validator1', validateSchema: () => null, validateData: () => null, - }); + } as any); const checkValidator = new CheckValidator(stubValidatorLoader); // Act Assert @@ -62,13 +62,13 @@ it('Should throw if the arguments of a validator is invalid', async () => { ], }; const stubValidatorLoader = sinon.stubInterface(); - stubValidatorLoader.load.resolves({ + stubValidatorLoader.getValidator.returns({ name: 'validator1', validateSchema: () => { throw new Error(); }, validateData: () => null, - }); + } as any); const checkValidator = new CheckValidator(stubValidatorLoader); // Act Assert diff --git a/packages/build/test/schema-parser/middleware/setConstraints.spec.ts b/packages/build/test/schema-parser/middleware/setConstraints.spec.ts index 7ed9f00c..aaafd6dd 100644 --- a/packages/build/test/schema-parser/middleware/setConstraints.spec.ts +++ b/packages/build/test/schema-parser/middleware/setConstraints.spec.ts @@ -11,15 +11,18 @@ import * as sinon from 'ts-sinon'; it('Should set and compose constraints', async () => { // Arrange const stubValidatorLoader = sinon.stubInterface(); - stubValidatorLoader.load.callsFake(async (name) => ({ - name, - validateData: () => null, - validateSchema: () => null, - getConstraints: (args) => { - if (name === 'required') return [Constraint.Required()]; - return [Constraint.MinValue(args.value)]; - }, - })); + stubValidatorLoader.getValidator.callsFake( + (name) => + ({ + name, + validateData: () => null, + validateSchema: () => null, + getConstraints: (args: any) => { + if (name === 'required') return [Constraint.Required()]; + return [Constraint.MinValue(args.value)]; + }, + } as any) + ); const schema: RawAPISchema = { templateSource: 'existed/path', diff --git a/packages/build/test/schema-parser/schemaParser.spec.ts b/packages/build/test/schema-parser/schemaParser.spec.ts index 1d9f5a0c..7c5a2755 100644 --- a/packages/build/test/schema-parser/schemaParser.spec.ts +++ b/packages/build/test/schema-parser/schemaParser.spec.ts @@ -62,11 +62,11 @@ request: }; }; stubSchemaReader.readSchema.returns(generator()); - stubValidatorLoader.load.resolves({ + stubValidatorLoader.getValidator.returns({ name: 'validator1', validateSchema: () => null, validateData: () => null, - }); + } as any); const schemaParser = container.get(TYPES.SchemaParser); // Act diff --git a/packages/build/test/spec-generator/schema.ts b/packages/build/test/spec-generator/schema.ts index f11cc491..581dbb97 100644 --- a/packages/build/test/spec-generator/schema.ts +++ b/packages/build/test/spec-generator/schema.ts @@ -6,6 +6,7 @@ import { promises as fs } from 'fs'; import { APISchema, Constraint, + InputValidator, IValidatorLoader, TemplateEngine, TYPES as CORE_TYPES, @@ -30,59 +31,59 @@ const getSchemaPaths = () => }); }); +class MockValidator extends InputValidator { + constructor(public name: string, private constraintsFn: any) { + super(); + } + + public validateData(): void { + return; + } + + public validateSchema(): void { + return; + } + + public override getConstraints(args: any) { + return this.constraintsFn(args); + } +} + const getStubLoader = () => { const validatorLoader = sinon.stubInterface(); - validatorLoader.load.callsFake(async (name) => { + + validatorLoader.getValidator.callsFake((name) => { switch (name) { case 'required': - return { - name: 'required', - validateSchema: () => null, - validateData: () => null, - getConstraints: () => [Constraint.Required()], - }; + return new MockValidator('required', () => [Constraint.Required()]); + case 'minValue': - return { - name: 'minValue', - validateSchema: () => null, - validateData: () => null, - getConstraints: (args) => [Constraint.MinValue(args.value)], - }; + return new MockValidator('minValue', (args: any) => [ + Constraint.MinValue(args.value), + ]); + case 'maxValue': - return { - name: 'maxValue', - validateSchema: () => null, - validateData: () => null, - getConstraints: (args) => [Constraint.MaxValue(args.value)], - }; + return new MockValidator('maxValue', (args: any) => [ + Constraint.MaxValue(args.value), + ]); + case 'minLength': - return { - name: 'minLength', - validateSchema: () => null, - validateData: () => null, - getConstraints: (args) => [Constraint.MinLength(args.value)], - }; + return new MockValidator('minLength', (args: any) => [ + Constraint.MinLength(args.value), + ]); + case 'maxLength': - return { - name: 'maxLength', - validateSchema: () => null, - validateData: () => null, - getConstraints: (args) => [Constraint.MaxLength(args.value)], - }; + return new MockValidator('maxLength', (args: any) => [ + Constraint.MaxLength(args.value), + ]); case 'regex': - return { - name: 'regex', - validateSchema: () => null, - validateData: () => null, - getConstraints: (args) => [Constraint.Regex(args.value)], - }; + return new MockValidator('regex', (args: any) => [ + Constraint.Regex(args.value), + ]); case 'enum': - return { - name: 'enum', - validateSchema: () => null, - validateData: () => null, - getConstraints: (args) => [Constraint.Enum(args.value)], - }; + return new MockValidator('enum', (args: any) => [ + Constraint.Enum(args.value), + ]); default: throw new Error(`Validator ${name} is not implemented in test bed.`); } diff --git a/packages/core/src/containers/container.ts b/packages/core/src/containers/container.ts index 3899727b..91076d97 100644 --- a/packages/core/src/containers/container.ts +++ b/packages/core/src/containers/container.ts @@ -16,7 +16,9 @@ export class Container { } public async load(options: ICoreOptions) { - this.inversifyContainer.load(artifactBuilderModule(options.artifact)); + await this.inversifyContainer.loadAsync( + artifactBuilderModule(options.artifact) + ); await this.inversifyContainer.loadAsync(executorModule()); await this.inversifyContainer.loadAsync( templateEngineModule(options.template) @@ -25,6 +27,10 @@ export class Container { await this.inversifyContainer.loadAsync(extensionModule(options)); } + public async unload() { + await this.inversifyContainer.unbindAllAsync(); + } + public getInversifyContainer() { return this.inversifyContainer; } diff --git a/packages/core/src/containers/modules/artifactBuilder.ts b/packages/core/src/containers/modules/artifactBuilder.ts index 96ba5aff..f9d8c128 100644 --- a/packages/core/src/containers/modules/artifactBuilder.ts +++ b/packages/core/src/containers/modules/artifactBuilder.ts @@ -1,4 +1,4 @@ -import { ContainerModule, interfaces } from 'inversify'; +import { AsyncContainerModule, ContainerModule, interfaces } from 'inversify'; import { PersistentStore, LocalFilePersistentStore, @@ -16,7 +16,7 @@ import { import { ArtifactBuilderOptions } from '../../options'; export const artifactBuilderModule = (options: IArtifactBuilderOptions) => - new ContainerModule((bind) => { + new AsyncContainerModule(async (bind) => { // Options bind( TYPES.ArtifactBuilderInputOptions diff --git a/packages/core/src/lib/extension-loader/extensionLoader.ts b/packages/core/src/lib/extension-loader/extensionLoader.ts index c1594c7c..295e122c 100644 --- a/packages/core/src/lib/extension-loader/extensionLoader.ts +++ b/packages/core/src/lib/extension-loader/extensionLoader.ts @@ -79,7 +79,9 @@ export class ExtensionLoader { this.extensionRegistry.get(type)!.forEach(({ name, extension }) => { bind(type).to(extension); bind(TYPES.ExtensionConfig) - .toConstantValue(name.length > 0 ? this.config[name] : undefined) + // Note they we can't bind undefined to container or it throw error while unbinding. + // https://github.com/inversify/InversifyJS/issues/1462#issuecomment-1202099036 + .toConstantValue(name.length > 0 ? this.config[name] : {}) .whenInjectedInto(extension); }); } diff --git a/packages/core/test/containers/continer.spec.ts b/packages/core/test/containers/continer.spec.ts index c1dbe2fb..551753b1 100644 --- a/packages/core/test/containers/continer.spec.ts +++ b/packages/core/test/containers/continer.spec.ts @@ -33,6 +33,7 @@ it('Container should load options and resolve all dependencies', async () => { const artifactBuilder = container.get(TYPES.ArtifactBuilder); const { templates } = await templateEngine.compile(); await artifactBuilder.build({ templates, schemas: [] }); + await container.unload(); // Assert expect(fs.existsSync(resultPath)).toBeTruthy(); }); diff --git a/packages/core/test/validators/validatorLoader.spec.ts b/packages/core/test/validators/validatorLoader.spec.ts index 52e9945f..5cfa232b 100644 --- a/packages/core/test/validators/validatorLoader.spec.ts +++ b/packages/core/test/validators/validatorLoader.spec.ts @@ -16,10 +16,8 @@ describe('Test validator loader for built-in validators', () => { .inSingletonScope(); }); - afterEach(() => { - container.unbind(TYPES.ValidatorLoader); - // TODO: Found some issues while unloading extension module - // https://github.com/inversify/InversifyJS/issues/1462 + afterEach(async () => { + await container.unbindAllAsync(); }); it.each([ From 73e5ab9e09e38629634b094962ac02365647f882 Mon Sep 17 00:00:00 2001 From: Ivan Tsai Date: Wed, 3 Aug 2022 14:33:14 +0800 Subject: [PATCH 03/20] feat(core): add module name to extension --- packages/build/test/spec-generator/schema.ts | 2 +- packages/core/src/containers/index.ts | 1 + packages/core/src/containers/types.ts | 1 + packages/core/src/lib/extension-loader/extensionLoader.ts | 6 +++++- .../built-in-extensions/query-builder/reqTagRunner.ts | 3 ++- .../built-in-extensions/validator/filterChecker.ts | 4 +++- packages/core/src/models/extensions/base.ts | 7 ++++++- .../built-in-validators/dataTypeValidator.spec.ts | 8 ++++---- .../built-in-validators/integerTypeValidator.spec.ts | 8 ++++---- .../built-in-validators/requiredValidator.spec.ts | 8 ++++---- .../built-in-validators/stringTypeValidator.spec.ts | 8 ++++---- .../built-in-validators/uuidTypeValidator.spec.ts | 8 ++++---- packages/core/test/validators/validatorLoader.spec.ts | 4 ++-- 13 files changed, 41 insertions(+), 27 deletions(-) diff --git a/packages/build/test/spec-generator/schema.ts b/packages/build/test/spec-generator/schema.ts index 581dbb97..b0d57d6e 100644 --- a/packages/build/test/spec-generator/schema.ts +++ b/packages/build/test/spec-generator/schema.ts @@ -33,7 +33,7 @@ const getSchemaPaths = () => class MockValidator extends InputValidator { constructor(public name: string, private constraintsFn: any) { - super(); + super({}, name); } public validateData(): void { diff --git a/packages/core/src/containers/index.ts b/packages/core/src/containers/index.ts index 2f90771a..4479e8f2 100644 --- a/packages/core/src/containers/index.ts +++ b/packages/core/src/containers/index.ts @@ -1,3 +1,4 @@ import 'reflect-metadata'; export * from './types'; export * from './container'; +export * from './modules'; diff --git a/packages/core/src/containers/types.ts b/packages/core/src/containers/types.ts index c9abeda0..3a396dcc 100644 --- a/packages/core/src/containers/types.ts +++ b/packages/core/src/containers/types.ts @@ -27,6 +27,7 @@ export const TYPES = { ValidatorLoader: Symbol.for('ValidatorLoader'), // Extensions ExtensionConfig: Symbol.for('ExtensionConfig'), + ExtensionName: Symbol.for('ExtensionName'), Extension_TemplateEngine: Symbol.for('Extension_TemplateEngine'), Extension_InputValidator: Symbol.for('Extension_InputValidator'), }; diff --git a/packages/core/src/lib/extension-loader/extensionLoader.ts b/packages/core/src/lib/extension-loader/extensionLoader.ts index 295e122c..20dd4aa3 100644 --- a/packages/core/src/lib/extension-loader/extensionLoader.ts +++ b/packages/core/src/lib/extension-loader/extensionLoader.ts @@ -25,6 +25,7 @@ export class ExtensionLoader { this.config = config; } + /** Load external extensions (should be called by core package) */ public async loadExternalExtensionModules() { if (this.bound) throw new Error( @@ -81,7 +82,10 @@ export class ExtensionLoader { bind(TYPES.ExtensionConfig) // Note they we can't bind undefined to container or it throw error while unbinding. // https://github.com/inversify/InversifyJS/issues/1462#issuecomment-1202099036 - .toConstantValue(name.length > 0 ? this.config[name] : {}) + .toConstantValue(name.length > 0 ? this.config[name] || {} : {}) + .whenInjectedInto(extension); + bind(TYPES.ExtensionName) + .toConstantValue(name || '') .whenInjectedInto(extension); }); } diff --git a/packages/core/src/lib/template-engine/built-in-extensions/query-builder/reqTagRunner.ts b/packages/core/src/lib/template-engine/built-in-extensions/query-builder/reqTagRunner.ts index 3f7694c3..ce65a2a8 100644 --- a/packages/core/src/lib/template-engine/built-in-extensions/query-builder/reqTagRunner.ts +++ b/packages/core/src/lib/template-engine/built-in-extensions/query-builder/reqTagRunner.ts @@ -15,9 +15,10 @@ export class ReqTagRunner extends TagRunner { constructor( @inject(TYPES.ExtensionConfig) config: any, + @inject(TYPES.ExtensionName) name: string, @inject(TYPES.Executor) executor: IExecutor ) { - super(config); + super(config, name); this.executor = executor; } diff --git a/packages/core/src/lib/template-engine/built-in-extensions/validator/filterChecker.ts b/packages/core/src/lib/template-engine/built-in-extensions/validator/filterChecker.ts index ece0a7f4..2c2dc4b1 100644 --- a/packages/core/src/lib/template-engine/built-in-extensions/validator/filterChecker.ts +++ b/packages/core/src/lib/template-engine/built-in-extensions/validator/filterChecker.ts @@ -16,11 +16,13 @@ export class FilterChecker extends CompileTimeExtension { constructor( @inject(TYPES.ExtensionConfig) config: any, + @inject(TYPES.ExtensionName) + name: string, @inject(TYPES.CompilerEnvironment) @named('compileTime') compileTimeEnv: nunjucks.Environment ) { - super(config); + super(config, name); this.env = compileTimeEnv; } diff --git a/packages/core/src/models/extensions/base.ts b/packages/core/src/models/extensions/base.ts index 4bb053f4..053783c7 100644 --- a/packages/core/src/models/extensions/base.ts +++ b/packages/core/src/models/extensions/base.ts @@ -3,11 +3,16 @@ import { inject, injectable } from 'inversify'; @injectable() export abstract class ExtensionBase { + public readonly moduleName: string; public activate?(): Promise; private config?: C; - constructor(@inject(TYPES.ExtensionConfig) config?: C) { + constructor( + @inject(TYPES.ExtensionConfig) config: C, + @inject(TYPES.ExtensionName) name: string + ) { this.config = config; + this.moduleName = name; } protected getConfig(): C | undefined { diff --git a/packages/core/test/validators/built-in-validators/dataTypeValidator.spec.ts b/packages/core/test/validators/built-in-validators/dataTypeValidator.spec.ts index 8e58fd38..81f1a444 100644 --- a/packages/core/test/validators/built-in-validators/dataTypeValidator.spec.ts +++ b/packages/core/test/validators/built-in-validators/dataTypeValidator.spec.ts @@ -12,7 +12,7 @@ describe('Test "date" type validator', () => { // Arrange const args = JSON.parse(inputArgs); // Act - const validator = new DateTypeValidator(); + const validator = new DateTypeValidator({}, ''); // Assert expect(() => validator.validateSchema(args)).not.toThrow(); } @@ -29,7 +29,7 @@ describe('Test "date" type validator', () => { // Arrange const args = JSON.parse(inputArgs); // Act - const validator = new DateTypeValidator(); + const validator = new DateTypeValidator({}, ''); // Assert expect(() => validator.validateSchema(args)).toThrow(); @@ -50,7 +50,7 @@ describe('Test "date" type validator', () => { const args = JSON.parse(inputArgs); // Act - const validator = new DateTypeValidator(); + const validator = new DateTypeValidator({}, ''); // Assert expect(() => validator.validateData(data, args)).not.toThrow(); @@ -68,7 +68,7 @@ describe('Test "date" type validator', () => { const args = JSON.parse(inputArgs); // Act - const validator = new DateTypeValidator(); + const validator = new DateTypeValidator({}, ''); // Assert expect(() => validator.validateData(data, args)).toThrow(); diff --git a/packages/core/test/validators/built-in-validators/integerTypeValidator.spec.ts b/packages/core/test/validators/built-in-validators/integerTypeValidator.spec.ts index 47b89f11..852f0c75 100644 --- a/packages/core/test/validators/built-in-validators/integerTypeValidator.spec.ts +++ b/packages/core/test/validators/built-in-validators/integerTypeValidator.spec.ts @@ -26,7 +26,7 @@ describe('Test "integer" type validator', () => { // Arrange const args = JSON.parse(inputArgs); // Act - const validator = new IntegerTypeValidator(); + const validator = new IntegerTypeValidator({}, ''); // Assert expect(() => validator.validateSchema(args)).not.toThrow(); @@ -45,7 +45,7 @@ describe('Test "integer" type validator', () => { // Arrange const args = JSON.parse(inputArgs); // Act - const validator = new IntegerTypeValidator(); + const validator = new IntegerTypeValidator({}, ''); // Assert expect(() => validator.validateSchema(args)).toThrow(); @@ -68,7 +68,7 @@ describe('Test "integer" type validator', () => { // Arrange const args = JSON.parse(inputArgs); // Act - const validator = new IntegerTypeValidator(); + const validator = new IntegerTypeValidator({}, ''); // Assert expect(() => validator.validateData(data, args)).not.toThrow(); @@ -89,7 +89,7 @@ describe('Test "integer" type validator', () => { // Arrange const args = JSON.parse(inputArgs); // Act - const validator = new IntegerTypeValidator(); + const validator = new IntegerTypeValidator({}, ''); // Assert expect(() => validator.validateData(data, args)).toThrow(); diff --git a/packages/core/test/validators/built-in-validators/requiredValidator.spec.ts b/packages/core/test/validators/built-in-validators/requiredValidator.spec.ts index 39b04854..268e3553 100644 --- a/packages/core/test/validators/built-in-validators/requiredValidator.spec.ts +++ b/packages/core/test/validators/built-in-validators/requiredValidator.spec.ts @@ -15,7 +15,7 @@ describe('Test "required" type validator', () => { // Arrange const args = JSON.parse(inputArgs); // Act - const validator = new RequiredValidator(); + const validator = new RequiredValidator({}, ''); // Assert expect(() => validator.validateSchema(args)).not.toThrow(); @@ -34,7 +34,7 @@ describe('Test "required" type validator', () => { // Arrange const args = JSON.parse(inputArgs); // Act - const validator = new RequiredValidator(); + const validator = new RequiredValidator({}, ''); // Assert expect(() => validator.validateSchema(args)).toThrow(); @@ -55,7 +55,7 @@ describe('Test "required" type validator', () => { // Arrange const args = JSON.parse(inputArgs); // Act - const validator = new RequiredValidator(); + const validator = new RequiredValidator({}, ''); // Assert expect(() => validator.validateData(data, args)).not.toThrow(); @@ -79,7 +79,7 @@ describe('Test "required" type validator', () => { // Arrange const args = JSON.parse(inputArgs); // Act - const validator = new RequiredValidator(); + const validator = new RequiredValidator({}, ''); // Assert expect(() => validator.validateData(data, args)).toThrow(); diff --git a/packages/core/test/validators/built-in-validators/stringTypeValidator.spec.ts b/packages/core/test/validators/built-in-validators/stringTypeValidator.spec.ts index 1afb2c93..fa81afa5 100644 --- a/packages/core/test/validators/built-in-validators/stringTypeValidator.spec.ts +++ b/packages/core/test/validators/built-in-validators/stringTypeValidator.spec.ts @@ -18,7 +18,7 @@ describe('Test "string" type validator', () => { // Arrange const args = JSON.parse(inputArgs); // Act - const validator = new StringTypeValidator(); + const validator = new StringTypeValidator({}, ''); // Assert expect(() => validator.validateSchema(args)).not.toThrow(); @@ -37,7 +37,7 @@ describe('Test "string" type validator', () => { // Arrange const args = JSON.parse(inputArgs); // Act - const validator = new StringTypeValidator(); + const validator = new StringTypeValidator({}, ''); // Assert expect(() => validator.validateSchema(args)).toThrow(); @@ -55,7 +55,7 @@ describe('Test "string" type validator', () => { // Arrange const args = JSON.parse(inputArgs); // Act - const validator = new StringTypeValidator(); + const validator = new StringTypeValidator({}, ''); // Assert expect(() => validator.validateData(data, args)).not.toThrow(); @@ -72,7 +72,7 @@ describe('Test "string" type validator', () => { // Arrange const args = JSON.parse(inputArgs); // Act - const validator = new StringTypeValidator(); + const validator = new StringTypeValidator({}, ''); // Assert expect(() => validator.validateData(data, args)).toThrow(); diff --git a/packages/core/test/validators/built-in-validators/uuidTypeValidator.spec.ts b/packages/core/test/validators/built-in-validators/uuidTypeValidator.spec.ts index 24aa31bc..d50cc8ab 100644 --- a/packages/core/test/validators/built-in-validators/uuidTypeValidator.spec.ts +++ b/packages/core/test/validators/built-in-validators/uuidTypeValidator.spec.ts @@ -13,7 +13,7 @@ describe('Test "uuid" type validator ', () => { // Arrange const args = JSON.parse(inputArgs); // Act - const validator = new UUIDTypeValidator(); + const validator = new UUIDTypeValidator({}, ''); // Assert expect(() => validator.validateSchema(args)).not.toThrow(); @@ -32,7 +32,7 @@ describe('Test "uuid" type validator ', () => { // Arrange const args = JSON.parse(inputArgs); // Act - const validator = new UUIDTypeValidator(); + const validator = new UUIDTypeValidator({}, ''); // Assert expect(() => validator.validateSchema(args)).toThrow(); @@ -54,7 +54,7 @@ describe('Test "uuid" type validator ', () => { // Arrange const args = JSON.parse(inputArgs); // Act - const validator = new UUIDTypeValidator(); + const validator = new UUIDTypeValidator({}, ''); // Assert expect(() => validator.validateData(data, args)).not.toThrow(); @@ -72,7 +72,7 @@ describe('Test "uuid" type validator ', () => { // Arrange const args = JSON.parse(inputArgs); // Act - const validator = new UUIDTypeValidator(); + const validator = new UUIDTypeValidator({}, ''); // Assert expect(() => validator.validateData(data, args)).toThrow(); diff --git a/packages/core/test/validators/validatorLoader.spec.ts b/packages/core/test/validators/validatorLoader.spec.ts index 5cfa232b..f7469ea7 100644 --- a/packages/core/test/validators/validatorLoader.spec.ts +++ b/packages/core/test/validators/validatorLoader.spec.ts @@ -79,8 +79,8 @@ describe('Test validator loader for extension validators with one module', () => .inSingletonScope(); }); - afterEach(() => { - container.unbind(TYPES.ValidatorLoader); + afterEach(async () => { + await container.unbindAllAsync(); }); it.each([ // custom validator From b2df344ba21cf46ede0b5cb81761acc853632f00 Mon Sep 17 00:00:00 2001 From: Ivan Tsai Date: Wed, 3 Aug 2022 14:40:14 +0800 Subject: [PATCH 04/20] refactor(serve): unite extension loader - Use same container with core package instead of creating a new one (for external extensions). - Add two types of extensions: route middleware and formatter. - Bind Vulcan application to container. - Using centralized extension loader, remove the old loader in serve package. - Use new super class for all extensions. Co-authored-by: kokokuo --- packages/serve/src/containers/container.ts | 28 ++++--- .../src/containers/modules/application.ts | 8 ++ .../serve/src/containers/modules/extension.ts | 18 +++++ .../serve/src/containers/modules/index.ts | 4 +- ...teGeneratorModule.ts => routeGenerator.ts} | 4 +- packages/serve/src/containers/types.ts | 3 + packages/serve/src/index.ts | 1 - packages/serve/src/lib/app.ts | 31 ++++---- packages/serve/src/lib/loader.ts | 55 -------------- .../auditLogMiddleware.ts | 24 +++--- .../built-in-middleware/corsMiddleware.ts | 21 ------ .../middleware/built-in-middleware/index.ts | 20 ----- .../rateLimitMiddleware.ts | 22 ------ .../src/lib/middleware/corsMiddleware.ts | 16 ++++ packages/serve/src/lib/middleware/index.ts | 24 +++++- .../serve/src/lib/middleware/middleware.ts | 39 ---------- .../src/lib/middleware/rateLimitMiddleware.ts | 16 ++++ .../requestIdMiddleware.ts | 21 ++++-- .../response-format/helpers.ts | 2 +- .../response-format/index.ts | 0 .../response-format/middleware.ts | 48 ++++++------ .../lib/response-formatter/csvFormatter.ts | 16 ++-- .../serve/src/lib/response-formatter/index.ts | 2 - .../lib/response-formatter/jsonFormatter.ts | 12 +-- .../route/route-component/requestValidator.ts | 4 +- packages/serve/src/lib/server.ts | 7 +- packages/serve/src/models/extensions/index.ts | 2 + .../extensions}/responseFormatter.ts | 17 +++-- .../src/models/extensions/routeMiddleware.ts | 40 ++++++++++ packages/serve/src/models/index.ts | 2 +- packages/serve/src/models/middlewareConfig.ts | 27 ------- packages/serve/src/models/serveConfig.ts | 3 - packages/serve/test/app.spec.ts | 68 ++++++++++------- .../auditLogMiddleware.spec.ts | 25 +++---- .../corsMiddleware.spec.ts | 18 ++--- .../rateLimitMiddleware.spec.ts | 23 +++--- .../requestIdMiddleware.spec.ts | 42 +++++------ .../formatResponseMiddleware.spec.ts | 74 +++++++------------ .../response-format/helpers.spec.ts | 36 +++------ .../serve/test/middlewares/loader.spec.ts | 39 ---------- .../test-custom-middlewares/index.ts | 7 -- .../testModeMiddleware.ts | 11 +-- .../serve/test/response-formatter/csv.spec.ts | 4 +- .../test/response-formatter/json.spec.ts | 4 +- .../route-component/requestValidator.spec.ts | 8 +- packages/serve/test/test.spec.ts | 28 +++++++ 46 files changed, 414 insertions(+), 510 deletions(-) create mode 100644 packages/serve/src/containers/modules/application.ts create mode 100644 packages/serve/src/containers/modules/extension.ts rename packages/serve/src/containers/modules/{routeGeneratorModule.ts => routeGenerator.ts} (94%) delete mode 100644 packages/serve/src/lib/loader.ts rename packages/serve/src/lib/middleware/{built-in-middleware => }/auditLogMiddleware.ts (59%) delete mode 100644 packages/serve/src/lib/middleware/built-in-middleware/corsMiddleware.ts delete mode 100644 packages/serve/src/lib/middleware/built-in-middleware/index.ts delete mode 100644 packages/serve/src/lib/middleware/built-in-middleware/rateLimitMiddleware.ts create mode 100644 packages/serve/src/lib/middleware/corsMiddleware.ts delete mode 100644 packages/serve/src/lib/middleware/middleware.ts create mode 100644 packages/serve/src/lib/middleware/rateLimitMiddleware.ts rename packages/serve/src/lib/middleware/{built-in-middleware => }/requestIdMiddleware.ts (75%) rename packages/serve/src/lib/middleware/{built-in-middleware => }/response-format/helpers.ts (94%) rename packages/serve/src/lib/middleware/{built-in-middleware => }/response-format/index.ts (100%) rename packages/serve/src/lib/middleware/{built-in-middleware => }/response-format/middleware.ts (51%) create mode 100644 packages/serve/src/models/extensions/index.ts rename packages/serve/src/{lib/response-formatter => models/extensions}/responseFormatter.ts (82%) create mode 100644 packages/serve/src/models/extensions/routeMiddleware.ts delete mode 100644 packages/serve/src/models/middlewareConfig.ts delete mode 100644 packages/serve/test/middlewares/loader.spec.ts create mode 100644 packages/serve/test/test.spec.ts diff --git a/packages/serve/src/containers/container.ts b/packages/serve/src/containers/container.ts index 3cd7a85d..1dc09829 100644 --- a/packages/serve/src/containers/container.ts +++ b/packages/serve/src/containers/container.ts @@ -1,25 +1,35 @@ import { Container as InversifyContainer } from 'inversify'; import { Container as CoreContainer } from '@vulcan-sql/core'; -import { routeGeneratorModule } from './modules'; +import { + applicationModule, + extensionModule, + routeGeneratorModule, +} from './modules'; import { ServeConfig } from '../models'; export class Container { - private inversifyContainer = new InversifyContainer(); + private inversifyContainer?: InversifyContainer; + private coreContainer?: CoreContainer; public get(type: symbol) { - return this.inversifyContainer.get(type); + const instance = this.inversifyContainer?.get(type); + if (!instance) + throw new Error(`Cannot resolve ${type.toString()} in container`); + return instance; } public async load(config: ServeConfig) { - const coreContainer = new CoreContainer(); - await coreContainer.load(config); - this.inversifyContainer.parent = coreContainer.getInversifyContainer(); + this.coreContainer = new CoreContainer(); + await this.coreContainer.load(config); + this.inversifyContainer = this.coreContainer.getInversifyContainer(); this.inversifyContainer.load(routeGeneratorModule()); + await this.inversifyContainer.loadAsync(extensionModule(config)); + await this.inversifyContainer.loadAsync(applicationModule()); } - public unload() { - this.inversifyContainer.parent?.unbindAll(); - this.inversifyContainer.unbindAll(); + public async unload() { + await this.coreContainer?.unload(); + await this.inversifyContainer?.unbindAllAsync(); } public getInversifyContainer() { diff --git a/packages/serve/src/containers/modules/application.ts b/packages/serve/src/containers/modules/application.ts new file mode 100644 index 00000000..27a81a0d --- /dev/null +++ b/packages/serve/src/containers/modules/application.ts @@ -0,0 +1,8 @@ +import { VulcanApplication } from '@vulcan-sql/serve'; +import { AsyncContainerModule } from 'inversify'; +import { TYPES } from '../types'; + +export const applicationModule = () => + new AsyncContainerModule(async (bind) => { + bind(TYPES.VulcanApplication).to(VulcanApplication); + }); diff --git a/packages/serve/src/containers/modules/extension.ts b/packages/serve/src/containers/modules/extension.ts new file mode 100644 index 00000000..db5eaa64 --- /dev/null +++ b/packages/serve/src/containers/modules/extension.ts @@ -0,0 +1,18 @@ +import { ExtensionLoader } from '@vulcan-sql/core'; +import { AsyncContainerModule } from 'inversify'; +import { ServeConfig } from '../../models/serveConfig'; +import { BuiltInRouteMiddlewares } from '@vulcan-sql/serve/middleware'; +import { BuiltInFormatters } from '@vulcan-sql/serve/response-formatter'; + +export const extensionModule = (options: ServeConfig) => + new AsyncContainerModule(async (bind) => { + const loader = new ExtensionLoader(options); + // Internal extension modules + + // route middlewares (single module) + loader.loadInternalExtensionModule(BuiltInRouteMiddlewares); + // formatter (single module) + loader.loadInternalExtensionModule(BuiltInFormatters); + + loader.bindExtensions(bind); + }); diff --git a/packages/serve/src/containers/modules/index.ts b/packages/serve/src/containers/modules/index.ts index 6e0209ca..079db97d 100644 --- a/packages/serve/src/containers/modules/index.ts +++ b/packages/serve/src/containers/modules/index.ts @@ -1 +1,3 @@ -export * from './routeGeneratorModule'; +export * from './routeGenerator'; +export * from './extension'; +export * from './application'; diff --git a/packages/serve/src/containers/modules/routeGeneratorModule.ts b/packages/serve/src/containers/modules/routeGenerator.ts similarity index 94% rename from packages/serve/src/containers/modules/routeGeneratorModule.ts rename to packages/serve/src/containers/modules/routeGenerator.ts index 77ee62ec..3111e98b 100644 --- a/packages/serve/src/containers/modules/routeGeneratorModule.ts +++ b/packages/serve/src/containers/modules/routeGenerator.ts @@ -17,7 +17,7 @@ export const routeGeneratorModule = () => .to(RequestTransformer) .inSingletonScope(); - // Request Transformer + // Request Validator bind(TYPES.RequestValidator) .to(RequestValidator) .inSingletonScope(); @@ -27,7 +27,7 @@ export const routeGeneratorModule = () => .to(PaginationTransformer) .inSingletonScope(); - // Roue Generator + // Route Generator bind(TYPES.RouteGenerator) .to(RouteGenerator) .inSingletonScope(); diff --git a/packages/serve/src/containers/types.ts b/packages/serve/src/containers/types.ts index 84927da5..1cfddfe3 100644 --- a/packages/serve/src/containers/types.ts +++ b/packages/serve/src/containers/types.ts @@ -8,4 +8,7 @@ export const TYPES = { // Application AppConfig: Symbol.for('AppConfig'), VulcanApplication: Symbol.for('VulcanApplication'), + // Extensions + Extension_RouteMiddleware: Symbol.for('Extension_RouteMiddleware'), + Extension_Formatter: Symbol.for('Extension_Formatter'), }; diff --git a/packages/serve/src/index.ts b/packages/serve/src/index.ts index 22348702..28d9cbab 100644 --- a/packages/serve/src/index.ts +++ b/packages/serve/src/index.ts @@ -2,5 +2,4 @@ export * from './lib/route'; export * from './lib/middleware'; export * from './lib/app'; export * from './models'; -export * from './models'; export * from './containers'; diff --git a/packages/serve/src/lib/app.ts b/packages/serve/src/lib/app.ts index 3243cd2d..bcdc4b4e 100644 --- a/packages/serve/src/lib/app.ts +++ b/packages/serve/src/lib/app.ts @@ -2,7 +2,6 @@ import { APISchema } from '@vulcan-sql/core'; import * as Koa from 'koa'; import * as KoaRouter from 'koa-router'; import { isEmpty, uniq } from 'lodash'; -import { BaseRouteMiddleware, BuiltInRouteMiddlewares } from './middleware'; import { RestfulRoute, BaseRoute, @@ -10,18 +9,26 @@ import { GraphQLRoute, RouteGenerator, } from './route'; -import { AppConfig } from '../models'; -import { importExtensions, loadComponents } from './loader'; +import { inject, injectable, multiInject, optional } from 'inversify'; +import { TYPES } from '../containers'; +import { BaseRouteMiddleware } from '../models'; +@injectable() export class VulcanApplication { private app: Koa; - private config: AppConfig; private restfulRouter: KoaRouter; private graphqlRouter: KoaRouter; private generator: RouteGenerator; - constructor(config: AppConfig, generator: RouteGenerator) { - this.config = config; + private routeMiddlewares: BaseRouteMiddleware[]; + + constructor( + @inject(TYPES.RouteGenerator) generator: RouteGenerator, + @multiInject(TYPES.Extension_RouteMiddleware) + @optional() + routeMiddlewares: BaseRouteMiddleware[] = [] + ) { this.generator = generator; + this.routeMiddlewares = routeMiddlewares; this.app = new Koa(); this.restfulRouter = new KoaRouter(); this.graphqlRouter = new KoaRouter(); @@ -77,17 +84,7 @@ export class VulcanApplication { /** load built-in and extensions middleware classes for app used */ public async useMiddleware() { - // import extension middleware classes - const classesOfExtension = await importExtensions( - 'middlewares', - this.config.extensions - ); - const map = await loadComponents( - [...BuiltInRouteMiddlewares, ...classesOfExtension], - this.config - ); - for (const name of Object.keys(map)) { - const middleware = map[name]; + for (const middleware of this.routeMiddlewares) { this.app.use(middleware.handle.bind(middleware)); } } diff --git a/packages/serve/src/lib/loader.ts b/packages/serve/src/lib/loader.ts deleted file mode 100644 index b244f395..00000000 --- a/packages/serve/src/lib/loader.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { BaseResponseFormatter } from './response-formatter'; -import { - defaultImport, - ClassType, - ModuleProperties, - mergedModules, - SourceOfExtensions, -} from '@vulcan-sql/core'; -import { BaseRouteMiddleware } from './middleware'; -import { AppConfig } from '../models'; -// The extension module interface -export interface ExtensionModule extends ModuleProperties { - ['middlewares']: ClassType[]; - ['response-formatter']: ClassType[]; -} - -type ExtensionName = 'middlewares' | 'response-formatter'; - -export const importExtensions = async ( - name: ExtensionName, - extensions?: SourceOfExtensions -) => { - // if extensions setup, load response formatter classes in the extensions - if (extensions) { - // import extension which user customized - const modules = await defaultImport(...extensions); - const module = await mergedModules(modules); - // return middleware classes in folder - return module[name] || []; - } - return []; -}; - -/** - * load components which inherit supper vulcan component class, may contains built-in or extensions - * @param classesOfComponent the classes of component which inherit supper vulcan component class - * @returns the created instance - */ -export const loadComponents = async ( - classesOfComponent: ClassType[], - config?: AppConfig -): Promise<{ [name: string]: T }> => { - const map: { [name: string]: T } = {}; - // create each extension - for (const cls of classesOfComponent) { - const component = new cls(config) as T; - if (component.name in map) { - throw new Error( - `The identifier name "${component.name}" of component class ${cls.name} has been defined in other extensions` - ); - } - map[component.name] = component; - } - return map; -}; diff --git a/packages/serve/src/lib/middleware/built-in-middleware/auditLogMiddleware.ts b/packages/serve/src/lib/middleware/auditLogMiddleware.ts similarity index 59% rename from packages/serve/src/lib/middleware/built-in-middleware/auditLogMiddleware.ts rename to packages/serve/src/lib/middleware/auditLogMiddleware.ts index 7e6f555c..72822129 100644 --- a/packages/serve/src/lib/middleware/built-in-middleware/auditLogMiddleware.ts +++ b/packages/serve/src/lib/middleware/auditLogMiddleware.ts @@ -1,17 +1,17 @@ -import { getLogger, ILogger, LoggerOptions } from '@vulcan-sql/core'; -import { BuiltInMiddleware } from '../middleware'; +import { + getLogger, + LoggerOptions, + VulcanInternalExtension, +} from '@vulcan-sql/core'; +import { BuiltInMiddleware } from '@vulcan-sql/serve/models'; import { KoaRouterContext, KoaNext } from '@vulcan-sql/serve/route'; -import { AppConfig } from '@vulcan-sql/serve/models'; -export class AuditLoggingMiddleware extends BuiltInMiddleware { - private logger: ILogger; - constructor(config: AppConfig) { - super('audit-log', config); - - // read logger options from config, if is undefined will set default value - const options = this.getOptions() as LoggerOptions; - this.logger = getLogger({ scopeName: 'AUDIT', options }); - } +@VulcanInternalExtension('audit-log') +export class AuditLoggingMiddleware extends BuiltInMiddleware { + private logger = getLogger({ + scopeName: 'AUDIT', + options: this.getOptions(), + }); public async handle(context: KoaRouterContext, next: KoaNext) { if (!this.enabled) return next(); diff --git a/packages/serve/src/lib/middleware/built-in-middleware/corsMiddleware.ts b/packages/serve/src/lib/middleware/built-in-middleware/corsMiddleware.ts deleted file mode 100644 index 91f21b3e..00000000 --- a/packages/serve/src/lib/middleware/built-in-middleware/corsMiddleware.ts +++ /dev/null @@ -1,21 +0,0 @@ -import * as Koa from 'koa'; -import * as cors from '@koa/cors'; -import { KoaRouterContext, KoaNext } from '@vulcan-sql/serve/route'; -import { BuiltInMiddleware } from '../middleware'; -import { AppConfig } from '@vulcan-sql/serve/models'; - -export type CorsOptions = cors.Options; - -export class CorsMiddleware extends BuiltInMiddleware { - private koaCors: Koa.Middleware; - - constructor(config: AppConfig) { - super('cors', config); - const options = this.getOptions() as CorsOptions; - this.koaCors = cors(options); - } - public async handle(context: KoaRouterContext, next: KoaNext) { - if (!this.enabled) return next(); - return this.koaCors(context, next); - } -} diff --git a/packages/serve/src/lib/middleware/built-in-middleware/index.ts b/packages/serve/src/lib/middleware/built-in-middleware/index.ts deleted file mode 100644 index b0e5a17c..00000000 --- a/packages/serve/src/lib/middleware/built-in-middleware/index.ts +++ /dev/null @@ -1,20 +0,0 @@ -export * from './corsMiddleware'; -export * from './requestIdMiddleware'; -export * from './auditLogMiddleware'; -export * from './rateLimitMiddleware'; -export * from './response-format'; - -import { CorsMiddleware } from './corsMiddleware'; -import { RateLimitMiddleware } from './rateLimitMiddleware'; -import { RequestIdMiddleware } from './requestIdMiddleware'; -import { AuditLoggingMiddleware } from './auditLogMiddleware'; -import { ResponseFormatMiddleware } from './response-format'; - -// The order is the middleware running order -export const BuiltInRouteMiddlewares = [ - CorsMiddleware, - RateLimitMiddleware, - RequestIdMiddleware, - AuditLoggingMiddleware, - ResponseFormatMiddleware, -]; diff --git a/packages/serve/src/lib/middleware/built-in-middleware/rateLimitMiddleware.ts b/packages/serve/src/lib/middleware/built-in-middleware/rateLimitMiddleware.ts deleted file mode 100644 index aeeb75fa..00000000 --- a/packages/serve/src/lib/middleware/built-in-middleware/rateLimitMiddleware.ts +++ /dev/null @@ -1,22 +0,0 @@ -import * as Koa from 'koa'; -import { RateLimit, RateLimitOptions } from 'koa2-ratelimit'; -import { KoaRouterContext, KoaNext } from '@vulcan-sql/serve/route'; -import { BuiltInMiddleware } from '../middleware'; -import { AppConfig } from '@vulcan-sql/serve/models'; - -export { RateLimitOptions }; - -export class RateLimitMiddleware extends BuiltInMiddleware { - private koaRateLimit: Koa.Middleware; - constructor(config: AppConfig) { - super('rate-limit', config); - - const options = this.getOptions() as RateLimitOptions; - this.koaRateLimit = RateLimit.middleware(options); - } - - public async handle(context: KoaRouterContext, next: KoaNext) { - if (!this.enabled) return next(); - return this.koaRateLimit(context, next); - } -} diff --git a/packages/serve/src/lib/middleware/corsMiddleware.ts b/packages/serve/src/lib/middleware/corsMiddleware.ts new file mode 100644 index 00000000..d0dc608a --- /dev/null +++ b/packages/serve/src/lib/middleware/corsMiddleware.ts @@ -0,0 +1,16 @@ +import * as cors from '@koa/cors'; +import { KoaRouterContext, KoaNext } from '@vulcan-sql/serve/route'; +import { BuiltInMiddleware } from '@vulcan-sql/serve/models'; +import { VulcanInternalExtension } from '@vulcan-sql/core'; + +export type CorsOptions = cors.Options; + +@VulcanInternalExtension('cors') +export class CorsMiddleware extends BuiltInMiddleware { + private koaCors = cors(this.getOptions()); + + public async handle(context: KoaRouterContext, next: KoaNext) { + if (!this.enabled) return next(); + return this.koaCors(context, next); + } +} diff --git a/packages/serve/src/lib/middleware/index.ts b/packages/serve/src/lib/middleware/index.ts index 3cdea892..bab3afa3 100644 --- a/packages/serve/src/lib/middleware/index.ts +++ b/packages/serve/src/lib/middleware/index.ts @@ -1,3 +1,21 @@ -// export non-default -export { BaseRouteMiddleware } from './middleware'; -export * from './built-in-middleware'; +export * from './corsMiddleware'; +export * from './requestIdMiddleware'; +export * from './auditLogMiddleware'; +export * from './rateLimitMiddleware'; +export * from './response-format'; + +import { CorsMiddleware } from './corsMiddleware'; +import { RateLimitMiddleware } from './rateLimitMiddleware'; +import { RequestIdMiddleware } from './requestIdMiddleware'; +import { AuditLoggingMiddleware } from './auditLogMiddleware'; +import { ResponseFormatMiddleware } from './response-format'; +import { ClassType, ExtensionBase } from '@vulcan-sql/core'; + +// The order is the middleware running order +export const BuiltInRouteMiddlewares: ClassType[] = [ + CorsMiddleware, + RateLimitMiddleware, + RequestIdMiddleware, + AuditLoggingMiddleware, + ResponseFormatMiddleware, +]; diff --git a/packages/serve/src/lib/middleware/middleware.ts b/packages/serve/src/lib/middleware/middleware.ts deleted file mode 100644 index 885f2457..00000000 --- a/packages/serve/src/lib/middleware/middleware.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { AppConfig, BuiltInOptions } from '@vulcan-sql/serve/models'; -import { KoaRouterContext, KoaNext } from '@vulcan-sql/serve/route'; -import { isUndefined } from 'lodash'; - -export abstract class BaseRouteMiddleware { - protected config: AppConfig; - // An identifier to check the options set or not in the middlewares section of serve config - public readonly name: string; - constructor(name: string, config: AppConfig) { - this.name = name; - this.config = config; - } - public abstract handle( - context: KoaRouterContext, - next: KoaNext - ): Promise; - - protected getConfig() { - if (this.config && this.config.middlewares) - return this.config.middlewares[this.name]; - return undefined; - } -} - -export abstract class BuiltInMiddleware extends BaseRouteMiddleware { - // middleware is enabled or not, default is enabled beside you give "enabled: false" in config. - protected enabled: boolean; - constructor(name: string, config: AppConfig) { - super(name, config); - - const value = this.getConfig()?.['enabled'] as boolean; - this.enabled = isUndefined(value) ? true : value; - } - protected getOptions() { - if (this.getConfig()) - return this.getConfig()?.['options'] as BuiltInOptions; - return undefined; - } -} diff --git a/packages/serve/src/lib/middleware/rateLimitMiddleware.ts b/packages/serve/src/lib/middleware/rateLimitMiddleware.ts new file mode 100644 index 00000000..9e34f616 --- /dev/null +++ b/packages/serve/src/lib/middleware/rateLimitMiddleware.ts @@ -0,0 +1,16 @@ +import { RateLimit, RateLimitOptions } from 'koa2-ratelimit'; +import { KoaRouterContext, KoaNext } from '@vulcan-sql/serve/route'; +import { BuiltInMiddleware } from '@vulcan-sql/serve/models'; +import { VulcanInternalExtension } from '@vulcan-sql/core'; + +export { RateLimitOptions }; + +@VulcanInternalExtension('rate-limit') +export class RateLimitMiddleware extends BuiltInMiddleware { + private koaRateLimit = RateLimit.middleware(this.getOptions()); + + public async handle(context: KoaRouterContext, next: KoaNext) { + if (!this.enabled) return next(); + return this.koaRateLimit(context, next); + } +} diff --git a/packages/serve/src/lib/middleware/built-in-middleware/requestIdMiddleware.ts b/packages/serve/src/lib/middleware/requestIdMiddleware.ts similarity index 75% rename from packages/serve/src/lib/middleware/built-in-middleware/requestIdMiddleware.ts rename to packages/serve/src/lib/middleware/requestIdMiddleware.ts index f8c4d40d..b9dcba31 100644 --- a/packages/serve/src/lib/middleware/built-in-middleware/requestIdMiddleware.ts +++ b/packages/serve/src/lib/middleware/requestIdMiddleware.ts @@ -1,19 +1,28 @@ import * as uuid from 'uuid'; -import { FieldInType, asyncReqIdStorage } from '@vulcan-sql/core'; +import { + FieldInType, + asyncReqIdStorage, + VulcanInternalExtension, +} from '@vulcan-sql/core'; import { KoaRouterContext, KoaNext } from '@vulcan-sql/serve/route'; -import { BuiltInMiddleware } from '../middleware'; -import { AppConfig } from '@vulcan-sql/serve/models'; +import { BuiltInMiddleware } from '@vulcan-sql/serve/models'; +import { TYPES as CORE_TYPES } from '@vulcan-sql/core'; +import { inject } from 'inversify'; export interface RequestIdOptions { name: string; fieldIn: FieldInType.HEADER | FieldInType.QUERY; } -export class RequestIdMiddleware extends BuiltInMiddleware { +@VulcanInternalExtension('request-id') +export class RequestIdMiddleware extends BuiltInMiddleware { private options: RequestIdOptions; - constructor(config: AppConfig) { - super('request-id', config); + constructor( + @inject(CORE_TYPES.ExtensionConfig) config: any, + @inject(CORE_TYPES.ExtensionName) name: string + ) { + super(config, name); // read request-id options from config. this.options = (this.getOptions() as RequestIdOptions) || { name: 'X-Request-ID', diff --git a/packages/serve/src/lib/middleware/built-in-middleware/response-format/helpers.ts b/packages/serve/src/lib/middleware/response-format/helpers.ts similarity index 94% rename from packages/serve/src/lib/middleware/built-in-middleware/response-format/helpers.ts rename to packages/serve/src/lib/middleware/response-format/helpers.ts index e40e77f9..39009b6d 100644 --- a/packages/serve/src/lib/middleware/built-in-middleware/response-format/helpers.ts +++ b/packages/serve/src/lib/middleware/response-format/helpers.ts @@ -1,5 +1,5 @@ import { KoaRouterContext } from '@vulcan-sql/serve/route'; -import { BaseResponseFormatter } from '@vulcan-sql/serve/response-formatter'; +import { BaseResponseFormatter } from '@vulcan-sql/serve/models'; export type ResponseFormatterMap = { [name: string]: BaseResponseFormatter; diff --git a/packages/serve/src/lib/middleware/built-in-middleware/response-format/index.ts b/packages/serve/src/lib/middleware/response-format/index.ts similarity index 100% rename from packages/serve/src/lib/middleware/built-in-middleware/response-format/index.ts rename to packages/serve/src/lib/middleware/response-format/index.ts diff --git a/packages/serve/src/lib/middleware/built-in-middleware/response-format/middleware.ts b/packages/serve/src/lib/middleware/response-format/middleware.ts similarity index 51% rename from packages/serve/src/lib/middleware/built-in-middleware/response-format/middleware.ts rename to packages/serve/src/lib/middleware/response-format/middleware.ts index 4aeda792..75efd07d 100644 --- a/packages/serve/src/lib/middleware/built-in-middleware/response-format/middleware.ts +++ b/packages/serve/src/lib/middleware/response-format/middleware.ts @@ -1,48 +1,54 @@ -import { AppConfig } from '@vulcan-sql/serve/models'; -import { KoaRouterContext, KoaNext } from '@vulcan-sql/serve/route'; -import { BuiltInMiddleware } from '../../middleware'; -import { checkUsableFormat } from './helpers'; -import { importExtensions, loadComponents } from '@vulcan-sql/serve/loader'; import { BaseResponseFormatter, - BuiltInFormatters, -} from '@vulcan-sql/serve/response-formatter'; + BuiltInMiddleware, +} from '@vulcan-sql/serve/models'; +import { KoaRouterContext, KoaNext } from '@vulcan-sql/serve/route'; +import { checkUsableFormat, ResponseFormatterMap } from './helpers'; +import { VulcanInternalExtension } from '@vulcan-sql/core'; +import { TYPES as CORE_TYPES } from '@vulcan-sql/core'; +import { inject, multiInject } from 'inversify'; +import { TYPES } from '@vulcan-sql/serve/containers'; export type ResponseFormatOptions = { formats: string[]; default: string; }; -export class ResponseFormatMiddleware extends BuiltInMiddleware { +@VulcanInternalExtension('response-format') +export class ResponseFormatMiddleware extends BuiltInMiddleware { public readonly defaultFormat; public readonly supportedFormats: string[]; + private formatters: ResponseFormatterMap; - constructor(config: AppConfig) { - super('response-format', config); + constructor( + @inject(CORE_TYPES.ExtensionConfig) config: any, + @inject(CORE_TYPES.ExtensionName) name: string, + @multiInject(TYPES.Extension_Formatter) formatters: BaseResponseFormatter[] + ) { + super(config, name); const options = (this.getOptions() as ResponseFormatOptions) || {}; const formats = options.formats || []; + this.formatters = formatters.reduce( + (prev, formatter) => { + prev[formatter.name] = formatter; + return prev; + }, + {} + ); this.supportedFormats = formats.map((format) => format.toLowerCase()); this.defaultFormat = !options.default ? 'json' : options.default; } + public async handle(context: KoaRouterContext, next: KoaNext) { // return to skip the middleware, if disabled if (!this.enabled) return next(); - const classesOfExtension = await importExtensions( - 'response-formatter', - this.config.extensions - ); - const formatters = await loadComponents([ - ...BuiltInFormatters, - ...classesOfExtension, - ]); - // get supported and request format to use. const format = checkUsableFormat({ context, - formatters, + formatters: this.formatters, supportedFormats: this.supportedFormats, defaultFormat: this.defaultFormat, }); @@ -51,7 +57,7 @@ export class ResponseFormatMiddleware extends BuiltInMiddleware { // go to next to run middleware and route await next(); // format the response and route handler ran. - formatters[format].formatToResponse(context); + this.formatters[format].formatToResponse(context); return; } } diff --git a/packages/serve/src/lib/response-formatter/csvFormatter.ts b/packages/serve/src/lib/response-formatter/csvFormatter.ts index 5e92afda..30e3335a 100644 --- a/packages/serve/src/lib/response-formatter/csvFormatter.ts +++ b/packages/serve/src/lib/response-formatter/csvFormatter.ts @@ -1,8 +1,15 @@ import * as Stream from 'stream'; -import { DataColumn, getLogger } from '@vulcan-sql/core'; +import { + DataColumn, + getLogger, + VulcanInternalExtension, +} from '@vulcan-sql/core'; import { isArray, isObject, isUndefined } from 'lodash'; import { KoaRouterContext } from '../route'; -import { BaseResponseFormatter, toBuffer } from './responseFormatter'; +import { + BaseResponseFormatter, + toBuffer, +} from '../../models/extensions/responseFormatter'; const logger = getLogger({ scopeName: 'SERVE' }); @@ -74,10 +81,9 @@ class CsvTransformer extends Stream.Transform { } } +@VulcanInternalExtension() export class CsvFormatter extends BaseResponseFormatter { - constructor() { - super('csv'); - } + public readonly name = 'csv'; public format(data: Stream.Readable, columns?: DataColumn[]) { if (!columns) throw new Error('must provide columns'); diff --git a/packages/serve/src/lib/response-formatter/index.ts b/packages/serve/src/lib/response-formatter/index.ts index 6cd18d88..7e22958e 100644 --- a/packages/serve/src/lib/response-formatter/index.ts +++ b/packages/serve/src/lib/response-formatter/index.ts @@ -1,7 +1,5 @@ -export * from './responseFormatter'; export * from './csvFormatter'; export * from './jsonFormatter'; -export * from '../loader'; import { CsvFormatter } from './csvFormatter'; import { JsonFormatter } from './jsonFormatter'; diff --git a/packages/serve/src/lib/response-formatter/jsonFormatter.ts b/packages/serve/src/lib/response-formatter/jsonFormatter.ts index 4ddc20e4..636b7e0c 100644 --- a/packages/serve/src/lib/response-formatter/jsonFormatter.ts +++ b/packages/serve/src/lib/response-formatter/jsonFormatter.ts @@ -1,6 +1,9 @@ import * as Stream from 'stream'; -import { getLogger } from '@vulcan-sql/core'; -import { BaseResponseFormatter, toBuffer } from './responseFormatter'; +import { getLogger, VulcanInternalExtension } from '@vulcan-sql/core'; +import { + BaseResponseFormatter, + toBuffer, +} from '../../models/extensions/responseFormatter'; import { isUndefined } from 'lodash'; import { KoaRouterContext } from '../route'; @@ -46,10 +49,9 @@ class JsonStringTransformer extends Stream.Transform { } } +@VulcanInternalExtension() export class JsonFormatter extends BaseResponseFormatter { - constructor() { - super('json'); - } + public readonly name = 'json'; public format(data: Stream.Readable) { const jsonStream = new JsonStringTransformer(); diff --git a/packages/serve/src/lib/route/route-component/requestValidator.ts b/packages/serve/src/lib/route/route-component/requestValidator.ts index 7e10fe63..d5ec2979 100644 --- a/packages/serve/src/lib/route/route-component/requestValidator.ts +++ b/packages/serve/src/lib/route/route-component/requestValidator.ts @@ -35,7 +35,9 @@ export class RequestValidator implements IRequestValidator { ) { await Promise.all( schemaValidators.map(async (schemaValidator) => { - const validator = await this.validatorLoader.load(schemaValidator.name); + const validator = this.validatorLoader.getValidator( + schemaValidator.name + ); validator.validateData(fieldValue, schemaValidator.args); }) ); diff --git a/packages/serve/src/lib/server.ts b/packages/serve/src/lib/server.ts index 4243c0af..a0a533e0 100644 --- a/packages/serve/src/lib/server.ts +++ b/packages/serve/src/lib/server.ts @@ -1,9 +1,7 @@ -import { omit } from 'lodash'; import * as http from 'http'; import { Container, TYPES } from '../containers'; import { ServeConfig } from '../models'; import { VulcanApplication } from './app'; -import { RouteGenerator } from './route'; import { APISchema } from '@vulcan-sql/core'; export class VulcanServer { @@ -20,12 +18,11 @@ export class VulcanServer { if (this.server) throw new Error('Server has created, please close it first.'); - // Get generator + // Load container await this.container.load(this.config); - const generator = this.container.get(TYPES.RouteGenerator); // Create application - const app = new VulcanApplication(omit(this.config, 'template'), generator); + const app = this.container.get(TYPES.VulcanApplication); await app.useMiddleware(); await app.buildRoutes(this.schemas, this.config.types); // Run server diff --git a/packages/serve/src/models/extensions/index.ts b/packages/serve/src/models/extensions/index.ts new file mode 100644 index 00000000..00c2d76a --- /dev/null +++ b/packages/serve/src/models/extensions/index.ts @@ -0,0 +1,2 @@ +export * from './routeMiddleware'; +export * from './responseFormatter'; diff --git a/packages/serve/src/lib/response-formatter/responseFormatter.ts b/packages/serve/src/models/extensions/responseFormatter.ts similarity index 82% rename from packages/serve/src/lib/response-formatter/responseFormatter.ts rename to packages/serve/src/models/extensions/responseFormatter.ts index 027555a1..24c9db39 100644 --- a/packages/serve/src/lib/response-formatter/responseFormatter.ts +++ b/packages/serve/src/models/extensions/responseFormatter.ts @@ -1,7 +1,8 @@ -import { DataColumn } from '@vulcan-sql/core'; +import { DataColumn, ExtensionBase, VulcanExtension } from '@vulcan-sql/core'; import { has } from 'lodash'; import * as Stream from 'stream'; -import { KoaRouterContext } from '../route'; +import { TYPES } from '../../containers/types'; +import { KoaRouterContext } from '../../lib/route'; export type BodyResponse = { data: Stream.Readable; @@ -29,12 +30,12 @@ export interface IFormatter { formatToResponse(ctx: KoaRouterContext): void; } -export abstract class BaseResponseFormatter implements IFormatter { - public readonly name: string; - constructor(name: string) { - this.name = name; - } - +@VulcanExtension(TYPES.Extension_Formatter) +export abstract class BaseResponseFormatter + extends ExtensionBase + implements IFormatter +{ + public abstract readonly name: string; public formatToResponse(ctx: KoaRouterContext) { // return empty csv stream data or column is not exist if (!has(ctx.response.body, 'data') || !has(ctx.response.body, 'columns')) { diff --git a/packages/serve/src/models/extensions/routeMiddleware.ts b/packages/serve/src/models/extensions/routeMiddleware.ts new file mode 100644 index 00000000..e7cfa03f --- /dev/null +++ b/packages/serve/src/models/extensions/routeMiddleware.ts @@ -0,0 +1,40 @@ +import { ExtensionBase, VulcanExtension } from '@vulcan-sql/core'; +import { KoaRouterContext, KoaNext } from '@vulcan-sql/serve/route'; +import { inject } from 'inversify'; +import { isUndefined } from 'lodash'; +import { TYPES } from '../../containers/types'; +import { TYPES as CORE_TYPES } from '@vulcan-sql/core'; + +export interface BuiltInMiddlewareConfig