pbtsdb 0.8.0 → 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
 
@@ -129,12 +128,12 @@ export const { Provider, useStore } = createReactProvider({
129
128
  posts: c('posts', {
130
129
  omitOnInsert: ['created', 'updated'] as const,
131
130
  relations: { author: users },
132
- alwaysExpand: ['author'],
131
+ alwaysFetchRelations: ['author'],
133
132
  }),
134
133
  comments: c('comments', {
135
134
  omitOnInsert: ['created', 'updated'] as const,
136
135
  relations: { author: users },
137
- alwaysExpand: ['author'],
136
+ alwaysFetchRelations: ['author'],
138
137
  }),
139
138
  });
140
139
 
@@ -154,14 +153,22 @@ export function App() {
154
153
  ```typescript
155
154
  // BlogDashboard.tsx
156
155
  import { useLiveQuery } from '@tanstack/react-db';
156
+ import { eq } from '@tanstack/db';
157
+ import { materialize } from 'pbtsdb';
157
158
  import { useStore } from './app';
158
159
 
159
160
  export function BlogDashboard() {
160
- const [posts] = useStore('posts');
161
+ const [posts, users] = useStore('posts', 'users');
161
162
 
162
163
  const { data: allPosts, isLoading } = useLiveQuery((q) =>
163
164
  q.from({ posts })
164
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
+ }))
165
172
  );
166
173
 
167
174
  if (isLoading) return <div>Loading posts...</div>;
@@ -173,8 +180,8 @@ export function BlogDashboard() {
173
180
  <article key={post.id}>
174
181
  <h2>{post.title}</h2>
175
182
  <p>{post.content}</p>
176
- {/* Expanded author is fully typed! */}
177
- <small>By {post.expand?.author?.username}</small>
183
+ {/* Author was filed into the users collection by alwaysFetchRelations */}
184
+ <small>By {post.author?.username}</small>
178
185
  </article>
179
186
  ))}
180
187
  </div>
@@ -189,16 +196,22 @@ export function BlogDashboard() {
189
196
  import { useLiveQuery } from '@tanstack/react-db';
190
197
  import { eq } from '@tanstack/db';
191
198
  import { useStore } from './app';
192
- import { newRecordId } from 'pbtsdb';
199
+ import { materialize, newRecordId } from 'pbtsdb';
193
200
 
194
201
  export function PostWithComments({ postId }: { postId: string }) {
195
- const [comments, posts] = useStore('comments', 'posts');
202
+ const [comments, posts, users] = useStore('comments', 'posts', 'users');
196
203
 
197
204
  // Real-time comments for this post
198
205
  const { data: postComments } = useLiveQuery((q) =>
199
206
  q.from({ comments })
200
207
  .where(({ comments }) => eq(comments.post, postId))
201
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
+ }))
202
215
  );
203
216
 
204
217
  const handleAddComment = (text: string, authorId: string) => {
@@ -216,7 +229,7 @@ export function PostWithComments({ postId }: { postId: string }) {
216
229
  <h3>Comments ({postComments?.length || 0})</h3>
217
230
  {postComments?.map(comment => (
218
231
  <div key={comment.id}>
219
- <strong>{comment.expand?.author?.username}:</strong>
232
+ <strong>{comment.author?.username}:</strong>
220
233
  <p>{comment.text}</p>
221
234
  </div>
222
235
  ))}
@@ -230,7 +243,7 @@ export function PostWithComments({ postId }: { postId: string }) {
230
243
  - ✅ Type-safe queries
231
244
  - ✅ Automatic real-time updates
232
245
  - ✅ Optimistic mutations
233
- - ✅ Expanded relations
246
+ - ✅ Related data read from its own collection, no embedded copies
234
247
 
235
248
  ## Core Concepts
236
249
 
@@ -273,6 +286,22 @@ const { data } = useLiveQuery((q) =>
273
286
  - **Shared:** Multiple components using the same collection share one subscription
274
287
  - **No manual control needed:** The collection handles all subscription management internally
275
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
+
276
305
  ### Mutations and Refetch
277
306
 
278
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.
@@ -325,13 +354,14 @@ const collection = c(collectionName: string, options?: CreateCollectionOptions);
325
354
  - `options` - Optional configuration
326
355
 
327
356
  **Options:**
328
- - `relations?: Record<string, Collection>` - Collections that receive expanded records for each relation; declares what `alwaysExpand` and `collection.expand()` may name
329
- - `alwaysExpand?: readonly string[]` - Expand paths applied on every fetch (e.g. `['author', 'book.author']`)
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
330
359
  - `omitOnInsert?: readonly string[]` - Fields to make optional during insert (e.g., `['created', 'updated'] as const`)
331
360
  - `syncMode?: 'eager' | 'on-demand'` - Data fetching strategy (default: `'eager'`)
332
361
  - `onInsert?: InsertMutationFn | false` - Custom insert handler or `false` to disable
333
362
  - `onUpdate?: UpdateMutationFn | false` - Custom update handler or `false` to disable
334
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))
335
365
  - `ignoreAutoCancellation?: boolean` - Ignore PocketBase auto-cancellation errors (default: `true`)
336
366
  - `collectionOptions?: object` - Additional TanStack DB collection options passed through directly (see [Collection Options Passthrough](#collection-options-passthrough))
337
367
 
@@ -345,61 +375,61 @@ const c = createCollection<MySchema>(pb, queryClient);
345
375
  const booksCollection = c('books', {});
346
376
  ```
347
377
 
348
- With always-expanded 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
+
349
384
  ```typescript
350
385
  const c = createCollection<MySchema>(pb, queryClient);
351
- const authorsCollection = c('authors', {});
352
- const booksCollection = c('books', {
353
- relations: { author: authorsCollection }, // where expanded authors are upserted
354
- alwaysExpand: ['author'], // expanded on every fetch
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
355
391
  });
356
-
357
- const { data } = useLiveQuery((q) => q.from({ books: booksCollection }));
358
- // data[0].expand?.author is typed and populated
359
392
  ```
360
393
 
361
- #### Per-query expand
362
-
363
- Declare relations once, then ask for expansion per query with `collection.expand()`.
364
- A view shares the collection's store, realtime subscription, and mutations; only
365
- its fetches add the `expand` parameter.
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)`:
366
398
 
367
399
  ```typescript
