@expo/entity 0.23.0 → 0.25.1
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/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 +4 -0
- package/build/index.js +7 -1
- package/build/index.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/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/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 +4 -0
- 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
|
@@ -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')) {
|