Skip to content

unchainedshop/cockpit-api

Repository files navigation

Cockpit API

npm version CI License: MIT Node.js

A TypeScript client for interacting with Cockpit CMS, including GraphQL requests, content management, and schema stitching support.

Installation

npm install --save @unchainedshop/cockpit-api

Package Exports

This package provides three entry points:

Export Description
@unchainedshop/cockpit-api Full-featured async API client with caching and response transformation
@unchainedshop/cockpit-api/schema GraphQL schema stitching utilities
@unchainedshop/cockpit-api/fetch Lightweight client for edge/RSC environments

Quick Start

Main Client

import { CockpitAPI } from '@unchainedshop/cockpit-api';

// With explicit endpoint
const cockpit = await CockpitAPI({
  endpoint: 'https://your-cockpit-instance.com/api/graphql',
});

// Or using environment variables
const cockpit = await CockpitAPI();  // Uses COCKPIT_GRAPHQL_ENDPOINT

Lightweight Fetch Client (Edge/RSC)

import { createFetchClient } from '@unchainedshop/cockpit-api/fetch';

// Synchronous initialization - no await needed
const cockpit = createFetchClient({
  endpoint: process.env.NEXT_PUBLIC_COCKPIT_ENDPOINT,
  tenant: 'mytenant',
  cache: 'force-cache',  // Uses platform caching
});

const page = await cockpit.pageByRoute('/about', { locale: 'en' });

GraphQL Schema Stitching

import { makeCockpitGraphQLSchema } from '@unchainedshop/cockpit-api/schema';
import { stitchSchemas } from '@graphql-tools/stitch';

const cockpitSchema = await makeCockpitGraphQLSchema({
  tenantHeader: 'x-cockpit-space',
  filterMutations: true,
});

const gatewaySchema = stitchSchemas({
  subschemas: [{ schema: cockpitSchema }],
});

Main Client API

GraphQL Requests

import { gql } from 'graphql-tag';

const query = gql`
  query {
    allPosts {
      title
      content
    }
  }
`;

const result = await cockpit.graphQL(query, {});

Content Operations

// Get a single content item
const post = await cockpit.getContentItem({ model: 'posts', id: '123' });

// With locale and field selection
const localizedPost = await cockpit.getContentItem({
  model: 'posts',
  id: '123',
  locale: 'en',
  queryParams: { fields: { title: 1, content: 1 } }
});

// Get multiple content items - always returns { data, meta? }
const response = await cockpit.getContentItems('posts', {
  limit: 10,
  sort: { _created: -1 },
  filter: { published: true }
});
// response: { data: Post[], meta?: { total: number } } | null

// Access items and metadata
const items = response?.data || [];
const total = response?.meta?.total;

// Get tree structure
const tree = await cockpit.getContentTree('categories', {
  parent: 'root-id',
  populate: 2
});

// Aggregation pipeline
const stats = await cockpit.getAggregateModel({
  model: 'orders',
  pipeline: [{ $group: { _id: '$status', count: { $sum: 1 } } }]
});

// Create content item
const newPost = await cockpit.postContentItem('posts', { title: 'New Post' });

// Delete content item
await cockpit.deleteContentItem('posts', '123');

Pages

// List pages - always returns { data, meta? }
const response = await cockpit.pages({ locale: 'en', limit: 50 });
const allPages = response?.data || [];
const total = response?.meta?.total;

// Get page by ID
const page = await cockpit.pageById({ page: 'blog', id: '123', locale: 'en' });

// Get page by route
const aboutPage = await cockpit.pageByRoute('/about', { locale: 'en', populate: 2 });

Menus

// Get all menus
const menus = await cockpit.pagesMenus({ locale: 'en' });

// Get specific menu
const mainMenu = await cockpit.pagesMenu('main-navigation', { locale: 'en' });

Routes & Sitemap

const routes = await cockpit.pagesRoutes('en');
const sitemap = await cockpit.pagesSitemap();
const settings = await cockpit.pagesSetting('en');
const fullRoute = await cockpit.getFullRouteForSlug('my-slug');

Search (Detektivo addon)

const results = await cockpit.search({
  index: 'products',
  q: 'search term',
  limit: 10,
  offset: 0
});

Localization (Lokalize addon)

const translations = await cockpit.localize('my-project', {
  locale: 'en',
  nested: true
});

Assets

import { ImageSizeMode, MimeType } from '@unchainedshop/cockpit-api';

// Get asset metadata
const asset = await cockpit.assetById('asset-id');

// Get transformed image
const image = await cockpit.imageAssetById('asset-id', {
  m: ImageSizeMode.BestFit,
  w: 800,
  h: 600,
  q: 80,
  mime: MimeType.WEBP
});

System

// Health check
const health = await cockpit.healthCheck();

// Clear cache (async in v2.2.0+)
await cockpit.clearCache();  // Clear all
await cockpit.clearCache('pages');  // Clear by pattern

