@bobbykim/manguito-cms-api 0.9.2 → 0.9.4

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
@@ -1,17 +1,25 @@
1
1
  import {
2
2
  buildRelationsMap,
3
+ checkRelationInput,
4
+ checkRequiredInParagraphItems,
3
5
  createFieldKeyMap,
4
6
  createFieldKeyMapFromProjection,
5
7
  deleteParagraphField,
8
+ findMissingReferences,
6
9
  isColumnBacked,
10
+ isMissing,
11
+ isPlainObject,
7
12
  junctionOrderBy,
8
13
  oneOrMany,
14
+ ownParagraphRowIds,
9
15
  paragraphItems,
16
+ paragraphItemsIn,
17
+ paragraphTypeOf,
10
18
  persistJunctionField,
11
19
  persistParagraphField,
12
20
  resolveRelationBareIds,
13
21
  resolveRelationField
14
- } from "./chunk-GUZZQZSN.mjs";
22
+ } from "./chunk-EFTMAG7N.mjs";
15
23
 
16
24
  // src/paths.ts
17
25
  function normalizePrefix(prefix) {
@@ -39,7 +47,7 @@ function createVersionedPaths(prefix, version) {
39
47
 
40
48
  // src/app.ts
41
49
  import { Hono } from "hono";
42
- import { sql as sql11 } from "drizzle-orm";
50
+ import { sql as sql10 } from "drizzle-orm";
43
51
 
44
52
  // src/middleware/cors.ts
45
53
  function createCorsMiddleware(corsConfig) {
@@ -141,7 +149,7 @@ var ERROR_STATUS_MAP = {
141
149
  var errorHandler = (err, c) => {
142
150
  console.error(err.stack ?? err.message);
143
151
  const raw = err.code;
144
- const deliberate = raw !== void 0 && raw !== "INTERNAL_ERROR" && raw in ERROR_STATUS_MAP;
152
+ const deliberate = raw !== void 0 && raw !== "INTERNAL_ERROR" && Object.hasOwn(ERROR_STATUS_MAP, raw);
145
153
  if (!deliberate) {
146
154
  return c.json({ ok: false, error: { code: "INTERNAL_ERROR", message: "Internal server error" } }, 500);
147
155
  }
@@ -732,16 +740,16 @@ function parseFilters(url, validFields, columnFor) {
732
740
  }
733
741
 
734
742
  // src/routes/content.ts
743
+ function pickProgrammatic(resolved, names) {
744
+ const out = {};
745
+ for (const name of names) if (Object.hasOwn(resolved, name)) out[name] = resolved[name];
746
+ return out;
747
+ }
735
748
  function isPublished(item) {
736
749
  return item["published"] === true;
737
750
  }
738
751
  function registerPublicContentRoutes(app, registry, repos, projectors, paths, listRateLimit, resolver, resolverProjectors) {
739
752
  const forResolver = resolverProjectors ?? projectors;
740
- function pickProgrammatic(resolved, names) {
741
- const out = {};
742
- for (const name of names) if (name in resolved) out[name] = resolved[name];
743
- return out;
744
- }
745
753
  async function respondItem(row, typeName, programmatic) {
746
754
  const data = projectRow(row, typeName, projectors);
747
755
  if (!resolver?.hasSchema(typeName)) return data;
@@ -1050,7 +1058,7 @@ function registerPublicMediaRoutes(app, mediaRepo, paths, listRateLimit) {
1050
1058
  }
1051
1059
 
1052
1060
  // src/routes/admin/content.ts
1053
- import { sql as sql4 } from "drizzle-orm";
1061
+ import { sql as sql3 } from "drizzle-orm";
1054
1062
 
1055
1063
  // src/references-in-use.ts
1056
1064
  import { sql as sql2 } from "drizzle-orm";
@@ -1062,25 +1070,42 @@ function quoteIdent(name) {
1062
1070
  function isRequiredSingleReferenceTo(f, targetType) {
1063
1071
  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;
1064
1072
  }
1073
+ function requiredReferencesTo(owners, targetType) {
1074
+ return owners.flatMap(
1075
+ (owner) => owner.fields.filter((f) => isRequiredSingleReferenceTo(f, targetType)).map((f) => ({ owner, f }))
1076
+ );
1077
+ }
1065
1078
  async function findRequiredReferrers(db, registry, targetType, id) {
1066
- const owners = [
1067
- ...Object.values(registry.content_types),
1068
- ...Object.values(registry.taxonomy_types),
1069
- ...Object.values(registry.paragraph_types)
1070
- ];
1071
- const out = [];
1072
- for (const owner of owners) {
1073
- for (const f of owner.fields) {
1074
- if (!isRequiredSingleReferenceTo(f, targetType)) continue;
1075
- const notSelf = owner.name === targetType ? sql2` AND id <> ${id}` : sql2``;
1076
- const r = await db.execute(
1077
- 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}`
1078
- );
1079
- const n = r.rows[0].n;
1080
- if (n > 0) out.push({ type_label: owner.label, field_label: f.label, count: n });
1081
- }
1079
+ const targetFields = registry.content_types[targetType]?.fields ?? registry.taxonomy_types[targetType]?.fields ?? [];
1080
+ const itemOwners = [...Object.values(registry.content_types), ...Object.values(registry.taxonomy_types)];
1081
+ const itemFields = requiredReferencesTo(itemOwners, targetType);
1082
+ const paragraphFields = requiredReferencesTo(Object.values(registry.paragraph_types), targetType);
1083
+ const ownIds = paragraphFields.length > 0 ? await ownParagraphRowIds(db, registry, targetFields, id) : [];
1084
+ const [itemCounts, paragraphCounts] = await Promise.all([
1085
+ Promise.all(
1086
+ itemFields.map(async ({ owner, f }) => {
1087
+ const notSelf = owner.name === targetType ? sql2` AND id <> ${id}` : sql2``;
1088
+ const r = await db.execute(
1089
+ 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}`
1090
+ );
1091
+ return { owner, f, n: r.rows[0].n, own: 0 };
1092
+ })
1093
+ ),
1094
+ Promise.all(
1095
+ paragraphFields.map(async ({ owner, f }) => {
1096
+ const r = await db.execute(
1097
+ sql2`SELECT count(*)::int AS n, count(*) FILTER (WHERE id = ANY(${sql2.param(ownIds)}::uuid[]))::int AS own FROM ${sql2.raw(quoteIdent(owner.db.table_name))} WHERE ${sql2.raw(quoteIdent(f.db_column.column_name))} = ${id}`
1098
+ );
1099
+ const { n, own } = r.rows[0];
1100
+ return { owner, f, n, own };
1101
+ })
1102
+ )
1103
+ ]);
1104
+ const others = [];
1105
+ for (const { owner, f, n, own } of [...itemCounts, ...paragraphCounts]) {
1106
+ if (n - own > 0) others.push({ type_label: owner.label, field_label: f.label, count: n - own });
1082
1107
  }
1083
- return out;
1108
+ return { others, fromOwnParagraphs: paragraphCounts.some((c) => c.own > 0) };
1084
1109
  }
1085
1110
  function inUseMessage(referrers) {
1086
1111
  const total = referrers.reduce((sum, r) => sum + r.count, 0);
@@ -1098,7 +1123,7 @@ function isForeignKeyViolation(err) {
1098
1123
  }
1099
1124
 
1100
1125
  // src/routes/admin/content.ts
1101
- import { relationCardinality as relationCardinality3 } from "@bobbykim/manguito-cms-core";
1126
+ import { relationCardinality as relationCardinality2 } from "@bobbykim/manguito-cms-core";
1102
1127
 
1103
1128
  // src/media-references.ts
1104
1129
  function extractMediaId(value) {
@@ -1110,7 +1135,7 @@ function topLevelMediaDelta(mediaFields, before, after) {
1110
1135
  for (const f of mediaFields) {
1111
1136
  const key = f.db_column?.column_name;
1112
1137
  if (key === void 0 || key === "") continue;
1113
- if (after !== null && !(key in after)) continue;
1138
+ if (after !== null && !Object.hasOwn(after, key)) continue;
1114
1139
  const oldId = before ? extractMediaId(before[key]) : null;
1115
1140
  const newId = after ? extractMediaId(after[key]) : null;
1116
1141
  if (newId === oldId) continue;
@@ -1134,131 +1159,6 @@ async function applyMediaReferenceDelta(delta, mediaRepo) {
1134
1159
  if (netRemove.length > 0) await mediaRepo.decrementReferenceCount(netRemove);
1135
1160
  }
1136
1161
 
1137
- // src/relation-input.ts
1138
- import { sql as sql3 } from "drizzle-orm";
1139
- import { relationCardinality as relationCardinality2 } from "@bobbykim/manguito-cms-core";
1140
- var UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
1141
- function isUuid(v) {
1142
- return typeof v === "string" && UUID_RE.test(v);
1143
- }
1144
- function isPlainObject(v) {
1145
- return typeof v === "object" && v !== null && !Array.isArray(v);
1146
- }
1147
- function checkRelationInput(fields, body, registry, path = "") {
1148
- const errors = [];
1149
- for (const field of fields) {
1150
- if (!(field.name in body)) continue;
1151
- const cardinality = relationCardinality2(field);
1152
- if (cardinality === null) continue;
1153
- const value = body[field.name];
1154
- const name = `${path}${field.name}`;
1155
- if (field.field_type === "reference") {
1156
- const ok = cardinality === "one" ? value === null || isUuid(value) : Array.isArray(value) && value.every(isUuid);
1157
- if (!ok) {
1158
- errors.push({
1159
- field: name,
1160
- message: cardinality === "one" ? `${name} holds one item: send a UUID string or null.` : `${name} holds a list: send an array of UUID strings.`
1161
- });
1162
- }
1163
- continue;
1164
- }
1165
- const items = cardinality === "one" ? value === null ? [] : isPlainObject(value) ? [value] : void 0 : Array.isArray(value) && value.every(isPlainObject) ? value : void 0;
1166
- if (items === void 0) {
1167
- errors.push({
1168
- field: name,
1169
- message: cardinality === "one" ? `${name} holds one item: send an object or null.` : `${name} holds a list: send an array of objects.`
1170
- });
1171
- continue;
1172
- }
1173
- const pType = paragraphTypeOf(field, registry);
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 paragraphTypeOf(field, registry) {
1183
- const comp = field.ui_component;
1184
- return comp.component === "paragraph-embed" ? registry.paragraph_types[comp.ref] : void 0;
1185
- }
1186
- function paragraphItemsIn(fields, body, registry, path) {
1187
- const out = [];
1188
- for (const field of fields) {
1189
- if (field.field_type !== "paragraph" || !(field.name in body)) continue;
1190
- const pType = paragraphTypeOf(field, registry);
1191
- if (!pType) continue;
1192
- const value = body[field.name];
1193
- const name = `${path}${field.name}`;
1194
- if (relationCardinality2(field) === "one") {
1195
- if (isPlainObject(value)) out.push({ item: value, fields: pType.fields, prefix: `${name}.` });
1196
- } else if (Array.isArray(value)) {
1197
- value.forEach((item, i) => {
1198
- if (isPlainObject(item)) out.push({ item, fields: pType.fields, prefix: `${name}[${i}].` });
1199
- });
1200
- }
1201
- }
1202
- return out;
1203
- }
1204
- function isMissing(value) {
1205
- if (value === null || value === void 0) return true;
1206
- if (typeof value === "string" && value.trim() === "") return true;
1207
- if (Array.isArray(value) && value.length === 0) return true;
1208
- return false;
1209
- }
1210
- function checkRequiredInParagraphItems(fields, body, registry, path = "") {
1211
- const errors = [];
1212
- for (const { item, fields: itemFields, prefix } of paragraphItemsIn(fields, body, registry, path)) {
1213
- for (const f of itemFields) {
1214
- if (!f.required || f.removed === true || !f.db_column || f.db_column.junction) continue;
1215
- if (isMissing(item[f.name])) {
1216
- errors.push({ field: `${prefix}${f.name}`, message: `${prefix}${f.name} is required.` });
1217
- }
1218
- }
1219
- errors.push(...checkRequiredInParagraphItems(itemFields, item, registry, prefix));
1220
- }
1221
- return errors;
1222
- }
1223
- function quoteIdent2(name) {
1224
- if (!/^[a-z][a-z0-9_-]*$/.test(name)) throw new Error(`Unsafe identifier: ${name}`);
1225
- return `"${name}"`;
1226
- }
1227
- function collectReferences(fields, body, registry, path, out) {
1228
- for (const f of fields) {
1229
- if (!(f.name in body) || !f.db_column) continue;
1230
- const table = f.db_column.junction ? f.db_column.junction.right_table : f.db_column.foreign_key?.table;
1231
- if (!table) continue;
1232
- const value = body[f.name];
1233
- const name = `${path}${f.name}`;
1234
- if (typeof value === "string") {
1235
- out.push({ table, id: value, field: name });
1236
- } else if (Array.isArray(value)) {
1237
- value.forEach((v, i) => {
1238
- if (typeof v === "string") out.push({ table, id: v, field: `${name}[${i}]` });
1239
- });
1240
- }
1241
- }
1242
- for (const { item, fields: itemFields, prefix } of paragraphItemsIn(fields, body, registry, path)) {
1243
- collectReferences(itemFields, item, registry, prefix, out);
1244
- }
1245
- }
1246
- async function findMissingReferences(db, fields, body, registry) {
1247
- const uses = [];
1248
- collectReferences(fields, body, registry, "", uses);
1249
- if (uses.length === 0) return [];
1250
- const lookups = [...new Set(uses.map((u) => u.table))].map(async (table) => {
1251
- const ids = [...new Set(uses.filter((u) => u.table === table && isUuid(u.id)).map((u) => u.id.toLowerCase()))];
1252
- if (ids.length === 0) return [table, /* @__PURE__ */ new Set()];
1253
- const result = await db.execute(
1254
- sql3`SELECT id FROM ${sql3.raw(quoteIdent2(table))} WHERE id IN (${sql3.join(ids.map((id) => sql3`${id}`), sql3`, `)})`
1255
- );
1256
- return [table, new Set(result.rows.map((r) => String(r.id).toLowerCase()))];
1257
- });
1258
- const found = new Map(await Promise.all(lookups));
1259
- 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.` }));
1260
- }
1261
-
1262
1162
  // src/field-values.ts
1263
1163
  var VARCHAR_DEFAULT_LIMIT = 255;
1264
1164
  var INTEGER_MIN = -2147483648;
@@ -1270,7 +1170,7 @@ var EMPTY_MEANS_NULL = /* @__PURE__ */ new Set(["date", "integer", "float", "enu
1270
1170
  function emptyValuesToNull(fields, body, registry) {
1271
1171
  const out = { ...body };
1272
1172
  for (const f of fields) {
1273
- if (!(f.name in out)) continue;
1173
+ if (!Object.hasOwn(out, f.name)) continue;
1274
1174
  const value = out[f.name];
1275
1175
  if (value === "" && EMPTY_MEANS_NULL.has(f.field_type)) {
1276
1176
  out[f.name] = null;
@@ -1375,7 +1275,7 @@ function rangeError(name, value, min, max) {
1375
1275
  function checkFieldValues(fields, body, registry, path = "") {
1376
1276
  const errors = [];
1377
1277
  for (const f of fields) {
1378
- if (!(f.name in body) || f.removed === true) continue;
1278
+ if (!Object.hasOwn(body, f.name) || f.removed === true) continue;
1379
1279
  const name = `${path}${f.name}`;
1380
1280
  const message = valueError(f, body[f.name], name);
1381
1281
  if (message) errors.push({ field: name, message });
@@ -1387,24 +1287,22 @@ function checkFieldValues(fields, body, registry, path = "") {
1387
1287
  }
1388
1288
 
1389
1289
  // src/routes/admin/content.ts
1390
- function quoteIdent3(name) {
1290
+ function quoteIdent2(name) {
1391
1291
  if (!/^[a-z][a-z0-9_-]*$/.test(name)) throw new Error(`Unsafe identifier: ${name}`);
1392
1292
  return `"${name}"`;
1393
1293
  }
1394
1294
  async function loadParagraphRows(db, registry, pType, parentId, fieldName) {
1395
1295
  const result = await db.execute(
1396
- sql4`SELECT * FROM ${sql4.raw(quoteIdent3(pType.db.table_name))} WHERE parent_id = ${parentId} AND parent_field = ${fieldName} ORDER BY "order" ASC`
1296
+ sql3`SELECT * FROM ${sql3.raw(quoteIdent2(pType.db.table_name))} WHERE parent_id = ${parentId} AND parent_field = ${fieldName} ORDER BY "order" ASC, id ASC`
1397
1297
  );
1398
1298
  const rows = result.rows;
1399
1299
  for (const nf of pType.fields) {
1400
1300
  if (nf.db_column !== null) continue;
1401
- const comp = nf.ui_component;
1402
- if (comp.component !== "paragraph-embed" || !comp.ref) continue;
1403
- const nType = registry.paragraph_types[comp.ref];
1301
+ const nType = paragraphTypeOf(nf, registry);
1404
1302
  if (!nType) continue;
1405
1303
  for (const row of rows) {
1406
1304
  row[nf.name] = oneOrMany(
1407
- relationCardinality3(nf) ?? "many",
1305
+ relationCardinality2(nf) ?? "many",
1408
1306
  await loadParagraphRows(db, registry, nType, row["id"], nf.name)
1409
1307
  );
1410
1308
  }
@@ -1412,35 +1310,36 @@ async function loadParagraphRows(db, registry, pType, parentId, fieldName) {
1412
1310
  return rows;
1413
1311
  }
1414
1312
  async function loadRelationsForEdit(db, registry, fields, id, row) {
1415
- for (const f of fields) {
1416
- if (f.db_column === null) {
1417
- const comp = f.ui_component;
1418
- if (comp.component !== "paragraph-embed" || !comp.ref) continue;
1419
- const pType = registry.paragraph_types[comp.ref];
1420
- if (!pType) continue;
1421
- row[f.name] = oneOrMany(relationCardinality3(f) ?? "many", await loadParagraphRows(db, registry, pType, id, f.name));
1422
- } else if (f.db_column.junction) {
1423
- const j = f.db_column.junction;
1424
- const r = await db.execute(
1425
- sql4`SELECT ${sql4.raw(quoteIdent3(j.right_column))} FROM ${sql4.raw(quoteIdent3(j.table_name))} WHERE ${sql4.raw(quoteIdent3(j.left_column))} = ${id}${junctionOrderBy(j)}`
1426
- );
1427
- row[f.name] = r.rows.map((linked) => linked[j.right_column]);
1428
- }
1429
- }
1313
+ const loaded = await Promise.all(
1314
+ fields.map(async (f) => {
1315
+ if (f.db_column === null) {
1316
+ const pType = paragraphTypeOf(f, registry);
1317
+ if (!pType) return null;
1318
+ return [f.name, oneOrMany(relationCardinality2(f) ?? "many", await loadParagraphRows(db, registry, pType, id, f.name))];
1319
+ }
1320
+ if (f.db_column.junction) {
1321
+ const j = f.db_column.junction;
1322
+ const r = await db.execute(
1323
+ sql3`SELECT ${sql3.raw(quoteIdent2(j.right_column))} FROM ${sql3.raw(quoteIdent2(j.table_name))} WHERE ${sql3.raw(quoteIdent2(j.left_column))} = ${id}${junctionOrderBy(j)}`
1324
+ );
1325
+ return [f.name, [...new Set(r.rows.map((linked) => linked[j.right_column]))]];
1326
+ }
1327
+ return null;
1328
+ })
1329
+ );
1330
+ for (const entry of loaded) if (entry) row[entry[0]] = entry[1];
1430
1331
  }
1431
1332
  async function valuesForPublishCheck(db, registry, requiredFields, id, storedLabels, body) {
1432
1333
  const stored = { ...storedLabels };
1433
- if (db) await loadRelationsForEdit(db, registry, requiredFields.filter((f) => !(f.name in body)), id, stored);
1334
+ if (db) await loadRelationsForEdit(db, registry, requiredFields.filter((f) => !Object.hasOwn(body, f.name)), id, stored);
1434
1335
  return { ...stored, ...body };
1435
1336
  }
1436
1337
  async function persistRelationFields(db, registry, fields, body, itemId, ownerTable) {
1437
1338
  const deltas = [];
1438
1339
  for (const f of fields) {
1439
- if (!(f.name in body)) continue;
1340
+ if (!Object.hasOwn(body, f.name)) continue;
1440
1341
  if (f.db_column === null) {
1441
- const comp = f.ui_component;
1442
- if (comp.component !== "paragraph-embed" || !comp.ref) continue;
1443
- const pType = registry.paragraph_types[comp.ref];
1342
+ const pType = paragraphTypeOf(f, registry);
1444
1343
  if (!pType) continue;
1445
1344
  deltas.push(await persistParagraphField(db, itemId, ownerTable, f.name, pType, paragraphItems(f, body[f.name]), registry));
1446
1345
  } else if (f.db_column.junction) {
@@ -1451,9 +1350,49 @@ async function persistRelationFields(db, registry, fields, body, itemId, ownerTa
1451
1350
  }
1452
1351
  return deltas;
1453
1352
  }
1353
+ async function deleteOwnParagraphs(db, registry, fields, itemId) {
1354
+ const deltas = [];
1355
+ for (const f of fields) {
1356
+ const pType = paragraphTypeOf(f, registry);
1357
+ if (!pType) continue;
1358
+ deltas.push(await deleteParagraphField(db, itemId, f.name, pType, registry));
1359
+ }
1360
+ return deltas;
1361
+ }
1362
+ function releasedFromParagraphsOnly(paragraphDeltas, topLevel) {
1363
+ const stillHeld = new Set(topLevel.removed);
1364
+ return { added: [], removed: mergeMediaDeltas(...paragraphDeltas).removed.filter((mediaId) => !stillHeld.has(mediaId)) };
1365
+ }
1366
+ async function deleteItemCascade(db, registry, mediaRepo, target) {
1367
+ const { typeName, fields, mediaFields, repo, id, row } = target;
1368
+ const referrers = db ? await findRequiredReferrers(db, registry, typeName, id) : { others: [], fromOwnParagraphs: false };
1369
+ if (referrers.others.length > 0) return { ok: false, message: inUseMessage(referrers.others) };
1370
+ const topLevel = topLevelMediaDelta(mediaFields, row, null);
1371
+ if (!db || !referrers.fromOwnParagraphs) {
1372
+ try {
1373
+ await repo.delete(id);
1374
+ } catch (err) {
1375
+ if (isForeignKeyViolation(err)) return { ok: false, message: IN_USE_RACE_MESSAGE };
1376
+ throw err;
1377
+ }
1378
+ const paragraphDeltas2 = db ? await deleteOwnParagraphs(db, registry, fields, id) : [];
1379
+ await applyMediaReferenceDelta(mergeMediaDeltas(topLevel, ...paragraphDeltas2), mediaRepo);
1380
+ return { ok: true };
1381
+ }
1382
+ const paragraphDeltas = await deleteOwnParagraphs(db, registry, fields, id);
1383
+ try {
1384
+ await repo.delete(id);
1385
+ } catch (err) {
1386
+ await applyMediaReferenceDelta(releasedFromParagraphsOnly(paragraphDeltas, topLevel), mediaRepo);
1387
+ if (isForeignKeyViolation(err)) return { ok: false, message: IN_USE_RACE_MESSAGE };
1388
+ throw err;
1389
+ }
1390
+ await applyMediaReferenceDelta(mergeMediaDeltas(topLevel, ...paragraphDeltas), mediaRepo);
1391
+ return { ok: true };
1392
+ }
1454
1393
  async function lookupBasePathId(db, pathOrName) {
1455
1394
  const r = await db.execute(
1456
- sql4`SELECT id FROM base_paths WHERE path = ${pathOrName} OR name = ${pathOrName} LIMIT 1`
1395
+ sql3`SELECT id FROM base_paths WHERE path = ${pathOrName} OR name = ${pathOrName} LIMIT 1`
1457
1396
  );
1458
1397
  return r.rows[0]?.id ?? null;
1459
1398
  }
@@ -1560,7 +1499,6 @@ function registerAdminContentRoutes(app, registry, repos, projectors, mediaRepo,
1560
1499
  const mediaFields = contentType.fields.filter(
1561
1500
  (f) => f.field_type === "image" || f.field_type === "video" || f.field_type === "file"
1562
1501
  );
1563
- const paragraphFieldDefs = contentType.fields.filter((f) => f.db_column === null);
1564
1502
  const searchableColumns = [
1565
1503
  ...contentType.fields.filter((f) => f.field_type === "text/plain" && f.db_column !== null).map((f) => f.db_column.column_name),
1566
1504
  ...contentType.system_fields.some((f) => f.name === "slug") ? ["slug"] : []
@@ -1710,7 +1648,7 @@ function registerAdminContentRoutes(app, registry, repos, projectors, mediaRepo,
1710
1648
  }
1711
1649
  for (const f of columnBackedFields) {
1712
1650
  const key = f.db_column.column_name;
1713
- if (!(key in storageBody)) continue;
1651
+ if (!Object.hasOwn(storageBody, key)) continue;
1714
1652
  columnData[key] = storageBody[key];
1715
1653
  }
1716
1654
  if (db) {
@@ -1752,7 +1690,7 @@ function registerAdminContentRoutes(app, registry, repos, projectors, mediaRepo,
1752
1690
  404
1753
1691
  );
1754
1692
  }
1755
- if (!contentType.only_one && "slug" in body) {
1693
+ if (!contentType.only_one && Object.hasOwn(body, "slug")) {
1756
1694
  const slug = body["slug"];
1757
1695
  if (typeof slug !== "string" || slug.trim() === "") {
1758
1696
  return c.json(
@@ -1822,11 +1760,11 @@ function registerAdminContentRoutes(app, registry, repos, projectors, mediaRepo,
1822
1760
  }
1823
1761
  const patchColumnBackedFields = contentType.fields.filter(isColumnBacked);
1824
1762
  const patchData = {};
1825
- if (!contentType.only_one && "slug" in body) patchData["slug"] = body["slug"];
1826
- if ("published" in body) patchData["published"] = body["published"];
1763
+ if (!contentType.only_one && Object.hasOwn(body, "slug")) patchData["slug"] = body["slug"];
1764
+ if (Object.hasOwn(body, "published")) patchData["published"] = body["published"];
1827
1765
  for (const f of patchColumnBackedFields) {
1828
1766
  const key = f.db_column.column_name;
1829
- if (!(key in storageBody)) continue;
1767
+ if (!Object.hasOwn(storageBody, key)) continue;
1830
1768
  patchData[key] = storageBody[key];
1831
1769
  }
1832
1770
  const updated = await repo.update(id, patchData);
@@ -1876,31 +1814,17 @@ function registerAdminContentRoutes(app, registry, repos, projectors, mediaRepo,
1876
1814
  404
1877
1815
  );
1878
1816
  }
1879
- const referrers = db ? await findRequiredReferrers(db, registry, typeName, id) : [];
1880
- if (referrers.length > 0) {
1881
- return c.json({ ok: false, error: { code: "ITEM_IN_USE", message: inUseMessage(referrers) } }, 409);
1882
- }
1883
- try {
1884
- await repo.delete(id);
1885
- } catch (err) {
1886
- if (isForeignKeyViolation(err)) {
1887
- return c.json({ ok: false, error: { code: "ITEM_IN_USE", message: IN_USE_RACE_MESSAGE } }, 409);
1888
- }
1889
- throw err;
1890
- }
1891
- const mediaDeltas = [
1892
- topLevelMediaDelta(mediaFields, item, null)
1893
- ];
1894
- if (db) {
1895
- for (const f of paragraphFieldDefs) {
1896
- const comp = f.ui_component;
1897
- if (comp.component !== "paragraph-embed" || !comp.ref) continue;
1898
- const pType = registry.paragraph_types[comp.ref];
1899
- if (!pType) continue;
1900
- mediaDeltas.push(await deleteParagraphField(db, id, f.name, pType, registry));
1901
- }
1817
+ const outcome = await deleteItemCascade(db, registry, mediaRepo, {
1818
+ typeName,
1819
+ fields: contentType.fields,
1820
+ mediaFields,
1821
+ repo,
1822
+ id,
1823
+ row: item
1824
+ });
1825
+ if (!outcome.ok) {
1826
+ return c.json({ ok: false, error: { code: "ITEM_IN_USE", message: outcome.message } }, 409);
1902
1827
  }
1903
- await applyMediaReferenceDelta(mergeMediaDeltas(...mediaDeltas), mediaRepo);
1904
1828
  return c.json({ ok: true });
1905
1829
  }
1906
1830
  );
@@ -1921,7 +1845,6 @@ function registerAdminContentRoutes(app, registry, repos, projectors, mediaRepo,
1921
1845
  const mediaFields = taxonomyType.fields.filter(
1922
1846
  (f) => f.field_type === "image" || f.field_type === "video" || f.field_type === "file"
1923
1847
  );
1924
- const paragraphFieldDefs = taxonomyType.fields.filter((f) => f.db_column === null);
1925
1848
  const searchableColumns = [
1926
1849
  ...taxonomyType.fields.filter((f) => f.field_type === "text/plain" && f.db_column !== null).map((f) => f.db_column.column_name),
1927
1850
  ...taxonomyType.system_fields.some((f) => f.name === "slug") ? ["slug"] : []
@@ -2007,7 +1930,7 @@ function registerAdminContentRoutes(app, registry, repos, projectors, mediaRepo,
2007
1930
  };
2008
1931
  for (const f of taxColumnBackedFields) {
2009
1932
  const key = f.db_column.column_name;
2010
- if (!(key in storageBody)) continue;
1933
+ if (!Object.hasOwn(storageBody, key)) continue;
2011
1934
  taxColumnData[key] = storageBody[key];
2012
1935
  }
2013
1936
  const item = await repo.create(taxColumnData);
@@ -2059,10 +1982,10 @@ function registerAdminContentRoutes(app, registry, repos, projectors, mediaRepo,
2059
1982
  }
2060
1983
  const taxPatchColumnBackedFields = taxonomyType.fields.filter(isColumnBacked);
2061
1984
  const taxPatchData = {};
2062
- if ("published" in body) taxPatchData["published"] = body["published"];
1985
+ if (Object.hasOwn(body, "published")) taxPatchData["published"] = body["published"];
2063
1986
  for (const f of taxPatchColumnBackedFields) {
2064
1987
  const key = f.db_column.column_name;
2065
- if (!(key in storageBody)) continue;
1988
+ if (!Object.hasOwn(storageBody, key)) continue;
2066
1989
  taxPatchData[key] = storageBody[key];
2067
1990
  }
2068
1991
  const updated = await repo.update(id, taxPatchData);
@@ -2095,31 +2018,17 @@ function registerAdminContentRoutes(app, registry, repos, projectors, mediaRepo,
2095
2018
  404
2096
2019
  );
2097
2020
  }
2098
- const referrers = db ? await findRequiredReferrers(db, registry, typeName, id) : [];
2099
- if (referrers.length > 0) {
2100
- return c.json({ ok: false, error: { code: "ITEM_IN_USE", message: inUseMessage(referrers) } }, 409);
2101
- }
2102
- try {
2103
- await repo.delete(id);
2104
- } catch (err) {
2105
- if (isForeignKeyViolation(err)) {
2106
- return c.json({ ok: false, error: { code: "ITEM_IN_USE", message: IN_USE_RACE_MESSAGE } }, 409);
2107
- }
2108
- throw err;
2109
- }
2110
- const mediaDeltas = [
2111
- topLevelMediaDelta(mediaFields, item, null)
2112
- ];
2113
- if (db) {
2114
- for (const f of paragraphFieldDefs) {
2115
- const comp = f.ui_component;
2116
- if (comp.component !== "paragraph-embed" || !comp.ref) continue;
2117
- const pType = registry.paragraph_types[comp.ref];
2118
- if (!pType) continue;
2119
- mediaDeltas.push(await deleteParagraphField(db, id, f.name, pType, registry));
2120
- }
2021
+ const outcome = await deleteItemCascade(db, registry, mediaRepo, {
2022
+ typeName,
2023
+ fields: taxonomyType.fields,
2024
+ mediaFields,
2025
+ repo,
2026
+ id,
2027
+ row: item
2028
+ });
2029
+ if (!outcome.ok) {
2030
+ return c.json({ ok: false, error: { code: "ITEM_IN_USE", message: outcome.message } }, 409);
2121
2031
  }
2122
- await applyMediaReferenceDelta(mergeMediaDeltas(...mediaDeltas), mediaRepo);
2123
2032
  return c.json({ ok: true });
2124
2033
  }
2125
2034
  );
@@ -2581,7 +2490,7 @@ function registerAdminMediaRoutes(app, mediaRepo, storage, requirePermission, ma
2581
2490
 
2582
2491
  // src/routes/admin/auth.ts
2583
2492
  import { getCookie as getCookie2 } from "hono/cookie";
2584
- import { sql as sql5 } from "drizzle-orm";
2493
+ import { sql as sql4 } from "drizzle-orm";
2585
2494
 
2586
2495
  // src/auth/password.ts
2587
2496
  import { hashPassword, verifyPassword } from "@bobbykim/manguito-cms-core";
@@ -2647,7 +2556,7 @@ function registerAuthRoutes(app, db) {
2647
2556
  );
2648
2557
  }
2649
2558
  const userResult = await db.execute(
2650
- sql5`SELECT id, email, password_hash, role_id, token_version
2559
+ sql4`SELECT id, email, password_hash, role_id, token_version
2651
2560
  FROM users WHERE email = ${email} LIMIT 1`
2652
2561
  );
2653
2562
  const user = userResult.rows[0];
@@ -2660,7 +2569,7 @@ function registerAuthRoutes(app, db) {
2660
2569
  return c.json(invalidCredentials, 401);
2661
2570
  }
2662
2571
  const roleResult = await db.execute(
2663
- sql5`SELECT r.name AS name, u.must_change_password
2572
+ sql4`SELECT r.name AS name, u.must_change_password
2664
2573
  FROM roles r
2665
2574
  JOIN users u ON u.role_id = r.id
2666
2575
  WHERE r.id = ${user.role_id} AND u.id = ${user.id}
@@ -2720,7 +2629,7 @@ function registerAuthRoutes(app, db) {
2720
2629
  );
2721
2630
  }
2722
2631
  const rowResult = await db.execute(
2723
- sql5`SELECT u.token_version, r.name AS role
2632
+ sql4`SELECT u.token_version, r.name AS role
2724
2633
  FROM users u
2725
2634
  JOIN roles r ON r.id = u.role_id
2726
2635
  WHERE u.id = ${payload.user_id}
@@ -2744,7 +2653,7 @@ function registerAuthRoutes(app, db) {
2744
2653
  app.post("/admin/api/auth/logout", authMiddleware, async (c) => {
2745
2654
  const user = c.get("user");
2746
2655
  await db.execute(
2747
- sql5`UPDATE users SET token_version = token_version + 1 WHERE id = ${user.id}`
2656
+ sql4`UPDATE users SET token_version = token_version + 1 WHERE id = ${user.id}`
2748
2657
  );
2749
2658
  clearAuthCookie(c, "auth_token");
2750
2659
  clearAuthCookie(c, "refresh_token", "/admin/api/auth");
@@ -2753,7 +2662,7 @@ function registerAuthRoutes(app, db) {
2753
2662
  }
2754
2663
 
2755
2664
  // src/routes/admin/users.ts
2756
- import { sql as sql6 } from "drizzle-orm";
2665
+ import { sql as sql5 } from "drizzle-orm";
2757
2666
  import { randomBytes } from "crypto";
2758
2667
  function getActingUser(c) {
2759
2668
  return c.get("user");
@@ -2788,7 +2697,7 @@ function registerUserRoutes(app, db, requirePermission, requireHierarchy) {
2788
2697
  );
2789
2698
  }
2790
2699
  const hashResult = await db.execute(
2791
- sql6`SELECT password_hash FROM users WHERE id = ${actingUser.id} LIMIT 1`
2700
+ sql5`SELECT password_hash FROM users WHERE id = ${actingUser.id} LIMIT 1`
2792
2701
  );
2793
2702
  const hashRow = hashResult.rows[0];
2794
2703
  if (!hashRow || !await verifyPassword(currentPassword, hashRow.password_hash)) {
@@ -2799,7 +2708,7 @@ function registerUserRoutes(app, db, requirePermission, requireHierarchy) {
2799
2708
  }
2800
2709
  const newHash = await hashPassword(newPassword);
2801
2710
  await db.execute(
2802
- sql6`UPDATE users
2711
+ sql5`UPDATE users
2803
2712
  SET password_hash = ${newHash},
2804
2713
  must_change_password = false,
2805
2714
  token_version = token_version + 1,
@@ -2810,7 +2719,7 @@ function registerUserRoutes(app, db, requirePermission, requireHierarchy) {
2810
2719
  });
2811
2720
  app.get("/admin/api/users", requirePermission("users:read"), async (c) => {
2812
2721
  const result = await db.execute(
2813
- sql6`SELECT u.id, u.email, r.name AS role, u.must_change_password,
2722
+ sql5`SELECT u.id, u.email, r.name AS role, u.must_change_password,
2814
2723
  u.created_at, u.updated_at
2815
2724
  FROM users u
2816
2725
  JOIN roles r ON r.id = u.role_id
@@ -2821,7 +2730,7 @@ function registerUserRoutes(app, db, requirePermission, requireHierarchy) {
2821
2730
  app.get("/admin/api/users/:id", requirePermission("users:read"), async (c) => {
2822
2731
  const id = c.req.param("id");
2823
2732
  const result = await db.execute(
2824
- sql6`SELECT u.id, u.email, r.name AS role, u.must_change_password,
2733
+ sql5`SELECT u.id, u.email, r.name AS role, u.must_change_password,
2825
2734
  u.created_at, u.updated_at
2826
2735
  FROM users u
2827
2736
  JOIN roles r ON r.id = u.role_id
@@ -2866,7 +2775,7 @@ function registerUserRoutes(app, db, requirePermission, requireHierarchy) {
2866
2775
  );
2867
2776
  }
2868
2777
  const roleResult = await db.execute(
2869
- sql6`SELECT id FROM roles WHERE name = ${roleName} LIMIT 1`
2778
+ sql5`SELECT id FROM roles WHERE name = ${roleName} LIMIT 1`
2870
2779
  );
2871
2780
  const roleRow = roleResult.rows[0];
2872
2781
  if (!roleRow) {
@@ -2881,7 +2790,7 @@ function registerUserRoutes(app, db, requirePermission, requireHierarchy) {
2881
2790
  const tempPassword = generateTempPassword();
2882
2791
  const passwordHash = await hashPassword(tempPassword);
2883
2792
  const insertResult = await db.execute(
2884
- sql6`INSERT INTO users
2793
+ sql5`INSERT INTO users
2885
2794
  (id, email, password_hash, role_id, must_change_password, token_version,
2886
2795
  created_at, updated_at)
2887
2796
  VALUES
@@ -2936,7 +2845,7 @@ function registerUserRoutes(app, db, requirePermission, requireHierarchy) {
2936
2845
  );
2937
2846
  }
2938
2847
  const existingResult = await db.execute(
2939
- sql6`SELECT id, email, role_id FROM users WHERE id = ${id} LIMIT 1`
2848
+ sql5`SELECT id, email, role_id FROM users WHERE id = ${id} LIMIT 1`
2940
2849
  );
2941
2850
  const existing = existingResult.rows[0];
2942
2851
  if (!existing) {
@@ -2950,7 +2859,7 @@ function registerUserRoutes(app, db, requirePermission, requireHierarchy) {
2950
2859
  let changeRole = false;
2951
2860
  if (typeof body.role === "string" && body.role) {
2952
2861
  const roleResult = await db.execute(
2953
- sql6`SELECT id FROM roles WHERE name = ${body.role} LIMIT 1`
2862
+ sql5`SELECT id FROM roles WHERE name = ${body.role} LIMIT 1`
2954
2863
  );
2955
2864
  const roleRow = roleResult.rows[0];
2956
2865
  if (!roleRow) {
@@ -2970,7 +2879,7 @@ function registerUserRoutes(app, db, requirePermission, requireHierarchy) {
2970
2879
  }
2971
2880
  if (changeRole) {
2972
2881
  await db.execute(
2973
- sql6`UPDATE users
2882
+ sql5`UPDATE users
2974
2883
  SET email = ${newEmail},
2975
2884
  role_id = ${newRoleId},
2976
2885
  token_version = token_version + 1,
@@ -2979,7 +2888,7 @@ function registerUserRoutes(app, db, requirePermission, requireHierarchy) {
2979
2888
  );
2980
2889
  } else {
2981
2890
  await db.execute(
2982
- sql6`UPDATE users
2891
+ sql5`UPDATE users
2983
2892
  SET email = ${newEmail},
2984
2893
  role_id = ${newRoleId},
2985
2894
  updated_at = NOW()
@@ -2987,7 +2896,7 @@ function registerUserRoutes(app, db, requirePermission, requireHierarchy) {
2987
2896
  );
2988
2897
  }
2989
2898
  const updatedResult = await db.execute(
2990
- sql6`SELECT u.id, u.email, r.name AS role, u.must_change_password,
2899
+ sql5`SELECT u.id, u.email, r.name AS role, u.must_change_password,
2991
2900
  u.created_at, u.updated_at
2992
2901
  FROM users u
2993
2902
  JOIN roles r ON r.id = u.role_id
@@ -3024,7 +2933,7 @@ function registerUserRoutes(app, db, requirePermission, requireHierarchy) {
3024
2933
  );
3025
2934
  }
3026
2935
  const result = await db.execute(
3027
- sql6`DELETE FROM users WHERE id = ${id} RETURNING id`
2936
+ sql5`DELETE FROM users WHERE id = ${id} RETURNING id`
3028
2937
  );
3029
2938
  if (result.rows.length === 0) {
3030
2939
  return c.json(
@@ -3055,7 +2964,7 @@ function registerUserRoutes(app, db, requirePermission, requireHierarchy) {
3055
2964
  );
3056
2965
  }
3057
2966
  const existingResult = await db.execute(
3058
- sql6`SELECT id FROM users WHERE id = ${id} LIMIT 1`
2967
+ sql5`SELECT id FROM users WHERE id = ${id} LIMIT 1`
3059
2968
  );
3060
2969
  const existing = existingResult.rows[0];
3061
2970
  if (!existing) {
@@ -3067,7 +2976,7 @@ function registerUserRoutes(app, db, requirePermission, requireHierarchy) {
3067
2976
  const tempPassword = generateTempPassword();
3068
2977
  const passwordHash = await hashPassword(tempPassword);
3069
2978
  await db.execute(
3070
- sql6`UPDATE users
2979
+ sql5`UPDATE users
3071
2980
  SET password_hash = ${passwordHash},
3072
2981
  must_change_password = true,
3073
2982
  token_version = token_version + 1,
@@ -3080,10 +2989,10 @@ function registerUserRoutes(app, db, requirePermission, requireHierarchy) {
3080
2989
  }
3081
2990
 
3082
2991
  // src/routes/admin/config.ts
3083
- import { sql as sql7 } from "drizzle-orm";
2992
+ import { sql as sql6 } from "drizzle-orm";
3084
2993
  var API_VERSION = (() => {
3085
2994
  try {
3086
- return "0.9.2";
2995
+ return "0.9.4";
3087
2996
  } catch {
3088
2997
  return "0.0.0";
3089
2998
  }
@@ -3102,7 +3011,7 @@ function registerConfigRoute(app, config, registry, db) {
3102
3011
  permissions: r.permissions
3103
3012
  }));
3104
3013
  const userResult = await db.execute(
3105
- sql7`SELECT email, must_change_password FROM users WHERE id = ${actingUser.id} LIMIT 1`
3014
+ sql6`SELECT email, must_change_password FROM users WHERE id = ${actingUser.id} LIMIT 1`
3106
3015
  );
3107
3016
  const userRow = userResult.rows[0];
3108
3017
  return c.json({
@@ -3128,7 +3037,7 @@ function registerConfigRoute(app, config, registry, db) {
3128
3037
  }
3129
3038
 
3130
3039
  // src/routes/admin/schema.ts
3131
- import { sql as sql8 } from "drizzle-orm";
3040
+ import { sql as sql7 } from "drizzle-orm";
3132
3041
  function liveFields(fields) {
3133
3042
  return fields.filter((f) => f.removed !== true);
3134
3043
  }
@@ -3168,7 +3077,7 @@ function registerSchemaRoute(app, registry, db) {
3168
3077
  const data = await Promise.all(
3169
3078
  Object.values(registry.content_types).map(async (ct) => {
3170
3079
  const result = await db.execute(
3171
- sql8`SELECT COUNT(*)::int AS count FROM ${sql8.raw(`"${ct.db.table_name}"`)}`
3080
+ sql7`SELECT COUNT(*)::int AS count FROM ${sql7.raw(`"${ct.db.table_name}"`)}`
3172
3081
  );
3173
3082
  const count = Number(result.rows[0].count);
3174
3083
  return { name: ct.name, label: ct.label, only_one: ct.only_one, count };
@@ -3180,7 +3089,7 @@ function registerSchemaRoute(app, registry, db) {
3180
3089
  const data = await Promise.all(
3181
3090
  Object.values(registry.taxonomy_types).map(async (tt) => {
3182
3091
  const result = await db.execute(
3183
- sql8`SELECT COUNT(*)::int AS count FROM ${sql8.raw(`"${tt.db.table_name}"`)}`
3092
+ sql7`SELECT COUNT(*)::int AS count FROM ${sql7.raw(`"${tt.db.table_name}"`)}`
3184
3093
  );
3185
3094
  const count = Number(result.rows[0].count);
3186
3095
  return { name: tt.name, label: tt.label, count };
@@ -3192,14 +3101,14 @@ function registerSchemaRoute(app, registry, db) {
3192
3101
 
3193
3102
  // src/repositories/content.ts
3194
3103
  import crypto from "crypto";
3195
- import { sql as sql9 } from "drizzle-orm";
3104
+ import { sql as sql8 } from "drizzle-orm";
3196
3105
  var SORTABLE_FIELDS2 = /* @__PURE__ */ new Set(["title", "created_at", "updated_at"]);
3197
3106
  function codeError(code, message) {
3198
3107
  const err = new Error(message);
3199
3108
  err.code = code;
3200
3109
  return err;
3201
3110
  }
3202
- function quoteIdent4(name) {
3111
+ function quoteIdent3(name) {
3203
3112
  if (!/^[a-z][a-z0-9_-]*$/.test(name)) {
3204
3113
  throw new Error(`Unsafe SQL identifier: ${name}`);
3205
3114
  }
@@ -3214,43 +3123,43 @@ function quoteField(name) {
3214
3123
  function buildConditions(published_only, filters, search) {
3215
3124
  const conds = [];
3216
3125
  if (published_only) {
3217
- conds.push(sql9`published = true`);
3126
+ conds.push(sql8`published = true`);
3218
3127
  }
3219
3128
  if (search && search.term.trim() !== "" && search.columns.length > 0) {
3220
3129
  const pattern = `%${search.term.trim()}%`;
3221
- const orConds = search.columns.map((col) => sql9`${sql9.raw(quoteField(col))} ILIKE ${pattern}`);
3222
- conds.push(sql9`(${sql9.join(orConds, sql9` OR `)})`);
3130
+ const orConds = search.columns.map((col) => sql8`${sql8.raw(quoteField(col))} ILIKE ${pattern}`);
3131
+ conds.push(sql8`(${sql8.join(orConds, sql8` OR `)})`);
3223
3132
  }
3224
3133
  if (filters) {
3225
3134
  for (const [field, value] of Object.entries(filters)) {
3226
- const col = sql9.raw(quoteField(field));
3135
+ const col = sql8.raw(quoteField(field));
3227
3136
  if (Array.isArray(value)) {
3228
3137
  if (value.length === 0) continue;
3229
- const inList = sql9.join(
3230
- value.map((v) => sql9`${v}`),
3231
- sql9`, `
3138
+ const inList = sql8.join(
3139
+ value.map((v) => sql8`${v}`),
3140
+ sql8`, `
3232
3141
  );
3233
- conds.push(sql9`${col} IN (${inList})`);
3142
+ conds.push(sql8`${col} IN (${inList})`);
3234
3143
  } else if (typeof value === "object" && value !== null) {
3235
3144
  const op = value;
3236
- if (op.gt !== void 0) conds.push(sql9`${col} > ${op.gt}`);
3237
- if (op.gte !== void 0) conds.push(sql9`${col} >= ${op.gte}`);
3238
- if (op.lt !== void 0) conds.push(sql9`${col} < ${op.lt}`);
3239
- if (op.lte !== void 0) conds.push(sql9`${col} <= ${op.lte}`);
3145
+ if (op.gt !== void 0) conds.push(sql8`${col} > ${op.gt}`);
3146
+ if (op.gte !== void 0) conds.push(sql8`${col} >= ${op.gte}`);
3147
+ if (op.lt !== void 0) conds.push(sql8`${col} < ${op.lt}`);
3148
+ if (op.lte !== void 0) conds.push(sql8`${col} <= ${op.lte}`);
3240
3149
  } else {
3241
- conds.push(sql9`${col} = ${value}`);
3150
+ conds.push(sql8`${col} = ${value}`);
3242
3151
  }
3243
3152
  }
3244
3153
  }
3245
3154
  return conds;
3246
3155
  }
3247
3156
  function whereFragment(conds) {
3248
- return conds.length > 0 ? sql9` WHERE ${sql9.join(conds, sql9` AND `)}` : sql9``;
3157
+ return conds.length > 0 ? sql8` WHERE ${sql8.join(conds, sql8` AND `)}` : sql8``;
3249
3158
  }
3250
3159
  function createDrizzleContentRepository(db, tableName, options = {}) {
3251
3160
  const { relations = {}, publishedRelations = false, sortableColumns } = options;
3252
3161
  function tableRaw() {
3253
- return sql9.raw(quoteIdent4(tableName));
3162
+ return sql8.raw(quoteIdent3(tableName));
3254
3163
  }
3255
3164
  async function resolveRows(rows, include, cache) {
3256
3165
  if (rows.length === 0) return rows;
@@ -3293,14 +3202,14 @@ function createDrizzleContentRepository(db, tableName, options = {}) {
3293
3202
  const offset = (page - 1) * per_page;
3294
3203
  const conds = buildConditions(published_only, filters, search);
3295
3204
  const where = whereFragment(conds);
3296
- const sortCol = sql9.raw(quoteIdent4(sort_by));
3297
- const sortDir = sql9.raw(sort_order === "desc" ? "DESC" : "ASC");
3205
+ const sortCol = sql8.raw(quoteIdent3(sort_by));
3206
+ const sortDir = sql8.raw(sort_order === "desc" ? "DESC" : "ASC");
3298
3207
  const countResult = await db.execute(
3299
- sql9`SELECT COUNT(*) AS total FROM ${tableRaw()}${where}`
3208
+ sql8`SELECT COUNT(*) AS total FROM ${tableRaw()}${where}`
3300
3209
  );
3301
3210
  const total = Number(countResult.rows[0]?.["total"] ?? 0);
3302
3211
  const dataResult = await db.execute(
3303
- sql9`SELECT * FROM ${tableRaw()}${where} ORDER BY ${sortCol} ${sortDir} LIMIT ${per_page} OFFSET ${offset}`
3212
+ sql8`SELECT * FROM ${tableRaw()}${where} ORDER BY ${sortCol} ${sortDir} LIMIT ${per_page} OFFSET ${offset}`
3304
3213
  );
3305
3214
  const rows = dataResult.rows;
3306
3215
  const cache = /* @__PURE__ */ new Map();
@@ -3321,7 +3230,7 @@ function createDrizzleContentRepository(db, tableName, options = {}) {
3321
3230
  },
3322
3231
  async findOne(id, include = []) {
3323
3232
  const result = await db.execute(
3324
- sql9`SELECT * FROM ${tableRaw()} WHERE id = ${id} LIMIT 1`
3233
+ sql8`SELECT * FROM ${tableRaw()} WHERE id = ${id} LIMIT 1`
3325
3234
  );
3326
3235
  if (result.rows.length === 0) return null;
3327
3236
  const rows = result.rows;
@@ -3331,7 +3240,7 @@ function createDrizzleContentRepository(db, tableName, options = {}) {
3331
3240
  },
3332
3241
  async findBySlug(slug, include = []) {
3333
3242
  const result = await db.execute(
3334
- sql9`SELECT * FROM ${tableRaw()} WHERE slug = ${slug} LIMIT 1`
3243
+ sql8`SELECT * FROM ${tableRaw()} WHERE slug = ${slug} LIMIT 1`
3335
3244
  );
3336
3245
  if (result.rows.length === 0) return null;
3337
3246
  const rows = result.rows;
@@ -3347,10 +3256,10 @@ function createDrizzleContentRepository(db, tableName, options = {}) {
3347
3256
  updated_at: /* @__PURE__ */ new Date()
3348
3257
  };
3349
3258
  const entries = Object.entries(record).filter(([, v]) => v !== void 0);
3350
- const colsSql = sql9.join(entries.map(([k]) => sql9.raw(quoteIdent4(k))), sql9`, `);
3351
- const valsSql = sql9.join(entries.map(([, v]) => sql9`${v}`), sql9`, `);
3259
+ const colsSql = sql8.join(entries.map(([k]) => sql8.raw(quoteIdent3(k))), sql8`, `);
3260
+ const valsSql = sql8.join(entries.map(([, v]) => sql8`${v}`), sql8`, `);
3352
3261
  const result = await db.execute(
3353
- sql9`INSERT INTO ${tableRaw()} (${colsSql}) VALUES (${valsSql}) RETURNING *`
3262
+ sql8`INSERT INTO ${tableRaw()} (${colsSql}) VALUES (${valsSql}) RETURNING *`
3354
3263
  );
3355
3264
  return result.rows[0];
3356
3265
  },
@@ -3363,25 +3272,25 @@ function createDrizzleContentRepository(db, tableName, options = {}) {
3363
3272
  if (entries.length === 0) {
3364
3273
  return this.findOne(id);
3365
3274
  }
3366
- const setClauses = sql9.join(
3367
- entries.map(([k, v]) => sql9`${sql9.raw(quoteIdent4(k))} = ${v}`),
3368
- sql9`, `
3275
+ const setClauses = sql8.join(
3276
+ entries.map(([k, v]) => sql8`${sql8.raw(quoteIdent3(k))} = ${v}`),
3277
+ sql8`, `
3369
3278
  );
3370
3279
  const result = await db.execute(
3371
- sql9`UPDATE ${tableRaw()} SET ${setClauses} WHERE id = ${id} RETURNING *`
3280
+ sql8`UPDATE ${tableRaw()} SET ${setClauses} WHERE id = ${id} RETURNING *`
3372
3281
  );
3373
3282
  if (result.rows.length === 0) return null;
3374
3283
  return result.rows[0];
3375
3284
  },
3376
3285
  async delete(id) {
3377
- await db.execute(sql9`DELETE FROM ${tableRaw()} WHERE id = ${id}`);
3286
+ await db.execute(sql8`DELETE FROM ${tableRaw()} WHERE id = ${id}`);
3378
3287
  },
3379
3288
  async findAll(opts = {}) {
3380
3289
  const { include = [], published_only } = opts;
3381
3290
  const conds = buildConditions(published_only, void 0, void 0);
3382
3291
  const where = whereFragment(conds);
3383
3292
  const result = await db.execute(
3384
- sql9`SELECT * FROM ${tableRaw()}${where} ORDER BY created_at ASC`
3293
+ sql8`SELECT * FROM ${tableRaw()}${where} ORDER BY created_at ASC`
3385
3294
  );
3386
3295
  const rows = result.rows;
3387
3296
  const cache = /* @__PURE__ */ new Map();
@@ -3393,23 +3302,23 @@ function createDrizzleContentRepository(db, tableName, options = {}) {
3393
3302
 
3394
3303
  // src/repositories/media.ts
3395
3304
  import crypto2 from "crypto";
3396
- import { sql as sql10 } from "drizzle-orm";
3305
+ import { sql as sql9 } from "drizzle-orm";
3397
3306
  function createMediaRepository(db) {
3398
- const TABLE = sql10.raw('"media"');
3307
+ const TABLE = sql9.raw('"media"');
3399
3308
  return {
3400
3309
  async findMany(opts = {}) {
3401
3310
  const { page = 1, per_page = 10, type, orphaned } = opts;
3402
3311
  const offset = (page - 1) * per_page;
3403
3312
  const conds = [];
3404
- if (type) conds.push(sql10`type = ${type}`);
3405
- if (orphaned) conds.push(sql10`reference_count = 0`);
3406
- const where = conds.length > 0 ? sql10` WHERE ${sql10.join(conds, sql10` AND `)}` : sql10``;
3407
- const countResult = await db.execute(sql10`SELECT COUNT(*) AS total FROM ${TABLE}${where}`);
3313
+ if (type) conds.push(sql9`type = ${type}`);
3314
+ if (orphaned) conds.push(sql9`reference_count = 0`);
3315
+ const where = conds.length > 0 ? sql9` WHERE ${sql9.join(conds, sql9` AND `)}` : sql9``;
3316
+ const countResult = await db.execute(sql9`SELECT COUNT(*) AS total FROM ${TABLE}${where}`);
3408
3317
  const total = Number(
3409
3318
  countResult.rows[0]?.["total"] ?? 0
3410
3319
  );
3411
3320
  const dataResult = await db.execute(
3412
- sql10`SELECT * FROM ${TABLE}${where} ORDER BY created_at DESC LIMIT ${per_page} OFFSET ${offset}`
3321
+ sql9`SELECT * FROM ${TABLE}${where} ORDER BY created_at DESC LIMIT ${per_page} OFFSET ${offset}`
3413
3322
  );
3414
3323
  const total_pages = Math.ceil(total / per_page);
3415
3324
  return {
@@ -3427,7 +3336,7 @@ function createMediaRepository(db) {
3427
3336
  },
3428
3337
  async findOne(id) {
3429
3338
  const result = await db.execute(
3430
- sql10`SELECT * FROM ${TABLE} WHERE id = ${id} LIMIT 1`
3339
+ sql9`SELECT * FROM ${TABLE} WHERE id = ${id} LIMIT 1`
3431
3340
  );
3432
3341
  if (result.rows.length === 0) return null;
3433
3342
  return result.rows[0];
@@ -3441,10 +3350,10 @@ function createMediaRepository(db) {
3441
3350
  updated_at: /* @__PURE__ */ new Date()
3442
3351
  };
3443
3352
  const entries = Object.entries(record).filter(([, v]) => v !== void 0);
3444
- const colsSql = sql10.join(entries.map(([k]) => sql10.raw(`"${k}"`)), sql10`, `);
3445
- const valsSql = sql10.join(entries.map(([, v]) => sql10`${v}`), sql10`, `);
3353
+ const colsSql = sql9.join(entries.map(([k]) => sql9.raw(`"${k}"`)), sql9`, `);
3354
+ const valsSql = sql9.join(entries.map(([, v]) => sql9`${v}`), sql9`, `);
3446
3355
  const result = await db.execute(
3447
- sql10`INSERT INTO ${TABLE} (${colsSql}) VALUES (${valsSql}) RETURNING *`
3356
+ sql9`INSERT INTO ${TABLE} (${colsSql}) VALUES (${valsSql}) RETURNING *`
3448
3357
  );
3449
3358
  return result.rows[0];
3450
3359
  },
@@ -3454,31 +3363,31 @@ function createMediaRepository(db) {
3454
3363
  ([k, v]) => k !== "id" && v !== void 0
3455
3364
  );
3456
3365
  if (entries.length === 0) return this.findOne(id);
3457
- const setClauses = sql10.join(
3458
- entries.map(([k, v]) => sql10`${sql10.raw(`"${k}"`)} = ${v}`),
3459
- sql10`, `
3366
+ const setClauses = sql9.join(
3367
+ entries.map(([k, v]) => sql9`${sql9.raw(`"${k}"`)} = ${v}`),
3368
+ sql9`, `
3460
3369
  );
3461
3370
  const result = await db.execute(
3462
- sql10`UPDATE ${TABLE} SET ${setClauses} WHERE id = ${id} RETURNING *`
3371
+ sql9`UPDATE ${TABLE} SET ${setClauses} WHERE id = ${id} RETURNING *`
3463
3372
  );
3464
3373
  if (result.rows.length === 0) return null;
3465
3374
  return result.rows[0];
3466
3375
  },
3467
3376
  async delete(id) {
3468
- await db.execute(sql10`DELETE FROM ${TABLE} WHERE id = ${id}`);
3377
+ await db.execute(sql9`DELETE FROM ${TABLE} WHERE id = ${id}`);
3469
3378
  },
3470
3379
  async incrementReferenceCount(ids) {
3471
3380
  if (ids.length === 0) return;
3472
- const inList = sql10.join(ids.map((id) => sql10`${id}`), sql10`, `);
3381
+ const inList = sql9.join(ids.map((id) => sql9`${id}`), sql9`, `);
3473
3382
  await db.execute(
3474
- sql10`UPDATE ${TABLE} SET reference_count = reference_count + 1, updated_at = NOW() WHERE id IN (${inList})`
3383
+ sql9`UPDATE ${TABLE} SET reference_count = reference_count + 1, updated_at = NOW() WHERE id IN (${inList})`
3475
3384
  );
3476
3385
  },
3477
3386
  async decrementReferenceCount(ids) {
3478
3387
  if (ids.length === 0) return;
3479
- const inList = sql10.join(ids.map((id) => sql10`${id}`), sql10`, `);
3388
+ const inList = sql9.join(ids.map((id) => sql9`${id}`), sql9`, `);
3480
3389
  await db.execute(
3481
- sql10`UPDATE ${TABLE} SET reference_count = GREATEST(reference_count - 1, 0), updated_at = NOW() WHERE id IN (${inList})`
3390
+ sql9`UPDATE ${TABLE} SET reference_count = GREATEST(reference_count - 1, 0), updated_at = NOW() WHERE id IN (${inList})`
3482
3391
  );
3483
3392
  }
3484
3393
  };
@@ -3611,7 +3520,7 @@ function createCmsApp(options) {
3611
3520
  const requirePermission = createPermissionMiddleware(rolesRegistry);
3612
3521
  const getUserRole = async (userId) => {
3613
3522
  const result = await db.execute(
3614
- sql11`SELECT r.name AS role
3523
+ sql10`SELECT r.name AS role
3615
3524
  FROM users u
3616
3525
  JOIN roles r ON r.id = u.role_id
3617
3526
  WHERE u.id = ${userId}
@@ -3781,7 +3690,7 @@ function createCmsApp(options) {
3781
3690
  handler: null,
3782
3691
  error: null
3783
3692
  }));
3784
- const ready = import("./handler-MFF32JB4.mjs").then(({ createGraphQLHandler }) => {
3693
+ const ready = import("./handler-5OTVPIAF.mjs").then(({ createGraphQLHandler }) => {
3785
3694
  gqlPasses.forEach((pass, i) => {
3786
3695
  const mount = mounts[i];
3787
3696
  try {