@vertz/openapi 0.1.1 → 0.1.2

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 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
- const tasks = await api.tasks.list();
114
- const task = await api.tasks.get(taskId);
115
- const created = await api.tasks.create({ title: 'New task' });
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/dist/cli.js CHANGED
@@ -4,7 +4,7 @@ import {
4
4
  loadSpec,
5
5
  parseOpenAPI,
6
6
  resolveConfig
7
- } from "./shared/chunk-er3k8t3a.js";
7
+ } from "./shared/chunk-h8tb765a.js";
8
8
 
9
9
  // src/cli.ts
10
10
  function parseArgs(args) {
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
@@ -21,7 +21,7 @@ import {
21
21
  sanitizeIdentifier,
22
22
  sanitizeTypeName,
23
23
  writeIncremental
24
- } from "./shared/chunk-er3k8t3a.js";
24
+ } from "./shared/chunk-h8tb765a.js";
25
25
  export {
26
26
  writeIncremental,
27
27
  sanitizeTypeName,
@@ -41,86 +41,93 @@ function groupOperations(operations, strategy, options) {
41
41
  }
42
42
 
43
43
  // src/generators/client-generator.ts
44
+ function camelCase(name) {
45
+ return name.charAt(0).toLowerCase() + name.slice(1);
46
+ }
47
+ function generateAuthField(scheme) {
48
+ switch (scheme.type) {
49
+ case "bearer":
50
+ return ` ${camelCase(scheme.name)}?: string | (() => string | Promise<string>);`;
51
+ case "basic":
52
+ return ` ${camelCase(scheme.name)}?: { username: string; password: string };`;
53
+ case "apiKey":
54
+ return ` ${camelCase(scheme.name)}?: string | (() => string | Promise<string>);`;
55
+ case "oauth2":
56
+ return ` ${camelCase(scheme.name)}?: string | (() => string | Promise<string>);`;
57
+ }
58
+ }
59
+ function generateAuthStrategy(scheme) {
60
+ const fieldName = camelCase(scheme.name);
61
+ const lines = [];
62
+ switch (scheme.type) {
63
+ case "bearer":
64
+ lines.push(` ...(options.auth?.${fieldName} ? [{`);
65
+ lines.push(` type: 'bearer' as const,`);
66
+ lines.push(` token: options.auth?.${fieldName},`);
67
+ lines.push(" }] : []),");
68
+ break;
69
+ case "basic":
70
+ lines.push(` ...(options.auth?.${fieldName} ? [{`);
71
+ lines.push(` type: 'basic' as const,`);
72
+ lines.push(` ...options.auth?.${fieldName},`);
73
+ lines.push(" }] : []),");
74
+ break;
75
+ case "apiKey":
76
+ lines.push(` ...(options.auth?.${fieldName} ? [{`);
77
+ lines.push(` type: 'apiKey' as const,`);
78
+ lines.push(` key: options.auth?.${fieldName},`);
79
+ lines.push(` location: '${scheme.in}' as const,`);
80
+ lines.push(` name: '${scheme.paramName}',`);
81
+ lines.push(" }] : []),");
82
+ break;
83
+ case "oauth2":
84
+ lines.push(` ...(options.auth?.${fieldName} ? [{`);
85
+ lines.push(` type: 'bearer' as const,`);
86
+ lines.push(` token: options.auth?.${fieldName},`);
87
+ lines.push(" }] : []),");
88
+ break;
89
+ }
90
+ return lines;
91
+ }
44
92
  function generateClient(resources, config) {
45
93
  const defaultBaseURL = config.baseURL ? `'${config.baseURL.replace(/'/g, "\\'")}'` : "''";
94
+ const schemes = config.securitySchemes ?? [];
95
+ const hasAuth = schemes.length > 0;
46
96
  const lines = [];
47
97
  lines.push("// Generated by @vertz/openapi — do not edit");
48
98
  lines.push("");
99
+ lines.push("import { FetchClient } from '@vertz/fetch';");
100
+ lines.push("import type { FetchClientConfig } from '@vertz/fetch';");
49
101
  for (const r of resources) {
50
102
  lines.push(`import { create${r.name}Resource } from './resources/${r.identifier}';`);
51
103
  }
52
104
  lines.push("");
53
- lines.push("export interface HttpClient {");
54
- lines.push(" get<T>(path: string, options?: { query?: Record<string, unknown> }): Promise<T>;");
55
- lines.push(" post<T>(path: string, body?: unknown, options?: { query?: Record<string, unknown> }): Promise<T>;");
56
- lines.push(" put<T>(path: string, body?: unknown, options?: { query?: Record<string, unknown> }): Promise<T>;");
57
- lines.push(" patch<T>(path: string, body?: unknown, options?: { query?: Record<string, unknown> }): Promise<T>;");
58
- lines.push(" delete<T>(path: string, options?: { query?: Record<string, unknown> }): Promise<T>;");
59
- lines.push("}");
60
- lines.push("");
61
- lines.push("export interface ClientOptions {");
62
- lines.push(" baseURL?: string;");
63
- lines.push(" headers?: Record<string, string>;");
64
- lines.push(" fetch?: typeof globalThis.fetch;");
65
- lines.push("}");
105
+ if (hasAuth) {
106
+ lines.push("export interface ClientAuth {");
107
+ for (const scheme of schemes) {
108
+ lines.push(generateAuthField(scheme));
109
+ }
110
+ lines.push("}");
111
+ lines.push("");
112
+ }
113
+ if (hasAuth) {
114
+ lines.push("export type ClientOptions = FetchClientConfig & { auth?: ClientAuth };");
115
+ } else {
116
+ lines.push("export type ClientOptions = FetchClientConfig;");
117
+ }
66
118
  lines.push("");
67
119
  lines.push("export function createClient(options: ClientOptions = {}) {");
68
- lines.push(` const baseURL = (options.baseURL ?? ${defaultBaseURL}).replace(/\\/$/, '');`);
69
- lines.push(" const customFetch = options.fetch ?? globalThis.fetch.bind(globalThis);");
70
- lines.push(" const defaultHeaders = options.headers ?? {};");
71
- lines.push("");
72
- lines.push(" const client: HttpClient = {");
73
- lines.push(" async get<T>(path: string, opts?: { query?: Record<string, unknown> }): Promise<T> {");
74
- lines.push(" let url = `${baseURL}${path}`;");
75
- lines.push(" if (opts?.query) {");
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("");
120
+ lines.push(` const client = new FetchClient({`);
121
+ lines.push(` baseURL: ${defaultBaseURL},`);
122
+ if (hasAuth) {
123
+ lines.push(" authStrategies: [");
124
+ for (const scheme of schemes) {
125
+ lines.push(...generateAuthStrategy(scheme));
126
+ }
127
+ lines.push(" ],");
106
128
  }
107
- lines.push(" async delete<T>(path: string, opts?: { query?: Record<string, unknown> }): Promise<T> {");
108
- lines.push(" let url = `${baseURL}${path}`;");
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(" };");
129
+ lines.push(" ...options,");
130
+ lines.push(" });");
124
131
  lines.push("");
125
132
  lines.push(" return {");
126
133
  for (const r of resources) {
@@ -129,21 +136,6 @@ function generateClient(resources, config) {
129
136
  lines.push(" };");
130
137
  lines.push("}");
131
138
  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
139
  lines.push("export type Client = ReturnType<typeof createClient>;");
148
140
  lines.push("");
149
141
  return { path: "client.ts", content: lines.join(`
@@ -268,13 +260,13 @@ function generateResources(resources) {
268
260
  function generateResourceFile(resource) {
269
261
  const lines = [];
270
262
  const typeImports = collectTypeImports(resource);
271
- lines.push("import type { HttpClient } from '../client';");
263
+ lines.push("import type { FetchClient, FetchResponse } from '@vertz/fetch';");
272
264
  if (typeImports.size > 0) {
273
265
  const sorted = [...typeImports].sort();
274
266
  lines.push(`import type { ${sorted.join(", ")} } from '../types/${resource.identifier}';`);
275
267
  }
276
268
  lines.push("");
277
- lines.push(`export function create${resource.name}Resource(client: HttpClient) {`);
269
+ lines.push(`export function create${resource.name}Resource(client: FetchClient) {`);
278
270
  lines.push(" return {");
279
271
  validateUniqueMethodNames(resource);
280
272
  for (const op of resource.operations) {
@@ -330,22 +322,22 @@ function buildParams(op) {
330
322
  }
331
323
  function buildReturnType(op) {
332
324
  if (op.responseStatus === 204)
333
- return "Promise<void>";
325
+ return "Promise<FetchResponse<void>>";
334
326
  if (op.response?.name) {
335
327
  const safeName = sanitizeTypeName(op.response.name);
336
328
  if (op.response.jsonSchema.type === "array") {
337
- return `Promise<${safeName}[]>`;
329
+ return `Promise<FetchResponse<${safeName}[]>>`;
338
330
  }
339
- return `Promise<${safeName}>`;
331
+ return `Promise<FetchResponse<${safeName}>>`;
340
332
  }
341
333
  if (op.response?.jsonSchema.type === "array") {
342
- return "Promise<unknown[]>";
334
+ return "Promise<FetchResponse<unknown[]>>";
343
335
  }
344
336
  if (op.response) {
345
337
  const name = capitalize(op.operationId) + "Response";
346
- return `Promise<${name}>`;
338
+ return `Promise<FetchResponse<${name}>>`;
347
339
  }
348
- return "Promise<void>";
340
+ return "Promise<FetchResponse<void>>";
349
341
  }
350
342
  function buildCall(op) {
351
343
  const method = op.method.toLowerCase();
@@ -721,7 +713,10 @@ function generateAll(spec, options) {
721
713
  const files = [];
722
714
  files.push(...generateTypes(resources, schemas));
723
715
  files.push(...generateResources(resources));
724
- files.push(generateClient(resources, { baseURL: opts.baseURL }));
716
+ files.push(generateClient(resources, {
717
+ baseURL: opts.baseURL,
718
+ securitySchemes: spec.securitySchemes
719
+ }));
725
720
  if (opts.schemas) {
726
721
  files.push(...generateSchemas(resources, schemas));
727
722
  }
@@ -734,10 +729,19 @@ function generateReadme(spec, options) {
734
729
  lines.push("");
735
730
  lines.push(`> Auto-generated from OpenAPI ${spec.version} spec (v${spec.info.version})`);
736
731
  lines.push("");
732
+ lines.push("## Prerequisites");
733
+ lines.push("");
734
+ lines.push("This SDK requires `@vertz/fetch` as a peer dependency:");
735
+ lines.push("");
736
+ lines.push("```bash");
737
+ lines.push("bun add @vertz/fetch");
738
+ lines.push("```");
739
+ lines.push("");
737
740
  lines.push("## Usage");
738
741
  lines.push("");
739
742
  lines.push("```typescript");
740
743
  lines.push("import { createClient } from './client';");
744
+ lines.push("import { isOk } from '@vertz/fetch';");
741
745
  lines.push("");
742
746
  lines.push(`const api = createClient(${options.baseURL ? `{ baseURL: '${options.baseURL}' }` : ""});`);
743
747
  lines.push("```");
@@ -1212,6 +1216,64 @@ function collectComponentSchemas(spec, version) {
1212
1216
  name
1213
1217
  }));
1214
1218
  }
1219
+ function extractSecuritySchemes(spec) {
1220
+ const components = isRecord2(spec.components) ? spec.components : undefined;
1221
+ const schemes = components && isRecord2(components.securitySchemes) ? components.securitySchemes : undefined;
1222
+ if (!schemes)
1223
+ return [];
1224
+ const result = [];
1225
+ for (const [name, scheme] of Object.entries(schemes)) {
1226
+ if (!isRecord2(scheme))
1227
+ continue;
1228
+ const description = typeof scheme.description === "string" ? scheme.description : undefined;
1229
+ if (scheme.type === "http") {
1230
+ if (scheme.scheme === "bearer") {
1231
+ result.push({ type: "bearer", name, description });
1232
+ } else if (scheme.scheme === "basic") {
1233
+ result.push({ type: "basic", name, description });
1234
+ }
1235
+ } else if (scheme.type === "apiKey") {
1236
+ const location = scheme.in;
1237
+ const paramName = typeof scheme.name === "string" ? scheme.name : name;
1238
+ result.push({ type: "apiKey", name, in: location, paramName, description });
1239
+ } else if (scheme.type === "oauth2" && isRecord2(scheme.flows)) {
1240
+ const flows = {};
1241
+ const rawFlows = scheme.flows;
1242
+ if (isRecord2(rawFlows.authorizationCode)) {
1243
+ const ac = rawFlows.authorizationCode;
1244
+ flows.authorizationCode = {
1245
+ authorizationUrl: String(ac.authorizationUrl ?? ""),
1246
+ tokenUrl: String(ac.tokenUrl ?? ""),
1247
+ scopes: isRecord2(ac.scopes) ? ac.scopes : {}
1248
+ };
1249
+ }
1250
+ if (isRecord2(rawFlows.clientCredentials)) {
1251
+ const cc = rawFlows.clientCredentials;
1252
+ flows.clientCredentials = {
1253
+ tokenUrl: String(cc.tokenUrl ?? ""),
1254
+ scopes: isRecord2(cc.scopes) ? cc.scopes : {}
1255
+ };
1256
+ }
1257
+ result.push({ type: "oauth2", name, flows, description });
1258
+ }
1259
+ }
1260
+ return result;
1261
+ }
1262
+ function extractOperationSecurity(operation, globalSecurity) {
1263
+ const security = Array.isArray(operation.security) ? operation.security : globalSecurity;
1264
+ if (security.length === 0 && !Array.isArray(operation.security))
1265
+ return;
1266
+ const schemes = [];
1267
+ for (const requirement of security) {
1268
+ if (isRecord2(requirement)) {
1269
+ schemes.push(...Object.keys(requirement));
1270
+ }
1271
+ }
1272
+ return {
1273
+ required: schemes.length > 0,
1274
+ schemes
1275
+ };
1276
+ }
1215
1277
  function parseOpenAPI(spec) {
1216
1278
  const version = getVersion(spec);
1217
1279
  if (!isRecord2(spec.info)) {
@@ -1220,6 +1282,7 @@ function parseOpenAPI(spec) {
1220
1282
  if (!isRecord2(spec.paths)) {
1221
1283
  throw new OpenAPIParserError2("OpenAPI spec is missing required field: paths");
1222
1284
  }
1285
+ const globalSecurity = Array.isArray(spec.security) ? spec.security : [];
1223
1286
  const operations = [];
1224
1287
  for (const [path, pathItem] of Object.entries(spec.paths)) {
1225
1288
  if (!isRecord2(pathItem)) {
@@ -1233,7 +1296,8 @@ function parseOpenAPI(spec) {
1233
1296
  const operationId = typeof operation.operationId === "string" ? operation.operationId : `${method}_${path}`;
1234
1297
  const { pathParams, queryParams } = extractParameters(path, pathItem, operation, spec, version);
1235
1298
  const successResponse = pickSuccessResponse(operation, spec);
1236
- operations.push({
1299
+ const security = extractOperationSecurity(operation, globalSecurity);
1300
+ const parsed = {
1237
1301
  operationId,
1238
1302
  methodName: normalizeOperationId(operationId, method.toUpperCase(), path),
1239
1303
  method: method.toUpperCase(),
@@ -1244,10 +1308,18 @@ function parseOpenAPI(spec) {
1244
1308
  response: successResponse.schema ? resolveSchemaForOutput(successResponse.schema, spec, version) : undefined,
1245
1309
  responseStatus: successResponse.status,
1246
1310
  tags: Array.isArray(operation.tags) ? operation.tags.filter((tag) => typeof tag === "string") : []
1247
- });
1311
+ };
1312
+ if (security)
1313
+ parsed.security = security;
1314
+ operations.push(parsed);
1248
1315
  }
1249
1316
  }
1250
- return { operations, schemas: collectComponentSchemas(spec, version), version };
1317
+ return {
1318
+ operations,
1319
+ schemas: collectComponentSchemas(spec, version),
1320
+ securitySchemes: extractSecuritySchemes(spec),
1321
+ version
1322
+ };
1251
1323
  }
1252
1324
 
1253
1325
  // src/writer/incremental.ts
@@ -1358,7 +1430,8 @@ async function generateFromOpenAPI(config) {
1358
1430
  version: typeof info?.version === "string" ? info.version : "0.0.0"
1359
1431
  },
1360
1432
  resources,
1361
- schemas: parsed.schemas
1433
+ schemas: parsed.schemas,
1434
+ securitySchemes: parsed.securitySchemes
1362
1435
  };
1363
1436
  const files = generateAll(spec, {
1364
1437
  schemas: config.schemas,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vertz/openapi",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
4
4
  "description": "OpenAPI 3.x parser and TypeScript SDK generator for Vertz",
5
5
  "license": "MIT",
6
6
  "repository": {