@distilled.cloud/core 0.30.3 → 1.0.0-rc.2

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.
Files changed (132) hide show
  1. package/lib/api.d.ts +165 -0
  2. package/lib/api.d.ts.map +1 -0
  3. package/lib/api.js +178 -0
  4. package/lib/api.js.map +1 -0
  5. package/lib/codegen/cli.d.ts +29 -0
  6. package/lib/codegen/cli.d.ts.map +1 -0
  7. package/lib/codegen/cli.js +165 -0
  8. package/lib/codegen/cli.js.map +1 -0
  9. package/lib/codegen/emit.d.ts +129 -0
  10. package/lib/codegen/emit.d.ts.map +1 -0
  11. package/lib/codegen/emit.js +105 -0
  12. package/lib/codegen/emit.js.map +1 -0
  13. package/lib/codegen/format.d.ts +23 -0
  14. package/lib/codegen/format.d.ts.map +1 -0
  15. package/lib/codegen/format.js +28 -0
  16. package/lib/codegen/format.js.map +1 -0
  17. package/lib/codegen/generator.d.ts +334 -0
  18. package/lib/codegen/generator.d.ts.map +1 -0
  19. package/lib/codegen/generator.js +691 -0
  20. package/lib/codegen/generator.js.map +1 -0
  21. package/lib/codegen/graph.d.ts +36 -0
  22. package/lib/codegen/graph.d.ts.map +1 -0
  23. package/lib/codegen/graph.js +136 -0
  24. package/lib/codegen/graph.js.map +1 -0
  25. package/lib/codegen/members.d.ts +25 -0
  26. package/lib/codegen/members.d.ts.map +1 -0
  27. package/lib/codegen/members.js +55 -0
  28. package/lib/codegen/members.js.map +1 -0
  29. package/lib/codegen/naming.d.ts +29 -0
  30. package/lib/codegen/naming.d.ts.map +1 -0
  31. package/lib/codegen/naming.js +74 -0
  32. package/lib/codegen/naming.js.map +1 -0
  33. package/lib/codegen/openapi-cli.d.ts +38 -0
  34. package/lib/codegen/openapi-cli.d.ts.map +1 -0
  35. package/lib/codegen/openapi-cli.js +107 -0
  36. package/lib/codegen/openapi-cli.js.map +1 -0
  37. package/lib/codegen/openapi.d.ts +115 -0
  38. package/lib/codegen/openapi.d.ts.map +1 -0
  39. package/lib/codegen/openapi.js +1220 -0
  40. package/lib/codegen/openapi.js.map +1 -0
  41. package/lib/codegen/operations.d.ts +24 -0
  42. package/lib/codegen/operations.d.ts.map +1 -0
  43. package/lib/codegen/operations.js +56 -0
  44. package/lib/codegen/operations.js.map +1 -0
  45. package/lib/codegen/pagination.d.ts +39 -0
  46. package/lib/codegen/pagination.d.ts.map +1 -0
  47. package/lib/codegen/pagination.js +33 -0
  48. package/lib/codegen/pagination.js.map +1 -0
  49. package/lib/codegen/prelude.d.ts +15 -0
  50. package/lib/codegen/prelude.d.ts.map +1 -0
  51. package/lib/codegen/prelude.js +60 -0
  52. package/lib/codegen/prelude.js.map +1 -0
  53. package/lib/error-category.d.ts +28 -0
  54. package/lib/error-category.d.ts.map +1 -0
  55. package/lib/error-category.js +46 -0
  56. package/lib/error-category.js.map +1 -0
  57. package/lib/errors.d.ts +1 -0
  58. package/lib/errors.d.ts.map +1 -1
  59. package/lib/errors.js +1 -0
  60. package/lib/errors.js.map +1 -1
  61. package/lib/json-patch.d.ts +25 -32
  62. package/lib/json-patch.d.ts.map +1 -1
  63. package/lib/json-patch.js +23 -95
  64. package/lib/json-patch.js.map +1 -1
  65. package/lib/pagination.d.ts +37 -51
  66. package/lib/pagination.d.ts.map +1 -1
  67. package/lib/pagination.js +72 -90
  68. package/lib/pagination.js.map +1 -1
  69. package/lib/protocol-http.d.ts +74 -0
  70. package/lib/protocol-http.d.ts.map +1 -0
  71. package/lib/protocol-http.js +554 -0
  72. package/lib/protocol-http.js.map +1 -0
  73. package/lib/protocol-rest.d.ts +124 -0
  74. package/lib/protocol-rest.d.ts.map +1 -0
  75. package/lib/protocol-rest.js +242 -0
  76. package/lib/protocol-rest.js.map +1 -0
  77. package/lib/retry.d.ts +8 -2
  78. package/lib/retry.d.ts.map +1 -1
  79. package/lib/retry.js +21 -15
  80. package/lib/retry.js.map +1 -1
  81. package/lib/schema.d.ts +7 -8
  82. package/lib/schema.d.ts.map +1 -1
  83. package/lib/schema.js +7 -8
  84. package/lib/schema.js.map +1 -1
  85. package/lib/trait.d.ts +150 -0
  86. package/lib/trait.d.ts.map +1 -0
  87. package/lib/trait.js +107 -0
  88. package/lib/trait.js.map +1 -0
  89. package/package.json +18 -75
  90. package/src/api.ts +446 -0
  91. package/src/codegen/cli.ts +268 -0
  92. package/src/codegen/emit.ts +207 -0
  93. package/src/codegen/format.ts +47 -0
  94. package/src/codegen/generator.ts +1153 -0
  95. package/src/codegen/graph.ts +151 -0
  96. package/src/codegen/members.ts +71 -0
  97. package/src/codegen/naming.ts +86 -0
  98. package/src/codegen/openapi-cli.ts +166 -0
  99. package/src/codegen/openapi.ts +1450 -0
  100. package/src/codegen/operations.ts +76 -0
  101. package/src/codegen/pagination.ts +71 -0
  102. package/src/codegen/prelude.ts +70 -0
  103. package/src/error-category.ts +84 -0
  104. package/src/errors.ts +2 -0
  105. package/src/json-patch.ts +26 -110
  106. package/src/pagination.ts +86 -142
  107. package/src/protocol-http.ts +699 -0
  108. package/src/protocol-rest.ts +367 -0
  109. package/src/retry.ts +20 -21
  110. package/src/schema.ts +7 -8
  111. package/src/trait.ts +238 -0
  112. package/README.md +0 -30
  113. package/lib/client.d.ts +0 -167
  114. package/lib/client.d.ts.map +0 -1
  115. package/lib/client.js +0 -659
  116. package/lib/client.js.map +0 -1
  117. package/lib/schemas.d.ts +0 -60
  118. package/lib/schemas.d.ts.map +0 -1
  119. package/lib/schemas.js +0 -79
  120. package/lib/schemas.js.map +0 -1
  121. package/lib/sensitive.d.ts +0 -71
  122. package/lib/sensitive.d.ts.map +0 -1
  123. package/lib/sensitive.js +0 -96
  124. package/lib/sensitive.js.map +0 -1
  125. package/lib/traits.d.ts +0 -421
  126. package/lib/traits.d.ts.map +0 -1
  127. package/lib/traits.js +0 -737
  128. package/lib/traits.js.map +0 -1
  129. package/src/client.ts +0 -1177
  130. package/src/schemas.ts +0 -128
  131. package/src/sensitive.ts +0 -119
  132. 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