@forklaunch/core 0.11.6 → 0.12.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.
@@ -10,18 +10,28 @@ function cors(corsOptions) {
10
10
  return res.getHeaders()[key];
11
11
  };
12
12
  }
13
- corsMiddleware(corsOptions)(req, res, next ?? (() => {
14
- }));
13
+ corsMiddleware(corsOptions)(
14
+ req,
15
+ res,
16
+ next ?? (() => {
17
+ })
18
+ );
15
19
  };
16
20
  }
17
21
 
18
22
  // src/http/router/expressLikeRouter.ts
19
23
  import {
24
+ isRecord as isRecord2,
20
25
  sanitizePathSlashes,
21
26
  toPrettyCamelCase,
22
27
  toRecord
23
28
  } from "@forklaunch/common";
24
29
 
30
+ // src/http/guards/hasVersionedSchema.ts
31
+ function hasVersionedSchema(contractDetails) {
32
+ return typeof contractDetails === "object" && contractDetails !== null && "versions" in contractDetails && contractDetails.versions !== null;
33
+ }
34
+
25
35
  // src/http/guards/isForklaunchRouter.ts
26
36
  function isForklaunchRouter(maybeForklaunchRouter) {
27
37
  return maybeForklaunchRouter != null && typeof maybeForklaunchRouter === "object" && "basePath" in maybeForklaunchRouter && "routes" in maybeForklaunchRouter && Array.isArray(maybeForklaunchRouter.routes);
@@ -52,12 +62,16 @@ function isForklaunchExpressLikeRouter(maybeForklaunchExpressLikeRouter) {
52
62
 
53
63
  // src/http/guards/isPathParamContractDetails.ts
54
64
  function isPathParamHttpContractDetails(maybePathParamHttpContractDetails) {
55
- return maybePathParamHttpContractDetails != null && typeof maybePathParamHttpContractDetails === "object" && "name" in maybePathParamHttpContractDetails && "summary" in maybePathParamHttpContractDetails && "responses" in maybePathParamHttpContractDetails && maybePathParamHttpContractDetails.name != null && maybePathParamHttpContractDetails.summary != null && maybePathParamHttpContractDetails.responses != null;
65
+ return maybePathParamHttpContractDetails != null && typeof maybePathParamHttpContractDetails === "object" && "name" in maybePathParamHttpContractDetails && "summary" in maybePathParamHttpContractDetails && maybePathParamHttpContractDetails.name != null && maybePathParamHttpContractDetails.summary != null && ("responses" in maybePathParamHttpContractDetails && maybePathParamHttpContractDetails.responses != null || "versions" in maybePathParamHttpContractDetails && typeof maybePathParamHttpContractDetails.versions === "object" && maybePathParamHttpContractDetails.versions != null && Object.values(maybePathParamHttpContractDetails.versions).every(
66
+ (version) => "responses" in version && version.responses != null
67
+ ));
56
68
  }
57
69
 
58
70
  // src/http/guards/isHttpContractDetails.ts
59
71
  function isHttpContractDetails(maybeContractDetails) {
60
- return isPathParamHttpContractDetails(maybeContractDetails) && "body" in maybeContractDetails && maybeContractDetails.body != null;
72
+ return isPathParamHttpContractDetails(maybeContractDetails) && ("body" in maybeContractDetails && maybeContractDetails.body != null || "versions" in maybeContractDetails && typeof maybeContractDetails.versions === "object" && maybeContractDetails.versions != null && Object.values(maybeContractDetails.versions).every(
73
+ (version) => "body" in version && version.body != null
74
+ ));
61
75
  }
62
76
 
63
77
  // src/http/guards/isTypedHandler.ts
@@ -206,7 +220,10 @@ async function checkAuthorizationToken(authorizationMethod, authorizationToken,
206
220
  if (!authorizationMethod.mapRoles) {
207
221
  return [500, "No role mapping function provided."];
208
222
  }
209
- const resourceRoles = await authorizationMethod.mapRoles(resourceId, req);
223
+ const resourceRoles = await authorizationMethod.mapRoles(
224
+ resourceId,
225
+ req
226
+ );
210
227
  if ("allowedRoles" in authorizationMethod && authorizationMethod.allowedRoles) {
211
228
  if (resourceRoles.intersection(authorizationMethod.allowedRoles).size === 0) {
212
229
  return invalidAuthorizationTokenRoles;
@@ -227,6 +244,7 @@ async function parseRequestAuth(req, res, next) {
227
244
  const [error, message] = await checkAuthorizationToken(
228
245
  auth,
229
246
  req.headers[auth.headerName ?? "Authorization"] || req.headers[auth.headerName ?? "authorization"],
247
+ // we can safely cast here because we know that the user will supply resolution for the request
230
248
  req
231
249
  ) ?? [];
232
250
  if (error != null) {
@@ -546,6 +564,7 @@ function enrichDetails(path, contractDetails, requestSchema, responseSchemas, op
546
564
  }
547
565
 
548
566
  // src/http/middleware/request/parse.middleware.ts
567
+ import { isRecord } from "@forklaunch/common";
549
568
  import {
550
569
  prettyPrintParseErrors
551
570
  } from "@forklaunch/validator";
@@ -568,10 +587,49 @@ function parse(req, res, next) {
568
587
  headers: req.headers,
569
588
  body: req.body
570
589
  };
571
- const parsedRequest = req.schemaValidator.parse(
572
- req.requestSchema,
573
- request
574
- );
590
+ const schemaValidator = req.schemaValidator;
591
+ let matchedVersions;
592
+ let parsedRequest;
593
+ let collectedParseErrors;
594
+ if (req.contractDetails.versions) {
595
+ if (isRecord(req.requestSchema)) {
596
+ let runningParseErrors = "";
597
+ matchedVersions = [];
598
+ Object.entries(req.requestSchema).forEach(([version, schema]) => {
599
+ const parsingResult = schemaValidator.parse(schema, request);
600
+ if (parsingResult.ok) {
601
+ parsedRequest = parsingResult;
602
+ matchedVersions.push(version);
603
+ req.version = version;
604
+ res.version = req.version;
605
+ } else {
606
+ runningParseErrors += prettyPrintParseErrors(
607
+ parsingResult.errors,
608
+ `Version ${version} request`
609
+ );
610
+ }
611
+ });
612
+ if (!parsedRequest) {
613
+ parsedRequest = {
614
+ ok: false,
615
+ errors: []
616
+ };
617
+ collectedParseErrors = runningParseErrors;
618
+ }
619
+ } else {
620
+ req.version = Object.keys(req.contractDetails.versions).pop();
621
+ res.version = req.version;
622
+ parsedRequest = {
623
+ ok: true,
624
+ value: request
625
+ };
626
+ matchedVersions = Object.keys(req.contractDetails.versions);
627
+ }
628
+ } else {
629
+ const parsingResult = schemaValidator.parse(req.requestSchema, request);
630
+ parsedRequest = parsingResult;
631
+ matchedVersions = 0;
632
+ }
575
633
  if (parsedRequest.ok && isRequestShape(parsedRequest.value)) {
576
634
  req.body = parsedRequest.value.body;
577
635
  req.params = parsedRequest.value.params;
@@ -602,10 +660,7 @@ function parse(req, res, next) {
602
660
  res.status(400);
603
661
  if (hasSend(res)) {
604
662
  res.send(
605
- `${prettyPrintParseErrors(
606
- parsedRequest.errors,
607
- "Request"
608
- )}
663
+ `${collectedParseErrors ?? prettyPrintParseErrors(parsedRequest.errors, "Request")}
609
664
 
610
665
  Correlation id: ${req.context.correlationId ?? "No correlation ID"}`
611
666
  );
@@ -615,13 +670,14 @@ Correlation id: ${req.context.correlationId ?? "No correlation ID"}`
615
670
  return;
616
671
  case "warning":
617
672
  req.openTelemetryCollector.warn(
618
- prettyPrintParseErrors(parsedRequest.errors, "Request")
673
+ collectedParseErrors ?? prettyPrintParseErrors(parsedRequest.errors, "Request")
619
674
  );
620
675
  break;
621
676
  case "none":
622
677
  break;
623
678
  }
624
679
  }
680
+ req._parsedVersions = matchedVersions;
625
681
  next?.();
626
682
  }
627
683
 
@@ -759,13 +815,12 @@ function discriminateResponseBodies(schemaValidator, responses) {
759
815
 
760
816
  // src/http/router/expressLikeRouter.ts
761
817
  var ForklaunchExpressLikeRouter = class _ForklaunchExpressLikeRouter {
762
- constructor(basePath, schemaValidator, internal, postEnrichMiddleware, openTelemetryCollector, sdkName) {
818
+ constructor(basePath, schemaValidator, internal, postEnrichMiddleware, openTelemetryCollector) {
763
819
  this.basePath = basePath;
764
820
  this.schemaValidator = schemaValidator;
765
821
  this.internal = internal;
766
822
  this.postEnrichMiddleware = postEnrichMiddleware;
767
823
  this.openTelemetryCollector = openTelemetryCollector;
768
- this.sdkName = sdkName;
769
824
  if (process.env.NODE_ENV !== "test" && !process.env.VITEST) {
770
825
  process.on("uncaughtException", (err) => {
771
826
  this.openTelemetryCollector.error(`Uncaught exception: ${err}`);
@@ -788,8 +843,9 @@ var ForklaunchExpressLikeRouter = class _ForklaunchExpressLikeRouter {
788
843
  requestHandler;
789
844
  routers = [];
790
845
  routes = [];
791
- fetchMap = {};
846
+ _fetchMap = {};
792
847
  sdk = {};
848
+ sdkPaths = {};
793
849
  /**
794
850
  * Resolves middlewares based on the contract details.
795
851
  *
@@ -859,50 +915,112 @@ var ForklaunchExpressLikeRouter = class _ForklaunchExpressLikeRouter {
859
915
  }
860
916
  return controllerHandler;
861
917
  }
862
- #compile(contractDetails) {
918
+ #processContractDetailsIO(contractDetailsIO, params) {
863
919
  const schemaValidator = this.schemaValidator;
864
- let body = null;
865
- if (isHttpContractDetails(contractDetails)) {
866
- body = discriminateBody(this.schemaValidator, contractDetails.body);
867
- }
868
- const requestSchema = schemaValidator.compile(
869
- schemaValidator.schemify({
870
- ...contractDetails.params ? { params: contractDetails.params } : {},
871
- ...contractDetails.requestHeaders ? { headers: contractDetails.requestHeaders } : {},
872
- ...contractDetails.query ? { query: contractDetails.query } : {},
873
- ...body != null ? { body: body.schema } : {}
874
- })
875
- );
876
- const responseEntries = {
920
+ const responseSchemas = {
877
921
  400: schemaValidator.string,
878
922
  401: schemaValidator.string,
879
923
  403: schemaValidator.string,
880
924
  404: schemaValidator.string,
881
925
  500: schemaValidator.string,
882
- ...isPathParamHttpContractDetails(contractDetails) || isHttpContractDetails(contractDetails) ? Object.fromEntries(
926
+ ...Object.fromEntries(
883
927
  Object.entries(
884
928
  discriminateResponseBodies(
885
929
  this.schemaValidator,
886
- contractDetails.responses
930
+ contractDetailsIO.responses
887
931
  )
888
932
  ).map(([key, value]) => {
889
- return [key, value.schema];
933
+ return [Number(key), value.schema];
890
934
  })
891
- ) : {}
935
+ )
892
936
  };
893
- const responseSchemas = {
894
- responses: {},
895
- ...contractDetails.responseHeaders ? {
896
- headers: schemaValidator.compile(
897
- schemaValidator.schemify(contractDetails.responseHeaders)
937
+ return {
938
+ requestSchema: schemaValidator.compile(
939
+ schemaValidator.schemify({
940
+ ...params != null ? { params } : { params: schemaValidator.unknown },
941
+ ...contractDetailsIO.requestHeaders != null ? { headers: contractDetailsIO.requestHeaders } : { headers: schemaValidator.unknown },
942
+ ...contractDetailsIO.query != null ? { query: contractDetailsIO.query } : { query: schemaValidator.unknown },
943
+ ...contractDetailsIO.body != null ? {
944
+ body: discriminateBody(
945
+ this.schemaValidator,
946
+ contractDetailsIO.body
947
+ )?.schema
948
+ } : { body: schemaValidator.unknown }
949
+ })
950
+ ),
951
+ responseSchemas: {
952
+ ...contractDetailsIO.responseHeaders != null ? {
953
+ headers: schemaValidator.compile(
954
+ schemaValidator.schemify(contractDetailsIO.responseHeaders)
955
+ )
956
+ } : { headers: schemaValidator.unknown },
957
+ responses: Object.fromEntries(
958
+ Object.entries(responseSchemas).map(([key, value]) => {
959
+ return [
960
+ key,
961
+ schemaValidator.compile(schemaValidator.schemify(value))
962
+ ];
963
+ })
898
964
  )
899
- } : {}
965
+ }
900
966
  };
901
- Object.entries(responseEntries).forEach(([code, responseShape]) => {
902
- responseSchemas.responses[Number(code)] = schemaValidator.compile(
903
- schemaValidator.schemify(responseShape)
967
+ }
968
+ #compile(contractDetails) {
969
+ const schemaValidator = this.schemaValidator;
970
+ let requestSchema;
971
+ let responseSchemas;
972
+ if (hasVersionedSchema(contractDetails)) {
973
+ requestSchema = {};
974
+ responseSchemas = {};
975
+ Object.entries(contractDetails.versions ?? {}).forEach(
976
+ ([version, versionedContractDetails]) => {
977
+ const {
978
+ requestSchema: versionedRequestSchema,
979
+ responseSchemas: versionedResponseSchemas
980
+ } = this.#processContractDetailsIO(
981
+ versionedContractDetails,
982
+ contractDetails.params
983
+ );
984
+ if (isRecord2(requestSchema)) {
985
+ requestSchema = {
986
+ ...requestSchema,
987
+ [version]: versionedRequestSchema
988
+ };
989
+ }
990
+ if (isRecord2(responseSchemas)) {
991
+ responseSchemas = {
992
+ ...responseSchemas,
993
+ [version]: versionedResponseSchemas
994
+ };
995
+ }
996
+ }
904
997
  );
905
- });
998
+ } else {
999
+ const {
1000
+ requestSchema: unversionedRequestSchema,
1001
+ responseSchemas: unversionedResponseSchemas
1002
+ } = this.#processContractDetailsIO(
1003
+ {
1004
+ ..."params" in contractDetails && contractDetails.params != null ? { params: contractDetails.params } : { params: schemaValidator.unknown },
1005
+ ..."requestHeaders" in contractDetails && contractDetails.requestHeaders != null ? { requestHeaders: contractDetails.requestHeaders } : {
1006
+ requestHeaders: schemaValidator.unknown
1007
+ },
1008
+ ..."responseHeaders" in contractDetails && contractDetails.responseHeaders != null ? { responseHeaders: contractDetails.responseHeaders } : {
1009
+ responseHeaders: schemaValidator.unknown
1010
+ },
1011
+ ..."query" in contractDetails && contractDetails.query != null ? { query: contractDetails.query } : {
1012
+ query: schemaValidator.unknown
1013
+ },
1014
+ ..."body" in contractDetails && contractDetails.body != null ? { body: contractDetails.body } : {
1015
+ body: schemaValidator.unknown
1016
+ },
1017
+ responses: "responses" in contractDetails && contractDetails.responses != null ? contractDetails.responses : schemaValidator.unknown
1018
+ },
1019
+ contractDetails.params
1020
+ );
1021
+ requestSchema = unversionedRequestSchema;
1022
+ responseSchemas = unversionedResponseSchemas;
1023
+ }
906
1024
  return {
907
1025
  requestSchema,
908
1026
  responseSchemas
@@ -911,15 +1029,18 @@ var ForklaunchExpressLikeRouter = class _ForklaunchExpressLikeRouter {
911
1029
  /**
912
1030
  * Fetches a route from the route map and executes it with the given parameters.
913
1031
  *
914
- * @template Path - The path type that extends keyof fetchMap and string.
1032
+ * @template Path - The path type that extends keyof _fetchMap and string.
915
1033
  * @param {Path} path - The route path
916
- * @param {Parameters<fetchMap[Path]>[1]} [requestInit] - Optional request initialization parameters.
917
- * @returns {Promise<ReturnType<fetchMap[Path]>>} - The result of executing the route handler.
1034
+ * @param {Parameters<_fetchMap[Path]>[1]} [requestInit] - Optional request initialization parameters.
1035
+ * @returns {Promise<ReturnType<_fetchMap[Path]>>} - The result of executing the route handler.
918
1036
  */
919
1037
  fetch = async (path, ...reqInit) => {
920
- return this.fetchMap[path](
1038
+ const method = reqInit[0]?.method;
1039
+ const version = reqInit[0] != null && "version" in reqInit[0] ? reqInit[0].version : void 0;
1040
+ return (version ? this._fetchMap[path][method ?? "GET"][version] : this._fetchMap[path][method ?? "GET"])(
921
1041
  path,
922
1042
  reqInit[0]
1043
+ // reqInit
923
1044
  );
924
1045
  };
925
1046
  /**
@@ -929,7 +1050,7 @@ var ForklaunchExpressLikeRouter = class _ForklaunchExpressLikeRouter {
929
1050
  * @param controllerHandler
930
1051
  * @returns
931
1052
  */
932
- #localParamRequest(handlers, controllerHandler) {
1053
+ #localParamRequest(handlers, controllerHandler, version) {
933
1054
  return async (route, request) => {
934
1055
  let statusCode;
935
1056
  let responseMessage;
@@ -939,7 +1060,8 @@ var ForklaunchExpressLikeRouter = class _ForklaunchExpressLikeRouter {
939
1060
  query: request?.query ?? {},
940
1061
  headers: request?.headers ?? {},
941
1062
  body: discriminateBody(this.schemaValidator, request?.body)?.schema ?? {},
942
- path: route
1063
+ path: route,
1064
+ version
943
1065
  };
944
1066
  const res = {
945
1067
  status: (code) => {
@@ -960,7 +1082,8 @@ var ForklaunchExpressLikeRouter = class _ForklaunchExpressLikeRouter {
960
1082
  },
961
1083
  sseEmitter: (generator) => {
962
1084
  responseMessage = generator();
963
- }
1085
+ },
1086
+ version
964
1087
  };
965
1088
  let cursor = handlers.shift();
966
1089
  if (cursor) {
@@ -994,20 +1117,20 @@ var ForklaunchExpressLikeRouter = class _ForklaunchExpressLikeRouter {
994
1117
  registerRoute(method, path, registrationMethod, contractDetailsOrMiddlewareOrTypedHandler, ...middlewareOrMiddlewareAndTypedHandler) {
995
1118
  if (isTypedHandler(contractDetailsOrMiddlewareOrTypedHandler)) {
996
1119
  const { contractDetails, handlers } = contractDetailsOrMiddlewareOrTypedHandler;
997
- this.registerRoute(method, path, registrationMethod, contractDetails, ...handlers);
998
- return this;
1120
+ const router = this.registerRoute(method, path, registrationMethod, contractDetails, ...handlers);
1121
+ return router;
999
1122
  } else {
1000
1123
  const maybeTypedHandler = middlewareOrMiddlewareAndTypedHandler[middlewareOrMiddlewareAndTypedHandler.length - 1];
1001
1124
  if (isTypedHandler(maybeTypedHandler)) {
1002
1125
  const { contractDetails, handlers } = maybeTypedHandler;
1003
- this.registerRoute(
1126
+ const router = this.registerRoute(
1004
1127
  method,
1005
1128
  path,
1006
1129
  registrationMethod,
1007
1130
  contractDetails,
1008
1131
  ...middlewareOrMiddlewareAndTypedHandler.concat(handlers)
1009
1132
  );
1010
- return this;
1133
+ return router;
1011
1134
  } else {
1012
1135
  if (isExpressLikeSchemaHandler(contractDetailsOrMiddlewareOrTypedHandler) || isTypedHandler(contractDetailsOrMiddlewareOrTypedHandler)) {
1013
1136
  throw new Error("Contract details are not defined");
@@ -1021,6 +1144,17 @@ var ForklaunchExpressLikeRouter = class _ForklaunchExpressLikeRouter {
1021
1144
  "Contract details are malformed for route definition"
1022
1145
  );
1023
1146
  }
1147
+ if (contractDetails.versions) {
1148
+ const parserTypes = Object.values(contractDetails.versions).map(
1149
+ (version) => discriminateBody(this.schemaValidator, version.body)?.parserType
1150
+ );
1151
+ const allParserTypesSame = parserTypes.length === 0 || parserTypes.every((pt) => pt === parserTypes[0]);
1152
+ if (!allParserTypesSame) {
1153
+ throw new Error(
1154
+ "All versioned contractDetails must have the same parsing type for body."
1155
+ );
1156
+ }
1157
+ }
1024
1158
  this.routes.push({
1025
1159
  basePath: this.basePath,
1026
1160
  path,
@@ -1029,22 +1163,39 @@ var ForklaunchExpressLikeRouter = class _ForklaunchExpressLikeRouter {
1029
1163
  });
1030
1164
  const { requestSchema, responseSchemas } = this.#compile(contractDetails);
1031
1165
  const controllerHandler = this.#extractControllerHandler(handlers);
1166
+ const resolvedMiddlewares = this.#resolveMiddlewares(
1167
+ path,
1168
+ contractDetails,
1169
+ requestSchema,
1170
+ responseSchemas
1171
+ ).concat(handlers);
1032
1172
  registrationMethod.bind(this.internal)(
1033
1173
  path,
1034
- ...this.#resolveMiddlewares(
1035
- path,
1036
- contractDetails,
1037
- requestSchema,
1038
- responseSchemas
1039
- ).concat(handlers),
1174
+ ...resolvedMiddlewares,
1040
1175
  this.#parseAndRunControllerHandler(controllerHandler)
1041
1176
  );
1042
- const localParamRequest = this.#localParamRequest(
1043
- handlers,
1044
- controllerHandler
1177
+ toRecord(this._fetchMap)[sanitizePathSlashes(`${this.basePath}${path}`)] = {
1178
+ ...this._fetchMap[sanitizePathSlashes(`${this.basePath}${path}`)] ?? {},
1179
+ [method.toUpperCase()]: contractDetails.versions ? Object.fromEntries(
1180
+ Object.keys(contractDetails.versions).map((version) => [
1181
+ version,
1182
+ this.#localParamRequest(handlers, controllerHandler, version)
1183
+ ])
1184
+ ) : this.#localParamRequest(handlers, controllerHandler)
1185
+ };
1186
+ toRecord(this.sdk)[toPrettyCamelCase(contractDetails.name)] = contractDetails.versions ? Object.fromEntries(
1187
+ Object.keys(contractDetails.versions).map((version) => [
1188
+ version,
1189
+ (req) => this.#localParamRequest(
1190
+ handlers,
1191
+ controllerHandler,
1192
+ version
1193
+ )(`${this.basePath}${path}`, req)
1194
+ ])
1195
+ ) : (req) => this.#localParamRequest(handlers, controllerHandler)(
1196
+ `${this.basePath}${path}`,
1197
+ req
1045
1198
  );
1046
- toRecord(this.fetchMap)[sanitizePathSlashes(`${this.basePath}${path}`)] = localParamRequest;
1047
- toRecord(this.sdk)[toPrettyCamelCase(contractDetails.name ?? this.basePath)] = (req) => localParamRequest(`${this.basePath}${path}`, req);
1048
1199
  return this;
1049
1200
  }
1050
1201
  }
@@ -1130,8 +1281,8 @@ var ForklaunchExpressLikeRouter = class _ForklaunchExpressLikeRouter {
1130
1281
  return this;
1131
1282
  }
1132
1283
  addRouterToSdk(router) {
1133
- Object.entries(router.fetchMap).map(
1134
- ([key, value]) => toRecord(this.fetchMap)[sanitizePathSlashes(`${this.basePath}${key}`)] = value
1284
+ Object.entries(router._fetchMap).map(
1285
+ ([key, value]) => toRecord(this._fetchMap)[sanitizePathSlashes(`${this.basePath}${key}`)] = value
1135
1286
  );
1136
1287
  const existingSdk = this.sdk[router.sdkName ?? toPrettyCamelCase(router.basePath)];
1137
1288
  toRecord(this.sdk)[router.sdkName ?? toPrettyCamelCase(router.basePath)] = {
@@ -1391,8 +1542,9 @@ var ForklaunchExpressLikeRouter = class _ForklaunchExpressLikeRouter {
1391
1542
  cloneInternals(clone) {
1392
1543
  clone.routers = [...this.routers];
1393
1544
  clone.routes = [...this.routes];
1394
- clone.fetchMap = { ...this.fetchMap };
1545
+ clone._fetchMap = { ...this._fetchMap };
1395
1546
  clone.sdk = { ...this.sdk };
1547
+ clone.sdkPaths = { ...this.sdkPaths };
1396
1548
  }
1397
1549
  clone() {
1398
1550
  const clone = new _ForklaunchExpressLikeRouter(
@@ -1400,8 +1552,7 @@ var ForklaunchExpressLikeRouter = class _ForklaunchExpressLikeRouter {
1400
1552
  this.schemaValidator,
1401
1553
  this.internal,
1402
1554
  this.postEnrichMiddleware,
1403
- this.openTelemetryCollector,
1404
- this.sdkName
1555
+ this.openTelemetryCollector
1405
1556
  );
1406
1557
  this.cloneInternals(clone);
1407
1558
  return clone;
@@ -1456,9 +1607,13 @@ function typedHandler(_schemaValidator, pathOrContractMethod, contractMethodOrCo
1456
1607
  throw new Error("Invalid definition for handler");
1457
1608
  }
1458
1609
  }
1610
+ if (isPath(pathOrContractMethod) && typeof contractMethodOrContractDetails !== "string") {
1611
+ throw new Error("Contract method not supplied, bailing");
1612
+ }
1459
1613
  return {
1460
1614
  _typedHandler: true,
1461
1615
  _path: isPath(pathOrContractMethod) ? pathOrContractMethod : void 0,
1616
+ _method: isPath(pathOrContractMethod) ? contractMethodOrContractDetails : pathOrContractMethod,
1462
1617
  contractDetails,
1463
1618
  handlers
1464
1619
  };
@@ -2498,37 +2653,57 @@ var getCodeForStatus = (status) => {
2498
2653
  var httpStatusCodes_default = HTTPStatuses;
2499
2654
 
2500
2655
  // src/http/mcpGenerator/mcpGenerator.ts
2501
- import { isNever as isNever3, isRecord, safeStringify as safeStringify2 } from "@forklaunch/common";
2656
+ import { isNever as isNever3, isRecord as isRecord3, safeStringify as safeStringify2 } from "@forklaunch/common";
2502
2657
  import { string, ZodSchemaValidator } from "@forklaunch/validator/zod";
2503
2658
  import { FastMCP } from "fastmcp";
2504
2659
 
2660
+ // src/http/guards/isVersionedInputSchema.ts
2661
+ function isUnionable(schema) {
2662
+ return schema.length > 1;
2663
+ }
2664
+
2505
2665
  // src/http/router/unpackRouters.ts
2506
- import { toPrettyCamelCase as toPrettyCamelCase2 } from "@forklaunch/common";
2507
- function unpackRouters(routers, recursiveBasePath = [], recursiveSdkPath = []) {
2666
+ function unpackRouters(routers, recursiveBasePath = []) {
2508
2667
  return routers.reduce((acc, router) => {
2509
2668
  acc.push({
2510
2669
  fullPath: [...recursiveBasePath, router.basePath].join(""),
2511
- sdkPath: [
2512
- ...recursiveSdkPath,
2513
- toPrettyCamelCase2(router.sdkName ?? router.basePath)
2514
- ].join("."),
2515
2670
  router
2516
2671
  });
2517
2672
  acc.push(
2518
- ...unpackRouters(
2519
- router.routers,
2520
- [...recursiveBasePath, router.basePath],
2521
- [
2522
- ...recursiveSdkPath,
2523
- toPrettyCamelCase2(router.sdkName ?? router.basePath)
2524
- ]
2525
- )
2673
+ ...unpackRouters(router.routers, [
2674
+ ...recursiveBasePath,
2675
+ router.basePath
2676
+ ])
2526
2677
  );
2527
2678
  return acc;
2528
2679
  }, []);
2529
2680
  }
2530
2681
 
2531
2682
  // src/http/mcpGenerator/mcpGenerator.ts
2683
+ function generateInputSchema(schemaValidator, body, params, query, requestHeaders, auth) {
2684
+ let discriminatedBody;
2685
+ if (body) {
2686
+ discriminatedBody = discriminateBody(schemaValidator, body);
2687
+ }
2688
+ return schemaValidator.schemify({
2689
+ ...discriminatedBody && body ? {
2690
+ ..."contentType" in body ? { contentType: body.contentType } : {},
2691
+ body: schemaValidator.schemify(discriminatedBody.schema)
2692
+ } : {},
2693
+ ...params ? { params: schemaValidator.schemify(params) } : {},
2694
+ ...query ? { query: schemaValidator.schemify(query) } : {},
2695
+ ...requestHeaders ? {
2696
+ headers: schemaValidator.schemify({
2697
+ ...requestHeaders,
2698
+ ...auth ? {
2699
+ [auth.headerName ?? "authorization"]: string.startsWith(
2700
+ auth.tokenPrefix ?? ("basic" in auth ? "Basic " : "Bearer ")
2701
+ )
2702
+ } : {}
2703
+ })
2704
+ } : {}
2705
+ });
2706
+ }
2532
2707
  function generateMcpServer(schemaValidator, protocol, host, port, version, routers, options2, contentTypeMap) {
2533
2708
  if (!(schemaValidator instanceof ZodSchemaValidator)) {
2534
2709
  throw new Error(
@@ -2542,35 +2717,36 @@ function generateMcpServer(schemaValidator, protocol, host, port, version, route
2542
2717
  });
2543
2718
  unpackRouters(routers).forEach(({ fullPath, router }) => {
2544
2719
  router.routes.forEach((route) => {
2545
- let discriminatedBody;
2546
- if ("body" in route.contractDetails) {
2547
- discriminatedBody = discriminateBody(
2548
- schemaValidator,
2549
- route.contractDetails.body
2720
+ const inputSchemas = [];
2721
+ if (route.contractDetails.versions) {
2722
+ Object.values(route.contractDetails.versions).forEach((version2) => {
2723
+ inputSchemas.push(
2724
+ generateInputSchema(
2725
+ schemaValidator,
2726
+ version2.body,
2727
+ route.contractDetails.params,
2728
+ version2.query,
2729
+ version2.requestHeaders,
2730
+ route.contractDetails.auth
2731
+ )
2732
+ );
2733
+ });
2734
+ } else {
2735
+ inputSchemas.push(
2736
+ generateInputSchema(
2737
+ schemaValidator,
2738
+ route.contractDetails.body,
2739
+ route.contractDetails.params,
2740
+ route.contractDetails.query,
2741
+ route.contractDetails.requestHeaders,
2742
+ route.contractDetails.auth
2743
+ )
2550
2744
  );
2551
2745
  }
2552
- const inputSchema = schemaValidator.schemify({
2553
- ...discriminatedBody && "body" in route.contractDetails ? {
2554
- ..."contentType" in route.contractDetails.body ? { contentType: route.contractDetails.body.contentType } : {},
2555
- body: schemaValidator.schemify(discriminatedBody.schema)
2556
- } : {},
2557
- ...route.contractDetails.params ? { params: schemaValidator.schemify(route.contractDetails.params) } : {},
2558
- ...route.contractDetails.query ? { query: schemaValidator.schemify(route.contractDetails.query) } : {},
2559
- ...route.contractDetails.requestHeaders ? {
2560
- headers: schemaValidator.schemify({
2561
- ...route.contractDetails.requestHeaders,
2562
- ...route.contractDetails.auth ? {
2563
- [route.contractDetails.auth.headerName ?? "authorization"]: string.startsWith(
2564
- route.contractDetails.auth.tokenPrefix ?? ("basic" in route.contractDetails.auth ? "Basic " : "Bearer ")
2565
- )
2566
- } : {}
2567
- })
2568
- } : {}
2569
- });
2570
2746
  mcpServer.addTool({
2571
2747
  name: route.contractDetails.name,
2572
2748
  description: route.contractDetails.summary,
2573
- parameters: inputSchema,
2749
+ parameters: isUnionable(inputSchemas) ? schemaValidator.union(inputSchemas) : inputSchemas[0],
2574
2750
  execute: async (args) => {
2575
2751
  const { contentType, body, params, query, headers } = args;
2576
2752
  let url = `${protocol}://${host}:${port}${fullPath}${route.path}`;
@@ -2582,6 +2758,22 @@ function generateMcpServer(schemaValidator, protocol, host, port, version, route
2582
2758
  );
2583
2759
  }
2584
2760
  }
2761
+ let bodySchema;
2762
+ let responsesSchemas;
2763
+ if (route.contractDetails.versions) {
2764
+ Object.values(route.contractDetails.versions).forEach(
2765
+ (version2, index) => {
2766
+ if (version2.body && schemaValidator.parse(inputSchemas[index], args).ok) {
2767
+ bodySchema = version2.body;
2768
+ responsesSchemas = version2.responses;
2769
+ }
2770
+ }
2771
+ );
2772
+ } else {
2773
+ bodySchema = route.contractDetails.body;
2774
+ responsesSchemas = route.contractDetails.responses;
2775
+ }
2776
+ const discriminatedBody = bodySchema ? discriminateBody(schemaValidator, bodySchema) : void 0;
2585
2777
  let parsedBody;
2586
2778
  if (discriminatedBody) {
2587
2779
  switch (discriminatedBody.parserType) {
@@ -2599,7 +2791,7 @@ function generateMcpServer(schemaValidator, protocol, host, port, version, route
2599
2791
  }
2600
2792
  case "multipart": {
2601
2793
  const formData = new FormData();
2602
- if (isRecord(body)) {
2794
+ if (isRecord3(body)) {
2603
2795
  for (const key in body) {
2604
2796
  if (typeof body[key] === "string" || body[key] instanceof Blob) {
2605
2797
  formData.append(key, body[key]);
@@ -2614,7 +2806,7 @@ function generateMcpServer(schemaValidator, protocol, host, port, version, route
2614
2806
  break;
2615
2807
  }
2616
2808
  case "urlEncoded": {
2617
- if (isRecord(body)) {
2809
+ if (isRecord3(body)) {
2618
2810
  parsedBody = new URLSearchParams(
2619
2811
  Object.entries(body).map(([key, value]) => [
2620
2812
  key,
@@ -2657,9 +2849,12 @@ function generateMcpServer(schemaValidator, protocol, host, port, version, route
2657
2849
  `Error received while proxying request to ${url}: ${await response.text()}`
2658
2850
  );
2659
2851
  }
2852
+ if (!responsesSchemas) {
2853
+ throw new Error("No responses schemas found");
2854
+ }
2660
2855
  const contractContentType = discriminateResponseBodies(
2661
2856
  schemaValidator,
2662
- route.contractDetails.responses
2857
+ responsesSchemas
2663
2858
  )[response.status].contentType;
2664
2859
  switch (contentTypeMap && contentTypeMap[contractContentType] ? contentTypeMap[contractContentType] : contractContentType) {
2665
2860
  case "application/json":
@@ -2715,15 +2910,56 @@ function generateMcpServer(schemaValidator, protocol, host, port, version, route
2715
2910
  import {
2716
2911
  prettyPrintParseErrors as prettyPrintParseErrors2
2717
2912
  } from "@forklaunch/validator";
2913
+
2914
+ // src/http/guards/isResponseCompiledSchema.ts
2915
+ function isResponseCompiledSchema(schema) {
2916
+ return typeof schema === "object" && schema !== null && "responses" in schema;
2917
+ }
2918
+
2919
+ // src/http/middleware/response/parse.middleware.ts
2718
2920
  function parse2(req, res, next) {
2719
- const { headers, responses } = res.responseSchemas;
2921
+ let headers;
2922
+ let responses;
2923
+ const responseSchemas = res.responseSchemas;
2924
+ const schemaValidator = req.schemaValidator;
2925
+ if (!isResponseCompiledSchema(responseSchemas)) {
2926
+ const parsedVersions = req._parsedVersions;
2927
+ if (typeof parsedVersions === "number") {
2928
+ throw new Error("Request failed to parse given version map");
2929
+ }
2930
+ const mappedHeaderSchemas = parsedVersions.map(
2931
+ (version) => responseSchemas[version].headers
2932
+ );
2933
+ const mappedResponseSchemas = parsedVersions.map(
2934
+ (version) => responseSchemas[version].responses
2935
+ );
2936
+ const collapsedResponseSchemas = mappedResponseSchemas.reduce((acc, responseSchema) => {
2937
+ Object.entries(responseSchema).forEach(([status, schema]) => {
2938
+ if (!acc[Number(status)]) {
2939
+ acc[Number(status)] = [];
2940
+ }
2941
+ acc[Number(status)].push(schema);
2942
+ });
2943
+ return acc;
2944
+ }, {});
2945
+ headers = schemaValidator.union(mappedHeaderSchemas);
2946
+ responses = Object.fromEntries(
2947
+ Object.entries(collapsedResponseSchemas).map(([status, schemas]) => [
2948
+ status,
2949
+ schemaValidator.union(schemas)
2950
+ ])
2951
+ );
2952
+ } else {
2953
+ headers = responseSchemas.headers;
2954
+ responses = responseSchemas.responses;
2955
+ }
2720
2956
  const statusCode = Number(res.statusCode);
2721
- const parsedResponse = req.schemaValidator.parse(
2722
- responses?.[statusCode],
2957
+ const parsedResponse = schemaValidator.parse(
2958
+ [400, 401, 404, 403, 500].includes(statusCode) ? schemaValidator.union([schemaValidator.string, responses?.[statusCode]]) : responses?.[statusCode],
2723
2959
  res.bodyData
2724
2960
  );
2725
- const parsedHeaders = req.schemaValidator.parse(
2726
- headers ?? req.schemaValidator.unknown,
2961
+ const parsedHeaders = schemaValidator.parse(
2962
+ headers ?? schemaValidator.unknown,
2727
2963
  res.getHeaders()
2728
2964
  );
2729
2965
  const parseErrors = [];
@@ -2779,7 +3015,7 @@ import {
2779
3015
  isAsyncGenerator,
2780
3016
  isNever as isNever4,
2781
3017
  isNodeJsWriteableStream,
2782
- isRecord as isRecord2,
3018
+ isRecord as isRecord4,
2783
3019
  readableStreamToAsyncIterable,
2784
3020
  safeStringify as safeStringify3
2785
3021
  } from "@forklaunch/common";
@@ -2822,9 +3058,21 @@ function enrichExpressLikeSend(instance, req, res, originalOperation, originalSe
2822
3058
  originalSend.call(instance, "Not Found");
2823
3059
  errorSent = true;
2824
3060
  }
3061
+ let responses;
3062
+ if (req.contractDetails.responses == null && (req.contractDetails.versions == null || Object.values(req.contractDetails.versions).some(
3063
+ (version) => version?.responses == null
3064
+ ))) {
3065
+ throw new Error("Responses schema definitions are required");
3066
+ } else {
3067
+ if (req.contractDetails.responses != null) {
3068
+ responses = req.contractDetails.responses;
3069
+ } else {
3070
+ responses = req.contractDetails.versions[req.version].responses;
3071
+ }
3072
+ }
2825
3073
  const responseBodies = discriminateResponseBodies(
2826
3074
  req.schemaValidator,
2827
- req.contractDetails.responses
3075
+ responses
2828
3076
  );
2829
3077
  if (responseBodies != null && responseBodies[Number(res.statusCode)] != null) {
2830
3078
  res.type(responseBodies[Number(res.statusCode)].contentType);
@@ -2893,7 +3141,7 @@ ${res.locals.errorMessage}`;
2893
3141
  } else {
2894
3142
  const parserType = responseBodies?.[Number(res.statusCode)]?.parserType;
2895
3143
  res.bodyData = data;
2896
- if (isRecord2(data)) {
3144
+ if (isRecord4(data)) {
2897
3145
  switch (parserType) {
2898
3146
  case "json":
2899
3147
  res.bodyData = "json" in data ? data.json : data;
@@ -2950,7 +3198,8 @@ ${res.locals.errorMessage}`;
2950
3198
  }
2951
3199
 
2952
3200
  // src/http/openApiV3Generator/openApiV3Generator.ts
2953
- import { openApiCompliantPath, toPrettyCamelCase as toPrettyCamelCase3 } from "@forklaunch/common";
3201
+ import { openApiCompliantPath } from "@forklaunch/common";
3202
+ var OPENAPI_DEFAULT_VERSION = Symbol("default");
2954
3203
  function toUpperCase(str) {
2955
3204
  return str.charAt(0).toUpperCase() + str.slice(1);
2956
3205
  }
@@ -2960,25 +3209,50 @@ function transformBasePath(basePath) {
2960
3209
  }
2961
3210
  return `/${basePath}`;
2962
3211
  }
2963
- function generateOpenApiDocument(protocol, host, port, tags, paths, securitySchemes, otherServers) {
3212
+ function generateOpenApiDocument(protocol, host, port, versionedTags, versionedPaths, versionedSecuritySchemes, otherServers) {
2964
3213
  return {
2965
- openapi: "3.1.0",
2966
- info: {
2967
- title: process.env.API_TITLE || "",
2968
- version: process.env.VERSION || "1.0.0"
2969
- },
2970
- components: {
2971
- securitySchemes
2972
- },
2973
- tags,
2974
- servers: [
2975
- {
2976
- url: `${protocol}://${host}:${port}`,
2977
- description: "Main Server"
3214
+ [OPENAPI_DEFAULT_VERSION]: {
3215
+ openapi: "3.1.0",
3216
+ info: {
3217
+ title: process.env.API_TITLE || "API Definition",
3218
+ version: process.env.VERSION || "latest"
3219
+ },
3220
+ components: {
3221
+ securitySchemes: versionedSecuritySchemes[OPENAPI_DEFAULT_VERSION]
2978
3222
  },
2979
- ...otherServers || []
2980
- ],
2981
- paths
3223
+ tags: versionedTags[OPENAPI_DEFAULT_VERSION],
3224
+ servers: [
3225
+ {
3226
+ url: `${protocol}://${host}:${port}`,
3227
+ description: "Main Server"
3228
+ },
3229
+ ...otherServers || []
3230
+ ],
3231
+ paths: versionedPaths[OPENAPI_DEFAULT_VERSION]
3232
+ },
3233
+ ...Object.fromEntries(
3234
+ Object.entries(versionedPaths).map(([version, paths]) => [
3235
+ version,
3236
+ {
3237
+ openapi: "3.1.0",
3238
+ info: {
3239
+ title: process.env.API_TITLE || "API Definition",
3240
+ version
3241
+ },
3242
+ components: {
3243
+ securitySchemes: versionedSecuritySchemes[version]
3244
+ },
3245
+ tags: versionedTags[version],
3246
+ servers: [
3247
+ {
3248
+ url: `${protocol}://${host}:${port}`,
3249
+ description: "Main Server"
3250
+ }
3251
+ ],
3252
+ paths
3253
+ }
3254
+ ])
3255
+ )
2982
3256
  };
2983
3257
  }
2984
3258
  function contentResolver(schemaValidator, body, contentType) {
@@ -2997,143 +3271,232 @@ function contentResolver(schemaValidator, body, contentType) {
2997
3271
  }
2998
3272
  };
2999
3273
  }
3000
- function generateSwaggerDocument(schemaValidator, protocol, host, port, routers, otherServers) {
3001
- const tags = [];
3002
- const paths = {};
3003
- const securitySchemes = {};
3004
- unpackRouters(routers).forEach(({ fullPath, router, sdkPath }) => {
3274
+ function generateOperationObject(schemaValidator, path, method, controllerName, sdkPaths, securitySchemes, name, summary, responses, params, responseHeaders, requestHeaders, query, body, auth) {
3275
+ const typedSchemaValidator = schemaValidator;
3276
+ const coercedResponses = {};
3277
+ const discriminatedResponseBodiesResult = discriminateResponseBodies(
3278
+ schemaValidator,
3279
+ responses
3280
+ );
3281
+ for (const key in discriminatedResponseBodiesResult) {
3282
+ coercedResponses[key] = {
3283
+ description: httpStatusCodes_default[key],
3284
+ content: contentResolver(
3285
+ schemaValidator,
3286
+ discriminatedResponseBodiesResult[key].schema,
3287
+ discriminatedResponseBodiesResult[key].contentType
3288
+ ),
3289
+ headers: responseHeaders ? Object.fromEntries(
3290
+ Object.entries(responseHeaders).map(([key2, value]) => [
3291
+ key2,
3292
+ {
3293
+ schema: typedSchemaValidator.openapi(value)
3294
+ }
3295
+ ])
3296
+ ) : void 0
3297
+ };
3298
+ }
3299
+ const commonErrors = [400, 404, 500];
3300
+ for (const error of commonErrors) {
3301
+ if (!(error in responses)) {
3302
+ coercedResponses[error] = {
3303
+ description: httpStatusCodes_default[error],
3304
+ content: contentResolver(schemaValidator, schemaValidator.string)
3305
+ };
3306
+ }
3307
+ }
3308
+ const operationObject = {
3309
+ tags: [controllerName],
3310
+ summary: `${name}: ${summary}`,
3311
+ parameters: [],
3312
+ responses: coercedResponses,
3313
+ operationId: sdkPaths[[method, path].join(".")]
3314
+ };
3315
+ if (params) {
3316
+ for (const key in params) {
3317
+ operationObject.parameters?.push({
3318
+ name: key,
3319
+ in: "path",
3320
+ schema: typedSchemaValidator.openapi(params[key])
3321
+ });
3322
+ }
3323
+ }
3324
+ const discriminatedBodyResult = body ? discriminateBody(schemaValidator, body) : null;
3325
+ if (discriminatedBodyResult) {
3326
+ operationObject.requestBody = {
3327
+ required: true,
3328
+ content: contentResolver(
3329
+ schemaValidator,
3330
+ discriminatedBodyResult.schema,
3331
+ discriminatedBodyResult.contentType
3332
+ )
3333
+ };
3334
+ }
3335
+ if (requestHeaders) {
3336
+ for (const key in requestHeaders) {
3337
+ operationObject.parameters?.push({
3338
+ name: key,
3339
+ in: "header",
3340
+ schema: typedSchemaValidator.openapi(requestHeaders[key])
3341
+ });
3342
+ }
3343
+ }
3344
+ if (query) {
3345
+ for (const key in query) {
3346
+ operationObject.parameters?.push({
3347
+ name: key,
3348
+ in: "query",
3349
+ schema: typedSchemaValidator.openapi(query[key])
3350
+ });
3351
+ }
3352
+ }
3353
+ if (auth) {
3354
+ responses[401] = {
3355
+ description: httpStatusCodes_default[401],
3356
+ content: contentResolver(schemaValidator, schemaValidator.string)
3357
+ };
3358
+ responses[403] = {
3359
+ description: httpStatusCodes_default[403],
3360
+ content: contentResolver(schemaValidator, schemaValidator.string)
3361
+ };
3362
+ if ("basic" in auth) {
3363
+ operationObject.security = [
3364
+ {
3365
+ basic: Array.from(
3366
+ "allowedPermissions" in auth ? auth.allowedPermissions?.values() || [] : []
3367
+ )
3368
+ }
3369
+ ];
3370
+ securitySchemes["basic"] = {
3371
+ type: "http",
3372
+ scheme: "basic"
3373
+ };
3374
+ } else if (auth) {
3375
+ operationObject.security = [
3376
+ {
3377
+ [auth.headerName !== "Authorization" ? "bearer" : "apiKey"]: Array.from(
3378
+ "allowedPermissions" in auth ? auth.allowedPermissions?.values() || [] : []
3379
+ )
3380
+ }
3381
+ ];
3382
+ if (auth.headerName && auth.headerName !== "Authorization") {
3383
+ securitySchemes[auth.headerName] = {
3384
+ type: "apiKey",
3385
+ in: "header",
3386
+ name: auth.headerName
3387
+ };
3388
+ } else {
3389
+ securitySchemes["Authorization"] = {
3390
+ type: "http",
3391
+ scheme: "bearer",
3392
+ bearerFormat: "JWT"
3393
+ };
3394
+ }
3395
+ }
3396
+ }
3397
+ return operationObject;
3398
+ }
3399
+ function generateOpenApiSpecs(schemaValidator, protocol, host, port, routers, otherServers) {
3400
+ const versionedPaths = {
3401
+ [OPENAPI_DEFAULT_VERSION]: {}
3402
+ };
3403
+ const versionedTags = {
3404
+ [OPENAPI_DEFAULT_VERSION]: []
3405
+ };
3406
+ const versionedSecuritySchemes = {
3407
+ [OPENAPI_DEFAULT_VERSION]: {}
3408
+ };
3409
+ unpackRouters(routers).forEach(({ fullPath, router }) => {
3005
3410
  const controllerName = transformBasePath(fullPath);
3006
- tags.push({
3007
- name: controllerName,
3008
- description: `${toUpperCase(controllerName)} Operations`
3009
- });
3010
3411
  router.routes.forEach((route) => {
3011
3412
  const openApiPath = openApiCompliantPath(
3012
3413
  `${fullPath}${route.path === "/" ? "" : route.path}`
3013
3414
  );
3014
- if (!paths[openApiPath]) {
3015
- paths[openApiPath] = {};
3016
- }
3017
- const { name, summary, query, requestHeaders } = route.contractDetails;
3018
- const responses = {};
3019
- const discriminatedResponseBodiesResult = discriminateResponseBodies(
3020
- schemaValidator,
3021
- route.contractDetails.responses
3022
- );
3023
- for (const key in discriminatedResponseBodiesResult) {
3024
- responses[key] = {
3025
- description: httpStatusCodes_default[key],
3026
- content: contentResolver(
3415
+ const { name, summary, params, versions, auth } = route.contractDetails;
3416
+ if (versions) {
3417
+ for (const version of Object.keys(versions)) {
3418
+ if (!versionedPaths[version]) {
3419
+ versionedPaths[version] = {};
3420
+ }
3421
+ if (!versionedPaths[version][openApiPath]) {
3422
+ versionedPaths[version][openApiPath] = {};
3423
+ }
3424
+ if (!versionedTags[version]) {
3425
+ versionedTags[version] = [];
3426
+ }
3427
+ if (!versionedTags[version].find((tag) => tag.name === controllerName)) {
3428
+ versionedTags[version].push({
3429
+ name: controllerName,
3430
+ description: `${toUpperCase(controllerName)} Operations`
3431
+ });
3432
+ }
3433
+ if (!versionedSecuritySchemes[version]) {
3434
+ versionedSecuritySchemes[version] = {};
3435
+ }
3436
+ const { query, requestHeaders, body, responses, responseHeaders } = versions[version];
3437
+ const operationObject = generateOperationObject(
3027
3438
  schemaValidator,
3028
- discriminatedResponseBodiesResult[key].schema,
3029
- discriminatedResponseBodiesResult[key].contentType
3030
- )
3031
- };
3032
- }
3033
- const commonErrors = [400, 404, 500];
3034
- for (const error of commonErrors) {
3035
- if (!(error in responses)) {
3036
- responses[error] = {
3037
- description: httpStatusCodes_default[error],
3038
- content: contentResolver(schemaValidator, schemaValidator.string)
3039
- };
3439
+ route.path,
3440
+ route.method,
3441
+ controllerName,
3442
+ router.sdkPaths,
3443
+ versionedSecuritySchemes[version],
3444
+ name,
3445
+ summary,
3446
+ responses,
3447
+ params,
3448
+ responseHeaders,
3449
+ requestHeaders,
3450
+ query,
3451
+ body,
3452
+ auth
3453
+ );
3454
+ if (route.method !== "middleware") {
3455
+ versionedPaths[version][openApiPath][route.method] = operationObject;
3456
+ }
3040
3457
  }
3041
- }
3042
- const operationObject = {
3043
- tags: [controllerName],
3044
- summary: `${name}: ${summary}`,
3045
- parameters: [],
3046
- responses,
3047
- operationId: `${sdkPath}.${toPrettyCamelCase3(name)}`
3048
- };
3049
- if (route.contractDetails.params) {
3050
- for (const key in route.contractDetails.params) {
3051
- operationObject.parameters?.push({
3052
- name: key,
3053
- in: "path",
3054
- schema: schemaValidator.openapi(
3055
- route.contractDetails.params[key]
3056
- )
3057
- });
3458
+ } else {
3459
+ if (!versionedPaths[OPENAPI_DEFAULT_VERSION]) {
3460
+ versionedPaths[OPENAPI_DEFAULT_VERSION] = {};
3058
3461
  }
3059
- }
3060
- const discriminatedBodyResult = "body" in route.contractDetails ? discriminateBody(schemaValidator, route.contractDetails.body) : null;
3061
- if (discriminatedBodyResult) {
3062
- operationObject.requestBody = {
3063
- required: true,
3064
- content: contentResolver(
3065
- schemaValidator,
3066
- discriminatedBodyResult.schema,
3067
- discriminatedBodyResult.contentType
3068
- )
3069
- };
3070
- }
3071
- if (requestHeaders) {
3072
- for (const key in requestHeaders) {
3073
- operationObject.parameters?.push({
3074
- name: key,
3075
- in: "header",
3076
- schema: schemaValidator.openapi(
3077
- requestHeaders[key]
3078
- )
3079
- });
3462
+ if (!versionedPaths[OPENAPI_DEFAULT_VERSION][openApiPath]) {
3463
+ versionedPaths[OPENAPI_DEFAULT_VERSION][openApiPath] = {};
3080
3464
  }
3081
- }
3082
- if (query) {
3083
- for (const key in query) {
3084
- operationObject.parameters?.push({
3085
- name: key,
3086
- in: "query",
3087
- schema: schemaValidator.openapi(query[key])
3465
+ if (!versionedTags[OPENAPI_DEFAULT_VERSION]) {
3466
+ versionedTags[OPENAPI_DEFAULT_VERSION] = [];
3467
+ }
3468
+ if (!versionedTags[OPENAPI_DEFAULT_VERSION].find(
3469
+ (tag) => tag.name === controllerName
3470
+ )) {
3471
+ versionedTags[OPENAPI_DEFAULT_VERSION].push({
3472
+ name: controllerName,
3473
+ description: `${toUpperCase(controllerName)} Operations`
3088
3474
  });
3089
3475
  }
3090
- }
3091
- if (route.contractDetails.auth) {
3092
- responses[401] = {
3093
- description: httpStatusCodes_default[401],
3094
- content: contentResolver(schemaValidator, schemaValidator.string)
3095
- };
3096
- responses[403] = {
3097
- description: httpStatusCodes_default[403],
3098
- content: contentResolver(schemaValidator, schemaValidator.string)
3099
- };
3100
- if ("basic" in route.contractDetails.auth) {
3101
- operationObject.security = [
3102
- {
3103
- basic: Array.from(
3104
- "allowedPermissions" in route.contractDetails.auth ? route.contractDetails.auth.allowedPermissions?.values() || [] : []
3105
- )
3106
- }
3107
- ];
3108
- securitySchemes["basic"] = {
3109
- type: "http",
3110
- scheme: "basic"
3111
- };
3112
- } else if (route.contractDetails.auth) {
3113
- operationObject.security = [
3114
- {
3115
- [route.contractDetails.auth.headerName !== "Authorization" ? "bearer" : "apiKey"]: Array.from(
3116
- "allowedPermissions" in route.contractDetails.auth ? route.contractDetails.auth.allowedPermissions?.values() || [] : []
3117
- )
3118
- }
3119
- ];
3120
- if (route.contractDetails.auth.headerName && route.contractDetails.auth.headerName !== "Authorization") {
3121
- securitySchemes[route.contractDetails.auth.headerName] = {
3122
- type: "apiKey",
3123
- in: "header",
3124
- name: route.contractDetails.auth.headerName
3125
- };
3126
- } else {
3127
- securitySchemes["Authorization"] = {
3128
- type: "http",
3129
- scheme: "bearer",
3130
- bearerFormat: "JWT"
3131
- };
3132
- }
3476
+ if (!versionedSecuritySchemes[OPENAPI_DEFAULT_VERSION]) {
3477
+ versionedSecuritySchemes[OPENAPI_DEFAULT_VERSION] = {};
3478
+ }
3479
+ const { query, requestHeaders, body, responses, responseHeaders } = route.contractDetails;
3480
+ const operationObject = generateOperationObject(
3481
+ schemaValidator,
3482
+ route.path,
3483
+ route.method,
3484
+ controllerName,
3485
+ router.sdkPaths,
3486
+ versionedSecuritySchemes[OPENAPI_DEFAULT_VERSION],
3487
+ name,
3488
+ summary,
3489
+ responses,
3490
+ params,
3491
+ responseHeaders,
3492
+ requestHeaders,
3493
+ query,
3494
+ body,
3495
+ auth
3496
+ );
3497
+ if (route.method !== "middleware") {
3498
+ versionedPaths[OPENAPI_DEFAULT_VERSION][openApiPath][route.method] = operationObject;
3133
3499
  }
3134
- }
3135
- if (route.method !== "middleware") {
3136
- paths[openApiPath][route.method] = operationObject;
3137
3500
  }
3138
3501
  });
3139
3502
  });
@@ -3141,13 +3504,116 @@ function generateSwaggerDocument(schemaValidator, protocol, host, port, routers,
3141
3504
  protocol,
3142
3505
  host,
3143
3506
  port,
3144
- tags,
3145
- paths,
3146
- securitySchemes,
3507
+ versionedTags,
3508
+ versionedPaths,
3509
+ versionedSecuritySchemes,
3147
3510
  otherServers
3148
3511
  );
3149
3512
  }
3150
3513
 
3514
+ // src/http/sdk/sdkClient.ts
3515
+ import { hashString, safeStringify as safeStringify4, toRecord as toRecord2 } from "@forklaunch/common";
3516
+
3517
+ // src/http/guards/isSdkRouter.ts
3518
+ function isSdkRouter(value) {
3519
+ return typeof value === "object" && value !== null && "sdk" in value && "_fetchMap" in value && "sdkPaths" in value;
3520
+ }
3521
+
3522
+ // src/http/sdk/sdkClient.ts
3523
+ function mapToSdk(schemaValidator, routerMap, runningPath = void 0) {
3524
+ const routerUniquenessCache = /* @__PURE__ */ new Set();
3525
+ return Object.fromEntries(
3526
+ Object.entries(routerMap).map(([key, value]) => {
3527
+ if (routerUniquenessCache.has(hashString(safeStringify4(value)))) {
3528
+ throw new Error(
3529
+ `SdkClient: Cannot use the same router pointer twice. Please clone the duplicate router with .clone() or only use the router once.`
3530
+ );
3531
+ }
3532
+ routerUniquenessCache.add(hashString(safeStringify4(value)));
3533
+ const currentPath = runningPath ? [runningPath, key].join(".") : key;
3534
+ if (isSdkRouter(value)) {
3535
+ Object.entries(value.sdkPaths).forEach(([routePath, sdkKey]) => {
3536
+ if ("controllerSdkPaths" in value && Array.isArray(value.controllerSdkPaths) && value.controllerSdkPaths.includes(routePath)) {
3537
+ value.sdkPaths[routePath] = [currentPath, sdkKey].join(".");
3538
+ }
3539
+ });
3540
+ return [key, value.sdk];
3541
+ } else {
3542
+ return [
3543
+ key,
3544
+ mapToSdk(
3545
+ schemaValidator,
3546
+ value,
3547
+ runningPath ? [runningPath, key].join(".") : key
3548
+ )
3549
+ ];
3550
+ }
3551
+ })
3552
+ );
3553
+ }
3554
+ function flattenFetchMap(schemaValidator, routerMap) {
3555
+ const _fetchMap = Object.entries(routerMap).reduce(
3556
+ (acc, [, value]) => {
3557
+ if ("_fetchMap" in value) {
3558
+ return {
3559
+ ...acc,
3560
+ ...value._fetchMap
3561
+ };
3562
+ } else {
3563
+ return {
3564
+ ...acc,
3565
+ ...flattenFetchMap(schemaValidator, value)
3566
+ };
3567
+ }
3568
+ },
3569
+ {}
3570
+ );
3571
+ return _fetchMap;
3572
+ }
3573
+ function mapToFetch(schemaValidator, routerMap) {
3574
+ const flattenedFetchMap = flattenFetchMap(
3575
+ schemaValidator,
3576
+ routerMap
3577
+ );
3578
+ return (path, ...reqInit) => {
3579
+ const method = reqInit[0]?.method;
3580
+ const version = reqInit[0] != null && "version" in reqInit[0] ? reqInit[0].version : void 0;
3581
+ return (version ? toRecord2(toRecord2(flattenedFetchMap[path])[method ?? "GET"])[version] : toRecord2(flattenedFetchMap[path])[method ?? "GET"])(path, reqInit[0]);
3582
+ };
3583
+ }
3584
+ function sdkClient(schemaValidator, routerMap) {
3585
+ return {
3586
+ _finalizedSdk: true,
3587
+ sdk: mapToSdk(schemaValidator, routerMap),
3588
+ fetch: mapToFetch(schemaValidator, routerMap)
3589
+ };
3590
+ }
3591
+
3592
+ // src/http/sdk/sdkRouter.ts
3593
+ import { toPrettyCamelCase as toPrettyCamelCase2 } from "@forklaunch/common";
3594
+ function sdkRouter(schemaValidator, controller, router) {
3595
+ const controllerSdkPaths = [];
3596
+ const mappedSdk = Object.fromEntries(
3597
+ Object.entries(controller).map(([key, value]) => {
3598
+ const sdkPath = [value._method, value._path].join(".");
3599
+ controllerSdkPaths.push(sdkPath);
3600
+ router.sdkPaths[sdkPath] = key;
3601
+ return [
3602
+ key,
3603
+ router.sdk[toPrettyCamelCase2(value.contractDetails.name)]
3604
+ ];
3605
+ })
3606
+ );
3607
+ const _fetchMap = router._fetchMap;
3608
+ return {
3609
+ sdk: mappedSdk,
3610
+ fetch: router.fetch,
3611
+ _fetchMap,
3612
+ sdkPaths: router.sdkPaths,
3613
+ controllerSdkPaths
3614
+ };
3615
+ }
3616
+
3151
3617
  // src/http/telemetry/evaluateTelemetryOptions.ts
3152
3618
  function evaluateTelemetryOptions(telemetryOptions) {
3153
3619
  return {
@@ -3178,6 +3644,7 @@ export {
3178
3644
  ForklaunchExpressLikeApplication,
3179
3645
  ForklaunchExpressLikeRouter,
3180
3646
  HTTPStatuses,
3647
+ OPENAPI_DEFAULT_VERSION,
3181
3648
  OpenTelemetryCollector,
3182
3649
  delete_,
3183
3650
  discriminateBody,
@@ -3185,7 +3652,7 @@ export {
3185
3652
  enrichExpressLikeSend,
3186
3653
  evaluateTelemetryOptions,
3187
3654
  generateMcpServer,
3188
- generateSwaggerDocument,
3655
+ generateOpenApiSpecs,
3189
3656
  get,
3190
3657
  getCodeForStatus,
3191
3658
  head,
@@ -3208,6 +3675,8 @@ export {
3208
3675
  post,
3209
3676
  put,
3210
3677
  recordMetric,
3678
+ sdkClient,
3679
+ sdkRouter,
3211
3680
  trace3 as trace,
3212
3681
  typedAuthHandler,
3213
3682
  typedHandler