UNPKG

@tanstack/db

Version:

A reactive client store for building super fast apps on sync

216 lines (167 loc) 5.98 kB
# Schema Patterns Reference ## StandardSchema Integration TanStack DB accepts any [StandardSchema](https://standardschema.dev)-compatible library via the `schema` option. ### Supported Libraries - [Zod](https://zod.dev), [Valibot](https://valibot.dev), [ArkType](https://arktype.io), [Effect Schema](https://effect.website/docs/schema/introduction/) ## TInput vs TOutput - **TInput** -- type accepted by `insert()` and `update()` - **TOutput** -- type stored in collection and returned from queries When no transforms exist, TInput === TOutput. ```typescript const schema = z.object({ id: z.string(), created_at: z.string().transform((val) => new Date(val)), }) // TInput: { id: string, created_at: string } // TOutput: { id: string, created_at: Date } ``` ## Union Pattern for Transforms (Required) When a schema transforms A to B, TInput **must** accept both A and B. During `update()`, the draft contains TOutput data. ```typescript // WRONG -- update() fails because draft.created_at is Date but schema expects string z.string().transform((val) => new Date(val)) // CORRECT z.union([z.string(), z.date()]).transform((val) => typeof val === 'string' ? new Date(val) : val, ) // TInput: string | Date, TOutput: Date ``` ## Defaults ```typescript const schema = z.object({ id: z.string(), text: z.string(), completed: z.boolean().default(false), priority: z.number().default(0), tags: z.array(z.string()).default([]), created_at: z.date().default(() => new Date()), }) // insert({ id: "1", text: "Task" }) -- missing fields auto-filled ``` ## Computed Fields ```typescript const schema = z .object({ id: z.string(), first_name: z.string(), last_name: z.string(), }) .transform((data) => ({ ...data, full_name: `${data.first_name} ${data.last_name}`, })) ``` ## Combining Defaults with Transforms ```typescript const schema = z.object({ created_at: z .string() .default(() => new Date().toISOString()) .transform((val) => new Date(val)), }) ``` ## Validation Examples ```typescript // Basic constraints z.string().min(3).max(100) z.string().email() z.number().int().positive() z.enum(['active', 'inactive']) z.array(z.string()).min(1) // Optional/nullable z.string().optional() // can be omitted z.string().nullable() // can be null // Cross-field z.object({ start: z.string(), end: z.string() }).refine( (d) => new Date(d.end) > new Date(d.start), 'End must be after start', ) // Custom z.string().refine((v) => /^[a-zA-Z0-9_]+$/.test(v), 'Alphanumeric only') ``` ## SchemaValidationError ```typescript import { SchemaValidationError } from '@tanstack/db' try { collection.insert({ id: '1', email: 'bad', age: -5 }) } catch (error) { if (error instanceof SchemaValidationError) { error.type // "insert" or "update" error.message // "Validation failed with 2 issues" error.issues // [{ path: ["email"], message: "Invalid email" }, ...] } } ``` ## Scope: Schema vs Sync — Two Separate Paths **Schemas validate client mutations only** (`insert()`, `update()`). Synced data from backends (Electric, PowerSync, etc.) bypasses the schema entirely. This means for types that need transformation (e.g., `timestamptz`): - **Sync path**: handled by the adapter's parser (e.g., Electric's `shapeOptions.parser`) - **Mutation path**: handled by the Zod schema You need BOTH configured for full type safety. See electric-adapter.md for the dual-path pattern. ### Simpler date coercion (Zod-specific) With Zod, `z.coerce.date()` is simpler than the `z.union([z.string(), z.date()]).transform(...)` pattern: ```typescript // Zod-specific: z.coerce.date() accepts string, number, or Date as input const schema = z.object({ created_at: z.coerce.date(), }) // TInput: { created_at: string | number | Date } (coerce accepts many types) // TOutput: { created_at: Date } ``` This satisfies the TInput-superset-of-TOutput requirement automatically. Other StandardSchema libraries have their own coercion patterns — consult library docs. ### Important - Validation is synchronous, runs on every mutation - Keep transforms simple for performance ## Where TOutput Appears - Data stored in collection and returned from queries - `PendingMutation.modified` - Mutation handler `transaction.mutations[].modified` ## Performance Keep transforms simple -- validation runs synchronously on every mutation. ## Complete Example ```typescript import { z } from 'zod' import { createCollection, SchemaValidationError } from '@tanstack/react-db' import { queryCollectionOptions } from '@tanstack/query-db-collection' const todoSchema = z.object({ id: z.string(), text: z.string().min(1, 'Text is required'), completed: z.boolean().default(false), priority: z.enum(['low', 'medium', 'high']).default('medium'), created_at: z .union([z.string(), z.date()]) .transform((val) => (typeof val === 'string' ? new Date(val) : val)) .default(() => new Date()), }) const todosCollection = createCollection( queryCollectionOptions({ queryKey: ['todos'], queryFn: async () => fetch('/api/todos').then((r) => r.json()), queryClient, getKey: (item) => item.id, schema: todoSchema, onInsert: async ({ transaction }) => { const todo = transaction.mutations[0].modified await api.todos.create({ ...todo, created_at: todo.created_at.toISOString(), }) }, }), ) // Defaults and transforms applied todosCollection.insert({ id: '1', text: 'Buy groceries' }) // => { id: "1", text: "Buy groceries", completed: false, priority: "medium", created_at: Date } // Update works -- draft contains TOutput, schema accepts via union todosCollection.update('1', (draft) => { draft.completed = true }) // Error handling try { todosCollection.insert({ id: '2', text: '' }) } catch (e) { if (e instanceof SchemaValidationError) { console.log(e.issues) // [{ path: ["text"], message: "Text is required" }] } } ```