@frontera-sdk/core 1.50.36 → 1.50.38
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/client.ts +24 -1
- package/src/transport.ts +74 -0
package/package.json
CHANGED
package/src/client.ts
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
import { resolveConfig, type FronteraConfig, type FronteraCredential } from './config'
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
httpRequest,
|
|
4
|
+
httpRequestEnvelope,
|
|
5
|
+
httpRequestRaw,
|
|
6
|
+
type RequestOptions,
|
|
7
|
+
} from './transport'
|
|
3
8
|
|
|
4
9
|
/**
|
|
5
10
|
* The transport-level entry point. Domain clients (`@frontera-sdk/blueprint`, and
|
|
@@ -23,6 +28,24 @@ export class FronteraClient {
|
|
|
23
28
|
return httpRequest<T>(this.config, path, options, this.fetchImpl)
|
|
24
29
|
}
|
|
25
30
|
|
|
31
|
+
/**
|
|
32
|
+
* Same auth and error mapping as `request`, but the 2xx `Response` comes back
|
|
33
|
+
* with its body unread — for SSE and other non-JSON payloads. Domain clients
|
|
34
|
+
* stream through this rather than rebuilding the Authorization header.
|
|
35
|
+
*/
|
|
36
|
+
requestRaw(path: string, options: RequestOptions = {}): Promise<Response> {
|
|
37
|
+
return httpRequestRaw(this.config, path, options, this.fetchImpl)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Same as `request` but skips the envelope unwrap, so paginated routes keep
|
|
42
|
+
* `nextCursor` (which sits BESIDE `data` in the envelope and is otherwise
|
|
43
|
+
* dropped).
|
|
44
|
+
*/
|
|
45
|
+
requestEnvelope<T>(path: string, options: RequestOptions = {}): Promise<T> {
|
|
46
|
+
return httpRequestEnvelope<T>(this.config, path, options, this.fetchImpl)
|
|
47
|
+
}
|
|
48
|
+
|
|
26
49
|
withCredential(credential: FronteraCredential): FronteraClient {
|
|
27
50
|
return new FronteraClient({ ...this.config, credential }, this.fetchImpl)
|
|
28
51
|
}
|
package/src/transport.ts
CHANGED
|
@@ -77,6 +77,80 @@ function buildUrl(config: FronteraConfig, path: string, query?: RequestOptions['
|
|
|
77
77
|
return url.toString()
|
|
78
78
|
}
|
|
79
79
|
|
|
80
|
+
/**
|
|
81
|
+
* Like `httpRequest`, but the 2xx `Response` is returned with its body
|
|
82
|
+
* untouched. This is what streaming endpoints need: an SSE body can only be
|
|
83
|
+
* read once, so the transport must not consume it. Non-2xx responses are still
|
|
84
|
+
* read and mapped to `FronteraError` — an error body is JSON, never a stream.
|
|
85
|
+
*/
|
|
86
|
+
export async function httpRequestRaw(
|
|
87
|
+
config: FronteraConfig,
|
|
88
|
+
path: string,
|
|
89
|
+
options: RequestOptions = {},
|
|
90
|
+
fetchImpl: typeof fetch = fetch,
|
|
91
|
+
): Promise<Response> {
|
|
92
|
+
const headers: Record<string, string> = {
|
|
93
|
+
...lowerCasedKeys(options.headers),
|
|
94
|
+
...authHeaders(config),
|
|
95
|
+
}
|
|
96
|
+
// FormData carries its own multipart content type WITH a boundary, which
|
|
97
|
+
// only it can generate — setting one here, or stringifying it, produces a
|
|
98
|
+
// body the server cannot parse.
|
|
99
|
+
const isFormData = typeof FormData !== 'undefined' && options.body instanceof FormData
|
|
100
|
+
if (options.body !== undefined && !isFormData) headers['content-type'] = 'application/json'
|
|
101
|
+
|
|
102
|
+
const response = await fetchImpl(buildUrl(config, path, options.query), {
|
|
103
|
+
method: options.method ?? 'GET',
|
|
104
|
+
headers,
|
|
105
|
+
body:
|
|
106
|
+
options.body === undefined
|
|
107
|
+
? undefined
|
|
108
|
+
: isFormData
|
|
109
|
+
? // `FormData`, not `BodyInit`: the guard above narrowed it to exactly
|
|
110
|
+
// that, and `BodyInit` is a DOM-lib global the CLI's tsconfig does
|
|
111
|
+
// not load (`lib: ["ESNext"]`, `types: ["bun"]`) — naming it here
|
|
112
|
+
// failed that package's typecheck on a file it only borrows.
|
|
113
|
+
(options.body as FormData)
|
|
114
|
+
: JSON.stringify(options.body),
|
|
115
|
+
signal: options.signal,
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
if (!response.ok) {
|
|
119
|
+
const text = await response.text()
|
|
120
|
+
let parsed: unknown = null
|
|
121
|
+
if (text.length > 0) {
|
|
122
|
+
try {
|
|
123
|
+
parsed = JSON.parse(text)
|
|
124
|
+
} catch {
|
|
125
|
+
parsed = text
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
throw errorFromResponse(response.status, parsed)
|
|
129
|
+
}
|
|
130
|
+
return response
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Like `httpRequest`, but WITHOUT the envelope unwrap. Paginated routes put
|
|
135
|
+
* `nextCursor` beside `data` in the envelope, so unwrapping to `data` silently
|
|
136
|
+
* drops the cursor — callers that page need the whole body.
|
|
137
|
+
*/
|
|
138
|
+
export async function httpRequestEnvelope<T>(
|
|
139
|
+
config: FronteraConfig,
|
|
140
|
+
path: string,
|
|
141
|
+
options: RequestOptions = {},
|
|
142
|
+
fetchImpl: typeof fetch = fetch,
|
|
143
|
+
): Promise<T> {
|
|
144
|
+
const response = await httpRequestRaw(config, path, options, fetchImpl)
|
|
145
|
+
const text = await response.text()
|
|
146
|
+
if (text.length === 0) return null as T
|
|
147
|
+
try {
|
|
148
|
+
return JSON.parse(text) as T
|
|
149
|
+
} catch {
|
|
150
|
+
return text as T
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
80
154
|
/**
|
|
81
155
|
* `fetchImpl` is injectable so tests never touch the network. Production
|
|
82
156
|
* callers omit it and get the platform `fetch`.
|