@beechcms/api 0.6.5 → 0.6.7

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.
Files changed (36) hide show
  1. package/assets/dashboard/assets/{chart-IiMhdPtU.js → chart-D_uST30B.js} +2 -2
  2. package/assets/dashboard/assets/index-B58kBLrh.css +1 -0
  3. package/assets/dashboard/assets/{index-Culul_0c.js → index-DjniT5Z9.js} +93 -93
  4. package/assets/dashboard/assets/{pie-chart-recharts-BZAzxTy0.js → pie-chart-recharts-CnVomiSi.js} +1 -1
  5. package/assets/dashboard/assets/{timeseries-chart-recharts-BFQVPxPT.js → timeseries-chart-recharts-CTqi8qnV.js} +1 -1
  6. package/assets/dashboard/index.html +2 -2
  7. package/dist/auth/constants.d.ts +10 -0
  8. package/dist/auth/utils/refresh-token.d.ts +1 -0
  9. package/dist/features/automations/api/automations.schema.d.ts +16 -16
  10. package/dist/features/automations/engine/cron-runner.utils.d.ts +1 -0
  11. package/dist/features/content/constants.d.ts +12 -0
  12. package/dist/features/rotate-field/rotate-field.schema.d.ts +11 -0
  13. package/dist/index.d.ts +1 -1
  14. package/dist/index.js +115 -103
  15. package/dist/public/access-policy.d.ts +1 -1
  16. package/dist/public/api-key-middleware.d.ts +6 -0
  17. package/dist/public/cache-utils.d.ts +10 -0
  18. package/dist/public/problem-details.d.ts +32 -0
  19. package/dist/public/public-errors.d.ts +13 -0
  20. package/dist/public/response-builder.d.ts +22 -0
  21. package/dist/shared/db/fixtures/demo-data.fixtures.d.ts +12 -0
  22. package/dist/shared/db/repositories/content.repository.d1.d.ts +4 -3
  23. package/dist/shared/db/repositories/d1-setup-checklist.repository.d.ts +12 -0
  24. package/dist/shared/db/repositories/demo-data.repository.d1.d.ts +1 -1
  25. package/dist/shared/demo-data-sql.d.ts +1 -0
  26. package/dist/shared/email/email.types.d.ts +79 -0
  27. package/dist/shared/email/templates/automation-mail.d.ts +7 -0
  28. package/dist/shared/email/templates/password-changed.d.ts +15 -0
  29. package/dist/shared/email/templates/password-reset.d.ts +18 -0
  30. package/dist/shared/email/templates/shell.d.ts +58 -0
  31. package/dist/shared/jobs/fts-sync.d.ts +1 -0
  32. package/dist/shared/storage/upload.d.ts +7 -0
  33. package/dist/shared/utils/request-utils.d.ts +13 -0
  34. package/dist/shared/utils/storage-utils.d.ts +7 -0
  35. package/package.json +2 -2
  36. package/assets/dashboard/assets/index-DWm-ERoT.css +0 -1
