Справочник

TypeScript SDK

@citron/sdk е малък клиент без зависимости по време на изпълнение. Работи на Node 18.17+, в edge среди и в браузъра и се доставя като ESM и CommonJS.

Инсталиране#

SDK все още не е в npm. Изпращаме го като архив на пакета: пишете ни и ще го споделим с екипа ви.

bash
# Not on npm yet: install the package tarball we send you
npm install ./citron-sdk-0.1.0.tgz
ИмпортКакво съдържа
@citron/sdkcreateClient, грешките, типовете, generateTypes и помощниците за уебкуки.
@citron/sdk/webhooksverifyWebhook, constructWebhookEvent, signWebhookBody и нищо друго, за най-малки edge пакети.
@citron/sdk/nextcitronTags и createRevalidateHandler за App Router на Next.js.

Създаване на клиент#

lib/citron.ts
import { createClient } from '@citron/sdk';

export const citron = createClient({
  baseUrl: 'https://api.console.citronos.com', // "…/v1" is accepted too
  apiKey: process.env.CITRON_API_KEY,          // ck_pub_… (content and GraphQL only)
  locale: 'bg',                                // flatten localized fields to Bulgarian
  richtext: 'html',                            // html | json | both (API default: both)
  orgSlug: 'your-org',                         // for contact, forms and booking helpers
  timeoutMs: 10_000,                           // per attempt
  retry: { retries: 2, baseDelayMs: 300, maxDelayMs: 10_000 }, // or false
  cache: false,                                // true / { ttlMs } for an in-memory ETag cache
});
НастройкаПо подразбиране
baseUrlзадължителноАдресът на API. Приема се и завършек /v1.
apiKeyнямаck_pub_… или ck_prev_…. Нужен е за съдържанието и GraphQL, не и за контакт, формуляри и срещи.
localeнямаПреведените полета идват на този език при всяко четене на съдържание.
previewfalseЧете черновите (status=draft). Изисква ключ ck_prev_; с ключ ck_pub_ клиентът отказва да се създаде.
richtextспоред API (both)html, json или both.
orgSlugнямаSlug на работното пространство за помощниците за контакт и срещи.
timeoutMs10000За всеки опит; 0 го изключва.
retry{ retries: 2, baseDelayMs: 300, maxDelayMs: 10000 }Или false.
cachefalsetrue проверява отново с ETag; { ttlMs } освен това връща от паметта за толкова време.
headers, fetchOptions, fetchнямаИзпращат се с всяка заявка; fetch заменя globalThis.fetch.

Четене на съдържание#

МетодВикаВръща
collection(slug).list(options)GET /v1/content/{slug}{ data, meta, nextCursor }
collection(slug).get(slugOrId, options)GET /v1/content/{slug}/{slugOrId}Записа; хвърля грешка при 404.
collection(slug).find(slugOrId, options)същотоЗаписа или null.
collection(slug).single(options)GET /v1/content/{slug}Документа на единична колекция.
collection(slug).all(options)всички странициАсинхронен итератор по записите.

Настройките на списъка съответстват на параметрите на REST: limit → pageSize, page или cursor, filter, sort, fields, populate, locale, richtext, preview, плюс rawLocales: true, за да получите карти { bg, en }, дори когато клиентът има език. Всяко извикване приема и headers, signal и fetchOptions.

TypeScript
const { data, meta } = await citron.collection('products').list({
  locale: 'en',
  filter: {
    category: 'station',              // filter[category]=station
    capacity: { gte: 5 },             // filter[capacity][gte]=5
    code: { in: ['BIO-6', 'BIO-10'] }, // filter[code][in]=BIO-6,BIO-10
  },
  sort: '-publishedAt',               // or { field: 'publishedAt', order: 'desc' }
  page: 1,
  limit: 20,                          // sent as pageSize
});
console.log(meta.total, data.length);

Типове от вашите колекции#

