@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.js CHANGED
@@ -200,22 +200,34 @@ function groupBy(items, key) {
200
200
  }
201
201
  return result;
202
202
  }
203
+ function needsFkResolution(row, fkColumn, fieldName) {
204
+ if (fkColumn !== fieldName && !(fkColumn in row)) return false;
205
+ const current = row[fieldName];
206
+ return !(typeof current === "object" && current !== null);
207
+ }
203
208
  async function resolveRelationField(db, rows, fieldName, rel, cache, publishedOnly = false) {
204
209
  if (rows.length === 0) return;
210
+ const batch = [...new Set(rows)];
205
211
  if (rel.type === "paragraph") {
206
- const parentIds = rows.map((r) => r["id"]);
212
+ const parentIds = batch.map((r) => r["id"]);
207
213
  const inList = import_drizzle_orm2.sql.join(parentIds.map((id) => import_drizzle_orm2.sql`${id}`), import_drizzle_orm2.sql`, `);
208
214
  const result = await db.execute(
209
215
  import_drizzle_orm2.sql`SELECT * FROM ${import_drizzle_orm2.sql.raw(quoteIdent(rel.table))} WHERE parent_id IN (${inList}) AND parent_field = ${fieldName} ORDER BY "order" ASC`
210
216
  );
211
217
  const byParent = groupBy(result.rows, "parent_id");
212
- for (const row of rows) {
218
+ for (const row of batch) {
213
219
  row[fieldName] = byParent[row["id"]] ?? [];
214
220
  }
215
221
  } else if (rel.type === "reference") {
216
- const fkValues = rows.map((r) => r[rel.fk_column]).filter(Boolean);
222
+ const dropFk = rel.fk_column !== fieldName;
223
+ const pending = batch.filter((r) => needsFkResolution(r, rel.fk_column, fieldName));
224
+ if (pending.length === 0) return;
225
+ const fkValues = pending.map((r) => r[rel.fk_column]).filter(Boolean);
217
226
  if (fkValues.length === 0) {
218
- for (const row of rows) row[fieldName] = null;
227
+ for (const row of pending) {
228
+ row[fieldName] = null;
229
+ if (dropFk) delete row[rel.fk_column];
230
+ }
219
231
  return;
220
232
  }
221
233
  const unique = [...new Set(fkValues)];
@@ -230,12 +242,13 @@ async function resolveRelationField(db, rows, fieldName, rel, cache, publishedOn
230
242
  cache.set(`${rel.table}:${item["id"]}`, item);
231
243
  }
232
244
  }
233
- for (const row of rows) {
245
+ for (const row of pending) {
234
246
  const fkVal = row[rel.fk_column];
235
247
  row[fieldName] = fkVal ? cache.get(`${rel.table}:${fkVal}`) ?? null : null;
248
+ if (dropFk) delete row[rel.fk_column];
236
249
  }
237
250
  } else if (rel.type === "junction") {
238
- const parentIds = rows.map((r) => r["id"]);
251
+ const parentIds = batch.map((r) => r["id"]);
239
252
  const inList = import_drizzle_orm2.sql.join(parentIds.map((id) => import_drizzle_orm2.sql`${id}`), import_drizzle_orm2.sql`, `);
240
253
  const orderBy = rel.order_column ? import_drizzle_orm2.sql` ORDER BY "order" ASC` : import_drizzle_orm2.sql``;
241
254
  const junctionResult = await db.execute(
@@ -255,14 +268,20 @@ async function resolveRelationField(db, rows, fieldName, rel, cache, publishedOn
255
268
  }
256
269
  }
257
270
  const byLeft = groupBy(jRows, rel.left_column);
258
- for (const row of rows) {
271
+ for (const row of batch) {
259
272
  const jEntries = byLeft[row["id"]] ?? [];
260
273
  row[fieldName] = jEntries.map((jr) => cache.get(`${rel.table}:${jr[rel.right_column]}`)).filter(Boolean);
261
274
  }
262
275
  } else if (rel.type === "media") {
263
- const fkValues = rows.map((r) => r[rel.fk_column]).filter(Boolean);
276
+ const dropFk = rel.fk_column !== fieldName;
277
+ const pending = batch.filter((r) => needsFkResolution(r, rel.fk_column, fieldName));
278
+ if (pending.length === 0) return;
279
+ const fkValues = pending.map((r) => r[rel.fk_column]).filter(Boolean);
264
280
  if (fkValues.length === 0) {
265
- for (const row of rows) row[fieldName] = null;
281
+ for (const row of pending) {
282
+ row[fieldName] = null;
283
+ if (dropFk) delete row[rel.fk_column];
284
+ }
266
285
  return;
267
286
  }
268
287
  const unique = [...new Set(fkValues)];
@@ -274,9 +293,10 @@ async function resolveRelationField(db, rows, fieldName, rel, cache, publishedOn
274
293
  cache.set(`media:${item["id"]}`, item);
275
294
  }
276
295
  }
277
- for (const row of rows) {
296
+ for (const row of pending) {
278
297
  const fkVal = row[rel.fk_column];
279
298
  row[fieldName] = fkVal ? cache.get(`media:${fkVal}`) ?? null : null;
299
+ if (dropFk) delete row[rel.fk_column];
280
300
  }
281
301
  }
282
302
  }
@@ -313,6 +333,53 @@ var init_relations = __esm({
313
333
  }
314
334
  });
315
335
 
336
+ // src/field-keys.ts
337
+ function isColumnBacked(field) {
338
+ const col = field.db_column;
339
+ if (col === null) return false;
340
+ if (col.column_name === "") return false;
341
+ if (col.junction) return false;
342
+ return true;
343
+ }
344
+ function createFieldKeyMap(fields) {
345
+ const labelToColumn = /* @__PURE__ */ new Map();
346
+ const columnToLabel = /* @__PURE__ */ new Map();
347
+ for (const f of fields) {
348
+ if (!isColumnBacked(f)) continue;
349
+ labelToColumn.set(f.name, f.db_column.column_name);
350
+ columnToLabel.set(f.db_column.column_name, f.name);
351
+ }
352
+ for (const f of fields) {
353
+ const columnOwner = columnToLabel.get(f.name);
354
+ if (columnOwner !== void 0 && columnOwner !== f.name) {
355
+ throw new Error(
356
+ `Fatal: field key map failed to build \u2014 field label "${f.name}" collides with the storage column of field "${columnOwner}". A field label may not reuse another field's column name. Rename the field, then run \`manguito validate\` to check your schema.`
357
+ );
358
+ }
359
+ }
360
+ const diverges = [...labelToColumn].some(([label, column]) => label !== column);
361
+ function remap(input, lookup) {
362
+ const out = {};
363
+ for (const key of Object.keys(input)) {
364
+ out[lookup.get(key) ?? key] = input[key];
365
+ }
366
+ return out;
367
+ }
368
+ return {
369
+ toStorage: (input) => remap(input, labelToColumn),
370
+ toLabels: (row) => remap(row, columnToLabel),
371
+ columnFor: (label) => labelToColumn.get(label),
372
+ labelFor: (column) => columnToLabel.get(column),
373
+ labels: [...labelToColumn.keys()],
374
+ diverges
375
+ };
376
+ }
377
+ var init_field_keys = __esm({
378
+ "src/field-keys.ts"() {
379
+ "use strict";
380
+ }
381
+ });
382
+
316
383
  // src/graphql/scalars.ts
317
384
  var import_graphql, DateTimeScalar, JSONScalar;
318
385
  var init_scalars = __esm({
@@ -484,12 +551,13 @@ function buildFilterInputType(type) {
484
551
  if (Object.keys(fields).length === 0) return null;
485
552
  return new import_graphql3.GraphQLInputObjectType({ name: `${graphqlTypeName(type.name)}Filter`, fields });
486
553
  }
487
- function translateFilters(input, nameMap) {
554
+ function translateFilters(input, nameMap, columnFor) {
488
555
  const out = {};
489
556
  if (!input) return out;
490
557
  for (const [gqlField, raw] of Object.entries(input)) {
491
558
  if (raw === null || typeof raw !== "object") continue;
492
- const column = nameMap.toSchema(gqlField);
559
+ const label = nameMap.toSchema(gqlField);
560
+ const column = columnFor?.(label) ?? label;
493
561
  const spec = raw;
494
562
  if (spec["eq"] !== void 0) {
495
563
  out[column] = spec["eq"];
@@ -541,29 +609,34 @@ function codeError2(code, message) {
541
609
  function isPublished2(item) {
542
610
  return !!item && item["published"] === true;
543
611
  }
544
- function scalarFieldResolver(schemaName) {
545
- return (parent) => parent[schemaName];
612
+ function resolveFieldValue(field, row) {
613
+ const key = isColumnBacked(field) ? field.db_column.column_name : field.name;
614
+ return row[key] ?? null;
615
+ }
616
+ function scalarFieldResolver(field) {
617
+ return (parent) => resolveFieldValue(field, parent);
546
618
  }
547
619
  function relationFieldResolver(typeName, schemaFieldName) {
548
620
  return (parent, _args, ctx) => ctx.loaders.load(typeName, schemaFieldName, parent);
549
621
  }
550
- function programmaticFieldResolver(typeName, schemaFieldName, mediaFieldNames = []) {
622
+ function programmaticFieldResolver(typeName, schemaFieldName, mediaFieldNames = [], fieldKeys) {
551
623
  return async (parent, _args, ctx) => {
552
624
  let p = ctx.programmaticMemo.get(parent);
553
625
  if (!p) {
554
- p = resolveProgrammaticRow(typeName, parent, ctx, mediaFieldNames);
626
+ p = resolveProgrammaticRow(typeName, parent, ctx, mediaFieldNames, fieldKeys);
555
627
  ctx.programmaticMemo.set(parent, p);
556
628
  }
557
629
  return (await p)[schemaFieldName];
558
630
  };
559
631
  }
560
- async function resolveProgrammaticRow(typeName, parent, ctx, mediaFieldNames) {
561
- if (mediaFieldNames.length === 0) return ctx.resolver.resolveItem(typeName, parent);
632
+ async function resolveProgrammaticRow(typeName, parent, ctx, mediaFieldNames, fieldKeys) {
633
+ const toLabels = (row) => fieldKeys ? fieldKeys.toLabels(row) : row;
634
+ if (mediaFieldNames.length === 0) return ctx.resolver.resolveItem(typeName, toLabels(parent));
562
635
  const enriched = { ...parent };
563
636
  await Promise.all(mediaFieldNames.map((name) => ctx.loaders.load(typeName, name, enriched)));
564
- return ctx.resolver.resolveItem(typeName, enriched);
637
+ return ctx.resolver.resolveItem(typeName, toLabels(enriched));
565
638
  }
566
- function collectionResolver(typeName, nameMap) {
639
+ function collectionResolver(typeName, nameMap, fieldKeys) {
567
640
  return async (_root, args, ctx) => {
568
641
  const page = args.page ?? 1;
569
642
  const perPage = args.perPage ?? 10;
@@ -580,7 +653,7 @@ function collectionResolver(typeName, nameMap) {
580
653
  per_page: perPage,
581
654
  sort_by: args.sortBy ?? "created_at",
582
655
  sort_order: args.sortOrder ?? "asc",
583
- filters: translateFilters(args.filter, nameMap)
656
+ filters: translateFilters(args.filter, nameMap, fieldKeys?.columnFor)
584
657
  });
585
658
  return { data: result.data, meta: result.meta };
586
659
  };
@@ -612,11 +685,12 @@ var init_resolvers = __esm({
612
685
  "use strict";
613
686
  import_graphql4 = require("graphql");
614
687
  init_filters();
688
+ init_field_keys();
615
689
  }
616
690
  });
617
691
 
618
692
  // src/graphql/schema.ts
619
- function buildGraphQLSchema(registry) {
693
+ function buildGraphQLSchema(registry, fieldKeyMaps = {}) {
620
694
  const objectTypes = /* @__PURE__ */ new Map();
621
695
  const enumTypes = /* @__PURE__ */ new Map();
622
696
  for (const [name, enumType] of Object.entries(registry.enum_types)) {
@@ -692,11 +766,16 @@ function buildGraphQLSchema(registry) {
692
766
  const outType = outputTypeForField(field);
693
767
  let resolve;
694
768
  if (field.field_type === "programmatic") {
695
- resolve = programmaticFieldResolver(machineName, field.name, mediaFieldNames);
769
+ resolve = programmaticFieldResolver(
770
+ machineName,
771
+ field.name,
772
+ mediaFieldNames,
773
+ fieldKeyMaps[machineName]
774
+ );
696
775
  } else if (field.field_type === "reference" || field.field_type === "paragraph" || field.field_type === "image" || field.field_type === "video" || field.field_type === "file") {
697
776
  resolve = relationFieldResolver(machineName, field.name);
698
777
  } else {
699
- resolve = scalarFieldResolver(field.name);
778
+ resolve = scalarFieldResolver(field);
700
779
  }
701
780
  fields[gqlName] = { type: outType, resolve };
702
781
  }
@@ -752,7 +831,7 @@ function buildGraphQLSchema(registry) {
752
831
  sortOrder: { type: SortOrderEnum },
753
832
  ...filterType ? { filter: { type: filterType } } : {}
754
833
  },
755
- resolve: collectionResolver(name, nameMap)
834
+ resolve: collectionResolver(name, nameMap, fieldKeyMaps[name])
756
835
  };
757
836
  queryFields[singleQueryName(name)] = {
758
837
  type: objType,
@@ -772,7 +851,7 @@ function buildGraphQLSchema(registry) {
772
851
  queryFields[collectionQueryName(name)] = {
773
852
  type: new import_graphql5.GraphQLNonNull(listType),
774
853
  args: { page: { type: import_graphql5.GraphQLInt }, perPage: { type: import_graphql5.GraphQLInt } },
775
- resolve: collectionResolver(name, buildFieldNameMap(tt.fields.map((f) => f.name)))
854
+ resolve: collectionResolver(name, buildFieldNameMap(tt.fields.map((f) => f.name)), fieldKeyMaps[name])
776
855
  };
777
856
  queryFields[singleQueryName(name)] = {
778
857
  type: objType,
@@ -914,8 +993,8 @@ function isClientSafeGraphQLError(error) {
914
993
  }
915
994
  return false;
916
995
  }
917
- function createGraphQLHandler(registry, repos, resolver, db, options) {
918
- const schema = buildGraphQLSchema(registry);
996
+ function createGraphQLHandler(registry, repos, fieldKeyMaps, resolver, db, options) {
997
+ const schema = buildGraphQLSchema(registry, fieldKeyMaps);
919
998
  const { plugins } = buildArmorPlugin({
920
999
  maxDepth: options.maxDepth,
921
1000
  maxComplexity: options.maxComplexity
@@ -967,6 +1046,25 @@ __export(index_exports, {
967
1046
  });
968
1047
  module.exports = __toCommonJS(index_exports);
969
1048
 
1049
+ // src/paths.ts
1050
+ function normalizePrefix(prefix) {
1051
+ if (prefix === void 0) return "/api";
1052
+ const withSlash = prefix.startsWith("/") ? prefix : `/${prefix}`;
1053
+ const trimmed = withSlash.replace(/\/+$/, "");
1054
+ return trimmed === "" ? "/api" : trimmed;
1055
+ }
1056
+ function createPublicPaths(prefix) {
1057
+ return {
1058
+ collection: (basePath) => `${prefix}/${basePath}`,
1059
+ item: (basePath) => `${prefix}/${basePath}/:slug`,
1060
+ taxonomyCollection: (typeName) => `${prefix}/taxonomy/${typeName}`,
1061
+ taxonomyItem: (typeName) => `${prefix}/taxonomy/${typeName}/:id`,
1062
+ mediaCollection: () => `${prefix}/media`,
1063
+ mediaItem: () => `${prefix}/media/:id`,
1064
+ openapi: () => `${prefix}/openapi.json`
1065
+ };
1066
+ }
1067
+
970
1068
  // src/app.ts
971
1069
  var import_hono = require("hono");
972
1070
  var import_drizzle_orm10 = require("drizzle-orm");
@@ -1565,28 +1663,31 @@ function parseInclude(includeParam) {
1565
1663
  if (!includeParam) return [];
1566
1664
  return includeParam.split(",").map((s) => s.trim()).filter(Boolean);
1567
1665
  }
1568
- function parseFilters(url, validFields) {
1666
+ function parseFilters(url, validFields, columnFor) {
1569
1667
  const { searchParams } = new URL(url);
1570
1668
  const filters = {};
1669
+ const toKey = (label) => columnFor?.(label) ?? label;
1571
1670
  for (const [key, value] of searchParams.entries()) {
1572
1671
  const simpleMatch = /^filter\[([^\]]+)\]$/.exec(key);
1573
1672
  const opMatch = /^filter\[([^\]]+)\]\[([^\]]+)\]$/.exec(key);
1574
1673
  if (simpleMatch) {
1575
1674
  const field = simpleMatch[1];
1576
1675
  if (!validFields.has(field)) return { ok: false, invalidField: field };
1577
- const existing = filters[field];
1676
+ const storageKey = toKey(field);
1677
+ const existing = filters[storageKey];
1578
1678
  if (existing !== void 0) {
1579
- filters[field] = Array.isArray(existing) ? [...existing, value] : [existing, value];
1679
+ filters[storageKey] = Array.isArray(existing) ? [...existing, value] : [existing, value];
1580
1680
  } else {
1581
- filters[field] = value;
1681
+ filters[storageKey] = value;
1582
1682
  }
1583
1683
  } else if (opMatch) {
1584
1684
  const field = opMatch[1];
1585
1685
  const operator = opMatch[2];
1586
1686
  if (!validFields.has(field)) return { ok: false, invalidField: field };
1587
1687
  if (!["gt", "gte", "lt", "lte"].includes(operator)) continue;
1588
- const existing = filters[field] ?? {};
1589
- filters[field] = { ...existing, [operator]: value };
1688
+ const storageKey = toKey(field);
1689
+ const existing = filters[storageKey] ?? {};
1690
+ filters[storageKey] = { ...existing, [operator]: value };
1590
1691
  }
1591
1692
  }
1592
1693
  return { ok: true, filters };
@@ -1596,7 +1697,7 @@ function parseFilters(url, validFields) {
1596
1697
  function isPublished(item) {
1597
1698
  return item["published"] === true;
1598
1699
  }
1599
- function registerPublicContentRoutes(app, registry, repos, listRateLimit, resolver) {
1700
+ function registerPublicContentRoutes(app, registry, repos, fieldKeyMaps, paths, listRateLimit, resolver) {
1600
1701
  function registerListRoute(path, handler) {
1601
1702
  if (listRateLimit) {
1602
1703
  app.get(path, listRateLimit, handler);
@@ -1604,7 +1705,7 @@ function registerPublicContentRoutes(app, registry, repos, listRateLimit, resolv
1604
1705
  app.get(path, handler);
1605
1706
  }
1606
1707
  }
1607
- registerListRoute("/api/content", (c) => {
1708
+ registerListRoute(paths.collection("content"), (c) => {
1608
1709
  const data = Object.values(registry.content_types).map((ct) => ({
1609
1710
  name: ct.name,
1610
1711
  label: ct.label,
@@ -1612,7 +1713,7 @@ function registerPublicContentRoutes(app, registry, repos, listRateLimit, resolv
1612
1713
  }));
1613
1714
  return c.json({ ok: true, data });
1614
1715
  });
1615
- registerListRoute("/api/taxonomy", (c) => {
1716
+ registerListRoute(paths.collection("taxonomy"), (c) => {
1616
1717
  const data = Object.values(registry.taxonomy_types).map((tt) => ({
1617
1718
  name: tt.name,
1618
1719
  label: tt.label
@@ -1631,7 +1732,7 @@ function registerPublicContentRoutes(app, registry, repos, listRateLimit, resolv
1631
1732
  ...contentType.system_fields.map((f) => f.name)
1632
1733
  ]);
1633
1734
  if (contentType.only_one) {
1634
- app.get(`/api/${basePath}`, async (c) => {
1735
+ app.get(paths.collection(basePath), async (c) => {
1635
1736
  const result = await repo.findMany({ published_only: true, page: 1, per_page: 1 });
1636
1737
  if (result.data.length === 0) {
1637
1738
  return c.json(
@@ -1639,12 +1740,13 @@ function registerPublicContentRoutes(app, registry, repos, listRateLimit, resolv
1639
1740
  404
1640
1741
  );
1641
1742
  }
1642
- let data = result.data[0];
1743
+ let data = fieldKeyMaps[typeName].toLabels(result.data[0]);
1643
1744
  if (resolver?.hasSchema(typeName)) data = await resolver.resolveItem(typeName, data);
1644
1745
  return c.json({ ok: true, data });
1645
1746
  });
1646
1747
  } else {
1647
- registerListRoute(`/api/${basePath}`, async (c) => {
1748
+ registerListRoute(paths.collection(basePath), async (c) => {
1749
+ const fieldKeys = fieldKeyMaps[typeName];
1648
1750
  const pagination = parsePagination(c.req.query("page"), c.req.query("per_page"));
1649
1751
  if (!pagination.ok) {
1650
1752
  return c.json(
@@ -1684,7 +1786,7 @@ function registerPublicContentRoutes(app, registry, repos, listRateLimit, resolv
1684
1786
  400
1685
1787
  );
1686
1788
  }
1687
- const filtersResult = parseFilters(c.req.url, filterableFieldNames);
1789
+ const filtersResult = parseFilters(c.req.url, filterableFieldNames, fieldKeys.columnFor);
1688
1790
  if (!filtersResult.ok) {
1689
1791
  return c.json(
1690
1792
  {
@@ -1721,13 +1823,13 @@ function registerPublicContentRoutes(app, registry, repos, listRateLimit, resolv
1721
1823
  filters: filtersResult.filters,
1722
1824
  include
1723
1825
  });
1724
- if (resolver?.hasSchema(typeName)) {
1725
- const resolved = await resolver.resolveList(typeName, result.data);
1726
- return c.json({ ...result, data: resolved });
1727
- }
1728
- return c.json(result);
1826
+ const labeled = result.data.map(
1827
+ (row) => fieldKeys.toLabels(row)
1828
+ );
1829
+ const data = resolver?.hasSchema(typeName) ? await resolver.resolveList(typeName, labeled) : labeled;
1830
+ return c.json({ ...result, data });
1729
1831
  });
1730
- app.get(`/api/${basePath}/:slug`, async (c) => {
1832
+ app.get(paths.item(basePath), async (c) => {
1731
1833
  const slug = c.req.param("slug");
1732
1834
  const include = parseInclude(c.req.query("include"));
1733
1835
  for (const field of include) {
@@ -1754,7 +1856,7 @@ function registerPublicContentRoutes(app, registry, repos, listRateLimit, resolv
1754
1856
  404
1755
1857
  );
1756
1858
  }
1757
- let data = item;
1859
+ let data = fieldKeyMaps[typeName].toLabels(item);
1758
1860
  if (resolver?.hasSchema(typeName)) data = await resolver.resolveItem(typeName, data);
1759
1861
  return c.json({ ok: true, data });
1760
1862
  });
@@ -1763,7 +1865,7 @@ function registerPublicContentRoutes(app, registry, repos, listRateLimit, resolv
1763
1865
  for (const [typeName] of Object.entries(registry.taxonomy_types)) {
1764
1866
  const repo = repos[typeName];
1765
1867
  if (!repo) continue;
1766
- registerListRoute(`/api/taxonomy/${typeName}`, async (c) => {
1868
+ registerListRoute(paths.taxonomyCollection(typeName), async (c) => {
1767
1869
  const pagination = parsePagination(c.req.query("page"), c.req.query("per_page"));
1768
1870
  if (!pagination.ok) {
1769
1871
  return c.json(
@@ -1782,13 +1884,14 @@ function registerPublicContentRoutes(app, registry, repos, listRateLimit, resolv
1782
1884
  page: pagination.page,
1783
1885
  per_page: pagination.per_page
1784
1886
  });
1785
- if (resolver?.hasSchema(typeName)) {
1786
- const resolved = await resolver.resolveList(typeName, result.data);
1787
- return c.json({ ...result, data: resolved });
1788
- }
1789
- return c.json(result);
1887
+ const fieldKeys = fieldKeyMaps[typeName];
1888
+ const labeled = result.data.map(
1889
+ (row) => fieldKeys.toLabels(row)
1890
+ );
1891
+ const data = resolver?.hasSchema(typeName) ? await resolver.resolveList(typeName, labeled) : labeled;
1892
+ return c.json({ ...result, data });
1790
1893
  });
1791
- app.get(`/api/taxonomy/${typeName}/:id`, async (c) => {
1894
+ app.get(paths.taxonomyItem(typeName), async (c) => {
1792
1895
  const id = c.req.param("id");
1793
1896
  const item = await repo.findOne(id);
1794
1897
  if (!item || !isPublished(item)) {
@@ -1800,7 +1903,7 @@ function registerPublicContentRoutes(app, registry, repos, listRateLimit, resolv
1800
1903
  404
1801
1904
  );
1802
1905
  }
1803
- let data = item;
1906
+ let data = fieldKeyMaps[typeName].toLabels(item);
1804
1907
  if (resolver?.hasSchema(typeName)) data = await resolver.resolveItem(typeName, data);
1805
1908
  return c.json({ ok: true, data });
1806
1909
  });
@@ -1816,7 +1919,7 @@ function parsePagination2(pageStr, perPageStr) {
1816
1919
  if (!Number.isInteger(per_page) || per_page < 1 || per_page > 100) return { ok: false };
1817
1920
  return { ok: true, page, per_page };
1818
1921
  }
1819
- function registerPublicMediaRoutes(app, mediaRepo, listRateLimit) {
1922
+ function registerPublicMediaRoutes(app, mediaRepo, paths, listRateLimit) {
1820
1923
  const mediaListHandler = async (c) => {
1821
1924
  const pagination = parsePagination2(c.req.query("page"), c.req.query("per_page"));
1822
1925
  if (!pagination.ok) {
@@ -1855,11 +1958,11 @@ function registerPublicMediaRoutes(app, mediaRepo, listRateLimit) {
1855
1958
  return c.json(result);
1856
1959
  };
1857
1960
  if (listRateLimit) {
1858
- app.get("/api/media", listRateLimit, mediaListHandler);
1961
+ app.get(paths.mediaCollection(), listRateLimit, mediaListHandler);
1859
1962
  } else {
1860
- app.get("/api/media", mediaListHandler);
1963
+ app.get(paths.mediaCollection(), mediaListHandler);
1861
1964
  }
1862
- app.get("/api/media/:id", async (c) => {
1965
+ app.get(paths.mediaItem(), async (c) => {
1863
1966
  const id = c.req.param("id");
1864
1967
  const item = await mediaRepo.findOne(id);
1865
1968
  if (!item) {
@@ -1883,9 +1986,11 @@ function topLevelMediaDelta(mediaFields, before, after) {
1883
1986
  const added = [];
1884
1987
  const removed = [];
1885
1988
  for (const f of mediaFields) {
1886
- if (after !== null && !(f.name in after)) continue;
1887
- const oldId = before ? extractMediaId(before[f.name]) : null;
1888
- const newId = after ? extractMediaId(after[f.name]) : null;
1989
+ const key = f.db_column?.column_name;
1990
+ if (key === void 0 || key === "") continue;
1991
+ if (after !== null && !(key in after)) continue;
1992
+ const oldId = before ? extractMediaId(before[key]) : null;
1993
+ const newId = after ? extractMediaId(after[key]) : null;
1889
1994
  if (newId === oldId) continue;
1890
1995
  if (oldId) removed.push(oldId);
1891
1996
  if (newId) added.push(newId);
@@ -1909,6 +2014,7 @@ async function applyMediaReferenceDelta(delta, mediaRepo) {
1909
2014
 
1910
2015
  // src/routes/admin/content.ts
1911
2016
  init_relations();
2017
+ init_field_keys();
1912
2018
  function quoteIdent2(name) {
1913
2019
  if (!/^[a-z][a-z0-9_-]*$/.test(name)) throw new Error(`Unsafe identifier: ${name}`);
1914
2020
  return `"${name}"`;
@@ -1946,7 +2052,7 @@ function isEmpty(value) {
1946
2052
  function checkRequiredFields(fields, data) {
1947
2053
  return fields.filter((f) => f.required && isEmpty(data[f.name])).map((f) => ({ field: f.name, message: `${f.label} is required` }));
1948
2054
  }
1949
- function parseListQuery(url, schemaFieldNames, relationFieldNames) {
2055
+ function parseListQuery(url, schemaFieldNames, relationFieldNames, columnFor) {
1950
2056
  const searchParams = new URL(url).searchParams;
1951
2057
  const pagination = parsePagination(
1952
2058
  searchParams.get("page") ?? void 0,
@@ -1981,7 +2087,7 @@ function parseListQuery(url, schemaFieldNames, relationFieldNames) {
1981
2087
  status: 400
1982
2088
  };
1983
2089
  }
1984
- const filtersResult = parseFilters(url, schemaFieldNames);
2090
+ const filtersResult = parseFilters(url, schemaFieldNames, columnFor);
1985
2091
  if (!filtersResult.ok) {
1986
2092
  return {
1987
2093
  ok: false,
@@ -2013,7 +2119,7 @@ function parseListQuery(url, schemaFieldNames, relationFieldNames) {
2013
2119
  search
2014
2120
  };
2015
2121
  }
2016
- function registerAdminContentRoutes(app, registry, repos, mediaRepo, requirePermission, db) {
2122
+ function registerAdminContentRoutes(app, registry, repos, fieldKeyMaps, mediaRepo, requirePermission, db) {
2017
2123
  for (const [typeName, contentType] of Object.entries(registry.content_types)) {
2018
2124
  const basePath = `content/${typeName}`;
2019
2125
  const repo = repos[typeName];
@@ -2025,6 +2131,7 @@ function registerAdminContentRoutes(app, registry, repos, mediaRepo, requirePerm
2025
2131
  const relationFieldNames = new Set(
2026
2132
  contentType.fields.filter((f) => RELATION_FIELD_TYPES.has(f.field_type)).map((f) => f.name)
2027
2133
  );
2134
+ const fieldKeys = fieldKeyMaps[typeName];
2028
2135
  const requiredFields = contentType.fields.filter((f) => f.required);
2029
2136
  const mediaFields = contentType.fields.filter(
2030
2137
  (f) => f.field_type === "image" || f.field_type === "video" || f.field_type === "file"
@@ -2038,7 +2145,7 @@ function registerAdminContentRoutes(app, registry, repos, mediaRepo, requirePerm
2038
2145
  `/admin/api/${basePath}`,
2039
2146
  requirePermission("content:read"),
2040
2147
  async (c) => {
2041
- const parsed = parseListQuery(c.req.url, schemaFieldNames, relationFieldNames);
2148
+ const parsed = parseListQuery(c.req.url, schemaFieldNames, relationFieldNames, fieldKeys.columnFor);
2042
2149
  if (!parsed.ok) {
2043
2150
  return c.json({ ok: false, error: parsed.response }, parsed.status);
2044
2151
  }
@@ -2058,7 +2165,8 @@ function registerAdminContentRoutes(app, registry, repos, mediaRepo, requirePerm
2058
2165
  findOpts.search = { term: parsed.search, columns: searchableColumns };
2059
2166
  }
2060
2167
  const result = await repo.findMany(findOpts);
2061
- return c.json(result);
2168
+ const data = result.data.map((row) => fieldKeys.toLabels(row));
2169
+ return c.json({ ...result, data });
2062
2170
  }
2063
2171
  );
2064
2172
  app.get(
@@ -2093,7 +2201,8 @@ function registerAdminContentRoutes(app, registry, repos, mediaRepo, requirePerm
2093
2201
  }
2094
2202
  }
2095
2203
  }
2096
- return c.json({ ok: true, data: item });
2204
+ const data = fieldKeys.toLabels(item);
2205
+ return c.json({ ok: true, data });
2097
2206
  }
2098
2207
  );
2099
2208
  app.post(
@@ -2160,6 +2269,7 @@ function registerAdminContentRoutes(app, registry, repos, mediaRepo, requirePerm
2160
2269
  }
2161
2270
  );
2162
2271
  const writeNewItem = async (c, body) => {
2272
+ const storageBody = fieldKeys.toStorage(body);
2163
2273
  const fieldErrors = checkRequiredFields(requiredFields, body);
2164
2274
  if (fieldErrors.length > 0) {
2165
2275
  return c.json(
@@ -2183,9 +2293,7 @@ function registerAdminContentRoutes(app, registry, repos, mediaRepo, requirePerm
2183
2293
  const junctionFields = contentType.fields.filter(
2184
2294
  (f) => f.db_column !== null && f.db_column.junction !== void 0
2185
2295
  );
2186
- const columnFields = contentType.fields.filter(
2187
- (f) => f.db_column !== null && f.db_column.junction === void 0
2188
- );
2296
+ const columnBackedFields = contentType.fields.filter(isColumnBacked);
2189
2297
  const columnData = {
2190
2298
  published: body["published"] ?? false
2191
2299
  };
@@ -2194,9 +2302,10 @@ function registerAdminContentRoutes(app, registry, repos, mediaRepo, requirePerm
2194
2302
  } else {
2195
2303
  columnData["slug"] = body["slug"];
2196
2304
  }
2197
- for (const f of columnFields) {
2198
- const val = body[f.name];
2199
- columnData[f.db_column.column_name] = val !== void 0 ? val : null;
2305
+ for (const f of columnBackedFields) {
2306
+ const key = f.db_column.column_name;
2307
+ if (!(key in storageBody)) continue;
2308
+ columnData[key] = storageBody[key];
2200
2309
  }
2201
2310
  if (db) {
2202
2311
  const basePathId = await lookupBasePathId(db, contentType.default_base_path);
@@ -2216,7 +2325,7 @@ function registerAdminContentRoutes(app, registry, repos, mediaRepo, requirePerm
2216
2325
  }
2217
2326
  const item = await repo.create(columnData);
2218
2327
  const itemId = item["id"];
2219
- const mediaDeltas = [topLevelMediaDelta(mediaFields, null, body)];
2328
+ const mediaDeltas = [topLevelMediaDelta(mediaFields, null, storageBody)];
2220
2329
  if (db) {
2221
2330
  for (const f of paragraphFields) {
2222
2331
  const comp = f.ui_component;
@@ -2233,7 +2342,8 @@ function registerAdminContentRoutes(app, registry, repos, mediaRepo, requirePerm
2233
2342
  }
2234
2343
  }
2235
2344
  await applyMediaReferenceDelta(mergeMediaDeltas(...mediaDeltas), mediaRepo);
2236
- return c.json({ ok: true, data: item }, 201);
2345
+ const data = fieldKeys.toLabels(item);
2346
+ return c.json({ ok: true, data }, 201);
2237
2347
  };
2238
2348
  app.patch(
2239
2349
  `/admin/api/${basePath}/:id`,
@@ -2293,11 +2403,12 @@ function registerAdminContentRoutes(app, registry, repos, mediaRepo, requirePerm
2293
2403
  }
2294
2404
  );
2295
2405
  const writeExistingItem = async (c, id, existing, body) => {
2406
+ const storageBody = fieldKeys.toStorage(body);
2296
2407
  if (body["published"] === true) {
2297
2408
  const publishDeny = await requirePermission("content:edit")(c, async () => {
2298
2409
  });
2299
2410
  if (publishDeny) return publishDeny;
2300
- const merged = { ...existing, ...body };
2411
+ const merged = { ...fieldKeys.toLabels(existing), ...body };
2301
2412
  const fieldErrors = checkRequiredFields(requiredFields, merged);
2302
2413
  if (fieldErrors.length > 0) {
2303
2414
  return c.json(
@@ -2317,17 +2428,14 @@ function registerAdminContentRoutes(app, registry, repos, mediaRepo, requirePerm
2317
2428
  const patchJunctionFields = contentType.fields.filter(
2318
2429
  (f) => f.db_column !== null && f.db_column.junction !== void 0
2319
2430
  );
2320
- const patchColumnFields = contentType.fields.filter(
2321
- (f) => f.db_column !== null && f.db_column.junction === void 0
2322
- );
2431
+ const patchColumnBackedFields = contentType.fields.filter(isColumnBacked);
2323
2432
  const patchData = {};
2324
2433
  if (!contentType.only_one && "slug" in body) patchData["slug"] = body["slug"];
2325
2434
  if ("published" in body) patchData["published"] = body["published"];
2326
- for (const f of patchColumnFields) {
2327
- if (f.name in body) {
2328
- const val = body[f.name];
2329
- patchData[f.db_column.column_name] = val !== void 0 ? val : null;
2330
- }
2435
+ for (const f of patchColumnBackedFields) {
2436
+ const key = f.db_column.column_name;
2437
+ if (!(key in storageBody)) continue;
2438
+ patchData[key] = storageBody[key];
2331
2439
  }
2332
2440
  const updated = await repo.update(id, patchData);
2333
2441
  if (!updated) {
@@ -2337,7 +2445,7 @@ function registerAdminContentRoutes(app, registry, repos, mediaRepo, requirePerm
2337
2445
  );
2338
2446
  }
2339
2447
  const mediaDeltas = [
2340
- topLevelMediaDelta(mediaFields, existing, body)
2448
+ topLevelMediaDelta(mediaFields, existing, storageBody)
2341
2449
  ];
2342
2450
  if (db) {
2343
2451
  for (const f of patchParagraphFields) {
@@ -2355,7 +2463,8 @@ function registerAdminContentRoutes(app, registry, repos, mediaRepo, requirePerm
2355
2463
  }
2356
2464
  }
2357
2465
  await applyMediaReferenceDelta(mergeMediaDeltas(...mediaDeltas), mediaRepo);
2358
- return c.json({ ok: true, data: updated });
2466
+ const data = fieldKeys.toLabels(updated);
2467
+ return c.json({ ok: true, data });
2359
2468
  };
2360
2469
  if (contentType.only_one) {
2361
2470
  app.put(
@@ -2415,6 +2524,7 @@ function registerAdminContentRoutes(app, registry, repos, mediaRepo, requirePerm
2415
2524
  const relationFieldNames = new Set(
2416
2525
  taxonomyType.fields.filter((f) => RELATION_FIELD_TYPES.has(f.field_type)).map((f) => f.name)
2417
2526
  );
2527
+ const fieldKeys = fieldKeyMaps[typeName];
2418
2528
  const requiredFields = taxonomyType.fields.filter((f) => f.required);
2419
2529
  const mediaFields = taxonomyType.fields.filter(
2420
2530
  (f) => f.field_type === "image" || f.field_type === "video" || f.field_type === "file"
@@ -2428,7 +2538,7 @@ function registerAdminContentRoutes(app, registry, repos, mediaRepo, requirePerm
2428
2538
  `/admin/api/taxonomy/${typeName}`,
2429
2539
  requirePermission("content:read"),
2430
2540
  async (c) => {
2431
- const parsed = parseListQuery(c.req.url, schemaFieldNames, relationFieldNames);
2541
+ const parsed = parseListQuery(c.req.url, schemaFieldNames, relationFieldNames, fieldKeys.columnFor);
2432
2542
  if (!parsed.ok) {
2433
2543
  return c.json({ ok: false, error: parsed.response }, parsed.status);
2434
2544
  }
@@ -2448,7 +2558,8 @@ function registerAdminContentRoutes(app, registry, repos, mediaRepo, requirePerm
2448
2558
  findOpts.search = { term: parsed.search, columns: searchableColumns };
2449
2559
  }
2450
2560
  const result = await repo.findMany(findOpts);
2451
- return c.json(result);
2561
+ const data = result.data.map((row) => fieldKeys.toLabels(row));
2562
+ return c.json({ ...result, data });
2452
2563
  }
2453
2564
  );
2454
2565
  app.get(
@@ -2463,7 +2574,8 @@ function registerAdminContentRoutes(app, registry, repos, mediaRepo, requirePerm
2463
2574
  404
2464
2575
  );
2465
2576
  }
2466
- return c.json({ ok: true, data: item });
2577
+ const data = fieldKeys.toLabels(item);
2578
+ return c.json({ ok: true, data });
2467
2579
  }
2468
2580
  );
2469
2581
  app.post(
@@ -2471,6 +2583,7 @@ function registerAdminContentRoutes(app, registry, repos, mediaRepo, requirePerm
2471
2583
  requirePermission("content:create"),
2472
2584
  async (c) => {
2473
2585
  const body = await c.req.json();
2586
+ const storageBody = fieldKeys.toStorage(body);
2474
2587
  const fieldErrors = checkRequiredFields(requiredFields, body);
2475
2588
  if (fieldErrors.length > 0) {
2476
2589
  return c.json(
@@ -2490,20 +2603,19 @@ function registerAdminContentRoutes(app, registry, repos, mediaRepo, requirePerm
2490
2603
  });
2491
2604
  if (publishDeny) return publishDeny;
2492
2605
  }
2493
- const taxColumnFields = taxonomyType.fields.filter(
2494
- (f) => f.db_column !== null && f.db_column.junction === void 0
2495
- );
2496
2606
  const taxParagraphFields = taxonomyType.fields.filter((f) => f.db_column === null);
2607
+ const taxColumnBackedFields = taxonomyType.fields.filter(isColumnBacked);
2497
2608
  const taxColumnData = {
2498
2609
  published: body["published"] ?? false
2499
2610
  };
2500
- for (const f of taxColumnFields) {
2501
- const val = body[f.name];
2502
- taxColumnData[f.db_column.column_name] = val !== void 0 ? val : null;
2611
+ for (const f of taxColumnBackedFields) {
2612
+ const key = f.db_column.column_name;
2613
+ if (!(key in storageBody)) continue;
2614
+ taxColumnData[key] = storageBody[key];
2503
2615
  }
2504
2616
  const item = await repo.create(taxColumnData);
2505
2617
  const taxItemId = item["id"];
2506
- const mediaDeltas = [topLevelMediaDelta(mediaFields, null, body)];
2618
+ const mediaDeltas = [topLevelMediaDelta(mediaFields, null, storageBody)];
2507
2619
  if (db) {
2508
2620
  for (const f of taxParagraphFields) {
2509
2621
  const comp = f.ui_component;
@@ -2515,7 +2627,8 @@ function registerAdminContentRoutes(app, registry, repos, mediaRepo, requirePerm
2515
2627
  }
2516
2628
  }
2517
2629
  await applyMediaReferenceDelta(mergeMediaDeltas(...mediaDeltas), mediaRepo);
2518
- return c.json({ ok: true, data: item }, 201);
2630
+ const data = fieldKeys.toLabels(item);
2631
+ return c.json({ ok: true, data }, 201);
2519
2632
  }
2520
2633
  );
2521
2634
  app.patch(
@@ -2524,6 +2637,7 @@ function registerAdminContentRoutes(app, registry, repos, mediaRepo, requirePerm
2524
2637
  async (c) => {
2525
2638
  const id = c.req.param("id");
2526
2639
  const body = await c.req.json();
2640
+ const storageBody = fieldKeys.toStorage(body);
2527
2641
  const existing = await repo.findOne(id);
2528
2642
  if (!existing) {
2529
2643
  return c.json(
@@ -2535,7 +2649,7 @@ function registerAdminContentRoutes(app, registry, repos, mediaRepo, requirePerm
2535
2649
  const publishDeny = await requirePermission("content:edit")(c, async () => {
2536
2650
  });
2537
2651
  if (publishDeny) return publishDeny;
2538
- const merged = { ...existing, ...body };
2652
+ const merged = { ...fieldKeys.toLabels(existing), ...body };
2539
2653
  const fieldErrors = checkRequiredFields(requiredFields, merged);
2540
2654
  if (fieldErrors.length > 0) {
2541
2655
  return c.json(
@@ -2551,17 +2665,14 @@ function registerAdminContentRoutes(app, registry, repos, mediaRepo, requirePerm
2551
2665
  );
2552
2666
  }
2553
2667
  }
2554
- const taxPatchColumnFields = taxonomyType.fields.filter(
2555
- (f) => f.db_column !== null && f.db_column.junction === void 0
2556
- );
2557
2668
  const taxPatchParagraphFields = taxonomyType.fields.filter((f) => f.db_column === null);
2669
+ const taxPatchColumnBackedFields = taxonomyType.fields.filter(isColumnBacked);
2558
2670
  const taxPatchData = {};
2559
2671
  if ("published" in body) taxPatchData["published"] = body["published"];
2560
- for (const f of taxPatchColumnFields) {
2561
- if (f.name in body) {
2562
- const val = body[f.name];
2563
- taxPatchData[f.db_column.column_name] = val !== void 0 ? val : null;
2564
- }
2672
+ for (const f of taxPatchColumnBackedFields) {
2673
+ const key = f.db_column.column_name;
2674
+ if (!(key in storageBody)) continue;
2675
+ taxPatchData[key] = storageBody[key];
2565
2676
  }
2566
2677
  const updated = await repo.update(id, taxPatchData);
2567
2678
  if (!updated) {
@@ -2571,7 +2682,7 @@ function registerAdminContentRoutes(app, registry, repos, mediaRepo, requirePerm
2571
2682
  );
2572
2683
  }
2573
2684
  const mediaDeltas = [
2574
- topLevelMediaDelta(mediaFields, existing, body)
2685
+ topLevelMediaDelta(mediaFields, existing, storageBody)
2575
2686
  ];
2576
2687
  if (db) {
2577
2688
  for (const f of taxPatchParagraphFields) {
@@ -2584,7 +2695,8 @@ function registerAdminContentRoutes(app, registry, repos, mediaRepo, requirePerm
2584
2695
  }
2585
2696
  }
2586
2697
  await applyMediaReferenceDelta(mergeMediaDeltas(...mediaDeltas), mediaRepo);
2587
- return c.json({ ok: true, data: updated });
2698
+ const data = fieldKeys.toLabels(updated);
2699
+ return c.json({ ok: true, data });
2588
2700
  }
2589
2701
  );
2590
2702
  app.delete(
@@ -3576,7 +3688,7 @@ function registerUserRoutes(app, db, requirePermission, requireHierarchy) {
3576
3688
  var import_drizzle_orm6 = require("drizzle-orm");
3577
3689
  var API_VERSION = (() => {
3578
3690
  try {
3579
- return "0.3.3";
3691
+ return "0.4.0";
3580
3692
  } catch {
3581
3693
  return "0.0.0";
3582
3694
  }
@@ -3978,6 +4090,7 @@ function createMediaRepository(db) {
3978
4090
  }
3979
4091
 
3980
4092
  // src/app.ts
4093
+ init_field_keys();
3981
4094
  var MISSING_STORAGE_ERROR = `\u2717 api.storage is required but not configured.
3982
4095
  Add a storage adapter to your manguito.config.ts:
3983
4096
 
@@ -3991,7 +4104,8 @@ function createCmsApp(options) {
3991
4104
  if (!options.storage) {
3992
4105
  throw new Error(MISSING_STORAGE_ERROR);
3993
4106
  }
3994
- const prefix = options.prefix ?? "/api";
4107
+ const prefix = normalizePrefix(options.prefix);
4108
+ const publicPaths = createPublicPaths(prefix);
3995
4109
  const { storage, registry, db, rateLimit, media, cors } = options;
3996
4110
  const cmsName = options.name ?? "Manguito CMS";
3997
4111
  const maxFileSize = media?.max_file_size;
@@ -4071,23 +4185,27 @@ function createCmsApp(options) {
4071
4185
  registerAuthRoutes(app, db);
4072
4186
  app.use("/admin/api/*", authMiddleware);
4073
4187
  app.use("/admin/api/*", mustChangePasswordCheck);
4074
- app.get("/api/openapi.json", (c) => {
4188
+ app.get(publicPaths.openapi(), (c) => {
4075
4189
  const paths = {};
4076
4190
  for (const ct of Object.values(registry.content_types)) {
4077
4191
  const base = ct.default_base_path;
4078
4192
  if (ct.only_one) {
4079
- paths[`/api/${base}`] = { get: { summary: `Get ${ct.label}`, tags: [ct.label] } };
4193
+ paths[publicPaths.collection(base)] = { get: { summary: `Get ${ct.label}`, tags: [ct.label] } };
4080
4194
  } else {
4081
- paths[`/api/${base}`] = { get: { summary: `List published ${ct.label}`, tags: [ct.label] } };
4082
- paths[`/api/${base}/{slug}`] = { get: { summary: `Get ${ct.label} by slug`, tags: [ct.label] } };
4195
+ paths[publicPaths.collection(base)] = { get: { summary: `List published ${ct.label}`, tags: [ct.label] } };
4196
+ paths[publicPaths.item(base).replace(":slug", "{slug}")] = {
4197
+ get: { summary: `Get ${ct.label} by slug`, tags: [ct.label] }
4198
+ };
4083
4199
  }
4084
4200
  }
4085
4201
  for (const tt of Object.values(registry.taxonomy_types)) {
4086
- paths[`/api/taxonomy/${tt.name}`] = { get: { summary: `List published ${tt.label}`, tags: [tt.label] } };
4087
- paths[`/api/taxonomy/${tt.name}/{id}`] = { get: { summary: `Get ${tt.label} by id`, tags: [tt.label] } };
4202
+ paths[publicPaths.taxonomyCollection(tt.name)] = { get: { summary: `List published ${tt.label}`, tags: [tt.label] } };
4203
+ paths[publicPaths.taxonomyItem(tt.name).replace(":id", "{id}")] = {
4204
+ get: { summary: `Get ${tt.label} by id`, tags: [tt.label] }
4205
+ };
4088
4206
  }
4089
- paths["/api/media"] = { get: { summary: "List media items", tags: ["Media"] } };
4090
- paths["/api/media/{id}"] = { get: { summary: "Get media item by id", tags: ["Media"] } };
4207
+ paths[publicPaths.mediaCollection()] = { get: { summary: "List media items", tags: ["Media"] } };
4208
+ paths[publicPaths.mediaItem().replace(":id", "{id}")] = { get: { summary: "Get media item by id", tags: ["Media"] } };
4091
4209
  return c.json({
4092
4210
  openapi: "3.0.3",
4093
4211
  info: { title: "Manguito CMS Public API", version: "1.0.0" },
@@ -4102,14 +4220,24 @@ function createCmsApp(options) {
4102
4220
  paths: {}
4103
4221
  })
4104
4222
  );
4105
- registerPublicContentRoutes(app, registry, publicRepos, listRateLimit, programmaticResolver);
4106
- registerPublicMediaRoutes(app, mediaRepo, listRateLimit);
4223
+ const fieldKeyMaps = Object.fromEntries([
4224
+ ...Object.entries(registry.content_types).map(([typeName, ct]) => [
4225
+ typeName,
4226
+ createFieldKeyMap(ct.fields)
4227
+ ]),
4228
+ ...Object.entries(registry.taxonomy_types).map(([typeName, tt]) => [
4229
+ typeName,
4230
+ createFieldKeyMap(tt.fields)
4231
+ ])
4232
+ ]);
4233
+ registerPublicContentRoutes(app, registry, publicRepos, fieldKeyMaps, publicPaths, listRateLimit, programmaticResolver);
4234
+ registerPublicMediaRoutes(app, mediaRepo, publicPaths, listRateLimit);
4107
4235
  if (options.graphql?.enabled) {
4108
4236
  const gqlOptions = options.graphql;
4109
4237
  let gqlHandler = null;
4110
4238
  let initError = null;
4111
4239
  const ready = Promise.resolve().then(() => (init_handler(), handler_exports)).then(({ createGraphQLHandler: createGraphQLHandler2 }) => {
4112
- gqlHandler = createGraphQLHandler2(registry, graphqlRepos, programmaticResolver, db, gqlOptions);
4240
+ gqlHandler = createGraphQLHandler2(registry, graphqlRepos, fieldKeyMaps, programmaticResolver, db, gqlOptions);
4113
4241
  }).catch((err) => {
4114
4242
  initError = err;
4115
4243
  const message = err instanceof Error ? err.message : String(err);
@@ -4149,7 +4277,7 @@ function createCmsApp(options) {
4149
4277
  );
4150
4278
  registerSchemaRoute(app, registry, db);
4151
4279
  registerUserRoutes(app, db, requirePermission, requireHierarchy);
4152
- registerAdminContentRoutes(app, registry, repos, mediaRepo, requirePermission, db);
4280
+ registerAdminContentRoutes(app, registry, repos, fieldKeyMaps, mediaRepo, requirePermission, db);
4153
4281
  registerAdminMediaRoutes(app, mediaRepo, storage, requirePermission, maxFileSize);
4154
4282
  return { prefix, app };
4155
4283
  }
@@ -4190,7 +4318,7 @@ function resolveGraphQL(cfg) {
4190
4318
  };
4191
4319
  }
4192
4320
  function createAPIAdapter(options = {}) {
4193
- const prefix = options.prefix ?? "/api";
4321
+ const prefix = normalizePrefix(options.prefix);
4194
4322
  const media = options.media?.max_file_size !== void 0 ? { max_file_size: options.media.max_file_size } : void 0;
4195
4323
  return {
4196
4324
  prefix,