Referință
TypeScript SDK
@citron/sdk este un client compact, fără dependențe la rulare. Funcționează pe Node 18.17+, în mediile edge și în browser și este distribuit ca ESM și CommonJS.
Instalare#
SDK-ul nu este încă publicat pe npm. Îl trimitem ca arhivă a pachetului: scrieți-ne și îl vom partaja cu echipa dumneavoastră.
# Not on npm yet: install the package tarball we send you
npm install ./citron-sdk-0.1.0.tgz| Import | Ce conține |
|---|---|
@citron/sdk | createClient, erorile, tipurile, generateTypes și utilitarele pentru webhook-uri. |
@citron/sdk/webhooks | verifyWebhook, constructWebhookEvent, signWebhookBody și nimic altceva, pentru pachete edge cât mai mici. |
@citron/sdk/next | citronTags și createRevalidateHandler pentru App Router din Next.js. |
Crearea unui client#
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
});| Opțiune | Implicit | |
|---|---|---|
baseUrl | obligatoriu | Originea API. Este acceptat și un sufix /v1. |
apiKey | niciuna | ck_pub_… sau ck_prev_…. Necesară pentru conținut și GraphQL, nu și pentru contact, formulare sau rezervări. |
locale | niciuna | Aduce câmpurile localizate la această limbă la fiecare apel de conținut. |
preview | false | Citește ciornele (status=draft). Necesită o cheie ck_prev_; cu o cheie ck_pub_ clientul refuză să pornească. |
richtext | valoarea implicită a API (both) | html, json sau both. |
orgSlug | niciuna | Slug-ul spațiului de lucru pentru utilitarele de contact și rezervări. |
timeoutMs | 10000 | Pentru fiecare încercare; 0 îl dezactivează. |
retry | { retries: 2, baseDelayMs: 300, maxDelayMs: 10000 } | Sau false. |
cache | false | true revalidează cu ETag-uri; { ttlMs } servește în plus din memorie pe durata respectivă. |
headers, fetchOptions, fetch | niciuna | Sunt trimise cu fiecare cerere; fetch înlocuiește globalThis.fetch. |
Citirea conținutului#
| Metodă | Apelează | Returnează |
|---|---|---|
collection(slug).list(options) | GET /v1/content/{slug} | { data, meta, nextCursor } |
collection(slug).get(slugOrId, options) | GET /v1/content/{slug}/{slugOrId} | Înregistrarea; aruncă o eroare la 404. |
collection(slug).find(slugOrId, options) | același | Înregistrarea sau null. |
collection(slug).single(options) | GET /v1/content/{slug} | Documentul unei colecții singleton. |
collection(slug).all(options) | toate paginile | Un iterator asincron peste înregistrări. |
Opțiunile de listare corespund parametrilor REST: limit → pageSize, page sau cursor, filter, sort, fields, populate, locale, richtext, preview, plus rawLocales: true pentru a primi hărți { bg, en } chiar și atunci când clientul are o limbă setată. Fiecare apel acceptă și headers, signal și fetchOptions.
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);Tipuri din colecțiile dumneavoastră#
Comanda citron-sdk typegen citește definițiile colecțiilor vizibile pentru cheia dumneavoastră și scrie un fișier TypeScript. Transmiteți harta generată către createClient, iar slug-urile colecțiilor, câmpurile, filtrele și populate vor fi verificate de compilator.
npx citron-sdk typegen \
--base-url https://api.console.citronos.com \
--api-key "$CITRON_API_KEY" \
--org your-org \
--out src/citron.generated.ts| Opțiune | Variabilă de mediu | |
|---|---|---|
--base-url | CITRON_BASE_URL | Originea API. |
--api-key | CITRON_API_KEY | O cheie de livrare a spațiului de lucru. |
--org | CITRON_ORG | Slug-ul spațiului de lucru (cheia trebuie să îi aparțină). |
--out | Fișierul de ieșire; - afișează la ieșirea standard. Implicit citron.generated.ts. | |
--from | Citește definițiile dintr-un fișier JSON salvat, în loc de API. |
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');Fiecare colecție primește un tip …Data (câmpurile localizate aduse la o singură limbă, așa cum le primiți cu locale) și un tip …Raw (câmpurile localizate ca CitronLocaleMap<T>, așa cum le primiți cu rawLocales: true). Colecțiile live sunt omise, deoarece API-ul de livrare nu le servește. Rulați din nou comanda de fiecare dată când se modifică o colecție.
GraphQL#
citron.graphql(query, variables, options) returnează data și aruncă CitronGraphQLError când răspunsul conține errors. { persisted: true } trimite mai întâi hash-ul, iar interogarea doar dacă API-ul o solicită. Consultați GraphQL.
// 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);Contact, formulare și rezervări#
| Metodă | Apelează |
|---|---|
submitContact(input, { turnstileToken, idempotencyKey, orgSlug }) | POST /v1/public/{orgSlug}/contacts |
forms.get(key) | GET /v1/forms/{key} (niciodată din cache) |
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 |
Fiecare scriere trimite un Idempotency-Key, generat automat dacă nu transmiteți unul; transmiteți-l pe al dumneavoastră pentru a-l păstra și după reîncărcarea paginii. Consultați formulare, contact și rezervări.
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);Webhook-uri#
verifyWebhook(rawBody, signatureHeader, secret, { maxAgeSeconds }) returnează true sau false și compară în timp constant. constructWebhookEvent verifică și parsează într-un singur pas și aruncă WebhookVerificationError. Ambele folosesc Web Crypto. Consultați webhook-uri.
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;
}
}Erori, reîncercări și limite de timp#
Orice eșec este un CitronError cu status (0 când nu a existat niciun răspuns), code, message, requestId, details și retryAfter, plus isNotFound și isRetryable. Subclasele sunt CitronNetworkError, CitronTimeoutError și CitronGraphQLError.
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;
}
}- Reîncercările acoperă 408, 429, 500, 502, 503, 504 și erorile de rețea, cu întârziere exponențială și variație aleatorie, pentru cererile GET, GraphQL și scrierile care au un
Idempotency-Key. UnRetry-Aftermai lung decâtmaxDelayMsaruncă imediat o eroare, curetryAftercompletat. - Codurile proprii ale SDK-ului:
NETWORK_ERROR,TIMEOUT,CONFIG_ERROR,INVALID_CURSOR,INVALID_FILTER,INVALID_RESPONSE,GRAPHQL_ERROR. Codurile API-ului sunt enumerate la erori.
App Router din Next.js#
Lăsați cache-ul SDK-ului dezactivat și lăsați Next.js să gestioneze cache-ul: transmiteți fetchOptions: { next: { revalidate, tags } } la fiecare apel, etichetați cu citronTags și revalidați dintr-un webhook.
// 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) } },
});Chei și medii de rulare#
- Păstrați cheile în variabile de mediu pe server. O cheie
ck_pub_citește doar conținutul publicat, dar limita de cereri se aplică per cheie, așa că o cheie inclusă într-un pachet public permite oricui să vă consume bugetul. - Nu trimiteți niciodată o cheie
ck_prev_către browser: aceasta citește toate ciornele. - Contactul, formularele și rezervările nu necesită cheie, așa că aceste utilitare pot fi folosite în siguranță în browser.
- Într-un mediu de rulare fără
fetchglobal, transmiteți unul în configurația clientului.
Această referință acoperă doar API-ul public și respectă contractul de compatibilitate al API-ului: răspunsurile publice pot doar să primească câmpuri noi. Descrierea OpenAPI a acestor rute se află la /developers/openapi.json.