@expo/entity 0.24.0 → 0.25.2

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 (117) 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/EntityLoader.d.ts +3 -2
  12. package/build/EntityLoader.js +5 -4
  13. package/build/EntityLoader.js.map +1 -1
  14. package/build/EntityLoaderFactory.d.ts +2 -2
  15. package/build/EntityLoaderFactory.js +2 -2
  16. package/build/EntityLoaderFactory.js.map +1 -1
  17. package/build/EntityMutationInfo.d.ts +12 -3
  18. package/build/EntityMutator.d.ts +5 -4
  19. package/build/EntityMutator.js +31 -24
  20. package/build/EntityMutator.js.map +1 -1
  21. package/build/EntityMutatorFactory.d.ts +4 -4
  22. package/build/EntityMutatorFactory.js +6 -6
  23. package/build/EntityMutatorFactory.js.map +1 -1
  24. package/build/EntityPrivacyPolicy.d.ts +15 -4
  25. package/build/EntityPrivacyPolicy.js +14 -14
  26. package/build/EntityPrivacyPolicy.js.map +1 -1
  27. package/build/IEntityGenericCacher.d.ts +2 -2
  28. package/build/ReadonlyEntity.js +1 -1
  29. package/build/ReadonlyEntity.js.map +1 -1
  30. package/build/ViewerScopedEntityLoaderFactory.d.ts +2 -2
  31. package/build/ViewerScopedEntityLoaderFactory.js +2 -2
  32. package/build/ViewerScopedEntityLoaderFactory.js.map +1 -1
  33. package/build/ViewerScopedEntityMutatorFactory.d.ts +4 -4
  34. package/build/ViewerScopedEntityMutatorFactory.js +6 -6
  35. package/build/ViewerScopedEntityMutatorFactory.js.map +1 -1
  36. package/build/__tests__/ComposedCacheAdapter-test.d.ts +1 -0
  37. package/build/__tests__/ComposedCacheAdapter-test.js +198 -0
  38. package/build/__tests__/ComposedCacheAdapter-test.js.map +1 -0
  39. package/build/__tests__/ComposedSecondaryEntityCache-test.d.ts +1 -0
  40. package/build/__tests__/ComposedSecondaryEntityCache-test.js +65 -0
  41. package/build/__tests__/ComposedSecondaryEntityCache-test.js.map +1 -0
  42. package/build/__tests__/EntityCommonUseCases-test.js +1 -1
  43. package/build/__tests__/EntityCommonUseCases-test.js.map +1 -1
  44. package/build/__tests__/EntityEdges-test.js +260 -37
  45. package/build/__tests__/EntityEdges-test.js.map +1 -1
  46. package/build/__tests__/EntityLoader-constructor-test.js +2 -1
  47. package/build/__tests__/EntityLoader-constructor-test.js.map +1 -1
  48. package/build/__tests__/EntityLoader-test.js +19 -11
  49. package/build/__tests__/EntityLoader-test.js.map +1 -1
  50. package/build/__tests__/EntityMutator-test.js +96 -43
  51. package/build/__tests__/EntityMutator-test.js.map +1 -1
  52. package/build/__tests__/EntityPrivacyPolicy-test.js +23 -12
  53. package/build/__tests__/EntityPrivacyPolicy-test.js.map +1 -1
  54. package/build/__tests__/EntitySecondaryCacheLoader-test.js +1 -1
  55. package/build/__tests__/EntitySecondaryCacheLoader-test.js.map +1 -1
  56. package/build/__tests__/ViewerScopedEntityLoaderFactory-test.js +3 -2
  57. package/build/__tests__/ViewerScopedEntityLoaderFactory-test.js.map +1 -1
  58. package/build/__tests__/ViewerScopedEntityMutatorFactory-test.js +3 -2
  59. package/build/__tests__/ViewerScopedEntityMutatorFactory-test.js.map +1 -1
  60. package/build/index.d.ts +2 -0
  61. package/build/index.js +5 -1
  62. package/build/index.js.map +1 -1
  63. package/build/rules/AlwaysAllowPrivacyPolicyRule.d.ts +2 -1
  64. package/build/rules/AlwaysAllowPrivacyPolicyRule.js +1 -1
  65. package/build/rules/AlwaysAllowPrivacyPolicyRule.js.map +1 -1
  66. package/build/rules/AlwaysDenyPrivacyPolicyRule.d.ts +2 -1
  67. package/build/rules/AlwaysDenyPrivacyPolicyRule.js +1 -1
  68. package/build/rules/AlwaysDenyPrivacyPolicyRule.js.map +1 -1
  69. package/build/rules/AlwaysSkipPrivacyPolicyRule.d.ts +2 -1
  70. package/build/rules/AlwaysSkipPrivacyPolicyRule.js +1 -1
  71. package/build/rules/AlwaysSkipPrivacyPolicyRule.js.map +1 -1
  72. package/build/rules/PrivacyPolicyRule.d.ts +2 -1
  73. package/build/rules/PrivacyPolicyRule.js.map +1 -1
  74. package/build/rules/__tests__/AlwaysAllowPrivacyPolicyRule-test.js +1 -0
  75. package/build/rules/__tests__/AlwaysAllowPrivacyPolicyRule-test.js.map +1 -1
  76. package/build/rules/__tests__/AlwaysDenyPrivacyPolicyRule-test.js +1 -0
  77. package/build/rules/__tests__/AlwaysDenyPrivacyPolicyRule-test.js.map +1 -1
  78. package/build/rules/__tests__/AlwaysSkipPrivacyPolicyRule-test.js +1 -0
  79. package/build/rules/__tests__/AlwaysSkipPrivacyPolicyRule-test.js.map +1 -1
  80. package/build/utils/testing/PrivacyPolicyRuleTestUtils.d.ts +2 -0
  81. package/build/utils/testing/PrivacyPolicyRuleTestUtils.js +6 -6
  82. package/build/utils/testing/PrivacyPolicyRuleTestUtils.js.map +1 -1
  83. package/package.json +1 -1
  84. package/src/ComposedEntityCacheAdapter.ts +86 -0
  85. package/src/ComposedSecondaryEntityCache.ts +63 -0
  86. package/src/Entity.ts +6 -4
  87. package/src/EntityAssociationLoader.ts +4 -4
  88. package/src/EntityLoader.ts +5 -1
  89. package/src/EntityLoaderFactory.ts +4 -2
  90. package/src/EntityMutationInfo.ts +13 -3
  91. package/src/EntityMutator.ts +44 -21
  92. package/src/EntityMutatorFactory.ts +10 -4
  93. package/src/EntityPrivacyPolicy.ts +31 -1
  94. package/src/IEntityGenericCacher.ts +2 -2
  95. package/src/ReadonlyEntity.ts +1 -1
  96. package/src/ViewerScopedEntityLoaderFactory.ts +8 -3
  97. package/src/ViewerScopedEntityMutatorFactory.ts +22 -7
  98. package/src/__tests__/ComposedCacheAdapter-test.ts +280 -0
  99. package/src/__tests__/ComposedSecondaryEntityCache-test.ts +101 -0
  100. package/src/__tests__/EntityCommonUseCases-test.ts +2 -1
  101. package/src/__tests__/EntityEdges-test.ts +286 -45
  102. package/src/__tests__/EntityLoader-constructor-test.ts +3 -1
  103. package/src/__tests__/EntityLoader-test.ts +26 -1
  104. package/src/__tests__/EntityMutator-test.ts +99 -37
  105. package/src/__tests__/EntityPrivacyPolicy-test.ts +66 -7
  106. package/src/__tests__/EntitySecondaryCacheLoader-test.ts +1 -0
  107. package/src/__tests__/ViewerScopedEntityLoaderFactory-test.ts +4 -2
  108. package/src/__tests__/ViewerScopedEntityMutatorFactory-test.ts +6 -2
  109. package/src/index.ts +2 -0
  110. package/src/rules/AlwaysAllowPrivacyPolicyRule.ts +2 -0
  111. package/src/rules/AlwaysDenyPrivacyPolicyRule.ts +2 -0
  112. package/src/rules/AlwaysSkipPrivacyPolicyRule.ts +2 -0
  113. package/src/rules/PrivacyPolicyRule.ts +2 -0
  114. package/src/rules/__tests__/AlwaysAllowPrivacyPolicyRule-test.ts +2 -0
  115. package/src/rules/__tests__/AlwaysDenyPrivacyPolicyRule-test.ts +2 -0
  116. package/src/rules/__tests__/AlwaysSkipPrivacyPolicyRule-test.ts +2 -0
  117. package/src/utils/testing/PrivacyPolicyRuleTestUtils.ts +14 -6
