@shipworthy/openapi-fetch 0.0.2 → 1.0.0

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/README.md CHANGED
@@ -14,12 +14,14 @@ To generate the `paths` type, install the CLI and its TypeScript generator as de
14
14
  bun add -d @shipworthy/openapi-cli @shipworthy/openapi-typescript
15
15
  ```
16
16
 
17
- These packages are build-time tooling; the fetch client has no runtime or peer dependencies.
17
+ These packages are build-time tooling. The fetch package has no runtime or peer dependencies.
18
18
 
19
19
  Requirements:
20
20
 
21
- - **No dependencies or peer dependencies.**
22
- - **ESM and CommonJS.** Published packages contain compiled JavaScript in both formats with matching `.d.ts` and `.d.cts` declarations in `dist/`; no TypeScript transpilation of `node_modules` is needed. Declarations type-check with TypeScript 5.0 and later.
21
+ - **No runtime or peer dependencies.** Compile optional case mappings with `@shipworthy/openapi-typescript` at build time.
22
+ - **ESM and CommonJS.** Both runtime entries contain both formats with matching `.d.ts` and
23
+ `.d.cts` declarations in `dist/`; no TypeScript transpilation of `node_modules` is needed.
24
+ Declarations type-check with TypeScript 5.0 and later.
23
25
  - **No Node.js engine requirement for the client.** The code-generation CLI needs Node.js `>=22.19.0`, or Bun with `bunx --bun`.
24
26
  - **Fetch API globals.** The client uses `fetch`, `Request`, `Response`, `Headers`, `FormData`, and `URLSearchParams` from `globalThis`, so it runs in browsers, Bun, Deno, and Node.js 18+. To use a different implementation, pass `fetch` and/or `Request` to `createClient`.
25
27
  - **TypeScript** with `strict` (or at least `strictNullChecks`) is recommended. Required/optional inference for `params` and `body` depends on it.
@@ -29,16 +31,16 @@ Requirements:
29
31
  ### 1. Generate types
30
32
 
31
33
  ```sh
32
- bunx --bun @shipworthy/openapi-cli generate typescript ./openapi.yaml -o ./src/api/schema.ts
34
+ bunx --bun @shipworthy/openapi-cli generate typescript -i ./openapi.yaml -o ./src/api
33
35
  ```
34
36
 
35
- The generated file exports `paths`, `components`, and `operations` interfaces. `openapi-fetch` only needs `paths`.
37
+ This writes `src/api/types.ts`. The generated file exports `paths`, `components`, and `operations` interfaces. `openapi-fetch` only needs `paths`.
36
38
 
37
39
  ### 2. Create a client and call it
38
40
 
39
41
  ```ts
40
42
  import createClient from "@shipworthy/openapi-fetch";
41
- import type { paths } from "./api/schema";
43
+ import type { paths } from "./api/types";
42
44
 
43
45
  const client = createClient<paths>({ baseUrl: "https://api.example.com/v1" });
44
46
 
@@ -85,7 +87,7 @@ Each call returns `Promise<FetchResponse>`, a discriminated union:
85
87
 
86
88
  ## API reference
87
89
 
88
- The package's single entry point (`@shipworthy/openapi-fetch`) provides these exports.
90
+ The main entry point (`@shipworthy/openapi-fetch`) provides these exports.
89
91
 
90
92
  ### `createClient<Paths, Media>(options?)` (default and named export)
91
93
 
