@beechcms/api 0.6.0 → 0.6.1

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);
@@ -336,9 +336,9 @@ async function buildRelationsMap(context, seed, entries) {
336
336
  });
337
337
  const map = {};
338
338
  for (const item of items) {
339
- const id = item.id;
340
- const data = item.data;
341
- const label = data[labelAlias];
339
+ const row = item;
340
+ const id = row.id;
341
+ const label = row[labelAlias];
342
342
  map[id] = label != null && label !== "" ? String(label) : id;
343
343
  }
344
344
  relations[branch.alias] = map;
@@ -1150,7 +1150,7 @@ async function bulkHandler(context) {
1150
1150
  }
1151
1151
 
1152
1152
  // src/features/content/handlers/kanban-position.ts
1153
- import { resolveKanbanConfig as resolveKanbanConfig2 } from "@beechcms/core";
1153
+ import { resolveKanbanConfig as resolveKanbanConfig2, EntryNotFoundError as EntryNotFoundError3 } from "@beechcms/core";
1154
1154
  async function kanbanPositionHandler(context) {
1155
1155
  const slug = context.req.param("slug");
1156
1156
  const id = context.req.param("id");
@@ -1175,9 +1175,13 @@ async function kanbanPositionHandler(context) {
1175
1175
  if (!compat.compatible || !compat.candidates.some((c) => c.branchId === axisBranchId)) {
1176
1176
  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
1177
  }
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 });
1178
+ try {
1179
+ await context.get("repository").findById(seed, id);
1180
+ } catch (error) {
1181
+ if (error instanceof EntryNotFoundError3) {
1182
+ return publicProblem(context, { type: "content-not-found", title: "Not Found", status: 404, detail: CONTENT_ERRORS.NOT_FOUND });
1183
+ }
1184
+ throw error;
1181
1185
  }
1182
1186
  await context.get("kanbanPositionRepository").setPosition(slug, id, axisBranchId, position);
1183
1187
  return context.json({ success: true });
@@ -1231,7 +1235,7 @@ async function kanbanMoveHandler(context) {
1231
1235
  const currentTags = current[axisBranch.alias] ?? [];
1232
1236
  const { oldValue, newValue } = body.axis;
1233
1237
  const withoutOld = oldValue !== null ? currentTags.filter((t) => t !== oldValue) : currentTags;
1234
- const nextTags = newValue !== null ? [...withoutOld, newValue] : withoutOld;
1238
+ const nextTags = newValue !== null && !withoutOld.includes(newValue) ? [...withoutOld, newValue] : withoutOld;
1235
1239
  patch = { [axisBranch.alias]: nextTags };
1236
1240
  }
