Skip to content

Commit

Permalink
feat: transactional plugin (#102)
Browse files Browse the repository at this point in the history
  • Loading branch information
Papooch committed Jan 22, 2024
1 parent dc14abe commit 0bd7682
Show file tree
Hide file tree
Showing 19 changed files with 669 additions and 50 deletions.
3 changes: 2 additions & 1 deletion .github/workflows/publish-to-npm.workflow.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ jobs:
- uses: actions/setup-node@v2
with:
node-version: 16.x
registry-url: "https://registry.npmjs.org"
registry-url: 'https://registry.npmjs.org'
cache: npm

- name: Configure git
Expand All @@ -29,6 +29,7 @@ jobs:
git config --local user.email 'github-action[bot]@github.com'
- run: yarn
- run: yarn workspace nestjs-cls build
- run: yarn test
- run: yarn monodeploy
env:
Expand Down
3 changes: 2 additions & 1 deletion .github/workflows/run-tests.workflow.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,14 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
node: [16, 18]
node: [16, 18, 20]
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v2
with:
node-version: ${{ matrix.node }}.x
cache: npm
- run: yarn
- run: yarn workspace nestjs-cls build
- run: yarn lint
- run: yarn test
6 changes: 3 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,9 @@
],
"packageManager": "yarn@3.6.1",
"scripts": {
"test": "yarn workspaces foreach run test",
"build": "yarn workspaces foreach run build",
"format": "prettier --write \"packages/**/*.ts\" \"test/**/*.ts\"",
"test": "yarn workspaces foreach --topological-dev run test",
"build": "yarn workspaces foreach --topological-dev run build",
"format": "prettier --write \"packages/**/*.ts\"",
"lint": "eslint \"packages/**/*.ts\"",
"lint:fix": "eslint \"packages/**/*.ts\" --fix",
"depcruise": "yarn depcruise packages --include-only \"^packages/.*/src\" --exclude \"\\.spec\\.ts\" --config --output-type dot | dot -T svg | yarn depcruise-wrap-stream-in-html > dependency-graph.html"
Expand Down
1 change: 1 addition & 0 deletions packages/core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,5 @@ export * from './lib/cls.service';
export * from './lib/cls.decorators';
export * from './lib/cls.options';
export * from './lib/plugin/cls-plugin.interface';
export * from './utils/copy-method-metadata';
export { Terminal } from './types/terminal.type';
20 changes: 2 additions & 18 deletions packages/core/src/lib/cls-initializers/use-cls.decorator.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import 'reflect-metadata';
import { copyMethodMetadata } from '../../utils/copy-method-metadata';
import { ClsServiceManager } from '../cls-service-manager';
import { CLS_ID } from '../cls.constants';
import { ClsDecoratorOptions } from '../cls.options';
Expand Down Expand Up @@ -60,23 +61,6 @@ export function UseCls<TArgs extends any[]>(
return original.apply(this, args);
});
};
copyMetadata(original, descriptor.value);
copyMethodMetadata(original, descriptor.value);
};
}

