@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 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
- ```typescript
12
- const secondaryCacheLoader = new TestSecondaryRedisCacheLoader(
13
- new RedisSecondaryEntityCache(
14
- redisTestEntityConfiguration,
15
- genericRedisCacheContext,
16
- (loadParams) => `${loadParams.id}`
17
- ),
18
- RedisTestEntity.loader(viewerContext)
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
- ```typescript
23
- const loadParams = { id: createdEntity.getID() };
24
- const results = await secondaryCacheLoader.loadManyAsync([loadParams]);
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, constructRedisKey: (params: Readonly<TLoadParams>) => string);
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, constructRedisKey) {
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.63.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 eslint src",
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.63.0",
32
- "@expo/entity-cache-adapter-redis": "^0.63.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": "5.9.3"
35
+ "typescript": "6.0.3"
36
+ },
37
+ "engines": {
38
+ "node": ">=18"
39
39
  },
40
- "gitHead": "dbd1cc847952754acc0eb407165cbb7b8350d2df"
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
- constructRedisKey: (params: Readonly<TLoadParams>) => string,
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
- makeKeyFn(..._parts: string[]): string {
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
- (loadParams) => `test-key-${loadParams.id}`,
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
- (loadParams) => `test-key-${loadParams.id}`,
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 UUIDField({
55
+ id: new StringField({
57
56
  columnName: 'id',
58
57
  cache: false,
59
58
  }),