@expo/entity 0.49.0 → 0.51.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 (46) hide show
  1. package/build/src/AuthorizationResultBasedEntityMutator.d.ts +6 -6
  2. package/build/src/AuthorizationResultBasedEntityMutator.js +9 -2
  3. package/build/src/AuthorizationResultBasedEntityMutator.js.map +1 -1
  4. package/build/src/EntityCompanion.js +1 -1
  5. package/build/src/EntityCompanionProvider.d.ts +4 -4
  6. package/build/src/EntityDatabaseAdapter.d.ts +17 -0
  7. package/build/src/EntityDatabaseAdapter.js +11 -0
  8. package/build/src/EntityDatabaseAdapter.js.map +1 -1
  9. package/build/src/EntityMutationInfo.d.ts +4 -8
  10. package/build/src/EntityMutationValidatorConfiguration.d.ts +26 -0
  11. package/build/src/{EntityMutationValidator.js → EntityMutationValidatorConfiguration.js} +3 -3
  12. package/build/src/EntityMutationValidatorConfiguration.js.map +1 -0
  13. package/build/src/EntityMutatorFactory.d.ts +2 -2
  14. package/build/src/EntityMutatorFactory.js.map +1 -1
  15. package/build/src/IEntityCacheAdapter.js +2 -0
  16. package/build/src/IEntityCacheAdapter.js.map +1 -1
  17. package/build/src/IEntityCacheAdapterProvider.js +2 -0
  18. package/build/src/IEntityCacheAdapterProvider.js.map +1 -1
  19. package/build/src/IEntityDatabaseAdapterProvider.js +2 -0
  20. package/build/src/IEntityDatabaseAdapterProvider.js.map +1 -1
  21. package/build/src/IEntityGenericCacher.js +2 -0
  22. package/build/src/IEntityGenericCacher.js.map +1 -1
  23. package/build/src/index.d.ts +1 -1
  24. package/build/src/index.js +1 -1
  25. package/build/src/index.js.map +1 -1
  26. package/package.json +5 -5
  27. package/src/AuthorizationResultBasedEntityMutator.ts +28 -16
  28. package/src/EntityCompanion.ts +1 -1
  29. package/src/EntityCompanionProvider.ts +5 -5
  30. package/src/EntityDatabaseAdapter.ts +19 -0
  31. package/src/EntityMutationInfo.ts +29 -17
  32. package/src/EntityMutationValidatorConfiguration.ts +64 -0
  33. package/src/EntityMutatorFactory.ts +3 -3
  34. package/src/IEntityCacheAdapter.ts +4 -0
  35. package/src/IEntityCacheAdapterProvider.ts +4 -0
  36. package/src/IEntityDatabaseAdapterProvider.ts +4 -0
  37. package/src/IEntityGenericCacher.ts +4 -0
  38. package/src/__tests__/EntityMutator-test.ts +161 -54
  39. package/src/__tests__/GenericEntityCacheAdapter-test.ts +24 -0
  40. package/src/__tests__/GenericSecondaryEntityCache-test.ts +180 -0
  41. package/src/index.ts +1 -1
  42. package/src/utils/__testfixtures__/PrivacyPolicyRuleTestUtils.ts +2 -2
  43. package/src/utils/__testfixtures__/StubDatabaseAdapter.ts +11 -1
  44. package/build/src/EntityMutationValidator.d.ts +0 -11
  45. package/build/src/EntityMutationValidator.js.map +0 -1
  46. package/src/EntityMutationValidator.ts +0 -29
@@ -30,7 +30,10 @@ import {
30
30
  EntityMutationTriggerConfiguration,
31
31
  EntityNonTransactionalMutationTrigger,
32
32
  } from '../EntityMutationTriggerConfiguration';
33
- import { EntityMutationValidator } from '../EntityMutationValidator';
33
+ import {
34
+ EntityMutationValidator,
35
+ EntityMutationValidatorConfiguration,
36
+ } from '../EntityMutationValidatorConfiguration';
34
37
  import { EntityMutatorFactory } from '../EntityMutatorFactory';
35
38
  import { EntityPrivacyPolicyEvaluationContext } from '../EntityPrivacyPolicy';
36
39
  import { EntityQueryContext, EntityTransactionalQueryContext } from '../EntityQueryContext';
