@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.
Files changed (125) hide show
  1. package/dist/index.d.ts +1 -2
  2. package/dist/index.d.ts.map +1 -1
  3. package/dist/index.js +9 -10
  4. package/dist/index.js.map +1 -1
  5. package/dist/src/SQLJsonExpressions.d.ts +2 -0
  6. package/dist/src/SQLJsonExpressions.d.ts.map +1 -1
  7. package/dist/src/SQLJsonExpressions.js +8 -0
  8. package/dist/src/SQLJsonExpressions.js.map +1 -1
  9. package/dist/src/SQLSelect.d.ts.map +1 -1
  10. package/dist/src/SQLSelect.js +7 -0
  11. package/dist/src/SQLSelect.js.map +1 -1
  12. package/dist/src/SQLWhere.d.ts +6 -0
  13. package/dist/src/SQLWhere.d.ts.map +1 -1
  14. package/dist/src/SQLWhere.js +39 -1
  15. package/dist/src/SQLWhere.js.map +1 -1
  16. package/dist/src/filters/SQLFilter.d.ts +61 -25
  17. package/dist/src/filters/SQLFilter.d.ts.map +1 -1
  18. package/dist/src/filters/SQLFilter.js +183 -380
  19. package/dist/src/filters/SQLFilter.js.map +1 -1
  20. package/dist/src/filters/{modern/compilers → compilers}/contains.d.ts +1 -1
  21. package/dist/src/filters/compilers/contains.d.ts.map +1 -0
  22. package/dist/src/filters/{modern/compilers → compilers}/contains.js +6 -6
  23. package/dist/src/filters/compilers/contains.js.map +1 -0
  24. package/dist/src/filters/{modern/compilers → compilers}/equals.d.ts +1 -1
  25. package/dist/src/filters/compilers/equals.d.ts.map +1 -0
  26. package/dist/src/filters/{modern/compilers → compilers}/equals.js +7 -7
  27. package/dist/src/filters/compilers/equals.js.map +1 -0
  28. package/dist/src/filters/{modern/compilers → compilers}/greater.d.ts +1 -1
  29. package/dist/src/filters/compilers/greater.d.ts.map +1 -0
  30. package/dist/src/filters/{modern/compilers → compilers}/greater.js +4 -4
  31. package/dist/src/filters/compilers/greater.js.map +1 -0
  32. package/dist/src/filters/{modern/compilers → compilers}/in.d.ts +1 -1
  33. package/dist/src/filters/compilers/in.d.ts.map +1 -0
  34. package/dist/src/filters/{modern/compilers → compilers}/in.js +7 -7
  35. package/dist/src/filters/compilers/in.js.map +1 -0
  36. package/dist/src/filters/compilers/index.d.ts.map +1 -0
  37. package/dist/src/filters/compilers/index.js.map +1 -0
  38. package/dist/src/filters/{modern/compilers → compilers}/less.d.ts +1 -1
  39. package/dist/src/filters/compilers/less.d.ts.map +1 -0
  40. package/dist/src/filters/{modern/compilers → compilers}/less.js +4 -4
  41. package/dist/src/filters/compilers/less.js.map +1 -0
  42. package/dist/src/filters/helpers/isJSONColumn.d.ts +4 -0
  43. package/dist/src/filters/helpers/isJSONColumn.d.ts.map +1 -0
  44. package/dist/src/filters/helpers/isJSONColumn.js +16 -0
  45. package/dist/src/filters/helpers/isJSONColumn.js.map +1 -0
  46. package/dist/src/filters/{modern/helpers → helpers}/normalizeCompareValue.d.ts +2 -2
  47. package/dist/src/filters/helpers/normalizeCompareValue.d.ts.map +1 -0
  48. package/dist/src/filters/{modern/helpers → helpers}/normalizeCompareValue.js +18 -13
  49. package/dist/src/filters/helpers/normalizeCompareValue.js.map +1 -0
  50. package/dist/tests/filters/$and.test.js +49 -18
  51. package/dist/tests/filters/$and.test.js.map +1 -1
  52. package/dist/tests/filters/$contains.test.js +20 -20
  53. package/dist/tests/filters/$contains.test.js.map +1 -1
  54. package/dist/tests/filters/$eq.test.js +59 -53
  55. package/dist/tests/filters/$eq.test.js.map +1 -1
  56. package/dist/tests/filters/$gt.test.js +18 -18
  57. package/dist/tests/filters/$gt.test.js.map +1 -1
  58. package/dist/tests/filters/$gte.test.js +14 -14
  59. package/dist/tests/filters/$gte.test.js.map +1 -1
  60. package/dist/tests/filters/$in.test.js +24 -24
  61. package/dist/tests/filters/$in.test.js.map +1 -1
  62. package/dist/tests/filters/$lt.test.js +14 -14
  63. package/dist/tests/filters/$lt.test.js.map +1 -1
  64. package/dist/tests/filters/$lte.test.js +14 -14
  65. package/dist/tests/filters/$lte.test.js.map +1 -1
  66. package/dist/tests/filters/$neq.test.js +3 -3
  67. package/dist/tests/filters/$neq.test.js.map +1 -1
  68. package/dist/tests/filters/$not.test.js +5 -5
  69. package/dist/tests/filters/$not.test.js.map +1 -1
  70. package/dist/tests/filters/$or.test.js +16 -16
  71. package/dist/tests/filters/$or.test.js.map +1 -1
  72. package/dist/tests/filters/dot-syntax.test.js +10 -10
  73. package/dist/tests/filters/dot-syntax.test.js.map +1 -1
  74. package/dist/tests/filters/exists.test.js +16 -16
  75. package/dist/tests/filters/exists.test.js.map +1 -1
  76. package/dist/tests/filters/joined-relations.test.js +31 -31
  77. package/dist/tests/filters/joined-relations.test.js.map +1 -1
  78. package/dist/tests/filters/special-cases.test.js +11 -11
  79. package/dist/tests/filters/special-cases.test.js.map +1 -1
  80. package/dist/tests/filters/wildcard.test.js +8 -8
  81. package/dist/tests/filters/wildcard.test.js.map +1 -1
  82. package/dist/tests/utils/index.d.ts +7 -7
  83. package/dist/tests/utils/index.d.ts.map +1 -1
  84. package/dist/tests/utils/index.js +6 -6
  85. package/dist/tests/utils/index.js.map +1 -1
  86. package/dist/tsconfig.tsbuildinfo +1 -1
  87. package/package.json +2 -2
  88. package/src/SQLJsonExpressions.ts +10 -0
  89. package/src/SQLSelect.ts +9 -0
  90. package/src/SQLWhere.ts +48 -1
  91. package/src/filters/SQLFilter.ts +203 -485
  92. package/src/filters/{modern/compilers → compilers}/contains.ts +5 -5
  93. package/src/filters/{modern/compilers → compilers}/equals.ts +6 -6
  94. package/src/filters/{modern/compilers → compilers}/greater.ts +3 -3
  95. package/src/filters/{modern/compilers → compilers}/in.ts +6 -6
  96. package/src/filters/{modern/compilers → compilers}/less.ts +3 -3
  97. package/src/filters/helpers/isJSONColumn.ts +13 -0
  98. package/src/filters/{modern/helpers → helpers}/normalizeCompareValue.ts +20 -14
  99. package/dist/src/filters/modern/SQLModernFilter.d.ts +0 -73
  100. package/dist/src/filters/modern/SQLModernFilter.d.ts.map +0 -1
  101. package/dist/src/filters/modern/SQLModernFilter.js +0 -200
  102. package/dist/src/filters/modern/SQLModernFilter.js.map +0 -1
  103. package/dist/src/filters/modern/compilers/contains.d.ts.map +0 -1
  104. package/dist/src/filters/modern/compilers/contains.js.map +0 -1
  105. package/dist/src/filters/modern/compilers/equals.d.ts.map +0 -1
  106. package/dist/src/filters/modern/compilers/equals.js.map +0 -1
  107. package/dist/src/filters/modern/compilers/greater.d.ts.map +0 -1
  108. package/dist/src/filters/modern/compilers/greater.js.map +0 -1
  109. package/dist/src/filters/modern/compilers/in.d.ts.map +0 -1
  110. package/dist/src/filters/modern/compilers/in.js.map +0 -1
  111. package/dist/src/filters/modern/compilers/index.d.ts.map +0 -1
  112. package/dist/src/filters/modern/compilers/index.js.map +0 -1
  113. package/dist/src/filters/modern/compilers/less.d.ts.map +0 -1
  114. package/dist/src/filters/modern/compilers/less.js.map +0 -1
  115. package/dist/src/filters/modern/helpers/isJSONColumn.d.ts +0 -4
  116. package/dist/src/filters/modern/helpers/isJSONColumn.d.ts.map +0 -1
  117. package/dist/src/filters/modern/helpers/isJSONColumn.js +0 -16
  118. package/dist/src/filters/modern/helpers/isJSONColumn.js.map +0 -1
  119. package/dist/src/filters/modern/helpers/normalizeCompareValue.d.ts.map +0 -1
  120. package/dist/src/filters/modern/helpers/normalizeCompareValue.js.map +0 -1
  121. package/src/filters/modern/SQLModernFilter.ts +0 -256
  122. package/src/filters/modern/helpers/isJSONColumn.ts +0 -13
  123. /package/dist/src/filters/{modern/compilers → compilers}/index.d.ts +0 -0
  124. /package/dist/src/filters/{modern/compilers → compilers}/index.js +0 -0
  125. /package/src/filters/{modern/compilers → compilers}/index.ts +0 -0
