mirror of
https://github.com/expo/entity.git
synced 2026-11-13 20:28:06 -10:00
Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a38e77e9af | ||
|
|
9ba1d430e0 |
No files matched your search
@@ -1,13 +1,6 @@
|
||||
import {
|
||||
CacheLoadResult,
|
||||
CacheStatus,
|
||||
EntityConfiguration,
|
||||
transformCacheObjectToFields,
|
||||
transformFieldsToCacheObject,
|
||||
} from '@expo/entity';
|
||||
import { CacheLoadResult, CacheStatus } from '@expo/entity';
|
||||
import { Redis } from 'ioredis';
|
||||
|
||||
import { redisTransformerMap } from './RedisCommon';
|
||||
import wrapNativeRedisCallAsync from './errors/wrapNativeRedisCallAsync';
|
||||
|
||||
// Sentinel value we store in Redis to negatively cache a database miss.
|
||||
@@ -33,15 +26,12 @@ export interface GenericRedisCacheContext {
|
||||
ttlSecondsNegative: number;
|
||||
}
|
||||
|
||||
export default class GenericRedisCacher<TFields> {
|
||||
constructor(
|
||||
private readonly context: GenericRedisCacheContext,
|
||||
private readonly entityConfiguration: EntityConfiguration<TFields>
|
||||
) {}
|
||||
export default class GenericRedisCacher {
|
||||
constructor(private readonly context: GenericRedisCacheContext) {}
|
||||
|
||||
public async loadManyAsync(
|
||||
keys: readonly string[]
|
||||
): Promise<ReadonlyMap<string, CacheLoadResult<TFields>>> {
|
||||
): Promise<ReadonlyMap<string, CacheLoadResult>> {
|
||||
if (keys.length === 0) {
|
||||
return new Map();
|
||||
}
|
||||
@@ -50,7 +40,7 @@ export default class GenericRedisCacher<TFields> {
|
||||
this.context.redisClient.mget(...keys)
|
||||
);
|
||||
|
||||
const results = new Map<string, CacheLoadResult<TFields>>();
|
||||
const results = new Map<string, CacheLoadResult>();
|
||||
for (let i = 0; i < keys.length; i++) {
|
||||
const key = keys[i]!;
|
||||
const redisResult = redisResults[i];
|
||||
@@ -62,11 +52,7 @@ export default class GenericRedisCacher<TFields> {
|
||||
} else if (redisResult) {
|
||||
results.set(key, {
|
||||
status: CacheStatus.HIT,
|
||||
item: transformCacheObjectToFields(
|
||||
this.entityConfiguration,
|
||||
redisTransformerMap,
|
||||
JSON.parse(redisResult)
|
||||
),
|
||||
item: JSON.parse(redisResult),
|
||||
});
|
||||
} else {
|
||||
results.set(key, {
|
||||
@@ -77,7 +63,7 @@ export default class GenericRedisCacher<TFields> {
|
||||
return results;
|
||||
}
|
||||
|
||||
public async cacheManyAsync(objectMap: ReadonlyMap<string, Readonly<TFields>>): Promise<void> {
|
||||
public async cacheManyAsync(objectMap: ReadonlyMap<string, object>): Promise<void> {
|
||||
if (objectMap.size === 0) {
|
||||
return;
|
||||
}
|
||||
@@ -86,9 +72,7 @@ export default class GenericRedisCacher<TFields> {
|
||||
objectMap.forEach((object, key) => {
|
||||
redisTransaction = redisTransaction.set(
|
||||
key,
|
||||
JSON.stringify(
|
||||
transformFieldsToCacheObject(this.entityConfiguration, redisTransformerMap, object)
|
||||
),
|
||||
JSON.stringify(object),
|
||||
'EX',
|
||||
this.context.ttlSecondsPositive
|
||||
);
|
||||
|
||||
@@ -1,8 +1,18 @@
|
||||
import { EntityCacheAdapter, EntityConfiguration, CacheLoadResult, mapKeys } from '@expo/entity';
|
||||
import {
|
||||
EntityCacheAdapter,
|
||||
EntityConfiguration,
|
||||
CacheLoadResult,
|
||||
FieldTransformerMap,
|
||||
mapKeys,
|
||||
transformCacheObjectToFields,
|
||||
CacheStatus,
|
||||
transformFieldsToCacheObject,
|
||||
} from '@expo/entity';
|
||||
import invariant from 'invariant';
|
||||
import { Redis } from 'ioredis';
|
||||
|
||||
import GenericRedisCacher from './GenericRedisCacher';
|
||||
import { redisTransformerMap } from './RedisCommon';
|
||||
|
||||
export interface RedisCacheAdapterContext {
|
||||
/**
|
||||
@@ -42,33 +52,51 @@ export interface RedisCacheAdapterContext {
|
||||
}
|
||||
|
||||
export default class RedisCacheAdapter<TFields> extends EntityCacheAdapter<TFields> {
|
||||
private readonly genericRedisCacher: GenericRedisCacher<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.
|
||||
*/
|
||||
private readonly redisFieldTransformer: FieldTransformerMap = redisTransformerMap;
|
||||
private readonly genericRedisCacher: GenericRedisCacher;
|
||||
constructor(
|
||||
entityConfiguration: EntityConfiguration<TFields>,
|
||||
private readonly context: RedisCacheAdapterContext
|
||||
) {
|
||||
super(entityConfiguration);
|
||||
|
||||
this.genericRedisCacher = new GenericRedisCacher(
|
||||
{
|
||||
redisClient: context.redisClient,
|
||||
ttlSecondsNegative: context.ttlSecondsNegative,
|
||||
ttlSecondsPositive: context.ttlSecondsPositive,
|
||||
},
|
||||
entityConfiguration
|
||||
);
|
||||
this.genericRedisCacher = new GenericRedisCacher({
|
||||
redisClient: context.redisClient,
|
||||
ttlSecondsNegative: context.ttlSecondsNegative,
|
||||
ttlSecondsPositive: context.ttlSecondsPositive,
|
||||
});
|
||||
}
|
||||
|
||||
public async loadManyAsync<N extends keyof TFields>(
|
||||
fieldName: N,
|
||||
fieldValues: readonly NonNullable<TFields[N]>[]
|
||||
): Promise<ReadonlyMap<NonNullable<TFields[N]>, CacheLoadResult<TFields>>> {
|
||||
): Promise<ReadonlyMap<NonNullable<TFields[N]>, CacheLoadResult>> {
|
||||
const redisCacheKeyToFieldValueMapping = new Map(
|
||||
fieldValues.map((fieldValue) => [this.makeCacheKey(fieldName, fieldValue), fieldValue])
|
||||
);
|
||||
const cacheResults = await this.genericRedisCacher.loadManyAsync(
|
||||
const rawCacheResults = await this.genericRedisCacher.loadManyAsync(
|
||||
Array.from(redisCacheKeyToFieldValueMapping.keys())
|
||||
);
|
||||
const cacheResults = new Map<string, CacheLoadResult>();
|
||||
for (const [redisCacheKey, rawCacheResult] of rawCacheResults) {
|
||||
if (rawCacheResult.status === CacheStatus.HIT) {
|
||||
cacheResults.set(redisCacheKey, {
|
||||
status: CacheStatus.HIT,
|
||||
item: transformCacheObjectToFields(
|
||||
this.entityConfiguration,
|
||||
this.redisFieldTransformer,
|
||||
rawCacheResult.item
|
||||
),
|
||||
});
|
||||
} else {
|
||||
cacheResults.set(redisCacheKey, rawCacheResult);
|
||||
}
|
||||
}
|
||||
|
||||
return mapKeys(cacheResults, (redisCacheKey) => {
|
||||
const fieldValue = redisCacheKeyToFieldValueMapping.get(redisCacheKey);
|
||||
@@ -83,10 +111,21 @@ export default class RedisCacheAdapter<TFields> extends EntityCacheAdapter<TFiel
|
||||
|
||||
public async cacheManyAsync<N extends keyof TFields>(
|
||||
fieldName: N,
|
||||
objectMap: ReadonlyMap<NonNullable<TFields[N]>, Readonly<TFields>>
|
||||
objectMap: ReadonlyMap<NonNullable<TFields[N]>, object>
|
||||
): Promise<void> {
|
||||
const cacheObjectMap = new Map<NonNullable<TFields[N]>, object>();
|
||||
for (const [fieldValue, object] of objectMap) {
|
||||
cacheObjectMap.set(
|
||||
fieldValue,
|
||||
transformFieldsToCacheObject(
|
||||
this.entityConfiguration,
|
||||
this.redisFieldTransformer,
|
||||
object as Readonly<TFields>
|
||||
)
|
||||
);
|
||||
}
|
||||
await this.genericRedisCacher.cacheManyAsync(
|
||||
mapKeys(objectMap, (fieldValue) => this.makeCacheKey(fieldName, fieldValue))
|
||||
mapKeys(cacheObjectMap, (fieldValue) => this.makeCacheKey(fieldName, fieldValue))
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { CacheStatus, UUIDField, EntityConfiguration } from '@expo/entity';
|
||||
import { CacheStatus, UUIDField, EntityConfiguration, DateField } from '@expo/entity';
|
||||
import { Redis, Pipeline } from 'ioredis';
|
||||
import { mock, when, instance, anything, verify } from 'ts-mockito';
|
||||
|
||||
@@ -6,6 +6,7 @@ import RedisCacheAdapter from '../RedisCacheAdapter';
|
||||
|
||||
type BlahFields = {
|
||||
id: string;
|
||||
date: Date | null;
|
||||
};
|
||||
|
||||
const entityConfiguration = new EntityConfiguration<BlahFields>({
|
||||
@@ -13,6 +14,7 @@ const entityConfiguration = new EntityConfiguration<BlahFields>({
|
||||
tableName: 'blah',
|
||||
schema: {
|
||||
id: new UUIDField({ columnName: 'id', cache: true }),
|
||||
date: new DateField({ columnName: 'date' }),
|
||||
},
|
||||
databaseAdapterFlavor: 'postgres',
|
||||
cacheAdapterFlavor: 'redis',
|
||||
@@ -62,6 +64,40 @@ describe(RedisCacheAdapter, () => {
|
||||
const results = await cacheAdapter.loadManyAsync('id', []);
|
||||
expect(results).toEqual(new Map());
|
||||
});
|
||||
|
||||
it('transforms a cache object to an Entity object', async () => {
|
||||
const redisResults = new Map();
|
||||
|
||||
const mockRedisClient = mock<Redis>();
|
||||
|
||||
// need to have one anything() for each element of spread, in this case 1
|
||||
when(mockRedisClient.mget(anything())).thenCall(async (...keys) =>
|
||||
keys.map((k) => redisResults.get(k) ?? null)
|
||||
);
|
||||
|
||||
const cacheAdapter = new RedisCacheAdapter(entityConfiguration, {
|
||||
redisClient: instance(mockRedisClient),
|
||||
makeKeyFn: (...parts) => parts.join(':'),
|
||||
cacheKeyVersion: 1,
|
||||
cacheKeyPrefix: 'hello-',
|
||||
ttlSecondsPositive: 1,
|
||||
ttlSecondsNegative: 2,
|
||||
});
|
||||
|
||||
const date = new Date();
|
||||
redisResults.set(
|
||||
cacheAdapter['makeCacheKey']('id', 'wat'),
|
||||
JSON.stringify({ id: 'wat', date: date.toISOString() })
|
||||
);
|
||||
|
||||
const results = await cacheAdapter.loadManyAsync('id', ['wat']);
|
||||
|
||||
expect(results.get('wat')).toMatchObject({
|
||||
status: CacheStatus.HIT,
|
||||
item: { id: 'wat', date },
|
||||
});
|
||||
expect(results.size).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('cacheManyAsync', () => {
|
||||
@@ -98,6 +134,41 @@ describe(RedisCacheAdapter, () => {
|
||||
ttl: 1,
|
||||
});
|
||||
});
|
||||
|
||||
it('transforms an Entity object to a cache object', async () => {
|
||||
const redisResults = new Map();
|
||||
|
||||
const mockPipeline = mock<Pipeline>();
|
||||
when(mockPipeline.set(anything(), anything(), anything(), anything())).thenCall(
|
||||
(key, value, code, ttl) => {
|
||||
redisResults.set(key, { value, code, ttl });
|
||||
return pipeline;
|
||||
}
|
||||
);
|
||||
when(mockPipeline.exec()).thenResolve({} as any);
|
||||
const pipeline = instance(mockPipeline);
|
||||
|
||||
const mockRedisClient = mock<Redis>();
|
||||
when(mockRedisClient.multi()).thenReturn(pipeline);
|
||||
|
||||
const cacheAdapter = new RedisCacheAdapter(entityConfiguration, {
|
||||
redisClient: instance(mockRedisClient),
|
||||
makeKeyFn: (...parts) => parts.join(':'),
|
||||
cacheKeyVersion: 1,
|
||||
cacheKeyPrefix: 'hello-',
|
||||
ttlSecondsPositive: 1,
|
||||
ttlSecondsNegative: 2,
|
||||
});
|
||||
const date = new Date();
|
||||
await cacheAdapter.cacheManyAsync('id', new Map([['wat', { id: 'wat', date }]]));
|
||||
|
||||
const cacheKey = cacheAdapter['makeCacheKey']('id', 'wat');
|
||||
expect(redisResults.get(cacheKey)).toMatchObject({
|
||||
value: JSON.stringify({ id: 'wat', date: date.toISOString() }),
|
||||
code: 'EX',
|
||||
ttl: 1,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('cacheDBMissesAsync', () => {
|
||||
|
||||
@@ -3,9 +3,15 @@ import {
|
||||
EntityConfiguration,
|
||||
filterMap,
|
||||
ISecondaryEntityCache,
|
||||
transformCacheObjectToFields,
|
||||
transformFieldsToCacheObject,
|
||||
zipToMap,
|
||||
} from '@expo/entity';
|
||||
import { GenericRedisCacheContext, GenericRedisCacher } from '@expo/entity-cache-adapter-redis';
|
||||
import {
|
||||
GenericRedisCacheContext,
|
||||
GenericRedisCacher,
|
||||
redisTransformerMap,
|
||||
} from '@expo/entity-cache-adapter-redis';
|
||||
import invariant from 'invariant';
|
||||
|
||||
/**
|
||||
@@ -16,14 +22,14 @@ import invariant from 'invariant';
|
||||
export default class RedisSecondaryEntityCache<TFields, TLoadParams>
|
||||
implements ISecondaryEntityCache<TFields, TLoadParams>
|
||||
{
|
||||
private readonly genericRedisCacher: GenericRedisCacher<TFields>;
|
||||
private readonly genericRedisCacher: GenericRedisCacher;
|
||||
|
||||
constructor(
|
||||
entityConfiguration: EntityConfiguration<TFields>,
|
||||
private readonly entityConfiguration: EntityConfiguration<TFields>,
|
||||
genericRedisCacheContext: GenericRedisCacheContext,
|
||||
private readonly constructRedisKey: (params: Readonly<TLoadParams>) => string
|
||||
) {
|
||||
this.genericRedisCacher = new GenericRedisCacher(genericRedisCacheContext, entityConfiguration);
|
||||
this.genericRedisCacher = new GenericRedisCacher(genericRedisCacheContext);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -68,7 +74,14 @@ export default class RedisSecondaryEntityCache<TFields, TLoadParams>
|
||||
if (cacheLoadResult.status === CacheStatus.HIT) {
|
||||
const loadParams = redisKeyToLoadParamsMap.get(redisKey);
|
||||
invariant(loadParams !== undefined, 'load params should be in redis key map');
|
||||
results.set(loadParams, cacheLoadResult.item);
|
||||
results.set(
|
||||
loadParams,
|
||||
transformCacheObjectToFields(
|
||||
this.entityConfiguration,
|
||||
redisTransformerMap,
|
||||
cacheLoadResult.item
|
||||
)
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -90,10 +103,13 @@ export default class RedisSecondaryEntityCache<TFields, TLoadParams>
|
||||
results.set(fetchMiss, null);
|
||||
}
|
||||
|
||||
const objectsToCache: Map<string, Readonly<TFields>> = new Map();
|
||||
const objectsToCache: Map<string, object> = new Map();
|
||||
for (const [loadParams, object] of fetchResults.entries()) {
|
||||
if (object) {
|
||||
objectsToCache.set(this.constructRedisKey(loadParams), object);
|
||||
objectsToCache.set(
|
||||
this.constructRedisKey(loadParams),
|
||||
transformFieldsToCacheObject(this.entityConfiguration, redisTransformerMap, object)
|
||||
);
|
||||
results.set(loadParams, object);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ export default abstract class EntityCacheAdapter<TFields> {
|
||||
public abstract loadManyAsync<N extends keyof TFields>(
|
||||
fieldName: N,
|
||||
fieldValues: readonly NonNullable<TFields[N]>[]
|
||||
): Promise<ReadonlyMap<NonNullable<TFields[N]>, CacheLoadResult<TFields>>>;
|
||||
): Promise<ReadonlyMap<NonNullable<TFields[N]>, CacheLoadResult>>;
|
||||
|
||||
/**
|
||||
* Cache many objects fetched from the DB.
|
||||
@@ -26,7 +26,7 @@ export default abstract class EntityCacheAdapter<TFields> {
|
||||
*/
|
||||
public abstract cacheManyAsync<N extends keyof TFields>(
|
||||
fieldName: N,
|
||||
objectMap: ReadonlyMap<NonNullable<TFields[N]>, Readonly<TFields>>
|
||||
objectMap: ReadonlyMap<NonNullable<TFields[N]>, object>
|
||||
): Promise<void>;
|
||||
|
||||
/**
|
||||
|
||||
@@ -10,10 +10,10 @@ export enum CacheStatus {
|
||||
NEGATIVE,
|
||||
}
|
||||
|
||||
export type CacheLoadResult<TFields> =
|
||||
export type CacheLoadResult =
|
||||
| {
|
||||
status: CacheStatus.HIT;
|
||||
item: Readonly<TFields>;
|
||||
item: Readonly<object>;
|
||||
}
|
||||
| {
|
||||
status: CacheStatus.MISS;
|
||||
@@ -82,7 +82,7 @@ export default class ReadThroughEntityCache<TFields> {
|
||||
const results: Map<NonNullable<TFields[N]>, readonly Readonly<TFields>[]> = new Map();
|
||||
cacheLoadResults.forEach((cacheLoadResult, fieldValue) => {
|
||||
if (cacheLoadResult.status === CacheStatus.HIT) {
|
||||
results.set(fieldValue, [cacheLoadResult.item]);
|
||||
results.set(fieldValue, [cacheLoadResult.item as Readonly<TFields>]);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -95,7 +95,7 @@ export default class ReadThroughEntityCache<TFields> {
|
||||
return !objectsFromFulfillerForFv || objectsFromFulfillerForFv.length === 0;
|
||||
});
|
||||
|
||||
const objectsToCache: Map<NonNullable<TFields[N]>, Readonly<TFields>> = new Map();
|
||||
const objectsToCache: Map<NonNullable<TFields[N]>, object> = new Map();
|
||||
for (const [fieldValue, objects] of dbFetchResults.entries()) {
|
||||
if (objects.length > 1) {
|
||||
// multiple objects received for what was supposed to be a unique query, don't add to return map nor cache
|
||||
|
||||
@@ -3,6 +3,7 @@ import invariant from 'invariant';
|
||||
import EntityCacheAdapter from '../../EntityCacheAdapter';
|
||||
import EntityConfiguration from '../../EntityConfiguration';
|
||||
import IEntityCacheAdapterProvider from '../../IEntityCacheAdapterProvider';
|
||||
import { FieldTransformerMap } from '../../internal/EntityFieldTransformationUtils';
|
||||
import { CacheStatus, CacheLoadResult } from '../../internal/ReadThroughEntityCache';
|
||||
|
||||
export class NoCacheStubCacheAdapterProvider implements IEntityCacheAdapterProvider {
|
||||
@@ -14,11 +15,15 @@ export class NoCacheStubCacheAdapterProvider implements IEntityCacheAdapterProvi
|
||||
}
|
||||
|
||||
export class NoCacheStubCacheAdapter<TFields> extends EntityCacheAdapter<TFields> {
|
||||
public getFieldTransformerMap(): FieldTransformerMap {
|
||||
return new Map();
|
||||
}
|
||||
|
||||
public async loadManyAsync<N extends keyof TFields>(
|
||||
_fieldName: N,
|
||||
fieldValues: readonly NonNullable<TFields[N]>[]
|
||||
): Promise<ReadonlyMap<NonNullable<TFields[N]>, CacheLoadResult<TFields>>> {
|
||||
return fieldValues.reduce((acc: Map<NonNullable<TFields[N]>, CacheLoadResult<TFields>>, v) => {
|
||||
): Promise<ReadonlyMap<NonNullable<TFields[N]>, CacheLoadResult>> {
|
||||
return fieldValues.reduce((acc: Map<NonNullable<TFields[N]>, CacheLoadResult>, v) => {
|
||||
acc.set(v, {
|
||||
status: CacheStatus.MISS,
|
||||
});
|
||||
@@ -28,7 +33,7 @@ export class NoCacheStubCacheAdapter<TFields> extends EntityCacheAdapter<TFields
|
||||
|
||||
public async cacheManyAsync<N extends keyof TFields>(
|
||||
_fieldName: N,
|
||||
_objectMap: ReadonlyMap<NonNullable<TFields[N]>, Readonly<TFields>>
|
||||
_objectMap: ReadonlyMap<NonNullable<TFields[N]>, object>
|
||||
): Promise<void> {}
|
||||
|
||||
public async cacheDBMissesAsync<N extends keyof TFields>(
|
||||
@@ -48,26 +53,27 @@ export class InMemoryFullCacheStubCacheAdapterProvider implements IEntityCacheAd
|
||||
getCacheAdapter<TFields>(
|
||||
entityConfiguration: EntityConfiguration<TFields>
|
||||
): EntityCacheAdapter<TFields> {
|
||||
return new InMemoryFullCacheStubCacheAdapter(
|
||||
entityConfiguration,
|
||||
this.cache as Map<string, Readonly<TFields>>
|
||||
);
|
||||
return new InMemoryFullCacheStubCacheAdapter(entityConfiguration, this.cache);
|
||||
}
|
||||
}
|
||||
|
||||
export class InMemoryFullCacheStubCacheAdapter<TFields> extends EntityCacheAdapter<TFields> {
|
||||
constructor(
|
||||
entityConfiguration: EntityConfiguration<TFields>,
|
||||
readonly cache: Map<string, Readonly<TFields>>
|
||||
readonly cache: Map<string, Readonly<object>>
|
||||
) {
|
||||
super(entityConfiguration);
|
||||
}
|
||||
|
||||
public getFieldTransformerMap(): FieldTransformerMap {
|
||||
return new Map();
|
||||
}
|
||||
|
||||
public async loadManyAsync<N extends keyof TFields>(
|
||||
fieldName: N,
|
||||
fieldValues: readonly NonNullable<TFields[N]>[]
|
||||
): Promise<ReadonlyMap<NonNullable<TFields[N]>, CacheLoadResult<TFields>>> {
|
||||
const results = new Map<NonNullable<TFields[N]>, CacheLoadResult<TFields>>();
|
||||
): Promise<ReadonlyMap<NonNullable<TFields[N]>, CacheLoadResult>> {
|
||||
const results = new Map<NonNullable<TFields[N]>, CacheLoadResult>();
|
||||
fieldValues.forEach((fieldValue) => {
|
||||
const cacheKey = this.createCacheKey(fieldName, fieldValue);
|
||||
if (!this.cache.has(cacheKey)) {
|
||||
@@ -88,7 +94,7 @@ export class InMemoryFullCacheStubCacheAdapter<TFields> extends EntityCacheAdapt
|
||||
|
||||
public async cacheManyAsync<N extends keyof TFields>(
|
||||
fieldName: N,
|
||||
objectMap: ReadonlyMap<NonNullable<TFields[N]>, Readonly<TFields>>
|
||||
objectMap: ReadonlyMap<NonNullable<TFields[N]>, object>
|
||||
): Promise<void> {
|
||||
objectMap.forEach((obj, fieldValue) => {
|
||||
const cacheKey = this.createCacheKey(fieldName, fieldValue);
|
||||
|
||||
Reference in new issue
Block a user