Командата citron-sdk typegen чете дефинициите на колекциите, които ключът ви вижда, и записва TypeScript файл. Подайте картата от него на createClient и компилаторът ще проверява slug на колекциите, полетата, филтрите и populate.

bash
npx citron-sdk typegen \
  --base-url https://api.console.citronos.com \
  --api-key "$CITRON_API_KEY" \
  --org your-org \
  --out src/citron.generated.ts
ФлагПроменлива на средата
--base-urlCITRON_BASE_URLАдресът на API.
--api-keyCITRON_API_KEYКлюч за доставка на работното пространство.
--orgCITRON_ORGSlug на работното пространство (ключът трябва да е от него).
--outИзходен файл; - го извежда на стандартния изход. По подразбиране citron.generated.ts.
--fromЧете дефинициите от записан JSON файл вместо от API.
TypeScript
import { createClient } from '@citron/sdk';
import type { CitronCollections } from './citron.generated';

const citron = createClient<CitronCollections>({
  baseUrl: 'https://api.console.citronos.com',
  apiKey: process.env.CITRON_API_KEY,
  locale: 'en',
});

const { data } = await citron.collection('products').list({ filter: { category: 'station' } });
const name: string | null | undefined = data[0]?.data.name; // typed from the collection

// @ts-expect-error: a misspelt collection is a compile error
citron.collection('prodcts');

Всяка колекция получава тип …Data (преведените полета са на един език, както ги получавате с locale) и тип …Raw (преведените полета са CitronLocaleMap<T>, както ги получавате с rawLocales: true). Живите колекции се пропускат, защото API за доставка не ги обслужва. Пускайте командата отново при всяка промяна на колекция.

GraphQL#

citron.graphql(query, variables, options) връща data и хвърля CitronGraphQLError, когато отговорът съдържа errors. { persisted: true } изпраща първо хеша, а самата заявка само когато API я поиска. Вижте GraphQL.

TypeScript
// persisted: true does both steps for you and uses GET by default
const data = await citron.graphql<{ products: { data: { slug: string | null }[]; meta: { total: number } } }>(
  'query ($n: Int) { products(pageSize: $n) { data { slug } meta { total } } }',
  { n: 10 },
  { persisted: true },
);
console.log(data.products.meta.total);

Контакт, формуляри и срещи#

МетодВика
submitContact(input, { turnstileToken, idempotencyKey, orgSlug })POST /v1/public/{orgSlug}/contacts
forms.get(key)GET /v1/forms/{key} (никога от кеша)
forms.submit(key, data, { renderToken, turnstileToken, referrer, utm })POST /v1/forms/{key}/submissions
booking.types(), booking.slots(params), booking.book(input)/v1/public/{orgSlug}/appointment-types, …/appointments/slots, …/appointments

Всяко записване изпраща Idempotency-Key, генериран, ако не подадете свой; подайте свой, за да го запазите и след презареждане на страницата. Вижте формуляри, контакт и срещи.

TypeScript
const types = await citron.booking.types({ orgSlug: 'your-org' });
const slots = await citron.booking.slots(
  { typeId: types[0].id, mode: 'online', from: new Date(), to: new Date(Date.now() + 14 * 86_400_000) },
  { orgSlug: 'your-org' },
);

const appointment = await citron.booking.book(
  {
    contact: { name: 'Ivan Petrov', email: 'ivan@example.com', phone: '+359888000000' },
    appointmentTypeId: types[0].id,
    staffUserId: slots[0].staffUserId,
    mode: 'online',
    startsAt: slots[0].startsAt,
  },
  { idempotencyKey, orgSlug: 'your-org' },
);
console.log(appointment.id, appointment.status);

Уебкуки#

verifyWebhook(rawBody, signatureHeader, secret, { maxAgeSeconds }) връща true или false и сравнява за постоянно време. constructWebhookEvent проверява и разчита наведнъж и хвърля WebhookVerificationError. И двете ползват Web Crypto. Вижте уебкуки.