368
- const tagsCollection = c('tags', {});
369
- const booksCollection = c('books', {
370
- relations: { author: authorsCollection, tags: tagsCollection },
371
- });
400
+ import { eq } from '@tanstack/db';
401
+ import { materialize } from 'pbtsdb';
372
402
 
373
- function BookList() {
374
- const [books] = useStore('books');
375
- const { data } = useLiveQuery((q) =>
376
- q.from({ books: books.expand('tags') })
377
- .where(({ books }) => eq(books.genre, 'Fiction'))
378
- );
379
- // data[0].expand?.tags is Tags[] | undefined; data[0].expand?.author is a type error
380
- }
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
381
412
  ```
382
413
 
383
- Paths can be nested through a target collection's own `relations`:
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:
384
424
 
385
425
  ```typescript
386
- const metadata = c('book_metadata', { relations: { book: booksCollection } });
387
- const { data } = useLiveQuery((q) => q.from({ m: metadata.expand('book.author') }));
388
- // data[0].expand?.book?.expand?.author?.name
426
+ const { data } = useLiveQuery((q) => q.from({ b: books.fetchRelations('tags') }));
427
+ // tags referenced by these books are now in the tags collection
389
428
  ```
390
429
 
391
- Rows fetched through a view keep their `expand` data in the shared store, and the
392
- realtime subscription requests every relation in use, so echoes keep it populated.
393
-
394
- Relation targets stay live too. While a query expands into `tags` (or through
395
- it into `colors`), those collections keep their realtime subscriptions, and a
396
- change to a tag or a color updates the embedded copy on affected rows in
397
- place, so `book.expand?.tags?.[0].name` refreshes without a write to the book.
398
- While an optimistic mutation is pending on the parent row, TanStack DB shows
399
- the frozen optimistic snapshot, so a patched `expand` becomes visible once
400
- that mutation settles. Deleting a related record clears the reference and the
401
- copy, matching what PocketBase does server-side. Targets are released when
402
- the last query on the parent unmounts.
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.
403
433
 
404
434
  #### Collection Options Passthrough
405
435
 
@@ -581,6 +611,10 @@ both transports:
581
611
  Setting the matching header on REST requests remains the application's job, via
582
612
  `pb.beforeSend`. Requires `pocketbase >= 0.22.0`.
583
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
+
584
618
  ### Utility Functions
585
619
 
586
620
  #### newRecordId()
@@ -611,6 +645,7 @@ pbtsdb re-exports commonly used TanStack DB utilities so you don't need to depen
611
645
  ```typescript
