npm package: @nexera/delivery
Description: Nexera CMS Delivery SDK — read content and query via GraphQL
Latest version: 0.1.0

# @nexera/delivery

Nexera CMS Delivery SDK — read published content and query via GraphQL.

## Installation

```bash
npm install @nexera/delivery
```

**Requirements:** Node.js 18+ (uses the global `fetch` API). Works in browsers, edge runtimes, and Node.js.

## Quick Start

```ts
import { createDeliveryClient } from '@nexera/delivery';

const delivery = createDeliveryClient({
  baseUrl: 'https://delivery.yourcms.com',
  deliveryToken: 'dlt_abc123...',
  stackId: 'your-stack-uuid',
  tenantId: 'your-tenant-uuid',
  environmentId: 'your-environment-uuid',
});

// Typed entry access
interface BlogPost {
  title: string;
  body: string;
  slug: string;
}

const post = await delivery.entries.get<BlogPost>('content-model-id', 'entry-id');
console.log(post.data.title); // fully typed
```

## Configuration

```ts
createDeliveryClient({
  baseUrl: string;           // Delivery API base URL
  deliveryToken: string;     // Environment-scoped delivery token
  stackId: string;           // Stack UUID
  tenantId: string;          // Tenant UUID
  environmentId: string;     // Environment UUID
  defaultLocale?: string;    // Default locale for all queries (e.g., 'en')
  fetch?: typeof fetch;      // Custom fetch (for polyfills or testing)
  fetchOptions?: RequestInit; // Default fetch options (e.g., Next.js revalidate)
})
```

## Entries

### `delivery.entries.get<T>(contentModelId, entryId, options?)`

Fetch a single published entry by ID.

```ts
const entry = await delivery.entries.get<BlogPost>('model-id', 'entry-id');
console.log(entry.data.title);
console.log(entry.layout); // LayoutNode[] for page template entries
```

Options: `preview` (boolean), `renderMode` (string).

### `delivery.entries.list<T>(contentModelId, options?)`

List entries with pagination.

```ts
const result = await delivery.entries.list<BlogPost>('model-id', {
  page: 1,
  limit: 10,
  locale: 'en',
});

console.log(result.items);      // DeliveryEntry<BlogPost>[]
console.log(result.totalCount);
```

Options: `page`, `limit`, `locale`, `preview`.

### `delivery.entries.query<T>(contentModelId, options?)`

Query entries with advanced filters via POST.

```ts
const result = await delivery.entries.query<BlogPost>('model-id', {
  locale: 'fr',
  preview: false,
  page: 1,
  limit: 20,
});
```

### `delivery.entries.listAll<T>(contentModelId, options?)`

Auto-paginate through all entries. Returns an async generator.

```ts
for await (const page of delivery.entries.listAll<BlogPost>('model-id')) {
  for (const entry of page) {
    console.log(entry.data.title);
  }
}
```

### `delivery.entries.getFirst<T>(contentModelId, options?)`

Get the first entry. Useful for singleton content models (site settings, homepage).

```ts
const settings = await delivery.entries.getFirst<SiteSettings>('settings-model-id');
```

## GraphQL

The CMS generates a dynamic GraphQL schema based on your content models.

### `delivery.graphql.query<T>(query, variables?, options?)`

```ts
const result = await delivery.graphql.query<{ blogPosts: BlogPost[] }>(`
  query {
    blogPosts(limit: 10) {
      title
      slug
      publishedAt
    }
  }
`);
console.log(result.blogPosts);
```

With variables:

```ts
const result = await delivery.graphql.query(
  `query GetPost($slug: String!) { blogPost(slug: $slug) { title body } }`,
  { slug: 'hello-world' },
);
```

Using the page-types endpoint:

```ts
const result = await delivery.graphql.query(
  `{ pageTypes { name } }`,
  undefined,
  { endpoint: 'page-types' },
);
```

### `delivery.graphql.getSchema(endpoint?)`

Fetch the raw GraphQL schema (SDL).

```ts
const sdl = await delivery.graphql.getSchema();
```

## Pagination

```ts
import { extractPaginationMeta } from '@nexera/delivery';

const result = await delivery.entries.list('model-id', { page: 2, limit: 10 });
const meta = extractPaginationMeta(result);

console.log(meta.totalPages);     // e.g., 5
console.log(meta.hasNextPage);    // true
console.log(meta.hasPreviousPage); // true
```

## Error Handling

```ts
import {
  CmsError,
  CmsAuthenticationError,
  CmsNotFoundError,
  CmsRateLimitError,
  CmsGraphQLError,
} from '@nexera/delivery';

try {
  await delivery.entries.get('model-id', 'entry-id');
} catch (error) {
  if (error instanceof CmsNotFoundError) {
    console.log(`${error.resourceType} not found: ${error.resourceId}`);
  } else if (error instanceof CmsAuthenticationError) {
    console.log('Check your delivery token');
  } else if (error instanceof CmsRateLimitError) {
    console.log(`Retry after ${error.retryAfterMs}ms`);
  } else if (error instanceof CmsGraphQLError) {
    console.log('GraphQL errors:', error.graphqlErrors);
  } else if (error instanceof CmsError) {
    console.log(`HTTP ${error.statusCode}:`, error.responseBody);
  }
}
```

## Next.js Example

```ts
// app/blog/page.tsx
import { createDeliveryClient } from '@nexera/delivery';

const delivery = createDeliveryClient({
  baseUrl: process.env.DELIVERY_API_URL!,
  deliveryToken: process.env.DELIVERY_TOKEN!,
  stackId: process.env.STACK_ID!,
  tenantId: process.env.TENANT_ID!,
  environmentId: process.env.ENVIRONMENT_ID!,
  fetchOptions: { next: { revalidate: 60 } },
});

export default async function BlogPage() {
  const { items: posts } = await delivery.entries.list<BlogPost>('blog-model-id');
  return (
    <ul>
      {posts.map((post) => (
        <li key={post.id}>{post.data.title}</li>
      ))}
    </ul>
  );
}
```

## Related

- **[@nexera/management](https://www.npmjs.com/package/@nexera/management)** — Full CRUD for entries, content models, assets, and more.

## License

MIT