diff --git a/lib/condition/index.ts b/lib/condition/index.ts index ccabc77..054f887 100644 --- a/lib/condition/index.ts +++ b/lib/condition/index.ts @@ -4,16 +4,87 @@ import { transformValue } from '@lib/entity/helpers/transformValues'; import { EntityKey, EntityValue } from '@lib/entity/types'; import { BASE_OPERATOR, OPERATORS, Operators, ValidationError } from '@lib/utils'; +/** + * Condition builder for DynamoDB condition expressions. + * + * This class provides a fluent interface for building complex condition expressions + * that can be used in DynamoDB operations like PutItem, UpdateItem, and DeleteItem. + * It supports all DynamoDB condition operators and functions with type safety. + * + * @template E - The entity class type + * + * @example + * ```typescript + * // Basic conditions + * const condition = new Condition(User) + * .attribute('status').eq('active') + * .attribute('age').gt(18); + * + * // Complex conditions with grouping + * const condition = new Condition(User) + * .attribute('status').eq('active') + * .parenthesis( + * new Condition(User) + * .attribute('age').gt(18) + * .or + * .attribute('role').eq('admin') + * ); + * + * // Using with entity manager + * await UserManager.update( + * { id: 'user-123' }, + * { status: 'inactive' }, + * { condition } + * ); + * ``` + * + * @see {@link https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Expressions.ConditionExpressions.html} for DynamoDB condition expressions + * @see {@link https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Expressions.OperatorsAndFunctions.html} for DynamoDB operators and functions + */ export default class Condition { + /** The entity class for type-safe attribute access */ protected entity: E; + /** The current logical operator (AND or OR) for chaining conditions */ protected logicalOperator: typeof BASE_OPERATOR.and | typeof BASE_OPERATOR.or = BASE_OPERATOR.and; + /** Array of operators that make up the condition expression */ protected operators: Operators; + /** + * Creates a new Condition instance for the specified entity. + * + * @param entity - The entity class to build conditions for + * + * @example + * ```typescript + * const condition = new Condition(User); + * ``` + */ constructor(entity: E) { this.entity = entity; this.operators = []; } + /** + * Starts building a condition for a specific attribute. + * + * This method returns an object with all available comparison operators + * and functions for the specified attribute, providing a fluent interface + * for building conditions. + * + * @template C - The condition instance type + * @template K - The attribute key type + * @param key - The attribute name to build a condition for + * @returns An object with comparison operators and functions + * + * @example + * ```typescript + * const condition = new Condition(User) + * .attribute('status').eq('active') + * .attribute('age').gt(18) + * .attribute('email').beginsWith('admin@') + * .attribute('tags').contains('premium'); + * ``` + */ public attribute>(this: C, key: K) { this.maybePushLogicalOperator(); @@ -137,6 +208,25 @@ export default class Condition { }; } + /** + * Wraps a condition in parentheses for grouping. + * + * @param condition - Optional condition to wrap in parentheses + * @returns The condition instance for chaining + * + * @example + * ```typescript + * const condition = new Condition(User) + * .attribute('status').eq('active') + * .and + * .parenthesis( + * new Condition(User) + * .attribute('age').gt(18) + * .or + * .attribute('role').eq('admin') + * ); + * ``` + */ public parenthesis(condition?: Condition): this { if (condition) { this.maybePushLogicalOperator(); @@ -145,10 +235,34 @@ export default class Condition { return this; } + /** + * Alias for parenthesis() method for grouping conditions. + * + * @param condition - Optional condition to wrap in parentheses + * @returns The condition instance for chaining + */ public group(condition?: Condition): this { return this.parenthesis(condition); } + /** + * Adds another condition to this condition. + * + * @param condition - The condition to add + * @returns The condition instance for chaining + * + * @example + * ```typescript + * const baseCondition = new Condition(User) + * .attribute('status').eq('active'); + * + * const finalCondition = baseCondition + * .condition( + * new Condition(User) + * .attribute('age').gt(18) + * ); + * ``` + */ public condition(condition?: Condition): this { if (condition) { this.maybePushLogicalOperator(); @@ -157,51 +271,108 @@ export default class Condition { return this; } + /** + * Sets the logical operator to AND for the next condition. + * + * @returns The condition instance for chaining + * + * @example + * ```typescript + * const condition = new Condition(User) + * .attribute('status').eq('active') + * .attribute('age').gt(18); + * ``` + */ public get and(): this { this.logicalOperator = BASE_OPERATOR.and; return this; } + /** + * Sets the logical operator to OR for the next condition. + * + * @returns The condition instance for chaining + * + * @example + * ```typescript + * const condition = new Condition(User) + * .attribute('status').eq('active') + * .or + * .attribute('role').eq('admin'); + * ``` + */ public get or(): this { this.logicalOperator = BASE_OPERATOR.or; return this; } + /** + * Adds an equality comparison operator. + * @protected + */ protected eq>(operators: Operators, key: K, value: EntityValue): this { operators.push(...OPERATORS.eq(key, transformValue(this.entity, key, value))); return this; } + /** + * Adds a not equal comparison operator. + * @protected + */ protected ne>(operators: Operators, key: K, value: EntityValue): this { operators.push(...OPERATORS.ne(key, transformValue(this.entity, key, value))); return this; } + /** + * Adds a less than comparison operator. + * @protected + */ protected lt>(operators: Operators, key: K, value: EntityValue): this { operators.push(...OPERATORS.lt(key, transformValue(this.entity, key, value))); return this; } + /** + * Adds a less than or equal comparison operator. + * @protected + */ protected le>(operators: Operators, key: K, value: EntityValue): this { operators.push(...OPERATORS.le(key, transformValue(this.entity, key, value))); return this; } + /** + * Adds a greater than comparison operator. + * @protected + */ protected gt>(operators: Operators, key: K, value: EntityValue): this { operators.push(...OPERATORS.gt(key, transformValue(this.entity, key, value))); return this; } + /** + * Adds a greater than or equal comparison operator. + * @protected + */ protected ge>(operators: Operators, key: K, value: EntityValue): this { operators.push(...OPERATORS.ge(key, transformValue(this.entity, key, value))); return this; } + /** + * Adds a begins_with function operator. + * @protected + */ protected beginsWith>(operators: Operators, key: K, value: EntityValue): this { operators.push(...OPERATORS.beginsWith(key, transformValue(this.entity, key, value))); return this; } + /** + * Adds a BETWEEN operator. + * @protected + */ protected between>( operators: Operators, key: K, @@ -214,6 +385,10 @@ export default class Condition { return this; } + /** + * Conditionally adds a logical operator between conditions. + * @protected + */ protected maybePushLogicalOperator(): void { if (this.operators.length > 0) { this.operators.push(BASE_OPERATOR.space, this.logicalOperator, BASE_OPERATOR.space); @@ -222,4 +397,9 @@ export default class Condition { } } +/** + * DynamoDB attribute types enumeration. + * + * @see {@link AttributeType} for the full enumeration + */ export { AttributeType }; diff --git a/lib/condition/types.ts b/lib/condition/types.ts index 36f6a0b..a36d7e7 100644 --- a/lib/condition/types.ts +++ b/lib/condition/types.ts @@ -1,12 +1,28 @@ +/** + * DynamoDB attribute types enumeration. + * + * These correspond to the attribute types used in DynamoDB operations + * and condition expressions. + */ export enum AttributeType { + /** String attribute type */ String = 'S', + /** String set attribute type */ StringSet = 'SS', + /** Number attribute type */ Number = 'N', + /** Number set attribute type */ NumberSet = 'NS', + /** Binary attribute type */ Binary = 'B', + /** Binary set attribute type */ BinarySet = 'BS', + /** Boolean attribute type */ Boolean = 'BOOL', + /** Null attribute type */ Null = 'NULL', + /** List attribute type */ List = 'L', + /** Map attribute type */ Map = 'M', } diff --git a/lib/decorators/index.ts b/lib/decorators/index.ts index f39bc2b..ce4b24b 100644 --- a/lib/decorators/index.ts +++ b/lib/decorators/index.ts @@ -16,50 +16,121 @@ import { stringSortKey, } from '@lib/decorators/helpers/primaryKey'; +/** + * Attribute decorators for defining entity properties. + * + * These decorators are used to define the structure and behavior of entity properties + * in Dynamode. They specify the data type, key roles, and index configurations. + * + * @example + * ```typescript + * class User extends Entity { + * @attribute.partitionKey.string() + * id: string; + * + * @attribute.sortKey.number() + * createdAt: number; + * + * @attribute.string() + * name: string; + * + * @attribute.gsi.partitionKey.string({ indexName: 'NameIndex' }) + * nameIndex: string; + * + * @attribute.lsi.sortKey.string({ indexName: 'StatusIndex' }) + * status: string; + * } + * ``` + * + * @see {@link https://blazejkustra.github.io/dynamode/docs/guide/entity/decorators} for more information + */ const attribute = { + /** String attribute decorators */ string, + /** Number attribute decorators */ number, + /** Boolean attribute decorators */ boolean, + /** Object attribute decorators */ object, + /** Array attribute decorators */ array, + /** Set attribute decorators */ set, + /** Map attribute decorators */ map, + /** Binary attribute decorators */ binary, + /** Date attribute decorators */ date: { + /** String-based date decorators */ string: stringDate, + /** Number-based date decorators */ number: numberDate, }, + /** Partition key decorators */ partitionKey: { + /** String partition key decorators */ string: stringPartitionKey, + /** Number partition key decorators */ number: numberPartitionKey, }, + /** Sort key decorators */ sortKey: { + /** String sort key decorators */ string: stringSortKey, + /** Number sort key decorators */ number: numberSortKey, }, + /** Global Secondary Index (GSI) decorators */ gsi: { + /** GSI partition key decorators */ partitionKey: { + /** String GSI partition key decorators */ string: stringGsiPartitionKey, + /** Number GSI partition key decorators */ number: numberGsiPartitionKey, }, + /** GSI sort key decorators */ sortKey: { + /** String GSI sort key decorators */ string: stringGsiSortKey, + /** Number GSI sort key decorators */ number: numberGsiSortKey, }, }, + /** Local Secondary Index (LSI) decorators */ lsi: { + /** LSI sort key decorators */ sortKey: { + /** String LSI sort key decorators */ string: stringLsiSortKey, + /** Number LSI sort key decorators */ number: numberLsiSortKey, }, }, + /** Prefix decorator for adding prefixes to attribute values */ prefix, + /** Suffix decorator for adding suffixes to attribute values */ suffix, }; +/** + * Entity decorators for defining entity-level configurations. + * + * @example + * ```typescript + * @entity.customName('CustomUser') + * class User extends Entity { + * @attribute.partitionKey.string() + * id: string; + * } + * ``` + */ const entity = { + /** Custom name decorator for entities */ customName, }; diff --git a/lib/decorators/types.ts b/lib/decorators/types.ts index 7733f39..67af543 100644 --- a/lib/decorators/types.ts +++ b/lib/decorators/types.ts @@ -1,8 +1,17 @@ +/** + * Options for prefix and suffix decorators. + */ export type PrefixSuffixOptions = { + /** Prefix to add to attribute values */ prefix?: string; + /** Suffix to add to attribute values */ suffix?: string; }; +/** + * Options for index decorators. + */ export type IndexDecoratorOptions = { + /** Name of the index */ indexName: string; }; diff --git a/lib/dynamode/index.ts b/lib/dynamode/index.ts index 6cbb024..732d75e 100644 --- a/lib/dynamode/index.ts +++ b/lib/dynamode/index.ts @@ -3,14 +3,67 @@ import DDB, { DDBType } from '@lib/dynamode/ddb'; import separator, { SeparatorType } from '@lib/dynamode/separator'; import DynamodeStorage from '@lib/dynamode/storage'; +/** + * Main Dynamode class that provides access to all core functionality. + * + * This class serves as the central hub for Dynamode operations, providing + * access to DynamoDB client, storage management, data conversion, and + * separator utilities. It follows a singleton pattern with a default instance. + * + * @example + * ```typescript + * import Dynamode from 'dynamode'; + * + * // Configure Dynamode + * Dynamode.configure({ + * region: 'us-east-1', + * endpoint: 'http://localhost:8000' // for local development + * }); + * + * // Access DynamoDB client + * const ddb = Dynamode.ddb.get(); + * + * // Access storage for entity management + * const entityClass = Dynamode.storage.getEntityClass('User'); + * + * // Use converter for data transformation + * const converted = Dynamode.converter.toDynamo({ id: '123', name: 'John' }); + * + * // Use separator for key operations + * const key = Dynamode.separator.join(['user', '123']); + * ``` + * + * @see {@link https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Introduction.html} for DynamoDB overview + * @see {@link https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/HowItWorks.html} for DynamoDB concepts + */ class Dynamode { + /** Default singleton instance of Dynamode */ static default: Dynamode = new Dynamode(); + /** DynamoDB client wrapper for AWS SDK operations */ public ddb: DDBType; + /** Storage manager for entity and table metadata */ public storage: DynamodeStorage; + /** Data converter for DynamoDB attribute value transformations */ public converter: typeof converter; + /** Separator utility for key operations and data separation */ public separator: SeparatorType; + /** + * Creates a new Dynamode instance. + * + * Initializes all core components including the DynamoDB client, + * storage manager, converter, and separator utilities. + * + * @example + * ```typescript + * // Create a new instance (usually not needed) + * const dynamode = new Dynamode(); + * + * // Use the default singleton instance (recommended) + * const dynamode = Dynamode.default; + * ``` + */ constructor() { this.ddb = DDB(); this.storage = new DynamodeStorage(); @@ -19,4 +72,10 @@ class Dynamode { } } +/** + * Default Dynamode instance. + * + * This is the singleton instance that should be used throughout + * the application for all Dynamode operations. + */ export default Dynamode.default; diff --git a/lib/dynamode/storage/types.ts b/lib/dynamode/storage/types.ts index 40e35b5..e458e74 100644 --- a/lib/dynamode/storage/types.ts +++ b/lib/dynamode/storage/types.ts @@ -1,9 +1,23 @@ import Entity from '@lib/entity'; import { Metadata } from '@lib/table/types'; +/** + * Storage and metadata types for Dynamode. + */ + +/** + * Attribute types that can be used for indexes. + */ export type IndexAttributeType = StringConstructor | NumberConstructor; + +/** + * Attribute types that can be used for timestamps. + */ export type TimestampAttributeType = StringConstructor | NumberConstructor; +/** + * All supported attribute types in Dynamode. + */ export type AttributeType = | StringConstructor | NumberConstructor @@ -14,64 +28,133 @@ export type AttributeType = | MapConstructor | Uint8ArrayConstructor; +/** + * Roles that attributes can have in Dynamode. + */ export type AttributeRole = 'partitionKey' | 'sortKey' | 'index' | 'date' | 'attribute' | 'dynamodeEntity'; + +/** + * Roles that attributes can have in secondary indexes. + */ export type AttributeIndexRole = 'gsiPartitionKey' | 'gsiSortKey' | 'lsiSortKey'; +/** + * Base attribute metadata structure. + */ type BaseAttributeMetadata = { + /** Name of the property */ propertyName: string; + /** Type of the attribute */ type: AttributeType; + /** Optional prefix for the attribute value */ prefix?: string; + /** Optional suffix for the attribute value */ suffix?: string; }; -export type IndexMetadata = { name: string; role: AttributeIndexRole }; +/** + * Metadata for secondary index attributes. + */ +export type IndexMetadata = { + /** Name of the index */ + name: string; + /** Role of the attribute in the index */ + role: AttributeIndexRole; +}; +/** + * Complete attribute metadata including role and indexes. + */ export type AttributeMetadata = BaseAttributeMetadata & { + /** Role of the attribute */ role: AttributeRole; + /** Associated indexes */ indexes?: IndexMetadata[]; }; +/** + * Metadata for attributes that are part of indexes. + */ export type AttributeIndexMetadata = BaseAttributeMetadata & { + /** Role is always 'index' for this type */ role: 'index'; + /** Required indexes for this attribute */ indexes: IndexMetadata[]; }; +/** + * Collection of attribute metadata for an entity. + */ export type AttributesMetadata = { [attributeName: string]: AttributeMetadata; }; +/** + * Complete entity metadata including table and attributes. + */ export type EntityMetadata = { + /** Name of the table */ tableName: string; + /** Entity class */ entity: typeof Entity; + /** Attributes metadata */ attributes: AttributesMetadata; }; +/** + * Collection of entity metadata. + */ export type EntitiesMetadata = { [entityName: string]: EntityMetadata; }; +/** + * Table metadata including entity and configuration. + */ export type TableMetadata = { + /** Table entity class */ tableEntity: typeof Entity; + /** Table configuration metadata */ metadata: Metadata; }; +/** + * Collection of table metadata. + */ export type TablesMetadata = { [tableName: string]: TableMetadata; }; -// helpers +/** + * Helper types for validation operations. + */ +/** + * Type for validating metadata attributes. + */ export type ValidateMetadataAttribute = { + /** Name of the entity */ entityName: string; + /** Name of the attribute */ name: string; + /** Attributes metadata */ attributes: AttributesMetadata; + /** Valid roles for the attribute */ validRoles: AttributeRole[]; + /** Optional index name */ indexName?: string; }; +/** + * Type for validating decorated attributes. + */ export type ValidateDecoratedAttribute = { + /** Name of the entity */ entityName: string; + /** Name of the attribute */ name: string; + /** Attribute metadata */ attribute: AttributeMetadata; + /** Table metadata */ metadata: Metadata; }; diff --git a/lib/entity/entityManager.ts b/lib/entity/entityManager.ts index 58bdc9e..fdd71de 100644 --- a/lib/entity/entityManager.ts +++ b/lib/entity/entityManager.ts @@ -57,34 +57,156 @@ import { } from '@lib/transactionWrite/types'; import { AttributeValues, ExpressionBuilder, fromDynamo, NotFoundError } from '@lib/utils'; +/** + * Creates an EntityManager instance for a specific entity and table. + * + * The EntityManager provides a comprehensive set of methods for performing + * CRUD operations, batch operations, and transactions on DynamoDB entities. + * It handles the conversion between entity instances and DynamoDB attribute values, + * manages conditions, and provides type-safe operations. + * + * @param entity - The entity class constructor + * @param tableName - The name of the DynamoDB table + * @returns An object containing all entity management methods + * + * @example + * ```typescript + * class User extends Entity { + * @attribute.partitionKey.string() + * id: string; + * + * @attribute.string() + * name: string; + * } + * + * const UserManager = EntityManager(User, 'users-table'); + * + * // Create a new user + * const user = await UserManager.put(new User({ id: '1', name: 'John' })); + * + * // Get a user + * const retrievedUser = await UserManager.get({ id: '1' }); + * + * // Update a user + * const updatedUser = await UserManager.update({ id: '1' }, { set: { name: 'Jane' } }); + * ``` + * + * @see {@link https://blazejkustra.github.io/dynamode/docs/guide/managers/entityManager} for more information + */ export default function EntityManager, E extends typeof Entity>(entity: E, tableName: string) { + /** + * Creates a new condition builder for this entity. + * + * @returns A new Condition instance for building conditional expressions + * + * @example + * ```typescript + * const condition = UserManager.condition() + * .attribute('name').eq('John') + * .and() + * .attribute('age').gt(18); + * + * await UserManager.update({ id: '1' }, { set: { status: 'active' } }, { condition }); + * ``` + */ function condition(): Condition { return new Condition(entity); } + /** + * Creates a new query builder for this entity. + * + * @returns A new Query instance for building query operations + * + * @example + * ```typescript + * const users = await UserManager.query() + * .partitionKey('id').eq('user-123') + * .sortKey('createdAt').gt(new Date('2023-01-01')) + * .run(); + * ``` + */ function query(): Query { return new Query(entity); } + /** + * Creates a new scan builder for this entity. + * + * @returns A new Scan instance for building scan operations + * + * ⚠️ Keep in mind that scan operations are expensive, slow, and against [best practices](https://dynobase.dev/dynamodb-best-practices/). + * + * @example + * ```typescript + * const allUsers = await UserManager.scan() + * .attribute('status').eq('active') + * .run(); + * ``` + */ function scan(): Scan { return new Scan(entity); } + /** + * Retrieves a single item from the table by its primary key. + * + * @param primaryKey - The primary key of the item to retrieve + * @param options - Optional configuration for the get operation + * @returns A promise that resolves to the entity instance + * @throws {NotFoundError} When the item is not found + * + * @example + * ```typescript + * // Get a user by ID + * const user = await UserManager.get({ id: 'user-123' }); + * + * // Get with consistent read + * const user = await UserManager.get({ id: 'user-123' }, { consistent: true }); + * + * // Get only specific attributes + * const user = await UserManager.get({ id: 'user-123' }, { + * attributes: ['id', 'name'] + * }); + * ``` + */ function get( primaryKey: TablePrimaryKey, options?: EntityGetOptions & { return?: 'default' }, ): Promise>; + /** + * Retrieves a single item from the table by its primary key, returning the raw AWS response. + * + * @param primaryKey - The primary key of the item to retrieve + * @param options - Configuration for the get operation with return type 'output' + * @returns A promise that resolves to the raw GetItemCommandOutput + */ function get( primaryKey: TablePrimaryKey, options: EntityGetOptions & { return: 'output' }, ): Promise; + /** + * Builds the GetItem command input without executing it. + * + * @param primaryKey - The primary key of the item to retrieve + * @param options - Configuration for the get operation with return type 'input' + * @returns The GetItemCommandInput object + */ function get( primaryKey: TablePrimaryKey, options: EntityGetOptions & { return: 'input' }, ): GetItemCommandInput; + /** + * Retrieves a single item from the table by its primary key. + * + * @param primaryKey - The primary key of the item to retrieve + * @param options - Optional configuration for the get operation + * @returns A promise that resolves to the entity instance, raw AWS response, or command input + * @throws {NotFoundError} When the item is not found and return type is 'default' + */ function get( primaryKey: TablePrimaryKey, options?: EntityGetOptions, @@ -119,24 +241,79 @@ export default function EntityManager, E extends typeof En })(); } + /** + * Updates an item in the table by its primary key. + * + * @param primaryKey - The primary key of the item to update + * @param props - The update operations to perform + * @param options - Optional configuration for the update operation + * @returns A promise that resolves to the updated entity instance + * + * @example + * ```typescript + * // Update with SET operation + * const updatedUser = await UserManager.update( + * { id: 'user-123' }, + * { set: { name: 'Jane', age: 25 } } + * ); + * + * // Update with ADD operation (for numbers and sets) + * const updatedUser = await UserManager.update( + * { id: 'user-123' }, + * { add: { score: 10 } } + * ); + * + * // Update with conditional expression + * const condition = UserManager.condition().attribute('version').eq(1); + * const updatedUser = await UserManager.update( + * { id: 'user-123' }, + * { set: { name: 'Jane' } }, + * { condition } + * ); + * ``` + */ function update( primaryKey: TablePrimaryKey, props: UpdateProps, options?: EntityUpdateOptions & { return?: 'default' }, ): Promise>; + /** + * Updates an item in the table, returning the raw AWS response. + * + * @param primaryKey - The primary key of the item to update + * @param props - The update operations to perform + * @param options - Configuration for the update operation with return type 'output' + * @returns A promise that resolves to the raw UpdateItemCommandOutput + */ function update( primaryKey: TablePrimaryKey, props: UpdateProps, options: EntityUpdateOptions & { return: 'output' }, ): Promise; + /** + * Builds the UpdateItem command input without executing it. + * + * @param primaryKey - The primary key of the item to update + * @param props - The update operations to perform + * @param options - Configuration for the update operation with return type 'input' + * @returns The UpdateItemCommandInput object + */ function update( primaryKey: TablePrimaryKey, props: UpdateProps, options: EntityUpdateOptions & { return: 'input' }, ): UpdateItemCommandInput; + /** + * Updates an item in the table by its primary key. + * + * @param primaryKey - The primary key of the item to update + * @param props - The update operations to perform (set, add, remove, etc.) + * @param options - Optional configuration for the update operation + * @returns A promise that resolves to the updated entity instance, raw AWS response, or command input + */ function update( primaryKey: TablePrimaryKey, props: UpdateProps, @@ -174,14 +351,56 @@ export default function EntityManager, E extends typeof En })(); } + /** + * Puts (creates or overwrites) an item in the table. + * + * @param item - The entity instance to put + * @param options - Optional configuration for the put operation + * @returns A promise that resolves to the entity instance + * + * @example + * ```typescript + * const user = new User({ id: 'user-123', name: 'John', age: 30 }); + * const savedUser = await UserManager.put(user); + * + * // Put with conditional expression + * const condition = UserManager.condition().attribute('id').not().exists(); + * const savedUser = await UserManager.put(user, { condition }); + * + * // Put without overwriting existing items + * const savedUser = await UserManager.put(user, { overwrite: false }); + * ``` + */ function put(item: InstanceType, options?: EntityPutOptions & { return?: 'default' }): Promise>; + /** + * Puts an item in the table, returning the raw AWS response. + * + * @param item - The entity instance to put + * @param options - Configuration for the put operation with return type 'output' + * @returns A promise that resolves to the raw PutItemCommandOutput + */ function put( item: InstanceType, options: EntityPutOptions & { return: 'output' }, ): Promise; + + /** + * Builds the PutItem command input without executing it. + * + * @param item - The entity instance to put + * @param options - Configuration for the put operation with return type 'input' + * @returns The PutItemCommandInput object + */ function put(item: InstanceType, options: EntityPutOptions & { return: 'input' }): PutItemCommandInput; + /** + * Puts (creates or overwrites) an item in the table. + * + * @param item - The entity instance to put + * @param options - Optional configuration for the put operation + * @returns A promise that resolves to the entity instance, raw AWS response, or command input + */ function put( item: InstanceType, options?: EntityPutOptions, @@ -220,17 +439,59 @@ export default function EntityManager, E extends typeof En })(); } + /** + * Creates a new item in the table (fails if item already exists). + * + * This is equivalent to calling `put()` with `overwrite: false`. + * + * @param item - The entity instance to create + * @param options - Optional configuration for the create operation + * @returns A promise that resolves to the entity instance + * @throws {ConditionalCheckFailedException} When the item already exists + * + * @example + * ```typescript + * const user = new User({ id: 'user-123', name: 'John', age: 30 }); + * const createdUser = await UserManager.create(user); + * + * // Create with conditional expression + * const condition = UserManager.condition().attribute('status').eq('pending'); + * const createdUser = await UserManager.create(user, { condition }); + * ``` + */ function create( item: InstanceType, options?: EntityPutOptions & { return?: 'default' }, ): Promise>; + /** + * Creates a new item in the table, returning the raw AWS response. + * + * @param item - The entity instance to create + * @param options - Configuration for the create operation with return type 'output' + * @returns A promise that resolves to the raw PutItemCommandOutput + */ function create( item: InstanceType, options: EntityPutOptions & { return: 'output' }, ): Promise; + + /** + * Builds the PutItem command input for creating an item without executing it. + * + * @param item - The entity instance to create + * @param options - Configuration for the create operation with return type 'input' + * @returns The PutItemCommandInput object + */ function create(item: InstanceType, options: EntityPutOptions & { return: 'input' }): PutItemCommandInput; + /** + * Creates a new item in the table (fails if item already exists). + * + * @param item - The entity instance to create + * @param options - Optional configuration for the create operation + * @returns A promise that resolves to the entity instance, raw AWS response, or command input + */ function create( item: InstanceType, options?: EntityPutOptions, @@ -239,21 +500,65 @@ export default function EntityManager, E extends typeof En return put(item, { ...options, overwrite } as any); } + /** + * Deletes an item from the table by its primary key. + * + * @param primaryKey - The primary key of the item to delete + * @param options - Optional configuration for the delete operation + * @returns A promise that resolves to the deleted entity instance or null if not found + * + * @example + * ```typescript + * // Delete a user + * const deletedUser = await UserManager.delete({ id: 'user-123' }); + * + * // Delete with conditional expression + * const condition = UserManager.condition().attribute('status').eq('inactive'); + * const deletedUser = await UserManager.delete({ id: 'user-123' }, { condition }); + * + * // Delete and throw error if item doesn't exist + * const deletedUser = await UserManager.delete( + * { id: 'user-123' }, + * { throwErrorIfNotExists: true } + * ); + * ``` + */ function _delete( primaryKey: TablePrimaryKey, options?: EntityDeleteOptions & { return?: 'default' }, ): Promise | null>; + /** + * Deletes an item from the table, returning the raw AWS response. + * + * @param primaryKey - The primary key of the item to delete + * @param options - Configuration for the delete operation with return type 'output' + * @returns A promise that resolves to the raw DeleteItemCommandOutput + */ function _delete( primaryKey: TablePrimaryKey, options: EntityDeleteOptions & { return: 'output' }, ): Promise; + /** + * Builds the DeleteItem command input without executing it. + * + * @param primaryKey - The primary key of the item to delete + * @param options - Configuration for the delete operation with return type 'input' + * @returns The DeleteItemCommandInput object + */ function _delete( primaryKey: TablePrimaryKey, options: EntityDeleteOptions & { return: 'input' }, ): DeleteItemCommandInput; + /** + * Deletes an item from the table by its primary key. + * + * @param primaryKey - The primary key of the item to delete + * @param options - Optional configuration for the delete operation + * @returns A promise that resolves to the deleted entity instance, null, raw AWS response, or command input + */ function _delete( primaryKey: TablePrimaryKey, options?: EntityDeleteOptions, @@ -291,21 +596,70 @@ export default function EntityManager, E extends typeof En })(); } + /** + * Retrieves multiple items from the table by their primary keys. + * + * @param primaryKeys - Array of primary keys to retrieve + * @param options - Optional configuration for the batch get operation + * @returns A promise that resolves to an object containing retrieved items and unprocessed keys + * + * @example + * ```typescript + * // Get multiple users + * const result = await UserManager.batchGet([ + * { id: 'user-1' }, + * { id: 'user-2' }, + * { id: 'user-3' } + * ]); + * + * console.log(result.items); // Array of User instances + * console.log(result.unprocessedKeys); // Keys that couldn't be processed + * + * // Get with consistent read + * const result = await UserManager.batchGet(primaryKeys, { consistent: true }); + * + * // Get only specific attributes + * const result = await UserManager.batchGet(primaryKeys, { + * attributes: ['id', 'name'] + * }); + * ``` + */ function batchGet( primaryKeys: Array>, options?: EntityBatchGetOptions & { return?: 'default' }, ): Promise>; + /** + * Retrieves multiple items, returning the raw AWS response. + * + * @param primaryKeys - Array of primary keys to retrieve + * @param options - Configuration for the batch get operation with return type 'output' + * @returns A promise that resolves to the raw BatchGetItemCommandOutput + */ function batchGet( primaryKeys: Array>, options: EntityBatchGetOptions & { return: 'output' }, ): Promise; + /** + * Builds the BatchGetItem command input without executing it. + * + * @param primaryKeys - Array of primary keys to retrieve + * @param options - Configuration for the batch get operation with return type 'input' + * @returns The BatchGetItemCommandInput object + */ function batchGet( primaryKeys: Array>, options: EntityBatchGetOptions & { return: 'input' }, ): BatchGetItemCommandInput; + /** + * Retrieves multiple items from the table by their primary keys. + * + * @param primaryKeys - Array of primary keys to retrieve + * @param options - Optional configuration for the batch get operation + * @returns A promise that resolves to the batch get result, raw AWS response, or command input + */ function batchGet( primaryKeys: Array>, options?: EntityBatchGetOptions, @@ -353,21 +707,62 @@ export default function EntityManager, E extends typeof En })(); } + /** + * Puts multiple items in the table in a single batch operation. + * + * @param items - Array of entity instances to put + * @param options - Optional configuration for the batch put operation + * @returns A promise that resolves to an object containing processed items and unprocessed items + * + * @example + * ```typescript + * const users = [ + * new User({ id: 'user-1', name: 'John' }), + * new User({ id: 'user-2', name: 'Jane' }), + * new User({ id: 'user-3', name: 'Bob' }) + * ]; + * + * const result = await UserManager.batchPut(users); + * console.log(result.items); // Array of processed User instances + * console.log(result.unprocessedItems); // Items that couldn't be processed + * ``` + */ function batchPut( items: Array>, options?: EntityBatchPutOptions & { return?: 'default' }, ): Promise>; + /** + * Puts multiple items, returning the raw AWS response. + * + * @param items - Array of entity instances to put + * @param options - Configuration for the batch put operation with return type 'output' + * @returns A promise that resolves to the raw BatchWriteItemCommandOutput + */ function batchPut( items: Array>, options: EntityBatchPutOptions & { return: 'output' }, ): Promise; + /** + * Builds the BatchWriteItem command input for putting items without executing it. + * + * @param items - Array of entity instances to put + * @param options - Configuration for the batch put operation with return type 'input' + * @returns The BatchWriteItemCommandInput object + */ function batchPut( items: Array>, options: EntityBatchPutOptions & { return: 'input' }, ): BatchWriteItemCommandInput; + /** + * Puts multiple items in the table in a single batch operation. + * + * @param items - Array of entity instances to put + * @param options - Optional configuration for the batch put operation + * @returns A promise that resolves to the batch put result, raw AWS response, or command input + */ function batchPut( items: Array>, options?: EntityBatchPutOptions, @@ -415,21 +810,61 @@ export default function EntityManager, E extends typeof En })(); } + /** + * Deletes multiple items from the table by their primary keys in a single batch operation. + * + * @param primaryKeys - Array of primary keys to delete + * @param options - Optional configuration for the batch delete operation + * @returns A promise that resolves to an object containing unprocessed items + * + * @example + * ```typescript + * const primaryKeys = [ + * { id: 'user-1' }, + * { id: 'user-2' }, + * { id: 'user-3' } + * ]; + * + * const result = await UserManager.batchDelete(primaryKeys); + * console.log(result.unprocessedItems); // Keys that couldn't be processed + * ``` + */ function batchDelete( primaryKeys: Array>, options?: EntityBatchDeleteOptions & { return?: 'default' }, ): Promise>>; + /** + * Deletes multiple items, returning the raw AWS response. + * + * @param primaryKeys - Array of primary keys to delete + * @param options - Configuration for the batch delete operation with return type 'output' + * @returns A promise that resolves to the raw BatchWriteItemCommandOutput + */ function batchDelete( primaryKeys: Array>, options: EntityBatchDeleteOptions & { return: 'output' }, ): Promise; + /** + * Builds the BatchWriteItem command input for deleting items without executing it. + * + * @param primaryKeys - Array of primary keys to delete + * @param options - Configuration for the batch delete operation with return type 'input' + * @returns The BatchWriteItemCommandInput object + */ function batchDelete( primaryKeys: Array>, options: EntityBatchDeleteOptions & { return: 'input' }, ): BatchWriteItemCommandInput; + /** + * Deletes multiple items from the table by their primary keys in a single batch operation. + * + * @param primaryKeys - Array of primary keys to delete + * @param options - Optional configuration for the batch delete operation + * @returns A promise that resolves to the batch delete result, raw AWS response, or command input + */ function batchDelete( primaryKeys: Array>, options?: EntityBatchDeleteOptions, @@ -474,6 +909,31 @@ export default function EntityManager, E extends typeof En })(); } + /** + * Creates a transaction get operation for retrieving an item. + * + * This method creates a transaction get operation that can be used with + * Dynamode's transaction system. The operation is not executed immediately + * but must be passed to a transaction manager. + * + * @param primaryKey - The primary key of the item to retrieve + * @param options - Optional configuration for the transaction get operation + * @returns A TransactionGet object that can be used in a transaction + * + * @example + * ```typescript + * const getOperation = UserManager.transactionGet({ id: 'user-123' }); + * + * // Use in a transaction + * const result = await Dynamode.transaction.write([ + * getOperation, + * UserManager.transaction.put(newUser), + * UserManager.transaction.delete({ id: 'old-user' }) + * ]); + * ``` + * + * You can read more about transactions [here](https://blazejkustra.github.io/dynamode/docs/guide/transactions). + */ function transactionGet( primaryKey: TablePrimaryKey, options?: EntityTransactionGetOptions>, @@ -494,6 +954,24 @@ export default function EntityManager, E extends typeof En return commandInput; } + /** + * Creates a transaction update operation for updating an item. + * + * @param primaryKey - The primary key of the item to update + * @param props - The update operations to perform + * @param options - Optional configuration for the transaction update operation + * @returns A TransactionUpdate object that can be used in a transaction + * + * @example + * ```typescript + * const updateOperation = UserManager.transactionUpdate( + * { id: 'user-123' }, + * { set: { name: 'Jane', status: 'active' } } + * ); + * ``` + * + * You can read more about transactions [here](https://blazejkustra.github.io/dynamode/docs/guide/transactions). + */ function transactionUpdate( primaryKey: TablePrimaryKey, props: UpdateProps, @@ -522,6 +1000,20 @@ export default function EntityManager, E extends typeof En return commandInput; } + /** + * Creates a transaction put operation for putting an item. + * + * @param item - The entity instance to put + * @param options - Optional configuration for the transaction put operation + * @returns A TransactionPut object that can be used in a transaction + * + * @example + * ```typescript + * const putOperation = UserManager.transactionPut(new User({ id: 'user-123', name: 'John' })); + * ``` + * + * You can read more about transactions [here](https://blazejkustra.github.io/dynamode/docs/guide/transactions). + */ function transactionPut(item: InstanceType, options?: EntityTransactionPutOptions): TransactionPut { const overwrite = options?.overwrite ?? true; const partitionKey = Dynamode.storage.getEntityMetadata(entity.name).partitionKey as EntityKey; @@ -547,11 +1039,41 @@ export default function EntityManager, E extends typeof En return commandInput; } + /** + * Creates a transaction create operation for creating an item (fails if item already exists). + * + * This is equivalent to calling `transactionPut()` with `overwrite: false`. + * + * @param item - The entity instance to create + * @param options - Optional configuration for the transaction create operation + * @returns A TransactionPut object that can be used in a transaction + * + * @example + * ```typescript + * const createOperation = UserManager.transactionCreate(new User({ id: 'user-123', name: 'John' })); + * ``` + * + * You can read more about transactions [here](https://blazejkustra.github.io/dynamode/docs/guide/transactions). + */ function transactionCreate(item: InstanceType, options?: EntityTransactionPutOptions): TransactionPut { const overwrite = options?.overwrite ?? false; return transactionPut(item, { ...options, overwrite }); } + /** + * Creates a transaction delete operation for deleting an item. + * + * @param primaryKey - The primary key of the item to delete + * @param options - Optional configuration for the transaction delete operation + * @returns A TransactionDelete object that can be used in a transaction + * + * @example + * ```typescript + * const deleteOperation = UserManager.transactionDelete({ id: 'user-123' }); + * ``` + * + * You can read more about transactions [here](https://blazejkustra.github.io/dynamode/docs/guide/transactions). + */ function transactionDelete( primaryKey: TablePrimaryKey, options?: EntityTransactionDeleteOptions, @@ -573,6 +1095,21 @@ export default function EntityManager, E extends typeof En return commandInput; } + /** + * Creates a transaction condition check operation. + * + * @param primaryKey - The primary key of the item to check + * @param conditionInstance - The condition to check + * @returns A TransactionCondition object that can be used in a transaction + * + * @example + * ```typescript + * const condition = UserManager.condition().attribute('status').eq('active'); + * const conditionCheck = UserManager.transactionCondition({ id: 'user-123' }, condition); + * ``` + * + * You can read more about transactions [here](https://blazejkustra.github.io/dynamode/docs/guide/transactions). + */ function transactionCondition( primaryKey: TablePrimaryKey, conditionInstance: Condition, @@ -595,26 +1132,44 @@ export default function EntityManager, E extends typeof En } return { + /** Query builder for this entity */ query, + /** Scan builder for this entity */ scan, + /** Condition builder for this entity */ condition, + /** Get a single item by primary key */ get, + /** Update an item by primary key */ update, + /** Put (create or overwrite) an item */ put, + /** Create a new item (fails if exists) */ create, + /** Delete an item by primary key */ delete: _delete, + /** Get multiple items by primary keys */ batchGet, + /** Put multiple items */ batchPut, + /** Delete multiple items by primary keys */ batchDelete, + /** Transaction operations */ transaction: { + /** Create a transaction get operation */ get: transactionGet, + /** Create a transaction update operation */ update: transactionUpdate, + /** Create a transaction put operation */ put: transactionPut, + /** Create a transaction create operation */ create: transactionCreate, + /** Create a transaction delete operation */ delete: transactionDelete, + /** Create a transaction condition check operation */ condition: transactionCondition, }, }; diff --git a/lib/entity/index.ts b/lib/entity/index.ts index 01bdefb..6160ead 100644 --- a/lib/entity/index.ts +++ b/lib/entity/index.ts @@ -1,9 +1,70 @@ import Dynamode from '@lib/dynamode/index'; import { DYNAMODE_ENTITY } from '@lib/utils'; +/** + * Base class for all Dynamode entities. + * + * This class serves as the foundation for all entity classes in Dynamode. + * It automatically registers the entity with the Dynamode storage system + * and provides a unique identifier for each entity instance. + * + * @example + * ```typescript + * class User extends Entity { + * @attribute.partitionKey.string() + * id: string; + * + * @attribute.string() + * name: string; + * + * constructor(props: { id: string; name: string }) { + * super(); + * this.id = props.id; + * this.name = props.name; + * } + * } + * ``` + * + * @see {@link https://blazejkustra.github.io/dynamode/docs/getting_started/introduction} for more information + * @see {@link https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/bp-general-nosql-design.html} for DynamoDB data modeling best practices + * @see {@link https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/bp-modeling-nosql.html} for NoSQL data modeling concepts + * @see {@link https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/HowItWorks.CoreComponents.html} for DynamoDB core components + */ export default class Entity { + /** + * Unique identifier for the entity type. + * This is automatically set to the constructor's name. + * + * @readonly + * @example + * ```typescript + * const user = new User({ id: '1', name: 'John' }); + * console.log(user.dynamodeEntity); // "User" + * + * You can also use the `@entity.customName` decorator to change the name of the entity. + * @entity.customName('CustomName') + * ``` + */ public readonly dynamodeEntity!: string; + /** + * Creates a new Entity instance. + * + * @param args - Variable arguments (currently unused but reserved for future use) + * + * @example + * ```typescript + * class Product extends Entity { + * @attribute.partitionKey.string() + * id: string; + * + * constructor(props: { id: string }) { + * super(); // Always call super() first + * this.id = props.id; + * } + * } + * ``` + */ // eslint-disable-next-line @typescript-eslint/no-unused-vars, unused-imports/no-unused-vars constructor(...args: unknown[]) { this.dynamodeEntity = this.constructor.name; diff --git a/lib/entity/types.ts b/lib/entity/types.ts index e29bc3c..aeaa88a 100644 --- a/lib/entity/types.ts +++ b/lib/entity/types.ts @@ -18,36 +18,83 @@ import Entity from '@lib/entity'; import { Metadata, TablePrimaryKey } from '@lib/table/types'; import { AttributeNames, AttributeValues, FlattenObject } from '@lib/utils'; -// Return types - +/** + * Return type options for DynamoDB operations. + */ export type ReturnOption = 'default' | 'input' | 'output'; + +/** + * Return values options for DynamoDB operations. + */ export type ReturnValues = 'none' | 'allOld' | 'allNew' | 'updatedOld' | 'updatedNew'; + +/** + * Limited return values options for DynamoDB operations. + */ export type ReturnValuesLimited = 'none' | 'allOld'; -// Entity Props +/** + * Entity property types and utilities. + */ +/** + * Properties of an entity instance. + * + * @template E - The entity class type + */ export type EntityProperties = Partial>>; + +/** + * Key names of an entity. + * + * @template E - The entity class type + */ export type EntityKey = keyof EntityProperties extends string ? keyof EntityProperties : never; -export type EntityValue> = FlattenObject>[K]; -// entityManager.get +/** + * Value type for a specific entity key. + * + * @template E - The entity class type + * @template K - The key type + */ +export type EntityValue> = FlattenObject>[K]; +/** + * Options for entity get operations. + * + * @template E - The entity class type + */ export type EntityGetOptions = { + /** Additional DynamoDB input parameters */ extraInput?: Partial; + /** Return type option */ return?: ReturnOption; + /** Specific attributes to retrieve */ attributes?: Array>; + /** Whether to use consistent read */ consistent?: boolean; }; +/** + * Built projection expression for get operations. + */ export type BuildGetProjectionExpression = { + /** Attribute names mapping */ attributeNames?: AttributeNames; + /** Projection expression string */ projectionExpression?: string; }; // entityManager.update +/** + * Utility type to pick properties of a specific type. + * + * @template T - The object type + * @template Value - The value type to filter by + */ export type PickByType = Omit< { [P in keyof T as T[P] extends Value | undefined ? P : never]: T[P]; @@ -59,14 +106,27 @@ export type PickByType = Omit< : U | Record : never; +/** + * Update operations for entity update operations. + * + * @template E - The entity class type + */ export type UpdateProps = RequireAtLeastOne<{ + /** ADD operation for numbers and sets */ add?: PickByType, number | Set>; + /** SET operation for setting attribute values */ set?: EntityProperties; + /** SET operation only if attribute doesn't exist */ setIfNotExists?: EntityProperties; + /** LIST_APPEND operation for arrays */ listAppend?: PickByType, Array>; + /** Increment operation for numbers */ increment?: PickByType, number>; + /** Decrement operation for numbers */ decrement?: PickByType, number>; + /** DELETE operation for sets */ delete?: PickByType, Set>; + /** REMOVE operation for removing attributes */ remove?: Array>; }>; diff --git a/lib/index.ts b/lib/index.ts index 80f26cc..31e7f97 100644 --- a/lib/index.ts +++ b/lib/index.ts @@ -1,3 +1,46 @@ +/** + * @fileoverview Main entry point for the Dynamode library. + * + * Dynamode is a modeling tool for Amazon's DynamoDB that provides a straightforward, + * object-oriented class-based solution to model your data. It includes strongly typed + * classes and methods, query and scan builders, and much more. + * + * @example + * ```typescript + * import Dynamode, { Entity, TableManager, attribute } from 'dynamode'; + * + * // Configure Dynamode + * Dynamode.configure({ + * region: 'us-east-1', + * endpoint: 'http://localhost:8000' // for local development + * }); + * + * // Define an entity + * class User extends Entity { + * @attribute.partitionKey.string() + * id: string; + * + * @attribute.string() + * name: string; + * } + * + * // Create table manager + * const UserTableManager = new TableManager(User, { + * tableName: 'users-table', + * partitionKey: 'id' + * }); + * + * // Create table and get entity manager + * await UserTableManager.createTable(); + * const UserManager = UserTableManager.entityManager(); + * + * // Use the entity manager + * const user = await UserManager.put(new User({ id: '1', name: 'John' })); + * ``` + * + * @see {@link https://blazejkustra.github.io/dynamode/docs/getting_started/introduction} for more information + */ + import * as dynamode from '@lib/module'; export default dynamode; diff --git a/lib/module.ts b/lib/module.ts index abab036..b896cad 100644 --- a/lib/module.ts +++ b/lib/module.ts @@ -19,34 +19,61 @@ declare global { interface File {} } +/** + * Main Dynamode module exports. + * + * This module provides all the core functionality of Dynamode, including + * entity management, table operations, querying, scanning, and transactions. + * + * @example + * ```typescript + * import { Entity, TableManager, attribute } from 'dynamode'; + * + * class User extends Entity { + * @attribute.partitionKey.string() + * id: string; + * + * @attribute.string() + * name: string; + * } + * + * const UserTableManager = new TableManager(User, { + * tableName: 'users-table', + * partitionKey: 'id' + * }); + * ``` + */ export { - //Condition + /** Condition builder for building conditional expressions */ Condition, + /** Attribute type definitions */ AttributeType, - //decorators + /** Attribute decorators for defining entity properties */ attribute, + /** Entity decorators for entity-level configurations */ entity, - //Entity + /** Base Entity class for all Dynamode entities */ Entity, - //table manager + /** Table manager for managing DynamoDB tables */ TableManager, - //Query + /** Query builder for DynamoDB query operations */ Query, - //Scan + /** Scan builder for DynamoDB scan operations */ Scan, - //Dynamode + /** Main Dynamode instance for configuration and global operations */ Dynamode, - //transactions + /** Transaction get operations */ transactionGet, + /** Transaction write operations */ transactionWrite, - //Stream + /** Stream operations for DynamoDB streams */ Stream, }; diff --git a/lib/query/index.ts b/lib/query/index.ts index 9631d07..214affe 100644 --- a/lib/query/index.ts +++ b/lib/query/index.ts @@ -17,19 +17,111 @@ import { ValidationError, } from '@lib/utils'; +/** + * Query builder for DynamoDB query operations. + * + * The Query class provides a fluent interface for building and executing + * DynamoDB query operations. It supports querying by partition key and + * sort key with various comparison operators, and can work with both + * primary keys and secondary indexes. + * + * @example + * ```typescript + * // Query by partition key only + * const users = await UserManager.query() + * .partitionKey('id').eq('user-123') + * .run(); + * + * // Query by partition key and sort key + * const users = await UserManager.query() + * .partitionKey('id').eq('user-123') + * .sortKey('createdAt').gt(new Date('2023-01-01')) + * .run(); + * + * // Query with filter expression + * const users = await UserManager.query() + * .partitionKey('id').eq('user-123') + * .attribute('status').eq('active') + * .run(); + * + * // Query with index + * const users = await UserManager.query() + * .indexName('StatusIndex') + * .partitionKey('status').eq('active') + * .sortKey('createdAt').between(startDate, endDate) + * .run(); + * ``` + * + * @see {@link https://blazejkustra.github.io/dynamode/docs/guide/query} for more information + * @see {@link https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Query.html} for DynamoDB Query operations + */ export default class Query, E extends typeof Entity> extends RetrieverBase { + /** The DynamoDB QueryInput object */ protected declare input: QueryInput; + /** Key condition operators for building key condition expressions */ protected keyOperators: Operators = []; + /** Metadata for the partition key attribute */ protected partitionKeyMetadata?: AttributeMetadata; + /** Metadata for the sort key attribute */ protected sortKeyMetadata?: AttributeMetadata; + /** + * Creates a new Query instance. + * + * @param entity - The entity class to query + */ constructor(entity: E) { super(entity); } + /** + * Executes the query operation. + * + * @param options - Optional configuration for the query execution + * @returns A promise that resolves to the query results + * + * @example + * ```typescript + * // Execute query and get all results + * const result = await UserManager.query() + * .partitionKey('id').eq('user-123') + * .run(); + * + * // Execute query with pagination + * const result = await UserManager.query() + * .partitionKey('id').eq('user-123') + * .run({ all: false, max: 10 }); + * + * // Execute query with delay between pages + * const result = await UserManager.query() + * .partitionKey('id').eq('user-123') + * .run({ all: true, delay: 100 }); + * ``` + */ public run(options?: QueryRunOptions & { return?: 'default' }): Promise>; + + /** + * Executes the query operation, returning the raw AWS response. + * + * @param options - Configuration for the query execution with return type 'output' + * @returns A promise that resolves to the raw QueryCommandOutput + */ public run(options: QueryRunOptions & { return: 'output' }): Promise; + + /** + * Builds the Query command input without executing it. + * + * @param options - Configuration for the query execution with return type 'input' + * @returns The QueryInput object + */ public run(options: QueryRunOptions & { return: 'input' }): QueryInput; + + /** + * Executes the query operation. + * + * @param options - Optional configuration for the query execution + * @returns A promise that resolves to the query results, raw AWS response, or command input + */ public run(options?: QueryRunOptions): Promise | QueryCommandOutput> | QueryInput { this.setAssociatedIndexName(); this.buildQueryInput(options?.extraInput); @@ -76,34 +168,101 @@ export default class Query, E extends typeof Entity> exten })(); } + /** + * Specifies the partition key for the query. + * + * @template Q - The query instance type + * @template K - The partition key type + * @param key - The partition key attribute name + * @returns An object with comparison operators for the partition key + * + * @example + * ```typescript + * const users = await UserManager.query() + * .partitionKey('id').eq('user-123') + * .run(); + * ``` + */ public partitionKey, K extends EntityKey & TablePartitionKeys>(this: Q, key: K) { this.maybePushKeyLogicalOperator(); const attributes = Dynamode.storage.getEntityAttributes(this.entity.name); this.partitionKeyMetadata = attributes[key as string]; return { + /** Equal comparison operator */ eq: (value: EntityValue): Q => this.eq(this.keyOperators, key, value), }; } + /** + * Specifies the sort key for the query. + * + * @template Q - The query instance type + * @template K - The sort key type + * @param key - The sort key attribute name + * @returns An object with comparison operators for the sort key + * + * @example + * ```typescript + * const users = await UserManager.query() + * .partitionKey('id').eq('user-123') + * .sortKey('createdAt').gt(new Date('2023-01-01')) + * .run(); + * + * // Using between operator + * const users = await UserManager.query() + * .partitionKey('id').eq('user-123') + * .sortKey('createdAt').between(startDate, endDate) + * .run(); + * ``` + */ public sortKey, K extends EntityKey & TableSortKeys>(this: Q, key: K) { this.maybePushKeyLogicalOperator(); const attributes = Dynamode.storage.getEntityAttributes(this.entity.name); this.sortKeyMetadata = attributes[key as string]; return { + /** Equal comparison operator */ eq: (value: EntityValue): Q => this.eq(this.keyOperators, key, value), + /** Not equal comparison operator */ ne: (value: EntityValue): Q => this.ne(this.keyOperators, key, value), + /** Less than comparison operator */ lt: (value: EntityValue): Q => this.lt(this.keyOperators, key, value), + /** Less than or equal comparison operator */ le: (value: EntityValue): Q => this.le(this.keyOperators, key, value), + /** Greater than comparison operator */ gt: (value: EntityValue): Q => this.gt(this.keyOperators, key, value), + /** Greater than or equal comparison operator */ ge: (value: EntityValue): Q => this.ge(this.keyOperators, key, value), + /** Begins with comparison operator (for string values) */ beginsWith: (value: EntityValue): Q => this.beginsWith(this.keyOperators, key, value), + /** Between comparison operator */ between: (value1: EntityValue, value2: EntityValue): Q => this.between(this.keyOperators, key, value1, value2), }; } + /** + * Sets the sort order for the query results. + * + * @param order - The sort order ('ascending' or 'descending') + * @returns The query instance for method chaining + * + * @example + * ```typescript + * // Sort in ascending order (default) + * const users = await UserManager.query() + * .partitionKey('id').eq('user-123') + * .sort('ascending') + * .run(); + * + * // Sort in descending order + * const users = await UserManager.query() + * .partitionKey('id').eq('user-123') + * .sort('descending') + * .run(); + * ``` + */ public sort(order: 'ascending' | 'descending'): this { this.input.ScanIndexForward = order === 'ascending'; return this; diff --git a/lib/query/types.ts b/lib/query/types.ts index 539ef27..0ab22f3 100644 --- a/lib/query/types.ts +++ b/lib/query/types.ts @@ -4,24 +4,49 @@ import type { ReturnOption } from '@lib/entity/types'; import { Metadata, TableRetrieverLastKey } from '@lib/table/types'; import { AttributeNames, AttributeValues } from '@lib/utils'; +/** + * Options for query operation execution. + */ export type QueryRunOptions = { + /** Additional DynamoDB query input parameters */ extraInput?: Partial; + /** Return type option */ return?: ReturnOption; + /** Whether to fetch all results (pagination) */ all?: boolean; + /** Delay between paginated requests in milliseconds */ delay?: number; + /** Maximum number of items to return */ max?: number; }; +/** + * Output from query operation execution. + * + * @template M - The metadata type for the table + * @template E - The entity class type + */ export type QueryRunOutput, E extends typeof Entity> = { + /** Array of entity instances returned from the query */ items: Array>; + /** Number of items returned */ count: number; + /** Number of items scanned */ scannedCount: number; + /** Last evaluated key for pagination */ lastKey?: TableRetrieverLastKey; }; +/** + * Built query condition expression components. + */ export type BuildQueryConditionExpression = { + /** Attribute names mapping */ attributeNames: AttributeNames; + /** Attribute values mapping */ attributeValues: AttributeValues; + /** Filter expression string */ conditionExpression: string; + /** Key condition expression string */ keyConditionExpression: string; }; diff --git a/lib/retriever/index.ts b/lib/retriever/index.ts index 2b5a0e1..86c3a9b 100644 --- a/lib/retriever/index.ts +++ b/lib/retriever/index.ts @@ -8,11 +8,26 @@ import { EntityKey } from '@lib/entity/types'; import { Metadata, TableIndexNames, TableRetrieverLastKey } from '@lib/table/types'; import { AttributeNames, AttributeValues } from '@lib/utils'; +/** + * Base class for DynamoDB retrieval operations (Query and Scan). + * + * This abstract class provides common functionality for both Query and Scan operations, + * including index selection, pagination, filtering, and projection. It extends the + * Condition class to provide filtering capabilities. + */ export default class RetrieverBase, E extends typeof Entity> extends Condition { + /** The DynamoDB input object (QueryInput or ScanInput) */ protected input: QueryInput | ScanInput; + /** Attribute names mapping for expression attribute names */ protected attributeNames: AttributeNames = {}; + /** Attribute values mapping for expression attribute values */ protected attributeValues: AttributeValues = {}; + /** + * Creates a new RetrieverBase instance. + * + * @param entity - The entity class to retrieve data for + */ constructor(entity: E) { super(entity); this.input = { @@ -20,16 +35,64 @@ export default class RetrieverBase, E extends typeof Entit }; } + /** + * Specifies the index to query or scan. + * + * @param name - The name of the index to use + * @returns The retriever instance for chaining + * + * @example + * ```typescript + * const users = await UserManager.query() + * .indexName('StatusIndex') + * .partitionKey('status').eq('active') + * .run(); + * ``` + */ public indexName(name: TableIndexNames) { this.input.IndexName = String(name); return this; } + /** + * Limits the number of items returned. + * + * @param count - Maximum number of items to return + * @returns The retriever instance for chaining + * + * @example + * ```typescript + * const users = await UserManager.query() + * .partitionKey('status').eq('active') + * .limit(10) + * .run(); + * ``` + */ public limit(count: number) { this.input.Limit = count; return this; } + /** + * Sets the starting point for pagination. + * + * @param key - The last evaluated key from a previous operation + * @returns The retriever instance for chaining + * + * @example + * ```typescript + * const firstPage = await UserManager.query() + * .partitionKey('status').eq('active') + * .limit(10) + * .run(); + * + * const secondPage = await UserManager.query() + * .partitionKey('status').eq('active') + * .limit(10) + * .startAt(firstPage.lastKey) + * .run(); + * ``` + */ public startAt(key?: TableRetrieverLastKey) { if (key) { this.input.ExclusiveStartKey = convertRetrieverLastKeyToAttributeValues(this.entity, key); @@ -38,16 +101,65 @@ export default class RetrieverBase, E extends typeof Entit return this; } + /** + * Enables consistent read for the operation. + * + * Consistent reads return data that reflects all successful write operations + * that completed before the read operation. This may increase latency and + * consume more read capacity. + * + * @returns The retriever instance for chaining + * + * @example + * ```typescript + * const user = await UserManager.query() + * .partitionKey('id').eq('user-123') + * .consistent() + * .run(); + * ``` + */ public consistent() { this.input.ConsistentRead = true; return this; } + /** + * Returns only the count of items instead of the actual items. + * + * This is more efficient when you only need to know how many items + * match your criteria without retrieving the actual data. + * + * @returns The retriever instance for chaining + * + * @example + * ```typescript + * const result = await UserManager.query() + * .partitionKey('status').eq('active') + * .count() + * .run(); + * + * console.log('Active users count:', result.count); + * ``` + */ public count() { this.input.Select = 'COUNT'; return this; } + /** + * Specifies which attributes to return. + * + * @param attributes - Array of attribute names to project + * @returns The retriever instance for chaining + * + * @example + * ```typescript + * const users = await UserManager.query() + * .partitionKey('status').eq('active') + * .attributes(['id', 'name', 'email']) + * .run(); + * ``` + */ public attributes(attributes: Array>) { this.input.ProjectionExpression = buildGetProjectionExpression( attributes, diff --git a/lib/scan/index.ts b/lib/scan/index.ts index f532713..d905b46 100644 --- a/lib/scan/index.ts +++ b/lib/scan/index.ts @@ -7,16 +7,95 @@ import type { ScanRunOptions, ScanRunOutput } from '@lib/scan/types'; import { Metadata } from '@lib/table/types'; import { ExpressionBuilder, isNotEmptyString } from '@lib/utils'; +/** + * Scan builder for DynamoDB scan operations. + * + * The Scan class provides a fluent interface for building and executing + * DynamoDB scan operations. Scans examine every item in the table or index + * and can be filtered using filter expressions. + * + * ⚠️ **Warning**: Scan operations are expensive and slow. They examine every + * item in the table or index, which can consume significant read capacity. + * Consider using Query operations instead when possible. + * + * @example + * ```typescript + * // Scan all items with a filter + * const users = await UserManager.scan() + * .attribute('status').eq('active') + * .run(); + * + * // Scan with pagination + * const users = await UserManager.scan() + * .attribute('age').gt(18) + * .run({ all: false, max: 100 }); + * + * // Scan with parallel segments + * const users = await UserManager.scan() + * .segment(0) + * .totalSegments(4) + * .run(); + * ``` + * + * @see {@link https://blazejkustra.github.io/dynamode/docs/guide/scan} for more information + * @see {@link https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Scan.html} for DynamoDB Scan operations + */ export default class Scan, E extends typeof Entity> extends RetrieverBase { + /** The DynamoDB ScanInput object */ protected declare input: ScanInput; + /** + * Creates a new Scan instance. + * + * @param entity - The entity class to scan + */ constructor(entity: E) { super(entity); } + /** + * Executes the scan operation. + * + * @param options - Optional configuration for the scan execution + * @returns A promise that resolves to the scan results + * + * @example + * ```typescript + * // Execute scan and get all results + * const result = await UserManager.scan() + * .attribute('status').eq('active') + * .run(); + * + * // Execute scan with pagination + * const result = await UserManager.scan() + * .attribute('age').gt(18) + * .run({ all: false, max: 100 }); + * ``` + */ public run(options?: ScanRunOptions & { return?: 'default' }): Promise>; + + /** + * Executes the scan operation, returning the raw AWS response. + * + * @param options - Configuration for the scan execution with return type 'output' + * @returns A promise that resolves to the raw ScanCommandOutput + */ public run(options: ScanRunOptions & { return: 'output' }): Promise; + + /** + * Builds the Scan command input without executing it. + * + * @param options - Configuration for the scan execution with return type 'input' + * @returns The ScanInput object + */ public run(options: ScanRunOptions & { return: 'input' }): ScanInput; + + /** + * Executes the scan operation. + * + * @param options - Optional configuration for the scan execution + * @returns A promise that resolves to the scan results, raw AWS response, or command input + */ public run(options?: ScanRunOptions): Promise | ScanCommandOutput> | ScanInput { this.buildScanInput(options?.extraInput); @@ -44,11 +123,41 @@ export default class Scan, E extends typeof Entity> extend })(); } + /** + * Sets the segment number for parallel scan operations. + * + * @param value - The segment number (0-based) + * @returns The scan instance for method chaining + * + * @example + * ```typescript + * // Use segment 0 of 4 total segments + * const users = await UserManager.scan() + * .segment(0) + * .totalSegments(4) + * .run(); + * ``` + */ public segment(value: number) { this.input.Segment = value; return this; } + /** + * Sets the total number of segments for parallel scan operations. + * + * @param value - The total number of segments + * @returns The scan instance for method chaining + * + * @example + * ```typescript + * // Use 4 parallel segments + * const users = await UserManager.scan() + * .segment(0) + * .totalSegments(4) + * .run(); + * ``` + */ public totalSegments(value: number) { this.input.TotalSegments = value; return this; diff --git a/lib/scan/types.ts b/lib/scan/types.ts index cfe4168..acd375a 100644 --- a/lib/scan/types.ts +++ b/lib/scan/types.ts @@ -4,20 +4,41 @@ import type { ReturnOption } from '@lib/entity/types'; import { Metadata, TableRetrieverLastKey } from '@lib/table/types'; import { AttributeNames, AttributeValues } from '@lib/utils'; +/** + * Options for scan operation execution. + */ export type ScanRunOptions = { + /** Additional DynamoDB scan input parameters */ extraInput?: Partial; + /** Return type option */ return?: ReturnOption; }; +/** + * Output from scan operation execution. + * + * @template M - The metadata type for the table + * @template E - The entity class type + */ export type ScanRunOutput, E extends typeof Entity> = { + /** Array of entity instances returned from the scan */ items: Array>; + /** Number of items returned */ count: number; + /** Number of items scanned */ scannedCount: number; + /** Last evaluated key for pagination */ lastKey?: TableRetrieverLastKey; }; +/** + * Built scan condition expression components. + */ export type BuildScanConditionExpression = { + /** Attribute names mapping */ attributeNames: AttributeNames; + /** Attribute values mapping */ attributeValues: AttributeValues; + /** Filter expression string */ conditionExpression: string; }; diff --git a/lib/stream/index.ts b/lib/stream/index.ts index 1d45393..134952e 100644 --- a/lib/stream/index.ts +++ b/lib/stream/index.ts @@ -5,15 +5,84 @@ import { AttributeValues, DynamodeStreamError, fromDynamo } from '@lib/utils'; import { DynamoDBRecord } from './types'; +/** + * Stream class for processing DynamoDB stream records. + * + * This class provides a convenient way to process DynamoDB stream records + * and convert them into Dynamode entity instances. It handles different + * stream view types and operations (INSERT, MODIFY, REMOVE). + * + * @template E - The entity class type + * + * @example + * ```typescript + * // In an AWS Lambda function + * export const handler = async (event: any) => { + * for (const record of event.Records) { + * const stream = new Stream(record); + * + * if (stream.operation === 'insert') { + * console.log('New item:', stream.newImage); + * } else if (stream.operation === 'modify') { + * console.log('Old item:', stream.oldImage); + * console.log('New item:', stream.newImage); + * } else if (stream.operation === 'remove') { + * console.log('Removed item:', stream.oldImage); + * } + * + * // Type-safe entity checking + * if (stream.isEntity(User)) { + * // stream is now typed as Stream + * console.log('User entity:', stream.newImage?.name); + * } + * } + * }; + * ``` + * + * @see {@link https://blazejkustra.github.io/dynamode/docs/guide/stream} for more information + * @see {@link https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Streams.html} for DynamoDB Streams documentation + * @see {@link https://docs.aws.amazon.com/lambda/latest/dg/with-ddb.html} for AWS Lambda with DynamoDB streams + */ export default class Stream { + /** The type of stream data available */ streamType: 'newImage' | 'oldImage' | 'both'; + /** The operation that triggered the stream record */ operation: 'insert' | 'modify' | 'remove'; + /** The old image of the item (available for MODIFY and REMOVE operations) */ oldImage?: InstanceType; + /** The new image of the item (available for INSERT and MODIFY operations) */ newImage?: InstanceType; - // Dynamode entity class + /** The Dynamode entity class for this stream record */ entity: E; + /** + * Creates a new Stream instance from a DynamoDB stream record. + * + * @param record - The DynamoDB stream record containing event information + * @throws {DynamodeStreamError} When the operation is invalid + * @throws {DynamodeStreamError} When the record is invalid + * @throws {DynamodeStreamError} When the stream type is KEYS_ONLY (not supported) + * @throws {DynamodeStreamError} When the stream type is invalid + * @throws {DynamodeStreamError} When the processed item is not a Dynamode entity + * + * @example + * ```typescript + * // In AWS Lambda handler + * export const handler = async (event: any) => { + * for (const record of event.Records) { + * try { + * const stream = new Stream(record); + * // Process the stream... + * } catch (error) { + * if (error instanceof DynamodeStreamError) { + * console.error('Stream processing error:', error.message); + * } + * } + * } + * }; + * ``` + */ constructor({ dynamodb: record, eventName }: DynamoDBRecord) { switch (eventName) { case 'INSERT': @@ -66,6 +135,33 @@ export default class Stream { } } + /** + * Type guard to check if the stream record is for a specific entity type. + * + * This method provides type-safe entity checking and narrows the type + * of the stream instance to the specified entity type. + * + * @template TargetEntity - The target entity class type + * @param entity - The entity class to check against + * @returns True if the stream record is for the specified entity type + * + * @example + * ```typescript + * const stream = new Stream(record); + * + * if (stream.isEntity(User)) { + * // stream is now typed as Stream + * console.log('User name:', stream.newImage?.name); + * console.log('User email:', stream.newImage?.email); + * } + * + * if (stream.isEntity(Product)) { + * // stream is now typed as Stream + * console.log('Product title:', stream.newImage?.title); + * console.log('Product price:', stream.newImage?.price); + * } + * ``` + */ isEntity(entity: TargetEntity): this is Stream { return this.entity === entity; } diff --git a/lib/stream/types.ts b/lib/stream/types.ts index 265b048..5d3b7d9 100644 --- a/lib/stream/types.ts +++ b/lib/stream/types.ts @@ -1,26 +1,52 @@ import type { AttributeValue } from '@aws-sdk/client-dynamodb'; -// For compatibility with aws lambda: https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/aws-lambda/trigger/dynamodb-stream.d.ts +/** + * Attribute value type for AWS Lambda compatibility. + * + * This type is used for compatibility with AWS Lambda DynamoDB stream triggers. + * @see {@link https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/aws-lambda/trigger/dynamodb-stream.d.ts} + */ type AttributeValueAWSLambda = { + /** Binary attribute */ B?: string | undefined; + /** Binary set attribute */ BS?: string[] | undefined; + /** Boolean attribute */ BOOL?: boolean | undefined; + /** List attribute */ L?: AttributeValueAWSLambda[] | undefined; + /** Map attribute */ M?: { [id: string]: AttributeValueAWSLambda } | undefined; + /** Number attribute */ N?: string | undefined; + /** Number set attribute */ NS?: string[] | undefined; + /** Null attribute */ NULL?: boolean | undefined; + /** String attribute */ S?: string | undefined; + /** String set attribute */ SS?: string[] | undefined; }; +/** + * DynamoDB stream payload containing item images. + */ export type StreamPayload = { + /** New image of the item after the change */ NewImage?: Record | undefined; + /** Old image of the item before the change */ OldImage?: Record | undefined; + /** Type of stream view */ StreamViewType?: 'KEYS_ONLY' | 'NEW_IMAGE' | 'OLD_IMAGE' | 'NEW_AND_OLD_IMAGES' | undefined; }; +/** + * DynamoDB stream record containing event information. + */ export type DynamoDBRecord = { + /** Name of the event that triggered the stream record */ eventName?: 'INSERT' | 'MODIFY' | 'REMOVE' | undefined; + /** DynamoDB stream payload */ dynamodb?: StreamPayload | undefined; }; diff --git a/lib/table/index.ts b/lib/table/index.ts index 38ba9c9..5f5e778 100644 --- a/lib/table/index.ts +++ b/lib/table/index.ts @@ -30,10 +30,78 @@ import { isNotEmptyArray, Narrow, ValidationError } from '@lib/utils'; import { getTableGlobalSecondaryIndexes, getTableLocalSecondaryIndexes } from './helpers/indexes'; +/** + * Manages DynamoDB table operations and provides entity managers. + * + * The TableManager is responsible for creating, deleting, and managing DynamoDB tables, + * as well as providing entity managers for performing CRUD operations on entities + * within those tables. + * + * @example + * ```typescript + * class User extends Entity { + * @attribute.partitionKey.string() + * id: string; + * + * @attribute.string() + * name: string; + * } + * + * const UserTableManager = new TableManager(User, { + * tableName: 'users-table', + * partitionKey: 'id', + * indexes: { + * NameIndex: { + * partitionKey: 'name' + * } + * } + * }); + * + * // Create the table + * await UserTableManager.createTable(); + * + * // Get an entity manager + * const UserManager = UserTableManager.entityManager(); + * ``` + * + * @see {@link https://blazejkustra.github.io/dynamode/docs/guide/managers/tableManager} for more information + */ export default class TableManager, TE extends typeof Entity> { + /** + * The table metadata configuration. + * + * @readonly + */ public tableMetadata: M; + + /** + * The base entity class for this table. + * + * @readonly + */ public tableEntity: TE; + /** + * Creates a new TableManager instance. + * + * @param tableEntity - The base entity class for the table + * @param tableMetadata - The table configuration metadata + * + * @example + * ```typescript + * const tableManager = new TableManager(User, { + * tableName: 'users-table', + * partitionKey: 'id', + * sortKey: 'createdAt', + * indexes: { + * StatusIndex: { + * partitionKey: 'status', + * sortKey: 'createdAt' + * } + * } + * }); + * ``` + */ constructor(tableEntity: TE, tableMetadata: Narrow) { const metadata: M = tableMetadata as M; @@ -45,8 +113,51 @@ export default class TableManager, TE extends typeof Enti this.tableEntity = tableEntity; } + /** + * Creates an entity manager for the table. + * + * @returns An EntityManager instance for the base table entity + * + * @example + * ```typescript + * // Get manager for base table entity + * const UserManager = UserTableManager.entityManager(); + * const user = await UserManager.get({ id: 'user-123' }); + * ``` + */ public entityManager(): ReturnType>; + /** + * Creates an entity manager for a specific entity. + * + * @param entity - The entity class constructor + * @returns An EntityManager instance for the specific entity + * + * @example + * ```typescript + * // Get manager for specific entity + * const AdminManager = UserTableManager.entityManager(Admin); + * const admin = await AdminManager.get({ id: 'admin-123' }); + * ``` + */ public entityManager(entity: E): ReturnType>; + + /** + * Creates an entity manager for the table. + * + * @param entity - Optional entity class constructor. If not provided, uses the base table entity. + * @returns An EntityManager instance for the specified entity or base table entity + * + * @example + * ```typescript + * // Get manager for base table entity + * const UserManager = UserTableManager.entityManager(); + * const user = await UserManager.get({ id: 'user-123' }); + * + * // Get manager for specific entity + * const AdminManager = UserTableManager.entityManager(Admin); + * const admin = await AdminManager.get({ id: 'admin-123' }); + * ``` + */ public entityManager( entity?: E, ): ReturnType> | ReturnType> { @@ -58,9 +169,63 @@ export default class TableManager, TE extends typeof Enti return EntityManager(this.tableEntity, this.tableMetadata.tableName); } + /** + * Creates a DynamoDB table with default return type (converted table data). + * + * @param options - Optional configuration for table creation + * @returns A promise that resolves to converted table data + * + * @example + * ```typescript + * const tableData = await UserTableManager.createTable(); + * ``` + */ public createTable(options?: TableCreateOptions & { return?: 'default' }): Promise; + + /** + * Creates a DynamoDB table, returning the raw AWS response. + * + * @param options - Configuration for table creation with return type 'output' + * @returns A promise that resolves to the raw CreateTableCommandOutput + * + * @example + * ```typescript + * const response = await UserTableManager.createTable({ return: 'output' }); + * console.log(response.TableDescription); + * ``` + */ public createTable(options: TableCreateOptions & { return: 'output' }): Promise; + + /** + * Builds the CreateTable command input without executing it. + * + * @param options - Configuration for table creation with return type 'input' + * @returns The CreateTableCommandInput object + * + * @example + * ```typescript + * const input = UserTableManager.createTable({ return: 'input' }); + * // Use input with AWS SDK directly + * ``` + */ public createTable(options: TableCreateOptions & { return: 'input' }): CreateTableCommandInput; + + /** + * Creates a DynamoDB table with the specified configuration. + * + * @param options - Optional configuration for table creation + * @returns A promise that resolves to table data, raw AWS response, or command input + * + * @example + * ```typescript + * // Create table with custom options + * const tableData = await UserTableManager.createTable({ + * throughput: { read: 5, write: 5 }, + * tags: { Environment: 'production' }, + * deletionProtection: true + * }); + * ``` + */ public createTable( options?: TableCreateOptions, ): Promise | CreateTableCommandInput { @@ -100,15 +265,69 @@ export default class TableManager, TE extends typeof Enti })(); } + /** + * Deletes a DynamoDB table with default return type (converted table data). + * + * @param tableName - The name of the table to delete + * @param options - Optional configuration for table deletion + * @returns A promise that resolves to converted table data + * + * @example + * ```typescript + * const tableData = await UserTableManager.deleteTable('UserTable'); + * ``` + */ public deleteTable(tableName: string, options?: TableDeleteOptions & { return?: 'default' }): Promise; + /** + * Deletes a DynamoDB table, returning the raw AWS response. + * + * @param tableName - The name of the table to delete + * @param options - Configuration for table deletion with return type 'output' + * @returns A promise that resolves to the raw DeleteTableCommandOutput + * + * @example + * ```typescript + * const response = await UserTableManager.deleteTable('UserTable', { return: 'output' }); + * console.log(response.TableDescription); + * ``` + */ public deleteTable( tableName: string, options: TableDeleteOptions & { return: 'output' }, ): Promise; + /** + * Builds the DeleteTable command input without executing it. + * + * @param tableName - The name of the table to delete + * @param options - Configuration for table deletion with return type 'input' + * @returns The DeleteTableCommandInput object + * + * @example + * ```typescript + * const input = UserTableManager.deleteTable('UserTable', { return: 'input' }); + * // Use input with AWS SDK directly + * ``` + */ public deleteTable(tableName: string, options: TableDeleteOptions & { return: 'input' }): DeleteTableCommandInput; + /** + * Deletes a DynamoDB table with the specified configuration. + * + * @param tableName - The name of the table to delete + * @param options - Optional configuration for table deletion + * @returns A promise that resolves to table data, raw AWS response, or command input + * @throws {ValidationError} When the table name doesn't match the table metadata + * + * @example + * ```typescript + * // Delete table with custom options + * const tableData = await UserTableManager.deleteTable('UserTable', { + * extraInput: { BillingMode: 'PAY_PER_REQUEST' } + * }); + * ``` + */ public deleteTable( tableName: string, options?: TableDeleteOptions, @@ -134,21 +353,75 @@ export default class TableManager, TE extends typeof Enti })(); } + /** + * Creates a table index with default return type (converted table data). + * + * @param indexName - The name of the index to create + * @param options - Optional configuration for index creation + * @returns A promise that resolves to converted table data + * + * @example + * ```typescript + * const tableData = await UserTableManager.createTableIndex('GSI_1_NAME'); + * ``` + */ public createTableIndex( indexName: TableIndexNames, options?: TableCreateIndexOptions & { return?: 'default' }, ): Promise; + /** + * Creates a table index, returning the raw AWS response. + * + * @param indexName - The name of the index to create + * @param options - Configuration for index creation with return type 'output' + * @returns A promise that resolves to the raw UpdateTableCommandOutput + * + * @example + * ```typescript + * const response = await UserTableManager.createTableIndex('GSI_1_NAME', { return: 'output' }); + * console.log(response.TableDescription); + * ``` + */ public createTableIndex( indexName: TableIndexNames, options: TableCreateIndexOptions & { return: 'output' }, ): Promise; + /** + * Builds the UpdateTable command input for creating an index without executing it. + * + * @param indexName - The name of the index to create + * @param options - Configuration for index creation with return type 'input' + * @returns The UpdateTableCommandInput object + * + * @example + * ```typescript + * const input = UserTableManager.createTableIndex('GSI_1_NAME', { return: 'input' }); + * // Use input with AWS SDK directly + * ``` + */ public createTableIndex( indexName: TableIndexNames, options: TableCreateIndexOptions & { return: 'input' }, ): UpdateTableCommandInput; + /** + * Creates a table index with the specified configuration. + * + * @param indexName - The name of the index to create + * @param options - Optional configuration for index creation + * @returns A promise that resolves to table data, raw AWS response, or command input + * @throws {ValidationError} When the index is not decorated in the entity or missing partition key + * + * @example + * ```typescript + * // Create index with custom throughput + * const tableData = await UserTableManager.createTableIndex('GSI_1_NAME', { + * throughput: { read: 5, write: 5 } + * }); + * ``` + */ public createTableIndex( indexName: TableIndexNames, options?: TableCreateIndexOptions, @@ -195,21 +468,75 @@ export default class TableManager, TE extends typeof Enti })(); } + /** + * Deletes a table index with default return type (converted table data). + * + * @param indexName - The name of the index to delete + * @param options - Optional configuration for index deletion + * @returns A promise that resolves to converted table data + * + * @example + * ```typescript + * const tableData = await UserTableManager.deleteTableIndex('OldIndex'); + * ``` + */ public deleteTableIndex( indexName: string, options?: TableDeleteIndexOptions & { return?: 'default' }, ): Promise; + /** + * Deletes a table index, returning the raw AWS response. + * + * @param indexName - The name of the index to delete + * @param options - Configuration for index deletion with return type 'output' + * @returns A promise that resolves to the raw UpdateTableCommandOutput + * + * @example + * ```typescript + * const response = await UserTableManager.deleteTableIndex('OldIndex', { return: 'output' }); + * console.log(response.TableDescription); + * ``` + */ public deleteTableIndex( indexName: string, options: TableDeleteIndexOptions & { return: 'output' }, ): Promise; + /** + * Builds the UpdateTable command input for deleting an index without executing it. + * + * @param indexName - The name of the index to delete + * @param options - Configuration for index deletion with return type 'input' + * @returns The UpdateTableCommandInput object + * + * @example + * ```typescript + * const input = UserTableManager.deleteTableIndex('OldIndex', { return: 'input' }); + * // Use input with AWS SDK directly + * ``` + */ public deleteTableIndex( indexName: string, options: TableDeleteIndexOptions & { return: 'input' }, ): UpdateTableCommandInput; + /** + * Deletes a table index with the specified configuration. + * + * @param indexName - The name of the index to delete + * @param options - Optional configuration for index deletion + * @returns A promise that resolves to table data, raw AWS response, or command input + * @throws {ValidationError} When the index is still decorated in the entity + * + * @example + * ```typescript + * // Delete index with custom options + * const tableData = await UserTableManager.deleteTableIndex('OldIndex', { + * extraInput: { BillingMode: 'PAY_PER_REQUEST' } + * }); + * ``` + */ public deleteTableIndex( indexName: string, options?: TableDeleteIndexOptions, @@ -242,9 +569,53 @@ export default class TableManager, TE extends typeof Enti })(); } + /** + * Validates a table with default return type (converted table data). + * + * @param options - Optional configuration for table validation + * @returns A promise that resolves to converted table data + * + * @example + * ```typescript + * const tableData = await UserTableManager.validateTable(); + * ``` + */ public validateTable(options?: TableValidateOptions & { return?: 'default' }): Promise; + + /** + * Validates the table, returning the raw AWS response. + * + * @param options - Configuration for table validation with return type 'output' + * @returns A promise that resolves to the raw DescribeTableCommandOutput + * + * @example + * ```typescript + * const response = await UserTableManager.validateTable({ return: 'output' }); + * console.log(response.Table); + * ``` + */ public validateTable(options: TableValidateOptions & { return: 'output' }): Promise; + + /** + * Builds the DescribeTable command input without executing it. + * + * @param options - Configuration for table validation with return type 'input' + * @returns The DescribeTableCommandInput object + * + * @example + * ```typescript + * const input = UserTableManager.validateTable({ return: 'input' }); + * // Use input with AWS SDK directly + * ``` + */ public validateTable(options: TableValidateOptions & { return: 'input' }): DescribeTableCommandInput; + + /** + * Validates that the existing DynamoDB table matches the table metadata configuration. + * + * @param options - Optional configuration for table validation + * @returns A promise that resolves to table data, raw AWS response, or command input + */ public validateTable( options?: TableValidateOptions, ): Promise | DescribeTableCommandInput { diff --git a/lib/table/types.ts b/lib/table/types.ts index af9d124..38c3f6e 100644 --- a/lib/table/types.ts +++ b/lib/table/types.ts @@ -9,32 +9,66 @@ import { import Entity from '@lib/entity'; import { ReturnOption } from '@lib/entity/types'; -// Table metadata types - +/** + * Table metadata types and configurations. + */ + +/** + * Entity key type for table metadata. + * + * @template E - The entity class type + */ type EntityKey = keyof InstanceType extends string ? keyof InstanceType : never; +/** + * Table indexes metadata configuration. + * + * @template E - The entity class type + */ type TableIndexesMetadata = { [indexName: string]: { + /** Partition key for the index */ partitionKey?: EntityKey; + /** Sort key for the index */ sortKey?: EntityKey; }; }; +/** + * Table metadata configuration. + * + * @template E - The entity class type + */ export type Metadata = { + /** Name of the DynamoDB table */ tableName: string; + /** Partition key attribute name */ partitionKey: EntityKey; + /** Sort key attribute name (optional) */ sortKey?: EntityKey; + /** Secondary indexes configuration */ indexes?: TableIndexesMetadata; + /** Created at timestamp attribute (optional) */ createdAt?: EntityKey; + /** Updated at timestamp attribute (optional) */ updatedAt?: EntityKey; }; +/** Sort key type from metadata */ type SK, E extends typeof Entity> = M['sortKey']; +/** Partition key type from metadata */ type PK, E extends typeof Entity> = M['partitionKey']; +/** Indexes type from metadata */ type Idx, E extends typeof Entity> = M['indexes']; +/** + * Primary key type for table operations. + * + * @template M - The metadata type + * @template E - The entity class type + */ export type TablePrimaryKey, E extends typeof Entity> = Pick< InstanceType, Extract, SK extends string ? PK | SK : PK> diff --git a/lib/transactionGet/index.ts b/lib/transactionGet/index.ts index ba63a1f..4d28404 100644 --- a/lib/transactionGet/index.ts +++ b/lib/transactionGet/index.ts @@ -5,6 +5,32 @@ import { convertAttributeValuesToEntity } from '@lib/entity/helpers/converters'; import type { TransactionGetInput, TransactionGetOptions, TransactionGetOutput } from '@lib/transactionGet/types'; import { NotFoundError } from '@lib/utils'; +/** + * Executes a DynamoDB TransactGetItems operation. + * + * This function allows you to retrieve multiple items from one or more tables + * in a single atomic operation. All items are retrieved or none are retrieved. + * + * @param transactions - Array of transaction get operations + * @returns A promise that resolves to the transaction results + * @throws {NotFoundError} When items are not found and throwOnNotFound is true + * + * @example + * ```typescript + * // Get multiple items in a transaction + * const result = await transactionGet([ + * UserManager.transactionGet({ id: 'user-1' }), + * UserManager.transactionGet({ id: 'user-2' }), + * UserManager.transactionGet({ id: 'product-1' }), + * ]); + * + * console.log('Users:', result.items[0], result.items[1]); + * console.log('Product:', result.items[2]); + * ``` + * + * @see {@link https://blazejkustra.github.io/dynamode/docs/guide/transactions#transactiongettransactions-options} for more examples + * @see {@link https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/transaction-apis.html#transaction-apis-get} for AWS TransactGetItems documentation + */ export default function transactionGet>( transactions: TransactionGetInput<[...E]>, ): Promise>; diff --git a/lib/transactionGet/types.ts b/lib/transactionGet/types.ts index 1f9c4e3..e3a81cd 100644 --- a/lib/transactionGet/types.ts +++ b/lib/transactionGet/types.ts @@ -2,22 +2,48 @@ import { Get, TransactGetItemsCommandInput } from '@aws-sdk/client-dynamodb'; import Entity from '@lib/entity'; import type { ReturnOption } from '@lib/entity/types'; +/** + * Transaction get operation for a specific entity. + * + * @template E - The entity class type + */ export type TransactionGet = { + /** DynamoDB get operation */ get: Get; + /** Entity class for the operation */ entity: E; }; +/** + * Input type for transaction get operations. + * + * @template E - Array of entity class types + */ export type TransactionGetInput> = { readonly [K in keyof E]: TransactionGet; }; +/** + * Options for transaction get operations. + */ export type TransactionGetOptions = { + /** Return type option */ return?: ReturnOption; + /** Additional DynamoDB transaction get input parameters */ extraInput?: Partial; + /** Whether to throw an error if items are not found */ throwOnNotFound?: boolean; }; +/** + * Output from transaction get operations. + * + * @template E - Array of entity class types + * @template Extra - Additional type for items that couldn't be retrieved + */ export type TransactionGetOutput, Extra = never> = { + /** Array of retrieved entity instances */ items: { [K in keyof E]: InstanceType | Extra }; + /** Number of items retrieved */ count: number; }; diff --git a/lib/transactionWrite/index.ts b/lib/transactionWrite/index.ts index 630c36b..d97eed9 100644 --- a/lib/transactionWrite/index.ts +++ b/lib/transactionWrite/index.ts @@ -9,6 +9,32 @@ import type { TransactionWriteOutput, } from '@lib/transactionWrite/types'; +/** + * Executes a DynamoDB TransactWriteItems operation. + * + * This function allows you to perform multiple write operations (Put, Update, Delete, ConditionCheck) + * across one or more tables in a single atomic transaction. All operations succeed or all fail. + * + * @param transactions - Array of transaction write operations + * @param options - Optional configuration for the transaction + * @returns A promise that resolves to the transaction results + * + * @example + * ```typescript + * // Perform multiple write operations in a transaction + * const result = await transactionWrite([ + * UserManager.transaction.put(new User({ id: 'user-1', name: 'John' })), + * UserManager.transaction.update({ id: 'user-2' }, { set: { status: 'active' } }), + * UserManager.transaction.delete({ id: 'product-1' }), + * UserManager.transaction.condition({ id: 'user-3' }, UserManager.condition().attribute('status').eq('active')), + * ]); + * + * console.log('Transaction completed:', result.count, 'operations'); + * ``` + * + * @see {@link https://blazejkustra.github.io/dynamode/docs/guide/transactions#transactionwritetransactions-options} for more examples + * @see {@link https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/transaction-apis.html#transaction-apis-write} for AWS TransactWriteItems documentation + */ export default function transactionWrite>>( transactions: TransactionWriteInput<[...TW]>, options?: TransactionWriteOptions & { return?: 'default' }, diff --git a/lib/transactionWrite/types.ts b/lib/transactionWrite/types.ts index 5590a09..20ec01e 100644 --- a/lib/transactionWrite/types.ts +++ b/lib/transactionWrite/types.ts @@ -2,38 +2,96 @@ import { ConditionCheck, Delete, Put, TransactWriteItemsCommandInput, Update } f import Entity from '@lib/entity'; import type { ReturnOption } from '@lib/entity/types'; +/** + * Transaction update operation for a specific entity. + * + * @template E - The entity class type + */ export type TransactionUpdate = { + /** Entity class for the operation */ entity: E; + /** DynamoDB update operation */ update: Update; }; -export type TransactionPut = { entity: E; put: Put }; + +/** + * Transaction put operation for a specific entity. + * + * @template E - The entity class type + */ +export type TransactionPut = { + /** Entity class for the operation */ + entity: E; + /** DynamoDB put operation */ + put: Put; +}; + +/** + * Transaction delete operation for a specific entity. + * + * @template E - The entity class type + */ export type TransactionDelete = { + /** Entity class for the operation */ entity: E; + /** DynamoDB delete operation */ delete: Delete; }; + +/** + * Transaction condition check operation for a specific entity. + * + * @template E - The entity class type + */ export type TransactionCondition = { + /** Entity class for the operation */ entity: E; + /** DynamoDB condition check operation */ condition: ConditionCheck; }; + +/** + * Union type for all transaction write operations. + * + * @template E - The entity class type + */ export type TransactionWrite = | TransactionUpdate | TransactionPut | TransactionDelete | TransactionCondition; +/** + * Input type for transaction write operations. + * + * @template TW - Array of transaction write operations + */ export type TransactionWriteInput>> = { readonly [K in keyof TW]: TW[K]; }; +/** + * Options for transaction write operations. + */ export type TransactionWriteOptions = { + /** Return type option */ return?: ReturnOption; + /** Additional DynamoDB transaction write input parameters */ extraInput?: Partial; + /** Idempotency key for the transaction */ idempotencyKey?: string; }; +/** + * Output from transaction write operations. + * + * @template TW - Array of transaction write operations + */ export type TransactionWriteOutput>> = { + /** Array of items from put operations */ items: { [K in keyof TW]: TW[K] extends TransactionPut ? InstanceType : undefined; }; + /** Number of operations in the transaction */ count: number; }; diff --git a/lib/utils/constants.ts b/lib/utils/constants.ts index cae6fdc..7de3efa 100644 --- a/lib/utils/constants.ts +++ b/lib/utils/constants.ts @@ -1,15 +1,41 @@ import { AttributeType } from '@lib/dynamode/storage/types'; import { insertBetween } from '@lib/utils'; +/** + * Core constants for Dynamode operations. + */ + +/** + * The property name used to identify Dynamode entities. + * This is automatically added to all entity instances. + */ export const DYNAMODE_ENTITY = 'dynamodeEntity'; +/** + * Mapping of JavaScript types to DynamoDB attribute types for keys. + * Only String and Number types are allowed for partition and sort keys. + */ export const DYNAMODE_DYNAMO_KEY_TYPE_MAP = new Map([ [String, 'S'], [Number, 'N'], ]); +/** + * Array of attribute types that are allowed for DynamoDB keys. + * DynamoDB only supports String and Number types for partition and sort keys. + */ export const DYNAMODE_ALLOWED_KEY_TYPES: AttributeType[] = [String, Number]; +/** + * Set of reserved words that cannot be used as attribute names in DynamoDB. + * + * These are SQL and DynamoDB reserved words that would cause conflicts + * if used as attribute names in expressions. Dynamode automatically + * handles these by using expression attribute names when needed. + * + * @see {@link https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/ReservedWords.html} for complete list of reserved words + * @see {@link https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Expressions.ExpressionAttributeNames.html} for expression attribute names + */ export const RESERVED_WORDS = new Set([ 'ABORT', 'ABSOLUTE', @@ -586,87 +612,132 @@ export const RESERVED_WORDS = new Set([ 'ZONE', ]); +/** + * Expression operator types for building DynamoDB expressions. + */ + +/** + * Represents a literal expression string in an operator chain. + */ export type OperatorExpression = { expression: string }; + +/** + * Represents an attribute name reference in an operator chain. + */ export type OperatorKey = { key: string }; + +/** + * Represents an attribute value with its corresponding key in an operator chain. + */ export type OperatorValue = { value: unknown; key: string }; + +/** + * Union type for all possible operator elements in an expression. + */ export type Operators = Array; +/** + * Base operators for building DynamoDB expressions. + * + * These are the fundamental building blocks for constructing + * condition expressions, update expressions, and other DynamoDB + * expression types. + */ export const BASE_OPERATOR = { - /** space */ + /** Space character */ space: { expression: ' ' }, - /** , */ + /** Comma separator */ comma: { expression: ',' }, - /** ( */ + /** Left parenthesis */ leftParenthesis: { expression: '(' }, - /** ) */ + /** Right parenthesis */ rightParenthesis: { expression: ')' }, - /** + */ + /** Plus operator */ plus: { expression: '+' }, - /** - */ + /** Minus operator */ minus: { expression: '-' }, - /** NOT */ + /** NOT logical operator */ not: { expression: 'NOT' }, - /** AND */ + /** AND logical operator */ and: { expression: 'AND' }, - /** OR */ + /** OR logical operator */ or: { expression: 'OR' }, - /** = */ + /** Equal comparison operator */ eq: { expression: '=' }, - /** <> */ + /** Not equal comparison operator */ ne: { expression: '<>' }, - /** < */ + /** Less than comparison operator */ lt: { expression: '<' }, - /** <= */ + /** Less than or equal comparison operator */ le: { expression: '<=' }, - /** > */ + /** Greater than comparison operator */ gt: { expression: '>' }, - /** >= */ + /** Greater than or equal comparison operator */ ge: { expression: '>=' }, - /** attribute_exists */ + /** attribute_exists function */ attributeExists: { expression: 'attribute_exists' }, - /** contains */ + /** contains function */ contains: { expression: 'contains' }, - /** IN */ + /** IN operator */ in: { expression: 'IN' }, - /** BETWEEN */ + /** BETWEEN operator */ between: { expression: 'BETWEEN' }, - /** attribute_type */ + /** attribute_type function */ attributeType: { expression: 'attribute_type' }, - /** begins_with */ + /** begins_with function */ beginsWith: { expression: 'begins_with' }, - /** attribute_not_exists */ + /** attribute_not_exists function */ attributeNotExists: { expression: 'attribute_not_exists' }, - /** size */ + /** size function */ size: { expression: 'size' }, - /** if_not_exists */ + /** if_not_exists function */ ifNotExists: { expression: 'if_not_exists' }, - /** list_append */ + /** list_append function */ listAppend: { expression: 'list_append' }, - /** SET */ + /** SET update action */ set: { expression: 'SET' }, - /** ADD */ + /** ADD update action */ add: { expression: 'ADD' }, - /** REMOVE */ + /** REMOVE update action */ remove: { expression: 'REMOVE' }, - /** DELETE */ + /** DELETE update action */ delete: { expression: 'DELETE' }, } as const; +/** + * Condition operators for building DynamoDB condition expressions. + * + * These operators construct complex condition expressions by combining + * attribute names, values, and comparison operators. Each operator + * returns an array of operator elements that can be processed to + * generate the final expression string. + */ export const OPERATORS = { - /** parenthesis */ + /** + * Wraps an operator structure in parentheses. + * + * @param operatorStructure - The operators to wrap in parentheses + * @returns Operators wrapped in parentheses + */ parenthesis: (operatorStructure: Operators): Operators => [ BASE_OPERATOR.leftParenthesis, ...operatorStructure, BASE_OPERATOR.rightParenthesis, ], - /** $K = $V */ + /** + * Creates an equality comparison: $K = $V + * + * @param key - The attribute name + * @param value - The value to compare against + * @returns Operators for equality comparison + */ eq: (key: string, value: unknown): Operators => [ { key }, BASE_OPERATOR.space, @@ -674,7 +745,13 @@ export const OPERATORS = { BASE_OPERATOR.space, { value, key }, ], - /** $K <> $V */ + /** + * Creates a not equal comparison: $K <> $V + * + * @param key - The attribute name + * @param value - The value to compare against + * @returns Operators for not equal comparison + */ ne: (key: string, value: unknown): Operators => [ { key }, BASE_OPERATOR.space, @@ -682,7 +759,13 @@ export const OPERATORS = { BASE_OPERATOR.space, { value, key }, ], - /** $K < $V */ + /** + * Creates a less than comparison: $K < $V + * + * @param key - The attribute name + * @param value - The value to compare against + * @returns Operators for less than comparison + */ lt: (key: string, value: unknown): Operators => [ { key }, BASE_OPERATOR.space, @@ -690,7 +773,13 @@ export const OPERATORS = { BASE_OPERATOR.space, { value, key }, ], - /** $K <= $V */ + /** + * Creates a less than or equal comparison: $K <= $V + * + * @param key - The attribute name + * @param value - The value to compare against + * @returns Operators for less than or equal comparison + */ le: (key: string, value: unknown): Operators => [ { key }, BASE_OPERATOR.space, @@ -698,7 +787,13 @@ export const OPERATORS = { BASE_OPERATOR.space, { value, key }, ], - /** $K > $V */ + /** + * Creates a greater than comparison: $K > $V + * + * @param key - The attribute name + * @param value - The value to compare against + * @returns Operators for greater than comparison + */ gt: (key: string, value: unknown): Operators => [ { key }, BASE_OPERATOR.space, @@ -706,7 +801,13 @@ export const OPERATORS = { BASE_OPERATOR.space, { value, key }, ], - /** $K >= $V */ + /** + * Creates a greater than or equal comparison: $K >= $V + * + * @param key - The attribute name + * @param value - The value to compare against + * @returns Operators for greater than or equal comparison + */ ge: (key: string, value: unknown): Operators => [ { key }, BASE_OPERATOR.space, @@ -715,14 +816,31 @@ export const OPERATORS = { { value, key }, ], - /** attribute_exists($K) */ + /** + * Creates an attribute_exists function: attribute_exists($K) + * + * @param key - The attribute name to check for existence + * @returns Operators for attribute_exists function + */ attributeExists: (key: string): Operators => [BASE_OPERATOR.attributeExists, ...OPERATORS.parenthesis([{ key }])], - /** contains($K, $V) */ + /** + * Creates a contains function: contains($K, $V) + * + * @param key - The attribute name + * @param value - The value to check if the attribute contains + * @returns Operators for contains function + */ contains: (key: string, value: unknown): Operators => [ BASE_OPERATOR.contains, ...OPERATORS.parenthesis([{ key }, BASE_OPERATOR.comma, BASE_OPERATOR.space, { value, key }]), ], - /** $K IN ($V, $V, $V) */ + /** + * Creates an IN operator: $K IN ($V, $V, $V) + * + * @param key - The attribute name + * @param values - Array of values to check against + * @returns Operators for IN comparison + */ in: (key: string, values: unknown[]): Operators => [ { key }, BASE_OPERATOR.space, @@ -735,7 +853,14 @@ export const OPERATORS = { ), BASE_OPERATOR.rightParenthesis, ], - /** $K BETWEEN $V AND $V */ + /** + * Creates a BETWEEN operator: $K BETWEEN $V AND $V + * + * @param key - The attribute name + * @param value1 - The lower bound value + * @param value2 - The upper bound value + * @returns Operators for BETWEEN comparison + */ between: (key: string, value1: unknown, value2: unknown): Operators => [ { key }, BASE_OPERATOR.space, @@ -747,48 +872,119 @@ export const OPERATORS = { BASE_OPERATOR.space, { value: value2, key }, ], - /** attribute_type($K, $V) */ + /** + * Creates an attribute_type function: attribute_type($K, $V) + * + * @param key - The attribute name + * @param value - The expected attribute type + * @returns Operators for attribute_type function + */ attributeType: (key: string, value: unknown): Operators => [ BASE_OPERATOR.attributeType, ...OPERATORS.parenthesis([{ key }, BASE_OPERATOR.comma, BASE_OPERATOR.space, { value, key }]), ], - /** begins_with($K, $V) */ + /** + * Creates a begins_with function: begins_with($K, $V) + * + * @param key - The attribute name + * @param value - The prefix to check for + * @returns Operators for begins_with function + */ beginsWith: (key: string, value: unknown): Operators => [ BASE_OPERATOR.beginsWith, ...OPERATORS.parenthesis([{ key }, BASE_OPERATOR.comma, BASE_OPERATOR.space, { value, key }]), ], - /** attribute_not_exists($K) */ + /** + * Creates an attribute_not_exists function: attribute_not_exists($K) + * + * @param key - The attribute name to check for non-existence + * @returns Operators for attribute_not_exists function + */ attributeNotExists: (key: string): Operators => [ BASE_OPERATOR.attributeNotExists, ...OPERATORS.parenthesis([{ key }]), ], - /** NOT contains($K, $V) */ + /** + * Creates a NOT contains function: NOT contains($K, $V) + * + * @param key - The attribute name + * @param value - The value to check if the attribute does not contain + * @returns Operators for NOT contains function + */ notContains: (key: string, value: unknown): Operators => [ BASE_OPERATOR.not, BASE_OPERATOR.space, ...OPERATORS.contains(key, value), ], - /** NOT ($K IN $V, $V, $V) */ + /** + * Creates a NOT IN operator: NOT ($K IN $V, $V, $V) + * + * @param key - The attribute name + * @param values - Array of values to check against + * @returns Operators for NOT IN comparison + */ notIn: (key: string, values: unknown[]): Operators => [ BASE_OPERATOR.not, BASE_OPERATOR.space, ...OPERATORS.parenthesis(OPERATORS.in(key, values)), ], - /** NOT $K = $V */ + /** + * Creates a NOT equal comparison: NOT $K = $V (equivalent to $K <> $V) + * + * @param key - The attribute name + * @param value - The value to compare against + * @returns Operators for NOT equal comparison + */ notEq: (key: string, value: unknown): Operators => OPERATORS.ne(key, value), - /** NOT $K <> $V */ + /** + * Creates a NOT not equal comparison: NOT $K <> $V (equivalent to $K = $V) + * + * @param key - The attribute name + * @param value - The value to compare against + * @returns Operators for NOT not equal comparison + */ notNe: (key: string, value: unknown): Operators => OPERATORS.eq(key, value), - /** NOT $K < $V */ + /** + * Creates a NOT less than comparison: NOT $K < $V (equivalent to $K >= $V) + * + * @param key - The attribute name + * @param value - The value to compare against + * @returns Operators for NOT less than comparison + */ notLt: (key: string, value: unknown): Operators => OPERATORS.ge(key, value), - /** NOT $K <= $V */ + /** + * Creates a NOT less than or equal comparison: NOT $K <= $V (equivalent to $K > $V) + * + * @param key - The attribute name + * @param value - The value to compare against + * @returns Operators for NOT less than or equal comparison + */ notLe: (key: string, value: unknown): Operators => OPERATORS.gt(key, value), - /** NOT $K > $V */ + /** + * Creates a NOT greater than comparison: NOT $K > $V (equivalent to $K <= $V) + * + * @param key - The attribute name + * @param value - The value to compare against + * @returns Operators for NOT greater than comparison + */ notGt: (key: string, value: unknown): Operators => OPERATORS.le(key, value), - /** NOT $K >= $V */ + /** + * Creates a NOT greater than or equal comparison: NOT $K >= $V (equivalent to $K < $V) + * + * @param key - The attribute name + * @param value - The value to compare against + * @returns Operators for NOT greater than or equal comparison + */ notGe: (key: string, value: unknown): Operators => OPERATORS.lt(key, value), - /** size($K) = $V */ + /** + * Creates a size function with equality: size($K) = $V + * + * @param key - The attribute name + * @param value - The size value to compare against + * @returns Operators for size equality comparison + */ sizeEq: (key: string, value: unknown): Operators => [ BASE_OPERATOR.size, ...OPERATORS.parenthesis([{ key }]), @@ -797,7 +993,13 @@ export const OPERATORS = { BASE_OPERATOR.space, { value, key }, ], - /** size($K) <> $V */ + /** + * Creates a size function with not equal: size($K) <> $V + * + * @param key - The attribute name + * @param value - The size value to compare against + * @returns Operators for size not equal comparison + */ sizeNe: (key: string, value: unknown): Operators => [ BASE_OPERATOR.size, ...OPERATORS.parenthesis([{ key }]), @@ -806,7 +1008,13 @@ export const OPERATORS = { BASE_OPERATOR.space, { value, key }, ], - /** size($K) < $V */ + /** + * Creates a size function with less than: size($K) < $V + * + * @param key - The attribute name + * @param value - The size value to compare against + * @returns Operators for size less than comparison + */ sizeLt: (key: string, value: unknown): Operators => [ BASE_OPERATOR.size, ...OPERATORS.parenthesis([{ key }]), @@ -815,7 +1023,13 @@ export const OPERATORS = { BASE_OPERATOR.space, { value, key }, ], - /** size($K) <= $V */ + /** + * Creates a size function with less than or equal: size($K) <= $V + * + * @param key - The attribute name + * @param value - The size value to compare against + * @returns Operators for size less than or equal comparison + */ sizeLe: (key: string, value: unknown): Operators => [ BASE_OPERATOR.size, ...OPERATORS.parenthesis([{ key }]), @@ -824,7 +1038,13 @@ export const OPERATORS = { BASE_OPERATOR.space, { value, key }, ], - /** size($K) > $V */ + /** + * Creates a size function with greater than: size($K) > $V + * + * @param key - The attribute name + * @param value - The size value to compare against + * @returns Operators for size greater than comparison + */ sizeGt: (key: string, value: unknown): Operators => [ BASE_OPERATOR.size, ...OPERATORS.parenthesis([{ key }]), @@ -833,7 +1053,13 @@ export const OPERATORS = { BASE_OPERATOR.space, { value, key }, ], - /** size($K) >= $V */ + /** + * Creates a size function with greater than or equal: size($K) >= $V + * + * @param key - The attribute name + * @param value - The size value to compare against + * @returns Operators for size greater than or equal comparison + */ sizeGe: (key: string, value: unknown): Operators => [ BASE_OPERATOR.size, ...OPERATORS.parenthesis([{ key }]), @@ -842,7 +1068,15 @@ export const OPERATORS = { BASE_OPERATOR.space, { value, key }, ], - /** attribute_exists(Id) AND attribute_not_exists(Id) */ + /** + * Creates an impossible condition: attribute_exists($K) AND attribute_not_exists($K) + * + * This creates a condition that can never be true, useful for testing + * or creating conditions that should always fail. + * + * @param key - The attribute name + * @returns Operators for impossible condition + */ impossibleCondition: (key: string): Operators => [ ...OPERATORS.attributeExists(key), BASE_OPERATOR.space, @@ -852,8 +1086,20 @@ export const OPERATORS = { ], }; +/** + * Update operators for building DynamoDB update expressions. + * + * These operators construct update expressions for DynamoDB UpdateItem + * operations, including SET, ADD, REMOVE, and DELETE actions. + */ export const UPDATE_OPERATORS = { - /** $K = $V */ + /** + * Creates a SET operation: $K = $V + * + * @param key - The attribute name to set + * @param value - The value to set + * @returns Operators for SET operation + */ set: (key: string, value: unknown): Operators => [ { key }, BASE_OPERATOR.space, @@ -861,7 +1107,13 @@ export const UPDATE_OPERATORS = { BASE_OPERATOR.space, { value, key }, ], - /** $K = if_not_exists($K, $V) */ + /** + * Creates a SET operation with if_not_exists: $K = if_not_exists($K, $V) + * + * @param key - The attribute name to set + * @param value - The default value if attribute doesn't exist + * @returns Operators for SET if_not_exists operation + */ setIfNotExists: (key: string, value: unknown): Operators => [ { key }, BASE_OPERATOR.space, @@ -870,7 +1122,13 @@ export const UPDATE_OPERATORS = { BASE_OPERATOR.ifNotExists, ...OPERATORS.parenthesis([{ key }, BASE_OPERATOR.comma, BASE_OPERATOR.space, { value, key }]), ], - /** $K = list_append($K, $V) */ + /** + * Creates a SET operation with list_append: $K = list_append($K, $V) + * + * @param key - The list attribute name + * @param value - The list to append + * @returns Operators for SET list_append operation + */ listAppend: (key: string, value: unknown): Operators => [ { key }, BASE_OPERATOR.space, @@ -879,7 +1137,13 @@ export const UPDATE_OPERATORS = { BASE_OPERATOR.listAppend, ...OPERATORS.parenthesis([{ key }, BASE_OPERATOR.comma, BASE_OPERATOR.space, { value, key }]), ], - /** $K = $K + $V */ + /** + * Creates a SET operation with increment: $K = $K + $V + * + * @param key - The numeric attribute name + * @param value - The value to add + * @returns Operators for SET increment operation + */ increment: (key: string, value: unknown): Operators => [ { key }, BASE_OPERATOR.space, @@ -891,7 +1155,13 @@ export const UPDATE_OPERATORS = { BASE_OPERATOR.space, { value, key }, ], - /** $K = $K - $V */ + /** + * Creates a SET operation with decrement: $K = $K - $V + * + * @param key - The numeric attribute name + * @param value - The value to subtract + * @returns Operators for SET decrement operation + */ decrement: (key: string, value: unknown): Operators => [ { key }, BASE_OPERATOR.space, @@ -903,10 +1173,27 @@ export const UPDATE_OPERATORS = { BASE_OPERATOR.space, { value, key }, ], - /** $K $V */ + /** + * Creates an ADD operation: $K $V + * + * @param key - The attribute name + * @param value - The value to add (for numbers or sets) + * @returns Operators for ADD operation + */ add: (key: string, value: unknown): Operators => [{ key }, BASE_OPERATOR.space, { value, key }], - /** $K $V */ + /** + * Creates a DELETE operation: $K $V + * + * @param key - The set attribute name + * @param value - The set elements to delete + * @returns Operators for DELETE operation + */ delete: (key: string, value: unknown): Operators => [{ key }, BASE_OPERATOR.space, { value, key }], - /** $K */ + /** + * Creates a REMOVE operation: $K + * + * @param key - The attribute name to remove + * @returns Operators for REMOVE operation + */ remove: (key: string): Operators => [{ key }], }; diff --git a/lib/utils/types.ts b/lib/utils/types.ts index 418d23c..9352854 100644 --- a/lib/utils/types.ts +++ b/lib/utils/types.ts @@ -1,44 +1,112 @@ import type { AttributeValue } from '@aws-sdk/client-dynamodb'; -// Common +/** + * Common utility types for Dynamode operations. + */ +/** + * DynamoDB attribute values mapping. + */ export type AttributeValues = Record; + +/** + * DynamoDB attribute names mapping. + */ export type AttributeNames = Record; + +/** + * Generic object type for flexible data structures. + */ export type GenericObject = Record; -// Flatten entity +/** + * Entity flattening utility types. + */ + +/** + * Flattens a nested object structure into a single level with dot notation keys. + * + * @template TValue - The type to flatten + * @example + * ```typescript + * type Nested = { + * user: { + * name: string; + * address: { + * city: string; + * }; + * }; + * }; + * + * type Flattened = FlattenObject; + * // Result: { + * // 'user.name': string; + * // 'user.address.city': string; + * // } + * ``` + */ export type FlattenObject = CollapseEntries>; +/** Internal entry type for flattening operations */ type Entry = { key: string; value: unknown }; +/** Empty entry type for terminal values */ type EmptyEntry = { key: ''; value: TValue }; +/** Types that are excluded from flattening */ type ExcludedTypes = Date | Set | Map | Uint8Array; +/** Array index encoder type */ type ArrayEncoder = `[${bigint}]`; +/** Escapes array keys in dot notation */ type EscapeArrayKey = TKey extends `${infer TKeyBefore}.${ArrayEncoder}${infer TKeyAfter}` ? EscapeArrayKey<`${TKeyBefore}${ArrayEncoder}${TKeyAfter}`> : TKey; -// Transforms entries to one flattened type +/** + * Transforms entries to one flattened type. + * + * @template TEntry - The entry type to collapse + */ type CollapseEntries = { [E in TEntry as EscapeArrayKey]: E['value']; }; -// Transforms array type to object +/** + * Transforms array type to object. + * + * @template TValue - The value type + * @template TValueInitial - The initial value type for recursion control + */ type CreateArrayEntry = OmitItself< TValue extends unknown[] ? { [k: ArrayEncoder]: TValue[number] } : TValue, TValueInitial >; -// Omit the type that references itself +/** + * Omit the type that references itself to prevent infinite recursion. + * + * @template TValue - The value type + * @template TValueInitial - The initial value type for recursion control + */ type OmitItself = TValue extends TValueInitial ? EmptyEntry : OmitExcludedTypes; -// Omit the type that is listed in ExcludedTypes union +/** + * Omit the type that is listed in ExcludedTypes union. + * + * @template TValue - The value type + * @template TValueInitial - The initial value type for recursion control + */ type OmitExcludedTypes = TValue extends ExcludedTypes ? EmptyEntry : CreateObjectEntries; +/** + * Creates object entries for flattening operations. + * + * @template TValue - The value type + * @template TValueInitial - The initial value type for recursion control + */ type CreateObjectEntries = TValue extends object ? { // Checks that Key is of type string @@ -66,9 +134,20 @@ type CreateObjectEntries = TValue extends object }[keyof TValue] // Builds entry for each key : EmptyEntry; -// Narrow utility +/** + * Narrow utility types. + */ +/** Types that can be narrowed */ type Narrowable = string | number | bigint | boolean; + +/** + * Narrow utility type for const assertions. + * + * This utility type helps with const assertions and type narrowing. + * + * @template T - The type to narrow + */ export type Narrow = | (T extends Narrowable ? T : never) | {