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/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