@jakub.knejzlik/ts-query
Version:
TypeScript implementation of SQL builder
137 lines (136 loc) • 5.77 kB
JavaScript
;
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.DefaultFlavor = void 0;
const dayjs_1 = __importDefault(require("dayjs"));
const Expression_1 = require("../Expression");
const utc_1 = __importDefault(require("dayjs/plugin/utc"));
const timezone_1 = __importDefault(require("dayjs/plugin/timezone"));
dayjs_1.default.extend(utc_1.default);
dayjs_1.default.extend(timezone_1.default);
class DefaultFlavor {
constructor(options) {
this.options = options;
this.columnQuotes = "`";
this.stringQuotes = `"`;
this.options = options;
}
escapeColumn(name, legacyParsing) {
if (name === "NULL") {
return name;
}
if (legacyParsing) {
// Handle unary minus prefix: -column_name → -`column_name`
const unaryMinusMatch = name.match(/^-([a-zA-Z_][a-zA-Z0-9_.]*\*?)$/);
if (unaryMinusMatch) {
return `-${this.escapeColumn(unaryMinusMatch[1], legacyParsing)}`;
}
const columnMatch = name.match(/^[\.a-zA-Z0-9_*]+$/);
if (columnMatch) {
return name
.split(".")
.map(part => part === "*" ? "*" : `${this.columnQuotes}${part.replace(new RegExp(`/${this.columnQuotes}/`, "g"), `${this.columnQuotes}${this.columnQuotes}`)}${this.columnQuotes}`)
.join(".");
}
// In legacy mode, allow through values that appear to be:
// - Already escaped (contains column quotes)
// - Complex expressions (parentheses, operators, spaces)
// - String literals (already quoted with string quotes)
// - Special values like *, +0, -1, TRUE, FALSE
if (name.includes(this.columnQuotes) ||
name.includes(this.stringQuotes) ||
name.includes('(') ||
name.includes(')') ||
name.includes(' ') ||
name.includes(',') ||
/^[+\-]?\d+$/.test(name) ||
/^[*]$/.test(name) ||
name === 'TRUE' ||
name === 'FALSE') {
return `${name}`;
}
// Escape the column name to prevent SQL injection
return `${this.columnQuotes}${name
.replace(new RegExp(this.columnQuotes, "g"), `${this.columnQuotes}${this.columnQuotes}`)
.split(".")
.join(`${this.columnQuotes}.${this.columnQuotes}`)}${this.columnQuotes}`;
}
return name
.split(".")
.map(part => part === "*" ? "*" : `${this.columnQuotes}${part}${this.columnQuotes}`)
.join(".");
}
escapeTable(table) {
if (table.indexOf("-") !== -1) {
return `${this.columnQuotes}${table}${this.columnQuotes}`;
}
return this.escapeColumn(table);
}
escapeRawValue(value) {
return `${value}`;
}
escapeValue(value) {
var _a;
if (value === null) {
return "NULL";
}
if (value instanceof Date) {
return this.escapeValue((0, dayjs_1.default)(value).tz((_a = this.options) === null || _a === void 0 ? void 0 : _a.timezone).format("YYYY-MM-DD HH:mm:ss"));
}
if (typeof value === "string") {
return `${this.stringQuotes}${value.replace(new RegExp(`${this.stringQuotes}`, "g"), `${this.stringQuotes}${this.stringQuotes}`)}${this.stringQuotes}`;
}
return `${value}`;
}
escapeLimitAndOffset(limit, offset) {
let str = "";
if (limit !== undefined) {
str += ` LIMIT ${limit}`;
}
if (offset !== undefined) {
str += ` OFFSET ${offset}`;
}
return str;
}
escapeFunction(fn) {
const args = fn.value
.map((x) => Expression_1.Expression.deserialize(x))
.map((x) => x.toSQL(this));
if (fn.name === "DATEADD") {
const argsValues = fn.value.map((x) => Expression_1.ExpressionBase.deserializeValue(x));
const interval = parseInt(argsValues[1].value.toString(), 10);
if (isNaN(interval)) {
throw new Error(`Invalid DATEADD interval: ${argsValues[1].value}`);
}
const intervalType = argsValues[2].value.toString().toLowerCase();
// MySQL supports: YEAR, MONTH, DAY, HOUR, MINUTE, SECOND, WEEK, QUARTER
const validIntervalTypes = ['year', 'month', 'day', 'hour', 'minute', 'second', 'week', 'quarter'];
if (!validIntervalTypes.includes(intervalType)) {
throw new Error(`Invalid DATEADD interval type: ${intervalType}`);
}
return `DATE_ADD(${args[0]}, INTERVAL ${interval} ${intervalType.toUpperCase()})`;
}
return `${fn.name}(${args.join(",")})`;
}
escapeOperation(fn) {
return ("(" +
fn.value
.map((x) => Expression_1.Expression.deserialize(x).toSQL(this))
.join(` ${fn.operation} `) +
")");
}
escapeUnion(unionType, leftSQL, rightSQL) {
return `(${leftSQL}) ${unionType} (${rightSQL})`;
}
escapeLikeCondition(column, pattern, isLike, caseInsensitive) {
const notPrefix = isLike ? "" : "NOT ";
if (caseInsensitive) {
// MySQL/SQLite: use LOWER() wrapper
return `LOWER(${column}) ${notPrefix}LIKE LOWER(${pattern})`;
}
return `${column} ${notPrefix}LIKE ${pattern}`;
}
}
exports.DefaultFlavor = DefaultFlavor;