@expo/entity 0.43.0 → 0.45.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.
Files changed (80) hide show
  1. package/build/AuthorizationResultBasedEntityLoader.js +5 -1
  2. package/build/AuthorizationResultBasedEntityLoader.js.map +1 -1
  3. package/build/AuthorizationResultBasedEntityMutator.d.ts +87 -2
  4. package/build/AuthorizationResultBasedEntityMutator.js +122 -8
  5. package/build/AuthorizationResultBasedEntityMutator.js.map +1 -1
  6. package/build/EntityLoaderUtils.d.ts +15 -3
  7. package/build/EntityLoaderUtils.js +25 -10
  8. package/build/EntityLoaderUtils.js.map +1 -1
  9. package/build/EntityQueryContext.d.ts +53 -7
  10. package/build/EntityQueryContext.js +65 -10
  11. package/build/EntityQueryContext.js.map +1 -1
  12. package/build/EntityQueryContextProvider.d.ts +5 -1
  13. package/build/EntityQueryContextProvider.js +11 -4
  14. package/build/EntityQueryContextProvider.js.map +1 -1
  15. package/build/IEntityGenericCacher.d.ts +2 -2
  16. package/build/errors/EntityNotFoundError.d.ts +8 -1
  17. package/build/errors/EntityNotFoundError.js +7 -2
  18. package/build/errors/EntityNotFoundError.js.map +1 -1
  19. package/build/index.d.ts +1 -0
  20. package/build/index.js +1 -0
  21. package/build/index.js.map +1 -1
  22. package/build/internal/CompositeFieldHolder.d.ts +13 -0
  23. package/build/internal/CompositeFieldHolder.js +7 -0
  24. package/build/internal/CompositeFieldHolder.js.map +1 -1
  25. package/build/internal/CompositeFieldValueMap.d.ts +3 -0
  26. package/build/internal/CompositeFieldValueMap.js +3 -0
  27. package/build/internal/CompositeFieldValueMap.js.map +1 -1
  28. package/build/internal/EntityDataManager.d.ts +22 -3
  29. package/build/internal/EntityDataManager.js +99 -11
  30. package/build/internal/EntityDataManager.js.map +1 -1
  31. package/build/internal/EntityFieldTransformationUtils.d.ts +20 -0
  32. package/build/internal/EntityFieldTransformationUtils.js +15 -0
  33. package/build/internal/EntityFieldTransformationUtils.js.map +1 -1
  34. package/build/internal/EntityLoadInterfaces.d.ts +8 -0
  35. package/build/internal/EntityLoadInterfaces.js +2 -0
  36. package/build/internal/EntityLoadInterfaces.js.map +1 -1
  37. package/build/internal/EntityTableDataCoordinator.d.ts +2 -0
  38. package/build/internal/EntityTableDataCoordinator.js +2 -0
  39. package/build/internal/EntityTableDataCoordinator.js.map +1 -1
  40. package/build/internal/ReadThroughEntityCache.d.ts +8 -0
  41. package/build/internal/ReadThroughEntityCache.js +5 -0
  42. package/build/internal/ReadThroughEntityCache.js.map +1 -1
  43. package/build/internal/SingleFieldHolder.d.ts +7 -0
  44. package/build/internal/SingleFieldHolder.js +7 -0
  45. package/build/internal/SingleFieldHolder.js.map +1 -1
  46. package/build/metrics/EntityMetricsUtils.d.ts +4 -3
  47. package/build/metrics/EntityMetricsUtils.js +6 -3
  48. package/build/metrics/EntityMetricsUtils.js.map +1 -1
  49. package/build/metrics/IEntityMetricsAdapter.d.ts +21 -0
  50. package/build/metrics/IEntityMetricsAdapter.js.map +1 -1
  51. package/build/tsconfig.build.tsbuildinfo +1 -1
  52. package/build/utils/EntityCreationUtils.d.ts +14 -0
  53. package/build/utils/EntityCreationUtils.js +57 -0
  54. package/build/utils/EntityCreationUtils.js.map +1 -0
  55. package/package.json +13 -13
  56. package/src/AuthorizationResultBasedEntityLoader.ts +7 -1
  57. package/src/AuthorizationResultBasedEntityMutator.ts +133 -15
  58. package/src/EntityLoaderUtils.ts +43 -12
  59. package/src/EntityQueryContext.ts +68 -13
  60. package/src/EntityQueryContextProvider.ts +20 -3
  61. package/src/IEntityGenericCacher.ts +2 -2
  62. package/src/__tests__/AuthorizationResultBasedEntityLoader-test.ts +98 -0
  63. package/src/__tests__/EntityQueryContext-test.ts +141 -26
  64. package/src/errors/EntityNotFoundError.ts +51 -4
  65. package/src/errors/__tests__/EntityDatabaseAdapterError-test.ts +26 -0
  66. package/src/index.ts +1 -0
  67. package/src/internal/CompositeFieldHolder.ts +15 -0
  68. package/src/internal/CompositeFieldValueMap.ts +3 -0
  69. package/src/internal/EntityDataManager.ts +170 -10
  70. package/src/internal/EntityFieldTransformationUtils.ts +20 -0
  71. package/src/internal/EntityLoadInterfaces.ts +8 -0
  72. package/src/internal/EntityTableDataCoordinator.ts +2 -0
  73. package/src/internal/ReadThroughEntityCache.ts +8 -0
  74. package/src/internal/SingleFieldHolder.ts +7 -0
  75. package/src/internal/__tests__/EntityDataManager-test.ts +708 -186
  76. package/src/metrics/EntityMetricsUtils.ts +7 -0
  77. package/src/metrics/IEntityMetricsAdapter.ts +27 -0
  78. package/src/utils/EntityCreationUtils.ts +143 -0
  79. package/src/utils/__testfixtures__/StubDatabaseAdapter.ts +13 -1
  80. package/src/utils/__tests__/EntityCreationUtils-test.ts +354 -0
