@depup/openapi-typescript 7.13.0-depup.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.
Files changed (106) hide show
  1. package/CHANGELOG.md +1162 -0
  2. package/CONTRIBUTING.md +149 -0
  3. package/LICENSE +21 -0
  4. package/README.md +32 -0
  5. package/bin/cli.js +297 -0
  6. package/changes.json +14 -0
  7. package/dist/index.cjs +152 -0
  8. package/dist/index.cjs.map +1 -0
  9. package/dist/index.d.cts +517 -0
  10. package/dist/index.d.mts +515 -0
  11. package/dist/index.d.ts +517 -0
  12. package/dist/index.mjs +85 -0
  13. package/dist/index.mjs.map +1 -0
  14. package/dist/lib/redoc.cjs +129 -0
  15. package/dist/lib/redoc.cjs.map +1 -0
  16. package/dist/lib/redoc.mjs +122 -0
  17. package/dist/lib/redoc.mjs.map +1 -0
  18. package/dist/lib/ts.cjs +470 -0
  19. package/dist/lib/ts.cjs.map +1 -0
  20. package/dist/lib/ts.mjs +431 -0
  21. package/dist/lib/ts.mjs.map +1 -0
  22. package/dist/lib/utils.cjs +295 -0
  23. package/dist/lib/utils.cjs.map +1 -0
  24. package/dist/lib/utils.mjs +278 -0
  25. package/dist/lib/utils.mjs.map +1 -0
  26. package/dist/transform/components-object.cjs +150 -0
  27. package/dist/transform/components-object.cjs.map +1 -0
  28. package/dist/transform/components-object.mjs +127 -0
  29. package/dist/transform/components-object.mjs.map +1 -0
  30. package/dist/transform/header-object.cjs +48 -0
  31. package/dist/transform/header-object.cjs.map +1 -0
  32. package/dist/transform/header-object.mjs +42 -0
  33. package/dist/transform/header-object.mjs.map +1 -0
  34. package/dist/transform/index.cjs +124 -0
  35. package/dist/transform/index.cjs.map +1 -0
  36. package/dist/transform/index.mjs +118 -0
  37. package/dist/transform/index.mjs.map +1 -0
  38. package/dist/transform/media-type-object.cjs +14 -0
  39. package/dist/transform/media-type-object.cjs.map +1 -0
  40. package/dist/transform/media-type-object.mjs +12 -0
  41. package/dist/transform/media-type-object.mjs.map +1 -0
  42. package/dist/transform/operation-object.cjs +105 -0
  43. package/dist/transform/operation-object.cjs.map +1 -0
  44. package/dist/transform/operation-object.mjs +96 -0
  45. package/dist/transform/operation-object.mjs.map +1 -0
  46. package/dist/transform/parameter-object.cjs +11 -0
  47. package/dist/transform/parameter-object.cjs.map +1 -0
  48. package/dist/transform/parameter-object.mjs +9 -0
  49. package/dist/transform/parameter-object.mjs.map +1 -0
  50. package/dist/transform/parameters-array.cjs +120 -0
  51. package/dist/transform/parameters-array.cjs.map +1 -0
  52. package/dist/transform/parameters-array.mjs +114 -0
  53. package/dist/transform/parameters-array.mjs.map +1 -0
  54. package/dist/transform/path-item-object.cjs +84 -0
  55. package/dist/transform/path-item-object.cjs.map +1 -0
  56. package/dist/transform/path-item-object.mjs +78 -0
  57. package/dist/transform/path-item-object.mjs.map +1 -0
  58. package/dist/transform/paths-enum.cjs +35 -0
  59. package/dist/transform/paths-enum.cjs.map +1 -0
  60. package/dist/transform/paths-enum.mjs +33 -0
  61. package/dist/transform/paths-enum.mjs.map +1 -0
  62. package/dist/transform/paths-object.cjs +134 -0
  63. package/dist/transform/paths-object.cjs.map +1 -0
  64. package/dist/transform/paths-object.mjs +128 -0
  65. package/dist/transform/paths-object.mjs.map +1 -0
  66. package/dist/transform/request-body-object.cjs +68 -0
  67. package/dist/transform/request-body-object.cjs.map +1 -0
  68. package/dist/transform/request-body-object.mjs +62 -0
  69. package/dist/transform/request-body-object.mjs.map +1 -0
  70. package/dist/transform/response-object.cjs +123 -0
  71. package/dist/transform/response-object.cjs.map +1 -0
  72. package/dist/transform/response-object.mjs +117 -0
  73. package/dist/transform/response-object.mjs.map +1 -0
  74. package/dist/transform/responses-object.cjs +36 -0
  75. package/dist/transform/responses-object.cjs.map +1 -0
  76. package/dist/transform/responses-object.mjs +30 -0
  77. package/dist/transform/responses-object.mjs.map +1 -0
  78. package/dist/transform/schema-object.cjs +502 -0
  79. package/dist/transform/schema-object.cjs.map +1 -0
  80. package/dist/transform/schema-object.mjs +493 -0
  81. package/dist/transform/schema-object.mjs.map +1 -0
  82. package/dist/transform/webhooks-object.cjs +37 -0
  83. package/dist/transform/webhooks-object.cjs.map +1 -0
  84. package/dist/transform/webhooks-object.mjs +31 -0
  85. package/dist/transform/webhooks-object.mjs.map +1 -0
  86. package/package.json +102 -0
  87. package/src/index.ts +108 -0
  88. package/src/lib/redoc.ts +164 -0
  89. package/src/lib/ts.ts +616 -0
  90. package/src/lib/utils.ts +394 -0
  91. package/src/transform/components-object.ts +161 -0
  92. package/src/transform/header-object.ts +45 -0
  93. package/src/transform/index.ts +116 -0
  94. package/src/transform/media-type-object.ts +18 -0
  95. package/src/transform/operation-object.ts +104 -0
  96. package/src/transform/parameter-object.ts +15 -0
  97. package/src/transform/parameters-array.ts +129 -0
  98. package/src/transform/path-item-object.ts +102 -0
  99. package/src/transform/paths-enum.ts +43 -0
  100. package/src/transform/paths-object.ts +134 -0
  101. package/src/transform/request-body-object.ts +59 -0
  102. package/src/transform/response-object.ts +112 -0
  103. package/src/transform/responses-object.ts +36 -0
  104. package/src/transform/schema-object.ts +737 -0
  105. package/src/transform/webhooks-object.ts +27 -0
  106. package/src/types.ts +735 -0
