Client
How to create a Cairn client and use it to read and write content, generated from @cairn-cms/client's own TSDoc comments.
Functions
createClient
function createClient(config: CairnClientConfig): CairnClientconfig.apiUrl | string | optional | Override for local development; defaults to the hosted cairn API. |
config.dataset | string | optional | Which dataset to read from and write to. Reads default to the project's
default dataset as the API resolves it (including any delivery route
configured for it); writes default to "production". Point this at
another dataset's slug - e.g. a separate staging dataset - to serve a
site from it. A *branch* isn't publicly readable by its slug; preview a
branch through a preview session, or route a live dataset to it. |
config.projectId | string | required | The project's slug (see apps/api's auto-generated slugs) - visible in the dashboard's URL (app.cairn.press/<slug>/...) once the project is open. |
config.token | string | optional | A project API token (see the Dashboard's "API tokens" section), for
the write methods below - create/update/delete/publish/
unpublish. The read methods (fetch/fetchOne/fetchSingleton)
never need one; they only ever read already-public, published
content, the same as this client's image() URLs. |
Returns: CairnClient
createImageUrl
A single call that returns the built URL directly - no separate .url()
step, no network call, no token.
A bare call, or one passing only a *named* size preset, builds a URL
against cdn.cairn.press - a public bucket Cloudflare serves through a
small caching Worker (no apps/api request on a cache hit, which is the
overwhelming majority of traffic once anything's been requested once).
Passing fit/format/quality, or an explicit { w, h } pixel size,
falls back to the original, authenticated-capable API route (apiUrl)
instead - an arbitrary, caller-chosen transform doesn't have a bounded
set of keys to pre-generate or cache indefinitely the way a named preset
does (see apps/api's asset route for the same reasoning already applied
there).
dataset only matters for that transform fallback path (the CDN path
has no notion of dataset - only published content ever reaches it, and
publishing always targets whichever dataset originated the publish) -
defaults to "production", matching createClient's own dataset
option.
function createImageUrl(config: { apiUrl?: string; cdnUrl?: string; dataset?: string; projectId: string }): (source: ImageValue, options?: ImageUrlOptions) => stringconfig | { apiUrl?: string; cdnUrl?: string; dataset?: string; projectId: string } | required |
Returns: (source: ImageValue, options?: ImageUrlOptions) => string
createPreviewClient
The preview-mode counterpart to createClient (ROADMAP.md §4) - reads
draft (or a specific dataset's published) content through the
token-gated /preview/... routes instead of the public, published-only
/published/... ones. Meant to be used from the create-cairn
preview-route template's pages, not called directly by most integrators;
createClient's read methods remain the right choice for a live page
that was never meant to show unpublished content.
For asset URLs in preview mode, see createPreviewImageUrl in
image.ts - a separate function, not a method here, matching how
createImageUrl is already separate from createClient on the
published side.
function createPreviewClient(config: CairnPreviewClientConfig): CairnPreviewClientconfig.apiUrl | string | optional | Override for local development; defaults to the hosted cairn API. |
config.previewToken | string | required | A preview *session* token - what createPreviewHandler (see
packages/client/src/next.ts) stores in its HttpOnly cookie after
exchanging the single-use grant from the Dashboard's "Preview" action
URL param. Not the grant itself, and not something most integrating
code mints or handles directly - the preview-route template is
normally the only place this ever gets read from a cookie and passed
in. Short-lived (~2h) and revocable; a client built with an expired or
revoked session throws on first use, not at construction. This is the
same token createPreviewImageUrl (see image.ts) needs to build
authorized asset URLs - the two are separate calls since not every
page needs images. |
config.projectId | string | required |
Returns: CairnPreviewClient
createPreviewImageUrl
The preview-mode counterpart to createImageUrl - builds a
secure.cdn.cairn.press URL authorized by a short-lived, asset-scoped
token that's minted and cached automatically behind the scenes (and
refreshed as needed on later calls). Async, unlike createImageUrl's
imageUrl - unavoidably, since minting that token needs a network call
the plain published-URL case never does. There's still nothing to sign
or verify yourself: this is the only preview asset-URL surface this
client has.
Only a bare call or a named preset (see PreviewImageUrlOptions) -
apps/cdn-secure can only serve what's already been generated (at upload
time, unconditionally, regardless of publish state - so a preset is
rarely actually missing on this side), never an arbitrary transform.
function createPreviewImageUrl(config: PreviewImageUrlConfig): (source: ImageValue, options?: PreviewImageUrlOptions) => Promise<string>config.apiUrl | string | optional | Override for local development; defaults to the hosted cairn API. |
config.previewToken | string | required | The same preview *session* token createPreviewClient uses - see CairnPreviewClientConfig.previewToken's doc comment in client.ts. |
config.projectId | string | required | |
config.secureCdnUrl | string | optional | Override for local development; defaults to the hosted secure/preview asset CDN. |
Returns: (source: ImageValue, options?: PreviewImageUrlOptions) => Promise<string>
sanitizeLinkHref
The link target toHTML/<PortableText> will actually render: an
http(s)/mailto/tel URL or a relative path/fragment, otherwise null (the
link is dropped, its text kept). Content is written by editors and API
tokens, and a javascript: href in published rich text would otherwise
run script on the customer's own site when clicked. Custom link
renderers receive the same sanitized value.
function sanitizeLinkHref(href: string): string | nullhref | string | required |
Returns: string | null
toHTML
Renders Portable Text blocks to an HTML string - usable directly in any
frontend (drop it into innerHTML, or a framework's raw-HTML escape
hatch). For a React tree instead of a string, see @cairn-cms/client/react's
PortableText component.
function toHTML(blocks: PortableTextBlock[] | null | undefined, components?: PortableTextHtmlComponents): stringblocks | PortableTextBlock[] | null | undefined | required | |
components.block | Partial<Record<PortableTextBlockStyle, (children: string) => string>> | optional | |
components.list | Partial<Record<PortableTextListItem, (children: string) => string>> | optional | |
components.listItem | Partial<Record<PortableTextListItem, (children: string) => string>> | optional | |
components.marks | Partial<Record<PortableTextDecorator, (children: string) => string>> & { link?: (children: string, href: string) => string } | optional |
Returns: string
toPlainText
true to plain-text a Portable Text value even when it's missing the array wrapper - defensive against hand-authored/legacy content.
function toPlainText(blocks: PortableTextBlock[] | null | undefined): stringblocks | PortableTextBlock[] | null | undefined | required |
Returns: string
CairnClient
Returned by createClient().
create
Creates a new draft document. Requires token.
create<T>(type: string, data: T): Promise<Document<T>>type | string | required | |
data | T | required |
Returns: Promise<Document<T>>
delete
Deletes a document outright (draft and published data, and its version history). Requires token.
delete(type: string, id: string): Promise<void>type | string | required | |
id | string | required |
Returns: Promise<void>
fetch
Every published document of a type, ordered the same way the Dashboard
orders them - manual rank for orderable types, otherwise the type's own
configured default sort (the schema editor's "Sort Entries By"), falling
back to most recently published first if none is configured. Pass
options.orderBy to override this with an explicit order, or to filter,
select specific fields, resolve references, or paginate server-side -
see FetchOptions. Throws (rather than returning an empty array) if
options describes an invalid query, e.g. an unknown field name.
fetch<T>(type: string, options?: FetchOptions<T>): Promise<PublishedDocument<T>[]>type | string | required | |
options.include | string[] | optional | Document type identifiers to resolve inline wherever referenced. Resolution traverses nested objects/arrays but follows one reference hop. Top-level reference field names are also accepted for compatibility. |
options.limit | number | optional | Defaults to 100, capped at 200 server-side. |
options.locale | string | optional | Which locale a localized field (see the project's Settings) resolves
to - the requested locale's value if present, else the project's
default locale's. Omit to just get the default locale directly; every
localized field's raw { [locale]: value } map is always resolved
down to a plain value either way, so a project with no locales
configured needs nothing extra here at all. |
options.orderBy | { direction?: "asc" | "desc"; field: Extract<keyof T, string> | string }[] | optional | One or more sort terms, applied in order; direction defaults to "asc". |
options.select | (Extract<keyof T, string> | string)[] | optional | Only return these fields - dotted paths reach into nested object fields, e.g. "duration.start". Omit for the full document. |
options.skip | number | optional | |
options.where | { [K in keyof T]?: WhereValue<T[K]> } | optional | Filters, ANDed together. |
Returns: Promise<PublishedDocument<T>[]>
fetchOne
A single published document by id - null if it doesn't exist or has never been published.
fetchOne<T>(type: string, id: string, options?: FetchOneOptions): Promise<PublishedDocument<T> | null>type | string | required | |
id | string | required | |
options.include | string[] | optional | |
options.locale | string | optional |
Returns: Promise<PublishedDocument<T> | null>
fetchSingleton
The published version of a singleton document type - null if it's never been published.
fetchSingleton<T>(type: string, options?: FetchOneOptions): Promise<PublishedDocument<T> | null>type | string | required | |
options.include | string[] | optional | |
options.locale | string | optional |
Returns: Promise<PublishedDocument<T> | null>
publish
Publishes a document's current draft. Requires token.
publish<T>(type: string, id: string): Promise<Document<T>>type | string | required | |
id | string | required |
Returns: Promise<Document<T>>
unpublish
Unpublishes a document - it keeps its draft, but stops being served by the read methods above. Requires token.
unpublish<T>(type: string, id: string): Promise<Document<T>>type | string | required | |
id | string | required |
Returns: Promise<Document<T>>
update
Overwrites a document's draft data. Requires token.
update<T>(type: string, id: string, data: T): Promise<Document<T>>type | string | required | |
id | string | required | |
data | T | required |
Returns: Promise<Document<T>>
uploadAsset
Uploads a file as a new asset - use its id as an image/images field's { assetId }. Requires token.
uploadAsset(file: Blob, filename: string): Promise<UploadedAsset>file | Blob | required | |
filename | string | required |
Returns: Promise<UploadedAsset>
CairnPreviewClient
Returned by createPreviewClient().
fetch
Documents of a type in the session's dataset, at whichever of draft/published the session's view says - with the same filter/select/include/order/pagination options as CairnClient.fetch.
fetch<T>(type: string, options?: FetchOptions<T>): Promise<PreviewDocument<T>[]>type | string | required | |
options.include | string[] | optional | Document type identifiers to resolve inline wherever referenced. Resolution traverses nested objects/arrays but follows one reference hop. Top-level reference field names are also accepted for compatibility. |
options.limit | number | optional | Defaults to 100, capped at 200 server-side. |
options.locale | string | optional | Which locale a localized field (see the project's Settings) resolves
to - the requested locale's value if present, else the project's
default locale's. Omit to just get the default locale directly; every
localized field's raw { [locale]: value } map is always resolved
down to a plain value either way, so a project with no locales
configured needs nothing extra here at all. |
options.orderBy | { direction?: "asc" | "desc"; field: Extract<keyof T, string> | string }[] | optional | One or more sort terms, applied in order; direction defaults to "asc". |
options.select | (Extract<keyof T, string> | string)[] | optional | Only return these fields - dotted paths reach into nested object fields, e.g. "duration.start". Omit for the full document. |
options.skip | number | optional | |
options.where | { [K in keyof T]?: WhereValue<T[K]> } | optional | Filters, ANDed together. |
Returns: Promise<PreviewDocument<T>[]>
fetchOne
A single document by id, at the session's view - null if no document with that id/type exists in the session's dataset at all (not merely unpublished - see PreviewDocument's own doc comment for that case).
fetchOne<T>(type: string, id: string, options?: FetchOneOptions): Promise<PreviewDocument<T> | null>type | string | required | |
id | string | required | |
options.include | string[] | optional | |
options.locale | string | optional |
Returns: Promise<PreviewDocument<T> | null>
fetchSingleton
A singleton type's one document, auto-created server-side on first preview the same way the Dashboard's own editor does.
fetchSingleton<T>(type: string, options?: FetchOneOptions): Promise<PreviewDocument<T> | null>type | string | required | |
options.include | string[] | optional | |
options.locale | string | optional |
Returns: Promise<PreviewDocument<T> | null>
resolve
Resolves and validates the session server-side (it can't be checked client-side - verifying it needs a DB lookup that never leaves apps/api). Throws if the session is missing, invalid, expired, or revoked. Call this first, before rendering anything, the same way the preview-route template does to decide where to redirect.
resolve(): Promise<PreviewResolution>Returns: Promise<PreviewResolution>
Types
CairnClientConfig
apiUrl | string | optional | Override for local development; defaults to the hosted cairn API. |
dataset | string | optional | Which dataset to read from and write to. Reads default to the project's
default dataset as the API resolves it (including any delivery route
configured for it); writes default to "production". Point this at
another dataset's slug - e.g. a separate staging dataset - to serve a
site from it. A *branch* isn't publicly readable by its slug; preview a
branch through a preview session, or route a live dataset to it. |
projectId | string | required | The project's slug (see apps/api's auto-generated slugs) - visible in the dashboard's URL (app.cairn.press/<slug>/...) once the project is open. |
token | string | optional | A project API token (see the Dashboard's "API tokens" section), for
the write methods below - create/update/delete/publish/
unpublish. The read methods (fetch/fetchOne/fetchSingleton)
never need one; they only ever read already-public, published
content, the same as this client's image() URLs. |
CairnPreviewClientConfig
apiUrl | string | optional | Override for local development; defaults to the hosted cairn API. |
previewToken | string | required | A preview *session* token - what createPreviewHandler (see
packages/client/src/next.ts) stores in its HttpOnly cookie after
exchanging the single-use grant from the Dashboard's "Preview" action
URL param. Not the grant itself, and not something most integrating
code mints or handles directly - the preview-route template is
normally the only place this ever gets read from a cookie and passed
in. Short-lived (~2h) and revocable; a client built with an expired or
revoked session throws on first use, not at construction. This is the
same token createPreviewImageUrl (see image.ts) needs to build
authorized asset URLs - the two are separate calls since not every
page needs images. |
projectId | string | required |
DeliveredDocument
A delivered document from either API - _publishedAt is null only for
a never-published draft seen in preview. What @cairn-cms/client/next's
createClient returns, since one call can be served by either side.
type DeliveredDocument<T> = PreviewDocument<T>Document
A document as the write methods see it - the Dashboard-internal CRUD
shape, not the delivery one PublishedDocument/PreviewDocument use.
T's fields are the current draft, flat at the top level; the published
snapshot can't also be flat (it's a second, coexisting copy of the same
field names), so it nests under _published instead - null if this
document has never been published. Nested field values (reference/
image/video pointers) are the raw stored shape, untagged - unlike
delivery, the Dashboard already knows every field's type statically.
type Document<T> = T & { _createdAt: string; _id: string; _key: string; _modified: boolean; _published: T | null; _publishedAt: string | null; _rank: string | null; _singletonKey: string | null; _sourceDocumentId: string | null; _status: string; _updatedAt: string; _versionCount: number }FetchOneOptions
The subset of FetchOptions fetchOne/fetchSingleton accept.
include | string[] | optional | |
locale | string | optional |
FetchOptions
Options for fetch() - filters (ANDed together), field selection, one
one-hop reference resolution, sorting, and pagination. Compiles down to
the query params apps/api's /published/types/:type/documents route
accepts (see packages/db/src/publishedQuery.ts) - this just spares
callers from hand-building that query string themselves.
include | string[] | optional | Document type identifiers to resolve inline wherever referenced. Resolution traverses nested objects/arrays but follows one reference hop. Top-level reference field names are also accepted for compatibility. |
limit | number | optional | Defaults to 100, capped at 200 server-side. |
locale | string | optional | Which locale a localized field (see the project's Settings) resolves
to - the requested locale's value if present, else the project's
default locale's. Omit to just get the default locale directly; every
localized field's raw { [locale]: value } map is always resolved
down to a plain value either way, so a project with no locales
configured needs nothing extra here at all. |
orderBy | { direction?: "asc" | "desc"; field: Extract<keyof T, string> | string }[] | optional | One or more sort terms, applied in order; direction defaults to "asc". |
select | (Extract<keyof T, string> | string)[] | optional | Only return these fields - dotted paths reach into nested object fields, e.g. "duration.start". Omit for the full document. |
skip | number | optional | |
where | { [K in keyof T]?: WhereValue<T[K]> } | optional | Filters, ANDed together. |
ImageFit
type ImageFit = "cover" | "contain" | "fill" | "inside" | "outside" | "scale-down"ImageFormat
type ImageFormat = "webp" | "avif" | "jpeg" | "png"ImageSize
Either a named preset, or explicit pixel dimensions (h optional - omit to scale proportionally).
type ImageSize = ImageSizePreset | { h?: number; w: number }ImageSizePreset
A fixed, named width - matches apps/api's IMAGE_SIZE_PRESETS. Prefer
this over an explicit { w, h } where you can: Cloudflare bills per
*unique* (image, transform) combination per month, so a caller-chosen
arbitrary width makes cost scale with however many distinct sizes get
requested across every project on the platform. A preset means the same
source image only ever produces one of these variants, no matter who
asks - and it's the only form the API can serve from a pre-generated
file instead of a live (billed) transform, see apps/api's asset route.
type ImageSizePreset = "blur" | "thumbnail" | "small" | "medium" | "large"ImageUrlOptions
Transform options for imageUrl() - all optional, same query params apps/api's asset route accepts.
fit | ImageFit | optional | |
format | ImageFormat | optional | |
quality | number | optional | |
size | ImageSize | optional | A named preset (see ImageSizePreset), or explicit { w, h? } pixel dimensions for the rare case a preset doesn't fit. |
ImageValue
What an image field's value looks like in document data - see @cairn-cms/schema's validator.
assetId | string | required | |
filename | string | optional |
PortableTextBlock
_key | string | required | |
_type | "block" | required | |
children | PortableTextSpan[] | required | |
level | number | optional | |
listItem | PortableTextListItem | optional | |
markDefs | PortableTextMarkDef[] | required | |
style | PortableTextBlockStyle | required |
PortableTextBlockStyle
type PortableTextBlockStyle = "normal" | "h1" | "h2" | "h3" | "h4" | "blockquote"PortableTextDecorator
type PortableTextDecorator = "strong" | "em" | "underline" | "strike-through" | "code"PortableTextHtmlComponents
Per-node renderers toHTML falls back to its plain-tag defaults when
unset - matching @portabletext/to-html's components shape, so
swapping in e.g. Tailwind classes or custom tags per block/mark type
works the same way it does with that package.
block | Partial<Record<PortableTextBlockStyle, (children: string) => string>> | optional | |
list | Partial<Record<PortableTextListItem, (children: string) => string>> | optional | |
listItem | Partial<Record<PortableTextListItem, (children: string) => string>> | optional | |
marks | Partial<Record<PortableTextDecorator, (children: string) => string>> & { link?: (children: string, href: string) => string } | optional |
PortableTextListItem
type PortableTextListItem = "bullet" | "number"PortableTextMarkDef
_key | string | required | |
_type | "link" | required | |
href | string | required |
PortableTextSpan
Portable Text types and parsing - the frontend counterpart to a richText
field (see @cairn-cms/schema's PortableTextBlock, which this mirrors).
Deliberately redefined here rather than imported from @cairn-cms/schema:
@cairn-cms/client has no dependency on @cairn-cms/schema (it's meant to
be a light, standalone package any frontend can install, the same reason
image.ts's createImageUrl doesn't depend on it either), and
this shape is a stable data contract either way.
_key | string | required | |
_type | "span" | required | |
marks | string[] | optional | |
text | string | required |
PreviewDocument
A document as a preview client sees it (ROADMAP.md §4) - T's fields
are flat at the top level, sourced from whichever of draft or published
the preview token's view says. Unlike PublishedDocument, never "not
found" for an existing document just because it isn't live - that's the
whole point of preview.
The one case this type can't fully capture: view: "published" on a
document that's never actually been published has nothing to show, so
T's fields are simply absent from the response at runtime (only _id/
_key/_publishedAt are present) despite this type saying they're
there - check _publishedAt for null first if a project might preview
unpublished content this way.
type PreviewDocument<T> = T & { _id: string; _key: string; _publishedAt: string | null }PreviewImageUrlConfig
apiUrl | string | optional | Override for local development; defaults to the hosted cairn API. |
previewToken | string | required | The same preview *session* token createPreviewClient uses - see CairnPreviewClientConfig.previewToken's doc comment in client.ts. |
projectId | string | required | |
secureCdnUrl | string | optional | Override for local development; defaults to the hosted secure/preview asset CDN. |
PreviewImageUrlOptions
imageUrl()'s only option in preview mode - a named preset, or omit for the original. Unlike createImageUrl, there's no API-route fallback for anything else (no apiUrl transform path exists on the preview side) - fit/format/quality/explicit dimensions simply aren't offered here.
size | ImageSizePreset | optional |
PreviewResolution
What GET .../preview/resolve reports once a token verifies - see apps/api's routes/preview.ts. documentType/documentId are null for a token minted without one (e.g. previewing a whole dataset/branch rather than jumping to one document).
datasetId | string | required | |
documentId | string | null | required | |
documentType | string | null | required | |
view | "draft" | "published" | required |
PublishedDocument
A published document - T's fields sit flat at the top level alongside
this fixed, underscore-prefixed metadata (never colliding with a content
field name - the schema rejects a leading underscore on one). Matches
apps/api's delivery envelope exactly; see packages/db's
shapeDeliveryData/buildEnvelope for where this shape comes from.
_id is the document *type's* API identifier (e.g. "post") - the same
for every document of that type; _key is this specific document's own
unique instance id. Mirrors how an array/blockRef item's own _id/_key
pair works.
type PublishedDocument<T> = T & { _id: string; _key: string; _publishedAt: string }UploadedAsset
What apps/api's POST /assets returns - id is what an image field's { assetId } points at.
filename | string | required | |
height | number | null | required | |
id | string | required | |
mimeType | string | required | |
size | number | required | |
width | number | null | required |
WhereOp
A single filter's comparison operator, matching apps/api's fields.<path>[<op>] query param.
type WhereOp<V> = { eq: V } | { ne: V } | { gt: V } | { gte: V } | { lt: V } | { lte: V } | { in: V[] } | { match: string }WhereValue
A bare value means equality - { category: "edits" } is shorthand for { category: { eq: "edits" } }, same convention as Prisma/Drizzle's own where shape.
type WhereValue<V> = V | WhereOp<V>