218 lines
7.2 KiB
Markdown
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`
|