Location
`lib/query.ts:102-188 (compare)`, `:300-385 (beginsWith)`, `:395-499 (between)`
Problem
Three methods with ~90% identical code:
- Resolve sort key(s) — string passthrough vs `facet.sk()`/`index.sk()`
- Build `QueryInput` with `TableName`, `IndexName`, `ExpressionAttributeNames = { '#PK', '#SK' }`, `ExpressionAttributeValues` with the partition value plus sort values
- Apply `Limit`, `ScanIndexForward`, `ExclusiveStartKey` (decoded cursor)
- Apply filter via `expression-builder` and merge into names/values
- Call `dynamoDb.query`, collect items through `facet.out()`, attach cursor from `LastEvaluatedKey`
Only two things differ across the three: the `KeyConditionExpression` template, and how many sort placeholders (`:sort` vs `:start`/`:end`). About 200 lines of dead weight.
Proposed shape
```ts
private async runQuery(
keyCondition: {
expression: string; // uses '#PK', '#SK', and named sort placeholders
sortValues: Record<string, AttributeValue>; // e.g. { ':sort': {...} }
},
options: QueryOptions<T, PK, SK>,
): Promise<QueryResult> {
// scaffolding lives here
}
// callers shrink to:
equals(sort, options = {}) {
return this.runQuery(
{ expression: '#PK = :partition AND #SK = :sort', sortValues: this.buildSort(sort, options.shard) },
options,
);
}
```
Benefits
- One place to add query features (e.g., `ProjectionExpression`, `ConsistentRead`, `Select`).
- Fewer lines; easier review.
- Removes three duplicate call sites to `facet.out()` / `encodeCursor(LastEvaluatedKey)`.
Risks
Behavior must stay identical. The sort-key construction (string passthrough vs built key) needs to be factored out carefully — `between` builds two, the others build one. A small `buildSortValues(...)` helper handles both.
Priority
Medium — not urgent, but this file is the hottest surface for future changes and shrinking it pays back fast.
Location
`lib/query.ts:102-188 (compare)`, `:300-385 (beginsWith)`, `:395-499 (between)`
Problem
Three methods with ~90% identical code:
Only two things differ across the three: the `KeyConditionExpression` template, and how many sort placeholders (`:sort` vs `:start`/`:end`). About 200 lines of dead weight.
Proposed shape
```ts
private async runQuery(
keyCondition: {
expression: string; // uses '#PK', '#SK', and named sort placeholders
sortValues: Record<string, AttributeValue>; // e.g. { ':sort': {...} }
},
options: QueryOptions<T, PK, SK>,
): Promise<QueryResult> {
// scaffolding lives here
}
// callers shrink to:
equals(sort, options = {}) {
return this.runQuery(
{ expression: '#PK = :partition AND #SK = :sort', sortValues: this.buildSort(sort, options.shard) },
options,
);
}
```
Benefits
Risks
Behavior must stay identical. The sort-key construction (string passthrough vs built key) needs to be factored out carefully — `between` builds two, the others build one. A small `buildSortValues(...)` helper handles both.
Priority
Medium — not urgent, but this file is the hottest surface for future changes and shrinking it pays back fast.