@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.
- package/build/src/AuthorizationResultBasedEntityMutator.d.ts +6 -6
- package/build/src/AuthorizationResultBasedEntityMutator.js +9 -2
- package/build/src/AuthorizationResultBasedEntityMutator.js.map +1 -1
- package/build/src/EntityCompanion.js +1 -1
- package/build/src/EntityCompanionProvider.d.ts +4 -4
- package/build/src/EntityDatabaseAdapter.d.ts +17 -0
- package/build/src/EntityDatabaseAdapter.js +11 -0
- package/build/src/EntityDatabaseAdapter.js.map +1 -1
- package/build/src/EntityMutationInfo.d.ts +4 -8
- package/build/src/EntityMutationValidatorConfiguration.d.ts +26 -0
- package/build/src/{EntityMutationValidator.js → EntityMutationValidatorConfiguration.js} +3 -3
- package/build/src/EntityMutationValidatorConfiguration.js.map +1 -0
- package/build/src/EntityMutatorFactory.d.ts +2 -2
- package/build/src/EntityMutatorFactory.js.map +1 -1
- package/build/src/IEntityCacheAdapter.js +2 -0
- package/build/src/IEntityCacheAdapter.js.map +1 -1
- package/build/src/IEntityCacheAdapterProvider.js +2 -0
- package/build/src/IEntityCacheAdapterProvider.js.map +1 -1
- package/build/src/IEntityDatabaseAdapterProvider.js +2 -0
- package/build/src/IEntityDatabaseAdapterProvider.js.map +1 -1
- package/build/src/IEntityGenericCacher.js +2 -0
- package/build/src/IEntityGenericCacher.js.map +1 -1
- package/build/src/index.d.ts +1 -1
- package/build/src/index.js +1 -1
- package/build/src/index.js.map +1 -1
- package/package.json +5 -5
- package/src/AuthorizationResultBasedEntityMutator.ts +28 -16
- package/src/EntityCompanion.ts +1 -1
- package/src/EntityCompanionProvider.ts +5 -5
- package/src/EntityDatabaseAdapter.ts +19 -0
- package/src/EntityMutationInfo.ts +29 -17
- package/src/EntityMutationValidatorConfiguration.ts +64 -0
- package/src/EntityMutatorFactory.ts +3 -3
- package/src/IEntityCacheAdapter.ts +4 -0
- package/src/IEntityCacheAdapterProvider.ts +4 -0
- package/src/IEntityDatabaseAdapterProvider.ts +4 -0
- package/src/IEntityGenericCacher.ts +4 -0
- package/src/__tests__/EntityMutator-test.ts +161 -54
- package/src/__tests__/GenericEntityCacheAdapter-test.ts +24 -0
- package/src/__tests__/GenericSecondaryEntityCache-test.ts +180 -0
- package/src/index.ts +1 -1
- package/src/utils/__testfixtures__/PrivacyPolicyRuleTestUtils.ts +2 -2
- package/src/utils/__testfixtures__/StubDatabaseAdapter.ts +11 -1
- package/build/src/EntityMutationValidator.d.ts +0 -11
- package/build/src/EntityMutationValidator.js.map +0 -1
- package/src/EntityMutationValidator.ts +0 -29
|
@@ -30,7 +30,10 @@ import {
|
|
|
30
30
|
EntityMutationTriggerConfiguration,
|
|
31
31
|
EntityNonTransactionalMutationTrigger,
|
|
32
32
|
} from '../EntityMutationTriggerConfiguration';
|
|
33
|
-
import {
|
|
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:
|
|
116
|
+
mutationValidators: EntityMutationValidatorConfiguration<
|
|
93
117
|
TestFields,
|
|
94
118
|
'customIdField',
|
|
95
119
|
ViewerContext,
|
|
96
120
|
TestEntity,
|
|
97
121
|
keyof TestFields
|
|
98
|
-
|
|
99
|
-
):
|
|
122
|
+
>,
|
|
123
|
+
): EntityMutationValidatorConfiguration<
|
|
100
124
|
TestFields,
|
|
101
125
|
'customIdField',
|
|
102
126
|
ViewerContext,
|
|
103
127
|
TestEntity,
|
|
104
128
|
keyof TestFields
|
|
105
|
-
>
|
|
106
|
-
return
|
|
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:
|
|
138
|
+
mutationValidatorSpies: EntityMutationValidatorConfiguration<
|
|
112
139
|
TestFields,
|
|
113
140
|
'customIdField',
|
|
114
141
|
ViewerContext,
|
|
115
142
|
TestEntity,
|
|
116
143
|
keyof TestFields
|
|
117
|
-
|
|
118
|
-
|
|
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
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
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
|
-
|
|
254
|
+
const sk = s as keyof typeof executed;
|
|
255
|
+
if (executed[sk]) {
|
|
204
256
|
verify(
|
|
205
|
-
|
|
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
|
-
|
|
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:
|
|
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:
|
|
338
|
+
const mutationValidators: EntityMutationValidatorConfiguration<
|
|
287
339
|
TestFields,
|
|
288
340
|
'customIdField',
|
|
289
341
|
ViewerContext,
|
|
290
342
|
TestEntity,
|
|
291
343
|
keyof TestFields
|
|
292
|
-
>
|
|
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(
|
|
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(
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
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(
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
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('
|
|
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(
|
|
1180
|
-
|
|
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 {
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
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 './
|
|
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
|
-
|
|
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
|
-
}
|