@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.
- package/CHANGELOG.md +14 -0
- package/dist/builders/batch-get/create-batch-get-builder.d.ts.map +1 -1
- package/dist/builders/batch-get/create-batch-get-builder.js +8 -3
- package/dist/builders/get/create-get-builder.d.ts.map +1 -1
- package/dist/builders/get/create-get-builder.js +2 -19
- package/dist/builders/query/create-query-builder.d.ts.map +1 -1
- package/dist/builders/query/create-query-builder.js +34 -3
- package/dist/builders/query/types.d.ts +16 -0
- package/dist/builders/query/types.d.ts.map +1 -1
- package/dist/builders/scan/create-scan-builder.d.ts.map +1 -1
- package/dist/builders/scan/create-scan-builder.js +42 -2
- package/dist/builders/scan/types.d.ts +35 -0
- package/dist/builders/scan/types.d.ts.map +1 -1
- package/dist/builders/shared/index.d.ts +1 -0
- package/dist/builders/shared/index.d.ts.map +1 -1
- package/dist/builders/shared/index.js +1 -0
- package/dist/builders/shared/projection.d.ts +15 -0
- package/dist/builders/shared/projection.d.ts.map +1 -0
- package/dist/builders/shared/projection.js +26 -0
- package/package.json +1 -1
- package/src/builders/batch-get/create-batch-get-builder.test.ts +30 -2
- package/src/builders/batch-get/create-batch-get-builder.ts +10 -3
- package/src/builders/get/create-get-builder.ts +1 -24
- package/src/builders/query/create-query-builder.test.ts +205 -1
- package/src/builders/query/create-query-builder.ts +40 -5
- package/src/builders/query/types.ts +17 -0
- package/src/builders/scan/create-scan-builder.test.ts +162 -6
- package/src/builders/scan/create-scan-builder.ts +52 -4
- package/src/builders/scan/types.ts +39 -0
- package/src/builders/shared/index.ts +1 -0
- package/src/builders/shared/projection.ts +28 -0
- 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;
|
|
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
|
-
//
|
|
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
|
-
...(
|
|
44
|
-
ProjectionExpression:
|
|
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;
|
|
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;
|
|
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
|
-
...(
|
|
249
|
-
|
|
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;
|
|
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,
|
|
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
|
-
|
|
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":"
|
|
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 +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"}
|
|
@@ -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
|
@@ -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
|
-
//
|
|
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
|
-
...(
|
|
72
|
-
ProjectionExpression:
|
|
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
|
*/
|