@distilled.cloud/core 0.30.3 → 1.0.0-rc.1
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/lib/api.d.ts +165 -0
- package/lib/api.d.ts.map +1 -0
- package/lib/api.js +178 -0
- package/lib/api.js.map +1 -0
- package/lib/codegen/cli.d.ts +29 -0
- package/lib/codegen/cli.d.ts.map +1 -0
- package/lib/codegen/cli.js +165 -0
- package/lib/codegen/cli.js.map +1 -0
- package/lib/codegen/emit.d.ts +129 -0
- package/lib/codegen/emit.d.ts.map +1 -0
- package/lib/codegen/emit.js +105 -0
- package/lib/codegen/emit.js.map +1 -0
- package/lib/codegen/format.d.ts +23 -0
- package/lib/codegen/format.d.ts.map +1 -0
- package/lib/codegen/format.js +28 -0
- package/lib/codegen/format.js.map +1 -0
- package/lib/codegen/generator.d.ts +334 -0
- package/lib/codegen/generator.d.ts.map +1 -0
- package/lib/codegen/generator.js +691 -0
- package/lib/codegen/generator.js.map +1 -0
- package/lib/codegen/graph.d.ts +36 -0
- package/lib/codegen/graph.d.ts.map +1 -0
- package/lib/codegen/graph.js +136 -0
- package/lib/codegen/graph.js.map +1 -0
- package/lib/codegen/members.d.ts +25 -0
- package/lib/codegen/members.d.ts.map +1 -0
- package/lib/codegen/members.js +55 -0
- package/lib/codegen/members.js.map +1 -0
- package/lib/codegen/naming.d.ts +29 -0
- package/lib/codegen/naming.d.ts.map +1 -0
- package/lib/codegen/naming.js +74 -0
- package/lib/codegen/naming.js.map +1 -0
- package/lib/codegen/openapi-cli.d.ts +38 -0
- package/lib/codegen/openapi-cli.d.ts.map +1 -0
- package/lib/codegen/openapi-cli.js +107 -0
- package/lib/codegen/openapi-cli.js.map +1 -0
- package/lib/codegen/openapi.d.ts +115 -0
- package/lib/codegen/openapi.d.ts.map +1 -0
- package/lib/codegen/openapi.js +1220 -0
- package/lib/codegen/openapi.js.map +1 -0
- package/lib/codegen/operations.d.ts +24 -0
- package/lib/codegen/operations.d.ts.map +1 -0
- package/lib/codegen/operations.js +56 -0
- package/lib/codegen/operations.js.map +1 -0
- package/lib/codegen/pagination.d.ts +39 -0
- package/lib/codegen/pagination.d.ts.map +1 -0
- package/lib/codegen/pagination.js +33 -0
- package/lib/codegen/pagination.js.map +1 -0
- package/lib/codegen/prelude.d.ts +15 -0
- package/lib/codegen/prelude.d.ts.map +1 -0
- package/lib/codegen/prelude.js +60 -0
- package/lib/codegen/prelude.js.map +1 -0
- package/lib/error-category.d.ts +28 -0
- package/lib/error-category.d.ts.map +1 -0
- package/lib/error-category.js +46 -0
- package/lib/error-category.js.map +1 -0
- package/lib/errors.d.ts +1 -0
- package/lib/errors.d.ts.map +1 -1
- package/lib/errors.js +1 -0
- package/lib/errors.js.map +1 -1
- package/lib/json-patch.d.ts +25 -32
- package/lib/json-patch.d.ts.map +1 -1
- package/lib/json-patch.js +23 -95
- package/lib/json-patch.js.map +1 -1
- package/lib/pagination.d.ts +37 -51
- package/lib/pagination.d.ts.map +1 -1
- package/lib/pagination.js +72 -90
- package/lib/pagination.js.map +1 -1
- package/lib/protocol-http.d.ts +74 -0
- package/lib/protocol-http.d.ts.map +1 -0
- package/lib/protocol-http.js +554 -0
- package/lib/protocol-http.js.map +1 -0
- package/lib/protocol-rest.d.ts +124 -0
- package/lib/protocol-rest.d.ts.map +1 -0
- package/lib/protocol-rest.js +242 -0
- package/lib/protocol-rest.js.map +1 -0
- package/lib/retry.d.ts +8 -2
- package/lib/retry.d.ts.map +1 -1
- package/lib/retry.js +21 -15
- package/lib/retry.js.map +1 -1
- package/lib/schema.d.ts +7 -8
- package/lib/schema.d.ts.map +1 -1
- package/lib/schema.js +7 -8
- package/lib/schema.js.map +1 -1
- package/lib/trait.d.ts +150 -0
- package/lib/trait.d.ts.map +1 -0
- package/lib/trait.js +107 -0
- package/lib/trait.js.map +1 -0
- package/package.json +18 -75
- package/src/api.ts +446 -0
- package/src/codegen/cli.ts +268 -0
- package/src/codegen/emit.ts +207 -0
- package/src/codegen/format.ts +47 -0
- package/src/codegen/generator.ts +1153 -0
- package/src/codegen/graph.ts +151 -0
- package/src/codegen/members.ts +71 -0
- package/src/codegen/naming.ts +86 -0
- package/src/codegen/openapi-cli.ts +166 -0
- package/src/codegen/openapi.ts +1450 -0
- package/src/codegen/operations.ts +76 -0
- package/src/codegen/pagination.ts +71 -0
- package/src/codegen/prelude.ts +70 -0
- package/src/error-category.ts +84 -0
- package/src/errors.ts +2 -0
- package/src/json-patch.ts +26 -110
- package/src/pagination.ts +86 -142
- package/src/protocol-http.ts +699 -0
- package/src/protocol-rest.ts +367 -0
- package/src/retry.ts +20 -21
- package/src/schema.ts +7 -8
- package/src/trait.ts +238 -0
- package/README.md +0 -30
- package/lib/client.d.ts +0 -167
- package/lib/client.d.ts.map +0 -1
- package/lib/client.js +0 -659
- package/lib/client.js.map +0 -1
- package/lib/schemas.d.ts +0 -60
- package/lib/schemas.d.ts.map +0 -1
- package/lib/schemas.js +0 -79
- package/lib/schemas.js.map +0 -1
- package/lib/sensitive.d.ts +0 -71
- package/lib/sensitive.d.ts.map +0 -1
- package/lib/sensitive.js +0 -96
- package/lib/sensitive.js.map +0 -1
- package/lib/traits.d.ts +0 -421
- package/lib/traits.d.ts.map +0 -1
- package/lib/traits.js +0 -737
- package/lib/traits.js.map +0 -1
- package/src/client.ts +0 -1177
- package/src/schemas.ts +0 -128
- package/src/sensitive.ts +0 -119
- package/src/traits.ts +0 -996
package/lib/client.js
DELETED
|
@@ -1,659 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* REST API Client
|
|
3
|
-
*
|
|
4
|
-
* Provides the core API.make() factory for building typed Effect-based API operations.
|
|
5
|
-
* This is the shared client for REST/OpenAPI-style SDKs (PlanetScale, Neon, GCP).
|
|
6
|
-
*
|
|
7
|
-
* AWS and Cloudflare have their own more specialized client implementations,
|
|
8
|
-
* but they share the same OperationMethod pattern.
|
|
9
|
-
*
|
|
10
|
-
* @example
|
|
11
|
-
* ```ts
|
|
12
|
-
* import { API } from "@distilled.cloud/core/client";
|
|
13
|
-
*
|
|
14
|
-
* const listDatabases = API.make(() => ({
|
|
15
|
-
* inputSchema: ListDatabasesInput,
|
|
16
|
-
* outputSchema: ListDatabasesOutput,
|
|
17
|
-
* errors: [NotFound, Forbidden] as const,
|
|
18
|
-
* }));
|
|
19
|
-
*
|
|
20
|
-
* // Direct call
|
|
21
|
-
* const result = yield* listDatabases({ organization: "my-org" });
|
|
22
|
-
*
|
|
23
|
-
* // Yield first for requirement-free function
|
|
24
|
-
* const fn = yield* listDatabases;
|
|
25
|
-
* const result = yield* fn({ organization: "my-org" });
|
|
26
|
-
* ```
|
|
27
|
-
*/
|
|
28
|
-
import * as Config from "effect/Config";
|
|
29
|
-
import * as Context from "effect/Context";
|
|
30
|
-
import * as Effect from "effect/Effect";
|
|
31
|
-
import { pipe } from "effect/Function";
|
|
32
|
-
import * as Option from "effect/Option";
|
|
33
|
-
import { pipeArguments } from "effect/Pipeable";
|
|
34
|
-
import * as Ref from "effect/Ref";
|
|
35
|
-
import { MinimumLogLevel } from "effect/References";
|
|
36
|
-
import * as Schema from "effect/Schema";
|
|
37
|
-
import * as AST from "effect/SchemaAST";
|
|
38
|
-
import * as Stream from "effect/Stream";
|
|
39
|
-
import * as FetchHttpClient from "effect/unstable/http/FetchHttpClient";
|
|
40
|
-
import * as HttpBody from "effect/unstable/http/HttpBody";
|
|
41
|
-
import * as HttpClient from "effect/unstable/http/HttpClient";
|
|
42
|
-
import * as HttpClientError from "effect/unstable/http/HttpClientError";
|
|
43
|
-
import * as HttpClientRequest from "effect/unstable/http/HttpClientRequest";
|
|
44
|
-
import { SingleShotGen } from "effect/Utils";
|
|
45
|
-
import { extractItems, paginateWithDefaults, } from "./pagination.js";
|
|
46
|
-
import { makeDefault } from "./retry.js";
|
|
47
|
-
import * as Traits from "./traits.js";
|
|
48
|
-
import { getPath } from "./traits.js";
|
|
49
|
-
// `DISTILLED_DEBUG=1` forces the MinimumLogLevel to Debug for SDK operations,
|
|
50
|
-
// independent of the caller's logger config. Users who set `MinimumLogLevel`
|
|
51
|
-
// to Debug themselves get the same logs without needing the env var.
|
|
52
|
-
const distilledDebugConfig = Config.string("DISTILLED_DEBUG")
|
|
53
|
-
.pipe(Config.map((raw) => raw === "1"))
|
|
54
|
-
.pipe(Effect.orElseSucceed(() => false));
|
|
55
|
-
const isEffectLike = (value) => typeof value === "object" &&
|
|
56
|
-
value !== null &&
|
|
57
|
-
typeof value.pipe === "function" &&
|
|
58
|
-
typeof value[Symbol.iterator] ===
|
|
59
|
-
"function";
|
|
60
|
-
// ============================================================================
|
|
61
|
-
// AST Helpers
|
|
62
|
-
// ============================================================================
|
|
63
|
-
/**
|
|
64
|
-
* Check if a schema AST represents an array type.
|
|
65
|
-
* Follows encoding chains and Suspend wrappers.
|
|
66
|
-
*/
|
|
67
|
-
function isArrayAST(ast) {
|
|
68
|
-
if (ast._tag === "Arrays")
|
|
69
|
-
return true;
|
|
70
|
-
if (ast._tag === "Suspend")
|
|
71
|
-
return isArrayAST(ast.thunk());
|
|
72
|
-
if (ast.encoding && ast.encoding.length > 0)
|
|
73
|
-
return isArrayAST(ast.encoding[0].to);
|
|
74
|
-
return false;
|
|
75
|
-
}
|
|
76
|
-
/**
|
|
77
|
-
* Resolve a (possibly `Schema.suspend`-wrapped) AST down to the concrete
|
|
78
|
-
* underlying node by forcing the memoized thunk. Generated SDK schemas may
|
|
79
|
-
* wrap each request/response struct in `Schema.suspend(() => ...)` so the
|
|
80
|
-
* (expensive) schema construction is deferred from module-load time to the
|
|
81
|
-
* first time the operation is actually called. Trait extraction needs the
|
|
82
|
-
* real node, so we force it here. `Suspend.thunk` memoizes, so this only
|
|
83
|
-
* pays the construction cost once per operation. Returns the input AST
|
|
84
|
-
* untouched when it isn't a Suspend (the common case for non-suspended SDKs).
|
|
85
|
-
*/
|
|
86
|
-
function resolveAst(ast) {
|
|
87
|
-
return ast._tag === "Suspend" ? resolveAst(ast.thunk()) : ast;
|
|
88
|
-
}
|
|
89
|
-
// ============================================================================
|
|
90
|
-
// Form URL-Encoded Builder (Stripe deepObject style)
|
|
91
|
-
// ============================================================================
|
|
92
|
-
/**
|
|
93
|
-
* Recursively flatten a nested object into Stripe-style bracket notation
|
|
94
|
-
* for application/x-www-form-urlencoded encoding.
|
|
95
|
-
*
|
|
96
|
-
* Examples:
|
|
97
|
-
* { amount: 2000 } -> "amount=2000"
|
|
98
|
-
* { shipping: { address: { city: "SF" } } } -> "shipping[address][city]=SF"
|
|
99
|
-
* { expand: ["data"] } -> "expand[0]=data"
|
|
100
|
-
* { metadata: { key: "val" } } -> "metadata[key]=val"
|
|
101
|
-
*/
|
|
102
|
-
function flattenToFormPairs(obj, prefix = "") {
|
|
103
|
-
const pairs = [];
|
|
104
|
-
for (const [key, value] of Object.entries(obj)) {
|
|
105
|
-
if (value === undefined || value === null)
|
|
106
|
-
continue;
|
|
107
|
-
const fullKey = prefix ? `${prefix}[${key}]` : key;
|
|
108
|
-
if (Array.isArray(value)) {
|
|
109
|
-
for (let i = 0; i < value.length; i++) {
|
|
110
|
-
const item = value[i];
|
|
111
|
-
if (item !== null &&
|
|
112
|
-
item !== undefined &&
|
|
113
|
-
typeof item === "object" &&
|
|
114
|
-
!Array.isArray(item)) {
|
|
115
|
-
pairs.push(...flattenToFormPairs(item, `${fullKey}[${i}]`));
|
|
116
|
-
}
|
|
117
|
-
else if (item !== undefined && item !== null) {
|
|
118
|
-
pairs.push([`${fullKey}[${i}]`, String(item)]);
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
else if (typeof value === "object") {
|
|
123
|
-
pairs.push(...flattenToFormPairs(value, fullKey));
|
|
124
|
-
}
|
|
125
|
-
else if (typeof value === "boolean") {
|
|
126
|
-
pairs.push([fullKey, value ? "true" : "false"]);
|
|
127
|
-
}
|
|
128
|
-
else {
|
|
129
|
-
pairs.push([fullKey, String(value)]);
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
return pairs;
|
|
133
|
-
}
|
|
134
|
-
/**
|
|
135
|
-
* Build a URLSearchParams from a nested object using Stripe deepObject encoding.
|
|
136
|
-
*/
|
|
137
|
-
function buildFormUrlEncoded(body) {
|
|
138
|
-
const pairs = flattenToFormPairs(body);
|
|
139
|
-
const params = new URLSearchParams();
|
|
140
|
-
for (const [key, value] of pairs) {
|
|
141
|
-
params.append(key, value);
|
|
142
|
-
}
|
|
143
|
-
return params.toString();
|
|
144
|
-
}
|
|
145
|
-
// ============================================================================
|
|
146
|
-
// Multipart FormData Builder
|
|
147
|
-
// ============================================================================
|
|
148
|
-
/**
|
|
149
|
-
* Check if a value is a File or Blob.
|
|
150
|
-
*/
|
|
151
|
-
function isFileOrBlob(value) {
|
|
152
|
-
return ((typeof File !== "undefined" && value instanceof File) ||
|
|
153
|
-
(typeof Blob !== "undefined" && value instanceof Blob));
|
|
154
|
-
}
|
|
155
|
-
/**
|
|
156
|
-
* Build a FormData from a record of body properties.
|
|
157
|
-
* Handles files/blobs, arrays of files, objects (as JSON blobs), and primitives.
|
|
158
|
-
*
|
|
159
|
-
* This is used for multipart operations (e.g., Cloudflare Workers script uploads)
|
|
160
|
-
* where the body contains a mix of metadata objects and file uploads.
|
|
161
|
-
*/
|
|
162
|
-
function buildFormData(body) {
|
|
163
|
-
const formData = new FormData();
|
|
164
|
-
for (const [key, value] of Object.entries(body)) {
|
|
165
|
-
if (value === undefined || value === null)
|
|
166
|
-
continue;
|
|
167
|
-
if (isFileOrBlob(value)) {
|
|
168
|
-
// Single file/blob
|
|
169
|
-
formData.append(key, value, value instanceof File ? value.name : key);
|
|
170
|
-
}
|
|
171
|
-
else if (Array.isArray(value) &&
|
|
172
|
-
value.length > 0 &&
|
|
173
|
-
isFileOrBlob(value[0])) {
|
|
174
|
-
// Array of files/blobs — append each individually
|
|
175
|
-
for (const file of value) {
|
|
176
|
-
if (isFileOrBlob(file)) {
|
|
177
|
-
formData.append(file instanceof File ? file.name : key, file, file instanceof File ? file.name : undefined);
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
|
-
else if (typeof value === "object" && value !== null) {
|
|
182
|
-
// Object → append as JSON string (matches wrangler's formData.set(key, JSON.stringify(value)))
|
|
183
|
-
formData.append(key, JSON.stringify(value));
|
|
184
|
-
}
|
|
185
|
-
else {
|
|
186
|
-
// Primitive → append as string
|
|
187
|
-
formData.append(key, String(value));
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
return formData;
|
|
191
|
-
}
|
|
192
|
-
/**
|
|
193
|
-
* Set a raw binary HTTP request body.
|
|
194
|
-
*
|
|
195
|
-
* Used for `T.Http({ contentType: "binary" })` operations (e.g. R2 PutObject)
|
|
196
|
-
* where `parts.body` is the value of the lone `T.HttpBody()` field — wide
|
|
197
|
-
* input types are accepted: `Blob`, `Uint8Array`, `ArrayBuffer`, `string`,
|
|
198
|
-
* web `ReadableStream<Uint8Array>`, or Effect `Stream.Stream<Uint8Array>`.
|
|
199
|
-
* Stream-shaped inputs are sent as true streaming `HttpBody.stream(...)`
|
|
200
|
-
* uploads.
|
|
201
|
-
*
|
|
202
|
-
* The `Content-Type` header is left untouched (the operation's
|
|
203
|
-
* `content-type` header field already populated `parts.headers`).
|
|
204
|
-
*/
|
|
205
|
-
function setBinaryBody(request, body, contentType) {
|
|
206
|
-
// The body's own content-type is what the request ultimately sends — pass the
|
|
207
|
-
// resolved media type (e.g. application/x-ndjson) into the HttpBody so it is
|
|
208
|
-
// not clobbered back to the octet-stream default.
|
|
209
|
-
if (body instanceof Uint8Array) {
|
|
210
|
-
return Effect.succeed(HttpClientRequest.setBody(HttpBody.uint8Array(body, contentType))(request));
|
|
211
|
-
}
|
|
212
|
-
if (typeof ArrayBuffer !== "undefined" && body instanceof ArrayBuffer) {
|
|
213
|
-
return Effect.succeed(HttpClientRequest.setBody(HttpBody.uint8Array(new Uint8Array(body), contentType))(request));
|
|
214
|
-
}
|
|
215
|
-
if (typeof Blob !== "undefined" && body instanceof Blob) {
|
|
216
|
-
// Stream the Blob through `HttpBody.stream` rather than buffering — keeps
|
|
217
|
-
// memory bounded for large uploads.
|
|
218
|
-
const blob = body;
|
|
219
|
-
const blobStream = Stream.fromReadableStream({
|
|
220
|
-
evaluate: () => blob.stream(),
|
|
221
|
-
onError: (cause) => new HttpBody.HttpBodyError({ reason: { _tag: "JsonError" }, cause }),
|
|
222
|
-
});
|
|
223
|
-
return Effect.succeed(HttpClientRequest.setBody(HttpBody.stream(blobStream, contentType))(request));
|
|
224
|
-
}
|
|
225
|
-
if (typeof ReadableStream !== "undefined" && body instanceof ReadableStream) {
|
|
226
|
-
const rs = body;
|
|
227
|
-
const rsStream = Stream.fromReadableStream({
|
|
228
|
-
evaluate: () => rs,
|
|
229
|
-
onError: (cause) => new HttpBody.HttpBodyError({ reason: { _tag: "JsonError" }, cause }),
|
|
230
|
-
});
|
|
231
|
-
return Effect.succeed(HttpClientRequest.setBody(HttpBody.stream(rsStream, contentType))(request));
|
|
232
|
-
}
|
|
233
|
-
if (Stream.isStream(body)) {
|
|
234
|
-
// Effect Stream — pass straight through to `HttpBody.stream`.
|
|
235
|
-
return Effect.succeed(HttpClientRequest.setBody(HttpBody.stream(body, contentType))(request));
|
|
236
|
-
}
|
|
237
|
-
if (typeof body === "string") {
|
|
238
|
-
return Effect.succeed(HttpClientRequest.setBody(HttpBody.text(body, contentType))(request));
|
|
239
|
-
}
|
|
240
|
-
return Effect.fail(new HttpBody.HttpBodyError({
|
|
241
|
-
reason: { _tag: "JsonError" },
|
|
242
|
-
cause: new TypeError(`Binary HTTP body must be a Blob, Uint8Array, ArrayBuffer, ReadableStream, Stream<Uint8Array>, or string; got ${body === null ? "null" : typeof body}`),
|
|
243
|
-
}));
|
|
244
|
-
}
|
|
245
|
-
// ============================================================================
|
|
246
|
-
// API Client Factory
|
|
247
|
-
// ============================================================================
|
|
248
|
-
/**
|
|
249
|
-
* Creates an API namespace bound to a specific SDK's client configuration.
|
|
250
|
-
*
|
|
251
|
-
* @example
|
|
252
|
-
* ```ts
|
|
253
|
-
* // In planetscale-sdk/src/client.ts
|
|
254
|
-
* export const API = makeAPI({
|
|
255
|
-
* credentials: Credentials,
|
|
256
|
-
* getBaseUrl: (c) => c.apiBaseUrl,
|
|
257
|
-
* getAuthHeaders: (c) => ({ Authorization: c.token }),
|
|
258
|
-
* matchError: matchPlanetScaleError,
|
|
259
|
-
* ParseError: PlanetScaleParseError,
|
|
260
|
-
* });
|
|
261
|
-
* ```
|
|
262
|
-
*/
|
|
263
|
-
export const makeAPI = (config) => {
|
|
264
|
-
return {
|
|
265
|
-
make: (configFn) => {
|
|
266
|
-
let prepared;
|
|
267
|
-
const prepare = () => {
|
|
268
|
-
if (prepared)
|
|
269
|
-
return prepared;
|
|
270
|
-
const opConfig = configFn();
|
|
271
|
-
// Support both input/output and inputSchema/outputSchema aliases
|
|
272
|
-
const inputSchema = (opConfig.inputSchema ?? opConfig.input);
|
|
273
|
-
const outputSchema = (opConfig.outputSchema ?? opConfig.output);
|
|
274
|
-
const inputAst = resolveAst(inputSchema.ast);
|
|
275
|
-
const outputAst = resolveAst(outputSchema.ast);
|
|
276
|
-
// Read trait annotations from the *unresolved* schema ASTs. A trait
|
|
277
|
-
// applied to an already-suspended schema (e.g. `T.ResponsePath` on a
|
|
278
|
-
// shared, suspended response struct) lives on the Suspend node itself,
|
|
279
|
-
// which `resolveAst` descends past — so resolving first would drop it.
|
|
280
|
-
// `getAnnotation` follows Suspend thunks, so the unresolved ast finds
|
|
281
|
-
// annotations at any suspend depth.
|
|
282
|
-
const httpTrait = Traits.getHttpTrait(inputSchema.ast);
|
|
283
|
-
if (!httpTrait) {
|
|
284
|
-
throw new Error("Input schema must have Http trait");
|
|
285
|
-
}
|
|
286
|
-
const method = httpTrait.method;
|
|
287
|
-
prepared = {
|
|
288
|
-
opConfig,
|
|
289
|
-
inputSchema,
|
|
290
|
-
outputSchema,
|
|
291
|
-
inputAst,
|
|
292
|
-
outputAst,
|
|
293
|
-
responsePath: Traits.getResponsePath(outputSchema.ast),
|
|
294
|
-
graphqlOp: Traits.getGraphQLOp(inputSchema.ast),
|
|
295
|
-
noFollowRedirect: Traits.getNoFollowRedirect(inputSchema.ast),
|
|
296
|
-
httpTrait,
|
|
297
|
-
method,
|
|
298
|
-
spanName: `${method} ${httpTrait.path}`,
|
|
299
|
-
};
|
|
300
|
-
return prepared;
|
|
301
|
-
};
|
|
302
|
-
const innerFn = (input, requestOptions) => Effect.gen(function* () {
|
|
303
|
-
const { opConfig, inputSchema, outputSchema, inputAst, outputAst, responsePath, graphqlOp, noFollowRedirect, httpTrait, method, } = prepare();
|
|
304
|
-
const credentials = yield* config.credentials;
|
|
305
|
-
const creds = isEffectLike(credentials)
|
|
306
|
-
? yield* credentials
|
|
307
|
-
: credentials;
|
|
308
|
-
const client = yield* HttpClient.HttpClient;
|
|
309
|
-
// Fall back to the Service trait when the consumer leaves
|
|
310
|
-
// `getBaseUrl` empty (per-service hosts rather than per-credentials).
|
|
311
|
-
let baseUrl = config.getBaseUrl(creds);
|
|
312
|
-
if (!baseUrl) {
|
|
313
|
-
const svcTrait = Traits.getServiceTrait(inputAst);
|
|
314
|
-
if (svcTrait?.rootUrl) {
|
|
315
|
-
baseUrl = svcTrait.rootUrl + (svcTrait.servicePath ?? "");
|
|
316
|
-
}
|
|
317
|
-
}
|
|
318
|
-
const authHeaders = config.getAuthHeaders(creds);
|
|
319
|
-
// Use schema-aware request builder for proper camelCase → wire_name mapping
|
|
320
|
-
let parts = Traits.buildRequestParts(inputAst, httpTrait, input, inputSchema);
|
|
321
|
-
// GraphQL: wrap variables in the standard GraphQL request envelope.
|
|
322
|
-
// All input fields become `variables`; `query` and `operationName`
|
|
323
|
-
// come from the trait (baked in at generation time).
|
|
324
|
-
if (graphqlOp) {
|
|
325
|
-
parts = {
|
|
326
|
-
...parts,
|
|
327
|
-
body: {
|
|
328
|
-
query: graphqlOp.query,
|
|
329
|
-
operationName: graphqlOp.operationName,
|
|
330
|
-
variables: parts.body ?? {},
|
|
331
|
-
},
|
|
332
|
-
};
|
|
333
|
-
}
|
|
334
|
-
if (config.transformRequestParts) {
|
|
335
|
-
parts = config.transformRequestParts({
|
|
336
|
-
input: input,
|
|
337
|
-
method,
|
|
338
|
-
pathTemplate: httpTrait.path,
|
|
339
|
-
parts,
|
|
340
|
-
requestOptions,
|
|
341
|
-
});
|
|
342
|
-
}
|
|
343
|
-
// Inject a baked-in `api-version` query param for versioned APIs
|
|
344
|
-
// (e.g. Azure ARM, where it is required on every call and differs per
|
|
345
|
-
// resource provider). Applied for all methods; a caller-supplied
|
|
346
|
-
// `api-version` already present in the query takes precedence.
|
|
347
|
-
if (httpTrait.apiVersion &&
|
|
348
|
-
parts.query["api-version"] === undefined) {
|
|
349
|
-
parts = {
|
|
350
|
-
...parts,
|
|
351
|
-
query: { ...parts.query, "api-version": httpTrait.apiVersion },
|
|
352
|
-
};
|
|
353
|
-
}
|
|
354
|
-
const requestHeaders = config.getRequestHeaders?.(requestOptions, {
|
|
355
|
-
input: input,
|
|
356
|
-
method,
|
|
357
|
-
pathTemplate: httpTrait.path,
|
|
358
|
-
parts,
|
|
359
|
-
credentials: creds,
|
|
360
|
-
}) ?? {};
|
|
361
|
-
let request = HttpClientRequest.make(method)(baseUrl + parts.path).pipe(HttpClientRequest.setHeaders(authHeaders), HttpClientRequest.setHeaders(parts.headers), HttpClientRequest.setHeaders(requestHeaders), HttpClientRequest.setHeader("Accept", "application/json"));
|
|
362
|
-
// Set Content-Type based on body type
|
|
363
|
-
// - Skip for FormData (multipart) — browser sets boundary
|
|
364
|
-
// - Skip for binary — `parts.headers` already carries a caller-supplied
|
|
365
|
-
// `content-type` header (e.g. R2 PutObject's `content-type` field)
|
|
366
|
-
// - Use form-urlencoded for Stripe-style APIs
|
|
367
|
-
// - Default to JSON
|
|
368
|
-
const isFormUrlEncoded = httpTrait.contentType === "form-urlencoded";
|
|
369
|
-
const isBinaryBody = httpTrait.contentType === "binary";
|
|
370
|
-
if (parts.isMultipart) {
|
|
371
|
-
// browser/runtime sets Content-Type with boundary
|
|
372
|
-
}
|
|
373
|
-
else if (isBinaryBody) {
|
|
374
|
-
// Content-Type is applied via the body below (setBinaryBody), so it
|
|
375
|
-
// is not clobbered back to octet-stream by `setBody`.
|
|
376
|
-
}
|
|
377
|
-
else if (isFormUrlEncoded) {
|
|
378
|
-
request = HttpClientRequest.setHeader("Content-Type", "application/x-www-form-urlencoded")(request);
|
|
379
|
-
}
|
|
380
|
-
else {
|
|
381
|
-
request = HttpClientRequest.setHeader("Content-Type", "application/json")(request);
|
|
382
|
-
}
|
|
383
|
-
if (Object.keys(parts.query).length > 0) {
|
|
384
|
-
request = HttpClientRequest.setUrlParams(request, parts.query);
|
|
385
|
-
}
|
|
386
|
-
if (method !== "GET" && parts.body !== undefined) {
|
|
387
|
-
if (parts.isMultipart) {
|
|
388
|
-
// Build FormData from body properties for multipart operations
|
|
389
|
-
const formData = buildFormData(parts.body);
|
|
390
|
-
request = HttpClientRequest.setBody(HttpBody.formData(formData))(request);
|
|
391
|
-
}
|
|
392
|
-
else if (isBinaryBody) {
|
|
393
|
-
// Raw binary HTTP body — `parts.body` is the value of the lone
|
|
394
|
-
// `T.HttpBody()` field (e.g. a `Blob`, `Uint8Array`, or string),
|
|
395
|
-
// not a record of body fields. Caller's `content-type` header
|
|
396
|
-
// wins, then the op's `bodyMediaType`, else octet-stream.
|
|
397
|
-
const binaryContentType = parts.headers["content-type"] ??
|
|
398
|
-
parts.headers["Content-Type"] ??
|
|
399
|
-
httpTrait.bodyMediaType ??
|
|
400
|
-
"application/octet-stream";
|
|
401
|
-
request = yield* setBinaryBody(request, parts.body, binaryContentType);
|
|
402
|
-
}
|
|
403
|
-
else if (isFormUrlEncoded) {
|
|
404
|
-
// Encode body as form-urlencoded with deepObject bracket notation
|
|
405
|
-
const encoded = buildFormUrlEncoded(parts.body);
|
|
406
|
-
request = HttpClientRequest.setBody(HttpBody.text(encoded, "application/x-www-form-urlencoded"))(request);
|
|
407
|
-
}
|
|
408
|
-
else {
|
|
409
|
-
request = yield* HttpClientRequest.bodyJson(parts.body)(request);
|
|
410
|
-
}
|
|
411
|
-
}
|
|
412
|
-
else if (method === "GET" && parts.body !== undefined) {
|
|
413
|
-
// For GET requests, remaining non-annotated fields go as query params
|
|
414
|
-
const extraQuery = {};
|
|
415
|
-
for (const [key, value] of Object.entries(parts.body)) {
|
|
416
|
-
if (value !== undefined) {
|
|
417
|
-
extraQuery[key] = String(value);
|
|
418
|
-
}
|
|
419
|
-
}
|
|
420
|
-
if (Object.keys(extraQuery).length > 0) {
|
|
421
|
-
request = HttpClientRequest.setUrlParams(request, extraQuery);
|
|
422
|
-
}
|
|
423
|
-
}
|
|
424
|
-
const requestUrl = baseUrl + parts.path;
|
|
425
|
-
yield* Effect.logDebug(`→ ${method} ${requestUrl}`);
|
|
426
|
-
// For operations that opt out of following redirects, hand the
|
|
427
|
-
// underlying fetch a `redirect: "manual"` request init so the
|
|
428
|
-
// 3xx surfaces here instead of being chased to the IdP.
|
|
429
|
-
const executeRequest = noFollowRedirect
|
|
430
|
-
? client.execute(request).pipe(Effect.scoped, Effect.provideService(FetchHttpClient.RequestInit, {
|
|
431
|
-
redirect: "manual",
|
|
432
|
-
}))
|
|
433
|
-
: client.execute(request).pipe(Effect.scoped);
|
|
434
|
-
const response = yield* executeRequest;
|
|
435
|
-
yield* Effect.logDebug(`← ${response.status} ${method} ${requestUrl}`);
|
|
436
|
-
// For ops that opted out of redirect-following, treat 3xx as
|
|
437
|
-
// success: synthesize a body containing the Location header
|
|
438
|
-
// value at `locationField` (default `"url"`) and feed that
|
|
439
|
-
// through the normal output schema decode below.
|
|
440
|
-
if (noFollowRedirect &&
|
|
441
|
-
response.status >= 300 &&
|
|
442
|
-
response.status < 400) {
|
|
443
|
-
const location = response.headers["location"] ?? response.headers["Location"];
|
|
444
|
-
if (location !== undefined) {
|
|
445
|
-
const synthBody = {
|
|
446
|
-
[noFollowRedirect.locationField ?? "url"]: location,
|
|
447
|
-
};
|
|
448
|
-
return yield* Schema.decodeUnknownEffect(outputSchema)(synthBody).pipe(Effect.catchTag("SchemaError", (cause) => Effect.fail(new config.ParseError({ body: synthBody, cause }))));
|
|
449
|
-
}
|
|
450
|
-
}
|
|
451
|
-
if (response.status >= 400) {
|
|
452
|
-
// Try to parse error body as JSON; fall back to text if not JSON
|
|
453
|
-
const errorBody = yield* response.json.pipe(Effect.catchIf(() => true, () => response.text.pipe(Effect.map((text) => ({ _nonJsonError: true, body: text })), Effect.catchIf(() => true, () => Effect.succeed({
|
|
454
|
-
_nonJsonError: true,
|
|
455
|
-
body: `HTTP ${response.status}`,
|
|
456
|
-
})))));
|
|
457
|
-
return yield* config.matchError(response.status, errorBody, opConfig.errors, response.headers);
|
|
458
|
-
}
|
|
459
|
-
// For void-returning operations (e.g. DELETE 204 No Content)
|
|
460
|
-
if (AST.isVoid(outputAst)) {
|
|
461
|
-
return undefined;
|
|
462
|
-
}
|
|
463
|
-
// Raw octet-stream download (`responseContentType: "binary"`):
|
|
464
|
-
// bypass the JSON/text decode path entirely. The output schema is
|
|
465
|
-
// a Struct shaped like `{ body: Stream<Uint8Array>, ...headers }`
|
|
466
|
-
// (see `T.BinaryResponseBody()` / `T.HttpResponseHeader()`); we
|
|
467
|
-
// populate it by reading response headers and wrapping the body
|
|
468
|
-
// bytes in `Stream.succeed`. We buffer through
|
|
469
|
-
// `response.arrayBuffer` first so the resulting stream is
|
|
470
|
-
// scope-free — callers can consume it after the underlying scope
|
|
471
|
-
// has closed. (True chunked streaming would require threading a
|
|
472
|
-
// Scope through every operation's return type, which would break
|
|
473
|
-
// the uniform `OperationMethod<I, A, E, never>` shape.)
|
|
474
|
-
if (httpTrait.responseContentType === "binary") {
|
|
475
|
-
const bytes = yield* response.arrayBuffer;
|
|
476
|
-
const stream = Stream.succeed(new Uint8Array(bytes));
|
|
477
|
-
return Traits.buildBinaryResponse(outputAst, stream, response.headers);
|
|
478
|
-
}
|
|
479
|
-
// For 204 No Content: if schema is not Unknown, return undefined.
|
|
480
|
-
// If schema IS Unknown, return empty string (so callers get a defined value).
|
|
481
|
-
if (response.status === 204) {
|
|
482
|
-
if (outputAst._tag === "Unknown") {
|
|
483
|
-
return "";
|
|
484
|
-
}
|
|
485
|
-
return undefined;
|
|
486
|
-
}
|
|
487
|
-
// Try to parse response as JSON; fall back to text for non-JSON responses
|
|
488
|
-
// (e.g., multipart/form-data worker scripts, raw KV values)
|
|
489
|
-
const rawBody = yield* response.json.pipe(Effect.catchIf(() => true, () => response.text.pipe(Effect.map((text) => text))));
|
|
490
|
-
let responseBody = config.transformResponse
|
|
491
|
-
? config.transformResponse(rawBody)
|
|
492
|
-
: rawBody;
|
|
493
|
-
// GraphQL: surface errors[] (returned with HTTP 200) via matchError,
|
|
494
|
-
// then leave unwrap to the output schema's `T.ResponsePath` trait,
|
|
495
|
-
// which the generator emits with the field path from `data`. This
|
|
496
|
-
// handles namespaced ops (e.g. `data.channels.byId`) uniformly with
|
|
497
|
-
// top-level ones (e.g. `data.me`).
|
|
498
|
-
if (graphqlOp) {
|
|
499
|
-
const envelope = responseBody;
|
|
500
|
-
if (envelope &&
|
|
501
|
-
Array.isArray(envelope.errors) &&
|
|
502
|
-
envelope.errors.length > 0) {
|
|
503
|
-
return yield* config.matchError(response.status, envelope, opConfig.errors, response.headers);
|
|
504
|
-
}
|
|
505
|
-
responseBody = envelope?.data ?? null;
|
|
506
|
-
}
|
|
507
|
-
// Some APIs return a JSON *string* (double-encoded JSON). `response.json`
|
|
508
|
-
// then yields a string, `getPath` bails out, and we would decode the wrong
|
|
509
|
-
// shape (e.g. Cloudflare envelopes without unwrapping `result`).
|
|
510
|
-
if (typeof responseBody === "string") {
|
|
511
|
-
try {
|
|
512
|
-
responseBody = JSON.parse(responseBody);
|
|
513
|
-
}
|
|
514
|
-
catch {
|
|
515
|
-
// leave as string for callers that expect raw text
|
|
516
|
-
}
|
|
517
|
-
}
|
|
518
|
-
// Some APIs (Cloudflare) answer with a 2xx status but an error
|
|
519
|
-
// envelope (`success: false`) rather than a 4xx. Route those through
|
|
520
|
-
// `matchError` with the operation's typed `errors` so per-operation
|
|
521
|
-
// matchers fire — otherwise the envelope falls through to schema
|
|
522
|
-
// decoding and surfaces as an opaque ParseError.
|
|
523
|
-
if (config.isErrorEnvelope?.(responseBody)) {
|
|
524
|
-
return yield* config.matchError(response.status, responseBody, opConfig.errors, response.headers);
|
|
525
|
-
}
|
|
526
|
-
// Tracks a `result: null` success that we optimistically coerce to
|
|
527
|
-
// `{}` below. Some ops legitimately return `null` (e.g. a per-zone
|
|
528
|
-
// singleton that was never configured) and declare a nullable
|
|
529
|
-
// output schema — for those, decoding `{}` fails, so we retry the
|
|
530
|
-
// decode with `null` (see the decode block).
|
|
531
|
-
let resultWasNull = false;
|
|
532
|
-
if (responsePath) {
|
|
533
|
-
const nested = getPath(responseBody, responsePath);
|
|
534
|
-
if (nested !== undefined) {
|
|
535
|
-
if (responsePath === "result" && nested === null) {
|
|
536
|
-
responseBody = {};
|
|
537
|
-
resultWasNull = true;
|
|
538
|
-
}
|
|
539
|
-
else {
|
|
540
|
-
responseBody = nested;
|
|
541
|
-
}
|
|
542
|
-
}
|
|
543
|
-
}
|
|
544
|
-
// Handle Cloudflare-style paginated responses where result is
|
|
545
|
-
// { items: [...] } but the schema expects an array
|
|
546
|
-
if (isArrayAST(outputAst) &&
|
|
547
|
-
!Array.isArray(responseBody) &&
|
|
548
|
-
typeof responseBody === "object" &&
|
|
549
|
-
responseBody !== null &&
|
|
550
|
-
"items" in responseBody &&
|
|
551
|
-
Array.isArray(responseBody.items)) {
|
|
552
|
-
responseBody = responseBody.items;
|
|
553
|
-
}
|
|
554
|
-
// A list operation whose schema is an array, but whose `result` came
|
|
555
|
-
// back `null` (coerced to `{}` above), is simply an empty collection:
|
|
556
|
-
// Cloudflare returns `result: null` instead of `[]` when there are
|
|
557
|
-
// zero items. Coerce to `[]` so the list decodes cleanly rather than
|
|
558
|
-
// failing the array decode and surfacing as a ParseError.
|
|
559
|
-
if (resultWasNull && isArrayAST(outputAst)) {
|
|
560
|
-
responseBody = [];
|
|
561
|
-
resultWasNull = false;
|
|
562
|
-
}
|
|
563
|
-
// Distinguish two very different decode failures:
|
|
564
|
-
// 1. NON-empty body that doesn't match the schema → a genuine
|
|
565
|
-
// schema gap in the SDK. Surface as `ParseError` (NOT retryable)
|
|
566
|
-
// so it gets patched (Typed Error Doctrine). Retrying it would
|
|
567
|
-
// only mask the bug.
|
|
568
|
-
// 2. EMPTY / null body where a structured response was expected →
|
|
569
|
-
// there is nothing to parse. This is a transient, incomplete
|
|
570
|
-
// transport response (e.g. the edge answering a 2xx with a bare
|
|
571
|
-
// `null`/empty body under load), NOT a schema bug. Surface it as
|
|
572
|
-
// a retryable `TransportError` so the bounded retry policy
|
|
573
|
-
// re-fetches the real body. (Void/204 and nullable-schema ops
|
|
574
|
-
// decode an empty body successfully and never reach here.)
|
|
575
|
-
const bodyIsEmpty = rawBody === null || rawBody === undefined || rawBody === "";
|
|
576
|
-
return yield* Schema.decodeUnknownEffect(outputSchema)(responseBody).pipe(Effect.catchTag("SchemaError", (cause) =>
|
|
577
|
-
// A `result: null` success coerced to `{}` that the schema
|
|
578
|
-
// rejects: retry decoding the genuine `null` (the schema may be
|
|
579
|
-
// a nullable union). Only then surface the parse error.
|
|
580
|
-
resultWasNull
|
|
581
|
-
? Schema.decodeUnknownEffect(outputSchema)(null).pipe(Effect.catchTag("SchemaError", () => Effect.fail(new config.ParseError({ body: rawBody, cause }))))
|
|
582
|
-
: bodyIsEmpty
|
|
583
|
-
? Effect.fail(new HttpClientError.HttpClientError({
|
|
584
|
-
reason: new HttpClientError.TransportError({
|
|
585
|
-
request,
|
|
586
|
-
cause,
|
|
587
|
-
description: "Empty response body where a structured response was expected",
|
|
588
|
-
}),
|
|
589
|
-
}))
|
|
590
|
-
: Effect.fail(new config.ParseError({ body: rawBody, cause }))));
|
|
591
|
-
});
|
|
592
|
-
// Auto-retry every operation using the SDK's per-client `Retry`
|
|
593
|
-
// Context.Service. The policy is read with `Effect.serviceOption`
|
|
594
|
-
// and falls back to `Retry.makeDefault` (transient/throttling/server
|
|
595
|
-
// errors with capped exponential backoff + jitter, 5 attempts) when
|
|
596
|
-
// no policy has been provided in context. This mirrors the AWS
|
|
597
|
-
// pattern in `packages/aws/src/client/api.ts` and lets callers
|
|
598
|
-
// install a blanket policy at the layer level instead of wrapping
|
|
599
|
-
// every call site with `Effect.retry(...)`.
|
|
600
|
-
const retryTag = config.retry;
|
|
601
|
-
const fn = (input, requestOptions) => {
|
|
602
|
-
const { spanName, method, httpTrait } = prepare();
|
|
603
|
-
const withRetry = Effect.gen(function* () {
|
|
604
|
-
const lastError = yield* Ref.make(undefined);
|
|
605
|
-
const policy = (yield* Effect.serviceOption(retryTag)).pipe(Option.map((value) => typeof value === "function" ? value(lastError) : value), Option.getOrElse(() => makeDefault(lastError)));
|
|
606
|
-
return yield* pipe(innerFn(input, requestOptions), Effect.tapError((error) => Ref.set(lastError, error)), policy.while
|
|
607
|
-
? (eff) => Effect.retry(eff, {
|
|
608
|
-
while: policy.while,
|
|
609
|
-
schedule: policy.schedule,
|
|
610
|
-
})
|
|
611
|
-
: (eff) => eff);
|
|
612
|
-
});
|
|
613
|
-
const withSpan = withRetry.pipe(Effect.withSpan(spanName, {
|
|
614
|
-
attributes: {
|
|
615
|
-
"http.method": method,
|
|
616
|
-
"http.route": httpTrait.path,
|
|
617
|
-
},
|
|
618
|
-
}));
|
|
619
|
-
return Effect.flatMap(distilledDebugConfig, (isDebug) => isDebug
|
|
620
|
-
? Effect.provideService(withSpan, MinimumLogLevel, "Debug")
|
|
621
|
-
: withSpan);
|
|
622
|
-
};
|
|
623
|
-
const Proto = {
|
|
624
|
-
[Symbol.iterator]() {
|
|
625
|
-
return new SingleShotGen(this.asEffect());
|
|
626
|
-
},
|
|
627
|
-
pipe() {
|
|
628
|
-
return pipeArguments(this.asEffect(), arguments);
|
|
629
|
-
},
|
|
630
|
-
asEffect() {
|
|
631
|
-
return Effect.map(Effect.context(), (context) => (input, requestOptions) => Effect.provideContext(fn(input, requestOptions), context));
|
|
632
|
-
},
|
|
633
|
-
};
|
|
634
|
-
return Object.assign(fn, Proto);
|
|
635
|
-
},
|
|
636
|
-
makePaginated: (configFn, paginateFn) => {
|
|
637
|
-
const opConfig = configFn();
|
|
638
|
-
const pagination = opConfig.pagination;
|
|
639
|
-
// Create the base operation
|
|
640
|
-
const baseFn = makeAPI(config).make(() => ({
|
|
641
|
-
inputSchema: opConfig.inputSchema ?? opConfig.input,
|
|
642
|
-
outputSchema: opConfig.outputSchema ?? opConfig.output,
|
|
643
|
-
errors: opConfig.errors,
|
|
644
|
-
}));
|
|
645
|
-
const paginate = paginateFn ?? paginateWithDefaults;
|
|
646
|
-
// Stream all pages
|
|
647
|
-
const pagesFn = (input, requestOptions) => paginate(baseFn, input, pagination, requestOptions);
|
|
648
|
-
// Stream individual items
|
|
649
|
-
const itemsFn = (input, requestOptions) => pagination.items
|
|
650
|
-
? extractItems(pagesFn(input, requestOptions), pagination.items)
|
|
651
|
-
: pagesFn(input, requestOptions);
|
|
652
|
-
const result = baseFn;
|
|
653
|
-
result.pages = pagesFn;
|
|
654
|
-
result.items = itemsFn;
|
|
655
|
-
return result;
|
|
656
|
-
},
|
|
657
|
-
};
|
|
658
|
-
};
|
|
659
|
-
//# sourceMappingURL=client.js.map
|