@orbit/record-cache
Version:
Orbit base classes used to access and maintain a set of records.
596 lines (533 loc) • 17.9 kB
text/typescript
import { Assertion, Orbit } from '@orbit/core';
import {
buildQuery,
buildTransform,
DefaultRequestOptions,
FullRequestOptions,
FullResponse,
OperationTerm,
QueryOrExpressions,
RequestOptions,
TransformOrOperations
} from '@orbit/data';
import {
InitializedRecord,
RecordIdentity,
RecordOperation,
RecordOperationResult,
RecordOperationTerm,
RecordQuery,
RecordQueryBuilder,
RecordQueryExpression,
RecordQueryResult,
recordsReferencedByOperations,
RecordTransform,
RecordTransformBuilder,
RecordTransformBuilderFunc,
RecordTransformResult,
SyncRecordQueryable,
SyncRecordUpdatable
} from '@orbit/records';
import { deepGet, Dict, toArray } from '@orbit/utils';
import { SyncLiveQuery } from './live-query/sync-live-query';
import { SyncCacheIntegrityProcessor } from './operation-processors/sync-cache-integrity-processor';
import { SyncSchemaConsistencyProcessor } from './operation-processors/sync-schema-consistency-processor';
import { SyncSchemaValidationProcessor } from './operation-processors/sync-schema-validation-processor';
import {
SyncInverseTransformOperator,
SyncInverseTransformOperators
} from './operators/sync-inverse-transform-operators';
import {
SyncQueryOperator,
SyncQueryOperators
} from './operators/sync-query-operators';
import {
SyncTransformOperator,
SyncTransformOperators
} from './operators/sync-transform-operators';
import {
RecordChangeset,
RecordRelationshipIdentity,
SyncRecordAccessor
} from './record-accessor';
import {
RecordCache,
RecordCacheQueryOptions,
RecordCacheSettings,
RecordCacheTransformOptions
} from './record-cache';
import { RecordTransformBuffer } from './record-transform-buffer';
import { PatchResult, RecordCacheUpdateDetails } from './response';
import {
SyncOperationProcessor,
SyncOperationProcessorClass
} from './sync-operation-processor';
const { assert, deprecate } = Orbit;
export interface SyncRecordCacheSettings<
QO extends RequestOptions = RecordCacheQueryOptions,
TO extends RequestOptions = RecordCacheTransformOptions,
QB = RecordQueryBuilder,
TB = RecordTransformBuilder
> extends RecordCacheSettings<QO, TO, QB, TB> {
processors?: SyncOperationProcessorClass[];
queryOperators?: Dict<SyncQueryOperator>;
transformOperators?: Dict<SyncTransformOperator>;
inverseTransformOperators?: Dict<SyncInverseTransformOperator>;
debounceLiveQueries?: boolean;
transformBuffer?: RecordTransformBuffer;
}
export abstract class SyncRecordCache<
QO extends RequestOptions = RecordCacheQueryOptions,
TO extends RequestOptions = RecordCacheTransformOptions,
QB = RecordQueryBuilder,
TB = RecordTransformBuilder,
QueryResponseDetails = unknown,
TransformResponseDetails extends RecordCacheUpdateDetails = RecordCacheUpdateDetails
>
extends RecordCache<QO, TO, QB, TB>
implements
SyncRecordAccessor,
SyncRecordQueryable<QueryResponseDetails, QB, QO>,
SyncRecordUpdatable<TransformResponseDetails, TB, TO> {
protected _processors: SyncOperationProcessor[];
protected _queryOperators: Dict<SyncQueryOperator>;
protected _transformOperators: Dict<SyncTransformOperator>;
protected _inverseTransformOperators: Dict<SyncInverseTransformOperator>;
protected _debounceLiveQueries: boolean;
protected _transformBuffer?: RecordTransformBuffer;
constructor(settings: SyncRecordCacheSettings<QO, TO, QB, TB>) {
super(settings);
this._queryOperators = settings.queryOperators ?? SyncQueryOperators;
this._transformOperators =
settings.transformOperators ?? SyncTransformOperators;
this._inverseTransformOperators =
settings.inverseTransformOperators ?? SyncInverseTransformOperators;
this._debounceLiveQueries = settings.debounceLiveQueries !== false;
this._transformBuffer = settings.transformBuffer;
const processors: SyncOperationProcessorClass[] = settings.processors
? settings.processors
: [SyncSchemaConsistencyProcessor, SyncCacheIntegrityProcessor];
if (settings.autoValidate !== false && settings.processors === undefined) {
processors.push(SyncSchemaValidationProcessor);
}
this._processors = processors.map((Processor) => {
let processor = new Processor(this);
assert(
'Each processor must extend SyncOperationProcessor',
processor instanceof SyncOperationProcessor
);
return processor;
});
}
get processors(): SyncOperationProcessor[] {
return this._processors;
}
getQueryOperator(op: string): SyncQueryOperator {
return this._queryOperators[op];
}
getTransformOperator(op: string): SyncTransformOperator {
return this._transformOperators[op];
}
getInverseTransformOperator(op: string): SyncInverseTransformOperator {
return this._inverseTransformOperators[op];
}
// Abstract methods for getting records and relationships
abstract getRecordSync(
recordIdentity: RecordIdentity
): InitializedRecord | undefined;
abstract getRecordsSync(
typeOrIdentities?: string | RecordIdentity[]
): InitializedRecord[];
abstract getInverseRelationshipsSync(
recordIdentityOrIdentities: RecordIdentity | RecordIdentity[]
): RecordRelationshipIdentity[];
// Abstract methods for setting records and relationships
abstract setRecordSync(record: InitializedRecord): void;
abstract setRecordsSync(records: InitializedRecord[]): void;
abstract removeRecordSync(
recordIdentity: RecordIdentity
): InitializedRecord | undefined;
abstract removeRecordsSync(
recordIdentities: RecordIdentity[]
): InitializedRecord[];
abstract addInverseRelationshipsSync(
relationships: RecordRelationshipIdentity[]
): void;
abstract removeInverseRelationshipsSync(
relationships: RecordRelationshipIdentity[]
): void;
applyRecordChangesetSync(changeset: RecordChangeset): void {
const {
setRecords,
removeRecords,
addInverseRelationships,
removeInverseRelationships
} = changeset;
if (setRecords && setRecords.length > 0) {
this.setRecordsSync(setRecords);
}
if (removeRecords && removeRecords.length > 0) {
this.removeRecordsSync(removeRecords);
}
if (addInverseRelationships && addInverseRelationships.length > 0) {
this.addInverseRelationshipsSync(addInverseRelationships);
}
if (removeInverseRelationships && removeInverseRelationships.length > 0) {
this.removeInverseRelationshipsSync(removeInverseRelationships);
}
}
getRelatedRecordSync(
identity: RecordIdentity,
relationship: string
): RecordIdentity | null | undefined {
const record = this.getRecordSync(identity);
if (record) {
return deepGet(record, ['relationships', relationship, 'data']);
}
return undefined;
}
getRelatedRecordsSync(
identity: RecordIdentity,
relationship: string
): RecordIdentity[] | undefined {
const record = this.getRecordSync(identity);
if (record) {
return deepGet(record, ['relationships', relationship, 'data']);
}
return undefined;
}
/**
* Queries the cache.
*/
query<RequestData extends RecordQueryResult = RecordQueryResult>(
queryOrExpressions: QueryOrExpressions<RecordQueryExpression, QB>,
options?: DefaultRequestOptions<QO>,
id?: string
): RequestData;
query<RequestData extends RecordQueryResult = RecordQueryResult>(
queryOrExpressions: QueryOrExpressions<RecordQueryExpression, QB>,
options: FullRequestOptions<QO>,
id?: string
): FullResponse<RequestData, QueryResponseDetails, RecordOperation>;
query<RequestData extends RecordQueryResult = RecordQueryResult>(
queryOrExpressions: QueryOrExpressions<RecordQueryExpression, QB>,
options?: QO,
id?: string
):
| RequestData
| FullResponse<RequestData, QueryResponseDetails, RecordOperation> {
const query = buildQuery<RecordQueryExpression, QB>(
queryOrExpressions,
options,
id,
this._queryBuilder
);
const response = this._query<RequestData>(query, options);
if (options?.fullResponse) {
return response;
} else {
return response.data as RequestData;
}
}
/**
* Updates the cache.
*/
update<RequestData extends RecordTransformResult = RecordTransformResult>(
transformOrOperations: TransformOrOperations<RecordOperation, TB>,
options?: DefaultRequestOptions<TO>,
id?: string
): RequestData;
update<RequestData extends RecordTransformResult = RecordTransformResult>(
transformOrOperations: TransformOrOperations<RecordOperation, TB>,
options: FullRequestOptions<TO>,
id?: string
): FullResponse<RequestData, TransformResponseDetails, RecordOperation>;
update<RequestData extends RecordTransformResult = RecordTransformResult>(
transformOrOperations: TransformOrOperations<RecordOperation, TB>,
options?: TO,
id?: string
):
| RequestData
| FullResponse<RequestData, TransformResponseDetails, RecordOperation> {
const transform = buildTransform(
transformOrOperations,
options,
id,
this._transformBuilder
);
const response = this._update<RequestData>(transform, options);
if (options?.fullResponse) {
return response;
} else {
return response.data as RequestData;
}
}
/**
* Patches the cache with an operation or operations.
*
* @deprecated since v0.17
*/
patch(
operationOrOperations:
| RecordOperation
| RecordOperation[]
| RecordOperationTerm
| RecordOperationTerm[]
| RecordTransformBuilderFunc
): PatchResult {
deprecate(
'SyncRecordCache#patch has been deprecated. Use SyncRecordCache#update instead.'
);
// TODO - Why is this `this` cast necessary for TS to understand the correct
// method overload?
const { data, details } = (this as any).update(operationOrOperations, {
fullResponse: true
});
return {
inverse: details?.inverseOperations || [],
data: Array.isArray(data) ? data : [data]
};
}
liveQuery(
queryOrExpressions: QueryOrExpressions<RecordQueryExpression, QB>,
options?: DefaultRequestOptions<QO>,
id?: string
): SyncLiveQuery<QO, TO, QB, TB> {
const query = buildQuery(
queryOrExpressions,
options,
id,
this.queryBuilder
);
let debounce = options && (options as any).debounce;
if (typeof debounce !== 'boolean') {
debounce = this._debounceLiveQueries;
}
return new SyncLiveQuery<QO, TO, QB, TB>({
debounce,
cache: this,
query
});
}
/////////////////////////////////////////////////////////////////////////////
// Protected methods
/////////////////////////////////////////////////////////////////////////////
protected _query<RequestData extends RecordQueryResult = RecordQueryResult>(
query: RecordQuery,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
options?: QO
): FullResponse<RequestData, QueryResponseDetails, RecordOperation> {
let data;
if (Array.isArray(query.expressions)) {
data = [];
for (let expression of query.expressions) {
const queryOperator = this.getQueryOperator(expression.op);
if (!queryOperator) {
throw new Error(`Unable to find query operator: ${expression.op}`);
}
data.push(
queryOperator(
this,
expression,
this.getQueryOptions(query, expression)
)
);
}
} else {
const expression = query.expressions as RecordQueryExpression;
const queryOperator = this.getQueryOperator(expression.op);
if (!queryOperator) {
throw new Error(`Unable to find query operator: ${expression.op}`);
}
data = queryOperator(
this,
expression,
this.getQueryOptions(query, expression)
);
}
return { data: data as RequestData };
}
protected _update<
RequestData extends RecordTransformResult = RecordTransformResult
>(
transform: RecordTransform,
options?: TO
): FullResponse<RequestData, TransformResponseDetails, RecordOperation> {
if (this.getTransformOptions(transform)?.useBuffer) {
const buffer = this._initTransformBuffer(transform);
buffer.startTrackingChanges();
const response = buffer.update(transform, {
fullResponse: true
});
const changes = buffer.stopTrackingChanges();
this.applyRecordChangesetSync(changes);
const {
appliedOperations,
appliedOperationResults
} = response.details as TransformResponseDetails;
for (let i = 0, len = appliedOperations.length; i < len; i++) {
this.emit('patch', appliedOperations[i], appliedOperationResults[i]);
}
return response as FullResponse<
RequestData,
TransformResponseDetails,
RecordOperation
>;
} else {
const response = {
data: []
} as FullResponse<
RecordOperationResult[],
RecordCacheUpdateDetails,
RecordOperation
>;
if (options?.fullResponse) {
response.details = {
appliedOperations: [],
appliedOperationResults: [],
inverseOperations: []
};
}
let data: RecordTransformResult;
if (Array.isArray(transform.operations)) {
this._applyTransformOperations(
transform,
transform.operations,
response,
true
);
data = response.data;
} else {
this._applyTransformOperation(
transform,
transform.operations,
response,
true
);
if (Array.isArray(response.data)) {
data = response.data[0];
}
}
if (options?.fullResponse) {
response.details?.inverseOperations.reverse();
}
return {
...response,
data
} as FullResponse<RequestData, TransformResponseDetails, RecordOperation>;
}
}
protected _getTransformBuffer(): RecordTransformBuffer {
if (this._transformBuffer === undefined) {
throw new Assertion(
'transformBuffer must be provided to cache via constructor settings'
);
}
return this._transformBuffer;
}
protected _initTransformBuffer(
transform: RecordTransform
): RecordTransformBuffer {
const buffer = this._getTransformBuffer();
const records = recordsReferencedByOperations(
toArray(transform.operations)
);
const inverseRelationships = this.getInverseRelationshipsSync(records);
const relatedRecords = inverseRelationships.map((ir) => ir.record);
Array.prototype.push.apply(records, relatedRecords);
buffer.resetState();
buffer.setRecordsSync(this.getRecordsSync(records));
buffer.addInverseRelationshipsSync(inverseRelationships);
return buffer;
}
protected _applyTransformOperations(
transform: RecordTransform,
ops: RecordOperation[] | RecordOperationTerm[],
response: FullResponse<
RecordOperationResult[],
RecordCacheUpdateDetails,
RecordOperation
>,
primary = false
): void {
for (const op of ops) {
this._applyTransformOperation(transform, op, response, primary);
}
}
protected _applyTransformOperation(
transform: RecordTransform,
operation: RecordOperation | RecordOperationTerm,
response: FullResponse<
RecordOperationResult[],
RecordCacheUpdateDetails,
RecordOperation
>,
primary = false
): void {
if (operation instanceof OperationTerm) {
operation = operation.toOperation() as RecordOperation;
}
for (let processor of this._processors) {
processor.validate(operation);
}
const inverseTransformOperator = this.getInverseTransformOperator(
operation.op
);
const inverseOp: RecordOperation | undefined = inverseTransformOperator(
this,
operation,
this.getTransformOptions(transform, operation)
);
if (inverseOp) {
response.details?.inverseOperations?.push(inverseOp);
// Query and perform related `before` operations
for (let processor of this._processors) {
this._applyTransformOperations(
transform,
processor.before(operation),
response
);
}
// Query related `after` operations before performing
// the requested operation. These will be applied on success.
let preparedOps = [];
for (let processor of this._processors) {
preparedOps.push(processor.after(operation));
}
// Perform the requested operation
let transformOperator = this.getTransformOperator(operation.op);
let data = transformOperator(
this,
operation,
this.getTransformOptions(transform, operation)
);
if (primary) {
response.data?.push(data);
}
if (response.details) {
response.details.appliedOperationResults.push(data);
response.details.appliedOperations.push(operation);
}
// Query and perform related `immediate` operations
for (let processor of this._processors) {
processor.immediate(operation);
}
// Emit event
this.emit('patch', operation, data);
// Perform prepared operations after performing the requested operation
for (let ops of preparedOps) {
this._applyTransformOperations(transform, ops, response);
}
// Query and perform related `finally` operations
for (let processor of this._processors) {
this._applyTransformOperations(
transform,
processor.finally(operation),
response
);
}
} else if (primary) {
response.data?.push(undefined);
}
}
}