@plenary/client 1.0.2 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +29 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +51 -1
- package/dist/index.d.ts +51 -1
- package/dist/index.js +27 -0
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.cjs
CHANGED
|
@@ -30,11 +30,13 @@ __export(index_exports, {
|
|
|
30
30
|
buildListParams: () => buildListParams,
|
|
31
31
|
buildReadParams: () => buildReadParams,
|
|
32
32
|
createClient: () => createClient,
|
|
33
|
+
entryUrl: () => entryUrl,
|
|
33
34
|
errorFromResponse: () => errorFromResponse,
|
|
34
35
|
isEntryWebhookPayload: () => isEntryWebhookPayload,
|
|
35
36
|
isFormWebhookPayload: () => isFormWebhookPayload,
|
|
36
37
|
parseWebhookPayload: () => parseWebhookPayload,
|
|
37
38
|
parseWebhookRequest: () => parseWebhookRequest,
|
|
39
|
+
planRevalidation: () => planRevalidation,
|
|
38
40
|
verifyWebhookSecret: () => verifyWebhookSecret
|
|
39
41
|
});
|
|
40
42
|
module.exports = __toCommonJS(index_exports);
|
|
@@ -316,6 +318,31 @@ async function parseWebhookRequest(request, options) {
|
|
|
316
318
|
});
|
|
317
319
|
return parseWebhookPayload(body);
|
|
318
320
|
}
|
|
321
|
+
function entryUrl(payload) {
|
|
322
|
+
const { slug, path } = payload.entry;
|
|
323
|
+
if (!slug) return null;
|
|
324
|
+
const dir = path && path !== "/" ? path : "";
|
|
325
|
+
if (!dir && slug === "home") return "/";
|
|
326
|
+
return `${dir}/${slug}`;
|
|
327
|
+
}
|
|
328
|
+
function planRevalidation(payload, options) {
|
|
329
|
+
const none = { relevant: false, test: false, paths: [] };
|
|
330
|
+
if (!isEntryWebhookPayload(payload)) return none;
|
|
331
|
+
const test = payload.event === WEBHOOK_TEST_EVENT;
|
|
332
|
+
if (!test && options.events && !options.events.includes(payload.event)) {
|
|
333
|
+
return none;
|
|
334
|
+
}
|
|
335
|
+
const modelKey = payload.model.key;
|
|
336
|
+
const mapped = modelKey === void 0 ? void 0 : options.paths?.[modelKey];
|
|
337
|
+
let paths;
|
|
338
|
+
if (mapped === void 0) {
|
|
339
|
+
paths = (options.entryUrl ?? entryUrl)(payload);
|
|
340
|
+
} else {
|
|
341
|
+
paths = typeof mapped === "function" ? mapped(payload) : mapped;
|
|
342
|
+
}
|
|
343
|
+
const pathList = paths == null ? [] : typeof paths === "string" ? [paths] : paths;
|
|
344
|
+
return { relevant: true, test, paths: [...new Set(pathList)] };
|
|
345
|
+
}
|
|
319
346
|
// Annotate the CommonJS export names for ESM import in node:
|
|
320
347
|
0 && (module.exports = {
|
|
321
348
|
ENTRY_WEBHOOK_EVENTS,
|
|
@@ -328,11 +355,13 @@ async function parseWebhookRequest(request, options) {
|
|
|
328
355
|
buildListParams,
|
|
329
356
|
buildReadParams,
|
|
330
357
|
createClient,
|
|
358
|
+
entryUrl,
|
|
331
359
|
errorFromResponse,
|
|
332
360
|
isEntryWebhookPayload,
|
|
333
361
|
isFormWebhookPayload,
|
|
334
362
|
parseWebhookPayload,
|
|
335
363
|
parseWebhookRequest,
|
|
364
|
+
planRevalidation,
|
|
336
365
|
verifyWebhookSecret
|
|
337
366
|
});
|
|
338
367
|
//# sourceMappingURL=index.cjs.map
|
package/dist/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts","../src/query.ts","../src/errors.ts","../src/client.ts","../src/webhook.ts"],"sourcesContent":["/**\n * @plenary/client — the framework-free Plenary content client: published-read\n * content fetch, public form submission, and a typed query builder. Configured\n * with a public delivery API key; no admin endpoints.\n */\n\nexport { createClient, type PlenaryClient } from \"./client.js\";\nexport {\n buildReadParams,\n buildListParams,\n type QueryBase,\n} from \"./query.js\";\nexport {\n PlenaryError,\n PlenaryNotFoundError,\n PlenaryValidationError,\n PlenaryForbiddenError,\n errorFromResponse,\n} from \"./errors.js\";\nexport {\n ENTRY_WEBHOOK_EVENTS,\n WEBHOOK_TEST_EVENT,\n PlenaryWebhookError,\n verifyWebhookSecret,\n parseWebhookPayload,\n parseWebhookRequest,\n isEntryWebhookPayload,\n isFormWebhookPayload,\n type EntryWebhookEvent,\n type WebhookActor,\n type WebhookEntryRef,\n type WebhookFormEntryRef,\n type EntryWebhookPayload,\n type FormWebhookPayload,\n type WebhookPayload,\n type ParseWebhookOptions,\n} from \"./webhook.js\";\nexport type {\n ClientConfig,\n Entry,\n Where,\n WhereCondition,\n FilterTree,\n Sort,\n SortDirection,\n Hydrate,\n ReadOptions,\n ListOptions,\n ListResult,\n PublicFormFieldType,\n PublicFormField,\n PublicFormDefinition,\n FormSubmitOptions,\n FormSubmitResult,\n MediaVariant,\n MediaDetail,\n} from \"./types.js\";\n","import type {\n Where,\n WhereCondition,\n FilterTree,\n Sort,\n Hydrate,\n ReadOptions,\n ListOptions,\n} from \"./types.js\";\n\n/** The known `where:` operators; anything else on a condition object is passed through. */\nconst OPERATORS = new Set([\n \"$eq\",\n \"$ne\",\n \"$lt\",\n \"$lte\",\n \"$gt\",\n \"$gte\",\n \"$in\",\n \"$nin\",\n \"$like\",\n \"$ilike\",\n \"$is_null\",\n \"$under\",\n]);\n\n/** A value is a condition when it's a plain object whose keys are all operators. */\nfunction isCondition(value: unknown): value is WhereCondition {\n if (typeof value !== \"object\" || value === null || Array.isArray(value)) return false;\n const keys = Object.keys(value);\n return keys.length > 0 && keys.every((k) => OPERATORS.has(k));\n}\n\nfunction encodeOperatorValue(op: string, value: unknown): string {\n if (op === \"$in\" || op === \"$nin\") {\n return Array.isArray(value) ? value.map(String).join(\",\") : String(value);\n }\n if (op === \"$is_null\") {\n return String(Boolean(value));\n }\n return String(value);\n}\n\nfunction appendWhere(params: URLSearchParams, where: Where): void {\n for (const [field, value] of Object.entries(where)) {\n if (isCondition(value)) {\n for (const [op, opValue] of Object.entries(value)) {\n params.append(`where:${field}.${op}`, encodeOperatorValue(op, opValue));\n }\n } else {\n // Bare value — the server assumes `$eq`.\n params.append(`where:${field}`, String(value));\n }\n }\n}\n\nfunction appendSort(params: URLSearchParams, sort: Sort): void {\n // Insertion order is the sort precedence (left-to-right).\n for (const [field, dir] of Object.entries(sort)) {\n params.append(`sort:${field}`, dir);\n }\n}\n\nfunction appendHydrate(params: URLSearchParams, hydrate: Hydrate): void {\n for (const [field, spec] of Object.entries(hydrate)) {\n params.append(`hydrate:${field}`, spec === \"*\" ? \"*\" : spec.join(\",\"));\n }\n}\n\n/** Base context every request carries: which site and locale. */\nexport interface QueryBase {\n siteKey: string;\n locale: string;\n}\n\nfunction appendCommon(params: URLSearchParams, base: QueryBase, opts: ReadOptions): void {\n params.set(\"site_key\", opts.siteKey ?? base.siteKey);\n params.set(\"locale\", opts.locale ?? base.locale);\n\n if (opts.include && opts.exclude) {\n throw new Error(\"plenary: `include` and `exclude` are mutually exclusive\");\n }\n if (opts.include?.length) params.set(\"include\", opts.include.join(\",\"));\n if (opts.exclude?.length) params.set(\"exclude\", opts.exclude.join(\",\"));\n if (opts.hydrate) appendHydrate(params, opts.hydrate);\n}\n\n/** Builds the query for a single-entry read (get / singleton). */\nexport function buildReadParams(base: QueryBase, opts: ReadOptions = {}): URLSearchParams {\n const params = new URLSearchParams();\n appendCommon(params, base, opts);\n return params;\n}\n\n/** Builds the query for a list read, including filters, sort, and pagination. */\nexport function buildListParams(base: QueryBase, opts: ListOptions = {}): URLSearchParams {\n const params = new URLSearchParams();\n appendCommon(params, base, opts);\n\n if (opts.where && opts.filters) {\n throw new Error(\"plenary: `where` and `filters` are mutually exclusive\");\n }\n if (opts.filters) {\n params.set(\"filters\", JSON.stringify(opts.filters satisfies FilterTree));\n } else if (opts.where) {\n appendWhere(params, opts.where);\n }\n\n if (opts.sort) appendSort(params, opts.sort);\n if (opts.limit !== undefined) params.set(\"limit\", String(opts.limit));\n if (opts.offset !== undefined) params.set(\"offset\", String(opts.offset));\n if (opts.count) params.set(\"count\", \"true\");\n\n return params;\n}\n","/** The server's error envelope: `{ error: { code, vars? } }`. */\ninterface ServerErrorBody {\n error?: { code?: string; vars?: Record<string, unknown> };\n}\n\n/** Base error for a non-2xx response from the CMS. */\nexport class PlenaryError extends Error {\n readonly status: number;\n /** The server's i18n error code (e.g. `entry_query_limit_exceeded`), if any. */\n readonly code: string;\n /** Interpolation variables the server attached to the code. */\n readonly vars?: Record<string, unknown>;\n\n constructor(status: number, code: string, vars?: Record<string, unknown>) {\n super(`Plenary request failed (${status}): ${code}`);\n this.name = \"PlenaryError\";\n this.status = status;\n this.code = code;\n this.vars = vars;\n }\n}\n\n/** A 404 — model or entry not found (or hidden by a private model). */\nexport class PlenaryNotFoundError extends PlenaryError {\n constructor(code: string, vars?: Record<string, unknown>) {\n super(404, code, vars);\n this.name = \"PlenaryNotFoundError\";\n }\n}\n\n/** A 400/422 — malformed query or invalid input. */\nexport class PlenaryValidationError extends PlenaryError {\n constructor(status: number, code: string, vars?: Record<string, unknown>) {\n super(status, code, vars);\n this.name = \"PlenaryValidationError\";\n }\n}\n\n/** A 403 — the key may not read the requested site/resource. */\nexport class PlenaryForbiddenError extends PlenaryError {\n constructor(code: string, vars?: Record<string, unknown>) {\n super(403, code, vars);\n this.name = \"PlenaryForbiddenError\";\n }\n}\n\n/** Builds the right error subclass from a failed response's status and body. */\nexport function errorFromResponse(status: number, body: unknown): PlenaryError {\n const parsed = body as ServerErrorBody | null;\n const code = parsed?.error?.code ?? `http_${status}`;\n const vars = parsed?.error?.vars;\n switch (status) {\n case 404:\n return new PlenaryNotFoundError(code, vars);\n case 403:\n return new PlenaryForbiddenError(code, vars);\n case 400:\n case 422:\n return new PlenaryValidationError(status, code, vars);\n default:\n return new PlenaryError(status, code, vars);\n }\n}\n","import type {\n ClientConfig,\n Entry,\n ReadOptions,\n ListOptions,\n ListResult,\n PublicFormDefinition,\n FormSubmitOptions,\n FormSubmitResult,\n MediaDetail,\n} from \"./types.js\";\nimport { buildReadParams, buildListParams, type QueryBase } from \"./query.js\";\nimport { errorFromResponse } from \"./errors.js\";\n\n/** The content client returned by {@link createClient}. */\nexport interface PlenaryClient {\n /** Fetch a singleton model's entry. */\n getSingleton<TData = Record<string, unknown>>(\n modelKey: string,\n opts?: ReadOptions,\n ): Promise<Entry<TData>>;\n\n /** Fetch one entry of a collection/page by slug or UUID. */\n getEntry<TData = Record<string, unknown>>(\n modelKey: string,\n slugOrId: string,\n opts?: ReadOptions,\n ): Promise<Entry<TData>>;\n\n /** List entries of a collection/page model. */\n listEntries<TData = Record<string, unknown>>(\n modelKey: string,\n opts?: ListOptions,\n ): Promise<ListResult<TData>>;\n\n /**\n * Fetch a form's render-safe definition — the field list a renderer needs,\n * readable with a public delivery key.\n */\n getForm(formKey: string): Promise<PublicFormDefinition>;\n\n /** Submit a public form. */\n submitForm(\n formKey: string,\n data: Record<string, unknown>,\n opts?: FormSubmitOptions,\n ): Promise<FormSubmitResult>;\n\n /**\n * Fetch a media item's detail (URLs + variants). Requires a key that may read\n * media; a public delivery key cannot, so this is used in authoring/preview\n * contexts or by sites configured with a media-capable key.\n */\n getMedia(mediaId: string): Promise<MediaDetail>;\n}\n\nfunction joinUrl(baseUrl: string, path: string): string {\n return `${baseUrl.replace(/\\/+$/, \"\")}${path}`;\n}\n\nexport function createClient(config: ClientConfig): PlenaryClient {\n const base: QueryBase = { siteKey: config.siteKey, locale: config.locale };\n const doFetch = config.fetch ?? globalThis.fetch;\n\n async function request<T>(\n method: string,\n path: string,\n params: URLSearchParams,\n body?: unknown,\n ): Promise<{ data: T; response: Response }> {\n const url = `${joinUrl(config.baseUrl, path)}?${params.toString()}`;\n const headers: Record<string, string> = { \"X-API-KEY\": config.apiKey };\n if (body !== undefined) headers[\"Content-Type\"] = \"application/json\";\n\n const response = await doFetch(url, {\n method,\n headers,\n body: body !== undefined ? JSON.stringify(body) : undefined,\n });\n\n const parsed = response.status === 204 ? null : await response.json().catch(() => null);\n if (!response.ok) {\n throw errorFromResponse(response.status, parsed);\n }\n return { data: parsed as T, response };\n }\n\n return {\n async getSingleton<TData>(modelKey: string, opts: ReadOptions = {}) {\n const params = buildReadParams(base, opts);\n const { data } = await request<Entry<TData>>(\n \"GET\",\n `/api/v1/content/${encodeURIComponent(modelKey)}`,\n params,\n );\n return data;\n },\n\n async getEntry<TData>(modelKey: string, slugOrId: string, opts: ReadOptions = {}) {\n const params = buildReadParams(base, opts);\n const { data } = await request<Entry<TData>>(\n \"GET\",\n `/api/v1/content/${encodeURIComponent(modelKey)}/${encodeURIComponent(slugOrId)}`,\n params,\n );\n return data;\n },\n\n async listEntries<TData>(modelKey: string, opts: ListOptions = {}) {\n const params = buildListParams(base, opts);\n const { data, response } = await request<Entry<TData>[]>(\n \"GET\",\n `/api/v1/content/${encodeURIComponent(modelKey)}`,\n params,\n );\n const result: ListResult<TData> = { items: data };\n const total = response.headers.get(\"X-Total-Count\");\n if (total !== null) result.total = Number(total);\n return result;\n },\n\n async getForm(formKey: string) {\n const { data } = await request<PublicFormDefinition>(\n \"GET\",\n `/api/v1/forms/${encodeURIComponent(formKey)}`,\n new URLSearchParams(),\n );\n return data;\n },\n\n async submitForm(\n formKey: string,\n data: Record<string, unknown>,\n opts: FormSubmitOptions = {},\n ) {\n const params = new URLSearchParams();\n params.set(\"site_key\", opts.siteKey ?? base.siteKey);\n const body: Record<string, unknown> = { data };\n if (opts.website !== undefined) body.website = opts.website;\n if (opts.ts !== undefined) body.ts = opts.ts;\n const { data: result } = await request<FormSubmitResult>(\n \"POST\",\n `/api/v1/forms/${encodeURIComponent(formKey)}/entries`,\n params,\n body,\n );\n return result;\n },\n\n async getMedia(mediaId: string) {\n const { data } = await request<MediaDetail>(\n \"GET\",\n `/api/v1/media/${encodeURIComponent(mediaId)}`,\n new URLSearchParams(),\n );\n return data;\n },\n };\n}\n","/**\n * Webhook consumer helpers: the payload types Plenary POSTs\n * and framework-neutral verification/parsing. Plenary does not sign requests\n * yet; verification is a shared-secret comparison against a header or query\n * parameter. When HMAC signing ships, it slots in here without changing the\n * call sites.\n */\n\n/** The five entry lifecycle events a content-model webhook can subscribe to. */\nexport type EntryWebhookEvent =\n | \"entry.created\"\n | \"entry.updated\"\n | \"entry.published\"\n | \"entry.unpublished\"\n | \"entry.deleted\";\n\nexport const ENTRY_WEBHOOK_EVENTS: readonly EntryWebhookEvent[] = [\n \"entry.created\",\n \"entry.updated\",\n \"entry.published\",\n \"entry.unpublished\",\n \"entry.deleted\",\n];\n\n/** The event carried by the console's Test button. The body is otherwise an\n * ordinary entry or form payload, built from the model's most recent entry. */\nexport const WEBHOOK_TEST_EVENT = \"webhook.test\";\n\n/** Who caused the event. `label` survives the actor's deletion. */\nexport interface WebhookActor {\n type: \"user\" | \"api_key\" | \"system\";\n id?: string;\n label: string;\n}\n\n/** The entry's identity. Field selection never touches these. */\nexport interface WebhookEntryRef {\n id: string;\n site_key?: string;\n locale?: string;\n slug?: string;\n /** Directory path of the entry's folder (\"/\" at the model root). Does NOT\n * include the entry's own slug — join them to build a URL. */\n path?: string;\n status?: string;\n created_at?: string;\n updated_at?: string;\n published_at?: string;\n}\n\n/** A form entry's identity: no slug, locale, path, or publish state. */\nexport interface WebhookFormEntryRef {\n id: string;\n site_key?: string;\n status?: string;\n created_at?: string;\n source?: { ip?: string; user_agent?: string; referer?: string };\n}\n\n/** Payload for every `entry.*` event (and entry-shaped `webhook.test`). */\nexport interface EntryWebhookPayload<TData = Record<string, unknown>> {\n event: EntryWebhookEvent | typeof WEBHOOK_TEST_EVENT;\n delivery_id: string;\n occurred_at: string;\n webhook: { id: string; name?: string };\n model: { id: string; key?: string };\n entry: WebhookEntryRef;\n actor: WebhookActor;\n /** Paths that differ from the previous revision. Always present; empty for\n * `entry.created` and `entry.deleted`. */\n changed: string[];\n revision_id?: string;\n /** Field values per the webhook's field selection; absent for \"no field data\". */\n data?: TData;\n}\n\n/** Payload for `form.submitted` (and form-shaped `webhook.test`). */\nexport interface FormWebhookPayload<TData = Record<string, unknown>> {\n event: \"form.submitted\" | typeof WEBHOOK_TEST_EVENT;\n delivery_id: string;\n occurred_at: string;\n webhook: { id: string; name?: string };\n form: { id: string; key?: string };\n form_entry: WebhookFormEntryRef;\n data?: TData;\n}\n\nexport type WebhookPayload = EntryWebhookPayload | FormWebhookPayload;\n\nexport function isEntryWebhookPayload(p: WebhookPayload): p is EntryWebhookPayload {\n return \"entry\" in p;\n}\n\nexport function isFormWebhookPayload(p: WebhookPayload): p is FormWebhookPayload {\n return \"form_entry\" in p;\n}\n\n/** A request that is not a valid webhook delivery. `status` is the HTTP status\n * an endpoint should answer with — 4xx makes the delivery fail fast in the\n * console's delivery log instead of being retried for seven hours. */\nexport class PlenaryWebhookError extends Error {\n readonly status: number;\n\n constructor(status: number, message: string) {\n super(message);\n this.name = \"PlenaryWebhookError\";\n this.status = status;\n }\n}\n\n/** Constant-time comparison of a provided secret against the expected one.\n * An empty or missing `secret` never matches, so an unset environment\n * variable can't turn into \"accept an empty header\". */\nexport function verifyWebhookSecret(\n provided: string | null | undefined,\n secret: string,\n): boolean {\n if (typeof provided !== \"string\" || !secret) return false;\n const enc = new TextEncoder();\n const a = enc.encode(provided);\n const b = enc.encode(secret);\n let diff = a.length ^ b.length;\n const len = Math.max(a.length, b.length);\n for (let i = 0; i < len; i++) diff |= (a[i] ?? 0) ^ (b[i] ?? 0);\n return diff === 0;\n}\n\n/** Shape-checks a parsed JSON body and types it. Throws {@link PlenaryWebhookError}\n * (400) for anything that is not a Plenary webhook payload. */\nexport function parseWebhookPayload(body: unknown): WebhookPayload {\n const p = body as Record<string, unknown> | null;\n if (\n p === null ||\n typeof p !== \"object\" ||\n typeof p[\"event\"] !== \"string\" ||\n typeof p[\"delivery_id\"] !== \"string\" ||\n !(\"entry\" in p || \"form_entry\" in p)\n ) {\n throw new PlenaryWebhookError(400, \"Not a Plenary webhook payload\");\n }\n return p as unknown as WebhookPayload;\n}\n\n/** Options for {@link parseWebhookRequest}. */\nexport interface ParseWebhookOptions {\n /** The shared secret configured on the webhook (as a secret header or query\n * parameter in the console). */\n secret: string;\n /** Header the secret arrives in. Default `x-plenary-secret`. */\n secretHeader?: string;\n /** Query parameter checked when the header is absent. Default `secret`. */\n secretQuery?: string;\n}\n\n/**\n * Verifies and parses a webhook delivery from a Fetch-API `Request`:\n * POST only, secret in the configured header or query parameter, JSON body of\n * one of the two payload shapes. Throws {@link PlenaryWebhookError} with the\n * status the endpoint should answer (405, 401, or 400).\n */\nexport async function parseWebhookRequest(\n request: Request,\n options: ParseWebhookOptions,\n): Promise<WebhookPayload> {\n if (request.method !== \"POST\") {\n throw new PlenaryWebhookError(405, \"Method not allowed\");\n }\n if (!options.secret) {\n throw new PlenaryWebhookError(500, \"Webhook secret is not configured\");\n }\n const header = options.secretHeader ?? \"x-plenary-secret\";\n const query = options.secretQuery ?? \"secret\";\n const provided =\n request.headers.get(header) ?? new URL(request.url).searchParams.get(query);\n if (!verifyWebhookSecret(provided, options.secret)) {\n throw new PlenaryWebhookError(401, \"Invalid webhook secret\");\n }\n const body = await request.json().catch(() => {\n throw new PlenaryWebhookError(400, \"Body is not valid JSON\");\n });\n return parseWebhookPayload(body);\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/query.ts","../src/errors.ts","../src/client.ts","../src/webhook.ts"],"sourcesContent":["/**\n * @plenary/client — the framework-free Plenary content client: published-read\n * content fetch, public form submission, and a typed query builder. Configured\n * with a public delivery API key; no admin endpoints.\n */\n\nexport { createClient, type PlenaryClient } from \"./client.js\";\nexport {\n buildReadParams,\n buildListParams,\n type QueryBase,\n} from \"./query.js\";\nexport {\n PlenaryError,\n PlenaryNotFoundError,\n PlenaryValidationError,\n PlenaryForbiddenError,\n errorFromResponse,\n} from \"./errors.js\";\nexport {\n ENTRY_WEBHOOK_EVENTS,\n WEBHOOK_TEST_EVENT,\n PlenaryWebhookError,\n verifyWebhookSecret,\n parseWebhookPayload,\n parseWebhookRequest,\n isEntryWebhookPayload,\n isFormWebhookPayload,\n entryUrl,\n planRevalidation,\n type RevalidatePaths,\n type RevalidateOptions,\n type RevalidationPlan,\n type EntryWebhookEvent,\n type WebhookActor,\n type WebhookEntryRef,\n type WebhookFormEntryRef,\n type EntryWebhookPayload,\n type FormWebhookPayload,\n type WebhookPayload,\n type ParseWebhookOptions,\n} from \"./webhook.js\";\nexport type {\n ClientConfig,\n Entry,\n EntryDraft,\n Where,\n WhereCondition,\n FilterTree,\n Sort,\n SortDirection,\n Hydrate,\n ReadOptions,\n ListOptions,\n ListResult,\n PublicFormFieldType,\n PublicFormField,\n PublicFormDefinition,\n FormSubmitOptions,\n FormSubmitResult,\n MediaVariant,\n MediaDetail,\n} from \"./types.js\";\n","import type {\n Where,\n WhereCondition,\n FilterTree,\n Sort,\n Hydrate,\n ReadOptions,\n ListOptions,\n} from \"./types.js\";\n\n/** The known `where:` operators; anything else on a condition object is passed through. */\nconst OPERATORS = new Set([\n \"$eq\",\n \"$ne\",\n \"$lt\",\n \"$lte\",\n \"$gt\",\n \"$gte\",\n \"$in\",\n \"$nin\",\n \"$like\",\n \"$ilike\",\n \"$is_null\",\n \"$under\",\n]);\n\n/** A value is a condition when it's a plain object whose keys are all operators. */\nfunction isCondition(value: unknown): value is WhereCondition {\n if (typeof value !== \"object\" || value === null || Array.isArray(value)) return false;\n const keys = Object.keys(value);\n return keys.length > 0 && keys.every((k) => OPERATORS.has(k));\n}\n\nfunction encodeOperatorValue(op: string, value: unknown): string {\n if (op === \"$in\" || op === \"$nin\") {\n return Array.isArray(value) ? value.map(String).join(\",\") : String(value);\n }\n if (op === \"$is_null\") {\n return String(Boolean(value));\n }\n return String(value);\n}\n\nfunction appendWhere(params: URLSearchParams, where: Where): void {\n for (const [field, value] of Object.entries(where)) {\n if (isCondition(value)) {\n for (const [op, opValue] of Object.entries(value)) {\n params.append(`where:${field}.${op}`, encodeOperatorValue(op, opValue));\n }\n } else {\n // Bare value — the server assumes `$eq`.\n params.append(`where:${field}`, String(value));\n }\n }\n}\n\nfunction appendSort(params: URLSearchParams, sort: Sort): void {\n // Insertion order is the sort precedence (left-to-right).\n for (const [field, dir] of Object.entries(sort)) {\n params.append(`sort:${field}`, dir);\n }\n}\n\nfunction appendHydrate(params: URLSearchParams, hydrate: Hydrate): void {\n for (const [field, spec] of Object.entries(hydrate)) {\n params.append(`hydrate:${field}`, spec === \"*\" ? \"*\" : spec.join(\",\"));\n }\n}\n\n/** Base context every request carries: which site and locale. */\nexport interface QueryBase {\n siteKey: string;\n locale: string;\n}\n\nfunction appendCommon(params: URLSearchParams, base: QueryBase, opts: ReadOptions): void {\n params.set(\"site_key\", opts.siteKey ?? base.siteKey);\n params.set(\"locale\", opts.locale ?? base.locale);\n\n if (opts.include && opts.exclude) {\n throw new Error(\"plenary: `include` and `exclude` are mutually exclusive\");\n }\n if (opts.include?.length) params.set(\"include\", opts.include.join(\",\"));\n if (opts.exclude?.length) params.set(\"exclude\", opts.exclude.join(\",\"));\n if (opts.hydrate) appendHydrate(params, opts.hydrate);\n}\n\n/** Builds the query for a single-entry read (get / singleton). */\nexport function buildReadParams(base: QueryBase, opts: ReadOptions = {}): URLSearchParams {\n const params = new URLSearchParams();\n appendCommon(params, base, opts);\n return params;\n}\n\n/** Builds the query for a list read, including filters, sort, and pagination. */\nexport function buildListParams(base: QueryBase, opts: ListOptions = {}): URLSearchParams {\n const params = new URLSearchParams();\n appendCommon(params, base, opts);\n\n if (opts.where && opts.filters) {\n throw new Error(\"plenary: `where` and `filters` are mutually exclusive\");\n }\n if (opts.filters) {\n params.set(\"filters\", JSON.stringify(opts.filters satisfies FilterTree));\n } else if (opts.where) {\n appendWhere(params, opts.where);\n }\n\n if (opts.sort) appendSort(params, opts.sort);\n if (opts.limit !== undefined) params.set(\"limit\", String(opts.limit));\n if (opts.offset !== undefined) params.set(\"offset\", String(opts.offset));\n if (opts.count) params.set(\"count\", \"true\");\n\n return params;\n}\n","/** The server's error envelope: `{ error: { code, vars? } }`. */\ninterface ServerErrorBody {\n error?: { code?: string; vars?: Record<string, unknown> };\n}\n\n/** Base error for a non-2xx response from the CMS. */\nexport class PlenaryError extends Error {\n readonly status: number;\n /** The server's i18n error code (e.g. `entry_query_limit_exceeded`), if any. */\n readonly code: string;\n /** Interpolation variables the server attached to the code. */\n readonly vars?: Record<string, unknown>;\n\n constructor(status: number, code: string, vars?: Record<string, unknown>) {\n super(`Plenary request failed (${status}): ${code}`);\n this.name = \"PlenaryError\";\n this.status = status;\n this.code = code;\n this.vars = vars;\n }\n}\n\n/** A 404 — model or entry not found (or hidden by a private model). */\nexport class PlenaryNotFoundError extends PlenaryError {\n constructor(code: string, vars?: Record<string, unknown>) {\n super(404, code, vars);\n this.name = \"PlenaryNotFoundError\";\n }\n}\n\n/** A 400/422 — malformed query or invalid input. */\nexport class PlenaryValidationError extends PlenaryError {\n constructor(status: number, code: string, vars?: Record<string, unknown>) {\n super(status, code, vars);\n this.name = \"PlenaryValidationError\";\n }\n}\n\n/** A 403 — the key may not read the requested site/resource. */\nexport class PlenaryForbiddenError extends PlenaryError {\n constructor(code: string, vars?: Record<string, unknown>) {\n super(403, code, vars);\n this.name = \"PlenaryForbiddenError\";\n }\n}\n\n/** Builds the right error subclass from a failed response's status and body. */\nexport function errorFromResponse(status: number, body: unknown): PlenaryError {\n const parsed = body as ServerErrorBody | null;\n const code = parsed?.error?.code ?? `http_${status}`;\n const vars = parsed?.error?.vars;\n switch (status) {\n case 404:\n return new PlenaryNotFoundError(code, vars);\n case 403:\n return new PlenaryForbiddenError(code, vars);\n case 400:\n case 422:\n return new PlenaryValidationError(status, code, vars);\n default:\n return new PlenaryError(status, code, vars);\n }\n}\n","import type {\n ClientConfig,\n Entry,\n ReadOptions,\n ListOptions,\n ListResult,\n PublicFormDefinition,\n FormSubmitOptions,\n FormSubmitResult,\n MediaDetail,\n} from \"./types.js\";\nimport { buildReadParams, buildListParams, type QueryBase } from \"./query.js\";\nimport { errorFromResponse } from \"./errors.js\";\n\n/** The content client returned by {@link createClient}. */\nexport interface PlenaryClient {\n /** Fetch a singleton model's entry. */\n getSingleton<TData = Record<string, unknown>>(\n modelKey: string,\n opts?: ReadOptions,\n ): Promise<Entry<TData>>;\n\n /** Fetch one entry of a collection/page by slug or UUID. */\n getEntry<TData = Record<string, unknown>>(\n modelKey: string,\n slugOrId: string,\n opts?: ReadOptions,\n ): Promise<Entry<TData>>;\n\n /** List entries of a collection/page model. */\n listEntries<TData = Record<string, unknown>>(\n modelKey: string,\n opts?: ListOptions,\n ): Promise<ListResult<TData>>;\n\n /**\n * Fetch a form's render-safe definition — the field list a renderer needs,\n * readable with a public delivery key.\n */\n getForm(formKey: string): Promise<PublicFormDefinition>;\n\n /** Submit a public form. */\n submitForm(\n formKey: string,\n data: Record<string, unknown>,\n opts?: FormSubmitOptions,\n ): Promise<FormSubmitResult>;\n\n /**\n * Fetch a media item's detail (URLs + variants). Requires a key that may read\n * media; a public delivery key cannot, so this is used in authoring/preview\n * contexts or by sites configured with a media-capable key.\n */\n getMedia(mediaId: string): Promise<MediaDetail>;\n}\n\nfunction joinUrl(baseUrl: string, path: string): string {\n return `${baseUrl.replace(/\\/+$/, \"\")}${path}`;\n}\n\nexport function createClient(config: ClientConfig): PlenaryClient {\n const base: QueryBase = { siteKey: config.siteKey, locale: config.locale };\n const doFetch = config.fetch ?? globalThis.fetch;\n\n async function request<T>(\n method: string,\n path: string,\n params: URLSearchParams,\n body?: unknown,\n ): Promise<{ data: T; response: Response }> {\n const url = `${joinUrl(config.baseUrl, path)}?${params.toString()}`;\n const headers: Record<string, string> = { \"X-API-KEY\": config.apiKey };\n if (body !== undefined) headers[\"Content-Type\"] = \"application/json\";\n\n const response = await doFetch(url, {\n method,\n headers,\n body: body !== undefined ? JSON.stringify(body) : undefined,\n });\n\n const parsed = response.status === 204 ? null : await response.json().catch(() => null);\n if (!response.ok) {\n throw errorFromResponse(response.status, parsed);\n }\n return { data: parsed as T, response };\n }\n\n return {\n async getSingleton<TData>(modelKey: string, opts: ReadOptions = {}) {\n const params = buildReadParams(base, opts);\n const { data } = await request<Entry<TData>>(\n \"GET\",\n `/api/v1/content/${encodeURIComponent(modelKey)}`,\n params,\n );\n return data;\n },\n\n async getEntry<TData>(modelKey: string, slugOrId: string, opts: ReadOptions = {}) {\n const params = buildReadParams(base, opts);\n const { data } = await request<Entry<TData>>(\n \"GET\",\n `/api/v1/content/${encodeURIComponent(modelKey)}/${encodeURIComponent(slugOrId)}`,\n params,\n );\n return data;\n },\n\n async listEntries<TData>(modelKey: string, opts: ListOptions = {}) {\n const params = buildListParams(base, opts);\n const { data, response } = await request<Entry<TData>[]>(\n \"GET\",\n `/api/v1/content/${encodeURIComponent(modelKey)}`,\n params,\n );\n const result: ListResult<TData> = { items: data };\n const total = response.headers.get(\"X-Total-Count\");\n if (total !== null) result.total = Number(total);\n return result;\n },\n\n async getForm(formKey: string) {\n const { data } = await request<PublicFormDefinition>(\n \"GET\",\n `/api/v1/forms/${encodeURIComponent(formKey)}`,\n new URLSearchParams(),\n );\n return data;\n },\n\n async submitForm(\n formKey: string,\n data: Record<string, unknown>,\n opts: FormSubmitOptions = {},\n ) {\n const params = new URLSearchParams();\n params.set(\"site_key\", opts.siteKey ?? base.siteKey);\n const body: Record<string, unknown> = { data };\n if (opts.website !== undefined) body.website = opts.website;\n if (opts.ts !== undefined) body.ts = opts.ts;\n const { data: result } = await request<FormSubmitResult>(\n \"POST\",\n `/api/v1/forms/${encodeURIComponent(formKey)}/entries`,\n params,\n body,\n );\n return result;\n },\n\n async getMedia(mediaId: string) {\n const { data } = await request<MediaDetail>(\n \"GET\",\n `/api/v1/media/${encodeURIComponent(mediaId)}`,\n new URLSearchParams(),\n );\n return data;\n },\n };\n}\n","/**\n * Webhook consumer helpers: the payload types Plenary POSTs\n * and framework-neutral verification/parsing. Plenary does not sign requests\n * yet; verification is a shared-secret comparison against a header or query\n * parameter. When HMAC signing ships, it slots in here without changing the\n * call sites.\n */\n\n/** The five entry lifecycle events a content-model webhook can subscribe to. */\nexport type EntryWebhookEvent =\n | \"entry.created\"\n | \"entry.updated\"\n | \"entry.published\"\n | \"entry.unpublished\"\n | \"entry.deleted\";\n\nexport const ENTRY_WEBHOOK_EVENTS: readonly EntryWebhookEvent[] = [\n \"entry.created\",\n \"entry.updated\",\n \"entry.published\",\n \"entry.unpublished\",\n \"entry.deleted\",\n];\n\n/** The event carried by the console's Test button. The body is otherwise an\n * ordinary entry or form payload, built from the model's most recent entry. */\nexport const WEBHOOK_TEST_EVENT = \"webhook.test\";\n\n/** Who caused the event. `label` survives the actor's deletion. */\nexport interface WebhookActor {\n type: \"user\" | \"api_key\" | \"system\";\n id?: string;\n label: string;\n}\n\n/** The entry's identity. Field selection never touches these. */\nexport interface WebhookEntryRef {\n id: string;\n site_key?: string;\n locale?: string;\n slug?: string;\n /** Directory path of the entry's folder (\"/\" at the model root). Does NOT\n * include the entry's own slug — join them to build a URL. */\n path?: string;\n status?: string;\n created_at?: string;\n updated_at?: string;\n published_at?: string;\n}\n\n/** A form entry's identity: no slug, locale, path, or publish state. */\nexport interface WebhookFormEntryRef {\n id: string;\n site_key?: string;\n status?: string;\n created_at?: string;\n source?: { ip?: string; user_agent?: string; referer?: string };\n}\n\n/** Payload for every `entry.*` event (and entry-shaped `webhook.test`). */\nexport interface EntryWebhookPayload<TData = Record<string, unknown>> {\n event: EntryWebhookEvent | typeof WEBHOOK_TEST_EVENT;\n delivery_id: string;\n occurred_at: string;\n webhook: { id: string; name?: string };\n model: { id: string; key?: string };\n entry: WebhookEntryRef;\n actor: WebhookActor;\n /** Paths that differ from the previous revision. Always present; empty for\n * `entry.created` and `entry.deleted`. */\n changed: string[];\n revision_id?: string;\n /** Field values per the webhook's field selection; absent for \"no field data\". */\n data?: TData;\n}\n\n/** Payload for `form.submitted` (and form-shaped `webhook.test`). */\nexport interface FormWebhookPayload<TData = Record<string, unknown>> {\n event: \"form.submitted\" | typeof WEBHOOK_TEST_EVENT;\n delivery_id: string;\n occurred_at: string;\n webhook: { id: string; name?: string };\n form: { id: string; key?: string };\n form_entry: WebhookFormEntryRef;\n data?: TData;\n}\n\nexport type WebhookPayload = EntryWebhookPayload | FormWebhookPayload;\n\nexport function isEntryWebhookPayload(p: WebhookPayload): p is EntryWebhookPayload {\n return \"entry\" in p;\n}\n\nexport function isFormWebhookPayload(p: WebhookPayload): p is FormWebhookPayload {\n return \"form_entry\" in p;\n}\n\n/** A request that is not a valid webhook delivery. `status` is the HTTP status\n * an endpoint should answer with — 4xx makes the delivery fail fast in the\n * console's delivery log instead of being retried for seven hours. */\nexport class PlenaryWebhookError extends Error {\n readonly status: number;\n\n constructor(status: number, message: string) {\n super(message);\n this.name = \"PlenaryWebhookError\";\n this.status = status;\n }\n}\n\n/** Constant-time comparison of a provided secret against the expected one.\n * An empty or missing `secret` never matches, so an unset environment\n * variable can't turn into \"accept an empty header\". */\nexport function verifyWebhookSecret(\n provided: string | null | undefined,\n secret: string,\n): boolean {\n if (typeof provided !== \"string\" || !secret) return false;\n const enc = new TextEncoder();\n const a = enc.encode(provided);\n const b = enc.encode(secret);\n let diff = a.length ^ b.length;\n const len = Math.max(a.length, b.length);\n for (let i = 0; i < len; i++) diff |= (a[i] ?? 0) ^ (b[i] ?? 0);\n return diff === 0;\n}\n\n/** Shape-checks a parsed JSON body and types it. Throws {@link PlenaryWebhookError}\n * (400) for anything that is not a Plenary webhook payload. */\nexport function parseWebhookPayload(body: unknown): WebhookPayload {\n const p = body as Record<string, unknown> | null;\n if (\n p === null ||\n typeof p !== \"object\" ||\n typeof p[\"event\"] !== \"string\" ||\n typeof p[\"delivery_id\"] !== \"string\" ||\n !(\"entry\" in p || \"form_entry\" in p)\n ) {\n throw new PlenaryWebhookError(400, \"Not a Plenary webhook payload\");\n }\n return p as unknown as WebhookPayload;\n}\n\n/** Options for {@link parseWebhookRequest}. */\nexport interface ParseWebhookOptions {\n /** The shared secret configured on the webhook (as a secret header or query\n * parameter in the console). */\n secret: string;\n /** Header the secret arrives in. Default `x-plenary-secret`. */\n secretHeader?: string;\n /** Query parameter checked when the header is absent. Default `secret`. */\n secretQuery?: string;\n}\n\n/**\n * Verifies and parses a webhook delivery from a Fetch-API `Request`:\n * POST only, secret in the configured header or query parameter, JSON body of\n * one of the two payload shapes. Throws {@link PlenaryWebhookError} with the\n * status the endpoint should answer (405, 401, or 400).\n */\nexport async function parseWebhookRequest(\n request: Request,\n options: ParseWebhookOptions,\n): Promise<WebhookPayload> {\n if (request.method !== \"POST\") {\n throw new PlenaryWebhookError(405, \"Method not allowed\");\n }\n if (!options.secret) {\n throw new PlenaryWebhookError(500, \"Webhook secret is not configured\");\n }\n const header = options.secretHeader ?? \"x-plenary-secret\";\n const query = options.secretQuery ?? \"secret\";\n const provided =\n request.headers.get(header) ?? new URL(request.url).searchParams.get(query);\n if (!verifyWebhookSecret(provided, options.secret)) {\n throw new PlenaryWebhookError(401, \"Invalid webhook secret\");\n }\n const body = await request.json().catch(() => {\n throw new PlenaryWebhookError(400, \"Body is not valid JSON\");\n });\n return parseWebhookPayload(body);\n}\n\n/**\n * Revalidation planning — which site URLs one entry webhook affects. Shared by\n * the framework adapters (`@plenary/next`, `@plenary/nuxt`), which own the\n * cache calls; this part is pure.\n */\n\n/** Paths for one model: a fixed path, a list, or a function of the payload.\n * A function may return `null`/`undefined` to revalidate nothing. */\nexport type RevalidatePaths =\n | string\n | string[]\n | ((payload: EntryWebhookPayload) => string | string[] | null | undefined);\n\nexport interface RevalidateOptions extends ParseWebhookOptions {\n /**\n * Extra or overriding paths per model key. A mapped model replaces the\n * default URL entirely — include `entryUrl(payload)` (exported) in the\n * returned list to keep it.\n */\n paths?: Record<string, RevalidatePaths>;\n /** Builds the default URL for unmapped models. Defaults to joining the\n * entry's folder `path` and `slug`; return `null` to skip. */\n entryUrl?: (payload: EntryWebhookPayload) => string | null | undefined;\n /** Entry events that trigger revalidation. Default: all five — revalidation\n * is idempotent and cheap, so over-firing beats a stale page. */\n events?: EntryWebhookEvent[];\n}\n\n/** Default URL for an entry: folder path + slug (`/blog` + `spring-sale` →\n * `/blog/spring-sale`). A root-folder entry slugged `home` is the site's home\n * page, `/`. Entries without a slug (singletons) get no default. */\nexport function entryUrl(payload: EntryWebhookPayload): string | null {\n const { slug, path } = payload.entry;\n if (!slug) return null;\n const dir = path && path !== \"/\" ? path : \"\";\n if (!dir && slug === \"home\") return \"/\";\n return `${dir}/${slug}`;\n}\n\nexport interface RevalidationPlan {\n /** Whether this payload is one the handler acts on. */\n relevant: boolean;\n /** True for the console's Test button — report, don't revalidate. */\n test: boolean;\n paths: string[];\n}\n\n/** Decides which paths one payload revalidates. Form payloads and entry\n * events outside `options.events` are not relevant. */\nexport function planRevalidation(\n payload: WebhookPayload,\n options: RevalidateOptions,\n): RevalidationPlan {\n const none = { relevant: false, test: false, paths: [] };\n if (!isEntryWebhookPayload(payload)) return none;\n\n const test = payload.event === WEBHOOK_TEST_EVENT;\n if (!test && options.events && !options.events.includes(payload.event as EntryWebhookEvent)) {\n return none;\n }\n\n const modelKey = payload.model.key;\n const mapped = modelKey === undefined ? undefined : options.paths?.[modelKey];\n let paths: string | string[] | null | undefined;\n if (mapped === undefined) {\n paths = (options.entryUrl ?? entryUrl)(payload);\n } else {\n paths = typeof mapped === \"function\" ? mapped(payload) : mapped;\n }\n const pathList = paths == null ? [] : typeof paths === \"string\" ? [paths] : paths;\n\n return { relevant: true, test, paths: [...new Set(pathList)] };\n}\n"],"mappings":"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","names":[]}
|
package/dist/index.d.cts
CHANGED
|
@@ -35,6 +35,20 @@ interface Entry<TData = Record<string, unknown>> {
|
|
|
35
35
|
path: string;
|
|
36
36
|
/** Field values keyed by field key; a `blocks` field holds a component tree. */
|
|
37
37
|
data: TData;
|
|
38
|
+
/**
|
|
39
|
+
* Changes saved to a published entry that are not live yet, or null (absent
|
|
40
|
+
* on a read through a public key). `data` and `slug` stay the live copy
|
|
41
|
+
* until the entry is published again. A public key flagged `include_drafts`
|
|
42
|
+
* reads the pending content in `data` and `slug` instead and never sees
|
|
43
|
+
* this key.
|
|
44
|
+
*/
|
|
45
|
+
draft?: EntryDraft | null;
|
|
46
|
+
}
|
|
47
|
+
/** The pending changes on a published entry: the content a publish would make live. */
|
|
48
|
+
interface EntryDraft<TData = Record<string, unknown>> {
|
|
49
|
+
data: TData;
|
|
50
|
+
slug: string | null;
|
|
51
|
+
updated_at: string;
|
|
38
52
|
}
|
|
39
53
|
/** Comparison operators supported by `where:` and filter trees. */
|
|
40
54
|
interface WhereCondition {
|
|
@@ -352,5 +366,41 @@ interface ParseWebhookOptions {
|
|
|
352
366
|
* status the endpoint should answer (405, 401, or 400).
|
|
353
367
|
*/
|
|
354
368
|
declare function parseWebhookRequest(request: Request, options: ParseWebhookOptions): Promise<WebhookPayload>;
|
|
369
|
+
/**
|
|
370
|
+
* Revalidation planning — which site URLs one entry webhook affects. Shared by
|
|
371
|
+
* the framework adapters (`@plenary/next`, `@plenary/nuxt`), which own the
|
|
372
|
+
* cache calls; this part is pure.
|
|
373
|
+
*/
|
|
374
|
+
/** Paths for one model: a fixed path, a list, or a function of the payload.
|
|
375
|
+
* A function may return `null`/`undefined` to revalidate nothing. */
|
|
376
|
+
type RevalidatePaths = string | string[] | ((payload: EntryWebhookPayload) => string | string[] | null | undefined);
|
|
377
|
+
interface RevalidateOptions extends ParseWebhookOptions {
|
|
378
|
+
/**
|
|
379
|
+
* Extra or overriding paths per model key. A mapped model replaces the
|
|
380
|
+
* default URL entirely — include `entryUrl(payload)` (exported) in the
|
|
381
|
+
* returned list to keep it.
|
|
382
|
+
*/
|
|
383
|
+
paths?: Record<string, RevalidatePaths>;
|
|
384
|
+
/** Builds the default URL for unmapped models. Defaults to joining the
|
|
385
|
+
* entry's folder `path` and `slug`; return `null` to skip. */
|
|
386
|
+
entryUrl?: (payload: EntryWebhookPayload) => string | null | undefined;
|
|
387
|
+
/** Entry events that trigger revalidation. Default: all five — revalidation
|
|
388
|
+
* is idempotent and cheap, so over-firing beats a stale page. */
|
|
389
|
+
events?: EntryWebhookEvent[];
|
|
390
|
+
}
|
|
391
|
+
/** Default URL for an entry: folder path + slug (`/blog` + `spring-sale` →
|
|
392
|
+
* `/blog/spring-sale`). A root-folder entry slugged `home` is the site's home
|
|
393
|
+
* page, `/`. Entries without a slug (singletons) get no default. */
|
|
394
|
+
declare function entryUrl(payload: EntryWebhookPayload): string | null;
|
|
395
|
+
interface RevalidationPlan {
|
|
396
|
+
/** Whether this payload is one the handler acts on. */
|
|
397
|
+
relevant: boolean;
|
|
398
|
+
/** True for the console's Test button — report, don't revalidate. */
|
|
399
|
+
test: boolean;
|
|
400
|
+
paths: string[];
|
|
401
|
+
}
|
|
402
|
+
/** Decides which paths one payload revalidates. Form payloads and entry
|
|
403
|
+
* events outside `options.events` are not relevant. */
|
|
404
|
+
declare function planRevalidation(payload: WebhookPayload, options: RevalidateOptions): RevalidationPlan;
|
|
355
405
|
|
|
356
|
-
export { type ClientConfig, ENTRY_WEBHOOK_EVENTS, type Entry, type EntryWebhookEvent, type EntryWebhookPayload, type FilterTree, type FormSubmitOptions, type FormSubmitResult, type FormWebhookPayload, type Hydrate, type ListOptions, type ListResult, type MediaDetail, type MediaVariant, type ParseWebhookOptions, type PlenaryClient, PlenaryError, PlenaryForbiddenError, PlenaryNotFoundError, PlenaryValidationError, PlenaryWebhookError, type PublicFormDefinition, type PublicFormField, type PublicFormFieldType, type QueryBase, type ReadOptions, type Sort, type SortDirection, WEBHOOK_TEST_EVENT, type WebhookActor, type WebhookEntryRef, type WebhookFormEntryRef, type WebhookPayload, type Where, type WhereCondition, buildListParams, buildReadParams, createClient, errorFromResponse, isEntryWebhookPayload, isFormWebhookPayload, parseWebhookPayload, parseWebhookRequest, verifyWebhookSecret };
|
|
406
|
+
export { type ClientConfig, ENTRY_WEBHOOK_EVENTS, type Entry, type EntryDraft, type EntryWebhookEvent, type EntryWebhookPayload, type FilterTree, type FormSubmitOptions, type FormSubmitResult, type FormWebhookPayload, type Hydrate, type ListOptions, type ListResult, type MediaDetail, type MediaVariant, type ParseWebhookOptions, type PlenaryClient, PlenaryError, PlenaryForbiddenError, PlenaryNotFoundError, PlenaryValidationError, PlenaryWebhookError, type PublicFormDefinition, type PublicFormField, type PublicFormFieldType, type QueryBase, type ReadOptions, type RevalidateOptions, type RevalidatePaths, type RevalidationPlan, type Sort, type SortDirection, WEBHOOK_TEST_EVENT, type WebhookActor, type WebhookEntryRef, type WebhookFormEntryRef, type WebhookPayload, type Where, type WhereCondition, buildListParams, buildReadParams, createClient, entryUrl, errorFromResponse, isEntryWebhookPayload, isFormWebhookPayload, parseWebhookPayload, parseWebhookRequest, planRevalidation, verifyWebhookSecret };
|
package/dist/index.d.ts
CHANGED
|
@@ -35,6 +35,20 @@ interface Entry<TData = Record<string, unknown>> {
|
|
|
35
35
|
path: string;
|
|
36
36
|
/** Field values keyed by field key; a `blocks` field holds a component tree. */
|
|
37
37
|
data: TData;
|
|
38
|
+
/**
|
|
39
|
+
* Changes saved to a published entry that are not live yet, or null (absent
|
|
40
|
+
* on a read through a public key). `data` and `slug` stay the live copy
|
|
41
|
+
* until the entry is published again. A public key flagged `include_drafts`
|
|
42
|
+
* reads the pending content in `data` and `slug` instead and never sees
|
|
43
|
+
* this key.
|
|
44
|
+
*/
|
|
45
|
+
draft?: EntryDraft | null;
|
|
46
|
+
}
|
|
47
|
+
/** The pending changes on a published entry: the content a publish would make live. */
|
|
48
|
+
interface EntryDraft<TData = Record<string, unknown>> {
|
|
49
|
+
data: TData;
|
|
50
|
+
slug: string | null;
|
|
51
|
+
updated_at: string;
|
|
38
52
|
}
|
|
39
53
|
/** Comparison operators supported by `where:` and filter trees. */
|
|
40
54
|
interface WhereCondition {
|
|
@@ -352,5 +366,41 @@ interface ParseWebhookOptions {
|
|
|
352
366
|
* status the endpoint should answer (405, 401, or 400).
|
|
353
367
|
*/
|
|
354
368
|
declare function parseWebhookRequest(request: Request, options: ParseWebhookOptions): Promise<WebhookPayload>;
|
|
369
|
+
/**
|
|
370
|
+
* Revalidation planning — which site URLs one entry webhook affects. Shared by
|
|
371
|
+
* the framework adapters (`@plenary/next`, `@plenary/nuxt`), which own the
|
|
372
|
+
* cache calls; this part is pure.
|
|
373
|
+
*/
|
|
374
|
+
/** Paths for one model: a fixed path, a list, or a function of the payload.
|
|
375
|
+
* A function may return `null`/`undefined` to revalidate nothing. */
|
|
376
|
+
type RevalidatePaths = string | string[] | ((payload: EntryWebhookPayload) => string | string[] | null | undefined);
|
|
377
|
+
interface RevalidateOptions extends ParseWebhookOptions {
|
|
378
|
+
/**
|
|
379
|
+
* Extra or overriding paths per model key. A mapped model replaces the
|
|
380
|
+
* default URL entirely — include `entryUrl(payload)` (exported) in the
|
|
381
|
+
* returned list to keep it.
|
|
382
|
+
*/
|
|
383
|
+
paths?: Record<string, RevalidatePaths>;
|
|
384
|
+
/** Builds the default URL for unmapped models. Defaults to joining the
|
|
385
|
+
* entry's folder `path` and `slug`; return `null` to skip. */
|
|
386
|
+
entryUrl?: (payload: EntryWebhookPayload) => string | null | undefined;
|
|
387
|
+
/** Entry events that trigger revalidation. Default: all five — revalidation
|
|
388
|
+
* is idempotent and cheap, so over-firing beats a stale page. */
|
|
389
|
+
events?: EntryWebhookEvent[];
|
|
390
|
+
}
|
|
391
|
+
/** Default URL for an entry: folder path + slug (`/blog` + `spring-sale` →
|
|
392
|
+
* `/blog/spring-sale`). A root-folder entry slugged `home` is the site's home
|
|
393
|
+
* page, `/`. Entries without a slug (singletons) get no default. */
|
|
394
|
+
declare function entryUrl(payload: EntryWebhookPayload): string | null;
|
|
395
|
+
interface RevalidationPlan {
|
|
396
|
+
/** Whether this payload is one the handler acts on. */
|
|
397
|
+
relevant: boolean;
|
|
398
|
+
/** True for the console's Test button — report, don't revalidate. */
|
|
399
|
+
test: boolean;
|
|
400
|
+
paths: string[];
|
|
401
|
+
}
|
|
402
|
+
/** Decides which paths one payload revalidates. Form payloads and entry
|
|
403
|
+
* events outside `options.events` are not relevant. */
|
|
404
|
+
declare function planRevalidation(payload: WebhookPayload, options: RevalidateOptions): RevalidationPlan;
|
|
355
405
|
|
|
356
|
-
export { type ClientConfig, ENTRY_WEBHOOK_EVENTS, type Entry, type EntryWebhookEvent, type EntryWebhookPayload, type FilterTree, type FormSubmitOptions, type FormSubmitResult, type FormWebhookPayload, type Hydrate, type ListOptions, type ListResult, type MediaDetail, type MediaVariant, type ParseWebhookOptions, type PlenaryClient, PlenaryError, PlenaryForbiddenError, PlenaryNotFoundError, PlenaryValidationError, PlenaryWebhookError, type PublicFormDefinition, type PublicFormField, type PublicFormFieldType, type QueryBase, type ReadOptions, type Sort, type SortDirection, WEBHOOK_TEST_EVENT, type WebhookActor, type WebhookEntryRef, type WebhookFormEntryRef, type WebhookPayload, type Where, type WhereCondition, buildListParams, buildReadParams, createClient, errorFromResponse, isEntryWebhookPayload, isFormWebhookPayload, parseWebhookPayload, parseWebhookRequest, verifyWebhookSecret };
|
|
406
|
+
export { type ClientConfig, ENTRY_WEBHOOK_EVENTS, type Entry, type EntryDraft, type EntryWebhookEvent, type EntryWebhookPayload, type FilterTree, type FormSubmitOptions, type FormSubmitResult, type FormWebhookPayload, type Hydrate, type ListOptions, type ListResult, type MediaDetail, type MediaVariant, type ParseWebhookOptions, type PlenaryClient, PlenaryError, PlenaryForbiddenError, PlenaryNotFoundError, PlenaryValidationError, PlenaryWebhookError, type PublicFormDefinition, type PublicFormField, type PublicFormFieldType, type QueryBase, type ReadOptions, type RevalidateOptions, type RevalidatePaths, type RevalidationPlan, type Sort, type SortDirection, WEBHOOK_TEST_EVENT, type WebhookActor, type WebhookEntryRef, type WebhookFormEntryRef, type WebhookPayload, type Where, type WhereCondition, buildListParams, buildReadParams, createClient, entryUrl, errorFromResponse, isEntryWebhookPayload, isFormWebhookPayload, parseWebhookPayload, parseWebhookRequest, planRevalidation, verifyWebhookSecret };
|
package/dist/index.js
CHANGED
|
@@ -275,6 +275,31 @@ async function parseWebhookRequest(request, options) {
|
|
|
275
275
|
});
|
|
276
276
|
return parseWebhookPayload(body);
|
|
277
277
|
}
|
|
278
|
+
function entryUrl(payload) {
|
|
279
|
+
const { slug, path } = payload.entry;
|
|
280
|
+
if (!slug) return null;
|
|
281
|
+
const dir = path && path !== "/" ? path : "";
|
|
282
|
+
if (!dir && slug === "home") return "/";
|
|
283
|
+
return `${dir}/${slug}`;
|
|
284
|
+
}
|
|
285
|
+
function planRevalidation(payload, options) {
|
|
286
|
+
const none = { relevant: false, test: false, paths: [] };
|
|
287
|
+
if (!isEntryWebhookPayload(payload)) return none;
|
|
288
|
+
const test = payload.event === WEBHOOK_TEST_EVENT;
|
|
289
|
+
if (!test && options.events && !options.events.includes(payload.event)) {
|
|
290
|
+
return none;
|
|
291
|
+
}
|
|
292
|
+
const modelKey = payload.model.key;
|
|
293
|
+
const mapped = modelKey === void 0 ? void 0 : options.paths?.[modelKey];
|
|
294
|
+
let paths;
|
|
295
|
+
if (mapped === void 0) {
|
|
296
|
+
paths = (options.entryUrl ?? entryUrl)(payload);
|
|
297
|
+
} else {
|
|
298
|
+
paths = typeof mapped === "function" ? mapped(payload) : mapped;
|
|
299
|
+
}
|
|
300
|
+
const pathList = paths == null ? [] : typeof paths === "string" ? [paths] : paths;
|
|
301
|
+
return { relevant: true, test, paths: [...new Set(pathList)] };
|
|
302
|
+
}
|
|
278
303
|
export {
|
|
279
304
|
ENTRY_WEBHOOK_EVENTS,
|
|
280
305
|
PlenaryError,
|
|
@@ -286,11 +311,13 @@ export {
|
|
|
286
311
|
buildListParams,
|
|
287
312
|
buildReadParams,
|
|
288
313
|
createClient,
|
|
314
|
+
entryUrl,
|
|
289
315
|
errorFromResponse,
|
|
290
316
|
isEntryWebhookPayload,
|
|
291
317
|
isFormWebhookPayload,
|
|
292
318
|
parseWebhookPayload,
|
|
293
319
|
parseWebhookRequest,
|
|
320
|
+
planRevalidation,
|
|
294
321
|
verifyWebhookSecret
|
|
295
322
|
};
|
|
296
323
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/query.ts","../src/errors.ts","../src/client.ts","../src/webhook.ts"],"sourcesContent":["import type {\n Where,\n WhereCondition,\n FilterTree,\n Sort,\n Hydrate,\n ReadOptions,\n ListOptions,\n} from \"./types.js\";\n\n/** The known `where:` operators; anything else on a condition object is passed through. */\nconst OPERATORS = new Set([\n \"$eq\",\n \"$ne\",\n \"$lt\",\n \"$lte\",\n \"$gt\",\n \"$gte\",\n \"$in\",\n \"$nin\",\n \"$like\",\n \"$ilike\",\n \"$is_null\",\n \"$under\",\n]);\n\n/** A value is a condition when it's a plain object whose keys are all operators. */\nfunction isCondition(value: unknown): value is WhereCondition {\n if (typeof value !== \"object\" || value === null || Array.isArray(value)) return false;\n const keys = Object.keys(value);\n return keys.length > 0 && keys.every((k) => OPERATORS.has(k));\n}\n\nfunction encodeOperatorValue(op: string, value: unknown): string {\n if (op === \"$in\" || op === \"$nin\") {\n return Array.isArray(value) ? value.map(String).join(\",\") : String(value);\n }\n if (op === \"$is_null\") {\n return String(Boolean(value));\n }\n return String(value);\n}\n\nfunction appendWhere(params: URLSearchParams, where: Where): void {\n for (const [field, value] of Object.entries(where)) {\n if (isCondition(value)) {\n for (const [op, opValue] of Object.entries(value)) {\n params.append(`where:${field}.${op}`, encodeOperatorValue(op, opValue));\n }\n } else {\n // Bare value — the server assumes `$eq`.\n params.append(`where:${field}`, String(value));\n }\n }\n}\n\nfunction appendSort(params: URLSearchParams, sort: Sort): void {\n // Insertion order is the sort precedence (left-to-right).\n for (const [field, dir] of Object.entries(sort)) {\n params.append(`sort:${field}`, dir);\n }\n}\n\nfunction appendHydrate(params: URLSearchParams, hydrate: Hydrate): void {\n for (const [field, spec] of Object.entries(hydrate)) {\n params.append(`hydrate:${field}`, spec === \"*\" ? \"*\" : spec.join(\",\"));\n }\n}\n\n/** Base context every request carries: which site and locale. */\nexport interface QueryBase {\n siteKey: string;\n locale: string;\n}\n\nfunction appendCommon(params: URLSearchParams, base: QueryBase, opts: ReadOptions): void {\n params.set(\"site_key\", opts.siteKey ?? base.siteKey);\n params.set(\"locale\", opts.locale ?? base.locale);\n\n if (opts.include && opts.exclude) {\n throw new Error(\"plenary: `include` and `exclude` are mutually exclusive\");\n }\n if (opts.include?.length) params.set(\"include\", opts.include.join(\",\"));\n if (opts.exclude?.length) params.set(\"exclude\", opts.exclude.join(\",\"));\n if (opts.hydrate) appendHydrate(params, opts.hydrate);\n}\n\n/** Builds the query for a single-entry read (get / singleton). */\nexport function buildReadParams(base: QueryBase, opts: ReadOptions = {}): URLSearchParams {\n const params = new URLSearchParams();\n appendCommon(params, base, opts);\n return params;\n}\n\n/** Builds the query for a list read, including filters, sort, and pagination. */\nexport function buildListParams(base: QueryBase, opts: ListOptions = {}): URLSearchParams {\n const params = new URLSearchParams();\n appendCommon(params, base, opts);\n\n if (opts.where && opts.filters) {\n throw new Error(\"plenary: `where` and `filters` are mutually exclusive\");\n }\n if (opts.filters) {\n params.set(\"filters\", JSON.stringify(opts.filters satisfies FilterTree));\n } else if (opts.where) {\n appendWhere(params, opts.where);\n }\n\n if (opts.sort) appendSort(params, opts.sort);\n if (opts.limit !== undefined) params.set(\"limit\", String(opts.limit));\n if (opts.offset !== undefined) params.set(\"offset\", String(opts.offset));\n if (opts.count) params.set(\"count\", \"true\");\n\n return params;\n}\n","/** The server's error envelope: `{ error: { code, vars? } }`. */\ninterface ServerErrorBody {\n error?: { code?: string; vars?: Record<string, unknown> };\n}\n\n/** Base error for a non-2xx response from the CMS. */\nexport class PlenaryError extends Error {\n readonly status: number;\n /** The server's i18n error code (e.g. `entry_query_limit_exceeded`), if any. */\n readonly code: string;\n /** Interpolation variables the server attached to the code. */\n readonly vars?: Record<string, unknown>;\n\n constructor(status: number, code: string, vars?: Record<string, unknown>) {\n super(`Plenary request failed (${status}): ${code}`);\n this.name = \"PlenaryError\";\n this.status = status;\n this.code = code;\n this.vars = vars;\n }\n}\n\n/** A 404 — model or entry not found (or hidden by a private model). */\nexport class PlenaryNotFoundError extends PlenaryError {\n constructor(code: string, vars?: Record<string, unknown>) {\n super(404, code, vars);\n this.name = \"PlenaryNotFoundError\";\n }\n}\n\n/** A 400/422 — malformed query or invalid input. */\nexport class PlenaryValidationError extends PlenaryError {\n constructor(status: number, code: string, vars?: Record<string, unknown>) {\n super(status, code, vars);\n this.name = \"PlenaryValidationError\";\n }\n}\n\n/** A 403 — the key may not read the requested site/resource. */\nexport class PlenaryForbiddenError extends PlenaryError {\n constructor(code: string, vars?: Record<string, unknown>) {\n super(403, code, vars);\n this.name = \"PlenaryForbiddenError\";\n }\n}\n\n/** Builds the right error subclass from a failed response's status and body. */\nexport function errorFromResponse(status: number, body: unknown): PlenaryError {\n const parsed = body as ServerErrorBody | null;\n const code = parsed?.error?.code ?? `http_${status}`;\n const vars = parsed?.error?.vars;\n switch (status) {\n case 404:\n return new PlenaryNotFoundError(code, vars);\n case 403:\n return new PlenaryForbiddenError(code, vars);\n case 400:\n case 422:\n return new PlenaryValidationError(status, code, vars);\n default:\n return new PlenaryError(status, code, vars);\n }\n}\n","import type {\n ClientConfig,\n Entry,\n ReadOptions,\n ListOptions,\n ListResult,\n PublicFormDefinition,\n FormSubmitOptions,\n FormSubmitResult,\n MediaDetail,\n} from \"./types.js\";\nimport { buildReadParams, buildListParams, type QueryBase } from \"./query.js\";\nimport { errorFromResponse } from \"./errors.js\";\n\n/** The content client returned by {@link createClient}. */\nexport interface PlenaryClient {\n /** Fetch a singleton model's entry. */\n getSingleton<TData = Record<string, unknown>>(\n modelKey: string,\n opts?: ReadOptions,\n ): Promise<Entry<TData>>;\n\n /** Fetch one entry of a collection/page by slug or UUID. */\n getEntry<TData = Record<string, unknown>>(\n modelKey: string,\n slugOrId: string,\n opts?: ReadOptions,\n ): Promise<Entry<TData>>;\n\n /** List entries of a collection/page model. */\n listEntries<TData = Record<string, unknown>>(\n modelKey: string,\n opts?: ListOptions,\n ): Promise<ListResult<TData>>;\n\n /**\n * Fetch a form's render-safe definition — the field list a renderer needs,\n * readable with a public delivery key.\n */\n getForm(formKey: string): Promise<PublicFormDefinition>;\n\n /** Submit a public form. */\n submitForm(\n formKey: string,\n data: Record<string, unknown>,\n opts?: FormSubmitOptions,\n ): Promise<FormSubmitResult>;\n\n /**\n * Fetch a media item's detail (URLs + variants). Requires a key that may read\n * media; a public delivery key cannot, so this is used in authoring/preview\n * contexts or by sites configured with a media-capable key.\n */\n getMedia(mediaId: string): Promise<MediaDetail>;\n}\n\nfunction joinUrl(baseUrl: string, path: string): string {\n return `${baseUrl.replace(/\\/+$/, \"\")}${path}`;\n}\n\nexport function createClient(config: ClientConfig): PlenaryClient {\n const base: QueryBase = { siteKey: config.siteKey, locale: config.locale };\n const doFetch = config.fetch ?? globalThis.fetch;\n\n async function request<T>(\n method: string,\n path: string,\n params: URLSearchParams,\n body?: unknown,\n ): Promise<{ data: T; response: Response }> {\n const url = `${joinUrl(config.baseUrl, path)}?${params.toString()}`;\n const headers: Record<string, string> = { \"X-API-KEY\": config.apiKey };\n if (body !== undefined) headers[\"Content-Type\"] = \"application/json\";\n\n const response = await doFetch(url, {\n method,\n headers,\n body: body !== undefined ? JSON.stringify(body) : undefined,\n });\n\n const parsed = response.status === 204 ? null : await response.json().catch(() => null);\n if (!response.ok) {\n throw errorFromResponse(response.status, parsed);\n }\n return { data: parsed as T, response };\n }\n\n return {\n async getSingleton<TData>(modelKey: string, opts: ReadOptions = {}) {\n const params = buildReadParams(base, opts);\n const { data } = await request<Entry<TData>>(\n \"GET\",\n `/api/v1/content/${encodeURIComponent(modelKey)}`,\n params,\n );\n return data;\n },\n\n async getEntry<TData>(modelKey: string, slugOrId: string, opts: ReadOptions = {}) {\n const params = buildReadParams(base, opts);\n const { data } = await request<Entry<TData>>(\n \"GET\",\n `/api/v1/content/${encodeURIComponent(modelKey)}/${encodeURIComponent(slugOrId)}`,\n params,\n );\n return data;\n },\n\n async listEntries<TData>(modelKey: string, opts: ListOptions = {}) {\n const params = buildListParams(base, opts);\n const { data, response } = await request<Entry<TData>[]>(\n \"GET\",\n `/api/v1/content/${encodeURIComponent(modelKey)}`,\n params,\n );\n const result: ListResult<TData> = { items: data };\n const total = response.headers.get(\"X-Total-Count\");\n if (total !== null) result.total = Number(total);\n return result;\n },\n\n async getForm(formKey: string) {\n const { data } = await request<PublicFormDefinition>(\n \"GET\",\n `/api/v1/forms/${encodeURIComponent(formKey)}`,\n new URLSearchParams(),\n );\n return data;\n },\n\n async submitForm(\n formKey: string,\n data: Record<string, unknown>,\n opts: FormSubmitOptions = {},\n ) {\n const params = new URLSearchParams();\n params.set(\"site_key\", opts.siteKey ?? base.siteKey);\n const body: Record<string, unknown> = { data };\n if (opts.website !== undefined) body.website = opts.website;\n if (opts.ts !== undefined) body.ts = opts.ts;\n const { data: result } = await request<FormSubmitResult>(\n \"POST\",\n `/api/v1/forms/${encodeURIComponent(formKey)}/entries`,\n params,\n body,\n );\n return result;\n },\n\n async getMedia(mediaId: string) {\n const { data } = await request<MediaDetail>(\n \"GET\",\n `/api/v1/media/${encodeURIComponent(mediaId)}`,\n new URLSearchParams(),\n );\n return data;\n },\n };\n}\n","/**\n * Webhook consumer helpers: the payload types Plenary POSTs\n * and framework-neutral verification/parsing. Plenary does not sign requests\n * yet; verification is a shared-secret comparison against a header or query\n * parameter. When HMAC signing ships, it slots in here without changing the\n * call sites.\n */\n\n/** The five entry lifecycle events a content-model webhook can subscribe to. */\nexport type EntryWebhookEvent =\n | \"entry.created\"\n | \"entry.updated\"\n | \"entry.published\"\n | \"entry.unpublished\"\n | \"entry.deleted\";\n\nexport const ENTRY_WEBHOOK_EVENTS: readonly EntryWebhookEvent[] = [\n \"entry.created\",\n \"entry.updated\",\n \"entry.published\",\n \"entry.unpublished\",\n \"entry.deleted\",\n];\n\n/** The event carried by the console's Test button. The body is otherwise an\n * ordinary entry or form payload, built from the model's most recent entry. */\nexport const WEBHOOK_TEST_EVENT = \"webhook.test\";\n\n/** Who caused the event. `label` survives the actor's deletion. */\nexport interface WebhookActor {\n type: \"user\" | \"api_key\" | \"system\";\n id?: string;\n label: string;\n}\n\n/** The entry's identity. Field selection never touches these. */\nexport interface WebhookEntryRef {\n id: string;\n site_key?: string;\n locale?: string;\n slug?: string;\n /** Directory path of the entry's folder (\"/\" at the model root). Does NOT\n * include the entry's own slug — join them to build a URL. */\n path?: string;\n status?: string;\n created_at?: string;\n updated_at?: string;\n published_at?: string;\n}\n\n/** A form entry's identity: no slug, locale, path, or publish state. */\nexport interface WebhookFormEntryRef {\n id: string;\n site_key?: string;\n status?: string;\n created_at?: string;\n source?: { ip?: string; user_agent?: string; referer?: string };\n}\n\n/** Payload for every `entry.*` event (and entry-shaped `webhook.test`). */\nexport interface EntryWebhookPayload<TData = Record<string, unknown>> {\n event: EntryWebhookEvent | typeof WEBHOOK_TEST_EVENT;\n delivery_id: string;\n occurred_at: string;\n webhook: { id: string; name?: string };\n model: { id: string; key?: string };\n entry: WebhookEntryRef;\n actor: WebhookActor;\n /** Paths that differ from the previous revision. Always present; empty for\n * `entry.created` and `entry.deleted`. */\n changed: string[];\n revision_id?: string;\n /** Field values per the webhook's field selection; absent for \"no field data\". */\n data?: TData;\n}\n\n/** Payload for `form.submitted` (and form-shaped `webhook.test`). */\nexport interface FormWebhookPayload<TData = Record<string, unknown>> {\n event: \"form.submitted\" | typeof WEBHOOK_TEST_EVENT;\n delivery_id: string;\n occurred_at: string;\n webhook: { id: string; name?: string };\n form: { id: string; key?: string };\n form_entry: WebhookFormEntryRef;\n data?: TData;\n}\n\nexport type WebhookPayload = EntryWebhookPayload | FormWebhookPayload;\n\nexport function isEntryWebhookPayload(p: WebhookPayload): p is EntryWebhookPayload {\n return \"entry\" in p;\n}\n\nexport function isFormWebhookPayload(p: WebhookPayload): p is FormWebhookPayload {\n return \"form_entry\" in p;\n}\n\n/** A request that is not a valid webhook delivery. `status` is the HTTP status\n * an endpoint should answer with — 4xx makes the delivery fail fast in the\n * console's delivery log instead of being retried for seven hours. */\nexport class PlenaryWebhookError extends Error {\n readonly status: number;\n\n constructor(status: number, message: string) {\n super(message);\n this.name = \"PlenaryWebhookError\";\n this.status = status;\n }\n}\n\n/** Constant-time comparison of a provided secret against the expected one.\n * An empty or missing `secret` never matches, so an unset environment\n * variable can't turn into \"accept an empty header\". */\nexport function verifyWebhookSecret(\n provided: string | null | undefined,\n secret: string,\n): boolean {\n if (typeof provided !== \"string\" || !secret) return false;\n const enc = new TextEncoder();\n const a = enc.encode(provided);\n const b = enc.encode(secret);\n let diff = a.length ^ b.length;\n const len = Math.max(a.length, b.length);\n for (let i = 0; i < len; i++) diff |= (a[i] ?? 0) ^ (b[i] ?? 0);\n return diff === 0;\n}\n\n/** Shape-checks a parsed JSON body and types it. Throws {@link PlenaryWebhookError}\n * (400) for anything that is not a Plenary webhook payload. */\nexport function parseWebhookPayload(body: unknown): WebhookPayload {\n const p = body as Record<string, unknown> | null;\n if (\n p === null ||\n typeof p !== \"object\" ||\n typeof p[\"event\"] !== \"string\" ||\n typeof p[\"delivery_id\"] !== \"string\" ||\n !(\"entry\" in p || \"form_entry\" in p)\n ) {\n throw new PlenaryWebhookError(400, \"Not a Plenary webhook payload\");\n }\n return p as unknown as WebhookPayload;\n}\n\n/** Options for {@link parseWebhookRequest}. */\nexport interface ParseWebhookOptions {\n /** The shared secret configured on the webhook (as a secret header or query\n * parameter in the console). */\n secret: string;\n /** Header the secret arrives in. Default `x-plenary-secret`. */\n secretHeader?: string;\n /** Query parameter checked when the header is absent. Default `secret`. */\n secretQuery?: string;\n}\n\n/**\n * Verifies and parses a webhook delivery from a Fetch-API `Request`:\n * POST only, secret in the configured header or query parameter, JSON body of\n * one of the two payload shapes. Throws {@link PlenaryWebhookError} with the\n * status the endpoint should answer (405, 401, or 400).\n */\nexport async function parseWebhookRequest(\n request: Request,\n options: ParseWebhookOptions,\n): Promise<WebhookPayload> {\n if (request.method !== \"POST\") {\n throw new PlenaryWebhookError(405, \"Method not allowed\");\n }\n if (!options.secret) {\n throw new PlenaryWebhookError(500, \"Webhook secret is not configured\");\n }\n const header = options.secretHeader ?? \"x-plenary-secret\";\n const query = options.secretQuery ?? \"secret\";\n const provided =\n request.headers.get(header) ?? new URL(request.url).searchParams.get(query);\n if (!verifyWebhookSecret(provided, options.secret)) {\n throw new PlenaryWebhookError(401, \"Invalid webhook secret\");\n }\n const body = await request.json().catch(() => {\n throw new PlenaryWebhookError(400, \"Body is not valid JSON\");\n });\n return parseWebhookPayload(body);\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/query.ts","../src/errors.ts","../src/client.ts","../src/webhook.ts"],"sourcesContent":["import type {\n Where,\n WhereCondition,\n FilterTree,\n Sort,\n Hydrate,\n ReadOptions,\n ListOptions,\n} from \"./types.js\";\n\n/** The known `where:` operators; anything else on a condition object is passed through. */\nconst OPERATORS = new Set([\n \"$eq\",\n \"$ne\",\n \"$lt\",\n \"$lte\",\n \"$gt\",\n \"$gte\",\n \"$in\",\n \"$nin\",\n \"$like\",\n \"$ilike\",\n \"$is_null\",\n \"$under\",\n]);\n\n/** A value is a condition when it's a plain object whose keys are all operators. */\nfunction isCondition(value: unknown): value is WhereCondition {\n if (typeof value !== \"object\" || value === null || Array.isArray(value)) return false;\n const keys = Object.keys(value);\n return keys.length > 0 && keys.every((k) => OPERATORS.has(k));\n}\n\nfunction encodeOperatorValue(op: string, value: unknown): string {\n if (op === \"$in\" || op === \"$nin\") {\n return Array.isArray(value) ? value.map(String).join(\",\") : String(value);\n }\n if (op === \"$is_null\") {\n return String(Boolean(value));\n }\n return String(value);\n}\n\nfunction appendWhere(params: URLSearchParams, where: Where): void {\n for (const [field, value] of Object.entries(where)) {\n if (isCondition(value)) {\n for (const [op, opValue] of Object.entries(value)) {\n params.append(`where:${field}.${op}`, encodeOperatorValue(op, opValue));\n }\n } else {\n // Bare value — the server assumes `$eq`.\n params.append(`where:${field}`, String(value));\n }\n }\n}\n\nfunction appendSort(params: URLSearchParams, sort: Sort): void {\n // Insertion order is the sort precedence (left-to-right).\n for (const [field, dir] of Object.entries(sort)) {\n params.append(`sort:${field}`, dir);\n }\n}\n\nfunction appendHydrate(params: URLSearchParams, hydrate: Hydrate): void {\n for (const [field, spec] of Object.entries(hydrate)) {\n params.append(`hydrate:${field}`, spec === \"*\" ? \"*\" : spec.join(\",\"));\n }\n}\n\n/** Base context every request carries: which site and locale. */\nexport interface QueryBase {\n siteKey: string;\n locale: string;\n}\n\nfunction appendCommon(params: URLSearchParams, base: QueryBase, opts: ReadOptions): void {\n params.set(\"site_key\", opts.siteKey ?? base.siteKey);\n params.set(\"locale\", opts.locale ?? base.locale);\n\n if (opts.include && opts.exclude) {\n throw new Error(\"plenary: `include` and `exclude` are mutually exclusive\");\n }\n if (opts.include?.length) params.set(\"include\", opts.include.join(\",\"));\n if (opts.exclude?.length) params.set(\"exclude\", opts.exclude.join(\",\"));\n if (opts.hydrate) appendHydrate(params, opts.hydrate);\n}\n\n/** Builds the query for a single-entry read (get / singleton). */\nexport function buildReadParams(base: QueryBase, opts: ReadOptions = {}): URLSearchParams {\n const params = new URLSearchParams();\n appendCommon(params, base, opts);\n return params;\n}\n\n/** Builds the query for a list read, including filters, sort, and pagination. */\nexport function buildListParams(base: QueryBase, opts: ListOptions = {}): URLSearchParams {\n const params = new URLSearchParams();\n appendCommon(params, base, opts);\n\n if (opts.where && opts.filters) {\n throw new Error(\"plenary: `where` and `filters` are mutually exclusive\");\n }\n if (opts.filters) {\n params.set(\"filters\", JSON.stringify(opts.filters satisfies FilterTree));\n } else if (opts.where) {\n appendWhere(params, opts.where);\n }\n\n if (opts.sort) appendSort(params, opts.sort);\n if (opts.limit !== undefined) params.set(\"limit\", String(opts.limit));\n if (opts.offset !== undefined) params.set(\"offset\", String(opts.offset));\n if (opts.count) params.set(\"count\", \"true\");\n\n return params;\n}\n","/** The server's error envelope: `{ error: { code, vars? } }`. */\ninterface ServerErrorBody {\n error?: { code?: string; vars?: Record<string, unknown> };\n}\n\n/** Base error for a non-2xx response from the CMS. */\nexport class PlenaryError extends Error {\n readonly status: number;\n /** The server's i18n error code (e.g. `entry_query_limit_exceeded`), if any. */\n readonly code: string;\n /** Interpolation variables the server attached to the code. */\n readonly vars?: Record<string, unknown>;\n\n constructor(status: number, code: string, vars?: Record<string, unknown>) {\n super(`Plenary request failed (${status}): ${code}`);\n this.name = \"PlenaryError\";\n this.status = status;\n this.code = code;\n this.vars = vars;\n }\n}\n\n/** A 404 — model or entry not found (or hidden by a private model). */\nexport class PlenaryNotFoundError extends PlenaryError {\n constructor(code: string, vars?: Record<string, unknown>) {\n super(404, code, vars);\n this.name = \"PlenaryNotFoundError\";\n }\n}\n\n/** A 400/422 — malformed query or invalid input. */\nexport class PlenaryValidationError extends PlenaryError {\n constructor(status: number, code: string, vars?: Record<string, unknown>) {\n super(status, code, vars);\n this.name = \"PlenaryValidationError\";\n }\n}\n\n/** A 403 — the key may not read the requested site/resource. */\nexport class PlenaryForbiddenError extends PlenaryError {\n constructor(code: string, vars?: Record<string, unknown>) {\n super(403, code, vars);\n this.name = \"PlenaryForbiddenError\";\n }\n}\n\n/** Builds the right error subclass from a failed response's status and body. */\nexport function errorFromResponse(status: number, body: unknown): PlenaryError {\n const parsed = body as ServerErrorBody | null;\n const code = parsed?.error?.code ?? `http_${status}`;\n const vars = parsed?.error?.vars;\n switch (status) {\n case 404:\n return new PlenaryNotFoundError(code, vars);\n case 403:\n return new PlenaryForbiddenError(code, vars);\n case 400:\n case 422:\n return new PlenaryValidationError(status, code, vars);\n default:\n return new PlenaryError(status, code, vars);\n }\n}\n","import type {\n ClientConfig,\n Entry,\n ReadOptions,\n ListOptions,\n ListResult,\n PublicFormDefinition,\n FormSubmitOptions,\n FormSubmitResult,\n MediaDetail,\n} from \"./types.js\";\nimport { buildReadParams, buildListParams, type QueryBase } from \"./query.js\";\nimport { errorFromResponse } from \"./errors.js\";\n\n/** The content client returned by {@link createClient}. */\nexport interface PlenaryClient {\n /** Fetch a singleton model's entry. */\n getSingleton<TData = Record<string, unknown>>(\n modelKey: string,\n opts?: ReadOptions,\n ): Promise<Entry<TData>>;\n\n /** Fetch one entry of a collection/page by slug or UUID. */\n getEntry<TData = Record<string, unknown>>(\n modelKey: string,\n slugOrId: string,\n opts?: ReadOptions,\n ): Promise<Entry<TData>>;\n\n /** List entries of a collection/page model. */\n listEntries<TData = Record<string, unknown>>(\n modelKey: string,\n opts?: ListOptions,\n ): Promise<ListResult<TData>>;\n\n /**\n * Fetch a form's render-safe definition — the field list a renderer needs,\n * readable with a public delivery key.\n */\n getForm(formKey: string): Promise<PublicFormDefinition>;\n\n /** Submit a public form. */\n submitForm(\n formKey: string,\n data: Record<string, unknown>,\n opts?: FormSubmitOptions,\n ): Promise<FormSubmitResult>;\n\n /**\n * Fetch a media item's detail (URLs + variants). Requires a key that may read\n * media; a public delivery key cannot, so this is used in authoring/preview\n * contexts or by sites configured with a media-capable key.\n */\n getMedia(mediaId: string): Promise<MediaDetail>;\n}\n\nfunction joinUrl(baseUrl: string, path: string): string {\n return `${baseUrl.replace(/\\/+$/, \"\")}${path}`;\n}\n\nexport function createClient(config: ClientConfig): PlenaryClient {\n const base: QueryBase = { siteKey: config.siteKey, locale: config.locale };\n const doFetch = config.fetch ?? globalThis.fetch;\n\n async function request<T>(\n method: string,\n path: string,\n params: URLSearchParams,\n body?: unknown,\n ): Promise<{ data: T; response: Response }> {\n const url = `${joinUrl(config.baseUrl, path)}?${params.toString()}`;\n const headers: Record<string, string> = { \"X-API-KEY\": config.apiKey };\n if (body !== undefined) headers[\"Content-Type\"] = \"application/json\";\n\n const response = await doFetch(url, {\n method,\n headers,\n body: body !== undefined ? JSON.stringify(body) : undefined,\n });\n\n const parsed = response.status === 204 ? null : await response.json().catch(() => null);\n if (!response.ok) {\n throw errorFromResponse(response.status, parsed);\n }\n return { data: parsed as T, response };\n }\n\n return {\n async getSingleton<TData>(modelKey: string, opts: ReadOptions = {}) {\n const params = buildReadParams(base, opts);\n const { data } = await request<Entry<TData>>(\n \"GET\",\n `/api/v1/content/${encodeURIComponent(modelKey)}`,\n params,\n );\n return data;\n },\n\n async getEntry<TData>(modelKey: string, slugOrId: string, opts: ReadOptions = {}) {\n const params = buildReadParams(base, opts);\n const { data } = await request<Entry<TData>>(\n \"GET\",\n `/api/v1/content/${encodeURIComponent(modelKey)}/${encodeURIComponent(slugOrId)}`,\n params,\n );\n return data;\n },\n\n async listEntries<TData>(modelKey: string, opts: ListOptions = {}) {\n const params = buildListParams(base, opts);\n const { data, response } = await request<Entry<TData>[]>(\n \"GET\",\n `/api/v1/content/${encodeURIComponent(modelKey)}`,\n params,\n );\n const result: ListResult<TData> = { items: data };\n const total = response.headers.get(\"X-Total-Count\");\n if (total !== null) result.total = Number(total);\n return result;\n },\n\n async getForm(formKey: string) {\n const { data } = await request<PublicFormDefinition>(\n \"GET\",\n `/api/v1/forms/${encodeURIComponent(formKey)}`,\n new URLSearchParams(),\n );\n return data;\n },\n\n async submitForm(\n formKey: string,\n data: Record<string, unknown>,\n opts: FormSubmitOptions = {},\n ) {\n const params = new URLSearchParams();\n params.set(\"site_key\", opts.siteKey ?? base.siteKey);\n const body: Record<string, unknown> = { data };\n if (opts.website !== undefined) body.website = opts.website;\n if (opts.ts !== undefined) body.ts = opts.ts;\n const { data: result } = await request<FormSubmitResult>(\n \"POST\",\n `/api/v1/forms/${encodeURIComponent(formKey)}/entries`,\n params,\n body,\n );\n return result;\n },\n\n async getMedia(mediaId: string) {\n const { data } = await request<MediaDetail>(\n \"GET\",\n `/api/v1/media/${encodeURIComponent(mediaId)}`,\n new URLSearchParams(),\n );\n return data;\n },\n };\n}\n","/**\n * Webhook consumer helpers: the payload types Plenary POSTs\n * and framework-neutral verification/parsing. Plenary does not sign requests\n * yet; verification is a shared-secret comparison against a header or query\n * parameter. When HMAC signing ships, it slots in here without changing the\n * call sites.\n */\n\n/** The five entry lifecycle events a content-model webhook can subscribe to. */\nexport type EntryWebhookEvent =\n | \"entry.created\"\n | \"entry.updated\"\n | \"entry.published\"\n | \"entry.unpublished\"\n | \"entry.deleted\";\n\nexport const ENTRY_WEBHOOK_EVENTS: readonly EntryWebhookEvent[] = [\n \"entry.created\",\n \"entry.updated\",\n \"entry.published\",\n \"entry.unpublished\",\n \"entry.deleted\",\n];\n\n/** The event carried by the console's Test button. The body is otherwise an\n * ordinary entry or form payload, built from the model's most recent entry. */\nexport const WEBHOOK_TEST_EVENT = \"webhook.test\";\n\n/** Who caused the event. `label` survives the actor's deletion. */\nexport interface WebhookActor {\n type: \"user\" | \"api_key\" | \"system\";\n id?: string;\n label: string;\n}\n\n/** The entry's identity. Field selection never touches these. */\nexport interface WebhookEntryRef {\n id: string;\n site_key?: string;\n locale?: string;\n slug?: string;\n /** Directory path of the entry's folder (\"/\" at the model root). Does NOT\n * include the entry's own slug — join them to build a URL. */\n path?: string;\n status?: string;\n created_at?: string;\n updated_at?: string;\n published_at?: string;\n}\n\n/** A form entry's identity: no slug, locale, path, or publish state. */\nexport interface WebhookFormEntryRef {\n id: string;\n site_key?: string;\n status?: string;\n created_at?: string;\n source?: { ip?: string; user_agent?: string; referer?: string };\n}\n\n/** Payload for every `entry.*` event (and entry-shaped `webhook.test`). */\nexport interface EntryWebhookPayload<TData = Record<string, unknown>> {\n event: EntryWebhookEvent | typeof WEBHOOK_TEST_EVENT;\n delivery_id: string;\n occurred_at: string;\n webhook: { id: string; name?: string };\n model: { id: string; key?: string };\n entry: WebhookEntryRef;\n actor: WebhookActor;\n /** Paths that differ from the previous revision. Always present; empty for\n * `entry.created` and `entry.deleted`. */\n changed: string[];\n revision_id?: string;\n /** Field values per the webhook's field selection; absent for \"no field data\". */\n data?: TData;\n}\n\n/** Payload for `form.submitted` (and form-shaped `webhook.test`). */\nexport interface FormWebhookPayload<TData = Record<string, unknown>> {\n event: \"form.submitted\" | typeof WEBHOOK_TEST_EVENT;\n delivery_id: string;\n occurred_at: string;\n webhook: { id: string; name?: string };\n form: { id: string; key?: string };\n form_entry: WebhookFormEntryRef;\n data?: TData;\n}\n\nexport type WebhookPayload = EntryWebhookPayload | FormWebhookPayload;\n\nexport function isEntryWebhookPayload(p: WebhookPayload): p is EntryWebhookPayload {\n return \"entry\" in p;\n}\n\nexport function isFormWebhookPayload(p: WebhookPayload): p is FormWebhookPayload {\n return \"form_entry\" in p;\n}\n\n/** A request that is not a valid webhook delivery. `status` is the HTTP status\n * an endpoint should answer with — 4xx makes the delivery fail fast in the\n * console's delivery log instead of being retried for seven hours. */\nexport class PlenaryWebhookError extends Error {\n readonly status: number;\n\n constructor(status: number, message: string) {\n super(message);\n this.name = \"PlenaryWebhookError\";\n this.status = status;\n }\n}\n\n/** Constant-time comparison of a provided secret against the expected one.\n * An empty or missing `secret` never matches, so an unset environment\n * variable can't turn into \"accept an empty header\". */\nexport function verifyWebhookSecret(\n provided: string | null | undefined,\n secret: string,\n): boolean {\n if (typeof provided !== \"string\" || !secret) return false;\n const enc = new TextEncoder();\n const a = enc.encode(provided);\n const b = enc.encode(secret);\n let diff = a.length ^ b.length;\n const len = Math.max(a.length, b.length);\n for (let i = 0; i < len; i++) diff |= (a[i] ?? 0) ^ (b[i] ?? 0);\n return diff === 0;\n}\n\n/** Shape-checks a parsed JSON body and types it. Throws {@link PlenaryWebhookError}\n * (400) for anything that is not a Plenary webhook payload. */\nexport function parseWebhookPayload(body: unknown): WebhookPayload {\n const p = body as Record<string, unknown> | null;\n if (\n p === null ||\n typeof p !== \"object\" ||\n typeof p[\"event\"] !== \"string\" ||\n typeof p[\"delivery_id\"] !== \"string\" ||\n !(\"entry\" in p || \"form_entry\" in p)\n ) {\n throw new PlenaryWebhookError(400, \"Not a Plenary webhook payload\");\n }\n return p as unknown as WebhookPayload;\n}\n\n/** Options for {@link parseWebhookRequest}. */\nexport interface ParseWebhookOptions {\n /** The shared secret configured on the webhook (as a secret header or query\n * parameter in the console). */\n secret: string;\n /** Header the secret arrives in. Default `x-plenary-secret`. */\n secretHeader?: string;\n /** Query parameter checked when the header is absent. Default `secret`. */\n secretQuery?: string;\n}\n\n/**\n * Verifies and parses a webhook delivery from a Fetch-API `Request`:\n * POST only, secret in the configured header or query parameter, JSON body of\n * one of the two payload shapes. Throws {@link PlenaryWebhookError} with the\n * status the endpoint should answer (405, 401, or 400).\n */\nexport async function parseWebhookRequest(\n request: Request,\n options: ParseWebhookOptions,\n): Promise<WebhookPayload> {\n if (request.method !== \"POST\") {\n throw new PlenaryWebhookError(405, \"Method not allowed\");\n }\n if (!options.secret) {\n throw new PlenaryWebhookError(500, \"Webhook secret is not configured\");\n }\n const header = options.secretHeader ?? \"x-plenary-secret\";\n const query = options.secretQuery ?? \"secret\";\n const provided =\n request.headers.get(header) ?? new URL(request.url).searchParams.get(query);\n if (!verifyWebhookSecret(provided, options.secret)) {\n throw new PlenaryWebhookError(401, \"Invalid webhook secret\");\n }\n const body = await request.json().catch(() => {\n throw new PlenaryWebhookError(400, \"Body is not valid JSON\");\n });\n return parseWebhookPayload(body);\n}\n\n/**\n * Revalidation planning — which site URLs one entry webhook affects. Shared by\n * the framework adapters (`@plenary/next`, `@plenary/nuxt`), which own the\n * cache calls; this part is pure.\n */\n\n/** Paths for one model: a fixed path, a list, or a function of the payload.\n * A function may return `null`/`undefined` to revalidate nothing. */\nexport type RevalidatePaths =\n | string\n | string[]\n | ((payload: EntryWebhookPayload) => string | string[] | null | undefined);\n\nexport interface RevalidateOptions extends ParseWebhookOptions {\n /**\n * Extra or overriding paths per model key. A mapped model replaces the\n * default URL entirely — include `entryUrl(payload)` (exported) in the\n * returned list to keep it.\n */\n paths?: Record<string, RevalidatePaths>;\n /** Builds the default URL for unmapped models. Defaults to joining the\n * entry's folder `path` and `slug`; return `null` to skip. */\n entryUrl?: (payload: EntryWebhookPayload) => string | null | undefined;\n /** Entry events that trigger revalidation. Default: all five — revalidation\n * is idempotent and cheap, so over-firing beats a stale page. */\n events?: EntryWebhookEvent[];\n}\n\n/** Default URL for an entry: folder path + slug (`/blog` + `spring-sale` →\n * `/blog/spring-sale`). A root-folder entry slugged `home` is the site's home\n * page, `/`. Entries without a slug (singletons) get no default. */\nexport function entryUrl(payload: EntryWebhookPayload): string | null {\n const { slug, path } = payload.entry;\n if (!slug) return null;\n const dir = path && path !== \"/\" ? path : \"\";\n if (!dir && slug === \"home\") return \"/\";\n return `${dir}/${slug}`;\n}\n\nexport interface RevalidationPlan {\n /** Whether this payload is one the handler acts on. */\n relevant: boolean;\n /** True for the console's Test button — report, don't revalidate. */\n test: boolean;\n paths: string[];\n}\n\n/** Decides which paths one payload revalidates. Form payloads and entry\n * events outside `options.events` are not relevant. */\nexport function planRevalidation(\n payload: WebhookPayload,\n options: RevalidateOptions,\n): RevalidationPlan {\n const none = { relevant: false, test: false, paths: [] };\n if (!isEntryWebhookPayload(payload)) return none;\n\n const test = payload.event === WEBHOOK_TEST_EVENT;\n if (!test && options.events && !options.events.includes(payload.event as EntryWebhookEvent)) {\n return none;\n }\n\n const modelKey = payload.model.key;\n const mapped = modelKey === undefined ? undefined : options.paths?.[modelKey];\n let paths: string | string[] | null | undefined;\n if (mapped === undefined) {\n paths = (options.entryUrl ?? entryUrl)(payload);\n } else {\n paths = typeof mapped === \"function\" ? mapped(payload) : mapped;\n }\n const pathList = paths == null ? [] : typeof paths === \"string\" ? [paths] : paths;\n\n return { relevant: true, test, paths: [...new Set(pathList)] };\n}\n"],"mappings":"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","names":[]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@plenary/client",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "Plenary framework-free API client: content fetch, form submission, and query builder.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"sideEffects": false,
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
"dist"
|
|
19
19
|
],
|
|
20
20
|
"dependencies": {
|
|
21
|
-
"@plenary/core": "1.0
|
|
21
|
+
"@plenary/core": "1.1.0"
|
|
22
22
|
},
|
|
23
23
|
"license": "MIT",
|
|
24
24
|
"scripts": {
|