diff --git a/package.json b/package.json index 50063c52..ad90c03b 100644 --- a/package.json +++ b/package.json @@ -56,6 +56,7 @@ "from2": "^2.3.0", "jest": "27.5.1", "nx": "14.0.3", + "pg-mem": "^2.6.3", "prettier": "^2.5.1", "supertest": "^6.2.3", "ts-essentials": "^9.1.2", diff --git a/packages/build/src/containers/container.ts b/packages/build/src/containers/container.ts index e5609dbb..9af5ca38 100644 --- a/packages/build/src/containers/container.ts +++ b/packages/build/src/containers/container.ts @@ -1,25 +1,39 @@ import { Container as InversifyContainer } from 'inversify'; import { Container as CoreContainer } from '@vulcan-sql/core'; import { IBuildOptions } from '@vulcan-sql/build/models'; -import { schemaParserModule } from './modules'; +import { + documentGeneratorModule, + extensionModule, + 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.inversifyContainer.load(schemaParserModule(options.schemaParser)); + this.coreContainer = new CoreContainer(); + await this.coreContainer.load(options); + this.inversifyContainer = this.coreContainer.getInversifyContainer(); + await this.inversifyContainer.loadAsync( + schemaParserModule(options['schema-parser']) + ); + await this.inversifyContainer.loadAsync(extensionModule(options)); + await this.inversifyContainer.loadAsync( + documentGeneratorModule(options['document-generator']) + ); } - 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/containers/index.ts b/packages/build/src/containers/index.ts index 2f90771a..4479e8f2 100644 --- a/packages/build/src/containers/index.ts +++ b/packages/build/src/containers/index.ts @@ -1,3 +1,4 @@ import 'reflect-metadata'; export * from './types'; export * from './container'; +export * from './modules'; diff --git a/packages/build/src/containers/modules/documentGenerator.ts b/packages/build/src/containers/modules/documentGenerator.ts new file mode 100644 index 00000000..b92141d2 --- /dev/null +++ b/packages/build/src/containers/modules/documentGenerator.ts @@ -0,0 +1,25 @@ +import { + IDocumentGeneratorOptions, + SpecGenerator, +} from '@vulcan-sql/build/models'; +import { AsyncContainerModule, interfaces } from 'inversify'; +import { DocumentGenerator } from '../../lib/document-generator'; +import { DocumentGeneratorOptions } from '../../options/documentGenerator'; +import { TYPES } from '../types'; + +export const documentGeneratorModule = (options?: IDocumentGeneratorOptions) => + new AsyncContainerModule(async (bind) => { + // Options + bind( + TYPES.DocumentGeneratorInputOptions + ).toConstantValue(options || ({} as any)); + bind(TYPES.DocumentGeneratorOptions) + .to(DocumentGeneratorOptions) + .inSingletonScope(); + + // Document generator + bind(TYPES.DocumentGenerator).to(DocumentGenerator); + bind>( + TYPES.Factory_SpecGenerator + ).toAutoNamedFactory(TYPES.Extension_SpecGenerator); + }); diff --git a/packages/build/src/containers/modules/extension.ts b/packages/build/src/containers/modules/extension.ts new file mode 100644 index 00000000..fc00cf7e --- /dev/null +++ b/packages/build/src/containers/modules/extension.ts @@ -0,0 +1,19 @@ +import { builtInSchemaReader } from '@vulcan-sql/build/schema-parser'; +import { ExtensionLoader } from '@vulcan-sql/core'; +import { AsyncContainerModule } from 'inversify'; +import { builtInSpecGenerator } from '../../lib/document-generator'; +import { IBuildOptions } from '../../models/buildOptions'; + +export const extensionModule = (options: IBuildOptions) => + new AsyncContainerModule(async (bind) => { + const loader = new ExtensionLoader(options); + // Internal extension modules + + // Schema reader + loader.loadInternalExtensionModule(builtInSchemaReader); + + // Spec generator + loader.loadInternalExtensionModule(builtInSpecGenerator); + + loader.bindExtensions(bind); + }); diff --git a/packages/build/src/containers/modules/index.ts b/packages/build/src/containers/modules/index.ts index 1084ca58..08532140 100644 --- a/packages/build/src/containers/modules/index.ts +++ b/packages/build/src/containers/modules/index.ts @@ -1 +1,3 @@ export * from './schemaParser'; +export * from './extension'; +export * from './documentGenerator'; diff --git a/packages/build/src/containers/modules/schemaParser.ts b/packages/build/src/containers/modules/schemaParser.ts index faec3c8e..11789019 100644 --- a/packages/build/src/containers/modules/schemaParser.ts +++ b/packages/build/src/containers/modules/schemaParser.ts @@ -1,22 +1,18 @@ -import { - ISchemaParserOptions, - SchemaReaderType, -} from '@vulcan-sql/build/models'; -import { - FileSchemaReader, - SchemaParser, - SchemaReader, -} from '@vulcan-sql/build/schema-parser'; -import { ContainerModule, interfaces } from 'inversify'; +import { ISchemaParserOptions, SchemaReader } from '@vulcan-sql/build/models'; +import { SchemaParser } from '@vulcan-sql/build/schema-parser'; +import { AsyncContainerModule, interfaces } from 'inversify'; import { SchemaParserOptions } from '../../options/schemaParser'; import { TYPES } from '../types'; -import { SchemaParserMiddlewares } from '@vulcan-sql/build/schema-parser/middleware'; +import { + SchemaParserMiddleware, + SchemaParserMiddlewares, +} from '@vulcan-sql/build/schema-parser/middleware'; -export const schemaParserModule = (options: ISchemaParserOptions) => - new ContainerModule((bind) => { +export const schemaParserModule = (options?: ISchemaParserOptions) => + new AsyncContainerModule(async (bind) => { // Options bind(TYPES.SchemaParserInputOptions).toConstantValue( - options + options || ({} as any) ); bind(TYPES.SchemaParserOptions) .to(SchemaParserOptions) @@ -24,19 +20,25 @@ export const schemaParserModule = (options: ISchemaParserOptions) => // Schema reader bind(TYPES.SchemaReader) - .to(FileSchemaReader) - .inSingletonScope() - .whenTargetNamed(SchemaReaderType.LocalFile); - + .toDynamicValue((context) => { + const factory = context.container.get< + interfaces.AutoNamedFactory + >(TYPES.Factory_SchemaReader); + const options = context.container.get( + TYPES.SchemaParserOptions + ); + return factory(options.reader); + }) + .inSingletonScope(); bind>( TYPES.Factory_SchemaReader - ).toAutoNamedFactory(TYPES.SchemaReader); + ).toAutoNamedFactory(TYPES.Extension_SchemaReader); // Schema parser bind(TYPES.SchemaParser).to(SchemaParser).inSingletonScope(); // Middleware for (const middleware of SchemaParserMiddlewares) { - bind(TYPES.SchemaParserMiddleware).to(middleware); + bind(TYPES.SchemaParserMiddleware).to(middleware); } }); diff --git a/packages/build/src/containers/types.ts b/packages/build/src/containers/types.ts index b5e2a07f..28d18200 100644 --- a/packages/build/src/containers/types.ts +++ b/packages/build/src/containers/types.ts @@ -1,8 +1,17 @@ export const TYPES = { + // Schema SchemaParserInputOptions: Symbol.for('SchemaParserInputOptions'), SchemaParserOptions: Symbol.for('SchemaParserOptions'), SchemaReader: Symbol.for('SchemaReader'), Factory_SchemaReader: Symbol.for('Factory_SchemaReader'), SchemaParser: Symbol.for('SchemaParser'), SchemaParserMiddleware: Symbol.for('SchemaParserMiddleware'), + // Document + DocumentGenerator: Symbol.for('DocumentGenerator'), + DocumentGeneratorInputOptions: Symbol.for('DocumentGeneratorInputOptions'), + DocumentGeneratorOptions: Symbol.for('DocumentGeneratorOptions'), + Factory_SpecGenerator: Symbol.for('Factory_SpecGenerator'), + // Extension + Extension_SchemaReader: Symbol.for('Extension_SchemaReader'), + Extension_SpecGenerator: Symbol.for('Extension_SpecGenerator'), }; diff --git a/packages/build/src/lib/document-generator/documentGenerator.ts b/packages/build/src/lib/document-generator/documentGenerator.ts new file mode 100644 index 00000000..6cdfb616 --- /dev/null +++ b/packages/build/src/lib/document-generator/documentGenerator.ts @@ -0,0 +1,37 @@ +import { APISchema } from '@vulcan-sql/core'; +import { inject, injectable, interfaces } from 'inversify'; +import { TYPES } from '../../containers/types'; +import { SpecGenerator } from '../../models/extensions'; +import { DocumentGeneratorOptions } from '../../options/documentGenerator'; +import * as jsYAML from 'js-yaml'; +import * as path from 'path'; +import { promises as fs } from 'fs'; + +@injectable() +export class DocumentGenerator { + private specGenerators: SpecGenerator[]; + private folderPath: string; + + constructor( + @inject(TYPES.Factory_SpecGenerator) + specGeneratorFactory: interfaces.AutoNamedFactory, + @inject(TYPES.DocumentGeneratorOptions) options: DocumentGeneratorOptions + ) { + this.specGenerators = []; + for (const spec of options.specs) { + this.specGenerators.push(specGeneratorFactory(spec)); + } + this.folderPath = options.folderPath; + } + + public async generateDocuments(schemas: APISchema[]) { + for (const generator of this.specGenerators) { + const spec = generator.getSpec(schemas); + const filePath = path.resolve( + this.folderPath, + `spec-${generator.getExtensionId()}.yaml` + ); + await fs.writeFile(filePath, jsYAML.dump(spec), 'utf-8'); + } + } +} diff --git a/packages/build/src/lib/document-generator/index.ts b/packages/build/src/lib/document-generator/index.ts new file mode 100644 index 00000000..f82391cc --- /dev/null +++ b/packages/build/src/lib/document-generator/index.ts @@ -0,0 +1,2 @@ +export * from './spec-generator'; +export * from './documentGenerator'; diff --git a/packages/build/src/lib/document-generator/spec-generator/index.ts b/packages/build/src/lib/document-generator/spec-generator/index.ts new file mode 100644 index 00000000..5804bee8 --- /dev/null +++ b/packages/build/src/lib/document-generator/spec-generator/index.ts @@ -0,0 +1,4 @@ +import { OAS3SpecGenerator } from './oas3'; +export const builtInSpecGenerator = [OAS3SpecGenerator]; + +export * from './oas3'; diff --git a/packages/build/src/lib/spec-generator/oas3/index.ts b/packages/build/src/lib/document-generator/spec-generator/oas3/index.ts similarity index 100% rename from packages/build/src/lib/spec-generator/oas3/index.ts rename to packages/build/src/lib/document-generator/spec-generator/oas3/index.ts diff --git a/packages/build/src/lib/spec-generator/oas3/oas3SpecGenerator.ts b/packages/build/src/lib/document-generator/spec-generator/oas3/oas3SpecGenerator.ts similarity index 94% rename from packages/build/src/lib/spec-generator/oas3/oas3SpecGenerator.ts rename to packages/build/src/lib/document-generator/spec-generator/oas3/oas3SpecGenerator.ts index 24d129cb..8262c4bd 100644 --- a/packages/build/src/lib/spec-generator/oas3/oas3SpecGenerator.ts +++ b/packages/build/src/lib/document-generator/spec-generator/oas3/oas3SpecGenerator.ts @@ -1,4 +1,4 @@ -import { SpecGenerator } from '../specGenerator'; +import { SpecGenerator } from '../../../../models/extensions/specGenerator'; import * as oas3 from 'openapi3-ts'; import { APISchema, @@ -14,19 +14,24 @@ import { RequestSchema as RequestParameter, RequiredConstraint, ResponseProperty, + VulcanExtensionId, + VulcanInternalExtension, } from '@vulcan-sql/core'; import { isEmpty } from 'lodash'; +import { DocumentGeneratorSpec } from '@vulcan-sql/build/models'; +@VulcanInternalExtension() +@VulcanExtensionId(DocumentGeneratorSpec.oas3) export class OAS3SpecGenerator extends SpecGenerator { // Follow the OpenAPI specification version 3.0.3 // see https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md private oaiVersion = '3.0.3'; - public getSpec() { + public getSpec(schemas: APISchema[]) { const spec: oas3.OpenAPIObject = { openapi: this.getOAIVersion(), info: this.getInfo(), - paths: this.getPaths(), + paths: this.getPaths(schemas), }; return spec; } @@ -43,9 +48,8 @@ export class OAS3SpecGenerator extends SpecGenerator { }; } - private getPaths(): oas3.PathsObject { + private getPaths(schemas: APISchema[]): oas3.PathsObject { const paths: oas3.PathsObject = {}; - const schemas = this.getSchemas(); for (const schema of schemas) { paths[this.convertToOASPath(schema.urlPath)] = this.getPath(schema); } 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/schema-parser/schema-reader/fileSchemaReader.ts b/packages/build/src/lib/schema-parser/schema-reader/fileSchemaReader.ts index 485ddac7..a57a4abb 100644 --- a/packages/build/src/lib/schema-parser/schema-reader/fileSchemaReader.ts +++ b/packages/build/src/lib/schema-parser/schema-reader/fileSchemaReader.ts @@ -1,28 +1,46 @@ -import { SchemaFormat, SchemaData, SchemaReader } from './schemaReader'; +import { + SchemaFormat, + SchemaData, + SchemaReader, + SchemaReaderType, +} from '@vulcan-sql/build/models'; 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/build/containers'; import { SchemaParserOptions } from '@vulcan-sql/build/options'; +import { + VulcanExtensionId, + VulcanInternalExtension, + TYPES as CORE_TYPES, +} from '@vulcan-sql/core'; +import { inject } from 'inversify'; +import { TYPES } from '@vulcan-sql/build/containers'; export interface FileSchemaReaderOptions { folderPath: string; } -@injectable() -export class FileSchemaReader implements SchemaReader { +@VulcanInternalExtension() +@VulcanExtensionId(SchemaReaderType.LocalFile) +export class FileSchemaReader extends SchemaReader { private options: SchemaParserOptions; - constructor(@inject(TYPES.SchemaParserOptions) options: SchemaParserOptions) { + constructor( + @inject(TYPES.SchemaParserOptions) options: SchemaParserOptions, + @inject(CORE_TYPES.ExtensionConfig) config: any, + @inject(CORE_TYPES.ExtensionName) moduleName: string + ) { + super(config, moduleName); this.options = options; } public async *readSchema(): AsyncGenerator { + if (!this.options?.folderPath) + throw new Error(`Config schema-parser.folderPath must be defined`); const files = await this.getSchemaFilePaths(); for (const file of files) { - const fileName = path.relative(this.options.folderPath, file); + const fileName = path.relative(this.options!.folderPath, file); const { ext } = path.parse(fileName); const sourceName = fileName.replace(new RegExp(`\\${ext}$`), ''); yield { @@ -36,7 +54,7 @@ export class FileSchemaReader implements SchemaReader { private async getSchemaFilePaths(): Promise { return new Promise((resolve, reject) => { glob( - path.resolve(this.options.folderPath, '**', '*.yaml'), + path.resolve(this.options!.folderPath, '**', '*.yaml'), { nodir: true }, (err, files) => { if (err) return reject(err); diff --git a/packages/build/src/lib/schema-parser/schema-reader/index.ts b/packages/build/src/lib/schema-parser/schema-reader/index.ts index e3adf38f..459e4462 100644 --- a/packages/build/src/lib/schema-parser/schema-reader/index.ts +++ b/packages/build/src/lib/schema-parser/schema-reader/index.ts @@ -1,2 +1,4 @@ -export * from './schemaReader'; +import { FileSchemaReader } from './fileSchemaReader'; +export const builtInSchemaReader = [FileSchemaReader]; + export * from './fileSchemaReader'; diff --git a/packages/build/src/lib/schema-parser/schema-reader/schemaReader.ts b/packages/build/src/lib/schema-parser/schema-reader/schemaReader.ts deleted file mode 100644 index 47b3704c..00000000 --- a/packages/build/src/lib/schema-parser/schema-reader/schemaReader.ts +++ /dev/null @@ -1,14 +0,0 @@ -export enum SchemaFormat { - YAML = 'YAML', -} - -export interface SchemaData { - /** The identifier of this schema, we might use this name to mapping SQL sources. */ - sourceName: string; - content: string; - type: SchemaFormat; -} - -export interface SchemaReader { - readSchema(): AsyncGenerator; -} diff --git a/packages/build/src/lib/schema-parser/schemaParser.ts b/packages/build/src/lib/schema-parser/schemaParser.ts index 5c7d4793..4304cc4e 100644 --- a/packages/build/src/lib/schema-parser/schemaParser.ts +++ b/packages/build/src/lib/schema-parser/schemaParser.ts @@ -1,5 +1,9 @@ import { APISchema, AllTemplateMetadata } from '@vulcan-sql/core'; -import { SchemaData, SchemaFormat, SchemaReader } from './schema-reader'; +import { + SchemaData, + SchemaFormat, + SchemaReader, +} from '@vulcan-sql/build/models'; import * as yaml from 'js-yaml'; import { RawAPISchema, SchemaParserMiddleware } from './middleware'; import * as compose from 'koa-compose'; @@ -12,7 +16,6 @@ import { } from 'inversify'; import { TYPES } from '@vulcan-sql/build/containers'; import { SchemaParserOptions } from '@vulcan-sql/build/options'; - export interface SchemaParseResult { schemas: APISchema[]; } @@ -23,14 +26,13 @@ export class SchemaParser { private middleware: SchemaParserMiddleware['handle'][] = []; constructor( - @inject(TYPES.Factory_SchemaReader) - schemaReaderFactory: interfaces.AutoNamedFactory, - @inject(TYPES.SchemaParserOptions) schemaParserOptions: SchemaParserOptions, + @inject(TYPES.SchemaReader) + schemaReader: SchemaReader, @multiInject(TYPES.SchemaParserMiddleware) @optional() middlewares: SchemaParserMiddleware[] = [] ) { - this.schemaReader = schemaReaderFactory(schemaParserOptions.reader); + this.schemaReader = schemaReader; // Load middleware middlewares.forEach(this.use.bind(this)); diff --git a/packages/build/src/lib/spec-generator/index.ts b/packages/build/src/lib/spec-generator/index.ts deleted file mode 100644 index d5a1984b..00000000 --- a/packages/build/src/lib/spec-generator/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './oas3'; diff --git a/packages/build/src/lib/spec-generator/specGenerator.ts b/packages/build/src/lib/spec-generator/specGenerator.ts deleted file mode 100644 index 327deeed..00000000 --- a/packages/build/src/lib/spec-generator/specGenerator.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { APISchema } from '@vulcan-sql/core'; -import { IBuildOptions } from '../../models/buildOptions'; - -export abstract class SpecGenerator { - private schemas: APISchema[]; - private config: IBuildOptions; - - constructor(schemas: APISchema[], config: IBuildOptions) { - this.schemas = schemas; - this.config = config; - } - - abstract getSpec(): T; - - protected getName() { - return this.config.name || 'API Server'; - } - - protected getDescription() { - return this.config.description; - } - - protected getVersion() { - return this.config.version || '0.0.1'; - } - - protected getSchemas() { - return this.schemas; - } -} diff --git a/packages/build/src/lib/vulcanBuilder.ts b/packages/build/src/lib/vulcanBuilder.ts index 36315f4b..303601bf 100644 --- a/packages/build/src/lib/vulcanBuilder.ts +++ b/packages/build/src/lib/vulcanBuilder.ts @@ -6,6 +6,7 @@ import { TYPES as CORE_TYPES, VulcanArtifactBuilder, } from '@vulcan-sql/core'; +import { DocumentGenerator } from './document-generator'; export class VulcanBuilder { public async build(options: IBuildOptions) { @@ -18,12 +19,17 @@ export class VulcanBuilder { const artifactBuilder = container.get( CORE_TYPES.ArtifactBuilder ); + const documentGenerator = container.get( + TYPES.DocumentGenerator + ); const { metadata, templates } = await templateEngine.compile(); const { schemas } = await schemaParser.parse({ metadata }); await artifactBuilder.build({ schemas, templates }); - container.unload(); + await documentGenerator.generateDocuments(schemas); + + await container.unload(); } } diff --git a/packages/build/src/models/buildOptions.ts b/packages/build/src/models/buildOptions.ts index 12d44c20..e32b2ffa 100644 --- a/packages/build/src/models/buildOptions.ts +++ b/packages/build/src/models/buildOptions.ts @@ -1,9 +1,8 @@ import { ICoreOptions } from '@vulcan-sql/core'; +import { IDocumentGeneratorOptions } from './documentGeneratorOptions'; import { ISchemaParserOptions } from './schemaParserOptions'; export interface IBuildOptions extends ICoreOptions { - name?: string; - description?: string; - version?: string; - schemaParser: ISchemaParserOptions; + 'schema-parser'?: ISchemaParserOptions; + 'document-generator'?: IDocumentGeneratorOptions; } diff --git a/packages/build/src/models/documentGeneratorOptions.ts b/packages/build/src/models/documentGeneratorOptions.ts new file mode 100644 index 00000000..976301e6 --- /dev/null +++ b/packages/build/src/models/documentGeneratorOptions.ts @@ -0,0 +1,9 @@ +export enum DocumentGeneratorSpec { + oas3 = 'oas3', +} + +export interface IDocumentGeneratorOptions { + /** Target specification of our APIs, e.g. OpenAPI, Tinyspec ...etc. */ + specs?: (string | DocumentGeneratorSpec)[]; + folderPath: string; +} diff --git a/packages/build/src/models/extensions/index.ts b/packages/build/src/models/extensions/index.ts new file mode 100644 index 00000000..b25ba372 --- /dev/null +++ b/packages/build/src/models/extensions/index.ts @@ -0,0 +1,2 @@ +export * from './schemaReader'; +export * from './specGenerator'; diff --git a/packages/build/src/models/extensions/schemaReader.ts b/packages/build/src/models/extensions/schemaReader.ts new file mode 100644 index 00000000..ffb147d7 --- /dev/null +++ b/packages/build/src/models/extensions/schemaReader.ts @@ -0,0 +1,18 @@ +import { ExtensionBase, VulcanExtension } from '@vulcan-sql/core'; +import { TYPES } from '../../containers/types'; + +export enum SchemaFormat { + YAML = 'YAML', +} + +export interface SchemaData { + /** The identifier of this schema, we might use this name to mapping SQL sources. */ + sourceName: string; + content: string; + type: SchemaFormat; +} + +@VulcanExtension(TYPES.Extension_SchemaReader, { enforcedId: true }) +export abstract class SchemaReader extends ExtensionBase { + abstract readSchema(): AsyncGenerator; +} diff --git a/packages/build/src/models/extensions/specGenerator.ts b/packages/build/src/models/extensions/specGenerator.ts new file mode 100644 index 00000000..77a0a489 --- /dev/null +++ b/packages/build/src/models/extensions/specGenerator.ts @@ -0,0 +1,37 @@ +import { + APISchema, + ExtensionBase, + ProjectOptions, + TYPES as CORE_TYPES, + VulcanExtension, +} from '@vulcan-sql/core'; +import { inject } from 'inversify'; +import { TYPES } from '../../containers/types'; + +@VulcanExtension(TYPES.Extension_SpecGenerator, { enforcedId: true }) +export abstract class SpecGenerator extends ExtensionBase { + abstract getSpec(schemas: APISchema[]): T; + + private projectOption: ProjectOptions; + + constructor( + @inject(CORE_TYPES.ProjectOptions) projectOption: ProjectOptions, + @inject(CORE_TYPES.ExtensionName) moduleName: string, + @inject(CORE_TYPES.ExtensionConfig) config: C + ) { + super(config, moduleName); + this.projectOption = projectOption; + } + + protected getName() { + return this.projectOption.name || 'API Server'; + } + + protected getDescription() { + return this.projectOption.description; + } + + protected getVersion() { + return this.projectOption.version || '0.0.1'; + } +} diff --git a/packages/build/src/models/index.ts b/packages/build/src/models/index.ts index dddb9211..92f21b7c 100644 --- a/packages/build/src/models/index.ts +++ b/packages/build/src/models/index.ts @@ -1,2 +1,4 @@ export * from './buildOptions'; export * from './schemaParserOptions'; +export * from './extensions'; +export * from './documentGeneratorOptions'; diff --git a/packages/build/src/models/schemaParserOptions.ts b/packages/build/src/models/schemaParserOptions.ts index a88c9d8f..fd5ef11d 100644 --- a/packages/build/src/models/schemaParserOptions.ts +++ b/packages/build/src/models/schemaParserOptions.ts @@ -1,8 +1,8 @@ -export interface ISchemaParserOptions { - reader: SchemaReaderType; - folderPath: string; -} - export enum SchemaReaderType { LocalFile = 'LocalFile', } + +export interface ISchemaParserOptions { + reader: SchemaReaderType | string; + folderPath: string; +} diff --git a/packages/build/src/options/documentGenerator.ts b/packages/build/src/options/documentGenerator.ts new file mode 100644 index 00000000..8e633456 --- /dev/null +++ b/packages/build/src/options/documentGenerator.ts @@ -0,0 +1,28 @@ +import { injectable, inject, optional } from 'inversify'; +import { TYPES } from '@vulcan-sql/build/containers'; +import { IDocumentGeneratorOptions } from '@vulcan-sql/build/models'; +import { IsOptional, IsArray, validateSync, IsString } from 'class-validator'; + +@injectable() +export class DocumentGeneratorOptions implements IDocumentGeneratorOptions { + @IsArray() + @IsOptional() + public readonly specs = ['oas3']; + + @IsString() + public readonly folderPath!: string; + + constructor( + @inject(TYPES.DocumentGeneratorInputOptions) + @optional() + options: Partial = {} + ) { + Object.assign(this, options); + const errors = validateSync(this); + if (errors.length > 0) { + throw new Error( + 'Invalid document generator options: ' + errors.join(', ') + ); + } + } +} diff --git a/packages/build/src/options/index.ts b/packages/build/src/options/index.ts index 1084ca58..c4127ed3 100644 --- a/packages/build/src/options/index.ts +++ b/packages/build/src/options/index.ts @@ -1 +1,2 @@ export * from './schemaParser'; +export * from './documentGenerator'; diff --git a/packages/build/src/options/schemaParser.ts b/packages/build/src/options/schemaParser.ts index 50e6770b..4a51adef 100644 --- a/packages/build/src/options/schemaParser.ts +++ b/packages/build/src/options/schemaParser.ts @@ -9,7 +9,7 @@ import { IsOptional, IsString, validateSync } from 'class-validator'; @injectable() export class SchemaParserOptions implements ISchemaParserOptions { @IsString() - public readonly reader: SchemaReaderType = SchemaReaderType.LocalFile; + public readonly reader: string = SchemaReaderType.LocalFile; @IsString() @IsOptional() diff --git a/packages/build/test/builder/.gitignore b/packages/build/test/builder/.gitignore index 49850731..860deb70 100644 --- a/packages/build/test/builder/.gitignore +++ b/packages/build/test/builder/.gitignore @@ -1 +1,2 @@ -result.json \ No newline at end of file +result.json +spec-*.yaml \ No newline at end of file diff --git a/packages/build/test/builder/builder.spec.ts b/packages/build/test/builder/builder.spec.ts index 424ee30e..1e6a8b5d 100644 --- a/packages/build/test/builder/builder.spec.ts +++ b/packages/build/test/builder/builder.spec.ts @@ -1,9 +1,13 @@ import { VulcanBuilder } from '../../src'; import * as path from 'path'; -import { IBuildOptions, SchemaReaderType } from '@vulcan-sql/build/models'; import { - PersistentStoreType, - SerializerType, + DocumentGeneratorSpec, + IBuildOptions, + SchemaReaderType, +} from '@vulcan-sql/build/models'; +import { + ArtifactBuilderProviderType, + ArtifactBuilderSerializerType, TemplateProviderType, } from '@vulcan-sql/core'; @@ -11,20 +15,24 @@ it('Builder.build should work', async () => { // Arrange const builder = new VulcanBuilder(); const options: IBuildOptions = { - schemaParser: { + 'schema-parser': { reader: SchemaReaderType.LocalFile, folderPath: path.resolve(__dirname, 'source'), }, + 'document-generator': { + specs: [DocumentGeneratorSpec.oas3], + folderPath: path.resolve(__dirname), + }, artifact: { - provider: PersistentStoreType.LocalFile, - serializer: SerializerType.JSON, + provider: ArtifactBuilderProviderType.LocalFile, + serializer: ArtifactBuilderSerializerType.JSON, filePath: path.resolve(__dirname, 'result.json'), }, template: { provider: TemplateProviderType.LocalFile, folderPath: path.resolve(__dirname, 'source'), }, - extensions: [], + extensions: {}, }; // Act, Assert diff --git a/packages/build/test/builder/source/detail/role.yaml b/packages/build/test/builder/source/detail/role.yaml index a49c6827..2ec8b2fc 100644 --- a/packages/build/test/builder/source/detail/role.yaml +++ b/packages/build/test/builder/source/detail/role.yaml @@ -2,7 +2,7 @@ name: Role url: /detail/role/:id request: - name: id - in: query + fieldIn: query description: role id validators: - uuid diff --git a/packages/build/test/builder/source/user.yaml b/packages/build/test/builder/source/user.yaml index d6e4111b..4a324552 100644 --- a/packages/build/test/builder/source/user.yaml +++ b/packages/build/test/builder/source/user.yaml @@ -2,7 +2,7 @@ name: User url: /user/:id request: - name: id - in: query + fieldIn: query description: user id validators: - uuid diff --git a/packages/build/test/document-generator/.gitignore b/packages/build/test/document-generator/.gitignore new file mode 100644 index 00000000..1265a84a --- /dev/null +++ b/packages/build/test/document-generator/.gitignore @@ -0,0 +1 @@ +spec-*.yaml \ No newline at end of file diff --git a/packages/build/test/document-generator/document-generator.spec.ts b/packages/build/test/document-generator/document-generator.spec.ts new file mode 100644 index 00000000..92f20d9f --- /dev/null +++ b/packages/build/test/document-generator/document-generator.spec.ts @@ -0,0 +1,34 @@ +import { SpecGenerator } from '@vulcan-sql/build'; +import { DocumentGenerator } from '@vulcan-sql/build/doc-generator'; +import * as sinon from 'ts-sinon'; +import * as path from 'path'; +import { promises as fs } from 'fs'; +import faker from '@faker-js/faker'; + +it('Document generator should write YAML files while generating documents', async () => { + // Arrange + const mockSpec = { someSpec: faker.datatype.number() }; + const documentGenerator = new DocumentGenerator( + (id: string) => { + const mockSpecGenerator = sinon.stubInterface(); + mockSpecGenerator.getSpec.returns(mockSpec); + mockSpecGenerator.getExtensionId.returns(id); + return mockSpecGenerator; + }, + { + specs: ['spec1', 'spec2'], + folderPath: __dirname, + } + ); + + // Act + await documentGenerator.generateDocuments([]); + + // Arrange + expect( + await fs.readFile(path.resolve(__dirname, 'spec-spec1.yaml'), 'utf-8') + ).toEqual(`someSpec: ${mockSpec.someSpec}\n`); + expect( + await fs.readFile(path.resolve(__dirname, 'spec-spec2.yaml'), 'utf-8') + ).toEqual(`someSpec: ${mockSpec.someSpec}\n`); +}); diff --git a/packages/build/test/spec-generator/.gitignore b/packages/build/test/document-generator/spec-generator/.gitignore similarity index 100% rename from packages/build/test/spec-generator/.gitignore rename to packages/build/test/document-generator/spec-generator/.gitignore diff --git a/packages/build/test/spec-generator/oas3-errors.spec.ts b/packages/build/test/document-generator/spec-generator/oas3-errors.spec.ts similarity index 56% rename from packages/build/test/spec-generator/oas3-errors.spec.ts rename to packages/build/test/document-generator/spec-generator/oas3-errors.spec.ts index fd629663..72dd8fc8 100644 --- a/packages/build/test/spec-generator/oas3-errors.spec.ts +++ b/packages/build/test/document-generator/spec-generator/oas3-errors.spec.ts @@ -1,9 +1,11 @@ -import { OAS3SpecGenerator } from '@vulcan-sql/build/spec-generator'; +import { OAS3SpecGenerator } from '@vulcan-sql/build/doc-generator'; it('Should throw error with invalid fieldIn', async () => { // Arrange - const generator = new OAS3SpecGenerator( - [ + const generator = new OAS3SpecGenerator({}, '', {}); + // Act, Arrange + expect(() => + generator.getSpec([ { urlPath: '/user', request: [ @@ -13,19 +15,16 @@ it('Should throw error with invalid fieldIn', async () => { }, ], } as any, - ], - {} as any - ); - // Act, Arrange - expect(() => generator.getSpec()).toThrow( - `FieldInType INVALID_VALUE is not supported` - ); + ]) + ).toThrow(`FieldInType INVALID_VALUE is not supported`); }); it('Should throw error with invalid FieldType', async () => { // Arrange - const generator = new OAS3SpecGenerator( - [ + const generator = new OAS3SpecGenerator({}, '', {}); + // Act, Arrange + expect(() => + generator.getSpec([ { urlPath: '/user', request: [ @@ -36,11 +35,6 @@ it('Should throw error with invalid FieldType', async () => { }, ], } as any, - ], - {} as any - ); - // Act, Arrange - expect(() => generator.getSpec()).toThrow( - `FieldDataType INVALID_VALUE is not supported` - ); + ]) + ).toThrow(`FieldDataType INVALID_VALUE is not supported`); }); diff --git a/packages/build/test/spec-generator/oas3.spec.ts b/packages/build/test/document-generator/spec-generator/oas3.spec.ts similarity index 90% rename from packages/build/test/spec-generator/oas3.spec.ts rename to packages/build/test/document-generator/spec-generator/oas3.spec.ts index fec33d8e..9e9c575c 100644 --- a/packages/build/test/spec-generator/oas3.spec.ts +++ b/packages/build/test/document-generator/spec-generator/oas3.spec.ts @@ -1,4 +1,4 @@ -import { OAS3SpecGenerator } from '@vulcan-sql/build/spec-generator'; +import { OAS3SpecGenerator } from '@vulcan-sql/build/doc-generator'; import { getSchemas, getConfig } from './schema'; import * as jsYaml from 'js-yaml'; import { promises as fs } from 'fs'; @@ -6,17 +6,17 @@ import * as path from 'path'; import { get } from 'lodash'; const getGenerator = async () => { - const schema = await getSchemas(); const config = getConfig(); - return new OAS3SpecGenerator(schema, config); + return new OAS3SpecGenerator(config, '', {}); }; it('Should generate specification without error', async () => { // Arrange const generator = await getGenerator(); + const schemas = await getSchemas(); // Act, Arrange expect(async () => { - const spec = generator.getSpec(); + const spec = generator.getSpec(schemas); await fs.writeFile( path.resolve(__dirname, 'oas3-spec.yaml'), jsYaml.dump(spec), @@ -28,8 +28,9 @@ it('Should generate specification without error', async () => { it('Parameters in path should be converted to correct format', async () => { // Arrange const generator = await getGenerator(); + const schemas = await getSchemas(); // Act - const spec = generator.getSpec(); + const spec = generator.getSpec(schemas); // Arrange expect(Object.keys(spec.paths)[0]).toBe('/user/{id}'); expect(Object.keys(spec.paths)[1]).toBe('/user/{id}/order/{oid}'); @@ -39,8 +40,9 @@ it('Parameters in path should be converted to correct format', async () => { it('Should extract the correct parameters', async () => { // Arrange const generator = await getGenerator(); + const schemas = await getSchemas(); // Act - const spec = generator.getSpec(); + const spec = generator.getSpec(schemas); // Arrange expect(spec.paths['/user/{id}']?.get.parameters[0]).toEqual( expect.objectContaining({ @@ -93,8 +95,9 @@ it('Should extract the correct parameters', async () => { it('Should extract the correct response', async () => { // Arrange const generator = await getGenerator(); + const schemas = await getSchemas(); // Act - const spec = generator.getSpec(); + const spec = generator.getSpec(schemas); // Arrange expect( get( @@ -198,8 +201,9 @@ it('Should extract the correct response', async () => { it('Should extract correct errors', async () => { // Arrange const generator = await getGenerator(); + const schemas = await getSchemas(); // Act - const spec = generator.getSpec(); + const spec = generator.getSpec(schemas); // Arrange expect( get( @@ -240,8 +244,9 @@ it('Should extract correct errors', async () => { it('Should extract correct API description', async () => { // Arrange const generator = await getGenerator(); + const schemas = await getSchemas(); // Act - const spec = generator.getSpec(); + const spec = generator.getSpec(schemas); // Arrange expect(get(spec, 'paths./user/{id}.get.description')).toBe( 'Get user information' diff --git a/packages/build/test/spec-generator/schema.ts b/packages/build/test/document-generator/spec-generator/schema.ts similarity index 63% rename from packages/build/test/spec-generator/schema.ts rename to packages/build/test/document-generator/spec-generator/schema.ts index f11cc491..d9d82a40 100644 --- a/packages/build/test/spec-generator/schema.ts +++ b/packages/build/test/document-generator/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({}, name); + } + + 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.`); } @@ -125,7 +126,7 @@ export const getConfig = (): IBuildOptions => { // We don't care about the options of these components. template: {} as any, artifact: {} as any, - schemaParser: {} as any, - extensions: [] as any, + 'schema-parser': {} as any, + extensions: {} as any, }; }; diff --git a/packages/build/test/spec-generator/schemas/0-user.yaml b/packages/build/test/document-generator/spec-generator/schemas/0-user.yaml similarity index 100% rename from packages/build/test/spec-generator/schemas/0-user.yaml rename to packages/build/test/document-generator/spec-generator/schemas/0-user.yaml diff --git a/packages/build/test/spec-generator/schemas/1-user-order.yaml b/packages/build/test/document-generator/spec-generator/schemas/1-user-order.yaml similarity index 100% rename from packages/build/test/spec-generator/schemas/1-user-order.yaml rename to packages/build/test/document-generator/spec-generator/schemas/1-user-order.yaml diff --git a/packages/build/test/spec-generator/schemas/2-users.yaml b/packages/build/test/document-generator/spec-generator/schemas/2-users.yaml similarity index 100% rename from packages/build/test/spec-generator/schemas/2-users.yaml rename to packages/build/test/document-generator/spec-generator/schemas/2-users.yaml 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/schema-reader/fileSchemaReader.spec.ts b/packages/build/test/schema-parser/schema-reader/fileSchemaReader.spec.ts index 715266d1..c6955f3d 100644 --- a/packages/build/test/schema-parser/schema-reader/fileSchemaReader.spec.ts +++ b/packages/build/test/schema-parser/schema-reader/fileSchemaReader.spec.ts @@ -1,37 +1,17 @@ -import { FileSchemaReader, SchemaData } from '@vulcan-sql/build/schema-parser'; +import { SchemaData, SchemaReaderType } from '@vulcan-sql/build/models'; +import { FileSchemaReader } from '@vulcan-sql/build/schema-parser'; import * as path from 'path'; -import { Container } from 'inversify'; -import { TYPES } from '@vulcan-sql/build/containers'; -import { SchemaParserOptions } from '@vulcan-sql/build/options'; -import { - ISchemaParserOptions, - SchemaReaderType, -} from '@vulcan-sql/build/models'; - -let container: Container; - -beforeEach(() => { - container = new Container(); - container - .bind>(TYPES.SchemaParserInputOptions) - .toConstantValue({ - folderPath: path.resolve(__dirname, '../test-schema'), - reader: SchemaReaderType.LocalFile, - }); - container - .bind(TYPES.SchemaParserOptions) - .to(SchemaParserOptions) - .inSingletonScope(); - container.bind(TYPES.SchemaReader).to(FileSchemaReader).inSingletonScope(); -}); - -afterEach(() => { - container.unbindAll(); -}); it('File schema reader should provide correct files and contents', async () => { // Arrange - const schemaReader = container.get(TYPES.SchemaReader); + const schemaReader = new FileSchemaReader( + { + folderPath: path.resolve(__dirname, '../test-schema'), + reader: SchemaReaderType.LocalFile, + }, + {}, + '' + ); const schemas: SchemaData[] = []; // Act diff --git a/packages/build/test/schema-parser/schema-reader/fileSchemaReaderError.spec.ts b/packages/build/test/schema-parser/schema-reader/fileSchemaReaderError.spec.ts index ede95cbe..ec22d269 100644 --- a/packages/build/test/schema-parser/schema-reader/fileSchemaReaderError.spec.ts +++ b/packages/build/test/schema-parser/schema-reader/fileSchemaReaderError.spec.ts @@ -1,33 +1,6 @@ +import { SchemaReaderType } from '@vulcan-sql/build'; import { FileSchemaReader } from '@vulcan-sql/build/schema-parser'; import * as path from 'path'; -import { SchemaParserOptions } from '@vulcan-sql/build/options'; -import { TYPES } from '@vulcan-sql/build/containers'; -import { - ISchemaParserOptions, - SchemaReaderType, -} from '@vulcan-sql/build/models'; -import { Container } from 'inversify'; - -let container: Container; - -beforeEach(() => { - container = new Container(); - container - .bind>(TYPES.SchemaParserInputOptions) - .toConstantValue({ - folderPath: path.resolve(__dirname, '../test-schema'), - reader: SchemaReaderType.LocalFile, - }); - container - .bind(TYPES.SchemaParserOptions) - .to(SchemaParserOptions) - .inSingletonScope(); - container.bind(TYPES.SchemaReader).to(FileSchemaReader).inSingletonScope(); -}); - -afterEach(() => { - container.unbindAll(); -}); jest.mock('glob', () => { return ( @@ -41,7 +14,14 @@ jest.mock('glob', () => { it('File schema reader should throw error with file search errors', async () => { // Arrange - const schemaReader = container.get(TYPES.SchemaReader); + const schemaReader = new FileSchemaReader( + { + folderPath: path.resolve(__dirname, '../test-schema'), + reader: SchemaReaderType.LocalFile, + }, + {}, + '' + ); // Act, Assert const iter = schemaReader.readSchema(); await expect(iter.next()).rejects.toThrow('mock error'); diff --git a/packages/build/test/schema-parser/schemaParser.spec.ts b/packages/build/test/schema-parser/schemaParser.spec.ts index 1d9f5a0c..30add1c3 100644 --- a/packages/build/test/schema-parser/schemaParser.spec.ts +++ b/packages/build/test/schema-parser/schemaParser.spec.ts @@ -1,14 +1,12 @@ -import { TYPES } from '@vulcan-sql/build/containers'; +import { extensionModule, TYPES } from '@vulcan-sql/build/containers'; import { ISchemaParserOptions, + SchemaFormat, + SchemaReader, SchemaReaderType, } from '@vulcan-sql/build/models'; import { SchemaParserOptions } from '@vulcan-sql/build/options'; -import { - SchemaFormat, - SchemaParser, - SchemaReader, -} from '@vulcan-sql/build/schema-parser'; +import { SchemaParser } from '@vulcan-sql/build/schema-parser'; import { IValidatorLoader, TYPES as CORE_TYPES } from '@vulcan-sql/core'; import { Container } from 'inversify'; import * as sinon from 'ts-sinon'; @@ -17,18 +15,23 @@ let container: Container; let stubSchemaReader: sinon.StubbedInstance; let stubValidatorLoader: sinon.StubbedInstance; -beforeEach(() => { +beforeEach(async () => { container = new Container(); stubSchemaReader = sinon.stubInterface(); stubValidatorLoader = sinon.stubInterface(); - container - .bind(TYPES.Factory_SchemaReader) - .toConstantValue(() => stubSchemaReader); + await container.loadAsync( + extensionModule({ + schemaParser: { + folderPath: '', + }, + } as any) + ); + + container.bind(TYPES.SchemaReader).toConstantValue(stubSchemaReader); container .bind>(TYPES.SchemaParserInputOptions) .toConstantValue({ - folderPath: '', reader: SchemaReaderType.LocalFile, }); container @@ -62,11 +65,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/core/src/containers/container.ts b/packages/core/src/containers/container.ts index f9b14fa7..b488aeca 100644 --- a/packages/core/src/containers/container.ts +++ b/packages/core/src/containers/container.ts @@ -1,11 +1,14 @@ import { ICoreOptions } from '@vulcan-sql/core/models'; import { Container as InversifyContainer } from 'inversify'; +import { ProjectOptions } from '../options'; +import { extensionModule } from './modules'; import { artifactBuilderModule, executorModule, templateEngineModule, validatorLoaderModule, } from './modules'; +import { TYPES } from './types'; export class Container { private inversifyContainer = new InversifyContainer(); @@ -15,14 +18,25 @@ export class Container { } public async load(options: ICoreOptions) { - this.inversifyContainer.load(artifactBuilderModule(options.artifact)); - await this.inversifyContainer.loadAsync(executorModule()); + // Project options + this.inversifyContainer + .bind(TYPES.ProjectInputOptions) + .toConstantValue(options); + this.inversifyContainer.bind(TYPES.ProjectOptions).to(ProjectOptions); + await this.inversifyContainer.loadAsync( - templateEngineModule(options.template, options.extensions || []) + artifactBuilderModule(options.artifact) ); + await this.inversifyContainer.loadAsync(executorModule(options.executor)); await this.inversifyContainer.loadAsync( - validatorLoaderModule(options.extensions) + templateEngineModule(options.template) ); + await this.inversifyContainer.loadAsync(validatorLoaderModule()); + await this.inversifyContainer.loadAsync(extensionModule(options)); + } + + public async unload() { + await this.inversifyContainer.unbindAllAsync(); } public getInversifyContainer() { 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/modules/artifactBuilder.ts b/packages/core/src/containers/modules/artifactBuilder.ts index 96ba5aff..a4eb4649 100644 --- a/packages/core/src/containers/modules/artifactBuilder.ts +++ b/packages/core/src/containers/modules/artifactBuilder.ts @@ -1,22 +1,18 @@ -import { ContainerModule, interfaces } from 'inversify'; +import { AsyncContainerModule, interfaces } from 'inversify'; import { - PersistentStore, - LocalFilePersistentStore, - Serializer, - JSONSerializer, ArtifactBuilder, VulcanArtifactBuilder, } from '@vulcan-sql/core/artifact-builder'; import { TYPES } from '../types'; import { - SerializerType, - PersistentStoreType, IArtifactBuilderOptions, + PersistentStore, + Serializer, } from '@vulcan-sql/core/models'; import { ArtifactBuilderOptions } from '../../options'; export const artifactBuilderModule = (options: IArtifactBuilderOptions) => - new ContainerModule((bind) => { + new AsyncContainerModule(async (bind) => { // Options bind( TYPES.ArtifactBuilderInputOptions @@ -26,24 +22,36 @@ export const artifactBuilderModule = (options: IArtifactBuilderOptions) => .inSingletonScope(); // PersistentStore - bind(TYPES.PersistentStore) - .to(LocalFilePersistentStore) - .inSingletonScope() - .whenTargetNamed(PersistentStoreType.LocalFile); - bind>( TYPES.Factory_PersistentStore - ).toAutoNamedFactory(TYPES.PersistentStore); + ).toAutoNamedFactory(TYPES.Extension_PersistentStore); + bind(TYPES.PersistentStore) + .toDynamicValue((context) => { + const factory = context.container.get< + interfaces.AutoNamedFactory + >(TYPES.Factory_PersistentStore); + const options = context.container.get( + TYPES.ArtifactBuilderOptions + ); + return factory(options.provider); + }) + .inSingletonScope(); // Serializer - bind>(TYPES.Serializer) - .to(JSONSerializer) - .inSingletonScope() - .whenTargetNamed(SerializerType.JSON); - bind>>( TYPES.Factory_Serializer - ).toAutoNamedFactory>(TYPES.Serializer); + ).toAutoNamedFactory>(TYPES.Extension_Serializer); + bind>(TYPES.Serializer) + .toDynamicValue((context) => { + const factory = context.container.get< + interfaces.AutoNamedFactory> + >(TYPES.Factory_Serializer); + const options = context.container.get( + TYPES.ArtifactBuilderOptions + ); + return factory(options.serializer); + }) + .inSingletonScope(); // ArtifactBuilder bind(TYPES.ArtifactBuilder) diff --git a/packages/core/src/containers/modules/executor.ts b/packages/core/src/containers/modules/executor.ts index 74091f2b..ce6f5c12 100644 --- a/packages/core/src/containers/modules/executor.ts +++ b/packages/core/src/containers/modules/executor.ts @@ -1,48 +1,30 @@ -import { - IExecutor, - QueryExecutor, - SQLClauseOperation, -} from '@vulcan-sql/core/data-query'; -import { Pagination } from '../../models/pagination'; -import { DataResult, IDataSource } from '@vulcan-sql/core/data-source'; -import { AsyncContainerModule } from 'inversify'; +import { IExecutor, QueryExecutor } from '@vulcan-sql/core/data-query'; +import { AsyncContainerModule, interfaces } from 'inversify'; import { TYPES } from '../types'; -import { Stream } from 'stream'; +import { DataSource } from '../../models/extensions'; +import { IExecutorOptions } from '../../models'; +import { ExecutorOptions } from '../../options'; -/** - * TODO: Mock data source to make data query builder could create by IoC - * need to update after real data source implemented. - * */ - -class MockDataSource implements IDataSource { - public async execute({ - statement, - operations, - pagination, - }: { - statement: string; - operations: SQLClauseOperation; - pagination?: Pagination | undefined; - }) { - return { - getColumns: () => { - return []; - }, - getData: () => { - return new Stream.Readable(); - }, - } as DataResult; - } -} - -export const executorModule = () => +export const executorModule = (options: IExecutorOptions = {}) => new AsyncContainerModule(async (bind) => { - /** - * TODO: bind mock data source, need to update after real data source implemented. - */ - bind(TYPES.DataSource).toConstantValue(new MockDataSource()); - bind(TYPES.Executor).toDynamicValue((context) => { - const dataSource = context.container.get(TYPES.DataSource); - return new QueryExecutor(dataSource); + // Options + bind(TYPES.ExecutorInputOptions).toConstantValue(options); + bind(TYPES.ExecutorOptions).to(ExecutorOptions); + + // Data source + bind>( + TYPES.Factory_DataSource + ).toAutoNamedFactory(TYPES.Extension_DataSource); + bind(TYPES.DataSource).toDynamicValue((context) => { + const factory = context.container.get< + interfaces.AutoNamedFactory + >(TYPES.Factory_DataSource); + const options = context.container.get( + TYPES.ExecutorOptions + ); + return factory(options.type); }); + + // Executor + bind(TYPES.Executor).to(QueryExecutor); }); diff --git a/packages/core/src/containers/modules/extension.ts b/packages/core/src/containers/modules/extension.ts new file mode 100644 index 00000000..48a0f649 --- /dev/null +++ b/packages/core/src/containers/modules/extension.ts @@ -0,0 +1,42 @@ +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'; +import { + builtInCodeLoader, + builtInTemplateProvider, +} from '@vulcan-sql/core/template-engine'; +import { + builtInPersistentStore, + builtInSerializer, +} from '@vulcan-sql/core/artifact-builder'; +import { builtInDataSource } from '@vulcan-sql/core/data-source'; + +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); + // Template provider (single module) + loader.loadInternalExtensionModule(builtInTemplateProvider); + // Serializer (single module) + loader.loadInternalExtensionModule(builtInSerializer); + // Persistent store (single module) + loader.loadInternalExtensionModule(builtInPersistentStore); + // Code Loader (single module) + loader.loadInternalExtensionModule(builtInCodeLoader); + // Data source (single module) + loader.loadInternalExtensionModule(builtInDataSource); + + // 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..a714b604 100644 --- a/packages/core/src/containers/modules/templateEngine.ts +++ b/packages/core/src/containers/modules/templateEngine.ts @@ -1,11 +1,10 @@ -import { TYPES } from '@vulcan-sql/core/containers'; +import { TYPES } from '@vulcan-sql/core/types'; import { + CodeLoader, ITemplateEngineOptions, - TemplateProviderType, + TemplateProvider, } from '@vulcan-sql/core/models'; import { - FileTemplateProvider, - TemplateProvider, InMemoryCodeLoader, NunjucksCompiler, Compiler, @@ -14,14 +13,8 @@ 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( @@ -32,26 +25,35 @@ export const templateEngineModule = ( .inSingletonScope(); // TemplateProvider - bind(TYPES.TemplateProvider) - .to(FileTemplateProvider) - .inSingletonScope() - .whenTargetNamed(TemplateProviderType.LocalFile); - bind>( TYPES.Factory_TemplateProvider - ).toAutoNamedFactory(TYPES.TemplateProvider); + ).toAutoNamedFactory(TYPES.Extension_TemplateProvider); + + if (options.provider) { + // Template provider is an optional component, but we can't use templateEngine.compile() if provider wasn't bound. + bind(TYPES.TemplateProvider) + .toDynamicValue((context) => { + const factory = context.container.get< + interfaces.AutoNamedFactory + >(TYPES.Factory_TemplateProvider); + return factory(options.provider!); + }) + .inSingletonScope(); + } // Compiler environment bind(TYPES.CompilerEnvironment) .toDynamicValue((context) => { // We only need loader in runtime - const loader = context.container.get( + const codeLoader = context.container.get( TYPES.CompilerLoader ); - return new nunjucks.Environment(loader); + + return new nunjucks.Environment(codeLoader); }) .inSingletonScope() .whenTargetNamed('runtime'); + bind(TYPES.CompilerEnvironment) .toDynamicValue(() => { return new nunjucks.Environment(); @@ -60,8 +62,19 @@ export const templateEngineModule = ( .whenTargetNamed('compileTime'); // Loader - bind(TYPES.CompilerLoader) - .to(InMemoryCodeLoader) + bind>( + TYPES.Factory_CompilerLoader + ).toAutoNamedFactory(TYPES.Extension_CompilerLoader); + bind(TYPES.CompilerLoader) + .toDynamicValue((context) => { + const loaderFactory = context.container.get< + interfaces.AutoNamedFactory + >(TYPES.Factory_CompilerLoader); + const options = context.container.get( + TYPES.TemplateEngineOptions + ); + return loaderFactory(options.codeLoader); + }) .inSingletonScope(); // Compiler @@ -71,7 +84,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..76148881 100644 --- a/packages/core/src/containers/types.ts +++ b/packages/core/src/containers/types.ts @@ -1,4 +1,7 @@ export const TYPES = { + // Root options + ProjectOptions: Symbol.for('ProjectOptions'), + ProjectInputOptions: Symbol.for('ProjectInputOptions'), // Artifact builder PersistentStore: Symbol.for('PersistentStore'), Factory_PersistentStore: Symbol.for('Factory_PersistentStore'), @@ -12,6 +15,7 @@ export const TYPES = { Factory_TemplateProvider: Symbol.for('Factory_TemplateProvider'), CompilerExtension: Symbol.for('CompilerExtension'), CompilerLoader: Symbol.for('CompilerLoader'), + Factory_CompilerLoader: Symbol.for('Factory_CompilerLoader'), CompilerEnvironment: Symbol.for('CompilerEnvironment'), Compiler: Symbol.for('Compiler'), TemplateEngine: Symbol.for('TemplateEngine'), @@ -19,12 +23,21 @@ export const TYPES = { TemplateEngineInputOptions: Symbol.for('TemplateEngineInputOptions'), // Executor Executor: Symbol.for('Executor'), - // Data Query Builder + ExecutorOptions: Symbol.for('ExecutorOptions'), + ExecutorInputOptions: Symbol.for('ExecutorInputOptions'), DataQueryBuilder: Symbol.for('DataQueryBuilder'), - // Data Source DataSource: Symbol.for('DataSource'), + Factory_DataSource: Symbol.for('Factory_DataSource'), // Validator ValidatorLoader: Symbol.for('ValidatorLoader'), - // source of extensions - SourceOfExtensions: Symbol.for('SourceOfExtensions'), + // Extensions + ExtensionConfig: Symbol.for('ExtensionConfig'), + ExtensionName: Symbol.for('ExtensionName'), + Extension_TemplateEngine: Symbol.for('Extension_TemplateEngine'), + Extension_InputValidator: Symbol.for('Extension_InputValidator'), + Extension_TemplateProvider: Symbol.for('Extension_TemplateProvider'), + Extension_Serializer: Symbol.for('Extension_Serializer'), + Extension_PersistentStore: Symbol.for('Extension_PersistentStore'), + Extension_CompilerLoader: Symbol.for('Extension_CompilerLoader'), + Extension_DataSource: Symbol.for('Extension_DataSource'), }; 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/index.ts b/packages/core/src/lib/artifact-builder/persistent-stores/index.ts index 4822db96..d1129c19 100644 --- a/packages/core/src/lib/artifact-builder/persistent-stores/index.ts +++ b/packages/core/src/lib/artifact-builder/persistent-stores/index.ts @@ -1,2 +1,4 @@ -export * from './persistentStore'; +import { LocalFilePersistentStore } from './localFilePersistentStore'; +export const builtInPersistentStore = [LocalFilePersistentStore]; + export * from './localFilePersistentStore'; 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..3975dd37 100644 --- a/packages/core/src/lib/artifact-builder/persistent-stores/localFilePersistentStore.ts +++ b/packages/core/src/lib/artifact-builder/persistent-stores/localFilePersistentStore.ts @@ -1,16 +1,26 @@ -import { PersistentStore } from './persistentStore'; import { promises as fs } from 'fs'; -import { injectable, inject } from 'inversify'; -import { TYPES } from '@vulcan-sql/core/containers'; -import { IArtifactBuilderOptions } from '@vulcan-sql/core/models'; +import { + ArtifactBuilderProviderType, + IArtifactBuilderOptions, + PersistentStore, + VulcanExtensionId, + VulcanInternalExtension, +} from '@vulcan-sql/core/models'; +import { inject } from 'inversify'; +import { TYPES } from '@vulcan-sql/core/types'; +import { ArtifactBuilderOptions } from '@vulcan-sql/core/options'; -@injectable() -export class LocalFilePersistentStore implements PersistentStore { +@VulcanInternalExtension() +@VulcanExtensionId(ArtifactBuilderProviderType.LocalFile) +export class LocalFilePersistentStore extends PersistentStore { private filePath: string; constructor( - @inject(TYPES.ArtifactBuilderOptions) options: IArtifactBuilderOptions + @inject(TYPES.ArtifactBuilderOptions) options: ArtifactBuilderOptions, + @inject(TYPES.ExtensionConfig) config: any, + @inject(TYPES.ExtensionName) moduleName: string ) { + super(config, moduleName); this.filePath = options.filePath; } diff --git a/packages/core/src/lib/artifact-builder/persistent-stores/persistentStore.ts b/packages/core/src/lib/artifact-builder/persistent-stores/persistentStore.ts deleted file mode 100644 index d2e152a6..00000000 --- a/packages/core/src/lib/artifact-builder/persistent-stores/persistentStore.ts +++ /dev/null @@ -1,4 +0,0 @@ -export interface PersistentStore { - save(data: Buffer): Promise; - load(): Promise; -} diff --git a/packages/core/src/lib/artifact-builder/serializers/index.ts b/packages/core/src/lib/artifact-builder/serializers/index.ts index 6b2619e7..51765de0 100644 --- a/packages/core/src/lib/artifact-builder/serializers/index.ts +++ b/packages/core/src/lib/artifact-builder/serializers/index.ts @@ -1,2 +1,4 @@ +import { JSONSerializer } from './jsonSerializer'; +export const builtInSerializer = [JSONSerializer]; + export * from './jsonSerializer'; -export * from './serializer'; diff --git a/packages/core/src/lib/artifact-builder/serializers/jsonSerializer.ts b/packages/core/src/lib/artifact-builder/serializers/jsonSerializer.ts index 17b86116..197efe0c 100644 --- a/packages/core/src/lib/artifact-builder/serializers/jsonSerializer.ts +++ b/packages/core/src/lib/artifact-builder/serializers/jsonSerializer.ts @@ -1,8 +1,13 @@ -import { Serializer } from './serializer'; -import { injectable } from 'inversify'; +import { + ArtifactBuilderSerializerType, + Serializer, + VulcanExtensionId, + VulcanInternalExtension, +} from '@vulcan-sql/core/models'; -@injectable() -export class JSONSerializer implements Serializer { +@VulcanInternalExtension() +@VulcanExtensionId(ArtifactBuilderSerializerType.JSON) +export class JSONSerializer extends Serializer { public serialize(data: T): Buffer { return Buffer.from(JSON.stringify(data), 'utf-8'); } diff --git a/packages/core/src/lib/artifact-builder/serializers/serializer.ts b/packages/core/src/lib/artifact-builder/serializers/serializer.ts deleted file mode 100644 index 789fdf8b..00000000 --- a/packages/core/src/lib/artifact-builder/serializers/serializer.ts +++ /dev/null @@ -1,4 +0,0 @@ -export interface Serializer { - serialize(data: T): Buffer; - deserialize(raw: Buffer): T; -} diff --git a/packages/core/src/lib/artifact-builder/vulcanArtifactBuilder.ts b/packages/core/src/lib/artifact-builder/vulcanArtifactBuilder.ts index e4ac9198..37630b5f 100644 --- a/packages/core/src/lib/artifact-builder/vulcanArtifactBuilder.ts +++ b/packages/core/src/lib/artifact-builder/vulcanArtifactBuilder.ts @@ -1,9 +1,8 @@ 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 { IArtifactBuilderOptions } from '../../models/artifactBuilderOptions'; +import { PersistentStore } from '@vulcan-sql/core/models'; +import { Serializer } from '@vulcan-sql/core/models'; +import { inject, injectable } from 'inversify'; +import { TYPES } from '@vulcan-sql/core/types'; @injectable() export class VulcanArtifactBuilder implements ArtifactBuilder { @@ -11,14 +10,13 @@ export class VulcanArtifactBuilder implements ArtifactBuilder { private persistentStore: PersistentStore; constructor( - @inject(TYPES.Factory_PersistentStore) - persistentStoreFactory: interfaces.AutoNamedFactory, - @inject(TYPES.Factory_Serializer) - serializerFactory: interfaces.AutoNamedFactory>, - @inject(TYPES.ArtifactBuilderOptions) options: IArtifactBuilderOptions + @inject(TYPES.PersistentStore) + persistentStore: PersistentStore, + @inject(TYPES.Serializer) + serializer: Serializer ) { - this.serializer = serializerFactory(options.serializer); - this.persistentStore = persistentStoreFactory(options.provider); + this.serializer = serializer; + this.persistentStore = persistentStore; } public async build(artifact: Artifact): Promise { diff --git a/packages/core/src/lib/data-query/builder/dataQueryBuilder.ts b/packages/core/src/lib/data-query/builder/dataQueryBuilder.ts index 49b7124f..f6bbbf61 100644 --- a/packages/core/src/lib/data-query/builder/dataQueryBuilder.ts +++ b/packages/core/src/lib/data-query/builder/dataQueryBuilder.ts @@ -1,5 +1,4 @@ -import { IDataSource } from '@vulcan-sql/core/data-source'; -import { Pagination } from '@vulcan-sql/core/models'; +import { DataSource, Pagination } from '@vulcan-sql/core/models'; import { find, isEmpty } from 'lodash'; import { @@ -180,7 +179,7 @@ export interface SQLClauseOperation { export interface IDataQueryBuilder { readonly statement: string; readonly operations: SQLClauseOperation; - readonly dataSource: IDataSource; + readonly dataSource: DataSource; // Select clause methods select(...columns: Array): IDataQueryBuilder; @@ -405,7 +404,7 @@ export class DataQueryBuilder implements IDataQueryBuilder { public readonly statement: string; // record all operations for different SQL clauses public readonly operations: SQLClauseOperation; - public readonly dataSource: IDataSource; + public readonly dataSource: DataSource; public pagination?: Pagination; constructor({ statement, @@ -414,7 +413,7 @@ export class DataQueryBuilder implements IDataQueryBuilder { }: { statement: string; operations?: SQLClauseOperation; - dataSource: IDataSource; + dataSource: DataSource; }) { this.statement = statement; this.dataSource = dataSource; diff --git a/packages/core/src/lib/data-query/executor.ts b/packages/core/src/lib/data-query/executor.ts index 72b50fa0..9a0020fc 100644 --- a/packages/core/src/lib/data-query/executor.ts +++ b/packages/core/src/lib/data-query/executor.ts @@ -1,15 +1,16 @@ -import { TYPES } from '@vulcan-sql/core'; +import { DataSource } from '@vulcan-sql/core'; import { inject, injectable } from 'inversify'; +import { TYPES } from '@vulcan-sql/core/types'; import { DataQueryBuilder, IDataQueryBuilder } from './builder'; -import { IDataSource } from '../data-source'; export interface IExecutor { createBuilder(query: string): Promise; } +@injectable() export class QueryExecutor implements IExecutor { - private dataSource: IDataSource; - constructor(dataSource: IDataSource) { + private dataSource: DataSource; + constructor(@inject(TYPES.DataSource) dataSource: DataSource) { this.dataSource = dataSource; } /** diff --git a/packages/core/src/lib/data-source/dataSource.ts b/packages/core/src/lib/data-source/dataSource.ts deleted file mode 100644 index f2c0e43a..00000000 --- a/packages/core/src/lib/data-source/dataSource.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { SQLClauseOperation } from '@vulcan-sql/core/data-query'; -import { Pagination } from '@vulcan-sql/core/models'; -import { Stream } from 'stream'; - -export type DataColumn = { name: string; type: string }; - -export type DataResult = { - getColumns: () => DataColumn[]; - getData: () => Stream; -}; -export interface IDataSource { - execute({ - statement, - operations, - pagination, - }: { - statement: string; - operations: SQLClauseOperation; - pagination?: Pagination; - }): Promise; -} diff --git a/packages/core/src/lib/data-source/index.ts b/packages/core/src/lib/data-source/index.ts index f80a6659..9e57f399 100644 --- a/packages/core/src/lib/data-source/index.ts +++ b/packages/core/src/lib/data-source/index.ts @@ -1 +1,4 @@ -export * from './dataSource'; +import { PGDataSource } from './pg'; +export const builtInDataSource = [PGDataSource]; + +export * from './pg'; diff --git a/packages/core/src/lib/data-source/pg.ts b/packages/core/src/lib/data-source/pg.ts new file mode 100644 index 00000000..d6f10f58 --- /dev/null +++ b/packages/core/src/lib/data-source/pg.ts @@ -0,0 +1,23 @@ +import { Stream } from 'stream'; +import { + DataResult, + DataSource, + ExecuteOptions, + VulcanExtensionId, + VulcanInternalExtension, +} from '../../models/extensions'; + +@VulcanInternalExtension() +@VulcanExtensionId('pg') +export class PGDataSource extends DataSource { + public async execute(options: ExecuteOptions): Promise { + return { + getColumns: () => { + return []; + }, + getData: () => { + return new Stream(); + }, + }; + } +} 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..9f7ed72b --- /dev/null +++ b/packages/core/src/lib/extension-loader/extensionLoader.ts @@ -0,0 +1,144 @@ +import { ExtensionBase, ICoreOptions } from '@vulcan-sql/core/models'; +import { interfaces } from 'inversify'; +import { ClassType, defaultImport } from '../utils'; +import { + EXTENSION_ENFORCED_ID_METADATA_KEY, + EXTENSION_IDENTIFIER_METADATA_KEY, + 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; + } + + /** Load external extensions (should be called by core package) */ + 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 }) => { + const extensionBinding = bind(type).to(extension); + const { extensionId } = this.getExtensionMetadata(extension); + if (extensionId) + extensionBinding.when((request) => { + // If request contains named tag, i.e. @named or getNamed(), we check the extensionId, otherwise we fulfill the request. + // It makes both @named tag and @multiInject work at same time. + const namedTag = request.target.getNamedTag(); + if (namedTag) return namedTag.value === extensionId; + return true; + }); + 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] || {} : {}) + .whenInjectedInto(extension); + bind(TYPES.ExtensionName) + .toConstantValue(name || '') + .whenInjectedInto(extension); + }); + } + this.bound = true; + } + + private getExtensionMetadata(extension: ClassType) { + const extensionId = Reflect.getMetadata( + EXTENSION_IDENTIFIER_METADATA_KEY, + extension + ); + const enforcedId = Reflect.getMetadata( + EXTENSION_ENFORCED_ID_METADATA_KEY, + extension + ); + if (enforcedId && !extensionId) + throw new Error( + `Extension ${extension.name} needed an extension id but was not found, please use the decorator @VulcanExtensionId to set the id.` + ); + + return { + extensionId, + }; + } + + 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..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 @@ -1,15 +1,24 @@ -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.ExtensionName) name: string, + @inject(TYPES.Executor) executor: IExecutor + ) { + super(config, name); 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..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 @@ -1,31 +1,32 @@ -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.ExtensionName) + name: string, @inject(TYPES.CompilerEnvironment) @named('compileTime') compileTimeEnv: nunjucks.Environment ) { - super(); + super(config, name); 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 +40,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/code-loader/codeLoader.ts b/packages/core/src/lib/template-engine/code-loader/codeLoader.ts deleted file mode 100644 index 70a0273b..00000000 --- a/packages/core/src/lib/template-engine/code-loader/codeLoader.ts +++ /dev/null @@ -1,6 +0,0 @@ -import * as nunjucks from 'nunjucks'; - -export interface ICodeLoader { - setSource(name: string, code: string): void; - getSource(name: string): nunjucks.LoaderSource | null; -} diff --git a/packages/core/src/lib/template-engine/code-loader/inMemoryCodeLoader.ts b/packages/core/src/lib/template-engine/code-loader/inMemoryCodeLoader.ts index a0c7fdf0..917c556c 100644 --- a/packages/core/src/lib/template-engine/code-loader/inMemoryCodeLoader.ts +++ b/packages/core/src/lib/template-engine/code-loader/inMemoryCodeLoader.ts @@ -1,9 +1,13 @@ import * as nunjucks from 'nunjucks'; -import { injectable } from 'inversify'; -import { ICodeLoader } from './codeLoader'; +import { + CodeLoader, + VulcanExtensionId, + VulcanInternalExtension, +} from '@vulcan-sql/core/models'; -@injectable() -export class InMemoryCodeLoader implements ICodeLoader { +@VulcanInternalExtension() +@VulcanExtensionId('inMemory') +export class InMemoryCodeLoader extends CodeLoader { private source = new Map(); public setSource(name: string, code: string) { diff --git a/packages/core/src/lib/template-engine/code-loader/index.ts b/packages/core/src/lib/template-engine/code-loader/index.ts index 621ece6a..3f98746d 100644 --- a/packages/core/src/lib/template-engine/code-loader/index.ts +++ b/packages/core/src/lib/template-engine/code-loader/index.ts @@ -1,2 +1,4 @@ -export * from './codeLoader'; +import { InMemoryCodeLoader } from './inMemoryCodeLoader'; +export const builtInCodeLoader = [InMemoryCodeLoader]; + export * from './inMemoryCodeLoader'; 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..d86f0eea 100644 --- a/packages/core/src/lib/template-engine/template-providers/fileTemplateProvider.ts +++ b/packages/core/src/lib/template-engine/template-providers/fileTemplateProvider.ts @@ -1,28 +1,44 @@ -import { Template, TemplateProvider } from './templateProvider'; +import { + Template, + TemplateProvider, +} from '../../../models/extensions/templateProvider'; 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 { ITemplateEngineOptions } from '@vulcan-sql/core/models'; +import { + ITemplateEngineOptions, + TemplateProviderType, + VulcanExtensionId, + VulcanInternalExtension, +} from '@vulcan-sql/core/models'; +import { inject } from 'inversify'; +import { TYPES } from '@vulcan-sql/core/types'; +import { TemplateEngineOptions } from '@vulcan-sql/core/options'; -@injectable() -export class FileTemplateProvider implements TemplateProvider { +@VulcanInternalExtension() +@VulcanExtensionId(TemplateProviderType.LocalFile) +export class FileTemplateProvider extends TemplateProvider { private options: ITemplateEngineOptions; constructor( - @inject(TYPES.TemplateEngineOptions) options: ITemplateEngineOptions + @inject(TYPES.TemplateEngineOptions) options: TemplateEngineOptions, + @inject(TYPES.ExtensionConfig) config: any, + @inject(TYPES.ExtensionName) moduleName: string ) { + super(config, moduleName); this.options = options; } public async *getTemplates(): AsyncGenerator