@cocreate/postgresql 1.0.0 → 1.1.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/CHANGELOG.md +7 -0
- package/package.json +13 -10
- package/src/index.js +70 -42
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,10 @@
|
|
|
1
|
+
# [1.1.0](https://github.com/CoCreate-app/CoCreate-postgresql/compare/v1.0.0...v1.1.0) (2026-07-18)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
### Features
|
|
5
|
+
|
|
6
|
+
* update package.json description and keywords; migrate to ES modules in index.js ([b91abfa](https://github.com/CoCreate-app/CoCreate-postgresql/commit/b91abfaaf64923dda537fa541e43c1fa8ea6e377))
|
|
7
|
+
|
|
1
8
|
# 1.0.0 (2026-07-18)
|
|
2
9
|
|
|
3
10
|
|
package/package.json
CHANGED
|
@@ -1,16 +1,19 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cocreate/postgresql",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "1.1.0",
|
|
4
|
+
"description": "Lightweight PostgreSQL CRUD wrapper and multi-tenant state routing client for the CoCreate ecosystem.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"postgresql",
|
|
7
|
-
"
|
|
8
|
-
"
|
|
9
|
-
"
|
|
10
|
-
"
|
|
11
|
-
"
|
|
12
|
-
"
|
|
13
|
-
"
|
|
7
|
+
"postgres",
|
|
8
|
+
"sql",
|
|
9
|
+
"jsonb",
|
|
10
|
+
"crud-api",
|
|
11
|
+
"abstract-crud",
|
|
12
|
+
"database-abstraction",
|
|
13
|
+
"multi-tenant",
|
|
14
|
+
"multi-tenant-routing",
|
|
15
|
+
"realtime-sync",
|
|
16
|
+
"state-synchronization"
|
|
14
17
|
],
|
|
15
18
|
"publishConfig": {
|
|
16
19
|
"access": "public"
|
|
@@ -38,6 +41,6 @@
|
|
|
38
41
|
"main": "./src/index.js",
|
|
39
42
|
"dependencies": {
|
|
40
43
|
"@cocreate/utils": "^1.44.0",
|
|
41
|
-
"
|
|
44
|
+
"pg": "^8.22.0"
|
|
42
45
|
}
|
|
43
46
|
}
|
package/src/index.js
CHANGED
|
@@ -1,11 +1,29 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
/********************************************************************************
|
|
2
|
+
* Copyright (C) 2026 CoCreate and Contributors.
|
|
3
|
+
*
|
|
4
|
+
* This program is free software: you can redistribute it and/or modify
|
|
5
|
+
* it under the terms of the GNU Affero General Public License as published
|
|
6
|
+
* by the Free Software Foundation, either version 3 of the License, or
|
|
7
|
+
* (at your option) any later version.
|
|
8
|
+
*
|
|
9
|
+
* This program is distributed in the hope that it will be useful,
|
|
10
|
+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
11
|
+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
12
|
+
* GNU Affero General Public License for more details.
|
|
13
|
+
*
|
|
14
|
+
* You should have received a copy of the GNU Affero General Public License
|
|
15
|
+
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
16
|
+
********************************************************************************/
|
|
17
|
+
|
|
18
|
+
import pg from "pg";
|
|
19
|
+
const { Pool } = pg;
|
|
20
|
+
import {
|
|
3
21
|
dotNotationToObject,
|
|
4
22
|
queryData,
|
|
5
23
|
searchData,
|
|
6
24
|
sortData,
|
|
7
25
|
isValidDate
|
|
8
|
-
}
|
|
26
|
+
} from "@cocreate/utils";
|
|
9
27
|
|
|
10
28
|
const organizations = {};
|
|
11
29
|
|
|
@@ -14,8 +32,9 @@ const organizations = {};
|
|
|
14
32
|
*/
|
|
15
33
|
async function dbClient(data) {
|
|
16
34
|
if (data.storageUrl) {
|
|
17
|
-
if (!organizations[data.organization_id])
|
|
35
|
+
if (!organizations[data.organization_id]) {
|
|
18
36
|
organizations[data.organization_id] = {};
|
|
37
|
+
}
|
|
19
38
|
try {
|
|
20
39
|
if (!organizations[data.organization_id][data.storageUrl]) {
|
|
21
40
|
// Establish PostgreSQL Pool with optimized connection properties
|
|
@@ -29,7 +48,7 @@ async function dbClient(data) {
|
|
|
29
48
|
return organizations[data.organization_id][data.storageUrl];
|
|
30
49
|
} catch (error) {
|
|
31
50
|
console.error(
|
|
32
|
-
`${data.organization_id}: storageName ${data.storageName} failed to connect to PostgreSQL`
|
|
51
|
+
`${data.organization_id}: storageName ${data.storageName || "postgresql"} failed to connect to PostgreSQL`
|
|
33
52
|
);
|
|
34
53
|
errorHandler(data, error);
|
|
35
54
|
return { status: false };
|
|
@@ -40,10 +59,41 @@ async function dbClient(data) {
|
|
|
40
59
|
return;
|
|
41
60
|
}
|
|
42
61
|
|
|
62
|
+
/**
|
|
63
|
+
* Global Event Listener: orgDeleted
|
|
64
|
+
* Gracefully terminates active PostgreSQL connection pools.
|
|
65
|
+
*/
|
|
66
|
+
process.on("orgDeleted", (organization_id) => {
|
|
67
|
+
const orgPools = organizations[organization_id];
|
|
68
|
+
if (orgPools) {
|
|
69
|
+
console.log(`[PostgreSQL Utilities] Cleanup Scheduled: Initiating 5-second grace period for Org: ${organization_id}`);
|
|
70
|
+
setTimeout(async () => {
|
|
71
|
+
const currentPools = organizations[organization_id];
|
|
72
|
+
if (!currentPools) return;
|
|
73
|
+
|
|
74
|
+
console.log(`[PostgreSQL Utilities] Cleanup Executing: Closing connection pools for Org: ${organization_id}`);
|
|
75
|
+
const poolList = Object.values(currentPools);
|
|
76
|
+
|
|
77
|
+
for (const pool of poolList) {
|
|
78
|
+
try {
|
|
79
|
+
if (pool && typeof pool.end === "function") {
|
|
80
|
+
await pool.end();
|
|
81
|
+
}
|
|
82
|
+
} catch (err) {
|
|
83
|
+
console.error(`[PostgreSQL Utilities] Error closing connection pool for Org ${organization_id}:`, err);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
delete organizations[organization_id];
|
|
88
|
+
console.log(`[PostgreSQL Utilities] Memory Cleared: Registry freed for Org: ${organization_id}`);
|
|
89
|
+
}, 5000);
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
|
|
43
93
|
/**
|
|
44
94
|
* Universal router matching the exact entry-point signature of the MongoDB wrapper.
|
|
45
95
|
*/
|
|
46
|
-
function send(data) {
|
|
96
|
+
export function send(data) {
|
|
47
97
|
let [type, method] = data.method.split(".");
|
|
48
98
|
if (type === "database") return database(method, data);
|
|
49
99
|
if (type === "array") return array(method, data);
|
|
@@ -61,7 +111,7 @@ function database(method, data) {
|
|
|
61
111
|
|
|
62
112
|
try {
|
|
63
113
|
const pool = await dbClient(data);
|
|
64
|
-
if (!pool || pool.status === false) return data;
|
|
114
|
+
if (!pool || pool.status === false) return resolve(data);
|
|
65
115
|
|
|
66
116
|
if (method === "read") {
|
|
67
117
|
const sql = "SELECT datname FROM pg_database WHERE datistemplate = false;";
|
|
@@ -87,12 +137,12 @@ function database(method, data) {
|
|
|
87
137
|
if (isFilter)
|
|
88
138
|
databaseArray.push({
|
|
89
139
|
database: dbObj,
|
|
90
|
-
storage: data.storageName
|
|
140
|
+
storage: data.storageName || "postgresql"
|
|
91
141
|
});
|
|
92
142
|
} else {
|
|
93
143
|
databaseArray.push({
|
|
94
144
|
database: dbObj,
|
|
95
|
-
storage: data.storageName
|
|
145
|
+
storage: data.storageName || "postgresql"
|
|
96
146
|
});
|
|
97
147
|
}
|
|
98
148
|
}
|
|
@@ -122,9 +172,6 @@ function database(method, data) {
|
|
|
122
172
|
console.log(method, "error", error);
|
|
123
173
|
resolve(data);
|
|
124
174
|
}
|
|
125
|
-
},
|
|
126
|
-
(error) => {
|
|
127
|
-
errorHandler(data, error);
|
|
128
175
|
}
|
|
129
176
|
);
|
|
130
177
|
}
|
|
@@ -140,7 +187,7 @@ function array(method, data) {
|
|
|
140
187
|
|
|
141
188
|
try {
|
|
142
189
|
const pool = await dbClient(data);
|
|
143
|
-
if (!pool || pool.status === false) return data;
|
|
190
|
+
if (!pool || pool.status === false) return resolve(data);
|
|
144
191
|
|
|
145
192
|
if (data.request) data.array = data.request;
|
|
146
193
|
|
|
@@ -178,13 +225,13 @@ function array(method, data) {
|
|
|
178
225
|
arrayArray.push({
|
|
179
226
|
name: row.table_name,
|
|
180
227
|
database,
|
|
181
|
-
storage: data.storageName
|
|
228
|
+
storage: data.storageName || "postgresql"
|
|
182
229
|
});
|
|
183
230
|
} else {
|
|
184
231
|
arrayArray.push({
|
|
185
232
|
name: row.table_name,
|
|
186
233
|
database,
|
|
187
|
-
storage: data.storageName
|
|
234
|
+
storage: data.storageName || "postgresql"
|
|
188
235
|
});
|
|
189
236
|
}
|
|
190
237
|
}
|
|
@@ -222,7 +269,7 @@ function array(method, data) {
|
|
|
222
269
|
arrayArray.push({
|
|
223
270
|
name: array,
|
|
224
271
|
database,
|
|
225
|
-
storage: data.storageName
|
|
272
|
+
storage: data.storageName || "postgresql"
|
|
226
273
|
});
|
|
227
274
|
|
|
228
275
|
arraysLength -= 1;
|
|
@@ -247,7 +294,7 @@ function array(method, data) {
|
|
|
247
294
|
name: newName,
|
|
248
295
|
oldName: oldName,
|
|
249
296
|
database,
|
|
250
|
-
storage: data.storageName
|
|
297
|
+
storage: data.storageName || "postgresql"
|
|
251
298
|
});
|
|
252
299
|
|
|
253
300
|
arraysLength -= 1;
|
|
@@ -271,7 +318,7 @@ function array(method, data) {
|
|
|
271
318
|
arrayArray.push({
|
|
272
319
|
name: array,
|
|
273
320
|
database,
|
|
274
|
-
storage: data.storageName
|
|
321
|
+
storage: data.storageName || "postgresql"
|
|
275
322
|
});
|
|
276
323
|
|
|
277
324
|
arraysLength -= 1;
|
|
@@ -290,9 +337,6 @@ function array(method, data) {
|
|
|
290
337
|
console.log(method, "error", error);
|
|
291
338
|
resolve(data);
|
|
292
339
|
}
|
|
293
|
-
},
|
|
294
|
-
(error) => {
|
|
295
|
-
errorHandler(data, error);
|
|
296
340
|
}
|
|
297
341
|
);
|
|
298
342
|
}
|
|
@@ -305,7 +349,7 @@ function object(method, data) {
|
|
|
305
349
|
async (resolve, reject) => {
|
|
306
350
|
try {
|
|
307
351
|
const pool = await dbClient(data);
|
|
308
|
-
if (!pool || pool.status === false) return data;
|
|
352
|
+
if (!pool || pool.status === false) return resolve(data);
|
|
309
353
|
|
|
310
354
|
let type = "object";
|
|
311
355
|
let documents = [];
|
|
@@ -322,7 +366,7 @@ function object(method, data) {
|
|
|
322
366
|
|
|
323
367
|
for (let array of arrays) {
|
|
324
368
|
const reference = {
|
|
325
|
-
$storage: data.storageName,
|
|
369
|
+
$storage: data.storageName || "postgresql",
|
|
326
370
|
$database: database,
|
|
327
371
|
$array: array
|
|
328
372
|
};
|
|
@@ -347,7 +391,7 @@ function object(method, data) {
|
|
|
347
391
|
if (!Array.isArray($database)) $database = [data[type][i].$database];
|
|
348
392
|
if (!Array.isArray($array)) $array = [data[type][i].$array];
|
|
349
393
|
|
|
350
|
-
if (!$storage.includes(data.storageName)) $storage.push(data.storageName);
|
|
394
|
+
if (!$storage.includes(data.storageName || "postgresql")) $storage.push(data.storageName || "postgresql");
|
|
351
395
|
if (!$database.includes(database)) $database.push(database);
|
|
352
396
|
if (!$array.includes(array)) $array.push(array);
|
|
353
397
|
|
|
@@ -439,7 +483,6 @@ function object(method, data) {
|
|
|
439
483
|
if (isFilter) {
|
|
440
484
|
for (let row of result.rows) {
|
|
441
485
|
const parsedRow = parseSqlRow(row);
|
|
442
|
-
// Perform dynamic text searching if requested
|
|
443
486
|
if (data.$filter && data.$filter.search) {
|
|
444
487
|
let isMatch = searchData(parsedRow, data.$filter.search);
|
|
445
488
|
if (!isMatch) continue;
|
|
@@ -452,18 +495,15 @@ function object(method, data) {
|
|
|
452
495
|
} else if (result.rows.length > 0) {
|
|
453
496
|
let dbRecord = parseSqlRow(result.rows[0]);
|
|
454
497
|
|
|
455
|
-
// Offline synchronization logic
|
|
456
498
|
if ($storage.length && data[type][i].modified && data[type][i].modified.on) {
|
|
457
499
|
let clientTime = new Date(data[type][i].modified.on);
|
|
458
500
|
let dbTime = new Date(dbRecord.modified ? dbRecord.modified.on : 0);
|
|
459
501
|
|
|
460
502
|
if (clientTime > dbTime) {
|
|
461
|
-
// Client modified newer: perform a full synchronization merge and commit write
|
|
462
503
|
const updatedRecord = { ...dbRecord, ...data[type][i] };
|
|
463
504
|
const savedRecord = await executeUpdateTransaction(pool, array, data[type][i]._id, updatedRecord, data);
|
|
464
505
|
data[type][i] = { ...savedRecord, $storage, $database, $array };
|
|
465
506
|
} else {
|
|
466
|
-
// DB has newer or identical stamp: resolve with database state
|
|
467
507
|
data[type][i] = { ...data[type][i], ...dbRecord, $storage, $database, $array };
|
|
468
508
|
}
|
|
469
509
|
} else {
|
|
@@ -489,7 +529,6 @@ function object(method, data) {
|
|
|
489
529
|
data[type][i] = { ...updatedDoc, $storage, $database, $array };
|
|
490
530
|
}
|
|
491
531
|
} else if (isFilter) {
|
|
492
|
-
// Read target row IDs first for consistent processing
|
|
493
532
|
const selectSql = `SELECT _id FROM "${array}" ${sqlWhere}`;
|
|
494
533
|
process.emit("usage", {
|
|
495
534
|
type: "egress",
|
|
@@ -550,9 +589,6 @@ function object(method, data) {
|
|
|
550
589
|
console.log(method, "error", error);
|
|
551
590
|
resolve(data);
|
|
552
591
|
}
|
|
553
|
-
},
|
|
554
|
-
(error) => {
|
|
555
|
-
errorHandler(data, error);
|
|
556
592
|
}
|
|
557
593
|
);
|
|
558
594
|
}
|
|
@@ -565,7 +601,6 @@ async function executeUpdateTransaction(pool, arrayName, docId, rawUpdateInput,
|
|
|
565
601
|
try {
|
|
566
602
|
await client.query("BEGIN;");
|
|
567
603
|
|
|
568
|
-
// Row lock the targeted document preventing race conditions
|
|
569
604
|
const selectSql = `SELECT * FROM "${arrayName}" WHERE _id = $1 AND organization_id = $2 FOR UPDATE;`;
|
|
570
605
|
const selectParams = [docId, requestData.organization_id];
|
|
571
606
|
|
|
@@ -581,16 +616,13 @@ async function executeUpdateTransaction(pool, arrayName, docId, rawUpdateInput,
|
|
|
581
616
|
}
|
|
582
617
|
}
|
|
583
618
|
|
|
584
|
-
// Compute updated structural fields and payload blocks
|
|
585
619
|
const modified = {
|
|
586
620
|
on: new Date(requestData.timeStamp),
|
|
587
621
|
by: requestData.user_id || requestData.clientId
|
|
588
622
|
};
|
|
589
623
|
|
|
590
|
-
// Run deep JavaScript nested update mutator reproducing MongoDB operator behavior
|
|
591
624
|
const updatedPayload = applyMongoUpdate(currentRecord, rawUpdateInput);
|
|
592
625
|
|
|
593
|
-
// Clean out system structures before committing payload to SQL database JSONB field
|
|
594
626
|
const savePayload = { ...updatedPayload };
|
|
595
627
|
delete savePayload._id;
|
|
596
628
|
delete savePayload.organization_id;
|
|
@@ -637,15 +669,13 @@ async function executeUpdateTransaction(pool, arrayName, docId, rawUpdateInput,
|
|
|
637
669
|
function applyMongoUpdate(record, updatePayload) {
|
|
638
670
|
let target = { ...record };
|
|
639
671
|
|
|
640
|
-
// Process set operators
|
|
641
672
|
Object.keys(updatePayload).forEach((rawKey) => {
|
|
642
|
-
if (rawKey.startsWith("$")) return;
|
|
673
|
+
if (rawKey.startsWith("$")) return;
|
|
643
674
|
|
|
644
675
|
let cleanKey = rawKey.replace(/\[(\d+)\]/g, ".$1");
|
|
645
676
|
setNestedValue(target, cleanKey, updatePayload[rawKey]);
|
|
646
677
|
});
|
|
647
678
|
|
|
648
|
-
// Process Mongo Operators: $set, $unset, $push, $pull, $addToSet, $inc
|
|
649
679
|
if (updatePayload.$set) {
|
|
650
680
|
Object.keys(updatePayload.$set).forEach((key) => {
|
|
651
681
|
let cleanKey = key.replace(/\[(\d+)\]/g, ".$1");
|
|
@@ -736,7 +766,6 @@ function compileFilterToSQL(filter, organizationId) {
|
|
|
736
766
|
const parseConditionGroup = (queryObj, isOrJoin = false) => {
|
|
737
767
|
let localClauses = [];
|
|
738
768
|
|
|
739
|
-
// Special Handling for nested boolean operators ($or, $and)
|
|
740
769
|
if (queryObj.$or && Array.isArray(queryObj.$or)) {
|
|
741
770
|
let orClauses = queryObj.$or.map(subQuery => parseConditionGroup(subQuery)).filter(c => c.length);
|
|
742
771
|
if (orClauses.length) {
|
|
@@ -750,7 +779,6 @@ function compileFilterToSQL(filter, organizationId) {
|
|
|
750
779
|
}
|
|
751
780
|
}
|
|
752
781
|
|
|
753
|
-
// Process fields recursively supporting dot path targets
|
|
754
782
|
for (let key in queryObj) {
|
|
755
783
|
if (["$or", "$and"].includes(key)) continue;
|
|
756
784
|
|
|
@@ -890,4 +918,4 @@ function errorHandler(data, error, database, array) {
|
|
|
890
918
|
}
|
|
891
919
|
}
|
|
892
920
|
|
|
893
|
-
|
|
921
|
+
export default { send };
|