@ftschopp/dynatable-core 1.3.0 → 1.4.1

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 (32) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/dist/builders/batch-get/create-batch-get-builder.d.ts.map +1 -1
  3. package/dist/builders/batch-get/create-batch-get-builder.js +8 -3
  4. package/dist/builders/get/create-get-builder.d.ts.map +1 -1
  5. package/dist/builders/get/create-get-builder.js +2 -19
  6. package/dist/builders/query/create-query-builder.d.ts.map +1 -1
  7. package/dist/builders/query/create-query-builder.js +34 -3
  8. package/dist/builders/query/types.d.ts +16 -0
  9. package/dist/builders/query/types.d.ts.map +1 -1
  10. package/dist/builders/scan/create-scan-builder.d.ts.map +1 -1
  11. package/dist/builders/scan/create-scan-builder.js +42 -2
  12. package/dist/builders/scan/types.d.ts +35 -0
  13. package/dist/builders/scan/types.d.ts.map +1 -1
  14. package/dist/builders/shared/index.d.ts +1 -0
  15. package/dist/builders/shared/index.d.ts.map +1 -1
  16. package/dist/builders/shared/index.js +1 -0
  17. package/dist/builders/shared/projection.d.ts +15 -0
  18. package/dist/builders/shared/projection.d.ts.map +1 -0
  19. package/dist/builders/shared/projection.js +26 -0
  20. package/package.json +1 -1
  21. package/src/builders/batch-get/create-batch-get-builder.test.ts +30 -2
  22. package/src/builders/batch-get/create-batch-get-builder.ts +10 -3
  23. package/src/builders/get/create-get-builder.ts +1 -24
  24. package/src/builders/query/create-query-builder.test.ts +205 -1
  25. package/src/builders/query/create-query-builder.ts +40 -5
  26. package/src/builders/query/types.ts +17 -0
  27. package/src/builders/scan/create-scan-builder.test.ts +162 -6
  28. package/src/builders/scan/create-scan-builder.ts +52 -4
  29. package/src/builders/scan/types.ts +39 -0
  30. package/src/builders/shared/index.ts +1 -0
  31. package/src/builders/shared/projection.ts +28 -0
  32. package/tests/integration/instagram-clone.integration.test.ts +23 -4
