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