@tetsujs/openapi 0.5.3 → 0.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/src/index.js CHANGED
@@ -373,8 +373,8 @@ function operationOf(entry, options, components, envelopes, ids, warn) {
373
373
  };
374
374
  }
375
375
  function declareEnvelopes(entry, envelopes) {
376
- for (const [status, schema] of declaredResponses(entry)) {
377
- const described = schema === null ? undefined : emitted(schema, "", () => {});
376
+ for (const [status, { body }] of declaredResponses(entry)) {
377
+ const described = body === null ? undefined : emitted(body, "", () => {});
378
378
  const code = envelopes.format.code;
379
379
  for (const branch of described ? branchesOf(described, code) : []) {
380
380
  const found = code(branch);
@@ -390,9 +390,62 @@ function declaredResponses(entry) {
390
390
  return [];
391
391
  }
392
392
  if ("~standard" in response) {
393
- return [["200", response]];
393
+ return [["200", { body: response }]];
394
394
  }
395
- return Object.entries(response);
395
+ return Object.entries(response).map(([status, value]) => [
396
+ status,
397
+ asDeclared(value)
398
+ ]);
399
+ }
400
+ function asDeclared(value) {
401
+ if (value === null || "~standard" in value) {
402
+ return { body: value };
403
+ }
404
+ return {
405
+ body: value.body ?? null,
406
+ ...value.headers === undefined ? {} : { headers: value.headers },
407
+ ...value.cookies === undefined ? {} : { cookies: value.cookies }
408
+ };
409
+ }
410
+ function declaredHeaders(declared, warn) {
411
+ const headers = {};
412
+ const [properties, required] = shapeOf2(declared.headers, "a response's headers", warn);
413
+ for (const [name, property] of Object.entries(properties)) {
414
+ const { description, ...schema } = property;
415
+ headers[name] = {
416
+ ...typeof description === "string" ? { description } : {},
417
+ ...required.has(name) ? { required: true } : {},
418
+ schema
419
+ };
420
+ }
421
+ const cookies = setCookie(declared.cookies, warn);
422
+ if (cookies) {
423
+ headers["set-cookie"] = cookies;
424
+ }
425
+ return Object.keys(headers).length > 0 ? headers : undefined;
426
+ }
427
+ function setCookie(schema, warn) {
428
+ const [properties, required] = shapeOf2(schema, "a response's cookies", warn);
429
+ const names = Object.keys(properties);
430
+ if (names.length === 0) {
431
+ return;
432
+ }
433
+ const lines = names.map((name) => {
434
+ const description = properties[name]?.["description"];
435
+ return typeof description === "string" ? `- \`${name}\`: ${description}` : `- \`${name}\``;
436
+ });
437
+ return {
438
+ description: ["Sets these cookies:", "", ...lines].join(`
439
+ `),
440
+ ...required.size > 0 ? { required: true } : {},
441
+ schema: { type: "string" }
442
+ };
443
+ }
444
+ function shapeOf2(schema, subject, warn) {
445
+ const described = schema ? emitted(schema, subject, warn, "input") : undefined;
446
+ const properties = described?.properties ?? {};
447
+ const required = new Set(Array.isArray(described?.required) ? described.required : []);
448
+ return [properties, required];
396
449
  }
397
450
  function branchesFor(schema, status, envelopes, warn) {
398
451
  return branchesOf(schema, envelopes.format.code).map((branch) => {
@@ -463,7 +516,7 @@ function derivedId(entry) {
463
516
  }
464
517
  function requestBody(entry, warn) {
465
518
  const declared = entry.def.schema?.body;
466
- const bodyType = entry.def.bodyType ?? (declared ? "json" : undefined);
519
+ const bodyType = entry.def.bodyType ?? (declared || entry.def.rawBody ? "json" : undefined);
467
520
  if (!bodyType) {
468
521
  return;
469
522
  }
@@ -508,15 +561,18 @@ function responses(entry, options, contributed, components, envelopes, warn) {
508
561
  const add = (status, answer) => {
509
562
  answers.set(status, [...answers.get(status) ?? [], answer]);
510
563
  };
511
- for (const [status, schema] of declaredResponses(entry)) {
512
- const described = schema === null ? undefined : emitted(schema, "a response", warn);
564
+ const declared = declaredResponses(entry);
565
+ for (const [status, { body, ...rest }] of declared) {
566
+ const described = body === null ? undefined : emitted(body, "a response", warn);
567
+ const headers = declaredHeaders({ body, ...rest }, warn);
513
568
  add(status, {
514
569
  placeholder: describeStatus(status),
515
570
  said: described ? wordsOf(described, envelopes) : [],
516
- schemas: described ? branchesFor(unionOnly(described), Number(status), envelopes, warn) : []
571
+ schemas: described ? branchesFor(unionOnly(described), Number(status), envelopes, warn) : [],
572
+ ...headers === undefined ? {} : { headers }
517
573
  });
518
574
  }
519
- if (![...answers.keys()].some((status) => status.startsWith("2"))) {
575
+ if (declared.length === 0) {
520
576
  add("200", { placeholder: "Successful response", said: [], schemas: [] });
521
577
  }
522
578
  for (const [status, answer] of failures(entry, options, contributed, components, envelopes, warn)) {
@@ -541,7 +597,7 @@ function failures(entry, options, contributed, components, envelopes, warn) {
541
597
  const ref = error === undefined ? components.ref(failureName(status), described) : envelopes.ref(status, error, described, warn);
542
598
  return ref;
543
599
  };
544
- if (schema?.params || schema?.query || schema?.headers || schema?.body) {
600
+ if (schema?.params || schema?.query || schema?.headers || schema?.cookies || schema?.body) {
545
601
  found.push([
546
602
  options.validationStatus,
547
603
  {
@@ -587,7 +643,7 @@ function failures(entry, options, contributed, components, envelopes, warn) {
587
643
  }
588
644
  ]);
589
645
  }
590
- if (schema?.body || entry.def.bodyType) {
646
+ if (schema?.body || entry.def.bodyType || entry.def.rawBody) {
591
647
  const unparsable = parseFailure(entry.def.bodyType);
592
648
  if (unparsable) {
593
649
  found.push([
@@ -745,6 +801,63 @@ function capitalize(value) {
745
801
  function lowerFirst(value) {
746
802
  return value.charAt(0).toLowerCase() + value.slice(1);
747
803
  }
804
+
805
+ // packages/openapi/src/refs.ts
806
+ var data = new Set(["const", "default", "enum", "example", "examples"]);
807
+ function unresolvedRefs(node, document) {
808
+ const unresolved = new Set;
809
+ const visited = new Map;
810
+ const pending = [[node, document]];
811
+ for (let next = pending.pop();next; next = pending.pop()) {
812
+ const [value, base] = next;
813
+ if (typeof value !== "object" || value === null) {
814
+ continue;
815
+ }
816
+ const bases = visited.get(value) ?? new Set;
817
+ if (bases.has(base)) {
818
+ continue;
819
+ }
820
+ visited.set(value, bases.add(base));
821
+ const schema = value;
822
+ const resource = !Array.isArray(value) && typeof schema["$id"] === "string" ? schema : base;
823
+ const ref = Array.isArray(value) ? undefined : schema["$ref"];
824
+ if (typeof ref === "string" && ref.startsWith("#") && !resolves(ref.slice(1), resource, document)) {
825
+ unresolved.add(ref);
826
+ }
827
+ const children = Object.entries(schema).filter(([keyword]) => Array.isArray(value) || !data.has(keyword));
828
+ for (let index = children.length - 1;index >= 0; index -= 1) {
829
+ pending.push([children[index]?.[1], resource]);
830
+ }
831
+ }
832
+ return [...unresolved];
833
+ }
834
+ function resolves(fragment, base, document) {
835
+ if (fragment === "") {
836
+ return base !== document;
837
+ }
838
+ if (!fragment.startsWith("/")) {
839
+ return true;
840
+ }
841
+ let target = base;
842
+ for (const token of fragment.slice(1).split("/")) {
843
+ if (typeof target !== "object" || target === null) {
844
+ return false;
845
+ }
846
+ const key = decoded(token);
847
+ if (key === undefined || !Object.hasOwn(target, key)) {
848
+ return false;
849
+ }
850
+ target = target[key];
851
+ }
852
+ return typeof target === "object" && target !== null || typeof target === "boolean";
853
+ }
854
+ function decoded(token) {
855
+ try {
856
+ return decodeURIComponent(token).replaceAll("~1", "/").replaceAll("~0", "~");
857
+ } catch {
858
+ return;
859
+ }
860
+ }
748
861
  // packages/openapi/src/controller.ts
749
862
  import { onMount, route } from "@tetsujs/core";
750
863
 
@@ -787,7 +900,7 @@ function docsPage(options) {
787
900
  "<body>",
788
901
  body(options.ui, url),
789
902
  `<script src="${escapeHtml(assets.script)}"${verified(assets.integrity?.script)}></script>`,
790
- bootstrap(options.ui, url),
903
+ bootstrap(options.ui),
791
904
  "</body>",
792
905
  "</html>"
793
906
  ].filter(Boolean).join(`
@@ -800,15 +913,16 @@ function body(ui, url) {
800
913
  if (ui === "redoc") {
801
914
  return `<redoc spec-url="${url}"></redoc>`;
802
915
  }
803
- return '<div id="swagger-ui"></div>';
916
+ return `<div id="swagger-ui" data-url="${url}"></div>`;
804
917
  }
805
- function bootstrap(ui, url) {
918
+ function bootstrap(ui) {
806
919
  if (ui !== "swagger-ui") {
807
920
  return "";
808
921
  }
809
922
  return [
810
923
  "<script>",
811
- `SwaggerUIBundle({ url: "${url}", dom_id: "#swagger-ui" });`,
924
+ 'const root = document.getElementById("swagger-ui");',
925
+ 'SwaggerUIBundle({ url: root.dataset.url, dom_id: "#swagger-ui" });',
812
926
  "</script>"
813
927
  ].join(`
814
928
  `);
@@ -938,17 +1052,36 @@ function openapi(app, options) {
938
1052
  };
939
1053
  }
940
1054
  const tags = options.tags ? tagsOf(options.tags, used, warnings) : undefined;
941
- return {
942
- document: {
943
- openapi: "3.1.0",
944
- info: options.info,
945
- ...options.servers ? { servers: options.servers } : {},
946
- ...tags ? { tags } : {},
947
- paths,
948
- ...componentsOf(schemes, components.schemas)
949
- },
950
- warnings
1055
+ const document = {
1056
+ openapi: "3.1.0",
1057
+ info: options.info,
1058
+ ...options.servers ? { servers: options.servers } : {},
1059
+ ...tags ? { tags } : {},
1060
+ paths,
1061
+ ...componentsOf(schemes, components.schemas)
951
1062
  };
1063
+ refsOf(document, occupied, warnings);
1064
+ return { document, warnings };
1065
+ }
1066
+ function refsOf(document, routes, warnings) {
1067
+ const unresolved = (ref) => `refers to "${ref}", which is no schema in the document: references are embedded as they are, and resolve against the document's root`;
1068
+ for (const [template, item] of Object.entries(document.paths)) {
1069
+ for (const [method, operation] of Object.entries(item)) {
1070
+ const route = routes.get(`${method} ${template}`) ?? "";
1071
+ for (const ref of unresolvedRefs(operation, document)) {
1072
+ warnings.push({ route, message: `a schema ${unresolved(ref)}` });
1073
+ }
1074
+ }
1075
+ }
1076
+ const schemas = document.components?.schemas ?? {};
1077
+ for (const [name, schema] of Object.entries(schemas)) {
1078
+ for (const ref of unresolvedRefs(schema, document)) {
1079
+ warnings.push({
1080
+ route: "",
1081
+ message: `components/schemas/${name} ${unresolved(ref)}`
1082
+ });
1083
+ }
1084
+ }
952
1085
  }
953
1086
  function tagsOf(described, used, warnings) {
954
1087
  const tags = Object.entries(described).map(([name, description]) => ({ name, description }));
@@ -988,5 +1121,5 @@ export {
988
1121
  secured
989
1122
  };
990
1123
 
991
- //# debugId=3A3B60517CA111D464756E2164756E21
1124
+ //# debugId=A9BC9A29B6C6C1DA64756E2164756E21
992
1125
  //# sourceMappingURL=index.js.map