@bobbykim/manguito-cms-api 0.5.0 → 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
@@ -45,7 +45,8 @@ function buildFieldKeyMap(pairs, allLabels, droppedKeys) {
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 === label) continue;
49
50
  const columnOwner = columnToLabel.get(label);
50
51
  if (columnOwner !== void 0 && columnOwner !== label) {
51
52
  throw new Error(
@@ -90,7 +91,12 @@ function createFieldKeyMap(fields) {
90
91
  }
91
92
  return buildFieldKeyMap(
92
93
  pairs,
93
- fields.map((f) => f.name),
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
+ })),
94
100
  droppedKeys
95
101
  );
96
102
  }
@@ -107,7 +113,13 @@ function createFieldKeyMapFromProjection(projectionType, allFields) {
107
113
  for (const label of projected) dropped.delete(label);
108
114
  return buildFieldKeyMap(
109
115
  pairs,
110
- allFields.map((f) => f.name),
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
+ })),
111
123
  dropped
112
124
  );
113
125
  }
@@ -537,10 +549,13 @@ var init_naming = __esm({
537
549
  });
538
550
 
539
551
  // src/graphql/filters.ts
540
- 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
+ );
541
556
  return new import_graphql3.GraphQLEnumType({
542
557
  name: `${typeName}SortField`,
543
- values: Object.fromEntries(SORTABLE.map((s) => [s.gql, { value: s.value }]))
558
+ values: Object.fromEntries(resolvable.map((s) => [s.gql, { value: s.value }]))
544
559
  });
545
560
  }
