@travetto/model-mongo
Version:
Mongo backing for the travetto model module.
215 lines (193 loc) • 7.78 kB
text/typescript
import {
Binary, type CreateIndexesOptions, type Filter, type FindCursor, type IndexDirection, ObjectId, type WithId as MongoWithId
} from 'mongodb';
import { AppError, castTo, Class, toConcrete, TypedObject } from '@travetto/runtime';
import { type DistanceUnit, type PageableModelQuery, type WhereClause, ModelQueryUtil } from '@travetto/model-query';
import type { ModelType, IndexField, IndexConfig } from '@travetto/model';
import { DataUtil, SchemaRegistry, type Point } from '@travetto/schema';
const PointImpl = toConcrete<Point>();
type IdxCfg = CreateIndexesOptions;
/**
* Converting units to various radians
*/
const RADIANS_TO: Record<DistanceUnit, number> = {
km: 6378,
mi: 3963,
m: 6378000,
ft: 20924640,
rad: 1
};
export type WithId<T, I = unknown> = T & { _id?: I };
export type BasicIdx = Record<string, IndexDirection>;
export type PlainIdx = Record<string, -1 | 0 | 1>;
/**
* Basic mongo utils for conforming to the model module
*/
export class MongoUtil {
static toIndex<T extends ModelType>(f: IndexField<T>): PlainIdx {
const keys = [];
while (typeof f !== 'number' && typeof f !== 'boolean' && Object.keys(f)) {
const key = TypedObject.keys(f)[0];
f = castTo(f[key]);
keys.push(key);
}
const rf: number | boolean = castTo(f);
return {
[keys.join('.')]: typeof rf === 'boolean' ? (rf ? 1 : 0) : castTo<-1 | 1 | 0>(rf)
};
}
static uuid(val: string): Binary {
return new Binary(Buffer.from(val.replaceAll('-', ''), 'hex'), Binary.SUBTYPE_UUID);
}
static idToString(id: string | ObjectId | Binary): string {
if (typeof id === 'string') {
return id;
} else if (id instanceof ObjectId) {
return id.toHexString();
} else {
return Buffer.from(id.buffer).toString('hex');
}
}
static extractWhereFilter<T extends ModelType, U extends WhereClause<T>>(cls: Class<T>, where?: U, checkExpiry = true): Filter<T> {
where = castTo(ModelQueryUtil.getWhereClause(cls, where, checkExpiry));
return castTo(where ? this.extractWhereClause(cls, where) : {});
}
/**
* Build mongo where clause
*/
static extractWhereClause<T>(cls: Class<T>, o: WhereClause<T>): Record<string, unknown> {
if (ModelQueryUtil.has$And(o)) {
return { $and: o.$and.map(x => this.extractWhereClause<T>(cls, x)) };
} else if (ModelQueryUtil.has$Or(o)) {
return { $or: o.$or.map(x => this.extractWhereClause<T>(cls, x)) };
} else if (ModelQueryUtil.has$Not(o)) {
return { $nor: [this.extractWhereClause<T>(cls, o.$not)] };
} else {
return this.extractSimple(cls, o);
}
}
/**/
static extractSimple<T>(base: Class<T> | undefined, o: Record<string, unknown>, path: string = '', recursive: boolean = true): Record<string, unknown> {
const schema = base ? SchemaRegistry.get(base) : undefined;
const out: Record<string, unknown> = {};
const sub = o;
const keys = Object.keys(sub);
for (const key of keys) {
const subpath = `${path}${key}`;
const v: Record<string, unknown> = castTo(sub[key]);
const subField = schema?.totalView.schema[key];
const isPlain = v && DataUtil.isPlainObject(v);
const firstKey = isPlain ? Object.keys(v)[0] : '';
if (subpath === 'id') {
if (!firstKey) {
out._id = Array.isArray(v) ? v.map(x => this.uuid(x)) : this.uuid(`${v}`);
} else if (firstKey === '$in' || firstKey === '$nin' || firstKey === '$eq' || firstKey === '$ne') {
const temp = v[firstKey];
out._id = { [firstKey]: Array.isArray(temp) ? temp.map(x => this.uuid(x)) : this.uuid(`${temp}`) };
} else {
throw new AppError('Invalid id query');
}
} else if ((isPlain && !firstKey.startsWith('$')) || v?.constructor?.Ⲑid) {
if (recursive) {
Object.assign(out, this.extractSimple(subField?.type, v, `${subpath}.`, recursive));
} else {
out[subpath] = v;
}
} else {
if (firstKey === '$gt' || firstKey === '$lt' || firstKey === '$gte' || firstKey === '$lte') {
for (const [sk, sv] of Object.entries(v)) {
v[sk] = ModelQueryUtil.resolveComparator(sv);
}
} else if (firstKey === '$exists' && subField?.array) {
const exists = v.$exists;
if (!exists) {
delete v.$exists;
v.$in = [null, []];
} else {
v.$exists = true;
v.$nin = [null, []];
}
} else if (firstKey === '$regex') {
v.$regex = DataUtil.toRegex(castTo(v.$regex));
} else if (firstKey && '$near' in v) {
const dist: number = castTo(v.$maxDistance);
const distance = dist / RADIANS_TO[(castTo<DistanceUnit>(v.$unit) ?? 'km')];
v.$maxDistance = distance;
delete v.$unit;
} else if (firstKey && '$geoWithin' in v) {
const coords: [number, number][] = castTo(v.$geoWithin);
const first = coords[0];
const last = coords[coords.length - 1];
// Connect if not
if (first[0] !== last[0] || first[1] !== last[1]) {
coords.push(first);
}
v.$geoWithin = {
$geometry: {
type: 'Polygon',
coordinates: [coords]
}
};
}
out[subpath === 'id' ? '_id' : subpath] = v;
}
}
return out;
}
static getExtraIndices<T extends ModelType>(cls: Class<T>): BasicIdx[] {
const out: BasicIdx[] = [];
const textFields: string[] = [];
SchemaRegistry.visitFields(cls, (field, path) => {
if (field.type === PointImpl) {
const name = [...path, field].map(x => x.name).join('.');
out.push({ [name]: '2d' });
} else if (field.specifiers?.includes('text') && (field.specifiers?.includes('long') || field.specifiers.includes('search'))) {
const name = [...path, field].map(x => x.name).join('.');
textFields.push(name);
}
});
if (textFields.length) {
const text: BasicIdx = {};
for (const field of textFields) {
text[field] = 'text';
}
out.push(text);
}
return out;
}
static getPlainIndex(idx: IndexConfig<ModelType>): PlainIdx {
let out: PlainIdx = {};
for (const cfg of idx.fields.map(x => this.toIndex(x))) {
out = Object.assign(out, cfg);
}
return out;
}
static getIndices<T extends ModelType>(cls: Class<T>, indices: IndexConfig<ModelType>[] = []): [BasicIdx, IdxCfg][] {
return [
...indices.map(idx => [this.getPlainIndex(idx), (idx.type === 'unique' ? { unique: true } : {})] as const),
...this.getExtraIndices(cls).map((x) => [x, {}] as const)
].map(x => [...x]);
}
static prepareCursor<T extends ModelType>(cls: Class<T>, cursor: FindCursor<T | MongoWithId<T>>, query: PageableModelQuery<T>): FindCursor<T> {
if (query.select) {
const selectKey = Object.keys(query.select)[0];
const select = typeof selectKey === 'string' && selectKey.startsWith('$') ? query.select : this.extractSimple(cls, query.select);
// Remove id if not explicitly defined, and selecting fields directly
if (!select['_id']) {
const values = new Set([...Object.values(select)]);
if (values.has(1) || values.has(true)) {
select['_id'] = false;
}
}
cursor.project(select);
}
if (query.sort) {
cursor = cursor.sort(Object.assign({}, ...query.sort.map(x => this.extractSimple(cls, x))));
}
cursor = cursor.limit(Math.trunc(query.limit ?? 200));
if (query.offset && typeof query.offset === 'number') {
cursor = cursor.skip(Math.trunc(query.offset ?? 0));
}
return castTo(cursor);
}
}