Skip to content

Commit 48e0e6e

Browse files
authored
fix(NODE-4108): improve return type for withTransaction() (#3236)
1 parent 922412f commit 48e0e6e

File tree

4 files changed

+111
-20
lines changed

4 files changed

+111
-20
lines changed

global.d.ts

+8
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,14 @@ declare global {
3636
}
3737

3838
namespace Mocha {
39+
interface SuiteFunction {
40+
(title: string, metadata: MongoDBMetadataUI, fn: (this: Suite) => void): Mocha.Suite;
41+
}
42+
43+
interface PendingSuiteFunction {
44+
(title: string, metadata: MongoDBMetadataUI, fn: (this: Suite) => void): Mocha.Suite;
45+
}
46+
3947
interface TestFunction {
4048
(title: string, metadata: MongoDBMetadataUI, fn: Mocha.Func): Mocha.Test;
4149
(title: string, metadata: MongoDBMetadataUI, fn: Mocha.AsyncFunc): Mocha.Test;

src/sessions.ts

+16-6
Original file line numberDiff line numberDiff line change
@@ -457,20 +457,30 @@ export class ClientSession extends TypedEventEmitter<ClientSessionEvents> {
457457
}
458458

459459
/**
460-
* Runs a provided lambda within a transaction, retrying either the commit operation
460+
* Runs a provided callback within a transaction, retrying either the commitTransaction operation
461461
* or entire transaction as needed (and when the error permits) to better ensure that
462462
* the transaction can complete successfully.
463463
*
464-
* IMPORTANT: This method requires the user to return a Promise, all lambdas that do not
465-
* return a Promise will result in undefined behavior.
464+
* **IMPORTANT:** This method requires the user to return a Promise, and `await` all operations.
465+
* Any callbacks that do not return a Promise will result in undefined behavior.
466466
*
467-
* @param fn - A lambda to run within a transaction
468-
* @param options - Optional settings for the transaction
467+
* @remarks
468+
* This function:
469+
* - Will return the command response from the final commitTransaction if every operation is successful (can be used as a truthy object)
470+
* - Will return `undefined` if the transaction is explicitly aborted with `await session.abortTransaction()`
471+
* - Will throw if one of the operations throws or `throw` statement is used inside the `withTransaction` callback
472+
*
473+
* Checkout a descriptive example here:
474+
* @see https://www.mongodb.com/developer/quickstart/node-transactions/
475+
*
476+
* @param fn - callback to run within a transaction
477+
* @param options - optional settings for the transaction
478+
* @returns A raw command response or undefined
469479
*/
470480
withTransaction<T = void>(
471481
fn: WithTransactionCallback<T>,
472482
options?: TransactionOptions
473-
): ReturnType<typeof fn> {
483+
): Promise<Document | undefined> {
474484
const startTime = now();
475485
return attemptTransaction(this, startTime, fn, options);
476486
}

src/transactions.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ export interface TransactionOptions extends CommandOperationOptions {
6868
writeConcern?: WriteConcern;
6969
/** A default read preference for commands in this transaction */
7070
readPreference?: ReadPreference;
71-
71+
/** Specifies the maximum amount of time to allow a commit action on a transaction to run in milliseconds */
7272
maxCommitTimeMS?: number;
7373
}
7474

test/integration/transactions/transactions.test.js renamed to test/integration/transactions/transactions.test.ts

+86-13
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,25 @@
1-
'use strict';
1+
import { expect } from 'chai';
22

3-
const { expect } = require('chai');
4-
const { Topology } = require('../../../src/sdam/topology');
5-
const { ClientSession } = require('../../../src/sessions');
6-
const { MongoNetworkError } = require('../../../src/error');
3+
import { Collection, MongoClient, ServerSessionPool } from '../../../src';
4+
import { MongoNetworkError } from '../../../src/error';
5+
import { ClientSession } from '../../../src/sessions';
76

87
describe('Transactions', function () {
98
describe('withTransaction', function () {
10-
let session, sessionPool;
11-
beforeEach(() => {
12-
const topology = new Topology('localhost:27017');
9+
let session: ClientSession;
10+
let sessionPool: ServerSessionPool;
11+
let client: MongoClient;
12+
13+
beforeEach(async function () {
14+
client = this.configuration.newClient();
15+
const topology = (await client.connect()).topology;
1316
sessionPool = topology.s.sessionPool;
14-
session = new ClientSession(topology, sessionPool);
17+
session = new ClientSession(topology, sessionPool, {});
1518
});
1619

17-
afterEach(() => {
20+
afterEach(async () => {
1821
sessionPool.endAllPooledSessions();
22+
await client.close();
1923
});
2024

2125
it('should provide a useful error if a Promise is not returned', {
@@ -27,6 +31,7 @@ describe('Transactions', function () {
2731
return false;
2832
}
2933

34+
// @ts-expect-error: Testing that a non promise returning function is handled correctly
3035
expect(() => session.withTransaction(fnThatDoesntReturnPromise)).to.throw(
3136
/must return a Promise/
3237
);
@@ -37,8 +42,7 @@ describe('Transactions', function () {
3742

3843
it('should return readable error if promise rejected with no reason', {
3944
metadata: {
40-
requires: { topology: ['replicaset', 'sharded'], mongodb: '>=4.0.2' },
41-
serverless: 'forbid'
45+
requires: { topology: ['replicaset', 'sharded'], mongodb: '>=4.2.0', serverless: 'forbid' }
4246
},
4347
test: function (done) {
4448
function fnThatReturnsBadPromise() {
@@ -54,6 +58,73 @@ describe('Transactions', function () {
5458
});
5559
}
5660
});
61+
62+
describe(
63+
'return value semantics',
64+
{ requires: { mongodb: '>=4.2.0', topology: '!single' } },
65+
() => {
66+
let client: MongoClient;
67+
let collection: Collection<{ a: number }>;
68+
69+
beforeEach(async function () {
70+
client = this.configuration.newClient();
71+
await client.connect();
72+
collection = await client
73+
.db('withTransactionReturnType')
74+
.createCollection('withTransactionReturnType');
75+
});
76+
77+
afterEach(async function () {
78+
await collection.drop();
79+
await client.close();
80+
});
81+
82+
it('should return undefined when transaction is aborted explicitly', async () => {
83+
const session = client.startSession();
84+
85+
const withTransactionResult = await session
86+
.withTransaction(async session => {
87+
await collection.insertOne({ a: 1 }, { session });
88+
await collection.findOne({ a: 1 }, { session });
89+
await session.abortTransaction();
90+
})
91+
.finally(async () => await session.endSession());
92+
93+
expect(withTransactionResult).to.be.undefined;
94+
});
95+
96+
it('should return raw command when transaction is successfully committed', async () => {
97+
const session = client.startSession();
98+
99+
const withTransactionResult = await session
100+
.withTransaction(async session => {
101+
await collection.insertOne({ a: 1 }, { session });
102+
await collection.findOne({ a: 1 }, { session });
103+
})
104+
.finally(async () => await session.endSession());
105+
106+
expect(withTransactionResult).to.exist;
107+
expect(withTransactionResult).to.be.an('object');
108+
expect(withTransactionResult).to.have.property('ok', 1);
109+
});
110+
111+
it('should throw when transaction is aborted due to an error', async () => {
112+
const session = client.startSession();
113+
114+
const withTransactionResult = await session
115+
.withTransaction(async session => {
116+
await collection.insertOne({ a: 1 }, { session });
117+
await collection.findOne({ a: 1 }, { session });
118+
throw new Error("I don't wanna transact anymore!");
119+
})
120+
.catch(error => error)
121+
.finally(async () => await session.endSession());
122+
123+
expect(withTransactionResult).to.be.instanceOf(Error);
124+
expect(withTransactionResult.message).to.equal("I don't wanna transact anymore!");
125+
});
126+
}
127+
);
57128
});
58129

59130
describe('startTransaction', function () {
@@ -137,7 +208,9 @@ describe('Transactions', function () {
137208

138209
coll.insertOne({ b: 2 }, { session }, err => {
139210
expect(err).to.exist.and.to.be.an.instanceof(MongoNetworkError);
140-
expect(err.hasErrorLabel('TransientTransactionError')).to.be.true;
211+
if (err instanceof MongoNetworkError) {
212+
expect(err.hasErrorLabel('TransientTransactionError')).to.be.true;
213+
}
141214

142215
session.abortTransaction(() => session.endSession(() => client.close(done)));
143216
});

0 commit comments

Comments
 (0)