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/entity.cjs CHANGED
@@ -1,20 +1,1131 @@
1
1
  'use strict';
2
2
 
3
- var chunkZ334X72N_cjs = require('./chunk-Z334X72N.cjs');
4
- require('./chunk-ELULXDSB.cjs');
5
- require('./chunk-7UJJ7JXM.cjs');
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
- Object.defineProperty(exports, "createIndex", {
10
- enumerable: true,
11
- get: function () { return chunkZ334X72N_cjs.createIndex; }
12
- });
13
- Object.defineProperty(exports, "createQueries", {
14
- enumerable: true,
15
- get: function () { return chunkZ334X72N_cjs.createQueries; }
16
- });
17
- Object.defineProperty(exports, "defineEntity", {
18
- enumerable: true,
19
- get: function () { return chunkZ334X72N_cjs.defineEntity; }
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;