package/dist/index.js CHANGED
@@ -2249,25 +2249,25 @@ setupApp.post("/auth/setup", async (context) => {
2249
2249
  id: "sec-0",
2250
2250
  hideLabel: true,
2251
2251
  columns: [
2252
- { id: "c0", widgets: [{ id: "w0", type: "core/text", config: { content: "# Acme SaaS Analytics\nBenvenuto nella dashboard di produzione. Qui puoi monitorare le metriche in tempo reale." } }] }
2252
+ { id: "c0", widgets: [{ id: "w0", type: "core/text", config: { content: "# Acme SaaS Analytics\nWelcome to your production dashboard. Monitor real-time metrics and KPIs here." } }] }
2253
2253
  ]
2254
2254
  },
2255
2255
  {
2256
2256
  id: "sec-1",
2257
2257
  hideLabel: true,
2258
2258
  columns: [
2259
- { id: "c1", widgets: [{ id: "w1", type: "core/stat", config: { seedSlug: "clienti", formula: { op: "sum", column: "mrr" }, window: "all" }, title: "Totale MRR" }] },
2260
- { id: "c2", widgets: [{ id: "w2", type: "core/stat", config: { seedSlug: "clienti", formula: { op: "count" }, window: "all" }, title: "Clienti Attivi" }] },
2261
- { id: "c3", widgets: [{ id: "w3", type: "core/stat", config: { seedSlug: "abbonamenti", formula: { op: "count" }, window: "all" }, title: "Abbonamenti" }] },
2262
- { id: "c4", widgets: [{ id: "w4", type: "core/stat", config: { seedSlug: "ticket", formula: { op: "countWhere", column: "ticket_status", value: "open" }, window: "all" }, title: "Ticket Aperti" }] }
2259
+ { id: "c1", widgets: [{ id: "w1", type: "core/stat", config: { seedSlug: "clienti", formula: { op: "sum", column: "mrr" }, window: "all" }, title: "Total MRR" }] },
2260
+ { id: "c2", widgets: [{ id: "w2", type: "core/stat", config: { seedSlug: "clienti", formula: { op: "count" }, window: "all" }, title: "Active Customers" }] },
2261
+ { id: "c3", widgets: [{ id: "w3", type: "core/stat", config: { seedSlug: "abbonamenti", formula: { op: "count" }, window: "all" }, title: "Subscriptions" }] },
2262
+ { id: "c4", widgets: [{ id: "w4", type: "core/stat", config: { seedSlug: "ticket", formula: { op: "countWhere", column: "ticket_status", value: "open" }, window: "all" }, title: "Open Tickets" }] }
2263
2263
  ]
2264
2264
  },
2265
2265
  {
2266
2266
  id: "sec-2",
2267
2267
  hideLabel: true,
2268
2268
  columns: [
2269
- { id: "c5", widgets: [{ id: "w5", type: "core/area-chart", config: { seedSlug: "clienti", groupColumn: "created_at", window: "all" }, title: "Andamento Iscrizioni" }] },
2270
- { id: "c6", widgets: [{ id: "w6", type: "core/pie-chart", config: { seedSlug: "clienti", column: "tier", window: "all" }, title: "Distribuzione Piani" }] }
2269
+ { id: "c5", widgets: [{ id: "w5", type: "core/area-chart", config: { seedSlug: "clienti", groupColumn: "created_at", window: "all" }, title: "Signups Trend" }] },
2270
+ { id: "c6", widgets: [{ id: "w6", type: "core/pie-chart", config: { seedSlug: "clienti", column: "tier", window: "all" }, title: "Plan Distribution" }] }
2271
2271
  ],
2272
2272
  columnSpans: [8, 4]
2273
2273
  },
@@ -2275,8 +2275,8 @@ setupApp.post("/auth/setup", async (context) => {
2275
2275
  id: "sec-3",
2276
2276
  hideLabel: true,
2277
2277
  columns: [
2278
- { id: "c7", widgets: [{ id: "w7", type: "core/data-table", config: { seedSlug: "clienti", pageSize: 5 }, title: "Ultimi Iscritti" }] },
2279
- { id: "c8", widgets: [{ id: "w8", type: "core/data-table", config: { seedSlug: "changelog", pageSize: 5 }, title: "Ultime Release" }] }
2278
+ { id: "c7", widgets: [{ id: "w7", type: "core/data-table", config: { seedSlug: "clienti", pageSize: 5 }, title: "Recent Signups" }] },
2279
+ { id: "c8", widgets: [{ id: "w8", type: "core/data-table", config: { seedSlug: "changelog", pageSize: 5 }, title: "Latest Releases" }] }
2280
2280
  ],
2281
2281
  columnSpans: [6, 6]
2282
2282
  }
@@ -2292,17 +2292,17 @@ setupApp.post("/auth/setup", async (context) => {
2292
2292
  id: "sec-4",
2293
2293
  hideLabel: true,
2294
2294
  columns: [
2295
- { id: "c9", widgets: [{ id: "w9", type: "core/stat", config: { seedSlug: "ticket", formula: { op: "countWhere", column: "priority", value: "high" }, window: "all" }, title: "Ticket Alta Priorit\xE0" }] },
2296
- { id: "c10", widgets: [{ id: "w10", type: "core/stat", config: { seedSlug: "ticket", formula: { op: "countWhere", column: "ticket_status", value: "closed" }, window: "all" }, title: "Ticket Chiusi" }] },
2297
- { id: "c11", widgets: [{ id: "w11", type: "core/stat", config: { seedSlug: "changelog", formula: { op: "count" }, window: "all" }, title: "Update Rilasciati" }] }
2295
+ { id: "c9", widgets: [{ id: "w9", type: "core/stat", config: { seedSlug: "ticket", formula: { op: "countWhere", column: "priority", value: "high" }, window: "all" }, title: "High Priority Tickets" }] },
2296
+ { id: "c10", widgets: [{ id: "w10", type: "core/stat", config: { seedSlug: "ticket", formula: { op: "countWhere", column: "ticket_status", value: "closed" }, window: "all" }, title: "Closed Tickets" }] },
2297
+ { id: "c11", widgets: [{ id: "w11", type: "core/stat", config: { seedSlug: "changelog", formula: { op: "count" }, window: "all" }, title: "Released Updates" }] }
2298
2298
  ]
2299
2299
  },
2300
2300
  {
2301
2301
  id: "sec-5",
2302
2302
  hideLabel: true,
2303
2303
  columns: [
2304
- { id: "c12", widgets: [{ id: "w12", type: "core/bar-chart", config: { seedSlug: "ticket", groupColumn: "created_at", window: "all" }, title: "Ticket nel tempo" }] },
2305
- { id: "c13", widgets: [{ id: "w13", type: "core/line-chart", config: { seedSlug: "abbonamenti", groupColumn: "created_at", window: "all" }, title: "Nuovi Abbonamenti" }] }
2304
+ { id: "c12", widgets: [{ id: "w12", type: "core/bar-chart", config: { seedSlug: "ticket", groupColumn: "created_at", window: "all" }, title: "Tickets Over Time" }] },
2305
+ { id: "c13", widgets: [{ id: "w13", type: "core/line-chart", config: { seedSlug: "abbonamenti", groupColumn: "created_at", window: "all" }, title: "New Subscriptions" }] }
2306
2306
  ],
2307
2307
  columnSpans: [6, 6]
2308
2308
  },
@@ -2310,7 +2310,7 @@ setupApp.post("/auth/setup", async (context) => {
2310
2310
  id: "sec-6",
2311
2311
  hideLabel: true,
2312
2312
  columns: [
2313
- { id: "c14", widgets: [{ id: "w14", type: "core/data-table", config: { seedSlug: "ticket", pageSize: 10 }, title: "Ticket in lavorazione" }] }
2313
+ { id: "c14", widgets: [{ id: "w14", type: "core/data-table", config: { seedSlug: "ticket", pageSize: 10 }, title: "Active Tickets" }] }
2314
2314
  ]
2315
2315
  }
2316
2316
  ]
@@ -2342,6 +2342,7 @@ import { Hono as Hono6 } from "hono";
2342
2342
  import {
2343
2343
  validateAndSanitizeSeedPayload as validateAndSanitizeSeedPayload5,
2344
2344
  resolvePolicies as resolvePolicies5,
2345
+ EntryNotFoundError as EntryNotFoundError6,
2345
2346
  RelationTargetNotFoundError
2346
2347
  } from "@beechcms/core";
2347
2348
 
@@ -2503,18 +2504,17 @@ draftApp.post("/:slug/:id/draft/publish", draftGuard, async (context) => {
2503
2504
  const id = context.req.param("id");
2504
2505
  const seed = context.get("getSeed")(slug);
2505
2506
  const repository = context.get("repository");
2506
- const draft = await repository.getDraft(seed, id);
2507
- if (!draft) {
2508
- return publicProblem(context, {
2509
- type: "draft-not-found",
2510
- title: "Not Found",
2511
- status: 404,
2512
- detail: "No pending draft to publish"
2513
- });
2514
- }
2515
2507
  try {
2516
2508
  await repository.publishDraft(seed, id);
2517
2509
  } catch (err) {
2510
+ if (err instanceof EntryNotFoundError6) {
2511
+ return publicProblem(context, {
2512
+ type: "draft-not-found",
2513
+ title: "Not Found",
2514
+ status: 404,
2515
+ detail: "No pending draft to publish"
2516
+ });
2517
+ }
2518
2518
  if (err instanceof RelationTargetNotFoundError) {
2519
2519
  return publicProblem(context, {
2520
2520
  type: "relation-target-not-found",
@@ -2525,9 +2525,7 @@ draftApp.post("/:slug/:id/draft/publish", draftGuard, async (context) => {
2525
2525
  }
2526
2526
  throw err;
2527
2527
  }
2528
- const displayValue = draft[seed.displayNameAlias];
2529
- const displayStr = typeof displayValue === "string" ? displayValue : id;
2530
- logDraftActivity(context, id, slug, displayStr, "draft published");
2528
+ logDraftActivity(context, id, slug, id, "draft published");
2531
2529
  return context.json({ success: true });
2532
2530
  });
2533
2531
  draftApp.delete("/:slug/:id/draft", draftGuard, async (context) => {
@@ -4736,7 +4734,7 @@ function withCachedResponse(edgeCache, cacheKey, response) {
4736
4734
  }
4737
4735
 
4738
4736
  // src/public/read-single.ts
4739
- import { EntryNotFoundError as EntryNotFoundError6 } from "@beechcms/core";
4737
+ import { EntryNotFoundError as EntryNotFoundError7 } from "@beechcms/core";
4740
4738
 
4741
4739
  // src/public/entry-projection.ts
4742
4740
  import { filterEntryForActor as filterEntryForActor2 } from "@beechcms/core";
@@ -4787,7 +4785,7 @@ async function readSingleEntry(input) {
4787
4785
  meta: buildPublicSingleMeta(seedSlug)
4788
4786
  };
4789
4787
  } catch (error) {
4790
- if (error instanceof EntryNotFoundError6) {
4788
+ if (error instanceof EntryNotFoundError7) {
4791
4789
  return { ok: false, detail: `Entry '${label}' not found for content type '${seedSlug}'.` };
4792
4790
  }
4793
4791
  throw error;
@@ -5171,7 +5169,7 @@ async function publicAddHandler(context) {
5171
5169
  }
5172
5170
 
5173
5171
  // src/public/public-edit.ts
5174
- import { isValidContentStatus as isValidContentStatus4, resolvePolicies as resolvePolicies9, EntryNotFoundError as EntryNotFoundError7 } from "@beechcms/core";
5172
+ import { isValidContentStatus as isValidContentStatus4, resolvePolicies as resolvePolicies9, EntryNotFoundError as EntryNotFoundError8 } from "@beechcms/core";
5175
5173
  var UUID_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
5176
5174
  function errorMessage(context, error) {
5177
5175
  if (context.env.ENV !== "production" && error instanceof Error) return error.message;
@@ -5298,7 +5296,7 @@ async function publicEditHandler(context) {
5298
5296
  );
5299
5297
  return context.json({ success: true, id, slug: slugResult.value.nextSlug }, 200);
5300
5298
  } catch (error) {
5301
- if (error instanceof EntryNotFoundError7) {
5299
+ if (error instanceof EntryNotFoundError8) {
5302
5300
  return publicProblem(context, { type: "entry-not-found", title: "Not Found", status: 404, detail: `Entry '${id}' not found for content type '${seedSlug}'.` });
5303
5301
  }
5304
5302
  console.error("Public edit error:", error);
@@ -5604,7 +5602,7 @@ import { createMiddleware as createMiddleware2 } from "hono/factory";
5604
5602
 
5605
5603
  // src/shared/db/repositories/content.repository.d1.ts
5606
5604
  import {
5607
- EntryNotFoundError as EntryNotFoundError8,
5605
+ EntryNotFoundError as EntryNotFoundError9,
5608
5606
  RepositoryError as RepositoryError2,
5609
5607
  SlugConflictError as SlugConflictError4,
5610
5608
  RelationTargetNotFoundError as RelationTargetNotFoundError2,
@@ -5862,13 +5860,13 @@ var D1ContentRepository = class extends BaseD1Repository {
5862
5860
  const tableName = this.getTableName(seed.slug);
5863
5861
  const entryRow = await this.database.prepare(`SELECT * FROM ${tableName} WHERE id = ? LIMIT 1`).bind(id).first();
5864
5862
  if (!entryRow) {
5865
- throw new EntryNotFoundError8(`Entry ${id} not found in ${seed.slug}`);
5863
+ throw new EntryNotFoundError9(`Entry ${id} not found in ${seed.slug}`);
5866
5864
  }
5867
5865
  const data = await this.rowToData(seed, entryRow);
5868
5866
  await this.attachMultiRelations(seed, id, data);
5869
5867
  return data;
5870
5868
  } catch (error) {
5871
- if (error instanceof EntryNotFoundError8) throw error;
5869
+ if (error instanceof EntryNotFoundError9) throw error;
5872
5870
  throw this.mapError(error, `findById(${seed.slug}, ${id})`);
5873
5871
  }
5874
5872
  }
@@ -5882,13 +5880,13 @@ var D1ContentRepository = class extends BaseD1Repository {
5882
5880
  const tableName = this.getTableName(seed.slug);
5883
5881
  const entryRow = await this.database.prepare(`SELECT * FROM ${tableName} WHERE slug = ? LIMIT 1`).bind(slug).first();
5884
5882
  if (!entryRow) {
5885
- throw new EntryNotFoundError8(`Entry with slug "${slug}" not found in ${seed.slug}`);
5883
+ throw new EntryNotFoundError9(`Entry with slug "${slug}" not found in ${seed.slug}`);
5886
5884
  }
5887
5885
  const data = await this.rowToData(seed, entryRow);
5888
5886
  await this.attachMultiRelations(seed, entryRow.id, data);
5889
5887
  return data;
5890
5888
  } catch (error) {
5891
- if (error instanceof EntryNotFoundError8) throw error;
5889
+ if (error instanceof EntryNotFoundError9) throw error;
5892
5890
  throw this.mapError(error, `findBySlug(${seed.slug}, ${slug})`);
5893
5891
  }
5894
5892
  }
@@ -6265,16 +6263,16 @@ var D1ContentRepository = class extends BaseD1Repository {
6265
6263
  if (batchStmts.length === 1) {
6266
6264
  const updateResult = await batchStmts[0].run();
6267
6265
  if (updateResult.meta.changes === 0) {
6268
- throw new EntryNotFoundError8(`Entry ${id} not found in ${seed.slug}`);
6266
+ throw new EntryNotFoundError9(`Entry ${id} not found in ${seed.slug}`);
6269
6267
  }
6270
6268
  } else if (batchStmts.length > 1) {
6271
6269
  const results = await this.database.batch(batchStmts);
6272
6270
  if (stmt && (results[0].meta?.changes ?? 0) === 0) {
6273
- throw new EntryNotFoundError8(`Entry ${id} not found in ${seed.slug}`);
6271
+ throw new EntryNotFoundError9(`Entry ${id} not found in ${seed.slug}`);
6274
6272
  }
6275
6273
  }
6276
6274
  } catch (error) {
6277
- if (error instanceof EntryNotFoundError8) throw error;
6275
+ if (error instanceof EntryNotFoundError9) throw error;
6278
6276
  throw this.mapError(error, `update(${seed.slug}, ${id})`);
6279
6277
  }
6280
6278
  if (this.hooks?.afterUpdate) {
@@ -6299,12 +6297,12 @@ var D1ContentRepository = class extends BaseD1Repository {
6299
6297
  const tableName = this.getTableName(seed.slug);
6300
6298
  const entryRow = await this.database.prepare(`SELECT * FROM ${tableName} WHERE id = ?`).bind(id).first();
6301
6299
  if (!entryRow) {
6302
- throw new EntryNotFoundError8(`Entry ${id} not found in ${seed.slug}`);
6300
+ throw new EntryNotFoundError9(`Entry ${id} not found in ${seed.slug}`);
6303
6301
  }
6304
6302
  await this.database.prepare(`DELETE FROM ${tableName} WHERE id = ?`).bind(id).run();
6305
6303
  row = await this.rowToData(seed, entryRow);
6306
6304
  } catch (error) {
6307
- if (error instanceof EntryNotFoundError8) throw error;
6305
+ if (error instanceof EntryNotFoundError9) throw error;
6308
6306
  throw this.mapError(error, `delete(${seed.slug}, ${id})`);
6309
6307
  }
6310
6308
  if (this.hooks?.afterDelete) {
@@ -6560,7 +6558,8 @@ var D1ContentRepository = class extends BaseD1Repository {
6560
6558
  try {
6561
6559
  if (!seed.allowDrafts) return false;
6562
6560
  const draftTableName = this.getTableName(seed.slug, true);
6563
- const draftExistsResult = await this.database.prepare(`SELECT 1 FROM ${draftTableName} WHERE entry_id = ? LIMIT 1`).bind(entryId).first();
6561
+ const liveTableName = this.getTableName(seed.slug);
6562
+ const draftExistsResult = await this.database.prepare(`SELECT 1 FROM ${draftTableName} WHERE entry_id = ? UNION SELECT 1 FROM ${liveTableName} WHERE id = ? AND status = 'draft' LIMIT 1`).bind(entryId, entryId).first();
6564
6563
  return draftExistsResult !== null;
6565
6564
  } catch (error) {
6566
6565
  throw this.mapError(error, `hasDraft(${seed.slug}, ${entryId})`);
@@ -6569,7 +6568,8 @@ var D1ContentRepository = class extends BaseD1Repository {
6569
6568
  /**
6570
6569
  * Promotes a draft to the live entry: validates all relation targets referenced by the
6571
6570
  * draft still exist, copies the draft's scalar fields into the live row, replaces the
6572
- * live junction rows with the draft's, and deletes the draft — all as one batch.
6571
+ * live junction rows with the draft's, sets status to 'published', and deletes the draft — all as one batch.
6572
+ * If no mirror draft row exists but the live entry has status = 'draft', updates status to 'published'.
6573
6573
  * A no-op if `seed.allowDrafts` is false.
6574
6574
  *
6575
6575
  * @throws EntryNotFoundError if no draft row exists for `entryId`.
@@ -6582,7 +6582,12 @@ var D1ContentRepository = class extends BaseD1Repository {
6582
6582
  const liveTableName = this.getTableName(seed.slug);
6583
6583
  const draftRow = await this.database.prepare(`SELECT * FROM ${draftTableName} WHERE entry_id = ?`).bind(entryId).first();
6584
6584
  if (!draftRow) {
6585
- throw new EntryNotFoundError8(`No draft found for ${entryId} in ${seed.slug}`);
6585
+ const liveRow = await this.database.prepare(`SELECT 1 FROM ${liveTableName} WHERE id = ? AND status = 'draft'`).bind(entryId).first();
6586
+ if (!liveRow) {
6587
+ throw new EntryNotFoundError9(`No draft found for ${entryId} in ${seed.slug}`);
6588
+ }
6589
+ await this.database.prepare(`UPDATE ${liveTableName} SET status = 'published', updated_at = (unixepoch()) WHERE id = ?`).bind(entryId).run();
6590
+ return;
6586
6591
  }
6587
6592
  let touchedFields = [];
6588
6593
  if (draftRow && typeof draftRow["_touched_fields"] === "string") {
@@ -6606,15 +6611,9 @@ var D1ContentRepository = class extends BaseD1Repository {
6606
6611
  queryBindings.push(draftRow[branch.alias]);
6607
6612
  }
6608
6613
  }
6609
- let updateSql = "";
6610
- if (updateClauses.length > 0) {
6611
- updateClauses.push("updated_at = (unixepoch())");
6612
- updateSql = `UPDATE ${liveTableName} SET ${updateClauses.join(", ")} WHERE id = ?`;
6613
- queryBindings.push(entryId);
6614
- } else {
6615
- updateSql = `UPDATE ${liveTableName} SET updated_at = (unixepoch()) WHERE id = ?`;
6616
- queryBindings.push(entryId);
6617
- }
6614
+ updateClauses.push("status = 'published'", "updated_at = (unixepoch())");
6615
+ const updateSql = `UPDATE ${liveTableName} SET ${updateClauses.join(", ")} WHERE id = ?`;
6616
+ queryBindings.push(entryId);
6618
6617
  const batchStmts = [
6619
6618
  this.database.prepare(updateSql).bind(...queryBindings),
6620
6619
  this.database.prepare(`DELETE FROM ${draftTableName} WHERE entry_id = ?`).bind(entryId)
@@ -6637,7 +6636,7 @@ var D1ContentRepository = class extends BaseD1Repository {
6637
6636
  }
6638
6637
  await this.database.batch(batchStmts);
6639
6638
  } catch (error) {
6640
- if (error instanceof EntryNotFoundError8) throw error;
6639
+ if (error instanceof EntryNotFoundError9) throw error;
6641
6640
  if (error instanceof RelationTargetNotFoundError2) throw error;
6642
6641
  throw this.mapError(error, `publishDraft(${seed.slug}, ${entryId})`);
6643
6642
  }
@@ -6665,15 +6664,28 @@ var D1ContentRepository = class extends BaseD1Repository {
6665
6664
  try {
6666
6665
  if (!seed.allowDrafts) return;
6667
6666
  const draftTableName = this.getTableName(seed.slug, true);
6668
- await this.database.prepare(`DELETE FROM ${draftTableName} WHERE entry_id = ?`).bind(entryId).run();
6667
+ const liveTableName = this.getTableName(seed.slug);
6668
+ const hasMirror = await this.database.prepare(`SELECT 1 FROM ${draftTableName} WHERE entry_id = ? LIMIT 1`).bind(entryId).first();
6669
+ if (hasMirror) {
6670
+ const batchStmts = [
6671
+ this.database.prepare(`DELETE FROM ${draftTableName} WHERE entry_id = ?`).bind(entryId)
6672
+ ];
6673
+ for (const branch of multiRelBranches(seed)) {
6674
+ const jdt = jDraftTable(seed.slug, branch.alias);
6675
+ batchStmts.push(this.database.prepare(`DELETE FROM ${jdt} WHERE entry_id = ?`).bind(entryId));
6676
+ }
6677
+ await this.database.batch(batchStmts);
6678
+ } else {
6679
+ await this.database.prepare(`DELETE FROM ${liveTableName} WHERE id = ? AND status = 'draft'`).bind(entryId).run();
6680
+ }
6669
6681
  } catch (error) {
6670
6682
  throw this.mapError(error, `deleteDraft(${seed.slug}, ${entryId})`);
6671
6683
  }
6672
6684
  }
6673
6685
  /**
6674
6686
  * Finds every pending draft across the given seeds, `UNION ALL`-ing one query per seed
6675
- * so results span heterogeneous `content_{slug}_drafts` tables, then left-joins each
6676
- * against `activity_logs` to attribute the most recent "draft saved" action.
6687
+ * so results span heterogeneous `content_{slug}_drafts` tables and live draft entries,
6688
+ * then left-joins each against `activity_logs` to attribute the most recent action.
6677
6689
  *
6678
6690
  * @param seeds Seeds to search; drafts are looked up per-seed since draft tables aren't unified.
6679
6691
  * @returns Draft summaries ordered by most-recently-updated first. `title` falls back to the
@@ -6689,17 +6701,18 @@ var D1ContentRepository = class extends BaseD1Repository {
6689
6701
  const liveTable = `content_${seed.slug}`;
6690
6702
  const titleCol = seed.displayNameAlias;
6691
6703
  const isScalar = seed.branches.some((b) => b.alias === titleCol && !(b.type === "relation" && b.multiple === true));
6692
- const titleExpr = isScalar ? `COALESCE(d.${titleCol}, l.${titleCol})` : `COALESCE(l.slug, d.entry_id)`;
6704
+ const titleExpr = isScalar ? `COALESCE(d.${titleCol}, l.${titleCol}, l.slug, d.entry_id, l.id)` : `COALESCE(l.slug, d.entry_id, l.id)`;
6693
6705
  const seedLabel = seed.labelPlural ?? seed.label;
6694
6706
  unionSelects.push(`
6695
6707
  SELECT
6696
6708
  ? AS seed_slug,
6697
6709
  ? AS seed_label,
6698
- d.entry_id AS id,
6699
- d.updated_at AS updated_at,
6710
+ COALESCE(d.entry_id, l.id) AS id,
6711
+ COALESCE(d.updated_at, l.updated_at) AS updated_at,
6700
6712
  ${titleExpr} AS title
6701
- FROM ${draftTable} d
6702
- LEFT JOIN ${liveTable} l ON l.id = d.entry_id
6713
+ FROM ${liveTable} l
6714
+ LEFT JOIN ${draftTable} d ON d.entry_id = l.id
6715
+ WHERE l.status = 'draft' OR d.entry_id IS NOT NULL
6703
6716
  `);
6704
6717
  bindings.push(seed.slug, seedLabel);
6705
6718
  }
@@ -6721,8 +6734,7 @@ var D1ContentRepository = class extends BaseD1Repository {
6721
6734
  SELECT id FROM activity_logs
6722
6735
  WHERE entity_id = ad.id
6723
6736
  AND entity_slug = ad.seed_slug
6724
- AND action = 'update'
6725
- AND json_extract(details, '$.note') = 'draft saved'
6737
+ AND action IN ('create', 'update')
6726
6738
  ORDER BY created_at DESC
6727
6739
  LIMIT 1
6728
6740
  )
@@ -7664,13 +7676,13 @@ var D1SiteSettingsRepository = class {
7664
7676
  const { results } = await this.db.prepare("SELECT key, value FROM site_settings").all();
7665
7677
  const map = new Map(results.map((r) => [r.key, r.value]));
7666
7678
  return {
7667
- siteTitle: map.get("siteTitle") ?? DEFAULTS.siteTitle,
7668
- defaultLanguage: map.get("defaultLanguage") ?? DEFAULTS.defaultLanguage,
7669
- timezone: map.get("timezone") ?? DEFAULTS.timezone,
7670
- currency: map.get("currency") ?? DEFAULTS.currency,
7671
- companyName: map.get("companyName") ?? null,
7672
- companyWebsite: map.get("companyWebsite") ?? null,
7673
- companyAbbreviation: map.get("companyAbbreviation") ?? null
7679
+ siteTitle: map.get("siteTitle") || DEFAULTS.siteTitle,
7680
+ defaultLanguage: map.get("defaultLanguage") || DEFAULTS.defaultLanguage,
7681
+ timezone: map.get("timezone") || DEFAULTS.timezone,
7682
+ currency: map.get("currency") || DEFAULTS.currency,
7683
+ companyName: map.get("companyName") || null,
7684
+ companyWebsite: map.get("companyWebsite") || null,
7685
+ companyAbbreviation: map.get("companyAbbreviation") || null
7674
7686
  };
7675
7687
  }
7676
7688
  async setMany(values) {
@@ -7679,7 +7691,7 @@ var D1SiteSettingsRepository = class {
7679
7691
  const stmts = entries.map(
7680
7692
  ([key, value]) => this.db.prepare(
7681
7693
  "INSERT INTO site_settings (key, value) VALUES (?, ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value"
7682
- ).bind(key, value)
7694
+ ).bind(key, value ?? "")
7683
7695
  );
7684
7696
  await this.db.batch(stmts);
7685
7697
  }
@@ -7689,10 +7701,10 @@ var D1SiteSettingsRepository = class {
7689
7701
  var nowSec = Math.floor(Date.now() / 1e3);
7690
7702
  var DAY = 86400;
7691
7703
  var t_c01 = nowSec - 25 * DAY;
7692
- var t_c02 = nowSec - 20 * DAY;
7693
- var t_c03 = nowSec - 15 * DAY;
7694
- var t_c04 = nowSec - 10 * DAY;
7695
- var t_c05 = nowSec - 3 * DAY;
7704
+ var t_c02 = nowSec - 18 * DAY;
7705
+ var t_c03 = nowSec - 12 * DAY;
7706
+ var t_c04 = nowSec - 6 * DAY;
7707
+ var t_c05 = nowSec - 1 * DAY;
7696
7708
  function toIsoDate(sec) {
7697
7709
  return new Date(sec * 1e3).toISOString().split("T")[0];
7698
7710
  }
@@ -7723,11 +7735,11 @@ var DEMO_CLIENTI_FIXTURES = [
7723
7735
  },
7724
7736
  {
7725
7737
  id: "c0200000-0000-4000-8000-000000000002",
7726
- slug: "davide-ferrari",
7738
+ slug: "david-ferrari",
7727
7739
  status: "published",
7728
7740
  data: {
7729
- name: "Davide Ferrari",
7730
- email: "davide.ferrari@innovasolutions.it",
7741
+ name: "David Ferrari",
7742
+ email: "david.ferrari@innovasolutions.io",
7731
7743
  company: "Innova Solutions",
7732
7744
  tier: "pro",
7733
7745
  account_status: "active",
@@ -7736,11 +7748,11 @@ var DEMO_CLIENTI_FIXTURES = [
7736
7748
  },
7737
7749
  {
7738
7750
  id: "c0300000-0000-4000-8000-000000000003",
7739
- slug: "filippo-lombardi",
7751
+ slug: "philip-lombardi",
7740
7752
  status: "published",
7741
7753
  data: {
7742
- name: "Filippo Lombardi",
7743
- email: "filippo.lombardi@novalabs.dev",
7754
+ name: "Philip Lombardi",
7755
+ email: "philip.lombardi@novalabs.dev",
7744
7756
  company: "Nova Labs",
7745
7757
  tier: "free",
7746
7758
  account_status: "churned",
@@ -7749,11 +7761,11 @@ var DEMO_CLIENTI_FIXTURES = [
7749
7761
  },
7750
7762
  {
7751
7763
  id: "c0400000-0000-4000-8000-000000000004",
7752
- slug: "elisa-gallo",
7764
+ slug: "sarah-gallo",
7753
7765
  status: "published",
7754
7766
  data: {
7755
- name: "Elisa Gallo",
7756
- email: "elisa.gallo@alphadynamics.io",
7767
+ name: "Sarah Gallo",
7768
+ email: "sarah.gallo@alphadynamics.io",
7757
7769
  company: "Alpha Dynamics",
7758
7770
  tier: "enterprise",
7759
7771
  account_status: "active",
@@ -7762,11 +7774,11 @@ var DEMO_CLIENTI_FIXTURES = [
7762
7774
  },
7763
7775
  {
7764
7776
  id: "c0500000-0000-4000-8000-000000000005",
7765
- slug: "matteo-conti",
7777
+ slug: "matthew-conti",
7766
7778
  status: "published",
7767
7779
  data: {
7768
- name: "Matteo Conti",
7769
- email: "matteo.conti@cloudbase.app",
7780
+ name: "Matthew Conti",
7781
+ email: "matthew.conti@cloudbase.app",
7770
7782
  company: "CloudBase",
7771
7783
  tier: "free",
7772
7784
  account_status: "active",
@@ -7777,7 +7789,7 @@ var DEMO_CLIENTI_FIXTURES = [
7777
7789
  var DEMO_ABBONAMENTI_FIXTURES = [
7778
7790
  {
7779
7791
  id: "a0100000-0000-4000-8000-000000000001",
7780
- slug: "abbonamento-vertex-digital",
7792
+ slug: "subscription-vertex-digital",
7781
7793
  status: "published",
7782
7794
  data: {
7783
7795
  customer_id: "c0100000-0000-4000-8000-000000000001",
@@ -7788,7 +7800,7 @@ var DEMO_ABBONAMENTI_FIXTURES = [
7788
7800
  },
7789
7801
  {
7790
7802
  id: "a0200000-0000-4000-8000-000000000002",
7791
- slug: "abbonamento-innova-solutions",
7803
+ slug: "subscription-innova-solutions",
7792
7804
  status: "published",
7793
7805
  data: {
7794
7806
  customer_id: "c0200000-0000-4000-8000-000000000002",
@@ -7799,7 +7811,7 @@ var DEMO_ABBONAMENTI_FIXTURES = [
7799
7811
  },
7800
7812
  {
7801
7813
  id: "a0300000-0000-4000-8000-000000000003",
7802
- slug: "abbonamento-alpha-dynamics",
7814
+ slug: "subscription-alpha-dynamics",
7803
7815
  status: "published",
7804
7816
  data: {
7805
7817
  customer_id: "c0400000-0000-4000-8000-000000000004",
@@ -7812,10 +7824,10 @@ var DEMO_ABBONAMENTI_FIXTURES = [
7812
7824
  var DEMO_TICKET_FIXTURES = [
7813
7825
  {
7814
7826
  id: "t0100000-0000-4000-8000-000000000001",
7815
- slug: "integrazione-webhook-fallita",
7827
+ slug: "webhook-integration-failure",
7816
7828
  status: "published",
7817
7829
  data: {
7818
- title: "Integrazione webhook fallita su eventi invoice.paid",
7830
+ title: "Webhook integration failure on invoice.paid event",
7819
7831
  customer_id: "c0100000-0000-4000-8000-000000000001",
7820
7832
  priority: "high",
7821
7833
  category: "technical",
@@ -7824,10 +7836,10 @@ var DEMO_TICKET_FIXTURES = [
7824
7836
  },
7825
7837
  {
7826
7838
  id: "t0200000-0000-4000-8000-000000000002",
7827
- slug: "richiesta-fattura-elettronica",
7839
+ slug: "annual-billing-vat-invoicing",
7828
7840
  status: "published",
7829
7841
  data: {
7830
- title: "Richiesta variazione codice SDI per fatturazione annuale",
7842
+ title: "Annual billing VAT invoice code update request",
7831
7843
  customer_id: "c0400000-0000-4000-8000-000000000004",
7832
7844
  priority: "medium",
7833
7845
  category: "billing",
@@ -7843,7 +7855,7 @@ var DEMO_CHANGELOG_FIXTURES = [
7843
7855
  data: {
7844
7856
  version: "v2.4.0",
7845
7857
  release_date: toIsoDate(nowSec - 22 * DAY),
7846
- features: toRichtextDoc("Introdotto il supporto per la privacy nativa a 4 livelli, cifratura AES-256-GCM e Blind Indexing per la ricerca sicura.")
7858
+ features: toRichtextDoc("Introduced 4-tier native privacy support, AES-256-GCM encryption, and Blind Indexing for secure search.")
7847
7859
  }
7848
7860
  },
7849
7861
  {
@@ -7853,31 +7865,31 @@ var DEMO_CHANGELOG_FIXTURES = [
7853
7865
  data: {
7854
7866
  version: "v2.3.0",
7855
7867
  release_date: toIsoDate(nowSec - 8 * DAY),
7856
- features: toRichtextDoc("Aggiunto l engine di automazioni avanzate e il posizionamento dinamico delle schede Kanban.")
7868
+ features: toRichtextDoc("Added advanced automation engine and dynamic Kanban card positioning.")
7857
7869
  }
7858
7870
  }
7859
7871
  ];
7860
7872
  var DEMO_ARTICOLI_FIXTURES = [
7861
7873
  {
7862
7874
  id: "ar100000-0000-4000-8000-000000000001",
7863
- slug: "guida-alla-privacy-application-level-encryption",
7875
+ slug: "guide-to-application-level-encryption",
7864
7876
  status: "published",
7865
7877
  data: {
7866
- title: "Guida all Application-Level Encryption in BeechCMS",
7878
+ title: "Guide to Application-Level Encryption in BeechCMS",
7867
7879
  author: "Flavio De Musso",
7868
7880
  cover_image: "https://images.unsplash.com/photo-1551288049-bebda4e38f71.jpg?w=800&q=80",
7869
- body: toRichtextDoc("Scopri come proteggere i dati riservati dei tuoi clienti direttamente nell engine senza sacrificare le performance edge di Cloudflare Workers.")
7881
+ body: toRichtextDoc("Learn how to protect confidential customer data directly inside the engine without sacrificing Cloudflare Workers edge performance.")
7870
7882
  }
7871
7883
  },
7872
7884
  {
7873
7885
  id: "ar200000-0000-4000-8000-000000000002",
7874
- slug: "architettura-botanical-engine-e-d1",
7886
+ slug: "botanical-engine-and-cloudflare-d1",
7875
7887
  status: "published",
7876
7888
  data: {
7877
- title: "Architettura Botanical Engine & Cloudflare D1",
7889
+ title: "Botanical Engine Architecture & Cloudflare D1",
7878
7890
  author: "Beech Core Team",
7879
7891
  cover_image: "https://images.unsplash.com/photo-1460925895917-afdab827c52f.jpg?w=800&q=80",
7880
- body: toRichtextDoc("Una panoramica approfondita sulla separazione tra i contratti di dominio di @beechcms/core e la persistenza SQLite edge.")
7892
+ body: toRichtextDoc("An in-depth overview of the separation between @beechcms/core domain contracts and edge SQLite persistence.")
7881
7893
  }
7882
7894
  }
7883
7895
  ];
@@ -6,7 +6,7 @@ export declare function checkPublicOperation(seed: Seed, operation: PublicOperat
6
6
  } | {
7
7
  readonly ok: false;
8
8
  readonly error: {
9
- readonly error: 'Forbidden';
9
+ readonly error: "Forbidden";
10
10
  readonly message: `Public ${string} is not allowed for content type '${string}'.`;
11
11
  };
12
12
  };
@@ -0,0 +1,6 @@
1
+ import type { Context, Next } from 'hono';
2
+ /**
3
+ * API key auth middleware for Public API routes.
4
+ * Uses X-API-Key header only.
5
+ */
6
+ export declare function apiKeyMiddleware(): (c: Context, next: Next) => Promise<Response | void>;
@@ -0,0 +1,10 @@
1
+ import type { Context } from 'hono';
2
+ type EdgeCache = {
3
+ cache: Cache;
4
+ executionCtx: {
5
+ waitUntil: (p: Promise<unknown>) => void;
6
+ };
7
+ } | null;
8
+ export declare function resolveEdgeCache(c: Context): EdgeCache;
9
+ export declare function withCachedResponse(edgeCache: EdgeCache, cacheKey: Request, response: Response): Response;
10
+ export {};
@@ -0,0 +1,32 @@
1
+ import type { Context } from 'hono';
2
+ /**
3
+ * Returns a Problem+JSON response when the error is a SQLite FK violation,
4
+ * or null when the error is unrelated (let the caller handle it).
5
+ *
6
+ * @param c Hono context
7
+ * @param error The caught error
8
+ * @param method HTTP method of the current request (e.g. 'DELETE', 'POST')
9
+ */
10
+ export declare function fkProblemOrNull(c: Context, error: unknown, method: string): Response | null;
11
+ export interface PublicProblemDetailItem {
12
+ field: string;
13
+ expected: string;
14
+ received: string;
15
+ message: string;
16
+ }
17
+ type PublicProblemInput = {
18
+ type: string;
19
+ title: string;
20
+ status: 400 | 401 | 403 | 404 | 405 | 409 | 422 | 429 | 500 | 501;
21
+ detail: string;
22
+ errors?: PublicProblemDetailItem[];
23
+ headers?: Record<string, string>;
24
+ };
25
+ export declare function internalErrorDetail(env: {
26
+ ENV?: string;
27
+ }, error: unknown): string;
28
+ /**
29
+ * Restituisce errori API in formato Problem Details (RFC 9457).
30
+ */
31
+ export declare function publicProblem(c: Context, input: PublicProblemInput): Response;
32
+ export {};
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Error constants for Public API responses.
3
+ */
4
+ export declare const PUBLIC_ERRORS: {
5
+ readonly API_KEY_UNAUTHORIZED: {
6
+ readonly error: "Unauthorized";
7
+ readonly message: "Missing or invalid API key. Provide a valid key via X-API-Key header.";
8
+ };
9
+ readonly API_KEY_FORBIDDEN: {
10
+ readonly error: "Forbidden";
11
+ readonly message: "Public API access is not configured for this instance.";
12
+ };
13
+ };