@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,220 @@
|
|
|
1
|
+
# Local Adapters Reference
|
|
2
|
+
|
|
3
|
+
Both adapters are included in the core package.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pnpm add @tanstack/react-db
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## localOnlyCollectionOptions
|
|
14
|
+
|
|
15
|
+
In-memory only. No persistence. No cross-tab sync.
|
|
16
|
+
|
|
17
|
+
### Required Config
|
|
18
|
+
|
|
19
|
+
```typescript
|
|
20
|
+
import {
|
|
21
|
+
createCollection,
|
|
22
|
+
localOnlyCollectionOptions,
|
|
23
|
+
} from '@tanstack/react-db'
|
|
24
|
+
|
|
25
|
+
const collection = createCollection(
|
|
26
|
+
localOnlyCollectionOptions({
|
|
27
|
+
id: 'ui-state',
|
|
28
|
+
getKey: (item) => item.id,
|
|
29
|
+
}),
|
|
30
|
+
)
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
- `id` -- unique collection identifier
|
|
34
|
+
- `getKey` -- extracts unique key from each item
|
|
35
|
+
|
|
36
|
+
### Optional Config
|
|
37
|
+
|
|
38
|
+
| Option | Default | Description |
|
|
39
|
+
| ------------- | ------- | -------------------------------------- |
|
|
40
|
+
| `schema` | (none) | StandardSchema validator |
|
|
41
|
+
| `initialData` | (none) | Array of items to populate on creation |
|
|
42
|
+
| `onInsert` | (none) | Handler before confirming inserts |
|
|
43
|
+
| `onUpdate` | (none) | Handler before confirming updates |
|
|
44
|
+
| `onDelete` | (none) | Handler before confirming deletes |
|
|
45
|
+
|
|
46
|
+
### Direct Mutations
|
|
47
|
+
|
|
48
|
+
```typescript
|
|
49
|
+
collection.insert({ id: 'theme', mode: 'dark' })
|
|
50
|
+
collection.update('theme', (draft) => {
|
|
51
|
+
draft.mode = 'light'
|
|
52
|
+
})
|
|
53
|
+
collection.delete('theme')
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
### initialData
|
|
57
|
+
|
|
58
|
+
```typescript
|
|
59
|
+
localOnlyCollectionOptions({
|
|
60
|
+
id: 'ui-state',
|
|
61
|
+
getKey: (item) => item.id,
|
|
62
|
+
initialData: [
|
|
63
|
+
{ id: 'sidebar', isOpen: false },
|
|
64
|
+
{ id: 'theme', mode: 'light' },
|
|
65
|
+
],
|
|
66
|
+
})
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
### acceptMutations in Manual Transactions
|
|
70
|
+
|
|
71
|
+
When using `createTransaction`, call `collection.utils.acceptMutations(transaction)` in `mutationFn`:
|
|
72
|
+
|
|
73
|
+
```typescript
|
|
74
|
+
import { createTransaction } from '@tanstack/react-db'
|
|
75
|
+
|
|
76
|
+
const tx = createTransaction({
|
|
77
|
+
mutationFn: async ({ transaction }) => {
|
|
78
|
+
// Handle server mutations first, then:
|
|
79
|
+
localData.utils.acceptMutations(transaction)
|
|
80
|
+
},
|
|
81
|
+
})
|
|
82
|
+
tx.mutate(() => {
|
|
83
|
+
localData.insert({ id: 'draft-1', data: '...' })
|
|
84
|
+
})
|
|
85
|
+
await tx.commit()
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
---
|
|
89
|
+
|
|
90
|
+
## localStorageCollectionOptions
|
|
91
|
+
|
|
92
|
+
Persists to `localStorage`. Cross-tab sync via storage events. Survives reloads.
|
|
93
|
+
|
|
94
|
+
### Required Config
|
|
95
|
+
|
|
96
|
+
```typescript
|
|
97
|
+
import {
|
|
98
|
+
createCollection,
|
|
99
|
+
localStorageCollectionOptions,
|
|
100
|
+
} from '@tanstack/react-db'
|
|
101
|
+
|
|
102
|
+
const collection = createCollection(
|
|
103
|
+
localStorageCollectionOptions({
|
|
104
|
+
id: 'user-preferences',
|
|
105
|
+
storageKey: 'app-user-prefs',
|
|
106
|
+
getKey: (item) => item.id,
|
|
107
|
+
}),
|
|
108
|
+
)
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
- `id` -- unique collection identifier
|
|
112
|
+
- `storageKey` -- localStorage key for all collection data
|
|
113
|
+
- `getKey` -- extracts unique key from each item
|
|
114
|
+
|
|
115
|
+
### Optional Config
|
|
116
|
+
|
|
117
|
+
| Option | Default | Description |
|
|
118
|
+
| ----------------- | -------------- | -------------------------------------------------------------------- |
|
|
119
|
+
| `schema` | (none) | StandardSchema validator |
|
|
120
|
+
| `storage` | `localStorage` | Custom storage (`sessionStorage` or any localStorage-compatible API) |
|
|
121
|
+
| `storageEventApi` | `window` | Event API for cross-tab sync |
|
|
122
|
+
| `onInsert` | (none) | Handler on insert |
|
|
123
|
+
| `onUpdate` | (none) | Handler on update |
|
|
124
|
+
| `onDelete` | (none) | Handler on delete |
|
|
125
|
+
|
|
126
|
+
### Using sessionStorage
|
|
127
|
+
|
|
128
|
+
```typescript
|
|
129
|
+
localStorageCollectionOptions({
|
|
130
|
+
id: 'session-data',
|
|
131
|
+
storageKey: 'session-key',
|
|
132
|
+
storage: sessionStorage,
|
|
133
|
+
getKey: (item) => item.id,
|
|
134
|
+
})
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
### Custom Storage Backend
|
|
138
|
+
|
|
139
|
+
Provide any object with `getItem`, `setItem`, `removeItem`:
|
|
140
|
+
|
|
141
|
+
```typescript
|
|
142
|
+
const encryptedStorage = {
|
|
143
|
+
getItem: (key) => {
|
|
144
|
+
const v = localStorage.getItem(key)
|
|
145
|
+
return v ? decrypt(v) : null
|
|
146
|
+
},
|
|
147
|
+
setItem: (key, value) => localStorage.setItem(key, encrypt(value)),
|
|
148
|
+
removeItem: (key) => localStorage.removeItem(key),
|
|
149
|
+
}
|
|
150
|
+
localStorageCollectionOptions({
|
|
151
|
+
id: 'secure',
|
|
152
|
+
storageKey: 'enc-key',
|
|
153
|
+
storage: encryptedStorage,
|
|
154
|
+
getKey: (i) => i.id,
|
|
155
|
+
})
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
### acceptMutations
|
|
159
|
+
|
|
160
|
+
Same as LocalOnly -- call `collection.utils.acceptMutations(transaction)` in manual transactions.
|
|
161
|
+
|
|
162
|
+
---
|
|
163
|
+
|
|
164
|
+
## Comparison
|
|
165
|
+
|
|
166
|
+
| Feature | LocalOnly | LocalStorage |
|
|
167
|
+
| --------------- | ---------------- | ------------ |
|
|
168
|
+
| Persistence | None (in-memory) | localStorage |
|
|
169
|
+
| Cross-tab sync | No | Yes |
|
|
170
|
+
| Survives reload | No | Yes |
|
|
171
|
+
| Performance | Fastest | Fast |
|
|
172
|
+
| Size limits | Memory | ~5-10MB |
|
|
173
|
+
|
|
174
|
+
## Complete Example
|
|
175
|
+
|
|
176
|
+
```typescript
|
|
177
|
+
import {
|
|
178
|
+
createCollection,
|
|
179
|
+
localOnlyCollectionOptions,
|
|
180
|
+
localStorageCollectionOptions,
|
|
181
|
+
} from '@tanstack/react-db'
|
|
182
|
+
import { z } from 'zod'
|
|
183
|
+
|
|
184
|
+
// In-memory UI state
|
|
185
|
+
const modalState = createCollection(
|
|
186
|
+
localOnlyCollectionOptions({
|
|
187
|
+
id: 'modal-state',
|
|
188
|
+
getKey: (item) => item.id,
|
|
189
|
+
initialData: [
|
|
190
|
+
{ id: 'confirm-delete', isOpen: false },
|
|
191
|
+
{ id: 'settings', isOpen: false },
|
|
192
|
+
],
|
|
193
|
+
}),
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
// Persistent user prefs
|
|
197
|
+
const userPrefs = createCollection(
|
|
198
|
+
localStorageCollectionOptions({
|
|
199
|
+
id: 'user-preferences',
|
|
200
|
+
storageKey: 'app-user-prefs',
|
|
201
|
+
getKey: (item) => item.id,
|
|
202
|
+
schema: z.object({
|
|
203
|
+
id: z.string(),
|
|
204
|
+
theme: z.enum(['light', 'dark', 'auto']),
|
|
205
|
+
language: z.string(),
|
|
206
|
+
notifications: z.boolean(),
|
|
207
|
+
}),
|
|
208
|
+
}),
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
modalState.update('settings', (draft) => {
|
|
212
|
+
draft.isOpen = true
|
|
213
|
+
})
|
|
214
|
+
userPrefs.insert({
|
|
215
|
+
id: 'current-user',
|
|
216
|
+
theme: 'dark',
|
|
217
|
+
language: 'en',
|
|
218
|
+
notifications: true,
|
|
219
|
+
})
|
|
220
|
+
```
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
# PowerSync Adapter Reference
|
|
2
|
+
|
|
3
|
+
## Install
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
pnpm add @tanstack/powersync-db-collection @powersync/web @journeyapps/wa-sqlite
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
## Required Config
|
|
10
|
+
|
|
11
|
+
```typescript
|
|
12
|
+
import { createCollection } from '@tanstack/react-db'
|
|
13
|
+
import { powerSyncCollectionOptions } from '@tanstack/powersync-db-collection'
|
|
14
|
+
import { Schema, Table, column, PowerSyncDatabase } from '@powersync/web'
|
|
15
|
+
|
|
16
|
+
const APP_SCHEMA = new Schema({
|
|
17
|
+
documents: new Table({
|
|
18
|
+
name: column.text,
|
|
19
|
+
author: column.text,
|
|
20
|
+
created_at: column.text,
|
|
21
|
+
archived: column.integer,
|
|
22
|
+
}),
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
const db = new PowerSyncDatabase({
|
|
26
|
+
database: { dbFilename: 'app.sqlite' },
|
|
27
|
+
schema: APP_SCHEMA,
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
const documentsCollection = createCollection(
|
|
31
|
+
powerSyncCollectionOptions({
|
|
32
|
+
database: db,
|
|
33
|
+
table: APP_SCHEMA.props.documents,
|
|
34
|
+
}),
|
|
35
|
+
)
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
- `database` -- `PowerSyncDatabase` instance
|
|
39
|
+
- `table` -- PowerSync `Table` from schema (provides `getKey` and type inference)
|
|
40
|
+
|
|
41
|
+
## Optional Config (with defaults)
|
|
42
|
+
|
|
43
|
+
| Option | Default | Description |
|
|
44
|
+
| ------------------------ | ------- | ------------------------------------------------------------------------------------- |
|
|
45
|
+
| `schema` | (none) | StandardSchema for mutation validation |
|
|
46
|
+
| `deserializationSchema` | (none) | Transforms SQLite types to output types; required when input types differ from SQLite |
|
|
47
|
+
| `onDeserializationError` | (none) | Fatal error handler; **required** when using `schema` or `deserializationSchema` |
|
|
48
|
+
| `serializer` | (none) | Per-field functions to serialize output types back to SQLite |
|
|
49
|
+
| `syncBatchSize` | `1000` | Batch size for initial sync |
|
|
50
|
+
|
|
51
|
+
### SQLite Type Mapping
|
|
52
|
+
|
|
53
|
+
| PowerSync Column | TypeScript Type |
|
|
54
|
+
| ---------------- | ---------------- |
|
|
55
|
+
| `column.text` | `string \| null` |
|
|
56
|
+
| `column.integer` | `number \| null` |
|
|
57
|
+
| `column.real` | `number \| null` |
|
|
58
|
+
|
|
59
|
+
All columns nullable by default. `id: string` is always included automatically.
|
|
60
|
+
|
|
61
|
+
## Conversions (4 patterns)
|
|
62
|
+
|
|
63
|
+
### 1. Type Inference Only (no schema)
|
|
64
|
+
|
|
65
|
+
```typescript
|
|
66
|
+
const collection = createCollection(
|
|
67
|
+
powerSyncCollectionOptions({
|
|
68
|
+
database: db,
|
|
69
|
+
table: APP_SCHEMA.props.documents,
|
|
70
|
+
}),
|
|
71
|
+
)
|
|
72
|
+
// Input/Output: { id: string, name: string | null, created_at: string | null, ... }
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
### 2. Schema Validation (same SQLite types)
|
|
76
|
+
|
|
77
|
+
```typescript
|
|
78
|
+
const schema = z.object({
|
|
79
|
+
id: z.string(),
|
|
80
|
+
name: z.string().min(3),
|
|
81
|
+
author: z.string(),
|
|
82
|
+
created_at: z.string(),
|
|
83
|
+
archived: z.number(),
|
|
84
|
+
})
|
|
85
|
+
const collection = createCollection(
|
|
86
|
+
powerSyncCollectionOptions({
|
|
87
|
+
database: db,
|
|
88
|
+
table: APP_SCHEMA.props.documents,
|
|
89
|
+
schema,
|
|
90
|
+
onDeserializationError: (error) => {
|
|
91
|
+
/* fatal */
|
|
92
|
+
},
|
|
93
|
+
}),
|
|
94
|
+
)
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
### 3. Transform SQLite to Rich Output Types
|
|
98
|
+
|
|
99
|
+
```typescript
|
|
100
|
+
const schema = z.object({
|
|
101
|
+
id: z.string(),
|
|
102
|
+
name: z.string().nullable(),
|
|
103
|
+
created_at: z
|
|
104
|
+
.string()
|
|
105
|
+
.nullable()
|
|
106
|
+
.transform((val) => (val ? new Date(val) : null)),
|
|
107
|
+
archived: z
|
|
108
|
+
.number()
|
|
109
|
+
.nullable()
|
|
110
|
+
.transform((val) => (val != null ? val > 0 : null)),
|
|
111
|
+
})
|
|
112
|
+
const collection = createCollection(
|
|
113
|
+
powerSyncCollectionOptions({
|
|
114
|
+
database: db,
|
|
115
|
+
table: APP_SCHEMA.props.documents,
|
|
116
|
+
schema,
|
|
117
|
+
onDeserializationError: (error) => {
|
|
118
|
+
/* fatal */
|
|
119
|
+
},
|
|
120
|
+
serializer: { created_at: (value) => (value ? value.toISOString() : null) },
|
|
121
|
+
}),
|
|
122
|
+
)
|
|
123
|
+
// Input: { created_at: string | null, ... }
|
|
124
|
+
// Output: { created_at: Date | null, archived: boolean | null, ... }
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
### 4. Custom Input + Output with deserializationSchema
|
|
128
|
+
|
|
129
|
+
```typescript
|
|
130
|
+
const schema = z.object({
|
|
131
|
+
id: z.string(),
|
|
132
|
+
name: z.string(),
|
|
133
|
+
created_at: z.date(),
|
|
134
|
+
archived: z.boolean(),
|
|
135
|
+
})
|
|
136
|
+
const deserializationSchema = z.object({
|
|
137
|
+
id: z.string(),
|
|
138
|
+
name: z.string(),
|
|
139
|
+
created_at: z.string().transform((val) => new Date(val)),
|
|
140
|
+
archived: z.number().transform((val) => val > 0),
|
|
141
|
+
})
|
|
142
|
+
const collection = createCollection(
|
|
143
|
+
powerSyncCollectionOptions({
|
|
144
|
+
database: db,
|
|
145
|
+
table: APP_SCHEMA.props.documents,
|
|
146
|
+
schema,
|
|
147
|
+
deserializationSchema,
|
|
148
|
+
onDeserializationError: (error) => {
|
|
149
|
+
/* fatal */
|
|
150
|
+
},
|
|
151
|
+
}),
|
|
152
|
+
)
|
|
153
|
+
// Input: { created_at: Date, archived: boolean }
|
|
154
|
+
// Output: { created_at: Date, archived: boolean }
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
## Metadata Tracking
|
|
158
|
+
|
|
159
|
+
Enable on the table, then pass metadata with operations:
|
|
160
|
+
|
|
161
|
+
```typescript
|
|
162
|
+
const APP_SCHEMA = new Schema({
|
|
163
|
+
documents: new Table({ name: column.text }, { trackMetadata: true }),
|
|
164
|
+
})
|
|
165
|
+
|
|
166
|
+
await collection.insert(
|
|
167
|
+
{ id: crypto.randomUUID(), name: 'Report' },
|
|
168
|
+
{ metadata: { source: 'web-app', userId: 'user-123' } },
|
|
169
|
+
).isPersisted.promise
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
Metadata appears as `entry.metadata` (stringified JSON) in PowerSync `CrudEntry`.
|
|
173
|
+
|
|
174
|
+
## Advanced Transactions
|
|
175
|
+
|
|
176
|
+
```typescript
|
|
177
|
+
import { createTransaction } from '@tanstack/react-db'
|
|
178
|
+
import { PowerSyncTransactor } from '@tanstack/powersync-db-collection'
|
|
179
|
+
|
|
180
|
+
const tx = createTransaction({
|
|
181
|
+
autoCommit: false,
|
|
182
|
+
mutationFn: async ({ transaction }) => {
|
|
183
|
+
await new PowerSyncTransactor({ database: db }).applyTransaction(
|
|
184
|
+
transaction,
|
|
185
|
+
)
|
|
186
|
+
},
|
|
187
|
+
})
|
|
188
|
+
tx.mutate(() => {
|
|
189
|
+
documentsCollection.insert({
|
|
190
|
+
id: crypto.randomUUID(),
|
|
191
|
+
name: 'Doc 1',
|
|
192
|
+
created_at: new Date().toISOString(),
|
|
193
|
+
})
|
|
194
|
+
})
|
|
195
|
+
await tx.commit()
|
|
196
|
+
await tx.isPersisted.promise
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
## Complete Example
|
|
200
|
+
|
|
201
|
+
```typescript
|
|
202
|
+
import { Schema, Table, column, PowerSyncDatabase } from '@powersync/web'
|
|
203
|
+
import { createCollection } from '@tanstack/react-db'
|
|
204
|
+
import { powerSyncCollectionOptions } from '@tanstack/powersync-db-collection'
|
|
205
|
+
import { z } from 'zod'
|
|
206
|
+
|
|
207
|
+
const APP_SCHEMA = new Schema({
|
|
208
|
+
tasks: new Table({
|
|
209
|
+
title: column.text,
|
|
210
|
+
due_date: column.text,
|
|
211
|
+
completed: column.integer,
|
|
212
|
+
}),
|
|
213
|
+
})
|
|
214
|
+
const db = new PowerSyncDatabase({
|
|
215
|
+
database: { dbFilename: 'app.sqlite' },
|
|
216
|
+
schema: APP_SCHEMA,
|
|
217
|
+
})
|
|
218
|
+
|
|
219
|
+
const taskSchema = z.object({
|
|
220
|
+
id: z.string(),
|
|
221
|
+
title: z.string().nullable(),
|
|
222
|
+
due_date: z
|
|
223
|
+
.string()
|
|
224
|
+
.nullable()
|
|
225
|
+
.transform((val) => (val ? new Date(val) : null)),
|
|
226
|
+
completed: z
|
|
227
|
+
.number()
|
|
228
|
+
.nullable()
|
|
229
|
+
.transform((val) => (val != null ? val > 0 : null)),
|
|
230
|
+
})
|
|
231
|
+
|
|
232
|
+
const tasksCollection = createCollection(
|
|
233
|
+
powerSyncCollectionOptions({
|
|
234
|
+
database: db,
|
|
235
|
+
table: APP_SCHEMA.props.tasks,
|
|
236
|
+
schema: taskSchema,
|
|
237
|
+
onDeserializationError: (error) => console.error('Fatal:', error),
|
|
238
|
+
syncBatchSize: 500,
|
|
239
|
+
}),
|
|
240
|
+
)
|
|
241
|
+
```
|
|
@@ -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
|