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.d.ts CHANGED
@@ -1,18 +1,20 @@
1
- import { G as GetBuilder } from './batch-builder-BytHNL_u.js';
2
- import { S as ScanBuilder, T as Table } from './table-BhEeYauU.js';
3
- import { UpdateBuilder } from './builders/update-builder.js';
4
- import { StandardSchemaV1 } from './standard-schema.js';
5
1
  import { DynamoItem, TableConfig, Index } from './types.js';
6
2
  import { PutBuilder } from './builders/put-builder.js';
3
+ import { G as GetBuilder } from './batch-builder-Cdo49C2r.js';
7
4
  import { DeleteBuilder } from './builders/delete-builder.js';
8
- import { Q as QueryBuilder } from './query-builder-BNWRCrJW.js';
9
- import { r as PrimaryKeyWithoutExpression, P as PrimaryKey } from './conditions-DD0bvyHm.js';
10
- import './builder-types-CzuLR4Th.js';
11
- import './builders/transaction-builder.js';
5
+ import { UpdateBuilder } from './builders/update-builder.js';
6
+ import { s as Path, t as PathType, C as Condition, q as ConditionOperator, r as PrimaryKeyWithoutExpression, P as PrimaryKey } from './conditions-DD0bvyHm.js';
7
+ import { TransactionBuilder } from './builders/transaction-builder.js';
8
+ import { U as UpdateCommandParams } from './builder-types-CzuLR4Th.js';
9
+ import { T as Table, S as ScanBuilder } from './table-CZBMkW2Z.js';
10
+ import { Q as QueryBuilder } from './query-builder-CUWdavZw.js';
11
+ import { StandardSchemaV1 } from './standard-schema.js';
12
12
  import '@aws-sdk/lib-dynamodb';
13
13
  import './builders/condition-check-builder.js';
14
14
  import './builders/paginator.js';
15
15
 
16
+ type SetElementType<T> = T extends Set<infer U> ? U : T extends Array<infer U> ? U : never;
17
+ type PathSetElementType<T, K extends Path<T>> = SetElementType<PathType<T, K>>;
16
18
  /**
17
19
  * Entity-aware wrapper for PutBuilder that automatically provides entity name to batch operations
18
20
  */
