@jakub.knejzlik/ts-query
Version:
TypeScript implementation of SQL builder
375 lines (374 loc) • 15.6 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.NotCondition = exports.ColumnComparisonCondition = exports.LikeCondition = exports.NullCondition = exports.NotInCondition = exports.InCondition = exports.BetweenCondition = exports.LogicalCondition = exports.BinaryCondition = exports.Cond = exports.Conditions = exports.Condition = void 0;
const Expression_1 = require("./Expression");
const Query_1 = require("./Query");
// Normalizes the value(s) passed to an IN/NOT IN condition.
// - A SelectQuery (passed directly or as the only element of an array) becomes a subquery.
// - An empty/null list is rendered fail-closed as `(NULL)` so an empty IN never silently
// drops the filter (which would expose all rows). The NULL marker survives serialization.
const normalizeInValues = (values) => {
if (values instanceof Query_1.SelectQuery) {
return { subquery: values, values: [] };
}
const arr = values !== null && values !== void 0 ? values : [];
const subqueryCount = arr.filter((v) => v instanceof Query_1.SelectQuery).length;
if (subqueryCount > 0) {
// A subquery is only valid as the sole value: `IN (SELECT ...)`. Mixing a
// subquery with scalar values (or multiple subqueries) is invalid SQL — fail
// loud rather than emitting malformed SQL / throwing later on deserialize.
if (subqueryCount > 1 || arr.length > 1) {
throw new Error("Cond.in/notIn: cannot mix a subquery with other values or use multiple subqueries; pass a single subquery or a list of scalar values");
}
return { subquery: arr[0], values: [] };
}
if (arr.length === 0) {
return { values: [Query_1.Q.exprValue(Query_1.Q.raw("NULL"))] };
}
return { values: arr.map((v) => Query_1.Q.exprValue(v)) };
};
class Condition {
toSQL(flavor) {
throw new Error("Method not implemented.");
}
toJSON() {
throw new Error("Method not implemented.");
}
serialize() {
return JSON.stringify(this.toJSON());
}
static fromJSON(json) {
switch (json.type) {
case "BinaryCondition":
return BinaryCondition.fromJSON(json);
case "LogicalCondition":
return LogicalCondition.fromJSON(json);
case "BetweenCondition":
return BetweenCondition.fromJSON(json);
case "InCondition":
return InCondition.fromJSON(json);
case "NotInCondition":
return NotInCondition.fromJSON(json);
case "NullCondition":
return NullCondition.fromJSON(json);
case "LikeCondition":
return LikeCondition.fromJSON(json);
case "ColumnComparisonCondition":
return ColumnComparisonCondition.fromJSON(json);
case "NotCondition":
return NotCondition.fromJSON(json);
default:
throw new Error(`Unknown condition type: ${json.type} (${JSON.stringify(json)})`);
}
}
static deserialize(value) {
if (typeof value === "string") {
try {
return Condition.fromJSON(JSON.parse(value));
}
catch (_a) {
return null;
}
}
return value;
}
}
exports.Condition = Condition;
class BinaryCondition extends Condition {
constructor(key, value, operator) {
super();
this.key = Query_1.Q.expr(key);
this.value = Query_1.Q.value(value);
this.operator = operator;
}
toSQL(flavor) {
return `${this.key.toSQL(flavor)} ${this.operator} ${this.value.toSQL(flavor)}`;
}
// serialization
toJSON() {
return {
type: "BinaryCondition",
key: this.key.serialize(),
value: this.value.serialize(),
operator: this.operator,
};
}
static fromJSON(json) {
return new BinaryCondition(Expression_1.ExpressionBase.deserialize(json.key), Expression_1.ExpressionBase.deserializeValue(json.value), json.operator);
}
}
exports.BinaryCondition = BinaryCondition;
class LogicalCondition extends Condition {
constructor(conditions, operator) {
super();
this.conditions = conditions;
this.operator = operator;
}
toSQL(flavor) {
return `(${this.conditions
.map((c) => c.toSQL(flavor))
.join(` ${this.operator} `)})`;
}
// serialization
toJSON() {
return {
type: "LogicalCondition",
conditions: this.conditions.map((condition) => condition.toJSON()),
operator: this.operator,
};
}
static fromJSON(json) {
const conditions = json.conditions.map(Condition.fromJSON);
return new LogicalCondition(conditions, json.operator);
}
}
exports.LogicalCondition = LogicalCondition;
class BetweenCondition extends Condition {
constructor(key, from, to) {
super();
this.key = Query_1.Q.expr(key);
this.from = Query_1.Q.expr(from);
this.to = Query_1.Q.expr(to);
}
toSQL(flavor) {
return `${this.key.toSQL(flavor)} BETWEEN ${this.from.toSQL(flavor)} AND ${this.to.toSQL(flavor)}`;
}
// serialization
toJSON() {
return {
type: "BetweenCondition",
key: this.key.serialize(),
from: this.from.serialize(),
to: this.to.serialize(),
};
}
static fromJSON(json) {
return new BetweenCondition(Expression_1.ExpressionBase.deserialize(json.key), Expression_1.ExpressionBase.deserializeValue(json.from), Expression_1.ExpressionBase.deserializeValue(json.to));
}
}
exports.BetweenCondition = BetweenCondition;
class InCondition extends Condition {
constructor(key, values) {
super();
this.key = Query_1.Q.expr(key);
const normalized = normalizeInValues(values);
this.values = normalized.values;
this.subquery = normalized.subquery;
}
toSQL(flavor) {
const right = this.subquery
? this.subquery.toSQL(flavor)
: this.values.map((v) => v.toSQL(flavor)).join(", ");
return `${this.key.toSQL(flavor)} IN (${right})`;
}
// serialization
toJSON() {
return Object.assign({ type: "InCondition", key: this.key.serialize() }, (this.subquery
? { subquery: this.subquery.toJSON() }
: { values: this.values.map((v) => v.serialize()) }));
}
static fromJSON(json) {
if (json.subquery) {
return new InCondition(Expression_1.ExpressionBase.deserialize(json.key), Query_1.SelectQuery.fromJSON(json.subquery));
}
return new InCondition(Expression_1.ExpressionBase.deserialize(json.key), json.values.map(Expression_1.ExpressionBase.deserializeValue));
}
}
exports.InCondition = InCondition;
class NotInCondition extends Condition {
constructor(key, values) {
super();
this.key = Query_1.Q.expr(key);
const normalized = normalizeInValues(values);
this.values = normalized.values;
this.subquery = normalized.subquery;
}
toSQL(flavor) {
const right = this.subquery
? this.subquery.toSQL(flavor)
: this.values.map((v) => v.toSQL(flavor)).join(", ");
return `${this.key.toSQL(flavor)} NOT IN (${right})`;
}
// serialization
toJSON() {
return Object.assign({ type: "NotInCondition", key: this.key.serialize() }, (this.subquery
? { subquery: this.subquery.toJSON() }
: { values: this.values.map((v) => v.serialize()) }));
}
static fromJSON(json) {
if (json.subquery) {
return new NotInCondition(Expression_1.ExpressionBase.deserialize(json.key), Query_1.SelectQuery.fromJSON(json.subquery));
}
return new NotInCondition(Expression_1.ExpressionBase.deserialize(json.key), json.values.map(Expression_1.ExpressionBase.deserializeValue));
}
}
exports.NotInCondition = NotInCondition;
class NullCondition extends Condition {
constructor(key, isNull) {
super();
this.key = Query_1.Q.expr(key);
this.isNull = isNull;
}
toSQL(flavor) {
return `${this.key.toSQL(flavor)} IS ${this.isNull ? "" : "NOT "}NULL`;
}
// serialization
toJSON() {
return {
type: "NullCondition",
key: this.key.serialize(),
isNull: this.isNull,
};
}
static fromJSON(json) {
return new NullCondition(Expression_1.ExpressionBase.deserialize(json.key), json.isNull);
}
}
exports.NullCondition = NullCondition;
class LikeCondition extends Condition {
constructor(key, pattern, isLike, caseInsensitive = false) {
super();
this.key = Query_1.Q.expr(key);
this.pattern = pattern;
this.isLike = isLike;
this.caseInsensitive = caseInsensitive;
}
toSQL(flavor) {
const escapedColumn = this.key.toSQL(flavor);
const escapedPattern = flavor.escapeValue(this.pattern);
return flavor.escapeLikeCondition(escapedColumn, escapedPattern, this.isLike, this.caseInsensitive);
}
// serialization
toJSON() {
return {
type: "LikeCondition",
key: this.key.serialize(),
pattern: this.pattern,
isLike: this.isLike,
caseInsensitive: this.caseInsensitive,
};
}
static fromJSON(json) {
var _a;
return new LikeCondition(Expression_1.ExpressionBase.deserialize(json.key), json.pattern, json.isLike, (_a = json.caseInsensitive) !== null && _a !== void 0 ? _a : false);
}
}
exports.LikeCondition = LikeCondition;
class ColumnComparisonCondition extends Condition {
constructor(leftKey, rightKey, operator) {
super();
this.leftKey = Query_1.Q.expr(leftKey);
this.rightKey = Query_1.Q.expr(rightKey);
this.operator = operator;
}
toSQL(flavor) {
return `${this.leftKey.toSQL(flavor)} ${this.operator} ${this.rightKey.toSQL(flavor)}`;
}
// serialization
toJSON() {
return {
type: "ColumnComparisonCondition",
leftKey: this.leftKey.serialize(),
rightKey: this.rightKey.serialize(),
operator: this.operator,
};
}
static fromJSON(json) {
return new ColumnComparisonCondition(Expression_1.ExpressionBase.deserialize(json.leftKey), Expression_1.ExpressionBase.deserialize(json.rightKey), json.operator);
}
}
exports.ColumnComparisonCondition = ColumnComparisonCondition;
class NotCondition extends Condition {
constructor(condition) {
super();
this.condition = condition;
}
toSQL(flavor) {
return `NOT (${this.condition.toSQL(flavor)})`;
}
// serialization
toJSON() {
return {
type: "NotCondition",
condition: this.condition.toJSON(),
};
}
static fromJSON(json) {
return new NotCondition(Condition.fromJSON(json.condition));
}
}
exports.NotCondition = NotCondition;
exports.Conditions = {
fromString: (column, value, delimiters = [" "]) => {
const str = `${value}`;
if (str.indexOf(">=") === 0) {
return exports.Conditions.greaterThanOrEqual(column, str.substring(2));
}
if (str.indexOf("<=") === 0) {
return exports.Conditions.lessThanOrEqual(column, str.substring(2));
}
if (str.indexOf(">") === 0) {
return exports.Conditions.greaterThan(column, str.substring(1));
}
if (str.indexOf("<") === 0) {
return exports.Conditions.lessThan(column, str.substring(1));
}
if (str.indexOf("~") === 0) {
const searchValue = str.substring(1);
const conditions = [exports.Conditions.like(column, `${searchValue}%`)];
for (const delimiter of delimiters) {
conditions.push(exports.Conditions.like(column, `%${delimiter}${searchValue}%`));
}
return exports.Conditions.or(conditions);
}
if (str.indexOf("!~") === 0) {
const searchValue = str.substring(2);
const conditions = [exports.Conditions.notLike(column, `${searchValue}%`)];
for (const delimiter of delimiters) {
conditions.push(exports.Conditions.notLike(column, `%${delimiter}${searchValue}%`));
}
return exports.Conditions.and(conditions);
}
if (str.indexOf("!") === 0) {
return exports.Conditions.notEqual(column, `${str.substring(1)}%`);
}
return exports.Conditions.equal(column, str);
},
equal: (key, value) => new BinaryCondition(key, value, "="),
notEqual: (key, value) => new BinaryCondition(key, value, "!="),
greaterThan: (key, value) => new BinaryCondition(key, value, ">"),
lessThan: (key, value) => new BinaryCondition(key, value, "<"),
greaterThanOrEqual: (key, value) => new BinaryCondition(key, value, ">="),
lessThanOrEqual: (key, value) => new BinaryCondition(key, value, "<="),
between: (key, values) => new BetweenCondition(key, Query_1.Q.exprValue(values[0]), Query_1.Q.exprValue(values[1])),
// Fail-closed: an empty/null list renders `IN (NULL)` (never matches) instead of being
// dropped, so an empty IN can never silently expand visibility. Accepts a subquery too.
in: (key, values) => new InCondition(key, values),
notIn: (key, values) => new NotInCondition(key, values),
and: (conditions) => {
const _c = (conditions || []).filter((c) => c !== null);
return _c.length > 0 ? new LogicalCondition(_c, "AND") : null;
},
or: (conditions) => {
const _c = (conditions || []).filter((c) => c !== null);
return _c.length > 0 ? new LogicalCondition(_c, "OR") : null;
},
isNull: (key) => new NullCondition(key, true),
isNotNull: (key) => new NullCondition(key, false),
// Deprecated: use isNull instead
null: (key) => new NullCondition(key, true),
// Deprecated: use isNotNull instead
notNull: (key) => new NullCondition(key, false),
like: (key, pattern, opts) => { var _a; return new LikeCondition(key, pattern, true, (_a = opts === null || opts === void 0 ? void 0 : opts.caseInsensitive) !== null && _a !== void 0 ? _a : false); },
notLike: (key, pattern, opts) => { var _a; return new LikeCondition(key, pattern, false, (_a = opts === null || opts === void 0 ? void 0 : opts.caseInsensitive) !== null && _a !== void 0 ? _a : false); },
ilike: (key, pattern) => new LikeCondition(key, pattern, true, true),
notIlike: (key, pattern) => new LikeCondition(key, pattern, false, true),
columnEqual: (leftKey, rightKey) => new ColumnComparisonCondition(leftKey, rightKey, "="),
columnNotEqual: (leftKey, rightKey) => new ColumnComparisonCondition(leftKey, rightKey, "!="),
columnGreaterThan: (leftKey, rightKey) => new ColumnComparisonCondition(leftKey, rightKey, ">"),
columnLessThan: (leftKey, rightKey) => new ColumnComparisonCondition(leftKey, rightKey, "<"),
columnGreaterThanOrEqual: (leftKey, rightKey) => new ColumnComparisonCondition(leftKey, rightKey, ">="),
columnLessThanOrEqual: (leftKey, rightKey) => new ColumnComparisonCondition(leftKey, rightKey, "<="),
not: (condition) => new NotCondition(condition),
};
exports.Cond = exports.Conditions;