@forklaunch/core 0.7.3 → 0.8.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.
@@ -41,6 +41,8 @@ __export(http_exports, {
41
41
  HTTPStatuses: () => HTTPStatuses,
42
42
  OpenTelemetryCollector: () => OpenTelemetryCollector,
43
43
  delete_: () => delete_,
44
+ discriminateBody: () => discriminateBody,
45
+ discriminateResponseBodies: () => discriminateResponseBodies,
44
46
  enrichExpressLikeSend: () => enrichExpressLikeSend,
45
47
  evaluateTelemetryOptions: () => evaluateTelemetryOptions,
46
48
  generateSwaggerDocument: () => generateSwaggerDocument,
@@ -278,6 +280,7 @@ async function parseRequestAuth(req, res, next) {
278
280
  req
279
281
  ) ?? [];
280
282
  if (error != null) {
283
+ res.type("text/plain");
281
284
  res.status(error).send(message);
282
285
  next?.(new Error(message));
283
286
  }
@@ -548,7 +551,7 @@ function enrichDetails(path, contractDetails, requestSchema, responseSchemas, op
548
551
  [ATTR_API_NAME]: req.contractDetails?.name || "unknown",
549
552
  [import_semantic_conventions.ATTR_HTTP_REQUEST_METHOD]: req.method,
550
553
  [import_semantic_conventions.ATTR_HTTP_ROUTE]: req.originalPath || "unknown",
551
- [import_semantic_conventions.ATTR_HTTP_RESPONSE_STATUS_CODE]: res.statusCode
554
+ [import_semantic_conventions.ATTR_HTTP_RESPONSE_STATUS_CODE]: Number(res.statusCode)
552
555
  });
553
556
  });
554
557
  next?.();
@@ -563,9 +566,9 @@ function hasSend(res) {
563
566
  return typeof res === "object" && res !== null && "send" in res;
564
567
  }
565
568
 