/**
* Copies all metadata from one object to another.
* Useful for overwriting function definition in
* decorators while keeping all previously
* attached metadata
*
* @param from object to copy metadata from
* @param to object to copy metadata to
*/
function copyMetadata(from: any, to: any) {
const metadataKeys = Reflect.getMetadataKeys(from);
metadataKeys.map((key) => {
const value = Reflect.getMetadata(key, from);
Reflect.defineMetadata(key, value, to);
});
}
16 changes: 16 additions & 0 deletions packages/core/src/utils/copy-method-metadata.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
/**
* Copies all metadata from one object to another.
* Useful for overwriting function definition in
* decorators while keeping all previously
* attached metadata
*
* @param from object to copy metadata from
* @param to object to copy metadata to
*/
export function copyMethodMetadata(from: any, to: any) {
const metadataKeys = Reflect.getMetadataKeys(from);
metadataKeys.map((key) => {
const value = Reflect.getMetadata(key, from);
Reflect.defineMetadata(key, value, to);
});
}
17 changes: 17 additions & 0 deletions packages/transactional/jest.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
module.exports = {
moduleFileExtensions: ['js', 'json', 'ts'],
rootDir: '.',
testRegex: '.*\\.spec\\.ts$',
transform: {
'^.+\\.(t|j)s$': 'ts-jest',
},
collectCoverageFrom: ['src/**/*.(t|j)s'],
coverageDirectory: '../coverage',
testEnvironment: 'node',
globals: {
'ts-jest': {
isolatedModules: true,
maxWorkers: 1,
},
},
};
70 changes: 70 additions & 0 deletions packages/transactional/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
{
"name": "@nestjs-cls/transactional",
"version": "0.0.1",
"description": "A nestjs-cls plugin for transactional decorators",
"author": "papooch",
"license": "MIT",
"engines": {
"node": ">=12.17.0"
},
"repository": {
"type": "git",
"url": "git+https://github.com/Papooch/nestjs-cls.git"
},
"homepage": "https://papooch.github.io/nestjs-cls/",
"keywords": [
"nest",
"nestjs",
"cls",
"continuation-local-storage",
"als",
"AsyncLocalStorage",
"async_hooks",
"request context",
"async context"
],
"main": "dist/src/index.js",
"types": "dist/src/index.d.ts",
"files": [
"dist/src/**/!(*.spec).d.ts",
"dist/src/**/!(*.spec).js"
],
"scripts": {
"prepack": "cp ../../LICENSE ./LICENSE && cp ../../README.md ./README.md && yarn build",
"prebuild": "rimraf dist",
"build": "tsc",
"test": "jest",
"test:watch": "jest --watch",
"test:cov": "jest --coverage"
},
"peerDependencies": {
"@nestjs/common": "> 7.0.0 < 11",
"@nestjs/core": "> 7.0.0 < 11",
"nestjs-cls": "workspace:*",
"reflect-metadata": "*",
"rxjs": ">= 7"
},
"devDependencies": {
"@nestjs/cli": "^10.0.2",
"@nestjs/common": "^10.0.0",
"@nestjs/core": "^10.0.0",
"@nestjs/graphql": "^12.0.1",
"@nestjs/platform-express": "^10.0.0",
"@nestjs/schematics": "^10.0.1",
"@nestjs/testing": "^10.0.0",
"@types/express": "^4.17.13",
"@types/jest": "^28.1.2",
"@types/node": "^18.0.0",
"@types/supertest": "^2.0.12",
"jest": "^28.1.1",
"reflect-metadata": "^0.1.13",
"rimraf": "^3.0.2",
"rxjs": "^7.5.5",
"supertest": "^6.2.3",
"ts-jest": "^28.0.5",
"ts-loader": "^9.3.0",
"ts-node": "^10.8.1",
"tsconfig-paths": "^4.0.0",
"typescript": "~4.8.0"
}
}
File renamed without changes.
46 changes: 46 additions & 0 deletions packages/transactional/src/lib/interfaces.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
export interface TransactionalAdapterOptions<TClient, TOptions> {
startTransaction: (
options: TOptions,
fn: (...args: any[]) => Promise<any>,
setClient: (client?: TClient) => void,
) => Promise<any>;
getClient: () => TClient;
}

export type TransactionalOptionsAdapterFactory<TConnection, TClient, TOptions> =
(connection: TConnection) => TransactionalAdapterOptions<TClient, TOptions>;

export interface TransactionalAdapter<TConnection, TClient, TOptions> {
/**
* Token used to inject the `connection` into the adapter.
* It is later used to create transactions.
*/
connectionToken: any;

/**
* Function that accepts the `connection` based on the `connectionToken`
*
* Returns an object implementing the `TransactionalAdapterOptions` interface
* with the `startTransaction` and `getClient` methods.
*/
optionsFactory: TransactionalOptionsAdapterFactory<
TConnection,
TClient,
TOptions
>;
}

export interface TransactionalPluginOptions<TConnection, TClient, TOptions> {
adapter: TransactionalAdapter<TConnection, TClient, TOptions>;
imports?: any[];
}

export type TClientFromAdapter<TAdapter> =
TAdapter extends TransactionalAdapter<any, infer TClient, any>
? TClient
: never;

export type TOptionsFromAdapter<TAdapter> =
TAdapter extends TransactionalAdapter<any, any, infer TOptions>
? TOptions
: never;
27 changes: 27 additions & 0 deletions packages/transactional/src/lib/plugin-transactional.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { Provider } from '@nestjs/common';
import { ClsPlugin } from 'nestjs-cls';
import { TransactionalPluginOptions } from './interfaces';
import { TRANSACTIONAL_OPTIONS, TRANSACTION_CONNECTION } from './symbols';
import { TransactionHost } from './transaction-host';

