@arcaelas/dynamite 1.0.25 → 2.1.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/README.md +13 -13
- package/{src → build/cjs}/@types/index.d.ts +26 -1
- package/build/cjs/@types/index.js.map +1 -0
- package/{src → build/cjs}/core/client.d.ts +22 -19
- package/build/cjs/core/client.js +384 -0
- package/build/cjs/core/client.js.map +1 -0
- package/build/cjs/core/decorator.d.ts +38 -0
- package/build/cjs/core/decorator.js +38 -0
- package/build/cjs/core/decorator.js.map +1 -0
- package/build/cjs/core/table.d.ts +55 -0
- package/build/cjs/core/table.js +851 -0
- package/build/cjs/core/table.js.map +1 -0
- package/build/cjs/decorators/indexes.d.ts +20 -0
- package/build/cjs/decorators/indexes.js +45 -0
- package/build/cjs/decorators/indexes.js.map +1 -0
- package/{src → build/cjs}/decorators/relations.d.ts +8 -7
- package/{src → build/cjs}/decorators/relations.js +9 -8
- package/build/cjs/decorators/relations.js.map +1 -0
- package/build/cjs/decorators/timestamps.d.ts +20 -0
- package/build/cjs/decorators/timestamps.js +34 -0
- package/build/cjs/decorators/timestamps.js.map +1 -0
- package/build/cjs/decorators/transforms.d.ts +41 -0
- package/build/cjs/decorators/transforms.js +98 -0
- package/build/cjs/decorators/transforms.js.map +1 -0
- package/{src → build/cjs}/index.d.ts +5 -3
- package/{src → build/cjs}/index.js +7 -6
- package/build/cjs/index.js.map +1 -0
- package/build/cjs/package.json +1 -0
- package/build/cjs/test/basic.d.ts +1 -0
- package/build/cjs/test/basic.js +248 -0
- package/build/cjs/test/basic.js.map +1 -0
- package/build/cjs/test/bulk.d.ts +1 -0
- package/build/cjs/test/bulk.js +108 -0
- package/build/cjs/test/bulk.js.map +1 -0
- package/build/cjs/test/contracts.d.ts +1 -0
- package/build/cjs/test/contracts.js +343 -0
- package/build/cjs/test/contracts.js.map +1 -0
- package/build/cjs/test/filters.d.ts +1 -0
- package/build/cjs/test/filters.js +190 -0
- package/build/cjs/test/filters.js.map +1 -0
- package/build/cjs/test/index.d.ts +1 -0
- package/build/cjs/test/index.js +36 -0
- package/build/cjs/test/index.js.map +1 -0
- package/build/cjs/test/query_scan.d.ts +1 -0
- package/build/cjs/test/query_scan.js +195 -0
- package/build/cjs/test/query_scan.js.map +1 -0
- package/build/cjs/test/relations.d.ts +1 -0
- package/build/cjs/test/relations.js +246 -0
- package/build/cjs/test/relations.js.map +1 -0
- package/build/cjs/test/transactions.d.ts +1 -0
- package/build/cjs/test/transactions.js +145 -0
- package/build/cjs/test/transactions.js.map +1 -0
- package/{src → build/cjs}/utils/relations.d.ts +1 -0
- package/{src → build/cjs}/utils/relations.js +5 -10
- package/build/cjs/utils/relations.js.map +1 -0
- package/build/cjs/utils/ulid.d.ts +10 -0
- package/build/cjs/utils/ulid.js +55 -0
- package/build/cjs/utils/ulid.js.map +1 -0
- package/build/esm/@types/index.d.ts +213 -0
- package/build/esm/@types/index.js +8 -0
- package/build/esm/@types/index.js.map +1 -0
- package/build/esm/core/client.d.ts +96 -0
- package/build/esm/core/client.js +375 -0
- package/build/esm/core/client.js.map +1 -0
- package/build/esm/core/decorator.d.ts +38 -0
- package/build/esm/core/decorator.js +34 -0
- package/build/esm/core/decorator.js.map +1 -0
- package/build/esm/core/table.d.ts +55 -0
- package/build/esm/core/table.js +848 -0
- package/build/esm/core/table.js.map +1 -0
- package/build/esm/decorators/indexes.d.ts +20 -0
- package/build/esm/decorators/indexes.js +42 -0
- package/build/esm/decorators/indexes.js.map +1 -0
- package/build/esm/decorators/relations.d.ts +75 -0
- package/build/esm/decorators/relations.js +112 -0
- package/build/esm/decorators/relations.js.map +1 -0
- package/build/esm/decorators/timestamps.d.ts +20 -0
- package/build/esm/decorators/timestamps.js +31 -0
- package/build/esm/decorators/timestamps.js.map +1 -0
- package/build/esm/decorators/transforms.d.ts +41 -0
- package/build/esm/decorators/transforms.js +92 -0
- package/build/esm/decorators/transforms.js.map +1 -0
- package/build/esm/index.d.ts +17 -0
- package/build/esm/index.js +24 -0
- package/build/esm/index.js.map +1 -0
- package/build/esm/package.json +1 -0
- package/build/esm/test/basic.d.ts +1 -0
- package/build/esm/test/basic.js +245 -0
- package/build/esm/test/basic.js.map +1 -0
- package/build/esm/test/bulk.d.ts +1 -0
- package/build/esm/test/bulk.js +105 -0
- package/build/esm/test/bulk.js.map +1 -0
- package/build/esm/test/contracts.d.ts +1 -0
- package/build/esm/test/contracts.js +340 -0
- package/build/esm/test/contracts.js.map +1 -0
- package/build/esm/test/filters.d.ts +1 -0
- package/build/esm/test/filters.js +187 -0
- package/build/esm/test/filters.js.map +1 -0
- package/build/esm/test/index.d.ts +1 -0
- package/build/esm/test/index.js +31 -0
- package/build/esm/test/index.js.map +1 -0
- package/build/esm/test/query_scan.d.ts +1 -0
- package/build/esm/test/query_scan.js +192 -0
- package/build/esm/test/query_scan.js.map +1 -0
- package/build/esm/test/relations.d.ts +1 -0
- package/build/esm/test/relations.js +243 -0
- package/build/esm/test/relations.js.map +1 -0
- package/build/esm/test/transactions.d.ts +1 -0
- package/build/esm/test/transactions.js +142 -0
- package/build/esm/test/transactions.js.map +1 -0
- package/build/esm/utils/relations.d.ts +42 -0
- package/build/esm/utils/relations.js +207 -0
- package/build/esm/utils/relations.js.map +1 -0
- package/build/esm/utils/ulid.d.ts +10 -0
- package/build/esm/utils/ulid.js +52 -0
- package/build/esm/utils/ulid.js.map +1 -0
- package/package.json +31 -9
- package/src/core/client.js +0 -294
- package/src/core/decorator.d.ts +0 -29
- package/src/core/decorator.js +0 -103
- package/src/core/table.d.ts +0 -81
- package/src/core/table.js +0 -892
- package/src/decorators/indexes.d.ts +0 -38
- package/src/decorators/indexes.js +0 -59
- package/src/decorators/timestamps.d.ts +0 -54
- package/src/decorators/timestamps.js +0 -72
- package/src/decorators/transforms.d.ts +0 -99
- package/src/decorators/transforms.js +0 -166
- /package/{src → build/cjs}/@types/index.js +0 -0
|
@@ -0,0 +1,851 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
/**
|
|
4
|
+
* @file table.ts
|
|
5
|
+
* @description Tabla autocontenida con arquitectura minimalista y Symbol storage
|
|
6
|
+
* @autor Miguel Alejandro
|
|
7
|
+
* @fecha 2025-01-28
|
|
8
|
+
*/
|
|
9
|
+
const client_dynamodb_1 = require("@aws-sdk/client-dynamodb");
|
|
10
|
+
const util_dynamodb_1 = require("@aws-sdk/util-dynamodb");
|
|
11
|
+
const relations_1 = require("../utils/relations");
|
|
12
|
+
const client_1 = require("./client");
|
|
13
|
+
const decorator_1 = require("./decorator");
|
|
14
|
+
const OP_MAP = {
|
|
15
|
+
'=': '=', '<>': '<>', '!=': '<>', '<': '<', '<=': '<=', '>': '>', '>=': '>=',
|
|
16
|
+
in: 'in', include: 'include', contains: 'include',
|
|
17
|
+
$eq: '=', $ne: '<>', $lt: '<', $lte: '<=', $gt: '>', $gte: '>=',
|
|
18
|
+
$in: 'in', $include: 'include', $contains: 'include',
|
|
19
|
+
$exists: 'attribute_exists', $notExists: 'attribute_not_exists',
|
|
20
|
+
};
|
|
21
|
+
const OPERATORS = new Set(Object.keys(OP_MAP));
|
|
22
|
+
class Table {
|
|
23
|
+
constructor(props = {}) {
|
|
24
|
+
(0, client_1.requireClient)();
|
|
25
|
+
const schema = this.constructor[decorator_1.SCHEMA];
|
|
26
|
+
// Flag de persistencia con closure (no enumerable)
|
|
27
|
+
let __isPersisted = false;
|
|
28
|
+
Object.defineProperty(this, "__isPersisted", {
|
|
29
|
+
enumerable: false,
|
|
30
|
+
configurable: false,
|
|
31
|
+
get: () => __isPersisted,
|
|
32
|
+
set: (v) => {
|
|
33
|
+
__isPersisted = v;
|
|
34
|
+
},
|
|
35
|
+
});
|
|
36
|
+
for (const column_name in schema.columns) {
|
|
37
|
+
const column = schema.columns[column_name];
|
|
38
|
+
if (column.store?.relation) {
|
|
39
|
+
let relation_value = props[column_name] ?? undefined;
|
|
40
|
+
let cached = undefined;
|
|
41
|
+
let dirty = true;
|
|
42
|
+
Object.defineProperty(this, column_name, {
|
|
43
|
+
enumerable: true,
|
|
44
|
+
configurable: true,
|
|
45
|
+
set: (v) => {
|
|
46
|
+
relation_value = v;
|
|
47
|
+
dirty = true;
|
|
48
|
+
},
|
|
49
|
+
get: () => {
|
|
50
|
+
if (relation_value === undefined)
|
|
51
|
+
return undefined;
|
|
52
|
+
if (!dirty)
|
|
53
|
+
return cached;
|
|
54
|
+
const RelatedModel = column.store.relation.model();
|
|
55
|
+
const type = column.store.relation.type;
|
|
56
|
+
if (type === "HasMany" || type === "ManyToMany") {
|
|
57
|
+
cached = []
|
|
58
|
+
.concat(relation_value ?? [])
|
|
59
|
+
.filter(Boolean)
|
|
60
|
+
.map((item) => item instanceof RelatedModel ? item : new RelatedModel(item));
|
|
61
|
+
}
|
|
62
|
+
else {
|
|
63
|
+
if (relation_value === null) {
|
|
64
|
+
cached = null;
|
|
65
|
+
dirty = false;
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
cached =
|
|
69
|
+
relation_value instanceof RelatedModel
|
|
70
|
+
? relation_value
|
|
71
|
+
: new RelatedModel(relation_value);
|
|
72
|
+
}
|
|
73
|
+
dirty = false;
|
|
74
|
+
return cached;
|
|
75
|
+
},
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
else {
|
|
79
|
+
// Columna normal con closure
|
|
80
|
+
let value = props[column_name];
|
|
81
|
+
Object.defineProperty(this, column_name, {
|
|
82
|
+
enumerable: true,
|
|
83
|
+
configurable: true,
|
|
84
|
+
get: () => {
|
|
85
|
+
const computed = column.get.reduce((v, fn) => fn(v), value);
|
|
86
|
+
// Cache default values so they don't regenerate on each access
|
|
87
|
+
if (value == null && computed != null) {
|
|
88
|
+
value = computed;
|
|
89
|
+
}
|
|
90
|
+
return computed;
|
|
91
|
+
},
|
|
92
|
+
set: (next) => {
|
|
93
|
+
value = column.set.reduce((accumulated, fn) => fn(accumulated, value), next);
|
|
94
|
+
},
|
|
95
|
+
});
|
|
96
|
+
this[column_name] = props[column_name];
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
toJSON() {
|
|
101
|
+
const schema = this.constructor[decorator_1.SCHEMA];
|
|
102
|
+
const result = {};
|
|
103
|
+
for (const column_name in schema.columns) {
|
|
104
|
+
const column = schema.columns[column_name];
|
|
105
|
+
const value = this[column_name];
|
|
106
|
+
if (value === null || value === undefined)
|
|
107
|
+
continue;
|
|
108
|
+
if (column.store?.relation) {
|
|
109
|
+
result[column_name] = Array.isArray(value)
|
|
110
|
+
? value.map((item) => (item?.toJSON ? item.toJSON() : item))
|
|
111
|
+
: value?.toJSON
|
|
112
|
+
? value.toJSON()
|
|
113
|
+
: value;
|
|
114
|
+
}
|
|
115
|
+
else {
|
|
116
|
+
result[column_name] = value;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
return result;
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* @description Convierte la instancia a un payload listo para DynamoDB
|
|
123
|
+
* @returns Objeto con nombres de columnas de DB y valores apropiados
|
|
124
|
+
*/
|
|
125
|
+
_toDBPayload() {
|
|
126
|
+
const schema = this.constructor[decorator_1.SCHEMA];
|
|
127
|
+
const payload = {};
|
|
128
|
+
for (const prop_name in schema.columns) {
|
|
129
|
+
const column = schema.columns[prop_name];
|
|
130
|
+
// Skip relations
|
|
131
|
+
if (column.store?.relation) {
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
const value = this[prop_name];
|
|
135
|
+
const db_name = column.name || prop_name;
|
|
136
|
+
// Skip undefined values (DynamoDB doesn't support them)
|
|
137
|
+
if (value !== undefined) {
|
|
138
|
+
payload[db_name] = value;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
return payload;
|
|
142
|
+
}
|
|
143
|
+
toString() {
|
|
144
|
+
return JSON.stringify(this);
|
|
145
|
+
}
|
|
146
|
+
async save() {
|
|
147
|
+
const schema = this.constructor[decorator_1.SCHEMA];
|
|
148
|
+
const payload = this._toDBPayload();
|
|
149
|
+
if (this.__isPersisted) {
|
|
150
|
+
await (0, client_1.requireClient)().send(new client_dynamodb_1.PutItemCommand({
|
|
151
|
+
TableName: schema.name,
|
|
152
|
+
Item: (0, util_dynamodb_1.marshall)(payload, { removeUndefinedValues: true }),
|
|
153
|
+
}));
|
|
154
|
+
return true;
|
|
155
|
+
}
|
|
156
|
+
const created = await this.constructor.create(Object.fromEntries(Object.keys(schema.columns)
|
|
157
|
+
.filter(k => !schema.columns[k].store?.relation)
|
|
158
|
+
.map(k => [k, this[k]])));
|
|
159
|
+
for (const key in schema.columns) {
|
|
160
|
+
if (!schema.columns[key].store?.relation) {
|
|
161
|
+
this[key] = created[key];
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
this.__isPersisted = true;
|
|
165
|
+
return true;
|
|
166
|
+
}
|
|
167
|
+
async update(data) {
|
|
168
|
+
const schema = this.constructor[decorator_1.SCHEMA];
|
|
169
|
+
// Filtrar relaciones (ignorarlas) en vez de lanzar error
|
|
170
|
+
const filtered_data = {};
|
|
171
|
+
for (const key in data) {
|
|
172
|
+
const column = schema.columns[key];
|
|
173
|
+
// Solo incluir campos que NO son relaciones
|
|
174
|
+
if (!column?.store?.relation) {
|
|
175
|
+
filtered_data[key] = data[key];
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
const affected = await this.constructor.update(filtered_data, {
|
|
179
|
+
[schema.primary_key]: this[schema.primary_key],
|
|
180
|
+
});
|
|
181
|
+
if (affected > 0) {
|
|
182
|
+
// Actualizar la instancia con los nuevos valores (incluyendo updated_at)
|
|
183
|
+
for (const key in filtered_data) {
|
|
184
|
+
this[key] = filtered_data[key];
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
return affected > 0;
|
|
188
|
+
}
|
|
189
|
+
async destroy() {
|
|
190
|
+
const schema = this.constructor[decorator_1.SCHEMA];
|
|
191
|
+
const id = this[schema.primary_key];
|
|
192
|
+
if (!id)
|
|
193
|
+
throw new Error("Cannot destroy record without ID");
|
|
194
|
+
for (const column_name in schema.columns) {
|
|
195
|
+
if (schema.columns[column_name].store?.softDelete) {
|
|
196
|
+
this[column_name] = new Date().toISOString();
|
|
197
|
+
await this.save();
|
|
198
|
+
return null;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
return this.forceDestroy();
|
|
202
|
+
}
|
|
203
|
+
async forceDestroy() {
|
|
204
|
+
const schema = this.constructor[decorator_1.SCHEMA];
|
|
205
|
+
const id = this[schema.primary_key];
|
|
206
|
+
if (!id)
|
|
207
|
+
throw new Error("Cannot destroy record without ID");
|
|
208
|
+
await (0, client_1.requireClient)().send(new client_dynamodb_1.DeleteItemCommand({
|
|
209
|
+
TableName: schema.name,
|
|
210
|
+
Key: (0, util_dynamodb_1.marshall)({ [schema.primary_key]: id }),
|
|
211
|
+
}));
|
|
212
|
+
return null;
|
|
213
|
+
}
|
|
214
|
+
async attach(RelatedModel, related_id, pivot_data) {
|
|
215
|
+
const schema = this.constructor[decorator_1.SCHEMA];
|
|
216
|
+
const primary_key = schema.primary_key || "id";
|
|
217
|
+
// VALIDACIÓN PRIORITARIA: Verificar que la instancia esté persistida
|
|
218
|
+
const local_id = this[primary_key];
|
|
219
|
+
const is_persisted = this.__isPersisted;
|
|
220
|
+
if (!local_id || !is_persisted) {
|
|
221
|
+
throw new Error("No se puede attach sin ID: la instancia debe persistirse primero con save() o create()");
|
|
222
|
+
}
|
|
223
|
+
const related_table_name = RelatedModel[decorator_1.SCHEMA]?.name;
|
|
224
|
+
if (!related_table_name) {
|
|
225
|
+
throw new Error("Related model no tiene SCHEMA definido");
|
|
226
|
+
}
|
|
227
|
+
let relation = null;
|
|
228
|
+
for (const column_name in schema.columns) {
|
|
229
|
+
const rel = schema.columns[column_name].store?.relation;
|
|
230
|
+
if (rel?.type === "ManyToMany" &&
|
|
231
|
+
rel.model()[decorator_1.SCHEMA]?.name === related_table_name) {
|
|
232
|
+
relation = rel;
|
|
233
|
+
break;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
if (!relation) {
|
|
237
|
+
throw new Error(`No se encontró relación ManyToMany entre ${schema.name} y ${related_table_name}`);
|
|
238
|
+
}
|
|
239
|
+
const foreign_key_value = this[relation.localKey];
|
|
240
|
+
const result = await (0, client_1.requireClient)().send(new client_dynamodb_1.ScanCommand({
|
|
241
|
+
TableName: relation.pivotTable,
|
|
242
|
+
FilterExpression: "#fk = :local_id AND #rk = :related_id",
|
|
243
|
+
ExpressionAttributeNames: {
|
|
244
|
+
"#fk": relation.foreignKey,
|
|
245
|
+
"#rk": relation.relatedKey,
|
|
246
|
+
},
|
|
247
|
+
ExpressionAttributeValues: (0, util_dynamodb_1.marshall)({
|
|
248
|
+
":local_id": foreign_key_value,
|
|
249
|
+
":related_id": related_id,
|
|
250
|
+
}),
|
|
251
|
+
}));
|
|
252
|
+
if (result.Items && result.Items.length > 0)
|
|
253
|
+
return;
|
|
254
|
+
await (0, client_1.requireClient)().send(new client_dynamodb_1.PutItemCommand({
|
|
255
|
+
TableName: relation.pivotTable,
|
|
256
|
+
Item: (0, util_dynamodb_1.marshall)({
|
|
257
|
+
id: `${foreign_key_value}_${related_id}`,
|
|
258
|
+
[relation.foreignKey]: foreign_key_value,
|
|
259
|
+
[relation.relatedKey]: related_id,
|
|
260
|
+
created_at: new Date().toISOString(),
|
|
261
|
+
...pivot_data,
|
|
262
|
+
}, { removeUndefinedValues: true }),
|
|
263
|
+
}));
|
|
264
|
+
}
|
|
265
|
+
async detach(RelatedModel, related_id) {
|
|
266
|
+
const schema = this.constructor[decorator_1.SCHEMA];
|
|
267
|
+
const related_table_name = RelatedModel[decorator_1.SCHEMA]?.name;
|
|
268
|
+
if (!related_table_name)
|
|
269
|
+
return;
|
|
270
|
+
let relation = null;
|
|
271
|
+
for (const column_name in schema.columns) {
|
|
272
|
+
const rel = schema.columns[column_name].store?.relation;
|
|
273
|
+
if (rel?.type === "ManyToMany" &&
|
|
274
|
+
rel.model()[decorator_1.SCHEMA]?.name === related_table_name) {
|
|
275
|
+
relation = rel;
|
|
276
|
+
break;
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
if (!relation)
|
|
280
|
+
return;
|
|
281
|
+
const local_id = this[relation.localKey];
|
|
282
|
+
if (!local_id)
|
|
283
|
+
return;
|
|
284
|
+
const result = await (0, client_1.requireClient)().send(new client_dynamodb_1.ScanCommand({
|
|
285
|
+
TableName: relation.pivotTable,
|
|
286
|
+
FilterExpression: "#fk = :local_id AND #rk = :related_id",
|
|
287
|
+
ExpressionAttributeNames: {
|
|
288
|
+
"#fk": relation.foreignKey,
|
|
289
|
+
"#rk": relation.relatedKey,
|
|
290
|
+
},
|
|
291
|
+
ExpressionAttributeValues: (0, util_dynamodb_1.marshall)({
|
|
292
|
+
":local_id": local_id,
|
|
293
|
+
":related_id": related_id,
|
|
294
|
+
}),
|
|
295
|
+
}));
|
|
296
|
+
if (!result.Items || result.Items.length === 0)
|
|
297
|
+
return;
|
|
298
|
+
await (0, client_1.requireClient)().send(new client_dynamodb_1.DeleteItemCommand({
|
|
299
|
+
TableName: relation.pivotTable,
|
|
300
|
+
Key: (0, util_dynamodb_1.marshall)({ id: (0, util_dynamodb_1.unmarshall)(result.Items[0]).id }),
|
|
301
|
+
}));
|
|
302
|
+
}
|
|
303
|
+
/**
|
|
304
|
+
* Sincronizar relación ManyToMany reemplazando todas las relaciones existentes
|
|
305
|
+
* @param RelatedModel Modelo relacionado
|
|
306
|
+
* @param related_ids Array de IDs a sincronizar
|
|
307
|
+
*/
|
|
308
|
+
async sync(RelatedModel, related_ids) {
|
|
309
|
+
const schema = this.constructor[decorator_1.SCHEMA];
|
|
310
|
+
const related_table_name = RelatedModel[decorator_1.SCHEMA]?.name;
|
|
311
|
+
if (!related_table_name) {
|
|
312
|
+
throw new Error(`No se encontró schema para el modelo relacionado`);
|
|
313
|
+
}
|
|
314
|
+
// Buscar la relación ManyToMany
|
|
315
|
+
let relation = null;
|
|
316
|
+
for (const column_name in schema.columns) {
|
|
317
|
+
const rel = schema.columns[column_name].store?.relation;
|
|
318
|
+
if (rel?.type === "ManyToMany" &&
|
|
319
|
+
rel.model()[decorator_1.SCHEMA]?.name === related_table_name) {
|
|
320
|
+
relation = rel;
|
|
321
|
+
break;
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
if (!relation) {
|
|
325
|
+
throw new Error(`No se encontró relación ManyToMany entre ${schema.name} y ${related_table_name}`);
|
|
326
|
+
}
|
|
327
|
+
const local_id = this[relation.localKey];
|
|
328
|
+
if (!local_id) {
|
|
329
|
+
throw new Error(`El valor de ${relation.localKey} no está definido`);
|
|
330
|
+
}
|
|
331
|
+
// 1. Obtener todas las relaciones existentes
|
|
332
|
+
const scan_result = await (0, client_1.requireClient)().send(new client_dynamodb_1.ScanCommand({
|
|
333
|
+
TableName: relation.pivotTable,
|
|
334
|
+
FilterExpression: "#fk = :local_id",
|
|
335
|
+
ExpressionAttributeNames: { "#fk": relation.foreignKey },
|
|
336
|
+
ExpressionAttributeValues: (0, util_dynamodb_1.marshall)({ ":local_id": local_id }),
|
|
337
|
+
}));
|
|
338
|
+
const existing_ids = new Set((scan_result.Items || []).map((item) => (0, util_dynamodb_1.unmarshall)(item)[relation.relatedKey]));
|
|
339
|
+
const target_ids = new Set(related_ids);
|
|
340
|
+
// 2. Detach relaciones que no están en la lista objetivo
|
|
341
|
+
for (const existing_id of existing_ids) {
|
|
342
|
+
if (!target_ids.has(existing_id)) {
|
|
343
|
+
await this.detach(RelatedModel, existing_id);
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
// 3. Attach nuevas relaciones que no existen
|
|
347
|
+
for (const target_id of target_ids) {
|
|
348
|
+
if (!existing_ids.has(target_id)) {
|
|
349
|
+
await this.attach(RelatedModel, target_id);
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
static async create(data, tx) {
|
|
354
|
+
const instance = new this(data);
|
|
355
|
+
const schema = this[decorator_1.SCHEMA];
|
|
356
|
+
const payload = instance._toDBPayload();
|
|
357
|
+
const pk_db_name = schema.columns[schema.primary_key]?.name || schema.primary_key;
|
|
358
|
+
const condition = {
|
|
359
|
+
expression: 'attribute_not_exists(#pk)',
|
|
360
|
+
names: { '#pk': pk_db_name },
|
|
361
|
+
};
|
|
362
|
+
if (tx) {
|
|
363
|
+
tx.addPut(schema.name, payload, condition);
|
|
364
|
+
tx.onCommit(() => { instance.__isPersisted = true; });
|
|
365
|
+
}
|
|
366
|
+
else {
|
|
367
|
+
try {
|
|
368
|
+
await (0, client_1.requireClient)().send(new client_dynamodb_1.PutItemCommand({
|
|
369
|
+
TableName: schema.name,
|
|
370
|
+
Item: (0, util_dynamodb_1.marshall)(payload, { removeUndefinedValues: true }),
|
|
371
|
+
ConditionExpression: condition.expression,
|
|
372
|
+
ExpressionAttributeNames: condition.names,
|
|
373
|
+
}));
|
|
374
|
+
}
|
|
375
|
+
catch (e) {
|
|
376
|
+
if (e.name === 'ConditionalCheckFailedException') {
|
|
377
|
+
throw new Error(`Record with ${schema.primary_key} '${instance[schema.primary_key]}' already exists in ${schema.name}`);
|
|
378
|
+
}
|
|
379
|
+
throw e;
|
|
380
|
+
}
|
|
381
|
+
instance.__isPersisted = true;
|
|
382
|
+
}
|
|
383
|
+
return instance;
|
|
384
|
+
}
|
|
385
|
+
/**
|
|
386
|
+
* @description Extract PK value from filters if the filter is a simple PK equality. Returns null otherwise.
|
|
387
|
+
* @description Extrae el valor de PK de los filtros si es una igualdad simple por PK. Retorna null en otro caso.
|
|
388
|
+
*/
|
|
389
|
+
static _extractPK(filters) {
|
|
390
|
+
const schema = this[decorator_1.SCHEMA];
|
|
391
|
+
const keys = Object.keys(filters);
|
|
392
|
+
if (keys.length !== 1 || keys[0] !== schema.primary_key)
|
|
393
|
+
return null;
|
|
394
|
+
const val = filters[schema.primary_key];
|
|
395
|
+
if (val === null || val === undefined)
|
|
396
|
+
return null;
|
|
397
|
+
// Valor plano o { $eq: value }
|
|
398
|
+
if (typeof val !== 'object' || Array.isArray(val))
|
|
399
|
+
return val;
|
|
400
|
+
const op_keys = Object.keys(val);
|
|
401
|
+
if (op_keys.length === 1 && (OP_MAP[op_keys[0]] || op_keys[0]) === '=')
|
|
402
|
+
return val[op_keys[0]];
|
|
403
|
+
return null;
|
|
404
|
+
}
|
|
405
|
+
static async update(updates, filters, tx) {
|
|
406
|
+
const schema = this[decorator_1.SCHEMA];
|
|
407
|
+
const parsed_updates = {};
|
|
408
|
+
for (const key in updates) {
|
|
409
|
+
const column = schema.columns[key];
|
|
410
|
+
if (!column?.store?.relation) {
|
|
411
|
+
parsed_updates[key] = updates[key];
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
// Optimización: si el filtro es PK exacta, GetItem directo
|
|
415
|
+
const pk_value = this._extractPK(filters);
|
|
416
|
+
let records;
|
|
417
|
+
if (pk_value !== null) {
|
|
418
|
+
const client = (0, client_1.requireClient)();
|
|
419
|
+
const result = await client.send(new client_dynamodb_1.GetItemCommand({
|
|
420
|
+
TableName: schema.name,
|
|
421
|
+
Key: (0, util_dynamodb_1.marshall)({ [schema.primary_key]: pk_value }),
|
|
422
|
+
}));
|
|
423
|
+
if (!result.Item)
|
|
424
|
+
return 0;
|
|
425
|
+
const raw = (0, util_dynamodb_1.unmarshall)(result.Item);
|
|
426
|
+
const db_to_prop = {};
|
|
427
|
+
for (const p in schema.columns)
|
|
428
|
+
db_to_prop[schema.columns[p].name] = p;
|
|
429
|
+
const mapped = {};
|
|
430
|
+
for (const k in raw) {
|
|
431
|
+
if (raw[k] != null)
|
|
432
|
+
mapped[db_to_prop[k] || k] = raw[k];
|
|
433
|
+
}
|
|
434
|
+
const instance = new this(mapped);
|
|
435
|
+
instance.__isPersisted = true;
|
|
436
|
+
records = [instance];
|
|
437
|
+
}
|
|
438
|
+
else {
|
|
439
|
+
records = await this.where(filters);
|
|
440
|
+
if (records.length === 0)
|
|
441
|
+
return 0;
|
|
442
|
+
}
|
|
443
|
+
for (const record of records) {
|
|
444
|
+
for (const [key, value] of Object.entries(parsed_updates)) {
|
|
445
|
+
record[key] = value;
|
|
446
|
+
}
|
|
447
|
+
// Auto-renovar campos @UpdatedAt
|
|
448
|
+
for (const col_name in schema.columns) {
|
|
449
|
+
if (schema.columns[col_name].store?.updatedAt && !(col_name in parsed_updates)) {
|
|
450
|
+
record[col_name] = undefined;
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
if (tx) {
|
|
454
|
+
tx.addPut(schema.name, record._toDBPayload());
|
|
455
|
+
}
|
|
456
|
+
else {
|
|
457
|
+
await (0, client_1.requireClient)().send(new client_dynamodb_1.PutItemCommand({
|
|
458
|
+
TableName: schema.name,
|
|
459
|
+
Item: (0, util_dynamodb_1.marshall)(record._toDBPayload(), { removeUndefinedValues: true }),
|
|
460
|
+
}));
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
return records.length;
|
|
464
|
+
}
|
|
465
|
+
static async delete(filters, tx) {
|
|
466
|
+
const schema = this[decorator_1.SCHEMA];
|
|
467
|
+
// Optimización: si el filtro es PK exacta y no hay softDelete, DeleteItem directo
|
|
468
|
+
const pk_value = this._extractPK(filters);
|
|
469
|
+
const has_soft_delete = Object.values(schema.columns).some(c => c.store.softDelete);
|
|
470
|
+
if (pk_value !== null && !has_soft_delete) {
|
|
471
|
+
if (tx) {
|
|
472
|
+
tx.addDelete(schema.name, { [schema.primary_key]: pk_value });
|
|
473
|
+
}
|
|
474
|
+
else {
|
|
475
|
+
await (0, client_1.requireClient)().send(new client_dynamodb_1.DeleteItemCommand({
|
|
476
|
+
TableName: schema.name,
|
|
477
|
+
Key: (0, util_dynamodb_1.marshall)({ [schema.primary_key]: pk_value }),
|
|
478
|
+
}));
|
|
479
|
+
}
|
|
480
|
+
return 1;
|
|
481
|
+
}
|
|
482
|
+
// Fallback: where() + delete por cada uno
|
|
483
|
+
const records = await this.where(filters);
|
|
484
|
+
if (records.length === 0)
|
|
485
|
+
return 0;
|
|
486
|
+
for (const record of records) {
|
|
487
|
+
const id = record[schema.primary_key];
|
|
488
|
+
if (!id)
|
|
489
|
+
continue;
|
|
490
|
+
if (tx) {
|
|
491
|
+
tx.addDelete(schema.name, { [schema.primary_key]: id });
|
|
492
|
+
}
|
|
493
|
+
else {
|
|
494
|
+
await (0, client_1.requireClient)().send(new client_dynamodb_1.DeleteItemCommand({
|
|
495
|
+
TableName: schema.name,
|
|
496
|
+
Key: (0, util_dynamodb_1.marshall)({ [schema.primary_key]: id }),
|
|
497
|
+
}));
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
return records.length;
|
|
501
|
+
}
|
|
502
|
+
/**
|
|
503
|
+
* @description Atomically increment a numeric field by amount. Uses DynamoDB SET expression.
|
|
504
|
+
* @description Incrementa atómicamente un campo numérico. Usa expresión SET de DynamoDB.
|
|
505
|
+
*/
|
|
506
|
+
static async _atomicAdd(table_class, field, amount, filters, tx) {
|
|
507
|
+
const schema = table_class[decorator_1.SCHEMA];
|
|
508
|
+
const column = schema.columns[field];
|
|
509
|
+
if (!column)
|
|
510
|
+
throw new Error(`Unknown column '${field}' in ${schema.name}`);
|
|
511
|
+
const db_name = column.name || field;
|
|
512
|
+
const expr = `SET #f = if_not_exists(#f, :zero) + :amt`;
|
|
513
|
+
const names = { '#f': db_name };
|
|
514
|
+
const values = { ':amt': amount, ':zero': 0 };
|
|
515
|
+
const pk_value = table_class._extractPK(filters);
|
|
516
|
+
if (pk_value !== null) {
|
|
517
|
+
const key = { [schema.primary_key]: pk_value };
|
|
518
|
+
if (tx) {
|
|
519
|
+
tx.addUpdate(schema.name, key, expr, names, values);
|
|
520
|
+
}
|
|
521
|
+
else {
|
|
522
|
+
await (0, client_1.requireClient)().send(new client_dynamodb_1.UpdateItemCommand({
|
|
523
|
+
TableName: schema.name,
|
|
524
|
+
Key: (0, util_dynamodb_1.marshall)(key),
|
|
525
|
+
UpdateExpression: expr,
|
|
526
|
+
ExpressionAttributeNames: names,
|
|
527
|
+
ExpressionAttributeValues: (0, util_dynamodb_1.marshall)(values),
|
|
528
|
+
}));
|
|
529
|
+
}
|
|
530
|
+
return 1;
|
|
531
|
+
}
|
|
532
|
+
const records = await table_class.where(filters);
|
|
533
|
+
if (records.length === 0)
|
|
534
|
+
return 0;
|
|
535
|
+
if (tx) {
|
|
536
|
+
for (const record of records) {
|
|
537
|
+
tx.addUpdate(schema.name, { [schema.primary_key]: record[schema.primary_key] }, expr, names, values);
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
else {
|
|
541
|
+
const client = (0, client_1.requireClient)();
|
|
542
|
+
await Promise.all(records.map((record) => client.send(new client_dynamodb_1.UpdateItemCommand({
|
|
543
|
+
TableName: schema.name,
|
|
544
|
+
Key: (0, util_dynamodb_1.marshall)({ [schema.primary_key]: record[schema.primary_key] }),
|
|
545
|
+
UpdateExpression: expr,
|
|
546
|
+
ExpressionAttributeNames: names,
|
|
547
|
+
ExpressionAttributeValues: (0, util_dynamodb_1.marshall)(values),
|
|
548
|
+
}))));
|
|
549
|
+
}
|
|
550
|
+
return records.length;
|
|
551
|
+
}
|
|
552
|
+
static async increment(field, amount, filters, tx) {
|
|
553
|
+
return Table._atomicAdd(this, field, amount, filters, tx);
|
|
554
|
+
}
|
|
555
|
+
static async decrement(field, amount, filters, tx) {
|
|
556
|
+
return Table._atomicAdd(this, field, -amount, filters, tx);
|
|
557
|
+
}
|
|
558
|
+
async increment(field, amount = 1) {
|
|
559
|
+
const schema = this.constructor[decorator_1.SCHEMA];
|
|
560
|
+
const pk = this[schema.primary_key];
|
|
561
|
+
if (!pk)
|
|
562
|
+
throw new Error('Cannot increment without primary key');
|
|
563
|
+
await Table._atomicAdd(this.constructor, field, amount, { [schema.primary_key]: pk });
|
|
564
|
+
this[field] = (this[field] || 0) + amount;
|
|
565
|
+
}
|
|
566
|
+
async decrement(field, amount = 1) {
|
|
567
|
+
const schema = this.constructor[decorator_1.SCHEMA];
|
|
568
|
+
const pk = this[schema.primary_key];
|
|
569
|
+
if (!pk)
|
|
570
|
+
throw new Error('Cannot decrement without primary key');
|
|
571
|
+
await Table._atomicAdd(this.constructor, field, -amount, { [schema.primary_key]: pk });
|
|
572
|
+
this[field] = (this[field] || 0) - amount;
|
|
573
|
+
}
|
|
574
|
+
static async where(field_or_filters, operator_or_value, value, options) {
|
|
575
|
+
const schema = this[decorator_1.SCHEMA];
|
|
576
|
+
// -- Normalización: todas las sobrecargas -> { field: { $op: value } } --
|
|
577
|
+
let raw_filters;
|
|
578
|
+
let opts;
|
|
579
|
+
if (typeof field_or_filters === 'string') {
|
|
580
|
+
if (OPERATORS.has(operator_or_value)) {
|
|
581
|
+
raw_filters = { [field_or_filters]: { [operator_or_value]: value } };
|
|
582
|
+
opts = options || {};
|
|
583
|
+
}
|
|
584
|
+
else {
|
|
585
|
+
raw_filters = { [field_or_filters]: { $eq: operator_or_value } };
|
|
586
|
+
opts = value && typeof value === 'object' && !Array.isArray(value) ? value : {};
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
else {
|
|
590
|
+
raw_filters = field_or_filters ?? {};
|
|
591
|
+
opts = operator_or_value && typeof operator_or_value === 'object' && !Array.isArray(operator_or_value)
|
|
592
|
+
? operator_or_value : {};
|
|
593
|
+
}
|
|
594
|
+
const filters = {};
|
|
595
|
+
for (const [field, val] of Object.entries(raw_filters)) {
|
|
596
|
+
if (val === undefined)
|
|
597
|
+
continue;
|
|
598
|
+
if (!schema.columns[field])
|
|
599
|
+
throw new Error(`Unknown column '${field}' in ${schema.name}`);
|
|
600
|
+
if (val !== null && typeof val === 'object' && !Array.isArray(val) && Object.keys(val).some(k => OPERATORS.has(k))) {
|
|
601
|
+
filters[field] = val;
|
|
602
|
+
}
|
|
603
|
+
else {
|
|
604
|
+
filters[field] = { $eq: val };
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
if (opts.limit === 0)
|
|
608
|
+
return [];
|
|
609
|
+
// Soft delete: excluir registros eliminados salvo que se pida lo contrario
|
|
610
|
+
if (!opts._includeTrashed) {
|
|
611
|
+
for (const col_name in schema.columns) {
|
|
612
|
+
if (schema.columns[col_name].store?.softDelete && !(col_name in filters)) {
|
|
613
|
+
filters[col_name] = { $notExists: true };
|
|
614
|
+
break;
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
// -- Índice inverso db_name → prop_name --
|
|
619
|
+
const db_to_prop = {};
|
|
620
|
+
for (const prop_name in schema.columns) {
|
|
621
|
+
db_to_prop[schema.columns[prop_name].name] = prop_name;
|
|
622
|
+
}
|
|
623
|
+
// -- Detectar mejor índice para QueryCommand --
|
|
624
|
+
// Prioridad: 1) PK con $eq 2) GSI con $eq 3) Scan
|
|
625
|
+
let query_field = null;
|
|
626
|
+
let query_value = null;
|
|
627
|
+
let query_index = undefined;
|
|
628
|
+
// Primero buscar PK
|
|
629
|
+
for (const [field, ops] of Object.entries(filters)) {
|
|
630
|
+
if (field !== schema.primary_key)
|
|
631
|
+
continue;
|
|
632
|
+
const eq_key = Object.keys(ops).find(k => (OP_MAP[k] || k) === '=');
|
|
633
|
+
if (eq_key && ops[eq_key] !== null) {
|
|
634
|
+
query_field = field;
|
|
635
|
+
query_value = ops[eq_key];
|
|
636
|
+
query_index = undefined;
|
|
637
|
+
break;
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
// Si no hay PK, buscar el mejor GSI
|
|
641
|
+
if (!query_field) {
|
|
642
|
+
for (const [field, ops] of Object.entries(filters)) {
|
|
643
|
+
const eq_key = Object.keys(ops).find(k => (OP_MAP[k] || k) === '=');
|
|
644
|
+
if (!eq_key || ops[eq_key] === null)
|
|
645
|
+
continue;
|
|
646
|
+
const db_name = schema.columns[field]?.name || field;
|
|
647
|
+
if (schema.gsis?.has(db_name)) {
|
|
648
|
+
query_field = field;
|
|
649
|
+
query_value = ops[eq_key];
|
|
650
|
+
query_index = `${db_name}_index`;
|
|
651
|
+
break;
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
// -- Construir expressions --
|
|
656
|
+
// El campo elegido para Query va a KeyConditionExpression
|
|
657
|
+
// TODO el resto (incluyendo soft delete, otros filtros) va a FilterExpression (server-side)
|
|
658
|
+
const key_expressions = [];
|
|
659
|
+
const filter_expressions = [];
|
|
660
|
+
const attr_names = {};
|
|
661
|
+
const attr_values = {};
|
|
662
|
+
let idx = 0;
|
|
663
|
+
for (const [field, ops] of Object.entries(filters)) {
|
|
664
|
+
const column = schema.columns[field];
|
|
665
|
+
const db_name = column?.name || field;
|
|
666
|
+
for (const [op_key, op_val] of Object.entries(ops)) {
|
|
667
|
+
if (op_val === undefined)
|
|
668
|
+
continue;
|
|
669
|
+
const op = OP_MAP[op_key] || op_key;
|
|
670
|
+
const nk = `#a${idx}`;
|
|
671
|
+
const vk = `:v${idx}`;
|
|
672
|
+
attr_names[nk] = db_name;
|
|
673
|
+
// KeyConditionExpression: solo el campo de Query con $eq
|
|
674
|
+
const is_query_key = query_field === field && op === '=' && op_val === query_value;
|
|
675
|
+
if ((op === '=' && op_val === null) || op === 'attribute_not_exists') {
|
|
676
|
+
filter_expressions.push(`attribute_not_exists(${nk})`);
|
|
677
|
+
}
|
|
678
|
+
else if ((op === '<>' && op_val === null) || op === 'attribute_exists') {
|
|
679
|
+
filter_expressions.push(`attribute_exists(${nk})`);
|
|
680
|
+
}
|
|
681
|
+
else if (op === 'in' && Array.isArray(op_val)) {
|
|
682
|
+
if (op_val.length === 0)
|
|
683
|
+
throw new Error(`Operator 'in' requires a non-empty array.`);
|
|
684
|
+
const conds = op_val.map((v, i) => { const k = `${vk}_${i}`; attr_values[k] = v; return `${nk} = ${k}`; });
|
|
685
|
+
filter_expressions.push(`(${conds.join(' OR ')})`);
|
|
686
|
+
}
|
|
687
|
+
else if (op === 'include') {
|
|
688
|
+
attr_values[vk] = op_val;
|
|
689
|
+
filter_expressions.push(`contains(${nk}, ${vk})`);
|
|
690
|
+
}
|
|
691
|
+
else if (is_query_key) {
|
|
692
|
+
attr_values[vk] = op_val;
|
|
693
|
+
key_expressions.push(`${nk} ${op} ${vk}`);
|
|
694
|
+
}
|
|
695
|
+
else {
|
|
696
|
+
attr_values[vk] = op_val;
|
|
697
|
+
filter_expressions.push(`${nk} ${op} ${vk}`);
|
|
698
|
+
}
|
|
699
|
+
idx++;
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
// Proyección
|
|
703
|
+
if (opts.attributes?.length) {
|
|
704
|
+
for (const attr of opts.attributes) {
|
|
705
|
+
const col = schema.columns[String(attr)];
|
|
706
|
+
const pk = `#p${idx++}`;
|
|
707
|
+
attr_names[pk] = col?.name || String(attr);
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
// -- Ejecución: Query o Scan --
|
|
711
|
+
let use_query = query_field !== null && key_expressions.length > 0;
|
|
712
|
+
let items = [];
|
|
713
|
+
const base_params = { TableName: schema.name };
|
|
714
|
+
if (Object.keys(attr_names).length > 0)
|
|
715
|
+
base_params.ExpressionAttributeNames = attr_names;
|
|
716
|
+
if (Object.keys(attr_values).length > 0)
|
|
717
|
+
base_params.ExpressionAttributeValues = (0, util_dynamodb_1.marshall)(attr_values, { removeUndefinedValues: true });
|
|
718
|
+
if (filter_expressions.length > 0)
|
|
719
|
+
base_params.FilterExpression = filter_expressions.join(' AND ');
|
|
720
|
+
if (opts.attributes?.length) {
|
|
721
|
+
base_params.ProjectionExpression = Object.keys(attr_names).filter(k => k.startsWith('#p')).join(', ');
|
|
722
|
+
}
|
|
723
|
+
if (use_query) {
|
|
724
|
+
base_params.KeyConditionExpression = key_expressions.join(' AND ');
|
|
725
|
+
if (query_index)
|
|
726
|
+
base_params.IndexName = query_index;
|
|
727
|
+
}
|
|
728
|
+
const client = (0, client_1.requireClient)();
|
|
729
|
+
const unmarshal_items = (raw_items) => {
|
|
730
|
+
for (const item of raw_items) {
|
|
731
|
+
const raw = (0, util_dynamodb_1.unmarshall)(item);
|
|
732
|
+
const mapped = {};
|
|
733
|
+
for (const k in raw) {
|
|
734
|
+
if (raw[k] != null)
|
|
735
|
+
mapped[db_to_prop[k] || k] = raw[k];
|
|
736
|
+
}
|
|
737
|
+
items.push(mapped);
|
|
738
|
+
}
|
|
739
|
+
};
|
|
740
|
+
// Intentar Query. Si el GSI no existe, fall back a Scan y desregistrar GSI.
|
|
741
|
+
if (use_query && query_index) {
|
|
742
|
+
try {
|
|
743
|
+
let last_key;
|
|
744
|
+
do {
|
|
745
|
+
if (last_key)
|
|
746
|
+
base_params.ExclusiveStartKey = last_key;
|
|
747
|
+
const result = await client.send(new client_dynamodb_1.QueryCommand(base_params));
|
|
748
|
+
if (result.Items)
|
|
749
|
+
unmarshal_items(result.Items);
|
|
750
|
+
last_key = result.LastEvaluatedKey;
|
|
751
|
+
} while (last_key);
|
|
752
|
+
}
|
|
753
|
+
catch (e) {
|
|
754
|
+
if (e.name === 'ResourceNotFoundException' || e.message?.includes('index')) {
|
|
755
|
+
// GSI no existe: remover del cache, mover KeyCondition a Filter, reintentar como Scan
|
|
756
|
+
schema.gsis.delete(query_field);
|
|
757
|
+
delete base_params.KeyConditionExpression;
|
|
758
|
+
delete base_params.IndexName;
|
|
759
|
+
delete base_params.ExclusiveStartKey;
|
|
760
|
+
const key_as_filter = key_expressions.join(' AND ');
|
|
761
|
+
base_params.FilterExpression = base_params.FilterExpression
|
|
762
|
+
? `${key_as_filter} AND ${base_params.FilterExpression}`
|
|
763
|
+
: key_as_filter;
|
|
764
|
+
use_query = false;
|
|
765
|
+
items = [];
|
|
766
|
+
}
|
|
767
|
+
else {
|
|
768
|
+
throw e;
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
// Query por PK (sin GSI, no puede fallar por índice faltante) o Scan fallback
|
|
773
|
+
if (!use_query || (use_query && !query_index)) {
|
|
774
|
+
let last_key;
|
|
775
|
+
const cmd = use_query ? client_dynamodb_1.QueryCommand : client_dynamodb_1.ScanCommand;
|
|
776
|
+
do {
|
|
777
|
+
if (last_key)
|
|
778
|
+
base_params.ExclusiveStartKey = last_key;
|
|
779
|
+
const result = await client.send(new cmd(base_params));
|
|
780
|
+
if (result.Items)
|
|
781
|
+
unmarshal_items(result.Items);
|
|
782
|
+
last_key = result.LastEvaluatedKey;
|
|
783
|
+
} while (last_key);
|
|
784
|
+
}
|
|
785
|
+
// Ordenar antes de paginar
|
|
786
|
+
if (opts.order) {
|
|
787
|
+
let sort_field = schema.primary_key;
|
|
788
|
+
let sort_dir = 'ASC';
|
|
789
|
+
if (typeof opts.order === 'string') {
|
|
790
|
+
sort_dir = opts.order;
|
|
791
|
+
for (const cn in schema.columns) {
|
|
792
|
+
if (schema.columns[cn].store?.createdAt) {
|
|
793
|
+
sort_field = cn;
|
|
794
|
+
break;
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
else {
|
|
799
|
+
const [f] = Object.keys(opts.order);
|
|
800
|
+
sort_field = f;
|
|
801
|
+
sort_dir = opts.order[f];
|
|
802
|
+
}
|
|
803
|
+
items.sort((a, b) => {
|
|
804
|
+
if (a[sort_field] < b[sort_field])
|
|
805
|
+
return sort_dir === 'ASC' ? -1 : 1;
|
|
806
|
+
if (a[sort_field] > b[sort_field])
|
|
807
|
+
return sort_dir === 'ASC' ? 1 : -1;
|
|
808
|
+
return 0;
|
|
809
|
+
});
|
|
810
|
+
}
|
|
811
|
+
// Paginar
|
|
812
|
+
const skip = opts.skip ?? opts.offset ?? 0;
|
|
813
|
+
if (skip > 0 || opts.limit !== undefined) {
|
|
814
|
+
items = items.slice(skip, opts.limit !== undefined ? skip + opts.limit : undefined);
|
|
815
|
+
}
|
|
816
|
+
// Instanciar
|
|
817
|
+
const instances = items.map((item) => {
|
|
818
|
+
if (opts.attributes) {
|
|
819
|
+
const instance = Object.create(this.prototype);
|
|
820
|
+
for (const attr of opts.attributes) {
|
|
821
|
+
const column = schema.columns[attr];
|
|
822
|
+
if (!column)
|
|
823
|
+
continue;
|
|
824
|
+
const val = item[attr] ?? null;
|
|
825
|
+
Object.defineProperty(instance, attr, {
|
|
826
|
+
enumerable: true, configurable: true,
|
|
827
|
+
get: () => column.get.reduce((v, fn) => fn(v), val),
|
|
828
|
+
});
|
|
829
|
+
}
|
|
830
|
+
return instance;
|
|
831
|
+
}
|
|
832
|
+
const instance = new this(item);
|
|
833
|
+
instance.__isPersisted = true;
|
|
834
|
+
return instance;
|
|
835
|
+
});
|
|
836
|
+
if (opts.include) {
|
|
837
|
+
await (0, relations_1.processIncludes)(instances, opts.include, this);
|
|
838
|
+
}
|
|
839
|
+
return instances;
|
|
840
|
+
}
|
|
841
|
+
static async first(filters, options) {
|
|
842
|
+
const results = await this.where(filters, { ...options, limit: 1 });
|
|
843
|
+
return results[0];
|
|
844
|
+
}
|
|
845
|
+
static async last(filters, options) {
|
|
846
|
+
const results = await this.where(filters ?? {}, { ...options, order: 'DESC', limit: 1 });
|
|
847
|
+
return results[0];
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
exports.default = Table;
|
|
851
|
+
//# sourceMappingURL=table.js.map
|