@ioka-technologies/asyncapi-ts-client-template 0.0.8 → 0.0.11
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 +113 -289
- package/examples/auth-retry/README.md +384 -0
- package/examples/auth-retry/asyncapi.yaml +464 -0
- package/examples/auth-retry/login-flow-example.ts +375 -0
- package/package.json +3 -2
- package/template/helpers/security.js +137 -0
- package/template/index.js +113 -122
- package/template/src/client.ts.js +24 -2
- package/template/src/models.ts.js +157 -17
- package/template/src/runtime/auth/headers.ts.js +95 -0
- package/template/src/runtime/auth/index.ts.js +15 -0
- package/template/src/runtime/auth/types.ts.js +74 -0
- package/template/src/runtime/retry/index.ts.js +18 -0
- package/template/src/runtime/retry/manager.ts.js +150 -0
- package/template/src/runtime/retry/presets.ts.js +81 -0
- package/template/src/runtime/retry/types.ts.js +60 -0
- package/template/src/runtime/transports/http.ts.js +112 -17
- package/template/src/runtime/transports/websocket.ts.js +28 -3
- package/template/src/runtime/types.ts.js +12 -3
- package/USAGE.md +0 -586
package/template/index.js
CHANGED
|
@@ -38,70 +38,67 @@ module.exports = function ({ asyncapi, params }) {
|
|
|
38
38
|
? params.packageVersion
|
|
39
39
|
: version;
|
|
40
40
|
|
|
41
|
-
const transports = (params.transports || 'websocket,http').split(',').map(t => t.trim());
|
|
42
|
-
|
|
43
41
|
// Generate all files from the main index.js
|
|
44
42
|
return [
|
|
45
|
-
React.createElement(File, { name:
|
|
43
|
+
React.createElement(File, { name: 'package.json' },
|
|
46
44
|
JSON.stringify({
|
|
47
45
|
name: packageName,
|
|
48
46
|
version: packageVersion,
|
|
49
47
|
description: `${description || title} - TypeScript AsyncAPI Client`,
|
|
50
|
-
main:
|
|
51
|
-
types:
|
|
48
|
+
main: 'dist/index.js',
|
|
49
|
+
types: 'dist/index.d.ts',
|
|
52
50
|
scripts: {
|
|
53
|
-
build:
|
|
54
|
-
dev:
|
|
55
|
-
test:
|
|
56
|
-
lint:
|
|
57
|
-
prepare:
|
|
51
|
+
build: 'tsc',
|
|
52
|
+
dev: 'tsc --watch',
|
|
53
|
+
test: 'jest',
|
|
54
|
+
lint: 'eslint src/**/*.ts',
|
|
55
|
+
prepare: 'npm run build'
|
|
58
56
|
},
|
|
59
57
|
keywords: [
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
58
|
+
'asyncapi',
|
|
59
|
+
'websocket',
|
|
60
|
+
'http',
|
|
61
|
+
'client',
|
|
62
|
+
'typescript',
|
|
65
63
|
title.toLowerCase().replace(/[^a-z0-9]/g, '-')
|
|
66
64
|
],
|
|
67
|
-
author: params.author ||
|
|
68
|
-
license: params.license ||
|
|
65
|
+
author: params.author || 'AsyncAPI Generator',
|
|
66
|
+
license: params.license || 'Apache-2.0',
|
|
69
67
|
dependencies: {
|
|
70
|
-
uuid:
|
|
68
|
+
uuid: '^9.0.0'
|
|
71
69
|
},
|
|
72
70
|
optionalDependencies: {
|
|
73
|
-
ws:
|
|
71
|
+
ws: '^8.14.0'
|
|
74
72
|
},
|
|
75
73
|
devDependencies: {
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
typescript:
|
|
80
|
-
eslint:
|
|
81
|
-
|
|
82
|
-
|
|
74
|
+
'@types/ws': '^8.5.0',
|
|
75
|
+
'@types/uuid': '^9.0.0',
|
|
76
|
+
'@types/node': '^20.0.0',
|
|
77
|
+
typescript: '^5.0.0',
|
|
78
|
+
eslint: '^8.0.0',
|
|
79
|
+
'@typescript-eslint/eslint-plugin': '^6.0.0',
|
|
80
|
+
'@typescript-eslint/parser': '^6.0.0',
|
|
83
81
|
...(params.generateTests && {
|
|
84
|
-
jest:
|
|
85
|
-
|
|
86
|
-
|
|
82
|
+
jest: '^29.0.0',
|
|
83
|
+
'@types/jest': '^29.0.0',
|
|
84
|
+
'ts-jest': '^29.0.0'
|
|
87
85
|
})
|
|
88
86
|
},
|
|
89
87
|
files: [
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
"USAGE.md"
|
|
88
|
+
'dist/**/*',
|
|
89
|
+
'README.md'
|
|
93
90
|
]
|
|
94
91
|
}, null, 2)
|
|
95
92
|
),
|
|
96
93
|
|
|
97
|
-
React.createElement(File, { name:
|
|
94
|
+
React.createElement(File, { name: 'tsconfig.json' },
|
|
98
95
|
JSON.stringify({
|
|
99
96
|
compilerOptions: {
|
|
100
|
-
target:
|
|
101
|
-
module:
|
|
102
|
-
lib: [
|
|
103
|
-
outDir:
|
|
104
|
-
rootDir:
|
|
97
|
+
target: 'ES2020',
|
|
98
|
+
module: 'ES2020',
|
|
99
|
+
lib: ['ES2020', 'DOM'],
|
|
100
|
+
outDir: './dist',
|
|
101
|
+
rootDir: './src',
|
|
105
102
|
strict: true,
|
|
106
103
|
esModuleInterop: true,
|
|
107
104
|
skipLibCheck: true,
|
|
@@ -109,75 +106,61 @@ module.exports = function ({ asyncapi, params }) {
|
|
|
109
106
|
declaration: true,
|
|
110
107
|
declarationMap: true,
|
|
111
108
|
sourceMap: true,
|
|
112
|
-
moduleResolution:
|
|
109
|
+
moduleResolution: 'node',
|
|
113
110
|
allowSyntheticDefaultImports: true,
|
|
114
111
|
experimentalDecorators: true,
|
|
115
112
|
emitDecoratorMetadata: true,
|
|
116
113
|
resolveJsonModule: true,
|
|
117
|
-
typeRoots: [
|
|
114
|
+
typeRoots: ['node_modules/@types']
|
|
118
115
|
},
|
|
119
116
|
include: [
|
|
120
|
-
|
|
117
|
+
'src/**/*'
|
|
121
118
|
],
|
|
122
119
|
exclude: [
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
120
|
+
'node_modules',
|
|
121
|
+
'dist',
|
|
122
|
+
'**/*.test.ts',
|
|
123
|
+
'**/*.spec.ts'
|
|
127
124
|
]
|
|
128
125
|
}, null, 2)
|
|
129
126
|
),
|
|
130
127
|
|
|
131
|
-
React.createElement(File, { name:
|
|
128
|
+
React.createElement(File, { name: 'README.md' },
|
|
132
129
|
`# ${title}
|
|
133
130
|
|
|
134
131
|
${description || 'Generated TypeScript AsyncAPI client'}
|
|
135
132
|
|
|
136
|
-
##
|
|
137
|
-
|
|
138
|
-
\`\`\`bash
|
|
139
|
-
npm install ${packageName}
|
|
140
|
-
\`\`\`
|
|
141
|
-
|
|
142
|
-
## Quick Start
|
|
143
|
-
|
|
144
|
-
See [USAGE.md](./USAGE.md) for detailed usage instructions.
|
|
133
|
+
## Overview
|
|
145
134
|
|
|
146
|
-
|
|
135
|
+
This TypeScript client provides type-safe access to your AsyncAPI service with automatic transport selection and built-in error handling. Generated from your AsyncAPI specification, it offers seamless integration with both WebSocket and HTTP protocols.
|
|
147
136
|
|
|
148
|
-
|
|
137
|
+
## Technical Requirements
|
|
149
138
|
|
|
150
|
-
-
|
|
151
|
-
-
|
|
152
|
-
`
|
|
153
|
-
),
|
|
139
|
+
- Node.js 16+
|
|
140
|
+
- TypeScript 4.5+
|
|
154
141
|
|
|
155
|
-
|
|
156
|
-
`# ${title} - Usage Guide
|
|
142
|
+
## Supported Transports
|
|
157
143
|
|
|
158
|
-
|
|
144
|
+
- WebSocket (with auto-reconnection)
|
|
145
|
+
- HTTP (with retry logic)
|
|
159
146
|
|
|
160
147
|
## Installation
|
|
161
148
|
|
|
162
|
-
###
|
|
163
|
-
For browser environments, install the package normally:
|
|
164
|
-
\`\`\`bash
|
|
165
|
-
npm install ${packageName}
|
|
166
|
-
\`\`\`
|
|
149
|
+
### For Node.js Projects
|
|
167
150
|
|
|
168
|
-
### Node.js Usage
|
|
169
|
-
For Node.js environments, you'll also need the WebSocket library:
|
|
170
151
|
\`\`\`bash
|
|
171
152
|
npm install ${packageName} ws
|
|
172
153
|
\`\`\`
|
|
173
154
|
|
|
174
|
-
|
|
155
|
+
### For Browser Projects
|
|
156
|
+
|
|
157
|
+
\`\`\`bash
|
|
158
|
+
npm install ${packageName}
|
|
159
|
+
\`\`\`
|
|
175
160
|
|
|
176
|
-
|
|
177
|
-
- **Browser**: Uses the native \`WebSocket\` API
|
|
178
|
-
- **Node.js**: Uses the \`ws\` library (must be installed separately)
|
|
161
|
+
## Quick Start
|
|
179
162
|
|
|
180
|
-
|
|
163
|
+
### WebSocket Client
|
|
181
164
|
|
|
182
165
|
\`\`\`typescript
|
|
183
166
|
import { ${clientName} } from '${packageName}';
|
|
@@ -190,25 +173,16 @@ const client = new ${clientName}({
|
|
|
190
173
|
}
|
|
191
174
|
});
|
|
192
175
|
|
|
193
|
-
// Connect
|
|
176
|
+
// Connect and send messages
|
|
194
177
|
await client.connect();
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
userId: '123'
|
|
201
|
-
});
|
|
202
|
-
console.log('Response:', response);
|
|
203
|
-
} catch (error) {
|
|
204
|
-
console.error('Error:', error);
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
// Disconnect when done
|
|
208
|
-
await client.disconnect();
|
|
178
|
+
const response = await client.sendMessage({
|
|
179
|
+
text: 'Hello, World!',
|
|
180
|
+
userId: '123'
|
|
181
|
+
});
|
|
182
|
+
console.log('Response:', response);
|
|
209
183
|
\`\`\`
|
|
210
184
|
|
|
211
|
-
|
|
185
|
+
### HTTP Client
|
|
212
186
|
|
|
213
187
|
\`\`\`typescript
|
|
214
188
|
import { ${clientName} } from '${packageName}';
|
|
@@ -221,36 +195,24 @@ const client = new ${clientName}({
|
|
|
221
195
|
}
|
|
222
196
|
});
|
|
223
197
|
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
userId: '123'
|
|
229
|
-
});
|
|
230
|
-
console.log('Response:', response);
|
|
231
|
-
} catch (error) {
|
|
232
|
-
console.error('Error:', error);
|
|
233
|
-
}
|
|
198
|
+
const response = await client.sendMessage({
|
|
199
|
+
text: 'Hello, World!',
|
|
200
|
+
userId: '123'
|
|
201
|
+
});
|
|
234
202
|
\`\`\`
|
|
235
203
|
|
|
236
204
|
## Configuration Options
|
|
237
205
|
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
}
|
|
245
|
-
\`\`\`
|
|
206
|
+
| Option | Type | Description | Default |
|
|
207
|
+
|--------|------|-------------|---------|
|
|
208
|
+
| \`type\` | \`'websocket' \\| 'http'\` | Transport protocol | Required |
|
|
209
|
+
| \`url\` | \`string\` | Server URL | Required |
|
|
210
|
+
| \`headers\` | \`Record<string, string>\` | Request headers | \`{}\` |
|
|
211
|
+
| \`timeout\` | \`number\` | Request timeout (ms) | \`30000\` |
|
|
246
212
|
|
|
247
213
|
## Error Handling
|
|
248
214
|
|
|
249
|
-
The client
|
|
250
|
-
|
|
251
|
-
- \`TransportError\`: General transport errors
|
|
252
|
-
- \`ConnectionError\`: Connection-related errors
|
|
253
|
-
- \`TimeoutError\`: Request timeout errors
|
|
215
|
+
The client provides specific error types for different scenarios:
|
|
254
216
|
|
|
255
217
|
\`\`\`typescript
|
|
256
218
|
import { TransportError, ConnectionError, TimeoutError } from '${packageName}';
|
|
@@ -261,23 +223,52 @@ try {
|
|
|
261
223
|
if (error instanceof ConnectionError) {
|
|
262
224
|
console.error('Connection failed:', error.message);
|
|
263
225
|
} else if (error instanceof TimeoutError) {
|
|
264
|
-
console.error('Request timed out
|
|
226
|
+
console.error('Request timed out');
|
|
265
227
|
} else if (error instanceof TransportError) {
|
|
266
228
|
console.error('Transport error:', error.message);
|
|
267
229
|
}
|
|
268
230
|
}
|
|
269
231
|
\`\`\`
|
|
270
232
|
|
|
271
|
-
##
|
|
233
|
+
## Environment Compatibility
|
|
234
|
+
|
|
235
|
+
- **Browser**: Uses native WebSocket API (no additional dependencies)
|
|
236
|
+
- **Node.js**: Requires \`ws\` package for WebSocket support
|
|
237
|
+
|
|
238
|
+
## Development
|
|
272
239
|
|
|
273
|
-
### WebSocket Issues in Node.js
|
|
274
|
-
If you get an error like "WebSocket implementation not available", make sure you have installed the \`ws\` package:
|
|
275
240
|
\`\`\`bash
|
|
276
|
-
|
|
241
|
+
# Build the project
|
|
242
|
+
npm run build
|
|
243
|
+
|
|
244
|
+
# Watch for changes
|
|
245
|
+
npm run dev
|
|
246
|
+
|
|
247
|
+
# Run tests
|
|
248
|
+
npm test
|
|
249
|
+
|
|
250
|
+
# Lint code
|
|
251
|
+
npm run lint
|
|
277
252
|
\`\`\`
|
|
278
253
|
|
|
279
|
-
|
|
280
|
-
|
|
254
|
+
## Generated from AsyncAPI
|
|
255
|
+
|
|
256
|
+
- **AsyncAPI Version**: ${asyncapi.version()}
|
|
257
|
+
- **Generated**: ${new Date().toISOString()}
|
|
258
|
+
- **Title**: ${title}
|
|
259
|
+
- **Version**: ${packageVersion}
|
|
260
|
+
|
|
261
|
+
## Contributing
|
|
262
|
+
|
|
263
|
+
1. Fork the repository
|
|
264
|
+
2. Create a feature branch
|
|
265
|
+
3. Make your changes and add tests
|
|
266
|
+
4. Run the test suite: \`npm test\`
|
|
267
|
+
5. Submit a pull request
|
|
268
|
+
|
|
269
|
+
## License
|
|
270
|
+
|
|
271
|
+
${params.license || 'Apache-2.0'}
|
|
281
272
|
`
|
|
282
273
|
)
|
|
283
274
|
];
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
/* eslint-disable no-unused-vars */
|
|
2
2
|
import { File } from '@asyncapi/generator-react-sdk';
|
|
3
|
+
import { operationRequiresAuth, extractOperationSecurityMap } from '../helpers/security.js';
|
|
3
4
|
|
|
4
5
|
function generateClient(asyncapi, clientName) {
|
|
5
6
|
// Method name sanitization function
|
|
@@ -9,7 +10,7 @@ function generateClient(asyncapi, clientName) {
|
|
|
9
10
|
// Convert to camelCase and remove invalid characters
|
|
10
11
|
let sanitized = operationId
|
|
11
12
|
// Replace dots, hyphens, underscores, spaces, and forward slashes with camelCase
|
|
12
|
-
.replace(/[.\-_\s
|
|
13
|
+
.replace(/[.\-_\s/]+(.)/g, (_, char) => char.toUpperCase())
|
|
13
14
|
// Remove any remaining invalid characters
|
|
14
15
|
.replace(/[^a-zA-Z0-9]/g, '')
|
|
15
16
|
// Ensure it starts with a lowercase letter if it starts with a number
|
|
@@ -42,6 +43,7 @@ function generateClient(asyncapi, clientName) {
|
|
|
42
43
|
|
|
43
44
|
let content = `import { TransportFactory } from './runtime/transports/factory';
|
|
44
45
|
import { Transport, TransportConfig, RequestOptions, MessageEnvelope } from './runtime/types';
|
|
46
|
+
import { AuthCredentials } from './runtime/auth/types';
|
|
45
47
|
import * as Models from './models';
|
|
46
48
|
|
|
47
49
|
export class ${clientName} {
|
|
@@ -70,6 +72,26 @@ export class ${clientName} {
|
|
|
70
72
|
this.transport.unsubscribe(channel, callback);
|
|
71
73
|
}
|
|
72
74
|
|
|
75
|
+
/**
|
|
76
|
+
* Update authentication configuration
|
|
77
|
+
* @param auth New authentication configuration
|
|
78
|
+
*/
|
|
79
|
+
updateAuth(auth: AuthCredentials): void {
|
|
80
|
+
this.config.auth = auth;
|
|
81
|
+
// If transport supports auth updates, update it
|
|
82
|
+
if (this.transport && typeof (this.transport as any).updateAuth === 'function') {
|
|
83
|
+
(this.transport as any).updateAuth(auth);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Get current authentication configuration
|
|
89
|
+
* @returns Current auth configuration
|
|
90
|
+
*/
|
|
91
|
+
getAuth(): AuthCredentials | undefined {
|
|
92
|
+
return this.config.auth;
|
|
93
|
+
}
|
|
94
|
+
|
|
73
95
|
// Generated operation methods
|
|
74
96
|
`;
|
|
75
97
|
|
|
@@ -374,4 +396,4 @@ module.exports = function ({ asyncapi, params }) {
|
|
|
374
396
|
{generatedContent}
|
|
375
397
|
</File>
|
|
376
398
|
);
|
|
377
|
-
}
|
|
399
|
+
};
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { File } from '@asyncapi/generator-react-sdk';
|
|
3
3
|
|
|
4
4
|
function generateModels(asyncapi) {
|
|
5
|
-
let content =
|
|
5
|
+
let content = '// Generated TypeScript models from AsyncAPI specification\n\n';
|
|
6
6
|
|
|
7
7
|
// Helper functions for TypeScript identifier generation
|
|
8
8
|
function toTypeScriptIdentifier(str) {
|
|
@@ -42,8 +42,104 @@ function generateModels(asyncapi) {
|
|
|
42
42
|
// Extract message schemas and build channel mapping
|
|
43
43
|
const components = asyncapi.components();
|
|
44
44
|
const messageSchemas = [];
|
|
45
|
+
const componentSchemas = [];
|
|
45
46
|
const messageToChannels = new Map();
|
|
46
47
|
const generatedTypes = new Set();
|
|
48
|
+
const schemaRegistry = new Map();
|
|
49
|
+
|
|
50
|
+
// Build schema registry from components.schemas
|
|
51
|
+
// Try to access the raw AsyncAPI document
|
|
52
|
+
let rawDoc = null;
|
|
53
|
+
try {
|
|
54
|
+
if (asyncapi.json && typeof asyncapi.json === 'function') {
|
|
55
|
+
rawDoc = asyncapi.json();
|
|
56
|
+
} else if (asyncapi._json) {
|
|
57
|
+
rawDoc = asyncapi._json;
|
|
58
|
+
}
|
|
59
|
+
} catch (e) {
|
|
60
|
+
// Ignore
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Extract schemas from raw document if available
|
|
64
|
+
if (rawDoc && rawDoc.components && rawDoc.components.schemas) {
|
|
65
|
+
Object.entries(rawDoc.components.schemas).forEach(([name, schema]) => {
|
|
66
|
+
if (name && typeof name === 'string' && schema && typeof schema === 'object') {
|
|
67
|
+
schemaRegistry.set(name, schema);
|
|
68
|
+
componentSchemas.push({
|
|
69
|
+
name,
|
|
70
|
+
typeName: toTypeScriptTypeName(name),
|
|
71
|
+
schema: schema,
|
|
72
|
+
description: schema.description
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Fallback: try the components.schemas() method
|
|
79
|
+
if (componentSchemas.length === 0 && components && components.schemas) {
|
|
80
|
+
try {
|
|
81
|
+
const schemas = components.schemas();
|
|
82
|
+
if (schemas) {
|
|
83
|
+
// Try different ways to access schemas
|
|
84
|
+
let schemaEntries = [];
|
|
85
|
+
|
|
86
|
+
if (schemas instanceof Map) {
|
|
87
|
+
schemaEntries = Array.from(schemas.entries());
|
|
88
|
+
} else if (typeof schemas === 'object') {
|
|
89
|
+
schemaEntries = Object.entries(schemas);
|
|
90
|
+
} else if (schemas.all && typeof schemas.all === 'function') {
|
|
91
|
+
// AsyncAPI parser might have an all() method
|
|
92
|
+
const allSchemas = schemas.all();
|
|
93
|
+
if (Array.isArray(allSchemas)) {
|
|
94
|
+
schemaEntries = allSchemas.map(schema => {
|
|
95
|
+
const name = schema.uid ? schema.uid() : (schema.id ? schema.id() : null);
|
|
96
|
+
return [name, schema];
|
|
97
|
+
}).filter(([name]) => name);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
schemaEntries.forEach(([name, schema]) => {
|
|
102
|
+
// Skip internal AsyncAPI parser objects and numeric keys
|
|
103
|
+
if (!name || name === 'collections' || name === '_meta' || name.startsWith('_') || /^\d+$/.test(name)) {
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
let schemaData = null;
|
|
108
|
+
let description = null;
|
|
109
|
+
|
|
110
|
+
try {
|
|
111
|
+
// Handle different schema object types
|
|
112
|
+
if (schema && typeof schema.json === 'function') {
|
|
113
|
+
schemaData = schema.json();
|
|
114
|
+
} else if (schema && typeof schema === 'object') {
|
|
115
|
+
schemaData = schema;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (schema && typeof schema.description === 'function') {
|
|
119
|
+
description = schema.description();
|
|
120
|
+
} else if (schema && schema.description) {
|
|
121
|
+
description = schema.description;
|
|
122
|
+
}
|
|
123
|
+
} catch (e) {
|
|
124
|
+
// Ignore schema extraction errors
|
|
125
|
+
console.warn(`Failed to extract schema for ${name}:`, e.message);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (schemaData && typeof name === 'string' && name.length > 0) {
|
|
129
|
+
schemaRegistry.set(name, schemaData);
|
|
130
|
+
componentSchemas.push({
|
|
131
|
+
name,
|
|
132
|
+
typeName: toTypeScriptTypeName(name),
|
|
133
|
+
schema: schemaData,
|
|
134
|
+
description
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
} catch (e) {
|
|
140
|
+
console.warn('Failed to extract component schemas:', e.message);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
47
143
|
|
|
48
144
|
// First, build channel to message mapping
|
|
49
145
|
if (asyncapi.channels) {
|
|
@@ -126,13 +222,25 @@ function generateModels(asyncapi) {
|
|
|
126
222
|
}
|
|
127
223
|
|
|
128
224
|
// Helper function to convert JSON schema to TypeScript type
|
|
129
|
-
function jsonSchemaToTypeScriptType(schema) {
|
|
225
|
+
function jsonSchemaToTypeScriptType(schema, fieldName = '') {
|
|
130
226
|
if (!schema) return 'any';
|
|
131
227
|
|
|
132
|
-
// Handle $ref
|
|
228
|
+
// Handle $ref - resolve from schema registry
|
|
133
229
|
if (schema.$ref) {
|
|
134
230
|
const refName = schema.$ref.split('/').pop();
|
|
135
|
-
return
|
|
231
|
+
// Always return the type name for $ref, since we generate all component schemas
|
|
232
|
+
const typeName = toTypeScriptTypeName(refName);
|
|
233
|
+
return typeName;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// Handle resolved $ref - check for x-parser-schema-id which indicates original schema name
|
|
237
|
+
if (schema['x-parser-schema-id'] && typeof schema['x-parser-schema-id'] === 'string') {
|
|
238
|
+
const schemaId = schema['x-parser-schema-id'];
|
|
239
|
+
// Check if this matches a known component schema
|
|
240
|
+
if (schemaRegistry.has(schemaId)) {
|
|
241
|
+
const typeName = toTypeScriptTypeName(schemaId);
|
|
242
|
+
return typeName;
|
|
243
|
+
}
|
|
136
244
|
}
|
|
137
245
|
|
|
138
246
|
if (!schema.type) {
|
|
@@ -158,10 +266,19 @@ function generateModels(asyncapi) {
|
|
|
158
266
|
case 'boolean':
|
|
159
267
|
return 'boolean';
|
|
160
268
|
case 'array': {
|
|
161
|
-
|
|
162
|
-
|
|
269
|
+
if (schema.items) {
|
|
270
|
+
const itemType = jsonSchemaToTypeScriptType(schema.items, fieldName);
|
|
271
|
+
return `${itemType}[]`;
|
|
272
|
+
}
|
|
273
|
+
return 'any[]';
|
|
163
274
|
}
|
|
164
275
|
case 'object':
|
|
276
|
+
// For objects with properties, we should generate inline types or check if it's a known schema
|
|
277
|
+
if (schema.properties) {
|
|
278
|
+
// This is a complex object - for now return Record<string, any>
|
|
279
|
+
// In a more sophisticated implementation, we could generate inline types
|
|
280
|
+
return 'Record<string, any>';
|
|
281
|
+
}
|
|
165
282
|
return 'Record<string, any>';
|
|
166
283
|
default:
|
|
167
284
|
return 'any';
|
|
@@ -175,7 +292,7 @@ function generateModels(asyncapi) {
|
|
|
175
292
|
}
|
|
176
293
|
|
|
177
294
|
const fields = Object.entries(schema.properties).map(([fieldName, fieldSchema]) => {
|
|
178
|
-
const tsType = jsonSchemaToTypeScriptType(fieldSchema);
|
|
295
|
+
const tsType = jsonSchemaToTypeScriptType(fieldSchema, fieldName);
|
|
179
296
|
const optional = !schema.required || !schema.required.includes(fieldName);
|
|
180
297
|
const optionalMarker = optional ? '?' : '';
|
|
181
298
|
|
|
@@ -190,31 +307,54 @@ function generateModels(asyncapi) {
|
|
|
190
307
|
return fields;
|
|
191
308
|
}
|
|
192
309
|
|
|
193
|
-
// Generate interfaces for
|
|
310
|
+
// Generate interfaces for component schemas first (so they can be referenced by messages)
|
|
311
|
+
componentSchemas.forEach(schema => {
|
|
312
|
+
const doc = schema.description ? `/** ${schema.description} */\n` : `/** ${schema.name} */\n`;
|
|
313
|
+
|
|
314
|
+
content += `${doc}export interface ${schema.typeName} {\n`;
|
|
315
|
+
content += generateMessageInterface(schema.schema, schema.typeName);
|
|
316
|
+
content += '\n}\n\n';
|
|
317
|
+
|
|
318
|
+
// Track generated types to avoid duplicates
|
|
319
|
+
generatedTypes.add(schema.typeName);
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
// Generate interfaces for each message (only if not already generated as component schema)
|
|
194
323
|
messageSchemas.forEach(schema => {
|
|
324
|
+
const interfaceName = `${schema.typeName}Payload`;
|
|
325
|
+
|
|
326
|
+
// Check if this is a duplicate of a component schema
|
|
327
|
+
// For message payloads that match component schema names, skip the payload version
|
|
328
|
+
if (generatedTypes.has(schema.typeName) || generatedTypes.has(interfaceName)) {
|
|
329
|
+
// Skip generating the payload version if we already have the component schema
|
|
330
|
+
return;
|
|
331
|
+
}
|
|
332
|
+
|
|
195
333
|
const doc = schema.description ? `/** ${schema.description} */\n` : `/** ${schema.name} message payload */\n`;
|
|
196
334
|
|
|
197
|
-
content += `${doc}export interface ${
|
|
335
|
+
content += `${doc}export interface ${interfaceName} {\n`;
|
|
198
336
|
content += generateMessageInterface(schema.payload, schema.typeName);
|
|
199
|
-
content +=
|
|
337
|
+
content += '\n}\n\n';
|
|
338
|
+
|
|
339
|
+
generatedTypes.add(interfaceName);
|
|
200
340
|
});
|
|
201
341
|
|
|
202
342
|
// Generate a union type for all message payloads
|
|
203
343
|
if (messageSchemas.length > 0) {
|
|
204
344
|
const payloadTypes = messageSchemas.map(schema => `${schema.typeName}Payload`).join(' | ');
|
|
205
|
-
content +=
|
|
345
|
+
content += '/** Union type for all message payloads */\n';
|
|
206
346
|
content += `export type MessagePayload = ${payloadTypes};\n\n`;
|
|
207
347
|
|
|
208
348
|
// Generate message type constants
|
|
209
|
-
content +=
|
|
210
|
-
content +=
|
|
349
|
+
content += '/** Message type constants */\n';
|
|
350
|
+
content += 'export const MessageTypes = {\n';
|
|
211
351
|
messageSchemas.forEach(schema => {
|
|
212
352
|
content += ` ${schema.typeName.toUpperCase()}: '${schema.name}',\n`;
|
|
213
353
|
});
|
|
214
|
-
content +=
|
|
354
|
+
content += '} as const;\n\n';
|
|
215
355
|
|
|
216
|
-
content +=
|
|
217
|
-
content +=
|
|
356
|
+
content += '/** Message type union */\n';
|
|
357
|
+
content += 'export type MessageType = typeof MessageTypes[keyof typeof MessageTypes];\n\n';
|
|
218
358
|
}
|
|
219
359
|
|
|
220
360
|
return content;
|
|
@@ -226,4 +366,4 @@ module.exports = function ({ asyncapi, params }) {
|
|
|
226
366
|
{generateModels(asyncapi)}
|
|
227
367
|
</File>
|
|
228
368
|
);
|
|
229
|
-
}
|
|
369
|
+
};
|