@vertz/openapi 0.1.1 → 0.1.3
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/README.md +19 -3
- package/bin/openapi.ts +1 -1
- package/dist/cli.js +1 -1
- package/dist/index.d.ts +40 -0
- package/dist/index.js +1 -1
- package/dist/shared/{chunk-er3k8t3a.js → chunk-19t7k664.js} +198 -113
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -104,15 +104,31 @@ console.log(`${result.written} files written, ${result.skipped} unchanged`);
|
|
|
104
104
|
|
|
105
105
|
## Using the Generated SDK
|
|
106
106
|
|
|
107
|
+
The generated SDK uses `@vertz/fetch` under the hood. Install it in the project that consumes the SDK:
|
|
108
|
+
|
|
109
|
+
```bash
|
|
110
|
+
bun add @vertz/fetch
|
|
111
|
+
```
|
|
112
|
+
|
|
107
113
|
```ts
|
|
108
114
|
import { createClient } from './generated/client';
|
|
115
|
+
import { isOk } from '@vertz/fetch';
|
|
109
116
|
|
|
110
117
|
const api = createClient({ baseURL: 'https://api.example.com' });
|
|
111
118
|
|
|
112
119
|
// Fully typed — params, body, and response types are inferred
|
|
113
|
-
|
|
114
|
-
const
|
|
115
|
-
|
|
120
|
+
// Returns FetchResponse<T> (Result type) — use isOk/isErr to handle
|
|
121
|
+
const result = await api.tasks.list();
|
|
122
|
+
if (isOk(result)) {
|
|
123
|
+
console.log(result.data); // typed as Task[]
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// All FetchClient features available: auth strategies, retries, hooks
|
|
127
|
+
const api = createClient({
|
|
128
|
+
baseURL: 'https://api.example.com',
|
|
129
|
+
authStrategies: [{ type: 'bearer', token: 'my-token' }],
|
|
130
|
+
retry: { retries: 3 },
|
|
131
|
+
});
|
|
116
132
|
```
|
|
117
133
|
|
|
118
134
|
## Custom Operation ID Normalization
|
package/bin/openapi.ts
CHANGED
package/dist/cli.js
CHANGED
package/dist/index.d.ts
CHANGED
|
@@ -7,6 +7,7 @@ interface ParsedSpec {
|
|
|
7
7
|
};
|
|
8
8
|
resources: ParsedResource[];
|
|
9
9
|
schemas: ParsedSchema[];
|
|
10
|
+
securitySchemes: ParsedSecurityScheme[];
|
|
10
11
|
}
|
|
11
12
|
interface ParsedResource {
|
|
12
13
|
name: string;
|
|
@@ -24,6 +25,11 @@ interface ParsedOperation {
|
|
|
24
25
|
response?: ParsedSchema;
|
|
25
26
|
responseStatus: number;
|
|
26
27
|
tags: string[];
|
|
28
|
+
security?: OperationSecurity;
|
|
29
|
+
}
|
|
30
|
+
interface OperationSecurity {
|
|
31
|
+
required: boolean;
|
|
32
|
+
schemes: string[];
|
|
27
33
|
}
|
|
28
34
|
interface ParsedParameter {
|
|
29
35
|
name: string;
|
|
@@ -34,6 +40,37 @@ interface ParsedSchema {
|
|
|
34
40
|
name?: string;
|
|
35
41
|
jsonSchema: Record<string, unknown>;
|
|
36
42
|
}
|
|
43
|
+
type ParsedSecurityScheme = {
|
|
44
|
+
type: "bearer";
|
|
45
|
+
name: string;
|
|
46
|
+
description?: string;
|
|
47
|
+
} | {
|
|
48
|
+
type: "basic";
|
|
49
|
+
name: string;
|
|
50
|
+
description?: string;
|
|
51
|
+
} | {
|
|
52
|
+
type: "apiKey";
|
|
53
|
+
name: string;
|
|
54
|
+
in: "header" | "query" | "cookie";
|
|
55
|
+
paramName: string;
|
|
56
|
+
description?: string;
|
|
57
|
+
} | {
|
|
58
|
+
type: "oauth2";
|
|
59
|
+
name: string;
|
|
60
|
+
flows: ParsedOAuthFlows;
|
|
61
|
+
description?: string;
|
|
62
|
+
};
|
|
63
|
+
interface ParsedOAuthFlows {
|
|
64
|
+
authorizationCode?: {
|
|
65
|
+
authorizationUrl: string;
|
|
66
|
+
tokenUrl: string;
|
|
67
|
+
scopes: Record<string, string>;
|
|
68
|
+
};
|
|
69
|
+
clientCredentials?: {
|
|
70
|
+
tokenUrl: string;
|
|
71
|
+
scopes: Record<string, string>;
|
|
72
|
+
};
|
|
73
|
+
}
|
|
37
74
|
interface OperationContext {
|
|
38
75
|
/** Raw operationId from the spec */
|
|
39
76
|
operationId: string;
|
|
@@ -117,9 +154,11 @@ declare function sanitizeIdentifier(name: string): string;
|
|
|
117
154
|
declare function generateAll(spec: ParsedSpec, options?: GenerateOptions): GeneratedFile[];
|
|
118
155
|
/**
|
|
119
156
|
* Generate the main client.ts file.
|
|
157
|
+
* Uses @vertz/fetch FetchClient instead of hand-rolling HTTP methods.
|
|
120
158
|
*/
|
|
121
159
|
declare function generateClient(resources: ParsedResource[], config: {
|
|
122
160
|
baseURL?: string;
|
|
161
|
+
securitySchemes?: ParsedSecurityScheme[];
|
|
123
162
|
}): GeneratedFile;
|
|
124
163
|
/**
|
|
125
164
|
* Convert a JSON Schema object to a TypeScript type expression string.
|
|
@@ -154,6 +193,7 @@ declare function generateTypes(resources: ParsedResource[], schemas: ParsedSchem
|
|
|
154
193
|
declare function parseOpenAPI(spec: Record<string, unknown>): {
|
|
155
194
|
operations: ParsedOperation[];
|
|
156
195
|
schemas: ParsedSchema[];
|
|
196
|
+
securitySchemes: ParsedSecurityScheme[];
|
|
157
197
|
version: "3.0" | "3.1";
|
|
158
198
|
};
|
|
159
199
|
interface ResolveOptions {
|
package/dist/index.js
CHANGED
|
@@ -41,86 +41,102 @@ function groupOperations(operations, strategy, options) {
|
|
|
41
41
|
}
|
|
42
42
|
|
|
43
43
|
// src/generators/client-generator.ts
|
|
44
|
+
function camelCase(name) {
|
|
45
|
+
const leadingUpper = name.match(/^[A-Z]+/);
|
|
46
|
+
if (leadingUpper) {
|
|
47
|
+
const prefix = leadingUpper[0];
|
|
48
|
+
if (prefix.length >= name.length)
|
|
49
|
+
return name.toLowerCase();
|
|
50
|
+
if (prefix.length > 1) {
|
|
51
|
+
return prefix.slice(0, -1).toLowerCase() + name.slice(prefix.length - 1);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return name.charAt(0).toLowerCase() + name.slice(1);
|
|
55
|
+
}
|
|
56
|
+
function generateAuthField(scheme) {
|
|
57
|
+
switch (scheme.type) {
|
|
58
|
+
case "bearer":
|
|
59
|
+
return ` ${camelCase(scheme.name)}?: string | (() => string | Promise<string>);`;
|
|
60
|
+
case "basic":
|
|
61
|
+
return ` ${camelCase(scheme.name)}?: { username: string; password: string };`;
|
|
62
|
+
case "apiKey":
|
|
63
|
+
return ` ${camelCase(scheme.name)}?: string | (() => string | Promise<string>);`;
|
|
64
|
+
case "oauth2":
|
|
65
|
+
return ` ${camelCase(scheme.name)}?: string | (() => string | Promise<string>);`;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
function generateAuthStrategy(scheme) {
|
|
69
|
+
const fieldName = camelCase(scheme.name);
|
|
70
|
+
const lines = [];
|
|
71
|
+
switch (scheme.type) {
|
|
72
|
+
case "bearer":
|
|
73
|
+
lines.push(` ...(options.auth?.${fieldName} ? [{`);
|
|
74
|
+
lines.push(` type: 'bearer' as const,`);
|
|
75
|
+
lines.push(` token: options.auth?.${fieldName},`);
|
|
76
|
+
lines.push(" }] : []),");
|
|
77
|
+
break;
|
|
78
|
+
case "basic":
|
|
79
|
+
lines.push(` ...(options.auth?.${fieldName} ? [{`);
|
|
80
|
+
lines.push(` type: 'basic' as const,`);
|
|
81
|
+
lines.push(` ...options.auth?.${fieldName},`);
|
|
82
|
+
lines.push(" }] : []),");
|
|
83
|
+
break;
|
|
84
|
+
case "apiKey":
|
|
85
|
+
lines.push(` ...(options.auth?.${fieldName} ? [{`);
|
|
86
|
+
lines.push(` type: 'apiKey' as const,`);
|
|
87
|
+
lines.push(` key: options.auth?.${fieldName},`);
|
|
88
|
+
lines.push(` location: '${scheme.in}' as const,`);
|
|
89
|
+
lines.push(` name: '${scheme.paramName}',`);
|
|
90
|
+
lines.push(" }] : []),");
|
|
91
|
+
break;
|
|
92
|
+
case "oauth2":
|
|
93
|
+
lines.push(` ...(options.auth?.${fieldName} ? [{`);
|
|
94
|
+
lines.push(` type: 'bearer' as const,`);
|
|
95
|
+
lines.push(` token: options.auth?.${fieldName},`);
|
|
96
|
+
lines.push(" }] : []),");
|
|
97
|
+
break;
|
|
98
|
+
}
|
|
99
|
+
return lines;
|
|
100
|
+
}
|
|
44
101
|
function generateClient(resources, config) {
|
|
45
102
|
const defaultBaseURL = config.baseURL ? `'${config.baseURL.replace(/'/g, "\\'")}'` : "''";
|
|
103
|
+
const schemes = config.securitySchemes ?? [];
|
|
104
|
+
const hasAuth = schemes.length > 0;
|
|
46
105
|
const lines = [];
|
|
47
106
|
lines.push("// Generated by @vertz/openapi — do not edit");
|
|
48
107
|
lines.push("");
|
|
108
|
+
lines.push("import { FetchClient } from '@vertz/fetch';");
|
|
109
|
+
lines.push("import type { FetchClientConfig } from '@vertz/fetch';");
|
|
49
110
|
for (const r of resources) {
|
|
50
111
|
lines.push(`import { create${r.name}Resource } from './resources/${r.identifier}';`);
|
|
51
112
|
}
|
|
52
113
|
lines.push("");
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
114
|
+
if (hasAuth) {
|
|
115
|
+
lines.push("export interface ClientAuth {");
|
|
116
|
+
for (const scheme of schemes) {
|
|
117
|
+
lines.push(generateAuthField(scheme));
|
|
118
|
+
}
|
|
119
|
+
lines.push("}");
|
|
120
|
+
lines.push("");
|
|
121
|
+
}
|
|
122
|
+
if (hasAuth) {
|
|
123
|
+
lines.push("export type ClientOptions = FetchClientConfig & { auth?: ClientAuth };");
|
|
124
|
+
} else {
|
|
125
|
+
lines.push("export type ClientOptions = FetchClientConfig;");
|
|
126
|
+
}
|
|
66
127
|
lines.push("");
|
|
67
128
|
lines.push("export function createClient(options: ClientOptions = {}) {");
|
|
68
|
-
lines.push(` const
|
|
69
|
-
lines.push(
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
lines.push(" const params = new URLSearchParams();");
|
|
77
|
-
lines.push(" for (const [k, v] of Object.entries(opts.query)) { if (v != null) params.set(k, String(v)); }");
|
|
78
|
-
lines.push(" const qs = params.toString();");
|
|
79
|
-
lines.push(" if (qs) url += `?${qs}`;");
|
|
80
|
-
lines.push(" }");
|
|
81
|
-
lines.push(" const res = await customFetch(url, { method: 'GET', headers: defaultHeaders });");
|
|
82
|
-
lines.push(" if (!res.ok) throw await ApiError.from(res);");
|
|
83
|
-
lines.push(" if (res.status === 204) return undefined as T;");
|
|
84
|
-
lines.push(" return res.json();");
|
|
85
|
-
lines.push(" },");
|
|
86
|
-
lines.push("");
|
|
87
|
-
for (const method of ["post", "put", "patch"]) {
|
|
88
|
-
lines.push(` async ${method}<T>(path: string, body?: unknown, opts?: { query?: Record<string, unknown> }): Promise<T> {`);
|
|
89
|
-
lines.push(` let url = \`\${baseURL}\${path}\`;`);
|
|
90
|
-
lines.push(" if (opts?.query) {");
|
|
91
|
-
lines.push(" const params = new URLSearchParams();");
|
|
92
|
-
lines.push(" for (const [k, v] of Object.entries(opts.query)) { if (v != null) params.set(k, String(v)); }");
|
|
93
|
-
lines.push(" const qs = params.toString();");
|
|
94
|
-
lines.push(" if (qs) url += `?${qs}`;");
|
|
95
|
-
lines.push(" }");
|
|
96
|
-
lines.push(` const res = await customFetch(url, {`);
|
|
97
|
-
lines.push(` method: '${method.toUpperCase()}',`);
|
|
98
|
-
lines.push(" headers: { ...defaultHeaders, 'Content-Type': 'application/json' },");
|
|
99
|
-
lines.push(" body: body !== undefined ? JSON.stringify(body) : undefined,");
|
|
100
|
-
lines.push(" });");
|
|
101
|
-
lines.push(" if (!res.ok) throw await ApiError.from(res);");
|
|
102
|
-
lines.push(" if (res.status === 204) return undefined as T;");
|
|
103
|
-
lines.push(" return res.json();");
|
|
104
|
-
lines.push(" },");
|
|
105
|
-
lines.push("");
|
|
129
|
+
lines.push(` const client = new FetchClient({`);
|
|
130
|
+
lines.push(` baseURL: ${defaultBaseURL},`);
|
|
131
|
+
if (hasAuth) {
|
|
132
|
+
lines.push(" authStrategies: [");
|
|
133
|
+
for (const scheme of schemes) {
|
|
134
|
+
lines.push(...generateAuthStrategy(scheme));
|
|
135
|
+
}
|
|
136
|
+
lines.push(" ],");
|
|
106
137
|
}
|
|
107
|
-
lines.push("
|
|
108
|
-
lines.push("
|
|
109
|
-
lines.push(" if (opts?.query) {");
|
|
110
|
-
lines.push(" const params = new URLSearchParams();");
|
|
111
|
-
lines.push(" for (const [k, v] of Object.entries(opts.query)) { if (v != null) params.set(k, String(v)); }");
|
|
112
|
-
lines.push(" const qs = params.toString();");
|
|
113
|
-
lines.push(" if (qs) url += `?${qs}`;");
|
|
114
|
-
lines.push(" }");
|
|
115
|
-
lines.push(" const res = await customFetch(url, {");
|
|
116
|
-
lines.push(" method: 'DELETE',");
|
|
117
|
-
lines.push(" headers: defaultHeaders,");
|
|
118
|
-
lines.push(" });");
|
|
119
|
-
lines.push(" if (!res.ok) throw await ApiError.from(res);");
|
|
120
|
-
lines.push(" if (res.status === 204) return undefined as T;");
|
|
121
|
-
lines.push(" return res.json();");
|
|
122
|
-
lines.push(" },");
|
|
123
|
-
lines.push(" };");
|
|
138
|
+
lines.push(" ...options,");
|
|
139
|
+
lines.push(" });");
|
|
124
140
|
lines.push("");
|
|
125
141
|
lines.push(" return {");
|
|
126
142
|
for (const r of resources) {
|
|
@@ -129,21 +145,6 @@ function generateClient(resources, config) {
|
|
|
129
145
|
lines.push(" };");
|
|
130
146
|
lines.push("}");
|
|
131
147
|
lines.push("");
|
|
132
|
-
lines.push("export class ApiError extends Error {");
|
|
133
|
-
lines.push(" override name = 'ApiError';");
|
|
134
|
-
lines.push(" public data: unknown;");
|
|
135
|
-
lines.push("");
|
|
136
|
-
lines.push(" constructor(public status: number, body: string) {");
|
|
137
|
-
lines.push(" super(`API error ${status}: ${body}`);");
|
|
138
|
-
lines.push(" try { this.data = JSON.parse(body); } catch { this.data = body; }");
|
|
139
|
-
lines.push(" }");
|
|
140
|
-
lines.push("");
|
|
141
|
-
lines.push(" static async from(res: Response): Promise<ApiError> {");
|
|
142
|
-
lines.push(" const body = await res.text();");
|
|
143
|
-
lines.push(" return new ApiError(res.status, body);");
|
|
144
|
-
lines.push(" }");
|
|
145
|
-
lines.push("}");
|
|
146
|
-
lines.push("");
|
|
147
148
|
lines.push("export type Client = ReturnType<typeof createClient>;");
|
|
148
149
|
lines.push("");
|
|
149
150
|
return { path: "client.ts", content: lines.join(`
|
|
@@ -249,6 +250,14 @@ var VALID_IDENTIFIER = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/;
|
|
|
249
250
|
function isValidIdentifier(name) {
|
|
250
251
|
return VALID_IDENTIFIER.test(name);
|
|
251
252
|
}
|
|
253
|
+
function toPascalCase(name) {
|
|
254
|
+
const cleaned = name.replace(/[^A-Za-z0-9]+/g, " ").trim();
|
|
255
|
+
if (!cleaned)
|
|
256
|
+
return "_";
|
|
257
|
+
const segments = cleaned.split(/\s+/).filter(Boolean);
|
|
258
|
+
const result = segments.map((s) => s.charAt(0).toUpperCase() + s.slice(1)).join("");
|
|
259
|
+
return /^[0-9]/.test(result) ? `_${result}` : result;
|
|
260
|
+
}
|
|
252
261
|
|
|
253
262
|
// src/generators/resource-generator.ts
|
|
254
263
|
function generateResources(resources) {
|
|
@@ -268,13 +277,13 @@ function generateResources(resources) {
|
|
|
268
277
|
function generateResourceFile(resource) {
|
|
269
278
|
const lines = [];
|
|
270
279
|
const typeImports = collectTypeImports(resource);
|
|
271
|
-
lines.push("import type {
|
|
280
|
+
lines.push("import type { FetchClient, FetchResponse } from '@vertz/fetch';");
|
|
272
281
|
if (typeImports.size > 0) {
|
|
273
282
|
const sorted = [...typeImports].sort();
|
|
274
283
|
lines.push(`import type { ${sorted.join(", ")} } from '../types/${resource.identifier}';`);
|
|
275
284
|
}
|
|
276
285
|
lines.push("");
|
|
277
|
-
lines.push(`export function create${resource.name}Resource(client:
|
|
286
|
+
lines.push(`export function create${resource.name}Resource(client: FetchClient) {`);
|
|
278
287
|
lines.push(" return {");
|
|
279
288
|
validateUniqueMethodNames(resource);
|
|
280
289
|
for (const op of resource.operations) {
|
|
@@ -330,22 +339,22 @@ function buildParams(op) {
|
|
|
330
339
|
}
|
|
331
340
|
function buildReturnType(op) {
|
|
332
341
|
if (op.responseStatus === 204)
|
|
333
|
-
return "Promise<void
|
|
342
|
+
return "Promise<FetchResponse<void>>";
|
|
334
343
|
if (op.response?.name) {
|
|
335
344
|
const safeName = sanitizeTypeName(op.response.name);
|
|
336
345
|
if (op.response.jsonSchema.type === "array") {
|
|
337
|
-
return `Promise<${safeName}[]
|
|
346
|
+
return `Promise<FetchResponse<${safeName}[]>>`;
|
|
338
347
|
}
|
|
339
|
-
return `Promise<${safeName}
|
|
348
|
+
return `Promise<FetchResponse<${safeName}>>`;
|
|
340
349
|
}
|
|
341
350
|
if (op.response?.jsonSchema.type === "array") {
|
|
342
|
-
return "Promise<unknown[]
|
|
351
|
+
return "Promise<FetchResponse<unknown[]>>";
|
|
343
352
|
}
|
|
344
353
|
if (op.response) {
|
|
345
|
-
const name =
|
|
346
|
-
return `Promise<${name}
|
|
354
|
+
const name = toPascalCase(op.operationId) + "Response";
|
|
355
|
+
return `Promise<FetchResponse<${name}>>`;
|
|
347
356
|
}
|
|
348
|
-
return "Promise<void
|
|
357
|
+
return "Promise<FetchResponse<void>>";
|
|
349
358
|
}
|
|
350
359
|
function buildCall(op) {
|
|
351
360
|
const method = op.method.toLowerCase();
|
|
@@ -376,7 +385,7 @@ function collectTypeImports(resource) {
|
|
|
376
385
|
if (op.response.name) {
|
|
377
386
|
types.add(sanitizeTypeName(op.response.name));
|
|
378
387
|
} else {
|
|
379
|
-
types.add(
|
|
388
|
+
types.add(toPascalCase(op.operationId) + "Response");
|
|
380
389
|
}
|
|
381
390
|
}
|
|
382
391
|
if (op.requestBody) {
|
|
@@ -391,13 +400,10 @@ function collectTypeImports(resource) {
|
|
|
391
400
|
function deriveInputName(op) {
|
|
392
401
|
if (op.requestBody?.name)
|
|
393
402
|
return sanitizeTypeName(op.requestBody.name);
|
|
394
|
-
return
|
|
403
|
+
return toPascalCase(op.operationId) + "Input";
|
|
395
404
|
}
|
|
396
405
|
function deriveQueryName(op) {
|
|
397
|
-
return
|
|
398
|
-
}
|
|
399
|
-
function capitalize(s) {
|
|
400
|
-
return s.charAt(0).toUpperCase() + s.slice(1);
|
|
406
|
+
return toPascalCase(op.operationId) + "Query";
|
|
401
407
|
}
|
|
402
408
|
|
|
403
409
|
// src/generators/json-schema-to-zod.ts
|
|
@@ -615,18 +621,19 @@ ${entries.join(`,
|
|
|
615
621
|
function deriveResponseSchemaName(op) {
|
|
616
622
|
if (op.response?.name)
|
|
617
623
|
return toSchemaVarName(op.response.name);
|
|
618
|
-
return toSchemaVarName(op.operationId + "Response");
|
|
624
|
+
return toSchemaVarName(toPascalCase(op.operationId) + "Response");
|
|
619
625
|
}
|
|
620
626
|
function deriveInputSchemaName(op) {
|
|
621
627
|
if (op.requestBody?.name)
|
|
622
628
|
return toSchemaVarName(op.requestBody.name);
|
|
623
|
-
return toSchemaVarName(op.operationId + "Input");
|
|
629
|
+
return toSchemaVarName(toPascalCase(op.operationId) + "Input");
|
|
624
630
|
}
|
|
625
631
|
function deriveQuerySchemaName(op) {
|
|
626
|
-
return toSchemaVarName(op.operationId + "Query");
|
|
632
|
+
return toSchemaVarName(toPascalCase(op.operationId) + "Query");
|
|
627
633
|
}
|
|
628
634
|
function toSchemaVarName(name) {
|
|
629
|
-
const
|
|
635
|
+
const sanitized = sanitizeTypeName(name);
|
|
636
|
+
const camel = sanitized.charAt(0).toLowerCase() + sanitized.slice(1);
|
|
630
637
|
return camel + "Schema";
|
|
631
638
|
}
|
|
632
639
|
|
|
@@ -687,15 +694,15 @@ function generateResourceTypes(resource, namedSchemas) {
|
|
|
687
694
|
function deriveResponseName(op) {
|
|
688
695
|
if (op.response?.name)
|
|
689
696
|
return sanitizeTypeName(op.response.name);
|
|
690
|
-
return
|
|
697
|
+
return toPascalCase(op.operationId) + "Response";
|
|
691
698
|
}
|
|
692
699
|
function deriveInputName2(op) {
|
|
693
700
|
if (op.requestBody?.name)
|
|
694
701
|
return sanitizeTypeName(op.requestBody.name);
|
|
695
|
-
return
|
|
702
|
+
return toPascalCase(op.operationId) + "Input";
|
|
696
703
|
}
|
|
697
704
|
function deriveQueryName2(op) {
|
|
698
|
-
return
|
|
705
|
+
return toPascalCase(op.operationId) + "Query";
|
|
699
706
|
}
|
|
700
707
|
function generateQueryInterface(name, op, namedSchemas) {
|
|
701
708
|
const lines = op.queryParams.map((param) => {
|
|
@@ -710,9 +717,6 @@ ${lines.join(`
|
|
|
710
717
|
}
|
|
711
718
|
`;
|
|
712
719
|
}
|
|
713
|
-
function capitalize2(s) {
|
|
714
|
-
return s.charAt(0).toUpperCase() + s.slice(1);
|
|
715
|
-
}
|
|
716
720
|
|
|
717
721
|
// src/generators/index.ts
|
|
718
722
|
function generateAll(spec, options) {
|
|
@@ -721,7 +725,10 @@ function generateAll(spec, options) {
|
|
|
721
725
|
const files = [];
|
|
722
726
|
files.push(...generateTypes(resources, schemas));
|
|
723
727
|
files.push(...generateResources(resources));
|
|
724
|
-
files.push(generateClient(resources, {
|
|
728
|
+
files.push(generateClient(resources, {
|
|
729
|
+
baseURL: opts.baseURL,
|
|
730
|
+
securitySchemes: spec.securitySchemes
|
|
731
|
+
}));
|
|
725
732
|
if (opts.schemas) {
|
|
726
733
|
files.push(...generateSchemas(resources, schemas));
|
|
727
734
|
}
|
|
@@ -734,10 +741,19 @@ function generateReadme(spec, options) {
|
|
|
734
741
|
lines.push("");
|
|
735
742
|
lines.push(`> Auto-generated from OpenAPI ${spec.version} spec (v${spec.info.version})`);
|
|
736
743
|
lines.push("");
|
|
744
|
+
lines.push("## Prerequisites");
|
|
745
|
+
lines.push("");
|
|
746
|
+
lines.push("This SDK requires `@vertz/fetch` as a peer dependency:");
|
|
747
|
+
lines.push("");
|
|
748
|
+
lines.push("```bash");
|
|
749
|
+
lines.push("bun add @vertz/fetch");
|
|
750
|
+
lines.push("```");
|
|
751
|
+
lines.push("");
|
|
737
752
|
lines.push("## Usage");
|
|
738
753
|
lines.push("");
|
|
739
754
|
lines.push("```typescript");
|
|
740
755
|
lines.push("import { createClient } from './client';");
|
|
756
|
+
lines.push("import { isOk } from '@vertz/fetch';");
|
|
741
757
|
lines.push("");
|
|
742
758
|
lines.push(`const api = createClient(${options.baseURL ? `{ baseURL: '${options.baseURL}' }` : ""});`);
|
|
743
759
|
lines.push("```");
|
|
@@ -1212,6 +1228,64 @@ function collectComponentSchemas(spec, version) {
|
|
|
1212
1228
|
name
|
|
1213
1229
|
}));
|
|
1214
1230
|
}
|
|
1231
|
+
function extractSecuritySchemes(spec) {
|
|
1232
|
+
const components = isRecord2(spec.components) ? spec.components : undefined;
|
|
1233
|
+
const schemes = components && isRecord2(components.securitySchemes) ? components.securitySchemes : undefined;
|
|
1234
|
+
if (!schemes)
|
|
1235
|
+
return [];
|
|
1236
|
+
const result = [];
|
|
1237
|
+
for (const [name, scheme] of Object.entries(schemes)) {
|
|
1238
|
+
if (!isRecord2(scheme))
|
|
1239
|
+
continue;
|
|
1240
|
+
const description = typeof scheme.description === "string" ? scheme.description : undefined;
|
|
1241
|
+
if (scheme.type === "http") {
|
|
1242
|
+
if (scheme.scheme === "bearer") {
|
|
1243
|
+
result.push({ type: "bearer", name, description });
|
|
1244
|
+
} else if (scheme.scheme === "basic") {
|
|
1245
|
+
result.push({ type: "basic", name, description });
|
|
1246
|
+
}
|
|
1247
|
+
} else if (scheme.type === "apiKey") {
|
|
1248
|
+
const location = scheme.in;
|
|
1249
|
+
const paramName = typeof scheme.name === "string" ? scheme.name : name;
|
|
1250
|
+
result.push({ type: "apiKey", name, in: location, paramName, description });
|
|
1251
|
+
} else if (scheme.type === "oauth2" && isRecord2(scheme.flows)) {
|
|
1252
|
+
const flows = {};
|
|
1253
|
+
const rawFlows = scheme.flows;
|
|
1254
|
+
if (isRecord2(rawFlows.authorizationCode)) {
|
|
1255
|
+
const ac = rawFlows.authorizationCode;
|
|
1256
|
+
flows.authorizationCode = {
|
|
1257
|
+
authorizationUrl: String(ac.authorizationUrl ?? ""),
|
|
1258
|
+
tokenUrl: String(ac.tokenUrl ?? ""),
|
|
1259
|
+
scopes: isRecord2(ac.scopes) ? ac.scopes : {}
|
|
1260
|
+
};
|
|
1261
|
+
}
|
|
1262
|
+
if (isRecord2(rawFlows.clientCredentials)) {
|
|
1263
|
+
const cc = rawFlows.clientCredentials;
|
|
1264
|
+
flows.clientCredentials = {
|
|
1265
|
+
tokenUrl: String(cc.tokenUrl ?? ""),
|
|
1266
|
+
scopes: isRecord2(cc.scopes) ? cc.scopes : {}
|
|
1267
|
+
};
|
|
1268
|
+
}
|
|
1269
|
+
result.push({ type: "oauth2", name, flows, description });
|
|
1270
|
+
}
|
|
1271
|
+
}
|
|
1272
|
+
return result;
|
|
1273
|
+
}
|
|
1274
|
+
function extractOperationSecurity(operation, globalSecurity) {
|
|
1275
|
+
const security = Array.isArray(operation.security) ? operation.security : globalSecurity;
|
|
1276
|
+
if (security.length === 0 && !Array.isArray(operation.security))
|
|
1277
|
+
return;
|
|
1278
|
+
const schemes = [];
|
|
1279
|
+
for (const requirement of security) {
|
|
1280
|
+
if (isRecord2(requirement)) {
|
|
1281
|
+
schemes.push(...Object.keys(requirement));
|
|
1282
|
+
}
|
|
1283
|
+
}
|
|
1284
|
+
return {
|
|
1285
|
+
required: schemes.length > 0,
|
|
1286
|
+
schemes
|
|
1287
|
+
};
|
|
1288
|
+
}
|
|
1215
1289
|
function parseOpenAPI(spec) {
|
|
1216
1290
|
const version = getVersion(spec);
|
|
1217
1291
|
if (!isRecord2(spec.info)) {
|
|
@@ -1220,6 +1294,7 @@ function parseOpenAPI(spec) {
|
|
|
1220
1294
|
if (!isRecord2(spec.paths)) {
|
|
1221
1295
|
throw new OpenAPIParserError2("OpenAPI spec is missing required field: paths");
|
|
1222
1296
|
}
|
|
1297
|
+
const globalSecurity = Array.isArray(spec.security) ? spec.security : [];
|
|
1223
1298
|
const operations = [];
|
|
1224
1299
|
for (const [path, pathItem] of Object.entries(spec.paths)) {
|
|
1225
1300
|
if (!isRecord2(pathItem)) {
|
|
@@ -1233,7 +1308,8 @@ function parseOpenAPI(spec) {
|
|
|
1233
1308
|
const operationId = typeof operation.operationId === "string" ? operation.operationId : `${method}_${path}`;
|
|
1234
1309
|
const { pathParams, queryParams } = extractParameters(path, pathItem, operation, spec, version);
|
|
1235
1310
|
const successResponse = pickSuccessResponse(operation, spec);
|
|
1236
|
-
|
|
1311
|
+
const security = extractOperationSecurity(operation, globalSecurity);
|
|
1312
|
+
const parsed = {
|
|
1237
1313
|
operationId,
|
|
1238
1314
|
methodName: normalizeOperationId(operationId, method.toUpperCase(), path),
|
|
1239
1315
|
method: method.toUpperCase(),
|
|
@@ -1244,10 +1320,18 @@ function parseOpenAPI(spec) {
|
|
|
1244
1320
|
response: successResponse.schema ? resolveSchemaForOutput(successResponse.schema, spec, version) : undefined,
|
|
1245
1321
|
responseStatus: successResponse.status,
|
|
1246
1322
|
tags: Array.isArray(operation.tags) ? operation.tags.filter((tag) => typeof tag === "string") : []
|
|
1247
|
-
}
|
|
1323
|
+
};
|
|
1324
|
+
if (security)
|
|
1325
|
+
parsed.security = security;
|
|
1326
|
+
operations.push(parsed);
|
|
1248
1327
|
}
|
|
1249
1328
|
}
|
|
1250
|
-
return {
|
|
1329
|
+
return {
|
|
1330
|
+
operations,
|
|
1331
|
+
schemas: collectComponentSchemas(spec, version),
|
|
1332
|
+
securitySchemes: extractSecuritySchemes(spec),
|
|
1333
|
+
version
|
|
1334
|
+
};
|
|
1251
1335
|
}
|
|
1252
1336
|
|
|
1253
1337
|
// src/writer/incremental.ts
|
|
@@ -1358,7 +1442,8 @@ async function generateFromOpenAPI(config) {
|
|
|
1358
1442
|
version: typeof info?.version === "string" ? info.version : "0.0.0"
|
|
1359
1443
|
},
|
|
1360
1444
|
resources,
|
|
1361
|
-
schemas: parsed.schemas
|
|
1445
|
+
schemas: parsed.schemas,
|
|
1446
|
+
securitySchemes: parsed.securitySchemes
|
|
1362
1447
|
};
|
|
1363
1448
|
const files = generateAll(spec, {
|
|
1364
1449
|
schemas: config.schemas,
|