@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.
@@ -210,6 +210,7 @@ async function parseRequestAuth(req, res, next) {
210
210
  req
211
211
  ) ?? [];
212
212
  if (error != null) {
213
+ res.type("text/plain");
213
214
  res.status(error).send(message);
214
215
  next?.(new Error(message));
215
216
  }
@@ -484,7 +485,7 @@ function enrichDetails(path, contractDetails, requestSchema, responseSchemas, op
484
485
  [ATTR_API_NAME]: req.contractDetails?.name || "unknown",
485
486
  [ATTR_HTTP_REQUEST_METHOD]: req.method,
486
487
  [ATTR_HTTP_ROUTE]: req.originalPath || "unknown",
487
- [ATTR_HTTP_RESPONSE_STATUS_CODE]: res.statusCode
488
+ [ATTR_HTTP_RESPONSE_STATUS_CODE]: Number(res.statusCode)
488
489
  });
489
490
  });
490
491
  next?.();
@@ -501,9 +502,9 @@ function hasSend(res) {
501
502
  return typeof res === "object" && res !== null && "send" in res;
502
503
  }
503
504
 
504
- // src/http/guards/isResponseShape.ts
505
- function isResponseShape(maybeResponseShape) {
506
- return maybeResponseShape != null && "body" in maybeResponseShape && "query" in maybeResponseShape && "params" in maybeResponseShape && "headers" in maybeResponseShape;
505
+ // src/http/guards/isRequestShape.ts
506
+ function isRequestShape(maybeResponseShape) {
507
+ return maybeResponseShape != null && ("body" in maybeResponseShape || "query" in maybeResponseShape || "params" in maybeResponseShape || "headers" in maybeResponseShape);
507
508
  }
508
509
 
509
510
  // src/http/middleware/request/parse.middleware.ts
@@ -518,7 +519,7 @@ function parse(req, res, next) {
518
519
  req.requestSchema,
519
520
  request
520
521
  );
521
- if (parsedRequest.ok && isResponseShape(parsedRequest.value)) {
522
+ if (parsedRequest.ok && isRequestShape(parsedRequest.value)) {
522
523
  req.body = parsedRequest.value.body;
523
524
  req.params = parsedRequest.value.params;
524
525
  Object.defineProperty(req, "query", {
@@ -527,7 +528,7 @@ function parse(req, res, next) {
527
528
  enumerable: true,
528
529
  configurable: false
529
530
  });
530
- req.headers = parsedRequest.value.headers;
531
+ req.headers = parsedRequest.value.headers ?? {};
531
532
  }
532
533
  if (!parsedRequest.ok) {
533
534
  switch (req.contractDetails.options?.requestValidation) {
@@ -560,11 +561,132 @@ Correlation id: ${req.context.correlationId ?? "No correlation ID"}`
560
561
  next?.();
561
562
  }
562
563
 
564
+ // src/http/router/discriminateBody.ts
565
+ function discriminateBody(schemaValidator, body) {
566
+ if (body == null) {
567
+ return void 0;
568
+ }
569
+ const maybeTypedBody = body;
570
+ if ("text" in maybeTypedBody && maybeTypedBody.text != null) {
571
+ return {
572
+ contentType: maybeTypedBody.contentType ?? "text/plain",
573
+ parserType: "text",
574
+ schema: maybeTypedBody.text
575
+ };
576
+ } else if ("json" in maybeTypedBody && maybeTypedBody.json != null) {
577
+ return {
578
+ contentType: maybeTypedBody.contentType ?? "application/json",
579
+ parserType: "json",
580
+ schema: maybeTypedBody.json
581
+ };
582
+ } else if ("file" in maybeTypedBody && maybeTypedBody.file != null) {
583
+ return {
584
+ contentType: maybeTypedBody.contentType ?? "application/octet-stream",
585
+ parserType: "file",
586
+ schema: maybeTypedBody.file
587
+ };
588
+ } else if ("multipartForm" in maybeTypedBody && maybeTypedBody.multipartForm != null) {
589
+ return {
590
+ contentType: maybeTypedBody.contentType ?? "multipart/form-data",
591
+ parserType: "multipart",
592
+ schema: maybeTypedBody.multipartForm
593
+ };
594
+ } else if ("urlEncodedForm" in maybeTypedBody && maybeTypedBody.urlEncodedForm != null) {
595
+ return {
596
+ contentType: maybeTypedBody.contentType ?? "application/x-www-form-urlencoded",
597
+ parserType: "urlEncoded",
598
+ schema: maybeTypedBody.urlEncodedForm
599
+ };
600
+ } else if ("schema" in maybeTypedBody && maybeTypedBody.schema != null) {
601
+ return {
602
+ contentType: maybeTypedBody.contentType ?? "application/json",
603
+ parserType: "text",
604
+ schema: maybeTypedBody.schema
605
+ };
606
+ } else if (schemaValidator.isInstanceOf(
607
+ maybeTypedBody,
608
+ schemaValidator.string
609
+ )) {
610
+ return {
611
+ contentType: "text/plain",
612
+ parserType: "text",
613
+ schema: maybeTypedBody
614
+ };
615
+ } else {
616
+ return {
617
+ contentType: "application/json",
618
+ parserType: "json",
619
+ schema: maybeTypedBody
620
+ };
621
+ }
622
+ }
623
+ function discriminateResponseBodies(schemaValidator, responses) {
624
+ const discriminatedResponses = {};
625
+ for (const [statusCode, response] of Object.entries(responses)) {
626
+ if (response != null && typeof response === "object") {
627
+ if ("json" in response && response.json != null) {
628
+ discriminatedResponses[Number(statusCode)] = {
629
+ contentType: ("contentType" in response && typeof response.contentType === "string" ? response.contentType : "application/json") ?? "application/json",
630
+ parserType: "json",
631
+ schema: response.json
632
+ };
633
+ } else if ("schema" in response && response.schema != null) {
634
+ discriminatedResponses[Number(statusCode)] = {
635
+ contentType: ("contentType" in response && typeof response.contentType === "string" ? response.contentType : "application/json") ?? "application/json",
636
+ parserType: "text",
637
+ schema: response.schema
638
+ };
639
+ } else if ("text" in response && response.text != null) {
640
+ discriminatedResponses[Number(statusCode)] = {
641
+ contentType: ("contentType" in response && typeof response.contentType === "string" ? response.contentType : "text/plain") ?? "text/plain",
642
+ parserType: "text",
643
+ schema: response.text
644
+ };
645
+ } else if ("file" in response && response.file != null) {
646
+ discriminatedResponses[Number(statusCode)] = {
647
+ contentType: ("contentType" in response && typeof response.contentType === "string" ? response.contentType : "application/octet-stream") ?? "application/octet-stream",
648
+ parserType: "file",
649
+ schema: response.file
650
+ };
651
+ } else if ("event" in response && response.event != null) {
652
+ discriminatedResponses[Number(statusCode)] = {
653
+ contentType: ("contentType" in response && typeof response.contentType === "string" ? response.contentType : "text/event-stream") ?? "text/event-stream",
654
+ parserType: "serverSentEvent",
655
+ schema: response.event
656
+ };
657
+ } else if (schemaValidator.isInstanceOf(
658
+ response,
659
+ schemaValidator.string
660
+ )) {
661
+ discriminatedResponses[Number(statusCode)] = {
662
+ contentType: "text/plain",
663
+ parserType: "text",
664
+ schema: response
665
+ };
666
+ } else {
667
+ discriminatedResponses[Number(statusCode)] = {
668
+ contentType: "application/json",
669
+ parserType: "json",
670
+ schema: response
671
+ };
672
+ }
673
+ } else {
674
+ discriminatedResponses[Number(statusCode)] = {
675
+ contentType: "application/json",
676
+ parserType: "json",
677
+ schema: response
678
+ };
679
+ }
680
+ }
681
+ return discriminatedResponses;
682
+ }
683
+
563
684
  // src/http/router/expressLikeRouter.ts
564
685
  var ForklaunchExpressLikeRouter = class {
565
- constructor(basePath, schemaValidator, internal, openTelemetryCollector) {
686
+ constructor(basePath, schemaValidator, internal, postEnrichMiddleware, openTelemetryCollector) {
566
687
  this.schemaValidator = schemaValidator;
567
688
  this.internal = internal;
689
+ this.postEnrichMiddleware = postEnrichMiddleware;
568
690
  this.openTelemetryCollector = openTelemetryCollector;
569
691
  this.basePath = basePath;
570
692
  }
@@ -587,6 +709,7 @@ var ForklaunchExpressLikeRouter = class {
587
709
  responseSchemas,
588
710
  this.openTelemetryCollector
589
711
  ),
712
+ ...this.postEnrichMiddleware,
590
713
  parse,
591
714
  parseRequestAuth
592
715
  ];
@@ -642,12 +765,16 @@ var ForklaunchExpressLikeRouter = class {
642
765
  }
643
766
  #compile(contractDetails) {
644
767
  const schemaValidator = this.schemaValidator;
768
+ let body = null;
769
+ if (isHttpContractDetails(contractDetails)) {
770
+ body = discriminateBody(this.schemaValidator, contractDetails.body);
771
+ }
645
772
  const requestSchema = schemaValidator.compile(
646
773
  schemaValidator.schemify({
647
774
  ...contractDetails.params ? { params: contractDetails.params } : {},
648
775
  ...contractDetails.requestHeaders ? { headers: contractDetails.requestHeaders } : {},
649
776
  ...contractDetails.query ? { query: contractDetails.query } : {},
650
- ...isHttpContractDetails(contractDetails) && contractDetails.body != null ? { body: contractDetails.body } : {}
777
+ ...body != null ? { body: body.schema } : {}
651
778
  })
652
779
  );
653
780
  const responseEntries = {
@@ -656,9 +783,16 @@ var ForklaunchExpressLikeRouter = class {
656
783
  403: schemaValidator.string,
657
784
  404: schemaValidator.string,
658
785
  500: schemaValidator.string,
659
- ...isPathParamHttpContractDetails(contractDetails) || isHttpContractDetails(contractDetails) ? {
660
- ...contractDetails.responses
661
- } : {}
786
+ ...isPathParamHttpContractDetails(contractDetails) || isHttpContractDetails(contractDetails) ? Object.fromEntries(
787
+ Object.entries(
788
+ discriminateResponseBodies(
789
+ this.schemaValidator,
790
+ contractDetails.responses
791
+ )
792
+ ).map(([key, value]) => {
793
+ return [key, value.schema];
794
+ })
795
+ ) : {}
662
796
  };
663
797
  const responseSchemas = {
664
798
  responses: {},
@@ -694,7 +828,7 @@ var ForklaunchExpressLikeRouter = class {
694
828
  params: request?.params ?? {},
695
829
  query: request?.query ?? {},
696
830
  headers: request?.headers ?? {},
697
- body: request?.body ?? {},
831
+ body: discriminateBody(this.schemaValidator, request?.body)?.schema ?? {},
698
832
  path: route
699
833
  };
700
834
  const res = {
@@ -713,6 +847,9 @@ var ForklaunchExpressLikeRouter = class {
713
847
  },
714
848
  setHeader: (key, value) => {
715
849
  responseHeaders[key] = value;
850
+ },
851
+ sseEmiter: (generator) => {
852
+ responseMessage = generator;
716
853
  }
717
854
  };
718
855
  let cursor = handlers.shift();
@@ -738,7 +875,7 @@ var ForklaunchExpressLikeRouter = class {
738
875
  }
739
876
  });
740
877
  return {
741
- code: statusCode,
878
+ code: Number(statusCode),
742
879
  response: responseMessage,
743
880
  headers: responseHeaders
744
881
  };
@@ -1146,10 +1283,17 @@ var ForklaunchExpressLikeApplication = class extends ForklaunchExpressLikeRouter
1146
1283
  *
1147
1284
  * @param {SV} schemaValidator - The schema validator.
1148
1285
  */
1149
- constructor(schemaValidator, internal, openTelemetryCollector) {
1150
- super("/", schemaValidator, internal, openTelemetryCollector);
1286
+ constructor(schemaValidator, internal, postEnrichMiddleware, openTelemetryCollector) {
1287
+ super(
1288
+ "/",
1289
+ schemaValidator,
1290
+ internal,
1291
+ postEnrichMiddleware,
1292
+ openTelemetryCollector
1293
+ );
1151
1294
  this.schemaValidator = schemaValidator;
1152
1295
  this.internal = internal;
1296
+ this.postEnrichMiddleware = postEnrichMiddleware;
1153
1297
  this.openTelemetryCollector = openTelemetryCollector;
1154
1298
  this.internal.use(createContext(this.schemaValidator));
1155
1299
  this.internal.use(cors);
@@ -2222,8 +2366,9 @@ import {
2222
2366
  } from "@forklaunch/validator";
2223
2367
  function parse2(req, res, next) {
2224
2368
  const { headers, responses } = res.responseSchemas;
2369
+ const statusCode = Number(res.statusCode);
2225
2370
  const parsedResponse = req.schemaValidator.parse(
2226
- responses?.[res.statusCode],
2371
+ responses?.[statusCode],
2227
2372
  res.bodyData
2228
2373
  );
2229
2374
  const parsedHeaders = req.schemaValidator.parse(
@@ -2251,7 +2396,7 @@ function parse2(req, res, next) {
2251
2396
  default:
2252
2397
  case "error":
2253
2398
  res.type("text/plain");
2254
- res.status(400);
2399
+ res.status(500);
2255
2400
  if (hasSend(res)) {
2256
2401
  res.send(
2257
2402
  `Invalid response:
@@ -2278,6 +2423,17 @@ ${parseErrors.join("\n\n")}`
2278
2423
  next?.();
2279
2424
  }
2280
2425
 
2426
+ // src/http/middleware/response/enrichExpressLikeSend.middleware.ts
2427
+ import {
2428
+ isAsyncGenerator,
2429
+ isNever as isNever3,
2430
+ isNodeJsWriteableStream,
2431
+ isRecord,
2432
+ readableStreamToAsyncIterable,
2433
+ safeStringify as safeStringify2
2434
+ } from "@forklaunch/common";
2435
+ import { Readable, Transform } from "stream";
2436
+
2281
2437
  // src/http/telemetry/recordMetric.ts
2282
2438
  import {
2283
2439
  ATTR_HTTP_REQUEST_METHOD as ATTR_HTTP_REQUEST_METHOD3,
@@ -2303,21 +2459,121 @@ function recordMetric(req, res) {
2303
2459
  [ATTR_CORRELATION_ID]: req.context.correlationId,
2304
2460
  [ATTR_HTTP_REQUEST_METHOD3]: req.method,
2305
2461
  [ATTR_HTTP_ROUTE3]: req.originalPath,
2306
- [ATTR_HTTP_RESPONSE_STATUS_CODE3]: res.statusCode || 0
2462
+ [ATTR_HTTP_RESPONSE_STATUS_CODE3]: Number(res.statusCode) || 0
2307
2463
  });
2308
2464
  res.metricRecorded = true;
2309
2465
  }
2310
2466
 
2311
2467
  // src/http/middleware/response/enrichExpressLikeSend.middleware.ts
2312
- function enrichExpressLikeSend(instance, req, res, originalSend, data, shouldEnrich) {
2313
- let parseErrorSent;
2314
- if (shouldEnrich) {
2315
- recordMetric(req, res);
2316
- if (res.statusCode === 404) {
2468
+ function enrichExpressLikeSend(instance, req, res, originalOperation, originalSend, data, shouldEnrich) {
2469
+ let errorSent = false;
2470
+ if (data == null) {
2471
+ originalSend.call(instance, data);
2472
+ return;
2473
+ }
2474
+ if (res.statusCode === 404) {
2475
+ res.type("text/plain");
2476
+ res.status(404);
2477
+ logger("error").error("Not Found");
2478
+ originalSend.call(instance, "Not Found");
2479
+ errorSent = true;
2480
+ }
2481
+ const responseBodies = discriminateResponseBodies(
2482
+ req.schemaValidator,
2483
+ req.contractDetails.responses
2484
+ );
2485
+ if (responseBodies != null && responseBodies[Number(res.statusCode)] != null) {
2486
+ res.type(responseBodies[Number(res.statusCode)].contentType);
2487
+ }
2488
+ if (data instanceof File || data instanceof Blob) {
2489
+ if (data instanceof File) {
2490
+ res.setHeader(
2491
+ "Content-Disposition",
2492
+ `attachment; filename="${data.name}"`
2493
+ );
2494
+ }
2495
+ if (isNodeJsWriteableStream(res)) {
2496
+ Readable.from(readableStreamToAsyncIterable(data.stream())).pipe(
2497
+ res
2498
+ );
2499
+ } else {
2500
+ res.type("text/plain");
2501
+ res.status(500);
2502
+ originalSend.call(instance, "Not a NodeJS WritableStream");
2503
+ errorSent = true;
2504
+ }
2505
+ } else if (isAsyncGenerator(data)) {
2506
+ let firstPass = true;
2507
+ const transformer = new Transform({
2508
+ objectMode: true,
2509
+ transform(chunk, _encoding, callback) {
2510
+ if (firstPass) {
2511
+ res.bodyData = chunk;
2512
+ parse2(req, res, (err) => {
2513
+ if (err) {
2514
+ let errorString = err.message;
2515
+ if (res.locals.errorMessage) {
2516
+ errorString += `
2517
+ ------------------
2518
+ ${res.locals.errorMessage}`;
2519
+ }
2520
+ logger("error").error(errorString);
2521
+ res.type("text/plain");
2522
+ res.status(500);
2523
+ originalSend.call(instance, errorString);
2524
+ errorSent = true;
2525
+ callback(new Error(errorString));
2526
+ }
2527
+ });
2528
+ firstPass = false;
2529
+ }
2530
+ if (!errorSent) {
2531
+ let data2 = "";
2532
+ for (const [key, value] of Object.entries(chunk)) {
2533
+ data2 += `${key}: ${typeof value === "string" ? value : safeStringify2(value)}
2534
+ `;
2535
+ }
2536
+ data2 += "\n";
2537
+ callback(null, data2);
2538
+ }
2539
+ }
2540
+ });
2541
+ if (isNodeJsWriteableStream(res)) {
2542
+ Readable.from(data).pipe(transformer).pipe(res);
2543
+ } else {
2317
2544
  res.type("text/plain");
2318
- res.status(404);
2319
- logger("error").error("Not Found");
2320
- originalSend.call(instance, "Not Found");
2545
+ res.status(500);
2546
+ originalSend.call(instance, "Not a NodeJS WritableStream");
2547
+ errorSent = true;
2548
+ }
2549
+ } else {
2550
+ const parserType = responseBodies?.[Number(res.statusCode)]?.parserType;
2551
+ res.bodyData = data;
2552
+ if (isRecord(data)) {
2553
+ switch (parserType) {
2554
+ case "json":
2555
+ res.bodyData = "json" in data ? data.json : data;
2556
+ break;
2557
+ case "text":
2558
+ res.bodyData = "text" in data ? data.text : data;
2559
+ break;
2560
+ case "file":
2561
+ res.bodyData = "file" in data ? data.file : data;
2562
+ break;
2563
+ case "serverSentEvent":
2564
+ res.bodyData = "event" in data ? data.event : data;
2565
+ break;
2566
+ case "multipart":
2567
+ res.bodyData = "multipart" in data ? data.multipart : data;
2568
+ break;
2569
+ case void 0:
2570
+ res.bodyData = data;
2571
+ break;
2572
+ default:
2573
+ isNever3(parserType);
2574
+ res.bodyData = data;
2575
+ break;
2576
+ }
2321
2577
  }
2322
2578
  parse2(req, res, (err) => {
2323
2579
  if (err) {
@@ -2331,12 +2587,21 @@ ${res.locals.errorMessage}`;
2331
2587
  res.type("text/plain");
2332
2588
  res.status(500);
2333
2589
  originalSend.call(instance, errorString);
2334
- parseErrorSent = true;
2590
+ errorSent = true;
2335
2591
  }
2336
2592
  });
2593
+ if (!errorSent) {
2594
+ if (typeof data === "string") {
2595
+ res.type("text/plain");
2596
+ originalSend.call(instance, data);
2597
+ } else if (!(data instanceof File)) {
2598
+ res.sent = true;
2599
+ originalOperation.call(instance, data);
2600
+ }
2601
+ }
2337
2602
  }
2338
- if (!parseErrorSent) {
2339
- originalSend.call(instance, data);
2603
+ if (shouldEnrich) {
2604
+ recordMetric(req, res);
2340
2605
  }
2341
2606
  }
2342
2607
 
@@ -2375,10 +2640,14 @@ function generateOpenApiDocument(port, tags, paths) {
2375
2640
  paths
2376
2641
  };
2377
2642
  }