@@ -1,551 +1,269 @@
1
1
  import { SimpleError } from '@simonbackx/simple-errors';
2
- import { StamhoofdCompareValue, StamhoofdFilter } from '@stamhoofd/structures';
3
- import { SQL } from '../SQL';
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 SQLFilterCompiler = (filter: StamhoofdFilter, filters: SQLFilterDefinitions) => Promise<SQLWhere | null> | SQLWhere | null;
12
- export type SQLFilterDefinitions = Record<string, SQLFilterCompiler>;
13
-
14
- export async function andSQLFilterCompiler(filter: StamhoofdFilter, filters: SQLFilterDefinitions): Promise<SQLWhere> {
15
- const runners = await compileSQLFilter(filter, filters);
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
- if (val === null) {
87
- return null;
88
- }
17
+ export enum SQLValueType {
18
+ /** At the root of a select */
19
+ Table = 'Table',
89
20
 
90
- if (typeof val === 'object' && '$' in val) {
91
- const specialValue = val['$'];
21
+ /** Column with type string */
22
+ String = 'String',
92
23
 
93
- switch (specialValue) {
94
- case '$now':
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
- return val;
102
- }
27
+ /** Column with type number */
28
+ Number = 'Number',
103
29
 
104
- /**
105
- * WARNING: only use this on one-to-one relations. Using it on one-to-many relations will result in duplicate results.
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
- const w = await compileToSQLFilter(filter, definitions);
118
- return new SQLWhereJoin(join, w);
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
- throw new SimpleError({
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
- export function createSQLOneToOneRelationFilterCompiler(baseSelect: InstanceType<typeof SQLSelect> & SQLExpression, definitions: SQLFilterDefinitions): SQLFilterCompiler {
141
- return async (filter: StamhoofdFilter) => {
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
- // Already joined, but creates a namespace
149
- export function createSQLFilterNamespace(definitions: SQLFilterDefinitions): SQLFilterCompiler {
150
- return (filter: StamhoofdFilter) => {
151
- return andSQLFilterCompiler(filter, definitions);
152
- };
42
+ /** {...} */
43
+ JSONObject = 'JSONObject',
153
44
  }
154
45
 
155
- export enum SQLValueType {
156
- JSONBoolean = 'JSONBoolean',
157
- JSONString = 'JSONString',
158
- }
46
+ export type SQLCurrentColumn = {
47
+ expression: SQLExpression;
159
48
 
160
- type SQLExpressionFilterOptions = {
161
- normalizeValue?: (v: SQLScalarValue | null) => SQLScalarValue | null;
162
- isJSONValue?: boolean;
163
- isJSONObject?: boolean;
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?: SQLValueType;
63
+ type: SQLValueType;
170
64
  checkPermission?: () => Promise<void>;
171
65
  };
172
66
 
173
- export function createSQLExpressionFilterCompiler(sqlExpression: SQLExpression, options: SQLExpressionFilterOptions = {}): SQLFilterCompiler {
174
- const { isJSONObject = false, isJSONValue = false, nullable = false } = options;
175
- let normalizeValue = options.normalizeValue;
176
- normalizeValue = normalizeValue ?? (v => v);
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
- switch (type) {
190
- case SQLValueType.JSONBoolean: {
191
- return expression;
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
- sqlExpression = castJsonType(sqlExpression, options.type);
200
- convertToExpression = (q: SQLScalarValue | null) => {
201
- const base = scalarToSQLJSONExpression(q);
202
- return castJsonType(base, options.type);
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
- const v = f.$in.map(a => norm(a));
266
- const nullIncluded = v.includes(null);
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
- const createSqlArray = (value: SQLScalarValue[]): SQLArray => {
289
- if (isJSONValue) {
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 new SQLWhereEqual(sqlExpression, SQLWhereSign.Equal, createSqlArray(v as SQLScalarValue[]));
317
- }
97
+ return await runner({
98
+ nullable: false,
99
+ ...column,
100
+ });
101
+ };
102
+ };
318
103
 
319
- if ('$neq' in f) {
320
- if (isJSONObject) {
321
- const v = norm(f.$neq);
104
+ return (filter: StamhoofdFilter) => {
105
+ return $andSQLFilterCompiler(filter, wildcardCompiler);
106
+ };
107
+ }
322
108
 
323
- return new SQLWhereNot(
324
- new SQLJsonContains(
325
- sqlExpression,
326
- convertToExpression(JSON.stringify(v)),
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
- if ('$gt' in f) {
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
- if ('$gte' in f) {
348
- if (isJSONObject) {
349
- throw new Error('Greater than is not supported in this place');
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
- if (f.$gte === null) {
353
- // >= null is always everything
354
- return new SQLWhereEqual(new SQLSafeValue(1), SQLWhereSign.Equal, new SQLSafeValue(1));
355
- }
356
- return new SQLWhereEqual(sqlExpression, SQLWhereSign.GreaterEqual, convertToExpression(norm(f.$gte)));
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
- if ('$lte' in f) {
360
- if (isJSONObject) {
361
- throw new Error('Greater than is not supported in this place');
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
- if (f.$lte === null) {
365
- // <= null is same as equal to null
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
- const base = new SQLWhereEqual(sqlExpression, SQLWhereSign.LessEqual, convertToExpression(norm(f.$lte)));
155
+ return async (column: SQLCurrentColumn) => {
156
+ const wheres = (await Promise.all(
157
+ runners.map(runner => (runner(column))),
158
+ ));
370
159
 
371
- if (nullable) {
372
- return new SQLWhereOr([
373
- // Null values are also smaller than any value - required for sorting
374
- new SQLWhereEqual(sqlExpression, SQLWhereSign.Equal, new SQLNull()),
375
- base,
376
- ]);
377
- }
160
+ return new SQLWhereAnd(wheres);
161
+ };
162
+ }
378
163
 
379
- return new SQLWhereEqual(sqlExpression, SQLWhereSign.LessEqual, convertToExpression(norm(f.$lte)));
380
- }
164
+ export function $orSQLFilterCompiler(filter: StamhoofdFilter, filters: SQLFilterCompiler): SQLFilterRunner {
165
+ const runners = compileSQLFilter(filter, filters);
381
166
 
382
- if ('$lt' in f) {
383
- if (isJSONObject) {
384
- throw new Error('Less than is not supported in this place');
385
- }
167
+ return async (column: SQLCurrentColumn) => {
168
+ const wheres = (await Promise.all(
169
+ runners.map(runner => (runner(column))),
170
+ ));
386
171
 
387
- if (f.$lt === null) {
388
- // < null is always nothing, there is nothing smaller than null in MySQL - to be consistent with order by behaviour
389
- return new SQLWhereEqual(new SQLSafeValue(1), SQLWhereSign.Equal, new SQLSafeValue(0));
390
- }
172
+ return new SQLWhereOr(wheres);
173
+ };
174
+ }
391
175
 
392
- const base = new SQLWhereEqual(sqlExpression, SQLWhereSign.Less, convertToExpression(norm(f.$lt)));
176
+ export function $notSQLFilterCompiler(filter: StamhoofdFilter, filters: SQLFilterCompiler): SQLFilterRunner {
177
+ const andRunner = $andSQLFilterCompiler(filter, filters);
393
178
 
394
- if (nullable) {
395
- return new SQLWhereOr([
396
- // Null values are also smaller than any value - required for sorting
397
- new SQLWhereEqual(sqlExpression, SQLWhereSign.Equal, new SQLNull()),
398
- base,
399
- ]);
400
- }
179
+ return async (column: SQLCurrentColumn) => {
180
+ return new SQLWhereNot(await andRunner(column));
181
+ };
182
+ }
401
183
 
402
- return base;
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
- if ('$contains' in f) {
406
- const needle = norm(f.$contains);
193
+ export const baseSQLFilterCompilers: SQLFilterDefinitions = {
194
+ $and: $andSQLFilterCompiler,
195
+ $or: $orSQLFilterCompiler,
196
+ $not: $notSQLFilterCompiler,
197
+ $eq: $equalsSQLFilterCompiler,
198
+ $neq: invertFilterCompiler($equalsSQLFilterCompiler),
407
199
 
408
- if (typeof needle !== 'string') {
409
- throw new Error('Invalid needle for contains filter');
410
- }
200
+ $lt: $lessThanSQLFilterCompiler,
201
+ $gt: $greaterThanSQLFilterCompiler,
202
+ $lte: invertFilterCompiler($greaterThanSQLFilterCompiler),
203
+ $gte: invertFilterCompiler($lessThanSQLFilterCompiler),
411
204
 
412
- if (isJSONObject) {
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
- if (isJSONValue) {
427
- // We need to do case insensitive search, so need to convert the sqlExpression from utf8mb4 to varchar
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
- return new SQLWhereLike(
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: 'Invalid filter',
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 createSQLColumnFilterCompiler(name: string | SQLColumnExpression, options?: SQLExpressionFilterOptions): SQLFilterCompiler {
453
- const column = name instanceof SQLColumnExpression ? name : SQL.column(name);
454
- return createSQLExpressionFilterCompiler(column, options);
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 const baseSQLFilterCompilers: SQLFilterDefinitions = {
458
- $and: andSQLFilterCompiler,
459
- $or: orSQLFilterCompiler,
460
- $not: notSQLFilterCompiler,
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
- function objectToArray(f: StamhoofdFilter & object): StamhoofdFilter[] {
464
- const splitted: StamhoofdFilter[] = [];
465
- for (const key of Object.keys(f)) {
466
- splitted.push({ [key]: f[key] });
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
- async function compileSQLFilter(filter: StamhoofdFilter, definitions: SQLFilterDefinitions): Promise<SQLWhere[]> {
472
- if (filter === undefined) {
473
- return [];
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
- const runners: SQLWhere[] = [];
477
-
478
- for (const f of (Array.isArray(filter) ? filter : (typeof filter === 'object' && filter !== null ? objectToArray(filter) : [filter]))) {
479
- if (!f) {
480
- continue;
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;