dyno-table 2.1.0 → 2.2.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.
Files changed (42) hide show
  1. package/README.md +628 -348
  2. package/dist/builders/delete-builder.d.cts +1 -1
  3. package/dist/builders/delete-builder.d.ts +1 -1
  4. package/dist/builders/put-builder.cjs.map +1 -1
  5. package/dist/builders/put-builder.d.cts +1 -1
  6. package/dist/builders/put-builder.d.ts +1 -1
  7. package/dist/builders/put-builder.js.map +1 -1
  8. package/dist/builders/query-builder.cjs +44 -21
  9. package/dist/builders/query-builder.cjs.map +1 -1
  10. package/dist/builders/query-builder.d.cts +1 -1
  11. package/dist/builders/query-builder.d.ts +1 -1
  12. package/dist/builders/query-builder.js +44 -21
  13. package/dist/builders/query-builder.js.map +1 -1
  14. package/dist/builders/update-builder.cjs.map +1 -1
  15. package/dist/builders/update-builder.d.cts +6 -6
  16. package/dist/builders/update-builder.d.ts +6 -6
  17. package/dist/builders/update-builder.js.map +1 -1
  18. package/dist/entity.cjs +183 -41
  19. package/dist/entity.cjs.map +1 -1
  20. package/dist/entity.d.cts +91 -10
  21. package/dist/entity.d.ts +91 -10
  22. package/dist/entity.js +183 -41
  23. package/dist/entity.js.map +1 -1
  24. package/dist/index.cjs +2667 -2489
  25. package/dist/index.cjs.map +1 -1
  26. package/dist/index.d.cts +5 -5
  27. package/dist/index.d.ts +5 -5
  28. package/dist/index.js +2667 -2489
  29. package/dist/index.js.map +1 -1
  30. package/dist/{query-builder-BNWRCrJW.d.ts → query-builder-CUWdavZw.d.ts} +2 -0
  31. package/dist/{query-builder-DZ9JKgBN.d.cts → query-builder-DoZzZz_c.d.cts} +2 -0
  32. package/dist/{table-BhEeYauU.d.ts → table-CZBMkW2Z.d.ts} +9 -8
  33. package/dist/{table-BpNOboD9.d.cts → table-f-3wsT7K.d.cts} +9 -8
  34. package/dist/table.cjs +2510 -2474
  35. package/dist/table.cjs.map +1 -1
  36. package/dist/table.d.cts +9 -9
  37. package/dist/table.d.ts +9 -9
  38. package/dist/table.js +2510 -2474
  39. package/dist/table.js.map +1 -1
  40. package/package.json +2 -2
  41. package/dist/{batch-builder-CcxFDKhe.d.cts → batch-builder-BPoHyN_Q.d.cts} +1 -1
  42. package/dist/{batch-builder-BytHNL_u.d.ts → batch-builder-Cdo49C2r.d.ts} +1 -1
package/dist/entity.cjs CHANGED
@@ -27,6 +27,137 @@ function createEntityAwareGetBuilder(builder, entityName) {
27
27
  function createEntityAwareDeleteBuilder(builder, entityName) {
28
28
  return createEntityAwareBuilder(builder, entityName);
29
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
+ if (value === void 0) {
116
+ throw new Error("Value is required when setting a single path");
117
+ }
118
+ this.builder.set(valuesOrPath, value);
119
+ }
120
+ return this;
121
+ }
122
+ remove(path) {
123
+ this.builder.remove(path);
124
+ return this;
125
+ }
126
+ add(path, value) {
127
+ this.builder.add(path, value);
128
+ return this;
129
+ }
130
+ deleteElementsFromSet(path, value) {
131
+ this.builder.deleteElementsFromSet(path, value);
132
+ return this;
133
+ }
134
+ condition(condition) {
135
+ this.builder.condition(condition);
136
+ return this;
137
+ }
138
+ returnValues(returnValues) {
139
+ this.builder.returnValues(returnValues);
140
+ return this;
141
+ }
142
+ toDynamoCommand() {
143
+ return this.builder.toDynamoCommand();
144
+ }
145
+ withTransaction(transaction) {
146
+ this.applyEntityUpdates();
147
+ this.builder.withTransaction(transaction);
148
+ }
149
+ debug() {
150
+ return this.builder.debug();
151
+ }
152
+ async execute() {
153
+ this.updateDataApplied = false;
154
+ this.applyEntityUpdates();
155
+ return this.builder.execute();
156
+ }
157
+ };
158
+ function createEntityAwareUpdateBuilder(builder, entityName) {
159
+ return new EntityAwareUpdateBuilder(builder, entityName);
160
+ }
30
161
 
31
162
  // src/conditions.ts
