@expo/entity-secondary-cache-redis 0.63.0 → 0.64.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +14 -14
- package/build/src/RedisSecondaryEntityCache.d.ts +1 -1
- package/build/src/RedisSecondaryEntityCache.js +12 -1
- package/package.json +15 -15
- package/src/RedisSecondaryEntityCache.ts +14 -1
- package/src/__integration-tests__/RedisSecondaryEntityCache-integration-test.ts +40 -5
- package/src/__testfixtures__/RedisTestEntity.ts +1 -2
package/README.md
CHANGED
|
@@ -8,18 +8,18 @@
|
|
|
8
8
|
|
|
9
9
|
1. Create a concrete implementation of abstract class `EntitySecondaryCacheLoader`, in this example `TestEntitySecondaryCacheLoader`. The underlying data can come from anywhere, but an entity is constructed from the data and then authorized for the viewer.
|
|
10
10
|
2. Create an instance of your `EntitySecondaryCacheLoader`, passing in a `RedisSecondaryEntityCache`.
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
11
|
+
```typescript
|
|
12
|
+
const secondaryCacheLoader = new TestSecondaryRedisCacheLoader(
|
|
13
|
+
new RedisSecondaryEntityCache(
|
|
14
|
+
redisTestEntityConfiguration,
|
|
15
|
+
genericRedisCacheContext,
|
|
16
|
+
(loadParams) => `${loadParams.id}`,
|
|
17
|
+
),
|
|
18
|
+
RedisTestEntity.loader(viewerContext),
|
|
19
|
+
);
|
|
20
|
+
```
|
|
21
21
|
3. Load entities through it:
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
22
|
+
```typescript
|
|
23
|
+
const loadParams = { id: createdEntity.getID() };
|
|
24
|
+
const results = await secondaryCacheLoader.loadManyAsync([loadParams]);
|
|
25
|
+
```
|
|
@@ -5,5 +5,5 @@ import type { GenericRedisCacheContext } from '@expo/entity-cache-adapter-redis'
|
|
|
5
5
|
* A redis GenericSecondaryEntityCache.
|
|
6
6
|
*/
|
|
7
7
|
export declare class RedisSecondaryEntityCache<TFields extends Record<string, any>, TIDField extends keyof TFields, TLoadParams> extends GenericSecondaryEntityCache<TFields, TIDField, TLoadParams> {
|
|
8
|
-
constructor(entityConfiguration: EntityConfiguration<TFields, TIDField>, genericRedisCacheContext: GenericRedisCacheContext,
|
|
8
|
+
constructor(entityConfiguration: EntityConfiguration<TFields, TIDField>, genericRedisCacheContext: GenericRedisCacheContext, cacheKeyNamespace: string, constructRedisKeyParts: (params: Readonly<TLoadParams>) => readonly string[]);
|
|
9
9
|
}
|
|
@@ -4,7 +4,18 @@ import { GenericRedisCacher } from '@expo/entity-cache-adapter-redis';
|
|
|
4
4
|
* A redis GenericSecondaryEntityCache.
|
|
5
5
|
*/
|
|
6
6
|
export class RedisSecondaryEntityCache extends GenericSecondaryEntityCache {
|
|
7
|
-
constructor(entityConfiguration, genericRedisCacheContext,
|
|
7
|
+
constructor(entityConfiguration, genericRedisCacheContext, cacheKeyNamespace, constructRedisKeyParts) {
|
|
8
|
+
const constructRedisKey = (params) => {
|
|
9
|
+
const cacheKeyParts = [
|
|
10
|
+
genericRedisCacheContext.cacheKeyPrefix,
|
|
11
|
+
'secondary',
|
|
12
|
+
cacheKeyNamespace,
|
|
13
|
+
...constructRedisKeyParts(params),
|
|
14
|
+
];
|
|
15
|
+
const delimiter = genericRedisCacheContext.cacheKeyDelimiter;
|
|
16
|
+
const escape = (s) => s.replaceAll('\\', '\\\\').replaceAll(delimiter, `\\${delimiter}`);
|
|
17
|
+
return cacheKeyParts.map(escape).join(delimiter);
|
|
18
|
+
};
|
|
8
19
|
super(new GenericRedisCacher(genericRedisCacheContext, entityConfiguration), constructRedisKey);
|
|
9
20
|
}
|
|
10
21
|
}
|
package/package.json
CHANGED
|
@@ -1,41 +1,41 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@expo/entity-secondary-cache-redis",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.64.0",
|
|
4
4
|
"description": "Redis secondary cache for @expo/entity",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"entity"
|
|
7
|
+
],
|
|
8
|
+
"license": "MIT",
|
|
9
|
+
"author": "Expo",
|
|
5
10
|
"files": [
|
|
6
11
|
"build",
|
|
7
12
|
"!*.tsbuildinfo",
|
|
8
13
|
"!__*",
|
|
9
14
|
"src"
|
|
10
15
|
],
|
|
16
|
+
"type": "module",
|
|
11
17
|
"main": "build/src/index.js",
|
|
12
18
|
"types": "build/src/index.d.ts",
|
|
13
19
|
"scripts": {
|
|
14
20
|
"build": "tsc --build",
|
|
15
21
|
"prepack": "rm -rf build && yarn build",
|
|
16
22
|
"clean": "yarn build --clean",
|
|
17
|
-
"lint": "yarn run --top-level
|
|
23
|
+
"lint": "yarn run --top-level oxlint --type-aware src",
|
|
18
24
|
"lint-fix": "yarn run lint --fix",
|
|
19
25
|
"integration": "yarn integration:all --rootDir $(pwd)"
|
|
20
26
|
},
|
|
21
|
-
"engines": {
|
|
22
|
-
"node": ">=18"
|
|
23
|
-
},
|
|
24
|
-
"keywords": [
|
|
25
|
-
"entity"
|
|
26
|
-
],
|
|
27
|
-
"author": "Expo",
|
|
28
|
-
"license": "MIT",
|
|
29
|
-
"type": "module",
|
|
30
27
|
"dependencies": {
|
|
31
|
-
"@expo/entity": "^0.
|
|
32
|
-
"@expo/entity-cache-adapter-redis": "^0.
|
|
28
|
+
"@expo/entity": "^0.64.0",
|
|
29
|
+
"@expo/entity-cache-adapter-redis": "^0.64.0"
|
|
33
30
|
},
|
|
34
31
|
"devDependencies": {
|
|
35
32
|
"@expo/nullthrows": "2.1.0",
|
|
36
33
|
"@jest/globals": "30.3.0",
|
|
37
34
|
"ioredis": "5.10.1",
|
|
38
|
-
"typescript": "
|
|
35
|
+
"typescript": "6.0.3"
|
|
36
|
+
},
|
|
37
|
+
"engines": {
|
|
38
|
+
"node": ">=18"
|
|
39
39
|
},
|
|
40
|
-
"gitHead": "
|
|
40
|
+
"gitHead": "3f10a9e70eab45ae95acdae133055d8a3ec04ce8"
|
|
41
41
|
}
|
|
@@ -14,8 +14,21 @@ export class RedisSecondaryEntityCache<
|
|
|
14
14
|
constructor(
|
|
15
15
|
entityConfiguration: EntityConfiguration<TFields, TIDField>,
|
|
16
16
|
genericRedisCacheContext: GenericRedisCacheContext,
|
|
17
|
-
|
|
17
|
+
cacheKeyNamespace: string,
|
|
18
|
+
constructRedisKeyParts: (params: Readonly<TLoadParams>) => readonly string[],
|
|
18
19
|
) {
|
|
20
|
+
const constructRedisKey = (params: Readonly<TLoadParams>): string => {
|
|
21
|
+
const cacheKeyParts = [
|
|
22
|
+
genericRedisCacheContext.cacheKeyPrefix,
|
|
23
|
+
'secondary',
|
|
24
|
+
cacheKeyNamespace,
|
|
25
|
+
...constructRedisKeyParts(params),
|
|
26
|
+
];
|
|
27
|
+
const delimiter = genericRedisCacheContext.cacheKeyDelimiter;
|
|
28
|
+
const escape = (s: string): string =>
|
|
29
|
+
s.replaceAll('\\', '\\\\').replaceAll(delimiter, `\\${delimiter}`);
|
|
30
|
+
return cacheKeyParts.map(escape).join(delimiter);
|
|
31
|
+
};
|
|
19
32
|
super(new GenericRedisCacher(genericRedisCacheContext, entityConfiguration), constructRedisKey);
|
|
20
33
|
}
|
|
21
34
|
}
|
|
@@ -82,9 +82,7 @@ describe(RedisSecondaryEntityCache, () => {
|
|
|
82
82
|
beforeAll(() => {
|
|
83
83
|
genericRedisCacheContext = {
|
|
84
84
|
redisClient,
|
|
85
|
-
|
|
86
|
-
throw new Error('should not be used by this test');
|
|
87
|
-
},
|
|
85
|
+
cacheKeyDelimiter: ':',
|
|
88
86
|
cacheKeyPrefix: 'test-',
|
|
89
87
|
ttlSecondsPositive: 86400, // 1 day
|
|
90
88
|
ttlSecondsNegative: 600, // 10 minutes
|
|
@@ -114,7 +112,8 @@ describe(RedisSecondaryEntityCache, () => {
|
|
|
114
112
|
new RedisSecondaryEntityCache(
|
|
115
113
|
redisTestEntityConfiguration,
|
|
116
114
|
genericRedisCacheContext,
|
|
117
|
-
|
|
115
|
+
'test-namespace',
|
|
116
|
+
(loadParams) => ['test-key', loadParams.id],
|
|
118
117
|
),
|
|
119
118
|
EntitySecondaryCacheLoader.getConstructionUtilsForEntityClass(RedisTestEntity, viewerContext),
|
|
120
119
|
RedisTestEntity.loaderWithAuthorizationResults(viewerContext),
|
|
@@ -154,7 +153,8 @@ describe(RedisSecondaryEntityCache, () => {
|
|
|
154
153
|
new RedisSecondaryEntityCache(
|
|
155
154
|
redisTestEntityConfiguration,
|
|
156
155
|
genericRedisCacheContext,
|
|
157
|
-
|
|
156
|
+
'test-namespace',
|
|
157
|
+
(loadParams) => ['test-key', loadParams.id],
|
|
158
158
|
),
|
|
159
159
|
EntitySecondaryCacheLoader.getConstructionUtilsForEntityClass(RedisTestEntity, viewerContext),
|
|
160
160
|
RedisTestEntity.loaderWithAuthorizationResults(viewerContext),
|
|
@@ -165,4 +165,39 @@ describe(RedisSecondaryEntityCache, () => {
|
|
|
165
165
|
expect(results.size).toBe(1);
|
|
166
166
|
expect(results.get(loadParams)).toBe(null);
|
|
167
167
|
});
|
|
168
|
+
|
|
169
|
+
it('does not collapse cache keys with delimiter-bearing parts and uses namespace', async () => {
|
|
170
|
+
const viewerContext = new TestViewerContext(
|
|
171
|
+
createRedisIntegrationTestEntityCompanionProvider(genericRedisCacheContext),
|
|
172
|
+
);
|
|
173
|
+
|
|
174
|
+
const id = 'id-with-delimiter-' + genericRedisCacheContext.cacheKeyDelimiter;
|
|
175
|
+
|
|
176
|
+
const createdEntity = await RedisTestEntity.creator(viewerContext)
|
|
177
|
+
.setField('id', id)
|
|
178
|
+
.setField('name', 'wat')
|
|
179
|
+
.createAsync();
|
|
180
|
+
|
|
181
|
+
const secondaryCacheLoader = new TestSecondaryRedisCacheLoader(
|
|
182
|
+
new RedisSecondaryEntityCache(
|
|
183
|
+
redisTestEntityConfiguration,
|
|
184
|
+
genericRedisCacheContext,
|
|
185
|
+
'test-namespace',
|
|
186
|
+
(loadParams) => ['test-key', loadParams.id],
|
|
187
|
+
),
|
|
188
|
+
EntitySecondaryCacheLoader.getConstructionUtilsForEntityClass(RedisTestEntity, viewerContext),
|
|
189
|
+
RedisTestEntity.loaderWithAuthorizationResults(viewerContext),
|
|
190
|
+
);
|
|
191
|
+
|
|
192
|
+
const loadParams = { id };
|
|
193
|
+
const results = await secondaryCacheLoader.loadManyAsync([loadParams]);
|
|
194
|
+
expect(results.size).toBe(1);
|
|
195
|
+
expect(nullthrows(results.get(loadParams)).enforceValue().getID()).toEqual(
|
|
196
|
+
createdEntity.getID(),
|
|
197
|
+
);
|
|
198
|
+
|
|
199
|
+
const redisKey = `${genericRedisCacheContext.cacheKeyPrefix}:secondary:test-namespace:test-key:id-with-delimiter-\\${genericRedisCacheContext.cacheKeyDelimiter}`;
|
|
200
|
+
const redisValues = await genericRedisCacheContext.redisClient.mget(redisKey);
|
|
201
|
+
expect(redisValues[0]).toBeTruthy();
|
|
202
|
+
});
|
|
168
203
|
});
|
|
@@ -5,7 +5,6 @@ import {
|
|
|
5
5
|
EntityConfiguration,
|
|
6
6
|
EntityPrivacyPolicy,
|
|
7
7
|
StringField,
|
|
8
|
-
UUIDField,
|
|
9
8
|
} from '@expo/entity';
|
|
10
9
|
|
|
11
10
|
export type RedisTestEntityFields = {
|
|
@@ -53,7 +52,7 @@ export const redisTestEntityConfiguration = new EntityConfiguration<RedisTestEnt
|
|
|
53
52
|
idField: 'id',
|
|
54
53
|
tableName: 'redis_test_entities',
|
|
55
54
|
schema: {
|
|
56
|
-
id: new
|
|
55
|
+
id: new StringField({
|
|
57
56
|
columnName: 'id',
|
|
58
57
|
cache: false,
|
|
59
58
|
}),
|