# pbtsdb

> Type-safe PocketBase integration with TanStack Query and TanStack DB for React applications. Provides reactive collections with automatic real-time subscriptions, optimistic mutations, full TypeScript type safety, and minimal boilerplate.

This library connects PocketBase (backend-as-a-service) to TanStack's reactive database tools. Use it when building React applications that need real-time data synchronization with PocketBase while maintaining strict type safety.

## Core Pattern (React)

```typescript
import { createCollection, createReactProvider, newRecordId } from 'pbtsdb';
import { useLiveQuery } from '@tanstack/react-db';
import { eq } from '@tanstack/db';

// 1. Define schema with type and relations properties
export type Schema = {
    books: {
        type: Book;
        relations: {
            author?: Author;
        };
    };
    authors: {
        type: Author;
        relations: {};
    };
}

// 2. Create collections using curried createCollection
const c = createCollection<Schema>(pb, queryClient);
const collections = {
    books: c('books', {}),
    authors: c('authors', {}),
};

// 3. Create React Provider and useStore hook
const { Provider, useStore } = createReactProvider(collections);

// 4. Wrap your app with Provider
<Provider>
    <App />
</Provider>

// 5. Use in components - useStore returns an array!
function BooksList() {
    const [books] = useStore('books');  // ✅ Destructure from array
    const { data } = useLiveQuery((q) => q.from({ books }));
    // Subscriptions start/stop automatically with component lifecycle
}

function BooksWithAuthors() {
    const [books, authors] = useStore('books', 'authors');  // ✅ Variadic access
    const { data } = useLiveQuery((q) =>
        q.from({ book: books })
            .join({ author: authors }, ({ book, author }) => eq(book.author, author.id), 'left')
    );
}
```

**useStore() API:**
- Single key: `const [books] = useStore('books')` → returns array with typed collection
- Multiple keys: `const [books, authors] = useStore('books', 'authors')` → returns typed tuple
- Types are inferred from the collections passed to `createReactProvider`

**Custom Collection Keys:**
```typescript
const collections = {
    myBooks: c('books', {})  // Key 'myBooks', PocketBase collection 'books'
};
const { Provider, useStore } = createReactProvider(collections);
const [myBooks] = useStore('myBooks');
```

## Key Concepts

- **createCollection**: Curried function `createCollection<Schema>(pb, queryClient)` returns `(collectionName, options) => Collection`
- **createReactProvider**: Wraps collections for React, returns `{ Provider, useStore }`
- **Collections are lazy**: No network activity until first query
- **Subscriptions are automatic**: Start when component mounts, stop 5s after unmount
- **Schema structure**: Use `type` and `relations` properties (lowercase)
- **Package name**: Import from `pbtsdb`

## Non-React Usage

For non-React environments, use `createCollection` directly:

```typescript
import { createCollection } from 'pbtsdb';

const c = createCollection<Schema>(pb, queryClient);
const booksCollection = c('books', {});

// Collections have TanStack DB interface
// Use with useLiveQuery or direct access
```

## Testing

```bash
npm test  # Auto-resets DB, starts server, runs tests, stops server
```

Tests use real PocketBase instance (not mocked). Server infrastructure is fully automated.

## Documentation

- [AGENTS.md](AGENTS.md): Complete development guide with architecture, patterns, and testing
- [README.md](README.md): User-facing API reference with examples
- [test/schema.ts](test/schema.ts): Reference schema implementation

## Mutations

Collections support insert, update, and delete with automatic PocketBase sync:

```typescript
import { newRecordId, createCollection, createReactProvider } from 'pbtsdb';

// Setup with custom mutation handlers (optional)
const c = createCollection<Schema>(pb, queryClient);
const collections = {
    books: c('books', {
        onInsert: async ({ transaction }) => { /* custom logic */ },
        onUpdate: false, // Disable updates (throws error if called)
        onDelete: false, // Disable deletes
    })
};
const { Provider, useStore } = createReactProvider(collections);

// In component
function BooksList() {
    const [books] = useStore('books');

    // Insert with optimistic update (UI updates immediately)
    const tx = books.insert({
        id: newRecordId(), // Generate PocketBase-compatible ID
        title: 'New Book',
        // ... other fields
    });

    // Update with optimistic update
    books.update('record_id', (draft) => {
        draft.title = 'Updated Title';
    });

    // Update with config (non-optimistic - waits for server)
    books.update('record_id', { optimistic: false }, (draft) => {
        draft.title = 'Server-First Update';
    });

    // Delete with optimistic update
    books.delete('record_id');

    // Batch mutations - merges multiple updates to same record
    books.utils.writeBatch(() => {
        books.update('id1', (draft) => { draft.field1 = 'value1' });
        books.update('id1', (draft) => { draft.field2 = 'value2' }); // Merged!
        books.update('id2', (draft) => { draft.field = 'value' });
    });

    // Wait for persistence
    await tx.isPersisted.promise; // States: pending → persisting → completed
}
```

**Key Points:**
- Mutations are optimistic by default (UI updates immediately)
- Auto-syncs to PocketBase in background
- Batch mutations merge updates to same record
- Transaction states: pending → persisting → completed
- Use `newRecordId()` to generate PocketBase-compatible IDs

## Common Patterns

**Approach 1: Auto-Expand (Recommended)**
```typescript
const c = createCollection<Schema>(pb, queryClient);
const authors = c('authors', {});
const books = c('books', {
    expand: {
        author: authors  // Auto-expand and auto-upsert
    }
});

// Expand is automatic - no .expand() call needed
const { data } = useLiveQuery((q) => q.from({ books }));

// Expanded records automatically inserted into authors collection
// data[0].expand?.author is typed!
```

**When to use Auto-Expand:**
- You want expanded records to populate their own collections automatically
- Performance: Single server query with server-side expand
- Related data always available when fetching parent records

**Approach 2: TanStack Joins**
```typescript
const c = createCollection<Schema>(pb, queryClient);
const collections = {
    jobs: c('jobs', {}),
    customers: c('customers', {}),
};
const { Provider, useStore } = createReactProvider(collections);

// In component
const [jobs, customers] = useStore('jobs', 'customers');
// Client-side joins with full type safety, supports inner/left/right/full joins
```

## Critical Rules

- NEVER use `any` types - defeats the purpose of this library
- useStore always returns an array - destructure it: `const [books] = useStore('books')`
- Create ONE set of collections per PocketBase connection
- Schema must match actual PocketBase collection structure
- Code should be self-documenting - avoid obvious comments

## Optional

### When NOT to Use

- Simple CRUD without real-time (PocketBase SDK is simpler)
- Very large collections (>10k records - use pagination)
- Non-PocketBase backends (use TanStack Query directly)
- Server-side only Node.js (use PocketBase SDK directly)

### Package Details

- Requires React 18+, TypeScript 5.0+, PocketBase 0.21.0+
- Collections use TanStack Query for caching and TanStack DB for reactivity
- Real-time via PocketBase Server-Sent Events (SSE)
- Automatic reconnection with exponential backoff on subscription failures
- Default syncMode is 'eager' (matches TanStack DB default)