@@ -5,7 +5,7 @@ import EntityLoaderFactory from './EntityLoaderFactory';
5
5
  import EntityMutationTriggerConfiguration from './EntityMutationTriggerConfiguration';
6
6
  import EntityMutationValidator from './EntityMutationValidator';
7
7
  import { CreateMutator, UpdateMutator, DeleteMutator } from './EntityMutator';
8
- import EntityPrivacyPolicy from './EntityPrivacyPolicy';
8
+ import EntityPrivacyPolicy, { EntityPrivacyPolicyEvaluationContext } from './EntityPrivacyPolicy';
9
9
  import { EntityQueryContext } from './EntityQueryContext';
10
10
  import ViewerContext from './ViewerContext';
11
11
  import IEntityMetricsAdapter from './metrics/IEntityMetricsAdapter';
@@ -72,11 +72,13 @@ export default class EntityMutatorFactory<
72
72
  */
73
73
  forCreate(
74
74
  viewerContext: TViewerContext,
75
- queryContext: EntityQueryContext
75
+ queryContext: EntityQueryContext,
76
+ privacyPolicyEvaluationContext: EntityPrivacyPolicyEvaluationContext
76
77
  ): CreateMutator<TFields, TID, TViewerContext, TEntity, TPrivacyPolicy, TSelectedFields> {
77
78
  return new CreateMutator(
78
79
  viewerContext,
79
80
  queryContext,
81
+ privacyPolicyEvaluationContext,
80
82
  this.entityConfiguration,
81
83
  this.entityClass,
82
84
  this.privacyPolicy,
@@ -96,11 +98,13 @@ export default class EntityMutatorFactory<
96
98
  */
97
99
  forUpdate(
98
100
  existingEntity: TEntity,
99
- queryContext: EntityQueryContext
101
+ queryContext: EntityQueryContext,
102
+ privacyPolicyEvaluationContext: EntityPrivacyPolicyEvaluationContext
100
103
  ): UpdateMutator<TFields, TID, TViewerContext, TEntity, TPrivacyPolicy, TSelectedFields> {
101
104
  return new UpdateMutator(
102
105
  existingEntity.getViewerContext(),
103
106
  queryContext,
107
+ privacyPolicyEvaluationContext,
104
108
  this.entityConfiguration,
105
109
  this.entityClass,
106
110
  this.privacyPolicy,
@@ -120,11 +124,13 @@ export default class EntityMutatorFactory<
120
124
  */
121
125
  forDelete(
122
126
  existingEntity: TEntity,
123
- queryContext: EntityQueryContext
127
+ queryContext: EntityQueryContext,
128
+ privacyPolicyEvaluationContext: EntityPrivacyPolicyEvaluationContext
124
129
  ): DeleteMutator<TFields, TID, TViewerContext, TEntity, TPrivacyPolicy, TSelectedFields> {
125
130
  return new DeleteMutator(
126
131
  existingEntity.getViewerContext(),
127
132
  queryContext,
133
+ privacyPolicyEvaluationContext,
128
134
  this.entityConfiguration,
129
135
  this.entityClass,
130
136
  this.privacyPolicy,
@@ -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<
@@ -9,7 +9,7 @@ export default interface IEntityGenericCacher<TFields> {
9
9
 
10
10
  cacheManyAsync(objectMap: ReadonlyMap<string, Readonly<TFields>>): Promise<void>;
11
11
 
12
- cacheDBMissesAsync(keys: string[]): Promise<void>;
12
+ cacheDBMissesAsync(keys: readonly string[]): Promise<void>;
13
13
 
14
- invalidateManyAsync(keys: string[]): Promise<void>;
14
+ invalidateManyAsync(keys: readonly string[]): Promise<void>;
15
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
+ });
@@ -0,0 +1,101 @@
1
+ import invariant from 'invariant';
2
+ import nullthrows from 'nullthrows';
3
+
4
+ import ComposedSecondaryEntityCache from '../ComposedSecondaryEntityCache';
5
+ import { ISecondaryEntityCache } from '../EntitySecondaryCacheLoader';
6
+
7
+ type TestFields = { id: string };
8
+ type TestLoadParams = { lp: string };
9
+
10
+ class TestEntitySecondaryCache implements ISecondaryEntityCache<TestFields, TestLoadParams> {
11
+ constructor(
12
+ private readonly prefilledResults: Map<Readonly<TestLoadParams>, Readonly<TestFields>>
13
+ ) {}
14
+
15
+ async loadManyThroughAsync(
16
+ loadParamsArray: readonly Readonly<TestLoadParams>[],
17
+ fetcher: (
18
+ fetcherLoadParamsArray: readonly Readonly<TestLoadParams>[]
19
+ ) => Promise<ReadonlyMap<Readonly<TestLoadParams>, Readonly<TestFields> | null>>
20
+ ): Promise<ReadonlyMap<Readonly<TestLoadParams>, Readonly<TestFields> | null>> {
21
+ // this does an unusual method of calling fetcher, but there's no constraint that says fetcher can only be called once
22
+ // so this tests that
23
+
24
+ const retMap = new Map<Readonly<TestLoadParams>, Readonly<TestFields> | null>();
25
+ for (const loadParams of loadParamsArray) {
26
+ if (this.prefilledResults.has(loadParams)) {
27
+ retMap.set(loadParams, nullthrows(this.prefilledResults.get(loadParams)));
28
+ } else {
29
+ const fetcherResult = await fetcher([loadParams]);
30
+ const toSet = fetcherResult.get(loadParams);
31
+ invariant(toSet !== undefined, 'should be set');
32
+ retMap.set(loadParams, toSet);
33
+ }
34
+ }
35
+ return retMap;
36
+ }
37
+
38
+ async invalidateManyAsync(loadParamsArray: readonly Readonly<TestLoadParams>[]): Promise<void> {
39
+ for (const loadParams of loadParamsArray) {
40
+ this.prefilledResults.delete(loadParams);
41
+ }
42
+ }
43
+ }
44
+
45
+ describe(ComposedSecondaryEntityCache, () => {
46
+ it('composes correctly', async () => {
47
+ // TODO(wschurman): investigate whether we can use immutable or something to do better object equality for the map keys
48
+ const lp1 = { lp: '1' };
49
+ const lp2 = { lp: '2' };
50
+ const lp3 = { lp: '3' };
51
+
52
+ const primarySecondaryEntityCache = new TestEntitySecondaryCache(
53
+ new Map([[lp1, { id: 'primary-1' }]])
54
+ );
55
+ const fallbackSecondaryEntityCache = new TestEntitySecondaryCache(
56
+ new Map([[lp2, { id: 'fallback-2' }]])
57
+ );
58
+
59
+ const composedSecondaryEntityCache = new ComposedSecondaryEntityCache([
60
+ primarySecondaryEntityCache,
61
+ fallbackSecondaryEntityCache,
62
+ ]);
63
+
64
+ const results = await composedSecondaryEntityCache.loadManyThroughAsync(
65
+ [lp1, lp2, lp3],
66
+ async (fetcherLoadParamsArray) =>
67
+ new Map(fetcherLoadParamsArray.map((flp) => [flp, { id: `db-fetched-${flp.lp}` }]))
68
+ );
69
+
70
+ expect(results.get(lp1)).toEqual({ id: 'primary-1' });
71
+ expect(results.get(lp2)).toEqual({ id: 'fallback-2' });
72
+ expect(results.get(lp3)).toEqual({ id: 'db-fetched-3' });
73
+
74
+ await composedSecondaryEntityCache.invalidateManyAsync([lp1, lp2, lp3]);
75
+
76
+ const resultsAfterInvalidate = await composedSecondaryEntityCache.loadManyThroughAsync(
77
+ [lp1, lp2, lp3],
78
+ async (fetcherLoadParamsArray) =>
79
+ new Map(fetcherLoadParamsArray.map((flp) => [flp, { id: `db-fetched-${flp.lp}` }]))
80
+ );
81
+
82
+ expect(resultsAfterInvalidate.get(lp1)).toEqual({ id: 'db-fetched-1' });
83
+ expect(resultsAfterInvalidate.get(lp2)).toEqual({ id: 'db-fetched-2' });
84
+ expect(resultsAfterInvalidate.get(lp3)).toEqual({ id: 'db-fetched-3' });
85
+ });
86
+
87
+ it('handles n=0 compose case', async () => {
88
+ const lp1 = { lp: '1' };
89
+ const composedSecondaryEntityCache = new ComposedSecondaryEntityCache<
90
+ TestLoadParams,
91
+ TestFields
92
+ >([]);
93
+ const results = await composedSecondaryEntityCache.loadManyThroughAsync(
94
+ [lp1],
95
+ async (fetcherLoadParamsArray) =>
96
+ new Map(fetcherLoadParamsArray.map((flp) => [flp, { id: `db-fetched-${flp.lp}` }]))
97
+ );
98
+
99
+ expect(results.get(lp1)).toEqual({ id: 'db-fetched-1' });
100
+ });
101
+ });
@@ -5,7 +5,7 @@ import Entity from '../Entity';
5
5
  import EntityCompanionProvider, { EntityCompanionDefinition } from '../EntityCompanionProvider';
6
6
  import EntityConfiguration from '../EntityConfiguration';
7
7
  import { UUIDField } from '../EntityFields';
8
- import EntityPrivacyPolicy from '../EntityPrivacyPolicy';
8
+ import EntityPrivacyPolicy, { EntityPrivacyPolicyEvaluationContext } from '../EntityPrivacyPolicy';
9
9
  import { EntityQueryContext } from '../EntityQueryContext';
10
10
  import ViewerContext from '../ViewerContext';
11
11
  import { enforceResultsAsync } from '../entityUtils';
@@ -51,6 +51,7 @@ class DenyIfNotOwnerPrivacyPolicyRule extends PrivacyPolicyRule<
51
51
  async evaluateAsync(
52
52
  viewerContext: TestUserViewerContext,
53
53
  _queryContext: EntityQueryContext,
54
+ _evaluationContext: EntityPrivacyPolicyEvaluationContext,
54
55
  entity: BlahEntity
55
56
  ): Promise<RuleEvaluationResult> {
56
57
  if (viewerContext.getUserID() === entity.getField('ownerID')) {