TypeScript
import { constructWebhookEvent, WebhookVerificationError } from '@citron/sdk/webhooks';

// Any runtime with Request/Response: Node 18+, edge, Workers
export async function handleCitronWebhook(req: Request): Promise<Response> {
  const raw = await req.text(); // the RAW body: re-serialised JSON has different bytes
  try {
    const event = await constructWebhookEvent(raw, req.headers.get('x-citron-signature'), process.env.CITRON_WEBHOOK_SECRET!, {
      maxAgeSeconds: 24 * 3600, // optional; the signed "at" makes old deliveries refusable
    });
    console.log(event.event, event.collection, event.entry.slug);
    return new Response(null, { status: 204 });
  } catch (err) {
    if (err instanceof WebhookVerificationError) return new Response(null, { status: 401 });
    throw err;
  }
}

Грешки, повторни опити и изчакване#

Всеки неуспех е CitronError със status (0, когато няма отговор), code, message, requestId, details и retryAfter, плюс isNotFound и isRetryable. Подкласовете са CitronNetworkError, CitronTimeoutError и CitronGraphQLError.

TypeScript
import { CitronGraphQLError, isCitronError } from '@citron/sdk';

try {
  await citron.collection('products').get('does-not-exist');
} catch (err) {
  if (err instanceof CitronGraphQLError) {
    console.error(err.errors, err.data); // GraphQL errors and partial data
  } else if (isCitronError(err)) {
    console.error(err.status, err.code, err.message, err.requestId, err.details);
    if (err.isNotFound) { /* render your 404 */ }
    if (err.isRetryable) { /* 0, 408, 429 or 5xx; err.retryAfter when the server said */ }
  } else {
    throw err;
  }
}
  • Повторните опити покриват 408, 429, 500, 502, 503, 504 и мрежовите грешки, с експоненциално нарастваща пауза и случайно отклонение, за GET заявки, GraphQL и записвания с Idempotency-Key. Retry-After, по-дълъг от maxDelayMs, води до незабавна грешка с попълнено retryAfter.
  • Собствените кодове на SDK: NETWORK_ERROR, TIMEOUT, CONFIG_ERROR, INVALID_CURSOR, INVALID_FILTER, INVALID_RESPONSE, GRAPHQL_ERROR. Кодовете на API са изброени в грешки.

App Router на Next.js#

Оставете кеша на SDK изключен и оставете кеширането на Next.js: подавайте fetchOptions: { next: { revalidate, tags } } при всяко извикване, слагайте тагове с citronTags и обновявайте от уебкука.

lib/citron.ts
// lib/citron.ts
import { createClient } from '@citron/sdk';
import { citronTags } from '@citron/sdk/next';

export const citron = createClient({
  baseUrl: process.env.CITRON_BASE_URL!,
  apiKey: process.env.CITRON_API_KEY!,
  locale: 'bg',
  richtext: 'html',
});

export const getProduct = (slug: string) =>
  citron.collection('products').find(slug, {
    populate: ['related'],
    fetchOptions: { next: { revalidate: 3600, tags: citronTags('products', slug) } },
  });

Ключове и среди#

  • Пазете ключовете в променливи на средата на сървъра. Ключът ck_pub_ чете само публикуваното съдържание, но лимитът на заявките е за ключ, така че ключ в публичен пакет позволява на всеки да изразходва вашия лимит.
  • Никога не изпращайте ключ ck_prev_ към браузъра: той чете всички чернови.
  • Контактът, формулярите и срещите не изискват ключ, така че тези помощници са безопасни в браузъра.
  • В среда без глобален fetch подайте свой в настройките на клиента.

Справочникът описва само публичното API и следва договора му за съвместимост: публичните отговори само получават нови полета. Описанието на тези адреси във формат OpenAPI е на /developers/openapi.json.