@arcaelas/dynamite 1.0.17 → 1.0.19

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.
Files changed (67) hide show
  1. package/package.json +40 -2
  2. package/src/@types/index.d.ts +116 -75
  3. package/src/core/client.d.ts +36 -0
  4. package/src/core/client.js +80 -27
  5. package/src/core/decorator.d.ts +44 -0
  6. package/src/core/decorator.js +133 -0
  7. package/src/core/method.d.ts +73 -0
  8. package/src/core/method.js +140 -0
  9. package/src/core/table.d.ts +44 -86
  10. package/src/core/table.js +510 -310
  11. package/src/decorators/indexes.d.ts +38 -0
  12. package/src/decorators/indexes.js +67 -0
  13. package/src/decorators/relations.d.ts +55 -0
  14. package/src/decorators/relations.js +84 -0
  15. package/src/decorators/timestamps.d.ts +54 -0
  16. package/src/decorators/timestamps.js +67 -0
  17. package/src/decorators/transforms.d.ts +86 -0
  18. package/src/decorators/transforms.js +154 -0
  19. package/src/index.d.ts +10 -16
  20. package/src/index.js +50 -32
  21. package/src/index.test.d.ts +13 -0
  22. package/src/index.test.js +1992 -0
  23. package/src/utils/relations.d.ts +34 -12
  24. package/src/utils/relations.js +109 -133
  25. package/src/@types/index.js +0 -9
  26. package/src/core/wrapper.d.ts +0 -17
  27. package/src/core/wrapper.js +0 -46
  28. package/src/decorators/belongs_to.d.ts +0 -1
  29. package/src/decorators/belongs_to.js +0 -24
  30. package/src/decorators/created_at.d.ts +0 -1
  31. package/src/decorators/created_at.js +0 -11
  32. package/src/decorators/default.d.ts +0 -1
  33. package/src/decorators/default.js +0 -47
  34. package/src/decorators/has_many.d.ts +0 -1
  35. package/src/decorators/has_many.js +0 -24
  36. package/src/decorators/index.d.ts +0 -11
  37. package/src/decorators/index.js +0 -36
  38. package/src/decorators/index_sort.d.ts +0 -12
  39. package/src/decorators/index_sort.js +0 -43
  40. package/src/decorators/mutate.d.ts +0 -2
  41. package/src/decorators/mutate.js +0 -51
  42. package/src/decorators/name.d.ts +0 -1
  43. package/src/decorators/name.js +0 -28
  44. package/src/decorators/not_null.d.ts +0 -1
  45. package/src/decorators/not_null.js +0 -13
  46. package/src/decorators/primary_key.d.ts +0 -6
  47. package/src/decorators/primary_key.js +0 -30
  48. package/src/decorators/updated_at.d.ts +0 -12
  49. package/src/decorators/updated_at.js +0 -26
  50. package/src/decorators/validate.d.ts +0 -1
  51. package/src/decorators/validate.js +0 -53
  52. package/src/utils/batch-relations.d.ts +0 -14
  53. package/src/utils/batch-relations.js +0 -131
  54. package/src/utils/circular-detector.d.ts +0 -82
  55. package/src/utils/circular-detector.js +0 -212
  56. package/src/utils/memory-manager.d.ts +0 -42
  57. package/src/utils/memory-manager.js +0 -107
  58. package/src/utils/naming.d.ts +0 -8
  59. package/src/utils/naming.js +0 -18
  60. package/src/utils/projection.d.ts +0 -12
  61. package/src/utils/projection.js +0 -51
  62. package/src/utils/security-validator.d.ts +0 -49
  63. package/src/utils/security-validator.js +0 -163
  64. package/src/utils/throttle-manager.d.ts +0 -78
  65. package/src/utils/throttle-manager.js +0 -201
  66. package/src/utils/transaction-manager.d.ts +0 -88
  67. package/src/utils/transaction-manager.js +0 -300
package/src/core/table.js CHANGED
@@ -1,82 +1,144 @@
1
1
  "use strict";
2
2
  /**
3
3
  * @file table.ts
4
- * @descripcion Clase Table rediseñada con API completa y tipado estricto
4
+ * @description Tabla autocontenida con arquitectura minimalista y Symbol storage
5
5
  * @autor Miguel Alejandro
6
- * @fecha 2025-07-30
6
+ * @fecha 2025-01-28
7
7
  */
8
8
  Object.defineProperty(exports, "__esModule", { value: true });
9
- exports.STORE = void 0;
10
9
  const client_dynamodb_1 = require("@aws-sdk/client-dynamodb");
11
10
  const util_dynamodb_1 = require("@aws-sdk/util-dynamodb");
12
- const relations_1 = require("../utils/relations");
13
11
  const client_1 = require("./client");
14
- const wrapper_1 = require("./wrapper");
15
- Object.defineProperty(exports, "STORE", { enumerable: true, get: function () { return wrapper_1.STORE; } });
16
- /** Tipos importados desde @types */
12
+ const decorator_1 = require("./decorator");
13
+ const relations_1 = require("../utils/relations");
14
+ // Alias de operadores para sintaxis objeto
15
+ const OPERATOR_ALIASES = {
16
+ '$eq': '=',
17
+ '$ne': '!=',
18
+ '$lt': '<',
19
+ '$lte': '<=',
20
+ '$gt': '>',
21
+ '$gte': '>=',
22
+ '$in': 'in',
23
+ '$nin': 'not-in',
24
+ 'contains': 'contains',
25
+ 'begins-with': 'begins-with',
26
+ 'beginsWith': 'begins-with',
27
+ // Operadores directos también soportados
28
+ '=': '=',
29
+ '!=': '!=',
30
+ '<': '<',
31
+ '<=': '<=',
32
+ '>': '>',
33
+ '>=': '>=',
34
+ 'in': 'in',
35
+ 'not-in': 'not-in',
36
+ };
17
37
  // =============================================================================
