@tanstack/db 0.5.30 → 0.5.32
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/collection/subscription.cjs +6 -6
- package/dist/cjs/collection/subscription.cjs.map +1 -1
- package/dist/cjs/errors.cjs +8 -0
- package/dist/cjs/errors.cjs.map +1 -1
- package/dist/cjs/errors.d.cts +3 -0
- package/dist/cjs/index.cjs +13 -10
- package/dist/cjs/index.cjs.map +1 -1
- package/dist/cjs/query/builder/types.d.cts +28 -31
- package/dist/cjs/query/compiler/index.cjs +3 -0
- package/dist/cjs/query/compiler/index.cjs.map +1 -1
- package/dist/cjs/query/index.d.cts +1 -0
- package/dist/cjs/query/query-once.cjs +28 -0
- package/dist/cjs/query/query-once.cjs.map +1 -0
- package/dist/cjs/query/query-once.d.cts +57 -0
- package/dist/cjs/query/subset-dedupe.cjs +8 -7
- package/dist/cjs/query/subset-dedupe.cjs.map +1 -1
- package/dist/esm/collection/subscription.js +6 -6
- package/dist/esm/collection/subscription.js.map +1 -1
- package/dist/esm/errors.d.ts +3 -0
- package/dist/esm/errors.js +8 -0
- package/dist/esm/errors.js.map +1 -1
- package/dist/esm/index.js +6 -3
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/query/builder/types.d.ts +28 -31
- package/dist/esm/query/compiler/index.js +4 -1
- package/dist/esm/query/compiler/index.js.map +1 -1
- package/dist/esm/query/index.d.ts +1 -0
- package/dist/esm/query/query-once.d.ts +57 -0
- package/dist/esm/query/query-once.js +28 -0
- package/dist/esm/query/query-once.js.map +1 -0
- package/dist/esm/query/subset-dedupe.js +8 -7
- package/dist/esm/query/subset-dedupe.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/collection/subscription.ts +6 -6
- package/src/errors.ts +11 -0
- package/src/query/builder/types.ts +64 -50
- package/src/query/compiler/index.ts +5 -0
- package/src/query/index.ts +3 -0
- package/src/query/query-once.ts +115 -0
- package/src/query/subset-dedupe.ts +14 -15
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
# Electric Adapter Reference
|
|
2
|
+
|
|
3
|
+
## Install
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
pnpm add @tanstack/electric-db-collection @tanstack/react-db
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
## Required Config
|
|
10
|
+
|
|
11
|
+
```typescript
|
|
12
|
+
import { createCollection } from '@tanstack/react-db'
|
|
13
|
+
import { electricCollectionOptions } from '@tanstack/electric-db-collection'
|
|
14
|
+
|
|
15
|
+
const collection = createCollection(
|
|
16
|
+
electricCollectionOptions({
|
|
17
|
+
shapeOptions: { url: '/api/todos' },
|
|
18
|
+
getKey: (item) => item.id,
|
|
19
|
+
}),
|
|
20
|
+
)
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
- `shapeOptions` -- ElectricSQL ShapeStream config; `url` is the proxy URL to Electric
|
|
24
|
+
- `getKey` -- extracts unique key from each item
|
|
25
|
+
|
|
26
|
+
## Optional Config
|
|
27
|
+
|
|
28
|
+
| Option | Default | Description |
|
|
29
|
+
| --------------------- | ------- | --------------------------------------------------- |
|
|
30
|
+
| `id` | (none) | Unique collection identifier |
|
|
31
|
+
| `schema` | (none) | StandardSchema validator |
|
|
32
|
+
| `shapeOptions.params` | (none) | Additional shape params (e.g. `{ table: 'todos' }`) |
|
|
33
|
+
| `onInsert` | (none) | Persistence handler; should return `{ txid }` |
|
|
34
|
+
| `onUpdate` | (none) | Persistence handler; should return `{ txid }` |
|
|
35
|
+
| `onDelete` | (none) | Persistence handler; should return `{ txid }` |
|
|
36
|
+
|
|
37
|
+
## Three Sync Strategies
|
|
38
|
+
|
|
39
|
+
### 1. Txid Return (Recommended)
|
|
40
|
+
|
|
41
|
+
Handler returns `{ txid }`. Client waits for that txid in the Electric stream.
|
|
42
|
+
|
|
43
|
+
```typescript
|
|
44
|
+
onInsert: async ({ transaction }) => {
|
|
45
|
+
const response = await api.todos.create(transaction.mutations[0].modified)
|
|
46
|
+
return { txid: response.txid }
|
|
47
|
+
},
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
### 2. awaitMatch (Custom Match)
|
|
51
|
+
|
|
52
|
+
Use when txids are unavailable. Import `isChangeMessage` to match on message content.
|
|
53
|
+
|
|
54
|
+
```typescript
|
|
55
|
+
import { isChangeMessage } from "@tanstack/electric-db-collection"
|
|
56
|
+
|
|
57
|
+
onInsert: async ({ transaction, collection }) => {
|
|
58
|
+
const newItem = transaction.mutations[0].modified
|
|
59
|
+
await api.todos.create(newItem)
|
|
60
|
+
await collection.utils.awaitMatch(
|
|
61
|
+
(message) =>
|
|
62
|
+
isChangeMessage(message) &&
|
|
63
|
+
message.headers.operation === "insert" &&
|
|
64
|
+
message.value.text === newItem.text,
|
|
65
|
+
5000 // timeout ms, defaults to 3000
|
|
66
|
+
)
|
|
67
|
+
},
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
### 3. Simple Timeout (Prototyping)
|
|
71
|
+
|
|
72
|
+
```typescript
|
|
73
|
+
onInsert: async ({ transaction }) => {
|
|
74
|
+
await api.todos.create(transaction.mutations[0].modified)
|
|
75
|
+
await new Promise((resolve) => setTimeout(resolve, 2000))
|
|
76
|
+
},
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## Utility Methods (`collection.utils`)
|
|
80
|
+
|
|
81
|
+
- `awaitTxId(txid, timeout?)` -- wait for txid in Electric stream; default timeout 30s
|
|
82
|
+
- `awaitMatch(matchFn, timeout?)` -- wait for message matching predicate; default timeout 3000ms
|
|
83
|
+
|
|
84
|
+
### Helper Exports
|
|
85
|
+
|
|
86
|
+
```typescript
|
|
87
|
+
import {
|
|
88
|
+
isChangeMessage,
|
|
89
|
+
isControlMessage,
|
|
90
|
+
} from '@tanstack/electric-db-collection'
|
|
91
|
+
// isChangeMessage(msg) -- true for insert/update/delete
|
|
92
|
+
// isControlMessage(msg) -- true for up-to-date/must-refetch
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
## generateTxId Backend Pattern
|
|
96
|
+
|
|
97
|
+
The txid **must** be queried inside the same Postgres transaction as the mutation.
|
|
98
|
+
|
|
99
|
+
```typescript
|
|
100
|
+
async function generateTxId(tx: any): Promise<number> {
|
|
101
|
+
const result = await tx`SELECT pg_current_xact_id()::xid::text as txid`
|
|
102
|
+
const txid = result[0]?.txid
|
|
103
|
+
if (txid === undefined) throw new Error('Failed to get transaction ID')
|
|
104
|
+
return parseInt(txid, 10)
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
async function createTodo(data) {
|
|
108
|
+
let txid!: number
|
|
109
|
+
const result = await sql.begin(async (tx) => {
|
|
110
|
+
txid = await generateTxId(tx) // INSIDE the transaction
|
|
111
|
+
const [todo] = await tx`INSERT INTO todos ${tx(data)} RETURNING *`
|
|
112
|
+
return todo
|
|
113
|
+
})
|
|
114
|
+
return { todo: result, txid }
|
|
115
|
+
}
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
Querying txid outside the transaction produces a mismatched txid -- `awaitTxId` stalls indefinitely.
|
|
119
|
+
|
|
120
|
+
## Schema vs Parser: Two Separate Paths
|
|
121
|
+
|
|
122
|
+
When using Electric with a schema, data enters the collection via **two independent paths**:
|
|
123
|
+
|
|
124
|
+
1. **Sync path** — Electric's `ShapeStream` applies the `parser` from `shapeOptions`. The schema is NOT applied to synced data.
|
|
125
|
+
2. **Mutation path** — `insert()` and `update()` run through the collection schema. The parser is not involved.
|
|
126
|
+
|
|
127
|
+
For types that need transformation (e.g., `timestamptz`), you need BOTH configured:
|
|
128
|
+
|
|
129
|
+
```typescript
|
|
130
|
+
const todosCollection = createCollection(
|
|
131
|
+
electricCollectionOptions({
|
|
132
|
+
schema: z.object({
|
|
133
|
+
id: z.string(),
|
|
134
|
+
text: z.string(),
|
|
135
|
+
completed: z.boolean(), // Electric auto-parses bools
|
|
136
|
+
created_at: z.coerce.date(), // mutation path: coerce string → Date
|
|
137
|
+
}),
|
|
138
|
+
shapeOptions: {
|
|
139
|
+
url: '/api/todos',
|
|
140
|
+
parser: {
|
|
141
|
+
timestamptz: (value: string) => new Date(value), // sync path: parse incoming strings
|
|
142
|
+
},
|
|
143
|
+
},
|
|
144
|
+
getKey: (item) => item.id,
|
|
145
|
+
}),
|
|
146
|
+
)
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
### Postgres → Electric type handling
|
|
150
|
+
|
|
151
|
+
| PG type | Electric auto-parses? | Schema needed? | Parser needed? |
|
|
152
|
+
| -------------- | --------------------- | ----------------- | --------------------------------------------------- |
|
|
153
|
+
| `text`, `uuid` | Yes (string) | `z.string()` | No |
|
|
154
|
+
| `int4`, `int8` | Yes (number) | `z.number()` | No |
|
|
155
|
+
| `bool` | Yes (boolean) | `z.boolean()` | No |
|
|
156
|
+
| `timestamptz` | No (stays string) | `z.coerce.date()` | Yes — `parser: { timestamptz: (v) => new Date(v) }` |
|
|
157
|
+
| `jsonb` | Yes (parsed object) | As needed | No |
|
|
158
|
+
|
|
159
|
+
Note: `z.coerce.date()` is Zod-specific. Other StandardSchema libraries have their own coercion patterns.
|
|
160
|
+
|
|
161
|
+
## Proxy Route
|
|
162
|
+
|
|
163
|
+
Electric collections connect to a proxy URL (`shapeOptions.url`), not directly to Electric. Your app server must forward shape requests to Electric, passing through the Electric protocol query params.
|
|
164
|
+
|
|
165
|
+
The proxy route must:
|
|
166
|
+
|
|
167
|
+
1. Accept GET requests at the URL you specify in `shapeOptions.url`
|
|
168
|
+
2. Forward all query parameters (these are Electric protocol params like `offset`, `handle`, `live`, etc.)
|
|
169
|
+
3. Proxy the response (SSE stream) back to the client
|
|
170
|
+
4. Optionally add authentication headers or filter params
|
|
171
|
+
|
|
172
|
+
Implementation depends on your framework — use `createServerFn` in TanStack Start, API routes in Next.js, `loader` in Remix, etc. See the `@electric-sql/client` skills for proxy route examples:
|
|
173
|
+
|
|
174
|
+
```bash
|
|
175
|
+
npx @electric-sql/client intent list
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
## Electric Client Skills
|
|
179
|
+
|
|
180
|
+
For deeper Electric-specific guidance (ShapeStream config, shape filtering, etc.), load the Electric client's built-in skills:
|
|
181
|
+
|
|
182
|
+
```bash
|
|
183
|
+
npx @electric-sql/client intent list
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
## Debug Logging
|
|
187
|
+
|
|
188
|
+
```javascript
|
|
189
|
+
localStorage.debug = 'ts/db:electric'
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
## Complete Example
|
|
193
|
+
|
|
194
|
+
Always use a schema — types are inferred automatically, avoiding generic placement confusion.
|
|
195
|
+
|
|
196
|
+
```typescript
|
|
197
|
+
import { createCollection } from '@tanstack/react-db'
|
|
198
|
+
import { electricCollectionOptions } from '@tanstack/electric-db-collection'
|
|
199
|
+
import { z } from 'zod'
|
|
200
|
+
|
|
201
|
+
const todoSchema = z.object({
|
|
202
|
+
id: z.string(),
|
|
203
|
+
text: z.string().min(1),
|
|
204
|
+
completed: z.boolean(),
|
|
205
|
+
created_at: z.coerce.date(),
|
|
206
|
+
})
|
|
207
|
+
|
|
208
|
+
const todosCollection = createCollection(
|
|
209
|
+
electricCollectionOptions({
|
|
210
|
+
id: 'todos',
|
|
211
|
+
schema: todoSchema,
|
|
212
|
+
getKey: (item) => item.id,
|
|
213
|
+
shapeOptions: {
|
|
214
|
+
url: '/api/todos',
|
|
215
|
+
params: { table: 'todos' },
|
|
216
|
+
parser: {
|
|
217
|
+
timestamptz: (value: string) => new Date(value), // sync path
|
|
218
|
+
},
|
|
219
|
+
},
|
|
220
|
+
onInsert: async ({ transaction }) => {
|
|
221
|
+
const response = await api.todos.create(transaction.mutations[0].modified)
|
|
222
|
+
return { txid: response.txid }
|
|
223
|
+
},
|
|
224
|
+
onUpdate: async ({ transaction }) => {
|
|
225
|
+
const { original, changes } = transaction.mutations[0]
|
|
226
|
+
const response = await api.todos.update({
|
|
227
|
+
where: { id: original.id },
|
|
228
|
+
data: changes,
|
|
229
|
+
})
|
|
230
|
+
return { txid: response.txid }
|
|
231
|
+
},
|
|
232
|
+
onDelete: async ({ transaction }) => {
|
|
233
|
+
const response = await api.todos.delete(transaction.mutations[0].key)
|
|
234
|
+
return { txid: response.txid }
|
|
235
|
+
},
|
|
236
|
+
}),
|
|
237
|
+
)
|
|
238
|
+
```
|
|
@@ -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
|
+
```
|