@expo/entity 0.43.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.
Files changed (62) hide show
  1. package/build/AuthorizationResultBasedEntityMutator.d.ts +87 -2
  2. package/build/AuthorizationResultBasedEntityMutator.js +122 -8
  3. package/build/AuthorizationResultBasedEntityMutator.js.map +1 -1
  4. package/build/EntityLoaderUtils.d.ts +15 -3
  5. package/build/EntityLoaderUtils.js +25 -10
  6. package/build/EntityLoaderUtils.js.map +1 -1
  7. package/build/EntityQueryContext.d.ts +53 -7
  8. package/build/EntityQueryContext.js +65 -10
  9. package/build/EntityQueryContext.js.map +1 -1
  10. package/build/EntityQueryContextProvider.d.ts +5 -1
  11. package/build/EntityQueryContextProvider.js +11 -4
  12. package/build/EntityQueryContextProvider.js.map +1 -1
  13. package/build/IEntityGenericCacher.d.ts +2 -2
  14. package/build/internal/CompositeFieldHolder.d.ts +13 -0
  15. package/build/internal/CompositeFieldHolder.js +7 -0
  16. package/build/internal/CompositeFieldHolder.js.map +1 -1
  17. package/build/internal/CompositeFieldValueMap.d.ts +3 -0
  18. package/build/internal/CompositeFieldValueMap.js +3 -0
  19. package/build/internal/CompositeFieldValueMap.js.map +1 -1
  20. package/build/internal/EntityDataManager.d.ts +22 -3
  21. package/build/internal/EntityDataManager.js +99 -11
  22. package/build/internal/EntityDataManager.js.map +1 -1
  23. package/build/internal/EntityFieldTransformationUtils.d.ts +20 -0
  24. package/build/internal/EntityFieldTransformationUtils.js +15 -0
  25. package/build/internal/EntityFieldTransformationUtils.js.map +1 -1
  26. package/build/internal/EntityLoadInterfaces.d.ts +8 -0
  27. package/build/internal/EntityLoadInterfaces.js +2 -0
  28. package/build/internal/EntityLoadInterfaces.js.map +1 -1
  29. package/build/internal/EntityTableDataCoordinator.d.ts +2 -0
  30. package/build/internal/EntityTableDataCoordinator.js +2 -0
  31. package/build/internal/EntityTableDataCoordinator.js.map +1 -1
  32. package/build/internal/ReadThroughEntityCache.d.ts +8 -0
  33. package/build/internal/ReadThroughEntityCache.js +5 -0
  34. package/build/internal/ReadThroughEntityCache.js.map +1 -1
  35. package/build/internal/SingleFieldHolder.d.ts +7 -0
  36. package/build/internal/SingleFieldHolder.js +7 -0
  37. package/build/internal/SingleFieldHolder.js.map +1 -1
  38. package/build/metrics/EntityMetricsUtils.d.ts +4 -3
  39. package/build/metrics/EntityMetricsUtils.js +6 -3
  40. package/build/metrics/EntityMetricsUtils.js.map +1 -1
  41. package/build/metrics/IEntityMetricsAdapter.d.ts +21 -0
  42. package/build/metrics/IEntityMetricsAdapter.js.map +1 -1
  43. package/package.json +13 -13
  44. package/src/AuthorizationResultBasedEntityMutator.ts +133 -15
  45. package/src/EntityLoaderUtils.ts +43 -12
  46. package/src/EntityQueryContext.ts +68 -13
  47. package/src/EntityQueryContextProvider.ts +20 -3
  48. package/src/IEntityGenericCacher.ts +2 -2
  49. package/src/__tests__/AuthorizationResultBasedEntityLoader-test.ts +98 -0
  50. package/src/__tests__/EntityQueryContext-test.ts +141 -26
  51. package/src/internal/CompositeFieldHolder.ts +15 -0
  52. package/src/internal/CompositeFieldValueMap.ts +3 -0
  53. package/src/internal/EntityDataManager.ts +170 -10
  54. package/src/internal/EntityFieldTransformationUtils.ts +20 -0
  55. package/src/internal/EntityLoadInterfaces.ts +8 -0
  56. package/src/internal/EntityTableDataCoordinator.ts +2 -0
  57. package/src/internal/ReadThroughEntityCache.ts +8 -0
  58. package/src/internal/SingleFieldHolder.ts +7 -0
  59. package/src/internal/__tests__/EntityDataManager-test.ts +708 -186
  60. package/src/metrics/EntityMetricsUtils.ts +7 -0
  61. package/src/metrics/IEntityMetricsAdapter.ts +27 -0
  62. package/src/utils/__testfixtures__/StubDatabaseAdapter.ts +13 -1
