// Copyright IBM Corp. and LoopBack contributors 2018,2020. All Rights Reserved. // Node module: @loopback/boot // This file is licensed under the MIT License. // License text available at https://opensource.org/licenses/MIT import { Application, Binding, BindingFilter, BindingFromClassOptions, BindingScope, Component, Constructor, Context, CoreBindings, createBindingFromClass, MixinTarget, } from '@loopback/core'; import {BootComponent} from '../boot.component'; import {createComponentApplicationBooterBinding} from '../booters/component-application.booter'; import {Bootstrapper} from '../bootstrapper'; import {BootBindings, BootTags} from '../keys'; import {Bootable, Booter, BootOptions, InstanceWithBooters} from '../types'; /* eslint-enable @typescript-eslint/no-unused-vars */ // Binding is re-exported as Binding / Booter types are needed when consuming // BootMixin and this allows a user to import them from the same package (UX!) export {Binding}; /** * Mixin for @loopback/boot. This Mixin provides the following: * - Implements the Bootable Interface as follows. * - Add a `projectRoot` property to the Class * - Adds an optional `bootOptions` property to the Class that can be used to * store the Booter conventions. * - Adds the `BootComponent` to the Class (which binds the Bootstrapper and default Booters) * - Provides the `boot()` convenience method to call Bootstrapper.boot() * - Provides the `booter()` convenience method to bind a Booter(s) to the Application * - Override `component()` to call `mountComponentBooters` * - Adds `mountComponentBooters` which binds Booters to the application from `component.booters[]` * * @param superClass - Application class * @returns A new class that extends the super class with boot related methods * * @typeParam T - Type of the application class as the target for the mixin */ export function BootMixin>(superClass: T) { return class extends superClass implements Bootable { projectRoot: string; bootOptions?: BootOptions; // eslint-disable-next-line @typescript-eslint/no-explicit-any constructor(...args: any[]) { super(...args); this.component(BootComponent); // We Dynamically bind the Project Root and Boot Options so these values can // be used to resolve an instance of the Bootstrapper (as they are dependencies) this.bind(BootBindings.PROJECT_ROOT).toDynamicValue( () => this.projectRoot, ); this.bind(BootBindings.BOOT_OPTIONS).toDynamicValue( () => this.bootOptions ?? {}, ); } booted: boolean; /** * Override to detect and warn about starting without booting. */ // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore public async start(): Promise { await super.start(); if (!this.booted) { process.emitWarning( 'App started without booting. Did you forget to call ' + '`await app.boot()`?', 'LoopBackWarning', ); } } /** * Convenience method to call bootstrapper.boot() by resolving bootstrapper */ async boot(): Promise { /* eslint-disable @typescript-eslint/ban-ts-comment */ // A workaround to access protected Application methods const self = this as unknown as Application; if (this.state === 'booting') { // @ts-ignore return self.awaitState('booted'); } // @ts-ignore self.assertNotInProcess('boot'); // @ts-ignore self.assertInStates('boot', 'created', 'booted'); if (this.state === 'booted') return; // @ts-ignore self.setState('booting'); // Get a instance of the BootStrapper const bootstrapper: Bootstrapper = await this.get( BootBindings.BOOTSTRAPPER_KEY, ); await bootstrapper.boot(); // @ts-ignore this.setState('booted'); this.booted = true; /* eslint-enable @typescript-eslint/ban-ts-comment */ } /** * Given a N number of Booter Classes, this method binds them using the * prefix and tag expected by the Bootstrapper. * * @param booterCls - Booter classes to bind to the Application * * @example * ```ts * app.booters(MyBooter, MyOtherBooter) * ``` */ booters(...booterCls: Constructor[]): Binding[] { return booterCls.map(cls => bindBooter(this as unknown as Context, cls)); } /** * Register a booter to boot a sub-application. See * {@link createComponentApplicationBooterBinding} for more details. * * @param subApp - A sub-application with artifacts to be booted * @param filter - A binding filter to select what bindings from the sub * application should be added to the main application. */ applicationBooter(subApp: Application & Bootable, filter?: BindingFilter) { const binding = createComponentApplicationBooterBinding(subApp, filter); this.add(binding); return binding; } /** * Override to ensure any Booter's on a Component are also mounted. * * @param component - The component to add. * * @example * ```ts * * export class ProductComponent { * booters = [ControllerBooter, RepositoryBooter]; * providers = { * [AUTHENTICATION_STRATEGY]: AuthStrategy, * [AUTHORIZATION_ROLE]: Role, * }; * }; * * app.component(ProductComponent); * ``` */ // Unfortunately, TypeScript does not allow overriding methods inherited // from mapped types. https://github.com/microsoft/TypeScript/issues/38496 // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore public component( componentCtor: Constructor, nameOrOptions?: string | BindingFromClassOptions, ) { const binding = super.component(componentCtor, nameOrOptions); const instance = this.getSync(binding.key); this.mountComponentBooters(instance); return binding; } /** * Get an instance of a component and mount all it's * booters. This function is intended to be used internally * by component() * * @param component - The component to mount booters of */ mountComponentBooters( componentInstanceOrClass: Constructor | InstanceWithBooters, ) { const componentInstance = resolveComponentInstance(this); if (componentInstance.booters) { this.booters(...componentInstance.booters); } /** * Determines if componentInstanceOrClass is an instance of a component, * or a class that needs to be instantiated from context. * @param ctx */ function resolveComponentInstance(ctx: Readonly) { if (typeof componentInstanceOrClass !== 'function') { return componentInstanceOrClass; } // TODO(semver-major) @bajtos: Reminder to remove this on the next major release const componentName = componentInstanceOrClass.name; const componentKey = `${CoreBindings.COMPONENTS}.${componentName}`; return ctx.getSync(componentKey); } } }; } /** * Method which binds a given Booter to a given Context with the Prefix and * Tags expected by the Bootstrapper * * @param ctx - The Context to bind the Booter Class * @param booterCls - Booter class to be bound */ export function bindBooter( ctx: Context, booterCls: Constructor, ): Binding { const binding = createBindingFromClass(booterCls, { namespace: BootBindings.BOOTERS, defaultScope: BindingScope.SINGLETON, }).tag(BootTags.BOOTER); ctx.add(binding); /** * Set up configuration binding as alias to `BootBindings.BOOT_OPTIONS` * so that the booter can use `@config`. */ if (binding.tagMap.artifactNamespace) { ctx .configure(binding.key) .toAlias( `${BootBindings.BOOT_OPTIONS.key}#${binding.tagMap.artifactNamespace}`, ); } return binding; } // eslint-disable-next-line @typescript-eslint/naming-convention export const _bindBooter = bindBooter; // For backward-compatibility