@@ -14,8 +14,15 @@ export enum TransactionIsolationLevel {
14
14
  SERIALIZABLE = 'SERIALIZABLE',
15
15
  }
16
16
 
17
+ export enum TransactionalDataLoaderMode {
18
+ ENABLED = 'ENABLED',
19
+ ENABLED_BATCH_ONLY = 'ENABLED_BATCH_ONLY',
20
+ DISABLED = 'DISABLED',
21
+ }
22
+
17
23
  export type TransactionConfig = {
18
24
  isolationLevel?: TransactionIsolationLevel;
25
+ transactionalDataLoaderMode?: TransactionalDataLoaderMode;
19
26
  };
20
27
 
21
28
  /**
@@ -28,7 +35,7 @@ export type TransactionConfig = {
28
35
  export abstract class EntityQueryContext {
29
36
  constructor(private readonly queryInterface: any) {}
30
37
 
31
- abstract isInTransaction(): boolean;
38
+ abstract isInTransaction(): this is EntityTransactionalQueryContext;
32
39
 
33
40
  getQueryInterface(): any {
34
41
  return this.queryInterface;
@@ -54,7 +61,7 @@ export class EntityNonTransactionalQueryContext extends EntityQueryContext {
54
61
  super(queryInterface);
55
62
  }
56
63
 
57
- isInTransaction(): boolean {
64
+ override isInTransaction(): this is EntityTransactionalQueryContext {
58
65
  return false;
59
66
  }
60
67
 
@@ -75,6 +82,11 @@ export class EntityNonTransactionalQueryContext extends EntityQueryContext {
75
82
  * dependent triggers and validators will run within the transaction.
76
83
  */