@@ -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
  });
@@ -14,6 +14,10 @@ import {
14
14
  } from '../internal/EntityLoadInterfaces';
15
15
 
16
16
  declare const CompositeFieldHolderSerializedBrand: unique symbol;
17
+
18
+ /**
19
+ * @internal
20
+ */
17
21
  export type SerializedCompositeFieldHolder = string & {
18
22
  readonly [CompositeFieldHolderSerializedBrand]: true;
19
23
  };
@@ -21,6 +25,8 @@ export type SerializedCompositeFieldHolder = string & {
21
25
  /**
22
26
  * A load key that represents a composite field (set of fieldName) on an entity.
23
27
  * Must be defined in the entity configuration composite field definition.
28
+ *
29
+ * @internal
24
30
  */
25
31
  export class CompositeFieldHolder<
26
32
  TFields extends Record<string, any>,
@@ -151,12 +157,18 @@ export class CompositeFieldHolder<
151
157
  }
152
158
 
153
159
  declare const CompositeFieldValueHolderSerializedBrand: unique symbol;
160
+
161
+ /**
162
+ * @internal
163
+ */
154
164
  export type SerializedCompositeFieldValueHolder = string & {
155
165
  readonly [CompositeFieldValueHolderSerializedBrand]: true;
156
166
  };
157
167
 
158
168
  /**
159
169
  * A load value for a CompositeFieldHolder.
170
+ *
171
+ * @internal
160
172
  */
161
173
  export class CompositeFieldValueHolder<
162
174
  TFields extends Record<string, any>,
@@ -201,6 +213,9 @@ export class CompositeFieldValueHolder<
201
213
  }
202
214
  }
203
215
 
216
+ /**
217
+ * @internal
218
+ */
204
219
  export class CompositeFieldValueHolderMap<
205
220
  TFields extends Record<string, any>,
206
221
  N extends EntityCompositeField<TFields>,
@@ -4,6 +4,9 @@ import {
4
4
  CompositeFieldValueHolder,
5
5
  } from './CompositeFieldHolder';
6
6
 
7
+ /**
8
+ * @internal
9
+ */
7
10
  export class CompositeFieldValueMap<
8
11
  TFields extends Record<string, any>,
9
12
  N extends EntityCompositeField<TFields>,
@@ -7,7 +7,11 @@ import EntityDatabaseAdapter, {
7
7
  QuerySelectionModifiers,
8
8
  QuerySelectionModifiersWithOrderByRaw,
9
9
  } from '../EntityDatabaseAdapter';
10
- import { EntityQueryContext } from '../EntityQueryContext';
10
+ import {
11
+ EntityQueryContext,
12
+ EntityTransactionalQueryContext,
13
+ TransactionalDataLoaderMode,
14
+ } from '../EntityQueryContext';
11
15
  import EntityQueryContextProvider from '../EntityQueryContextProvider';
12
16
  import { partitionErrors } from '../entityUtils';
13
17
  import { IEntityLoadKey, IEntityLoadValue, LoadPair } from './EntityLoadInterfaces';
@@ -21,18 +25,28 @@ import IEntityMetricsAdapter, {
21
25
  } from '../metrics/IEntityMetricsAdapter';
22
26
  import { computeIfAbsent } from '../utils/collections/maps';
23
27
 
28
+ type DataLoaderMap<TFields extends Record<string, any>> = Map<
29
+ string,
30
+ DataLoader<unknown, readonly Readonly<TFields>[]>
31
+ >;
32
+
24
33
  /**
25
34
  * A data manager is responsible for orchestrating multiple sources of entity
26
35
  * data including local caches, EntityCacheAdapter, and EntityDatabaseAdapter.
27
36
  *
28
37
  * It is also responsible for invalidating all sources of data when mutated using EntityMutator.
38
+ *
39
+ * @internal
29
40
  */
30
41
  export default class EntityDataManager<
31
42
  TFields extends Record<string, any>,
32
43
  TIDField extends keyof TFields,
