klasik 1.0.11 → 1.0.13
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 +116 -2
- package/USAGE_EXAMPLES.md +160 -0
- package/dist/cli.js +4 -0
- package/dist/k8s-client-generator.d.ts +5 -0
- package/dist/k8s-client-generator.js +2 -1
- package/dist/spec-downloader.d.ts +31 -0
- package/dist/spec-downloader.js +172 -2
- package/package.json +4 -2
- package/test-discriminated-output/.openapi-generator-ignore +23 -0
- package/test-discriminated-output/api/default-api.ts +142 -0
- package/test-discriminated-output/api.ts +19 -0
- package/test-discriminated-output/base.ts +86 -0
- package/test-discriminated-output/common.ts +151 -0
- package/test-discriminated-output/configuration.ts +152 -0
- package/test-discriminated-output/index.ts +18 -0
- package/test-discriminated-output/models/capability.js +80 -0
- package/test-discriminated-output/models/capability.ts +74 -0
- package/test-discriminated-output/models/helm-component.js +82 -0
- package/test-discriminated-output/models/helm-component.ts +83 -0
- package/test-discriminated-output/models/index.ts +4 -0
- package/test-discriminated-output/models/kustomize-component.js +70 -0
- package/test-discriminated-output/models/kustomize-component.ts +70 -0
- package/test-discriminated-output/models/manifest-component.js +58 -0
- package/test-discriminated-output/models/manifest-component.ts +57 -0
package/README.md
CHANGED
|
@@ -6,6 +6,8 @@ Download OpenAPI specifications from remote URLs and generate TypeScript clients
|
|
|
6
6
|
|
|
7
7
|
- 📥 **Download OpenAPI specs** from remote URLs or local files
|
|
8
8
|
- 📁 **Multiple input formats** - HTTP/HTTPS URLs, file:// URLs, absolute/relative paths
|
|
9
|
+
- 🔐 **Authentication support** - Custom headers including Bearer tokens and API keys
|
|
10
|
+
- 🔗 **External reference resolution** - Automatically download referenced schema files (`$ref`)
|
|
9
11
|
- 🔄 **Automatic transformation** - Converts API responses to class instances using class-transformer
|
|
10
12
|
- 🎯 **Type-safe** - Full TypeScript support with decorators
|
|
11
13
|
- 🛠️ **Configurable** - Custom headers, timeouts, and templates
|
|
@@ -81,6 +83,9 @@ Generate a TypeScript client from an OpenAPI spec (remote URL or local file).
|
|
|
81
83
|
- Supports: `https://...`, `http://...`, `file://...`, `/absolute/path`, `./relative/path`
|
|
82
84
|
- `-o, --output <dir>` - Output directory for generated client code (required)
|
|
83
85
|
- `-H, --header <header...>` - Custom headers for HTTP requests (format: "Key: Value")
|
|
86
|
+
- Can be used multiple times for multiple headers
|
|
87
|
+
- Perfect for authorization: `--header "Authorization: Bearer token"`
|
|
88
|
+
- `-r, --resolve-refs` - Resolve and download external `$ref` references
|
|
84
89
|
- `-t, --template <dir>` - Custom template directory
|
|
85
90
|
- `-k, --keep-spec` - Keep the downloaded spec file after generation
|
|
86
91
|
- `--timeout <ms>` - Request timeout in milliseconds for HTTP requests (default: 30000)
|
|
@@ -88,13 +93,27 @@ Generate a TypeScript client from an OpenAPI spec (remote URL or local file).
|
|
|
88
93
|
**Examples:**
|
|
89
94
|
|
|
90
95
|
```bash
|
|
91
|
-
# From remote URL
|
|
96
|
+
# From remote URL with authorization
|
|
92
97
|
klasik generate \
|
|
93
98
|
--url https://api.example.com/openapi.json \
|
|
94
99
|
--output ./client \
|
|
95
100
|
--header "Authorization: Bearer token123" \
|
|
96
101
|
--timeout 60000
|
|
97
102
|
|
|
103
|
+
# With external reference resolution
|
|
104
|
+
klasik generate \
|
|
105
|
+
--url https://api.example.com/openapi.json \
|
|
106
|
+
--output ./client \
|
|
107
|
+
--header "Authorization: Bearer token123" \
|
|
108
|
+
--resolve-refs
|
|
109
|
+
|
|
110
|
+
# Multiple headers
|
|
111
|
+
klasik generate \
|
|
112
|
+
--url https://api.example.com/openapi.json \
|
|
113
|
+
--output ./client \
|
|
114
|
+
--header "Authorization: Bearer token" \
|
|
115
|
+
--header "X-API-Version: v1"
|
|
116
|
+
|
|
98
117
|
# From local file
|
|
99
118
|
klasik generate \
|
|
100
119
|
--url ./my-spec.json \
|
|
@@ -114,15 +133,24 @@ Download an OpenAPI spec from a remote URL without generating code.
|
|
|
114
133
|
- `-u, --url <url>` - Remote URL to download the OpenAPI spec from (required)
|
|
115
134
|
- `-o, --output <file>` - Output file path for the downloaded spec (required)
|
|
116
135
|
- `-H, --header <header...>` - Custom headers for the request
|
|
136
|
+
- `-r, --resolve-refs` - Resolve and download external `$ref` references
|
|
117
137
|
- `--timeout <ms>` - Request timeout in milliseconds (default: 30000)
|
|
118
138
|
|
|
119
|
-
**
|
|
139
|
+
**Examples:**
|
|
120
140
|
|
|
121
141
|
```bash
|
|
142
|
+
# Download spec only
|
|
122
143
|
klasik download \
|
|
123
144
|
--url https://api.example.com/openapi.json \
|
|
124
145
|
--output ./specs/api-spec.json \
|
|
125
146
|
--header "Authorization: Bearer token123"
|
|
147
|
+
|
|
148
|
+
# Download spec with all external references
|
|
149
|
+
klasik download \
|
|
150
|
+
--url https://api.example.com/openapi.json \
|
|
151
|
+
--output ./specs/api-spec.json \
|
|
152
|
+
--header "Authorization: Bearer token123" \
|
|
153
|
+
--resolve-refs
|
|
126
154
|
```
|
|
127
155
|
|
|
128
156
|
## Programmatic API
|
|
@@ -141,6 +169,7 @@ await generator.generate({
|
|
|
141
169
|
'Authorization': 'Bearer token123',
|
|
142
170
|
'X-Custom-Header': 'value'
|
|
143
171
|
},
|
|
172
|
+
resolveReferences: true, // Download external $ref files
|
|
144
173
|
templateDir: './custom-templates', // Optional
|
|
145
174
|
keepSpec: true, // Keep downloaded spec file
|
|
146
175
|
timeout: 60000 // Request timeout in ms
|
|
@@ -160,6 +189,7 @@ const specPath = await downloader.download({
|
|
|
160
189
|
headers: {
|
|
161
190
|
'Authorization': 'Bearer token123'
|
|
162
191
|
},
|
|
192
|
+
resolveReferences: true, // Download external $ref files
|
|
163
193
|
timeout: 30000
|
|
164
194
|
});
|
|
165
195
|
|
|
@@ -235,6 +265,90 @@ const user = response.data; // user is instanceof User ✅
|
|
|
235
265
|
console.log(user.name); // Fully typed!
|
|
236
266
|
```
|
|
237
267
|
|
|
268
|
+
## External Reference Resolution
|
|
269
|
+
|
|
270
|
+
Klasik can automatically resolve and download external `$ref` references in your OpenAPI specs. This is useful when your spec splits schemas across multiple files.
|
|
271
|
+
|
|
272
|
+
### How It Works
|
|
273
|
+
|
|
274
|
+
When you enable `--resolve-refs` (CLI) or `resolveReferences: true` (programmatic), klasik will:
|
|
275
|
+
|
|
276
|
+
1. **Parse the main spec** for external `$ref` references
|
|
277
|
+
2. **Download all referenced files** preserving directory structure
|
|
278
|
+
3. **Recursively resolve nested references** in downloaded files
|
|
279
|
+
4. **Use the same authentication** for all downloads
|
|
280
|
+
|
|
281
|
+
### Example OpenAPI Spec with External References
|
|
282
|
+
|
|
283
|
+
```json
|
|
284
|
+
{
|
|
285
|
+
"openapi": "3.0.0",
|
|
286
|
+
"paths": {
|
|
287
|
+
"/users": {
|
|
288
|
+
"get": {
|
|
289
|
+
"responses": {
|
|
290
|
+
"200": {
|
|
291
|
+
"content": {
|
|
292
|
+
"application/json": {
|
|
293
|
+
"schema": {
|
|
294
|
+
"$ref": "./schemas/users-orgs.yaml#/components/schemas/Org"
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
With `--resolve-refs`, klasik will automatically download `./schemas/users-orgs.yaml` and any files it references.
|
|
307
|
+
|
|
308
|
+
### Output Structure
|
|
309
|
+
|
|
310
|
+
Referenced files are downloaded preserving the directory structure:
|
|
311
|
+
|
|
312
|
+
```
|
|
313
|
+
output/
|
|
314
|
+
├── openapi-spec.json # Main spec
|
|
315
|
+
└── schemas/
|
|
316
|
+
├── users-orgs.yaml # Referenced schema
|
|
317
|
+
└── common/
|
|
318
|
+
└── types.yaml # Nested reference
|
|
319
|
+
```
|
|
320
|
+
|
|
321
|
+
### Supported Reference Types
|
|
322
|
+
|
|
323
|
+
- **Relative paths**: `./schemas/user.yaml`, `../common/types.yaml`
|
|
324
|
+
- **Remote URLs**: `https://api.example.com/schemas/user.yaml`
|
|
325
|
+
- **Mixed**: Main spec from HTTPS, references can be relative or absolute
|
|
326
|
+
|
|
327
|
+
### CLI Example
|
|
328
|
+
|
|
329
|
+
```bash
|
|
330
|
+
# Download spec with all external references
|
|
331
|
+
npx klasik generate \
|
|
332
|
+
--url https://api.example.com/openapi.json \
|
|
333
|
+
--output ./client \
|
|
334
|
+
--header "Authorization: Bearer token" \
|
|
335
|
+
--resolve-refs
|
|
336
|
+
```
|
|
337
|
+
|
|
338
|
+
### Programmatic Example
|
|
339
|
+
|
|
340
|
+
```typescript
|
|
341
|
+
import { K8sClientGenerator } from 'klasik';
|
|
342
|
+
|
|
343
|
+
await new K8sClientGenerator().generate({
|
|
344
|
+
specUrl: 'https://api.example.com/openapi.json',
|
|
345
|
+
outputDir: './client',
|
|
346
|
+
headers: { 'Authorization': 'Bearer token' },
|
|
347
|
+
resolveReferences: true,
|
|
348
|
+
keepSpec: true // Keep all downloaded files
|
|
349
|
+
});
|
|
350
|
+
```
|
|
351
|
+
|
|
238
352
|
## Advanced Configuration
|
|
239
353
|
|
|
240
354
|
### Custom Error Handling
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
# Klasik Usage Examples
|
|
2
|
+
|
|
3
|
+
## Using Custom Headers (Authorization)
|
|
4
|
+
|
|
5
|
+
### CLI Example - Authorization Token
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
# Using Bearer token
|
|
9
|
+
npx klasik generate \
|
|
10
|
+
--url https://api.example.com/openapi.json \
|
|
11
|
+
--output ./client \
|
|
12
|
+
--header "Authorization: Bearer your-token-here"
|
|
13
|
+
|
|
14
|
+
# Using API key
|
|
15
|
+
npx klasik generate \
|
|
16
|
+
--url https://api.example.com/openapi.json \
|
|
17
|
+
--output ./client \
|
|
18
|
+
--header "X-API-Key: your-api-key"
|
|
19
|
+
|
|
20
|
+
# Multiple headers
|
|
21
|
+
npx klasik generate \
|
|
22
|
+
--url https://api.example.com/openapi.json \
|
|
23
|
+
--output ./client \
|
|
24
|
+
--header "Authorization: Bearer your-token" \
|
|
25
|
+
--header "X-Custom-Header: custom-value"
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
### Programmatic Example - Authorization Token
|
|
29
|
+
|
|
30
|
+
```typescript
|
|
31
|
+
import { K8sClientGenerator } from 'klasik';
|
|
32
|
+
|
|
33
|
+
const generator = new K8sClientGenerator();
|
|
34
|
+
|
|
35
|
+
await generator.generate({
|
|
36
|
+
specUrl: 'https://api.example.com/openapi.json',
|
|
37
|
+
outputDir: './client',
|
|
38
|
+
headers: {
|
|
39
|
+
'Authorization': 'Bearer your-token-here',
|
|
40
|
+
'X-API-Key': 'your-api-key'
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Resolving External References
|
|
46
|
+
|
|
47
|
+
### CLI Example - With Reference Resolution
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
# Download spec and all external $ref files
|
|
51
|
+
npx klasik generate \
|
|
52
|
+
--url https://api.example.com/openapi.json \
|
|
53
|
+
--output ./client \
|
|
54
|
+
--resolve-refs \
|
|
55
|
+
--header "Authorization: Bearer your-token"
|
|
56
|
+
|
|
57
|
+
# Download only (no code generation)
|
|
58
|
+
npx klasik download \
|
|
59
|
+
--url https://api.example.com/openapi.json \
|
|
60
|
+
--output ./specs/api-spec.json \
|
|
61
|
+
--resolve-refs \
|
|
62
|
+
--header "Authorization: Bearer your-token"
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
### Programmatic Example - With Reference Resolution
|
|
66
|
+
|
|
67
|
+
```typescript
|
|
68
|
+
import { K8sClientGenerator } from 'klasik';
|
|
69
|
+
|
|
70
|
+
const generator = new K8sClientGenerator();
|
|
71
|
+
|
|
72
|
+
await generator.generate({
|
|
73
|
+
specUrl: 'https://api.example.com/openapi.json',
|
|
74
|
+
outputDir: './client',
|
|
75
|
+
resolveReferences: true,
|
|
76
|
+
headers: {
|
|
77
|
+
'Authorization': 'Bearer your-token-here'
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
## How Reference Resolution Works
|
|
83
|
+
|
|
84
|
+
When you enable `--resolve-refs` (or `resolveReferences: true`), klasik will:
|
|
85
|
+
|
|
86
|
+
1. **Parse the main OpenAPI spec** for external `$ref` references like:
|
|
87
|
+
```yaml
|
|
88
|
+
schema:
|
|
89
|
+
$ref: './schemas/users-orgs.yaml#/components/schemas/Org'
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
2. **Download all referenced files** preserving the directory structure:
|
|
93
|
+
```
|
|
94
|
+
output/
|
|
95
|
+
├── openapi-spec.json (main spec)
|
|
96
|
+
└── schemas/
|
|
97
|
+
└── users-orgs.yaml (referenced schema)
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
3. **Recursively resolve nested references** - if `users-orgs.yaml` has its own `$ref` to other files, those will be downloaded too
|
|
101
|
+
|
|
102
|
+
4. **Use the same headers** for all downloads (useful when all files require authentication)
|
|
103
|
+
|
|
104
|
+
### Supported Reference Types
|
|
105
|
+
|
|
106
|
+
- **Relative paths**: `./schemas/user.yaml`, `../common/types.yaml`
|
|
107
|
+
- **Remote URLs**: `https://api.example.com/schemas/user.yaml`
|
|
108
|
+
- **Mixed**: Main spec from HTTPS, references can be relative or absolute
|
|
109
|
+
|
|
110
|
+
### Example OpenAPI Spec with External References
|
|
111
|
+
|
|
112
|
+
```json
|
|
113
|
+
{
|
|
114
|
+
"openapi": "3.0.0",
|
|
115
|
+
"info": {
|
|
116
|
+
"title": "My API",
|
|
117
|
+
"version": "1.0.0"
|
|
118
|
+
},
|
|
119
|
+
"paths": {
|
|
120
|
+
"/users": {
|
|
121
|
+
"get": {
|
|
122
|
+
"responses": {
|
|
123
|
+
"200": {
|
|
124
|
+
"content": {
|
|
125
|
+
"application/json": {
|
|
126
|
+
"schema": {
|
|
127
|
+
"$ref": "./schemas/users.yaml#/components/schemas/UserList"
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
When you run with `--resolve-refs`, klasik will automatically download `./schemas/users.yaml`.
|
|
140
|
+
|
|
141
|
+
## Complete Example
|
|
142
|
+
|
|
143
|
+
```bash
|
|
144
|
+
# Full workflow with private API
|
|
145
|
+
npx klasik generate \
|
|
146
|
+
--url https://private-api.example.com/v1/openapi.json \
|
|
147
|
+
--output ./generated-client \
|
|
148
|
+
--header "Authorization: Bearer eyJhbGc..." \
|
|
149
|
+
--header "X-API-Version: v1" \
|
|
150
|
+
--resolve-refs \
|
|
151
|
+
--keep-spec \
|
|
152
|
+
--timeout 60000
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
This will:
|
|
156
|
+
- Download the OpenAPI spec with authorization headers
|
|
157
|
+
- Resolve and download all external schema references
|
|
158
|
+
- Generate the TypeScript client
|
|
159
|
+
- Keep all downloaded specs in the output directory
|
|
160
|
+
- Use a 60-second timeout for all HTTP requests
|
package/dist/cli.js
CHANGED
|
@@ -51,6 +51,7 @@ program
|
|
|
51
51
|
.option('-H, --header <header...>', 'Custom headers for the request (format: "Key: Value")')
|
|
52
52
|
.option('-t, --template <dir>', 'Custom template directory')
|
|
53
53
|
.option('-k, --keep-spec', 'Keep the downloaded spec file after generation', false)
|
|
54
|
+
.option('-r, --resolve-refs', 'Resolve and download external $ref references', false)
|
|
54
55
|
.option('--timeout <ms>', 'Request timeout in milliseconds', '30000')
|
|
55
56
|
.action(async (options) => {
|
|
56
57
|
try {
|
|
@@ -80,6 +81,7 @@ program
|
|
|
80
81
|
headers: Object.keys(headers).length > 0 ? headers : undefined,
|
|
81
82
|
templateDir: options.template,
|
|
82
83
|
keepSpec: options.keepSpec,
|
|
84
|
+
resolveReferences: options.resolveRefs,
|
|
83
85
|
timeout: parseInt(options.timeout, 10),
|
|
84
86
|
});
|
|
85
87
|
process.exit(0);
|
|
@@ -95,6 +97,7 @@ program
|
|
|
95
97
|
.requiredOption('-u, --url <url>', 'Remote URL to download the OpenAPI spec from')
|
|
96
98
|
.requiredOption('-o, --output <file>', 'Output file path for the downloaded spec')
|
|
97
99
|
.option('-H, --header <header...>', 'Custom headers for the request (format: "Key: Value")')
|
|
100
|
+
.option('-r, --resolve-refs', 'Resolve and download external $ref references', false)
|
|
98
101
|
.option('--timeout <ms>', 'Request timeout in milliseconds', '30000')
|
|
99
102
|
.action(async (options) => {
|
|
100
103
|
try {
|
|
@@ -115,6 +118,7 @@ program
|
|
|
115
118
|
url: options.url,
|
|
116
119
|
outputPath: path.resolve(options.output),
|
|
117
120
|
headers: Object.keys(headers).length > 0 ? headers : undefined,
|
|
121
|
+
resolveReferences: options.resolveRefs,
|
|
118
122
|
timeout: parseInt(options.timeout, 10),
|
|
119
123
|
});
|
|
120
124
|
process.exit(0);
|
|
@@ -28,6 +28,11 @@ export interface K8sClientGeneratorOptions {
|
|
|
28
28
|
* @default false
|
|
29
29
|
*/
|
|
30
30
|
keepSpec?: boolean;
|
|
31
|
+
/**
|
|
32
|
+
* Whether to resolve and download external $ref references
|
|
33
|
+
* @default false
|
|
34
|
+
*/
|
|
35
|
+
resolveReferences?: boolean;
|
|
31
36
|
/**
|
|
32
37
|
* Request timeout for downloading spec in milliseconds
|
|
33
38
|
* @default 30000
|
|
@@ -46,7 +46,7 @@ class K8sClientGenerator {
|
|
|
46
46
|
* Generate TypeScript client from a remote OpenAPI spec URL
|
|
47
47
|
*/
|
|
48
48
|
async generate(options) {
|
|
49
|
-
const { specUrl, outputDir, mode = 'full', headers, templateDir, keepSpec = false, timeout, } = options;
|
|
49
|
+
const { specUrl, outputDir, mode = 'full', headers, templateDir, keepSpec = false, resolveReferences = false, timeout, } = options;
|
|
50
50
|
let specPath;
|
|
51
51
|
try {
|
|
52
52
|
// Step 1: Download the OpenAPI spec
|
|
@@ -55,6 +55,7 @@ class K8sClientGenerator {
|
|
|
55
55
|
url: specUrl,
|
|
56
56
|
headers,
|
|
57
57
|
timeout,
|
|
58
|
+
resolveReferences,
|
|
58
59
|
// If keepSpec is true, save the spec in the output directory
|
|
59
60
|
outputPath: keepSpec ? path.join(outputDir, 'openapi-spec.json') : undefined,
|
|
60
61
|
};
|
|
@@ -22,8 +22,19 @@ export interface DownloadOptions {
|
|
|
22
22
|
* @default 30000
|
|
23
23
|
*/
|
|
24
24
|
timeout?: number;
|
|
25
|
+
/**
|
|
26
|
+
* Whether to resolve and download external $ref references
|
|
27
|
+
* @default false
|
|
28
|
+
*/
|
|
29
|
+
resolveReferences?: boolean;
|
|
30
|
+
/**
|
|
31
|
+
* Base URL for resolving relative references (used when main spec is from HTTP)
|
|
32
|
+
* If not provided, will be derived from the main spec URL
|
|
33
|
+
*/
|
|
34
|
+
baseUrl?: string;
|
|
25
35
|
}
|
|
26
36
|
export declare class SpecDownloader {
|
|
37
|
+
private downloadedRefs;
|
|
27
38
|
/**
|
|
28
39
|
* Download an OpenAPI specification from a URL or load from a local file
|
|
29
40
|
* @param options Download options
|
|
@@ -50,4 +61,24 @@ export declare class SpecDownloader {
|
|
|
50
61
|
* Clean up temporary files
|
|
51
62
|
*/
|
|
52
63
|
cleanupTemp(): void;
|
|
64
|
+
/**
|
|
65
|
+
* Get base URL from a full URL (removes the filename)
|
|
66
|
+
*/
|
|
67
|
+
private getBaseUrl;
|
|
68
|
+
/**
|
|
69
|
+
* Resolve and download all external $ref references in the spec
|
|
70
|
+
*/
|
|
71
|
+
private resolveExternalRefs;
|
|
72
|
+
/**
|
|
73
|
+
* Find all external $ref values in the spec
|
|
74
|
+
*/
|
|
75
|
+
private findExternalRefs;
|
|
76
|
+
/**
|
|
77
|
+
* Check if a $ref is external (references another file)
|
|
78
|
+
*/
|
|
79
|
+
private isExternalRef;
|
|
80
|
+
/**
|
|
81
|
+
* Download a single reference file
|
|
82
|
+
*/
|
|
83
|
+
private downloadReference;
|
|
53
84
|
}
|
package/dist/spec-downloader.js
CHANGED
|
@@ -41,16 +41,25 @@ const axios_1 = __importDefault(require("axios"));
|
|
|
41
41
|
const fs = __importStar(require("fs"));
|
|
42
42
|
const path = __importStar(require("path"));
|
|
43
43
|
class SpecDownloader {
|
|
44
|
+
constructor() {
|
|
45
|
+
this.downloadedRefs = new Set();
|
|
46
|
+
}
|
|
44
47
|
/**
|
|
45
48
|
* Download an OpenAPI specification from a URL or load from a local file
|
|
46
49
|
* @param options Download options
|
|
47
50
|
* @returns Path to the spec file
|
|
48
51
|
*/
|
|
49
52
|
async download(options) {
|
|
50
|
-
const { url, outputPath, headers, timeout = 30000 } = options;
|
|
53
|
+
const { url, outputPath, headers, timeout = 30000, resolveReferences = false, baseUrl } = options;
|
|
54
|
+
// Reset downloaded refs tracking for each new download
|
|
55
|
+
this.downloadedRefs.clear();
|
|
51
56
|
// Check if it's a local file path
|
|
52
57
|
if (this.isLocalFile(url)) {
|
|
53
|
-
|
|
58
|
+
const specPath = await this.loadLocalFile(url, outputPath);
|
|
59
|
+
if (resolveReferences) {
|
|
60
|
+
await this.resolveExternalRefs(specPath, headers, timeout, baseUrl || path.dirname(specPath));
|
|
61
|
+
}
|
|
62
|
+
return specPath;
|
|
54
63
|
}
|
|
55
64
|
// Otherwise, download from HTTP/HTTPS
|
|
56
65
|
console.log(`Downloading OpenAPI spec from ${url}...`);
|
|
@@ -88,6 +97,11 @@ class SpecDownloader {
|
|
|
88
97
|
// Write to file
|
|
89
98
|
fs.writeFileSync(specPath, specContent, 'utf-8');
|
|
90
99
|
console.log(`OpenAPI spec downloaded successfully to ${specPath}`);
|
|
100
|
+
// Resolve references if requested
|
|
101
|
+
if (resolveReferences) {
|
|
102
|
+
const resolvedBaseUrl = baseUrl || this.getBaseUrl(url);
|
|
103
|
+
await this.resolveExternalRefs(specPath, headers, timeout, resolvedBaseUrl);
|
|
104
|
+
}
|
|
91
105
|
return specPath;
|
|
92
106
|
}
|
|
93
107
|
catch (error) {
|
|
@@ -209,5 +223,161 @@ class SpecDownloader {
|
|
|
209
223
|
console.log('Cleaned up temporary files');
|
|
210
224
|
}
|
|
211
225
|
}
|
|
226
|
+
/**
|
|
227
|
+
* Get base URL from a full URL (removes the filename)
|
|
228
|
+
*/
|
|
229
|
+
getBaseUrl(url) {
|
|
230
|
+
try {
|
|
231
|
+
const urlObj = new URL(url);
|
|
232
|
+
const pathParts = urlObj.pathname.split('/');
|
|
233
|
+
pathParts.pop(); // Remove filename
|
|
234
|
+
urlObj.pathname = pathParts.join('/');
|
|
235
|
+
return urlObj.toString();
|
|
236
|
+
}
|
|
237
|
+
catch {
|
|
238
|
+
// If URL parsing fails, return the original URL
|
|
239
|
+
return url;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
/**
|
|
243
|
+
* Resolve and download all external $ref references in the spec
|
|
244
|
+
*/
|
|
245
|
+
async resolveExternalRefs(specPath, headers, timeout, baseUrl) {
|
|
246
|
+
console.log('Resolving external references...');
|
|
247
|
+
const content = fs.readFileSync(specPath, 'utf-8');
|
|
248
|
+
let spec;
|
|
249
|
+
try {
|
|
250
|
+
spec = JSON.parse(content);
|
|
251
|
+
}
|
|
252
|
+
catch {
|
|
253
|
+
console.log('Spec is not JSON, skipping reference resolution');
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
const refs = this.findExternalRefs(spec);
|
|
257
|
+
if (refs.length === 0) {
|
|
258
|
+
console.log('No external references found');
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
console.log(`Found ${refs.length} external reference(s)`);
|
|
262
|
+
const specDir = path.dirname(specPath);
|
|
263
|
+
const isRemoteSpec = !this.isLocalFile(baseUrl || '');
|
|
264
|
+
for (const ref of refs) {
|
|
265
|
+
await this.downloadReference(ref, specDir, baseUrl, isRemoteSpec, headers, timeout);
|
|
266
|
+
}
|
|
267
|
+
console.log('All external references resolved successfully');
|
|
268
|
+
}
|
|
269
|
+
/**
|
|
270
|
+
* Find all external $ref values in the spec
|
|
271
|
+
*/
|
|
272
|
+
findExternalRefs(obj, refs = []) {
|
|
273
|
+
if (typeof obj !== 'object' || obj === null) {
|
|
274
|
+
return refs;
|
|
275
|
+
}
|
|
276
|
+
if (Array.isArray(obj)) {
|
|
277
|
+
for (const item of obj) {
|
|
278
|
+
this.findExternalRefs(item, refs);
|
|
279
|
+
}
|
|
280
|
+
return refs;
|
|
281
|
+
}
|
|
282
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
283
|
+
if (key === '$ref' && typeof value === 'string') {
|
|
284
|
+
// Check if it's an external reference (contains a file path)
|
|
285
|
+
if (this.isExternalRef(value)) {
|
|
286
|
+
// Remove the fragment (#/...) part if present
|
|
287
|
+
const refPath = value.split('#')[0];
|
|
288
|
+
if (refPath && !refs.includes(refPath)) {
|
|
289
|
+
refs.push(refPath);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
else {
|
|
294
|
+
this.findExternalRefs(value, refs);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
return refs;
|
|
298
|
+
}
|
|
299
|
+
/**
|
|
300
|
+
* Check if a $ref is external (references another file)
|
|
301
|
+
*/
|
|
302
|
+
isExternalRef(ref) {
|
|
303
|
+
// External refs start with ./ or ../ or / or http:// or https://
|
|
304
|
+
return (ref.startsWith('./') ||
|
|
305
|
+
ref.startsWith('../') ||
|
|
306
|
+
ref.startsWith('http://') ||
|
|
307
|
+
ref.startsWith('https://') ||
|
|
308
|
+
(ref.startsWith('/') && !ref.startsWith('/#')));
|
|
309
|
+
}
|
|
310
|
+
/**
|
|
311
|
+
* Download a single reference file
|
|
312
|
+
*/
|
|
313
|
+
async downloadReference(ref, specDir, baseUrl, isRemoteSpec, headers, timeout) {
|
|
314
|
+
// Skip if already downloaded
|
|
315
|
+
if (this.downloadedRefs.has(ref)) {
|
|
316
|
+
return;
|
|
317
|
+
}
|
|
318
|
+
this.downloadedRefs.add(ref);
|
|
319
|
+
let refUrl;
|
|
320
|
+
let outputPath;
|
|
321
|
+
if (isRemoteSpec && baseUrl) {
|
|
322
|
+
// For remote specs, construct the full URL
|
|
323
|
+
refUrl = new URL(ref, baseUrl).toString();
|
|
324
|
+
// Preserve the directory structure in the output
|
|
325
|
+
const refPath = ref.split('#')[0];
|
|
326
|
+
outputPath = path.join(specDir, refPath);
|
|
327
|
+
}
|
|
328
|
+
else {
|
|
329
|
+
// For local specs, resolve relative to the spec directory
|
|
330
|
+
refUrl = ref;
|
|
331
|
+
outputPath = path.resolve(specDir, ref.split('#')[0]);
|
|
332
|
+
}
|
|
333
|
+
console.log(` - Downloading reference: ${ref}`);
|
|
334
|
+
try {
|
|
335
|
+
// Ensure output directory exists
|
|
336
|
+
const outputDir = path.dirname(outputPath);
|
|
337
|
+
if (!fs.existsSync(outputDir)) {
|
|
338
|
+
fs.mkdirSync(outputDir, { recursive: true });
|
|
339
|
+
}
|
|
340
|
+
if (this.isLocalFile(refUrl) && !isRemoteSpec) {
|
|
341
|
+
// Copy local file
|
|
342
|
+
const sourcePath = path.resolve(specDir, refUrl.split('#')[0]);
|
|
343
|
+
if (fs.existsSync(sourcePath)) {
|
|
344
|
+
const content = fs.readFileSync(sourcePath, 'utf-8');
|
|
345
|
+
fs.writeFileSync(outputPath, content, 'utf-8');
|
|
346
|
+
console.log(` ✓ Copied: ${sourcePath} -> ${outputPath}`);
|
|
347
|
+
// Recursively resolve references in the downloaded file
|
|
348
|
+
await this.resolveExternalRefs(outputPath, headers, timeout, path.dirname(sourcePath));
|
|
349
|
+
}
|
|
350
|
+
else {
|
|
351
|
+
console.warn(` ⚠ Reference file not found: ${sourcePath}`);
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
else {
|
|
355
|
+
// Download from HTTP/HTTPS
|
|
356
|
+
const response = await axios_1.default.get(refUrl, {
|
|
357
|
+
headers,
|
|
358
|
+
timeout: timeout || 30000,
|
|
359
|
+
responseType: 'text',
|
|
360
|
+
});
|
|
361
|
+
let content = response.data;
|
|
362
|
+
if (typeof content !== 'string') {
|
|
363
|
+
content = JSON.stringify(content, null, 2);
|
|
364
|
+
}
|
|
365
|
+
fs.writeFileSync(outputPath, content, 'utf-8');
|
|
366
|
+
console.log(` ✓ Downloaded: ${refUrl}`);
|
|
367
|
+
// Recursively resolve references in the downloaded file
|
|
368
|
+
const refBaseUrl = this.getBaseUrl(refUrl);
|
|
369
|
+
await this.resolveExternalRefs(outputPath, headers, timeout, refBaseUrl);
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
catch (error) {
|
|
373
|
+
if (axios_1.default.isAxiosError(error)) {
|
|
374
|
+
console.error(` ✗ Failed to download ${ref}: ${error.message}`);
|
|
375
|
+
}
|
|
376
|
+
else {
|
|
377
|
+
console.error(` ✗ Failed to process ${ref}:`, error);
|
|
378
|
+
}
|
|
379
|
+
// Continue with other references even if one fails
|
|
380
|
+
}
|
|
381
|
+
}
|
|
212
382
|
}
|
|
213
383
|
exports.SpecDownloader = SpecDownloader;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "klasik",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.13",
|
|
4
4
|
"description": "Download OpenAPI specs from remote URLs and generate TypeScript clients with class-transformer support",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -26,8 +26,10 @@
|
|
|
26
26
|
"license": "MIT",
|
|
27
27
|
"dependencies": {
|
|
28
28
|
"axios": "^1.6.0",
|
|
29
|
+
"class-transformer": "^0.5.1",
|
|
29
30
|
"commander": "^11.0.0",
|
|
30
|
-
"openapi-class-transformer": "1.0.
|
|
31
|
+
"openapi-class-transformer": "^1.0.11",
|
|
32
|
+
"reflect-metadata": "^0.2.2"
|
|
31
33
|
},
|
|
32
34
|
"devDependencies": {
|
|
33
35
|
"@types/jest": "^29.5.0",
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# OpenAPI Generator Ignore
|
|
2
|
+
# Generated by openapi-generator https://github.com/openapitools/openapi-generator
|
|
3
|
+
|
|
4
|
+
# Use this file to prevent files from being overwritten by the generator.
|
|
5
|
+
# The patterns follow closely to .gitignore or .dockerignore.
|
|
6
|
+
|
|
7
|
+
# As an example, the C# client generator defines ApiClient.cs.
|
|
8
|
+
# You can make changes and tell OpenAPI Generator to ignore just this file by uncommenting the following line:
|
|
9
|
+
#ApiClient.cs
|
|
10
|
+
|
|
11
|
+
# You can match any string of characters against a directory, file or extension with a single asterisk (*):
|
|
12
|
+
#foo/*/qux
|
|
13
|
+
# The above matches foo/bar/qux and foo/baz/qux, but not foo/bar/baz/qux
|
|
14
|
+
|
|
15
|
+
# You can recursively match patterns against a directory, file or extension with a double asterisk (**):
|
|
16
|
+
#foo/**/qux
|
|
17
|
+
# This matches foo/bar/qux, foo/baz/qux, and foo/bar/baz/qux
|
|
18
|
+
|
|
19
|
+
# You can also negate patterns with an exclamation (!).
|
|
20
|
+
# For example, you can ignore all files in a docs folder with the file extension .md:
|
|
21
|
+
#docs/*.md
|
|
22
|
+
# Then explicitly reverse the ignore rule for a single file:
|
|
23
|
+
#!docs/README.md
|