Files
agentic-dev/packages/cms-client/AGENTS.md

218 lines
7.2 KiB
Markdown

# @repo/cms-client -- Dual-Mode Payload Client
## Purpose
Provides a typed, uniform interface for accessing Payload CMS data via either the Local API (direct in-process calls) or the HTTP REST API (network requests). The consuming app chooses the mode at startup and injects the Payload instance -- this package never imports it.
## **NEVER import from `@repo/cms-core`, `@repo/core`, or any `apps/*` package.**
This package is completely standalone. The Payload instance is INJECTED at app startup, never imported by this package.
## File Structure
```
packages/cms-client/
src/
types.ts # FindOptions, PayloadClientResult, PayloadClient interface
client.ts # createPayloadClient() factory -- returns Local or HTTP client
local-client.ts # LocalPayloadClient class -- wraps Payload Local API
http-client.ts # HTTPPayloadClient class -- wraps Payload REST API via fetch
index.ts # Package entry: re-exports factory, classes, and types
package.json
AGENTS.md
```
## Dual-Mode Initialization
### Local Mode (primary -- used for server-side code with direct DB access)
Local mode wraps the Payload Local API. It requires a live `Payload` instance, which is obtained via `getPayload()` in the consuming app:
```typescript
// Example: apps/web-next/src/lib/payload.ts
import { getPayload } from "payload";
import config from "@repo/cms-core/src/payload.config";
import { createPayloadClient } from "@repo/cms-client";
const payload = await getPayload({ config });
const client = createPayloadClient({ mode: "local", payload });
// Now use client.find(), client.create(), etc.
const articles = await client.find("articles", {
where: { status: { equals: "published" } },
sort: "-publishedAt",
limit: 10,
});
```
### HTTP Mode (fallback -- used for external services without direct DB access)
HTTP mode makes REST calls to the Payload API. It only needs the base URL:
```typescript
import { createPayloadClient } from "@repo/cms-client";
const client = createPayloadClient({
mode: "http",
baseURL: "http://localhost:3001",
});
// Same API surface as local mode
const articles = await client.find("articles", {
where: { status: { equals: "published" } },
limit: 10,
});
```
## Initialization Table
| App / Context | Mode | How initialized | Why this mode |
|---|---|---|---|
| `apps/cms` (server-side) | Local | `getPayload({ config })` from `@repo/cms-core` | Same process as Payload, direct DB access |
| `apps/web-next` (server components/actions) | Local | `getPayload({ config })` from `@repo/cms-core` | Server-side rendering needs fast DB access |
| `apps/web-tanstack` (server loaders) | Local | `getPayload({ config })` from `@repo/cms-core` | Server-side data loading needs fast DB access |
| Client-side (browser) | N/A | Does not use this package directly | Browser goes through tRPC, server handles CMS access |
| External services / microservices | HTTP | `createPayloadClient({ mode: "http", baseURL })` | No access to Payload instance, only REST API |
## PayloadClient API Reference
All methods are available on both Local and HTTP clients via the `PayloadClient` interface.
### `find<T>(collection, options?): Promise<PayloadClientResult<T>>`
Paginated query for documents in a collection.
```typescript
const result = await client.find<Article>("articles", {
where: { status: { equals: "published" } },
sort: "-publishedAt",
limit: 10,
page: 1,
depth: 2,
locale: "en",
});
// result.docs, result.totalDocs, result.totalPages, etc.
```
### `findByID<T>(collection, id, options?): Promise<T>`
Fetch a single document by ID.
```typescript
const article = await client.findByID<Article>("articles", "abc123", {
depth: 2,
});
```
### `create<T>(collection, data, options?): Promise<T>`
Create a new document.
```typescript
const newArticle = await client.create<Article>("articles", {
title: "My Article",
content: "...",
author: "user-id-123",
status: "draft",
}, { depth: 1 });
```
### `update<T>(collection, id, data, options?): Promise<T>`
Update an existing document (partial update).
```typescript
const updated = await client.update<Article>("articles", "abc123", {
status: "published",
publishedAt: new Date().toISOString(),
});
```
### `delete<T>(collection, id): Promise<T>`
Delete a document by ID.
```typescript
const deleted = await client.delete<Article>("articles", "abc123");
```
## FindOptions Interface
```typescript
interface FindOptions {
where?: Record<string, unknown>; // Payload query operators ({ field: { equals: value } })
sort?: string; // Field name, prefix with "-" for descending
limit?: number; // Max documents per page (default: 10)
page?: number; // Page number (1-based)
depth?: number; // Relationship population depth (default: 1)
locale?: string; // Locale for localized fields
}
```
## PayloadClientResult Interface
```typescript
interface PayloadClientResult<T> {
docs: T[]; // Array of documents for current page
totalDocs: number; // Total matching documents across all pages
limit: number; // Max docs per page (as requested)
totalPages: number; // Total number of pages
page: number; // Current page number (1-based)
pagingCounter: number; // Index of first doc on current page
hasPrevPage: boolean; // Whether a previous page exists
hasNextPage: boolean; // Whether a next page exists
prevPage: number | null; // Previous page number, or null
nextPage: number | null; // Next page number, or null
}
```
## App Startup Pattern
The Payload instance is always created in the consuming app, then injected into the client:
```typescript
// apps/web-next/src/lib/payload.ts
import { getPayload } from "payload";
import config from "@repo/cms-core/src/payload.config";
import { createPayloadClient, type PayloadClient } from "@repo/cms-client";
let cachedClient: PayloadClient | null = null;
export async function getPayloadClient(): Promise<PayloadClient> {
if (cachedClient) return cachedClient;
const payload = await getPayload({ config });
cachedClient = createPayloadClient({ mode: "local", payload });
return cachedClient;
}
```
This pattern ensures:
1. The Payload instance is created once and reused
2. The cms-client package never imports config or Payload itself
3. Each app controls its own initialization
## Type Generation
Payload generates TypeScript types from your collection/global definitions:
```bash
cd apps/cms && pnpm generate:types
# Runs: payload generate:types
# Outputs to: packages/cms-core/src/payload-types.ts (configured in payload.config.ts)
```
After adding or modifying collections/globals in `@repo/cms-core`, re-run type generation to keep types in sync.
## Dependencies
| Dependency | Purpose |
|---|---|
| `payload` | `Payload` type for the Local API client constructor (type-only at build time) |
## Cross-References
- **CMS configuration:** `packages/cms-core/` -- see `packages/cms-core/AGENTS.md`
- **CMS app (where getPayload is called):** `apps/cms/` -- see `apps/cms/AGENTS.md`
- **Core use cases (consumers of this client):** `packages/core/` -- see `packages/core/AGENTS.md`