@baqhub/sdk
Version:
The official JavaScript SDK for the BAQ federated app platform.
274 lines (273 loc) • 9.31 kB
JavaScript
import { JSONPath } from "jsonpath-plus";
import flow from "lodash/flow.js";
import isEqual from "lodash/isEqual.js";
import isString from "lodash/isString.js";
import orderBy from "lodash/orderBy.js";
import uniqBy from "lodash/uniqBy.js";
import { Constants } from "../../constants.js";
import { Array } from "../../helpers/array.js";
import { Str } from "../../helpers/string.js";
import { isDefined } from "../../helpers/type.js";
import { RecordSource, } from "../records/record.js";
import { RecordKey } from "../records/recordKey.js";
import { normalizePath } from "./pathHelpers.js";
import { QueryDate } from "./queryDate.js";
import { Q, QueryFilter } from "./queryFilter.js";
import { QuerySort, QuerySortDirection, QuerySortProperty } from "./querySort.js";
const includeLinkSpecialValues = [
"entity",
"standing",
"existential",
];
const defaultIncludeLinks = [
"entity",
"existential",
];
const defaultSources = [
RecordSource.SELF,
RecordSource.NOTIFICATION,
RecordSource.SUBSCRIPTION,
];
const defaultSortBy = [
QuerySortProperty.VERSION_RECEIVED_AT,
QuerySortDirection.DESCENDING,
];
//
// I/O.
//
function queryNew(query) {
return query;
}
function queryOfKey(key, baseQuery = {}) {
const { entity, recordId } = RecordKey.toComponents(key);
return {
...baseQuery,
pageSize: 1,
filter: Q.and(Q.author(entity), Q.id(recordId)),
};
}
function querySingleToQueryString(query) {
if (!query) {
return "";
}
return paramsToString([
["include_links", includeLinksToString(query.includeLinks)],
["include_deleted", query.includeDeleted ? "true" : undefined],
["proxy_to", query.proxyTo],
]);
}
function queryToQueryString(query) {
const filterStrings = query.filter && QueryFilter.toListString(query.filter);
return paramsToString([
["sort", query.sort && QuerySort.toString(query.sort)],
["min", query.min && QueryDate.toString(query.min)],
["max", query.max && QueryDate.toString(query.max)],
["page_start", query.pageStart && QueryDate.toString(query.pageStart)],
["page_size", (query.pageSize || Constants.defaultPageSize).toString()],
["distinct", query.distinct && normalizePath(query.distinct)],
["sources", query.sources?.join(",")],
...(filterStrings || []).map(f => ["filter", f]),
["include_links", includeLinksToString(query.includeLinks)],
["include_deleted", query.includeDeleted ? "true" : undefined],
["proxy_to", query.proxyTo],
]);
}
function queryToSync(query, boundary) {
return {
max: undefined,
min: boundary,
sort: QuerySort.syncDefault,
pageStart: undefined,
pageSize: 100,
distinct: undefined,
sources: query.sources,
filter: query.filter,
mode: query.mode,
includeLinks: query.includeLinks,
includeDeleted: true,
proxyTo: query.proxyTo,
};
}
function queryFindBoundary(query, record) {
const sort = query.sort || QuerySort.default;
return [QuerySort.findDateInRecord(record, sort), record.id];
}
function includeLinksToString(includeLinks) {
function includeLinkToString(link) {
if (includeLinkSpecialValues.includes(link)) {
return link;
}
return normalizePath(link);
}
return includeLinks?.map(includeLinkToString).join(",");
}
function includeLinksIsSuperset(links1, links2) {
const l1 = links1 || defaultIncludeLinks;
const l2 = links2 || defaultIncludeLinks;
return Array.isSuperset(l1, l2);
}
function sourcesIsSuperset(sources1, sources2) {
const s1 = sources1 || defaultSources;
const s2 = sources2 || defaultSources;
return Array.isSuperset(s1, s2);
}
function paramsToString(params) {
const filteredParams = params
.map(p => (isDefined(p[1]) ? [p[0], p[1]] : undefined))
.filter(isDefined);
return Str.buildQuery(filteredParams);
}
function findQueryMaxDate(query, sortDirection) {
if (query.pageStart && sortDirection === QuerySortDirection.DESCENDING) {
return query.pageStart;
}
return query.max;
}
function findQueryMinDate(query, sortDirection) {
if (query.pageStart && sortDirection === QuerySortDirection.ASCENDING) {
return query.pageStart;
}
return query.min;
}
function queryFilter(query, records, { ignorePageSize, boundary } = {}) {
const sortBy = query.sort || defaultSortBy;
const sortDir = QuerySort.toDirection(sortBy);
const isAscending = sortDir === QuerySortDirection.ASCENDING;
const maxDate = findQueryMaxDate(query, sortDir);
const minDate = findQueryMinDate(query, sortDir);
function sort() {
const order = isAscending ? "asc" : "desc";
return (list) => {
return orderBy(list, r => QuerySort.findDateInRecord(r, sortBy), order);
};
}
function distinct() {
const { distinct } = query;
if (!distinct) {
return (list) => list;
}
const distinctValue = (record) => {
const recordValues = JSONPath({
path: distinct,
json: record,
}).filter(isDefined);
if (recordValues.length === 0) {
return `null+${record.author.entity}+${record.id}`;
}
// TODO: Implement link detection for correct logic.
const valueToString = (value) => {
if (isString(value)) {
return `"${value}"`;
}
if ("versionHash" in value) {
return `${value.entity}+${value.recordId}+${value.versionHash}`;
}
if ("recordId" in value) {
return `${value.entity}+${value.recordId}`;
}
if ("entity" in value) {
return value.entity;
}
return String(value);
};
return recordValues.map(valueToString).join(":");
};
return (list) => uniqBy(list, distinctValue);
}
function filter() {
function isMatch(record) {
// Exclude deleted.
if ("noContent" in record) {
return false;
}
// Date boundaries.
const recordDate = QuerySort.findDateInRecord(record, sortBy);
const recordId = record.id;
if (QueryDate.compare(recordDate, recordId, maxDate) > 0) {
return false;
}
if (QueryDate.compare(recordDate, recordId, minDate) < 0) {
return false;
}
if (boundary) {
const result = QueryDate.compare(recordDate, recordId, boundary);
if (isAscending ? result > 0 : result < 0) {
return false;
}
}
if (query.mode && record.mode !== query.mode) {
return false;
}
// Sources.
const sources = query.sources || defaultSources;
if (!sources.includes(record.source) &&
record.source !== RecordSource.PROXY) {
return false;
}
// Filter.
return !query.filter || QueryFilter.isMatch(record, query.filter);
}
return (list) => {
return list.filter(isMatch);
};
}
function pageSize() {
if (ignorePageSize) {
return list => list;
}
return list => {
return list.slice(0, query.pageSize || Constants.defaultPageSize);
};
}
return flow(sort(), distinct(), filter(), pageSize())(records);
}
function queryIsMatch(query1, query2) {
const { ["filter"]: filter1, ...q1 } = query1;
const { ["filter"]: filter2, ...q2 } = query2;
if (!isEqual(q1, q2)) {
return false;
}
if (filter1 && filter2) {
return (QueryFilter.isSuperset(filter1, filter2) &&
QueryFilter.isSuperset(filter2, filter1));
}
return !filter1 && !filter2;
}
function queryIsSuperset(query1, query2) {
if (query1.distinct !== query2.distinct) {
return false;
}
return queryIsSyncSuperset(query1, query2);
}
function queryIsSyncSuperset(query1, query2) {
if (query1.mode !== query2.mode || query1.proxyTo !== query2.proxyTo) {
return false;
}
if (!includeLinksIsSuperset(query1.includeLinks, query2.includeLinks)) {
return false;
}
if (!sourcesIsSuperset(query1.sources, query2.sources)) {
return false;
}
if (query2.includeDeleted && !query1.includeDeleted) {
return false;
}
if (query1.filter && query2.filter) {
return QueryFilter.isSuperset(query1.filter, query2.filter);
}
return !query1.filter && !query2.filter;
}
export const Query = {
new: queryNew,
ofKey: queryOfKey,
singleToQueryString: querySingleToQueryString,
toQueryString: queryToQueryString,
toSync: queryToSync,
findBoundary: queryFindBoundary,
filter: queryFilter,
isMatch: queryIsMatch,
isSuperset: queryIsSuperset,
isSyncSuperset: queryIsSyncSuperset,
defaultIncludeLinks,
defaultSources,
};