index.js 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398
  1. "use strict";
  2. /**
  3. * Adapted from https://github.com/mysqljs/sqlstring/blob/cd528556b4b6bcf300c3db515026935dedf7cfa1/lib/SqlString.js
  4. * MIT LICENSE: https://github.com/mysqljs/sqlstring/blob/cd528556b4b6bcf300c3db515026935dedf7cfa1/LICENSE
  5. */
  6. Object.defineProperty(exports, "__esModule", { value: true });
  7. exports.raw = exports.format = exports.escape = exports.arrayToList = exports.bufferToString = exports.objectToValues = exports.escapeId = exports.dateToString = void 0;
  8. const node_buffer_1 = require("node:buffer");
  9. const regex = {
  10. backtick: /`/g,
  11. dot: /\./g,
  12. timezone: /([+\-\s])(\d\d):?(\d\d)?/,
  13. escapeChars: /[\0\b\t\n\r\x1a"'\\]/g,
  14. };
  15. const CHARS_ESCAPE_MAP = {
  16. '\0': '\\0',
  17. '\b': '\\b',
  18. '\t': '\\t',
  19. '\n': '\\n',
  20. '\r': '\\r',
  21. '\x1a': '\\Z',
  22. '"': '\\"',
  23. "'": "\\'",
  24. '\\': '\\\\',
  25. };
  26. const charCode = {
  27. singleQuote: 39,
  28. backtick: 96,
  29. backslash: 92,
  30. dash: 45,
  31. slash: 47,
  32. asterisk: 42,
  33. questionMark: 63,
  34. newline: 10,
  35. space: 32,
  36. tab: 9,
  37. carriageReturn: 13,
  38. };
  39. const isRecord = (value) => typeof value === 'object' && value !== null && !Array.isArray(value);
  40. const isWordChar = (code) => (code >= 65 && code <= 90) ||
  41. (code >= 97 && code <= 122) ||
  42. (code >= 48 && code <= 57) ||
  43. code === 95;
  44. const isWhitespace = (code) => code === charCode.space ||
  45. code === charCode.tab ||
  46. code === charCode.newline ||
  47. code === charCode.carriageReturn;
  48. const hasOnlyWhitespaceBetween = (sql, start, end) => {
  49. if (start >= end)
  50. return true;
  51. for (let i = start; i < end; i++) {
  52. const code = sql.charCodeAt(i);
  53. if (code !== charCode.space &&
  54. code !== charCode.tab &&
  55. code !== charCode.newline &&
  56. code !== charCode.carriageReturn)
  57. return false;
  58. }
  59. return true;
  60. };
  61. const toLower = (code) => code | 32;
  62. const matchesWord = (sql, position, word, length) => {
  63. for (let offset = 0; offset < word.length; offset++)
  64. if (toLower(sql.charCodeAt(position + offset)) !== word.charCodeAt(offset))
  65. return false;
  66. return ((position === 0 || !isWordChar(sql.charCodeAt(position - 1))) &&
  67. (position + word.length >= length ||
  68. !isWordChar(sql.charCodeAt(position + word.length))));
  69. };
  70. const skipSqlContext = (sql, position) => {
  71. const currentChar = sql.charCodeAt(position);
  72. const nextChar = sql.charCodeAt(position + 1);
  73. if (currentChar === charCode.singleQuote) {
  74. for (let cursor = position + 1; cursor < sql.length; cursor++) {
  75. if (sql.charCodeAt(cursor) === charCode.backslash)
  76. cursor++;
  77. else if (sql.charCodeAt(cursor) === charCode.singleQuote)
  78. return cursor + 1;
  79. }
  80. return sql.length;
  81. }
  82. if (currentChar === charCode.backtick) {
  83. const length = sql.length;
  84. for (let cursor = position + 1; cursor < length; cursor++) {
  85. if (sql.charCodeAt(cursor) !== charCode.backtick)
  86. continue;
  87. if (sql.charCodeAt(cursor + 1) === charCode.backtick) {
  88. cursor++;
  89. continue;
  90. }
  91. return cursor + 1;
  92. }
  93. return length;
  94. }
  95. if (currentChar === charCode.dash && nextChar === charCode.dash) {
  96. const lineBreak = sql.indexOf('\n', position + 2);
  97. return lineBreak === -1 ? sql.length : lineBreak + 1;
  98. }
  99. if (currentChar === charCode.slash && nextChar === charCode.asterisk) {
  100. const commentEnd = sql.indexOf('*/', position + 2);
  101. return commentEnd === -1 ? sql.length : commentEnd + 2;
  102. }
  103. return -1;
  104. };
  105. const findNextPlaceholder = (sql, start) => {
  106. const sqlLength = sql.length;
  107. for (let position = start; position < sqlLength; position++) {
  108. const code = sql.charCodeAt(position);
  109. if (code === charCode.questionMark)
  110. return position;
  111. if (code === charCode.singleQuote ||
  112. code === charCode.backtick ||
  113. code === charCode.dash ||
  114. code === charCode.slash) {
  115. const contextEnd = skipSqlContext(sql, position);
  116. if (contextEnd !== -1)
  117. position = contextEnd - 1;
  118. }
  119. }
  120. return -1;
  121. };
  122. const findSetKeyword = (sql, startFrom = 0) => {
  123. const length = sql.length;
  124. for (let position = startFrom; position < length; position++) {
  125. const code = sql.charCodeAt(position);
  126. const lower = code | 32;
  127. if (code === charCode.singleQuote ||
  128. code === charCode.backtick ||
  129. code === charCode.dash ||
  130. code === charCode.slash) {
  131. const contextEnd = skipSqlContext(sql, position);
  132. if (contextEnd !== -1) {
  133. position = contextEnd - 1;
  134. continue;
  135. }
  136. }
  137. if (lower === 115 && matchesWord(sql, position, 'set', length))
  138. return position + 3;
  139. if (lower === 107 && matchesWord(sql, position, 'key', length)) {
  140. let cursor = position + 3;
  141. while (cursor < length && isWhitespace(sql.charCodeAt(cursor)))
  142. cursor++;
  143. if (matchesWord(sql, cursor, 'update', length))
  144. return cursor + 6;
  145. }
  146. }
  147. return -1;
  148. };
  149. const isDate = (value) => Object.prototype.toString.call(value) === '[object Date]';
  150. const hasSqlString = (value) => typeof value === 'object' &&
  151. value !== null &&
  152. 'toSqlString' in value &&
  153. typeof value.toSqlString === 'function';
  154. const escapeString = (value) => {
  155. regex.escapeChars.lastIndex = 0;
  156. let chunkIndex = 0;
  157. let escapedValue = '';
  158. let match;
  159. for (match = regex.escapeChars.exec(value); match !== null; match = regex.escapeChars.exec(value)) {
  160. escapedValue += value.slice(chunkIndex, match.index);
  161. escapedValue += CHARS_ESCAPE_MAP[match[0]];
  162. chunkIndex = regex.escapeChars.lastIndex;
  163. }
  164. if (chunkIndex === 0)
  165. return `'${value}'`;
  166. if (chunkIndex < value.length)
  167. return `'${escapedValue}${value.slice(chunkIndex)}'`;
  168. return `'${escapedValue}'`;
  169. };
  170. const pad2 = (value) => (value < 10 ? '0' + value : '' + value);
  171. const pad3 = (value) => value < 10 ? '00' + value : value < 100 ? '0' + value : '' + value;
  172. const pad4 = (value) => value < 10
  173. ? '000' + value
  174. : value < 100
  175. ? '00' + value
  176. : value < 1000
  177. ? '0' + value
  178. : '' + value;
  179. const convertTimezone = (tz) => {
  180. if (tz === 'Z')
  181. return 0;
  182. const timezoneMatch = tz.match(regex.timezone);
  183. if (timezoneMatch)
  184. return ((timezoneMatch[1] === '-' ? -1 : 1) *
  185. (Number.parseInt(timezoneMatch[2], 10) +
  186. (timezoneMatch[3] ? Number.parseInt(timezoneMatch[3], 10) : 0) / 60) *
  187. 60);
  188. return false;
  189. };
  190. const dateToString = (date, timezone) => {
  191. if (Number.isNaN(date.getTime()))
  192. return 'NULL';
  193. let year;
  194. let month;
  195. let day;
  196. let hour;
  197. let minute;
  198. let second;
  199. let millisecond;
  200. if (timezone === 'local') {
  201. year = date.getFullYear();
  202. month = date.getMonth() + 1;
  203. day = date.getDate();
  204. hour = date.getHours();
  205. minute = date.getMinutes();
  206. second = date.getSeconds();
  207. millisecond = date.getMilliseconds();
  208. }
  209. else {
  210. const timezoneOffsetMinutes = convertTimezone(timezone);
  211. let time = date.getTime();
  212. if (timezoneOffsetMinutes !== false && timezoneOffsetMinutes !== 0)
  213. time += timezoneOffsetMinutes * 60000;
  214. const adjustedDate = new Date(time);
  215. year = adjustedDate.getUTCFullYear();
  216. month = adjustedDate.getUTCMonth() + 1;
  217. day = adjustedDate.getUTCDate();
  218. hour = adjustedDate.getUTCHours();
  219. minute = adjustedDate.getUTCMinutes();
  220. second = adjustedDate.getUTCSeconds();
  221. millisecond = adjustedDate.getUTCMilliseconds();
  222. }
  223. // YYYY-MM-DD HH:mm:ss.mmm
  224. return escapeString(pad4(year) +
  225. '-' +
  226. pad2(month) +
  227. '-' +
  228. pad2(day) +
  229. ' ' +
  230. pad2(hour) +
  231. ':' +
  232. pad2(minute) +
  233. ':' +
  234. pad2(second) +
  235. '.' +
  236. pad3(millisecond));
  237. };
  238. exports.dateToString = dateToString;
  239. const escapeId = (value, forbidQualified) => {
  240. if (Array.isArray(value)) {
  241. const length = value.length;
  242. const parts = new Array(length);
  243. for (let i = 0; i < length; i++)
  244. parts[i] = (0, exports.escapeId)(value[i], forbidQualified);
  245. return parts.join(', ');
  246. }
  247. const identifier = String(value);
  248. const hasJsonOperator = identifier.indexOf('->') !== -1;
  249. if (forbidQualified || hasJsonOperator) {
  250. if (identifier.indexOf('`') === -1)
  251. return `\`${identifier}\``;
  252. return `\`${identifier.replace(regex.backtick, '``')}\``;
  253. }
  254. if (identifier.indexOf('`') === -1 && identifier.indexOf('.') === -1)
  255. return `\`${identifier}\``;
  256. return `\`${identifier
  257. .replace(regex.backtick, '``')
  258. .replace(regex.dot, '`.`')}\``;
  259. };
  260. exports.escapeId = escapeId;
  261. const objectToValues = (object, timezone) => {
  262. const keys = Object.keys(object);
  263. const keysLength = keys.length;
  264. if (keysLength === 0)
  265. return '';
  266. let sql = '';
  267. for (let i = 0; i < keysLength; i++) {
  268. const key = keys[i];
  269. const value = object[key];
  270. if (typeof value === 'function')
  271. continue;
  272. if (sql.length > 0)
  273. sql += ', ';
  274. sql += (0, exports.escapeId)(key);
  275. sql += ' = ';
  276. sql += (0, exports.escape)(value, true, timezone);
  277. }
  278. return sql;
  279. };
  280. exports.objectToValues = objectToValues;
  281. const bufferToString = (buffer) => `X${escapeString(buffer.toString('hex'))}`;
  282. exports.bufferToString = bufferToString;
  283. const arrayToList = (array, timezone) => {
  284. const length = array.length;
  285. const parts = new Array(length);
  286. for (let i = 0; i < length; i++) {
  287. const value = array[i];
  288. if (Array.isArray(value))
  289. parts[i] = `(${(0, exports.arrayToList)(value, timezone)})`;
  290. else
  291. parts[i] = (0, exports.escape)(value, true, timezone);
  292. }
  293. return parts.join(', ');
  294. };
  295. exports.arrayToList = arrayToList;
  296. const escape = (value, stringifyObjects, timezone) => {
  297. if (value === undefined || value === null)
  298. return 'NULL';
  299. switch (typeof value) {
  300. case 'boolean':
  301. return value ? 'true' : 'false';
  302. case 'number':
  303. case 'bigint':
  304. return value + '';
  305. case 'object': {
  306. if (isDate(value))
  307. return (0, exports.dateToString)(value, timezone || 'local');
  308. if (Array.isArray(value))
  309. return (0, exports.arrayToList)(value, timezone);
  310. if (node_buffer_1.Buffer.isBuffer(value))
  311. return (0, exports.bufferToString)(value);
  312. if (value instanceof Uint8Array)
  313. return (0, exports.bufferToString)(node_buffer_1.Buffer.from(value));
  314. if (hasSqlString(value))
  315. return String(value.toSqlString());
  316. if (!(stringifyObjects === undefined || stringifyObjects === null))
  317. return escapeString(String(value));
  318. if (isRecord(value))
  319. return (0, exports.objectToValues)(value, timezone);
  320. return escapeString(String(value));
  321. }
  322. case 'string':
  323. return escapeString(value);
  324. default:
  325. return escapeString(String(value));
  326. }
  327. };
  328. exports.escape = escape;
  329. const format = (sql, values, stringifyObjects, timezone) => {
  330. if (values === undefined || values === null)
  331. return sql;
  332. const valuesArray = Array.isArray(values) ? values : [values];
  333. const length = valuesArray.length;
  334. let setIndex = -2; // -2 = not yet computed, -1 = no SET found
  335. let result = '';
  336. let chunkIndex = 0;
  337. let valuesIndex = 0;
  338. let placeholderPosition = findNextPlaceholder(sql, 0);
  339. while (valuesIndex < length && placeholderPosition !== -1) {
  340. // Count consecutive question marks to detect ? vs ?? vs ???+
  341. let placeholderEnd = placeholderPosition + 1;
  342. let escapedValue;
  343. while (sql.charCodeAt(placeholderEnd) === 63)
  344. placeholderEnd++;
  345. const placeholderLength = placeholderEnd - placeholderPosition;
  346. const currentValue = valuesArray[valuesIndex];
  347. if (placeholderLength > 2) {
  348. placeholderPosition = findNextPlaceholder(sql, placeholderEnd);
  349. continue;
  350. }
  351. if (placeholderLength === 2)
  352. escapedValue = (0, exports.escapeId)(currentValue);
  353. else if (typeof currentValue === 'number')
  354. escapedValue = `${currentValue}`;
  355. else if (typeof currentValue === 'object' &&
  356. currentValue !== null &&
  357. !stringifyObjects) {
  358. // Lazy: compute SET position only when we first encounter an object
  359. if (setIndex === -2)
  360. setIndex = findSetKeyword(sql);
  361. if (setIndex !== -1 &&
  362. setIndex <= placeholderPosition &&
  363. hasOnlyWhitespaceBetween(sql, setIndex, placeholderPosition) &&
  364. !hasSqlString(currentValue) &&
  365. !Array.isArray(currentValue) &&
  366. !node_buffer_1.Buffer.isBuffer(currentValue) &&
  367. !(currentValue instanceof Uint8Array) &&
  368. !isDate(currentValue) &&
  369. isRecord(currentValue)) {
  370. escapedValue = (0, exports.objectToValues)(currentValue, timezone);
  371. setIndex = findSetKeyword(sql, placeholderEnd);
  372. }
  373. else
  374. escapedValue = (0, exports.escape)(currentValue, true, timezone);
  375. }
  376. else
  377. escapedValue = (0, exports.escape)(currentValue, stringifyObjects, timezone);
  378. result += sql.slice(chunkIndex, placeholderPosition);
  379. result += escapedValue;
  380. chunkIndex = placeholderEnd;
  381. valuesIndex++;
  382. placeholderPosition = findNextPlaceholder(sql, placeholderEnd);
  383. }
  384. if (chunkIndex === 0)
  385. return sql;
  386. if (chunkIndex < sql.length)
  387. return result + sql.slice(chunkIndex);
  388. return result;
  389. };
  390. exports.format = format;
  391. const raw = (sql) => {
  392. if (typeof sql !== 'string')
  393. throw new TypeError('argument sql must be a string');
  394. return {
  395. toSqlString: () => sql,
  396. };
  397. };
  398. exports.raw = raw;