@@ -0,0 +1,116 @@
1
+ import { performance } from "node:perf_hooks";
2
+ import ts, { type InterfaceDeclaration, type TypeLiteralNode } from "typescript";
3
+ import { NEVER, STRING, stringToAST, tsModifiers, tsRecord } from "../lib/ts.js";
4
+ import { createRef, debug } from "../lib/utils.js";
5
+ import type { GlobalContext, OpenAPI3 } from "../types.js";
6
+ import transformComponentsObject from "./components-object.js";
7
+ import makeApiPathsEnum from "./paths-enum.js";
8
+ import transformPathsObject from "./paths-object.js";
9
+ import transformSchemaObject from "./schema-object.js";
10
+ import transformWebhooksObject from "./webhooks-object.js";
11
+
12
+ type SchemaTransforms = keyof Pick<OpenAPI3, "paths" | "webhooks" | "components" | "$defs">;
13
+
14
+ const transformers: Record<SchemaTransforms, (node: any, options: GlobalContext) => ts.Node | ts.Node[]> = {
15
+ paths: transformPathsObject,
16
+ webhooks: transformWebhooksObject,
17
+ components: transformComponentsObject,
18
+ $defs: (node, options) => transformSchemaObject(node, { path: createRef(["$defs"]), ctx: options, schema: node }),
19
+ };
20
+
21
+ // Inline helper types for readOnly/writeOnly markers (when readWriteMarkers is enabled)
22
+ const READ_WRITE_HELPER_TYPES = `
23
+ export type $Read<T> = { readonly $read: T };
24
+ export type $Write<T> = { readonly $write: T };
25
+ export type Readable<T> = T extends $Write<any> ? never : T extends $Read<infer U> ? Readable<U> : T extends (infer E)[] ? Readable<E>[] : T extends object ? { [K in keyof T as NonNullable<T[K]> extends $Write<any> ? never : K]: Readable<T[K]> } : T;
26
+ export type Writable<T> = T extends $Read<any> ? never : T extends $Write<infer U> ? Writable<U> : T extends (infer E)[] ? Writable<E>[] : T extends object ? { [K in keyof T as NonNullable<T[K]> extends $Read<any> ? never : K]: Writable<T[K]> } & { [K in keyof T as NonNullable<T[K]> extends $Read<any> ? K : never]?: never } : T;
27
+ `;
28
+
29
+ export default function transformSchema(schema: OpenAPI3, ctx: GlobalContext) {
30
+ const type: ts.Node[] = [];
31
+
32
+ // Add inline helper types for readOnly/writeOnly markers
33
+ if (ctx.readWriteMarkers) {
34
+ const helperNodes = stringToAST(READ_WRITE_HELPER_TYPES) as ts.Node[];
35
+ type.push(...helperNodes);
36
+ }
37
+
38
+ if (ctx.inject) {
39
+ const injectNodes = stringToAST(ctx.inject) as ts.Node[];
40
+ type.push(...injectNodes);
41
+ }
42
+
43
+ for (const root of Object.keys(transformers) as SchemaTransforms[]) {
44
+ const emptyObj = ts.factory.createTypeAliasDeclaration(
45
+ /* modifiers */ tsModifiers({ export: true }),
46
+ /* name */ root,
47
+ /* typeParameters */ undefined,
48
+ /* type */ tsRecord(STRING, NEVER),
49
+ );
50
+
51
+ if (schema[root] && typeof schema[root] === "object") {
52
+ const rootT = performance.now();
53
+ const subTypes = ([] as ts.Node[]).concat(transformers[root](schema[root], ctx));
54
+ for (const subType of subTypes) {
55
+ if (ts.isTypeNode(subType)) {
56
+ if ((subType as ts.TypeLiteralNode).members?.length) {
57
+ type.push(
58
+ ctx.exportType
59
+ ? ts.factory.createTypeAliasDeclaration(
60
+ /* modifiers */ tsModifiers({ export: true }),
61
+ /* name */ root,
62
+ /* typeParameters */ undefined,
63
+ /* type */ subType,
64
+ )
65
+ : ts.factory.createInterfaceDeclaration(
66
+ /* modifiers */ tsModifiers({ export: true }),
67
+ /* name */ root,
68
+ /* typeParameters */ undefined,
69
+ /* heritageClauses */ undefined,
70
+ /* members */ (subType as TypeLiteralNode).members,
71
+ ),
72
+ );
73
+ debug(`${root} done`, "ts", performance.now() - rootT);
74
+ } else {
75
+ type.push(emptyObj);
76
+ debug(`${root} done (skipped)`, "ts", 0);
77
+ }
78
+ } else if (ts.isTypeAliasDeclaration(subType)) {
79
+ type.push(subType);
80
+ } else {
81
+ type.push(emptyObj);
82
+ debug(`${root} done (skipped)`, "ts", 0);
83
+ }
84
+ }
85
+ } else {
86
+ type.push(emptyObj);
87
+ debug(`${root} done (skipped)`, "ts", 0);
88
+ }
89
+ }
90
+
91
+ // inject
92
+ let hasOperations = false;
93
+ for (const injectedType of ctx.injectFooter) {
94
+ if (!hasOperations && (injectedType as InterfaceDeclaration)?.name?.escapedText === "operations") {
95
+ hasOperations = true;
96
+ }
97
+ type.push(injectedType);
98
+ }
99
+ if (!hasOperations) {
100
+ // if no operations created, inject empty operations type
101
+ type.push(
102
+ ts.factory.createTypeAliasDeclaration(
103
+ /* modifiers */ tsModifiers({ export: true }),
104
+ /* name */ "operations",
105
+ /* typeParameters */ undefined,
106
+ /* type */ tsRecord(STRING, NEVER),
107
+ ),
108
+ );
109
+ }
110
+
111
+ if (ctx.makePathsEnum && schema.paths) {
112
+ type.push(makeApiPathsEnum(schema.paths));
113
+ }
114
+
115
+ return type;
116
+ }
@@ -0,0 +1,18 @@
1
+ import type ts from "typescript";
2
+ import { UNKNOWN } from "../lib/ts.js";
3
+ import type { MediaTypeObject, TransformNodeOptions } from "../types.js";
4
+ import transformSchemaObject from "./schema-object.js";
5
+
6
+ /**
7
+ * Transform MediaTypeObject nodes (4.8.14)
8
+ * @see https://spec.openapis.org/oas/v3.1.0#media-type-object
9
+ */
10
+ export default function transformMediaTypeObject(
11
+ mediaTypeObject: MediaTypeObject,
12
+ options: TransformNodeOptions,
13
+ ): ts.TypeNode {
14
+ if (!mediaTypeObject.schema) {
15
+ return UNKNOWN;
16
+ }
17
+ return transformSchemaObject(mediaTypeObject.schema, options);
18
+ }
@@ -0,0 +1,104 @@
1
+ import ts from "typescript";
2
+ import { addJSDocComment, NEVER, oapiRef, QUESTION_TOKEN, tsModifiers, tsPropertyIndex } from "../lib/ts.js";
3
+ import { createRef } from "../lib/utils.js";
4
+ import type { OperationObject, RequestBodyObject, TransformNodeOptions } from "../types.js";
5
+ import { transformParametersArray } from "./parameters-array.js";
6
+ import transformRequestBodyObject from "./request-body-object.js";
7
+ import transformResponsesObject from "./responses-object.js";
8
+
9
+ /**
10
+ * Transform OperationObject nodes (4.8.10)
11
+ * @see https://spec.openapis.org/oas/v3.1.0#operation-object
12
+ */
13
+ export default function transformOperationObject(
14
+ operationObject: OperationObject,
15
+ options: TransformNodeOptions,
16
+ ): ts.TypeElement[] {
17
+ const type: ts.TypeElement[] = [];
18
+
19
+ // parameters
20
+ type.push(...transformParametersArray(operationObject.parameters ?? [], options));
21
+
22
+ // requestBody
23
+ if (operationObject.requestBody) {
24
+ const requestBodyType =
25
+ "$ref" in operationObject.requestBody
26
+ ? oapiRef(operationObject.requestBody.$ref)
27
+ : transformRequestBodyObject(operationObject.requestBody, {
28
+ ...options,
29
+ path: createRef([options.path, "requestBody"]),
30
+ });
31
+ const required = !!(
32
+ "$ref" in operationObject.requestBody
33
+ ? options.ctx.resolve<RequestBodyObject>(operationObject.requestBody.$ref)
34
+ : operationObject.requestBody
35
+ )?.required;
36
+ const property = ts.factory.createPropertySignature(
37
+ /* modifiers */ tsModifiers({ readonly: options.ctx.immutable }),
38
+ /* name */ tsPropertyIndex("requestBody"),
39
+ /* questionToken */ required ? undefined : QUESTION_TOKEN,
40
+ /* type */ requestBodyType,
41
+ );
42
+ addJSDocComment(operationObject.requestBody, property);
43
+ type.push(property);
44
+ } else {
45
+ type.push(
46
+ ts.factory.createPropertySignature(
47
+ /* modifiers */ tsModifiers({ readonly: options.ctx.immutable }),
48
+ /* name */ tsPropertyIndex("requestBody"),
49
+ /* questionToken */ QUESTION_TOKEN,
50
+ /* type */ NEVER,
51
+ ),
52
+ );
53
+ }
54
+
55
+ // responses
56
+ type.push(
57
+ ts.factory.createPropertySignature(
58
+ /* modifiers */ tsModifiers({ readonly: options.ctx.immutable }),
59
+ /* name */ tsPropertyIndex("responses"),
60
+ /* questionToken */ undefined,
61
+ /* type */ transformResponsesObject(operationObject.responses ?? {}, options),
62
+ ),
63
+ );
64
+
65
+ return type;
66
+ }
67
+
68
+ /** inject an operation at the top level */
69
+ export function injectOperationObject(
70
+ operationId: string,
71
+ operationObject: OperationObject,
72
+ options: TransformNodeOptions,
73
+ ): void {
74
+ // find or create top-level operations interface
75
+ let operations = options.ctx.injectFooter.find(
76
+ (node) => ts.isInterfaceDeclaration(node) && (node as ts.InterfaceDeclaration).name.text === "operations",
77
+ ) as unknown as ts.InterfaceDeclaration;
78
+ if (!operations) {
79
+ operations = ts.factory.createInterfaceDeclaration(
80
+ /* modifiers */ tsModifiers({
81
+ export: true,
82
+ // important: do NOT make this immutable
83
+ }),
84
+ /* name */ ts.factory.createIdentifier("operations"),
85
+ /* typeParameters */ undefined,
86
+ /* heritageClauses */ undefined,
87
+ /* members */ [],
88
+ );
89
+ options.ctx.injectFooter.push(operations);
90
+ }
91
+
92
+ // inject operation object
93
+ const type = transformOperationObject(operationObject, options);
94
+ // @ts-expect-error this is OK to mutate
95
+ operations.members = ts.factory.createNodeArray([
96
+ ...operations.members,
97
+ ts.factory.createPropertySignature(
98
+ /* modifiers */ tsModifiers({ readonly: options.ctx.immutable }),
99
+ /* name */ tsPropertyIndex(operationId),
100
+ /* questionToken */ undefined,
101
+ /* type */ ts.factory.createTypeLiteralNode(type),
102
+ ),
103
+ ]);
104
+ }
@@ -0,0 +1,15 @@
1
+ import type ts from "typescript";
2
+ import { STRING } from "../lib/ts.js";
3
+ import type { ParameterObject, TransformNodeOptions } from "../types.js";
4
+ import transformSchemaObject from "./schema-object.js";
5
+
6
+ /**
7
+ * Transform ParameterObject nodes (4.8.12)
8
+ * @see https://spec.openapis.org/oas/v3.1.0#parameter-object
9
+ */
10
+ export default function transformParameterObject(
11
+ parameterObject: ParameterObject,
12
+ options: TransformNodeOptions,
13
+ ): ts.TypeNode {
14
+ return parameterObject.schema ? transformSchemaObject(parameterObject.schema, options) : STRING; // assume a parameter is a string by default rather than "unknown"
15
+ }
@@ -0,0 +1,129 @@
1
+ import ts from "typescript";
2
+ import { addJSDocComment, NEVER, oapiRef, QUESTION_TOKEN, tsModifiers, tsPropertyIndex } from "../lib/ts.js";
3
+ import { createRef } from "../lib/utils.js";
4
+ import type { ParameterObject, ReferenceObject, TransformNodeOptions } from "../types.js";
5
+ import transformParameterObject from "./parameter-object.js";
6
+
7
+ // Regex to match path parameters in URL
8
+ const PATH_PARAM_RE = /\{([^}]+)\}/g;
9
+
10
+ /**
11
+ * Create a synthetic path parameter object from a parameter name
12
+ */
13
+ function createPathParameter(paramName: string): ParameterObject {
14
+ return {
15
+ name: paramName,
16
+ in: "path",
17
+ required: true,
18
+ schema: { type: "string" },
19
+ };
20
+ }
21
+
22
+ /**
23
+ * Extract path parameters from a URL
24
+ */
25
+ function extractPathParamsFromUrl(path: string): ParameterObject[] {
26
+ const params: ParameterObject[] = [];
27
+ const matches = path.match(PATH_PARAM_RE);
28
+ if (matches) {
29
+ for (const match of matches) {
30
+ const paramName = match.slice(1, -1);
31
+ params.push(createPathParameter(paramName));
32
+ }
33
+ }
34
+ return params;
35
+ }
36
+
37
+ /**
38
+ * Synthetic type. Array of (ParameterObject | ReferenceObject)s found in OperationObject and PathItemObject.
39
+ */
40
+ export function transformParametersArray(
41
+ parametersArray: (ParameterObject | ReferenceObject)[],
42
+ options: TransformNodeOptions,
43
+ ): ts.TypeElement[] {
44
+ const type: ts.TypeElement[] = [];
45
+
46
+ // Create a working copy of parameters array
47
+ const workingParameters = [...parametersArray];
48
+
49
+ // Generate path parameters if enabled
50
+ if (options.ctx.generatePathParams && options.path) {
51
+ const pathString = Array.isArray(options.path) ? options.path[0] : options.path;
52
+ if (typeof pathString === "string") {
53
+ const pathParams = extractPathParamsFromUrl(pathString);
54
+ // Only add path parameters that aren't already defined
55
+ for (const param of pathParams) {
56
+ const exists = workingParameters.some((p) => {
57
+ const resolved = "$ref" in p ? options.ctx.resolve<ParameterObject>(p.$ref) : p;
58
+ return resolved?.in === "path" && resolved?.name === param.name;
59
+ });
60
+ if (!exists) {
61
+ workingParameters.push(param);
62
+ }
63
+ }
64
+ }
65
+ }
66
+
67
+ // parameters
68
+ const paramType: ts.TypeElement[] = [];
69
+ for (const paramIn of ["query", "header", "path", "cookie"] as ParameterObject["in"][]) {
70
+ const paramLocType: ts.TypeElement[] = [];
71
+ let operationParameters = workingParameters.map((param) => ({
72
+ original: param,
73
+ resolved: "$ref" in param ? options.ctx.resolve<ParameterObject>(param.$ref) : param,
74
+ }));
75
+
76
+ // this is the only array type in the spec, so we have to one-off sort here
77
+ if (options.ctx.alphabetize) {
78
+ operationParameters.sort((a, b) => (a.resolved?.name ?? "").localeCompare(b.resolved?.name ?? ""));
79
+ }
80
+ if (options.ctx.excludeDeprecated) {
81
+ operationParameters = operationParameters.filter(
82
+ ({ resolved }) => !resolved?.deprecated && !resolved?.schema?.deprecated,
83
+ );
84
+ }
85
+ for (const { original, resolved } of operationParameters) {
86
+ if (resolved?.in !== paramIn) {
87
+ continue;
88
+ }
89
+ let optional: ts.QuestionToken | undefined;
90
+ if (paramIn !== "path" && !(resolved as ParameterObject).required) {
91
+ optional = QUESTION_TOKEN;
92
+ }
93
+ const subType =
94
+ "$ref" in original
95
+ ? oapiRef(original.$ref, resolved)
96
+ : transformParameterObject(resolved as ParameterObject, {
97
+ ...options,
98
+ path: createRef([options.path, "parameters", resolved.in, resolved.name]),
99
+ });
100
+ const property = ts.factory.createPropertySignature(
101
+ /* modifiers */ tsModifiers({ readonly: options.ctx.immutable }),
102
+ /* name */ tsPropertyIndex(resolved?.name),
103
+ /* questionToken */ optional,
104
+ /* type */ subType,
105
+ );
106
+ addJSDocComment(resolved, property);
107
+ paramLocType.push(property);
108
+ }
109
+ const allOptional = paramLocType.every((node) => !!node.questionToken);
110
+ paramType.push(
111
+ ts.factory.createPropertySignature(
112
+ /* modifiers */ tsModifiers({ readonly: options.ctx.immutable }),
113
+ /* name */ tsPropertyIndex(paramIn),
114
+ /* questionToken */ allOptional || !paramLocType.length ? QUESTION_TOKEN : undefined,
115
+ /* type */ paramLocType.length ? ts.factory.createTypeLiteralNode(paramLocType) : NEVER,
116
+ ),
117
+ );
118
+ }
119
+ type.push(
120
+ ts.factory.createPropertySignature(
121
+ /* modifiers */ tsModifiers({ readonly: options.ctx.immutable }),
122
+ /* name */ tsPropertyIndex("parameters"),
123
+ /* questionToken */ !paramType.length ? QUESTION_TOKEN : undefined,
124
+ /* type */ paramType.length ? ts.factory.createTypeLiteralNode(paramType) : NEVER,
125
+ ),
126
+ );
127
+
128
+ return type;
129
+ }
@@ -0,0 +1,102 @@
1
+ import ts from "typescript";
2
+ import { addJSDocComment, NEVER, oapiRef, QUESTION_TOKEN, tsModifiers, tsPropertyIndex } from "../lib/ts.js";
3
+ import { createRef } from "../lib/utils.js";
4
+ import type {
5
+ OperationObject,
6
+ ParameterObject,
7
+ PathItemObject,
8
+ ReferenceObject,
9
+ TransformNodeOptions,
10
+ } from "../types.js";
11
+ import transformOperationObject, { injectOperationObject } from "./operation-object.js";
12
+ import { transformParametersArray } from "./parameters-array.js";
13
+
14
+ export type Method = "get" | "put" | "post" | "delete" | "options" | "head" | "patch" | "trace";
15
+
16
+ /**
17
+ * Transform PathItem nodes (4.8.9)
18
+ * @see https://spec.openapis.org/oas/v3.1.0#path-item-object
19
+ */
20
+ export default function transformPathItemObject(pathItem: PathItemObject, options: TransformNodeOptions): ts.TypeNode {
21
+ const type: ts.TypeElement[] = [];
22
+
23
+ // parameters
24
+ type.push(
25
+ ...transformParametersArray(pathItem.parameters ?? [], {
26
+ ...options,
27
+ path: createRef([options.path, "parameters"]),
28
+ }),
29
+ );
30
+
31
+ // methods
32
+ for (const method of ["get", "put", "post", "delete", "options", "head", "patch", "trace"] as Method[]) {
33
+ const operationObject = pathItem[method];
34
+ if (
35
+ !operationObject ||
36
+ (options.ctx.excludeDeprecated &&
37
+ ("$ref" in operationObject ? options.ctx.resolve<OperationObject>(operationObject.$ref) : operationObject)
38
+ ?.deprecated)
39
+ ) {
40
+ type.push(
41
+ ts.factory.createPropertySignature(
42
+ /* modifiers */ tsModifiers({ readonly: options.ctx.immutable }),
43
+ /* name */ tsPropertyIndex(method),
44
+ /* questionToken */ QUESTION_TOKEN,
45
+ /* type */ NEVER,
46
+ ),
47
+ );
48
+ continue;
49
+ }
50
+
51
+ // fold top-level PathItem parameters into method-level, with the latter overriding the former
52
+ const keyedParameters: Record<string, ParameterObject | ReferenceObject> = {};
53
+ if (!("$ref" in operationObject)) {
54
+ // important: OperationObject parameters come last, and will override any conflicts with PathItem parameters
55
+ for (const parameter of [...(pathItem.parameters ?? []), ...(operationObject.parameters ?? [])]) {
56
+ // fix: #1798, use unique key
57
+ const name =
58
+ "$ref" in parameter
59
+ ? `${options.ctx.resolve<ParameterObject>(parameter.$ref)?.in}-${options.ctx.resolve<ParameterObject>(parameter.$ref)?.name}`
60
+ : `${parameter.in}-${parameter.name}`;
61
+ if (name) {
62
+ keyedParameters[name] = parameter;
63
+ }
64
+ }
65
+ }
66
+
67
+ let operationType: ts.TypeNode;
68
+ if ("$ref" in operationObject) {
69
+ operationType = oapiRef(operationObject.$ref);
70
+ }
71
+ // if operationId exists, move into an `operations` export and pass the reference in here
72
+ else if (operationObject.operationId) {
73
+ // workaround for issue caused by redocly ref parsing: https://github.com/openapi-ts/openapi-typescript/issues/1542
74
+ const operationId = operationObject.operationId.replace(HASH_RE, "/");
75
+ operationType = oapiRef(createRef(["operations", operationId]));
76
+ injectOperationObject(
77
+ operationId,
78
+ { ...operationObject, parameters: Object.values(keyedParameters) },
79
+ { ...options, path: createRef([options.path, method]) },
80
+ );
81
+ } else {
82
+ operationType = ts.factory.createTypeLiteralNode(
83
+ transformOperationObject(
84
+ { ...operationObject, parameters: Object.values(keyedParameters) },
85
+ { ...options, path: createRef([options.path, method]) },
86
+ ),
87
+ );
88
+ }
89
+ const property = ts.factory.createPropertySignature(
90
+ /* modifiers */ tsModifiers({ readonly: options.ctx.immutable }),
91
+ /* name */ tsPropertyIndex(method),
92
+ /* questionToken */ undefined,
93
+ /* type */ operationType,
94
+ );
95
+ addJSDocComment(operationObject, property);
96
+ type.push(property);
97
+ }
98
+
99
+ return ts.factory.createTypeLiteralNode(type);
100
+ }
101
+
102
+ const HASH_RE = /#/g;
@@ -0,0 +1,43 @@
1
+ import type ts from "typescript";
2
+ import { tsEnum } from "../lib/ts.js";
3
+ import { getEntries } from "../lib/utils.js";
4
+ import type { PathsObject } from "../types.js";
5
+
6
+ export default function makeApiPathsEnum(pathsObject: PathsObject): ts.EnumDeclaration {
7
+ const enumKeys = [];
8
+ const enumMetaData = [];
9
+
10
+ for (const [url, pathItemObject] of getEntries(pathsObject)) {
11
+ for (const [method, operation] of Object.entries(pathItemObject)) {
12
+ if (!["get", "put", "post", "delete", "options", "head", "patch", "trace"].includes(method)) {
13
+ continue;
14
+ }
15
+
16
+ // Generate a name from the operation ID
17
+ let pathName: string;
18
+ if (operation.operationId) {
19
+ pathName = operation.operationId;
20
+ } else {
21
+ // If the operation ID is not present, construct a name from the method and path
22
+ pathName = (method + url)
23
+ .split("/")
24
+ .map((part) => {
25
+ const capitalised = part.charAt(0).toUpperCase() + part.slice(1);
26
+
27
+ // Remove any characters not allowed as enum keys, and attempt to remove
28
+ // named parameters.
29
+ return capitalised.replace(/{.*}|:.*|[^a-zA-Z\d_]+/, "");
30
+ })
31
+ .join("");
32
+ }
33
+ enumKeys.push(url);
34
+ enumMetaData.push({
35
+ name: pathName,
36
+ });
37
+ }
38
+ }
39
+
40
+ return tsEnum("ApiPaths", enumKeys, enumMetaData, {
41
+ export: true,
42
+ });
43
+ }
@@ -0,0 +1,134 @@
1
+ import { performance } from "node:perf_hooks";
2
+ import ts from "typescript";
3
+ import { addJSDocComment, oapiRef, stringToAST, tsModifiers, tsPropertyIndex } from "../lib/ts.js";
4
+ import { createRef, debug, getEntries } from "../lib/utils.js";
5
+ import type {
6
+ GlobalContext,
7
+ OperationObject,
8
+ ParameterObject,
9
+ PathItemObject,
10
+ PathsObject,
11
+ ReferenceObject,
12
+ } from "../types.js";
13
+ import transformPathItemObject, { type Method } from "./path-item-object.js";
14
+
15
+ const PATH_PARAM_RE = /\{[^}]+\}/g;
16
+
17
+ /**
18
+ * Transform the PathsObject node (4.8.8)
19
+ * @see https://spec.openapis.org/oas/v3.1.0#operation-object
20
+ */
21
+ export default function transformPathsObject(pathsObject: PathsObject, ctx: GlobalContext): ts.TypeNode {
22
+ const type: ts.TypeElement[] = [];
23
+ for (const [url, pathItemObject] of getEntries(pathsObject, ctx)) {
24
+ if (!pathItemObject || typeof pathItemObject !== "object") {
25
+ continue;
26
+ }
27
+
28
+ const pathT = performance.now();
29
+
30
+ // handle $ref
31
+ if ("$ref" in pathItemObject) {
32
+ const property = ts.factory.createPropertySignature(
33
+ /* modifiers */ tsModifiers({ readonly: ctx.immutable }),
34
+ /* name */ tsPropertyIndex(url),
35
+ /* questionToken */ undefined,
36
+ /* type */ oapiRef(pathItemObject.$ref),
37
+ );
38
+ addJSDocComment(pathItemObject, property);
39
+ type.push(property);
40
+ } else {
41
+ const pathItemType = transformPathItemObject(pathItemObject, {
42
+ path: createRef(["paths", url]),
43
+ ctx,
44
+ });
45
+
46
+ // pathParamsAsTypes
47
+ if (ctx.pathParamsAsTypes && url.includes("{")) {
48
+ const pathParams = extractPathParams(pathItemObject, ctx);
49
+ const matches = url.match(PATH_PARAM_RE);
50
+ let rawPath = `\`${url}\``;
51
+ if (matches) {
52
+ for (const match of matches) {
53
+ const paramName = match.slice(1, -1);
54
+ const param = pathParams[paramName];
55
+ switch (param?.schema?.type) {
56
+ case "number":
57
+ case "integer":
58
+ rawPath = rawPath.replace(match, "${number}");
59
+ break;
60
+ case "boolean":
61
+ rawPath = rawPath.replace(match, "${boolean}");
62
+ break;
63
+ default:
64
+ rawPath = rawPath.replace(match, "${string}");
65
+ break;
66
+ }
67
+ }
68
+ // note: creating a string template literal’s AST manually is hard!
69
+ // just pass an arbitrary string to TS
70
+ const pathType = (stringToAST(rawPath)[0] as any)?.expression;
71
+ if (pathType) {
72
+ type.push(
73
+ ts.factory.createIndexSignature(
74
+ /* modifiers */ tsModifiers({ readonly: ctx.immutable }),
75
+ /* parameters */ [
76
+ ts.factory.createParameterDeclaration(
77
+ /* modifiers */ undefined,
78
+ /* dotDotDotToken */ undefined,
79
+ /* name */ "path",
80
+ /* questionToken */ undefined,
81
+ /* type */ pathType,
82
+ /* initializer */ undefined,
83
+ ),
84
+ ],
85
+ /* type */ pathItemType,
86
+ ),
87
+ );
88
+ continue;
89
+ }
90
+ }
91
+ }
92
+
93
+ type.push(
94
+ ts.factory.createPropertySignature(
95
+ /* modifiers */ tsModifiers({ readonly: ctx.immutable }),
96
+ /* name */ tsPropertyIndex(url),
97
+ /* questionToken */ undefined,
98
+ /* type */ pathItemType,
99
+ ),
100
+ );
101
+
102
+ debug(`Transformed path "${url}"`, "ts", performance.now() - pathT);
103
+ }
104
+ }
105
+
106
+ return ts.factory.createTypeLiteralNode(type);
107
+ }
108
+
109
+ function extractPathParams(pathItemObject: PathItemObject, ctx: GlobalContext) {
110
+ const params: Record<string, ParameterObject> = {};
111
+ for (const p of pathItemObject.parameters ?? []) {
112
+ const resolved = "$ref" in p && p.$ref ? ctx.resolve<ParameterObject>(p.$ref) : (p as ParameterObject);
113
+ if (resolved && resolved.in === "path") {
114
+ params[resolved.name] = resolved;
115
+ }
116
+ }
117
+ for (const method of ["get", "put", "post", "delete", "options", "head", "patch", "trace"] as Method[]) {
118
+ if (!(method in pathItemObject)) {
119
+ continue;
120
+ }
121
+ const resolvedMethod = (pathItemObject[method] as ReferenceObject).$ref
122
+ ? ctx.resolve<OperationObject>((pathItemObject[method] as ReferenceObject).$ref)
123
+ : (pathItemObject[method] as OperationObject);
124
+ if (resolvedMethod?.parameters) {
125
+ for (const p of resolvedMethod.parameters) {
126
+ const resolvedParam = "$ref" in p && p.$ref ? ctx.resolve<ParameterObject>(p.$ref) : (p as ParameterObject);
127
+ if (resolvedParam && resolvedParam.in === "path") {
128
+ params[resolvedParam.name] = resolvedParam;
129
+ }
130
+ }
131
+ }
132
+ }
133
+ return params;
134
+ }