@bobbykim/manguito-cms-api 0.3.3 → 0.4.0

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,11 +1,32 @@
1
1
  import {
2
2
  buildRelationsMap,
3
+ createFieldKeyMap,
3
4
  deleteParagraphField,
5
+ isColumnBacked,
4
6
  persistJunctionField,
5
7
  persistParagraphField,
6
8
  resolveRelationBareIds,
7
9
  resolveRelationField
8
- } from "./chunk-TK4DNRFF.mjs";
10
+ } from "./chunk-4F2CRXPG.mjs";
11
+
12
+ // src/paths.ts
13
+ function normalizePrefix(prefix) {
14
+ if (prefix === void 0) return "/api";
15
+ const withSlash = prefix.startsWith("/") ? prefix : `/${prefix}`;
16
+ const trimmed = withSlash.replace(/\/+$/, "");
17
+ return trimmed === "" ? "/api" : trimmed;
18
+ }
19
+ function createPublicPaths(prefix) {
20
+ return {
21
+ collection: (basePath) => `${prefix}/${basePath}`,
22
+ item: (basePath) => `${prefix}/${basePath}/:slug`,
23
+ taxonomyCollection: (typeName) => `${prefix}/taxonomy/${typeName}`,
24
+ taxonomyItem: (typeName) => `${prefix}/taxonomy/${typeName}/:id`,
25
+ mediaCollection: () => `${prefix}/media`,
26
+ mediaItem: () => `${prefix}/media/:id`,
27
+ openapi: () => `${prefix}/openapi.json`
28
+ };
29
+ }
9
30
 
10
31
  // src/app.ts
11
32
  import { Hono } from "hono";
@@ -605,28 +626,31 @@ function parseInclude(includeParam) {
605
626
  if (!includeParam) return [];
606
627
  return includeParam.split(",").map((s) => s.trim()).filter(Boolean);
607
628
  }
608
- function parseFilters(url, validFields) {
629
+ function parseFilters(url, validFields, columnFor) {
609
630
  const { searchParams } = new URL(url);
610
631
  const filters = {};
632
+ const toKey = (label) => columnFor?.(label) ?? label;
611
633
  for (const [key, value] of searchParams.entries()) {
612
634
  const simpleMatch = /^filter\[([^\]]+)\]$/.exec(key);
613
635
  const opMatch = /^filter\[([^\]]+)\]\[([^\]]+)\]$/.exec(key);
614
636
  if (simpleMatch) {
615
637
  const field = simpleMatch[1];
616
638
  if (!validFields.has(field)) return { ok: false, invalidField: field };
617
- const existing = filters[field];
639
+ const storageKey = toKey(field);
640
+ const existing = filters[storageKey];
618
641
  if (existing !== void 0) {
619
- filters[field] = Array.isArray(existing) ? [...existing, value] : [existing, value];
642
+ filters[storageKey] = Array.isArray(existing) ? [...existing, value] : [existing, value];
620
643
  } else {
621
- filters[field] = value;
644
+ filters[storageKey] = value;
622
645
  }
623
646
  } else if (opMatch) {
624
647
  const field = opMatch[1];
625
648
  const operator = opMatch[2];
626
649
  if (!validFields.has(field)) return { ok: false, invalidField: field };
627
650
  if (!["gt", "gte", "lt", "lte"].includes(operator)) continue;
628
- const existing = filters[field] ?? {};
629
- filters[field] = { ...existing, [operator]: value };
651
+ const storageKey = toKey(field);
652
+ const existing = filters[storageKey] ?? {};
653
+ filters[storageKey] = { ...existing, [operator]: value };
630
654
  }
631
655
  }
632
656
  return { ok: true, filters };
