mirror of
https://github.com/expo/entity.git
synced 2026-11-15 03:48:08 -10:00
Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
41bd0722c9 | ||
|
|
03a1fce067 |
No files matched your search
@@ -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
@@ -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))
|
||||||
|
|||||||
+125
@@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,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",
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
Reference in new issue
Block a user