@nka212bg/backend-utils 0.1.44 → 0.1.45

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/db/dbUtils.js CHANGED
@@ -1,17 +1,51 @@
1
- // HELPERS
2
- exports.stripString = (input) => {
1
+ exports.sqlSanitize = (input, { backslashEscapes = false } = {}) => {
3
2
  if (!input) return input;
3
+ input = String(input);
4
4
 
5
- return (
6
- String(input)
7
- // .replace(/([!@#$%^&*,/\\<>]| )+/g, " ")
8
- .replace(/\/\*|\*\/| +/g, " ")
9
- .replace(/(( +$)?(' *)( +$)?)+/g, "'")
10
- .replace(/(( +$)?(" *)( +$)?)+/g, '"')
11
- .replace(/\-+/g, "-")
12
- .replace(/ +/g, " ")
13
- .trim()
14
- );
5
+ let out = "";
6
+ let ended = false;
7
+
8
+ for (let i = 0; i < input.length; i++) {
9
+ const ch = input[i];
10
+ const next = input[i + 1];
11
+ const isComment = (ch == "-" && next == "-") || (ch == "/" && next == "*");
12
+ const isSpace = /\s/.test(ch);
13
+
14
+ if (ended && !isComment && !isSpace && ch != ";") throw "Multiple statements in sql";
15
+
16
+ if (ch == "'" || ch == '"' || ch == "`") {
17
+ let end = i + 1;
18
+ while (true) {
19
+ if (backslashEscapes && input[end] == "\\") {
20
+ end += 2;
21
+ continue;
22
+ }
23
+ if (end >= input.length) throw "Unclosed quote in sql";
24
+ if (input[end] == ch) {
25
+ if (input[end + 1] != ch) break;
26
+ end++; // doubled quote is an escaped quote
27
+ }
28
+ end++;
29
+ }
30
+ out += input.slice(i, end + 1);
31
+ i = end;
32
+ } else if (ch == "-" && next == "-") {
33
+ const end = input.indexOf("\n", i);
34
+ i = end == -1 ? input.length : end - 1;
35
+ } else if (ch == "/" && next == "*") {
36
+ const end = input.indexOf("*/", i + 2);
37
+ i = end == -1 ? input.length : end + 1;
38
+ if (!out.endsWith(" ")) out += " ";
39
+ } else if (ch == ";") {
40
+ ended = true;
41
+ } else if (isSpace) {
42
+ if (!out.endsWith(" ")) out += " ";
43
+ } else {
44
+ out += ch;
45
+ }
46
+ }
47
+
48
+ return out.trim();
15
49
  };
16
50
 
17
51
  exports.paramsSanitize = (params) => {
@@ -19,6 +53,11 @@ exports.paramsSanitize = (params) => {
19
53
  if (!Array.isArray(params)) params = [params];
20
54
 
21
55
  return params.map((param) => {
56
+ // blobs: Buffer, Uint8Array and other typed arrays, ArrayBuffer
57
+ if (Buffer.isBuffer(param)) return param;
58
+ if (ArrayBuffer.isView(param)) return Buffer.from(param.buffer, param.byteOffset, param.byteLength);
59
+ if (param instanceof ArrayBuffer) return Buffer.from(param);
60
+
22
61
  if (typeof param == "object" && param !== null) return JSON.stringify(param);
23
62
  if (typeof param == "boolean") return param ? 1 : 0;
24
63
  if (typeof param == "number") return param;
package/db/mysql2.js CHANGED
@@ -1,4 +1,4 @@
1
- const { paramsSanitize, stripString } = require("./dbUtils");
1
+ const { paramsSanitize } = require("./dbUtils");
2
2
  const { createPool } = require("mysql2");
3
3
 
4
4
  const pool = createPool({
@@ -16,7 +16,7 @@ exports.mysql = (sql, params) => {
16
16
  // (SELECT JSON_ARRAYAGG(JSON_OBJECT('CountryId', CountryID)) FROM country) AS Countries // JSON_ARRAYAGG and JSON_OBJECT demos
17
17
  return new Promise((resolve) => {
18
18
  try {
19
- sql = stripString(sql);
19
+ sql = String(sql ?? "").trim();
20
20
  if (!sql) throw "Missing sql";
21
21
 
22
22
  if (params?.length) {
package/db/sqlite3.js CHANGED
@@ -1,7 +1,8 @@
1
- const { paramsSanitize, stripString } = require("./dbUtils");
1
+ const { paramsSanitize } = require("./dbUtils");
2
2
  const sqlite3 = require("sqlite3").verbose();
3
3
 
4
4
  const dbs = {};
5
+ const opening = {};
5
6
  let vacuumed = [];
6
7
  let day = new Date().getDate();
7
8
 
@@ -26,9 +27,10 @@ setInterval(() => {
26
27
  exports.sqlite = (dbName, sql, params = []) => {
27
28
  return new Promise(async (resolve) => {
28
29
  try {
29
- sql = stripString(sql);
30
+ sql = String(sql ?? "").trim();
30
31
  if (!sql) throw "Missing sql";
31
- const isWrite = /^(INSERT|DELETE|UPDATE)/i.test(sql);
32
+
33
+ const isWrite = /^(INSERT|DELETE|UPDATE|REPLACE)/i.test(sql);
32
34
  const queryPlaceholdersLength = (sql.match(/\?/g) || []).length; // [?]
33
35
 
34
36
  if (queryPlaceholdersLength < params.length) {
@@ -37,57 +39,65 @@ exports.sqlite = (dbName, sql, params = []) => {
37
39
  params = params.concat(Array(queryPlaceholdersLength - params.length).fill(params[params.length - 1]));
38
40
  }
39
41
 
40
- let dbError;
41
42
  if (!dbs[dbName]) {
42
- let db;
43
- await new Promise(async (resolve) => {
44
- db = new sqlite3.Database(dbName, (error) => {
45
- dbError = error;
43
+ // concurrent callers wait for the same opening, so only one connection is created
44
+ opening[dbName] ||= new Promise((resolve, reject) => {
45
+ const db = new sqlite3.Database(dbName, (error) => {
46
+ delete opening[dbName];
47
+ if (error) return reject(error);
48
+
49
+ // some db settings here
50
+ db.run("pragma synchronous = normal");
51
+
52
+ if (dbName.endsWith("/main.db")) {
53
+ db.run("pragma temp_store = memory").run("pragma mmap_size = 3000000000");
54
+ }
55
+
56
+ dbs[dbName] = db;
46
57
  resolve();
47
58
  });
48
59
  });
49
60
 
50
- if (dbError) throw dbError;
51
-
52
- db.time = Date.now();
53
- dbs[dbName] = db;
61
+ await opening[dbName];
62
+ }
54
63
 
55
- // some db settings here
56
- dbs[dbName].run("pragma synchronous = normal");
64
+ // keep a local reference, the interval may delete dbs[dbName] before the callback runs
65
+ const db = dbs[dbName];
66
+ db.time = Date.now(); // last use, the interval closes it after 5 min idle
57
67
 
58
- if (dbName.endsWith("/main.db")) {
59
- dbs[dbName].run("pragma temp_store = memory").run("pragma mmap_size = 3000000000");
68
+ db[isWrite ? "run" : "all"](sql, paramsSanitize(params), function (error, res) {
69
+ if (error) {
70
+ printError(error);
71
+ return resolve({ error });
60
72
  }
61
- }
62
-
63
- dbs[dbName][isWrite ? "run" : "all"](sql, paramsSanitize(params), function (error, res) {
64
- if (error) return resolve({ error });
65
73
 
66
74
  !res?.length && (res = undefined);
67
75
  const changes = this.changes;
68
76
 
69
- if (/^INSERT/i.test(sql)) {
70
- const targetDB = /INTO\s(.*?) /i.exec(sql)[1];
77
+ if (/^(INSERT|REPLACE)/i.test(sql)) {
78
+ const targetDB = /INTO\s+([^\s(]+)/i.exec(sql)?.[1];
71
79
  const insertId = this.lastID;
72
- return dbs[dbName].all(`SELECT name FROM pragma_table_info(?) WHERE pk = 1 LIMIT 1`, [targetDB], function (error, insertData) {
73
- insertData = insertData?.[0] ? { [insertData[0].name]: insertId } : undefined;
80
+ if (!targetDB) return resolve({ error, data: undefined, changes });
74
81
 
75
- resolve({ error, data: insertData, changes });
82
+ // primary key name per table, cached on the connection so the pragma runs once per table
83
+ db.pk ||= {};
84
+ if (targetDB in db.pk) return resolve({ error, data: db.pk[targetDB] ? { [db.pk[targetDB]]: insertId } : undefined, changes });
85
+
86
+ return db.all(`SELECT name FROM pragma_table_info(?) WHERE pk = 1 LIMIT 1`, [targetDB], function (error, insertData) {
87
+ const pk = insertData?.[0]?.name;
88
+ if (!error) db.pk[targetDB] = pk;
89
+
90
+ resolve({ error, data: pk ? { [pk]: insertId } : undefined, changes });
76
91
  printError(error);
77
92
  });
78
93
  } else {
79
94
  resolve({ error, data: res, changes });
80
95
  printError(error);
81
96
 
82
- if (/^DELETE/i.test(sql)) dbs[dbName].vacuum = true;
97
+ if (/^DELETE/i.test(sql) && changes) db.vacuum = true;
83
98
  }
84
99
  });
85
100
  } catch (error) {
86
- try {
87
- dbs[dbName].close();
88
- } catch {}
89
-
90
- delete dbs[dbName];
91
101
  resolve({ error, data: undefined });
92
102
  printError(error);
93
103
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@nka212bg/backend-utils",
3
3
  "author": "nka212bg",
4
- "version": "0.1.44",
4
+ "version": "0.1.45",
5
5
  "main": "index.js",
6
6
  "dependencies": {
7
7
  "@maxmind/geoip2-node": "^5.0.0",