@tanstack/db 0.5.29 → 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.
- package/dist/cjs/index.cjs +10 -10
- package/dist/cjs/query/builder/index.cjs +4 -2
- package/dist/cjs/query/builder/index.cjs.map +1 -1
- package/dist/esm/index.js +2 -2
- package/dist/esm/query/builder/index.js +5 -3
- package/dist/esm/query/builder/index.js.map +1 -1
- package/package.json +3 -2
- package/skills/db-core/SKILL.md +61 -0
- package/skills/db-core/collection-setup/SKILL.md +427 -0
- package/skills/db-core/collection-setup/references/electric-adapter.md +238 -0
- package/skills/db-core/collection-setup/references/local-adapters.md +220 -0
- package/skills/db-core/collection-setup/references/powersync-adapter.md +241 -0
- package/skills/db-core/collection-setup/references/query-adapter.md +183 -0
- package/skills/db-core/collection-setup/references/rxdb-adapter.md +152 -0
- package/skills/db-core/collection-setup/references/schema-patterns.md +215 -0
- package/skills/db-core/collection-setup/references/trailbase-adapter.md +147 -0
- package/skills/db-core/custom-adapter/SKILL.md +285 -0
- package/skills/db-core/live-queries/SKILL.md +332 -0
- package/skills/db-core/live-queries/references/operators.md +302 -0
- package/skills/db-core/mutations-optimistic/SKILL.md +375 -0
- package/skills/db-core/mutations-optimistic/references/transaction-api.md +207 -0
- package/skills/meta-framework/SKILL.md +361 -0
- package/src/query/builder/index.ts +17 -2
|
@@ -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
|
+
```
|