32
163
  var createComparisonCondition = (type) => (attr, value) => ({
@@ -83,51 +214,61 @@ var IndexBuilder = class {
83
214
  * @param options - Options for building indexes
84
215
  * @returns Record of GSI attribute names to their updated values
85
216
  */
86
- buildForUpdate(currentData, updates) {
217
+ buildForUpdate(currentData, updates, options = {}) {
87
218
  const attributes = {};
88
219
  const updatedItem = { ...currentData, ...updates };
220
+ if (options.forceRebuildIndexes && options.forceRebuildIndexes.length > 0) {
221
+ const invalidIndexes = options.forceRebuildIndexes.filter((indexName) => !this.indexes[indexName]);
222
+ if (invalidIndexes.length > 0) {
223
+ throw new Error(
224
+ `Cannot force rebuild unknown indexes: ${invalidIndexes.join(", ")}. Available indexes: ${Object.keys(this.indexes).join(", ")}`
225
+ );
226
+ }
227
+ }
89
228
  for (const [indexName, indexDef] of Object.entries(this.indexes)) {
90
- if (indexDef.isReadOnly) {
229
+ const isForced = options.forceRebuildIndexes?.includes(indexName);
230
+ if (indexDef.isReadOnly && !isForced) {
91
231
  continue;
92
232
  }
93
- let shouldUpdateIndex = false;
94
- try {
95
- const currentKey = indexDef.generateKey(currentData);
96
- const updatedKey = indexDef.generateKey(updatedItem);
97
- if (currentKey.pk !== updatedKey.pk || currentKey.sk !== updatedKey.sk) {
233
+ if (!isForced) {
234
+ let shouldUpdateIndex = false;
235
+ try {
236
+ const currentKey = indexDef.generateKey(currentData);
237
+ const updatedKey = indexDef.generateKey(updatedItem);
238
+ if (currentKey.pk !== updatedKey.pk || currentKey.sk !== updatedKey.sk) {
239
+ shouldUpdateIndex = true;
240
+ }
241
+ } catch {
98
242
  shouldUpdateIndex = true;
99
243
  }
100
- } catch {
101
- shouldUpdateIndex = true;
102
- }
103
- if (!shouldUpdateIndex) {
104
- continue;
244
+ if (!shouldUpdateIndex) {
245
+ continue;
246
+ }
105
247
  }
248
+ let key;
106
249
  try {
107
- const key = indexDef.generateKey(updatedItem);
108
- if (this.hasUndefinedValues(key)) {
109
- throw new Error(
110
- `Cannot update entity: insufficient data to regenerate index "${indexName}". All attributes required by the index must be provided in the update operation, or the index must be marked as readOnly.`
111
- );
112
- }
113
- const gsiConfig = this.table.gsis[indexName];
114
- if (!gsiConfig) {
115
- throw new Error(`GSI configuration not found for index: ${indexName}`);
116
- }
117
- if (key.pk) {
118
- attributes[gsiConfig.partitionKey] = key.pk;
119
- }
120
- if (key.sk && gsiConfig.sortKey) {
121
- attributes[gsiConfig.sortKey] = key.sk;
122
- }
250
+ key = indexDef.generateKey(updatedItem);
123
251
  } catch (error) {
124
- if (error instanceof Error && error.message.includes("insufficient data")) {
125
- throw error;
252
+ if (error instanceof Error) {
253
+ throw new Error(`Missing attributes: ${error.message}`);
126
254
  }
255
+ throw error;
256
+ }
257
+ if (this.hasUndefinedValues(key)) {
127
258
  throw new Error(
128
- `Cannot update entity: insufficient data to regenerate index "${indexName}". All attributes required by the index must be provided in the update operation, or the index must be readOnly.`
259
+ `Missing attributes: Cannot update entity: insufficient data to regenerate index "${indexName}". All attributes required by the index must be provided in the update operation, or the index must be marked as readOnly.`
129
260
  );
130
261
  }
262
+ const gsiConfig = this.table.gsis[indexName];
263
+ if (!gsiConfig) {
264
+ throw new Error(`GSI configuration not found for index: ${indexName}`);
265
+ }
266
+ if (key.pk) {
267
+ attributes[gsiConfig.partitionKey] = key.pk;
268
+ }
269
+ if (key.sk && gsiConfig.sortKey) {
270
+ attributes[gsiConfig.sortKey] = key.sk;
271
+ }
131
272
  }
132
273
  return attributes;
133
274
  }
@@ -150,12 +291,12 @@ function buildIndexes(dataForKeyGeneration, table, indexes, excludeReadOnly = fa
150
291
  const indexBuilder = new IndexBuilder(table, indexes);
151
292
  return indexBuilder.buildForCreate(dataForKeyGeneration, { excludeReadOnly });
152
293
  }
153
- function buildIndexUpdates(currentData, updates, table, indexes) {
294
+ function buildIndexUpdates(currentData, updates, table, indexes, forceRebuildIndexes) {
154
295
  if (!indexes) {
155
296
  return {};
156
297
  }
157
298
  const indexBuilder = new IndexBuilder(table, indexes);
158
- return indexBuilder.buildForUpdate(currentData, updates);
299
+ return indexBuilder.buildForUpdate(currentData, updates, { forceRebuildIndexes });
159
300
  }
160
301
 
161
302
  // src/entity/entity.ts
@@ -349,15 +490,16 @@ function defineEntity(config) {
349
490
  const primaryKeyObj = config.primaryKey.generateKey(key);
350
491
  const builder = table.update(primaryKeyObj);
351
492
  builder.condition(eq(entityTypeAttributeName, config.name));
352
- const timestamps = generateTimestamps(["updatedAt"], data);
353
- const indexUpdates = buildIndexUpdates(
354
- { ...key },
355
- { ...data, ...timestamps },
493
+ const entityAwareBuilder = createEntityAwareUpdateBuilder(builder, config.name);
494
+ entityAwareBuilder.configureEntityLogic({
495
+ data,
496
+ key,
356
497
  table,
357
- config.indexes
358
- );
359
- builder.set({ ...data, ...timestamps, ...indexUpdates });
360
- return builder;
498
+ indexes: config.indexes,
499
+ generateTimestamps: () => generateTimestamps(["updatedAt"], data),
500
+ buildIndexUpdates
501
+ });
502
+ return entityAwareBuilder;
361
503
  },
362
504
  delete: (key) => {
363
505
  const builder = table.delete(config.primaryKey.generateKey(key));