@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.
- package/dist/index.js.map +1 -1
- package/dist/index.js.meta.json +1 -1
- package/dist/package.json +11 -4
- package/dist/plugins.js.meta.json +1 -1
- package/package.json +11 -4
- package/src/accounts/accounts.ts +0 -82
- package/src/accounts/api.ts +0 -59
- package/src/accounts/index.ts +0 -1
- package/src/auth/auth.ts +0 -210
- package/src/auth/index.ts +0 -1
- package/src/auth/tests/auth.spec.ts +0 -14
- package/src/blacklist/blacklist.ts +0 -54
- package/src/blacklist/index.ts +0 -1
- package/src/blacklist/tests/blacklist.spec.ts +0 -46
- package/src/cache/appMetadata.ts +0 -88
- package/src/cache/base/index.ts +0 -150
- package/src/cache/docWritethrough.ts +0 -105
- package/src/cache/generic.ts +0 -33
- package/src/cache/index.ts +0 -8
- package/src/cache/invite.ts +0 -86
- package/src/cache/passwordReset.ts +0 -49
- package/src/cache/tests/docWritethrough.spec.ts +0 -296
- package/src/cache/tests/user.spec.ts +0 -145
- package/src/cache/tests/writethrough.spec.ts +0 -139
- package/src/cache/user.ts +0 -154
- package/src/cache/writethrough.ts +0 -133
- package/src/configs/configs.ts +0 -263
- package/src/configs/index.ts +0 -1
- package/src/configs/tests/configs.spec.ts +0 -184
- package/src/constants/db.ts +0 -75
- package/src/constants/index.ts +0 -2
- package/src/constants/misc.ts +0 -36
- package/src/context/Context.ts +0 -14
- package/src/context/identity.ts +0 -58
- package/src/context/index.ts +0 -3
- package/src/context/mainContext.ts +0 -422
- package/src/context/tests/index.spec.ts +0 -255
- package/src/context/types.ts +0 -26
- package/src/db/Replication.ts +0 -94
- package/src/db/couch/DatabaseImpl.ts +0 -511
- package/src/db/couch/connections.ts +0 -89
- package/src/db/couch/index.ts +0 -4
- package/src/db/couch/pouchDB.ts +0 -97
- package/src/db/couch/pouchDump.ts +0 -0
- package/src/db/couch/tests/DatabaseImpl.spec.ts +0 -118
- package/src/db/couch/utils.ts +0 -55
- package/src/db/db.ts +0 -34
- package/src/db/errors.ts +0 -14
- package/src/db/index.ts +0 -12
- package/src/db/instrumentation.ts +0 -199
- package/src/db/lucene.ts +0 -721
- package/src/db/searchIndexes/index.ts +0 -1
- package/src/db/searchIndexes/searchIndexes.ts +0 -62
- package/src/db/tests/DatabaseImpl.spec.ts +0 -55
- package/src/db/tests/connections.spec.ts +0 -22
- package/src/db/tests/index.spec.ts +0 -32
- package/src/db/tests/lucene.spec.ts +0 -400
- package/src/db/tests/pouch.spec.js +0 -62
- package/src/db/tests/utils.spec.ts +0 -63
- package/src/db/utils.ts +0 -208
- package/src/db/views.ts +0 -245
- package/src/docIds/conversions.ts +0 -60
- package/src/docIds/ids.ts +0 -126
- package/src/docIds/index.ts +0 -2
- package/src/docIds/newid.ts +0 -5
- package/src/docIds/params.ts +0 -189
- package/src/docUpdates/index.ts +0 -24
- package/src/environment.ts +0 -293
- package/src/errors/errors.ts +0 -119
- package/src/errors/index.ts +0 -1
- package/src/events/analytics.ts +0 -6
- package/src/events/asyncEvents/index.ts +0 -2
- package/src/events/asyncEvents/publisher.ts +0 -12
- package/src/events/asyncEvents/queue.ts +0 -22
- package/src/events/backfill.ts +0 -183
- package/src/events/documentId.ts +0 -56
- package/src/events/events.ts +0 -47
- package/src/events/identification.ts +0 -311
- package/src/events/index.ts +0 -15
- package/src/events/processors/AnalyticsProcessor.ts +0 -64
- package/src/events/processors/AuditLogsProcessor.ts +0 -92
- package/src/events/processors/LoggingProcessor.ts +0 -36
- package/src/events/processors/Processors.ts +0 -52
- package/src/events/processors/async/DocumentUpdateProcessor.ts +0 -38
- package/src/events/processors/index.ts +0 -19
- package/src/events/processors/posthog/PosthogProcessor.ts +0 -118
- package/src/events/processors/posthog/index.ts +0 -3
- package/src/events/processors/posthog/rateLimiting.ts +0 -106
- package/src/events/processors/posthog/tests/PosthogProcessor.spec.ts +0 -164
- package/src/events/processors/types.ts +0 -1
- package/src/events/publishers/account.ts +0 -41
- package/src/events/publishers/ai.ts +0 -21
- package/src/events/publishers/app.ts +0 -168
- package/src/events/publishers/auditLog.ts +0 -26
- package/src/events/publishers/auth.ts +0 -73
- package/src/events/publishers/automation.ts +0 -110
- package/src/events/publishers/backfill.ts +0 -74
- package/src/events/publishers/backup.ts +0 -42
- package/src/events/publishers/datasource.ts +0 -48
- package/src/events/publishers/email.ts +0 -17
- package/src/events/publishers/environmentVariable.ts +0 -38
- package/src/events/publishers/group.ts +0 -99
- package/src/events/publishers/index.ts +0 -25
- package/src/events/publishers/installation.ts +0 -38
- package/src/events/publishers/layout.ts +0 -26
- package/src/events/publishers/license.ts +0 -84
- package/src/events/publishers/org.ts +0 -37
- package/src/events/publishers/plugin.ts +0 -47
- package/src/events/publishers/query.ts +0 -89
- package/src/events/publishers/role.ts +0 -62
- package/src/events/publishers/rows.ts +0 -29
- package/src/events/publishers/screen.ts +0 -36
- package/src/events/publishers/serve.ts +0 -43
- package/src/events/publishers/table.ts +0 -70
- package/src/events/publishers/user.ts +0 -202
- package/src/events/publishers/view.ts +0 -107
- package/src/features/features.ts +0 -277
- package/src/features/index.ts +0 -2
- package/src/features/tests/features.spec.ts +0 -267
- package/src/features/tests/utils.ts +0 -64
- package/src/helpers.ts +0 -9
- package/src/index.ts +0 -59
- package/src/installation.ts +0 -115
- package/src/logging/alerts.ts +0 -26
- package/src/logging/correlation/correlation.ts +0 -15
- package/src/logging/correlation/index.ts +0 -1
- package/src/logging/correlation/middleware.ts +0 -18
- package/src/logging/index.ts +0 -4
- package/src/logging/pino/logger.ts +0 -239
- package/src/logging/pino/middleware.ts +0 -48
- package/src/logging/system.ts +0 -81
- package/src/logging/tests/system.spec.ts +0 -61
- package/src/middleware/adminOnly.ts +0 -9
- package/src/middleware/auditLog.ts +0 -6
- package/src/middleware/authenticated.ts +0 -247
- package/src/middleware/builderOnly.ts +0 -21
- package/src/middleware/builderOrAdmin.ts +0 -21
- package/src/middleware/contentSecurityPolicy.ts +0 -113
- package/src/middleware/csrf.ts +0 -81
- package/src/middleware/errorHandling.ts +0 -43
- package/src/middleware/index.ts +0 -24
- package/src/middleware/internalApi.ts +0 -23
- package/src/middleware/ip.ts +0 -12
- package/src/middleware/joi-validator.ts +0 -58
- package/src/middleware/matchers.ts +0 -39
- package/src/middleware/passport/datasource/google.ts +0 -102
- package/src/middleware/passport/local.ts +0 -54
- package/src/middleware/passport/sso/google.ts +0 -77
- package/src/middleware/passport/sso/oidc.ts +0 -152
- package/src/middleware/passport/sso/sso.ts +0 -138
- package/src/middleware/passport/sso/tests/google.spec.ts +0 -68
- package/src/middleware/passport/sso/tests/oidc.spec.ts +0 -144
- package/src/middleware/passport/sso/tests/sso.spec.ts +0 -197
- package/src/middleware/passport/utils.ts +0 -38
- package/src/middleware/querystringToBody.ts +0 -28
- package/src/middleware/tenancy.ts +0 -36
- package/src/middleware/tests/builder.spec.ts +0 -181
- package/src/middleware/tests/contentSecurityPolicy.spec.ts +0 -75
- package/src/middleware/tests/matchers.spec.ts +0 -100
- package/src/migrations/definitions.ts +0 -40
- package/src/migrations/index.ts +0 -2
- package/src/migrations/migrations.ts +0 -186
- package/src/migrations/tests/__snapshots__/migrations.spec.ts.snap +0 -11
- package/src/migrations/tests/migrations.spec.ts +0 -64
- package/src/objectStore/buckets/app.ts +0 -53
- package/src/objectStore/buckets/global.ts +0 -29
- package/src/objectStore/buckets/index.ts +0 -3
- package/src/objectStore/buckets/plugins.ts +0 -71
- package/src/objectStore/buckets/tests/app.spec.ts +0 -161
- package/src/objectStore/buckets/tests/global.spec.ts +0 -74
- package/src/objectStore/buckets/tests/plugins.spec.ts +0 -111
- package/src/objectStore/cloudfront.ts +0 -41
- package/src/objectStore/index.ts +0 -3
- package/src/objectStore/objectStore.ts +0 -585
- package/src/objectStore/utils.ts +0 -113
- package/src/platform/index.ts +0 -3
- package/src/platform/platformDb.ts +0 -6
- package/src/platform/tenants.ts +0 -101
- package/src/platform/tests/tenants.spec.ts +0 -26
- package/src/platform/users.ts +0 -129
- package/src/plugin/index.ts +0 -1
- package/src/plugin/tests/validation.spec.ts +0 -209
- package/src/plugin/utils.ts +0 -175
- package/src/queue/constants.ts +0 -8
- package/src/queue/inMemoryQueue.ts +0 -189
- package/src/queue/index.ts +0 -2
- package/src/queue/listeners.ts +0 -199
- package/src/queue/queue.ts +0 -84
- package/src/redis/index.ts +0 -6
- package/src/redis/init.ts +0 -118
- package/src/redis/redis.ts +0 -358
- package/src/redis/redlockImpl.ts +0 -155
- package/src/redis/tests/redis.spec.ts +0 -207
- package/src/redis/tests/redlockImpl.spec.ts +0 -105
- package/src/redis/utils.ts +0 -128
- package/src/security/auth.ts +0 -24
- package/src/security/encryption.ts +0 -185
- package/src/security/index.ts +0 -1
- package/src/security/permissions.ts +0 -166
- package/src/security/roles.ts +0 -655
- package/src/security/secrets.ts +0 -20
- package/src/security/sessions.ts +0 -123
- package/src/security/tests/auth.spec.ts +0 -45
- package/src/security/tests/encryption.spec.ts +0 -31
- package/src/security/tests/permissions.spec.ts +0 -146
- package/src/security/tests/secrets.spec.ts +0 -35
- package/src/security/tests/sessions.spec.ts +0 -12
- package/src/sql/designDoc.ts +0 -17
- package/src/sql/index.ts +0 -5
- package/src/sql/sql.ts +0 -1854
- package/src/sql/sqlTable.ts +0 -319
- package/src/sql/utils.ts +0 -193
- package/src/tenancy/db.ts +0 -6
- package/src/tenancy/index.ts +0 -2
- package/src/tenancy/tenancy.ts +0 -148
- package/src/tenancy/tests/tenancy.spec.ts +0 -184
- package/src/timers/index.ts +0 -1
- package/src/timers/timers.ts +0 -22
- package/src/users/db.ts +0 -582
- package/src/users/events.ts +0 -176
- package/src/users/index.ts +0 -4
- package/src/users/lookup.ts +0 -99
- package/src/users/test/db.spec.ts +0 -188
- package/src/users/test/utils.spec.ts +0 -67
- package/src/users/users.ts +0 -353
- package/src/users/utils.ts +0 -81
- package/src/utils/Duration.ts +0 -56
- package/src/utils/hashing.ts +0 -15
- package/src/utils/index.ts +0 -4
- package/src/utils/stringUtils.ts +0 -8
- package/src/utils/tests/Duration.spec.ts +0 -19
- package/src/utils/tests/utils.spec.ts +0 -204
- package/src/utils/utils.ts +0 -249
- package/tests/core/logging.ts +0 -34
- package/tests/core/users/users.spec.js +0 -53
- package/tests/core/utilities/index.ts +0 -7
- package/tests/core/utilities/jestUtils.ts +0 -33
- package/tests/core/utilities/mocks/alerts.ts +0 -4
- package/tests/core/utilities/mocks/date.ts +0 -3
- package/tests/core/utilities/mocks/events.ts +0 -132
- package/tests/core/utilities/mocks/index.ts +0 -9
- package/tests/core/utilities/mocks/licenses.ts +0 -119
- package/tests/core/utilities/queue.ts +0 -9
- package/tests/core/utilities/structures/Chance.ts +0 -20
- package/tests/core/utilities/structures/accounts.ts +0 -80
- package/tests/core/utilities/structures/apps.ts +0 -21
- package/tests/core/utilities/structures/common.ts +0 -7
- package/tests/core/utilities/structures/db.ts +0 -12
- package/tests/core/utilities/structures/documents/index.ts +0 -1
- package/tests/core/utilities/structures/documents/platform/index.ts +0 -1
- package/tests/core/utilities/structures/documents/platform/installation.ts +0 -12
- package/tests/core/utilities/structures/generator.ts +0 -3
- package/tests/core/utilities/structures/index.ts +0 -15
- package/tests/core/utilities/structures/koa.ts +0 -16
- package/tests/core/utilities/structures/licenses.ts +0 -190
- package/tests/core/utilities/structures/plugins.ts +0 -19
- package/tests/core/utilities/structures/quotas.ts +0 -72
- package/tests/core/utilities/structures/scim.ts +0 -80
- package/tests/core/utilities/structures/sso.ts +0 -118
- package/tests/core/utilities/structures/tenants.ts +0 -5
- package/tests/core/utilities/structures/userGroups.ts +0 -10
- package/tests/core/utilities/structures/users.ts +0 -89
- package/tests/core/utilities/testContainerUtils.ts +0 -165
- package/tests/core/utilities/utils/index.ts +0 -2
- package/tests/core/utilities/utils/queue.ts +0 -27
- package/tests/core/utilities/utils/time.ts +0 -3
- package/tests/extra/DBTestConfiguration.ts +0 -36
- package/tests/extra/index.ts +0 -2
- package/tests/extra/testEnv.ts +0 -95
- package/tests/index.ts +0 -2
- package/tests/jestEnv.ts +0 -10
- 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
|