@budibase/backend-core 2.27.4 → 2.27.6

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 (51) hide show
  1. package/dist/index.js +1414 -163
  2. package/dist/index.js.map +4 -4
  3. package/dist/index.js.meta.json +1 -1
  4. package/dist/package.json +5 -4
  5. package/dist/plugins.js.map +1 -1
  6. package/dist/plugins.js.meta.json +1 -1
  7. package/dist/src/constants/db.d.ts +6 -0
  8. package/dist/src/constants/db.js +7 -1
  9. package/dist/src/constants/db.js.map +1 -1
  10. package/dist/src/environment.d.ts +2 -0
  11. package/dist/src/environment.js +2 -0
  12. package/dist/src/environment.js.map +1 -1
  13. package/dist/src/index.d.ts +1 -0
  14. package/dist/src/index.js +2 -1
  15. package/dist/src/index.js.map +1 -1
  16. package/dist/src/objectStore/objectStore.d.ts +10 -2
  17. package/dist/src/objectStore/objectStore.js +13 -2
  18. package/dist/src/objectStore/objectStore.js.map +1 -1
  19. package/dist/src/sql/designDoc.d.ts +2 -0
  20. package/dist/src/sql/designDoc.js +20 -0
  21. package/dist/src/sql/designDoc.js.map +1 -0
  22. package/dist/src/sql/index.d.ts +4 -0
  23. package/dist/src/sql/index.js +36 -0
  24. package/dist/src/sql/index.js.map +1 -0
  25. package/dist/src/sql/sql.d.ts +21 -0
  26. package/dist/src/sql/sql.js +752 -0
  27. package/dist/src/sql/sql.js.map +1 -0
  28. package/dist/src/sql/sqlStatements.d.ts +14 -0
  29. package/dist/src/sql/sqlStatements.js +60 -0
  30. package/dist/src/sql/sqlStatements.js.map +1 -0
  31. package/dist/src/sql/sqlTable.d.ts +13 -0
  32. package/dist/src/sql/sqlTable.js +231 -0
  33. package/dist/src/sql/sqlTable.js.map +1 -0
  34. package/dist/src/sql/utils.d.ts +22 -0
  35. package/dist/src/sql/utils.js +133 -0
  36. package/dist/src/sql/utils.js.map +1 -0
  37. package/dist/tests/core/utilities/mocks/licenses.d.ts +1 -0
  38. package/dist/tests/core/utilities/mocks/licenses.js +5 -1
  39. package/dist/tests/core/utilities/mocks/licenses.js.map +1 -1
  40. package/package.json +5 -4
  41. package/src/constants/db.ts +6 -0
  42. package/src/environment.ts +3 -0
  43. package/src/index.ts +1 -0
  44. package/src/objectStore/objectStore.ts +21 -7
  45. package/src/sql/designDoc.ts +17 -0
  46. package/src/sql/index.ts +5 -0
  47. package/src/sql/sql.ts +852 -0
  48. package/src/sql/sqlStatements.ts +79 -0
  49. package/src/sql/sqlTable.ts +289 -0
  50. package/src/sql/utils.ts +134 -0
  51. package/tests/core/utilities/mocks/licenses.ts +4 -0
