diff --git a/src/queryBuilders/getItemQueryBuilder.ts b/src/queryBuilders/getItemQueryBuilder.ts index 7ed063a..e256102 100644 --- a/src/queryBuilders/getItemQueryBuilder.ts +++ b/src/queryBuilders/getItemQueryBuilder.ts @@ -9,6 +9,7 @@ import { StripKeys, } from "../typeHelpers"; import { preventAwait } from "../util/preventAwait"; +import { QueryCompiler } from "../queryCompiler"; export interface GetQueryBuilderInterface { keys & PickSkRequired>( @@ -77,15 +78,8 @@ export class GetQueryBuilder } execute = async (): Promise> | undefined> => { - const command = new GetCommand({ - TableName: this.#props.node.table?.table, - Key: this.#props.node.keys?.keys, - ConsistentRead: this.#props.node.consistentRead?.enabled, - ProjectionExpression: this.#props.node.attributes?.attributes.join(", "), - }); - + const command = this.#props.queryCompiler.compile(this.#props.node); const item = await this.#props.ddbClient.send(command); - return (item.Item as StripKeys>) ?? undefined; }; } @@ -98,4 +92,5 @@ preventAwait( interface GetQueryBuilderProps { readonly node: GetNode; readonly ddbClient: DynamoDBDocumentClient; + readonly queryCompiler: QueryCompiler; } diff --git a/src/queryBuilders/queryQueryBuilder.ts b/src/queryBuilders/queryQueryBuilder.ts index 23c5256..5a2ae3a 100644 --- a/src/queryBuilders/queryQueryBuilder.ts +++ b/src/queryBuilders/queryQueryBuilder.ts @@ -14,6 +14,7 @@ import { NotExpression, } from "../nodes/operands"; import { QueryNode } from "../nodes/queryNode"; +import { QueryCompiler } from "../queryCompiler"; import { DeepPartial, GetFromPath, @@ -630,166 +631,9 @@ export class QueryQueryBuilder< }) as any; } - compileFilterExpression = ( - expression: FilterExpressionNode, - filterExpressionAttributeValues: Map - ) => { - let res = ""; - - expression.expressions.forEach((joinNode, i) => { - if (i !== 0) { - res += ` ${joinNode.joinType} `; - } - - res += this.compileFilterExpressionJoinNodes( - joinNode, - filterExpressionAttributeValues - ); - }); - - return res; - }; - - compileFilterExpressionJoinNodes = ( - { expr }: FilterExpressionJoinTypeNode, - filterExpressionAttributeValues: Map - ) => { - let res = ""; - const offset = filterExpressionAttributeValues.size; - const attributeValue = `:filterExpressionValue${offset}`; - - switch (expr.kind) { - case "FilterExpressionNode": { - res += "("; - res += this.compileFilterExpression( - expr, - filterExpressionAttributeValues - ); - res += ")"; - break; - } - - case "FilterExpressionComparatorExpressions": { - // TODO: Instead of expr.key, use AttributeNames here to avoid - // problems with using reserved words. - res += `${expr.key} ${expr.operation} ${attributeValue}`; - filterExpressionAttributeValues.set(attributeValue, expr.value); - break; - } - - case "FilterExpressionNotExpression": { - res += "NOT ("; - res += this.compileFilterExpression( - expr.expr, - filterExpressionAttributeValues - ); - res += ")"; - break; - } - - case "BetweenConditionExpression": { - res += `${expr.key} BETWEEN ${attributeValue}left AND ${attributeValue}right`; - filterExpressionAttributeValues.set(`${attributeValue}left`, expr.left); - filterExpressionAttributeValues.set( - `${attributeValue}right`, - expr.right - ); - break; - } - - case "AttributeExistsFunctionExpression": { - res += `attribute_exists(${expr.key})`; - break; - } - - case "AttributeNotExistsFunctionExpression": { - res += `attribute_not_exists(${expr.key})`; - break; - } - - case "BeginsWithFunctionExpression": { - res += `begins_with(${expr.key}, ${attributeValue})`; - - filterExpressionAttributeValues.set(attributeValue, expr.substr); - } - } - - return res; - }; - - compileKeyConditionExpression = ( - keyConditionAttributeValues: Map - ) => { - let res = ""; - this.#props.node.keyConditions.forEach((keyCondition, i) => { - if (i !== 0) { - res += " AND "; - } - - const attributeValue = `:keyConditionValue${i}`; - if (keyCondition.operation.kind === "KeyConditionComparatorExpression") { - // TODO: Instead of expr.key, use AttributeNames here to avoid - // problems with using reserved words. - res += `${keyCondition.operation.key} ${keyCondition.operation.operation} ${attributeValue}`; - keyConditionAttributeValues.set( - attributeValue, - keyCondition.operation.value - ); - } else if (keyCondition.operation.kind === "BetweenConditionExpression") { - res += `${keyCondition.operation.key} BETWEEN ${attributeValue}left AND ${attributeValue}right`; - keyConditionAttributeValues.set( - `${attributeValue}left`, - keyCondition.operation.left - ); - keyConditionAttributeValues.set( - `${attributeValue}right`, - keyCondition.operation.right - ); - } else if ( - keyCondition.operation.kind === "BeginsWithFunctionExpression" - ) { - res += `begins_with(${keyCondition.operation.key}, ${attributeValue})`; - keyConditionAttributeValues.set( - attributeValue, - keyCondition.operation.substr - ); - } - }); - - return res; - }; - execute = async (): Promise>[] | undefined> => { - const keyConditionAttributeValues = new Map(); - const filterExpressionAttributeValues = new Map(); - - const compiledKeyConditionExpression = this.compileKeyConditionExpression( - keyConditionAttributeValues - ); - - const compiledFilterExpression = this.compileFilterExpression( - this.#props.node.filterExpression, - filterExpressionAttributeValues - ); - - const command = new QueryCommand({ - TableName: this.#props.node.table.table, - KeyConditionExpression: compiledKeyConditionExpression, - FilterExpression: compiledFilterExpression - ? compiledFilterExpression - : undefined, - Limit: this.#props.node.limit?.limit, - ExpressionAttributeValues: { - ...Object.fromEntries(keyConditionAttributeValues), - ...Object.fromEntries(filterExpressionAttributeValues), - }, - ScanIndexForward: this.#props.node.scanIndexForward?.enabled, - ConsistentRead: this.#props.node.consistentRead?.enabled, - ProjectionExpression: this.#props.node.attributes?.attributes.join(", "), - }); - + const command = this.#props.queryCompiler.compile(this.#props.node); const result = await this.#props.ddbClient.send(command); - return (result.Items as StripKeys>[]) ?? undefined; }; } @@ -802,4 +646,5 @@ preventAwait( interface GetQueryBuilderProps { readonly node: QueryNode; readonly ddbClient: DynamoDBDocumentClient; + readonly queryCompiler: QueryCompiler; } diff --git a/src/queryCompiler/index.ts b/src/queryCompiler/index.ts new file mode 100644 index 0000000..c9bdb8d --- /dev/null +++ b/src/queryCompiler/index.ts @@ -0,0 +1 @@ +export * from "./queryCompiler"; diff --git a/src/queryCompiler/queryCompiler.ts b/src/queryCompiler/queryCompiler.ts new file mode 100644 index 0000000..f18568c --- /dev/null +++ b/src/queryCompiler/queryCompiler.ts @@ -0,0 +1,206 @@ +import { GetCommand, QueryCommand } from "@aws-sdk/lib-dynamodb"; +import { FilterExpressionJoinTypeNode } from "../nodes/filterExpressionJoinTypeNode"; +import { FilterExpressionNode } from "../nodes/filterExpressionNode"; +import { GetNode } from "../nodes/getNode"; +import { KeyConditionNode } from "../nodes/keyConditionNode"; +import { QueryNode } from "../nodes/queryNode"; + +export class QueryCompiler { + compile(rootNode: QueryNode): QueryCommand; + compile(rootNode: GetNode): GetCommand; + compile(rootNode: QueryNode | GetNode) { + switch (rootNode.kind) { + case "GetNode": + return this.compileGetNode(rootNode); + case "QueryNode": + return this.compileQueryNode(rootNode); + } + } + + compileGetNode(getNode: GetNode): GetCommand { + const { + table: tableNode, + keys: keysNode, + consistentRead: consistentReadNode, + attributes: attributesNode, + } = getNode; + + return new GetCommand({ + TableName: tableNode.table, + Key: keysNode?.keys, + ConsistentRead: consistentReadNode?.enabled, + ProjectionExpression: attributesNode?.attributes.join(", "), + }); + } + + compileQueryNode(queryNode: QueryNode): QueryCommand { + const { + table: tableNode, + filterExpression: filterExpressionNode, + keyConditions: keyConditionsNode, + limit: limitNode, + scanIndexForward: scanIndexForwardNode, + consistentRead: consistentReadNode, + attributes: attributesNode, + } = queryNode; + + const keyConditionAttributeValues = new Map(); + const filterExpressionAttributeValues = new Map(); + + const compiledKeyConditionExpression = this.compileKeyConditionExpression( + keyConditionsNode, + keyConditionAttributeValues + ); + + const compiledFilterExpression = this.compileFilterExpression( + filterExpressionNode, + filterExpressionAttributeValues + ); + + return new QueryCommand({ + TableName: tableNode.table, + KeyConditionExpression: compiledKeyConditionExpression, + FilterExpression: compiledFilterExpression + ? compiledFilterExpression + : undefined, + Limit: limitNode?.limit, + ExpressionAttributeValues: { + ...Object.fromEntries(keyConditionAttributeValues), + ...Object.fromEntries(filterExpressionAttributeValues), + }, + ScanIndexForward: scanIndexForwardNode?.enabled, + ConsistentRead: consistentReadNode?.enabled, + ProjectionExpression: attributesNode?.attributes?.join(", "), + }); + } + + compileFilterExpression = ( + expression: FilterExpressionNode, + filterExpressionAttributeValues: Map + ) => { + let res = ""; + + expression.expressions.forEach((joinNode, i) => { + if (i !== 0) { + res += ` ${joinNode.joinType} `; + } + + res += this.compileFilterExpressionJoinNodes( + joinNode, + filterExpressionAttributeValues + ); + }); + + return res; + }; + + compileFilterExpressionJoinNodes = ( + { expr }: FilterExpressionJoinTypeNode, + filterExpressionAttributeValues: Map + ) => { + let res = ""; + const offset = filterExpressionAttributeValues.size; + const attributeValue = `:filterExpressionValue${offset}`; + + switch (expr.kind) { + case "FilterExpressionNode": { + res += "("; + res += this.compileFilterExpression( + expr, + filterExpressionAttributeValues + ); + res += ")"; + break; + } + + case "FilterExpressionComparatorExpressions": { + // TODO: Instead of expr.key, use AttributeNames here to avoid + // problems with using reserved words. + res += `${expr.key} ${expr.operation} ${attributeValue}`; + filterExpressionAttributeValues.set(attributeValue, expr.value); + break; + } + + case "FilterExpressionNotExpression": { + res += "NOT ("; + res += this.compileFilterExpression( + expr.expr, + filterExpressionAttributeValues + ); + res += ")"; + break; + } + + case "BetweenConditionExpression": { + res += `${expr.key} BETWEEN ${attributeValue}left AND ${attributeValue}right`; + filterExpressionAttributeValues.set(`${attributeValue}left`, expr.left); + filterExpressionAttributeValues.set( + `${attributeValue}right`, + expr.right + ); + break; + } + + case "AttributeExistsFunctionExpression": { + res += `attribute_exists(${expr.key})`; + break; + } + + case "AttributeNotExistsFunctionExpression": { + res += `attribute_not_exists(${expr.key})`; + break; + } + + case "BeginsWithFunctionExpression": { + res += `begins_with(${expr.key}, ${attributeValue})`; + + filterExpressionAttributeValues.set(attributeValue, expr.substr); + } + } + + return res; + }; + + compileKeyConditionExpression = ( + keyConditions: KeyConditionNode[], + keyConditionAttributeValues: Map + ) => { + let res = ""; + keyConditions.forEach((keyCondition, i) => { + if (i !== 0) { + res += " AND "; + } + + const attributeValue = `:keyConditionValue${i}`; + if (keyCondition.operation.kind === "KeyConditionComparatorExpression") { + // TODO: Instead of expr.key, use AttributeNames here to avoid + // problems with using reserved words. + res += `${keyCondition.operation.key} ${keyCondition.operation.operation} ${attributeValue}`; + keyConditionAttributeValues.set( + attributeValue, + keyCondition.operation.value + ); + } else if (keyCondition.operation.kind === "BetweenConditionExpression") { + res += `${keyCondition.operation.key} BETWEEN ${attributeValue}left AND ${attributeValue}right`; + keyConditionAttributeValues.set( + `${attributeValue}left`, + keyCondition.operation.left + ); + keyConditionAttributeValues.set( + `${attributeValue}right`, + keyCondition.operation.right + ); + } else if ( + keyCondition.operation.kind === "BeginsWithFunctionExpression" + ) { + res += `begins_with(${keyCondition.operation.key}, ${attributeValue})`; + keyConditionAttributeValues.set( + attributeValue, + keyCondition.operation.substr + ); + } + }); + + return res; + }; +} diff --git a/src/queryCreator.ts b/src/queryCreator.ts index 144dfeb..06c0664 100644 --- a/src/queryCreator.ts +++ b/src/queryCreator.ts @@ -4,6 +4,7 @@ import { QueryQueryBuilder, QueryQueryBuilderInterface, } from "./queryBuilders/queryQueryBuilder"; +import { QueryCompiler } from "./queryCompiler"; export class QueryCreator { readonly #props: QueryCreatorProps; @@ -31,6 +32,7 @@ export class QueryCreator { }, }, ddbClient: this.#props.ddbClient, + queryCompiler: this.#props.queryCompiler, }); } @@ -51,10 +53,12 @@ export class QueryCreator { }, }, ddbClient: this.#props.ddbClient, + queryCompiler: this.#props.queryCompiler, }); } } export interface QueryCreatorProps { readonly ddbClient: DynamoDBDocumentClient; + readonly queryCompiler: QueryCompiler; } diff --git a/src/tsynamo.ts b/src/tsynamo.ts index 4bbc226..3bff07c 100644 --- a/src/tsynamo.ts +++ b/src/tsynamo.ts @@ -1,9 +1,15 @@ import { DynamoDBDocumentClient } from "@aws-sdk/lib-dynamodb"; import { QueryCreator } from "./queryCreator"; +import { QueryCompiler } from "./queryCompiler/queryCompiler"; export class Tsynamo extends QueryCreator { constructor(args: TsynamoProps) { - super(args); + const queryCompiler = new QueryCompiler(); + + super({ + ...args, + queryCompiler, + }); } }