@bagelink/sdk 1.20.54 → 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.
@@ -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
+ }