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