612
646
  import {
613
647
  // Includes helpers
648
+ materialize,
614
649
  toArray,
615
650
  createEffect,
616
651
 
@@ -671,7 +706,7 @@ export function TaskBoard({ userId }: { userId: string }) {
671
706
  ```typescript
672
707
  // ProductCatalog.tsx
673
708
  import { useLiveQuery } from '@tanstack/react-db';
674
- import { and, gte, lte } from '@tanstack/db';
709
+ import { and, eq, lte } from '@tanstack/db';
675
710
  import { useStore } from './app';
676
711
 
677
712
  export function ProductCatalog() {
@@ -683,12 +718,12 @@ export function ProductCatalog() {
683
718
  const { data: filteredProducts } = useLiveQuery((q) => {
684
719
  let query = q.from({ products })
685
720
  .where(({ products }) => and(
686
- products.in_stock === true,
721
+ eq(products.in_stock, true),
687
722
  lte(products.price, maxPrice)
688
723
  ));
689
724
 
690
725
  if (category) {
691
- query = query.where(({ products }) => products.category === category);
726
+ query = query.where(({ products }) => eq(products.category, category));
692
727
  }
693
728
 
694
729
  return query.orderBy(({ products }) => products.rating, 'desc');
@@ -718,13 +753,20 @@ export function ProductCatalog() {
718
753
  import { useLiveQuery } from '@tanstack/react-db';
719
754
  import { eq } from '@tanstack/db';
720
755
  import { useStore } from './app';
721
- import { newRecordId } from 'pbtsdb';
756
+ import { materialize, newRecordId } from 'pbtsdb';
722
757
 
723
758
  export function SocialFeed({ currentUserId }: { currentUserId: string }) {
724
- const [posts, likes] = useStore('posts', 'likes');
759
+ const [posts, likes, users] = useStore('posts', 'likes', 'users');
725
760
 
726
761
  const { data: feedPosts } = useLiveQuery((q) =>
727
- 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
+ }))
728
770
  );
729
771
 
730
772
  const { data: userLikes } = useLiveQuery((q) =>
@@ -746,7 +788,7 @@ export function SocialFeed({ currentUserId }: { currentUserId: string }) {
746
788
  <div>
747
789
  {feedPosts?.map(post => (
748
790
  <div key={post.id}>
749
- <strong>{post.expand?.author?.username}</strong>
791
+ <strong>{post.author?.username}</strong>
750
792
  <p>{post.content}</p>
751
793
  <button onClick={() => handleLike(post.id)}>
752
794
  {likedPostIds.has(post.id) ? '❤️' : '🤍'} {post.likes_count}
@@ -828,8 +870,9 @@ export function CreateBookForm() {
828
870
 
829
871
  if (tx.state === 'completed') setTitle('');
830
872
  else setError('Failed to create book');
831
- } catch (err: any) {
832
- 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');
833
876
  }
834
877
  };
835
878
 
@@ -893,20 +936,27 @@ TanStack DB 0.6.0 introduces **includes** — nested subqueries within `select()
893
936
 
894
937
  #### Single relation with `findOne()`
895
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
+
896
943
  ```typescript
897
944
  import { useLiveQuery, eq } from '@tanstack/react-db';
945
+ import { materialize } from 'pbtsdb';
898
946
 
899
947
  const { data: booksWithAuthors } = useLiveQuery((q) =>
900
948
  q.from({ b: booksCollection }).select(({ b }) => ({
901
949
  id: b.id,
902
950
  title: b.title,
903
- author: q
904
- .from({ a: authorsCollection })
905
- .where(({ a }) => eq(a.id, b.author))
906
- .select(({ a }) => ({ id: a.id, name: a.name }))
907
- .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
+ ),
908
957
  }))
909
958
  );
959
+ // booksWithAuthors[0].author?.name
910
960
  ```
911
961
 
912
962
  #### Many relation with `toArray()`
@@ -929,27 +979,31 @@ const { data: booksWithTags } = useLiveQuery((q) =>
929
979
  );
930
980
  ```
931
981
 
932
- #### Combining expand with includes
982
+ #### Includes on filed relations
933
983
 
934
- 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:
935
985
 
936
986
  ```typescript
937
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' });
938
990
  const booksCollection = c('books', {
939
991
  syncMode: 'on-demand',
940
- relations: { author: authorsCollection }, // Auto-populates authorsCollection
941
- alwaysExpand: ['author'],
992
+ relations: { author: authorsCollection }, // where expanded authors are filed
993
+ alwaysFetchRelations: ['author'],
942
994
  });
943
995
 
944
996
  const { data } = useLiveQuery((q) =>
945
997
  q.from({ b: booksCollection }).select(({ b }) => ({
946
998
  id: b.id,
947
999
  title: b.title,
948
- // Query from the expand-populated authorsCollection
949
- author: q.from({ a: authorsCollection })
950
- .where(({ a }) => eq(a.id, b.author))
951
- .select(({ a }) => ({ id: a.id, name: a.name }))
952
- .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
+ ),
953
1007
  tags: toArray(
954
1008
  q.from({ bt: bookTagsCollection })
955
1009
  .where(({ bt }) => eq(bt.book, b.id))
@@ -992,11 +1046,11 @@ const books = c('books', {
992
1046
  omitOnInsert: ['created', 'updated'] as const
993
1047
  });
994
1048
 
995
- // ✅ Good - with always-expanded relations
1049
+ // ✅ Good - with always-fetched relations
996
1050
  const authors = c('authors', {});
997
1051
  const books = c('books', {
998
1052
  relations: { author: authors },
999
- alwaysExpand: ['author'],
1053
+ alwaysFetchRelations: ['author'],
1000
1054
  });
1001
1055
  ```
1002
1056
 
@@ -1019,7 +1073,7 @@ export const { Provider, useStore } = createReactProvider({
1019
1073
 
1020
1074
  ### 2. Create Dependencies Before Dependents
1021
1075
 
1022
- When using expand collections, create the target collection first:
1076
+ When declaring `relations`, create the target collection first:
1023
1077
 
1024
1078
  ```typescript
1025
1079
  // ✅ Good - authors exists before books references it
@@ -1027,7 +1081,7 @@ const c = createCollection<MySchema>(pb, queryClient);
1027
1081
  const authors = c('authors', {});
1028
1082
  const books = c('books', {
1029
1083
  relations: { author: authors }, // authors is already created
1030
- alwaysExpand: ['author'],
1084
+ alwaysFetchRelations: ['author'],
1031
1085
  });
1032
1086
 
1033
1087
  // ❌ Bad - can't reference what doesn't exist yet
@@ -1035,7 +1089,7 @@ const books = c('books', {
1035
1089
  relations: {
1036
1090
  author: ??? // Where is authors?
1037
1091
  },
1038
- alwaysExpand: ['author'],
1092
+ alwaysFetchRelations: ['author'],
1039
1093
  });
1040
1094
  ```
1041
1095
 
@@ -1068,25 +1122,28 @@ if (!data?.length) return <div>No posts found</div>;
1068
1122
  return <PostsList posts={data} />;
1069
1123
  ```
1070
1124
 
1071
- ### 5. Use Expand for Performance
1125
+ ### 5. Choose Between alwaysFetchRelations and Joins
1072
1126
 
1073
- 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:
1074
1129
 
1075
1130
  ```typescript
1076
- // ✅ Fast - single query with server-side expand
1077
1131
  const c = createCollection<MySchema>(pb, queryClient);
1078
1132
  const authors = c('authors', {});
1079
1133
  const posts = c('posts', {
1080
1134
  relations: { author: authors },
1081
- alwaysExpand: ['author'], // Auto-expand on every fetch
1135
+ alwaysFetchRelations: ['author'], // expanded on every posts request
1082
1136
  });
1083
1137
 
1084
1138
  const { data } = useLiveQuery((q) => q.from({ posts }));
1085
-
1086
- // ⚠️ Slower - multiple queries + client-side join
1087
- // Only use TanStack DB joins for inner/right/full join behavior
1088
1139
  ```
1089
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
+
1090
1147
  ### 6. Configure QueryClient Defaults
1091
1148
 
1092
1149
  ```typescript
@@ -1165,12 +1222,13 @@ Contributions welcome! Please open an issue or PR.
1165
1222
  ### Development Setup
1166
1223
 
1167
1224
  **Prerequisites:**
1168
- - Node.js 18+
1225
+ - Node.js 20+
1169
1226
  - Git
1227
+ - The [PocketBase](https://pocketbase.io/docs/) binary on your `PATH` (the test server uses it)
1170
1228
 
1171
1229
  **Clone and Install:**
1172
1230
  ```bash
1173
- git clone https://github.com/yourusername/pbtsdb
1231
+ git clone https://github.com/nathanstitt/pbtsdb
1174
1232
  cd pbtsdb
1175
1233
  npm install
1176
1234
  ```