@@ -31,8 +33,87 @@ type EntityAwareGetBuilder<T extends DynamoItem> = GetBuilder<T> & {
31
33
  type EntityAwareDeleteBuilder = DeleteBuilder & {
32
34
  readonly entityName: string;
33
35
  };
36
+ /**
37
+ * Entity-aware wrapper for UpdateBuilder that adds forceIndexRebuild functionality
38
+ * and automatically provides entity name to batch operations
39
+ */
40
+ declare class EntityAwareUpdateBuilder<T extends DynamoItem> {
41
+ private forceRebuildIndexes;
42
+ readonly entityName: string;
43
+ private builder;
44
+ private entityConfig?;
45
+ private updateDataApplied;
46
+ constructor(builder: UpdateBuilder<T>, entityName: string);
47
+ /**
48
+ * Configure entity-specific logic for automatic timestamp generation and index updates
49
+ */
50
+ configureEntityLogic(config: {
51
+ data: Partial<T>;
52
+ key: T;
53
+ table: Table;
54
+ indexes: Record<string, IndexDefinition<T>> | undefined;
55
+ generateTimestamps: () => Record<string, string | number>;
56
+ buildIndexUpdates: (currentData: T, updates: Partial<T>, table: Table, indexes: Record<string, IndexDefinition<T>> | undefined, forceRebuildIndexes?: string[]) => Record<string, string>;
57
+ }): void;
58
+ /**
59
+ * Forces a rebuild of one or more readonly indexes during the update operation.
60
+ *
61
+ * By default, readonly indexes are not updated during entity updates to prevent
62
+ * errors when required index attributes are missing. This method allows you to
63
+ * override that behavior and force specific indexes to be rebuilt.
64
+ *
65
+ * @example
66
+ * ```typescript
67
+ * // Force rebuild a single readonly index
68
+ * const result = await repo.update({ id: 'TREX-001' }, { status: 'ACTIVE' })
69
+ * .forceIndexRebuild('gsi1')
70
+ * .execute();
71
+ *
72
+ * // Force rebuild multiple readonly indexes
73
+ * const result = await repo.update({ id: 'TREX-001' }, { status: 'ACTIVE' })
74
+ * .forceIndexRebuild(['gsi1', 'gsi2'])
75
+ * .execute();
76
+ *
77
+ * // Chain with other update operations
78
+ * const result = await repo.update({ id: 'TREX-001' }, { status: 'ACTIVE' })
79
+ * .set('lastUpdated', new Date().toISOString())
80
+ * .forceIndexRebuild('gsi1')
81
+ * .condition(op => op.eq('status', 'INACTIVE'))
82
+ * .execute();
83
+ * ```
84
+ *
85
+ * @param indexes - A single index name or array of index names to force rebuild
86
+ * @returns The builder instance for method chaining
87
+ */
88
+ forceIndexRebuild(indexes: string | string[]): this;
89
+ /**
90
+ * Gets the list of indexes that should be force rebuilt.
91
+ * This is used internally by entity update logic.
92
+ *
93
+ * @returns Array of index names to force rebuild
94
+ */
95
+ getForceRebuildIndexes(): string[];
96
+ /**
97
+ * Apply entity-specific update data (timestamps and index updates)
98
+ * This is called automatically when needed
99
+ */
100
+ private applyEntityUpdates;
101
+ set(values: Partial<T>): this;
102
+ set<K extends Path<T>>(path: K, value: PathType<T, K>): this;
103
+ remove<K extends Path<T>>(path: K): this;
104
+ add<K extends Path<T>>(path: K, value: PathType<T, K>): this;
105
+ deleteElementsFromSet<K extends Path<T>>(path: K, value: PathSetElementType<T, K>[] | Set<PathSetElementType<T, K>>): this;
106
+ condition(condition: Condition | ((op: ConditionOperator<T>) => Condition)): this;
107
+ returnValues(returnValues: "ALL_NEW" | "UPDATED_NEW" | "ALL_OLD" | "UPDATED_OLD" | "NONE"): this;
108
+ toDynamoCommand(): UpdateCommandParams;
109
+ withTransaction(transaction: TransactionBuilder): void;
110
+ debug(): ReturnType<UpdateBuilder<T>['debug']>;
111
+ execute(): Promise<{
112
+ item?: T;
113
+ }>;
114
+ }
34
115
 
35
- type QueryFunction<T extends DynamoItem, I, R> = (input: I) => R;
116
+ type QueryFunction<_T extends DynamoItem, I, R> = (input: I) => R;
36
117
  type QueryFunctionWithSchema<T extends DynamoItem, I, R> = QueryFunction<T, I, R> & {
37
118
  schema?: StandardSchemaV1<I>;
38
119
  };
@@ -120,7 +201,7 @@ Q extends QueryRecord<T> = QueryRecord<T>> {
120
201
  create: (data: TInput) => EntityAwarePutBuilder<T>;
121
202
  upsert: (data: TInput & I) => EntityAwarePutBuilder<T>;
122
203
  get: (key: I) => EntityAwareGetBuilder<T>;
123
- update: (key: I, data: Partial<T>) => UpdateBuilder<T>;
204
+ update: (key: I, data: Partial<T>) => EntityAwareUpdateBuilder<T>;
124
205
  delete: (key: I) => EntityAwareDeleteBuilder;
125
206
  query: Q;
126
207
  scan: () => ScanBuilder<T>;
package/dist/entity.js CHANGED
@@ -25,6 +25,137 @@ function createEntityAwareGetBuilder(builder, entityName) {
25
25
  function createEntityAwareDeleteBuilder(builder, entityName) {
26
26
  return createEntityAwareBuilder(builder, entityName);
27
27
  }
28
+ var EntityAwareUpdateBuilder = class {
29
+ forceRebuildIndexes = [];
30
+ entityName;
31
+ builder;
32
+ entityConfig;
33
+ updateDataApplied = false;
34
+ constructor(builder, entityName) {
35
+ this.builder = builder;
36
+ this.entityName = entityName;
37
+ }
38
+ /**
39
+ * Configure entity-specific logic for automatic timestamp generation and index updates
40
+ */
41
+ configureEntityLogic(config) {
42
+ this.entityConfig = config;
43
+ }
44
+ /**
45
+ * Forces a rebuild of one or more readonly indexes during the update operation.
46
+ *
47
+ * By default, readonly indexes are not updated during entity updates to prevent
48
+ * errors when required index attributes are missing. This method allows you to
49
+ * override that behavior and force specific indexes to be rebuilt.
50
+ *
51
+ * @example
52
+ * ```typescript
53
+ * // Force rebuild a single readonly index
54
+ * const result = await repo.update({ id: 'TREX-001' }, { status: 'ACTIVE' })
55
+ * .forceIndexRebuild('gsi1')
56
+ * .execute();
57
+ *
58
+ * // Force rebuild multiple readonly indexes
59
+ * const result = await repo.update({ id: 'TREX-001' }, { status: 'ACTIVE' })
60
+ * .forceIndexRebuild(['gsi1', 'gsi2'])
61
+ * .execute();
62
+ *
63
+ * // Chain with other update operations
64
+ * const result = await repo.update({ id: 'TREX-001' }, { status: 'ACTIVE' })
65
+ * .set('lastUpdated', new Date().toISOString())
66
+ * .forceIndexRebuild('gsi1')
67
+ * .condition(op => op.eq('status', 'INACTIVE'))
68
+ * .execute();
69
+ * ```
70
+ *
71
+ * @param indexes - A single index name or array of index names to force rebuild
72
+ * @returns The builder instance for method chaining
73
+ */
74
+ forceIndexRebuild(indexes) {
75
+ if (Array.isArray(indexes)) {
76
+ this.forceRebuildIndexes = [...this.forceRebuildIndexes, ...indexes];
77
+ } else {
78
+ this.forceRebuildIndexes.push(indexes);
79
+ }
80
+ return this;
81
+ }
82
+ /**
83
+ * Gets the list of indexes that should be force rebuilt.
84
+ * This is used internally by entity update logic.
85
+ *
86
+ * @returns Array of index names to force rebuild
87
+ */
88
+ getForceRebuildIndexes() {
89
+ return [...this.forceRebuildIndexes];
90
+ }
91
+ /**
92
+ * Apply entity-specific update data (timestamps and index updates)
93
+ * This is called automatically when needed
94
+ */
95
+ applyEntityUpdates() {
96
+ if (!this.entityConfig || this.updateDataApplied) return;
97
+ const timestamps = this.entityConfig.generateTimestamps();
98
+ const updatedItem = { ...this.entityConfig.key, ...this.entityConfig.data, ...timestamps };
99
+ const indexUpdates = this.entityConfig.buildIndexUpdates(
100
+ this.entityConfig.key,
101
+ updatedItem,
102
+ this.entityConfig.table,
103
+ this.entityConfig.indexes,
104
+ this.forceRebuildIndexes
105
+ );
106
+ this.builder.set({ ...this.entityConfig.data, ...timestamps, ...indexUpdates });
107
+ this.updateDataApplied = true;
108
+ }
109
+ set(valuesOrPath, value) {
110
+ if (typeof valuesOrPath === "object") {
111
+ this.builder.set(valuesOrPath);
112
+ } else {
113
+ if (value === void 0) {
114
+ throw new Error("Value is required when setting a single path");
115
+ }
116
+ this.builder.set(valuesOrPath, value);
117
+ }
118
+ return this;
119
+ }
120
+ remove(path) {
121
+ this.builder.remove(path);
122
+ return this;
123
+ }
124
+ add(path, value) {
125
+ this.builder.add(path, value);
126
+ return this;
127
+ }
128
+ deleteElementsFromSet(path, value) {
129
+ this.builder.deleteElementsFromSet(path, value);
130
+ return this;
131
+ }
132
+ condition(condition) {
133
+ this.builder.condition(condition);
134
+ return this;
135
+ }
136
+ returnValues(returnValues) {
137
+ this.builder.returnValues(returnValues);
138
+ return this;
139
+ }
140
+ toDynamoCommand() {
141
+ return this.builder.toDynamoCommand();
142
+ }
143
+ withTransaction(transaction) {
144
+ this.applyEntityUpdates();
145
+ this.builder.withTransaction(transaction);
146
+ }
147
+ debug() {
148
+ return this.builder.debug();
149
+ }
150
+ async execute() {
151
+ this.updateDataApplied = false;
152
+ this.applyEntityUpdates();
153
+ return this.builder.execute();
154
+ }
155
+ };
156
+ function createEntityAwareUpdateBuilder(builder, entityName) {
157
+ return new EntityAwareUpdateBuilder(builder, entityName);
158
+ }
28
159
 
29
160
  // src/conditions.ts
30
161
  var createComparisonCondition = (type) => (attr, value) => ({
@@ -81,51 +212,61 @@ var IndexBuilder = class {
81
212
  * @param options - Options for building indexes
82
213
  * @returns Record of GSI attribute names to their updated values
83
214
  */
84
- buildForUpdate(currentData, updates) {
215
+ buildForUpdate(currentData, updates, options = {}) {
85
216
  const attributes = {};
86
217
  const updatedItem = { ...currentData, ...updates };
218
+ if (options.forceRebuildIndexes && options.forceRebuildIndexes.length > 0) {
219
+ const invalidIndexes = options.forceRebuildIndexes.filter((indexName) => !this.indexes[indexName]);
220
+ if (invalidIndexes.length > 0) {
221
+ throw new Error(
222
+ `Cannot force rebuild unknown indexes: ${invalidIndexes.join(", ")}. Available indexes: ${Object.keys(this.indexes).join(", ")}`
223
+ );
224
+ }
225
+ }
87
226
  for (const [indexName, indexDef] of Object.entries(this.indexes)) {
88
- if (indexDef.isReadOnly) {
227
+ const isForced = options.forceRebuildIndexes?.includes(indexName);
228
+ if (indexDef.isReadOnly && !isForced) {
89
229
  continue;
90
230
  }
91
- let shouldUpdateIndex = false;
92
- try {
93
- const currentKey = indexDef.generateKey(currentData);
94
- const updatedKey = indexDef.generateKey(updatedItem);
95
- if (currentKey.pk !== updatedKey.pk || currentKey.sk !== updatedKey.sk) {
231
+ if (!isForced) {
232
+ let shouldUpdateIndex = false;
233
+ try {
234
+ const currentKey = indexDef.generateKey(currentData);
235
+ const updatedKey = indexDef.generateKey(updatedItem);
236
+ if (currentKey.pk !== updatedKey.pk || currentKey.sk !== updatedKey.sk) {
237
+ shouldUpdateIndex = true;
238
+ }
239
+ } catch {
96
240
  shouldUpdateIndex = true;
97
241
  }
98
- } catch {
99
- shouldUpdateIndex = true;
100
- }
101
- if (!shouldUpdateIndex) {
102
- continue;
242
+ if (!shouldUpdateIndex) {
243
+ continue;
244
+ }
103
245
  }
246
+ let key;
104
247
  try {
105
- const key = indexDef.generateKey(updatedItem);
106
- if (this.hasUndefinedValues(key)) {
107
- throw new Error(
108
- `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.`
109
- );
110
- }
111
- const gsiConfig = this.table.gsis[indexName];
112
- if (!gsiConfig) {
113
- throw new Error(`GSI configuration not found for index: ${indexName}`);
114
- }
115
- if (key.pk) {
116
- attributes[gsiConfig.partitionKey] = key.pk;
117
- }
118
- if (key.sk && gsiConfig.sortKey) {
119
- attributes[gsiConfig.sortKey] = key.sk;
120
- }
248
+ key = indexDef.generateKey(updatedItem);
121
249
  } catch (error) {
122
- if (error instanceof Error && error.message.includes("insufficient data")) {
123
- throw error;
250
+ if (error instanceof Error) {
251
+ throw new Error(`Missing attributes: ${error.message}`);
124
252
  }
253
+ throw error;
254
+ }
255
+ if (this.hasUndefinedValues(key)) {
125
256
  throw new Error(
126
- `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.`
257
+ `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.`
127
258
  );
128
259
  }
260
+ const gsiConfig = this.table.gsis[indexName];
261
+ if (!gsiConfig) {
262
+ throw new Error(`GSI configuration not found for index: ${indexName}`);
263
+ }
264
+ if (key.pk) {
265
+ attributes[gsiConfig.partitionKey] = key.pk;
266
+ }
267
+ if (key.sk && gsiConfig.sortKey) {
268
+ attributes[gsiConfig.sortKey] = key.sk;
269
+ }
129
270
  }
130
271
  return attributes;
131
272
  }
@@ -148,12 +289,12 @@ function buildIndexes(dataForKeyGeneration, table, indexes, excludeReadOnly = fa
148
289
  const indexBuilder = new IndexBuilder(table, indexes);
149
290
  return indexBuilder.buildForCreate(dataForKeyGeneration, { excludeReadOnly });
150
291
  }
151
- function buildIndexUpdates(currentData, updates, table, indexes) {
292
+ function buildIndexUpdates(currentData, updates, table, indexes, forceRebuildIndexes) {
152
293
  if (!indexes) {
153
294
  return {};
154
295
  }
155
296
  const indexBuilder = new IndexBuilder(table, indexes);
156
- return indexBuilder.buildForUpdate(currentData, updates);
297
+ return indexBuilder.buildForUpdate(currentData, updates, { forceRebuildIndexes });
157
298
  }
158
299
 
159
300
  // src/entity/entity.ts
@@ -347,15 +488,16 @@ function defineEntity(config) {
347
488
  const primaryKeyObj = config.primaryKey.generateKey(key);
348
489
  const builder = table.update(primaryKeyObj);
349
490
  builder.condition(eq(entityTypeAttributeName, config.name));
350
- const timestamps = generateTimestamps(["updatedAt"], data);
351
- const indexUpdates = buildIndexUpdates(
352
- { ...key },
353
- { ...data, ...timestamps },
491
+ const entityAwareBuilder = createEntityAwareUpdateBuilder(builder, config.name);
492
+ entityAwareBuilder.configureEntityLogic({
493
+ data,
494
+ key,
354
495
  table,
355
- config.indexes
356
- );
357
- builder.set({ ...data, ...timestamps, ...indexUpdates });
358
- return builder;
496
+ indexes: config.indexes,
497
+ generateTimestamps: () => generateTimestamps(["updatedAt"], data),
498
+ buildIndexUpdates
499
+ });
500
+ return entityAwareBuilder;
359
501
  },
360
502
  delete: (key) => {
361
503
  const builder = table.delete(config.primaryKey.generateKey(key));