UNPKG

@jakub.knejzlik/ts-query

Version:

TypeScript implementation of SQL builder

375 lines (374 loc) 15.6 kB
"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;