@iamthes/query-builder
Version:
Builds sql query for RDBMS
482 lines (437 loc) • 14.8 kB
text/typescript
import {IConditionExpression} from "./condition-expression.interface";
import {ISelectItem} from "./select-item.interface";
import {QueryKind} from "./query-type.enum";
import {isArray, isString, trim, endsWith, find, includes} from "lodash";
const isNumber = require("is-number");
export default class Sql {
private _queryKind: QueryKind;
private _selects: Array<ISelectItem>;
private _tables: Array<string>;
private _wheres: Array<string>;
private _whereConcat: string;
private _whereConcatDefault: string;
private _sets: Array<any>;
private _limit: number;
private _offset: number;
private _orderBys: Array<any>;
private _joins: Array<any>;
private _groupBys: Array<string>;
private _whereGroupCount: number;
private _openWhereGroupCount: number;
private _havings: Array<any>;
constructor() {
this.reset();
}
private reset() {
this._queryKind = undefined;
this._selects = [];
this._tables = [];
this._wheres = [];
this._whereConcat = "and";
this._whereConcatDefault = "and";
this._sets = [];
this._limit = undefined;
this._offset = undefined;
this._orderBys = [];
this._joins = [];
this._groupBys = [];
this._whereGroupCount = 0;
this._openWhereGroupCount = 0;
this._havings = [];
return this;
}
select();
select(field: string);
select(func: string, field: any, alias: string);
select(field?: any, alias?: string, func?: string) {
this._queryKind = QueryKind.SELECT;
switch (arguments.length) {
case 0:
this._selects.push({ "field": "*" });
break;
case 1:
if (!isArray(field)) field = String(field).split(",");
(field as Array<string>).map(f => trim(f)).filter(Boolean).forEach(f => {
var [field, alias] = f.split(/\s*as\s*/i);
this._selects.push({ field, alias });
});
break;
case 2:
if (isString(field)) {
this._selects.push({ field, alias });
}
break;
default:
var args: Array<any> = Array.prototype.slice.call(arguments);
func = args.shift(); // First parameter
alias = args.pop(); // Last parameter
args = args.map(f => isArray(f) ? f.join(", ") : f); // Do flatten array.
this._selects.push({ func, alias, "field": args.join(", ") });
}
return this;
}
between(field, value, otherValue) {
if (otherValue === undefined) {
var split = value.split("..");
if (split.length > 1) {
[value, otherValue] = split;
}
}
var sql = field + " between " + this._wrap(value) + " and " + this._wrap(otherValue);
this._where(sql);
return this;
}
table(table: Array<string>);
table(table: string);
table(table: any) {
if (!isArray(table)) {
table = [table];
}
table.forEach(t => this._tables.push(t));
return this;
}
from(table: Array<string>);
from(table: string);
from(table: any) {
return this.table(table);
}
into(table: string);
into(table: Array<string>);
into(table: any) {
return this.table(table);
}
private static _operators = [">=", "<=", "!=", "<>", ">", "<", "!@", "@", "%$", "^%", "%", "like", "not like", "is", "is null", "is not null"];
private _conditionExpr(field: string, value?: any, operator?: string): IConditionExpression {
// console.log('_conditionExpr', arguments);
var wrapValue = true;
if (!operator) {
operator = find(Sql._operators, o => endsWith(field, o));
if (operator) {
field = field.slice(0, -operator.length).trim();
}
}
switch (operator) {
case "is null":
operator = "is";
value = "@null";
break;
case "is not null":
operator = "is not";
value = "@null";
break;
default:
if (!operator) {
operator = "=";
}
}
if (value === null) {
value = "@null";
}
if (isString(value) && value.substr(0, 1) === "@") {
wrapValue = false;
value = value.substr(1);
}
if (wrapValue && value !== undefined) {
value = this._wrap(value);
}
return { field, operator, value };
}
where(field, operator?, value?) {
if (typeof field === "object") {
for (var i in field) {
this.where(i, field[i]);
}
return this;
}
if (value === undefined) {
value = operator;
operator = undefined;
}
// console.log("field, value, operator ", field, value, operator);
var expr = this._conditionExpr(field, value, operator);
// console.log("expr ", expr);
switch (expr.operator) {
case "@": return this.whereIn(field.slice(0, -1), value);
case "!@": return this.whereNotIn(field.slice(0, -2), value);
case "!%": return this.notLike(field.slice(0, -2), value, "both");
case "%": return this.like(field.slice(0, -1), value, "both");
case "^%": return this.like(field.slice(0, -2), value, "right");
case "%$": return this.like(field.slice(0, -2), value, "left");
}
this._where(`${expr.field} ${expr.operator} ${expr.value}`);
return this;
}
whereNotIn(field, values) {
return this.whereIn(field, values, "not in");
}
whereIn(field, values: Array<string>, op = "in") {
if (!isArray(values)) values = [String(values)];
values = values.map(this._wrap);
var value = "(" + values.join(",") + ")";
var where = field + " " + op + " " + value;
this._where(where);
return this;
}
limit(limit: number, offset?: number) {
this._limit = limit;
if (offset !== undefined) {
this._offset = offset;
}
return this;
}
top(n: number) {
return this.limit(n);
}
offset(offset: number) {
this._offset = offset;
return this;
}
skip(n: number) {
return this.offset(n);
}
notLike(field, match, side) {
return this.like(field, match, side, "not like");
}
like(field: string, match = "", side = "both", op = "like") {
field = trim(field);
switch (side) {
case "left": match = "%" + match; break;
case "right": match += "%"; break;
case "both": {
if (isString(match) && match.length === 0) {
match = "%";
} else {
match = "%" + match + "%";
}
} break;
default:
throw new Error(`Unknown side ${side}`);
}
// console.log('field', JSON.stringify(field));
// console.log('op', JSON.stringify(op));
// console.log('match', JSON.stringify(match));
// console.log(JSON.stringify(`${field} ${op} ${this._wrap(match)}`));
this._where(`${field} ${op} ${this._wrap(match)}`);
return this;
}
groupBy(fields: Array<string>) {
if (!isArray(fields)) {
fields = String(fields).split(",");
}
fields
.map(f => trim(f))
.filter(f => f.length > 0)
.forEach(f => {
this._groupBys.push(f);
});
return this;
}
andOp() {
this._whereConcat = "and";
return this;
}
orOp() {
this._whereConcat = "or";
return this;
}
beginWhereGroup() {
this._whereGroupCount++;
this._openWhereGroupCount++;
return this;
}
endWhereGroup() {
if (this._whereGroupCount > 0) {
var whereCount = this._wheres.length;
if (this._openWhereGroupCount >= this._whereGroupCount) {
this._openWhereGroupCount--;
} else if (whereCount > 0) {
this._wheres[whereCount - 1] += ")";
}
this._whereGroupCount--;
}
return this;
}
private _endQuery() {
while (this._whereGroupCount > 0) {
this.endWhereGroup();
}
}
private static _joinTypes = ["inner", "outer", "left", "right", "left outer", "right outer"];
join(table: string, on: string, join: string) {
if (!includes(Sql._joinTypes, join)) join = "";
var expr = trim(`${join} join ${table} on ${on}`);
this._joins.push(expr);
return this;
}
leftJoin(table: string, on: string) {
return this.join(table, on, "left");
}
having(field, value) {
var expr = this._conditionExpr(field, value);
this._havings.push(`${expr.field} ${expr.operator} ${expr.value}`);
return this;
}
orderBy(field, direction: string = "asc") {
direction = direction.toLowerCase();
if (direction !== "asc") direction = "desc";
var expr = `${field} ${direction}`;
this._orderBys.push(expr);
return this;
}
get() {
switch (this._queryKind) {
case QueryKind.SELECT: return this.getSelect();
case QueryKind.DELETE: return this.getDelete();
case QueryKind.INSERT: return this.getInsert();
case QueryKind.UPDATE: return this.getUpdate();
default:
throw new TypeError(`Unknown kind of query ${this._queryKind}`);
}
}
getSelect() {
this._endQuery();
var sql = "select ";
var selects = this._selects
.map(item => {
var field = item.field;
if (item.func) field = item.func + "(" + field + ")";
if (item.alias) field = field + " as " + item.alias;
return field;
})
.join(", ");
if (selects === "") selects = "*";
sql += selects;
if (this._tables.length > 0) sql += " " + "from " + this._tables.join(", ");
if (this._joins.length > 0) sql += " " + this._joins.join(" ");
if (this._wheres.length > 0) sql += " " + "where " + this._wheres.join(" ");
if (this._groupBys.length > 0) sql += " " + "group by " + this._groupBys.join(", ");
if (this._havings.length > 0) sql += " " + "having " + this._havings.join(" ");
if (this._orderBys.length > 0) sql += " " + "order by " + this._orderBys.join(", ");
if (isNumber(this._limit)) {
sql += " ";
sql = this.getLimit(sql, this._limit, this._offset);
}
this.reset();
return sql;
}
delete(table?: string) {
this._queryKind = QueryKind.DELETE;
if (table) {
this._tables.push(table);
}
return this;
}
getDelete() {
var table = this._tables[0];
var result = "delete " + table;
if (this._wheres.length > 0) {
result += " " + "where " + this._wheres.join(" ");
}
this.reset();
return result;
}
update(table?: string) {
this._queryKind = QueryKind.UPDATE;
if (table) {
this._tables.push(table);
}
return this;
}
getUpdate() {
this._endQuery();
var table = this._tables[0];
var result = "update " + table + " set ";
result += this._sets
.map(item => {
var value = item.value;
if (item.wrapValue) value = this._wrap(value);
return `${item.name} = ${value}`;
})
.join(", ");
if (this._wheres.length > 0) {
result += " where " + this._wheres.join(" ");
}
this.reset();
return result;
}
insert() {
this._queryKind = QueryKind.INSERT;
return this;
}
set(name, value, wrapValue: boolean = true) {
if (arguments.length === 1) {
for (var i in name) {
this.set(i, name[i], true);
}
return this;
}
if (value === null || value === undefined) {
value = "null";
wrapValue = false;
}
this._sets.push({
name: name,
value: value,
wrapValue: wrapValue
});
return this;
}
getInsert() {
var table = this._tables[0];
var result = "insert into " + table;
var names = this._sets.map(item => item.name);
var values = this._sets.map(item => {
var value = item.value;
if (item.wrapValue) value = this._wrap(value);
return value;
});
result += "(" + names.join(", ") + ") values(" + values.join(", ") + ")";
this.reset();
return result;
}
getLimit(sql, limit, offset): string {
throw "getLimit() not supported.";
}
private _where(sql: string) {
var concat = "";
if (this._wheres.length > 0) {
concat = this._whereConcat + " ";
}
while (this._openWhereGroupCount > 0) {
concat += "(";
this._openWhereGroupCount--;
}
// console.log("concat _w ", JSON.stringify(concat));
// console.log("sql _w ", JSON.stringify(sql));
this._whereConcat = this._whereConcatDefault;
this._wheres.push(concat + sql);
// console.log("this._wheres ", this._wheres);
}
private _wrap(value: any) {
// console.log("_wrap ", value);
if (!isNumber(value)) {
value = '\'' + JSON.stringify(value).replace(/^"|"$/g, '')
.replace(/'/g, "\\'")
.replace(/\\"/g, '"') + '\'';
}
return value;
}
private _quote(value: string, wrapInQuotes: boolean) {
console.log("_quote ", value);
if (isNumber(value)) {
return value;
}
value = value
.replace("\\", "\\\\")
.replace("\0", "\\0")
.replace("\n", "\\n")
.replace("\r", "\\r")
.replace("'", "\\'")
.replace("\"", "\\\"")
.replace("\x1a", "\\Z");
if (wrapInQuotes) {
value = "'" + value + "'";
}
return value;
}
}