@cocreate/sqlite 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.
package/src/index.js ADDED
@@ -0,0 +1,925 @@
1
+ const sqlite3 = require("sqlite3");
2
+ const {
3
+ dotNotationToObject,
4
+ queryData,
5
+ searchData,
6
+ sortData,
7
+ isValidDate
8
+ } = require("@cocreate/utils");
9
+
10
+ const organizations = {};
11
+
12
+ /**
13
+ * Promisified database helpers to clean up sqlite3's callback-based API.
14
+ */
15
+ function runQuery(db, sql, params = []) {
16
+ return new Promise((resolve, reject) => {
17
+ db.run(sql, params, function (err) {
18
+ if (err) reject(err);
19
+ else resolve({ lastID: this.lastID, changes: this.changes });
20
+ });
21
+ });
22
+ }
23
+
24
+ function allQuery(db, sql, params = []) {
25
+ return new Promise((resolve, reject) => {
26
+ db.all(sql, params, (err, rows) => {
27
+ if (err) reject(err);
28
+ else resolve(rows);
29
+ });
30
+ });
31
+ }
32
+
33
+ function getQuery(db, sql, params = []) {
34
+ return new Promise((resolve, reject) => {
35
+ db.get(sql, params, (err, row) => {
36
+ if (err) reject(err);
37
+ else resolve(row);
38
+ });
39
+ });
40
+ }
41
+
42
+ /**
43
+ * Dynamically resolves or caches a long-lived SQLite Connection handle.
44
+ */
45
+ async function dbClient(data) {
46
+ if (data.storageUrl) {
47
+ if (!organizations[data.organization_id]) {
48
+ organizations[data.organization_id] = {};
49
+ }
50
+ try {
51
+ if (!organizations[data.organization_id][data.storageUrl]) {
52
+ // Establish sqlite3 handle. Path can be file based or in-memory ':memory:'
53
+ const dbPath = data.storageUrl.replace("sqlite://", "");
54
+
55
+ const connectionPromise = new Promise((resolve, reject) => {
56
+ const client = new sqlite3.Database(dbPath, (err) => {
57
+ if (err) reject(err);
58
+ else resolve(client);
59
+ });
60
+ });
61
+
62
+ organizations[data.organization_id][data.storageUrl] = connectionPromise;
63
+ }
64
+
65
+ // Await the cached connection promise resolution
66
+ organizations[data.organization_id][data.storageUrl] = await organizations[data.organization_id][data.storageUrl];
67
+ return organizations[data.organization_id][data.storageUrl];
68
+ } catch (error) {
69
+ console.error(
70
+ `${data.organization_id}: storageUrl ${data.storageUrl} failed to connect to SQLite`
71
+ );
72
+ errorHandler(data, error);
73
+ return { status: false };
74
+ }
75
+ }
76
+
77
+ errorHandler(data, "missing StorageUrl");
78
+ return;
79
+ }
80
+
81
+ /**
82
+ * Global Event Listener: orgDeleted
83
+ * Gracefully terminates active SQLite connections and releases lock pools.
84
+ */
85
+ process.on("orgDeleted", (organization_id) => {
86
+ const orgPools = organizations[organization_id];
87
+ if (orgPools) {
88
+ console.log(`[SQLite Utilities] Cleanup Scheduled: Initiating 5-second grace period for Org: ${organization_id}`);
89
+ setTimeout(async () => {
90
+ const currentPools = organizations[organization_id];
91
+ if (!currentPools) return;
92
+
93
+ console.log(`[SQLite Utilities] Cleanup Executing: Closing connection handles for Org: ${organization_id}`);
94
+ const dbList = Object.values(currentPools);
95
+
96
+ for (const db of dbList) {
97
+ try {
98
+ if (db && typeof db.close === "function") {
99
+ await new Promise((resolve, reject) => {
100
+ db.close((err) => {
101
+ if (err) reject(err);
102
+ else resolve();
103
+ });
104
+ });
105
+ }
106
+ } catch (err) {
107
+ console.error(`[SQLite Utilities] Error closing connection for Org ${organization_id}:`, err);
108
+ }
109
+ }
110
+
111
+ delete organizations[organization_id];
112
+ console.log(`[SQLite Utilities] Memory Cleared: Registry freed for Org: ${organization_id}`);
113
+ }, 5000);
114
+ }
115
+ });
116
+
117
+ /**
118
+ * Universal router routing request methods safely.
119
+ */
120
+ function send(data) {
121
+ let [type, method] = data.method.split(".");
122
+ if (type === "database") return database(method, data);
123
+ if (type === "array") return array(method, data);
124
+ if (type === "object") return object(method, data);
125
+ }
126
+
127
+ /**
128
+ * "database" operations mapped to SQLite Files (emulated catalogs).
129
+ */
130
+ function database(method, data) {
131
+ return new Promise(
132
+ async (resolve, reject) => {
133
+ let type = "database";
134
+ let databaseArray = [];
135
+
136
+ try {
137
+ const db = await dbClient(data);
138
+ if (!db || db.status === false) return data;
139
+
140
+ if (method === "read") {
141
+ // Emulating catalog query targeting active database context
142
+ const sql = "PRAGMA database_list;";
143
+ process.emit("usage", {
144
+ type: "egress",
145
+ data: { sql },
146
+ organization_id: data.organization_id
147
+ });
148
+
149
+ const res = await allQuery(db, sql);
150
+ process.emit("usage", {
151
+ type: "ingress",
152
+ data: res,
153
+ organization_id: data.organization_id
154
+ });
155
+
156
+ for (let row of res) {
157
+ const dbObj = { name: row.file ? row.file.split("/").pop() : "memory" };
158
+ if (data.$filter && data.$filter.query) {
159
+ let isFilter = queryData(dbObj, data.$filter.query);
160
+ if (isFilter) {
161
+ databaseArray.push({
162
+ database: dbObj,
163
+ storage: data.storageName
164
+ });
165
+ }
166
+ } else {
167
+ databaseArray.push({
168
+ database: dbObj,
169
+ storage: data.storageName
170
+ });
171
+ }
172
+ }
173
+ resolve(createData(data, databaseArray, type));
174
+ }
175
+ if (method === "delete") {
176
+ // SQLite databases are typically filesystem files. We empty schema inside in-memory/file context safely
177
+ const tables = await allQuery(db, "SELECT name FROM sqlite_master WHERE type='table';");
178
+ for (let t of tables) {
179
+ if (!t.name.startsWith("sqlite_")) {
180
+ const sql = `DROP TABLE IF EXISTS "${t.name}";`;
181
+ process.emit("usage", {
182
+ type: "egress",
183
+ data: { sql },
184
+ organization_id: data.organization_id
185
+ });
186
+ await runQuery(db, sql);
187
+ }
188
+ }
189
+ resolve({ status: true });
190
+ }
191
+ } catch (error) {
192
+ errorHandler(data, error);
193
+ console.log(method, "error", error);
194
+ resolve(data);
195
+ }
196
+ },
197
+ (error) => {
198
+ errorHandler(data, error);
199
+ }
200
+ );
201
+ }
202
+
203
+ /**
204
+ * "array" operations mapped to SQL Tables.
205
+ */
206
+ function array(method, data) {
207
+ return new Promise(
208
+ async (resolve, reject) => {
209
+ let type = "array";
210
+ let arrayArray = [];
211
+
212
+ try {
213
+ const db = await dbClient(data);
214
+ if (!db || db.status === false) return data;
215
+
216
+ if (data.request) data.array = data.request;
217
+
218
+ let databases = data.database;
219
+ if (!Array.isArray(databases)) databases = [databases];
220
+
221
+ let databasesLength = databases.length;
222
+ for (let database of databases) {
223
+ if (method === "read") {
224
+ const sql = "SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%';";
225
+ process.emit("usage", {
226
+ type: "egress",
227
+ data: { sql },
228
+ organization_id: data.organization_id
229
+ });
230
+
231
+ const res = await allQuery(db, sql);
232
+ process.emit("usage", {
233
+ type: "ingress",
234
+ data: res,
235
+ organization_id: data.organization_id
236
+ });
237
+
238
+ for (let row of res) {
239
+ const tableObj = { name: row.name };
240
+ if (data.$filter && data.$filter.query) {
241
+ let isFilter = queryData(tableObj, data.$filter.query);
242
+ if (isFilter) {
243
+ arrayArray.push({
244
+ name: row.name,
245
+ database,
246
+ storage: data.storageName
247
+ });
248
+ }
249
+ } else {
250
+ arrayArray.push({
251
+ name: row.name,
252
+ database,
253
+ storage: data.storageName
254
+ });
255
+ }
256
+ }
257
+
258
+ databasesLength -= 1;
259
+ if (!databasesLength) {
260
+ data = createData(data, arrayArray, type);
261
+ resolve(data);
262
+ }
263
+ } else {
264
+ let arrays = data.array;
265
+ if (method === "update") arrays = Object.entries(data.array);
266
+ if (!Array.isArray(arrays)) arrays = [arrays];
267
+
268
+ let arraysLength = arrays.length;
269
+ for (let array of arrays) {
270
+ if (method === "create") {
271
+ const sql = `
272
+ CREATE TABLE IF NOT EXISTS "${array}" (
273
+ _id TEXT PRIMARY KEY,
274
+ organization_id TEXT,
275
+ data TEXT DEFAULT '{}',
276
+ created TEXT,
277
+ modified TEXT
278
+ );
279
+ `;
280
+ process.emit("usage", {
281
+ type: "egress",
282
+ data: { sql },
283
+ organization_id: data.organization_id
284
+ });
285
+
286
+ await runQuery(db, sql);
287
+ arrayArray.push({
288
+ name: array,
289
+ database,
290
+ storage: data.storageName
291
+ });
292
+
293
+ arraysLength -= 1;
294
+ if (!arraysLength) databasesLength -= 1;
295
+ if (!databasesLength && !arraysLength) {
296
+ data = createData(data, arrayArray, type);
297
+ resolve(data);
298
+ }
299
+ } else {
300
+ if (method === "update") {
301
+ let [oldName, newName] = array;
302
+ const sql = `ALTER TABLE "${oldName}" RENAME TO "${newName}";`;
303
+ process.emit("usage", {
304
+ type: "egress",
305
+ data: { sql },
306
+ organization_id: data.organization_id
307
+ });
308
+
309
+ await runQuery(db, sql);
310
+ arrayArray.push({
311
+ name: newName,
312
+ oldName: oldName,
313
+ database,
314
+ storage: data.storageName
315
+ });
316
+
317
+ arraysLength -= 1;
318
+ if (!arraysLength) databasesLength -= 1;
319
+ if (!databasesLength && !arraysLength) {
320
+ data = createData(data, arrayArray, type);
321
+ resolve(data);
322
+ }
323
+ }
324
+
325
+ if (method === "delete") {
326
+ const sql = `DROP TABLE IF EXISTS "${array}";`;
327
+ process.emit("usage", {
328
+ type: "egress",
329
+ data: { sql },
330
+ organization_id: data.organization_id
331
+ });
332
+
333
+ await runQuery(db, sql);
334
+ arrayArray.push({
335
+ name: array,
336
+ database,
337
+ storage: data.storageName
338
+ });
339
+
340
+ arraysLength -= 1;
341
+ if (!arraysLength) databasesLength -= 1;
342
+ if (!databasesLength && !arraysLength) {
343
+ data = createData(data, arrayArray, type);
344
+ resolve(data);
345
+ }
346
+ }
347
+ }
348
+ }
349
+ }
350
+ }
351
+ } catch (error) {
352
+ errorHandler(data, error);
353
+ console.log(method, "error", error);
354
+ resolve(data);
355
+ }
356
+ },
357
+ (error) => {
358
+ errorHandler(data, error);
359
+ }
360
+ );
361
+ }
362
+
363
+ /**
364
+ * "object" operations mapped to SQL Rows.
365
+ */
366
+ function object(method, data) {
367
+ return new Promise(
368
+ async (resolve, reject) => {
369
+ try {
370
+ const db = await dbClient(data);
371
+ if (!db || db.status === false) return data;
372
+
373
+ let type = "object";
374
+ let documents = [];
375
+
376
+ if (!data["timeStamp"]) {
377
+ data["timeStamp"] = new Date().toISOString();
378
+ }
379
+
380
+ let databases = data.database;
381
+ if (!Array.isArray(databases)) databases = [databases];
382
+
383
+ for (let database of databases) {
384
+ let arrays = data.array;
385
+ if (!Array.isArray(arrays)) arrays = [arrays];
386
+
387
+ for (let array of arrays) {
388
+ const reference = {
389
+ $storage: data.storageName,
390
+ $database: database,
391
+ $array: array
392
+ };
393
+
394
+ if (!data[type]) data[type] = [];
395
+ else if (typeof data[type] === "string")
396
+ data[type] = [{ _id: data[type] }];
397
+ else if (!Array.isArray(data[type]))
398
+ data[type] = [data[type]];
399
+
400
+ let isFilter = !!data.$filter;
401
+ if ((isFilter && !data[type].length) || data.isFilter) {
402
+ data[type].splice(0, 0, { isFilter: "isEmptyObjectFilter" });
403
+ }
404
+
405
+ for (let i = 0; i < data[type].length; i++) {
406
+ let $storage = data[type][i].$storage || [];
407
+ let $database = data[type][i].$database || [];
408
+ let $array = data[type][i].$array || [];
409
+
410
+ if (!Array.isArray($storage)) $storage = [data[type][i].$storage];
411
+ if (!Array.isArray($database)) $database = [data[type][i].$database];
412
+ if (!Array.isArray($array)) $array = [data[type][i].$array];
413
+
414
+ if (!$storage.includes(data.storageName)) $storage.push(data.storageName);
415
+ if (!$database.includes(database)) $database.push(database);
416
+ if (!$array.includes(array)) $array.push(array);
417
+
418
+ delete data[type][i].$storage;
419
+ delete data[type][i].$database;
420
+ delete data[type][i].$array;
421
+
422
+ let queryFilter = isFilter ? data.$filter : (data[type][i].$filter || null);
423
+ let { sqlWhere, params } = compileFilterToSQL(queryFilter, data.organization_id);
424
+
425
+ if (method === "create") {
426
+ data[type][i] = replaceArray(data[type][i]);
427
+ data[type][i] = dotNotationToObject(data[type][i]);
428
+
429
+ const id = data[type][i]._id || `row_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
430
+ const orgId = data.organization_id;
431
+ const created = {
432
+ on: new Date(data.timeStamp),
433
+ by: data.user_id || data.clientId
434
+ };
435
+
436
+ const recordPayload = { ...data[type][i] };
437
+ delete recordPayload._id;
438
+ delete recordPayload.organization_id;
439
+
440
+ const sql = `
441
+ INSERT INTO "${array}" (_id, organization_id, data, created, modified)
442
+ VALUES (?, ?, ?, ?, NULL);
443
+ `;
444
+ const queryParams = [id, orgId, JSON.stringify(recordPayload), JSON.stringify(created)];
445
+
446
+ process.emit("usage", {
447
+ type: "egress",
448
+ data: { sql, queryParams },
449
+ organization_id: data.organization_id
450
+ });
451
+
452
+ await runQuery(db, sql, queryParams);
453
+
454
+ const createdDoc = await getQuery(db, `SELECT * FROM "${array}" WHERE _id = ? AND organization_id = ?`, [id, orgId]);
455
+ if (createdDoc) {
456
+ const formattedDoc = parseSqlRow(createdDoc);
457
+ data[type][i] = { ...formattedDoc, $storage, $database, $array };
458
+ }
459
+ }
460
+ else if (method === "read") {
461
+ let sql = `SELECT * FROM "${array}" ${sqlWhere}`;
462
+
463
+ if (data.$filter && data.$filter.sort) {
464
+ const sortClauses = data.$filter.sort.map(s => {
465
+ let dir = s.direction === -1 || s.direction === "desc" ? "DESC" : "ASC";
466
+ if (["_id", "organization_id"].includes(s.key)) {
467
+ return `"${s.key}" ${dir}`;
468
+ }
469
+ return `json_extract(data, '$.${s.key}') ${dir}`;
470
+ });
471
+ sql += ` ORDER BY ${sortClauses.join(", ")}`;
472
+ }
473
+ if (data.$filter && typeof data.$filter.limit === "number") {
474
+ sql += ` LIMIT ${data.$filter.limit}`;
475
+ }
476
+ if (data.$filter && typeof data.$filter.index === "number") {
477
+ sql += ` OFFSET ${data.$filter.index}`;
478
+ }
479
+
480
+ if (data[type][i]._id && !isFilter) {
481
+ sql = `SELECT * FROM "${array}" WHERE _id = ? AND organization_id = ?`;
482
+ params = [data[type][i]._id, data.organization_id];
483
+ }
484
+
485
+ process.emit("usage", {
486
+ type: "egress",
487
+ data: { sql, params },
488
+ organization_id: data.organization_id
489
+ });
490
+
491
+ const rows = await allQuery(db, sql, params);
492
+ process.emit("usage", {
493
+ type: "ingress",
494
+ data: rows,
495
+ organization_id: data.organization_id
496
+ });
497
+
498
+ if (isFilter) {
499
+ for (let row of rows) {
500
+ const parsedRow = parseSqlRow(row);
501
+ if (data.$filter && data.$filter.search) {
502
+ let isMatch = searchData(parsedRow, data.$filter.search);
503
+ if (!isMatch) continue;
504
+ }
505
+ documents.push({
506
+ ...parsedRow,
507
+ ...reference
508
+ });
509
+ }
510
+ } else if (rows.length > 0) {
511
+ let dbRecord = parseSqlRow(rows[0]);
512
+
513
+ if ($storage.length && data[type][i].modified && data[type][i].modified.on) {
514
+ let clientTime = new Date(data[type][i].modified.on);
515
+ let dbTime = new Date(dbRecord.modified ? dbRecord.modified.on : 0);
516
+
517
+ if (clientTime > dbTime) {
518
+ const updatedRecord = { ...dbRecord, ...data[type][i] };
519
+ const savedRecord = await executeUpdateTransaction(db, array, data[type][i]._id, updatedRecord, data);
520
+ data[type][i] = { ...savedRecord, $storage, $database, $array };
521
+ } else {
522
+ data[type][i] = { ...data[type][i], ...dbRecord, $storage, $database, $array };
523
+ }
524
+ } else {
525
+ data[type][i] = {
526
+ ...data[type][i],
527
+ ...dbRecord,
528
+ $storage, $database, $array
529
+ };
530
+ }
531
+ } else {
532
+ data[type].splice(i, 1);
533
+ i--;
534
+ }
535
+ }
536
+ else if (method === "update") {
537
+ const updatePayload = { ...data[type][i] };
538
+ delete updatePayload._id;
539
+ delete updatePayload.$filter;
540
+
541
+ if (data[type][i]._id) {
542
+ const updatedDoc = await executeUpdateTransaction(db, array, data[type][i]._id, updatePayload, data);
543
+ if (updatedDoc) {
544
+ data[type][i] = { ...updatedDoc, $storage, $database, $array };
545
+ }
546
+ } else if (isFilter) {
547
+ const selectSql = `SELECT _id FROM "${array}" ${sqlWhere}`;
548
+ process.emit("usage", {
549
+ type: "egress",
550
+ data: { selectSql, params },
551
+ organization_id: data.organization_id
552
+ });
553
+ const selectRows = await allQuery(db, selectSql, params);
554
+
555
+ for (let row of selectRows) {
556
+ const updatedDoc = await executeUpdateTransaction(db, array, row._id, updatePayload, data);
557
+ if (updatedDoc) {
558
+ documents.push({ ...updatedDoc, ...reference });
559
+ }
560
+ }
561
+ }
562
+ }
563
+ else if (method === "delete") {
564
+ let sql, queryParams;
565
+ if (data[type][i]._id) {
566
+ sql = `DELETE FROM "${array}" WHERE _id = ? AND organization_id = ?;`;
567
+ queryParams = [data[type][i]._id, data.organization_id];
568
+ } else if (isFilter) {
569
+ sql = `DELETE FROM "${array}" ${sqlWhere};`;
570
+ queryParams = params;
571
+ }
572
+
573
+ if (sql) {
574
+ process.emit("usage", {
575
+ type: "egress",
576
+ data: { sql, queryParams },
577
+ organization_id: data.organization_id
578
+ });
579
+ const runResult = await runQuery(db, sql, queryParams);
580
+ process.emit("usage", {
581
+ type: "ingress",
582
+ data: runResult,
583
+ organization_id: data.organization_id
584
+ });
585
+
586
+ if (isFilter) {
587
+ // Return deleted references
588
+ documents.push({ _id: data[type][i]._id, ...reference });
589
+ } else {
590
+ data[type][i] = { _id: data[type][i]._id, $storage, $database, $array };
591
+ }
592
+ }
593
+ }
594
+ }
595
+ }
596
+ }
597
+
598
+ data = createData(data, documents, type);
599
+ resolve(data);
600
+ } catch (error) {
601
+ errorHandler(data, error);
602
+ console.log(method, "error", error);
603
+ resolve(data);
604
+ }
605
+ },
606
+ (error) => {
607
+ errorHandler(data, error);
608
+ }
609
+ );
610
+ }
611
+
612
+ /**
613
+ * Safe transaction helper to update unstructured payloads with MongoDB behavior inside SQLite.
614
+ */
615
+ async function executeUpdateTransaction(db, arrayName, docId, rawUpdateInput, requestData) {
616
+ try {
617
+ // SQLite is single-write bound, using IMMEDIATE locks database cleanly
618
+ await runQuery(db, "BEGIN IMMEDIATE TRANSACTION;");
619
+
620
+ const selectSql = `SELECT * FROM "${arrayName}" WHERE _id = ? AND organization_id = ?;`;
621
+ const currentRecordRaw = await getQuery(db, selectSql, [docId, requestData.organization_id]);
622
+ let currentRecord = currentRecordRaw ? parseSqlRow(currentRecordRaw) : null;
623
+
624
+ if (!currentRecord) {
625
+ if (requestData.upsert || rawUpdateInput.upsert || rawUpdateInput.$upsert) {
626
+ currentRecord = { _id: docId, organization_id: requestData.organization_id };
627
+ } else {
628
+ await runQuery(db, "ROLLBACK;");
629
+ return null;
630
+ }
631
+ }
632
+
633
+ const modified = {
634
+ on: new Date(requestData.timeStamp),
635
+ by: requestData.user_id || requestData.clientId
636
+ };
637
+
638
+ const updatedPayload = applyMongoUpdate(currentRecord, rawUpdateInput);
639
+
640
+ const savePayload = { ...updatedPayload };
641
+ delete savePayload._id;
642
+ delete savePayload.organization_id;
643
+ delete savePayload.created;
644
+ delete savePayload.modified;
645
+
646
+ const updateSql = `
647
+ UPDATE "${arrayName}"
648
+ SET data = ?,
649
+ modified = ?
650
+ WHERE _id = ? AND organization_id = ?;
651
+ `;
652
+ const updateParams = [JSON.stringify(savePayload), JSON.stringify(modified), docId, requestData.organization_id];
653
+
654
+ process.emit("usage", {
655
+ type: "egress",
656
+ data: { sql: updateSql, updateParams },
657
+ organization_id: requestData.organization_id
658
+ });
659
+
660
+ await runQuery(db, updateSql, updateParams);
661
+ await runQuery(db, "COMMIT;");
662
+
663
+ const finalRecordRaw = await getQuery(db, selectSql, [docId, requestData.organization_id]);
664
+ return parseSqlRow(finalRecordRaw);
665
+ } catch (error) {
666
+ await runQuery(db, "ROLLBACK;");
667
+ throw error;
668
+ }
669
+ }
670
+
671
+ /**
672
+ * Applies MongoDB update operators recursively to maintain unified schema manipulation behaviors.
673
+ */
674
+ function applyMongoUpdate(record, updatePayload) {
675
+ let target = { ...record };
676
+
677
+ Object.keys(updatePayload).forEach((rawKey) => {
678
+ if (rawKey.startsWith("$")) return;
679
+ let cleanKey = rawKey.replace(/\[(\d+)\]/g, ".$1");
680
+ setNestedValue(target, cleanKey, updatePayload[rawKey]);
681
+ });
682
+
683
+ if (updatePayload.$set) {
684
+ Object.keys(updatePayload.$set).forEach((key) => {
685
+ let cleanKey = key.replace(/\[(\d+)\]/g, ".$1");
686
+ setNestedValue(target, cleanKey, updatePayload.$set[key]);
687
+ });
688
+ }
689
+
690
+ if (updatePayload.$unset) {
691
+ Object.keys(updatePayload.$unset).forEach((key) => {
692
+ let cleanKey = key.replace(/\[(\d+)\]/g, ".$1");
693
+ deleteNestedValue(target, cleanKey);
694
+ });
695
+ }
696
+
697
+ if (updatePayload.$inc) {
698
+ Object.keys(updatePayload.$inc).forEach((key) => {
699
+ let cleanKey = key.replace(/\[(\d+)\]/g, ".$1");
700
+ let currentVal = getNestedValue(target, cleanKey) || 0;
701
+ setNestedValue(target, cleanKey, Number(currentVal) + Number(updatePayload.$inc[key]));
702
+ });
703
+ }
704
+
705
+ if (updatePayload.$push) {
706
+ Object.keys(updatePayload.$push).forEach((key) => {
707
+ let cleanKey = key.replace(/\[(\d+)\]/g, ".$1");
708
+ let arr = getNestedValue(target, cleanKey);
709
+ if (!Array.isArray(arr)) arr = [];
710
+
711
+ const pushValue = updatePayload.$push[key];
712
+ if (pushValue && pushValue.$each) {
713
+ let valuesToPush = pushValue.$each;
714
+ let position = typeof pushValue.$position === "number" ? pushValue.$position : arr.length;
715
+ arr.splice(position, 0, ...valuesToPush);
716
+ } else {
717
+ arr.push(pushValue);
718
+ }
719
+ setNestedValue(target, cleanKey, arr);
720
+ });
721
+ }
722
+
723
+ if (updatePayload.$pull) {
724
+ Object.keys(updatePayload.$pull).forEach((key) => {
725
+ let cleanKey = key.replace(/\[(\d+)\]/g, ".$1");
726
+ let arr = getNestedValue(target, cleanKey);
727
+ if (Array.isArray(arr)) {
728
+ const filterVal = updatePayload.$pull[key];
729
+ arr = arr.filter(item => {
730
+ if (typeof filterVal === "object" && filterVal !== null) {
731
+ return !queryData(item, filterVal);
732
+ }
733
+ return item !== filterVal;
734
+ });
735
+ setNestedValue(target, cleanKey, arr);
736
+ }
737
+ });
738
+ }
739
+
740
+ if (updatePayload.$addToSet) {
741
+ Object.keys(updatePayload.$addToSet).forEach((key) => {
742
+ let cleanKey = key.replace(/\[(\d+)\]/g, ".$1");
743
+ let arr = getNestedValue(target, cleanKey);
744
+ if (!Array.isArray(arr)) arr = [];
745
+
746
+ const addValue = updatePayload.$addToSet[key];
747
+ if (addValue && addValue.$each) {
748
+ addValue.$each.forEach(item => {
749
+ if (!arr.includes(item)) arr.push(item);
750
+ });
751
+ } else {
752
+ if (!arr.includes(addValue)) arr.push(addValue);
753
+ }
754
+ setNestedValue(target, cleanKey, arr);
755
+ });
756
+ }
757
+
758
+ return target;
759
+ }
760
+
761
+ /**
762
+ * Translates standard MongoDB JSON filters into native SQLite query filters.
763
+ */
764
+ function compileFilterToSQL(filter, organizationId) {
765
+ let clauses = ["organization_id = ?"];
766
+ let params = [organizationId];
767
+
768
+ if (filter && filter.query) {
769
+ const parseConditionGroup = (queryObj, isOrJoin = false) => {
770
+ let localClauses = [];
771
+
772
+ if (queryObj.$or && Array.isArray(queryObj.$or)) {
773
+ let orClauses = queryObj.$or.map(subQuery => parseConditionGroup(subQuery)).filter(c => c.length);
774
+ if (orClauses.length) {
775
+ localClauses.push("(" + orClauses.join(" OR ") + ")");
776
+ }
777
+ }
778
+ if (queryObj.$and && Array.isArray(queryObj.$and)) {
779
+ let andClauses = queryObj.$and.map(subQuery => parseConditionGroup(subQuery)).filter(c => c.length);
780
+ if (andClauses.length) {
781
+ localClauses.push("(" + andClauses.join(" AND ") + ")");
782
+ }
783
+ }
784
+
785
+ for (let key in queryObj) {
786
+ if (["$or", "$and"].includes(key)) continue;
787
+
788
+ let value = queryObj[key];
789
+ const targetField = ["_id", "organization_id"].includes(key)
790
+ ? `"${key}"`
791
+ : `json_extract(data, '$.${key}')`;
792
+
793
+ if (value && typeof value === "object" && !Array.isArray(value)) {
794
+ for (let op in value) {
795
+ let operand = value[op];
796
+ params.push(operand);
797
+
798
+ if (op === "$gt") localClauses.push(`${targetField} > ?`);
799
+ else if (op === "$gte") localClauses.push(`${targetField} >= ?`);
800
+ else if (op === "$lt") localClauses.push(`${targetField} < ?`);
801
+ else if (op === "$lte") localClauses.push(`${targetField} <= ?`);
802
+ else if (op === "$ne") localClauses.push(`${targetField} != ?`);
803
+ else if (op === "$in") {
804
+ params.pop(); // Remove single item
805
+ const placeholders = operand.map(() => "?").join(", ");
806
+ operand.forEach(o => params.push(o));
807
+ localClauses.push(`${targetField} IN (${placeholders})`);
808
+ }
809
+ }
810
+ } else {
811
+ params.push(value);
812
+ localClauses.push(`${targetField} = ?`);
813
+ }
814
+ }
815
+
816
+ return localClauses.join(isOrJoin ? " OR " : " AND ");
817
+ };
818
+
819
+ const processedQuery = parseConditionGroup(filter.query);
820
+ if (processedQuery) {
821
+ clauses.push(processedQuery);
822
+ }
823
+ }
824
+
825
+ return {
826
+ sqlWhere: clauses.length > 0 ? "WHERE " + clauses.join(" AND ") : "",
827
+ params
828
+ };
829
+ }
830
+
831
+ /**
832
+ * Resolves SQLite columns and rows back into unified JSON format.
833
+ */
834
+ function parseSqlRow(row) {
835
+ if (!row) return null;
836
+ const recordData = typeof row.data === "string" ? JSON.parse(row.data) : (row.data || {});
837
+ return {
838
+ _id: row._id,
839
+ organization_id: row.organization_id,
840
+ created: typeof row.created === "string" ? JSON.parse(row.created) : row.created,
841
+ modified: typeof row.modified === "string" ? JSON.parse(row.modified) : row.modified,
842
+ ...recordData
843
+ };
844
+ }
845
+
846
+ function replaceArray(data = {}) {
847
+ let object = {};
848
+ Object.keys(data).forEach((key) => {
849
+ object[key.replace(/\[(\d+)\]/g, ".$1")] = data[key];
850
+ });
851
+ return object;
852
+ }
853
+
854
+ function getNestedValue(obj, path) {
855
+ return path.split(".").reduce((acc, part) => acc && acc[part], obj);
856
+ }
857
+
858
+ function setNestedValue(obj, path, value) {
859
+ const parts = path.split(".");
860
+ const last = parts.pop();
861
+ const target = parts.reduce((acc, part) => acc[part] = acc[part] || {}, obj);
862
+ target[last] = value;
863
+ }
864
+
865
+ function deleteNestedValue(obj, path) {
866
+ const parts = path.split(".");
867
+ const last = parts.pop();
868
+ const target = parts.reduce((acc, part) => acc && acc[part], obj);
869
+ if (target) delete target[last];
870
+ }
871
+
872
+ /**
873
+ * Syncs the output response data arrays back cleanly.
874
+ */
875
+ function createData(data, array, type) {
876
+ if (data[type] && data[type][0] && data[type][0].isFilter === "isEmptyObjectFilter") {
877
+ data[type].shift();
878
+ data.isFilter = true;
879
+ }
880
+
881
+ let key = type !== "object" ? "name" : "_id";
882
+
883
+ if (!Array.isArray(data[type])) {
884
+ console.log("data[type] is not an array", type);
885
+ } else {
886
+ for (let i = 0; i < array.length; i++) {
887
+ const matchIndex = data[type].findIndex((item) => item[key] === array[i][key]);
888
+ if (matchIndex !== -1) {
889
+ for (let $type of ["$storage", "$database", "$array"]) {
890
+ if (!data[type][matchIndex][$type]) data[type][matchIndex][$type] = [];
891
+ if (!Array.isArray(data[type][matchIndex][$type])) {
892
+ data[type][matchIndex][$type] = [data[type][matchIndex][$type]];
893
+ }
894
+ if (!data[type][matchIndex][$type].includes(array[i][$type])) {
895
+ data[type][matchIndex][$type].push(array[i][$type]);
896
+ }
897
+ delete array[i][$type];
898
+ }
899
+ data[type][matchIndex] = { ...data[type][matchIndex], ...array[i] };
900
+ } else {
901
+ data[type].push(array[i]);
902
+ }
903
+ }
904
+ }
905
+ return data;
906
+ }
907
+
908
+ function errorHandler(data, error, database, array) {
909
+ let errorMessage = typeof error === "object" && error.message ? error.message : error;
910
+ let errorObject = {
911
+ message: errorMessage,
912
+ storage: "sqlite"
913
+ };
914
+
915
+ if (database) errorObject.database = database;
916
+ if (array) errorObject.array = array;
917
+
918
+ if (Array.isArray(data.error)) {
919
+ data.error.push(errorObject);
920
+ } else {
921
+ data.error = [errorObject];
922
+ }
923
+ }
924
+
925
+ module.exports = { send };