-
Notifications
You must be signed in to change notification settings - Fork 28
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(transactional-adapter-mongoose): add
mongoose
adapter
- Loading branch information
Showing
9 changed files
with
499 additions
and
1 deletion.
There are no files selected for viewing
5 changes: 5 additions & 0 deletions
5
packages/transactional-adapters/transactional-adapter-mongoose/README.md
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
# @nestjs-cls/transactional-adapter-knex | ||
|
||
Mongoose adapter for the `@nestjs-cls/transactional` plugin. | ||
|
||
### ➡️ [Go to the documentation website](https://papooch.github.io/nestjs-cls/plugins/available-plugins/transactional/mongoose-adapter) 📖 |
17 changes: 17 additions & 0 deletions
17
packages/transactional-adapters/transactional-adapter-mongoose/jest.config.js
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
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: { | ||
'^.+\\.ts$': [ | ||
'ts-jest', | ||
{ | ||
isolatedModules: true, | ||
maxWorkers: 1, | ||
}, | ||
], | ||
}, | ||
collectCoverageFrom: ['src/**/*.ts'], | ||
coverageDirectory: '../coverage', | ||
testEnvironment: 'node', | ||
}; |
73 changes: 73 additions & 0 deletions
73
packages/transactional-adapters/transactional-adapter-mongoose/package.json
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,73 @@ | ||
{ | ||
"name": "@nestjs-cls/transactional-adapter-mongoose", | ||
"version": "1.0.0", | ||
"description": "A mongoose adapter for @nestjs-cls/transactional", | ||
"author": "papooch", | ||
"license": "MIT", | ||
"engines": { | ||
"node": ">=18" | ||
}, | ||
"publishConfig": { | ||
"access": "public" | ||
}, | ||
"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", | ||
"transaction", | ||
"transactional", | ||
"transactional decorator", | ||
"aop", | ||
"mongoose" | ||
], | ||
"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", | ||
"prebuild": "rimraf dist", | ||
"build": "tsc", | ||
"test": "jest", | ||
"test:watch": "jest --watch", | ||
"test:cov": "jest --coverage" | ||
}, | ||
"peerDependencies": { | ||
"@nestjs-cls/transactional": "workspace:^2.2.2", | ||
"mongoose": "> 8", | ||
"nestjs-cls": "workspace:^4.3.0" | ||
}, | ||
"devDependencies": { | ||
"@nestjs/cli": "^10.0.2", | ||
"@nestjs/common": "^10.3.7", | ||
"@nestjs/core": "^10.3.7", | ||
"@nestjs/testing": "^10.3.7", | ||
"@types/jest": "^28.1.2", | ||
"@types/node": "^18.0.0", | ||
"jest": "^29.7.0", | ||
"mongodb-memory-server": "^9.4.0", | ||
"mongoose": "^8.4.4", | ||
"reflect-metadata": "^0.1.13", | ||
"rimraf": "^3.0.2", | ||
"rxjs": "^7.5.5", | ||
"sqlite3": "^5.1.7", | ||
"ts-jest": "^29.1.2", | ||
"ts-loader": "^9.3.0", | ||
"ts-node": "^10.8.1", | ||
"tsconfig-paths": "^4.0.0", | ||
"typescript": "5.0" | ||
} | ||
} |
1 change: 1 addition & 0 deletions
1
packages/transactional-adapters/transactional-adapter-mongoose/src/index.ts
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
export * from './lib/transactional-adapter-mongoose'; |
67 changes: 67 additions & 0 deletions
67
...ctional-adapters/transactional-adapter-mongoose/src/lib/transactional-adapter-mongoose.ts
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,67 @@ | ||
import { TransactionalAdapter } from '@nestjs-cls/transactional'; | ||
import mongoose, { ClientSession, Connection } from 'mongoose'; | ||
|
||
type MongooseTransactionOptions = Parameters<Connection['transaction']>[1]; | ||
|
||
export interface MongoDBTransactionalAdapterOptions { | ||
/** | ||
* The injection token for the mongoose Connection instance. | ||
*/ | ||
mongooseConnectionToken: any; | ||
|
||
/** | ||
* Default options for the transaction. These will be merged with any transaction-specific options | ||
* passed to the `@Transactional` decorator or the `TransactionHost#withTransaction` method. | ||
*/ | ||
defaultTxOptions?: Partial<MongooseTransactionOptions>; | ||
|
||
/** | ||
* Only supported for `mongoose >= 8.4` | ||
* | ||
* Whether to automatically enable the | ||
* [native AsyncLocalStorage integration](https://mongoosejs.com/docs/transactions.html#asynclocalstorage) | ||
* for transactions. This will set the `transactionAsyncLocalStorage` option to `true` in Mongoose. | ||
* | ||
* If enabled, there is no need to pass the session (`tx`) of `TransactionHost` to queries. | ||
* All queries executed within a `TransactionalHost#withTransaction` or the `@Transactional` decorator | ||
* will be executed within the same transaction. | ||
*/ | ||
enableNativeAsyncLocalStorage?: boolean; | ||
} | ||
|
||
export class TransactionalAdapterMongoose | ||
implements | ||
TransactionalAdapter< | ||
Connection, | ||
ClientSession | null, | ||
MongooseTransactionOptions | ||
> | ||
{ | ||
connectionToken: any; | ||
|
||
defaultTxOptions?: Partial<MongooseTransactionOptions>; | ||
|
||
constructor(options: MongoDBTransactionalAdapterOptions) { | ||
this.connectionToken = options.mongooseConnectionToken; | ||
this.defaultTxOptions = options.defaultTxOptions; | ||
if (options.enableNativeAsyncLocalStorage) { | ||
mongoose.set('transactionAsyncLocalStorage', true); | ||
} | ||
} | ||
|
||
optionsFactory(connection: Connection) { | ||
return { | ||
wrapWithTransaction: async ( | ||
options: MongooseTransactionOptions, | ||
fn: (...args: any[]) => Promise<any>, | ||
setTx: (tx?: ClientSession) => void, | ||
) => { | ||
return connection.transaction((session) => { | ||
setTx(session); | ||
return fn(); | ||
}, options); | ||
}, | ||
getFallbackInstance: () => null, | ||
}; | ||
} | ||
} |
216 changes: 216 additions & 0 deletions
216
...ional-adapters/transactional-adapter-mongoose/test/transactional-adapter-mongoose.spec.ts
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,216 @@ | ||
/* eslint-disable @typescript-eslint/no-non-null-assertion */ | ||
import { | ||
ClsPluginTransactional, | ||
Transactional, | ||
TransactionHost, | ||
} from '@nestjs-cls/transactional'; | ||
import { Injectable, Module } from '@nestjs/common'; | ||
import { Test, TestingModule } from '@nestjs/testing'; | ||
import { ObjectId, WriteConcern } from 'mongodb'; | ||
import { MongoMemoryReplSet } from 'mongodb-memory-server'; | ||
import mongoose, { Connection, Schema } from 'mongoose'; | ||
import { ClsModule, UseCls } from 'nestjs-cls'; | ||
import { TransactionalAdapterMongoose } from '../src'; | ||
|
||
const MONGOOSE_CONNECTION = 'MONGOOSE_CONNECTION'; | ||
|
||
const userSchema = new Schema({ | ||
name: String, | ||
email: String, | ||
}); | ||
|
||
const User = mongoose.model('user', userSchema); | ||
|
||
@Injectable() | ||
class UserRepository { | ||
constructor( | ||
private readonly txHost: TransactionHost<TransactionalAdapterMongoose>, | ||
) {} | ||
|
||
async getUserById(id: ObjectId) { | ||
const user = await User.findById(id).session(this.txHost.tx).lean(); | ||
return user; | ||
} | ||
|
||
async createUser(name: string) { | ||
const user = new User({ name: name, email: `${name}@email.com` }); | ||
await user.save({ session: this.txHost.tx }); | ||
return user.toObject(); | ||
} | ||
} | ||
|
||
@Injectable() | ||
class UserService { | ||
constructor( | ||
private readonly userRepository: UserRepository, | ||
private readonly txHost: TransactionHost<TransactionalAdapterMongoose>, | ||
) {} | ||
|
||
@UseCls() | ||
async withoutTransaction() { | ||
const r1 = await this.userRepository.createUser('Jim'); | ||
const r2 = await this.userRepository.getUserById(r1!._id); | ||
return { r1, r2 }; | ||
} | ||
|
||
@Transactional() | ||
async transactionWithDecorator() { | ||
const r1 = await this.userRepository.createUser('John'); | ||
const r2 = await this.userRepository.getUserById(r1!._id); | ||
|
||
return { r1, r2 }; | ||
} | ||
|
||
@Transactional<TransactionalAdapterMongoose>({ | ||
writeConcern: new WriteConcern('majority'), | ||
}) | ||
async transactionWithDecoratorWithOptions() { | ||
const r1 = await this.userRepository.createUser('James'); | ||
const r2 = await User.findOne({ _id: r1._id }).lean(); | ||
const r3 = await this.userRepository.getUserById(r1!._id); | ||
return { r1, r2, r3 }; | ||
} | ||
|
||
async transactionWithFunctionWrapper() { | ||
return this.txHost.withTransaction( | ||
{ | ||
writeConcern: new WriteConcern('majority'), | ||
}, | ||
async () => { | ||
const r1 = await this.userRepository.createUser('Joe'); | ||
const r2 = await User.findOne({ _id: r1!._id }).lean(); | ||
const r3 = await this.userRepository.getUserById(r1!._id); | ||
return { r1, r2, r3 }; | ||
}, | ||
); | ||
} | ||
|
||
@Transactional() | ||
async transactionWithDecoratorError() { | ||
await this.userRepository.createUser('Nobody'); | ||
throw new Error('Rollback'); | ||
} | ||
} | ||
|
||
const replSet = new MongoMemoryReplSet({ | ||
replSet: { count: 2, dbName: 'default' }, | ||
}); | ||
|
||
@Module({ | ||
providers: [ | ||
{ | ||
provide: MONGOOSE_CONNECTION, | ||
useFactory: async () => { | ||
const mongo = await mongoose.connect(replSet.getUri()); | ||
return mongo.connection; | ||
}, | ||
}, | ||
], | ||
exports: [MONGOOSE_CONNECTION], | ||
}) | ||
class MongooseModule {} | ||
|
||
@Module({ | ||
imports: [ | ||
MongooseModule, | ||
ClsModule.forRoot({ | ||
plugins: [ | ||
new ClsPluginTransactional({ | ||
imports: [MongooseModule], | ||
adapter: new TransactionalAdapterMongoose({ | ||
mongooseConnectionToken: MONGOOSE_CONNECTION, | ||
}), | ||
enableTransactionProxy: true, | ||
}), | ||
], | ||
}), | ||
], | ||
providers: [UserService, UserRepository], | ||
}) | ||
class AppModule {} | ||
|
||
describe('Transactional', () => { | ||
let mongo: Connection; | ||
let module: TestingModule; | ||
let callingService: UserService; | ||
|
||
beforeAll(async () => { | ||
await replSet.start(); | ||
}); | ||
|
||
beforeEach(async () => { | ||
module = await Test.createTestingModule({ | ||
imports: [AppModule], | ||
}).compile(); | ||
await module.init(); | ||
callingService = module.get(UserService); | ||
mongo = module.get(MONGOOSE_CONNECTION); | ||
}); | ||
|
||
afterEach(async () => { | ||
await User.deleteMany(); | ||
}); | ||
|
||
afterAll(async () => { | ||
await mongo.destroy(); | ||
await replSet.stop({ force: true }); | ||
}); | ||
|
||
describe('TransactionalAdapterKnex', () => { | ||
it('should work without an active transaction', async () => { | ||
const { r1, r2 } = await callingService.withoutTransaction(); | ||
expect(r1).toEqual(r2); | ||
const users = await User.find().lean(); | ||
expect(users).toEqual(expect.arrayContaining([r1])); | ||
}); | ||
|
||
it('should run a transaction with the default options with a decorator', async () => { | ||
const { r1, r2 } = await callingService.transactionWithDecorator(); | ||
expect(r1).toEqual(r2); | ||
const users = await User.find().lean(); | ||
expect(users).toEqual(expect.arrayContaining([r1])); | ||
}); | ||
|
||
it('should run a transaction with the specified options with a decorator', async () => { | ||
const { r1, r2, r3 } = | ||
await callingService.transactionWithDecoratorWithOptions(); | ||
expect(r1).toEqual(r3); | ||
expect(r2).toBeNull(); | ||
const users = await User.find().lean(); | ||
expect(users).toEqual(expect.arrayContaining([r1])); | ||
}); | ||
it('should run a transaction with the specified options with a function wrapper', async () => { | ||
const { r1, r2, r3 } = | ||
await callingService.transactionWithFunctionWrapper(); | ||
expect(r1).toEqual(r3); | ||
expect(r2).toBeNull(); | ||
const users = await User.find().lean(); | ||
expect(users).toEqual(expect.arrayContaining([r1])); | ||
}); | ||
|
||
it('should rollback a transaction on error', async () => { | ||
await expect( | ||
callingService.transactionWithDecoratorError(), | ||
).rejects.toThrow(new Error('Rollback')); | ||
const users = await User.find().lean(); | ||
expect(users).toEqual( | ||
expect.not.arrayContaining([{ name: 'Nobody' }]), | ||
); | ||
}); | ||
}); | ||
}); | ||
|
||
describe('Default options', () => { | ||
it('Should correctly set default options on the adapter instance', async () => { | ||
const adapter = new TransactionalAdapterMongoose({ | ||
mongooseConnectionToken: MONGOOSE_CONNECTION, | ||
defaultTxOptions: { | ||
readConcern: { level: 'snapshot' }, | ||
}, | ||
}); | ||
|
||
expect(adapter.defaultTxOptions).toEqual({ | ||
readConcern: { level: 'snapshot' }, | ||
}); | ||
}); | ||
}); |
8 changes: 8 additions & 0 deletions
8
packages/transactional-adapters/transactional-adapter-mongoose/tsconfig.json
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
{ | ||
"extends": "../../../tsconfig.json", | ||
"compilerOptions": { | ||
"outDir": "dist", | ||
"rootDir": "." | ||
}, | ||
"include": ["src/**/*.ts", "test/**/*.ts"] | ||
} |
Oops, something went wrong.