UNPKG

8.34 kBPlain TextView Raw
1// Copyright IBM Corp. and LoopBack contributors 2018,2020. All Rights Reserved.
2// Node module: @loopback/boot
3// This file is licensed under the MIT License.
4// License text available at https://opensource.org/licenses/MIT
5
6import {
7 Application,
8 Binding,
9 BindingFilter,
10 BindingFromClassOptions,
11 BindingScope,
12 Component,
13 Constructor,
14 Context,
15 CoreBindings,
16 createBindingFromClass,
17 MixinTarget,
18} from '@loopback/core';
19import {BootComponent} from '../boot.component';
20import {createComponentApplicationBooterBinding} from '../booters/component-application.booter';
21import {Bootstrapper} from '../bootstrapper';
22import {BootBindings, BootTags} from '../keys';
23import {Bootable, Booter, BootOptions, InstanceWithBooters} from '../types';
24
25/* eslint-enable @typescript-eslint/no-unused-vars */
26
27// Binding is re-exported as Binding / Booter types are needed when consuming
28// BootMixin and this allows a user to import them from the same package (UX!)
29export {Binding};
30
31/**
32 * Mixin for @loopback/boot. This Mixin provides the following:
33 * - Implements the Bootable Interface as follows.
34 * - Add a `projectRoot` property to the Class
35 * - Adds an optional `bootOptions` property to the Class that can be used to
36 * store the Booter conventions.
37 * - Adds the `BootComponent` to the Class (which binds the Bootstrapper and default Booters)
38 * - Provides the `boot()` convenience method to call Bootstrapper.boot()
39 * - Provides the `booter()` convenience method to bind a Booter(s) to the Application
40 * - Override `component()` to call `mountComponentBooters`
41 * - Adds `mountComponentBooters` which binds Booters to the application from `component.booters[]`
42 *
43 * @param superClass - Application class
44 * @returns A new class that extends the super class with boot related methods
45 *
46 * @typeParam T - Type of the application class as the target for the mixin
47 */
48export function BootMixin<T extends MixinTarget<Application>>(superClass: T) {
49 return class extends superClass implements Bootable {
50 projectRoot: string;
51 bootOptions?: BootOptions;
52
53 // eslint-disable-next-line @typescript-eslint/no-explicit-any
54 constructor(...args: any[]) {
55 super(...args);
56 this.component(BootComponent);
57
58 // We Dynamically bind the Project Root and Boot Options so these values can
59 // be used to resolve an instance of the Bootstrapper (as they are dependencies)
60 this.bind(BootBindings.PROJECT_ROOT).toDynamicValue(
61 () => this.projectRoot,
62 );
63 this.bind(BootBindings.BOOT_OPTIONS).toDynamicValue(
64 () => this.bootOptions ?? {},
65 );
66 }
67
68 booted: boolean;
69
70 /**
71 * Override to detect and warn about starting without booting.
72 */
73 // eslint-disable-next-line @typescript-eslint/ban-ts-comment
74 // @ts-ignore
75 public async start(): Promise<void> {
76 await super.start();
77 if (!this.booted) {
78 process.emitWarning(
79 'App started without booting. Did you forget to call ' +
80 '`await app.boot()`?',
81 'LoopBackWarning',
82 );
83 }
84 }
85
86 /**
87 * Convenience method to call bootstrapper.boot() by resolving bootstrapper
88 */
89 async boot(): Promise<void> {
90 /* eslint-disable @typescript-eslint/ban-ts-comment */
91 // A workaround to access protected Application methods
92 const self = this as unknown as Application;
93
94 if (this.state === 'booting') {
95 // @ts-ignore
96 return self.awaitState('booted');
97 }
98 // @ts-ignore
99 self.assertNotInProcess('boot');
100 // @ts-ignore
101 self.assertInStates('boot', 'created', 'booted');
102
103 if (this.state === 'booted') return;
104 // @ts-ignore
105 self.setState('booting');
106
107 // Get a instance of the BootStrapper
108 const bootstrapper: Bootstrapper = await this.get(
109 BootBindings.BOOTSTRAPPER_KEY,
110 );
111
112 await bootstrapper.boot();
113
114 // @ts-ignore
115 this.setState('booted');
116 this.booted = true;
117
118 /* eslint-enable @typescript-eslint/ban-ts-comment */
119 }
120
121 /**
122 * Given a N number of Booter Classes, this method binds them using the
123 * prefix and tag expected by the Bootstrapper.
124 *
125 * @param booterCls - Booter classes to bind to the Application
126 *
127 * @example
128 * ```ts
129 * app.booters(MyBooter, MyOtherBooter)
130 * ```
131 */
132 booters(...booterCls: Constructor<Booter>[]): Binding[] {
133 return booterCls.map(cls => bindBooter(this as unknown as Context, cls));
134 }
135
136 /**
137 * Register a booter to boot a sub-application. See
138 * {@link createComponentApplicationBooterBinding} for more details.
139 *
140 * @param subApp - A sub-application with artifacts to be booted
141 * @param filter - A binding filter to select what bindings from the sub
142 * application should be added to the main application.
143 */
144 applicationBooter(subApp: Application & Bootable, filter?: BindingFilter) {
145 const binding = createComponentApplicationBooterBinding(subApp, filter);
146 this.add(binding);
147 return binding;
148 }
149
150 /**
151 * Override to ensure any Booter's on a Component are also mounted.
152 *
153 * @param component - The component to add.
154 *
155 * @example
156 * ```ts
157 *
158 * export class ProductComponent {
159 * booters = [ControllerBooter, RepositoryBooter];
160 * providers = {
161 * [AUTHENTICATION_STRATEGY]: AuthStrategy,
162 * [AUTHORIZATION_ROLE]: Role,
163 * };
164 * };
165 *
166 * app.component(ProductComponent);
167 * ```
168 */
169 // Unfortunately, TypeScript does not allow overriding methods inherited
170 // from mapped types. https://github.com/microsoft/TypeScript/issues/38496
171 // eslint-disable-next-line @typescript-eslint/ban-ts-comment
172 // @ts-ignore
173 public component<C extends Component = Component>(
174 componentCtor: Constructor<C>,
175 nameOrOptions?: string | BindingFromClassOptions,
176 ) {
177 const binding = super.component(componentCtor, nameOrOptions);
178 const instance = this.getSync<InstanceWithBooters>(binding.key);
179
180 this.mountComponentBooters(instance);
181 return binding;
182 }
183
184 /**
185 * Get an instance of a component and mount all it's
186 * booters. This function is intended to be used internally
187 * by component()
188 *
189 * @param component - The component to mount booters of
190 */
191 mountComponentBooters(
192 componentInstanceOrClass: Constructor<unknown> | InstanceWithBooters,
193 ) {
194 const componentInstance = resolveComponentInstance(this);
195 if (componentInstance.booters) {
196 this.booters(...componentInstance.booters);
197 }
198
199 /**
200 * Determines if componentInstanceOrClass is an instance of a component,
201 * or a class that needs to be instantiated from context.
202 * @param ctx
203 */
204 function resolveComponentInstance(ctx: Readonly<Context>) {
205 if (typeof componentInstanceOrClass !== 'function') {
206 return componentInstanceOrClass;
207 }
208
209 // TODO(semver-major) @bajtos: Reminder to remove this on the next major release
210 const componentName = componentInstanceOrClass.name;
211 const componentKey = `${CoreBindings.COMPONENTS}.${componentName}`;
212 return ctx.getSync<InstanceWithBooters>(componentKey);
213 }
214 }
215 };
216}
217
218/**
219 * Method which binds a given Booter to a given Context with the Prefix and
220 * Tags expected by the Bootstrapper
221 *
222 * @param ctx - The Context to bind the Booter Class
223 * @param booterCls - Booter class to be bound
224 */
225export function bindBooter(
226 ctx: Context,
227 booterCls: Constructor<Booter>,
228): Binding {
229 const binding = createBindingFromClass(booterCls, {
230 namespace: BootBindings.BOOTERS,
231 defaultScope: BindingScope.SINGLETON,
232 }).tag(BootTags.BOOTER);
233 ctx.add(binding);
234 /**
235 * Set up configuration binding as alias to `BootBindings.BOOT_OPTIONS`
236 * so that the booter can use `@config`.
237 */
238 if (binding.tagMap.artifactNamespace) {
239 ctx
240 .configure(binding.key)
241 .toAlias(
242 `${BootBindings.BOOT_OPTIONS.key}#${binding.tagMap.artifactNamespace}`,
243 );
244 }
245 return binding;
246}
247
248// eslint-disable-next-line @typescript-eslint/naming-convention
249export const _bindBooter = bindBooter; // For backward-compatibility