18
- // FUNCIONES UTILITARIAS
38
+ // FUNCIONES UTILITARIAS SIMPLIFICADAS
19
39
  // =============================================================================
20
40
  function validateOperator(operator) {
21
- const validOps = [
22
- "=",
23
- "!=",
24
- "<",
25
- "<=",
26
- ">",
27
- ">=",
28
- "in",
29
- "not-in",
30
- "contains",
31
- "begins-with",
32
- ];
41
+ const validOps = ["=", "!=", "<", "<=", ">", ">=", "in", "not-in", "contains", "begins-with"];
33
42
  if (!validOps.includes(operator)) {
34
43
  throw new Error(`Operador inválido: ${operator}. Válidos: ${validOps.join(", ")}`);
35
44
  }
36
45
  return operator;
37
46
  }
38
- function buildConditionExpression(filters, operator = "=") {
47
+ /**
48
+ * @description Detecta si un valor es un objeto con operadores
49
+ */
50
+ function isOperatorObject(value) {
51
+ if (value === null || value === undefined || typeof value !== 'object' || Array.isArray(value)) {
52
+ return false;
53
+ }
54
+ // Es objeto con operadores si alguna key es un operador conocido
55
+ return Object.keys(value).some(key => key in OPERATOR_ALIASES);
56
+ }
57
+ /**
58
+ * @description Genera expresión para un operador y valor específico
59
+ */
60
+ function buildSingleExpression(nameKey, valueKeyPrefix, op, val, names, values, fieldName) {
61
+ // Manejar arrays vacíos
62
+ if (op === "in" && Array.isArray(val) && val.length === 0) {
63
+ return "1 = 0"; // Ningún resultado coincide
64
+ }
65
+ if (op === "not-in" && Array.isArray(val) && val.length === 0) {
66
+ return null; // Excluir nada = todo pasa
67
+ }
68
+ names[nameKey] = fieldName;
69
+ if (op === "in" && Array.isArray(val)) {
70
+ const inValues = val.map((v, i) => {
71
+ const inValueKey = `${valueKeyPrefix}_${i}`;
72
+ values[inValueKey] = v;
73
+ return inValueKey;
74
+ });
75
+ return `${nameKey} IN (${inValues.join(", ")})`;
76
+ }
77
+ else if (op === "not-in" && Array.isArray(val)) {
78
+ const notInValues = val.map((v, i) => {
79
+ const notInValueKey = `${valueKeyPrefix}_${i}`;
80
+ values[notInValueKey] = v;
81
+ return notInValueKey;
82
+ });
83
+ return `NOT ${nameKey} IN (${notInValues.join(", ")})`;
84
+ }
85
+ else if (op === "contains") {
86
+ values[valueKeyPrefix] = val;
87
+ return `contains(${nameKey}, ${valueKeyPrefix})`;
88
+ }
89
+ else if (op === "begins-with") {
90
+ values[valueKeyPrefix] = val;
91
+ return `begins_with(${nameKey}, ${valueKeyPrefix})`;
92
+ }
93
+ else if (op === "!=") {
94
+ values[valueKeyPrefix] = val;
95
+ return `${nameKey} <> ${valueKeyPrefix}`;
96
+ }
97
+ else {
98
+ values[valueKeyPrefix] = val;
99
+ return `${nameKey} ${op} ${valueKeyPrefix}`;
100
+ }
101
+ }
102
+ function buildConditionExpression(filters, defaultOperator = "=") {
39
103
  const expressions = [];
40
104
  const names = {};
41
105
  const values = {};
42
- Object.entries(filters).forEach(([key, value], index) => {
43
- const nameKey = `#attr${index}`;
44
- const valueKey = `:val${index}`;
45
- names[nameKey] = key;
46
- if (operator === "in" && Array.isArray(value)) {
47
- const inValues = value.map((v, i) => {
48
- const inValueKey = `:val${index}_${i}`;
49
- values[inValueKey] = v;
50
- return inValueKey;
51
- });
52
- expressions.push(`${nameKey} IN (${inValues.join(", ")})`);
53
- }
54
- else if (operator === "not-in" && Array.isArray(value)) {
55
- const notInValues = value.map((v, i) => {
56
- const notInValueKey = `:val${index}_${i}`;
57
- values[notInValueKey] = v;
58
- return notInValueKey;
59
- });
60
- expressions.push(`NOT ${nameKey} IN (${notInValues.join(", ")})`);
61
- }
62
- else if (operator === "contains") {
63
- values[valueKey] = value;
64
- expressions.push(`contains(${nameKey}, ${valueKey})`);
65
- }
66
- else if (operator === "begins-with") {
67
- values[valueKey] = value;
68
- expressions.push(`begins_with(${nameKey}, ${valueKey})`);
69
- }
70
- else if (operator === "!=") {
71
- // DynamoDB uses <> for not equal
72
- values[valueKey] = value;
73
- expressions.push(`${nameKey} <> ${valueKey}`);
106
+ let index = 0;
107
+ for (const [fieldName, filterValue] of Object.entries(filters)) {
108
+ // Ignorar valores undefined
109
+ if (filterValue === undefined)
110
+ continue;
111
+ // Detectar sintaxis objeto con operadores: { campo: { operador: valor } }
112
+ if (isOperatorObject(filterValue)) {
113
+ // Procesar cada operador en el objeto
114
+ for (const [opKey, opValue] of Object.entries(filterValue)) {
115
+ // Ignorar valores undefined dentro del objeto
116
+ if (opValue === undefined)
117
+ continue;
118
+ const resolvedOp = OPERATOR_ALIASES[opKey];
119
+ if (!resolvedOp)
120
+ continue; // Ignorar keys desconocidas
121
+ const nameKey = `#attr${index}`;
122
+ const valueKey = `:val${index}`;
123
+ const expr = buildSingleExpression(nameKey, valueKey, resolvedOp, opValue, names, values, fieldName);
124
+ if (expr)
125
+ expressions.push(expr);
126
+ index++;
127
+ }
74
128
  }
75
129
  else {
76
- values[valueKey] = value;
77
- expressions.push(`${nameKey} ${operator} ${valueKey}`);
130
+ // Valor simple: usar operador default
131
+ // Ignorar null solo si no hay operador explícito
132
+ if (filterValue === null)
133
+ continue;
134
+ const nameKey = `#attr${index}`;
135
+ const valueKey = `:val${index}`;
136
+ const expr = buildSingleExpression(nameKey, valueKey, defaultOperator, filterValue, names, values, fieldName);
137
+ if (expr)
138
+ expressions.push(expr);
139
+ index++;
78
140
  }
79
- });
141
+ }
80
142
  return {
81
143
  expression: expressions.join(" AND "),
82
144
  names,
@@ -84,297 +146,398 @@ function buildConditionExpression(filters, operator = "=") {
84
146
  };
85
147
  }
86
148
  // =============================================================================
87
- // CLASE TABLE REDISEÑADA
149
+ // CLASE TABLE AUTOCONTENIDA
88
150
  // =============================================================================
89
151
  class Table {
90
- constructor(data) {
152
+ constructor(props = {}) {
91
153
  (0, client_1.requireClient)();
92
- const meta = (0, wrapper_1.mustMeta)(Object.getPrototypeOf(this).constructor);
93
- // Inicializar propiedades con valores por defecto
94
- meta.columns.forEach((col, key) => {
95
- if (typeof key === "string" && !(key in data)) {
96
- // Forzar aplicación de valor por defecto mediante setter
97
- if (col.default !== undefined) {
98
- const defaultValue = typeof col.default === "function" ? col.default() : col.default;
99
- this[key] = defaultValue;
100
- }
101
- else {
102
- this[key] = undefined;
154
+ const schema = this.constructor[decorator_1.SCHEMA];
155
+ // Inicializar VALUES
156
+ this[decorator_1.VALUES] = {};
157
+ // Procesar cada columna del schema
158
+ for (const column_name in schema.columns) {
159
+ const column = schema.columns[column_name];
160
+ if (column.store?.relation) {
161
+ // ═══════════════════════════════════════════════════════════════
162
+ // RELACIONES: getter lazy que convierte a instancias, setter bloqueado
163
+ // ═══════════════════════════════════════════════════════════════
164
+ let relation_data = props[column_name] ?? undefined;
165
+ Object.defineProperty(this, column_name, {
166
+ enumerable: true,
167
+ configurable: true, // Permite reconfigurar cuando processIncludes carga datos
168
+ set: () => undefined, // Relaciones son read-only
169
+ get: () => {
170
+ if (relation_data === undefined)
171
+ return undefined;
172
+ const RelatedModel = column.store.relation.model();
173
+ const type = column.store.relation.type;
174
+ if (type === 'HasMany') {
175
+ // HasMany: Array de instancias
176
+ return [].concat(relation_data ?? [])
177
+ .filter(Boolean)
178
+ .map((item) => item instanceof RelatedModel ? item : new RelatedModel(item));
179
+ }
180
+ else {
181
+ // HasOne / BelongsTo: Instancia única o null
182
+ if (relation_data === null)
183
+ return null;
184
+ return relation_data instanceof RelatedModel
185
+ ? relation_data
186
+ : new RelatedModel(relation_data);
187
+ }
188
+ },
189
+ });
190
+ }
191
+ else {
192
+ // ═══════════════════════════════════════════════════════════════
193
+ // COLUMNAS REGULARES: getter/setter con pipelines via reduce
194
+ // ═══════════════════════════════════════════════════════════════
195
+ const has_initial_value = column_name in props;
196
+ let value = has_initial_value ? props[column_name] : null;
197
+ // Aplicar set pipeline en inicialización (solo si usuario proporciona valor)
198
+ if (has_initial_value) {
199
+ value = column.set.reduce((v, fn) => fn(v), value);
103
200
  }
201
+ // Almacenar en VALUES para compatibilidad con métodos existentes
202
+ this[decorator_1.VALUES][column_name] = value;
203
+ Object.defineProperty(this, column_name, {
204
+ enumerable: true,
205
+ configurable: true,
206
+ get: () => {
207
+ // Aplicar get pipeline
208
+ let v = column.get.reduce((val, fn) => fn(val), value);
209
+ // Almacenar si se generó default (para consistencia)
210
+ if (value === null && v !== null) {
211
+ value = v;
212
+ this[decorator_1.VALUES][column_name] = v;
213
+ }
214
+ return v;
215
+ },
216
+ set: (new_value) => {
217
+ // Aplicar set pipeline
218
+ value = column.set.reduce((v, fn) => fn(v), new_value ?? null);
219
+ this[decorator_1.VALUES][column_name] = value;
220
+ },
221
+ });
104
222
  }
105
- });
106
- Object.assign(this, data);
223
+ }
107
224
  }
108
- /** Serializar instancia a JSON plano */
225
+ // **Métodos de instancia con API preservada**
226
+ /**
227
+ * @description Serializa la instancia a JSON incluyendo relaciones recursivamente
228
+ * @returns Objeto JSON con todas las propiedades y relaciones
229
+ */
109
230
  toJSON() {
110
- const meta = (0, wrapper_1.mustMeta)(Object.getPrototypeOf(this).constructor);
231
+ const schema = this.constructor[decorator_1.SCHEMA];
111
232
  const result = {};
112
- meta.columns.forEach((column, key) => {
113
- if (typeof key === "string") {
114
- // Acceder a la propiedad directamente para activar getters virtuales
115
- const value = this[key];
116
- if (value !== undefined) {
117
- result[key] = value;
233
+ for (const column_name in schema.columns) {
234
+ const column = schema.columns[column_name];
235
+ const value = this[column_name];
236
+ if (value === null || value === undefined)
237
+ continue;
238
+ if (column.store?.relation) {
239
+ // Serializar relaciones recursivamente
240
+ if (Array.isArray(value)) {
241
+ result[column_name] = value.map(item => item?.toJSON ? item.toJSON() : item);
242
+ }
243
+ else {
244
+ result[column_name] = value?.toJSON ? value.toJSON() : value;
118
245
  }
119
246
  }
120
- });
121
- // Incluir propiedades enumerables ad-hoc no registradas como relaciones
122
- // Esto permite persistir campos opcionales sin decoradores (p.ej. 'notes')
123
- for (const key of Object.keys(this)) {
124
- if (result[key] !== undefined)
125
- continue;
126
- if (meta.relations.has && meta.relations.has(key))
127
- continue;
128
- const val = this[key];
129
- if (val === undefined)
130
- continue;
131
- if (typeof val === "function")
247
+ else {
248
+ result[column_name] = value;
249
+ }
250
+ }
251
+ return result;
252
+ }
253
+ /**
254
+ * @description Serializa la instancia a string JSON
255
+ * @returns String JSON con todas las propiedades y relaciones
256
+ */
257
+ toString() {
258
+ return JSON.stringify(this);
259
+ }
260
+ /**
261
+ * @description Genera payload para operaciones de DB (excluye relaciones)
262
+ */
263
+ _toDBPayload() {
264
+ const schema = this.constructor[decorator_1.SCHEMA];
265
+ const result = {};
266
+ for (const column_name in schema.columns) {
267
+ const column = schema.columns[column_name];
268
+ if (column.store?.relation)
132
269
  continue;
133
- result[key] = val;
270
+ const value = this[column_name];
271
+ if (value !== null && value !== undefined) {
272
+ result[column_name] = value;
273
+ }
134
274
  }
135
275
  return result;
136
276
  }
137
- /** Guardar instancia (crear o actualizar) */
138
277
  async save() {
139
- const id = this.id;
140
- const Ctor = this.constructor;
141
- const meta = (0, wrapper_1.mustMeta)(Ctor);
142
- const now = new Date().toISOString();
143
- const isNew = id === undefined || id === null;
144
- // Actualizar campos de timestamp
145
- meta.columns.forEach((col, key) => {
146
- if (col.createdAt && isNew) {
147
- // Solo establecer createdAt si es un nuevo registro
148
- this[key] = now;
278
+ const schema = this.constructor[decorator_1.SCHEMA];
279
+ const id = this[decorator_1.VALUES][schema.primary_key];
280
+ const is_new = id === undefined || id === null;
281
+ // Validación lazy
282
+ for (const column_name in schema.columns) {
283
+ const column = schema.columns[column_name];
284
+ if (column.store?.lazy_validators && !column.store?.relation) {
285
+ const value = this[decorator_1.VALUES][column_name];
286
+ for (const validator of column.store.lazy_validators) {
287
+ const result = validator(value);
288
+ if (result !== true) {
289
+ throw new Error(typeof result === "string" ? result : "Validación fallida");
290
+ }
291
+ }
149
292
  }
150
- else if (col.updatedAt) {
151
- // Actualizar updatedAt en cada guardado
152
- this[key] = now;
293
+ }
294
+ // Timestamps automáticos (updatedAt solamente, createdAt se genera via pipeline)
295
+ const now = new Date().toISOString();
296
+ for (const column_name in schema.columns) {
297
+ const column = schema.columns[column_name];
298
+ if (column.store?.updatedAt) {
299
+ // Usar setter para sincronizar closure y VALUES
300
+ this[column_name] = now;
153
301
  }
154
- });
155
- const payload = this.toJSON();
156
- const client = (0, client_1.requireClient)();
157
- if (isNew) {
158
- // Crear nuevo registro
159
- const created = await Ctor.create(payload);
160
- Object.assign(this, created);
302
+ }
303
+ // Preparar datos para DB (omitir relaciones)
304
+ const db_data = this._toDBPayload();
305
+ if (is_new) {
306
+ // Insertar nuevo registro
307
+ await this.insertIntoDynamoDB(db_data);
161
308
  }
162
309
  else {
163
310
  // Actualizar registro existente
164
- await client.send(new client_dynamodb_1.PutItemCommand({
165
- TableName: meta.name,
166
- Item: (0, util_dynamodb_1.marshall)(payload, { removeUndefinedValues: true }),
167
- }));
311
+ await this.updateInDynamoDB(db_data);
168
312
  }
169
313
  return this;
170
314
  }
171
- /** Actualizar instancia */
172
- async update(patch) {
173
- Object.assign(this, patch);
174
- return await this.save();
175
- }
176
- /** Eliminar instancia */
177
315
  async destroy() {
178
- const id = this.id;
179
- if (id === undefined || id === null) {
180
- throw new Error("destroy() requiere que la instancia tenga un id");
316
+ const schema = this.constructor[decorator_1.SCHEMA];
317
+ const id = this[decorator_1.VALUES][schema.primary_key];
318
+ if (!id) {
319
+ throw new Error('Cannot destroy record without ID');
181
320
  }
182
- const Ctor = this.constructor;
183
- return await Ctor.delete({ id: String(id) });
321
+ // Buscar si hay soft delete configurado
322
+ let softDeleteColumn = null;
323
+ for (const column_name in schema.columns) {
324
+ const column = schema.columns[column_name];
325
+ if (column.store?.softDelete) {
326
+ softDeleteColumn = column_name;
327
+ break;
328
+ }
329
+ }
330
+ if (softDeleteColumn) {
331
+ // Soft delete
332
+ this[decorator_1.VALUES][softDeleteColumn] = new Date().toISOString();
333
+ await this.save();
334
+ }
335
+ else {
336
+ // Hard delete
337
+ await this.deleteFromDynamoDB(id);
338
+ }
339
+ return null;
184
340
  }
185
- // ===========================================================================
186
- // MÉTODOS ESTÁTICOS SEGÚN ESPECIFICACIONES
187
- // ===========================================================================
188
- /**
189
- * Crear un nuevo registro en la base de datos
190
- */
191
- static async create(data) {
192
- const client = (0, client_1.requireClient)();
193
- const meta = (0, wrapper_1.mustMeta)(this);
341
+ async forceDestroy() {
342
+ const schema = this.constructor[decorator_1.SCHEMA];
343
+ const id = this[decorator_1.VALUES][schema.primary_key];
344
+ if (!id) {
345
+ throw new Error('Cannot destroy record without ID');
346
+ }
347
+ await this.deleteFromDynamoDB(id);
348
+ return null;
349
+ }
350
+ // **Métodos estáticos con API preservada**
351
+ static async create(data, tx) {
194
352
  const instance = new this(data);
195
353
  // Establecer timestamps si corresponde
354
+ const schema = this[decorator_1.SCHEMA];
196
355
  const now = new Date().toISOString();
197
- meta.columns.forEach((col, key) => {
198
- if (col.createdAt && instance[key] === undefined) {
199
- instance[key] = now;
200
- }
201
- if (col.updatedAt) {
202
- instance[key] = now;
356
+ // Solo updatedAt, createdAt se genera via pipeline
357
+ for (const column_name in schema.columns) {
358
+ const column = schema.columns[column_name];
359
+ if (column.store?.updatedAt) {
360
+ // Usar setter para sincronizar closure y VALUES
361
+ instance[column_name] = now;
203
362
  }
204
- });
205
- const payload = instance.toJSON();
206
- await client.send(new client_dynamodb_1.PutItemCommand({
207
- TableName: meta.name,
208
- Item: (0, util_dynamodb_1.marshall)(payload, { removeUndefinedValues: true }),
209
- }));
363
+ }
364
+ const payload = instance._toDBPayload();
365
+ if (tx) {
366
+ tx.addPut(schema.name, payload);
367
+ }
368
+ else {
369
+ const client = (0, client_1.requireClient)();
370
+ await client.send(new client_dynamodb_1.PutItemCommand({
371
+ TableName: schema.name,
372
+ Item: (0, util_dynamodb_1.marshall)(payload, { removeUndefinedValues: true }),
373
+ }));
374
+ }
210
375
  return instance;
211
376
  }
212
- /**
213
- * Actualizar registros en la base de datos
214
- * @param updates - Campos a actualizar. Los campos con valor `undefined` se ignoran.
215
- * @param filters - Filtros para seleccionar los registros a actualizar
216
- * @returns Número de registros actualizados
217
- */
218
- static async update(updates, filters) {
219
- // Obtener metadatos del modelo para manejar timestamps
220
- const meta = (0, wrapper_1.mustMeta)(this);
221
- const client = (0, client_1.requireClient)();
222
- // Buscar registros que coincidan con los filtros
223
- const recordsToUpdate = await this.where(filters);
224
- if (recordsToUpdate.length === 0) {
377
+ static async update(updates, filters, tx) {
378
+ const records = await this.where(filters);
379
+ if (records.length === 0) {
225
380
  return 0;
226
381
  }
227
- // No filtrar campos undefined aquí para permitir establecer valores nulos/explicitos
228
- const cleanUpdates = { ...updates };
229
- // Verificar si hay campos para actualizar
230
- if (Object.keys(cleanUpdates).length === 0) {
231
- return recordsToUpdate.length; // No hay cambios que hacer
232
- }
233
- // Actualizar cada registro
234
- const updatePromises = recordsToUpdate.map(async (record) => {
235
- // Obtener datos actuales
236
- const currentData = record.toJSON();
237
- // Aplicar actualizaciones, preservando valores existentes
238
- const updatedData = { ...currentData };
239
- // Aplicar solo los campos que no son undefined
240
- for (const [key, value] of Object.entries(cleanUpdates)) {
241
- if (value !== undefined) {
242
- updatedData[key] = value;
243
- }
382
+ let updatedCount = 0;
383
+ const schema = this[decorator_1.SCHEMA];
384
+ const client = tx ? null : (0, client_1.requireClient)();
385
+ for (const record of records) {
386
+ // Aplicar actualizaciones
387
+ for (const [key, value] of Object.entries(updates)) {
388
+ record[key] = value;
244
389
  }
245
- // Actualizar timestamp updated_at si está configurado
246
- let updatedAtKey = undefined;
247
- for (const [k, col] of meta.columns.entries()) {
248
- if (col.updatedAt === true) {
249
- updatedAtKey = k;
390
+ // Actualizar timestamp
391
+ const now = new Date().toISOString();
392
+ for (const column_name in schema.columns) {
393
+ const column = schema.columns[column_name];
394
+ if (column.store?.updatedAt) {
395
+ // Usar setter para sincronizar closure y VALUES
396
+ record[column_name] = now;
250
397
  break;
251
398
  }
252
399
  }
253
- if (updatedAtKey !== undefined) {
254
- updatedData[String(updatedAtKey)] = new Date().toISOString();
400
+ const updated_data = record._toDBPayload();
401
+ if (tx) {
402
+ tx.addPut(schema.name, updated_data);
255
403
  }
256
- // Actualizar en la base de datos
257
- await client.send(new client_dynamodb_1.PutItemCommand({
258
- TableName: meta.name,
259
- Item: (0, util_dynamodb_1.marshall)(updatedData, { removeUndefinedValues: true }),
260
- }));
261
- });
262
- await Promise.all(updatePromises);
263
- return recordsToUpdate.length;
404
+ else {
405
+ await client.send(new client_dynamodb_1.PutItemCommand({
406
+ TableName: schema.name,
407
+ Item: (0, util_dynamodb_1.marshall)(updated_data, { removeUndefinedValues: true }),
408
+ }));
409
+ }
410
+ updatedCount++;
411
+ }
412
+ return updatedCount;
264
413
  }
265
- /**
266
- * Eliminar registros de la base de datos
267
- */
268
- static async delete(filters) {
269
- const recordsToDelete = await this.where(filters);
270
- if (recordsToDelete.length === 0) {
414
+ static async delete(filters, tx) {
415
+ const records = await this.where(filters);
416
+ if (records.length === 0) {
271
417
  return 0;
272
418
  }
273
- const client = (0, client_1.requireClient)();
274
- const meta = (0, wrapper_1.mustMeta)(this);
275
- const deletePromises = recordsToDelete.map(async (record) => {
276
- const id = record.id;
419
+ const schema = this[decorator_1.SCHEMA];
420
+ const client = tx ? null : (0, client_1.requireClient)();
421
+ let deletedCount = 0;
422
+ for (const record of records) {
423
+ const id = record[decorator_1.VALUES][schema.primary_key];
277
424
  if (id) {
278
- await client.send(new client_dynamodb_1.DeleteItemCommand({
279
- TableName: meta.name,
280
- Key: (0, util_dynamodb_1.marshall)({ id: String(id) }),
281
- }));
425
+ if (tx) {
426
+ tx.addDelete(schema.name, { [schema.primary_key]: id });
427
+ }
428
+ else {
429
+ await client.send(new client_dynamodb_1.DeleteItemCommand({
430
+ TableName: schema.name,
431
+ Key: (0, util_dynamodb_1.marshall)({ [schema.primary_key]: id }),
432
+ }));
433
+ }
434
+ deletedCount++;
282
435
  }
283
- });
284
- await Promise.all(deletePromises);
285
- return recordsToDelete.length;
436
+ }
437
+ return deletedCount;
286
438
  }
287
- /** Implementación del método where */
288
- static async where(...args) {
439
+ static async where(field_or_filters, operator_or_value, value, options) {
289
440
  const client = (0, client_1.requireClient)();
290
- const meta = (0, wrapper_1.mustMeta)(this);
441
+ const schema = this[decorator_1.SCHEMA];
442
+ // Parsear argumentos dinámicamente (como el original)
291
443
  let filters;
292
- let options = {};
444
+ let queryOptions;
293
445
  let operator = "=";
294
- // Parsear argumentos según la sobrecarga utilizada
295
- if (args.length === 2) {
296
- if (typeof args[0] === "string") {
297
- // where(field, value) - check if value is array for IN operation
298
- const value = args[1];
299
- if (Array.isArray(value)) {
300
- operator = "in";
301
- }
302
- filters = { [args[0]]: value };
303
- }
304
- else {
305
- // where(filters, options)
306
- filters = args[0];
307
- options = args[1] || {};
308
- }
446
+ if (typeof operator_or_value === 'string') {
447
+ // where(field, operator, value, options?)
448
+ operator = validateOperator(operator_or_value);
449
+ filters = { [field_or_filters]: value };
450
+ queryOptions = options || {};
309
451
  }
310
- else if (args.length === 3) {
311
- // where(field, operator, value)
312
- const [field, op, value] = args;
313
- operator = validateOperator(op);
314
- filters = { [field]: value };
452
+ else if (value !== undefined) {
453
+ // where(field, value, options?)
454
+ filters = { [field_or_filters]: Array.isArray(operator_or_value) ? { in: operator_or_value } : operator_or_value };
455
+ queryOptions = value || {};
315
456
  }
316
- else if (args.length === 1) {
317
- // where(filters)
318
- filters = args[0];
457
+ else if (operator_or_value !== undefined && typeof operator_or_value === 'object') {
458
+ // where(filters, options?)
459
+ filters = field_or_filters;
460
+ queryOptions = operator_or_value;
319
461
  }
320
462
  else {
321
- throw new Error("Argumentos inválidos para where()");
463
+ // where(filters?)
464
+ filters = field_or_filters;
465
+ queryOptions = {};
322
466
  }
323
- // Validar límites y opciones
324
- if (options.limit && options.limit < 0) {
467
+ // Validar opciones
468
+ if (queryOptions.limit === 0)
469
+ return [];
470
+ if (queryOptions.limit && queryOptions.limit < 0) {
325
471
  throw new Error("limit debe ser mayor o igual a 0");
326
472
  }
327
- if (options.skip && options.skip < 0) {
473
+ if (queryOptions.skip && queryOptions.skip < 0) {
328
474
  throw new Error("skip debe ser mayor o igual a 0");
329
475
  }
330
- if (options.order && !["ASC", "DESC"].includes(options.order)) {
476
+ if (queryOptions.order && !["ASC", "DESC"].includes(queryOptions.order)) {
331
477
  throw new Error('order debe ser "ASC" o "DESC"');
332
478
  }
333
- // Construir la consulta DynamoDB
479
+ // Auto-excluir soft deleted
480
+ if (!queryOptions._includeTrashed) {
481
+ for (const column_name in schema.columns) {
482
+ const column = schema.columns[column_name];
483
+ if (column.store?.softDelete && !(column_name in filters)) {
484
+ filters[column_name] = null;
485
+ break;
486
+ }
487
+ }
488
+ }
489
+ // Construir consulta DynamoDB
334
490
  const { expression, names, values } = buildConditionExpression(filters, operator);
335
491
  const scanParams = {
336
- TableName: meta.name,
492
+ TableName: schema.name,
493
+ ExpressionAttributeNames: names || {},
337
494
  };
338
- // Initialize ExpressionAttributeNames to avoid conflicts
339
- if (!scanParams.ExpressionAttributeNames) {
340
- scanParams.ExpressionAttributeNames = {};
341
- }
342
495
  if (expression) {
343
496
  scanParams.FilterExpression = expression;
344
- // Merge filter attribute names
345
- Object.assign(scanParams.ExpressionAttributeNames, names);
346
- scanParams.ExpressionAttributeValues = (0, util_dynamodb_1.marshall)(values);
497
+ scanParams.ExpressionAttributeValues = (0, util_dynamodb_1.marshall)(values || {}, { removeUndefinedValues: true });
347
498
  }
348
- if (options.attributes) {
349
- // Handle projection attributes with proper aliases
350
- const projectionExpressions = options.attributes.map((attr, index) => {
499
+ // Proyección solo si hay atributos específicos (ignorar array vacío)
500
+ if (queryOptions.attributes && queryOptions.attributes.length > 0) {
501
+ const projectionExpressions = queryOptions.attributes.map((attr, index) => {
351
502
  const aliasKey = `#proj${index}`;
352
- scanParams.ExpressionAttributeNames[aliasKey] = attr;
503
+ scanParams.ExpressionAttributeNames[aliasKey] = String(attr);
353
504
  return aliasKey;
354
505
  });
355
506
  scanParams.ProjectionExpression = projectionExpressions.join(", ");
356
507
  }
508
+ // Limpiar ExpressionAttributeNames si está vacío (DynamoDB lo rechaza)
509
+ if (Object.keys(scanParams.ExpressionAttributeNames).length === 0) {
510
+ delete scanParams.ExpressionAttributeNames;
511
+ }
357
512
  // Ejecutar consulta con paginación
358
513
  let allItems = [];
359
514
  let lastEvaluatedKey = undefined;
360
515
  let scannedCount = 0;
361
- const targetSkip = options.skip || 0;
362
- const targetLimit = options.limit || 100;
516
+ const targetSkip = queryOptions.skip || 0;
517
+ const targetLimit = queryOptions.limit ?? 100;
363
518
  do {
364
519
  if (lastEvaluatedKey) {
365
520
  scanParams.ExclusiveStartKey = lastEvaluatedKey;
366
521
  }
367
522
  const result = await client.send(new client_dynamodb_1.ScanCommand(scanParams));
368
523
  if (result.Items) {
369
- const items = result.Items.map((item) => (0, util_dynamodb_1.unmarshall)(item));
524
+ const items = result.Items.map((item) => {
525
+ const raw = (0, util_dynamodb_1.unmarshall)(item);
526
+ // Limpiar null/undefined
527
+ for (const k of Object.keys(raw)) {
528
+ if (raw[k] === null || raw[k] === undefined) {
529
+ delete raw[k];
530
+ }
531
+ }
532
+ return raw;
533
+ });
370
534
  for (const item of items) {
371
535
  if (scannedCount < targetSkip) {
372
536
  scannedCount++;
373
537
  continue;
374
538
  }
375
- if (allItems.length >= targetLimit) {
539
+ if (allItems.length >= targetLimit)
376
540
  break;
377
- }
378
541
  allItems.push(item);
379
542
  scannedCount++;
380
543
  }
@@ -382,77 +545,114 @@ class Table {
382
545
  lastEvaluatedKey = result.LastEvaluatedKey;
383
546
  } while (lastEvaluatedKey && allItems.length < targetLimit);
384
547
  // Aplicar ordenamiento
385
- if (options.order) {
548
+ if (queryOptions.order) {
386
549
  allItems.sort((a, b) => {
387
- const sortField = Object.keys(filters)[0] || "id";
550
+ const sortField = Object.keys(filters)[0] || schema.primary_key;
388
551
  const aVal = a[sortField];
389
552
  const bVal = b[sortField];
390
553
  if (aVal < bVal)
391
- return options.order === "ASC" ? -1 : 1;
554
+ return queryOptions.order === "ASC" ? -1 : 1;
392
555
  if (aVal > bVal)
393
- return options.order === "ASC" ? 1 : -1;
556
+ return queryOptions.order === "ASC" ? 1 : -1;
394
557
  return 0;
395
558
  });
396
559
  }
397
- // Convertir a instancias del modelo
560
+ // Convertir a instancias (schema ya definido arriba)
398
561
  const instances = allItems.map((item) => {
399
- if (options.attributes) {
400
- // When using attribute selection, create minimal instances
401
- // only with the requested fields to avoid default value population
562
+ if (queryOptions.attributes) {
563
+ // Proyección parcial: crear instancia con solo los atributos solicitados
402
564
  const instance = Object.create(this.prototype);
403
- Object.assign(instance, item);
565
+ instance[decorator_1.VALUES] = item;
566
+ // Definir getters para los atributos proyectados
567
+ for (const attr of queryOptions.attributes) {
568
+ const column = schema.columns[attr];
569
+ if (column) {
570
+ Object.defineProperty(instance, attr, {
571
+ enumerable: true,
572
+ configurable: true,
573
+ get: () => {
574
+ let value = instance[decorator_1.VALUES][attr] ?? null;
575
+ // Aplicar get pipeline
576
+ for (const fn of column.get) {
577
+ value = fn(value);
578
+ }
579
+ return value;
580
+ }
581
+ });
582
+ }
583
+ }
404
584
  return instance;
405
585
  }
406
586
  else {
407
- // Normal instantiation with all defaults
408
587
  return new this(item);
409
588
  }
410
589
  });
411
- // Procesar includes si están presentes
412
- if (options.include) {
413
- return await (0, relations_1.processIncludes)(this, instances, options.include);
590
+ // Procesar includes para cargar relaciones
591
+ if (queryOptions.include) {
592
+ await (0, relations_1.processIncludes)(instances, queryOptions.include, this);
414
593
  }
415
594
  return instances;
416
595
  }
417
- static async first(...args) {
418
- const results = await this.where(...args);
419
- return results[0] || undefined;
596
+ static async first(field_or_filters, operator_or_value, value_or_options) {
597
+ const results = await this.where(field_or_filters, operator_or_value, value_or_options, { limit: 1 });
598
+ return results[0];
420
599
  }
421
- static async last(...args) {
422
- // Soporte de firmas sin generar una cuarta sobrecarga inválida para where()
423
- if (args.length === 0) {
424
- const results = await this.where({}, { order: "DESC", limit: 1 });
425
- return results[0] || undefined;
426
- }
427
- if (args.length === 1) {
428
- if (typeof args[0] === "object" && !Array.isArray(args[0])) {
429
- const results = await this.where(args[0], {
430
- order: "DESC",
431
- limit: 1,
432
- });
433
- return results[0] || undefined;
600
+ static async last(field_or_filters, operator_or_value) {
601
+ if (field_or_filters === undefined) {
602
+ const results = await this.where({}, { order: 'DESC', limit: 1 });
603
+ return results[0];
604
+ }
605
+ else {
606
+ const results = await this.where(field_or_filters, operator_or_value, undefined, { order: 'DESC', limit: 1 });
607
+ return results[0];
608
+ }
609
+ }
610
+ static async withTrashed(filters, options) {
611
+ return await this.where(filters ?? {}, { ...options, _includeTrashed: true });
612
+ }
613
+ static async onlyTrashed(filters, options) {
614
+ const schema = this[decorator_1.SCHEMA];
615
+ let softDeleteColumn = null;
616
+ for (const column_name in schema.columns) {
617
+ const column = schema.columns[column_name];
618
+ if (column.store?.softDelete) {
619
+ softDeleteColumn = column_name;
620
+ break;
434
621
  }
435
- throw new Error("Se requiere un valor para el campo de filtro");
436
- }
437
- if (args.length === 2) {
438
- // field, value
439
- const results = await this.where(args[0], args[1]);
440
- return results[results.length - 1];
441
- }
442
- if (args.length === 3) {
443
- // field, operator, value
444
- const results = await this.where(args[0], args[1], args[2]);
445
- return results[results.length - 1];
446
- }
447
- if (args.length === 2 && typeof args[1] === "object") {
448
- const results = await this.where(args[0], {
449
- ...(args[1] || {}),
450
- order: "DESC",
451
- limit: 1,
452
- });
453
- return results[0] || undefined;
454
622
  }
455
- throw new Error("Argumentos no válidos para last()");
623
+ if (!softDeleteColumn) {
624
+ throw new Error("onlyTrashed() requiere un campo @SoftDelete configurado");
625
+ }
626
+ const merged_filters = {
627
+ ...filters,
628
+ [softDeleteColumn]: { "!=": null },
629
+ };
630
+ return await this.where(merged_filters, { ...options, _includeTrashed: true });
631
+ }
632
+ // **Métodos privados para DynamoDB**
633
+ async insertIntoDynamoDB(data) {
634
+ const client = (0, client_1.requireClient)();
635
+ const schema = this.constructor[decorator_1.SCHEMA];
636
+ await client.send(new client_dynamodb_1.PutItemCommand({
637
+ TableName: schema.name,
638
+ Item: (0, util_dynamodb_1.marshall)(data, { removeUndefinedValues: true }),
639
+ }));
640
+ }
641
+ async updateInDynamoDB(data) {
642
+ const client = (0, client_1.requireClient)();
643
+ const schema = this.constructor[decorator_1.SCHEMA];
644
+ await client.send(new client_dynamodb_1.PutItemCommand({
645
+ TableName: schema.name,
646
+ Item: (0, util_dynamodb_1.marshall)(data, { removeUndefinedValues: true }),
647
+ }));
648
+ }
649
+ async deleteFromDynamoDB(id) {
650
+ const client = (0, client_1.requireClient)();
651
+ const schema = this.constructor[decorator_1.SCHEMA];
652
+ await client.send(new client_dynamodb_1.DeleteItemCommand({
653
+ TableName: schema.name,
654
+ Key: (0, util_dynamodb_1.marshall)({ [schema.primary_key]: id }),
655
+ }));
456
656
  }
457
657
  }
458
658
  exports.default = Table;