@budibase/backend-core 3.2.4 → 3.2.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 (272) hide show
  1. package/dist/index.js.map +1 -1
  2. package/dist/index.js.meta.json +1 -1
  3. package/dist/package.json +11 -4
  4. package/dist/plugins.js.meta.json +1 -1
  5. package/package.json +11 -4
  6. package/src/accounts/accounts.ts +0 -82
  7. package/src/accounts/api.ts +0 -59
  8. package/src/accounts/index.ts +0 -1
  9. package/src/auth/auth.ts +0 -210
  10. package/src/auth/index.ts +0 -1
  11. package/src/auth/tests/auth.spec.ts +0 -14
  12. package/src/blacklist/blacklist.ts +0 -54
  13. package/src/blacklist/index.ts +0 -1
  14. package/src/blacklist/tests/blacklist.spec.ts +0 -46
  15. package/src/cache/appMetadata.ts +0 -88
  16. package/src/cache/base/index.ts +0 -150
  17. package/src/cache/docWritethrough.ts +0 -105
  18. package/src/cache/generic.ts +0 -33
  19. package/src/cache/index.ts +0 -8
  20. package/src/cache/invite.ts +0 -86
  21. package/src/cache/passwordReset.ts +0 -49
  22. package/src/cache/tests/docWritethrough.spec.ts +0 -296
  23. package/src/cache/tests/user.spec.ts +0 -145
  24. package/src/cache/tests/writethrough.spec.ts +0 -139
  25. package/src/cache/user.ts +0 -154
  26. package/src/cache/writethrough.ts +0 -133
  27. package/src/configs/configs.ts +0 -263
  28. package/src/configs/index.ts +0 -1
  29. package/src/configs/tests/configs.spec.ts +0 -184
  30. package/src/constants/db.ts +0 -75
  31. package/src/constants/index.ts +0 -2
  32. package/src/constants/misc.ts +0 -36
  33. package/src/context/Context.ts +0 -14
  34. package/src/context/identity.ts +0 -58
  35. package/src/context/index.ts +0 -3
  36. package/src/context/mainContext.ts +0 -422
  37. package/src/context/tests/index.spec.ts +0 -255
  38. package/src/context/types.ts +0 -26
  39. package/src/db/Replication.ts +0 -94
  40. package/src/db/couch/DatabaseImpl.ts +0 -511
  41. package/src/db/couch/connections.ts +0 -89
  42. package/src/db/couch/index.ts +0 -4
  43. package/src/db/couch/pouchDB.ts +0 -97
  44. package/src/db/couch/pouchDump.ts +0 -0
  45. package/src/db/couch/tests/DatabaseImpl.spec.ts +0 -118
  46. package/src/db/couch/utils.ts +0 -55
  47. package/src/db/db.ts +0 -34
  48. package/src/db/errors.ts +0 -14
  49. package/src/db/index.ts +0 -12
  50. package/src/db/instrumentation.ts +0 -199
  51. package/src/db/lucene.ts +0 -721
  52. package/src/db/searchIndexes/index.ts +0 -1
  53. package/src/db/searchIndexes/searchIndexes.ts +0 -62
  54. package/src/db/tests/DatabaseImpl.spec.ts +0 -55
  55. package/src/db/tests/connections.spec.ts +0 -22
  56. package/src/db/tests/index.spec.ts +0 -32
  57. package/src/db/tests/lucene.spec.ts +0 -400
  58. package/src/db/tests/pouch.spec.js +0 -62
  59. package/src/db/tests/utils.spec.ts +0 -63
  60. package/src/db/utils.ts +0 -208
  61. package/src/db/views.ts +0 -245
  62. package/src/docIds/conversions.ts +0 -60
  63. package/src/docIds/ids.ts +0 -126
  64. package/src/docIds/index.ts +0 -2
  65. package/src/docIds/newid.ts +0 -5
  66. package/src/docIds/params.ts +0 -189
  67. package/src/docUpdates/index.ts +0 -24
  68. package/src/environment.ts +0 -293
  69. package/src/errors/errors.ts +0 -119
  70. package/src/errors/index.ts +0 -1
  71. package/src/events/analytics.ts +0 -6
  72. package/src/events/asyncEvents/index.ts +0 -2
  73. package/src/events/asyncEvents/publisher.ts +0 -12
  74. package/src/events/asyncEvents/queue.ts +0 -22
  75. package/src/events/backfill.ts +0 -183
  76. package/src/events/documentId.ts +0 -56
  77. package/src/events/events.ts +0 -47
  78. package/src/events/identification.ts +0 -311
  79. package/src/events/index.ts +0 -15
  80. package/src/events/processors/AnalyticsProcessor.ts +0 -64
  81. package/src/events/processors/AuditLogsProcessor.ts +0 -92
  82. package/src/events/processors/LoggingProcessor.ts +0 -36
  83. package/src/events/processors/Processors.ts +0 -52
  84. package/src/events/processors/async/DocumentUpdateProcessor.ts +0 -38
  85. package/src/events/processors/index.ts +0 -19
  86. package/src/events/processors/posthog/PosthogProcessor.ts +0 -118
  87. package/src/events/processors/posthog/index.ts +0 -3
  88. package/src/events/processors/posthog/rateLimiting.ts +0 -106
  89. package/src/events/processors/posthog/tests/PosthogProcessor.spec.ts +0 -164
  90. package/src/events/processors/types.ts +0 -1
  91. package/src/events/publishers/account.ts +0 -41
  92. package/src/events/publishers/ai.ts +0 -21
  93. package/src/events/publishers/app.ts +0 -168
  94. package/src/events/publishers/auditLog.ts +0 -26
  95. package/src/events/publishers/auth.ts +0 -73
  96. package/src/events/publishers/automation.ts +0 -110
  97. package/src/events/publishers/backfill.ts +0 -74
  98. package/src/events/publishers/backup.ts +0 -42
  99. package/src/events/publishers/datasource.ts +0 -48
  100. package/src/events/publishers/email.ts +0 -17
  101. package/src/events/publishers/environmentVariable.ts +0 -38
  102. package/src/events/publishers/group.ts +0 -99
  103. package/src/events/publishers/index.ts +0 -25
  104. package/src/events/publishers/installation.ts +0 -38
  105. package/src/events/publishers/layout.ts +0 -26
  106. package/src/events/publishers/license.ts +0 -84
  107. package/src/events/publishers/org.ts +0 -37
  108. package/src/events/publishers/plugin.ts +0 -47
  109. package/src/events/publishers/query.ts +0 -89
  110. package/src/events/publishers/role.ts +0 -62
  111. package/src/events/publishers/rows.ts +0 -29
  112. package/src/events/publishers/screen.ts +0 -36
  113. package/src/events/publishers/serve.ts +0 -43
  114. package/src/events/publishers/table.ts +0 -70
  115. package/src/events/publishers/user.ts +0 -202
  116. package/src/events/publishers/view.ts +0 -107
  117. package/src/features/features.ts +0 -277
  118. package/src/features/index.ts +0 -2
  119. package/src/features/tests/features.spec.ts +0 -267
  120. package/src/features/tests/utils.ts +0 -64
  121. package/src/helpers.ts +0 -9
  122. package/src/index.ts +0 -59
  123. package/src/installation.ts +0 -115
  124. package/src/logging/alerts.ts +0 -26
  125. package/src/logging/correlation/correlation.ts +0 -15
  126. package/src/logging/correlation/index.ts +0 -1
  127. package/src/logging/correlation/middleware.ts +0 -18
  128. package/src/logging/index.ts +0 -4
  129. package/src/logging/pino/logger.ts +0 -239
  130. package/src/logging/pino/middleware.ts +0 -48
  131. package/src/logging/system.ts +0 -81
  132. package/src/logging/tests/system.spec.ts +0 -61
  133. package/src/middleware/adminOnly.ts +0 -9
  134. package/src/middleware/auditLog.ts +0 -6
  135. package/src/middleware/authenticated.ts +0 -247
  136. package/src/middleware/builderOnly.ts +0 -21
  137. package/src/middleware/builderOrAdmin.ts +0 -21
  138. package/src/middleware/contentSecurityPolicy.ts +0 -113
  139. package/src/middleware/csrf.ts +0 -81
  140. package/src/middleware/errorHandling.ts +0 -43
  141. package/src/middleware/index.ts +0 -24
  142. package/src/middleware/internalApi.ts +0 -23
  143. package/src/middleware/ip.ts +0 -12
  144. package/src/middleware/joi-validator.ts +0 -58
  145. package/src/middleware/matchers.ts +0 -39
  146. package/src/middleware/passport/datasource/google.ts +0 -102
  147. package/src/middleware/passport/local.ts +0 -54
  148. package/src/middleware/passport/sso/google.ts +0 -77
  149. package/src/middleware/passport/sso/oidc.ts +0 -152
  150. package/src/middleware/passport/sso/sso.ts +0 -138
  151. package/src/middleware/passport/sso/tests/google.spec.ts +0 -68
  152. package/src/middleware/passport/sso/tests/oidc.spec.ts +0 -144
  153. package/src/middleware/passport/sso/tests/sso.spec.ts +0 -197
  154. package/src/middleware/passport/utils.ts +0 -38
  155. package/src/middleware/querystringToBody.ts +0 -28
  156. package/src/middleware/tenancy.ts +0 -36
  157. package/src/middleware/tests/builder.spec.ts +0 -181
  158. package/src/middleware/tests/contentSecurityPolicy.spec.ts +0 -75
  159. package/src/middleware/tests/matchers.spec.ts +0 -100
  160. package/src/migrations/definitions.ts +0 -40
  161. package/src/migrations/index.ts +0 -2
  162. package/src/migrations/migrations.ts +0 -186
  163. package/src/migrations/tests/__snapshots__/migrations.spec.ts.snap +0 -11
  164. package/src/migrations/tests/migrations.spec.ts +0 -64
  165. package/src/objectStore/buckets/app.ts +0 -53
  166. package/src/objectStore/buckets/global.ts +0 -29
  167. package/src/objectStore/buckets/index.ts +0 -3
  168. package/src/objectStore/buckets/plugins.ts +0 -71
  169. package/src/objectStore/buckets/tests/app.spec.ts +0 -161
  170. package/src/objectStore/buckets/tests/global.spec.ts +0 -74
  171. package/src/objectStore/buckets/tests/plugins.spec.ts +0 -111
  172. package/src/objectStore/cloudfront.ts +0 -41
  173. package/src/objectStore/index.ts +0 -3
  174. package/src/objectStore/objectStore.ts +0 -585
  175. package/src/objectStore/utils.ts +0 -113
  176. package/src/platform/index.ts +0 -3
  177. package/src/platform/platformDb.ts +0 -6
  178. package/src/platform/tenants.ts +0 -101
  179. package/src/platform/tests/tenants.spec.ts +0 -26
  180. package/src/platform/users.ts +0 -129
  181. package/src/plugin/index.ts +0 -1
  182. package/src/plugin/tests/validation.spec.ts +0 -209
  183. package/src/plugin/utils.ts +0 -175
  184. package/src/queue/constants.ts +0 -8
  185. package/src/queue/inMemoryQueue.ts +0 -189
  186. package/src/queue/index.ts +0 -2
  187. package/src/queue/listeners.ts +0 -199
  188. package/src/queue/queue.ts +0 -84
  189. package/src/redis/index.ts +0 -6
  190. package/src/redis/init.ts +0 -118
  191. package/src/redis/redis.ts +0 -358
  192. package/src/redis/redlockImpl.ts +0 -155
  193. package/src/redis/tests/redis.spec.ts +0 -207
  194. package/src/redis/tests/redlockImpl.spec.ts +0 -105
  195. package/src/redis/utils.ts +0 -128
  196. package/src/security/auth.ts +0 -24
  197. package/src/security/encryption.ts +0 -185
  198. package/src/security/index.ts +0 -1
  199. package/src/security/permissions.ts +0 -166
  200. package/src/security/roles.ts +0 -655
  201. package/src/security/secrets.ts +0 -20
  202. package/src/security/sessions.ts +0 -123
  203. package/src/security/tests/auth.spec.ts +0 -45
  204. package/src/security/tests/encryption.spec.ts +0 -31
  205. package/src/security/tests/permissions.spec.ts +0 -146
  206. package/src/security/tests/secrets.spec.ts +0 -35
  207. package/src/security/tests/sessions.spec.ts +0 -12
  208. package/src/sql/designDoc.ts +0 -17
  209. package/src/sql/index.ts +0 -5
  210. package/src/sql/sql.ts +0 -1854
  211. package/src/sql/sqlTable.ts +0 -319
  212. package/src/sql/utils.ts +0 -193
  213. package/src/tenancy/db.ts +0 -6
  214. package/src/tenancy/index.ts +0 -2
  215. package/src/tenancy/tenancy.ts +0 -148
  216. package/src/tenancy/tests/tenancy.spec.ts +0 -184
  217. package/src/timers/index.ts +0 -1
  218. package/src/timers/timers.ts +0 -22
  219. package/src/users/db.ts +0 -582
  220. package/src/users/events.ts +0 -176
  221. package/src/users/index.ts +0 -4
  222. package/src/users/lookup.ts +0 -99
  223. package/src/users/test/db.spec.ts +0 -188
  224. package/src/users/test/utils.spec.ts +0 -67
  225. package/src/users/users.ts +0 -353
  226. package/src/users/utils.ts +0 -81
  227. package/src/utils/Duration.ts +0 -56
  228. package/src/utils/hashing.ts +0 -15
  229. package/src/utils/index.ts +0 -4
  230. package/src/utils/stringUtils.ts +0 -8
  231. package/src/utils/tests/Duration.spec.ts +0 -19
  232. package/src/utils/tests/utils.spec.ts +0 -204
  233. package/src/utils/utils.ts +0 -249
  234. package/tests/core/logging.ts +0 -34
  235. package/tests/core/users/users.spec.js +0 -53
  236. package/tests/core/utilities/index.ts +0 -7
  237. package/tests/core/utilities/jestUtils.ts +0 -33
  238. package/tests/core/utilities/mocks/alerts.ts +0 -4
  239. package/tests/core/utilities/mocks/date.ts +0 -3
  240. package/tests/core/utilities/mocks/events.ts +0 -132
  241. package/tests/core/utilities/mocks/index.ts +0 -9
  242. package/tests/core/utilities/mocks/licenses.ts +0 -119
  243. package/tests/core/utilities/queue.ts +0 -9
  244. package/tests/core/utilities/structures/Chance.ts +0 -20
  245. package/tests/core/utilities/structures/accounts.ts +0 -80
  246. package/tests/core/utilities/structures/apps.ts +0 -21
  247. package/tests/core/utilities/structures/common.ts +0 -7
  248. package/tests/core/utilities/structures/db.ts +0 -12
  249. package/tests/core/utilities/structures/documents/index.ts +0 -1
  250. package/tests/core/utilities/structures/documents/platform/index.ts +0 -1
  251. package/tests/core/utilities/structures/documents/platform/installation.ts +0 -12
  252. package/tests/core/utilities/structures/generator.ts +0 -3
  253. package/tests/core/utilities/structures/index.ts +0 -15
  254. package/tests/core/utilities/structures/koa.ts +0 -16
  255. package/tests/core/utilities/structures/licenses.ts +0 -190
  256. package/tests/core/utilities/structures/plugins.ts +0 -19
  257. package/tests/core/utilities/structures/quotas.ts +0 -72
  258. package/tests/core/utilities/structures/scim.ts +0 -80
  259. package/tests/core/utilities/structures/sso.ts +0 -118
  260. package/tests/core/utilities/structures/tenants.ts +0 -5
  261. package/tests/core/utilities/structures/userGroups.ts +0 -10
  262. package/tests/core/utilities/structures/users.ts +0 -89
  263. package/tests/core/utilities/testContainerUtils.ts +0 -165
  264. package/tests/core/utilities/utils/index.ts +0 -2
  265. package/tests/core/utilities/utils/queue.ts +0 -27
  266. package/tests/core/utilities/utils/time.ts +0 -3
  267. package/tests/extra/DBTestConfiguration.ts +0 -36
  268. package/tests/extra/index.ts +0 -2
  269. package/tests/extra/testEnv.ts +0 -95
  270. package/tests/index.ts +0 -2
  271. package/tests/jestEnv.ts +0 -10
  272. package/tests/jestSetup.ts +0 -36
