@tanstack/db 0.5.30 → 0.5.31

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,183 @@
1
+ # Query Adapter Reference
2
+
3
+ ## Install
4
+
5
+ ```bash
6
+ pnpm add @tanstack/query-db-collection @tanstack/query-core @tanstack/db
7
+ ```
8
+
9
+ ## Required Config
10
+
11
+ ```typescript
12
+ import { QueryClient } from '@tanstack/query-core'
13
+ import { createCollection } from '@tanstack/db'
14
+ import { queryCollectionOptions } from '@tanstack/query-db-collection'
15
+
16
+ const queryClient = new QueryClient()
17
+ const collection = createCollection(
18
+ queryCollectionOptions({
19
+ queryKey: ['todos'],
20
+ queryFn: async () => fetch('/api/todos').then((r) => r.json()),
21
+ queryClient,
22
+ getKey: (item) => item.id,
23
+ }),
24
+ )
25
+ ```
26
+
27
+ - `queryKey` -- TanStack Query cache key
28
+ - `queryFn` -- fetches data; must be provided (throws `QueryFnRequiredError` if missing)
29
+ - `queryClient` -- `QueryClient` instance
30
+ - `getKey` -- extracts unique key from each item
31
+
32
+ ## Optional Config (with defaults)
33
+
34
+ | Option | Default | Description |
35
+ | ----------------- | ------------ | ----------------------------------------------- |
36
+ | `id` | (none) | Unique collection identifier |
37
+ | `schema` | (none) | StandardSchema validator |
38
+ | `select` | (none) | Extracts array items when wrapped with metadata |
39
+ | `enabled` | `true` | Whether query runs automatically |
40
+ | `refetchInterval` | `0` | Polling interval in ms; 0 = disabled |
41
+ | `retry` | (TQ default) | Retry config for failed queries |
42
+ | `retryDelay` | (TQ default) | Delay between retries |
43
+ | `staleTime` | (TQ default) | How long data is considered fresh |
44
+ | `meta` | (none) | Metadata passed to queryFn context |
45
+ | `startSync` | `true` | Start syncing immediately |
46
+ | `syncMode` | (none) | Set `"on-demand"` for predicate push-down |
47
+
48
+ ### Persistence Handlers
49
+
50
+ ```typescript
51
+ onInsert: async ({ transaction }) => {
52
+ await api.createTodos(transaction.mutations.map((m) => m.modified))
53
+ // return nothing or { refetch: true } to trigger refetch
54
+ // return { refetch: false } to skip refetch
55
+ },
56
+ onUpdate: async ({ transaction }) => {
57
+ await api.updateTodos(transaction.mutations.map((m) => ({ id: m.key, changes: m.changes })))
58
+ },
59
+ onDelete: async ({ transaction }) => {
60
+ await api.deleteTodos(transaction.mutations.map((m) => m.key))
61
+ },
62
+ ```
63
+
64
+ ## Utility Methods (`collection.utils`)
65
+
66
+ - `refetch(opts?)` -- manual refetch; `opts.throwOnError` (default `false`); bypasses `enabled: false`
67
+ - `writeInsert(data)` -- insert directly to synced store (bypasses optimistic system)
68
+ - `writeUpdate(data)` -- update directly in synced store
69
+ - `writeDelete(keys)` -- delete directly from synced store
70
+ - `writeUpsert(data)` -- insert or update directly
71
+ - `writeBatch(callback)` -- multiple write ops atomically
72
+
73
+ Direct writes bypass optimistic updates, do NOT trigger refetches, and update TQ cache immediately.
74
+
75
+ ```typescript
76
+ collection.utils.writeBatch(() => {
77
+ collection.utils.writeInsert({ id: '1', text: 'Buy milk' })
78
+ collection.utils.writeUpdate({ id: '2', completed: true })
79
+ collection.utils.writeDelete('3')
80
+ })
81
+ ```
82
+
83
+ ## Predicate Push-Down (syncMode: "on-demand")
84
+
85
+ Query predicates (where, orderBy, limit, offset) passed to `queryFn` via `ctx.meta.loadSubsetOptions`.
86
+
87
+ ```typescript
88
+ import { parseLoadSubsetOptions } from '@tanstack/query-db-collection'
89
+
90
+ queryFn: async (ctx) => {
91
+ const { filters, sorts, limit, offset } = parseLoadSubsetOptions(
92
+ ctx.meta?.loadSubsetOptions,
93
+ )
94
+ // filters: [{ field: ['category'], operator: 'eq', value: 'electronics' }]
95
+ // sorts: [{ field: ['price'], direction: 'asc', nulls: 'last' }]
96
+ }
97
+ ```
98
+
99
+ ### Expression Helpers (from `@tanstack/db`)
100
+
101
+ - `parseLoadSubsetOptions(opts)` -- returns `{ filters, sorts, limit, offset }`
102
+ - `parseWhereExpression(expr, { handlers })` -- custom handlers per operator
103
+ - `parseOrderByExpression(expr)` -- returns `[{ field, direction, nulls }]`
104
+ - `extractSimpleComparisons(expr)` -- flat AND-ed comparisons only
105
+
106
+ Supported operators: `eq`, `gt`, `gte`, `lt`, `lte`, `and`, `or`, `in`
107
+
108
+ ## Dynamic queryKey
109
+
110
+ ```typescript
111
+ queryKey: (opts) => {
112
+ const parsed = parseLoadSubsetOptions(opts)
113
+ const key = ["products"]
114
+ parsed.filters.forEach((f) => key.push(`${f.field.join(".")}-${f.operator}-${f.value}`))
115
+ if (parsed.limit) key.push(`limit-${parsed.limit}`)
116
+ return key
117
+ },
118
+ ```
119
+
120
+ ## Complete Example
121
+
122
+ ```typescript
123
+ import { QueryClient } from '@tanstack/query-core'
124
+ import { createCollection } from '@tanstack/react-db'
125
+ import {
126
+ queryCollectionOptions,
127
+ parseLoadSubsetOptions,
128
+ } from '@tanstack/query-db-collection'
129
+
130
+ const queryClient = new QueryClient()
131
+
132
+ const productsCollection = createCollection(
133
+ queryCollectionOptions({
134
+ id: 'products',
135
+ queryKey: ['products'],
136
+ queryClient,
137
+ getKey: (item) => item.id,
138
+ syncMode: 'on-demand',
139
+ queryFn: async (ctx) => {
140
+ const { filters, sorts, limit } = parseLoadSubsetOptions(
141
+ ctx.meta?.loadSubsetOptions,
142
+ )
143
+ const params = new URLSearchParams()
144
+ filters.forEach(({ field, operator, value }) => {
145
+ params.set(`${field.join('.')}_${operator}`, String(value))
146
+ })
147
+ if (sorts.length > 0) {
148
+ params.set(
149
+ 'sort',
150
+ sorts.map((s) => `${s.field.join('.')}:${s.direction}`).join(','),
151
+ )
152
+ }
153
+ if (limit) params.set('limit', String(limit))
154
+ return fetch(`/api/products?${params}`).then((r) => r.json())
155
+ },
156
+ onInsert: async ({ transaction }) => {
157
+ const serverItems = await api.createProducts(
158
+ transaction.mutations.map((m) => m.modified),
159
+ )
160
+ productsCollection.utils.writeBatch(() => {
161
+ serverItems.forEach((item) =>
162
+ productsCollection.utils.writeInsert(item),
163
+ )
164
+ })
165
+ return { refetch: false }
166
+ },
167
+ onUpdate: async ({ transaction }) => {
168
+ await api.updateProducts(
169
+ transaction.mutations.map((m) => ({ id: m.key, changes: m.changes })),
170
+ )
171
+ },
172
+ onDelete: async ({ transaction }) => {
173
+ await api.deleteProducts(transaction.mutations.map((m) => m.key))
174
+ },
175
+ }),
176
+ )
177
+ ```
178
+
179
+ ## Key Behaviors
180
+
181
+ - `queryFn` result is treated as **complete state** -- missing items are deleted
182
+ - Empty array from `queryFn` deletes all items
183
+ - Direct writes update TQ cache but are overridden by subsequent `queryFn` results
@@ -0,0 +1,152 @@
1
+ # RxDB Adapter Reference
2
+
3
+ ## Install
4
+
5
+ ```bash
6
+ pnpm add @tanstack/rxdb-db-collection rxdb @tanstack/react-db
7
+ ```
8
+
9
+ ## Required Config
10
+
11
+ ```typescript
12
+ import { createCollection } from '@tanstack/react-db'
13
+ import { rxdbCollectionOptions } from '@tanstack/rxdb-db-collection'
14
+
15
+ const todosCollection = createCollection(
16
+ rxdbCollectionOptions({
17
+ rxCollection: db.todos,
18
+ }),
19
+ )
20
+ ```
21
+
22
+ - `rxCollection` -- the underlying RxDB `RxCollection` instance
23
+
24
+ ## Optional Config (with defaults)
25
+
26
+ | Option | Default | Description |
27
+ | --------------- | ----------------------- | -------------------------------------------------------------------------------------------------- |
28
+ | `id` | (none) | Unique collection identifier |
29
+ | `schema` | (none) | StandardSchema validator (RxDB has its own validation; this adds TanStack DB-side validation) |
30
+ | `startSync` | `true` | Start ingesting RxDB data immediately |
31
+ | `syncBatchSize` | `1000` | Max documents per batch during initial sync from RxDB; only affects initial load, not live updates |
32
+ | `onInsert` | (default: `bulkUpsert`) | Override default insert persistence |
33
+ | `onUpdate` | (default: `patch`) | Override default update persistence |
34
+ | `onDelete` | (default: `bulkRemove`) | Override default delete persistence |
35
+
36
+ ## Key Behavior: String Keys
37
+
38
+ RxDB primary keys are always strings. The `getKey` function is derived from the RxDB schema's `primaryKey` field automatically. All key values will be strings.
39
+
40
+ ## RxDB Setup (prerequisite)
41
+
42
+ ```typescript
43
+ import { createRxDatabase } from 'rxdb/plugins/core'
44
+ import { getRxStorageLocalstorage } from 'rxdb/plugins/storage-localstorage'
45
+
46
+ const db = await createRxDatabase({
47
+ name: 'my-app',
48
+ storage: getRxStorageLocalstorage(),
49
+ })
50
+
51
+ await db.addCollections({
52
+ todos: {
53
+ schema: {
54
+ title: 'todos',
55
+ version: 0,
56
+ type: 'object',
57
+ primaryKey: 'id',
58
+ properties: {
59
+ id: { type: 'string', maxLength: 100 },
60
+ text: { type: 'string' },
61
+ completed: { type: 'boolean' },
62
+ },
63
+ required: ['id', 'text', 'completed'],
64
+ },
65
+ },
66
+ })
67
+ ```
68
+
69
+ ## Backend Sync (optional, RxDB-managed)
70
+
71
+ Replication is configured directly on the RxDB collection, independent of TanStack DB. Changes from replication flow into the TanStack DB collection via RxDB's change stream automatically.
72
+
73
+ ```typescript
74
+ import { replicateRxCollection } from 'rxdb/plugins/replication'
75
+
76
+ const replicationState = replicateRxCollection({
77
+ collection: db.todos,
78
+ pull: { handler: myPullHandler },
79
+ push: { handler: myPushHandler },
80
+ })
81
+ ```
82
+
83
+ ## Data Flow
84
+
85
+ - Writes via `todosCollection.insert/update/delete` persist to RxDB
86
+ - Direct RxDB writes (or replication changes) flow into the TanStack collection via change streams
87
+ - Initial sync loads data in batches of `syncBatchSize`
88
+ - Ongoing updates stream one by one via RxDB's change feed
89
+
90
+ ## Indexes
91
+
92
+ RxDB schema indexes do not affect TanStack DB query performance (queries run in-memory). Indexes may still matter if you query RxDB directly, use filtered replication, or selectively load subsets.
93
+
94
+ ## Complete Example
95
+
96
+ ```typescript
97
+ import { createRxDatabase } from 'rxdb/plugins/core'
98
+ import { getRxStorageLocalstorage } from 'rxdb/plugins/storage-localstorage'
99
+ import { createCollection } from '@tanstack/react-db'
100
+ import { rxdbCollectionOptions } from '@tanstack/rxdb-db-collection'
101
+ import { z } from 'zod'
102
+
103
+ type Todo = { id: string; text: string; completed: boolean }
104
+
105
+ const db = await createRxDatabase({
106
+ name: 'my-todos',
107
+ storage: getRxStorageLocalstorage(),
108
+ })
109
+
110
+ await db.addCollections({
111
+ todos: {
112
+ schema: {
113
+ title: 'todos',
114
+ version: 0,
115
+ type: 'object',
116
+ primaryKey: 'id',
117
+ properties: {
118
+ id: { type: 'string', maxLength: 100 },
119
+ text: { type: 'string' },
120
+ completed: { type: 'boolean' },
121
+ },
122
+ required: ['id', 'text', 'completed'],
123
+ },
124
+ },
125
+ })
126
+
127
+ const todoSchema = z.object({
128
+ id: z.string(),
129
+ text: z.string().min(1),
130
+ completed: z.boolean(),
131
+ })
132
+
133
+ const todosCollection = createCollection(
134
+ rxdbCollectionOptions({
135
+ rxCollection: db.todos,
136
+ schema: todoSchema,
137
+ startSync: true,
138
+ syncBatchSize: 500,
139
+ }),
140
+ )
141
+
142
+ // Usage
143
+ todosCollection.insert({
144
+ id: crypto.randomUUID(),
145
+ text: 'Buy milk',
146
+ completed: false,
147
+ })
148
+ todosCollection.update('some-id', (draft) => {
149
+ draft.completed = true
150
+ })
151
+ todosCollection.delete('some-id')
152
+ ```
@@ -0,0 +1,215 @@
1
+ # Schema Patterns Reference
2
+
3
+ ## StandardSchema Integration
4
+
5
+ TanStack DB accepts any [StandardSchema](https://standardschema.dev)-compatible library via the `schema` option.
6
+
7
+ ### Supported Libraries
8
+
9
+ - [Zod](https://zod.dev), [Valibot](https://valibot.dev), [ArkType](https://arktype.io), [Effect Schema](https://effect.website/docs/schema/introduction/)
10
+
11
+ ## TInput vs TOutput
12
+
13
+ - **TInput** -- type accepted by `insert()` and `update()`
14
+ - **TOutput** -- type stored in collection and returned from queries
15
+
16
+ When no transforms exist, TInput === TOutput.
17
+
18
+ ```typescript
19
+ const schema = z.object({
20
+ id: z.string(),
21
+ created_at: z.string().transform((val) => new Date(val)),
22
+ })
23
+ // TInput: { id: string, created_at: string }
24
+ // TOutput: { id: string, created_at: Date }
25
+ ```
26
+
27
+ ## Union Pattern for Transforms (Required)
28
+
29
+ When a schema transforms A to B, TInput **must** accept both A and B. During `update()`, the draft contains TOutput data.
30
+
31
+ ```typescript
32
+ // WRONG -- update() fails because draft.created_at is Date but schema expects string
33
+ z.string().transform((val) => new Date(val))
34
+
35
+ // CORRECT
36
+ z.union([z.string(), z.date()]).transform((val) =>
37
+ typeof val === 'string' ? new Date(val) : val,
38
+ )
39
+ // TInput: string | Date, TOutput: Date
40
+ ```
41
+
42
+ ## Defaults
43
+
44
+ ```typescript
45
+ const schema = z.object({
46
+ id: z.string(),
47
+ text: z.string(),
48
+ completed: z.boolean().default(false),
49
+ priority: z.number().default(0),
50
+ tags: z.array(z.string()).default([]),
51
+ created_at: z.date().default(() => new Date()),
52
+ })
53
+ // insert({ id: "1", text: "Task" }) -- missing fields auto-filled
54
+ ```
55
+
56
+ ## Computed Fields
57
+
58
+ ```typescript
59
+ const schema = z
60
+ .object({
61
+ id: z.string(),
62
+ first_name: z.string(),
63
+ last_name: z.string(),
64
+ })
65
+ .transform((data) => ({
66
+ ...data,
67
+ full_name: `${data.first_name} ${data.last_name}`,
68
+ }))
69
+ ```
70
+
71
+ ## Combining Defaults with Transforms
72
+
73
+ ```typescript
74
+ const schema = z.object({
75
+ created_at: z
76
+ .string()
77
+ .default(() => new Date().toISOString())
78
+ .transform((val) => new Date(val)),
79
+ })
80
+ ```
81
+
82
+ ## Validation Examples
83
+
84
+ ```typescript
85
+ // Basic constraints
86
+ z.string().min(3).max(100)
87
+ z.string().email()
88
+ z.number().int().positive()
89
+ z.enum(['active', 'inactive'])
90
+ z.array(z.string()).min(1)
91
+
92
+ // Optional/nullable
93
+ z.string().optional() // can be omitted
94
+ z.string().nullable() // can be null
95
+
96
+ // Cross-field
97
+ z.object({ start: z.string(), end: z.string() }).refine(
98
+ (d) => new Date(d.end) > new Date(d.start),
99
+ 'End must be after start',
100
+ )
101
+
102
+ // Custom
103
+ z.string().refine((v) => /^[a-zA-Z0-9_]+$/.test(v), 'Alphanumeric only')
104
+ ```
105
+
106
+ ## SchemaValidationError
107
+
108
+ ```typescript
109
+ import { SchemaValidationError } from '@tanstack/db'
110
+
111
+ try {
112
+ collection.insert({ id: '1', email: 'bad', age: -5 })
113
+ } catch (error) {
114
+ if (error instanceof SchemaValidationError) {
115
+ error.type // "insert" or "update"
116
+ error.message // "Validation failed with 2 issues"
117
+ error.issues // [{ path: ["email"], message: "Invalid email" }, ...]
118
+ }
119
+ }
120
+ ```
121
+
122
+ ## Scope: Schema vs Sync — Two Separate Paths
123
+
124
+ **Schemas validate client mutations only** (`insert()`, `update()`). Synced data from backends (Electric, PowerSync, etc.) bypasses the schema entirely.
125
+
126
+ This means for types that need transformation (e.g., `timestamptz`):
127
+
128
+ - **Sync path**: handled by the adapter's parser (e.g., Electric's `shapeOptions.parser`)
129
+ - **Mutation path**: handled by the Zod schema
130
+
131
+ You need BOTH configured for full type safety. See electric-adapter.md for the dual-path pattern.
132
+
133
+ ### Simpler date coercion (Zod-specific)
134
+
135
+ With Zod, `z.coerce.date()` is simpler than the `z.union([z.string(), z.date()]).transform(...)` pattern:
136
+
137
+ ```typescript
138
+ // Zod-specific: z.coerce.date() accepts string, number, or Date as input
139
+ const schema = z.object({
140
+ created_at: z.coerce.date(),
141
+ })
142
+ // TInput: { created_at: string | number | Date } (coerce accepts many types)
143
+ // TOutput: { created_at: Date }
144
+ ```
145
+
146
+ This satisfies the TInput-superset-of-TOutput requirement automatically. Other StandardSchema libraries have their own coercion patterns — consult library docs.
147
+
148
+ ### Important
149
+
150
+ - Validation is synchronous, runs on every mutation
151
+ - Keep transforms simple for performance
152
+
153
+ ## Where TOutput Appears
154
+
155
+ - Data stored in collection and returned from queries
156
+ - `PendingMutation.modified`
157
+ - Mutation handler `transaction.mutations[].modified`
158
+
159
+ ## Performance
160
+
161
+ Keep transforms simple -- validation runs synchronously on every mutation.
162
+
163
+ ## Complete Example
164
+
165
+ ```typescript
166
+ import { z } from 'zod'
167
+ import { createCollection, SchemaValidationError } from '@tanstack/react-db'
168
+ import { queryCollectionOptions } from '@tanstack/query-db-collection'
169
+
170
+ const todoSchema = z.object({
171
+ id: z.string(),
172
+ text: z.string().min(1, 'Text is required'),
173
+ completed: z.boolean().default(false),
174
+ priority: z.enum(['low', 'medium', 'high']).default('medium'),
175
+ created_at: z
176
+ .union([z.string(), z.date()])
177
+ .transform((val) => (typeof val === 'string' ? new Date(val) : val))
178
+ .default(() => new Date()),
179
+ })
180
+
181
+ const todosCollection = createCollection(
182
+ queryCollectionOptions({
183
+ queryKey: ['todos'],
184
+ queryFn: async () => fetch('/api/todos').then((r) => r.json()),
185
+ queryClient,
186
+ getKey: (item) => item.id,
187
+ schema: todoSchema,
188
+ onInsert: async ({ transaction }) => {
189
+ const todo = transaction.mutations[0].modified
190
+ await api.todos.create({
191
+ ...todo,
192
+ created_at: todo.created_at.toISOString(),
193
+ })
194
+ },
195
+ }),
196
+ )
197
+
198
+ // Defaults and transforms applied
199
+ todosCollection.insert({ id: '1', text: 'Buy groceries' })
200
+ // => { id: "1", text: "Buy groceries", completed: false, priority: "medium", created_at: Date }
201
+
202
+ // Update works -- draft contains TOutput, schema accepts via union
203
+ todosCollection.update('1', (draft) => {
204
+ draft.completed = true
205
+ })
206
+
207
+ // Error handling
208
+ try {
209
+ todosCollection.insert({ id: '2', text: '' })
210
+ } catch (e) {
211
+ if (e instanceof SchemaValidationError) {
212
+ console.log(e.issues) // [{ path: ["text"], message: "Text is required" }]
213
+ }
214
+ }
215
+ ```
@@ -0,0 +1,147 @@
1
+ # TrailBase Adapter Reference
2
+
3
+ ## Install
4
+
5
+ ```bash
6
+ pnpm add @tanstack/trailbase-db-collection @tanstack/react-db trailbase
7
+ ```
8
+
9
+ ## Required Config
10
+
11
+ ```typescript
12
+ import { createCollection } from '@tanstack/react-db'
13
+ import { trailBaseCollectionOptions } from '@tanstack/trailbase-db-collection'
14
+ import { initClient } from 'trailbase'
15
+
16
+ const trailBaseClient = initClient('https://your-trailbase-instance.com')
17
+
18
+ const todosCollection = createCollection(
19
+ trailBaseCollectionOptions({
20
+ id: 'todos',
21
+ recordApi: trailBaseClient.records('todos'),
22
+ getKey: (item) => item.id,
23
+ }),
24
+ )
25
+ ```
26
+
27
+ - `id` -- unique collection identifier
28
+ - `recordApi` -- TrailBase Record API instance from `trailBaseClient.records(tableName)`
29
+ - `getKey` -- extracts unique key from each item
30
+
31
+ ## Optional Config
32
+
33
+ | Option | Default | Description |
34
+ | ----------- | ------- | --------------------------------------------------------------------------------- |
35
+ | `schema` | (none) | StandardSchema validator |
36
+ | `parse` | (none) | Object mapping field names to functions that transform data coming FROM TrailBase |
37
+ | `serialize` | (none) | Object mapping field names to functions that transform data going TO TrailBase |
38
+ | `onInsert` | (none) | Handler called on insert |
39
+ | `onUpdate` | (none) | Handler called on update |
40
+ | `onDelete` | (none) | Handler called on delete |
41
+
42
+ ## Conversions (parse/serialize)
43
+
44
+ TrailBase uses different data formats (e.g. Unix timestamps). Use `parse` and `serialize` for field-level transformations.
45
+
46
+ ```typescript
47
+ type SelectTodo = {
48
+ id: string
49
+ text: string
50
+ created_at: number // Unix timestamp from TrailBase
51
+ completed: boolean
52
+ }
53
+
54
+ type Todo = {
55
+ id: string
56
+ text: string
57
+ created_at: Date // Rich JS type for app usage
58
+ completed: boolean
59
+ }
60
+
61
+ const collection = createCollection<SelectTodo, Todo>(
62
+ trailBaseCollectionOptions({
63
+ id: 'todos',
64
+ recordApi: trailBaseClient.records('todos'),
65
+ getKey: (item) => item.id,
66
+ parse: {
67
+ created_at: (ts) => new Date(ts * 1000),
68
+ },
69
+ serialize: {
70
+ created_at: (date) => Math.floor(date.valueOf() / 1000),
71
+ },
72
+ }),
73
+ )
74
+ ```
75
+
76
+ ## Real-time Subscriptions
77
+
78
+ Automatic when `enable_subscriptions` is enabled on the TrailBase server. No additional client config needed -- the collection subscribes to changes automatically.
79
+
80
+ ## Persistence Handlers
81
+
82
+ ```typescript
83
+ onInsert: async ({ transaction }) => {
84
+ const newItem = transaction.mutations[0].modified
85
+ },
86
+ onUpdate: async ({ transaction }) => {
87
+ const { original, modified } = transaction.mutations[0]
88
+ },
89
+ onDelete: async ({ transaction }) => {
90
+ const deletedItem = transaction.mutations[0].original
91
+ },
92
+ ```
93
+
94
+ TrailBase handles persistence through the Record API automatically. Custom handlers are for additional logic only.
95
+
96
+ ## Complete Example
97
+
98
+ ```typescript
99
+ import { createCollection } from '@tanstack/react-db'
100
+ import { trailBaseCollectionOptions } from '@tanstack/trailbase-db-collection'
101
+ import { initClient } from 'trailbase'
102
+ import { z } from 'zod'
103
+
104
+ const trailBaseClient = initClient('https://your-trailbase-instance.com')
105
+
106
+ const todoSchema = z.object({
107
+ id: z.string(),
108
+ text: z.string(),
109
+ completed: z.boolean(),
110
+ created_at: z.date(),
111
+ })
112
+
113
+ type SelectTodo = {
114
+ id: string
115
+ text: string
116
+ completed: boolean
117
+ created_at: number
118
+ }
119
+
120
+ type Todo = z.infer<typeof todoSchema>
121
+
122
+ const todosCollection = createCollection<SelectTodo, Todo>(
123
+ trailBaseCollectionOptions({
124
+ id: 'todos',
125
+ recordApi: trailBaseClient.records('todos'),
126
+ getKey: (item) => item.id,
127
+ schema: todoSchema,
128
+ parse: {
129
+ created_at: (ts) => new Date(ts * 1000),
130
+ },
131
+ serialize: {
132
+ created_at: (date) => Math.floor(date.valueOf() / 1000),
133
+ },
134
+ onInsert: async ({ transaction }) => {
135
+ console.log('Created:', transaction.mutations[0].modified)
136
+ },
137
+ }),
138
+ )
139
+
140
+ // Usage
141
+ todosCollection.insert({
142
+ id: crypto.randomUUID(),
143
+ text: 'Review PR',
144
+ completed: false,
145
+ created_at: new Date(),
146
+ })
147
+ ```