db-model-router 1.0.0

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.
@@ -0,0 +1,346 @@
1
+ const Database = require("better-sqlite3");
2
+ const { jsonSafeParse } = require("../commons/function");
3
+
4
+ let db = null;
5
+ const WHERE_INVALID = "Invalid filter object";
6
+
7
+ function connect(config) {
8
+ const dbPath = config.database || config.filename || ":memory:";
9
+ const options = {};
10
+ if (config.readonly) options.readonly = true;
11
+ if (config.fileMustExist) options.fileMustExist = true;
12
+ if (config.verbose) options.verbose = config.verbose;
13
+ db = new Database(dbPath, options);
14
+ db.pragma("journal_mode = WAL");
15
+ return db;
16
+ }
17
+
18
+ function query(sql, parameter = []) {
19
+ const stmt = db.prepare(sql);
20
+ if (sql.trimStart().match(/^(SELECT|PRAGMA|WITH\s)/i)) {
21
+ return stmt.all(...parameter);
22
+ }
23
+ return stmt.run(...parameter);
24
+ }
25
+
26
+ function sort_builder(sort) {
27
+ if (!sort || sort.length < 1) {
28
+ return { query: "", value: [] };
29
+ }
30
+ let query_items = [];
31
+ let value = [];
32
+ for (const item of sort) {
33
+ if (item[0] === "-") {
34
+ query_items.push("?? DESC");
35
+ value.push(item.replace("-", ""));
36
+ } else {
37
+ query_items.push("?? ASC");
38
+ value.push(item);
39
+ }
40
+ }
41
+ return {
42
+ query: "ORDER BY " + query_items.join(","),
43
+ value,
44
+ };
45
+ }
46
+
47
+ function where(filter, safeDelete = null) {
48
+ if (filter !== null && filter !== "" && !Array.isArray(filter)) {
49
+ return null;
50
+ }
51
+ try {
52
+ if (
53
+ filter === null ||
54
+ filter === "" ||
55
+ filter.length === 0 ||
56
+ (Array.isArray(filter[0]) && filter[0].length === 0) ||
57
+ (Array.isArray(filter[0]) &&
58
+ Array.isArray(filter[0][0]) &&
59
+ filter[0][0].length === 0)
60
+ ) {
61
+ if (safeDelete === null) {
62
+ return { query: "", value: [] };
63
+ } else {
64
+ filter = [[]];
65
+ }
66
+ }
67
+ } catch (err) {
68
+ return null;
69
+ }
70
+
71
+ if (safeDelete !== null) {
72
+ for (const filterItem of filter) {
73
+ filterItem.push([safeDelete, "=", 0]);
74
+ }
75
+ }
76
+
77
+ const valid_conditionals = [
78
+ "=",
79
+ "like",
80
+ "not like",
81
+ "in",
82
+ "not in",
83
+ "<",
84
+ ">",
85
+ "<=",
86
+ ">=",
87
+ "!=",
88
+ ];
89
+ let conditionOr = [];
90
+ let value = [];
91
+
92
+ for (const i of filter) {
93
+ let conditionAnd = [];
94
+ for (const j of i) {
95
+ if (!valid_conditionals.includes(j[1])) {
96
+ return null;
97
+ }
98
+ if ((j[1] === "in" || j[1] === "not in") && !Array.isArray(j[2])) {
99
+ return null;
100
+ }
101
+ if (j[1] === "in" || j[1] === "not in") {
102
+ conditionAnd.push(
103
+ escapeId(j[0]) + " " + j[1] + " (" + arrayParam(j[2].length) + ")",
104
+ );
105
+ value.push(...j[2]);
106
+ } else if (j[1] === "like" || j[1] === "not like") {
107
+ conditionAnd.push(escapeId(j[0]) + " " + j[1] + " ?");
108
+ value.push("%" + j[2] + "%");
109
+ } else {
110
+ conditionAnd.push(escapeId(j[0]) + " " + j[1] + " ?");
111
+ value.push(j[2]);
112
+ }
113
+ }
114
+ conditionOr.push(conditionAnd.join(" AND "));
115
+ }
116
+
117
+ return {
118
+ query: "WHERE ((" + conditionOr.join(") OR (") + "))",
119
+ value,
120
+ };
121
+ }
122
+
123
+ function get(table, filter = [], sort = [], safeDelete = null) {
124
+ const whereData = where(filter, safeDelete);
125
+ const sortData = sort_builder(sort);
126
+ if (whereData == null) {
127
+ throw new Error(WHERE_INVALID);
128
+ }
129
+
130
+ const sortQuery = resolveSortIdentifiers(sortData);
131
+ const statement = `SELECT * FROM ${escapeId(table)} ${whereData.query} ${sortQuery}`;
132
+ const rows = db.prepare(statement).all(...whereData.value);
133
+ const count = qcount(table, filter, safeDelete);
134
+ return { data: jsonSafeParse(rows), count };
135
+ }
136
+
137
+ function list(
138
+ table,
139
+ filter = [],
140
+ sort = [],
141
+ safeDelete = null,
142
+ page = 0,
143
+ limit = 30,
144
+ ) {
145
+ const whereData = where(filter, safeDelete);
146
+ const sortData = sort_builder(sort);
147
+ if (whereData == null) {
148
+ throw new Error(WHERE_INVALID);
149
+ }
150
+
151
+ const sortQuery = resolveSortIdentifiers(sortData);
152
+ const offset = page * limit;
153
+ const statement = `SELECT * FROM ${escapeId(table)} ${whereData.query} ${sortQuery} LIMIT ? OFFSET ?`;
154
+ const rows = db.prepare(statement).all(...whereData.value, limit, offset);
155
+ const count = qcount(table, filter, safeDelete);
156
+ return { data: jsonSafeParse(rows), count };
157
+ }
158
+
159
+ function qcount(table, filter, safeDelete = null) {
160
+ const whereData = where(filter, safeDelete);
161
+ if (whereData == null) {
162
+ return 0;
163
+ }
164
+ const statement = `SELECT count(*) AS number FROM ${escapeId(table)} ${whereData.query}`;
165
+ try {
166
+ const result = db.prepare(statement).get(...whereData.value);
167
+ return result ? result.number : 0;
168
+ } catch (err) {
169
+ return 0;
170
+ }
171
+ }
172
+
173
+ function remove(table, filter, safeDelete = null) {
174
+ const whereData = where(filter);
175
+ if (whereData == null) {
176
+ throw new Error(WHERE_INVALID);
177
+ }
178
+ if (whereData.value.length < 1) {
179
+ throw new Error("unable to remove as there are no filter attributes");
180
+ }
181
+
182
+ let statement;
183
+ let params;
184
+ if (safeDelete != null) {
185
+ statement = `UPDATE ${escapeId(table)} SET ${escapeId(safeDelete)} = 1 ${whereData.query}`;
186
+ params = whereData.value;
187
+ } else {
188
+ statement = `DELETE FROM ${escapeId(table)} ${whereData.query}`;
189
+ params = whereData.value;
190
+ }
191
+
192
+ const result = db.prepare(statement).run(...params);
193
+ const rows = result.changes || 0;
194
+ return {
195
+ message: rows + " " + table + (rows > 1 ? "s" : "") + " removed",
196
+ };
197
+ }
198
+
199
+ function upsert(table, data, uniqueKeys = []) {
200
+ let array = Array.isArray(data) ? [...data] : [data];
201
+ const total = array.length;
202
+ const columns = Object.keys(array[0]);
203
+
204
+ if (!uniqueKeys || uniqueKeys.length === 0) {
205
+ // No unique keys — just do a plain insert
206
+ return insert(table, data, uniqueKeys);
207
+ }
208
+
209
+ const colList = columns.map(escapeId).join(",");
210
+ const placeholders = columns.map(() => "?").join(",");
211
+ const nonUniqueColumns = columns.filter((c) => !uniqueKeys.includes(c));
212
+ const updateSet = nonUniqueColumns
213
+ .map((c) => `${escapeId(c)} = excluded.${escapeId(c)}`)
214
+ .join(",");
215
+ const conflictCols = uniqueKeys.map(escapeId).join(",");
216
+
217
+ let sql;
218
+ if (updateSet) {
219
+ sql = `INSERT INTO ${escapeId(table)} (${colList}) VALUES (${placeholders}) ON CONFLICT(${conflictCols}) DO UPDATE SET ${updateSet}`;
220
+ } else {
221
+ sql = `INSERT INTO ${escapeId(table)} (${colList}) VALUES (${placeholders}) ON CONFLICT(${conflictCols}) DO NOTHING`;
222
+ }
223
+
224
+ const stmt = db.prepare(sql);
225
+ let lastId = 0;
226
+
227
+ const runAll = db.transaction(() => {
228
+ for (const row of array) {
229
+ const vals = columns.map((c) => row[c]);
230
+ const result = stmt.run(...vals);
231
+ if (result.lastInsertRowid) {
232
+ lastId = Number(result.lastInsertRowid);
233
+ }
234
+ }
235
+ });
236
+ runAll();
237
+
238
+ const response = {
239
+ rows: total,
240
+ message:
241
+ (total === 1
242
+ ? `1 ${namify(table)} is `
243
+ : `${total} ${namify(table)}s are `) + "saved",
244
+ type: "success",
245
+ };
246
+ if (total === 1) {
247
+ response.id = lastId;
248
+ }
249
+ return response;
250
+ }
251
+
252
+ function insert(table, data, uniqueKeys = []) {
253
+ let array = Array.isArray(data) ? [...data] : [data];
254
+ const total = array.length;
255
+ const columns = Object.keys(array[0]);
256
+
257
+ const colList = columns.map(escapeId).join(",");
258
+ const placeholders = columns.map(() => "?").join(",");
259
+
260
+ let sql;
261
+ if (uniqueKeys && uniqueKeys.length > 0) {
262
+ const conflictCols = uniqueKeys.map(escapeId).join(",");
263
+ sql = `INSERT OR IGNORE INTO ${escapeId(table)} (${colList}) VALUES (${placeholders})`;
264
+ } else {
265
+ sql = `INSERT INTO ${escapeId(table)} (${colList}) VALUES (${placeholders})`;
266
+ }
267
+
268
+ const stmt = db.prepare(sql);
269
+ let lastId = 0;
270
+
271
+ const runAll = db.transaction(() => {
272
+ for (const row of array) {
273
+ const vals = columns.map((c) => row[c]);
274
+ const result = stmt.run(...vals);
275
+ if (result.lastInsertRowid) {
276
+ lastId = Number(result.lastInsertRowid);
277
+ }
278
+ }
279
+ });
280
+ runAll();
281
+
282
+ const response = {
283
+ rows: total,
284
+ message:
285
+ (total === 1
286
+ ? `1 ${namify(table)} is `
287
+ : `${total} ${namify(table)}s are `) + "saved",
288
+ type: "success",
289
+ };
290
+ if (total === 1) {
291
+ response.id = lastId;
292
+ }
293
+ return response;
294
+ }
295
+
296
+ function disconnect() {
297
+ if (db) {
298
+ db.close();
299
+ db = null;
300
+ }
301
+ }
302
+
303
+ // --- Utility helpers ---
304
+
305
+ function escapeId(name) {
306
+ // Quote identifier to avoid reserved-word conflicts
307
+ return '"' + name.replace(/"/g, '""') + '"';
308
+ }
309
+
310
+ function arrayParam(number) {
311
+ let str = "";
312
+ for (let i = 0; i < number; i++) {
313
+ str += i === 0 ? "?" : ",?";
314
+ }
315
+ return str;
316
+ }
317
+
318
+ function resolveSortIdentifiers(sortData) {
319
+ if (!sortData.query) return "";
320
+ let q = sortData.query;
321
+ for (const v of sortData.value) {
322
+ q = q.replace("??", escapeId(v));
323
+ }
324
+ return q;
325
+ }
326
+
327
+ function namify(text) {
328
+ return text
329
+ .replace("_", " ")
330
+ .replace(/(^\w{1})|(\s+\w{1})/g, (letter) => letter.toUpperCase());
331
+ }
332
+
333
+ module.exports = {
334
+ connect,
335
+ get,
336
+ list,
337
+ where,
338
+ query,
339
+ qcount,
340
+ remove,
341
+ upsert,
342
+ change: upsert,
343
+ insert,
344
+ disconnect,
345
+ close: disconnect,
346
+ };