@kidecms/core 0.1.5 → 0.1.6

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.
@@ -149,11 +149,19 @@ export default function DocumentActions({
149
149
  const form = document.createElement("form");
150
150
  form.method = "post";
151
151
  form.action = restoreEndpoint;
152
- form.innerHTML = `
153
- <input type="hidden" name="_action" value="restore" />
154
- <input type="hidden" name="version" value="${version}" />
155
- <input type="hidden" name="redirectTo" value="${redirectTo ?? window.location.pathname + window.location.search}" />
156
- `;
152
+
153
+ const addInput = (name: string, value: string) => {
154
+ const input = document.createElement("input");
155
+ input.type = "hidden";
156
+ input.name = name;
157
+ input.value = value;
158
+ form.appendChild(input);
159
+ };
160
+
161
+ addInput("_action", "restore");
162
+ addInput("version", String(version));
163
+ addInput("redirectTo", redirectTo ?? window.location.pathname + window.location.search);
164
+
157
165
  document.body.appendChild(form);
158
166
  form.submit();
159
167
  };
package/dist/api.js CHANGED
@@ -5,6 +5,7 @@ import { getCollectionMap, getTranslatableFieldNames, isStructuralField } from "
5
5
  import { getDb } from "./runtime";
6
6
  import { getSchema } from "./schema";
7
7
  import { cloneValue, createRichTextFromPlainText, slugify } from "./values";
8
+ import { dispatchWebhooks } from "./webhooks";
8
9
  const now = () => new Date().toISOString();
9
10
  const pick = (input, keys) => Object.fromEntries(keys.filter((key) => key in input).map((key) => [key, input[key]]));
10
11
  const isJsonField = (field) => field.type === "richText" ||
@@ -405,6 +406,7 @@ export const createCms = (config) => {
405
406
  }
406
407
  const result = await this.findById(docId, {}, context);
407
408
  await collection.hooks?.afterCreate?.(result, hookContext);
409
+ dispatchWebhooks(config, "create", slug, result, context.user);
408
410
  return result;
409
411
  },
410
412
  async update(id, data, context = {}) {
@@ -479,6 +481,7 @@ export const createCms = (config) => {
479
481
  }
480
482
  const result = await this.findById(id, { status: "any" }, context);
481
483
  await collection.hooks?.afterUpdate?.(result, hookContext);
484
+ dispatchWebhooks(config, "update", slug, result, context.user);
482
485
  return result;
483
486
  },
484
487
  async delete(id, context = {}) {
@@ -499,6 +502,7 @@ export const createCms = (config) => {
499
502
  await db.delete(tables.versions).where(eq(tables.versions._docId, id));
500
503
  await db.delete(tables.main).where(eq(tables.main._id, id));
501
504
  await collection.hooks?.afterDelete?.(existing, hookContext);
505
+ dispatchWebhooks(config, "delete", slug, existing, context.user);
502
506
  },
503
507
  async publish(id, context = {}) {
504
508
  if (!collection.drafts)
@@ -529,6 +533,7 @@ export const createCms = (config) => {
529
533
  await db.update(tables.main).set(updateValues).where(eq(tables.main._id, id));
530
534
  const result = await this.findById(id, { status: "any" }, context);
531
535
  await collection.hooks?.afterPublish?.(result, hookContext);
536
+ dispatchWebhooks(config, "publish", slug, result, context.user);
532
537
  return result;
533
538
  },
534
539
  async unpublish(id, context = {}) {
@@ -559,6 +564,7 @@ export const createCms = (config) => {
559
564
  await db.update(tables.main).set(updateValues).where(eq(tables.main._id, id));
560
565
  const result = (await this.findById(id, { status: "any" }, context));
561
566
  await collection.hooks?.afterUnpublish?.(result, hookContext);
567
+ dispatchWebhooks(config, "unpublish", slug, result, context.user);
562
568
  return result;
563
569
  },
564
570
  async schedule(id, publishAt, unpublishAt, context = {}) {
@@ -0,0 +1,71 @@
1
+ const MAX_RETRIES = 3;
2
+ const TIMEOUT_MS = 5000;
3
+ const RETRY_DELAYS = [1000, 3000, 9000];
4
+ const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
5
+ async function deliverOnce(webhook, body) {
6
+ const controller = new AbortController();
7
+ const timeout = setTimeout(() => controller.abort(), TIMEOUT_MS);
8
+ try {
9
+ const response = await fetch(webhook.url, {
10
+ method: webhook.method ?? "POST",
11
+ headers: { "Content-Type": "application/json", ...webhook.headers },
12
+ body,
13
+ signal: controller.signal,
14
+ });
15
+ return { ok: response.ok, status: response.status };
16
+ }
17
+ catch (err) {
18
+ return { ok: false, error: err instanceof Error ? err.message : String(err) };
19
+ }
20
+ finally {
21
+ clearTimeout(timeout);
22
+ }
23
+ }
24
+ async function deliverWebhook(webhook, body) {
25
+ for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
26
+ const result = await deliverOnce(webhook, body);
27
+ if (result.ok) {
28
+ if (attempt > 0) {
29
+ console.log(` [webhook] ${webhook.name} delivered after ${attempt + 1} attempts`);
30
+ }
31
+ return;
32
+ }
33
+ const detail = result.status ? `HTTP ${result.status}` : result.error;
34
+ if (attempt < MAX_RETRIES) {
35
+ console.warn(` [webhook] ${webhook.name} failed (${detail}) — retrying in ${RETRY_DELAYS[attempt]}ms`);
36
+ await sleep(RETRY_DELAYS[attempt]);
37
+ }
38
+ else {
39
+ console.error(` [webhook] ${webhook.name} failed permanently after ${MAX_RETRIES + 1} attempts: ${detail}`);
40
+ }
41
+ }
42
+ }
43
+ export async function dispatchWebhooks(config, event, collectionSlug, doc, user) {
44
+ const webhooks = config.admin?.webhooks;
45
+ if (!webhooks || webhooks.length === 0)
46
+ return;
47
+ const matching = webhooks.filter((webhook) => {
48
+ if (!webhook.events.includes(event))
49
+ return false;
50
+ if (webhook.collections && !webhook.collections.includes(collectionSlug))
51
+ return false;
52
+ return true;
53
+ });
54
+ if (matching.length === 0)
55
+ return;
56
+ const context = {
57
+ user: user ?? null,
58
+ event,
59
+ collection: collectionSlug,
60
+ timestamp: new Date().toISOString(),
61
+ };
62
+ // Fire all matching webhooks in parallel — don't block the operation
63
+ for (const webhook of matching) {
64
+ const payload = webhook.payload ? webhook.payload(doc, context) : { event, collection: collectionSlug, doc, user, timestamp: context.timestamp };
65
+ const body = JSON.stringify(payload);
66
+ // Fire and forget — failures are logged but don't bubble up
67
+ deliverWebhook(webhook, body).catch((err) => {
68
+ console.error(` [webhook] ${webhook.name} dispatcher error:`, err);
69
+ });
70
+ }
71
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kidecms/core",
3
- "version": "0.1.5",
3
+ "version": "0.1.6",
4
4
  "description": "Code-first CMS framework built for Astro.",
5
5
  "author": "Matti Hernesniemi",
6
6
  "license": "MIT",