77
84
  export class EntityTransactionalQueryContext extends EntityQueryContext {
85
+ /**
86
+ * @internal
87
+ */
88
+ public readonly childQueryContexts: EntityNestedTransactionalQueryContext[] = [];
89
+
78
90
  private readonly postCommitInvalidationCallbacks: PostCommitCallback[] = [];
79
91
  private readonly postCommitCallbacks: PostCommitCallback[] = [];
80
92
 
@@ -83,6 +95,11 @@ export class EntityTransactionalQueryContext extends EntityQueryContext {
83
95
  constructor(
84
96
  queryInterface: any,
85
97
  private readonly entityQueryContextProvider: EntityQueryContextProvider,
98
+ /**
99
+ * @internal
100
+ */
101
+ readonly transactionId: string,
102
+ public readonly transactionalDataLoaderMode: TransactionalDataLoaderMode,
86
103
  ) {
87
104
  super(queryInterface);
88
105
  }
@@ -121,6 +138,9 @@ export class EntityTransactionalQueryContext extends EntityQueryContext {
121
138
  this.postCommitCallbacks.push(callback);
122
139
  }
123
140
 
141
+ /**
142
+ * @internal
143
+ */
124
144
  public async runPreCommitCallbacksAsync(): Promise<void> {
125
145
  const callbacks = [...this.preCommitCallbacks]
126
146
  .sort((a, b) => a.order - b.order)
@@ -132,6 +152,9 @@ export class EntityTransactionalQueryContext extends EntityQueryContext {
132
152
  }
133
153
  }
134
154
 
155
+ /**
156
+ * @internal
157
+ */
135
158
  public async runPostCommitCallbacksAsync(): Promise<void> {
136
159
  const invalidationCallbacks = [...this.postCommitInvalidationCallbacks];
137
160
  this.postCommitInvalidationCallbacks.length = 0;
@@ -142,10 +165,14 @@ export class EntityTransactionalQueryContext extends EntityQueryContext {
142
165
  await Promise.all(callbacks.map((callback) => callback()));
143
166
  }
144
167
 
145
- isInTransaction(): boolean {
168
+ override isInTransaction(): this is EntityTransactionalQueryContext {
146
169
  return true;
147
170
  }
148
171
 
172
+ isInNestedTransaction(): this is EntityNestedTransactionalQueryContext {
173
+ return false;
174
+ }
175
+
149
176
  async runInTransactionIfNotInTransactionAsync<T>(
150
177
  transactionScope: (queryContext: EntityTransactionalQueryContext) => Promise<T>,
151
178
  transactionConfig?: TransactionConfig,
@@ -181,31 +208,59 @@ export class EntityNestedTransactionalQueryContext extends EntityTransactionalQu
181
208
 
182
209
  constructor(
183
210
  queryInterface: any,
184
- private readonly parentQueryContext: EntityTransactionalQueryContext,
211
+ /**
212
+ * @internal
213
+ */
214
+ readonly parentQueryContext: EntityTransactionalQueryContext,
185
215
  entityQueryContextProvider: EntityQueryContextProvider,
216
+ transactionId: string,
217
+ transactionalDataLoaderMode: TransactionalDataLoaderMode,
186
218
  ) {
187
- super(queryInterface, entityQueryContextProvider);
219
+ super(queryInterface, entityQueryContextProvider, transactionId, transactionalDataLoaderMode);
220
+ parentQueryContext.childQueryContexts.push(this);
188
221
  }
189
222
 
190
- public override appendPostCommitCallback(callback: PostCommitCallback): void {
191
- this.postCommitInvalidationCallbacksToTransfer.push(callback);
223
+ override isInNestedTransaction(): this is EntityNestedTransactionalQueryContext {
224
+ return true;
192
225
  }
193
226
 
194
- public override appendPostCommitInvalidationCallback(callback: PostCommitCallback): void {
227
+ public override appendPostCommitCallback(callback: PostCommitCallback): void {
228
+ // explicitly do not add to the super-class's post-commit callbacks
229
+ // instead, we will add them to the parent transaction's post-commit callbacks
230
+ // after the nested transaction has been committed
195
231
  this.postCommitCallbacksToTransfer.push(callback);
196
232
  }
197
233
 
198
- public override runPostCommitCallbacksAsync(): Promise<void> {
199
- throw new Error(
200
- 'Must not call runPostCommitCallbacksAsync on EntityNestedTransactionalQueryContext',
201
- );
234
+ public override appendPostCommitInvalidationCallback(callback: PostCommitCallback): void {
235
+ super.appendPostCommitInvalidationCallback(callback);
236
+ this.postCommitInvalidationCallbacksToTransfer.push(callback);
202
237
  }
203
238
 
204
- public transferPostCommitCallbacksToParent(): void {
239
+ /**
240
+ * The behavior of callbacks for nested transactions are a bit different than for normal
241
+ * transactions.
242
+ * - Post-commit (non-invalidation) callbacks are run at the end of the outermost transaction
243
+ * since they often contain side-effects that only should run if the transaction doesn't roll back.
244
+ * The outermost transaction has the final say on the commit state of itself and all sub-transactions.
245
+ * - Invalidation callbacks are run at the end of both the nested transaction iteself but also transferred
246
+ * to the parent transaction to be run at the end of it (and recurse upwards, accumulating invalations).
247
+ * This is to ensure the dataloader cache is never stale no matter the DBMS transaction isolation
248
+ * semantics. See the note in `AuthorizationResultBasedBaseMutator` for more details.
249
+ *
250
+ * @internal
251
+ */
252
+ public override async runPostCommitCallbacksAsync(): Promise<void> {
253
+ // run the post-commit callbacks for the nested transaction now
254
+ // (this technically also would run regular post-commit callbacks, but they are empty)
255
+ await super.runPostCommitCallbacksAsync();
256
+
257
+ // transfer a copy of the post-commit invalidation callbacks to the parent transaction
258
+ // to also be run at the end of it (or recurse in the case of the parent transaction being nested as well)
205
259
  for (const callback of this.postCommitInvalidationCallbacksToTransfer) {
206
260
  this.parentQueryContext.appendPostCommitInvalidationCallback(callback);
207
261
  }
208
262
 
263
+ // transfer post-commit callbacks to patent
209
264
  for (const callback of this.postCommitCallbacksToTransfer) {
210
265
  this.parentQueryContext.appendPostCommitCallback(callback);
211
266
  }
@@ -1,8 +1,11 @@
1
+ import { randomUUID } from 'node:crypto';
2
+
1
3
  import {
2
4
  EntityTransactionalQueryContext,
3
5
  EntityNonTransactionalQueryContext,
4
6
  EntityNestedTransactionalQueryContext,
5
7
  TransactionConfig,
8
+ TransactionalDataLoaderMode,
6
9
  } from './EntityQueryContext';
7
10
 
8
11
  /**
@@ -21,6 +24,13 @@ export default abstract class EntityQueryContextProvider {
21
24
  */
22
25
  protected abstract getQueryInterface(): any;
23
26
 
27
+ /**
28
+ * @returns true if the transactional dataloader should be disabled for all transactions.
29
+ */
30
+ protected defaultTransactionalDataLoaderMode(): TransactionalDataLoaderMode {
31
+ return TransactionalDataLoaderMode.ENABLED;
32
+ }
33
+
24
34
  /**
25
35
  * Vend a transaction runner for use in runInTransactionAsync.
26
36
  */
@@ -43,7 +53,12 @@ export default abstract class EntityQueryContextProvider {
43
53
  const [returnedValue, queryContext] = await this.createTransactionRunner<
44
54
  [T, EntityTransactionalQueryContext]
45
55
  >(transactionConfig)(async (queryInterface) => {
46
- const queryContext = new EntityTransactionalQueryContext(queryInterface, this);
56
+ const queryContext = new EntityTransactionalQueryContext(
57
+ queryInterface,
58
+ this,
59
+ randomUUID(),
60
+ transactionConfig?.transactionalDataLoaderMode ?? this.defaultTransactionalDataLoaderMode(),
61
+ );
47
62
  const result = await transactionScope(queryContext);
48
63
  await queryContext.runPreCommitCallbacksAsync();
49
64
  return [result, queryContext];
@@ -69,13 +84,15 @@ export default abstract class EntityQueryContextProvider {
69
84
  innerQueryInterface,
70
85
  outerQueryContext,
71
86
  this,
87
+ randomUUID(),
88
+ outerQueryContext.transactionalDataLoaderMode,
72
89
  );
73
90
  const result = await transactionScope(innerQueryContext);
74
91
  await innerQueryContext.runPreCommitCallbacksAsync();
75
92
  return [result, innerQueryContext];
76
93
  });
77
- // post-commit callbacks are appended to parent transaction instead of run, but only after the transaction has succeeded
78
- innerQueryContext.transferPostCommitCallbacksToParent();
94
+ // behavior of this call differs for nested transaction query contexts from regular transaction query contexts
95
+ await innerQueryContext.runPostCommitCallbacksAsync();
79
96
  return returnedValue;
80
97
  }
81
98
  }
@@ -59,8 +59,8 @@ export default interface IEntityGenericCacher<
59
59
  * from makeCacheKeyForStorage because invalidation can optionally be configured to invalidate a larger set of keys than
60
60
  * the one for just the current cache version, which can be useful for things like push safety.
61
61
  *
62
- * @param key - load key of the cache key
63
- * @param values - load values of the cache key
62
+ * @param key - load key for the cache keys
63
+ * @param value - load value for the cache keys
64
64
  */
65
65
  makeCacheKeysForInvalidation<
66
66
  TLoadKey extends IEntityLoadKey<TFields, TIDField, TSerializedLoadValue, TLoadValue>,
@@ -890,6 +890,104 @@ describe(AuthorizationResultBasedEntityLoader, () => {
890
890
  ).once();
891
891
  });
892
892
 
893
+ it('invalidates upon invalidate by entity within transaction', async () => {
894
+ const viewerContext = instance(mock(ViewerContext));
895
+ const privacyPolicyEvaluationContext =
896
+ instance(
897
+ mock<
898
+ EntityPrivacyPolicyEvaluationContext<
899
+ TestFields,
900
+ 'customIdField',
901
+ ViewerContext,
902
+ TestEntity
903
+ >
904
+ >(),
905
+ );
906
+ const metricsAdapter = instance(mock<IEntityMetricsAdapter>());
907
+
908
+ const privacyPolicy = instance(mock(TestEntityPrivacyPolicy));
909
+ const dataManagerMock = mock<EntityDataManager<TestFields, 'customIdField'>>();
910
+ const dataManagerInstance = instance(dataManagerMock);
911
+
912
+ const id1 = uuidv4();
913
+ const entityMock = mock(TestEntity);
914
+ const date = new Date();
915
+
916
+ when(entityMock.getAllDatabaseFields()).thenReturn({
917
+ customIdField: id1,
918
+ testIndexedField: 'h1',
919
+ intField: 5,
920
+ stringField: 'huh',
921
+ dateField: date,
922
+ nullableField: null,
923
+ });
924
+ const entityInstance = instance(entityMock);
925
+
926
+ await new StubQueryContextProvider().runInTransactionAsync(async (queryContext) => {
927
+ const utils = new EntityLoaderUtils(
928
+ viewerContext,
929
+ queryContext,
930
+ privacyPolicyEvaluationContext,
931
+ testEntityConfiguration,
932
+ TestEntity,
933
+ /* entitySelectedFields */ undefined,
934
+ privacyPolicy,
935
+ dataManagerInstance,
936
+ metricsAdapter,
937
+ );
938
+ const entityLoader = new AuthorizationResultBasedEntityLoader(
939
+ queryContext,
940
+ testEntityConfiguration,
941
+ TestEntity,
942
+ dataManagerInstance,
943
+ metricsAdapter,
944
+ utils,
945
+ );
946
+ entityLoader.utils.invalidateEntityForTransaction(queryContext, entityInstance);
947
+
948
+ verify(
949
+ dataManagerMock.invalidateKeyValuePairsForTransaction(queryContext, anything()),
950
+ ).once();
951
+ verify(
952
+ dataManagerMock.invalidateKeyValuePairsForTransaction(
953
+ queryContext,
954
+ deepEqualEntityAware([
955
+ [
956
+ new SingleFieldHolder<TestFields, 'customIdField', 'customIdField'>('customIdField'),
957
+ new SingleFieldValueHolder<TestFields, 'customIdField'>(id1),
958
+ ],
959
+ [
960
+ new SingleFieldHolder<TestFields, 'customIdField', 'testIndexedField'>(
961
+ 'testIndexedField',
962
+ ),
963
+ new SingleFieldValueHolder<TestFields, 'testIndexedField'>('h1'),
964
+ ],
965
+ [
966
+ new SingleFieldHolder<TestFields, 'customIdField', 'intField'>('intField'),
967
+ new SingleFieldValueHolder<TestFields, 'intField'>(5),
968
+ ],
969
+ [
970
+ new SingleFieldHolder<TestFields, 'customIdField', 'stringField'>('stringField'),
971
+ new SingleFieldValueHolder<TestFields, 'stringField'>('huh'),
972
+ ],
973
+ [
974
+ new SingleFieldHolder<TestFields, 'customIdField', 'dateField'>('dateField'),
975
+ new SingleFieldValueHolder<TestFields, 'dateField'>(date),
976
+ ],
977
+ [
978
+ new CompositeFieldHolder(['stringField', 'intField']),
979
+ new CompositeFieldValueHolder({ stringField: 'huh', intField: 5 }),
980
+ ],
981
+ [
982
+ new CompositeFieldHolder(['stringField', 'testIndexedField']),
983
+ new CompositeFieldValueHolder({ stringField: 'huh', testIndexedField: 'h1' }),
984
+ ],
985
+ ]),
986
+ ),
987
+ ).once();
988
+ });
989
+ });
990
+
893
991
  it('returns error result when not allowed', async () => {
894
992
  const viewerContext = instance(mock(ViewerContext));
895
993
  const privacyPolicyEvaluationContext =
@@ -1,7 +1,18 @@
1
+ import assert from 'assert';
1
2
  import invariant from 'invariant';
2
3
 
3
- import { EntityQueryContext, TransactionIsolationLevel } from '../EntityQueryContext';
4
+ import EntityCompanionProvider from '../EntityCompanionProvider';
5
+ import {
6
+ EntityQueryContext,
7
+ TransactionalDataLoaderMode,
8
+ TransactionConfig,
9
+ TransactionIsolationLevel,
10
+ } from '../EntityQueryContext';
11
+ import EntityQueryContextProvider from '../EntityQueryContextProvider';
4
12
  import ViewerContext from '../ViewerContext';
13
+ import NoOpEntityMetricsAdapter from '../metrics/NoOpEntityMetricsAdapter';
14
+ import { InMemoryFullCacheStubCacheAdapterProvider } from '../utils/__testfixtures__/StubCacheAdapter';
15
+ import StubDatabaseAdapterProvider from '../utils/__testfixtures__/StubDatabaseAdapterProvider';
5
16
  import { createUnitTestEntityCompanionProvider } from '../utils/__testfixtures__/createUnitTestEntityCompanionProvider';
6
17
 
7
18
  describe(EntityQueryContext, () => {
@@ -89,7 +100,9 @@ describe(EntityQueryContext, () => {
89
100
  throw new Error('wat');
90
101
  });
91
102
  const postCommitInvalidationCallback = jest.fn(async (): Promise<void> => {});
103
+ const postCommitNestedInvalidationCallback = jest.fn(async (): Promise<void> => {});
92
104
  const postCommitCallback = jest.fn(async (): Promise<void> => {});
105
+ const postCommitNestedCallback = jest.fn(async (): Promise<void> => {});
93
106
 
94
107
  await viewerContext.runInTransactionForDatabaseAdaptorFlavorAsync(
95
108
  'postgres',
@@ -100,19 +113,19 @@ describe(EntityQueryContext, () => {
100
113
 
101
114
  await Promise.all([
102
115
  queryContext.runInNestedTransactionAsync(async (innerQueryContext) => {
103
- innerQueryContext.appendPostCommitCallback(postCommitCallback);
116
+ innerQueryContext.appendPostCommitCallback(postCommitNestedCallback);
104
117
  innerQueryContext.appendPostCommitInvalidationCallback(
105
- postCommitInvalidationCallback,
118
+ postCommitNestedInvalidationCallback,
106
119
  );
107
120
  innerQueryContext.appendPreCommitCallback(preCommitNestedCallback, 0);
108
121
  }),
109
122
  (async () => {
110
123
  try {
111
124
  await queryContext.runInNestedTransactionAsync(async (innerQueryContext) => {
112
- // these two shouldn't be called
113
- innerQueryContext.appendPostCommitCallback(postCommitCallback);
125
+ // these two shouldn't be called due to throwing pre-commit callback
126
+ innerQueryContext.appendPostCommitCallback(postCommitNestedCallback);
114
127
  innerQueryContext.appendPostCommitInvalidationCallback(
115
- postCommitInvalidationCallback,
128
+ postCommitNestedInvalidationCallback,
116
129
  );
117
130
  innerQueryContext.appendPreCommitCallback(preCommitNestedCallbackThrow, 0);
118
131
  });
@@ -125,26 +138,10 @@ describe(EntityQueryContext, () => {
125
138
  expect(preCommitCallback).toHaveBeenCalledTimes(1);
126
139
  expect(preCommitNestedCallback).toHaveBeenCalledTimes(1);
127
140
  expect(preCommitNestedCallbackThrow).toHaveBeenCalledTimes(1);
128
- expect(postCommitCallback).toHaveBeenCalledTimes(2);
129
- expect(postCommitInvalidationCallback).toHaveBeenCalledTimes(2);
130
- });
131
-
132
- it('does not support calling runPostCommitCallbacksAsync on nested transaction', async () => {
133
- const companionProvider = createUnitTestEntityCompanionProvider();
134
- const viewerContext = new ViewerContext(companionProvider);
135
-
136
- await expect(
137
- viewerContext.runInTransactionForDatabaseAdaptorFlavorAsync(
138
- 'postgres',
139
- async (queryContext) => {
140
- await queryContext.runInNestedTransactionAsync(async (innerQueryContext) => {
141
- await innerQueryContext.runPostCommitCallbacksAsync();
142
- });
143
- },
144
- ),
145
- ).rejects.toThrowError(
146
- 'Must not call runPostCommitCallbacksAsync on EntityNestedTransactionalQueryContext',
147
- );
141
+ expect(postCommitCallback).toHaveBeenCalledTimes(1);
142
+ expect(postCommitInvalidationCallback).toHaveBeenCalledTimes(1);
143
+ expect(postCommitNestedInvalidationCallback).toHaveBeenCalledTimes(2);
144
+ expect(postCommitNestedCallback).toHaveBeenCalledTimes(1);
148
145
  });
149
146
  });
150
147
 
@@ -168,5 +165,123 @@ describe(EntityQueryContext, () => {
168
165
 
169
166
  expect(queryContextProviderSpy).toHaveBeenCalledWith(transactionScopeFn, transactionConfig);
170
167
  });
168
+
169
+ it('makes the result query context enable/disable transactional dataloaders', async () => {
170
+ const companionProvider = createUnitTestEntityCompanionProvider();
171
+ const viewerContext = new ViewerContext(companionProvider);
172
+
173
+ await viewerContext.runInTransactionForDatabaseAdaptorFlavorAsync(
174
+ 'postgres',
175
+ async (queryContext) => {
176
+ assert(queryContext.isInTransaction());
177
+ expect(queryContext.transactionalDataLoaderMode).toBe(
178
+ TransactionalDataLoaderMode.DISABLED,
179
+ );
180
+ },
181
+ { transactionalDataLoaderMode: TransactionalDataLoaderMode.DISABLED },
182
+ );
183
+
184
+ await viewerContext.runInTransactionForDatabaseAdaptorFlavorAsync(
185
+ 'postgres',
186
+ async (queryContext) => {
187
+ assert(queryContext.isInTransaction());
188
+ expect(queryContext.transactionalDataLoaderMode).toBe(
189
+ TransactionalDataLoaderMode.ENABLED_BATCH_ONLY,
190
+ );
191
+ },
192
+ { transactionalDataLoaderMode: TransactionalDataLoaderMode.ENABLED_BATCH_ONLY },
193
+ );
194
+
195
+ await viewerContext.runInTransactionForDatabaseAdaptorFlavorAsync(
196
+ 'postgres',
197
+ async (queryContext) => {
198
+ assert(queryContext.isInTransaction());
199
+ expect(queryContext.transactionalDataLoaderMode).toBe(
200
+ TransactionalDataLoaderMode.ENABLED,
201
+ );
202
+ },
203
+ );
204
+ });
205
+ });
206
+
207
+ describe('global defaultTransactionalDataLoaderMode', () => {
208
+ class StubQueryContextProviderWithDisabledTransactionalDataLoaders extends EntityQueryContextProvider {
209
+ protected getQueryInterface(): any {
210
+ return {};
211
+ }
212
+
213
+ protected override defaultTransactionalDataLoaderMode(): TransactionalDataLoaderMode {
214
+ return TransactionalDataLoaderMode.DISABLED;
215
+ }
216
+
217
+ protected createTransactionRunner<T>(
218
+ _transactionConfig?: TransactionConfig,
219
+ ): (transactionScope: (queryInterface: any) => Promise<T>) => Promise<T> {
220
+ return (transactionScope) => Promise.resolve(transactionScope({}));
221
+ }
222
+
223
+ protected createNestedTransactionRunner<T>(
224
+ _outerQueryInterface: any,
225
+ ): (transactionScope: (queryInterface: any) => Promise<T>) => Promise<T> {
226
+ return (transactionScope) => Promise.resolve(transactionScope({}));
227
+ }
228
+ }
229
+
230
+ it('respects the global setting but allows overriding by transaction config', async () => {
231
+ const companionProvider = new EntityCompanionProvider(
232
+ new NoOpEntityMetricsAdapter(),
233
+ new Map([
234
+ [
235
+ 'postgres',
236
+ {
237
+ adapterProvider: new StubDatabaseAdapterProvider(),
238
+ queryContextProvider:
239
+ new StubQueryContextProviderWithDisabledTransactionalDataLoaders(),
240
+ },
241
+ ],
242
+ ]),
243
+ new Map([
244
+ [
245
+ 'redis',
246
+ {
247
+ cacheAdapterProvider: new InMemoryFullCacheStubCacheAdapterProvider(),
248
+ },
249
+ ],
250
+ ]),
251
+ );
252
+ const viewerContext = new ViewerContext(companionProvider);
253
+
254
+ await viewerContext.runInTransactionForDatabaseAdaptorFlavorAsync(
255
+ 'postgres',
256
+ async (queryContext) => {
257
+ assert(queryContext.isInTransaction());
258
+ expect(queryContext.transactionalDataLoaderMode).toBe(
259
+ TransactionalDataLoaderMode.DISABLED,
260
+ );
261
+ },
262
+ { transactionalDataLoaderMode: TransactionalDataLoaderMode.DISABLED },
263
+ );
264
+
265
+ await viewerContext.runInTransactionForDatabaseAdaptorFlavorAsync(
266
+ 'postgres',
267
+ async (queryContext) => {
268
+ assert(queryContext.isInTransaction());
269
+ expect(queryContext.transactionalDataLoaderMode).toBe(
270
+ TransactionalDataLoaderMode.ENABLED_BATCH_ONLY,
271
+ );
272
+ },
273
+ { transactionalDataLoaderMode: TransactionalDataLoaderMode.ENABLED_BATCH_ONLY },
274
+ );
275
+
276
+ await viewerContext.runInTransactionForDatabaseAdaptorFlavorAsync(
277
+ 'postgres',
278
+ async (queryContext) => {
279
+ assert(queryContext.isInTransaction());
280
+ expect(queryContext.transactionalDataLoaderMode).toBe(
281
+ TransactionalDataLoaderMode.DISABLED,
282
+ );
283
+ },
284
+ );
285
+ });
171
286
  });
172
287
  });
@@ -4,6 +4,33 @@ import EntityPrivacyPolicy from '../EntityPrivacyPolicy';
4
4
  import ReadonlyEntity from '../ReadonlyEntity';
5
5
  import ViewerContext from '../ViewerContext';
6
6
 
7
+ type EntityNotFoundOptions<
8
+ TFields extends Record<string, any>,
9
+ TIDField extends keyof NonNullable<Pick<TFields, TSelectedFields>>,
10
+ TViewerContext extends ViewerContext,
11
+ TEntity extends ReadonlyEntity<TFields, TIDField, TViewerContext, TSelectedFields>,
12
+ TPrivacyPolicy extends EntityPrivacyPolicy<
13
+ TFields,
14
+ TIDField,
15
+ TViewerContext,
16
+ TEntity,
17
+ TSelectedFields
18
+ >,
19
+ N extends keyof TFields,
20
+ TSelectedFields extends keyof TFields = keyof TFields,
21
+ > = {
22
+ entityClass: IEntityClass<
23
+ TFields,
24
+ TIDField,
25
+ TViewerContext,
26
+ TEntity,
27
+ TPrivacyPolicy,
28
+ TSelectedFields
29
+ >;
30
+ fieldName: N;
31
+ fieldValue: TFields[N];
32
+ };
33
+
7
34
  export default class EntityNotFoundError<
8
35
  TFields extends Record<string, any>,
9
36
  TIDField extends keyof NonNullable<Pick<TFields, TSelectedFields>>,
@@ -22,18 +49,38 @@ export default class EntityNotFoundError<
22
49
  public readonly state = EntityErrorState.PERMANENT;
23
50
  public readonly code = EntityErrorCode.ERR_ENTITY_NOT_FOUND;
24
51
 
52
+ constructor(message: string);
25
53
  constructor(
26
- entityClass: IEntityClass<
54
+ options: EntityNotFoundOptions<
27
55
  TFields,
28
56
  TIDField,
29
57
  TViewerContext,
30
58
  TEntity,
31
59
  TPrivacyPolicy,
60
+ N,
32
61
  TSelectedFields
33
62
  >,
34
- fieldName: N,
35
- fieldValue: TFields[N],
63
+ );
64
+
65
+ constructor(
66
+ messageOrOptions:
67
+ | string
68
+ | EntityNotFoundOptions<
69
+ TFields,
70
+ TIDField,
71
+ TViewerContext,
72
+ TEntity,
73
+ TPrivacyPolicy,
74
+ N,
75
+ TSelectedFields
76
+ >,
36
77
  ) {
37
- super(`Entity not found: ${entityClass.name} (${String(fieldName)} = ${fieldValue})`);
78
+ if (typeof messageOrOptions === 'string') {
79
+ super(messageOrOptions);
80
+ } else {
81
+ super(
82
+ `Entity not found: ${messageOrOptions.entityClass.name} (${String(messageOrOptions.fieldName)} = ${messageOrOptions.fieldValue})`,
83
+ );
84
+ }
38
85
  }
39
86
  }
@@ -0,0 +1,26 @@
1
+ import EntityDatabaseAdapterError, {
2
+ EntityDatabaseAdapterTransientError,
3
+ EntityDatabaseAdapterUnknownError,
4
+ EntityDatabaseAdapterCheckConstraintError,
5
+ EntityDatabaseAdapterExclusionConstraintError,
6
+ EntityDatabaseAdapterForeignKeyConstraintError,
7
+ EntityDatabaseAdapterNotNullConstraintError,
8
+ EntityDatabaseAdapterUniqueConstraintError,
9
+ } from '../EntityDatabaseAdapterError';
10
+
11
+ describe(EntityDatabaseAdapterError, () => {
12
+ // necessary for coverage within the entity package since these errors are
13
+ // currently only ever instantiated by database adapter implementations
14
+ it('instantiates all errors successfully', () => {
15
+ const errors = [
16
+ new EntityDatabaseAdapterTransientError('test'),
17
+ new EntityDatabaseAdapterUnknownError('test'),
18
+ new EntityDatabaseAdapterCheckConstraintError('test'),
19
+ new EntityDatabaseAdapterExclusionConstraintError('test'),
20
+ new EntityDatabaseAdapterForeignKeyConstraintError('test'),
21
+ new EntityDatabaseAdapterNotNullConstraintError('test'),
22
+ new EntityDatabaseAdapterUniqueConstraintError('test'),
23
+ ];
24
+ expect(errors).not.toBeFalsy();
25
+ });
26
+ });
package/src/index.ts CHANGED
@@ -85,6 +85,7 @@ export { default as AlwaysDenyPrivacyPolicyRule } from './rules/AlwaysDenyPrivac
85
85
  export { default as AlwaysSkipPrivacyPolicyRule } from './rules/AlwaysSkipPrivacyPolicyRule';
86
86
  export { default as PrivacyPolicyRule } from './rules/PrivacyPolicyRule';
87
87
  export * from './rules/PrivacyPolicyRule';
88
+ export * from './utils/EntityCreationUtils';
88
89
  export * from './utils/EntityPrivacyUtils';
89
90
  export * from './utils/mergeEntityMutationTriggerConfigurations';
90
91
  export * from './utils/collections/maps';