1237
1241
  const validation = validateAndSanitizeSeedPayload3(seed, patch, {
@@ -1504,7 +1508,7 @@ widgetApp.get("/distribution/:seed", async (context) => {
1504
1508
 
1505
1509
  // src/features/rotate-field/rotate-field.handler.ts
1506
1510
  import { Hono as Hono3 } from "hono";
1507
- import { resolvePolicies as resolvePolicies4, verifyHashField, sha256hex as sha256hex2, validateAndSanitizeSeedPayload as validateAndSanitizeSeedPayload4, EntryNotFoundError as EntryNotFoundError3 } from "@beechcms/core";
1511
+ import { resolvePolicies as resolvePolicies4, verifyHashField, sha256hex as sha256hex2, validateAndSanitizeSeedPayload as validateAndSanitizeSeedPayload4, EntryNotFoundError as EntryNotFoundError4 } from "@beechcms/core";
1508
1512
 
1509
1513
  // src/features/rotate-field/rotate-field.schema.ts
1510
1514
  import { z } from "zod";
@@ -1571,7 +1575,7 @@ rotateFieldApp.post("/:slug/:id/rotate-field", async (context) => {
1571
1575
  try {
1572
1576
  contentRecord = await context.get("repository").findById(seed, entryId);
1573
1577
  } catch (error) {
1574
- if (error instanceof EntryNotFoundError3) {
1578
+ if (error instanceof EntryNotFoundError4) {
1575
1579
  return publicProblem(context, {
1576
1580
  type: "content-not-found",
1577
1581
  title: "Not Found",
@@ -2274,7 +2278,7 @@ import {
2274
2278
 
2275
2279
  // src/features/draft/draft.middleware.ts
2276
2280
  import { createMiddleware } from "hono/factory";
2277
- import { EntryNotFoundError as EntryNotFoundError4 } from "@beechcms/core";
2281
+ import { EntryNotFoundError as EntryNotFoundError5 } from "@beechcms/core";
2278
2282
  var draftGuard = createMiddleware(async (context, next) => {
2279
2283
  const slug = context.req.param("slug");
2280
2284
  const id = context.req.param("id");
@@ -2307,7 +2311,7 @@ var draftGuard = createMiddleware(async (context, next) => {
2307
2311
  try {
2308
2312
  await repository.findById(seed, id);
2309
2313
  } catch (error) {
2310
- if (error instanceof EntryNotFoundError4) {
2314
+ if (error instanceof EntryNotFoundError5) {
2311
2315
  return publicProblem(context, {
2312
2316
  type: "content-not-found",
2313
2317
  title: "Not Found",
@@ -5294,7 +5298,15 @@ function publicRateLimitMiddleware() {
5294
5298
  return async (c, next) => {
5295
5299
  const readMethod = isReadMethod2(c.req.method);
5296
5300
  const limiterName = readMethod ? "publicApiRead" : "publicApiWrite";
5297
- const seed = c.req.param("seed") ?? "no-seed";
5301
+ const path = c.req.path;
5302
+ let seed = "no-seed";
5303
+ if (path.startsWith("/api/v1/public/")) {
5304
+ const remaining = path.slice("/api/v1/public/".length);
5305
+ const firstSegment = remaining.split("/")[0];
5306
+ if (firstSegment && firstSegment !== "health" && firstSegment !== "schema" && firstSegment !== "schema.html") {
5307
+ seed = firstSegment;
5308
+ }
5309
+ }
5298
5310
  const key = `${getClientIp(c.req)}:${seed}:${limiterName}`;
5299
5311
  const result = await c.get("rateLimiters").getLimiter(limiterName).checkLimit(key);
5300
5312
  if (!result.isAllowed) {
@@ -5364,7 +5376,7 @@ function withCachedResponse(edgeCache, cacheKey, response) {
5364
5376
  }
5365
5377
 
5366
5378
  // src/public/read-single.ts
5367
- import { EntryNotFoundError as EntryNotFoundError5 } from "@beechcms/core";
5379
+ import { EntryNotFoundError as EntryNotFoundError6 } from "@beechcms/core";
5368
5380
 
5369
5381
  // src/public/entry-projection.ts
5370
5382
  import { resolvePolicies as resolvePolicies7 } from "@beechcms/core";
@@ -5427,7 +5439,7 @@ async function readSingleEntry(input) {
5427
5439
  meta: buildPublicSingleMeta(seedSlug)
5428
5440
  };
5429
5441
  } catch (error) {
5430
- if (error instanceof EntryNotFoundError5) {
5442
+ if (error instanceof EntryNotFoundError6) {
5431
5443
  return { ok: false, detail: `Entry '${label}' not found for content type '${seedSlug}'.` };
5432
5444
  }
5433
5445
  throw error;
@@ -5554,6 +5566,7 @@ async function readListEntries(input) {
5554
5566
  const sortDir = (cleanStr(query.orderDir) ?? "desc").toLowerCase() === "asc" ? "ASC" : "DESC";
5555
5567
  const { items, total } = await repository.findMany(seed, {
5556
5568
  filters: engineFilters,
5569
+ filterLogic: parsedFilter?.logic,
5557
5570
  search: search || void 0,
5558
5571
  status: publishedOnly ? "published" : null,
5559
5572
  pagination: {
@@ -5761,7 +5774,7 @@ async function publicAddHandler(context) {
5761
5774
  }
5762
5775
 
5763
5776
  // src/public/public-edit.ts
5764
- import { isValidContentStatus as isValidContentStatus4, resolvePolicies as resolvePolicies8, EntryNotFoundError as EntryNotFoundError6 } from "@beechcms/core";
5777
+ import { isValidContentStatus as isValidContentStatus4, resolvePolicies as resolvePolicies8, EntryNotFoundError as EntryNotFoundError7 } from "@beechcms/core";
5765
5778
  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
5779
  function errorMessage(context, error) {
5767
5780
  if (context.env.ENV !== "production" && error instanceof Error) return error.message;
@@ -5869,7 +5882,7 @@ async function publicEditHandler(context) {
5869
5882
  });
5870
5883
  return context.json({ success: true, id, slug: slugResult.value.nextSlug }, 200);
5871
5884
  } catch (error) {
5872
- if (error instanceof EntryNotFoundError6) {
5885
+ if (error instanceof EntryNotFoundError7) {
5873
5886
  return publicProblem(context, { type: "entry-not-found", title: "Not Found", status: 404, detail: `Entry '${id}' not found for content type '${seedSlug}'.` });
5874
5887
  }
5875
5888
  console.error("Public edit error:", error);
@@ -6092,7 +6105,7 @@ import { createMiddleware as createMiddleware2 } from "hono/factory";
6092
6105
 
6093
6106
  // src/shared/db/repositories/content.repository.d1.ts
6094
6107
  import {
6095
- EntryNotFoundError as EntryNotFoundError7,
6108
+ EntryNotFoundError as EntryNotFoundError8,
6096
6109
  RepositoryError as RepositoryError2,
6097
6110
  SlugConflictError as SlugConflictError4,
6098
6111
  RelationTargetNotFoundError as RelationTargetNotFoundError2,
@@ -6120,7 +6133,7 @@ var BaseD1Repository = class {
6120
6133
  */
6121
6134
  mapError(error, context) {
6122
6135
  const message = error?.message || "Unknown database error";
6123
- if (message.includes("UNIQUE constraint failed:") && message.includes(".slug")) {
6136
+ if (message.includes("UNIQUE constraint failed") && message.includes("slug")) {
6124
6137
  return new SlugConflictError3(`${context}: ${message}`);
6125
6138
  }
6126
6139
  return new RepositoryError(`${context}: ${message}`, error);
@@ -6274,13 +6287,13 @@ var D1ContentRepository = class extends BaseD1Repository {
6274
6287
  const tableName = this.getTableName(seed.slug);
6275
6288
  const entryRow = await this.database.prepare(`SELECT * FROM ${tableName} WHERE id = ? LIMIT 1`).bind(id).first();
6276
6289
  if (!entryRow) {
6277
- throw new EntryNotFoundError7(`Entry ${id} not found in ${seed.slug}`);
6290
+ throw new EntryNotFoundError8(`Entry ${id} not found in ${seed.slug}`);
6278
6291
  }
6279
6292
  const data = this.rowToData(seed, entryRow);
6280
6293
  await this.attachMultiRelations(seed, id, data);
6281
6294
  return data;
6282
6295
  } catch (error) {
6283
- if (error instanceof EntryNotFoundError7) throw error;
6296
+ if (error instanceof EntryNotFoundError8) throw error;
6284
6297
  throw this.mapError(error, `findById(${seed.slug}, ${id})`);
6285
6298
  }
6286
6299
  }
@@ -6294,13 +6307,13 @@ var D1ContentRepository = class extends BaseD1Repository {
6294
6307
  const tableName = this.getTableName(seed.slug);
6295
6308
  const entryRow = await this.database.prepare(`SELECT * FROM ${tableName} WHERE slug = ? LIMIT 1`).bind(slug).first();
6296
6309
  if (!entryRow) {
6297
- throw new EntryNotFoundError7(`Entry with slug "${slug}" not found in ${seed.slug}`);
6310
+ throw new EntryNotFoundError8(`Entry with slug "${slug}" not found in ${seed.slug}`);
6298
6311
  }
6299
6312
  const data = this.rowToData(seed, entryRow);
6300
6313
  await this.attachMultiRelations(seed, entryRow.id, data);
6301
6314
  return data;
6302
6315
  } catch (error) {
6303
- if (error instanceof EntryNotFoundError7) throw error;
6316
+ if (error instanceof EntryNotFoundError8) throw error;
6304
6317
  throw this.mapError(error, `findBySlug(${seed.slug}, ${slug})`);
6305
6318
  }
6306
6319
  }
@@ -6616,16 +6629,16 @@ var D1ContentRepository = class extends BaseD1Repository {
6616
6629
  if (batchStmts.length === 1) {
6617
6630
  const updateResult = await batchStmts[0].run();
6618
6631
  if (updateResult.meta.changes === 0) {
6619
- throw new EntryNotFoundError7(`Entry ${id} not found in ${seed.slug}`);
6632
+ throw new EntryNotFoundError8(`Entry ${id} not found in ${seed.slug}`);
6620
6633
  }
6621
6634
  } else if (batchStmts.length > 1) {
6622
6635
  const results = await this.database.batch(batchStmts);
6623
6636
  if (stmt && (results[0].meta?.changes ?? 0) === 0) {
6624
- throw new EntryNotFoundError7(`Entry ${id} not found in ${seed.slug}`);
6637
+ throw new EntryNotFoundError8(`Entry ${id} not found in ${seed.slug}`);
6625
6638
  }
6626
6639
  }
6627
6640
  } catch (error) {
6628
- if (error instanceof EntryNotFoundError7) throw error;
6641
+ if (error instanceof EntryNotFoundError8) throw error;
6629
6642
  throw this.mapError(error, `update(${seed.slug}, ${id})`);
6630
6643
  }
6631
6644
  if (this.hooks?.afterUpdate) {
@@ -6650,12 +6663,12 @@ var D1ContentRepository = class extends BaseD1Repository {
6650
6663
  const tableName = this.getTableName(seed.slug);
6651
6664
  const entryRow = await this.database.prepare(`SELECT * FROM ${tableName} WHERE id = ?`).bind(id).first();
6652
6665
  if (!entryRow) {
6653
- throw new EntryNotFoundError7(`Entry ${id} not found in ${seed.slug}`);
6666
+ throw new EntryNotFoundError8(`Entry ${id} not found in ${seed.slug}`);
6654
6667
  }
6655
6668
  await this.database.prepare(`DELETE FROM ${tableName} WHERE id = ?`).bind(id).run();
6656
6669
  row = this.rowToData(seed, entryRow);
6657
6670
  } catch (error) {
6658
- if (error instanceof EntryNotFoundError7) throw error;
6671
+ if (error instanceof EntryNotFoundError8) throw error;
6659
6672
  throw this.mapError(error, `delete(${seed.slug}, ${id})`);
6660
6673
  }
6661
6674
  if (this.hooks?.afterDelete) {
@@ -6909,7 +6922,7 @@ var D1ContentRepository = class extends BaseD1Repository {
6909
6922
  const liveTableName = this.getTableName(seed.slug);
6910
6923
  const draftRow = await this.database.prepare(`SELECT * FROM ${draftTableName} WHERE entry_id = ?`).bind(entryId).first();
6911
6924
  if (!draftRow) {
6912
- throw new EntryNotFoundError7(`No draft found for ${entryId} in ${seed.slug}`);
6925
+ throw new EntryNotFoundError8(`No draft found for ${entryId} in ${seed.slug}`);
6913
6926
  }
6914
6927
  let touchedFields = [];
6915
6928
  if (draftRow && typeof draftRow["_touched_fields"] === "string") {
@@ -6964,7 +6977,7 @@ var D1ContentRepository = class extends BaseD1Repository {
6964
6977
  }
6965
6978
  await this.database.batch(batchStmts);
6966
6979
  } catch (error) {
6967
- if (error instanceof EntryNotFoundError7) throw error;
6980
+ if (error instanceof EntryNotFoundError8) throw error;
6968
6981
  if (error instanceof RelationTargetNotFoundError2) throw error;
6969
6982
  throw this.mapError(error, `publishDraft(${seed.slug}, ${entryId})`);
6970
6983
  }
@@ -10172,8 +10185,8 @@ function createBeechApp(config) {
10172
10185
  }
10173
10186
  return null;
10174
10187
  },
10175
- allowMethods: ["GET", "POST", "PUT", "DELETE", "OPTIONS"],
10176
- allowHeaders: ["Content-Type", "Authorization", "X-API-Key"],
10188
+ allowMethods: ["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"],
10189
+ allowHeaders: ["Content-Type", "Authorization", "X-API-Key", "Idempotency-Key"],
10177
10190
  credentials: true
10178
10191
  })(context, next);
10179
10192
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@beechcms/api",
3
- "version": "0.6.0",
3
+ "version": "0.6.1",
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.1",
24
24
  "@upstash/qstash": "^2.11.0",
25
25
  "bcryptjs": "^2.4.3",
26
26
  "hono": "^4.12.21",