@@ -111,16 +113,17 @@ function createClient<Paths extends {}, Media extends MediaType = MediaType>(
111
113
  | `querySerializer` | `QuerySerializer \| QuerySerializerOptions` | form/explode for arrays, deepObject/explode for objects | Global query string serialization (see [Query serialization](#query-serialization)). |
112
114
  | `bodySerializer` | `BodySerializer` | `defaultBodySerializer` | Global request body serializer. |
113
115
  | `pathSerializer` | `PathSerializer` | `defaultPathSerializer` | Global path template serializer. |
116
+ | `caseMapping` | `CaseMapping` | none | Installs schema-based casing middleware for every request and parsed result. |
114
117
  | `requestInitExt` | `Record<string, unknown>` | `undefined` | Passed as the second argument to `fetch`. It is only used on Node.js 18+ with undici (detected through `process.versions.undici`) and ignored elsewhere, including Bun. Example: an undici `dispatcher`. |
115
118
 
116
119
  #### `Client<Paths, Media>`
117
120
 
118
- | Member | Description |
119
- | ------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
120
- | `GET(url, init?)`, `PUT`, `POST`, `DELETE`, `OPTIONS`, `HEAD`, `PATCH`, `TRACE` | Call an endpoint. `url` must be a path in `Paths` that defines that method. Use the template form, such as `"/pets/{petId}"`, not a pre-filled URL. |
121
- | `request(method, url, init?)` | Same as above with a lowercase `HttpMethod` (`"get"`, `"post"`, ...). The method is uppercased at runtime. |
122
- | `use(...middleware)` | Register client-level middleware. Falsy entries are skipped. Throws `Error("Middleware must be an object with one of `onRequest()`, `onResponse() or `onError()`")` for anything else that is invalid. |
123
- | `eject(...middleware)` | Unregister middleware by reference. |
121
+ | Member | Description |
122
+ | ------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- |
123
+ | `GET(url, init?)`, `PUT`, `POST`, `DELETE`, `OPTIONS`, `HEAD`, `PATCH`, `TRACE` | Call an endpoint. `url` must be a path in `Paths` that defines that method. Use the template form, such as `"/pets/{petId}"`, not a pre-filled URL. |
124
+ | `request(method, url, init?)` | Same as above with a lowercase `HttpMethod` (`"get"`, `"post"`, ...). The method is uppercased at runtime. |
125
+ | `use(...middleware)` | Register client-level middleware. Falsy entries are skipped. Invalid objects throw `Error("Middleware must be an object with a supported hook")`. |
126
+ | `eject(...middleware)` | Unregister middleware by reference. |
124
127
 
125
128
  The `init` argument is required when the operation has required parameters or a required body. Otherwise it is optional.
126
129
 
@@ -170,7 +173,7 @@ import createClient, {
170
173
  createPathBasedClient,
171
174
  wrapAsPathBasedClient,
172
175
  } from "@shipworthy/openapi-fetch";
173
- import type { paths } from "./api/schema";
176
+ import type { paths } from "./api/types";
174
177
 
175
178
  const api = createPathBasedClient<paths>({ baseUrl: "https://api.example.com/v1" });
176
179
  const { data, error } = await api["/pets/{petId}"].GET({ params: { path: { petId: "123" } } });
@@ -209,7 +212,8 @@ client.use(auth);
209
212
  // later: client.eject(auth);
210
213
  ```
211
214
 
212
- A `Middleware` is an object with at least one of `onRequest`, `onResponse`, or `onError`. Each hook receives `MiddlewareCallbackParams`:
215
+ A `Middleware` has at least one of `onRequestInit`, `onRequest`, `onError`, `onResponse`, or
216
+ `onResult`. The existing HTTP hooks receive `MiddlewareCallbackParams`:
213
217
 
214
218
  | Field | Description |
215
219
  | ------------ | ------------------------------------------------------------------------------------------------------------- |
@@ -223,6 +227,10 @@ A `Middleware` is an object with at least one of `onRequest`, `onResponse`, or `
223
227
 
224
228
  The order of the hooks and their valid return values:
225
229
 
230
+ - **`onRequestInit`** runs before path, query, header, cookie, and body serialization, in
231
+ registration order. It receives `method`, `schemaPath`, `{ params, body }` as `input`, and
232
+ `headers` merged from client, request, and parameter headers. Return a complete replacement
233
+ `{ params, body }`, or `undefined` to keep the input. Custom serializers receive the result.
226
234
  - **`onRequest`** runs in registration order, client-level first and then per-request `middleware`.
227
235
  - Return a `Request` to replace the request.
228
236
  - Return a `Response` to skip `fetch`, the remaining `onRequest` hooks, and **all** `onResponse` hooks. The returned `Response` is used as-is: no `onResponse` hook runs for it, including hooks registered before the one that returned it. This matches upstream openapi-fetch.
@@ -237,6 +245,56 @@ The order of the hooks and their valid return values:
237
245
  - Return a `Response` to replace it.
238
246
  - Return `undefined` to keep it.
239
247
  - Any other truthy value throws `Error("onResponse: must return new Response() when modifying the response")`.
248
+ - **`onResult`** runs after response parsing, in reverse order. It receives the parsed `result`,
249
+ original `method` and `schemaPath`, and the final `Response`. Return a `data` and/or `error`
250
+ update, or `undefined` to keep the parsed values. It also runs for a synthetic `Response`
251
+ returned by `onRequest`, a response recovered by `onError`, and empty bodies. It cannot replace
252
+ `result.response`.
253
+
254
+ ### Schema-based case mapping
255
+
256
+ Pass a `caseMapping` to `createClient` to convert app-facing names in typed path, query,
257
+ header, and cookie parameters and JSON request bodies to the names declared by OpenAPI. It
258
+ converts parsed JSON success and error bodies back before returning `data` or `error`. The raw
259
+ `Response` and dictionary keys retain their wire spelling. Other response parsing modes leave
260
+ successful data unchanged.
261
+
262
+ Only fields declared by the selected schema are renamed. Undeclared fields keep their API
263
+ spelling, and an error response with no matching status schema or `default` response is returned
264
+ without conversion. String values that happen to name fields also keep their API spelling, for
265
+ example a JSON Pointer such as `errors[].pointer: "/slot_id"`.
266
+
267
+ ```ts
268
+ import createClient from "@shipworthy/openapi-fetch";
269
+ import { caseMapping } from "./generated/case-mapping";
270
+ import type { paths } from "./generated/types";
271
+
272
+ const client = createClient<paths>({ baseUrl: "https://api.example.com", caseMapping });
273
+ ```
274
+
275
+ For manual registration, import `createCaseMappingMiddleware` from
276
+ `@shipworthy/openapi-fetch/middleware/transform-casing` and call
277
+ `client.use(createCaseMappingMiddleware(caseMapping))`.
278
+
279
+ The mapping must come from the same OpenAPI document and camelCase naming rule as `paths`.
280
+ At build time, `buildCaseMapping(parsedService)` from
281
+ `@shipworthy/openapi-typescript/case-mapping` returns an
282
+ `Effect<CaseMapping, CaseMappingGenerationError>`. Include the resulting plain object in
283
+ generated code and pass it to the client:
284
+
285
+ ```ts
286
+ import { buildCaseMapping } from "@shipworthy/openapi-typescript/case-mapping";
287
+ import { decode } from "@shipworthy/openapi-transformer";
288
+ import { Effect } from "effect";
289
+
290
+ const caseMapping = await Effect.runPromise(
291
+ decode(document).pipe(Effect.flatMap(buildCaseMapping)),
292
+ );
293
+ ```
294
+
295
+ `openapi generate tanstack-query --camel-case` generates the matching types and embeds this
296
+ mapping automatically. `openapi generate typescript --camel-case` without the `tanstack-query`
297
+ plugin generates types only; it does not install runtime conversion.
240
298
 
241
299
  ### Headers
242
300
 
@@ -482,20 +540,21 @@ function removeTrailingSlash(url: string): string; // removes one trailing "/"
482
540
 
483
541
  Client and request types:
484
542
 
485
- | Type | Purpose |
486
- | ------------------------------------------------------------------------------------------------------- | ----------------------------------------------------- |
487
- | `Client<Paths, Media>` | Return type of `createClient`. |
488
- | `ClientOptions` | Options for `createClient`. |
489
- | `PathBasedClient<Paths, Media>`, `ClientForPath<PathInfo, Media>` | Path-based client shapes. |
490
- | `ClientMethod`, `ClientRequestMethod` | Signatures of `GET`/`POST`/... and of `request`. |
491
- | `FetchOptions<Op>`, `RequestOptions<Op>`, `MaybeOptionalInit<PathItem, Method>` | Shape of the `init` argument for an operation. |
492
- | `ParamsOption<Op>`, `RequestBodyOption<Op>`, `DefaultParamsOption` | The `params` and `body` parts of `init`. |
493
- | `FetchResponse<Op, Init, Media>` | The `{ data, error, response }` union. |
494
- | `ParseAs`, `ParseAsResponse<T, Options>` | `parseAs` values and the resulting `data` type. |
495
- | `HeadersOptions`, `QuerySerializer<T>`, `QuerySerializerOptions`, `BodySerializer<T>`, `PathSerializer` | Option types. |
496
- | `Middleware`, `MiddlewareCallbackParams`, `MiddlewareRequestParams`, `MergedOptions` | Middleware types. |
497
- | `ClientPathsWithMethod<Client, Method>` | The paths of a client instance that support `Method`. |
498
- | `MethodResponse<Client, Method, Path, Options?>` | The non-nullable `data` type for a call. |
543
+ | Type | Purpose |
544
+ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------- |
545
+ | `Client<Paths, Media>` | Return type of `createClient`. |
546
+ | `ClientOptions` | Options for `createClient`. |
547
+ | `PathBasedClient<Paths, Media>`, `ClientForPath<PathInfo, Media>` | Path-based client shapes. |
548
+ | `ClientMethod`, `ClientRequestMethod` | Signatures of `GET`/`POST`/... and of `request`. |
549
+ | `FetchOptions<Op>`, `RequestOptions<Op>`, `MaybeOptionalInit<PathItem, Method>` | Shape of the `init` argument for an operation. |
550
+ | `ParamsOption<Op>`, `RequestBodyOption<Op>`, `DefaultParamsOption` | The `params` and `body` parts of `init`. |
551
+ | `FetchResponse<Op, Init, Media>` | The `{ data, error, response }` union. |
552
+ | `ParseAs`, `ParseAsResponse<T, Options>` | `parseAs` values and the resulting `data` type. |
553
+ | `HeadersOptions`, `QuerySerializer<T>`, `QuerySerializerOptions`, `BodySerializer<T>`, `PathSerializer` | Option types. |
554
+ | `Middleware`, `MiddlewareCallbackParams`, `MiddlewareRequestInitParams`, `MiddlewareRequestInput`, `MiddlewareRequestParams`, `MiddlewareResult`, `MiddlewareResultParams`, `MergedOptions` | Middleware types. |
555
+ | `CaseMapping` (from `./middleware/transform-casing`) | Schema mapping accepted by `createCaseMappingMiddleware`. |
556
+ | `ClientPathsWithMethod<Client, Method>` | The paths of a client instance that support `Method`. |
557
+ | `MethodResponse<Client, Method, Path, Options?>` | The non-nullable `data` type for a call. |
499
558
 
500
559
  Schema helpers:
501
560
 
@@ -557,7 +616,7 @@ Without the flag, the types are unchanged.
557
616
  ## Key facts (for AI agents)
558
617
 
559
618
  - Import: `import createClient from "@shipworthy/openapi-fetch"`, a **default** export, also available as the named export `createClient`. Compiled ESM and CommonJS with `.d.ts`/`.d.cts` declarations and no dependencies.
560
- - Typical setup: `createClient<paths>({ baseUrl })`, where `paths` comes from `openapi generate typescript <schema> -o schema.ts`.
619
+ - Typical setup: `createClient<paths>({ baseUrl })`, where `paths` comes from `openapi generate typescript -i <schema> -o <dir>` (`<dir>/types.ts`).
561
620
  - Call signature: `client.GET("/template/{param}", { params: { path, query, header, cookie }, body, headers, parseAs, signal })`. Methods are uppercase: `GET PUT POST DELETE OPTIONS HEAD PATCH TRACE`. `client.request("get", path, init)` is also available.
562
621
  - Result: `{ data, error, response }`. The promise does not reject on 4XX/5XX. It rejects on network errors, invalid JSON in a successful response that has a `Content-Length`, and misbehaving middleware.
563
622
  - Default serialization:
@@ -567,7 +626,8 @@ Without the flag, the types are unchanged.
567
626
  - Query arrays repeat the key (`a=1&a=2`), and query objects use `deepObject` (`o[x]=1`).
568
627
  - Header precedence: client `headers` < request `headers` < `params.header`. `null` deletes a header. `headers` accepts a plain object, a `Headers` instance, or `[name, value]` tuples.
569
628
  - `params.cookie` is sent as a `Cookie` header (form/explode, URI-encoded), appended to any existing `Cookie` header.
570
- - Middleware: `client.use({ onRequest, onResponse, onError })`. `onRequest` runs in order; `onResponse` and `onError` run in reverse.
629
+ - Middleware: `client.use({ onRequestInit, onRequest, onError, onResponse, onResult })`.
630
+ Input and request hooks run in order; error, response, and parsed-result hooks run in reverse.
571
631
  - Path-based alternative: `createPathBasedClient<paths>(opts)["/pets/{id}"].GET(init)`.
572
632
  - Response data is not validated at runtime.
573
633
 
package/dist/index.cjs CHANGED
@@ -2,7 +2,7 @@ Object.defineProperties(exports, {
2
2
  __esModule: { value: true },
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
- //#region src/headers.ts
5
+ // src/headers.ts
6
6
  /** Iterate any `HeadersOptions` form (plain object, `Headers`, or `[name, value]` tuples) as entries */
7
7
  function headerEntries(h) {
8
8
  if (h instanceof Headers) return h.entries();
@@ -20,8 +20,166 @@ function mergeHeaders(...allHeaders) {
20
20
  }
21
21
  return finalHeaders;
22
22
  }
23
- //#endregion
24
- //#region src/serializers/body.ts
23
+ // src/middleware/transform-casing.ts
24
+ function expandSchemas(schemas, mapping, visited = /* @__PURE__ */ new Set()) {
25
+ return schemas.flatMap((schema) => {
26
+ const nested = schema.variants ?? [];
27
+ if (!schema.ref || visited.has(schema.ref)) return [schema, ...expandSchemas(nested, mapping, visited)];
28
+ const referenced = mapping.schemas?.[schema.ref];
29
+ const next = new Set(visited).add(schema.ref);
30
+ return [schema, ...expandSchemas(referenced ? [referenced, ...nested] : nested, mapping, next)];
31
+ });
32
+ }
33
+ function createConverter(mapping) {
34
+ const schemaIds = /* @__PURE__ */ new WeakMap();
35
+ const plans = /* @__PURE__ */ new Map();
36
+ let nextId = 0;
37
+ function planFor(schemas, direction) {
38
+ if (schemas.length === 0) return void 0;
39
+ const key = `${direction}:${schemas.map((schema) => {
40
+ let id = schemaIds.get(schema);
41
+ if (id === void 0) {
42
+ id = ++nextId;
43
+ schemaIds.set(schema, id);
44
+ }
45
+ return id;
46
+ }).join(",")}`;
47
+ const existing = plans.get(key);
48
+ if (existing) return existing;
49
+ const expanded = expandSchemas(schemas, mapping);
50
+ const fields = /* @__PURE__ */ new Map();
51
+ for (const schema of expanded) for (const [wireName, field] of Object.entries(schema.properties ?? {})) {
52
+ const sourceName = direction === "toApi" ? field.name : wireName;
53
+ const targetName = direction === "toApi" ? wireName : field.name;
54
+ const current = fields.get(sourceName);
55
+ if (current) {
56
+ if (current.name !== targetName) throw new Error(`Conflicting case mapping for ${sourceName}`);
57
+ if (field.schema) current.schemas.push(field.schema);
58
+ } else fields.set(sourceName, {
59
+ name: targetName,
60
+ schemas: field.schema ? [field.schema] : []
61
+ });
62
+ }
63
+ const plan = {
64
+ expanded,
65
+ fields,
66
+ declared: new Set(expanded.flatMap((schema) => schema.declared ?? [])),
67
+ patterns: expanded.flatMap((schema) => Object.entries(schema.patternProperties ?? {}).map(([pattern, nested]) => ({
68
+ regex: new RegExp(pattern),
69
+ schema: nested
70
+ }))),
71
+ additional: expanded.flatMap((schema) => schema.additionalProperties ? [schema.additionalProperties] : []),
72
+ prefixLength: expanded.reduce((max, schema) => Math.max(max, schema.prefixItems?.length ?? 0), 0)
73
+ };
74
+ plans.set(key, plan);
75
+ return plan;
76
+ }
77
+ function itemPlan(plan, index, direction) {
78
+ if (index >= plan.prefixLength) {
79
+ if (plan.items === void 0) plan.items = planFor(plan.expanded.flatMap((schema) => schema.items ? [schema.items] : []), direction) ?? null;
80
+ return plan.items ?? void 0;
81
+ }
82
+ const prefixItems = plan.prefixItems ??= [];
83
+ if (prefixItems[index] === void 0) prefixItems[index] = planFor(plan.expanded.flatMap((schema) => {
84
+ const prefix = schema.prefixItems;
85
+ const item = prefix && index < prefix.length ? prefix[index] : schema.items;
86
+ return item ? [item] : [];
87
+ }), direction) ?? null;
88
+ return prefixItems[index] ?? void 0;
89
+ }
90
+ function convertWithPlan(value, plan, direction) {
91
+ if (!plan || value === null || value === void 0) return value;
92
+ if (Array.isArray(value)) return value.map((item, index) => convertWithPlan(item, itemPlan(plan, index, direction), direction));
93
+ if (typeof value !== "object") return value;
94
+ const prototype = Object.getPrototypeOf(value);
95
+ if (prototype !== Object.prototype && prototype !== null) return value;
96
+ const convertedNames = /* @__PURE__ */ new Set();
97
+ const entries = Object.entries(value).map(([name, item]) => {
98
+ const field = plan.fields.get(name);
99
+ const matched = [];
100
+ let hasPattern = false;
101
+ for (const pattern of plan.patterns) if (pattern.regex.test(name)) {
102
+ hasPattern = true;
103
+ if (pattern.schema) matched.push(pattern.schema);
104
+ }
105
+ let nested;
106
+ if (matched.length > 0) nested = planFor([...field?.schemas ?? [], ...matched], direction);
107
+ else if (field) {
108
+ if (field.nested === void 0) field.nested = planFor(field.schemas, direction) ?? null;
109
+ nested = field.nested ?? void 0;
110
+ } else if (!plan.declared.has(name) && !hasPattern) {
111
+ if (plan.additionalPlan === void 0) plan.additionalPlan = planFor(plan.additional, direction) ?? null;
112
+ nested = plan.additionalPlan ?? void 0;
113
+ }
114
+ const targetName = field?.name ?? name;
115
+ if (convertedNames.has(targetName)) throw new Error(`Conflicting case mapping for ${targetName}`);
116
+ convertedNames.add(targetName);
117
+ return [targetName, convertWithPlan(item, nested, direction)];
118
+ });
119
+ return Object.fromEntries(entries);
120
+ }
121
+ return (value, schemas, direction) => {
122
+ if (schemas.length === 0 || value === null || value === void 0) return value;
123
+ return convertWithPlan(value, planFor(schemas, direction), direction);
124
+ };
125
+ }
126
+ function isJsonMediaType(mediaType) {
127
+ return mediaType === "application/json" || mediaType.endsWith("+json");
128
+ }
129
+ function mediaSchema(media, contentType) {
130
+ if (!media) return void 0;
131
+ const type = contentType?.split(";", 1)[0]?.trim().toLowerCase();
132
+ if (type && !isJsonMediaType(type)) return void 0;
133
+ const entries = Object.entries(media).filter(([name]) => isJsonMediaType(name.toLowerCase()));
134
+ const exact = entries.find(([name]) => name.toLowerCase() === type);
135
+ if (exact) return exact[1] ?? void 0;
136
+ const json = entries.find(([name]) => name.toLowerCase() === "application/json");
137
+ if (json) return json[1] ?? void 0;
138
+ return entries.length === 1 ? entries[0]?.[1] ?? void 0 : void 0;
139
+ }
140
+ function responseMedia(operation, response) {
141
+ const responses = operation.responses;
142
+ if (!responses) return void 0;
143
+ const status = String(response.status);
144
+ const range = `${status[0]}XX`;
145
+ return mediaSchema(Object.hasOwn(responses, status) ? responses[status] : Object.hasOwn(responses, range) ? responses[range] : responses.default, response.headers.get("content-type") ?? void 0);
146
+ }
147
+ function convertRequest(input, operation, convertValue, headers) {
148
+ const parameters = operation.parameters;
149
+ const params = input.params;
150
+ const bodySchema = mediaSchema(operation.requestBodies, headers.get("content-type") ?? void 0);
151
+ return {
152
+ params: parameters ? {
153
+ ...params,
154
+ path: convertValue(params.path, parameters.path ? [parameters.path] : [], "toApi"),
155
+ query: convertValue(params.query, parameters.query ? [parameters.query] : [], "toApi"),
156
+ header: convertValue(params.header, parameters.header ? [parameters.header] : [], "toApi"),
157
+ cookie: convertValue(params.cookie, parameters.cookie ? [parameters.cookie] : [], "toApi")
158
+ } : params,
159
+ body: bodySchema && input.body !== void 0 ? convertValue(input.body, [bodySchema], "toApi") : input.body
160
+ };
161
+ }
162
+ /** Convert generated camelCase values at the openapi-fetch request and result stages. */
163
+ function createCaseMappingMiddleware(mapping) {
164
+ const convertValue = createConverter(mapping);
165
+ return {
166
+ onRequestInit({ method, schemaPath, input, headers }) {
167
+ const operation = mapping.operations?.[`${method} ${schemaPath}`];
168
+ return operation ? convertRequest(input, operation, convertValue, headers) : void 0;
169
+ },
170
+ onResult({ method, schemaPath, result, options }) {
171
+ const operation = mapping.operations?.[`${method} ${schemaPath}`];
172
+ if (!operation) return;
173
+ const schema = responseMedia(operation, result.response);
174
+ if (!schema) return;
175
+ return {
176
+ ...options.parseAs === "json" && result.data !== void 0 ? { data: convertValue(result.data, [schema], "fromApi") } : {},
177
+ ...result.error !== void 0 ? { error: convertValue(result.error, [schema], "fromApi") } : {}
178
+ };
179
+ }
180
+ };
181
+ }
182
+ // src/serializers/body.ts
25
183
  /** Serialize body object to string */
26
184
  function defaultBodySerializer(body, headers) {
27
185
  if (body instanceof FormData) return body;
@@ -30,8 +188,7 @@ function defaultBodySerializer(body, headers) {
30
188
  }
31
189
  return JSON.stringify(body);
32
190
  }
33
- //#endregion
34
- //#region src/serializers/param.ts
191
+ // src/serializers/param.ts
35
192
  /** Serialize primitive params to string */
36
193
  function serializePrimitiveParam(name, value, options) {
37
194
  if (value === void 0 || value === null) return "";
@@ -91,8 +248,7 @@ function serializeArrayParam(name, value, options) {
91
248
  else values.push(serializePrimitiveParam(name, v, options));
92
249
  return options.style === "label" || options.style === "matrix" ? `${joiner}${values.join(joiner)}` : values.join(joiner);
93
250
  }
94
- //#endregion
95
- //#region src/serializers/cookie.ts
251
+ // src/serializers/cookie.ts
96
252
  /**
97
253
  * Serialize cookie params to a `Cookie` header value using the OpenAPI default
98
254
  * (style: form, explode: true). Arrays repeat the name, objects emit one pair per key,
@@ -114,8 +270,7 @@ function serializeCookieParams(cookieParams) {
114
270
  }
115
271
  return pairs.join("; ");
116
272
  }
117
- //#endregion
118
- //#region src/serializers/path.ts
273
+ // src/serializers/path.ts
119
274
  const PATH_PARAM_RE = /\{[^{}]+\}/g;
120
275
  /**
121
276
  * Handle different OpenAPI 3.x serialization styles
@@ -162,8 +317,7 @@ function defaultPathSerializer(pathname, pathParams) {
162
317
  }
163
318
  return nextURL;
164
319
  }
165
- //#endregion
166
- //#region src/serializers/query.ts
320
+ // src/serializers/query.ts
167
321
  /** Serialize query params to string */
168
322
  function createQuerySerializer(options) {
169
323
  return function querySerializer(queryParams) {
@@ -195,8 +349,7 @@ function createQuerySerializer(options) {
195
349
  return search.join("&");
196
350
  };
197
351
  }
198
- //#endregion
199
- //#region src/serializers/url.ts
352
+ // src/serializers/url.ts
200
353
  /** Construct URL string from baseUrl and handle path and query params */
201
354
  function createFinalURL(pathname, options) {
202
355
  let finalURL = `${options.baseUrl}${pathname}`;
@@ -211,8 +364,7 @@ function removeTrailingSlash(url) {
211
364
  if (url.endsWith("/")) return url.substring(0, url.length - 1);
212
365
  return url;
213
366
  }
214
- //#endregion
215
- //#region src/client.ts
367
+ // src/client.ts
216
368
  function supportsRequestInitExt() {
217
369
  const nodeProcess = globalThis.process;
218
370
  return typeof nodeProcess === "object" && Number.parseInt(nodeProcess?.versions?.node?.substring(0, 2) ?? "") >= 18 && nodeProcess?.versions?.undici;
@@ -225,15 +377,16 @@ function randomID() {
225
377
  * Create an openapi-fetch client.
226
378
  */
227
379
  function createClient(clientOptions) {
228
- let { baseUrl = "", Request: CustomRequest = globalThis.Request, fetch: baseFetch = globalThis.fetch, querySerializer: globalQuerySerializer, bodySerializer: globalBodySerializer, pathSerializer: globalPathSerializer, headers: baseHeaders, requestInitExt, ...baseOptions } = { ...clientOptions };
380
+ let { baseUrl = "", Request: CustomRequest = globalThis.Request, fetch: baseFetch = globalThis.fetch, querySerializer: globalQuerySerializer, bodySerializer: globalBodySerializer, pathSerializer: globalPathSerializer, caseMapping, headers: baseHeaders, requestInitExt, ...baseOptions } = { ...clientOptions };
229
381
  requestInitExt = supportsRequestInitExt() ? requestInitExt : void 0;
230
382
  baseUrl = removeTrailingSlash(baseUrl);
231
383
  const globalMiddlewares = [];
384
+ if (caseMapping !== void 0) globalMiddlewares.push(createCaseMappingMiddleware(caseMapping));
232
385
  /**
233
386
  * Per-request fetch (keeps settings created in createClient()
234
387
  */
235
388
  async function coreFetch(schemaPath, fetchOptions) {
236
- const { baseUrl: localBaseUrl, fetch = baseFetch, Request = CustomRequest, headers, params = {}, parseAs = "json", querySerializer: requestQuerySerializer, bodySerializer = globalBodySerializer ?? defaultBodySerializer, pathSerializer: requestPathSerializer, body, middleware: requestMiddlewares = [], ...init } = fetchOptions || {};
389
+ const { baseUrl: localBaseUrl, fetch = baseFetch, Request = CustomRequest, headers, params: initialParams = {}, parseAs = "json", querySerializer: requestQuerySerializer, bodySerializer = globalBodySerializer ?? defaultBodySerializer, pathSerializer: requestPathSerializer, body: initialBody, middleware: requestMiddlewares = [], ...init } = fetchOptions || {};
237
390
  let finalBaseUrl = baseUrl;
238
391
  if (localBaseUrl) finalBaseUrl = removeTrailingSlash(localBaseUrl) ?? baseUrl;
239
392
  let querySerializer = typeof globalQuerySerializer === "function" ? globalQuerySerializer : createQuerySerializer(globalQuerySerializer);
@@ -242,6 +395,41 @@ function createClient(clientOptions) {
242
395
  ...requestQuerySerializer
243
396
  });
244
397
  const pathSerializer = requestPathSerializer || globalPathSerializer || defaultPathSerializer;
398
+ const finalMiddlewares = [...globalMiddlewares, ...requestMiddlewares];
399
+ const method = String(init.method ?? "GET").toUpperCase();
400
+ let id;
401
+ let options;
402
+ let params = initialParams;
403
+ let body = initialBody;
404
+ if (finalMiddlewares.length) {
405
+ id = randomID();
406
+ options = Object.freeze({
407
+ baseUrl: finalBaseUrl,
408
+ fetch,
409
+ parseAs,
410
+ querySerializer,
411
+ bodySerializer,
412
+ pathSerializer
413
+ });
414
+ for (const m of finalMiddlewares) if (m && typeof m === "object" && typeof m.onRequestInit === "function") {
415
+ const result = await m.onRequestInit({
416
+ schemaPath,
417
+ method,
418
+ input: {
419
+ params,
420
+ body
421
+ },
422
+ headers: mergeHeaders(baseHeaders, headers, params.header),
423
+ options,
424
+ id
425
+ });
426
+ if (result !== void 0) {
427
+ if (!result || typeof result !== "object" || !("params" in result) || !("body" in result)) throw new Error("onRequestInit: must return request input when modifying it");
428
+ params = result.params;
429
+ body = result.body;
430
+ }
431
+ }
432
+ }
245
433
  const serializedBody = body === void 0 ? void 0 : bodySerializer(body, mergeHeaders(baseHeaders, headers, params.header));
246
434
  const finalHeaders = mergeHeaders(serializedBody === void 0 || serializedBody instanceof FormData ? {} : { "Content-Type": "application/json" }, baseHeaders, headers, params.header);
247
435
  const serializedCookies = serializeCookieParams(params.cookie);
@@ -249,7 +437,6 @@ function createClient(clientOptions) {
249
437
  const existingCookie = finalHeaders.get("Cookie");
250
438
  finalHeaders.set("Cookie", existingCookie ? `${existingCookie}; ${serializedCookies}` : serializedCookies);
251
439
  }
252
- const finalMiddlewares = [...globalMiddlewares, ...requestMiddlewares];
253
440
  const requestInit = {
254
441
  redirect: "follow",
255
442
  ...baseOptions,
@@ -257,8 +444,6 @@ function createClient(clientOptions) {
257
444
  body: serializedBody,
258
445
  headers: finalHeaders
259
446
  };
260
- let id;
261
- let options;
262
447
  let request = new Request(createFinalURL(schemaPath, {
263
448
  baseUrl: finalBaseUrl,
264
449
  params,
@@ -269,15 +454,6 @@ function createClient(clientOptions) {
269
454
  /** Add custom parameters to Request object */
270
455
  for (const key in init) if (!(key in request)) request[key] = init[key];
271
456
  if (finalMiddlewares.length) {
272
- id = randomID();
273
- options = Object.freeze({
274
- baseUrl: finalBaseUrl,
275
- fetch,
276
- parseAs,
277
- querySerializer,
278
- bodySerializer,
279
- pathSerializer
280
- });
281
457
  for (const m of finalMiddlewares) if (m && typeof m === "object" && typeof m.onRequest === "function") {
282
458
  const result = await m.onRequest({
283
459
  request,
@@ -346,14 +522,15 @@ function createClient(clientOptions) {
346
522
  }
347
523
  const finalResponse = response;
348
524
  const contentLength = finalResponse.headers.get("Content-Length");
349
- if (finalResponse.status === 204 || request.method === "HEAD" || contentLength === "0" && !finalResponse.headers.get("Transfer-Encoding")?.includes("chunked")) return finalResponse.ok ? {
525
+ let parsedResult;
526
+ if (finalResponse.status === 204 || request.method === "HEAD" || contentLength === "0" && !finalResponse.headers.get("Transfer-Encoding")?.includes("chunked")) parsedResult = finalResponse.ok ? {
350
527
  data: void 0,
351
528
  response: finalResponse
352
529
  } : {
353
530
  error: void 0,
354
531
  response: finalResponse
355
532
  };
356
- if (finalResponse.ok) {
533
+ else if (finalResponse.ok) {
357
534
  const getResponseData = async () => {
358
535
  if (parseAs === "stream") return finalResponse.body;
359
536
  if (parseAs === "json" && !contentLength) {
@@ -362,24 +539,50 @@ function createClient(clientOptions) {
362
539
  }
363
540
  return await finalResponse[parseAs]();
364
541
  };
365
- return {
542
+ parsedResult = {
366
543
  data: await getResponseData(),
367
544
  response: finalResponse
368
545
  };
546
+ } else {
547
+ const raw = await finalResponse.text();
548
+ if (!raw) parsedResult = {
549
+ error: void 0,
550
+ response: finalResponse
551
+ };
552
+ else {
553
+ let error = raw;
554
+ try {
555
+ error = JSON.parse(raw);
556
+ } catch {}
557
+ parsedResult = {
558
+ error,
559
+ response: finalResponse
560
+ };
561
+ }
369
562
  }
370
- const raw = await finalResponse.text();
371
- if (!raw) return {
372
- error: void 0,
373
- response: finalResponse
374
- };
375
- let error = raw;
376
- try {
377
- error = JSON.parse(raw);
378
- } catch {}
379
- return {
380
- error,
381
- response: finalResponse
382
- };
563
+ for (let index = finalMiddlewares.length - 1; index >= 0; index--) {
564
+ const m = finalMiddlewares[index];
565
+ if (m && typeof m === "object" && typeof m.onResult === "function") {
566
+ const update = await m.onResult({
567
+ request,
568
+ schemaPath,
569
+ method,
570
+ params,
571
+ options,
572
+ id,
573
+ result: parsedResult
574
+ });
575
+ if (update !== void 0) {
576
+ if (!update || typeof update !== "object" || Object.keys(update).some((key) => key !== "data" && key !== "error")) throw new Error("onResult: must return data and/or error when modifying the result");
577
+ parsedResult = {
578
+ ...parsedResult,
579
+ ...update,
580
+ response: finalResponse
581
+ };
582
+ }
583
+ }
584
+ }
585
+ return parsedResult;
383
586
  }
384
587
  return {
385
588
  request(method, url, init) {
@@ -448,7 +651,7 @@ function createClient(clientOptions) {
448
651
  use(...middleware) {
449
652
  for (const m of middleware) {
450
653
  if (!m) continue;
451
- if (typeof m !== "object" || !("onRequest" in m || "onResponse" in m || "onError" in m)) throw new Error("Middleware must be an object with one of `onRequest()`, `onResponse() or `onError()`");
654
+ if (typeof m !== "object" || !("onRequestInit" in m || "onRequest" in m || "onResponse" in m || "onError" in m || "onResult" in m)) throw new Error("Middleware must be an object with a supported hook");
452
655
  globalMiddlewares.push(m);
453
656
  }
454
657
  },
@@ -461,8 +664,7 @@ function createClient(clientOptions) {
461
664
  }
462
665
  };
463
666
  }
464
- //#endregion
465
- //#region src/path-client.ts
667
+ // src/path-client.ts
466
668
  var PathCallForwarder = class {
467
669
  client;
468
670
  url;
@@ -522,7 +724,6 @@ function wrapAsPathBasedClient(coreClient) {
522
724
  function createPathBasedClient(clientOptions) {
523
725
  return wrapAsPathBasedClient(createClient(clientOptions));
524
726
  }
525
- //#endregion
526
727
  exports.createClient = createClient;
527
728
  exports.createFinalURL = createFinalURL;
528
729
  exports.createPathBasedClient = createPathBasedClient;