566
- // src/http/guards/isResponseShape.ts
567
- function isResponseShape(maybeResponseShape) {
568
- return maybeResponseShape != null && "body" in maybeResponseShape && "query" in maybeResponseShape && "params" in maybeResponseShape && "headers" in maybeResponseShape;
569
+ // src/http/guards/isRequestShape.ts
570
+ function isRequestShape(maybeResponseShape) {
571
+ return maybeResponseShape != null && ("body" in maybeResponseShape || "query" in maybeResponseShape || "params" in maybeResponseShape || "headers" in maybeResponseShape);
569
572
  }
570
573
 
571
574
  // src/http/middleware/request/parse.middleware.ts
@@ -580,7 +583,7 @@ function parse(req, res, next) {
580
583
  req.requestSchema,
581
584
  request
582
585
  );
583
- if (parsedRequest.ok && isResponseShape(parsedRequest.value)) {
586
+ if (parsedRequest.ok && isRequestShape(parsedRequest.value)) {
584
587
  req.body = parsedRequest.value.body;
585
588
  req.params = parsedRequest.value.params;
586
589
  Object.defineProperty(req, "query", {
@@ -589,7 +592,7 @@ function parse(req, res, next) {
589
592
  enumerable: true,
590
593
  configurable: false
591
594
  });
592
- req.headers = parsedRequest.value.headers;
595
+ req.headers = parsedRequest.value.headers ?? {};
593
596
  }
594
597
  if (!parsedRequest.ok) {
595
598
  switch (req.contractDetails.options?.requestValidation) {
@@ -622,11 +625,132 @@ Correlation id: ${req.context.correlationId ?? "No correlation ID"}`
622
625
  next?.();
623
626
  }
624
627
 
628
+ // src/http/router/discriminateBody.ts
629
+ function discriminateBody(schemaValidator, body) {
630
+ if (body == null) {
631
+ return void 0;
632
+ }
633
+ const maybeTypedBody = body;
634
+ if ("text" in maybeTypedBody && maybeTypedBody.text != null) {
635
+ return {
636
+ contentType: maybeTypedBody.contentType ?? "text/plain",
637
+ parserType: "text",
638
+ schema: maybeTypedBody.text
639
+ };
640
+ } else if ("json" in maybeTypedBody && maybeTypedBody.json != null) {
641
+ return {
642
+ contentType: maybeTypedBody.contentType ?? "application/json",
643
+ parserType: "json",
644
+ schema: maybeTypedBody.json
645
+ };
646
+ } else if ("file" in maybeTypedBody && maybeTypedBody.file != null) {
647
+ return {
648
+ contentType: maybeTypedBody.contentType ?? "application/octet-stream",
649
+ parserType: "file",
650
+ schema: maybeTypedBody.file
651
+ };
652
+ } else if ("multipartForm" in maybeTypedBody && maybeTypedBody.multipartForm != null) {
653
+ return {
654
+ contentType: maybeTypedBody.contentType ?? "multipart/form-data",
655
+ parserType: "multipart",
656
+ schema: maybeTypedBody.multipartForm
657
+ };
658
+ } else if ("urlEncodedForm" in maybeTypedBody && maybeTypedBody.urlEncodedForm != null) {
659
+ return {
660
+ contentType: maybeTypedBody.contentType ?? "application/x-www-form-urlencoded",
661
+ parserType: "urlEncoded",
662
+ schema: maybeTypedBody.urlEncodedForm
663
+ };
664
+ } else if ("schema" in maybeTypedBody && maybeTypedBody.schema != null) {
665
+ return {
666
+ contentType: maybeTypedBody.contentType ?? "application/json",
667
+ parserType: "text",
668
+ schema: maybeTypedBody.schema
669
+ };
670
+ } else if (schemaValidator.isInstanceOf(
671
+ maybeTypedBody,
672
+ schemaValidator.string
673
+ )) {
674
+ return {
675
+ contentType: "text/plain",
676
+ parserType: "text",
677
+ schema: maybeTypedBody
678
+ };
679
+ } else {
680
+ return {
681
+ contentType: "application/json",
682
+ parserType: "json",
683
+ schema: maybeTypedBody
684
+ };
685
+ }
686
+ }
687
+ function discriminateResponseBodies(schemaValidator, responses) {
688
+ const discriminatedResponses = {};
689
+ for (const [statusCode, response] of Object.entries(responses)) {
690
+ if (response != null && typeof response === "object") {
691
+ if ("json" in response && response.json != null) {
692
+ discriminatedResponses[Number(statusCode)] = {
693
+ contentType: ("contentType" in response && typeof response.contentType === "string" ? response.contentType : "application/json") ?? "application/json",
694
+ parserType: "json",
695
+ schema: response.json
696
+ };
697
+ } else if ("schema" in response && response.schema != null) {
698
+ discriminatedResponses[Number(statusCode)] = {
699
+ contentType: ("contentType" in response && typeof response.contentType === "string" ? response.contentType : "application/json") ?? "application/json",
700
+ parserType: "text",
701
+ schema: response.schema
702
+ };
703
+ } else if ("text" in response && response.text != null) {
704
+ discriminatedResponses[Number(statusCode)] = {
705
+ contentType: ("contentType" in response && typeof response.contentType === "string" ? response.contentType : "text/plain") ?? "text/plain",
706
+ parserType: "text",
707
+ schema: response.text
708
+ };
709
+ } else if ("file" in response && response.file != null) {
710
+ discriminatedResponses[Number(statusCode)] = {
711
+ contentType: ("contentType" in response && typeof response.contentType === "string" ? response.contentType : "application/octet-stream") ?? "application/octet-stream",
712
+ parserType: "file",
713
+ schema: response.file
714
+ };
715
+ } else if ("event" in response && response.event != null) {
716
+ discriminatedResponses[Number(statusCode)] = {
717
+ contentType: ("contentType" in response && typeof response.contentType === "string" ? response.contentType : "text/event-stream") ?? "text/event-stream",
718
+ parserType: "serverSentEvent",
719
+ schema: response.event
720
+ };
721
+ } else if (schemaValidator.isInstanceOf(
722
+ response,
723
+ schemaValidator.string
724
+ )) {
725
+ discriminatedResponses[Number(statusCode)] = {
726
+ contentType: "text/plain",
727
+ parserType: "text",
728
+ schema: response
729
+ };
730
+ } else {
731
+ discriminatedResponses[Number(statusCode)] = {
732
+ contentType: "application/json",
733
+ parserType: "json",
734
+ schema: response
735
+ };
736
+ }
737
+ } else {
738
+ discriminatedResponses[Number(statusCode)] = {
739
+ contentType: "application/json",
740
+ parserType: "json",
741
+ schema: response
742
+ };
743
+ }
744
+ }
745
+ return discriminatedResponses;
746
+ }
747
+
625
748
  // src/http/router/expressLikeRouter.ts
626
749
  var ForklaunchExpressLikeRouter = class {
627
- constructor(basePath, schemaValidator, internal, openTelemetryCollector) {
750
+ constructor(basePath, schemaValidator, internal, postEnrichMiddleware, openTelemetryCollector) {
628
751
  this.schemaValidator = schemaValidator;
629
752
  this.internal = internal;
753
+ this.postEnrichMiddleware = postEnrichMiddleware;
630
754
  this.openTelemetryCollector = openTelemetryCollector;
631
755
  this.basePath = basePath;
632
756
  }
@@ -649,6 +773,7 @@ var ForklaunchExpressLikeRouter = class {
649
773
  responseSchemas,
650
774
  this.openTelemetryCollector
651
775
  ),
776
+ ...this.postEnrichMiddleware,
652
777
  parse,
653
778
  parseRequestAuth
654
779
  ];
@@ -704,12 +829,16 @@ var ForklaunchExpressLikeRouter = class {
704
829
  }
705
830
  #compile(contractDetails) {
706
831
  const schemaValidator = this.schemaValidator;
832
+ let body = null;
833
+ if (isHttpContractDetails(contractDetails)) {
834
+ body = discriminateBody(this.schemaValidator, contractDetails.body);
835
+ }
707
836
  const requestSchema = schemaValidator.compile(
708
837
  schemaValidator.schemify({
709
838
  ...contractDetails.params ? { params: contractDetails.params } : {},
710
839
  ...contractDetails.requestHeaders ? { headers: contractDetails.requestHeaders } : {},
711
840
  ...contractDetails.query ? { query: contractDetails.query } : {},
712
- ...isHttpContractDetails(contractDetails) && contractDetails.body != null ? { body: contractDetails.body } : {}
841
+ ...body != null ? { body: body.schema } : {}
713
842
  })
714
843
  );
715
844
  const responseEntries = {
@@ -718,9 +847,16 @@ var ForklaunchExpressLikeRouter = class {
718
847
  403: schemaValidator.string,
719
848
  404: schemaValidator.string,
720
849
  500: schemaValidator.string,
721
- ...isPathParamHttpContractDetails(contractDetails) || isHttpContractDetails(contractDetails) ? {
722
- ...contractDetails.responses
723
- } : {}
850
+ ...isPathParamHttpContractDetails(contractDetails) || isHttpContractDetails(contractDetails) ? Object.fromEntries(
851
+ Object.entries(
852
+ discriminateResponseBodies(
853
+ this.schemaValidator,
854
+ contractDetails.responses
855
+ )
856
+ ).map(([key, value]) => {
857
+ return [key, value.schema];
858
+ })
859
+ ) : {}
724
860
  };
725
861
  const responseSchemas = {
726
862
  responses: {},
@@ -756,7 +892,7 @@ var ForklaunchExpressLikeRouter = class {
756
892
  params: request?.params ?? {},
757
893
  query: request?.query ?? {},
758
894
  headers: request?.headers ?? {},
759
- body: request?.body ?? {},
895
+ body: discriminateBody(this.schemaValidator, request?.body)?.schema ?? {},
760
896
  path: route
761
897
  };
762
898
  const res = {
@@ -775,6 +911,9 @@ var ForklaunchExpressLikeRouter = class {
775
911
  },
776
912
  setHeader: (key, value) => {
777
913
  responseHeaders[key] = value;
914
+ },
915
+ sseEmiter: (generator) => {
916
+ responseMessage = generator;
778
917
  }
779
918
  };
780
919
  let cursor = handlers.shift();
@@ -800,7 +939,7 @@ var ForklaunchExpressLikeRouter = class {
800
939
  }
801
940
  });
802
941
  return {
803
- code: statusCode,
942
+ code: Number(statusCode),
804
943
  response: responseMessage,
805
944
  headers: responseHeaders
806
945
  };
@@ -1208,10 +1347,17 @@ var ForklaunchExpressLikeApplication = class extends ForklaunchExpressLikeRouter
1208
1347
  *
1209
1348
  * @param {SV} schemaValidator - The schema validator.
1210
1349
  */
1211
- constructor(schemaValidator, internal, openTelemetryCollector) {
1212
- super("/", schemaValidator, internal, openTelemetryCollector);
1350
+ constructor(schemaValidator, internal, postEnrichMiddleware, openTelemetryCollector) {
1351
+ super(
1352
+ "/",
1353
+ schemaValidator,
1354
+ internal,
1355
+ postEnrichMiddleware,
1356
+ openTelemetryCollector
1357
+ );
1213
1358
  this.schemaValidator = schemaValidator;
1214
1359
  this.internal = internal;
1360
+ this.postEnrichMiddleware = postEnrichMiddleware;
1215
1361
  this.openTelemetryCollector = openTelemetryCollector;
1216
1362
  this.internal.use(createContext(this.schemaValidator));
1217
1363
  this.internal.use(cors);
@@ -2282,8 +2428,9 @@ var httpStatusCodes_default = HTTPStatuses;
2282
2428
  var import_validator2 = require("@forklaunch/validator");
2283
2429
  function parse2(req, res, next) {
2284
2430
  const { headers, responses } = res.responseSchemas;
2431
+ const statusCode = Number(res.statusCode);
2285
2432
  const parsedResponse = req.schemaValidator.parse(
2286
- responses?.[res.statusCode],
2433
+ responses?.[statusCode],
2287
2434
  res.bodyData
2288
2435
  );
2289
2436
  const parsedHeaders = req.schemaValidator.parse(
@@ -2311,7 +2458,7 @@ function parse2(req, res, next) {
2311
2458
  default:
2312
2459
  case "error":
2313
2460
  res.type("text/plain");
2314
- res.status(400);
2461
+ res.status(500);
2315
2462
  if (hasSend(res)) {
2316
2463
  res.send(
2317
2464
  `Invalid response:
@@ -2338,6 +2485,10 @@ ${parseErrors.join("\n\n")}`
2338
2485
  next?.();
2339
2486
  }
2340
2487
 
2488
+ // src/http/middleware/response/enrichExpressLikeSend.middleware.ts
2489
+ var import_common4 = require("@forklaunch/common");
2490
+ var import_stream = require("stream");
2491
+
2341
2492
  // src/http/telemetry/recordMetric.ts
2342
2493
  var import_semantic_conventions3 = require("@opentelemetry/semantic-conventions");
2343
2494
 
@@ -2356,21 +2507,121 @@ function recordMetric(req, res) {
2356
2507
  [ATTR_CORRELATION_ID]: req.context.correlationId,
2357
2508
  [import_semantic_conventions3.ATTR_HTTP_REQUEST_METHOD]: req.method,
2358
2509
  [import_semantic_conventions3.ATTR_HTTP_ROUTE]: req.originalPath,
2359
- [import_semantic_conventions3.ATTR_HTTP_RESPONSE_STATUS_CODE]: res.statusCode || 0
2510
+ [import_semantic_conventions3.ATTR_HTTP_RESPONSE_STATUS_CODE]: Number(res.statusCode) || 0
2360
2511
  });
2361
2512
  res.metricRecorded = true;
2362
2513
  }
2363
2514
 
2364
2515
  // src/http/middleware/response/enrichExpressLikeSend.middleware.ts
2365
- function enrichExpressLikeSend(instance, req, res, originalSend, data, shouldEnrich) {
2366
- let parseErrorSent;
2367
- if (shouldEnrich) {
2368
- recordMetric(req, res);
2369
- if (res.statusCode === 404) {
2516
+ function enrichExpressLikeSend(instance, req, res, originalOperation, originalSend, data, shouldEnrich) {
2517
+ let errorSent = false;
2518
+ if (data == null) {
2519
+ originalSend.call(instance, data);
2520
+ return;
2521
+ }
2522
+ if (res.statusCode === 404) {
2523
+ res.type("text/plain");
2524
+ res.status(404);
2525
+ logger("error").error("Not Found");
2526
+ originalSend.call(instance, "Not Found");
2527
+ errorSent = true;
2528
+ }
2529
+ const responseBodies = discriminateResponseBodies(
2530
+ req.schemaValidator,
2531
+ req.contractDetails.responses
2532
+ );
2533
+ if (responseBodies != null && responseBodies[Number(res.statusCode)] != null) {
2534
+ res.type(responseBodies[Number(res.statusCode)].contentType);
2535
+ }
2536
+ if (data instanceof File || data instanceof Blob) {
2537
+ if (data instanceof File) {
2538
+ res.setHeader(
2539
+ "Content-Disposition",
2540
+ `attachment; filename="${data.name}"`
2541
+ );
2542
+ }
2543
+ if ((0, import_common4.isNodeJsWriteableStream)(res)) {
2544
+ import_stream.Readable.from((0, import_common4.readableStreamToAsyncIterable)(data.stream())).pipe(
2545
+ res
2546
+ );
2547
+ } else {
2548
+ res.type("text/plain");
2549
+ res.status(500);
2550
+ originalSend.call(instance, "Not a NodeJS WritableStream");
2551
+ errorSent = true;
2552
+ }
2553
+ } else if ((0, import_common4.isAsyncGenerator)(data)) {
2554
+ let firstPass = true;
2555
+ const transformer = new import_stream.Transform({
2556
+ objectMode: true,
2557
+ transform(chunk, _encoding, callback) {
2558
+ if (firstPass) {
2559
+ res.bodyData = chunk;
2560
+ parse2(req, res, (err) => {
2561
+ if (err) {
2562
+ let errorString = err.message;
2563
+ if (res.locals.errorMessage) {
2564
+ errorString += `
2565
+ ------------------
2566
+ ${res.locals.errorMessage}`;
2567
+ }
2568
+ logger("error").error(errorString);
2569
+ res.type("text/plain");
2570
+ res.status(500);
2571
+ originalSend.call(instance, errorString);
2572
+ errorSent = true;
2573
+ callback(new Error(errorString));
2574
+ }
2575
+ });
2576
+ firstPass = false;
2577
+ }
2578
+ if (!errorSent) {
2579
+ let data2 = "";
2580
+ for (const [key, value] of Object.entries(chunk)) {
2581
+ data2 += `${key}: ${typeof value === "string" ? value : (0, import_common4.safeStringify)(value)}
2582
+ `;
2583
+ }
2584
+ data2 += "\n";
2585
+ callback(null, data2);
2586
+ }
2587
+ }
2588
+ });
2589
+ if ((0, import_common4.isNodeJsWriteableStream)(res)) {
2590
+ import_stream.Readable.from(data).pipe(transformer).pipe(res);
2591
+ } else {
2370
2592
  res.type("text/plain");
2371
- res.status(404);
2372
- logger("error").error("Not Found");
2373
- originalSend.call(instance, "Not Found");
2593
+ res.status(500);
2594
+ originalSend.call(instance, "Not a NodeJS WritableStream");
2595
+ errorSent = true;
2596
+ }
2597
+ } else {
2598
+ const parserType = responseBodies?.[Number(res.statusCode)]?.parserType;
2599
+ res.bodyData = data;
2600
+ if ((0, import_common4.isRecord)(data)) {
2601
+ switch (parserType) {
2602
+ case "json":
2603
+ res.bodyData = "json" in data ? data.json : data;
2604
+ break;
2605
+ case "text":
2606
+ res.bodyData = "text" in data ? data.text : data;
2607
+ break;
2608
+ case "file":
2609
+ res.bodyData = "file" in data ? data.file : data;
2610
+ break;
2611
+ case "serverSentEvent":
2612
+ res.bodyData = "event" in data ? data.event : data;
2613
+ break;
2614
+ case "multipart":
2615
+ res.bodyData = "multipart" in data ? data.multipart : data;
2616
+ break;
2617
+ case void 0:
2618
+ res.bodyData = data;
2619
+ break;
2620
+ default:
2621
+ (0, import_common4.isNever)(parserType);
2622
+ res.bodyData = data;
2623
+ break;
2624
+ }
2374
2625
  }
2375
2626
  parse2(req, res, (err) => {
2376
2627
  if (err) {
@@ -2384,12 +2635,21 @@ ${res.locals.errorMessage}`;
2384
2635
  res.type("text/plain");
2385
2636
  res.status(500);
2386
2637
  originalSend.call(instance, errorString);
2387
- parseErrorSent = true;
2638
+ errorSent = true;
2388
2639
  }
2389
2640
  });
2641
+ if (!errorSent) {
2642
+ if (typeof data === "string") {
2643
+ res.type("text/plain");
2644
+ originalSend.call(instance, data);
2645
+ } else if (!(data instanceof File)) {
2646
+ res.sent = true;
2647
+ originalOperation.call(instance, data);
2648
+ }
2649
+ }
2390
2650
  }
2391
- if (!parseErrorSent) {
2392
- originalSend.call(instance, data);
2651
+ if (shouldEnrich) {
2652
+ recordMetric(req, res);
2393
2653
  }
2394
2654
  }
2395
2655
 
@@ -2428,10 +2688,14 @@ function generateOpenApiDocument(port, tags, paths) {
2428
2688
  paths
2429
2689
  };
2430
2690
  }
