pbtsdb 0.8.0 → 0.9.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 +184 -100
- package/dist/{chunk-KMR6I4ZC.js → chunk-LT7BDI37.js} +315 -247
- package/dist/chunk-LT7BDI37.js.map +1 -0
- package/dist/core.d.ts +37 -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,87 @@ 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
|
-
|
|
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
|
+
|
|
434
|
+
Back-relations work the same way and go one step further. PocketBase names them
|
|
435
|
+
`<collection>_via_<field>`; fetching one files every child that references the
|
|
436
|
+
parent, and pbtsdb records that the child subset for that parent is complete,
|
|
437
|
+
so a child query filtered by the foreign key is served from the store:
|
|
393
438
|
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
439
|
+
```typescript
|
|
440
|
+
const comments = c('comments', { syncMode: 'on-demand' });
|
|
441
|
+
const cards = c('cards', { syncMode: 'on-demand', relations: { comments_via_card: comments } });
|
|
442
|
+
|
|
443
|
+
const { data } = useLiveQuery((q) =>
|
|
444
|
+
q.from({ card: cards.fetchRelations('comments_via_card') })
|
|
445
|
+
.where(({ card }) => eq(card.id, cardId))
|
|
446
|
+
.select(({ card }) => ({
|
|
447
|
+
...card,
|
|
448
|
+
comments: materialize(q.from({ cm: comments }).where(({ cm }) => eq(cm.card, card.id))),
|
|
449
|
+
}))
|
|
450
|
+
);
|
|
451
|
+
// one request; the comments include reads from the store
|
|
452
|
+
```
|
|
453
|
+
|
|
454
|
+
A subset stays marked while the child collection is subscribed (the parent holds
|
|
455
|
+
it live) and is forgotten when a child row is pruned from the store, when the
|
|
456
|
+
child's realtime subscription stops, or when the collection is cleaned up.
|
|
457
|
+
PocketBase caps a back-relation expand at 1000 records, and a capped expand is
|
|
458
|
+
never treated as complete.
|
|
403
459
|
|
|
404
460
|
#### Collection Options Passthrough
|
|
405
461
|
|
|
@@ -581,6 +637,10 @@ both transports:
|
|
|
581
637
|
Setting the matching header on REST requests remains the application's job, via
|
|
582
638
|
`pb.beforeSend`. Requires `pocketbase >= 0.22.0`.
|
|
583
639
|
|
|
640
|
+
An `expand` you add here is yours: pbtsdb strips only the paths it requested
|
|
641
|
+
through `alwaysFetchRelations` and `fetchRelations()`, so records expanded by
|
|
642
|
+
this option stay on the echoed rows, untyped.
|
|
643
|
+
|
|
584
644
|
### Utility Functions
|
|
585
645
|
|
|
586
646
|
#### newRecordId()
|
|
@@ -611,6 +671,7 @@ pbtsdb re-exports commonly used TanStack DB utilities so you don't need to depen
|
|
|
611
671
|
```typescript
|
|
612
672
|
import {
|
|
613
673
|
// Includes helpers
|
|
674
|
+
materialize,
|
|
614
675
|
toArray,
|
|
615
676
|
createEffect,
|
|
616
677
|
|
|
@@ -671,7 +732,7 @@ export function TaskBoard({ userId }: { userId: string }) {
|
|
|
671
732
|
```typescript
|
|
672
733
|
// ProductCatalog.tsx
|
|
673
734
|
import { useLiveQuery } from '@tanstack/react-db';
|
|
674
|
-
import { and,
|
|
735
|
+
import { and, eq, lte } from '@tanstack/db';
|
|
675
736
|
import { useStore } from './app';
|
|
676
737
|
|
|
677
738
|
export function ProductCatalog() {
|
|
@@ -683,12 +744,12 @@ export function ProductCatalog() {
|
|
|
683
744
|
const { data: filteredProducts } = useLiveQuery((q) => {
|
|
684
745
|
let query = q.from({ products })
|
|
685
746
|
.where(({ products }) => and(
|
|
686
|
-
products.in_stock
|
|
747
|
+
eq(products.in_stock, true),
|
|
687
748
|
lte(products.price, maxPrice)
|
|
688
749
|
));
|
|
689
750
|
|
|
690
751
|
if (category) {
|
|
691
|
-
query = query.where(({ products }) => products.category
|
|
752
|
+
query = query.where(({ products }) => eq(products.category, category));
|
|
692
753
|
}
|
|
693
754
|
|
|
694
755
|
return query.orderBy(({ products }) => products.rating, 'desc');
|
|
@@ -718,13 +779,20 @@ export function ProductCatalog() {
|
|
|
718
779
|
import { useLiveQuery } from '@tanstack/react-db';
|
|
719
780
|
import { eq } from '@tanstack/db';
|
|
720
781
|
import { useStore } from './app';
|
|
721
|
-
import { newRecordId } from 'pbtsdb';
|
|
782
|
+
import { materialize, newRecordId } from 'pbtsdb';
|
|
722
783
|
|
|
723
784
|
export function SocialFeed({ currentUserId }: { currentUserId: string }) {
|
|
724
|
-
const [posts, likes] = useStore('posts', 'likes');
|
|
785
|
+
const [posts, likes, users] = useStore('posts', 'likes', 'users');
|
|
725
786
|
|
|
726
787
|
const { data: feedPosts } = useLiveQuery((q) =>
|
|
727
|
-
q.from({ posts })
|
|
788
|
+
q.from({ posts })
|
|
789
|
+
.orderBy(({ posts }) => posts.created, 'desc')
|
|
790
|
+
.select(({ posts }) => ({
|
|
791
|
+
...posts,
|
|
792
|
+
author: materialize(
|
|
793
|
+
q.from({ u: users }).where(({ u }) => eq(u.id, posts.author)).findOne()
|
|
794
|
+
),
|
|
795
|
+
}))
|
|
728
796
|
);
|
|
729
797
|
|
|
730
798
|
const { data: userLikes } = useLiveQuery((q) =>
|
|
@@ -746,7 +814,7 @@ export function SocialFeed({ currentUserId }: { currentUserId: string }) {
|
|
|
746
814
|
<div>
|
|
747
815
|
{feedPosts?.map(post => (
|
|
748
816
|
<div key={post.id}>
|
|
749
|
-
<strong>{post.
|
|
817
|
+
<strong>{post.author?.username}</strong>
|
|
750
818
|
<p>{post.content}</p>
|
|
751
819
|
<button onClick={() => handleLike(post.id)}>
|
|
752
820
|
{likedPostIds.has(post.id) ? '❤️' : '🤍'} {post.likes_count}
|
|
@@ -828,8 +896,9 @@ export function CreateBookForm() {
|
|
|
828
896
|
|
|
829
897
|
if (tx.state === 'completed') setTitle('');
|
|
830
898
|
else setError('Failed to create book');
|
|
831
|
-
} catch (err
|
|
832
|
-
|
|
899
|
+
} catch (err) {
|
|
900
|
+
const failure = err as { data?: Record<string, unknown>; message?: string };
|
|
901
|
+
setError(failure.data ? Object.values(failure.data).join(', ') : failure.message ?? 'Failed');
|
|
833
902
|
}
|
|
834
903
|
};
|
|
835
904
|
|
|
@@ -893,20 +962,27 @@ TanStack DB 0.6.0 introduces **includes** — nested subqueries within `select()
|
|
|
893
962
|
|
|
894
963
|
#### Single relation with `findOne()`
|
|
895
964
|
|
|
965
|
+
Wrap a `findOne()` subquery in `materialize()` to get a plain `T | undefined`
|
|
966
|
+
value that updates when the child changes. Without it, the include is a live
|
|
967
|
+
sub-collection rather than a value.
|
|
968
|
+
|
|
896
969
|
```typescript
|
|
897
970
|
import { useLiveQuery, eq } from '@tanstack/react-db';
|
|
971
|
+
import { materialize } from 'pbtsdb';
|
|
898
972
|
|
|
899
973
|
const { data: booksWithAuthors } = useLiveQuery((q) =>
|
|
900
974
|
q.from({ b: booksCollection }).select(({ b }) => ({
|
|
901
975
|
id: b.id,
|
|
902
976
|
title: b.title,
|
|
903
|
-
author:
|
|
904
|
-
.from({ a: authorsCollection })
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
977
|
+
author: materialize(
|
|
978
|
+
q.from({ a: authorsCollection })
|
|
979
|
+
.where(({ a }) => eq(a.id, b.author))
|
|
980
|
+
.select(({ a }) => ({ id: a.id, name: a.name }))
|
|
981
|
+
.findOne()
|
|
982
|
+
),
|
|
908
983
|
}))
|
|
909
984
|
);
|
|
985
|
+
// booksWithAuthors[0].author?.name
|
|
910
986
|
```
|
|
911
987
|
|
|
912
988
|
#### Many relation with `toArray()`
|
|
@@ -929,27 +1005,31 @@ const { data: booksWithTags } = useLiveQuery((q) =>
|
|
|
929
1005
|
);
|
|
930
1006
|
```
|
|
931
1007
|
|
|
932
|
-
####
|
|
1008
|
+
#### Includes on filed relations
|
|
933
1009
|
|
|
934
|
-
Use PocketBase's `expand` to
|
|
1010
|
+
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
1011
|
|
|
936
1012
|
```typescript
|
|
937
1013
|
const authorsCollection = c('authors', { syncMode: 'on-demand' });
|
|
1014
|
+
const tagsCollection = c('tags', { syncMode: 'on-demand' });
|
|
1015
|
+
const bookTagsCollection = c('book_tags', { syncMode: 'on-demand' });
|
|
938
1016
|
const booksCollection = c('books', {
|
|
939
1017
|
syncMode: 'on-demand',
|
|
940
|
-
relations: { author: authorsCollection }, //
|
|
941
|
-
|
|
1018
|
+
relations: { author: authorsCollection }, // where expanded authors are filed
|
|
1019
|
+
alwaysFetchRelations: ['author'],
|
|
942
1020
|
});
|
|
943
1021
|
|
|
944
1022
|
const { data } = useLiveQuery((q) =>
|
|
945
1023
|
q.from({ b: booksCollection }).select(({ b }) => ({
|
|
946
1024
|
id: b.id,
|
|
947
1025
|
title: b.title,
|
|
948
|
-
//
|
|
949
|
-
author:
|
|
950
|
-
.
|
|
951
|
-
|
|
952
|
-
|
|
1026
|
+
// Reads from authorsCollection with no request; the rows are already filed
|
|
1027
|
+
author: materialize(
|
|
1028
|
+
q.from({ a: authorsCollection })
|
|
1029
|
+
.where(({ a }) => eq(a.id, b.author))
|
|
1030
|
+
.select(({ a }) => ({ id: a.id, name: a.name }))
|
|
1031
|
+
.findOne()
|
|
1032
|
+
),
|
|
953
1033
|
tags: toArray(
|
|
954
1034
|
q.from({ bt: bookTagsCollection })
|
|
955
1035
|
.where(({ bt }) => eq(bt.book, b.id))
|
|
@@ -992,11 +1072,11 @@ const books = c('books', {
|
|
|
992
1072
|
omitOnInsert: ['created', 'updated'] as const
|
|
993
1073
|
});
|
|
994
1074
|
|
|
995
|
-
// ✅ Good - with always-
|
|
1075
|
+
// ✅ Good - with always-fetched relations
|
|
996
1076
|
const authors = c('authors', {});
|
|
997
1077
|
const books = c('books', {
|
|
998
1078
|
relations: { author: authors },
|
|
999
|
-
|
|
1079
|
+
alwaysFetchRelations: ['author'],
|
|
1000
1080
|
});
|
|
1001
1081
|
```
|
|
1002
1082
|
|
|
@@ -1019,7 +1099,7 @@ export const { Provider, useStore } = createReactProvider({
|
|
|
1019
1099
|
|
|
1020
1100
|
### 2. Create Dependencies Before Dependents
|
|
1021
1101
|
|
|
1022
|
-
When
|
|
1102
|
+
When declaring `relations`, create the target collection first:
|
|
1023
1103
|
|
|
1024
1104
|
```typescript
|
|
1025
1105
|
// ✅ Good - authors exists before books references it
|
|
@@ -1027,7 +1107,7 @@ const c = createCollection<MySchema>(pb, queryClient);
|
|
|
1027
1107
|
const authors = c('authors', {});
|
|
1028
1108
|
const books = c('books', {
|
|
1029
1109
|
relations: { author: authors }, // authors is already created
|
|
1030
|
-
|
|
1110
|
+
alwaysFetchRelations: ['author'],
|
|
1031
1111
|
});
|
|
1032
1112
|
|
|
1033
1113
|
// ❌ Bad - can't reference what doesn't exist yet
|
|
@@ -1035,7 +1115,7 @@ const books = c('books', {
|
|
|
1035
1115
|
relations: {
|
|
1036
1116
|
author: ??? // Where is authors?
|
|
1037
1117
|
},
|
|
1038
|
-
|
|
1118
|
+
alwaysFetchRelations: ['author'],
|
|
1039
1119
|
});
|
|
1040
1120
|
```
|
|
1041
1121
|
|
|
@@ -1068,25 +1148,28 @@ if (!data?.length) return <div>No posts found</div>;
|
|
|
1068
1148
|
return <PostsList posts={data} />;
|
|
1069
1149
|
```
|
|
1070
1150
|
|
|
1071
|
-
### 5.
|
|
1151
|
+
### 5. Choose Between alwaysFetchRelations and Joins
|
|
1072
1152
|
|
|
1073
|
-
|
|
1153
|
+
`alwaysFetchRelations` costs one request but carries the related record once
|
|
1154
|
+
per parent row, on every fetch of the parent:
|
|
1074
1155
|
|
|
1075
1156
|
```typescript
|
|
1076
|
-
// ✅ Fast - single query with server-side expand
|
|
1077
1157
|
const c = createCollection<MySchema>(pb, queryClient);
|
|
1078
1158
|
const authors = c('authors', {});
|
|
1079
1159
|
const posts = c('posts', {
|
|
1080
1160
|
relations: { author: authors },
|
|
1081
|
-
|
|
1161
|
+
alwaysFetchRelations: ['author'], // expanded on every posts request
|
|
1082
1162
|
});
|
|
1083
1163
|
|
|
1084
1164
|
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
1165
|
```
|
|
1089
1166
|
|
|
1167
|
+
A join or a `materialize()` include costs one batched request per query
|
|
1168
|
+
(fetching each distinct related row once, however many parent rows reference
|
|
1169
|
+
it) and, once the rows are filed, subsequent queries make no request at all.
|
|
1170
|
+
Prefer `alwaysFetchRelations` when the parent is the only path by which those
|
|
1171
|
+
rows enter an on-demand collection; otherwise let the query load them.
|
|
1172
|
+
|
|
1090
1173
|
### 6. Configure QueryClient Defaults
|
|
1091
1174
|
|
|
1092
1175
|
```typescript
|
|
@@ -1165,12 +1248,13 @@ Contributions welcome! Please open an issue or PR.
|
|
|
1165
1248
|
### Development Setup
|
|
1166
1249
|
|
|
1167
1250
|
**Prerequisites:**
|
|
1168
|
-
- Node.js
|
|
1251
|
+
- Node.js 20+
|
|
1169
1252
|
- Git
|
|
1253
|
+
- The [PocketBase](https://pocketbase.io/docs/) binary on your `PATH` (the test server uses it)
|
|
1170
1254
|
|
|
1171
1255
|
**Clone and Install:**
|
|
1172
1256
|
```bash
|
|
1173
|
-
git clone https://github.com/
|
|
1257
|
+
git clone https://github.com/nathanstitt/pbtsdb
|
|
1174
1258
|
cd pbtsdb
|
|
1175
1259
|
npm install
|
|
1176
1260
|
```
|