@orbit/record-cache
Version:
Orbit base classes used to access and maintain a set of records.
283 lines (263 loc) • 8.18 kB
text/typescript
import { QueryExpressionParseError, RequestOptions } from '@orbit/data';
import {
AttributeSortSpecifier,
FindRecord,
FindRecords,
FindRelatedRecord,
FindRelatedRecords,
InitializedRecord,
RecordIdentity,
RecordNotFoundException,
RecordQueryExpression,
RecordQueryExpressionResult,
SortSpecifier
} from '@orbit/records';
import { deepGet, Dict, isNone } from '@orbit/utils';
import { AsyncRecordAccessor } from '../record-accessor';
export interface AsyncQueryOperator {
(
cache: AsyncRecordAccessor,
expression: RecordQueryExpression,
options?: RequestOptions
): Promise<RecordQueryExpressionResult>;
}
export const AsyncQueryOperators: Dict<AsyncQueryOperator> = {
async findRecord(
cache: AsyncRecordAccessor,
expression: RecordQueryExpression,
options?: RequestOptions
): Promise<RecordQueryExpressionResult> {
const { record } = expression as FindRecord;
const currentRecord = await cache.getRecordAsync(record);
if (!currentRecord) {
if (options?.raiseNotFoundExceptions) {
throw new RecordNotFoundException(record.type, record.id);
}
}
return currentRecord;
},
async findRecords(
cache: AsyncRecordAccessor,
expression: RecordQueryExpression,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
options?: RequestOptions
): Promise<InitializedRecord[]> {
const exp = expression as FindRecords;
let results = await cache.getRecordsAsync(exp.records || exp.type);
if (exp.filter) {
results = filterRecords(results, exp.filter);
}
if (exp.sort) {
results = sortRecords(results, exp.sort);
}
if (exp.page) {
results = paginateRecords(results, exp.page);
}
return results;
},
async findRelatedRecords(
cache: AsyncRecordAccessor,
expression: RecordQueryExpression,
options?: RequestOptions
): Promise<InitializedRecord[] | undefined> {
const exp = expression as FindRelatedRecords;
const { record, relationship } = exp;
const relatedIds = await cache.getRelatedRecordsAsync(record, relationship);
if (!relatedIds || relatedIds.length === 0) {
if (!(await cache.getRecordAsync(record))) {
if (options?.raiseNotFoundExceptions) {
throw new RecordNotFoundException(record.type, record.id);
} else {
return undefined;
}
}
return [];
}
let results = await cache.getRecordsAsync(relatedIds);
if (exp.filter) {
results = filterRecords(results, exp.filter);
}
if (exp.sort) {
results = sortRecords(results, exp.sort);
}
if (exp.page) {
results = paginateRecords(results, exp.page);
}
return results;
},
async findRelatedRecord(
cache: AsyncRecordAccessor,
expression: RecordQueryExpression,
options?: RequestOptions
): Promise<InitializedRecord | null | undefined> {
const exp = expression as FindRelatedRecord;
const { record, relationship } = exp;
const relatedId = await cache.getRelatedRecordAsync(record, relationship);
if (relatedId) {
return (await cache.getRecordAsync(relatedId)) || null;
} else {
if (!(await cache.getRecordAsync(record))) {
if (options?.raiseNotFoundExceptions) {
throw new RecordNotFoundException(record.type, record.id);
} else {
return undefined;
}
}
return null;
}
}
};
function filterRecords(records: InitializedRecord[], filters: any[]) {
return records.filter((record) => {
for (let i = 0, l = filters.length; i < l; i++) {
if (!applyFilter(record, filters[i])) {
return false;
}
}
return true;
});
}
function applyFilter(record: InitializedRecord, filter: any): boolean {
if (filter.kind === 'attribute') {
let actual = deepGet(record, ['attributes', filter.attribute]);
if (actual === undefined) {
return false;
}
let expected = filter.value;
switch (filter.op) {
case 'equal':
return actual === expected;
case 'gt':
return actual > expected;
case 'gte':
return actual >= expected;
case 'lt':
return actual < expected;
case 'lte':
return actual <= expected;
default:
throw new QueryExpressionParseError(
'Filter operation ${filter.op} not recognized for Store.'
);
}
} else if (filter.kind === 'relatedRecords') {
let actual: RecordIdentity[] = deepGet(record, [
'relationships',
filter.relation,
'data'
]);
if (actual === undefined) {
return false;
}
let expected: RecordIdentity[] = filter.records;
switch (filter.op) {
case 'equal':
return (
actual.length === expected.length &&
expected.every((e) =>
actual.some((a) => a.id === e.id && a.type === e.type)
)
);
case 'all':
return expected.every((e) =>
actual.some((a) => a.id === e.id && a.type === e.type)
);
case 'some':
return expected.some((e) =>
actual.some((a) => a.id === e.id && a.type === e.type)
);
case 'none':
return !expected.some((e) =>
actual.some((a) => a.id === e.id && a.type === e.type)
);
default:
throw new QueryExpressionParseError(
'Filter operation ${filter.op} not recognized for Store.'
);
}
} else if (filter.kind === 'relatedRecord') {
let actual = deepGet(record, ['relationships', filter.relation, 'data']);
if (actual === undefined) {
return false;
}
let expected = filter.record;
switch (filter.op) {
case 'equal':
if (actual === null) {
return expected === null;
} else {
if (Array.isArray(expected)) {
return expected.some(
(e) => actual.type === e.type && actual.id === e.id
);
} else if (expected) {
return actual.type === expected.type && actual.id === expected.id;
} else {
return false;
}
}
default:
throw new QueryExpressionParseError(
'Filter operation ${filter.op} not recognized for Store.'
);
}
}
return false;
}
function sortRecords(
records: InitializedRecord[],
sortSpecifiers: SortSpecifier[]
): InitializedRecord[] {
const comparisonValues = new Map();
records.forEach((record) => {
comparisonValues.set(
record,
sortSpecifiers.map((sortSpecifier) => {
if (sortSpecifier.kind === 'attribute') {
return deepGet(record, [
'attributes',
(sortSpecifier as AttributeSortSpecifier).attribute
]);
} else {
throw new QueryExpressionParseError(
'Sort specifier ${sortSpecifier.kind} not recognized for Store.'
);
}
})
);
});
const comparisonOrders = sortSpecifiers.map((sortExpression) =>
sortExpression.order === 'descending' ? -1 : 1
);
return records.sort((record1, record2) => {
const values1 = comparisonValues.get(record1);
const values2 = comparisonValues.get(record2);
for (let i = 0; i < sortSpecifiers.length; i++) {
if (values1[i] < values2[i]) {
return -comparisonOrders[i];
} else if (values1[i] > values2[i]) {
return comparisonOrders[i];
} else if (isNone(values1[i]) && !isNone(values2[i])) {
return comparisonOrders[i];
} else if (isNone(values2[i]) && !isNone(values1[i])) {
return -comparisonOrders[i];
}
}
return 0;
});
}
function paginateRecords(
records: InitializedRecord[],
paginationOptions: any
): InitializedRecord[] {
if (paginationOptions.limit !== undefined) {
let offset =
paginationOptions.offset === undefined ? 0 : paginationOptions.offset;
let limit = paginationOptions.limit;
return records.slice(offset, offset + limit);
} else {
throw new QueryExpressionParseError(
'Pagination options not recognized for Store. Please specify `offset` and `limit`.'
);
}
}