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 +159 -101
- package/dist/{chunk-KMR6I4ZC.js → chunk-YWTU6QXM.js} +147 -240
- package/dist/chunk-YWTU6QXM.js.map +1 -0
- package/dist/core.d.ts +33 -96
- package/dist/core.js +1 -1
- package/dist/index.d.ts +18 -11
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/llms.txt +33 -13
- package/package.json +1 -1
- package/dist/chunk-KMR6I4ZC.js.map +0 -1
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
|
|
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/
|
|
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.
|
|
45
|
-
-
|
|
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
|
|
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
|
-
|
|
131
|
+
alwaysFetchRelations: ['author'],
|
|
133
132
|
}),
|
|
134
133
|
comments: c('comments', {
|
|
135
134
|
omitOnInsert: ['created', 'updated'] as const,
|
|
136
135
|
relations: { author: users },
|
|
137
|
-
|
|
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
|
-
{/*
|
|
177
|
-
<small>By {post.
|
|
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.
|
|
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
|
-
- ✅
|
|
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 `
|
|
329
|
-
- `
|
|
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
|
-
|
|
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
|
|
352
|
-
const
|
|
353
|
-
|
|
354
|
-
|
|
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
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
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
|
-
|
|
369
|
-
|
|
370
|
-
relations: { author: authorsCollection, tags: tagsCollection },
|
|
371
|
-
});
|
|
400
|
+
import { eq } from '@tanstack/db';
|
|
401
|
+
import { materialize } from 'pbtsdb';
|
|
372
402
|
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
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
|
-
|
|
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
|
|
387
|
-
|
|
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
|
-
|
|
392
|
-
|
|
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,
|
|
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
|
|
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
|
|
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 })
|
|
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.
|
|
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
|
|
832
|
-
|
|
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:
|
|
904
|
-
.from({ a: authorsCollection })
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
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
|
-
####
|
|
982
|
+
#### Includes on filed relations
|
|
933
983
|
|
|
934
|
-
Use PocketBase's `expand` to
|
|
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 }, //
|
|
941
|
-
|
|
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
|
-
//
|
|
949
|
-
author:
|
|
950
|
-
.
|
|
951
|
-
|
|
952
|
-
|
|
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-
|
|
1049
|
+
// ✅ Good - with always-fetched relations
|
|
996
1050
|
const authors = c('authors', {});
|
|
997
1051
|
const books = c('books', {
|
|
998
1052
|
relations: { author: authors },
|
|
999
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
1125
|
+
### 5. Choose Between alwaysFetchRelations and Joins
|
|
1072
1126
|
|
|
1073
|
-
|
|
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
|
-
|
|
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
|
|
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/
|
|
1231
|
+
git clone https://github.com/nathanstitt/pbtsdb
|
|
1174
1232
|
cd pbtsdb
|
|
1175
1233
|
npm install
|
|
1176
1234
|
```
|