@bobbykim/manguito-cms-api 0.6.0 → 0.7.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
@@ -38,7 +38,7 @@ function isColumnBacked(field) {
38
38
  if (col.junction) return false;
39
39
  return true;
40
40
  }
41
- function buildFieldKeyMap(pairs, allLabels, droppedKeys) {
41
+ function buildFieldKeyMap(pairs, allLabels, droppedColumns, droppedLabels) {
42
42
  const labelToColumn = /* @__PURE__ */ new Map();
43
43
  const columnToLabel = /* @__PURE__ */ new Map();
44
44
  for (const { label, column } of pairs) {
@@ -46,7 +46,7 @@ function buildFieldKeyMap(pairs, allLabels, droppedKeys) {
46
46
  columnToLabel.set(column, label);
47
47
  }
48
48
  for (const { label, ownColumn } of allLabels) {
49
- if (ownColumn === label) continue;
49
+ if (ownColumn !== void 0) continue;
50
50
  const columnOwner = columnToLabel.get(label);
51
51
  if (columnOwner !== void 0 && columnOwner !== label) {
52
52
  throw new Error(
@@ -55,23 +55,33 @@ function buildFieldKeyMap(pairs, allLabels, droppedKeys) {
55
55
  }
56
56
  }
57
57
  for (const label of labelToColumn.keys()) {
58
- if (droppedKeys.has(label)) labelToColumn.delete(label);
58
+ if (droppedLabels.has(label)) labelToColumn.delete(label);
59
59
  }
60
60
  for (const column of columnToLabel.keys()) {
61
- if (droppedKeys.has(column)) columnToLabel.delete(column);
61
+ if (droppedColumns.has(column)) columnToLabel.delete(column);
62
62
  }
63
63
  const diverges = [...labelToColumn].some(([label, column]) => label !== column);
64
- function remap(input, lookup) {
64
+ function remap(input, lookup, dropped, droppedIfUnmapped) {
65
65
  const out = {};
66
66
  for (const key of Object.keys(input)) {
67
- if (droppedKeys.has(key)) continue;
68
- out[lookup.get(key) ?? key] = input[key];
67
+ const mapped = lookup.get(key);
68
+ if (mapped !== void 0) {
69
+ out[mapped] = input[key];
70
+ continue;
71
+ }
72
+ if (dropped.has(key) || droppedIfUnmapped?.has(key)) continue;
73
+ out[key] = input[key];
69
74
  }
70
75
  return out;
71
76
  }
72
77
  return {
73
- toStorage: (input) => remap(input, labelToColumn),
74
- toLabels: (row) => remap(row, columnToLabel),
78
+ // toStorage reads LABEL-keyed input: a live label maps and writes
79
+ // regardless of either drop set; an unmapped key is refused if it is
80
+ // EITHER the tombstone's own name (droppedLabels) or a raw, unexposed
81
+ // column (droppedColumns) — see remap's comment.
82
+ // toLabels reads COLUMN-keyed input, so it consults only droppedColumns.
83
+ toStorage: (input) => remap(input, labelToColumn, droppedLabels, droppedColumns),
84
+ toLabels: (row) => remap(row, columnToLabel, droppedColumns),
75
85
  columnFor: (label) => labelToColumn.get(label),
76
86
  labelFor: (column) => columnToLabel.get(column),
77
87
  labels: [...labelToColumn.keys()],
@@ -79,14 +89,15 @@ function buildFieldKeyMap(pairs, allLabels, droppedKeys) {
79
89
  };
80
90
  }
81
91
  function createFieldKeyMap(fields) {
82
- const droppedKeys = /* @__PURE__ */ new Set();
92
+ const droppedColumns = /* @__PURE__ */ new Set();
93
+ const droppedLabels = /* @__PURE__ */ new Set();
83
94
  const pairs = [];
84
95
  for (const f of fields) {
85
96
  if (!isColumnBacked(f)) continue;
86
97
  pairs.push({ label: f.name, column: f.db_column.column_name });
87
98
  if (f.removed === true) {
88
- droppedKeys.add(f.name);
89
- droppedKeys.add(f.db_column.column_name);
99
+ droppedLabels.add(f.name);
100
+ droppedColumns.add(f.db_column.column_name);
90
101
  }
91
102
  }
92
103
  return buildFieldKeyMap(
@@ -97,30 +108,33 @@ function createFieldKeyMap(fields) {
97
108
  // forbids present-and-undefined.
98
109
  ...isColumnBacked(f) && { ownColumn: f.db_column.column_name }
99
110
  })),
100
- droppedKeys
111
+ droppedColumns,
112
+ droppedLabels
101
113
  );
102
114
  }
103
115
  function createFieldKeyMapFromProjection(projectionType, allFields) {
104
116
  const pairs = projectionType.fields.map((f) => ({ label: f.exposed_as, column: f.column_name }));
105
- const projected = new Set(pairs.map((p) => p.label));
106
- const dropped = /* @__PURE__ */ new Set();
117
+ const projectedColumns = new Set(pairs.map((p) => p.column));
118
+ const projectedLabels = new Set(pairs.map((p) => p.label));
119
+ const droppedColumns = /* @__PURE__ */ new Set();
120
+ const droppedLabels = /* @__PURE__ */ new Set();
107
121
  for (const f of allFields) {
108
122
  if (!isColumnBacked(f)) continue;
109
- dropped.add(f.name);
110
- dropped.add(f.db_column.column_name);
123
+ if (!projectedColumns.has(f.db_column.column_name)) droppedColumns.add(f.db_column.column_name);
124
+ if (!projectedLabels.has(f.name)) droppedLabels.add(f.name);
111
125
  }
112
- for (const p of pairs) dropped.delete(p.column);
113
- for (const label of projected) dropped.delete(label);
114
126
  return buildFieldKeyMap(
115
127
  pairs,
116
128
  allFields.map((f) => ({
117
129
  label: f.name,
118
- // See buildFieldKeyMap's collision check: this is what lets it tell a
119
- // version exposing a field's own column under a new label (fine) from a
120
- // label that reuses another field's column (not fine).
130
+ // See buildFieldKeyMap's collision check: `ownColumn` marks every
131
+ // column-backed field so the check skips it entirely — only a
132
+ // paragraph/junction/programmatic label (no column of its own) can
133
+ // still collide.
121
134
  ...isColumnBacked(f) && { ownColumn: f.db_column.column_name }
122
135
  })),
123
- dropped
136
+ droppedColumns,
137
+ droppedLabels
124
138
  );
125
139
  }
126
140
  var init_field_keys = __esm({
@@ -299,9 +313,8 @@ function groupBy(items, key) {
299
313
  }
300
314
  return result;
301
315
  }
302
- function needsFkResolution(row, fkColumn, fieldName) {
303
- if (fkColumn !== fieldName && !(fkColumn in row)) return false;
304
- const current = row[fieldName];
316
+ function needsFkResolution(row, fkColumn) {
317
+ const current = row[fkColumn];
305
318
  return !(typeof current === "object" && current !== null);
306
319
  }
307
320
  async function resolveRelationField(db, rows, fieldName, rel, cache, publishedOnly = false) {
@@ -318,15 +331,11 @@ async function resolveRelationField(db, rows, fieldName, rel, cache, publishedOn
318
331
  row[fieldName] = byParent[row["id"]] ?? [];
319
332
  }
320
333
  } else if (rel.type === "reference") {
321
- const dropFk = rel.fk_column !== fieldName;
322
- const pending = batch.filter((r) => needsFkResolution(r, rel.fk_column, fieldName));
334
+ const pending = batch.filter((r) => needsFkResolution(r, rel.fk_column));
323
335
  if (pending.length === 0) return;
324
336
  const fkValues = pending.map((r) => r[rel.fk_column]).filter(Boolean);
325
337
  if (fkValues.length === 0) {
326
- for (const row of pending) {
327
- row[fieldName] = null;
328
- if (dropFk) delete row[rel.fk_column];
329
- }
338
+ for (const row of pending) row[rel.fk_column] = null;
330
339
  return;
331
340
  }
332
341
  const unique = [...new Set(fkValues)];
@@ -343,8 +352,7 @@ async function resolveRelationField(db, rows, fieldName, rel, cache, publishedOn
343
352
  }
344
353
  for (const row of pending) {
345
354
  const fkVal = row[rel.fk_column];
346
- row[fieldName] = fkVal ? cache.get(`${rel.table}:${fkVal}`) ?? null : null;
347
- if (dropFk) delete row[rel.fk_column];
355
+ row[rel.fk_column] = fkVal ? cache.get(`${rel.table}:${fkVal}`) ?? null : null;
348
356
  }
349
357
  } else if (rel.type === "junction") {
350
358
  const parentIds = batch.map((r) => r["id"]);
@@ -372,15 +380,11 @@ async function resolveRelationField(db, rows, fieldName, rel, cache, publishedOn
372
380
  row[fieldName] = jEntries.map((jr) => cache.get(`${rel.table}:${jr[rel.right_column]}`)).filter(Boolean);
373
381
  }
374
382
  } else if (rel.type === "media") {
375
- const dropFk = rel.fk_column !== fieldName;
376
- const pending = batch.filter((r) => needsFkResolution(r, rel.fk_column, fieldName));
383
+ const pending = batch.filter((r) => needsFkResolution(r, rel.fk_column));
377
384
  if (pending.length === 0) return;
378
385
  const fkValues = pending.map((r) => r[rel.fk_column]).filter(Boolean);
379
386
  if (fkValues.length === 0) {
380
- for (const row of pending) {
381
- row[fieldName] = null;
382
- if (dropFk) delete row[rel.fk_column];
383
- }
387
+ for (const row of pending) row[rel.fk_column] = null;
384
388
  return;
385
389
  }
386
390
  const unique = [...new Set(fkValues)];
@@ -394,8 +398,7 @@ async function resolveRelationField(db, rows, fieldName, rel, cache, publishedOn
394
398
  }
395
399
  for (const row of pending) {
396
400
  const fkVal = row[rel.fk_column];
397
- row[fieldName] = fkVal ? cache.get(`media:${fkVal}`) ?? null : null;
398
- if (dropFk) delete row[rel.fk_column];
401
+ row[rel.fk_column] = fkVal ? cache.get(`media:${fkVal}`) ?? null : null;
399
402
  }
400
403
  }
401
404
  }
@@ -677,29 +680,22 @@ function scalarFieldResolver(field, fallback) {
677
680
  function relationFieldResolver(typeName, schemaFieldName) {
678
681
  return (parent, _args, ctx) => ctx.loaders.load(typeName, schemaFieldName, parent);
679
682
  }
680
- function programmaticFieldResolver(typeName, schemaFieldName, mediaFields = [], fieldKeys) {
683
+ function programmaticFieldResolver(typeName, schemaFieldName, mediaFieldNames = [], fieldKeys) {
681
684
  return async (parent, _args, ctx) => {
682
685
  let p = ctx.programmaticMemo.get(parent);
683
686
  if (!p) {
684
- p = resolveProgrammaticRow(typeName, parent, ctx, mediaFields, fieldKeys);
687
+ p = resolveProgrammaticRow(typeName, parent, ctx, mediaFieldNames, fieldKeys);
685
688
  ctx.programmaticMemo.set(parent, p);
686
689
  }
687
690
  return (await p)[schemaFieldName];
688
691
  };
689
692
  }
690
- async function resolveProgrammaticRow(typeName, parent, ctx, mediaFields, fieldKeys) {
693
+ async function resolveProgrammaticRow(typeName, parent, ctx, mediaFieldNames, fieldKeys) {
691
694
  const toLabels = (row) => fieldKeys ? fieldKeys.toLabels(row) : row;
692
- if (mediaFields.length === 0) return ctx.resolver.resolveItem(typeName, toLabels(parent));
695
+ if (mediaFieldNames.length === 0) return ctx.resolver.resolveItem(typeName, toLabels(parent));
693
696
  const enriched = { ...parent };
694
- await Promise.all(mediaFields.map((m) => ctx.loaders.load(typeName, m.name, enriched)));
695
- return ctx.resolver.resolveItem(typeName, relabelMedia(toLabels(enriched), enriched, mediaFields));
696
- }
697
- function relabelMedia(record, enriched, mediaFields) {
698
- for (const { name, exposedAs } of mediaFields) {
699
- if (!(name in enriched)) continue;
700
- record[exposedAs] = enriched[name];
701
- }
702
- return record;
697
+ await Promise.all(mediaFieldNames.map((name) => ctx.loaders.load(typeName, name, enriched)));
698
+ return ctx.resolver.resolveItem(typeName, toLabels(enriched));
703
699
  }
704
700
  function collectionResolver(typeName, nameMap, fieldKeys) {
705
701
  return async (_root, args, ctx) => {
@@ -757,7 +753,7 @@ var init_resolvers = __esm({
757
753
  });
758
754
 
759
755
  // src/graphql/schema.ts
760
- function buildGraphQLSchema(registry, fieldKeyMaps = {}, view) {
756
+ function buildGraphQLSchema(registry, fieldKeyMaps = {}, view, programmaticKeyMaps = fieldKeyMaps) {
761
757
  const objectTypes = /* @__PURE__ */ new Map();
762
758
  const enumTypes = /* @__PURE__ */ new Map();
763
759
  for (const [name, enumType] of Object.entries(registry.enum_types)) {
@@ -821,9 +817,9 @@ function buildGraphQLSchema(registry, fieldKeyMaps = {}, view) {
821
817
  }
822
818
  function buildObjectType(machineName, type) {
823
819
  const visible = viewFieldsFor(machineName, type);
824
- const mediaFields = visible.filter(
825
- (v) => v.field.field_type === "image" || v.field.field_type === "video" || v.field.field_type === "file"
826
- ).map((v) => ({ name: v.field.name, exposedAs: v.exposedAs }));
820
+ const mediaFieldNames = type.fields.filter(
821
+ (f) => f.removed !== true && (f.field_type === "image" || f.field_type === "video" || f.field_type === "file")
822
+ ).map((f) => f.name);
827
823
  return new import_graphql5.GraphQLObjectType({
828
824
  name: graphqlTypeName(machineName),
829
825
  fields: () => {
@@ -843,7 +839,7 @@ function buildGraphQLSchema(registry, fieldKeyMaps = {}, view) {
843
839
  const outType = outputTypeForField(field, vf.required);
844
840
  let resolve;
845
841
  if (field.field_type === "programmatic") {
846
- resolve = programmaticFieldResolver(machineName, field.name, mediaFields, fieldKeyMaps[machineName]);
842
+ resolve = programmaticFieldResolver(machineName, field.name, mediaFieldNames, programmaticKeyMaps[machineName]);
847
843
  } else if (field.field_type === "reference" || field.field_type === "paragraph" || field.field_type === "image" || field.field_type === "video" || field.field_type === "file") {
848
844
  resolve = relationFieldResolver(machineName, field.name);
849
845
  } else {
@@ -888,7 +884,10 @@ function buildGraphQLSchema(registry, fieldKeyMaps = {}, view) {
888
884
  for (const [name, ct] of Object.entries(registry.content_types)) {
889
885
  const objType = objectTypes.get(name);
890
886
  const visible = viewFieldsFor(name, ct);
891
- const nameMap = buildFieldNameMap(visible.map((v) => v.exposedAs));
887
+ const nameMap = buildFieldNameMap([
888
+ ...visible.map((v) => v.exposedAs),
889
+ ...ct.system_fields.map((s) => s.name)
890
+ ]);
892
891
  if (ct.only_one) {
893
892
  queryFields[singleQueryName(name)] = { type: objType, resolve: singletonResolver(name) };
894
893
  continue;
@@ -1076,7 +1075,8 @@ function createRelationLoaders(db, registry) {
1076
1075
  async (parents) => {
1077
1076
  const rows = parents;
1078
1077
  await resolveRelationField(db, rows, fieldName, rel, cache, true);
1079
- return rows.map((r) => r[fieldName]);
1078
+ const resultKey = rel.type === "reference" || rel.type === "media" ? rel.fk_column : fieldName;
1079
+ return rows.map((r) => r[resultKey]);
1080
1080
  },
1081
1081
  // Batch within a tick; do not memoize by parent identity (rows are mutated
1082
1082
  // and may recur across nesting levels).
@@ -1161,7 +1161,7 @@ function createGraphQLHandler(registry, repos, fieldKeyMaps, resolver, db, optio
1161
1161
  currentVersion: versioning.currentVersion
1162
1162
  });
1163
1163
  }
1164
- const schema = buildGraphQLSchema(registry, fieldKeyMaps, view);
1164
+ const schema = buildGraphQLSchema(registry, fieldKeyMaps, view, versioning.currentFieldKeyMaps ?? fieldKeyMaps);
1165
1165
  const { plugins } = buildArmorPlugin({
1166
1166
  maxDepth: options.maxDepth,
1167
1167
  maxComplexity: options.maxComplexity
@@ -1817,18 +1817,21 @@ function createProgrammaticResolver(resolvers, options) {
1817
1817
  }
1818
1818
 
1819
1819
  // src/projector.ts
1820
+ init_field_keys();
1820
1821
  function isPlainRow(v) {
1821
1822
  return typeof v === "object" && v !== null && !Array.isArray(v) && !(v instanceof Date);
1822
1823
  }
1823
1824
  var MEDIA_FIELD_TYPES = /* @__PURE__ */ new Set(["image", "video", "file"]);
1824
- function nestedTargets(fields) {
1825
+ function nestedTargets(fields, map) {
1825
1826
  const out = [];
1826
1827
  for (const f of fields) {
1827
1828
  if (MEDIA_FIELD_TYPES.has(f.field_type)) continue;
1828
1829
  if (f.field_type !== "paragraph" && f.field_type !== "reference") continue;
1829
1830
  const ref = f.ui_component.ref;
1830
1831
  if (!ref) continue;
1831
- out.push({ label: f.name, target: ref });
1832
+ const label = isColumnBacked(f) ? map.labelFor(f.db_column.column_name) : f.name;
1833
+ if (label === void 0) continue;
1834
+ out.push({ label, target: ref });
1832
1835
  }
1833
1836
  return out;
1834
1837
  }
@@ -1846,7 +1849,7 @@ function buildProjectors(registry, fieldKeyMaps, fallbacks) {
1846
1849
  const typeFallbacks = fallbacks?.[typeName];
1847
1850
  projectors[typeName] = {
1848
1851
  map,
1849
- nested: nestedTargets(type.fields),
1852
+ nested: nestedTargets(type.fields, map),
1850
1853
  // Spread rather than assign: with exactOptionalPropertyTypes, an optional
1851
1854
  // property may be ABSENT but not present-and-undefined.
1852
1855
  ...typeFallbacks !== void 0 && { fallbacks: typeFallbacks }
@@ -1933,7 +1936,28 @@ function parseFilters(url, validFields, columnFor) {
1933
1936
  function isPublished(item) {
1934
1937
  return item["published"] === true;
1935
1938
  }
1936
- function registerPublicContentRoutes(app, registry, repos, projectors, paths, listRateLimit, resolver) {
1939
+ function registerPublicContentRoutes(app, registry, repos, projectors, paths, listRateLimit, resolver, resolverProjectors) {
1940
+ const forResolver = resolverProjectors ?? projectors;
1941
+ function pickProgrammatic(resolved, names) {
1942
+ const out = {};
1943
+ for (const name of names) if (name in resolved) out[name] = resolved[name];
1944
+ return out;
1945
+ }
1946
+ async function respondItem(row, typeName, programmatic) {
1947
+ const data = projectRow(row, typeName, projectors);
1948
+ if (!resolver?.hasSchema(typeName)) return data;
1949
+ const resolved = await resolver.resolveItem(typeName, projectRow(row, typeName, forResolver));
1950
+ return { ...data, ...pickProgrammatic(resolved, programmatic) };
1951
+ }
1952
+ async function respondList(rows, typeName, programmatic) {
1953
+ const data = rows.map((row) => projectRow(row, typeName, projectors));
1954
+ if (!resolver?.hasSchema(typeName)) return data;
1955
+ const resolved = await resolver.resolveList(
1956
+ typeName,
1957
+ rows.map((row) => projectRow(row, typeName, forResolver))
1958
+ );
1959
+ return data.map((d, i) => ({ ...d, ...pickProgrammatic(resolved[i], programmatic) }));
1960
+ }
1937
1961
  function registerListRoute(path, handler) {
1938
1962
  if (listRateLimit) {
1939
1963
  app.get(path, listRateLimit, handler);
@@ -1957,18 +1981,29 @@ function registerPublicContentRoutes(app, registry, repos, projectors, paths, li
1957
1981
  return c.json({ ok: true, data });
1958
1982
  });
1959
1983
  for (const [typeName, contentType] of Object.entries(registry.content_types)) {
1984
+ let translateInclude2 = function(requested) {
1985
+ const include = [];
1986
+ for (const field of requested) {
1987
+ const registryName = includeNames.get(field);
1988
+ if (registryName === void 0) return { ok: false, field };
1989
+ include.push(registryName);
1990
+ }
1991
+ return { ok: true, include };
1992
+ };
1993
+ var translateInclude = translateInclude2;
1960
1994
  const basePath = contentType.default_base_path;
1961
1995
  const repo = repos[typeName];
1962
1996
  if (!repo) continue;
1963
1997
  const fieldKeys = projectors[typeName].map;
1964
- const relationFieldNames = /* @__PURE__ */ new Set();
1998
+ const programmatic = contentType.fields.filter((f) => f.field_type === "programmatic").map((f) => f.name);
1999
+ const includeNames = /* @__PURE__ */ new Map();
1965
2000
  for (const f of contentType.fields) {
1966
2001
  if (!RELATION_FIELD_TYPES.has(f.field_type)) continue;
1967
2002
  if (isColumnBacked(f)) {
1968
2003
  const label = fieldKeys.labelFor(f.db_column.column_name);
1969
- if (label !== void 0) relationFieldNames.add(label);
2004
+ if (label !== void 0) includeNames.set(label, f.name);
1970
2005
  } else {
1971
- relationFieldNames.add(f.name);
2006
+ includeNames.set(f.name, f.name);
1972
2007
  }
1973
2008
  }
1974
2009
  const filterableFieldNames = /* @__PURE__ */ new Set([
@@ -1987,8 +2022,11 @@ function registerPublicContentRoutes(app, registry, repos, projectors, paths, li
1987
2022
  404
1988
2023
  );
1989
2024
  }
1990
- let data = projectRow(result.data[0], typeName, projectors);
1991
- if (resolver?.hasSchema(typeName)) data = await resolver.resolveItem(typeName, data);
2025
+ const data = await respondItem(
2026
+ result.data[0],
2027
+ typeName,
2028
+ programmatic
2029
+ );
1992
2030
  return c.json({ ok: true, data });
1993
2031
  });
1994
2032
  } else {
@@ -2045,21 +2083,20 @@ function registerPublicContentRoutes(app, registry, repos, projectors, paths, li
2045
2083
  400
2046
2084
  );
2047
2085
  }
2048
- const include = parseInclude(c.req.query("include"));
2049
- for (const field of include) {
2050
- if (!relationFieldNames.has(field)) {
2051
- return c.json(
2052
- {
2053
- ok: false,
2054
- error: {
2055
- code: "INVALID_INCLUDE_FIELD",
2056
- message: `'${field}' is not a valid relation field`
2057
- }
2058
- },
2059
- 400
2060
- );
2061
- }
2086
+ const translated = translateInclude2(parseInclude(c.req.query("include")));
2087
+ if (!translated.ok) {
2088
+ return c.json(
2089
+ {
2090
+ ok: false,
2091
+ error: {
2092
+ code: "INVALID_INCLUDE_FIELD",
2093
+ message: `'${translated.field}' is not a valid relation field`
2094
+ }
2095
+ },
2096
+ 400
2097
+ );
2062
2098
  }
2099
+ const include = translated.include;
2063
2100
  const sortColumn = fieldKeys.labels.includes(sortBy) ? fieldKeys.columnFor(sortBy) : sortBy;
2064
2101
  const result = await repo.findMany({
2065
2102
  published_only: true,
@@ -2070,29 +2107,25 @@ function registerPublicContentRoutes(app, registry, repos, projectors, paths, li
2070
2107
  filters: filtersResult.filters,
2071
2108
  include
2072
2109
  });
2073
- const labeled = result.data.map(
2074
- (row) => projectRow(row, typeName, projectors)
2075
- );
2076
- const data = resolver?.hasSchema(typeName) ? await resolver.resolveList(typeName, labeled) : labeled;
2110
+ const data = await respondList(result.data, typeName, programmatic);
2077
2111
  return c.json({ ...result, data });
2078
2112
  });
2079
2113
  app.get(paths.item(basePath), async (c) => {
2080
2114
  const slug = c.req.param("slug");
2081
- const include = parseInclude(c.req.query("include"));
2082
- for (const field of include) {
2083
- if (!relationFieldNames.has(field)) {
2084
- return c.json(
2085
- {
2086
- ok: false,
2087
- error: {
2088
- code: "INVALID_INCLUDE_FIELD",
2089
- message: `'${field}' is not a valid relation field`
2090
- }
2091
- },
2092
- 400
2093
- );
2094
- }
2115
+ const translated = translateInclude2(parseInclude(c.req.query("include")));
2116
+ if (!translated.ok) {
2117
+ return c.json(
2118
+ {
2119
+ ok: false,
2120
+ error: {
2121
+ code: "INVALID_INCLUDE_FIELD",
2122
+ message: `'${translated.field}' is not a valid relation field`
2123
+ }
2124
+ },
2125
+ 400
2126
+ );
2095
2127
  }
2128
+ const include = translated.include;
2096
2129
  const item = await repo.findBySlug(slug, include);
2097
2130
  if (!item || !isPublished(item)) {
2098
2131
  return c.json(
@@ -2103,15 +2136,15 @@ function registerPublicContentRoutes(app, registry, repos, projectors, paths, li
2103
2136
  404
2104
2137
  );
2105
2138
  }
2106
- let data = projectRow(item, typeName, projectors);
2107
- if (resolver?.hasSchema(typeName)) data = await resolver.resolveItem(typeName, data);
2139
+ const data = await respondItem(item, typeName, programmatic);
2108
2140
  return c.json({ ok: true, data });
2109
2141
  });
2110
2142
  }
2111
2143
  }
2112
- for (const [typeName] of Object.entries(registry.taxonomy_types)) {
2144
+ for (const [typeName, taxonomyType] of Object.entries(registry.taxonomy_types)) {
2113
2145
  const repo = repos[typeName];
2114
2146
  if (!repo) continue;
2147
+ const programmatic = taxonomyType.fields.filter((f) => f.field_type === "programmatic").map((f) => f.name);
2115
2148
  registerListRoute(paths.taxonomyCollection(typeName), async (c) => {
2116
2149
  const pagination = parsePagination(c.req.query("page"), c.req.query("per_page"));
2117
2150
  if (!pagination.ok) {
@@ -2131,10 +2164,7 @@ function registerPublicContentRoutes(app, registry, repos, projectors, paths, li
2131
2164
  page: pagination.page,
2132
2165
  per_page: pagination.per_page
2133
2166
  });
2134
- const labeled = result.data.map(
2135
- (row) => projectRow(row, typeName, projectors)
2136
- );
2137
- const data = resolver?.hasSchema(typeName) ? await resolver.resolveList(typeName, labeled) : labeled;
2167
+ const data = await respondList(result.data, typeName, programmatic);
2138
2168
  return c.json({ ...result, data });
2139
2169
  });
2140
2170
  app.get(paths.taxonomyItem(typeName), async (c) => {
@@ -2149,8 +2179,7 @@ function registerPublicContentRoutes(app, registry, repos, projectors, paths, li
2149
2179
  404
2150
2180
  );
2151
2181
  }
2152
- let data = projectRow(item, typeName, projectors);
2153
- if (resolver?.hasSchema(typeName)) data = await resolver.resolveItem(typeName, data);
2182
+ const data = await respondItem(item, typeName, programmatic);
2154
2183
  return c.json({ ok: true, data });
2155
2184
  });
2156
2185
  }
@@ -3942,7 +3971,7 @@ function registerUserRoutes(app, db, requirePermission, requireHierarchy) {
3942
3971
  var import_drizzle_orm6 = require("drizzle-orm");
3943
3972
  var API_VERSION = (() => {
3944
3973
  try {
3945
- return "0.6.0";
3974
+ return "0.7.0";
3946
3975
  } catch {
3947
3976
  return "0.0.0";
3948
3977
  }
@@ -4606,7 +4635,7 @@ function createCmsApp(options) {
4606
4635
  app.use(path, attachDeprecationHeaders);
4607
4636
  }
4608
4637
  }
4609
- registerPublicContentRoutes(app, registry, surface.repos, surface.projectors, surface.paths, listRateLimit, programmaticResolver);
4638
+ registerPublicContentRoutes(app, registry, surface.repos, surface.projectors, surface.paths, listRateLimit, programmaticResolver, projectors);
4610
4639
  }
4611
4640
  if (options.versions !== void 0) {
4612
4641
  app.all(`${prefix}/:version/*`, async (c, next) => {
@@ -4672,7 +4701,8 @@ function createCmsApp(options) {
4672
4701
  endpoint: mount.path,
4673
4702
  currentVersion: model.current,
4674
4703
  ...projection !== void 0 && { projection },
4675
- ...currentProjection !== void 0 && { currentProjection }
4704
+ ...currentProjection !== void 0 && { currentProjection },
4705
+ currentFieldKeyMaps: fieldKeyMaps
4676
4706
  }
4677
4707
  );
4678
4708
  } catch (err) {