@bobbykim/manguito-cms-api 0.7.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
@@ -4,11 +4,13 @@ import {
4
4
  createFieldKeyMapFromProjection,
5
5
  deleteParagraphField,
6
6
  isColumnBacked,
7
+ oneOrMany,
8
+ paragraphItems,
7
9
  persistJunctionField,
8
10
  persistParagraphField,
9
11
  resolveRelationBareIds,
10
12
  resolveRelationField
11
- } from "./chunk-FXPKGPYF.mjs";
13
+ } from "./chunk-IHEIX6P4.mjs";
12
14
 
13
15
  // src/paths.ts
14
16
  function normalizePrefix(prefix) {
@@ -36,7 +38,7 @@ function createVersionedPaths(prefix, version) {
36
38
 
37
39
  // src/app.ts
38
40
  import { Hono } from "hono";
39
- import { sql as sql9 } from "drizzle-orm";
41
+ import { sql as sql11 } from "drizzle-orm";
40
42
 
41
43
  // src/middleware/cors.ts
42
44
  function createCorsMiddleware(corsConfig) {
@@ -130,16 +132,20 @@ var ERROR_STATUS_MAP = {
130
132
  UNSUPPORTED_MIME_TYPE: 415,
131
133
  STORAGE_ERROR: 502,
132
134
  MEDIA_IN_USE: 409,
135
+ ITEM_IN_USE: 409,
133
136
  PRESIGNED_URL_EXPIRED: 410,
134
137
  RATE_LIMITED: 429,
135
138
  INTERNAL_ERROR: 500
136
139
  };
137
140
  var errorHandler = (err, c) => {
138
141
  console.error(err.stack ?? err.message);
139
- const apiErr = err;
140
- const code = apiErr.code ?? "INTERNAL_ERROR";
141
- const status = ERROR_STATUS_MAP[code] ?? 500;
142
- return c.json({ ok: false, error: { code, message: err.message } }, status);
142
+ const raw = err.code;
143
+ const deliberate = raw !== void 0 && raw !== "INTERNAL_ERROR" && raw in ERROR_STATUS_MAP;
144
+ if (!deliberate) {
145
+ return c.json({ ok: false, error: { code: "INTERNAL_ERROR", message: "Internal server error" } }, 500);
146
+ }
147
+ const status = ERROR_STATUS_MAP[raw] ?? 500;
148
+ return c.json({ ok: false, error: { code: raw, message: err.message } }, status);
143
149
  };
144
150
 
145
151
  // src/middleware/auth.ts
@@ -1043,7 +1049,55 @@ function registerPublicMediaRoutes(app, mediaRepo, paths, listRateLimit) {
1043
1049
  }
1044
1050
 
1045
1051
  // src/routes/admin/content.ts
1052
+ import { sql as sql4 } from "drizzle-orm";
1053
+
1054
+ // src/references-in-use.ts
1046
1055
  import { sql as sql2 } from "drizzle-orm";
1056
+ import { relationCardinality } from "@bobbykim/manguito-cms-core";
1057
+ function quoteIdent(name) {
1058
+ if (!/^[a-z][a-z0-9_-]*$/.test(name)) throw new Error(`Unsafe identifier: ${name}`);
1059
+ return `"${name}"`;
1060
+ }
1061
+ function isRequiredSingleReferenceTo(f, targetType) {
1062
+ return f.field_type === "reference" && f.required && relationCardinality(f) === "one" && f.db_column?.foreign_key !== void 0 && f.ui_component.component === "typeahead-select" && f.ui_component.ref === targetType;
1063
+ }
1064
+ async function findRequiredReferrers(db, registry, targetType, id) {
1065
+ const owners = [
1066
+ ...Object.values(registry.content_types),
1067
+ ...Object.values(registry.taxonomy_types),
1068
+ ...Object.values(registry.paragraph_types)
1069
+ ];
1070
+ const out = [];
1071
+ for (const owner of owners) {
1072
+ for (const f of owner.fields) {
1073
+ if (!isRequiredSingleReferenceTo(f, targetType)) continue;
1074
+ const notSelf = owner.name === targetType ? sql2` AND id <> ${id}` : sql2``;
1075
+ const r = await db.execute(
1076
+ sql2`SELECT count(*)::int AS n FROM ${sql2.raw(quoteIdent(owner.db.table_name))} WHERE ${sql2.raw(quoteIdent(f.db_column.column_name))} = ${id}${notSelf}`
1077
+ );
1078
+ const n = r.rows[0].n;
1079
+ if (n > 0) out.push({ type_label: owner.label, field_label: f.label, count: n });
1080
+ }
1081
+ }
1082
+ return out;
1083
+ }
1084
+ function inUseMessage(referrers) {
1085
+ const total = referrers.reduce((sum, r) => sum + r.count, 0);
1086
+ const where = referrers.map((r) => `${r.count} in ${r.type_label} (${r.field_label})`).join(", ");
1087
+ return `This item is still used by ${total} item${total === 1 ? "" : "s"}: ${where}. Remove it from those first.`;
1088
+ }
1089
+ var IN_USE_RACE_MESSAGE = "This item is still in use. Remove it from the items that use it first.";
1090
+ function isForeignKeyViolation(err) {
1091
+ let e = err;
1092
+ for (let depth = 0; depth < 3 && typeof e === "object" && e !== null; depth++) {
1093
+ if (e.code === "23503") return true;
1094
+ e = e.cause;
1095
+ }
1096
+ return false;
1097
+ }
1098
+
1099
+ // src/routes/admin/content.ts
1100
+ import { relationCardinality as relationCardinality3 } from "@bobbykim/manguito-cms-core";
1047
1101
 
1048
1102
  // src/media-references.ts
1049
1103
  function extractMediaId(value) {
@@ -1079,14 +1133,137 @@ async function applyMediaReferenceDelta(delta, mediaRepo) {
1079
1133
  if (netRemove.length > 0) await mediaRepo.decrementReferenceCount(netRemove);
1080
1134
  }
1081
1135
 
1136
+ // src/relation-input.ts
1137
+ import { sql as sql3 } from "drizzle-orm";
1138
+ import { relationCardinality as relationCardinality2 } from "@bobbykim/manguito-cms-core";
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;
1140
+ function isUuid(v) {
1141
+ return typeof v === "string" && UUID_RE.test(v);
1142
+ }
1143
+ function isPlainObject(v) {
1144
+ return typeof v === "object" && v !== null && !Array.isArray(v);
1145
+ }
1146
+ function checkRelationInput(fields, body, registry, path = "") {
1147
+ const errors = [];
1148
+ for (const field of fields) {
1149
+ if (!(field.name in body)) continue;
1150
+ const cardinality = relationCardinality2(field);
1151
+ if (cardinality === null) continue;
1152
+ const value = body[field.name];
1153
+ const name = `${path}${field.name}`;
1154
+ if (field.field_type === "reference") {
1155
+ const ok = cardinality === "one" ? value === null || isUuid(value) : Array.isArray(value) && value.every(isUuid);
1156
+ if (!ok) {
1157
+ errors.push({
1158
+ field: name,
1159
+ message: cardinality === "one" ? `${name} holds one item: send a UUID string or null.` : `${name} holds a list: send an array of UUID strings.`
1160
+ });
1161
+ }
1162
+ continue;
1163
+ }
1164
+ const items = cardinality === "one" ? value === null ? [] : isPlainObject(value) ? [value] : void 0 : Array.isArray(value) && value.every(isPlainObject) ? value : void 0;
1165
+ if (items === void 0) {
1166
+ errors.push({
1167
+ field: name,
1168
+ message: cardinality === "one" ? `${name} holds one item: send an object or null.` : `${name} holds a list: send an array of objects.`
1169
+ });
1170
+ continue;
1171
+ }
1172
+ const comp = field.ui_component;
1173
+ const pType = comp.component === "paragraph-embed" ? registry.paragraph_types[comp.ref] : void 0;
1174
+ if (!pType) continue;
1175
+ items.forEach((item, i) => {
1176
+ const prefix = cardinality === "one" ? `${name}.` : `${name}[${i}].`;
1177
+ errors.push(...checkRelationInput(pType.fields, item, registry, prefix));
1178
+ });
1179
+ }
1180
+ return errors;
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
+ }
1258
+
1082
1259
  // src/routes/admin/content.ts
1083
- function quoteIdent(name) {
1260
+ function quoteIdent3(name) {
1084
1261
  if (!/^[a-z][a-z0-9_-]*$/.test(name)) throw new Error(`Unsafe identifier: ${name}`);
1085
1262
  return `"${name}"`;
1086
1263
  }
1087
1264
  async function loadParagraphRows(db, registry, pType, parentId, fieldName) {
1088
1265
  const result = await db.execute(
1089
- sql2`SELECT * FROM ${sql2.raw(quoteIdent(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`
1090
1267
  );
1091
1268
  const rows = result.rows;
1092
1269
  for (const nf of pType.fields) {
@@ -1096,26 +1273,35 @@ async function loadParagraphRows(db, registry, pType, parentId, fieldName) {
1096
1273
  const nType = registry.paragraph_types[comp.ref];
1097
1274
  if (!nType) continue;
1098
1275
  for (const row of rows) {
1099
- row[nf.name] = await loadParagraphRows(db, registry, nType, row["id"], nf.name);
1276
+ row[nf.name] = oneOrMany(
1277
+ relationCardinality3(nf) ?? "many",
1278
+ await loadParagraphRows(db, registry, nType, row["id"], nf.name)
1279
+ );
1100
1280
  }
1101
1281
  }
1102
1282
  return rows;
1103
1283
  }
1104
1284
  async function lookupBasePathId(db, pathOrName) {
1105
1285
  const r = await db.execute(
1106
- sql2`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`
1107
1287
  );
1108
1288
  return r.rows[0]?.id ?? null;
1109
1289
  }
1110
1290
  var SLUG_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
1111
- function isEmpty(value) {
1112
- if (value === null || value === void 0) return true;
1113
- if (typeof value === "string" && value.trim() === "") return true;
1114
- if (Array.isArray(value) && value.length === 0) return true;
1115
- return false;
1116
- }
1117
1291
  function checkRequiredFields(fields, data) {
1118
- return fields.filter((f) => f.required && isEmpty(data[f.name])).map((f) => ({ field: f.name, message: `${f.label} is required` }));
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;
1119
1305
  }
1120
1306
  function parseListQuery(url, schemaFieldNames, relationFieldNames, columnFor) {
1121
1307
  const searchParams = new URL(url).searchParams;
@@ -1258,11 +1444,11 @@ function registerAdminContentRoutes(app, registry, repos, projectors, mediaRepo,
1258
1444
  if (comp.component !== "paragraph-embed" || !comp.ref) continue;
1259
1445
  const pType = registry.paragraph_types[comp.ref];
1260
1446
  if (!pType) continue;
1261
- row[f.name] = await loadParagraphRows(db, registry, pType, id, f.name);
1447
+ row[f.name] = oneOrMany(relationCardinality3(f) ?? "many", await loadParagraphRows(db, registry, pType, id, f.name));
1262
1448
  } else if (f.db_column.junction) {
1263
1449
  const j = f.db_column.junction;
1264
1450
  const r = await db.execute(
1265
- sql2`SELECT ${sql2.raw(quoteIdent(j.right_column))} FROM ${sql2.raw(quoteIdent(j.table_name))} WHERE ${sql2.raw(quoteIdent(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}`
1266
1452
  );
1267
1453
  row[f.name] = r.rows.map(
1268
1454
  (row2) => row2[j.right_column]
@@ -1338,6 +1524,8 @@ function registerAdminContentRoutes(app, registry, repos, projectors, mediaRepo,
1338
1524
  }
1339
1525
  );
1340
1526
  const writeNewItem = async (c, body) => {
1527
+ const inputError = await relationInputError(c, contentType.fields, body, registry, db);
1528
+ if (inputError) return inputError;
1341
1529
  const storageBody = fieldKeys.toStorage(body);
1342
1530
  const fieldErrors = checkRequiredFields(requiredFields, body);
1343
1531
  if (fieldErrors.length > 0) {
@@ -1401,7 +1589,7 @@ function registerAdminContentRoutes(app, registry, repos, projectors, mediaRepo,
1401
1589
  if (comp.component !== "paragraph-embed" || !comp.ref) continue;
1402
1590
  const pType = registry.paragraph_types[comp.ref];
1403
1591
  if (!pType) continue;
1404
- const items = Array.isArray(body[f.name]) ? body[f.name] : [];
1592
+ const items = paragraphItems(f, body[f.name]);
1405
1593
  mediaDeltas.push(await persistParagraphField(db, itemId, contentType.db.table_name, f.name, pType, items, registry));
1406
1594
  }
1407
1595
  for (const f of junctionFields) {
@@ -1472,6 +1660,8 @@ function registerAdminContentRoutes(app, registry, repos, projectors, mediaRepo,
1472
1660
  }
1473
1661
  );
1474
1662
  const writeExistingItem = async (c, id, existing, body) => {
1663
+ const inputError = await relationInputError(c, contentType.fields, body, registry, db);
1664
+ if (inputError) return inputError;
1475
1665
  const storageBody = fieldKeys.toStorage(body);
1476
1666
  if (body["published"] === true) {
1477
1667
  const publishDeny = await requirePermission("content:edit")(c, async () => {
@@ -1518,14 +1708,16 @@ function registerAdminContentRoutes(app, registry, repos, projectors, mediaRepo,
1518
1708
  ];
1519
1709
  if (db) {
1520
1710
  for (const f of patchParagraphFields) {
1711
+ if (!(f.name in body)) continue;
1521
1712
  const comp = f.ui_component;
1522
1713
  if (comp.component !== "paragraph-embed" || !comp.ref) continue;
1523
1714
  const pType = registry.paragraph_types[comp.ref];
1524
1715
  if (!pType) continue;
1525
- const items = Array.isArray(body[f.name]) ? body[f.name] : [];
1716
+ const items = paragraphItems(f, body[f.name]);
1526
1717
  mediaDeltas.push(await persistParagraphField(db, id, contentType.db.table_name, f.name, pType, items, registry));
1527
1718
  }
1528
1719
  for (const f of patchJunctionFields) {
1720
+ if (!(f.name in body)) continue;
1529
1721
  const junction = f.db_column.junction;
1530
1722
  const relatedIds = Array.isArray(body[f.name]) ? body[f.name].filter((v) => typeof v === "string") : [];
1531
1723
  await persistJunctionField(db, id, junction, relatedIds);
@@ -1565,6 +1757,18 @@ function registerAdminContentRoutes(app, registry, repos, projectors, mediaRepo,
1565
1757
  404
1566
1758
  );
1567
1759
  }
1760
+ const referrers = db ? await findRequiredReferrers(db, registry, typeName, id) : [];
1761
+ if (referrers.length > 0) {
1762
+ return c.json({ ok: false, error: { code: "ITEM_IN_USE", message: inUseMessage(referrers) } }, 409);
1763
+ }
1764
+ try {
1765
+ await repo.delete(id);
1766
+ } catch (err) {
1767
+ if (isForeignKeyViolation(err)) {
1768
+ return c.json({ ok: false, error: { code: "ITEM_IN_USE", message: IN_USE_RACE_MESSAGE } }, 409);
1769
+ }
1770
+ throw err;
1771
+ }
1568
1772
  const mediaDeltas = [
1569
1773
  topLevelMediaDelta(mediaFields, item, null)
1570
1774
  ];
@@ -1578,7 +1782,6 @@ function registerAdminContentRoutes(app, registry, repos, projectors, mediaRepo,
1578
1782
  }
1579
1783
  }
1580
1784
  await applyMediaReferenceDelta(mergeMediaDeltas(...mediaDeltas), mediaRepo);
1581
- await repo.delete(id);
1582
1785
  return c.json({ ok: true });
1583
1786
  }
1584
1787
  );
@@ -1656,6 +1859,8 @@ function registerAdminContentRoutes(app, registry, repos, projectors, mediaRepo,
1656
1859
  requirePermission("content:create"),
1657
1860
  async (c) => {
1658
1861
  const body = await c.req.json();
1862
+ const inputError = await relationInputError(c, taxonomyType.fields, body, registry, db);
1863
+ if (inputError) return inputError;
1659
1864
  const storageBody = fieldKeys.toStorage(body);
1660
1865
  const fieldErrors = checkRequiredFields(requiredFields, body);
1661
1866
  if (fieldErrors.length > 0) {
@@ -1695,7 +1900,7 @@ function registerAdminContentRoutes(app, registry, repos, projectors, mediaRepo,
1695
1900
  if (comp.component !== "paragraph-embed" || !comp.ref) continue;
1696
1901
  const pType = registry.paragraph_types[comp.ref];
1697
1902
  if (!pType) continue;
1698
- const items = Array.isArray(body[f.name]) ? body[f.name] : [];
1903
+ const items = paragraphItems(f, body[f.name]);
1699
1904
  mediaDeltas.push(await persistParagraphField(db, taxItemId, taxonomyType.db.table_name, f.name, pType, items, registry));
1700
1905
  }
1701
1906
  }
@@ -1718,6 +1923,8 @@ function registerAdminContentRoutes(app, registry, repos, projectors, mediaRepo,
1718
1923
  404
1719
1924
  );
1720
1925
  }
1926
+ const inputError = await relationInputError(c, taxonomyType.fields, body, registry, db);
1927
+ if (inputError) return inputError;
1721
1928
  if (body["published"] === true) {
1722
1929
  const publishDeny = await requirePermission("content:edit")(c, async () => {
1723
1930
  });
@@ -1759,11 +1966,12 @@ function registerAdminContentRoutes(app, registry, repos, projectors, mediaRepo,
1759
1966
  ];
1760
1967
  if (db) {
1761
1968
  for (const f of taxPatchParagraphFields) {
1969
+ if (!(f.name in body)) continue;
1762
1970
  const comp = f.ui_component;
1763
1971
  if (comp.component !== "paragraph-embed" || !comp.ref) continue;
1764
1972
  const pType = registry.paragraph_types[comp.ref];
1765
1973
  if (!pType) continue;
1766
- const items = Array.isArray(body[f.name]) ? body[f.name] : [];
1974
+ const items = paragraphItems(f, body[f.name]);
1767
1975
  mediaDeltas.push(await persistParagraphField(db, id, taxonomyType.db.table_name, f.name, pType, items, registry));
1768
1976
  }
1769
1977
  }
@@ -1784,6 +1992,18 @@ function registerAdminContentRoutes(app, registry, repos, projectors, mediaRepo,
1784
1992
  404
1785
1993
  );
1786
1994
  }
1995
+ const referrers = db ? await findRequiredReferrers(db, registry, typeName, id) : [];
1996
+ if (referrers.length > 0) {
1997
+ return c.json({ ok: false, error: { code: "ITEM_IN_USE", message: inUseMessage(referrers) } }, 409);
1998
+ }
1999
+ try {
2000
+ await repo.delete(id);
2001
+ } catch (err) {
2002
+ if (isForeignKeyViolation(err)) {
2003
+ return c.json({ ok: false, error: { code: "ITEM_IN_USE", message: IN_USE_RACE_MESSAGE } }, 409);
2004
+ }
2005
+ throw err;
2006
+ }
1787
2007
  const mediaDeltas = [
1788
2008
  topLevelMediaDelta(mediaFields, item, null)
1789
2009
  ];
@@ -1797,7 +2017,6 @@ function registerAdminContentRoutes(app, registry, repos, projectors, mediaRepo,
1797
2017
  }
1798
2018
  }
1799
2019
  await applyMediaReferenceDelta(mergeMediaDeltas(...mediaDeltas), mediaRepo);
1800
- await repo.delete(id);
1801
2020
  return c.json({ ok: true });
1802
2021
  }
1803
2022
  );
@@ -2259,7 +2478,7 @@ function registerAdminMediaRoutes(app, mediaRepo, storage, requirePermission, ma
2259
2478
 
2260
2479
  // src/routes/admin/auth.ts
2261
2480
  import { getCookie as getCookie2 } from "hono/cookie";
2262
- import { sql as sql3 } from "drizzle-orm";
2481
+ import { sql as sql5 } from "drizzle-orm";
2263
2482
 
2264
2483
  // src/auth/password.ts
2265
2484
  import { hashPassword, verifyPassword } from "@bobbykim/manguito-cms-core";
@@ -2325,7 +2544,7 @@ function registerAuthRoutes(app, db) {
2325
2544
  );
2326
2545
  }
2327
2546
  const userResult = await db.execute(
2328
- sql3`SELECT id, email, password_hash, role_id, token_version
2547
+ sql5`SELECT id, email, password_hash, role_id, token_version
2329
2548
  FROM users WHERE email = ${email} LIMIT 1`
2330
2549
  );
2331
2550
  const user = userResult.rows[0];
@@ -2338,7 +2557,7 @@ function registerAuthRoutes(app, db) {
2338
2557
  return c.json(invalidCredentials, 401);
2339
2558
  }
2340
2559
  const roleResult = await db.execute(
2341
- sql3`SELECT r.name AS name, u.must_change_password
2560
+ sql5`SELECT r.name AS name, u.must_change_password
2342
2561
  FROM roles r
2343
2562
  JOIN users u ON u.role_id = r.id
2344
2563
  WHERE r.id = ${user.role_id} AND u.id = ${user.id}
@@ -2398,7 +2617,7 @@ function registerAuthRoutes(app, db) {
2398
2617
  );
2399
2618
  }
2400
2619
  const rowResult = await db.execute(
2401
- sql3`SELECT u.token_version, r.name AS role
2620
+ sql5`SELECT u.token_version, r.name AS role
2402
2621
  FROM users u
2403
2622
  JOIN roles r ON r.id = u.role_id
2404
2623
  WHERE u.id = ${payload.user_id}
@@ -2422,7 +2641,7 @@ function registerAuthRoutes(app, db) {
2422
2641
  app.post("/admin/api/auth/logout", authMiddleware, async (c) => {
2423
2642
  const user = c.get("user");
2424
2643
  await db.execute(
2425
- sql3`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}`
2426
2645
  );
2427
2646
  clearAuthCookie(c, "auth_token");
2428
2647
  clearAuthCookie(c, "refresh_token", "/admin/api/auth");
@@ -2431,7 +2650,7 @@ function registerAuthRoutes(app, db) {
2431
2650
  }
2432
2651
 
2433
2652
  // src/routes/admin/users.ts
2434
- import { sql as sql4 } from "drizzle-orm";
2653
+ import { sql as sql6 } from "drizzle-orm";
2435
2654
  import { randomBytes } from "crypto";
2436
2655
  function getActingUser(c) {
2437
2656
  return c.get("user");
@@ -2466,7 +2685,7 @@ function registerUserRoutes(app, db, requirePermission, requireHierarchy) {
2466
2685
  );
2467
2686
  }
2468
2687
  const hashResult = await db.execute(
2469
- sql4`SELECT password_hash FROM users WHERE id = ${actingUser.id} LIMIT 1`
2688
+ sql6`SELECT password_hash FROM users WHERE id = ${actingUser.id} LIMIT 1`
2470
2689
  );
2471
2690
  const hashRow = hashResult.rows[0];
2472
2691
  if (!hashRow || !await verifyPassword(currentPassword, hashRow.password_hash)) {
@@ -2477,7 +2696,7 @@ function registerUserRoutes(app, db, requirePermission, requireHierarchy) {
2477
2696
  }
2478
2697
  const newHash = await hashPassword(newPassword);
2479
2698
  await db.execute(
2480
- sql4`UPDATE users
2699
+ sql6`UPDATE users
2481
2700
  SET password_hash = ${newHash},
2482
2701
  must_change_password = false,
2483
2702
  token_version = token_version + 1,
@@ -2488,7 +2707,7 @@ function registerUserRoutes(app, db, requirePermission, requireHierarchy) {
2488
2707
  });
2489
2708
  app.get("/admin/api/users", requirePermission("users:read"), async (c) => {
2490
2709
  const result = await db.execute(
2491
- sql4`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,
2492
2711
  u.created_at, u.updated_at
2493
2712
  FROM users u
2494
2713
  JOIN roles r ON r.id = u.role_id
@@ -2499,7 +2718,7 @@ function registerUserRoutes(app, db, requirePermission, requireHierarchy) {
2499
2718
  app.get("/admin/api/users/:id", requirePermission("users:read"), async (c) => {
2500
2719
  const id = c.req.param("id");
2501
2720
  const result = await db.execute(
2502
- sql4`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,
2503
2722
  u.created_at, u.updated_at
2504
2723
  FROM users u
2505
2724
  JOIN roles r ON r.id = u.role_id
@@ -2544,7 +2763,7 @@ function registerUserRoutes(app, db, requirePermission, requireHierarchy) {
2544
2763
  );
2545
2764
  }
2546
2765
  const roleResult = await db.execute(
2547
- sql4`SELECT id FROM roles WHERE name = ${roleName} LIMIT 1`
2766
+ sql6`SELECT id FROM roles WHERE name = ${roleName} LIMIT 1`
2548
2767
  );
2549
2768
  const roleRow = roleResult.rows[0];
2550
2769
  if (!roleRow) {
@@ -2559,7 +2778,7 @@ function registerUserRoutes(app, db, requirePermission, requireHierarchy) {
2559
2778
  const tempPassword = generateTempPassword();
2560
2779
  const passwordHash = await hashPassword(tempPassword);
2561
2780
  const insertResult = await db.execute(
2562
- sql4`INSERT INTO users
2781
+ sql6`INSERT INTO users
2563
2782
  (id, email, password_hash, role_id, must_change_password, token_version,
2564
2783
  created_at, updated_at)
2565
2784
  VALUES
@@ -2614,7 +2833,7 @@ function registerUserRoutes(app, db, requirePermission, requireHierarchy) {
2614
2833
  );
2615
2834
  }
2616
2835
  const existingResult = await db.execute(
2617
- sql4`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`
2618
2837
  );
2619
2838
  const existing = existingResult.rows[0];
2620
2839
  if (!existing) {
@@ -2628,7 +2847,7 @@ function registerUserRoutes(app, db, requirePermission, requireHierarchy) {
2628
2847
  let changeRole = false;
2629
2848
  if (typeof body.role === "string" && body.role) {
2630
2849
  const roleResult = await db.execute(
2631
- sql4`SELECT id FROM roles WHERE name = ${body.role} LIMIT 1`
2850
+ sql6`SELECT id FROM roles WHERE name = ${body.role} LIMIT 1`
2632
2851
  );
2633
2852
  const roleRow = roleResult.rows[0];
2634
2853
  if (!roleRow) {
@@ -2648,7 +2867,7 @@ function registerUserRoutes(app, db, requirePermission, requireHierarchy) {
2648
2867
  }
2649
2868
  if (changeRole) {
2650
2869
  await db.execute(
2651
- sql4`UPDATE users
2870
+ sql6`UPDATE users
2652
2871
  SET email = ${newEmail},
2653
2872
  role_id = ${newRoleId},
2654
2873
  token_version = token_version + 1,
@@ -2657,7 +2876,7 @@ function registerUserRoutes(app, db, requirePermission, requireHierarchy) {
2657
2876
  );
2658
2877
  } else {
2659
2878
  await db.execute(
2660
- sql4`UPDATE users
2879
+ sql6`UPDATE users
2661
2880
  SET email = ${newEmail},
2662
2881
  role_id = ${newRoleId},
2663
2882
  updated_at = NOW()
@@ -2665,7 +2884,7 @@ function registerUserRoutes(app, db, requirePermission, requireHierarchy) {
2665
2884
  );
2666
2885
  }
2667
2886
  const updatedResult = await db.execute(
2668
- sql4`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,
2669
2888
  u.created_at, u.updated_at
2670
2889
  FROM users u
2671
2890
  JOIN roles r ON r.id = u.role_id
@@ -2702,7 +2921,7 @@ function registerUserRoutes(app, db, requirePermission, requireHierarchy) {
2702
2921
  );
2703
2922
  }
2704
2923
  const result = await db.execute(
2705
- sql4`DELETE FROM users WHERE id = ${id} RETURNING id`
2924
+ sql6`DELETE FROM users WHERE id = ${id} RETURNING id`
2706
2925
  );
2707
2926
  if (result.rows.length === 0) {
2708
2927
  return c.json(
@@ -2733,7 +2952,7 @@ function registerUserRoutes(app, db, requirePermission, requireHierarchy) {
2733
2952
  );
2734
2953
  }
2735
2954
  const existingResult = await db.execute(
2736
- sql4`SELECT id FROM users WHERE id = ${id} LIMIT 1`
2955
+ sql6`SELECT id FROM users WHERE id = ${id} LIMIT 1`
2737
2956
  );
2738
2957
  const existing = existingResult.rows[0];
2739
2958
  if (!existing) {
@@ -2745,7 +2964,7 @@ function registerUserRoutes(app, db, requirePermission, requireHierarchy) {
2745
2964
  const tempPassword = generateTempPassword();
2746
2965
  const passwordHash = await hashPassword(tempPassword);
2747
2966
  await db.execute(
2748
- sql4`UPDATE users
2967
+ sql6`UPDATE users
2749
2968
  SET password_hash = ${passwordHash},
2750
2969
  must_change_password = true,
2751
2970
  token_version = token_version + 1,
@@ -2758,10 +2977,10 @@ function registerUserRoutes(app, db, requirePermission, requireHierarchy) {
2758
2977
  }
2759
2978
 
2760
2979
  // src/routes/admin/config.ts
2761
- import { sql as sql5 } from "drizzle-orm";
2980
+ import { sql as sql7 } from "drizzle-orm";
2762
2981
  var API_VERSION = (() => {
2763
2982
  try {
2764
- return "0.7.0";
2983
+ return "0.8.1";
2765
2984
  } catch {
2766
2985
  return "0.0.0";
2767
2986
  }
@@ -2780,7 +2999,7 @@ function registerConfigRoute(app, config, registry, db) {
2780
2999
  permissions: r.permissions
2781
3000
  }));
2782
3001
  const userResult = await db.execute(
2783
- sql5`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`
2784
3003
  );
2785
3004
  const userRow = userResult.rows[0];
2786
3005
  return c.json({
@@ -2806,7 +3025,7 @@ function registerConfigRoute(app, config, registry, db) {
2806
3025
  }
2807
3026
 
2808
3027
  // src/routes/admin/schema.ts
2809
- import { sql as sql6 } from "drizzle-orm";
3028
+ import { sql as sql8 } from "drizzle-orm";
2810
3029
  function liveFields(fields) {
2811
3030
  return fields.filter((f) => f.removed !== true);
2812
3031
  }
@@ -2846,7 +3065,7 @@ function registerSchemaRoute(app, registry, db) {
2846
3065
  const data = await Promise.all(
2847
3066
  Object.values(registry.content_types).map(async (ct) => {
2848
3067
  const result = await db.execute(
2849
- sql6`SELECT COUNT(*)::int AS count FROM ${sql6.raw(`"${ct.db.table_name}"`)}`
3068
+ sql8`SELECT COUNT(*)::int AS count FROM ${sql8.raw(`"${ct.db.table_name}"`)}`
2850
3069
  );
2851
3070
  const count = Number(result.rows[0].count);
2852
3071
  return { name: ct.name, label: ct.label, only_one: ct.only_one, count };
@@ -2858,7 +3077,7 @@ function registerSchemaRoute(app, registry, db) {
2858
3077
  const data = await Promise.all(
2859
3078
  Object.values(registry.taxonomy_types).map(async (tt) => {
2860
3079
  const result = await db.execute(
2861
- sql6`SELECT COUNT(*)::int AS count FROM ${sql6.raw(`"${tt.db.table_name}"`)}`
3080
+ sql8`SELECT COUNT(*)::int AS count FROM ${sql8.raw(`"${tt.db.table_name}"`)}`
2862
3081
  );
2863
3082
  const count = Number(result.rows[0].count);
2864
3083
  return { name: tt.name, label: tt.label, count };
@@ -2870,14 +3089,14 @@ function registerSchemaRoute(app, registry, db) {
2870
3089
 
2871
3090
  // src/repositories/content.ts
2872
3091
  import crypto from "crypto";
2873
- import { sql as sql7 } from "drizzle-orm";
3092
+ import { sql as sql9 } from "drizzle-orm";
2874
3093
  var SORTABLE_FIELDS2 = /* @__PURE__ */ new Set(["title", "created_at", "updated_at"]);
2875
3094
  function codeError(code, message) {
2876
3095
  const err = new Error(message);
2877
3096
  err.code = code;
2878
3097
  return err;
2879
3098
  }
2880
- function quoteIdent2(name) {
3099
+ function quoteIdent4(name) {
2881
3100
  if (!/^[a-z][a-z0-9_-]*$/.test(name)) {
2882
3101
  throw new Error(`Unsafe SQL identifier: ${name}`);
2883
3102
  }
@@ -2892,43 +3111,43 @@ function quoteField(name) {
2892
3111
  function buildConditions(published_only, filters, search) {
2893
3112
  const conds = [];
2894
3113
  if (published_only) {
2895
- conds.push(sql7`published = true`);
3114
+ conds.push(sql9`published = true`);
2896
3115
  }
2897
3116
  if (search && search.term.trim() !== "" && search.columns.length > 0) {
2898
3117
  const pattern = `%${search.term.trim()}%`;
2899
- const orConds = search.columns.map((col) => sql7`${sql7.raw(quoteField(col))} ILIKE ${pattern}`);
2900
- conds.push(sql7`(${sql7.join(orConds, sql7` OR `)})`);
3118
+ const orConds = search.columns.map((col) => sql9`${sql9.raw(quoteField(col))} ILIKE ${pattern}`);
3119
+ conds.push(sql9`(${sql9.join(orConds, sql9` OR `)})`);
2901
3120
  }
2902
3121
  if (filters) {
2903
3122
  for (const [field, value] of Object.entries(filters)) {
2904
- const col = sql7.raw(quoteField(field));
3123
+ const col = sql9.raw(quoteField(field));
2905
3124
  if (Array.isArray(value)) {
2906
3125
  if (value.length === 0) continue;
2907
- const inList = sql7.join(
2908
- value.map((v) => sql7`${v}`),
2909
- sql7`, `
3126
+ const inList = sql9.join(
3127
+ value.map((v) => sql9`${v}`),
3128
+ sql9`, `
2910
3129
  );
2911
- conds.push(sql7`${col} IN (${inList})`);
3130
+ conds.push(sql9`${col} IN (${inList})`);
2912
3131
  } else if (typeof value === "object" && value !== null) {
2913
3132
  const op = value;
2914
- if (op.gt !== void 0) conds.push(sql7`${col} > ${op.gt}`);
2915
- if (op.gte !== void 0) conds.push(sql7`${col} >= ${op.gte}`);
2916
- if (op.lt !== void 0) conds.push(sql7`${col} < ${op.lt}`);
2917
- if (op.lte !== void 0) conds.push(sql7`${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}`);
2918
3137
  } else {
2919
- conds.push(sql7`${col} = ${value}`);
3138
+ conds.push(sql9`${col} = ${value}`);
2920
3139
  }
2921
3140
  }
2922
3141
  }
2923
3142
  return conds;
2924
3143
  }
2925
3144
  function whereFragment(conds) {
2926
- return conds.length > 0 ? sql7` WHERE ${sql7.join(conds, sql7` AND `)}` : sql7``;
3145
+ return conds.length > 0 ? sql9` WHERE ${sql9.join(conds, sql9` AND `)}` : sql9``;
2927
3146
  }
2928
3147
  function createDrizzleContentRepository(db, tableName, options = {}) {
2929
3148
  const { relations = {}, publishedRelations = false, sortableColumns } = options;
2930
3149
  function tableRaw() {
2931
- return sql7.raw(quoteIdent2(tableName));
3150
+ return sql9.raw(quoteIdent4(tableName));
2932
3151
  }
2933
3152
  async function resolveRows(rows, include, cache) {
2934
3153
  if (rows.length === 0) return rows;
@@ -2971,14 +3190,14 @@ function createDrizzleContentRepository(db, tableName, options = {}) {
2971
3190
  const offset = (page - 1) * per_page;
2972
3191
  const conds = buildConditions(published_only, filters, search);
2973
3192
  const where = whereFragment(conds);
2974
- const sortCol = sql7.raw(quoteIdent2(sort_by));
2975
- const sortDir = sql7.raw(sort_order === "desc" ? "DESC" : "ASC");
3193
+ const sortCol = sql9.raw(quoteIdent4(sort_by));
3194
+ const sortDir = sql9.raw(sort_order === "desc" ? "DESC" : "ASC");
2976
3195
  const countResult = await db.execute(
2977
- sql7`SELECT COUNT(*) AS total FROM ${tableRaw()}${where}`
3196
+ sql9`SELECT COUNT(*) AS total FROM ${tableRaw()}${where}`
2978
3197
  );
2979
3198
  const total = Number(countResult.rows[0]?.["total"] ?? 0);
2980
3199
  const dataResult = await db.execute(
2981
- sql7`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}`
2982
3201
  );
2983
3202
  const rows = dataResult.rows;
2984
3203
  const cache = /* @__PURE__ */ new Map();
@@ -2999,7 +3218,7 @@ function createDrizzleContentRepository(db, tableName, options = {}) {
2999
3218
  },
3000
3219
  async findOne(id, include = []) {
3001
3220
  const result = await db.execute(
3002
- sql7`SELECT * FROM ${tableRaw()} WHERE id = ${id} LIMIT 1`
3221
+ sql9`SELECT * FROM ${tableRaw()} WHERE id = ${id} LIMIT 1`
3003
3222
  );
3004
3223
  if (result.rows.length === 0) return null;
3005
3224
  const rows = result.rows;
@@ -3009,7 +3228,7 @@ function createDrizzleContentRepository(db, tableName, options = {}) {
3009
3228
  },
3010
3229
  async findBySlug(slug, include = []) {
3011
3230
  const result = await db.execute(
3012
- sql7`SELECT * FROM ${tableRaw()} WHERE slug = ${slug} LIMIT 1`
3231
+ sql9`SELECT * FROM ${tableRaw()} WHERE slug = ${slug} LIMIT 1`
3013
3232
  );
3014
3233
  if (result.rows.length === 0) return null;
3015
3234
  const rows = result.rows;
@@ -3025,10 +3244,10 @@ function createDrizzleContentRepository(db, tableName, options = {}) {
3025
3244
  updated_at: /* @__PURE__ */ new Date()
3026
3245
  };
3027
3246
  const entries = Object.entries(record).filter(([, v]) => v !== void 0);
3028
- const colsSql = sql7.join(entries.map(([k]) => sql7.raw(quoteIdent2(k))), sql7`, `);
3029
- const valsSql = sql7.join(entries.map(([, v]) => sql7`${v}`), sql7`, `);
3247
+ const colsSql = sql9.join(entries.map(([k]) => sql9.raw(quoteIdent4(k))), sql9`, `);
3248
+ const valsSql = sql9.join(entries.map(([, v]) => sql9`${v}`), sql9`, `);
3030
3249
  const result = await db.execute(
3031
- sql7`INSERT INTO ${tableRaw()} (${colsSql}) VALUES (${valsSql}) RETURNING *`
3250
+ sql9`INSERT INTO ${tableRaw()} (${colsSql}) VALUES (${valsSql}) RETURNING *`
3032
3251
  );
3033
3252
  return result.rows[0];
3034
3253
  },
@@ -3041,25 +3260,25 @@ function createDrizzleContentRepository(db, tableName, options = {}) {
3041
3260
  if (entries.length === 0) {
3042
3261
  return this.findOne(id);
3043
3262
  }
3044
- const setClauses = sql7.join(
3045
- entries.map(([k, v]) => sql7`${sql7.raw(quoteIdent2(k))} = ${v}`),
3046
- sql7`, `
3263
+ const setClauses = sql9.join(
3264
+ entries.map(([k, v]) => sql9`${sql9.raw(quoteIdent4(k))} = ${v}`),
3265
+ sql9`, `
3047
3266
  );
3048
3267
  const result = await db.execute(
3049
- sql7`UPDATE ${tableRaw()} SET ${setClauses} WHERE id = ${id} RETURNING *`
3268
+ sql9`UPDATE ${tableRaw()} SET ${setClauses} WHERE id = ${id} RETURNING *`
3050
3269
  );
3051
3270
  if (result.rows.length === 0) return null;
3052
3271
  return result.rows[0];
3053
3272
  },
3054
3273
  async delete(id) {
3055
- await db.execute(sql7`DELETE FROM ${tableRaw()} WHERE id = ${id}`);
3274
+ await db.execute(sql9`DELETE FROM ${tableRaw()} WHERE id = ${id}`);
3056
3275
  },
3057
3276
  async findAll(opts = {}) {
3058
3277
  const { include = [], published_only } = opts;
3059
3278
  const conds = buildConditions(published_only, void 0, void 0);
3060
3279
  const where = whereFragment(conds);
3061
3280
  const result = await db.execute(
3062
- sql7`SELECT * FROM ${tableRaw()}${where} ORDER BY created_at ASC`
3281
+ sql9`SELECT * FROM ${tableRaw()}${where} ORDER BY created_at ASC`
3063
3282
  );
3064
3283
  const rows = result.rows;
3065
3284
  const cache = /* @__PURE__ */ new Map();
@@ -3071,23 +3290,23 @@ function createDrizzleContentRepository(db, tableName, options = {}) {
3071
3290
 
3072
3291
  // src/repositories/media.ts
3073
3292
  import crypto2 from "crypto";
3074
- import { sql as sql8 } from "drizzle-orm";
3293
+ import { sql as sql10 } from "drizzle-orm";
3075
3294
  function createMediaRepository(db) {
3076
- const TABLE = sql8.raw('"media"');
3295
+ const TABLE = sql10.raw('"media"');
3077
3296
  return {
3078
3297
  async findMany(opts = {}) {
3079
3298
  const { page = 1, per_page = 10, type, orphaned } = opts;
3080
3299
  const offset = (page - 1) * per_page;
3081
3300
  const conds = [];
3082
- if (type) conds.push(sql8`type = ${type}`);
3083
- if (orphaned) conds.push(sql8`reference_count = 0`);
3084
- const where = conds.length > 0 ? sql8` WHERE ${sql8.join(conds, sql8` AND `)}` : sql8``;
3085
- const countResult = await db.execute(sql8`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}`);
3086
3305
  const total = Number(
3087
3306
  countResult.rows[0]?.["total"] ?? 0
3088
3307
  );
3089
3308
  const dataResult = await db.execute(
3090
- sql8`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}`
3091
3310
  );
3092
3311
  const total_pages = Math.ceil(total / per_page);
3093
3312
  return {
@@ -3105,7 +3324,7 @@ function createMediaRepository(db) {
3105
3324
  },
3106
3325
  async findOne(id) {
3107
3326
  const result = await db.execute(
3108
- sql8`SELECT * FROM ${TABLE} WHERE id = ${id} LIMIT 1`
3327
+ sql10`SELECT * FROM ${TABLE} WHERE id = ${id} LIMIT 1`
3109
3328
  );
3110
3329
  if (result.rows.length === 0) return null;
3111
3330
  return result.rows[0];
@@ -3119,10 +3338,10 @@ function createMediaRepository(db) {
3119
3338
  updated_at: /* @__PURE__ */ new Date()
3120
3339
  };
3121
3340
  const entries = Object.entries(record).filter(([, v]) => v !== void 0);
3122
- const colsSql = sql8.join(entries.map(([k]) => sql8.raw(`"${k}"`)), sql8`, `);
3123
- const valsSql = sql8.join(entries.map(([, v]) => sql8`${v}`), sql8`, `);
3341
+ const colsSql = sql10.join(entries.map(([k]) => sql10.raw(`"${k}"`)), sql10`, `);
3342
+ const valsSql = sql10.join(entries.map(([, v]) => sql10`${v}`), sql10`, `);
3124
3343
  const result = await db.execute(
3125
- sql8`INSERT INTO ${TABLE} (${colsSql}) VALUES (${valsSql}) RETURNING *`
3344
+ sql10`INSERT INTO ${TABLE} (${colsSql}) VALUES (${valsSql}) RETURNING *`
3126
3345
  );
3127
3346
  return result.rows[0];
3128
3347
  },
@@ -3132,31 +3351,31 @@ function createMediaRepository(db) {
3132
3351
  ([k, v]) => k !== "id" && v !== void 0
3133
3352
  );
3134
3353
  if (entries.length === 0) return this.findOne(id);
3135
- const setClauses = sql8.join(
3136
- entries.map(([k, v]) => sql8`${sql8.raw(`"${k}"`)} = ${v}`),
3137
- sql8`, `
3354
+ const setClauses = sql10.join(
3355
+ entries.map(([k, v]) => sql10`${sql10.raw(`"${k}"`)} = ${v}`),
3356
+ sql10`, `
3138
3357
  );
3139
3358
  const result = await db.execute(
3140
- sql8`UPDATE ${TABLE} SET ${setClauses} WHERE id = ${id} RETURNING *`
3359
+ sql10`UPDATE ${TABLE} SET ${setClauses} WHERE id = ${id} RETURNING *`
3141
3360
  );
3142
3361
  if (result.rows.length === 0) return null;
3143
3362
  return result.rows[0];
3144
3363
  },
3145
3364
  async delete(id) {
3146
- await db.execute(sql8`DELETE FROM ${TABLE} WHERE id = ${id}`);
3365
+ await db.execute(sql10`DELETE FROM ${TABLE} WHERE id = ${id}`);
3147
3366
  },
3148
3367
  async incrementReferenceCount(ids) {
3149
3368
  if (ids.length === 0) return;
3150
- const inList = sql8.join(ids.map((id) => sql8`${id}`), sql8`, `);
3369
+ const inList = sql10.join(ids.map((id) => sql10`${id}`), sql10`, `);
3151
3370
  await db.execute(
3152
- sql8`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})`
3153
3372
  );
3154
3373
  },
3155
3374
  async decrementReferenceCount(ids) {
3156
3375
  if (ids.length === 0) return;
3157
- const inList = sql8.join(ids.map((id) => sql8`${id}`), sql8`, `);
3376
+ const inList = sql10.join(ids.map((id) => sql10`${id}`), sql10`, `);
3158
3377
  await db.execute(
3159
- sql8`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})`
3160
3379
  );
3161
3380
  }
3162
3381
  };
@@ -3289,7 +3508,7 @@ function createCmsApp(options) {
3289
3508
  const requirePermission = createPermissionMiddleware(rolesRegistry);
3290
3509
  const getUserRole = async (userId) => {
3291
3510
  const result = await db.execute(
3292
- sql9`SELECT r.name AS role
3511
+ sql11`SELECT r.name AS role
3293
3512
  FROM users u
3294
3513
  JOIN roles r ON r.id = u.role_id
3295
3514
  WHERE u.id = ${userId}
@@ -3459,7 +3678,7 @@ function createCmsApp(options) {
3459
3678
  handler: null,
3460
3679
  error: null
3461
3680
  }));
3462
- const ready = import("./handler-GED3I7Z2.mjs").then(({ createGraphQLHandler }) => {
3681
+ const ready = import("./handler-VEOWJRAL.mjs").then(({ createGraphQLHandler }) => {
3463
3682
  gqlPasses.forEach((pass, i) => {
3464
3683
  const mount = mounts[i];
3465
3684
  try {