package/CHANGELOG.md CHANGED
@@ -1,3 +1,17 @@
1
+ ## @ftschopp/dynatable-core [1.4.1](https://github.com/ftschopp/dynatable/compare/@ftschopp/dynatable-core@1.4.0...@ftschopp/dynatable-core@1.4.1) (2026-05-08)
2
+
3
+
4
+ ### Bug Fixes
5
+
6
+ * **core:** use ExpressionAttributeNames placeholders for projections ([#25](https://github.com/ftschopp/dynatable/issues/25)) ([0e14ebd](https://github.com/ftschopp/dynatable/commit/0e14ebd64cfc58eb2b2353fa7678239bdddb22a6)), closes [#5](https://github.com/ftschopp/dynatable/issues/5)
7
+
8
+ # @ftschopp/dynatable-core [1.4.0](https://github.com/ftschopp/dynatable/compare/@ftschopp/dynatable-core@1.3.0...@ftschopp/dynatable-core@1.4.0) (2026-05-08)
9
+
10
+
11
+ ### Features
12
+
13
+ * **core:** add iterate() async iterator and Scan executeWithPagination ([#21](https://github.com/ftschopp/dynatable/issues/21)) ([c31d458](https://github.com/ftschopp/dynatable/commit/c31d45861e92ba0c52240128084939128a6e4acc)), closes [#4](https://github.com/ftschopp/dynatable/issues/4)
14
+
1
15
  # @ftschopp/dynatable-core [1.3.0](https://github.com/ftschopp/dynatable/compare/@ftschopp/dynatable-core@1.2.5...@ftschopp/dynatable-core@1.3.0) (2026-05-07)
2
16
 
3
17
 
@@ -1 +1 @@
1
- {"version":3,"file":"create-batch-get-builder.d.ts","sourceRoot":"","sources":["../../../src/builders/batch-get/create-batch-get-builder.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,cAAc,EAAE,MAAM,0BAA0B,CAAC;AAM1D,OAAO,EAAE,eAAe,EAAE,MAAM,SAAS,CAAC;AAC1C,OAAO,EAAE,cAAc,EAAE,MAAM,6BAA6B,CAAC;AAE7D;;;;;;;GAOG;AACH,wBAAgB,qBAAqB,CAAC,KAAK,EACzC,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE;IAAE,IAAI,EAAE,GAAG,EAAE,CAAA;CAAE,CAAC,EAC7C,MAAM,EAAE,cAAc,EACtB,OAAO,CAAC,EAAE;IACR,UAAU,CAAC,EAAE,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;IAC7B,cAAc,CAAC,EAAE,OAAO,CAAC;CAC1B,EACD,MAAM,CAAC,EAAE,cAAc,GACtB,eAAe,CAAC,KAAK,CAAC,CA0ExB"}
1
+ {"version":3,"file":"create-batch-get-builder.d.ts","sourceRoot":"","sources":["../../../src/builders/batch-get/create-batch-get-builder.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,cAAc,EAAE,MAAM,0BAA0B,CAAC;AAO1D,OAAO,EAAE,eAAe,EAAE,MAAM,SAAS,CAAC;AAC1C,OAAO,EAAE,cAAc,EAAE,MAAM,6BAA6B,CAAC;AAE7D;;;;;;;GAOG;AACH,wBAAgB,qBAAqB,CAAC,KAAK,EACzC,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE;IAAE,IAAI,EAAE,GAAG,EAAE,CAAA;CAAE,CAAC,EAC7C,MAAM,EAAE,cAAc,EACtB,OAAO,CAAC,EAAE;IACR,UAAU,CAAC,EAAE,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;IAC7B,cAAc,CAAC,EAAE,OAAO,CAAC;CAC1B,EACD,MAAM,CAAC,EAAE,cAAc,GACtB,eAAe,CAAC,KAAK,CAAC,CAgFxB"}
@@ -2,6 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.createBatchGetBuilder = createBatchGetBuilder;
4
4
  const lib_dynamodb_1 = require("@aws-sdk/lib-dynamodb");
5
+ const shared_1 = require("../shared");
5
6
  /**
6
7
  * Creates a BatchGetBuilder to retrieve multiple items by their keys.
7
8
  *
@@ -36,12 +37,16 @@ function createBatchGetBuilder(requestItems, client, options, logger) {
36
37
  * Build the underlying DynamoDB input parameters.
37
38
  */
38
39
  dbParams() {
39
- // Apply projection and consistentRead to all tables
40
+ // Build the projection once (with #-placeholders so reserved words
41
+ // like name/date/status/type don't blow up at DynamoDB) and reuse
42
+ // it per-table.
43
+ const proj = projection.length > 0 ? (0, shared_1.buildProjectionExpression)(projection.map(String)) : undefined;
40
44
  const enhancedRequestItems = Object.entries(requestItems).reduce((acc, [tableName, tableRequest]) => {
41
45
  acc[tableName] = {
42
46
  Keys: tableRequest.Keys,
43
- ...(projection.length > 0 && {
44
- ProjectionExpression: projection.join(', '),
47
+ ...(proj && {
48
+ ProjectionExpression: proj.ProjectionExpression,
49
+ ExpressionAttributeNames: proj.ExpressionAttributeNames,
45
50
  }),
46
51
  ...(isConsistent && { ConsistentRead: true }),
47
52
  };
@@ -1 +1 @@
1
- {"version":3,"file":"create-get-builder.d.ts","sourceRoot":"","sources":["../../../src/builders/get/create-get-builder.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,cAAc,EAAE,MAAM,0BAA0B,CAAC;AAE1D,OAAO,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AACrC,OAAO,EAAE,cAAc,EAAE,MAAM,6BAA6B,CAAC;AA0B7D;;GAEG;AACH,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,KAAK,EAC9C,SAAS,EAAE,MAAM,EACjB,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EACxB,MAAM,EAAE,cAAc,EACtB,OAAO,CAAC,EAAE;IACR,UAAU,CAAC,EAAE,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;IAC7B,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,sBAAsB,CAAC,EAAE,SAAS,GAAG,OAAO,GAAG,MAAM,CAAC;CACvD,EACD,MAAM,CAAC,EAAE,cAAc,GACtB,UAAU,CAAC,QAAQ,EAAE,KAAK,CAAC,CAgG7B"}
1
+ {"version":3,"file":"create-get-builder.d.ts","sourceRoot":"","sources":["../../../src/builders/get/create-get-builder.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,cAAc,EAAE,MAAM,0BAA0B,CAAC;AAG1D,OAAO,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AACrC,OAAO,EAAE,cAAc,EAAE,MAAM,6BAA6B,CAAC;AAE7D;;GAEG;AACH,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,KAAK,EAC9C,SAAS,EAAE,MAAM,EACjB,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EACxB,MAAM,EAAE,cAAc,EACtB,OAAO,CAAC,EAAE;IACR,UAAU,CAAC,EAAE,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;IAC7B,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,sBAAsB,CAAC,EAAE,SAAS,GAAG,OAAO,GAAG,MAAM,CAAC;CACvD,EACD,MAAM,CAAC,EAAE,cAAc,GACtB,UAAU,CAAC,QAAQ,EAAE,KAAK,CAAC,CAgG7B"}
@@ -2,24 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.createGetBuilder = createGetBuilder;
4
4
  const lib_dynamodb_1 = require("@aws-sdk/lib-dynamodb");
5
- /**
6
- * Builds a ProjectionExpression with proper ExpressionAttributeNames
7
- * to handle reserved words and special characters.
8
- */
9
- function buildProjectionExpression(attrs) {
10
- const names = {};
11
- const projectionParts = [];
12
- attrs.forEach((attr) => {
13
- // Use a placeholder for the attribute name to avoid reserved word conflicts
14
- const placeholder = `#${attr}`;
15
- names[placeholder] = attr;
16
- projectionParts.push(placeholder);
17
- });
18
- return {
19
- ProjectionExpression: projectionParts.join(', '),
20
- ExpressionAttributeNames: names,
21
- };
22
- }
5
+ const shared_1 = require("../shared");
23
6
  /**
24
7
  * Creates a GetBuilder to retrieve an item by its key.
25
8
  */
@@ -68,7 +51,7 @@ function createGetBuilder(tableName, key, client, options, logger) {
68
51
  };
69
52
  // Add projection with proper ExpressionAttributeNames
70
53
  if (projection.length > 0) {
71
- const projectionExpr = buildProjectionExpression(projection);
54
+ const projectionExpr = (0, shared_1.buildProjectionExpression)(projection);
72
55
  params.ProjectionExpression = projectionExpr.ProjectionExpression;
73
56
  params.ExpressionAttributeNames = projectionExpr.ExpressionAttributeNames;
74
57
  }
@@ -1 +1 @@
1
- {"version":3,"file":"create-query-builder.d.ts","sourceRoot":"","sources":["../../../src/builders/query/create-query-builder.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,cAAc,EAAE,MAAM,0BAA0B,CAAC;AAK1D,OAAO,EAAE,YAAY,EAA8B,MAAM,SAAS,CAAC;AACnE,OAAO,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAC;AAEnD,OAAO,EAAE,cAAc,EAAE,MAAM,6BAA6B,CAAC;AA2V7D;;GAEG;AACH,wBAAgB,kBAAkB,CAAC,KAAK,EACtC,SAAS,EAAE,MAAM,EACjB,MAAM,EAAE,cAAc,EACtB,KAAK,CAAC,EAAE,eAAe,EACvB,MAAM,CAAC,EAAE,cAAc,EACvB,UAAU,CAAC,EAAE,MAAM,GAClB,YAAY,CAAC,KAAK,CAAC,CA6BrB"}
1
+ {"version":3,"file":"create-query-builder.d.ts","sourceRoot":"","sources":["../../../src/builders/query/create-query-builder.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,cAAc,EAAE,MAAM,0BAA0B,CAAC;AAM1D,OAAO,EAAE,YAAY,EAA8B,MAAM,SAAS,CAAC;AACnE,OAAO,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAC;AAEnD,OAAO,EAAE,cAAc,EAAE,MAAM,6BAA6B,CAAC;AA6X7D;;GAEG;AACH,wBAAgB,kBAAkB,CAAC,KAAK,EACtC,SAAS,EAAE,MAAM,EACjB,MAAM,EAAE,cAAc,EACtB,KAAK,CAAC,EAAE,eAAe,EACvB,MAAM,CAAC,EAAE,cAAc,EACvB,UAAU,CAAC,EAAE,MAAM,GAClB,YAAY,CAAC,KAAK,CAAC,CA6BrB"}
@@ -4,6 +4,7 @@ exports.createQueryBuilder = createQueryBuilder;
4
4
  const lib_dynamodb_1 = require("@aws-sdk/lib-dynamodb");
5
5
  const operators_1 = require("../shared/operators");
6
6
  const conditions_1 = require("../shared/conditions");
7
+ const projection_1 = require("../shared/projection");
7
8
  const model_utils_1 = require("../../utils/model-utils");
8
9
  /**
9
10
  * Maps a model attribute name to its corresponding DynamoDB key name (PK/SK)
@@ -226,6 +227,16 @@ function createQueryExecutor(state) {
226
227
  ...(keyExpr.values ?? {}),
227
228
  ...(filterExpr.values ?? {}),
228
229
  };
230
+ // Build ProjectionExpression with placeholders so reserved words
231
+ // (name, date, status, type, …) don't blow up at DynamoDB.
232
+ // Idempotent merge: filter/key and projection placeholders both map
233
+ // `#name → name`, so collisions resolve to the same value.
234
+ let projectionExpression;
235
+ if (state.projection && state.projection.length > 0) {
236
+ const proj = (0, projection_1.buildProjectionExpression)(state.projection.map((a) => String(a)));
237
+ projectionExpression = proj.ProjectionExpression;
238
+ Object.assign(allNames, proj.ExpressionAttributeNames);
239
+ }
229
240
  return {
230
241
  TableName: state.tableName,
231
242
  ...(keyExpr.expression && {
@@ -245,9 +256,8 @@ function createQueryExecutor(state) {
245
256
  ...(state.scanForward !== undefined && {
246
257
  ScanIndexForward: state.scanForward,
247
258
  }),
248
- ...(state.projection &&
249
- state.projection.length > 0 && {
250
- ProjectionExpression: state.projection.join(', '),
259
+ ...(projectionExpression && {
260
+ ProjectionExpression: projectionExpression,
251
261
  }),
252
262
  ...(state.consistentReadEnabled && { ConsistentRead: true }),
253
263
  ...(state.exclusiveStartKey && {
@@ -255,6 +265,14 @@ function createQueryExecutor(state) {
255
265
  }),
256
266
  };
257
267
  },
268
+ /**
269
+ * ⚠️ Returns only the FIRST page of results. DynamoDB caps each Query
270
+ * response at ~1MB (or `Limit` if set). If the matching set is larger,
271
+ * the remaining items are silently dropped.
272
+ *
273
+ * Use {@link executeWithPagination} when you need to drive pagination
274
+ * yourself, or {@link iterate} to walk every matching item lazily.
275
+ */
258
276
  async execute() {
259
277
  const params = this.dbParams();
260
278
  const result = await state.client.send(new lib_dynamodb_1.QueryCommand(params));
@@ -272,6 +290,19 @@ function createQueryExecutor(state) {
272
290
  scannedCount: result.ScannedCount,
273
291
  };
274
292
  },
293
+ async *iterate() {
294
+ const baseParams = this.dbParams();
295
+ let cursor = baseParams.ExclusiveStartKey;
296
+ do {
297
+ const params = { ...baseParams, ExclusiveStartKey: cursor };
298
+ const result = await state.client.send(new lib_dynamodb_1.QueryCommand(params));
299
+ state.logger?.log('QueryCommand', params, result);
300
+ for (const item of (result.Items ?? [])) {
301
+ yield item;
302
+ }
303
+ cursor = result.LastEvaluatedKey;
304
+ } while (cursor);
305
+ },
275
306
  };
276
307
  }
277
308
  /**
@@ -60,6 +60,22 @@ export interface QueryExecutor<Model> extends ExecutableBuilder<Model[]> {
60
60
  * Executes the query and returns result with pagination metadata
61
61
  */
62
62
  executeWithPagination(): Promise<QueryResult<Model>>;
63
+ /**
64
+ * Returns an async iterator that paginates internally and yields one item at
65
+ * a time. Memory stays at one page; you can `break` out of the loop early.
66
+ *
67
+ * `Limit` set via `.limit()` is forwarded to DynamoDB as a *per-request*
68
+ * cap, not a total cap. To cap the total, count yourself inside the loop:
69
+ *
70
+ * ```ts
71
+ * let n = 0;
72
+ * for await (const item of builder.iterate()) {
73
+ * if (n++ >= 1000) break;
74
+ * process(item);
75
+ * }
76
+ * ```
77
+ */
78
+ iterate(): AsyncIterableIterator<Model>;
63
79
  }
64
80
  /**
65
81
  * Main Query Builder interface with type-safe where clause
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../src/builders/query/types.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,iBAAiB,EAAE,WAAW,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AAEjF;;GAEG;AACH,MAAM,WAAW,WAAW,CAAC,KAAK;IAChC;;OAEG;IACH,KAAK,EAAE,KAAK,EAAE,CAAC;IAEf;;OAEG;IACH,gBAAgB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;IAEvC;;OAEG;IACH,KAAK,CAAC,EAAE,MAAM,CAAC;IAEf;;OAEG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAED;;GAEG;AACH,MAAM,WAAW,aAAa,CAAC,KAAK,CAAE,SAAQ,iBAAiB,CAAC,KAAK,EAAE,CAAC;IACtE;;OAEG;IACH,KAAK,CAAC,KAAK,EAAE,MAAM,GAAG,aAAa,CAAC,KAAK,CAAC,CAAC;IAE3C;;OAEG;IACH,gBAAgB,CAAC,OAAO,EAAE,OAAO,GAAG,aAAa,CAAC,KAAK,CAAC,CAAC;IAEzD;;OAEG;IACH,QAAQ,CAAC,SAAS,EAAE,MAAM,GAAG,aAAa,CAAC,KAAK,CAAC,CAAC;IAElD;;OAEG;IACH,MAAM,CAAC,KAAK,EAAE,CAAC,MAAM,KAAK,CAAC,EAAE,GAAG,aAAa,CAAC,KAAK,CAAC,CAAC;IAErD;;OAEG;IACH,cAAc,IAAI,aAAa,CAAC,KAAK,CAAC,CAAC;IAEvC;;OAEG;IACH,SAAS,CAAC,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,aAAa,CAAC,KAAK,CAAC,CAAC;IAE1D;;OAEG;IACH,QAAQ,IAAI,GAAG,CAAC;IAEhB;;OAEG;IACH,OAAO,IAAI,OAAO,CAAC,KAAK,EAAE,CAAC,CAAC;IAE5B;;OAEG;IACH,qBAAqB,IAAI,OAAO,CAAC,WAAW,CAAC,KAAK,CAAC,CAAC,CAAC;CACtD;AAED;;GAEG;AACH,MAAM,WAAW,YAAY,CAAC,KAAK;IACjC;;;;;;;OAOG;IACH,KAAK,CAAC,EAAE,EAAE,CAAC,IAAI,EAAE,WAAW,CAAC,KAAK,CAAC,EAAE,EAAE,EAAE,SAAS,KAAK,SAAS,GAAG,aAAa,CAAC,KAAK,CAAC,CAAC;CACzF"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../src/builders/query/types.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,iBAAiB,EAAE,WAAW,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AAEjF;;GAEG;AACH,MAAM,WAAW,WAAW,CAAC,KAAK;IAChC;;OAEG;IACH,KAAK,EAAE,KAAK,EAAE,CAAC;IAEf;;OAEG;IACH,gBAAgB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;IAEvC;;OAEG;IACH,KAAK,CAAC,EAAE,MAAM,CAAC;IAEf;;OAEG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAED;;GAEG;AACH,MAAM,WAAW,aAAa,CAAC,KAAK,CAAE,SAAQ,iBAAiB,CAAC,KAAK,EAAE,CAAC;IACtE;;OAEG;IACH,KAAK,CAAC,KAAK,EAAE,MAAM,GAAG,aAAa,CAAC,KAAK,CAAC,CAAC;IAE3C;;OAEG;IACH,gBAAgB,CAAC,OAAO,EAAE,OAAO,GAAG,aAAa,CAAC,KAAK,CAAC,CAAC;IAEzD;;OAEG;IACH,QAAQ,CAAC,SAAS,EAAE,MAAM,GAAG,aAAa,CAAC,KAAK,CAAC,CAAC;IAElD;;OAEG;IACH,MAAM,CAAC,KAAK,EAAE,CAAC,MAAM,KAAK,CAAC,EAAE,GAAG,aAAa,CAAC,KAAK,CAAC,CAAC;IAErD;;OAEG;IACH,cAAc,IAAI,aAAa,CAAC,KAAK,CAAC,CAAC;IAEvC;;OAEG;IACH,SAAS,CAAC,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,aAAa,CAAC,KAAK,CAAC,CAAC;IAE1D;;OAEG;IACH,QAAQ,IAAI,GAAG,CAAC;IAEhB;;OAEG;IACH,OAAO,IAAI,OAAO,CAAC,KAAK,EAAE,CAAC,CAAC;IAE5B;;OAEG;IACH,qBAAqB,IAAI,OAAO,CAAC,WAAW,CAAC,KAAK,CAAC,CAAC,CAAC;IAErD;;;;;;;;;;;;;;OAcG;IACH,OAAO,IAAI,qBAAqB,CAAC,KAAK,CAAC,CAAC;CACzC;AAED;;GAEG;AACH,MAAM,WAAW,YAAY,CAAC,KAAK;IACjC;;;;;;;OAOG;IACH,KAAK,CAAC,EAAE,EAAE,CAAC,IAAI,EAAE,WAAW,CAAC,KAAK,CAAC,EAAE,EAAE,EAAE,SAAS,KAAK,SAAS,GAAG,aAAa,CAAC,KAAK,CAAC,CAAC;CACzF"}
@@ -1 +1 @@
1
- {"version":3,"file":"create-scan-builder.d.ts","sourceRoot":"","sources":["../../../src/builders/scan/create-scan-builder.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,cAAc,EAAE,MAAM,0BAA0B,CAAC;AAE1D,OAAO,EAAgC,SAAS,EAAmB,MAAM,WAAW,CAAC;AACrF,OAAO,EAAE,WAAW,EAAE,MAAM,SAAS,CAAC;AACtC,OAAO,EAAE,cAAc,EAAE,MAAM,6BAA6B,CAAC;AAE7D;;GAEG;AACH,wBAAgB,iBAAiB,CAAC,KAAK,EACrC,SAAS,EAAE,MAAM,EACjB,MAAM,EAAE,cAAc,EACtB,OAAO,GAAE,SAAS,EAAO,EACzB,eAAe,GAAE,CAAC,MAAM,KAAK,CAAC,EAAO,EACrC,UAAU,CAAC,EAAE,MAAM,EACnB,gBAAgB,UAAQ,EACxB,SAAS,CAAC,EAAE,MAAM,EAClB,iBAAiB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EACvC,aAAa,CAAC,EAAE;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,aAAa,EAAE,MAAM,CAAA;CAAE,EAC1D,MAAM,CAAC,EAAE,cAAc,GACtB,WAAW,CAAC,KAAK,CAAC,CAmMpB"}
1
+ {"version":3,"file":"create-scan-builder.d.ts","sourceRoot":"","sources":["../../../src/builders/scan/create-scan-builder.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,cAAc,EAAE,MAAM,0BAA0B,CAAC;AAE1D,OAAO,EAGL,SAAS,EAGV,MAAM,WAAW,CAAC;AACnB,OAAO,EAAE,WAAW,EAAc,MAAM,SAAS,CAAC;AAClD,OAAO,EAAE,cAAc,EAAE,MAAM,6BAA6B,CAAC;AAE7D;;GAEG;AACH,wBAAgB,iBAAiB,CAAC,KAAK,EACrC,SAAS,EAAE,MAAM,EACjB,MAAM,EAAE,cAAc,EACtB,OAAO,GAAE,SAAS,EAAO,EACzB,eAAe,GAAE,CAAC,MAAM,KAAK,CAAC,EAAO,EACrC,UAAU,CAAC,EAAE,MAAM,EACnB,gBAAgB,UAAQ,EACxB,SAAS,CAAC,EAAE,MAAM,EAClB,iBAAiB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EACvC,aAAa,CAAC,EAAE;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,aAAa,EAAE,MAAM,CAAA;CAAE,EAC1D,MAAM,CAAC,EAAE,cAAc,GACtB,WAAW,CAAC,KAAK,CAAC,CA6OpB"}
@@ -54,10 +54,18 @@ function createScanBuilder(tableName, client, filters = [], projectionAttrs = []
54
54
  expressionAttributeNames = result.names;
55
55
  expressionAttributeValues = result.values;
56
56
  }
57
- // Build ProjectionExpression
57
+ // Build ProjectionExpression with placeholders so reserved words
58
+ // (name, date, status, type, …) don't blow up at DynamoDB.
58
59
  let projectionExpression = '';
59
60
  if (projectionAttrs.length > 0) {
60
- projectionExpression = projectionAttrs.map((attr) => String(attr)).join(', ');
61
+ const proj = (0, shared_1.buildProjectionExpression)(projectionAttrs.map((attr) => String(attr)));
62
+ projectionExpression = proj.ProjectionExpression;
63
+ // Idempotent merge: filter and projection placeholders both map
64
+ // `#name → name`, so collisions resolve to the same value.
65
+ expressionAttributeNames = {
66
+ ...proj.ExpressionAttributeNames,
67
+ ...expressionAttributeNames,
68
+ };
61
69
  }
62
70
  const params = {
63
71
  TableName: tableName,
@@ -92,12 +100,44 @@ function createScanBuilder(tableName, client, filters = [], projectionAttrs = []
92
100
  }
93
101
  return params;
94
102
  },
103
+ /**
104
+ * ⚠️ Returns only the FIRST page of results. DynamoDB caps each Scan
105
+ * response at ~1MB (or `Limit` if set). If the matching set is larger,
106
+ * the remaining items are silently dropped.
107
+ *
108
+ * Use {@link executeWithPagination} when you need to drive pagination
109
+ * yourself, or {@link iterate} to walk every matching item lazily.
110
+ */
95
111
  async execute() {
96
112
  const params = build().dbParams();
97
113
  const response = await client.send(new lib_dynamodb_1.ScanCommand(params));
98
114
  logger?.log('ScanCommand', params, response);
99
115
  return (response.Items || []);
100
116
  },
117
+ async executeWithPagination() {
118
+ const params = build().dbParams();
119
+ const response = await client.send(new lib_dynamodb_1.ScanCommand(params));
120
+ logger?.log('ScanCommand', params, response);
121
+ return {
122
+ items: (response.Items ?? []),
123
+ lastEvaluatedKey: response.LastEvaluatedKey,
124
+ count: response.Count,
125
+ scannedCount: response.ScannedCount,
126
+ };
127
+ },
128
+ async *iterate() {
129
+ const baseParams = build().dbParams();
130
+ let cursor = baseParams.ExclusiveStartKey;
131
+ do {
132
+ const params = { ...baseParams, ExclusiveStartKey: cursor };
133
+ const response = await client.send(new lib_dynamodb_1.ScanCommand(params));
134
+ logger?.log('ScanCommand', params, response);
135
+ for (const item of (response.Items ?? [])) {
136
+ yield item;
137
+ }
138
+ cursor = response.LastEvaluatedKey;
139
+ } while (cursor);
140
+ },
101
141
  });
102
142
  return build();
103
143
  }
@@ -1,4 +1,17 @@
1
1
  import { ExecutableBuilder, AttrBuilder, OpBuilder, Condition } from '../shared';
2
+ /**
3
+ * A single page of scan results plus metadata for manual pagination.
4
+ */
5
+ export interface ScanResult<Model> {
6
+ /** Items returned in this page. */
7
+ items: Model[];
8
+ /** Cursor for the next page; `undefined` when there are no more results. */
9
+ lastEvaluatedKey?: Record<string, any>;
10
+ /** Number of items returned after applying the filter. */
11
+ count?: number;
12
+ /** Number of items examined before applying the filter. */
13
+ scannedCount?: number;
14
+ }
2
15
  /**
3
16
  * Builder for DynamoDB Scan operations.
4
17
  * Scans the entire table or index without requiring key conditions.
@@ -39,5 +52,27 @@ export interface ScanBuilder<Model> extends ExecutableBuilder<Model[]> {
39
52
  * Returns a new immutable builder.
40
53
  */
41
54
  segment(segmentNumber: number, totalSegments: number): ScanBuilder<Model>;
55
+ /**
56
+ * Executes a single Scan request and returns the page along with the
57
+ * `lastEvaluatedKey` cursor and counts. Use this when you want to drive
58
+ * pagination yourself (e.g. expose a cursor to a client).
59
+ */
60
+ executeWithPagination(): Promise<ScanResult<Model>>;
61
+ /**
62
+ * Returns an async iterator that paginates internally and yields one item at
63
+ * a time. Memory stays at one page; you can `break` out of the loop early.
64
+ *
65
+ * `Limit` set via `.limit()` is forwarded to DynamoDB as a *per-request*
66
+ * cap, not a total cap. To cap the total, count yourself inside the loop:
67
+ *
68
+ * ```ts
69
+ * let n = 0;
70
+ * for await (const item of builder.iterate()) {
71
+ * if (n++ >= 1000) break;
72
+ * process(item);
73
+ * }
74
+ * ```
75
+ */
76
+ iterate(): AsyncIterableIterator<Model>;
42
77
  }
43
78
  //# sourceMappingURL=types.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../src/builders/scan/types.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,WAAW,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AAEjF;;;GAGG;AACH,MAAM,WAAW,WAAW,CAAC,KAAK,CAAE,SAAQ,iBAAiB,CAAC,KAAK,EAAE,CAAC;IACpE;;;OAGG;IACH,MAAM,CAAC,EAAE,EAAE,CAAC,IAAI,EAAE,WAAW,CAAC,KAAK,CAAC,EAAE,EAAE,EAAE,SAAS,KAAK,SAAS,GAAG,WAAW,CAAC,KAAK,CAAC,CAAC;IAEvF;;;OAGG;IACH,MAAM,CAAC,KAAK,EAAE,CAAC,MAAM,KAAK,CAAC,EAAE,GAAG,WAAW,CAAC,KAAK,CAAC,CAAC;IAEnD;;;OAGG;IACH,KAAK,CAAC,KAAK,EAAE,MAAM,GAAG,WAAW,CAAC,KAAK,CAAC,CAAC;IAEzC;;;OAGG;IACH,cAAc,IAAI,WAAW,CAAC,KAAK,CAAC,CAAC;IAErC;;;OAGG;IACH,UAAU,CAAC,SAAS,EAAE,MAAM,GAAG,WAAW,CAAC,KAAK,CAAC,CAAC;IAElD;;;OAGG;IACH,SAAS,CAAC,iBAAiB,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,WAAW,CAAC,KAAK,CAAC,CAAC;IAEtE;;;OAGG;IACH,OAAO,CAAC,aAAa,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,GAAG,WAAW,CAAC,KAAK,CAAC,CAAC;CAC3E"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../src/builders/scan/types.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,iBAAiB,EAAE,WAAW,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AAEjF;;GAEG;AACH,MAAM,WAAW,UAAU,CAAC,KAAK;IAC/B,mCAAmC;IACnC,KAAK,EAAE,KAAK,EAAE,CAAC;IACf,4EAA4E;IAC5E,gBAAgB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;IACvC,0DAA0D;IAC1D,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,2DAA2D;IAC3D,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAED;;;GAGG;AACH,MAAM,WAAW,WAAW,CAAC,KAAK,CAAE,SAAQ,iBAAiB,CAAC,KAAK,EAAE,CAAC;IACpE;;;OAGG;IACH,MAAM,CAAC,EAAE,EAAE,CAAC,IAAI,EAAE,WAAW,CAAC,KAAK,CAAC,EAAE,EAAE,EAAE,SAAS,KAAK,SAAS,GAAG,WAAW,CAAC,KAAK,CAAC,CAAC;IAEvF;;;OAGG;IACH,MAAM,CAAC,KAAK,EAAE,CAAC,MAAM,KAAK,CAAC,EAAE,GAAG,WAAW,CAAC,KAAK,CAAC,CAAC;IAEnD;;;OAGG;IACH,KAAK,CAAC,KAAK,EAAE,MAAM,GAAG,WAAW,CAAC,KAAK,CAAC,CAAC;IAEzC;;;OAGG;IACH,cAAc,IAAI,WAAW,CAAC,KAAK,CAAC,CAAC;IAErC;;;OAGG;IACH,UAAU,CAAC,SAAS,EAAE,MAAM,GAAG,WAAW,CAAC,KAAK,CAAC,CAAC;IAElD;;;OAGG;IACH,SAAS,CAAC,iBAAiB,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,WAAW,CAAC,KAAK,CAAC,CAAC;IAEtE;;;OAGG;IACH,OAAO,CAAC,aAAa,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,GAAG,WAAW,CAAC,KAAK,CAAC,CAAC;IAE1E;;;;OAIG;IACH,qBAAqB,IAAI,OAAO,CAAC,UAAU,CAAC,KAAK,CAAC,CAAC,CAAC;IAEpD;;;;;;;;;;;;;;OAcG;IACH,OAAO,IAAI,qBAAqB,CAAC,KAAK,CAAC,CAAC;CACzC"}
@@ -1,4 +1,5 @@
1
1
  export * from './types';
2
2
  export * from './operators';
3
3
  export * from './conditions';
4
+ export * from './projection';
4
5
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/builders/shared/index.ts"],"names":[],"mappings":"AAAA,cAAc,SAAS,CAAC;AACxB,cAAc,aAAa,CAAC;AAC5B,cAAc,cAAc,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/builders/shared/index.ts"],"names":[],"mappings":"AAAA,cAAc,SAAS,CAAC;AACxB,cAAc,aAAa,CAAC;AAC5B,cAAc,cAAc,CAAC;AAC7B,cAAc,cAAc,CAAC"}
@@ -17,3 +17,4 @@ Object.defineProperty(exports, "__esModule", { value: true });
17
17
  __exportStar(require("./types"), exports);
18
18
  __exportStar(require("./operators"), exports);
19
19
  __exportStar(require("./conditions"), exports);
20
+ __exportStar(require("./projection"), exports);
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Builds a `ProjectionExpression` together with the matching
3
+ * `ExpressionAttributeNames` map so that any attribute — including
4
+ * DynamoDB reserved words like `name`, `date`, `status`, `type`, `data`,
5
+ * `count`, `size`, `value`, `time`, `year`, `source`, … — is referenced
6
+ * via a `#placeholder`.
7
+ *
8
+ * Without this indirection DynamoDB rejects the request with
9
+ * `ValidationException: Attribute name is a reserved keyword`.
10
+ */
11
+ export declare function buildProjectionExpression(attrs: string[]): {
12
+ ProjectionExpression: string;
13
+ ExpressionAttributeNames: Record<string, string>;
14
+ };
15
+ //# sourceMappingURL=projection.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"projection.d.ts","sourceRoot":"","sources":["../../../src/builders/shared/projection.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AACH,wBAAgB,yBAAyB,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG;IAC1D,oBAAoB,EAAE,MAAM,CAAC;IAC7B,wBAAwB,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAClD,CAcA"}
@@ -0,0 +1,26 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.buildProjectionExpression = buildProjectionExpression;
4
+ /**
5
+ * Builds a `ProjectionExpression` together with the matching
6
+ * `ExpressionAttributeNames` map so that any attribute — including
7
+ * DynamoDB reserved words like `name`, `date`, `status`, `type`, `data`,
8
+ * `count`, `size`, `value`, `time`, `year`, `source`, … — is referenced
9
+ * via a `#placeholder`.
10
+ *
11
+ * Without this indirection DynamoDB rejects the request with
12
+ * `ValidationException: Attribute name is a reserved keyword`.
13
+ */
14
+ function buildProjectionExpression(attrs) {
15
+ const names = {};
16
+ const projectionParts = [];
17
+ attrs.forEach((attr) => {
18
+ const placeholder = `#${attr}`;
19
+ names[placeholder] = attr;
20
+ projectionParts.push(placeholder);
21
+ });
22
+ return {
23
+ ProjectionExpression: projectionParts.join(', '),
24
+ ExpressionAttributeNames: names,
25
+ };
26
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ftschopp/dynatable-core",
3
- "version": "1.3.0",
3
+ "version": "1.4.1",
4
4
  "description": "Core library for DynamoDB single table design",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -64,7 +64,12 @@ describe('BatchGetBuilder', () => {
64
64
  { pk: 'USER#alice', sk: 'USER#alice' },
65
65
  { pk: 'USER#bob', sk: 'USER#bob' },
66
66
  ],
67
- ProjectionExpression: 'username, name, followerCount',
67
+ ProjectionExpression: '#username, #name, #followerCount',
68
+ ExpressionAttributeNames: {
69
+ '#username': 'username',
70
+ '#name': 'name',
71
+ '#followerCount': 'followerCount',
72
+ },
68
73
  },
69
74
  },
70
75
  });
@@ -155,11 +160,34 @@ describe('BatchGetBuilder', () => {
155
160
  { pk: 'USER#alice', sk: 'USER#alice' },
156
161
  { pk: 'USER#bob', sk: 'USER#bob' },
157
162
  ],
158
- ProjectionExpression: 'username, name',
163
+ ProjectionExpression: '#username, #name',
164
+ ExpressionAttributeNames: {
165
+ '#username': 'username',
166
+ '#name': 'name',
167
+ },
159
168
  ConsistentRead: true,
160
169
  },
161
170
  },
162
171
  });
163
172
  });
173
+
174
+ test('reserved DynamoDB words like "name", "date", "status" can be projected', () => {
175
+ const requestItems = {
176
+ Users: {
177
+ Keys: [{ pk: 'USER#alice', sk: 'USER#alice' }],
178
+ },
179
+ };
180
+
181
+ const builder = createBatchGetBuilder<User>(requestItems, client).select(['name']);
182
+ const params = builder.dbParams();
183
+
184
+ expect(params.RequestItems!['Users']).toEqual({
185
+ Keys: [{ pk: 'USER#alice', sk: 'USER#alice' }],
186
+ ProjectionExpression: '#name',
187
+ ExpressionAttributeNames: {
188
+ '#name': 'name',
189
+ },
190
+ });
191
+ });
164
192
  });
