@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.
Files changed (145) hide show
  1. package/build/ComposedEntityCacheAdapter.d.ts +19 -0
  2. package/build/ComposedEntityCacheAdapter.js +66 -0
  3. package/build/ComposedEntityCacheAdapter.js.map +1 -0
  4. package/build/ComposedSecondaryEntityCache.d.ts +15 -0
  5. package/build/ComposedSecondaryEntityCache.js +37 -0
  6. package/build/ComposedSecondaryEntityCache.js.map +1 -0
  7. package/build/Entity.js +6 -6
  8. package/build/Entity.js.map +1 -1
  9. package/build/EntityAssociationLoader.js +4 -4
  10. package/build/EntityAssociationLoader.js.map +1 -1
  11. package/build/EntityCacheAdapter.d.ts +2 -9
  12. package/build/EntityCacheAdapter.js.map +1 -1
  13. package/build/EntityFieldDefinition.d.ts +8 -0
  14. package/build/EntityFieldDefinition.js +5 -0
  15. package/build/EntityFieldDefinition.js.map +1 -1
  16. package/build/EntityFields.d.ts +38 -0
  17. package/build/EntityFields.js +38 -0
  18. package/build/EntityFields.js.map +1 -1
  19. package/build/EntityLoader.d.ts +3 -2
  20. package/build/EntityLoader.js +5 -4
  21. package/build/EntityLoader.js.map +1 -1
  22. package/build/EntityLoaderFactory.d.ts +2 -2
  23. package/build/EntityLoaderFactory.js +2 -2
  24. package/build/EntityLoaderFactory.js.map +1 -1
  25. package/build/EntityMutationInfo.d.ts +12 -3
  26. package/build/EntityMutator.d.ts +5 -4
  27. package/build/EntityMutator.js +31 -24
  28. package/build/EntityMutator.js.map +1 -1
  29. package/build/EntityMutatorFactory.d.ts +4 -4
  30. package/build/EntityMutatorFactory.js +6 -6
  31. package/build/EntityMutatorFactory.js.map +1 -1
  32. package/build/EntityPrivacyPolicy.d.ts +15 -4
  33. package/build/EntityPrivacyPolicy.js +14 -14
  34. package/build/EntityPrivacyPolicy.js.map +1 -1
  35. package/build/GenericSecondaryEntityCache.d.ts +19 -0
  36. package/build/GenericSecondaryEntityCache.js +74 -0
  37. package/build/GenericSecondaryEntityCache.js.map +1 -0
  38. package/build/IEntityGenericCacher.d.ts +11 -0
  39. package/build/IEntityGenericCacher.js +3 -0
  40. package/build/IEntityGenericCacher.js.map +1 -0
  41. package/build/ReadonlyEntity.js +1 -1
  42. package/build/ReadonlyEntity.js.map +1 -1
  43. package/build/ViewerScopedEntityLoaderFactory.d.ts +2 -2
  44. package/build/ViewerScopedEntityLoaderFactory.js +2 -2
  45. package/build/ViewerScopedEntityLoaderFactory.js.map +1 -1
  46. package/build/ViewerScopedEntityMutatorFactory.d.ts +4 -4
  47. package/build/ViewerScopedEntityMutatorFactory.js +6 -6
  48. package/build/ViewerScopedEntityMutatorFactory.js.map +1 -1
  49. package/build/__tests__/ComposedCacheAdapter-test.d.ts +1 -0
  50. package/build/__tests__/ComposedCacheAdapter-test.js +198 -0
  51. package/build/__tests__/ComposedCacheAdapter-test.js.map +1 -0
  52. package/build/__tests__/ComposedSecondaryEntityCache-test.d.ts +1 -0
  53. package/build/__tests__/ComposedSecondaryEntityCache-test.js +65 -0
  54. package/build/__tests__/ComposedSecondaryEntityCache-test.js.map +1 -0
  55. package/build/__tests__/EntityCommonUseCases-test.js +1 -1
  56. package/build/__tests__/EntityCommonUseCases-test.js.map +1 -1
  57. package/build/__tests__/EntityEdges-test.js +260 -37
  58. package/build/__tests__/EntityEdges-test.js.map +1 -1
  59. package/build/__tests__/EntityLoader-constructor-test.js +2 -1
  60. package/build/__tests__/EntityLoader-constructor-test.js.map +1 -1
  61. package/build/__tests__/EntityLoader-test.js +19 -11
  62. package/build/__tests__/EntityLoader-test.js.map +1 -1
  63. package/build/__tests__/EntityMutator-test.js +96 -43
  64. package/build/__tests__/EntityMutator-test.js.map +1 -1
  65. package/build/__tests__/EntityPrivacyPolicy-test.js +23 -12
  66. package/build/__tests__/EntityPrivacyPolicy-test.js.map +1 -1
  67. package/build/__tests__/EntitySecondaryCacheLoader-test.js +1 -1
  68. package/build/__tests__/EntitySecondaryCacheLoader-test.js.map +1 -1
  69. package/build/__tests__/ViewerScopedEntityLoaderFactory-test.js +3 -2
  70. package/build/__tests__/ViewerScopedEntityLoaderFactory-test.js.map +1 -1
  71. package/build/__tests__/ViewerScopedEntityMutatorFactory-test.js +3 -2
  72. package/build/__tests__/ViewerScopedEntityMutatorFactory-test.js.map +1 -1
  73. package/build/index.d.ts +2 -0
  74. package/build/index.js +3 -1
  75. package/build/index.js.map +1 -1
  76. package/build/internal/ReadThroughEntityCache.d.ts +2 -3
  77. package/build/internal/ReadThroughEntityCache.js +2 -6
  78. package/build/internal/ReadThroughEntityCache.js.map +1 -1
  79. package/build/internal/__tests__/ReadThroughEntityCache-test.js +0 -32
  80. package/build/internal/__tests__/ReadThroughEntityCache-test.js.map +1 -1
  81. package/build/rules/AlwaysAllowPrivacyPolicyRule.d.ts +2 -1
  82. package/build/rules/AlwaysAllowPrivacyPolicyRule.js +1 -1
  83. package/build/rules/AlwaysAllowPrivacyPolicyRule.js.map +1 -1
  84. package/build/rules/AlwaysDenyPrivacyPolicyRule.d.ts +2 -1
  85. package/build/rules/AlwaysDenyPrivacyPolicyRule.js +1 -1
  86. package/build/rules/AlwaysDenyPrivacyPolicyRule.js.map +1 -1
  87. package/build/rules/AlwaysSkipPrivacyPolicyRule.d.ts +2 -1
  88. package/build/rules/AlwaysSkipPrivacyPolicyRule.js +1 -1
  89. package/build/rules/AlwaysSkipPrivacyPolicyRule.js.map +1 -1
  90. package/build/rules/PrivacyPolicyRule.d.ts +2 -1
  91. package/build/rules/PrivacyPolicyRule.js.map +1 -1
  92. package/build/rules/__tests__/AlwaysAllowPrivacyPolicyRule-test.js +1 -0
  93. package/build/rules/__tests__/AlwaysAllowPrivacyPolicyRule-test.js.map +1 -1
  94. package/build/rules/__tests__/AlwaysDenyPrivacyPolicyRule-test.js +1 -0
  95. package/build/rules/__tests__/AlwaysDenyPrivacyPolicyRule-test.js.map +1 -1
  96. package/build/rules/__tests__/AlwaysSkipPrivacyPolicyRule-test.js +1 -0
  97. package/build/rules/__tests__/AlwaysSkipPrivacyPolicyRule-test.js.map +1 -1
  98. package/build/utils/testing/PrivacyPolicyRuleTestUtils.d.ts +2 -0
  99. package/build/utils/testing/PrivacyPolicyRuleTestUtils.js +6 -6
  100. package/build/utils/testing/PrivacyPolicyRuleTestUtils.js.map +1 -1
  101. package/build/utils/testing/StubCacheAdapter.d.ts +6 -9
  102. package/build/utils/testing/StubCacheAdapter.js +0 -6
  103. package/build/utils/testing/StubCacheAdapter.js.map +1 -1
  104. package/package.json +1 -1
  105. package/src/ComposedEntityCacheAdapter.ts +86 -0
  106. package/src/ComposedSecondaryEntityCache.ts +63 -0
  107. package/src/Entity.ts +6 -4
  108. package/src/EntityAssociationLoader.ts +4 -4
  109. package/src/EntityCacheAdapter.ts +2 -10
  110. package/src/EntityFieldDefinition.ts +8 -0
  111. package/src/EntityFields.ts +45 -0
  112. package/src/EntityLoader.ts +5 -1
  113. package/src/EntityLoaderFactory.ts +4 -2
  114. package/src/EntityMutationInfo.ts +13 -3
  115. package/src/EntityMutator.ts +44 -21
  116. package/src/EntityMutatorFactory.ts +10 -4
  117. package/src/EntityPrivacyPolicy.ts +31 -1
  118. package/src/GenericSecondaryEntityCache.ts +98 -0
  119. package/src/IEntityGenericCacher.ts +15 -0
  120. package/src/ReadonlyEntity.ts +1 -1
  121. package/src/ViewerScopedEntityLoaderFactory.ts +8 -3
  122. package/src/ViewerScopedEntityMutatorFactory.ts +22 -7
  123. package/src/__tests__/ComposedCacheAdapter-test.ts +280 -0
  124. package/src/__tests__/ComposedSecondaryEntityCache-test.ts +101 -0
  125. package/src/__tests__/EntityCommonUseCases-test.ts +2 -1
  126. package/src/__tests__/EntityEdges-test.ts +286 -45
  127. package/src/__tests__/EntityLoader-constructor-test.ts +3 -1
  128. package/src/__tests__/EntityLoader-test.ts +26 -1
  129. package/src/__tests__/EntityMutator-test.ts +99 -37
  130. package/src/__tests__/EntityPrivacyPolicy-test.ts +66 -7
  131. package/src/__tests__/EntitySecondaryCacheLoader-test.ts +1 -0
  132. package/src/__tests__/ViewerScopedEntityLoaderFactory-test.ts +4 -2
  133. package/src/__tests__/ViewerScopedEntityMutatorFactory-test.ts +6 -2
  134. package/src/index.ts +2 -0
  135. package/src/internal/ReadThroughEntityCache.ts +6 -28
  136. package/src/internal/__tests__/ReadThroughEntityCache-test.ts +0 -44
  137. package/src/rules/AlwaysAllowPrivacyPolicyRule.ts +2 -0
  138. package/src/rules/AlwaysDenyPrivacyPolicyRule.ts +2 -0
  139. package/src/rules/AlwaysSkipPrivacyPolicyRule.ts +2 -0
  140. package/src/rules/PrivacyPolicyRule.ts +2 -0
  141. package/src/rules/__tests__/AlwaysAllowPrivacyPolicyRule-test.ts +2 -0
  142. package/src/rules/__tests__/AlwaysDenyPrivacyPolicyRule-test.ts +2 -0
  143. package/src/rules/__tests__/AlwaysSkipPrivacyPolicyRule-test.ts +2 -0
  144. package/src/utils/testing/PrivacyPolicyRuleTestUtils.ts +14 -6
  145. 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(viewerContext, queryContext, entity);
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
+ }
@@ -156,6 +156,6 @@ export default abstract class ReadonlyEntity<
156
156
  return viewerContext
157
157
  .getViewerScopedEntityCompanionForClass(this)
158
158
  .getLoaderFactory()
159
- .forLoad(queryContext);
159
+ .forLoad(queryContext, { cascadingDeleteCause: null });
160
160
  }
161
161
  }
@@ -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(this.viewerContext, queryContext);
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(this.viewerContext, queryContext);
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(existingEntity, queryContext);
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(existingEntity, queryContext);
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
+ });