Compare commits

...
Author SHA1 Message Date
Quinlan Jung 41bd0722c9 v0.23.0 (#155)
* v0.23.0

* update changelog
2022-02-09 13:59:40 -08:00
Quinlan Jung 03a1fce067 move responsibility of transformer to generic redis cacher (#153)
* will's commit

* integ tests

* fix codecov
2022-02-07 15:04:02 -08:00
24 changed files with 269 additions and 164 deletions

No files matched your search

+9
View File
@@ -3,6 +3,15 @@
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [0.23.0](https://github.com/expo/entity/compare/v0.22.0...v0.23.0) (2022-02-09)
### Breaking changes
* Move data transforming responsibilities to the CacheAdapter instead of the ReadThroughEntityCache. Affects any classes extending the EntityCacheAdapter and relying on the transformation behavior of the ReadThroughEntityCache. ([#153](https://github.com/expo/entity/pull/153))
# [0.22.0](https://github.com/expo/entity/compare/v0.21.0...v0.22.0) (2022-02-04) # [0.22.0](https://github.com/expo/entity/compare/v0.21.0...v0.22.0) (2022-02-04)
+1 -1
View File
@@ -4,5 +4,5 @@
], ],
"npmClient": "yarn", "npmClient": "yarn",
"useWorkspaces": true, "useWorkspaces": true,
"version": "0.22.0" "version": "0.23.0"
} }
@@ -3,6 +3,15 @@
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [0.23.0](https://github.com/expo/entity/compare/v0.22.0...v0.23.0) (2022-02-09)
### Chores
* Move data transforming responsibilities to the CacheAdapter instead of the ReadThroughEntityCache. ([#153](https://github.com/expo/entity/pull/153))
# [0.22.0](https://github.com/expo/entity/compare/v0.21.0...v0.22.0) (2022-02-04) # [0.22.0](https://github.com/expo/entity/compare/v0.21.0...v0.22.0) (2022-02-04)
**Note:** Version bump only for package @expo/entity-cache-adapter-redis **Note:** Version bump only for package @expo/entity-cache-adapter-redis
@@ -1,6 +1,6 @@
{ {
"name": "@expo/entity-cache-adapter-redis", "name": "@expo/entity-cache-adapter-redis",
"version": "0.22.0", "version": "0.23.0",
"description": "Redis cache adapter for @expo/entity", "description": "Redis cache adapter for @expo/entity",
"files": [ "files": [
"build", "build",
@@ -33,6 +33,6 @@
"ioredis": "^4.27.3" "ioredis": "^4.27.3"
}, },
"devDependencies": { "devDependencies": {
"@expo/entity": "^0.22.0" "@expo/entity": "^0.23.0"
} }
} }
@@ -1,6 +1,13 @@
import { CacheLoadResult, CacheStatus } from '@expo/entity'; import {
CacheLoadResult,
CacheStatus,
EntityConfiguration,
transformCacheObjectToFields,
transformFieldsToCacheObject,
} from '@expo/entity';
import { Redis } from 'ioredis'; import { Redis } from 'ioredis';
import { redisTransformerMap } from './RedisCommon';
import wrapNativeRedisCallAsync from './errors/wrapNativeRedisCallAsync'; import wrapNativeRedisCallAsync from './errors/wrapNativeRedisCallAsync';
// Sentinel value we store in Redis to negatively cache a database miss. // Sentinel value we store in Redis to negatively cache a database miss.
@@ -26,12 +33,15 @@ export interface GenericRedisCacheContext {
ttlSecondsNegative: number; ttlSecondsNegative: number;
} }
export default class GenericRedisCacher { export default class GenericRedisCacher<TFields> {
constructor(private readonly context: GenericRedisCacheContext) {} constructor(
private readonly context: GenericRedisCacheContext,
private readonly entityConfiguration: EntityConfiguration<TFields>
) {}
public async loadManyAsync( public async loadManyAsync(
keys: readonly string[] keys: readonly string[]
): Promise<ReadonlyMap<string, CacheLoadResult>> { ): Promise<ReadonlyMap<string, CacheLoadResult<TFields>>> {
if (keys.length === 0) { if (keys.length === 0) {
return new Map(); return new Map();
} }
@@ -40,7 +50,7 @@ export default class GenericRedisCacher {
this.context.redisClient.mget(...keys) this.context.redisClient.mget(...keys)
); );
const results = new Map<string, CacheLoadResult>(); const results = new Map<string, CacheLoadResult<TFields>>();
for (let i = 0; i < keys.length; i++) { for (let i = 0; i < keys.length; i++) {
const key = keys[i]!; const key = keys[i]!;
const redisResult = redisResults[i]; const redisResult = redisResults[i];
@@ -52,7 +62,11 @@ export default class GenericRedisCacher {
} else if (redisResult) { } else if (redisResult) {
results.set(key, { results.set(key, {
status: CacheStatus.HIT, status: CacheStatus.HIT,
item: JSON.parse(redisResult), item: transformCacheObjectToFields(
this.entityConfiguration,
redisTransformerMap,
JSON.parse(redisResult)
),
}); });
} else { } else {
results.set(key, { results.set(key, {
@@ -63,7 +77,7 @@ export default class GenericRedisCacher {
return results; return results;
} }
public async cacheManyAsync(objectMap: ReadonlyMap<string, object>): Promise<void> { public async cacheManyAsync(objectMap: ReadonlyMap<string, Readonly<TFields>>): Promise<void> {
if (objectMap.size === 0) { if (objectMap.size === 0) {
return; return;
} }
@@ -72,7 +86,9 @@ export default class GenericRedisCacher {
objectMap.forEach((object, key) => { objectMap.forEach((object, key) => {
redisTransaction = redisTransaction.set( redisTransaction = redisTransaction.set(
key, key,
JSON.stringify(object), JSON.stringify(
transformFieldsToCacheObject(this.entityConfiguration, redisTransformerMap, object)
),
'EX', 'EX',
this.context.ttlSecondsPositive this.context.ttlSecondsPositive
); );
@@ -1,15 +1,8 @@
import { import { EntityCacheAdapter, EntityConfiguration, CacheLoadResult, mapKeys } from '@expo/entity';
EntityCacheAdapter,
EntityConfiguration,
CacheLoadResult,
FieldTransformerMap,
mapKeys,
} from '@expo/entity';
import invariant from 'invariant'; import invariant from 'invariant';
import { Redis } from 'ioredis'; import { Redis } from 'ioredis';
import GenericRedisCacher from './GenericRedisCacher'; import GenericRedisCacher from './GenericRedisCacher';
import { redisTransformerMap } from './RedisCommon';
export interface RedisCacheAdapterContext { export interface RedisCacheAdapterContext {
/** /**
@@ -49,28 +42,27 @@ export interface RedisCacheAdapterContext {
} }
export default class RedisCacheAdapter<TFields> extends EntityCacheAdapter<TFields> { export default class RedisCacheAdapter<TFields> extends EntityCacheAdapter<TFields> {
private readonly genericRedisCacher: GenericRedisCacher; private readonly genericRedisCacher: GenericRedisCacher<TFields>;
constructor( constructor(
entityConfiguration: EntityConfiguration<TFields>, entityConfiguration: EntityConfiguration<TFields>,
private readonly context: RedisCacheAdapterContext private readonly context: RedisCacheAdapterContext
) { ) {
super(entityConfiguration); super(entityConfiguration);
this.genericRedisCacher = new GenericRedisCacher({ this.genericRedisCacher = new GenericRedisCacher(
{
redisClient: context.redisClient, redisClient: context.redisClient,
ttlSecondsNegative: context.ttlSecondsNegative, ttlSecondsNegative: context.ttlSecondsNegative,
ttlSecondsPositive: context.ttlSecondsPositive, ttlSecondsPositive: context.ttlSecondsPositive,
}); },
} entityConfiguration
);
public getFieldTransformerMap(): FieldTransformerMap {
return redisTransformerMap;
} }
public async loadManyAsync<N extends keyof TFields>( public async loadManyAsync<N extends keyof TFields>(
fieldName: N, fieldName: N,
fieldValues: readonly NonNullable<TFields[N]>[] fieldValues: readonly NonNullable<TFields[N]>[]
): Promise<ReadonlyMap<NonNullable<TFields[N]>, CacheLoadResult>> { ): Promise<ReadonlyMap<NonNullable<TFields[N]>, CacheLoadResult<TFields>>> {
const redisCacheKeyToFieldValueMapping = new Map( const redisCacheKeyToFieldValueMapping = new Map(
fieldValues.map((fieldValue) => [this.makeCacheKey(fieldName, fieldValue), fieldValue]) fieldValues.map((fieldValue) => [this.makeCacheKey(fieldName, fieldValue), fieldValue])
); );
@@ -91,7 +83,7 @@ export default class RedisCacheAdapter<TFields> extends EntityCacheAdapter<TFiel
public async cacheManyAsync<N extends keyof TFields>( public async cacheManyAsync<N extends keyof TFields>(
fieldName: N, fieldName: N,
objectMap: ReadonlyMap<NonNullable<TFields[N]>, object> objectMap: ReadonlyMap<NonNullable<TFields[N]>, Readonly<TFields>>
): Promise<void> { ): Promise<void> {
await this.genericRedisCacher.cacheManyAsync( await this.genericRedisCacher.cacheManyAsync(
mapKeys(objectMap, (fieldValue) => this.makeCacheKey(fieldName, fieldValue)) mapKeys(objectMap, (fieldValue) => this.makeCacheKey(fieldName, fieldValue))
@@ -0,0 +1,125 @@
import { CacheStatus, ViewerContext } from '@expo/entity';
import Redis from 'ioredis';
import { URL } from 'url';
import GenericRedisCacher from '../GenericRedisCacher';
import { RedisCacheAdapterContext } from '../RedisCacheAdapter';
import RedisTestEntity, {
redisTestEntityConfiguration,
RedisTestEntityFields,
} from '../testfixtures/RedisTestEntity';
import { createRedisIntegrationTestEntityCompanionProvider } from '../testfixtures/createRedisIntegrationTestEntityCompanionProvider';
class TestViewerContext extends ViewerContext {}
describe(GenericRedisCacher, () => {
let redisCacheAdapterContext: RedisCacheAdapterContext;
beforeAll(() => {
redisCacheAdapterContext = {
redisClient: new Redis(new URL(process.env['REDIS_URL']!).toString()),
makeKeyFn(...parts: string[]): string {
const delimiter = ':';
const escapedParts = parts.map((part) =>
part.replace('\\', '\\\\').replace(delimiter, `\\${delimiter}`)
);
return escapedParts.join(delimiter);
},
cacheKeyPrefix: 'test-',
cacheKeyVersion: 1,
ttlSecondsPositive: 86400, // 1 day
ttlSecondsNegative: 600, // 10 minutes
};
});
beforeEach(async () => {
await redisCacheAdapterContext.redisClient.flushdb();
});
afterAll(async () => {
redisCacheAdapterContext.redisClient.disconnect();
});
it('has correct caching and loading behavior', async () => {
const viewerContext = new TestViewerContext(
createRedisIntegrationTestEntityCompanionProvider(redisCacheAdapterContext)
);
const genericRedisCacher = new GenericRedisCacher(
{
redisClient: redisCacheAdapterContext.redisClient,
ttlSecondsNegative: redisCacheAdapterContext.ttlSecondsNegative,
ttlSecondsPositive: redisCacheAdapterContext.ttlSecondsPositive,
},
redisTestEntityConfiguration
);
const date = new Date();
const entity1Created = await RedisTestEntity.creator(viewerContext)
.setField('name', 'blah')
.setField('dateField', date)
.enforceCreateAsync();
const testKey = `test-id-key-${entity1Created.getID()}`;
const objectMap = new Map<string, Readonly<RedisTestEntityFields>>([
[testKey, entity1Created.getAllFields()],
]);
await genericRedisCacher.cacheManyAsync(objectMap);
const cachedJSON = await redisCacheAdapterContext.redisClient.get(testKey);
const cachedValue = JSON.parse(cachedJSON!);
expect(cachedValue).toMatchObject({
id: entity1Created.getID(),
dateField: date.toISOString(),
});
const loadedObjectMap = await genericRedisCacher.loadManyAsync([testKey]);
const cacheLoadResult = loadedObjectMap.get(testKey)!;
expect(cacheLoadResult).toMatchObject({
status: CacheStatus.HIT,
item: entity1Created.getAllFields(),
});
expect(loadedObjectMap.size).toBe(1);
});
it('has correct negative caching behaviour', async () => {
const genericRedisCacher = new GenericRedisCacher(
{
redisClient: redisCacheAdapterContext.redisClient,
ttlSecondsNegative: redisCacheAdapterContext.ttlSecondsNegative,
ttlSecondsPositive: redisCacheAdapterContext.ttlSecondsPositive,
},
redisTestEntityConfiguration
);
const testKey = `test-id-key-non-existent-id`;
await genericRedisCacher.cacheDBMissesAsync([testKey]);
const loadedObjectMap = await genericRedisCacher.loadManyAsync([testKey]);
const cacheLoadResult = loadedObjectMap.get(testKey)!;
expect(cacheLoadResult.status).toBe(CacheStatus.NEGATIVE);
});
it('has correct invalidation behaviour', async () => {
const viewerContext = new TestViewerContext(
createRedisIntegrationTestEntityCompanionProvider(redisCacheAdapterContext)
);
const genericRedisCacher = new GenericRedisCacher(
{
redisClient: redisCacheAdapterContext.redisClient,
ttlSecondsNegative: redisCacheAdapterContext.ttlSecondsNegative,
ttlSecondsPositive: redisCacheAdapterContext.ttlSecondsPositive,
},
redisTestEntityConfiguration
);
const date = new Date();
const entity1Created = await RedisTestEntity.creator(viewerContext)
.setField('name', 'blah')
.setField('dateField', date)
.enforceCreateAsync();
const testKey = `test-id-key-${entity1Created.getID()}`;
const objectMap = new Map<string, Readonly<RedisTestEntityFields>>([
[testKey, entity1Created.getAllFields()],
]);
await genericRedisCacher.cacheManyAsync(objectMap);
await genericRedisCacher.invalidateManyAsync([testKey]);
const loadedObjectMap = await genericRedisCacher.loadManyAsync([testKey]);
const cacheLoadResult = loadedObjectMap.get(testKey)!;
expect(cacheLoadResult.status).toBe(CacheStatus.MISS);
});
});
@@ -3,6 +3,14 @@
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [0.23.0](https://github.com/expo/entity/compare/v0.22.0...v0.23.0) (2022-02-09)
**Note:** Version bump only for package @expo/entity-database-adapter-knex
# [0.22.0](https://github.com/expo/entity/compare/v0.21.0...v0.22.0) (2022-02-04) # [0.22.0](https://github.com/expo/entity/compare/v0.21.0...v0.22.0) (2022-02-04)
@@ -1,6 +1,6 @@
{ {
"name": "@expo/entity-database-adapter-knex", "name": "@expo/entity-database-adapter-knex",
"version": "0.22.0", "version": "0.23.0",
"description": "Knex database adapter for @expo/entity", "description": "Knex database adapter for @expo/entity",
"files": [ "files": [
"build", "build",
@@ -33,7 +33,7 @@
"knex": "^1.0.2" "knex": "^1.0.2"
}, },
"devDependencies": { "devDependencies": {
"@expo/entity": "^0.22.0", "@expo/entity": "^0.23.0",
"pg": "^8.7.1" "pg": "^8.7.1"
} }
} }
+8
View File
@@ -3,6 +3,14 @@
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [0.23.0](https://github.com/expo/entity/compare/v0.22.0...v0.23.0) (2022-02-09)
**Note:** Version bump only for package @expo/entity-example
# [0.22.0](https://github.com/expo/entity/compare/v0.21.0...v0.22.0) (2022-02-04) # [0.22.0](https://github.com/expo/entity/compare/v0.21.0...v0.22.0) (2022-02-04)
**Note:** Version bump only for package @expo/entity-example **Note:** Version bump only for package @expo/entity-example
+2 -2
View File
@@ -1,7 +1,7 @@
{ {
"name": "@expo/entity-example", "name": "@expo/entity-example",
"private": true, "private": true,
"version": "0.22.0", "version": "0.23.0",
"description": "An example integration of the @expo/entity framework", "description": "An example integration of the @expo/entity framework",
"scripts": { "scripts": {
"tsc": "tsc", "tsc": "tsc",
@@ -23,7 +23,7 @@
"author": "Expo", "author": "Expo",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@expo/entity": "^0.22.0", "@expo/entity": "^0.23.0",
"apollo-server-koa": "^2.16.1", "apollo-server-koa": "^2.16.1",
"graphql": "^15.3.0", "graphql": "^15.3.0",
"koa": "^2.13.0", "koa": "^2.13.0",
@@ -3,6 +3,14 @@
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [0.23.0](https://github.com/expo/entity/compare/v0.22.0...v0.23.0) (2022-02-09)
**Note:** Version bump only for package @expo/entity-full-integration-tests
# [0.22.0](https://github.com/expo/entity/compare/v0.21.0...v0.22.0) (2022-02-04) # [0.22.0](https://github.com/expo/entity/compare/v0.21.0...v0.22.0) (2022-02-04)
**Note:** Version bump only for package @expo/entity-full-integration-tests **Note:** Version bump only for package @expo/entity-full-integration-tests
@@ -1,7 +1,7 @@
{ {
"name": "@expo/entity-full-integration-tests", "name": "@expo/entity-full-integration-tests",
"private": true, "private": true,
"version": "0.22.0", "version": "0.23.0",
"description": "Full redis and knex integration tests for the entity framework", "description": "Full redis and knex integration tests for the entity framework",
"scripts": { "scripts": {
"tsc": "tsc", "tsc": "tsc",
@@ -27,8 +27,8 @@
"@expo/entity-database-adapter-knex": "*" "@expo/entity-database-adapter-knex": "*"
}, },
"devDependencies": { "devDependencies": {
"@expo/entity": "^0.22.0", "@expo/entity": "^0.23.0",
"@expo/entity-cache-adapter-redis": "^0.22.0", "@expo/entity-cache-adapter-redis": "^0.23.0",
"@expo/entity-database-adapter-knex": "^0.22.0" "@expo/entity-database-adapter-knex": "^0.23.0"
} }
} }
@@ -3,6 +3,14 @@
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [0.23.0](https://github.com/expo/entity/compare/v0.22.0...v0.23.0) (2022-02-09)
**Note:** Version bump only for package @expo/entity-ip-address-field
# [0.22.0](https://github.com/expo/entity/compare/v0.21.0...v0.22.0) (2022-02-04) # [0.22.0](https://github.com/expo/entity/compare/v0.21.0...v0.22.0) (2022-02-04)
**Note:** Version bump only for package @expo/entity-ip-address-field **Note:** Version bump only for package @expo/entity-ip-address-field
@@ -1,6 +1,6 @@
{ {
"name": "@expo/entity-ip-address-field", "name": "@expo/entity-ip-address-field",
"version": "0.22.0", "version": "0.23.0",
"description": "IP address EntityField definitions for @expo/entity", "description": "IP address EntityField definitions for @expo/entity",
"files": [ "files": [
"build", "build",
@@ -28,7 +28,7 @@
"ip-address": "^8.1.0" "ip-address": "^8.1.0"
}, },
"devDependencies": { "devDependencies": {
"@expo/entity": "^0.22.0", "@expo/entity": "^0.23.0",
"@types/jsbn": "^1.2.29" "@types/jsbn": "^1.2.29"
}, },
"peerDependencies": { "peerDependencies": {
@@ -3,6 +3,15 @@
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [0.23.0](https://github.com/expo/entity/compare/v0.22.0...v0.23.0) (2022-02-09)
### Chores
* Move data transforming responsibilities to the CacheAdapter instead of the RedisSecondaryEntityCache. ([#153](https://github.com/expo/entity/pull/153))
# [0.22.0](https://github.com/expo/entity/compare/v0.21.0...v0.22.0) (2022-02-04) # [0.22.0](https://github.com/expo/entity/compare/v0.21.0...v0.22.0) (2022-02-04)
**Note:** Version bump only for package @expo/entity-secondary-cache-redis **Note:** Version bump only for package @expo/entity-secondary-cache-redis
@@ -1,6 +1,6 @@
{ {
"name": "@expo/entity-secondary-cache-redis", "name": "@expo/entity-secondary-cache-redis",
"version": "0.22.0", "version": "0.23.0",
"description": "Redis secondary cache for @expo/entity", "description": "Redis secondary cache for @expo/entity",
"files": [ "files": [
"build", "build",
@@ -34,8 +34,8 @@
"ioredis": "^4.17.3" "ioredis": "^4.17.3"
}, },
"devDependencies": { "devDependencies": {
"@expo/entity": "^0.22.0", "@expo/entity": "^0.23.0",
"@expo/entity-cache-adapter-redis": "^0.22.0", "@expo/entity-cache-adapter-redis": "^0.23.0",
"nullthrows": "^1.1.1" "nullthrows": "^1.1.1"
} }
} }
@@ -3,15 +3,9 @@ import {
EntityConfiguration, EntityConfiguration,
filterMap, filterMap,
ISecondaryEntityCache, ISecondaryEntityCache,
transformCacheObjectToFields,
transformFieldsToCacheObject,
zipToMap, zipToMap,
} from '@expo/entity'; } from '@expo/entity';
import { import { GenericRedisCacheContext, GenericRedisCacher } from '@expo/entity-cache-adapter-redis';
GenericRedisCacheContext,
GenericRedisCacher,
redisTransformerMap,
} from '@expo/entity-cache-adapter-redis';
import invariant from 'invariant'; import invariant from 'invariant';
/** /**
@@ -22,14 +16,14 @@ import invariant from 'invariant';
export default class RedisSecondaryEntityCache<TFields, TLoadParams> export default class RedisSecondaryEntityCache<TFields, TLoadParams>
implements ISecondaryEntityCache<TFields, TLoadParams> implements ISecondaryEntityCache<TFields, TLoadParams>
{ {
private readonly genericRedisCacher: GenericRedisCacher; private readonly genericRedisCacher: GenericRedisCacher<TFields>;
constructor( constructor(
private readonly entityConfiguration: EntityConfiguration<TFields>, entityConfiguration: EntityConfiguration<TFields>,
genericRedisCacheContext: GenericRedisCacheContext, genericRedisCacheContext: GenericRedisCacheContext,
private readonly constructRedisKey: (params: Readonly<TLoadParams>) => string private readonly constructRedisKey: (params: Readonly<TLoadParams>) => string
) { ) {
this.genericRedisCacher = new GenericRedisCacher(genericRedisCacheContext); this.genericRedisCacher = new GenericRedisCacher(genericRedisCacheContext, entityConfiguration);
} }
/** /**
@@ -74,14 +68,7 @@ export default class RedisSecondaryEntityCache<TFields, TLoadParams>
if (cacheLoadResult.status === CacheStatus.HIT) { if (cacheLoadResult.status === CacheStatus.HIT) {
const loadParams = redisKeyToLoadParamsMap.get(redisKey); const loadParams = redisKeyToLoadParamsMap.get(redisKey);
invariant(loadParams !== undefined, 'load params should be in redis key map'); invariant(loadParams !== undefined, 'load params should be in redis key map');
results.set( results.set(loadParams, cacheLoadResult.item);
loadParams,
transformCacheObjectToFields(
this.entityConfiguration,
redisTransformerMap,
cacheLoadResult.item
)
);
} }
}); });
@@ -103,13 +90,10 @@ export default class RedisSecondaryEntityCache<TFields, TLoadParams>
results.set(fetchMiss, null); results.set(fetchMiss, null);
} }
const objectsToCache: Map<string, object> = new Map(); const objectsToCache: Map<string, Readonly<TFields>> = new Map();
for (const [loadParams, object] of fetchResults.entries()) { for (const [loadParams, object] of fetchResults.entries()) {
if (object) { if (object) {
objectsToCache.set( objectsToCache.set(this.constructRedisKey(loadParams), object);
this.constructRedisKey(loadParams),
transformFieldsToCacheObject(this.entityConfiguration, redisTransformerMap, object)
);
results.set(loadParams, object); results.set(loadParams, object);
} }
} }
+9
View File
@@ -3,6 +3,15 @@
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [0.23.0](https://github.com/expo/entity/compare/v0.22.0...v0.23.0) (2022-02-09)
### Breaking changes
* Move data transforming responsibilities to the CacheAdapter instead of the ReadThroughEntityCache. Affects any classes extending the EntityCacheAdapter and relying on the transformation behavior of the ReadThroughEntityCache. ([#153](https://github.com/expo/entity/pull/153))
# [0.22.0](https://github.com/expo/entity/compare/v0.21.0...v0.22.0) (2022-02-04) # [0.22.0](https://github.com/expo/entity/compare/v0.21.0...v0.22.0) (2022-02-04)
**Note:** Version bump only for package @expo/entity **Note:** Version bump only for package @expo/entity
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@expo/entity", "name": "@expo/entity",
"version": "0.22.0", "version": "0.23.0",
"description": "A privacy-first data model", "description": "A privacy-first data model",
"files": [ "files": [
"build", "build",
+2 -10
View File
@@ -1,5 +1,4 @@
import EntityConfiguration from './EntityConfiguration'; import EntityConfiguration from './EntityConfiguration';
import { FieldTransformerMap } from './internal/EntityFieldTransformationUtils';
import { CacheLoadResult } from './internal/ReadThroughEntityCache'; import { CacheLoadResult } from './internal/ReadThroughEntityCache';
/** /**
@@ -9,13 +8,6 @@ import { CacheLoadResult } from './internal/ReadThroughEntityCache';
export default abstract class EntityCacheAdapter<TFields> { export default abstract class EntityCacheAdapter<TFields> {
constructor(protected readonly entityConfiguration: EntityConfiguration<TFields>) {} constructor(protected readonly entityConfiguration: EntityConfiguration<TFields>) {}
/**
* Transformer definitions for field types. Used to modify values as they are read from or written to
* the cache. Override in concrete subclasses to change transformation behavior.
* If a field type is not present in the map, then fields of that type will not be transformed.
*/
public abstract getFieldTransformerMap(): FieldTransformerMap;
/** /**
* Load many objects from cache. * Load many objects from cache.
* @param fieldName - object field being queried * @param fieldName - object field being queried
@@ -25,7 +17,7 @@ export default abstract class EntityCacheAdapter<TFields> {
public abstract loadManyAsync<N extends keyof TFields>( public abstract loadManyAsync<N extends keyof TFields>(
fieldName: N, fieldName: N,
fieldValues: readonly NonNullable<TFields[N]>[] fieldValues: readonly NonNullable<TFields[N]>[]
): Promise<ReadonlyMap<NonNullable<TFields[N]>, CacheLoadResult>>; ): Promise<ReadonlyMap<NonNullable<TFields[N]>, CacheLoadResult<TFields>>>;
/** /**
* Cache many objects fetched from the DB. * Cache many objects fetched from the DB.
@@ -34,7 +26,7 @@ export default abstract class EntityCacheAdapter<TFields> {
*/ */
public abstract cacheManyAsync<N extends keyof TFields>( public abstract cacheManyAsync<N extends keyof TFields>(
fieldName: N, fieldName: N,
objectMap: ReadonlyMap<NonNullable<TFields[N]>, object> objectMap: ReadonlyMap<NonNullable<TFields[N]>, Readonly<TFields>>
): Promise<void>; ): Promise<void>;
/** /**
@@ -3,11 +3,6 @@ import invariant from 'invariant';
import EntityCacheAdapter from '../EntityCacheAdapter'; import EntityCacheAdapter from '../EntityCacheAdapter';
import EntityConfiguration from '../EntityConfiguration'; import EntityConfiguration from '../EntityConfiguration';
import { filterMap } from '../utils/collections/maps'; import { filterMap } from '../utils/collections/maps';
import {
FieldTransformerMap,
transformCacheObjectToFields,
transformFieldsToCacheObject,
} from './EntityFieldTransformationUtils';
export enum CacheStatus { export enum CacheStatus {
HIT, HIT,
@@ -15,10 +10,10 @@ export enum CacheStatus {
NEGATIVE, NEGATIVE,
} }
export type CacheLoadResult = export type CacheLoadResult<TFields> =
| { | {
status: CacheStatus.HIT; status: CacheStatus.HIT;
item: Readonly<object>; item: Readonly<TFields>;
} }
| { | {
status: CacheStatus.MISS; status: CacheStatus.MISS;
@@ -32,14 +27,10 @@ export type CacheLoadResult =
* {@link EntityCacheAdapter} within the {@link EntityDataManager}. * {@link EntityCacheAdapter} within the {@link EntityDataManager}.
*/ */
export default class ReadThroughEntityCache<TFields> { export default class ReadThroughEntityCache<TFields> {
private readonly fieldTransformerMap: FieldTransformerMap;
constructor( constructor(
private readonly entityConfiguration: EntityConfiguration<TFields>, private readonly entityConfiguration: EntityConfiguration<TFields>,
private readonly entityCacheAdapter: EntityCacheAdapter<TFields> private readonly entityCacheAdapter: EntityCacheAdapter<TFields>
) { ) {}
this.fieldTransformerMap = entityCacheAdapter.getFieldTransformerMap();
}
private isFieldCacheable<N extends keyof TFields>(fieldName: N): boolean { private isFieldCacheable<N extends keyof TFields>(fieldName: N): boolean {
return this.entityConfiguration.cacheableKeys.has(fieldName); return this.entityConfiguration.cacheableKeys.has(fieldName);
@@ -91,13 +82,7 @@ export default class ReadThroughEntityCache<TFields> {
const results: Map<NonNullable<TFields[N]>, readonly Readonly<TFields>[]> = new Map(); const results: Map<NonNullable<TFields[N]>, readonly Readonly<TFields>[]> = new Map();
cacheLoadResults.forEach((cacheLoadResult, fieldValue) => { cacheLoadResults.forEach((cacheLoadResult, fieldValue) => {
if (cacheLoadResult.status === CacheStatus.HIT) { if (cacheLoadResult.status === CacheStatus.HIT) {
results.set(fieldValue, [ results.set(fieldValue, [cacheLoadResult.item]);
transformCacheObjectToFields(
this.entityConfiguration,
this.fieldTransformerMap,
cacheLoadResult.item
),
]);
} }
}); });
@@ -110,7 +95,7 @@ export default class ReadThroughEntityCache<TFields> {
return !objectsFromFulfillerForFv || objectsFromFulfillerForFv.length === 0; return !objectsFromFulfillerForFv || objectsFromFulfillerForFv.length === 0;
}); });
const objectsToCache: Map<NonNullable<TFields[N]>, object> = new Map(); const objectsToCache: Map<NonNullable<TFields[N]>, Readonly<TFields>> = new Map();
for (const [fieldValue, objects] of dbFetchResults.entries()) { for (const [fieldValue, objects] of dbFetchResults.entries()) {
if (objects.length > 1) { if (objects.length > 1) {
// multiple objects received for what was supposed to be a unique query, don't add to return map nor cache // multiple objects received for what was supposed to be a unique query, don't add to return map nor cache
@@ -123,14 +108,7 @@ export default class ReadThroughEntityCache<TFields> {
} }
const uniqueObject = objects[0]; const uniqueObject = objects[0];
if (uniqueObject) { if (uniqueObject) {
objectsToCache.set( objectsToCache.set(fieldValue, uniqueObject);
fieldValue,
transformFieldsToCacheObject(
this.entityConfiguration,
this.fieldTransformerMap,
uniqueObject
)
);
results.set(fieldValue, [uniqueObject]); results.set(fieldValue, [uniqueObject]);
} }
} }
@@ -40,7 +40,6 @@ describe(ReadThroughEntityCache, () => {
describe('readManyThroughAsync', () => { describe('readManyThroughAsync', () => {
it('fetches from DB upon cache miss and caches the result', async () => { it('fetches from DB upon cache miss and caches the result', async () => {
const cacheAdapterMock = mock<EntityCacheAdapter<BlahFields>>(); const cacheAdapterMock = mock<EntityCacheAdapter<BlahFields>>();
when(cacheAdapterMock.getFieldTransformerMap()).thenReturn(new Map());
const cacheAdapter = instance(cacheAdapterMock); const cacheAdapter = instance(cacheAdapterMock);
const entityCache = new ReadThroughEntityCache(makeEntityConfiguration(true), cacheAdapter); const entityCache = new ReadThroughEntityCache(makeEntityConfiguration(true), cacheAdapter);
const fetcher = createIdFetcher(['wat', 'who']); const fetcher = createIdFetcher(['wat', 'who']);
@@ -77,7 +76,6 @@ describe(ReadThroughEntityCache, () => {
it('does not fetch from the DB or cache results when all cache fetches are hits', async () => { it('does not fetch from the DB or cache results when all cache fetches are hits', async () => {
const cacheAdapterMock = mock<EntityCacheAdapter<BlahFields>>(); const cacheAdapterMock = mock<EntityCacheAdapter<BlahFields>>();
when(cacheAdapterMock.getFieldTransformerMap()).thenReturn(new Map());
const cacheAdapter = instance(cacheAdapterMock); const cacheAdapter = instance(cacheAdapterMock);
const entityCache = new ReadThroughEntityCache(makeEntityConfiguration(true), cacheAdapter); const entityCache = new ReadThroughEntityCache(makeEntityConfiguration(true), cacheAdapter);
const fetcher = createIdFetcher(['wat', 'who']); const fetcher = createIdFetcher(['wat', 'who']);
@@ -114,7 +112,6 @@ describe(ReadThroughEntityCache, () => {
it('negatively caches db misses', async () => { it('negatively caches db misses', async () => {
const cacheAdapterMock = mock<EntityCacheAdapter<BlahFields>>(); const cacheAdapterMock = mock<EntityCacheAdapter<BlahFields>>();
when(cacheAdapterMock.getFieldTransformerMap()).thenReturn(new Map());
const cacheAdapter = instance(cacheAdapterMock); const cacheAdapter = instance(cacheAdapterMock);
const entityCache = new ReadThroughEntityCache(makeEntityConfiguration(true), cacheAdapter); const entityCache = new ReadThroughEntityCache(makeEntityConfiguration(true), cacheAdapter);
@@ -135,7 +132,6 @@ describe(ReadThroughEntityCache, () => {
it('does not return or fetch negatively cached results from DB', async () => { it('does not return or fetch negatively cached results from DB', async () => {
const cacheAdapterMock = mock<EntityCacheAdapter<BlahFields>>(); const cacheAdapterMock = mock<EntityCacheAdapter<BlahFields>>();
when(cacheAdapterMock.getFieldTransformerMap()).thenReturn(new Map());
const cacheAdapter = instance(cacheAdapterMock); const cacheAdapter = instance(cacheAdapterMock);
const entityCache = new ReadThroughEntityCache(makeEntityConfiguration(true), cacheAdapter); const entityCache = new ReadThroughEntityCache(makeEntityConfiguration(true), cacheAdapter);
const fetcher = createIdFetcher([]); const fetcher = createIdFetcher([]);
@@ -153,7 +149,6 @@ describe(ReadThroughEntityCache, () => {
it('does a mix and match of hit, miss, and negative', async () => { it('does a mix and match of hit, miss, and negative', async () => {
const cacheAdapterMock = mock<EntityCacheAdapter<BlahFields>>(); const cacheAdapterMock = mock<EntityCacheAdapter<BlahFields>>();
when(cacheAdapterMock.getFieldTransformerMap()).thenReturn(new Map());
const cacheAdapter = instance(cacheAdapterMock); const cacheAdapter = instance(cacheAdapterMock);
const entityCache = new ReadThroughEntityCache(makeEntityConfiguration(true), cacheAdapter); const entityCache = new ReadThroughEntityCache(makeEntityConfiguration(true), cacheAdapter);
const fetcher = createIdFetcher(['wat', 'who', 'why']); const fetcher = createIdFetcher(['wat', 'who', 'why']);
@@ -189,7 +184,6 @@ describe(ReadThroughEntityCache, () => {
it('does not call into cache for field that is not cacheable', async () => { it('does not call into cache for field that is not cacheable', async () => {
const cacheAdapterMock = mock<EntityCacheAdapter<BlahFields>>(); const cacheAdapterMock = mock<EntityCacheAdapter<BlahFields>>();
when(cacheAdapterMock.getFieldTransformerMap()).thenReturn(new Map());
const cacheAdapter = instance(cacheAdapterMock); const cacheAdapter = instance(cacheAdapterMock);
const entityCache = new ReadThroughEntityCache(makeEntityConfiguration(false), cacheAdapter); const entityCache = new ReadThroughEntityCache(makeEntityConfiguration(false), cacheAdapter);
const fetcher = createIdFetcher(['wat']); const fetcher = createIdFetcher(['wat']);
@@ -197,44 +191,6 @@ describe(ReadThroughEntityCache, () => {
verify(cacheAdapterMock.loadManyAsync('id', anything())).never(); verify(cacheAdapterMock.loadManyAsync('id', anything())).never();
expect(result).toEqual(new Map([['wat', [{ id: 'wat' }]]])); expect(result).toEqual(new Map([['wat', [{ id: 'wat' }]]]));
}); });
it('transforms fields for cache storage', async () => {
const cacheAdapterMock = mock<EntityCacheAdapter<BlahFields>>();
when(cacheAdapterMock.getFieldTransformerMap()).thenReturn(
new Map([
[
UUIDField.name,
{
read: (val) => val.split('-')[0],
write: (val) => `${val}-in-cache`,
},
],
])
);
const cacheAdapter = instance(cacheAdapterMock);
const entityCache = new ReadThroughEntityCache(makeEntityConfiguration(true), cacheAdapter);
const fetcher = createIdFetcher(['wat', 'who']);
when(cacheAdapterMock.loadManyAsync('id', deepEqual(['wat', 'who']))).thenResolve(
new Map([
['wat', { status: CacheStatus.MISS }],
['who', { status: CacheStatus.HIT, item: { id: 'who-in-cache' } }],
])
);
const result = await entityCache.readManyThroughAsync('id', ['wat', 'who'], fetcher);
verify(cacheAdapterMock.loadManyAsync('id', deepEqual(['wat', 'who']))).once();
verify(
cacheAdapterMock.cacheManyAsync('id', deepEqual(new Map([['wat', { id: 'wat-in-cache' }]])))
).once();
expect(result).toEqual(
new Map([
['wat', [{ id: 'wat' }]],
['who', [{ id: 'who' }]],
])
);
});
}); });
describe('invalidateManyAsync', () => { describe('invalidateManyAsync', () => {
@@ -3,7 +3,6 @@ import invariant from 'invariant';
import EntityCacheAdapter from '../../EntityCacheAdapter'; import EntityCacheAdapter from '../../EntityCacheAdapter';
import EntityConfiguration from '../../EntityConfiguration'; import EntityConfiguration from '../../EntityConfiguration';
import IEntityCacheAdapterProvider from '../../IEntityCacheAdapterProvider'; import IEntityCacheAdapterProvider from '../../IEntityCacheAdapterProvider';
import { FieldTransformerMap } from '../../internal/EntityFieldTransformationUtils';
import { CacheStatus, CacheLoadResult } from '../../internal/ReadThroughEntityCache'; import { CacheStatus, CacheLoadResult } from '../../internal/ReadThroughEntityCache';
export class NoCacheStubCacheAdapterProvider implements IEntityCacheAdapterProvider { export class NoCacheStubCacheAdapterProvider implements IEntityCacheAdapterProvider {
@@ -15,15 +14,11 @@ export class NoCacheStubCacheAdapterProvider implements IEntityCacheAdapterProvi
} }
export class NoCacheStubCacheAdapter<TFields> extends EntityCacheAdapter<TFields> { export class NoCacheStubCacheAdapter<TFields> extends EntityCacheAdapter<TFields> {
public getFieldTransformerMap(): FieldTransformerMap {
return new Map();
}
public async loadManyAsync<N extends keyof TFields>( public async loadManyAsync<N extends keyof TFields>(
_fieldName: N, _fieldName: N,
fieldValues: readonly NonNullable<TFields[N]>[] fieldValues: readonly NonNullable<TFields[N]>[]
): Promise<ReadonlyMap<NonNullable<TFields[N]>, CacheLoadResult>> { ): Promise<ReadonlyMap<NonNullable<TFields[N]>, CacheLoadResult<TFields>>> {
return fieldValues.reduce((acc: Map<NonNullable<TFields[N]>, CacheLoadResult>, v) => { return fieldValues.reduce((acc: Map<NonNullable<TFields[N]>, CacheLoadResult<TFields>>, v) => {
acc.set(v, { acc.set(v, {
status: CacheStatus.MISS, status: CacheStatus.MISS,
}); });
@@ -33,7 +28,7 @@ export class NoCacheStubCacheAdapter<TFields> extends EntityCacheAdapter<TFields
public async cacheManyAsync<N extends keyof TFields>( public async cacheManyAsync<N extends keyof TFields>(
_fieldName: N, _fieldName: N,
_objectMap: ReadonlyMap<NonNullable<TFields[N]>, object> _objectMap: ReadonlyMap<NonNullable<TFields[N]>, Readonly<TFields>>
): Promise<void> {} ): Promise<void> {}
public async cacheDBMissesAsync<N extends keyof TFields>( public async cacheDBMissesAsync<N extends keyof TFields>(
@@ -53,27 +48,26 @@ export class InMemoryFullCacheStubCacheAdapterProvider implements IEntityCacheAd
getCacheAdapter<TFields>( getCacheAdapter<TFields>(
entityConfiguration: EntityConfiguration<TFields> entityConfiguration: EntityConfiguration<TFields>
): EntityCacheAdapter<TFields> { ): EntityCacheAdapter<TFields> {
return new InMemoryFullCacheStubCacheAdapter(entityConfiguration, this.cache); return new InMemoryFullCacheStubCacheAdapter(
entityConfiguration,
this.cache as Map<string, Readonly<TFields>>
);
} }
} }
export class InMemoryFullCacheStubCacheAdapter<TFields> extends EntityCacheAdapter<TFields> { export class InMemoryFullCacheStubCacheAdapter<TFields> extends EntityCacheAdapter<TFields> {
constructor( constructor(
entityConfiguration: EntityConfiguration<TFields>, entityConfiguration: EntityConfiguration<TFields>,
readonly cache: Map<string, Readonly<object>> readonly cache: Map<string, Readonly<TFields>>
) { ) {
super(entityConfiguration); super(entityConfiguration);
} }
public getFieldTransformerMap(): FieldTransformerMap {
return new Map();
}
public async loadManyAsync<N extends keyof TFields>( public async loadManyAsync<N extends keyof TFields>(
fieldName: N, fieldName: N,
fieldValues: readonly NonNullable<TFields[N]>[] fieldValues: readonly NonNullable<TFields[N]>[]
): Promise<ReadonlyMap<NonNullable<TFields[N]>, CacheLoadResult>> { ): Promise<ReadonlyMap<NonNullable<TFields[N]>, CacheLoadResult<TFields>>> {
const results = new Map<NonNullable<TFields[N]>, CacheLoadResult>(); const results = new Map<NonNullable<TFields[N]>, CacheLoadResult<TFields>>();
fieldValues.forEach((fieldValue) => { fieldValues.forEach((fieldValue) => {
const cacheKey = this.createCacheKey(fieldName, fieldValue); const cacheKey = this.createCacheKey(fieldName, fieldValue);
if (!this.cache.has(cacheKey)) { if (!this.cache.has(cacheKey)) {
@@ -94,7 +88,7 @@ export class InMemoryFullCacheStubCacheAdapter<TFields> extends EntityCacheAdapt
public async cacheManyAsync<N extends keyof TFields>( public async cacheManyAsync<N extends keyof TFields>(
fieldName: N, fieldName: N,
objectMap: ReadonlyMap<NonNullable<TFields[N]>, object> objectMap: ReadonlyMap<NonNullable<TFields[N]>, Readonly<TFields>>
): Promise<void> { ): Promise<void> {
objectMap.forEach((obj, fieldValue) => { objectMap.forEach((obj, fieldValue) => {
const cacheKey = this.createCacheKey(fieldName, fieldValue); const cacheKey = this.createCacheKey(fieldName, fieldValue);