@ftschopp/dynatable-core 1.4.2 → 1.4.3
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 +7 -0
- package/dist/builders/query/create-query-builder.d.ts.map +1 -1
- package/dist/builders/query/create-query-builder.js +19 -2
- package/dist/core/types.d.ts +5 -0
- package/dist/core/types.d.ts.map +1 -1
- package/package.json +11 -2
- package/eslint.config.mjs +0 -4
- package/jest.config.js +0 -11
- package/src/builders/README.md +0 -339
- package/src/builders/batch-get/README.md +0 -43
- package/src/builders/batch-get/create-batch-get-builder.test.ts +0 -193
- package/src/builders/batch-get/create-batch-get-builder.ts +0 -108
- package/src/builders/batch-get/index.ts +0 -2
- package/src/builders/batch-get/types.ts +0 -28
- package/src/builders/batch-write/README.md +0 -204
- package/src/builders/batch-write/create-batch-write-builder.test.ts +0 -173
- package/src/builders/batch-write/create-batch-write-builder.ts +0 -49
- package/src/builders/batch-write/index.ts +0 -2
- package/src/builders/batch-write/types.ts +0 -33
- package/src/builders/delete/create-delete-builder.test.ts +0 -294
- package/src/builders/delete/create-delete-builder.ts +0 -100
- package/src/builders/delete/index.ts +0 -2
- package/src/builders/delete/types.ts +0 -17
- package/src/builders/get/create-get-builder.test.ts +0 -272
- package/src/builders/get/create-get-builder.ts +0 -117
- package/src/builders/get/index.ts +0 -2
- package/src/builders/get/types.ts +0 -39
- package/src/builders/index.ts +0 -14
- package/src/builders/put/create-put-builder.test.ts +0 -213
- package/src/builders/put/create-put-builder.ts +0 -160
- package/src/builders/put/index.ts +0 -2
- package/src/builders/put/types.ts +0 -24
- package/src/builders/query/create-query-builder.test.ts +0 -770
- package/src/builders/query/create-query-builder.ts +0 -455
- package/src/builders/query/index.ts +0 -2
- package/src/builders/query/types.ts +0 -109
- package/src/builders/scan/create-scan-builder.test.ts +0 -416
- package/src/builders/scan/create-scan-builder.ts +0 -265
- package/src/builders/scan/index.ts +0 -2
- package/src/builders/scan/types.ts +0 -88
- package/src/builders/shared/conditions.ts +0 -58
- package/src/builders/shared/index.ts +0 -4
- package/src/builders/shared/operators.test.ts +0 -270
- package/src/builders/shared/operators.ts +0 -208
- package/src/builders/shared/projection.ts +0 -28
- package/src/builders/shared/types.ts +0 -101
- package/src/builders/transact-get/README.md +0 -167
- package/src/builders/transact-get/create-transact-get-builder.test.ts +0 -239
- package/src/builders/transact-get/create-transact-get-builder.ts +0 -67
- package/src/builders/transact-get/index.ts +0 -2
- package/src/builders/transact-get/types.ts +0 -31
- package/src/builders/transact-write/README.md +0 -202
- package/src/builders/transact-write/create-transact-write-builder.test.ts +0 -288
- package/src/builders/transact-write/create-transact-write-builder.ts +0 -124
- package/src/builders/transact-write/index.ts +0 -2
- package/src/builders/transact-write/types.ts +0 -71
- package/src/builders/update/create-update-builder.test.ts +0 -573
- package/src/builders/update/create-update-builder.ts +0 -375
- package/src/builders/update/index.ts +0 -2
- package/src/builders/update/types.ts +0 -63
- package/src/core/types.test.ts +0 -641
- package/src/core/types.ts +0 -366
- package/src/entity/create-entity-api.ts +0 -224
- package/src/entity/index.ts +0 -19
- package/src/entity/middleware/factories.ts +0 -15
- package/src/entity/middleware/types.ts +0 -7
- package/src/entity/middleware/with-middleware.ts +0 -37
- package/src/entity/types.ts +0 -79
- package/src/entity/validation/key-validation.ts +0 -34
- package/src/index.ts +0 -23
- package/src/table.ts +0 -116
- package/src/utils/dynamodb-logger.test.ts +0 -246
- package/src/utils/dynamodb-logger.ts +0 -175
- package/src/utils/model-utils.test.ts +0 -452
- package/src/utils/model-utils.ts +0 -188
- package/src/utils/zod-utils.test.ts +0 -363
- package/src/utils/zod-utils.ts +0 -56
- package/tests/integration/instagram-clone.integration.test.ts +0 -1019
- package/tests/integration/pagination-timestamps.integration.test.ts +0 -374
- package/tests/integration/transactions.integration.test.ts +0 -528
- package/tsconfig.json +0 -12
|
@@ -1,455 +0,0 @@
|
|
|
1
|
-
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
2
|
-
import { DynamoDBClient } from '@aws-sdk/client-dynamodb';
|
|
3
|
-
import { QueryCommand, QueryCommandInput } from '@aws-sdk/lib-dynamodb';
|
|
4
|
-
import { Condition, AttrBuilder, AttrRef } from '../shared';
|
|
5
|
-
import { createOpBuilder } from '../shared/operators';
|
|
6
|
-
import { buildExpression } from '../shared/conditions';
|
|
7
|
-
import { buildProjectionExpression } from '../shared/projection';
|
|
8
|
-
import { QueryBuilder, QueryExecutor, QueryResult } from './types';
|
|
9
|
-
import { ModelDefinition } from '../../core/types';
|
|
10
|
-
import { extractTemplateVars } from '../../utils/model-utils';
|
|
11
|
-
import { DynamoDBLogger } from '../../utils/dynamodb-logger';
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
* Internal state for the query builder
|
|
15
|
-
*/
|
|
16
|
-
type QueryState<Model> = {
|
|
17
|
-
tableName: string;
|
|
18
|
-
client: DynamoDBClient;
|
|
19
|
-
model?: ModelDefinition;
|
|
20
|
-
condition?: Condition;
|
|
21
|
-
indexName?: string;
|
|
22
|
-
limitValue?: number;
|
|
23
|
-
scanForward?: boolean;
|
|
24
|
-
projection?: (keyof Model)[];
|
|
25
|
-
consistentReadEnabled?: boolean;
|
|
26
|
-
exclusiveStartKey?: Record<string, any>;
|
|
27
|
-
logger?: DynamoDBLogger;
|
|
28
|
-
entityType?: string;
|
|
29
|
-
};
|
|
30
|
-
|
|
31
|
-
/**
|
|
32
|
-
* Maps a model attribute name to its corresponding DynamoDB key name (PK/SK)
|
|
33
|
-
* Also returns the key template for value transformation
|
|
34
|
-
*/
|
|
35
|
-
function getKeyNameForAttribute(
|
|
36
|
-
fieldName: string,
|
|
37
|
-
model?: ModelDefinition,
|
|
38
|
-
indexName?: string
|
|
39
|
-
): { keyName: string; template: string } | null {
|
|
40
|
-
if (!model?.key) return null;
|
|
41
|
-
|
|
42
|
-
// When an index is specified, check model.index first for key templates
|
|
43
|
-
if (indexName && model.index) {
|
|
44
|
-
for (const [keyName, keyDef] of Object.entries(model.index)) {
|
|
45
|
-
if (!keyName.startsWith(indexName)) {
|
|
46
|
-
continue;
|
|
47
|
-
}
|
|
48
|
-
const templateVars = extractTemplateVars(keyDef.value);
|
|
49
|
-
if (templateVars.includes(fieldName)) {
|
|
50
|
-
return { keyName, template: keyDef.value };
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
for (const [keyName, keyDef] of Object.entries(model.key)) {
|
|
56
|
-
const templateVars = extractTemplateVars(keyDef.value);
|
|
57
|
-
if (templateVars.includes(fieldName)) {
|
|
58
|
-
return { keyName, template: keyDef.value };
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
return null;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
/**
|
|
65
|
-
* Apply key template to transform an attribute value to its key value.
|
|
66
|
-
*
|
|
67
|
-
* For multi-variable templates (e.g. "RES#${category}#${code}") we can only fill
|
|
68
|
-
* the variable matching `fieldName`. For `beginsWith` queries we truncate the
|
|
69
|
-
* result at the first remaining `${...}` placeholder so the prefix is usable.
|
|
70
|
-
* For other operators an exact match is required, so we throw a clear error.
|
|
71
|
-
*/
|
|
72
|
-
function applyKeyTemplate(
|
|
73
|
-
template: string,
|
|
74
|
-
fieldName: string,
|
|
75
|
-
fieldValue: any,
|
|
76
|
-
keyOperator?: string
|
|
77
|
-
): string {
|
|
78
|
-
const substituted = template.replace(`\${${fieldName}}`, String(fieldValue));
|
|
79
|
-
const nextPlaceholder = substituted.indexOf('${');
|
|
80
|
-
if (nextPlaceholder === -1) return substituted;
|
|
81
|
-
|
|
82
|
-
if (keyOperator === 'beginsWith') {
|
|
83
|
-
return substituted.slice(0, nextPlaceholder);
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
throw new Error(
|
|
87
|
-
`Cannot use operator "${keyOperator ?? 'unknown'}" on key template "${template}" ` +
|
|
88
|
-
`with only "${fieldName}" provided — the template still contains unfilled ` +
|
|
89
|
-
`variable(s). Use beginsWith for prefix queries on composite keys.`
|
|
90
|
-
);
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
/**
|
|
94
|
-
* Separates a condition tree into key conditions and filter conditions
|
|
95
|
-
* Key conditions are rewritten to use actual DynamoDB key names (PK/SK)
|
|
96
|
-
*/
|
|
97
|
-
function separateConditions(
|
|
98
|
-
condition: Condition,
|
|
99
|
-
model?: ModelDefinition,
|
|
100
|
-
indexName?: string
|
|
101
|
-
): { keyConditions: Condition[]; filterConditions: Condition[] } {
|
|
102
|
-
const keyConditions: Condition[] = [];
|
|
103
|
-
const filterConditions: Condition[] = [];
|
|
104
|
-
|
|
105
|
-
function traverse(cond: Condition) {
|
|
106
|
-
// If it's a combinator
|
|
107
|
-
if (cond.operator && cond.children) {
|
|
108
|
-
// For KeyConditionExpression, we can only use AND at the top level
|
|
109
|
-
if (cond.operator === 'AND') {
|
|
110
|
-
cond.children.forEach((child) => {
|
|
111
|
-
// Check if this is a leaf condition or another combinator
|
|
112
|
-
if (child.children) {
|
|
113
|
-
// Nested combinator - goes to filter
|
|
114
|
-
filterConditions.push(child);
|
|
115
|
-
} else {
|
|
116
|
-
traverse(child);
|
|
117
|
-
}
|
|
118
|
-
});
|
|
119
|
-
} else {
|
|
120
|
-
// OR at any level must go to filter
|
|
121
|
-
filterConditions.push(cond);
|
|
122
|
-
}
|
|
123
|
-
return;
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
// It's a leaf condition - check if it's a key field
|
|
127
|
-
// Extract field name from the expression (e.g., "#username" -> "username")
|
|
128
|
-
const fieldMatch = cond.expression.match(/#(\w+)/);
|
|
129
|
-
if (fieldMatch && fieldMatch[1]) {
|
|
130
|
-
const fieldName = fieldMatch[1];
|
|
131
|
-
const keyInfo = getKeyNameForAttribute(fieldName, model, indexName);
|
|
132
|
-
|
|
133
|
-
if (keyInfo) {
|
|
134
|
-
// This is a key field - rewrite the condition to use the actual key name
|
|
135
|
-
// and apply the template to the value
|
|
136
|
-
|
|
137
|
-
// Find the value placeholder in the condition and transform it
|
|
138
|
-
const newValues: Record<string, any> = {};
|
|
139
|
-
if (cond.values) {
|
|
140
|
-
for (const [valuePlaceholder, value] of Object.entries(cond.values)) {
|
|
141
|
-
// Apply the key template to transform the value
|
|
142
|
-
const transformedValue = applyKeyTemplate(
|
|
143
|
-
keyInfo.template,
|
|
144
|
-
fieldName,
|
|
145
|
-
value,
|
|
146
|
-
cond.keyOperator
|
|
147
|
-
);
|
|
148
|
-
newValues[valuePlaceholder] = transformedValue;
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
const rewrittenCond = {
|
|
153
|
-
...cond,
|
|
154
|
-
expression: cond.expression.replace(
|
|
155
|
-
new RegExp(`#${fieldName}\\b`, 'g'),
|
|
156
|
-
`#${keyInfo.keyName}`
|
|
157
|
-
),
|
|
158
|
-
names: {
|
|
159
|
-
[`#${keyInfo.keyName}`]: keyInfo.keyName,
|
|
160
|
-
},
|
|
161
|
-
values: newValues,
|
|
162
|
-
};
|
|
163
|
-
|
|
164
|
-
keyConditions.push(rewrittenCond);
|
|
165
|
-
} else {
|
|
166
|
-
filterConditions.push(cond);
|
|
167
|
-
}
|
|
168
|
-
} else {
|
|
169
|
-
// Can't determine field, put in filter to be safe
|
|
170
|
-
filterConditions.push(cond);
|
|
171
|
-
}
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
traverse(condition);
|
|
175
|
-
|
|
176
|
-
return { keyConditions, filterConditions };
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
/**
|
|
180
|
-
* Builds a simple AND expression from multiple conditions
|
|
181
|
-
*/
|
|
182
|
-
function buildSimpleAndExpression(conditions: Condition[]): {
|
|
183
|
-
expression: string;
|
|
184
|
-
names: Record<string, string>;
|
|
185
|
-
values: Record<string, any>;
|
|
186
|
-
} {
|
|
187
|
-
if (conditions.length === 0) {
|
|
188
|
-
return { expression: '', names: {}, values: {} };
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
const expressions: string[] = [];
|
|
192
|
-
const names: Record<string, string> = {};
|
|
193
|
-
const values: Record<string, any> = {};
|
|
194
|
-
|
|
195
|
-
conditions.forEach((cond) => {
|
|
196
|
-
expressions.push(cond.expression);
|
|
197
|
-
Object.assign(names, cond.names || {});
|
|
198
|
-
Object.assign(values, cond.values || {});
|
|
199
|
-
});
|
|
200
|
-
|
|
201
|
-
return {
|
|
202
|
-
expression: expressions.join(' AND '),
|
|
203
|
-
names,
|
|
204
|
-
values,
|
|
205
|
-
};
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
/**
|
|
209
|
-
* Creates the query executor with all query methods
|
|
210
|
-
*/
|
|
211
|
-
function createQueryExecutor<Model>(state: QueryState<Model>): QueryExecutor<Model> {
|
|
212
|
-
return {
|
|
213
|
-
limit(count) {
|
|
214
|
-
return createQueryExecutor({
|
|
215
|
-
...state,
|
|
216
|
-
limitValue: count,
|
|
217
|
-
});
|
|
218
|
-
},
|
|
219
|
-
|
|
220
|
-
scanIndexForward(forward) {
|
|
221
|
-
return createQueryExecutor({
|
|
222
|
-
...state,
|
|
223
|
-
scanForward: forward,
|
|
224
|
-
});
|
|
225
|
-
},
|
|
226
|
-
|
|
227
|
-
useIndex(indexName) {
|
|
228
|
-
return createQueryExecutor({
|
|
229
|
-
...state,
|
|
230
|
-
indexName,
|
|
231
|
-
});
|
|
232
|
-
},
|
|
233
|
-
|
|
234
|
-
select(attrs) {
|
|
235
|
-
return createQueryExecutor({
|
|
236
|
-
...state,
|
|
237
|
-
projection: attrs,
|
|
238
|
-
});
|
|
239
|
-
},
|
|
240
|
-
|
|
241
|
-
consistentRead() {
|
|
242
|
-
return createQueryExecutor({
|
|
243
|
-
...state,
|
|
244
|
-
consistentReadEnabled: true,
|
|
245
|
-
});
|
|
246
|
-
},
|
|
247
|
-
|
|
248
|
-
startFrom(key) {
|
|
249
|
-
return createQueryExecutor({
|
|
250
|
-
...state,
|
|
251
|
-
exclusiveStartKey: key,
|
|
252
|
-
});
|
|
253
|
-
},
|
|
254
|
-
|
|
255
|
-
dbParams(): QueryCommandInput {
|
|
256
|
-
if (!state.condition) {
|
|
257
|
-
throw new Error('No where condition specified');
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
// Separate key conditions from filter conditions
|
|
261
|
-
const { keyConditions, filterConditions } = separateConditions(
|
|
262
|
-
state.condition,
|
|
263
|
-
state.model,
|
|
264
|
-
state.indexName
|
|
265
|
-
);
|
|
266
|
-
|
|
267
|
-
// DynamoDB Query requires at least a partition-key condition. If
|
|
268
|
-
// separation didn't pick anything as a key condition, the caller is
|
|
269
|
-
// either filtering on non-key attributes (which means they want
|
|
270
|
-
// scan()) or referencing the wrong attribute name. Fail loudly here
|
|
271
|
-
// instead of letting the SDK reject the request at execute time.
|
|
272
|
-
if (keyConditions.length === 0) {
|
|
273
|
-
const keyTemplates = state.indexName
|
|
274
|
-
? Object.entries(state.model?.index ?? {})
|
|
275
|
-
.filter(([keyName]) => keyName.startsWith(state.indexName!))
|
|
276
|
-
.map(([, def]) => def.value)
|
|
277
|
-
: Object.values(state.model?.key ?? {}).map((def) => def.value);
|
|
278
|
-
const templateVars = Array.from(
|
|
279
|
-
new Set(keyTemplates.flatMap((tpl) => extractTemplateVars(tpl)))
|
|
280
|
-
);
|
|
281
|
-
const indexHint = state.indexName ? ` on index "${state.indexName}"` : '';
|
|
282
|
-
const fieldsHint =
|
|
283
|
-
templateVars.length > 0
|
|
284
|
-
? ` Expected a condition on one of: ${templateVars.join(', ')}.`
|
|
285
|
-
: '';
|
|
286
|
-
throw new Error(
|
|
287
|
-
`Query requires a condition on the partition key${indexHint}.${fieldsHint} ` +
|
|
288
|
-
`For non-key filtering use scan() instead.`
|
|
289
|
-
);
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
// Build KeyConditionExpression (simple AND)
|
|
293
|
-
const keyExpr = buildSimpleAndExpression(keyConditions);
|
|
294
|
-
|
|
295
|
-
// Auto-inject an entity-type filter so query() only returns items
|
|
296
|
-
// belonging to this entity (single-table designs share PKs across
|
|
297
|
-
// entities, especially on GSIs).
|
|
298
|
-
const allFilters = [...filterConditions];
|
|
299
|
-
if (state.entityType) {
|
|
300
|
-
allFilters.push({
|
|
301
|
-
expression: '#_type = :_type',
|
|
302
|
-
names: { '#_type': '_type' },
|
|
303
|
-
values: { ':_type': state.entityType },
|
|
304
|
-
});
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
// Build FilterExpression (can be complex with OR, NOT, etc.)
|
|
308
|
-
let filterExpr = { expression: '', names: {}, values: {} };
|
|
309
|
-
if (allFilters.length > 0) {
|
|
310
|
-
if (allFilters.length === 1 && allFilters[0]) {
|
|
311
|
-
filterExpr = buildExpression(allFilters[0]);
|
|
312
|
-
} else {
|
|
313
|
-
// Multiple filter conditions - combine with AND
|
|
314
|
-
filterExpr = buildExpression({
|
|
315
|
-
expression: '',
|
|
316
|
-
operator: 'AND',
|
|
317
|
-
children: allFilters,
|
|
318
|
-
});
|
|
319
|
-
}
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
// Merge attribute names and values from both expressions
|
|
323
|
-
const allNames: Record<string, string> = {
|
|
324
|
-
...(keyExpr.names ?? {}),
|
|
325
|
-
...(filterExpr.names ?? {}),
|
|
326
|
-
};
|
|
327
|
-
const allValues = {
|
|
328
|
-
...(keyExpr.values ?? {}),
|
|
329
|
-
...(filterExpr.values ?? {}),
|
|
330
|
-
};
|
|
331
|
-
|
|
332
|
-
// Build ProjectionExpression with placeholders so reserved words
|
|
333
|
-
// (name, date, status, type, …) don't blow up at DynamoDB.
|
|
334
|
-
// Idempotent merge: filter/key and projection placeholders both map
|
|
335
|
-
// `#name → name`, so collisions resolve to the same value.
|
|
336
|
-
let projectionExpression: string | undefined;
|
|
337
|
-
if (state.projection && state.projection.length > 0) {
|
|
338
|
-
const proj = buildProjectionExpression(
|
|
339
|
-
(state.projection as (keyof Model)[]).map((a) => String(a))
|
|
340
|
-
);
|
|
341
|
-
projectionExpression = proj.ProjectionExpression;
|
|
342
|
-
Object.assign(allNames, proj.ExpressionAttributeNames);
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
return {
|
|
346
|
-
TableName: state.tableName,
|
|
347
|
-
...(keyExpr.expression && {
|
|
348
|
-
KeyConditionExpression: keyExpr.expression,
|
|
349
|
-
}),
|
|
350
|
-
...(filterExpr.expression && {
|
|
351
|
-
FilterExpression: filterExpr.expression,
|
|
352
|
-
}),
|
|
353
|
-
...(Object.keys(allNames).length && {
|
|
354
|
-
ExpressionAttributeNames: allNames,
|
|
355
|
-
}),
|
|
356
|
-
...(Object.keys(allValues).length && {
|
|
357
|
-
ExpressionAttributeValues: allValues,
|
|
358
|
-
}),
|
|
359
|
-
...(state.indexName && { IndexName: state.indexName }),
|
|
360
|
-
...(state.limitValue && { Limit: state.limitValue }),
|
|
361
|
-
...(state.scanForward !== undefined && {
|
|
362
|
-
ScanIndexForward: state.scanForward,
|
|
363
|
-
}),
|
|
364
|
-
...(projectionExpression && {
|
|
365
|
-
ProjectionExpression: projectionExpression,
|
|
366
|
-
}),
|
|
367
|
-
...(state.consistentReadEnabled && { ConsistentRead: true }),
|
|
368
|
-
...(state.exclusiveStartKey && {
|
|
369
|
-
ExclusiveStartKey: state.exclusiveStartKey,
|
|
370
|
-
}),
|
|
371
|
-
};
|
|
372
|
-
},
|
|
373
|
-
|
|
374
|
-
/**
|
|
375
|
-
* ⚠️ Returns only the FIRST page of results. DynamoDB caps each Query
|
|
376
|
-
* response at ~1MB (or `Limit` if set). If the matching set is larger,
|
|
377
|
-
* the remaining items are silently dropped.
|
|
378
|
-
*
|
|
379
|
-
* Use {@link executeWithPagination} when you need to drive pagination
|
|
380
|
-
* yourself, or {@link iterate} to walk every matching item lazily.
|
|
381
|
-
*/
|
|
382
|
-
async execute(): Promise<Model[]> {
|
|
383
|
-
const params = this.dbParams();
|
|
384
|
-
const result = await state.client.send(new QueryCommand(params));
|
|
385
|
-
state.logger?.log('QueryCommand', params, result);
|
|
386
|
-
return (result.Items ?? []) as unknown as Model[];
|
|
387
|
-
},
|
|
388
|
-
|
|
389
|
-
async executeWithPagination(): Promise<QueryResult<Model>> {
|
|
390
|
-
const params = this.dbParams();
|
|
391
|
-
const result = await state.client.send(new QueryCommand(params));
|
|
392
|
-
state.logger?.log('QueryCommand', params, result);
|
|
393
|
-
return {
|
|
394
|
-
items: (result.Items ?? []) as unknown as Model[],
|
|
395
|
-
lastEvaluatedKey: result.LastEvaluatedKey,
|
|
396
|
-
count: result.Count,
|
|
397
|
-
scannedCount: result.ScannedCount,
|
|
398
|
-
};
|
|
399
|
-
},
|
|
400
|
-
|
|
401
|
-
async *iterate(): AsyncIterableIterator<Model> {
|
|
402
|
-
const baseParams = this.dbParams();
|
|
403
|
-
let cursor: Record<string, any> | undefined = baseParams.ExclusiveStartKey;
|
|
404
|
-
do {
|
|
405
|
-
const params = { ...baseParams, ExclusiveStartKey: cursor };
|
|
406
|
-
const result = await state.client.send(new QueryCommand(params));
|
|
407
|
-
state.logger?.log('QueryCommand', params, result);
|
|
408
|
-
for (const item of (result.Items ?? []) as unknown as Model[]) {
|
|
409
|
-
yield item;
|
|
410
|
-
}
|
|
411
|
-
cursor = result.LastEvaluatedKey;
|
|
412
|
-
} while (cursor);
|
|
413
|
-
},
|
|
414
|
-
};
|
|
415
|
-
}
|
|
416
|
-
|
|
417
|
-
/**
|
|
418
|
-
* Creates a QueryBuilder for a table
|
|
419
|
-
*/
|
|
420
|
-
export function createQueryBuilder<Model>(
|
|
421
|
-
tableName: string,
|
|
422
|
-
client: DynamoDBClient,
|
|
423
|
-
model?: ModelDefinition,
|
|
424
|
-
logger?: DynamoDBLogger,
|
|
425
|
-
entityType?: string
|
|
426
|
-
): QueryBuilder<Model> {
|
|
427
|
-
return {
|
|
428
|
-
where(fn) {
|
|
429
|
-
// Create attribute builder proxy
|
|
430
|
-
const attrProxy = new Proxy({} as AttrBuilder<Model>, {
|
|
431
|
-
get(_, prop: string): AttrRef {
|
|
432
|
-
return { name: prop };
|
|
433
|
-
},
|
|
434
|
-
});
|
|
435
|
-
|
|
436
|
-
// Create a scoped opBuilder for this query to avoid global state
|
|
437
|
-
const opBuilder = createOpBuilder();
|
|
438
|
-
|
|
439
|
-
// Call the user's function to get the condition tree
|
|
440
|
-
const condition = fn(attrProxy, opBuilder);
|
|
441
|
-
|
|
442
|
-
// Create initial state with the condition
|
|
443
|
-
const initialState: QueryState<Model> = {
|
|
444
|
-
tableName,
|
|
445
|
-
client,
|
|
446
|
-
model,
|
|
447
|
-
condition,
|
|
448
|
-
logger,
|
|
449
|
-
entityType,
|
|
450
|
-
};
|
|
451
|
-
|
|
452
|
-
return createQueryExecutor(initialState);
|
|
453
|
-
},
|
|
454
|
-
};
|
|
455
|
-
}
|
|
@@ -1,109 +0,0 @@
|
|
|
1
|
-
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
2
|
-
import { ExecutableBuilder, AttrBuilder, OpBuilder, Condition } from '../shared';
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Query result with pagination support
|
|
6
|
-
*/
|
|
7
|
-
export interface QueryResult<Model> {
|
|
8
|
-
/**
|
|
9
|
-
* The items returned by the query
|
|
10
|
-
*/
|
|
11
|
-
items: Model[];
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
* The LastEvaluatedKey for pagination (if more results available)
|
|
15
|
-
*/
|
|
16
|
-
lastEvaluatedKey?: Record<string, any>;
|
|
17
|
-
|
|
18
|
-
/**
|
|
19
|
-
* Number of items examined before applying filters
|
|
20
|
-
*/
|
|
21
|
-
count?: number;
|
|
22
|
-
|
|
23
|
-
/**
|
|
24
|
-
* Number of items returned after applying filters
|
|
25
|
-
*/
|
|
26
|
-
scannedCount?: number;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
/**
|
|
30
|
-
* Query executor with additional query-specific options
|
|
31
|
-
*/
|
|
32
|
-
export interface QueryExecutor<Model> extends ExecutableBuilder<Model[]> {
|
|
33
|
-
/**
|
|
34
|
-
* Limit the number of items to return
|
|
35
|
-
*/
|
|
36
|
-
limit(count: number): QueryExecutor<Model>;
|
|
37
|
-
|
|
38
|
-
/**
|
|
39
|
-
* Scan index forward (ascending) or backward (descending)
|
|
40
|
-
*/
|
|
41
|
-
scanIndexForward(forward: boolean): QueryExecutor<Model>;
|
|
42
|
-
|
|
43
|
-
/**
|
|
44
|
-
* Use a secondary index
|
|
45
|
-
*/
|
|
46
|
-
useIndex(indexName: string): QueryExecutor<Model>;
|
|
47
|
-
|
|
48
|
-
/**
|
|
49
|
-
* Select specific attributes to return
|
|
50
|
-
*/
|
|
51
|
-
select(attrs: (keyof Model)[]): QueryExecutor<Model>;
|
|
52
|
-
|
|
53
|
-
/**
|
|
54
|
-
* Use consistent read
|
|
55
|
-
*/
|
|
56
|
-
consistentRead(): QueryExecutor<Model>;
|
|
57
|
-
|
|
58
|
-
/**
|
|
59
|
-
* Start query from a specific key (for pagination)
|
|
60
|
-
*/
|
|
61
|
-
startFrom(key: Record<string, any>): QueryExecutor<Model>;
|
|
62
|
-
|
|
63
|
-
/**
|
|
64
|
-
* Returns the raw DynamoDB query parameters
|
|
65
|
-
*/
|
|
66
|
-
dbParams(): any;
|
|
67
|
-
|
|
68
|
-
/**
|
|
69
|
-
* Executes the query and returns matching items
|
|
70
|
-
*/
|
|
71
|
-
execute(): Promise<Model[]>;
|
|
72
|
-
|
|
73
|
-
/**
|
|
74
|
-
* Executes the query and returns result with pagination metadata
|
|
75
|
-
*/
|
|
76
|
-
executeWithPagination(): Promise<QueryResult<Model>>;
|
|
77
|
-
|
|
78
|
-
/**
|
|
79
|
-
* Returns an async iterator that paginates internally and yields one item at
|
|
80
|
-
* a time. Memory stays at one page; you can `break` out of the loop early.
|
|
81
|
-
*
|
|
82
|
-
* `Limit` set via `.limit()` is forwarded to DynamoDB as a *per-request*
|
|
83
|
-
* cap, not a total cap. To cap the total, count yourself inside the loop:
|
|
84
|
-
*
|
|
85
|
-
* ```ts
|
|
86
|
-
* let n = 0;
|
|
87
|
-
* for await (const item of builder.iterate()) {
|
|
88
|
-
* if (n++ >= 1000) break;
|
|
89
|
-
* process(item);
|
|
90
|
-
* }
|
|
91
|
-
* ```
|
|
92
|
-
*/
|
|
93
|
-
iterate(): AsyncIterableIterator<Model>;
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
/**
|
|
97
|
-
* Main Query Builder interface with type-safe where clause
|
|
98
|
-
*/
|
|
99
|
-
export interface QueryBuilder<Model> {
|
|
100
|
-
/**
|
|
101
|
-
* Build a condition expression using attributes and operators
|
|
102
|
-
* Usage: .where((attr, op) => op.eq(attr.username, 'juanca'))
|
|
103
|
-
* Usage: .where((attr, op) => op.and(
|
|
104
|
-
* op.eq(attr.username, 'juanca'),
|
|
105
|
-
* op.gt(attr.age, 18)
|
|
106
|
-
* ))
|
|
107
|
-
*/
|
|
108
|
-
where(fn: (attr: AttrBuilder<Model>, op: OpBuilder) => Condition): QueryExecutor<Model>;
|
|
109
|
-
}
|