pbtsdb 0.0.1 → 0.1.0
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/README.md +546 -574
- package/dist/index.d.ts +376 -403
- package/dist/index.js +280 -477
- package/dist/index.js.map +1 -1
- package/llms.txt +163 -22
- package/package.json +20 -20
package/README.md
CHANGED
|
@@ -8,6 +8,7 @@ A TypeScript library that seamlessly integrates [PocketBase](https://pocketbase.
|
|
|
8
8
|
- 🎯 **Full TypeScript type safety** for queries and relations
|
|
9
9
|
- ⚡ **Reactive collections** with TanStack DB
|
|
10
10
|
- 🔄 **Automatic caching** via TanStack Query
|
|
11
|
+
- ✨ **Optimistic mutations** with insert/update/delete support
|
|
11
12
|
- 🎨 **React hooks** for easy component integration
|
|
12
13
|
- 🔗 **Type-safe joins** and relation expansion
|
|
13
14
|
|
|
@@ -17,21 +18,22 @@ A TypeScript library that seamlessly integrates [PocketBase](https://pocketbase.
|
|
|
17
18
|
- [Quick Start](#quick-start)
|
|
18
19
|
- [Core Concepts](#core-concepts)
|
|
19
20
|
- [API Reference](#api-reference)
|
|
20
|
-
- [
|
|
21
|
-
- [React
|
|
21
|
+
- [createCollection()](#createcollection)
|
|
22
|
+
- [React Integration](#react-integration)
|
|
22
23
|
- [Subscriptions](#subscriptions)
|
|
23
24
|
- [Usage Examples](#usage-examples)
|
|
24
25
|
- [Basic Queries](#basic-queries)
|
|
25
26
|
- [Filtering and Sorting](#filtering-and-sorting)
|
|
26
27
|
- [Relations and Joins](#relations-and-joins)
|
|
27
28
|
- [Real-time Updates](#real-time-updates)
|
|
29
|
+
- [Mutations](#mutations)
|
|
28
30
|
- [TypeScript](#typescript)
|
|
29
31
|
- [Best Practices](#best-practices)
|
|
30
32
|
|
|
31
33
|
## Installation
|
|
32
34
|
|
|
33
35
|
```bash
|
|
34
|
-
npm install
|
|
36
|
+
npm install pbtsdb pocketbase @tanstack/react-query @tanstack/react-db @tanstack/query-db-collection
|
|
35
37
|
```
|
|
36
38
|
|
|
37
39
|
### Peer Dependencies
|
|
@@ -43,107 +45,187 @@ npm install pocketbase-tanstack-db pocketbase @tanstack/react-query @tanstack/re
|
|
|
43
45
|
- `react` >= 18.0.0
|
|
44
46
|
- `react-dom` >= 18.0.0
|
|
45
47
|
|
|
48
|
+
All peer dependencies use minimum version constraints - newer versions should work.
|
|
49
|
+
|
|
46
50
|
## Quick Start
|
|
47
51
|
|
|
52
|
+
Let's build a **real-world blog** with posts, authors, and comments using pbtsdb.
|
|
53
|
+
|
|
48
54
|
### 1. Define Your Schema
|
|
49
55
|
|
|
50
|
-
|
|
51
|
-
that you install https://github.com/satohshi/pocketbase-schema-generator as a pocketbase hook. When configured a schema file will be auto-generated on every schema change.
|
|
56
|
+
First, generate your types from PocketBase. Install [pocketbase-schema-generator](https://github.com/satohshi/pocketbase-schema-generator) as a PocketBase hook to auto-generate types on schema changes.
|
|
52
57
|
|
|
53
58
|
```typescript
|
|
54
|
-
|
|
59
|
+
// schema.ts - Auto-generated from PocketBase
|
|
60
|
+
interface Post {
|
|
61
|
+
id: string;
|
|
62
|
+
title: string;
|
|
63
|
+
content: string;
|
|
64
|
+
author: string; // FK to users
|
|
65
|
+
published: boolean;
|
|
66
|
+
created: string;
|
|
67
|
+
updated: string;
|
|
68
|
+
}
|
|
55
69
|
|
|
56
|
-
|
|
57
|
-
interface Author {
|
|
70
|
+
interface User {
|
|
58
71
|
id: string;
|
|
59
|
-
|
|
72
|
+
username: string;
|
|
60
73
|
email: string;
|
|
74
|
+
avatar?: string;
|
|
61
75
|
created: string;
|
|
62
76
|
updated: string;
|
|
63
77
|
}
|
|
64
78
|
|
|
65
|
-
interface
|
|
79
|
+
interface Comment {
|
|
66
80
|
id: string;
|
|
67
|
-
|
|
68
|
-
author: string;
|
|
69
|
-
|
|
70
|
-
published_date: string;
|
|
81
|
+
post: string; // FK to posts
|
|
82
|
+
author: string; // FK to users
|
|
83
|
+
text: string;
|
|
71
84
|
created: string;
|
|
72
85
|
updated: string;
|
|
73
86
|
}
|
|
74
87
|
|
|
75
|
-
//
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
};
|
|
88
|
+
// Schema declaration for pbtsdb
|
|
89
|
+
type BlogSchema = {
|
|
90
|
+
posts: {
|
|
91
|
+
type: Post;
|
|
92
|
+
relations: { author?: User };
|
|
93
|
+
};
|
|
94
|
+
users: {
|
|
95
|
+
type: User;
|
|
96
|
+
relations: {};
|
|
85
97
|
};
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
};
|
|
92
|
-
back: {};
|
|
98
|
+
comments: {
|
|
99
|
+
type: Comment;
|
|
100
|
+
relations: {
|
|
101
|
+
post?: Post;
|
|
102
|
+
author?: User;
|
|
93
103
|
};
|
|
94
104
|
};
|
|
95
105
|
}
|
|
96
106
|
```
|
|
97
107
|
|
|
98
|
-
### 2.
|
|
108
|
+
### 2. Set Up Your App
|
|
99
109
|
|
|
100
110
|
```typescript
|
|
111
|
+
// app.tsx
|
|
101
112
|
import PocketBase from 'pocketbase';
|
|
102
|
-
import { QueryClient } from '@tanstack/react-query';
|
|
103
|
-
import {
|
|
113
|
+
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
|
114
|
+
import { createCollection, createReactProvider } from 'pbtsdb';
|
|
104
115
|
|
|
105
|
-
// Initialize PocketBase
|
|
106
116
|
const pb = new PocketBase('http://localhost:8090');
|
|
107
|
-
|
|
108
|
-
// Initialize QueryClient
|
|
109
117
|
const queryClient = new QueryClient({
|
|
110
118
|
defaultOptions: {
|
|
111
|
-
queries: {
|
|
112
|
-
|
|
113
|
-
},
|
|
114
|
-
},
|
|
119
|
+
queries: { staleTime: 60_000 } // Cache for 1 minute
|
|
120
|
+
}
|
|
115
121
|
});
|
|
116
122
|
|
|
117
|
-
// Create
|
|
118
|
-
const
|
|
123
|
+
// Create collections with automatic type inference
|
|
124
|
+
const c = createCollection<BlogSchema>(pb, queryClient);
|
|
125
|
+
export const { Provider, useStore } = createReactProvider({
|
|
126
|
+
posts: c('posts', {
|
|
127
|
+
omitOnInsert: ['created', 'updated'] as const
|
|
128
|
+
}),
|
|
129
|
+
users: c('users', {}),
|
|
130
|
+
comments: c('comments', {
|
|
131
|
+
omitOnInsert: ['created', 'updated'] as const
|
|
132
|
+
})
|
|
133
|
+
});
|
|
119
134
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
135
|
+
export function App() {
|
|
136
|
+
return (
|
|
137
|
+
<QueryClientProvider client={queryClient}>
|
|
138
|
+
<Provider>
|
|
139
|
+
<BlogDashboard />
|
|
140
|
+
</Provider>
|
|
141
|
+
</QueryClientProvider>
|
|
142
|
+
);
|
|
143
|
+
}
|
|
123
144
|
```
|
|
124
145
|
|
|
125
|
-
### 3.
|
|
146
|
+
### 3. Build Your Components
|
|
126
147
|
|
|
127
148
|
```typescript
|
|
149
|
+
// BlogDashboard.tsx
|
|
128
150
|
import { useLiveQuery } from '@tanstack/react-db';
|
|
151
|
+
import { useStore } from './app';
|
|
129
152
|
|
|
130
|
-
function
|
|
131
|
-
const
|
|
132
|
-
|
|
153
|
+
export function BlogDashboard() {
|
|
154
|
+
const [posts] = useStore('posts');
|
|
155
|
+
|
|
156
|
+
const { data: allPosts, isLoading } = useLiveQuery((q) =>
|
|
157
|
+
q.from({ posts })
|
|
158
|
+
.orderBy(({ posts }) => posts.created, 'desc')
|
|
133
159
|
);
|
|
134
160
|
|
|
135
|
-
if (isLoading) return <div>Loading...</div>;
|
|
161
|
+
if (isLoading) return <div>Loading posts...</div>;
|
|
136
162
|
|
|
137
163
|
return (
|
|
138
|
-
<
|
|
139
|
-
|
|
140
|
-
|
|
164
|
+
<div>
|
|
165
|
+
<h1>Blog Posts</h1>
|
|
166
|
+
{allPosts?.map(post => (
|
|
167
|
+
<article key={post.id}>
|
|
168
|
+
<h2>{post.title}</h2>
|
|
169
|
+
<p>{post.content}</p>
|
|
170
|
+
{/* Expanded author is fully typed! */}
|
|
171
|
+
<small>By {post.expand?.author?.username}</small>
|
|
172
|
+
</article>
|
|
141
173
|
))}
|
|
142
|
-
</
|
|
174
|
+
</div>
|
|
143
175
|
);
|
|
144
176
|
}
|
|
145
177
|
```
|
|
146
178
|
|
|
179
|
+
### 4. Add Real-time Comments
|
|
180
|
+
|
|
181
|
+
```typescript
|
|
182
|
+
// PostWithComments.tsx
|
|
183
|
+
import { useLiveQuery } from '@tanstack/react-db';
|
|
184
|
+
import { eq } from '@tanstack/db';
|
|
185
|
+
import { useStore } from './app';
|
|
186
|
+
import { newRecordId } from 'pbtsdb';
|
|
187
|
+
|
|
188
|
+
export function PostWithComments({ postId }: { postId: string }) {
|
|
189
|
+
const [comments, posts] = useStore('comments', 'posts');
|
|
190
|
+
|
|
191
|
+
// Real-time comments for this post
|
|
192
|
+
const { data: postComments } = useLiveQuery((q) =>
|
|
193
|
+
q.from({ comments })
|
|
194
|
+
.where(({ comments }) => eq(comments.post, postId))
|
|
195
|
+
.orderBy(({ comments }) => comments.created, 'desc')
|
|
196
|
+
);
|
|
197
|
+
|
|
198
|
+
const handleAddComment = (text: string, authorId: string) => {
|
|
199
|
+
comments.insert({
|
|
200
|
+
id: newRecordId(),
|
|
201
|
+
post: postId,
|
|
202
|
+
author: authorId,
|
|
203
|
+
text
|
|
204
|
+
});
|
|
205
|
+
// Comment appears instantly (optimistic), syncs to PocketBase in background
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
return (
|
|
209
|
+
<div>
|
|
210
|
+
<h3>Comments ({postComments?.length || 0})</h3>
|
|
211
|
+
{postComments?.map(comment => (
|
|
212
|
+
<div key={comment.id}>
|
|
213
|
+
<strong>{comment.expand?.author?.username}:</strong>
|
|
214
|
+
<p>{comment.text}</p>
|
|
215
|
+
</div>
|
|
216
|
+
))}
|
|
217
|
+
<CommentForm onSubmit={handleAddComment} />
|
|
218
|
+
</div>
|
|
219
|
+
);
|
|
220
|
+
}
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
**That's it!** You now have a real-time blog with:
|
|
224
|
+
- ✅ Type-safe queries
|
|
225
|
+
- ✅ Automatic real-time updates
|
|
226
|
+
- ✅ Optimistic mutations
|
|
227
|
+
- ✅ Expanded relations
|
|
228
|
+
|
|
147
229
|
## Core Concepts
|
|
148
230
|
|
|
149
231
|
### Collections
|
|
@@ -151,8 +233,9 @@ function BooksList() {
|
|
|
151
233
|
Collections are reactive data stores that automatically sync with PocketBase:
|
|
152
234
|
|
|
153
235
|
```typescript
|
|
154
|
-
// Create a collection
|
|
155
|
-
const
|
|
236
|
+
// Create a collection using the curried API
|
|
237
|
+
const c = createCollection<MySchema>(pb, queryClient);
|
|
238
|
+
const booksCollection = c('books', {});
|
|
156
239
|
|
|
157
240
|
// Collections automatically:
|
|
158
241
|
// - Fetch data from PocketBase
|
|
@@ -167,22 +250,23 @@ Collections manage subscriptions **automatically** based on query lifecycle:
|
|
|
167
250
|
|
|
168
251
|
```typescript
|
|
169
252
|
// Collections are lazy - no subscription until queried
|
|
170
|
-
const
|
|
253
|
+
const c = createCollection<MySchema>(pb, queryClient);
|
|
254
|
+
const booksCollection = c('books', {});
|
|
171
255
|
|
|
172
256
|
// Subscription starts automatically when query becomes active
|
|
173
257
|
const { data } = useLiveQuery((q) =>
|
|
174
258
|
q.from({ books: booksCollection })
|
|
175
259
|
);
|
|
176
260
|
// ✅ Subscribed to changes while component is mounted
|
|
177
|
-
// ✅ Unsubscribes automatically when component unmounts
|
|
178
|
-
|
|
179
|
-
// Advanced: Manual subscription control
|
|
180
|
-
await booksCollection.subscribe(); // Subscribe to all
|
|
181
|
-
await booksCollection.subscribe('record_id'); // Subscribe to specific record
|
|
182
|
-
booksCollection.unsubscribe('record_id'); // Unsubscribe from specific record
|
|
183
|
-
booksCollection.unsubscribeAll(); // Clear all subscriptions
|
|
261
|
+
// ✅ Unsubscribes automatically when component unmounts
|
|
184
262
|
```
|
|
185
263
|
|
|
264
|
+
**Subscription Lifecycle:**
|
|
265
|
+
- **Lazy:** No subscription starts until the first `useLiveQuery` using the collection renders
|
|
266
|
+
- **Automatic:** Subscription starts when first subscriber mounts, stops when last subscriber unmounts
|
|
267
|
+
- **Shared:** Multiple components using the same collection share one subscription
|
|
268
|
+
- **No manual control needed:** The collection handles all subscription management internally
|
|
269
|
+
|
|
186
270
|
### Type Safety
|
|
187
271
|
|
|
188
272
|
Full TypeScript support with compile-time type checking:
|
|
@@ -200,138 +284,137 @@ const { data } = useLiveQuery((q) =>
|
|
|
200
284
|
|
|
201
285
|
## API Reference
|
|
202
286
|
|
|
203
|
-
###
|
|
287
|
+
### createCollection()
|
|
204
288
|
|
|
205
|
-
The main
|
|
206
|
-
|
|
207
|
-
#### Constructor
|
|
289
|
+
The main function for creating type-safe collections. Uses a curried API for better type inference.
|
|
208
290
|
|
|
209
291
|
```typescript
|
|
210
|
-
|
|
292
|
+
const c = createCollection<Schema>(pb: PocketBase, queryClient: QueryClient);
|
|
293
|
+
const collection = c(collectionName: string, options?: CreateCollectionOptions);
|
|
211
294
|
```
|
|
212
295
|
|
|
213
296
|
**Parameters:**
|
|
214
|
-
- `
|
|
215
|
-
- `queryClient
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
```typescript
|
|
219
|
-
const factory = new CollectionFactory<MySchema>(pb, queryClient);
|
|
220
|
-
```
|
|
221
|
-
|
|
222
|
-
#### create()
|
|
223
|
-
|
|
224
|
-
Creates a reactive collection from a PocketBase collection.
|
|
225
|
-
|
|
226
|
-
```typescript
|
|
227
|
-
create<CollectionName>(
|
|
228
|
-
collection: CollectionName,
|
|
229
|
-
options?: CreateCollectionOptions
|
|
230
|
-
): Collection & SubscribableCollection
|
|
231
|
-
```
|
|
297
|
+
- `pb` - PocketBase instance
|
|
298
|
+
- `queryClient` - TanStack Query QueryClient instance
|
|
299
|
+
- `collectionName` - Name of the PocketBase collection
|
|
300
|
+
- `options` - Optional configuration
|
|
232
301
|
|
|
233
302
|
**Options:**
|
|
234
|
-
- `expand?: string
|
|
235
|
-
- `
|
|
303
|
+
- `expand?: Record<string, Collection>` - Relations to auto-expand and auto-upsert on every fetch
|
|
304
|
+
- `omitOnInsert?: readonly string[]` - Fields to make optional during insert (e.g., `['created', 'updated'] as const`)
|
|
305
|
+
- `syncMode?: 'eager' | 'on-demand'` - Data fetching strategy (default: `'eager'`)
|
|
306
|
+
- `onInsert?: InsertMutationFn | false` - Custom insert handler or `false` to disable
|
|
307
|
+
- `onUpdate?: UpdateMutationFn | false` - Custom update handler or `false` to disable
|
|
308
|
+
- `onDelete?: DeleteMutationFn | false` - Custom delete handler or `false` to disable
|
|
309
|
+
|
|
310
|
+
**Returns:** Fully-typed Collection instance with subscription capabilities
|
|
236
311
|
|
|
237
312
|
**Examples:**
|
|
238
313
|
|
|
239
314
|
Basic collection (lazy, subscribes automatically on first query):
|
|
240
315
|
```typescript
|
|
241
|
-
const
|
|
316
|
+
const c = createCollection<MySchema>(pb, queryClient);
|
|
317
|
+
const booksCollection = c('books', {});
|
|
242
318
|
```
|
|
243
319
|
|
|
244
|
-
With expand:
|
|
320
|
+
With auto-expand relations:
|
|
245
321
|
```typescript
|
|
246
|
-
const
|
|
247
|
-
|
|
322
|
+
const c = createCollection<MySchema>(pb, queryClient);
|
|
323
|
+
const authorsCollection = c('authors', {});
|
|
324
|
+
const booksCollection = c('books', {
|
|
325
|
+
expand: {
|
|
326
|
+
author: authorsCollection // Auto-expand and auto-upsert
|
|
327
|
+
}
|
|
248
328
|
});
|
|
249
329
|
|
|
250
|
-
//
|
|
330
|
+
// Expand is automatic on every fetch
|
|
251
331
|
const { data } = useLiveQuery((q) => q.from({ books: booksCollection }));
|
|
252
|
-
data[0].expand?.author // ✅ Typed!
|
|
253
|
-
```
|
|
254
332
|
|
|
255
|
-
|
|
256
|
-
```typescript
|
|
257
|
-
const authorsCollection = factory.create('authors');
|
|
258
|
-
const booksCollection = factory.create('books', {
|
|
259
|
-
relations: {
|
|
260
|
-
author: authorsCollection
|
|
261
|
-
}
|
|
262
|
-
});
|
|
333
|
+
// Expanded records auto-inserted into authorsCollection
|
|
263
334
|
```
|
|
264
335
|
|
|
265
|
-
### React
|
|
336
|
+
### React Integration
|
|
266
337
|
|
|
267
|
-
|
|
338
|
+
#### createReactProvider()
|
|
268
339
|
|
|
269
|
-
|
|
340
|
+
Creates a React Provider and useStore hook from a collections map.
|
|
270
341
|
|
|
271
342
|
```typescript
|
|
272
|
-
|
|
273
|
-
{children}
|
|
274
|
-
</CollectionsProvider>
|
|
343
|
+
const { Provider, useStore } = createReactProvider(collections: CollectionsMap);
|
|
275
344
|
```
|
|
276
345
|
|
|
346
|
+
**Parameters:**
|
|
347
|
+
- `collections` - Object mapping keys to Collection instances
|
|
348
|
+
|
|
349
|
+
**Returns:**
|
|
350
|
+
- `Provider` - React Context Provider component
|
|
351
|
+
- `useStore` - Hook to access collections (variadic args, returns typed tuple)
|
|
352
|
+
|
|
277
353
|
**Example:**
|
|
278
354
|
```typescript
|
|
279
|
-
import {
|
|
355
|
+
import { createCollection, createReactProvider } from 'pbtsdb';
|
|
280
356
|
|
|
357
|
+
const c = createCollection<MySchema>(pb, queryClient);
|
|
281
358
|
const collections = {
|
|
282
|
-
authors:
|
|
283
|
-
books:
|
|
359
|
+
authors: c('authors', {}),
|
|
360
|
+
books: c('books', {
|
|
361
|
+
omitOnInsert: ['created', 'updated'] as const
|
|
362
|
+
}),
|
|
284
363
|
};
|
|
285
364
|
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
365
|
+
const { Provider, useStore } = createReactProvider(collections);
|
|
366
|
+
|
|
367
|
+
// Wrap your app
|
|
368
|
+
<Provider>
|
|
369
|
+
<App />
|
|
370
|
+
</Provider>
|
|
371
|
+
```
|
|
372
|
+
|
|
373
|
+
**With custom collection key:**
|
|
374
|
+
```typescript
|
|
375
|
+
const collections = {
|
|
376
|
+
myBooks: c('books', {}) // Key 'myBooks', PocketBase collection 'books'
|
|
377
|
+
};
|
|
378
|
+
|
|
379
|
+
const { Provider, useStore } = createReactProvider(collections);
|
|
380
|
+
|
|
381
|
+
// Access via custom key
|
|
382
|
+
const [myBooks] = useStore('myBooks');
|
|
293
383
|
```
|
|
294
384
|
|
|
295
385
|
#### useStore()
|
|
296
386
|
|
|
297
|
-
Access
|
|
387
|
+
Access collections from the provider. Uses variadic arguments and returns a typed tuple.
|
|
298
388
|
|
|
389
|
+
**Single collection:**
|
|
299
390
|
```typescript
|
|
300
|
-
const collection = useStore
|
|
391
|
+
const [collection] = useStore('key')
|
|
301
392
|
```
|
|
302
393
|
|
|
303
|
-
**
|
|
394
|
+
**Multiple collections:**
|
|
395
|
+
```typescript
|
|
396
|
+
const [col1, col2, col3] = useStore('key1', 'key2', 'key3')
|
|
397
|
+
```
|
|
398
|
+
|
|
399
|
+
**Examples:**
|
|
304
400
|
```typescript
|
|
305
401
|
function BooksList() {
|
|
306
|
-
const
|
|
402
|
+
const [books] = useStore('books'); // ✅ Typed automatically!
|
|
307
403
|
|
|
308
404
|
const { data } = useLiveQuery((q) =>
|
|
309
|
-
q.from({ books
|
|
405
|
+
q.from({ books })
|
|
310
406
|
);
|
|
311
407
|
|
|
312
408
|
return <div>{/* ... */}</div>;
|
|
313
409
|
}
|
|
314
|
-
```
|
|
315
|
-
|
|
316
|
-
#### useStores()
|
|
317
410
|
|
|
318
|
-
Access multiple collections at once.
|
|
319
|
-
|
|
320
|
-
```typescript
|
|
321
|
-
const [col1, col2] = useStores<[Type1, Type2]>(keys: string[])
|
|
322
|
-
```
|
|
323
|
-
|
|
324
|
-
**Example:**
|
|
325
|
-
```typescript
|
|
326
411
|
function BooksWithAuthors() {
|
|
327
|
-
const [
|
|
328
|
-
['books', 'authors']
|
|
329
|
-
);
|
|
412
|
+
const [books, authors] = useStore('books', 'authors'); // ✅ Variadic!
|
|
330
413
|
|
|
331
414
|
const { data } = useLiveQuery((q) =>
|
|
332
|
-
q.from({ book:
|
|
415
|
+
q.from({ book: books })
|
|
333
416
|
.join(
|
|
334
|
-
{ author:
|
|
417
|
+
{ author: authors },
|
|
335
418
|
({ book, author }) => eq(book.author, author.id),
|
|
336
419
|
'left'
|
|
337
420
|
)
|
|
@@ -343,50 +426,26 @@ function BooksWithAuthors() {
|
|
|
343
426
|
|
|
344
427
|
### Subscriptions
|
|
345
428
|
|
|
346
|
-
Collections
|
|
347
|
-
|
|
348
|
-
#### subscribe()
|
|
349
|
-
|
|
350
|
-
Subscribe to collection changes.
|
|
351
|
-
|
|
352
|
-
```typescript
|
|
353
|
-
// Subscribe to all records
|
|
354
|
-
await collection.subscribe();
|
|
355
|
-
|
|
356
|
-
// Subscribe to specific record
|
|
357
|
-
await collection.subscribe('record_id');
|
|
358
|
-
```
|
|
359
|
-
|
|
360
|
-
#### unsubscribe()
|
|
361
|
-
|
|
362
|
-
Unsubscribe from changes.
|
|
363
|
-
|
|
364
|
-
```typescript
|
|
365
|
-
// Unsubscribe from all
|
|
366
|
-
collection.unsubscribe();
|
|
367
|
-
|
|
368
|
-
// Unsubscribe from specific record
|
|
369
|
-
collection.unsubscribe('record_id');
|
|
370
|
-
```
|
|
429
|
+
Collections manage real-time subscriptions to PocketBase **automatically**. No manual subscription management is needed for normal usage.
|
|
371
430
|
|
|
372
|
-
####
|
|
373
|
-
|
|
374
|
-
Clear all subscriptions for a collection.
|
|
431
|
+
#### Automatic Subscription Lifecycle
|
|
375
432
|
|
|
376
433
|
```typescript
|
|
377
|
-
|
|
434
|
+
// Subscriptions start automatically when useLiveQuery renders
|
|
435
|
+
function MyComponent() {
|
|
436
|
+
const [books] = useStore('books');
|
|
437
|
+
const { data } = useLiveQuery((q) => q.from({ books }));
|
|
438
|
+
// ✅ Subscription active while this component is mounted
|
|
439
|
+
// ✅ Automatically stops when component unmounts
|
|
440
|
+
}
|
|
378
441
|
```
|
|
379
442
|
|
|
380
443
|
#### isSubscribed()
|
|
381
444
|
|
|
382
|
-
Check subscription
|
|
445
|
+
Check if a collection has an active subscription.
|
|
383
446
|
|
|
384
447
|
```typescript
|
|
385
|
-
// Check collection-wide subscription
|
|
386
448
|
const isSubbed = collection.isSubscribed(); // boolean
|
|
387
|
-
|
|
388
|
-
// Check specific record subscription
|
|
389
|
-
const isRecordSubbed = collection.isSubscribed('record_id'); // boolean
|
|
390
449
|
```
|
|
391
450
|
|
|
392
451
|
#### waitForSubscription()
|
|
@@ -394,519 +453,432 @@ const isRecordSubbed = collection.isSubscribed('record_id'); // boolean
|
|
|
394
453
|
Wait for subscription to be established (useful in tests).
|
|
395
454
|
|
|
396
455
|
```typescript
|
|
397
|
-
await collection.waitForSubscription(); // Wait
|
|
398
|
-
await collection.waitForSubscription(
|
|
399
|
-
await collection.waitForSubscription(undefined, 5000); // With timeout
|
|
456
|
+
await collection.waitForSubscription(); // Wait with default 5s timeout
|
|
457
|
+
await collection.waitForSubscription(10000); // Wait with custom timeout (ms)
|
|
400
458
|
```
|
|
401
459
|
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
### Basic Queries
|
|
405
|
-
|
|
406
|
-
#### Fetch All Records
|
|
407
|
-
|
|
408
|
-
```typescript
|
|
409
|
-
const { data: books, isLoading, error } = useLiveQuery((q) =>
|
|
410
|
-
q.from({ books: booksCollection })
|
|
411
|
-
);
|
|
412
|
-
|
|
413
|
-
if (isLoading) return <div>Loading...</div>;
|
|
414
|
-
if (error) return <div>Error: {error.message}</div>;
|
|
460
|
+
### Utility Functions
|
|
415
461
|
|
|
416
|
-
|
|
417
|
-
<ul>
|
|
418
|
-
{books?.map(book => (
|
|
419
|
-
<li key={book.id}>{book.title}</li>
|
|
420
|
-
))}
|
|
421
|
-
</ul>
|
|
422
|
-
);
|
|
423
|
-
```
|
|
462
|
+
#### newRecordId()
|
|
424
463
|
|
|
425
|
-
|
|
464
|
+
Generate a PocketBase-compatible record ID (15-character alphanumeric string).
|
|
426
465
|
|
|
427
466
|
```typescript
|
|
428
|
-
import {
|
|
467
|
+
import { newRecordId } from 'pbtsdb';
|
|
429
468
|
|
|
430
|
-
const
|
|
469
|
+
const id = newRecordId(); // "a1b2c3d4e5f6g7h"
|
|
431
470
|
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
471
|
+
// Use when creating records
|
|
472
|
+
const newBook = {
|
|
473
|
+
id: newRecordId(),
|
|
474
|
+
title: 'New Book',
|
|
475
|
+
// ... other fields
|
|
476
|
+
};
|
|
436
477
|
|
|
437
|
-
|
|
478
|
+
booksCollection.insert(newBook);
|
|
438
479
|
```
|
|
439
480
|
|
|
440
|
-
|
|
481
|
+
**Returns:** `string` - 15-character lowercase alphanumeric ID
|
|
482
|
+
|
|
483
|
+
## Usage Examples
|
|
441
484
|
|
|
442
|
-
|
|
485
|
+
### Example 1: Task Manager with Filtering
|
|
443
486
|
|
|
444
487
|
```typescript
|
|
445
|
-
|
|
488
|
+
// TaskBoard.tsx
|
|
489
|
+
import { useLiveQuery } from '@tanstack/react-db';
|
|
490
|
+
import { eq, and } from '@tanstack/db';
|
|
491
|
+
import { useStore } from './app';
|
|
446
492
|
|
|
447
|
-
|
|
448
|
-
const
|
|
449
|
-
q.from({ books: booksCollection })
|
|
450
|
-
.where(({ books }) => eq(books.genre, 'Fiction'))
|
|
451
|
-
);
|
|
493
|
+
export function TaskBoard({ userId }: { userId: string }) {
|
|
494
|
+
const [tasks] = useStore('tasks');
|
|
452
495
|
|
|
453
|
-
// Filter by
|
|
454
|
-
const { data } = useLiveQuery((q) =>
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
);
|
|
496
|
+
// Filter tasks by assignee and status - updates in real-time
|
|
497
|
+
const { data: myTasks } = useLiveQuery((q) =>
|
|
498
|
+
q.from({ tasks })
|
|
499
|
+
.where(({ tasks }) => and(eq(tasks.assignee, userId), eq(tasks.status, 'in_progress')))
|
|
500
|
+
.orderBy(({ tasks }) => tasks.due_date, 'asc')
|
|
501
|
+
);
|
|
460
502
|
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
.where(({ books }) => and(
|
|
465
|
-
eq(books.genre, 'Fiction'),
|
|
466
|
-
gt(books.published_date, '2020-01-01')
|
|
467
|
-
))
|
|
468
|
-
);
|
|
503
|
+
const handleComplete = (taskId: string) => {
|
|
504
|
+
tasks.update(taskId, (draft) => { draft.status = 'done'; });
|
|
505
|
+
};
|
|
469
506
|
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
507
|
+
return (
|
|
508
|
+
<div>
|
|
509
|
+
<h2>My Tasks ({myTasks?.length || 0})</h2>
|
|
510
|
+
{myTasks?.map(task => (
|
|
511
|
+
<div key={task.id}>
|
|
512
|
+
{task.title}
|
|
513
|
+
<button onClick={() => handleComplete(task.id)}>Complete</button>
|
|
514
|
+
</div>
|
|
515
|
+
))}
|
|
516
|
+
</div>
|
|
517
|
+
);
|
|
518
|
+
}
|
|
478
519
|
```
|
|
479
520
|
|
|
480
|
-
|
|
521
|
+
### Example 2: E-commerce Product Catalog with Filtering
|
|
481
522
|
|
|
482
523
|
```typescript
|
|
483
|
-
|
|
524
|
+
// ProductCatalog.tsx
|
|
525
|
+
import { useLiveQuery } from '@tanstack/react-db';
|
|
526
|
+
import { and, gte, lte } from '@tanstack/db';
|
|
527
|
+
import { useStore } from './app';
|
|
528
|
+
|
|
529
|
+
export function ProductCatalog() {
|
|
530
|
+
const [products] = useStore('products');
|
|
531
|
+
const [category, setCategory] = useState<string | null>(null);
|
|
532
|
+
const [maxPrice, setMaxPrice] = useState(1000);
|
|
533
|
+
|
|
534
|
+
// Dynamic filtering - updates reactively
|
|
535
|
+
const { data: filteredProducts } = useLiveQuery((q) => {
|
|
536
|
+
let query = q.from({ products })
|
|
537
|
+
.where(({ products }) => and(
|
|
538
|
+
products.in_stock === true,
|
|
539
|
+
lte(products.price, maxPrice)
|
|
540
|
+
));
|
|
541
|
+
|
|
542
|
+
if (category) {
|
|
543
|
+
query = query.where(({ products }) => products.category === category);
|
|
544
|
+
}
|
|
484
545
|
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
)
|
|
546
|
+
return query.orderBy(({ products }) => products.rating, 'desc');
|
|
547
|
+
});
|
|
548
|
+
|
|
549
|
+
return (
|
|
550
|
+
<div>
|
|
551
|
+
<select onChange={(e) => setCategory(e.target.value || null)}>
|
|
552
|
+
<option value="">All Categories</option>
|
|
553
|
+
<option value="electronics">Electronics</option>
|
|
554
|
+
</select>
|
|
555
|
+
<input type="range" max="1000" value={maxPrice}
|
|
556
|
+
onChange={(e) => setMaxPrice(+e.target.value)} />
|
|
557
|
+
|
|
558
|
+
{filteredProducts?.map(product => (
|
|
559
|
+
<ProductCard key={product.id} product={product} />
|
|
560
|
+
))}
|
|
561
|
+
</div>
|
|
562
|
+
);
|
|
563
|
+
}
|
|
496
564
|
```
|
|
497
565
|
|
|
498
|
-
|
|
566
|
+
### Example 3: Social Media Feed with Likes
|
|
499
567
|
|
|
500
568
|
```typescript
|
|
501
|
-
//
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
// Sort ascending
|
|
508
|
-
const { data } = useLiveQuery((q) =>
|
|
509
|
-
q.from({ books: booksCollection })
|
|
510
|
-
.orderBy(({ books }) => books.title, 'asc')
|
|
511
|
-
);
|
|
512
|
-
|
|
513
|
-
// Multiple sorts
|
|
514
|
-
const { data } = useLiveQuery((q) =>
|
|
515
|
-
q.from({ books: booksCollection })
|
|
516
|
-
.orderBy(({ books }) => books.genre, 'asc')
|
|
517
|
-
.orderBy(({ books }) => books.published_date, 'desc')
|
|
518
|
-
);
|
|
519
|
-
```
|
|
569
|
+
// SocialFeed.tsx
|
|
570
|
+
import { useLiveQuery } from '@tanstack/react-db';
|
|
571
|
+
import { eq } from '@tanstack/db';
|
|
572
|
+
import { useStore } from './app';
|
|
573
|
+
import { newRecordId } from 'pbtsdb';
|
|
520
574
|
|
|
521
|
-
|
|
575
|
+
export function SocialFeed({ currentUserId }: { currentUserId: string }) {
|
|
576
|
+
const [posts, likes] = useStore('posts', 'likes');
|
|
522
577
|
|
|
523
|
-
|
|
578
|
+
const { data: feedPosts } = useLiveQuery((q) =>
|
|
579
|
+
q.from({ posts }).orderBy(({ posts }) => posts.created, 'desc')
|
|
580
|
+
);
|
|
524
581
|
|
|
525
|
-
|
|
582
|
+
const { data: userLikes } = useLiveQuery((q) =>
|
|
583
|
+
q.from({ likes }).where(({ likes }) => eq(likes.user, currentUserId))
|
|
584
|
+
);
|
|
526
585
|
|
|
527
|
-
|
|
528
|
-
// Create collection with expand
|
|
529
|
-
const booksCollection = factory.create('books', {
|
|
530
|
-
expand: 'author' as const // ← Type-safe!
|
|
531
|
-
});
|
|
586
|
+
const likedPostIds = new Set(userLikes?.map(l => l.post) || []);
|
|
532
587
|
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
);
|
|
588
|
+
const handleLike = (postId: string) => {
|
|
589
|
+
if (likedPostIds.has(postId)) {
|
|
590
|
+
const like = userLikes?.find(l => l.post === postId);
|
|
591
|
+
if (like) likes.delete(like.id);
|
|
592
|
+
} else {
|
|
593
|
+
likes.insert({ id: newRecordId(), post: postId, user: currentUserId });
|
|
594
|
+
}
|
|
595
|
+
};
|
|
537
596
|
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
}
|
|
597
|
+
return (
|
|
598
|
+
<div>
|
|
599
|
+
{feedPosts?.map(post => (
|
|
600
|
+
<div key={post.id}>
|
|
601
|
+
<strong>{post.expand?.author?.username}</strong>
|
|
602
|
+
<p>{post.content}</p>
|
|
603
|
+
<button onClick={() => handleLike(post.id)}>
|
|
604
|
+
{likedPostIds.has(post.id) ? '❤️' : '🤍'} {post.likes_count}
|
|
605
|
+
</button>
|
|
606
|
+
</div>
|
|
607
|
+
))}
|
|
608
|
+
</div>
|
|
609
|
+
);
|
|
610
|
+
}
|
|
544
611
|
```
|
|
545
612
|
|
|
546
|
-
|
|
613
|
+
### Example 4: Real-time Collaborative Todo List
|
|
547
614
|
|
|
548
615
|
```typescript
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
}
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
616
|
+
// CollaborativeTodoList.tsx
|
|
617
|
+
import { useLiveQuery } from '@tanstack/react-db';
|
|
618
|
+
import { eq } from '@tanstack/db';
|
|
619
|
+
import { useStore } from './app';
|
|
620
|
+
import { newRecordId } from 'pbtsdb';
|
|
621
|
+
|
|
622
|
+
export function CollaborativeTodoList({ listId, userId }: { listId: string; userId: string }) {
|
|
623
|
+
const [todos] = useStore('todos');
|
|
624
|
+
const [newText, setNewText] = useState('');
|
|
625
|
+
|
|
626
|
+
// Real-time todos - updates when any user adds/edits
|
|
627
|
+
const { data: allTodos } = useLiveQuery((q) =>
|
|
628
|
+
q.from({ todos })
|
|
629
|
+
.where(({ todos }) => eq(todos.list_id, listId))
|
|
630
|
+
.orderBy(({ todos }) => todos.created, 'asc')
|
|
631
|
+
);
|
|
561
632
|
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
}
|
|
568
|
-
});
|
|
633
|
+
const handleAdd = () => {
|
|
634
|
+
if (!newText.trim()) return;
|
|
635
|
+
todos.insert({ id: newRecordId(), text: newText, completed: false, list_id: listId, created_by: userId });
|
|
636
|
+
setNewText('');
|
|
637
|
+
};
|
|
569
638
|
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
639
|
+
return (
|
|
640
|
+
<div>
|
|
641
|
+
<input value={newText} onChange={(e) => setNewText(e.target.value)}
|
|
642
|
+
onKeyPress={(e) => e.key === 'Enter' && handleAdd()} />
|
|
643
|
+
<ul>
|
|
644
|
+
{allTodos?.map(todo => (
|
|
645
|
+
<li key={todo.id}>
|
|
646
|
+
<input type="checkbox" checked={todo.completed}
|
|
647
|
+
onChange={() => todos.update(todo.id, d => { d.completed = !d.completed; })} />
|
|
648
|
+
{todo.text}
|
|
649
|
+
<button onClick={() => todos.delete(todo.id)}>×</button>
|
|
650
|
+
</li>
|
|
651
|
+
))}
|
|
652
|
+
</ul>
|
|
653
|
+
</div>
|
|
654
|
+
);
|
|
655
|
+
}
|
|
585
656
|
```
|
|
586
657
|
|
|
587
|
-
|
|
658
|
+
Real-time collaboration works automatically - when User A adds/edits a todo, User B sees it instantly.
|
|
588
659
|
|
|
589
|
-
|
|
590
|
-
const { data } = useLiveQuery((q) =>
|
|
591
|
-
q.from({ book: booksCollection })
|
|
592
|
-
.join(
|
|
593
|
-
{ author: authorsCollection },
|
|
594
|
-
({ book, author }) => eq(book.author, author.id),
|
|
595
|
-
'inner' // Only books WITH authors
|
|
596
|
-
)
|
|
597
|
-
);
|
|
598
|
-
```
|
|
599
|
-
|
|
600
|
-
#### Complex Multi-Collection Joins
|
|
660
|
+
### Example 5: Form with Optimistic Updates and Error Handling
|
|
601
661
|
|
|
602
662
|
```typescript
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
663
|
+
// CreateBookForm.tsx
|
|
664
|
+
import { useStore } from './app';
|
|
665
|
+
import { newRecordId } from 'pbtsdb';
|
|
606
666
|
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
({ book, author }) => eq(book.author, author.id),
|
|
612
|
-
'left'
|
|
613
|
-
)
|
|
614
|
-
.join(
|
|
615
|
-
{ metadata: metadataCollection },
|
|
616
|
-
({ book, metadata }) => eq(book.id, metadata.book),
|
|
617
|
-
'left'
|
|
618
|
-
)
|
|
619
|
-
.select(({ book, author, metadata }) => ({
|
|
620
|
-
...book,
|
|
621
|
-
expand: {
|
|
622
|
-
author: author ? { ...author } : undefined,
|
|
623
|
-
metadata: metadata ? { ...metadata } : undefined
|
|
624
|
-
}
|
|
625
|
-
}))
|
|
626
|
-
);
|
|
627
|
-
```
|
|
667
|
+
export function CreateBookForm() {
|
|
668
|
+
const [books] = useStore('books');
|
|
669
|
+
const [title, setTitle] = useState('');
|
|
670
|
+
const [error, setError] = useState<string | null>(null);
|
|
628
671
|
|
|
629
|
-
|
|
672
|
+
const handleSubmit = async (e: React.FormEvent) => {
|
|
673
|
+
e.preventDefault();
|
|
674
|
+
setError(null);
|
|
630
675
|
|
|
631
|
-
|
|
676
|
+
try {
|
|
677
|
+
// Optimistic insert - appears instantly
|
|
678
|
+
const tx = books.insert({ id: newRecordId(), title, author: 'author_id' });
|
|
679
|
+
await tx.isPersisted.promise;
|
|
632
680
|
|
|
633
|
-
|
|
681
|
+
if (tx.state === 'completed') setTitle('');
|
|
682
|
+
else setError('Failed to create book');
|
|
683
|
+
} catch (err: any) {
|
|
684
|
+
setError(err.data ? Object.values(err.data).join(', ') : err.message);
|
|
685
|
+
}
|
|
686
|
+
};
|
|
634
687
|
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
688
|
+
return (
|
|
689
|
+
<form onSubmit={handleSubmit}>
|
|
690
|
+
{error && <div className="error">{error}</div>}
|
|
691
|
+
<input value={title} onChange={(e) => setTitle(e.target.value)} required />
|
|
692
|
+
<button type="submit">Add Book</button>
|
|
693
|
+
</form>
|
|
639
694
|
);
|
|
640
|
-
|
|
641
|
-
// Subscription automatically starts when component mounts
|
|
642
|
-
// Component re-renders automatically when:
|
|
643
|
-
// - A book is created
|
|
644
|
-
// - A book is updated
|
|
645
|
-
// - A book is deleted
|
|
646
|
-
// Subscription automatically stops when component unmounts (with 5s delay)
|
|
647
|
-
|
|
648
|
-
return <ul>{/* ... */}</ul>;
|
|
649
695
|
}
|
|
650
696
|
```
|
|
651
697
|
|
|
652
|
-
|
|
653
|
-
- ✅ Collections are lazy - no network activity on creation
|
|
654
|
-
- ✅ Subscription starts when first `useLiveQuery` becomes active
|
|
655
|
-
- ✅ Multiple queries share a single subscription per collection
|
|
656
|
-
- ✅ Subscription stops 5 seconds after last query unmounts
|
|
657
|
-
- ✅ Prevents thrashing during rapid mount/unmount cycles
|
|
658
|
-
|
|
659
|
-
#### Manual Subscription Control (Advanced)
|
|
698
|
+
Optimistic updates show changes instantly; automatic rollback on server errors.
|
|
660
699
|
|
|
661
|
-
|
|
700
|
+
### Example 6: Dashboard with Multiple Collections and Joins
|
|
662
701
|
|
|
663
702
|
```typescript
|
|
664
|
-
|
|
703
|
+
// ProjectDashboard.tsx
|
|
704
|
+
import { useLiveQuery } from '@tanstack/react-db';
|
|
705
|
+
import { eq } from '@tanstack/db';
|
|
706
|
+
import { useStore } from './app';
|
|
665
707
|
|
|
666
|
-
|
|
667
|
-
|
|
708
|
+
export function ProjectDashboard({ projectId }: { projectId: string }) {
|
|
709
|
+
const [projects, tasks, teamMembers, users] = useStore('projects', 'tasks', 'team_members', 'users');
|
|
668
710
|
|
|
669
|
-
|
|
670
|
-
|
|
711
|
+
const { data: projectList } = useLiveQuery((q) =>
|
|
712
|
+
q.from({ projects }).where(({ projects }) => eq(projects.id, projectId))
|
|
713
|
+
);
|
|
671
714
|
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
```
|
|
715
|
+
const { data: projectTasks } = useLiveQuery((q) =>
|
|
716
|
+
q.from({ tasks }).where(({ tasks }) => eq(tasks.project, projectId))
|
|
717
|
+
);
|
|
676
718
|
|
|
677
|
-
|
|
719
|
+
// Join team members with users
|
|
720
|
+
const { data: team } = useLiveQuery((q) =>
|
|
721
|
+
q.from({ member: teamMembers })
|
|
722
|
+
.where(({ member }) => eq(member.project, projectId))
|
|
723
|
+
.join({ user: users }, ({ member, user }) => eq(member.user, user.id), 'left')
|
|
724
|
+
.select(({ member, user }) => ({ id: member.id, role: member.role, name: user?.name }))
|
|
725
|
+
);
|
|
678
726
|
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
await booksCollection.subscribe('book_id_123');
|
|
727
|
+
const completed = projectTasks?.filter(t => t.completed).length || 0;
|
|
728
|
+
const total = projectTasks?.length || 0;
|
|
682
729
|
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
730
|
+
return (
|
|
731
|
+
<div>
|
|
732
|
+
<h1>{projectList?.[0]?.name}</h1>
|
|
733
|
+
<p>Progress: {completed}/{total} tasks</p>
|
|
734
|
+
<p>Team: {team?.map(m => m.name).join(', ')}</p>
|
|
735
|
+
</div>
|
|
736
|
+
);
|
|
686
737
|
}
|
|
687
|
-
|
|
688
|
-
// Unsubscribe from specific record
|
|
689
|
-
booksCollection.unsubscribe('book_id_123');
|
|
690
738
|
```
|
|
691
739
|
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
```typescript
|
|
695
|
-
// Useful in tests
|
|
696
|
-
await booksCollection.waitForSubscription();
|
|
697
|
-
|
|
698
|
-
// Now safe to create/update records and expect real-time updates
|
|
699
|
-
const newBook = await pb.collection('books').create({ /* ... */ });
|
|
700
|
-
|
|
701
|
-
// Wait for update to propagate
|
|
702
|
-
await waitFor(() => {
|
|
703
|
-
expect(data?.some(b => b.id === newBook.id)).toBe(true);
|
|
704
|
-
});
|
|
705
|
-
```
|
|
740
|
+
Demonstrates variadic `useStore()`, client-side aggregations, and TanStack DB joins.
|
|
706
741
|
|
|
707
742
|
## TypeScript
|
|
708
743
|
|
|
709
|
-
|
|
744
|
+
pbtsdb is fully type-safe. Here's what you need to know:
|
|
710
745
|
|
|
711
|
-
Define
|
|
746
|
+
### Define Your Schema
|
|
712
747
|
|
|
713
|
-
|
|
714
|
-
import type { SchemaDeclaration } from 'pocketbase-tanstack-db';
|
|
748
|
+
Use the simple schema format shown in the Quick Start:
|
|
715
749
|
|
|
716
|
-
|
|
750
|
+
```typescript
|
|
751
|
+
type MySchema = {
|
|
717
752
|
collection_name: {
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
// Forward relations (FK fields)
|
|
722
|
-
field_name: [
|
|
723
|
-
'target_collection',
|
|
724
|
-
is_array // false for single, true for array
|
|
725
|
-
];
|
|
726
|
-
};
|
|
727
|
-
back: {
|
|
728
|
-
// Back relations (reverse lookups)
|
|
729
|
-
relation_name: ['source_collection', is_array];
|
|
730
|
-
};
|
|
753
|
+
type: RecordInterface; // Your record type
|
|
754
|
+
relations: {
|
|
755
|
+
field_name: RelatedType; // Related record types
|
|
731
756
|
};
|
|
732
757
|
};
|
|
733
758
|
}
|
|
734
759
|
```
|
|
735
760
|
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
```typescript
|
|
739
|
-
interface Post {
|
|
740
|
-
id: string;
|
|
741
|
-
title: string;
|
|
742
|
-
content: string;
|
|
743
|
-
author: string; // FK to users
|
|
744
|
-
tags: string[]; // FK array to tags
|
|
745
|
-
created: string;
|
|
746
|
-
updated: string;
|
|
747
|
-
}
|
|
748
|
-
|
|
749
|
-
interface User {
|
|
750
|
-
id: string;
|
|
751
|
-
username: string;
|
|
752
|
-
email: string;
|
|
753
|
-
created: string;
|
|
754
|
-
updated: string;
|
|
755
|
-
}
|
|
756
|
-
|
|
757
|
-
interface Tag {
|
|
758
|
-
id: string;
|
|
759
|
-
name: string;
|
|
760
|
-
created: string;
|
|
761
|
-
updated: string;
|
|
762
|
-
}
|
|
763
|
-
|
|
764
|
-
interface BlogSchema extends SchemaDeclaration {
|
|
765
|
-
posts: {
|
|
766
|
-
Row: Post;
|
|
767
|
-
Relations: {
|
|
768
|
-
forward: {
|
|
769
|
-
author: ['users', false]; // Single relation
|
|
770
|
-
tags: ['tags', true]; // Array relation
|
|
771
|
-
};
|
|
772
|
-
back: {};
|
|
773
|
-
};
|
|
774
|
-
};
|
|
775
|
-
users: {
|
|
776
|
-
Row: User;
|
|
777
|
-
Relations: {
|
|
778
|
-
forward: {};
|
|
779
|
-
back: {
|
|
780
|
-
posts: ['posts', true]; // One user, many posts
|
|
781
|
-
};
|
|
782
|
-
};
|
|
783
|
-
};
|
|
784
|
-
tags: {
|
|
785
|
-
Row: Tag;
|
|
786
|
-
Relations: {
|
|
787
|
-
forward: {};
|
|
788
|
-
back: {
|
|
789
|
-
posts: ['posts', true]; // One tag, many posts
|
|
790
|
-
};
|
|
791
|
-
};
|
|
792
|
-
};
|
|
793
|
-
}
|
|
794
|
-
```
|
|
761
|
+
**Pro tip:** Use [pocketbase-schema-generator](https://github.com/satohshi/pocketbase-schema-generator) to auto-generate types from your PocketBase database.
|
|
795
762
|
|
|
796
|
-
### Type-Safe
|
|
763
|
+
### Type-Safe Collections
|
|
797
764
|
|
|
798
|
-
|
|
765
|
+
Always create collections with proper type parameters:
|
|
799
766
|
|
|
800
767
|
```typescript
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
768
|
+
// ✅ Good - full type safety
|
|
769
|
+
const c = createCollection<MySchema>(pb, queryClient);
|
|
770
|
+
const books = c('books', {
|
|
771
|
+
omitOnInsert: ['created', 'updated'] as const
|
|
804
772
|
});
|
|
805
773
|
|
|
806
|
-
//
|
|
807
|
-
|
|
808
|
-
|
|
774
|
+
// ✅ Good - with auto-expand relations
|
|
775
|
+
const authors = c('authors', {});
|
|
776
|
+
const books = c('books', {
|
|
777
|
+
expand: {
|
|
778
|
+
author: authors
|
|
779
|
+
}
|
|
780
|
+
});
|
|
809
781
|
```
|
|
810
782
|
|
|
811
783
|
## Best Practices
|
|
812
784
|
|
|
813
|
-
### 1.
|
|
814
|
-
|
|
815
|
-
**Use Type-Safe Expand when:**
|
|
816
|
-
- You need fast, single-query performance
|
|
817
|
-
- Relations are straightforward
|
|
785
|
+
### 1. Define Collections Centrally
|
|
818
786
|
|
|
787
|
+
Define all collections once at app initialization:
|
|
819
788
|
|
|
820
789
|
```typescript
|
|
821
|
-
// ✅
|
|
822
|
-
const
|
|
823
|
-
|
|
790
|
+
// ✅ Do this - centralized, type-safe
|
|
791
|
+
const c = createCollection<MySchema>(pb, queryClient);
|
|
792
|
+
|
|
793
|
+
export const { Provider, useStore } = createReactProvider({
|
|
794
|
+
posts: c('posts', { omitOnInsert: ['created', 'updated'] as const }),
|
|
795
|
+
users: c('users', {}),
|
|
796
|
+
comments: c('comments', { omitOnInsert: ['created', 'updated'] as const })
|
|
824
797
|
});
|
|
825
798
|
```
|
|
826
799
|
|
|
827
|
-
|
|
828
|
-
- You need inner/right/full joins
|
|
829
|
-
- You want the related records to update in response to sync
|
|
830
|
-
- Complex client-side filtering after joins
|
|
831
|
-
- Building computed fields from multiple collections
|
|
800
|
+
### 2. Create Dependencies Before Dependents
|
|
832
801
|
|
|
802
|
+
When using expand collections, create the target collection first:
|
|
833
803
|
|
|
834
804
|
```typescript
|
|
835
|
-
// ✅
|
|
836
|
-
const
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
```typescript
|
|
845
|
-
// ✅ Define collections once
|
|
846
|
-
const collections = {
|
|
847
|
-
books: factory.create('books'),
|
|
848
|
-
authors: factory.create('authors'),
|
|
849
|
-
};
|
|
805
|
+
// ✅ Good - authors exists before books references it
|
|
806
|
+
const c = createCollection<MySchema>(pb, queryClient);
|
|
807
|
+
const authors = c('authors', {});
|
|
808
|
+
const books = c('books', {
|
|
809
|
+
expand: {
|
|
810
|
+
author: authors // authors is already created
|
|
811
|
+
}
|
|
812
|
+
});
|
|
850
813
|
|
|
851
|
-
//
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
814
|
+
// ❌ Bad - can't reference what doesn't exist yet
|
|
815
|
+
const books = c('books', {
|
|
816
|
+
expand: {
|
|
817
|
+
author: ??? // Where is authors?
|
|
818
|
+
}
|
|
819
|
+
});
|
|
855
820
|
```
|
|
856
821
|
|
|
857
|
-
### 3.
|
|
822
|
+
### 3. Subscriptions are Automatic
|
|
823
|
+
|
|
824
|
+
Don't manually subscribe - just use `useLiveQuery`:
|
|
858
825
|
|
|
859
826
|
```typescript
|
|
860
|
-
// ✅
|
|
861
|
-
const
|
|
827
|
+
// ✅ Do this
|
|
828
|
+
const { data } = useLiveQuery((q) => q.from({ posts }));
|
|
862
829
|
|
|
863
|
-
//
|
|
864
|
-
|
|
830
|
+
// ❌ Don't do this
|
|
831
|
+
useEffect(() => {
|
|
832
|
+
posts.subscribe();
|
|
833
|
+
return () => posts.unsubscribe();
|
|
834
|
+
}, []);
|
|
865
835
|
```
|
|
866
836
|
|
|
867
|
-
### 4. Handle Loading
|
|
837
|
+
### 4. Handle Loading States
|
|
838
|
+
|
|
839
|
+
Always check loading and error states:
|
|
868
840
|
|
|
869
841
|
```typescript
|
|
870
|
-
const { data, isLoading, error } = useLiveQuery((q) =>
|
|
871
|
-
q.from({ books: booksCollection })
|
|
872
|
-
);
|
|
842
|
+
const { data, isLoading, error } = useLiveQuery((q) => q.from({ posts }));
|
|
873
843
|
|
|
874
|
-
if (isLoading) return <
|
|
875
|
-
if (error) return <
|
|
876
|
-
if (!data?.length) return <
|
|
844
|
+
if (isLoading) return <div>Loading...</div>;
|
|
845
|
+
if (error) return <div>Error: {error.message}</div>;
|
|
846
|
+
if (!data?.length) return <div>No posts found</div>;
|
|
877
847
|
|
|
878
|
-
return <
|
|
848
|
+
return <PostsList posts={data} />;
|
|
879
849
|
```
|
|
880
850
|
|
|
881
|
-
### 5.
|
|
851
|
+
### 5. Use Expand for Performance
|
|
882
852
|
|
|
883
|
-
|
|
853
|
+
Use PocketBase's expand feature for better performance:
|
|
884
854
|
|
|
885
855
|
```typescript
|
|
886
|
-
//
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
856
|
+
// ✅ Fast - single query with server-side expand
|
|
857
|
+
const c = createCollection<MySchema>(pb, queryClient);
|
|
858
|
+
const authors = c('authors', {});
|
|
859
|
+
const posts = c('posts', {
|
|
860
|
+
expand: {
|
|
861
|
+
author: authors // Auto-expand on every fetch
|
|
862
|
+
}
|
|
863
|
+
});
|
|
891
864
|
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
865
|
+
const { data } = useLiveQuery((q) => q.from({ posts }));
|
|
866
|
+
|
|
867
|
+
// ⚠️ Slower - multiple queries + client-side join
|
|
868
|
+
// Only use TanStack DB joins for inner/right/full join behavior
|
|
896
869
|
```
|
|
897
870
|
|
|
898
|
-
### 6.
|
|
871
|
+
### 6. Configure QueryClient Defaults
|
|
899
872
|
|
|
900
873
|
```typescript
|
|
901
874
|
const queryClient = new QueryClient({
|
|
902
875
|
defaultOptions: {
|
|
903
876
|
queries: {
|
|
904
|
-
staleTime:
|
|
905
|
-
gcTime:
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
},
|
|
877
|
+
staleTime: 60_000, // 1 minute
|
|
878
|
+
gcTime: 300_000, // 5 minutes
|
|
879
|
+
refetchOnWindowFocus: false
|
|
880
|
+
}
|
|
881
|
+
}
|
|
910
882
|
});
|
|
911
883
|
```
|
|
912
884
|
|
|
@@ -917,7 +889,7 @@ const queryClient = new QueryClient({
|
|
|
917
889
|
By default, pbtsdb logs debug messages to the console in development mode. You can integrate with your own logging service (Sentry, LogRocket, etc.) using `setLogger`:
|
|
918
890
|
|
|
919
891
|
```typescript
|
|
920
|
-
import { setLogger } from '
|
|
892
|
+
import { setLogger } from 'pbtsdb';
|
|
921
893
|
|
|
922
894
|
// Example: Send errors to Sentry
|
|
923
895
|
setLogger({
|
|
@@ -946,7 +918,7 @@ setLogger({
|
|
|
946
918
|
**Disable logging completely:**
|
|
947
919
|
|
|
948
920
|
```typescript
|
|
949
|
-
import { setLogger } from '
|
|
921
|
+
import { setLogger } from 'pbtsdb';
|
|
950
922
|
|
|
951
923
|
setLogger({
|
|
952
924
|
debug: () => {},
|
|
@@ -958,14 +930,14 @@ setLogger({
|
|
|
958
930
|
**Reset to default logger:**
|
|
959
931
|
|
|
960
932
|
```typescript
|
|
961
|
-
import { resetLogger } from '
|
|
933
|
+
import { resetLogger } from 'pbtsdb';
|
|
962
934
|
|
|
963
935
|
resetLogger();
|
|
964
936
|
```
|
|
965
937
|
|
|
966
938
|
## License
|
|
967
939
|
|
|
968
|
-
|
|
940
|
+
MIT
|
|
969
941
|
|
|
970
942
|
## Contributing
|
|
971
943
|
|
|
@@ -979,8 +951,8 @@ Contributions welcome! Please open an issue or PR.
|
|
|
979
951
|
|
|
980
952
|
**Clone and Install:**
|
|
981
953
|
```bash
|
|
982
|
-
git clone https://github.com/yourusername/
|
|
983
|
-
cd
|
|
954
|
+
git clone https://github.com/yourusername/pbtsdb
|
|
955
|
+
cd pbtsdb
|
|
984
956
|
npm install
|
|
985
957
|
```
|
|
986
958
|
|