@@ -57,6 +60,27 @@ import {
57
60
  TestFields,
58
61
  } from '../utils/__testfixtures__/TestEntity';
59
62
 
63
+ class TestMutationValidator extends EntityMutationValidator<
64
+ TestFields,
65
+ 'customIdField',
66
+ ViewerContext,
67
+ TestEntity,
68
+ keyof TestFields
69
+ > {
70
+ async executeAsync(
71
+ _viewerContext: ViewerContext,
72
+ _queryContext: EntityQueryContext,
73
+ _entity: TestEntity,
74
+ _mutationInfo: EntityValidatorMutationInfo<
75
+ TestFields,
76
+ 'customIdField',
77
+ ViewerContext,
78
+ TestEntity,
79
+ keyof TestFields
80
+ >,
81
+ ): Promise<void> {}
82
+ }
83
+
60
84
  class TestMutationTrigger extends EntityMutationTrigger<
61
85
  TestFields,
62
86
  'customIdField',
@@ -89,33 +113,48 @@ class TestNonTransactionalMutationTrigger extends EntityNonTransactionalMutation
89
113
  }
90
114
 
91
115
  const setUpMutationValidatorSpies = (
92
- mutationValidators: EntityMutationValidator<
116
+ mutationValidators: EntityMutationValidatorConfiguration<
93
117
  TestFields,
94
118
  'customIdField',
95
119
  ViewerContext,
96
120
  TestEntity,
97
121
  keyof TestFields
98
- >[],
99
- ): EntityMutationValidator<
122
+ >,
123
+ ): EntityMutationValidatorConfiguration<
100
124
  TestFields,
101
125
  'customIdField',
102
126
  ViewerContext,
103
127
  TestEntity,
104
128
  keyof TestFields
