@airdraft/core
v0.1.23
Published
Airdraft core engine — schema, collections, slug/publish semantics
Readme
@airdraft/core
Runtime engine for Airdraft — the git-native headless CMS. Provides the storage adapters, collection engine, field validation, plugin system, and all shared TypeScript types used across the monorepo.
Installation
npm install @airdraft/coreExports
Engine
The CMS engine is instantiated internally by @airdraft/next. You interact with it via the server client returned by createCmsClient(). The class is exported for adapter authors:
| Export | Description |
|---|---|
| CmsEngine | Core engine class. Methods: listEntries, getEntry, createEntry, updateEntry, deleteEntry, getSchema, getCollection. |
Configuration
| Export | Description |
|---|---|
| defineConfig(config) | Type-safe helper to define a CmsConfig. Pass your adapter, collections (or schemaPath), plugins, basePath, and defaultLocale. |
| asCollectionConfig(raw) | Safely casts a JSON schema import to CollectionConfig. Use when importing airdraft.schema.json at runtime. |
Field Validation
| Export | Description |
|---|---|
| validateFields(data, fields) | Validates a record against a FieldMap. Returns ValidationError[]. |
| validateField(name, value, config) | Validates a single field value against its FieldConfig. |
Supported field types
string · text · number · boolean · date · datetime · rich-text · media · url · list · select · multiselect · relation · relations · object · blocks · image (deprecated — use media)
rich-textstores Markdown/MDX body content and contributes towordCount.textstores multi-line plain text (textarea).mediasupportsmultiple: truefor multi-file fields andacceptfor MIME filtering.urlvalidates absolute URLs (https://…) and root-relative paths (/…).blocksembeds a sub-schema as a structured object or repeatable array. Sub-fields are defined viafields: Record<string, FieldConfig>.datetimestores an ISO-8601 datetime string.
Storage Adapters
| Export | Description |
|---|---|
| LocalAdapter | Reads/writes MDX/MD/JSON/YAML files on the local filesystem. Used in development. |
| GitHubAdapter | Reads/writes via the GitHub Contents API. Used in production/serverless. |
Errors
| Export | Description |
|---|---|
| ValidationError | Thrown when field validation fails (HTTP 422). Carries details: Array<{ field, message }>. |
| EntryNotFoundError | Thrown when a specific entry does not exist. |
| CollectionNotFoundError | Thrown when a collection name is not registered. |
| SlugConflictError | Thrown when creating an entry whose slug already exists. |
| ConflictError | Thrown on SHA mismatch (optimistic concurrency). |
| UnauthorizedError | Thrown when the request lacks valid credentials (HTTP 401). |
| ForbiddenError | Thrown when the actor lacks sufficient role permissions (HTTP 403). |
| GitHubError | Thrown when the GitHub API returns an unexpected error. |
Types
All shared TypeScript types are exported from this package:
CmsConfig · CollectionConfig · CollectionMap · CmsSchema · FieldType · FieldConfig · StorageAdapter · Plugin · Entry · RichEntry · EntrySibling · RichListResult · FileResult · WriteOptions · DeleteOptions · FileListItem · AuditEvent · MediaItem · InferCollectionData<C>
Use InferCollectionData<C> with asCollectionConfig() to get fully typed entry data from a collection config:
import { asCollectionConfig, InferCollectionData } from '@airdraft/core'
import schema from './airdraft.schema.json'
const posts = asCollectionConfig(schema.collections.posts)
type PostData = InferCollectionData<typeof posts>AuditEvent.error includes an optional details array (Array<{ field: string; message: string }>) that carries per-field validation failures.
Rich entry types
| Type | Description |
|---|---|
| RichEntry<TData> | Extends Entry with wordCount, readTime, and prev/next sibling navigation. Returned by all read paths. |
| EntrySibling<TData> | Lightweight stub returned as prev/next on RichEntry. Contains slug, a subset of data fields, wordCount, and readTime. |
| RichListResult<TData> | Returned by listEntries. Includes entries, total, page, pages, hasNext, hasPrev. |
| ListEntriesResult | Deprecated. Alias for RichListResult. |
CollectionConfig reference
interface CollectionConfig {
path: string // glob pattern, e.g. 'content/posts/**'
label?: string // human-readable name shown in the UI
titleField?: string // field used as the display title (defaults to 'title')
fields: Record<string, FieldConfig>
format: 'mdx' | 'md' | 'json' | 'yaml'
defaultSort?: SortField | SortField[]
slugSource?: string // field to derive slug from on creation
previewUrl?: string // preview URL template, e.g. '/blog/{slug}'
publish?: boolean // enable draft/published state
wordCountFields?: string[] // override which fields contribute to wordCount
siblingFields?: string[] // fields included in prev/next sibling stubs
calculateReadTime?: (wc: number) => string
storeComputedFields?: boolean
}| Option | Type | Default | Description |
|---|---|---|---|
| titleField | string | 'title' | Field used as the display name in the editor's entry list. |
| format | 'mdx' \| 'md' \| 'json' \| 'yaml' | required | File format for entries. Both md and mdx are parsed as Markdown; mdx additionally allows JSX. |
| wordCountFields | string[] | all rich-text fields (+ body for MDX/MD) | Fields whose text content is summed for wordCount. |
| siblingFields | string[] | all non-rich-text, non-text fields | Fields included in sibling stubs. |
| calculateReadTime | (wc: number) => string | 200 wpm | Custom read-time estimator. |
| storeComputedFields | boolean | false | When true, writes _computed: { wordCount, readTime } to frontmatter on create/update. |
ListOptions additions
| Option | Type | Description |
|---|---|---|
| page | number | 1-based page number. Converted to offset = (page - 1) * limit. Takes precedence over offset when both are provided. |
GetOptions additions
| Option | Type | Description |
|---|---|---|
| siblings | boolean \| SiblingsOptions | When true, resolves prev/next adjacent entries in the collection. Pass an object to override sort, status, filter, or fields. |
SiblingsOptions shape:
{
sort?: SortField | SortField[]
status?: 'published' | 'draft' | 'all'
filter?: Record<string, unknown | FilterOperator>
fields?: string[] // per-call override of CollectionConfig.siblingFields
}Content utilities
| Export | Description |
|---|---|
| countWords(markdown) | Counts words in a Markdown/MDX string (strips code blocks, images, links, and headings before counting). |
| defaultReadTime(wordCount) | Returns a read-time string at 200 wpm, e.g. "3 min read". |
Plugin API
import { defineConfig } from '@airdraft/core'
import type { Plugin, CmsSchema } from '@airdraft/core'
const myPlugin: Plugin = {
name: 'my-plugin',
// Mutate the effective schema (add/remove/modify fields or collections)
schema(base: CmsSchema): CmsSchema {
return base // or return a mutated copy
},
middleware(req) {
// Runs before routing — throw UnauthorizedError / ForbiddenError to block
},
transformResponse(req, res) {
// Called after every successful response; may mutate headers
return res
},
hooks: {
transformEntry(entry, collection) {
// Enrich entries — inject resolved URLs, computed fields, etc.
return entry
},
onAuditEvent(event) {
// Receives a wide event for every completed CMS request.
// event.error?.details contains per-field validation errors.
console.log(event)
},
},
}Plugin interface reference:
| Member | Type | Description |
|---|---|---|
| name | string | Unique plugin identifier |
| schema? | (schema: CmsSchema) => CmsSchema | Mutate the effective schema — add fields, inject shared types |
| typeContributions? | Record<string, CollectionConfig> | Register reusable sub-schemas by name for blocks fields |
| middleware? | (req) => void \| Promise<void> | Pre-routing auth / rate-limit enforcement |
| transformResponse? | (req, res) => res | Post-response header mutation (e.g. Set-Cookie) |
| hooks.transformEntry? | (entry, collection) => entry | Enrich entries on every read |
| hooks.onAuditEvent? | (event: AuditEvent) => void \| Promise<void> | Wide-event audit logging |
Changelog
See CHANGELOG.md.
