@beechcms/api 0.6.0 → 0.6.2

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.js CHANGED
@@ -36,10 +36,10 @@ function parseLoginBody(body) {
36
36
  const password = obj.password;
37
37
  if (typeof email !== "string" || typeof password !== "string") return null;
38
38
  if (!email.trim() || !password) return null;
39
- return { email: email.trim(), password };
39
+ return { email: email.trim().toLowerCase(), password };
40
40
  }
41
41
  function validateLoginInput(email, password) {
42
- return email.length <= MAX_EMAIL_LENGTH && EMAIL_REGEX.test(email) && password.trim().length >= MIN_PASSWORD_LENGTH && password.length <= MAX_PASSWORD_LENGTH && new TextEncoder().encode(password).length <= MAX_PASSWORD_BYTES;
42
+ return email.length <= MAX_EMAIL_LENGTH && EMAIL_REGEX.test(email) && password.length >= MIN_PASSWORD_LENGTH && password.length <= MAX_PASSWORD_LENGTH && new TextEncoder().encode(password).length <= MAX_PASSWORD_BYTES;
43
43
  }
44
44
  async function verifyPassword(plainPassword, hash, hashProvider) {
45
45
  return hashProvider.verify(plainPassword, hash);
@@ -188,7 +188,8 @@ async function applyPrivacy(data, seed) {
188
188
  );
189
189
  }
190
190
  if (privacy === "hash" && value != null) {
191
- result[alias] = await sha256hex(String(value));
191
+ const serialized = typeof value === "string" ? value : JSON.stringify(value);
192
+ result[alias] = await sha256hex(serialized);
192
193
  } else {
193
194
  result[alias] = value;
194
195
  }
@@ -336,9 +337,9 @@ async function buildRelationsMap(context, seed, entries) {
336
337
  });
337
338
  const map = {};
338
339
  for (const item of items) {
339
- const id = item.id;
340
- const data = item.data;
341
- const label = data[labelAlias];
340
+ const row = item;
341
+ const id = row.id;
342
+ const label = row[labelAlias];
342
343
  map[id] = label != null && label !== "" ? String(label) : id;
343
344
  }
344
345
  relations[branch.alias] = map;
