pbtsdb 0.7.3 → 0.9.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 CHANGED
@@ -10,7 +10,7 @@ A TypeScript library that seamlessly integrates [PocketBase](https://pocketbase.
10
10
  - 🔄 **Automatic caching** via TanStack Query
11
11
  - ✨ **Optimistic mutations** with insert/update/delete support
12
12
  - 🎨 **React hooks** for easy component integration
13
- - 🔗 **Type-safe joins** and relation expansion
13
+ - 🔗 **Type-safe joins** and relation fetching into their own collections
14
14
 
15
15
  ## Table of Contents
16
16
 
@@ -19,34 +19,33 @@ A TypeScript library that seamlessly integrates [PocketBase](https://pocketbase.
19
19
  - [Core Concepts](#core-concepts)
20
20
  - [API Reference](#api-reference)
21
21
  - [createCollection()](#createcollection)
22
+ - [Related data](#related-data)
22
23
  - [React Integration](#react-integration)
23
24
  - [Subscriptions](#subscriptions)
25
+ - [Utility Functions](#utility-functions)
24
26
  - [Usage Examples](#usage-examples)
25
- - [Basic Queries](#basic-queries)
26
- - [Filtering and Sorting](#filtering-and-sorting)
27
- - [Relations and Joins](#relations-and-joins)
28
27
  - [Includes (Nested Subqueries)](#includes-nested-subqueries)
29
- - [Real-time Updates](#real-time-updates)
30
- - [Mutations](#mutations)
31
28
  - [TypeScript](#typescript)
32
29
  - [Best Practices](#best-practices)
30
+ - [Configuration](#configuration)
33
31
 
34
32
  ## Installation
35
33
 
36
34
  ```bash
37
- npm install pbtsdb pocketbase @tanstack/react-query @tanstack/react-db @tanstack/query-db-collection
35
+ npm install pbtsdb pocketbase @tanstack/db @tanstack/query-db-collection @tanstack/react-query @tanstack/react-db
38
36
  ```
39
37
 
40
38
  ### Peer Dependencies
41
39
 
42
40
  - `pocketbase` >= 0.22.0
41
+ - `@tanstack/db` >= 0.6.0
42
+ - `@tanstack/query-db-collection` >= 1.0.40
43
43
  - `@tanstack/react-query` >= 5.0.0
44
- - `@tanstack/react-db` >= 0.1.0
45
- - `@tanstack/query-db-collection` >= 1.0.0
46
- - `react` >= 18.0.0
47
- - `react-dom` >= 18.0.0
44
+ - `@tanstack/react-db` >= 0.1.86 (optional; only for `createReactProvider`)
45
+ - `react` and `react-dom` >= 18.0.0 (optional)
48
46
 
49
- All peer dependencies use minimum version constraints - newer versions should work.
47
+ All peer dependencies use minimum version constraints; newer versions should work. The
48
+ non-React entry point `pbtsdb/core` needs neither `react` nor `@tanstack/react-db`.
50
49
 
51
50
  ## Quick Start
52
51
 
@@ -123,14 +122,19 @@ const queryClient = new QueryClient({
123
122
 
124
123
  // Create collections with automatic type inference
125
124
  const c = createCollection<BlogSchema>(pb, queryClient);
125
+ const users = c('users', {});
126
126
  export const { Provider, useStore } = createReactProvider({
127
+ users,
127
128
  posts: c('posts', {
128
- omitOnInsert: ['created', 'updated'] as const
129
+ omitOnInsert: ['created', 'updated'] as const,
130
+ relations: { author: users },
131
+ alwaysFetchRelations: ['author'],
129
132
  }),
130
- users: c('users', {}),
131
133
  comments: c('comments', {
132
- omitOnInsert: ['created', 'updated'] as const
133
- })
134
+ omitOnInsert: ['created', 'updated'] as const,
135
+ relations: { author: users },
136
+ alwaysFetchRelations: ['author'],
137
+ }),
134
138
  });
135
139
 
136
140
  export function App() {
@@ -149,14 +153,22 @@ export function App() {
149
153
  ```typescript
150
154
  // BlogDashboard.tsx
151
155
  import { useLiveQuery } from '@tanstack/react-db';
156
+ import { eq } from '@tanstack/db';
157
+ import { materialize } from 'pbtsdb';
152
158
  import { useStore } from './app';
153
159
 
154
160
  export function BlogDashboard() {
155
- const [posts] = useStore('posts');
161
+ const [posts, users] = useStore('posts', 'users');
156
162
 
157
163
  const { data: allPosts, isLoading } = useLiveQuery((q) =>
158
164
  q.from({ posts })
159
165
  .orderBy(({ posts }) => posts.created, 'desc')
166
+ .select(({ posts }) => ({
167
+ ...posts,
168
+ author: materialize(
169
+ q.from({ u: users }).where(({ u }) => eq(u.id, posts.author)).findOne()
170
+ ),
171
+ }))
160
172
  );
161
173
 
162
174
  if (isLoading) return <div>Loading posts...</div>;
@@ -168,8 +180,8 @@ export function BlogDashboard() {
168
180
  <article key={post.id}>
169
181
  <h2>{post.title}</h2>
170
182
  <p>{post.content}</p>
171
- {/* Expanded author is fully typed! */}
172
- <small>By {post.expand?.author?.username}</small>
183
+ {/* Author was filed into the users collection by alwaysFetchRelations */}
184
+ <small>By {post.author?.username}</small>
173
185
  </article>
174
186
  ))}
175
187
  </div>
@@ -184,16 +196,22 @@ export function BlogDashboard() {
184
196
  import { useLiveQuery } from '@tanstack/react-db';
185
197
  import { eq } from '@tanstack/db';
186
198
  import { useStore } from './app';
187
- import { newRecordId } from 'pbtsdb';
199
+ import { materialize, newRecordId } from 'pbtsdb';
188
200
 
189
201
  export function PostWithComments({ postId }: { postId: string }) {
190
- const [comments, posts] = useStore('comments', 'posts');
202
+ const [comments, posts, users] = useStore('comments', 'posts', 'users');
191
203
 
192
204
  // Real-time comments for this post
193
205
  const { data: postComments } = useLiveQuery((q) =>
194
206
  q.from({ comments })
195
207
  .where(({ comments }) => eq(comments.post, postId))
196
208
  .orderBy(({ comments }) => comments.created, 'desc')
209
+ .select(({ comments }) => ({
210
+ ...comments,
211
+ author: materialize(
212
+ q.from({ u: users }).where(({ u }) => eq(u.id, comments.author)).findOne()
213
+ ),
214
+ }))
197
215
  );
198
216
 
199
217
  const handleAddComment = (text: string, authorId: string) => {
@@ -211,7 +229,7 @@ export function PostWithComments({ postId }: { postId: string }) {
211
229
  <h3>Comments ({postComments?.length || 0})</h3>
212
230
  {postComments?.map(comment => (
213
231
  <div key={comment.id}>
214
- <strong>{comment.expand?.author?.username}:</strong>
232
+ <strong>{comment.author?.username}:</strong>
215
233
  <p>{comment.text}</p>
216
234
  </div>
217
235
  ))}
@@ -225,7 +243,7 @@ export function PostWithComments({ postId }: { postId: string }) {
225
243
  - ✅ Type-safe queries
226
244
  - ✅ Automatic real-time updates
227
245
  - ✅ Optimistic mutations
228
- - ✅ Expanded relations
246
+ - ✅ Related data read from its own collection, no embedded copies
229
247
 
230
248
  ## Core Concepts
231
249
 
@@ -268,6 +286,22 @@ const { data } = useLiveQuery((q) =>
268
286
  - **Shared:** Multiple components using the same collection share one subscription
269
287
  - **No manual control needed:** The collection handles all subscription management internally
270
288
 
289
+ ### Sync Modes
290
+
291
+ Every collection is either **eager** (the default) or **on-demand**:
292
+
293
+ ```typescript
294
+ const authors = c('authors', {}); // eager: one full fetch, then realtime
295
+ const books = c('books', { syncMode: 'on-demand' }); // on-demand: fetch what queries ask for
296
+ ```
297
+
298
+ An eager collection loads all of its rows on first use and answers every query
299
+ from memory. An on-demand collection translates each live query's `where`,
300
+ `orderBy`, and `limit` into a PocketBase request, so only the rows a query asks
301
+ for enter the store, and different filters are cached under different keys.
302
+ Use on-demand for large collections; realtime keeps both modes current once rows
303
+ are loaded.
304
+
271
305
  ### Mutations and Refetch
272
306
 
273
307
  By default, pbtsdb does **not** refetch the entire collection after a successful insert, update, or delete. The realtime subscription delivers server-confirmed rows — including server-assigned fields like `id`, `created`, `updated`, and any values rewritten by PocketBase hooks — and TanStack DB's optimistic write keeps the UI consistent in the meantime. The post-mutation refetch is therefore redundant.
@@ -320,12 +354,14 @@ const collection = c(collectionName: string, options?: CreateCollectionOptions);
320
354
  - `options` - Optional configuration
321
355
 
322
356
  **Options:**
323
- - `expand?: Record<string, Collection>` - Relations to auto-expand and auto-upsert on every fetch
357
+ - `relations?: Record<string, Collection>` - Collections that receive expanded records for each relation; declares what `alwaysFetchRelations` and `collection.fetchRelations()` may name
358
+ - `alwaysFetchRelations?: readonly string[]` - Expand paths fetched with every request and filed into their `relations` targets; never kept on the row
324
359
  - `omitOnInsert?: readonly string[]` - Fields to make optional during insert (e.g., `['created', 'updated'] as const`)
325
360
  - `syncMode?: 'eager' | 'on-demand'` - Data fetching strategy (default: `'eager'`)
326
361
  - `onInsert?: InsertMutationFn | false` - Custom insert handler or `false` to disable
327
362
  - `onUpdate?: UpdateMutationFn | false` - Custom update handler or `false` to disable
328
363
  - `onDelete?: DeleteMutationFn | false` - Custom delete handler or `false` to disable
364
+ - `refetchOnMutation?: boolean` - Refetch the collection after a built-in insert/update/delete succeeds (default: `false`; see [Mutations and Refetch](#mutations-and-refetch))
329
365
  - `ignoreAutoCancellation?: boolean` - Ignore PocketBase auto-cancellation errors (default: `true`)
330
366
  - `collectionOptions?: object` - Additional TanStack DB collection options passed through directly (see [Collection Options Passthrough](#collection-options-passthrough))
331
367
 
@@ -339,40 +375,81 @@ const c = createCollection<MySchema>(pb, queryClient);
339
375
  const booksCollection = c('books', {});
340
376
  ```
341
377
 
342
- With auto-expand relations:
378
+ #### Related data
379
+
380
+ PocketBase `expand` is used only to bring related records into their own
381
+ collections. Rows never carry `expand`; read related records from the target
382
+ collection.
383
+
343
384
  ```typescript
344
385
  const c = createCollection<MySchema>(pb, queryClient);
345
- const authorsCollection = c('authors', {});
346
- const booksCollection = c('books', {
347
- expand: {
348
- author: authorsCollection // Auto-expand and auto-upsert
349
- }
386
+ const authors = c('authors', { syncMode: 'on-demand' });
387
+ const tags = c('tags', { syncMode: 'on-demand' });
388
+ const books = c('books', {
389
+ relations: { author: authors, tags }, // where expanded records are filed
390
+ alwaysFetchRelations: ['author'], // fetched with every books request
350
391
  });
392
+ ```
351
393
 
352
- // Expand is automatic on every fetch
353
- const { data } = useLiveQuery((q) => q.from({ books: booksCollection }));
394
+ Every books request expands `author`; the expanded authors are filed into
395
+ `authors` (an on-demand target has its sync started) and removed from the
396
+ book rows. Read them through `materialize()` in a query, a join, or
397
+ `authors.get(book.author)`:
354
398
 
355
- // Expanded records auto-inserted into authorsCollection
399
+ ```typescript
400
+ import { eq } from '@tanstack/db';
401
+ import { materialize } from 'pbtsdb';
402
+
403
+ const { data } = useLiveQuery((q) =>
404
+ q.from({ b: books }).select(({ b }) => ({
405
+ ...b,
406
+ author: materialize(
407
+ q.from({ a: authors }).where(({ a }) => eq(a.id, b.author)).findOne()
408
+ ),
409
+ }))
410
+ );
411
+ // data[0].author?.name is Authors | undefined and updates when the author changes
412
+ ```
413
+
414
+ Because the authors are already in the store, that include makes no request:
415
+ an id-only load (`eq(id, x)`, `inArray(id, [...])`, or an `or` of those) is
416
+ served from the synced store when every id is present, except when the query
417
+ reads through a `fetchRelations()` view, whose fetch goes to PocketBase so its
418
+ paths get filed; anything else is fetched in one batched request. An empty
419
+ `inArray(id, [])` yields no rows and no request.
420
+
421
+ Fetch a relation for one query only with `fetchRelations()`; the view shares the
422
+ collection's store, realtime subscription, and mutations, and only its fetches
423
+ add the `expand` parameter:
424
+
425
+ ```typescript
426
+ const { data } = useLiveQuery((q) => q.from({ b: books.fetchRelations('tags') }));
427
+ // tags referenced by these books are now in the tags collection
356
428
  ```
357
429
 
430
+ Paths can be nested through a target collection's own `relations`
431
+ (`'book.author'`). While a query fetches into a target, that target keeps its
432
+ realtime subscription, so `get()` reads stay fresh.
433
+
358
434
  #### Collection Options Passthrough
359
435
 
360
436
  Pass any [TanStack DB `BaseCollectionConfig`](https://tanstack.com/db/latest/docs/overview) option directly via `collectionOptions`. This is useful for configuring indexing, garbage collection, and other collection-level settings:
361
437
 
362
438
  ```typescript
363
- import { BasicIndex } from 'pbtsdb';
364
-
365
439
  const c = createCollection<MySchema>(pb, queryClient);
366
440
  const booksCollection = c('books', {
367
441
  collectionOptions: {
368
- autoIndex: 'eager',
369
- defaultIndexType: BasicIndex,
370
442
  gcTime: 60000, // 1 minute GC
371
443
  startSync: true, // Start syncing immediately
372
444
  }
373
445
  });
374
446
  ```
375
447
 
448
+ pbtsdb defaults `autoIndex` to `'eager'` with `defaultIndexType: BTreeIndex`, so
449
+ `orderBy` + `limit` queries page lazily instead of loading the whole subset (and
450
+ TanStack DB does not warn about a missing index). Pass `autoIndex: 'off'` or a
451
+ different `defaultIndexType` in `collectionOptions` to change that per collection.
452
+
376
453
  The following fields are managed by pbtsdb and excluded from `collectionOptions`: `getKey`, `syncMode`, `onInsert`, `onUpdate`, `onDelete`, `schema`.
377
454
 
378
455
  ### React Integration
@@ -534,6 +611,10 @@ both transports:
534
611
  Setting the matching header on REST requests remains the application's job, via
535
612
  `pb.beforeSend`. Requires `pocketbase >= 0.22.0`.
536
613
 
614
+ An `expand` you add here is yours: pbtsdb strips only the paths it requested
615
+ through `alwaysFetchRelations` and `fetchRelations()`, so records expanded by
616
+ this option stay on the echoed rows, untyped.
617
+
537
618
  ### Utility Functions
538
619
 
539
620
  #### newRecordId()
@@ -564,6 +645,7 @@ pbtsdb re-exports commonly used TanStack DB utilities so you don't need to depen
564
645
  ```typescript
565
646
  import {
566
647
  // Includes helpers
648
+ materialize,
567
649
  toArray,
568
650
  createEffect,
569
651
 
@@ -624,7 +706,7 @@ export function TaskBoard({ userId }: { userId: string }) {
624
706
  ```typescript
625
707
  // ProductCatalog.tsx
626
708
  import { useLiveQuery } from '@tanstack/react-db';
627
- import { and, gte, lte } from '@tanstack/db';
709
+ import { and, eq, lte } from '@tanstack/db';
628
710
  import { useStore } from './app';
629
711
 
630
712
  export function ProductCatalog() {
@@ -636,12 +718,12 @@ export function ProductCatalog() {
636
718
  const { data: filteredProducts } = useLiveQuery((q) => {
637
719
  let query = q.from({ products })
638
720
  .where(({ products }) => and(
639
- products.in_stock === true,
721
+ eq(products.in_stock, true),
640
722
  lte(products.price, maxPrice)
641
723
  ));
642
724
 
643
725
  if (category) {
644
- query = query.where(({ products }) => products.category === category);
726
+ query = query.where(({ products }) => eq(products.category, category));
645
727
  }
646
728
 
647
729
  return query.orderBy(({ products }) => products.rating, 'desc');
@@ -671,13 +753,20 @@ export function ProductCatalog() {
671
753
  import { useLiveQuery } from '@tanstack/react-db';
672
754
  import { eq } from '@tanstack/db';
673
755
  import { useStore } from './app';
674
- import { newRecordId } from 'pbtsdb';
756
+ import { materialize, newRecordId } from 'pbtsdb';
675
757
 
676
758
  export function SocialFeed({ currentUserId }: { currentUserId: string }) {
677
- const [posts, likes] = useStore('posts', 'likes');
759
+ const [posts, likes, users] = useStore('posts', 'likes', 'users');
678
760
 
679
761
  const { data: feedPosts } = useLiveQuery((q) =>
680
- q.from({ posts }).orderBy(({ posts }) => posts.created, 'desc')
762
+ q.from({ posts })
763
+ .orderBy(({ posts }) => posts.created, 'desc')
764
+ .select(({ posts }) => ({
765
+ ...posts,
766
+ author: materialize(
767
+ q.from({ u: users }).where(({ u }) => eq(u.id, posts.author)).findOne()
768
+ ),
769
+ }))
681
770
  );
682
771
 
683
772
  const { data: userLikes } = useLiveQuery((q) =>
@@ -699,7 +788,7 @@ export function SocialFeed({ currentUserId }: { currentUserId: string }) {
699
788
  <div>
700
789
  {feedPosts?.map(post => (
701
790
  <div key={post.id}>
702
- <strong>{post.expand?.author?.username}</strong>
791
+ <strong>{post.author?.username}</strong>
703
792
  <p>{post.content}</p>
704
793
  <button onClick={() => handleLike(post.id)}>
705
794
  {likedPostIds.has(post.id) ? '❤️' : '🤍'} {post.likes_count}
@@ -781,8 +870,9 @@ export function CreateBookForm() {
781
870
 
782
871
  if (tx.state === 'completed') setTitle('');
783
872
  else setError('Failed to create book');
784
- } catch (err: any) {
785
- setError(err.data ? Object.values(err.data).join(', ') : err.message);
873
+ } catch (err) {
874
+ const failure = err as { data?: Record<string, unknown>; message?: string };
875
+ setError(failure.data ? Object.values(failure.data).join(', ') : failure.message ?? 'Failed');
786
876
  }
787
877
  };
788
878
 
@@ -846,20 +936,27 @@ TanStack DB 0.6.0 introduces **includes** — nested subqueries within `select()
846
936
 
847
937
  #### Single relation with `findOne()`
848
938
 
939
+ Wrap a `findOne()` subquery in `materialize()` to get a plain `T | undefined`
940
+ value that updates when the child changes. Without it, the include is a live
941
+ sub-collection rather than a value.
942
+
849
943
  ```typescript
850
944
  import { useLiveQuery, eq } from '@tanstack/react-db';
945
+ import { materialize } from 'pbtsdb';
851
946
 
852
947
  const { data: booksWithAuthors } = useLiveQuery((q) =>
853
948
  q.from({ b: booksCollection }).select(({ b }) => ({
854
949
  id: b.id,
855
950
  title: b.title,
856
- author: q
857
- .from({ a: authorsCollection })
858
- .where(({ a }) => eq(a.id, b.author))
859
- .select(({ a }) => ({ id: a.id, name: a.name }))
860
- .findOne(),
951
+ author: materialize(
952
+ q.from({ a: authorsCollection })
953
+ .where(({ a }) => eq(a.id, b.author))
954
+ .select(({ a }) => ({ id: a.id, name: a.name }))
955
+ .findOne()
956
+ ),
861
957
  }))
862
958
  );
959
+ // booksWithAuthors[0].author?.name
863
960
  ```
864
961
 
865
962
  #### Many relation with `toArray()`
@@ -882,26 +979,31 @@ const { data: booksWithTags } = useLiveQuery((q) =>
882
979
  );
883
980
  ```
884
981
 
885
- #### Combining expand with includes
982
+ #### Includes on filed relations
886
983
 
887
- Use PocketBase's `expand` to auto-populate a related collection, then use includes to query from it:
984
+ Use PocketBase's `expand` to file a relation into its target collection, then use includes to query from it. Because the rows are already in the store, the include makes no request:
888
985
 
889
986
  ```typescript
890
987
  const authorsCollection = c('authors', { syncMode: 'on-demand' });
988
+ const tagsCollection = c('tags', { syncMode: 'on-demand' });
989
+ const bookTagsCollection = c('book_tags', { syncMode: 'on-demand' });
891
990
  const booksCollection = c('books', {
892
991
  syncMode: 'on-demand',
893
- expand: { author: authorsCollection }, // Auto-populates authorsCollection
992
+ relations: { author: authorsCollection }, // where expanded authors are filed
993
+ alwaysFetchRelations: ['author'],
894
994
  });
895
995
 
896
996
  const { data } = useLiveQuery((q) =>
897
997
  q.from({ b: booksCollection }).select(({ b }) => ({
898
998
  id: b.id,
899
999
  title: b.title,
900
- // Query from the expand-populated authorsCollection
901
- author: q.from({ a: authorsCollection })
902
- .where(({ a }) => eq(a.id, b.author))
903
- .select(({ a }) => ({ id: a.id, name: a.name }))
904
- .findOne(),
1000
+ // Reads from authorsCollection with no request; the rows are already filed
1001
+ author: materialize(
1002
+ q.from({ a: authorsCollection })
1003
+ .where(({ a }) => eq(a.id, b.author))
1004
+ .select(({ a }) => ({ id: a.id, name: a.name }))
1005
+ .findOne()
1006
+ ),
905
1007
  tags: toArray(
906
1008
  q.from({ bt: bookTagsCollection })
907
1009
  .where(({ bt }) => eq(bt.book, b.id))
@@ -944,12 +1046,11 @@ const books = c('books', {
944
1046
  omitOnInsert: ['created', 'updated'] as const
945
1047
  });
946
1048
 
947
- // ✅ Good - with auto-expand relations
1049
+ // ✅ Good - with always-fetched relations
948
1050
  const authors = c('authors', {});
949
1051
  const books = c('books', {
950
- expand: {
951
- author: authors
952
- }
1052
+ relations: { author: authors },
1053
+ alwaysFetchRelations: ['author'],
953
1054
  });
954
1055
  ```
955
1056
 
@@ -972,23 +1073,23 @@ export const { Provider, useStore } = createReactProvider({
972
1073
 
973
1074
  ### 2. Create Dependencies Before Dependents
974
1075
 
975
- When using expand collections, create the target collection first:
1076
+ When declaring `relations`, create the target collection first:
976
1077
 
977
1078
  ```typescript
978
1079
  // ✅ Good - authors exists before books references it
979
1080
  const c = createCollection<MySchema>(pb, queryClient);
980
1081
  const authors = c('authors', {});
981
1082
  const books = c('books', {
982
- expand: {
983
- author: authors // authors is already created
984
- }
1083
+ relations: { author: authors }, // authors is already created
1084
+ alwaysFetchRelations: ['author'],
985
1085
  });
986
1086
 
987
1087
  // ❌ Bad - can't reference what doesn't exist yet
988
1088
  const books = c('books', {
989
- expand: {
1089
+ relations: {
990
1090
  author: ??? // Where is authors?
991
- }
1091
+ },
1092
+ alwaysFetchRelations: ['author'],
992
1093
  });
993
1094
  ```
994
1095
 
@@ -1021,26 +1122,28 @@ if (!data?.length) return <div>No posts found</div>;
1021
1122
  return <PostsList posts={data} />;
1022
1123
  ```
1023
1124
 
1024
- ### 5. Use Expand for Performance
1125
+ ### 5. Choose Between alwaysFetchRelations and Joins
1025
1126
 
1026
- Use PocketBase's expand feature for better performance:
1127
+ `alwaysFetchRelations` costs one request but carries the related record once
1128
+ per parent row, on every fetch of the parent:
1027
1129
 
1028
1130
  ```typescript
1029
- // ✅ Fast - single query with server-side expand
1030
1131
  const c = createCollection<MySchema>(pb, queryClient);
1031
1132
  const authors = c('authors', {});
1032
1133
  const posts = c('posts', {
1033
- expand: {
1034
- author: authors // Auto-expand on every fetch
1035
- }
1134
+ relations: { author: authors },
1135
+ alwaysFetchRelations: ['author'], // expanded on every posts request
1036
1136
  });
1037
1137
 
1038
1138
  const { data } = useLiveQuery((q) => q.from({ posts }));
1039
-
1040
- // ⚠️ Slower - multiple queries + client-side join
1041
- // Only use TanStack DB joins for inner/right/full join behavior
1042
1139
  ```
1043
1140
 
1141
+ A join or a `materialize()` include costs one batched request per query
1142
+ (fetching each distinct related row once, however many parent rows reference
1143
+ it) and, once the rows are filed, subsequent queries make no request at all.
1144
+ Prefer `alwaysFetchRelations` when the parent is the only path by which those
1145
+ rows enter an on-demand collection; otherwise let the query load them.
1146
+
1044
1147
  ### 6. Configure QueryClient Defaults
1045
1148
 
1046
1149
  ```typescript
@@ -1119,12 +1222,13 @@ Contributions welcome! Please open an issue or PR.
1119
1222
  ### Development Setup
1120
1223
 
1121
1224
  **Prerequisites:**
1122
- - Node.js 18+
1225
+ - Node.js 20+
1123
1226
  - Git
1227
+ - The [PocketBase](https://pocketbase.io/docs/) binary on your `PATH` (the test server uses it)
1124
1228
 
1125
1229
  **Clone and Install:**
1126
1230
  ```bash
1127
- git clone https://github.com/yourusername/pbtsdb
1231
+ git clone https://github.com/nathanstitt/pbtsdb
1128
1232
  cd pbtsdb
1129
1233
  npm install
1130
1234
  ```