@cocreate/dynamodb 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,889 @@
1
+ const { DynamoDBClient } = require("@aws-sdk/client-dynamodb");
2
+ const {
3
+ DynamoDBDocumentClient,
4
+ ScanCommand,
5
+ GetCommand,
6
+ PutCommand,
7
+ UpdateCommand,
8
+ DeleteCommand,
9
+ TransactWriteCommand
10
+ } = require("@aws-sdk/lib-dynamodb");
11
+ const { CreateTableCommand, DeleteTableCommand, ListTablesCommand } = require("@aws-sdk/client-dynamodb");
12
+ const {
13
+ dotNotationToObject,
14
+ queryData,
15
+ searchData,
16
+ sortData,
17
+ isValidDate
18
+ } = require("@cocreate/utils");
19
+
20
+ const organizations = {};
21
+
22
+ /**
23
+ * Dynamically resolves or caches an AWS DynamoDB Document Client instance.
24
+ * Normalizes AWS credentials out of your structured data.url or data.storageUrl object.
25
+ */
26
+ async function dbClient(data) {
27
+ const storageUrl = data.storageUrl || data.url;
28
+
29
+ if (storageUrl) {
30
+ if (!organizations[data.organization_id]) {
31
+ organizations[data.organization_id] = {};
32
+ }
33
+ try {
34
+ if (!organizations[data.organization_id][storageUrl]) {
35
+ const connectionPromise = (async () => {
36
+ let config = {};
37
+
38
+ // Resolve structured connection object (Recommended)
39
+ if (typeof storageUrl === "object" && storageUrl !== null) {
40
+ config = {
41
+ region: storageUrl.region || "us-east-1",
42
+ credentials: {
43
+ accessKeyId: storageUrl.accessKeyId || storageUrl.access_key_id,
44
+ secretAccessKey: storageUrl.secretAccessKey || storageUrl.secret_access_key
45
+ }
46
+ };
47
+ if (storageUrl.endpoint) {
48
+ config.endpoint = storageUrl.endpoint; // Support DynamoDB Local
49
+ }
50
+ }
51
+ // Handle string-fallback parses (e.g. dynamodb://region?accessKeyId=...&secretAccessKey=...)
52
+ else if (typeof storageUrl === "string" && storageUrl.startsWith("dynamodb://")) {
53
+ const urlParsed = new URL(storageUrl);
54
+ config = {
55
+ region: urlParsed.host || "us-east-1",
56
+ credentials: {
57
+ accessKeyId: urlParsed.searchParams.get("accessKeyId"),
58
+ secretAccessKey: urlParsed.searchParams.get("secretAccessKey")
59
+ }
60
+ };
61
+ }
62
+
63
+ const baseClient = new DynamoDBClient(config);
64
+
65
+ // Wrap with the DocumentClient to handle JavaScript types natively (no need for raw DynamoDB marshalling)
66
+ const docClient = DynamoDBDocumentClient.from(baseClient, {
67
+ marshallOptions: {
68
+ removeUndefinedValues: true,
69
+ convertEmptyValues: true
70
+ }
71
+ });
72
+
73
+ // Verify client viability by executing a lightweight command
74
+ await docClient.send(new ListTablesCommand({ Limit: 1 }));
75
+ return docClient;
76
+ })();
77
+
78
+ organizations[data.organization_id][storageUrl] = connectionPromise;
79
+ }
80
+
81
+ return await organizations[data.organization_id][storageUrl];
82
+ } catch (error) {
83
+ console.error(`${data.organization_id}: DynamoDB failed to connect`);
84
+ errorHandler(data, error);
85
+ return { status: false };
86
+ }
87
+ }
88
+
89
+ errorHandler(data, "missing DynamoDB Connection URL/Credentials");
90
+ return;
91
+ }
92
+
93
+ /**
94
+ * Global Event Listener: orgDeleted
95
+ * Cleans up active DynamoDB Client instances and destroys connection configurations.
96
+ */
97
+ process.on("orgDeleted", (organization_id) => {
98
+ const orgPools = organizations[organization_id];
99
+ if (orgPools) {
100
+ console.log(`[DynamoDB Utilities] Cleanup Scheduled: Initiating 5-second grace period for Org: ${organization_id}`);
101
+ setTimeout(async () => {
102
+ const currentPools = organizations[organization_id];
103
+ if (!currentPools) return;
104
+
105
+ console.log(`[DynamoDB Utilities] Cleanup Executing: Destroying active clients for Org: ${organization_id}`);
106
+ const clientPromises = Object.values(currentPools);
107
+
108
+ for (const clientPromise of clientPromises) {
109
+ try {
110
+ const client = await clientPromise;
111
+ if (client && typeof client.destroy === "function") {
112
+ client.destroy();
113
+ }
114
+ } catch (err) {
115
+ console.error(`[DynamoDB Utilities] Error closing DynamoDB client for Org ${organization_id}:`, err);
116
+ }
117
+ }
118
+
119
+ delete organizations[organization_id];
120
+ console.log(`[DynamoDB Utilities] Memory Cleared: Registry freed for Org: ${organization_id}`);
121
+ }, 5000);
122
+ }
123
+ });
124
+
125
+ /**
126
+ * Universal router matching your standard pipeline signature.
127
+ */
128
+ function send(data) {
129
+ let [type, method] = data.method.split(".");
130
+ if (type === "database") return database(method, data);
131
+ if (type === "array") return array(method, data);
132
+ if (type === "object") return object(method, data);
133
+ }
134
+
135
+ /**
136
+ * "database" operations mapped to DynamoDB logical grouping names.
137
+ * DynamoDB is serverless and works globally on an AWS Region basis, so we emulate schemas.
138
+ */
139
+ function database(method, data) {
140
+ const storageName = data.storageName || data.provider || "dynamodb";
141
+ return new Promise(async (resolve) => {
142
+ let type = "database";
143
+ let databaseArray = [];
144
+
145
+ try {
146
+ const db = await dbClient(data);
147
+ if (!db || db.status === false) return resolve(data);
148
+
149
+ if (method === "read") {
150
+ // We treat the current target region or designated config name as our active "database" catalog
151
+ const dbName = data.database || "default";
152
+ const dbObj = { name: dbName };
153
+
154
+ process.emit("usage", {
155
+ type: "egress",
156
+ data: { api: "listDatabases", context: dbName },
157
+ organization_id: data.organization_id
158
+ });
159
+
160
+ if (data.$filter && data.$filter.query) {
161
+ let isFilter = queryData(dbObj, data.$filter.query);
162
+ if (isFilter) {
163
+ databaseArray.push({ database: dbObj, storage: storageName });
164
+ }
165
+ } else {
166
+ databaseArray.push({ database: dbObj, storage: storageName });
167
+ }
168
+ resolve(createData(data, databaseArray, type));
169
+ }
170
+ if (method === "delete") {
171
+ // To drop a database partition, we scan and drop all tables belonging to the organization namespace
172
+ const listRes = await db.send(new ListTablesCommand({}));
173
+ const prefix = `${data.organization_id}_`;
174
+
175
+ for (let tName of listRes.TableNames || []) {
176
+ if (tName.startsWith(prefix)) {
177
+ process.emit("usage", {
178
+ type: "egress",
179
+ data: { api: "deleteTable", table: tName },
180
+ organization_id: data.organization_id
181
+ });
182
+ await db.send(new DeleteTableCommand({ TableName: tName }));
183
+ }
184
+ }
185
+ resolve({ status: true });
186
+ }
187
+ } catch (error) {
188
+ errorHandler(data, error);
189
+ console.log(method, "error", error);
190
+ resolve(data);
191
+ }
192
+ });
193
+ }
194
+
195
+ /**
196
+ * "array" operations mapped to AWS DynamoDB Tables.
197
+ * To achieve strict isolation, we prefix table names: "orgId_arrayName"
198
+ */
199
+ function array(method, data) {
200
+ const storageName = data.storageName || data.provider || "dynamodb";
201
+ return new Promise(async (resolve) => {
202
+ let type = "array";
203
+ let arrayArray = [];
204
+
205
+ try {
206
+ const db = await dbClient(data);
207
+ if (!db || db.status === false) return resolve(data);
208
+
209
+ if (data.request) data.array = data.request;
210
+
211
+ let databases = data.database;
212
+ if (!Array.isArray(databases)) databases = [databases];
213
+
214
+ let databasesLength = databases.length;
215
+ for (let database of databases) {
216
+ const prefix = `${data.organization_id}_`;
217
+
218
+ if (method === "read") {
219
+ process.emit("usage", {
220
+ type: "egress",
221
+ data: { api: "listTables" },
222
+ organization_id: data.organization_id
223
+ });
224
+
225
+ const listRes = await db.send(new ListTablesCommand({}));
226
+ for (let tName of listRes.TableNames || []) {
227
+ if (tName.startsWith(prefix)) {
228
+ const cleanName = tName.substring(prefix.length);
229
+ const tableObj = { name: cleanName };
230
+
231
+ if (data.$filter && data.$filter.query) {
232
+ let isFilter = queryData(tableObj, data.$filter.query);
233
+ if (isFilter) {
234
+ arrayArray.push({ name: cleanName, database, storage: storageName });
235
+ }
236
+ } else {
237
+ arrayArray.push({ name: cleanName, database, storage: storageName });
238
+ }
239
+ }
240
+ }
241
+
242
+ databasesLength -= 1;
243
+ if (!databasesLength) {
244
+ data = createData(data, arrayArray, type);
245
+ resolve(data);
246
+ }
247
+ } else {
248
+ let arrays = data.array;
249
+ if (method === "update") arrays = Object.entries(data.array);
250
+ if (!Array.isArray(arrays)) arrays = [arrays];
251
+
252
+ let arraysLength = arrays.length;
253
+ for (let array of arrays) {
254
+ if (method === "create") {
255
+ const realTableName = `${prefix}${array}`;
256
+
257
+ process.emit("usage", {
258
+ type: "egress",
259
+ data: { api: "createTable", table: realTableName },
260
+ organization_id: data.organization_id
261
+ });
262
+
263
+ // DynamoDB requires a defined partition key scheme. We map '_id' as the primary hash key.
264
+ await db.send(new CreateTableCommand({
265
+ TableName: realTableName,
266
+ KeySchema: [{ AttributeName: "_id", KeyType: "HASH" }],
267
+ AttributeDefinitions: [{ AttributeName: "_id", AttributeType: "S" }],
268
+ BillingMode: "PAY_PER_REQUEST" // Serverless/On-Demand pricing
269
+ }));
270
+
271
+ arrayArray.push({ name: array, database, storage: storageName });
272
+
273
+ arraysLength -= 1;
274
+ if (!arraysLength) databasesLength -= 1;
275
+ if (!databasesLength && !arraysLength) {
276
+ data = createData(data, arrayArray, type);
277
+ resolve(data);
278
+ }
279
+ } else {
280
+ if (method === "update") {
281
+ let [oldName, newName] = array;
282
+ const oldRealName = `${prefix}${oldName}`;
283
+ const newRealName = `${prefix}${newName}`;
284
+
285
+ process.emit("usage", {
286
+ type: "egress",
287
+ data: { api: "renameTableEmulation", from: oldRealName, to: newRealName },
288
+ organization_id: data.organization_id
289
+ });
290
+
291
+ // AWS DynamoDB does not natively support table renaming. We emulate this by creating
292
+ // the new table and migrating all scanned items into the new workspace.
293
+ await db.send(new CreateTableCommand({
294
+ TableName: newRealName,
295
+ KeySchema: [{ AttributeName: "_id", KeyType: "HASH" }],
296
+ AttributeDefinitions: [{ AttributeName: "_id", AttributeType: "S" }],
297
+ BillingMode: "PAY_PER_REQUEST"
298
+ }));
299
+
300
+ // Scan items out of old table and bulk-insert them
301
+ const scanRes = await db.send(new ScanCommand({ TableName: oldRealName }));
302
+ for (let item of scanRes.Items || []) {
303
+ await db.send(new PutCommand({ TableName: newRealName, Item: item }));
304
+ }
305
+
306
+ // Drop old table cleanly
307
+ await db.send(new DeleteTableCommand({ TableName: oldRealName }));
308
+
309
+ arrayArray.push({ name: newName, oldName: oldName, database, storage: storageName });
310
+
311
+ arraysLength -= 1;
312
+ if (!arraysLength) databasesLength -= 1;
313
+ if (!databasesLength && !arraysLength) {
314
+ data = createData(data, arrayArray, type);
315
+ resolve(data);
316
+ }
317
+ }
318
+
319
+ if (method === "delete") {
320
+ const realTableName = `${prefix}${array}`;
321
+ process.emit("usage", {
322
+ type: "egress",
323
+ data: { api: "deleteTable", table: realTableName },
324
+ organization_id: data.organization_id
325
+ });
326
+
327
+ await db.send(new DeleteTableCommand({ TableName: realTableName }));
328
+
329
+ arrayArray.push({ name: array, database, storage: storageName });
330
+
331
+ arraysLength -= 1;
332
+ if (!arraysLength) databasesLength -= 1;
333
+ if (!databasesLength && !arraysLength) {
334
+ data = createData(data, arrayArray, type);
335
+ resolve(data);
336
+ }
337
+ }
338
+ }
339
+ }
340
+ }
341
+ }
342
+ } catch (error) {
343
+ errorHandler(data, error);
344
+ console.log(method, "error", error);
345
+ resolve(data);
346
+ }
347
+ });
348
+ }
349
+
350
+ /**
351
+ * "object" operations mapped to AWS DynamoDB Table Items.
352
+ */
353
+ function object(method, data) {
354
+ const storageName = data.storageName || data.provider || "dynamodb";
355
+ return new Promise(async (resolve) => {
356
+ try {
357
+ const db = await dbClient(data);
358
+ if (!db || db.status === false) return resolve(data);
359
+
360
+ let type = "object";
361
+ let documents = [];
362
+
363
+ if (!data["timeStamp"]) {
364
+ data["timeStamp"] = new Date().toISOString();
365
+ }
366
+
367
+ let databases = data.database;
368
+ if (!Array.isArray(databases)) databases = [databases];
369
+
370
+ for (let database of databases) {
371
+ let arrays = data.array;
372
+ if (!Array.isArray(arrays)) arrays = [arrays];
373
+
374
+ for (let array of arrays) {
375
+ const realTableName = `${data.organization_id}_${array}`;
376
+ const reference = {
377
+ $storage: storageName,
378
+ $database: database,
379
+ $array: array
380
+ };
381
+
382
+ if (!data[type]) data[type] = [];
383
+ else if (typeof data[type] === "string")
384
+ data[type] = [{ _id: data[type] }];
385
+ else if (!Array.isArray(data[type]))
386
+ data[type] = [data[type]];
387
+
388
+ let isFilter = !!data.$filter;
389
+ if ((isFilter && !data[type].length) || data.isFilter) {
390
+ data[type].splice(0, 0, { isFilter: "isEmptyObjectFilter" });
391
+ }
392
+
393
+ for (let i = 0; i < data[type].length; i++) {
394
+ let $storage = data[type][i].$storage || [];
395
+ let $database = data[type][i].$database || [];
396
+ let $array = data[type][i].$array || [];
397
+
398
+ if (!Array.isArray($storage)) $storage = [data[type][i].$storage];
399
+ if (!Array.isArray($database)) $database = [data[type][i].$database];
400
+ if (!Array.isArray($array)) $array = [data[type][i].$array];
401
+
402
+ if (!$storage.includes(storageName)) $storage.push(storageName);
403
+ if (!$database.includes(database)) $database.push(database);
404
+ if (!$array.includes(array)) $array.push(array);
405
+
406
+ delete data[type][i].$storage;
407
+ delete data[type][i].$database;
408
+ delete data[type][i].$array;
409
+
410
+ let queryFilter = isFilter ? data.$filter : (data[type][i].$filter || null);
411
+
412
+ if (method === "create") {
413
+ data[type][i] = replaceArray(data[type][i]);
414
+ data[type][i] = dotNotationToObject(data[type][i]);
415
+
416
+ const id = data[type][i]._id || `item_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
417
+ const orgId = data.organization_id;
418
+ const created = {
419
+ on: new Date(data.timeStamp).toISOString(),
420
+ by: data.user_id || data.clientId
421
+ };
422
+
423
+ const recordPayload = { ...data[type][i] };
424
+ delete recordPayload._id;
425
+ delete recordPayload.organization_id;
426
+
427
+ const finalItem = {
428
+ _id: id,
429
+ organization_id: orgId,
430
+ created,
431
+ modified: null,
432
+ ...recordPayload
433
+ };
434
+
435
+ process.emit("usage", {
436
+ type: "egress",
437
+ data: { api: "putItem", table: realTableName, id },
438
+ organization_id: data.organization_id
439
+ });
440
+
441
+ await db.send(new PutCommand({
442
+ TableName: realTableName,
443
+ Item: finalItem
444
+ }));
445
+
446
+ data[type][i] = { ...finalItem, $storage, $database, $array };
447
+ }
448
+ else if (method === "read") {
449
+ if (data[type][i]._id && !isFilter) {
450
+ process.emit("usage", {
451
+ type: "egress",
452
+ data: { api: "getItem", table: realTableName, id: data[type][i]._id },
453
+ organization_id: data.organization_id
454
+ });
455
+
456
+ const getRes = await db.send(new GetCommand({
457
+ TableName: realTableName,
458
+ Key: { _id: data[type][i]._id }
459
+ }));
460
+
461
+ if (getRes.Item) {
462
+ let dbRecord = getRes.Item;
463
+
464
+ // Dynamic offline timings sync
465
+ if ($storage.length && data[type][i].modified && data[type][i].modified.on) {
466
+ let clientTime = new Date(data[type][i].modified.on);
467
+ let dbTime = new Date(dbRecord.modified ? dbRecord.modified.on : 0);
468
+
469
+ if (clientTime > dbTime) {
470
+ const updatedRecord = { ...dbRecord, ...data[type][i] };
471
+ const savedRecord = await executeUpdateTransaction(db, realTableName, data[type][i]._id, updatedRecord, data);
472
+ data[type][i] = { ...savedRecord, $storage, $database, $array };
473
+ } else {
474
+ data[type][i] = { ...data[type][i], ...dbRecord, $storage, $database, $array };
475
+ }
476
+ } else {
477
+ data[type][i] = { ...data[type][i], ...dbRecord, $storage, $database, $array };
478
+ }
479
+ } else {
480
+ data[type].splice(i, 1);
481
+ i--;
482
+ }
483
+ } else {
484
+ process.emit("usage", {
485
+ type: "egress",
486
+ data: { api: "scanTable", table: realTableName },
487
+ organization_id: data.organization_id
488
+ });
489
+
490
+ // Compile query filters dynamically
491
+ let scanParams = compileFilterToDynamo(realTableName, queryFilter, data.organization_id);
492
+ const scanRes = await db.send(new ScanCommand(scanParams));
493
+
494
+ let rows = scanRes.Items || [];
495
+
496
+ // Handle in-memory sorting/limit offset fallbacks for unindexed scan sets
497
+ if (data.$filter && data.$filter.sort) {
498
+ rows = sortData(rows, data.$filter.sort);
499
+ }
500
+ if (data.$filter && typeof data.$filter.index === "number") {
501
+ rows = rows.slice(data.$filter.index);
502
+ }
503
+ if (data.$filter && typeof data.$filter.limit === "number") {
504
+ rows = rows.slice(0, data.$filter.limit);
505
+ }
506
+
507
+ for (let parsedRow of rows) {
508
+ if (data.$filter && data.$filter.search) {
509
+ let isMatch = searchData(parsedRow, data.$filter.search);
510
+ if (!isMatch) continue;
511
+ }
512
+ documents.push({
513
+ ...parsedRow,
514
+ ...reference
515
+ });
516
+ }
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(db, realTableName, data[type][i]._id, updatePayload, data);
526
+ if (updatedDoc) {
527
+ data[type][i] = { ...updatedDoc, $storage, $database, $array };
528
+ }
529
+ } else if (isFilter) {
530
+ let scanParams = compileFilterToDynamo(realTableName, queryFilter, data.organization_id);
531
+ process.emit("usage", {
532
+ type: "egress",
533
+ data: { api: "scanForUpdate", table: realTableName },
534
+ organization_id: data.organization_id
535
+ });
536
+ const scanRes = await db.send(new ScanCommand(scanParams));
537
+
538
+ for (let item of scanRes.Items || []) {
539
+ const updatedDoc = await executeUpdateTransaction(db, realTableName, item._id, updatePayload, data);
540
+ if (updatedDoc) {
541
+ documents.push({ ...updatedDoc, ...reference });
542
+ }
543
+ }
544
+ }
545
+ }
546
+ else if (method === "delete") {
547
+ if (data[type][i]._id) {
548
+ process.emit("usage", {
549
+ type: "egress",
550
+ data: { api: "deleteItem", table: realTableName, id: data[type][i]._id },
551
+ organization_id: data.organization_id
552
+ });
553
+ await db.send(new DeleteCommand({
554
+ TableName: realTableName,
555
+ Key: { _id: data[type][i]._id }
556
+ }));
557
+ data[type][i] = { _id: data[type][i]._id, $storage, $database, $array };
558
+ } else if (isFilter) {
559
+ let scanParams = compileFilterToDynamo(realTableName, queryFilter, data.organization_id);
560
+ process.emit("usage", {
561
+ type: "egress",
562
+ data: { api: "scanForDelete", table: realTableName },
563
+ organization_id: data.organization_id
564
+ });
565
+ const scanRes = await db.send(new ScanCommand(scanParams));
566
+
567
+ for (let item of scanRes.Items || []) {
568
+ await db.send(new DeleteCommand({
569
+ TableName: realTableName,
570
+ Key: { _id: item._id }
571
+ }));
572
+ documents.push({ _id: item._id, ...reference });
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
+ }
589
+
590
+ /**
591
+ * Executes a transactional item write lock pipeline simulating isolation behaviors inside DynamoDB.
592
+ */
593
+ async function executeUpdateTransaction(db, realTableName, itemId, rawUpdateInput, requestData) {
594
+ try {
595
+ // DynamoDB implements transactions via TransactWriteItems. We first read the item, apply changes
596
+ // in code, and perform a conditional update check to prevent race condition write-collisions.
597
+ const getRes = await db.send(new GetCommand({
598
+ TableName: realTableName,
599
+ Key: { _id: itemId }
600
+ }));
601
+
602
+ let currentRecord = getRes.Item || null;
603
+
604
+ if (!currentRecord) {
605
+ if (requestData.upsert || rawUpdateInput.upsert || rawUpdateInput.$upsert) {
606
+ currentRecord = { _id: itemId, organization_id: requestData.organization_id };
607
+ } else {
608
+ return null;
609
+ }
610
+ }
611
+
612
+ const modified = {
613
+ on: new Date(requestData.timeStamp).toISOString(),
614
+ by: requestData.user_id || requestData.clientId
615
+ };
616
+
617
+ const updatedPayload = applyMongoUpdate(currentRecord, rawUpdateInput);
618
+
619
+ const savePayload = { ...updatedPayload };
620
+ delete savePayload._id;
621
+ delete savePayload.organization_id;
622
+ delete savePayload.created;
623
+ delete savePayload.modified;
624
+
625
+ const recordToWrite = {
626
+ _id: itemId,
627
+ organization_id: requestData.organization_id,
628
+ created: currentRecord.created || modified,
629
+ modified,
630
+ ...savePayload
631
+ };
632
+
633
+ process.emit("usage", {
634
+ type: "egress",
635
+ data: { api: "transactWriteItem", table: realTableName, id: itemId },
636
+ organization_id: requestData.organization_id
637
+ });
638
+
639
+ // Commit using AWS SDK's atomic validation transaction
640
+ await db.send(new TransactWriteCommand({
641
+ TransactItems: [
642
+ {
643
+ Put: {
644
+ TableName: realTableName,
645
+ Item: recordToWrite
646
+ }
647
+ }
648
+ ]
649
+ }));
650
+
651
+ return recordToWrite;
652
+ } catch (error) {
653
+ throw error;
654
+ }
655
+ }
656
+
657
+ /**
658
+ * Applies deep MongoDB modification operators ($set, $unset, $push, $pull, $addToSet, $inc)
659
+ * recursively on a target record.
660
+ */
661
+ function applyMongoUpdate(record, updatePayload) {
662
+ let target = { ...record };
663
+
664
+ Object.keys(updatePayload).forEach((rawKey) => {
665
+ if (rawKey.startsWith("$")) return;
666
+ let cleanKey = rawKey.replace(/\[(\d+)\]/g, ".$1");
667
+ setNestedValue(target, cleanKey, updatePayload[rawKey]);
668
+ });
669
+
670
+ if (updatePayload.$set) {
671
+ Object.keys(updatePayload.$set).forEach((key) => {
672
+ let cleanKey = key.replace(/\[(\d+)\]/g, ".$1");
673
+ setNestedValue(target, cleanKey, updatePayload.$set[key]);
674
+ });
675
+ }
676
+
677
+ if (updatePayload.$unset) {
678
+ Object.keys(updatePayload.$unset).forEach((key) => {
679
+ let cleanKey = key.replace(/\[(\d+)\]/g, ".$1");
680
+ deleteNestedValue(target, cleanKey);
681
+ });
682
+ }
683
+
684
+ if (updatePayload.$inc) {
685
+ Object.keys(updatePayload.$inc).forEach((key) => {
686
+ let cleanKey = key.replace(/\[(\d+)\]/g, ".$1");
687
+ let currentVal = getNestedValue(target, cleanKey) || 0;
688
+ setNestedValue(target, cleanKey, Number(currentVal) + Number(updatePayload.$inc[key]));
689
+ });
690
+ }
691
+
692
+ if (updatePayload.$push) {
693
+ Object.keys(updatePayload.$push).forEach((key) => {
694
+ let cleanKey = key.replace(/\[(\d+)\]/g, ".$1");
695
+ let arr = getNestedValue(target, cleanKey);
696
+ if (!Array.isArray(arr)) arr = [];
697
+
698
+ const pushValue = updatePayload.$push[key];
699
+ if (pushValue && pushValue.$each) {
700
+ let valuesToPush = pushValue.$each;
701
+ let position = typeof pushValue.$position === "number" ? pushValue.$position : arr.length;
702
+ arr.splice(position, 0, ...valuesToPush);
703
+ } else {
704
+ arr.push(pushValue);
705
+ }
706
+ setNestedValue(target, cleanKey, arr);
707
+ });
708
+ }
709
+
710
+ if (updatePayload.$pull) {
711
+ Object.keys(updatePayload.$pull).forEach((key) => {
712
+ let cleanKey = key.replace(/\[(\d+)\]/g, ".$1");
713
+ let arr = getNestedValue(target, cleanKey);
714
+ if (Array.isArray(arr)) {
715
+ const filterVal = updatePayload.$pull[key];
716
+ arr = arr.filter(item => {
717
+ if (typeof filterVal === "object" && filterVal !== null) {
718
+ return !queryData(item, filterVal);
719
+ }
720
+ return item !== filterVal;
721
+ });
722
+ setNestedValue(target, cleanKey, arr);
723
+ }
724
+ });
725
+ }
726
+
727
+ if (updatePayload.$addToSet) {
728
+ Object.keys(updatePayload.$addToSet).forEach((key) => {
729
+ let cleanKey = key.replace(/\[(\d+)\]/g, ".$1");
730
+ let arr = getNestedValue(target, cleanKey);
731
+ if (!Array.isArray(arr)) arr = [];
732
+
733
+ const addValue = updatePayload.$addToSet[key];
734
+ if (addValue && addValue.$each) {
735
+ addValue.$each.forEach(item => {
736
+ if (!arr.includes(item)) arr.push(item);
737
+ });
738
+ } else {
739
+ if (!arr.includes(addValue)) arr.push(addValue);
740
+ }
741
+ setNestedValue(target, cleanKey, arr);
742
+ });
743
+ }
744
+
745
+ return target;
746
+ }
747
+
748
+ /**
749
+ * Translates CoCreate Mongo-style filters dynamically into AWS DynamoDB query schemas.
750
+ */
751
+ function compileFilterToDynamo(tableName, filter, organizationId) {
752
+ let params = {
753
+ TableName: tableName,
754
+ FilterExpression: "organization_id = :org_id",
755
+ ExpressionAttributeValues: {
756
+ ":org_id": organizationId
757
+ }
758
+ };
759
+
760
+ if (filter && filter.query) {
761
+ let filterExpressions = ["organization_id = :org_id"];
762
+ let expressionVals = { ":org_id": organizationId };
763
+ let expressionNames = {};
764
+ let count = 0;
765
+
766
+ for (let key in filter.query) {
767
+ if (["$or", "$and", "organization_id"].includes(key)) continue;
768
+
769
+ let value = filter.query[key];
770
+ const valPlaceholder = `:val_${count}`;
771
+ const namePlaceholder = `#attr_${count}`;
772
+
773
+ expressionNames[namePlaceholder] = key;
774
+
775
+ if (value && typeof value === "object" && !Array.isArray(value)) {
776
+ for (let op in value) {
777
+ let operand = value[op];
778
+ let specificValPlaceholder = `${valPlaceholder}_${op.replace("$", "")}`;
779
+ expressionVals[specificValPlaceholder] = operand;
780
+
781
+ if (op === "$gt") filterExpressions.push(`${namePlaceholder} > ${specificValPlaceholder}`);
782
+ else if (op === "$gte") filterExpressions.push(`${namePlaceholder} >= ${specificValPlaceholder}`);
783
+ else if (op === "$lt") filterExpressions.push(`${namePlaceholder} < ${specificValPlaceholder}`);
784
+ else if (op === "$lte") filterExpressions.push(`${namePlaceholder} <= ${specificValPlaceholder}`);
785
+ else if (op === "$ne") filterExpressions.push(`${namePlaceholder} <> ${specificValPlaceholder}`);
786
+ else if (op === "$in") {
787
+ delete expressionVals[specificValPlaceholder];
788
+ const placeholders = operand.map((v, index) => {
789
+ const inPlaceholder = `${valPlaceholder}_in_${index}`;
790
+ expressionVals[inPlaceholder] = v;
791
+ return inPlaceholder;
792
+ });
793
+ filterExpressions.push(`${namePlaceholder} IN (${placeholders.join(", ")})`);
794
+ }
795
+ }
796
+ } else {
797
+ expressionVals[valPlaceholder] = value;
798
+ filterExpressions.push(`${namePlaceholder} = ${valPlaceholder}`);
799
+ }
800
+ count++;
801
+ }
802
+
803
+ params.FilterExpression = filterExpressions.join(" AND ");
804
+ params.ExpressionAttributeValues = expressionVals;
805
+ if (Object.keys(expressionNames).length > 0) {
806
+ params.ExpressionAttributeNames = expressionNames;
807
+ }
808
+ }
809
+
810
+ return params;
811
+ }
812
+
813
+ function replaceArray(data = {}) {
814
+ let object = {};
815
+ Object.keys(data).forEach((key) => {
816
+ object[key.replace(/\[(\d+)\]/g, ".$1")] = data[key];
817
+ });
818
+ return object;
819
+ }
820
+
821
+ function getNestedValue(obj, path) {
822
+ return path.split(".").reduce((acc, part) => acc && acc[part], obj);
823
+ }
824
+
825
+ function setNestedValue(obj, path, value) {
826
+ const parts = path.split(".");
827
+ const last = parts.pop();
828
+ const target = parts.reduce((acc, part) => acc[part] = acc[part] || {}, obj);
829
+ target[last] = value;
830
+ }
831
+
832
+ function deleteNestedValue(obj, path) {
833
+ const parts = path.split(".");
834
+ const last = parts.pop();
835
+ const target = parts.reduce((acc, part) => acc && acc[part], obj);
836
+ if (target) delete target[last];
837
+ }
838
+
839
+ function createData(data, array, type) {
840
+ if (data[type] && data[type][0] && data[type][0].isFilter === "isEmptyObjectFilter") {
841
+ data[type].shift();
842
+ data.isFilter = true;
843
+ }
844
+
845
+ let key = type !== "object" ? "name" : "_id";
846
+
847
+ if (!Array.isArray(data[type])) {
848
+ console.log("data[type] is not an array", type);
849
+ } else {
850
+ for (let i = 0; i < array.length; i++) {
851
+ const matchIndex = data[type].findIndex((item) => item[key] === array[i][key]);
852
+ if (matchIndex !== -1) {
853
+ for (let $type of ["$storage", "$database", "$array"]) {
854
+ if (!data[type][matchIndex][$type]) data[type][matchIndex][$type] = [];
855
+ if (!Array.isArray(data[type][matchIndex][$type])) {
856
+ data[type][matchIndex][$type] = [data[type][matchIndex][$type]];
857
+ }
858
+ if (!data[type][matchIndex][$type].includes(array[i][$type])) {
859
+ data[type][matchIndex][$type].push(array[i][$type]);
860
+ }
861
+ delete array[i][$type];
862
+ }
863
+ data[type][matchIndex] = { ...data[type][matchIndex], ...array[i] };
864
+ } else {
865
+ data[type].push(array[i]);
866
+ }
867
+ }
868
+ }
869
+ return data;
870
+ }
871
+
872
+ function errorHandler(data, error, database, array) {
873
+ let errorMessage = typeof error === "object" && error.message ? error.message : error;
874
+ let errorObject = {
875
+ message: errorMessage,
876
+ storage: "dynamodb"
877
+ };
878
+
879
+ if (database) errorObject.database = database;
880
+ if (array) errorObject.array = array;
881
+
882
+ if (Array.isArray(data.error)) {
883
+ data.error.push(errorObject);
884
+ } else {
885
+ data.error = [errorObject];
886
+ }
887
+ }
888
+
889
+ module.exports = { send };