@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/.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 +910 -0
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 };
|