betterddb
Version:
A definition-based DynamoDB wrapper library that provides a schema-driven and fully typesafe DAL.
171 lines • 7.43 kB
JavaScript
/* eslint-disable no-unused-vars */
import { QueryCommand, } from "@aws-sdk/lib-dynamodb";
import {} from "../betterddb.js";
import { getOperatorExpression } from "../operator.js";
import {} from "../types/paginated-result.js";
export class QueryBuilder {
parent;
key;
keyConditions = [];
filterConditions = [];
expressionAttributeNames = {};
expressionAttributeValues;
index;
limit;
lastKey;
ascending = true;
constructor(parent, key) {
this.parent = parent;
this.key = key;
const keys = this.parent.getKeys();
const pkName = keys.primary.name;
const builtKey = this.parent.buildKey(this.key);
this.expressionAttributeNames = {
"#pk": pkName,
};
this.expressionAttributeValues = {
":pk_value": builtKey[pkName],
};
}
usingIndex(indexName) {
if (!this.parent.getKeys().gsis) {
throw new Error("No global secondary indexes defined for this table");
}
if (!(indexName in this.parent.getKeys().gsis)) {
throw new Error("index does not exist");
}
this.index = this.parent.getKeys().gsis[indexName];
if (!this.index) {
throw new Error("Failed to get index configuration");
}
const pkName = this.index.primary.name;
const builtKey = this.parent.buildIndexes(this.key);
this.expressionAttributeNames["#pk"] = pkName;
this.expressionAttributeValues[":pk_value"] = builtKey[pkName];
return this;
}
sortAscending() {
this.ascending = true;
return this;
}
sortDescending() {
this.ascending = false;
return this;
}
where(operator, values) {
const keys = this.parent.getKeys();
// Determine the sort key name from either the index or the primary keys.
const sortKeyName = this.index ? this.index.sort?.name : keys.sort?.name;
if (!sortKeyName) {
throw new Error("Sort key is not defined for this table/index.");
}
const nameKey = "#sk";
this.expressionAttributeNames[nameKey] = sortKeyName;
// Enforce that a complex sort key requires an object input.
if (typeof values !== "object" || values === null) {
throw new Error(`For complex sort keys, please provide an object with all necessary properties.`);
}
if (operator === "between") {
if (!Array.isArray(values) || values.length !== 2) {
throw new Error(`For 'between' operator, values must be a tuple of two objects`);
}
const valueKeyStart = ":sk_start";
const valueKeyEnd = ":sk_end";
// Use the key definition's build function to build the key from the full object.
this.expressionAttributeValues[valueKeyStart] = this.index
? this.parent.buildIndexes(values[0])[sortKeyName]
: this.parent.buildKey(values[0])[sortKeyName];
this.expressionAttributeValues[valueKeyEnd] = this.index
? this.parent.buildIndexes(values[1])[sortKeyName]
: this.parent.buildKey(values[1])[sortKeyName];
this.keyConditions.push(`${nameKey} BETWEEN ${valueKeyStart} AND ${valueKeyEnd}`);
}
else if (operator === "begins_with") {
const valueKey = ":sk_value";
this.expressionAttributeValues[valueKey] = this.index
? this.parent.buildIndexes(values)[sortKeyName]
: this.parent.buildKey(values)[sortKeyName];
this.keyConditions.push(`begins_with(${nameKey}, ${valueKey})`);
}
else {
// For eq, lt, lte, gt, gte:
const valueKey = ":sk_value";
this.expressionAttributeValues[valueKey] = this.index
? this.parent.buildIndexes(values)[sortKeyName]
: this.parent.buildKey(values)[sortKeyName];
const condition = getOperatorExpression(operator, nameKey, valueKey);
this.keyConditions.push(condition);
}
return this;
}
filter(attribute, operator, values) {
const attrStr = String(attribute);
const randomString = Math.random().toString(36).substring(2, 15);
const placeholderName = `#attr_${attrStr}_${randomString}`;
this.expressionAttributeNames[placeholderName] = attrStr;
if (operator === "between") {
if (!Array.isArray(values) || values.length !== 2) {
throw new Error("For 'between' operator, values must be a tuple of two items");
}
const placeholderValueStart = `:val_start_${attrStr}_${randomString}`;
const placeholderValueEnd = `:val_end_${attrStr}_${randomString}`;
this.expressionAttributeValues[placeholderValueStart] =
values[0];
this.expressionAttributeValues[placeholderValueEnd] = values[1];
this.filterConditions.push(`${placeholderName} BETWEEN ${placeholderValueStart} AND ${placeholderValueEnd}`);
}
else if (operator === "begins_with" || operator === "contains") {
const placeholderValue = `:val_${attrStr}_${randomString}`;
this.expressionAttributeValues[placeholderValue] = values;
this.filterConditions.push(`${operator}(${placeholderName}, ${placeholderValue})`);
}
else {
const placeholderValue = `:val_${attrStr}_${randomString}`;
this.expressionAttributeValues[placeholderValue] = values;
const condition = getOperatorExpression(operator, placeholderName, placeholderValue);
this.filterConditions.push(condition);
}
return this;
}
limitResults(limit) {
this.limit = limit;
return this;
}
startFrom(lastKey) {
this.lastKey = lastKey;
return this;
}
/**
* Executes the query and returns a Promise that resolves with an array of items.
*/
async execute() {
this.keyConditions.unshift(`#pk = :pk_value`);
const keyConditionExpression = this.keyConditions.join(" AND ");
const params = {
TableName: this.parent.getTableName(),
KeyConditionExpression: keyConditionExpression,
ExpressionAttributeNames: this.expressionAttributeNames,
ExpressionAttributeValues: this.expressionAttributeValues,
ScanIndexForward: this.ascending,
Limit: this.limit,
ExclusiveStartKey: this.lastKey,
IndexName: this.index?.name ?? undefined,
};
if (this.parent.getEntityType()) {
this.filterConditions.push(`#entity = :entity_value`);
this.expressionAttributeNames["#entity"] = "entityType";
this.expressionAttributeValues[":entity_value"] =
this.parent.getEntityType();
}
params.FilterExpression =
this.filterConditions.length > 0
? this.filterConditions.join(" AND ")
: undefined;
const result = await this.parent.getClient().send(new QueryCommand(params));
return {
items: this.parent.getSchema().array().parse(result.Items),
lastKey: result.LastEvaluatedKey ?? undefined,
};
}
}
//# sourceMappingURL=query-builder.js.map