@thisisagile/easy-mongo
Version:
Add support for MongoDB
178 lines (165 loc) • 7.74 kB
text/typescript
import { Filter, FindOptions } from './MongoProvider';
import {
asNumber,
asString,
Currency,
Get,
Id,
ifDefined,
ifNotEmpty,
isDefined,
isPresent,
isPrimitive,
isString,
meta,
ofGet,
on,
OneOrMore,
Optional,
PartialRecord,
toArray,
use,
} from '@thisisagile/easy';
import { toMongoType } from './Utils';
export const asc = 1;
export const desc = -1;
export type Accumulators = '$sum' | '$count' | '$multiply' | '$avg' | '$first' | '$last' | '$min' | '$max' | '$push' | '$addToSet' | '$size';
export type Accumulator = PartialRecord<Accumulators, Filter>;
export class FilterBuilder<Options> {
constructor(private filters: { [K in keyof Options]: (v: Options[K]) => Filter }) {}
from = (q: Partial<Options> = {}): Filter =>
stages.match.match(
meta(q)
.entries()
.reduce((acc, [key, value]) => ({ ...acc, ...ifDefined(this.filters[key as keyof Options], f => f(value as Options[keyof Options])) }), {})
);
}
type Sort = Record<string, typeof asc | typeof desc>;
export class SortBuilder {
constructor(private sorts: Record<string, Sort>) {}
get keys(): string[] {
return Object.keys(this.sorts);
}
from = (
s: {
s?: string;
} = {},
alt?: string
): Optional<Filter> => stages.sort.sort(this.sorts[s?.s ?? ''] ?? this.sorts[alt ?? '']);
}
export class IncludeBuilder {
constructor(private includes: Record<string, (string | Record<string, 1>)[]>) {}
get keys(): string[] {
return Object.keys(this.includes);
}
from = (
i: {
i?: string;
} = {},
alt?: string
): Optional<Filter> => stages.project.include(...(this.includes[i?.i ?? ''] ?? this.includes[alt ?? ''] ?? []));
}
const escapeRegex = (s: string) => s.replace(/[|\\{}()[\]^$+*?.]/g, '\\$&').replace(/-/g, '\\x2d');
export const stages = {
root: '$$ROOT',
current: '$$CURRENT',
id: '_id',
decode: {
object: (f: Filter) => use(Object.entries(f)[0], ([k, v]) => ofGet(v, k)),
fields: (f: Filter) => Object.entries(f).reduce((res, [k, v]) => on(res, r => ifDefined(ofGet(v, k), nv => (r[k] = nv))), {} as any),
fieldsArrays: (f: Filter) => Object.entries(f).reduce((res, [k, v]) => on(res, r => (r[k] = use(toArray(v), vs => vs.map(v => ofGet(v, k))))), {} as any),
id: (f: Filter | string) => (isString(f) ? `$${asString(f)}` : isPrimitive(f) ? f : Object.entries(f).map(([k, v]) => ofGet(v, k))[0]),
},
match: {
match: (f: Record<string, Get<Optional<Filter>, string>>) => ({ $match: stages.decode.fields(f) }),
filter: <Options>(filters: { [K in keyof Options]: (v: Options[K]) => Filter }) => new FilterBuilder<Options>(filters),
or: (...filters: Filter[]) => ({ $or: toArray(filters).map(f => stages.decode.object(f)) }),
gt: (value: Filter) => ({ $gt: value }),
gte: (value: Filter) => ({ $gte: value }),
lt: (value: Filter) => ({ $lt: value }),
lte: (value: Filter) => ({ $lte: value }),
isIn: (value: OneOrMore<unknown>, separator = ',') => ({ $in: isString(value) ? value.split(separator) : value }),
notIn: (value: OneOrMore<unknown>, separator = ',') => ({ $nin: isString(value) ? value.split(separator) : value }),
after: (date: unknown) => stages.match.gte(toMongoType(date)),
before: (date: unknown) => stages.match.lt(toMongoType(date)),
anywhere: (q: string) => ({ $regex: escapeRegex(q), $options: 'i' }),
money: (currency: Currency, value: Filter) => (key: string) => ({
[`${key}.currency`]: currency.id,
...stages.decode.fields({ [`${key}.value`]: value }),
}),
},
sort: {
sort: ($sort: Sort) => (isPresent($sort) ? { $sort } : undefined),
sorter: (sorts: Record<string, Sort>) => new SortBuilder(sorts),
asc: (key: string) => stages.sort.sort({ [key]: asc }),
desc: (key: string) => stages.sort.sort({ [key]: desc }),
},
group: {
group: (fields: Record<string, Accumulator>) => ({
by: (by: Filter) => ({ $group: Object.assign({ _id: stages.decode.id(by) }, stages.decode.fields(fields)) }),
}),
date:
(format = '%Y-%m-%d') =>
(key: string) => ({ $dateToString: { date: `$${key}`, format } }),
count: (): Accumulator => ({ $count: {} }),
sum: (from?: string): Accumulator => (isDefined(from) ? { $sum: `$${from}` } : { $sum: 1 }),
avg: (from?: string) => ({ $avg: `$${from}` }),
multiply: (...multiply: string[]) => ({ $multiply: multiply.map(m => `$${m}`) }),
first: (from?: string): Accumulator => ({ $first: `$${from}` }),
last: (from?: string): Accumulator => ({ $last: `$${from}` }),
min: (from?: string): Accumulator => ({ $min: `$${from}` }),
max: (from?: string): Accumulator => ({ $max: `$${from}` }),
addToSet: (from?: string): Accumulator => ({ $addToSet: `$${from}` }),
push: (from = '$ROOT'): Accumulator => ({ $push: `$${from}` }),
size: (from?: string): Accumulator => ({ $size: `$${from}` }),
},
search: {
search: (f: Record<string, Get<Filter, string>>) => ifDefined(stages.decode.id(f), $search => ({ $search })),
auto: (value?: Id) => (key: string) => ifDefined(value, v => ({ autocomplete: { path: key, query: [v] } })),
fuzzy:
(value?: string, maxEdits = 1) =>
(key?: string) =>
ifDefined(value, v => ({
text: {
query: v,
path: key === 'wildcard' ? { wildcard: '*' } : key,
fuzzy: { maxEdits },
},
})),
},
set: {
set: (f: Record<string, Get<Filter, string>>) => ({ $set: stages.decode.fields(f) }),
score: () => ({ $meta: 'searchScore' }),
},
skip: {
skip: (o: FindOptions = {}): Optional<Filter> => ifDefined(o.skip, { $skip: asNumber(o.skip) }),
take: (o: FindOptions = {}): Optional<Filter> => ifDefined(o.take, { $limit: asNumber(o.take) }),
},
project: {
include: (...includes: (string | Record<string, 1 | string>)[]): Optional<Filter> =>
ifNotEmpty(includes, es => ({ $project: es.reduce((a: Filter, b: Filter) => ({ ...a, ...(isString(b) ? { [b]: 1 } : b) }), {}) })),
exclude: (...excludes: (string | Record<string, 0>)[]): Optional<Filter> =>
ifNotEmpty(excludes, es => ({ $project: es.reduce((a: Filter, b: Filter) => ({ ...a, ...(isString(b) ? { [b]: 0 } : b) }), {}) })),
includes: (includes: Record<string, (string | Record<string, 1>)[]>) => new IncludeBuilder(includes),
project: (project?: Filter) => ifDefined(project, $project => ({ $project })),
date: (key: string, format?: string) => ({ $toDate: `$${key}`, ...ifDefined(format, { format }) }),
duration: (from: string, to: string) => ({ $divide: [{ $subtract: [stages.project.date(from), stages.project.date(to)] }, 1000] }),
},
replaceWith: {
replaceWith: (f?: Filter): Optional<Filter> => ifDefined(f, { $replaceWith: f }),
merge: (...objects: Filter[]): Optional<Filter> => ifNotEmpty(objects, os => ({ $mergeObjects: os })),
rootAnd: (...objects: Filter[]): Optional<Filter> => stages.replaceWith.merge(stages.root, ...objects),
currentAnd: (...objects: Filter[]): Optional<Filter> => stages.replaceWith.merge(stages.current, ...objects),
reroot: (prop: string): Filter => ({ $replaceRoot: { newRoot: `$${prop}` } }),
concat: (...props: string[]): Optional<Filter> => ifNotEmpty(props, ps => ({ $concatArrays: ps.map(p => `$${p}`) })),
},
facet: {
facet: (f: Record<string, OneOrMore<Get<Optional<Filter>, string>>>) => ({ $facet: stages.decode.fieldsArrays(f) }),
unwind: (from?: string) => (f?: string) => ({ $unwind: `$${from ?? f}` }),
count: (from?: string) => (f?: string) => ({ $sortByCount: `$${from ?? f}` }),
data: () => [],
},
unwind: {
unwind: (prop?: string) => ({ $unwind: `$${prop}` }),
},
};