Entities

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

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:

PropertyTypeRequiredDescription
tableTableYesThe table from defineTable
schemaz.ZodObjectYesA Zod object schema
keysobjectYesKey generation functions

Schema Definition

Use any Zod object schema to define your entity structure:

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:

Key Definitions

Keys determine how items are stored and retrieved in DynamoDB. Each key can be:

Static Values

keys: {
  type: () => "user",
}

Field References

keys: {
  pk: (entity) => entity.id,
  sk: (entity) => entity.email,
}

Composite Keys (Arrays)

Arrays are automatically joined with #:

keys: {
  pk: (user) => ["user", user.id],     // becomes "user#<id>"
  sk: (user) => ["profile", user.email], // becomes "profile#<email>"
}

Computed Values

keys: {
  createdAt: (entity) => entity.createdAt || new Date().toISOString(),
  updatedAt: () => new Date().toISOString(),
}

Example: Full Key Definition

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:

const user = await users.put({
  id: "123",
  email: "alice@example.com",
  name: "Alice",
});

get(key)

Fetch a single item:

const user = await users.get({
  pk: ["user", "123"],
  sk: "alice@example.com",
});

update(key, patch)

Update an existing item:

const updated = await users.update(
  { pk: ["user", "123"], sk: "alice@example.com" },
  { name: "Alice Smith" }
);

delete(key)

Remove an item:

await users.delete({
  pk: ["user", "123"],
  sk: "alice@example.com",
});

query(key, options?)

Query with pagination:

const results = await users.query(
  { pk: ["user", "123"] },
  { Limit: 10 }
);

queryOne(key, options?)

Get the first matching item:

const user = await users.queryOne({
  index: "gsi2",
  gsi2pk: "user#email",
  gsi2sk: "alice@example.com",
});

queryAll(key, options?)

Get all matching items (handles pagination):

const allPosts = await posts.queryAll({
  pk: ["user", "123"],
});

Entity Properties

const users = defineEntity({ /* ... */ });

users.definition  // The original entity definition

Next Steps