@expo/entity-database-adapter-knex 0.42.0 → 0.44.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@expo/entity-database-adapter-knex",
3
- "version": "0.42.0",
3
+ "version": "0.44.0",
4
4
  "description": "Knex database adapter for @expo/entity",
5
5
  "files": [
6
6
  "build",
@@ -28,24 +28,24 @@
28
28
  "author": "Expo",
29
29
  "license": "MIT",
30
30
  "dependencies": {
31
- "@expo/entity": "^0.42.0",
31
+ "@expo/entity": "^0.44.0",
32
32
  "knex": "^3.1.0"
33
33
  },
34
34
  "devDependencies": {
35
- "@expo/entity-testing-utils": "^0.42.0",
35
+ "@expo/entity-testing-utils": "^0.44.0",
36
36
  "@types/jest": "^29.5.14",
37
37
  "@types/node": "^20.14.1",
38
38
  "ctix": "^2.7.0",
39
- "eslint": "^8.57.1",
40
- "eslint-config-universe": "^14.0.0",
41
- "eslint-plugin-tsdoc": "^0.3.0",
39
+ "eslint": "^9.26.0",
40
+ "eslint-config-universe": "^15.0.3",
41
+ "eslint-plugin-tsdoc": "^0.4.0",
42
42
  "jest": "^29.7.0",
43
43
  "pg": "8.14.1",
44
- "prettier": "^3.3.3",
44
+ "prettier": "^3.5.3",
45
45
  "prettier-plugin-organize-imports": "^4.1.0",
46
- "ts-jest": "^29.3.1",
46
+ "ts-jest": "^29.3.2",
47
47
  "ts-mockito": "^2.6.1",
48
48
  "typescript": "^5.8.3"
49
49
  },
50
- "gitHead": "8414d96d948882735687da146e84913397cd8368"
50
+ "gitHead": "b9b09933e5a5b02037aa4688c5969705a29995c0"
51
51
  }
@@ -1,4 +1,4 @@
1
- import { ViewerContext } from '@expo/entity';
1
+ import { TransactionalDataLoaderMode, ViewerContext } from '@expo/entity';
2
2
  import { knex, Knex } from 'knex';
3
3
  import nullthrows from 'nullthrows';
4
4
 
