forked from tsai/budibase
5 changed files with 342 additions and 25 deletions
@ -0,0 +1,11 @@ |
|||||
|
exports.Operation = { |
||||
|
CREATE: "CREATE", |
||||
|
READ: "READ", |
||||
|
UPDATE: "UPDATE", |
||||
|
DELETE: "DELETE", |
||||
|
} |
||||
|
|
||||
|
exports.SortDirection = { |
||||
|
ASCENDING: "ASCENDING", |
||||
|
DESCENDING: "DESCENDING", |
||||
|
} |
||||
@ -0,0 +1,124 @@ |
|||||
|
const { Operation, SortDirection } = require("./constants") |
||||
|
|
||||
|
const BASE_LIMIT = 5000 |
||||
|
|
||||
|
function addFilters(query, filters) { |
||||
|
function iterate(structure, fn) { |
||||
|
for (let [key, value] of Object.entries(structure)) { |
||||
|
fn(key, value) |
||||
|
} |
||||
|
} |
||||
|
if (filters.string) { |
||||
|
iterate(filters.string, (key, value) => { |
||||
|
query = query.where(key, "like", `${value}%`) |
||||
|
}) |
||||
|
} |
||||
|
if (filters.range) { |
||||
|
iterate(filters.range, (key, value) => { |
||||
|
if (!value.high || !value.low) { |
||||
|
return |
||||
|
} |
||||
|
query = query.whereBetween(key, [value.low, value.high]) |
||||
|
}) |
||||
|
} |
||||
|
if (filters.equal) { |
||||
|
iterate(filters.equal, (key, value) => { |
||||
|
query = query.where({ [key]: value }) |
||||
|
}) |
||||
|
} |
||||
|
if (filters.notEqual) { |
||||
|
iterate(filters.notEqual, (key, value) => { |
||||
|
query = query.whereNot({ [key]: value }) |
||||
|
}) |
||||
|
} |
||||
|
if (filters.empty) { |
||||
|
iterate(filters.empty, key => { |
||||
|
query = query.whereNull(key) |
||||
|
}) |
||||
|
} |
||||
|
if (filters.notEmpty) { |
||||
|
iterate(filters.notEmpty, key => { |
||||
|
query = query.whereNotNull(key) |
||||
|
}) |
||||
|
} |
||||
|
return query |
||||
|
} |
||||
|
|
||||
|
function buildCreate(knex, json) { |
||||
|
const { endpoint, body } = json |
||||
|
let query = knex(endpoint.entityId) |
||||
|
return query.insert(body).toString() |
||||
|
} |
||||
|
|
||||
|
function buildRead(knex, json, limit) { |
||||
|
const { endpoint, resource, filters, sort, paginate } = json |
||||
|
let query = knex(endpoint.entityId) |
||||
|
// handle select
|
||||
|
if (resource.fields && resource.fields.length > 0) { |
||||
|
query = query.select(resource.fields) |
||||
|
} else { |
||||
|
query = query.select("*") |
||||
|
} |
||||
|
// handle where
|
||||
|
query = addFilters(query, filters) |
||||
|
// 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.page && paginate.limit) { |
||||
|
const page = paginate.page <= 1 ? 0 : paginate.page - 1 |
||||
|
const offset = page * paginate.limit |
||||
|
query = query.offset(offset).limit(paginate.limit) |
||||
|
} else if (paginate.limit) { |
||||
|
query = query.limit(paginate.limit) |
||||
|
} else { |
||||
|
query.limit(limit) |
||||
|
} |
||||
|
return query.toString() |
||||
|
} |
||||
|
|
||||
|
function buildUpdate(knex, json) { |
||||
|
const { endpoint, body, filters } = json |
||||
|
let query = knex(endpoint.entityId) |
||||
|
query = addFilters(query, filters) |
||||
|
return query.update(body).toString() |
||||
|
} |
||||
|
|
||||
|
function buildDelete(knex, json) { |
||||
|
const { endpoint, filters } = json |
||||
|
let query = knex(endpoint.entityId) |
||||
|
query = addFilters(query, filters) |
||||
|
return query.delete().toString() |
||||
|
} |
||||
|
|
||||
|
class SqlQueryBuilder { |
||||
|
// pass through client to get flavour of SQL
|
||||
|
constructor(client, limit = BASE_LIMIT) { |
||||
|
this._client = client |
||||
|
this._limit = limit |
||||
|
} |
||||
|
|
||||
|
buildQuery(json) { |
||||
|
const { endpoint } = json |
||||
|
const knex = require("knex")({ client: this._client }) |
||||
|
const operation = endpoint.operation |
||||
|
switch (operation) { |
||||
|
case Operation.CREATE: |
||||
|
return buildCreate(knex, json) |
||||
|
case Operation.READ: |
||||
|
return buildRead(knex, json, this._limit) |
||||
|
case Operation.UPDATE: |
||||
|
return buildUpdate(knex, json) |
||||
|
case Operation.DELETE: |
||||
|
return buildDelete(knex, json) |
||||
|
default: |
||||
|
throw `Operation ${operation} type is not supported by SQL query builder` |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
module.exports = SqlQueryBuilder |
||||
@ -0,0 +1,120 @@ |
|||||
|
const Sql = require("../base/sql") |
||||
|
|
||||
|
const TABLE_NAME = "test" |
||||
|
|
||||
|
function endpoint(table, operation) { |
||||
|
return { |
||||
|
datasourceId: "Postgres", |
||||
|
operation: operation, |
||||
|
entityId: table || TABLE_NAME, |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
function generateReadJson({ table, fields, filters, sort, paginate} = {}) { |
||||
|
return { |
||||
|
endpoint: endpoint(table || TABLE_NAME, "READ"), |
||||
|
resource: { |
||||
|
fields: fields || [], |
||||
|
}, |
||||
|
filters: filters || {}, |
||||
|
sort: sort || {}, |
||||
|
paginate: paginate || {}, |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
function generateCreateJson(table = TABLE_NAME, body = {}) { |
||||
|
return { |
||||
|
endpoint: endpoint(table, "CREATE"), |
||||
|
body, |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
function generateUpdateJson(table = TABLE_NAME, body = {}, filters = {}) { |
||||
|
return { |
||||
|
endpoint: endpoint(table, "UPDATE"), |
||||
|
filters, |
||||
|
body, |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
function generateDeleteJson(table = TABLE_NAME, filters = {}) { |
||||
|
return { |
||||
|
endpoint: endpoint(table, "DELETE"), |
||||
|
filters, |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
describe("SQL query builder", () => { |
||||
|
const limit = 500 |
||||
|
const client = "pg" |
||||
|
let sql |
||||
|
|
||||
|
beforeEach(() => { |
||||
|
sql = new Sql(client, limit) |
||||
|
}) |
||||
|
|
||||
|
it("should test a basic read", () => { |
||||
|
const query = sql.buildQuery(generateReadJson()) |
||||
|
expect(query).toEqual(`select * from "${TABLE_NAME}" limit ${limit}`) |
||||
|
}) |
||||
|
|
||||
|
it("should test a read with specific columns", () => { |
||||
|
const query = sql.buildQuery(generateReadJson({ |
||||
|
fields: ["name", "age"] |
||||
|
})) |
||||
|
expect(query).toEqual(`select "name", "age" from "${TABLE_NAME}" limit ${limit}`) |
||||
|
}) |
||||
|
|
||||
|
it("should test a where string starts with read", () => { |
||||
|
const query = sql.buildQuery(generateReadJson({ |
||||
|
filters: { |
||||
|
string: { |
||||
|
name: "John", |
||||
|
} |
||||
|
} |
||||
|
})) |
||||
|
expect(query).toEqual(`select * from "${TABLE_NAME}" where "name" like 'John%' limit ${limit}`) |
||||
|
}) |
||||
|
|
||||
|
it("should test a where range read", () => { |
||||
|
const query = sql.buildQuery(generateReadJson({ |
||||
|
filters: { |
||||
|
range: { |
||||
|
age: { |
||||
|
low: 2, |
||||
|
high: 10, |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
})) |
||||
|
expect(query).toEqual(`select * from "${TABLE_NAME}" where "age" between 2 and 10 limit ${limit}`) |
||||
|
}) |
||||
|
|
||||
|
it("should test an create statement", () => { |
||||
|
const query = sql.buildQuery(generateCreateJson(TABLE_NAME, { |
||||
|
name: "Michael", |
||||
|
age: 45, |
||||
|
})) |
||||
|
expect(query).toEqual(`insert into "${TABLE_NAME}" ("age", "name") values (45, 'Michael')`) |
||||
|
}) |
||||
|
|
||||
|
it("should test an update statement", () => { |
||||
|
const query = sql.buildQuery(generateUpdateJson(TABLE_NAME, { |
||||
|
name: "John" |
||||
|
}, { |
||||
|
equal: { |
||||
|
id: 1001, |
||||
|
} |
||||
|
})) |
||||
|
expect(query).toEqual(`update "${TABLE_NAME}" set "name" = 'John' where "id" = 1001`) |
||||
|
}) |
||||
|
|
||||
|
it("should test a delete statement", () => { |
||||
|
const query = sql.buildQuery(generateDeleteJson(TABLE_NAME, { |
||||
|
equal: { |
||||
|
id: 1001, |
||||
|
} |
||||
|
})) |
||||
|
expect(query).toEqual(`delete from "${TABLE_NAME}" where "id" = 1001`) |
||||
|
}) |
||||
|
}) |
||||
Loading…
Reference in new issue