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 +184 -80
- package/dist/chunk-YWTU6QXM.js +857 -0
- package/dist/chunk-YWTU6QXM.js.map +1 -0
- package/dist/core.d.ts +105 -90
- package/dist/core.js +1 -1
- package/dist/index.d.ts +19 -13
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/llms.txt +37 -13
- package/package.json +4 -4
- package/dist/chunk-P3KUBRZK.js +0 -517
- package/dist/chunk-P3KUBRZK.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
|
|
|
@@ -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
|
-
{/*
|
|
172
|
-
<small>By {post.
|
|
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.
|
|
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
|
-
- ✅
|
|
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
|
-
- `
|
|
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
|
-
|
|
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
|
|
346
|
-
const
|
|
347
|
-
|
|
348
|
-
|
|
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
|
-
|
|
353
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
|
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
|
|
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 })
|
|
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.
|
|
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
|
|
785
|
-
|
|
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:
|
|
857
|
-
.from({ a: authorsCollection })
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
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
|
-
####
|
|
982
|
+
#### Includes on filed relations
|
|
886
983
|
|
|
887
|
-
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:
|
|
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
|
-
|
|
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
|
-
//
|
|
901
|
-
author:
|
|
902
|
-
.
|
|
903
|
-
|
|
904
|
-
|
|
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
|
|
1049
|
+
// ✅ Good - with always-fetched relations
|
|
948
1050
|
const authors = c('authors', {});
|
|
949
1051
|
const books = c('books', {
|
|
950
|
-
|
|
951
|
-
|
|
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
|
|
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
|
-
|
|
983
|
-
|
|
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
|
-
|
|
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.
|
|
1125
|
+
### 5. Choose Between alwaysFetchRelations and Joins
|
|
1025
1126
|
|
|
1026
|
-
|
|
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
|
-
|
|
1034
|
-
|
|
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
|
|
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/
|
|
1231
|
+
git clone https://github.com/nathanstitt/pbtsdb
|
|
1128
1232
|
cd pbtsdb
|
|
1129
1233
|
npm install
|
|
1130
1234
|
```
|