@stamhoofd/sql 2.83.5 → 2.84.0
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 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +9 -0
- package/dist/index.js.map +1 -1
- package/dist/src/QueryableModel.d.ts +1 -0
- package/dist/src/QueryableModel.d.ts.map +1 -1
- package/dist/src/QueryableModel.js +20 -2
- package/dist/src/QueryableModel.js.map +1 -1
- package/dist/src/SQL.d.ts +9 -16
- package/dist/src/SQL.d.ts.map +1 -1
- package/dist/src/SQL.js +16 -13
- package/dist/src/SQL.js.map +1 -1
- package/dist/src/SQLDelete.d.ts +2 -2
- package/dist/src/SQLDelete.d.ts.map +1 -1
- package/dist/src/SQLDelete.js +13 -3
- package/dist/src/SQLDelete.js.map +1 -1
- package/dist/src/SQLExpression.d.ts +3 -0
- package/dist/src/SQLExpression.d.ts.map +1 -1
- package/dist/src/SQLExpression.js.map +1 -1
- package/dist/src/SQLExpressions.d.ts +17 -6
- package/dist/src/SQLExpressions.d.ts.map +1 -1
- package/dist/src/SQLExpressions.js +20 -12
- package/dist/src/SQLExpressions.js.map +1 -1
- package/dist/src/SQLJoin.d.ts +3 -3
- package/dist/src/SQLJoin.d.ts.map +1 -1
- package/dist/src/SQLJoin.js +5 -1
- package/dist/src/SQLJoin.js.map +1 -1
- package/dist/src/SQLJsonExpressions.d.ts +22 -0
- package/dist/src/SQLJsonExpressions.d.ts.map +1 -1
- package/dist/src/SQLJsonExpressions.js +56 -4
- package/dist/src/SQLJsonExpressions.js.map +1 -1
- package/dist/src/SQLSelect.d.ts +10 -4
- package/dist/src/SQLSelect.d.ts.map +1 -1
- package/dist/src/SQLSelect.js +25 -11
- package/dist/src/SQLSelect.js.map +1 -1
- package/dist/src/SQLWhere.d.ts +29 -1
- package/dist/src/SQLWhere.d.ts.map +1 -1
- package/dist/src/SQLWhere.js +181 -12
- package/dist/src/SQLWhere.js.map +1 -1
- package/dist/src/filters/SQLFilter.d.ts +1 -0
- package/dist/src/filters/SQLFilter.d.ts.map +1 -1
- package/dist/src/filters/SQLFilter.js +8 -0
- package/dist/src/filters/SQLFilter.js.map +1 -1
- package/dist/src/filters/modern/SQLModernFilter.d.ts +73 -0
- package/dist/src/filters/modern/SQLModernFilter.d.ts.map +1 -0
- package/dist/src/filters/modern/SQLModernFilter.js +200 -0
- package/dist/src/filters/modern/SQLModernFilter.js.map +1 -0
- package/dist/src/filters/modern/compilers/contains.d.ts +4 -0
- package/dist/src/filters/modern/compilers/contains.d.ts.map +1 -0
- package/dist/src/filters/modern/compilers/contains.js +28 -0
- package/dist/src/filters/modern/compilers/contains.js.map +1 -0
- package/dist/src/filters/modern/compilers/equals.d.ts +4 -0
- package/dist/src/filters/modern/compilers/equals.d.ts.map +1 -0
- package/dist/src/filters/modern/compilers/equals.js +46 -0
- package/dist/src/filters/modern/compilers/equals.js.map +1 -0
- package/dist/src/filters/modern/compilers/greater.d.ts +4 -0
- package/dist/src/filters/modern/compilers/greater.d.ts.map +1 -0
- package/dist/src/filters/modern/compilers/greater.js +17 -0
- package/dist/src/filters/modern/compilers/greater.js.map +1 -0
- package/dist/src/filters/modern/compilers/in.d.ts +4 -0
- package/dist/src/filters/modern/compilers/in.d.ts.map +1 -0
- package/dist/src/filters/modern/compilers/in.js +50 -0
- package/dist/src/filters/modern/compilers/in.js.map +1 -0
- package/dist/src/filters/modern/compilers/index.d.ts +5 -0
- package/dist/src/filters/modern/compilers/index.d.ts.map +1 -0
- package/dist/src/filters/modern/compilers/index.js +8 -0
- package/dist/src/filters/modern/compilers/index.js.map +1 -0
- package/dist/src/filters/modern/compilers/less.d.ts +4 -0
- package/dist/src/filters/modern/compilers/less.d.ts.map +1 -0
- package/dist/src/filters/modern/compilers/less.js +17 -0
- package/dist/src/filters/modern/compilers/less.js.map +1 -0
- package/dist/src/filters/modern/helpers/isJSONColumn.d.ts +4 -0
- package/dist/src/filters/modern/helpers/isJSONColumn.d.ts.map +1 -0
- package/dist/src/filters/modern/helpers/isJSONColumn.js +16 -0
- package/dist/src/filters/modern/helpers/isJSONColumn.js.map +1 -0
- package/dist/src/filters/modern/helpers/normalizeCompareValue.d.ts +9 -0
- package/dist/src/filters/modern/helpers/normalizeCompareValue.d.ts.map +1 -0
- package/dist/src/filters/modern/helpers/normalizeCompareValue.js +82 -0
- package/dist/src/filters/modern/helpers/normalizeCompareValue.js.map +1 -0
- package/dist/tests/filters/$and.test.d.ts +2 -0
- package/dist/tests/filters/$and.test.d.ts.map +1 -0
- package/dist/tests/filters/$and.test.js +185 -0
- package/dist/tests/filters/$and.test.js.map +1 -0
- package/dist/tests/filters/$contains.test.d.ts +2 -0
- package/dist/tests/filters/$contains.test.d.ts.map +1 -0
- package/dist/tests/filters/$contains.test.js +701 -0
- package/dist/tests/filters/$contains.test.js.map +1 -0
- package/dist/tests/filters/$eq.test.d.ts +2 -0
- package/dist/tests/filters/$eq.test.d.ts.map +1 -0
- package/dist/tests/filters/$eq.test.js +986 -0
- package/dist/tests/filters/$eq.test.js.map +1 -0
- package/dist/tests/filters/$gt.test.d.ts +2 -0
- package/dist/tests/filters/$gt.test.d.ts.map +1 -0
- package/dist/tests/filters/$gt.test.js +463 -0
- package/dist/tests/filters/$gt.test.js.map +1 -0
- package/dist/tests/filters/$gte.test.d.ts +2 -0
- package/dist/tests/filters/$gte.test.d.ts.map +1 -0
- package/dist/tests/filters/$gte.test.js +433 -0
- package/dist/tests/filters/$gte.test.js.map +1 -0
- package/dist/tests/filters/$in.test.d.ts +2 -0
- package/dist/tests/filters/$in.test.d.ts.map +1 -0
- package/dist/tests/filters/$in.test.js +590 -0
- package/dist/tests/filters/$in.test.js.map +1 -0
- package/dist/tests/filters/$lt.test.d.ts +2 -0
- package/dist/tests/filters/$lt.test.d.ts.map +1 -0
- package/dist/tests/filters/$lt.test.js +433 -0
- package/dist/tests/filters/$lt.test.js.map +1 -0
- package/dist/tests/filters/$lte.test.d.ts +2 -0
- package/dist/tests/filters/$lte.test.d.ts.map +1 -0
- package/dist/tests/filters/$lte.test.js +472 -0
- package/dist/tests/filters/$lte.test.js.map +1 -0
- package/dist/tests/filters/$neq.test.d.ts +2 -0
- package/dist/tests/filters/$neq.test.d.ts.map +1 -0
- package/dist/tests/filters/$neq.test.js +32 -0
- package/dist/tests/filters/$neq.test.js.map +1 -0
- package/dist/tests/filters/$not.test.d.ts +2 -0
- package/dist/tests/filters/$not.test.d.ts.map +1 -0
- package/dist/tests/filters/$not.test.js +50 -0
- package/dist/tests/filters/$not.test.js.map +1 -0
- package/dist/tests/filters/$or.test.d.ts +2 -0
- package/dist/tests/filters/$or.test.d.ts.map +1 -0
- package/dist/tests/filters/$or.test.js +185 -0
- package/dist/tests/filters/$or.test.js.map +1 -0
- package/dist/tests/filters/dot-syntax.test.d.ts +2 -0
- package/dist/tests/filters/dot-syntax.test.d.ts.map +1 -0
- package/dist/tests/filters/dot-syntax.test.js +210 -0
- package/dist/tests/filters/dot-syntax.test.js.map +1 -0
- package/dist/tests/filters/exists.test.d.ts +2 -0
- package/dist/tests/filters/exists.test.d.ts.map +1 -0
- package/dist/tests/filters/exists.test.js +106 -0
- package/dist/tests/filters/exists.test.js.map +1 -0
- package/dist/tests/filters/joined-relations.test.d.ts +2 -0
- package/dist/tests/filters/joined-relations.test.d.ts.map +1 -0
- package/dist/tests/filters/joined-relations.test.js +167 -0
- package/dist/tests/filters/joined-relations.test.js.map +1 -0
- package/dist/tests/filters/special-cases.test.d.ts +2 -0
- package/dist/tests/filters/special-cases.test.d.ts.map +1 -0
- package/dist/tests/filters/special-cases.test.js +114 -0
- package/dist/tests/filters/special-cases.test.js.map +1 -0
- package/dist/tests/filters/wildcard.test.d.ts +2 -0
- package/dist/tests/filters/wildcard.test.d.ts.map +1 -0
- package/dist/tests/filters/wildcard.test.js +67 -0
- package/dist/tests/filters/wildcard.test.js.map +1 -0
- package/dist/tests/jest.global.setup.d.ts +3 -0
- package/dist/tests/jest.global.setup.d.ts.map +1 -0
- package/dist/tests/jest.global.setup.js +7 -0
- package/dist/tests/jest.global.setup.js.map +1 -0
- package/dist/tests/jest.setup.d.ts +2 -0
- package/dist/tests/jest.setup.d.ts.map +1 -0
- package/dist/tests/jest.setup.js +5 -0
- package/dist/tests/jest.setup.js.map +1 -0
- package/dist/tests/utils/index.d.ts +57 -0
- package/dist/tests/utils/index.d.ts.map +1 -0
- package/dist/tests/utils/index.js +206 -0
- package/dist/tests/utils/index.js.map +1 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +4 -3
- package/src/QueryableModel.ts +22 -2
- package/src/SQL.ts +21 -30
- package/src/SQLDelete.ts +26 -15
- package/src/SQLExpression.ts +4 -0
- package/src/SQLExpressions.ts +23 -14
- package/src/SQLJoin.ts +8 -4
- package/src/SQLJsonExpressions.ts +65 -4
- package/src/SQLSelect.ts +31 -15
- package/src/SQLWhere.ts +208 -13
- package/src/filters/SQLFilter.ts +8 -0
- package/src/filters/modern/SQLModernFilter.ts +256 -0
- package/src/filters/modern/compilers/contains.ts +43 -0
- package/src/filters/modern/compilers/equals.ts +72 -0
- package/src/filters/modern/compilers/greater.ts +20 -0
- package/src/filters/modern/compilers/in.ts +62 -0
- package/src/filters/modern/compilers/index.ts +4 -0
- package/src/filters/modern/compilers/less.ts +19 -0
- package/src/filters/modern/helpers/isJSONColumn.ts +13 -0
- package/src/filters/modern/helpers/normalizeCompareValue.ts +95 -0
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
import { SimpleError } from '@simonbackx/simple-errors';
|
|
2
|
+
import { compileFilter, FilterCompiler, FilterDefinitions, filterDefinitionsToCompiler, RequiredFilterCompiler, StamhoofdFilter } from '@stamhoofd/structures';
|
|
3
|
+
import { SQLExpression, SQLExpressionOptions, SQLQuery } from '../../SQLExpression';
|
|
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
|
+
|
|
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 SQLModernFilterDefinitions = FilterDefinitions<SQLFilterRunner>;
|
|
16
|
+
|
|
17
|
+
export enum SQLModernValueType {
|
|
18
|
+
/** At the root of a select */
|
|
19
|
+
Table = 'Table',
|
|
20
|
+
|
|
21
|
+
/** Column with type string */
|
|
22
|
+
String = 'String',
|
|
23
|
+
|
|
24
|
+
/** MySQL Datetime */
|
|
25
|
+
Datetime = 'Datetime',
|
|
26
|
+
|
|
27
|
+
/** Column with type number */
|
|
28
|
+
Number = 'Number',
|
|
29
|
+
|
|
30
|
+
/** Column with type boolean, meaning 1 or 0 */
|
|
31
|
+
Boolean = 'Boolean',
|
|
32
|
+
|
|
33
|
+
/** True or false in JSON */
|
|
34
|
+
JSONBoolean = 'JSONBoolean',
|
|
35
|
+
JSONString = 'JSONString',
|
|
36
|
+
|
|
37
|
+
JSONNumber = 'JSONNumber',
|
|
38
|
+
|
|
39
|
+
/** [...] */
|
|
40
|
+
JSONArray = 'JSONArray',
|
|
41
|
+
|
|
42
|
+
/** {...} */
|
|
43
|
+
JSONObject = 'JSONObject',
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export type SQLCurrentColumn = {
|
|
47
|
+
expression: SQLExpression;
|
|
48
|
+
|
|
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
|
+
*/
|
|
54
|
+
nullable?: boolean;
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* JSON nullable
|
|
58
|
+
*/
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Type of this column, use to normalize values received from filters
|
|
62
|
+
*/
|
|
63
|
+
type: SQLModernValueType;
|
|
64
|
+
checkPermission?: () => Promise<void>;
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
export function createColumnFilter(column: SQLCurrentColumn, childDefinitions?: SQLModernFilterDefinitions): SQLFilterCompiler {
|
|
68
|
+
return (filter: StamhoofdFilter) => {
|
|
69
|
+
const compiler = childDefinitions ? filterDefinitionsToCompiler(childDefinitions) : filterDefinitionsToCompiler(baseModernSQLFilterCompilers);
|
|
70
|
+
const runner = $andSQLFilterCompiler(filter, compiler);
|
|
71
|
+
|
|
72
|
+
return (_: SQLCurrentColumn) => {
|
|
73
|
+
return runner({
|
|
74
|
+
nullable: false,
|
|
75
|
+
...column,
|
|
76
|
+
});
|
|
77
|
+
};
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function createWildcardColumnFilter(getColumn: (key: string) => SQLCurrentColumn, childDefinitions?: (key: string) => SQLModernFilterDefinitions): SQLFilterCompiler {
|
|
82
|
+
const wildcardCompiler = (filter: StamhoofdFilter, _, key: string) => {
|
|
83
|
+
const compiler = childDefinitions ? filterDefinitionsToCompiler(childDefinitions(key)) : filterDefinitionsToCompiler(baseModernSQLFilterCompilers);
|
|
84
|
+
const runner = $andSQLFilterCompiler(filter, compiler);
|
|
85
|
+
|
|
86
|
+
return (_: SQLCurrentColumn) => {
|
|
87
|
+
return runner({
|
|
88
|
+
nullable: false,
|
|
89
|
+
...getColumn(key),
|
|
90
|
+
});
|
|
91
|
+
};
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
return (filter: StamhoofdFilter) => {
|
|
95
|
+
return $andSQLFilterCompiler(filter, wildcardCompiler);
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Filter with a subquery that should return at least one result.
|
|
101
|
+
*/
|
|
102
|
+
export function createExistsFilter(baseSelect: InstanceType<typeof SQLSelect> & SQLExpression, definitions: SQLModernFilterDefinitions): SQLFilterCompiler {
|
|
103
|
+
return (filter: StamhoofdFilter, _: SQLFilterCompiler) => {
|
|
104
|
+
if (filter !== null && typeof filter === 'object' && '$elemMatch' in filter) {
|
|
105
|
+
filter = filter['$elemMatch'] as StamhoofdFilter;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const runner = compileToSQLRunner(filter, definitions);
|
|
109
|
+
|
|
110
|
+
return async (_: SQLCurrentColumn) => {
|
|
111
|
+
const w = await runner({
|
|
112
|
+
expression: SQLRootExpression,
|
|
113
|
+
type: SQLModernValueType.Table,
|
|
114
|
+
nullable: false,
|
|
115
|
+
});
|
|
116
|
+
const q = baseSelect.clone().andWhere(w);
|
|
117
|
+
return new SQLWhereExists(q);
|
|
118
|
+
};
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* WARNING: only use this on one-to-one relations. Using it on one-to-many relations will result in duplicate results.
|
|
124
|
+
*
|
|
125
|
+
* 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)
|
|
126
|
+
*/
|
|
127
|
+
export function createJoinedRelationFilter(join: SQLJoin, definitions: SQLModernFilterDefinitions, options: { doesRelationAlwaysExist: boolean } = { doesRelationAlwaysExist: true }): SQLFilterCompiler {
|
|
128
|
+
return (filter: StamhoofdFilter, _: SQLFilterCompiler) => {
|
|
129
|
+
if (filter !== null && typeof filter === 'object' && '$elemMatch' in filter) {
|
|
130
|
+
filter = filter['$elemMatch'] as StamhoofdFilter;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return async (_: SQLCurrentColumn) => {
|
|
134
|
+
const w = await compileToModernSQLFilter(filter, definitions);
|
|
135
|
+
return new SQLWhereJoin(join, w, {
|
|
136
|
+
doesRelationAlwaysExist: options.doesRelationAlwaysExist,
|
|
137
|
+
});
|
|
138
|
+
};
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export function $andSQLFilterCompiler(filter: StamhoofdFilter, filters: SQLFilterCompiler): SQLFilterRunner {
|
|
143
|
+
const runners = compileSQLFilter(filter, filters);
|
|
144
|
+
|
|
145
|
+
return async (column: SQLCurrentColumn) => {
|
|
146
|
+
const wheres = (await Promise.all(
|
|
147
|
+
runners.map(runner => (runner(column))),
|
|
148
|
+
));
|
|
149
|
+
|
|
150
|
+
return new SQLWhereAnd(wheres);
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export function $orSQLFilterCompiler(filter: StamhoofdFilter, filters: SQLFilterCompiler): SQLFilterRunner {
|
|
155
|
+
const runners = compileSQLFilter(filter, filters);
|
|
156
|
+
|
|
157
|
+
return async (column: SQLCurrentColumn) => {
|
|
158
|
+
const wheres = (await Promise.all(
|
|
159
|
+
runners.map(runner => (runner(column))),
|
|
160
|
+
));
|
|
161
|
+
|
|
162
|
+
return new SQLWhereOr(wheres);
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export function $notSQLFilterCompiler(filter: StamhoofdFilter, filters: SQLFilterCompiler): SQLFilterRunner {
|
|
167
|
+
const andRunner = $andSQLFilterCompiler(filter, filters);
|
|
168
|
+
|
|
169
|
+
return async (column: SQLCurrentColumn) => {
|
|
170
|
+
return new SQLWhereNot(await andRunner(column));
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function invertFilterCompiler(compiler: SQLRequiredFilterCompiler): SQLRequiredFilterCompiler {
|
|
175
|
+
return (filter: StamhoofdFilter, parentCompiler: SQLFilterCompiler) => {
|
|
176
|
+
const runner = compiler(filter, parentCompiler);
|
|
177
|
+
return async (column) => {
|
|
178
|
+
return new SQLWhereNot(await runner(column));
|
|
179
|
+
};
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
export const baseModernSQLFilterCompilers: SQLModernFilterDefinitions = {
|
|
184
|
+
$and: $andSQLFilterCompiler,
|
|
185
|
+
$or: $orSQLFilterCompiler,
|
|
186
|
+
$not: $notSQLFilterCompiler,
|
|
187
|
+
$eq: $equalsSQLFilterCompiler,
|
|
188
|
+
$neq: invertFilterCompiler($equalsSQLFilterCompiler),
|
|
189
|
+
|
|
190
|
+
$lt: $lessThanSQLFilterCompiler,
|
|
191
|
+
$gt: $greaterThanSQLFilterCompiler,
|
|
192
|
+
$lte: invertFilterCompiler($greaterThanSQLFilterCompiler),
|
|
193
|
+
$gte: invertFilterCompiler($lessThanSQLFilterCompiler),
|
|
194
|
+
|
|
195
|
+
$in: $inSQLFilterCompiler,
|
|
196
|
+
|
|
197
|
+
$contains: $containsSQLFilterCompiler,
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
const compileSQLFilter = compileFilter<SQLFilterRunner>;
|
|
201
|
+
|
|
202
|
+
export const SQLRootExpression: SQLExpression = {
|
|
203
|
+
getSQL(options?: SQLExpressionOptions): SQLQuery {
|
|
204
|
+
throw new SimpleError({
|
|
205
|
+
code: 'invalid_filter',
|
|
206
|
+
message: 'Root level filters are not allowed to use $eq or $neq',
|
|
207
|
+
});
|
|
208
|
+
},
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
export function compileToSQLRunner(filter: StamhoofdFilter, definitions: SQLModernFilterDefinitions): SQLFilterRunner {
|
|
212
|
+
if (filter === null) {
|
|
213
|
+
return () => {
|
|
214
|
+
return new SQLWhereAnd([]); // No filter, return empty where
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
const compiler = filterDefinitionsToCompiler(definitions); // this compiler searches in the definition for the right compiler for the given key
|
|
218
|
+
const runner = $andSQLFilterCompiler(filter, compiler);
|
|
219
|
+
return runner;
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
export async function compileToModernSQLFilter(filter: StamhoofdFilter, filters: SQLModernFilterDefinitions): Promise<SQLWhere> {
|
|
223
|
+
const runner = compileToSQLRunner(filter, filters);
|
|
224
|
+
return await runner({
|
|
225
|
+
expression: SQLRootExpression,
|
|
226
|
+
type: SQLModernValueType.Table,
|
|
227
|
+
nullable: false,
|
|
228
|
+
});
|
|
229
|
+
};
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Casts json strings, numbers and booleans to native MySQL types. This includes json null to mysql null.
|
|
233
|
+
*/
|
|
234
|
+
export function normalizeColumn(column: SQLCurrentColumn): SQLCurrentColumn {
|
|
235
|
+
if (column.type === SQLModernValueType.JSONString) {
|
|
236
|
+
return {
|
|
237
|
+
expression: new SQLJsonValue(column.expression, 'CHAR'),
|
|
238
|
+
type: SQLModernValueType.String,
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
if (column.type === SQLModernValueType.JSONBoolean) {
|
|
243
|
+
return {
|
|
244
|
+
expression: new SQLJsonValue(column.expression, 'UNSIGNED'),
|
|
245
|
+
type: SQLModernValueType.Boolean,
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
if (column.type === SQLModernValueType.JSONNumber) {
|
|
250
|
+
return {
|
|
251
|
+
expression: new SQLJsonValue(column.expression, 'UNSIGNED'),
|
|
252
|
+
type: SQLModernValueType.Number,
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
return column;
|
|
256
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { SimpleError } from '@simonbackx/simple-errors';
|
|
2
|
+
import { assertFilterCompareValue, StamhoofdFilter } from '@stamhoofd/structures';
|
|
3
|
+
import { scalarToSQLExpression, SQLLower, SQLNull } from '../../../SQLExpressions';
|
|
4
|
+
import { SQLJsonSearch } from '../../../SQLJsonExpressions';
|
|
5
|
+
import { SQLWhereEqual, SQLWhereLike, SQLWhereSign } from '../../../SQLWhere';
|
|
6
|
+
import { normalizeColumn, SQLCurrentColumn, SQLSyncFilterRunner, SQLModernValueType } from '../SQLModernFilter';
|
|
7
|
+
import { normalizeCompareValue } from '../helpers/normalizeCompareValue';
|
|
8
|
+
|
|
9
|
+
export function $containsSQLFilterCompiler(filter: StamhoofdFilter): SQLSyncFilterRunner {
|
|
10
|
+
return (originalColumn: SQLCurrentColumn) => {
|
|
11
|
+
const column = normalizeColumn(originalColumn);
|
|
12
|
+
const value = normalizeCompareValue(assertFilterCompareValue(filter), column.type);
|
|
13
|
+
|
|
14
|
+
if (typeof value !== 'string') {
|
|
15
|
+
throw new SimpleError({
|
|
16
|
+
code: 'invalid_filter',
|
|
17
|
+
message: 'Expected string at $contains filter',
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
if (column.type === SQLModernValueType.JSONArray || column.type === SQLModernValueType.JSONObject) {
|
|
22
|
+
// For JSON arrays and objects, we use JSON_CONTAINS
|
|
23
|
+
return new SQLWhereEqual(
|
|
24
|
+
new SQLJsonSearch(
|
|
25
|
+
new SQLLower(column.expression),
|
|
26
|
+
'one',
|
|
27
|
+
scalarToSQLExpression(
|
|
28
|
+
'%' + SQLWhereLike.escape(value) + '%',
|
|
29
|
+
),
|
|
30
|
+
),
|
|
31
|
+
SQLWhereSign.NotEqual,
|
|
32
|
+
new SQLNull(),
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return new SQLWhereLike(
|
|
37
|
+
column.expression,
|
|
38
|
+
scalarToSQLExpression(
|
|
39
|
+
'%' + SQLWhereLike.escape(value) + '%',
|
|
40
|
+
),
|
|
41
|
+
);
|
|
42
|
+
};
|
|
43
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { assertFilterCompareValue, StamhoofdFilter } from '@stamhoofd/structures';
|
|
2
|
+
import { scalarToSQLExpression, SQLLower, SQLNull } from '../../../SQLExpressions';
|
|
3
|
+
import { SQLJsonContains, SQLJsonSearch, SQLJsonValue } from '../../../SQLJsonExpressions';
|
|
4
|
+
import { SQLWhere, SQLWhereEqual, SQLWhereLike, SQLWhereOr, SQLWhereSign } from '../../../SQLWhere';
|
|
5
|
+
import { normalizeColumn, SQLCurrentColumn, SQLSyncFilterRunner, SQLModernValueType } from '../SQLModernFilter';
|
|
6
|
+
import { isJSONColumn } from '../helpers/isJSONColumn';
|
|
7
|
+
import { normalizeCompareValue } from '../helpers/normalizeCompareValue';
|
|
8
|
+
|
|
9
|
+
export function $equalsSQLFilterCompiler(filter: StamhoofdFilter): SQLSyncFilterRunner {
|
|
10
|
+
return (originalColumn: SQLCurrentColumn) => {
|
|
11
|
+
const column = normalizeColumn(originalColumn);
|
|
12
|
+
const value = normalizeCompareValue(assertFilterCompareValue(filter), column.type);
|
|
13
|
+
/**
|
|
14
|
+
* Special case, checking for equality with a JSON array.
|
|
15
|
+
* This should return true if the JSON array contains the value exactly.
|
|
16
|
+
*
|
|
17
|
+
* This differs from $contains, which will check for 'LIKE' inside the JSON array.
|
|
18
|
+
*/
|
|
19
|
+
if (column.type === SQLModernValueType.JSONArray) {
|
|
20
|
+
let where: SQLWhere;
|
|
21
|
+
|
|
22
|
+
if (typeof value === 'string') {
|
|
23
|
+
where = new SQLWhereEqual(
|
|
24
|
+
new SQLJsonSearch(
|
|
25
|
+
new SQLLower(column.expression),
|
|
26
|
+
'one',
|
|
27
|
+
scalarToSQLExpression(
|
|
28
|
+
SQLWhereLike.escape(value),
|
|
29
|
+
),
|
|
30
|
+
),
|
|
31
|
+
SQLWhereSign.NotEqual,
|
|
32
|
+
new SQLNull(),
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
else {
|
|
36
|
+
where = new SQLJsonContains(
|
|
37
|
+
column.expression,
|
|
38
|
+
scalarToSQLExpression(JSON.stringify(value)),
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// If comparing against null, also check for native MySQL null (the column does not exist)
|
|
43
|
+
if (value === null) {
|
|
44
|
+
where = new SQLWhereOr([
|
|
45
|
+
where,
|
|
46
|
+
new SQLWhereEqual(
|
|
47
|
+
column.expression,
|
|
48
|
+
SQLWhereSign.Equal,
|
|
49
|
+
new SQLNull(),
|
|
50
|
+
),
|
|
51
|
+
]);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return where;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (value === null && isJSONColumn(column)) {
|
|
58
|
+
// JSON values can either resolve to null or "null" in MySQL.
|
|
59
|
+
return new SQLWhereEqual(
|
|
60
|
+
new SQLJsonValue(column.expression), // casts json null to null, including invalid paths
|
|
61
|
+
SQLWhereSign.Equal,
|
|
62
|
+
new SQLNull(),
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return new SQLWhereEqual(
|
|
67
|
+
column.expression,
|
|
68
|
+
SQLWhereSign.Equal,
|
|
69
|
+
scalarToSQLExpression(value),
|
|
70
|
+
);
|
|
71
|
+
};
|
|
72
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { assertFilterCompareValue, StamhoofdFilter } from '@stamhoofd/structures';
|
|
2
|
+
import { scalarToSQLExpression } from '../../../SQLExpressions';
|
|
3
|
+
import { SQLWhereEqual, SQLWhereSign } from '../../../SQLWhere';
|
|
4
|
+
import { normalizeColumn, SQLCurrentColumn, SQLSyncFilterRunner } from '../SQLModernFilter';
|
|
5
|
+
import { normalizeCompareValue } from '../helpers/normalizeCompareValue';
|
|
6
|
+
|
|
7
|
+
export function $greaterThanSQLFilterCompiler(filter: StamhoofdFilter): SQLSyncFilterRunner {
|
|
8
|
+
return (originalColumn: SQLCurrentColumn) => {
|
|
9
|
+
const column = normalizeColumn(originalColumn);
|
|
10
|
+
const value = normalizeCompareValue(assertFilterCompareValue(filter), column.type);
|
|
11
|
+
|
|
12
|
+
const base = new SQLWhereEqual(
|
|
13
|
+
column.expression,
|
|
14
|
+
SQLWhereSign.Greater,
|
|
15
|
+
scalarToSQLExpression(value),
|
|
16
|
+
).setNullable(column.nullable);
|
|
17
|
+
|
|
18
|
+
return base;
|
|
19
|
+
};
|
|
20
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { SimpleError } from '@simonbackx/simple-errors';
|
|
2
|
+
import { assertFilterCompareValue, StamhoofdFilter } from '@stamhoofd/structures';
|
|
3
|
+
import { scalarToSQLExpression, SQLArray } from '../../../SQLExpressions';
|
|
4
|
+
import { SQLJsonOverlaps } from '../../../SQLJsonExpressions';
|
|
5
|
+
import { SQLWhereEqual, SQLWhereOr, SQLWhereSign } from '../../../SQLWhere';
|
|
6
|
+
import { normalizeColumn, SQLCurrentColumn, SQLSyncFilterRunner, SQLModernValueType } from '../SQLModernFilter';
|
|
7
|
+
import { normalizeCompareValue } from '../helpers/normalizeCompareValue';
|
|
8
|
+
import { $equalsSQLFilterCompiler } from './equals';
|
|
9
|
+
|
|
10
|
+
export function $inSQLFilterCompiler(filter: StamhoofdFilter): SQLSyncFilterRunner {
|
|
11
|
+
return (originalColumn: SQLCurrentColumn) => {
|
|
12
|
+
if (!Array.isArray(filter)) {
|
|
13
|
+
throw new SimpleError({
|
|
14
|
+
code: 'invalid_filter',
|
|
15
|
+
message: 'Expected array at $in filter',
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
if (filter.length > 100) {
|
|
20
|
+
throw new SimpleError({
|
|
21
|
+
code: 'invalid_filter',
|
|
22
|
+
message: 'Too many values in $in filter, maximum is 100',
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const column = normalizeColumn(originalColumn);
|
|
27
|
+
const values = filter.map(val => normalizeCompareValue(assertFilterCompareValue(val), column.type));
|
|
28
|
+
|
|
29
|
+
if (values.length === 0) {
|
|
30
|
+
// Return always false
|
|
31
|
+
return new SQLWhereOr([]);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const valuesWithoutNulls = values.filter(val => val !== null);
|
|
35
|
+
const hasNull = values.length !== valuesWithoutNulls.length;
|
|
36
|
+
|
|
37
|
+
if (hasNull) {
|
|
38
|
+
// We cannot 'in' check with null. On top of that, null has some special behaviour when used in JSON values (e.g. JSON null vs MySQL null).
|
|
39
|
+
return new SQLWhereOr([
|
|
40
|
+
$equalsSQLFilterCompiler(null)(column),
|
|
41
|
+
$inSQLFilterCompiler(valuesWithoutNulls)(column),
|
|
42
|
+
]);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (column.type === SQLModernValueType.JSONArray) {
|
|
46
|
+
const jsonValues = JSON.stringify(valuesWithoutNulls);
|
|
47
|
+
const valuesExpression = scalarToSQLExpression(jsonValues);
|
|
48
|
+
|
|
49
|
+
return new SQLJsonOverlaps(
|
|
50
|
+
column.expression,
|
|
51
|
+
valuesExpression,
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
const valuesExpression = valuesWithoutNulls.length === 1 ? scalarToSQLExpression(valuesWithoutNulls[0]) : new SQLArray(valuesWithoutNulls);
|
|
55
|
+
|
|
56
|
+
return new SQLWhereEqual(
|
|
57
|
+
column.expression,
|
|
58
|
+
SQLWhereSign.Equal,
|
|
59
|
+
valuesExpression,
|
|
60
|
+
);
|
|
61
|
+
};
|
|
62
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { assertFilterCompareValue, StamhoofdFilter } from '@stamhoofd/structures';
|
|
2
|
+
import { scalarToSQLExpression } from '../../../SQLExpressions';
|
|
3
|
+
import { SQLWhereEqual, SQLWhereSign } from '../../../SQLWhere';
|
|
4
|
+
import { normalizeColumn, SQLCurrentColumn, SQLSyncFilterRunner } from '../SQLModernFilter';
|
|
5
|
+
import { normalizeCompareValue } from '../helpers/normalizeCompareValue';
|
|
6
|
+
|
|
7
|
+
export function $lessThanSQLFilterCompiler(filter: StamhoofdFilter): SQLSyncFilterRunner {
|
|
8
|
+
return (originalColumn: SQLCurrentColumn) => {
|
|
9
|
+
const column = normalizeColumn(originalColumn);
|
|
10
|
+
const value = normalizeCompareValue(assertFilterCompareValue(filter), column.type);
|
|
11
|
+
|
|
12
|
+
const base = new SQLWhereEqual(
|
|
13
|
+
column.expression,
|
|
14
|
+
SQLWhereSign.Less,
|
|
15
|
+
scalarToSQLExpression(value),
|
|
16
|
+
).setNullable(column.nullable);
|
|
17
|
+
return base;
|
|
18
|
+
};
|
|
19
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { SQLCurrentColumn, SQLModernValueType } from '../SQLModernFilter';
|
|
2
|
+
|
|
3
|
+
export function isJSONColumn({ type }: SQLCurrentColumn): boolean {
|
|
4
|
+
return isJSONType(type);
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function isJSONType(type: SQLModernValueType): boolean {
|
|
8
|
+
return type === SQLModernValueType.JSONString
|
|
9
|
+
|| type === SQLModernValueType.JSONBoolean
|
|
10
|
+
|| type === SQLModernValueType.JSONNumber
|
|
11
|
+
|| type === SQLModernValueType.JSONArray
|
|
12
|
+
|| type === SQLModernValueType.JSONObject;
|
|
13
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { StamhoofdCompareValue } from '@stamhoofd/structures';
|
|
2
|
+
import { SQLModernValueType } from '../SQLModernFilter';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Prepares a compare value so we can compare it, given a certain column type.
|
|
6
|
+
*
|
|
7
|
+
* E.g. if you pass in true - and we are comparing against a mysql boolean column, convert it to 1.
|
|
8
|
+
*/
|
|
9
|
+
export function normalizeCompareValue(val: StamhoofdCompareValue, againstType: SQLModernValueType): string | number | Date | null | boolean {
|
|
10
|
+
if (againstType === SQLModernValueType.Table) {
|
|
11
|
+
throw new Error('Cannot compare at root level');
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
if (againstType === SQLModernValueType.JSONObject) {
|
|
15
|
+
if (val === null) {
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
throw new Error('Cannot compare with a JSON object');
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
if (val instanceof Date) {
|
|
22
|
+
if (againstType === SQLModernValueType.Datetime) {
|
|
23
|
+
return val;
|
|
24
|
+
}
|
|
25
|
+
throw new Error('Cannot compare a date with a non-datetime column');
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (typeof val === 'string') {
|
|
29
|
+
if (againstType === SQLModernValueType.String || againstType === SQLModernValueType.JSONString) {
|
|
30
|
+
return val.toLocaleLowerCase();
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (againstType === SQLModernValueType.JSONArray) {
|
|
34
|
+
// We'll search inside the array
|
|
35
|
+
return val.toLocaleLowerCase();
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
throw new Error('Cannot compare a string with a non-string column');
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (typeof val === 'boolean') {
|
|
42
|
+
if (againstType === SQLModernValueType.JSONBoolean) {
|
|
43
|
+
return val;
|
|
44
|
+
}
|
|
45
|
+
if (againstType === SQLModernValueType.Boolean || againstType === SQLModernValueType.Number) {
|
|
46
|
+
return val === true ? 1 : 0;
|
|
47
|
+
}
|
|
48
|
+
if (againstType === SQLModernValueType.JSONArray) {
|
|
49
|
+
// We'll search inside the array
|
|
50
|
+
return val;
|
|
51
|
+
}
|
|
52
|
+
throw new Error('Cannot compare a boolean with a non-boolean column');
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (typeof val === 'number') {
|
|
56
|
+
if (againstType === SQLModernValueType.JSONBoolean) {
|
|
57
|
+
return val === 1 ? true : false;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (againstType === SQLModernValueType.Boolean) {
|
|
61
|
+
if (val !== 1 && val !== 0) {
|
|
62
|
+
throw new Error('Cannot compare a number with a boolean column');
|
|
63
|
+
}
|
|
64
|
+
return val;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (againstType === SQLModernValueType.Number || againstType === SQLModernValueType.JSONNumber) {
|
|
68
|
+
return val;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (againstType === SQLModernValueType.JSONArray) {
|
|
72
|
+
// We'll search inside the array
|
|
73
|
+
return val;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
throw new Error('Cannot compare a number with a non-number column');
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (val === null) {
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (typeof val === 'object' && '$' in val) {
|
|
84
|
+
const specialValue = val['$'];
|
|
85
|
+
|
|
86
|
+
switch (specialValue) {
|
|
87
|
+
case '$now':
|
|
88
|
+
return normalizeCompareValue(new Date(), againstType);
|
|
89
|
+
default:
|
|
90
|
+
throw new Error('Unsupported magic value ' + specialValue);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return val;
|
|
95
|
+
}
|