@expo/entity 0.22.0 → 0.25.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/ComposedEntityCacheAdapter.d.ts +19 -0
- package/build/ComposedEntityCacheAdapter.js +66 -0
- package/build/ComposedEntityCacheAdapter.js.map +1 -0
- package/build/ComposedSecondaryEntityCache.d.ts +15 -0
- package/build/ComposedSecondaryEntityCache.js +37 -0
- package/build/ComposedSecondaryEntityCache.js.map +1 -0
- package/build/Entity.js +6 -6
- package/build/Entity.js.map +1 -1
- package/build/EntityAssociationLoader.js +4 -4
- package/build/EntityAssociationLoader.js.map +1 -1
- package/build/EntityCacheAdapter.d.ts +2 -9
- package/build/EntityCacheAdapter.js.map +1 -1
- package/build/EntityFieldDefinition.d.ts +8 -0
- package/build/EntityFieldDefinition.js +5 -0
- package/build/EntityFieldDefinition.js.map +1 -1
- package/build/EntityFields.d.ts +38 -0
- package/build/EntityFields.js +38 -0
- package/build/EntityFields.js.map +1 -1
- package/build/EntityLoader.d.ts +3 -2
- package/build/EntityLoader.js +5 -4
- package/build/EntityLoader.js.map +1 -1
- package/build/EntityLoaderFactory.d.ts +2 -2
- package/build/EntityLoaderFactory.js +2 -2
- package/build/EntityLoaderFactory.js.map +1 -1
- package/build/EntityMutationInfo.d.ts +12 -3
- package/build/EntityMutator.d.ts +5 -4
- package/build/EntityMutator.js +31 -24
- package/build/EntityMutator.js.map +1 -1
- package/build/EntityMutatorFactory.d.ts +4 -4
- package/build/EntityMutatorFactory.js +6 -6
- package/build/EntityMutatorFactory.js.map +1 -1
- package/build/EntityPrivacyPolicy.d.ts +15 -4
- package/build/EntityPrivacyPolicy.js +14 -14
- package/build/EntityPrivacyPolicy.js.map +1 -1
- package/build/GenericSecondaryEntityCache.d.ts +19 -0
- package/build/GenericSecondaryEntityCache.js +74 -0
- package/build/GenericSecondaryEntityCache.js.map +1 -0
- package/build/IEntityGenericCacher.d.ts +11 -0
- package/build/IEntityGenericCacher.js +3 -0
- package/build/IEntityGenericCacher.js.map +1 -0
- package/build/ReadonlyEntity.js +1 -1
- package/build/ReadonlyEntity.js.map +1 -1
- package/build/ViewerScopedEntityLoaderFactory.d.ts +2 -2
- package/build/ViewerScopedEntityLoaderFactory.js +2 -2
- package/build/ViewerScopedEntityLoaderFactory.js.map +1 -1
- package/build/ViewerScopedEntityMutatorFactory.d.ts +4 -4
- package/build/ViewerScopedEntityMutatorFactory.js +6 -6
- package/build/ViewerScopedEntityMutatorFactory.js.map +1 -1
- package/build/__tests__/ComposedCacheAdapter-test.d.ts +1 -0
- package/build/__tests__/ComposedCacheAdapter-test.js +198 -0
- package/build/__tests__/ComposedCacheAdapter-test.js.map +1 -0
- package/build/__tests__/ComposedSecondaryEntityCache-test.d.ts +1 -0
- package/build/__tests__/ComposedSecondaryEntityCache-test.js +65 -0
- package/build/__tests__/ComposedSecondaryEntityCache-test.js.map +1 -0
- package/build/__tests__/EntityCommonUseCases-test.js +1 -1
- package/build/__tests__/EntityCommonUseCases-test.js.map +1 -1
- package/build/__tests__/EntityEdges-test.js +260 -37
- package/build/__tests__/EntityEdges-test.js.map +1 -1
- package/build/__tests__/EntityLoader-constructor-test.js +2 -1
- package/build/__tests__/EntityLoader-constructor-test.js.map +1 -1
- package/build/__tests__/EntityLoader-test.js +19 -11
- package/build/__tests__/EntityLoader-test.js.map +1 -1
- package/build/__tests__/EntityMutator-test.js +96 -43
- package/build/__tests__/EntityMutator-test.js.map +1 -1
- package/build/__tests__/EntityPrivacyPolicy-test.js +23 -12
- package/build/__tests__/EntityPrivacyPolicy-test.js.map +1 -1
- package/build/__tests__/EntitySecondaryCacheLoader-test.js +1 -1
- package/build/__tests__/EntitySecondaryCacheLoader-test.js.map +1 -1
- package/build/__tests__/ViewerScopedEntityLoaderFactory-test.js +3 -2
- package/build/__tests__/ViewerScopedEntityLoaderFactory-test.js.map +1 -1
- package/build/__tests__/ViewerScopedEntityMutatorFactory-test.js +3 -2
- package/build/__tests__/ViewerScopedEntityMutatorFactory-test.js.map +1 -1
- package/build/index.d.ts +2 -0
- package/build/index.js +3 -1
- package/build/index.js.map +1 -1
- package/build/internal/ReadThroughEntityCache.d.ts +2 -3
- package/build/internal/ReadThroughEntityCache.js +2 -6
- package/build/internal/ReadThroughEntityCache.js.map +1 -1
- package/build/internal/__tests__/ReadThroughEntityCache-test.js +0 -32
- package/build/internal/__tests__/ReadThroughEntityCache-test.js.map +1 -1
- package/build/rules/AlwaysAllowPrivacyPolicyRule.d.ts +2 -1
- package/build/rules/AlwaysAllowPrivacyPolicyRule.js +1 -1
- package/build/rules/AlwaysAllowPrivacyPolicyRule.js.map +1 -1
- package/build/rules/AlwaysDenyPrivacyPolicyRule.d.ts +2 -1
- package/build/rules/AlwaysDenyPrivacyPolicyRule.js +1 -1
- package/build/rules/AlwaysDenyPrivacyPolicyRule.js.map +1 -1
- package/build/rules/AlwaysSkipPrivacyPolicyRule.d.ts +2 -1
- package/build/rules/AlwaysSkipPrivacyPolicyRule.js +1 -1
- package/build/rules/AlwaysSkipPrivacyPolicyRule.js.map +1 -1
- package/build/rules/PrivacyPolicyRule.d.ts +2 -1
- package/build/rules/PrivacyPolicyRule.js.map +1 -1
- package/build/rules/__tests__/AlwaysAllowPrivacyPolicyRule-test.js +1 -0
- package/build/rules/__tests__/AlwaysAllowPrivacyPolicyRule-test.js.map +1 -1
- package/build/rules/__tests__/AlwaysDenyPrivacyPolicyRule-test.js +1 -0
- package/build/rules/__tests__/AlwaysDenyPrivacyPolicyRule-test.js.map +1 -1
- package/build/rules/__tests__/AlwaysSkipPrivacyPolicyRule-test.js +1 -0
- package/build/rules/__tests__/AlwaysSkipPrivacyPolicyRule-test.js.map +1 -1
- package/build/utils/testing/PrivacyPolicyRuleTestUtils.d.ts +2 -0
- package/build/utils/testing/PrivacyPolicyRuleTestUtils.js +6 -6
- package/build/utils/testing/PrivacyPolicyRuleTestUtils.js.map +1 -1
- package/build/utils/testing/StubCacheAdapter.d.ts +6 -9
- package/build/utils/testing/StubCacheAdapter.js +0 -6
- package/build/utils/testing/StubCacheAdapter.js.map +1 -1
- package/package.json +1 -1
- package/src/ComposedEntityCacheAdapter.ts +86 -0
- package/src/ComposedSecondaryEntityCache.ts +63 -0
- package/src/Entity.ts +6 -4
- package/src/EntityAssociationLoader.ts +4 -4
- package/src/EntityCacheAdapter.ts +2 -10
- package/src/EntityFieldDefinition.ts +8 -0
- package/src/EntityFields.ts +45 -0
- package/src/EntityLoader.ts +5 -1
- package/src/EntityLoaderFactory.ts +4 -2
- package/src/EntityMutationInfo.ts +13 -3
- package/src/EntityMutator.ts +44 -21
- package/src/EntityMutatorFactory.ts +10 -4
- package/src/EntityPrivacyPolicy.ts +31 -1
- package/src/GenericSecondaryEntityCache.ts +98 -0
- package/src/IEntityGenericCacher.ts +15 -0
- package/src/ReadonlyEntity.ts +1 -1
- package/src/ViewerScopedEntityLoaderFactory.ts +8 -3
- package/src/ViewerScopedEntityMutatorFactory.ts +22 -7
- package/src/__tests__/ComposedCacheAdapter-test.ts +280 -0
- package/src/__tests__/ComposedSecondaryEntityCache-test.ts +101 -0
- package/src/__tests__/EntityCommonUseCases-test.ts +2 -1
- package/src/__tests__/EntityEdges-test.ts +286 -45
- package/src/__tests__/EntityLoader-constructor-test.ts +3 -1
- package/src/__tests__/EntityLoader-test.ts +26 -1
- package/src/__tests__/EntityMutator-test.ts +99 -37
- package/src/__tests__/EntityPrivacyPolicy-test.ts +66 -7
- package/src/__tests__/EntitySecondaryCacheLoader-test.ts +1 -0
- package/src/__tests__/ViewerScopedEntityLoaderFactory-test.ts +4 -2
- package/src/__tests__/ViewerScopedEntityMutatorFactory-test.ts +6 -2
- package/src/index.ts +2 -0
- package/src/internal/ReadThroughEntityCache.ts +6 -28
- package/src/internal/__tests__/ReadThroughEntityCache-test.ts +0 -44
- package/src/rules/AlwaysAllowPrivacyPolicyRule.ts +2 -0
- package/src/rules/AlwaysDenyPrivacyPolicyRule.ts +2 -0
- package/src/rules/AlwaysSkipPrivacyPolicyRule.ts +2 -0
- package/src/rules/PrivacyPolicyRule.ts +2 -0
- package/src/rules/__tests__/AlwaysAllowPrivacyPolicyRule-test.ts +2 -0
- package/src/rules/__tests__/AlwaysDenyPrivacyPolicyRule-test.ts +2 -0
- package/src/rules/__tests__/AlwaysSkipPrivacyPolicyRule-test.ts +2 -0
- package/src/utils/testing/PrivacyPolicyRuleTestUtils.ts +14 -6
- package/src/utils/testing/StubCacheAdapter.ts +11 -17
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { EntityCascadingDeletionInfo } from './EntityMutationInfo';
|
|
1
2
|
import { EntityQueryContext } from './EntityQueryContext';
|
|
2
3
|
import ReadonlyEntity from './ReadonlyEntity';
|
|
3
4
|
import ViewerContext from './ViewerContext';
|
|
@@ -7,6 +8,17 @@ import IEntityMetricsAdapter, {
|
|
|
7
8
|
} from './metrics/IEntityMetricsAdapter';
|
|
8
9
|
import PrivacyPolicyRule, { RuleEvaluationResult } from './rules/PrivacyPolicyRule';
|
|
9
10
|
|
|
11
|
+
/**
|
|
12
|
+
* Information about the reason this privacy policy is being evaluated.
|
|
13
|
+
*/
|
|
14
|
+
export type EntityPrivacyPolicyEvaluationContext = {
|
|
15
|
+
/**
|
|
16
|
+
* When this privacy policy is being evaluated as a result of a cascading deletion, this will be populated
|
|
17
|
+
* with information on the cascading delete.
|
|
18
|
+
*/
|
|
19
|
+
cascadingDeleteCause: EntityCascadingDeletionInfo | null;
|
|
20
|
+
};
|
|
21
|
+
|
|
10
22
|
export enum EntityPrivacyPolicyEvaluationMode {
|
|
11
23
|
ENFORCE,
|
|
12
24
|
DRY_RUN,
|
|
@@ -127,6 +139,7 @@ export default abstract class EntityPrivacyPolicy<
|
|
|
127
139
|
async authorizeCreateAsync(
|
|
128
140
|
viewerContext: TViewerContext,
|
|
129
141
|
queryContext: EntityQueryContext,
|
|
142
|
+
evaluationContext: EntityPrivacyPolicyEvaluationContext,
|
|
130
143
|
entity: TEntity,
|
|
131
144
|
metricsAdapter: IEntityMetricsAdapter
|
|
132
145
|
): Promise<TEntity> {
|
|
@@ -134,6 +147,7 @@ export default abstract class EntityPrivacyPolicy<
|
|
|
134
147
|
this.createRules,
|
|
135
148
|
viewerContext,
|
|
136
149
|
queryContext,
|
|
150
|
+
evaluationContext,
|
|
137
151
|
entity,
|
|
138
152
|
EntityAuthorizationAction.CREATE,
|
|
139
153
|
metricsAdapter
|
|
@@ -151,6 +165,7 @@ export default abstract class EntityPrivacyPolicy<
|
|
|
151
165
|
async authorizeReadAsync(
|
|
152
166
|
viewerContext: TViewerContext,
|
|
153
167
|
queryContext: EntityQueryContext,
|
|
168
|
+
evaluationContext: EntityPrivacyPolicyEvaluationContext,
|
|
154
169
|
entity: TEntity,
|
|
155
170
|
metricsAdapter: IEntityMetricsAdapter
|
|
156
171
|
): Promise<TEntity> {
|
|
@@ -158,6 +173,7 @@ export default abstract class EntityPrivacyPolicy<
|
|
|
158
173
|
this.readRules,
|
|
159
174
|
viewerContext,
|
|
160
175
|
queryContext,
|
|
176
|
+
evaluationContext,
|
|
161
177
|
entity,
|
|
162
178
|
EntityAuthorizationAction.READ,
|
|
163
179
|
metricsAdapter
|
|
@@ -175,6 +191,7 @@ export default abstract class EntityPrivacyPolicy<
|
|
|
175
191
|
async authorizeUpdateAsync(
|
|
176
192
|
viewerContext: TViewerContext,
|
|
177
193
|
queryContext: EntityQueryContext,
|
|
194
|
+
evaluationContext: EntityPrivacyPolicyEvaluationContext,
|
|
178
195
|
entity: TEntity,
|
|
179
196
|
metricsAdapter: IEntityMetricsAdapter
|
|
180
197
|
): Promise<TEntity> {
|
|
@@ -182,6 +199,7 @@ export default abstract class EntityPrivacyPolicy<
|
|
|
182
199
|
this.updateRules,
|
|
183
200
|
viewerContext,
|
|
184
201
|
queryContext,
|
|
202
|
+
evaluationContext,
|
|
185
203
|
entity,
|
|
186
204
|
EntityAuthorizationAction.UPDATE,
|
|
187
205
|
metricsAdapter
|
|
@@ -199,6 +217,7 @@ export default abstract class EntityPrivacyPolicy<
|
|
|
199
217
|
async authorizeDeleteAsync(
|
|
200
218
|
viewerContext: TViewerContext,
|
|
201
219
|
queryContext: EntityQueryContext,
|
|
220
|
+
evaluationContext: EntityPrivacyPolicyEvaluationContext,
|
|
202
221
|
entity: TEntity,
|
|
203
222
|
metricsAdapter: IEntityMetricsAdapter
|
|
204
223
|
): Promise<TEntity> {
|
|
@@ -206,6 +225,7 @@ export default abstract class EntityPrivacyPolicy<
|
|
|
206
225
|
this.deleteRules,
|
|
207
226
|
viewerContext,
|
|
208
227
|
queryContext,
|
|
228
|
+
evaluationContext,
|
|
209
229
|
entity,
|
|
210
230
|
EntityAuthorizationAction.DELETE,
|
|
211
231
|
metricsAdapter
|
|
@@ -216,6 +236,7 @@ export default abstract class EntityPrivacyPolicy<
|
|
|
216
236
|
ruleset: readonly PrivacyPolicyRule<TFields, TID, TViewerContext, TEntity, TSelectedFields>[],
|
|
217
237
|
viewerContext: TViewerContext,
|
|
218
238
|
queryContext: EntityQueryContext,
|
|
239
|
+
evaluationContext: EntityPrivacyPolicyEvaluationContext,
|
|
219
240
|
entity: TEntity,
|
|
220
241
|
action: EntityAuthorizationAction,
|
|
221
242
|
metricsAdapter: IEntityMetricsAdapter
|
|
@@ -228,6 +249,7 @@ export default abstract class EntityPrivacyPolicy<
|
|
|
228
249
|
ruleset,
|
|
229
250
|
viewerContext,
|
|
230
251
|
queryContext,
|
|
252
|
+
evaluationContext,
|
|
231
253
|
entity,
|
|
232
254
|
action
|
|
233
255
|
);
|
|
@@ -256,6 +278,7 @@ export default abstract class EntityPrivacyPolicy<
|
|
|
256
278
|
ruleset,
|
|
257
279
|
viewerContext,
|
|
258
280
|
queryContext,
|
|
281
|
+
evaluationContext,
|
|
259
282
|
entity,
|
|
260
283
|
action
|
|
261
284
|
);
|
|
@@ -285,6 +308,7 @@ export default abstract class EntityPrivacyPolicy<
|
|
|
285
308
|
ruleset,
|
|
286
309
|
viewerContext,
|
|
287
310
|
queryContext,
|
|
311
|
+
evaluationContext,
|
|
288
312
|
entity,
|
|
289
313
|
action
|
|
290
314
|
);
|
|
@@ -315,12 +339,18 @@ export default abstract class EntityPrivacyPolicy<
|
|
|
315
339
|
ruleset: readonly PrivacyPolicyRule<TFields, TID, TViewerContext, TEntity, TSelectedFields>[],
|
|
316
340
|
viewerContext: TViewerContext,
|
|
317
341
|
queryContext: EntityQueryContext,
|
|
342
|
+
evaluationContext: EntityPrivacyPolicyEvaluationContext,
|
|
318
343
|
entity: TEntity,
|
|
319
344
|
action: EntityAuthorizationAction
|
|
320
345
|
): Promise<TEntity> {
|
|
321
346
|
for (let i = 0; i < ruleset.length; i++) {
|
|
322
347
|
const rule = ruleset[i]!;
|
|
323
|
-
const ruleEvaluationResult = await rule.evaluateAsync(
|
|
348
|
+
const ruleEvaluationResult = await rule.evaluateAsync(
|
|
349
|
+
viewerContext,
|
|
350
|
+
queryContext,
|
|
351
|
+
evaluationContext,
|
|
352
|
+
entity
|
|
353
|
+
);
|
|
324
354
|
switch (ruleEvaluationResult) {
|
|
325
355
|
case RuleEvaluationResult.DENY:
|
|
326
356
|
throw new EntityNotAuthorizedError<
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import invariant from 'invariant';
|
|
2
|
+
|
|
3
|
+
import { ISecondaryEntityCache } from './EntitySecondaryCacheLoader';
|
|
4
|
+
import IEntityGenericCacher from './IEntityGenericCacher';
|
|
5
|
+
import { CacheStatus } from './internal/ReadThroughEntityCache';
|
|
6
|
+
import { filterMap, zipToMap } from './utils/collections/maps';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* A custom secondary read-through entity cache is a way to add a custom second layer of caching for a particular
|
|
10
|
+
* single entity load. One common way this may be used is to add a second layer of caching in a hot path that makes
|
|
11
|
+
* a call to {@link EntityLoader.loadManyByFieldEqualityConjunctionAsync} is guaranteed to return at most one entity.
|
|
12
|
+
*/
|
|
13
|
+
export default abstract class GenericSecondaryEntityCache<TFields, TLoadParams>
|
|
14
|
+
implements ISecondaryEntityCache<TFields, TLoadParams>
|
|
15
|
+
{
|
|
16
|
+
constructor(
|
|
17
|
+
protected readonly cacher: IEntityGenericCacher<TFields>,
|
|
18
|
+
protected readonly constructCacheKey: (params: Readonly<TLoadParams>) => string
|
|
19
|
+
) {}
|
|
20
|
+
|
|
21
|
+
public async loadManyThroughAsync(
|
|
22
|
+
loadParamsArray: readonly Readonly<TLoadParams>[],
|
|
23
|
+
fetcher: (
|
|
24
|
+
fetcherLoadParamsArray: readonly Readonly<TLoadParams>[]
|
|
25
|
+
) => Promise<ReadonlyMap<Readonly<TLoadParams>, Readonly<TFields> | null>>
|
|
26
|
+
): Promise<ReadonlyMap<Readonly<TLoadParams>, Readonly<TFields> | null>> {
|
|
27
|
+
const cacheKeys = loadParamsArray.map(this.constructCacheKey);
|
|
28
|
+
const cacheKeyToLoadParamsMap = zipToMap(cacheKeys, loadParamsArray);
|
|
29
|
+
|
|
30
|
+
const cacheLoadResults = await this.cacher.loadManyAsync(cacheKeys);
|
|
31
|
+
|
|
32
|
+
invariant(
|
|
33
|
+
cacheLoadResults.size === loadParamsArray.length,
|
|
34
|
+
`${this.constructor.name} loadMany should return a result for each key`
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
const cacheKeysToFetch = Array.from(
|
|
38
|
+
filterMap(
|
|
39
|
+
cacheLoadResults,
|
|
40
|
+
(cacheLoadResult) => cacheLoadResult.status === CacheStatus.MISS
|
|
41
|
+
).keys()
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
// put cache hits in result map
|
|
45
|
+
const results: Map<Readonly<TLoadParams>, Readonly<TFields> | null> = new Map();
|
|
46
|
+
cacheLoadResults.forEach((cacheLoadResult, cacheKey) => {
|
|
47
|
+
if (cacheLoadResult.status === CacheStatus.HIT) {
|
|
48
|
+
const loadParams = cacheKeyToLoadParamsMap.get(cacheKey);
|
|
49
|
+
invariant(loadParams !== undefined, 'load params should be in cache key map');
|
|
50
|
+
results.set(loadParams, cacheLoadResult.item);
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
// fetch any misses from DB, add DB objects to results, cache DB results, inform cache of any missing DB results
|
|
55
|
+
if (cacheKeysToFetch.length > 0) {
|
|
56
|
+
const loadParamsToFetch = cacheKeysToFetch.map((cacheKey) => {
|
|
57
|
+
const loadParams = cacheKeyToLoadParamsMap.get(cacheKey);
|
|
58
|
+
invariant(loadParams !== undefined, 'load params should be in cache key map');
|
|
59
|
+
return loadParams;
|
|
60
|
+
});
|
|
61
|
+
const fetchResults = await fetcher(loadParamsToFetch);
|
|
62
|
+
|
|
63
|
+
const fetchMisses = loadParamsToFetch.filter((loadParams) => {
|
|
64
|
+
// all values of fetchResults should be field objects or undefined
|
|
65
|
+
return !fetchResults.get(loadParams);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
for (const fetchMiss of fetchMisses) {
|
|
69
|
+
results.set(fetchMiss, null);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const objectsToCache: Map<string, Readonly<TFields>> = new Map();
|
|
73
|
+
for (const [loadParams, object] of fetchResults.entries()) {
|
|
74
|
+
if (object) {
|
|
75
|
+
objectsToCache.set(this.constructCacheKey(loadParams), object);
|
|
76
|
+
results.set(loadParams, object);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
await Promise.all([
|
|
81
|
+
this.cacher.cacheManyAsync(objectsToCache),
|
|
82
|
+
this.cacher.cacheDBMissesAsync(fetchMisses.map(this.constructCacheKey)),
|
|
83
|
+
]);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return results;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Invalidate the cache for objects cached by constructCacheKey(loadParams).
|
|
91
|
+
*
|
|
92
|
+
* @param loadParamsArray - load params to invalidate
|
|
93
|
+
*/
|
|
94
|
+
public invalidateManyAsync(loadParamsArray: readonly Readonly<TLoadParams>[]): Promise<void> {
|
|
95
|
+
const cacheKeys = loadParamsArray.map(this.constructCacheKey);
|
|
96
|
+
return this.cacher.invalidateManyAsync(cacheKeys);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { CacheLoadResult } from './internal/ReadThroughEntityCache';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* A cacher stores and loads key-value pairs. It also supports negative caching - it stores the absence
|
|
5
|
+
* of keys that don't exist in the backing datastore.
|
|
6
|
+
*/
|
|
7
|
+
export default interface IEntityGenericCacher<TFields> {
|
|
8
|
+
loadManyAsync(keys: readonly string[]): Promise<ReadonlyMap<string, CacheLoadResult<TFields>>>;
|
|
9
|
+
|
|
10
|
+
cacheManyAsync(objectMap: ReadonlyMap<string, Readonly<TFields>>): Promise<void>;
|
|
11
|
+
|
|
12
|
+
cacheDBMissesAsync(keys: readonly string[]): Promise<void>;
|
|
13
|
+
|
|
14
|
+
invalidateManyAsync(keys: readonly string[]): Promise<void>;
|
|
15
|
+
}
|
package/src/ReadonlyEntity.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import EntityLoader from './EntityLoader';
|
|
2
2
|
import EntityLoaderFactory from './EntityLoaderFactory';
|
|
3
|
-
import EntityPrivacyPolicy from './EntityPrivacyPolicy';
|
|
3
|
+
import EntityPrivacyPolicy, { EntityPrivacyPolicyEvaluationContext } from './EntityPrivacyPolicy';
|
|
4
4
|
import { EntityQueryContext } from './EntityQueryContext';
|
|
5
5
|
import ReadonlyEntity from './ReadonlyEntity';
|
|
6
6
|
import ViewerContext from './ViewerContext';
|
|
@@ -35,8 +35,13 @@ export default class ViewerScopedEntityLoaderFactory<
|
|
|
35
35
|
) {}
|
|
36
36
|
|
|
37
37
|
forLoad(
|
|
38
|
-
queryContext: EntityQueryContext
|
|
38
|
+
queryContext: EntityQueryContext,
|
|
39
|
+
privacyPolicyEvaluationContext: EntityPrivacyPolicyEvaluationContext
|
|
39
40
|
): EntityLoader<TFields, TID, TViewerContext, TEntity, TPrivacyPolicy, TSelectedFields> {
|
|
40
|
-
return this.entityLoaderFactory.forLoad(
|
|
41
|
+
return this.entityLoaderFactory.forLoad(
|
|
42
|
+
this.viewerContext,
|
|
43
|
+
queryContext,
|
|
44
|
+
privacyPolicyEvaluationContext
|
|
45
|
+
);
|
|
41
46
|
}
|
|
42
47
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { CreateMutator, UpdateMutator, DeleteMutator } from './EntityMutator';
|
|
2
2
|
import EntityMutatorFactory from './EntityMutatorFactory';
|
|
3
|
-
import EntityPrivacyPolicy from './EntityPrivacyPolicy';
|
|
3
|
+
import EntityPrivacyPolicy, { EntityPrivacyPolicyEvaluationContext } from './EntityPrivacyPolicy';
|
|
4
4
|
import { EntityQueryContext } from './EntityQueryContext';
|
|
5
5
|
import ReadonlyEntity from './ReadonlyEntity';
|
|
6
6
|
import ViewerContext from './ViewerContext';
|
|
@@ -35,22 +35,37 @@ export default class ViewerScopedEntityMutatorFactory<
|
|
|
35
35
|
) {}
|
|
36
36
|
|
|
37
37
|
forCreate(
|
|
38
|
-
queryContext: EntityQueryContext
|
|
38
|
+
queryContext: EntityQueryContext,
|
|
39
|
+
privacyPolicyEvaluationContext: EntityPrivacyPolicyEvaluationContext
|
|
39
40
|
): CreateMutator<TFields, TID, TViewerContext, TEntity, TPrivacyPolicy, TSelectedFields> {
|
|
40
|
-
return this.entityMutatorFactory.forCreate(
|
|
41
|
+
return this.entityMutatorFactory.forCreate(
|
|
42
|
+
this.viewerContext,
|
|
43
|
+
queryContext,
|
|
44
|
+
privacyPolicyEvaluationContext
|
|
45
|
+
);
|
|
41
46
|
}
|
|
42
47
|
|
|
43
48
|
forUpdate(
|
|
44
49
|
existingEntity: TEntity,
|
|
45
|
-
queryContext: EntityQueryContext
|
|
50
|
+
queryContext: EntityQueryContext,
|
|
51
|
+
privacyPolicyEvaluationContext: EntityPrivacyPolicyEvaluationContext
|
|
46
52
|
): UpdateMutator<TFields, TID, TViewerContext, TEntity, TPrivacyPolicy, TSelectedFields> {
|
|
47
|
-
return this.entityMutatorFactory.forUpdate(
|
|
53
|
+
return this.entityMutatorFactory.forUpdate(
|
|
54
|
+
existingEntity,
|
|
55
|
+
queryContext,
|
|
56
|
+
privacyPolicyEvaluationContext
|
|
57
|
+
);
|
|
48
58
|
}
|
|
49
59
|
|
|
50
60
|
forDelete(
|
|
51
61
|
existingEntity: TEntity,
|
|
52
|
-
queryContext: EntityQueryContext
|
|
62
|
+
queryContext: EntityQueryContext,
|
|
63
|
+
privacyPolicyEvaluationContext: EntityPrivacyPolicyEvaluationContext
|
|
53
64
|
): DeleteMutator<TFields, TID, TViewerContext, TEntity, TPrivacyPolicy, TSelectedFields> {
|
|
54
|
-
return this.entityMutatorFactory.forDelete(
|
|
65
|
+
return this.entityMutatorFactory.forDelete(
|
|
66
|
+
existingEntity,
|
|
67
|
+
queryContext,
|
|
68
|
+
privacyPolicyEvaluationContext
|
|
69
|
+
);
|
|
55
70
|
}
|
|
56
71
|
}
|
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
import invariant from 'invariant';
|
|
2
|
+
|
|
3
|
+
import ComposedEntityCacheAdapter from '../ComposedEntityCacheAdapter';
|
|
4
|
+
import EntityCacheAdapter from '../EntityCacheAdapter';
|
|
5
|
+
import EntityConfiguration from '../EntityConfiguration';
|
|
6
|
+
import { UUIDField } from '../EntityFields';
|
|
7
|
+
import { CacheLoadResult, CacheStatus } from '../internal/ReadThroughEntityCache';
|
|
8
|
+
|
|
9
|
+
type BlahFields = {
|
|
10
|
+
id: string;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
const entityConfiguration = new EntityConfiguration<BlahFields>({
|
|
14
|
+
idField: 'id',
|
|
15
|
+
tableName: 'blah',
|
|
16
|
+
schema: {
|
|
17
|
+
id: new UUIDField({ columnName: 'id', cache: true }),
|
|
18
|
+
},
|
|
19
|
+
databaseAdapterFlavor: 'postgres',
|
|
20
|
+
cacheAdapterFlavor: 'local-memory-and-redis',
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
export const DOES_NOT_EXIST_LOCAL_MEMORY_CACHE = Symbol('doesNotExist');
|
|
24
|
+
type LocalMemoryCacheValue<TFields> = Readonly<TFields> | typeof DOES_NOT_EXIST_LOCAL_MEMORY_CACHE;
|
|
25
|
+
|
|
26
|
+
class TestLocalCacheAdapter<TFields> extends EntityCacheAdapter<TFields> {
|
|
27
|
+
constructor(
|
|
28
|
+
entityConfiguration: EntityConfiguration<TFields>,
|
|
29
|
+
private readonly cache: Map<string, LocalMemoryCacheValue<TFields>>
|
|
30
|
+
) {
|
|
31
|
+
super(entityConfiguration);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
public async loadManyAsync<N extends keyof TFields>(
|
|
35
|
+
fieldName: N,
|
|
36
|
+
fieldValues: readonly NonNullable<TFields[N]>[]
|
|
37
|
+
): Promise<ReadonlyMap<NonNullable<TFields[N]>, CacheLoadResult<TFields>>> {
|
|
38
|
+
const localMemoryCacheKeyToFieldValueMapping = new Map(
|
|
39
|
+
fieldValues.map((fieldValue) => [this.makeCacheKey(fieldName, fieldValue), fieldValue])
|
|
40
|
+
);
|
|
41
|
+
const cacheResults = new Map<NonNullable<TFields[N]>, CacheLoadResult<TFields>>();
|
|
42
|
+
for (const [cacheKey, fieldValue] of localMemoryCacheKeyToFieldValueMapping) {
|
|
43
|
+
const cacheResult = this.cache.get(cacheKey);
|
|
44
|
+
if (cacheResult === DOES_NOT_EXIST_LOCAL_MEMORY_CACHE) {
|
|
45
|
+
cacheResults.set(fieldValue, {
|
|
46
|
+
status: CacheStatus.NEGATIVE,
|
|
47
|
+
});
|
|
48
|
+
} else if (cacheResult) {
|
|
49
|
+
cacheResults.set(fieldValue, {
|
|
50
|
+
status: CacheStatus.HIT,
|
|
51
|
+
item: cacheResult,
|
|
52
|
+
});
|
|
53
|
+
} else {
|
|
54
|
+
cacheResults.set(fieldValue, {
|
|
55
|
+
status: CacheStatus.MISS,
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return cacheResults;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
public async cacheManyAsync<N extends keyof TFields>(
|
|
64
|
+
fieldName: N,
|
|
65
|
+
objectMap: ReadonlyMap<NonNullable<TFields[N]>, Readonly<TFields>>
|
|
66
|
+
): Promise<void> {
|
|
67
|
+
for (const [fieldValue, item] of objectMap) {
|
|
68
|
+
const cacheKey = this.makeCacheKey(fieldName, fieldValue);
|
|
69
|
+
this.cache.set(cacheKey, item);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
public async cacheDBMissesAsync<N extends keyof TFields>(
|
|
74
|
+
fieldName: N,
|
|
75
|
+
fieldValues: readonly NonNullable<TFields[N]>[]
|
|
76
|
+
): Promise<void> {
|
|
77
|
+
for (const fieldValue of fieldValues) {
|
|
78
|
+
const cacheKey = this.makeCacheKey(fieldName, fieldValue);
|
|
79
|
+
this.cache.set(cacheKey, DOES_NOT_EXIST_LOCAL_MEMORY_CACHE);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
public async invalidateManyAsync<N extends keyof TFields>(
|
|
84
|
+
fieldName: N,
|
|
85
|
+
fieldValues: readonly NonNullable<TFields[N]>[]
|
|
86
|
+
): Promise<void> {
|
|
87
|
+
for (const fieldValue of fieldValues) {
|
|
88
|
+
const cacheKey = this.makeCacheKey(fieldName, fieldValue);
|
|
89
|
+
this.cache.delete(cacheKey);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
private makeCacheKey<N extends keyof TFields>(
|
|
94
|
+
fieldName: N,
|
|
95
|
+
fieldValue: NonNullable<TFields[N]>
|
|
96
|
+
): string {
|
|
97
|
+
const columnName = this.entityConfiguration.entityToDBFieldsKeyMapping.get(fieldName);
|
|
98
|
+
invariant(columnName, `database field mapping missing for ${fieldName}`);
|
|
99
|
+
const parts = [
|
|
100
|
+
this.entityConfiguration.tableName,
|
|
101
|
+
`${this.entityConfiguration.cacheKeyVersion}`,
|
|
102
|
+
columnName,
|
|
103
|
+
String(fieldValue),
|
|
104
|
+
];
|
|
105
|
+
const delimiter = ':';
|
|
106
|
+
const escapedParts = parts.map((part) =>
|
|
107
|
+
part.replace('\\', '\\\\').replace(delimiter, `\\${delimiter}`)
|
|
108
|
+
);
|
|
109
|
+
return escapedParts.join(delimiter);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function makeTestCacheAdapters(): {
|
|
114
|
+
primaryCache: Map<string, LocalMemoryCacheValue<BlahFields>>;
|
|
115
|
+
primaryCacheAdapter: TestLocalCacheAdapter<BlahFields>;
|
|
116
|
+
fallbackCache: Map<string, LocalMemoryCacheValue<BlahFields>>;
|
|
117
|
+
fallbackCacheAdapter: TestLocalCacheAdapter<BlahFields>;
|
|
118
|
+
cacheAdapter: ComposedEntityCacheAdapter<BlahFields>;
|
|
119
|
+
} {
|
|
120
|
+
const primaryCache = new Map();
|
|
121
|
+
const primaryCacheAdapter = new TestLocalCacheAdapter(entityConfiguration, primaryCache);
|
|
122
|
+
|
|
123
|
+
const fallbackCache = new Map();
|
|
124
|
+
const fallbackCacheAdapter = new TestLocalCacheAdapter(entityConfiguration, fallbackCache);
|
|
125
|
+
|
|
126
|
+
const cacheAdapter = new ComposedEntityCacheAdapter(entityConfiguration, [
|
|
127
|
+
primaryCacheAdapter,
|
|
128
|
+
fallbackCacheAdapter,
|
|
129
|
+
]);
|
|
130
|
+
|
|
131
|
+
return {
|
|
132
|
+
primaryCache,
|
|
133
|
+
primaryCacheAdapter,
|
|
134
|
+
fallbackCache,
|
|
135
|
+
fallbackCacheAdapter,
|
|
136
|
+
cacheAdapter,
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
describe(ComposedEntityCacheAdapter, () => {
|
|
141
|
+
describe('loadManyAsync', () => {
|
|
142
|
+
it('returns primary results when populated', async () => {
|
|
143
|
+
const { primaryCacheAdapter, cacheAdapter } = makeTestCacheAdapters();
|
|
144
|
+
|
|
145
|
+
const cacheHits = new Map<string, Readonly<BlahFields>>([['test-id-1', { id: 'test-id-1' }]]);
|
|
146
|
+
await primaryCacheAdapter.cacheManyAsync('id', cacheHits);
|
|
147
|
+
await primaryCacheAdapter.cacheDBMissesAsync('id', ['test-id-2']);
|
|
148
|
+
|
|
149
|
+
const results = await cacheAdapter.loadManyAsync('id', [
|
|
150
|
+
'test-id-1',
|
|
151
|
+
'test-id-2',
|
|
152
|
+
'test-id-3',
|
|
153
|
+
]);
|
|
154
|
+
|
|
155
|
+
expect(results.get('test-id-1')).toMatchObject({
|
|
156
|
+
status: CacheStatus.HIT,
|
|
157
|
+
item: { id: 'test-id-1' },
|
|
158
|
+
});
|
|
159
|
+
expect(results.get('test-id-2')).toMatchObject({ status: CacheStatus.NEGATIVE });
|
|
160
|
+
expect(results.get('test-id-3')).toMatchObject({ status: CacheStatus.MISS });
|
|
161
|
+
expect(results.size).toBe(3);
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
it('returns fallback adapter results primary is empty', async () => {
|
|
165
|
+
const { primaryCacheAdapter, cacheAdapter } = makeTestCacheAdapters();
|
|
166
|
+
|
|
167
|
+
const cacheHits = new Map<string, Readonly<BlahFields>>([['test-id-1', { id: 'test-id-1' }]]);
|
|
168
|
+
await primaryCacheAdapter.cacheManyAsync('id', cacheHits);
|
|
169
|
+
await primaryCacheAdapter.cacheDBMissesAsync('id', ['test-id-2']);
|
|
170
|
+
|
|
171
|
+
const results = await cacheAdapter.loadManyAsync('id', [
|
|
172
|
+
'test-id-1',
|
|
173
|
+
'test-id-2',
|
|
174
|
+
'test-id-3',
|
|
175
|
+
]);
|
|
176
|
+
|
|
177
|
+
expect(results.get('test-id-1')).toMatchObject({
|
|
178
|
+
status: CacheStatus.HIT,
|
|
179
|
+
item: { id: 'test-id-1' },
|
|
180
|
+
});
|
|
181
|
+
expect(results.get('test-id-2')).toMatchObject({ status: CacheStatus.NEGATIVE });
|
|
182
|
+
expect(results.get('test-id-3')).toMatchObject({ status: CacheStatus.MISS });
|
|
183
|
+
expect(results.size).toBe(3);
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
it('returns empty map when passed empty array of fieldValues', async () => {
|
|
187
|
+
const { cacheAdapter } = makeTestCacheAdapters();
|
|
188
|
+
const results = await cacheAdapter.loadManyAsync('id', []);
|
|
189
|
+
expect(results).toEqual(new Map());
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
it('handles 0 cache adapter compose case', async () => {
|
|
193
|
+
const cacheAdapter = new ComposedEntityCacheAdapter(entityConfiguration, []);
|
|
194
|
+
const results = await cacheAdapter.loadManyAsync('id', []);
|
|
195
|
+
expect(results).toEqual(new Map());
|
|
196
|
+
});
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
describe('cacheManyAsync', () => {
|
|
200
|
+
it('correctly caches all objects', async () => {
|
|
201
|
+
const {
|
|
202
|
+
primaryCache,
|
|
203
|
+
primaryCacheAdapter,
|
|
204
|
+
fallbackCache,
|
|
205
|
+
fallbackCacheAdapter,
|
|
206
|
+
cacheAdapter,
|
|
207
|
+
} = makeTestCacheAdapters();
|
|
208
|
+
|
|
209
|
+
await cacheAdapter.cacheManyAsync('id', new Map([['test-id-1', { id: 'test-id-1' }]]));
|
|
210
|
+
|
|
211
|
+
const primaryLocalMemoryCacheKey = primaryCacheAdapter['makeCacheKey']('id', 'test-id-1');
|
|
212
|
+
expect(primaryCache.get(primaryLocalMemoryCacheKey)).toMatchObject({
|
|
213
|
+
id: 'test-id-1',
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
const fallbackLocalMemoryCacheKey = fallbackCacheAdapter['makeCacheKey']('id', 'test-id-1');
|
|
217
|
+
expect(fallbackCache.get(fallbackLocalMemoryCacheKey)).toMatchObject({
|
|
218
|
+
id: 'test-id-1',
|
|
219
|
+
});
|
|
220
|
+
});
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
describe('cacheDBMissesAsync', () => {
|
|
224
|
+
it('correctly caches misses', async () => {
|
|
225
|
+
const {
|
|
226
|
+
primaryCache,
|
|
227
|
+
primaryCacheAdapter,
|
|
228
|
+
fallbackCache,
|
|
229
|
+
fallbackCacheAdapter,
|
|
230
|
+
cacheAdapter,
|
|
231
|
+
} = makeTestCacheAdapters();
|
|
232
|
+
|
|
233
|
+
await cacheAdapter.cacheDBMissesAsync('id', ['test-id-1']);
|
|
234
|
+
|
|
235
|
+
const primaryLocalMemoryCacheKey = primaryCacheAdapter['makeCacheKey']('id', 'test-id-1');
|
|
236
|
+
expect(primaryCache.get(primaryLocalMemoryCacheKey)).toBe(DOES_NOT_EXIST_LOCAL_MEMORY_CACHE);
|
|
237
|
+
|
|
238
|
+
const fallbackLocalMemoryCacheKey = fallbackCacheAdapter['makeCacheKey']('id', 'test-id-1');
|
|
239
|
+
expect(fallbackCache.get(fallbackLocalMemoryCacheKey)).toBe(
|
|
240
|
+
DOES_NOT_EXIST_LOCAL_MEMORY_CACHE
|
|
241
|
+
);
|
|
242
|
+
});
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
describe('invalidateManyAsync', () => {
|
|
246
|
+
it('invalidates correctly', async () => {
|
|
247
|
+
const {
|
|
248
|
+
primaryCache,
|
|
249
|
+
primaryCacheAdapter,
|
|
250
|
+
fallbackCache,
|
|
251
|
+
fallbackCacheAdapter,
|
|
252
|
+
cacheAdapter,
|
|
253
|
+
} = makeTestCacheAdapters();
|
|
254
|
+
|
|
255
|
+
const cacheHits = new Map<string, Readonly<BlahFields>>([['test-id-1', { id: 'test-id-1' }]]);
|
|
256
|
+
await primaryCacheAdapter.cacheManyAsync('id', cacheHits);
|
|
257
|
+
await primaryCacheAdapter.cacheDBMissesAsync('id', ['test-id-2']);
|
|
258
|
+
await fallbackCacheAdapter.cacheManyAsync('id', cacheHits);
|
|
259
|
+
await fallbackCacheAdapter.cacheDBMissesAsync('id', ['test-id-2']);
|
|
260
|
+
|
|
261
|
+
await cacheAdapter.invalidateManyAsync('id', ['test-id-1', 'test-id-2']);
|
|
262
|
+
|
|
263
|
+
const primaryLocalMemoryCacheKey1 = primaryCacheAdapter['makeCacheKey']('id', 'test-id-1');
|
|
264
|
+
expect(primaryCache.get(primaryLocalMemoryCacheKey1)).toBe(undefined);
|
|
265
|
+
const primaryLocalMemoryCacheKey2 = primaryCacheAdapter['makeCacheKey']('id', 'test-id-1');
|
|
266
|
+
expect(primaryCache.get(primaryLocalMemoryCacheKey2)).toBe(undefined);
|
|
267
|
+
|
|
268
|
+
const fallbackLocalMemoryCacheKey1 = fallbackCacheAdapter['makeCacheKey']('id', 'test-id-1');
|
|
269
|
+
expect(fallbackCache.get(fallbackLocalMemoryCacheKey1)).toBe(undefined);
|
|
270
|
+
const fallbackLocalMemoryCacheKey2 = fallbackCacheAdapter['makeCacheKey']('id', 'test-id-1');
|
|
271
|
+
expect(fallbackCache.get(fallbackLocalMemoryCacheKey2)).toBe(undefined);
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
it('returns when passed empty array of fieldValues', async () => {
|
|
275
|
+
const { cacheAdapter } = makeTestCacheAdapters();
|
|
276
|
+
|
|
277
|
+
await cacheAdapter.invalidateManyAsync('id', []);
|
|
278
|
+
});
|
|
279
|
+
});
|
|
280
|
+
});
|