Compare commits

..
Author SHA1 Message Date
William Schurman 6222f0f464 feat: dataloaders within transactions 2020-12-10 15:34:56 -08:00
Will Schurman e3b2b235f8 v0.10.0 (#97) 2020-11-16 20:27:33 -08:00
Will Schurman d9603298e0 fix: fix null handling in load paths (try 2) (#96) 2020-11-16 20:03:32 -08:00
23 changed files with 225 additions and 70 deletions

No files matched your search

+11
View File
@@ -3,6 +3,17 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [0.10.0](https://github.com/expo/entity/compare/v0.9.1...v0.10.0) (2020-11-17)
### Bug Fixes
* fix null handling in load paths (try 2) ([#96](https://github.com/expo/entity/issues/96)) ([d960329](https://github.com/expo/entity/commit/d9603298e0cc5c3c2b0cca959876e9195555fb57))
## [0.9.1](https://github.com/expo/entity/compare/v0.9.0...v0.9.1) (2020-11-06)
+1 -1
View File
@@ -4,5 +4,5 @@
],
"npmClient": "yarn",
"useWorkspaces": true,
"version": "0.9.1"
"version": "0.10.0"
}
@@ -3,6 +3,14 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [0.10.0](https://github.com/expo/entity/compare/v0.9.1...v0.10.0) (2020-11-17)
**Note:** Version bump only for package @expo/entity-cache-adapter-redis
## [0.9.1](https://github.com/expo/entity/compare/v0.9.0...v0.9.1) (2020-11-06)
**Note:** Version bump only for package @expo/entity-cache-adapter-redis
@@ -1,6 +1,6 @@
{
"name": "@expo/entity-cache-adapter-redis",
"version": "0.9.1",
"version": "0.10.0",
"description": "Redis cache adapter for @expo/entity",
"files": [
"build",
@@ -32,6 +32,6 @@
"ioredis": "^4.17.3"
},
"devDependencies": {
"@expo/entity": "^0.9.1"
"@expo/entity": "^0.10.0"
}
}
@@ -3,6 +3,14 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [0.10.0](https://github.com/expo/entity/compare/v0.9.1...v0.10.0) (2020-11-17)
**Note:** Version bump only for package @expo/entity-database-adapter-knex
## [0.9.1](https://github.com/expo/entity/compare/v0.9.0...v0.9.1) (2020-11-06)
**Note:** Version bump only for package @expo/entity-database-adapter-knex
@@ -1,6 +1,6 @@
{
"name": "@expo/entity-database-adapter-knex",
"version": "0.9.1",
"version": "0.10.0",
"description": "Knex database adapter for @expo/entity",
"files": [
"build",
@@ -32,6 +32,6 @@
"knex": "^0.21.5"
},
"devDependencies": {
"@expo/entity": "^0.9.1"
"@expo/entity": "^0.10.0"
}
}
+8
View File
@@ -3,6 +3,14 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [0.10.0](https://github.com/expo/entity/compare/v0.9.1...v0.10.0) (2020-11-17)
**Note:** Version bump only for package @expo/entity-example
## [0.9.1](https://github.com/expo/entity/compare/v0.9.0...v0.9.1) (2020-11-06)
**Note:** Version bump only for package @expo/entity-example
+2 -2
View File
@@ -1,7 +1,7 @@
{
"name": "@expo/entity-example",
"private": true,
"version": "0.9.1",
"version": "0.10.0",
"description": "An example integration of the @expo/entity framework",
"scripts": {
"tsc": "tsc",
@@ -22,7 +22,7 @@
"author": "Expo",
"license": "MIT",
"dependencies": {
"@expo/entity": "^0.9.1",
"@expo/entity": "^0.10.0",
"apollo-server-koa": "^2.16.1",
"graphql": "^15.3.0",
"koa": "^2.13.0",
@@ -3,6 +3,14 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [0.10.0](https://github.com/expo/entity/compare/v0.9.1...v0.10.0) (2020-11-17)
**Note:** Version bump only for package @expo/entity-full-integration-tests
## [0.9.1](https://github.com/expo/entity/compare/v0.9.0...v0.9.1) (2020-11-06)
@@ -1,7 +1,7 @@
{
"name": "@expo/entity-full-integration-tests",
"private": true,
"version": "0.9.1",
"version": "0.10.0",
"description": "Full redis and knex integration tests for the entity framework",
"scripts": {
"tsc": "tsc",
@@ -26,8 +26,8 @@
"@expo/entity-database-adapter-knex": "*"
},
"devDependencies": {
"@expo/entity": "^0.9.1",
"@expo/entity-cache-adapter-redis": "^0.9.1",
"@expo/entity-database-adapter-knex": "^0.9.1"
"@expo/entity": "^0.10.0",
"@expo/entity-cache-adapter-redis": "^0.10.0",
"@expo/entity-database-adapter-knex": "^0.10.0"
}
}
+11
View File
@@ -3,6 +3,17 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [0.10.0](https://github.com/expo/entity/compare/v0.9.1...v0.10.0) (2020-11-17)
### Bug Fixes
* fix null handling in load paths (try 2) ([#96](https://github.com/expo/entity/issues/96)) ([d960329](https://github.com/expo/entity/commit/d9603298e0cc5c3c2b0cca959876e9195555fb57))
## [0.9.1](https://github.com/expo/entity/compare/v0.9.0...v0.9.1) (2020-11-06)
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@expo/entity",
"version": "0.9.1",
"version": "0.10.0",
"description": "A privacy-first data model",
"files": [
"build",
+2 -2
View File
@@ -79,7 +79,7 @@ export default class EnforcingEntityLoader<
uniqueFieldName,
fieldValue
);
return entityResult.enforceValue();
return entityResult ? entityResult.enforceValue() : null;
}
/**
@@ -98,7 +98,7 @@ export default class EnforcingEntityLoader<
*/
async loadByIDNullableAsync(id: TID): Promise<TEntity | null> {
const entityResult = await this.entityLoader.loadByIDNullableAsync(id);
return entityResult.enforceValue();
return entityResult ? entityResult.enforceValue() : null;
}
/**
+27 -19
View File
@@ -60,7 +60,9 @@ export default class EntityAssociationLoader<
> {
const associatedEntityID = this.entity.getField(fieldIdentifyingAssociatedEntity);
if (!associatedEntityID) {
return result(null) as any;
return result(null) as Result<
null extends TFields[TIdentifyingField] ? TAssociatedEntity | null : TAssociatedEntity
>;
}
const loader = this.entity
@@ -68,7 +70,10 @@ export default class EntityAssociationLoader<
.getViewerScopedEntityCompanionForClass(associatedEntityClass)
.getLoaderFactory()
.forLoad(queryContext);
return (await loader.loadByIDAsync((associatedEntityID as unknown) as TAssociatedID)) as any;
return (await loader.loadByIDAsync((associatedEntityID as unknown) as TAssociatedID)) as Result<
null extends TFields[TIdentifyingField] ? TAssociatedEntity | null : TAssociatedEntity
>;
}
/**
@@ -134,7 +139,6 @@ export default class EntityAssociationLoader<
* @param queryContext - query context in which to perform the load
*/
async loadAssociatedEntityByFieldEqualingAsync<
TIdentifyingField extends keyof Pick<TFields, TSelectedFields>,
TAssociatedFields,
TAssociatedID,
TAssociatedEntity extends ReadonlyEntity<
@@ -152,7 +156,7 @@ export default class EntityAssociationLoader<
>,
TAssociatedSelectedFields extends keyof TAssociatedFields = keyof TAssociatedFields
>(
fieldIdentifyingAssociatedEntity: TIdentifyingField,
fieldIdentifyingAssociatedEntity: keyof Pick<TFields, TSelectedFields>,
associatedEntityClass: IEntityClass<
TAssociatedFields,
TAssociatedID,
@@ -167,12 +171,11 @@ export default class EntityAssociationLoader<
.getViewerScopedEntityCompanionForClass(associatedEntityClass)
.getQueryContextProvider()
.getQueryContext()
): Promise<Result<TAssociatedEntity | null>> {
): Promise<Result<TAssociatedEntity> | null> {
const associatedFieldValue = this.entity.getField(fieldIdentifyingAssociatedEntity);
if (!associatedFieldValue) {
return result(null);
return null;
}
const loader = this.entity
.getViewerContext()
.getViewerScopedEntityCompanionForClass(associatedEntityClass)
@@ -274,7 +277,7 @@ export default class EntityAssociationLoader<
>
],
queryContext?: EntityQueryContext
): Promise<Result<TEntity2 | null>>;
): Promise<Result<TEntity2> | null>;
/**
* Load an associated entity by folding a sequence of {@link EntityLoadThroughDirective}. At each
@@ -329,7 +332,7 @@ export default class EntityAssociationLoader<
>
],
queryContext?: EntityQueryContext
): Promise<Result<TEntity3 | null>>;
): Promise<Result<TEntity3> | null>;
/**
* Load an associated entity by folding a sequence of {@link EntityLoadThroughDirective}. At each
@@ -405,7 +408,7 @@ export default class EntityAssociationLoader<
>
],
queryContext?: EntityQueryContext
): Promise<Result<TEntity4 | null>>;
): Promise<Result<TEntity4> | null>;
/**
* Load an associated entity by folding a sequence of {@link EntityLoadThroughDirective}. At each
@@ -416,12 +419,12 @@ export default class EntityAssociationLoader<
async loadAssociatedEntityThroughAsync(
loadDirectives: EntityLoadThroughDirective<TViewerContext, any, any, any, any, any, any, any>[],
queryContext?: EntityQueryContext
): Promise<Result<ReadonlyEntity<any, any, any, any> | null>>;
): Promise<Result<ReadonlyEntity<any, any, any, any>> | null>;
async loadAssociatedEntityThroughAsync(
loadDirectives: EntityLoadThroughDirective<TViewerContext, any, any, any, any, any, any, any>[],
queryContext?: EntityQueryContext
): Promise<Result<ReadonlyEntity<any, any, any, any> | null>> {
): Promise<Result<ReadonlyEntity<any, any, any, any>> | null> {
let currentEntity: ReadonlyEntity<any, any, any, any> = this.entity;
for (const loadDirective of loadDirectives) {
const {
@@ -429,7 +432,7 @@ export default class EntityAssociationLoader<
fieldIdentifyingAssociatedEntity,
associatedEntityLookupByField,
} = loadDirective;
let associatedEntityResult: Result<ReadonlyEntity<any, any, any, any> | null>;
let associatedEntityResult: Result<ReadonlyEntity<any, any, any, any>> | null;
if (associatedEntityLookupByField) {
associatedEntityResult = await currentEntity
.associationLoader()
@@ -440,23 +443,28 @@ export default class EntityAssociationLoader<
queryContext
);
} else {
associatedEntityResult = await currentEntity
const associatedEntityResultLocal = await currentEntity
.associationLoader()
.loadAssociatedEntityAsync(
fieldIdentifyingAssociatedEntity,
associatedEntityClass,
queryContext
);
if (associatedEntityResultLocal.ok && associatedEntityResultLocal.value === null) {
associatedEntityResult = null;
} else {
associatedEntityResult = associatedEntityResultLocal;
}
}
if (!associatedEntityResult) {
return null;
}
if (!associatedEntityResult.ok) {
return result(associatedEntityResult.reason);
}
if (!associatedEntityResult.value) {
return result(null);
}
currentEntity = associatedEntityResult.value;
}
return result(currentEntity);
+10 -3
View File
@@ -132,13 +132,13 @@ export default class EntityLoader<
async loadByFieldEqualingAsync<N extends keyof Pick<TFields, TSelectedFields>>(
uniqueFieldName: N,
fieldValue: NonNullable<TFields[N]>
): Promise<Result<TEntity | null>> {
): Promise<Result<TEntity> | null> {
const entityResults = await this.loadManyByFieldEqualingAsync(uniqueFieldName, fieldValue);
invariant(
entityResults.length <= 1,
`loadByFieldEqualing: Multiple entities of type ${this.entityClass.name} found for ${uniqueFieldName}=${fieldValue}`
);
return entityResults[0] ?? result(null);
return entityResults[0] ?? null;
}
/**
@@ -160,7 +160,7 @@ export default class EntityLoader<
* @param id - ID of the entity
* @returns entity result for matching ID, or null if no entity exists for ID.
*/
async loadByIDNullableAsync(id: TID): Promise<Result<TEntity | null>> {
async loadByIDNullableAsync(id: TID): Promise<Result<TEntity> | null> {
return await this.loadByFieldEqualingAsync(this.idField, id as any);
}
@@ -284,6 +284,13 @@ export default class EntityLoader<
await this.dataManager.invalidateObjectFieldsAsync(objectFields);
}
async invalidateFieldsTransactionalAsync(objectFields: Readonly<TFields>): Promise<void> {
await this.dataManager.invalidateObjectFieldsTransactionalAsync(
this.queryContext,
objectFields
);
}
/**
* Invalidate all caches for an entity. One potential use case would be to keep the entity
* framework in sync with changes made to data outside of the framework.
+7
View File
@@ -205,6 +205,7 @@ export class CreateMutator<
queryContext.appendPostCommitCallback(
entityLoader.invalidateFieldsAsync.bind(entityLoader, insertResult)
);
await entityLoader.invalidateFieldsTransactionalAsync(insertResult);
const unauthorizedEntityAfterInsert = new this.entityClass(this.viewerContext, insertResult);
const newEntity = await entityLoader
@@ -395,6 +396,11 @@ export class UpdateMutator<
entityLoader.invalidateFieldsAsync.bind(entityLoader, this.fieldsForEntity)
);
await Promise.all([
entityLoader.invalidateFieldsTransactionalAsync(this.originalFieldsForEntity),
entityLoader.invalidateFieldsTransactionalAsync(this.fieldsForEntity),
]);
const unauthorizedEntityAfterUpdate = new this.entityClass(this.viewerContext, updateResult);
const updatedEntity = await entityLoader
.enforcing()
@@ -566,6 +572,7 @@ export class DeleteMutator<
queryContext.appendPostCommitCallback(
entityLoader.invalidateFieldsAsync.bind(entityLoader, this.entity.getAllDatabaseFields())
);
await entityLoader.invalidateFieldsTransactionalAsync(this.entity.getAllDatabaseFields());
await this.executeMutationTriggersOrValidatorsAsync(
this.mutationTriggers.afterDelete,
+7 -3
View File
@@ -1,3 +1,5 @@
import { v4 as uuidv4 } from 'uuid';
import EntityQueryContextProvider from './EntityQueryContextProvider';
export type PostCommitCallback = (...args: any) => Promise<any>;
@@ -12,7 +14,7 @@ export type PostCommitCallback = (...args: any) => Promise<any>;
export abstract class EntityQueryContext {
constructor(private readonly queryInterface: any) {}
abstract isInTransaction(): boolean;
abstract isInTransaction(): this is EntityTransactionalQueryContext;
getQueryInterface(): any {
return this.queryInterface;
@@ -31,7 +33,7 @@ export class EntityNonTransactionalQueryContext extends EntityQueryContext {
super(queryInterface);
}
isInTransaction(): boolean {
isInTransaction(): this is EntityTransactionalQueryContext {
return false;
}
@@ -43,6 +45,8 @@ export class EntityNonTransactionalQueryContext extends EntityQueryContext {
}
export class EntityTransactionalQueryContext extends EntityQueryContext {
public readonly transactionID = uuidv4();
private readonly postCommitCallbacks: PostCommitCallback[] = [];
public appendPostCommitCallback(callback: PostCommitCallback): void {
@@ -55,7 +59,7 @@ export class EntityTransactionalQueryContext extends EntityQueryContext {
await Promise.all(callbacks.map((callback) => callback()));
}
isInTransaction(): boolean {
isInTransaction(): this is EntityTransactionalQueryContext {
return true;
}
@@ -287,6 +287,7 @@ describe(EnforcingEntityLoader, () => {
'enforcing',
'invalidateFieldsAsync',
'invalidateEntityAsync',
'invalidateFieldsTransactionalAsync',
'tryConstructEntities',
];
expect(loaderProperties).toEqual(expect.arrayContaining(knownLoaderOnlyDifferences));
@@ -67,7 +67,7 @@ describe(EntityAssociationLoader, () => {
const loadedOtherResult = await testEntity
.associationLoader()
.loadAssociatedEntityByFieldEqualingAsync('stringField', TestEntity, 'customIdField');
expect(loadedOtherResult.enforceValue()?.getID()).toEqual(testOtherEntity.getID());
expect(loadedOtherResult?.enforceValue().getID()).toEqual(testOtherEntity.getID());
});
it('returns null when loading associated entities by field equaling a non existent association', async () => {
@@ -79,7 +79,7 @@ describe(EntityAssociationLoader, () => {
const loadedOtherResult = await testEntity
.associationLoader()
.loadAssociatedEntityByFieldEqualingAsync('stringField', TestEntity, 'customIdField');
expect(loadedOtherResult.enforceValue()).toBeNull();
expect(loadedOtherResult).toBeNull();
});
it('returns null when load-by field is null', async () => {
@@ -91,7 +91,7 @@ describe(EntityAssociationLoader, () => {
const loadedOtherResult = await testEntity
.associationLoader()
.loadAssociatedEntityByFieldEqualingAsync('nullableField', TestEntity, 'customIdField');
expect(loadedOtherResult.enforceValue()).toBeNull();
expect(loadedOtherResult).toBeNull();
});
});
@@ -160,7 +160,7 @@ describe(EntityAssociationLoader, () => {
fieldIdentifyingAssociatedEntity: 'foreignKey',
},
]);
expect(loaded2Result.enforceValue()?.getID()).toEqual(testEntity2.getID());
expect(loaded2Result?.enforceValue().getID()).toEqual(testEntity2.getID());
const loaded3Result = await testEntity.associationLoader().loadAssociatedEntityThroughAsync([
{
@@ -172,7 +172,7 @@ describe(EntityAssociationLoader, () => {
fieldIdentifyingAssociatedEntity: 'testIndexedField',
},
]);
expect(loaded3Result.enforceValue()?.getID()).toEqual(testEntity3.getID());
expect(loaded3Result?.enforceValue().getID()).toEqual(testEntity3.getID());
const loaded4Result = await testEntity.associationLoader().loadAssociatedEntityThroughAsync([
{
@@ -188,7 +188,7 @@ describe(EntityAssociationLoader, () => {
fieldIdentifyingAssociatedEntity: 'foreignKey',
},
]);
expect(loaded4Result.enforceValue()?.getID()).toEqual(testEntity4.getID());
expect(loaded4Result?.enforceValue().getID()).toEqual(testEntity4.getID());
});
it('fails when chain loading associated entity fails', async () => {
@@ -226,7 +226,7 @@ describe(EntityAssociationLoader, () => {
associatedEntityLookupByField: 'stringField',
},
]);
expect(loaded2Result.enforceValue()?.getID()).toEqual(testEntity2.getID());
expect(loaded2Result?.enforceValue().getID()).toEqual(testEntity2.getID());
});
it('returns null when chain loading by field equality returns null', async () => {
@@ -244,7 +244,7 @@ describe(EntityAssociationLoader, () => {
associatedEntityLookupByField: 'stringField',
},
]);
expect(loaded2Result.enforceValue()).toBeNull();
expect(loaded2Result).toBeNull();
});
it('returns null when chain loading by field is null', async () => {
@@ -261,7 +261,7 @@ describe(EntityAssociationLoader, () => {
fieldIdentifyingAssociatedEntity: 'nullableField',
},
]);
expect(loadedResult.enforceValue()).toBeNull();
expect(loadedResult).toBeNull();
});
});
});
@@ -81,16 +81,16 @@ describe(EntityLoader, () => {
const entityResultNumber3 = await entityLoader.loadByFieldEqualingAsync('numberField', 3);
expect(entityResultNumber3).not.toBeNull();
expect(entityResultNumber3.enforceValue()?.getID()).toEqual('world');
expect(entityResultNumber3!.enforceValue().getID()).toEqual('world');
const entityResultNumber4 = await entityLoader.loadByFieldEqualingAsync('numberField', 4);
expect(entityResultNumber4.enforceValue()).toBeNull();
expect(entityResultNumber4).toBeNull();
await expect(entityLoader.loadByFieldEqualingAsync('stringField', 'huh')).rejects.toThrowError(
'loadByFieldEqualing: Multiple entities of type TestEntity found for stringField=huh'
);
expect((await entityLoader.loadByIDNullableAsync('fake')).enforceValue()).toBeNull();
await expect(entityLoader.loadByIDNullableAsync('fake')).resolves.toBeNull();
await expect(entityLoader.loadByIDNullableAsync('hello')).resolves.not.toBeNull();
});
@@ -4,7 +4,7 @@ import EntityDatabaseAdapter, {
FieldEqualityCondition,
QuerySelectionModifiers,
} from '../EntityDatabaseAdapter';
import { EntityQueryContext } from '../EntityQueryContext';
import { EntityQueryContext, EntityTransactionalQueryContext } from '../EntityQueryContext';
import EntityQueryContextProvider from '../EntityQueryContextProvider';
import { partitionErrors } from '../entityUtils';
import {
@@ -15,6 +15,13 @@ import IEntityMetricsAdapter, { EntityMetricsLoadType } from '../metrics/IEntity
import { computeIfAbsent, zipToMap } from '../utils/collections/maps';
import ReadThroughEntityCache from './ReadThroughEntityCache';
type FieldDataLoader<TFields> = DataLoader<
NonNullable<TFields[keyof TFields]>,
readonly Readonly<TFields>[]
>;
type FieldDataLoadersMap<TFields> = Map<keyof TFields, FieldDataLoader<TFields>>;
/**
* A data manager is responsible for orchestrating multiple sources of entity
* data including local caches, {@link EntityCacheAdapter}, and {@link EntityDatabaseAdapter}.
@@ -22,9 +29,11 @@ import ReadThroughEntityCache from './ReadThroughEntityCache';
* It is also responsible for invalidating all sources of data when mutated using {@link EntityMutator}.
*/
export default class EntityDataManager<TFields> {
private readonly fieldDataLoaders: Map<
keyof TFields,
DataLoader<NonNullable<TFields[keyof TFields]>, readonly Readonly<TFields>[]>
private readonly regularFieldDataLoaders: FieldDataLoadersMap<TFields> = new Map();
private readonly transactionalFieldDataLoaders: Map<
string,
FieldDataLoadersMap<TFields>
> = new Map();
constructor(
@@ -35,10 +44,10 @@ export default class EntityDataManager<TFields> {
private readonly entityClassName: string
) {}
private getFieldDataLoaderForFieldName<N extends keyof TFields>(
private getRegularFieldDataLoderForFieldName<N extends keyof TFields>(
fieldName: N
): DataLoader<NonNullable<TFields[N]>, readonly Readonly<TFields>[]> {
return computeIfAbsent(this.fieldDataLoaders, fieldName, () => {
): FieldDataLoader<TFields> {
return computeIfAbsent(this.regularFieldDataLoaders, fieldName, () => {
return new DataLoader(
async (
fieldValues: readonly NonNullable<TFields[N]>[]
@@ -53,6 +62,43 @@ export default class EntityDataManager<TFields> {
});
}
private getTransactionalFieldDataLoaderForFieldName<N extends keyof TFields>(
queryContext: EntityTransactionalQueryContext,
fieldName: N
): FieldDataLoader<TFields> {
const fieldDataLoaderMap = computeIfAbsent(
this.transactionalFieldDataLoaders,
queryContext.transactionID,
() => new Map<keyof TFields, FieldDataLoader<TFields>>()
);
return computeIfAbsent(fieldDataLoaderMap, fieldName, () => {
return new DataLoader(
async (
fieldValues: readonly NonNullable<TFields[N]>[]
): Promise<readonly (readonly TFields[])[]> => {
const objectMap = await this.loadManyForTransactionalDataLoaderByFieldEqualingAsync(
fieldName,
fieldValues,
queryContext
);
return fieldValues.map((fv) => objectMap.get(fv) ?? []);
}
);
});
}
private async loadManyForTransactionalDataLoaderByFieldEqualingAsync<N extends keyof TFields>(
fieldName: N,
fieldValues: readonly NonNullable<TFields[N]>[],
queryContext: EntityTransactionalQueryContext
): Promise<ReadonlyMap<NonNullable<TFields[N]>, readonly Readonly<TFields>[]>> {
this.metricsAdapter.incrementDataManagerDatabaseLoadCount({
fieldValueCount: fieldValues.length,
entityClassName: this.entityClassName,
});
return await this.databaseAdapter.fetchManyWhereAsync(queryContext, fieldName, fieldValues);
}
private async loadManyForDataLoaderByFieldEqualingAsync<N extends keyof TFields>(
fieldName: N,
fieldValues: readonly NonNullable<TFields[N]>[]
@@ -61,10 +107,11 @@ export default class EntityDataManager<TFields> {
fieldValueCount: fieldValues.length,
entityClassName: this.entityClassName,
});
return await this.entityCache.readManyThroughAsync(
fieldName,
fieldValues,
async (fetcherValues) => {
async (fetcherValues: readonly NonNullable<TFields[N]>[]) => {
this.metricsAdapter.incrementDataManagerDatabaseLoadCount({
fieldValueCount: fieldValues.length,
entityClassName: this.entityClassName,
@@ -112,16 +159,13 @@ export default class EntityDataManager<TFields> {
);
}
// don't cache when in transaction, as rollbacks complicate things significantly
if (queryContext.isInTransaction()) {
return await this.databaseAdapter.fetchManyWhereAsync(queryContext, fieldName, fieldValues);
}
this.metricsAdapter.incrementDataManagerDataloaderLoadCount({
fieldValueCount: fieldValues.length,
entityClassName: this.entityClassName,
});
const dataLoader = this.getFieldDataLoaderForFieldName(fieldName);
const dataLoader = queryContext.isInTransaction()
? this.getTransactionalFieldDataLoaderForFieldName(queryContext, fieldName)
: this.getRegularFieldDataLoderForFieldName(fieldName);
const results = await dataLoader.loadMany(fieldValues);
const [values, errors] = partitionErrors(results);
if (errors.length > 0) {
@@ -192,7 +236,20 @@ export default class EntityDataManager<TFields> {
fieldValues: readonly NonNullable<TFields[N]>[]
): Promise<void> {
await this.entityCache.invalidateManyAsync(fieldName, fieldValues);
const dataLoader = this.getFieldDataLoaderForFieldName(fieldName);
const dataLoader = this.getRegularFieldDataLoderForFieldName(fieldName);
fieldValues.forEach((fieldValue) => dataLoader.clear(fieldValue));
}
private async invalidateManyByFieldEqualingTransactionalAsync<N extends keyof TFields>(
queryContext: EntityQueryContext,
fieldName: N,
fieldValues: readonly NonNullable<TFields[N]>[]
): Promise<void> {
if (!queryContext.isInTransaction()) {
return;
}
const dataLoader = this.getTransactionalFieldDataLoaderForFieldName(queryContext, fieldName);
fieldValues.forEach((fieldValue) => dataLoader.clear(fieldValue));
}
@@ -215,4 +272,21 @@ export default class EntityDataManager<TFields> {
})
);
}
async invalidateObjectFieldsTransactionalAsync(
queryContext: EntityQueryContext,
objectFields: Readonly<TFields>
): Promise<void> {
const keys = Object.keys(objectFields) as (keyof TFields)[];
await Promise.all(
keys.map(async (fieldName: keyof TFields) => {
const value = objectFields[fieldName];
if (value !== undefined) {
await this.invalidateManyByFieldEqualingTransactionalAsync(queryContext, fieldName, [
value as NonNullable<TFields[keyof TFields]>,
]);
}
})
);
}
}
@@ -570,7 +570,7 @@ describe(EntityDataManager, () => {
verify(metricsAdapterMock.incrementDataManagerDatabaseLoadCount(anyNumber())).never();
});
it('throws when a load by value is null or undefined', async () => {
it('throws when a load-by value is null or undefined', async () => {
const objects = getObjects();
const dataStore = StubDatabaseAdapter.convertFieldObjectsToDataStore(
testEntityConfiguration,
+3 -3
View File
@@ -559,17 +559,17 @@
which "^1.3.1"
"@expo/entity-cache-adapter-redis@file:packages/entity-cache-adapter-redis":
version "0.6.0"
version "0.10.0"
dependencies:
ioredis "^4.17.3"
"@expo/entity-database-adapter-knex@file:packages/entity-database-adapter-knex":
version "0.6.0"
version "0.10.0"
dependencies:
knex "^0.21.5"
"@expo/entity@file:packages/entity":
version "0.6.0"
version "0.10.0"
dependencies:
"@expo/results" "^1.0.0"
dataloader "^2.0.0"