105
- >[] => {
106
- return mutationValidators.map((validator) => spy(validator));
129
+ > => {
130
+ return {
131
+ beforeCreateAndUpdate: [spy(mutationValidators.beforeCreateAndUpdate![0]!)],
132
+ beforeDelete: [spy(mutationValidators.beforeDelete![0]!)],
133
+ };
107
134
  };
108
135
 
109
136
  const verifyValidatorCounts = (
110
137
  viewerContext: ViewerContext,
111
- mutationValidatorSpies: EntityMutationValidator<
138
+ mutationValidatorSpies: EntityMutationValidatorConfiguration<
112
139
  TestFields,
113
140
  'customIdField',
114
141
  ViewerContext,
115
142
  TestEntity,
116
143
  keyof TestFields
117
- >[],
118
- expectedCalls: number,
144
+ >,
145
+ executed: Record<
146
+ keyof Pick<
147
+ EntityMutationValidatorConfiguration<
148
+ TestFields,
149
+ 'customIdField',
150
+ ViewerContext,
151
+ TestEntity,
152
+ keyof TestFields
153
+ >,
154
+ 'beforeCreateAndUpdate' | 'beforeDelete'
155
+ >,
156
+ boolean
157
+ >,
119
158
  mutationInfo: EntityValidatorMutationInfo<
120
159
  TestFields,
121
160
  'customIdField',
@@ -124,16 +163,28 @@ const verifyValidatorCounts = (
124
163
  keyof TestFields
125
164
  >,
126
165
  ): void => {
127
- for (const validator of mutationValidatorSpies) {
128
- verify(
129
- validator.executeAsync(
130
- viewerContext,
131
- anyOfClass(EntityTransactionalQueryContext),
132
- anyOfClass(TestEntity),
133
- deepEqual(mutationInfo),
134
- ),
135
- ).times(expectedCalls);
136
- }
166
+ Object.keys(executed).forEach((s) => {
167
+ const sk = s as keyof typeof executed;
168
+ if (executed[sk]) {
169
+ verify(
170
+ mutationValidatorSpies[sk]![0]!.executeAsync(
171
+ viewerContext,
172
+ anyOfClass(EntityTransactionalQueryContext),
173
+ anyOfClass(TestEntity),
174
+ deepEqual(mutationInfo),
175
+ ),
176
+ ).once();
177
+ } else {
178
+ verify(
179
+ mutationValidatorSpies[sk]![0]!.executeAsync(
180
+ viewerContext,
181
+ anyOfClass(EntityTransactionalQueryContext),
182
+ anyOfClass(TestEntity),
183
+ deepEqual(mutationInfo),
184
+ ),
185
+ ).never();
186
+ }
187
+ });
137
188
  };
138
189
 
139
190
  const setUpMutationTriggerSpies = (
@@ -200,9 +251,10 @@ const verifyTriggerCounts = (
200
251
  >,
201
252
  ): void => {
202
253
  Object.keys(executed).forEach((s) => {
203
- if ((executed as any)[s]) {
254
+ const sk = s as keyof typeof executed;
255
+ if (executed[sk]) {
204
256
  verify(
205
- (mutationTriggerSpies as any)[s]![0].executeAsync(
257
+ mutationTriggerSpies[sk]![0]!.executeAsync(
206
258
  viewerContext,
207
259
  anyOfClass(EntityTransactionalQueryContext),
208
260
  anyOfClass(TestEntity),
@@ -211,7 +263,7 @@ const verifyTriggerCounts = (
211
263
  ).once();
212
264
  } else {
213
265
  verify(
214
- (mutationTriggerSpies as any)[s]![0].executeAsync(
266
+ mutationTriggerSpies[sk]![0]!.executeAsync(
215
267
  viewerContext,
216
268
  anyOfClass(EntityTransactionalQueryContext),
217
269
  anyOfClass(TestEntity),
@@ -268,13 +320,13 @@ const createEntityMutatorFactory = (
268
320
  TestEntityPrivacyPolicy
269
321
  >;
270
322
  metricsAdapter: IEntityMetricsAdapter;
271
- mutationValidators: EntityMutationValidator<
323
+ mutationValidators: EntityMutationValidatorConfiguration<
272
324
  TestFields,
273
325
  'customIdField',
274
326
  ViewerContext,
275
327
  TestEntity,
276
328
  keyof TestFields
277
- >[];
329
+ >;
278
330
  mutationTriggers: EntityMutationTriggerConfiguration<
279
331
  TestFields,
280
332
  'customIdField',
@@ -283,13 +335,16 @@ const createEntityMutatorFactory = (
283
335
  keyof TestFields
284
336
  >;
285
337
  } => {
286
- const mutationValidators: EntityMutationValidator<
338
+ const mutationValidators: EntityMutationValidatorConfiguration<
287
339
  TestFields,
288
340
  'customIdField',
289
341
  ViewerContext,
290
342
  TestEntity,
291
343
  keyof TestFields
292
- >[] = [new TestMutationTrigger()];
344
+ > = {
345
+ beforeCreateAndUpdate: [new TestMutationValidator()],
346
+ beforeDelete: [new TestMutationValidator()],
347
+ };
293
348
  const mutationTriggers: EntityMutationTriggerConfiguration<
294
349
  TestFields,
295
350
  'customIdField',
@@ -547,7 +602,15 @@ describe(EntityMutatorFactory, () => {
547
602
  .createAsync(),
548
603
  );
549
604
 
550
- verifyValidatorCounts(viewerContext, validatorSpies, 1, { type: EntityMutationType.CREATE });
605
+ verifyValidatorCounts(
606
+ viewerContext,
607
+ validatorSpies,
608
+ {
609
+ beforeCreateAndUpdate: true,
610
+ beforeDelete: false,
611
+ },
612
+ { type: EntityMutationType.CREATE },
613
+ );
551
614
  });
552
615
  });
553
616
 
@@ -801,11 +864,19 @@ describe(EntityMutatorFactory, () => {
801
864
  .updateAsync(),
802
865
  );
803
866
 
804
- verifyValidatorCounts(viewerContext, validatorSpies, 1, {
805
- type: EntityMutationType.UPDATE,
806
- previousValue: existingEntity,
807
- cascadingDeleteCause: null,
808
- });
867
+ verifyValidatorCounts(
868
+ viewerContext,
869
+ validatorSpies,
870
+ {
871
+ beforeCreateAndUpdate: true,
872
+ beforeDelete: false,
873
+ },
874
+ {
875
+ type: EntityMutationType.UPDATE,
876
+ previousValue: existingEntity,
877
+ cascadingDeleteCause: null,
878
+ },
879
+ );
809
880
  });
810
881
 
811
882
  it('passes manaully-specified cascading delete cause to privacy policy and validators and triggers', async () => {
@@ -893,11 +964,19 @@ describe(EntityMutatorFactory, () => {
893
964
  ),
894
965
  ).once();
895
966
 
896
- verifyValidatorCounts(viewerContext, validatorSpies, 1, {
897
- type: EntityMutationType.UPDATE,
898
- previousValue: existingEntity,
899
- cascadingDeleteCause,
900
- });
967
+ verifyValidatorCounts(
968
+ viewerContext,
969
+ validatorSpies,
970
+ {
971
+ beforeCreateAndUpdate: true,
972
+ beforeDelete: false,
973
+ },
974
+ {
975
+ type: EntityMutationType.UPDATE,
976
+ previousValue: existingEntity,
977
+ cascadingDeleteCause,
978
+ },
979
+ );
901
980
 
902
981
  verifyTriggerCounts(
903
982
  viewerContext,
@@ -1133,7 +1212,7 @@ describe(EntityMutatorFactory, () => {
1133
1212
  );
1134
1213
  });
1135
1214
 
1136
- it('does not execute validators', async () => {
1215
+ it('executes validators', async () => {
1137
1216
  const viewerContext = mock<ViewerContext>();
1138
1217
  const privacyPolicyEvaluationContext =
1139
1218
  instance(
@@ -1176,9 +1255,18 @@ describe(EntityMutatorFactory, () => {
1176
1255
  .deleteAsync(),
1177
1256
  );
1178
1257
 
1179
- verifyValidatorCounts(viewerContext, validatorSpies, 0, {
1180
- type: EntityMutationType.DELETE as any,
1181
- });
1258
+ verifyValidatorCounts(
1259
+ viewerContext,
1260
+ validatorSpies,
1261
+ {
1262
+ beforeCreateAndUpdate: false,
1263
+ beforeDelete: true,
1264
+ },
1265
+ {
1266
+ type: EntityMutationType.DELETE,
1267
+ cascadingDeleteCause: null,
1268
+ },
1269
+ );
1182
1270
  });
1183
1271
 
1184
1272
  it('passes manaully-specified cascading delete cause to privacy policy and triggers', async () => {
@@ -1198,20 +1286,26 @@ describe(EntityMutatorFactory, () => {
1198
1286
  const queryContext = new StubQueryContextProvider().getQueryContext();
1199
1287
 
1200
1288
  const id1 = uuidv4();
1201
- const { mutationTriggers, privacyPolicy, entityMutatorFactory, entityLoaderFactory } =
1202
- createEntityMutatorFactory([
1203
- {
1204
- customIdField: id1,
1205
- stringField: 'huh',
1206
- testIndexedField: '3',
1207
- intField: 3,
1208
- dateField: new Date(),
1209
- nullableField: null,
1210
- },
1211
- ]);
1289
+ const {
1290
+ mutationTriggers,
1291
+ mutationValidators,
1292
+ privacyPolicy,
1293
+ entityMutatorFactory,
1294
+ entityLoaderFactory,
1295
+ } = createEntityMutatorFactory([
1296
+ {
1297
+ customIdField: id1,
1298
+ stringField: 'huh',
1299
+ testIndexedField: '3',
1300
+ intField: 3,
1301
+ dateField: new Date(),
1302
+ nullableField: null,
1303
+ },
1304
+ ]);
1212
1305
 
1213
1306
  const spiedPrivacyPolicy = spy(privacyPolicy);
1214
1307
  const triggerSpies = setUpMutationTriggerSpies(mutationTriggers);
1308
+ const validatorSpies = setUpMutationValidatorSpies(mutationValidators);
1215
1309
 
1216
1310
  const existingEntity = await enforceAsyncResult(
1217
1311
  entityLoaderFactory
@@ -1240,6 +1334,19 @@ describe(EntityMutatorFactory, () => {
1240
1334
  ),
1241
1335
  ).once();
1242
1336
 
1337
+ verifyValidatorCounts(
1338
+ viewerContext,
1339
+ validatorSpies,
1340
+ {
1341
+ beforeCreateAndUpdate: false,
1342
+ beforeDelete: true,
1343
+ },
1344
+ {
1345
+ type: EntityMutationType.DELETE,
1346
+ cascadingDeleteCause,
1347
+ },
1348
+ );
1349
+
1243
1350
  verifyTriggerCounts(
1244
1351
  viewerContext,
1245
1352
  triggerSpies,
@@ -1462,7 +1569,7 @@ describe(EntityMutatorFactory, () => {
1462
1569
  simpleTestEntityConfiguration,
1463
1570
  SimpleTestEntity,
1464
1571
  instance(privacyPolicyMock),
1465
- [],
1572
+ {},
1466
1573
  {},
1467
1574
  entityLoaderFactory,
1468
1575
  databaseAdapter,
@@ -1588,7 +1695,7 @@ describe(EntityMutatorFactory, () => {
1588
1695
  simpleTestEntityConfiguration,
1589
1696
  SimpleTestEntity,
1590
1697
  privacyPolicy,
1591
- [],
1698
+ {},
1592
1699
  {},
1593
1700
  entityLoaderFactory,
1594
1701
  instance(databaseAdapterMock),
@@ -3,6 +3,7 @@ import { anything, deepEqual, instance, mock, verify, when } from 'ts-mockito';
3
3
 
4
4
  import { GenericEntityCacheAdapter } from '../GenericEntityCacheAdapter';
5
5
  import { IEntityGenericCacher } from '../IEntityGenericCacher';
6
+ import { EntityCacheAdapterTransientError } from '../errors/EntityCacheAdapterError';
6
7
  import { CacheStatus } from '../internal/ReadThroughEntityCache';
7
8
  import {
8
9
  SingleFieldHolder,
@@ -68,6 +69,29 @@ describe(GenericEntityCacheAdapter, () => {
68
69
  );
69
70
  expect(results).toEqual(new SingleFieldValueHolderMap(new Map()));
70
71
  });
72
+
73
+ it('rethrows EntityCacheAdapterTransientError from underlying cacher', async () => {
74
+ const mockGenericCacher = mock<IEntityGenericCacher<BlahFields, 'id'>>();
75
+ when(
76
+ mockGenericCacher.makeCacheKeyForStorage(
77
+ deepEqualEntityAware(new SingleFieldHolder('id')),
78
+ anything(),
79
+ ),
80
+ ).thenCall((fieldHolder, fieldValueHolder) => {
81
+ return `${fieldHolder.fieldName}.${fieldValueHolder.fieldValue}`;
82
+ });
83
+ const expectedError = new EntityCacheAdapterTransientError('Transient error');
84
+ when(mockGenericCacher.loadManyAsync(deepEqual(['id.wat']))).thenReject(expectedError);
85
+
86
+ const cacheAdapter = new GenericEntityCacheAdapter(instance(mockGenericCacher));
87
+ await expect(
88
+ cacheAdapter.loadManyAsync(new SingleFieldHolder('id'), [
89
+ new SingleFieldValueHolder('wat'),
90
+ ]),
91
+ ).rejects.toThrow(expectedError);
92
+
93
+ verify(mockGenericCacher.loadManyAsync(anything())).once();
94
+ });
71
95
  });
72
96
 
73
97
  describe('cacheManyAsync', () => {
@@ -0,0 +1,180 @@
1
+ import { describe, it, expect } from '@jest/globals';
2
+ import nullthrows from 'nullthrows';
3
+
4
+ import { EntitySecondaryCacheLoader } from '../EntitySecondaryCacheLoader';
5
+ import { GenericSecondaryEntityCache } from '../GenericSecondaryEntityCache';
6
+ import { IEntityGenericCacher } from '../IEntityGenericCacher';
7
+ import { ViewerContext } from '../ViewerContext';
8
+ import { IEntityLoadKey, IEntityLoadValue } from '../internal/EntityLoadInterfaces';
9
+ import { CacheLoadResult, CacheStatus } from '../internal/ReadThroughEntityCache';
10
+ import {
11
+ TestEntity,
12
+ TestEntityPrivacyPolicy,
13
+ TestFields,
14
+ } from '../utils/__testfixtures__/TestEntity';
15
+ import { createUnitTestEntityCompanionProvider } from '../utils/__testfixtures__/createUnitTestEntityCompanionProvider';
16
+ import { mapMapAsync } from '../utils/collections/maps';
17
+
18
+ type TestLoadParams = { intValue: number };
19
+
20
+ const DOES_NOT_EXIST = Symbol('doesNotExist');
21
+
22
+ class TestGenericCacher implements IEntityGenericCacher<TestFields, 'customIdField'> {
23
+ private readonly localMemoryCache = new Map<
24
+ string,
25
+ Readonly<TestFields> | typeof DOES_NOT_EXIST
26
+ >();
27
+
28
+ public async loadManyAsync(
29
+ keys: readonly string[],
30
+ ): Promise<ReadonlyMap<string, CacheLoadResult<TestFields>>> {
31
+ const cacheResults = new Map<string, CacheLoadResult<TestFields>>();
32
+ for (const key of keys) {
33
+ const cacheResult = this.localMemoryCache.get(key);
34
+ if (cacheResult === DOES_NOT_EXIST) {
35
+ cacheResults.set(key, {
36
+ status: CacheStatus.NEGATIVE,
37
+ });
38
+ } else if (cacheResult) {
39
+ cacheResults.set(key, {
40
+ status: CacheStatus.HIT,
41
+ item: cacheResult as unknown as TestFields,
42
+ });
43
+ } else {
44
+ cacheResults.set(key, {
45
+ status: CacheStatus.MISS,
46
+ });
47
+ }
48
+ }
49
+ return cacheResults;
50
+ }
51
+
52
+ public async cacheManyAsync(objectMap: ReadonlyMap<string, Readonly<TestFields>>): Promise<void> {
53
+ for (const [key, item] of objectMap) {
54
+ this.localMemoryCache.set(key, item);
55
+ }
56
+ }
57
+
58
+ public async cacheDBMissesAsync(keys: readonly string[]): Promise<void> {
59
+ for (const key of keys) {
60
+ this.localMemoryCache.set(key, DOES_NOT_EXIST);
61
+ }
62
+ }
63
+
64
+ public async invalidateManyAsync(keys: readonly string[]): Promise<void> {
65
+ for (const key of keys) {
66
+ this.localMemoryCache.delete(key);
67
+ }
68
+ }
69
+
70
+ makeCacheKeyForStorage<
71
+ TLoadKey extends IEntityLoadKey<TestFields, 'customIdField', TSerializedLoadValue, TLoadValue>,
72
+ TSerializedLoadValue,
73
+ TLoadValue extends IEntityLoadValue<TSerializedLoadValue>,
74
+ >(_key: TLoadKey, _value: TLoadValue): string {
75
+ throw new Error('Method not used by this test.');
76
+ }
77
+
78
+ makeCacheKeysForInvalidation<
79
+ TLoadKey extends IEntityLoadKey<TestFields, 'customIdField', TSerializedLoadValue, TLoadValue>,
80
+ TSerializedLoadValue,
81
+ TLoadValue extends IEntityLoadValue<TSerializedLoadValue>,
82
+ >(_key: TLoadKey, _value: TLoadValue): readonly string[] {
83
+ throw new Error('Method not used by this test.');
84
+ }
85
+ }
86
+
87
+ class TestSecondaryEntityCache<
88
+ TFields extends Record<string, any>,
89
+ TIDField extends keyof TFields,
90
+ TLoadParams,
91
+ > extends GenericSecondaryEntityCache<TFields, TIDField, TLoadParams> {}
92
+
93
+ class TestSecondaryCacheLoader extends EntitySecondaryCacheLoader<
94
+ TestLoadParams,
95
+ TestFields,
96
+ 'customIdField',
97
+ ViewerContext,
98
+ TestEntity,
99
+ TestEntityPrivacyPolicy
100
+ > {
101
+ public databaseLoadCount = 0;
102
+
103
+ protected override async fetchObjectsFromDatabaseAsync(
104
+ loadParamsArray: readonly Readonly<TestLoadParams>[],
105
+ ): Promise<ReadonlyMap<Readonly<Readonly<TestLoadParams>>, Readonly<TestFields> | null>> {
106
+ this.databaseLoadCount += loadParamsArray.length;
107
+
108
+ const emptyMap = new Map(loadParamsArray.map((p) => [p, null]));
109
+ return await mapMapAsync(emptyMap, async (_value, loadParams) => {
110
+ return (
111
+ (
112
+ await this.entityLoader.loadManyByFieldEqualityConjunctionAsync([
113
+ { fieldName: 'intField', fieldValue: loadParams.intValue },
114
+ ])
115
+ )[0]
116
+ ?.enforceValue()
117
+ ?.getAllFields() ?? null
118
+ );
119
+ });
120
+ }
121
+ }
122
+
123
+ describe(GenericSecondaryEntityCache, () => {
124
+ it('Loads through secondary loader, caches, and invalidates', async () => {
125
+ const viewerContext = new ViewerContext(createUnitTestEntityCompanionProvider());
126
+
127
+ const createdEntity = await TestEntity.creator(viewerContext)
128
+ .setField('intField', 1)
129
+ .createAsync();
130
+
131
+ const secondaryCacheLoader = new TestSecondaryCacheLoader(
132
+ new TestSecondaryEntityCache(
133
+ new TestGenericCacher(),
134
+ (params) => `intValue.${params.intValue}`,
135
+ ),
136
+ TestEntity.loaderWithAuthorizationResults(viewerContext),
137
+ );
138
+
139
+ const loadParams = { intValue: 1 };
140
+ const results = await secondaryCacheLoader.loadManyAsync([loadParams]);
141
+ expect(nullthrows(results.get(loadParams)).enforceValue().getID()).toEqual(
142
+ createdEntity.getID(),
143
+ );
144
+
145
+ expect(secondaryCacheLoader.databaseLoadCount).toEqual(1);
146
+
147
+ const results2 = await secondaryCacheLoader.loadManyAsync([loadParams]);
148
+ expect(nullthrows(results2.get(loadParams)).enforceValue().getID()).toEqual(
149
+ createdEntity.getID(),
150
+ );
151
+
152
+ expect(secondaryCacheLoader.databaseLoadCount).toEqual(1);
153
+
154
+ await secondaryCacheLoader.invalidateManyAsync([loadParams]);
155
+
156
+ const results3 = await secondaryCacheLoader.loadManyAsync([loadParams]);
157
+ expect(nullthrows(results3.get(loadParams)).enforceValue().getID()).toEqual(
158
+ createdEntity.getID(),
159
+ );
160
+
161
+ expect(secondaryCacheLoader.databaseLoadCount).toEqual(2);
162
+ });
163
+
164
+ it('correctly handles uncached and unfetchable load params', async () => {
165
+ const viewerContext = new ViewerContext(createUnitTestEntityCompanionProvider());
166
+
167
+ const secondaryCacheLoader = new TestSecondaryCacheLoader(
168
+ new TestSecondaryEntityCache(
169
+ new TestGenericCacher(),
170
+ (params) => `intValue.${params.intValue}`,
171
+ ),
172
+ TestEntity.loaderWithAuthorizationResults(viewerContext),
173
+ );
174
+
175
+ const loadParams = { intValue: 2 };
176
+ const results = await secondaryCacheLoader.loadManyAsync([loadParams]);
177
+ expect(results.size).toBe(1);
178
+ expect(results.get(loadParams)).toBe(null);
179
+ });
180
+ });
package/src/index.ts CHANGED
@@ -29,7 +29,7 @@ export * from './EntityLoaderFactory';
29
29
  export * from './EntityLoaderUtils';
30
30
  export * from './EntityMutationInfo';
31
31
  export * from './EntityMutationTriggerConfiguration';
32
- export * from './EntityMutationValidator';
32
+ export * from './EntityMutationValidatorConfiguration';
33
33
  export * from './EntityMutatorFactory';
34
34
  export * from './EntityPrivacyPolicy';
35
35
  export * from './EntityQueryContext';
@@ -11,7 +11,7 @@ export interface Case<
11
11
  TIDField extends keyof NonNullable<Pick<TFields, TSelectedFields>>,
12
12
  TViewerContext extends ViewerContext,
13
13
  TEntity extends ReadonlyEntity<TFields, TIDField, TViewerContext, TSelectedFields>,
14
- TSelectedFields extends keyof TFields,
14
+ TSelectedFields extends keyof TFields = keyof TFields,
15
15
  > {
16
16
  viewerContext: TViewerContext;
17
17
  queryContext: EntityQueryContext;
@@ -30,7 +30,7 @@ export type CaseMap<
30
30
  TIDField extends keyof NonNullable<Pick<TFields, TSelectedFields>>,
31
31
  TViewerContext extends ViewerContext,
32
32
  TEntity extends ReadonlyEntity<TFields, TIDField, TViewerContext, TSelectedFields>,
33
- TSelectedFields extends keyof TFields,
33
+ TSelectedFields extends keyof TFields = keyof TFields,
34
34
  > = Map<string, () => Promise<Case<TFields, TIDField, TViewerContext, TEntity, TSelectedFields>>>;
35
35
 
36
36
  /**
@@ -234,7 +234,13 @@ export class StubDatabaseAdapter<
234
234
  const objectIndex = objectCollection.findIndex((obj) => {
235
235
  return obj[tableIdField] === id;
236
236
  });
237
- invariant(objectIndex >= 0, 'should exist');
237
+
238
+ // SQL updates to a nonexistent row succeed but affect 0 rows,
239
+ // mirror that behavior here for better test simulation
240
+ if (objectIndex < 0) {
241
+ return [];
242
+ }
243
+
238
244
  objectCollection[objectIndex] = {
239
245
  ...objectCollection[objectIndex],
240
246
  ...object,
@@ -253,9 +259,13 @@ export class StubDatabaseAdapter<
253
259
  const objectIndex = objectCollection.findIndex((obj) => {
254
260
  return obj[tableIdField] === id;
255
261
  });
262
+
263
+ // SQL deletes to a nonexistent row succeed and affect 0 rows,
264
+ // mirror that behavior here for better test simulation
256
265
  if (objectIndex < 0) {
257
266
  return 0;
258
267
  }
268
+
259
269
  objectCollection.splice(objectIndex, 1);
260
270
  return 1;
261
271
  }
@@ -1,11 +0,0 @@
1
- import { EntityValidatorMutationInfo } from './EntityMutationInfo';
2
- import { EntityTransactionalQueryContext } from './EntityQueryContext';
3
- import { ReadonlyEntity } from './ReadonlyEntity';
4
- import { ViewerContext } from './ViewerContext';
5
- /**
6
- * A validator is a way to specify entity mutation validation that runs within the
7
- * same transaction as the mutation itself before creating or updating an entity.
8
- */
9
- export declare abstract class EntityMutationValidator<TFields extends Record<string, any>, TIDField extends keyof NonNullable<Pick<TFields, TSelectedFields>>, TViewerContext extends ViewerContext, TEntity extends ReadonlyEntity<TFields, TIDField, TViewerContext, TSelectedFields>, TSelectedFields extends keyof TFields = keyof TFields> {
10
- abstract executeAsync(viewerContext: TViewerContext, queryContext: EntityTransactionalQueryContext, entity: TEntity, mutationInfo: EntityValidatorMutationInfo<TFields, TIDField, TViewerContext, TEntity, TSelectedFields>): Promise<void>;
11
- }
@@ -1 +0,0 @@
1
- {"version":3,"file":"EntityMutationValidator.js","sourceRoot":"","sources":["../../src/EntityMutationValidator.ts"],"names":[],"mappings":";;;AAKA;;;GAGG;AACH,MAAsB,uBAAuB;CAmB5C;AAnBD,0DAmBC"}
@@ -1,29 +0,0 @@
1
- import { EntityValidatorMutationInfo } from './EntityMutationInfo';
2
- import { EntityTransactionalQueryContext } from './EntityQueryContext';
3
- import { ReadonlyEntity } from './ReadonlyEntity';
4
- import { ViewerContext } from './ViewerContext';
5
-
6
- /**
7
- * A validator is a way to specify entity mutation validation that runs within the
8
- * same transaction as the mutation itself before creating or updating an entity.
9
- */
10
- export abstract class EntityMutationValidator<
11
- TFields extends Record<string, any>,
12
- TIDField extends keyof NonNullable<Pick<TFields, TSelectedFields>>,
13
- TViewerContext extends ViewerContext,
14
- TEntity extends ReadonlyEntity<TFields, TIDField, TViewerContext, TSelectedFields>,
15
- TSelectedFields extends keyof TFields = keyof TFields,
16
- > {
17
- abstract executeAsync(
18
- viewerContext: TViewerContext,
19
- queryContext: EntityTransactionalQueryContext,
20
- entity: TEntity,
21
- mutationInfo: EntityValidatorMutationInfo<
22
- TFields,
23
- TIDField,
24
- TViewerContext,
25
- TEntity,
26
- TSelectedFields
27
- >,
28
- ): Promise<void>;
29
- }