@@ -636,7 +660,7 @@ function parseFilters(url, validFields) {
636
660
  function isPublished(item) {
637
661
  return item["published"] === true;
638
662
  }
639
- function registerPublicContentRoutes(app, registry, repos, listRateLimit, resolver) {
663
+ function registerPublicContentRoutes(app, registry, repos, fieldKeyMaps, paths, listRateLimit, resolver) {
640
664
  function registerListRoute(path, handler) {
641
665
  if (listRateLimit) {
642
666
  app.get(path, listRateLimit, handler);
@@ -644,7 +668,7 @@ function registerPublicContentRoutes(app, registry, repos, listRateLimit, resolv
644
668
  app.get(path, handler);
645
669
  }
646
670
  }
647
- registerListRoute("/api/content", (c) => {
671
+ registerListRoute(paths.collection("content"), (c) => {
648
672
  const data = Object.values(registry.content_types).map((ct) => ({
649
673
  name: ct.name,
650
674
  label: ct.label,
@@ -652,7 +676,7 @@ function registerPublicContentRoutes(app, registry, repos, listRateLimit, resolv
652
676
  }));
653
677
  return c.json({ ok: true, data });
654
678
  });
655
- registerListRoute("/api/taxonomy", (c) => {
679
+ registerListRoute(paths.collection("taxonomy"), (c) => {
656
680
  const data = Object.values(registry.taxonomy_types).map((tt) => ({
657
681
  name: tt.name,
658
682
  label: tt.label
@@ -671,7 +695,7 @@ function registerPublicContentRoutes(app, registry, repos, listRateLimit, resolv
671
695
  ...contentType.system_fields.map((f) => f.name)
672
696
  ]);
673
697
  if (contentType.only_one) {
674
- app.get(`/api/${basePath}`, async (c) => {
698
+ app.get(paths.collection(basePath), async (c) => {
675
699
  const result = await repo.findMany({ published_only: true, page: 1, per_page: 1 });
676
700
  if (result.data.length === 0) {
677
701
  return c.json(
@@ -679,12 +703,13 @@ function registerPublicContentRoutes(app, registry, repos, listRateLimit, resolv
679
703
  404
680
704
  );
681
705
  }
682
- let data = result.data[0];
706
+ let data = fieldKeyMaps[typeName].toLabels(result.data[0]);
683
707
  if (resolver?.hasSchema(typeName)) data = await resolver.resolveItem(typeName, data);
684
708
  return c.json({ ok: true, data });
685
709
  });
686
710
  } else {
687
- registerListRoute(`/api/${basePath}`, async (c) => {
711
+ registerListRoute(paths.collection(basePath), async (c) => {
712
+ const fieldKeys = fieldKeyMaps[typeName];
688
713
  const pagination = parsePagination(c.req.query("page"), c.req.query("per_page"));
689
714
  if (!pagination.ok) {
690
715
  return c.json(
@@ -724,7 +749,7 @@ function registerPublicContentRoutes(app, registry, repos, listRateLimit, resolv
724
749
  400
725
750
  );
726
751
  }
727
- const filtersResult = parseFilters(c.req.url, filterableFieldNames);
752
+ const filtersResult = parseFilters(c.req.url, filterableFieldNames, fieldKeys.columnFor);
728
753
  if (!filtersResult.ok) {
729
754
  return c.json(
730
755
  {
@@ -761,13 +786,13 @@ function registerPublicContentRoutes(app, registry, repos, listRateLimit, resolv
761
786
  filters: filtersResult.filters,
762
787
  include
763
788
  });
764
- if (resolver?.hasSchema(typeName)) {
765
- const resolved = await resolver.resolveList(typeName, result.data);
766
- return c.json({ ...result, data: resolved });
767
- }
768
- return c.json(result);
789
+ const labeled = result.data.map(
790
+ (row) => fieldKeys.toLabels(row)
791
+ );
792
+ const data = resolver?.hasSchema(typeName) ? await resolver.resolveList(typeName, labeled) : labeled;
793
+ return c.json({ ...result, data });
769
794
  });
770
- app.get(`/api/${basePath}/:slug`, async (c) => {
795
+ app.get(paths.item(basePath), async (c) => {
771
796
  const slug = c.req.param("slug");
772
797
  const include = parseInclude(c.req.query("include"));
773
798
  for (const field of include) {
@@ -794,7 +819,7 @@ function registerPublicContentRoutes(app, registry, repos, listRateLimit, resolv
794
819
  404
795
820
  );
796
821
  }
797
- let data = item;
822
+ let data = fieldKeyMaps[typeName].toLabels(item);
798
823
  if (resolver?.hasSchema(typeName)) data = await resolver.resolveItem(typeName, data);
799
824
  return c.json({ ok: true, data });
800
825
  });
@@ -803,7 +828,7 @@ function registerPublicContentRoutes(app, registry, repos, listRateLimit, resolv
803
828
  for (const [typeName] of Object.entries(registry.taxonomy_types)) {
804
829
  const repo = repos[typeName];
805
830
  if (!repo) continue;
806
- registerListRoute(`/api/taxonomy/${typeName}`, async (c) => {
831
+ registerListRoute(paths.taxonomyCollection(typeName), async (c) => {
807
832
  const pagination = parsePagination(c.req.query("page"), c.req.query("per_page"));
808
833
  if (!pagination.ok) {
809
834
  return c.json(
@@ -822,13 +847,14 @@ function registerPublicContentRoutes(app, registry, repos, listRateLimit, resolv
822
847
  page: pagination.page,
823
848
  per_page: pagination.per_page
824
849
  });
825
- if (resolver?.hasSchema(typeName)) {
826
- const resolved = await resolver.resolveList(typeName, result.data);
827
- return c.json({ ...result, data: resolved });
828
- }
829
- return c.json(result);
850
+ const fieldKeys = fieldKeyMaps[typeName];
851
+ const labeled = result.data.map(
852
+ (row) => fieldKeys.toLabels(row)
853
+ );
854
+ const data = resolver?.hasSchema(typeName) ? await resolver.resolveList(typeName, labeled) : labeled;
855
+ return c.json({ ...result, data });
830
856
  });
831
- app.get(`/api/taxonomy/${typeName}/:id`, async (c) => {
857
+ app.get(paths.taxonomyItem(typeName), async (c) => {
832
858
  const id = c.req.param("id");
833
859
  const item = await repo.findOne(id);
834
860
  if (!item || !isPublished(item)) {
@@ -840,7 +866,7 @@ function registerPublicContentRoutes(app, registry, repos, listRateLimit, resolv
840
866
  404
841
867
  );
842
868
  }
843
- let data = item;
869
+ let data = fieldKeyMaps[typeName].toLabels(item);
844
870
  if (resolver?.hasSchema(typeName)) data = await resolver.resolveItem(typeName, data);
845
871
  return c.json({ ok: true, data });
846
872
  });
@@ -856,7 +882,7 @@ function parsePagination2(pageStr, perPageStr) {
856
882
  if (!Number.isInteger(per_page) || per_page < 1 || per_page > 100) return { ok: false };
857
883
  return { ok: true, page, per_page };
858
884
  }
859
- function registerPublicMediaRoutes(app, mediaRepo, listRateLimit) {
885
+ function registerPublicMediaRoutes(app, mediaRepo, paths, listRateLimit) {
860
886
  const mediaListHandler = async (c) => {
861
887
  const pagination = parsePagination2(c.req.query("page"), c.req.query("per_page"));
862
888
  if (!pagination.ok) {
@@ -895,11 +921,11 @@ function registerPublicMediaRoutes(app, mediaRepo, listRateLimit) {
895
921
  return c.json(result);
896
922
  };
897
923
  if (listRateLimit) {
898
- app.get("/api/media", listRateLimit, mediaListHandler);
924
+ app.get(paths.mediaCollection(), listRateLimit, mediaListHandler);
899
925
  } else {
900
- app.get("/api/media", mediaListHandler);
926
+ app.get(paths.mediaCollection(), mediaListHandler);
901
927
  }
902
- app.get("/api/media/:id", async (c) => {
928
+ app.get(paths.mediaItem(), async (c) => {
903
929
  const id = c.req.param("id");
904
930
  const item = await mediaRepo.findOne(id);
905
931
  if (!item) {
@@ -923,9 +949,11 @@ function topLevelMediaDelta(mediaFields, before, after) {
923
949
  const added = [];
924
950
  const removed = [];
925
951
  for (const f of mediaFields) {
926
- if (after !== null && !(f.name in after)) continue;
927
- const oldId = before ? extractMediaId(before[f.name]) : null;
928
- const newId = after ? extractMediaId(after[f.name]) : null;
952
+ const key = f.db_column?.column_name;
953
+ if (key === void 0 || key === "") continue;
954
+ if (after !== null && !(key in after)) continue;
955
+ const oldId = before ? extractMediaId(before[key]) : null;
956
+ const newId = after ? extractMediaId(after[key]) : null;
929
957
  if (newId === oldId) continue;
930
958
  if (oldId) removed.push(oldId);
931
959
  if (newId) added.push(newId);
@@ -985,7 +1013,7 @@ function isEmpty(value) {
985
1013
  function checkRequiredFields(fields, data) {
986
1014
  return fields.filter((f) => f.required && isEmpty(data[f.name])).map((f) => ({ field: f.name, message: `${f.label} is required` }));
987
1015
  }
988
- function parseListQuery(url, schemaFieldNames, relationFieldNames) {
1016
+ function parseListQuery(url, schemaFieldNames, relationFieldNames, columnFor) {
989
1017
  const searchParams = new URL(url).searchParams;
990
1018
  const pagination = parsePagination(
991
1019
  searchParams.get("page") ?? void 0,
@@ -1020,7 +1048,7 @@ function parseListQuery(url, schemaFieldNames, relationFieldNames) {
1020
1048
  status: 400
1021
1049
  };
1022
1050
  }
1023
- const filtersResult = parseFilters(url, schemaFieldNames);
1051
+ const filtersResult = parseFilters(url, schemaFieldNames, columnFor);
1024
1052
  if (!filtersResult.ok) {
1025
1053
  return {
1026
1054
  ok: false,
@@ -1052,7 +1080,7 @@ function parseListQuery(url, schemaFieldNames, relationFieldNames) {
1052
1080
  search
1053
1081
  };
1054
1082
  }
1055
- function registerAdminContentRoutes(app, registry, repos, mediaRepo, requirePermission, db) {
1083
+ function registerAdminContentRoutes(app, registry, repos, fieldKeyMaps, mediaRepo, requirePermission, db) {
1056
1084
  for (const [typeName, contentType] of Object.entries(registry.content_types)) {
1057
1085
  const basePath = `content/${typeName}`;
1058
1086
  const repo = repos[typeName];
@@ -1064,6 +1092,7 @@ function registerAdminContentRoutes(app, registry, repos, mediaRepo, requirePerm
1064
1092
  const relationFieldNames = new Set(
1065
1093
  contentType.fields.filter((f) => RELATION_FIELD_TYPES.has(f.field_type)).map((f) => f.name)
1066
1094
  );
1095
+ const fieldKeys = fieldKeyMaps[typeName];
1067
1096
  const requiredFields = contentType.fields.filter((f) => f.required);
1068
1097
  const mediaFields = contentType.fields.filter(
1069
1098
  (f) => f.field_type === "image" || f.field_type === "video" || f.field_type === "file"
@@ -1077,7 +1106,7 @@ function registerAdminContentRoutes(app, registry, repos, mediaRepo, requirePerm
1077
1106
  `/admin/api/${basePath}`,
1078
1107
  requirePermission("content:read"),
1079
1108
  async (c) => {
1080
- const parsed = parseListQuery(c.req.url, schemaFieldNames, relationFieldNames);
1109
+ const parsed = parseListQuery(c.req.url, schemaFieldNames, relationFieldNames, fieldKeys.columnFor);
1081
1110
  if (!parsed.ok) {
1082
1111
  return c.json({ ok: false, error: parsed.response }, parsed.status);
1083
1112
  }
@@ -1097,7 +1126,8 @@ function registerAdminContentRoutes(app, registry, repos, mediaRepo, requirePerm
1097
1126
  findOpts.search = { term: parsed.search, columns: searchableColumns };
1098
1127
  }
1099
1128
  const result = await repo.findMany(findOpts);
1100
- return c.json(result);
1129
+ const data = result.data.map((row) => fieldKeys.toLabels(row));
1130
+ return c.json({ ...result, data });
1101
1131
  }
1102
1132
  );
1103
1133
  app.get(
@@ -1132,7 +1162,8 @@ function registerAdminContentRoutes(app, registry, repos, mediaRepo, requirePerm
1132
1162
  }
1133
1163
  }
1134
1164
  }
1135
- return c.json({ ok: true, data: item });
1165
+ const data = fieldKeys.toLabels(item);
1166
+ return c.json({ ok: true, data });
1136
1167
  }
1137
1168
  );
1138
1169
  app.post(
@@ -1199,6 +1230,7 @@ function registerAdminContentRoutes(app, registry, repos, mediaRepo, requirePerm
1199
1230
  }
1200
1231
  );
1201
1232
  const writeNewItem = async (c, body) => {
1233
+ const storageBody = fieldKeys.toStorage(body);
1202
1234
  const fieldErrors = checkRequiredFields(requiredFields, body);
1203
1235
  if (fieldErrors.length > 0) {
1204
1236
  return c.json(
@@ -1222,9 +1254,7 @@ function registerAdminContentRoutes(app, registry, repos, mediaRepo, requirePerm
1222
1254
  const junctionFields = contentType.fields.filter(
1223
1255
  (f) => f.db_column !== null && f.db_column.junction !== void 0
1224
1256
  );
1225
- const columnFields = contentType.fields.filter(
1226
- (f) => f.db_column !== null && f.db_column.junction === void 0
1227
- );
1257
+ const columnBackedFields = contentType.fields.filter(isColumnBacked);
1228
1258
  const columnData = {
1229
1259
  published: body["published"] ?? false
1230
1260
  };
@@ -1233,9 +1263,10 @@ function registerAdminContentRoutes(app, registry, repos, mediaRepo, requirePerm
1233
1263
  } else {
1234
1264
  columnData["slug"] = body["slug"];
1235
1265
  }
1236
- for (const f of columnFields) {
1237
- const val = body[f.name];
1238
- columnData[f.db_column.column_name] = val !== void 0 ? val : null;
1266
+ for (const f of columnBackedFields) {
1267
+ const key = f.db_column.column_name;
1268
+ if (!(key in storageBody)) continue;
1269
+ columnData[key] = storageBody[key];
1239
1270
  }
1240
1271
  if (db) {
1241
1272
  const basePathId = await lookupBasePathId(db, contentType.default_base_path);
@@ -1255,7 +1286,7 @@ function registerAdminContentRoutes(app, registry, repos, mediaRepo, requirePerm
1255
1286
  }
1256
1287
  const item = await repo.create(columnData);
1257
1288
  const itemId = item["id"];
1258
- const mediaDeltas = [topLevelMediaDelta(mediaFields, null, body)];
1289
+ const mediaDeltas = [topLevelMediaDelta(mediaFields, null, storageBody)];
1259
1290
  if (db) {
1260
1291
  for (const f of paragraphFields) {
1261
1292
  const comp = f.ui_component;
@@ -1272,7 +1303,8 @@ function registerAdminContentRoutes(app, registry, repos, mediaRepo, requirePerm
1272
1303
  }
1273
1304
  }
1274
1305
  await applyMediaReferenceDelta(mergeMediaDeltas(...mediaDeltas), mediaRepo);
1275
- return c.json({ ok: true, data: item }, 201);
1306
+ const data = fieldKeys.toLabels(item);
1307
+ return c.json({ ok: true, data }, 201);
1276
1308
  };
1277
1309
  app.patch(
1278
1310
  `/admin/api/${basePath}/:id`,
@@ -1332,11 +1364,12 @@ function registerAdminContentRoutes(app, registry, repos, mediaRepo, requirePerm
1332
1364
  }
1333
1365
  );
1334
1366
  const writeExistingItem = async (c, id, existing, body) => {
1367
+ const storageBody = fieldKeys.toStorage(body);
1335
1368
  if (body["published"] === true) {
1336
1369
  const publishDeny = await requirePermission("content:edit")(c, async () => {
1337
1370
  });
1338
1371
  if (publishDeny) return publishDeny;
1339
- const merged = { ...existing, ...body };
1372
+ const merged = { ...fieldKeys.toLabels(existing), ...body };
1340
1373
  const fieldErrors = checkRequiredFields(requiredFields, merged);
1341
1374
  if (fieldErrors.length > 0) {
1342
1375
  return c.json(
@@ -1356,17 +1389,14 @@ function registerAdminContentRoutes(app, registry, repos, mediaRepo, requirePerm
1356
1389
  const patchJunctionFields = contentType.fields.filter(
1357
1390
  (f) => f.db_column !== null && f.db_column.junction !== void 0
1358
1391
  );
1359
- const patchColumnFields = contentType.fields.filter(
1360
- (f) => f.db_column !== null && f.db_column.junction === void 0
1361
- );
1392
+ const patchColumnBackedFields = contentType.fields.filter(isColumnBacked);
1362
1393
  const patchData = {};
1363
1394
  if (!contentType.only_one && "slug" in body) patchData["slug"] = body["slug"];
1364
1395
  if ("published" in body) patchData["published"] = body["published"];
1365
- for (const f of patchColumnFields) {
1366
- if (f.name in body) {
1367
- const val = body[f.name];
1368
- patchData[f.db_column.column_name] = val !== void 0 ? val : null;
1369
- }
1396
+ for (const f of patchColumnBackedFields) {
1397
+ const key = f.db_column.column_name;
1398
+ if (!(key in storageBody)) continue;
1399
+ patchData[key] = storageBody[key];
1370
1400
  }
1371
1401
  const updated = await repo.update(id, patchData);
1372
1402
  if (!updated) {
@@ -1376,7 +1406,7 @@ function registerAdminContentRoutes(app, registry, repos, mediaRepo, requirePerm
1376
1406
  );
1377
1407
  }
1378
1408
  const mediaDeltas = [
1379
- topLevelMediaDelta(mediaFields, existing, body)
1409
+ topLevelMediaDelta(mediaFields, existing, storageBody)
1380
1410
  ];
1381
1411
  if (db) {
1382
1412
  for (const f of patchParagraphFields) {
@@ -1394,7 +1424,8 @@ function registerAdminContentRoutes(app, registry, repos, mediaRepo, requirePerm
1394
1424
  }
1395
1425
  }
1396
1426
  await applyMediaReferenceDelta(mergeMediaDeltas(...mediaDeltas), mediaRepo);
1397
- return c.json({ ok: true, data: updated });
1427
+ const data = fieldKeys.toLabels(updated);
1428
+ return c.json({ ok: true, data });
1398
1429
  };
1399
1430
  if (contentType.only_one) {
1400
1431
  app.put(
@@ -1454,6 +1485,7 @@ function registerAdminContentRoutes(app, registry, repos, mediaRepo, requirePerm
1454
1485
  const relationFieldNames = new Set(
1455
1486
  taxonomyType.fields.filter((f) => RELATION_FIELD_TYPES.has(f.field_type)).map((f) => f.name)
1456
1487
  );
1488
+ const fieldKeys = fieldKeyMaps[typeName];
1457
1489
  const requiredFields = taxonomyType.fields.filter((f) => f.required);
1458
1490
  const mediaFields = taxonomyType.fields.filter(
1459
1491
  (f) => f.field_type === "image" || f.field_type === "video" || f.field_type === "file"
@@ -1467,7 +1499,7 @@ function registerAdminContentRoutes(app, registry, repos, mediaRepo, requirePerm
1467
1499
  `/admin/api/taxonomy/${typeName}`,
1468
1500
  requirePermission("content:read"),
1469
1501
  async (c) => {
1470
- const parsed = parseListQuery(c.req.url, schemaFieldNames, relationFieldNames);
1502
+ const parsed = parseListQuery(c.req.url, schemaFieldNames, relationFieldNames, fieldKeys.columnFor);
1471
1503
  if (!parsed.ok) {
1472
1504
  return c.json({ ok: false, error: parsed.response }, parsed.status);
1473
1505
  }
@@ -1487,7 +1519,8 @@ function registerAdminContentRoutes(app, registry, repos, mediaRepo, requirePerm
1487
1519
  findOpts.search = { term: parsed.search, columns: searchableColumns };
1488
1520
  }
1489
1521
  const result = await repo.findMany(findOpts);
1490
- return c.json(result);
1522
+ const data = result.data.map((row) => fieldKeys.toLabels(row));
1523
+ return c.json({ ...result, data });
1491
1524
  }
1492
1525
  );
1493
1526
  app.get(
@@ -1502,7 +1535,8 @@ function registerAdminContentRoutes(app, registry, repos, mediaRepo, requirePerm
1502
1535
  404
1503
1536
  );
1504
1537
  }
1505
- return c.json({ ok: true, data: item });
1538
+ const data = fieldKeys.toLabels(item);
1539
+ return c.json({ ok: true, data });
1506
1540
  }
1507
1541
  );
1508
1542
  app.post(
@@ -1510,6 +1544,7 @@ function registerAdminContentRoutes(app, registry, repos, mediaRepo, requirePerm
1510
1544
  requirePermission("content:create"),
1511
1545
  async (c) => {
1512
1546
  const body = await c.req.json();
1547
+ const storageBody = fieldKeys.toStorage(body);
1513
1548
  const fieldErrors = checkRequiredFields(requiredFields, body);
1514
1549
  if (fieldErrors.length > 0) {
1515
1550
  return c.json(
@@ -1529,20 +1564,19 @@ function registerAdminContentRoutes(app, registry, repos, mediaRepo, requirePerm
1529
1564
  });
1530
1565
  if (publishDeny) return publishDeny;
1531
1566
  }
1532
- const taxColumnFields = taxonomyType.fields.filter(
1533
- (f) => f.db_column !== null && f.db_column.junction === void 0
1534
- );
1535
1567
  const taxParagraphFields = taxonomyType.fields.filter((f) => f.db_column === null);
1568
+ const taxColumnBackedFields = taxonomyType.fields.filter(isColumnBacked);
1536
1569
  const taxColumnData = {
1537
1570
  published: body["published"] ?? false
1538
1571
  };
1539
- for (const f of taxColumnFields) {
1540
- const val = body[f.name];
1541
- taxColumnData[f.db_column.column_name] = val !== void 0 ? val : null;
1572
+ for (const f of taxColumnBackedFields) {
1573
+ const key = f.db_column.column_name;
1574
+ if (!(key in storageBody)) continue;
1575
+ taxColumnData[key] = storageBody[key];
1542
1576
  }
1543
1577
  const item = await repo.create(taxColumnData);
1544
1578
  const taxItemId = item["id"];
1545
- const mediaDeltas = [topLevelMediaDelta(mediaFields, null, body)];
1579
+ const mediaDeltas = [topLevelMediaDelta(mediaFields, null, storageBody)];
1546
1580
  if (db) {
1547
1581
  for (const f of taxParagraphFields) {
1548
1582
  const comp = f.ui_component;
@@ -1554,7 +1588,8 @@ function registerAdminContentRoutes(app, registry, repos, mediaRepo, requirePerm
1554
1588
  }
1555
1589
  }
1556
1590
  await applyMediaReferenceDelta(mergeMediaDeltas(...mediaDeltas), mediaRepo);
1557
- return c.json({ ok: true, data: item }, 201);
1591
+ const data = fieldKeys.toLabels(item);
1592
+ return c.json({ ok: true, data }, 201);
1558
1593
  }
1559
1594
  );
1560
1595
  app.patch(
@@ -1563,6 +1598,7 @@ function registerAdminContentRoutes(app, registry, repos, mediaRepo, requirePerm
1563
1598
  async (c) => {
1564
1599
  const id = c.req.param("id");
1565
1600
  const body = await c.req.json();
1601
+ const storageBody = fieldKeys.toStorage(body);
1566
1602
  const existing = await repo.findOne(id);
1567
1603
  if (!existing) {
1568
1604
  return c.json(
@@ -1574,7 +1610,7 @@ function registerAdminContentRoutes(app, registry, repos, mediaRepo, requirePerm
1574
1610
  const publishDeny = await requirePermission("content:edit")(c, async () => {
1575
1611
  });
1576
1612
  if (publishDeny) return publishDeny;
1577
- const merged = { ...existing, ...body };
1613
+ const merged = { ...fieldKeys.toLabels(existing), ...body };
1578
1614
  const fieldErrors = checkRequiredFields(requiredFields, merged);
1579
1615
  if (fieldErrors.length > 0) {
1580
1616
  return c.json(
@@ -1590,17 +1626,14 @@ function registerAdminContentRoutes(app, registry, repos, mediaRepo, requirePerm
1590
1626
  );
1591
1627
  }
1592
1628
  }
1593
- const taxPatchColumnFields = taxonomyType.fields.filter(
1594
- (f) => f.db_column !== null && f.db_column.junction === void 0
1595
- );
1596
1629
  const taxPatchParagraphFields = taxonomyType.fields.filter((f) => f.db_column === null);
1630
+ const taxPatchColumnBackedFields = taxonomyType.fields.filter(isColumnBacked);
1597
1631
  const taxPatchData = {};
1598
1632
  if ("published" in body) taxPatchData["published"] = body["published"];
1599
- for (const f of taxPatchColumnFields) {
1600
- if (f.name in body) {
1601
- const val = body[f.name];
1602
- taxPatchData[f.db_column.column_name] = val !== void 0 ? val : null;
1603
- }
1633
+ for (const f of taxPatchColumnBackedFields) {
1634
+ const key = f.db_column.column_name;
1635
+ if (!(key in storageBody)) continue;
1636
+ taxPatchData[key] = storageBody[key];
1604
1637
  }
1605
1638
  const updated = await repo.update(id, taxPatchData);
1606
1639
  if (!updated) {
@@ -1610,7 +1643,7 @@ function registerAdminContentRoutes(app, registry, repos, mediaRepo, requirePerm
1610
1643
  );
1611
1644
  }
1612
1645
  const mediaDeltas = [
1613
- topLevelMediaDelta(mediaFields, existing, body)
1646
+ topLevelMediaDelta(mediaFields, existing, storageBody)
1614
1647
  ];
1615
1648
  if (db) {
1616
1649
  for (const f of taxPatchParagraphFields) {
@@ -1623,7 +1656,8 @@ function registerAdminContentRoutes(app, registry, repos, mediaRepo, requirePerm
1623
1656
  }
1624
1657
  }
1625
1658
  await applyMediaReferenceDelta(mergeMediaDeltas(...mediaDeltas), mediaRepo);
1626
- return c.json({ ok: true, data: updated });
1659
+ const data = fieldKeys.toLabels(updated);
1660
+ return c.json({ ok: true, data });
1627
1661
  }
1628
1662
  );
1629
1663
  app.delete(
@@ -2615,7 +2649,7 @@ function registerUserRoutes(app, db, requirePermission, requireHierarchy) {
2615
2649
  import { sql as sql5 } from "drizzle-orm";
2616
2650
  var API_VERSION = (() => {
2617
2651
  try {
2618
- return "0.3.3";
2652
+ return "0.4.0";
2619
2653
  } catch {
2620
2654
  return "0.0.0";
2621
2655
  }
@@ -3026,7 +3060,8 @@ function createCmsApp(options) {
3026
3060
  if (!options.storage) {
3027
3061
  throw new Error(MISSING_STORAGE_ERROR);
3028
3062
  }
3029
- const prefix = options.prefix ?? "/api";
3063
+ const prefix = normalizePrefix(options.prefix);
3064
+ const publicPaths = createPublicPaths(prefix);
3030
3065
  const { storage, registry, db, rateLimit, media, cors } = options;
3031
3066
  const cmsName = options.name ?? "Manguito CMS";
3032
3067
  const maxFileSize = media?.max_file_size;
@@ -3106,23 +3141,27 @@ function createCmsApp(options) {
3106
3141
  registerAuthRoutes(app, db);
3107
3142
  app.use("/admin/api/*", authMiddleware);
3108
3143
  app.use("/admin/api/*", mustChangePasswordCheck);
3109
- app.get("/api/openapi.json", (c) => {
3144
+ app.get(publicPaths.openapi(), (c) => {
3110
3145
  const paths = {};
3111
3146
  for (const ct of Object.values(registry.content_types)) {
3112
3147
  const base = ct.default_base_path;
3113
3148
  if (ct.only_one) {
3114
- paths[`/api/${base}`] = { get: { summary: `Get ${ct.label}`, tags: [ct.label] } };
3149
+ paths[publicPaths.collection(base)] = { get: { summary: `Get ${ct.label}`, tags: [ct.label] } };
3115
3150
  } else {
3116
- paths[`/api/${base}`] = { get: { summary: `List published ${ct.label}`, tags: [ct.label] } };
3117
- paths[`/api/${base}/{slug}`] = { get: { summary: `Get ${ct.label} by slug`, tags: [ct.label] } };
3151
+ paths[publicPaths.collection(base)] = { get: { summary: `List published ${ct.label}`, tags: [ct.label] } };
3152
+ paths[publicPaths.item(base).replace(":slug", "{slug}")] = {
3153
+ get: { summary: `Get ${ct.label} by slug`, tags: [ct.label] }
3154
+ };
3118
3155
  }
3119
3156
  }
3120
3157
  for (const tt of Object.values(registry.taxonomy_types)) {
3121
- paths[`/api/taxonomy/${tt.name}`] = { get: { summary: `List published ${tt.label}`, tags: [tt.label] } };
3122
- paths[`/api/taxonomy/${tt.name}/{id}`] = { get: { summary: `Get ${tt.label} by id`, tags: [tt.label] } };
3158
+ paths[publicPaths.taxonomyCollection(tt.name)] = { get: { summary: `List published ${tt.label}`, tags: [tt.label] } };
3159
+ paths[publicPaths.taxonomyItem(tt.name).replace(":id", "{id}")] = {
3160
+ get: { summary: `Get ${tt.label} by id`, tags: [tt.label] }
3161
+ };
3123
3162
  }
3124
- paths["/api/media"] = { get: { summary: "List media items", tags: ["Media"] } };
3125
- paths["/api/media/{id}"] = { get: { summary: "Get media item by id", tags: ["Media"] } };
3163
+ paths[publicPaths.mediaCollection()] = { get: { summary: "List media items", tags: ["Media"] } };
3164
+ paths[publicPaths.mediaItem().replace(":id", "{id}")] = { get: { summary: "Get media item by id", tags: ["Media"] } };
3126
3165
  return c.json({
3127
3166
  openapi: "3.0.3",
3128
3167
  info: { title: "Manguito CMS Public API", version: "1.0.0" },
@@ -3137,14 +3176,24 @@ function createCmsApp(options) {
3137
3176
  paths: {}
3138
3177
  })
3139
3178
  );
3140
- registerPublicContentRoutes(app, registry, publicRepos, listRateLimit, programmaticResolver);
3141
- registerPublicMediaRoutes(app, mediaRepo, listRateLimit);
3179
+ const fieldKeyMaps = Object.fromEntries([
3180
+ ...Object.entries(registry.content_types).map(([typeName, ct]) => [
3181
+ typeName,
3182
+ createFieldKeyMap(ct.fields)
3183
+ ]),
3184
+ ...Object.entries(registry.taxonomy_types).map(([typeName, tt]) => [
3185
+ typeName,
3186
+ createFieldKeyMap(tt.fields)
3187
+ ])
3188
+ ]);
3189
+ registerPublicContentRoutes(app, registry, publicRepos, fieldKeyMaps, publicPaths, listRateLimit, programmaticResolver);
3190
+ registerPublicMediaRoutes(app, mediaRepo, publicPaths, listRateLimit);
3142
3191
  if (options.graphql?.enabled) {
3143
3192
  const gqlOptions = options.graphql;
3144
3193
  let gqlHandler = null;
3145
3194
  let initError = null;
3146
- const ready = import("./handler-CGHZG3J5.mjs").then(({ createGraphQLHandler }) => {
3147
- gqlHandler = createGraphQLHandler(registry, graphqlRepos, programmaticResolver, db, gqlOptions);
3195
+ const ready = import("./handler-TPEULQ5W.mjs").then(({ createGraphQLHandler }) => {
3196
+ gqlHandler = createGraphQLHandler(registry, graphqlRepos, fieldKeyMaps, programmaticResolver, db, gqlOptions);
3148
3197
  }).catch((err) => {
3149
3198
  initError = err;
3150
3199
  const message = err instanceof Error ? err.message : String(err);
@@ -3184,7 +3233,7 @@ function createCmsApp(options) {
3184
3233
  );
3185
3234
  registerSchemaRoute(app, registry, db);
3186
3235
  registerUserRoutes(app, db, requirePermission, requireHierarchy);
3187
- registerAdminContentRoutes(app, registry, repos, mediaRepo, requirePermission, db);
3236
+ registerAdminContentRoutes(app, registry, repos, fieldKeyMaps, mediaRepo, requirePermission, db);
3188
3237
  registerAdminMediaRoutes(app, mediaRepo, storage, requirePermission, maxFileSize);
3189
3238
  return { prefix, app };
3190
3239
  }
@@ -3225,7 +3274,7 @@ function resolveGraphQL(cfg) {
3225
3274
  };
3226
3275
  }
3227
3276
  function createAPIAdapter(options = {}) {
3228
- const prefix = options.prefix ?? "/api";
3277
+ const prefix = normalizePrefix(options.prefix);
3229
3278
  const media = options.media?.max_file_size !== void 0 ? { max_file_size: options.media.max_file_size } : void 0;
3230
3279
  return {
3231
3280
  prefix,