Lightweight Fetch Client API

The fetch client is designed for edge/RSC environments with minimal overhead:

import { createFetchClient } from '@unchainedshop/cockpit-api/fetch';

const cockpit = createFetchClient({
  endpoint: process.env.NEXT_PUBLIC_COCKPIT_ENDPOINT,
  tenant: 'mytenant',
  cache: 'force-cache',
  apiKey: 'your-api-key',
  headers: { 'X-Custom-Header': 'value' }
});

// Available methods
const page = await cockpit.pageByRoute('/about', { locale: 'en' });

// List methods return { data, meta? }
const pagesResponse = await cockpit.pages({ locale: 'en' });
const pages = pagesResponse?.data || [];

const pageById = await cockpit.pageById('blog', '123', { locale: 'en' });

const itemsResponse = await cockpit.getContentItems('news', { locale: 'en', limit: 10 });
const items = itemsResponse?.data || [];

const item = await cockpit.getContentItem('news', '123', { locale: 'en' });
const custom = await cockpit.fetchRaw('/custom/endpoint', { param: 'value' });

Schema Stitching API

For building GraphQL gateways with Cockpit:

import { makeCockpitGraphQLSchema, createRemoteExecutor } from '@unchainedshop/cockpit-api/schema';

// Create schema for stitching
const schema = await makeCockpitGraphQLSchema({
  tenantHeader: 'x-cockpit-space',
  filterMutations: true,
  transforms: [],  // Additional GraphQL transforms
  extractTenant: (ctx) => ctx.req?.headers['x-tenant'],
  cockpitOptions: {
    endpoint: 'https://cms.example.com/api/graphql',
    apiKey: 'your-api-key',
    useAdminAccess: true
  }
});

// Or use the executor directly for custom implementations
const executor = createRemoteExecutor({
  tenantHeader: 'x-cockpit-space',
  cockpitOptions: { endpoint: '...' }
});

Configuration Options

const cockpit = await CockpitAPI({
  endpoint: 'https://...',      // Falls back to COCKPIT_GRAPHQL_ENDPOINT
  tenant: 'mytenant',           // Optional: for multi-tenant setups
  apiKey: 'your-api-key',       // Falls back to COCKPIT_SECRET env var
  useAdminAccess: true,         // Optional: inject api-Key header
  defaultLanguage: 'de',        // Language that maps to Cockpit's "default" locale (default: "de")
  preloadRoutes: true,          // Optional: preload route replacements
  cache: {
    max: 100,                   // Falls back to COCKPIT_CACHE_MAX (default: 100)
    ttl: 100000,                // Falls back to COCKPIT_CACHE_TTL (default: 100000)
    store: customStore,         // Optional: custom async cache store (Redis, Keyv, etc.)
  },
  // Or disable caching entirely
  // cache: false,
});

Environment Variables

COCKPIT_GRAPHQL_ENDPOINT=https://your-cockpit-instance.com/api/graphql
COCKPIT_SECRET=your-api-key                # Default API key
COCKPIT_SECRET_MYTENANT=tenant-api-key     # Tenant-specific API key
COCKPIT_CACHE_MAX=100                      # Max cache entries (default: 100)
COCKPIT_CACHE_TTL=100000                   # Cache TTL in ms (default: 100000)

Multi-Tenant Support

// Tenant-specific client
const cockpit = await CockpitAPI({
  endpoint: 'https://cms.example.com/api/graphql',
  tenant: 'mytenant',  // Requests use /:mytenant/api/... path
});

// Resolve tenant from URL
import { resolveTenantFromUrl, getTenantIds } from '@unchainedshop/cockpit-api';

const { tenant, slug } = resolveTenantFromUrl('https://mytenant.example.com/page');
const allTenants = getTenantIds();  // From COCKPIT_SECRET_* env vars

Custom Cache Stores

v2.2.0+ supports pluggable async cache stores for Redis, Keyv, or custom implementations:

import { createClient } from 'redis';
import type { AsyncCacheStore } from '@unchainedshop/cockpit-api';

// Redis example
const redisClient = createClient({ url: process.env.REDIS_URL });
await redisClient.connect();

const redisStore: AsyncCacheStore = {
  async get(key: string) {
    const value = await redisClient.get(key);
    return value ? JSON.parse(value) : undefined;
  },
  async set(key: string, value: unknown) {
    await redisClient.set(key, JSON.stringify(value), { EX: 100 });
  },
  async clear(pattern?: string) {
    if (pattern) {
      const keys = await redisClient.keys(`${pattern}*`);
      if (keys.length > 0) await redisClient.del(keys);
    } else {
      await redisClient.flushDb();
    }
  }
};

const cockpit = await CockpitAPI({
  endpoint: 'https://cms.example.com/api/graphql',
  cache: { store: redisStore }
});

Response Format (v3.0.0+)

All list methods return a consistent response format regardless of parameters:

interface CockpitListResponse<T> {
  data: T[];
  meta?: CockpitListMeta;  // Present when using pagination (skip parameter)
}

Methods with Consistent Response Format

  • getContentItems() - Always returns CockpitListResponse<T> | null
  • pages() - Always returns CockpitListResponse<T> | null
  • Fetch client methods - Always return CockpitListResponse<T> | null

Usage Example

import type { CockpitListResponse } from '@unchainedshop/cockpit-api';

// Always get { data, meta? } format
const response = await cockpit.getContentItems('posts', { limit: 10, skip: 0 });

// Access items
const items = response?.data || [];

// Access metadata (available when using skip parameter)
const total = response?.meta?.total;

Benefits:

  • No need to check if response is array or object
  • Predictable type signatures
  • Easier to work with pagination

TypeScript Support

import type {
  // Client
  CockpitAPIClient,
  CockpitAPIOptions,
  CacheManager,
  CacheOptions,
  AsyncCacheStore,

  // Query Options
  ContentItemQueryOptions,
  ContentListQueryOptions,
  TreeQueryOptions,
  PageQueryOptions,
  SearchQueryOptions,
  ImageAssetQueryParams,

  // Response Types
  CockpitPage,
  CockpitAsset,
  CockpitMenu,
  CockpitRoute,
  CockpitSearchResult,
  CockpitContentItem,
  CockpitListResponse,   // New: for paginated content responses
  CockpitListMeta,       // New: metadata in paginated responses

  // Schema Types
  MakeCockpitSchemaOptions,
  CockpitExecutorContext,

  // Fetch Types
  FetchClientOptions,
  FetchCacheMode,
} from '@unchainedshop/cockpit-api';

import { ImageSizeMode, MimeType } from '@unchainedshop/cockpit-api';

Breaking Changes

v2.0.0

  • lokalize() renamed to localize()
  • Methods use options objects instead of positional parameters
  • HTTP errors now throw instead of returning null (404 still returns null)
  • Each client instance has its own cache (no shared state)

v2.1.0 (New Features)

  • /schema subpackage for GraphQL schema stitching
  • /fetch subpackage for lightweight edge/RSC environments
  • preloadRoutes option for preloading route replacements
  • defaultLanguage option to configure which language maps to Cockpit's "default" locale
  • Expanded tenant utilities: resolveTenantFromUrl(), resolveTenantFromSubdomain()

v2.2.0 (Breaking Changes)

Async Cache Operations:

  • All cache operations are now async and return Promises
  • await cockpit.clearCache() is now required (was synchronous in v2.1.x)
  • Custom cache stores can be provided via cache.store option
  • Cache can be explicitly disabled with cache: false

Migration:

// Before (v2.1.x)
cockpit.clearCache();
cockpit.clearCache('ROUTE');

// After (v2.2.0)
await cockpit.clearCache();
await cockpit.clearCache('ROUTE');

v3.0.0 (Breaking Changes)

Consistent List Response Format:

All list methods now return CockpitListResponse<T> | null instead of varying between arrays and wrapped responses:

Changed Methods:

  • getContentItems() - Now always returns { data: T[], meta?: {...} } | null
  • pages() - Now always returns { data: T[], meta?: {...} } | null
  • Fetch client getContentItems() and pages() - Now always return { data: T[], meta?: {...} } | null

Migration:

// Before (v2.x)
const items = await cockpit.getContentItems('posts', { limit: 10 });
// items could be Post[] or null

const pages = await cockpit.pages({ limit: 10 });
// pages could be Page[] or null

// After (v3.0.0)
const itemsResponse = await cockpit.getContentItems('posts', { limit: 10 });
const items = itemsResponse?.data || [];
const total = itemsResponse?.meta?.total;

const pagesResponse = await cockpit.pages({ limit: 10 });
const pages = pagesResponse?.data || [];
const total = pagesResponse?.meta?.total;

Benefits:

  • Single, predictable return type for all list methods
  • No need to check Array.isArray() or normalize responses
  • Cleaner TypeScript types
  • Metadata always accessible via .meta property

TreeQueryOptions Type Correction:

TreeQueryOptions no longer incorrectly includes limit and skip parameters (which were always ignored). Tree structures use parent, populate, filter, and fields instead.

// Before (v2.x) - allowed but ignored
await cockpit.getContentTree('categories', { limit: 10 });  // ❌ TypeScript allowed this

// After (v3.0.0) - TypeScript prevents invalid usage
await cockpit.getContentTree('categories', {
  parent: 'root-id',  // ✅ Correct
  populate: 2,        // ✅ Correct
  filter: { active: true }  // ✅ Correct
});

Peer Dependencies

  • graphql (optional) - Required for the graphQL() method
  • @graphql-tools/wrap (optional) - Required for the /schema subpackage

Contributing

Contributions are welcome! Please open an issue or submit a pull request.

License

This project is licensed under the MIT License. See the LICENSE file for details.

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Packages

 
 
 

Contributors