@bobbykim/manguito-cms-api 0.4.3 → 0.6.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
@@ -30,6 +30,105 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
30
30
  ));
31
31
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
32
32
 
33
+ // src/field-keys.ts
34
+ function isColumnBacked(field) {
35
+ const col = field.db_column;
36
+ if (col === null) return false;
37
+ if (col.column_name === "") return false;
38
+ if (col.junction) return false;
39
+ return true;
40
+ }
41
+ function buildFieldKeyMap(pairs, allLabels, droppedKeys) {
42
+ const labelToColumn = /* @__PURE__ */ new Map();
43
+ const columnToLabel = /* @__PURE__ */ new Map();
44
+ for (const { label, column } of pairs) {
45
+ labelToColumn.set(label, column);
46
+ columnToLabel.set(column, label);
47
+ }
48
+ for (const { label, ownColumn } of allLabels) {
49
+ if (ownColumn === label) continue;
50
+ const columnOwner = columnToLabel.get(label);
51
+ if (columnOwner !== void 0 && columnOwner !== label) {
52
+ throw new Error(
53
+ `Fatal: field key map failed to build \u2014 field label "${label}" 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.`
54
+ );
55
+ }
56
+ }
57
+ for (const label of labelToColumn.keys()) {
58
+ if (droppedKeys.has(label)) labelToColumn.delete(label);
59
+ }
60
+ for (const column of columnToLabel.keys()) {
61
+ if (droppedKeys.has(column)) columnToLabel.delete(column);
62
+ }
63
+ const diverges = [...labelToColumn].some(([label, column]) => label !== column);
64
+ function remap(input, lookup) {
65
+ const out = {};
66
+ for (const key of Object.keys(input)) {
67
+ if (droppedKeys.has(key)) continue;
68
+ out[lookup.get(key) ?? key] = input[key];
69
+ }
70
+ return out;
71
+ }
72
+ return {
73
+ toStorage: (input) => remap(input, labelToColumn),
74
+ toLabels: (row) => remap(row, columnToLabel),
75
+ columnFor: (label) => labelToColumn.get(label),
76
+ labelFor: (column) => columnToLabel.get(column),
77
+ labels: [...labelToColumn.keys()],
78
+ diverges
79
+ };
80
+ }
81
+ function createFieldKeyMap(fields) {
82
+ const droppedKeys = /* @__PURE__ */ new Set();
83
+ const pairs = [];
84
+ for (const f of fields) {
85
+ if (!isColumnBacked(f)) continue;
86
+ pairs.push({ label: f.name, column: f.db_column.column_name });
87
+ if (f.removed === true) {
88
+ droppedKeys.add(f.name);
89
+ droppedKeys.add(f.db_column.column_name);
90
+ }
91
+ }
92
+ return buildFieldKeyMap(
93
+ pairs,
94
+ fields.map((f) => ({
95
+ label: f.name,
96
+ // Conditional spread, not a plain assignment — exactOptionalPropertyTypes
97
+ // forbids present-and-undefined.
98
+ ...isColumnBacked(f) && { ownColumn: f.db_column.column_name }
99
+ })),
100
+ droppedKeys
101
+ );
102
+ }
103
+ function createFieldKeyMapFromProjection(projectionType, allFields) {
104
+ 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();
107
+ for (const f of allFields) {
108
+ if (!isColumnBacked(f)) continue;
109
+ dropped.add(f.name);
110
+ dropped.add(f.db_column.column_name);
111
+ }
112
+ for (const p of pairs) dropped.delete(p.column);
113
+ for (const label of projected) dropped.delete(label);
114
+ return buildFieldKeyMap(
115
+ pairs,
116
+ allFields.map((f) => ({
117
+ 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).
121
+ ...isColumnBacked(f) && { ownColumn: f.db_column.column_name }
122
+ })),
123
+ dropped
124
+ );
125
+ }
126
+ var init_field_keys = __esm({
127
+ "src/field-keys.ts"() {
128
+ "use strict";
129
+ }
130
+ });
131
+
33
132
  // src/relations.ts
34
133
  function quoteIdent(name) {
35
134
  if (!/^[a-z][a-z0-9_-]*$/.test(name)) throw new Error(`Unsafe identifier: ${name}`);
@@ -333,65 +432,6 @@ var init_relations = __esm({
333
432
  }
334
433
  });
335
434
 
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
- const droppedKeys = /* @__PURE__ */ new Set();
348
- for (const f of fields) {
349
- if (!isColumnBacked(f)) continue;
350
- columnToLabel.set(f.db_column.column_name, f.name);
351
- if (f.removed === true) {
352
- droppedKeys.add(f.name);
353
- droppedKeys.add(f.db_column.column_name);
354
- continue;
355
- }
356
- labelToColumn.set(f.name, f.db_column.column_name);
357
- }
358
- for (const f of fields) {
359
- const columnOwner = columnToLabel.get(f.name);
360
- if (columnOwner !== void 0 && columnOwner !== f.name) {
361
- throw new Error(
362
- `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.`
363
- );
364
- }
365
- }
366
- for (const f of fields) {
367
- if (isColumnBacked(f) && f.removed === true) {
368
- columnToLabel.delete(f.db_column.column_name);
369
- }
370
- }
371
- const diverges = [...labelToColumn].some(([label, column]) => label !== column);
372
- function remap(input, lookup) {
373
- const out = {};
374
- for (const key of Object.keys(input)) {
375
- if (droppedKeys.has(key)) continue;
376
- out[lookup.get(key) ?? key] = input[key];
377
- }
378
- return out;
379
- }
380
- return {
381
- toStorage: (input) => remap(input, labelToColumn),
382
- toLabels: (row) => remap(row, columnToLabel),
383
- columnFor: (label) => labelToColumn.get(label),
384
- labelFor: (column) => columnToLabel.get(column),
385
- labels: [...labelToColumn.keys()],
386
- diverges
387
- };
388
- }
389
- var init_field_keys = __esm({
390
- "src/field-keys.ts"() {
391
- "use strict";
392
- }
393
- });
394
-
395
435
  // src/graphql/scalars.ts
396
436
  var import_graphql, DateTimeScalar, JSONScalar;