export class ClsPluginTransactional implements ClsPlugin {
name: 'cls-plugin-transactional';
providers: Provider[];
imports?: any[];

constructor(options: TransactionalPluginOptions<any, any, any>) {
this.imports = options.imports;
this.providers = [
TransactionHost,
{
provide: TRANSACTION_CONNECTION,
useExisting: options.adapter.connectionToken,
},
{
provide: TRANSACTIONAL_OPTIONS,
inject: [TRANSACTION_CONNECTION],
useFactory: options.adapter.optionsFactory,
},
];
}
}
3 changes: 3 additions & 0 deletions packages/transactional/src/lib/symbols.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export const TRANSACTION_CONNECTION = Symbol('TRANSACTION_CONNECTION');
export const TRANSACTIONAL_CLIENT = Symbol('TRANSACTIONAL_CLIENT');
export const TRANSACTIONAL_OPTIONS = Symbol('TRANSACTIONAL_OPTIONS');
59 changes: 59 additions & 0 deletions packages/transactional/src/lib/transaction-host.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { Inject, Injectable } from '@nestjs/common';
import { ClsServiceManager } from 'nestjs-cls';
import {
TClientFromAdapter,
TOptionsFromAdapter,
TransactionalAdapterOptions,
} from './interfaces';
import { TRANSACTIONAL_OPTIONS, TRANSACTIONAL_CLIENT } from './symbols';

@Injectable()
export class TransactionHost<TAdapter = never> {
private cls = ClsServiceManager.getClsService();

constructor(
@Inject(TRANSACTIONAL_OPTIONS)
private _options: TransactionalAdapterOptions<
TClientFromAdapter<TAdapter>,
TOptionsFromAdapter<TAdapter>
>,
) {}

get client(): TClientFromAdapter<TAdapter> {
if (!this.cls.isActive()) {
return this._options.getClient();
}
return this.cls.get(TRANSACTIONAL_CLIENT) ?? this._options.getClient();
}

withTransaction<R>(fn: (...args: any[]) => Promise<R>): Promise<R>;
withTransaction<R>(
options: TOptionsFromAdapter<TAdapter>,
fn: (...args: any[]) => Promise<R>,
): Promise<R>;
withTransaction<R>(
optionsOrFn: any,
maybeFn?: (...args: any[]) => Promise<R>,
) {
let options: any;
let fn: (...args: any[]) => Promise<R>;
if (maybeFn) {
options = optionsOrFn;
fn = maybeFn;
} else {
options = {};
fn = optionsOrFn;
}
return this.cls.run({ ifNested: 'inherit' }, () =>
this._options.startTransaction(
options,
fn,
this.setClient.bind(this),
),
);
}

private setClient(client?: TClientFromAdapter<TAdapter>) {
this.cls.set(TRANSACTIONAL_CLIENT, client);
}
}
40 changes: 40 additions & 0 deletions packages/transactional/src/lib/transactional.decorator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { Inject } from '@nestjs/common';
import { copyMethodMetadata } from 'nestjs-cls';
import { TOptionsFromAdapter } from './interfaces';
import { TransactionHost } from './transaction-host';

export function Transactional<TAdapter>(
options?: TOptionsFromAdapter<TAdapter>,
) {
const injectTransactionHost = Inject(TransactionHost);
return (
target: any,
propertyKey: string | symbol,
descriptor: TypedPropertyDescriptor<(...args: any) => Promise<any>>,
) => {
if (!target.__transactionHost) {
injectTransactionHost(target, '__transactionHost');
}
const original = descriptor.value;
if (typeof original !== 'function') {
throw new Error(
`The @Transactional decorator can be only used on functions, but ${propertyKey.toString()} is not a function.`,
);
}
descriptor.value = function (
this: { __transactionHost: TransactionHost },
...args: any[]
) {
if (!this.__transactionHost) {
throw new Error(
`Failed to inject transaction host into ${target.constructor.name}`,
);
}
return this.__transactionHost.withTransaction(
options as never,
original.bind(this, ...args),
);
};
copyMethodMetadata(original, descriptor.value);
};
}
Loading

0 comments on commit 0bd7682

Please sign in to comment.