2378
- function contentResolver(schemaValidator, body) {
2643
+ function contentResolver(schemaValidator, body, contentType) {
2379
2644
  const bodySpec = schemaValidator.openapi(body);
2380
- return body === schemaValidator.string ? {
2381
- "plain/text": {
2645
+ return contentType != null ? {
2646
+ [contentType]: {
2647
+ schema: bodySpec
2648
+ }
2649
+ } : body === schemaValidator.string ? {
2650
+ "text/plain": {
2382
2651
  schema: bodySpec
2383
2652
  }
2384
2653
  } : {
@@ -2403,15 +2672,29 @@ function generateSwaggerDocument(schemaValidator, port, routers) {
2403
2672
  }
2404
2673
  const { name, summary, query, requestHeaders } = route.contractDetails;
2405
2674
  const responses = {};
2406
- for (const key in route.contractDetails.responses) {
2675
+ const discriminatedResponseBodiesResult = discriminateResponseBodies(
2676
+ schemaValidator,
2677
+ route.contractDetails.responses
2678
+ );
2679
+ for (const key in discriminatedResponseBodiesResult) {
2407
2680
  responses[key] = {
2408
2681
  description: httpStatusCodes_default[key],
2409
2682
  content: contentResolver(
2410
2683
  schemaValidator,
2411
- route.contractDetails.responses[key]
2684
+ discriminatedResponseBodiesResult[key].schema,
2685
+ discriminatedResponseBodiesResult[key].contentType
2412
2686
  )
2413
2687
  };
2414
2688
  }
2689
+ const commonErrors = [400, 404, 500];
2690
+ for (const error of commonErrors) {
2691
+ if (!(error in responses)) {
2692
+ responses[error] = {
2693
+ description: httpStatusCodes_default[error],
2694
+ content: contentResolver(schemaValidator, schemaValidator.string)
2695
+ };
2696
+ }
2697
+ }
2415
2698
  const pathItemObject = {
2416
2699
  tags: [controllerName],
2417
2700
  summary: `${name}: ${summary}`,
@@ -2429,11 +2712,15 @@ function generateSwaggerDocument(schemaValidator, port, routers) {
2429
2712
  });
2430
2713
  }
2431
2714
  }
2432
- const body = route.contractDetails.body;
2433
- if (body) {
2715
+ const discriminatedBodyResult = "body" in route.contractDetails ? discriminateBody(schemaValidator, route.contractDetails.body) : null;
2716
+ if (discriminatedBodyResult) {
2434
2717
  pathItemObject.requestBody = {
2435
2718
  required: true,
2436
- content: contentResolver(schemaValidator, body)
2719
+ content: contentResolver(
2720
+ schemaValidator,
2721
+ discriminatedBodyResult.schema,
2722
+ discriminatedBodyResult.contentType
2723
+ )
2437
2724
  };
2438
2725
  }
2439
2726
  if (requestHeaders) {
@@ -2515,6 +2802,8 @@ export {
2515
2802
  HTTPStatuses,
2516
2803
  OpenTelemetryCollector,
2517
2804
  delete_,
2805
+ discriminateBody,
2806
+ discriminateResponseBodies,
2518
2807
  enrichExpressLikeSend,
2519
2808
  evaluateTelemetryOptions,
2520
2809
  generateSwaggerDocument,