33
44
  > {
34
- private readonly dataloaders: Map<string, DataLoader<unknown, readonly Readonly<TFields>[]>> =
35
- new Map();
45
+ // map from (load method type + data manager data loader key) to dataloader
46
+ private readonly dataLoaders: DataLoaderMap<TFields> = new Map();
47
+
48
+ // map from transaction id to dataloader map
49
+ private readonly transactionalDataLoaders: Map<string, DataLoaderMap<TFields>> = new Map();
36
50
 
37
51
  constructor(
38
52
  private readonly databaseAdapter: EntityDatabaseAdapter<TFields, TIDField>,
@@ -48,7 +62,7 @@ export default class EntityDataManager<
48
62
  TLoadValue extends IEntityLoadValue<TSerializedLoadValue>,
49
63
  >(key: TLoadKey): DataLoader<TSerializedLoadValue, readonly Readonly<TFields>[]> {
50
64
  return computeIfAbsent(
51
- this.dataloaders,
65
+ this.dataLoaders,
52
66
  key.getLoadMethodType() + key.getDataManagerDataLoaderKey(),
53
67
  () => {
54
68
  return new DataLoader(
@@ -58,7 +72,7 @@ export default class EntityDataManager<
58
72
  const values = serializedLoadValues.map((serializedLoadValue) =>
59
73
  key.deserializeLoadValue(serializedLoadValue),
60
74
  );
61
- const objectMap = await this.loadManyForDataLoaderAsync(key, values);
75
+ const objectMap = await this.loadManyForNonTransactionalDataLoaderAsync(key, values);
62
76
  return values.map((value) => objectMap.get(value) ?? []);
63
77
  },
64
78
  );
@@ -66,7 +80,7 @@ export default class EntityDataManager<
66
80
  );
67
81
  }
68
82
 
69
- private async loadManyForDataLoaderAsync<
83
+ private async loadManyForNonTransactionalDataLoaderAsync<
70
84
  TLoadKey extends IEntityLoadKey<TFields, TIDField, TSerializedLoadValue, TLoadValue>,
71
85
  TSerializedLoadValue,
72
86
  TLoadValue extends IEntityLoadValue<TSerializedLoadValue>,
@@ -76,6 +90,7 @@ export default class EntityDataManager<
76
90
  ): Promise<ReadonlyMap<TLoadValue, readonly Readonly<TFields>[]>> {
77
91
  this.metricsAdapter.incrementDataManagerLoadCount({
78
92
  type: IncrementLoadCountEventType.CACHE,
93
+ isInTransaction: false,
79
94
  fieldValueCount: values.length,
80
95
  entityClassName: this.entityClassName,
81
96
  loadType: key.getLoadMethodType(),
@@ -84,6 +99,7 @@ export default class EntityDataManager<
84
99
  return await this.entityCache.readManyThroughAsync(key, values, async (fetcherValues) => {
85
100
  this.metricsAdapter.incrementDataManagerLoadCount({
86
101
  type: IncrementLoadCountEventType.DATABASE,
102
+ isInTransaction: false,
87
103
  fieldValueCount: fetcherValues.length,
88
104
  entityClassName: this.entityClassName,
89
105
  loadType: key.getLoadMethodType(),
@@ -96,6 +112,65 @@ export default class EntityDataManager<
96
112
  });
97
113
  }
98
114
 
115
+ private getTransactionalDataLoaderForLoadKey<
116
+ TLoadKey extends IEntityLoadKey<TFields, TIDField, TSerializedLoadValue, TLoadValue>,
117
+ TSerializedLoadValue,
118
+ TLoadValue extends IEntityLoadValue<TSerializedLoadValue>,
119
+ >(
120
+ queryContext: EntityTransactionalQueryContext,
121
+ key: TLoadKey,
122
+ ): DataLoader<TSerializedLoadValue, readonly Readonly<TFields>[]> {
123
+ const dataLoaderMapForTransaction = computeIfAbsent(
124
+ this.transactionalDataLoaders,
125
+ queryContext.transactionId,
126
+ () => new Map(),
127
+ );
128
+ return computeIfAbsent(
129
+ dataLoaderMapForTransaction,
130
+ key.getLoadMethodType() + key.getDataManagerDataLoaderKey(),
131
+ () => {
132
+ return new DataLoader(
133
+ async (
134
+ serializedLoadValues: readonly TSerializedLoadValue[],
135
+ ): Promise<readonly (readonly TFields[])[]> => {
136
+ const values = serializedLoadValues.map((serializedLoadValue) =>
137
+ key.deserializeLoadValue(serializedLoadValue),
138
+ );
139
+ const objectMap = await this.loadManyForTransactionalDataLoaderAsync(
140
+ queryContext,
141
+ key,
142
+ values,
143
+ );
144
+ return values.map((value) => objectMap.get(value) ?? []);
145
+ },
146
+ {
147
+ // only cache if transactional dataloader caching is enabled for the transactional query context
148
+ cache: queryContext.transactionalDataLoaderMode === TransactionalDataLoaderMode.ENABLED,
149
+ },
150
+ );
151
+ },
152
+ );
153
+ }
154
+
155
+ private async loadManyForTransactionalDataLoaderAsync<
156
+ TLoadKey extends IEntityLoadKey<TFields, TIDField, TSerializedLoadValue, TLoadValue>,
157
+ TSerializedLoadValue,
158
+ TLoadValue extends IEntityLoadValue<TSerializedLoadValue>,
159
+ >(
160
+ queryContext: EntityTransactionalQueryContext,
161
+ key: TLoadKey,
162
+ values: readonly TLoadValue[],
163
+ ): Promise<ReadonlyMap<TLoadValue, readonly Readonly<TFields>[]>> {
164
+ this.metricsAdapter.incrementDataManagerLoadCount({
165
+ type: IncrementLoadCountEventType.DATABASE,
166
+ isInTransaction: true,
167
+ fieldValueCount: values.length,
168
+ entityClassName: this.entityClassName,
169
+ loadType: key.getLoadMethodType(),
170
+ });
171
+ return await this.databaseAdapter.fetchManyWhereAsync(queryContext, key, values);
172
+ }
173
+
99
174
  /**
100
175
  * Load many objects through read-through dataloader (batcher) and cache (optional).
101
176
  *
@@ -117,6 +192,7 @@ export default class EntityDataManager<
117
192
  this.metricsAdapter,
118
193
  EntityMetricsLoadType.LOAD_MANY,
119
194
  this.entityClassName,
195
+ queryContext,
120
196
  )(this.loadManyEqualingInternalAsync(queryContext, key, values));
121
197
  }
122
198
 
@@ -131,18 +207,30 @@ export default class EntityDataManager<
131
207
  ): Promise<ReadonlyMap<TLoadValue, readonly Readonly<TFields>[]>> {
132
208
  key.validateRuntimeLoadValuesForDataManagerDataLoader(values, this.entityClassName);
133
209
 
134
- // don't cache when in transaction, as rollbacks complicate things significantly
135
- if (queryContext.isInTransaction()) {
210
+ if (
211
+ queryContext.isInTransaction() &&
212
+ queryContext.transactionalDataLoaderMode === TransactionalDataLoaderMode.DISABLED
213
+ ) {
214
+ this.metricsAdapter.incrementDataManagerLoadCount({
215
+ type: IncrementLoadCountEventType.DATABASE,
216
+ isInTransaction: true,
217
+ fieldValueCount: values.length,
218
+ entityClassName: this.entityClassName,
219
+ loadType: key.getLoadMethodType(),
220
+ });
136
221
  return await this.databaseAdapter.fetchManyWhereAsync(queryContext, key, values);
137
222
  }
138
223
 
139
224
  this.metricsAdapter.incrementDataManagerLoadCount({
140
225
  type: IncrementLoadCountEventType.DATALOADER,
226
+ isInTransaction: queryContext.isInTransaction(),
141
227
  fieldValueCount: values.length,
142
228
  entityClassName: this.entityClassName,
143
229
  loadType: key.getLoadMethodType(),
144
230
  });
145
- const dataLoader = this.getDataLoaderForLoadKey(key);
231
+ const dataLoader = queryContext.isInTransaction()
232
+ ? this.getTransactionalDataLoaderForLoadKey(queryContext, key)
233
+ : this.getDataLoaderForLoadKey(key);
146
234
  const results = await dataLoader.loadMany(values.map((v) => key.serializeLoadValue(v)));
147
235
  const [successfulValues, errors] = partitionErrors(results);
148
236
  if (errors.length > 0) {
@@ -179,6 +267,7 @@ export default class EntityDataManager<
179
267
  this.metricsAdapter,
180
268
  EntityMetricsLoadType.LOAD_MANY_EQUALITY_CONJUNCTION,
181
269
  this.entityClassName,
270
+ queryContext,
182
271
  )(
183
272
  this.databaseAdapter.fetchManyByFieldEqualityConjunctionAsync(
184
273
  queryContext,
@@ -207,6 +296,7 @@ export default class EntityDataManager<
207
296
  this.metricsAdapter,
208
297
  EntityMetricsLoadType.LOAD_MANY_RAW,
209
298
  this.entityClassName,
299
+ queryContext,
210
300
  )(
211
301
  this.databaseAdapter.fetchManyByRawWhereClauseAsync(
212
302
  queryContext,
@@ -226,6 +316,16 @@ export default class EntityDataManager<
226
316
  this.getDataLoaderForLoadKey(key).clear(key.serializeLoadValue(value));
227
317
  }
228
318
 
319
+ private invalidateOneForTransaction<
320
+ TLoadKey extends IEntityLoadKey<TFields, TIDField, TSerializedLoadValue, TLoadValue>,
321
+ TSerializedLoadValue,
322
+ TLoadValue extends IEntityLoadValue<TSerializedLoadValue>,
323
+ >(queryContext: EntityTransactionalQueryContext, key: TLoadKey, value: TLoadValue): void {
324
+ this.getTransactionalDataLoaderForLoadKey(queryContext, key).clear(
325
+ key.serializeLoadValue(value),
326
+ );
327
+ }
328
+
229
329
  /**
230
330
  * Invalidate all caches, in-memory or otherwise, for sets of key-value pairs.
231
331
  * @param pairs - key-value pairs to invalidate
@@ -233,7 +333,67 @@ export default class EntityDataManager<
233
333
  public async invalidateKeyValuePairsAsync(
234
334
  pairs: readonly LoadPair<TFields, TIDField, any, any, any>[],
235
335
  ): Promise<void> {
236
- // TODO(wschurman): check for races with load
237
336
  await Promise.all(pairs.map(([key, value]) => this.invalidateOneAsync(key, value)));
238
337
  }
338
+
339
+ /**
340
+ * Invalidate all in-memory caches for sets of key-value pairs for all transactions and parent transactions.
341
+ * @param pairs - key-value pairs to invalidate
342
+ */
343
+ public invalidateKeyValuePairsForTransaction(
344
+ queryContext: EntityTransactionalQueryContext,
345
+ pairs: readonly LoadPair<TFields, TIDField, any, any, any>[],
346
+ ): void {
347
+ if (queryContext.transactionalDataLoaderMode === TransactionalDataLoaderMode.DISABLED) {
348
+ return;
349
+ }
350
+
351
+ // invalidate all query contexts in transaction tree
352
+ const outermostTransactionalQueryContext =
353
+ EntityDataManager.getOutermostTransactionalQueryContextIfInNestedTransaction(queryContext);
354
+ const allQueryContextsToInvalidate = [
355
+ outermostTransactionalQueryContext,
356
+ ...EntityDataManager.getAllDescendantTransactionalQueryContexts(
357
+ outermostTransactionalQueryContext,
358
+ ),
359
+ ];
360
+ for (const currentQueryContext of allQueryContextsToInvalidate) {
361
+ for (const [key, value] of pairs) {
362
+ this.invalidateOneForTransaction(currentQueryContext, key, value);
363
+ }
364
+ }
365
+ }
366
+
367
+ /**
368
+ * Traverse to root of transactional query context tree.
369
+ */
370
+ private static getOutermostTransactionalQueryContextIfInNestedTransaction(
371
+ queryContext: EntityTransactionalQueryContext,
372
+ ): EntityTransactionalQueryContext {
373
+ if (queryContext.isInNestedTransaction()) {
374
+ return EntityDataManager.getOutermostTransactionalQueryContextIfInNestedTransaction(
375
+ queryContext.parentQueryContext,
376
+ );
377
+ } else {
378
+ return queryContext;
379
+ }
380
+ }
381
+
382
+ /**
383
+ * Get a list of all child query contexts recursively for a given query context.
384
+ */
385
+ private static getAllDescendantTransactionalQueryContexts(
386
+ queryContext: EntityTransactionalQueryContext,
387
+ ): readonly EntityTransactionalQueryContext[] {
388
+ if (queryContext.childQueryContexts.length === 0) {
389
+ return [];
390
+ }
391
+
392
+ return queryContext.childQueryContexts.flatMap((childQueryContext) => {
393
+ return [
394
+ childQueryContext,
395
+ ...EntityDataManager.getAllDescendantTransactionalQueryContexts(childQueryContext),
396
+ ];
397
+ });
398
+ }
239
399
  }
@@ -3,6 +3,9 @@ import nullthrows from 'nullthrows';
3
3
 
4
4
  import EntityConfiguration from '../EntityConfiguration';
5
5
 
6
+ /**
7
+ * @internal
8
+ */
6
9
  export interface FieldTransformer<T> {
7
10
  /**
8
11
  * Transformation to apply when a value is read from an adapter.
@@ -17,9 +20,14 @@ export interface FieldTransformer<T> {
17
20
 
18
21
  /**
19
22
  * Map from concrete EntityFieldDefinition implementation class name to field transformer.
23
+ *
24
+ * @internal
20
25
  */
21
26
  export type FieldTransformerMap = Map<string, FieldTransformer<any>>;
22
27
 
28
+ /**
29
+ * @internal
30
+ */
23
31
  export const getDatabaseFieldForEntityField = <
24
32
  TFields extends Record<string, any>,
25
33
  TIDField extends keyof TFields,
@@ -32,6 +40,9 @@ export const getDatabaseFieldForEntityField = <
32
40
  return databaseField;
33
41
  };
34
42
 
43
+ /**
44
+ * @internal
45
+ */
35
46
  export const transformDatabaseObjectToFields = <
36
47
  TFields extends Record<string, any>,
37
48
  TIDField extends keyof TFields,
@@ -56,6 +67,9 @@ export const transformDatabaseObjectToFields = <
56
67
  return fields;
57
68
  };
58
69
 
70
+ /**
71
+ * @internal
72
+ */
59
73
  export const transformFieldsToDatabaseObject = <
60
74
  TFields extends Record<string, any>,
61
75
  TIDField extends keyof TFields,
@@ -79,6 +93,9 @@ export const transformFieldsToDatabaseObject = <
79
93
  return databaseObject;
80
94
  };
81
95
 
96
+ /**
97
+ * @internal
98
+ */
82
99
  export const transformCacheObjectToFields = <
83
100
  TFields extends Record<string, any>,
84
101
  TIDField extends keyof TFields,
@@ -100,6 +117,9 @@ export const transformCacheObjectToFields = <
100
117
  return fields;
101
118
  };
102
119
 
120
+ /**
121
+ * @internal
122
+ */
103
123
  export const transformFieldsToCacheObject = <
104
124
  TFields extends Record<string, any>,
105
125
  TIDField extends keyof TFields,
@@ -19,6 +19,8 @@ export enum EntityLoadMethodType {
19
19
  /**
20
20
  * Interface responsible for defining how the key and corresponding load values behave in the data manager, cache adapter,
21
21
  * and database adapter during entity field loading.
22
+ *
23
+ * @internal
22
24
  */
23
25
  export interface IEntityLoadKey<
24
26
  TFields extends Record<string, any>,
@@ -117,6 +119,8 @@ export interface IEntityLoadKey<
117
119
 
118
120
  /**
119
121
  * Interface for a load value corresponding to a load key.
122
+ *
123
+ * @internal
120
124
  */
121
125
  export interface IEntityLoadValue<TSerialized> extends ISerializable<TSerialized> {
122
126
  toString(): string;
@@ -124,6 +128,8 @@ export interface IEntityLoadValue<TSerialized> extends ISerializable<TSerialized
124
128
 
125
129
  /**
126
130
  * Map from load value interface to value.
131
+ *
132
+ * @internal
127
133
  */
128
134
  export abstract class LoadValueMap<
129
135
  TSerialized,
@@ -133,6 +139,8 @@ export abstract class LoadValueMap<
133
139
 
134
140
  /**
135
141
  * Load pair type for a load key and load value.
142
+ *
143
+ * @internal
136
144
  */
137
145
  export type LoadPair<
138
146
  TFields extends Record<string, any>,
@@ -12,6 +12,8 @@ import IEntityMetricsAdapter from '../metrics/IEntityMetricsAdapter';
12
12
  * Responsible for orchestrating fetching and caching of entity data from a
13
13
  * table. Note that one instance is shared amongst all entities that read from
14
14
  * the table to ensure cross-entity data consistency.
15
+ *
16
+ * @internal
15
17
  */
16
18
  export default class EntityTableDataCoordinator<
17
19
  TFields extends Record<string, any>,