397
437
  var init_scalars = __esm({
@@ -509,10 +549,13 @@ var init_naming = __esm({
509
549
  });
510
550
 
511
551
  // src/graphql/filters.ts
512
- function buildSortFieldEnum(typeName) {
552
+ function buildSortFieldEnum(typeName, fieldKeys) {
553
+ const resolvable = SORTABLE.filter(
554
+ (s) => s.gql === "createdAt" || s.gql === "updatedAt" || fieldKeys === void 0 || fieldKeys.columnFor(s.value) !== void 0
555
+ );
513
556
  return new import_graphql3.GraphQLEnumType({
514
557
  name: `${typeName}SortField`,
515
- values: Object.fromEntries(SORTABLE.map((s) => [s.gql, { value: s.value }]))
558
+ values: Object.fromEntries(resolvable.map((s) => [s.gql, { value: s.value }]))
516
559
  });
517
560
  }
518
561
  function comparableFilterInput(name, scalar) {
@@ -547,18 +590,19 @@ function filterInputForField(fieldType) {
547
590
  if (scalar === null) return null;
548
591
  return StringFilter;
549
592
  }
550
- function buildFilterInputType(type) {
593
+ function buildFilterInputType(type, viewFields) {
551
594
  const fields = {};
552
595
  fields["id"] = { type: IDFilter };
553
596
  fields["published"] = { type: BooleanFilter };
554
597
  fields["createdAt"] = { type: DateTimeFilter };
555
598
  fields["updatedAt"] = { type: DateTimeFilter };
556
599
  if (type.schema_type === "content-type") fields["slug"] = { type: StringFilter };
557
- for (const f of type.fields) {
600
+ 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 }));
601
+ for (const { field: f, exposedAs } of entries) {
558
602
  if (f.field_type === "programmatic" || f.field_type === "paragraph") continue;
559
603
  if (f.field_type === "image" || f.field_type === "video" || f.field_type === "file") continue;
560
604
  const input = filterInputForField(f.field_type);
561
- if (input) fields[buildFieldNameMap([f.name]).toGraphql(f.name)] = { type: input };
605
+ if (input) fields[buildFieldNameMap([exposedAs]).toGraphql(exposedAs)] = { type: input };
562
606
  }
563
607
  if (Object.keys(fields).length === 0) return null;
564
608
  return new import_graphql3.GraphQLInputObjectType({ name: `${graphqlTypeName(type.name)}Filter`, fields });
@@ -621,32 +665,41 @@ function codeError2(code, message) {
621
665
  function isPublished2(item) {
622
666
  return !!item && item["published"] === true;
623
667
  }
624
- function resolveFieldValue(field, row) {
668
+ function resolveFieldValue(field, row, fallback) {
625
669
  const key = isColumnBacked(field) ? field.db_column.column_name : field.name;
626
- return row[key] ?? null;
670
+ const value = row[key];
671
+ if ((value === null || value === void 0) && fallback !== void 0) return fallback;
672
+ return value ?? null;
627
673
  }
628
- function scalarFieldResolver(field) {
629
- return (parent) => resolveFieldValue(field, parent);
674
+ function scalarFieldResolver(field, fallback) {
675
+ return (parent) => resolveFieldValue(field, parent, fallback);
630
676
  }
631
677
  function relationFieldResolver(typeName, schemaFieldName) {
632
678
  return (parent, _args, ctx) => ctx.loaders.load(typeName, schemaFieldName, parent);
633
679
  }
634
- function programmaticFieldResolver(typeName, schemaFieldName, mediaFieldNames = [], fieldKeys) {
680
+ function programmaticFieldResolver(typeName, schemaFieldName, mediaFields = [], fieldKeys) {
635
681
  return async (parent, _args, ctx) => {
636
682
  let p = ctx.programmaticMemo.get(parent);
637
683
  if (!p) {
638
- p = resolveProgrammaticRow(typeName, parent, ctx, mediaFieldNames, fieldKeys);
684
+ p = resolveProgrammaticRow(typeName, parent, ctx, mediaFields, fieldKeys);
639
685
  ctx.programmaticMemo.set(parent, p);
640
686
  }
641
687
  return (await p)[schemaFieldName];
642
688
  };
643
689
  }
644
- async function resolveProgrammaticRow(typeName, parent, ctx, mediaFieldNames, fieldKeys) {
690
+ async function resolveProgrammaticRow(typeName, parent, ctx, mediaFields, fieldKeys) {
645
691
  const toLabels = (row) => fieldKeys ? fieldKeys.toLabels(row) : row;
646
- if (mediaFieldNames.length === 0) return ctx.resolver.resolveItem(typeName, toLabels(parent));
692
+ if (mediaFields.length === 0) return ctx.resolver.resolveItem(typeName, toLabels(parent));
647
693
  const enriched = { ...parent };
648
- await Promise.all(mediaFieldNames.map((name) => ctx.loaders.load(typeName, name, enriched)));
649
- return ctx.resolver.resolveItem(typeName, toLabels(enriched));
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;
650
703
  }
651
704
  function collectionResolver(typeName, nameMap, fieldKeys) {
652
705
  return async (_root, args, ctx) => {
@@ -704,7 +757,7 @@ var init_resolvers = __esm({
704
757
  });
705
758
 
706
759
  // src/graphql/schema.ts
707
- function buildGraphQLSchema(registry, fieldKeyMaps = {}) {
760
+ function buildGraphQLSchema(registry, fieldKeyMaps = {}, view) {
708
761
  const objectTypes = /* @__PURE__ */ new Map();
709
762
  const enumTypes = /* @__PURE__ */ new Map();
710
763
  for (const [name, enumType] of Object.entries(registry.enum_types)) {
@@ -723,14 +776,14 @@ function buildGraphQLSchema(registry, fieldKeyMaps = {}) {
723
776
  );
724
777
  }
725
778
  }
726
- function outputTypeForField(field) {
779
+ function outputTypeForField(field, required) {
727
780
  const scalar = scalarOutputType(field.field_type);
728
- if (scalar) return field.required ? new import_graphql5.GraphQLNonNull(scalar) : scalar;
781
+ if (scalar) return required ? new import_graphql5.GraphQLNonNull(scalar) : scalar;
729
782
  if (field.field_type === "enum") {
730
783
  const ref = field.ui_component.component === "select" ? field.ui_component.enum_ref : void 0;
731
784
  const et = ref ? enumTypes.get(ref) : void 0;
732
785
  const t = et ?? import_graphql5.GraphQLString;
733
- return field.required ? new import_graphql5.GraphQLNonNull(t) : t;
786
+ return required ? new import_graphql5.GraphQLNonNull(t) : t;
734
787
  }
735
788
  if (field.field_type === "image" || field.field_type === "video" || field.field_type === "file") {
736
789
  return MEDIA;
@@ -760,8 +813,17 @@ function buildGraphQLSchema(registry, fieldKeyMaps = {}) {
760
813
  }
761
814
  return import_graphql5.GraphQLString;
762
815
  }
816
+ function exposedFields(type) {
817
+ return type.fields.filter((f) => f.removed !== true);
818
+ }
819
+ function viewFieldsFor(machineName, type) {
820
+ return view?.[machineName]?.fields ?? exposedFields(type).map((f) => ({ field: f, exposedAs: f.name, required: f.required }));
821
+ }
763
822
  function buildObjectType(machineName, type) {
764
- const mediaFieldNames = type.fields.filter((f) => f.field_type === "image" || f.field_type === "video" || f.field_type === "file").map((f) => f.name);
823
+ 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 }));
765
827
  return new import_graphql5.GraphQLObjectType({
766
828
  name: graphqlTypeName(machineName),
767
829
  fields: () => {
@@ -775,23 +837,25 @@ function buildGraphQLSchema(registry, fieldKeyMaps = {}) {
775
837
  }
776
838
  fields["createdAt"] = { type: new import_graphql5.GraphQLNonNull(DateTimeScalar), resolve: (p) => p["created_at"] };
777
839
  fields["updatedAt"] = { type: new import_graphql5.GraphQLNonNull(DateTimeScalar), resolve: (p) => p["updated_at"] };
778
- for (const field of type.fields) {
779
- const gqlName = toCamelCase(field.name);
780
- const outType = outputTypeForField(field);
840
+ for (const vf of visible) {
841
+ const field = vf.field;
842
+ const gqlName = toCamelCase(vf.exposedAs);
843
+ const outType = outputTypeForField(field, vf.required);
781
844
  let resolve;
782
845
  if (field.field_type === "programmatic") {
783
- resolve = programmaticFieldResolver(
784
- machineName,
785
- field.name,
786
- mediaFieldNames,
787
- fieldKeyMaps[machineName]
788
- );
846
+ resolve = programmaticFieldResolver(machineName, field.name, mediaFields, fieldKeyMaps[machineName]);
789
847
  } else if (field.field_type === "reference" || field.field_type === "paragraph" || field.field_type === "image" || field.field_type === "video" || field.field_type === "file") {
790
848
  resolve = relationFieldResolver(machineName, field.name);
791
849
  } else {
792
- resolve = scalarFieldResolver(field);
850
+ resolve = scalarFieldResolver(field, vf.fallback);
793
851
  }
794
- fields[gqlName] = { type: outType, resolve };
852
+ fields[gqlName] = {
853
+ type: outType,
854
+ resolve,
855
+ // Conditional spread, not `deprecationReason: vf.deprecationReason`
856
+ // — exactOptionalPropertyTypes forbids present-and-undefined.
857
+ ...vf.deprecationReason !== void 0 && { deprecationReason: vf.deprecationReason }
858
+ };
795
859
  }
796
860
  return fields;
797
861
  }
@@ -823,7 +887,8 @@ function buildGraphQLSchema(registry, fieldKeyMaps = {}) {
823
887
  const queryFields = {};
824
888
  for (const [name, ct] of Object.entries(registry.content_types)) {
825
889
  const objType = objectTypes.get(name);
826
- const nameMap = buildFieldNameMap(ct.fields.map((f) => f.name));
890
+ const visible = viewFieldsFor(name, ct);
891
+ const nameMap = buildFieldNameMap(visible.map((v) => v.exposedAs));
827
892
  if (ct.only_one) {
828
893
  queryFields[singleQueryName(name)] = { type: objType, resolve: singletonResolver(name) };
829
894
  continue;
@@ -835,13 +900,13 @@ function buildGraphQLSchema(registry, fieldKeyMaps = {}) {
835
900
  meta: { type: new import_graphql5.GraphQLNonNull(PAGE_META), resolve: (r) => r.meta }
836
901
  }
837
902
  });
838
- const filterType = buildFilterInputType(ct);
903
+ const filterType = buildFilterInputType(ct, visible);
839
904
  queryFields[collectionQueryName(name)] = {
840
905
  type: new import_graphql5.GraphQLNonNull(listType),
841
906
  args: {
842
907
  page: { type: import_graphql5.GraphQLInt },
843
908
  perPage: { type: import_graphql5.GraphQLInt },
844
- sortBy: { type: buildSortFieldEnum(graphqlTypeName(name)) },
909
+ sortBy: { type: buildSortFieldEnum(graphqlTypeName(name), fieldKeyMaps[name]) },
845
910
  sortOrder: { type: SortOrderEnum },
846
911
  ...filterType ? { filter: { type: filterType } } : {}
847
912
  },
@@ -865,7 +930,11 @@ function buildGraphQLSchema(registry, fieldKeyMaps = {}) {
865
930
  queryFields[collectionQueryName(name)] = {
866
931
  type: new import_graphql5.GraphQLNonNull(listType),
867
932
  args: { page: { type: import_graphql5.GraphQLInt }, perPage: { type: import_graphql5.GraphQLInt } },
868
- resolve: collectionResolver(name, buildFieldNameMap(tt.fields.map((f) => f.name)), fieldKeyMaps[name])
933
+ resolve: collectionResolver(
934
+ name,
935
+ buildFieldNameMap(viewFieldsFor(name, tt).map((v) => v.exposedAs)),
936
+ fieldKeyMaps[name]
937
+ )
869
938
  };
870
939
  queryFields[singleQueryName(name)] = {
871
940
  type: objType,
@@ -915,6 +984,76 @@ var init_schema = __esm({
915
984
  }
916
985
  });
917
986
 
987
+ // src/graphql/version-view.ts
988
+ function byColumn(fields) {
989
+ const out = /* @__PURE__ */ new Map();
990
+ for (const f of fields) {
991
+ if (!isColumnBacked(f)) continue;
992
+ out.set(f.db_column.column_name, f);
993
+ }
994
+ return out;
995
+ }
996
+ function deprecationFor(field, currentLabels, currentVersion) {
997
+ const currentLabel = currentLabels.get(field.column_name);
998
+ if (currentLabel === void 0) {
999
+ return `Removed in ${currentVersion}; column retained while this version is live.`;
1000
+ }
1001
+ if (currentLabel !== field.exposed_as) {
1002
+ return `Renamed to '${currentLabel}' in ${currentVersion}.`;
1003
+ }
1004
+ return void 0;
1005
+ }
1006
+ function buildVersionView(input) {
1007
+ const { registry, projection, currentProjection, currentVersion } = input;
1008
+ const out = {};
1009
+ const sources = [
1010
+ registry.content_types,
1011
+ registry.taxonomy_types,
1012
+ registry.paragraph_types
1013
+ ];
1014
+ for (const source of sources) {
1015
+ for (const [typeName, type] of Object.entries(source)) {
1016
+ const projectionType = projection?.types[typeName];
1017
+ if (projectionType === void 0) {
1018
+ out[typeName] = {
1019
+ fields: type.fields.filter((f) => f.removed !== true).map((f) => ({ field: f, exposedAs: f.name, required: f.required }))
1020
+ };
1021
+ continue;
1022
+ }
1023
+ const columns = byColumn(type.fields);
1024
+ const currentLabels = /* @__PURE__ */ new Map();
1025
+ for (const f of currentProjection?.types[typeName]?.fields ?? []) {
1026
+ currentLabels.set(f.column_name, f.exposed_as);
1027
+ }
1028
+ const fields = [];
1029
+ for (const pf of projectionType.fields) {
1030
+ const field = columns.get(pf.column_name);
1031
+ if (field === void 0) continue;
1032
+ const reason = deprecationFor(pf, currentLabels, currentVersion);
1033
+ fields.push({
1034
+ field,
1035
+ exposedAs: pf.exposed_as,
1036
+ required: pf.required,
1037
+ ...pf.fallback !== void 0 && { fallback: pf.fallback },
1038
+ ...reason !== void 0 && { deprecationReason: reason }
1039
+ });
1040
+ }
1041
+ for (const f of type.fields) {
1042
+ if (isColumnBacked(f) || f.removed === true) continue;
1043
+ fields.push({ field: f, exposedAs: f.name, required: f.required });
1044
+ }
1045
+ out[typeName] = { fields };
1046
+ }
1047
+ }
1048
+ return out;
1049
+ }
1050
+ var init_version_view = __esm({
1051
+ "src/graphql/version-view.ts"() {
1052
+ "use strict";
1053
+ init_field_keys();
1054
+ }
1055
+ });
1056
+
918
1057
  // src/graphql/dataloaders.ts
919
1058
  function createRelationLoaders(db, registry) {
920
1059
  const relMaps = /* @__PURE__ */ new Map();
@@ -1007,15 +1146,29 @@ function isClientSafeGraphQLError(error) {
1007
1146
  }
1008
1147
  return false;
1009
1148
  }
1010
- function createGraphQLHandler(registry, repos, fieldKeyMaps, resolver, db, options) {
1011
- 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);
1012
1165
  const { plugins } = buildArmorPlugin({
1013
1166
  maxDepth: options.maxDepth,
1014
1167
  maxComplexity: options.maxComplexity
1015
1168
  });
1016
1169
  const yoga = (0, import_graphql_yoga.createYoga)({
1017
1170
  schema,
1018
- graphqlEndpoint: "/graphql",
1171
+ graphqlEndpoint: versioning.endpoint ?? "/graphql",
1019
1172
  graphiql: options.graphiql,
1020
1173
  landingPage: false,
1021
1174
  maskedErrors: {
@@ -1043,6 +1196,7 @@ var init_handler = __esm({
1043
1196
  "use strict";
1044
1197
  import_graphql_yoga = require("graphql-yoga");
1045
1198
  init_schema();
1199
+ init_version_view();
1046
1200
  init_dataloaders();
1047
1201
  init_security();
1048
1202
  }
@@ -1069,15 +1223,20 @@ function normalizePrefix(prefix) {
1069
1223
  }
1070
1224
  function createPublicPaths(prefix) {
1071
1225
  return {
1072
- collection: (basePath) => `${prefix}/${basePath}`,
1073
- item: (basePath) => `${prefix}/${basePath}/:slug`,
1074
- taxonomyCollection: (typeName) => `${prefix}/taxonomy/${typeName}`,
1075
- taxonomyItem: (typeName) => `${prefix}/taxonomy/${typeName}/:id`,
1076
1226
  mediaCollection: () => `${prefix}/media`,
1077
1227
  mediaItem: () => `${prefix}/media/:id`,
1078
1228
  openapi: () => `${prefix}/openapi.json`
1079
1229
  };
1080
1230
  }
1231
+ function createVersionedPaths(prefix, version) {
1232
+ const root = version === null ? prefix : `${prefix}/${version}`;
1233
+ return {
1234
+ collection: (basePath) => `${root}/${basePath}`,
1235
+ item: (basePath) => `${root}/${basePath}/:slug`,
1236
+ taxonomyCollection: (typeName) => `${root}/taxonomy/${typeName}`,
1237
+ taxonomyItem: (typeName) => `${root}/taxonomy/${typeName}/:id`
1238
+ };
1239
+ }
1081
1240
 
1082
1241
  // src/app.ts
1083
1242
  var import_hono = require("hono");
@@ -1147,7 +1306,7 @@ function createSecurityHeadersMiddleware(options = {}) {
1147
1306
  c.res.headers.set("X-Content-Type-Options", "nosniff");
1148
1307
  c.res.headers.set("X-Frame-Options", "DENY");
1149
1308
  c.res.headers.set("Referrer-Policy", "no-referrer");
1150
- const isGraphiql = graphiqlPath !== void 0 && c.req.path === graphiqlPath;
1309
+ const isGraphiql = graphiqlPath !== void 0 && (c.req.path === graphiqlPath || c.req.path.startsWith(`${graphiqlPath}/`));
1151
1310
  c.res.headers.set("Content-Security-Policy", isGraphiql ? graphiqlCsp : csp);
1152
1311
  };
1153
1312
  }
@@ -1673,7 +1832,7 @@ function nestedTargets(fields) {
1673
1832
  }
1674
1833
  return out;
1675
1834
  }
1676
- function buildProjectors(registry, fieldKeyMaps) {
1835
+ function buildProjectors(registry, fieldKeyMaps, fallbacks) {
1677
1836
  const projectors = {};
1678
1837
  const sources = [
1679
1838
  registry.content_types,
@@ -1684,7 +1843,14 @@ function buildProjectors(registry, fieldKeyMaps) {
1684
1843
  for (const [typeName, type] of Object.entries(source)) {
1685
1844
  const map = fieldKeyMaps[typeName];
1686
1845
  if (!map) continue;
1687
- projectors[typeName] = { map, nested: nestedTargets(type.fields) };
1846
+ const typeFallbacks = fallbacks?.[typeName];
1847
+ projectors[typeName] = {
1848
+ map,
1849
+ nested: nestedTargets(type.fields),
1850
+ // Spread rather than assign: with exactOptionalPropertyTypes, an optional
1851
+ // property may be ABSENT but not present-and-undefined.
1852
+ ...typeFallbacks !== void 0 && { fallbacks: typeFallbacks }
1853
+ };
1688
1854
  }
1689
1855
  }
1690
1856
  return projectors;
@@ -1693,6 +1859,11 @@ function projectRow(row, typeName, projectors) {
1693
1859
  const p = projectors[typeName];
1694
1860
  if (!p) return row;
1695
1861
  const out = p.map.toLabels(row);
1862
+ if (p.fallbacks !== void 0) {
1863
+ for (const [label, value] of Object.entries(p.fallbacks)) {
1864
+ if (out[label] === null || out[label] === void 0) out[label] = value;
1865
+ }
1866
+ }
1696
1867
  for (const { label, target } of p.nested) {
1697
1868
  const v = out[label];
1698
1869
  if (v === null || v === void 0) continue;
@@ -1705,6 +1876,9 @@ function projectRow(row, typeName, projectors) {
1705
1876
  return out;
1706
1877
  }
1707
1878
 
1879
+ // src/routes/content.ts
1880
+ init_field_keys();
1881
+
1708
1882
  // src/routes/query-params.ts
1709
1883
  var SORTABLE_FIELDS = /* @__PURE__ */ new Set(["title", "created_at", "updated_at"]);
1710
1884
  var RELATION_FIELD_TYPES = /* @__PURE__ */ new Set([
@@ -1786,13 +1960,24 @@ function registerPublicContentRoutes(app, registry, repos, projectors, paths, li
1786
1960
  const basePath = contentType.default_base_path;
1787
1961
  const repo = repos[typeName];
1788
1962
  if (!repo) continue;
1789
- const relationFieldNames = new Set(
1790
- contentType.fields.filter((f) => RELATION_FIELD_TYPES.has(f.field_type)).map((f) => f.name)
1791
- );
1963
+ const fieldKeys = projectors[typeName].map;
1964
+ const relationFieldNames = /* @__PURE__ */ new Set();
1965
+ for (const f of contentType.fields) {
1966
+ if (!RELATION_FIELD_TYPES.has(f.field_type)) continue;
1967
+ if (isColumnBacked(f)) {
1968
+ const label = fieldKeys.labelFor(f.db_column.column_name);
1969
+ if (label !== void 0) relationFieldNames.add(label);
1970
+ } else {
1971
+ relationFieldNames.add(f.name);
1972
+ }
1973
+ }
1792
1974
  const filterableFieldNames = /* @__PURE__ */ new Set([
1793
- ...contentType.fields.filter((f) => f.field_type !== "programmatic").map((f) => f.name),
1975
+ ...fieldKeys.labels,
1794
1976
  ...contentType.system_fields.map((f) => f.name)
1795
1977
  ]);
1978
+ const sortableFieldNames = new Set(
1979
+ [...SORTABLE_FIELDS].filter((f) => filterableFieldNames.has(f))
1980
+ );
1796
1981
  if (contentType.only_one) {
1797
1982
  app.get(paths.collection(basePath), async (c) => {
1798
1983
  const result = await repo.findMany({ published_only: true, page: 1, per_page: 1 });
@@ -1808,8 +1993,6 @@ function registerPublicContentRoutes(app, registry, repos, projectors, paths, li
1808
1993
  });
1809
1994
  } else {
1810
1995
  registerListRoute(paths.collection(basePath), async (c) => {
1811
- const projector = projectors[typeName];
1812
- const fieldKeys = projector.map;
1813
1996
  const pagination = parsePagination(c.req.query("page"), c.req.query("per_page"));
1814
1997
  if (!pagination.ok) {
1815
1998
  return c.json(
@@ -1824,13 +2007,13 @@ function registerPublicContentRoutes(app, registry, repos, projectors, paths, li
1824
2007
  );
1825
2008
  }
1826
2009
  const sortBy = c.req.query("sort_by") ?? "created_at";
1827
- if (!SORTABLE_FIELDS.has(sortBy)) {
2010
+ if (!sortableFieldNames.has(sortBy)) {
1828
2011
  return c.json(
1829
2012
  {
1830
2013
  ok: false,
1831
2014
  error: {
1832
2015
  code: "INVALID_SORT_FIELD",
1833
- message: `'${sortBy}' is not sortable. Allowed: title, created_at, updated_at`
2016
+ message: `'${sortBy}' is not sortable. Allowed: ${[...sortableFieldNames].join(", ")}`
1834
2017
  }
1835
2018
  },
1836
2019
  400
@@ -1877,7 +2060,7 @@ function registerPublicContentRoutes(app, registry, repos, projectors, paths, li
1877
2060
  );
1878
2061
  }
1879
2062
  }
1880
- const sortColumn = fieldKeys.columnFor(sortBy) ?? sortBy;
2063
+ const sortColumn = fieldKeys.labels.includes(sortBy) ? fieldKeys.columnFor(sortBy) : sortBy;
1881
2064
  const result = await repo.findMany({
1882
2065
  published_only: true,
1883
2066
  page: pagination.page,
@@ -3759,7 +3942,7 @@ function registerUserRoutes(app, db, requirePermission, requireHierarchy) {
3759
3942
  var import_drizzle_orm6 = require("drizzle-orm");
3760
3943
  var API_VERSION = (() => {
3761
3944
  try {
3762
- return "0.4.3";
3945
+ return "0.6.0";
3763
3946
  } catch {
3764
3947
  return "0.0.0";
3765
3948
  }
@@ -4166,6 +4349,95 @@ function createMediaRepository(db) {
4166
4349
 
4167
4350
  // src/app.ts
4168
4351
  init_field_keys();
4352
+
4353
+ // src/versions.ts
4354
+ init_field_keys();
4355
+ var VERSION_SEGMENT = /^v\d+$/;
4356
+ function versionNumber(version) {
4357
+ return Number.parseInt(version.slice(1), 10);
4358
+ }
4359
+ function classifyVersion(segment, model) {
4360
+ if (!VERSION_SEGMENT.test(segment)) return "not-a-version";
4361
+ if (model.live.includes(segment)) return "live";
4362
+ return versionNumber(segment) < versionNumber(model.current) ? "retired" : "unknown";
4363
+ }
4364
+ function fallbacksFor(projection) {
4365
+ const out = {};
4366
+ for (const [typeName, type] of Object.entries(projection.types)) {
4367
+ const perLabel = {};
4368
+ for (const f of type.fields) {
4369
+ if (f.fallback !== void 0) perLabel[f.exposed_as] = f.fallback;
4370
+ }
4371
+ if (Object.keys(perLabel).length > 0) out[typeName] = perLabel;
4372
+ }
4373
+ return out;
4374
+ }
4375
+ function buildVersionSurface(input) {
4376
+ const { version, projectionVersion, prefix, registry, model, makeRepo } = input;
4377
+ const paths = createVersionedPaths(prefix, version);
4378
+ const projection = model.projections[projectionVersion];
4379
+ const fieldKeyMaps = {};
4380
+ const sources = [
4381
+ registry.content_types,
4382
+ registry.taxonomy_types,
4383
+ registry.paragraph_types
4384
+ ];
4385
+ for (const source of sources) {
4386
+ for (const [typeName, type] of Object.entries(source)) {
4387
+ const projectionType = projection?.types[typeName];
4388
+ fieldKeyMaps[typeName] = projectionType ? createFieldKeyMapFromProjection(projectionType, type.fields) : createFieldKeyMap(type.fields);
4389
+ }
4390
+ }
4391
+ const fallbacks = projection ? fallbacksFor(projection) : void 0;
4392
+ const projectors = buildProjectors(registry, fieldKeyMaps, fallbacks);
4393
+ const sortableColumnsFor = (typeName) => {
4394
+ const map = fieldKeyMaps[typeName];
4395
+ const type = registry.content_types[typeName] ?? registry.taxonomy_types[typeName];
4396
+ const systemFieldNames = new Set((type?.system_fields ?? []).map((f) => f.name));
4397
+ const out = /* @__PURE__ */ new Set();
4398
+ for (const label of SORTABLE_FIELDS) {
4399
+ const column = map.columnFor(label);
4400
+ if (column !== void 0) out.add(column);
4401
+ else if (systemFieldNames.has(label)) out.add(label);
4402
+ }
4403
+ return out;
4404
+ };
4405
+ const repos = {};
4406
+ for (const [typeName, ct] of Object.entries(registry.content_types)) {
4407
+ repos[typeName] = makeRepo(typeName, ct.db.table_name, sortableColumnsFor(typeName));
4408
+ }
4409
+ for (const [typeName, tt] of Object.entries(registry.taxonomy_types)) {
4410
+ repos[typeName] = makeRepo(typeName, tt.db.table_name, sortableColumnsFor(typeName));
4411
+ }
4412
+ return { paths, fieldKeyMaps, projectors, repos };
4413
+ }
4414
+ function deprecationHeaders(input) {
4415
+ const { requested, model, successor } = input;
4416
+ const link = `<${successor}>; rel="successor-version"`;
4417
+ if (requested === null) {
4418
+ if (model.live.length <= 1) return null;
4419
+ return {
4420
+ Deprecation: "true",
4421
+ Link: link,
4422
+ Warning: `299 - "Unversioned path resolves to the latest version (${model.current}) and will change when a new version is cut. Pin a version."`
4423
+ };
4424
+ }
4425
+ if (requested === model.current) return null;
4426
+ return { Deprecation: "true", Link: link };
4427
+ }
4428
+
4429
+ // src/app.ts
4430
+ function versionRoutePaths(registry, paths) {
4431
+ const out = [paths.collection("content"), paths.collection("taxonomy")];
4432
+ for (const ct of Object.values(registry.content_types)) {
4433
+ out.push(paths.collection(ct.default_base_path));
4434
+ if (!ct.only_one) out.push(paths.item(ct.default_base_path));
4435
+ }
4436
+ for (const tt of Object.values(registry.taxonomy_types)) {
4437
+ out.push(paths.taxonomyCollection(tt.name), paths.taxonomyItem(tt.name));
4438
+ }
4439
+ return out;
4440
+ }
4169
4441
  var MISSING_STORAGE_ERROR = `\u2717 api.storage is required but not configured.
4170
4442
  Add a storage adapter to your manguito.config.ts:
4171
4443
 
@@ -4181,6 +4453,7 @@ function createCmsApp(options) {
4181
4453
  }
4182
4454
  const prefix = normalizePrefix(options.prefix);
4183
4455
  const publicPaths = createPublicPaths(prefix);
4456
+ const versionedPaths = createVersionedPaths(prefix, null);
4184
4457
  const { storage, registry, db, rateLimit, media, cors } = options;
4185
4458
  const cmsName = options.name ?? "Manguito CMS";
4186
4459
  const maxFileSize = media?.max_file_size;
@@ -4229,9 +4502,18 @@ function createCmsApp(options) {
4229
4502
  ])
4230
4503
  ]);
4231
4504
  const projectors = buildProjectors(registry, fieldKeyMaps);
4232
- const sortableColumnsFor = (typeName) => new Set(
4233
- [...SORTABLE_FIELDS].map((label) => fieldKeyMaps[typeName].columnFor(label) ?? label)
4234
- );
4505
+ const sortableColumnsFor = (typeName) => {
4506
+ const map = fieldKeyMaps[typeName];
4507
+ const type = registry.content_types[typeName] ?? registry.taxonomy_types[typeName];
4508
+ const systemFieldNames = new Set((type?.system_fields ?? []).map((f) => f.name));
4509
+ const out = /* @__PURE__ */ new Set();
4510
+ for (const label of SORTABLE_FIELDS) {
4511
+ const column = map.columnFor(label);
4512
+ if (column !== void 0) out.add(column);
4513
+ else if (systemFieldNames.has(label)) out.add(label);
4514
+ }
4515
+ return out;
4516
+ };
4235
4517
  const contentRepos = Object.fromEntries(
4236
4518
  Object.entries(registry.content_types).map(([typeName, ct]) => [
4237
4519
  typeName,
@@ -4250,41 +4532,14 @@ function createCmsApp(options) {
4250
4532
  );
4251
4533
  const repos = { ...contentRepos, ...taxonomyRepos };
4252
4534
  const mediaRepo = createMediaRepository(db);
4253
- const publicContentRepos = Object.fromEntries(
4254
- Object.entries(registry.content_types).map(([typeName, ct]) => [
4255
- typeName,
4256
- createDrizzleContentRepository(db, ct.db.table_name, {
4257
- relations: buildRelationsMap(ct.fields, registry),
4258
- publishedRelations: true,
4259
- sortableColumns: sortableColumnsFor(typeName)
4260
- })
4261
- ])
4262
- );
4263
- const publicTaxonomyRepos = Object.fromEntries(
4264
- Object.entries(registry.taxonomy_types).map(([typeName, tt]) => [
4265
- typeName,
4266
- createDrizzleContentRepository(db, tt.db.table_name, {
4267
- relations: buildRelationsMap(tt.fields, registry),
4268
- publishedRelations: true,
4269
- sortableColumns: sortableColumnsFor(typeName)
4270
- })
4271
- ])
4272
- );
4273
- const publicRepos = { ...publicContentRepos, ...publicTaxonomyRepos };
4274
- const graphqlRepos = Object.fromEntries([
4275
- ...Object.entries(registry.content_types).map(([typeName, ct]) => [
4276
- typeName,
4277
- createDrizzleContentRepository(db, ct.db.table_name, {
4278
- sortableColumns: sortableColumnsFor(typeName)
4279
- })
4280
- ]),
4281
- ...Object.entries(registry.taxonomy_types).map(([typeName, tt]) => [
4282
- typeName,
4283
- createDrizzleContentRepository(db, tt.db.table_name, {
4284
- sortableColumns: sortableColumnsFor(typeName)
4285
- })
4286
- ])
4287
- ]);
4535
+ const makeRepo = (typeName, tableName, sortableColumns) => {
4536
+ const type = registry.content_types[typeName] ?? registry.taxonomy_types[typeName];
4537
+ return createDrizzleContentRepository(db, tableName, {
4538
+ relations: buildRelationsMap(type.fields, registry),
4539
+ publishedRelations: true,
4540
+ sortableColumns
4541
+ });
4542
+ };
4288
4543
  registerAuthRoutes(app, db);
4289
4544
  app.use("/admin/api/*", authMiddleware);
4290
4545
  app.use("/admin/api/*", mustChangePasswordCheck);
@@ -4293,17 +4548,17 @@ function createCmsApp(options) {
4293
4548
  for (const ct of Object.values(registry.content_types)) {
4294
4549
  const base = ct.default_base_path;
4295
4550
  if (ct.only_one) {
4296
- paths[publicPaths.collection(base)] = { get: { summary: `Get ${ct.label}`, tags: [ct.label] } };
4551
+ paths[versionedPaths.collection(base)] = { get: { summary: `Get ${ct.label}`, tags: [ct.label] } };
4297
4552
  } else {
4298
- paths[publicPaths.collection(base)] = { get: { summary: `List published ${ct.label}`, tags: [ct.label] } };
4299
- paths[publicPaths.item(base).replace(":slug", "{slug}")] = {
4553
+ paths[versionedPaths.collection(base)] = { get: { summary: `List published ${ct.label}`, tags: [ct.label] } };
4554
+ paths[versionedPaths.item(base).replace(":slug", "{slug}")] = {
4300
4555
  get: { summary: `Get ${ct.label} by slug`, tags: [ct.label] }
4301
4556
  };
4302
4557
  }
4303
4558
  }
4304
4559
  for (const tt of Object.values(registry.taxonomy_types)) {
4305
- paths[publicPaths.taxonomyCollection(tt.name)] = { get: { summary: `List published ${tt.label}`, tags: [tt.label] } };
4306
- paths[publicPaths.taxonomyItem(tt.name).replace(":id", "{id}")] = {
4560
+ paths[versionedPaths.taxonomyCollection(tt.name)] = { get: { summary: `List published ${tt.label}`, tags: [tt.label] } };
4561
+ paths[versionedPaths.taxonomyItem(tt.name).replace(":id", "{id}")] = {
4307
4562
  get: { summary: `Get ${tt.label} by id`, tags: [tt.label] }
4308
4563
  };
4309
4564
  }
@@ -4323,35 +4578,166 @@ function createCmsApp(options) {
4323
4578
  paths: {}
4324
4579
  })
4325
4580
  );
4326
- registerPublicContentRoutes(app, registry, publicRepos, projectors, publicPaths, listRateLimit, programmaticResolver);
4581
+ const model = options.versions ?? {
4582
+ current: "v1",
4583
+ live: ["v1"],
4584
+ projections: {}
4585
+ };
4586
+ const passes = [
4587
+ ...options.versions !== void 0 ? model.live.map((v) => ({ version: v, projectionVersion: v })) : [],
4588
+ { version: null, projectionVersion: model.current }
4589
+ ];
4590
+ for (const pass of passes) {
4591
+ const surface = buildVersionSurface({ ...pass, prefix, registry, model, makeRepo });
4592
+ const headers = deprecationHeaders({
4593
+ requested: pass.version,
4594
+ model,
4595
+ // The version root, with no trailing slash — never
4596
+ // createVersionedPaths(prefix, model.current).collection(''), which
4597
+ // would yield `${prefix}/${model.current}/` in every Link header.
4598
+ successor: `${prefix}/${model.current}`
4599
+ });
4600
+ if (headers !== null) {
4601
+ const attachDeprecationHeaders = async (c, next) => {
4602
+ await next();
4603
+ for (const [key, value] of Object.entries(headers)) c.res.headers.set(key, value);
4604
+ };
4605
+ for (const path of versionRoutePaths(registry, surface.paths)) {
4606
+ app.use(path, attachDeprecationHeaders);
4607
+ }
4608
+ }
4609
+ registerPublicContentRoutes(app, registry, surface.repos, surface.projectors, surface.paths, listRateLimit, programmaticResolver);
4610
+ }
4611
+ if (options.versions !== void 0) {
4612
+ app.all(`${prefix}/:version/*`, async (c, next) => {
4613
+ const kind = classifyVersion(c.req.param("version"), model);
4614
+ if (kind === "not-a-version" || kind === "live") return next();
4615
+ const live = model.live.join(", ");
4616
+ return kind === "retired" ? c.json(
4617
+ {
4618
+ ok: false,
4619
+ error: {
4620
+ code: "VERSION_RETIRED",
4621
+ message: `Version ${c.req.param("version")} is no longer served. Live versions: ${live}.`
4622
+ }
4623
+ },
4624
+ 410
4625
+ ) : c.json(
4626
+ {
4627
+ ok: false,
4628
+ error: {
4629
+ code: "VERSION_NOT_FOUND",
4630
+ message: `No version ${c.req.param("version")} is served. Live versions: ${live}.`
4631
+ }
4632
+ },
4633
+ 404
4634
+ );
4635
+ });
4636
+ }
4327
4637
  registerPublicMediaRoutes(app, mediaRepo, publicPaths, listRateLimit);
4328
4638
  if (options.graphql?.enabled) {
4329
4639
  const gqlOptions = options.graphql;
4330
- let gqlHandler = null;
4331
- let initError = null;
4640
+ const makeGraphqlRepo = (_typeName, tableName, sortableColumns) => createDrizzleContentRepository(db, tableName, { sortableColumns });
4641
+ const gqlPasses = [
4642
+ ...options.versions !== void 0 ? model.live.map((v) => ({ version: v, projectionVersion: v })) : [],
4643
+ { version: null, projectionVersion: model.current }
4644
+ ];
4645
+ const mounts = gqlPasses.map((pass) => ({
4646
+ path: pass.version === null ? "/graphql" : `/graphql/${pass.version}`,
4647
+ version: pass.version,
4648
+ handler: null,
4649
+ error: null
4650
+ }));
4332
4651
  const ready = Promise.resolve().then(() => (init_handler(), handler_exports)).then(({ createGraphQLHandler: createGraphQLHandler2 }) => {
4333
- gqlHandler = createGraphQLHandler2(registry, graphqlRepos, fieldKeyMaps, programmaticResolver, db, gqlOptions);
4652
+ gqlPasses.forEach((pass, i) => {
4653
+ const mount = mounts[i];
4654
+ try {
4655
+ const surface = buildVersionSurface({
4656
+ ...pass,
4657
+ prefix,
4658
+ registry,
4659
+ model,
4660
+ makeRepo: makeGraphqlRepo
4661
+ });
4662
+ const projection = model.projections[pass.projectionVersion];
4663
+ const currentProjection = model.projections[model.current];
4664
+ mount.handler = createGraphQLHandler2(
4665
+ registry,
4666
+ surface.repos,
4667
+ surface.fieldKeyMaps,
4668
+ programmaticResolver,
4669
+ db,
4670
+ gqlOptions,
4671
+ {
4672
+ endpoint: mount.path,
4673
+ currentVersion: model.current,
4674
+ ...projection !== void 0 && { projection },
4675
+ ...currentProjection !== void 0 && { currentProjection }
4676
+ }
4677
+ );
4678
+ } catch (err) {
4679
+ mount.error = err;
4680
+ const message = err instanceof Error ? err.message : String(err);
4681
+ process.stderr.write(
4682
+ `\u2717 GraphQL schema failed to initialize for ${mount.path}; it will return 500. ${message}
4683
+ `
4684
+ );
4685
+ }
4686
+ });
4334
4687
  }).catch((err) => {
4335
- initError = err;
4688
+ for (const m of mounts) m.error = err;
4336
4689
  const message = err instanceof Error ? err.message : String(err);
4337
- process.stderr.write(`\u2717 GraphQL schema failed to initialize; /graphql will return 500. ${message}
4690
+ const paths = mounts.map((m) => m.path).join(", ");
4691
+ process.stderr.write(`\u2717 GraphQL module failed to load; ${paths} will return 500. ${message}
4338
4692
  `);
4339
4693
  });
4340
- const invokeGraphQL = async (c) => {
4341
- if (!gqlHandler && !initError) await ready;
4342
- if (!gqlHandler) {
4694
+ const invokeFor = (mount) => async (c) => {
4695
+ if (!mount.handler && !mount.error) await ready;
4696
+ if (!mount.handler) {
4343
4697
  return c.json(
4344
4698
  { ok: false, error: { code: "GRAPHQL_INIT_FAILED", message: "GraphQL schema failed to initialize" } },
4345
4699
  500
4346
4700
  );
4347
4701
  }
4348
- return gqlHandler(c, async () => {
4702
+ return mount.handler(c, async () => {
4349
4703
  });
4350
4704
  };
4351
- if (listRateLimit) {
4352
- app.all("/graphql", listRateLimit, invokeGraphQL);
4353
- } else {
4354
- app.all("/graphql", invokeGraphQL);
4705
+ for (const mount of mounts) {
4706
+ const headers = deprecationHeaders({
4707
+ requested: mount.version,
4708
+ model,
4709
+ successor: `/graphql/${model.current}`
4710
+ });
4711
+ if (headers === null) continue;
4712
+ const attach = async (c, next) => {
4713
+ await next();
4714
+ for (const [key, value] of Object.entries(headers)) c.res.headers.set(key, value);
4715
+ };
4716
+ app.use(mount.path, attach);
4717
+ }
4718
+ for (const mount of mounts) {
4719
+ if (listRateLimit) app.all(mount.path, listRateLimit, invokeFor(mount));
4720
+ else app.all(mount.path, invokeFor(mount));
4721
+ }
4722
+ if (options.versions !== void 0) {
4723
+ app.all("/graphql/:version", async (c, next) => {
4724
+ const segment = c.req.param("version");
4725
+ const kind = classifyVersion(segment, model);
4726
+ if (kind === "not-a-version" || kind === "live") return next();
4727
+ const live = model.live.join(", ");
4728
+ return c.json({
4729
+ errors: [
4730
+ {
4731
+ message: kind === "retired" ? `Version ${segment} is no longer served. Live versions: ${live}.` : `Version ${segment} does not exist. Live versions: ${live}.`,
4732
+ extensions: {
4733
+ code: kind === "retired" ? "VERSION_RETIRED" : "VERSION_UNKNOWN",
4734
+ current: model.current,
4735
+ live: model.live
4736
+ }
4737
+ }
4738
+ ]
4739
+ });
4740
+ });
4355
4741
  }
4356
4742
  }
4357
4743
  registerConfigRoute(