package/src/sql/sql.ts DELETED
@@ -1,1854 +0,0 @@
1
- import { Knex, knex } from "knex"
2
- import * as dbCore from "../db"
3
- import {
4
- getNativeSql,
5
- isExternalTable,
6
- isInvalidISODateString,
7
- isValidFilter,
8
- isValidISODateString,
9
- sqlLog,
10
- validateManyToMany,
11
- } from "./utils"
12
- import SqlTableQueryBuilder from "./sqlTable"
13
- import {
14
- Aggregation,
15
- AnySearchFilter,
16
- ArrayFilter,
17
- ArrayOperator,
18
- BasicOperator,
19
- BBReferenceFieldMetadata,
20
- CalculationType,
21
- FieldSchema,
22
- FieldType,
23
- INTERNAL_TABLE_SOURCE_ID,
24
- InternalSearchFilterOperator,
25
- JsonFieldMetadata,
26
- JsonTypes,
27
- LogicalOperator,
28
- Operation,
29
- prefixed,
30
- QueryJson,
31
- QueryOptions,
32
- RangeOperator,
33
- RelationshipsJson,
34
- SearchFilterKey,
35
- SearchFilters,
36
- SortOrder,
37
- SqlClient,
38
- SqlQuery,
39
- SqlQueryBinding,
40
- Table,
41
- TableSourceType,
42
- } from "@budibase/types"
43
- import environment from "../environment"
44
- import { dataFilters, helpers } from "@budibase/shared-core"
45
- import { cloneDeep } from "lodash"
46
-
47
- type QueryFunction = (query: SqlQuery | SqlQuery[], operation: Operation) => any
48
-
49
- export const COUNT_FIELD_NAME = "__bb_total"
50
-
51
- function getBaseLimit() {
52
- const envLimit = environment.SQL_MAX_ROWS
53
- ? parseInt(environment.SQL_MAX_ROWS)
54
- : null
55
- return envLimit || 5000
56
- }
57
-
58
- function getRelationshipLimit() {
59
- const envLimit = environment.SQL_MAX_RELATED_ROWS
60
- ? parseInt(environment.SQL_MAX_RELATED_ROWS)
61
- : null
62
- return envLimit || 500
63
- }
64
-
65
- function prioritisedArraySort(toSort: string[], priorities: string[]) {
66
- return toSort.sort((a, b) => {
67
- const aPriority = priorities.find(field => field && a.endsWith(field))
68
- const bPriority = priorities.find(field => field && b.endsWith(field))
69
- if (aPriority && !bPriority) {
70
- return -1
71
- }
72
- if (!aPriority && bPriority) {
73
- return 1
74
- }
75
- return a.localeCompare(b)
76
- })
77
- }
78
-
79
- function convertBooleans(query: SqlQuery | SqlQuery[]): SqlQuery | SqlQuery[] {
80
- if (Array.isArray(query)) {
81
- return query.map((q: SqlQuery) => convertBooleans(q) as SqlQuery)
82
- } else {
83
- if (query.bindings) {
84
- query.bindings = query.bindings.map(binding => {
85
- if (typeof binding === "boolean") {
86
- return binding ? 1 : 0
87
- }
88
- return binding
89
- })
90
- }
91
- }
92
- return query
93
- }
94
-
95
- function isSqs(table: Table): boolean {
96
- return (
97
- table.sourceType === TableSourceType.INTERNAL ||
98
- table.sourceId === INTERNAL_TABLE_SOURCE_ID
99
- )
100
- }
101
-
102
- function escapeQuotes(value: string, quoteChar = '"'): string {
103
- return value.replace(new RegExp(quoteChar, "g"), `${quoteChar}${quoteChar}`)
104
- }
105
-
106
- function wrap(value: string, quoteChar = '"'): string {
107
- return `${quoteChar}${escapeQuotes(value, quoteChar)}${quoteChar}`
108
- }
109
-
110
- function stringifyArray(value: any[], quoteStyle = '"'): string {
111
- for (let i in value) {
112
- if (typeof value[i] === "string") {
113
- value[i] = wrap(value[i], quoteStyle)
114
- }
115
- }
116
- return `[${value.join(",")}]`
117
- }
118
-
119
- const allowEmptyRelationships: Record<SearchFilterKey, boolean> = {
120
- [BasicOperator.EQUAL]: false,
121
- [BasicOperator.NOT_EQUAL]: true,
122
- [BasicOperator.EMPTY]: false,
123
- [BasicOperator.NOT_EMPTY]: true,
124
- [BasicOperator.FUZZY]: false,
125
- [BasicOperator.STRING]: false,
126
- [RangeOperator.RANGE]: false,
127
- [ArrayOperator.CONTAINS]: false,
128
- [ArrayOperator.NOT_CONTAINS]: true,
129
- [ArrayOperator.CONTAINS_ANY]: false,
130
- [ArrayOperator.ONE_OF]: false,
131
- [LogicalOperator.AND]: false,
132
- [LogicalOperator.OR]: false,
133
- }
134
-
135
- class InternalBuilder {
136
- private readonly client: SqlClient
137
- private readonly query: QueryJson
138
- private readonly splitter: dataFilters.ColumnSplitter
139
- private readonly knex: Knex
140
-
141
- constructor(client: SqlClient, knex: Knex, query: QueryJson) {
142
- this.client = client
143
- this.query = query
144
- this.knex = knex
145
-
146
- this.splitter = new dataFilters.ColumnSplitter([this.table], {
147
- aliases: this.query.tableAliases,
148
- columnPrefix: this.query.meta.columnPrefix,
149
- })
150
- }
151
-
152
- // states the various situations in which we need a full mapped select statement
153
- private readonly SPECIAL_SELECT_CASES = {
154
- POSTGRES_MONEY: (field: FieldSchema | undefined) => {
155
- return (
156
- this.client === SqlClient.POSTGRES &&
157
- field?.externalType?.includes("money")
158
- )
159
- },
160
- MSSQL_DATES: (field: FieldSchema | undefined) => {
161
- return (
162
- this.client === SqlClient.MS_SQL &&
163
- field?.type === FieldType.DATETIME &&
164
- field.timeOnly
165
- )
166
- },
167
- }
168
-
169
- get table(): Table {
170
- return this.query.meta.table
171
- }
172
-
173
- get knexClient(): Knex.Client {
174
- return this.knex.client as Knex.Client
175
- }
176
-
177
- getFieldSchema(key: string): FieldSchema | undefined {
178
- const { column } = this.splitter.run(key)
179
- return this.table.schema[column]
180
- }
181
-
182
- private quoteChars(): [string, string] {
183
- const wrapped = this.knexClient.wrapIdentifier("foo", {})
184
- return [wrapped[0], wrapped[wrapped.length - 1]]
185
- }
186
-
187
- // Takes a string like foo and returns a quoted string like [foo] for SQL
188
- // Server and "foo" for Postgres.
189
- private quote(str: string): string {
190
- return this.knexClient.wrapIdentifier(str, {})
191
- }
192
-
193
- private isQuoted(key: string): boolean {
194
- const [start, end] = this.quoteChars()
195
- return key.startsWith(start) && key.endsWith(end)
196
- }
197
-
198
- // Takes a string like a.b.c or an array like ["a", "b", "c"] and returns a
199
- // quoted identifier like [a].[b].[c] for SQL Server and `a`.`b`.`c` for
200
- // MySQL.
201
- private quotedIdentifier(key: string | string[]): string {
202
- if (!Array.isArray(key)) {
203
- key = this.splitIdentifier(key)
204
- }
205
- return key.map(part => this.quote(part)).join(".")
206
- }
207
-
208
- private quotedValue(value: string): string {
209
- const formatter = this.knexClient.formatter(this.knexClient.queryBuilder())
210
- return formatter.wrap(value, false)
211
- }
212
-
213
- private castIntToString(identifier: string | Knex.Raw): Knex.Raw {
214
- switch (this.client) {
215
- case SqlClient.ORACLE: {
216
- return this.knex.raw("to_char(??)", [identifier])
217
- }
218
- case SqlClient.POSTGRES: {
219
- return this.knex.raw("??::TEXT", [identifier])
220
- }
221
- case SqlClient.MY_SQL:
222
- case SqlClient.MARIADB: {
223
- return this.knex.raw("CAST(?? AS CHAR)", [identifier])
224
- }
225
- case SqlClient.SQL_LITE: {
226
- // Technically sqlite can actually represent numbers larger than a 64bit
227
- // int as a string, but it does it using scientific notation (e.g.
228
- // "1e+20") which is not what we want. Given that the external SQL
229
- // databases are limited to supporting only 64bit ints, we settle for
230
- // that here.
231
- return this.knex.raw("printf('%d', ??)", [identifier])
232
- }
233
- case SqlClient.MS_SQL: {
234
- return this.knex.raw("CONVERT(NVARCHAR, ??)", [identifier])
235
- }
236
- }
237
- }
238
-
239
- // Unfortuantely we cannot rely on knex's identifier escaping because it trims
240
- // the identifier string before escaping it, which breaks cases for us where
241
- // columns that start or end with a space aren't referenced correctly anymore.
242
- //
243
- // So whenever you're using an identifier binding in knex, e.g. knex.raw("??
244
- // as ?", ["foo", "bar"]), you need to make sure you call this:
245
- //
246
- // knex.raw("?? as ?", [this.quotedIdentifier("foo"), "bar"])
247
- //
248
- // Issue we filed against knex about this:
249
- // https://github.com/knex/knex/issues/6143
250
- private rawQuotedIdentifier(key: string): Knex.Raw {
251
- return this.knex.raw(this.quotedIdentifier(key))
252
- }
253
-
254
- // Turns an identifier like a.b.c or `a`.`b`.`c` into ["a", "b", "c"]
255
- private splitIdentifier(key: string): string[] {
256
- const [start, end] = this.quoteChars()
257
- if (this.isQuoted(key)) {
258
- return key.slice(1, -1).split(`${end}.${start}`)
259
- }
260
- return key.split(".")
261
- }
262
-
263
- private qualifyIdentifier(key: string): string {
264
- const tableName = this.getTableName()
265
- const parts = this.splitIdentifier(key)
266
- if (parts[0] !== tableName) {
267
- parts.unshift(tableName)
268
- }
269
- if (this.isQuoted(key)) {
270
- return this.quotedIdentifier(parts)
271
- }
272
- return parts.join(".")
273
- }
274
-
275
- private isFullSelectStatementRequired(): boolean {
276
- const { meta } = this.query
277
- for (let column of Object.values(meta.table.schema)) {
278
- if (this.SPECIAL_SELECT_CASES.POSTGRES_MONEY(column)) {
279
- return true
280
- } else if (this.SPECIAL_SELECT_CASES.MSSQL_DATES(column)) {
281
- return true
282
- }
283
- }
284
- return false
285
- }
286
-
287
- private generateSelectStatement(): (string | Knex.Raw)[] | "*" {
288
- const { meta, endpoint, resource } = this.query
289
-
290
- if (!resource || !resource.fields || resource.fields.length === 0) {
291
- return "*"
292
- }
293
-
294
- const alias = this.getTableName(endpoint.entityId)
295
- const schema = meta.table.schema
296
- if (!this.isFullSelectStatementRequired()) {
297
- return [this.knex.raw("??", [`${alias}.*`])]
298
- }
299
- // get just the fields for this table
300
- return resource.fields
301
- .map(field => {
302
- const parts = field.split(/\./g)
303
- let table: string | undefined = undefined
304
- let column = parts[0]
305
-
306
- // Just a column name, e.g.: "column"
307
- if (parts.length > 1) {
308
- table = parts[0]
309
- column = parts.slice(1).join(".")
310
- }
311
-
312
- return { table, column, field }
313
- })
314
- .filter(({ table }) => !table || table === alias)
315
- .map(({ table, column, field }) => {
316
- const columnSchema = schema[column]
317
-
318
- if (this.SPECIAL_SELECT_CASES.POSTGRES_MONEY(columnSchema)) {
319
- return this.knex.raw(`??::money::numeric as ??`, [
320
- this.rawQuotedIdentifier([table, column].join(".")),
321
- this.knex.raw(this.quote(field)),
322
- ])
323
- }
324
-
325
- if (this.SPECIAL_SELECT_CASES.MSSQL_DATES(columnSchema)) {
326
- // Time gets returned as timestamp from mssql, not matching the expected
327
- // HH:mm format
328
-
329
- // TODO: figure out how to express this safely without string
330
- // interpolation.
331
- return this.knex.raw(`CONVERT(varchar, ??, 108) as ??`, [
332
- this.rawQuotedIdentifier(field),
333
- this.knex.raw(this.quote(field)),
334
- ])
335
- }
336
-
337
- if (table) {
338
- return this.rawQuotedIdentifier(`${table}.${column}`)
339
- } else {
340
- return this.rawQuotedIdentifier(field)
341
- }
342
- })
343
- }
344
-
345
- // OracleDB can't use character-large-objects (CLOBs) in WHERE clauses,
346
- // so when we use them we need to wrap them in to_char(). This function
347
- // converts a field name to the appropriate identifier.
348
- private convertClobs(
349
- field: string,
350
- opts?: { forSelect?: boolean }
351
- ): Knex.Raw {
352
- if (this.client !== SqlClient.ORACLE) {
353
- throw new Error(
354
- "you've called convertClobs on a DB that's not Oracle, this is a mistake"
355
- )
356
- }
357
- const parts = this.splitIdentifier(field)
358
- const col = parts.pop()!
359
- const schema = this.table.schema[col]
360
- let identifier = this.rawQuotedIdentifier(field)
361
-
362
- if (
363
- schema.type === FieldType.STRING ||
364
- schema.type === FieldType.LONGFORM ||
365
- schema.type === FieldType.BB_REFERENCE_SINGLE ||
366
- schema.type === FieldType.BB_REFERENCE ||
367
- schema.type === FieldType.OPTIONS ||
368
- schema.type === FieldType.BARCODEQR
369
- ) {
370
- if (opts?.forSelect) {
371
- identifier = this.knex.raw("to_char(??) as ??", [
372
- identifier,
373
- this.rawQuotedIdentifier(col),
374
- ])
375
- } else {
376
- identifier = this.knex.raw("to_char(??)", [identifier])
377
- }
378
- }
379
- return identifier
380
- }
381
-
382
- private parse(input: any, schema: FieldSchema) {
383
- if (Array.isArray(input)) {
384
- return JSON.stringify(input)
385
- }
386
- if (input == undefined) {
387
- return null
388
- }
389
-
390
- if (
391
- this.client === SqlClient.ORACLE &&
392
- schema.type === FieldType.DATETIME &&
393
- schema.timeOnly
394
- ) {
395
- if (input instanceof Date) {
396
- const hours = input.getHours().toString().padStart(2, "0")
397
- const minutes = input.getMinutes().toString().padStart(2, "0")
398
- const seconds = input.getSeconds().toString().padStart(2, "0")
399
- return `${hours}:${minutes}:${seconds}`
400
- }
401
- if (typeof input === "string") {
402
- return new Date(`1970-01-01T${input}Z`)
403
- }
404
- }
405
-
406
- if (typeof input === "string") {
407
- if (isInvalidISODateString(input)) {
408
- return null
409
- }
410
- if (isValidISODateString(input)) {
411
- return new Date(input.trim())
412
- }
413
- }
414
- return input
415
- }
416
-
417
- private parseBody(body: Record<string, any>) {
418
- for (let [key, value] of Object.entries(body)) {
419
- const { column } = this.splitter.run(key)
420
- const schema = this.table.schema[column]
421
- if (!schema) {
422
- continue
423
- }
424
- body[key] = this.parse(value, schema)
425
- }
426
- return body
427
- }
428
-
429
- private parseFilters(filters: SearchFilters): SearchFilters {
430
- filters = cloneDeep(filters)
431
- for (const op of Object.values(BasicOperator)) {
432
- const filter = filters[op]
433
- if (!filter) {
434
- continue
435
- }
436
- for (const key of Object.keys(filter)) {
437
- if (Array.isArray(filter[key])) {
438
- filter[key] = JSON.stringify(filter[key])
439
- continue
440
- }
441
- const { column } = this.splitter.run(key)
442
- const schema = this.table.schema[column]
443
- if (!schema) {
444
- continue
445
- }
446
- filter[key] = this.parse(filter[key], schema)
447
- }
448
- }
449
-
450
- for (const op of Object.values(ArrayOperator)) {
451
- const filter = filters[op]
452
- if (!filter) {
453
- continue
454
- }
455
- for (const key of Object.keys(filter)) {
456
- const { column } = this.splitter.run(key)
457
- const schema = this.table.schema[column]
458
- if (!schema) {
459
- continue
460
- }
461
- filter[key] = filter[key].map(v => this.parse(v, schema))
462
- }
463
- }
464
-
465
- for (const op of Object.values(RangeOperator)) {
466
- const filter = filters[op]
467
- if (!filter) {
468
- continue
469
- }
470
- for (const key of Object.keys(filter)) {
471
- const { column } = this.splitter.run(key)
472
- const schema = this.table.schema[column]
473
- if (!schema) {
474
- continue
475
- }
476
- const value = filter[key]
477
- if ("low" in value) {
478
- value.low = this.parse(value.low, schema)
479
- }
480
- if ("high" in value) {
481
- value.high = this.parse(value.high, schema)
482
- }
483
- }
484
- }
485
-
486
- return filters
487
- }
488
-
489
- addJoinFieldCheck(query: Knex.QueryBuilder, relationship: RelationshipsJson) {
490
- const document = relationship.from?.split(".")[0] || ""
491
- return query.andWhere(`${document}.fieldName`, "=", relationship.column)
492
- }
493
-
494
- addRelationshipForFilter(
495
- query: Knex.QueryBuilder,
496
- allowEmptyRelationships: boolean,
497
- filterKey: string,
498
- whereCb: (filterKey: string, query: Knex.QueryBuilder) => Knex.QueryBuilder
499
- ): Knex.QueryBuilder {
500
- const { relationships, endpoint, tableAliases: aliases } = this.query
501
- const tableName = endpoint.entityId
502
- const fromAlias = aliases?.[tableName] || tableName
503
- const matches = (value: string) =>
504
- filterKey.match(new RegExp(`^${value}\\.`))
505
- if (!relationships) {
506
- return query
507
- }
508
- for (const relationship of relationships) {
509
- const relatedTableName = relationship.tableName
510
- const toAlias = aliases?.[relatedTableName] || relatedTableName
511
-
512
- const matchesTableName = matches(relatedTableName) || matches(toAlias)
513
- const matchesRelationName = matches(relationship.column)
514
-
515
- // this is the relationship which is being filtered
516
- if (
517
- (matchesTableName || matchesRelationName) &&
518
- relationship.to &&
519
- relationship.tableName
520
- ) {
521
- const joinTable = this.knex
522
- .select(this.knex.raw(1))
523
- .from({ [toAlias]: relatedTableName })
524
- let subQuery = joinTable.clone()
525
- const manyToMany = validateManyToMany(relationship)
526
- let updatedKey
527
-
528
- if (!matchesTableName) {
529
- updatedKey = filterKey.replace(
530
- new RegExp(`^${relationship.column}.`),
531
- `${aliases?.[relationship.tableName] || relationship.tableName}.`
532
- )
533
- } else {
534
- updatedKey = filterKey
535
- }
536
-
537
- if (manyToMany) {
538
- const throughAlias =
539
- aliases?.[manyToMany.through] || relationship.through
540
- let throughTable = this.tableNameWithSchema(manyToMany.through, {
541
- alias: throughAlias,
542
- schema: endpoint.schema,
543
- })
544
- subQuery = subQuery
545
- // add a join through the junction table
546
- .innerJoin(throughTable, function () {
547
- this.on(
548
- `${toAlias}.${manyToMany.toPrimary}`,
549
- "=",
550
- `${throughAlias}.${manyToMany.to}`
551
- )
552
- })
553
- // check the document in the junction table points to the main table
554
- .where(
555
- `${throughAlias}.${manyToMany.from}`,
556
- "=",
557
- this.rawQuotedIdentifier(`${fromAlias}.${manyToMany.fromPrimary}`)
558
- )
559
- // in SQS the same junction table is used for different many-to-many relationships between the
560
- // two same tables, this is needed to avoid rows ending up in all columns
561
- if (this.client === SqlClient.SQL_LITE) {
562
- subQuery = this.addJoinFieldCheck(subQuery, manyToMany)
563
- }
564
-
565
- query = query.where(q => {
566
- q.whereExists(whereCb(updatedKey, subQuery))
567
- if (allowEmptyRelationships) {
568
- q.orWhereNotExists(
569
- joinTable.clone().innerJoin(throughTable, function () {
570
- this.on(
571
- `${fromAlias}.${manyToMany.fromPrimary}`,
572
- "=",
573
- `${throughAlias}.${manyToMany.from}`
574
- )
575
- })
576
- )
577
- }
578
- })
579
- } else {
580
- const toKey = `${toAlias}.${relationship.to}`
581
- const foreignKey = `${fromAlias}.${relationship.from}`
582
- // "join" to the main table, making sure the ID matches that of the main
583
- subQuery = subQuery.where(
584
- toKey,
585
- "=",
586
- this.rawQuotedIdentifier(foreignKey)
587
- )
588
-
589
- query = query.where(q => {
590
- q.whereExists(whereCb(updatedKey, subQuery.clone()))
591
- if (allowEmptyRelationships) {
592
- q.orWhereNotExists(subQuery)
593
- }
594
- })
595
- }
596
- }
597
- }
598
- return query
599
- }
600
-
601
- // right now we only do filters on the specific table being queried
602
- addFilters(
603
- query: Knex.QueryBuilder,
604
- filters: SearchFilters | undefined,
605
- opts?: {
606
- relationship?: boolean
607
- }
608
- ): Knex.QueryBuilder {
609
- if (!filters) {
610
- return query
611
- }
612
- const builder = this
613
- filters = this.parseFilters({ ...filters })
614
- const aliases = this.query.tableAliases
615
- // if all or specified in filters, then everything is an or
616
- const shouldOr = filters.allOr
617
- const isSqlite = this.client === SqlClient.SQL_LITE
618
- const tableName = isSqlite ? this.table._id! : this.table.name
619
-
620
- function getTableAlias(name: string) {
621
- const alias = aliases?.[name]
622
- return alias || name
623
- }
624
- function iterate(
625
- structure: AnySearchFilter,
626
- operation: SearchFilterKey,
627
- fn: (
628
- query: Knex.QueryBuilder,
629
- key: string,
630
- value: any
631
- ) => Knex.QueryBuilder,
632
- complexKeyFn?: (
633
- query: Knex.QueryBuilder,
634
- key: string[],
635
- value: any
636
- ) => Knex.QueryBuilder
637
- ) {
638
- const handleRelationship = (
639
- q: Knex.QueryBuilder,
640
- key: string,
641
- value: any
642
- ) => {
643
- const [filterTableName, ...otherProperties] = key.split(".")
644
- const property = otherProperties.join(".")
645
- const alias = getTableAlias(filterTableName)
646
- return q.andWhere(subquery =>
647
- fn(subquery, alias ? `${alias}.${property}` : property, value)
648
- )
649
- }
650
-
651
- for (const key in structure) {
652
- const value = structure[key]
653
- const updatedKey = dbCore.removeKeyNumbering(key)
654
- const isRelationshipField = updatedKey.includes(".")
655
- const shouldProcessRelationship =
656
- opts?.relationship && isRelationshipField
657
-
658
- let castedTypeValue
659
- if (
660
- key === InternalSearchFilterOperator.COMPLEX_ID_OPERATOR &&
661
- (castedTypeValue = structure[key]) &&
662
- complexKeyFn
663
- ) {
664
- const alias = getTableAlias(tableName)
665
- query = complexKeyFn(
666
- query,
667
- castedTypeValue.id.map((x: string) =>
668
- alias ? `${alias}.${x}` : x
669
- ),
670
- castedTypeValue.values
671
- )
672
- } else if (!isRelationshipField) {
673
- const alias = getTableAlias(tableName)
674
- query = fn(
675
- query,
676
- alias ? `${alias}.${updatedKey}` : updatedKey,
677
- value
678
- )
679
- } else if (shouldProcessRelationship) {
680
- if (shouldOr) {
681
- query = query.or
682
- }
683
- query = builder.addRelationshipForFilter(
684
- query,
685
- allowEmptyRelationships[operation],
686
- updatedKey,
687
- (updatedKey, q) => {
688
- return handleRelationship(q, updatedKey, value)
689
- }
690
- )
691
- }
692
- }
693
- }
694
-
695
- const like = (q: Knex.QueryBuilder, key: string, value: any) => {
696
- if (filters?.fuzzyOr || shouldOr) {
697
- q = q.or
698
- }
699
- if (
700
- this.client === SqlClient.ORACLE ||
701
- this.client === SqlClient.SQL_LITE
702
- ) {
703
- return q.whereRaw(`LOWER(??) LIKE ?`, [
704
- this.rawQuotedIdentifier(key),
705
- `%${value.toLowerCase()}%`,
706
- ])
707
- }
708
- return q.whereILike(
709
- // @ts-expect-error knex types are wrong, raw is fine here
710
- this.rawQuotedIdentifier(key),
711
- this.knex.raw("?", [`%${value}%`])
712
- )
713
- }
714
-
715
- const contains = (mode: ArrayFilter, any = false) => {
716
- function addModifiers<T extends {}, Q>(q: Knex.QueryBuilder<T, Q>) {
717
- if (shouldOr || mode === filters?.containsAny) {
718
- q = q.or
719
- }
720
- if (mode === filters?.notContains) {
721
- q = q.not
722
- }
723
- return q
724
- }
725
-
726
- if (this.client === SqlClient.POSTGRES) {
727
- iterate(mode, ArrayOperator.CONTAINS, (q, key, value) => {
728
- q = addModifiers(q)
729
- if (any) {
730
- return q.whereRaw(`COALESCE(??::jsonb \\?| array??, FALSE)`, [
731
- this.rawQuotedIdentifier(key),
732
- this.knex.raw(stringifyArray(value, "'")),
733
- ])
734
- } else {
735
- return q.whereRaw(`COALESCE(??::jsonb @> '??', FALSE)`, [
736
- this.rawQuotedIdentifier(key),
737
- this.knex.raw(stringifyArray(value)),
738
- ])
739
- }
740
- })
741
- } else if (
742
- this.client === SqlClient.MY_SQL ||
743
- this.client === SqlClient.MARIADB
744
- ) {
745
- iterate(mode, ArrayOperator.CONTAINS, (q, key, value) => {
746
- return addModifiers(q).whereRaw(`COALESCE(?(??, ?), FALSE)`, [
747
- this.knex.raw(any ? "JSON_OVERLAPS" : "JSON_CONTAINS"),
748
- this.rawQuotedIdentifier(key),
749
- this.knex.raw(wrap(stringifyArray(value))),
750
- ])
751
- })
752
- } else {
753
- iterate(mode, ArrayOperator.CONTAINS, (q, key, value) => {
754
- if (value.length === 0) {
755
- return q
756
- }
757
-
758
- q = q.where(subQuery => {
759
- if (mode === filters?.notContains) {
760
- subQuery = subQuery.not
761
- }
762
-
763
- subQuery = subQuery.where(subSubQuery => {
764
- for (const elem of value) {
765
- if (mode === filters?.containsAny) {
766
- subSubQuery = subSubQuery.or
767
- } else {
768
- subSubQuery = subSubQuery.and
769
- }
770
-
771
- const lower =
772
- typeof elem === "string" ? `"${elem.toLowerCase()}"` : elem
773
-
774
- subSubQuery = subSubQuery.whereLike(
775
- // @ts-expect-error knex types are wrong, raw is fine here
776
- this.knex.raw(`COALESCE(LOWER(??), '')`, [
777
- this.rawQuotedIdentifier(key),
778
- ]),
779
- `%${lower}%`
780
- )
781
- }
782
- })
783
- if (mode === filters?.notContains) {
784
- subQuery = subQuery.or.whereNull(
785
- // @ts-expect-error knex types are wrong, raw is fine here
786
- this.rawQuotedIdentifier(key)
787
- )
788
- }
789
- return subQuery
790
- })
791
- return q
792
- })
793
- }
794
- }
795
-
796
- if (filters.$and) {
797
- const { $and } = filters
798
- for (const condition of $and.conditions) {
799
- query = query.where(b => {
800
- this.addFilters(b, condition, opts)
801
- })
802
- }
803
- }
804
-
805
- if (filters.$or) {
806
- const { $or } = filters
807
- query = query.where(b => {
808
- for (const condition of $or.conditions) {
809
- b.orWhere(c =>
810
- this.addFilters(c, { ...condition, allOr: true }, opts)
811
- )
812
- }
813
- })
814
- }
815
-
816
- if (filters.oneOf) {
817
- iterate(
818
- filters.oneOf,
819
- ArrayOperator.ONE_OF,
820
- (q, key: string, array) => {
821
- if (shouldOr) {
822
- q = q.or
823
- }
824
- if (this.client === SqlClient.ORACLE) {
825
- // @ts-ignore
826
- key = this.convertClobs(key)
827
- }
828
- return q.whereIn(key, Array.isArray(array) ? array : [array])
829
- },
830
- (q, key: string[], array) => {
831
- if (shouldOr) {
832
- q = q.or
833
- }
834
- if (this.client === SqlClient.ORACLE) {
835
- // @ts-ignore
836
- key = key.map(k => this.convertClobs(k))
837
- }
838
- return q.whereIn(key, Array.isArray(array) ? array : [array])
839
- }
840
- )
841
- }
842
- if (filters.string) {
843
- iterate(filters.string, BasicOperator.STRING, (q, key, value) => {
844
- if (shouldOr) {
845
- q = q.or
846
- }
847
- if (
848
- this.client === SqlClient.ORACLE ||
849
- this.client === SqlClient.SQL_LITE
850
- ) {
851
- return q.whereRaw(`LOWER(??) LIKE ?`, [
852
- this.rawQuotedIdentifier(key),
853
- `${value.toLowerCase()}%`,
854
- ])
855
- } else {
856
- return q.whereILike(key, `${value}%`)
857
- }
858
- })
859
- }
860
- if (filters.fuzzy) {
861
- iterate(filters.fuzzy, BasicOperator.FUZZY, like)
862
- }
863
- if (filters.range) {
864
- iterate(filters.range, RangeOperator.RANGE, (q, key, value) => {
865
- const isEmptyObject = (val: any) => {
866
- return (
867
- val &&
868
- Object.keys(val).length === 0 &&
869
- Object.getPrototypeOf(val) === Object.prototype
870
- )
871
- }
872
- if (isEmptyObject(value.low)) {
873
- value.low = ""
874
- }
875
- if (isEmptyObject(value.high)) {
876
- value.high = ""
877
- }
878
- const lowValid = isValidFilter(value.low),
879
- highValid = isValidFilter(value.high)
880
-
881
- const schema = this.getFieldSchema(key)
882
-
883
- let rawKey: string | Knex.Raw = key
884
- let high = value.high
885
- let low = value.low
886
-
887
- if (this.client === SqlClient.ORACLE) {
888
- rawKey = this.convertClobs(key)
889
- } else if (
890
- this.client === SqlClient.SQL_LITE &&
891
- schema?.type === FieldType.BIGINT
892
- ) {
893
- rawKey = this.knex.raw("CAST(?? AS INTEGER)", [
894
- this.rawQuotedIdentifier(key),
895
- ])
896
- high = this.knex.raw("CAST(? AS INTEGER)", [value.high])
897
- low = this.knex.raw("CAST(? AS INTEGER)", [value.low])
898
- }
899
-
900
- if (shouldOr) {
901
- q = q.or
902
- }
903
-
904
- if (lowValid && highValid) {
905
- // @ts-expect-error knex types are wrong, raw is fine here
906
- return q.whereBetween(rawKey, [low, high])
907
- } else if (lowValid) {
908
- // @ts-expect-error knex types are wrong, raw is fine here
909
- return q.where(rawKey, ">=", low)
910
- } else if (highValid) {
911
- // @ts-expect-error knex types are wrong, raw is fine here
912
- return q.where(rawKey, "<=", high)
913
- }
914
- return q
915
- })
916
- }
917
- if (filters.equal) {
918
- iterate(filters.equal, BasicOperator.EQUAL, (q, key, value) => {
919
- if (shouldOr) {
920
- q = q.or
921
- }
922
- if (this.client === SqlClient.MS_SQL) {
923
- return q.whereRaw(`CASE WHEN ?? = ? THEN 1 ELSE 0 END = 1`, [
924
- this.rawQuotedIdentifier(key),
925
- value,
926
- ])
927
- } else if (this.client === SqlClient.ORACLE) {
928
- const identifier = this.convertClobs(key)
929
- return q.where(subq =>
930
- // @ts-expect-error knex types are wrong, raw is fine here
931
- subq.whereNotNull(identifier).andWhere(identifier, value)
932
- )
933
- } else {
934
- return q.whereRaw(`COALESCE(?? = ?, FALSE)`, [
935
- this.rawQuotedIdentifier(key),
936
- value,
937
- ])
938
- }
939
- })
940
- }
941
- if (filters.notEqual) {
942
- iterate(filters.notEqual, BasicOperator.NOT_EQUAL, (q, key, value) => {
943
- if (shouldOr) {
944
- q = q.or
945
- }
946
- if (this.client === SqlClient.MS_SQL) {
947
- return q.whereRaw(`CASE WHEN ?? = ? THEN 1 ELSE 0 END = 0`, [
948
- this.rawQuotedIdentifier(key),
949
- value,
950
- ])
951
- } else if (this.client === SqlClient.ORACLE) {
952
- const identifier = this.convertClobs(key)
953
- return (
954
- q
955
- .where(subq =>
956
- subq.not
957
- // @ts-expect-error knex types are wrong, raw is fine here
958
- .whereNull(identifier)
959
- .and.where(identifier, "!=", value)
960
- )
961
- // @ts-expect-error knex types are wrong, raw is fine here
962
- .or.whereNull(identifier)
963
- )
964
- } else {
965
- return q.whereRaw(`COALESCE(?? != ?, TRUE)`, [
966
- this.rawQuotedIdentifier(key),
967
- value,
968
- ])
969
- }
970
- })
971
- }
972
- if (filters.empty) {
973
- iterate(filters.empty, BasicOperator.EMPTY, (q, key) => {
974
- if (shouldOr) {
975
- q = q.or
976
- }
977
- return q.whereNull(key)
978
- })
979
- }
980
- if (filters.notEmpty) {
981
- iterate(filters.notEmpty, BasicOperator.NOT_EMPTY, (q, key) => {
982
- if (shouldOr) {
983
- q = q.or
984
- }
985
- return q.whereNotNull(key)
986
- })
987
- }
988
- if (filters.contains) {
989
- contains(filters.contains)
990
- }
991
- if (filters.notContains) {
992
- contains(filters.notContains)
993
- }
994
- if (filters.containsAny) {
995
- contains(filters.containsAny, true)
996
- }
997
-
998
- const tableRef = aliases?.[this.table._id!] || this.table._id
999
- // when searching internal tables make sure long looking for rows
1000
- if (filters.documentType && !isExternalTable(this.table) && tableRef) {
1001
- // has to be its own option, must always be AND onto the search
1002
- query.andWhereLike(
1003
- `${tableRef}._id`,
1004
- `${prefixed(filters.documentType)}%`
1005
- )
1006
- }
1007
-
1008
- return query
1009
- }
1010
-
1011
- isSqs(): boolean {
1012
- return isSqs(this.table)
1013
- }
1014
-
1015
- getTableName(tableOrName?: Table | string): string {
1016
- let table: Table
1017
- if (typeof tableOrName === "string") {
1018
- const name = tableOrName
1019
- if (this.query.table?.name === name) {
1020
- table = this.query.table
1021
- } else if (this.query.meta.table?.name === name) {
1022
- table = this.query.meta.table
1023
- } else if (!this.query.meta.tables?.[name]) {
1024
- // This can legitimately happen in custom queries, where the user is
1025
- // querying against a table that may not have been imported into
1026
- // Budibase.
1027
- return name
1028
- } else {
1029
- table = this.query.meta.tables[name]
1030
- }
1031
- } else if (tableOrName) {
1032
- table = tableOrName
1033
- } else {
1034
- table = this.table
1035
- }
1036
-
1037
- let name = table.name
1038
- if (isSqs(table) && table._id) {
1039
- // SQS uses the table ID rather than the table name
1040
- name = table._id
1041
- }
1042
- const aliases = this.query.tableAliases || {}
1043
- return aliases[name] ? aliases[name] : name
1044
- }
1045
-
1046
- addDistinctCount(query: Knex.QueryBuilder): Knex.QueryBuilder {
1047
- if (!this.table.primary) {
1048
- throw new Error("SQL counting requires primary key to be supplied")
1049
- }
1050
- return query.countDistinct(
1051
- `${this.getTableName()}.${this.table.primary[0]} as ${COUNT_FIELD_NAME}`
1052
- )
1053
- }
1054
-
1055
- addAggregations(
1056
- query: Knex.QueryBuilder,
1057
- aggregations: Aggregation[]
1058
- ): Knex.QueryBuilder {
1059
- const fields = this.query.resource?.fields || []
1060
- const tableName = this.getTableName()
1061
- if (fields.length > 0) {
1062
- const qualifiedFields = fields.map(field => this.qualifyIdentifier(field))
1063
- if (this.client === SqlClient.ORACLE) {
1064
- const groupByFields = qualifiedFields.map(field =>
1065
- this.convertClobs(field)
1066
- )
1067
- const selectFields = qualifiedFields.map(field =>
1068
- this.convertClobs(field, { forSelect: true })
1069
- )
1070
- query = query.groupBy(groupByFields).select(selectFields)
1071
- } else {
1072
- query = query.groupBy(qualifiedFields).select(qualifiedFields)
1073
- }
1074
- }
1075
- for (const aggregation of aggregations) {
1076
- const op = aggregation.calculationType
1077
- if (op === CalculationType.COUNT) {
1078
- if ("distinct" in aggregation && aggregation.distinct) {
1079
- if (this.client === SqlClient.ORACLE) {
1080
- const field = this.convertClobs(`${tableName}.${aggregation.field}`)
1081
- query = query.select(
1082
- this.knex.raw(`COUNT(DISTINCT ??) as ??`, [
1083
- field,
1084
- aggregation.name,
1085
- ])
1086
- )
1087
- } else {
1088
- query = query.countDistinct(
1089
- `${tableName}.${aggregation.field} as ${aggregation.name}`
1090
- )
1091
- }
1092
- } else {
1093
- if (this.client === SqlClient.ORACLE) {
1094
- const field = this.convertClobs(`${tableName}.${aggregation.field}`)
1095
- query = query.select(
1096
- this.knex.raw(`COUNT(??) as ??`, [field, aggregation.name])
1097
- )
1098
- } else {
1099
- query = query.count(`${aggregation.field} as ${aggregation.name}`)
1100
- }
1101
- }
1102
- } else {
1103
- const fieldSchema = this.getFieldSchema(aggregation.field)
1104
- if (!fieldSchema) {
1105
- // This should not happen in practice.
1106
- throw new Error(
1107
- `field schema missing for aggregation target: ${aggregation.field}`
1108
- )
1109
- }
1110
-
1111
- let aggregate = this.knex.raw("??(??)", [
1112
- this.knex.raw(op),
1113
- this.rawQuotedIdentifier(`${tableName}.${aggregation.field}`),
1114
- ])
1115
-
1116
- if (fieldSchema.type === FieldType.BIGINT) {
1117
- aggregate = this.castIntToString(aggregate)
1118
- }
1119
-
1120
- query = query.select(
1121
- this.knex.raw("?? as ??", [aggregate, aggregation.name])
1122
- )
1123
- }
1124
- }
1125
- return query
1126
- }
1127
-
1128
- isAggregateField(field: string): boolean {
1129
- const found = this.query.resource?.aggregations?.find(
1130
- aggregation => aggregation.name === field
1131
- )
1132
- return !!found
1133
- }
1134
-
1135
- addSorting(query: Knex.QueryBuilder): Knex.QueryBuilder {
1136
- let { sort, resource } = this.query
1137
- const primaryKey = this.table.primary
1138
- const aliased = this.getTableName()
1139
- if (!Array.isArray(primaryKey)) {
1140
- throw new Error("Sorting requires primary key to be specified for table")
1141
- }
1142
- if (sort && Object.keys(sort || {}).length > 0) {
1143
- for (let [key, value] of Object.entries(sort)) {
1144
- const direction =
1145
- value.direction === SortOrder.ASCENDING ? "asc" : "desc"
1146
-
1147
- // TODO: figure out a way to remove this conditional, not relying on
1148
- // the defaults of each datastore.
1149
- let nulls: "first" | "last" | undefined = undefined
1150
- if (
1151
- this.client === SqlClient.POSTGRES ||
1152
- this.client === SqlClient.ORACLE
1153
- ) {
1154
- nulls = value.direction === SortOrder.ASCENDING ? "first" : "last"
1155
- }
1156
-
1157
- if (this.isAggregateField(key)) {
1158
- query = query.orderBy(key, direction, nulls)
1159
- } else {
1160
- let composite = `${aliased}.${key}`
1161
- if (this.client === SqlClient.ORACLE) {
1162
- query = query.orderByRaw(`?? ?? nulls ??`, [
1163
- this.convertClobs(composite),
1164
- this.knex.raw(direction),
1165
- this.knex.raw(nulls as string),
1166
- ])
1167
- } else {
1168
- query = query.orderBy(composite, direction, nulls)
1169
- }
1170
- }
1171
- }
1172
- }
1173
-
1174
- // add sorting by the primary key if the result isn't already sorted by it,
1175
- // to make sure result is deterministic
1176
- const hasAggregations = (resource?.aggregations?.length ?? 0) > 0
1177
- if (!hasAggregations && (!sort || sort[primaryKey[0]] === undefined)) {
1178
- query = query.orderBy(`${aliased}.${primaryKey[0]}`)
1179
- }
1180
- return query
1181
- }
1182
-
1183
- tableNameWithSchema(
1184
- tableName: string,
1185
- opts?: { alias?: string; schema?: string }
1186
- ) {
1187
- let withSchema = opts?.schema ? `${opts.schema}.${tableName}` : tableName
1188
- if (opts?.alias) {
1189
- withSchema += ` as ${opts.alias}`
1190
- }
1191
- return withSchema
1192
- }
1193
-
1194
- private buildJsonField(field: string): string {
1195
- const parts = field.split(".")
1196
- let unaliased: string
1197
-
1198
- let tableField: string
1199
- if (parts.length > 1) {
1200
- const alias = parts.shift()!
1201
- unaliased = parts.join(".")
1202
- tableField = `${alias}.${unaliased}`
1203
- } else {
1204
- unaliased = parts.join(".")
1205
- tableField = unaliased
1206
- }
1207
-
1208
- const separator = this.client === SqlClient.ORACLE ? " VALUE " : ","
1209
- return this.knex
1210
- .raw(`?${separator}??`, [unaliased, this.rawQuotedIdentifier(tableField)])
1211
- .toString()
1212
- }
1213
-
1214
- maxFunctionParameters() {
1215
- // functions like say json_build_object() in SQL have a limit as to how many can be performed
1216
- // before a limit is met, this limit exists in Postgres/SQLite. This can be very important, such as
1217
- // for JSON column building as part of relationships. We also have a default limit to avoid very complex
1218
- // functions being built - it is likely this is not necessary or the best way to do it.
1219
- switch (this.client) {
1220
- case SqlClient.SQL_LITE:
1221
- return 127
1222
- case SqlClient.POSTGRES:
1223
- return 100
1224
- // other DBs don't have a limit, but set some sort of limit
1225
- default:
1226
- return 200
1227
- }
1228
- }
1229
-
1230
- addJsonRelationships(
1231
- query: Knex.QueryBuilder,
1232
- fromTable: string,
1233
- relationships: RelationshipsJson[]
1234
- ): Knex.QueryBuilder {
1235
- const sqlClient = this.client
1236
- const knex = this.knex
1237
- const { resource, tableAliases: aliases, endpoint, meta } = this.query
1238
- const fields = resource?.fields || []
1239
- for (let relationship of relationships) {
1240
- const {
1241
- tableName: toTable,
1242
- through: throughTable,
1243
- to: toKey,
1244
- from: fromKey,
1245
- fromPrimary,
1246
- toPrimary,
1247
- } = relationship
1248
- // skip invalid relationships
1249
- if (!toTable || !fromTable) {
1250
- continue
1251
- }
1252
- const relatedTable = meta.tables?.[toTable]
1253
- const toAlias = aliases?.[toTable] || toTable,
1254
- fromAlias = aliases?.[fromTable] || fromTable,
1255
- throughAlias = (throughTable && aliases?.[throughTable]) || throughTable
1256
- let toTableWithSchema = this.tableNameWithSchema(toTable, {
1257
- alias: toAlias,
1258
- schema: endpoint.schema,
1259
- })
1260
- const requiredFields = [
1261
- ...(relatedTable?.primary || []),
1262
- relatedTable?.primaryDisplay,
1263
- ].filter(field => field) as string[]
1264
- // sort the required fields to first in the list, so they don't get sliced out
1265
- let relationshipFields = prioritisedArraySort(
1266
- fields.filter(field => field.split(".")[0] === toAlias),
1267
- requiredFields
1268
- )
1269
-
1270
- relationshipFields = relationshipFields.slice(
1271
- 0,
1272
- Math.floor(this.maxFunctionParameters() / 2)
1273
- )
1274
- const fieldList: string = relationshipFields
1275
- .map(field => this.buildJsonField(field))
1276
- .join(",")
1277
- // SQL Server uses TOP - which performs a little differently to the normal LIMIT syntax
1278
- // it reduces the result set rather than limiting how much data it filters over
1279
- const primaryKey = `${toAlias}.${toPrimary || toKey}`
1280
- let subQuery: Knex.QueryBuilder = knex
1281
- .from(toTableWithSchema)
1282
- // add sorting to get consistent order
1283
- .orderBy(primaryKey)
1284
-
1285
- const isManyToMany = throughTable && toPrimary && fromPrimary
1286
- let correlatedTo = isManyToMany
1287
- ? `${throughAlias}.${fromKey}`
1288
- : `${toAlias}.${toKey}`,
1289
- correlatedFrom = isManyToMany
1290
- ? `${fromAlias}.${fromPrimary}`
1291
- : `${fromAlias}.${fromKey}`
1292
- // many-to-many relationship needs junction table join
1293
- if (isManyToMany) {
1294
- let throughTableWithSchema = this.tableNameWithSchema(throughTable, {
1295
- alias: throughAlias,
1296
- schema: endpoint.schema,
1297
- })
1298
- subQuery = subQuery.join(throughTableWithSchema, function () {
1299
- this.on(`${toAlias}.${toPrimary}`, "=", `${throughAlias}.${toKey}`)
1300
- })
1301
- }
1302
-
1303
- // add the correlation to the overall query
1304
- subQuery = subQuery.where(
1305
- correlatedTo,
1306
- "=",
1307
- this.rawQuotedIdentifier(correlatedFrom)
1308
- )
1309
-
1310
- const standardWrap = (select: Knex.Raw): Knex.QueryBuilder => {
1311
- subQuery = subQuery.select(`${toAlias}.*`).limit(getRelationshipLimit())
1312
- // @ts-ignore - the from alias syntax isn't in Knex typing
1313
- return knex.select(select).from({
1314
- [toAlias]: subQuery,
1315
- })
1316
- }
1317
- let wrapperQuery: Knex.QueryBuilder | Knex.Raw
1318
- switch (sqlClient) {
1319
- case SqlClient.SQL_LITE:
1320
- // need to check the junction table document is to the right column, this is just for SQS
1321
- subQuery = this.addJoinFieldCheck(subQuery, relationship)
1322
- wrapperQuery = standardWrap(
1323
- this.knex.raw(`json_group_array(json_object(${fieldList}))`)
1324
- )
1325
- break
1326
- case SqlClient.POSTGRES:
1327
- wrapperQuery = standardWrap(
1328
- this.knex.raw(`json_agg(json_build_object(${fieldList}))`)
1329
- )
1330
- break
1331
- case SqlClient.MARIADB:
1332
- // can't use the standard wrap due to correlated sub-query limitations in MariaDB
1333
- wrapperQuery = subQuery.select(
1334
- knex.raw(
1335
- `json_arrayagg(json_object(${fieldList}) LIMIT ${getRelationshipLimit()})`
1336
- )
1337
- )
1338
- break
1339
- case SqlClient.MY_SQL:
1340
- case SqlClient.ORACLE:
1341
- wrapperQuery = standardWrap(
1342
- this.knex.raw(`json_arrayagg(json_object(${fieldList}))`)
1343
- )
1344
- break
1345
- case SqlClient.MS_SQL: {
1346
- const comparatorQuery = knex
1347
- .select(`${fromAlias}.*`)
1348
- // @ts-ignore - from alias syntax not TS supported
1349
- .from({
1350
- [fromAlias]: subQuery
1351
- .select(`${toAlias}.*`)
1352
- .limit(getRelationshipLimit()),
1353
- })
1354
-
1355
- wrapperQuery = knex.raw(
1356
- `(SELECT ?? = (${comparatorQuery} FOR JSON PATH))`,
1357
- [this.rawQuotedIdentifier(toAlias)]
1358
- )
1359
- break
1360
- }
1361
- default:
1362
- throw new Error(`JSON relationships not implement for ${sqlClient}`)
1363
- }
1364
-
1365
- query = query.select({ [relationship.column]: wrapperQuery })
1366
- }
1367
- return query
1368
- }
1369
-
1370
- addJoin(
1371
- query: Knex.QueryBuilder,
1372
- tables: { from: string; to: string; through?: string },
1373
- columns: {
1374
- from?: string
1375
- to?: string
1376
- fromPrimary?: string
1377
- toPrimary?: string
1378
- }[]
1379
- ): Knex.QueryBuilder {
1380
- const { tableAliases: aliases, endpoint } = this.query
1381
- const schema = endpoint.schema
1382
- const toTable = tables.to,
1383
- fromTable = tables.from,
1384
- throughTable = tables.through
1385
- const toAlias = aliases?.[toTable] || toTable,
1386
- throughAlias = (throughTable && aliases?.[throughTable]) || throughTable,
1387
- fromAlias = aliases?.[fromTable] || fromTable
1388
- let toTableWithSchema = this.tableNameWithSchema(toTable, {
1389
- alias: toAlias,
1390
- schema,
1391
- })
1392
- let throughTableWithSchema = throughTable
1393
- ? this.tableNameWithSchema(throughTable, {
1394
- alias: throughAlias,
1395
- schema,
1396
- })
1397
- : undefined
1398
- if (!throughTable) {
1399
- query = query.leftJoin(toTableWithSchema, function () {
1400
- for (let relationship of columns) {
1401
- const from = relationship.from,
1402
- to = relationship.to
1403
- this.orOn(`${fromAlias}.${from}`, "=", `${toAlias}.${to}`)
1404
- }
1405
- })
1406
- } else {
1407
- query = query
1408
- // @ts-ignore
1409
- .leftJoin(throughTableWithSchema, function () {
1410
- for (let relationship of columns) {
1411
- const fromPrimary = relationship.fromPrimary
1412
- const from = relationship.from
1413
- this.orOn(
1414
- `${fromAlias}.${fromPrimary}`,
1415
- "=",
1416
- `${throughAlias}.${from}`
1417
- )
1418
- }
1419
- })
1420
- .leftJoin(toTableWithSchema, function () {
1421
- for (let relationship of columns) {
1422
- const toPrimary = relationship.toPrimary
1423
- const to = relationship.to
1424
- this.orOn(`${toAlias}.${toPrimary}`, `${throughAlias}.${to}`)
1425
- }
1426
- })
1427
- }
1428
- return query
1429
- }
1430
-
1431
- qualifiedKnex(opts?: { alias?: string | boolean }): Knex.QueryBuilder {
1432
- let alias = this.query.tableAliases?.[this.query.endpoint.entityId]
1433
- if (opts?.alias === false) {
1434
- alias = undefined
1435
- } else if (typeof opts?.alias === "string") {
1436
- alias = opts.alias
1437
- }
1438
- return this.knex(
1439
- this.tableNameWithSchema(this.query.endpoint.entityId, {
1440
- alias,
1441
- schema: this.query.endpoint.schema,
1442
- })
1443
- )
1444
- }
1445
-
1446
- create(opts: QueryOptions): Knex.QueryBuilder {
1447
- const { body } = this.query
1448
- if (!body) {
1449
- throw new Error("Cannot create without row body")
1450
- }
1451
-
1452
- let query = this.qualifiedKnex({ alias: false })
1453
- const parsedBody = this.parseBody(body)
1454
-
1455
- if (this.client === SqlClient.ORACLE) {
1456
- // Oracle doesn't seem to automatically insert nulls
1457
- // if we don't specify them, so we need to do that here
1458
- for (const [column, schema] of Object.entries(
1459
- this.query.meta.table.schema
1460
- )) {
1461
- if (
1462
- schema.constraints?.presence === true ||
1463
- schema.type === FieldType.FORMULA ||
1464
- schema.type === FieldType.AUTO ||
1465
- schema.type === FieldType.LINK ||
1466
- schema.type === FieldType.AI
1467
- ) {
1468
- continue
1469
- }
1470
-
1471
- const value = parsedBody[column]
1472
- if (value == null) {
1473
- parsedBody[column] = null
1474
- }
1475
- }
1476
- } else {
1477
- // make sure no null values in body for creation
1478
- for (let [key, value] of Object.entries(parsedBody)) {
1479
- if (value == null) {
1480
- delete parsedBody[key]
1481
- }
1482
- }
1483
- }
1484
-
1485
- // mysql can't use returning
1486
- if (opts.disableReturning) {
1487
- return query.insert(parsedBody)
1488
- } else {
1489
- return query.insert(parsedBody).returning("*")
1490
- }
1491
- }
1492
-
1493
- bulkCreate(): Knex.QueryBuilder {
1494
- const { body } = this.query
1495
- let query = this.qualifiedKnex({ alias: false })
1496
- if (!Array.isArray(body)) {
1497
- return query
1498
- }
1499
- const parsedBody = body.map(row => this.parseBody(row))
1500
- return query.insert(parsedBody)
1501
- }
1502
-
1503
- bulkUpsert(): Knex.QueryBuilder {
1504
- const { body } = this.query
1505
- let query = this.qualifiedKnex({ alias: false })
1506
- if (!Array.isArray(body)) {
1507
- return query
1508
- }
1509
- const parsedBody = body.map(row => this.parseBody(row))
1510
- if (
1511
- this.client === SqlClient.POSTGRES ||
1512
- this.client === SqlClient.SQL_LITE ||
1513
- this.client === SqlClient.MY_SQL ||
1514
- this.client === SqlClient.MARIADB
1515
- ) {
1516
- const primary = this.table.primary
1517
- if (!primary) {
1518
- throw new Error("Primary key is required for upsert")
1519
- }
1520
- return query.insert(parsedBody).onConflict(primary).merge()
1521
- } else if (
1522
- this.client === SqlClient.MS_SQL ||
1523
- this.client === SqlClient.ORACLE
1524
- ) {
1525
- // No upsert or onConflict support in MSSQL/Oracle yet, see:
1526
- // https://github.com/knex/knex/pull/6050
1527
- return query.insert(parsedBody)
1528
- }
1529
- return query.upsert(parsedBody)
1530
- }
1531
-
1532
- read(
1533
- opts: {
1534
- limits?: { base: number; query: number }
1535
- } = {}
1536
- ): Knex.QueryBuilder {
1537
- let { endpoint, filters, paginate, relationships } = this.query
1538
- const { limits } = opts
1539
- const counting = endpoint.operation === Operation.COUNT
1540
-
1541
- const tableName = endpoint.entityId
1542
- // start building the query
1543
- let query = this.qualifiedKnex()
1544
- // handle pagination
1545
- let foundOffset: number | null = null
1546
- let foundLimit = limits?.query || limits?.base
1547
- if (paginate && paginate.page && paginate.limit) {
1548
- // @ts-ignore
1549
- const page = paginate.page <= 1 ? 0 : paginate.page - 1
1550
- const offset = page * paginate.limit
1551
- foundLimit = paginate.limit
1552
- foundOffset = offset
1553
- } else if (paginate && paginate.offset && paginate.limit) {
1554
- foundLimit = paginate.limit
1555
- foundOffset = paginate.offset
1556
- } else if (paginate && paginate.limit) {
1557
- foundLimit = paginate.limit
1558
- }
1559
- // counting should not sort, limit or offset
1560
- if (!counting) {
1561
- // add the found limit if supplied
1562
- if (foundLimit != null) {
1563
- query = query.limit(foundLimit)
1564
- }
1565
- // add overall pagination
1566
- if (foundOffset != null) {
1567
- query = query.offset(foundOffset)
1568
- }
1569
- }
1570
-
1571
- const aggregations = this.query.resource?.aggregations || []
1572
- if (counting) {
1573
- query = this.addDistinctCount(query)
1574
- } else if (aggregations.length > 0) {
1575
- query = this.addAggregations(query, aggregations)
1576
- } else {
1577
- query = query.select(this.generateSelectStatement())
1578
- }
1579
-
1580
- // have to add after as well (this breaks MS-SQL)
1581
- if (!counting) {
1582
- query = this.addSorting(query)
1583
- }
1584
-
1585
- query = this.addFilters(query, filters, { relationship: true })
1586
-
1587
- // handle relationships with a CTE for all others
1588
- if (relationships?.length && aggregations.length === 0) {
1589
- const mainTable =
1590
- this.query.tableAliases?.[this.query.endpoint.entityId] ||
1591
- this.query.endpoint.entityId
1592
- const cte = this.addSorting(
1593
- this.knex
1594
- .with("paginated", query)
1595
- .select(this.generateSelectStatement())
1596
- .from({
1597
- [mainTable]: "paginated",
1598
- })
1599
- )
1600
- // add JSON aggregations attached to the CTE
1601
- return this.addJsonRelationships(cte, tableName, relationships)
1602
- }
1603
-
1604
- return query
1605
- }
1606
-
1607
- update(opts: QueryOptions): Knex.QueryBuilder {
1608
- const { body, filters } = this.query
1609
- if (!body) {
1610
- throw new Error("Cannot update without row body")
1611
- }
1612
- let query = this.qualifiedKnex()
1613
- const parsedBody = this.parseBody(body)
1614
- query = this.addFilters(query, filters)
1615
- // mysql can't use returning
1616
- if (opts.disableReturning) {
1617
- return query.update(parsedBody)
1618
- } else {
1619
- return query.update(parsedBody).returning("*")
1620
- }
1621
- }
1622
-
1623
- delete(opts: QueryOptions): Knex.QueryBuilder {
1624
- const { filters } = this.query
1625
- let query = this.qualifiedKnex()
1626
- query = this.addFilters(query, filters)
1627
- // mysql can't use returning
1628
- if (opts.disableReturning) {
1629
- return query.delete()
1630
- } else {
1631
- return query.delete().returning(this.generateSelectStatement())
1632
- }
1633
- }
1634
- }
1635
-
1636
- class SqlQueryBuilder extends SqlTableQueryBuilder {
1637
- private readonly limit: number
1638
-
1639
- // pass through client to get flavour of SQL
1640
- constructor(client: SqlClient, limit: number = getBaseLimit()) {
1641
- super(client)
1642
- this.limit = limit
1643
- }
1644
-
1645
- private convertToNative(query: Knex.QueryBuilder, opts: QueryOptions = {}) {
1646
- const sqlClient = this.getSqlClient()
1647
- if (opts?.disableBindings) {
1648
- return { sql: query.toString() }
1649
- } else {
1650
- let native = getNativeSql(query)
1651
- if (sqlClient === SqlClient.SQL_LITE) {
1652
- native = convertBooleans(native)
1653
- }
1654
- return native
1655
- }
1656
- }
1657
-
1658
- /**
1659
- * @param json The JSON query DSL which is to be converted to SQL.
1660
- * @param opts extra options which are to be passed into the query builder, e.g. disableReturning
1661
- * which for the sake of mySQL stops adding the returning statement to inserts, updates and deletes.
1662
- * @return the query ready to be passed to the driver.
1663
- */
1664
- _query(json: QueryJson, opts: QueryOptions = {}): SqlQuery | SqlQuery[] {
1665
- const sqlClient = this.getSqlClient()
1666
- const config: Knex.Config = {
1667
- client: this.getBaseSqlClient(),
1668
- }
1669
- if (sqlClient === SqlClient.SQL_LITE || sqlClient === SqlClient.ORACLE) {
1670
- config.useNullAsDefault = true
1671
- }
1672
- const client = knex(config)
1673
- let query: Knex.QueryBuilder
1674
- const builder = new InternalBuilder(sqlClient, client, json)
1675
- switch (this._operation(json)) {
1676
- case Operation.CREATE:
1677
- query = builder.create(opts)
1678
- break
1679
- case Operation.READ:
1680
- query = builder.read({
1681
- limits: {
1682
- query: this.limit,
1683
- base: getBaseLimit(),
1684
- },
1685
- })
1686
- break
1687
- case Operation.COUNT:
1688
- // read without any limits to count
1689
- query = builder.read()
1690
- break
1691
- case Operation.UPDATE:
1692
- query = builder.update(opts)
1693
- break
1694
- case Operation.DELETE:
1695
- query = builder.delete(opts)
1696
- break
1697
- case Operation.BULK_CREATE:
1698
- query = builder.bulkCreate()
1699
- break
1700
- case Operation.BULK_UPSERT:
1701
- query = builder.bulkUpsert()
1702
- break
1703
- case Operation.CREATE_TABLE:
1704
- case Operation.UPDATE_TABLE:
1705
- case Operation.DELETE_TABLE:
1706
- return this._tableQuery(json)
1707
- default:
1708
- throw `Operation type is not supported by SQL query builder`
1709
- }
1710
-
1711
- return this.convertToNative(query, opts)
1712
- }
1713
-
1714
- async getReturningRow(queryFn: QueryFunction, json: QueryJson) {
1715
- if (!json.extra || !json.extra.idFilter) {
1716
- return {}
1717
- }
1718
- const input = this._query({
1719
- endpoint: {
1720
- ...json.endpoint,
1721
- operation: Operation.READ,
1722
- },
1723
- resource: {
1724
- fields: [],
1725
- },
1726
- filters: json.extra?.idFilter,
1727
- paginate: {
1728
- limit: 1,
1729
- },
1730
- meta: json.meta,
1731
- })
1732
- return queryFn(input, Operation.READ)
1733
- }
1734
-
1735
- // when creating if an ID has been inserted need to make sure
1736
- // the id filter is enriched with it before trying to retrieve the row
1737
- checkLookupKeys(id: any, json: QueryJson) {
1738
- if (!id || !json.meta.table || !json.meta.table.primary) {
1739
- return json
1740
- }
1741
- const primaryKey = json.meta.table.primary?.[0]
1742
- json.extra = {
1743
- idFilter: {
1744
- equal: {
1745
- [primaryKey]: id,
1746
- },
1747
- },
1748
- }
1749
- return json
1750
- }
1751
-
1752
- // this function recreates the returning functionality of postgres
1753
- async queryWithReturning(
1754
- json: QueryJson,
1755
- queryFn: QueryFunction,
1756
- processFn: Function = (result: any) => result
1757
- ) {
1758
- const sqlClient = this.getSqlClient()
1759
- const operation = this._operation(json)
1760
- const input = this._query(json, { disableReturning: true })
1761
- if (Array.isArray(input)) {
1762
- const responses = []
1763
- for (let query of input) {
1764
- responses.push(await queryFn(query, operation))
1765
- }
1766
- return responses
1767
- }
1768
- let row
1769
- // need to manage returning, a feature mySQL can't do
1770
- if (operation === Operation.DELETE) {
1771
- row = processFn(await this.getReturningRow(queryFn, json))
1772
- }
1773
- const response = await queryFn(input, operation)
1774
- const results = processFn(response)
1775
- // same as delete, manage returning
1776
- if (operation === Operation.CREATE || operation === Operation.UPDATE) {
1777
- let id
1778
- if (sqlClient === SqlClient.MS_SQL) {
1779
- id = results?.[0].id
1780
- } else if (
1781
- sqlClient === SqlClient.MY_SQL ||
1782
- sqlClient === SqlClient.MARIADB
1783
- ) {
1784
- id = results?.insertId
1785
- }
1786
- row = processFn(
1787
- await this.getReturningRow(queryFn, this.checkLookupKeys(id, json))
1788
- )
1789
- }
1790
- if (operation === Operation.COUNT) {
1791
- return results
1792
- }
1793
- if (operation !== Operation.READ) {
1794
- return row
1795
- }
1796
- return results.length ? results : [{ [operation.toLowerCase()]: true }]
1797
- }
1798
-
1799
- private getTableName(
1800
- table: Table,
1801
- aliases?: Record<string, string>
1802
- ): string | undefined {
1803
- let name = table.name
1804
- if (
1805
- table.sourceType === TableSourceType.INTERNAL ||
1806
- table.sourceId === INTERNAL_TABLE_SOURCE_ID
1807
- ) {
1808
- if (!table._id) {
1809
- return
1810
- }
1811
- // SQS uses the table ID rather than the table name
1812
- name = table._id
1813
- }
1814
- return aliases?.[name] || name
1815
- }
1816
-
1817
- convertJsonStringColumns<T extends Record<string, any>>(
1818
- table: Table,
1819
- results: T[],
1820
- aliases?: Record<string, string>
1821
- ): T[] {
1822
- const tableName = this.getTableName(table, aliases)
1823
- for (const [name, field] of Object.entries(table.schema)) {
1824
- if (!this._isJsonColumn(field)) {
1825
- continue
1826
- }
1827
- const fullName = `${tableName}.${name}` as keyof T
1828
- for (let row of results) {
1829
- if (typeof row[fullName] === "string") {
1830
- row[fullName] = JSON.parse(row[fullName])
1831
- }
1832
- if (typeof row[name] === "string") {
1833
- row[name as keyof T] = JSON.parse(row[name])
1834
- }
1835
- }
1836
- }
1837
- return results
1838
- }
1839
-
1840
- _isJsonColumn(
1841
- field: FieldSchema
1842
- ): field is JsonFieldMetadata | BBReferenceFieldMetadata {
1843
- return (
1844
- JsonTypes.includes(field.type) &&
1845
- !helpers.schema.isDeprecatedSingleUserColumn(field)
1846
- )
1847
- }
1848
-
1849
- log(query: string, values?: SqlQueryBinding) {
1850
- sqlLog(this.getSqlClient(), query, values)
1851
- }
1852
- }
1853
-
1854
- export default SqlQueryBuilder