import { Inject } from "@nestjs/common";
import { Constructor } from "@decaf-ts/decoration";
import {
injectableServiceKey,
ModelService,
Repository as CoreRepository,
Service as CoreService,
} from "@decaf-ts/core";
import { Model } from "@decaf-ts/decorator-validation";
import { InternalError } from "@decaf-ts/db-decorators";
export type DecafFactoryProvider = {
provide: string;
useFactory: () => unknown;
};
const pendingProviders = new Map<string, DecafFactoryProvider>();
/**
* @description Drains the providers registered by {@link Service}/{@link Repository} usages.
* @summary Consumed by {@link DecafCoreModule.forRoot} to wire the tokens that those parameter
* decorators reference back into Nest's DI container, since a parameter decorator alone cannot
* supply a provider.
*/
export function getRegisteredDecafProviders(): DecafFactoryProvider[] {
return Array.from(pendingProviders.values());
}
function registerProvider(token: string, useFactory: () => unknown): void {
if (pendingProviders.has(token)) return;
pendingProviders.set(token, { provide: token, useFactory });
}
function isModelConstructor(value: unknown): value is Constructor<any> {
return typeof value === "function" && value.prototype instanceof Model;
}
function modelServiceToken(model: Constructor<any>): string {
return `${model.name}Service`;
}
function modelRepositoryToken(
model: Constructor<any>,
flavour?: string
): string {
return flavour ? `${model.name}Repository@${flavour}` : `${model.name}Repository`;
}
/**
* @description Constructor parameter decorator that injects a decaf service.
* @summary Translates decaf's service resolution APIs into a Nest `@Inject()` call:
* - `@Service(SomeModel)` injects the matching `@service(SomeModel)` decaf service when one is
* registered; otherwise it falls back to the `ModelService<SomeModel>` singleton
* (via {@link ModelService.forModel}).
* - `@Service(SomeServiceClass)` or `@Service("alias")` injects the matching `@service()`-decorated
* decaf {@link CoreService} (via {@link CoreService.get}).
* - `@Service()` infers the parameter's type from emitted constructor metadata and resolves it the
* same way, provided the parameter is typed as a concrete class (generics like `ModelService<X>`
* are erased at runtime, so the model-service form always requires an explicit argument).
* @param key the model class, service class, or alias to resolve. Omit to infer from the parameter type.
*/
export function Service(key?: string | Constructor<any>): ParameterDecorator {
return (
target: object,
propertyKey: string | symbol | undefined,
parameterIndex: number
): void => {
let resolved: string | Constructor<any> | undefined = key;
if (typeof resolved === "undefined") {
const paramTypes: any[] =
Reflect.getMetadata("design:paramtypes", target) || [];
resolved = paramTypes[parameterIndex];
}
if (typeof resolved === "undefined") {
throw new InternalError(
`@Service() could not determine an injection type for parameter ${parameterIndex} of ${(target as Constructor<any>).name}. Provide an explicit argument, e.g. @Service(SomeService).`
);
}
const asModel = typeof resolved !== "string" && isModelConstructor(resolved);
const token = asModel
? modelServiceToken(resolved as Constructor<any>)
: injectableServiceKey(resolved as string | Constructor<any>);
registerProvider(token, () => {
if (asModel) {
try {
return CoreService.get(resolved as Constructor<any>);
} catch {
return ModelService.forModel(resolved as Constructor<any>);
}
}
return typeof resolved === "string"
? CoreService.get(resolved)
: CoreService.get(resolved as Constructor<any>);
});
return Inject(token)(target, propertyKey, parameterIndex);
};
}
/**
* @description Constructor parameter decorator that injects a decaf {@link CoreRepository} for `model`.
* @summary Unlike {@link Service}, `model` is always treated as a Model class: the same class passed
* to `@Service(model)` and `@Repository(model)` resolves to two different injected objects (a
* `ModelService<M>` vs a `Repository<M>` respectively), via {@link CoreRepository.forModel}.
* @param model the model class to resolve a repository for.
* @param flavour optional adapter flavour/alias, forwarded to {@link CoreRepository.forModel}.
*/
export function Repository(
model: Constructor<any>,
flavour?: string
): ParameterDecorator {
return (
target: object,
propertyKey: string | symbol | undefined,
parameterIndex: number
): void => {
const token = modelRepositoryToken(model, flavour);
registerProvider(token, () => CoreRepository.forModel(model, flavour));
return Inject(token)(target, propertyKey, parameterIndex);
};
}
Source