UNPKG

6.77 kBPlain TextView Raw
1// Copyright IBM Corp. and LoopBack contributors 2019,2020. All Rights Reserved.
2// Node module: @loopback/core
3// This file is licensed under the MIT License.
4// License text available at https://opensource.org/licenses/MIT
5
6import {
7 Binding,
8 BindingFilter,
9 BindingFromClassOptions,
10 BindingTemplate,
11 bindingTemplateFor,
12 ContextTags,
13 ContextView,
14 createBindingFromClass,
15 DecoratorFactory,
16 inject,
17 InjectionMetadata,
18 isDynamicValueProviderClass,
19 isProviderClass,
20 MetadataInspector,
21 transformValueOrPromise,
22} from '@loopback/context';
23import {ServiceOrProviderClass} from './application';
24import {CoreTags} from './keys';
25
26/**
27 * Representing an interface for services. In TypeScript, the `interface` does
28 * not have reflections at runtime. We use a string, a symbol or a Function as
29 * the type for the service interface.
30 */
31export type ServiceInterface = string | symbol | Function;
32
33/**
34 * Options to register a service binding
35 */
36export interface ServiceOptions extends BindingFromClassOptions {
37 interface?: ServiceInterface;
38}
39
40/**
41 * `@service` injects a service instance that matches the class or interface.
42 *
43 * @param serviceInterface - Interface for the service. It can be in one of the
44 * following forms:
45 *
46 * - A class, such as MyService
47 * - A string that identifies the interface, such as `'MyService'`
48 * - A symbol that identifies the interface, such as `Symbol('MyService')`
49 *
50 * If not provided, the value is inferred from the design:type of the parameter
51 * or property
52 *
53 * @example
54 * ```ts
55 *
56 * const ctx = new Context();
57 * ctx.bind('my-service').toClass(MyService);
58 * ctx.bind('logger').toClass(Logger);
59 *
60 * export class MyController {
61 * constructor(@service(MyService) private myService: MyService) {}
62 *
63 * @service()
64 * private logger: Logger;
65 * }
66 *
67 * ctx.bind('my-controller').toClass(MyController);
68 * await myController = ctx.get<MyController>('my-controller');
69 * ```
70 */
71export function service(
72 serviceInterface?: ServiceInterface,
73 metadata?: InjectionMetadata,
74) {
75 return inject(
76 '',
77 {decorator: '@service', ...metadata},
78 (ctx, injection, session) => {
79 let serviceType = serviceInterface;
80 if (!serviceType) {
81 if (typeof injection.methodDescriptorOrParameterIndex === 'number') {
82 serviceType = MetadataInspector.getDesignTypeForMethod(
83 injection.target,
84 injection.member!,
85 )?.parameterTypes[injection.methodDescriptorOrParameterIndex];
86 } else {
87 serviceType = MetadataInspector.getDesignTypeForProperty(
88 injection.target,
89 injection.member!,
90 );
91 }
92 }
93 if (serviceType === undefined) {
94 const targetName = DecoratorFactory.getTargetName(
95 injection.target,
96 injection.member,
97 injection.methodDescriptorOrParameterIndex,
98 );
99 const msg =
100 `No design-time type metadata found while inspecting ${targetName}. ` +
101 'You can either use `@service(ServiceClass)` or ensure `emitDecoratorMetadata` is enabled in your TypeScript configuration. ' +
102 'Run `tsc --showConfig` to print the final TypeScript configuration of your project.';
103 throw new Error(msg);
104 }
105
106 if (serviceType === Object || serviceType === Array) {
107 throw new Error(
108 'Service class cannot be inferred from design type. Use @service(ServiceClass).',
109 );
110 }
111 const view = new ContextView(ctx, filterByServiceInterface(serviceType));
112 const result = view.resolve({
113 optional: metadata?.optional,
114 asProxyWithInterceptors: metadata?.asProxyWithInterceptors,
115 session,
116 });
117
118 const serviceTypeName =
119 typeof serviceType === 'string'
120 ? serviceType
121 : typeof serviceType === 'symbol'
122 ? serviceType.toString()
123 : serviceType.name;
124 return transformValueOrPromise(result, values => {
125 if (values.length === 1) return values[0];
126 if (values.length >= 1) {
127 throw new Error(
128 `More than one bindings found for ${serviceTypeName}`,
129 );
130 } else {
131 if (metadata?.optional) {
132 return undefined;
133 }
134 throw new Error(
135 `No binding found for ${serviceTypeName}. Make sure a service ` +
136 `binding is created in context ${ctx.name} with serviceInterface (${serviceTypeName}).`,
137 );
138 }
139 });
140 },
141 );
142}
143
144/**
145 * Create a binding filter by service class
146 * @param serviceInterface - Service class matching the one used by `binding.toClass()`
147 * @param options - Options to control if subclasses should be skipped for matching
148 */
149export function filterByServiceInterface(
150 serviceInterface: ServiceInterface,
151): BindingFilter {
152 return binding =>
153 binding.valueConstructor === serviceInterface ||
154 binding.tagMap[CoreTags.SERVICE_INTERFACE] === serviceInterface;
155}
156
157/**
158 * Create a service binding from a class or provider
159 * @param cls - Service class or provider
160 * @param options - Service options
161 */
162export function createServiceBinding<S>(
163 cls: ServiceOrProviderClass<S>,
164 options: ServiceOptions = {},
165): Binding<S> {
166 let name = options.name;
167 if (!name && isProviderClass(cls)) {
168 // Trim `Provider` from the default service name
169 // This is needed to keep backward compatibility
170 const templateFn = bindingTemplateFor(cls);
171 const template = Binding.bind<S>('template').apply(templateFn);
172 if (
173 template.tagMap[ContextTags.PROVIDER] &&
174 !template.tagMap[ContextTags.NAME]
175 ) {
176 // The class is a provider and no `name` tag is found
177 name = cls.name.replace(/Provider$/, '');
178 }
179 }
180 if (!name && isDynamicValueProviderClass(cls)) {
181 // Trim `Provider` from the default service name
182 const templateFn = bindingTemplateFor(cls);
183 const template = Binding.bind<S>('template').apply(templateFn);
184 if (
185 template.tagMap[ContextTags.DYNAMIC_VALUE_PROVIDER] &&
186 !template.tagMap[ContextTags.NAME]
187 ) {
188 // The class is a provider and no `name` tag is found
189 name = cls.name.replace(/Provider$/, '');
190 }
191 }
192 const binding = createBindingFromClass(cls, {
193 name,
194 type: CoreTags.SERVICE,
195 ...options,
196 }).apply(asService(options.interface ?? cls));
197 return binding;
198}
199
200/**
201 * Create a binding template for a service interface
202 * @param serviceInterface - Service interface
203 */
204export function asService(serviceInterface: ServiceInterface): BindingTemplate {
205 return function serviceTemplate(binding: Binding) {
206 binding.tag({
207 [ContextTags.TYPE]: CoreTags.SERVICE,
208 [CoreTags.SERVICE_INTERFACE]: serviceInterface,
209 });
210 };
211}
212
\No newline at end of file