@@ -47,7 +47,7 @@ describe(PostgresEntityQueryContextProvider, () => {
47
47
  .updateAsync();
48
48
 
49
49
  // ensure the outer transaction is not aborted due to postgres error in inner transaction,
50
- // in this case the error triggered is a unique constraint violation
50
+ // in this case the error triggered is a unique constraint violation from a conflict with the first entity created above
51
51
  try {
52
52
  await queryContext.runInNestedTransactionAsync(async (innerQueryContext) => {
53
53
  const entity = await PostgresUniqueTestEntity.loader(
@@ -70,6 +70,360 @@ describe(PostgresEntityQueryContextProvider, () => {
70
70
  expect(entityLoaded.getField('name')).toEqual('wat3');
71
71
  });
72
72
 
73
+ test('dataloader consistency with nested transactions', async () => {
74
+ const vc1 = new ViewerContext(createKnexIntegrationTestEntityCompanionProvider(knexInstance));
75
+
76
+ // put it in local dataloader
77
+ const entity = await PostgresUniqueTestEntity.creator(vc1)
78
+ .setField('name', 'who')
79
+ .createAsync();
80
+ const entityLoaded = await PostgresUniqueTestEntity.loader(vc1).loadByIDAsync(entity.getID());
81
+ expect(entityLoaded.getField('name')).toEqual('who');
82
+
83
+ await vc1.runInTransactionForDatabaseAdaptorFlavorAsync('postgres', async (queryContext) => {
84
+ const entityLoadedOuter = await PostgresUniqueTestEntity.loader(vc1).loadByIDAsync(
85
+ entity.getID(),
86
+ );
87
+ expect(entityLoadedOuter.getField('name')).toEqual('who');
88
+
89
+ await queryContext.runInNestedTransactionAsync(async (innerQueryContext) => {
90
+ const entityLoadedInner = await PostgresUniqueTestEntity.loader(
91
+ vc1,
92
+ innerQueryContext,
93
+ ).loadByIDAsync(entity.getID());
94
+ const updatedEntity = await PostgresUniqueTestEntity.updater(
95
+ entityLoadedInner,
96
+ innerQueryContext,
97
+ )
98
+ .setField('name', 'wat')
99
+ .updateAsync();
100
+ expect(updatedEntity.getField('name')).toEqual('wat');
101
+ });
102
+
103
+ const entityLoadedAfterNested = await PostgresUniqueTestEntity.loader(
104
+ vc1,
105
+ queryContext,
106
+ ).loadByIDAsync(entity.getID());
107
+ expect(entityLoadedAfterNested.getField('name')).toEqual('wat');
108
+ });
109
+
110
+ const entityLoadedAfterTransaction = await PostgresUniqueTestEntity.loader(vc1).loadByIDAsync(
111
+ entity.getID(),
112
+ );
113
+ expect(entityLoadedAfterTransaction.getField('name')).toEqual('wat');
114
+ });
115
+
116
+ test('dataloader consistency with nested transactions that throw', async () => {
117
+ const vc1 = new ViewerContext(createKnexIntegrationTestEntityCompanionProvider(knexInstance));
118
+
119
+ // put it in local dataloader
120
+ const entity = await PostgresUniqueTestEntity.creator(vc1)
121
+ .setField('name', 'who')
122
+ .createAsync();
123
+ const entityLoaded = await PostgresUniqueTestEntity.loader(vc1).loadByIDAsync(entity.getID());
124
+ expect(entityLoaded.getField('name')).toEqual('who');
125
+
126
+ await vc1.runInTransactionForDatabaseAdaptorFlavorAsync('postgres', async (queryContext) => {
127
+ const entityLoadedOuter = await PostgresUniqueTestEntity.loader(vc1).loadByIDAsync(
128
+ entity.getID(),
129
+ );
130
+ expect(entityLoadedOuter.getField('name')).toEqual('who');
131
+
132
+ try {
133
+ await queryContext.runInNestedTransactionAsync(async (innerQueryContext) => {
134
+ const entityLoadedInner = await PostgresUniqueTestEntity.loader(
135
+ vc1,
136
+ innerQueryContext,
137
+ ).loadByIDAsync(entity.getID());
138
+ const updatedEntity = await PostgresUniqueTestEntity.updater(
139
+ entityLoadedInner,
140
+ innerQueryContext,
141
+ )
142
+ .setField('name', 'wat')
143
+ .updateAsync();
144
+ expect(updatedEntity.getField('name')).toEqual('wat');
145
+ throw new Error('wat');
146
+ });
147
+ } catch {}
148
+
149
+ const entityLoadedAfterNested = await PostgresUniqueTestEntity.loader(
150
+ vc1,
151
+ queryContext,
152
+ ).loadByIDAsync(entity.getID());
153
+ expect(entityLoadedAfterNested.getField('name')).toEqual('who');
154
+ });
155
+
156
+ const entityLoadedAfterTransaction = await PostgresUniqueTestEntity.loader(vc1).loadByIDAsync(
157
+ entity.getID(),
158
+ );
159
+ expect(entityLoadedAfterTransaction.getField('name')).toEqual('who');
160
+ });
161
+
162
+ test('dataloader consistency with concurrent loads outside of transaction', async () => {
163
+ const vc1 = new ViewerContext(createKnexIntegrationTestEntityCompanionProvider(knexInstance));
164
+
165
+ // put it in local dataloader
166
+ const entity = await PostgresUniqueTestEntity.creator(vc1)
167
+ .setField('name', 'who')
168
+ .createAsync();
169
+ const entityLoaded = await PostgresUniqueTestEntity.loader(vc1).loadByIDAsync(entity.getID());
170
+ expect(entityLoaded.getField('name')).toEqual('who');
171
+
172
+ let openBarrier1: () => void;
173
+ const barrier1 = new Promise<void>((resolve) => {
174
+ openBarrier1 = resolve;
175
+ });
176
+
177
+ let openBarrier2: () => void;
178
+ const barrier2 = new Promise<void>((resolve) => {
179
+ openBarrier2 = resolve;
180
+ });
181
+
182
+ await Promise.all([
183
+ vc1.runInTransactionForDatabaseAdaptorFlavorAsync('postgres', async (queryContext) => {
184
+ const entityLoadedOuter = await PostgresUniqueTestEntity.loader(vc1).loadByIDAsync(
185
+ entity.getID(),
186
+ );
187
+ expect(entityLoadedOuter.getField('name')).toEqual('who');
188
+
189
+ await queryContext.runInNestedTransactionAsync(async (innerQueryContext) => {
190
+ const entityLoadedInner = await PostgresUniqueTestEntity.loader(
191
+ vc1,
192
+ innerQueryContext,
193
+ ).loadByIDAsync(entity.getID());
194
+ const updatedEntity = await PostgresUniqueTestEntity.updater(
195
+ entityLoadedInner,
196
+ innerQueryContext,
197
+ )
198
+ .setField('name', 'wat')
199
+ .updateAsync();
200
+ expect(updatedEntity.getField('name')).toEqual('wat');
201
+ const entityLoadedAfterUpdate = await PostgresUniqueTestEntity.loader(
202
+ vc1,
203
+ innerQueryContext,
204
+ ).loadByIDAsync(entity.getID());
205
+ expect(entityLoadedAfterUpdate.getField('name')).toEqual('wat');
206
+ });
207
+ openBarrier1();
208
+ await barrier2;
209
+ }),
210
+ (async () => {
211
+ await barrier1;
212
+
213
+ const entityLoadedOutsideOfTransactionsBeforeNestedCommit =
214
+ await PostgresUniqueTestEntity.loader(vc1).loadByIDAsync(entity.getID());
215
+ expect(entityLoadedOutsideOfTransactionsBeforeNestedCommit.getField('name')).toEqual('who');
216
+ openBarrier2!();
217
+ })(),
218
+ ]);
219
+
220
+ const entityLoadedAfterTransaction = await PostgresUniqueTestEntity.loader(vc1).loadByIDAsync(
221
+ entity.getID(),
222
+ );
223
+ expect(entityLoadedAfterTransaction.getField('name')).toEqual('wat');
224
+ });
225
+
226
+ test('consistent behavior with and without transactional dataloader for concurrent loads outside of nested transaction', async () => {
227
+ // Subtransactions are not supported in postgres. See #194 for more info.
228
+ // Instead, savepoints and rollbacks are used to simulate subtransactions, which results in non-isolated read semantics.
229
+ //
230
+ // This test tests the same behavior exists whether there is a dataloader or not in transactions, thus indicating that
231
+ // the dataloader invalidation is not the issue.
232
+
233
+ const runTest = async (
234
+ transactionalDataLoaderMode: TransactionalDataLoaderMode,
235
+ ): Promise<void> => {
236
+ const vc1 = new ViewerContext(createKnexIntegrationTestEntityCompanionProvider(knexInstance));
237
+ await vc1.runInTransactionForDatabaseAdaptorFlavorAsync(
238
+ 'postgres',
239
+ async (outerQueryContext) => {
240
+ // put it in local dataloader
241
+ const entity = await PostgresUniqueTestEntity.creator(vc1, outerQueryContext)
242
+ .setField('name', 'who')
243
+ .createAsync();
244
+ const entityLoaded = await PostgresUniqueTestEntity.loader(
245
+ vc1,
246
+ outerQueryContext,
247
+ ).loadByIDAsync(entity.getID());
248
+ if (entityLoaded.getField('name') !== 'who') {
249
+ throw new Error('entity loaded wrong value');
250
+ }
251
+
252
+ let openBarrier1: () => void;
253
+ const barrier1 = new Promise<void>((resolve) => {
254
+ openBarrier1 = resolve;
255
+ });
256
+
257
+ let openBarrier2: () => void;
258
+ const barrier2 = new Promise<void>((resolve) => {
259
+ openBarrier2 = resolve;
260
+ });
261
+
262
+ await Promise.all([
263
+ outerQueryContext.runInNestedTransactionAsync(async (innerQueryContext) => {
264
+ const entityLoadedInner = await PostgresUniqueTestEntity.loader(
265
+ vc1,
266
+ innerQueryContext,
267
+ ).loadByIDAsync(entity.getID());
268
+ const updatedEntity = await PostgresUniqueTestEntity.updater(
269
+ entityLoadedInner,
270
+ innerQueryContext,
271
+ )
272
+ .setField('name', 'wat')
273
+ .updateAsync();
274
+ if (updatedEntity.getField('name') !== 'wat') {
275
+ throw new Error('entity updated wrong value');
276
+ }
277
+
278
+ const entityLoadedAfterUpdate = await PostgresUniqueTestEntity.loader(
279
+ vc1,
280
+ innerQueryContext,
281
+ ).loadByIDAsync(entity.getID());
282
+ if (entityLoadedAfterUpdate.getField('name') !== 'wat') {
283
+ throw new Error('entity loaded wrong value after update');
284
+ }
285
+ openBarrier1();
286
+ await barrier2;
287
+ }),
288
+ (async () => {
289
+ await barrier1;
290
+
291
+ // if postgres supported nested transactions, this would read isolated from the nested transaction above
292
+ // but since it doesn't, this will read the updated value.
293
+ const entityLoadedInOuterTransactionBeforeNestedCommit =
294
+ await PostgresUniqueTestEntity.loader(vc1, outerQueryContext).loadByIDAsync(
295
+ entity.getID(),
296
+ );
297
+ if (entityLoadedInOuterTransactionBeforeNestedCommit.getField('name') !== 'who') {
298
+ throw new Error('outer transaction read wrong value');
299
+ }
300
+
301
+ openBarrier2!();
302
+ })(),
303
+ ]);
304
+
305
+ const entityLoadedAfterTransaction = await PostgresUniqueTestEntity.loader(
306
+ vc1,
307
+ outerQueryContext,
308
+ ).loadByIDAsync(entity.getID());
309
+ if (entityLoadedAfterTransaction.getField('name') !== 'wat') {
310
+ throw new Error('entity loaded wrong value after transaction');
311
+ }
312
+ },
313
+ { transactionalDataLoaderMode },
314
+ );
315
+ };
316
+
317
+ await expect(runTest(TransactionalDataLoaderMode.DISABLED)).rejects.toThrow(
318
+ 'outer transaction read wrong value',
319
+ );
320
+ await expect(runTest(TransactionalDataLoaderMode.ENABLED)).rejects.toThrow(
321
+ 'outer transaction read wrong value',
322
+ );
323
+ await expect(runTest(TransactionalDataLoaderMode.ENABLED_BATCH_ONLY)).rejects.toThrow(
324
+ 'outer transaction read wrong value',
325
+ );
326
+ });
327
+
328
+ test('consistent behavior with and without transactional dataloader for concurrent mutations outside of nested transaction that reads', async () => {
329
+ // this test has a similar issue to the one above: absence of real nested transactions in postgres
330
+ // this should have the same behavior no matter if there is a dataloader or not in transactions
331
+
332
+ const runTest = async (
333
+ transactionalDataLoaderMode: TransactionalDataLoaderMode,
334
+ ): Promise<void> => {
335
+ const vc1 = new ViewerContext(createKnexIntegrationTestEntityCompanionProvider(knexInstance));
336
+ await vc1.runInTransactionForDatabaseAdaptorFlavorAsync(
337
+ 'postgres',
338
+ async (outerQueryContext) => {
339
+ // put it in local dataloader
340
+ const entity = await PostgresUniqueTestEntity.creator(vc1, outerQueryContext)
341
+ .setField('name', 'who')
342
+ .createAsync();
343
+ const entityLoaded = await PostgresUniqueTestEntity.loader(
344
+ vc1,
345
+ outerQueryContext,
346
+ ).loadByIDAsync(entity.getID());
347
+ if (entityLoaded.getField('name') !== 'who') {
348
+ throw new Error('entity loaded wrong value');
349
+ }
350
+
351
+ let openBarrier1: () => void;
352
+ const barrier1 = new Promise<void>((resolve) => {
353
+ openBarrier1 = resolve;
354
+ });
355
+
356
+ let openBarrier2: () => void;
357
+ const barrier2 = new Promise<void>((resolve) => {
358
+ openBarrier2 = resolve;
359
+ });
360
+
361
+ await Promise.all([
362
+ (async () => {
363
+ await barrier1;
364
+
365
+ const entityLoadedOuterAgain = await PostgresUniqueTestEntity.loader(
366
+ vc1,
367
+ outerQueryContext,
368
+ ).loadByIDAsync(entity.getID());
369
+ const updatedEntity = await PostgresUniqueTestEntity.updater(
370
+ entityLoadedOuterAgain,
371
+ outerQueryContext,
372
+ )
373
+ .setField('name', 'wat')
374
+ .updateAsync();
375
+ if (updatedEntity.getField('name') !== 'wat') {
376
+ throw new Error('entity updated wrong value');
377
+ }
378
+
379
+ openBarrier2!();
380
+ })(),
381
+
382
+ outerQueryContext.runInNestedTransactionAsync(async (innerQueryContext) => {
383
+ const entityLoadedInner = await PostgresUniqueTestEntity.loader(
384
+ vc1,
385
+ innerQueryContext,
386
+ ).loadByIDAsync(entity.getID());
387
+ if (entityLoadedInner.getField('name') !== 'who') {
388
+ throw new Error('entity loaded inner wrong value 1');
389
+ }
390
+
391
+ openBarrier1();
392
+ await barrier2;
393
+
394
+ const entityLoadedInnerAgain = await PostgresUniqueTestEntity.loader(
395
+ vc1,
396
+ innerQueryContext,
397
+ ).loadByIDAsync(entity.getID());
398
+ if (entityLoadedInnerAgain.getField('name') !== 'who') {
399
+ throw new Error('entity loaded inner wrong value 2');
400
+ }
401
+ }),
402
+ ]);
403
+
404
+ const entityLoadedAfterTransaction = await PostgresUniqueTestEntity.loader(
405
+ vc1,
406
+ outerQueryContext,
407
+ ).loadByIDAsync(entity.getID());
408
+ if (entityLoadedAfterTransaction.getField('name') !== 'wat') {
409
+ throw new Error('entity loaded wrong value after transaction');
410
+ }
411
+ },
412
+ { transactionalDataLoaderMode },
413
+ );
414
+ };
415
+
416
+ await expect(runTest(TransactionalDataLoaderMode.DISABLED)).rejects.toThrow(
417
+ 'entity loaded inner wrong value 2',
418
+ );
419
+ await expect(runTest(TransactionalDataLoaderMode.ENABLED)).rejects.toThrow(
420
+ 'entity loaded inner wrong value 2',
421
+ );
422
+ await expect(runTest(TransactionalDataLoaderMode.ENABLED_BATCH_ONLY)).rejects.toThrow(
423
+ 'entity loaded inner wrong value 2',
424
+ );
425
+ });
426
+
73
427
  it('supports multi-nested transactions', async () => {
74
428
  const vc1 = new ViewerContext(createKnexIntegrationTestEntityCompanionProvider(knexInstance));
75
429