pbtsdb 0.0.1
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/LICENSE +21 -0
- package/README.md +1035 -0
- package/dist/index.d.ts +614 -0
- package/dist/index.js +545 -0
- package/dist/index.js.map +1 -0
- package/llms.txt +91 -0
- package/package.json +84 -0
package/README.md
ADDED
|
@@ -0,0 +1,1035 @@
|
|
|
1
|
+
# pbtsdb: PocketBase TanStack Database Integration
|
|
2
|
+
|
|
3
|
+
> Type-safe PocketBase integration with TanStack Query and TanStack DB
|
|
4
|
+
|
|
5
|
+
A TypeScript library that seamlessly integrates [PocketBase](https://pocketbase.io) with [TanStack Query](https://tanstack.com/query) and [TanStack DB](https://tanstack.com/db), providing:
|
|
6
|
+
|
|
7
|
+
- 🔥 **Real-time subscriptions** with automatic synchronization
|
|
8
|
+
- 🎯 **Full TypeScript type safety** for queries and relations
|
|
9
|
+
- ⚡ **Reactive collections** with TanStack DB
|
|
10
|
+
- 🔄 **Automatic caching** via TanStack Query
|
|
11
|
+
- 🎨 **React hooks** for easy component integration
|
|
12
|
+
- 🔗 **Type-safe joins** and relation expansion
|
|
13
|
+
|
|
14
|
+
## Table of Contents
|
|
15
|
+
|
|
16
|
+
- [Installation](#installation)
|
|
17
|
+
- [Quick Start](#quick-start)
|
|
18
|
+
- [Core Concepts](#core-concepts)
|
|
19
|
+
- [API Reference](#api-reference)
|
|
20
|
+
- [CollectionFactory](#collectionfactory)
|
|
21
|
+
- [React Provider](#react-provider)
|
|
22
|
+
- [Subscriptions](#subscriptions)
|
|
23
|
+
- [Usage Examples](#usage-examples)
|
|
24
|
+
- [Basic Queries](#basic-queries)
|
|
25
|
+
- [Filtering and Sorting](#filtering-and-sorting)
|
|
26
|
+
- [Relations and Joins](#relations-and-joins)
|
|
27
|
+
- [Real-time Updates](#real-time-updates)
|
|
28
|
+
- [TypeScript](#typescript)
|
|
29
|
+
- [Best Practices](#best-practices)
|
|
30
|
+
|
|
31
|
+
## Installation
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
npm install pocketbase-tanstack-db pocketbase @tanstack/react-query @tanstack/react-db @tanstack/query-db-collection
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
### Peer Dependencies
|
|
38
|
+
|
|
39
|
+
- `pocketbase` >= 0.21.0
|
|
40
|
+
- `@tanstack/react-query` >= 5.0.0
|
|
41
|
+
- `@tanstack/react-db` >= 0.1.0
|
|
42
|
+
- `@tanstack/query-db-collection` >= 1.0.0
|
|
43
|
+
- `react` >= 18.0.0
|
|
44
|
+
- `react-dom` >= 18.0.0
|
|
45
|
+
|
|
46
|
+
## Quick Start
|
|
47
|
+
|
|
48
|
+
### 1. Define Your Schema
|
|
49
|
+
|
|
50
|
+
Create type-safe schema definitions for your PocketBase collections. The schema should be formed like the below, it's recommended
|
|
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.
|
|
52
|
+
|
|
53
|
+
```typescript
|
|
54
|
+
import type { SchemaDeclaration } from 'pocketbase-tanstack-db';
|
|
55
|
+
|
|
56
|
+
// Define your record types
|
|
57
|
+
interface Author {
|
|
58
|
+
id: string;
|
|
59
|
+
name: string;
|
|
60
|
+
email: string;
|
|
61
|
+
created: string;
|
|
62
|
+
updated: string;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
interface Book {
|
|
66
|
+
id: string;
|
|
67
|
+
title: string;
|
|
68
|
+
author: string; // FK to authors
|
|
69
|
+
genre: 'Fiction' | 'Non-Fiction' | 'Science Fiction';
|
|
70
|
+
published_date: string;
|
|
71
|
+
created: string;
|
|
72
|
+
updated: string;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Create schema declaration
|
|
76
|
+
interface MySchema extends SchemaDeclaration {
|
|
77
|
+
authors: {
|
|
78
|
+
Row: Author;
|
|
79
|
+
Relations: {
|
|
80
|
+
forward: {};
|
|
81
|
+
back: {
|
|
82
|
+
books: ['books', true]; // One-to-many relation
|
|
83
|
+
};
|
|
84
|
+
};
|
|
85
|
+
};
|
|
86
|
+
books: {
|
|
87
|
+
Row: Book;
|
|
88
|
+
Relations: {
|
|
89
|
+
forward: {
|
|
90
|
+
author: ['authors', false]; // Many-to-one relation
|
|
91
|
+
};
|
|
92
|
+
back: {};
|
|
93
|
+
};
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
### 2. Initialize PocketBase and Collections
|
|
99
|
+
|
|
100
|
+
```typescript
|
|
101
|
+
import PocketBase from 'pocketbase';
|
|
102
|
+
import { QueryClient } from '@tanstack/react-query';
|
|
103
|
+
import { CollectionFactory } from 'pocketbase-tanstack-db';
|
|
104
|
+
|
|
105
|
+
// Initialize PocketBase
|
|
106
|
+
const pb = new PocketBase('http://localhost:8090');
|
|
107
|
+
|
|
108
|
+
// Initialize QueryClient
|
|
109
|
+
const queryClient = new QueryClient({
|
|
110
|
+
defaultOptions: {
|
|
111
|
+
queries: {
|
|
112
|
+
staleTime: 1000 * 60, // 1 minute
|
|
113
|
+
},
|
|
114
|
+
},
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
// Create collection factory
|
|
118
|
+
const factory = new CollectionFactory<MySchema>(pb, queryClient);
|
|
119
|
+
|
|
120
|
+
// Create collections
|
|
121
|
+
const authorsCollection = factory.create('authors');
|
|
122
|
+
const booksCollection = factory.create('books');
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
### 3. Use in React Components
|
|
126
|
+
|
|
127
|
+
```typescript
|
|
128
|
+
import { useLiveQuery } from '@tanstack/react-db';
|
|
129
|
+
|
|
130
|
+
function BooksList() {
|
|
131
|
+
const { data: books, isLoading } = useLiveQuery((q) =>
|
|
132
|
+
q.from({ books: booksCollection })
|
|
133
|
+
);
|
|
134
|
+
|
|
135
|
+
if (isLoading) return <div>Loading...</div>;
|
|
136
|
+
|
|
137
|
+
return (
|
|
138
|
+
<ul>
|
|
139
|
+
{books?.map(book => (
|
|
140
|
+
<li key={book.id}>{book.title}</li>
|
|
141
|
+
))}
|
|
142
|
+
</ul>
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
## Core Concepts
|
|
148
|
+
|
|
149
|
+
### Collections
|
|
150
|
+
|
|
151
|
+
Collections are reactive data stores that automatically sync with PocketBase:
|
|
152
|
+
|
|
153
|
+
```typescript
|
|
154
|
+
// Create a collection
|
|
155
|
+
const booksCollection = factory.create('books');
|
|
156
|
+
|
|
157
|
+
// Collections automatically:
|
|
158
|
+
// - Fetch data from PocketBase
|
|
159
|
+
// - Subscribe to real-time updates
|
|
160
|
+
// - Update React components when data changes
|
|
161
|
+
// - Cache data via TanStack Query
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
### Real-time Subscriptions
|
|
165
|
+
|
|
166
|
+
Collections manage subscriptions **automatically** based on query lifecycle:
|
|
167
|
+
|
|
168
|
+
```typescript
|
|
169
|
+
// Collections are lazy - no subscription until queried
|
|
170
|
+
const booksCollection = factory.create('books');
|
|
171
|
+
|
|
172
|
+
// Subscription starts automatically when query becomes active
|
|
173
|
+
const { data } = useLiveQuery((q) =>
|
|
174
|
+
q.from({ books: booksCollection })
|
|
175
|
+
);
|
|
176
|
+
// ✅ Subscribed to changes while component is mounted
|
|
177
|
+
// ✅ Unsubscribes automatically when component unmounts (with 5s cleanup delay)
|
|
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
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
### Type Safety
|
|
187
|
+
|
|
188
|
+
Full TypeScript support with compile-time type checking:
|
|
189
|
+
|
|
190
|
+
```typescript
|
|
191
|
+
const { data } = useLiveQuery((q) =>
|
|
192
|
+
q.from({ books: booksCollection })
|
|
193
|
+
);
|
|
194
|
+
|
|
195
|
+
// TypeScript knows:
|
|
196
|
+
// - data[0].title is a string
|
|
197
|
+
// - data[0].genre is 'Fiction' | 'Non-Fiction' | 'Science Fiction'
|
|
198
|
+
// - data[0].author is a string (FK)
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
## API Reference
|
|
202
|
+
|
|
203
|
+
### CollectionFactory
|
|
204
|
+
|
|
205
|
+
The main class for creating type-safe collections.
|
|
206
|
+
|
|
207
|
+
#### Constructor
|
|
208
|
+
|
|
209
|
+
```typescript
|
|
210
|
+
new CollectionFactory<Schema>(pocketbase: PocketBase, queryClient: QueryClient)
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
**Parameters:**
|
|
214
|
+
- `pocketbase`: PocketBase instance
|
|
215
|
+
- `queryClient`: TanStack Query QueryClient instance
|
|
216
|
+
|
|
217
|
+
**Example:**
|
|
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
|
+
```
|
|
232
|
+
|
|
233
|
+
**Options:**
|
|
234
|
+
- `expand?: string` - Relations to expand (e.g., `'author,metadata'`)
|
|
235
|
+
- `relations?: Record<string, Collection>` - Collections for manual joins
|
|
236
|
+
|
|
237
|
+
**Examples:**
|
|
238
|
+
|
|
239
|
+
Basic collection (lazy, subscribes automatically on first query):
|
|
240
|
+
```typescript
|
|
241
|
+
const booksCollection = factory.create('books');
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
With expand:
|
|
245
|
+
```typescript
|
|
246
|
+
const booksCollection = factory.create('books', {
|
|
247
|
+
expand: 'author' as const // Type-safe expand
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
// Expanded relations available
|
|
251
|
+
const { data } = useLiveQuery((q) => q.from({ books: booksCollection }));
|
|
252
|
+
data[0].expand?.author // ✅ Typed!
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
With relations for joins:
|
|
256
|
+
```typescript
|
|
257
|
+
const authorsCollection = factory.create('authors');
|
|
258
|
+
const booksCollection = factory.create('books', {
|
|
259
|
+
relations: {
|
|
260
|
+
author: authorsCollection
|
|
261
|
+
}
|
|
262
|
+
});
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
### React Provider
|
|
266
|
+
|
|
267
|
+
Provide collections to your React component tree.
|
|
268
|
+
|
|
269
|
+
#### CollectionsProvider
|
|
270
|
+
|
|
271
|
+
```typescript
|
|
272
|
+
<CollectionsProvider collections={collectionsMap}>
|
|
273
|
+
{children}
|
|
274
|
+
</CollectionsProvider>
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
**Example:**
|
|
278
|
+
```typescript
|
|
279
|
+
import { CollectionsProvider } from 'pocketbase-tanstack-db';
|
|
280
|
+
|
|
281
|
+
const collections = {
|
|
282
|
+
authors: factory.create('authors'),
|
|
283
|
+
books: factory.create('books'),
|
|
284
|
+
};
|
|
285
|
+
|
|
286
|
+
function App() {
|
|
287
|
+
return (
|
|
288
|
+
<CollectionsProvider collections={collections}>
|
|
289
|
+
<BooksList />
|
|
290
|
+
</CollectionsProvider>
|
|
291
|
+
);
|
|
292
|
+
}
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
#### useStore()
|
|
296
|
+
|
|
297
|
+
Access a single collection from the provider.
|
|
298
|
+
|
|
299
|
+
```typescript
|
|
300
|
+
const collection = useStore<RecordType>(key: string)
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
**Example:**
|
|
304
|
+
```typescript
|
|
305
|
+
function BooksList() {
|
|
306
|
+
const booksCollection = useStore<Book>('books');
|
|
307
|
+
|
|
308
|
+
const { data } = useLiveQuery((q) =>
|
|
309
|
+
q.from({ books: booksCollection })
|
|
310
|
+
);
|
|
311
|
+
|
|
312
|
+
return <div>{/* ... */}</div>;
|
|
313
|
+
}
|
|
314
|
+
```
|
|
315
|
+
|
|
316
|
+
#### useStores()
|
|
317
|
+
|
|
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
|
+
function BooksWithAuthors() {
|
|
327
|
+
const [booksCollection, authorsCollection] = useStores<[Book, Author]>(
|
|
328
|
+
['books', 'authors']
|
|
329
|
+
);
|
|
330
|
+
|
|
331
|
+
const { data } = useLiveQuery((q) =>
|
|
332
|
+
q.from({ book: booksCollection })
|
|
333
|
+
.join(
|
|
334
|
+
{ author: authorsCollection },
|
|
335
|
+
({ book, author }) => eq(book.author, author.id),
|
|
336
|
+
'left'
|
|
337
|
+
)
|
|
338
|
+
);
|
|
339
|
+
|
|
340
|
+
return <div>{/* ... */}</div>;
|
|
341
|
+
}
|
|
342
|
+
```
|
|
343
|
+
|
|
344
|
+
### Subscriptions
|
|
345
|
+
|
|
346
|
+
Collections support real-time subscriptions to PocketBase.
|
|
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
|
+
```
|
|
371
|
+
|
|
372
|
+
#### unsubscribeAll()
|
|
373
|
+
|
|
374
|
+
Clear all subscriptions for a collection.
|
|
375
|
+
|
|
376
|
+
```typescript
|
|
377
|
+
collection.unsubscribeAll();
|
|
378
|
+
```
|
|
379
|
+
|
|
380
|
+
#### isSubscribed()
|
|
381
|
+
|
|
382
|
+
Check subscription status.
|
|
383
|
+
|
|
384
|
+
```typescript
|
|
385
|
+
// Check collection-wide subscription
|
|
386
|
+
const isSubbed = collection.isSubscribed(); // boolean
|
|
387
|
+
|
|
388
|
+
// Check specific record subscription
|
|
389
|
+
const isRecordSubbed = collection.isSubscribed('record_id'); // boolean
|
|
390
|
+
```
|
|
391
|
+
|
|
392
|
+
#### waitForSubscription()
|
|
393
|
+
|
|
394
|
+
Wait for subscription to be established (useful in tests).
|
|
395
|
+
|
|
396
|
+
```typescript
|
|
397
|
+
await collection.waitForSubscription(); // Wait for collection-wide
|
|
398
|
+
await collection.waitForSubscription('record_id'); // Wait for specific record
|
|
399
|
+
await collection.waitForSubscription(undefined, 5000); // With timeout
|
|
400
|
+
```
|
|
401
|
+
|
|
402
|
+
## Usage Examples
|
|
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>;
|
|
415
|
+
|
|
416
|
+
return (
|
|
417
|
+
<ul>
|
|
418
|
+
{books?.map(book => (
|
|
419
|
+
<li key={book.id}>{book.title}</li>
|
|
420
|
+
))}
|
|
421
|
+
</ul>
|
|
422
|
+
);
|
|
423
|
+
```
|
|
424
|
+
|
|
425
|
+
#### Fetch Single Record
|
|
426
|
+
|
|
427
|
+
```typescript
|
|
428
|
+
import { eq } from '@tanstack/db';
|
|
429
|
+
|
|
430
|
+
const bookId = 'abc123';
|
|
431
|
+
|
|
432
|
+
const { data: books } = useLiveQuery((q) =>
|
|
433
|
+
q.from({ books: booksCollection })
|
|
434
|
+
.where(({ books }) => eq(books.id, bookId))
|
|
435
|
+
);
|
|
436
|
+
|
|
437
|
+
const book = books?.[0];
|
|
438
|
+
```
|
|
439
|
+
|
|
440
|
+
### Filtering and Sorting
|
|
441
|
+
|
|
442
|
+
#### Basic Filtering
|
|
443
|
+
|
|
444
|
+
```typescript
|
|
445
|
+
import { eq, gt, and, or } from '@tanstack/db';
|
|
446
|
+
|
|
447
|
+
// Filter by genre
|
|
448
|
+
const { data } = useLiveQuery((q) =>
|
|
449
|
+
q.from({ books: booksCollection })
|
|
450
|
+
.where(({ books }) => eq(books.genre, 'Fiction'))
|
|
451
|
+
);
|
|
452
|
+
|
|
453
|
+
// Filter by date
|
|
454
|
+
const { data } = useLiveQuery((q) =>
|
|
455
|
+
q.from({ books: booksCollection })
|
|
456
|
+
.where(({ books }) =>
|
|
457
|
+
gt(books.published_date, '2020-01-01')
|
|
458
|
+
)
|
|
459
|
+
);
|
|
460
|
+
|
|
461
|
+
// Multiple conditions with AND
|
|
462
|
+
const { data } = useLiveQuery((q) =>
|
|
463
|
+
q.from({ books: booksCollection })
|
|
464
|
+
.where(({ books }) => and(
|
|
465
|
+
eq(books.genre, 'Fiction'),
|
|
466
|
+
gt(books.published_date, '2020-01-01')
|
|
467
|
+
))
|
|
468
|
+
);
|
|
469
|
+
|
|
470
|
+
// Multiple conditions with OR
|
|
471
|
+
const { data } = useLiveQuery((q) =>
|
|
472
|
+
q.from({ books: booksCollection })
|
|
473
|
+
.where(({ books }) => or(
|
|
474
|
+
eq(books.genre, 'Fiction'),
|
|
475
|
+
eq(books.genre, 'Science Fiction')
|
|
476
|
+
))
|
|
477
|
+
);
|
|
478
|
+
```
|
|
479
|
+
|
|
480
|
+
#### Advanced Filtering
|
|
481
|
+
|
|
482
|
+
```typescript
|
|
483
|
+
import { gte, lte, and } from '@tanstack/db';
|
|
484
|
+
|
|
485
|
+
// Complex nested queries
|
|
486
|
+
const { data } = useLiveQuery((q) =>
|
|
487
|
+
q.from({ books: booksCollection })
|
|
488
|
+
.where(({ books }) => and(
|
|
489
|
+
eq(books.genre, 'Fiction'),
|
|
490
|
+
or(
|
|
491
|
+
gte(books.published_date, '2020-01-01'),
|
|
492
|
+
lte(books.published_date, '2010-12-31')
|
|
493
|
+
)
|
|
494
|
+
))
|
|
495
|
+
);
|
|
496
|
+
```
|
|
497
|
+
|
|
498
|
+
#### Sorting
|
|
499
|
+
|
|
500
|
+
```typescript
|
|
501
|
+
// Sort descending
|
|
502
|
+
const { data } = useLiveQuery((q) =>
|
|
503
|
+
q.from({ books: booksCollection })
|
|
504
|
+
.orderBy(({ books }) => books.published_date, 'desc')
|
|
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
|
+
```
|
|
520
|
+
|
|
521
|
+
### Relations and Joins
|
|
522
|
+
|
|
523
|
+
#### Type-Safe Expand (Recommended)
|
|
524
|
+
|
|
525
|
+
Use PocketBase's built-in expand for fast, server-side joins:
|
|
526
|
+
|
|
527
|
+
```typescript
|
|
528
|
+
// Create collection with expand
|
|
529
|
+
const booksCollection = factory.create('books', {
|
|
530
|
+
expand: 'author' as const // ← Type-safe!
|
|
531
|
+
});
|
|
532
|
+
|
|
533
|
+
// Use in query
|
|
534
|
+
const { data } = useLiveQuery((q) =>
|
|
535
|
+
q.from({ books: booksCollection })
|
|
536
|
+
);
|
|
537
|
+
|
|
538
|
+
// Access expanded relations (fully typed!)
|
|
539
|
+
data?.forEach(book => {
|
|
540
|
+
if (book.expand?.author) {
|
|
541
|
+
console.log(book.expand.author.name); // ✅ Type-safe
|
|
542
|
+
}
|
|
543
|
+
});
|
|
544
|
+
```
|
|
545
|
+
|
|
546
|
+
#### Multiple Expands
|
|
547
|
+
|
|
548
|
+
```typescript
|
|
549
|
+
const booksCollection = factory.create('books', {
|
|
550
|
+
expand: 'author,metadata' as const
|
|
551
|
+
});
|
|
552
|
+
|
|
553
|
+
// Both relations expanded
|
|
554
|
+
data[0].expand?.author // ✅ Author type
|
|
555
|
+
data[0].expand?.metadata // ✅ Metadata type
|
|
556
|
+
```
|
|
557
|
+
|
|
558
|
+
#### TanStack DB Joins
|
|
559
|
+
|
|
560
|
+
For complex client-side joins:
|
|
561
|
+
|
|
562
|
+
```typescript
|
|
563
|
+
const authorsCollection = factory.create('authors');
|
|
564
|
+
const booksCollection = factory.create('books', {
|
|
565
|
+
relations: {
|
|
566
|
+
author: authorsCollection
|
|
567
|
+
}
|
|
568
|
+
});
|
|
569
|
+
|
|
570
|
+
// Manual join with full type safety
|
|
571
|
+
const { data } = useLiveQuery((q) =>
|
|
572
|
+
q.from({ book: booksCollection })
|
|
573
|
+
.join(
|
|
574
|
+
{ author: authorsCollection },
|
|
575
|
+
({ book, author }) => eq(book.author, author.id),
|
|
576
|
+
'left' // Join type: 'left' | 'right' | 'inner' | 'full'
|
|
577
|
+
)
|
|
578
|
+
.select(({ book, author }) => ({
|
|
579
|
+
...book,
|
|
580
|
+
expand: {
|
|
581
|
+
author: author ? { ...author } : undefined
|
|
582
|
+
}
|
|
583
|
+
}))
|
|
584
|
+
);
|
|
585
|
+
```
|
|
586
|
+
|
|
587
|
+
#### Inner Join (Filter Out Missing Relations)
|
|
588
|
+
|
|
589
|
+
```typescript
|
|
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
|
|
601
|
+
|
|
602
|
+
```typescript
|
|
603
|
+
const authorsCollection = factory.create('authors');
|
|
604
|
+
const booksCollection = factory.create('books');
|
|
605
|
+
const metadataCollection = factory.create('book_metadata');
|
|
606
|
+
|
|
607
|
+
const { data } = useLiveQuery((q) =>
|
|
608
|
+
q.from({ book: booksCollection })
|
|
609
|
+
.join(
|
|
610
|
+
{ author: authorsCollection },
|
|
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
|
+
```
|
|
628
|
+
|
|
629
|
+
### Real-time Updates
|
|
630
|
+
|
|
631
|
+
#### Automatic Updates
|
|
632
|
+
|
|
633
|
+
Collections automatically receive real-time updates based on query lifecycle:
|
|
634
|
+
|
|
635
|
+
```typescript
|
|
636
|
+
function BooksList() {
|
|
637
|
+
const { data } = useLiveQuery((q) =>
|
|
638
|
+
q.from({ books: booksCollection })
|
|
639
|
+
);
|
|
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
|
+
}
|
|
650
|
+
```
|
|
651
|
+
|
|
652
|
+
**Subscription Lifecycle:**
|
|
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)
|
|
660
|
+
|
|
661
|
+
For advanced use cases, you can manually control subscriptions:
|
|
662
|
+
|
|
663
|
+
```typescript
|
|
664
|
+
const booksCollection = factory.create('books');
|
|
665
|
+
|
|
666
|
+
// Manually subscribe (bypasses automatic lifecycle)
|
|
667
|
+
await booksCollection.subscribe();
|
|
668
|
+
|
|
669
|
+
// Subscribe to specific record
|
|
670
|
+
await booksCollection.subscribe('record_id');
|
|
671
|
+
|
|
672
|
+
// Unsubscribe when done
|
|
673
|
+
booksCollection.unsubscribe('record_id');
|
|
674
|
+
booksCollection.unsubscribeAll();
|
|
675
|
+
```
|
|
676
|
+
|
|
677
|
+
#### Subscribing to Specific Records
|
|
678
|
+
|
|
679
|
+
```typescript
|
|
680
|
+
// Subscribe to a specific book
|
|
681
|
+
await booksCollection.subscribe('book_id_123');
|
|
682
|
+
|
|
683
|
+
// Check if subscribed
|
|
684
|
+
if (booksCollection.isSubscribed('book_id_123')) {
|
|
685
|
+
console.log('Subscribed to book_id_123');
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
// Unsubscribe from specific record
|
|
689
|
+
booksCollection.unsubscribe('book_id_123');
|
|
690
|
+
```
|
|
691
|
+
|
|
692
|
+
#### Waiting for Subscription (Testing)
|
|
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
|
+
```
|
|
706
|
+
|
|
707
|
+
## TypeScript
|
|
708
|
+
|
|
709
|
+
### Schema Declaration
|
|
710
|
+
|
|
711
|
+
Define your schema with full type safety:
|
|
712
|
+
|
|
713
|
+
```typescript
|
|
714
|
+
import type { SchemaDeclaration } from 'pocketbase-tanstack-db';
|
|
715
|
+
|
|
716
|
+
interface MySchema extends SchemaDeclaration {
|
|
717
|
+
collection_name: {
|
|
718
|
+
Row: RecordType; // Your record type
|
|
719
|
+
Relations: {
|
|
720
|
+
forward: {
|
|
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
|
+
};
|
|
731
|
+
};
|
|
732
|
+
};
|
|
733
|
+
}
|
|
734
|
+
```
|
|
735
|
+
|
|
736
|
+
### Example: Blog Schema
|
|
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
|
+
```
|
|
795
|
+
|
|
796
|
+
### Type-Safe Expand
|
|
797
|
+
|
|
798
|
+
Use `as const` for type-safe expand strings:
|
|
799
|
+
|
|
800
|
+
```typescript
|
|
801
|
+
const postsCollection = factory.create('posts', {
|
|
802
|
+
expand: 'author,tags' as const
|
|
803
|
+
// ✅ TypeScript validates these are real relations
|
|
804
|
+
});
|
|
805
|
+
|
|
806
|
+
// Expanded fields are typed
|
|
807
|
+
data[0].expand?.author.username // ✅ string
|
|
808
|
+
data[0].expand?.tags[0].name // ✅ string
|
|
809
|
+
```
|
|
810
|
+
|
|
811
|
+
## Best Practices
|
|
812
|
+
|
|
813
|
+
### 1. Choose the Right Approach for Relations
|
|
814
|
+
|
|
815
|
+
**Use Type-Safe Expand when:**
|
|
816
|
+
- You need fast, single-query performance
|
|
817
|
+
- Relations are straightforward
|
|
818
|
+
|
|
819
|
+
|
|
820
|
+
```typescript
|
|
821
|
+
// ✅ Fast, simple, type-safe
|
|
822
|
+
const books = factory.create('books', {
|
|
823
|
+
expand: 'author' as const
|
|
824
|
+
});
|
|
825
|
+
```
|
|
826
|
+
|
|
827
|
+
**Use TanStack Joins when:**
|
|
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
|
|
832
|
+
|
|
833
|
+
|
|
834
|
+
```typescript
|
|
835
|
+
// ✅ Flexible, powerful, type-safe
|
|
836
|
+
const { data } = useLiveQuery((q) =>
|
|
837
|
+
q.from({ book: booksCollection })
|
|
838
|
+
.join({ author: authorsCollection }, ..., 'inner')
|
|
839
|
+
);
|
|
840
|
+
```
|
|
841
|
+
|
|
842
|
+
### 2. Use Provider for App-Wide Collections
|
|
843
|
+
|
|
844
|
+
```typescript
|
|
845
|
+
// ✅ Define collections once
|
|
846
|
+
const collections = {
|
|
847
|
+
books: factory.create('books'),
|
|
848
|
+
authors: factory.create('authors'),
|
|
849
|
+
};
|
|
850
|
+
|
|
851
|
+
// Use throughout app
|
|
852
|
+
<CollectionsProvider collections={collections}>
|
|
853
|
+
<App />
|
|
854
|
+
</CollectionsProvider>
|
|
855
|
+
```
|
|
856
|
+
|
|
857
|
+
### 3. Type Your Hooks
|
|
858
|
+
|
|
859
|
+
```typescript
|
|
860
|
+
// ✅ Explicit typing
|
|
861
|
+
const booksCollection = useStore<Book>('books');
|
|
862
|
+
|
|
863
|
+
// ✅ Tuple typing for multiple collections
|
|
864
|
+
const [books, authors] = useStores<[Book, Author]>(['books', 'authors']);
|
|
865
|
+
```
|
|
866
|
+
|
|
867
|
+
### 4. Handle Loading and Error States
|
|
868
|
+
|
|
869
|
+
```typescript
|
|
870
|
+
const { data, isLoading, error } = useLiveQuery((q) =>
|
|
871
|
+
q.from({ books: booksCollection })
|
|
872
|
+
);
|
|
873
|
+
|
|
874
|
+
if (isLoading) return <Spinner />;
|
|
875
|
+
if (error) return <ErrorMessage error={error} />;
|
|
876
|
+
if (!data?.length) return <EmptyState />;
|
|
877
|
+
|
|
878
|
+
return <BooksList books={data} />;
|
|
879
|
+
```
|
|
880
|
+
|
|
881
|
+
### 5. Subscriptions are Automatic
|
|
882
|
+
|
|
883
|
+
No need to manually subscribe/unsubscribe - the library handles it:
|
|
884
|
+
|
|
885
|
+
```typescript
|
|
886
|
+
// ❌ Don't do this (unless you have advanced use case)
|
|
887
|
+
useEffect(() => {
|
|
888
|
+
booksCollection.subscribe();
|
|
889
|
+
return () => booksCollection.unsubscribe();
|
|
890
|
+
}, []);
|
|
891
|
+
|
|
892
|
+
// ✅ Do this instead - automatic lifecycle management
|
|
893
|
+
const { data } = useLiveQuery((q) =>
|
|
894
|
+
q.from({ books: booksCollection })
|
|
895
|
+
);
|
|
896
|
+
```
|
|
897
|
+
|
|
898
|
+
### 6. Use QueryClient Configuration
|
|
899
|
+
|
|
900
|
+
```typescript
|
|
901
|
+
const queryClient = new QueryClient({
|
|
902
|
+
defaultOptions: {
|
|
903
|
+
queries: {
|
|
904
|
+
staleTime: 1000 * 60 * 5, // 5 minutes
|
|
905
|
+
gcTime: 1000 * 60 * 10, // 10 minutes
|
|
906
|
+
retry: 3,
|
|
907
|
+
refetchOnWindowFocus: false,
|
|
908
|
+
},
|
|
909
|
+
},
|
|
910
|
+
});
|
|
911
|
+
```
|
|
912
|
+
|
|
913
|
+
## Configuration
|
|
914
|
+
|
|
915
|
+
### Custom Logger Integration
|
|
916
|
+
|
|
917
|
+
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
|
+
|
|
919
|
+
```typescript
|
|
920
|
+
import { setLogger } from 'pocketbase-tanstack-db';
|
|
921
|
+
|
|
922
|
+
// Example: Send errors to Sentry
|
|
923
|
+
setLogger({
|
|
924
|
+
debug: (msg, context) => {
|
|
925
|
+
// Custom debug handling (e.g., only log in dev)
|
|
926
|
+
if (process.env.NODE_ENV === 'development') {
|
|
927
|
+
console.debug('[pbtsdb]', msg, context);
|
|
928
|
+
}
|
|
929
|
+
},
|
|
930
|
+
warn: (msg, context) => {
|
|
931
|
+
console.warn('[pbtsdb]', msg, context);
|
|
932
|
+
// Optional: Send to monitoring service
|
|
933
|
+
myMonitoringService.warn(msg, context);
|
|
934
|
+
},
|
|
935
|
+
error: (msg, context) => {
|
|
936
|
+
console.error('[pbtsdb]', msg, context);
|
|
937
|
+
// Send errors to error tracking service
|
|
938
|
+
Sentry.captureMessage(msg, {
|
|
939
|
+
level: 'error',
|
|
940
|
+
extra: context,
|
|
941
|
+
});
|
|
942
|
+
},
|
|
943
|
+
});
|
|
944
|
+
```
|
|
945
|
+
|
|
946
|
+
**Disable logging completely:**
|
|
947
|
+
|
|
948
|
+
```typescript
|
|
949
|
+
import { setLogger } from 'pocketbase-tanstack-db';
|
|
950
|
+
|
|
951
|
+
setLogger({
|
|
952
|
+
debug: () => {},
|
|
953
|
+
warn: () => {},
|
|
954
|
+
error: () => {},
|
|
955
|
+
});
|
|
956
|
+
```
|
|
957
|
+
|
|
958
|
+
**Reset to default logger:**
|
|
959
|
+
|
|
960
|
+
```typescript
|
|
961
|
+
import { resetLogger } from 'pocketbase-tanstack-db';
|
|
962
|
+
|
|
963
|
+
resetLogger();
|
|
964
|
+
```
|
|
965
|
+
|
|
966
|
+
## License
|
|
967
|
+
|
|
968
|
+
ISC
|
|
969
|
+
|
|
970
|
+
## Contributing
|
|
971
|
+
|
|
972
|
+
Contributions welcome! Please open an issue or PR.
|
|
973
|
+
|
|
974
|
+
### Development Setup
|
|
975
|
+
|
|
976
|
+
**Prerequisites:**
|
|
977
|
+
- Node.js 18+
|
|
978
|
+
- Git
|
|
979
|
+
|
|
980
|
+
**Clone and Install:**
|
|
981
|
+
```bash
|
|
982
|
+
git clone https://github.com/yourusername/pocketbase-tanstack-db
|
|
983
|
+
cd pocketbase-tanstack-db
|
|
984
|
+
npm install
|
|
985
|
+
```
|
|
986
|
+
|
|
987
|
+
### Running Tests
|
|
988
|
+
|
|
989
|
+
Tests use a real PocketBase instance with **fully automated infrastructure**:
|
|
990
|
+
|
|
991
|
+
```bash
|
|
992
|
+
npm test # Auto-resets DB → Starts server → Runs tests → Stops server
|
|
993
|
+
```
|
|
994
|
+
|
|
995
|
+
The `npm test` command automatically:
|
|
996
|
+
1. Resets the test database to a clean state
|
|
997
|
+
2. Applies migrations and creates test collections
|
|
998
|
+
3. Starts PocketBase server on port 8210
|
|
999
|
+
4. Runs all Vitest tests
|
|
1000
|
+
5. Stops the server when complete
|
|
1001
|
+
|
|
1002
|
+
**No manual server setup required!** All test infrastructure is automated.
|
|
1003
|
+
|
|
1004
|
+
**Advanced (for watch mode or debugging):**
|
|
1005
|
+
```bash
|
|
1006
|
+
# Start test server manually
|
|
1007
|
+
npm run test:server
|
|
1008
|
+
|
|
1009
|
+
# Run tests against running server (in another terminal)
|
|
1010
|
+
npm run test:run
|
|
1011
|
+
|
|
1012
|
+
# Just reset database without starting server
|
|
1013
|
+
npm run db:reset
|
|
1014
|
+
```
|
|
1015
|
+
|
|
1016
|
+
### Code Quality
|
|
1017
|
+
|
|
1018
|
+
```bash
|
|
1019
|
+
npm run checks # Run TypeScript type checking and linting
|
|
1020
|
+
npm run lint:fix # Auto-fix linting issues
|
|
1021
|
+
npm run typecheck # TypeScript only
|
|
1022
|
+
```
|
|
1023
|
+
|
|
1024
|
+
### Documentation
|
|
1025
|
+
|
|
1026
|
+
- See [AGENTS.md](AGENTS.md) for comprehensive development guidelines
|
|
1027
|
+
- See [test/README.md](test/README.md) for detailed testing documentation
|
|
1028
|
+
|
|
1029
|
+
---
|
|
1030
|
+
|
|
1031
|
+
**Built with:**
|
|
1032
|
+
- [PocketBase](https://pocketbase.io) - Backend-as-a-Service
|
|
1033
|
+
- [TanStack Query](https://tanstack.com/query) - Powerful data fetching
|
|
1034
|
+
- [TanStack DB](https://tanstack.com/db) - Reactive database
|
|
1035
|
+
- [TypeScript](https://www.typescriptlang.org) - Type safety
|