2021-06-24 19:16:48 +02:00
|
|
|
import { Knex, knex } from "knex"
|
2024-05-16 18:33:47 +02:00
|
|
|
import * as dbCore from "../db"
|
2024-05-28 19:30:06 +02:00
|
|
|
import {
|
|
|
|
getNativeSql,
|
|
|
|
isExternalTable,
|
2024-07-30 19:22:20 +02:00
|
|
|
isInvalidISODateString,
|
2024-06-14 15:35:35 +02:00
|
|
|
isValidFilter,
|
2024-07-30 19:22:20 +02:00
|
|
|
isValidISODateString,
|
2024-07-04 19:29:08 +02:00
|
|
|
sqlLog,
|
2024-09-03 19:24:50 +02:00
|
|
|
validateManyToMany,
|
2024-05-28 19:30:06 +02:00
|
|
|
} from "./utils"
|
2023-09-14 17:53:36 +02:00
|
|
|
import SqlTableQueryBuilder from "./sqlTable"
|
2021-06-24 19:17:26 +02:00
|
|
|
import {
|
2024-09-04 10:29:05 +02:00
|
|
|
Aggregation,
|
2024-07-09 14:01:44 +02:00
|
|
|
AnySearchFilter,
|
2024-07-30 12:03:54 +02:00
|
|
|
ArrayOperator,
|
|
|
|
BasicOperator,
|
2024-03-13 14:38:08 +01:00
|
|
|
BBReferenceFieldMetadata,
|
2024-09-04 10:29:05 +02:00
|
|
|
CalculationType,
|
2024-03-12 16:27:34 +01:00
|
|
|
FieldSchema,
|
|
|
|
FieldType,
|
2024-06-18 19:44:17 +02:00
|
|
|
INTERNAL_TABLE_SOURCE_ID,
|
2024-07-09 12:24:59 +02:00
|
|
|
InternalSearchFilterOperator,
|
2024-03-13 14:38:08 +01:00
|
|
|
JsonFieldMetadata,
|
2024-06-18 19:44:17 +02:00
|
|
|
JsonTypes,
|
2021-11-24 21:55:03 +01:00
|
|
|
Operation,
|
2024-06-18 19:44:17 +02:00
|
|
|
prefixed,
|
2021-06-24 19:17:26 +02:00
|
|
|
QueryJson,
|
2024-06-18 19:44:17 +02:00
|
|
|
QueryOptions,
|
2024-07-30 12:03:54 +02:00
|
|
|
RangeOperator,
|
2021-06-25 19:13:11 +02:00
|
|
|
RelationshipsJson,
|
2021-10-28 20:39:42 +02:00
|
|
|
SearchFilters,
|
2024-06-18 19:44:17 +02:00
|
|
|
SortOrder,
|
2024-06-14 15:35:35 +02:00
|
|
|
SqlClient,
|
|
|
|
SqlQuery,
|
2024-03-06 19:07:46 +01:00
|
|
|
SqlQueryBinding,
|
2024-03-12 16:27:34 +01:00
|
|
|
Table,
|
2024-04-15 19:23:39 +02:00
|
|
|
TableSourceType,
|
2022-08-11 14:50:05 +02:00
|
|
|
} from "@budibase/types"
|
2024-05-16 18:33:47 +02:00
|
|
|
import environment from "../environment"
|
2024-07-30 12:03:54 +02:00
|
|
|
import { dataFilters, helpers } from "@budibase/shared-core"
|
2024-08-19 17:49:40 +02:00
|
|
|
import { cloneDeep } from "lodash"
|
2021-10-28 20:39:42 +02:00
|
|
|
|
2024-04-04 19:16:23 +02:00
|
|
|
type QueryFunction = (query: SqlQuery | SqlQuery[], operation: Operation) => any
|
2024-03-04 16:47:27 +01:00
|
|
|
|
2024-08-01 14:03:58 +02:00
|
|
|
function getBaseLimit() {
|
|
|
|
const envLimit = environment.SQL_MAX_ROWS
|
|
|
|
? parseInt(environment.SQL_MAX_ROWS)
|
|
|
|
: null
|
|
|
|
return envLimit || 5000
|
|
|
|
}
|
2021-06-24 19:16:48 +02:00
|
|
|
|
2024-09-10 14:59:21 +02:00
|
|
|
function getRelationshipLimit() {
|
|
|
|
const envLimit = environment.SQL_MAX_RELATED_ROWS
|
|
|
|
? parseInt(environment.SQL_MAX_RELATED_ROWS)
|
|
|
|
: null
|
|
|
|
return envLimit || 500
|
|
|
|
}
|
|
|
|
|
2024-09-16 19:27:53 +02:00
|
|
|
function prioritisedArraySort(toSort: string[], priorities: string[]) {
|
|
|
|
return toSort.sort((a, b) => {
|
|
|
|
const aPriority = priorities.find(field => field && a.endsWith(field))
|
|
|
|
const bPriority = priorities.find(field => field && b.endsWith(field))
|
|
|
|
if (aPriority && !bPriority) {
|
|
|
|
return -1
|
|
|
|
}
|
|
|
|
if (!aPriority && bPriority) {
|
|
|
|
return 1
|
|
|
|
}
|
|
|
|
return a.localeCompare(b)
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2024-05-07 18:41:43 +02:00
|
|
|
function convertBooleans(query: SqlQuery | SqlQuery[]): SqlQuery | SqlQuery[] {
|
|
|
|
if (Array.isArray(query)) {
|
|
|
|
return query.map((q: SqlQuery) => convertBooleans(q) as SqlQuery)
|
|
|
|
} else {
|
|
|
|
if (query.bindings) {
|
|
|
|
query.bindings = query.bindings.map(binding => {
|
|
|
|
if (typeof binding === "boolean") {
|
|
|
|
return binding ? 1 : 0
|
|
|
|
}
|
|
|
|
return binding
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return query
|
|
|
|
}
|
|
|
|
|
2021-11-05 14:48:13 +01:00
|
|
|
class InternalBuilder {
|
2024-07-17 16:45:35 +02:00
|
|
|
private readonly client: SqlClient
|
2024-07-29 19:11:05 +02:00
|
|
|
private readonly query: QueryJson
|
2024-07-30 12:03:54 +02:00
|
|
|
private readonly splitter: dataFilters.ColumnSplitter
|
2024-07-30 12:54:46 +02:00
|
|
|
private readonly knex: Knex
|
2021-11-05 14:48:13 +01:00
|
|
|
|
2024-07-30 12:54:46 +02:00
|
|
|
constructor(client: SqlClient, knex: Knex, query: QueryJson) {
|
2021-11-05 14:48:13 +01:00
|
|
|
this.client = client
|
2024-07-29 19:11:05 +02:00
|
|
|
this.query = query
|
2024-07-30 12:54:46 +02:00
|
|
|
this.knex = knex
|
2024-07-30 12:03:54 +02:00
|
|
|
|
|
|
|
this.splitter = new dataFilters.ColumnSplitter([this.table], {
|
|
|
|
aliases: this.query.tableAliases,
|
|
|
|
columnPrefix: this.query.meta.columnPrefix,
|
|
|
|
})
|
2024-07-29 19:11:05 +02:00
|
|
|
}
|
|
|
|
|
2024-09-05 20:04:45 +02:00
|
|
|
// states the various situations in which we need a full mapped select statement
|
|
|
|
private readonly SPECIAL_SELECT_CASES = {
|
|
|
|
POSTGRES_MONEY: (field: FieldSchema | undefined) => {
|
|
|
|
return (
|
|
|
|
this.client === SqlClient.POSTGRES &&
|
|
|
|
field?.externalType?.includes("money")
|
|
|
|
)
|
|
|
|
},
|
|
|
|
MSSQL_DATES: (field: FieldSchema | undefined) => {
|
|
|
|
return (
|
|
|
|
this.client === SqlClient.MS_SQL &&
|
|
|
|
field?.type === FieldType.DATETIME &&
|
|
|
|
field.timeOnly
|
|
|
|
)
|
|
|
|
},
|
|
|
|
}
|
|
|
|
|
2024-07-29 19:11:05 +02:00
|
|
|
get table(): Table {
|
|
|
|
return this.query.meta.table
|
2021-06-03 17:31:24 +02:00
|
|
|
}
|
|
|
|
|
2024-07-30 12:54:46 +02:00
|
|
|
getFieldSchema(key: string): FieldSchema | undefined {
|
|
|
|
const { column } = this.splitter.run(key)
|
|
|
|
return this.table.schema[column]
|
|
|
|
}
|
|
|
|
|
2024-07-29 17:54:59 +02:00
|
|
|
// Takes a string like foo and returns a quoted string like [foo] for SQL Server
|
|
|
|
// and "foo" for Postgres.
|
|
|
|
private quote(str: string): string {
|
|
|
|
switch (this.client) {
|
|
|
|
case SqlClient.SQL_LITE:
|
|
|
|
case SqlClient.ORACLE:
|
|
|
|
case SqlClient.POSTGRES:
|
|
|
|
return `"${str}"`
|
|
|
|
case SqlClient.MS_SQL:
|
|
|
|
return `[${str}]`
|
2024-09-24 13:01:47 +02:00
|
|
|
case SqlClient.MARIADB:
|
2024-07-29 17:54:59 +02:00
|
|
|
case SqlClient.MY_SQL:
|
|
|
|
return `\`${str}\``
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Takes a string like a.b.c and returns a quoted identifier like [a].[b].[c]
|
|
|
|
// for SQL Server and `a`.`b`.`c` for MySQL.
|
|
|
|
private quotedIdentifier(key: string): string {
|
|
|
|
return key
|
|
|
|
.split(".")
|
|
|
|
.map(part => this.quote(part))
|
|
|
|
.join(".")
|
|
|
|
}
|
|
|
|
|
2024-09-05 20:04:45 +02:00
|
|
|
private isFullSelectStatementRequired(): boolean {
|
|
|
|
const { meta } = this.query
|
|
|
|
for (let column of Object.values(meta.table.schema)) {
|
|
|
|
if (this.SPECIAL_SELECT_CASES.POSTGRES_MONEY(column)) {
|
|
|
|
return true
|
|
|
|
} else if (this.SPECIAL_SELECT_CASES.MSSQL_DATES(column)) {
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
2024-07-30 12:54:46 +02:00
|
|
|
private generateSelectStatement(): (string | Knex.Raw)[] | "*" {
|
2024-10-01 12:48:14 +02:00
|
|
|
const { meta, endpoint, resource } = this.query
|
2024-07-29 17:54:59 +02:00
|
|
|
|
|
|
|
if (!resource || !resource.fields || resource.fields.length === 0) {
|
|
|
|
return "*"
|
|
|
|
}
|
|
|
|
|
2024-10-01 12:48:14 +02:00
|
|
|
const alias = this.getTableName(endpoint.entityId)
|
2024-09-05 20:04:45 +02:00
|
|
|
const schema = meta.table.schema
|
|
|
|
if (!this.isFullSelectStatementRequired()) {
|
2024-08-23 19:00:52 +02:00
|
|
|
return [this.knex.raw(`${this.quote(alias)}.*`)]
|
|
|
|
}
|
2024-09-05 20:04:45 +02:00
|
|
|
// get just the fields for this table
|
|
|
|
return resource.fields
|
|
|
|
.map(field => {
|
|
|
|
const parts = field.split(/\./g)
|
|
|
|
let table: string | undefined = undefined
|
|
|
|
let column = parts[0]
|
|
|
|
|
|
|
|
// Just a column name, e.g.: "column"
|
|
|
|
if (parts.length > 1) {
|
|
|
|
table = parts[0]
|
|
|
|
column = parts.slice(1).join(".")
|
|
|
|
}
|
2024-08-23 19:00:52 +02:00
|
|
|
|
2024-09-05 20:04:45 +02:00
|
|
|
return { table, column, field }
|
|
|
|
})
|
|
|
|
.filter(({ table }) => !table || table === alias)
|
|
|
|
.map(({ table, column, field }) => {
|
|
|
|
const columnSchema = schema[column]
|
|
|
|
|
|
|
|
if (this.SPECIAL_SELECT_CASES.POSTGRES_MONEY(columnSchema)) {
|
|
|
|
return this.knex.raw(
|
|
|
|
`${this.quotedIdentifier(
|
|
|
|
[table, column].join(".")
|
|
|
|
)}::money::numeric as ${this.quote(field)}`
|
|
|
|
)
|
|
|
|
}
|
2024-07-29 17:54:59 +02:00
|
|
|
|
2024-09-05 20:04:45 +02:00
|
|
|
if (this.SPECIAL_SELECT_CASES.MSSQL_DATES(columnSchema)) {
|
|
|
|
// Time gets returned as timestamp from mssql, not matching the expected
|
|
|
|
// HH:mm format
|
|
|
|
return this.knex.raw(`CONVERT(varchar, ${field}, 108) as "${field}"`)
|
|
|
|
}
|
2024-07-29 17:54:59 +02:00
|
|
|
|
2024-09-05 20:04:45 +02:00
|
|
|
const quoted = table
|
|
|
|
? `${this.quote(table)}.${this.quote(column)}`
|
|
|
|
: this.quote(field)
|
|
|
|
return this.knex.raw(quoted)
|
|
|
|
})
|
2024-07-29 17:54:59 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
// OracleDB can't use character-large-objects (CLOBs) in WHERE clauses,
|
|
|
|
// so when we use them we need to wrap them in to_char(). This function
|
|
|
|
// converts a field name to the appropriate identifier.
|
2024-07-29 19:11:05 +02:00
|
|
|
private convertClobs(field: string): string {
|
2024-07-29 17:54:59 +02:00
|
|
|
const parts = field.split(".")
|
|
|
|
const col = parts.pop()!
|
2024-07-29 19:11:05 +02:00
|
|
|
const schema = this.table.schema[col]
|
2024-07-29 17:54:59 +02:00
|
|
|
let identifier = this.quotedIdentifier(field)
|
|
|
|
if (
|
|
|
|
schema.type === FieldType.STRING ||
|
|
|
|
schema.type === FieldType.LONGFORM ||
|
|
|
|
schema.type === FieldType.BB_REFERENCE_SINGLE ||
|
2024-07-30 18:41:39 +02:00
|
|
|
schema.type === FieldType.BB_REFERENCE ||
|
2024-07-29 17:54:59 +02:00
|
|
|
schema.type === FieldType.OPTIONS ||
|
|
|
|
schema.type === FieldType.BARCODEQR
|
|
|
|
) {
|
|
|
|
identifier = `to_char(${identifier})`
|
|
|
|
}
|
|
|
|
return identifier
|
|
|
|
}
|
|
|
|
|
2024-07-30 12:03:54 +02:00
|
|
|
private parse(input: any, schema: FieldSchema) {
|
2024-07-29 19:20:49 +02:00
|
|
|
if (Array.isArray(input)) {
|
|
|
|
return JSON.stringify(input)
|
|
|
|
}
|
2024-07-29 17:54:59 +02:00
|
|
|
if (input == undefined) {
|
|
|
|
return null
|
|
|
|
}
|
2024-07-30 12:26:16 +02:00
|
|
|
|
|
|
|
if (
|
|
|
|
this.client === SqlClient.ORACLE &&
|
|
|
|
schema.type === FieldType.DATETIME &&
|
|
|
|
schema.timeOnly
|
|
|
|
) {
|
|
|
|
if (input instanceof Date) {
|
|
|
|
const hours = input.getHours().toString().padStart(2, "0")
|
|
|
|
const minutes = input.getMinutes().toString().padStart(2, "0")
|
|
|
|
const seconds = input.getSeconds().toString().padStart(2, "0")
|
|
|
|
return `${hours}:${minutes}:${seconds}`
|
|
|
|
}
|
|
|
|
if (typeof input === "string") {
|
2024-07-30 17:03:03 +02:00
|
|
|
return new Date(`1970-01-01T${input}Z`)
|
2024-07-30 12:26:16 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-07-30 12:03:54 +02:00
|
|
|
if (typeof input === "string") {
|
|
|
|
if (isInvalidISODateString(input)) {
|
|
|
|
return null
|
|
|
|
}
|
|
|
|
if (isValidISODateString(input)) {
|
|
|
|
return new Date(input.trim())
|
|
|
|
}
|
2024-07-29 17:54:59 +02:00
|
|
|
}
|
|
|
|
return input
|
|
|
|
}
|
|
|
|
|
2024-07-29 19:11:05 +02:00
|
|
|
private parseBody(body: any) {
|
2024-07-29 17:54:59 +02:00
|
|
|
for (let [key, value] of Object.entries(body)) {
|
2024-07-30 12:03:54 +02:00
|
|
|
const { column } = this.splitter.run(key)
|
|
|
|
const schema = this.table.schema[column]
|
|
|
|
if (!schema) {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
body[key] = this.parse(value, schema)
|
2024-07-29 17:54:59 +02:00
|
|
|
}
|
|
|
|
return body
|
|
|
|
}
|
|
|
|
|
2024-07-30 12:03:54 +02:00
|
|
|
private parseFilters(filters: SearchFilters): SearchFilters {
|
2024-08-19 17:49:40 +02:00
|
|
|
filters = cloneDeep(filters)
|
2024-07-30 12:03:54 +02:00
|
|
|
for (const op of Object.values(BasicOperator)) {
|
|
|
|
const filter = filters[op]
|
|
|
|
if (!filter) {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
for (const key of Object.keys(filter)) {
|
|
|
|
if (Array.isArray(filter[key])) {
|
|
|
|
filter[key] = JSON.stringify(filter[key])
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
const { column } = this.splitter.run(key)
|
|
|
|
const schema = this.table.schema[column]
|
|
|
|
if (!schema) {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
filter[key] = this.parse(filter[key], schema)
|
2024-07-29 17:54:59 +02:00
|
|
|
}
|
|
|
|
}
|
2024-07-29 19:20:49 +02:00
|
|
|
|
2024-07-30 12:03:54 +02:00
|
|
|
for (const op of Object.values(ArrayOperator)) {
|
|
|
|
const filter = filters[op]
|
|
|
|
if (!filter) {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
for (const key of Object.keys(filter)) {
|
|
|
|
const { column } = this.splitter.run(key)
|
|
|
|
const schema = this.table.schema[column]
|
|
|
|
if (!schema) {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
filter[key] = filter[key].map(v => this.parse(v, schema))
|
|
|
|
}
|
|
|
|
}
|
2024-07-29 19:20:49 +02:00
|
|
|
|
2024-07-30 12:03:54 +02:00
|
|
|
for (const op of Object.values(RangeOperator)) {
|
|
|
|
const filter = filters[op]
|
|
|
|
if (!filter) {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
for (const key of Object.keys(filter)) {
|
|
|
|
const { column } = this.splitter.run(key)
|
|
|
|
const schema = this.table.schema[column]
|
|
|
|
if (!schema) {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
const value = filter[key]
|
|
|
|
if ("low" in value) {
|
|
|
|
value.low = this.parse(value.low, schema)
|
|
|
|
}
|
|
|
|
if ("high" in value) {
|
|
|
|
value.high = this.parse(value.high, schema)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2024-07-29 19:20:49 +02:00
|
|
|
|
2024-07-30 12:03:54 +02:00
|
|
|
return filters
|
|
|
|
}
|
2024-07-29 19:20:49 +02:00
|
|
|
|
2024-08-29 19:56:14 +02:00
|
|
|
addJoinFieldCheck(query: Knex.QueryBuilder, relationship: RelationshipsJson) {
|
|
|
|
const document = relationship.from?.split(".")[0] || ""
|
|
|
|
return query.andWhere(`${document}.fieldName`, "=", relationship.column)
|
|
|
|
}
|
|
|
|
|
2024-08-27 19:34:05 +02:00
|
|
|
addRelationshipForFilter(
|
|
|
|
query: Knex.QueryBuilder,
|
|
|
|
filterKey: string,
|
|
|
|
whereCb: (query: Knex.QueryBuilder) => Knex.QueryBuilder
|
|
|
|
): Knex.QueryBuilder {
|
|
|
|
const mainKnex = this.knex
|
2024-08-28 19:41:02 +02:00
|
|
|
const { relationships, endpoint, tableAliases: aliases } = this.query
|
2024-08-27 19:34:05 +02:00
|
|
|
const tableName = endpoint.entityId
|
2024-08-28 19:55:15 +02:00
|
|
|
const fromAlias = aliases?.[tableName] || tableName
|
2024-08-29 18:58:11 +02:00
|
|
|
const matches = (possibleTable: string) =>
|
|
|
|
filterKey.startsWith(`${possibleTable}`)
|
2024-08-27 19:34:05 +02:00
|
|
|
if (!relationships) {
|
|
|
|
return query
|
|
|
|
}
|
|
|
|
for (const relationship of relationships) {
|
2024-08-29 18:58:11 +02:00
|
|
|
const relatedTableName = relationship.tableName
|
|
|
|
const toAlias = aliases?.[relatedTableName] || relatedTableName
|
2024-08-27 19:34:05 +02:00
|
|
|
// this is the relationship which is being filtered
|
2024-08-28 19:41:02 +02:00
|
|
|
if (
|
2024-08-29 18:58:11 +02:00
|
|
|
(matches(relatedTableName) || matches(toAlias)) &&
|
2024-08-28 19:41:02 +02:00
|
|
|
relationship.to &&
|
|
|
|
relationship.tableName
|
|
|
|
) {
|
|
|
|
let subQuery = mainKnex
|
|
|
|
.select(mainKnex.raw(1))
|
2024-08-28 19:55:15 +02:00
|
|
|
.from({ [toAlias]: relatedTableName })
|
2024-09-03 19:24:50 +02:00
|
|
|
const manyToMany = validateManyToMany(relationship)
|
|
|
|
if (manyToMany) {
|
2024-08-28 19:55:15 +02:00
|
|
|
const throughAlias =
|
2024-09-03 19:24:50 +02:00
|
|
|
aliases?.[manyToMany.through] || relationship.through
|
|
|
|
let throughTable = this.tableNameWithSchema(manyToMany.through, {
|
2024-08-28 19:55:15 +02:00
|
|
|
alias: throughAlias,
|
|
|
|
schema: endpoint.schema,
|
|
|
|
})
|
2024-09-03 19:24:50 +02:00
|
|
|
subQuery = subQuery
|
|
|
|
// add a join through the junction table
|
|
|
|
.innerJoin(throughTable, function () {
|
|
|
|
// @ts-ignore
|
|
|
|
this.on(
|
|
|
|
`${toAlias}.${manyToMany.toPrimary}`,
|
|
|
|
"=",
|
|
|
|
`${throughAlias}.${manyToMany.to}`
|
|
|
|
)
|
|
|
|
})
|
|
|
|
// check the document in the junction table points to the main table
|
|
|
|
.where(
|
|
|
|
`${throughAlias}.${manyToMany.from}`,
|
2024-08-28 19:55:15 +02:00
|
|
|
"=",
|
2024-09-03 19:24:50 +02:00
|
|
|
mainKnex.raw(
|
|
|
|
this.quotedIdentifier(`${fromAlias}.${manyToMany.fromPrimary}`)
|
|
|
|
)
|
2024-08-28 19:55:15 +02:00
|
|
|
)
|
2024-09-03 19:24:50 +02:00
|
|
|
// in SQS the same junction table is used for different many-to-many relationships between the
|
|
|
|
// two same tables, this is needed to avoid rows ending up in all columns
|
2024-08-29 19:56:14 +02:00
|
|
|
if (this.client === SqlClient.SQL_LITE) {
|
2024-09-03 19:24:50 +02:00
|
|
|
subQuery = this.addJoinFieldCheck(subQuery, manyToMany)
|
2024-08-29 19:56:14 +02:00
|
|
|
}
|
2024-09-03 19:24:50 +02:00
|
|
|
} else {
|
|
|
|
// "join" to the main table, making sure the ID matches that of the main
|
|
|
|
subQuery = subQuery.where(
|
|
|
|
`${toAlias}.${relationship.to}`,
|
|
|
|
"=",
|
|
|
|
mainKnex.raw(
|
|
|
|
this.quotedIdentifier(`${fromAlias}.${relationship.from}`)
|
|
|
|
)
|
2024-08-27 19:34:05 +02:00
|
|
|
)
|
2024-09-03 19:24:50 +02:00
|
|
|
}
|
2024-08-28 19:55:15 +02:00
|
|
|
query = query.whereExists(whereCb(subQuery))
|
2024-08-27 19:34:05 +02:00
|
|
|
break
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return query
|
|
|
|
}
|
|
|
|
|
2021-11-05 14:48:13 +01:00
|
|
|
// right now we only do filters on the specific table being queried
|
|
|
|
addFilters(
|
2024-02-28 16:18:07 +01:00
|
|
|
query: Knex.QueryBuilder,
|
2022-01-13 18:40:11 +01:00
|
|
|
filters: SearchFilters | undefined,
|
2024-07-29 19:11:05 +02:00
|
|
|
opts?: {
|
2024-06-27 19:18:41 +02:00
|
|
|
relationship?: boolean
|
|
|
|
}
|
2024-02-28 16:18:07 +01:00
|
|
|
): Knex.QueryBuilder {
|
2024-04-17 18:12:26 +02:00
|
|
|
if (!filters) {
|
|
|
|
return query
|
|
|
|
}
|
2024-08-28 19:41:02 +02:00
|
|
|
const builder = this
|
2024-08-21 14:25:48 +02:00
|
|
|
filters = this.parseFilters({ ...filters })
|
2024-07-29 19:11:05 +02:00
|
|
|
const aliases = this.query.tableAliases
|
2024-04-17 18:12:26 +02:00
|
|
|
// if all or specified in filters, then everything is an or
|
|
|
|
const allOr = filters.allOr
|
2024-08-28 19:41:02 +02:00
|
|
|
const isSqlite = this.client === SqlClient.SQL_LITE
|
|
|
|
const tableName = isSqlite ? this.table._id! : this.table.name
|
2024-04-16 18:45:06 +02:00
|
|
|
|
2024-04-15 19:23:39 +02:00
|
|
|
function getTableAlias(name: string) {
|
2024-07-29 19:11:05 +02:00
|
|
|
const alias = aliases?.[name]
|
2024-01-30 18:57:10 +01:00
|
|
|
return alias || name
|
|
|
|
}
|
2021-11-05 14:48:13 +01:00
|
|
|
function iterate(
|
2024-07-09 14:01:44 +02:00
|
|
|
structure: AnySearchFilter,
|
2024-08-28 19:41:02 +02:00
|
|
|
fn: (
|
|
|
|
query: Knex.QueryBuilder,
|
|
|
|
key: string,
|
|
|
|
value: any
|
|
|
|
) => Knex.QueryBuilder,
|
|
|
|
complexKeyFn?: (
|
|
|
|
query: Knex.QueryBuilder,
|
|
|
|
key: string[],
|
|
|
|
value: any
|
|
|
|
) => Knex.QueryBuilder
|
2021-11-05 14:48:13 +01:00
|
|
|
) {
|
2024-08-28 19:41:02 +02:00
|
|
|
const handleRelationship = (
|
|
|
|
q: Knex.QueryBuilder,
|
|
|
|
key: string,
|
|
|
|
value: any
|
|
|
|
) => {
|
|
|
|
const [filterTableName, ...otherProperties] = key.split(".")
|
|
|
|
const property = otherProperties.join(".")
|
|
|
|
const alias = getTableAlias(filterTableName)
|
|
|
|
return fn(q, alias ? `${alias}.${property}` : property, value)
|
|
|
|
}
|
2024-07-09 14:01:44 +02:00
|
|
|
for (const key in structure) {
|
|
|
|
const value = structure[key]
|
2023-02-13 18:13:59 +01:00
|
|
|
const updatedKey = dbCore.removeKeyNumbering(key)
|
2022-08-02 19:34:58 +02:00
|
|
|
const isRelationshipField = updatedKey.includes(".")
|
2024-08-28 19:41:02 +02:00
|
|
|
const shouldProcessRelationship =
|
|
|
|
opts?.relationship && isRelationshipField
|
2024-07-09 10:43:45 +02:00
|
|
|
|
2024-07-09 14:01:44 +02:00
|
|
|
let castedTypeValue
|
|
|
|
if (
|
|
|
|
key === InternalSearchFilterOperator.COMPLEX_ID_OPERATOR &&
|
|
|
|
(castedTypeValue = structure[key]) &&
|
|
|
|
complexKeyFn
|
|
|
|
) {
|
2024-07-09 10:43:45 +02:00
|
|
|
const alias = getTableAlias(tableName)
|
2024-08-28 19:41:02 +02:00
|
|
|
query = complexKeyFn(
|
|
|
|
query,
|
2024-07-09 14:01:44 +02:00
|
|
|
castedTypeValue.id.map((x: string) =>
|
|
|
|
alias ? `${alias}.${x}` : x
|
|
|
|
),
|
|
|
|
castedTypeValue.values
|
2024-07-09 10:43:45 +02:00
|
|
|
)
|
2024-08-15 19:52:17 +02:00
|
|
|
} else if (!isRelationshipField) {
|
2024-05-17 21:26:34 +02:00
|
|
|
const alias = getTableAlias(tableName)
|
2024-08-28 19:41:02 +02:00
|
|
|
query = fn(
|
|
|
|
query,
|
|
|
|
alias ? `${alias}.${updatedKey}` : updatedKey,
|
|
|
|
value
|
|
|
|
)
|
2024-09-03 19:24:50 +02:00
|
|
|
} else if (shouldProcessRelationship) {
|
2024-08-28 19:41:02 +02:00
|
|
|
query = builder.addRelationshipForFilter(query, updatedKey, q => {
|
|
|
|
return handleRelationship(q, updatedKey, value)
|
|
|
|
})
|
2022-01-13 18:40:11 +01:00
|
|
|
}
|
2021-11-05 14:48:13 +01:00
|
|
|
}
|
|
|
|
}
|
2022-07-19 18:25:41 +02:00
|
|
|
|
2024-08-28 19:41:02 +02:00
|
|
|
const like = (q: Knex.QueryBuilder, key: string, value: any) => {
|
2024-05-17 21:26:34 +02:00
|
|
|
const fuzzyOr = filters?.fuzzyOr
|
|
|
|
const fnc = fuzzyOr || allOr ? "orWhere" : "where"
|
2022-07-19 18:25:41 +02:00
|
|
|
// postgres supports ilike, nothing else does
|
2022-08-11 14:50:05 +02:00
|
|
|
if (this.client === SqlClient.POSTGRES) {
|
2024-08-28 19:41:02 +02:00
|
|
|
return q[fnc](key, "ilike", `%${value}%`)
|
2022-07-19 18:25:41 +02:00
|
|
|
} else {
|
|
|
|
const rawFnc = `${fnc}Raw`
|
|
|
|
// @ts-ignore
|
2024-08-28 19:41:02 +02:00
|
|
|
return q[rawFnc](`LOWER(${this.quotedIdentifier(key)}) LIKE ?`, [
|
2024-07-29 17:54:59 +02:00
|
|
|
`%${value.toLowerCase()}%`,
|
|
|
|
])
|
2022-07-19 18:25:41 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-07-09 14:01:44 +02:00
|
|
|
const contains = (mode: AnySearchFilter, any: boolean = false) => {
|
2024-05-20 18:01:52 +02:00
|
|
|
const rawFnc = allOr ? "orWhereRaw" : "whereRaw"
|
2022-07-27 12:40:46 +02:00
|
|
|
const not = mode === filters?.notContains ? "NOT " : ""
|
2022-07-27 17:37:29 +02:00
|
|
|
function stringifyArray(value: Array<any>, quoteStyle = '"'): string {
|
2022-07-27 12:40:46 +02:00
|
|
|
for (let i in value) {
|
|
|
|
if (typeof value[i] === "string") {
|
2022-07-27 17:37:29 +02:00
|
|
|
value[i] = `${quoteStyle}${value[i]}${quoteStyle}`
|
2022-07-27 12:40:46 +02:00
|
|
|
}
|
|
|
|
}
|
2022-07-27 17:37:29 +02:00
|
|
|
return `[${value.join(",")}]`
|
2022-07-27 12:40:46 +02:00
|
|
|
}
|
2022-08-15 13:24:41 +02:00
|
|
|
if (this.client === SqlClient.POSTGRES) {
|
2024-08-28 19:41:02 +02:00
|
|
|
iterate(mode, (q, key, value) => {
|
2022-07-27 17:37:29 +02:00
|
|
|
const wrap = any ? "" : "'"
|
2024-05-20 18:01:52 +02:00
|
|
|
const op = any ? "\\?| array" : "@>"
|
2022-07-27 12:40:46 +02:00
|
|
|
const fieldNames = key.split(/\./g)
|
2024-05-20 18:01:52 +02:00
|
|
|
const table = fieldNames[0]
|
|
|
|
const col = fieldNames[1]
|
2024-08-28 19:41:02 +02:00
|
|
|
return q[rawFnc](
|
2024-05-20 18:01:52 +02:00
|
|
|
`${not}COALESCE("${table}"."${col}"::jsonb ${op} ${wrap}${stringifyArray(
|
2022-07-27 17:40:07 +02:00
|
|
|
value,
|
|
|
|
any ? "'" : '"'
|
2024-05-20 18:01:52 +02:00
|
|
|
)}${wrap}, FALSE)`
|
2022-07-27 12:40:46 +02:00
|
|
|
)
|
|
|
|
})
|
2024-09-24 13:01:47 +02:00
|
|
|
} else if (
|
|
|
|
this.client === SqlClient.MY_SQL ||
|
|
|
|
this.client === SqlClient.MARIADB
|
|
|
|
) {
|
2022-07-27 14:19:47 +02:00
|
|
|
const jsonFnc = any ? "JSON_OVERLAPS" : "JSON_CONTAINS"
|
2024-08-28 19:41:02 +02:00
|
|
|
iterate(mode, (q, key, value) => {
|
|
|
|
return q[rawFnc](
|
2024-05-20 18:01:52 +02:00
|
|
|
`${not}COALESCE(${jsonFnc}(${key}, '${stringifyArray(
|
|
|
|
value
|
|
|
|
)}'), FALSE)`
|
2022-07-27 12:40:46 +02:00
|
|
|
)
|
|
|
|
})
|
|
|
|
} else {
|
2022-07-28 10:20:00 +02:00
|
|
|
const andOr = mode === filters?.containsAny ? " OR " : " AND "
|
2024-08-28 19:41:02 +02:00
|
|
|
iterate(mode, (q, key, value) => {
|
2022-07-28 10:20:00 +02:00
|
|
|
let statement = ""
|
2024-07-30 19:22:20 +02:00
|
|
|
const identifier = this.quotedIdentifier(key)
|
2022-07-27 12:40:46 +02:00
|
|
|
for (let i in value) {
|
|
|
|
if (typeof value[i] === "string") {
|
2023-03-28 12:46:29 +02:00
|
|
|
value[i] = `%"${value[i].toLowerCase()}"%`
|
2022-07-27 12:40:46 +02:00
|
|
|
} else {
|
|
|
|
value[i] = `%${value[i]}%`
|
|
|
|
}
|
2024-07-30 19:22:20 +02:00
|
|
|
statement += `${
|
|
|
|
statement ? andOr : ""
|
|
|
|
}COALESCE(LOWER(${identifier}), '') LIKE ?`
|
2022-07-27 12:40:46 +02:00
|
|
|
}
|
2024-04-15 16:31:46 +02:00
|
|
|
|
|
|
|
if (statement === "") {
|
2024-08-28 19:41:02 +02:00
|
|
|
return q
|
2024-04-15 16:31:46 +02:00
|
|
|
}
|
|
|
|
|
2024-07-30 19:22:20 +02:00
|
|
|
if (not) {
|
2024-08-28 19:41:02 +02:00
|
|
|
return q[rawFnc](
|
2024-07-30 19:22:20 +02:00
|
|
|
`(NOT (${statement}) OR ${identifier} IS NULL)`,
|
|
|
|
value
|
|
|
|
)
|
|
|
|
} else {
|
2024-08-28 19:41:02 +02:00
|
|
|
return q[rawFnc](statement, value)
|
2024-07-30 19:22:20 +02:00
|
|
|
}
|
2022-07-27 12:40:46 +02:00
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-08-05 12:33:44 +02:00
|
|
|
if (filters.$and) {
|
2024-08-05 15:09:33 +02:00
|
|
|
const { $and } = filters
|
2024-08-21 12:58:46 +02:00
|
|
|
for (const condition of $and.conditions) {
|
2024-08-21 14:25:48 +02:00
|
|
|
query = query.where(b => {
|
|
|
|
this.addFilters(b, condition, opts)
|
|
|
|
})
|
2024-08-21 12:58:46 +02:00
|
|
|
}
|
2024-08-05 12:33:44 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
if (filters.$or) {
|
|
|
|
const { $or } = filters
|
2024-08-21 14:25:48 +02:00
|
|
|
query = query.where(b => {
|
|
|
|
for (const condition of $or.conditions) {
|
|
|
|
b.orWhere(c =>
|
|
|
|
this.addFilters(c, { ...condition, allOr: true }, opts)
|
|
|
|
)
|
|
|
|
}
|
|
|
|
})
|
2024-08-05 12:33:44 +02:00
|
|
|
}
|
|
|
|
|
2021-11-05 14:48:13 +01:00
|
|
|
if (filters.oneOf) {
|
2024-07-09 14:01:44 +02:00
|
|
|
const fnc = allOr ? "orWhereIn" : "whereIn"
|
|
|
|
iterate(
|
|
|
|
filters.oneOf,
|
2024-08-28 19:41:02 +02:00
|
|
|
(q, key: string, array) => {
|
2024-07-29 10:57:24 +02:00
|
|
|
if (this.client === SqlClient.ORACLE) {
|
2024-07-29 19:11:05 +02:00
|
|
|
key = this.convertClobs(key)
|
2024-07-29 15:54:58 +02:00
|
|
|
array = Array.isArray(array) ? array : [array]
|
|
|
|
const binding = new Array(array.length).fill("?").join(",")
|
2024-08-28 19:41:02 +02:00
|
|
|
return q.whereRaw(`${key} IN (${binding})`, array)
|
2024-07-29 10:57:24 +02:00
|
|
|
} else {
|
2024-08-28 19:41:02 +02:00
|
|
|
return q[fnc](key, Array.isArray(array) ? array : [array])
|
2024-07-29 10:57:24 +02:00
|
|
|
}
|
2024-07-09 14:01:44 +02:00
|
|
|
},
|
2024-08-28 19:41:02 +02:00
|
|
|
(q, key: string[], array) => {
|
2024-08-02 18:17:33 +02:00
|
|
|
if (this.client === SqlClient.ORACLE) {
|
|
|
|
const keyStr = `(${key.map(k => this.convertClobs(k)).join(",")})`
|
|
|
|
const binding = `(${array
|
|
|
|
.map((a: any) => `(${new Array(a.length).fill("?").join(",")})`)
|
|
|
|
.join(",")})`
|
2024-08-28 19:41:02 +02:00
|
|
|
return q.whereRaw(`${keyStr} IN ${binding}`, array.flat())
|
2024-08-02 18:17:33 +02:00
|
|
|
} else {
|
2024-08-28 19:41:02 +02:00
|
|
|
return q[fnc](key, Array.isArray(array) ? array : [array])
|
2024-08-02 18:17:33 +02:00
|
|
|
}
|
2024-07-09 14:01:44 +02:00
|
|
|
}
|
|
|
|
)
|
2021-11-05 14:48:13 +01:00
|
|
|
}
|
|
|
|
if (filters.string) {
|
2024-08-28 19:41:02 +02:00
|
|
|
iterate(filters.string, (q, key, value) => {
|
2021-11-05 14:48:13 +01:00
|
|
|
const fnc = allOr ? "orWhere" : "where"
|
|
|
|
// postgres supports ilike, nothing else does
|
2022-08-11 14:50:05 +02:00
|
|
|
if (this.client === SqlClient.POSTGRES) {
|
2024-08-28 19:41:02 +02:00
|
|
|
return q[fnc](key, "ilike", `${value}%`)
|
2021-11-05 14:48:13 +01:00
|
|
|
} else {
|
|
|
|
const rawFnc = `${fnc}Raw`
|
|
|
|
// @ts-ignore
|
2024-08-28 19:41:02 +02:00
|
|
|
return q[rawFnc](`LOWER(${this.quotedIdentifier(key)}) LIKE ?`, [
|
2024-07-29 17:54:59 +02:00
|
|
|
`${value.toLowerCase()}%`,
|
|
|
|
])
|
2021-11-05 14:48:13 +01:00
|
|
|
}
|
|
|
|
})
|
|
|
|
}
|
|
|
|
if (filters.fuzzy) {
|
2022-07-19 18:25:41 +02:00
|
|
|
iterate(filters.fuzzy, like)
|
2021-11-05 14:48:13 +01:00
|
|
|
}
|
|
|
|
if (filters.range) {
|
2024-08-28 19:41:02 +02:00
|
|
|
iterate(filters.range, (q, key, value) => {
|
2023-02-15 16:10:02 +01:00
|
|
|
const isEmptyObject = (val: any) => {
|
|
|
|
return (
|
|
|
|
val &&
|
|
|
|
Object.keys(val).length === 0 &&
|
|
|
|
Object.getPrototypeOf(val) === Object.prototype
|
|
|
|
)
|
|
|
|
}
|
|
|
|
if (isEmptyObject(value.low)) {
|
|
|
|
value.low = ""
|
|
|
|
}
|
|
|
|
if (isEmptyObject(value.high)) {
|
|
|
|
value.high = ""
|
|
|
|
}
|
2023-09-14 17:53:36 +02:00
|
|
|
const lowValid = isValidFilter(value.low),
|
|
|
|
highValid = isValidFilter(value.high)
|
2024-07-30 12:54:46 +02:00
|
|
|
|
|
|
|
const schema = this.getFieldSchema(key)
|
|
|
|
|
|
|
|
if (this.client === SqlClient.ORACLE) {
|
|
|
|
// @ts-ignore
|
|
|
|
key = this.knex.raw(this.convertClobs(key))
|
|
|
|
}
|
|
|
|
|
2023-09-14 17:53:36 +02:00
|
|
|
if (lowValid && highValid) {
|
2024-07-30 12:54:46 +02:00
|
|
|
if (
|
|
|
|
schema?.type === FieldType.BIGINT &&
|
|
|
|
this.client === SqlClient.SQL_LITE
|
|
|
|
) {
|
2024-08-28 19:41:02 +02:00
|
|
|
return q.whereRaw(
|
2024-07-30 12:54:46 +02:00
|
|
|
`CAST(${key} AS INTEGER) BETWEEN CAST(? AS INTEGER) AND CAST(? AS INTEGER)`,
|
|
|
|
[value.low, value.high]
|
|
|
|
)
|
|
|
|
} else {
|
|
|
|
const fnc = allOr ? "orWhereBetween" : "whereBetween"
|
2024-08-28 19:41:02 +02:00
|
|
|
return q[fnc](key, [value.low, value.high])
|
2024-07-30 12:54:46 +02:00
|
|
|
}
|
2023-09-14 17:53:36 +02:00
|
|
|
} else if (lowValid) {
|
2024-07-30 12:54:46 +02:00
|
|
|
if (
|
|
|
|
schema?.type === FieldType.BIGINT &&
|
|
|
|
this.client === SqlClient.SQL_LITE
|
|
|
|
) {
|
2024-08-28 19:41:02 +02:00
|
|
|
return q.whereRaw(`CAST(${key} AS INTEGER) >= CAST(? AS INTEGER)`, [
|
|
|
|
value.low,
|
|
|
|
])
|
2024-07-30 12:54:46 +02:00
|
|
|
} else {
|
|
|
|
const fnc = allOr ? "orWhere" : "where"
|
2024-08-28 19:41:02 +02:00
|
|
|
return q[fnc](key, ">=", value.low)
|
2024-07-30 12:54:46 +02:00
|
|
|
}
|
2023-09-14 17:53:36 +02:00
|
|
|
} else if (highValid) {
|
2024-07-30 12:54:46 +02:00
|
|
|
if (
|
|
|
|
schema?.type === FieldType.BIGINT &&
|
|
|
|
this.client === SqlClient.SQL_LITE
|
|
|
|
) {
|
2024-08-28 19:41:02 +02:00
|
|
|
return q.whereRaw(`CAST(${key} AS INTEGER) <= CAST(? AS INTEGER)`, [
|
|
|
|
value.high,
|
|
|
|
])
|
2024-07-30 12:54:46 +02:00
|
|
|
} else {
|
|
|
|
const fnc = allOr ? "orWhere" : "where"
|
2024-08-28 19:41:02 +02:00
|
|
|
return q[fnc](key, "<=", value.high)
|
2024-07-30 12:54:46 +02:00
|
|
|
}
|
2021-11-05 14:48:13 +01:00
|
|
|
}
|
2024-08-28 19:41:02 +02:00
|
|
|
return q
|
2021-11-05 14:48:13 +01:00
|
|
|
})
|
|
|
|
}
|
|
|
|
if (filters.equal) {
|
2024-08-28 19:41:02 +02:00
|
|
|
iterate(filters.equal, (q, key, value) => {
|
2024-05-20 18:01:52 +02:00
|
|
|
const fnc = allOr ? "orWhereRaw" : "whereRaw"
|
|
|
|
if (this.client === SqlClient.MS_SQL) {
|
2024-08-28 19:41:02 +02:00
|
|
|
return q[fnc](
|
2024-07-29 17:54:59 +02:00
|
|
|
`CASE WHEN ${this.quotedIdentifier(key)} = ? THEN 1 ELSE 0 END = 1`,
|
2024-05-20 18:01:52 +02:00
|
|
|
[value]
|
|
|
|
)
|
2024-07-23 10:44:58 +02:00
|
|
|
} else if (this.client === SqlClient.ORACLE) {
|
2024-07-29 19:11:05 +02:00
|
|
|
const identifier = this.convertClobs(key)
|
2024-08-28 19:41:02 +02:00
|
|
|
return q[fnc](`(${identifier} IS NOT NULL AND ${identifier} = ?)`, [
|
|
|
|
value,
|
|
|
|
])
|
2024-05-20 18:01:52 +02:00
|
|
|
} else {
|
2024-08-28 19:41:02 +02:00
|
|
|
return q[fnc](`COALESCE(${this.quotedIdentifier(key)} = ?, FALSE)`, [
|
|
|
|
value,
|
|
|
|
])
|
2024-05-20 18:01:52 +02:00
|
|
|
}
|
2021-11-05 14:48:13 +01:00
|
|
|
})
|
|
|
|
}
|
|
|
|
if (filters.notEqual) {
|
2024-08-28 19:41:02 +02:00
|
|
|
iterate(filters.notEqual, (q, key, value) => {
|
2024-05-20 18:01:52 +02:00
|
|
|
const fnc = allOr ? "orWhereRaw" : "whereRaw"
|
|
|
|
if (this.client === SqlClient.MS_SQL) {
|
2024-08-28 19:41:02 +02:00
|
|
|
return q[fnc](
|
2024-07-29 17:54:59 +02:00
|
|
|
`CASE WHEN ${this.quotedIdentifier(key)} = ? THEN 1 ELSE 0 END = 0`,
|
2024-05-20 18:01:52 +02:00
|
|
|
[value]
|
|
|
|
)
|
2024-07-23 10:44:58 +02:00
|
|
|
} else if (this.client === SqlClient.ORACLE) {
|
2024-07-29 19:11:05 +02:00
|
|
|
const identifier = this.convertClobs(key)
|
2024-08-28 19:41:02 +02:00
|
|
|
return q[fnc](
|
2024-07-30 12:26:16 +02:00
|
|
|
`(${identifier} IS NOT NULL AND ${identifier} != ?) OR ${identifier} IS NULL`,
|
2024-07-23 12:39:50 +02:00
|
|
|
[value]
|
|
|
|
)
|
2024-05-20 18:01:52 +02:00
|
|
|
} else {
|
2024-08-28 19:41:02 +02:00
|
|
|
return q[fnc](`COALESCE(${this.quotedIdentifier(key)} != ?, TRUE)`, [
|
|
|
|
value,
|
|
|
|
])
|
2024-05-20 18:01:52 +02:00
|
|
|
}
|
2021-11-05 14:48:13 +01:00
|
|
|
})
|
|
|
|
}
|
|
|
|
if (filters.empty) {
|
2024-08-28 19:41:02 +02:00
|
|
|
iterate(filters.empty, (q, key) => {
|
2021-11-05 14:48:13 +01:00
|
|
|
const fnc = allOr ? "orWhereNull" : "whereNull"
|
2024-08-28 19:41:02 +02:00
|
|
|
return q[fnc](key)
|
2021-11-05 14:48:13 +01:00
|
|
|
})
|
|
|
|
}
|
|
|
|
if (filters.notEmpty) {
|
2024-08-28 19:41:02 +02:00
|
|
|
iterate(filters.notEmpty, (q, key) => {
|
2021-11-05 14:48:13 +01:00
|
|
|
const fnc = allOr ? "orWhereNotNull" : "whereNotNull"
|
2024-08-28 19:41:02 +02:00
|
|
|
return q[fnc](key)
|
2021-11-05 14:48:13 +01:00
|
|
|
})
|
|
|
|
}
|
2022-07-19 18:25:41 +02:00
|
|
|
if (filters.contains) {
|
2022-07-27 12:40:46 +02:00
|
|
|
contains(filters.contains)
|
|
|
|
}
|
|
|
|
if (filters.notContains) {
|
|
|
|
contains(filters.notContains)
|
2022-07-19 18:25:41 +02:00
|
|
|
}
|
2022-07-27 14:19:47 +02:00
|
|
|
if (filters.containsAny) {
|
|
|
|
contains(filters.containsAny, true)
|
2024-04-11 10:53:54 +02:00
|
|
|
}
|
|
|
|
|
2024-07-29 19:11:05 +02:00
|
|
|
const tableRef = aliases?.[this.table._id!] || this.table._id
|
2024-05-28 19:34:08 +02:00
|
|
|
// when searching internal tables make sure long looking for rows
|
2024-07-29 19:11:05 +02:00
|
|
|
if (filters.documentType && !isExternalTable(this.table) && tableRef) {
|
2024-05-28 19:34:08 +02:00
|
|
|
// has to be its own option, must always be AND onto the search
|
2024-05-29 12:56:52 +02:00
|
|
|
query.andWhereLike(
|
|
|
|
`${tableRef}._id`,
|
|
|
|
`${prefixed(filters.documentType)}%`
|
|
|
|
)
|
2024-05-28 19:34:08 +02:00
|
|
|
}
|
|
|
|
|
2021-06-23 20:05:32 +02:00
|
|
|
return query
|
|
|
|
}
|
2021-11-05 14:48:13 +01:00
|
|
|
|
2024-10-01 16:04:01 +02:00
|
|
|
isSqs(t?: Table): boolean {
|
|
|
|
const table = t || this.table
|
|
|
|
return (
|
|
|
|
table.sourceType === TableSourceType.INTERNAL ||
|
|
|
|
table.sourceId === INTERNAL_TABLE_SOURCE_ID
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
2024-10-01 12:48:14 +02:00
|
|
|
getTableName(t?: Table | string): string {
|
|
|
|
let table: Table
|
|
|
|
if (typeof t === "string") {
|
|
|
|
if (!this.query.meta.tables?.[t]) {
|
|
|
|
throw new Error(`Table ${t} not found`)
|
|
|
|
}
|
|
|
|
table = this.query.meta.tables[t]
|
|
|
|
} else if (t) {
|
|
|
|
table = t
|
|
|
|
} else {
|
|
|
|
table = this.table
|
|
|
|
}
|
|
|
|
|
|
|
|
let name = table.name
|
2024-10-01 16:04:01 +02:00
|
|
|
if (this.isSqs(table) && table._id) {
|
2024-10-01 12:48:14 +02:00
|
|
|
// SQS uses the table ID rather than the table name
|
|
|
|
name = table._id
|
|
|
|
}
|
|
|
|
const aliases = this.query.tableAliases || {}
|
|
|
|
return aliases[name] ? aliases[name] : name
|
|
|
|
}
|
|
|
|
|
2024-07-29 19:11:05 +02:00
|
|
|
addDistinctCount(query: Knex.QueryBuilder): Knex.QueryBuilder {
|
2024-10-01 12:48:14 +02:00
|
|
|
if (!this.table.primary) {
|
2024-06-19 18:10:15 +02:00
|
|
|
throw new Error("SQL counting requires primary key to be supplied")
|
|
|
|
}
|
2024-10-01 12:48:14 +02:00
|
|
|
return query.countDistinct(
|
2024-10-01 16:04:01 +02:00
|
|
|
`${this.getTableName()}.${this.table.primary[0]} as total`
|
2024-10-01 12:48:14 +02:00
|
|
|
)
|
2024-06-19 18:10:15 +02:00
|
|
|
}
|
|
|
|
|
2024-09-04 10:29:05 +02:00
|
|
|
addAggregations(
|
|
|
|
query: Knex.QueryBuilder,
|
|
|
|
aggregations: Aggregation[]
|
|
|
|
): Knex.QueryBuilder {
|
|
|
|
const fields = this.query.resource?.fields || []
|
2024-10-01 12:48:14 +02:00
|
|
|
const tableName = this.getTableName()
|
2024-09-04 10:29:05 +02:00
|
|
|
if (fields.length > 0) {
|
2024-10-01 12:48:14 +02:00
|
|
|
query = query.groupBy(fields.map(field => `${tableName}.${field}`))
|
2024-10-01 16:04:01 +02:00
|
|
|
query = query.select(fields.map(field => `${tableName}.${field}`))
|
2024-09-04 10:29:05 +02:00
|
|
|
}
|
|
|
|
for (const aggregation of aggregations) {
|
|
|
|
const op = aggregation.calculationType
|
2024-10-01 16:04:01 +02:00
|
|
|
const field = `${tableName}.${aggregation.field} as ${aggregation.name}`
|
2024-09-04 10:29:05 +02:00
|
|
|
switch (op) {
|
|
|
|
case CalculationType.COUNT:
|
|
|
|
query = query.count(field)
|
|
|
|
break
|
|
|
|
case CalculationType.SUM:
|
|
|
|
query = query.sum(field)
|
|
|
|
break
|
|
|
|
case CalculationType.AVG:
|
|
|
|
query = query.avg(field)
|
|
|
|
break
|
|
|
|
case CalculationType.MIN:
|
|
|
|
query = query.min(field)
|
|
|
|
break
|
|
|
|
case CalculationType.MAX:
|
|
|
|
query = query.max(field)
|
|
|
|
break
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return query
|
|
|
|
}
|
|
|
|
|
2024-07-29 19:11:05 +02:00
|
|
|
addSorting(query: Knex.QueryBuilder): Knex.QueryBuilder {
|
2024-09-26 16:22:10 +02:00
|
|
|
let { sort, resource } = this.query
|
2024-07-29 19:11:05 +02:00
|
|
|
const primaryKey = this.table.primary
|
2024-10-01 12:48:14 +02:00
|
|
|
const aliased = this.getTableName()
|
2024-06-19 18:10:15 +02:00
|
|
|
if (!Array.isArray(primaryKey)) {
|
|
|
|
throw new Error("Sorting requires primary key to be specified for table")
|
|
|
|
}
|
2023-08-04 14:53:30 +02:00
|
|
|
if (sort && Object.keys(sort || {}).length > 0) {
|
2022-02-04 17:17:36 +01:00
|
|
|
for (let [key, value] of Object.entries(sort)) {
|
2023-03-09 09:50:26 +01:00
|
|
|
const direction =
|
2024-06-14 10:45:30 +02:00
|
|
|
value.direction === SortOrder.ASCENDING ? "asc" : "desc"
|
2024-07-30 12:58:28 +02:00
|
|
|
|
2024-08-05 17:15:15 +02:00
|
|
|
// TODO: figure out a way to remove this conditional, not relying on
|
|
|
|
// the defaults of each datastore.
|
2024-07-30 12:58:28 +02:00
|
|
|
let nulls: "first" | "last" | undefined = undefined
|
|
|
|
if (
|
2024-07-30 17:56:59 +02:00
|
|
|
this.client === SqlClient.POSTGRES ||
|
|
|
|
this.client === SqlClient.ORACLE
|
2024-07-30 12:58:28 +02:00
|
|
|
) {
|
|
|
|
nulls = value.direction === SortOrder.ASCENDING ? "first" : "last"
|
|
|
|
}
|
2024-05-21 16:08:22 +02:00
|
|
|
|
2024-07-30 12:54:46 +02:00
|
|
|
let composite = `${aliased}.${key}`
|
|
|
|
if (this.client === SqlClient.ORACLE) {
|
2024-07-30 17:56:59 +02:00
|
|
|
query = query.orderByRaw(
|
|
|
|
`${this.convertClobs(composite)} ${direction} nulls ${nulls}`
|
2024-07-30 12:54:46 +02:00
|
|
|
)
|
|
|
|
} else {
|
|
|
|
query = query.orderBy(composite, direction, nulls)
|
|
|
|
}
|
2022-02-04 17:17:36 +01:00
|
|
|
}
|
2021-11-24 19:20:52 +01:00
|
|
|
}
|
2024-06-24 18:09:27 +02:00
|
|
|
|
2024-06-24 18:30:10 +02:00
|
|
|
// add sorting by the primary key if the result isn't already sorted by it,
|
|
|
|
// to make sure result is deterministic
|
2024-09-26 16:22:10 +02:00
|
|
|
const hasAggregations = (resource?.aggregations?.length ?? 0) > 0
|
|
|
|
if (!hasAggregations && (!sort || sort[primaryKey[0]] === undefined)) {
|
2024-06-24 18:09:27 +02:00
|
|
|
query = query.orderBy(`${aliased}.${primaryKey[0]}`)
|
|
|
|
}
|
2021-11-24 19:20:52 +01:00
|
|
|
return query
|
|
|
|
}
|
|
|
|
|
2023-11-28 19:43:38 +01:00
|
|
|
tableNameWithSchema(
|
|
|
|
tableName: string,
|
|
|
|
opts?: { alias?: string; schema?: string }
|
|
|
|
) {
|
|
|
|
let withSchema = opts?.schema ? `${opts.schema}.${tableName}` : tableName
|
|
|
|
if (opts?.alias) {
|
|
|
|
withSchema += ` as ${opts.alias}`
|
|
|
|
}
|
|
|
|
return withSchema
|
|
|
|
}
|
|
|
|
|
2024-09-06 17:47:43 +02:00
|
|
|
private buildJsonField(field: string): string {
|
|
|
|
const parts = field.split(".")
|
|
|
|
let tableField: string, unaliased: string
|
|
|
|
if (parts.length > 1) {
|
|
|
|
const alias = parts.shift()!
|
|
|
|
unaliased = parts.join(".")
|
|
|
|
tableField = `${this.quote(alias)}.${this.quote(unaliased)}`
|
|
|
|
} else {
|
|
|
|
unaliased = parts.join(".")
|
|
|
|
tableField = this.quote(unaliased)
|
|
|
|
}
|
|
|
|
const separator = this.client === SqlClient.ORACLE ? " VALUE " : ","
|
|
|
|
return `'${unaliased}'${separator}${tableField}`
|
|
|
|
}
|
|
|
|
|
2024-09-16 17:12:07 +02:00
|
|
|
maxFunctionParameters() {
|
|
|
|
// functions like say json_build_object() in SQL have a limit as to how many can be performed
|
|
|
|
// before a limit is met, this limit exists in Postgres/SQLite. This can be very important, such as
|
|
|
|
// for JSON column building as part of relationships. We also have a default limit to avoid very complex
|
|
|
|
// functions being built - it is likely this is not necessary or the best way to do it.
|
|
|
|
switch (this.client) {
|
|
|
|
case SqlClient.SQL_LITE:
|
|
|
|
return 127
|
|
|
|
case SqlClient.POSTGRES:
|
|
|
|
return 100
|
|
|
|
// other DBs don't have a limit, but set some sort of limit
|
|
|
|
default:
|
|
|
|
return 200
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-08-23 19:00:52 +02:00
|
|
|
addJsonRelationships(
|
|
|
|
query: Knex.QueryBuilder,
|
|
|
|
fromTable: string,
|
|
|
|
relationships: RelationshipsJson[]
|
|
|
|
): Knex.QueryBuilder {
|
2024-09-03 19:24:50 +02:00
|
|
|
const sqlClient = this.client
|
2024-09-04 18:42:30 +02:00
|
|
|
const knex = this.knex
|
2024-09-16 19:09:01 +02:00
|
|
|
const { resource, tableAliases: aliases, endpoint, meta } = this.query
|
2024-08-23 19:00:52 +02:00
|
|
|
const fields = resource?.fields || []
|
|
|
|
for (let relationship of relationships) {
|
|
|
|
const {
|
|
|
|
tableName: toTable,
|
|
|
|
through: throughTable,
|
|
|
|
to: toKey,
|
|
|
|
from: fromKey,
|
|
|
|
fromPrimary,
|
|
|
|
toPrimary,
|
|
|
|
} = relationship
|
|
|
|
// skip invalid relationships
|
2024-09-03 19:24:50 +02:00
|
|
|
if (!toTable || !fromTable) {
|
2024-08-23 19:00:52 +02:00
|
|
|
continue
|
|
|
|
}
|
2024-09-16 19:09:01 +02:00
|
|
|
const relatedTable = meta.tables?.[toTable]
|
2024-08-23 19:00:52 +02:00
|
|
|
const toAlias = aliases?.[toTable] || toTable,
|
2024-09-20 18:58:31 +02:00
|
|
|
fromAlias = aliases?.[fromTable] || fromTable,
|
|
|
|
throughAlias = (throughTable && aliases?.[throughTable]) || throughTable
|
2024-08-23 19:00:52 +02:00
|
|
|
let toTableWithSchema = this.tableNameWithSchema(toTable, {
|
|
|
|
alias: toAlias,
|
|
|
|
schema: endpoint.schema,
|
|
|
|
})
|
2024-09-16 19:09:01 +02:00
|
|
|
const requiredFields = [
|
|
|
|
...(relatedTable?.primary || []),
|
|
|
|
relatedTable?.primaryDisplay,
|
2024-09-16 19:27:53 +02:00
|
|
|
].filter(field => field) as string[]
|
|
|
|
// sort the required fields to first in the list, so they don't get sliced out
|
|
|
|
let relationshipFields = prioritisedArraySort(
|
|
|
|
fields.filter(field => field.split(".")[0] === toAlias),
|
|
|
|
requiredFields
|
|
|
|
)
|
2024-09-16 19:09:01 +02:00
|
|
|
|
2024-09-16 17:12:07 +02:00
|
|
|
relationshipFields = relationshipFields.slice(
|
|
|
|
0,
|
|
|
|
Math.floor(this.maxFunctionParameters() / 2)
|
2024-08-23 19:00:52 +02:00
|
|
|
)
|
|
|
|
const fieldList: string = relationshipFields
|
2024-09-06 17:47:43 +02:00
|
|
|
.map(field => this.buildJsonField(field))
|
2024-08-23 19:00:52 +02:00
|
|
|
.join(",")
|
2024-09-03 19:24:50 +02:00
|
|
|
// SQL Server uses TOP - which performs a little differently to the normal LIMIT syntax
|
|
|
|
// it reduces the result set rather than limiting how much data it filters over
|
|
|
|
const primaryKey = `${toAlias}.${toPrimary || toKey}`
|
2024-09-04 19:14:24 +02:00
|
|
|
let subQuery: Knex.QueryBuilder = knex
|
2024-08-23 19:00:52 +02:00
|
|
|
.from(toTableWithSchema)
|
2024-09-03 19:24:50 +02:00
|
|
|
// add sorting to get consistent order
|
|
|
|
.orderBy(primaryKey)
|
|
|
|
|
2024-09-20 18:58:31 +02:00
|
|
|
const isManyToMany = throughTable && toPrimary && fromPrimary
|
|
|
|
let correlatedTo = isManyToMany
|
|
|
|
? `${throughAlias}.${fromKey}`
|
|
|
|
: `${toAlias}.${toKey}`,
|
|
|
|
correlatedFrom = isManyToMany
|
|
|
|
? `${fromAlias}.${fromPrimary}`
|
|
|
|
: `${fromAlias}.${fromKey}`
|
|
|
|
// many-to-many relationship needs junction table join
|
|
|
|
if (isManyToMany) {
|
2024-09-03 19:24:50 +02:00
|
|
|
let throughTableWithSchema = this.tableNameWithSchema(throughTable, {
|
|
|
|
alias: throughAlias,
|
|
|
|
schema: endpoint.schema,
|
2024-08-23 19:00:52 +02:00
|
|
|
})
|
2024-09-20 18:58:31 +02:00
|
|
|
subQuery = subQuery.join(throughTableWithSchema, function () {
|
|
|
|
this.on(`${toAlias}.${toPrimary}`, "=", `${throughAlias}.${toKey}`)
|
|
|
|
})
|
2024-09-03 19:24:50 +02:00
|
|
|
}
|
|
|
|
|
2024-09-23 18:16:34 +02:00
|
|
|
// add the correlation to the overall query
|
|
|
|
subQuery = subQuery.where(
|
|
|
|
correlatedTo,
|
|
|
|
"=",
|
|
|
|
knex.raw(this.quotedIdentifier(correlatedFrom))
|
|
|
|
)
|
2024-09-03 19:24:50 +02:00
|
|
|
|
2024-09-04 18:42:30 +02:00
|
|
|
const standardWrap = (select: string): Knex.QueryBuilder => {
|
2024-09-23 18:16:34 +02:00
|
|
|
subQuery = subQuery.select(`${toAlias}.*`).limit(getRelationshipLimit())
|
2024-09-04 17:41:36 +02:00
|
|
|
// @ts-ignore - the from alias syntax isn't in Knex typing
|
2024-09-04 18:42:30 +02:00
|
|
|
return knex.select(knex.raw(select)).from({
|
2024-09-04 17:41:36 +02:00
|
|
|
[toAlias]: subQuery,
|
|
|
|
})
|
2024-09-04 17:21:32 +02:00
|
|
|
}
|
2024-09-04 18:42:30 +02:00
|
|
|
let wrapperQuery: Knex.QueryBuilder | Knex.Raw
|
|
|
|
switch (sqlClient) {
|
2024-08-23 19:00:52 +02:00
|
|
|
case SqlClient.SQL_LITE:
|
2024-09-04 18:42:30 +02:00
|
|
|
// need to check the junction table document is to the right column, this is just for SQS
|
|
|
|
subQuery = this.addJoinFieldCheck(subQuery, relationship)
|
|
|
|
wrapperQuery = standardWrap(
|
2024-08-23 19:00:52 +02:00
|
|
|
`json_group_array(json_object(${fieldList}))`
|
|
|
|
)
|
|
|
|
break
|
2024-09-04 18:42:30 +02:00
|
|
|
case SqlClient.POSTGRES:
|
|
|
|
wrapperQuery = standardWrap(
|
|
|
|
`json_agg(json_build_object(${fieldList}))`
|
|
|
|
)
|
|
|
|
break
|
2024-09-24 13:01:47 +02:00
|
|
|
case SqlClient.MARIADB:
|
2024-09-23 18:16:34 +02:00
|
|
|
// can't use the standard wrap due to correlated sub-query limitations in MariaDB
|
2024-09-04 19:14:24 +02:00
|
|
|
wrapperQuery = subQuery.select(
|
2024-09-23 18:16:34 +02:00
|
|
|
knex.raw(
|
|
|
|
`json_arrayagg(json_object(${fieldList}) LIMIT ${getRelationshipLimit()})`
|
|
|
|
)
|
2024-09-04 19:14:24 +02:00
|
|
|
)
|
|
|
|
break
|
2024-09-24 13:01:47 +02:00
|
|
|
case SqlClient.MY_SQL:
|
2024-09-04 18:42:30 +02:00
|
|
|
case SqlClient.ORACLE:
|
|
|
|
wrapperQuery = standardWrap(
|
|
|
|
`json_arrayagg(json_object(${fieldList}))`
|
|
|
|
)
|
|
|
|
break
|
|
|
|
case SqlClient.MS_SQL:
|
|
|
|
wrapperQuery = knex.raw(
|
|
|
|
`(SELECT ${this.quote(toAlias)} = (${knex
|
|
|
|
.select(`${fromAlias}.*`)
|
|
|
|
// @ts-ignore - from alias syntax not TS supported
|
|
|
|
.from({
|
2024-09-24 14:44:26 +02:00
|
|
|
[fromAlias]: subQuery
|
|
|
|
.select(`${toAlias}.*`)
|
|
|
|
.limit(getRelationshipLimit()),
|
2024-09-04 18:42:30 +02:00
|
|
|
})} FOR JSON PATH))`
|
|
|
|
)
|
|
|
|
break
|
2024-08-23 19:00:52 +02:00
|
|
|
default:
|
2024-09-04 18:42:30 +02:00
|
|
|
throw new Error(`JSON relationships not implement for ${sqlClient}`)
|
2024-08-29 19:56:14 +02:00
|
|
|
}
|
2024-09-04 17:21:32 +02:00
|
|
|
|
2024-09-04 17:17:25 +02:00
|
|
|
query = query.select({ [relationship.column]: wrapperQuery })
|
2024-08-23 19:00:52 +02:00
|
|
|
}
|
|
|
|
return query
|
|
|
|
}
|
|
|
|
|
2024-08-27 19:34:05 +02:00
|
|
|
addJoin(
|
|
|
|
query: Knex.QueryBuilder,
|
|
|
|
tables: { from: string; to: string; through?: string },
|
|
|
|
columns: {
|
|
|
|
from?: string
|
|
|
|
to?: string
|
|
|
|
fromPrimary?: string
|
|
|
|
toPrimary?: string
|
|
|
|
}[]
|
|
|
|
): Knex.QueryBuilder {
|
|
|
|
const { tableAliases: aliases, endpoint } = this.query
|
|
|
|
const schema = endpoint.schema
|
|
|
|
const toTable = tables.to,
|
|
|
|
fromTable = tables.from,
|
|
|
|
throughTable = tables.through
|
|
|
|
const toAlias = aliases?.[toTable] || toTable,
|
|
|
|
throughAlias = (throughTable && aliases?.[throughTable]) || throughTable,
|
|
|
|
fromAlias = aliases?.[fromTable] || fromTable
|
|
|
|
let toTableWithSchema = this.tableNameWithSchema(toTable, {
|
|
|
|
alias: toAlias,
|
|
|
|
schema,
|
|
|
|
})
|
|
|
|
let throughTableWithSchema = throughTable
|
|
|
|
? this.tableNameWithSchema(throughTable, {
|
|
|
|
alias: throughAlias,
|
|
|
|
schema,
|
|
|
|
})
|
|
|
|
: undefined
|
|
|
|
if (!throughTable) {
|
|
|
|
// @ts-ignore
|
|
|
|
query = query.leftJoin(toTableWithSchema, function () {
|
|
|
|
for (let relationship of columns) {
|
|
|
|
const from = relationship.from,
|
|
|
|
to = relationship.to
|
|
|
|
// @ts-ignore
|
|
|
|
this.orOn(`${fromAlias}.${from}`, "=", `${toAlias}.${to}`)
|
|
|
|
}
|
|
|
|
})
|
|
|
|
} else {
|
|
|
|
query = query
|
|
|
|
// @ts-ignore
|
|
|
|
.leftJoin(throughTableWithSchema, function () {
|
|
|
|
for (let relationship of columns) {
|
|
|
|
const fromPrimary = relationship.fromPrimary
|
|
|
|
const from = relationship.from
|
|
|
|
// @ts-ignore
|
|
|
|
this.orOn(
|
|
|
|
`${fromAlias}.${fromPrimary}`,
|
|
|
|
"=",
|
|
|
|
`${throughAlias}.${from}`
|
|
|
|
)
|
|
|
|
}
|
|
|
|
})
|
|
|
|
.leftJoin(toTableWithSchema, function () {
|
|
|
|
for (let relationship of columns) {
|
|
|
|
const toPrimary = relationship.toPrimary
|
|
|
|
const to = relationship.to
|
|
|
|
// @ts-ignore
|
|
|
|
this.orOn(`${toAlias}.${toPrimary}`, `${throughAlias}.${to}`)
|
|
|
|
}
|
|
|
|
})
|
|
|
|
}
|
|
|
|
return query
|
|
|
|
}
|
|
|
|
|
2024-07-30 12:54:46 +02:00
|
|
|
qualifiedKnex(opts?: { alias?: string | boolean }): Knex.QueryBuilder {
|
2024-07-29 19:11:05 +02:00
|
|
|
let alias = this.query.tableAliases?.[this.query.endpoint.entityId]
|
|
|
|
if (opts?.alias === false) {
|
|
|
|
alias = undefined
|
|
|
|
} else if (typeof opts?.alias === "string") {
|
|
|
|
alias = opts.alias
|
|
|
|
}
|
2024-07-30 12:54:46 +02:00
|
|
|
return this.knex(
|
2024-07-29 19:11:05 +02:00
|
|
|
this.tableNameWithSchema(this.query.endpoint.entityId, {
|
|
|
|
alias,
|
|
|
|
schema: this.query.endpoint.schema,
|
2024-04-18 16:40:43 +02:00
|
|
|
})
|
|
|
|
)
|
2023-12-01 16:27:49 +01:00
|
|
|
}
|
|
|
|
|
2024-07-30 12:54:46 +02:00
|
|
|
create(opts: QueryOptions): Knex.QueryBuilder {
|
2024-07-29 19:11:05 +02:00
|
|
|
const { body } = this.query
|
2024-07-30 12:54:46 +02:00
|
|
|
let query = this.qualifiedKnex({ alias: false })
|
2024-07-29 19:11:05 +02:00
|
|
|
const parsedBody = this.parseBody(body)
|
2024-08-02 17:58:12 +02:00
|
|
|
|
|
|
|
if (this.client === SqlClient.ORACLE) {
|
|
|
|
// Oracle doesn't seem to automatically insert nulls
|
|
|
|
// if we don't specify them, so we need to do that here
|
|
|
|
for (const [column, schema] of Object.entries(
|
|
|
|
this.query.meta.table.schema
|
|
|
|
)) {
|
|
|
|
if (
|
|
|
|
schema.constraints?.presence === true ||
|
|
|
|
schema.type === FieldType.FORMULA ||
|
|
|
|
schema.type === FieldType.AUTO ||
|
|
|
|
schema.type === FieldType.LINK
|
|
|
|
) {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
|
|
|
const value = parsedBody[column]
|
|
|
|
if (value == null) {
|
|
|
|
parsedBody[column] = null
|
|
|
|
}
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
// make sure no null values in body for creation
|
|
|
|
for (let [key, value] of Object.entries(parsedBody)) {
|
|
|
|
if (value == null) {
|
|
|
|
delete parsedBody[key]
|
|
|
|
}
|
2021-11-05 14:48:13 +01:00
|
|
|
}
|
|
|
|
}
|
2023-02-06 21:47:49 +01:00
|
|
|
|
2021-11-05 14:48:13 +01:00
|
|
|
// mysql can't use returning
|
|
|
|
if (opts.disableReturning) {
|
|
|
|
return query.insert(parsedBody)
|
|
|
|
} else {
|
2023-02-23 10:28:24 +01:00
|
|
|
return query.insert(parsedBody).returning("*")
|
2021-10-06 18:55:03 +02:00
|
|
|
}
|
|
|
|
}
|
2021-06-03 17:31:24 +02:00
|
|
|
|
2024-07-30 12:54:46 +02:00
|
|
|
bulkCreate(): Knex.QueryBuilder {
|
2024-07-29 19:11:05 +02:00
|
|
|
const { body } = this.query
|
2024-07-30 12:54:46 +02:00
|
|
|
let query = this.qualifiedKnex({ alias: false })
|
2021-11-12 20:24:56 +01:00
|
|
|
if (!Array.isArray(body)) {
|
|
|
|
return query
|
|
|
|
}
|
2024-07-29 19:11:05 +02:00
|
|
|
const parsedBody = body.map(row => this.parseBody(row))
|
2021-07-12 11:51:30 +02:00
|
|
|
return query.insert(parsedBody)
|
2021-06-18 14:14:45 +02:00
|
|
|
}
|
2021-06-03 17:31:24 +02:00
|
|
|
|
2024-07-30 12:54:46 +02:00
|
|
|
bulkUpsert(): Knex.QueryBuilder {
|
2024-07-29 19:11:05 +02:00
|
|
|
const { body } = this.query
|
2024-07-30 12:54:46 +02:00
|
|
|
let query = this.qualifiedKnex({ alias: false })
|
2024-06-18 18:43:25 +02:00
|
|
|
if (!Array.isArray(body)) {
|
|
|
|
return query
|
|
|
|
}
|
2024-07-29 19:11:05 +02:00
|
|
|
const parsedBody = body.map(row => this.parseBody(row))
|
2024-06-18 18:43:25 +02:00
|
|
|
if (
|
|
|
|
this.client === SqlClient.POSTGRES ||
|
|
|
|
this.client === SqlClient.SQL_LITE ||
|
2024-09-24 13:01:47 +02:00
|
|
|
this.client === SqlClient.MY_SQL ||
|
|
|
|
this.client === SqlClient.MARIADB
|
2024-06-18 18:43:25 +02:00
|
|
|
) {
|
2024-07-29 19:11:05 +02:00
|
|
|
const primary = this.table.primary
|
2024-06-18 18:43:25 +02:00
|
|
|
if (!primary) {
|
|
|
|
throw new Error("Primary key is required for upsert")
|
|
|
|
}
|
2024-09-06 17:47:43 +02:00
|
|
|
return query.insert(parsedBody).onConflict(primary).merge()
|
2024-07-29 10:57:24 +02:00
|
|
|
} else if (
|
|
|
|
this.client === SqlClient.MS_SQL ||
|
|
|
|
this.client === SqlClient.ORACLE
|
|
|
|
) {
|
|
|
|
// No upsert or onConflict support in MSSQL/Oracle yet, see:
|
2024-06-18 19:02:20 +02:00
|
|
|
// https://github.com/knex/knex/pull/6050
|
|
|
|
return query.insert(parsedBody)
|
2024-06-18 18:43:25 +02:00
|
|
|
}
|
|
|
|
return query.upsert(parsedBody)
|
|
|
|
}
|
|
|
|
|
2024-06-19 15:56:06 +02:00
|
|
|
read(
|
2024-06-19 16:08:12 +02:00
|
|
|
opts: {
|
|
|
|
limits?: { base: number; query: number }
|
|
|
|
} = {}
|
2024-06-19 15:56:06 +02:00
|
|
|
): Knex.QueryBuilder {
|
2024-08-27 19:34:05 +02:00
|
|
|
let { endpoint, filters, paginate, relationships } = this.query
|
2024-06-19 18:10:15 +02:00
|
|
|
const { limits } = opts
|
|
|
|
const counting = endpoint.operation === Operation.COUNT
|
2023-11-24 19:11:53 +01:00
|
|
|
|
2021-11-05 14:48:13 +01:00
|
|
|
const tableName = endpoint.entityId
|
|
|
|
// start building the query
|
2024-07-30 12:54:46 +02:00
|
|
|
let query = this.qualifiedKnex()
|
2024-06-18 13:47:59 +02:00
|
|
|
// handle pagination
|
|
|
|
let foundOffset: number | null = null
|
2024-06-19 15:56:06 +02:00
|
|
|
let foundLimit = limits?.query || limits?.base
|
2024-06-18 13:47:59 +02:00
|
|
|
if (paginate && paginate.page && paginate.limit) {
|
|
|
|
// @ts-ignore
|
|
|
|
const page = paginate.page <= 1 ? 0 : paginate.page - 1
|
|
|
|
const offset = page * paginate.limit
|
|
|
|
foundLimit = paginate.limit
|
|
|
|
foundOffset = offset
|
|
|
|
} else if (paginate && paginate.offset && paginate.limit) {
|
|
|
|
foundLimit = paginate.limit
|
|
|
|
foundOffset = paginate.offset
|
|
|
|
} else if (paginate && paginate.limit) {
|
|
|
|
foundLimit = paginate.limit
|
|
|
|
}
|
2024-06-19 19:46:48 +02:00
|
|
|
// counting should not sort, limit or offset
|
|
|
|
if (!counting) {
|
|
|
|
// add the found limit if supplied
|
|
|
|
if (foundLimit != null) {
|
|
|
|
query = query.limit(foundLimit)
|
|
|
|
}
|
|
|
|
// add overall pagination
|
|
|
|
if (foundOffset != null) {
|
|
|
|
query = query.offset(foundOffset)
|
|
|
|
}
|
2021-11-05 14:48:13 +01:00
|
|
|
}
|
2024-06-19 15:56:06 +02:00
|
|
|
|
2024-09-04 10:29:05 +02:00
|
|
|
const aggregations = this.query.resource?.aggregations || []
|
|
|
|
if (counting) {
|
|
|
|
query = this.addDistinctCount(query)
|
|
|
|
} else if (aggregations.length > 0) {
|
|
|
|
query = this.addAggregations(query, aggregations)
|
|
|
|
} else {
|
|
|
|
query = query.select(this.generateSelectStatement())
|
|
|
|
}
|
|
|
|
|
2021-11-24 19:20:52 +01:00
|
|
|
// have to add after as well (this breaks MS-SQL)
|
2024-09-03 19:24:50 +02:00
|
|
|
if (!counting) {
|
2024-08-23 19:00:52 +02:00
|
|
|
query = this.addSorting(query)
|
2021-11-24 19:20:52 +01:00
|
|
|
}
|
2024-06-14 15:35:35 +02:00
|
|
|
|
2024-09-06 20:34:02 +02:00
|
|
|
query = this.addFilters(query, filters, { relationship: true })
|
|
|
|
|
|
|
|
// handle relationships with a CTE for all others
|
2024-09-11 14:41:54 +02:00
|
|
|
if (relationships?.length) {
|
2024-09-06 20:34:02 +02:00
|
|
|
const mainTable =
|
|
|
|
this.query.tableAliases?.[this.query.endpoint.entityId] ||
|
|
|
|
this.query.endpoint.entityId
|
|
|
|
const cte = this.addSorting(
|
|
|
|
this.knex
|
|
|
|
.with("paginated", query)
|
|
|
|
.select(this.generateSelectStatement())
|
|
|
|
.from({
|
|
|
|
[mainTable]: "paginated",
|
|
|
|
})
|
|
|
|
)
|
|
|
|
// add JSON aggregations attached to the CTE
|
|
|
|
return this.addJsonRelationships(cte, tableName, relationships)
|
|
|
|
}
|
|
|
|
// no relationships found - return query
|
|
|
|
else {
|
|
|
|
return query
|
|
|
|
}
|
2021-11-05 14:48:13 +01:00
|
|
|
}
|
2021-06-03 17:31:24 +02:00
|
|
|
|
2024-07-30 12:54:46 +02:00
|
|
|
update(opts: QueryOptions): Knex.QueryBuilder {
|
2024-07-29 19:11:05 +02:00
|
|
|
const { body, filters } = this.query
|
2024-07-30 12:54:46 +02:00
|
|
|
let query = this.qualifiedKnex()
|
2024-07-29 19:11:05 +02:00
|
|
|
const parsedBody = this.parseBody(body)
|
|
|
|
query = this.addFilters(query, filters)
|
2021-11-05 14:48:13 +01:00
|
|
|
// mysql can't use returning
|
|
|
|
if (opts.disableReturning) {
|
|
|
|
return query.update(parsedBody)
|
|
|
|
} else {
|
2023-02-22 11:54:55 +01:00
|
|
|
return query.update(parsedBody).returning("*")
|
2021-11-05 14:48:13 +01:00
|
|
|
}
|
2021-06-18 14:14:45 +02:00
|
|
|
}
|
2021-06-03 17:31:24 +02:00
|
|
|
|
2024-07-30 12:54:46 +02:00
|
|
|
delete(opts: QueryOptions): Knex.QueryBuilder {
|
2024-07-29 19:11:05 +02:00
|
|
|
const { filters } = this.query
|
2024-07-30 12:54:46 +02:00
|
|
|
let query = this.qualifiedKnex()
|
2024-07-29 19:11:05 +02:00
|
|
|
query = this.addFilters(query, filters)
|
2021-11-05 14:48:13 +01:00
|
|
|
// mysql can't use returning
|
|
|
|
if (opts.disableReturning) {
|
|
|
|
return query.delete()
|
|
|
|
} else {
|
2024-07-30 12:54:46 +02:00
|
|
|
return query.delete().returning(this.generateSelectStatement())
|
2021-11-05 14:48:13 +01:00
|
|
|
}
|
2021-06-18 14:14:45 +02:00
|
|
|
}
|
2021-06-03 17:31:24 +02:00
|
|
|
}
|
|
|
|
|
2021-10-28 20:39:42 +02:00
|
|
|
class SqlQueryBuilder extends SqlTableQueryBuilder {
|
2021-06-24 19:16:48 +02:00
|
|
|
private readonly limit: number
|
2024-07-09 10:43:45 +02:00
|
|
|
|
2021-06-03 17:31:24 +02:00
|
|
|
// pass through client to get flavour of SQL
|
2024-08-01 14:03:58 +02:00
|
|
|
constructor(client: SqlClient, limit: number = getBaseLimit()) {
|
2021-10-28 20:39:42 +02:00
|
|
|
super(client)
|
2021-06-24 19:16:48 +02:00
|
|
|
this.limit = limit
|
2021-06-03 17:31:24 +02:00
|
|
|
}
|
|
|
|
|
2024-06-14 15:35:35 +02:00
|
|
|
private convertToNative(query: Knex.QueryBuilder, opts: QueryOptions = {}) {
|
|
|
|
const sqlClient = this.getSqlClient()
|
|
|
|
if (opts?.disableBindings) {
|
|
|
|
return { sql: query.toString() }
|
|
|
|
} else {
|
|
|
|
let native = getNativeSql(query)
|
|
|
|
if (sqlClient === SqlClient.SQL_LITE) {
|
|
|
|
native = convertBooleans(native)
|
|
|
|
}
|
|
|
|
return native
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-06-18 14:14:45 +02:00
|
|
|
/**
|
|
|
|
* @param json The JSON query DSL which is to be converted to SQL.
|
|
|
|
* @param opts extra options which are to be passed into the query builder, e.g. disableReturning
|
|
|
|
* which for the sake of mySQL stops adding the returning statement to inserts, updates and deletes.
|
2023-10-17 17:46:32 +02:00
|
|
|
* @return the query ready to be passed to the driver.
|
2021-06-18 14:14:45 +02:00
|
|
|
*/
|
2024-04-04 19:16:23 +02:00
|
|
|
_query(json: QueryJson, opts: QueryOptions = {}): SqlQuery | SqlQuery[] {
|
2021-10-28 20:39:42 +02:00
|
|
|
const sqlClient = this.getSqlClient()
|
2024-05-20 16:34:22 +02:00
|
|
|
const config: Knex.Config = {
|
2024-09-24 13:01:47 +02:00
|
|
|
client: this.getBaseSqlClient(),
|
2023-07-21 19:41:48 +02:00
|
|
|
}
|
2024-07-29 10:57:24 +02:00
|
|
|
if (sqlClient === SqlClient.SQL_LITE || sqlClient === SqlClient.ORACLE) {
|
2023-07-07 14:47:42 +02:00
|
|
|
config.useNullAsDefault = true
|
|
|
|
}
|
|
|
|
const client = knex(config)
|
2024-02-28 16:18:07 +01:00
|
|
|
let query: Knex.QueryBuilder
|
2024-07-30 12:54:46 +02:00
|
|
|
const builder = new InternalBuilder(sqlClient, client, json)
|
2021-06-03 19:48:04 +02:00
|
|
|
switch (this._operation(json)) {
|
2021-06-24 19:16:48 +02:00
|
|
|
case Operation.CREATE:
|
2024-07-30 12:54:46 +02:00
|
|
|
query = builder.create(opts)
|
2021-06-03 18:45:19 +02:00
|
|
|
break
|
2021-06-24 19:16:48 +02:00
|
|
|
case Operation.READ:
|
2024-07-30 12:54:46 +02:00
|
|
|
query = builder.read({
|
2024-06-19 16:08:12 +02:00
|
|
|
limits: {
|
|
|
|
query: this.limit,
|
2024-08-01 14:03:58 +02:00
|
|
|
base: getBaseLimit(),
|
2024-06-19 16:08:12 +02:00
|
|
|
},
|
2024-06-19 15:56:06 +02:00
|
|
|
})
|
|
|
|
break
|
2024-06-14 19:12:36 +02:00
|
|
|
case Operation.COUNT:
|
2024-06-19 18:10:15 +02:00
|
|
|
// read without any limits to count
|
2024-07-30 12:54:46 +02:00
|
|
|
query = builder.read()
|
2021-06-03 18:45:19 +02:00
|
|
|
break
|
2021-06-24 19:16:48 +02:00
|
|
|
case Operation.UPDATE:
|
2024-07-30 12:54:46 +02:00
|
|
|
query = builder.update(opts)
|
2021-06-03 18:45:19 +02:00
|
|
|
break
|
2021-06-24 19:16:48 +02:00
|
|
|
case Operation.DELETE:
|
2024-07-30 12:54:46 +02:00
|
|
|
query = builder.delete(opts)
|
2021-06-03 18:45:19 +02:00
|
|
|
break
|
2021-11-12 20:24:56 +01:00
|
|
|
case Operation.BULK_CREATE:
|
2024-07-30 12:54:46 +02:00
|
|
|
query = builder.bulkCreate()
|
2021-06-03 18:45:19 +02:00
|
|
|
break
|
2024-06-18 18:43:25 +02:00
|
|
|
case Operation.BULK_UPSERT:
|
2024-07-30 12:54:46 +02:00
|
|
|
query = builder.bulkUpsert()
|
2024-06-18 18:43:25 +02:00
|
|
|
break
|
2021-11-05 13:33:48 +01:00
|
|
|
case Operation.CREATE_TABLE:
|
|
|
|
case Operation.UPDATE_TABLE:
|
|
|
|
case Operation.DELETE_TABLE:
|
2021-10-28 20:39:42 +02:00
|
|
|
return this._tableQuery(json)
|
2021-06-03 17:31:24 +02:00
|
|
|
default:
|
2021-06-04 15:53:49 +02:00
|
|
|
throw `Operation type is not supported by SQL query builder`
|
2021-06-03 17:31:24 +02:00
|
|
|
}
|
2021-06-25 19:13:11 +02:00
|
|
|
|
2024-06-14 15:35:35 +02:00
|
|
|
return this.convertToNative(query, opts)
|
|
|
|
}
|
|
|
|
|
2024-03-04 16:47:27 +01:00
|
|
|
async getReturningRow(queryFn: QueryFunction, json: QueryJson) {
|
2021-11-05 19:55:36 +01:00
|
|
|
if (!json.extra || !json.extra.idFilter) {
|
|
|
|
return {}
|
|
|
|
}
|
|
|
|
const input = this._query({
|
|
|
|
endpoint: {
|
|
|
|
...json.endpoint,
|
|
|
|
operation: Operation.READ,
|
|
|
|
},
|
|
|
|
resource: {
|
|
|
|
fields: [],
|
|
|
|
},
|
2024-03-04 16:47:27 +01:00
|
|
|
filters: json.extra?.idFilter,
|
2021-11-05 19:55:36 +01:00
|
|
|
paginate: {
|
|
|
|
limit: 1,
|
|
|
|
},
|
|
|
|
meta: json.meta,
|
|
|
|
})
|
|
|
|
return queryFn(input, Operation.READ)
|
|
|
|
}
|
|
|
|
|
|
|
|
// when creating if an ID has been inserted need to make sure
|
|
|
|
// the id filter is enriched with it before trying to retrieve the row
|
|
|
|
checkLookupKeys(id: any, json: QueryJson) {
|
2024-04-16 18:39:05 +02:00
|
|
|
if (!id || !json.meta.table || !json.meta.table.primary) {
|
2021-11-05 19:55:36 +01:00
|
|
|
return json
|
|
|
|
}
|
|
|
|
const primaryKey = json.meta.table.primary?.[0]
|
|
|
|
json.extra = {
|
|
|
|
idFilter: {
|
|
|
|
equal: {
|
|
|
|
[primaryKey]: id,
|
|
|
|
},
|
|
|
|
},
|
|
|
|
}
|
|
|
|
return json
|
|
|
|
}
|
|
|
|
|
|
|
|
// this function recreates the returning functionality of postgres
|
|
|
|
async queryWithReturning(
|
|
|
|
json: QueryJson,
|
2024-03-04 16:47:27 +01:00
|
|
|
queryFn: QueryFunction,
|
2021-11-05 19:55:36 +01:00
|
|
|
processFn: Function = (result: any) => result
|
|
|
|
) {
|
|
|
|
const sqlClient = this.getSqlClient()
|
|
|
|
const operation = this._operation(json)
|
|
|
|
const input = this._query(json, { disableReturning: true })
|
|
|
|
if (Array.isArray(input)) {
|
|
|
|
const responses = []
|
|
|
|
for (let query of input) {
|
|
|
|
responses.push(await queryFn(query, operation))
|
|
|
|
}
|
|
|
|
return responses
|
|
|
|
}
|
|
|
|
let row
|
|
|
|
// need to manage returning, a feature mySQL can't do
|
|
|
|
if (operation === Operation.DELETE) {
|
|
|
|
row = processFn(await this.getReturningRow(queryFn, json))
|
|
|
|
}
|
|
|
|
const response = await queryFn(input, operation)
|
|
|
|
const results = processFn(response)
|
|
|
|
// same as delete, manage returning
|
|
|
|
if (operation === Operation.CREATE || operation === Operation.UPDATE) {
|
|
|
|
let id
|
2022-08-11 14:50:05 +02:00
|
|
|
if (sqlClient === SqlClient.MS_SQL) {
|
2021-11-05 19:55:36 +01:00
|
|
|
id = results?.[0].id
|
2024-09-24 13:01:47 +02:00
|
|
|
} else if (
|
|
|
|
sqlClient === SqlClient.MY_SQL ||
|
|
|
|
sqlClient === SqlClient.MARIADB
|
|
|
|
) {
|
2021-11-05 19:55:36 +01:00
|
|
|
id = results?.insertId
|
|
|
|
}
|
|
|
|
row = processFn(
|
|
|
|
await this.getReturningRow(queryFn, this.checkLookupKeys(id, json))
|
|
|
|
)
|
|
|
|
}
|
2024-06-18 19:44:17 +02:00
|
|
|
if (operation === Operation.COUNT) {
|
|
|
|
return results
|
|
|
|
}
|
2021-11-05 19:55:36 +01:00
|
|
|
if (operation !== Operation.READ) {
|
|
|
|
return row
|
|
|
|
}
|
|
|
|
return results.length ? results : [{ [operation.toLowerCase()]: true }]
|
|
|
|
}
|
2024-01-18 19:13:11 +01:00
|
|
|
|
2024-10-01 12:48:14 +02:00
|
|
|
private getTableName(
|
|
|
|
table: Table,
|
|
|
|
aliases?: Record<string, string>
|
|
|
|
): string | undefined {
|
|
|
|
let name = table.name
|
|
|
|
if (
|
|
|
|
table.sourceType === TableSourceType.INTERNAL ||
|
|
|
|
table.sourceId === INTERNAL_TABLE_SOURCE_ID
|
|
|
|
) {
|
|
|
|
if (!table._id) {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
// SQS uses the table ID rather than the table name
|
|
|
|
name = table._id
|
|
|
|
}
|
|
|
|
return aliases?.[name] ? aliases[name] : name
|
|
|
|
}
|
|
|
|
|
2024-05-23 15:57:38 +02:00
|
|
|
convertJsonStringColumns<T extends Record<string, any>>(
|
2024-03-12 16:27:34 +01:00
|
|
|
table: Table,
|
2024-05-23 15:57:38 +02:00
|
|
|
results: T[],
|
2024-03-19 17:28:25 +01:00
|
|
|
aliases?: Record<string, string>
|
2024-05-23 15:57:38 +02:00
|
|
|
): T[] {
|
2024-10-01 12:48:14 +02:00
|
|
|
const tableName = this.getTableName(table, aliases)
|
2024-03-12 16:27:34 +01:00
|
|
|
for (const [name, field] of Object.entries(table.schema)) {
|
|
|
|
if (!this._isJsonColumn(field)) {
|
|
|
|
continue
|
|
|
|
}
|
2024-10-01 12:48:14 +02:00
|
|
|
const fullName = `${tableName}.${name}`
|
2024-03-12 16:27:34 +01:00
|
|
|
for (let row of results) {
|
2024-05-23 15:57:38 +02:00
|
|
|
if (typeof row[fullName as keyof T] === "string") {
|
|
|
|
row[fullName as keyof T] = JSON.parse(row[fullName])
|
2024-03-12 16:27:34 +01:00
|
|
|
}
|
2024-05-23 15:57:38 +02:00
|
|
|
if (typeof row[name as keyof T] === "string") {
|
|
|
|
row[name as keyof T] = JSON.parse(row[name])
|
2024-03-12 16:27:34 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return results
|
|
|
|
}
|
|
|
|
|
2024-03-13 14:38:08 +01:00
|
|
|
_isJsonColumn(
|
|
|
|
field: FieldSchema
|
|
|
|
): field is JsonFieldMetadata | BBReferenceFieldMetadata {
|
2024-03-12 16:27:34 +01:00
|
|
|
return (
|
2024-05-23 16:00:13 +02:00
|
|
|
JsonTypes.includes(field.type) &&
|
|
|
|
!helpers.schema.isDeprecatedSingleUserColumn(field)
|
2024-03-12 16:27:34 +01:00
|
|
|
)
|
|
|
|
}
|
|
|
|
|
2024-03-06 19:07:46 +01:00
|
|
|
log(query: string, values?: SqlQueryBinding) {
|
2024-07-04 19:29:08 +02:00
|
|
|
sqlLog(this.getSqlClient(), query, values)
|
2024-01-18 19:13:11 +01:00
|
|
|
}
|
2021-06-03 17:31:24 +02:00
|
|
|
}
|
|
|
|
|
2021-11-05 14:56:54 +01:00
|
|
|
export default SqlQueryBuilder
|