dyno-table 2.6.0 → 2.6.2
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/dist/builders.cjs +3648 -43
- package/dist/builders.js +3648 -3
- package/dist/conditions.cjs +60 -67
- package/dist/conditions.js +46 -1
- package/dist/entity.cjs +1126 -15
- package/dist/entity.d.ts +2 -1
- package/dist/entity.js +1127 -3
- package/dist/index.cjs +5388 -270
- package/dist/index.js +5332 -6
- package/dist/table.cjs +4311 -7
- package/dist/table.js +4315 -4
- package/dist/utils.cjs +28 -10
- package/dist/utils.js +29 -1
- package/package.json +50 -65
- package/dist/chunk-2WIBY7PZ.js +0 -46
- package/dist/chunk-3DR6VOFW.cjs +0 -3349
- package/dist/chunk-42LH2UEM.js +0 -577
- package/dist/chunk-7UJJ7JXM.cjs +0 -63
- package/dist/chunk-ELULXDSB.cjs +0 -564
- package/dist/chunk-FF7FYGDH.js +0 -543
- package/dist/chunk-JZB6TYST.js +0 -818
- package/dist/chunk-NYJGW3XH.js +0 -3334
- package/dist/chunk-PB7BBCZO.cjs +0 -32
- package/dist/chunk-QVRMYGC4.js +0 -29
- package/dist/chunk-Z334X72N.cjs +0 -843
- package/dist/chunk-ZUBCW3LA.cjs +0 -579
package/dist/entity.cjs
CHANGED
|
@@ -1,20 +1,1131 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
// src/builders/entity-aware-builders.ts
|
|
4
|
+
function createEntityAwareBuilder(builder, entityName) {
|
|
5
|
+
return new Proxy(builder, {
|
|
6
|
+
get(target, prop, receiver) {
|
|
7
|
+
if (prop === "entityName") {
|
|
8
|
+
return entityName;
|
|
9
|
+
}
|
|
10
|
+
if (prop === "withBatch" && typeof target[prop] === "function") {
|
|
11
|
+
return (batch, entityType) => {
|
|
12
|
+
const typeToUse = entityType ?? entityName;
|
|
13
|
+
const fn = target[prop];
|
|
14
|
+
return fn.call(target, batch, typeToUse);
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
return Reflect.get(target, prop, receiver);
|
|
18
|
+
}
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
function createEntityAwarePutBuilder(builder, entityName) {
|
|
22
|
+
return createEntityAwareBuilder(builder, entityName);
|
|
23
|
+
}
|
|
24
|
+
function createEntityAwareGetBuilder(builder, entityName) {
|
|
25
|
+
return createEntityAwareBuilder(builder, entityName);
|
|
26
|
+
}
|
|
27
|
+
function createEntityAwareDeleteBuilder(builder, entityName) {
|
|
28
|
+
return createEntityAwareBuilder(builder, entityName);
|
|
29
|
+
}
|
|
30
|
+
var EntityAwareUpdateBuilder = class {
|
|
31
|
+
forceRebuildIndexes = [];
|
|
32
|
+
entityName;
|
|
33
|
+
builder;
|
|
34
|
+
entityConfig;
|
|
35
|
+
updateDataApplied = false;
|
|
36
|
+
constructor(builder, entityName) {
|
|
37
|
+
this.builder = builder;
|
|
38
|
+
this.entityName = entityName;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Configure entity-specific logic for automatic timestamp generation and index updates
|
|
42
|
+
*/
|
|
43
|
+
configureEntityLogic(config) {
|
|
44
|
+
this.entityConfig = config;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Forces a rebuild of one or more readonly indexes during the update operation.
|
|
48
|
+
*
|
|
49
|
+
* By default, readonly indexes are not updated during entity updates to prevent
|
|
50
|
+
* errors when required index attributes are missing. This method allows you to
|
|
51
|
+
* override that behavior and force specific indexes to be rebuilt.
|
|
52
|
+
*
|
|
53
|
+
* @example
|
|
54
|
+
* ```typescript
|
|
55
|
+
* // Force rebuild a single readonly index
|
|
56
|
+
* const result = await repo.update({ id: 'TREX-001' }, { status: 'ACTIVE' })
|
|
57
|
+
* .forceIndexRebuild('gsi1')
|
|
58
|
+
* .execute();
|
|
59
|
+
*
|
|
60
|
+
* // Force rebuild multiple readonly indexes
|
|
61
|
+
* const result = await repo.update({ id: 'TREX-001' }, { status: 'ACTIVE' })
|
|
62
|
+
* .forceIndexRebuild(['gsi1', 'gsi2'])
|
|
63
|
+
* .execute();
|
|
64
|
+
*
|
|
65
|
+
* // Chain with other update operations
|
|
66
|
+
* const result = await repo.update({ id: 'TREX-001' }, { status: 'ACTIVE' })
|
|
67
|
+
* .set('lastUpdated', new Date().toISOString())
|
|
68
|
+
* .forceIndexRebuild('gsi1')
|
|
69
|
+
* .condition(op => op.eq('status', 'INACTIVE'))
|
|
70
|
+
* .execute();
|
|
71
|
+
* ```
|
|
72
|
+
*
|
|
73
|
+
* @param indexes - A single index name or array of index names to force rebuild
|
|
74
|
+
* @returns The builder instance for method chaining
|
|
75
|
+
*/
|
|
76
|
+
forceIndexRebuild(indexes) {
|
|
77
|
+
if (Array.isArray(indexes)) {
|
|
78
|
+
this.forceRebuildIndexes = [...this.forceRebuildIndexes, ...indexes];
|
|
79
|
+
} else {
|
|
80
|
+
this.forceRebuildIndexes.push(indexes);
|
|
81
|
+
}
|
|
82
|
+
return this;
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Gets the list of indexes that should be force rebuilt.
|
|
86
|
+
* This is used internally by entity update logic.
|
|
87
|
+
*
|
|
88
|
+
* @returns Array of index names to force rebuild
|
|
89
|
+
*/
|
|
90
|
+
getForceRebuildIndexes() {
|
|
91
|
+
return [...this.forceRebuildIndexes];
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Apply entity-specific update data (timestamps and index updates)
|
|
95
|
+
* This is called automatically when needed
|
|
96
|
+
*/
|
|
97
|
+
applyEntityUpdates() {
|
|
98
|
+
if (!this.entityConfig || this.updateDataApplied) return;
|
|
99
|
+
const timestamps = this.entityConfig.generateTimestamps();
|
|
100
|
+
const updatedItem = { ...this.entityConfig.key, ...this.entityConfig.data, ...timestamps };
|
|
101
|
+
const indexUpdates = this.entityConfig.buildIndexUpdates(
|
|
102
|
+
this.entityConfig.key,
|
|
103
|
+
updatedItem,
|
|
104
|
+
this.entityConfig.table,
|
|
105
|
+
this.entityConfig.indexes,
|
|
106
|
+
this.forceRebuildIndexes
|
|
107
|
+
);
|
|
108
|
+
this.builder.set({ ...this.entityConfig.data, ...timestamps, ...indexUpdates });
|
|
109
|
+
this.updateDataApplied = true;
|
|
110
|
+
}
|
|
111
|
+
set(valuesOrPath, value) {
|
|
112
|
+
if (typeof valuesOrPath === "object") {
|
|
113
|
+
this.builder.set(valuesOrPath);
|
|
114
|
+
} else {
|
|
115
|
+
this.builder.set(valuesOrPath, value);
|
|
116
|
+
}
|
|
117
|
+
return this;
|
|
118
|
+
}
|
|
119
|
+
remove(path) {
|
|
120
|
+
this.builder.remove(path);
|
|
121
|
+
return this;
|
|
122
|
+
}
|
|
123
|
+
add(path, value) {
|
|
124
|
+
this.builder.add(path, value);
|
|
125
|
+
return this;
|
|
126
|
+
}
|
|
127
|
+
deleteElementsFromSet(path, value) {
|
|
128
|
+
this.builder.deleteElementsFromSet(path, value);
|
|
129
|
+
return this;
|
|
130
|
+
}
|
|
131
|
+
condition(condition) {
|
|
132
|
+
this.builder.condition(condition);
|
|
133
|
+
return this;
|
|
134
|
+
}
|
|
135
|
+
returnValues(returnValues) {
|
|
136
|
+
this.builder.returnValues(returnValues);
|
|
137
|
+
return this;
|
|
138
|
+
}
|
|
139
|
+
toDynamoCommand() {
|
|
140
|
+
return this.builder.toDynamoCommand();
|
|
141
|
+
}
|
|
142
|
+
withTransaction(transaction) {
|
|
143
|
+
this.applyEntityUpdates();
|
|
144
|
+
this.builder.withTransaction(transaction);
|
|
145
|
+
}
|
|
146
|
+
debug() {
|
|
147
|
+
return this.builder.debug();
|
|
148
|
+
}
|
|
149
|
+
async execute() {
|
|
150
|
+
this.updateDataApplied = false;
|
|
151
|
+
this.applyEntityUpdates();
|
|
152
|
+
return this.builder.execute();
|
|
153
|
+
}
|
|
154
|
+
};
|
|
155
|
+
function createEntityAwareUpdateBuilder(builder, entityName) {
|
|
156
|
+
return new EntityAwareUpdateBuilder(builder, entityName);
|
|
157
|
+
}
|
|
6
158
|
|
|
159
|
+
// src/conditions.ts
|
|
160
|
+
var createComparisonCondition = (type) => (attr, value) => ({
|
|
161
|
+
type,
|
|
162
|
+
attr,
|
|
163
|
+
value
|
|
164
|
+
});
|
|
165
|
+
var eq = createComparisonCondition("eq");
|
|
7
166
|
|
|
167
|
+
// src/errors.ts
|
|
168
|
+
var DynoTableError = class extends Error {
|
|
169
|
+
/**
|
|
170
|
+
* Machine-readable error code for programmatic error handling
|
|
171
|
+
* @example "KEY_GENERATION_FAILED", "VALIDATION_ERROR", etc.
|
|
172
|
+
*/
|
|
173
|
+
code;
|
|
174
|
+
/**
|
|
175
|
+
* Additional context about the error
|
|
176
|
+
* Contains operation-specific details like entity names, table names,
|
|
177
|
+
* expressions, conditions, and other relevant debugging information
|
|
178
|
+
*/
|
|
179
|
+
context;
|
|
180
|
+
/**
|
|
181
|
+
* The original error that caused this error (if wrapping another error)
|
|
182
|
+
* Useful for preserving AWS SDK errors or other underlying errors
|
|
183
|
+
*/
|
|
184
|
+
cause;
|
|
185
|
+
constructor(message, code, context = {}, cause) {
|
|
186
|
+
super(message);
|
|
187
|
+
this.name = "DynoTableError";
|
|
188
|
+
this.code = code;
|
|
189
|
+
this.context = context;
|
|
190
|
+
this.cause = cause;
|
|
191
|
+
if (Error.captureStackTrace) {
|
|
192
|
+
Error.captureStackTrace(this, this.constructor);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
};
|
|
196
|
+
var ValidationError = class extends DynoTableError {
|
|
197
|
+
constructor(message, code, context = {}, cause) {
|
|
198
|
+
super(message, code, context, cause);
|
|
199
|
+
this.name = "ValidationError";
|
|
200
|
+
}
|
|
201
|
+
};
|
|
202
|
+
var OperationError = class extends DynoTableError {
|
|
203
|
+
constructor(message, code, context = {}, cause) {
|
|
204
|
+
super(message, code, context, cause);
|
|
205
|
+
this.name = "OperationError";
|
|
206
|
+
}
|
|
207
|
+
};
|
|
208
|
+
var ExpressionError = class extends DynoTableError {
|
|
209
|
+
constructor(message, code, context = {}, cause) {
|
|
210
|
+
super(message, code, context, cause);
|
|
211
|
+
this.name = "ExpressionError";
|
|
212
|
+
}
|
|
213
|
+
};
|
|
214
|
+
var ConfigurationError = class extends DynoTableError {
|
|
215
|
+
constructor(message, code, context = {}, cause) {
|
|
216
|
+
super(message, code, context, cause);
|
|
217
|
+
this.name = "ConfigurationError";
|
|
218
|
+
}
|
|
219
|
+
};
|
|
220
|
+
var EntityError = class extends DynoTableError {
|
|
221
|
+
constructor(message, code, context = {}, cause) {
|
|
222
|
+
super(message, code, context, cause);
|
|
223
|
+
this.name = "EntityError";
|
|
224
|
+
}
|
|
225
|
+
};
|
|
226
|
+
var KeyGenerationError = class extends EntityError {
|
|
227
|
+
constructor(message, code, context = {}, cause) {
|
|
228
|
+
super(message, code, context, cause);
|
|
229
|
+
this.name = "KeyGenerationError";
|
|
230
|
+
}
|
|
231
|
+
};
|
|
232
|
+
var IndexGenerationError = class extends EntityError {
|
|
233
|
+
constructor(message, code, context = {}, cause) {
|
|
234
|
+
super(message, code, context, cause);
|
|
235
|
+
this.name = "IndexGenerationError";
|
|
236
|
+
}
|
|
237
|
+
};
|
|
238
|
+
var EntityValidationError = class extends ValidationError {
|
|
239
|
+
constructor(message, code, context = {}, cause) {
|
|
240
|
+
super(message, code, context, cause);
|
|
241
|
+
this.name = "EntityValidationError";
|
|
242
|
+
}
|
|
243
|
+
};
|
|
244
|
+
var ErrorCodes = {
|
|
245
|
+
// Key Generation Errors
|
|
246
|
+
KEY_GENERATION_FAILED: "KEY_GENERATION_FAILED",
|
|
247
|
+
KEY_MISSING_ATTRIBUTES: "KEY_MISSING_ATTRIBUTES",
|
|
248
|
+
KEY_INVALID_FORMAT: "KEY_INVALID_FORMAT",
|
|
249
|
+
// Index Errors
|
|
250
|
+
INDEX_GENERATION_FAILED: "INDEX_GENERATION_FAILED",
|
|
251
|
+
INDEX_MISSING_ATTRIBUTES: "INDEX_MISSING_ATTRIBUTES",
|
|
252
|
+
INDEX_NOT_FOUND: "INDEX_NOT_FOUND",
|
|
253
|
+
INDEX_READONLY_UPDATE_FAILED: "INDEX_READONLY_UPDATE_FAILED",
|
|
254
|
+
INDEX_UNDEFINED_VALUES: "INDEX_UNDEFINED_VALUES",
|
|
255
|
+
// Validation Errors
|
|
256
|
+
ENTITY_VALIDATION_FAILED: "ENTITY_VALIDATION_FAILED",
|
|
257
|
+
ASYNC_VALIDATION_NOT_SUPPORTED: "ASYNC_VALIDATION_NOT_SUPPORTED",
|
|
258
|
+
QUERY_INPUT_VALIDATION_FAILED: "QUERY_INPUT_VALIDATION_FAILED",
|
|
259
|
+
SCHEMA_VALIDATION_FAILED: "SCHEMA_VALIDATION_FAILED",
|
|
260
|
+
UNDEFINED_VALUE: "UNDEFINED_VALUE",
|
|
261
|
+
// Operation Errors
|
|
262
|
+
QUERY_FAILED: "QUERY_FAILED",
|
|
263
|
+
SCAN_FAILED: "SCAN_FAILED",
|
|
264
|
+
GET_FAILED: "GET_FAILED",
|
|
265
|
+
PUT_FAILED: "PUT_FAILED",
|
|
266
|
+
DELETE_FAILED: "DELETE_FAILED",
|
|
267
|
+
UPDATE_FAILED: "UPDATE_FAILED",
|
|
268
|
+
BATCH_GET_FAILED: "BATCH_GET_FAILED",
|
|
269
|
+
BATCH_WRITE_FAILED: "BATCH_WRITE_FAILED",
|
|
270
|
+
NO_UPDATE_ACTIONS: "NO_UPDATE_ACTIONS",
|
|
271
|
+
// Configuration Errors
|
|
272
|
+
GSI_NOT_FOUND: "GSI_NOT_FOUND",
|
|
273
|
+
SORT_KEY_REQUIRED: "SORT_KEY_REQUIRED",
|
|
274
|
+
SORT_KEY_NOT_DEFINED: "SORT_KEY_NOT_DEFINED",
|
|
275
|
+
PRIMARY_KEY_MISSING: "PRIMARY_KEY_MISSING",
|
|
276
|
+
INVALID_CHUNK_SIZE: "INVALID_CHUNK_SIZE",
|
|
277
|
+
CONDITION_REQUIRED: "CONDITION_REQUIRED",
|
|
278
|
+
CONDITION_GENERATION_FAILED: "CONDITION_GENERATION_FAILED",
|
|
279
|
+
PK_EXTRACTION_FAILED: "PK_EXTRACTION_FAILED"};
|
|
8
280
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
}
|
|
281
|
+
// src/utils/error-factory.ts
|
|
282
|
+
var ValidationErrors = {
|
|
283
|
+
indexSchemaValidationFailed: (validationIssues, keyType) => {
|
|
284
|
+
const keyLabel = keyType === "partition" ? "partition key" : keyType === "sort" ? "sort key" : "partition/sort key";
|
|
285
|
+
return new ValidationError(
|
|
286
|
+
`Index validation failed while generating ${keyLabel}: missing required attribute(s) or invalid values.`,
|
|
287
|
+
ErrorCodes.SCHEMA_VALIDATION_FAILED,
|
|
288
|
+
{
|
|
289
|
+
keyType,
|
|
290
|
+
validationIssues,
|
|
291
|
+
suggestion: `Provide the required attributes to construct the index ${keyLabel}`
|
|
292
|
+
}
|
|
293
|
+
);
|
|
294
|
+
},
|
|
295
|
+
noUpdateActions: (tableName, key) => new ValidationError("No update actions specified", ErrorCodes.NO_UPDATE_ACTIONS, {
|
|
296
|
+
tableName,
|
|
297
|
+
key,
|
|
298
|
+
suggestion: "Use set(), remove(), add(), or delete() to specify update actions"
|
|
299
|
+
}),
|
|
300
|
+
conditionRequired: (tableName, key) => new ValidationError("Condition is required for condition check operations", ErrorCodes.CONDITION_REQUIRED, {
|
|
301
|
+
tableName,
|
|
302
|
+
key,
|
|
303
|
+
suggestion: "Use the condition() method to specify a condition"
|
|
304
|
+
}),
|
|
305
|
+
queryInputValidationFailed: (entityName, queryName, validationIssues, providedInput) => new ValidationError(
|
|
306
|
+
`Query input validation failed for "${queryName}" on entity "${entityName}"`,
|
|
307
|
+
ErrorCodes.QUERY_INPUT_VALIDATION_FAILED,
|
|
308
|
+
{
|
|
309
|
+
entityName,
|
|
310
|
+
queryName,
|
|
311
|
+
validationIssues,
|
|
312
|
+
providedInput,
|
|
313
|
+
suggestion: "Ensure the query input matches the expected schema"
|
|
314
|
+
}
|
|
315
|
+
),
|
|
316
|
+
undefinedValue: (path, tableName, key) => new ValidationError(`Cannot set undefined value for attribute "${path}"`, ErrorCodes.UNDEFINED_VALUE, {
|
|
317
|
+
path,
|
|
318
|
+
tableName,
|
|
319
|
+
key,
|
|
320
|
+
suggestion: "DynamoDB does not support undefined values. Use remove() to delete an attribute, or provide a valid value (null, string, number, etc.)"
|
|
321
|
+
})
|
|
322
|
+
};
|
|
323
|
+
var ConfigurationErrors = {
|
|
324
|
+
invalidChunkSize: (size) => new ConfigurationError("Chunk size must be greater than 0", ErrorCodes.INVALID_CHUNK_SIZE, {
|
|
325
|
+
size,
|
|
326
|
+
suggestion: "Provide a chunk size greater than 0"
|
|
327
|
+
}),
|
|
328
|
+
sortKeyRequired: (tableName, partitionKey, sortKey) => new ConfigurationError("Sort key is required for this operation", ErrorCodes.SORT_KEY_REQUIRED, {
|
|
329
|
+
tableName,
|
|
330
|
+
partitionKey,
|
|
331
|
+
sortKey,
|
|
332
|
+
suggestion: "Provide a sort key value or use a table with only a partition key"
|
|
333
|
+
}),
|
|
334
|
+
sortKeyNotDefined: (tableName, partitionKey, indexName) => new ConfigurationError("Sort key is not defined for this table/index", ErrorCodes.SORT_KEY_NOT_DEFINED, {
|
|
335
|
+
tableName,
|
|
336
|
+
partitionKey,
|
|
337
|
+
indexName,
|
|
338
|
+
suggestion: "This operation requires a table/index with a sort key defined"
|
|
339
|
+
}),
|
|
340
|
+
gsiNotFound: (indexName, tableName, availableIndexes) => new ConfigurationError(`GSI "${indexName}" not found in table configuration`, ErrorCodes.GSI_NOT_FOUND, {
|
|
341
|
+
indexName,
|
|
342
|
+
tableName,
|
|
343
|
+
availableIndexes,
|
|
344
|
+
suggestion: `Use one of the available indexes: ${availableIndexes.join(", ")}`
|
|
345
|
+
}),
|
|
346
|
+
primaryKeyMissing: (tableName, partitionKeyName, providedItem) => new ConfigurationError(`Primary key value for '${partitionKeyName}' is missing`, ErrorCodes.PRIMARY_KEY_MISSING, {
|
|
347
|
+
tableName,
|
|
348
|
+
partitionKeyName,
|
|
349
|
+
providedItem,
|
|
350
|
+
suggestion: `Ensure the item includes a value for '${partitionKeyName}'`
|
|
351
|
+
}),
|
|
352
|
+
pkExtractionFailed: (tableName, indexName, item, cause) => new ConfigurationError(
|
|
353
|
+
`Failed to extract partition key from item for index "${indexName}"`,
|
|
354
|
+
ErrorCodes.PK_EXTRACTION_FAILED,
|
|
355
|
+
{
|
|
356
|
+
tableName,
|
|
357
|
+
indexName,
|
|
358
|
+
item,
|
|
359
|
+
suggestion: "Ensure the item has the required partition key attribute"
|
|
360
|
+
},
|
|
361
|
+
cause
|
|
362
|
+
),
|
|
363
|
+
conditionGenerationFailed: (condition, suggestion) => new ExpressionError("Failed to generate condition expression", ErrorCodes.CONDITION_GENERATION_FAILED, {
|
|
364
|
+
condition,
|
|
365
|
+
suggestion: suggestion || "Check that the condition is properly formed"
|
|
366
|
+
})
|
|
367
|
+
};
|
|
368
|
+
var OperationErrors = {
|
|
369
|
+
queryFailed: (tableName, context, cause) => new OperationError(
|
|
370
|
+
`Query operation failed on table "${tableName}"`,
|
|
371
|
+
ErrorCodes.QUERY_FAILED,
|
|
372
|
+
{
|
|
373
|
+
tableName,
|
|
374
|
+
operation: "query",
|
|
375
|
+
...context
|
|
376
|
+
},
|
|
377
|
+
cause
|
|
378
|
+
),
|
|
379
|
+
scanFailed: (tableName, context, cause) => new OperationError(
|
|
380
|
+
`Scan operation failed on table "${tableName}"`,
|
|
381
|
+
ErrorCodes.SCAN_FAILED,
|
|
382
|
+
{
|
|
383
|
+
tableName,
|
|
384
|
+
operation: "scan",
|
|
385
|
+
...context
|
|
386
|
+
},
|
|
387
|
+
cause
|
|
388
|
+
),
|
|
389
|
+
getFailed: (tableName, key, cause) => new OperationError(
|
|
390
|
+
`Get operation failed on table "${tableName}"`,
|
|
391
|
+
ErrorCodes.GET_FAILED,
|
|
392
|
+
{
|
|
393
|
+
tableName,
|
|
394
|
+
operation: "get",
|
|
395
|
+
key
|
|
396
|
+
},
|
|
397
|
+
cause
|
|
398
|
+
),
|
|
399
|
+
putFailed: (tableName, item, cause) => new OperationError(
|
|
400
|
+
`Put operation failed on table "${tableName}"`,
|
|
401
|
+
ErrorCodes.PUT_FAILED,
|
|
402
|
+
{
|
|
403
|
+
tableName,
|
|
404
|
+
operation: "put",
|
|
405
|
+
item
|
|
406
|
+
},
|
|
407
|
+
cause
|
|
408
|
+
),
|
|
409
|
+
updateFailed: (tableName, key, cause) => new OperationError(
|
|
410
|
+
`Update operation failed on table "${tableName}"`,
|
|
411
|
+
ErrorCodes.UPDATE_FAILED,
|
|
412
|
+
{
|
|
413
|
+
tableName,
|
|
414
|
+
operation: "update",
|
|
415
|
+
key
|
|
416
|
+
},
|
|
417
|
+
cause
|
|
418
|
+
),
|
|
419
|
+
deleteFailed: (tableName, key, cause) => new OperationError(
|
|
420
|
+
`Delete operation failed on table "${tableName}"`,
|
|
421
|
+
ErrorCodes.DELETE_FAILED,
|
|
422
|
+
{
|
|
423
|
+
tableName,
|
|
424
|
+
operation: "delete",
|
|
425
|
+
key
|
|
426
|
+
},
|
|
427
|
+
cause
|
|
428
|
+
),
|
|
429
|
+
batchGetFailed: (tableName, context, cause) => new OperationError(
|
|
430
|
+
`Batch get operation failed on table "${tableName}"`,
|
|
431
|
+
ErrorCodes.BATCH_GET_FAILED,
|
|
432
|
+
{
|
|
433
|
+
tableName,
|
|
434
|
+
operation: "batchGet",
|
|
435
|
+
...context
|
|
436
|
+
},
|
|
437
|
+
cause
|
|
438
|
+
),
|
|
439
|
+
batchWriteFailed: (tableName, context, cause) => new OperationError(
|
|
440
|
+
`Batch write operation failed on table "${tableName}"`,
|
|
441
|
+
ErrorCodes.BATCH_WRITE_FAILED,
|
|
442
|
+
{
|
|
443
|
+
tableName,
|
|
444
|
+
operation: "batchWrite",
|
|
445
|
+
...context
|
|
446
|
+
},
|
|
447
|
+
cause
|
|
448
|
+
)
|
|
449
|
+
};
|
|
450
|
+
var EntityErrors = {
|
|
451
|
+
validationFailed: (entityName, operation, validationIssues, providedData) => new EntityValidationError(
|
|
452
|
+
`Validation failed for entity "${entityName}" during ${operation} operation`,
|
|
453
|
+
ErrorCodes.ENTITY_VALIDATION_FAILED,
|
|
454
|
+
{
|
|
455
|
+
entityName,
|
|
456
|
+
operation,
|
|
457
|
+
validationIssues,
|
|
458
|
+
providedData,
|
|
459
|
+
suggestion: "Check that all required fields are provided and match the schema"
|
|
460
|
+
}
|
|
461
|
+
),
|
|
462
|
+
queryInputValidationFailed: (entityName, queryName, validationIssues, providedInput) => new EntityValidationError(
|
|
463
|
+
`Query input validation failed for "${queryName}" on entity "${entityName}"`,
|
|
464
|
+
ErrorCodes.QUERY_INPUT_VALIDATION_FAILED,
|
|
465
|
+
{
|
|
466
|
+
entityName,
|
|
467
|
+
queryName,
|
|
468
|
+
validationIssues,
|
|
469
|
+
providedInput,
|
|
470
|
+
suggestion: "Ensure the query input matches the expected schema"
|
|
471
|
+
}
|
|
472
|
+
),
|
|
473
|
+
asyncValidationNotSupported: (entityName, operation) => new EntityValidationError(
|
|
474
|
+
`Entity "${entityName}" uses async validation which is not supported in transactions/batches`,
|
|
475
|
+
ErrorCodes.ASYNC_VALIDATION_NOT_SUPPORTED,
|
|
476
|
+
{
|
|
477
|
+
entityName,
|
|
478
|
+
operation,
|
|
479
|
+
suggestion: "Use .execute() for async validation or switch to synchronous schema validation"
|
|
480
|
+
}
|
|
481
|
+
),
|
|
482
|
+
keyGenerationFailed: (entityName, operation, providedData, requiredAttributes, cause) => new KeyGenerationError(
|
|
483
|
+
`Failed to generate primary key for entity "${entityName}"`,
|
|
484
|
+
ErrorCodes.KEY_GENERATION_FAILED,
|
|
485
|
+
{
|
|
486
|
+
entityName,
|
|
487
|
+
operation,
|
|
488
|
+
providedData,
|
|
489
|
+
requiredAttributes,
|
|
490
|
+
suggestion: requiredAttributes ? `Ensure these attributes are provided: ${requiredAttributes.join(", ")}` : "Check that all required attributes for key generation are provided"
|
|
491
|
+
},
|
|
492
|
+
cause
|
|
493
|
+
),
|
|
494
|
+
keyInvalidFormat: (entityName, operation, providedData, generatedKey) => new KeyGenerationError(
|
|
495
|
+
`Primary key generation for entity "${entityName}" produced undefined/null partition key`,
|
|
496
|
+
ErrorCodes.KEY_INVALID_FORMAT,
|
|
497
|
+
{
|
|
498
|
+
entityName,
|
|
499
|
+
operation,
|
|
500
|
+
providedData,
|
|
501
|
+
generatedKey,
|
|
502
|
+
suggestion: "Ensure the key generation function returns valid pk (and sk if applicable) values"
|
|
503
|
+
}
|
|
504
|
+
),
|
|
505
|
+
keyMissingAttributes: (entityName, operation, missingAttributes, providedData) => new KeyGenerationError(
|
|
506
|
+
`Missing required attributes for key generation in entity "${entityName}": ${missingAttributes.join(", ")}`,
|
|
507
|
+
ErrorCodes.KEY_MISSING_ATTRIBUTES,
|
|
508
|
+
{
|
|
509
|
+
entityName,
|
|
510
|
+
operation,
|
|
511
|
+
missingAttributes,
|
|
512
|
+
providedData,
|
|
513
|
+
suggestion: `Provide the following attributes: ${missingAttributes.join(", ")}`
|
|
514
|
+
}
|
|
515
|
+
)
|
|
516
|
+
};
|
|
517
|
+
var IndexErrors = {
|
|
518
|
+
generationFailed: (indexName, operation, providedItem, partitionKeyAttribute, sortKeyAttribute, cause) => new IndexGenerationError(
|
|
519
|
+
`Failed to generate key for index "${indexName}"`,
|
|
520
|
+
ErrorCodes.INDEX_GENERATION_FAILED,
|
|
521
|
+
{
|
|
522
|
+
indexName,
|
|
523
|
+
operation,
|
|
524
|
+
providedItem,
|
|
525
|
+
partitionKeyAttribute,
|
|
526
|
+
sortKeyAttribute,
|
|
527
|
+
suggestion: "Ensure all attributes required by the index are present in the item"
|
|
528
|
+
},
|
|
529
|
+
cause
|
|
530
|
+
),
|
|
531
|
+
missingAttributes: (indexName, operation, missingAttributes, providedData, isReadOnly) => new IndexGenerationError(
|
|
532
|
+
`Cannot regenerate readonly index "${indexName}" - missing required attributes: ${missingAttributes.join(", ")}`,
|
|
533
|
+
ErrorCodes.INDEX_MISSING_ATTRIBUTES,
|
|
534
|
+
{
|
|
535
|
+
indexName,
|
|
536
|
+
operation,
|
|
537
|
+
missingAttributes,
|
|
538
|
+
providedData,
|
|
539
|
+
isReadOnly,
|
|
540
|
+
suggestion: isReadOnly ? "For readonly indexes, provide all attributes or use forceIndexRebuild() with complete data" : `Provide the following attributes: ${missingAttributes.join(", ")}`
|
|
541
|
+
}
|
|
542
|
+
),
|
|
543
|
+
undefinedValues: (indexName, operation, generatedKey, providedItem) => new IndexGenerationError(`Index "${indexName}" generated undefined values`, ErrorCodes.INDEX_UNDEFINED_VALUES, {
|
|
544
|
+
indexName,
|
|
545
|
+
operation,
|
|
546
|
+
generatedKey,
|
|
547
|
+
providedItem,
|
|
548
|
+
suggestion: "Ensure all attributes required by the index are present in the item"
|
|
549
|
+
}),
|
|
550
|
+
notFound: (requestedIndexes, availableIndexes, entityName, tableName) => new IndexGenerationError(
|
|
551
|
+
`Requested indexes not found: ${requestedIndexes.join(", ")}`,
|
|
552
|
+
ErrorCodes.INDEX_NOT_FOUND,
|
|
553
|
+
{
|
|
554
|
+
requestedIndexes,
|
|
555
|
+
availableIndexes,
|
|
556
|
+
entityName,
|
|
557
|
+
tableName,
|
|
558
|
+
suggestion: `Available indexes are: ${availableIndexes.join(", ")}`
|
|
559
|
+
}
|
|
560
|
+
),
|
|
561
|
+
readonlyUpdateFailed: (indexName, operation, providedData) => new IndexGenerationError(
|
|
562
|
+
`Cannot update readonly index "${indexName}" without forcing rebuild`,
|
|
563
|
+
ErrorCodes.INDEX_READONLY_UPDATE_FAILED,
|
|
564
|
+
{
|
|
565
|
+
indexName,
|
|
566
|
+
operation,
|
|
567
|
+
providedData,
|
|
568
|
+
isReadOnly: true,
|
|
569
|
+
suggestion: "Use forceIndexRebuild() to update readonly indexes, or provide all required attributes"
|
|
570
|
+
}
|
|
571
|
+
)
|
|
572
|
+
};
|
|
573
|
+
|
|
574
|
+
// src/utils/error-utils.ts
|
|
575
|
+
function getAwsErrorMessage(error) {
|
|
576
|
+
if (error instanceof Error) {
|
|
577
|
+
return error.message;
|
|
578
|
+
}
|
|
579
|
+
if (typeof error === "object" && error !== null && "message" in error) {
|
|
580
|
+
return String(error.message);
|
|
581
|
+
}
|
|
582
|
+
return void 0;
|
|
583
|
+
}
|
|
584
|
+
function extractRequiredAttributes(error) {
|
|
585
|
+
const message = getAwsErrorMessage(error);
|
|
586
|
+
if (!message) return void 0;
|
|
587
|
+
const patterns = [
|
|
588
|
+
/(?:missing|required)\s+(?:attribute|field|property)(?:s)?[:\s]+([a-zA-Z0-9_,\s]+)/i,
|
|
589
|
+
/(?:attribute|field|property)[:\s]+([a-zA-Z0-9_]+)\s+is\s+(?:missing|required)/i,
|
|
590
|
+
/"([a-zA-Z0-9_]+)"\s+is\s+(?:missing|required)/i
|
|
591
|
+
];
|
|
592
|
+
for (const pattern of patterns) {
|
|
593
|
+
const match = message.match(pattern);
|
|
594
|
+
if (match?.[1]) {
|
|
595
|
+
return match[1].split(",").map((attr) => attr.trim()).filter((attr) => attr.length > 0);
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
return void 0;
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
// src/entity/ddb-indexing.ts
|
|
602
|
+
var IndexBuilder = class {
|
|
603
|
+
/**
|
|
604
|
+
* Creates a new IndexBuilder instance
|
|
605
|
+
*
|
|
606
|
+
* @param table - The DynamoDB table instance
|
|
607
|
+
* @param indexes - The index definitions
|
|
608
|
+
*/
|
|
609
|
+
constructor(table, indexes = {}) {
|
|
610
|
+
this.table = table;
|
|
611
|
+
this.indexes = indexes;
|
|
612
|
+
}
|
|
613
|
+
/**
|
|
614
|
+
* Build index attributes for item creation
|
|
615
|
+
*
|
|
616
|
+
* @param item - The item to generate indexes for
|
|
617
|
+
* @param options - Options for building indexes
|
|
618
|
+
* @returns Record of GSI attribute names to their values
|
|
619
|
+
*/
|
|
620
|
+
buildForCreate(item, options = {}) {
|
|
621
|
+
const attributes = {};
|
|
622
|
+
for (const [indexName, indexDef] of Object.entries(this.indexes)) {
|
|
623
|
+
if (options.excludeReadOnly && indexDef.isReadOnly) {
|
|
624
|
+
continue;
|
|
625
|
+
}
|
|
626
|
+
let key;
|
|
627
|
+
try {
|
|
628
|
+
key = indexDef.generateKey(item);
|
|
629
|
+
if (this.hasUndefinedValues(key)) {
|
|
630
|
+
throw IndexErrors.undefinedValues(indexName, "create", key, item);
|
|
631
|
+
}
|
|
632
|
+
} catch (error) {
|
|
633
|
+
if (error instanceof DynoTableError) throw error;
|
|
634
|
+
throw IndexErrors.generationFailed(
|
|
635
|
+
indexName,
|
|
636
|
+
"create",
|
|
637
|
+
item,
|
|
638
|
+
indexDef.partitionKey,
|
|
639
|
+
indexDef.sortKey,
|
|
640
|
+
error instanceof Error ? error : void 0
|
|
641
|
+
);
|
|
642
|
+
}
|
|
643
|
+
const gsiConfig = this.table.gsis[indexName];
|
|
644
|
+
if (!gsiConfig) {
|
|
645
|
+
throw ConfigurationErrors.gsiNotFound(indexName, this.table.tableName, Object.keys(this.table.gsis));
|
|
646
|
+
}
|
|
647
|
+
if (key.pk) {
|
|
648
|
+
attributes[gsiConfig.partitionKey] = key.pk;
|
|
649
|
+
}
|
|
650
|
+
if (key.sk && gsiConfig.sortKey) {
|
|
651
|
+
attributes[gsiConfig.sortKey] = key.sk;
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
return attributes;
|
|
655
|
+
}
|
|
656
|
+
/**
|
|
657
|
+
* Build index attributes for item updates
|
|
658
|
+
*
|
|
659
|
+
* @param currentData - The current data before update
|
|
660
|
+
* @param updates - The update data
|
|
661
|
+
* @param options - Options for building indexes
|
|
662
|
+
* @returns Record of GSI attribute names to their updated values
|
|
663
|
+
*/
|
|
664
|
+
buildForUpdate(currentData, updates, options = {}) {
|
|
665
|
+
const attributes = {};
|
|
666
|
+
const updatedItem = { ...currentData, ...updates };
|
|
667
|
+
if (options.forceRebuildIndexes && options.forceRebuildIndexes.length > 0) {
|
|
668
|
+
const invalidIndexes = options.forceRebuildIndexes.filter((indexName) => !this.indexes[indexName]);
|
|
669
|
+
if (invalidIndexes.length > 0) {
|
|
670
|
+
throw IndexErrors.notFound(invalidIndexes, Object.keys(this.indexes), void 0, this.table.tableName);
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
for (const [indexName, indexDef] of Object.entries(this.indexes)) {
|
|
674
|
+
const isForced = options.forceRebuildIndexes?.includes(indexName);
|
|
675
|
+
if (indexDef.isReadOnly && !isForced) {
|
|
676
|
+
continue;
|
|
677
|
+
}
|
|
678
|
+
if (!isForced) {
|
|
679
|
+
let shouldUpdateIndex = false;
|
|
680
|
+
try {
|
|
681
|
+
const currentKey = indexDef.generateKey(currentData);
|
|
682
|
+
const updatedKey = indexDef.generateKey(updatedItem);
|
|
683
|
+
if (currentKey.pk !== updatedKey.pk || currentKey.sk !== updatedKey.sk) {
|
|
684
|
+
shouldUpdateIndex = true;
|
|
685
|
+
}
|
|
686
|
+
} catch {
|
|
687
|
+
shouldUpdateIndex = true;
|
|
688
|
+
}
|
|
689
|
+
if (!shouldUpdateIndex) {
|
|
690
|
+
continue;
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
let key;
|
|
694
|
+
try {
|
|
695
|
+
key = indexDef.generateKey(updatedItem);
|
|
696
|
+
} catch (error) {
|
|
697
|
+
if (error instanceof DynoTableError) throw error;
|
|
698
|
+
throw IndexErrors.missingAttributes(
|
|
699
|
+
indexName,
|
|
700
|
+
"update",
|
|
701
|
+
[],
|
|
702
|
+
// We don't know which specific attributes are missing from the error
|
|
703
|
+
updates,
|
|
704
|
+
indexDef.isReadOnly
|
|
705
|
+
);
|
|
706
|
+
}
|
|
707
|
+
if (this.hasUndefinedValues(key)) {
|
|
708
|
+
throw IndexErrors.undefinedValues(indexName, "update", key, updates);
|
|
709
|
+
}
|
|
710
|
+
const gsiConfig = this.table.gsis[indexName];
|
|
711
|
+
if (!gsiConfig) {
|
|
712
|
+
throw ConfigurationErrors.gsiNotFound(indexName, this.table.tableName, Object.keys(this.table.gsis));
|
|
713
|
+
}
|
|
714
|
+
if (key.pk) {
|
|
715
|
+
attributes[gsiConfig.partitionKey] = key.pk;
|
|
716
|
+
}
|
|
717
|
+
if (key.sk && gsiConfig.sortKey) {
|
|
718
|
+
attributes[gsiConfig.sortKey] = key.sk;
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
return attributes;
|
|
722
|
+
}
|
|
723
|
+
/**
|
|
724
|
+
* Check if a key has undefined values
|
|
725
|
+
*
|
|
726
|
+
* @param key - The index key to check
|
|
727
|
+
* @returns True if the key contains undefined values, false otherwise
|
|
728
|
+
*/
|
|
729
|
+
hasUndefinedValues(key) {
|
|
730
|
+
return (key.pk?.includes("undefined") ?? false) || (key.sk?.includes("undefined") ?? false);
|
|
731
|
+
}
|
|
732
|
+
};
|
|
733
|
+
|
|
734
|
+
// src/entity/index-utils.ts
|
|
735
|
+
function buildIndexes(dataForKeyGeneration, table, indexes, excludeReadOnly = false) {
|
|
736
|
+
if (!indexes) {
|
|
737
|
+
return {};
|
|
738
|
+
}
|
|
739
|
+
const indexBuilder = new IndexBuilder(table, indexes);
|
|
740
|
+
return indexBuilder.buildForCreate(dataForKeyGeneration, { excludeReadOnly });
|
|
741
|
+
}
|
|
742
|
+
function buildIndexUpdates(currentData, updates, table, indexes, forceRebuildIndexes) {
|
|
743
|
+
if (!indexes) {
|
|
744
|
+
return {};
|
|
745
|
+
}
|
|
746
|
+
const indexBuilder = new IndexBuilder(table, indexes);
|
|
747
|
+
return indexBuilder.buildForUpdate(currentData, updates, { forceRebuildIndexes });
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
// src/entity/entity.ts
|
|
751
|
+
function defineEntity(config) {
|
|
752
|
+
const entityTypeAttributeName = config.settings?.entityTypeAttributeName ?? "entityType";
|
|
753
|
+
const buildIndexes2 = (dataForKeyGeneration, table, excludeReadOnly = false) => {
|
|
754
|
+
return buildIndexes(dataForKeyGeneration, table, config.indexes, excludeReadOnly);
|
|
755
|
+
};
|
|
756
|
+
const wrapMethodWithPreparation = (originalMethod, prepareFn, context) => {
|
|
757
|
+
const wrappedMethod = (...args) => {
|
|
758
|
+
prepareFn();
|
|
759
|
+
return originalMethod.call(context, ...args);
|
|
760
|
+
};
|
|
761
|
+
Object.setPrototypeOf(wrappedMethod, originalMethod);
|
|
762
|
+
const propertyNames = Object.getOwnPropertyNames(originalMethod);
|
|
763
|
+
for (let i = 0; i < propertyNames.length; i++) {
|
|
764
|
+
const prop = propertyNames[i];
|
|
765
|
+
if (prop !== "length" && prop !== "name" && prop !== "prototype") {
|
|
766
|
+
const descriptor = Object.getOwnPropertyDescriptor(originalMethod, prop);
|
|
767
|
+
if (descriptor && descriptor.writable !== false && !descriptor.get) {
|
|
768
|
+
wrappedMethod[prop] = originalMethod[prop];
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
return wrappedMethod;
|
|
773
|
+
};
|
|
774
|
+
const generateTimestamps = (timestampsToGenerate, data) => {
|
|
775
|
+
if (!config.settings?.timestamps) return {};
|
|
776
|
+
const timestamps = {};
|
|
777
|
+
const now = /* @__PURE__ */ new Date();
|
|
778
|
+
const unixTime = Math.floor(Date.now() / 1e3);
|
|
779
|
+
const { createdAt, updatedAt } = config.settings.timestamps;
|
|
780
|
+
if (createdAt && timestampsToGenerate.includes("createdAt") && !data.createdAt) {
|
|
781
|
+
const name = createdAt.attributeName ?? "createdAt";
|
|
782
|
+
timestamps[name] = createdAt.format === "UNIX" ? unixTime : now.toISOString();
|
|
783
|
+
}
|
|
784
|
+
if (updatedAt && timestampsToGenerate.includes("updatedAt") && !data.updatedAt) {
|
|
785
|
+
const name = updatedAt.attributeName ?? "updatedAt";
|
|
786
|
+
timestamps[name] = updatedAt.format === "UNIX" ? unixTime : now.toISOString();
|
|
787
|
+
}
|
|
788
|
+
return timestamps;
|
|
789
|
+
};
|
|
790
|
+
return {
|
|
791
|
+
name: config.name,
|
|
792
|
+
createRepository: (table) => {
|
|
793
|
+
const repository = {
|
|
794
|
+
create: (data) => {
|
|
795
|
+
const builder = table.create({});
|
|
796
|
+
const prepareValidatedItemAsync = async () => {
|
|
797
|
+
const validatedData = await config.schema["~standard"].validate(data);
|
|
798
|
+
if ("issues" in validatedData && validatedData.issues) {
|
|
799
|
+
throw EntityErrors.validationFailed(config.name, "create", validatedData.issues, data);
|
|
800
|
+
}
|
|
801
|
+
const dataForKeyGeneration = {
|
|
802
|
+
...validatedData.value,
|
|
803
|
+
...generateTimestamps(["createdAt", "updatedAt"], validatedData.value)
|
|
804
|
+
};
|
|
805
|
+
let primaryKey;
|
|
806
|
+
try {
|
|
807
|
+
primaryKey = config.primaryKey.generateKey(dataForKeyGeneration);
|
|
808
|
+
if (primaryKey.pk === void 0 || primaryKey.pk === null) {
|
|
809
|
+
throw EntityErrors.keyInvalidFormat(config.name, "create", dataForKeyGeneration, primaryKey);
|
|
810
|
+
}
|
|
811
|
+
} catch (error) {
|
|
812
|
+
if (error instanceof DynoTableError) throw error;
|
|
813
|
+
throw EntityErrors.keyGenerationFailed(
|
|
814
|
+
config.name,
|
|
815
|
+
"create",
|
|
816
|
+
dataForKeyGeneration,
|
|
817
|
+
extractRequiredAttributes(error),
|
|
818
|
+
error instanceof Error ? error : void 0
|
|
819
|
+
);
|
|
820
|
+
}
|
|
821
|
+
const indexes = buildIndexes(dataForKeyGeneration, table, config.indexes, false);
|
|
822
|
+
const validatedItem = {
|
|
823
|
+
...dataForKeyGeneration,
|
|
824
|
+
[entityTypeAttributeName]: config.name,
|
|
825
|
+
[table.partitionKey]: primaryKey.pk,
|
|
826
|
+
...table.sortKey ? { [table.sortKey]: primaryKey.sk } : {},
|
|
827
|
+
...indexes
|
|
828
|
+
};
|
|
829
|
+
Object.assign(builder, { item: validatedItem });
|
|
830
|
+
return validatedItem;
|
|
831
|
+
};
|
|
832
|
+
const prepareValidatedItemSync = () => {
|
|
833
|
+
const validationResult = config.schema["~standard"].validate(data);
|
|
834
|
+
if (validationResult instanceof Promise) {
|
|
835
|
+
throw EntityErrors.asyncValidationNotSupported(config.name, "create");
|
|
836
|
+
}
|
|
837
|
+
if ("issues" in validationResult && validationResult.issues) {
|
|
838
|
+
throw EntityErrors.validationFailed(config.name, "create", validationResult.issues, data);
|
|
839
|
+
}
|
|
840
|
+
const dataForKeyGeneration = {
|
|
841
|
+
...validationResult.value,
|
|
842
|
+
...generateTimestamps(["createdAt", "updatedAt"], validationResult.value)
|
|
843
|
+
};
|
|
844
|
+
let primaryKey;
|
|
845
|
+
try {
|
|
846
|
+
primaryKey = config.primaryKey.generateKey(dataForKeyGeneration);
|
|
847
|
+
if (primaryKey.pk === void 0 || primaryKey.pk === null) {
|
|
848
|
+
throw EntityErrors.keyInvalidFormat(config.name, "create", dataForKeyGeneration, primaryKey);
|
|
849
|
+
}
|
|
850
|
+
} catch (error) {
|
|
851
|
+
if (error instanceof DynoTableError) throw error;
|
|
852
|
+
throw EntityErrors.keyGenerationFailed(
|
|
853
|
+
config.name,
|
|
854
|
+
"create",
|
|
855
|
+
dataForKeyGeneration,
|
|
856
|
+
extractRequiredAttributes(error),
|
|
857
|
+
error instanceof Error ? error : void 0
|
|
858
|
+
);
|
|
859
|
+
}
|
|
860
|
+
const indexes = buildIndexes(dataForKeyGeneration, table, config.indexes, false);
|
|
861
|
+
const validatedItem = {
|
|
862
|
+
...dataForKeyGeneration,
|
|
863
|
+
[entityTypeAttributeName]: config.name,
|
|
864
|
+
[table.partitionKey]: primaryKey.pk,
|
|
865
|
+
...table.sortKey ? { [table.sortKey]: primaryKey.sk } : {},
|
|
866
|
+
...indexes
|
|
867
|
+
};
|
|
868
|
+
Object.assign(builder, { item: validatedItem });
|
|
869
|
+
return validatedItem;
|
|
870
|
+
};
|
|
871
|
+
const originalExecute = builder.execute;
|
|
872
|
+
builder.execute = async () => {
|
|
873
|
+
await prepareValidatedItemAsync();
|
|
874
|
+
return await originalExecute.call(builder);
|
|
875
|
+
};
|
|
876
|
+
const originalWithTransaction = builder.withTransaction;
|
|
877
|
+
if (originalWithTransaction) {
|
|
878
|
+
builder.withTransaction = wrapMethodWithPreparation(
|
|
879
|
+
originalWithTransaction,
|
|
880
|
+
prepareValidatedItemSync,
|
|
881
|
+
builder
|
|
882
|
+
);
|
|
883
|
+
}
|
|
884
|
+
const originalWithBatch = builder.withBatch;
|
|
885
|
+
if (originalWithBatch) {
|
|
886
|
+
builder.withBatch = wrapMethodWithPreparation(originalWithBatch, prepareValidatedItemSync, builder);
|
|
887
|
+
}
|
|
888
|
+
return createEntityAwarePutBuilder(builder, config.name);
|
|
889
|
+
},
|
|
890
|
+
upsert: (data) => {
|
|
891
|
+
const builder = table.put({});
|
|
892
|
+
const prepareValidatedItemAsync = async () => {
|
|
893
|
+
const validatedData = await config.schema["~standard"].validate(data);
|
|
894
|
+
if ("issues" in validatedData && validatedData.issues) {
|
|
895
|
+
throw EntityErrors.validationFailed(config.name, "upsert", validatedData.issues, data);
|
|
896
|
+
}
|
|
897
|
+
const dataForKeyGeneration = {
|
|
898
|
+
...validatedData.value,
|
|
899
|
+
...generateTimestamps(["createdAt", "updatedAt"], validatedData.value)
|
|
900
|
+
};
|
|
901
|
+
let primaryKey;
|
|
902
|
+
try {
|
|
903
|
+
primaryKey = config.primaryKey.generateKey(dataForKeyGeneration);
|
|
904
|
+
if (primaryKey.pk === void 0 || primaryKey.pk === null) {
|
|
905
|
+
throw EntityErrors.keyInvalidFormat(config.name, "upsert", dataForKeyGeneration, primaryKey);
|
|
906
|
+
}
|
|
907
|
+
} catch (error) {
|
|
908
|
+
if (error instanceof DynoTableError) throw error;
|
|
909
|
+
throw EntityErrors.keyGenerationFailed(
|
|
910
|
+
config.name,
|
|
911
|
+
"upsert",
|
|
912
|
+
dataForKeyGeneration,
|
|
913
|
+
extractRequiredAttributes(error),
|
|
914
|
+
error instanceof Error ? error : void 0
|
|
915
|
+
);
|
|
916
|
+
}
|
|
917
|
+
const indexes = buildIndexes2(dataForKeyGeneration, table, false);
|
|
918
|
+
const validatedItem = {
|
|
919
|
+
[table.partitionKey]: primaryKey.pk,
|
|
920
|
+
...table.sortKey ? { [table.sortKey]: primaryKey.sk } : {},
|
|
921
|
+
...dataForKeyGeneration,
|
|
922
|
+
[entityTypeAttributeName]: config.name,
|
|
923
|
+
...indexes
|
|
924
|
+
};
|
|
925
|
+
Object.assign(builder, { item: validatedItem });
|
|
926
|
+
return validatedItem;
|
|
927
|
+
};
|
|
928
|
+
const prepareValidatedItemSync = () => {
|
|
929
|
+
const validationResult = config.schema["~standard"].validate(data);
|
|
930
|
+
if (validationResult instanceof Promise) {
|
|
931
|
+
throw EntityErrors.asyncValidationNotSupported(config.name, "upsert");
|
|
932
|
+
}
|
|
933
|
+
if ("issues" in validationResult && validationResult.issues) {
|
|
934
|
+
throw EntityErrors.validationFailed(config.name, "upsert", validationResult.issues, data);
|
|
935
|
+
}
|
|
936
|
+
const dataForKeyGeneration = {
|
|
937
|
+
...validationResult.value,
|
|
938
|
+
...generateTimestamps(["createdAt", "updatedAt"], validationResult.value)
|
|
939
|
+
};
|
|
940
|
+
let primaryKey;
|
|
941
|
+
try {
|
|
942
|
+
primaryKey = config.primaryKey.generateKey(dataForKeyGeneration);
|
|
943
|
+
if (primaryKey.pk === void 0 || primaryKey.pk === null) {
|
|
944
|
+
throw EntityErrors.keyInvalidFormat(config.name, "upsert", dataForKeyGeneration, primaryKey);
|
|
945
|
+
}
|
|
946
|
+
} catch (error) {
|
|
947
|
+
if (error instanceof DynoTableError) throw error;
|
|
948
|
+
throw EntityErrors.keyGenerationFailed(
|
|
949
|
+
config.name,
|
|
950
|
+
"upsert",
|
|
951
|
+
dataForKeyGeneration,
|
|
952
|
+
extractRequiredAttributes(error),
|
|
953
|
+
error instanceof Error ? error : void 0
|
|
954
|
+
);
|
|
955
|
+
}
|
|
956
|
+
const indexes = buildIndexes(dataForKeyGeneration, table, config.indexes, false);
|
|
957
|
+
const validatedItem = {
|
|
958
|
+
[table.partitionKey]: primaryKey.pk,
|
|
959
|
+
...table.sortKey ? { [table.sortKey]: primaryKey.sk } : {},
|
|
960
|
+
...dataForKeyGeneration,
|
|
961
|
+
[entityTypeAttributeName]: config.name,
|
|
962
|
+
...indexes
|
|
963
|
+
};
|
|
964
|
+
Object.assign(builder, { item: validatedItem });
|
|
965
|
+
return validatedItem;
|
|
966
|
+
};
|
|
967
|
+
const originalExecute = builder.execute;
|
|
968
|
+
builder.execute = async () => {
|
|
969
|
+
const validatedItem = await prepareValidatedItemAsync();
|
|
970
|
+
await originalExecute.call(builder);
|
|
971
|
+
return validatedItem;
|
|
972
|
+
};
|
|
973
|
+
const originalWithTransaction = builder.withTransaction;
|
|
974
|
+
if (originalWithTransaction) {
|
|
975
|
+
builder.withTransaction = wrapMethodWithPreparation(
|
|
976
|
+
originalWithTransaction,
|
|
977
|
+
prepareValidatedItemSync,
|
|
978
|
+
builder
|
|
979
|
+
);
|
|
980
|
+
}
|
|
981
|
+
const originalWithBatch = builder.withBatch;
|
|
982
|
+
if (originalWithBatch) {
|
|
983
|
+
builder.withBatch = wrapMethodWithPreparation(originalWithBatch, prepareValidatedItemSync, builder);
|
|
984
|
+
}
|
|
985
|
+
return createEntityAwarePutBuilder(builder, config.name);
|
|
986
|
+
},
|
|
987
|
+
get: (key) => {
|
|
988
|
+
const builder = table.get(config.primaryKey.generateKey(key));
|
|
989
|
+
return createEntityAwareGetBuilder(builder, config.name);
|
|
990
|
+
},
|
|
991
|
+
update: (key, data) => {
|
|
992
|
+
const primaryKeyObj = config.primaryKey.generateKey(key);
|
|
993
|
+
const builder = table.update(primaryKeyObj);
|
|
994
|
+
builder.condition(eq(entityTypeAttributeName, config.name));
|
|
995
|
+
const entityAwareBuilder = createEntityAwareUpdateBuilder(builder, config.name);
|
|
996
|
+
entityAwareBuilder.configureEntityLogic({
|
|
997
|
+
data,
|
|
998
|
+
key,
|
|
999
|
+
table,
|
|
1000
|
+
indexes: config.indexes,
|
|
1001
|
+
generateTimestamps: () => generateTimestamps(["updatedAt"], data),
|
|
1002
|
+
buildIndexUpdates
|
|
1003
|
+
});
|
|
1004
|
+
return entityAwareBuilder;
|
|
1005
|
+
},
|
|
1006
|
+
delete: (key) => {
|
|
1007
|
+
const builder = table.delete(config.primaryKey.generateKey(key));
|
|
1008
|
+
builder.condition(eq(entityTypeAttributeName, config.name));
|
|
1009
|
+
return createEntityAwareDeleteBuilder(builder, config.name);
|
|
1010
|
+
},
|
|
1011
|
+
query: Object.entries(config.queries || {}).reduce(
|
|
1012
|
+
(acc, [key, inputCallback]) => {
|
|
1013
|
+
acc[key] = (input) => {
|
|
1014
|
+
const queryEntity = {
|
|
1015
|
+
scan: repository.scan,
|
|
1016
|
+
get: (key2) => createEntityAwareGetBuilder(table.get(key2), config.name),
|
|
1017
|
+
query: (keyCondition) => {
|
|
1018
|
+
return table.query(keyCondition);
|
|
1019
|
+
}
|
|
1020
|
+
};
|
|
1021
|
+
const queryBuilderCallback = inputCallback(input);
|
|
1022
|
+
const builder = queryBuilderCallback(queryEntity);
|
|
1023
|
+
if (builder && typeof builder === "object" && "filter" in builder && typeof builder.filter === "function") {
|
|
1024
|
+
builder.filter(eq(entityTypeAttributeName, config.name));
|
|
1025
|
+
}
|
|
1026
|
+
if (builder && typeof builder === "object" && "execute" in builder) {
|
|
1027
|
+
const originalExecute = builder.execute;
|
|
1028
|
+
builder.execute = async () => {
|
|
1029
|
+
const queryFn = config.queries[key];
|
|
1030
|
+
if (queryFn && typeof queryFn === "function") {
|
|
1031
|
+
const schema = queryFn.schema;
|
|
1032
|
+
if (schema?.["~standard"]?.validate && typeof schema["~standard"].validate === "function") {
|
|
1033
|
+
const validationResult = schema["~standard"].validate(input);
|
|
1034
|
+
if ("issues" in validationResult && validationResult.issues) {
|
|
1035
|
+
throw EntityErrors.queryInputValidationFailed(config.name, key, validationResult.issues, input);
|
|
1036
|
+
}
|
|
1037
|
+
}
|
|
1038
|
+
}
|
|
1039
|
+
const result = await originalExecute.call(builder);
|
|
1040
|
+
if (!result) {
|
|
1041
|
+
throw OperationErrors.queryFailed(config.name, { queryName: key }, void 0);
|
|
1042
|
+
}
|
|
1043
|
+
return result;
|
|
1044
|
+
};
|
|
1045
|
+
}
|
|
1046
|
+
return builder;
|
|
1047
|
+
};
|
|
1048
|
+
return acc;
|
|
1049
|
+
},
|
|
1050
|
+
{}
|
|
1051
|
+
),
|
|
1052
|
+
scan: () => {
|
|
1053
|
+
const builder = table.scan();
|
|
1054
|
+
builder.filter(eq(entityTypeAttributeName, config.name));
|
|
1055
|
+
return builder;
|
|
1056
|
+
}
|
|
1057
|
+
};
|
|
1058
|
+
return repository;
|
|
1059
|
+
}
|
|
1060
|
+
};
|
|
1061
|
+
}
|
|
1062
|
+
function createQueries() {
|
|
1063
|
+
return {
|
|
1064
|
+
input: (schema) => ({
|
|
1065
|
+
query: (handler) => {
|
|
1066
|
+
const queryFn = (input) => (entity) => handler({ input, entity });
|
|
1067
|
+
queryFn.schema = schema;
|
|
1068
|
+
return queryFn;
|
|
1069
|
+
}
|
|
1070
|
+
})
|
|
1071
|
+
};
|
|
1072
|
+
}
|
|
1073
|
+
function createIndex() {
|
|
1074
|
+
return {
|
|
1075
|
+
input: (schema) => {
|
|
1076
|
+
const createIndexBuilder = (isReadOnly = false) => ({
|
|
1077
|
+
partitionKey: (pkFn) => ({
|
|
1078
|
+
sortKey: (skFn) => {
|
|
1079
|
+
const index = {
|
|
1080
|
+
name: "custom",
|
|
1081
|
+
partitionKey: "pk",
|
|
1082
|
+
sortKey: "sk",
|
|
1083
|
+
isReadOnly,
|
|
1084
|
+
generateKey: (item) => {
|
|
1085
|
+
const data = schema["~standard"].validate(item);
|
|
1086
|
+
if ("issues" in data && data.issues) {
|
|
1087
|
+
throw ValidationErrors.indexSchemaValidationFailed(data.issues, "both");
|
|
1088
|
+
}
|
|
1089
|
+
const validData = "value" in data ? data.value : item;
|
|
1090
|
+
return { pk: pkFn(validData), sk: skFn(validData) };
|
|
1091
|
+
}
|
|
1092
|
+
};
|
|
1093
|
+
return Object.assign(index, {
|
|
1094
|
+
readOnly: (value = false) => ({
|
|
1095
|
+
...index,
|
|
1096
|
+
isReadOnly: value
|
|
1097
|
+
})
|
|
1098
|
+
});
|
|
1099
|
+
},
|
|
1100
|
+
withoutSortKey: () => {
|
|
1101
|
+
const index = {
|
|
1102
|
+
name: "custom",
|
|
1103
|
+
partitionKey: "pk",
|
|
1104
|
+
isReadOnly,
|
|
1105
|
+
generateKey: (item) => {
|
|
1106
|
+
const data = schema["~standard"].validate(item);
|
|
1107
|
+
if ("issues" in data && data.issues) {
|
|
1108
|
+
throw ValidationErrors.indexSchemaValidationFailed(data.issues, "partition");
|
|
1109
|
+
}
|
|
1110
|
+
const validData = "value" in data ? data.value : item;
|
|
1111
|
+
return { pk: pkFn(validData) };
|
|
1112
|
+
}
|
|
1113
|
+
};
|
|
1114
|
+
return Object.assign(index, {
|
|
1115
|
+
readOnly: (value = true) => ({
|
|
1116
|
+
...index,
|
|
1117
|
+
isReadOnly: value
|
|
1118
|
+
})
|
|
1119
|
+
});
|
|
1120
|
+
}
|
|
1121
|
+
}),
|
|
1122
|
+
readOnly: (value = true) => createIndexBuilder(value)
|
|
1123
|
+
});
|
|
1124
|
+
return createIndexBuilder(false);
|
|
1125
|
+
}
|
|
1126
|
+
};
|
|
1127
|
+
}
|
|
1128
|
+
|
|
1129
|
+
exports.createIndex = createIndex;
|
|
1130
|
+
exports.createQueries = createQueries;
|
|
1131
|
+
exports.defineEntity = defineEntity;
|