1004 lines
28 KiB
Markdown
1004 lines
28 KiB
Markdown
# SDK Usage
|
|
|
|
**Impact: HIGH**
|
|
|
|
JavaScript SDK initialization, auth store patterns, error handling, request cancellation, and safe parameter binding.
|
|
|
|
---
|
|
|
|
## 1. Use Appropriate Auth Store for Your Platform
|
|
|
|
**Impact: HIGH (Proper auth persistence across sessions and page reloads)**
|
|
|
|
The auth store persists authentication state. Choose the right store type based on your platform: LocalAuthStore for browsers, AsyncAuthStore for React Native, or custom stores for specific needs.
|
|
|
|
**Incorrect (wrong store for platform):**
|
|
|
|
```javascript
|
|
// React Native: LocalAuthStore doesn't work correctly
|
|
import PocketBase from 'pocketbase';
|
|
const pb = new PocketBase('http://127.0.0.1:8090');
|
|
// Auth state lost on app restart!
|
|
|
|
// Deno server: LocalStorage shared between all clients
|
|
import PocketBase from 'pocketbase';
|
|
const pb = new PocketBase('http://127.0.0.1:8090');
|
|
// All clients share the same auth state!
|
|
|
|
// Server-side: Reusing single client for multiple users
|
|
const pb = new PocketBase('http://127.0.0.1:8090');
|
|
// User A logs in...
|
|
// User B's request uses User A's auth!
|
|
```
|
|
|
|
**Correct (platform-appropriate stores):**
|
|
|
|
```javascript
|
|
// Browser (default LocalAuthStore - works automatically)
|
|
import PocketBase from 'pocketbase';
|
|
const pb = new PocketBase('http://127.0.0.1:8090');
|
|
// Automatically persists to localStorage and syncs between tabs
|
|
|
|
// React Native (AsyncAuthStore)
|
|
import PocketBase, { AsyncAuthStore } from 'pocketbase';
|
|
import AsyncStorage from '@react-native-async-storage/async-storage';
|
|
|
|
const store = new AsyncAuthStore({
|
|
save: async (serialized) => {
|
|
await AsyncStorage.setItem('pb_auth', serialized);
|
|
},
|
|
initial: AsyncStorage.getItem('pb_auth'),
|
|
clear: async () => {
|
|
await AsyncStorage.removeItem('pb_auth');
|
|
}
|
|
});
|
|
|
|
const pb = new PocketBase('http://127.0.0.1:8090', store);
|
|
|
|
// Server-side / SSR (create client per request)
|
|
import PocketBase from 'pocketbase';
|
|
|
|
export function createServerClient(cookieHeader) {
|
|
const pb = new PocketBase('http://127.0.0.1:8090');
|
|
pb.authStore.loadFromCookie(cookieHeader || '');
|
|
return pb;
|
|
}
|
|
|
|
// Deno/Cloudflare Workers (memory-only store)
|
|
import PocketBase, { BaseAuthStore } from 'pocketbase';
|
|
|
|
class MemoryAuthStore extends BaseAuthStore {
|
|
// Token only persists for request duration
|
|
// Each request must include auth via cookie/header
|
|
}
|
|
|
|
const pb = new PocketBase('http://127.0.0.1:8090', new MemoryAuthStore());
|
|
```
|
|
|
|
**Custom auth store example:**
|
|
|
|
```javascript
|
|
import PocketBase, { BaseAuthStore } from 'pocketbase';
|
|
|
|
class SecureAuthStore extends BaseAuthStore {
|
|
constructor() {
|
|
super();
|
|
// Load initial state from secure storage
|
|
const data = secureStorage.get('pb_auth');
|
|
if (data) {
|
|
const { token, record } = JSON.parse(data);
|
|
this.save(token, record);
|
|
}
|
|
}
|
|
|
|
save(token, record) {
|
|
super.save(token, record);
|
|
// Persist to secure storage
|
|
secureStorage.set('pb_auth', JSON.stringify({ token, record }));
|
|
}
|
|
|
|
clear() {
|
|
super.clear();
|
|
secureStorage.remove('pb_auth');
|
|
}
|
|
}
|
|
|
|
const pb = new PocketBase('http://127.0.0.1:8090', new SecureAuthStore());
|
|
```
|
|
|
|
**Auth store methods:**
|
|
|
|
```javascript
|
|
// Available on all auth stores
|
|
pb.authStore.token; // Current token
|
|
pb.authStore.record; // Current auth record
|
|
pb.authStore.isValid; // Token exists and not expired
|
|
pb.authStore.isSuperuser; // Is superuser token
|
|
|
|
pb.authStore.save(token, record); // Save auth state
|
|
pb.authStore.clear(); // Clear auth state
|
|
|
|
// Listen for changes
|
|
const unsubscribe = pb.authStore.onChange((token, record) => {
|
|
console.log('Auth changed:', record?.email);
|
|
}, true); // true = fire immediately
|
|
|
|
// Cookie helpers (for SSR)
|
|
pb.authStore.loadFromCookie(cookieString);
|
|
pb.authStore.exportToCookie({ httpOnly: false, secure: true });
|
|
```
|
|
|
|
Reference: [PocketBase Authentication](https://pocketbase.io/docs/authentication/)
|
|
|
|
## 2. Understand and Control Auto-Cancellation
|
|
|
|
**Impact: MEDIUM (Prevents race conditions, improves UX for search/typeahead)**
|
|
|
|
The SDK automatically cancels duplicate pending requests. This prevents race conditions but requires understanding for proper use in concurrent scenarios.
|
|
|
|
**Incorrect (confused by auto-cancellation):**
|
|
|
|
```javascript
|
|
// These requests will interfere with each other!
|
|
async function loadDashboard() {
|
|
// Only the last one executes, others cancelled
|
|
const posts = pb.collection('posts').getList(1, 20);
|
|
const users = pb.collection('posts').getList(1, 10); // Different params but same path
|
|
const comments = pb.collection('posts').getList(1, 5);
|
|
|
|
// posts and users are cancelled, only comments executes
|
|
return Promise.all([posts, users, comments]); // First two fail!
|
|
}
|
|
|
|
// Realtime combined with polling causes cancellation
|
|
pb.collection('posts').subscribe('*', callback);
|
|
setInterval(() => {
|
|
pb.collection('posts').getList(); // May cancel realtime!
|
|
}, 5000);
|
|
```
|
|
|
|
**Correct (controlling auto-cancellation):**
|
|
|
|
```javascript
|
|
// Disable auto-cancellation for parallel requests
|
|
async function loadDashboard() {
|
|
const [posts, users, recent] = await Promise.all([
|
|
pb.collection('posts').getList(1, 20, { requestKey: null }),
|
|
pb.collection('users').getList(1, 10, { requestKey: null }),
|
|
pb.collection('posts').getList(1, 5, { requestKey: 'recent' })
|
|
]);
|
|
// All requests complete independently
|
|
return { posts, users, recent };
|
|
}
|
|
|
|
// Use unique request keys for different purposes
|
|
async function searchPosts(query) {
|
|
return pb.collection('posts').getList(1, 20, {
|
|
filter: pb.filter('title ~ {:q}', { q: query }),
|
|
requestKey: 'post-search' // Cancels previous searches only
|
|
});
|
|
}
|
|
|
|
async function loadPostDetails(postId) {
|
|
return pb.collection('posts').getOne(postId, {
|
|
requestKey: `post-${postId}` // Unique per post
|
|
});
|
|
}
|
|
|
|
// Typeahead search - auto-cancellation is helpful here
|
|
async function typeaheadSearch(query) {
|
|
// Previous search automatically cancelled when user types more
|
|
return pb.collection('products').getList(1, 10, {
|
|
filter: pb.filter('name ~ {:q}', { q: query })
|
|
// No requestKey = uses default (path-based), previous cancelled
|
|
});
|
|
}
|
|
|
|
// Globally disable auto-cancellation (use carefully)
|
|
pb.autoCancellation(false);
|
|
|
|
// Now all requests are independent
|
|
await Promise.all([
|
|
pb.collection('posts').getList(1, 20),
|
|
pb.collection('posts').getList(1, 10),
|
|
pb.collection('posts').getList(1, 5)
|
|
]);
|
|
|
|
// Re-enable
|
|
pb.autoCancellation(true);
|
|
```
|
|
|
|
**Manual cancellation:**
|
|
|
|
```javascript
|
|
// Cancel all pending requests
|
|
pb.cancelAllRequests();
|
|
|
|
// Cancel specific request by key
|
|
pb.cancelRequest('post-search');
|
|
|
|
// Example: Cancel on component unmount
|
|
function PostList() {
|
|
useEffect(() => {
|
|
loadPosts();
|
|
|
|
return () => {
|
|
// Cleanup: cancel pending requests
|
|
pb.cancelRequest('post-list');
|
|
};
|
|
}, []);
|
|
|
|
async function loadPosts() {
|
|
const result = await pb.collection('posts').getList(1, 20, {
|
|
requestKey: 'post-list'
|
|
});
|
|
setPosts(result.items);
|
|
}
|
|
}
|
|
|
|
// Handle cancellation in catch
|
|
async function fetchWithCancellation() {
|
|
try {
|
|
return await pb.collection('posts').getList();
|
|
} catch (error) {
|
|
if (error.isAbort) {
|
|
// Request was cancelled - this is expected
|
|
console.log('Request cancelled');
|
|
return null;
|
|
}
|
|
throw error;
|
|
}
|
|
}
|
|
```
|
|
|
|
**When to use each approach:**
|
|
|
|
| Scenario | Approach |
|
|
|----------|----------|
|
|
| Search/typeahead | Default (let it cancel) |
|
|
| Parallel data loading | `requestKey: null` |
|
|
| Grouped requests | Custom `requestKey` |
|
|
| Component cleanup | `cancelRequest(key)` |
|
|
| Testing/debugging | `autoCancellation(false)` |
|
|
| OAuth2 flow cancel | `cancelRequest(requestKey)` — properly rejects the `authWithOAuth2()` Promise (JS SDK v0.26.8+) |
|
|
|
|
> **Note (JS SDK v0.26.8):** Calling `pb.cancelRequest(requestKey)` while `authWithOAuth2()` is waiting now properly rejects the returned Promise. In earlier versions the manual cancellation did not account for the waiting realtime subscription, so the Promise could hang indefinitely.
|
|
|
|
Reference: [PocketBase Auto-Cancellation](https://github.com/pocketbase/js-sdk#auto-cancellation)
|
|
|
|
## 3. Handle SDK Errors Properly
|
|
|
|
**Impact: HIGH (Graceful error recovery, better UX, easier debugging)**
|
|
|
|
All SDK methods return Promises that may reject with `ClientResponseError`. Proper error handling improves user experience and simplifies debugging.
|
|
|
|
**Incorrect (ignoring or poorly handling errors):**
|
|
|
|
```javascript
|
|
// No error handling
|
|
const posts = await pb.collection('posts').getList();
|
|
|
|
// Generic catch that loses information
|
|
try {
|
|
await pb.collection('posts').create({ title: '' });
|
|
} catch (e) {
|
|
alert('Something went wrong'); // No useful info
|
|
}
|
|
|
|
// Not checking specific error types
|
|
try {
|
|
await pb.collection('posts').getOne('nonexistent');
|
|
} catch (e) {
|
|
console.log(e.message); // Missing status, response details
|
|
}
|
|
```
|
|
|
|
**Correct (comprehensive error handling):**
|
|
|
|
```javascript
|
|
import PocketBase, { ClientResponseError } from 'pocketbase';
|
|
|
|
const pb = new PocketBase('http://127.0.0.1:8090');
|
|
|
|
// Basic error handling with ClientResponseError
|
|
async function createPost(data) {
|
|
try {
|
|
return await pb.collection('posts').create(data);
|
|
} catch (error) {
|
|
if (error instanceof ClientResponseError) {
|
|
console.log('Status:', error.status);
|
|
console.log('Response:', error.response);
|
|
console.log('URL:', error.url);
|
|
console.log('Is abort:', error.isAbort);
|
|
|
|
// Handle specific status codes
|
|
switch (error.status) {
|
|
case 400:
|
|
// Validation error - extract user-friendly messages only
|
|
// IMPORTANT: Don't expose raw error.response.data to clients
|
|
// as it may leak internal field names and validation rules
|
|
const fieldErrors = {};
|
|
if (error.response?.data) {
|
|
for (const [field, details] of Object.entries(error.response.data)) {
|
|
fieldErrors[field] = details.message;
|
|
}
|
|
}
|
|
return { error: 'validation', fields: fieldErrors };
|
|
case 401:
|
|
// Unauthorized - need to login
|
|
return { error: 'unauthorized' };
|
|
case 403:
|
|
// Forbidden - no permission
|
|
return { error: 'forbidden' };
|
|
case 404:
|
|
// Not found
|
|
return { error: 'not_found' };
|
|
default:
|
|
return { error: 'server_error' };
|
|
}
|
|
}
|
|
throw error; // Re-throw non-PocketBase errors
|
|
}
|
|
}
|
|
|
|
// Handle validation errors with field details
|
|
async function updateProfile(userId, data) {
|
|
try {
|
|
return await pb.collection('users').update(userId, data);
|
|
} catch (error) {
|
|
if (error.status === 400 && error.response?.data) {
|
|
// Extract field-specific errors
|
|
const fieldErrors = {};
|
|
for (const [field, details] of Object.entries(error.response.data)) {
|
|
fieldErrors[field] = details.message;
|
|
}
|
|
return { success: false, errors: fieldErrors };
|
|
// { errors: { email: "Invalid email format", name: "Required field" } }
|
|
}
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
// Handle request cancellation
|
|
async function searchWithCancel(query) {
|
|
try {
|
|
return await pb.collection('posts').getList(1, 20, {
|
|
filter: pb.filter('title ~ {:query}', { query })
|
|
});
|
|
} catch (error) {
|
|
if (error.isAbort) {
|
|
// Request was cancelled (e.g., user typed again)
|
|
console.log('Search cancelled');
|
|
return null;
|
|
}
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
// Wrapper function for consistent error handling
|
|
async function pbRequest(fn) {
|
|
try {
|
|
return { data: await fn(), error: null };
|
|
} catch (error) {
|
|
if (error instanceof ClientResponseError) {
|
|
return {
|
|
data: null,
|
|
error: {
|
|
status: error.status,
|
|
message: error.response?.message || 'Request failed',
|
|
data: error.response?.data || null
|
|
}
|
|
};
|
|
}
|
|
return {
|
|
data: null,
|
|
error: { status: 0, message: error.message, data: null }
|
|
};
|
|
}
|
|
}
|
|
|
|
// Usage
|
|
const { data, error } = await pbRequest(() =>
|
|
pb.collection('posts').getList(1, 20)
|
|
);
|
|
|
|
if (error) {
|
|
console.log('Failed:', error.message);
|
|
} else {
|
|
console.log('Posts:', data.items);
|
|
}
|
|
```
|
|
|
|
**ClientResponseError structure:**
|
|
|
|
```typescript
|
|
interface ClientResponseError {
|
|
url: string; // The request URL
|
|
status: number; // HTTP status code (0 if network error)
|
|
response: { // API response body
|
|
code: number;
|
|
message: string;
|
|
data: { [field: string]: { code: string; message: string } };
|
|
};
|
|
isAbort: boolean; // True if request was cancelled
|
|
cause: Error | null; // Original error (added in JS SDK v0.26.1)
|
|
}
|
|
```
|
|
|
|
Reference: [PocketBase Error Handling](https://github.com/pocketbase/js-sdk#error-handling)
|
|
|
|
## 4. Use Field Modifiers for Incremental Updates
|
|
|
|
**Impact: HIGH (Atomic updates, prevents race conditions, cleaner code)**
|
|
|
|
PocketBase supports `+` and `-` modifiers for incrementing numbers, appending/removing relation IDs, and managing file arrays without replacing the entire value.
|
|
|
|
**Incorrect (read-modify-write pattern):**
|
|
|
|
```javascript
|
|
// Race condition: two users adding tags simultaneously
|
|
async function addTag(postId, newTagId) {
|
|
const post = await pb.collection('posts').getOne(postId);
|
|
const currentTags = post.tags || [];
|
|
|
|
// Another user might have added a tag in between!
|
|
await pb.collection('posts').update(postId, {
|
|
tags: [...currentTags, newTagId] // Might overwrite the other user's tag
|
|
});
|
|
}
|
|
|
|
// Inefficient for incrementing counters
|
|
async function incrementViews(postId) {
|
|
const post = await pb.collection('posts').getOne(postId);
|
|
await pb.collection('posts').update(postId, {
|
|
views: post.views + 1 // Extra read, race condition
|
|
});
|
|
}
|
|
```
|
|
|
|
**Correct (using field modifiers):**
|
|
|
|
```javascript
|
|
// Atomic relation append with + modifier
|
|
async function addTag(postId, newTagId) {
|
|
await pb.collection('posts').update(postId, {
|
|
'tags+': newTagId // Appends to existing tags atomically
|
|
});
|
|
}
|
|
|
|
// Append multiple relations
|
|
async function addTags(postId, tagIds) {
|
|
await pb.collection('posts').update(postId, {
|
|
'tags+': tagIds // Appends array of IDs
|
|
});
|
|
}
|
|
|
|
// Prepend relations (+ prefix)
|
|
async function prependTag(postId, tagId) {
|
|
await pb.collection('posts').update(postId, {
|
|
'+tags': tagId // Prepends to start of array
|
|
});
|
|
}
|
|
|
|
// Remove relations with - modifier
|
|
async function removeTag(postId, tagId) {
|
|
await pb.collection('posts').update(postId, {
|
|
'tags-': tagId // Removes specific tag
|
|
});
|
|
}
|
|
|
|
// Remove multiple relations
|
|
async function removeTags(postId, tagIds) {
|
|
await pb.collection('posts').update(postId, {
|
|
'tags-': tagIds // Removes all specified tags
|
|
});
|
|
}
|
|
|
|
// Atomic number increment
|
|
async function incrementViews(postId) {
|
|
await pb.collection('posts').update(postId, {
|
|
'views+': 1 // Atomic increment, no race condition
|
|
});
|
|
}
|
|
|
|
// Atomic number decrement
|
|
async function decrementStock(productId, quantity) {
|
|
await pb.collection('products').update(productId, {
|
|
'stock-': quantity // Atomic decrement
|
|
});
|
|
}
|
|
|
|
// File append (for multi-file fields)
|
|
async function addImage(albumId, newImage) {
|
|
await pb.collection('albums').update(albumId, {
|
|
'images+': newImage // Appends new file to existing
|
|
});
|
|
}
|
|
|
|
// File removal
|
|
async function removeImage(albumId, filename) {
|
|
await pb.collection('albums').update(albumId, {
|
|
'images-': filename // Removes specific file by name
|
|
});
|
|
}
|
|
|
|
// Combined modifiers in single update
|
|
async function updatePost(postId, data) {
|
|
await pb.collection('posts').update(postId, {
|
|
title: data.title, // Replace field
|
|
'views+': 1, // Increment number
|
|
'tags+': data.newTagId, // Append relation
|
|
'tags-': data.oldTagId, // Remove relation
|
|
'images+': data.newImage // Append file
|
|
});
|
|
}
|
|
```
|
|
|
|
**Modifier reference:**
|
|
|
|
| Modifier | Field Types | Description |
|
|
|----------|-------------|-------------|
|
|
| `field+` or `+field` | relation, file | Append/prepend to array |
|
|
| `field-` | relation, file | Remove from array |
|
|
| `field+` | number | Increment by value |
|
|
| `field-` | number | Decrement by value |
|
|
|
|
**Benefits:**
|
|
- **Atomic**: No read-modify-write race conditions
|
|
- **Efficient**: Single request, no extra read needed
|
|
- **Clean**: Expresses intent clearly
|
|
|
|
**Note:** Modifiers only work with `update()`, not `create()`.
|
|
|
|
Reference: [PocketBase Relations](https://pocketbase.io/docs/working-with-relations/)
|
|
|
|
## 5. Use Safe Parameter Binding in Filters
|
|
|
|
**Impact: CRITICAL (Prevents injection attacks, handles special characters correctly)**
|
|
|
|
Always use `pb.filter()` with parameter binding when constructing filters with user input. String concatenation is vulnerable to injection attacks.
|
|
|
|
**Incorrect (string concatenation - DANGEROUS):**
|
|
|
|
```javascript
|
|
// SQL/filter injection vulnerability!
|
|
async function searchPosts(userInput) {
|
|
// User input: `test" || id != "` breaks out of string
|
|
const posts = await pb.collection('posts').getList(1, 20, {
|
|
filter: `title ~ "${userInput}"` // VULNERABLE!
|
|
});
|
|
return posts;
|
|
}
|
|
|
|
// Even with escaping, easy to get wrong
|
|
async function searchByEmail(email) {
|
|
const escaped = email.replace(/"/g, '\\"'); // Incomplete escaping
|
|
const users = await pb.collection('users').getList(1, 1, {
|
|
filter: `email = "${escaped}"` // Still potentially vulnerable
|
|
});
|
|
return users;
|
|
}
|
|
|
|
// Template literals are just as dangerous
|
|
const filter = `status = "${status}" && author = "${authorId}"`;
|
|
```
|
|
|
|
**Correct (using pb.filter with parameters):**
|
|
|
|
```javascript
|
|
// Safe parameter binding
|
|
async function searchPosts(userInput) {
|
|
const posts = await pb.collection('posts').getList(1, 20, {
|
|
filter: pb.filter('title ~ {:search}', { search: userInput })
|
|
});
|
|
return posts;
|
|
}
|
|
|
|
// Multiple parameters
|
|
async function filterPosts(status, authorId, minViews) {
|
|
const posts = await pb.collection('posts').getList(1, 20, {
|
|
filter: pb.filter(
|
|
'status = {:status} && author = {:author} && views >= {:views}',
|
|
{ status, author: authorId, views: minViews }
|
|
)
|
|
});
|
|
return posts;
|
|
}
|
|
|
|
// Reusing parameters
|
|
async function searchBothFields(query) {
|
|
const results = await pb.collection('posts').getList(1, 20, {
|
|
filter: pb.filter(
|
|
'title ~ {:q} || content ~ {:q}',
|
|
{ q: query } // Same parameter used twice
|
|
)
|
|
});
|
|
return results;
|
|
}
|
|
|
|
// Different parameter types
|
|
async function complexFilter(options) {
|
|
const filter = pb.filter(
|
|
'created > {:date} && active = {:active} && category = {:cat}',
|
|
{
|
|
date: new Date('2024-01-01'), // Date objects handled correctly
|
|
active: true, // Booleans
|
|
cat: options.category // Strings auto-escaped
|
|
}
|
|
);
|
|
|
|
return pb.collection('posts').getList(1, 20, { filter });
|
|
}
|
|
|
|
// Null handling
|
|
async function filterWithOptional(category) {
|
|
// Only include filter if value provided
|
|
const filter = category
|
|
? pb.filter('category = {:cat}', { cat: category })
|
|
: '';
|
|
|
|
return pb.collection('posts').getList(1, 20, { filter });
|
|
}
|
|
|
|
// Building dynamic filters
|
|
async function dynamicSearch(filters) {
|
|
const conditions = [];
|
|
const params = {};
|
|
|
|
if (filters.title) {
|
|
conditions.push('title ~ {:title}');
|
|
params.title = filters.title;
|
|
}
|
|
|
|
if (filters.author) {
|
|
conditions.push('author = {:author}');
|
|
params.author = filters.author;
|
|
}
|
|
|
|
if (filters.minDate) {
|
|
conditions.push('created >= {:minDate}');
|
|
params.minDate = filters.minDate;
|
|
}
|
|
|
|
const filter = conditions.length > 0
|
|
? pb.filter(conditions.join(' && '), params)
|
|
: '';
|
|
|
|
return pb.collection('posts').getList(1, 20, { filter });
|
|
}
|
|
```
|
|
|
|
**Supported parameter types:**
|
|
|
|
| Type | Example | Notes |
|
|
|------|---------|-------|
|
|
| string | `'hello'` | Auto-escaped, quotes handled |
|
|
| number | `123`, `45.67` | No quotes added |
|
|
| boolean | `true`, `false` | Converted correctly |
|
|
| Date | `new Date()` | Formatted for PocketBase |
|
|
| null | `null` | For null comparisons |
|
|
| other | `{...}` | JSON.stringify() applied |
|
|
|
|
**Server-side is especially critical:**
|
|
|
|
```javascript
|
|
// Server-side code (Node.js, Deno, etc.) MUST use binding
|
|
// because malicious users control the input directly
|
|
|
|
export async function searchHandler(req) {
|
|
const userQuery = req.query.q; // Untrusted input!
|
|
|
|
// ALWAYS use pb.filter() on server
|
|
const results = await pb.collection('posts').getList(1, 20, {
|
|
filter: pb.filter('title ~ {:q}', { q: userQuery })
|
|
});
|
|
|
|
return results;
|
|
}
|
|
```
|
|
|
|
Reference: [PocketBase Filters](https://pocketbase.io/docs/api-rules-and-filters/)
|
|
|
|
## 6. Initialize PocketBase Client Correctly
|
|
|
|
**Impact: HIGH (Proper setup enables auth persistence, SSR support, and optimal performance)**
|
|
|
|
Client initialization should consider the environment (browser, Node.js, SSR), auth store persistence, and any required polyfills.
|
|
|
|
**Incorrect (environment-agnostic initialization):**
|
|
|
|
```javascript
|
|
// Missing polyfills in Node.js
|
|
import PocketBase from 'pocketbase';
|
|
const pb = new PocketBase('http://127.0.0.1:8090');
|
|
|
|
// Node.js: EventSource not defined error on realtime
|
|
pb.collection('posts').subscribe('*', callback); // Fails!
|
|
|
|
// Missing base URL
|
|
const pb = new PocketBase(); // Uses '/' - likely wrong
|
|
```
|
|
|
|
**Correct (environment-aware initialization):**
|
|
|
|
```javascript
|
|
// Browser setup (no polyfills needed)
|
|
// IMPORTANT: Use HTTPS in production (http is only for local development)
|
|
import PocketBase from 'pocketbase';
|
|
|
|
const pb = new PocketBase('http://127.0.0.1:8090'); // Use https:// in production
|
|
|
|
// Node.js setup (requires polyfills for realtime)
|
|
import PocketBase from 'pocketbase';
|
|
import { EventSource } from 'eventsource';
|
|
|
|
// Global polyfill for realtime
|
|
global.EventSource = EventSource;
|
|
|
|
const pb = new PocketBase('http://127.0.0.1:8090');
|
|
|
|
// React Native setup (async auth store)
|
|
import PocketBase, { AsyncAuthStore } from 'pocketbase';
|
|
import AsyncStorage from '@react-native-async-storage/async-storage';
|
|
import EventSource from 'react-native-sse';
|
|
|
|
global.EventSource = EventSource;
|
|
|
|
const store = new AsyncAuthStore({
|
|
save: async (serialized) => AsyncStorage.setItem('pb_auth', serialized),
|
|
initial: AsyncStorage.getItem('pb_auth'),
|
|
});
|
|
|
|
const pb = new PocketBase('http://127.0.0.1:8090', store);
|
|
```
|
|
|
|
**SSR initialization (per-request client):**
|
|
|
|
```javascript
|
|
// SvelteKit example
|
|
// src/hooks.server.js
|
|
import PocketBase from 'pocketbase';
|
|
|
|
export async function handle({ event, resolve }) {
|
|
// Create fresh client for each request
|
|
event.locals.pb = new PocketBase('http://127.0.0.1:8090');
|
|
|
|
// Load auth from request cookie
|
|
event.locals.pb.authStore.loadFromCookie(
|
|
event.request.headers.get('cookie') || ''
|
|
);
|
|
|
|
// Validate token
|
|
if (event.locals.pb.authStore.isValid) {
|
|
try {
|
|
await event.locals.pb.collection('users').authRefresh();
|
|
} catch {
|
|
event.locals.pb.authStore.clear();
|
|
}
|
|
}
|
|
|
|
const response = await resolve(event);
|
|
|
|
// Send updated auth cookie with secure options
|
|
response.headers.append(
|
|
'set-cookie',
|
|
event.locals.pb.authStore.exportToCookie({
|
|
httpOnly: true, // Prevent XSS access to auth token
|
|
secure: true, // HTTPS only
|
|
sameSite: 'Lax', // CSRF protection
|
|
})
|
|
);
|
|
|
|
return response;
|
|
}
|
|
```
|
|
|
|
**TypeScript with typed collections:**
|
|
|
|
```typescript
|
|
import PocketBase, { RecordService } from 'pocketbase';
|
|
|
|
// Define your record types
|
|
interface User {
|
|
id: string;
|
|
email: string;
|
|
name: string;
|
|
avatar?: string;
|
|
}
|
|
|
|
interface Post {
|
|
id: string;
|
|
title: string;
|
|
content: string;
|
|
author: string;
|
|
published: boolean;
|
|
}
|
|
|
|
// Create typed client interface
|
|
interface TypedPocketBase extends PocketBase {
|
|
collection(idOrName: string): RecordService;
|
|
collection(idOrName: 'users'): RecordService<User>;
|
|
collection(idOrName: 'posts'): RecordService<Post>;
|
|
}
|
|
|
|
const pb = new PocketBase('http://127.0.0.1:8090') as TypedPocketBase;
|
|
|
|
// Now methods are typed
|
|
const post = await pb.collection('posts').getOne('abc'); // Returns Post
|
|
const users = await pb.collection('users').getList(); // Returns ListResult<User>
|
|
```
|
|
|
|
Reference: [PocketBase JS SDK](https://github.com/pocketbase/js-sdk)
|
|
|
|
## 7. Use Send Hooks for Request Customization
|
|
|
|
**Impact: MEDIUM (Custom headers, logging, response transformation)**
|
|
|
|
The SDK provides `beforeSend` and `afterSend` hooks for intercepting and modifying requests and responses globally.
|
|
|
|
**Incorrect (repeating logic in every request):**
|
|
|
|
```javascript
|
|
// Adding headers to every request manually
|
|
const posts = await pb.collection('posts').getList(1, 20, {
|
|
headers: { 'X-Custom-Header': 'value' }
|
|
});
|
|
|
|
const users = await pb.collection('users').getList(1, 20, {
|
|
headers: { 'X-Custom-Header': 'value' } // Repeated!
|
|
});
|
|
|
|
// Logging each request manually
|
|
console.log('Fetching posts...');
|
|
const posts = await pb.collection('posts').getList();
|
|
console.log('Done');
|
|
```
|
|
|
|
**Correct (using send hooks):**
|
|
|
|
```javascript
|
|
import PocketBase from 'pocketbase';
|
|
|
|
const pb = new PocketBase('http://127.0.0.1:8090');
|
|
|
|
// beforeSend - modify requests before they're sent
|
|
pb.beforeSend = function(url, options) {
|
|
// Add custom headers to all requests
|
|
options.headers = Object.assign({}, options.headers, {
|
|
'X-Custom-Header': 'value',
|
|
'X-Request-ID': crypto.randomUUID()
|
|
});
|
|
|
|
// Log outgoing requests
|
|
console.log(`[${options.method}] ${url}`);
|
|
|
|
// Must return { url, options }
|
|
return { url, options };
|
|
};
|
|
|
|
// afterSend - process responses
|
|
pb.afterSend = function(response, data) {
|
|
// Log response status
|
|
console.log(`Response: ${response.status}`);
|
|
|
|
// Transform or extend response data
|
|
if (data && typeof data === 'object') {
|
|
data._fetchedAt = new Date().toISOString();
|
|
}
|
|
|
|
// Return the (possibly modified) data
|
|
return data;
|
|
};
|
|
|
|
// All requests now automatically have headers and logging
|
|
const posts = await pb.collection('posts').getList();
|
|
const users = await pb.collection('users').getList();
|
|
```
|
|
|
|
**Practical examples:**
|
|
|
|
```javascript
|
|
// Request timing / performance monitoring
|
|
let requestStart;
|
|
pb.beforeSend = function(url, options) {
|
|
requestStart = performance.now();
|
|
return { url, options };
|
|
};
|
|
|
|
pb.afterSend = function(response, data) {
|
|
const duration = performance.now() - requestStart;
|
|
console.log(`${response.url}: ${duration.toFixed(2)}ms`);
|
|
|
|
// Send to analytics
|
|
trackApiPerformance(response.url, duration);
|
|
|
|
return data;
|
|
};
|
|
|
|
// Add auth token from different source
|
|
pb.beforeSend = function(url, options) {
|
|
const externalToken = getTokenFromExternalAuth();
|
|
if (externalToken) {
|
|
options.headers = Object.assign({}, options.headers, {
|
|
'X-External-Auth': externalToken
|
|
});
|
|
}
|
|
return { url, options };
|
|
};
|
|
|
|
// Handle specific response codes globally
|
|
pb.afterSend = function(response, data) {
|
|
if (response.status === 401) {
|
|
// Token expired - trigger re-auth
|
|
handleAuthExpired();
|
|
}
|
|
|
|
if (response.status === 503) {
|
|
// Service unavailable - show maintenance message
|
|
showMaintenanceMode();
|
|
}
|
|
|
|
return data;
|
|
};
|
|
|
|
// Retry failed requests (simplified example)
|
|
const originalSend = pb.send.bind(pb);
|
|
pb.send = async function(path, options) {
|
|
try {
|
|
return await originalSend(path, options);
|
|
} catch (error) {
|
|
if (error.status === 429) { // Rate limited
|
|
await sleep(1000);
|
|
return originalSend(path, options); // Retry once
|
|
}
|
|
throw error;
|
|
}
|
|
};
|
|
|
|
// Add request correlation for debugging
|
|
let requestId = 0;
|
|
pb.beforeSend = function(url, options) {
|
|
requestId++;
|
|
const correlationId = `req-${Date.now()}-${requestId}`;
|
|
|
|
options.headers = Object.assign({}, options.headers, {
|
|
'X-Correlation-ID': correlationId
|
|
});
|
|
|
|
console.log(`[${correlationId}] Starting: ${url}`);
|
|
return { url, options };
|
|
};
|
|
|
|
pb.afterSend = function(response, data) {
|
|
console.log(`Complete: ${response.status}`);
|
|
return data;
|
|
};
|
|
```
|
|
|
|
**Hook signatures:**
|
|
|
|
```typescript
|
|
// beforeSend
|
|
beforeSend?: (
|
|
url: string,
|
|
options: SendOptions
|
|
) => { url: string; options: SendOptions } | Promise<{ url: string; options: SendOptions }>;
|
|
|
|
// afterSend
|
|
afterSend?: (
|
|
response: Response,
|
|
data: any
|
|
) => any | Promise<any>;
|
|
```
|
|
|
|
**Use cases:**
|
|
- Add custom headers (API keys, correlation IDs)
|
|
- Request/response logging
|
|
- Performance monitoring
|
|
- Global error handling
|
|
- Response transformation
|
|
- Authentication middleware
|
|
|
|
Reference: [PocketBase Send Hooks](https://github.com/pocketbase/js-sdk#send-hooks)
|
|
|