UNPKG

@deepkit/app

Version:

Deepkit App, CLI framework and service container

513 lines (450 loc) 15.4 kB
/* * Deepkit Framework * Copyright (C) 2021 Deepkit UG, Marc J. Schmidt * * This program is free software: you can redistribute it and/or modify * it under the terms of the MIT License. * * You should have received a copy of the MIT License along with this program. */ import { InjectorModule, InjectorModuleConfig, NormalizedProvider, ProviderWithScope, Token } from '@deepkit/injector'; import { AbstractClassType, ClassType, CustomError, ExtractClassType, isClass } from '@deepkit/core'; import { EventListener, EventToken } from '@deepkit/event'; import { WorkflowDefinition } from '@deepkit/workflow'; import { getPartialSerializeFunction, ReflectionFunction, ReflectionKind, ReflectionMethod, resolveReceiveType, serializer, Type, TypeClass, } from '@deepkit/type'; import { ControllerConfig } from './service-container.js'; export interface MiddlewareConfig { getClassTypes(): ClassType[]; } export type MiddlewareFactory = () => MiddlewareConfig; export type ExportType = AbstractClassType | string | AppModule<any> | Type | NormalizedProvider; /** * @reflection never */ export interface AddedListener { eventToken: EventToken; reflection: ReflectionMethod | ReflectionFunction; module?: InjectorModule; classType?: ClassType; methodName?: string; order: number; } export function stringifyListener(listener: AddedListener): string { if (listener.classType) { return listener.classType.name + '.' + listener.methodName; } return listener.reflection.name || 'anonymous function'; } export interface ModuleDefinition { /** * The name of the module. This is used in the configuration system. * It allows you to have multiple instances of the same module with different configurations * loaded from a configuration loader (e.g. env variables). * * The lowercase alphanumeric module name. * Choose a short unique name for best usability. If you don't have any configuration * or if you want that your configuration options are available without prefix, you can keep this undefined. */ name?: string; /** * Providers. */ providers?: (ProviderWithScope | ProviderWithScope[])[]; /** * Export providers (its token `provide` value) or modules you imported first. */ exports?: ExportType[]; /** * Module bootstrap class|function. * This class is instantiated or function executed on bootstrap and can set up various injected services. */ bootstrap?: ClassType | Function; /** * Configuration definition. * * @example * ```typescript * * class MyModuleConfig { * debug: boolean = false; * }); * * class MyModule extends createModuleClass({ * config: MyModuleConfig * }); * ``` */ config?: ClassType; /** * CLI controllers. */ controllers?: ClassType[]; /** * Register created workflows. This allows the Framework Debugger to collect * debug information and display the graph of your workflow. */ workflows?: WorkflowDefinition<any>[]; /** * Event listeners. * * @example with simple functions * ```typescript * { * listeners: [ * onEvent.listen((event: MyEvent) => {console.log('event triggered', event);}), * ] * } * ``` * * @example with services * ```typescript * * class MyListener { * @eventDispatcher.listen(onEvent) * onEvent(event: typeof onEvent['type']) { * console.log('event triggered', event); * } * } * * { * listeners: [ * MyListener, * ] * } * ``` */ listeners?: (EventListener | ClassType)[]; /** * HTTP middlewares. */ middlewares?: MiddlewareFactory[]; } export interface CreateModuleDefinition extends ModuleDefinition { /** * Whether all services should be moved to the root module/application. */ forRoot?: true; /** * Modules can not import other modules in the module definitions. * Use instead: * * ```typescript * class MyModule extends createModuleClass({}) { * imports = [new AnotherModule]; * } * ``` * * or * * ```typescript * class MyModule extends createModuleClass({}) { * process() { * this.addModuleImport(new AnotherModule); * } * } * ``` * * or switch to functional modules * * ```typescript * function myModule(module: AppModule) { * module.addModuleImport(new AnotherModule); * } * ``` */ imports?: undefined; } export type FunctionalModule = (module: AppModule<any>) => void; export type FunctionalModuleFactory = (...args: any[]) => (module: AppModule<any>) => void; export interface RootModuleDefinition extends ModuleDefinition { /** * Import another module. */ imports?: (AppModule<any> | FunctionalModule)[]; } export class ConfigurationInvalidError extends CustomError { } let moduleId = 0; /** * @reflection never */ export type DeepPartial<T> = T extends string | number | bigint | boolean | null | undefined | symbol | Date | Set<any> | Map<any, any> | Uint8Array | ArrayBuffer | ArrayBufferView | Error | RegExp | Function | Promise<any> ? T : Partial<{ [P in keyof T]: DeepPartial<T[P]>; }>; export interface AppModuleClass<C extends InjectorModuleConfig> { new(config?: DeepPartial<C>): AppModule<C>; } /** * Creates a new module class type from which you can extend. * * ```typescript * class MyModule extends createModuleClass({}) {} * * //and used like this * new App({ * imports: [new MyModule] * }); * ``` */ export function createModuleClass<C extends InjectorModuleConfig>(options: CreateModuleDefinition & { config?: ClassType<C> }): AppModuleClass<C> { /** @reflection never */ return class AnonAppModule extends AppModule<any> { constructor(config?: any) { super(config, options); } } as any; } /** * Creates a new module instance. * * This is mainly used for small non-reusable modules. * It's recommended to use `createModuleClass` and extend from it. * * @example * ```typescript * const myModule = createModuleClass({ * config: MyConfig * providers: [MyService] * }); * * const app = new App({ * imports: [myModule] * }); * ``` */ export function createModule<T extends CreateModuleDefinition>(options: T): AppModule<ExtractClassType<T['config']>> { return new (createModuleClass(options))(); } export type ListenerType = EventListener | ClassType; function extractConfigFromModuleClass(moduleClass: ClassType): ClassType | undefined { if (moduleClass === AppModule) { // This is not supported right now `new AppModule<MyConfig>()` const type = resolveReceiveType(moduleClass); if (type.kind !== ReflectionKind.class) return; if (AppModule.isPrototypeOf(type.classType) || type.classType === AppModule) { // This is the AppModule class itself, not a module extending it. return; } return type.classType; } let current: ClassType | undefined = moduleClass; // Find classType that extends AppModule while (current) { const parent = Object.getPrototypeOf(current) as ClassType | undefined; if (parent === AppModule) { const type = resolveReceiveType(current); if (type.kind !== ReflectionKind.class) return; const extendArgument = type.extendsArguments?.[0]; if (!extendArgument) return; if (extendArgument.kind !== ReflectionKind.class) return; if (AppModule.isPrototypeOf(extendArgument.classType) || extendArgument.classType === AppModule) { // This is the AppModule class itself, not a module extending it. return; } return extendArgument.classType; } current = parent; } return; } /** * The AppModule is the base class for all modules. * * You can use `createModule` to create a new module class or extend from `AppModule` manually. * * @example * ```typescript * * class MyModule extends AppModule { * providers = [MyService]; * exports = [MyService]; * * constructor(config: MyConfig) { * super(); * this.setConfigDefinition(MyConfig); * this.configure(config); * this.name = 'myModule'; * } * } */ export class AppModule<C extends InjectorModuleConfig = any> extends InjectorModule<C> { public setupConfigs: ((module: AppModule<any>, config: any) => void)[] = []; public imports: AppModule<any>[] = []; public controllers: ClassType[] = []; public commands: { name?: string, callback: Function }[] = []; public workflows: WorkflowDefinition<any>[] = []; public listeners: ListenerType[] = []; public middlewares: MiddlewareFactory[] = []; public uses: ((...args: any[]) => void)[] = []; public name: string = ''; constructor( config: DeepPartial<C> = {} as DeepPartial<C>, public options: RootModuleDefinition = {}, public setups: ((module: AppModule<any>, config: any) => void)[] = [], public id: number = moduleId++, ) { super(); if (options.name) this.name = options.name; if (this.options.imports) for (const m of this.options.imports) this.addModuleImport(m); if (this.options.providers) this.providers.push(...this.options.providers.flat()); if (this.options.exports) this.exports.push(...this.options.exports); if (this.options.controllers) this.controllers.push(...this.options.controllers); if (this.options.workflows) this.workflows.push(...this.options.workflows); if (this.options.listeners) this.listeners.push(...this.options.listeners); if (this.options.middlewares) this.middlewares.push(...this.options.middlewares); if ('forRoot' in this.options) this.forRoot(); if (this.options.config) { this.setConfigDefinition(this.options.config); } else { const configFromClass = extractConfigFromModuleClass(this.constructor as ClassType); if (configFromClass) { this.setConfigDefinition(configFromClass); } } this.configure(config as Partial<C>); } protected addModuleImport(m: AppModule<any> | FunctionalModule) { if (m instanceof AppModule) { this.addImport(m); } else { const module = new AppModule({}); m(module); this.addImport(module); } } /** * When all configuration loaders have been loaded, this method is called. * It allows to further manipulate the module state depending on the final config. * Possible use-cases: * - Add more providers depending on the configuration. * - Change the module imports depending on the configuration. * - Change provider setup via this.configureProvider<Provider>(provider => {}) depending on the configuration. */ process() { } /** * A hook that allows to react on a registered provider in some module. */ processProvider(module: AppModule<any>, token: Token, provider: ProviderWithScope) { } /** * A hook that allows to react on a registered controller in some module. */ processController(module: AppModule<any>, config: ControllerConfig) { } /** * A hook that allows to react on a registered event listeners in some module. */ processListener(module: AppModule<any>, listener: AddedListener) { } /** * After `process` and when all modules have been processed by the service container. * This is also after `processController` and `processProvider` have been called and the full * final module tree is known. Adding now new providers or modules doesn't have any effect. * * Last chance to set up the injector context, via this.setupProvider(). */ postProcess() { } /** * Renames this module instance. */ rename(name: string): this { this.name = name; return this; } getListeners(): ListenerType[] { return this.listeners; } getWorkflows(): WorkflowDefinition<any>[] { return this.workflows; } getMiddlewares(): MiddlewareFactory[] { return this.middlewares; } getControllers(): ClassType[] { return this.controllers; } getCommands(): { name?: string, callback: Function }[] { return this.commands; } addCommand(name: string | undefined, callback: (...args: []) => any): this { this.assertInjectorNotBuilt(); this.commands.push({ name, callback }); return this; } addController(...controller: ClassType[]): this { this.assertInjectorNotBuilt(); this.controllers.push(...controller); return this; } addListener(...listener: (EventListener | ClassType)[]): this { this.assertInjectorNotBuilt(); for (const l of listener) { if (!isClass(l)) continue; if (this.isProvided(l)) continue; this.addProvider(l); } this.listeners.push(...listener); return this; } addMiddleware(...middlewares: MiddlewareFactory[]): this { this.middlewares.push(...middlewares); return this; } /** * Allows to change the module config before `setup` and bootstrap is called. * This is the last step right before the config is validated. */ setupConfig(callback: (module: AppModule<C>, config: C) => void): this { this.setupConfigs.push(callback as any); return this; } /** * Allows to change the module after the configuration has been loaded, right before the service container is built. * * This enables you to change the module or its imports depending on the configuration the last time before their services are built. * * At this point no services can be requested as the service container was not built. */ setup(callback: (module: AppModule<C>, config: C) => void): this { this.setups.push(callback); return this; } /** * Allows to call services before the application bootstraps. * * This enables you to configure modules and request their services. */ use(callback: (...args: any[]) => void): this { this.uses.push(callback); return this; } getImports(): AppModule<any>[] { return super.getImports() as AppModule<any>[]; } getName(): string { return this.name; } /** * Sets configured values. */ configure(config: Partial<C>): this { if (this.configDefinition) { const configNormalized = getPartialSerializeFunction(resolveReceiveType(this.configDefinition) as TypeClass, serializer.deserializeRegistry)(config); Object.assign(this.config, configNormalized); } return this; } }