import type { ExprResult, ValidPaths, ValidPathsWithCond } from './types.js'; import type { Get } from 'type-fest'; import { AttributeMapBuilder } from './attribute-map.js'; /** * A compiled update operation ready for use in DynamoDB requests. * * @typeParam T - The entity type this update operates on */ export type CompiledUpdateOperation = { type: 'update_operation'; exprResult: ExprResult; value?: T; }; /** * A SET operation that assigns a value to an attribute. * * @typeParam T - The entity type * @typeParam Key - The path to the attribute being set */ export type SetOperation = ValidPaths> = { type: 'update_set_value'; key: Key; value: | Get | (Get extends number ? AddOp : never) | IfNotExistsOp; }; /** * An arithmetic add operation for numeric attributes. * * @typeParam T - The entity type */ type AddOp = { type: 'update_primitive_add_op'; key: ValidPathsWithCond; value: number; }; /** * A conditional set operation that only sets if the attribute doesn't exist. * * @typeParam T - The entity type * @typeParam Key - The path to the attribute */ type IfNotExistsOp = ValidPaths> = { type: 'update_if_not_exists_op'; key: Key; value: Get; }; /** * An operation that appends values to a list attribute. * * @typeParam T - The entity type * @typeParam Path - The path to the list attribute */ type SetAppendOperation< T, Path extends ValidPathsWithCond = ValidPathsWithCond< T, readonly unknown[] >, > = { type: 'update_set_append'; key: Path; value: Get; }; /** * An operation that prepends values to a list attribute. * * @typeParam T - The entity type * @typeParam Path - The path to the list attribute */ type SetPrependOperation< T, Path extends ValidPathsWithCond = ValidPathsWithCond< T, readonly unknown[] >, > = { type: 'update_set_prepend'; key: Path; value: Get; }; /** * Union of all possible update operation types. */ export type AnyOperation = | SetOperation | SetAppendOperation | SetPrependOperation | UpdateOperation; /** * An array of update operations to be applied together. * * @typeParam T - The entity type this update operates on */ export type UpdateOperation = AnyOperation[]; /** * Operations available in the update builder callback. * * @typeParam T - The entity type the updates operate on */ export type UpdateOps = { /** Sets an attribute to a value, optionally with add or if_not_exists modifiers */ set: >( key: Key, value: | Get | (Get extends number ? AddOp : never) | IfNotExistsOp, ) => SetOperation; /** Creates an add operation for incrementing numeric attributes */ opAdd: (key: ValidPathsWithCond, value: number) => AddOp; /** Creates an if_not_exists operation for conditional setting */ opIfNotExists: >( key: Key, value: Get, ) => IfNotExistsOp; /** Appends values to the end of a list attribute */ append: >( key: Path, value: Get, ) => SetAppendOperation; /** Prepends values to the beginning of a list attribute */ prepend: >( key: Path, value: Get, ) => SetPrependOperation; }; function set = ValidPaths>( key: Key, value: | Get | (Get extends number ? AddOp : never) | IfNotExistsOp, ): SetOperation { return { type: 'update_set_value', key, value } as SetOperation; } /** * Creates an add operation for incrementing a numeric attribute. * * @typeParam T - The entity type * @param key - Path to the numeric attribute * @param value - Amount to add (can be negative for subtraction) * @returns An AddOp to use with set() */ export function opAdd( key: ValidPathsWithCond, value: number, ): AddOp { return { type: 'update_primitive_add_op', key, value }; } /** * Creates an if_not_exists operation for conditional attribute setting. * * @typeParam T - The entity type * @typeParam Key - The path to the attribute * @param key - Path to the attribute * @param value - Default value if attribute doesn't exist * @returns An IfNotExistsOp to use with set() */ export function opIfNotExists>( key: Key, value: Get, ): IfNotExistsOp { return { type: 'update_if_not_exists_op', key, value }; } function append< T, Path extends ValidPathsWithCond = ValidPathsWithCond< T, readonly unknown[] >, >(key: Path, value: Get): SetAppendOperation { return { type: 'update_set_append', key, value }; } function prepend< T, Path extends ValidPathsWithCond = ValidPathsWithCond< T, readonly unknown[] >, >(key: Path, value: Get): SetPrependOperation { return { type: 'update_set_prepend', key, value }; } /** * Creates an update expression using a type-safe builder pattern. * * @typeParam T - The entity type the update operates on * @param builder - Callback that receives update operations and returns an array of operations * @returns An array of update operations ready for compilation * * @example * ```ts * const update = exprUpdate(({ set, opAdd }) => [ * set("name", "John"), * set("loginCount", opAdd("loginCount", 1)) * ]); * ``` */ export function exprUpdate( builder: (ops: UpdateOps) => AnyOperation[], ): UpdateOperation { return builder({ set: (key, value) => set(key, value), opAdd: (key: ValidPathsWithCond, value: number) => opAdd(key, value), opIfNotExists: (key, value) => opIfNotExists(key, value), append: (key, value) => append(key, value), prepend: (key, value) => prepend(key, value), }); } /** * Flattens nested operation arrays into a single array. */ function flattenOperations( ops: AnyOperation[], ): (SetOperation | SetAppendOperation | SetPrependOperation)[] { const result: ( | SetOperation | SetAppendOperation | SetPrependOperation )[] = []; for (const op of ops) { if (Array.isArray(op)) { result.push(...flattenOperations(op)); } else { result.push(op); } } return result; } /** * Compiles an update operation into a DynamoDB-ready expression. * * @typeParam T - The entity type the update operates on * @param operations - The update operations to compile * @returns A compiled update with expression string and attribute maps */ export function compileUpdateExpr( operations: UpdateOperation, ): CompiledUpdateOperation { const flatOps = flattenOperations(operations); const attrBuilder = new AttributeMapBuilder('u_'); const setOps = flatOps .map((op) => { switch (op.type) { case 'update_set_value': if (op.value && typeof op.value === 'object' && 'type' in op.value) { if (op.value.type === 'update_primitive_add_op') { return `${attrBuilder.attr(op.key)} = ${attrBuilder.attr(op.value.key)} + ${attrBuilder.value(op.value.value)}`; } else if (op.value.type === 'update_if_not_exists_op') { return `${attrBuilder.attr(op.key)} = if_not_exists(${attrBuilder.attr(op.value.key)}, ${attrBuilder.value(op.value.value)})`; } } return `${attrBuilder.attr(op.key)} = ${attrBuilder.value(op.value)}`; case 'update_set_append': { const aKey = attrBuilder.attr(op.key); return `${aKey} = list_append(${aKey}, ${attrBuilder.value(op.value)})`; } case 'update_set_prepend': { const pKey = attrBuilder.attr(op.key); return `${pKey} = list_append(${attrBuilder.value(op.value)}, ${pKey})`; } } }) .join(', '); return { type: 'update_operation', exprResult: { expr: `SET ${setOps}`, attrResult: attrBuilder.build(), }, }; }