@@ -885,11 +886,14 @@ var MEDIA_URL_PATTERN = /\/api\/media\/([^?#]+)/;
885
886
  function extractMediaKey(mediaUrl, cdnUrl) {
886
887
  const urlStr = String(mediaUrl);
887
888
  if (cdnUrl) {
888
- const normalizedCdn = cdnUrl.replace(/\/$/, "");
889
- if (urlStr.startsWith(normalizedCdn)) {
890
- const keyPart = urlStr.slice(normalizedCdn.length).replace(/^\//, "");
891
- const keyWithoutQuery = keyPart.split(/[?#]/)[0];
892
- return keyWithoutQuery ? decodeURIComponent(keyWithoutQuery) : null;
889
+ try {
890
+ const cdnOrigin = new URL(cdnUrl).origin;
891
+ const mediaUrlParsed = new URL(urlStr);
892
+ if (mediaUrlParsed.origin === cdnOrigin) {
893
+ const keyPart = mediaUrlParsed.pathname.replace(/^\//, "");
894
+ return keyPart ? decodeURIComponent(keyPart) : null;
895
+ }
896
+ } catch {
893
897
  }
894
898
  }
895
899
  const match = MEDIA_URL_PATTERN.exec(urlStr);
@@ -1150,7 +1154,7 @@ async function bulkHandler(context) {
1150
1154
  }
1151
1155
 
1152
1156
  // src/features/content/handlers/kanban-position.ts
1153
- import { resolveKanbanConfig as resolveKanbanConfig2 } from "@beechcms/core";
1157
+ import { resolveKanbanConfig as resolveKanbanConfig2, EntryNotFoundError as EntryNotFoundError3 } from "@beechcms/core";
1154
1158
  async function kanbanPositionHandler(context) {
1155
1159
  const slug = context.req.param("slug");
1156
1160
  const id = context.req.param("id");
@@ -1175,9 +1179,13 @@ async function kanbanPositionHandler(context) {
1175
1179
  if (!compat.compatible || !compat.candidates.some((c) => c.branchId === axisBranchId)) {
1176
1180
  return publicProblem(context, { type: "content-invalid-kanban-axis", title: "Bad Request", status: 400, detail: "axisBranchId is not a valid kanban candidate for this seed" });
1177
1181
  }
1178
- const entry = await context.get("repository").findById(seed, id);
1179
- if (!entry) {
1180
- return publicProblem(context, { type: "content-not-found", title: "Not Found", status: 404, detail: CONTENT_ERRORS.NOT_FOUND });
1182
+ try {
1183
+ await context.get("repository").findById(seed, id);
1184
+ } catch (error) {
1185
+ if (error instanceof EntryNotFoundError3) {
1186
+ return publicProblem(context, { type: "content-not-found", title: "Not Found", status: 404, detail: CONTENT_ERRORS.NOT_FOUND });
1187
+ }
1188
+ throw error;
1181
1189
  }
1182
1190
  await context.get("kanbanPositionRepository").setPosition(slug, id, axisBranchId, position);
1183
1191
  return context.json({ success: true });
@@ -1231,7 +1239,7 @@ async function kanbanMoveHandler(context) {
1231
1239
  const currentTags = current[axisBranch.alias] ?? [];
1232
1240
  const { oldValue, newValue } = body.axis;
1233
1241
  const withoutOld = oldValue !== null ? currentTags.filter((t) => t !== oldValue) : currentTags;
1234
- const nextTags = newValue !== null ? [...withoutOld, newValue] : withoutOld;
1242
+ const nextTags = newValue !== null && !withoutOld.includes(newValue) ? [...withoutOld, newValue] : withoutOld;
1235
1243
  patch = { [axisBranch.alias]: nextTags };
1236
1244
  }
1237
1245
  const validation = validateAndSanitizeSeedPayload3(seed, patch, {
@@ -1504,7 +1512,7 @@ widgetApp.get("/distribution/:seed", async (context) => {
1504
1512
 
1505
1513
  // src/features/rotate-field/rotate-field.handler.ts
1506
1514
  import { Hono as Hono3 } from "hono";
1507
- import { resolvePolicies as resolvePolicies4, verifyHashField, sha256hex as sha256hex2, validateAndSanitizeSeedPayload as validateAndSanitizeSeedPayload4, EntryNotFoundError as EntryNotFoundError3 } from "@beechcms/core";
1515
+ import { resolvePolicies as resolvePolicies4, verifyHashField, sha256hex as sha256hex2, validateAndSanitizeSeedPayload as validateAndSanitizeSeedPayload4, EntryNotFoundError as EntryNotFoundError4 } from "@beechcms/core";
1508
1516
 
1509
1517
  // src/features/rotate-field/rotate-field.schema.ts
1510
1518
  import { z } from "zod";
@@ -1571,7 +1579,7 @@ rotateFieldApp.post("/:slug/:id/rotate-field", async (context) => {
1571
1579
  try {
1572
1580
  contentRecord = await context.get("repository").findById(seed, entryId);
1573
1581
  } catch (error) {
1574
- if (error instanceof EntryNotFoundError3) {
1582
+ if (error instanceof EntryNotFoundError4) {
1575
1583
  return publicProblem(context, {
1576
1584
  type: "content-not-found",
1577
1585
  title: "Not Found",
@@ -2274,7 +2282,7 @@ import {
2274
2282
 
2275
2283
  // src/features/draft/draft.middleware.ts
2276
2284
  import { createMiddleware } from "hono/factory";
2277
- import { EntryNotFoundError as EntryNotFoundError4 } from "@beechcms/core";
2285
+ import { EntryNotFoundError as EntryNotFoundError5 } from "@beechcms/core";
2278
2286
  var draftGuard = createMiddleware(async (context, next) => {
2279
2287
  const slug = context.req.param("slug");
2280
2288
  const id = context.req.param("id");
@@ -2307,7 +2315,7 @@ var draftGuard = createMiddleware(async (context, next) => {
2307
2315
  try {
2308
2316
  await repository.findById(seed, id);
2309
2317
  } catch (error) {
2310
- if (error instanceof EntryNotFoundError4) {
2318
+ if (error instanceof EntryNotFoundError5) {
2311
2319
  return publicProblem(context, {
2312
2320
  type: "content-not-found",
2313
2321
  title: "Not Found",
@@ -5294,7 +5302,15 @@ function publicRateLimitMiddleware() {
5294
5302
  return async (c, next) => {
5295
5303
  const readMethod = isReadMethod2(c.req.method);
5296
5304
  const limiterName = readMethod ? "publicApiRead" : "publicApiWrite";
5297
- const seed = c.req.param("seed") ?? "no-seed";
5305
+ const path = c.req.path;
5306
+ let seed = "no-seed";
5307
+ if (path.startsWith("/api/v1/public/")) {
5308
+ const remaining = path.slice("/api/v1/public/".length);
5309
+ const firstSegment = remaining.split("/")[0];
5310
+ if (firstSegment && firstSegment !== "health" && firstSegment !== "schema" && firstSegment !== "schema.html") {
5311
+ seed = firstSegment;
5312
+ }
5313
+ }
5298
5314
  const key = `${getClientIp(c.req)}:${seed}:${limiterName}`;
5299
5315
  const result = await c.get("rateLimiters").getLimiter(limiterName).checkLimit(key);
5300
5316
  if (!result.isAllowed) {
@@ -5364,7 +5380,7 @@ function withCachedResponse(edgeCache, cacheKey, response) {
5364
5380
  }
5365
5381
 
5366
5382
  // src/public/read-single.ts
5367
- import { EntryNotFoundError as EntryNotFoundError5 } from "@beechcms/core";
5383
+ import { EntryNotFoundError as EntryNotFoundError6 } from "@beechcms/core";
5368
5384
 
5369
5385
  // src/public/entry-projection.ts
5370
5386
  import { resolvePolicies as resolvePolicies7 } from "@beechcms/core";
@@ -5380,7 +5396,11 @@ function applyPublicPolicies(data, seed) {
5380
5396
  const { public: isPublic, visibility } = resolvePolicies7(branch);
5381
5397
  if (!isPublic) continue;
5382
5398
  if (visibility === "hidden") continue;
5383
- result[branch.alias] = visibility === "masked" && typeof value === "string" && value.length > 0 ? "\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022" : value;
5399
+ if (visibility === "masked") {
5400
+ result[branch.alias] = typeof value === "string" && value.length > 0 ? "\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022" : null;
5401
+ } else {
5402
+ result[branch.alias] = value;
5403
+ }
5384
5404
  }
5385
5405
  return result;
5386
5406
  }
@@ -5427,7 +5447,7 @@ async function readSingleEntry(input) {
5427
5447
  meta: buildPublicSingleMeta(seedSlug)
5428
5448
  };
5429
5449
  } catch (error) {
5430
- if (error instanceof EntryNotFoundError5) {
5450
+ if (error instanceof EntryNotFoundError6) {
5431
5451
  return { ok: false, detail: `Entry '${label}' not found for content type '${seedSlug}'.` };
5432
5452
  }
5433
5453
  throw error;
@@ -5435,6 +5455,7 @@ async function readSingleEntry(input) {
5435
5455
  }
5436
5456
 
5437
5457
  // src/public/query-builder.ts
5458
+ import { resolvePolicies as resolvePolicies8 } from "@beechcms/core";
5438
5459
  var PUBLIC_FILTER_OPERATORS = /* @__PURE__ */ new Set([
5439
5460
  "eq",
5440
5461
  "neq",
@@ -5514,6 +5535,12 @@ function toEngineFilters2(seed, parsedFilter) {
5514
5535
  if (!parsedFilter || parsedFilter.where.length === 0) return [];
5515
5536
  return parsedFilter.where.map((cond) => {
5516
5537
  const branch = seed.branches.find((b) => b.alias === cond.field);
5538
+ if (branch) {
5539
+ const { public: isPublic, filter: filterable } = resolvePolicies8(branch);
5540
+ if (!isPublic || !filterable) {
5541
+ throw new TypeError(`Invalid filter: field '${cond.field}' is not filterable`);
5542
+ }
5543
+ }
5517
5544
  const type = branch ? mapBranchToFilterType(branch.type) : SYSTEM_COLUMNS2.has(cond.field) ? "system" : "text";
5518
5545
  return {
5519
5546
  column: cond.field,
@@ -5554,6 +5581,7 @@ async function readListEntries(input) {
5554
5581
  const sortDir = (cleanStr(query.orderDir) ?? "desc").toLowerCase() === "asc" ? "ASC" : "DESC";
5555
5582
  const { items, total } = await repository.findMany(seed, {
5556
5583
  filters: engineFilters,
5584
+ filterLogic: parsedFilter?.logic,
5557
5585
  search: search || void 0,
5558
5586
  status: publishedOnly ? "published" : null,
5559
5587
  pagination: {
@@ -5620,7 +5648,7 @@ async function publicReadHandler(context) {
5620
5648
  }
5621
5649
 
5622
5650
  // src/public/public-add.ts
5623
- import { isValidContentStatus as isValidContentStatus3, SlugConflictError as SlugConflictError2 } from "@beechcms/core";
5651
+ import { isValidContentStatus as isValidContentStatus3, resolvePolicies as resolvePolicies9, SlugConflictError as SlugConflictError2 } from "@beechcms/core";
5624
5652
 
5625
5653
  // src/public/slug-utils.ts
5626
5654
  import { slugify as slugify3, generateEntrySlug } from "@beechcms/core";
@@ -5705,6 +5733,13 @@ async function publicAddHandler(context) {
5705
5733
  if (!isValidContentStatus3(statusValue)) {
5706
5734
  return publicProblem(context, { type: "invalid-status", title: "Bad Request", status: 400, detail: "Invalid status. Allowed values are: draft, review, published" });
5707
5735
  }
5736
+ const sensitiveAliases = Object.keys(rawData).filter((alias) => {
5737
+ const branch = seed.branches.find((b) => b.alias === alias);
5738
+ return branch != null && resolvePolicies9(branch).public === false;
5739
+ });
5740
+ if (sensitiveAliases.length > 0) {
5741
+ return publicProblem(context, { type: "sensitive-field-edit", title: "Unprocessable Entity", status: 422, detail: `Cannot write internal fields: ${sensitiveAliases.join(", ")}` });
5742
+ }
5708
5743
  const sanitized = sanitizePublicPayload(seed, rawData, { operation: "create", allowNull: false, requireAtLeastOneValidField: true, enforceRequiredFields: true });
5709
5744
  if (!sanitized.ok) {
5710
5745
  if (sanitized.status === 422) {
@@ -5712,8 +5747,17 @@ async function publicAddHandler(context) {
5712
5747
  }
5713
5748
  return publicProblem(context, { type: sanitized.code, title: "Bad Request", status: 400, detail: sanitized.message, errors: sanitized.details });
5714
5749
  }
5750
+ let privacyData;
5751
+ try {
5752
+ privacyData = await applyPrivacy(sanitized.data, seed);
5753
+ } catch (error) {
5754
+ if (error instanceof PrivacyPolicyError) {
5755
+ return publicProblem(context, { type: "policy-not-implemented", title: "Not Implemented", status: 501, detail: error.message });
5756
+ }
5757
+ throw error;
5758
+ }
5715
5759
  const idempotencyKey = parseIdempotencyKey(context.req.header("Idempotency-Key"));
5716
- const finalSlug = pickSlug(body, sanitized.data) || context.get("idGenerator").uuid().slice(0, 8);
5760
+ const finalSlug = pickSlug(body, privacyData) || context.get("idGenerator").uuid().slice(0, 8);
5717
5761
  const repository = context.get("repository");
5718
5762
  const idempotencyRepository = context.get("idempotencyRepository");
5719
5763
  try {
@@ -5737,7 +5781,7 @@ async function publicAddHandler(context) {
5737
5781
  }
5738
5782
  const id = context.get("idGenerator").uuid();
5739
5783
  try {
5740
- await repository.create(seed, id, finalSlug, statusValue, sanitized.data);
5784
+ await repository.create(seed, id, finalSlug, statusValue, privacyData);
5741
5785
  } catch (error) {
5742
5786
  if (error instanceof SlugConflictError2) {
5743
5787
  return publicProblem(context, { type: "slug-conflict", title: "Conflict", status: 409, detail: `An entry with slug '${finalSlug}' already exists for content type '${seedSlug}'.` });
@@ -5750,7 +5794,7 @@ async function publicAddHandler(context) {
5750
5794
  }
5751
5795
  context.get("notificationService").notify({
5752
5796
  title: `${seed.label}: New entry`,
5753
- message: `A new entry ("${sanitized.data.title || sanitized.data.name || finalSlug}") has been added via the public API.`,
5797
+ message: `A new entry ("${privacyData.title || privacyData.name || finalSlug}") has been added via the public API.`,
5754
5798
  type: "success"
5755
5799
  });
5756
5800
  return context.json(responseBody, 201);
@@ -5761,7 +5805,7 @@ async function publicAddHandler(context) {
5761
5805
  }
5762
5806
 
5763
5807
  // src/public/public-edit.ts
5764
- import { isValidContentStatus as isValidContentStatus4, resolvePolicies as resolvePolicies8, EntryNotFoundError as EntryNotFoundError6 } from "@beechcms/core";
5808
+ import { isValidContentStatus as isValidContentStatus4, resolvePolicies as resolvePolicies10, EntryNotFoundError as EntryNotFoundError7 } from "@beechcms/core";
5765
5809
  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;
5766
5810
  function errorMessage(context, error) {
5767
5811
  if (context.env.ENV !== "production" && error instanceof Error) return error.message;
@@ -5811,7 +5855,7 @@ function resolveData(context, seed, body) {
5811
5855
  const sensitiveAliases = Object.keys(rawData).filter((alias) => {
5812
5856
  const branch = seed.branches.find((b) => b.alias === alias);
5813
5857
  if (!branch) return false;
5814
- const policies = resolvePolicies8(branch);
5858
+ const policies = resolvePolicies10(branch);
5815
5859
  return policies.privacy !== "plain" || policies.public === false;
5816
5860
  });
5817
5861
  if (sensitiveAliases.length > 0) {
@@ -5869,7 +5913,7 @@ async function publicEditHandler(context) {
5869
5913
  });
5870
5914
  return context.json({ success: true, id, slug: slugResult.value.nextSlug }, 200);
5871
5915
  } catch (error) {
5872
- if (error instanceof EntryNotFoundError6) {
5916
+ if (error instanceof EntryNotFoundError7) {
5873
5917
  return publicProblem(context, { type: "entry-not-found", title: "Not Found", status: 404, detail: `Entry '${id}' not found for content type '${seedSlug}'.` });
5874
5918
  }
5875
5919
  console.error("Public edit error:", error);
@@ -6092,7 +6136,7 @@ import { createMiddleware as createMiddleware2 } from "hono/factory";
6092
6136
 
6093
6137
  // src/shared/db/repositories/content.repository.d1.ts
6094
6138
  import {
6095
- EntryNotFoundError as EntryNotFoundError7,
6139
+ EntryNotFoundError as EntryNotFoundError8,
6096
6140
  RepositoryError as RepositoryError2,
6097
6141
  SlugConflictError as SlugConflictError4,
6098
6142
  RelationTargetNotFoundError as RelationTargetNotFoundError2,
@@ -6120,7 +6164,7 @@ var BaseD1Repository = class {
6120
6164
  */
6121
6165
  mapError(error, context) {
6122
6166
  const message = error?.message || "Unknown database error";
6123
- if (message.includes("UNIQUE constraint failed:") && message.includes(".slug")) {
6167
+ if (message.includes("UNIQUE constraint failed") && message.includes("slug")) {
6124
6168
  return new SlugConflictError3(`${context}: ${message}`);
6125
6169
  }
6126
6170
  return new RepositoryError(`${context}: ${message}`, error);
@@ -6274,13 +6318,13 @@ var D1ContentRepository = class extends BaseD1Repository {
6274
6318
  const tableName = this.getTableName(seed.slug);
6275
6319
  const entryRow = await this.database.prepare(`SELECT * FROM ${tableName} WHERE id = ? LIMIT 1`).bind(id).first();
6276
6320
  if (!entryRow) {
6277
- throw new EntryNotFoundError7(`Entry ${id} not found in ${seed.slug}`);
6321
+ throw new EntryNotFoundError8(`Entry ${id} not found in ${seed.slug}`);
6278
6322
  }
6279
6323
  const data = this.rowToData(seed, entryRow);
6280
6324
  await this.attachMultiRelations(seed, id, data);
6281
6325
  return data;
6282
6326
  } catch (error) {
6283
- if (error instanceof EntryNotFoundError7) throw error;
6327
+ if (error instanceof EntryNotFoundError8) throw error;
6284
6328
  throw this.mapError(error, `findById(${seed.slug}, ${id})`);
6285
6329
  }
6286
6330
  }
@@ -6294,13 +6338,13 @@ var D1ContentRepository = class extends BaseD1Repository {
6294
6338
  const tableName = this.getTableName(seed.slug);
6295
6339
  const entryRow = await this.database.prepare(`SELECT * FROM ${tableName} WHERE slug = ? LIMIT 1`).bind(slug).first();
6296
6340
  if (!entryRow) {
6297
- throw new EntryNotFoundError7(`Entry with slug "${slug}" not found in ${seed.slug}`);
6341
+ throw new EntryNotFoundError8(`Entry with slug "${slug}" not found in ${seed.slug}`);
6298
6342
  }
6299
6343
  const data = this.rowToData(seed, entryRow);
6300
6344
  await this.attachMultiRelations(seed, entryRow.id, data);
6301
6345
  return data;
6302
6346
  } catch (error) {
6303
- if (error instanceof EntryNotFoundError7) throw error;
6347
+ if (error instanceof EntryNotFoundError8) throw error;
6304
6348
  throw this.mapError(error, `findBySlug(${seed.slug}, ${slug})`);
6305
6349
  }
6306
6350
  }
@@ -6616,16 +6660,16 @@ var D1ContentRepository = class extends BaseD1Repository {
6616
6660
  if (batchStmts.length === 1) {
6617
6661
  const updateResult = await batchStmts[0].run();
6618
6662
  if (updateResult.meta.changes === 0) {
6619
- throw new EntryNotFoundError7(`Entry ${id} not found in ${seed.slug}`);
6663
+ throw new EntryNotFoundError8(`Entry ${id} not found in ${seed.slug}`);
6620
6664
  }
6621
6665
  } else if (batchStmts.length > 1) {
6622
6666
  const results = await this.database.batch(batchStmts);
6623
6667
  if (stmt && (results[0].meta?.changes ?? 0) === 0) {
6624
- throw new EntryNotFoundError7(`Entry ${id} not found in ${seed.slug}`);
6668
+ throw new EntryNotFoundError8(`Entry ${id} not found in ${seed.slug}`);
6625
6669
  }
6626
6670
  }
6627
6671
  } catch (error) {
6628
- if (error instanceof EntryNotFoundError7) throw error;
6672
+ if (error instanceof EntryNotFoundError8) throw error;
6629
6673
  throw this.mapError(error, `update(${seed.slug}, ${id})`);
6630
6674
  }
6631
6675
  if (this.hooks?.afterUpdate) {
@@ -6650,12 +6694,12 @@ var D1ContentRepository = class extends BaseD1Repository {
6650
6694
  const tableName = this.getTableName(seed.slug);
6651
6695
  const entryRow = await this.database.prepare(`SELECT * FROM ${tableName} WHERE id = ?`).bind(id).first();
6652
6696
  if (!entryRow) {
6653
- throw new EntryNotFoundError7(`Entry ${id} not found in ${seed.slug}`);
6697
+ throw new EntryNotFoundError8(`Entry ${id} not found in ${seed.slug}`);
6654
6698
  }
6655
6699
  await this.database.prepare(`DELETE FROM ${tableName} WHERE id = ?`).bind(id).run();
6656
6700
  row = this.rowToData(seed, entryRow);
6657
6701
  } catch (error) {
6658
- if (error instanceof EntryNotFoundError7) throw error;
6702
+ if (error instanceof EntryNotFoundError8) throw error;
6659
6703
  throw this.mapError(error, `delete(${seed.slug}, ${id})`);
6660
6704
  }
6661
6705
  if (this.hooks?.afterDelete) {
@@ -6769,21 +6813,12 @@ var D1ContentRepository = class extends BaseD1Repository {
6769
6813
  const draftTableName = this.getTableName(seed.slug, true);
6770
6814
  const mRelBranches = multiRelBranches(seed);
6771
6815
  const mRelAliases = new Set(mRelBranches.map((b) => b.alias));
6772
- let existingTouched = [];
6773
- try {
6774
- const existing = await this.database.prepare(`SELECT _touched_fields FROM ${draftTableName} WHERE entry_id = ?`).bind(entryId).first();
6775
- if (existing && existing._touched_fields) {
6776
- existingTouched = JSON.parse(existing._touched_fields);
6777
- }
6778
- } catch {
6779
- }
6780
- const currentTouched = new Set(existingTouched);
6816
+ const thisCallTouched = [];
6781
6817
  for (const branch of seed.branches) {
6782
6818
  if (Object.hasOwn(data, branch.alias)) {
6783
- currentTouched.add(branch.alias);
6819
+ thisCallTouched.push(branch.alias);
6784
6820
  }
6785
6821
  }
6786
- const updatedTouchedFields = Array.from(currentTouched);
6787
6822
  const columnNames = ["entry_id"];
6788
6823
  const placeholders = ["?"];
6789
6824
  const queryBindings = [entryId];
@@ -6800,8 +6835,14 @@ var D1ContentRepository = class extends BaseD1Repository {
6800
6835
  }
6801
6836
  columnNames.push("_touched_fields");
6802
6837
  placeholders.push("?");
6803
- queryBindings.push(JSON.stringify(updatedTouchedFields));
6804
- updateClauses.push(`_touched_fields = EXCLUDED._touched_fields`);
6838
+ queryBindings.push(JSON.stringify(thisCallTouched));
6839
+ updateClauses.push(`_touched_fields = (
6840
+ SELECT json_group_array(value) FROM (
6841
+ SELECT value FROM json_each(COALESCE(_touched_fields, '[]'))
6842
+ UNION
6843
+ SELECT value FROM json_each(excluded._touched_fields)
6844
+ )
6845
+ )`);
6805
6846
  updateClauses.push("updated_at = (unixepoch())");
6806
6847
  const sql = `
6807
6848
  INSERT INTO ${draftTableName} (${columnNames.join(", ")})
@@ -6909,7 +6950,7 @@ var D1ContentRepository = class extends BaseD1Repository {
6909
6950
  const liveTableName = this.getTableName(seed.slug);
6910
6951
  const draftRow = await this.database.prepare(`SELECT * FROM ${draftTableName} WHERE entry_id = ?`).bind(entryId).first();
6911
6952
  if (!draftRow) {
6912
- throw new EntryNotFoundError7(`No draft found for ${entryId} in ${seed.slug}`);
6953
+ throw new EntryNotFoundError8(`No draft found for ${entryId} in ${seed.slug}`);
6913
6954
  }
6914
6955
  let touchedFields = [];
6915
6956
  if (draftRow && typeof draftRow["_touched_fields"] === "string") {
@@ -6964,7 +7005,7 @@ var D1ContentRepository = class extends BaseD1Repository {
6964
7005
  }
6965
7006
  await this.database.batch(batchStmts);
6966
7007
  } catch (error) {
6967
- if (error instanceof EntryNotFoundError7) throw error;
7008
+ if (error instanceof EntryNotFoundError8) throw error;
6968
7009
  if (error instanceof RelationTargetNotFoundError2) throw error;
6969
7010
  throw this.mapError(error, `publishDraft(${seed.slug}, ${entryId})`);
6970
7011
  }
@@ -10172,8 +10213,8 @@ function createBeechApp(config) {
10172
10213
  }
10173
10214
  return null;
10174
10215
  },
10175
- allowMethods: ["GET", "POST", "PUT", "DELETE", "OPTIONS"],
10176
- allowHeaders: ["Content-Type", "Authorization", "X-API-Key"],
10216
+ allowMethods: ["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"],
10217
+ allowHeaders: ["Content-Type", "Authorization", "X-API-Key", "Idempotency-Key"],
10177
10218
  credentials: true
10178
10219
  })(context, next);
10179
10220
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@beechcms/api",
3
- "version": "0.6.0",
3
+ "version": "0.6.2",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/factory.d.ts",
@@ -20,7 +20,7 @@
20
20
  "dependencies": {
21
21
  "@aws-sdk/client-s3": "^3.995.0",
22
22
  "@aws-sdk/s3-request-presigner": "^3.995.0",
23
- "@beechcms/core": "^0.6.0",
23
+ "@beechcms/core": "^0.6.2",
24
24
  "@upstash/qstash": "^2.11.0",
25
25
  "bcryptjs": "^2.4.3",
26
26
  "hono": "^4.12.21",