@arcaelas/dynamite 1.0.14 → 1.0.17
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/package.json +1 -1
- package/src/@types/index.d.ts +96 -0
- package/src/@types/index.js +9 -0
- package/src/core/client.d.ts +69 -0
- package/src/core/client.js +164 -0
- package/src/core/table.d.ts +98 -0
- package/src/core/table.js +459 -0
- package/src/core/wrapper.d.ts +17 -0
- package/src/core/wrapper.js +46 -0
- package/src/decorators/belongs_to.d.ts +1 -0
- package/src/decorators/belongs_to.js +24 -0
- package/src/decorators/created_at.d.ts +1 -0
- package/src/decorators/created_at.js +11 -0
- package/src/decorators/default.d.ts +1 -0
- package/src/decorators/default.js +47 -0
- package/src/decorators/has_many.d.ts +1 -0
- package/src/decorators/has_many.js +24 -0
- package/src/decorators/index.d.ts +11 -0
- package/src/decorators/index.js +36 -0
- package/src/decorators/index_sort.d.ts +12 -0
- package/src/decorators/index_sort.js +43 -0
- package/src/decorators/mutate.d.ts +2 -0
- package/src/decorators/mutate.js +51 -0
- package/src/decorators/name.d.ts +1 -0
- package/src/decorators/name.js +28 -0
- package/src/decorators/not_null.d.ts +1 -0
- package/src/decorators/not_null.js +13 -0
- package/src/decorators/primary_key.d.ts +6 -0
- package/src/decorators/primary_key.js +30 -0
- package/src/decorators/updated_at.d.ts +12 -0
- package/src/decorators/updated_at.js +26 -0
- package/src/decorators/validate.d.ts +1 -0
- package/src/decorators/validate.js +53 -0
- package/src/index.d.ts +22 -0
- package/src/index.js +47 -0
- package/src/utils/batch-relations.d.ts +14 -0
- package/src/utils/batch-relations.js +131 -0
- package/src/utils/circular-detector.d.ts +82 -0
- package/src/utils/circular-detector.js +212 -0
- package/src/utils/memory-manager.d.ts +42 -0
- package/src/utils/memory-manager.js +107 -0
- package/src/utils/naming.d.ts +8 -0
- package/src/utils/naming.js +18 -0
- package/src/utils/projection.d.ts +12 -0
- package/src/utils/projection.js +51 -0
- package/src/utils/relations.d.ts +17 -0
- package/src/utils/relations.js +165 -0
- package/src/utils/security-validator.d.ts +49 -0
- package/src/utils/security-validator.js +163 -0
- package/src/utils/throttle-manager.d.ts +78 -0
- package/src/utils/throttle-manager.js +201 -0
- package/src/utils/transaction-manager.d.ts +88 -0
- package/src/utils/transaction-manager.js +300 -0
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* @file transaction-manager.ts
|
|
4
|
+
* @description Transaction simulation and rollback system for DynamoDB
|
|
5
|
+
* @author Miguel Alejandro
|
|
6
|
+
* @fecha 2025-08-31
|
|
7
|
+
*/
|
|
8
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
9
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
10
|
+
};
|
|
11
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
exports.TransactionManager = exports.TransactionError = void 0;
|
|
13
|
+
const client_dynamodb_1 = require("@aws-sdk/client-dynamodb");
|
|
14
|
+
const util_dynamodb_1 = require("@aws-sdk/util-dynamodb");
|
|
15
|
+
const security_validator_1 = __importDefault(require("./security-validator"));
|
|
16
|
+
const throttle_manager_1 = __importDefault(require("./throttle-manager"));
|
|
17
|
+
class TransactionManager {
|
|
18
|
+
static { this.DEFAULT_CONFIG = {
|
|
19
|
+
maxOperationsPerTransaction: 25, // DynamoDB limit
|
|
20
|
+
snapshotTTL: 3600000, // 1 hour
|
|
21
|
+
enableOptimisticLocking: true,
|
|
22
|
+
maxRetries: 3,
|
|
23
|
+
}; }
|
|
24
|
+
constructor(client, config) {
|
|
25
|
+
this.activeTransactions = new Map();
|
|
26
|
+
this.snapshots = new Map();
|
|
27
|
+
this.client = client;
|
|
28
|
+
this.config = { ...TransactionManager.DEFAULT_CONFIG, ...config };
|
|
29
|
+
// Cleanup expired snapshots
|
|
30
|
+
setInterval(() => this.cleanupExpiredSnapshots(), 300000); // 5 minutes
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Iniciar nueva transacción
|
|
34
|
+
*/
|
|
35
|
+
async beginTransaction(operations) {
|
|
36
|
+
const transactionId = this.generateTransactionId();
|
|
37
|
+
// Validar operaciones
|
|
38
|
+
this.validateOperations(operations);
|
|
39
|
+
// Crear snapshot del estado actual para rollback
|
|
40
|
+
const snapshot = await this.createSnapshot(transactionId, operations);
|
|
41
|
+
this.activeTransactions.set(transactionId, snapshot);
|
|
42
|
+
this.snapshots.set(transactionId, snapshot);
|
|
43
|
+
return transactionId;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Ejecutar transacción usando DynamoDB TransactWrite
|
|
47
|
+
*/
|
|
48
|
+
async commitTransaction(transactionId) {
|
|
49
|
+
const snapshot = this.activeTransactions.get(transactionId);
|
|
50
|
+
if (!snapshot) {
|
|
51
|
+
throw new TransactionError(`Transaction ${transactionId} not found`);
|
|
52
|
+
}
|
|
53
|
+
try {
|
|
54
|
+
snapshot.status = "pending";
|
|
55
|
+
// Agrupar operaciones en lotes de 25 (límite DynamoDB)
|
|
56
|
+
const batches = this.splitIntoBatches(snapshot.operations);
|
|
57
|
+
for (const batch of batches) {
|
|
58
|
+
await this.executeBatch(batch);
|
|
59
|
+
}
|
|
60
|
+
snapshot.status = "committed";
|
|
61
|
+
this.activeTransactions.delete(transactionId);
|
|
62
|
+
}
|
|
63
|
+
catch (error) {
|
|
64
|
+
snapshot.status = "failed";
|
|
65
|
+
// Intentar rollback automático
|
|
66
|
+
try {
|
|
67
|
+
await this.rollbackTransaction(transactionId);
|
|
68
|
+
}
|
|
69
|
+
catch (rollbackError) {
|
|
70
|
+
console.error("Rollback failed after commit error:", rollbackError);
|
|
71
|
+
}
|
|
72
|
+
throw new TransactionError(`Transaction commit failed: ${error.message}`);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Rollback manual de transacción
|
|
77
|
+
*/
|
|
78
|
+
async rollbackTransaction(transactionId) {
|
|
79
|
+
const snapshot = this.snapshots.get(transactionId);
|
|
80
|
+
if (!snapshot) {
|
|
81
|
+
throw new TransactionError(`Transaction snapshot ${transactionId} not found`);
|
|
82
|
+
}
|
|
83
|
+
try {
|
|
84
|
+
// Crear operaciones inversas para rollback
|
|
85
|
+
const rollbackOps = this.createRollbackOperations(snapshot.operations);
|
|
86
|
+
if (rollbackOps.length > 0) {
|
|
87
|
+
const batches = this.splitIntoBatches(rollbackOps);
|
|
88
|
+
for (const batch of batches) {
|
|
89
|
+
await this.executeBatch(batch);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
snapshot.status = "rolled_back";
|
|
93
|
+
this.activeTransactions.delete(transactionId);
|
|
94
|
+
}
|
|
95
|
+
catch (error) {
|
|
96
|
+
throw new TransactionError(`Rollback failed: ${error.message}`);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Crear snapshot del estado actual para rollback
|
|
101
|
+
*/
|
|
102
|
+
async createSnapshot(transactionId, operations) {
|
|
103
|
+
const snapshot = {
|
|
104
|
+
id: transactionId,
|
|
105
|
+
operations: [],
|
|
106
|
+
timestamp: Date.now(),
|
|
107
|
+
status: "pending",
|
|
108
|
+
};
|
|
109
|
+
// Para cada operación, guardar el estado actual del item
|
|
110
|
+
for (const op of operations) {
|
|
111
|
+
try {
|
|
112
|
+
const currentItem = await this.getCurrentItemState(op.tableName, op.key);
|
|
113
|
+
const snapshotOp = {
|
|
114
|
+
...op,
|
|
115
|
+
rollbackData: currentItem || undefined, // Estado actual para rollback
|
|
116
|
+
};
|
|
117
|
+
snapshot.operations.push(snapshotOp);
|
|
118
|
+
}
|
|
119
|
+
catch (error) {
|
|
120
|
+
if (error.name !== "ResourceNotFoundException") {
|
|
121
|
+
throw error;
|
|
122
|
+
}
|
|
123
|
+
// Item no existe, sin datos de rollback necesarios
|
|
124
|
+
snapshot.operations.push(op);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
return snapshot;
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Obtener estado actual de un item
|
|
131
|
+
*/
|
|
132
|
+
async getCurrentItemState(tableName, key) {
|
|
133
|
+
const params = {
|
|
134
|
+
TransactItems: [
|
|
135
|
+
{
|
|
136
|
+
Get: {
|
|
137
|
+
TableName: tableName,
|
|
138
|
+
Key: (0, util_dynamodb_1.marshall)(key),
|
|
139
|
+
},
|
|
140
|
+
},
|
|
141
|
+
],
|
|
142
|
+
};
|
|
143
|
+
const result = await throttle_manager_1.default.executeWithRetry(() => this.client.send(new client_dynamodb_1.TransactGetItemsCommand(params)), `GetItem-${tableName}`);
|
|
144
|
+
return result.Responses?.[0]?.Item
|
|
145
|
+
? (0, util_dynamodb_1.unmarshall)(result.Responses[0].Item)
|
|
146
|
+
: null;
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Crear operaciones inversas para rollback
|
|
150
|
+
*/
|
|
151
|
+
createRollbackOperations(operations) {
|
|
152
|
+
const rollbackOps = [];
|
|
153
|
+
// Procesar en orden inverso
|
|
154
|
+
for (let i = operations.length - 1; i >= 0; i--) {
|
|
155
|
+
const op = operations[i];
|
|
156
|
+
switch (op.type) {
|
|
157
|
+
case "create":
|
|
158
|
+
// Para rollback de create: delete el item
|
|
159
|
+
rollbackOps.push({
|
|
160
|
+
type: "delete",
|
|
161
|
+
tableName: op.tableName,
|
|
162
|
+
key: op.key,
|
|
163
|
+
});
|
|
164
|
+
break;
|
|
165
|
+
case "update":
|
|
166
|
+
if (op.rollbackData) {
|
|
167
|
+
// Para rollback de update: restore estado anterior
|
|
168
|
+
rollbackOps.push({
|
|
169
|
+
type: "update",
|
|
170
|
+
tableName: op.tableName,
|
|
171
|
+
key: op.key,
|
|
172
|
+
item: op.rollbackData,
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
break;
|
|
176
|
+
case "delete":
|
|
177
|
+
if (op.rollbackData) {
|
|
178
|
+
// Para rollback de delete: recrear el item
|
|
179
|
+
rollbackOps.push({
|
|
180
|
+
type: "create",
|
|
181
|
+
tableName: op.tableName,
|
|
182
|
+
key: op.key,
|
|
183
|
+
item: op.rollbackData,
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
break;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
return rollbackOps;
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* Ejecutar lote de operaciones usando TransactWrite
|
|
193
|
+
*/
|
|
194
|
+
async executeBatch(operations) {
|
|
195
|
+
const transactItems = operations.map((op) => {
|
|
196
|
+
security_validator_1.default.validateItemSize(op.item || op.key);
|
|
197
|
+
switch (op.type) {
|
|
198
|
+
case "create":
|
|
199
|
+
case "update":
|
|
200
|
+
return {
|
|
201
|
+
Put: {
|
|
202
|
+
TableName: op.tableName,
|
|
203
|
+
Item: (0, util_dynamodb_1.marshall)(op.item, { removeUndefinedValues: true }),
|
|
204
|
+
ConditionExpression: op.conditionExpression,
|
|
205
|
+
},
|
|
206
|
+
};
|
|
207
|
+
case "delete":
|
|
208
|
+
return {
|
|
209
|
+
Delete: {
|
|
210
|
+
TableName: op.tableName,
|
|
211
|
+
Key: (0, util_dynamodb_1.marshall)(op.key),
|
|
212
|
+
ConditionExpression: op.conditionExpression,
|
|
213
|
+
},
|
|
214
|
+
};
|
|
215
|
+
default:
|
|
216
|
+
throw new TransactionError(`Unknown operation type: ${op.type}`);
|
|
217
|
+
}
|
|
218
|
+
});
|
|
219
|
+
const command = new client_dynamodb_1.TransactWriteItemsCommand({
|
|
220
|
+
TransactItems: transactItems,
|
|
221
|
+
});
|
|
222
|
+
await throttle_manager_1.default.executeWithRetry(() => this.client.send(command), "TransactWrite");
|
|
223
|
+
}
|
|
224
|
+
/**
|
|
225
|
+
* Dividir operaciones en lotes según límites DynamoDB
|
|
226
|
+
*/
|
|
227
|
+
splitIntoBatches(operations) {
|
|
228
|
+
const batches = [];
|
|
229
|
+
const batchSize = this.config.maxOperationsPerTransaction;
|
|
230
|
+
for (let i = 0; i < operations.length; i += batchSize) {
|
|
231
|
+
batches.push(operations.slice(i, i + batchSize));
|
|
232
|
+
}
|
|
233
|
+
return batches;
|
|
234
|
+
}
|
|
235
|
+
/**
|
|
236
|
+
* Validar operaciones antes de transacción
|
|
237
|
+
*/
|
|
238
|
+
validateOperations(operations) {
|
|
239
|
+
if (operations.length === 0) {
|
|
240
|
+
throw new TransactionError("Transaction must contain at least one operation");
|
|
241
|
+
}
|
|
242
|
+
if (operations.length > this.config.maxOperationsPerTransaction * 10) {
|
|
243
|
+
throw new TransactionError(`Too many operations: ${operations.length}`);
|
|
244
|
+
}
|
|
245
|
+
for (const op of operations) {
|
|
246
|
+
// Validar estructura de la operación
|
|
247
|
+
if (!op.tableName || !op.key) {
|
|
248
|
+
throw new TransactionError("Invalid operation: missing tableName or key");
|
|
249
|
+
}
|
|
250
|
+
// Validar datos de seguridad
|
|
251
|
+
security_validator_1.default.validateQueryFilters(op.key);
|
|
252
|
+
if (op.item) {
|
|
253
|
+
security_validator_1.default.validateValue(op.item);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
/**
|
|
258
|
+
* Limpiar snapshots expirados
|
|
259
|
+
*/
|
|
260
|
+
cleanupExpiredSnapshots() {
|
|
261
|
+
const now = Date.now();
|
|
262
|
+
const expired = [];
|
|
263
|
+
for (const [id, snapshot] of this.snapshots.entries()) {
|
|
264
|
+
if (now - snapshot.timestamp > this.config.snapshotTTL) {
|
|
265
|
+
expired.push(id);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
expired.forEach((id) => {
|
|
269
|
+
this.snapshots.delete(id);
|
|
270
|
+
this.activeTransactions.delete(id);
|
|
271
|
+
});
|
|
272
|
+
if (expired.length > 0) {
|
|
273
|
+
console.log(`Cleaned up ${expired.length} expired transaction snapshots`);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
/**
|
|
277
|
+
* Obtener estado de transacción
|
|
278
|
+
*/
|
|
279
|
+
getTransactionStatus(transactionId) {
|
|
280
|
+
return this.snapshots.get(transactionId) || null;
|
|
281
|
+
}
|
|
282
|
+
/**
|
|
283
|
+
* Listar transacciones activas
|
|
284
|
+
*/
|
|
285
|
+
getActiveTransactions() {
|
|
286
|
+
return Array.from(this.activeTransactions.values());
|
|
287
|
+
}
|
|
288
|
+
generateTransactionId() {
|
|
289
|
+
return `txn_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
exports.TransactionManager = TransactionManager;
|
|
293
|
+
class TransactionError extends Error {
|
|
294
|
+
constructor(message) {
|
|
295
|
+
super(message);
|
|
296
|
+
this.name = "TransactionError";
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
exports.TransactionError = TransactionError;
|
|
300
|
+
//# sourceMappingURL=transaction-manager.js.map
|