@stamhoofd/sql 2.1.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/package.json +15 -0
- package/src/SQL.ts +58 -0
- package/src/SQLExpression.ts +30 -0
- package/src/SQLExpressions.ts +246 -0
- package/src/SQLJoin.ts +40 -0
- package/src/SQLJsonExpressions.ts +111 -0
- package/src/SQLOrderBy.ts +63 -0
- package/src/SQLSelect.ts +160 -0
- package/src/SQLWhere.ts +339 -0
- package/src/filters/SQLFilter.ts +264 -0
- package/src/filters/SQLSorter.ts +29 -0
package/package.json
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@stamhoofd/sql",
|
|
3
|
+
"version": "2.1.1",
|
|
4
|
+
"main": "./dist/index.js",
|
|
5
|
+
"types": "./dist/index.d.ts",
|
|
6
|
+
"license": "UNLICENCED",
|
|
7
|
+
"sideEffects": false,
|
|
8
|
+
"files": [
|
|
9
|
+
"src"
|
|
10
|
+
],
|
|
11
|
+
"scripts": {
|
|
12
|
+
"build": "tsc -b",
|
|
13
|
+
"build:full": "rm -rf ./dist && yarn build"
|
|
14
|
+
}
|
|
15
|
+
}
|
package/src/SQL.ts
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { SQLExpression } from "./SQLExpression";
|
|
2
|
+
import { SQLSelect } from "./SQLSelect";
|
|
3
|
+
import { SQLColumnExpression, SQLSafeValue, SQLTableExpression, SQLWildcardSelectExpression, scalarToSQLExpression } from "./SQLExpressions";
|
|
4
|
+
import { SQLJoin, SQLJoinType } from "./SQLJoin";
|
|
5
|
+
import { SQLJsonExtract } from "./SQLJsonExpressions";
|
|
6
|
+
|
|
7
|
+
class StaticSQL {
|
|
8
|
+
wildcard(namespace?: string) {
|
|
9
|
+
return new SQLWildcardSelectExpression(namespace)
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
column(namespace: string, column: string): SQLColumnExpression;
|
|
13
|
+
column(column: string): SQLColumnExpression;
|
|
14
|
+
column(namespaceOrColumn: string, column?: string): SQLColumnExpression {
|
|
15
|
+
if (column === undefined) {
|
|
16
|
+
return new SQLColumnExpression(namespaceOrColumn)
|
|
17
|
+
}
|
|
18
|
+
return new SQLColumnExpression(namespaceOrColumn, column)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
jsonValue(column: SQLExpression, path: string): SQLJsonExtract {
|
|
22
|
+
return new SQLJsonExtract(column, new SQLSafeValue(path))
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
table(namespace: string, table: string): SQLTableExpression;
|
|
26
|
+
table(table: string): SQLTableExpression;
|
|
27
|
+
table(namespaceOrTable: string, table?: string): SQLTableExpression {
|
|
28
|
+
if (table === undefined) {
|
|
29
|
+
return new SQLTableExpression(namespaceOrTable)
|
|
30
|
+
}
|
|
31
|
+
return new SQLTableExpression(namespaceOrTable, table)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
select(...columns: SQLExpression[]): InstanceType<typeof SQLSelect> {
|
|
35
|
+
if (columns.length === 0) {
|
|
36
|
+
return new SQLSelect(this.wildcard())
|
|
37
|
+
}
|
|
38
|
+
return new SQLSelect(...columns)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
leftJoin(table: SQLExpression) {
|
|
42
|
+
return new SQLJoin(SQLJoinType.Left, table)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
rightJoin(table: SQLExpression) {
|
|
46
|
+
return new SQLJoin(SQLJoinType.Right, table)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
innerJoin(table: SQLExpression) {
|
|
50
|
+
return new SQLJoin(SQLJoinType.Inner, table)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
join(table: SQLExpression) {
|
|
54
|
+
return new SQLJoin(SQLJoinType.Inner, table)
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export const SQL = new StaticSQL();
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
export type SQLExpressionOptions = {
|
|
2
|
+
defaultNamespace?: string
|
|
3
|
+
};
|
|
4
|
+
|
|
5
|
+
export type NormalizedSQLQuery = {query: string; params: any[]}
|
|
6
|
+
export type SQLQuery = NormalizedSQLQuery|string
|
|
7
|
+
|
|
8
|
+
export function joinSQLQuery(queries: (SQLQuery|undefined|null)[], seperator?: string): NormalizedSQLQuery {
|
|
9
|
+
queries = queries.filter(q => q !== undefined && q !== null)
|
|
10
|
+
return {
|
|
11
|
+
query: queries.map(q => typeof q === 'string' ? q : q!.query).join(seperator ?? ''),
|
|
12
|
+
params: queries.flatMap(q => typeof q === 'string' ? [] : q!.params)
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function normalizeSQLQuery(q: SQLQuery): NormalizedSQLQuery {
|
|
17
|
+
return {
|
|
18
|
+
query: typeof q === 'string' ? q : q.query,
|
|
19
|
+
params: typeof q === 'string' ? [] : q.params
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
export interface SQLExpression {
|
|
25
|
+
getSQL(options?: SQLExpressionOptions): SQLQuery
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function isSQLExpression(obj: unknown): obj is SQLExpression {
|
|
29
|
+
return typeof obj === 'object' && obj !== null && !!(obj as any).getSQL && typeof (obj as any).getSQL === 'function'
|
|
30
|
+
}
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
import { isSQLExpression, joinSQLQuery, SQLExpression, SQLExpressionOptions, SQLQuery } from "./SQLExpression";
|
|
2
|
+
import {Database} from "@simonbackx/simple-database"
|
|
3
|
+
|
|
4
|
+
export type SQLScalarValue = string|number|boolean|Date;
|
|
5
|
+
export type SQLDynamicExpression = SQLScalarValue|SQLScalarValue[]|null|SQLExpression
|
|
6
|
+
|
|
7
|
+
export function scalarToSQLJSONExpression(s: SQLScalarValue|null): SQLExpression {
|
|
8
|
+
if (s === null) {
|
|
9
|
+
return new SQLJSONValue(null)
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
if (s === true) {
|
|
13
|
+
return new SQLJSONValue(true)
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
if (s === false) {
|
|
17
|
+
return new SQLJSONValue(false)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
return new SQLScalar(s)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function scalarToSQLExpression(s: SQLScalarValue|null): SQLExpression {
|
|
24
|
+
if (s === null) {
|
|
25
|
+
return new SQLNull()
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return new SQLScalar(s)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function readDynamicSQLExpression(s: SQLDynamicExpression): SQLExpression {
|
|
32
|
+
if (Array.isArray(s)) {
|
|
33
|
+
return new SQLArray(s)
|
|
34
|
+
}
|
|
35
|
+
if (s === null) {
|
|
36
|
+
return new SQLNull()
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (typeof s === 'object' && !(s instanceof Date)) {
|
|
40
|
+
return s;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return new SQLScalar(s)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export class SQLCount implements SQLExpression {
|
|
47
|
+
expression: SQLExpression|null
|
|
48
|
+
|
|
49
|
+
constructor(expression: SQLExpression|null = null) {
|
|
50
|
+
this.expression = expression
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
getSQL(options?: SQLExpressionOptions): SQLQuery {
|
|
54
|
+
return joinSQLQuery([
|
|
55
|
+
'COUNT(',
|
|
56
|
+
this.expression ? this.expression.getSQL(options) : '*',
|
|
57
|
+
')'
|
|
58
|
+
])
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export class SQLSelectAs implements SQLExpression {
|
|
63
|
+
expression: SQLExpression
|
|
64
|
+
as: SQLAlias
|
|
65
|
+
|
|
66
|
+
constructor(expression: SQLExpression, as: SQLAlias) {
|
|
67
|
+
this.expression = expression
|
|
68
|
+
this.as = as;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
getSQL(options?: SQLExpressionOptions): SQLQuery {
|
|
72
|
+
return joinSQLQuery([
|
|
73
|
+
this.expression.getSQL(options),
|
|
74
|
+
' AS ',
|
|
75
|
+
this.as.getSQL(options)
|
|
76
|
+
])
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export class SQLAlias implements SQLExpression {
|
|
81
|
+
name: string;
|
|
82
|
+
|
|
83
|
+
constructor(name: string) {
|
|
84
|
+
this.name = name
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
getSQL(options?: SQLExpressionOptions): SQLQuery {
|
|
88
|
+
return Database.escapeId(this.name) ;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
export class SQLConcat implements SQLExpression {
|
|
94
|
+
expressions: SQLExpression[];
|
|
95
|
+
|
|
96
|
+
constructor(...expressions: SQLExpression[]) {
|
|
97
|
+
this.expressions = expressions
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
getSQL(options?: SQLExpressionOptions): SQLQuery {
|
|
101
|
+
return joinSQLQuery([
|
|
102
|
+
'CONCAT(',
|
|
103
|
+
joinSQLQuery(this.expressions.map(e => e.getSQL(options)), ', '),
|
|
104
|
+
')'
|
|
105
|
+
])
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
export class SQLAge implements SQLExpression {
|
|
111
|
+
expression: SQLExpression;
|
|
112
|
+
|
|
113
|
+
constructor(expression: SQLExpression) {
|
|
114
|
+
this.expression = expression
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
getSQL(options?: SQLExpressionOptions): SQLQuery {
|
|
118
|
+
return joinSQLQuery([
|
|
119
|
+
'TIMESTAMPDIFF(YEAR, ',
|
|
120
|
+
this.expression.getSQL(options),
|
|
121
|
+
', CURDATE())'
|
|
122
|
+
])
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export class SQLJSONValue implements SQLExpression {
|
|
127
|
+
value: null|true|false;
|
|
128
|
+
|
|
129
|
+
constructor(value: null|true|false) {
|
|
130
|
+
this.value = value;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
getSQL(options?: SQLExpressionOptions): SQLQuery {
|
|
134
|
+
return "CAST('"+JSON.stringify(this.value)+"' AS JSON)";
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export class SQLNull implements SQLExpression {
|
|
139
|
+
getSQL(options?: SQLExpressionOptions): SQLQuery {
|
|
140
|
+
return 'NULL';
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export class SQLNow implements SQLExpression {
|
|
145
|
+
getSQL(options?: SQLExpressionOptions): SQLQuery {
|
|
146
|
+
return 'NOW()';
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export class SQLScalar implements SQLExpression {
|
|
151
|
+
value: SQLScalarValue;
|
|
152
|
+
|
|
153
|
+
constructor(value: SQLScalarValue) {
|
|
154
|
+
this.value = value
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
getSQL(options?: SQLExpressionOptions): SQLQuery {
|
|
158
|
+
return {
|
|
159
|
+
query: '?',
|
|
160
|
+
params: [this.value]
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export class SQLSafeValue implements SQLExpression {
|
|
166
|
+
value: string|number;
|
|
167
|
+
|
|
168
|
+
constructor(value: string|number) {
|
|
169
|
+
this.value = value
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
getSQL(options?: SQLExpressionOptions): SQLQuery {
|
|
173
|
+
return JSON.stringify(this.value);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
export class SQLArray implements SQLExpression {
|
|
179
|
+
value: SQLScalarValue[];
|
|
180
|
+
|
|
181
|
+
constructor(value: SQLScalarValue[]) {
|
|
182
|
+
this.value = value
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
getSQL(options?: SQLExpressionOptions): SQLQuery {
|
|
186
|
+
return {
|
|
187
|
+
query: '(?)',
|
|
188
|
+
params: [this.value]
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
export class SQLWildcardSelectExpression implements SQLExpression {
|
|
194
|
+
namespace?: string;
|
|
195
|
+
|
|
196
|
+
constructor(namespace?: string) {
|
|
197
|
+
this.namespace = namespace
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
getSQL(options?: SQLExpressionOptions): SQLQuery {
|
|
201
|
+
return Database.escapeId(this.namespace ?? options?.defaultNamespace ?? '') + '.*'
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
export class SQLColumnExpression implements SQLExpression {
|
|
206
|
+
namespace?: string;
|
|
207
|
+
column: string;
|
|
208
|
+
|
|
209
|
+
constructor(namespace: string, column: string);
|
|
210
|
+
constructor(column: string);
|
|
211
|
+
constructor(namespaceOrColumn: string, column?: string) {
|
|
212
|
+
if (column === undefined) {
|
|
213
|
+
this.column = namespaceOrColumn;
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
this.namespace = namespaceOrColumn
|
|
217
|
+
this.column = column
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
getSQL(options?: SQLExpressionOptions): SQLQuery {
|
|
221
|
+
return Database.escapeId(this.namespace ?? options?.defaultNamespace ?? '') + '.' + Database.escapeId(this.column)
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
export class SQLTableExpression implements SQLExpression {
|
|
226
|
+
namespace?: string;
|
|
227
|
+
table: string;
|
|
228
|
+
|
|
229
|
+
constructor(namespace: string, table: string);
|
|
230
|
+
constructor(table: string);
|
|
231
|
+
constructor(namespaceOrTable: string, table?: string) {
|
|
232
|
+
if (table === undefined) {
|
|
233
|
+
this.table = namespaceOrTable;
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
this.namespace = namespaceOrTable
|
|
237
|
+
this.table = table
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
getSQL(options?: SQLExpressionOptions): SQLQuery {
|
|
241
|
+
if (!this.namespace) {
|
|
242
|
+
return Database.escapeId(this.table)
|
|
243
|
+
}
|
|
244
|
+
return Database.escapeId(this.table) + ' ' + Database.escapeId(this.namespace)
|
|
245
|
+
}
|
|
246
|
+
}
|
package/src/SQLJoin.ts
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { SQLExpression, SQLExpressionOptions, SQLQuery, joinSQLQuery } from "./SQLExpression";
|
|
2
|
+
import { SQLWhere, addWhereHelpers } from "./SQLWhere";
|
|
3
|
+
|
|
4
|
+
export enum SQLJoinType {
|
|
5
|
+
Left = "Left",
|
|
6
|
+
Right = "Right",
|
|
7
|
+
Inner = "Inner",
|
|
8
|
+
Outer = "Outer"
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export class JoinBase implements SQLExpression {
|
|
12
|
+
type = SQLJoinType.Left
|
|
13
|
+
table: SQLExpression;
|
|
14
|
+
_where: SQLWhere|null = null;
|
|
15
|
+
|
|
16
|
+
constructor(type: SQLJoinType, table: SQLExpression) {
|
|
17
|
+
this.type = type;
|
|
18
|
+
this.table = table;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
private getJoinPrefix(): string {
|
|
22
|
+
switch (this.type) {
|
|
23
|
+
case SQLJoinType.Left: return 'LEFT JOIN';
|
|
24
|
+
case SQLJoinType.Right: return 'RIGHT JOIN';
|
|
25
|
+
case SQLJoinType.Inner: return 'JOIN';
|
|
26
|
+
case SQLJoinType.Outer: return 'OUTER JOIN';
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
getSQL(options?: SQLExpressionOptions | undefined): SQLQuery {
|
|
31
|
+
return joinSQLQuery([
|
|
32
|
+
this.getJoinPrefix(),
|
|
33
|
+
this.table?.getSQL(),
|
|
34
|
+
this._where ? 'ON' : undefined,
|
|
35
|
+
this._where?.getSQL()
|
|
36
|
+
], ' ')
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export const SQLJoin = addWhereHelpers(JoinBase)
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { SQLExpression, SQLExpressionOptions, SQLQuery, joinSQLQuery } from "./SQLExpression";
|
|
2
|
+
import { SQLSafeValue } from "./SQLExpressions";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Same as target->path, JSON_EXTRACT(target, path)
|
|
6
|
+
*/
|
|
7
|
+
export class SQLJsonExtract implements SQLExpression {
|
|
8
|
+
target: SQLExpression
|
|
9
|
+
path: SQLExpression
|
|
10
|
+
|
|
11
|
+
constructor(target: SQLExpression, path: SQLExpression) {
|
|
12
|
+
this.target = target;
|
|
13
|
+
this.path = path;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
getSQL(options?: SQLExpressionOptions): SQLQuery {
|
|
17
|
+
return joinSQLQuery([
|
|
18
|
+
'JSON_EXTRACT(',
|
|
19
|
+
this.target.getSQL(options),
|
|
20
|
+
',',
|
|
21
|
+
this.path.getSQL(options),
|
|
22
|
+
')'
|
|
23
|
+
])
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* JSON_SEARCH(json_doc, one_or_all, search_str[, escape_char[, path] ...])
|
|
29
|
+
*/
|
|
30
|
+
export class SQLJsonSearch implements SQLExpression {
|
|
31
|
+
target: SQLExpression
|
|
32
|
+
oneOrAll: 'one'|'all'
|
|
33
|
+
searchStr: SQLExpression;
|
|
34
|
+
path: SQLExpression|null;
|
|
35
|
+
|
|
36
|
+
constructor(target: SQLExpression, oneOrAll: 'one'|'all', searchStr: SQLExpression, path: SQLExpression|null = null) {
|
|
37
|
+
this.target = target;
|
|
38
|
+
this.oneOrAll = oneOrAll;
|
|
39
|
+
this.searchStr = searchStr;
|
|
40
|
+
this.path = path;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
getSQL(options?: SQLExpressionOptions): SQLQuery {
|
|
44
|
+
return joinSQLQuery([
|
|
45
|
+
'JSON_SEARCH(',
|
|
46
|
+
this.target.getSQL(options),
|
|
47
|
+
',',
|
|
48
|
+
new SQLSafeValue(this.oneOrAll).getSQL(options),
|
|
49
|
+
',',
|
|
50
|
+
this.searchStr.getSQL(options),
|
|
51
|
+
...(this.path ? [
|
|
52
|
+
',',
|
|
53
|
+
this.path.getSQL(options)
|
|
54
|
+
] : []),
|
|
55
|
+
')'
|
|
56
|
+
])
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* JSON_CONTAINS(target, candidate[, path])
|
|
62
|
+
*/
|
|
63
|
+
export class SQLJsonContains implements SQLExpression {
|
|
64
|
+
target: SQLExpression
|
|
65
|
+
candidate: SQLExpression;
|
|
66
|
+
path: SQLExpression|null;
|
|
67
|
+
|
|
68
|
+
constructor(target: SQLExpression, candidate: SQLExpression, path: SQLExpression|null = null) {
|
|
69
|
+
this.target = target;
|
|
70
|
+
this.candidate = candidate;
|
|
71
|
+
this.path = path;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
getSQL(options?: SQLExpressionOptions): SQLQuery {
|
|
75
|
+
return joinSQLQuery([
|
|
76
|
+
'JSON_CONTAINS(',
|
|
77
|
+
this.target.getSQL(options),
|
|
78
|
+
',',
|
|
79
|
+
this.candidate.getSQL(options),
|
|
80
|
+
...(this.path ? [
|
|
81
|
+
',',
|
|
82
|
+
this.path.getSQL(options)
|
|
83
|
+
] : []),
|
|
84
|
+
')'
|
|
85
|
+
])
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* JSON_CONTAINS(json_doc1, json_doc2)
|
|
92
|
+
*/
|
|
93
|
+
export class SQLJsonOverlaps implements SQLExpression {
|
|
94
|
+
jsonDoc1: SQLExpression
|
|
95
|
+
jsonDoc2: SQLExpression;
|
|
96
|
+
|
|
97
|
+
constructor(jsonDoc1: SQLExpression, jsonDoc2: SQLExpression) {
|
|
98
|
+
this.jsonDoc1 = jsonDoc1;
|
|
99
|
+
this.jsonDoc2 = jsonDoc2;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
getSQL(options?: SQLExpressionOptions): SQLQuery {
|
|
103
|
+
return joinSQLQuery([
|
|
104
|
+
'JSON_OVERLAPS(',
|
|
105
|
+
this.jsonDoc1.getSQL(options),
|
|
106
|
+
',',
|
|
107
|
+
this.jsonDoc2.getSQL(options),
|
|
108
|
+
')'
|
|
109
|
+
])
|
|
110
|
+
}
|
|
111
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { SQLExpression, SQLExpressionOptions, SQLQuery, joinSQLQuery } from "./SQLExpression";
|
|
2
|
+
|
|
3
|
+
type GConstructor<T = {}> = new (...args: any[]) => T;
|
|
4
|
+
type Orderable = GConstructor<{ _orderBy: SQLOrderBy|null }>;
|
|
5
|
+
|
|
6
|
+
export function addOrderByHelpers<TBase extends Orderable>(Base: TBase) {
|
|
7
|
+
return class extends Base {
|
|
8
|
+
|
|
9
|
+
orderBy(orderBy: SQLOrderBy)
|
|
10
|
+
orderBy(column: SQLExpression, direction?: SQLOrderByDirection)
|
|
11
|
+
orderBy(columnOrOrderBy: SQLExpression, direction?: SQLOrderByDirection) {
|
|
12
|
+
let o = columnOrOrderBy as SQLOrderBy
|
|
13
|
+
if (!(columnOrOrderBy instanceof SQLOrderBy)) {
|
|
14
|
+
o = new SQLOrderBy({column: columnOrOrderBy, direction: direction ?? 'ASC'})
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
if (this._orderBy) {
|
|
18
|
+
this._orderBy.add(o)
|
|
19
|
+
} else {
|
|
20
|
+
this._orderBy = o;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return this;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
export type SQLOrderByDirection = 'ASC' | 'DESC';
|
|
30
|
+
export class SQLOrderBy implements SQLExpression {
|
|
31
|
+
orderBy: {column: SQLExpression, direction: SQLOrderByDirection}[] = [];
|
|
32
|
+
|
|
33
|
+
constructor(...orderBy: {column: SQLExpression, direction: SQLOrderByDirection}[]) {
|
|
34
|
+
this.orderBy = orderBy
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
static combine(orderBy: SQLOrderBy[]) {
|
|
38
|
+
return new SQLOrderBy(...orderBy.flatMap(o => o.orderBy))
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
add(orderBy: SQLOrderBy) {
|
|
42
|
+
this.orderBy.push(...orderBy.orderBy)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
getSQL(options?: SQLExpressionOptions | undefined): SQLQuery {
|
|
46
|
+
if (this.orderBy.length === 0) {
|
|
47
|
+
return '';
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return joinSQLQuery([
|
|
51
|
+
'ORDER BY ',
|
|
52
|
+
joinSQLQuery(
|
|
53
|
+
this.orderBy.map(o => {
|
|
54
|
+
return joinSQLQuery([
|
|
55
|
+
o.column.getSQL(options),
|
|
56
|
+
o.direction
|
|
57
|
+
], ' ')
|
|
58
|
+
}),
|
|
59
|
+
', '
|
|
60
|
+
)
|
|
61
|
+
])
|
|
62
|
+
}
|
|
63
|
+
}
|
package/src/SQLSelect.ts
ADDED
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import { SQLExpression, SQLExpressionOptions, SQLQuery, joinSQLQuery, normalizeSQLQuery } from "./SQLExpression";
|
|
2
|
+
import { SQLOrderBy, addOrderByHelpers } from "./SQLOrderBy";
|
|
3
|
+
import { SQLWhere, addWhereHelpers } from "./SQLWhere";
|
|
4
|
+
import {Database, SQLResultNamespacedRow} from "@simonbackx/simple-database"
|
|
5
|
+
import {SQLJoin} from './SQLJoin'
|
|
6
|
+
import { SQLAlias, SQLCount, SQLSelectAs, SQLWildcardSelectExpression } from "./SQLExpressions";
|
|
7
|
+
|
|
8
|
+
class SelectBase implements SQLExpression {
|
|
9
|
+
_columns: SQLExpression[]
|
|
10
|
+
_from: SQLExpression;
|
|
11
|
+
|
|
12
|
+
_limit: number|null = null;
|
|
13
|
+
_offset: number|null = null;
|
|
14
|
+
|
|
15
|
+
_where: SQLWhere|null = null;
|
|
16
|
+
_orderBy: SQLOrderBy|null = null;
|
|
17
|
+
_joins: (InstanceType<typeof SQLJoin>)[] = [];
|
|
18
|
+
|
|
19
|
+
constructor(...columns: SQLExpression[]) {
|
|
20
|
+
this._columns = columns;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
clone(): this {
|
|
24
|
+
const c = new SQLSelect(...this._columns)
|
|
25
|
+
Object.assign(c, this);
|
|
26
|
+
return c as any;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
from(table: SQLExpression): this {
|
|
30
|
+
this._from = table;
|
|
31
|
+
return this;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
join(join: InstanceType<typeof SQLJoin>): this {
|
|
35
|
+
this._joins.push(join);
|
|
36
|
+
return this;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
getSQL(options?: SQLExpressionOptions): SQLQuery {
|
|
40
|
+
const query: SQLQuery[] = [
|
|
41
|
+
'SELECT'
|
|
42
|
+
]
|
|
43
|
+
|
|
44
|
+
options = options ?? {}
|
|
45
|
+
options.defaultNamespace = (this._from as any).namespace ?? (this._from as any).table ?? undefined;
|
|
46
|
+
|
|
47
|
+
const columns = this._columns.map(c => c.getSQL(options))
|
|
48
|
+
query.push(
|
|
49
|
+
joinSQLQuery(columns, ', ')
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
query.push(
|
|
53
|
+
'FROM'
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
query.push(this._from.getSQL(options));
|
|
57
|
+
|
|
58
|
+
query.push(...this._joins.map(j => j.getSQL(options)))
|
|
59
|
+
|
|
60
|
+
if (this._where) {
|
|
61
|
+
query.push('WHERE')
|
|
62
|
+
query.push(this._where.getSQL(options))
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (this._orderBy) {
|
|
66
|
+
query.push(this._orderBy.getSQL(options))
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
if (this._limit !== null) {
|
|
71
|
+
query.push('LIMIT ' + this._limit)
|
|
72
|
+
if (this._offset !== null && this._offset !== 0) {
|
|
73
|
+
query.push('OFFSET ' + this._offset)
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return joinSQLQuery(query, ' ');
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
limit(limit: number|null, offset: number|null = null): this {
|
|
81
|
+
this._limit = limit;
|
|
82
|
+
this._offset = offset;
|
|
83
|
+
return this;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
async fetch(): Promise<SQLResultNamespacedRow[]> {
|
|
87
|
+
const {query, params} = normalizeSQLQuery(this.getSQL())
|
|
88
|
+
|
|
89
|
+
console.log(query, params);
|
|
90
|
+
const [rows] = await Database.select(query, params, {nestTables: true});
|
|
91
|
+
|
|
92
|
+
// Now map aggregated queries to the correct namespace
|
|
93
|
+
for (const row of rows) {
|
|
94
|
+
if (row['']) {
|
|
95
|
+
for (const column in row['']) {
|
|
96
|
+
const splitted = column.split('__');
|
|
97
|
+
if (splitted.length <= 1) {
|
|
98
|
+
console.warn('Aggregated column without namespace', column)
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
const namespace = splitted[0];
|
|
102
|
+
const name = splitted[1];
|
|
103
|
+
row[namespace] = row[namespace] ?? {};
|
|
104
|
+
row[namespace][name] = row[''][column];
|
|
105
|
+
}
|
|
106
|
+
delete row[''];
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return rows;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
first(required: false): Promise<SQLResultNamespacedRow|null>
|
|
113
|
+
first(required: true): Promise<SQLResultNamespacedRow>
|
|
114
|
+
async first(required = true): Promise<SQLResultNamespacedRow|null> {
|
|
115
|
+
const rows = await this.limit(1).fetch();
|
|
116
|
+
if (rows.length === 0) {
|
|
117
|
+
if (required) {
|
|
118
|
+
throw new Error('Required ' + this._from)
|
|
119
|
+
}
|
|
120
|
+
return null;
|
|
121
|
+
}
|
|
122
|
+
return rows[0]
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
async count(): Promise<number> {
|
|
126
|
+
this._columns = [
|
|
127
|
+
new SQLSelectAs(
|
|
128
|
+
new SQLCount(),
|
|
129
|
+
new SQLAlias('c')
|
|
130
|
+
)
|
|
131
|
+
]
|
|
132
|
+
this._offset = null;
|
|
133
|
+
this._limit = null;
|
|
134
|
+
this._orderBy = null;
|
|
135
|
+
|
|
136
|
+
const {query, params} = normalizeSQLQuery(this.getSQL());
|
|
137
|
+
console.log(query, params);
|
|
138
|
+
|
|
139
|
+
const [rows] = await Database.select(query, params, {nestTables: true});
|
|
140
|
+
if (rows.length === 1) {
|
|
141
|
+
const row = rows[0];
|
|
142
|
+
if ('' in row) {
|
|
143
|
+
const namespaced = row[''];
|
|
144
|
+
if ('c' in namespaced) {
|
|
145
|
+
const value = namespaced['c'];
|
|
146
|
+
if (typeof value === 'number' && Number.isInteger(value)) {
|
|
147
|
+
return value;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
console.warn('Invalid count SQL response', rows);
|
|
153
|
+
return 0;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export const SQLSelect = addOrderByHelpers(
|
|
158
|
+
addWhereHelpers(SelectBase)
|
|
159
|
+
)
|
|
160
|
+
|
package/src/SQLWhere.ts
ADDED
|
@@ -0,0 +1,339 @@
|
|
|
1
|
+
import { SQLExpression, SQLExpressionOptions, SQLQuery, joinSQLQuery, normalizeSQLQuery } from "./SQLExpression";
|
|
2
|
+
import { SQLArray, SQLDynamicExpression, SQLNull, readDynamicSQLExpression } from "./SQLExpressions";
|
|
3
|
+
|
|
4
|
+
type GConstructor<T = {}> = new (...args: any[]) => T;
|
|
5
|
+
type Whereable = GConstructor<{ _where: SQLWhere|null }>;
|
|
6
|
+
|
|
7
|
+
export type ParseWhereArguments = [
|
|
8
|
+
where: SQLWhere
|
|
9
|
+
] | [
|
|
10
|
+
whereOrColumn: SQLExpression,
|
|
11
|
+
sign: SQLWhereSign,
|
|
12
|
+
value: SQLDynamicExpression
|
|
13
|
+
] | [
|
|
14
|
+
whereOrColumn: SQLExpression,
|
|
15
|
+
value: SQLDynamicExpression
|
|
16
|
+
]
|
|
17
|
+
|
|
18
|
+
export function addWhereHelpers<TBase extends Whereable>(Base: TBase) {
|
|
19
|
+
return class extends Base {
|
|
20
|
+
parseWhere(...[whereOrColumn, signOrValue, value]: ParseWhereArguments): SQLWhere {
|
|
21
|
+
if (signOrValue === undefined) {
|
|
22
|
+
return whereOrColumn as SQLWhere;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
if (value !== undefined) {
|
|
26
|
+
return new SQLWhereEqual(
|
|
27
|
+
whereOrColumn,
|
|
28
|
+
signOrValue as SQLWhereSign,
|
|
29
|
+
readDynamicSQLExpression(value)
|
|
30
|
+
)
|
|
31
|
+
}
|
|
32
|
+
return new SQLWhereEqual(
|
|
33
|
+
whereOrColumn,
|
|
34
|
+
SQLWhereSign.Equal,
|
|
35
|
+
readDynamicSQLExpression(signOrValue)
|
|
36
|
+
)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
where<T>(this: T, ...args: ParseWhereArguments): T {
|
|
40
|
+
const w = (this as any).parseWhere(...args);
|
|
41
|
+
if (!(this as any)._where) {
|
|
42
|
+
(this as any)._where = w;
|
|
43
|
+
return this;
|
|
44
|
+
}
|
|
45
|
+
(this as any)._where = (this as any)._where.and(w);
|
|
46
|
+
return this;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
andWhere(...args: ParseWhereArguments) {
|
|
50
|
+
return this.where(...args)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
orWhere(...args: ParseWhereArguments) {
|
|
54
|
+
const w = this.parseWhere(...args);
|
|
55
|
+
if (!this._where) {
|
|
56
|
+
this._where = w;
|
|
57
|
+
return this;
|
|
58
|
+
}
|
|
59
|
+
this._where = this._where.or(w);
|
|
60
|
+
return this;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
whereNot(...args: ParseWhereArguments) {
|
|
64
|
+
const w = new SQLWhereNot(this.parseWhere(...args));
|
|
65
|
+
if (!this._where) {
|
|
66
|
+
this._where = w;
|
|
67
|
+
return this;
|
|
68
|
+
}
|
|
69
|
+
this._where = this._where.and(w);
|
|
70
|
+
return this;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
andWhereNot(...args: ParseWhereArguments) {
|
|
74
|
+
return this.whereNot(...args)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
orWhereNot(...args: ParseWhereArguments) {
|
|
78
|
+
const w = new SQLWhereNot(this.parseWhere(...args));
|
|
79
|
+
if (!this._where) {
|
|
80
|
+
this._where = w;
|
|
81
|
+
return this;
|
|
82
|
+
}
|
|
83
|
+
this._where = this._where.or(w);
|
|
84
|
+
return this;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export abstract class SQLWhere implements SQLExpression {
|
|
90
|
+
and(...where: SQLWhere[]): SQLWhere {
|
|
91
|
+
return new SQLWhereAnd([this, ...where]);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
or(...where: SQLWhere[]): SQLWhere {
|
|
95
|
+
return new SQLWhereOr([this, ...where]);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
get isSingle(): boolean {
|
|
99
|
+
return false;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
abstract getSQL(options?: SQLExpressionOptions): SQLQuery
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export enum SQLWhereSign {
|
|
106
|
+
Equal = '=',
|
|
107
|
+
Greater = '>',
|
|
108
|
+
Less = '<',
|
|
109
|
+
NotEqual = '!='
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export class SQLWhereEqual extends SQLWhere {
|
|
113
|
+
column: SQLExpression;
|
|
114
|
+
sign = SQLWhereSign.Equal
|
|
115
|
+
value: SQLExpression;
|
|
116
|
+
|
|
117
|
+
constructor (column: SQLExpression, sign: SQLWhereSign, value: SQLExpression)
|
|
118
|
+
constructor (column: SQLExpression, value: SQLExpression)
|
|
119
|
+
constructor (column: SQLExpression, signOrValue: SQLExpression | SQLWhereSign, value?: SQLExpression) {
|
|
120
|
+
super()
|
|
121
|
+
this.column = column;
|
|
122
|
+
|
|
123
|
+
if (value !== undefined) {
|
|
124
|
+
this.value = value;
|
|
125
|
+
this.sign = signOrValue as SQLWhereSign;
|
|
126
|
+
} else {
|
|
127
|
+
this.value = signOrValue as SQLExpression;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
clone(): this {
|
|
132
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
|
|
133
|
+
const c = (new (this.constructor as any)(this.column, this.sign, this.value)) as this
|
|
134
|
+
Object.assign(c, this);
|
|
135
|
+
return c;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
get isSingle(): boolean {
|
|
139
|
+
return true;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
inverted(): this {
|
|
143
|
+
return this.clone().invert()
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
invert(): this {
|
|
147
|
+
switch (this.sign) {
|
|
148
|
+
case SQLWhereSign.Equal: this.sign = SQLWhereSign.NotEqual; break;
|
|
149
|
+
case SQLWhereSign.NotEqual: this.sign = SQLWhereSign.Equal; break;
|
|
150
|
+
case SQLWhereSign.Greater: this.sign = SQLWhereSign.Less; break;
|
|
151
|
+
case SQLWhereSign.Less: this.sign = SQLWhereSign.Greater; break;
|
|
152
|
+
}
|
|
153
|
+
return this;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
getSQL(options?: SQLExpressionOptions): SQLQuery {
|
|
157
|
+
if (this.value instanceof SQLArray) {
|
|
158
|
+
if (this.sign !== SQLWhereSign.Equal && this.sign !== SQLWhereSign.NotEqual) {
|
|
159
|
+
throw new Error('Unsupported sign for array: ' + this.sign);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
return joinSQLQuery([
|
|
163
|
+
this.column.getSQL(options),
|
|
164
|
+
` ${(this.sign === SQLWhereSign.NotEqual) ? 'NOT IN' : 'IN'} `,
|
|
165
|
+
this.value.getSQL(options)
|
|
166
|
+
])
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
if (this.value instanceof SQLNull) {
|
|
171
|
+
if (this.sign !== SQLWhereSign.Equal && this.sign !== SQLWhereSign.NotEqual) {
|
|
172
|
+
throw new Error('Unsupported sign for NULL: ' + this.sign);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
return joinSQLQuery([
|
|
176
|
+
this.column.getSQL(options),
|
|
177
|
+
` IS ${(this.sign === SQLWhereSign.NotEqual) ? 'NOT ' : ''} `,
|
|
178
|
+
this.value.getSQL(options)
|
|
179
|
+
])
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
return joinSQLQuery([
|
|
183
|
+
this.column.getSQL(options),
|
|
184
|
+
` ${this.sign} `,
|
|
185
|
+
this.value.getSQL(options)
|
|
186
|
+
])
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
export class SQLWhereLike extends SQLWhere {
|
|
191
|
+
column: SQLExpression;
|
|
192
|
+
notLike = false;
|
|
193
|
+
value: SQLExpression;
|
|
194
|
+
|
|
195
|
+
constructor (column: SQLExpression, value: SQLExpression) {
|
|
196
|
+
super()
|
|
197
|
+
this.column = column;
|
|
198
|
+
this.value = value
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
static escape(str: string) {
|
|
202
|
+
return str.replace(/([%_])/g, '\\$1')
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
clone(): this {
|
|
206
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
|
|
207
|
+
const c = (new (this.constructor as any)(this.column, this.value)) as this
|
|
208
|
+
Object.assign(c, this);
|
|
209
|
+
return c;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
get isSingle(): boolean {
|
|
213
|
+
return true;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
inverted(): this {
|
|
217
|
+
return this.clone().invert()
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
invert(): this {
|
|
221
|
+
this.notLike = !this.notLike
|
|
222
|
+
return this;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
getSQL(options?: SQLExpressionOptions): SQLQuery {
|
|
226
|
+
return joinSQLQuery([
|
|
227
|
+
this.column.getSQL(options),
|
|
228
|
+
` ${this.notLike ? 'NOT LIKE' : 'LIKE'} `,
|
|
229
|
+
this.value.getSQL(options)
|
|
230
|
+
])
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
export class SQLWhereExists extends SQLWhere {
|
|
235
|
+
subquery: SQLExpression;
|
|
236
|
+
notExists = false;
|
|
237
|
+
|
|
238
|
+
constructor (subquery: SQLExpression) {
|
|
239
|
+
super()
|
|
240
|
+
this.subquery = subquery;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
clone(): this {
|
|
244
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
|
|
245
|
+
const c = (new (this.constructor as any)(this.subquery)) as this
|
|
246
|
+
Object.assign(c, this);
|
|
247
|
+
return c;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
get isSingle(): boolean {
|
|
251
|
+
return true;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
inverted(): this {
|
|
255
|
+
return this.clone().invert()
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
invert(): this {
|
|
259
|
+
this.notExists = !this.notExists
|
|
260
|
+
return this;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
getSQL(options?: SQLExpressionOptions): SQLQuery {
|
|
264
|
+
return joinSQLQuery([
|
|
265
|
+
`${this.notExists ? 'NOT EXISTS' : 'EXISTS'} (`,
|
|
266
|
+
this.subquery.getSQL({...options}),
|
|
267
|
+
`)`,
|
|
268
|
+
])
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
export class SQLWhereAnd extends SQLWhere {
|
|
273
|
+
children: SQLWhere[]
|
|
274
|
+
|
|
275
|
+
constructor (children: SQLWhere[]) {
|
|
276
|
+
super()
|
|
277
|
+
this.children = children;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
getSQL(options?: SQLExpressionOptions): SQLQuery {
|
|
281
|
+
return joinSQLQuery(
|
|
282
|
+
this.children.map(c => {
|
|
283
|
+
if (c.isSingle) {
|
|
284
|
+
return c.getSQL(options)
|
|
285
|
+
}
|
|
286
|
+
return joinSQLQuery(['(', c.getSQL(options), ')'])
|
|
287
|
+
}),
|
|
288
|
+
' AND '
|
|
289
|
+
)
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
export class SQLWhereOr extends SQLWhere {
|
|
294
|
+
children: SQLWhere[]
|
|
295
|
+
|
|
296
|
+
constructor (children: SQLWhere[]) {
|
|
297
|
+
super()
|
|
298
|
+
this.children = children;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
getSQL(options?: SQLExpressionOptions): SQLQuery {
|
|
302
|
+
return joinSQLQuery(
|
|
303
|
+
this.children.map(c => {
|
|
304
|
+
if (c.isSingle) {
|
|
305
|
+
return c.getSQL(options)
|
|
306
|
+
}
|
|
307
|
+
return joinSQLQuery(['(', c.getSQL(options), ')'])
|
|
308
|
+
}),
|
|
309
|
+
' OR '
|
|
310
|
+
)
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
export class SQLWhereNot extends SQLWhere {
|
|
315
|
+
a: SQLWhere
|
|
316
|
+
|
|
317
|
+
constructor (a: SQLWhere) {
|
|
318
|
+
super()
|
|
319
|
+
this.a = a;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
get isSingle(): boolean {
|
|
323
|
+
return this.a.isSingle;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
getSQL(options?: SQLExpressionOptions): SQLQuery {
|
|
327
|
+
// Optimize query
|
|
328
|
+
if (this.a instanceof SQLWhereEqual) {
|
|
329
|
+
return this.a.inverted().getSQL(options);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
const sqlA = normalizeSQLQuery(this.a.getSQL(options));
|
|
333
|
+
|
|
334
|
+
return {
|
|
335
|
+
query: `NOT (${sqlA.query})`,
|
|
336
|
+
params: sqlA.params
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
}
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
import { SimpleError } from "@simonbackx/simple-errors";
|
|
2
|
+
import { StamhoofdFilter, StamhoofdKeyFilterValue } from "@stamhoofd/structures";
|
|
3
|
+
import { SQL } from "../SQL";
|
|
4
|
+
import { SQLExpression } from "../SQLExpression";
|
|
5
|
+
import { SQLArray, SQLColumnExpression, SQLNull, SQLSafeValue, SQLScalarValue, scalarToSQLExpression, scalarToSQLJSONExpression } from "../SQLExpressions";
|
|
6
|
+
import { SQLJsonContains, SQLJsonOverlaps, SQLJsonSearch } from "../SQLJsonExpressions";
|
|
7
|
+
import { SQLSelect } from "../SQLSelect";
|
|
8
|
+
import { SQLWhere, SQLWhereAnd, SQLWhereEqual, SQLWhereExists, SQLWhereLike, SQLWhereNot, SQLWhereOr, SQLWhereSign } from "../SQLWhere";
|
|
9
|
+
|
|
10
|
+
export type SQLFilterCompiler = (filter: StamhoofdFilter, filters: SQLFilterDefinitions) => SQLWhere|null;
|
|
11
|
+
export type SQLFilterDefinitions = Record<string, SQLFilterCompiler>
|
|
12
|
+
|
|
13
|
+
export function andSQLFilterCompiler(filter: StamhoofdFilter, filters: SQLFilterDefinitions): SQLWhere {
|
|
14
|
+
const runners = compileSQLFilter(filter, filters);
|
|
15
|
+
return new SQLWhereAnd(runners)
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function orSQLFilterCompiler(filter: StamhoofdFilter, filters: SQLFilterDefinitions): SQLWhere {
|
|
19
|
+
const runners = compileSQLFilter(filter, filters);
|
|
20
|
+
return new SQLWhereOr(runners)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function notSQLFilterCompiler(filter: StamhoofdFilter, filters: SQLFilterDefinitions): SQLWhere {
|
|
24
|
+
const andRunner = andSQLFilterCompiler(filter, filters);
|
|
25
|
+
return new SQLWhereNot(andRunner)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function guardScalar(s: any): asserts s is SQLScalarValue|null {
|
|
29
|
+
if (typeof s !== 'string' && typeof s !== 'number' && typeof s !== 'boolean' && !(s instanceof Date) && s !== null) {
|
|
30
|
+
throw new Error('Invalid scalar value')
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function guardNotNullScalar(s: any): asserts s is SQLScalarValue {
|
|
36
|
+
if (typeof s !== 'string' && typeof s !== 'number' && typeof s !== 'boolean' && !(s instanceof Date)) {
|
|
37
|
+
throw new Error('Invalid scalar value')
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function guardString(s: any): asserts s is string {
|
|
42
|
+
if (typeof s !== 'string') {
|
|
43
|
+
throw new Error('Invalid string value')
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function createSQLRelationFilterCompiler(baseSelect: InstanceType<typeof SQLSelect> & SQLExpression, definitions: SQLFilterDefinitions): SQLFilterCompiler {
|
|
48
|
+
return (filter: StamhoofdFilter) => {
|
|
49
|
+
const f = filter as any;
|
|
50
|
+
|
|
51
|
+
if ('$elemMatch' in f) {
|
|
52
|
+
const w = compileToSQLFilter(f['$elemMatch'], definitions)
|
|
53
|
+
const q = baseSelect.clone().where(w);
|
|
54
|
+
return new SQLWhereExists(q)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
throw new Error('Invalid filter')
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Already joined, but creates a namespace
|
|
62
|
+
export function createSQLFilterNamespace(definitions: SQLFilterDefinitions): SQLFilterCompiler {
|
|
63
|
+
return (filter: StamhoofdFilter) => {
|
|
64
|
+
return andSQLFilterCompiler(filter, definitions)
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function createSQLExpressionFilterCompiler(sqlExpression: SQLExpression, normalizeValue?: (v: SQLScalarValue|null) => SQLScalarValue|null, isJSONValue = false, isJSONObject = false): SQLFilterCompiler {
|
|
69
|
+
const norm = normalizeValue ?? ((v) => v);
|
|
70
|
+
const convertToExpression = isJSONValue ? scalarToSQLJSONExpression : scalarToSQLExpression
|
|
71
|
+
|
|
72
|
+
return (filter: StamhoofdFilter, filters: SQLFilterDefinitions) => {
|
|
73
|
+
if (typeof filter === 'string' || typeof filter === 'number' || typeof filter === 'boolean' || filter === null || filter === undefined) {
|
|
74
|
+
filter = {
|
|
75
|
+
$eq: filter
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (Array.isArray(filter)) {
|
|
80
|
+
throw new Error('Unexpected array in filter')
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
const f = filter as any;
|
|
85
|
+
|
|
86
|
+
if ('$eq' in f) {
|
|
87
|
+
guardScalar(f.$eq);
|
|
88
|
+
|
|
89
|
+
if (isJSONObject) {
|
|
90
|
+
const v = norm(f.$eq);
|
|
91
|
+
|
|
92
|
+
// if (typeof v === 'string') {
|
|
93
|
+
// return new SQLWhereEqual(
|
|
94
|
+
// new SQLJsonSearch(sqlExpression, 'one', convertToExpression(v)),
|
|
95
|
+
// SQLWhereSign.NotEqual,
|
|
96
|
+
// new SQLNull()
|
|
97
|
+
// );
|
|
98
|
+
// }
|
|
99
|
+
|
|
100
|
+
// else
|
|
101
|
+
return new SQLWhereEqual(
|
|
102
|
+
new SQLJsonContains(
|
|
103
|
+
sqlExpression,
|
|
104
|
+
convertToExpression(JSON.stringify(v))
|
|
105
|
+
),
|
|
106
|
+
SQLWhereSign.Equal,
|
|
107
|
+
new SQLSafeValue(1)
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
return new SQLWhereEqual(sqlExpression, SQLWhereSign.Equal, convertToExpression(norm(f.$eq)));
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if ('$in' in f) {
|
|
114
|
+
if (!Array.isArray(f.$in)) {
|
|
115
|
+
throw new SimpleError({
|
|
116
|
+
code: 'invalid_filter',
|
|
117
|
+
message: 'Expected array at $in filter'
|
|
118
|
+
})
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (f.$in.length === 0) {
|
|
122
|
+
return new SQLWhereEqual(new SQLSafeValue(1), SQLWhereSign.Equal, new SQLSafeValue(0));
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const v = f.$in.map(a => norm(a));
|
|
126
|
+
|
|
127
|
+
if (isJSONObject) {
|
|
128
|
+
// else
|
|
129
|
+
return new SQLWhereEqual(
|
|
130
|
+
new SQLJsonOverlaps(
|
|
131
|
+
sqlExpression,
|
|
132
|
+
convertToExpression(JSON.stringify(v))
|
|
133
|
+
),
|
|
134
|
+
SQLWhereSign.Equal,
|
|
135
|
+
new SQLSafeValue(1)
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
return new SQLWhereEqual(sqlExpression, SQLWhereSign.Equal, new SQLArray(v));
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if ('$neq' in f) {
|
|
142
|
+
guardScalar(f.$neq);
|
|
143
|
+
|
|
144
|
+
if (isJSONObject) {
|
|
145
|
+
const v = norm(f.$eq);
|
|
146
|
+
|
|
147
|
+
// if (typeof v === 'string') {
|
|
148
|
+
// return new SQLWhereEqual(
|
|
149
|
+
// new SQLJsonSearch(sqlExpression, 'one', convertToExpression(v)),
|
|
150
|
+
// SQLWhereSign.Equal,
|
|
151
|
+
// new SQLNull()
|
|
152
|
+
// );
|
|
153
|
+
// }
|
|
154
|
+
|
|
155
|
+
// else
|
|
156
|
+
return new SQLWhereEqual(
|
|
157
|
+
new SQLJsonContains(
|
|
158
|
+
sqlExpression,
|
|
159
|
+
convertToExpression(JSON.stringify(v))
|
|
160
|
+
),
|
|
161
|
+
SQLWhereSign.Equal,
|
|
162
|
+
new SQLSafeValue(0)
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
return new SQLWhereEqual(sqlExpression, SQLWhereSign.NotEqual, convertToExpression(norm(f.$neq)));
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if ('$gt' in f) {
|
|
169
|
+
guardScalar(f.$gt);
|
|
170
|
+
|
|
171
|
+
if (isJSONObject) {
|
|
172
|
+
throw new Error('Greater than is not supported in this place')
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
if (f.$gt === null) {
|
|
176
|
+
// > null is same as not equal to null (everything is larger than null in mysql) - to be consistent with order by behaviour
|
|
177
|
+
return new SQLWhereEqual(sqlExpression, SQLWhereSign.NotEqual, convertToExpression(null));
|
|
178
|
+
}
|
|
179
|
+
return new SQLWhereEqual(sqlExpression, SQLWhereSign.Greater, convertToExpression(norm(f.$gt)));
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
if ('$lt' in f) {
|
|
183
|
+
guardScalar(f.$lt);
|
|
184
|
+
|
|
185
|
+
if (isJSONObject) {
|
|
186
|
+
throw new Error('Less than is not supported in this place')
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
if (f.$lt === null) {
|
|
190
|
+
// < null is always nothing, there is nothing smaller than null in MySQL - to be consistent with order by behaviour
|
|
191
|
+
return new SQLWhereEqual(new SQLSafeValue(1), SQLWhereSign.Equal, new SQLSafeValue(0));
|
|
192
|
+
}
|
|
193
|
+
return new SQLWhereEqual(sqlExpression, SQLWhereSign.Less, convertToExpression(norm(f.$lt)));
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
if ('$contains' in f) {
|
|
197
|
+
guardString(f.$contains);
|
|
198
|
+
|
|
199
|
+
if (isJSONObject) {
|
|
200
|
+
return new SQLWhereEqual(
|
|
201
|
+
new SQLJsonSearch(
|
|
202
|
+
sqlExpression,
|
|
203
|
+
'one',
|
|
204
|
+
convertToExpression(
|
|
205
|
+
'%'+SQLWhereLike.escape(f.$contains)+'%'
|
|
206
|
+
)
|
|
207
|
+
),
|
|
208
|
+
SQLWhereSign.NotEqual,
|
|
209
|
+
new SQLNull()
|
|
210
|
+
);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
return new SQLWhereLike(
|
|
214
|
+
sqlExpression,
|
|
215
|
+
convertToExpression(
|
|
216
|
+
'%'+SQLWhereLike.escape(f.$contains)+'%'
|
|
217
|
+
)
|
|
218
|
+
);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
throw new Error('Invalid filter ' + JSON.stringify(f))
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
export function createSQLColumnFilterCompiler(name: string | SQLColumnExpression, normalizeValue?: (v: SQLScalarValue|null) => SQLScalarValue|null): SQLFilterCompiler {
|
|
226
|
+
const column = name instanceof SQLColumnExpression ? name : SQL.column(name);
|
|
227
|
+
return createSQLExpressionFilterCompiler(column, normalizeValue)
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
export const baseSQLFilterCompilers: SQLFilterDefinitions = {
|
|
231
|
+
'$and': andSQLFilterCompiler,
|
|
232
|
+
'$or': orSQLFilterCompiler,
|
|
233
|
+
'$not': notSQLFilterCompiler,
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function compileSQLFilter(filter: StamhoofdFilter, definitions: SQLFilterDefinitions): SQLWhere[] {
|
|
237
|
+
if (filter === undefined) {
|
|
238
|
+
return [];
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
const runners: SQLWhere[] = []
|
|
242
|
+
|
|
243
|
+
for (const f of (Array.isArray(filter) ? filter : [filter])) {
|
|
244
|
+
if (!f) {
|
|
245
|
+
continue;
|
|
246
|
+
}
|
|
247
|
+
for (const key of Object.keys(f)) {
|
|
248
|
+
const filter = definitions[key];
|
|
249
|
+
if (!filter) {
|
|
250
|
+
throw new Error('Unsupported filter ' + key)
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
const s = filter(f[key] as StamhoofdFilter, definitions)
|
|
254
|
+
if (s === undefined || s === null) {
|
|
255
|
+
throw new Error('Unsupported filter value for ' + key)
|
|
256
|
+
}
|
|
257
|
+
runners.push(s);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
return runners
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
export const compileToSQLFilter = andSQLFilterCompiler
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { PlainObject } from "@simonbackx/simple-encoding";
|
|
2
|
+
import { SortDefinition, SortList } from "@stamhoofd/structures";
|
|
3
|
+
|
|
4
|
+
import { SQLOrderBy, SQLOrderByDirection } from "../SQLOrderBy";
|
|
5
|
+
|
|
6
|
+
export type SQLSortDefinition<T, B extends PlainObject = PlainObject> = SortDefinition<T, B> & {
|
|
7
|
+
toSQL(direction: SQLOrderByDirection): SQLOrderBy
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export type SQLSortDefinitions<T = any> = Record<string, SQLSortDefinition<T>>
|
|
11
|
+
|
|
12
|
+
export function compileToSQLSorter(sortBy: SortList, definitions: SQLSortDefinitions): SQLOrderBy {
|
|
13
|
+
const sorters: SQLOrderBy[] = [];
|
|
14
|
+
|
|
15
|
+
for (const s of sortBy) {
|
|
16
|
+
const d = definitions[s.key];
|
|
17
|
+
if (!d) {
|
|
18
|
+
throw new Error('Unknown sort key ' + s.key)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
sorters.push(d.toSQL(s.order));
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
if (sorters.length === 0) {
|
|
25
|
+
throw new Error('No sortBy passed')
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return SQLOrderBy.combine(sorters)
|
|
29
|
+
}
|