# Turbine > Type-safe entity mapping and query library for DynamoDB # Turbine A type-safe entity mapping and query library for DynamoDB. Turbine makes working with DynamoDB simple and type-safe. Define your entities with Zod schemas, and Turbine handles the rest: key generation, validation, queries, and pagination. ## Features - **Type-safe** - Full TypeScript support with Zod schema validation - **Simple API** - Intuitive `defineTable` and `defineEntity` functions - **Flexible keys** - Composite keys, computed values, and automatic joining - **Powerful queries** - Key conditions, filters, and built-in pagination - **Instance methods** - Convenient `update()` on returned objects ## Installation ```bash npm install dynamodb-turbine zod ``` or with yarn: ```bash yarn add dynamodb-turbine zod ``` ## Quick Example ```typescript import { defineTable, defineEntity } from "dynamodb-turbine"; import { z } from "zod"; // Define your table const table = defineTable({ name: "my-app-table", indexes: { table: { hashKey: "pk", rangeKey: "sk" }, gsi1: { hashKey: "type", rangeKey: "createdAt" }, }, }); // Define an entity with a Zod schema const users = defineEntity({ table, schema: z.object({ id: z.string(), email: z.string().email(), name: z.string(), createdAt: z.string().datetime().default(() => new Date().toISOString()), }), keys: { type: () => "user", pk: (u) => ["user", u.id], sk: (u) => ["profile", u.email], }, }); // Create a user const user = await users.put({ id: "123", email: "alice@example.com", name: "Alice", }); // Query users const recentUsers = await users.queryAll({ index: "gsi1", type: "user", createdAt: { beginsWith: "2024" }, }); ``` ## Next Steps - [Quick Start](/quick-start) - Get up and running in 5 minutes - [Tables](/tables) - Learn about table configuration - [Entities](/entities) - Define your data models - [Querying](/querying) - Master DynamoDB queries _LLMs: [go here](/llms.txt) to find the full documentation as a text file._ --- Turbine provides simple methods for Create, Read, Update, and Delete operations. ## Create: put() The `put()` method creates a new item in DynamoDB: ```typescript const user = await users.put({ id: "user-123", email: "alice@example.com", name: "Alice", }); ``` ### Behavior - Validates data against the Zod schema - Applies default values from the schema - Generates all key values - Returns an Instance with the created data ### With Defaults ```typescript const users = defineEntity({ table, schema: z.object({ id: z.string(), email: z.string().email(), role: z.enum(["user", "admin"]).default("user"), createdAt: z.string().datetime().default(() => new Date().toISOString()), }), keys: { /* ... */ }, }); const user = await users.put({ id: "123", email: "alice@example.com", }); console.log(user.role); // "user" (default) console.log(user.createdAt); // "2024-01-15T10:30:00.000Z" (generated) ``` ### Overwrites Existing Items `put()` will overwrite an existing item with the same keys: ```typescript // Creates user await users.put({ id: "123", email: "alice@example.com", name: "Alice" }); // Overwrites the entire item await users.put({ id: "123", email: "alice@example.com", name: "Alice Smith" }); ``` ## Read: get() The `get()` method fetches a single item by its primary key: ```typescript const user = await users.get({ pk: ["user", "123"], sk: ["profile", "alice@example.com"], }); if (user) { console.log(user.name); } ``` ### Key Format Keys must match the format defined in your entity: ```typescript // If your keys are defined as: keys: { pk: (u) => ["user", u.id], sk: (u) => u.email, } // Then get() expects: await users.get({ pk: ["user", "123"], // Array format sk: "alice@example.com", // String format }); ``` ### Returns - Returns the Instance if found - Returns `null` if not found ```typescript const user = await users.get({ pk: ["user", "999"], sk: "missing" }); console.log(user); // null ``` ## Update: update() There are two ways to update items: ### Entity Method Use `entity.update(key, patch)` when you have the keys: ```typescript const updated = await users.update( { pk: ["user", "123"], sk: "alice@example.com" }, { name: "Alice Smith", role: "admin" } ); ``` ### Instance Method Use `instance.update(patch)` for convenience when you have an instance: ```typescript const user = await users.get({ pk: ["user", "123"], sk: "alice@example.com", }); if (user) { await user.update({ name: "Alice Smith" }); } ``` ### Update Behavior - Only updates the specified fields - Validates the patch data against the schema - Returns the updated Instance - Mutates the instance object in-place (for instance method) ```typescript const user = await users.get({ /* ... */ }); console.log(user.name); // "Alice" await user.update({ name: "Alice Smith" }); console.log(user.name); // "Alice Smith" (updated in-place) ``` ### Partial Updates Only the fields you specify are updated: ```typescript // Original: { id: "123", name: "Alice", role: "user", email: "alice@example.com" } await users.update( { pk: ["user", "123"], sk: "alice@example.com" }, { role: "admin" } ); // Result: { id: "123", name: "Alice", role: "admin", email: "alice@example.com" } ``` ## Delete: delete() The `delete()` method removes an item: ```typescript await users.delete({ pk: ["user", "123"], sk: "alice@example.com", }); ``` ### Behavior - Deletes the item if it exists - Does nothing if the item doesn't exist (no error) - Returns `void` ## Error Handling All operations can throw errors: ```typescript import { TurbineError } from "dynamodb-turbine"; try { await users.put({ id: "123" }); // Missing required 'email' } catch (error) { if (error instanceof TurbineError) { console.error("Validation failed:", error.message); } } ``` Common errors: - Schema validation failures - Invalid key configurations - DynamoDB client errors ## Complete Example ```typescript import { defineTable, defineEntity } from "dynamodb-turbine"; import { z } from "zod"; const table = defineTable({ name: "app-data", indexes: { table: { hashKey: "pk", rangeKey: "sk" }, }, }); const users = defineEntity({ table, schema: z.object({ id: z.string(), email: z.string().email(), name: z.string(), role: z.enum(["user", "admin"]).default("user"), createdAt: z.string().datetime().default(() => new Date().toISOString()), updatedAt: z.string().datetime().optional(), }), keys: { pk: (u) => ["user", u.id], sk: (u) => ["profile", u.email], }, }); async function userWorkflow() { // Create const user = await users.put({ id: "user-123", email: "alice@example.com", name: "Alice", }); console.log("Created:", user.id); // Read const fetched = await users.get({ pk: ["user", "user-123"], sk: ["profile", "alice@example.com"], }); console.log("Fetched:", fetched?.name); // Update (entity method) await users.update( { pk: ["user", "user-123"], sk: ["profile", "alice@example.com"] }, { role: "admin", updatedAt: new Date().toISOString() } ); // Update (instance method) const current = await users.get({ pk: ["user", "user-123"], sk: ["profile", "alice@example.com"], }); await current?.update({ name: "Alice Smith" }); // Delete await users.delete({ pk: ["user", "user-123"], sk: ["profile", "alice@example.com"], }); console.log("Deleted"); } ``` ## Next Steps - [Querying](/querying) - Query multiple items - [TypeScript](/typescript) - Type safety with Instance types --- Entities are the core building block of Turbine. They define your data model with a Zod schema and specify how keys are generated. ## Basic Usage ```typescript import { defineEntity } from "dynamodb-turbine"; import { z } from "zod"; const users = defineEntity({ table, schema: z.object({ id: z.string(), email: z.string().email(), name: z.string(), }), keys: { pk: (user) => ["user", user.id], sk: (user) => user.email, }, }); ``` ## Entity Definition The `defineEntity` function accepts: | Property | Type | Required | Description | |----------|------|----------|-------------| | `table` | `Table` | Yes | The table from `defineTable` | | `schema` | `z.ZodObject` | Yes | A Zod object schema | | `keys` | `object` | Yes | Key generation functions | ## Schema Definition Use any Zod object schema to define your entity structure: ```typescript schema: z.object({ // Required fields id: z.string(), email: z.string().email(), // Optional fields name: z.string().optional(), age: z.number().optional(), // Fields with defaults role: z.enum(["user", "admin"]).default("user"), createdAt: z.string().datetime().default(() => new Date().toISOString()), // Complex types tags: z.array(z.string()).default(() => []), metadata: z.record(z.string(), z.unknown()).optional(), // Nested objects address: z.object({ street: z.string(), city: z.string(), country: z.string(), }).optional(), }) ``` ### Supported Zod Types Turbine supports all Zod types that can be serialized to JSON: - `z.string()`, `z.number()`, `z.boolean()` - `z.array()`, `z.object()`, `z.record()` - `z.enum()`, `z.union()`, `z.literal()` - `z.optional()`, `z.nullable()` - `z.default()`, `z.transform()` - Validation methods like `.email()`, `.uuid()`, `.min()`, `.max()` ## Key Definitions Keys determine how items are stored and retrieved in DynamoDB. Each key can be: ### Static Values ```typescript keys: { type: () => "user", } ``` ### Field References ```typescript keys: { pk: (entity) => entity.id, sk: (entity) => entity.email, } ``` ### Composite Keys (Arrays) Arrays are automatically joined with `#`: ```typescript keys: { pk: (user) => ["user", user.id], // becomes "user#" sk: (user) => ["profile", user.email], // becomes "profile#" } ``` ### Computed Values ```typescript keys: { createdAt: (entity) => entity.createdAt || new Date().toISOString(), updatedAt: () => new Date().toISOString(), } ``` ### Example: Full Key Definition ```typescript const posts = defineEntity({ table, schema: z.object({ id: z.string(), authorId: z.string(), title: z.string(), content: z.string(), createdAt: z.string().datetime().default(() => new Date().toISOString()), }), keys: { // Primary key pk: (post) => ["user", post.authorId], sk: (post) => ["post", post.createdAt, post.id], // GSI for querying all posts by date type: () => "post", gsi1pk: () => "post", gsi1sk: (post) => [post.createdAt, post.id], // GSI for fetching post by ID gsi2pk: () => "post#id", gsi2sk: (post) => post.id, }, }); ``` ## Entity Methods The returned entity object provides these methods: ### put(data) Create a new item: ```typescript const user = await users.put({ id: "123", email: "alice@example.com", name: "Alice", }); ``` ### get(key) Fetch a single item: ```typescript const user = await users.get({ pk: ["user", "123"], sk: "alice@example.com", }); ``` ### update(key, patch) Update an existing item: ```typescript const updated = await users.update( { pk: ["user", "123"], sk: "alice@example.com" }, { name: "Alice Smith" } ); ``` ### delete(key) Remove an item: ```typescript await users.delete({ pk: ["user", "123"], sk: "alice@example.com", }); ``` ### query(key, options?) Query with pagination: ```typescript const results = await users.query( { pk: ["user", "123"] }, { Limit: 10 } ); ``` ### queryOne(key, options?) Get the first matching item: ```typescript const user = await users.queryOne({ index: "gsi2", gsi2pk: "user#email", gsi2sk: "alice@example.com", }); ``` ### queryAll(key, options?) Get all matching items (handles pagination): ```typescript const allPosts = await posts.queryAll({ pk: ["user", "123"], }); ``` ## Entity Properties ```typescript const users = defineEntity({ /* ... */ }); users.definition // The original entity definition ``` ## Next Steps - [CRUD Operations](/crud) - Detailed guide to create, read, update, delete - [Querying](/querying) - Advanced query patterns - [TypeScript](/typescript) - Type inference and Instance types --- Turbine provides a custom error class and clear error messages to help you debug issues. ## TurbineError All Turbine-specific errors are instances of `TurbineError`: ```typescript import { TurbineError } from "dynamodb-turbine"; try { await users.put({ id: "123" }); // Missing required field } catch (error) { if (error instanceof TurbineError) { console.error("Turbine error:", error.message); } } ``` ## Common Error Scenarios ### Schema Validation Errors When data doesn't match your Zod schema: ```typescript const users = defineEntity({ table, schema: z.object({ id: z.string(), email: z.string().email(), age: z.number().min(0), }), keys: { /* ... */ }, }); // Missing required field await users.put({ id: "123" }); // Error: Required field 'email' is missing // Invalid email format await users.put({ id: "123", email: "not-an-email" }); // Error: Invalid email // Invalid number await users.put({ id: "123", email: "a@b.com", age: -5 }); // Error: Number must be greater than or equal to 0 ``` ### Invalid Index When querying a non-existent index: ```typescript const table = defineTable({ name: "my-table", indexes: { table: { hashKey: "pk", rangeKey: "sk" }, gsi1: { hashKey: "type" }, }, }); // Querying undefined index await users.query({ index: "gsi2", // Does not exist // ... }); // Error: Index 'gsi2' is not defined ``` ### Missing Key Fields When query keys don't match the index: ```typescript // Index requires 'type' as hash key await users.query({ index: "gsi1", // Missing 'type' field }); // Error: Missing required key 'type' for index 'gsi1' ``` ## Error Handling Patterns ### Try-Catch ```typescript async function createUser(data: UserInput) { try { return await users.put(data); } catch (error) { if (error instanceof TurbineError) { // Handle validation or configuration errors throw new Error(`Invalid user data: ${error.message}`); } // Re-throw other errors (network, permissions, etc.) throw error; } } ``` ### Checking Existence ```typescript async function getUserOrThrow(id: string) { const user = await users.get({ pk: ["user", id], sk: "profile", }); if (!user) { throw new Error(`User not found: ${id}`); } return user; } ``` ### Handling DynamoDB Errors DynamoDB client errors are passed through: ```typescript import { ConditionalCheckFailedException } from "@aws-sdk/client-dynamodb"; try { await users.put(data); } catch (error) { if (error instanceof TurbineError) { // Turbine validation error } else if (error instanceof ConditionalCheckFailedException) { // DynamoDB condition failed } else { // Other errors (network, permissions) } } ``` ## Validation Before Operations Use Zod's parse methods for pre-validation: ```typescript const userSchema = z.object({ id: z.string(), email: z.string().email(), name: z.string(), }); function validateUserInput(data: unknown) { const result = userSchema.safeParse(data); if (!result.success) { // Handle validation errors with detailed info const errors = result.error.issues.map(issue => ({ field: issue.path.join("."), message: issue.message, })); return { success: false, errors }; } return { success: true, data: result.data }; } ``` ## Debugging Tips ### Enable Logging Log operations for debugging: ```typescript async function putWithLogging(entity: Entity, data: T) { console.log("PUT:", JSON.stringify(data, null, 2)); try { const result = await entity.put(data); console.log("SUCCESS:", result); return result; } catch (error) { console.error("ERROR:", error); throw error; } } ``` ### Check Key Values Verify generated keys: ```typescript const users = defineEntity({ table, schema: z.object({ /* ... */ }), keys: { pk: (u) => { const key = ["user", u.id]; console.log("Generated pk:", key); return key; }, sk: (u) => u.email, }, }); ``` ### Inspect DynamoDB Items Check what's actually stored: ```typescript const user = await users.put({ id: "123", email: "alice@example.com", name: "Alice", }); // The instance includes all attributes console.log("Stored item:", { pk: user.pk, // "user#123" sk: user.sk, // "alice@example.com" id: user.id, email: user.email, name: user.name, }); ``` ## Error Reference | Error | Cause | Solution | |-------|-------|----------| | Schema validation failed | Data doesn't match Zod schema | Check required fields, types, and constraints | | Index not defined | Query uses unknown index name | Verify index exists in `defineTable` | | Missing key field | Query missing required key | Include all hash/range keys for the index | | Invalid key value | Key function returned invalid value | Check key functions return strings, numbers, or arrays | ## Next Steps - [Quick Start](/quick-start) - Review the basics - [TypeScript](/typescript) - Leverage type safety to prevent errors --- Effective key design is crucial for DynamoDB performance. Turbine makes it easy to implement common patterns. ## How Keys Work In Turbine, keys are defined as functions that transform your entity data into DynamoDB attributes: ```typescript keys: { pk: (user) => ["user", user.id], // "user#123" sk: (user) => ["profile", user.email], // "profile#alice@example.com" } ``` ### Array Joining Arrays are automatically joined with `#`: ```typescript ["user", "123"] // becomes "user#123" ["post", "2024-01-15", "abc"] // becomes "post#2024-01-15#abc" ``` ### Key Types | Type | Example | Result | |------|---------|--------| | String | `"user"` | `"user"` | | Number | `123` | `123` | | Array | `["user", id]` | `"user#"` | | Function | `() => "static"` | `"static"` | ## Single-Table Design Single-table design stores multiple entity types in one table, using key prefixes to differentiate them. ### Entity Type Prefix ```typescript const users = defineEntity({ table, schema: z.object({ id: z.string(), email: z.string(), name: z.string(), }), keys: { type: () => "user", pk: (u) => ["user", u.id], sk: () => "profile", }, }); const posts = defineEntity({ table, schema: z.object({ id: z.string(), authorId: z.string(), title: z.string(), }), keys: { type: () => "post", pk: (p) => ["user", p.authorId], sk: (p) => ["post", p.id], }, }); ``` ### Query by Entity Type Use a GSI with `type` as the hash key: ```typescript // Get all users const allUsers = await users.queryAll({ index: "gsi1", type: "user", }); // Get all posts const allPosts = await posts.queryAll({ index: "gsi1", type: "post", }); ``` ## Common Access Patterns ### One-to-Many Relationships Store related items under the same partition key: ```typescript // User owns posts const posts = defineEntity({ table, schema: z.object({ id: z.string(), authorId: z.string(), title: z.string(), createdAt: z.string().datetime(), }), keys: { pk: (p) => ["user", p.authorId], sk: (p) => ["post", p.createdAt, p.id], }, }); // Get all posts by user, sorted by date const userPosts = await posts.queryAll({ pk: ["user", userId], sk: { beginsWith: "post#" }, }); ``` ### Lookup by ID Create a GSI for direct ID lookups: ```typescript const posts = defineEntity({ table, schema: z.object({ id: z.string(), authorId: z.string(), title: z.string(), }), keys: { pk: (p) => ["user", p.authorId], sk: (p) => ["post", p.id], // GSI for ID lookup gsi1pk: () => "post#id", gsi1sk: (p) => p.id, }, }); // Fetch by ID (when you don't know the author) const post = await posts.queryOne({ index: "gsi1", gsi1pk: "post#id", gsi1sk: postId, }); ``` ### Unique Constraints Use a GSI to enforce uniqueness: ```typescript const users = defineEntity({ table, schema: z.object({ id: z.string(), email: z.string().email(), username: z.string(), }), keys: { pk: (u) => ["user", u.id], sk: () => "profile", // GSI for email lookup (unique) gsi1pk: () => "user#email", gsi1sk: (u) => u.email, // GSI for username lookup (unique) gsi2pk: () => "user#username", gsi2sk: (u) => u.username, }, }); // Check if email exists const existing = await users.queryOne({ index: "gsi1", gsi1pk: "user#email", gsi1sk: email, }); ``` ### Timeline / Feed Sort items by timestamp: ```typescript const posts = defineEntity({ table, schema: z.object({ id: z.string(), authorId: z.string(), title: z.string(), createdAt: z.string().datetime(), }), keys: { pk: (p) => ["user", p.authorId], sk: (p) => ["post", p.createdAt, p.id], // Global feed GSI type: () => "post", gsi1pk: () => "post#feed", gsi1sk: (p) => [p.createdAt, p.id], }, }); // User's posts (newest first) const userPosts = await posts.query( { pk: ["user", userId], sk: { beginsWith: "post#" } }, { ScanIndexForward: false, Limit: 20 } ); // Global feed (newest first) const feed = await posts.query( { index: "gsi1", gsi1pk: "post#feed" }, { ScanIndexForward: false, Limit: 20 } ); ``` ### Hierarchical Data Model hierarchies with composite keys: ```typescript // Organization > Team > Member const members = defineEntity({ table, schema: z.object({ orgId: z.string(), teamId: z.string(), userId: z.string(), role: z.string(), }), keys: { pk: (m) => ["org", m.orgId], sk: (m) => ["team", m.teamId, "member", m.userId], }, }); // All members in an org await members.queryAll({ pk: ["org", orgId], sk: { beginsWith: "team#" }, }); // All members in a specific team await members.queryAll({ pk: ["org", orgId], sk: { beginsWith: ["team", teamId, "member"] }, }); ``` ## Key Design Tips ### Use Consistent Prefixes Prefix partition keys with entity type for clarity: ```typescript // Good: Clear prefixes pk: (u) => ["user", u.id] pk: (p) => ["post", p.id] pk: (o) => ["order", o.id] // Avoid: Ambiguous keys pk: (u) => u.id ``` ### Include Timestamp for Sorting When you need chronological ordering, include timestamps in sort keys: ```typescript // Good: Sortable by time sk: (p) => [p.createdAt, p.id] // The ID at the end ensures uniqueness if timestamps collide ``` ### Use ISO Timestamps ISO format strings sort correctly as strings: ```typescript createdAt: z.string().datetime().default(() => new Date().toISOString()) // Results: "2024-01-15T10:30:00.000Z" sorts before "2024-02-01T08:00:00.000Z" ``` ### Plan Your GSIs Design GSIs based on your access patterns: | Access Pattern | GSI Design | |----------------|------------| | Get all entities of a type | `hashKey: type` | | Lookup by unique field | `hashKey: "entity#field", rangeKey: value` | | Timeline/feed | `hashKey: "entity#feed", rangeKey: timestamp` | | Filter by status | `hashKey: status, rangeKey: timestamp` | ## Example: Complete Entity Design ```typescript const posts = defineEntity({ table, schema: z.object({ id: z.string().uuid(), authorId: z.string(), title: z.string(), content: z.string(), status: z.enum(["draft", "published", "archived"]), createdAt: z.string().datetime().default(() => new Date().toISOString()), updatedAt: z.string().datetime().optional(), }), keys: { // Primary: User's posts sorted by date pk: (p) => ["user", p.authorId], sk: (p) => ["post", p.createdAt, p.id], // GSI1: All posts by type type: () => "post", // GSI2: Global feed gsi1pk: () => "post#feed", gsi1sk: (p) => [p.createdAt, p.id], // GSI3: Lookup by ID gsi2pk: () => "post#id", gsi2sk: (p) => p.id, // GSI4: Posts by status gsi3pk: (p) => ["post#status", p.status], gsi3sk: (p) => [p.createdAt, p.id], }, }); // Access patterns enabled: // 1. Get user's posts: query pk=user# // 2. Get all posts: query gsi1 type=post // 3. Global feed: query gsi2 gsi1pk=post#feed // 4. Lookup by ID: query gsi3 gsi2pk=post#id, gsi2sk= // 5. Posts by status: query gsi4 gsi3pk=post#status#published ``` ## Next Steps - [Querying](/querying) - Use your keys effectively - [Error Handling](/errors) - Handle key-related errors --- Turbine provides powerful query methods for retrieving multiple items from DynamoDB. ## Query Methods ### query() Returns paginated results: ```typescript const results = await users.query({ pk: ["user", "123"], }); console.log(results); // Array of Instance objects console.log(results.length); // Number of items in this page ``` ### queryOne() Returns the first matching item: ```typescript const user = await users.queryOne({ index: "byEmail", email: "alice@example.com", }); if (user) { console.log(user.name); } ``` ### queryAll() Fetches all pages automatically: ```typescript const allUsers = await users.queryAll({ type: "user", }); // Returns all matching items, handling pagination internally console.log(allUsers.length); ``` ## Key Conditions ### Hash Key (Required) Every query must specify a hash key with an equality condition: ```typescript // Simple value { pk: "user#123" } // Array (joined with #) { pk: ["user", "123"] } // Explicit equals { pk: { equals: "user#123" } } ``` ### Range Key (Optional) Range keys support various operators: ```typescript // Equality { sk: "profile" } { sk: { equals: "profile" } } // Prefix matching { sk: { beginsWith: "post#2024" } } // Comparisons { sk: { greaterThan: 100 } } { sk: { lessThan: 200 } } { sk: { greaterThanOrEquals: 100 } } { sk: { lessThanOrEquals: 200 } } // Range { sk: { between: ["2024-01-01", "2024-12-31"] } } ``` ## Index Selection Use the `index` property to query a GSI: ```typescript // Query the primary table index (default) await posts.query({ pk: ["user", "123"], }); // Query a GSI await posts.query({ index: "gsi1", gsi1pk: "post", gsi1sk: { beginsWith: "2024-01" }, }); ``` > The index name must match one defined in your `defineTable` call. ## Filter Expressions Filters narrow results after the key conditions are applied: ```typescript const activePosts = await posts.query( { pk: ["user", "123"] }, { filters: { status: "published", deletedAt: { notExists: true }, }, } ); ``` ### Filter Operators ```typescript filters: { // Equality status: "active", status: { equals: "active" }, status: { notEquals: "deleted" }, // Comparisons count: { greaterThan: 10 }, count: { lessThan: 100 }, count: { greaterThanOrEquals: 10 }, count: { lessThanOrEquals: 100 }, // Range count: { between: [10, 100] }, // String operations title: { beginsWith: "Hello" }, title: { contains: "world" }, title: { notContains: "spam" }, // Existence checks deletedAt: { exists: true }, deletedAt: { notExists: true }, } ``` ## Pagination ### Manual Pagination Use `query()` for manual control: ```typescript // First page const page1 = await users.query( { type: "user" }, { Limit: 10 } ); console.log(page1.length); // Up to 10 items console.log(page1.lastEvaluatedKey); // Cursor for next page // Next page using the convenience function if (page1.next) { const page2 = await page1.next(); } // Or manually with ExclusiveStartKey if (page1.lastEvaluatedKey) { const page2 = await users.query( { type: "user" }, { Limit: 10, ExclusiveStartKey: page1.lastEvaluatedKey } ); } ``` ### Automatic Pagination Use `queryAll()` to fetch everything: ```typescript const allUsers = await users.queryAll({ type: "user", }); // Handles all pagination internally console.log(allUsers.length); ``` > Be careful with `queryAll()` on large datasets as it fetches everything. ## Query Options Pass DynamoDB query options as the second argument: ```typescript const results = await posts.query( { pk: ["user", "123"] }, { // Pagination Limit: 20, ExclusiveStartKey: lastKey, // Sort order (false = descending) ScanIndexForward: false, // Consistency ConsistentRead: true, // Filters filters: { status: "published", }, } ); ``` ### Common Options | Option | Type | Description | |--------|------|-------------| | `Limit` | `number` | Maximum items to return | | `ExclusiveStartKey` | `object` | Start position for pagination | | `ScanIndexForward` | `boolean` | `true` = ascending, `false` = descending | | `ConsistentRead` | `boolean` | Use strongly consistent reads | | `filters` | `object` | Filter expressions | ## Examples ### Get Latest Posts ```typescript const latestPosts = await posts.query( { index: "gsi1", gsi1pk: "post", gsi1sk: { beginsWith: "2024" }, }, { Limit: 10, ScanIndexForward: false, // Newest first } ); ``` ### Find User by Email ```typescript const user = await users.queryOne({ index: "byEmail", email: "alice@example.com", }); ``` ### Get All User's Active Posts ```typescript const activePosts = await posts.queryAll( { pk: ["user", userId] }, { filters: { status: "published", deletedAt: { notExists: true }, }, } ); ``` ### Paginated Feed ```typescript async function getFeed(cursor?: Record) { const page = await posts.query( { index: "gsi1", gsi1pk: "post", }, { Limit: 20, ScanIndexForward: false, ExclusiveStartKey: cursor, filters: { status: "published", }, } ); return { items: page, nextCursor: page.lastEvaluatedKey, hasMore: !!page.next, }; } ``` ### Date Range Query ```typescript const postsInJanuary = await posts.queryAll({ index: "gsi1", gsi1pk: "post", gsi1sk: { between: ["2024-01-01", "2024-01-31"] }, }); ``` ## Next Steps - [Key Patterns](/key-patterns) - Design effective key structures - [TypeScript](/typescript) - Type safety with queries --- Get up and running with Turbine in just a few minutes. ## Prerequisites - Node.js 18+ - An AWS account with DynamoDB access - AWS credentials configured (via environment variables, AWS CLI, or IAM role) ## Installation Install Turbine and Zod: ```bash npm install dynamodb-turbine zod ``` ## Step 1: Define Your Table First, define your DynamoDB table structure. Turbine needs to know about your table's indexes: ```typescript import { defineTable } from "dynamodb-turbine"; const table = defineTable({ name: "my-app-table", indexes: { table: { hashKey: "pk", rangeKey: "sk" }, gsi1: { hashKey: "type", rangeKey: "createdAt" }, }, }); ``` The `table` index is your primary key. Additional indexes (like `gsi1`) are Global Secondary Indexes. ## Step 2: Define an Entity Create an entity with a Zod schema for validation: ```typescript import { defineEntity } from "dynamodb-turbine"; import { z } from "zod"; const users = defineEntity({ table, schema: z.object({ id: z.string(), email: z.string().email(), name: z.string(), role: z.enum(["user", "admin"]).default("user"), createdAt: z.string().datetime().default(() => new Date().toISOString()), }), keys: { type: () => "user", pk: (user) => ["user", user.id], sk: (user) => ["profile", user.email], }, }); ``` Key functions receive the entity data and return values that become DynamoDB attributes. Arrays are automatically joined with `#`. ## Step 3: Create Items Use `put()` to create new items: ```typescript const user = await users.put({ id: "user-123", email: "alice@example.com", name: "Alice", }); console.log(user.id); // "user-123" console.log(user.role); // "user" (default value) console.log(user.createdAt); // "2024-01-15T10:30:00.000Z" ``` ## Step 4: Fetch Items Use `get()` to fetch a single item by its keys: ```typescript const user = await users.get({ pk: ["user", "user-123"], sk: ["profile", "alice@example.com"], }); if (user) { console.log(user.name); // "Alice" } ``` ## Step 5: Update Items Update items using the entity method or the instance method: ```typescript // Entity method - requires keys await users.update( { pk: ["user", "user-123"], sk: ["profile", "alice@example.com"] }, { name: "Alice Smith" } ); // Instance method - more convenient const user = await users.get({ pk: ["user", "user-123"], sk: ["profile", "alice@example.com"] }); await user?.update({ name: "Alice Smith" }); ``` ## Step 6: Query Items Query for multiple items using indexes: ```typescript // Query all users created in 2024 const users2024 = await users.queryAll({ index: "gsi1", type: "user", createdAt: { beginsWith: "2024" }, }); // Query with pagination const page1 = await users.query( { index: "gsi1", type: "user" }, { Limit: 10 } ); if (page1.next) { const page2 = await page1.next(); } ``` ## Step 7: Delete Items Remove items with `delete()`: ```typescript await users.delete({ pk: ["user", "user-123"], sk: ["profile", "alice@example.com"], }); ``` ## Complete Example Here's a complete working example: ```typescript import { defineTable, defineEntity } from "dynamodb-turbine"; import { z } from "zod"; // Table definition const table = defineTable({ name: process.env.TABLE_NAME!, indexes: { table: { hashKey: "pk", rangeKey: "sk" }, gsi1: { hashKey: "type", rangeKey: "createdAt" }, }, }); // User entity const users = defineEntity({ table, schema: z.object({ id: z.string(), email: z.string().email(), name: z.string(), createdAt: z.string().datetime().default(() => new Date().toISOString()), }), keys: { type: () => "user", pk: (u) => ["user", u.id], sk: (u) => ["profile", u.email], }, }); // Usage async function main() { // Create const user = await users.put({ id: "123", email: "alice@example.com", name: "Alice", }); // Read const fetched = await users.get({ pk: ["user", "123"], sk: ["profile", "alice@example.com"], }); // Update await fetched?.update({ name: "Alice Smith" }); // Query const allUsers = await users.queryAll({ index: "gsi1", type: "user", }); // Delete await users.delete({ pk: ["user", "123"], sk: ["profile", "alice@example.com"], }); } ``` ## Next Steps - [Tables](/tables) - Learn about table configuration and indexes - [Entities](/entities) - Deep dive into entity definitions - [Querying](/querying) - Master query conditions and filters --- The `defineTable` function creates a table configuration that entities use to interact with DynamoDB. ## Basic Usage ```typescript import { defineTable } from "dynamodb-turbine"; const table = defineTable({ name: "my-dynamodb-table", indexes: { table: { hashKey: "pk", rangeKey: "sk" }, }, }); ``` ## Table Definition The `defineTable` function accepts a configuration object: | Property | Type | Required | Description | |----------|------|----------|-------------| | `name` | `string` | Yes | The DynamoDB table name | | `indexes` | `object` | Yes | Index definitions | | `documentClient` | `DynamoDBDocumentClient` | No | Custom AWS SDK client | ## Indexes The `indexes` object defines your table's primary key and any Global Secondary Indexes (GSIs). ### Primary Index The `table` key defines your primary key: ```typescript indexes: { table: { hashKey: "pk", rangeKey: "sk" }, } ``` - `hashKey` (required) - The partition key attribute name - `rangeKey` (optional) - The sort key attribute name ### Global Secondary Indexes Additional keys define GSIs: ```typescript indexes: { table: { hashKey: "pk", rangeKey: "sk" }, gsi1: { hashKey: "type", rangeKey: "createdAt" }, gsi2: { hashKey: "email" }, gsi3: { hashKey: "status", rangeKey: "updatedAt" }, } ``` > The index names (`gsi1`, `gsi2`, etc.) must match the index names in your DynamoDB table. ## Custom DynamoDB Client By default, Turbine creates a DynamoDB Document Client with sensible defaults: ```typescript // Default client configuration { convertEmptyValues: true, removeUndefinedValues: true, } ``` To use a custom client: ```typescript import { DynamoDBClient } from "@aws-sdk/client-dynamodb"; import { DynamoDBDocumentClient } from "@aws-sdk/lib-dynamodb"; const dynamoClient = new DynamoDBClient({ region: "us-east-1", endpoint: "http://localhost:8000", // Local DynamoDB }); const documentClient = DynamoDBDocumentClient.from(dynamoClient, { marshallOptions: { convertEmptyValues: true, removeUndefinedValues: true, }, }); const table = defineTable({ name: "my-table", documentClient, indexes: { table: { hashKey: "pk", rangeKey: "sk" }, }, }); ``` ## Table Properties The returned table object has the following properties: ```typescript const table = defineTable({ /* ... */ }); table.definition // The original table definition table.client // The DynamoDBDocumentClient instance ``` ## Common Patterns ### Single-Table Design For single-table design, define all your GSIs upfront: ```typescript const table = defineTable({ name: "app-data", indexes: { table: { hashKey: "pk", rangeKey: "sk" }, gsi1: { hashKey: "gsi1pk", rangeKey: "gsi1sk" }, gsi2: { hashKey: "gsi2pk", rangeKey: "gsi2sk" }, gsi3: { hashKey: "type", rangeKey: "createdAt" }, }, }); ``` ### Environment-Based Table Names ```typescript const table = defineTable({ name: process.env.DYNAMODB_TABLE_NAME!, indexes: { table: { hashKey: "pk", rangeKey: "sk" }, }, }); ``` ### Multiple Tables You can define multiple tables in your application: ```typescript const usersTable = defineTable({ name: "users", indexes: { table: { hashKey: "userId" }, byEmail: { hashKey: "email" }, }, }); const ordersTable = defineTable({ name: "orders", indexes: { table: { hashKey: "orderId" }, byUser: { hashKey: "userId", rangeKey: "createdAt" }, }, }); ``` ## Next Steps - [Entities](/entities) - Define data models using your table - [Key Patterns](/key-patterns) - Learn about key design strategies --- Turbine is built with TypeScript and provides full type safety throughout your application. ## Instance Type The `Instance` type represents the result of entity operations. Use it to type variables and function parameters: ```typescript import type { Instance } from "dynamodb-turbine"; // Get the Instance type from your entity type User = Instance; function displayUser(user: User) { console.log(user.id); console.log(user.email); console.log(user.name); } ``` ### What Instance Includes An Instance contains: 1. **All schema fields** - Every field from your Zod schema 2. **Generated key values** - The actual key values stored in DynamoDB 3. **update() method** - Convenience method for updates ```typescript const user = await users.put({ id: "123", email: "alice@example.com", name: "Alice", }); // Schema fields user.id; // string user.email; // string user.name; // string // Key values (also present as attributes) user.pk; // "user#123" user.sk; // "alice@example.com" // Instance method await user.update({ name: "Alice Smith" }); ``` ## Type Inference from Entities TypeScript automatically infers types from your entity definitions: ```typescript const users = defineEntity({ table, schema: z.object({ id: z.string(), email: z.string().email(), name: z.string().optional(), role: z.enum(["user", "admin"]), createdAt: z.string().datetime(), }), keys: { /* ... */ }, }); // TypeScript knows the exact shape const user = await users.put({ id: "123", email: "alice@example.com", role: "admin", // Must be "user" | "admin" createdAt: new Date().toISOString(), }); user.role; // Type: "user" | "admin" user.name; // Type: string | undefined ``` ## Type-Safe Operations ### put() The input type is inferred from your schema, respecting optional fields and defaults: ```typescript // Schema with defaults schema: z.object({ id: z.string(), name: z.string(), role: z.enum(["user", "admin"]).default("user"), createdAt: z.string().datetime().default(() => new Date().toISOString()), }) // Only required fields needed await users.put({ id: "123", name: "Alice", // role and createdAt have defaults, so they're optional }); // TypeScript error: missing 'name' await users.put({ id: "123", }); ``` ### get() and query() Return types are automatically `Instance | null` or `Instance[]`: ```typescript const user = await users.get({ /* ... */ }); // Type: Instance | null if (user) { // TypeScript knows user is not null here console.log(user.name); } const results = await users.queryAll({ /* ... */ }); // Type: Instance[] results.forEach(user => { console.log(user.email); // TypeScript knows the shape }); ``` ### update() The patch type is a partial of your schema: ```typescript // Entity update - accepts Partial await users.update( { pk: ["user", "123"], sk: "alice@example.com" }, { name: "Alice Smith" } // Partial ); // Instance update - same partial type await user.update({ role: "admin", // Type-checked against schema }); // TypeScript error: 'unknown' is not in schema await user.update({ unknown: "field", }); ``` ## Using Instance in Functions ### Function Parameters ```typescript type User = Instance; type Post = Instance; function formatUser(user: User): string { return `${user.name} (${user.email})`; } async function publishPost(post: Post): Promise { return post.update({ status: "published" }); } ``` ### Return Types ```typescript type User = Instance; async function getActiveUsers(): Promise { return users.queryAll({ type: "user", }, { filters: { status: "active", }, }); } async function findUserByEmail(email: string): Promise { return users.queryOne({ index: "byEmail", email, }); } ``` ### Generic Functions ```typescript import type { Entity, Instance } from "dynamodb-turbine"; // Generic function that works with any entity async function countItems>( entity: E, key: Parameters[0] ): Promise { const items = await entity.queryAll(key); return items.length; } const userCount = await countItems(users, { type: "user" }); const postCount = await countItems(posts, { type: "post" }); ``` ## Extracting Schema Types You can extract the raw schema type (without Instance methods): ```typescript import { z } from "zod"; const userSchema = z.object({ id: z.string(), email: z.string().email(), name: z.string(), }); // Raw schema type (for validation, forms, etc.) type UserData = z.infer; const users = defineEntity({ table, schema: userSchema, keys: { /* ... */ }, }); // Instance type (includes update method) type User = Instance; // UserData for input validation function validateUserInput(data: unknown): UserData { return userSchema.parse(data); } // User for database operations async function createUser(data: UserData): Promise { return users.put(data); } ``` ## Common Patterns ### Separating Entity and Types ```typescript // entities/user.ts import { defineEntity } from "dynamodb-turbine"; import type { Instance } from "dynamodb-turbine"; import { z } from "zod"; import { table } from "./table"; export const userSchema = z.object({ id: z.string(), email: z.string().email(), name: z.string(), role: z.enum(["user", "admin"]).default("user"), createdAt: z.string().datetime().default(() => new Date().toISOString()), }); export const users = defineEntity({ table, schema: userSchema, keys: { type: () => "user", pk: (u) => ["user", u.id], sk: (u) => ["profile", u.email], }, }); export type User = Instance; export type UserInput = z.input; ``` ```typescript // services/user-service.ts import { users, type User, type UserInput } from "../entities/user"; export async function createUser(input: UserInput): Promise { return users.put(input); } export async function getUserById(id: string): Promise { // Implementation } ``` ### API Response Types ```typescript type User = Instance; // Omit internal fields for API responses type UserResponse = Omit; function toResponse(user: User): UserResponse { const { pk, sk, update, ...rest } = user; return rest; } ``` ## Next Steps - [Key Patterns](/key-patterns) - Design effective key structures - [Error Handling](/errors) - Handle Turbine errors