package/src/sql/sql.ts ADDED
@@ -0,0 +1,852 @@
1
+ import { Knex, knex } from "knex"
2
+ import * as dbCore from "../db"
3
+ import {
4
+ isIsoDateString,
5
+ isValidFilter,
6
+ getNativeSql,
7
+ isExternalTable,
8
+ } from "./utils"
9
+ import { SqlStatements } from "./sqlStatements"
10
+ import SqlTableQueryBuilder from "./sqlTable"
11
+ import {
12
+ BBReferenceFieldMetadata,
13
+ FieldSchema,
14
+ FieldType,
15
+ JsonFieldMetadata,
16
+ Operation,
17
+ QueryJson,
18
+ SqlQuery,
19
+ RelationshipsJson,
20
+ SearchFilters,
21
+ SortDirection,
22
+ SqlQueryBinding,
23
+ Table,
24
+ TableSourceType,
25
+ INTERNAL_TABLE_SOURCE_ID,
26
+ SqlClient,
27
+ QueryOptions,
28
+ JsonTypes,
29
+ prefixed,
30
+ } from "@budibase/types"
31
+ import environment from "../environment"
32
+ import { helpers } from "@budibase/shared-core"
33
+
34
+ type QueryFunction = (query: SqlQuery | SqlQuery[], operation: Operation) => any
35
+
36
+ const envLimit = environment.SQL_MAX_ROWS
37
+ ? parseInt(environment.SQL_MAX_ROWS)
38
+ : null
39
+ const BASE_LIMIT = envLimit || 5000
40
+
41
+ // these are invalid dates sent by the client, need to convert them to a real max date
42
+ const MIN_ISO_DATE = "0000-00-00T00:00:00.000Z"
43
+ const MAX_ISO_DATE = "9999-00-00T00:00:00.000Z"
44
+
45
+ function likeKey(client: string, key: string): string {
46
+ let start: string, end: string
47
+ switch (client) {
48
+ case SqlClient.MY_SQL:
49
+ start = end = "`"
50
+ break
51
+ case SqlClient.SQL_LITE:
52
+ case SqlClient.ORACLE:
53
+ case SqlClient.POSTGRES:
54
+ start = end = '"'
55
+ break
56
+ case SqlClient.MS_SQL:
57
+ start = "["
58
+ end = "]"
59
+ break
60
+ default:
61
+ throw new Error("Unknown client generating like key")
62
+ }
63
+ const parts = key.split(".")
64
+ key = parts.map(part => `${start}${part}${end}`).join(".")
65
+ return key
66
+ }
67
+
68
+ function parse(input: any) {
69
+ if (Array.isArray(input)) {
70
+ return JSON.stringify(input)
71
+ }
72
+ if (input == undefined) {
73
+ return null
74
+ }
75
+ if (typeof input !== "string") {
76
+ return input
77
+ }
78
+ if (input === MAX_ISO_DATE || input === MIN_ISO_DATE) {
79
+ return null
80
+ }
81
+ if (isIsoDateString(input)) {
82
+ return new Date(input.trim())
83
+ }
84
+ return input
85
+ }
86
+
87
+ function parseBody(body: any) {
88
+ for (let [key, value] of Object.entries(body)) {
89
+ body[key] = parse(value)
90
+ }
91
+ return body
92
+ }
93
+
94
+ function parseFilters(filters: SearchFilters | undefined): SearchFilters {
95
+ if (!filters) {
96
+ return {}
97
+ }
98
+ for (let [key, value] of Object.entries(filters)) {
99
+ let parsed
100
+ if (typeof value === "object") {
101
+ parsed = parseFilters(value)
102
+ } else {
103
+ parsed = parse(value)
104
+ }
105
+ // @ts-ignore
106
+ filters[key] = parsed
107
+ }
108
+ return filters
109
+ }
110
+
111
+ function generateSelectStatement(
112
+ json: QueryJson,
113
+ knex: Knex
114
+ ): (string | Knex.Raw)[] | "*" {
115
+ const { resource, meta } = json
116
+
117
+ if (!resource) {
118
+ return "*"
119
+ }
120
+
121
+ const schema = meta?.table?.schema
122
+ return resource.fields.map(field => {
123
+ const fieldNames = field.split(/\./g)
124
+ const tableName = fieldNames[0]
125
+ const columnName = fieldNames[1]
126
+ const columnSchema = schema?.[columnName]
127
+ if (columnSchema && knex.client.config.client === SqlClient.POSTGRES) {
128
+ const externalType = schema[columnName].externalType
129
+ if (externalType?.includes("money")) {
130
+ return knex.raw(
131
+ `"${tableName}"."${columnName}"::money::numeric as "${field}"`
132
+ )
133
+ }
134
+ }
135
+ if (
136
+ knex.client.config.client === SqlClient.MS_SQL &&
137
+ columnSchema?.type === FieldType.DATETIME &&
138
+ columnSchema.timeOnly
139
+ ) {
140
+ // Time gets returned as timestamp from mssql, not matching the expected HH:mm format
141
+ return knex.raw(`CONVERT(varchar, ${field}, 108) as "${field}"`)
142
+ }
143
+ return `${field} as ${field}`
144
+ })
145
+ }
146
+
147
+ function getTableName(table?: Table): string | undefined {
148
+ // SQS uses the table ID rather than the table name
149
+ if (
150
+ table?.sourceType === TableSourceType.INTERNAL ||
151
+ table?.sourceId === INTERNAL_TABLE_SOURCE_ID
152
+ ) {
153
+ return table?._id
154
+ } else {
155
+ return table?.name
156
+ }
157
+ }
158
+
159
+ function convertBooleans(query: SqlQuery | SqlQuery[]): SqlQuery | SqlQuery[] {
160
+ if (Array.isArray(query)) {
161
+ return query.map((q: SqlQuery) => convertBooleans(q) as SqlQuery)
162
+ } else {
163
+ if (query.bindings) {
164
+ query.bindings = query.bindings.map(binding => {
165
+ if (typeof binding === "boolean") {
166
+ return binding ? 1 : 0
167
+ }
168
+ return binding
169
+ })
170
+ }
171
+ }
172
+ return query
173
+ }
174
+
175
+ class InternalBuilder {
176
+ private readonly client: string
177
+
178
+ constructor(client: string) {
179
+ this.client = client
180
+ }
181
+
182
+ // right now we only do filters on the specific table being queried
183
+ addFilters(
184
+ query: Knex.QueryBuilder,
185
+ filters: SearchFilters | undefined,
186
+ table: Table,
187
+ opts: { aliases?: Record<string, string>; relationship?: boolean }
188
+ ): Knex.QueryBuilder {
189
+ if (!filters) {
190
+ return query
191
+ }
192
+ filters = parseFilters(filters)
193
+ // if all or specified in filters, then everything is an or
194
+ const allOr = filters.allOr
195
+ const sqlStatements = new SqlStatements(this.client, table, { allOr })
196
+ const tableName =
197
+ this.client === SqlClient.SQL_LITE ? table._id! : table.name
198
+
199
+ function getTableAlias(name: string) {
200
+ const alias = opts.aliases?.[name]
201
+ return alias || name
202
+ }
203
+ function iterate(
204
+ structure: { [key: string]: any },
205
+ fn: (key: string, value: any) => void
206
+ ) {
207
+ for (let [key, value] of Object.entries(structure)) {
208
+ const updatedKey = dbCore.removeKeyNumbering(key)
209
+ const isRelationshipField = updatedKey.includes(".")
210
+ if (!opts.relationship && !isRelationshipField) {
211
+ const alias = getTableAlias(tableName)
212
+ fn(alias ? `${alias}.${updatedKey}` : updatedKey, value)
213
+ }
214
+ if (opts.relationship && isRelationshipField) {
215
+ const [filterTableName, property] = updatedKey.split(".")
216
+ const alias = getTableAlias(filterTableName)
217
+ fn(alias ? `${alias}.${property}` : property, value)
218
+ }
219
+ }
220
+ }
221
+
222
+ const like = (key: string, value: any) => {
223
+ const fuzzyOr = filters?.fuzzyOr
224
+ const fnc = fuzzyOr || allOr ? "orWhere" : "where"
225
+ // postgres supports ilike, nothing else does
226
+ if (this.client === SqlClient.POSTGRES) {
227
+ query = query[fnc](key, "ilike", `%${value}%`)
228
+ } else {
229
+ const rawFnc = `${fnc}Raw`
230
+ // @ts-ignore
231
+ query = query[rawFnc](`LOWER(${likeKey(this.client, key)}) LIKE ?`, [
232
+ `%${value.toLowerCase()}%`,
233
+ ])
234
+ }
235
+ }
236
+
237
+ const contains = (mode: object, any: boolean = false) => {
238
+ const rawFnc = allOr ? "orWhereRaw" : "whereRaw"
239
+ const not = mode === filters?.notContains ? "NOT " : ""
240
+ function stringifyArray(value: Array<any>, quoteStyle = '"'): string {
241
+ for (let i in value) {
242
+ if (typeof value[i] === "string") {
243
+ value[i] = `${quoteStyle}${value[i]}${quoteStyle}`
244
+ }
245
+ }
246
+ return `[${value.join(",")}]`
247
+ }
248
+ if (this.client === SqlClient.POSTGRES) {
249
+ iterate(mode, (key: string, value: Array<any>) => {
250
+ const wrap = any ? "" : "'"
251
+ const op = any ? "\\?| array" : "@>"
252
+ const fieldNames = key.split(/\./g)
253
+ const table = fieldNames[0]
254
+ const col = fieldNames[1]
255
+ query = query[rawFnc](
256
+ `${not}COALESCE("${table}"."${col}"::jsonb ${op} ${wrap}${stringifyArray(
257
+ value,
258
+ any ? "'" : '"'
259
+ )}${wrap}, FALSE)`
260
+ )
261
+ })
262
+ } else if (this.client === SqlClient.MY_SQL) {
263
+ const jsonFnc = any ? "JSON_OVERLAPS" : "JSON_CONTAINS"
264
+ iterate(mode, (key: string, value: Array<any>) => {
265
+ query = query[rawFnc](
266
+ `${not}COALESCE(${jsonFnc}(${key}, '${stringifyArray(
267
+ value
268
+ )}'), FALSE)`
269
+ )
270
+ })
271
+ } else {
272
+ const andOr = mode === filters?.containsAny ? " OR " : " AND "
273
+ iterate(mode, (key: string, value: Array<any>) => {
274
+ let statement = ""
275
+ for (let i in value) {
276
+ if (typeof value[i] === "string") {
277
+ value[i] = `%"${value[i].toLowerCase()}"%`
278
+ } else {
279
+ value[i] = `%${value[i]}%`
280
+ }
281
+ statement +=
282
+ (statement ? andOr : "") +
283
+ `COALESCE(LOWER(${likeKey(this.client, key)}), '') LIKE ?`
284
+ }
285
+
286
+ if (statement === "") {
287
+ return
288
+ }
289
+
290
+ // @ts-ignore
291
+ query = query[rawFnc](`${not}(${statement})`, value)
292
+ })
293
+ }
294
+ }
295
+
296
+ if (filters.oneOf) {
297
+ iterate(filters.oneOf, (key, array) => {
298
+ const fnc = allOr ? "orWhereIn" : "whereIn"
299
+ query = query[fnc](key, Array.isArray(array) ? array : [array])
300
+ })
301
+ }
302
+ if (filters.string) {
303
+ iterate(filters.string, (key, value) => {
304
+ const fnc = allOr ? "orWhere" : "where"
305
+ // postgres supports ilike, nothing else does
306
+ if (this.client === SqlClient.POSTGRES) {
307
+ query = query[fnc](key, "ilike", `${value}%`)
308
+ } else {
309
+ const rawFnc = `${fnc}Raw`
310
+ // @ts-ignore
311
+ query = query[rawFnc](`LOWER(${likeKey(this.client, key)}) LIKE ?`, [
312
+ `${value.toLowerCase()}%`,
313
+ ])
314
+ }
315
+ })
316
+ }
317
+ if (filters.fuzzy) {
318
+ iterate(filters.fuzzy, like)
319
+ }
320
+ if (filters.range) {
321
+ iterate(filters.range, (key, value) => {
322
+ const isEmptyObject = (val: any) => {
323
+ return (
324
+ val &&
325
+ Object.keys(val).length === 0 &&
326
+ Object.getPrototypeOf(val) === Object.prototype
327
+ )
328
+ }
329
+ if (isEmptyObject(value.low)) {
330
+ value.low = ""
331
+ }
332
+ if (isEmptyObject(value.high)) {
333
+ value.high = ""
334
+ }
335
+ const lowValid = isValidFilter(value.low),
336
+ highValid = isValidFilter(value.high)
337
+ if (lowValid && highValid) {
338
+ query = sqlStatements.between(query, key, value.low, value.high)
339
+ } else if (lowValid) {
340
+ query = sqlStatements.lte(query, key, value.low)
341
+ } else if (highValid) {
342
+ query = sqlStatements.gte(query, key, value.high)
343
+ }
344
+ })
345
+ }
346
+ if (filters.equal) {
347
+ iterate(filters.equal, (key, value) => {
348
+ const fnc = allOr ? "orWhereRaw" : "whereRaw"
349
+ if (this.client === SqlClient.MS_SQL) {
350
+ query = query[fnc](
351
+ `CASE WHEN ${likeKey(this.client, key)} = ? THEN 1 ELSE 0 END = 1`,
352
+ [value]
353
+ )
354
+ } else {
355
+ query = query[fnc](
356
+ `COALESCE(${likeKey(this.client, key)} = ?, FALSE)`,
357
+ [value]
358
+ )
359
+ }
360
+ })
361
+ }
362
+ if (filters.notEqual) {
363
+ iterate(filters.notEqual, (key, value) => {
364
+ const fnc = allOr ? "orWhereRaw" : "whereRaw"
365
+ if (this.client === SqlClient.MS_SQL) {
366
+ query = query[fnc](
367
+ `CASE WHEN ${likeKey(this.client, key)} = ? THEN 1 ELSE 0 END = 0`,
368
+ [value]
369
+ )
370
+ } else {
371
+ query = query[fnc](
372
+ `COALESCE(${likeKey(this.client, key)} != ?, TRUE)`,
373
+ [value]
374
+ )
375
+ }
376
+ })
377
+ }
378
+ if (filters.empty) {
379
+ iterate(filters.empty, key => {
380
+ const fnc = allOr ? "orWhereNull" : "whereNull"
381
+ query = query[fnc](key)
382
+ })
383
+ }
384
+ if (filters.notEmpty) {
385
+ iterate(filters.notEmpty, key => {
386
+ const fnc = allOr ? "orWhereNotNull" : "whereNotNull"
387
+ query = query[fnc](key)
388
+ })
389
+ }
390
+ if (filters.contains) {
391
+ contains(filters.contains)
392
+ }
393
+ if (filters.notContains) {
394
+ contains(filters.notContains)
395
+ }
396
+ if (filters.containsAny) {
397
+ contains(filters.containsAny, true)
398
+ }
399
+
400
+ // when searching internal tables make sure long looking for rows
401
+ if (filters.documentType && !isExternalTable(table)) {
402
+ const tableRef = opts?.aliases?.[table._id!] || table._id
403
+ // has to be its own option, must always be AND onto the search
404
+ query.andWhereLike(
405
+ `${tableRef}._id`,
406
+ `${prefixed(filters.documentType)}%`
407
+ )
408
+ }
409
+
410
+ return query
411
+ }
412
+
413
+ addSorting(query: Knex.QueryBuilder, json: QueryJson): Knex.QueryBuilder {
414
+ let { sort, paginate } = json
415
+ const table = json.meta.table
416
+ const tableName = getTableName(table)
417
+ const aliases = json.tableAliases
418
+ const aliased =
419
+ tableName && aliases?.[tableName] ? aliases[tableName] : table?.name
420
+ if (sort && Object.keys(sort || {}).length > 0) {
421
+ for (let [key, value] of Object.entries(sort)) {
422
+ const direction =
423
+ value.direction === SortDirection.ASCENDING ? "asc" : "desc"
424
+ let nulls
425
+ if (this.client === SqlClient.POSTGRES) {
426
+ // All other clients already sort this as expected by default, and adding this to the rest of the clients is causing issues
427
+ nulls = value.direction === SortDirection.ASCENDING ? "first" : "last"
428
+ }
429
+
430
+ query = query.orderBy(`${aliased}.${key}`, direction, nulls)
431
+ }
432
+ } else if (this.client === SqlClient.MS_SQL && paginate?.limit) {
433
+ // @ts-ignore
434
+ query = query.orderBy(`${aliased}.${table?.primary[0]}`)
435
+ }
436
+ return query
437
+ }
438
+
439
+ tableNameWithSchema(
440
+ tableName: string,
441
+ opts?: { alias?: string; schema?: string }
442
+ ) {
443
+ let withSchema = opts?.schema ? `${opts.schema}.${tableName}` : tableName
444
+ if (opts?.alias) {
445
+ withSchema += ` as ${opts.alias}`
446
+ }
447
+ return withSchema
448
+ }
449
+
450
+ addRelationships(
451
+ query: Knex.QueryBuilder,
452
+ fromTable: string,
453
+ relationships: RelationshipsJson[] | undefined,
454
+ schema: string | undefined,
455
+ aliases?: Record<string, string>
456
+ ): Knex.QueryBuilder {
457
+ if (!relationships) {
458
+ return query
459
+ }
460
+ const tableSets: Record<string, [RelationshipsJson]> = {}
461
+ // aggregate into table sets (all the same to tables)
462
+ for (let relationship of relationships) {
463
+ const keyObj: { toTable: string; throughTable: string | undefined } = {
464
+ toTable: relationship.tableName,
465
+ throughTable: undefined,
466
+ }
467
+ if (relationship.through) {
468
+ keyObj.throughTable = relationship.through
469
+ }
470
+ const key = JSON.stringify(keyObj)
471
+ if (tableSets[key]) {
472
+ tableSets[key].push(relationship)
473
+ } else {
474
+ tableSets[key] = [relationship]
475
+ }
476
+ }
477
+ for (let [key, relationships] of Object.entries(tableSets)) {
478
+ const { toTable, throughTable } = JSON.parse(key)
479
+ const toAlias = aliases?.[toTable] || toTable,
480
+ throughAlias = aliases?.[throughTable] || throughTable,
481
+ fromAlias = aliases?.[fromTable] || fromTable
482
+ let toTableWithSchema = this.tableNameWithSchema(toTable, {
483
+ alias: toAlias,
484
+ schema,
485
+ })
486
+ let throughTableWithSchema = this.tableNameWithSchema(throughTable, {
487
+ alias: throughAlias,
488
+ schema,
489
+ })
490
+ if (!throughTable) {
491
+ // @ts-ignore
492
+ query = query.leftJoin(toTableWithSchema, function () {
493
+ for (let relationship of relationships) {
494
+ const from = relationship.from,
495
+ to = relationship.to
496
+ // @ts-ignore
497
+ this.orOn(`${fromAlias}.${from}`, "=", `${toAlias}.${to}`)
498
+ }
499
+ })
500
+ } else {
501
+ query = query
502
+ // @ts-ignore
503
+ .leftJoin(throughTableWithSchema, function () {
504
+ for (let relationship of relationships) {
505
+ const fromPrimary = relationship.fromPrimary
506
+ const from = relationship.from
507
+ // @ts-ignore
508
+ this.orOn(
509
+ `${fromAlias}.${fromPrimary}`,
510
+ "=",
511
+ `${throughAlias}.${from}`
512
+ )
513
+ }
514
+ })
515
+ .leftJoin(toTableWithSchema, function () {
516
+ for (let relationship of relationships) {
517
+ const toPrimary = relationship.toPrimary
518
+ const to = relationship.to
519
+ // @ts-ignore
520
+ this.orOn(`${toAlias}.${toPrimary}`, `${throughAlias}.${to}`)
521
+ }
522
+ })
523
+ }
524
+ }
525
+ return query.limit(BASE_LIMIT)
526
+ }
527
+
528
+ knexWithAlias(
529
+ knex: Knex,
530
+ endpoint: QueryJson["endpoint"],
531
+ aliases?: QueryJson["tableAliases"]
532
+ ): Knex.QueryBuilder {
533
+ const tableName = endpoint.entityId
534
+ const tableAlias = aliases?.[tableName]
535
+
536
+ const query = knex(
537
+ this.tableNameWithSchema(tableName, {
538
+ alias: tableAlias,
539
+ schema: endpoint.schema,
540
+ })
541
+ )
542
+ return query
543
+ }
544
+
545
+ create(knex: Knex, json: QueryJson, opts: QueryOptions): Knex.QueryBuilder {
546
+ const { endpoint, body } = json
547
+ let query = this.knexWithAlias(knex, endpoint)
548
+ const parsedBody = parseBody(body)
549
+ // make sure no null values in body for creation
550
+ for (let [key, value] of Object.entries(parsedBody)) {
551
+ if (value == null) {
552
+ delete parsedBody[key]
553
+ }
554
+ }
555
+
556
+ // mysql can't use returning
557
+ if (opts.disableReturning) {
558
+ return query.insert(parsedBody)
559
+ } else {
560
+ return query.insert(parsedBody).returning("*")
561
+ }
562
+ }
563
+
564
+ bulkCreate(knex: Knex, json: QueryJson): Knex.QueryBuilder {
565
+ const { endpoint, body } = json
566
+ let query = this.knexWithAlias(knex, endpoint)
567
+ if (!Array.isArray(body)) {
568
+ return query
569
+ }
570
+ const parsedBody = body.map(row => parseBody(row))
571
+ return query.insert(parsedBody)
572
+ }
573
+
574
+ read(knex: Knex, json: QueryJson, limit: number): Knex.QueryBuilder {
575
+ let { endpoint, resource, filters, paginate, relationships, tableAliases } =
576
+ json
577
+
578
+ const tableName = endpoint.entityId
579
+ // select all if not specified
580
+ if (!resource) {
581
+ resource = { fields: [] }
582
+ }
583
+ let selectStatement: string | (string | Knex.Raw)[] = "*"
584
+ // handle select
585
+ if (resource.fields && resource.fields.length > 0) {
586
+ // select the resources as the format "table.columnName" - this is what is provided
587
+ // by the resource builder further up
588
+ selectStatement = generateSelectStatement(json, knex)
589
+ }
590
+ let foundLimit = limit || BASE_LIMIT
591
+ // handle pagination
592
+ let foundOffset: number | null = null
593
+ if (paginate && paginate.page && paginate.limit) {
594
+ // @ts-ignore
595
+ const page = paginate.page <= 1 ? 0 : paginate.page - 1
596
+ const offset = page * paginate.limit
597
+ foundLimit = paginate.limit
598
+ foundOffset = offset
599
+ } else if (paginate && paginate.limit) {
600
+ foundLimit = paginate.limit
601
+ }
602
+ // start building the query
603
+ let query = this.knexWithAlias(knex, endpoint, tableAliases)
604
+ query = query.limit(foundLimit)
605
+ if (foundOffset) {
606
+ query = query.offset(foundOffset)
607
+ }
608
+ query = this.addFilters(query, filters, json.meta.table, {
609
+ aliases: tableAliases,
610
+ })
611
+
612
+ // add sorting to pre-query
613
+ query = this.addSorting(query, json)
614
+ const alias = tableAliases?.[tableName] || tableName
615
+ let preQuery = knex({
616
+ [alias]: query,
617
+ } as any).select(selectStatement) as any
618
+ // have to add after as well (this breaks MS-SQL)
619
+ if (this.client !== SqlClient.MS_SQL) {
620
+ preQuery = this.addSorting(preQuery, json)
621
+ }
622
+ // handle joins
623
+ query = this.addRelationships(
624
+ preQuery,
625
+ tableName,
626
+ relationships,
627
+ endpoint.schema,
628
+ tableAliases
629
+ )
630
+ return this.addFilters(query, filters, json.meta.table, {
631
+ relationship: true,
632
+ aliases: tableAliases,
633
+ })
634
+ }
635
+
636
+ update(knex: Knex, json: QueryJson, opts: QueryOptions): Knex.QueryBuilder {
637
+ const { endpoint, body, filters, tableAliases } = json
638
+ let query = this.knexWithAlias(knex, endpoint, tableAliases)
639
+ const parsedBody = parseBody(body)
640
+ query = this.addFilters(query, filters, json.meta.table, {
641
+ aliases: tableAliases,
642
+ })
643
+ // mysql can't use returning
644
+ if (opts.disableReturning) {
645
+ return query.update(parsedBody)
646
+ } else {
647
+ return query.update(parsedBody).returning("*")
648
+ }
649
+ }
650
+
651
+ delete(knex: Knex, json: QueryJson, opts: QueryOptions): Knex.QueryBuilder {
652
+ const { endpoint, filters, tableAliases } = json
653
+ let query = this.knexWithAlias(knex, endpoint, tableAliases)
654
+ query = this.addFilters(query, filters, json.meta.table, {
655
+ aliases: tableAliases,
656
+ })
657
+ // mysql can't use returning
658
+ if (opts.disableReturning) {
659
+ return query.delete()
660
+ } else {
661
+ return query.delete().returning(generateSelectStatement(json, knex))
662
+ }
663
+ }
664
+ }
665
+
666
+ class SqlQueryBuilder extends SqlTableQueryBuilder {
667
+ private readonly limit: number
668
+ // pass through client to get flavour of SQL
669
+ constructor(client: string, limit: number = BASE_LIMIT) {
670
+ super(client)
671
+ this.limit = limit
672
+ }
673
+
674
+ /**
675
+ * @param json The JSON query DSL which is to be converted to SQL.
676
+ * @param opts extra options which are to be passed into the query builder, e.g. disableReturning
677
+ * which for the sake of mySQL stops adding the returning statement to inserts, updates and deletes.
678
+ * @return the query ready to be passed to the driver.
679
+ */
680
+ _query(json: QueryJson, opts: QueryOptions = {}): SqlQuery | SqlQuery[] {
681
+ const sqlClient = this.getSqlClient()
682
+ const config: Knex.Config = {
683
+ client: sqlClient,
684
+ }
685
+ if (sqlClient === SqlClient.SQL_LITE) {
686
+ config.useNullAsDefault = true
687
+ }
688
+
689
+ const client = knex(config)
690
+ let query: Knex.QueryBuilder
691
+ const builder = new InternalBuilder(sqlClient)
692
+ switch (this._operation(json)) {
693
+ case Operation.CREATE:
694
+ query = builder.create(client, json, opts)
695
+ break
696
+ case Operation.READ:
697
+ query = builder.read(client, json, this.limit)
698
+ break
699
+ case Operation.UPDATE:
700
+ query = builder.update(client, json, opts)
701
+ break
702
+ case Operation.DELETE:
703
+ query = builder.delete(client, json, opts)
704
+ break
705
+ case Operation.BULK_CREATE:
706
+ query = builder.bulkCreate(client, json)
707
+ break
708
+ case Operation.CREATE_TABLE:
709
+ case Operation.UPDATE_TABLE:
710
+ case Operation.DELETE_TABLE:
711
+ return this._tableQuery(json)
712
+ default:
713
+ throw `Operation type is not supported by SQL query builder`
714
+ }
715
+
716
+ if (opts?.disableBindings) {
717
+ return { sql: query.toString() }
718
+ } else {
719
+ let native = getNativeSql(query)
720
+ if (sqlClient === SqlClient.SQL_LITE) {
721
+ native = convertBooleans(native)
722
+ }
723
+ return native
724
+ }
725
+ }
726
+
727
+ async getReturningRow(queryFn: QueryFunction, json: QueryJson) {
728
+ if (!json.extra || !json.extra.idFilter) {
729
+ return {}
730
+ }
731
+ const input = this._query({
732
+ endpoint: {
733
+ ...json.endpoint,
734
+ operation: Operation.READ,
735
+ },
736
+ resource: {
737
+ fields: [],
738
+ },
739
+ filters: json.extra?.idFilter,
740
+ paginate: {
741
+ limit: 1,
742
+ },
743
+ meta: json.meta,
744
+ })
745
+ return queryFn(input, Operation.READ)
746
+ }
747
+
748
+ // when creating if an ID has been inserted need to make sure
749
+ // the id filter is enriched with it before trying to retrieve the row
750
+ checkLookupKeys(id: any, json: QueryJson) {
751
+ if (!id || !json.meta.table || !json.meta.table.primary) {
752
+ return json
753
+ }
754
+ const primaryKey = json.meta.table.primary?.[0]
755
+ json.extra = {
756
+ idFilter: {
757
+ equal: {
758
+ [primaryKey]: id,
759
+ },
760
+ },
761
+ }
762
+ return json
763
+ }
764
+
765
+ // this function recreates the returning functionality of postgres
766
+ async queryWithReturning(
767
+ json: QueryJson,
768
+ queryFn: QueryFunction,
769
+ processFn: Function = (result: any) => result
770
+ ) {
771
+ const sqlClient = this.getSqlClient()
772
+ const operation = this._operation(json)
773
+ const input = this._query(json, { disableReturning: true })
774
+ if (Array.isArray(input)) {
775
+ const responses = []
776
+ for (let query of input) {
777
+ responses.push(await queryFn(query, operation))
778
+ }
779
+ return responses
780
+ }
781
+ let row
782
+ // need to manage returning, a feature mySQL can't do
783
+ if (operation === Operation.DELETE) {
784
+ row = processFn(await this.getReturningRow(queryFn, json))
785
+ }
786
+ const response = await queryFn(input, operation)
787
+ const results = processFn(response)
788
+ // same as delete, manage returning
789
+ if (operation === Operation.CREATE || operation === Operation.UPDATE) {
790
+ let id
791
+ if (sqlClient === SqlClient.MS_SQL) {
792
+ id = results?.[0].id
793
+ } else if (sqlClient === SqlClient.MY_SQL) {
794
+ id = results?.insertId
795
+ }
796
+ row = processFn(
797
+ await this.getReturningRow(queryFn, this.checkLookupKeys(id, json))
798
+ )
799
+ }
800
+ if (operation !== Operation.READ) {
801
+ return row
802
+ }
803
+ return results.length ? results : [{ [operation.toLowerCase()]: true }]
804
+ }
805
+
806
+ convertJsonStringColumns<T extends Record<string, any>>(
807
+ table: Table,
808
+ results: T[],
809
+ aliases?: Record<string, string>
810
+ ): T[] {
811
+ const tableName = getTableName(table)
812
+ for (const [name, field] of Object.entries(table.schema)) {
813
+ if (!this._isJsonColumn(field)) {
814
+ continue
815
+ }
816
+ const aliasedTableName = (tableName && aliases?.[tableName]) || tableName
817
+ const fullName = `${aliasedTableName}.${name}`
818
+ for (let row of results) {
819
+ if (typeof row[fullName as keyof T] === "string") {
820
+ row[fullName as keyof T] = JSON.parse(row[fullName])
821
+ }
822
+ if (typeof row[name as keyof T] === "string") {
823
+ row[name as keyof T] = JSON.parse(row[name])
824
+ }
825
+ }
826
+ }
827
+ return results
828
+ }
829
+
830
+ _isJsonColumn(
831
+ field: FieldSchema
832
+ ): field is JsonFieldMetadata | BBReferenceFieldMetadata {
833
+ return (
834
+ JsonTypes.includes(field.type) &&
835
+ !helpers.schema.isDeprecatedSingleUserColumn(field)
836
+ )
837
+ }
838
+
839
+ log(query: string, values?: SqlQueryBinding) {
840
+ if (!environment.SQL_LOGGING_ENABLE) {
841
+ return
842
+ }
843
+ const sqlClient = this.getSqlClient()
844
+ let string = `[SQL] [${sqlClient.toUpperCase()}] query="${query}"`
845
+ if (values) {
846
+ string += ` values="${values.join(", ")}"`
847
+ }
848
+ console.log(string)
849
+ }
850
+ }
851
+
852
+ export default SqlQueryBuilder