-
-
Save flisboac/6af02b5254088558362757593dc54f9c to your computer and use it in GitHub Desktop.
// https://gist.github.com/flisboac/6af02b5254088558362757593dc54f9c | |
import { | |
Abstract, | |
DynamicModule, | |
INestApplicationContext, | |
Type, | |
} from '@nestjs/common'; | |
import { ContextId } from '@nestjs/core'; | |
import { | |
ClassConstructor, | |
ClassTransformOptions, | |
plainToClass, | |
TransformOptions, | |
TransformationType, | |
TransformFnParams, | |
} from 'class-transformer'; | |
export interface Transformer { | |
transform(params: TransformFnParams): Promise<unknown>; | |
} | |
export interface TransformerFunction { | |
(params: AsyncTransformerParams): Promise<unknown>; | |
} | |
export interface TransformerResolver { | |
resolveTransformer(options: TransformerResolverOptions): Promise<Transformer>; | |
} | |
export type TransformerResolverOptions = { | |
strict: boolean; | |
scoped: boolean; | |
contextId?: ContextId; | |
token: ResolvedTransformerToken; | |
}; | |
export type BaseTransformOptions = TransformOptions & { | |
select?: Type<any> | DynamicModule; | |
strict?: boolean; | |
scoped?: boolean; | |
}; | |
export type AsyncTransformWithOptions = BaseTransformOptions & { | |
token?: TransformerToken; | |
}; | |
export type AsyncTransformOptions = BaseTransformOptions & { | |
body?: TransformerFunction; | |
}; | |
export type AnyTransformOptions = | |
| AsyncTransformOptions | |
| AsyncTransformWithOptions; | |
export type AsyncTransformerParams = Omit<TransformFnParams, 'options'> & { | |
options: AnyTransformOptions; | |
}; | |
export type TransformEntry = { | |
propertyKey: string | symbol; | |
options: AnyTransformOptions; | |
}; | |
type Token<T> = string | symbol | Type<T> | Abstract<T>; | |
type AbstractTransformerType = Abstract<Transformer>; | |
type TransformerType = Type<Transformer>; | |
type TransformerToken = | |
| string | |
| symbol | |
| (() => AbstractTransformerType | TransformerType); | |
type ResolvedTransformerToken = | |
| string | |
| symbol | |
| AbstractTransformerType | |
| TransformerType; | |
const TRANSFORM_ENTRIES = Symbol('TRANSFORM_ENTRIES'); | |
class TransformContext implements TransformerResolver { | |
public appContext: INestApplicationContext; | |
public appContextId: ContextId | undefined; | |
public resolverToken: Token<TransformerResolver> | undefined; | |
async _initialize( | |
appContext: INestApplicationContext, | |
appContextId: ContextId | undefined, | |
resolverToken: Token<TransformerResolver> | undefined, | |
): Promise<void> { | |
if (this.appContext) { | |
throw new Error( | |
'The InjectedTransform mechanism was already initialized.', | |
); | |
} | |
this.appContext = appContext; | |
this.appContextId = appContextId; | |
this.resolverToken = resolverToken; | |
} | |
_ensureInitialized() { | |
if (!this.appContext) { | |
throw new Error('Async Transform context was not initialized yet.'); | |
} | |
} | |
async getTransformer( | |
options: AsyncTransformWithOptions, | |
): Promise<Transformer> { | |
const resolvedOptions = this.resolveOptions(options); | |
const { nestContext, strict, token } = resolvedOptions; | |
const resolver = this.resolverToken | |
? nestContext.get(this.resolverToken, { strict }) | |
: this; | |
const instance = await resolver.resolveTransformer(resolvedOptions); | |
if (!instance) { | |
const transformerName = | |
typeof token === 'function' ? token.name : String(token); | |
throw new Error( | |
`No instance for transformer of type ${transformerName} was created, or \`useContainer\` was not called yet.`, | |
); | |
} | |
return instance; | |
} | |
async resolveTransformer( | |
options: TransformerResolverOptions & { | |
nestContext: INestApplicationContext; | |
}, | |
): Promise<Transformer> { | |
const { nestContext, strict, contextId, scoped, token } = options; | |
if (scoped) { | |
return nestContext.resolve(token, contextId, { strict }); | |
} | |
return nestContext.get(token, { strict }); | |
} | |
addTransformEntry<T>( | |
type: Type<T> | Abstract<T>, | |
options: TransformEntry, | |
): void { | |
if (typeof type === 'function' && 'prototype' in type) { | |
type = type.prototype; | |
} | |
const constructor = type.constructor; | |
const entries: TransformEntry[] = | |
Reflect.getMetadata(TRANSFORM_ENTRIES, constructor) || []; | |
entries.push(options); | |
Reflect.defineMetadata(TRANSFORM_ENTRIES, entries, constructor); | |
} | |
listTransformEntries<T>(type: Type<T> | Abstract<T>): TransformEntry[] { | |
if (typeof type === 'function' && 'prototype' in type) { | |
type = type.prototype; | |
} | |
const constructor = type.constructor; | |
return Reflect.getMetadata(TRANSFORM_ENTRIES, constructor) || []; | |
} | |
private resolveOptions( | |
options: AsyncTransformWithOptions, | |
): TransformerResolverOptions & { nestContext: INestApplicationContext } { | |
const { select } = options; | |
let nestContext = this.appContext; | |
if (select) { | |
nestContext = this.appContext.select(select); | |
} | |
const strict = options.strict ?? !!module; | |
const scoped = options.scoped ?? false; | |
const token = resolveToken(options.token); | |
return { strict, scoped, token, nestContext }; | |
} | |
} | |
let context: TransformContext; | |
function getContext(): TransformContext { | |
if (!context) { | |
context = new TransformContext(); | |
} | |
return context; | |
} | |
function getInitializedContext(): TransformContext { | |
const ctx = getContext(); | |
ctx._ensureInitialized(); | |
return ctx; | |
} | |
function resolveToken(token: TransformerToken): ResolvedTransformerToken { | |
return typeof token === 'function' ? token() : token; | |
} | |
export interface UseContainerOptions { | |
contextId?: ContextId; | |
resolver?: Token<TransformerResolver>; | |
} | |
export async function useContainer( | |
_appContext: INestApplicationContext, | |
options: UseContainerOptions = {}, | |
): Promise<void> { | |
getContext()._initialize(_appContext, options.contextId, options.resolver); | |
} | |
export function AsyncTransform( | |
fn: TransformerFunction, | |
options?: AsyncTransformOptions, | |
): PropertyDecorator; | |
export function AsyncTransform( | |
options: AsyncTransformOptions & { body: TransformerToken }, | |
): PropertyDecorator; | |
export function AsyncTransform( | |
_body: | |
| TransformerFunction | |
| (AsyncTransformOptions & { body: TransformerToken }), | |
_options: AsyncTransformOptions = {}, | |
): PropertyDecorator { | |
let options = _options; | |
if (typeof _body === 'function') { | |
options.body = _body; | |
} else { | |
options = _body; | |
} | |
return (type: any, propertyKey) => { | |
getContext().addTransformEntry(type, { propertyKey, options }); | |
}; | |
} | |
export function AsyncTransformWith( | |
useClass: TransformerToken, | |
options?: AsyncTransformWithOptions, | |
): PropertyDecorator; | |
export function AsyncTransformWith( | |
options: AsyncTransformWithOptions & { token: TransformerToken }, | |
): PropertyDecorator; | |
export function AsyncTransformWith( | |
_token: | |
| TransformerToken | |
| (AsyncTransformWithOptions & { token: TransformerToken }), | |
_options: AsyncTransformWithOptions = {}, | |
): PropertyDecorator { | |
let options = _options; | |
if ( | |
typeof _token === 'string' || | |
typeof _token === 'symbol' || | |
typeof _token === 'function' | |
) { | |
options = { ..._options, token: _token }; | |
} else { | |
options = _token; | |
} | |
return (type: any, propertyKey) => { | |
getContext().addTransformEntry(type, { propertyKey, options }); | |
}; | |
} | |
export function asyncPlainToClass<T, V>( | |
type: ClassConstructor<T>, | |
plain: V[], | |
options?: ClassTransformOptions, | |
): Promise<T[]>; | |
export function asyncPlainToClass<T, V>( | |
type: ClassConstructor<T>, | |
plain: V, | |
options?: ClassTransformOptions, | |
): Promise<T>; | |
export async function asyncPlainToClass<T, V>( | |
type: ClassConstructor<T>, | |
plains: V[] | V, | |
callOptions: ClassTransformOptions = {}, | |
): Promise<T[] | T> { | |
const entries = getContext().listTransformEntries(type); | |
for (const entry of entries) { | |
const { propertyKey, options: decoratorOptions } = entry; | |
const options: AnyTransformOptions = { | |
...callOptions, | |
...decoratorOptions, | |
}; | |
const postTransform = async (transformed: T): Promise<T> => { | |
const value = transformed[propertyKey]; | |
const key = String(propertyKey); | |
const obj = transformed; | |
const type = TransformationType.PLAIN_TO_CLASS; | |
let propValue: unknown; | |
if ('body' in decoratorOptions) { | |
propValue = await decoratorOptions.body({ | |
value, | |
key, | |
obj, | |
type, | |
options, | |
}); | |
} else if ('token' in decoratorOptions) { | |
const transformer = await getInitializedContext().getTransformer( | |
decoratorOptions, | |
); | |
propValue = await transformer.transform({ | |
value, | |
key, | |
obj, | |
type, | |
options, | |
}); | |
} else { | |
// should never happen, tho | |
throw new Error('Invalid transform configuration'); | |
} | |
transformed[propertyKey] = propValue; | |
return transformed; | |
}; | |
if (Array.isArray(plains)) { | |
const transformed = plainToClass(type, plains, callOptions); | |
const result = await Promise.all(transformed.map(postTransform)); | |
return result; | |
} | |
const transformed = plainToClass(type, plains, callOptions); | |
const result = await postTransform(transformed); | |
return result; | |
} | |
} |
@volkramweber you should pass your NestJS instance to useContainer
right after you've created it -- near where you configure your custom ValidationPipe, but before calling app.listen()
(or, if you're working with a standalone app, before calling any other runtime logic or service method, etc). An example:
async function main() {
const app = await NestFactory.create(AppModule);
// Set up the validation pipe, and other things
await useContainer(app);
app.listen(3000);
}
main();
useContainer
's signature is async
to allow for eventual dynamic resolutions (or to perform any other async operations) while the DI container is being initially set up. But it's not necessary for it to be async
, because the only thing useContainer
does is to set up a global containing the DI container you just passed; you're free to change the signature to "sync" if you want.
Anyways, this error is thrown by this function. It's there just to ensure you've set up the Dependency Injection Context (aka. your NestJS instance) before you call any of the front-facing conversion methods (because you will need to inject services whilst converting plains, and whatnot).
@volkramweber I made a rookie mistake there... I'm sorry! I fixed the script, hopefully everything is all right, now. I also opened a PR on your repo with the fix.
@flisboac Thanks a lot for your work, it looks like the feature I really miss while using class-transformer.
I try to use the
AsyncTransform
decorator within a DTO andasyncPlainToClass
within a custom validation pipe.But I always get
Error: Async Transform context was not initialized yet.
on startup.Where and how do I have to call
useContainer
? Do I need to do anything else?