diff --git a/README.md b/README.md index 10243cc..2fec6df 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,6 @@ -![Arcaelas Insiders](https://raw.githubusercontent.com/arcaelas/dist/main/banner/svg/dark.svg#gh-dark-mode-only) -![Arcaelas Insiders](https://raw.githubusercontent.com/arcaelas/dist/main/banner/svg/light.svg#gh-light-mode-only) - -# @arcaelas/dynamite - -> **A modern, decorator-first ORM for DynamoDB with TypeScript support** -> Full-featured • Type-safe • Relationship support • Auto table creation • Zero boilerplate +

+ Dynamite ORM - Arcaelas Insiders for DynamoDB +

npm @@ -14,46 +10,17 @@ TypeScript

---- +# @arcaelas/dynamite -## 📚 Table of Contents - -- [🚀 Quick Start](#-quick-start) -- [📦 Installation](#-installation) -- [⚡ Basic Usage](#-basic-usage) -- [🎯 Decorators Reference](#-decorators-reference) -- [🔍 Query Operations](#-query-operations) -- [🔗 Relationships](#-relationships) -- [📝 TypeScript Types](#-typescript-types) -- [🛠️ Advanced Features](#-advanced-features) -- [⚙️ Configuration](#-configuration) -- [📖 API Reference](#-api-reference) -- [🔧 Development Setup](#-development-setup) -- [❓ Troubleshooting](#-troubleshooting) +> **A modern, decorator-first ORM for DynamoDB with TypeScript support** +> Full-featured | Type-safe | Relationships | Auto table creation | Transactions --- -## 🚀 Quick Start +## Quick Start ```typescript -import { - Table, - PrimaryKey, - Default, - CreatedAt, - UpdatedAt, - CreationOptional, - NonAttribute -} from "@arcaelas/dynamite"; -import { Dynamite } from "@arcaelas/dynamite"; - -// Configure connection -Dynamite.config({ - region: "us-east-1", - // For local development - endpoint: "http://localhost:8000", - credentials: { accessKeyId: "test", secretAccessKey: "test" } -}); +import { Dynamite, Table, PrimaryKey, Default, CreatedAt, UpdatedAt, CreationOptional } from "@arcaelas/dynamite"; // Define your model class User extends Table { @@ -61,1212 +28,434 @@ class User extends Table { @Default(() => crypto.randomUUID()) declare id: CreationOptional; - @Default(() => "") - declare name: CreationOptional; + declare name: string; + declare email: string; @Default(() => "customer") declare role: CreationOptional; @CreatedAt() - declare createdAt: CreationOptional; - - @UpdatedAt() - declare updatedAt: CreationOptional; - - // Computed property (not stored in database) - declare displayName: NonAttribute; - - constructor(data?: any) { - super(data); - - // Define computed property - Object.defineProperty(this, 'displayName', { - get: () => `${this.name} (${this.role})`, - enumerable: true - }); - } + declare created_at: CreationOptional; + + @UpdatedAt() + declare updated_at: CreationOptional; } -// Use it! -const user = await User.create({ - name: "John Doe" - // id, role, createdAt, updatedAt are optional (CreationOptional) +// Connect to DynamoDB +const dynamite = new Dynamite({ + region: "us-east-1", + endpoint: "http://localhost:8000", // DynamoDB Local + credentials: { accessKeyId: "test", secretAccessKey: "test" }, + tables: [User] }); -console.log(user.name); // "John Doe" -console.log(user.role); // "customer" -console.log(user.displayName); // "John Doe (customer)" -console.log(user.createdAt); // "2023-12-01T10:30:00.000Z" +await dynamite.connect(); + +// Use it! +const user = await User.create({ name: "John Doe", email: "john@example.com" }); +console.log(user.id); // "a1b2c3d4-..." +console.log(user.role); // "customer" +console.log(user.created_at); // "2025-01-15T10:30:00.000Z" ``` --- -## 📦 Installation +## Installation ```bash npm install @arcaelas/dynamite - -# Peer dependencies (if not already installed) -npm install @aws-sdk/client-dynamodb @aws-sdk/lib-dynamodb ``` --- -## ⚡ Basic Usage +## Decorators -### Table Definition +### Index Decorators -```typescript -import { - Table, - PrimaryKey, - Default, - Validate, - Mutate, - NotNull, - Name -} from "@arcaelas/dynamite"; +| Decorator | Description | +|-----------|-------------| +| `@PrimaryKey()` | Primary key (partition key) | +| `@Index()` | Partition key for GSI | +| `@IndexSort()` | Sort key | -@Name("custom_users") // Override table name -class User extends Table { - @PrimaryKey() - declare id: string; +### Data Decorators - @NotNull() - @Mutate((value) => (value as string).toLowerCase().trim()) - @Validate((value) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value as string) || "Invalid email") - declare email: string; +| Decorator | Description | +|-----------|-------------| +| `@Default(value \| fn)` | Default value (static or dynamic) | +| `@Mutate(fn)` | Transform value before save | +| `@Validate(fn)` | Validate value before save | +| `@Serialize(fromDB, toDB)` | Bidirectional transformation | +| `@NotNull()` | Required field validation | +| `@Name("custom")` | Custom column/table name | +| `@Column()` | Column configuration | - @Default(() => "") - declare name: string; +### Timestamp Decorators - @Default(() => 18) - @Validate((value) => (value as number) >= 0 || "Age must be positive") - declare age: number; +| Decorator | Description | +|-----------|-------------| +| `@CreatedAt()` | Auto-set on creation | +| `@UpdatedAt()` | Auto-set on every update | +| `@DeleteAt()` | Soft delete timestamp | - @Default(() => true) - declare active: boolean; +### Relationship Decorators - @CreatedAt() - declare createdAt: string; +| Decorator | Description | +|-----------|-------------| +| `@HasMany(() => Model, "foreign_key")` | One-to-many | +| `@HasOne(() => Model, "foreign_key")` | One-to-one | +| `@BelongsTo(() => Model, "local_key")` | Many-to-one | +| `@ManyToMany(() => Model, config)` | Many-to-many with pivot table | - @UpdatedAt() - declare updatedAt: string; -} -``` +--- -### CRUD Operations +## TypeScript Types ```typescript -// CREATE -const user = await User.create({ - id: "user-123", - email: "john@example.com", - name: "John Doe", - age: 25 -}); - -// READ -const allUsers = await User.where({}); -const activeUsers = await User.where({ active: true }); -const userById = await User.first({ id: "user-123" }); - -// UPDATE -await User.update("user-123", { name: "John Smith" }); -// or -user.name = "John Smith"; -await user.save(); - -// DELETE -await User.delete("user-123"); -// or -await user.destroy(); +import { + CreationOptional, // Optional during create(), required after + NonAttribute, // Excluded from database (computed/relations) + InferAttributes, // Extract DB attributes from model + InferRelations, // Extract relations from model + CreateInput, // Input type for create() + UpdateInput, // Input type for update() + WhereOptions, // Query options type + QueryOperator // Available operators +} from "@arcaelas/dynamite"; ``` ---- +### CreationOptional -## 🎯 Decorators Reference +Use for fields that are optional during creation but exist after: -### Core Decorators +```typescript +class User extends Table { + @PrimaryKey() + @Default(() => crypto.randomUUID()) + declare id: CreationOptional; // Optional in create() -| Decorator | Purpose | Example | -|-----------|---------|---------| -| `@PrimaryKey()` | Primary key (partition key) | `@PrimaryKey() declare id: string;` | -| `@Index()` | Partition key (alias for PrimaryKey) | `@Index() declare userId: string;` | -| `@IndexSort()` | Sort key | `@IndexSort() declare timestamp: string;` | -| `@Name("custom")` | Custom column/table name | `@Name("user_email") declare email: string;` | + declare name: string; // Required in create() -### Data Decorators + @CreatedAt() + declare created_at: CreationOptional; // Auto-generated +} +``` -| Decorator | Purpose | Example | -|-----------|---------|---------| -| `@Default(value\|fn)` | Default value | `@Default(() => uuid()) declare id: string;` | -| `@Mutate(fn)` | Transform value | `@Mutate((v) => v.toLowerCase()) declare email: string;` | -| `@Validate(fn)` | Validation function | `@Validate((v) => v.length > 0 \|\| "Required") declare name: string;` | -| `@NotNull()` | Not null validation | `@NotNull() declare email: string;` | +### NonAttribute -### Timestamp Decorators +Use for computed properties and relations (not stored in DB): -| Decorator | Purpose | Example | -|-----------|---------|---------| -| `@CreatedAt()` | Set on creation | `@CreatedAt() declare createdAt: string;` | -| `@UpdatedAt()` | Set on every update | `@UpdatedAt() declare updatedAt: string;` | +```typescript +class User extends Table { + declare first_name: string; + declare last_name: string; -### Relationship Decorators + // Computed property - not stored + declare full_name: NonAttribute; -| Decorator | Purpose | Example | -|-----------|---------|---------| -| `@HasMany(Model, foreignKey)` | One-to-many | `@HasMany(() => Order, "user_id") declare orders: any;` | -| `@BelongsTo(Model, localKey)` | Many-to-one | `@BelongsTo(() => User, "user_id") declare user: any;` | + // Relations - loaded via include + @HasMany(() => Order, "user_id") + declare orders: NonAttribute; +} +``` --- -## 🔍 Query Operations +## Query Operations ### Basic Queries ```typescript -// Get all records +// Get all const users = await User.where({}); // Filter by field -const activeUsers = await User.where({ active: true }); -const johnUsers = await User.where({ name: "John" }); +const admins = await User.where({ role: "admin" }); +const user = await User.where("email", "john@example.com"); -// Get first/last record -const firstUser = await User.first({ active: true }); -const lastUser = await User.last({ active: true }); +// First/Last +const first = await User.first({ active: true }); +const last = await User.last({}); ``` -### Advanced Queries with Operators +### Query Operators ```typescript -// Comparison operators -const adults = await User.where("age", ">=", 18); -const youngAdults = await User.where("age", "<", 30); -const specificAges = await User.where("age", "in", [25, 30, 35]); -const excludeAges = await User.where("age", "not-in", [16, 17]); - -// String operators -const gmailUsers = await User.where("email", "contains", "gmail"); -const usersByPrefix = await User.where("name", "begins-with", "John"); - -// Not equal -const nonAdmins = await User.where("role", "!=", "admin"); +// Comparison +await User.where("age", ">=", 18); +await User.where("age", "<", 65); +await User.where("status", "!=", "banned"); + +// Array membership +await User.where("role", "in", ["admin", "moderator"]); + +// String contains +await User.where("email", "$include", "gmail"); ``` +**Available operators:** `=`, `!=`, `<>`, `<`, `<=`, `>`, `>=`, `in`, `$include` + ### Query Options ```typescript -// Pagination and limiting const users = await User.where({}, { limit: 10, - skip: 20 -}); - -// Sorting -const users = await User.where({}, { - order: "ASC" // or "DESC" -}); - -// Select specific attributes -const users = await User.where({}, { - attributes: ["id", "name", "email"] -}); -``` - -### Method Chaining Alternative - -```typescript -// Using query builder style -const users = await User - .where("age", ">=", 18) - .where("active", true); - -// Complex conditions -const users = await User.where({ - age: 25, - active: true, - role: "customer" + skip: 20, + order: "DESC", + attributes: ["id", "name", "email"], + include: { + orders: { + where: { status: "completed" }, + limit: 5 + } + } }); ``` --- -## 🔗 Relationships +## Relationships -### Defining Relationships +### Defining Relations ```typescript -// User model class User extends Table { @PrimaryKey() declare id: string; @HasMany(() => Order, "user_id") - declare orders: any; + declare orders: NonAttribute; + + @HasOne(() => Profile, "user_id") + declare profile: NonAttribute; - @HasMany(() => Review, "user_id") - declare reviews: any; + @ManyToMany(() => Role, { + pivotTable: "user_roles", + foreignKey: "user_id", + relatedKey: "role_id" + }) + declare roles: NonAttribute; } -// Order model class Order extends Table { @PrimaryKey() declare id: string; - @NotNull() declare user_id: string; @BelongsTo(() => User, "user_id") - declare user: any; - - @HasMany(() => OrderItem, "order_id") - declare items: any; -} - -// OrderItem model -class OrderItem extends Table { - @PrimaryKey() - declare id: string; - - @NotNull() - declare order_id: string; - - @NotNull() - declare product_id: string; - - @BelongsTo(() => Order, "order_id") - declare order: any; - - @BelongsTo(() => Product, "product_id") - declare product: any; + declare user: NonAttribute; } ``` -### Loading Relationships +### Loading Relations ```typescript -// Load with relationships -const usersWithOrders = await User.where({}, { - include: { - orders: {} - } -}); - -// Nested relationships -const usersWithCompleteData = await User.where({}, { - include: { - orders: { - include: { - items: { - include: { - product: {} - } - } - } - } - } -}); - -// Filtered relationships -const usersWithRecentOrders = await User.where({}, { - include: { - orders: { - where: { status: "completed" }, - limit: 5, - order: "DESC" - } - } -}); - -// Relationship with specific attributes -const usersWithOrderSummary = await User.where({}, { +const users = await User.where({}, { include: { - orders: { - attributes: ["id", "total", "status"], - where: { status: "completed" } - } + orders: { where: { status: "completed" } }, + profile: {}, + roles: {} } }); ``` ---- - -## 📝 TypeScript Types - -Dynamite provides essential TypeScript types that are fundamental for proper model definition and type safety. These types help you define optional fields, exclude computed properties, and establish relationships. - -### Core Types - -#### `CreationOptional` - -Marks a field as optional during creation but required in the actual model instance. **Always use for auto-generated fields**: `id` (with @PrimaryKey), `createdAt` (@CreatedAt), `updatedAt` (@UpdatedAt), and any field with @Default decorator. +### ManyToMany Operations ```typescript -import { Table, PrimaryKey, Default, CreatedAt, UpdatedAt, CreationOptional } from "@arcaelas/dynamite"; - -class User extends Table { - // Always CreationOptional - auto-generated ID - @PrimaryKey() - @Default(() => crypto.randomUUID()) - declare id: CreationOptional; +const user = await User.first({ id: "user-1" }); - // Required fields during creation - declare name: string; - declare email: string; +// Attach relation +await user.attach(Role, "role-123"); - // Always CreationOptional - has default value - @Default(() => "customer") - declare role: CreationOptional; +// Detach relation +await user.detach(Role, "role-123"); - // Always CreationOptional - auto-set timestamps - @CreatedAt() - declare createdAt: CreationOptional; - - @UpdatedAt() - declare updatedAt: CreationOptional; -} - -// Usage - TypeScript knows exactly what's required -const user = await User.create({ - name: "John Doe", // Required - email: "john@test.com" // Required - // id, role, createdAt, updatedAt are automatically optional -}); +// Sync relations (replace all) +await user.sync(Role, ["role-1", "role-2", "role-3"]); ``` -**Rule of thumb**: Use `CreationOptional` for: -- `@PrimaryKey()` with `@Default()` → Always optional -- `@CreatedAt()` → Always optional -- `@UpdatedAt()` → Always optional -- Any field with `@Default()` → Always optional +--- -#### `NonAttribute` +## CRUD Operations -Excludes a field from database operations while keeping it in the TypeScript interface. Used for computed properties, getters, or virtual fields. +### Create ```typescript -import { Table, PrimaryKey, NonAttribute } from "@arcaelas/dynamite"; - -class User extends Table { - @PrimaryKey() - declare id: string; - - declare firstName: string; - declare lastName: string; - declare birthDate: string; - - // Computed property - not stored in database - declare fullName: NonAttribute; - declare age: NonAttribute; - - // Getter methods as non-attributes - declare getDisplayName: NonAttribute<() => string>; - - constructor(data?: any) { - super(data); - - // Define computed properties - Object.defineProperty(this, 'fullName', { - get: () => `${this.firstName} ${this.lastName}`, - enumerable: true - }); - - Object.defineProperty(this, 'age', { - get: () => { - const today = new Date(); - const birth = new Date(this.birthDate); - return today.getFullYear() - birth.getFullYear(); - }, - enumerable: true - }); - - Object.defineProperty(this, 'getDisplayName', { - value: () => this.fullName.toUpperCase(), - enumerable: false - }); - } -} - -// Usage const user = await User.create({ - id: "user-1", - firstName: "John", - lastName: "Doe", - birthDate: "1990-01-01" + name: "John Doe", + email: "john@example.com" }); - -console.log(user.fullName); // "John Doe" (not stored in DB) -console.log(user.age); // 34 (computed) -console.log(user.getDisplayName()); // "JOHN DOE" ``` -### Relationship Types - -#### `HasMany` - -Defines a one-to-many relationship where the model can have multiple related instances. +### Read ```typescript -import { Table, PrimaryKey, HasMany, NonAttribute } from "@arcaelas/dynamite"; - -class User extends Table { - @PrimaryKey() - declare id: string; - - declare name: string; - declare email: string; - - // One-to-many: User has many Orders - @HasMany(() => Order, "user_id") - declare orders: NonAttribute>; - - // One-to-many: User has many Reviews - @HasMany(() => Review, "user_id") - declare reviews: NonAttribute>; -} - -class Order extends Table { - @PrimaryKey() - declare id: string; - - declare user_id: string; - declare total: number; - declare status: string; -} - -class Review extends Table { - @PrimaryKey() - declare id: string; - - declare user_id: string; - declare rating: number; - declare comment: string; -} - -// Usage -const userWithOrders = await User.where({ id: "user-1" }, { - include: { - orders: { - where: { status: "completed" }, - limit: 10 - }, - reviews: { - where: { rating: { $gte: 4 } } - } - } -}); - -// TypeScript knows these are arrays -console.log(userWithOrders[0].orders.length); // number -console.log(userWithOrders[0].reviews[0].rating); // number +const users = await User.where({ active: true }); +const user = await User.first({ id: "user-123" }); ``` -#### `BelongsTo` - -Defines a many-to-one relationship where the model belongs to a single parent instance. +### Update ```typescript -import { Table, PrimaryKey, BelongsTo, NonAttribute } from "@arcaelas/dynamite"; +// Static update (bulk) +await User.update({ role: "premium" }, { id: "user-123" }); -class Order extends Table { - @PrimaryKey() - declare id: string; - - // Foreign key - @NotNull() - declare user_id: string; - - @NotNull() - declare category_id: string; - - declare total: number; - declare status: string; - - // Many-to-one: Order belongs to User - @BelongsTo(() => User, "user_id") - declare user: NonAttribute>; - - // Many-to-one: Order belongs to Category - @BelongsTo(() => Category, "category_id") - declare category: NonAttribute>; -} - -class User extends Table { - @PrimaryKey() - declare id: string; - - declare name: string; - declare email: string; -} - -class Category extends Table { - @PrimaryKey() - declare id: string; - - declare name: string; - declare description: string; -} - -// Usage -const orderWithRelations = await Order.where({ id: "order-1" }, { - include: { - user: { - attributes: ["id", "name", "email"] - }, - category: {} - } -}); +// Instance update +user.name = "Jane Doe"; +await user.save(); -// TypeScript knows these can be null or the related type -if (orderWithRelations[0].user) { - console.log(orderWithRelations[0].user.name); // string -} -if (orderWithRelations[0].category) { - console.log(orderWithRelations[0].category.name); // string -} +// Or +await user.update({ name: "Jane Doe" }); ``` -### Advanced Type Combinations - -#### Complete Model Example +### Delete ```typescript -import { - Table, - PrimaryKey, - Default, - CreatedAt, - UpdatedAt, - HasMany, - BelongsTo, - CreationOptional, - NonAttribute -} from "@arcaelas/dynamite"; - -class User extends Table { - // Always CreationOptional - auto-generated primary key - @PrimaryKey() - @Default(() => crypto.randomUUID()) - declare id: CreationOptional; - - // Required fields during creation - declare firstName: string; - declare lastName: string; - declare email: string; - - // Always CreationOptional - has default values - @Default(() => "customer") - declare role: CreationOptional; - - @Default(() => true) - declare active: CreationOptional; - - // Always CreationOptional - auto-set timestamps - @CreatedAt() - declare createdAt: CreationOptional; - - @UpdatedAt() - declare updatedAt: CreationOptional; +// Static delete (bulk) +await User.delete({ status: "inactive" }); - // Computed properties (not stored) - declare fullName: NonAttribute; - declare displayRole: NonAttribute; - - // Relationships (not stored directly) - @HasMany(() => Order, "user_id") - declare orders: NonAttribute>; - - @HasMany(() => Review, "user_id") - declare reviews: NonAttribute>; - - constructor(data?: any) { - super(data); - - // Define computed properties - Object.defineProperty(this, 'fullName', { - get: () => `${this.firstName} ${this.lastName}`, - enumerable: true - }); - - Object.defineProperty(this, 'displayRole', { - get: () => this.role.charAt(0).toUpperCase() + this.role.slice(1), - enumerable: true - }); - } -} - -class Order extends Table { - // Always CreationOptional - auto-generated ID - @PrimaryKey() - @Default(() => crypto.randomUUID()) - declare id: CreationOptional; - - // Required field during creation - declare user_id: string; - declare total: number; - - // Always CreationOptional - has default value - @Default(() => "pending") - declare status: CreationOptional; - - // Always CreationOptional - auto-set timestamp - @CreatedAt() - declare createdAt: CreationOptional; - - // Relationship - @BelongsTo(() => User, "user_id") - declare user: NonAttribute>; - - // Computed total with tax - declare totalWithTax: NonAttribute; - - constructor(data?: any) { - super(data); - - Object.defineProperty(this, 'totalWithTax', { - get: () => this.total * 1.1, // 10% tax - enumerable: true - }); - } -} - -// Perfect TypeScript inference -const createUser = async () => { - // TypeScript knows what's required vs optional - const user = await User.create({ - firstName: "John", // required - lastName: "Doe", // required - email: "john@test.com" // required - // id, role, active, createdAt, updatedAt are optional - }); - - // Computed properties work immediately - console.log(user.fullName); // "John Doe" - console.log(user.displayRole); // "Customer" - - return user; -}; - -// Load with relationships -const getUserWithOrders = async (userId: string) => { - const users = await User.where({ id: userId }, { - include: { - orders: { - include: { - user: {} // Recursive relationship - } - } - } - }); - - const user = users[0]; - if (user?.orders?.length > 0) { - console.log(`${user.fullName} has ${user.orders.length} orders`); - user.orders.forEach(order => { - console.log(`Order ${order.id}: $${order.totalWithTax}`); - }); - } - - return user; -}; -``` - -### Type Inference Benefits +// Instance delete (soft delete if @DeleteAt present) +await user.destroy(); -```typescript -// TypeScript will infer all the correct types -type UserCreationAttributes = { - firstName: string; // Required - lastName: string; // Required - email: string; // Required - // All these are automatically optional (CreationOptional): - id?: string; // @PrimaryKey + @Default - role?: string; // @Default - active?: boolean; // @Default - createdAt?: string; // @CreatedAt (always optional) - updatedAt?: string; // @UpdatedAt (always optional) -}; - -type UserAttributes = { - // All these exist in the instance (required after creation) - id: string; // CreationOptional but exists after creation - firstName: string; - lastName: string; - email: string; - role: string; // CreationOptional but exists after creation - active: boolean; // CreationOptional but exists after creation - createdAt: string; // CreationOptional but exists after creation - updatedAt: string; // CreationOptional but exists after creation - fullName: string; // NonAttribute computed property - displayRole: string; // NonAttribute computed property - orders: Order[]; // HasMany relationship (NonAttribute) - reviews: Review[]; // HasMany relationship (NonAttribute) -}; - -// Perfect type safety -const user: UserAttributes = await User.create({ - firstName: "John", - lastName: "Doe", - email: "john@example.com" -} satisfies UserCreationAttributes); +// Force hard delete +await user.forceDestroy(); ``` --- -## 🛠️ Advanced Features - -### Data Validation and Transformation +## Soft Deletes ```typescript -class User extends Table { +class Post extends Table { @PrimaryKey() declare id: string; - // Multiple transformations (executed in order) - @Mutate((value) => (value as string).trim()) - @Mutate((value) => (value as string).toLowerCase()) - @Validate((value) => /^[a-z0-9._%+-]+@[a-z0-9.-]+\.[a-z]{2,}$/.test(value as string) || "Invalid email format") - declare email: string; - - // Complex validation - @Validate((value) => { - const age = value as number; - if (age < 0) return "Age cannot be negative"; - if (age > 150) return "Age seems unrealistic"; - return true; - }) - declare age: number; + declare title: string; - // Multiple validators - @Validate((value) => (value as string).length >= 2 || "Name too short") - @Validate((value) => (value as string).length <= 50 || "Name too long") - @Validate((value) => /^[a-zA-Z\s]+$/.test(value as string) || "Name can only contain letters and spaces") - declare name: string; + @DeleteAt() + declare deleted_at: CreationOptional; } -``` -### Custom Table Names +// Soft delete +await post.destroy(); // Sets deleted_at timestamp -```typescript -// Table name override -@Name("custom_table_name") -class MyModel extends Table { - @PrimaryKey() - declare id: string; - - // Column name override - @Name("custom_column") - declare myField: string; -} -``` +// Query including soft-deleted +const all = await Post.withTrashed({}); -### Complex Queries +// Query only soft-deleted +const trashed = await Post.onlyTrashed({}); -```typescript -// Multiple conditions -const users = await User.where({ - age: 25, - active: true, - role: "premium" -}); - -// Range queries -const users = await User.where("createdAt", ">=", "2023-01-01"); - -// Array filtering -const premiumUsers = await User.where("role", "in", ["admin", "premium", "vip"]); - -// Pattern matching -const testUsers = await User.where("email", "contains", "@test.com"); +// Force hard delete +await post.forceDestroy(); ``` -### Batch Operations +--- + +## Transactions ```typescript -// Batch create -const users = await Promise.all([ - User.create({ id: "1", name: "User 1" }), - User.create({ id: "2", name: "User 2" }), - User.create({ id: "3", name: "User 3" }) -]); - -// Batch update -await Promise.all(users.map(user => { - user.active = false; - return user.save(); -})); +await dynamite.tx(async (tx) => { + const user = await User.create({ name: "John" }, tx); + await Order.create({ user_id: user.id, total: 100 }, tx); + // If any operation fails, all are rolled back +}); ``` --- -## ⚙️ Configuration +## Configuration -### Connection Setup +### DynamoDB Local ```typescript -import { Dynamite } from "@arcaelas/dynamite"; - -// AWS DynamoDB -Dynamite.config({ - region: "us-east-1", - credentials: { - accessKeyId: process.env.AWS_ACCESS_KEY_ID!, - secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY! - } -}); - -// DynamoDB Local -Dynamite.config({ +const dynamite = new Dynamite({ region: "us-east-1", endpoint: "http://localhost:8000", - credentials: { - accessKeyId: "test", - secretAccessKey: "test" - } + credentials: { accessKeyId: "test", secretAccessKey: "test" }, + tables: [User, Order, Product] }); -// With custom configuration -Dynamite.config({ - region: "us-east-1", - endpoint: "https://dynamodb.us-east-1.amazonaws.com", - credentials: { - accessKeyId: "your-key", - secretAccessKey: "your-secret" - }, - maxAttempts: 3, - requestTimeout: 3000 -}); +await dynamite.connect(); ``` -### Environment Variables - -```bash -# .env file -AWS_REGION=us-east-1 -AWS_ACCESS_KEY_ID=your-access-key -AWS_SECRET_ACCESS_KEY=your-secret-key -DYNAMODB_ENDPOINT=http://localhost:8000 # for local development -``` +### AWS DynamoDB ```typescript -// Load from environment -Dynamite.config({ - region: process.env.AWS_REGION!, - endpoint: process.env.DYNAMODB_ENDPOINT, +const dynamite = new Dynamite({ + region: "us-east-1", credentials: { accessKeyId: process.env.AWS_ACCESS_KEY_ID!, secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY! - } + }, + tables: [User, Order, Product] }); + +await dynamite.connect(); ``` -### Docker Setup for Development +### Docker Setup ```bash -# Start DynamoDB Local docker run -d -p 8000:8000 amazon/dynamodb-local - -# Or with Docker Compose -``` - -```yaml -# docker-compose.yml -version: '3.8' -services: - dynamodb-local: - image: amazon/dynamodb-local - ports: - - "8000:8000" - command: ["-jar", "DynamoDBLocal.jar", "-sharedDb", "-dbPath", "/home/dynamodblocal/data/"] - volumes: - - dynamodb_data:/home/dynamodblocal/data - working_dir: /home/dynamodblocal - -volumes: - dynamodb_data: ``` --- -## 📖 API Reference +## API Reference -### Table Class Methods - -#### Static Methods +### Static Methods ```typescript -// CRUD Operations -static async create(data: Partial>): Promise -static async update(id: string, data: Partial>): Promise -static async delete(id: string): Promise - -// Query Methods -static async where(filters?: Partial>, options?: WhereOptions): Promise -static async where(field: keyof InferAttributes, value: any): Promise -static async where(field: keyof InferAttributes, operator: QueryOperator, value: any): Promise - -static async first(filters?: Partial>): Promise -static async last(filters?: Partial>): Promise - -// Utility Methods -static async count(filters?: Partial>): Promise -static async exists(id: string): Promise -``` +// CRUD +static create(data, tx?): Promise +static update(data, filters, tx?): Promise +static delete(filters, tx?): Promise -#### Instance Methods +// Query +static where(filters, options?): Promise +static where(field, value): Promise +static where(field, operator, value): Promise +static first(filters?, options?): Promise +static last(filters?, options?): Promise -```typescript -// CRUD Operations -async save(): Promise -async update(data: Partial>): Promise -async destroy(): Promise -async reload(): Promise - -// Serialization -toJSON(): Record +// Soft deletes +static withTrashed(filters?, options?): Promise +static onlyTrashed(filters?, options?): Promise ``` -### Query Operators - -| Operator | Description | Example | -|----------|-------------|---------| -| `=` | Equal to (default) | `User.where("age", 25)` | -| `!=` | Not equal to | `User.where("status", "!=", "deleted")` | -| `<` | Less than | `User.where("age", "<", 18)` | -| `<=` | Less than or equal | `User.where("age", "<=", 65)` | -| `>` | Greater than | `User.where("score", ">", 100)` | -| `>=` | Greater than or equal | `User.where("age", ">=", 18)` | -| `in` | In array | `User.where("role", "in", ["admin", "user"])` | -| `not-in` | Not in array | `User.where("status", "not-in", ["banned", "deleted"])` | -| `contains` | String contains | `User.where("email", "contains", "gmail")` | -| `begins-with` | String starts with | `User.where("name", "begins-with", "John")` | - -### Type Definitions +### Instance Methods ```typescript -// Core Types - Essential for model definition -type InferAttributes = { - [K in keyof T]: T[K] extends NonAttribute ? never : T[K] -} - -type CreationOptional = T -// Marks fields as optional during creation but required in instances -// ALWAYS use for: @PrimaryKey + @Default, @CreatedAt, @UpdatedAt, any @Default -// Example: @CreatedAt() declare createdAt: CreationOptional - -type NonAttribute = T -// Excludes fields from database operations -// Example: declare fullName: NonAttribute - -// Relationship Types - Define model associations -type HasMany = T[] -// One-to-many relationship: Parent has multiple children -// Example: @HasMany(() => Order, "user_id") declare orders: NonAttribute> - -type BelongsTo = T | null -// Many-to-one relationship: Child belongs to parent -// Example: @BelongsTo(() => User, "user_id") declare user: NonAttribute> - -// Query Types -type QueryOperator = "=" | "!=" | "<" | "<=" | ">" | ">=" | "in" | "not-in" | "contains" | "begins-with" - -type WhereOptions = { - limit?: number; - skip?: number; - order?: "ASC" | "DESC"; - attributes?: (keyof InferAttributes)[]; - include?: { - [K in keyof T]?: T[K] extends NonAttribute | BelongsTo> - ? IncludeOptions | {} - : never; - }; -} - -type IncludeOptions = { - where?: Record; - limit?: number; - order?: "ASC" | "DESC"; - attributes?: string[]; - include?: Record; -} - -// Creation and Update Types -type CreationAttributes = { - [K in keyof InferAttributes]: InferAttributes[K] extends CreationOptional - ? U | undefined - : InferAttributes[K] -} +// CRUD +save(): Promise +update(data): Promise +destroy(): Promise +forceDestroy(): Promise -type UpdateAttributes = Partial> -``` - ---- - -## 🔧 Development Setup - -### Project Structure - -``` -src/ -├── core/ -│ ├── client.ts # Dynamite client configuration -│ ├── table.ts # Base Table class -│ └── wrapper.ts # Metadata management -├── decorators/ -│ ├── index.ts # @Index decorator -│ ├── primary_key.ts # @PrimaryKey decorator -│ ├── default.ts # @Default decorator -│ ├── validate.ts # @Validate decorator -│ ├── mutate.ts # @Mutate decorator -│ ├── created_at.ts # @CreatedAt decorator -│ ├── updated_at.ts # @UpdatedAt decorator -│ ├── not_null.ts # @NotNull decorator -│ ├── name.ts # @Name decorator -│ ├── has_many.ts # @HasMany decorator -│ └── belongs_to.ts # @BelongsTo decorator -├── utils/ -│ ├── relations.ts # Relationship handling -│ ├── naming.ts # Table/column naming -│ └── projection.ts # Field projection -├── @types/ -│ └── index.ts # TypeScript definitions -└── index.ts # Public API exports -``` - -### Running Tests - -```bash -# Start DynamoDB Local -docker run -d -p 8000:8000 amazon/dynamodb-local +// ManyToMany +attach(Model, id, pivotData?): Promise +detach(Model, id): Promise +sync(Model, ids): Promise -# Run tests -npm test - -# Run specific test -npm test -- --testNamePattern="should handle relationships" - -# Run with coverage -npm test -- --coverage -``` - -### Example Test - -```typescript -describe("User Model", () => { - beforeEach(async () => { - // Setup test data - await User.create({ - id: "test-user", - email: "test@example.com", - name: "Test User" - }); - }); - - it("should create user with defaults", async () => { - const user = await User.create({ - id: "user-2", - email: "user2@example.com" - }); - - expect(user.name).toBe(""); - expect(user.active).toBe(true); - expect(user.createdAt).toBeDefined(); - }); - - it("should validate email format", async () => { - await expect(User.create({ - id: "user-3", - email: "invalid-email" - })).rejects.toThrow("Invalid email"); - }); -}); +// Serialization +toJSON(): Record ``` --- -## ❓ Troubleshooting - -### Common Errors - -| Error | Cause | Solution | -|-------|-------|----------| -| `Metadata no encontrada` | Model imported before decorators executed | Ensure `connect()` runs first, avoid circular imports | -| `PartitionKey faltante` | No `@PrimaryKey()` or `@Index()` in model | Add primary key decorator | -| `Two keys can not have the same name` | PK & SK attribute name clash | Use different column names | -| `UnrecognizedClientException` | Wrong credentials or DynamoDB Local not running | Check credentials, start DynamoDB Local | -| `ValidationException` | Invalid attribute names or values | Check for reserved keywords, validate data | - -### Performance Tips +## Documentation -```typescript -// Use attributes to limit returned data -const users = await User.where({}, { - attributes: ["id", "name"] // Only return these fields -}); - -// Use pagination for large datasets -const users = await User.where({}, { - limit: 100, - skip: 0 -}); +For complete documentation, examples, and guides: -// Prefer specific queries over scanning all records -const activeUsers = await User.where({ active: true }); // Good -const allUsers = (await User.where({})).filter(u => u.active); // Bad -``` - -### Debugging - -```typescript -// Enable debug logging (if available) -Dynamite.config({ - region: "us-east-1", - logger: console // Log all DynamoDB operations -}); - -// Log query parameters -const users = await User.where({ active: true }); -console.log("Found users:", users.length); -``` - -### Best Practices - -1. **Always define a primary key** with `@PrimaryKey()` or `@Index()` -2. **Use TypeScript strict mode** for better type safety -3. **Validate user input** with `@Validate()` decorators -4. **Use attributes selection** to limit data transfer -5. **Handle relationships carefully** to avoid N+1 queries -6. **Use transactions** for complex operations (if needed) -7. **Monitor DynamoDB costs** in production +**[arcaelas.github.io/dynamite](https://arcaelas.github.io/dynamite)** --- -## 📄 License +## License MIT License - see [LICENSE](LICENSE) file for details. --- -## 🤝 Contributing - -1. Fork the repository -2. Create a feature branch: `git checkout -b feature/amazing-feature` -3. Make your changes and add tests -4. Ensure tests pass: `npm test` -5. Commit changes: `git commit -m 'feat: add amazing feature'` -6. Push to branch: `git push origin feature/amazing-feature` -7. Open a Pull Request - -### Development Guidelines - -- Follow TypeScript strict mode -- Add tests for new features -- Update documentation -- Use conventional commits -- Ensure backward compatibility - ---- - -**Made with ❤️ by [Miguel Alejandro](https://github.com/arcaelas) - [Arcaelas Insiders](https://github.com/arcaelas)** \ No newline at end of file +**Made with care by [Miguel Alejandro](https://github.com/arcaelas) - [Arcaelas Insiders](https://github.com/arcaelas)** diff --git a/docs/assets/cover.png b/docs/assets/cover.png new file mode 100644 index 0000000..6204796 Binary files /dev/null and b/docs/assets/cover.png differ