@bagelink/sdk 1.20.52 → 1.20.56
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/package.json +1 -1
- package/src/index.js +715 -0
- package/src/openAPITools/StreamController.js +62 -0
- package/src/openAPITools/functionGenerator.js +979 -0
- package/src/openAPITools/index.js +40 -0
- package/src/openAPITools/moduleGenerator.js +125 -0
- package/src/openAPITools/streamClient.js +276 -0
- package/src/openAPITools/streamDetector.js +204 -0
- package/src/openAPITools/typeGenerator.js +48 -0
- package/src/openAPITools/types/index.js +19 -0
- package/src/openAPITools/types/utils.js +30 -0
- package/src/openAPITools/utils.js +282 -0
- package/src/unwrap.js +30 -0
- package/src/utils.js +58 -0
|
@@ -0,0 +1,979 @@
|
|
|
1
|
+
import { isSSEStream, extractSSEEventTypes, extractSSEEventInfo, extractSSEEventSchemas } from './streamDetector';
|
|
2
|
+
import { dereference, isReferenceObject } from './types/utils';
|
|
3
|
+
import { cleanPath, formatType, formatVarType, schemaToType, toCamelCaseSafe, toPascalCase, } from './utils';
|
|
4
|
+
/**
|
|
5
|
+
* Collection of types referenced across generated functions
|
|
6
|
+
*/
|
|
7
|
+
const allTypes = [];
|
|
8
|
+
const primitiveTypes = [
|
|
9
|
+
'string',
|
|
10
|
+
'number',
|
|
11
|
+
'boolean',
|
|
12
|
+
'null',
|
|
13
|
+
'void',
|
|
14
|
+
'any',
|
|
15
|
+
'{ [key: string]: any }',
|
|
16
|
+
'undefined',
|
|
17
|
+
];
|
|
18
|
+
// Tracking for function generation
|
|
19
|
+
const functionsInventory = {};
|
|
20
|
+
const pathOperations = [];
|
|
21
|
+
const streamEventTypes = {}; // Track stream endpoints and their event types
|
|
22
|
+
const streamEventInfo = {}; // Track detailed event info for type generation
|
|
23
|
+
const streamEventSchemas = {}; // Track typed schemas: streamTypeName → {event → TSType}
|
|
24
|
+
/**
|
|
25
|
+
* Collects non-primitive types for import statements
|
|
26
|
+
* @param typeName - The type name to analyze
|
|
27
|
+
*/
|
|
28
|
+
function collectTypeForImportStatement(typeName) {
|
|
29
|
+
typeName = typeName.trim().replace('[]', '');
|
|
30
|
+
// Handle union types
|
|
31
|
+
if (typeName.includes('|')) {
|
|
32
|
+
typeName.split('|').forEach((singleType) => {
|
|
33
|
+
collectTypeForImportStatement(singleType.trim());
|
|
34
|
+
});
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
const isPrimitive = primitiveTypes.includes(typeName);
|
|
38
|
+
typeName = formatType(typeName);
|
|
39
|
+
if (!typeName || isPrimitive) {
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
if (!allTypes.includes(typeName)) {
|
|
43
|
+
allTypes.push(typeName);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Wrapper for schemaToType that also collects types for import statements
|
|
48
|
+
* @param schema - The schema to convert to a type
|
|
49
|
+
* @returns The TypeScript type string
|
|
50
|
+
*/
|
|
51
|
+
function schemaToTypeWithCollection(schema) {
|
|
52
|
+
const type = schemaToType(schema);
|
|
53
|
+
collectTypeForImportStatement(type);
|
|
54
|
+
return type;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Extracts the response type from an OpenAPI response object
|
|
58
|
+
* @param response - The OpenAPI response object
|
|
59
|
+
* @returns Object with type string and whether it's an array
|
|
60
|
+
*/
|
|
61
|
+
function getResponseType(response) {
|
|
62
|
+
if (isReferenceObject(response)) {
|
|
63
|
+
return { type: undefined, isArray: false };
|
|
64
|
+
}
|
|
65
|
+
const mediaTypeObject = response.content?.['application/json'];
|
|
66
|
+
if (!mediaTypeObject?.schema) {
|
|
67
|
+
return { type: undefined, isArray: false };
|
|
68
|
+
}
|
|
69
|
+
const schema = dereference(mediaTypeObject.schema);
|
|
70
|
+
const typeString = schemaToTypeWithCollection(schema);
|
|
71
|
+
// Check if schema is an array type
|
|
72
|
+
const isArray = schema?.type === 'array' || typeString?.endsWith('[]') || false;
|
|
73
|
+
return { type: typeString, isArray };
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Generates the response type string from OpenAPI responses
|
|
77
|
+
* @param responses - The OpenAPI responses object
|
|
78
|
+
* @returns Object with type string and array detection
|
|
79
|
+
*/
|
|
80
|
+
function generateResponseType(responses) {
|
|
81
|
+
if (!responses) {
|
|
82
|
+
return { type: '', isArray: false };
|
|
83
|
+
}
|
|
84
|
+
const types = [];
|
|
85
|
+
let isArrayResponse = false;
|
|
86
|
+
for (const [statusCode, response] of Object.entries(responses)) {
|
|
87
|
+
if (statusCode.startsWith('2')) {
|
|
88
|
+
const { type: responseType, isArray } = getResponseType(response);
|
|
89
|
+
if (responseType && responseType !== 'any') {
|
|
90
|
+
types.push(responseType);
|
|
91
|
+
// If any successful response is an array, mark as array
|
|
92
|
+
if (isArray) {
|
|
93
|
+
isArrayResponse = true;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return { type: types.join(' | '), isArray: isArrayResponse };
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Extracts path parameters from an API path
|
|
102
|
+
* @param path - The API path
|
|
103
|
+
* @returns Array of parameter names or undefined
|
|
104
|
+
*/
|
|
105
|
+
function getParamsFromPath(path) {
|
|
106
|
+
const pathParamRegex = /\{[^}]+\}/g;
|
|
107
|
+
return path.match(pathParamRegex)?.map(p => p.slice(1, -1));
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Formats a path with template literals for parameters
|
|
111
|
+
* @param path - The API path
|
|
112
|
+
* @returns The formatted path string
|
|
113
|
+
*/
|
|
114
|
+
function formatPathWithParams(path) {
|
|
115
|
+
const params = getParamsFromPath(path);
|
|
116
|
+
if (!params) {
|
|
117
|
+
return `'${path}'`;
|
|
118
|
+
}
|
|
119
|
+
return `\`${path.replace(/\{([^}]+)\}/g, (_, paramName) => `\${${toCamelCaseSafe(paramName)}}`)}\``;
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Processes request body information from an OpenAPI operation
|
|
123
|
+
* @param requestBody - The OpenAPI request body object
|
|
124
|
+
* @returns Object containing request body parameter and payload information
|
|
125
|
+
*/
|
|
126
|
+
function generateRequestBody(requestBody) {
|
|
127
|
+
// if (!requestBody || !requestBody?.content)
|
|
128
|
+
// return { requestBodyParam: '', requestBodyPayload: '' }
|
|
129
|
+
const content = dereference(requestBody)?.content;
|
|
130
|
+
if (!content || Object.keys(content).length === 0) {
|
|
131
|
+
return { requestBodyParam: '', requestBodyPayload: '' };
|
|
132
|
+
}
|
|
133
|
+
// Handle multipart/form-data requests (file uploads)
|
|
134
|
+
if (content['multipart/form-data']) {
|
|
135
|
+
const multipartFormData = content['multipart/form-data'];
|
|
136
|
+
// Special case for file uploads
|
|
137
|
+
if (isReferenceObject(multipartFormData.schema) && multipartFormData.schema?.$ref?.includes('Body_upload_files')) {
|
|
138
|
+
return {
|
|
139
|
+
requestBodyParam: 'file: File, options?: UploadOptions & { dirPath?: string, tags?: string[] }',
|
|
140
|
+
requestBodyPayload: 'formData',
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
// Generic form data handling
|
|
144
|
+
return {
|
|
145
|
+
requestBodyParam: 'formData: FormData',
|
|
146
|
+
requestBodyPayload: 'formData',
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
// Handle regular JSON requests
|
|
150
|
+
const jsonContent = content['application/json'];
|
|
151
|
+
if (!jsonContent || !jsonContent.schema) {
|
|
152
|
+
return { requestBodyParam: '', requestBodyPayload: '' };
|
|
153
|
+
}
|
|
154
|
+
const bodySchema = dereference(jsonContent.schema);
|
|
155
|
+
const requestBodyType = schemaToType(bodySchema);
|
|
156
|
+
collectTypeForImportStatement(requestBodyType);
|
|
157
|
+
const requestBodyPayload = (toCamelCaseSafe(bodySchema.title) || toCamelCaseSafe(requestBodyType) || 'requestBody');
|
|
158
|
+
const requestBodyParam = formatVarType({
|
|
159
|
+
varName: requestBodyPayload,
|
|
160
|
+
schema: bodySchema,
|
|
161
|
+
defaultValue: bodySchema.default,
|
|
162
|
+
});
|
|
163
|
+
return { requestBodyParam, requestBodyPayload };
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* Generates function parameters from OpenAPI parameters
|
|
167
|
+
* @param path - The API path (used to detect path params missing from spec)
|
|
168
|
+
* @param params - The OpenAPI parameters
|
|
169
|
+
* @param isFileUpload - Whether this is a file upload operation
|
|
170
|
+
* @returns Object containing parameter information
|
|
171
|
+
*/
|
|
172
|
+
function generateFunctionParameters(path, params, isFileUpload = false) {
|
|
173
|
+
// Special handling for file uploads
|
|
174
|
+
if (isFileUpload) {
|
|
175
|
+
return {
|
|
176
|
+
config: {
|
|
177
|
+
params: 'dir_path: options?.dirPath, tags: options?.tags',
|
|
178
|
+
},
|
|
179
|
+
params: '', // Empty string to ensure file is the first parameter
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
const functionParams = [];
|
|
183
|
+
const queryParams = [];
|
|
184
|
+
// Track which path params are already declared from the spec
|
|
185
|
+
const declaredPathParams = new Set();
|
|
186
|
+
for (const p of (params ?? [])) {
|
|
187
|
+
const param = dereference(p);
|
|
188
|
+
const pSchema = dereference(param.schema);
|
|
189
|
+
const paramType = schemaToType(pSchema);
|
|
190
|
+
collectTypeForImportStatement(paramType);
|
|
191
|
+
const paramName = param.name;
|
|
192
|
+
let varName = toCamelCaseSafe(param.name);
|
|
193
|
+
// If param name differs from varName (e.g., "last-event-id" → "lastEventId"),
|
|
194
|
+
// prefix with underscore to avoid unused variable linting errors
|
|
195
|
+
const needsMapping = paramName !== varName;
|
|
196
|
+
if (needsMapping && (param.in === 'query' || param.in === 'header')) {
|
|
197
|
+
varName = `_${varName}`;
|
|
198
|
+
}
|
|
199
|
+
if (param.in === 'path') {
|
|
200
|
+
declaredPathParams.add(paramName);
|
|
201
|
+
}
|
|
202
|
+
if (param.in === 'path' || param.in === 'query' || param.in === 'header') {
|
|
203
|
+
functionParams.push(formatVarType({
|
|
204
|
+
varName,
|
|
205
|
+
schema: pSchema,
|
|
206
|
+
required: param.required,
|
|
207
|
+
defaultValue: pSchema?.default,
|
|
208
|
+
}));
|
|
209
|
+
}
|
|
210
|
+
if (param.in === 'query' || param.in === 'header') {
|
|
211
|
+
queryParams.push(needsMapping ? `'${paramName}': ${varName}` : paramName);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
// Inject any path params present in the URL but missing from the spec's parameters list
|
|
215
|
+
const pathParamsInUrl = getParamsFromPath(path) ?? [];
|
|
216
|
+
for (const paramName of pathParamsInUrl) {
|
|
217
|
+
if (!declaredPathParams.has(paramName)) {
|
|
218
|
+
const varName = toCamelCaseSafe(paramName);
|
|
219
|
+
functionParams.unshift(`${varName}: string`);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
if (functionParams.length === 0 && queryParams.length === 0) {
|
|
223
|
+
return {};
|
|
224
|
+
}
|
|
225
|
+
return {
|
|
226
|
+
params: functionParams.join(', '),
|
|
227
|
+
config: queryParams.length ? { params: queryParams.join(', ') } : undefined,
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
/**
|
|
231
|
+
* Parses a parameter string into name, type, and default value
|
|
232
|
+
* @param paramStr - The parameter string to parse
|
|
233
|
+
* @returns Object containing parsed parameter information
|
|
234
|
+
*/
|
|
235
|
+
function parseParameter(paramStr) {
|
|
236
|
+
const trimmed = paramStr.trim();
|
|
237
|
+
// Check if parameter is optional (has ?)
|
|
238
|
+
const isOptional = trimmed.includes('?:');
|
|
239
|
+
// Extract parameter name
|
|
240
|
+
const nameMatch = trimmed.match(/^([a-z_$][\w$]*)/i);
|
|
241
|
+
const name = nameMatch?.[1] || '';
|
|
242
|
+
// Extract type (between : and = or end of string)
|
|
243
|
+
const typeMatch = trimmed.match(/:\s*([^=]+)/);
|
|
244
|
+
const type = typeMatch?.[1]?.trim() || 'any';
|
|
245
|
+
// Extract default value if exists
|
|
246
|
+
const defaultMatch = trimmed.match(/[=](.+)$/);
|
|
247
|
+
const defaultValue = defaultMatch?.[1]?.trim();
|
|
248
|
+
return { name, type, defaultValue, isOptional };
|
|
249
|
+
}
|
|
250
|
+
/**
|
|
251
|
+
* Combines and formats all parameters into a single object destructuring parameter
|
|
252
|
+
* @param parameters - The parameters object
|
|
253
|
+
* @param requestBodyParam - The request body parameter
|
|
254
|
+
* @returns A formatted parameter string with object destructuring
|
|
255
|
+
*/
|
|
256
|
+
function combineAllParams(parameters, requestBodyParam) {
|
|
257
|
+
let allParamsArray = [];
|
|
258
|
+
if (parameters.params) {
|
|
259
|
+
allParamsArray = parameters.params.split(',').map(p => p.trim());
|
|
260
|
+
}
|
|
261
|
+
if (requestBodyParam) {
|
|
262
|
+
allParamsArray.push(requestBodyParam.trim());
|
|
263
|
+
}
|
|
264
|
+
if (allParamsArray.length === 0) {
|
|
265
|
+
return '';
|
|
266
|
+
}
|
|
267
|
+
// Parse all parameters
|
|
268
|
+
const parsedParams = allParamsArray.filter(Boolean).map(parseParameter);
|
|
269
|
+
// Build destructuring pattern with default values
|
|
270
|
+
const destructuredNames = parsedParams
|
|
271
|
+
.map(p => p.defaultValue ? `${p.name} = ${p.defaultValue}` : p.name)
|
|
272
|
+
.join(', ');
|
|
273
|
+
// Build type definition - all parameters should be optional in the type
|
|
274
|
+
const typeDefinition = parsedParams
|
|
275
|
+
.map((p) => {
|
|
276
|
+
// Remove default value from type, make all optional
|
|
277
|
+
return `${p.name}?: ${p.type}`;
|
|
278
|
+
})
|
|
279
|
+
.join(', ');
|
|
280
|
+
return `{ ${destructuredNames} }: { ${typeDefinition} } = {}`;
|
|
281
|
+
}
|
|
282
|
+
/**
|
|
283
|
+
* Generates a unique type name for a stream endpoint
|
|
284
|
+
*/
|
|
285
|
+
function generateStreamTypeName(path) {
|
|
286
|
+
return `${toPascalCase(path
|
|
287
|
+
.split('/')
|
|
288
|
+
.filter(p => p && !/\{|\}/.test(p))
|
|
289
|
+
.join('_'))}StreamEvents`;
|
|
290
|
+
}
|
|
291
|
+
/**
|
|
292
|
+
* Generates a Stream function for SSE endpoints with full type safety
|
|
293
|
+
* @param method - The HTTP method
|
|
294
|
+
* @param path - The original API path
|
|
295
|
+
* @param formattedPath - The formatted API path
|
|
296
|
+
* @param allParams - The combined parameter string
|
|
297
|
+
* @param requestBodyPayload - The request body payload
|
|
298
|
+
* @param eventTypes - Array of SSE event types
|
|
299
|
+
* @param queryParams - Query/header parameter mappings (e.g. "'run_id': _runId")
|
|
300
|
+
* @returns A string with the SSE stream function
|
|
301
|
+
*/
|
|
302
|
+
function generateStreamFunction(method, path, formattedPath, allParams, requestBodyPayload, eventTypes, queryParams) {
|
|
303
|
+
if (allParams === 'undefined') {
|
|
304
|
+
allParams = '';
|
|
305
|
+
}
|
|
306
|
+
// Extract parameter names for the stream call (unused but kept for future use)
|
|
307
|
+
// const paramNames = allParams ? allParams.match(/\w+(?=\s*[?:])/g) || [] : []
|
|
308
|
+
const bodyVar = requestBodyPayload || '{}';
|
|
309
|
+
// Generate type name for this stream
|
|
310
|
+
const streamTypeName = generateStreamTypeName(path);
|
|
311
|
+
// Store event types for later type generation
|
|
312
|
+
if (eventTypes?.length) {
|
|
313
|
+
streamEventTypes[streamTypeName] = eventTypes;
|
|
314
|
+
}
|
|
315
|
+
const eventTypesComment = eventTypes?.length
|
|
316
|
+
? `\n * Event types: ${eventTypes.map(t => `"${t}"`).join(', ')}`
|
|
317
|
+
: '';
|
|
318
|
+
const eventTypesExample = eventTypes?.length
|
|
319
|
+
? eventTypes.map(t => `\n * .on('${t}', (data) => console.log(data))`).join('')
|
|
320
|
+
: '\n * .on(\'message\', (data) => console.log(data))';
|
|
321
|
+
const typeAnnotation = eventTypes?.length
|
|
322
|
+
? `StreamController<${streamTypeName}>`
|
|
323
|
+
: 'StreamController';
|
|
324
|
+
const streamParams = allParams ? `${allParams}, options?: SSEStreamOptions` : `options?: SSEStreamOptions`;
|
|
325
|
+
const regularParams = allParams;
|
|
326
|
+
if (method === 'post') {
|
|
327
|
+
return `{
|
|
328
|
+
/**
|
|
329
|
+
* Stream SSE events from this endpoint (returns StreamController)${eventTypesComment}
|
|
330
|
+
*
|
|
331
|
+
* @example
|
|
332
|
+
* const stream = api.endpoint.stream(params)${eventTypesExample}
|
|
333
|
+
* .on('error', (err) => console.error(err))
|
|
334
|
+
* .on('complete', () => console.log('Done!'))
|
|
335
|
+
*
|
|
336
|
+
* // Close stream when needed
|
|
337
|
+
* stream.close()
|
|
338
|
+
*/
|
|
339
|
+
stream: (${streamParams}): ${typeAnnotation} => {
|
|
340
|
+
return __transport.createSSEStreamPost<${streamTypeName}>(${formattedPath}, ${bodyVar}, options)
|
|
341
|
+
},
|
|
342
|
+
/**
|
|
343
|
+
* Regular POST request (non-streaming)
|
|
344
|
+
*/
|
|
345
|
+
post: async (${regularParams}): Promise<AxiosResponse<any>> => {
|
|
346
|
+
return __transport.axios.post(${formattedPath}, ${bodyVar})
|
|
347
|
+
}
|
|
348
|
+
}`;
|
|
349
|
+
}
|
|
350
|
+
else {
|
|
351
|
+
// Build query string construction for GET SSE streams
|
|
352
|
+
let queryStringCode = '';
|
|
353
|
+
if (queryParams?.length) {
|
|
354
|
+
// Generate URLSearchParams code to append query params to the SSE URL
|
|
355
|
+
// Query params use 'original_name': varName mapping format
|
|
356
|
+
const paramEntries = queryParams.map(p => {
|
|
357
|
+
const match = p.match(/^'([^']+)':\s*(.+)$/);
|
|
358
|
+
if (match)
|
|
359
|
+
return { key: match[1], varName: match[2] };
|
|
360
|
+
// Simple case: param name is the same as var name
|
|
361
|
+
return { key: p, varName: p };
|
|
362
|
+
});
|
|
363
|
+
const conditions = paramEntries.map(({ key, varName }) => `if (${varName}) __qs.set('${key}', String(${varName}))`).join('\n ');
|
|
364
|
+
queryStringCode = [
|
|
365
|
+
`const __qs = new URLSearchParams()`,
|
|
366
|
+
conditions,
|
|
367
|
+
`const __qsSuffix = __qs.size ? "?" + __qs.toString() : ""`,
|
|
368
|
+
].join('\n ');
|
|
369
|
+
}
|
|
370
|
+
const regularParamStr = queryParams?.length ? `, { params: {${queryParams.join(', ')}} }` : '';
|
|
371
|
+
// For GET streams with query params, build URL with suffix; otherwise plain URL
|
|
372
|
+
const urlLine = queryParams?.length
|
|
373
|
+
? `const url = ${formattedPath} + __qsSuffix`
|
|
374
|
+
: `const url = ${formattedPath}`;
|
|
375
|
+
const streamBody = queryParams?.length
|
|
376
|
+
? `\n ${queryStringCode}\n ${urlLine}`
|
|
377
|
+
: `\n ${urlLine}`;
|
|
378
|
+
return `{
|
|
379
|
+
/**
|
|
380
|
+
* Stream SSE events from this endpoint (returns StreamController)${eventTypesComment}
|
|
381
|
+
*
|
|
382
|
+
* @example
|
|
383
|
+
* const stream = api.endpoint.stream(params)${eventTypesExample}
|
|
384
|
+
* .on('error', (err) => console.error(err))
|
|
385
|
+
* .on('complete', () => console.log('Done!'))
|
|
386
|
+
*
|
|
387
|
+
* // Close stream when needed
|
|
388
|
+
* stream.close()
|
|
389
|
+
*/
|
|
390
|
+
stream: (${streamParams}): ${typeAnnotation} => {${streamBody}
|
|
391
|
+
return __transport.createSSEStream<${streamTypeName}>(url, options)
|
|
392
|
+
},
|
|
393
|
+
/**
|
|
394
|
+
* Regular GET request (non-streaming)
|
|
395
|
+
*/
|
|
396
|
+
get: async (${regularParams}): Promise<AxiosResponse<any>> => {
|
|
397
|
+
return __transport.axios.get(${formattedPath}${regularParamStr})
|
|
398
|
+
}
|
|
399
|
+
}`;
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
/**
|
|
403
|
+
* Generates an Axios function call as a string that returns ApiResponse
|
|
404
|
+
* @param method - The HTTP method
|
|
405
|
+
* @param formattedPath - The formatted API path
|
|
406
|
+
* @param allParams - The combined parameter string
|
|
407
|
+
* @param responseTypeStr - The response type string (will be converted to ApiResponse)
|
|
408
|
+
* @param parameters - Additional parameters
|
|
409
|
+
* @param requestBodyPayload - The request body payload
|
|
410
|
+
* @returns A string with the Axios function that returns ApiResponse
|
|
411
|
+
*/
|
|
412
|
+
function generateAxiosFunction(method, formattedPath, allParams, responseTypeStr, parameters, requestBodyPayload, isPrimitiveResponse) {
|
|
413
|
+
if (allParams === 'undefined') {
|
|
414
|
+
allParams = '';
|
|
415
|
+
}
|
|
416
|
+
// Convert AxiosResponse<T> to ApiResponse<T> in the return type
|
|
417
|
+
const apiResponseType = responseTypeStr.replace(/Promise<AxiosResponse<(.+)>>/, 'Promise<ApiResponse<$1>>');
|
|
418
|
+
let axiosFunction = `async (${allParams})${apiResponseType} => {`;
|
|
419
|
+
const returnExpression = isPrimitiveResponse
|
|
420
|
+
? 'response.data'
|
|
421
|
+
: 'ApiResponse.create(response.data, response)';
|
|
422
|
+
const paramStr = parameters.config?.params
|
|
423
|
+
? `params: {${parameters.config.params}}`
|
|
424
|
+
: '';
|
|
425
|
+
if (requestBodyPayload === 'formData') {
|
|
426
|
+
// Check if this is a file upload with specific file parameter
|
|
427
|
+
if (allParams.includes('file: File')) {
|
|
428
|
+
axiosFunction += `
|
|
429
|
+
const formData = new FormData()
|
|
430
|
+
formData.append('file', file)
|
|
431
|
+
formData.append('upload', file)
|
|
432
|
+
const response = await __transport.axios.${method}(${formattedPath}, formData, {
|
|
433
|
+
headers: { 'Content-Type': 'multipart/form-data' },
|
|
434
|
+
onUploadProgress: options?.onUploadProgress${paramStr ? `, ${paramStr}` : ''}
|
|
435
|
+
})
|
|
436
|
+
return ${returnExpression}`;
|
|
437
|
+
}
|
|
438
|
+
else {
|
|
439
|
+
// Generic FormData handling
|
|
440
|
+
axiosFunction += `
|
|
441
|
+
const response = await __transport.axios.${method}(${formattedPath}, formData, {
|
|
442
|
+
headers: { 'Content-Type': 'multipart/form-data' }${paramStr ? `, ${paramStr}` : ''}
|
|
443
|
+
})
|
|
444
|
+
return ${returnExpression}`;
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
else {
|
|
448
|
+
const configParams = paramStr ? `, { ${paramStr} }` : '';
|
|
449
|
+
const bodyVar = requestBodyPayload || '{}';
|
|
450
|
+
axiosFunction += `
|
|
451
|
+
const response = await __transport.axios.${method}(${formattedPath}${['get', 'delete'].includes(method)
|
|
452
|
+
? configParams
|
|
453
|
+
: `, ${bodyVar}${configParams}`})
|
|
454
|
+
return ${returnExpression}`;
|
|
455
|
+
}
|
|
456
|
+
axiosFunction += '}';
|
|
457
|
+
return axiosFunction;
|
|
458
|
+
}
|
|
459
|
+
/**
|
|
460
|
+
* Builds a JSDoc comment for an OpenAPI operation
|
|
461
|
+
* @param operation - The OpenAPI operation object
|
|
462
|
+
* @param method - The HTTP method
|
|
463
|
+
* @param path - The API path
|
|
464
|
+
* @returns The JSDoc comment as a string
|
|
465
|
+
*/
|
|
466
|
+
function buildJSDocComment(operation, method, path) {
|
|
467
|
+
let functionComment = '/**\n';
|
|
468
|
+
// Add summary
|
|
469
|
+
if (operation.summary) {
|
|
470
|
+
functionComment += ` * ${operation.summary}\n`;
|
|
471
|
+
}
|
|
472
|
+
// Add description if available
|
|
473
|
+
if (operation.description) {
|
|
474
|
+
// If there's already a summary, add a line break
|
|
475
|
+
if (operation.summary) {
|
|
476
|
+
functionComment += ` *\n`;
|
|
477
|
+
}
|
|
478
|
+
// Split description into lines and add each line with proper JSDoc formatting
|
|
479
|
+
const descriptionLines = operation.description.split('\n');
|
|
480
|
+
descriptionLines.forEach((line) => {
|
|
481
|
+
functionComment += ` * ${line}\n`;
|
|
482
|
+
});
|
|
483
|
+
}
|
|
484
|
+
// Add endpoint path and method information
|
|
485
|
+
functionComment += ` *\n * @endpoint ${method.toUpperCase()} ${path}\n`;
|
|
486
|
+
functionComment += ' */\n';
|
|
487
|
+
return functionComment;
|
|
488
|
+
}
|
|
489
|
+
/**
|
|
490
|
+
* Generates a function for an OpenAPI operation
|
|
491
|
+
* @param method - The HTTP method
|
|
492
|
+
* @param path - The API path
|
|
493
|
+
* @param operation - The OpenAPI operation object
|
|
494
|
+
* @returns The function as a string
|
|
495
|
+
*/
|
|
496
|
+
function generateFunctionForOperation(method, path, operation) {
|
|
497
|
+
if (!operation) {
|
|
498
|
+
return '';
|
|
499
|
+
}
|
|
500
|
+
// Check if this is an SSE stream endpoint
|
|
501
|
+
const isStream = isSSEStream(operation);
|
|
502
|
+
// Validate: GET and DELETE requests should not have request bodies
|
|
503
|
+
const methodLower = method.toLowerCase();
|
|
504
|
+
if (['get', 'delete'].includes(methodLower) && operation.requestBody) {
|
|
505
|
+
const requestBodyDeref = dereference(operation.requestBody);
|
|
506
|
+
const content = requestBodyDeref?.content;
|
|
507
|
+
// Check if request body has actual content with schemas
|
|
508
|
+
if (content && Object.keys(content).length > 0) {
|
|
509
|
+
// Check if any content type has a schema (indicating actual body data)
|
|
510
|
+
const hasSchema = Object.values(content).some(mediaType => mediaType && 'schema' in mediaType && mediaType.schema !== undefined && mediaType.schema !== null);
|
|
511
|
+
if (hasSchema) {
|
|
512
|
+
throw new Error(`Invalid OpenAPI spec: ${method.toUpperCase()} request at path "${path}" has a request body. `
|
|
513
|
+
+ `GET and DELETE requests should not have request bodies according to HTTP standards. `
|
|
514
|
+
+ `Please move the body parameters to query parameters or use a different HTTP method.`);
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
// Check if this is a file upload
|
|
519
|
+
const multiPartSchema = dereference(operation.requestBody)?.content?.['multipart/form-data']?.schema;
|
|
520
|
+
const isFileUpload = isReferenceObject(multiPartSchema) && multiPartSchema?.$ref?.includes('Body_upload_files');
|
|
521
|
+
const parameters = generateFunctionParameters(path, operation.parameters, isFileUpload);
|
|
522
|
+
const { requestBodyParam, requestBodyPayload } = generateRequestBody(operation.requestBody);
|
|
523
|
+
// Handle parameters appropriately based on operation type
|
|
524
|
+
const allParams = isFileUpload
|
|
525
|
+
? 'file: File, options?: UploadOptions & { dirPath?: string, tags?: string[] }'
|
|
526
|
+
: combineAllParams(parameters, requestBodyParam);
|
|
527
|
+
const { type: responseType, isArray: isArrayResponse } = generateResponseType(operation.responses);
|
|
528
|
+
const isPrimitiveResponse = primitiveTypes.includes(responseType);
|
|
529
|
+
// For arrays: ApiResponse<T[]> (works as array, has metadata)
|
|
530
|
+
// For single objects: T (runtime adds metadata, but type is plain T for assignment)
|
|
531
|
+
// Note: At runtime, single objects have metadata but TypeScript sees plain type for better DX
|
|
532
|
+
const responseTypeStr = responseType
|
|
533
|
+
? isArrayResponse
|
|
534
|
+
? `: Promise<ApiResponse<${responseType}>>`
|
|
535
|
+
: `: Promise<${responseType}>`
|
|
536
|
+
: '';
|
|
537
|
+
// Create JSDoc comment with OpenAPI documentation
|
|
538
|
+
const functionComment = buildJSDocComment(operation, method, path);
|
|
539
|
+
// Generate stream function for SSE endpoints
|
|
540
|
+
if (isStream) {
|
|
541
|
+
const eventTypes = extractSSEEventTypes(operation);
|
|
542
|
+
const eventInfo = extractSSEEventInfo(operation);
|
|
543
|
+
const eventSchemas = extractSSEEventSchemas(operation);
|
|
544
|
+
// Store detailed event info and typed schemas for type generation
|
|
545
|
+
const streamTypeName = generateStreamTypeName(path);
|
|
546
|
+
if (eventInfo?.length) {
|
|
547
|
+
streamEventInfo[streamTypeName] = eventInfo;
|
|
548
|
+
}
|
|
549
|
+
if (eventSchemas && Object.keys(eventSchemas).length > 0) {
|
|
550
|
+
streamEventSchemas[streamTypeName] = eventSchemas;
|
|
551
|
+
// Collect schema type names for import statements
|
|
552
|
+
Object.values(eventSchemas).forEach(tsType => collectTypeForImportStatement(tsType));
|
|
553
|
+
}
|
|
554
|
+
// Extract query param mappings for GET SSE streams (e.g. "'run_id': _runId")
|
|
555
|
+
const queryParams = parameters.config?.params
|
|
556
|
+
? parameters.config.params.split(',').map(p => p.trim()).filter(Boolean)
|
|
557
|
+
: undefined;
|
|
558
|
+
return functionComment + generateStreamFunction(method, path, formatPathWithParams(path), allParams, requestBodyPayload, eventTypes, queryParams);
|
|
559
|
+
}
|
|
560
|
+
return functionComment + generateAxiosFunction(method, formatPathWithParams(path), allParams, responseTypeStr, parameters, requestBodyPayload, isPrimitiveResponse);
|
|
561
|
+
}
|
|
562
|
+
/**
|
|
563
|
+
* Checks if there's a conflict with an existing path operation
|
|
564
|
+
* @param path - The API path
|
|
565
|
+
* @param method - The HTTP method
|
|
566
|
+
* @returns Whether there's a conflict
|
|
567
|
+
*/
|
|
568
|
+
function hasConflict(path, method) {
|
|
569
|
+
const cleanPathName = path
|
|
570
|
+
.split('/')
|
|
571
|
+
.filter(p => p && !/\{|\}/.test(p))
|
|
572
|
+
.join('/');
|
|
573
|
+
const matchingPaths = pathOperations.filter(p => p.path === cleanPathName && p.method === method);
|
|
574
|
+
pathOperations.push({ path: cleanPathName, method });
|
|
575
|
+
return matchingPaths.length > 0;
|
|
576
|
+
}
|
|
577
|
+
/**
|
|
578
|
+
* Generates a random string for function IDs
|
|
579
|
+
* @returns A random string
|
|
580
|
+
*/
|
|
581
|
+
function generateRandomString() {
|
|
582
|
+
return Math.random().toString(36).slice(7);
|
|
583
|
+
}
|
|
584
|
+
/**
|
|
585
|
+
* Creates a function placeholder and stores it in the inventory
|
|
586
|
+
* @param path - The API path
|
|
587
|
+
* @param method - The HTTP method
|
|
588
|
+
* @param operation - The OpenAPI operation
|
|
589
|
+
* @returns A function ID
|
|
590
|
+
*/
|
|
591
|
+
function createFunctionPlaceholder(path, method, operation) {
|
|
592
|
+
const funcID = generateRandomString();
|
|
593
|
+
if (operation) {
|
|
594
|
+
functionsInventory[funcID] = generateFunctionForOperation(method, path, operation);
|
|
595
|
+
}
|
|
596
|
+
else {
|
|
597
|
+
functionsInventory[funcID] = '';
|
|
598
|
+
}
|
|
599
|
+
return funcID;
|
|
600
|
+
}
|
|
601
|
+
/**
|
|
602
|
+
* Handles a path segment when generating functions
|
|
603
|
+
* @param path - The API path
|
|
604
|
+
* @param operation - The OpenAPI operation
|
|
605
|
+
* @param existingObj - Existing object to merge with
|
|
606
|
+
* @returns The updated object
|
|
607
|
+
*/
|
|
608
|
+
function handlePathSegment(path, operation, existingObj = {}) {
|
|
609
|
+
const methods = Object.keys(operation);
|
|
610
|
+
const obj = {};
|
|
611
|
+
for (const method of methods) {
|
|
612
|
+
let functionName = method.toLowerCase();
|
|
613
|
+
if (hasConflict(path, method)) {
|
|
614
|
+
const params = getParamsFromPath(path);
|
|
615
|
+
functionName += params ? `By${toPascalCase(params.pop() || '')}` : 'All';
|
|
616
|
+
}
|
|
617
|
+
obj[functionName] = createFunctionPlaceholder(path, method, operation[method]);
|
|
618
|
+
}
|
|
619
|
+
return { ...obj, ...existingObj };
|
|
620
|
+
}
|
|
621
|
+
/**
|
|
622
|
+
* Generates TypeScript API client functions from OpenAPI paths
|
|
623
|
+
* @param paths - The OpenAPI paths object
|
|
624
|
+
* @param baseUrl - The base URL for the API
|
|
625
|
+
* @returns The generated TypeScript code
|
|
626
|
+
*/
|
|
627
|
+
function generateFunctionsInternal(paths, baseUrl, moduleFactoryName) {
|
|
628
|
+
// Generation can run more than once in the same process (for example, one
|
|
629
|
+
// invocation per explicitly owned module). Keep each result independent.
|
|
630
|
+
allTypes.length = 0;
|
|
631
|
+
pathOperations.length = 0;
|
|
632
|
+
for (const inventory of [functionsInventory, streamEventTypes, streamEventInfo, streamEventSchemas]) {
|
|
633
|
+
for (const key of Object.keys(inventory))
|
|
634
|
+
delete inventory[key];
|
|
635
|
+
}
|
|
636
|
+
const body = {};
|
|
637
|
+
const allPathsClean = Object.keys(paths).map(cleanPath);
|
|
638
|
+
const httpMethods = new Set(['get', 'put', 'post', 'delete', 'options', 'head', 'patch', 'trace']);
|
|
639
|
+
// Process all paths to generate functions
|
|
640
|
+
for (const [path, operation] of Object.entries(paths)) {
|
|
641
|
+
const splitPath = path.split('/').filter(p => p && !/\{|\}/.test(p));
|
|
642
|
+
splitPath.reduce((acc, key, index, array) => {
|
|
643
|
+
const objFuncKey = toCamelCaseSafe(key);
|
|
644
|
+
if (!objFuncKey) {
|
|
645
|
+
return acc;
|
|
646
|
+
}
|
|
647
|
+
const methods = Object.keys(operation)
|
|
648
|
+
.filter(method => httpMethods.has(method));
|
|
649
|
+
if (methods.length === 0)
|
|
650
|
+
return acc;
|
|
651
|
+
const currentPathClean = cleanPath(path);
|
|
652
|
+
const currentPathPrefix = splitPath.slice(0, index + 1).join('/');
|
|
653
|
+
// Check if any other path has this path as a prefix (indicating child paths exist)
|
|
654
|
+
const prefixWithSlash = `${currentPathPrefix}/`;
|
|
655
|
+
const hasChildPaths = allPathsClean.some(otherPath => otherPath !== currentPathClean && otherPath.startsWith(prefixWithSlash));
|
|
656
|
+
if (index === array.length - 1
|
|
657
|
+
&& methods.length === 1
|
|
658
|
+
&& allPathsClean.filter(p => p === currentPathClean).length === 1
|
|
659
|
+
&& !hasChildPaths) {
|
|
660
|
+
// Single method endpoint with unique path and no child paths
|
|
661
|
+
const method = methods[0];
|
|
662
|
+
const opp = operation[method];
|
|
663
|
+
acc[objFuncKey] = createFunctionPlaceholder(path, method, opp);
|
|
664
|
+
}
|
|
665
|
+
else if (index === array.length - 1) {
|
|
666
|
+
// Multiple methods endpoint or path with conflicts or has child paths
|
|
667
|
+
// Ensure we have an object to merge with (handlePathSegment expects an object)
|
|
668
|
+
const existingValue = acc[objFuncKey];
|
|
669
|
+
const existingObj = (existingValue && typeof existingValue === 'object' && !Array.isArray(existingValue))
|
|
670
|
+
? existingValue
|
|
671
|
+
: {};
|
|
672
|
+
acc[objFuncKey] = handlePathSegment(path, operation, existingObj);
|
|
673
|
+
}
|
|
674
|
+
else if (!acc[objFuncKey] || typeof acc[objFuncKey] !== 'object') {
|
|
675
|
+
// Intermediate path segment - ensure it's an object
|
|
676
|
+
// If it's already a function, we need to preserve it but this shouldn't happen
|
|
677
|
+
// if hasChildPaths check works correctly
|
|
678
|
+
acc[objFuncKey] = {};
|
|
679
|
+
}
|
|
680
|
+
return acc[objFuncKey];
|
|
681
|
+
}, body);
|
|
682
|
+
}
|
|
683
|
+
// Generate TypeScript code from processed paths
|
|
684
|
+
let tsString = 'function createEndpointNamespaces(__transport: ApiTransport) {\n';
|
|
685
|
+
const apiObjectEntries = [];
|
|
686
|
+
// Convert object structure to TypeScript
|
|
687
|
+
for (const [parent, object] of Object.entries(body)) {
|
|
688
|
+
tsString += `
|
|
689
|
+
/**
|
|
690
|
+
* API functions for ${parent} endpoints
|
|
691
|
+
*/
|
|
692
|
+
const ${parent} = ${JSON.stringify(object, undefined, 2)};\n`;
|
|
693
|
+
apiObjectEntries.push(parent);
|
|
694
|
+
}
|
|
695
|
+
// Create aggregated api object
|
|
696
|
+
tsString += `
|
|
697
|
+
/**
|
|
698
|
+
* Aggregated API object containing all endpoints
|
|
699
|
+
*/
|
|
700
|
+
const api = {
|
|
701
|
+
${apiObjectEntries.map(name => ` ${name},`).join('\n')}
|
|
702
|
+
};
|
|
703
|
+
return { api, ${apiObjectEntries.join(', ')} }
|
|
704
|
+
}\n`;
|
|
705
|
+
// Replace placeholders with actual function bodies
|
|
706
|
+
Object.entries(functionsInventory).forEach(([key, value]) => {
|
|
707
|
+
tsString = tsString.replace(`"${key}"`, value);
|
|
708
|
+
});
|
|
709
|
+
// Generate the final TypeScript file
|
|
710
|
+
return moduleFactoryName
|
|
711
|
+
? moduleFileTemplate(tsString, allTypes, moduleFactoryName)
|
|
712
|
+
: fileTemplate(tsString, allTypes, baseUrl, apiObjectEntries);
|
|
713
|
+
}
|
|
714
|
+
export function generateFunctions(paths, baseUrl) {
|
|
715
|
+
return generateFunctionsInternal(paths, baseUrl);
|
|
716
|
+
}
|
|
717
|
+
/** Generate a transport-bound module client without legacy singleton state. */
|
|
718
|
+
export function generateModuleFunctions(paths, baseUrl, moduleFactoryName) {
|
|
719
|
+
return generateFunctionsInternal(paths, baseUrl, moduleFactoryName);
|
|
720
|
+
}
|
|
721
|
+
/**
|
|
722
|
+
* Generates TypeScript type definitions for stream events
|
|
723
|
+
* Uses detailed event info if available, falls back to simple event list
|
|
724
|
+
* @returns TypeScript type definitions string
|
|
725
|
+
*/
|
|
726
|
+
function generateStreamEventTypeDefinitions() {
|
|
727
|
+
if (Object.keys(streamEventTypes).length === 0) {
|
|
728
|
+
return '';
|
|
729
|
+
}
|
|
730
|
+
let typeDefs = '\n// ============================================================================\n';
|
|
731
|
+
typeDefs += '// Stream Event Type Definitions (Fully Typed!)\n';
|
|
732
|
+
typeDefs += '// ============================================================================\n\n';
|
|
733
|
+
for (const [typeName, events] of Object.entries(streamEventTypes)) {
|
|
734
|
+
const eventInfo = streamEventInfo[typeName];
|
|
735
|
+
const eventSchemas = streamEventSchemas[typeName]; // {event_name: "TSTypeName"} or undefined
|
|
736
|
+
typeDefs += `/**\n * Event types for ${typeName.replace('StreamEvents', '')} stream\n`;
|
|
737
|
+
typeDefs += ` * Events: ${events.map(e => `"${e}"`).join(', ')}\n`;
|
|
738
|
+
if (eventInfo?.length) {
|
|
739
|
+
typeDefs += ` *\n`;
|
|
740
|
+
for (const info of eventInfo) {
|
|
741
|
+
typeDefs += ` * - **${info.name}**: ${info.description || 'Event data'}\n`;
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
typeDefs += ` */\n`;
|
|
745
|
+
typeDefs += `export interface ${typeName} {\n`;
|
|
746
|
+
if (eventSchemas && Object.keys(eventSchemas).length > 0) {
|
|
747
|
+
// Fully typed: each event maps to its Pydantic model's TypeScript type
|
|
748
|
+
for (const event of events) {
|
|
749
|
+
const tsType = eventSchemas[event];
|
|
750
|
+
if (tsType) {
|
|
751
|
+
typeDefs += ` /** ${event} event — payload shape defined by ${tsType} */\n`;
|
|
752
|
+
typeDefs += ` ${event}: ${tsType}\n`;
|
|
753
|
+
}
|
|
754
|
+
else {
|
|
755
|
+
typeDefs += ` /** ${event} event data */\n`;
|
|
756
|
+
typeDefs += ` ${event}: any\n`;
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
else if (eventInfo?.length) {
|
|
761
|
+
// Field-level info from markdown docs but no schema refs
|
|
762
|
+
for (const info of eventInfo) {
|
|
763
|
+
typeDefs += ` /**\n * ${info.description || info.name}\n`;
|
|
764
|
+
if (info.fields?.length) {
|
|
765
|
+
info.fields.forEach(field => {
|
|
766
|
+
typeDefs += ` * - \`${field.name}\`: ${field.description || 'Field data'}\n`;
|
|
767
|
+
});
|
|
768
|
+
}
|
|
769
|
+
typeDefs += ` */\n`;
|
|
770
|
+
if (info.fields?.length) {
|
|
771
|
+
typeDefs += ` ${info.name}: {\n`;
|
|
772
|
+
for (const field of info.fields) {
|
|
773
|
+
typeDefs += ` /** ${field.description || field.name} */\n`;
|
|
774
|
+
typeDefs += ` ${field.name}: any\n`;
|
|
775
|
+
}
|
|
776
|
+
typeDefs += ` }\n`;
|
|
777
|
+
}
|
|
778
|
+
else {
|
|
779
|
+
typeDefs += ` ${info.name}: any\n`;
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
else {
|
|
784
|
+
// Fallback: no type info at all
|
|
785
|
+
for (const event of events) {
|
|
786
|
+
typeDefs += ` /** ${event} event data */\n`;
|
|
787
|
+
typeDefs += ` ${event}: any\n`;
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
typeDefs += '}\n\n';
|
|
791
|
+
}
|
|
792
|
+
return typeDefs;
|
|
793
|
+
}
|
|
794
|
+
/**
|
|
795
|
+
* Generates the TypeScript file template
|
|
796
|
+
* @param tsString - The generated TypeScript code
|
|
797
|
+
* @param typeForImport - Types to import
|
|
798
|
+
* @param baseURL - The base URL for the API
|
|
799
|
+
* @returns The final TypeScript file content
|
|
800
|
+
*/
|
|
801
|
+
function fileTemplate(tsString, typeForImport, baseURL, apiObjectEntries) {
|
|
802
|
+
const streamTypeDefs = generateStreamEventTypeDefinitions();
|
|
803
|
+
const hasStreamEndpoints = Object.keys(streamEventTypes).length > 0;
|
|
804
|
+
const streamImports = hasStreamEndpoints
|
|
805
|
+
? `type StreamController, type SSEStreamOptions,`
|
|
806
|
+
: '';
|
|
807
|
+
const templateCode = `import type { AxiosResponse } from 'axios';
|
|
808
|
+
import type { ${typeForImport.join(', ')} } from './types.d';
|
|
809
|
+
import {
|
|
810
|
+
ApiResponse,
|
|
811
|
+
createApiTransport,
|
|
812
|
+
configureApiTransport,
|
|
813
|
+
parseApiError,
|
|
814
|
+
formatFieldErrors,
|
|
815
|
+
useApiRequest,
|
|
816
|
+
useCancellableRequest,
|
|
817
|
+
unwrap,
|
|
818
|
+
${streamImports}
|
|
819
|
+
type ApiConfig,
|
|
820
|
+
type ParsedError,
|
|
821
|
+
type FastAPIValidationError,
|
|
822
|
+
type FastAPIErrorResponse,
|
|
823
|
+
type ResponseMetadata,
|
|
824
|
+
type ApiRequestState,
|
|
825
|
+
type RequestInterceptor,
|
|
826
|
+
type ResponseInterceptor,
|
|
827
|
+
type ApiTransport,
|
|
828
|
+
type Unwrapped
|
|
829
|
+
} from '@bagelink/sdk';
|
|
830
|
+
import type { Ref } from 'vue';
|
|
831
|
+
|
|
832
|
+
/**
|
|
833
|
+
* Options for file upload operations
|
|
834
|
+
*/
|
|
835
|
+
export interface UploadOptions {
|
|
836
|
+
/** Progress callback for upload operations */
|
|
837
|
+
onUploadProgress?: (progressEvent: any) => void
|
|
838
|
+
/** Directory path for the uploaded file */
|
|
839
|
+
dirPath?: string
|
|
840
|
+
/** Tags to associate with the uploaded file */
|
|
841
|
+
tags?: string[]
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
${streamTypeDefs}
|
|
845
|
+
|
|
846
|
+
const defaultTransport = createApiTransport({ baseURL: '${baseURL}' })
|
|
847
|
+
|
|
848
|
+
${tsString}
|
|
849
|
+
|
|
850
|
+
const defaultBindings = createEndpointNamespaces(defaultTransport)
|
|
851
|
+
export const api = defaultBindings.api
|
|
852
|
+
${apiObjectEntries.map(name => `export const ${name} = defaultBindings.${name}`).join('\n')}
|
|
853
|
+
export const axios = defaultTransport.axios
|
|
854
|
+
|
|
855
|
+
/** Bind all generated endpoint namespaces to an isolated transport. */
|
|
856
|
+
export function createApiClient(transport: ApiTransport) {
|
|
857
|
+
return createEndpointNamespaces(transport)
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
/** Resolve the default client's SSE base URL. */
|
|
861
|
+
export function getStreamBaseURL(): string {
|
|
862
|
+
return defaultTransport.getStreamBaseURL()
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
// ============================================================================
|
|
866
|
+
// API Instance & Configuration
|
|
867
|
+
// ============================================================================
|
|
868
|
+
|
|
869
|
+
/**
|
|
870
|
+
* API instance type with enhanced response objects
|
|
871
|
+
*/
|
|
872
|
+
type ApiInstance = typeof api & {
|
|
873
|
+
$raw: typeof api
|
|
874
|
+
$lastResponse: Ref<AxiosResponse | null>
|
|
875
|
+
$axios: typeof axios
|
|
876
|
+
$ApiResponse: typeof ApiResponse
|
|
877
|
+
$config: Ref<ApiConfig | null>
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
// Global state
|
|
881
|
+
let apiInstance: ApiInstance | null = null
|
|
882
|
+
|
|
883
|
+
/**
|
|
884
|
+
* Create and configure the API instance
|
|
885
|
+
* @param config - API configuration options
|
|
886
|
+
* @returns Configured API instance
|
|
887
|
+
*/
|
|
888
|
+
export function createApi(config: ApiConfig = {}): ApiInstance {
|
|
889
|
+
configureApiTransport(defaultTransport, config)
|
|
890
|
+
|
|
891
|
+
// API already returns ApiResponse directly!
|
|
892
|
+
// Just add the special properties
|
|
893
|
+
apiInstance = {
|
|
894
|
+
...api,
|
|
895
|
+
$raw: api,
|
|
896
|
+
$lastResponse: defaultTransport.lastResponse,
|
|
897
|
+
$axios: axios,
|
|
898
|
+
$ApiResponse: ApiResponse,
|
|
899
|
+
$config: defaultTransport.config
|
|
900
|
+
} as ApiInstance
|
|
901
|
+
|
|
902
|
+
return apiInstance
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
/**
|
|
906
|
+
* Access the configured API instance
|
|
907
|
+
* @returns API instance with enhanced response objects
|
|
908
|
+
* @throws Error if createApi hasn't been called
|
|
909
|
+
*/
|
|
910
|
+
export function useApi(): ApiInstance {
|
|
911
|
+
if (!apiInstance) {
|
|
912
|
+
throw new Error(
|
|
913
|
+
'API not initialized. Call createApi() first.\\n\\n' +
|
|
914
|
+
'Example:\\n' +
|
|
915
|
+
'import { createApi } from \\'./generated/api\\'\\n' +
|
|
916
|
+
'createApi({ baseURL: \\'/api\\' })'
|
|
917
|
+
)
|
|
918
|
+
}
|
|
919
|
+
return apiInstance
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
// ============================================================================
|
|
923
|
+
// Re-export utilities for convenience
|
|
924
|
+
// ============================================================================
|
|
925
|
+
|
|
926
|
+
export {
|
|
927
|
+
parseApiError,
|
|
928
|
+
formatFieldErrors,
|
|
929
|
+
useApiRequest,
|
|
930
|
+
useCancellableRequest,
|
|
931
|
+
unwrap,
|
|
932
|
+
ApiResponse,
|
|
933
|
+
type ApiConfig,
|
|
934
|
+
type ParsedError,
|
|
935
|
+
type ResponseMetadata,
|
|
936
|
+
type ApiRequestState,
|
|
937
|
+
type FastAPIValidationError,
|
|
938
|
+
type FastAPIErrorResponse,
|
|
939
|
+
type RequestInterceptor,
|
|
940
|
+
type ResponseInterceptor,
|
|
941
|
+
type Unwrapped
|
|
942
|
+
}
|
|
943
|
+
`;
|
|
944
|
+
// Remove potential double quotes around property names
|
|
945
|
+
return templateCode.replace(/"([^"]+)":/g, '$1:');
|
|
946
|
+
}
|
|
947
|
+
function moduleFileTemplate(tsString, typeForImport, moduleFactoryName) {
|
|
948
|
+
const streamTypeDefs = generateStreamEventTypeDefinitions();
|
|
949
|
+
const hasStreamEndpoints = Object.keys(streamEventTypes).length > 0;
|
|
950
|
+
const streamImports = hasStreamEndpoints
|
|
951
|
+
? 'type StreamController, type SSEStreamOptions,'
|
|
952
|
+
: '';
|
|
953
|
+
const typeImport = typeForImport.length
|
|
954
|
+
? `import type { ${typeForImport.join(', ')} } from './types.d';`
|
|
955
|
+
: '';
|
|
956
|
+
const templateCode = `${typeImport}
|
|
957
|
+
import {
|
|
958
|
+
\tApiResponse,
|
|
959
|
+
\t${streamImports}
|
|
960
|
+
\ttype ApiTransport
|
|
961
|
+
} from '@bagelink/sdk';
|
|
962
|
+
|
|
963
|
+
export interface UploadOptions {
|
|
964
|
+
\tonUploadProgress?: (progressEvent: any) => void
|
|
965
|
+
\tdirPath?: string
|
|
966
|
+
\ttags?: string[]
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
${streamTypeDefs}
|
|
970
|
+
|
|
971
|
+
${tsString}
|
|
972
|
+
|
|
973
|
+
/** Bind this module's generated endpoint namespaces to the app transport. */
|
|
974
|
+
export function ${moduleFactoryName}(transport: ApiTransport) {
|
|
975
|
+
\treturn createEndpointNamespaces(transport)
|
|
976
|
+
}
|
|
977
|
+
`;
|
|
978
|
+
return templateCode.replace(/"([^"]+)":/g, '$1:');
|
|
979
|
+
}
|