@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.
@@ -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