@bobbykim/manguito-cms-api 0.5.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,14 +38,15 @@ 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) {
45
45
  labelToColumn.set(label, column);
46
46
  columnToLabel.set(column, label);
47
47
  }
48
- for (const label of allLabels) {
48
+ for (const { label, ownColumn } of allLabels) {
49
+ if (ownColumn !== void 0) continue;
49
50
  const columnOwner = columnToLabel.get(label);
50
51
  if (columnOwner !== void 0 && columnOwner !== label) {
51
52
  throw new Error(
@@ -54,23 +55,33 @@ function buildFieldKeyMap(pairs, allLabels, droppedKeys) {
54
55
  }
55
56
  }
56
57
  for (const label of labelToColumn.keys()) {
57
- if (droppedKeys.has(label)) labelToColumn.delete(label);
58
+ if (droppedLabels.has(label)) labelToColumn.delete(label);
58
59
  }
59
60
  for (const column of columnToLabel.keys()) {
60
- if (droppedKeys.has(column)) columnToLabel.delete(column);
61
+ if (droppedColumns.has(column)) columnToLabel.delete(column);
61
62
  }
62
63
  const diverges = [...labelToColumn].some(([label, column]) => label !== column);
63
- function remap(input, lookup) {
64
+ function remap(input, lookup, dropped, droppedIfUnmapped) {
64
65
  const out = {};
65
66
  for (const key of Object.keys(input)) {
66
- if (droppedKeys.has(key)) continue;
67
- 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];
68
74
  }
69
75
  return out;
70
76
  }
71
77
  return {
72
- toStorage: (input) => remap(input, labelToColumn),
73
- 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),
74
85
  columnFor: (label) => labelToColumn.get(label),
75
86
  labelFor: (column) => columnToLabel.get(column),
76
87
  labels: [...labelToColumn.keys()],
@@ -78,37 +89,52 @@ function buildFieldKeyMap(pairs, allLabels, droppedKeys) {
78
89
  };
79
90
  }
80
91
  function createFieldKeyMap(fields) {
81
- const droppedKeys = /* @__PURE__ */ new Set();
92
+ const droppedColumns = /* @__PURE__ */ new Set();
93
+ const droppedLabels = /* @__PURE__ */ new Set();
82
94
  const pairs = [];
83
95
  for (const f of fields) {
84
96
  if (!isColumnBacked(f)) continue;
85
97
  pairs.push({ label: f.name, column: f.db_column.column_name });
86
98
  if (f.removed === true) {
87
- droppedKeys.add(f.name);
88
- droppedKeys.add(f.db_column.column_name);
99
+ droppedLabels.add(f.name);
100
+ droppedColumns.add(f.db_column.column_name);
89
101
  }
90
102
  }
91
103
  return buildFieldKeyMap(
92
104
  pairs,
93
- fields.map((f) => f.name),
94
- droppedKeys
105
+ fields.map((f) => ({
106
+ label: f.name,
107
+ // Conditional spread, not a plain assignment — exactOptionalPropertyTypes
108
+ // forbids present-and-undefined.
109
+ ...isColumnBacked(f) && { ownColumn: f.db_column.column_name }
110
+ })),
111
+ droppedColumns,
112
+ droppedLabels
95
113
  );
96
114
  }
97
115
  function createFieldKeyMapFromProjection(projectionType, allFields) {
98
116
  const pairs = projectionType.fields.map((f) => ({ label: f.exposed_as, column: f.column_name }));
99
- const projected = new Set(pairs.map((p) => p.label));
100
- 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();
101
121
  for (const f of allFields) {
102
122
  if (!isColumnBacked(f)) continue;
103
- dropped.add(f.name);
104
- 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);
105
125
  }
106
- for (const p of pairs) dropped.delete(p.column);
107
- for (const label of projected) dropped.delete(label);
108
126
  return buildFieldKeyMap(
109
127
  pairs,
110
- allFields.map((f) => f.name),
111
- dropped
128
+ allFields.map((f) => ({
129
+ label: f.name,
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.
134
+ ...isColumnBacked(f) && { ownColumn: f.db_column.column_name }
135
+ })),
136
+ droppedColumns,
137
+ droppedLabels
112
138
  );
113
139
  }
114
140
  var init_field_keys = __esm({
@@ -287,9 +313,8 @@ function groupBy(items, key) {
287
313
  }
288
314
  return result;
289
315
  }
290
- function needsFkResolution(row, fkColumn, fieldName) {
291
- if (fkColumn !== fieldName && !(fkColumn in row)) return false;
292
- const current = row[fieldName];
316
+ function needsFkResolution(row, fkColumn) {
317
+ const current = row[fkColumn];
293
318
  return !(typeof current === "object" && current !== null);
294
319
  }
295
320
  async function resolveRelationField(db, rows, fieldName, rel, cache, publishedOnly = false) {
@@ -306,15 +331,11 @@ async function resolveRelationField(db, rows, fieldName, rel, cache, publishedOn
306
331
  row[fieldName] = byParent[row["id"]] ?? [];
307
332
  }
308
333
  } else if (rel.type === "reference") {
309
- const dropFk = rel.fk_column !== fieldName;
310
- const pending = batch.filter((r) => needsFkResolution(r, rel.fk_column, fieldName));
334
+ const pending = batch.filter((r) => needsFkResolution(r, rel.fk_column));
311
335
  if (pending.length === 0) return;
312
336
  const fkValues = pending.map((r) => r[rel.fk_column]).filter(Boolean);
313
337
  if (fkValues.length === 0) {
314
- for (const row of pending) {
315
- row[fieldName] = null;
316
- if (dropFk) delete row[rel.fk_column];
317
- }
338
+ for (const row of pending) row[rel.fk_column] = null;
318
339
  return;
319
340
  }
320
341
  const unique = [...new Set(fkValues)];
@@ -331,8 +352,7 @@ async function resolveRelationField(db, rows, fieldName, rel, cache, publishedOn
331
352
  }
332
353
  for (const row of pending) {
333
354
  const fkVal = row[rel.fk_column];
334
- row[fieldName] = fkVal ? cache.get(`${rel.table}:${fkVal}`) ?? null : null;
335
- if (dropFk) delete row[rel.fk_column];
355
+ row[rel.fk_column] = fkVal ? cache.get(`${rel.table}:${fkVal}`) ?? null : null;
336
356
  }
337
357
  } else if (rel.type === "junction") {
338
358
  const parentIds = batch.map((r) => r["id"]);
@@ -360,15 +380,11 @@ async function resolveRelationField(db, rows, fieldName, rel, cache, publishedOn
360
380
  row[fieldName] = jEntries.map((jr) => cache.get(`${rel.table}:${jr[rel.right_column]}`)).filter(Boolean);
361
381
  }
362
382
  } else if (rel.type === "media") {
363
- const dropFk = rel.fk_column !== fieldName;
364
- const pending = batch.filter((r) => needsFkResolution(r, rel.fk_column, fieldName));
383
+ const pending = batch.filter((r) => needsFkResolution(r, rel.fk_column));
365
384
  if (pending.length === 0) return;
366
385
  const fkValues = pending.map((r) => r[rel.fk_column]).filter(Boolean);
367
386
  if (fkValues.length === 0) {
368
- for (const row of pending) {
369
- row[fieldName] = null;
370
- if (dropFk) delete row[rel.fk_column];
371
- }
387
+ for (const row of pending) row[rel.fk_column] = null;
372
388
  return;
373
389
  }
374
390
  const unique = [...new Set(fkValues)];
@@ -382,8 +398,7 @@ async function resolveRelationField(db, rows, fieldName, rel, cache, publishedOn
382
398
  }
383
399
  for (const row of pending) {
384
400
  const fkVal = row[rel.fk_column];
385
- row[fieldName] = fkVal ? cache.get(`media:${fkVal}`) ?? null : null;
386
- if (dropFk) delete row[rel.fk_column];
401
+ row[rel.fk_column] = fkVal ? cache.get(`media:${fkVal}`) ?? null : null;
387
402
  }
388
403
  }
389
404
  }
@@ -537,10 +552,13 @@ var init_naming = __esm({
537
552
  });
538
553
 
539
554
  // src/graphql/filters.ts
540
- function buildSortFieldEnum(typeName) {
555
+ function buildSortFieldEnum(typeName, fieldKeys) {
556
+ const resolvable = SORTABLE.filter(
557
+ (s) => s.gql === "createdAt" || s.gql === "updatedAt" || fieldKeys === void 0 || fieldKeys.columnFor(s.value) !== void 0
558
+ );
541
559
  return new import_graphql3.GraphQLEnumType({
542
560
  name: `${typeName}SortField`,
543
- values: Object.fromEntries(SORTABLE.map((s) => [s.gql, { value: s.value }]))
561
+ values: Object.fromEntries(resolvable.map((s) => [s.gql, { value: s.value }]))
544
562
  });
545
563
  }
546
564
  function comparableFilterInput(name, scalar) {
@@ -575,18 +593,19 @@ function filterInputForField(fieldType) {
575
593
  if (scalar === null) return null;
576
594
  return StringFilter;
577
595
  }
578
- function buildFilterInputType(type) {
596
+ function buildFilterInputType(type, viewFields) {
579
597
  const fields = {};
580
598
  fields["id"] = { type: IDFilter };
581
599
  fields["published"] = { type: BooleanFilter };
582
600
  fields["createdAt"] = { type: DateTimeFilter };
583
601
  fields["updatedAt"] = { type: DateTimeFilter };
584
602
  if (type.schema_type === "content-type") fields["slug"] = { type: StringFilter };
585
- for (const f of type.fields) {
603
+ const entries = viewFields !== void 0 ? viewFields.map((v) => ({ field: v.field, exposedAs: v.exposedAs })) : type.fields.filter((f) => f.removed !== true).map((f) => ({ field: f, exposedAs: f.name }));
604
+ for (const { field: f, exposedAs } of entries) {
586
605
  if (f.field_type === "programmatic" || f.field_type === "paragraph") continue;
587
606
  if (f.field_type === "image" || f.field_type === "video" || f.field_type === "file") continue;
588
607
  const input = filterInputForField(f.field_type);
589
- if (input) fields[buildFieldNameMap([f.name]).toGraphql(f.name)] = { type: input };
608
+ if (input) fields[buildFieldNameMap([exposedAs]).toGraphql(exposedAs)] = { type: input };
590
609
  }
591
610
  if (Object.keys(fields).length === 0) return null;
592
611
  return new import_graphql3.GraphQLInputObjectType({ name: `${graphqlTypeName(type.name)}Filter`, fields });
@@ -649,12 +668,14 @@ function codeError2(code, message) {
649
668
  function isPublished2(item) {
650
669
  return !!item && item["published"] === true;
651
670
  }
652
- function resolveFieldValue(field, row) {
671
+ function resolveFieldValue(field, row, fallback) {
653
672
  const key = isColumnBacked(field) ? field.db_column.column_name : field.name;
654
- return row[key] ?? null;
673
+ const value = row[key];
674
+ if ((value === null || value === void 0) && fallback !== void 0) return fallback;
675
+ return value ?? null;
655
676
  }
656
- function scalarFieldResolver(field) {
657
- return (parent) => resolveFieldValue(field, parent);
677
+ function scalarFieldResolver(field, fallback) {
678
+ return (parent) => resolveFieldValue(field, parent, fallback);
658
679
  }
659
680
  function relationFieldResolver(typeName, schemaFieldName) {
660
681
  return (parent, _args, ctx) => ctx.loaders.load(typeName, schemaFieldName, parent);
@@ -732,7 +753,7 @@ var init_resolvers = __esm({
732
753
  });
733
754
 
734
755
  // src/graphql/schema.ts
735
- function buildGraphQLSchema(registry, fieldKeyMaps = {}) {
756
+ function buildGraphQLSchema(registry, fieldKeyMaps = {}, view, programmaticKeyMaps = fieldKeyMaps) {
736
757
  const objectTypes = /* @__PURE__ */ new Map();
737
758
  const enumTypes = /* @__PURE__ */ new Map();
738
759
  for (const [name, enumType] of Object.entries(registry.enum_types)) {
@@ -751,14 +772,14 @@ function buildGraphQLSchema(registry, fieldKeyMaps = {}) {
751
772
  );
752
773
  }
753
774
  }
754
- function outputTypeForField(field) {
775
+ function outputTypeForField(field, required) {
755
776
  const scalar = scalarOutputType(field.field_type);
756
- if (scalar) return field.required ? new import_graphql5.GraphQLNonNull(scalar) : scalar;
777
+ if (scalar) return required ? new import_graphql5.GraphQLNonNull(scalar) : scalar;
757
778
  if (field.field_type === "enum") {
758
779
  const ref = field.ui_component.component === "select" ? field.ui_component.enum_ref : void 0;
759
780
  const et = ref ? enumTypes.get(ref) : void 0;
760
781
  const t = et ?? import_graphql5.GraphQLString;
761
- return field.required ? new import_graphql5.GraphQLNonNull(t) : t;
782
+ return required ? new import_graphql5.GraphQLNonNull(t) : t;
762
783
  }
763
784
  if (field.field_type === "image" || field.field_type === "video" || field.field_type === "file") {
764
785
  return MEDIA;
@@ -788,8 +809,17 @@ function buildGraphQLSchema(registry, fieldKeyMaps = {}) {
788
809
  }
789
810
  return import_graphql5.GraphQLString;
790
811
  }
812
+ function exposedFields(type) {
813
+ return type.fields.filter((f) => f.removed !== true);
814
+ }
815
+ function viewFieldsFor(machineName, type) {
816
+ return view?.[machineName]?.fields ?? exposedFields(type).map((f) => ({ field: f, exposedAs: f.name, required: f.required }));
817
+ }
791
818
  function buildObjectType(machineName, type) {
792
- const mediaFieldNames = type.fields.filter((f) => f.field_type === "image" || f.field_type === "video" || f.field_type === "file").map((f) => f.name);
819
+ const visible = viewFieldsFor(machineName, type);
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);
793
823
  return new import_graphql5.GraphQLObjectType({
794
824
  name: graphqlTypeName(machineName),
795
825
  fields: () => {
@@ -803,23 +833,25 @@ function buildGraphQLSchema(registry, fieldKeyMaps = {}) {
803
833
  }
804
834
  fields["createdAt"] = { type: new import_graphql5.GraphQLNonNull(DateTimeScalar), resolve: (p) => p["created_at"] };
805
835
  fields["updatedAt"] = { type: new import_graphql5.GraphQLNonNull(DateTimeScalar), resolve: (p) => p["updated_at"] };
806
- for (const field of type.fields) {
807
- const gqlName = toCamelCase(field.name);
808
- const outType = outputTypeForField(field);
836
+ for (const vf of visible) {
837
+ const field = vf.field;
838
+ const gqlName = toCamelCase(vf.exposedAs);
839
+ const outType = outputTypeForField(field, vf.required);
809
840
  let resolve;
810
841
  if (field.field_type === "programmatic") {
811
- resolve = programmaticFieldResolver(
812
- machineName,
813
- field.name,
814
- mediaFieldNames,
815
- fieldKeyMaps[machineName]
816
- );
842
+ resolve = programmaticFieldResolver(machineName, field.name, mediaFieldNames, programmaticKeyMaps[machineName]);
817
843
  } else if (field.field_type === "reference" || field.field_type === "paragraph" || field.field_type === "image" || field.field_type === "video" || field.field_type === "file") {
818
844
  resolve = relationFieldResolver(machineName, field.name);
819
845
  } else {
820
- resolve = scalarFieldResolver(field);
846
+ resolve = scalarFieldResolver(field, vf.fallback);
821
847
  }
822
- fields[gqlName] = { type: outType, resolve };
848
+ fields[gqlName] = {
849
+ type: outType,
850
+ resolve,
851
+ // Conditional spread, not `deprecationReason: vf.deprecationReason`
852
+ // — exactOptionalPropertyTypes forbids present-and-undefined.
853
+ ...vf.deprecationReason !== void 0 && { deprecationReason: vf.deprecationReason }
854
+ };
823
855
  }
824
856
  return fields;
825
857
  }
@@ -851,7 +883,11 @@ function buildGraphQLSchema(registry, fieldKeyMaps = {}) {
851
883
  const queryFields = {};
852
884
  for (const [name, ct] of Object.entries(registry.content_types)) {
853
885
  const objType = objectTypes.get(name);
854
- const nameMap = buildFieldNameMap(ct.fields.map((f) => f.name));
886
+ const visible = viewFieldsFor(name, ct);
887
+ const nameMap = buildFieldNameMap([
888
+ ...visible.map((v) => v.exposedAs),
889
+ ...ct.system_fields.map((s) => s.name)
890
+ ]);
855
891
  if (ct.only_one) {
856
892
  queryFields[singleQueryName(name)] = { type: objType, resolve: singletonResolver(name) };
857
893
  continue;
@@ -863,13 +899,13 @@ function buildGraphQLSchema(registry, fieldKeyMaps = {}) {
863
899
  meta: { type: new import_graphql5.GraphQLNonNull(PAGE_META), resolve: (r) => r.meta }
864
900
  }
865
901
  });
866
- const filterType = buildFilterInputType(ct);
902
+ const filterType = buildFilterInputType(ct, visible);
867
903
  queryFields[collectionQueryName(name)] = {
868
904
  type: new import_graphql5.GraphQLNonNull(listType),
869
905
  args: {
870
906
  page: { type: import_graphql5.GraphQLInt },
871
907
  perPage: { type: import_graphql5.GraphQLInt },
872
- sortBy: { type: buildSortFieldEnum(graphqlTypeName(name)) },
908
+ sortBy: { type: buildSortFieldEnum(graphqlTypeName(name), fieldKeyMaps[name]) },
873
909
  sortOrder: { type: SortOrderEnum },
874
910
  ...filterType ? { filter: { type: filterType } } : {}
875
911
  },
@@ -893,7 +929,11 @@ function buildGraphQLSchema(registry, fieldKeyMaps = {}) {
893
929
  queryFields[collectionQueryName(name)] = {
894
930
  type: new import_graphql5.GraphQLNonNull(listType),
895
931
  args: { page: { type: import_graphql5.GraphQLInt }, perPage: { type: import_graphql5.GraphQLInt } },
896
- resolve: collectionResolver(name, buildFieldNameMap(tt.fields.map((f) => f.name)), fieldKeyMaps[name])
932
+ resolve: collectionResolver(
933
+ name,
934
+ buildFieldNameMap(viewFieldsFor(name, tt).map((v) => v.exposedAs)),
935
+ fieldKeyMaps[name]
936
+ )
897
937
  };
898
938
  queryFields[singleQueryName(name)] = {
899
939
  type: objType,
@@ -943,6 +983,76 @@ var init_schema = __esm({
943
983
  }
944
984
  });
945
985
 
986
+ // src/graphql/version-view.ts
987
+ function byColumn(fields) {
988
+ const out = /* @__PURE__ */ new Map();
989
+ for (const f of fields) {
990
+ if (!isColumnBacked(f)) continue;
991
+ out.set(f.db_column.column_name, f);
992
+ }
993
+ return out;
994
+ }
995
+ function deprecationFor(field, currentLabels, currentVersion) {
996
+ const currentLabel = currentLabels.get(field.column_name);
997
+ if (currentLabel === void 0) {
998
+ return `Removed in ${currentVersion}; column retained while this version is live.`;
999
+ }
1000
+ if (currentLabel !== field.exposed_as) {
1001
+ return `Renamed to '${currentLabel}' in ${currentVersion}.`;
1002
+ }
1003
+ return void 0;
1004
+ }
1005
+ function buildVersionView(input) {
1006
+ const { registry, projection, currentProjection, currentVersion } = input;
1007
+ const out = {};
1008
+ const sources = [
1009
+ registry.content_types,
1010
+ registry.taxonomy_types,
1011
+ registry.paragraph_types
1012
+ ];
1013
+ for (const source of sources) {
1014
+ for (const [typeName, type] of Object.entries(source)) {
1015
+ const projectionType = projection?.types[typeName];
1016
+ if (projectionType === void 0) {
1017
+ out[typeName] = {
1018
+ fields: type.fields.filter((f) => f.removed !== true).map((f) => ({ field: f, exposedAs: f.name, required: f.required }))
1019
+ };
1020
+ continue;
1021
+ }
1022
+ const columns = byColumn(type.fields);
1023
+ const currentLabels = /* @__PURE__ */ new Map();
1024
+ for (const f of currentProjection?.types[typeName]?.fields ?? []) {
1025
+ currentLabels.set(f.column_name, f.exposed_as);
1026
+ }
1027
+ const fields = [];
1028
+ for (const pf of projectionType.fields) {
1029
+ const field = columns.get(pf.column_name);
1030
+ if (field === void 0) continue;
1031
+ const reason = deprecationFor(pf, currentLabels, currentVersion);
1032
+ fields.push({
1033
+ field,
1034
+ exposedAs: pf.exposed_as,
1035
+ required: pf.required,
1036
+ ...pf.fallback !== void 0 && { fallback: pf.fallback },
1037
+ ...reason !== void 0 && { deprecationReason: reason }
1038
+ });
1039
+ }
1040
+ for (const f of type.fields) {
1041
+ if (isColumnBacked(f) || f.removed === true) continue;
1042
+ fields.push({ field: f, exposedAs: f.name, required: f.required });
1043
+ }
1044
+ out[typeName] = { fields };
1045
+ }
1046
+ }
1047
+ return out;
1048
+ }
1049
+ var init_version_view = __esm({
1050
+ "src/graphql/version-view.ts"() {
1051
+ "use strict";
1052
+ init_field_keys();
1053
+ }
1054
+ });
1055
+
946
1056
  // src/graphql/dataloaders.ts
947
1057
  function createRelationLoaders(db, registry) {
948
1058
  const relMaps = /* @__PURE__ */ new Map();
@@ -965,7 +1075,8 @@ function createRelationLoaders(db, registry) {
965
1075
  async (parents) => {
966
1076
  const rows = parents;
967
1077
  await resolveRelationField(db, rows, fieldName, rel, cache, true);
968
- 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]);
969
1080
  },
970
1081
  // Batch within a tick; do not memoize by parent identity (rows are mutated
971
1082
  // and may recur across nesting levels).
@@ -1035,15 +1146,29 @@ function isClientSafeGraphQLError(error) {
1035
1146
  }
1036
1147
  return false;
1037
1148
  }
1038
- function createGraphQLHandler(registry, repos, fieldKeyMaps, resolver, db, options) {
1039
- const schema = buildGraphQLSchema(registry, fieldKeyMaps);
1149
+ function createGraphQLHandler(registry, repos, fieldKeyMaps, resolver, db, options, versioning = {}) {
1150
+ let view;
1151
+ if (versioning.projection !== void 0) {
1152
+ if (versioning.currentVersion === void 0) {
1153
+ throw new Error(
1154
+ 'createGraphQLHandler: currentVersion is required when a projection is supplied \u2014 deprecation reasons name it, and an empty one ships "Removed in ." into published SDL.'
1155
+ );
1156
+ }
1157
+ view = buildVersionView({
1158
+ registry,
1159
+ projection: versioning.projection,
1160
+ currentProjection: versioning.currentProjection,
1161
+ currentVersion: versioning.currentVersion
1162
+ });
1163
+ }
1164
+ const schema = buildGraphQLSchema(registry, fieldKeyMaps, view, versioning.currentFieldKeyMaps ?? fieldKeyMaps);
1040
1165
  const { plugins } = buildArmorPlugin({
1041
1166
  maxDepth: options.maxDepth,
1042
1167
  maxComplexity: options.maxComplexity
1043
1168
  });
1044
1169
  const yoga = (0, import_graphql_yoga.createYoga)({
1045
1170
  schema,
1046
- graphqlEndpoint: "/graphql",
1171
+ graphqlEndpoint: versioning.endpoint ?? "/graphql",
1047
1172
  graphiql: options.graphiql,
1048
1173
  landingPage: false,
1049
1174
  maskedErrors: {
@@ -1071,6 +1196,7 @@ var init_handler = __esm({
1071
1196
  "use strict";
1072
1197
  import_graphql_yoga = require("graphql-yoga");
1073
1198
  init_schema();
1199
+ init_version_view();
1074
1200
  init_dataloaders();
1075
1201
  init_security();
1076
1202
  }
@@ -1180,7 +1306,7 @@ function createSecurityHeadersMiddleware(options = {}) {
1180
1306
  c.res.headers.set("X-Content-Type-Options", "nosniff");
1181
1307
  c.res.headers.set("X-Frame-Options", "DENY");
1182
1308
  c.res.headers.set("Referrer-Policy", "no-referrer");
1183
- const isGraphiql = graphiqlPath !== void 0 && c.req.path === graphiqlPath;
1309
+ const isGraphiql = graphiqlPath !== void 0 && (c.req.path === graphiqlPath || c.req.path.startsWith(`${graphiqlPath}/`));
1184
1310
  c.res.headers.set("Content-Security-Policy", isGraphiql ? graphiqlCsp : csp);
1185
1311
  };
1186
1312
  }
@@ -1691,18 +1817,21 @@ function createProgrammaticResolver(resolvers, options) {
1691
1817
  }
1692
1818
 
1693
1819
  // src/projector.ts
1820
+ init_field_keys();
1694
1821
  function isPlainRow(v) {
1695
1822
  return typeof v === "object" && v !== null && !Array.isArray(v) && !(v instanceof Date);
1696
1823
  }
1697
1824
  var MEDIA_FIELD_TYPES = /* @__PURE__ */ new Set(["image", "video", "file"]);
1698
- function nestedTargets(fields) {
1825
+ function nestedTargets(fields, map) {
1699
1826
  const out = [];
1700
1827
  for (const f of fields) {
1701
1828
  if (MEDIA_FIELD_TYPES.has(f.field_type)) continue;
1702
1829
  if (f.field_type !== "paragraph" && f.field_type !== "reference") continue;
1703
1830
  const ref = f.ui_component.ref;
1704
1831
  if (!ref) continue;
1705
- 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 });
1706
1835
  }
1707
1836
  return out;
1708
1837
  }
@@ -1720,7 +1849,7 @@ function buildProjectors(registry, fieldKeyMaps, fallbacks) {
1720
1849
  const typeFallbacks = fallbacks?.[typeName];
1721
1850
  projectors[typeName] = {
1722
1851
  map,
1723
- nested: nestedTargets(type.fields),
1852
+ nested: nestedTargets(type.fields, map),
1724
1853
  // Spread rather than assign: with exactOptionalPropertyTypes, an optional
1725
1854
  // property may be ABSENT but not present-and-undefined.
1726
1855
  ...typeFallbacks !== void 0 && { fallbacks: typeFallbacks }
@@ -1807,7 +1936,28 @@ function parseFilters(url, validFields, columnFor) {
1807
1936
  function isPublished(item) {
1808
1937
  return item["published"] === true;
1809
1938
  }
1810
- 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
+ }
1811
1961
  function registerListRoute(path, handler) {
1812
1962
  if (listRateLimit) {
1813
1963
  app.get(path, listRateLimit, handler);
@@ -1831,18 +1981,29 @@ function registerPublicContentRoutes(app, registry, repos, projectors, paths, li
1831
1981
  return c.json({ ok: true, data });
1832
1982
  });
1833
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;
1834
1994
  const basePath = contentType.default_base_path;
1835
1995
  const repo = repos[typeName];
1836
1996
  if (!repo) continue;
1837
1997
  const fieldKeys = projectors[typeName].map;
1838
- 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();
1839
2000
  for (const f of contentType.fields) {
1840
2001
  if (!RELATION_FIELD_TYPES.has(f.field_type)) continue;
1841
2002
  if (isColumnBacked(f)) {
1842
2003
  const label = fieldKeys.labelFor(f.db_column.column_name);
1843
- if (label !== void 0) relationFieldNames.add(label);
2004
+ if (label !== void 0) includeNames.set(label, f.name);
1844
2005
  } else {
1845
- relationFieldNames.add(f.name);
2006
+ includeNames.set(f.name, f.name);
1846
2007
  }
1847
2008
  }
1848
2009
  const filterableFieldNames = /* @__PURE__ */ new Set([
@@ -1861,8 +2022,11 @@ function registerPublicContentRoutes(app, registry, repos, projectors, paths, li
1861
2022
  404
1862
2023
  );
1863
2024
  }
1864
- let data = projectRow(result.data[0], typeName, projectors);
1865
- if (resolver?.hasSchema(typeName)) data = await resolver.resolveItem(typeName, data);
2025
+ const data = await respondItem(
2026
+ result.data[0],
2027
+ typeName,
2028
+ programmatic
2029
+ );
1866
2030
  return c.json({ ok: true, data });
1867
2031
  });
1868
2032
  } else {
@@ -1919,21 +2083,20 @@ function registerPublicContentRoutes(app, registry, repos, projectors, paths, li
1919
2083
  400
1920
2084
  );
1921
2085
  }
1922
- const include = parseInclude(c.req.query("include"));
1923
- for (const field of include) {
1924
- if (!relationFieldNames.has(field)) {
1925
- return c.json(
1926
- {
1927
- ok: false,
1928
- error: {
1929
- code: "INVALID_INCLUDE_FIELD",
1930
- message: `'${field}' is not a valid relation field`
1931
- }
1932
- },
1933
- 400
1934
- );
1935
- }
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
+ );
1936
2098
  }
2099
+ const include = translated.include;
1937
2100
  const sortColumn = fieldKeys.labels.includes(sortBy) ? fieldKeys.columnFor(sortBy) : sortBy;
1938
2101
  const result = await repo.findMany({
1939
2102
  published_only: true,
@@ -1944,29 +2107,25 @@ function registerPublicContentRoutes(app, registry, repos, projectors, paths, li
1944
2107
  filters: filtersResult.filters,
1945
2108
  include
1946
2109
  });
1947
- const labeled = result.data.map(
1948
- (row) => projectRow(row, typeName, projectors)
1949
- );
1950
- const data = resolver?.hasSchema(typeName) ? await resolver.resolveList(typeName, labeled) : labeled;
2110
+ const data = await respondList(result.data, typeName, programmatic);
1951
2111
  return c.json({ ...result, data });
1952
2112
  });
1953
2113
  app.get(paths.item(basePath), async (c) => {
1954
2114
  const slug = c.req.param("slug");
1955
- const include = parseInclude(c.req.query("include"));
1956
- for (const field of include) {
1957
- if (!relationFieldNames.has(field)) {
1958
- return c.json(
1959
- {
1960
- ok: false,
1961
- error: {
1962
- code: "INVALID_INCLUDE_FIELD",
1963
- message: `'${field}' is not a valid relation field`
1964
- }
1965
- },
1966
- 400
1967
- );
1968
- }
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
+ );
1969
2127
  }
2128
+ const include = translated.include;
1970
2129
  const item = await repo.findBySlug(slug, include);
1971
2130
  if (!item || !isPublished(item)) {
1972
2131
  return c.json(
@@ -1977,15 +2136,15 @@ function registerPublicContentRoutes(app, registry, repos, projectors, paths, li
1977
2136
  404
1978
2137
  );
1979
2138
  }
1980
- let data = projectRow(item, typeName, projectors);
1981
- if (resolver?.hasSchema(typeName)) data = await resolver.resolveItem(typeName, data);
2139
+ const data = await respondItem(item, typeName, programmatic);
1982
2140
  return c.json({ ok: true, data });
1983
2141
  });
1984
2142
  }
1985
2143
  }
1986
- for (const [typeName] of Object.entries(registry.taxonomy_types)) {
2144
+ for (const [typeName, taxonomyType] of Object.entries(registry.taxonomy_types)) {
1987
2145
  const repo = repos[typeName];
1988
2146
  if (!repo) continue;
2147
+ const programmatic = taxonomyType.fields.filter((f) => f.field_type === "programmatic").map((f) => f.name);
1989
2148
  registerListRoute(paths.taxonomyCollection(typeName), async (c) => {
1990
2149
  const pagination = parsePagination(c.req.query("page"), c.req.query("per_page"));
1991
2150
  if (!pagination.ok) {
@@ -2005,10 +2164,7 @@ function registerPublicContentRoutes(app, registry, repos, projectors, paths, li
2005
2164
  page: pagination.page,
2006
2165
  per_page: pagination.per_page
2007
2166
  });
2008
- const labeled = result.data.map(
2009
- (row) => projectRow(row, typeName, projectors)
2010
- );
2011
- const data = resolver?.hasSchema(typeName) ? await resolver.resolveList(typeName, labeled) : labeled;
2167
+ const data = await respondList(result.data, typeName, programmatic);
2012
2168
  return c.json({ ...result, data });
2013
2169
  });
2014
2170
  app.get(paths.taxonomyItem(typeName), async (c) => {
@@ -2023,8 +2179,7 @@ function registerPublicContentRoutes(app, registry, repos, projectors, paths, li
2023
2179
  404
2024
2180
  );
2025
2181
  }
2026
- let data = projectRow(item, typeName, projectors);
2027
- if (resolver?.hasSchema(typeName)) data = await resolver.resolveItem(typeName, data);
2182
+ const data = await respondItem(item, typeName, programmatic);
2028
2183
  return c.json({ ok: true, data });
2029
2184
  });
2030
2185
  }
@@ -3816,7 +3971,7 @@ function registerUserRoutes(app, db, requirePermission, requireHierarchy) {
3816
3971
  var import_drizzle_orm6 = require("drizzle-orm");
3817
3972
  var API_VERSION = (() => {
3818
3973
  try {
3819
- return "0.5.0";
3974
+ return "0.7.0";
3820
3975
  } catch {
3821
3976
  return "0.0.0";
3822
3977
  }
@@ -4414,20 +4569,6 @@ function createCmsApp(options) {
4414
4569
  sortableColumns
4415
4570
  });
4416
4571
  };
4417
- const graphqlRepos = Object.fromEntries([
4418
- ...Object.entries(registry.content_types).map(([typeName, ct]) => [
4419
- typeName,
4420
- createDrizzleContentRepository(db, ct.db.table_name, {
4421
- sortableColumns: sortableColumnsFor(typeName)
4422
- })
4423
- ]),
4424
- ...Object.entries(registry.taxonomy_types).map(([typeName, tt]) => [
4425
- typeName,
4426
- createDrizzleContentRepository(db, tt.db.table_name, {
4427
- sortableColumns: sortableColumnsFor(typeName)
4428
- })
4429
- ])
4430
- ]);
4431
4572
  registerAuthRoutes(app, db);
4432
4573
  app.use("/admin/api/*", authMiddleware);
4433
4574
  app.use("/admin/api/*", mustChangePasswordCheck);
@@ -4494,7 +4635,7 @@ function createCmsApp(options) {
4494
4635
  app.use(path, attachDeprecationHeaders);
4495
4636
  }
4496
4637
  }
4497
- registerPublicContentRoutes(app, registry, surface.repos, surface.projectors, surface.paths, listRateLimit, programmaticResolver);
4638
+ registerPublicContentRoutes(app, registry, surface.repos, surface.projectors, surface.paths, listRateLimit, programmaticResolver, projectors);
4498
4639
  }
4499
4640
  if (options.versions !== void 0) {
4500
4641
  app.all(`${prefix}/:version/*`, async (c, next) => {
@@ -4525,31 +4666,108 @@ function createCmsApp(options) {
4525
4666
  registerPublicMediaRoutes(app, mediaRepo, publicPaths, listRateLimit);
4526
4667
  if (options.graphql?.enabled) {
4527
4668
  const gqlOptions = options.graphql;
4528
- let gqlHandler = null;
4529
- let initError = null;
4669
+ const makeGraphqlRepo = (_typeName, tableName, sortableColumns) => createDrizzleContentRepository(db, tableName, { sortableColumns });
4670
+ const gqlPasses = [
4671
+ ...options.versions !== void 0 ? model.live.map((v) => ({ version: v, projectionVersion: v })) : [],
4672
+ { version: null, projectionVersion: model.current }
4673
+ ];
4674
+ const mounts = gqlPasses.map((pass) => ({
4675
+ path: pass.version === null ? "/graphql" : `/graphql/${pass.version}`,
4676
+ version: pass.version,
4677
+ handler: null,
4678
+ error: null
4679
+ }));
4530
4680
  const ready = Promise.resolve().then(() => (init_handler(), handler_exports)).then(({ createGraphQLHandler: createGraphQLHandler2 }) => {
4531
- gqlHandler = createGraphQLHandler2(registry, graphqlRepos, fieldKeyMaps, programmaticResolver, db, gqlOptions);
4681
+ gqlPasses.forEach((pass, i) => {
4682
+ const mount = mounts[i];
4683
+ try {
4684
+ const surface = buildVersionSurface({
4685
+ ...pass,
4686
+ prefix,
4687
+ registry,
4688
+ model,
4689
+ makeRepo: makeGraphqlRepo
4690
+ });
4691
+ const projection = model.projections[pass.projectionVersion];
4692
+ const currentProjection = model.projections[model.current];
4693
+ mount.handler = createGraphQLHandler2(
4694
+ registry,
4695
+ surface.repos,
4696
+ surface.fieldKeyMaps,
4697
+ programmaticResolver,
4698
+ db,
4699
+ gqlOptions,
4700
+ {
4701
+ endpoint: mount.path,
4702
+ currentVersion: model.current,
4703
+ ...projection !== void 0 && { projection },
4704
+ ...currentProjection !== void 0 && { currentProjection },
4705
+ currentFieldKeyMaps: fieldKeyMaps
4706
+ }
4707
+ );
4708
+ } catch (err) {
4709
+ mount.error = err;
4710
+ const message = err instanceof Error ? err.message : String(err);
4711
+ process.stderr.write(
4712
+ `\u2717 GraphQL schema failed to initialize for ${mount.path}; it will return 500. ${message}
4713
+ `
4714
+ );
4715
+ }
4716
+ });
4532
4717
  }).catch((err) => {
4533
- initError = err;
4718
+ for (const m of mounts) m.error = err;
4534
4719
  const message = err instanceof Error ? err.message : String(err);
4535
- process.stderr.write(`\u2717 GraphQL schema failed to initialize; /graphql will return 500. ${message}
4720
+ const paths = mounts.map((m) => m.path).join(", ");
4721
+ process.stderr.write(`\u2717 GraphQL module failed to load; ${paths} will return 500. ${message}
4536
4722
  `);
4537
4723
  });
4538
- const invokeGraphQL = async (c) => {
4539
- if (!gqlHandler && !initError) await ready;
4540
- if (!gqlHandler) {
4724
+ const invokeFor = (mount) => async (c) => {
4725
+ if (!mount.handler && !mount.error) await ready;
4726
+ if (!mount.handler) {
4541
4727
  return c.json(
4542
4728
  { ok: false, error: { code: "GRAPHQL_INIT_FAILED", message: "GraphQL schema failed to initialize" } },
4543
4729
  500
4544
4730
  );
4545
4731
  }
4546
- return gqlHandler(c, async () => {
4732
+ return mount.handler(c, async () => {
4547
4733
  });
4548
4734
  };
4549
- if (listRateLimit) {
4550
- app.all("/graphql", listRateLimit, invokeGraphQL);
4551
- } else {
4552
- app.all("/graphql", invokeGraphQL);
4735
+ for (const mount of mounts) {
4736
+ const headers = deprecationHeaders({
4737
+ requested: mount.version,
4738
+ model,
4739
+ successor: `/graphql/${model.current}`
4740
+ });
4741
+ if (headers === null) continue;
4742
+ const attach = async (c, next) => {
4743
+ await next();
4744
+ for (const [key, value] of Object.entries(headers)) c.res.headers.set(key, value);
4745
+ };
4746
+ app.use(mount.path, attach);
4747
+ }
4748
+ for (const mount of mounts) {
4749
+ if (listRateLimit) app.all(mount.path, listRateLimit, invokeFor(mount));
4750
+ else app.all(mount.path, invokeFor(mount));
4751
+ }
4752
+ if (options.versions !== void 0) {
4753
+ app.all("/graphql/:version", async (c, next) => {
4754
+ const segment = c.req.param("version");
4755
+ const kind = classifyVersion(segment, model);
4756
+ if (kind === "not-a-version" || kind === "live") return next();
4757
+ const live = model.live.join(", ");
4758
+ return c.json({
4759
+ errors: [
4760
+ {
4761
+ message: kind === "retired" ? `Version ${segment} is no longer served. Live versions: ${live}.` : `Version ${segment} does not exist. Live versions: ${live}.`,
4762
+ extensions: {
4763
+ code: kind === "retired" ? "VERSION_RETIRED" : "VERSION_UNKNOWN",
4764
+ current: model.current,
4765
+ live: model.live
4766
+ }
4767
+ }
4768
+ ]
4769
+ });
4770
+ });
4553
4771
  }
4554
4772
  }
4555
4773
  registerConfigRoute(