2431
- function contentResolver(schemaValidator, body) {
2691
+ function contentResolver(schemaValidator, body, contentType) {
2432
2692
  const bodySpec = schemaValidator.openapi(body);
2433
- return body === schemaValidator.string ? {
2434
- "plain/text": {
2693
+ return contentType != null ? {
2694
+ [contentType]: {
2695
+ schema: bodySpec
2696
+ }
2697
+ } : body === schemaValidator.string ? {
2698
+ "text/plain": {
2435
2699
  schema: bodySpec
2436
2700
  }
2437
2701
  } : {
@@ -2456,15 +2720,29 @@ function generateSwaggerDocument(schemaValidator, port, routers) {
2456
2720
  }
2457
2721
  const { name, summary, query, requestHeaders } = route.contractDetails;
2458
2722
  const responses = {};
2459
- for (const key in route.contractDetails.responses) {
2723
+ const discriminatedResponseBodiesResult = discriminateResponseBodies(
2724
+ schemaValidator,
2725
+ route.contractDetails.responses
2726
+ );
2727
+ for (const key in discriminatedResponseBodiesResult) {
2460
2728
  responses[key] = {
2461
2729
  description: httpStatusCodes_default[key],
2462
2730
  content: contentResolver(
2463
2731
  schemaValidator,
2464
- route.contractDetails.responses[key]
2732
+ discriminatedResponseBodiesResult[key].schema,
2733
+ discriminatedResponseBodiesResult[key].contentType
2465
2734
  )
2466
2735
  };
2467
2736
  }
2737
+ const commonErrors = [400, 404, 500];
2738
+ for (const error of commonErrors) {
2739
+ if (!(error in responses)) {
2740
+ responses[error] = {
2741
+ description: httpStatusCodes_default[error],
2742
+ content: contentResolver(schemaValidator, schemaValidator.string)
2743
+ };
2744
+ }
2745
+ }
2468
2746
  const pathItemObject = {
2469
2747
  tags: [controllerName],
2470
2748
  summary: `${name}: ${summary}`,
@@ -2482,11 +2760,15 @@ function generateSwaggerDocument(schemaValidator, port, routers) {
2482
2760
  });
2483
2761
  }
2484
2762
  }
2485
- const body = route.contractDetails.body;
2486
- if (body) {
2763
+ const discriminatedBodyResult = "body" in route.contractDetails ? discriminateBody(schemaValidator, route.contractDetails.body) : null;
2764
+ if (discriminatedBodyResult) {
2487
2765
  pathItemObject.requestBody = {
2488
2766
  required: true,
2489
- content: contentResolver(schemaValidator, body)
2767
+ content: contentResolver(
2768
+ schemaValidator,
2769
+ discriminatedBodyResult.schema,
2770
+ discriminatedBodyResult.contentType
2771
+ )
2490
2772
  };
2491
2773
  }
2492
2774
  if (requestHeaders) {
@@ -2569,6 +2851,8 @@ function metricsDefinitions(metrics2) {
2569
2851
  HTTPStatuses,
2570
2852
  OpenTelemetryCollector,
2571
2853
  delete_,
2854
+ discriminateBody,
2855
+ discriminateResponseBodies,
2572
2856
  enrichExpressLikeSend,
2573
2857
  evaluateTelemetryOptions,
2574
2858
  generateSwaggerDocument,