@bagelink/sdk 1.7.101 → 1.8.3
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/bin/index.ts +16 -0
- package/dist/index.cjs +713 -0
- package/dist/index.d.cts +210 -2
- package/dist/index.d.mts +210 -2
- package/dist/index.d.ts +210 -2
- package/dist/index.mjs +708 -1
- package/package.json +1 -1
- package/src/index.ts +1 -0
- package/src/openAPITools/StreamController.ts +372 -0
- package/src/openAPITools/functionGenerator.ts +239 -1
- package/src/openAPITools/index.ts +2 -0
- package/src/openAPITools/streamClient.ts +247 -0
- package/src/openAPITools/streamDetector.ts +207 -0
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
/* eslint-disable ts/no-unnecessary-condition */
|
|
2
|
+
/* eslint-disable no-await-in-loop */
|
|
3
|
+
/* eslint-disable ts/strict-boolean-expressions */
|
|
4
|
+
/* eslint-disable ts/use-unknown-in-catch-callback-variable */
|
|
5
|
+
/**
|
|
6
|
+
* SSE (Server-Sent Events) Client Utilities
|
|
7
|
+
* Provides utilities for consuming Server-Sent Events streams
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { StreamEventMap } from './StreamController'
|
|
11
|
+
import { StreamController } from './StreamController'
|
|
12
|
+
|
|
13
|
+
export { type SSEEvent, StreamController, type StreamEventMap } from './StreamController'
|
|
14
|
+
|
|
15
|
+
export interface SSEStreamOptions {
|
|
16
|
+
/** Additional headers to send with the request */
|
|
17
|
+
headers?: Record<string, string>
|
|
18
|
+
/** Whether to include credentials (cookies) with the request */
|
|
19
|
+
withCredentials?: boolean
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Creates an SSE stream consumer with elegant event-based API
|
|
24
|
+
* @param url - The SSE endpoint URL
|
|
25
|
+
* @param options - Stream options
|
|
26
|
+
* @returns StreamController for chainable event handling
|
|
27
|
+
*
|
|
28
|
+
* @example
|
|
29
|
+
* ```typescript
|
|
30
|
+
* const stream = createSSEStream(url)
|
|
31
|
+
* .on('token', data => console.log(data))
|
|
32
|
+
* .on('done', () => console.log('Complete!'))
|
|
33
|
+
* .on('error', err => console.error(err))
|
|
34
|
+
*
|
|
35
|
+
* // Close when needed
|
|
36
|
+
* stream.close()
|
|
37
|
+
* ```
|
|
38
|
+
*/
|
|
39
|
+
export function createSSEStream<TEventMap extends StreamEventMap = StreamEventMap>(
|
|
40
|
+
url: string,
|
|
41
|
+
options: SSEStreamOptions = {}
|
|
42
|
+
): StreamController<TEventMap> {
|
|
43
|
+
const { headers = {}, withCredentials = true } = options
|
|
44
|
+
|
|
45
|
+
return new StreamController((onEvent, onError, onComplete) => {
|
|
46
|
+
const controller = new AbortController()
|
|
47
|
+
const { signal } = controller
|
|
48
|
+
|
|
49
|
+
// Start the fetch request
|
|
50
|
+
fetch(url, {
|
|
51
|
+
method: 'GET',
|
|
52
|
+
headers: {
|
|
53
|
+
Accept: 'text/event-stream',
|
|
54
|
+
...headers,
|
|
55
|
+
},
|
|
56
|
+
credentials: withCredentials ? 'include' : 'same-origin',
|
|
57
|
+
signal,
|
|
58
|
+
})
|
|
59
|
+
.then(async (response) => {
|
|
60
|
+
if (!response.ok) {
|
|
61
|
+
throw new Error(`HTTP ${response.status}: ${response.statusText}`)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (!response.body) {
|
|
65
|
+
throw new Error('Response body is null')
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const reader = response.body.getReader()
|
|
69
|
+
const decoder = new TextDecoder()
|
|
70
|
+
let buffer = ''
|
|
71
|
+
|
|
72
|
+
try {
|
|
73
|
+
while (true) {
|
|
74
|
+
const { done, value } = await reader.read()
|
|
75
|
+
|
|
76
|
+
if (done) {
|
|
77
|
+
onComplete()
|
|
78
|
+
break
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Decode the chunk and add to buffer
|
|
82
|
+
buffer += decoder.decode(value, { stream: true })
|
|
83
|
+
|
|
84
|
+
// Process complete events in the buffer
|
|
85
|
+
const lines = buffer.split('\n')
|
|
86
|
+
buffer = lines.pop() || '' // Keep incomplete line in buffer
|
|
87
|
+
|
|
88
|
+
let eventType = ''
|
|
89
|
+
let eventData = ''
|
|
90
|
+
|
|
91
|
+
for (const line of lines) {
|
|
92
|
+
if (line.startsWith('event:')) {
|
|
93
|
+
eventType = line.slice(6).trim()
|
|
94
|
+
} else if (line.startsWith('data:')) {
|
|
95
|
+
eventData = line.slice(5).trim()
|
|
96
|
+
} else if (line === '' && eventData) {
|
|
97
|
+
// Empty line indicates end of event
|
|
98
|
+
try {
|
|
99
|
+
const parsedData = JSON.parse(eventData)
|
|
100
|
+
onEvent({
|
|
101
|
+
type: eventType || 'message',
|
|
102
|
+
data: parsedData,
|
|
103
|
+
raw: eventData,
|
|
104
|
+
})
|
|
105
|
+
} catch {
|
|
106
|
+
// If not JSON, pass as string
|
|
107
|
+
onEvent({
|
|
108
|
+
type: eventType || 'message',
|
|
109
|
+
data: eventData,
|
|
110
|
+
raw: eventData,
|
|
111
|
+
})
|
|
112
|
+
}
|
|
113
|
+
eventType = ''
|
|
114
|
+
eventData = ''
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
} catch (error) {
|
|
119
|
+
if (error instanceof Error && error.name !== 'AbortError') {
|
|
120
|
+
onError(error)
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
})
|
|
124
|
+
.catch((error) => {
|
|
125
|
+
if (error.name !== 'AbortError') {
|
|
126
|
+
onError(error)
|
|
127
|
+
}
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
// Return cleanup function
|
|
131
|
+
return () => { controller.abort() }
|
|
132
|
+
})
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Creates an SSE stream consumer for POST requests with elegant event-based API
|
|
137
|
+
* @param url - The SSE endpoint URL
|
|
138
|
+
* @param body - Request body
|
|
139
|
+
* @param options - Stream options
|
|
140
|
+
* @returns StreamController for chainable event handling
|
|
141
|
+
*
|
|
142
|
+
* @example
|
|
143
|
+
* ```typescript
|
|
144
|
+
* const stream = createSSEStreamPost(url, { message: 'Hello' })
|
|
145
|
+
* .on('token', data => console.log(data))
|
|
146
|
+
* .on('done', () => console.log('Complete!'))
|
|
147
|
+
* .on('error', err => console.error(err))
|
|
148
|
+
* ```
|
|
149
|
+
*/
|
|
150
|
+
export function createSSEStreamPost<TEventMap extends StreamEventMap = StreamEventMap, TBody = any>(
|
|
151
|
+
url: string,
|
|
152
|
+
body: TBody,
|
|
153
|
+
options: SSEStreamOptions = {}
|
|
154
|
+
): StreamController<TEventMap> {
|
|
155
|
+
const { headers = {}, withCredentials = true } = options
|
|
156
|
+
|
|
157
|
+
return new StreamController((onEvent, onError, onComplete) => {
|
|
158
|
+
const controller = new AbortController()
|
|
159
|
+
const { signal } = controller
|
|
160
|
+
|
|
161
|
+
// Start the fetch request
|
|
162
|
+
fetch(url, {
|
|
163
|
+
method: 'POST',
|
|
164
|
+
headers: {
|
|
165
|
+
'Accept': 'text/event-stream',
|
|
166
|
+
'Content-Type': 'application/json',
|
|
167
|
+
...headers,
|
|
168
|
+
},
|
|
169
|
+
credentials: withCredentials ? 'include' : 'same-origin',
|
|
170
|
+
body: JSON.stringify(body),
|
|
171
|
+
signal,
|
|
172
|
+
})
|
|
173
|
+
.then(async (response) => {
|
|
174
|
+
if (!response.ok) {
|
|
175
|
+
throw new Error(`HTTP ${response.status}: ${response.statusText}`)
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
if (!response.body) {
|
|
179
|
+
throw new Error('Response body is null')
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const reader = response.body.getReader()
|
|
183
|
+
const decoder = new TextDecoder()
|
|
184
|
+
let buffer = ''
|
|
185
|
+
|
|
186
|
+
try {
|
|
187
|
+
while (true) {
|
|
188
|
+
const { done, value } = await reader.read()
|
|
189
|
+
|
|
190
|
+
if (done) {
|
|
191
|
+
onComplete()
|
|
192
|
+
break
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Decode the chunk and add to buffer
|
|
196
|
+
buffer += decoder.decode(value, { stream: true })
|
|
197
|
+
|
|
198
|
+
// Process complete events in the buffer
|
|
199
|
+
const lines = buffer.split('\n')
|
|
200
|
+
buffer = lines.pop() || '' // Keep incomplete line in buffer
|
|
201
|
+
|
|
202
|
+
let eventType = ''
|
|
203
|
+
let eventData = ''
|
|
204
|
+
|
|
205
|
+
for (const line of lines) {
|
|
206
|
+
if (line.startsWith('event:')) {
|
|
207
|
+
eventType = line.slice(6).trim()
|
|
208
|
+
} else if (line.startsWith('data:')) {
|
|
209
|
+
eventData = line.slice(5).trim()
|
|
210
|
+
} else if (line === '' && eventData) {
|
|
211
|
+
// Empty line indicates end of event
|
|
212
|
+
try {
|
|
213
|
+
const parsedData = JSON.parse(eventData)
|
|
214
|
+
onEvent({
|
|
215
|
+
type: eventType || 'message',
|
|
216
|
+
data: parsedData,
|
|
217
|
+
raw: eventData,
|
|
218
|
+
})
|
|
219
|
+
} catch {
|
|
220
|
+
// If not JSON, pass as string
|
|
221
|
+
onEvent({
|
|
222
|
+
type: eventType || 'message',
|
|
223
|
+
data: eventData,
|
|
224
|
+
raw: eventData,
|
|
225
|
+
})
|
|
226
|
+
}
|
|
227
|
+
eventType = ''
|
|
228
|
+
eventData = ''
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
} catch (error) {
|
|
233
|
+
if (error instanceof Error && error.name !== 'AbortError') {
|
|
234
|
+
onError(error)
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
})
|
|
238
|
+
.catch((error) => {
|
|
239
|
+
if (error.name !== 'AbortError') {
|
|
240
|
+
onError(error)
|
|
241
|
+
}
|
|
242
|
+
})
|
|
243
|
+
|
|
244
|
+
// Return cleanup function
|
|
245
|
+
return () => { controller.abort() }
|
|
246
|
+
})
|
|
247
|
+
}
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
import type { OperationObject as OpenAPIOperation } from './types'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Detects if an operation is an SSE (Server-Sent Events) stream endpoint
|
|
5
|
+
*
|
|
6
|
+
* Primary detection: Checks for text/event-stream content type in responses
|
|
7
|
+
* Fallback: Only if no content type is specified, check for explicit SSE keywords
|
|
8
|
+
*
|
|
9
|
+
* @param operation - The OpenAPI operation object
|
|
10
|
+
* @returns Whether the operation is an SSE stream
|
|
11
|
+
*/
|
|
12
|
+
export function isSSEStream(operation: OpenAPIOperation): boolean {
|
|
13
|
+
const responses = operation.responses || {}
|
|
14
|
+
|
|
15
|
+
// Check all response codes (200, 201, etc.)
|
|
16
|
+
for (const [statusCode, response] of Object.entries(responses)) {
|
|
17
|
+
// Only check successful responses (2xx)
|
|
18
|
+
if (!statusCode.startsWith('2')) continue
|
|
19
|
+
|
|
20
|
+
// Skip reference objects for now
|
|
21
|
+
if ('$ref' in response) continue
|
|
22
|
+
|
|
23
|
+
const content = (response as any).content || {}
|
|
24
|
+
const contentTypes = Object.keys(content)
|
|
25
|
+
|
|
26
|
+
// If response has content types defined
|
|
27
|
+
if (contentTypes.length > 0) {
|
|
28
|
+
// It's a stream if text/event-stream is present
|
|
29
|
+
// (even if application/json is also present as a fallback)
|
|
30
|
+
if ('text/event-stream' in content) {
|
|
31
|
+
return true
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// If we checked responses and found only application/json (no text/event-stream), it's not a stream
|
|
37
|
+
const hasDefinedContentTypes = Object.entries(responses).some(([statusCode, response]) => {
|
|
38
|
+
if (!statusCode.startsWith('2')) return false
|
|
39
|
+
if ('$ref' in response) return false
|
|
40
|
+
const content = (response as any).content || {}
|
|
41
|
+
return Object.keys(content).length > 0
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
if (hasDefinedContentTypes) {
|
|
45
|
+
return false
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Fallback: If no content types are defined at all, check description
|
|
49
|
+
// (Some OpenAPI specs might not properly define response content types)
|
|
50
|
+
const description = operation.description?.toLowerCase() || ''
|
|
51
|
+
const summary = operation.summary?.toLowerCase() || ''
|
|
52
|
+
|
|
53
|
+
const hasExplicitSSEMention
|
|
54
|
+
= /\bsse\b/.test(description)
|
|
55
|
+
|| /server-sent events/i.test(description)
|
|
56
|
+
|| /text\/event-stream/i.test(description)
|
|
57
|
+
|| /\bsse\b/.test(summary)
|
|
58
|
+
|
|
59
|
+
return hasExplicitSSEMention
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Information about an SSE event type including its fields
|
|
64
|
+
*/
|
|
65
|
+
export interface SSEEventTypeInfo {
|
|
66
|
+
/** Event type name */
|
|
67
|
+
name: string
|
|
68
|
+
/** Event description */
|
|
69
|
+
description?: string
|
|
70
|
+
/** Fields in the event data */
|
|
71
|
+
fields?: Array<{
|
|
72
|
+
name: string
|
|
73
|
+
description?: string
|
|
74
|
+
type?: string
|
|
75
|
+
}>
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Extracts SSE event types from operation description and examples
|
|
80
|
+
*
|
|
81
|
+
* Supports multiple documentation formats:
|
|
82
|
+
* 1. Markdown format: "1. **event_name**: Description"
|
|
83
|
+
* 2. Legacy format: type: "event_name"
|
|
84
|
+
* 3. SSE examples: event: event_name
|
|
85
|
+
*
|
|
86
|
+
* @param operation - The OpenAPI operation object
|
|
87
|
+
* @returns Array of event types or undefined
|
|
88
|
+
*/
|
|
89
|
+
export function extractSSEEventTypes(operation: OpenAPIOperation): string[] | undefined {
|
|
90
|
+
const description = operation.description || ''
|
|
91
|
+
const types = new Set<string>()
|
|
92
|
+
|
|
93
|
+
// Method 1: Extract from markdown numbered lists with bold event names
|
|
94
|
+
// Pattern: "1. **event_name**: Description"
|
|
95
|
+
const markdownMatches = description.matchAll(/^\d+\.\s+\*\*([a-z_]+)\*\*/gm)
|
|
96
|
+
for (const match of markdownMatches) {
|
|
97
|
+
types.add(match[1])
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Method 2: Legacy format - type: "event_name"
|
|
101
|
+
const typeMatches = description.matchAll(/type:\s*"([^"]+)"/g)
|
|
102
|
+
for (const match of typeMatches) {
|
|
103
|
+
types.add(match[1])
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Method 3: Extract from SSE examples in response content
|
|
107
|
+
const responses = operation.responses || {}
|
|
108
|
+
for (const response of Object.values(responses)) {
|
|
109
|
+
if ('content' in response) {
|
|
110
|
+
const content = (response as any).content || {}
|
|
111
|
+
const eventStream = content['text/event-stream']
|
|
112
|
+
if (eventStream?.example) {
|
|
113
|
+
// Parse SSE format: event: event_name
|
|
114
|
+
const exampleMatches = eventStream.example.matchAll(/^event:\s*([a-z_]+)/gm)
|
|
115
|
+
for (const match of exampleMatches) {
|
|
116
|
+
types.add(match[1])
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return types.size > 0 ? Array.from(types).sort() : undefined
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Extracts detailed SSE event information including field definitions
|
|
127
|
+
*
|
|
128
|
+
* Parses markdown documentation format:
|
|
129
|
+
* ```
|
|
130
|
+
* 1. **event_name**: Event description
|
|
131
|
+
* - `field_name`: Field description
|
|
132
|
+
* - `another_field`: Another description
|
|
133
|
+
* ```
|
|
134
|
+
*
|
|
135
|
+
* @param operation - The OpenAPI operation object
|
|
136
|
+
* @returns Array of event type information with fields
|
|
137
|
+
*/
|
|
138
|
+
export function extractSSEEventInfo(operation: OpenAPIOperation): SSEEventTypeInfo[] | undefined {
|
|
139
|
+
const description = operation.description || ''
|
|
140
|
+
const eventInfos: SSEEventTypeInfo[] = []
|
|
141
|
+
|
|
142
|
+
// Split description into sections by numbered events
|
|
143
|
+
const eventSections = description.split(/(?=^\d+\.\s+\*\*)/m)
|
|
144
|
+
|
|
145
|
+
for (const section of eventSections) {
|
|
146
|
+
// Extract event name and description
|
|
147
|
+
const eventMatch = section.match(/^\d+\.\s+\*\*([a-z_]+)\*\*:\s*([^\n]+)/m)
|
|
148
|
+
if (!eventMatch) continue
|
|
149
|
+
|
|
150
|
+
const [, eventName, eventDescription] = eventMatch
|
|
151
|
+
|
|
152
|
+
// Extract fields from the section
|
|
153
|
+
const fields: Array<{ name: string, description?: string, type?: string }> = []
|
|
154
|
+
const fieldMatches = section.matchAll(/^\s+[-*]\s+`([^`]+)`:\s*([^\n]+)/gm)
|
|
155
|
+
|
|
156
|
+
for (const fieldMatch of fieldMatches) {
|
|
157
|
+
const [, fieldName, fieldDescription] = fieldMatch
|
|
158
|
+
fields.push({
|
|
159
|
+
name: fieldName,
|
|
160
|
+
description: fieldDescription.trim(),
|
|
161
|
+
})
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
eventInfos.push({
|
|
165
|
+
name: eventName,
|
|
166
|
+
description: eventDescription,
|
|
167
|
+
fields: fields.length > 0 ? fields : undefined,
|
|
168
|
+
})
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// If no markdown events found, try to extract from examples
|
|
172
|
+
if (eventInfos.length === 0) {
|
|
173
|
+
const responses = operation.responses || {}
|
|
174
|
+
for (const response of Object.values(responses)) {
|
|
175
|
+
if ('content' in response) {
|
|
176
|
+
const content = (response as any).content || {}
|
|
177
|
+
const eventStream = content['text/event-stream']
|
|
178
|
+
if (eventStream?.example) {
|
|
179
|
+
// Parse example JSON to extract field names
|
|
180
|
+
const exampleMatches = eventStream.example.matchAll(/data:\s*(\{[^}]+\})/g)
|
|
181
|
+
const seenEvents = new Set<string>()
|
|
182
|
+
|
|
183
|
+
for (const match of exampleMatches) {
|
|
184
|
+
try {
|
|
185
|
+
const data = JSON.parse(match[1])
|
|
186
|
+
if (data.type && !seenEvents.has(data.type)) {
|
|
187
|
+
seenEvents.add(data.type)
|
|
188
|
+
const fields = Object.keys(data)
|
|
189
|
+
.filter(key => key !== 'type' && key !== 'sequence')
|
|
190
|
+
.map(key => ({ name: key }))
|
|
191
|
+
|
|
192
|
+
eventInfos.push({
|
|
193
|
+
name: data.type,
|
|
194
|
+
fields: fields.length > 0 ? fields : undefined,
|
|
195
|
+
})
|
|
196
|
+
}
|
|
197
|
+
} catch {
|
|
198
|
+
// Skip invalid JSON
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
return eventInfos.length > 0 ? eventInfos : undefined
|
|
207
|
+
}
|