Skip to content

Instantly share code, notes, and snippets.

@flisboac
Created July 14, 2021 18:07
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save flisboac/b778aba38e69e6a3dd9d6a70cda0edf9 to your computer and use it in GitHub Desktop.
Save flisboac/b778aba38e69e6a3dd9d6a70cda0edf9 to your computer and use it in GitHub Desktop.
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