mirror of
https://github.com/expo/entity.git
synced 2026-11-13 20:28:06 -10:00
Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6222f0f464 |
No files matched your search
@@ -284,6 +284,13 @@ export default class EntityLoader<
|
|||||||
await this.dataManager.invalidateObjectFieldsAsync(objectFields);
|
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
|
* 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.
|
* framework in sync with changes made to data outside of the framework.
|
||||||
|
|||||||
@@ -205,6 +205,7 @@ export class CreateMutator<
|
|||||||
queryContext.appendPostCommitCallback(
|
queryContext.appendPostCommitCallback(
|
||||||
entityLoader.invalidateFieldsAsync.bind(entityLoader, insertResult)
|
entityLoader.invalidateFieldsAsync.bind(entityLoader, insertResult)
|
||||||
);
|
);
|
||||||
|
await entityLoader.invalidateFieldsTransactionalAsync(insertResult);
|
||||||
|
|
||||||
const unauthorizedEntityAfterInsert = new this.entityClass(this.viewerContext, insertResult);
|
const unauthorizedEntityAfterInsert = new this.entityClass(this.viewerContext, insertResult);
|
||||||
const newEntity = await entityLoader
|
const newEntity = await entityLoader
|
||||||
@@ -395,6 +396,11 @@ export class UpdateMutator<
|
|||||||
entityLoader.invalidateFieldsAsync.bind(entityLoader, this.fieldsForEntity)
|
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 unauthorizedEntityAfterUpdate = new this.entityClass(this.viewerContext, updateResult);
|
||||||
const updatedEntity = await entityLoader
|
const updatedEntity = await entityLoader
|
||||||
.enforcing()
|
.enforcing()
|
||||||
@@ -566,6 +572,7 @@ export class DeleteMutator<
|
|||||||
queryContext.appendPostCommitCallback(
|
queryContext.appendPostCommitCallback(
|
||||||
entityLoader.invalidateFieldsAsync.bind(entityLoader, this.entity.getAllDatabaseFields())
|
entityLoader.invalidateFieldsAsync.bind(entityLoader, this.entity.getAllDatabaseFields())
|
||||||
);
|
);
|
||||||
|
await entityLoader.invalidateFieldsTransactionalAsync(this.entity.getAllDatabaseFields());
|
||||||
|
|
||||||
await this.executeMutationTriggersOrValidatorsAsync(
|
await this.executeMutationTriggersOrValidatorsAsync(
|
||||||
this.mutationTriggers.afterDelete,
|
this.mutationTriggers.afterDelete,
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { v4 as uuidv4 } from 'uuid';
|
||||||
|
|
||||||
import EntityQueryContextProvider from './EntityQueryContextProvider';
|
import EntityQueryContextProvider from './EntityQueryContextProvider';
|
||||||
|
|
||||||
export type PostCommitCallback = (...args: any) => Promise<any>;
|
export type PostCommitCallback = (...args: any) => Promise<any>;
|
||||||
@@ -12,7 +14,7 @@ export type PostCommitCallback = (...args: any) => Promise<any>;
|
|||||||
export abstract class EntityQueryContext {
|
export abstract class EntityQueryContext {
|
||||||
constructor(private readonly queryInterface: any) {}
|
constructor(private readonly queryInterface: any) {}
|
||||||
|
|
||||||
abstract isInTransaction(): boolean;
|
abstract isInTransaction(): this is EntityTransactionalQueryContext;
|
||||||
|
|
||||||
getQueryInterface(): any {
|
getQueryInterface(): any {
|
||||||
return this.queryInterface;
|
return this.queryInterface;
|
||||||
@@ -31,7 +33,7 @@ export class EntityNonTransactionalQueryContext extends EntityQueryContext {
|
|||||||
super(queryInterface);
|
super(queryInterface);
|
||||||
}
|
}
|
||||||
|
|
||||||
isInTransaction(): boolean {
|
isInTransaction(): this is EntityTransactionalQueryContext {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -43,6 +45,8 @@ export class EntityNonTransactionalQueryContext extends EntityQueryContext {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class EntityTransactionalQueryContext extends EntityQueryContext {
|
export class EntityTransactionalQueryContext extends EntityQueryContext {
|
||||||
|
public readonly transactionID = uuidv4();
|
||||||
|
|
||||||
private readonly postCommitCallbacks: PostCommitCallback[] = [];
|
private readonly postCommitCallbacks: PostCommitCallback[] = [];
|
||||||
|
|
||||||
public appendPostCommitCallback(callback: PostCommitCallback): void {
|
public appendPostCommitCallback(callback: PostCommitCallback): void {
|
||||||
@@ -55,7 +59,7 @@ export class EntityTransactionalQueryContext extends EntityQueryContext {
|
|||||||
await Promise.all(callbacks.map((callback) => callback()));
|
await Promise.all(callbacks.map((callback) => callback()));
|
||||||
}
|
}
|
||||||
|
|
||||||
isInTransaction(): boolean {
|
isInTransaction(): this is EntityTransactionalQueryContext {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -287,6 +287,7 @@ describe(EnforcingEntityLoader, () => {
|
|||||||
'enforcing',
|
'enforcing',
|
||||||
'invalidateFieldsAsync',
|
'invalidateFieldsAsync',
|
||||||
'invalidateEntityAsync',
|
'invalidateEntityAsync',
|
||||||
|
'invalidateFieldsTransactionalAsync',
|
||||||
'tryConstructEntities',
|
'tryConstructEntities',
|
||||||
];
|
];
|
||||||
expect(loaderProperties).toEqual(expect.arrayContaining(knownLoaderOnlyDifferences));
|
expect(loaderProperties).toEqual(expect.arrayContaining(knownLoaderOnlyDifferences));
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import EntityDatabaseAdapter, {
|
|||||||
FieldEqualityCondition,
|
FieldEqualityCondition,
|
||||||
QuerySelectionModifiers,
|
QuerySelectionModifiers,
|
||||||
} from '../EntityDatabaseAdapter';
|
} from '../EntityDatabaseAdapter';
|
||||||
import { EntityQueryContext } from '../EntityQueryContext';
|
import { EntityQueryContext, EntityTransactionalQueryContext } from '../EntityQueryContext';
|
||||||
import EntityQueryContextProvider from '../EntityQueryContextProvider';
|
import EntityQueryContextProvider from '../EntityQueryContextProvider';
|
||||||
import { partitionErrors } from '../entityUtils';
|
import { partitionErrors } from '../entityUtils';
|
||||||
import {
|
import {
|
||||||
@@ -15,6 +15,13 @@ import IEntityMetricsAdapter, { EntityMetricsLoadType } from '../metrics/IEntity
|
|||||||
import { computeIfAbsent, zipToMap } from '../utils/collections/maps';
|
import { computeIfAbsent, zipToMap } from '../utils/collections/maps';
|
||||||
import ReadThroughEntityCache from './ReadThroughEntityCache';
|
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
|
* A data manager is responsible for orchestrating multiple sources of entity
|
||||||
* data including local caches, {@link EntityCacheAdapter}, and {@link EntityDatabaseAdapter}.
|
* 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}.
|
* It is also responsible for invalidating all sources of data when mutated using {@link EntityMutator}.
|
||||||
*/
|
*/
|
||||||
export default class EntityDataManager<TFields> {
|
export default class EntityDataManager<TFields> {
|
||||||
private readonly fieldDataLoaders: Map<
|
private readonly regularFieldDataLoaders: FieldDataLoadersMap<TFields> = new Map();
|
||||||
keyof TFields,
|
|
||||||
DataLoader<NonNullable<TFields[keyof TFields]>, readonly Readonly<TFields>[]>
|
private readonly transactionalFieldDataLoaders: Map<
|
||||||
|
string,
|
||||||
|
FieldDataLoadersMap<TFields>
|
||||||
> = new Map();
|
> = new Map();
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@@ -35,10 +44,10 @@ export default class EntityDataManager<TFields> {
|
|||||||
private readonly entityClassName: string
|
private readonly entityClassName: string
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
private getFieldDataLoaderForFieldName<N extends keyof TFields>(
|
private getRegularFieldDataLoderForFieldName<N extends keyof TFields>(
|
||||||
fieldName: N
|
fieldName: N
|
||||||
): DataLoader<NonNullable<TFields[N]>, readonly Readonly<TFields>[]> {
|
): FieldDataLoader<TFields> {
|
||||||
return computeIfAbsent(this.fieldDataLoaders, fieldName, () => {
|
return computeIfAbsent(this.regularFieldDataLoaders, fieldName, () => {
|
||||||
return new DataLoader(
|
return new DataLoader(
|
||||||
async (
|
async (
|
||||||
fieldValues: readonly NonNullable<TFields[N]>[]
|
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>(
|
private async loadManyForDataLoaderByFieldEqualingAsync<N extends keyof TFields>(
|
||||||
fieldName: N,
|
fieldName: N,
|
||||||
fieldValues: readonly NonNullable<TFields[N]>[]
|
fieldValues: readonly NonNullable<TFields[N]>[]
|
||||||
@@ -61,10 +107,11 @@ export default class EntityDataManager<TFields> {
|
|||||||
fieldValueCount: fieldValues.length,
|
fieldValueCount: fieldValues.length,
|
||||||
entityClassName: this.entityClassName,
|
entityClassName: this.entityClassName,
|
||||||
});
|
});
|
||||||
|
|
||||||
return await this.entityCache.readManyThroughAsync(
|
return await this.entityCache.readManyThroughAsync(
|
||||||
fieldName,
|
fieldName,
|
||||||
fieldValues,
|
fieldValues,
|
||||||
async (fetcherValues) => {
|
async (fetcherValues: readonly NonNullable<TFields[N]>[]) => {
|
||||||
this.metricsAdapter.incrementDataManagerDatabaseLoadCount({
|
this.metricsAdapter.incrementDataManagerDatabaseLoadCount({
|
||||||
fieldValueCount: fieldValues.length,
|
fieldValueCount: fieldValues.length,
|
||||||
entityClassName: this.entityClassName,
|
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({
|
this.metricsAdapter.incrementDataManagerDataloaderLoadCount({
|
||||||
fieldValueCount: fieldValues.length,
|
fieldValueCount: fieldValues.length,
|
||||||
entityClassName: this.entityClassName,
|
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 results = await dataLoader.loadMany(fieldValues);
|
||||||
const [values, errors] = partitionErrors(results);
|
const [values, errors] = partitionErrors(results);
|
||||||
if (errors.length > 0) {
|
if (errors.length > 0) {
|
||||||
@@ -192,7 +236,20 @@ export default class EntityDataManager<TFields> {
|
|||||||
fieldValues: readonly NonNullable<TFields[N]>[]
|
fieldValues: readonly NonNullable<TFields[N]>[]
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
await this.entityCache.invalidateManyAsync(fieldName, fieldValues);
|
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));
|
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]>,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Reference in new issue
Block a user