nest-transact
Version:
This package give you a simplest possibility to use transactions with Nestjs
157 lines (131 loc) • 5.52 kB
text/typescript
import 'reflect-metadata';
import { Injectable } from '@nestjs/common';
import { EntityManager, Repository } from 'typeorm';
import { ModuleRef } from '@nestjs/core';
import { PARAMTYPES_METADATA, SELF_DECLARED_DEPS_METADATA } from '@nestjs/common/constants';
import _xor from 'lodash.xor';
type ClassType<T = any> = new (...args: any[]) => T;
type ForwardRef = {
forwardRef: () => any;
};
export interface WithTransactionOptions {
/**
* Class types, that will not rebuild in transaction,
* for example - it can be a service without any repositories
* or some cache service, and that service must not rebuild in
* app in any time
*/
excluded?: ClassType[];
}
()
export class TransactionFor<T = any> {
private cache: Map<string, any> = new Map();
constructor(private moduleRef: ModuleRef) {
}
public withTransaction(manager: EntityManager, transactionOptions: WithTransactionOptions = {}): this {
const newInstance = this.findArgumentsForProvider(this.constructor as ClassType<this>, manager, transactionOptions.excluded ?? []);
this.cache.clear();
return newInstance;
}
private getArgument(param: string | ClassType | ForwardRef, manager: EntityManager, excluded: ClassType[]): any {
if (typeof param === 'object' && 'forwardRef' in param) {
return this.moduleRef.get(param.forwardRef(), { strict: false });
}
const id = typeof param === 'string' ? param : typeof param === 'function' ? param.name : undefined;
if (id === undefined) {
throw new Error(`Can't get injection token from ${param}`);
}
const isExcluded = excluded.length > 0 && excluded.some((ex) => ex.name === id);
if (id === `${ModuleRef.name}`) {
return this.moduleRef;
}
if (isExcluded) {
/// Returns current instance of service, if it is excluded
return this.moduleRef.get(id, { strict: false });
}
let argument: Repository<any>;
if (this.cache.has(id)) {
return this.cache.get(id);
}
const canBeRepository = id.includes('Repository');
if (typeof param === 'string' || canBeRepository) {
// Fetch the dependency
let dependency: Repository<any> | null = null;
try {
if (canBeRepository) {
// Return directly if param is custom repository
return manager.getCustomRepository(param as any);
}
} catch (error) {
dependency = this.moduleRef.get(param, { strict: false });
}
if (dependency! instanceof Repository || canBeRepository) {
// If the dependency is a repository, make a new repository with the desired transaction manager.
const entity: any = dependency!.metadata.target;
argument = manager.getRepository(entity);
} else {
if (!dependency) {
dependency = this.moduleRef.get(param, { strict: false });
}
// The dependency is not a repository, use it directly.
argument = dependency!;
}
} else {
argument = this.findArgumentsForProvider(param as ClassType, manager, excluded);
}
this.cache.set(id, argument);
return argument;
}
private findArgumentsForProvider(constructor: ClassType, manager: EntityManager, excluded: ClassType[]): any {
const args: any[] = [];
const keys = Reflect.getMetadataKeys(constructor);
const missingParams: string[] = [];
keys.forEach((key) => {
if (key === PARAMTYPES_METADATA) {
const paramTypes: Array<{ name: string } | ClassType | string> = Reflect.getMetadata(key, constructor);
const selfParamTypes: Array<{ param: string } | { param: { name: string } }> = Reflect.getMetadata(SELF_DECLARED_DEPS_METADATA, constructor);
for (const param of paramTypes) {
// In case we could not get parameter types from 'design:paramtypes'
// metadata key
if (!param) {
const paramTypeNameMap = paramTypes
.filter((item) => !!item)
.reduce((acc, currVal, currIdx) => {
acc.set((currVal as { name: string }).name, currIdx);
return acc;
}, new Map());
const selfParamTypeMap = selfParamTypes
.filter((item) => !!item)
.reduce((acc, currVal, currIdx) => {
if (typeof currVal.param === 'string') {
acc.set(currVal.param, currIdx);
} else {
acc.set(currVal.param.name, currIdx);
}
return acc;
}, new Map());
const exclusion = _xor(Array.from(paramTypeNameMap.keys()), Array.from(selfParamTypeMap.keys()));
exclusion.forEach((item) => {
if (paramTypeNameMap.has(item)) {
const val = paramTypes[paramTypeNameMap.get(item)] as { name: string };
missingParams.push(val.name);
}
if (selfParamTypeMap.has(item)) {
const val = selfParamTypes[selfParamTypeMap.get(item)];
missingParams.push(typeof val?.param === 'object' ? val?.param?.name : val.param);
}
});
return;
}
const argument = this.getArgument(param as string, manager, excluded);
args.push(argument);
}
}
});
missingParams.forEach((item) => {
const argument = this.getArgument(item, manager, excluded);
args.push(argument);
});
return new constructor(...args);
}
}