165
193
  });
@@ -5,6 +5,7 @@ import {
5
5
  BatchGetCommandInput,
6
6
  BatchGetCommandOutput,
7
7
  } from '@aws-sdk/lib-dynamodb';
8
+ import { buildProjectionExpression } from '../shared';
8
9
  import { BatchGetBuilder } from './types';
9
10
  import { DynamoDBLogger } from '../../utils/dynamodb-logger';
10
11
 
@@ -63,13 +64,19 @@ export function createBatchGetBuilder<Model>(
63
64
  * Build the underlying DynamoDB input parameters.
64
65
  */
65
66
  dbParams(): BatchGetCommandInput {
66
- // Apply projection and consistentRead to all tables
67
+ // Build the projection once (with #-placeholders so reserved words
68
+ // like name/date/status/type don't blow up at DynamoDB) and reuse
69
+ // it per-table.
70
+ const proj =
71
+ projection.length > 0 ? buildProjectionExpression(projection.map(String)) : undefined;
72
+
67
73
  const enhancedRequestItems = Object.entries(requestItems).reduce(
68
74
  (acc, [tableName, tableRequest]) => {
69
75
  acc[tableName] = {
70
76
  Keys: tableRequest.Keys,
71
- ...(projection.length > 0 && {
72
- ProjectionExpression: projection.join(', '),
77
+ ...(proj && {
78
+ ProjectionExpression: proj.ProjectionExpression,
79
+ ExpressionAttributeNames: proj.ExpressionAttributeNames,
73
80
  }),
74
81
  ...(isConsistent && { ConsistentRead: true }),
75
82
  };
@@ -1,33 +1,10 @@
1
1
  /* eslint-disable @typescript-eslint/no-explicit-any */
2
2
  import { DynamoDBClient } from '@aws-sdk/client-dynamodb';
3
3
  import { GetCommand, GetCommandInput, GetCommandOutput } from '@aws-sdk/lib-dynamodb';
4
+ import { buildProjectionExpression } from '../shared';
4
5
  import { GetBuilder } from './types';
5
6
  import { DynamoDBLogger } from '../../utils/dynamodb-logger';
6
7
 
7
- /**
8
- * Builds a ProjectionExpression with proper ExpressionAttributeNames
9
- * to handle reserved words and special characters.
10
- */
11
- function buildProjectionExpression(attrs: string[]): {
12
- ProjectionExpression: string;
13
- ExpressionAttributeNames: Record<string, string>;
14
- } {
15
- const names: Record<string, string> = {};
16
- const projectionParts: string[] = [];
17
-
18
- attrs.forEach((attr) => {
19
- // Use a placeholder for the attribute name to avoid reserved word conflicts
20
- const placeholder = `#${attr}`;
21
- names[placeholder] = attr;
22
- projectionParts.push(placeholder);
23
- });
24
-
25
- return {
26
- ProjectionExpression: projectionParts.join(', '),
27
- ExpressionAttributeNames: names,
28
- };
29
- }
30
-
31
8
  /**
32
9
  * Creates a GetBuilder to retrieve an item by its key.
33
10
  */