@stamhoofd/sql 2.89.2 → 2.90.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +1 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +9 -10
- package/dist/index.js.map +1 -1
- package/dist/src/SQLJsonExpressions.d.ts +2 -0
- package/dist/src/SQLJsonExpressions.d.ts.map +1 -1
- package/dist/src/SQLJsonExpressions.js +8 -0
- package/dist/src/SQLJsonExpressions.js.map +1 -1
- package/dist/src/SQLSelect.d.ts.map +1 -1
- package/dist/src/SQLSelect.js +7 -0
- package/dist/src/SQLSelect.js.map +1 -1
- package/dist/src/SQLWhere.d.ts +6 -0
- package/dist/src/SQLWhere.d.ts.map +1 -1
- package/dist/src/SQLWhere.js +39 -1
- package/dist/src/SQLWhere.js.map +1 -1
- package/dist/src/filters/SQLFilter.d.ts +61 -25
- package/dist/src/filters/SQLFilter.d.ts.map +1 -1
- package/dist/src/filters/SQLFilter.js +183 -380
- package/dist/src/filters/SQLFilter.js.map +1 -1
- package/dist/src/filters/{modern/compilers → compilers}/contains.d.ts +1 -1
- package/dist/src/filters/compilers/contains.d.ts.map +1 -0
- package/dist/src/filters/{modern/compilers → compilers}/contains.js +6 -6
- package/dist/src/filters/compilers/contains.js.map +1 -0
- package/dist/src/filters/{modern/compilers → compilers}/equals.d.ts +1 -1
- package/dist/src/filters/compilers/equals.d.ts.map +1 -0
- package/dist/src/filters/{modern/compilers → compilers}/equals.js +7 -7
- package/dist/src/filters/compilers/equals.js.map +1 -0
- package/dist/src/filters/{modern/compilers → compilers}/greater.d.ts +1 -1
- package/dist/src/filters/compilers/greater.d.ts.map +1 -0
- package/dist/src/filters/{modern/compilers → compilers}/greater.js +4 -4
- package/dist/src/filters/compilers/greater.js.map +1 -0
- package/dist/src/filters/{modern/compilers → compilers}/in.d.ts +1 -1
- package/dist/src/filters/compilers/in.d.ts.map +1 -0
- package/dist/src/filters/{modern/compilers → compilers}/in.js +7 -7
- package/dist/src/filters/compilers/in.js.map +1 -0
- package/dist/src/filters/compilers/index.d.ts.map +1 -0
- package/dist/src/filters/compilers/index.js.map +1 -0
- package/dist/src/filters/{modern/compilers → compilers}/less.d.ts +1 -1
- package/dist/src/filters/compilers/less.d.ts.map +1 -0
- package/dist/src/filters/{modern/compilers → compilers}/less.js +4 -4
- package/dist/src/filters/compilers/less.js.map +1 -0
- package/dist/src/filters/helpers/isJSONColumn.d.ts +4 -0
- package/dist/src/filters/helpers/isJSONColumn.d.ts.map +1 -0
- package/dist/src/filters/helpers/isJSONColumn.js +16 -0
- package/dist/src/filters/helpers/isJSONColumn.js.map +1 -0
- package/dist/src/filters/{modern/helpers → helpers}/normalizeCompareValue.d.ts +2 -2
- package/dist/src/filters/helpers/normalizeCompareValue.d.ts.map +1 -0
- package/dist/src/filters/{modern/helpers → helpers}/normalizeCompareValue.js +18 -13
- package/dist/src/filters/helpers/normalizeCompareValue.js.map +1 -0
- package/dist/tests/filters/$and.test.js +49 -18
- package/dist/tests/filters/$and.test.js.map +1 -1
- package/dist/tests/filters/$contains.test.js +20 -20
- package/dist/tests/filters/$contains.test.js.map +1 -1
- package/dist/tests/filters/$eq.test.js +59 -53
- package/dist/tests/filters/$eq.test.js.map +1 -1
- package/dist/tests/filters/$gt.test.js +18 -18
- package/dist/tests/filters/$gt.test.js.map +1 -1
- package/dist/tests/filters/$gte.test.js +14 -14
- package/dist/tests/filters/$gte.test.js.map +1 -1
- package/dist/tests/filters/$in.test.js +24 -24
- package/dist/tests/filters/$in.test.js.map +1 -1
- package/dist/tests/filters/$lt.test.js +14 -14
- package/dist/tests/filters/$lt.test.js.map +1 -1
- package/dist/tests/filters/$lte.test.js +14 -14
- package/dist/tests/filters/$lte.test.js.map +1 -1
- package/dist/tests/filters/$neq.test.js +3 -3
- package/dist/tests/filters/$neq.test.js.map +1 -1
- package/dist/tests/filters/$not.test.js +5 -5
- package/dist/tests/filters/$not.test.js.map +1 -1
- package/dist/tests/filters/$or.test.js +16 -16
- package/dist/tests/filters/$or.test.js.map +1 -1
- package/dist/tests/filters/dot-syntax.test.js +10 -10
- package/dist/tests/filters/dot-syntax.test.js.map +1 -1
- package/dist/tests/filters/exists.test.js +16 -16
- package/dist/tests/filters/exists.test.js.map +1 -1
- package/dist/tests/filters/joined-relations.test.js +31 -31
- package/dist/tests/filters/joined-relations.test.js.map +1 -1
- package/dist/tests/filters/special-cases.test.js +11 -11
- package/dist/tests/filters/special-cases.test.js.map +1 -1
- package/dist/tests/filters/wildcard.test.js +8 -8
- package/dist/tests/filters/wildcard.test.js.map +1 -1
- package/dist/tests/utils/index.d.ts +7 -7
- package/dist/tests/utils/index.d.ts.map +1 -1
- package/dist/tests/utils/index.js +6 -6
- package/dist/tests/utils/index.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +2 -2
- package/src/SQLJsonExpressions.ts +10 -0
- package/src/SQLSelect.ts +9 -0
- package/src/SQLWhere.ts +48 -1
- package/src/filters/SQLFilter.ts +203 -485
- package/src/filters/{modern/compilers → compilers}/contains.ts +5 -5
- package/src/filters/{modern/compilers → compilers}/equals.ts +6 -6
- package/src/filters/{modern/compilers → compilers}/greater.ts +3 -3
- package/src/filters/{modern/compilers → compilers}/in.ts +6 -6
- package/src/filters/{modern/compilers → compilers}/less.ts +3 -3
- package/src/filters/helpers/isJSONColumn.ts +13 -0
- package/src/filters/{modern/helpers → helpers}/normalizeCompareValue.ts +20 -14
- package/dist/src/filters/modern/SQLModernFilter.d.ts +0 -73
- package/dist/src/filters/modern/SQLModernFilter.d.ts.map +0 -1
- package/dist/src/filters/modern/SQLModernFilter.js +0 -200
- package/dist/src/filters/modern/SQLModernFilter.js.map +0 -1
- package/dist/src/filters/modern/compilers/contains.d.ts.map +0 -1
- package/dist/src/filters/modern/compilers/contains.js.map +0 -1
- package/dist/src/filters/modern/compilers/equals.d.ts.map +0 -1
- package/dist/src/filters/modern/compilers/equals.js.map +0 -1
- package/dist/src/filters/modern/compilers/greater.d.ts.map +0 -1
- package/dist/src/filters/modern/compilers/greater.js.map +0 -1
- package/dist/src/filters/modern/compilers/in.d.ts.map +0 -1
- package/dist/src/filters/modern/compilers/in.js.map +0 -1
- package/dist/src/filters/modern/compilers/index.d.ts.map +0 -1
- package/dist/src/filters/modern/compilers/index.js.map +0 -1
- package/dist/src/filters/modern/compilers/less.d.ts.map +0 -1
- package/dist/src/filters/modern/compilers/less.js.map +0 -1
- package/dist/src/filters/modern/helpers/isJSONColumn.d.ts +0 -4
- package/dist/src/filters/modern/helpers/isJSONColumn.d.ts.map +0 -1
- package/dist/src/filters/modern/helpers/isJSONColumn.js +0 -16
- package/dist/src/filters/modern/helpers/isJSONColumn.js.map +0 -1
- package/dist/src/filters/modern/helpers/normalizeCompareValue.d.ts.map +0 -1
- package/dist/src/filters/modern/helpers/normalizeCompareValue.js.map +0 -1
- package/src/filters/modern/SQLModernFilter.ts +0 -256
- package/src/filters/modern/helpers/isJSONColumn.ts +0 -13
- /package/dist/src/filters/{modern/compilers → compilers}/index.d.ts +0 -0
- /package/dist/src/filters/{modern/compilers → compilers}/index.js +0 -0
- /package/src/filters/{modern/compilers → compilers}/index.ts +0 -0
package/src/filters/SQLFilter.ts
CHANGED
|
@@ -1,551 +1,269 @@
|
|
|
1
1
|
import { SimpleError } from '@simonbackx/simple-errors';
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import { SQLExpression } from '../SQLExpression';
|
|
5
|
-
import { SQLArray, SQLCast, SQLColumnExpression, SQLLower, SQLNull, SQLSafeValue, SQLScalarValue, scalarToSQLExpression } from '../SQLExpressions';
|
|
6
|
-
import { SQLJsonContains, SQLJsonOverlaps, SQLJsonSearch, SQLJsonUnquote, scalarToSQLJSONExpression } from '../SQLJsonExpressions';
|
|
7
|
-
import { SQLSelect } from '../SQLSelect';
|
|
8
|
-
import { SQLWhere, SQLWhereAnd, SQLWhereEqual, SQLWhereExists, SQLWhereJoin, SQLWhereLike, SQLWhereNot, SQLWhereOr, SQLWhereSign } from '../SQLWhere';
|
|
2
|
+
import { compileFilter, FilterCompiler, FilterDefinitions, filterDefinitionsToCompiler, RequiredFilterCompiler, StamhoofdFilter } from '@stamhoofd/structures';
|
|
3
|
+
import { SQLExpression, SQLExpressionOptions, SQLQuery } from '../SQLExpression';
|
|
9
4
|
import { SQLJoin } from '../SQLJoin';
|
|
5
|
+
import { SQLJsonValue } from '../SQLJsonExpressions';
|
|
6
|
+
import { SQLSelect } from '../SQLSelect';
|
|
7
|
+
import { SQLWhere, SQLWhereAnd, SQLWhereExists, SQLWhereJoin, SQLWhereNot, SQLWhereOr } from '../SQLWhere';
|
|
8
|
+
import { $equalsSQLFilterCompiler, $greaterThanSQLFilterCompiler, $inSQLFilterCompiler, $lessThanSQLFilterCompiler } from './compilers';
|
|
9
|
+
import { $containsSQLFilterCompiler } from './compilers/contains';
|
|
10
10
|
|
|
11
|
-
export type
|
|
12
|
-
export type
|
|
13
|
-
|
|
14
|
-
export
|
|
15
|
-
|
|
16
|
-
return new SQLWhereAnd(runners);
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
export async function orSQLFilterCompiler(filter: StamhoofdFilter, filters: SQLFilterDefinitions): Promise<SQLWhere> {
|
|
20
|
-
const runners = await compileSQLFilter(filter, filters);
|
|
21
|
-
return new SQLWhereOr(runners);
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
export async function notSQLFilterCompiler(filter: StamhoofdFilter, filters: SQLFilterDefinitions): Promise<SQLWhere> {
|
|
25
|
-
const andRunner = await andSQLFilterCompiler(filter, filters);
|
|
26
|
-
return new SQLWhereNot(andRunner);
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
function guardFilterCompareValue(val: any): StamhoofdCompareValue {
|
|
30
|
-
if (val instanceof Date) {
|
|
31
|
-
return val;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
if (typeof val === 'string') {
|
|
35
|
-
return val;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
if (typeof val === 'number') {
|
|
39
|
-
return val;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
if (typeof val === 'boolean') {
|
|
43
|
-
return val;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
if (val === null) {
|
|
47
|
-
return null;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
if (typeof val === 'object' && '$' in val) {
|
|
51
|
-
if (val['$'] === '$now') {
|
|
52
|
-
return val;
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
throw new Error('Invalid compare value. Expected a string, number, boolean, date or null.');
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
function doNormalizeValue(val: StamhoofdCompareValue, options?: SQLExpressionFilterOptions): string | number | Date | null | boolean {
|
|
60
|
-
if (val instanceof Date) {
|
|
61
|
-
return val;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
if (typeof val === 'string') {
|
|
65
|
-
if (options?.isJSONObject) {
|
|
66
|
-
return val;
|
|
67
|
-
}
|
|
68
|
-
return val.toLocaleLowerCase();
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
if (typeof val === 'boolean') {
|
|
72
|
-
if (options?.type === SQLValueType.JSONBoolean) {
|
|
73
|
-
return val;
|
|
74
|
-
}
|
|
75
|
-
return val === true ? 1 : 0;
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
if (typeof val === 'number') {
|
|
79
|
-
if (options?.type === SQLValueType.JSONBoolean) {
|
|
80
|
-
return val === 1 ? true : false;
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
return val;
|
|
84
|
-
}
|
|
11
|
+
export type SQLSyncFilterRunner = (column: SQLCurrentColumn) => SQLWhere;
|
|
12
|
+
export type SQLFilterRunner = (column: SQLCurrentColumn) => Promise<SQLWhere> | SQLWhere;
|
|
13
|
+
export type SQLFilterCompiler = FilterCompiler<SQLFilterRunner>;
|
|
14
|
+
export type SQLRequiredFilterCompiler = RequiredFilterCompiler<SQLFilterRunner>;
|
|
15
|
+
export type SQLFilterDefinitions = FilterDefinitions<SQLFilterRunner>;
|
|
85
16
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
17
|
+
export enum SQLValueType {
|
|
18
|
+
/** At the root of a select */
|
|
19
|
+
Table = 'Table',
|
|
89
20
|
|
|
90
|
-
|
|
91
|
-
|
|
21
|
+
/** Column with type string */
|
|
22
|
+
String = 'String',
|
|
92
23
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
return doNormalizeValue(new Date());
|
|
96
|
-
default:
|
|
97
|
-
throw new Error('Unsupported magic value ' + specialValue);
|
|
98
|
-
}
|
|
99
|
-
}
|
|
24
|
+
/** MySQL Datetime */
|
|
25
|
+
Datetime = 'Datetime',
|
|
100
26
|
|
|
101
|
-
|
|
102
|
-
|
|
27
|
+
/** Column with type number */
|
|
28
|
+
Number = 'Number',
|
|
103
29
|
|
|
104
|
-
/**
|
|
105
|
-
|
|
106
|
-
*/
|
|
107
|
-
export function createSQLJoinedRelationFilterCompiler(join: SQLJoin, definitions: SQLFilterDefinitions): SQLFilterCompiler {
|
|
108
|
-
return async (filter: StamhoofdFilter) => {
|
|
109
|
-
const f = filter as any;
|
|
110
|
-
|
|
111
|
-
if ('$elemMatch' in f) {
|
|
112
|
-
// $elemMatch is also supported but not required (since this is a one-to-one relation)
|
|
113
|
-
const w = await compileToSQLFilter(f['$elemMatch'] as StamhoofdFilter, definitions);
|
|
114
|
-
return new SQLWhereJoin(join, w);
|
|
115
|
-
}
|
|
30
|
+
/** Column with type boolean, meaning 1 or 0 */
|
|
31
|
+
Boolean = 'Boolean',
|
|
116
32
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
export function createSQLRelationFilterCompiler(baseSelect: InstanceType<typeof SQLSelect> & SQLExpression, definitions: SQLFilterDefinitions): SQLFilterCompiler {
|
|
123
|
-
return async (filter: StamhoofdFilter) => {
|
|
124
|
-
const f = filter as any;
|
|
125
|
-
|
|
126
|
-
if ('$elemMatch' in f) {
|
|
127
|
-
const w = await compileToSQLFilter(f['$elemMatch'], definitions);
|
|
128
|
-
const q = baseSelect.clone().where(w);
|
|
129
|
-
return new SQLWhereExists(q);
|
|
130
|
-
}
|
|
33
|
+
/** True or false in JSON */
|
|
34
|
+
JSONBoolean = 'JSONBoolean',
|
|
35
|
+
JSONString = 'JSONString',
|
|
131
36
|
|
|
132
|
-
|
|
133
|
-
code: 'invalid_filter',
|
|
134
|
-
message: 'Invalid filter',
|
|
135
|
-
human: $t('a5c30846-b8ae-410d-8fcd-bfc3f127623d'),
|
|
136
|
-
});
|
|
137
|
-
};
|
|
138
|
-
}
|
|
37
|
+
JSONNumber = 'JSONNumber',
|
|
139
38
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
const w = await compileToSQLFilter(filter, definitions);
|
|
143
|
-
const q = baseSelect.clone().where(w);
|
|
144
|
-
return new SQLWhereExists(q);
|
|
145
|
-
};
|
|
146
|
-
}
|
|
39
|
+
/** [...] */
|
|
40
|
+
JSONArray = 'JSONArray',
|
|
147
41
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
return (filter: StamhoofdFilter) => {
|
|
151
|
-
return andSQLFilterCompiler(filter, definitions);
|
|
152
|
-
};
|
|
42
|
+
/** {...} */
|
|
43
|
+
JSONObject = 'JSONObject',
|
|
153
44
|
}
|
|
154
45
|
|
|
155
|
-
export
|
|
156
|
-
|
|
157
|
-
JSONString = 'JSONString',
|
|
158
|
-
}
|
|
46
|
+
export type SQLCurrentColumn = {
|
|
47
|
+
expression: SQLExpression;
|
|
159
48
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
49
|
+
/**
|
|
50
|
+
* MySQL nullable. Please fill this in correctly! If a value can be null, can not exist (=mysql null), or can be JSONNull, set this to true.
|
|
51
|
+
*
|
|
52
|
+
* Mainly > and < operators will make sure the behaviour is consistent with MySQL sorting (normally comparing with null will always return false in MySQL)
|
|
53
|
+
*/
|
|
164
54
|
nullable?: boolean;
|
|
165
55
|
|
|
56
|
+
/**
|
|
57
|
+
* JSON nullable
|
|
58
|
+
*/
|
|
59
|
+
|
|
166
60
|
/**
|
|
167
61
|
* Type of this column, use to normalize values received from filters
|
|
168
62
|
*/
|
|
169
|
-
type
|
|
63
|
+
type: SQLValueType;
|
|
170
64
|
checkPermission?: () => Promise<void>;
|
|
171
65
|
};
|
|
172
66
|
|
|
173
|
-
export function
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
const norm = (val: any) => {
|
|
178
|
-
const n = doNormalizeValue(guardFilterCompareValue(val), options);
|
|
179
|
-
return normalizeValue(n);
|
|
180
|
-
};
|
|
181
|
-
let convertToExpression = scalarToSQLExpression;
|
|
182
|
-
|
|
183
|
-
if (isJSONValue) {
|
|
184
|
-
const castJsonType = (expression: SQLExpression, type: SQLValueType | undefined): SQLExpression => {
|
|
185
|
-
if (type === undefined) {
|
|
186
|
-
return expression;
|
|
187
|
-
}
|
|
67
|
+
export function createColumnFilter(column: SQLCurrentColumn, childDefinitions?: SQLFilterDefinitions): SQLFilterCompiler {
|
|
68
|
+
return (filter: StamhoofdFilter) => {
|
|
69
|
+
const compiler = childDefinitions ? filterDefinitionsToCompiler(childDefinitions) : filterDefinitionsToCompiler(baseSQLFilterCompilers);
|
|
70
|
+
const runner = $andSQLFilterCompiler(filter, compiler);
|
|
188
71
|
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
}
|
|
193
|
-
case SQLValueType.JSONString: {
|
|
194
|
-
return new SQLCast(new SQLJsonUnquote(expression), 'CHAR');
|
|
195
|
-
}
|
|
72
|
+
return async (_: SQLCurrentColumn) => {
|
|
73
|
+
if (column.checkPermission) {
|
|
74
|
+
await column.checkPermission();
|
|
196
75
|
}
|
|
76
|
+
return await runner({
|
|
77
|
+
nullable: false,
|
|
78
|
+
...column,
|
|
79
|
+
});
|
|
197
80
|
};
|
|
81
|
+
};
|
|
82
|
+
}
|
|
198
83
|
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
};
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
return async (filter: StamhoofdFilter, filters: SQLFilterDefinitions) => {
|
|
207
|
-
if (options.checkPermission) {
|
|
208
|
-
// throws error if no permissions
|
|
209
|
-
await options.checkPermission();
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
if (typeof filter === 'string' || typeof filter === 'number' || typeof filter === 'boolean' || filter === null || filter === undefined || filter instanceof Date) {
|
|
213
|
-
filter = {
|
|
214
|
-
$eq: filter,
|
|
215
|
-
};
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
if (Array.isArray(filter)) {
|
|
219
|
-
throw new Error('Unexpected array in filter');
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
const f = filter;
|
|
223
|
-
|
|
224
|
-
if ('$eq' in f) {
|
|
225
|
-
if (isJSONObject) {
|
|
226
|
-
const v = norm(f.$eq);
|
|
227
|
-
|
|
228
|
-
if (typeof v === 'string') {
|
|
229
|
-
// Custom query to support case insensitive comparing
|
|
230
|
-
|
|
231
|
-
return new SQLWhereEqual(
|
|
232
|
-
new SQLJsonSearch(
|
|
233
|
-
new SQLLower(sqlExpression),
|
|
234
|
-
'one',
|
|
235
|
-
convertToExpression(
|
|
236
|
-
SQLWhereLike.escape(v.toLocaleLowerCase()),
|
|
237
|
-
),
|
|
238
|
-
),
|
|
239
|
-
SQLWhereSign.NotEqual,
|
|
240
|
-
new SQLNull(),
|
|
241
|
-
);
|
|
242
|
-
}
|
|
243
|
-
// else
|
|
244
|
-
return new SQLJsonContains(
|
|
245
|
-
sqlExpression,
|
|
246
|
-
convertToExpression(v),
|
|
247
|
-
);
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
return new SQLWhereEqual(sqlExpression, SQLWhereSign.Equal, convertToExpression(norm(f.$eq)));
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
if ('$in' in f) {
|
|
254
|
-
if (!Array.isArray(f.$in)) {
|
|
255
|
-
throw new SimpleError({
|
|
256
|
-
code: 'invalid_filter',
|
|
257
|
-
message: 'Expected array at $in filter',
|
|
258
|
-
});
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
if (f.$in.length === 0) {
|
|
262
|
-
return new SQLWhereEqual(new SQLSafeValue(1), SQLWhereSign.Equal, new SQLSafeValue(0));
|
|
263
|
-
}
|
|
84
|
+
export function createWildcardColumnFilter(getColumn: (key: string) => SQLCurrentColumn, childDefinitions?: (key: string) => SQLFilterDefinitions, options?: { checkPermission?: (key: string) => Promise<void> }): SQLFilterCompiler {
|
|
85
|
+
const wildcardCompiler = (filter: StamhoofdFilter, _, key: string) => {
|
|
86
|
+
const compiler = childDefinitions ? filterDefinitionsToCompiler(childDefinitions(key)) : filterDefinitionsToCompiler(baseSQLFilterCompilers);
|
|
87
|
+
const runner = $andSQLFilterCompiler(filter, compiler);
|
|
264
88
|
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
if (isJSONObject) {
|
|
269
|
-
if (nullIncluded) {
|
|
270
|
-
// PROBLEM: The sql expression can either not exist (= resolve to mysql null), contains null in json (= JSON null), or contain a value.
|
|
271
|
-
// that makes comparing more difficult, to combat this, we still need to use SQLJsonOverlaps with the JSON null value
|
|
272
|
-
return new SQLWhereOr([
|
|
273
|
-
new SQLWhereEqual(sqlExpression, SQLWhereSign.Equal, new SQLNull()), // checks path not exists (= mysql null)
|
|
274
|
-
new SQLJsonOverlaps(
|
|
275
|
-
sqlExpression,
|
|
276
|
-
convertToExpression(JSON.stringify(v)), // contains json null
|
|
277
|
-
),
|
|
278
|
-
]);
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
// else
|
|
282
|
-
return new SQLJsonOverlaps(
|
|
283
|
-
sqlExpression,
|
|
284
|
-
convertToExpression(JSON.stringify(v)),
|
|
285
|
-
);
|
|
89
|
+
return async (_: SQLCurrentColumn) => {
|
|
90
|
+
if (options?.checkPermission) {
|
|
91
|
+
await options.checkPermission(key);
|
|
286
92
|
}
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
const type = options.type;
|
|
291
|
-
|
|
292
|
-
switch (type) {
|
|
293
|
-
case SQLValueType.JSONBoolean: {
|
|
294
|
-
// todo;
|
|
295
|
-
break;
|
|
296
|
-
}
|
|
297
|
-
case SQLValueType.JSONString: {
|
|
298
|
-
break;
|
|
299
|
-
}
|
|
300
|
-
}
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
return new SQLArray(value);
|
|
304
|
-
};
|
|
305
|
-
|
|
306
|
-
if (nullIncluded) {
|
|
307
|
-
const remaining = v.filter(v => v !== null);
|
|
308
|
-
if (remaining.length === 0) {
|
|
309
|
-
return new SQLWhereEqual(sqlExpression, SQLWhereSign.Equal, new SQLNull());
|
|
310
|
-
}
|
|
311
|
-
return new SQLWhereOr([
|
|
312
|
-
new SQLWhereEqual(sqlExpression, SQLWhereSign.Equal, new SQLNull()),
|
|
313
|
-
new SQLWhereEqual(sqlExpression, SQLWhereSign.Equal, createSqlArray(remaining)),
|
|
314
|
-
]);
|
|
93
|
+
const column = getColumn(key);
|
|
94
|
+
if (column.checkPermission) {
|
|
95
|
+
await column.checkPermission();
|
|
315
96
|
}
|
|
316
|
-
return
|
|
317
|
-
|
|
97
|
+
return await runner({
|
|
98
|
+
nullable: false,
|
|
99
|
+
...column,
|
|
100
|
+
});
|
|
101
|
+
};
|
|
102
|
+
};
|
|
318
103
|
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
104
|
+
return (filter: StamhoofdFilter) => {
|
|
105
|
+
return $andSQLFilterCompiler(filter, wildcardCompiler);
|
|
106
|
+
};
|
|
107
|
+
}
|
|
322
108
|
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
return new SQLWhereEqual(sqlExpression, SQLWhereSign.NotEqual, convertToExpression(norm(f.$neq)));
|
|
109
|
+
/**
|
|
110
|
+
* Filter with a subquery that should return at least one result.
|
|
111
|
+
*/
|
|
112
|
+
export function createExistsFilter(baseSelect: InstanceType<typeof SQLSelect> & SQLExpression, definitions: SQLFilterDefinitions): SQLFilterCompiler {
|
|
113
|
+
return (filter: StamhoofdFilter, _: SQLFilterCompiler) => {
|
|
114
|
+
if (filter !== null && typeof filter === 'object' && '$elemMatch' in filter) {
|
|
115
|
+
filter = filter['$elemMatch'] as StamhoofdFilter;
|
|
331
116
|
}
|
|
332
117
|
|
|
333
|
-
|
|
334
|
-
if (isJSONObject) {
|
|
335
|
-
throw new Error('Greater than is not supported in this place');
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
if (f.$gt === null) {
|
|
339
|
-
// > null is same as not equal to null (everything is larger than null in mysql) - to be consistent with order by behaviour
|
|
340
|
-
return new SQLWhereEqual(sqlExpression, SQLWhereSign.NotEqual, convertToExpression(null));
|
|
341
|
-
}
|
|
342
|
-
|
|
343
|
-
// For MySQL null values are never included in greater than, but we need this for consistent sorting behaviour
|
|
344
|
-
return new SQLWhereEqual(sqlExpression, SQLWhereSign.Greater, convertToExpression(norm(f.$gt)));
|
|
345
|
-
}
|
|
118
|
+
const runner = compileToSQLRunner(filter, definitions);
|
|
346
119
|
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
120
|
+
return async (_: SQLCurrentColumn) => {
|
|
121
|
+
const w = await runner({
|
|
122
|
+
expression: SQLRootExpression,
|
|
123
|
+
type: SQLValueType.Table,
|
|
124
|
+
nullable: false,
|
|
125
|
+
});
|
|
126
|
+
const q = baseSelect.clone().andWhere(w);
|
|
127
|
+
return new SQLWhereExists(q);
|
|
128
|
+
};
|
|
129
|
+
};
|
|
130
|
+
}
|
|
351
131
|
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
132
|
+
/**
|
|
133
|
+
* WARNING: only use this on one-to-one relations. Using it on one-to-many relations will result in duplicate results.
|
|
134
|
+
*
|
|
135
|
+
* By default doesRelationAlwaysExist is set to true, this means we expect the relation to always exist. This helps optimize the query (dropping the join if the where clause in the join is always true)
|
|
136
|
+
*/
|
|
137
|
+
export function createJoinedRelationFilter(join: SQLJoin, definitions: SQLFilterDefinitions, options: { doesRelationAlwaysExist: boolean } = { doesRelationAlwaysExist: true }): SQLFilterCompiler {
|
|
138
|
+
return (filter: StamhoofdFilter, _: SQLFilterCompiler) => {
|
|
139
|
+
if (filter !== null && typeof filter === 'object' && '$elemMatch' in filter) {
|
|
140
|
+
filter = filter['$elemMatch'] as StamhoofdFilter;
|
|
357
141
|
}
|
|
358
142
|
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
143
|
+
return async (_: SQLCurrentColumn) => {
|
|
144
|
+
const w = await compileToSQLFilter(filter, definitions);
|
|
145
|
+
return new SQLWhereJoin(join, w, {
|
|
146
|
+
doesRelationAlwaysExist: options.doesRelationAlwaysExist,
|
|
147
|
+
});
|
|
148
|
+
};
|
|
149
|
+
};
|
|
150
|
+
}
|
|
363
151
|
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
return new SQLWhereEqual(sqlExpression, SQLWhereSign.Equal, convertToExpression(norm(f.$lte)));
|
|
367
|
-
}
|
|
152
|
+
export function $andSQLFilterCompiler(filter: StamhoofdFilter, filters: SQLFilterCompiler): SQLFilterRunner {
|
|
153
|
+
const runners = compileSQLFilter(filter, filters);
|
|
368
154
|
|
|
369
|
-
|
|
155
|
+
return async (column: SQLCurrentColumn) => {
|
|
156
|
+
const wheres = (await Promise.all(
|
|
157
|
+
runners.map(runner => (runner(column))),
|
|
158
|
+
));
|
|
370
159
|
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
new SQLWhereEqual(sqlExpression, SQLWhereSign.Equal, new SQLNull()),
|
|
375
|
-
base,
|
|
376
|
-
]);
|
|
377
|
-
}
|
|
160
|
+
return new SQLWhereAnd(wheres);
|
|
161
|
+
};
|
|
162
|
+
}
|
|
378
163
|
|
|
379
|
-
|
|
380
|
-
|
|
164
|
+
export function $orSQLFilterCompiler(filter: StamhoofdFilter, filters: SQLFilterCompiler): SQLFilterRunner {
|
|
165
|
+
const runners = compileSQLFilter(filter, filters);
|
|
381
166
|
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
167
|
+
return async (column: SQLCurrentColumn) => {
|
|
168
|
+
const wheres = (await Promise.all(
|
|
169
|
+
runners.map(runner => (runner(column))),
|
|
170
|
+
));
|
|
386
171
|
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
}
|
|
172
|
+
return new SQLWhereOr(wheres);
|
|
173
|
+
};
|
|
174
|
+
}
|
|
391
175
|
|
|
392
|
-
|
|
176
|
+
export function $notSQLFilterCompiler(filter: StamhoofdFilter, filters: SQLFilterCompiler): SQLFilterRunner {
|
|
177
|
+
const andRunner = $andSQLFilterCompiler(filter, filters);
|
|
393
178
|
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
base,
|
|
399
|
-
]);
|
|
400
|
-
}
|
|
179
|
+
return async (column: SQLCurrentColumn) => {
|
|
180
|
+
return new SQLWhereNot(await andRunner(column));
|
|
181
|
+
};
|
|
182
|
+
}
|
|
401
183
|
|
|
402
|
-
|
|
403
|
-
|
|
184
|
+
function invertFilterCompiler(compiler: SQLRequiredFilterCompiler): SQLRequiredFilterCompiler {
|
|
185
|
+
return (filter: StamhoofdFilter, parentCompiler: SQLFilterCompiler) => {
|
|
186
|
+
const runner = compiler(filter, parentCompiler);
|
|
187
|
+
return async (column) => {
|
|
188
|
+
return new SQLWhereNot(await runner(column));
|
|
189
|
+
};
|
|
190
|
+
};
|
|
191
|
+
}
|
|
404
192
|
|
|
405
|
-
|
|
406
|
-
|
|
193
|
+
export const baseSQLFilterCompilers: SQLFilterDefinitions = {
|
|
194
|
+
$and: $andSQLFilterCompiler,
|
|
195
|
+
$or: $orSQLFilterCompiler,
|
|
196
|
+
$not: $notSQLFilterCompiler,
|
|
197
|
+
$eq: $equalsSQLFilterCompiler,
|
|
198
|
+
$neq: invertFilterCompiler($equalsSQLFilterCompiler),
|
|
407
199
|
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
200
|
+
$lt: $lessThanSQLFilterCompiler,
|
|
201
|
+
$gt: $greaterThanSQLFilterCompiler,
|
|
202
|
+
$lte: invertFilterCompiler($greaterThanSQLFilterCompiler),
|
|
203
|
+
$gte: invertFilterCompiler($lessThanSQLFilterCompiler),
|
|
411
204
|
|
|
412
|
-
|
|
413
|
-
return new SQLWhereEqual(
|
|
414
|
-
new SQLJsonSearch(
|
|
415
|
-
new SQLLower(sqlExpression),
|
|
416
|
-
'one',
|
|
417
|
-
convertToExpression(
|
|
418
|
-
'%' + SQLWhereLike.escape(needle) + '%',
|
|
419
|
-
),
|
|
420
|
-
),
|
|
421
|
-
SQLWhereSign.NotEqual,
|
|
422
|
-
new SQLNull(),
|
|
423
|
-
);
|
|
424
|
-
}
|
|
205
|
+
$in: $inSQLFilterCompiler,
|
|
425
206
|
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
return new SQLWhereLike(
|
|
429
|
-
new SQLCast(new SQLJsonUnquote(sqlExpression), 'CHAR'),
|
|
430
|
-
convertToExpression(
|
|
431
|
-
'%' + SQLWhereLike.escape(needle) + '%',
|
|
432
|
-
),
|
|
433
|
-
);
|
|
434
|
-
}
|
|
207
|
+
$contains: $containsSQLFilterCompiler,
|
|
208
|
+
};
|
|
435
209
|
|
|
436
|
-
|
|
437
|
-
sqlExpression,
|
|
438
|
-
convertToExpression(
|
|
439
|
-
'%' + SQLWhereLike.escape(needle) + '%',
|
|
440
|
-
),
|
|
441
|
-
);
|
|
442
|
-
}
|
|
210
|
+
const compileSQLFilter = compileFilter<SQLFilterRunner>;
|
|
443
211
|
|
|
212
|
+
export const SQLRootExpression: SQLExpression = {
|
|
213
|
+
getSQL(options?: SQLExpressionOptions): SQLQuery {
|
|
444
214
|
throw new SimpleError({
|
|
445
215
|
code: 'invalid_filter',
|
|
446
|
-
message: '
|
|
447
|
-
human: $t('a5c30846-b8ae-410d-8fcd-bfc3f127623d'),
|
|
216
|
+
message: 'Root level filters are not allowed to use $eq or $neq',
|
|
448
217
|
});
|
|
449
|
-
}
|
|
450
|
-
}
|
|
218
|
+
},
|
|
219
|
+
};
|
|
451
220
|
|
|
452
|
-
export function
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
221
|
+
export function compileToSQLRunner(filter: StamhoofdFilter, definitions: SQLFilterDefinitions): SQLFilterRunner {
|
|
222
|
+
if (filter === null) {
|
|
223
|
+
return () => {
|
|
224
|
+
return new SQLWhereAnd([]); // No filter, return empty where
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
const compiler = filterDefinitionsToCompiler(definitions); // this compiler searches in the definition for the right compiler for the given key
|
|
228
|
+
const runner = $andSQLFilterCompiler(filter, compiler);
|
|
229
|
+
return runner;
|
|
230
|
+
};
|
|
456
231
|
|
|
457
|
-
export
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
232
|
+
export async function compileToSQLFilter(filter: StamhoofdFilter, filters: SQLFilterDefinitions): Promise<SQLWhere> {
|
|
233
|
+
const runner = compileToSQLRunner(filter, filters);
|
|
234
|
+
return await runner({
|
|
235
|
+
expression: SQLRootExpression,
|
|
236
|
+
type: SQLValueType.Table,
|
|
237
|
+
nullable: false,
|
|
238
|
+
});
|
|
461
239
|
};
|
|
462
240
|
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
241
|
+
/**
|
|
242
|
+
* Casts json strings, numbers and booleans to native MySQL types. This includes json null to mysql null.
|
|
243
|
+
*/
|
|
244
|
+
export function normalizeColumn(column: SQLCurrentColumn): SQLCurrentColumn {
|
|
245
|
+
if (column.type === SQLValueType.JSONString) {
|
|
246
|
+
return {
|
|
247
|
+
expression: new SQLJsonValue(column.expression, 'CHAR'),
|
|
248
|
+
type: SQLValueType.String,
|
|
249
|
+
nullable: column.nullable,
|
|
250
|
+
};
|
|
467
251
|
}
|
|
468
|
-
return splitted;
|
|
469
|
-
}
|
|
470
252
|
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
253
|
+
if (column.type === SQLValueType.JSONBoolean) {
|
|
254
|
+
return {
|
|
255
|
+
expression: new SQLJsonValue(column.expression, 'UNSIGNED'),
|
|
256
|
+
type: SQLValueType.Boolean,
|
|
257
|
+
nullable: column.nullable,
|
|
258
|
+
};
|
|
474
259
|
}
|
|
475
260
|
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
}
|
|
482
|
-
if (!(typeof f === 'object' && f !== null)) {
|
|
483
|
-
throw new Error('Unsupported filter at this position: ' + f);
|
|
484
|
-
}
|
|
485
|
-
|
|
486
|
-
if (Object.keys(f).length > 1) {
|
|
487
|
-
// Multiple keys in the same object should always be combined with AND
|
|
488
|
-
runners.push(await andSQLFilterCompiler(objectToArray(f), definitions));
|
|
489
|
-
continue;
|
|
490
|
-
}
|
|
491
|
-
|
|
492
|
-
if (Array.isArray(f)) {
|
|
493
|
-
// Arrays in filters not direclty underneath $and or $or should be combined with AND
|
|
494
|
-
runners.push(await andSQLFilterCompiler(f, definitions));
|
|
495
|
-
continue;
|
|
496
|
-
}
|
|
497
|
-
|
|
498
|
-
for (const key of Object.keys(f)) {
|
|
499
|
-
let ff = definitions[key];
|
|
500
|
-
let value: StamhoofdFilter = f[key];
|
|
501
|
-
|
|
502
|
-
if (!ff) {
|
|
503
|
-
// Search with dot syntax shortcuts
|
|
504
|
-
if (key.includes('.')) {
|
|
505
|
-
const parts = key.split('.');
|
|
506
|
-
|
|
507
|
-
if (parts.length >= 2 && parts.every(p => p.length > 0)) {
|
|
508
|
-
let subKey = parts.shift() ?? '';
|
|
509
|
-
|
|
510
|
-
while (parts.length && !definitions[subKey]) {
|
|
511
|
-
subKey = subKey + '.' + parts.shift();
|
|
512
|
-
}
|
|
513
|
-
|
|
514
|
-
if (subKey && definitions[subKey]) {
|
|
515
|
-
const remaining = parts.join('.');
|
|
516
|
-
|
|
517
|
-
const transformeInto = {
|
|
518
|
-
[remaining]: value,
|
|
519
|
-
};
|
|
520
|
-
|
|
521
|
-
ff = definitions[subKey];
|
|
522
|
-
value = transformeInto;
|
|
523
|
-
}
|
|
524
|
-
}
|
|
525
|
-
}
|
|
526
|
-
}
|
|
527
|
-
|
|
528
|
-
if (!ff) {
|
|
529
|
-
throw new SimpleError({
|
|
530
|
-
code: 'invalid_filter',
|
|
531
|
-
message: 'Invalid filter ' + key,
|
|
532
|
-
human: $t('a5c30846-b8ae-410d-8fcd-bfc3f127623d'),
|
|
533
|
-
});
|
|
534
|
-
}
|
|
535
|
-
|
|
536
|
-
const s = await ff(value, definitions);
|
|
537
|
-
if (s === undefined || s === null) {
|
|
538
|
-
throw new SimpleError({
|
|
539
|
-
code: 'invalid_filter',
|
|
540
|
-
message: 'Invalid filter value for filter ' + key,
|
|
541
|
-
human: $t('a5c30846-b8ae-410d-8fcd-bfc3f127623d'),
|
|
542
|
-
});
|
|
543
|
-
}
|
|
544
|
-
runners.push(s);
|
|
545
|
-
}
|
|
261
|
+
if (column.type === SQLValueType.JSONNumber) {
|
|
262
|
+
return {
|
|
263
|
+
expression: new SQLJsonValue(column.expression, 'UNSIGNED'),
|
|
264
|
+
type: SQLValueType.Number,
|
|
265
|
+
nullable: column.nullable,
|
|
266
|
+
};
|
|
546
267
|
}
|
|
547
|
-
|
|
548
|
-
return runners;
|
|
268
|
+
return column;
|
|
549
269
|
}
|
|
550
|
-
|
|
551
|
-
export const compileToSQLFilter = andSQLFilterCompiler;
|