Created
July 14, 2021 18:07
-
-
Save flisboac/b778aba38e69e6a3dd9d6a70cda0edf9 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { ContextId } from '@nestjs/core'; | |
import { Abstract, INestApplicationContext, Type } from '@nestjs/common'; | |
import { Transform, TransformFnParams, TransformOptions } from 'class-transformer'; | |
/** | |
* A class-based property transformer. | |
* | |
* Functionally, the call to `this.transform` is what substitutes the transformer | |
* function passed into the default `Transform` decorator. This means that it must | |
* return the correct value, and NOT be asynchronous. If you DO return promises, be | |
* sure to await each promise-valued property in the transformed object. | |
* | |
* All transformers must be registered as class providers in some module used in | |
* your application (location not relevant, because provider resolution will be | |
* non-strict). Only one instance will be created for each transformer class; | |
* _InjectedTransform_ will cache all instances internally. | |
* | |
* Because of the usage of `ModuleRef.resolve`, each instance will be created in its | |
* own DI sub-tree; to avoid that and ensure they share the same sub-tree, pass a | |
* _ContextId_ when initializing _InjectedTransform_. See `useContainer` for more | |
* details. | |
*/ | |
export interface Transformer { | |
transform(params: TransformFnParams): unknown; | |
} | |
type AbstractTransformerType = Abstract<Transformer>; | |
type TransformerType = Type<Transformer>; | |
type TransformerToken = string | symbol | (() => AbstractTransformerType | TransformerType); | |
type ResolvedTransformerToken = string | symbol | AbstractTransformerType | TransformerType; | |
export type InjectedTransformOptions = TransformOptions & { | |
scoped?: boolean; | |
token?: TransformerToken; | |
}; | |
let initialized = false; | |
let registeredTransformers: InjectedTransformOptions[] = []; | |
const transformers = new Map<ResolvedTransformerToken, Transformer>(); | |
function resolveToken(token: TransformerToken): ResolvedTransformerToken { | |
return typeof token === 'function' ? token() : token; | |
} | |
function getTransformer(transformerToken: ResolvedTransformerToken): Transformer { | |
const transformer = transformers.get(transformerToken); | |
if (!transformer) { | |
const transformerName = typeof transformerToken === 'function' ? transformerToken.name : String(transformerToken); | |
throw new Error( | |
`No instance for transformer of type ${transformerName} was created, or \`useContainer\` was not called yet.`, | |
); | |
} | |
return transformer; | |
} | |
export interface UseContainerOptions { | |
contextId?: ContextId; | |
} | |
/** | |
* Initializes _InjectedTransform_'s semi-static dependency injection mechanism. | |
* | |
* _InjectedTransform_ works partly outside the DI container. | |
* | |
* Transformer instances are created through NestJS's `ModuleRef.resolve` if they're scoped. | |
* Lookup is always performed in a non-strict manner. This means that your transformers MUST | |
* be registered as a class provider, in a module somewhere. | |
* | |
* Because class-transformer does not support asynchronous custom transformers (AFAICT), | |
* there is no way to use `ModuleRef.resolve` at transform-time. | |
* | |
* For those reasons, the InjectedTransform mechanism preemptively resolves all transformers | |
* when `useContainer` is called, and then caches all of them by constructor. This means that | |
* scoped transformers will be instantiated in their own DI sub-tree; to force all of those | |
* scoped transformers to share the same sub-tree, you can pass a contextId. Non-scoped | |
* transformers are not affected by contextId. | |
* | |
* @param appContext The application's main/root module. | |
* @param contextId An optional context ID to be used when resolving the transformers. | |
*/ | |
export async function useContainer( | |
appContext: INestApplicationContext, | |
options: UseContainerOptions = {}, | |
): Promise<void> { | |
if (initialized) { | |
throw new Error('The InjectedTransform mechanism was already initialized.'); | |
} | |
for (const { scope, token } of registeredTransformers) { | |
const resolvedToken = resolveToken(token); | |
const resolvedScoped = scope ?? false; | |
if (!transformers.has(resolvedToken)) { | |
if (resolvedScoped) { | |
const instance = await appContext.resolve(resolvedToken, options.contextId, { strict: false }); | |
transformers.set(resolvedToken, instance); | |
} else { | |
const instance = appContext.get(resolvedToken, { strict: false }); | |
transformers.set(resolvedToken, instance); | |
} | |
} | |
} | |
registeredTransformers = undefined; | |
initialized = true; | |
} | |
export function InjectedTransform(useClass: TransformerToken, options?: InjectedTransformOptions): PropertyDecorator; | |
export function InjectedTransform( | |
options: InjectedTransformOptions & { useClass: TransformerToken }, | |
): PropertyDecorator; | |
export function InjectedTransform( | |
_token: TransformerToken | InjectedTransformOptions, | |
_options: InjectedTransformOptions = {}, | |
): PropertyDecorator { | |
let options = _options; | |
if (typeof _token === 'string' || typeof _token === 'symbol' || typeof _token === 'function') { | |
options = { ..._options, token: _token }; | |
} else { | |
options = _token; | |
} | |
const token = options.token; | |
registeredTransformers.push(options); | |
const transformerWrapper = (params: TransformFnParams) => { | |
const resolvedtoken = resolveToken(token); | |
const transformer = getTransformer(resolvedtoken); | |
return transformer.transform(params); | |
}; | |
return (type, propertyName) => { | |
Transform(transformerWrapper, options)(type, propertyName); | |
}; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment