@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/.github/FUNDING.yml +3 -0
- package/.github/workflows/automated.yml +56 -0
- package/CHANGELOG.md +6 -0
- package/CONTRIBUTING.md +97 -0
- package/CoCreate.config.js +23 -0
- package/LICENSE +683 -0
- package/README.md +88 -0
- package/demo/index.html +19 -0
- package/docs/index.html +228 -0
- package/package.json +43 -0
- package/prettier.config.js +16 -0
- package/release.config.js +30 -0
- package/src/index.js +893 -0
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 };
|