@bobbykim/manguito-cms-api 0.8.0 → 0.8.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.mjs CHANGED
@@ -38,7 +38,7 @@ function createVersionedPaths(prefix, version) {
38
38
 
39
39
  // src/app.ts
40
40
  import { Hono } from "hono";
41
- import { sql as sql10 } from "drizzle-orm";
41
+ import { sql as sql11 } from "drizzle-orm";
42
42
 
43
43
  // src/middleware/cors.ts
44
44
  function createCorsMiddleware(corsConfig) {
@@ -1049,7 +1049,7 @@ function registerPublicMediaRoutes(app, mediaRepo, paths, listRateLimit) {
1049
1049
  }
1050
1050
 
1051
1051
  // src/routes/admin/content.ts
1052
- import { sql as sql3 } from "drizzle-orm";
1052
+ import { sql as sql4 } from "drizzle-orm";
1053
1053
 
1054
1054
  // src/references-in-use.ts
1055
1055
  import { sql as sql2 } from "drizzle-orm";
@@ -1134,6 +1134,7 @@ async function applyMediaReferenceDelta(delta, mediaRepo) {
1134
1134
  }
1135
1135
 
1136
1136
  // src/relation-input.ts
1137
+ import { sql as sql3 } from "drizzle-orm";
1137
1138
  import { relationCardinality as relationCardinality2 } from "@bobbykim/manguito-cms-core";
1138
1139
  var UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
1139
1140
  function isUuid(v) {
@@ -1178,15 +1179,91 @@ function checkRelationInput(fields, body, registry, path = "") {
1178
1179
  }
1179
1180
  return errors;
1180
1181
  }
1182
+ function paragraphItemsIn(fields, body, registry, path) {
1183
+ const out = [];
1184
+ for (const field of fields) {
1185
+ if (field.field_type !== "paragraph" || !(field.name in body)) continue;
1186
+ const comp = field.ui_component;
1187
+ const pType = comp.component === "paragraph-embed" ? registry.paragraph_types[comp.ref] : void 0;
1188
+ if (!pType) continue;
1189
+ const value = body[field.name];
1190
+ const name = `${path}${field.name}`;
1191
+ if (relationCardinality2(field) === "one") {
1192
+ if (isPlainObject(value)) out.push({ item: value, fields: pType.fields, prefix: `${name}.` });
1193
+ } else if (Array.isArray(value)) {
1194
+ value.forEach((item, i) => {
1195
+ if (isPlainObject(item)) out.push({ item, fields: pType.fields, prefix: `${name}[${i}].` });
1196
+ });
1197
+ }
1198
+ }
1199
+ return out;
1200
+ }
1201
+ function isMissing(value) {
1202
+ if (value === null || value === void 0) return true;
1203
+ if (typeof value === "string" && value.trim() === "") return true;
1204
+ if (Array.isArray(value) && value.length === 0) return true;
1205
+ return false;
1206
+ }
1207
+ function checkRequiredInParagraphItems(fields, body, registry, path = "") {
1208
+ const errors = [];
1209
+ for (const { item, fields: itemFields, prefix } of paragraphItemsIn(fields, body, registry, path)) {
1210
+ for (const f of itemFields) {
1211
+ if (!f.required || f.removed === true || !f.db_column || f.db_column.junction) continue;
1212
+ if (isMissing(item[f.name])) {
1213
+ errors.push({ field: `${prefix}${f.name}`, message: `${prefix}${f.name} is required.` });
1214
+ }
1215
+ }
1216
+ errors.push(...checkRequiredInParagraphItems(itemFields, item, registry, prefix));
1217
+ }
1218
+ return errors;
1219
+ }
1220
+ function quoteIdent2(name) {
1221
+ if (!/^[a-z][a-z0-9_-]*$/.test(name)) throw new Error(`Unsafe identifier: ${name}`);
1222
+ return `"${name}"`;
1223
+ }
1224
+ function collectReferences(fields, body, registry, path, out) {
1225
+ for (const f of fields) {
1226
+ if (!(f.name in body) || !f.db_column) continue;
1227
+ const table = f.db_column.junction ? f.db_column.junction.right_table : f.db_column.foreign_key?.table;
1228
+ if (!table) continue;
1229
+ const value = body[f.name];
1230
+ const name = `${path}${f.name}`;
1231
+ if (typeof value === "string") {
1232
+ out.push({ table, id: value, field: name });
1233
+ } else if (Array.isArray(value)) {
1234
+ value.forEach((v, i) => {
1235
+ if (typeof v === "string") out.push({ table, id: v, field: `${name}[${i}]` });
1236
+ });
1237
+ }
1238
+ }
1239
+ for (const { item, fields: itemFields, prefix } of paragraphItemsIn(fields, body, registry, path)) {
1240
+ collectReferences(itemFields, item, registry, prefix, out);
1241
+ }
1242
+ }
1243
+ async function findMissingReferences(db, fields, body, registry) {
1244
+ const uses = [];
1245
+ collectReferences(fields, body, registry, "", uses);
1246
+ if (uses.length === 0) return [];
1247
+ const lookups = [...new Set(uses.map((u) => u.table))].map(async (table) => {
1248
+ const ids = [...new Set(uses.filter((u) => u.table === table && isUuid(u.id)).map((u) => u.id.toLowerCase()))];
1249
+ if (ids.length === 0) return [table, /* @__PURE__ */ new Set()];
1250
+ const result = await db.execute(
1251
+ sql3`SELECT id FROM ${sql3.raw(quoteIdent2(table))} WHERE id IN (${sql3.join(ids.map((id) => sql3`${id}`), sql3`, `)})`
1252
+ );
1253
+ return [table, new Set(result.rows.map((r) => String(r.id).toLowerCase()))];
1254
+ });
1255
+ const found = new Map(await Promise.all(lookups));
1256
+ return uses.filter((u) => !found.get(u.table).has(u.id.toLowerCase())).map((u) => ({ field: u.field, message: `${u.field} refers to an item that does not exist.` }));
1257
+ }
1181
1258
 
1182
1259
  // src/routes/admin/content.ts
1183
- function quoteIdent2(name) {
1260
+ function quoteIdent3(name) {
1184
1261
  if (!/^[a-z][a-z0-9_-]*$/.test(name)) throw new Error(`Unsafe identifier: ${name}`);
1185
1262
  return `"${name}"`;
1186
1263
  }
1187
1264
  async function loadParagraphRows(db, registry, pType, parentId, fieldName) {
1188
1265
  const result = await db.execute(
1189
- sql3`SELECT * FROM ${sql3.raw(quoteIdent2(pType.db.table_name))} WHERE parent_id = ${parentId} AND parent_field = ${fieldName} ORDER BY "order" ASC`
1266
+ sql4`SELECT * FROM ${sql4.raw(quoteIdent3(pType.db.table_name))} WHERE parent_id = ${parentId} AND parent_field = ${fieldName} ORDER BY "order" ASC`
1190
1267
  );
1191
1268
  const rows = result.rows;
1192
1269
  for (const nf of pType.fields) {
@@ -1206,27 +1283,25 @@ async function loadParagraphRows(db, registry, pType, parentId, fieldName) {
1206
1283
  }
1207
1284
  async function lookupBasePathId(db, pathOrName) {
1208
1285
  const r = await db.execute(
1209
- sql3`SELECT id FROM base_paths WHERE path = ${pathOrName} OR name = ${pathOrName} LIMIT 1`
1286
+ sql4`SELECT id FROM base_paths WHERE path = ${pathOrName} OR name = ${pathOrName} LIMIT 1`
1210
1287
  );
1211
1288
  return r.rows[0]?.id ?? null;
1212
1289
  }
1213
1290
  var SLUG_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
1214
- function isEmpty(value) {
1215
- if (value === null || value === void 0) return true;
1216
- if (typeof value === "string" && value.trim() === "") return true;
1217
- if (Array.isArray(value) && value.length === 0) return true;
1218
- return false;
1219
- }
1220
1291
  function checkRequiredFields(fields, data) {
1221
- return fields.filter((f) => f.required && isEmpty(data[f.name])).map((f) => ({ field: f.name, message: `${f.label} is required` }));
1222
- }
1223
- function relationShapeError(c, fields, body, registry) {
1224
- const details = checkRelationInput(fields, body, registry);
1225
- if (details.length === 0) return null;
1226
- return c.json(
1227
- { ok: false, error: { code: "VALIDATION_ERROR", message: "Relation fields have the wrong shape", details } },
1228
- 422
1229
- );
1292
+ return fields.filter((f) => f.required && isMissing(data[f.name])).map((f) => ({ field: f.name, message: `${f.label} is required` }));
1293
+ }
1294
+ async function relationInputError(c, fields, body, registry, db) {
1295
+ const refuse = (message, details) => c.json({ ok: false, error: { code: "VALIDATION_ERROR", message, details } }, 422);
1296
+ const shape = checkRelationInput(fields, body, registry);
1297
+ if (shape.length > 0) return refuse("Relation fields have the wrong shape", shape);
1298
+ const required = checkRequiredInParagraphItems(fields, body, registry);
1299
+ if (required.length > 0) return refuse("Required fields are missing", required);
1300
+ if (db) {
1301
+ const missing = await findMissingReferences(db, fields, body, registry);
1302
+ if (missing.length > 0) return refuse("Relation fields refer to items that do not exist", missing);
1303
+ }
1304
+ return null;
1230
1305
  }
1231
1306
  function parseListQuery(url, schemaFieldNames, relationFieldNames, columnFor) {
1232
1307
  const searchParams = new URL(url).searchParams;
@@ -1373,7 +1448,7 @@ function registerAdminContentRoutes(app, registry, repos, projectors, mediaRepo,
1373
1448
  } else if (f.db_column.junction) {
1374
1449
  const j = f.db_column.junction;
1375
1450
  const r = await db.execute(
1376
- sql3`SELECT ${sql3.raw(quoteIdent2(j.right_column))} FROM ${sql3.raw(quoteIdent2(j.table_name))} WHERE ${sql3.raw(quoteIdent2(j.left_column))} = ${id}`
1451
+ sql4`SELECT ${sql4.raw(quoteIdent3(j.right_column))} FROM ${sql4.raw(quoteIdent3(j.table_name))} WHERE ${sql4.raw(quoteIdent3(j.left_column))} = ${id}`
1377
1452
  );
1378
1453
  row[f.name] = r.rows.map(
1379
1454
  (row2) => row2[j.right_column]
@@ -1449,8 +1524,8 @@ function registerAdminContentRoutes(app, registry, repos, projectors, mediaRepo,
1449
1524
  }
1450
1525
  );
1451
1526
  const writeNewItem = async (c, body) => {
1452
- const shapeError = relationShapeError(c, contentType.fields, body, registry);
1453
- if (shapeError) return shapeError;
1527
+ const inputError = await relationInputError(c, contentType.fields, body, registry, db);
1528
+ if (inputError) return inputError;
1454
1529
  const storageBody = fieldKeys.toStorage(body);
1455
1530
  const fieldErrors = checkRequiredFields(requiredFields, body);
1456
1531
  if (fieldErrors.length > 0) {
@@ -1585,8 +1660,8 @@ function registerAdminContentRoutes(app, registry, repos, projectors, mediaRepo,
1585
1660
  }
1586
1661
  );
1587
1662
  const writeExistingItem = async (c, id, existing, body) => {
1588
- const shapeError = relationShapeError(c, contentType.fields, body, registry);
1589
- if (shapeError) return shapeError;
1663
+ const inputError = await relationInputError(c, contentType.fields, body, registry, db);
1664
+ if (inputError) return inputError;
1590
1665
  const storageBody = fieldKeys.toStorage(body);
1591
1666
  if (body["published"] === true) {
1592
1667
  const publishDeny = await requirePermission("content:edit")(c, async () => {
@@ -1784,8 +1859,8 @@ function registerAdminContentRoutes(app, registry, repos, projectors, mediaRepo,
1784
1859
  requirePermission("content:create"),
1785
1860
  async (c) => {
1786
1861
  const body = await c.req.json();
1787
- const shapeError = relationShapeError(c, taxonomyType.fields, body, registry);
1788
- if (shapeError) return shapeError;
1862
+ const inputError = await relationInputError(c, taxonomyType.fields, body, registry, db);
1863
+ if (inputError) return inputError;
1789
1864
  const storageBody = fieldKeys.toStorage(body);
1790
1865
  const fieldErrors = checkRequiredFields(requiredFields, body);
1791
1866
  if (fieldErrors.length > 0) {
@@ -1848,8 +1923,8 @@ function registerAdminContentRoutes(app, registry, repos, projectors, mediaRepo,
1848
1923
  404
1849
1924
  );
1850
1925
  }
1851
- const shapeError = relationShapeError(c, taxonomyType.fields, body, registry);
1852
- if (shapeError) return shapeError;
1926
+ const inputError = await relationInputError(c, taxonomyType.fields, body, registry, db);
1927
+ if (inputError) return inputError;
1853
1928
  if (body["published"] === true) {
1854
1929
  const publishDeny = await requirePermission("content:edit")(c, async () => {
1855
1930
  });
@@ -2403,7 +2478,7 @@ function registerAdminMediaRoutes(app, mediaRepo, storage, requirePermission, ma
2403
2478
 
2404
2479
  // src/routes/admin/auth.ts
2405
2480
  import { getCookie as getCookie2 } from "hono/cookie";
2406
- import { sql as sql4 } from "drizzle-orm";
2481
+ import { sql as sql5 } from "drizzle-orm";
2407
2482
 
2408
2483
  // src/auth/password.ts
2409
2484
  import { hashPassword, verifyPassword } from "@bobbykim/manguito-cms-core";
@@ -2469,7 +2544,7 @@ function registerAuthRoutes(app, db) {
2469
2544
  );
2470
2545
  }
2471
2546
  const userResult = await db.execute(
2472
- sql4`SELECT id, email, password_hash, role_id, token_version
2547
+ sql5`SELECT id, email, password_hash, role_id, token_version
2473
2548
  FROM users WHERE email = ${email} LIMIT 1`
2474
2549
  );
2475
2550
  const user = userResult.rows[0];
@@ -2482,7 +2557,7 @@ function registerAuthRoutes(app, db) {
2482
2557
  return c.json(invalidCredentials, 401);
2483
2558
  }
2484
2559
  const roleResult = await db.execute(
2485
- sql4`SELECT r.name AS name, u.must_change_password
2560
+ sql5`SELECT r.name AS name, u.must_change_password
2486
2561
  FROM roles r
2487
2562
  JOIN users u ON u.role_id = r.id
2488
2563
  WHERE r.id = ${user.role_id} AND u.id = ${user.id}
@@ -2542,7 +2617,7 @@ function registerAuthRoutes(app, db) {
2542
2617
  );
2543
2618
  }
2544
2619
  const rowResult = await db.execute(
2545
- sql4`SELECT u.token_version, r.name AS role
2620
+ sql5`SELECT u.token_version, r.name AS role
2546
2621
  FROM users u
2547
2622
  JOIN roles r ON r.id = u.role_id
2548
2623
  WHERE u.id = ${payload.user_id}
@@ -2566,7 +2641,7 @@ function registerAuthRoutes(app, db) {
2566
2641
  app.post("/admin/api/auth/logout", authMiddleware, async (c) => {
2567
2642
  const user = c.get("user");
2568
2643
  await db.execute(
2569
- sql4`UPDATE users SET token_version = token_version + 1 WHERE id = ${user.id}`
2644
+ sql5`UPDATE users SET token_version = token_version + 1 WHERE id = ${user.id}`
2570
2645
  );
2571
2646
  clearAuthCookie(c, "auth_token");
2572
2647
  clearAuthCookie(c, "refresh_token", "/admin/api/auth");
@@ -2575,7 +2650,7 @@ function registerAuthRoutes(app, db) {
2575
2650
  }
2576
2651
 
2577
2652
  // src/routes/admin/users.ts
2578
- import { sql as sql5 } from "drizzle-orm";
2653
+ import { sql as sql6 } from "drizzle-orm";
2579
2654
  import { randomBytes } from "crypto";
2580
2655
  function getActingUser(c) {
2581
2656
  return c.get("user");
@@ -2610,7 +2685,7 @@ function registerUserRoutes(app, db, requirePermission, requireHierarchy) {
2610
2685
  );
2611
2686
  }
2612
2687
  const hashResult = await db.execute(
2613
- sql5`SELECT password_hash FROM users WHERE id = ${actingUser.id} LIMIT 1`
2688
+ sql6`SELECT password_hash FROM users WHERE id = ${actingUser.id} LIMIT 1`
2614
2689
  );
2615
2690
  const hashRow = hashResult.rows[0];
2616
2691
  if (!hashRow || !await verifyPassword(currentPassword, hashRow.password_hash)) {
@@ -2621,7 +2696,7 @@ function registerUserRoutes(app, db, requirePermission, requireHierarchy) {
2621
2696
  }
2622
2697
  const newHash = await hashPassword(newPassword);
2623
2698
  await db.execute(
2624
- sql5`UPDATE users
2699
+ sql6`UPDATE users
2625
2700
  SET password_hash = ${newHash},
2626
2701
  must_change_password = false,
2627
2702
  token_version = token_version + 1,
@@ -2632,7 +2707,7 @@ function registerUserRoutes(app, db, requirePermission, requireHierarchy) {
2632
2707
  });
2633
2708
  app.get("/admin/api/users", requirePermission("users:read"), async (c) => {
2634
2709
  const result = await db.execute(
2635
- sql5`SELECT u.id, u.email, r.name AS role, u.must_change_password,
2710
+ sql6`SELECT u.id, u.email, r.name AS role, u.must_change_password,
2636
2711
  u.created_at, u.updated_at
2637
2712
  FROM users u
2638
2713
  JOIN roles r ON r.id = u.role_id
@@ -2643,7 +2718,7 @@ function registerUserRoutes(app, db, requirePermission, requireHierarchy) {
2643
2718
  app.get("/admin/api/users/:id", requirePermission("users:read"), async (c) => {
2644
2719
  const id = c.req.param("id");
2645
2720
  const result = await db.execute(
2646
- sql5`SELECT u.id, u.email, r.name AS role, u.must_change_password,
2721
+ sql6`SELECT u.id, u.email, r.name AS role, u.must_change_password,
2647
2722
  u.created_at, u.updated_at
2648
2723
  FROM users u
2649
2724
  JOIN roles r ON r.id = u.role_id
@@ -2688,7 +2763,7 @@ function registerUserRoutes(app, db, requirePermission, requireHierarchy) {
2688
2763
  );
2689
2764
  }
2690
2765
  const roleResult = await db.execute(
2691
- sql5`SELECT id FROM roles WHERE name = ${roleName} LIMIT 1`
2766
+ sql6`SELECT id FROM roles WHERE name = ${roleName} LIMIT 1`
2692
2767
  );
2693
2768
  const roleRow = roleResult.rows[0];
2694
2769
  if (!roleRow) {
@@ -2703,7 +2778,7 @@ function registerUserRoutes(app, db, requirePermission, requireHierarchy) {
2703
2778
  const tempPassword = generateTempPassword();
2704
2779
  const passwordHash = await hashPassword(tempPassword);
2705
2780
  const insertResult = await db.execute(
2706
- sql5`INSERT INTO users
2781
+ sql6`INSERT INTO users
2707
2782
  (id, email, password_hash, role_id, must_change_password, token_version,
2708
2783
  created_at, updated_at)
2709
2784
  VALUES
@@ -2758,7 +2833,7 @@ function registerUserRoutes(app, db, requirePermission, requireHierarchy) {
2758
2833
  );
2759
2834
  }
2760
2835
  const existingResult = await db.execute(
2761
- sql5`SELECT id, email, role_id FROM users WHERE id = ${id} LIMIT 1`
2836
+ sql6`SELECT id, email, role_id FROM users WHERE id = ${id} LIMIT 1`
2762
2837
  );
2763
2838
  const existing = existingResult.rows[0];
2764
2839
  if (!existing) {
@@ -2772,7 +2847,7 @@ function registerUserRoutes(app, db, requirePermission, requireHierarchy) {
2772
2847
  let changeRole = false;
2773
2848
  if (typeof body.role === "string" && body.role) {
2774
2849
  const roleResult = await db.execute(
2775
- sql5`SELECT id FROM roles WHERE name = ${body.role} LIMIT 1`
2850
+ sql6`SELECT id FROM roles WHERE name = ${body.role} LIMIT 1`
2776
2851
  );
2777
2852
  const roleRow = roleResult.rows[0];
2778
2853
  if (!roleRow) {
@@ -2792,7 +2867,7 @@ function registerUserRoutes(app, db, requirePermission, requireHierarchy) {
2792
2867
  }
2793
2868
  if (changeRole) {
2794
2869
  await db.execute(
2795
- sql5`UPDATE users
2870
+ sql6`UPDATE users
2796
2871
  SET email = ${newEmail},
2797
2872
  role_id = ${newRoleId},
2798
2873
  token_version = token_version + 1,
@@ -2801,7 +2876,7 @@ function registerUserRoutes(app, db, requirePermission, requireHierarchy) {
2801
2876
  );
2802
2877
  } else {
2803
2878
  await db.execute(
2804
- sql5`UPDATE users
2879
+ sql6`UPDATE users
2805
2880
  SET email = ${newEmail},
2806
2881
  role_id = ${newRoleId},
2807
2882
  updated_at = NOW()
@@ -2809,7 +2884,7 @@ function registerUserRoutes(app, db, requirePermission, requireHierarchy) {
2809
2884
  );
2810
2885
  }
2811
2886
  const updatedResult = await db.execute(
2812
- sql5`SELECT u.id, u.email, r.name AS role, u.must_change_password,
2887
+ sql6`SELECT u.id, u.email, r.name AS role, u.must_change_password,
2813
2888
  u.created_at, u.updated_at
2814
2889
  FROM users u
2815
2890
  JOIN roles r ON r.id = u.role_id
@@ -2846,7 +2921,7 @@ function registerUserRoutes(app, db, requirePermission, requireHierarchy) {
2846
2921
  );
2847
2922
  }
2848
2923
  const result = await db.execute(
2849
- sql5`DELETE FROM users WHERE id = ${id} RETURNING id`
2924
+ sql6`DELETE FROM users WHERE id = ${id} RETURNING id`
2850
2925
  );
2851
2926
  if (result.rows.length === 0) {
2852
2927
  return c.json(
@@ -2877,7 +2952,7 @@ function registerUserRoutes(app, db, requirePermission, requireHierarchy) {
2877
2952
  );
2878
2953
  }
2879
2954
  const existingResult = await db.execute(
2880
- sql5`SELECT id FROM users WHERE id = ${id} LIMIT 1`
2955
+ sql6`SELECT id FROM users WHERE id = ${id} LIMIT 1`
2881
2956
  );
2882
2957
  const existing = existingResult.rows[0];
2883
2958
  if (!existing) {
@@ -2889,7 +2964,7 @@ function registerUserRoutes(app, db, requirePermission, requireHierarchy) {
2889
2964
  const tempPassword = generateTempPassword();
2890
2965
  const passwordHash = await hashPassword(tempPassword);
2891
2966
  await db.execute(
2892
- sql5`UPDATE users
2967
+ sql6`UPDATE users
2893
2968
  SET password_hash = ${passwordHash},
2894
2969
  must_change_password = true,
2895
2970
  token_version = token_version + 1,
@@ -2902,10 +2977,10 @@ function registerUserRoutes(app, db, requirePermission, requireHierarchy) {
2902
2977
  }
2903
2978
 
2904
2979
  // src/routes/admin/config.ts
2905
- import { sql as sql6 } from "drizzle-orm";
2980
+ import { sql as sql7 } from "drizzle-orm";
2906
2981
  var API_VERSION = (() => {
2907
2982
  try {
2908
- return "0.8.0";
2983
+ return "0.8.1";
2909
2984
  } catch {
2910
2985
  return "0.0.0";
2911
2986
  }
@@ -2924,7 +2999,7 @@ function registerConfigRoute(app, config, registry, db) {
2924
2999
  permissions: r.permissions
2925
3000
  }));
2926
3001
  const userResult = await db.execute(
2927
- sql6`SELECT email, must_change_password FROM users WHERE id = ${actingUser.id} LIMIT 1`
3002
+ sql7`SELECT email, must_change_password FROM users WHERE id = ${actingUser.id} LIMIT 1`
2928
3003
  );
2929
3004
  const userRow = userResult.rows[0];
2930
3005
  return c.json({
@@ -2950,7 +3025,7 @@ function registerConfigRoute(app, config, registry, db) {
2950
3025
  }
2951
3026
 
2952
3027
  // src/routes/admin/schema.ts
2953
- import { sql as sql7 } from "drizzle-orm";
3028
+ import { sql as sql8 } from "drizzle-orm";
2954
3029
  function liveFields(fields) {
2955
3030
  return fields.filter((f) => f.removed !== true);
2956
3031
  }
@@ -2990,7 +3065,7 @@ function registerSchemaRoute(app, registry, db) {
2990
3065
  const data = await Promise.all(
2991
3066
  Object.values(registry.content_types).map(async (ct) => {
2992
3067
  const result = await db.execute(
2993
- sql7`SELECT COUNT(*)::int AS count FROM ${sql7.raw(`"${ct.db.table_name}"`)}`
3068
+ sql8`SELECT COUNT(*)::int AS count FROM ${sql8.raw(`"${ct.db.table_name}"`)}`
2994
3069
  );
2995
3070
  const count = Number(result.rows[0].count);
2996
3071
  return { name: ct.name, label: ct.label, only_one: ct.only_one, count };
@@ -3002,7 +3077,7 @@ function registerSchemaRoute(app, registry, db) {
3002
3077
  const data = await Promise.all(
3003
3078
  Object.values(registry.taxonomy_types).map(async (tt) => {
3004
3079
  const result = await db.execute(
3005
- sql7`SELECT COUNT(*)::int AS count FROM ${sql7.raw(`"${tt.db.table_name}"`)}`
3080
+ sql8`SELECT COUNT(*)::int AS count FROM ${sql8.raw(`"${tt.db.table_name}"`)}`
3006
3081
  );
3007
3082
  const count = Number(result.rows[0].count);
3008
3083
  return { name: tt.name, label: tt.label, count };
@@ -3014,14 +3089,14 @@ function registerSchemaRoute(app, registry, db) {
3014
3089
 
3015
3090
  // src/repositories/content.ts
3016
3091
  import crypto from "crypto";
3017
- import { sql as sql8 } from "drizzle-orm";
3092
+ import { sql as sql9 } from "drizzle-orm";
3018
3093
  var SORTABLE_FIELDS2 = /* @__PURE__ */ new Set(["title", "created_at", "updated_at"]);
3019
3094
  function codeError(code, message) {
3020
3095
  const err = new Error(message);
3021
3096
  err.code = code;
3022
3097
  return err;
3023
3098
  }
3024
- function quoteIdent3(name) {
3099
+ function quoteIdent4(name) {
3025
3100
  if (!/^[a-z][a-z0-9_-]*$/.test(name)) {
3026
3101
  throw new Error(`Unsafe SQL identifier: ${name}`);
3027
3102
  }
@@ -3036,43 +3111,43 @@ function quoteField(name) {
3036
3111
  function buildConditions(published_only, filters, search) {
3037
3112
  const conds = [];
3038
3113
  if (published_only) {
3039
- conds.push(sql8`published = true`);
3114
+ conds.push(sql9`published = true`);
3040
3115
  }
3041
3116
  if (search && search.term.trim() !== "" && search.columns.length > 0) {
3042
3117
  const pattern = `%${search.term.trim()}%`;
3043
- const orConds = search.columns.map((col) => sql8`${sql8.raw(quoteField(col))} ILIKE ${pattern}`);
3044
- conds.push(sql8`(${sql8.join(orConds, sql8` OR `)})`);
3118
+ const orConds = search.columns.map((col) => sql9`${sql9.raw(quoteField(col))} ILIKE ${pattern}`);
3119
+ conds.push(sql9`(${sql9.join(orConds, sql9` OR `)})`);
3045
3120
  }
3046
3121
  if (filters) {
3047
3122
  for (const [field, value] of Object.entries(filters)) {
3048
- const col = sql8.raw(quoteField(field));
3123
+ const col = sql9.raw(quoteField(field));
3049
3124
  if (Array.isArray(value)) {
3050
3125
  if (value.length === 0) continue;
3051
- const inList = sql8.join(
3052
- value.map((v) => sql8`${v}`),
3053
- sql8`, `
3126
+ const inList = sql9.join(
3127
+ value.map((v) => sql9`${v}`),
3128
+ sql9`, `
3054
3129
  );
3055
- conds.push(sql8`${col} IN (${inList})`);
3130
+ conds.push(sql9`${col} IN (${inList})`);
3056
3131
  } else if (typeof value === "object" && value !== null) {
3057
3132
  const op = value;
3058
- if (op.gt !== void 0) conds.push(sql8`${col} > ${op.gt}`);
3059
- if (op.gte !== void 0) conds.push(sql8`${col} >= ${op.gte}`);
3060
- if (op.lt !== void 0) conds.push(sql8`${col} < ${op.lt}`);
3061
- if (op.lte !== void 0) conds.push(sql8`${col} <= ${op.lte}`);
3133
+ if (op.gt !== void 0) conds.push(sql9`${col} > ${op.gt}`);
3134
+ if (op.gte !== void 0) conds.push(sql9`${col} >= ${op.gte}`);
3135
+ if (op.lt !== void 0) conds.push(sql9`${col} < ${op.lt}`);
3136
+ if (op.lte !== void 0) conds.push(sql9`${col} <= ${op.lte}`);
3062
3137
  } else {
3063
- conds.push(sql8`${col} = ${value}`);
3138
+ conds.push(sql9`${col} = ${value}`);
3064
3139
  }
3065
3140
  }
3066
3141
  }
3067
3142
  return conds;
3068
3143
  }
3069
3144
  function whereFragment(conds) {
3070
- return conds.length > 0 ? sql8` WHERE ${sql8.join(conds, sql8` AND `)}` : sql8``;
3145
+ return conds.length > 0 ? sql9` WHERE ${sql9.join(conds, sql9` AND `)}` : sql9``;
3071
3146
  }
3072
3147
  function createDrizzleContentRepository(db, tableName, options = {}) {
3073
3148
  const { relations = {}, publishedRelations = false, sortableColumns } = options;
3074
3149
  function tableRaw() {
3075
- return sql8.raw(quoteIdent3(tableName));
3150
+ return sql9.raw(quoteIdent4(tableName));
3076
3151
  }
3077
3152
  async function resolveRows(rows, include, cache) {
3078
3153
  if (rows.length === 0) return rows;
@@ -3115,14 +3190,14 @@ function createDrizzleContentRepository(db, tableName, options = {}) {
3115
3190
  const offset = (page - 1) * per_page;
3116
3191
  const conds = buildConditions(published_only, filters, search);
3117
3192
  const where = whereFragment(conds);
3118
- const sortCol = sql8.raw(quoteIdent3(sort_by));
3119
- const sortDir = sql8.raw(sort_order === "desc" ? "DESC" : "ASC");
3193
+ const sortCol = sql9.raw(quoteIdent4(sort_by));
3194
+ const sortDir = sql9.raw(sort_order === "desc" ? "DESC" : "ASC");
3120
3195
  const countResult = await db.execute(
3121
- sql8`SELECT COUNT(*) AS total FROM ${tableRaw()}${where}`
3196
+ sql9`SELECT COUNT(*) AS total FROM ${tableRaw()}${where}`
3122
3197
  );
3123
3198
  const total = Number(countResult.rows[0]?.["total"] ?? 0);
3124
3199
  const dataResult = await db.execute(
3125
- sql8`SELECT * FROM ${tableRaw()}${where} ORDER BY ${sortCol} ${sortDir} LIMIT ${per_page} OFFSET ${offset}`
3200
+ sql9`SELECT * FROM ${tableRaw()}${where} ORDER BY ${sortCol} ${sortDir} LIMIT ${per_page} OFFSET ${offset}`
3126
3201
  );
3127
3202
  const rows = dataResult.rows;
3128
3203
  const cache = /* @__PURE__ */ new Map();
@@ -3143,7 +3218,7 @@ function createDrizzleContentRepository(db, tableName, options = {}) {
3143
3218
  },
3144
3219
  async findOne(id, include = []) {
3145
3220
  const result = await db.execute(
3146
- sql8`SELECT * FROM ${tableRaw()} WHERE id = ${id} LIMIT 1`
3221
+ sql9`SELECT * FROM ${tableRaw()} WHERE id = ${id} LIMIT 1`
3147
3222
  );
3148
3223
  if (result.rows.length === 0) return null;
3149
3224
  const rows = result.rows;
@@ -3153,7 +3228,7 @@ function createDrizzleContentRepository(db, tableName, options = {}) {
3153
3228
  },
3154
3229
  async findBySlug(slug, include = []) {
3155
3230
  const result = await db.execute(
3156
- sql8`SELECT * FROM ${tableRaw()} WHERE slug = ${slug} LIMIT 1`
3231
+ sql9`SELECT * FROM ${tableRaw()} WHERE slug = ${slug} LIMIT 1`
3157
3232
  );
3158
3233
  if (result.rows.length === 0) return null;
3159
3234
  const rows = result.rows;
@@ -3169,10 +3244,10 @@ function createDrizzleContentRepository(db, tableName, options = {}) {
3169
3244
  updated_at: /* @__PURE__ */ new Date()
3170
3245
  };
3171
3246
  const entries = Object.entries(record).filter(([, v]) => v !== void 0);
3172
- const colsSql = sql8.join(entries.map(([k]) => sql8.raw(quoteIdent3(k))), sql8`, `);
3173
- const valsSql = sql8.join(entries.map(([, v]) => sql8`${v}`), sql8`, `);
3247
+ const colsSql = sql9.join(entries.map(([k]) => sql9.raw(quoteIdent4(k))), sql9`, `);
3248
+ const valsSql = sql9.join(entries.map(([, v]) => sql9`${v}`), sql9`, `);
3174
3249
  const result = await db.execute(
3175
- sql8`INSERT INTO ${tableRaw()} (${colsSql}) VALUES (${valsSql}) RETURNING *`
3250
+ sql9`INSERT INTO ${tableRaw()} (${colsSql}) VALUES (${valsSql}) RETURNING *`
3176
3251
  );
3177
3252
  return result.rows[0];
3178
3253
  },
@@ -3185,25 +3260,25 @@ function createDrizzleContentRepository(db, tableName, options = {}) {
3185
3260
  if (entries.length === 0) {
3186
3261
  return this.findOne(id);
3187
3262
  }
3188
- const setClauses = sql8.join(
3189
- entries.map(([k, v]) => sql8`${sql8.raw(quoteIdent3(k))} = ${v}`),
3190
- sql8`, `
3263
+ const setClauses = sql9.join(
3264
+ entries.map(([k, v]) => sql9`${sql9.raw(quoteIdent4(k))} = ${v}`),
3265
+ sql9`, `
3191
3266
  );
3192
3267
  const result = await db.execute(
3193
- sql8`UPDATE ${tableRaw()} SET ${setClauses} WHERE id = ${id} RETURNING *`
3268
+ sql9`UPDATE ${tableRaw()} SET ${setClauses} WHERE id = ${id} RETURNING *`
3194
3269
  );
3195
3270
  if (result.rows.length === 0) return null;
3196
3271
  return result.rows[0];
3197
3272
  },
3198
3273
  async delete(id) {
3199
- await db.execute(sql8`DELETE FROM ${tableRaw()} WHERE id = ${id}`);
3274
+ await db.execute(sql9`DELETE FROM ${tableRaw()} WHERE id = ${id}`);
3200
3275
  },
3201
3276
  async findAll(opts = {}) {
3202
3277
  const { include = [], published_only } = opts;
3203
3278
  const conds = buildConditions(published_only, void 0, void 0);
3204
3279
  const where = whereFragment(conds);
3205
3280
  const result = await db.execute(
3206
- sql8`SELECT * FROM ${tableRaw()}${where} ORDER BY created_at ASC`
3281
+ sql9`SELECT * FROM ${tableRaw()}${where} ORDER BY created_at ASC`
3207
3282
  );
3208
3283
  const rows = result.rows;
3209
3284
  const cache = /* @__PURE__ */ new Map();
@@ -3215,23 +3290,23 @@ function createDrizzleContentRepository(db, tableName, options = {}) {
3215
3290
 
3216
3291
  // src/repositories/media.ts
3217
3292
  import crypto2 from "crypto";
3218
- import { sql as sql9 } from "drizzle-orm";
3293
+ import { sql as sql10 } from "drizzle-orm";
3219
3294
  function createMediaRepository(db) {
3220
- const TABLE = sql9.raw('"media"');
3295
+ const TABLE = sql10.raw('"media"');
3221
3296
  return {
3222
3297
  async findMany(opts = {}) {
3223
3298
  const { page = 1, per_page = 10, type, orphaned } = opts;
3224
3299
  const offset = (page - 1) * per_page;
3225
3300
  const conds = [];
3226
- if (type) conds.push(sql9`type = ${type}`);
3227
- if (orphaned) conds.push(sql9`reference_count = 0`);
3228
- const where = conds.length > 0 ? sql9` WHERE ${sql9.join(conds, sql9` AND `)}` : sql9``;
3229
- const countResult = await db.execute(sql9`SELECT COUNT(*) AS total FROM ${TABLE}${where}`);
3301
+ if (type) conds.push(sql10`type = ${type}`);
3302
+ if (orphaned) conds.push(sql10`reference_count = 0`);
3303
+ const where = conds.length > 0 ? sql10` WHERE ${sql10.join(conds, sql10` AND `)}` : sql10``;
3304
+ const countResult = await db.execute(sql10`SELECT COUNT(*) AS total FROM ${TABLE}${where}`);
3230
3305
  const total = Number(
3231
3306
  countResult.rows[0]?.["total"] ?? 0
3232
3307
  );
3233
3308
  const dataResult = await db.execute(
3234
- sql9`SELECT * FROM ${TABLE}${where} ORDER BY created_at DESC LIMIT ${per_page} OFFSET ${offset}`
3309
+ sql10`SELECT * FROM ${TABLE}${where} ORDER BY created_at DESC LIMIT ${per_page} OFFSET ${offset}`
3235
3310
  );
3236
3311
  const total_pages = Math.ceil(total / per_page);
3237
3312
  return {
@@ -3249,7 +3324,7 @@ function createMediaRepository(db) {
3249
3324
  },
3250
3325
  async findOne(id) {
3251
3326
  const result = await db.execute(
3252
- sql9`SELECT * FROM ${TABLE} WHERE id = ${id} LIMIT 1`
3327
+ sql10`SELECT * FROM ${TABLE} WHERE id = ${id} LIMIT 1`
3253
3328
  );
3254
3329
  if (result.rows.length === 0) return null;
3255
3330
  return result.rows[0];
@@ -3263,10 +3338,10 @@ function createMediaRepository(db) {
3263
3338
  updated_at: /* @__PURE__ */ new Date()
3264
3339
  };
3265
3340
  const entries = Object.entries(record).filter(([, v]) => v !== void 0);
3266
- const colsSql = sql9.join(entries.map(([k]) => sql9.raw(`"${k}"`)), sql9`, `);
3267
- const valsSql = sql9.join(entries.map(([, v]) => sql9`${v}`), sql9`, `);
3341
+ const colsSql = sql10.join(entries.map(([k]) => sql10.raw(`"${k}"`)), sql10`, `);
3342
+ const valsSql = sql10.join(entries.map(([, v]) => sql10`${v}`), sql10`, `);
3268
3343
  const result = await db.execute(
3269
- sql9`INSERT INTO ${TABLE} (${colsSql}) VALUES (${valsSql}) RETURNING *`
3344
+ sql10`INSERT INTO ${TABLE} (${colsSql}) VALUES (${valsSql}) RETURNING *`
3270
3345
  );
3271
3346
  return result.rows[0];
3272
3347
  },
@@ -3276,31 +3351,31 @@ function createMediaRepository(db) {
3276
3351
  ([k, v]) => k !== "id" && v !== void 0
3277
3352
  );
3278
3353
  if (entries.length === 0) return this.findOne(id);
3279
- const setClauses = sql9.join(
3280
- entries.map(([k, v]) => sql9`${sql9.raw(`"${k}"`)} = ${v}`),
3281
- sql9`, `
3354
+ const setClauses = sql10.join(
3355
+ entries.map(([k, v]) => sql10`${sql10.raw(`"${k}"`)} = ${v}`),
3356
+ sql10`, `
3282
3357
  );
3283
3358
  const result = await db.execute(
3284
- sql9`UPDATE ${TABLE} SET ${setClauses} WHERE id = ${id} RETURNING *`
3359
+ sql10`UPDATE ${TABLE} SET ${setClauses} WHERE id = ${id} RETURNING *`
3285
3360
  );
3286
3361
  if (result.rows.length === 0) return null;
3287
3362
  return result.rows[0];
3288
3363
  },
3289
3364
  async delete(id) {
3290
- await db.execute(sql9`DELETE FROM ${TABLE} WHERE id = ${id}`);
3365
+ await db.execute(sql10`DELETE FROM ${TABLE} WHERE id = ${id}`);
3291
3366
  },
3292
3367
  async incrementReferenceCount(ids) {
3293
3368
  if (ids.length === 0) return;
3294
- const inList = sql9.join(ids.map((id) => sql9`${id}`), sql9`, `);
3369
+ const inList = sql10.join(ids.map((id) => sql10`${id}`), sql10`, `);
3295
3370
  await db.execute(
3296
- sql9`UPDATE ${TABLE} SET reference_count = reference_count + 1, updated_at = NOW() WHERE id IN (${inList})`
3371
+ sql10`UPDATE ${TABLE} SET reference_count = reference_count + 1, updated_at = NOW() WHERE id IN (${inList})`
3297
3372
  );
3298
3373
  },
3299
3374
  async decrementReferenceCount(ids) {
3300
3375
  if (ids.length === 0) return;
3301
- const inList = sql9.join(ids.map((id) => sql9`${id}`), sql9`, `);
3376
+ const inList = sql10.join(ids.map((id) => sql10`${id}`), sql10`, `);
3302
3377
  await db.execute(
3303
- sql9`UPDATE ${TABLE} SET reference_count = GREATEST(reference_count - 1, 0), updated_at = NOW() WHERE id IN (${inList})`
3378
+ sql10`UPDATE ${TABLE} SET reference_count = GREATEST(reference_count - 1, 0), updated_at = NOW() WHERE id IN (${inList})`
3304
3379
  );
3305
3380
  }
3306
3381
  };
@@ -3433,7 +3508,7 @@ function createCmsApp(options) {
3433
3508
  const requirePermission = createPermissionMiddleware(rolesRegistry);
3434
3509
  const getUserRole = async (userId) => {
3435
3510
  const result = await db.execute(
3436
- sql10`SELECT r.name AS role
3511
+ sql11`SELECT r.name AS role
3437
3512
  FROM users u
3438
3513
  JOIN roles r ON r.id = u.role_id
3439
3514
  WHERE u.id = ${userId}