-
Notifications
You must be signed in to change notification settings - Fork 140
/
comparison.builder.ts
127 lines (118 loc) · 4.23 KB
/
comparison.builder.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
import { CommonFieldComparisonBetweenType, FilterComparisonOperators } from '@nestjs-query/core';
import escapeRegExp from 'lodash.escaperegexp';
import { Model as MongooseModel, FilterQuery, Document, Types, Schema } from 'mongoose';
import { QuerySelector } from 'mongodb';
import { BadRequestException } from '@nestjs/common';
import { getSchemaKey } from './helpers';
/**
* @internal
*/
export type EntityComparisonField<Entity, F extends keyof Entity> =
| Entity[F]
| Entity[F][]
| CommonFieldComparisonBetweenType<Entity[F]>
| true
| false
| null;
/**
* @internal
* Builder to create SQL Comparisons. (=, !=, \>, etc...)
*/
export class ComparisonBuilder<Entity extends Document> {
static DEFAULT_COMPARISON_MAP: Record<string, string> = {
eq: '$eq',
neq: '$ne',
gt: '$gt',
gte: '$gte',
lt: '$lt',
lte: '$lte',
in: '$in',
notin: '$nin',
is: '$eq',
isnot: '$ne',
};
constructor(
readonly Model: MongooseModel<Entity>,
readonly comparisonMap: Record<string, string> = ComparisonBuilder.DEFAULT_COMPARISON_MAP,
) {}
/**
* Creates a valid SQL fragment with parameters.
*
* @param field - the property in Entity to create the comparison for.
* @param cmp - the FilterComparisonOperator (eq, neq, gt, etc...)
* @param val - the value to compare to.
*/
build<F extends keyof Entity>(
field: F,
cmp: FilterComparisonOperators<Entity[F]>,
val: EntityComparisonField<Entity, F>,
): FilterQuery<Entity> {
const schemaKey = getSchemaKey(`${String(field)}`);
const normalizedCmp = (cmp as string).toLowerCase();
let querySelector: QuerySelector<Entity[F]> | undefined;
if (this.comparisonMap[normalizedCmp]) {
// comparison operator (e.b. =, !=, >, <)
querySelector = { [this.comparisonMap[normalizedCmp]]: this.convertQueryValue(field, val as Entity[F]) };
}
if (normalizedCmp.includes('like')) {
querySelector = (this.likeComparison(normalizedCmp, val) as unknown) as QuerySelector<Entity[F]>;
}
if (normalizedCmp.includes('between')) {
querySelector = this.betweenComparison(normalizedCmp, field, val);
}
if (!querySelector) {
throw new BadRequestException(`unknown operator ${JSON.stringify(cmp)}`);
}
return { [schemaKey]: querySelector } as FilterQuery<Entity>;
}
private betweenComparison<F extends keyof Entity>(
cmp: string,
field: F,
val: EntityComparisonField<Entity, F>,
): QuerySelector<Entity[F]> {
if (!this.isBetweenVal(val)) {
throw new Error(`Invalid value for ${cmp} expected {lower: val, upper: val} got ${JSON.stringify(val)}`);
}
if (cmp === 'notbetween') {
return { $lt: this.convertQueryValue(field, val.lower), $gt: this.convertQueryValue(field, val.upper) };
}
return { $gte: this.convertQueryValue(field, val.lower), $lte: this.convertQueryValue(field, val.upper) };
}
private isBetweenVal<F extends keyof Entity>(
val: EntityComparisonField<Entity, F>,
): val is CommonFieldComparisonBetweenType<Entity[F]> {
return val !== null && typeof val === 'object' && 'lower' in val && 'upper' in val;
}
private likeComparison<F extends keyof Entity>(
cmp: string,
val: EntityComparisonField<Entity, F>,
): QuerySelector<string> {
const regExpStr = escapeRegExp(`${String(val)}`).replace(/%/g, '.*');
const regExp = new RegExp(regExpStr, cmp.includes('ilike') ? 'i' : undefined);
if (cmp.startsWith('not')) {
return { $not: { $regex: regExp } };
}
return { $regex: regExp };
}
private convertQueryValue<F extends keyof Entity>(field: F, val: Entity[F]): Entity[F] {
const schemaType = this.Model.schema.path(getSchemaKey(field as string));
if (!schemaType) {
throw new BadRequestException(`unknown comparison field ${String(field)}`);
}
if (schemaType instanceof Schema.Types.ObjectId) {
return this.convertToObjectId(val) as Entity[F];
}
return val;
}
private convertToObjectId(val: unknown): unknown {
if (Array.isArray(val)) {
return val.map((v) => this.convertToObjectId(v));
}
if (typeof val === 'string' || typeof val === 'number') {
if (Types.ObjectId.isValid(val)) {
return Types.ObjectId(val);
}
}
return val;
}
}