Skip to content

Commit 6af95e3

Browse files
laura-bergoensfrinyvonnickxav-car
authored
fix(api): nested DomainTransaction calls should use the same transaction
Co-authored-by: Yvonnick Frin <yvonnick.frin@pix.fr> Co-authored-by: Xavier Carron <xavier.carron@pix.fr>
1 parent b526f48 commit 6af95e3

File tree

2 files changed

+294
-0
lines changed

2 files changed

+294
-0
lines changed

Diff for: api/src/shared/domain/DomainTransaction.js

+4
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,10 @@ class DomainTransaction {
1010
}
1111

1212
static execute(lambda, transactionConfig) {
13+
const existingConn = DomainTransaction.getConnection();
14+
if (existingConn.isTransaction) {
15+
return lambda();
16+
}
1317
return knex.transaction((trx) => {
1418
const domainTransaction = new DomainTransaction(trx);
1519
return asyncLocalStorage.run({ transaction: domainTransaction }, lambda, domainTransaction);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,290 @@
1+
import { DomainTransaction, withTransaction } from '../../../../src/shared/domain/DomainTransaction.js';
2+
import { catchErr, expect, knex } from '../../../../tests/test-helper.js';
3+
4+
describe('Shared | Integration | Domain | DomainTransaction', function () {
5+
context('behaviour when nesting', function () {
6+
context('withTransaction in withTransaction', function () {
7+
it('should use the same transaction all the way', async function () {
8+
// given
9+
let didIGoAllTheWayToTheEnd = false;
10+
const addTwoFeaturesInTwoNestedWithTransaction = withTransaction(async function () {
11+
const knexConnA = DomainTransaction.getConnection();
12+
13+
// check empty in scope A
14+
const keys0 = await knexConnA('features').pluck('key');
15+
expect(keys0, 'it starts with an empty table').to.deepEqualArray([]);
16+
17+
// insert in scope A
18+
await knexConnA('features').insert({ key: 'scopeA' });
19+
20+
// check has one in scope A
21+
const keys1 = await knexConnA('features').pluck('key');
22+
expect(keys1, '"scopeA" has been inserted in first layer').to.deepEqualArray(['scopeA']);
23+
24+
// nested scope
25+
await withTransaction(async function () {
26+
const knexConnB = DomainTransaction.getConnection();
27+
28+
// check already has one in scope B
29+
const keys1 = await knexConnB('features').pluck('key');
30+
expect(keys1, '"scopeA" found in second layer').to.deepEqualArray(['scopeA']);
31+
32+
// insert in scope B
33+
await knexConnB('features').insert({ key: 'scopeB' });
34+
35+
// check has two in scope B
36+
const keys2 = await knexConnB('features').pluck('key').orderBy('key');
37+
expect(keys2, '"scopeB" also inserted, but in second layer').to.deepEqualArray(['scopeA', 'scopeB']);
38+
didIGoAllTheWayToTheEnd = true;
39+
})();
40+
});
41+
42+
// when
43+
await addTwoFeaturesInTwoNestedWithTransaction();
44+
45+
// then
46+
expect(didIGoAllTheWayToTheEnd).to.be.true;
47+
const finalKeys = await knex('features').pluck('key').orderBy('key');
48+
expect(finalKeys).to.deepEqualArray(['scopeA', 'scopeB']);
49+
});
50+
51+
it('should rollback everything when something goes wrong in the nested scope', async function () {
52+
// given
53+
const addTwoFeaturesInTwoNestedWithTransaction = withTransaction(async function () {
54+
const knexConnA = DomainTransaction.getConnection();
55+
56+
await knexConnA('features').insert({ key: 'scopeA' });
57+
58+
await withTransaction(async function () {
59+
const knexConnB = DomainTransaction.getConnection();
60+
61+
await knexConnB('features').insert({ key: 'scopeB' });
62+
63+
throw new Error("Let's rollback !");
64+
})();
65+
});
66+
67+
// when
68+
const err = await catchErr(addTwoFeaturesInTwoNestedWithTransaction)();
69+
70+
// then
71+
expect(err.message).to.equal("Let's rollback !");
72+
const { count } = await knex('features').count('id').first();
73+
expect(count).to.equal(0);
74+
});
75+
});
76+
77+
context('withTransaction in DomainTransaction.execute', function () {
78+
it('should use the same transaction all the way', async function () {
79+
// given
80+
let didIGoAllTheWayToTheEnd = false;
81+
const addTwoFeaturesInDomainTrExecuteAndWithTransaction = async function () {
82+
const knexConnA = DomainTransaction.getConnection();
83+
84+
// check empty in scope A
85+
const keys0 = await knexConnA('features').pluck('key');
86+
expect(keys0, 'it starts with an empty table').to.deepEqualArray([]);
87+
88+
// insert in scope A
89+
await knexConnA('features').insert({ key: 'scopeA' });
90+
91+
// check has one in scope A
92+
const keys1 = await knexConnA('features').pluck('key');
93+
expect(keys1, '"scopeA" has been inserted in first layer').to.deepEqualArray(['scopeA']);
94+
95+
// nested scope
96+
await withTransaction(async function () {
97+
const knexConnB = DomainTransaction.getConnection();
98+
99+
// check already has one in scope B
100+
const keys1 = await knexConnB('features').pluck('key');
101+
expect(keys1, '"scopeA" found in second layer').to.deepEqualArray(['scopeA']);
102+
103+
// insert in scope B
104+
await knexConnB('features').insert({ key: 'scopeB' });
105+
106+
// check has two in scope B
107+
const keys2 = await knexConnB('features').pluck('key').orderBy('key');
108+
expect(keys2, '"scopeB" also inserted, but in second layer').to.deepEqualArray(['scopeA', 'scopeB']);
109+
didIGoAllTheWayToTheEnd = true;
110+
})();
111+
};
112+
113+
// when
114+
await DomainTransaction.execute(addTwoFeaturesInDomainTrExecuteAndWithTransaction);
115+
116+
// then
117+
expect(didIGoAllTheWayToTheEnd).to.be.true;
118+
const finalKeys = await knex('features').pluck('key').orderBy('key');
119+
expect(finalKeys).to.deepEqualArray(['scopeA', 'scopeB']);
120+
});
121+
122+
it('should rollback everything when something goes wrong in the nested scope', async function () {
123+
// given
124+
const addTwoFeaturesInDomainTrExecuteAndWithTransaction = async function () {
125+
const knexConnA = DomainTransaction.getConnection();
126+
127+
await knexConnA('features').insert({ key: 'scopeA' });
128+
129+
await withTransaction(async function () {
130+
const knexConnB = DomainTransaction.getConnection();
131+
132+
await knexConnB('features').insert({ key: 'scopeB' });
133+
134+
throw new Error("Let's rollback !");
135+
})();
136+
};
137+
138+
// when
139+
const err = await catchErr(DomainTransaction.execute)(addTwoFeaturesInDomainTrExecuteAndWithTransaction);
140+
141+
// then
142+
expect(err.message).to.equal("Let's rollback !");
143+
const { count } = await knex('features').count('id').first();
144+
expect(count).to.equal(0);
145+
});
146+
});
147+
148+
context('DomainTransaction.execute in DomainTransaction.execute', function () {
149+
it('should use the same transaction all the way', async function () {
150+
// given
151+
let didIGoAllTheWayToTheEnd = false;
152+
const addTwoFeaturesInTwoDomainTrExecute = async function () {
153+
const knexConnA = DomainTransaction.getConnection();
154+
155+
// check empty in scope A
156+
const keys0 = await knexConnA('features').pluck('key');
157+
expect(keys0, 'it starts with an empty table').to.deepEqualArray([]);
158+
159+
// insert in scope A
160+
await knexConnA('features').insert({ key: 'scopeA' });
161+
162+
// check has one in scope A
163+
const keys1 = await knexConnA('features').pluck('key');
164+
expect(keys1, '"scopeA" has been inserted in first layer').to.deepEqualArray(['scopeA']);
165+
166+
// nested scope
167+
await DomainTransaction.execute(async function () {
168+
const knexConnB = DomainTransaction.getConnection();
169+
170+
// check already has one in scope B
171+
const keys1 = await knexConnB('features').pluck('key');
172+
expect(keys1, '"scopeA" found in second layer').to.deepEqualArray(['scopeA']);
173+
174+
// insert in scope B
175+
await knexConnB('features').insert({ key: 'scopeB' });
176+
177+
// check has two in scope B
178+
const keys2 = await knexConnB('features').pluck('key').orderBy('key');
179+
expect(keys2, '"scopeB" also inserted, but in second layer').to.deepEqualArray(['scopeA', 'scopeB']);
180+
didIGoAllTheWayToTheEnd = true;
181+
});
182+
};
183+
184+
// when
185+
await DomainTransaction.execute(addTwoFeaturesInTwoDomainTrExecute);
186+
187+
// then
188+
expect(didIGoAllTheWayToTheEnd).to.be.true;
189+
const finalKeys = await knex('features').pluck('key').orderBy('key');
190+
expect(finalKeys).to.deepEqualArray(['scopeA', 'scopeB']);
191+
});
192+
193+
it('should rollback everything when something goes wrong in the nested scope', async function () {
194+
// given
195+
const addTwoFeaturesInTwoDomainTrExecute = async function () {
196+
const knexConnA = DomainTransaction.getConnection();
197+
198+
await knexConnA('features').insert({ key: 'scopeA' });
199+
200+
await DomainTransaction.execute(async function () {
201+
const knexConnB = DomainTransaction.getConnection();
202+
203+
await knexConnB('features').insert({ key: 'scopeB' });
204+
205+
throw new Error("Let's rollback !");
206+
});
207+
};
208+
209+
// when
210+
const err = await catchErr(DomainTransaction.execute)(addTwoFeaturesInTwoDomainTrExecute);
211+
212+
// then
213+
expect(err.message).to.equal("Let's rollback !");
214+
const { count } = await knex('features').count('id').first();
215+
expect(count).to.equal(0);
216+
});
217+
});
218+
219+
context('DomainTransaction.execute in withTransaction', function () {
220+
it('should use the same transaction all the way', async function () {
221+
// given
222+
let didIGoAllTheWayToTheEnd = false;
223+
const addTwoFeaturesInWithTransactioAndDomainTrExecute = withTransaction(async function () {
224+
const knexConnA = DomainTransaction.getConnection();
225+
226+
// check empty in scope A
227+
const keys0 = await knexConnA('features').pluck('key');
228+
expect(keys0, 'it starts with an empty table').to.deepEqualArray([]);
229+
230+
// insert in scope A
231+
await knexConnA('features').insert({ key: 'scopeA' });
232+
233+
// check has one in scope A
234+
const keys1 = await knexConnA('features').pluck('key');
235+
expect(keys1, '"scopeA" has been inserted in first layer').to.deepEqualArray(['scopeA']);
236+
237+
// nested scope
238+
await DomainTransaction.execute(async function () {
239+
const knexConnB = DomainTransaction.getConnection();
240+
241+
// check already has one in scope B
242+
const keys1 = await knexConnB('features').pluck('key');
243+
expect(keys1, '"scopeA" found in second layer').to.deepEqualArray(['scopeA']);
244+
245+
// insert in scope B
246+
await knexConnB('features').insert({ key: 'scopeB' });
247+
248+
// check has two in scope B
249+
const keys2 = await knexConnB('features').pluck('key').orderBy('key');
250+
expect(keys2, '"scopeB" also inserted, but in second layer').to.deepEqualArray(['scopeA', 'scopeB']);
251+
didIGoAllTheWayToTheEnd = true;
252+
});
253+
});
254+
255+
// when
256+
await addTwoFeaturesInWithTransactioAndDomainTrExecute();
257+
258+
// then
259+
expect(didIGoAllTheWayToTheEnd).to.be.true;
260+
const finalKeys = await knex('features').pluck('key').orderBy('key');
261+
expect(finalKeys).to.deepEqualArray(['scopeA', 'scopeB']);
262+
});
263+
264+
it('should rollback everything when something goes wrong in the nested scope', async function () {
265+
// given
266+
const addTwoFeaturesInWithTransactioAndDomainTrExecute = withTransaction(async function () {
267+
const knexConnA = DomainTransaction.getConnection();
268+
269+
await knexConnA('features').insert({ key: 'scopeA' });
270+
271+
await DomainTransaction.execute(async function () {
272+
const knexConnB = DomainTransaction.getConnection();
273+
274+
await knexConnB('features').insert({ key: 'scopeB' });
275+
276+
throw new Error("Let's rollback !");
277+
});
278+
});
279+
280+
// when
281+
const err = await catchErr(addTwoFeaturesInWithTransactioAndDomainTrExecute)();
282+
283+
// then
284+
expect(err.message).to.equal("Let's rollback !");
285+
const { count } = await knex('features').count('id').first();
286+
expect(count).to.equal(0);
287+
});
288+
});
289+
});
290+
});

0 commit comments

Comments
 (0)