399 lines
14 KiB
JavaScript
399 lines
14 KiB
JavaScript
"use strict";
|
|
/**
|
|
* Adapted from https://github.com/mysqljs/sqlstring/blob/cd528556b4b6bcf300c3db515026935dedf7cfa1/lib/SqlString.js
|
|
* MIT LICENSE: https://github.com/mysqljs/sqlstring/blob/cd528556b4b6bcf300c3db515026935dedf7cfa1/LICENSE
|
|
*/
|
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
exports.raw = exports.format = exports.escape = exports.arrayToList = exports.bufferToString = exports.objectToValues = exports.escapeId = exports.dateToString = void 0;
|
|
const node_buffer_1 = require("node:buffer");
|
|
const regex = {
|
|
backtick: /`/g,
|
|
dot: /\./g,
|
|
timezone: /([+\-\s])(\d\d):?(\d\d)?/,
|
|
escapeChars: /[\0\b\t\n\r\x1a"'\\]/g,
|
|
};
|
|
const CHARS_ESCAPE_MAP = {
|
|
'\0': '\\0',
|
|
'\b': '\\b',
|
|
'\t': '\\t',
|
|
'\n': '\\n',
|
|
'\r': '\\r',
|
|
'\x1a': '\\Z',
|
|
'"': '\\"',
|
|
"'": "\\'",
|
|
'\\': '\\\\',
|
|
};
|
|
const charCode = {
|
|
singleQuote: 39,
|
|
backtick: 96,
|
|
backslash: 92,
|
|
dash: 45,
|
|
slash: 47,
|
|
asterisk: 42,
|
|
questionMark: 63,
|
|
newline: 10,
|
|
space: 32,
|
|
tab: 9,
|
|
carriageReturn: 13,
|
|
};
|
|
const isRecord = (value) => typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
const isWordChar = (code) => (code >= 65 && code <= 90) ||
|
|
(code >= 97 && code <= 122) ||
|
|
(code >= 48 && code <= 57) ||
|
|
code === 95;
|
|
const isWhitespace = (code) => code === charCode.space ||
|
|
code === charCode.tab ||
|
|
code === charCode.newline ||
|
|
code === charCode.carriageReturn;
|
|
const hasOnlyWhitespaceBetween = (sql, start, end) => {
|
|
if (start >= end)
|
|
return true;
|
|
for (let i = start; i < end; i++) {
|
|
const code = sql.charCodeAt(i);
|
|
if (code !== charCode.space &&
|
|
code !== charCode.tab &&
|
|
code !== charCode.newline &&
|
|
code !== charCode.carriageReturn)
|
|
return false;
|
|
}
|
|
return true;
|
|
};
|
|
const toLower = (code) => code | 32;
|
|
const matchesWord = (sql, position, word, length) => {
|
|
for (let offset = 0; offset < word.length; offset++)
|
|
if (toLower(sql.charCodeAt(position + offset)) !== word.charCodeAt(offset))
|
|
return false;
|
|
return ((position === 0 || !isWordChar(sql.charCodeAt(position - 1))) &&
|
|
(position + word.length >= length ||
|
|
!isWordChar(sql.charCodeAt(position + word.length))));
|
|
};
|
|
const skipSqlContext = (sql, position) => {
|
|
const currentChar = sql.charCodeAt(position);
|
|
const nextChar = sql.charCodeAt(position + 1);
|
|
if (currentChar === charCode.singleQuote) {
|
|
for (let cursor = position + 1; cursor < sql.length; cursor++) {
|
|
if (sql.charCodeAt(cursor) === charCode.backslash)
|
|
cursor++;
|
|
else if (sql.charCodeAt(cursor) === charCode.singleQuote)
|
|
return cursor + 1;
|
|
}
|
|
return sql.length;
|
|
}
|
|
if (currentChar === charCode.backtick) {
|
|
const length = sql.length;
|
|
for (let cursor = position + 1; cursor < length; cursor++) {
|
|
if (sql.charCodeAt(cursor) !== charCode.backtick)
|
|
continue;
|
|
if (sql.charCodeAt(cursor + 1) === charCode.backtick) {
|
|
cursor++;
|
|
continue;
|
|
}
|
|
return cursor + 1;
|
|
}
|
|
return length;
|
|
}
|
|
if (currentChar === charCode.dash && nextChar === charCode.dash) {
|
|
const lineBreak = sql.indexOf('\n', position + 2);
|
|
return lineBreak === -1 ? sql.length : lineBreak + 1;
|
|
}
|
|
if (currentChar === charCode.slash && nextChar === charCode.asterisk) {
|
|
const commentEnd = sql.indexOf('*/', position + 2);
|
|
return commentEnd === -1 ? sql.length : commentEnd + 2;
|
|
}
|
|
return -1;
|
|
};
|
|
const findNextPlaceholder = (sql, start) => {
|
|
const sqlLength = sql.length;
|
|
for (let position = start; position < sqlLength; position++) {
|
|
const code = sql.charCodeAt(position);
|
|
if (code === charCode.questionMark)
|
|
return position;
|
|
if (code === charCode.singleQuote ||
|
|
code === charCode.backtick ||
|
|
code === charCode.dash ||
|
|
code === charCode.slash) {
|
|
const contextEnd = skipSqlContext(sql, position);
|
|
if (contextEnd !== -1)
|
|
position = contextEnd - 1;
|
|
}
|
|
}
|
|
return -1;
|
|
};
|
|
const findSetKeyword = (sql, startFrom = 0) => {
|
|
const length = sql.length;
|
|
for (let position = startFrom; position < length; position++) {
|
|
const code = sql.charCodeAt(position);
|
|
const lower = code | 32;
|
|
if (code === charCode.singleQuote ||
|
|
code === charCode.backtick ||
|
|
code === charCode.dash ||
|
|
code === charCode.slash) {
|
|
const contextEnd = skipSqlContext(sql, position);
|
|
if (contextEnd !== -1) {
|
|
position = contextEnd - 1;
|
|
continue;
|
|
}
|
|
}
|
|
if (lower === 115 && matchesWord(sql, position, 'set', length))
|
|
return position + 3;
|
|
if (lower === 107 && matchesWord(sql, position, 'key', length)) {
|
|
let cursor = position + 3;
|
|
while (cursor < length && isWhitespace(sql.charCodeAt(cursor)))
|
|
cursor++;
|
|
if (matchesWord(sql, cursor, 'update', length))
|
|
return cursor + 6;
|
|
}
|
|
}
|
|
return -1;
|
|
};
|
|
const isDate = (value) => Object.prototype.toString.call(value) === '[object Date]';
|
|
const hasSqlString = (value) => typeof value === 'object' &&
|
|
value !== null &&
|
|
'toSqlString' in value &&
|
|
typeof value.toSqlString === 'function';
|
|
const escapeString = (value) => {
|
|
regex.escapeChars.lastIndex = 0;
|
|
let chunkIndex = 0;
|
|
let escapedValue = '';
|
|
let match;
|
|
for (match = regex.escapeChars.exec(value); match !== null; match = regex.escapeChars.exec(value)) {
|
|
escapedValue += value.slice(chunkIndex, match.index);
|
|
escapedValue += CHARS_ESCAPE_MAP[match[0]];
|
|
chunkIndex = regex.escapeChars.lastIndex;
|
|
}
|
|
if (chunkIndex === 0)
|
|
return `'${value}'`;
|
|
if (chunkIndex < value.length)
|
|
return `'${escapedValue}${value.slice(chunkIndex)}'`;
|
|
return `'${escapedValue}'`;
|
|
};
|
|
const pad2 = (value) => (value < 10 ? '0' + value : '' + value);
|
|
const pad3 = (value) => value < 10 ? '00' + value : value < 100 ? '0' + value : '' + value;
|
|
const pad4 = (value) => value < 10
|
|
? '000' + value
|
|
: value < 100
|
|
? '00' + value
|
|
: value < 1000
|
|
? '0' + value
|
|
: '' + value;
|
|
const convertTimezone = (tz) => {
|
|
if (tz === 'Z')
|
|
return 0;
|
|
const timezoneMatch = tz.match(regex.timezone);
|
|
if (timezoneMatch)
|
|
return ((timezoneMatch[1] === '-' ? -1 : 1) *
|
|
(Number.parseInt(timezoneMatch[2], 10) +
|
|
(timezoneMatch[3] ? Number.parseInt(timezoneMatch[3], 10) : 0) / 60) *
|
|
60);
|
|
return false;
|
|
};
|
|
const dateToString = (date, timezone) => {
|
|
if (Number.isNaN(date.getTime()))
|
|
return 'NULL';
|
|
let year;
|
|
let month;
|
|
let day;
|
|
let hour;
|
|
let minute;
|
|
let second;
|
|
let millisecond;
|
|
if (timezone === 'local') {
|
|
year = date.getFullYear();
|
|
month = date.getMonth() + 1;
|
|
day = date.getDate();
|
|
hour = date.getHours();
|
|
minute = date.getMinutes();
|
|
second = date.getSeconds();
|
|
millisecond = date.getMilliseconds();
|
|
}
|
|
else {
|
|
const timezoneOffsetMinutes = convertTimezone(timezone);
|
|
let time = date.getTime();
|
|
if (timezoneOffsetMinutes !== false && timezoneOffsetMinutes !== 0)
|
|
time += timezoneOffsetMinutes * 60000;
|
|
const adjustedDate = new Date(time);
|
|
year = adjustedDate.getUTCFullYear();
|
|
month = adjustedDate.getUTCMonth() + 1;
|
|
day = adjustedDate.getUTCDate();
|
|
hour = adjustedDate.getUTCHours();
|
|
minute = adjustedDate.getUTCMinutes();
|
|
second = adjustedDate.getUTCSeconds();
|
|
millisecond = adjustedDate.getUTCMilliseconds();
|
|
}
|
|
// YYYY-MM-DD HH:mm:ss.mmm
|
|
return escapeString(pad4(year) +
|
|
'-' +
|
|
pad2(month) +
|
|
'-' +
|
|
pad2(day) +
|
|
' ' +
|
|
pad2(hour) +
|
|
':' +
|
|
pad2(minute) +
|
|
':' +
|
|
pad2(second) +
|
|
'.' +
|
|
pad3(millisecond));
|
|
};
|
|
exports.dateToString = dateToString;
|
|
const escapeId = (value, forbidQualified) => {
|
|
if (Array.isArray(value)) {
|
|
const length = value.length;
|
|
const parts = new Array(length);
|
|
for (let i = 0; i < length; i++)
|
|
parts[i] = (0, exports.escapeId)(value[i], forbidQualified);
|
|
return parts.join(', ');
|
|
}
|
|
const identifier = String(value);
|
|
const hasJsonOperator = identifier.indexOf('->') !== -1;
|
|
if (forbidQualified || hasJsonOperator) {
|
|
if (identifier.indexOf('`') === -1)
|
|
return `\`${identifier}\``;
|
|
return `\`${identifier.replace(regex.backtick, '``')}\``;
|
|
}
|
|
if (identifier.indexOf('`') === -1 && identifier.indexOf('.') === -1)
|
|
return `\`${identifier}\``;
|
|
return `\`${identifier
|
|
.replace(regex.backtick, '``')
|
|
.replace(regex.dot, '`.`')}\``;
|
|
};
|
|
exports.escapeId = escapeId;
|
|
const objectToValues = (object, timezone) => {
|
|
const keys = Object.keys(object);
|
|
const keysLength = keys.length;
|
|
if (keysLength === 0)
|
|
return '';
|
|
let sql = '';
|
|
for (let i = 0; i < keysLength; i++) {
|
|
const key = keys[i];
|
|
const value = object[key];
|
|
if (typeof value === 'function')
|
|
continue;
|
|
if (sql.length > 0)
|
|
sql += ', ';
|
|
sql += (0, exports.escapeId)(key);
|
|
sql += ' = ';
|
|
sql += (0, exports.escape)(value, true, timezone);
|
|
}
|
|
return sql;
|
|
};
|
|
exports.objectToValues = objectToValues;
|
|
const bufferToString = (buffer) => `X${escapeString(buffer.toString('hex'))}`;
|
|
exports.bufferToString = bufferToString;
|
|
const arrayToList = (array, timezone) => {
|
|
const length = array.length;
|
|
const parts = new Array(length);
|
|
for (let i = 0; i < length; i++) {
|
|
const value = array[i];
|
|
if (Array.isArray(value))
|
|
parts[i] = `(${(0, exports.arrayToList)(value, timezone)})`;
|
|
else
|
|
parts[i] = (0, exports.escape)(value, true, timezone);
|
|
}
|
|
return parts.join(', ');
|
|
};
|
|
exports.arrayToList = arrayToList;
|
|
const escape = (value, stringifyObjects, timezone) => {
|
|
if (value === undefined || value === null)
|
|
return 'NULL';
|
|
switch (typeof value) {
|
|
case 'boolean':
|
|
return value ? 'true' : 'false';
|
|
case 'number':
|
|
case 'bigint':
|
|
return value + '';
|
|
case 'object': {
|
|
if (isDate(value))
|
|
return (0, exports.dateToString)(value, timezone || 'local');
|
|
if (Array.isArray(value))
|
|
return (0, exports.arrayToList)(value, timezone);
|
|
if (node_buffer_1.Buffer.isBuffer(value))
|
|
return (0, exports.bufferToString)(value);
|
|
if (value instanceof Uint8Array)
|
|
return (0, exports.bufferToString)(node_buffer_1.Buffer.from(value));
|
|
if (hasSqlString(value))
|
|
return String(value.toSqlString());
|
|
if (!(stringifyObjects === undefined || stringifyObjects === null))
|
|
return escapeString(String(value));
|
|
if (isRecord(value))
|
|
return (0, exports.objectToValues)(value, timezone);
|
|
return escapeString(String(value));
|
|
}
|
|
case 'string':
|
|
return escapeString(value);
|
|
default:
|
|
return escapeString(String(value));
|
|
}
|
|
};
|
|
exports.escape = escape;
|
|
const format = (sql, values, stringifyObjects, timezone) => {
|
|
if (values === undefined || values === null)
|
|
return sql;
|
|
const valuesArray = Array.isArray(values) ? values : [values];
|
|
const length = valuesArray.length;
|
|
let setIndex = -2; // -2 = not yet computed, -1 = no SET found
|
|
let result = '';
|
|
let chunkIndex = 0;
|
|
let valuesIndex = 0;
|
|
let placeholderPosition = findNextPlaceholder(sql, 0);
|
|
while (valuesIndex < length && placeholderPosition !== -1) {
|
|
// Count consecutive question marks to detect ? vs ?? vs ???+
|
|
let placeholderEnd = placeholderPosition + 1;
|
|
let escapedValue;
|
|
while (sql.charCodeAt(placeholderEnd) === 63)
|
|
placeholderEnd++;
|
|
const placeholderLength = placeholderEnd - placeholderPosition;
|
|
const currentValue = valuesArray[valuesIndex];
|
|
if (placeholderLength > 2) {
|
|
placeholderPosition = findNextPlaceholder(sql, placeholderEnd);
|
|
continue;
|
|
}
|
|
if (placeholderLength === 2)
|
|
escapedValue = (0, exports.escapeId)(currentValue);
|
|
else if (typeof currentValue === 'number')
|
|
escapedValue = `${currentValue}`;
|
|
else if (typeof currentValue === 'object' &&
|
|
currentValue !== null &&
|
|
!stringifyObjects) {
|
|
// Lazy: compute SET position only when we first encounter an object
|
|
if (setIndex === -2)
|
|
setIndex = findSetKeyword(sql);
|
|
if (setIndex !== -1 &&
|
|
setIndex <= placeholderPosition &&
|
|
hasOnlyWhitespaceBetween(sql, setIndex, placeholderPosition) &&
|
|
!hasSqlString(currentValue) &&
|
|
!Array.isArray(currentValue) &&
|
|
!node_buffer_1.Buffer.isBuffer(currentValue) &&
|
|
!(currentValue instanceof Uint8Array) &&
|
|
!isDate(currentValue) &&
|
|
isRecord(currentValue)) {
|
|
escapedValue = (0, exports.objectToValues)(currentValue, timezone);
|
|
setIndex = findSetKeyword(sql, placeholderEnd);
|
|
}
|
|
else
|
|
escapedValue = (0, exports.escape)(currentValue, true, timezone);
|
|
}
|
|
else
|
|
escapedValue = (0, exports.escape)(currentValue, stringifyObjects, timezone);
|
|
result += sql.slice(chunkIndex, placeholderPosition);
|
|
result += escapedValue;
|
|
chunkIndex = placeholderEnd;
|
|
valuesIndex++;
|
|
placeholderPosition = findNextPlaceholder(sql, placeholderEnd);
|
|
}
|
|
if (chunkIndex === 0)
|
|
return sql;
|
|
if (chunkIndex < sql.length)
|
|
return result + sql.slice(chunkIndex);
|
|
return result;
|
|
};
|
|
exports.format = format;
|
|
const raw = (sql) => {
|
|
if (typeof sql !== 'string')
|
|
throw new TypeError('argument sql must be a string');
|
|
return {
|
|
toSqlString: () => sql,
|
|
};
|
|
};
|
|
exports.raw = raw;
|