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