Skip to content

Commit 49d5d87

Browse files
committed
Start query implementation
1 parent 9296c66 commit 49d5d87

File tree

7 files changed

+959
-37
lines changed

7 files changed

+959
-37
lines changed

packages/dynamodb-entity-store-testutils/src/conditionExpressionEvaluator.ts

Lines changed: 4 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { NativeAttributeValue } from '@aws-sdk/lib-dynamodb'
2+
import { resolveAttributeName, resolveAttributeValue } from './expressionEvaluatorUtils.js'
23

34
// TODO - WARNING! THIS CODE IS FOR TESTS ONLY! DON'T USE THIS IN PRODUCTION
45
// I haven't fully validated this implementation, and so I can't guarantee it is correct
@@ -189,7 +190,7 @@ function splitByOperator(expression: string, operator: string): { left: string;
189190
* Evaluates a function call.
190191
*/
191192
function evaluateFunction(functionName: string, arg: string, context: EvaluationContext): boolean {
192-
const resolvedPath = resolveAttributeName(arg, context)
193+
const resolvedPath = resolveAttributeName(arg, context.expressionAttributeNames)
193194
const value = getValueAtPath(resolvedPath, context.item)
194195

195196
switch (functionName.toLowerCase()) {
@@ -232,41 +233,14 @@ function resolveValue(expression: string, context: EvaluationContext): NativeAtt
232233

233234
// Check if it's an expression attribute value
234235
if (trimmed.startsWith(':')) {
235-
if (!context.expressionAttributeValues) {
236-
throw new Error(`Expression attribute value ${trimmed} used but no ExpressionAttributeValues provided`)
237-
}
238-
const value = context.expressionAttributeValues[trimmed]
239-
if (value === undefined) {
240-
throw new Error(`Expression attribute value ${trimmed} not found in ExpressionAttributeValues`)
241-
}
242-
return value
236+
return resolveAttributeValue(trimmed, context.expressionAttributeValues)
243237
}
244238

245239
// Otherwise, it's an attribute path
246-
const resolvedPath = resolveAttributeName(trimmed, context)
240+
const resolvedPath = resolveAttributeName(trimmed, context.expressionAttributeNames)
247241
return getValueAtPath(resolvedPath, context.item)
248242
}
249243

250-
/**
251-
* Resolves an attribute name (handles #placeholder syntax).
252-
*/
253-
function resolveAttributeName(name: string, context: EvaluationContext): string {
254-
const trimmed = name.trim()
255-
256-
if (trimmed.startsWith('#')) {
257-
if (!context.expressionAttributeNames) {
258-
throw new Error(`Expression attribute name ${trimmed} used but no ExpressionAttributeNames provided`)
259-
}
260-
const resolved = context.expressionAttributeNames[trimmed]
261-
if (resolved === undefined) {
262-
throw new Error(`Expression attribute name ${trimmed} not found in ExpressionAttributeNames`)
263-
}
264-
return resolved
265-
}
266-
267-
return trimmed
268-
}
269-
270244
/**
271245
* Gets a value at a given path in the item.
272246
*/
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import { NativeAttributeValue } from '@aws-sdk/lib-dynamodb'
2+
3+
/**
4+
* Shared utilities for DynamoDB expression evaluation.
5+
* Used by both condition expression and key condition expression evaluators.
6+
*/
7+
8+
/**
9+
* Resolves an attribute name (handles #placeholder syntax).
10+
*/
11+
export function resolveAttributeName(
12+
name: string,
13+
expressionAttributeNames?: Record<string, string>
14+
): string {
15+
const trimmed = name.trim()
16+
17+
if (trimmed.startsWith('#')) {
18+
if (!expressionAttributeNames) {
19+
throw new Error(
20+
`Expression attribute name ${trimmed} used but no ExpressionAttributeNames provided`
21+
)
22+
}
23+
const resolved = expressionAttributeNames[trimmed]
24+
if (resolved === undefined) {
25+
throw new Error(`Expression attribute name ${trimmed} not found in ExpressionAttributeNames`)
26+
}
27+
return resolved
28+
}
29+
30+
return trimmed
31+
}
32+
33+
/**
34+
* Resolves an attribute value (handles :placeholder syntax).
35+
*/
36+
export function resolveAttributeValue(
37+
placeholder: string,
38+
expressionAttributeValues?: Record<string, NativeAttributeValue>
39+
): NativeAttributeValue {
40+
const trimmed = placeholder.trim()
41+
42+
if (!trimmed.startsWith(':')) {
43+
throw new Error(`Expected value placeholder starting with ':', got: ${trimmed}`)
44+
}
45+
46+
if (!expressionAttributeValues) {
47+
throw new Error(
48+
`Expression attribute value ${trimmed} used but no ExpressionAttributeValues provided`
49+
)
50+
}
51+
52+
const value = expressionAttributeValues[trimmed]
53+
if (value === undefined) {
54+
throw new Error(
55+
`Expression attribute value ${trimmed} not found in ExpressionAttributeValues`
56+
)
57+
}
58+
59+
return value
60+
}

packages/dynamodb-entity-store-testutils/src/fakeDynamoDBInterface.ts

Lines changed: 70 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,10 @@ import {
2121
} from '@aws-sdk/lib-dynamodb'
2222
import { DynamoDBInterface } from '@symphoniacloud/dynamodb-entity-store'
2323
import { evaluateConditionExpression } from './conditionExpressionEvaluator.js'
24+
import {
25+
parseKeyConditionExpression,
26+
matchesKeyCondition
27+
} from './keyConditionExpressionEvaluator.js'
2428

2529
export const METADATA = { $metadata: {} }
2630

@@ -36,13 +40,36 @@ const supportedParamKeysByFunction = {
3640
delete: ['TableName', 'Key'],
3741
batchWrite: ['RequestItems'],
3842
transactionWrite: ['TransactItems'],
43+
queryOnePage: [
44+
'TableName',
45+
'KeyConditionExpression',
46+
'ExpressionAttributeNames',
47+
'ExpressionAttributeValues',
48+
'Limit',
49+
'ExclusiveStartKey'
50+
],
51+
queryAllPages: [
52+
'TableName',
53+
'KeyConditionExpression',
54+
'ExpressionAttributeNames',
55+
'ExpressionAttributeValues'
56+
],
3957
scanOnePage: ['TableName'],
4058
scanAllPages: ['TableName']
4159
}
4260

4361
function checkSupportedParams(
4462
params: object,
45-
functionName: 'put' | 'get' | 'delete' | 'batchWrite' | 'transactionWrite' | 'scanOnePage' | 'scanAllPages'
63+
functionName:
64+
| 'put'
65+
| 'get'
66+
| 'delete'
67+
| 'batchWrite'
68+
| 'transactionWrite'
69+
| 'queryOnePage'
70+
| 'queryAllPages'
71+
| 'scanOnePage'
72+
| 'scanAllPages'
4673
) {
4774
const unsupportedKeys = Object.keys(params).filter(
4875
(key) => !supportedParamKeysByFunction[functionName].includes(key)
@@ -152,20 +179,52 @@ export class FakeDynamoDBInterface implements DynamoDBInterface {
152179
throw new Error('Not yet implemented')
153180
}
154181

155-
// eslint-disable-next-line @typescript-eslint/no-unused-vars
156182
async queryOnePage(params: QueryCommandInput): Promise<QueryCommandOutput> {
157-
throw new Error('Not yet implemented')
183+
checkSupportedParams(params, 'queryOnePage')
184+
const items = this.executeQuery(params)
185+
186+
// Apply limit if specified
187+
const limitedItems = params.Limit ? items.slice(0, params.Limit) : items
188+
189+
return {
190+
Items: limitedItems,
191+
...METADATA
192+
}
158193
}
159194

160-
// eslint-disable-next-line @typescript-eslint/no-unused-vars
161195
async queryAllPages(params: QueryCommandInput): Promise<QueryCommandOutput[]> {
162-
throw new Error('Not yet implemented')
196+
checkSupportedParams(params, 'queryAllPages')
197+
const items = this.executeQuery(params)
198+
199+
return [
200+
{
201+
Items: items,
202+
...METADATA
203+
}
204+
]
205+
}
206+
207+
private executeQuery(params: QueryCommandInput): Record<string, NativeAttributeValue>[] {
208+
if (!params.KeyConditionExpression) {
209+
throw new Error('KeyConditionExpression is required for query')
210+
}
211+
212+
// Parse the key condition
213+
const keyCondition = parseKeyConditionExpression(
214+
params.KeyConditionExpression,
215+
params.ExpressionAttributeNames,
216+
params.ExpressionAttributeValues
217+
)
218+
219+
// Get all items and filter by key condition
220+
const allItems = this.getTableFrom(params).allItems()
221+
return allItems.filter((item) => matchesKeyCondition(item, keyCondition))
163222
}
164223

165224
async scanOnePage(params: ScanCommandInput): Promise<ScanCommandOutput> {
166225
checkSupportedParams(params, 'scanOnePage')
167226
return {
168-
Items: this.getTableFrom(params).allItems(),
227+
Items: this.executeScan(params),
169228
...METADATA
170229
}
171230
}
@@ -174,12 +233,16 @@ export class FakeDynamoDBInterface implements DynamoDBInterface {
174233
checkSupportedParams(params, 'scanAllPages')
175234
return [
176235
{
177-
Items: this.getTableFrom(params).allItems(),
236+
Items: this.executeScan(params),
178237
...METADATA
179238
}
180239
]
181240
}
182241

242+
private executeScan(params: ScanCommandInput): Record<string, NativeAttributeValue>[] {
243+
return this.getTableFrom(params).allItems()
244+
}
245+
183246
private getTableFrom(withTableName: { TableName: string | undefined }) {
184247
return this.getTable(withTableName.TableName)
185248
}

packages/dynamodb-entity-store-testutils/src/index.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,10 @@ export {
44
UnsupportedConditionExpressionError,
55
ConditionalCheckFailedException
66
} from './conditionExpressionEvaluator.js'
7+
export {
8+
parseKeyConditionExpression,
9+
matchesKeyCondition,
10+
UnsupportedKeyConditionExpressionError,
11+
type KeyCondition
12+
} from './keyConditionExpressionEvaluator.js'
13+
export { resolveAttributeName, resolveAttributeValue } from './expressionEvaluatorUtils.js'

0 commit comments

Comments
 (0)