@travetto/model-query
Version:
Datastore abstraction for advanced query support.
93 lines (81 loc) • 2.98 kB
text/typescript
import { ModelRegistry, ModelType } from '@travetto/model';
import { castTo, Class, hasFunction } from '@travetto/runtime';
import { SchemaRegistry } from '@travetto/schema';
import { PageableModelQuery, Query } from '../model/query.ts';
import { ValidStringFields, WhereClauseRaw } from '../model/where-clause.ts';
import { ModelQuerySuggestSupport } from '../types/suggest.ts';
/**
* Tools for building suggestion queries
*/
export class ModelQuerySuggestUtil {
/**
* Type guard for determining if service supports query suggest operations
*/
static isSupported = hasFunction<ModelQuerySuggestSupport>('suggest');
/**
* Build regex for suggesting
*/
static getSuggestRegex(prefix?: string): RegExp {
return prefix ? new RegExp(`\\b${prefix}.*`, 'i') : /./;
}
/**
* Build suggest query on top of query language
*/
static getSuggestQuery<T extends ModelType>(cls: Class<T>, field: ValidStringFields<T>, prefix?: string, query?: Query<T>): Query<T> {
const config = ModelRegistry.get(cls);
const limit = query?.limit ?? 10;
const clauses: WhereClauseRaw<ModelType>[] = prefix ? [{ [field]: { $regex: this.getSuggestRegex(prefix) } }] : [];
if (config.subType) {
const { subTypeField, subTypeName } = SchemaRegistry.get(cls);
clauses.push({ [subTypeField]: subTypeName });
}
if (config.expiresAt) {
clauses.push({ [config.expiresAt]: { $gt: new Date() } });
}
if (query?.where) {
clauses.push(query.where);
}
return {
where: clauses.length ? (clauses.length > 1 ? { $and: clauses } : clauses[0]) : {},
limit,
select: query?.select
};
}
/**
* Join suggestion results
*/
static combineSuggestResults<T extends ModelType, U>(
cls: Class<T>,
field: ValidStringFields<T>,
prefix: string = '',
results: T[],
transform: (value: string, entity: T) => U,
limit?: number
): U[] {
const pattern = this.getSuggestRegex(prefix);
const out: ([string, U] | readonly [string, U])[] = [];
for (const r of results) {
const val = r[field];
if (Array.isArray(val)) {
out.push(...val.filter(f => pattern.test(f)).map((f: string) => [f, transform(f, r)] as const));
} else if (typeof val === 'string') {
out.push([val, transform(val, r)]);
}
}
return out
.toSorted((a, b) => a[0].localeCompare(b[0]))
.map((a) => a[1])
.filter((x, i, arr) => x !== arr[i - 1])
.slice(0, limit ?? 10);
}
/**
* Build suggestion query
*/
static getSuggestFieldQuery<T extends ModelType>(cls: Class<T>, field: ValidStringFields<T>, prefix?: string, query?: PageableModelQuery<T>): Query<T> {
const config = ModelRegistry.get(cls);
return this.getSuggestQuery<T>(cls, castTo(field), prefix, {
...(query ?? {}),
select: castTo({ [field]: true, ...(config.subType ? { [SchemaRegistry.get(cls).subTypeField]: true } : {}) })
});
}
}