budibase/packages/server/src/integrations/base/sql.ts

242 lines
6.3 KiB
TypeScript
Raw Normal View History

import { Knex, knex } from "knex"
const BASE_LIMIT = 5000
2021-06-24 19:17:26 +02:00
import {
QueryJson,
SearchFilters,
QueryOptions,
SortDirection,
Operation,
RelationshipsJson,
} from "../../definitions/datasource"
type KnexQuery = Knex.QueryBuilder | Knex
2021-06-24 19:17:26 +02:00
function addFilters(
query: KnexQuery,
2021-06-24 19:17:26 +02:00
filters: SearchFilters | undefined
): KnexQuery {
2021-06-24 19:17:26 +02:00
function iterate(
structure: { [key: string]: any },
fn: (key: string, value: any) => void
) {
for (let [key, value] of Object.entries(structure)) {
fn(key, value)
}
}
if (!filters) {
return query
}
2021-06-15 14:50:41 +02:00
// if all or specified in filters, then everything is an or
const allOr = filters.allOr
if (filters.string) {
iterate(filters.string, (key, value) => {
const fnc = allOr ? "orWhere" : "where"
query = query[fnc](key, "like", `${value}%`)
})
}
if (filters.range) {
iterate(filters.range, (key, value) => {
if (!value.high || !value.low) {
return
}
const fnc = allOr ? "orWhereBetween" : "whereBetween"
query = query[fnc](key, [value.low, value.high])
})
}
if (filters.equal) {
iterate(filters.equal, (key, value) => {
const fnc = allOr ? "orWhere" : "where"
query = query[fnc]({ [key]: value })
})
}
if (filters.notEqual) {
iterate(filters.notEqual, (key, value) => {
const fnc = allOr ? "orWhereNot" : "whereNot"
query = query[fnc]({ [key]: value })
})
}
if (filters.empty) {
iterate(filters.empty, key => {
const fnc = allOr ? "orWhereNull" : "whereNull"
query = query[fnc](key)
})
}
if (filters.notEmpty) {
iterate(filters.notEmpty, key => {
const fnc = allOr ? "orWhereNotNull" : "whereNotNull"
query = query[fnc](key)
})
}
return query
}
2021-06-25 19:34:21 +02:00
function addRelationships(
query: KnexQuery,
fromTable: string,
relationships: RelationshipsJson[] | undefined
): KnexQuery {
2021-06-23 20:05:32 +02:00
if (!relationships) {
return query
}
for (let relationship of relationships) {
const from = relationship.from,
to = relationship.to,
toTable = relationship.tableName
2021-06-23 20:05:32 +02:00
if (!relationship.through) {
// @ts-ignore
query = query.leftJoin(
toTable,
`${fromTable}.${from}`,
`${relationship.tableName}.${to}`
)
2021-06-23 20:05:32 +02:00
} else {
const throughTable = relationship.through
2021-06-23 20:05:32 +02:00
query = query
// @ts-ignore
.leftJoin(
throughTable,
`${fromTable}.${from}`,
`${throughTable}.${from}`
)
.leftJoin(toTable, `${toTable}.${to}`, `${throughTable}.${to}`)
2021-06-23 20:05:32 +02:00
}
}
return query
}
2021-06-25 19:34:21 +02:00
function buildCreate(
knex: Knex,
json: QueryJson,
opts: QueryOptions
): KnexQuery {
const { endpoint, body } = json
let query: KnexQuery = knex(endpoint.entityId)
// mysql can't use returning
if (opts.disableReturning) {
return query.insert(body)
} else {
return query.insert(body).returning("*")
}
}
function buildRead(knex: Knex, json: QueryJson, limit: number): KnexQuery {
2021-06-23 20:05:32 +02:00
let { endpoint, resource, filters, sort, paginate, relationships } = json
const tableName = endpoint.entityId
let query: KnexQuery = knex(tableName)
// select all if not specified
if (!resource) {
resource = { fields: [] }
}
// handle select
if (resource.fields && resource.fields.length > 0) {
query = query.select(resource.fields)
} else {
query = query.select("*")
}
// handle where
query = addFilters(query, filters)
2021-06-23 20:05:32 +02:00
// handle join
query = addRelationships(query, tableName, relationships)
// handle sorting
if (sort) {
for (let [key, value] of Object.entries(sort)) {
const direction = value === SortDirection.ASCENDING ? "asc" : "desc"
query = query.orderBy(key, direction)
}
}
// handle pagination
if (paginate && paginate.page && paginate.limit) {
// @ts-ignore
const page = paginate.page <= 1 ? 0 : paginate.page - 1
const offset = page * paginate.limit
query = query.offset(offset).limit(paginate.limit)
} else if (paginate && paginate.limit) {
query = query.limit(paginate.limit)
} else {
query.limit(limit)
}
return query
}
2021-06-25 19:34:21 +02:00
function buildUpdate(
knex: Knex,
json: QueryJson,
opts: QueryOptions
): KnexQuery {
const { endpoint, body, filters } = json
let query: KnexQuery = knex(endpoint.entityId)
query = addFilters(query, filters)
// mysql can't use returning
if (opts.disableReturning) {
return query.update(body)
} else {
return query.update(body).returning("*")
}
}
2021-06-25 19:34:21 +02:00
function buildDelete(
knex: Knex,
json: QueryJson,
opts: QueryOptions
): KnexQuery {
const { endpoint, filters } = json
let query: KnexQuery = knex(endpoint.entityId)
query = addFilters(query, filters)
// mysql can't use returning
if (opts.disableReturning) {
return query.delete()
} else {
return query.delete().returning("*")
}
}
class SqlQueryBuilder {
private readonly sqlClient: string
private readonly limit: number
// pass through client to get flavour of SQL
constructor(client: string, limit: number = BASE_LIMIT) {
this.sqlClient = client
this.limit = limit
}
/**
* @param json the input JSON structure from which an SQL query will be built.
* @return {string} the operation that was found in the JSON.
*/
_operation(json: QueryJson): Operation {
2021-06-03 19:48:04 +02:00
return json.endpoint.operation
}
/**
* @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.
* @return {{ sql: string, bindings: object }} the query ready to be passed to the driver.
*/
_query(json: QueryJson, opts: QueryOptions = {}) {
const client = knex({ client: this.sqlClient })
let query
2021-06-03 19:48:04 +02:00
switch (this._operation(json)) {
case Operation.CREATE:
query = buildCreate(client, json, opts)
break
case Operation.READ:
query = buildRead(client, json, this.limit)
break
case Operation.UPDATE:
query = buildUpdate(client, json, opts)
break
case Operation.DELETE:
query = buildDelete(client, json, opts)
break
default:
throw `Operation type is not supported by SQL query builder`
}
// @ts-ignore
return query.toSQL().toNative()
}
}
module.exports = SqlQueryBuilder