546
561
  function comparableFilterInput(name, scalar) {
@@ -575,18 +590,19 @@ function filterInputForField(fieldType) {
575
590
  if (scalar === null) return null;
576
591
  return StringFilter;
577
592
  }
578
- function buildFilterInputType(type) {
593
+ function buildFilterInputType(type, viewFields) {
579
594
  const fields = {};
580
595
  fields["id"] = { type: IDFilter };
581
596
  fields["published"] = { type: BooleanFilter };
582
597
  fields["createdAt"] = { type: DateTimeFilter };
583
598
  fields["updatedAt"] = { type: DateTimeFilter };
584
599
  if (type.schema_type === "content-type") fields["slug"] = { type: StringFilter };
585
- 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) {
586
602
  if (f.field_type === "programmatic" || f.field_type === "paragraph") continue;
587
603
  if (f.field_type === "image" || f.field_type === "video" || f.field_type === "file") continue;
588
604
  const input = filterInputForField(f.field_type);
589
- if (input) fields[buildFieldNameMap([f.name]).toGraphql(f.name)] = { type: input };
605
+ if (input) fields[buildFieldNameMap([exposedAs]).toGraphql(exposedAs)] = { type: input };
590
606
  }
591
607
  if (Object.keys(fields).length === 0) return null;
592
608
  return new import_graphql3.GraphQLInputObjectType({ name: `${graphqlTypeName(type.name)}Filter`, fields });
@@ -649,32 +665,41 @@ function codeError2(code, message) {
649
665
  function isPublished2(item) {
650
666
  return !!item && item["published"] === true;
651
667
  }
652
- function resolveFieldValue(field, row) {
668
+ function resolveFieldValue(field, row, fallback) {
653
669
  const key = isColumnBacked(field) ? field.db_column.column_name : field.name;
654
- 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;
655
673
  }
656
- function scalarFieldResolver(field) {
657
- return (parent) => resolveFieldValue(field, parent);
674
+ function scalarFieldResolver(field, fallback) {
675
+ return (parent) => resolveFieldValue(field, parent, fallback);
658
676
  }
659
677
  function relationFieldResolver(typeName, schemaFieldName) {
660
678
  return (parent, _args, ctx) => ctx.loaders.load(typeName, schemaFieldName, parent);
661
679
  }
662
- function programmaticFieldResolver(typeName, schemaFieldName, mediaFieldNames = [], fieldKeys) {
680
+ function programmaticFieldResolver(typeName, schemaFieldName, mediaFields = [], fieldKeys) {
663
681
  return async (parent, _args, ctx) => {
664
682
  let p = ctx.programmaticMemo.get(parent);
665
683
  if (!p) {
666
- p = resolveProgrammaticRow(typeName, parent, ctx, mediaFieldNames, fieldKeys);
684
+ p = resolveProgrammaticRow(typeName, parent, ctx, mediaFields, fieldKeys);
667
685
  ctx.programmaticMemo.set(parent, p);
668
686
  }
669
687
  return (await p)[schemaFieldName];
670
688
  };
671
689
  }
672
- async function resolveProgrammaticRow(typeName, parent, ctx, mediaFieldNames, fieldKeys) {
690
+ async function resolveProgrammaticRow(typeName, parent, ctx, mediaFields, fieldKeys) {
673
691
  const toLabels = (row) => fieldKeys ? fieldKeys.toLabels(row) : row;
674
- if (mediaFieldNames.length === 0) return ctx.resolver.resolveItem(typeName, toLabels(parent));
692
+ if (mediaFields.length === 0) return ctx.resolver.resolveItem(typeName, toLabels(parent));
675
693
  const enriched = { ...parent };
676
- await Promise.all(mediaFieldNames.map((name) => ctx.loaders.load(typeName, name, enriched)));
677
- 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;
678
703
  }
679
704
  function collectionResolver(typeName, nameMap, fieldKeys) {
680
705
  return async (_root, args, ctx) => {
@@ -732,7 +757,7 @@ var init_resolvers = __esm({
732
757
  });
733
758
 
734
759
  // src/graphql/schema.ts
735
- function buildGraphQLSchema(registry, fieldKeyMaps = {}) {
760
+ function buildGraphQLSchema(registry, fieldKeyMaps = {}, view) {
736
761
  const objectTypes = /* @__PURE__ */ new Map();
737
762
  const enumTypes = /* @__PURE__ */ new Map();
738
763
  for (const [name, enumType] of Object.entries(registry.enum_types)) {
@@ -751,14 +776,14 @@ function buildGraphQLSchema(registry, fieldKeyMaps = {}) {
751
776
  );
752
777
  }
753
778
  }
754
- function outputTypeForField(field) {
779
+ function outputTypeForField(field, required) {
755
780
  const scalar = scalarOutputType(field.field_type);
756
- if (scalar) return field.required ? new import_graphql5.GraphQLNonNull(scalar) : scalar;
781
+ if (scalar) return required ? new import_graphql5.GraphQLNonNull(scalar) : scalar;
757
782
  if (field.field_type === "enum") {
758
783
  const ref = field.ui_component.component === "select" ? field.ui_component.enum_ref : void 0;
759
784
  const et = ref ? enumTypes.get(ref) : void 0;
760
785
  const t = et ?? import_graphql5.GraphQLString;
761
- return field.required ? new import_graphql5.GraphQLNonNull(t) : t;
786
+ return required ? new import_graphql5.GraphQLNonNull(t) : t;
762
787
  }
763
788
  if (field.field_type === "image" || field.field_type === "video" || field.field_type === "file") {
764
789
  return MEDIA;
@@ -788,8 +813,17 @@ function buildGraphQLSchema(registry, fieldKeyMaps = {}) {
788
813
  }
789
814
  return import_graphql5.GraphQLString;
790
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
+ }
791
822
  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);
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 }));
793
827
  return new import_graphql5.GraphQLObjectType({
794
828
  name: graphqlTypeName(machineName),
795
829
  fields: () => {
@@ -803,23 +837,25 @@ function buildGraphQLSchema(registry, fieldKeyMaps = {}) {
803
837
  }
804
838
  fields["createdAt"] = { type: new import_graphql5.GraphQLNonNull(DateTimeScalar), resolve: (p) => p["created_at"] };
805
839
  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);
840
+ for (const vf of visible) {
841
+ const field = vf.field;
842
+ const gqlName = toCamelCase(vf.exposedAs);
843
+ const outType = outputTypeForField(field, vf.required);
809
844
  let resolve;
810
845
  if (field.field_type === "programmatic") {
811
- resolve = programmaticFieldResolver(
812
- machineName,
813
- field.name,
814
- mediaFieldNames,
815
- fieldKeyMaps[machineName]
816
- );
846
+ resolve = programmaticFieldResolver(machineName, field.name, mediaFields, fieldKeyMaps[machineName]);
817
847
  } else if (field.field_type === "reference" || field.field_type === "paragraph" || field.field_type === "image" || field.field_type === "video" || field.field_type === "file") {
818
848
  resolve = relationFieldResolver(machineName, field.name);
819
849
  } else {
820
- resolve = scalarFieldResolver(field);
850
+ resolve = scalarFieldResolver(field, vf.fallback);
821
851
  }
822
- 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
+ };
823
859
  }
824
860
  return fields;
825
861
  }
@@ -851,7 +887,8 @@ function buildGraphQLSchema(registry, fieldKeyMaps = {}) {
851
887
  const queryFields = {};
852
888
  for (const [name, ct] of Object.entries(registry.content_types)) {
853
889
  const objType = objectTypes.get(name);
854
- const nameMap = buildFieldNameMap(ct.fields.map((f) => f.name));
890
+ const visible = viewFieldsFor(name, ct);
891
+ const nameMap = buildFieldNameMap(visible.map((v) => v.exposedAs));
855
892
  if (ct.only_one) {
856
893
  queryFields[singleQueryName(name)] = { type: objType, resolve: singletonResolver(name) };
857
894
  continue;
@@ -863,13 +900,13 @@ function buildGraphQLSchema(registry, fieldKeyMaps = {}) {
863
900
  meta: { type: new import_graphql5.GraphQLNonNull(PAGE_META), resolve: (r) => r.meta }
864
901
  }
865
902
  });
866
- const filterType = buildFilterInputType(ct);
903
+ const filterType = buildFilterInputType(ct, visible);
867
904
  queryFields[collectionQueryName(name)] = {
868
905
  type: new import_graphql5.GraphQLNonNull(listType),
869
906
  args: {
870
907
  page: { type: import_graphql5.GraphQLInt },
871
908
  perPage: { type: import_graphql5.GraphQLInt },
872
- sortBy: { type: buildSortFieldEnum(graphqlTypeName(name)) },
909
+ sortBy: { type: buildSortFieldEnum(graphqlTypeName(name), fieldKeyMaps[name]) },
873
910
  sortOrder: { type: SortOrderEnum },
874
911
  ...filterType ? { filter: { type: filterType } } : {}
875
912
  },
@@ -893,7 +930,11 @@ function buildGraphQLSchema(registry, fieldKeyMaps = {}) {
893
930
  queryFields[collectionQueryName(name)] = {
894
931
  type: new import_graphql5.GraphQLNonNull(listType),
895
932
  args: { page: { type: import_graphql5.GraphQLInt }, perPage: { type: import_graphql5.GraphQLInt } },
896
- 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
+ )
897
938
  };
898
939
  queryFields[singleQueryName(name)] = {
899
940
  type: objType,
@@ -943,6 +984,76 @@ var init_schema = __esm({
943
984
  }
944
985
  });
945
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
+
946
1057
  // src/graphql/dataloaders.ts
947
1058
  function createRelationLoaders(db, registry) {
948
1059
  const relMaps = /* @__PURE__ */ new Map();
@@ -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);
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
  }
@@ -3816,7 +3942,7 @@ function registerUserRoutes(app, db, requirePermission, requireHierarchy) {
3816
3942
  var import_drizzle_orm6 = require("drizzle-orm");
3817
3943
  var API_VERSION = (() => {
3818
3944
  try {
3819
- return "0.5.0";
3945
+ return "0.6.0";
3820
3946
  } catch {
3821
3947
  return "0.0.0";
3822
3948
  }
@@ -4414,20 +4540,6 @@ function createCmsApp(options) {
4414
4540
  sortableColumns
4415
4541
  });
4416
4542
  };
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
4543
  registerAuthRoutes(app, db);
4432
4544
  app.use("/admin/api/*", authMiddleware);
4433
4545
  app.use("/admin/api/*", mustChangePasswordCheck);
@@ -4525,31 +4637,107 @@ function createCmsApp(options) {
4525
4637
  registerPublicMediaRoutes(app, mediaRepo, publicPaths, listRateLimit);
4526
4638
  if (options.graphql?.enabled) {
4527
4639
  const gqlOptions = options.graphql;
4528
- let gqlHandler = null;
4529
- 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
+ }));
4530
4651
  const ready = Promise.resolve().then(() => (init_handler(), handler_exports)).then(({ createGraphQLHandler: createGraphQLHandler2 }) => {
4531
- 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
+ });
4532
4687
  }).catch((err) => {
4533
- initError = err;
4688
+ for (const m of mounts) m.error = err;
4534
4689
  const message = err instanceof Error ? err.message : String(err);
4535
- 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}
4536
4692
  `);
4537
4693
  });
4538
- const invokeGraphQL = async (c) => {
4539
- if (!gqlHandler && !initError) await ready;
4540
- if (!gqlHandler) {
4694
+ const invokeFor = (mount) => async (c) => {
4695
+ if (!mount.handler && !mount.error) await ready;
4696
+ if (!mount.handler) {
4541
4697
  return c.json(
4542
4698
  { ok: false, error: { code: "GRAPHQL_INIT_FAILED", message: "GraphQL schema failed to initialize" } },
4543
4699
  500
4544
4700
  );
4545
4701
  }
4546
- return gqlHandler(c, async () => {
4702
+ return mount.handler(c, async () => {
4547
4703
  });
4548
4704
  };
4549
- if (listRateLimit) {
4550
- app.all("/graphql", listRateLimit, invokeGraphQL);
4551
- } else {
4552
- 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
+ });
4553
4741
  }
4554
4742
  }
4555
4743
  registerConfigRoute(