Initial commit
This commit is contained in:
143
.claude/skills/pocketbase-best-practices/rules/query-expand.md
Normal file
143
.claude/skills/pocketbase-best-practices/rules/query-expand.md
Normal file
@@ -0,0 +1,143 @@
|
||||
---
|
||||
title: Expand Relations Efficiently
|
||||
impact: HIGH
|
||||
impactDescription: Eliminates N+1 queries, reduces API calls by 90%+
|
||||
tags: query, relations, expand, joins, performance
|
||||
---
|
||||
|
||||
## Expand Relations Efficiently
|
||||
|
||||
Use the `expand` parameter to fetch related records in a single request. This eliminates N+1 query problems and dramatically reduces API calls.
|
||||
|
||||
**Incorrect (N+1 queries):**
|
||||
|
||||
```javascript
|
||||
// Fetching posts then authors separately - N+1 problem
|
||||
async function getPostsWithAuthors() {
|
||||
const posts = await pb.collection('posts').getList(1, 20);
|
||||
|
||||
// N additional requests for N posts!
|
||||
for (const post of posts.items) {
|
||||
post.authorData = await pb.collection('users').getOne(post.author);
|
||||
}
|
||||
|
||||
return posts;
|
||||
}
|
||||
// 21 API calls for 20 posts!
|
||||
|
||||
// Even worse with multiple relations
|
||||
async function getPostsWithAll() {
|
||||
const posts = await pb.collection('posts').getList(1, 20);
|
||||
|
||||
for (const post of posts.items) {
|
||||
post.author = await pb.collection('users').getOne(post.author);
|
||||
post.category = await pb.collection('categories').getOne(post.category);
|
||||
post.tags = await Promise.all(
|
||||
post.tags.map(id => pb.collection('tags').getOne(id))
|
||||
);
|
||||
}
|
||||
// 60+ API calls!
|
||||
}
|
||||
```
|
||||
|
||||
**Correct (using expand):**
|
||||
|
||||
```javascript
|
||||
// Single request with expanded relations
|
||||
async function getPostsWithAuthors() {
|
||||
const posts = await pb.collection('posts').getList(1, 20, {
|
||||
expand: 'author'
|
||||
});
|
||||
|
||||
// Access expanded data
|
||||
posts.items.forEach(post => {
|
||||
console.log('Author:', post.expand?.author?.name);
|
||||
});
|
||||
|
||||
return posts;
|
||||
}
|
||||
// 1 API call!
|
||||
|
||||
// Multiple relations
|
||||
async function getPostsWithAll() {
|
||||
const posts = await pb.collection('posts').getList(1, 20, {
|
||||
expand: 'author,category,tags'
|
||||
});
|
||||
|
||||
posts.items.forEach(post => {
|
||||
console.log('Author:', post.expand?.author?.name);
|
||||
console.log('Category:', post.expand?.category?.name);
|
||||
console.log('Tags:', post.expand?.tags?.map(t => t.name));
|
||||
});
|
||||
}
|
||||
// Still just 1 API call!
|
||||
|
||||
// Nested expansion (up to 6 levels)
|
||||
async function getPostsWithNestedData() {
|
||||
const posts = await pb.collection('posts').getList(1, 20, {
|
||||
expand: 'author.profile,category.parent,comments_via_post.author'
|
||||
});
|
||||
|
||||
posts.items.forEach(post => {
|
||||
// Nested relations
|
||||
console.log('Author profile:', post.expand?.author?.expand?.profile);
|
||||
console.log('Parent category:', post.expand?.category?.expand?.parent);
|
||||
|
||||
// Back-relations (comments that reference this post)
|
||||
console.log('Comments:', post.expand?.['comments_via_post']);
|
||||
});
|
||||
}
|
||||
|
||||
// Back-relation expansion
|
||||
// If comments collection has a 'post' relation field pointing to posts
|
||||
async function getPostWithComments(postId) {
|
||||
const post = await pb.collection('posts').getOne(postId, {
|
||||
expand: 'comments_via_post,comments_via_post.author'
|
||||
});
|
||||
|
||||
// Access comments that reference this post
|
||||
const comments = post.expand?.['comments_via_post'] || [];
|
||||
comments.forEach(comment => {
|
||||
console.log(`${comment.expand?.author?.name}: ${comment.text}`);
|
||||
});
|
||||
|
||||
return post;
|
||||
}
|
||||
```
|
||||
|
||||
**Expand syntax:**
|
||||
|
||||
| Syntax | Description |
|
||||
|--------|-------------|
|
||||
| `expand: 'author'` | Single relation |
|
||||
| `expand: 'author,tags'` | Multiple relations |
|
||||
| `expand: 'author.profile'` | Nested relation (2 levels) |
|
||||
| `expand: 'comments_via_post'` | Back-relation (records pointing to this) |
|
||||
|
||||
**Handling optional expand data:**
|
||||
|
||||
```javascript
|
||||
// Always use optional chaining - expand may be undefined
|
||||
const authorName = post.expand?.author?.name || 'Unknown';
|
||||
|
||||
// Type-safe access with TypeScript
|
||||
interface Post {
|
||||
id: string;
|
||||
title: string;
|
||||
author: string; // Relation ID
|
||||
expand?: {
|
||||
author?: User;
|
||||
};
|
||||
}
|
||||
|
||||
const posts = await pb.collection('posts').getList<Post>(1, 20, {
|
||||
expand: 'author'
|
||||
});
|
||||
```
|
||||
|
||||
**Limitations:**
|
||||
- Maximum 6 levels of nesting
|
||||
- Respects API rules on expanded collections
|
||||
- Large expansions may impact performance
|
||||
|
||||
Reference: [PocketBase Expand](https://pocketbase.io/docs/api-records/#expand)
|
||||
Reference in New Issue
Block a user