@lokalise/api-contracts 6.13.1 → 6.15.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
@@ -48,49 +48,75 @@ const deleteUser = defineApiContract({
48
48
 
49
49
  ### Non-JSON responses
50
50
 
51
- Use `textResponse` for text-based responses (plain text, CSV, HTML, XML, YAML, etc.):
51
+ Use `blobResponse` for any non-JSON response — text-based (plain text, CSV, HTML, XML, YAML, etc.) or binary (images, PDFs, etc.). It records the response `content-type` in the contract and hands the consumer a `Blob`, leaving the decode choice to the call site via `.text()`, `.arrayBuffer()`, or `.stream()`:
52
52
 
53
53
  ```ts
54
- import { defineApiContract, textResponse } from '@lokalise/api-contracts'
54
+ import { defineApiContract, blobResponse } from '@lokalise/api-contracts'
55
55
 
56
56
  const exportCsv = defineApiContract({
57
57
  method: 'get',
58
58
  pathResolver: () => '/export.csv',
59
- responsesByStatusCode: { 200: textResponse('text/csv') },
59
+ responsesByStatusCode: { 200: blobResponse('text/csv') },
60
60
  })
61
61
 
62
- const getPage = defineApiContract({
63
- method: 'get',
64
- pathResolver: () => '/page',
65
- responsesByStatusCode: { 200: textResponse('text/html') },
66
- })
67
-
68
- const getDocument = defineApiContract({
62
+ const downloadPhoto = defineApiContract({
69
63
  method: 'get',
70
- pathResolver: () => '/document',
71
- responsesByStatusCode: { 200: textResponse('application/xml') },
64
+ pathResolver: () => '/photo.png',
65
+ responsesByStatusCode: { 200: blobResponse('image/png') },
72
66
  })
73
67
  ```
74
68
 
75
- Use `blobResponse` for binary responses (images, PDFs, etc.):
69
+ > **Deprecated:** `textResponse` is deprecated in favour of `blobResponse`. Both declare the same protocol fact (the response `content-type`); they differ only in whether the client materializes the body as `string` or `Blob` — a consumer concern, not part of the contract. Where a string is needed, call `.text()` on the returned `Blob`. `textResponse` will be removed in a future major release.
70
+
71
+ ### Multiple content types
72
+
73
+ When a single status code can return more than one media type, map it to a `content` object keyed
74
+ by media type. Each value is a *body descriptor*: a bare Zod schema (JSON), `blobBody()` (opaque
75
+ binary or text), or `sseBody()` (Server-Sent Events). Add `allowNoBody: true` to also accept an
76
+ empty body.
76
77
 
77
78
  ```ts
78
- import { defineApiContract, blobResponse } from '@lokalise/api-contracts'
79
+ import { defineApiContract, blobBody, sseBody } from '@lokalise/api-contracts'
80
+ import { z } from 'zod/v4'
79
81
 
80
- const downloadPhoto = defineApiContract({
82
+ const downloadReport = defineApiContract({
81
83
  method: 'get',
82
- pathResolver: () => '/photo.png',
83
- responsesByStatusCode: { 200: blobResponse('image/png') },
84
+ pathResolver: () => '/report',
85
+ responsesByStatusCode: {
86
+ 200: {
87
+ description: 'Report in the requested format',
88
+ content: {
89
+ 'application/json': z.object({ rows: z.array(z.string()) }),
90
+ 'application/vnd.api+json': z.object({ data: z.object({ rows: z.array(z.string()) }) }),
91
+ 'text/csv': blobBody(),
92
+ 'application/pdf': blobBody(),
93
+ 'text/event-stream': sseBody({ row: z.object({ value: z.string() }) }),
94
+ },
95
+ allowNoBody: true,
96
+ },
97
+ },
84
98
  })
85
99
  ```
86
100
 
101
+ Media types are matched **exactly** — parameters stripped, case-insensitive — so distinct keys such
102
+ as `application/json` and `application/vnd.api+json` never collide, and a single status code may
103
+ expose any number of media types (including several JSON variants). The shape maps 1:1 to the
104
+ OpenAPI Response Object. The matched media type is **not** surfaced on the client response; read it
105
+ from `headers['content-type']` if you need to discriminate.
106
+
107
+ > **Deprecated:** `anyOfResponses` is deprecated in favour of content maps. A flat list can't
108
+ > declare multiple JSON media types, matches content-types by substring (false positives), and is
109
+ > order-dependent — content maps fix all three. `anyOfResponses` still works and will be removed in
110
+ > a future major release.
111
+
87
112
  ### SSE and dual-mode routes
88
113
 
89
- Use `sseResponse()` inside `responsesByStatusCode` to define SSE event schemas.
90
- For endpoints that can respond with either JSON or an SSE stream depending on the `Accept` header, use `anyOfResponses()` to declare both options on the same status code.
114
+ Use `sseResponse()` inside `responsesByStatusCode` for an SSE-only response. For a route that
115
+ returns either JSON or an SSE stream depending on the `Accept` header, use a content map (above)
116
+ with both an `application/json` and a `text/event-stream` entry.
91
117
 
92
118
  ```ts
93
- import { defineApiContract, sseResponse, anyOfResponses } from '@lokalise/api-contracts'
119
+ import { defineApiContract, sseResponse, sseBody } from '@lokalise/api-contracts'
94
120
  import { z } from 'zod/v4'
95
121
 
96
122
  // SSE-only
@@ -110,13 +136,15 @@ const chatCompletion = defineApiContract({
110
136
  pathResolver: () => '/chat/completions',
111
137
  requestBodySchema: z.object({ message: z.string() }),
112
138
  responsesByStatusCode: {
113
- 200: anyOfResponses([
114
- sseResponse({
115
- chunk: z.object({ delta: z.string() }),
116
- done: z.object({ finish_reason: z.string() }),
117
- }),
118
- z.object({ text: z.string() }),
119
- ]),
139
+ 200: {
140
+ content: {
141
+ 'application/json': z.object({ text: z.string() }),
142
+ 'text/event-stream': sseBody({
143
+ chunk: z.object({ delta: z.string() }),
144
+ done: z.object({ finish_reason: z.string() }),
145
+ }),
146
+ },
147
+ },
120
148
  },
121
149
  })
122
150
  ```
@@ -172,14 +200,7 @@ The `'2xx'` range key participates in SSE detection and success/error type narro
172
200
  All response factories accept an optional `ResponseOptions` object as their last argument.
173
201
 
174
202
  ```ts
175
- import {
176
- defineApiContract,
177
- noBodyResponse,
178
- textResponse,
179
- blobResponse,
180
- sseResponse,
181
- anyOfResponses,
182
- } from '@lokalise/api-contracts'
203
+ import { defineApiContract, noBodyResponse, blobBody, sseBody } from '@lokalise/api-contracts'
183
204
  import { z } from 'zod/v4'
184
205
 
185
206
  const contract = defineApiContract({
@@ -189,19 +210,22 @@ const contract = defineApiContract({
189
210
  responsesByStatusCode: {
190
211
  201: z.object({ id: z.string() }).describe('Created resource'),
191
212
  204: noBodyResponse({ description: 'Deleted — no content returned' }),
192
- 200: anyOfResponses(
193
- [
194
- z.object({ id: z.string() }).describe('JSON representation'),
195
- textResponse('text/csv', { description: 'CSV export' }),
196
- blobResponse('application/pdf', { description: 'PDF report' }),
197
- sseResponse({ update: z.object({ id: z.string() }) }, { description: 'Live update stream' }),
198
- ],
199
- { description: 'Multiple response formats available' },
200
- ),
213
+ 200: {
214
+ description: 'Multiple response formats available',
215
+ content: {
216
+ 'application/json': z.object({ id: z.string() }).describe('JSON representation'),
217
+ 'text/csv': blobBody(),
218
+ 'application/pdf': blobBody(),
219
+ 'text/event-stream': sseBody({ update: z.object({ id: z.string() }) }),
220
+ },
221
+ },
201
222
  },
202
223
  })
203
224
  ```
204
225
 
226
+ A content-map entry carries a single `description` for the whole response; per-media descriptions
227
+ aren't supported (a JSON descriptor can still carry its own via `.describe()`).
228
+
205
229
  `getSseSchemaByEventName(contract)` extracts SSE event schemas from a contract:
206
230
 
207
231
  ```ts
@@ -271,7 +295,7 @@ const contract = defineApiContract({
271
295
 
272
296
  ### Type utilities
273
297
 
274
- **`InferNonSseSuccessResponses<T>`** — TypeScript output type of all non-SSE 2xx responses. JSON schemas → `z.output<T>`, `textResponse` → `string`, `blobResponse` → `Blob`, `ContractNoBody`/`NoBodyResponse` → `undefined`, `sseResponse` → `never` (excluded). `anyOfResponses` entries are unpacked before mapping.
298
+ **`InferNonSseSuccessResponses<T>`** — TypeScript output type of all non-SSE 2xx responses. JSON schemas → `z.output<T>`, `textResponse` → `string`, `blobResponse` → `Blob`, `ContractNoBody`/`NoBodyResponse` → `undefined`, `sseResponse` → `never` (excluded). `anyOfResponses` and content-map entries are unpacked before mapping.
275
299
 
276
300
  ```ts
277
301
  import type { InferNonSseSuccessResponses } from '@lokalise/api-contracts'
@@ -280,7 +304,7 @@ type UserResponse = InferNonSseSuccessResponses<typeof getUser['responsesByStatu
280
304
  // { id: string; name: string }
281
305
 
282
306
  type CsvResponse = InferNonSseSuccessResponses<typeof exportCsv['responsesByStatusCode']>
283
- // string
307
+ // Blob
284
308
  ```
285
309
 
286
310
  **`InferJsonSuccessResponses<T>`** — union of Zod schema types for all JSON 2xx entries. Text, Blob, SSE, and `ContractNoBody` entries are excluded.
@@ -375,7 +399,7 @@ getIsEmptyResponseExpected(deleteUser) // true
375
399
  getIsEmptyResponseExpected(getUser) // false
376
400
  ```
377
401
 
378
- **`hasAnySuccessSseResponse`** — `true` when any 2xx entry (exact code or `'2xx'` range key) is an SSE response (including inside `anyOfResponses`).
402
+ **`hasAnySuccessSseResponse`** — `true` when any 2xx entry (exact code or `'2xx'` range key) is an SSE response (including inside `anyOfResponses` or a content map).
379
403
 
380
404
  ```ts
381
405
  import { hasAnySuccessSseResponse } from '@lokalise/api-contracts'
@@ -50,40 +50,61 @@ type SseInferClientResponseBody<T> = Extract<InferClientResponseBody<T>, AsyncIt
50
50
  * Like InferClientResponseBody but returns only non-SSE bodies — SSE entries resolve to never.
51
51
  */
52
52
  type NonSseInferClientResponseBody<T> = Exclude<InferClientResponseBody<T>, AsyncIterable<unknown>>;
53
- type WildcardSseBody<V, K extends WildcardStatusCodeKey> = K extends '2xx' ? SseInferClientResponseBody<V> : InferClientResponseBody<V>;
54
- type WildcardNonSseBody<V, K extends WildcardStatusCodeKey> = K extends '2xx' ? NonSseInferClientResponseBody<V> : InferClientResponseBody<V>;
53
+ type InferContentDescriptorBody<TDescriptor> = TDescriptor extends {
54
+ _tag: 'BlobBody';
55
+ } ? Blob : TDescriptor extends {
56
+ _tag: 'SseBody';
57
+ schemaByEventName: infer S extends SseSchemaByEventName;
58
+ } ? AsyncIterable<SseEventOf<S>> : TDescriptor extends z.ZodType ? InferSchemaOutput<TDescriptor> : never;
59
+ type ContentEntryVariants<TEntry> = (TEntry extends {
60
+ content: infer C;
61
+ } ? {
62
+ [CT in keyof C & string]: {
63
+ body: InferContentDescriptorBody<C[CT]>;
64
+ };
65
+ }[keyof C & string] : never) | (TEntry extends {
66
+ allowNoBody: true;
67
+ } ? {
68
+ body: null;
69
+ } : never);
70
+ type IsContentEntry<V> = V extends {
71
+ content: object;
72
+ } ? true : V extends {
73
+ allowNoBody: true;
74
+ } ? true : false;
75
+ type SseContentVariants<TEntry> = Extract<ContentEntryVariants<TEntry>, {
76
+ body: AsyncIterable<unknown>;
77
+ }>;
78
+ type NonSseContentVariants<TEntry> = Exclude<ContentEntryVariants<TEntry>, {
79
+ body: AsyncIterable<unknown>;
80
+ }>;
81
+ /** Response mode for a given status class: success codes filter by SSE/non-SSE; others pass all. */
82
+ type ResponseBodyMode = 'sse' | 'non-sse' | 'all';
83
+ type ContentVariantsForMode<TEntry, TMode extends ResponseBodyMode> = TMode extends 'sse' ? SseContentVariants<TEntry> : TMode extends 'non-sse' ? NonSseContentVariants<TEntry> : ContentEntryVariants<TEntry>;
84
+ type LegacyBodyForMode<V, TMode extends ResponseBodyMode> = TMode extends 'sse' ? SseInferClientResponseBody<V> : TMode extends 'non-sse' ? NonSseInferClientResponseBody<V> : InferClientResponseBody<V>;
85
+ /** Attaches `statusCode` + `headers` to each `{ body }` variant. */
86
+ type WithMeta<TStatusCode, THeaders, TVariant> = TVariant extends unknown ? Prettify<{
87
+ statusCode: TStatusCode;
88
+ headers: THeaders;
89
+ } & TVariant> : never;
90
+ /**
91
+ * Builds the response union member(s) for a status code `K` holding value `V`.
92
+ * Content-map entries expand to one body variant per media type; legacy entries keep the original
93
+ * `{ statusCode, headers, body }` shape (so existing contracts are byte-for-byte unchanged).
94
+ */
95
+ type ResponseMember<TStatusCode, THeaders, V, TMode extends ResponseBodyMode> = IsContentEntry<V> extends true ? WithMeta<TStatusCode, THeaders, ContentVariantsForMode<V, TMode>> : {
96
+ statusCode: TStatusCode;
97
+ headers: THeaders;
98
+ body: LegacyBodyForMode<V, TMode>;
99
+ };
55
100
  type ExactStatusCodes<TApiContract extends ApiContract> = keyof TApiContract['responsesByStatusCode'] & HttpStatusCode;
56
101
  type RangeStatusCodes<TApiContract extends ApiContract> = {
57
102
  [K in keyof TApiContract['responsesByStatusCode'] & HttpStatusCodeRange]: ExpandStatusRangeKey<K>;
58
103
  }[keyof TApiContract['responsesByStatusCode'] & HttpStatusCodeRange];
59
104
  type DefaultSuccessStatusCodes<TApiContract extends ApiContract> = Exclude<SuccessfulHttpStatusCode, ExactStatusCodes<TApiContract> | RangeStatusCodes<TApiContract>>;
60
105
  type DefaultNonSuccessStatusCodes<TApiContract extends ApiContract> = Exclude<Exclude<HttpStatusCode, SuccessfulHttpStatusCode>, ExactStatusCodes<TApiContract> | RangeStatusCodes<TApiContract>>;
61
- type WildcardSseEntry<TApiContract extends ApiContract, K extends WildcardStatusCodeKey> = K extends 'default' ? {
62
- statusCode: DefaultSuccessStatusCodes<TApiContract>;
63
- headers: InferClientResponseHeaders<TApiContract>;
64
- body: SseInferClientResponseBody<NonNullable<TApiContract['responsesByStatusCode'][K]>>;
65
- } | {
66
- statusCode: DefaultNonSuccessStatusCodes<TApiContract>;
67
- headers: InferClientResponseHeaders<TApiContract>;
68
- body: InferClientResponseBody<NonNullable<TApiContract['responsesByStatusCode'][K]>>;
69
- } : {
70
- statusCode: Exclude<ExpandStatusRangeKey<K>, ExactStatusCodes<TApiContract>>;
71
- headers: InferClientResponseHeaders<TApiContract>;
72
- body: WildcardSseBody<NonNullable<TApiContract['responsesByStatusCode'][K]>, K>;
73
- };
74
- type WildcardNonSseEntry<TApiContract extends ApiContract, K extends WildcardStatusCodeKey> = K extends 'default' ? {
75
- statusCode: DefaultSuccessStatusCodes<TApiContract>;
76
- headers: InferClientResponseHeaders<TApiContract>;
77
- body: NonSseInferClientResponseBody<NonNullable<TApiContract['responsesByStatusCode'][K]>>;
78
- } | {
79
- statusCode: DefaultNonSuccessStatusCodes<TApiContract>;
80
- headers: InferClientResponseHeaders<TApiContract>;
81
- body: InferClientResponseBody<NonNullable<TApiContract['responsesByStatusCode'][K]>>;
82
- } : {
83
- statusCode: Exclude<ExpandStatusRangeKey<K>, ExactStatusCodes<TApiContract>>;
84
- headers: InferClientResponseHeaders<TApiContract>;
85
- body: WildcardNonSseBody<NonNullable<TApiContract['responsesByStatusCode'][K]>, K>;
86
- };
106
+ type WildcardSseEntry<TApiContract extends ApiContract, K extends WildcardStatusCodeKey> = K extends 'default' ? ResponseMember<DefaultSuccessStatusCodes<TApiContract>, InferClientResponseHeaders<TApiContract>, NonNullable<TApiContract['responsesByStatusCode'][K]>, 'sse'> | ResponseMember<DefaultNonSuccessStatusCodes<TApiContract>, InferClientResponseHeaders<TApiContract>, NonNullable<TApiContract['responsesByStatusCode'][K]>, 'all'> : ResponseMember<Exclude<ExpandStatusRangeKey<K>, ExactStatusCodes<TApiContract>>, InferClientResponseHeaders<TApiContract>, NonNullable<TApiContract['responsesByStatusCode'][K]>, K extends '2xx' ? 'sse' : 'all'>;
107
+ type WildcardNonSseEntry<TApiContract extends ApiContract, K extends WildcardStatusCodeKey> = K extends 'default' ? ResponseMember<DefaultSuccessStatusCodes<TApiContract>, InferClientResponseHeaders<TApiContract>, NonNullable<TApiContract['responsesByStatusCode'][K]>, 'non-sse'> | ResponseMember<DefaultNonSuccessStatusCodes<TApiContract>, InferClientResponseHeaders<TApiContract>, NonNullable<TApiContract['responsesByStatusCode'][K]>, 'all'> : ResponseMember<Exclude<ExpandStatusRangeKey<K>, ExactStatusCodes<TApiContract>>, InferClientResponseHeaders<TApiContract>, NonNullable<TApiContract['responsesByStatusCode'][K]>, K extends '2xx' ? 'non-sse' : 'all'>;
87
108
  /**
88
109
  * Infers a discriminated union of `{ statusCode, headers, body }` for SSE mode:
89
110
  * - exact success status codes and `'2xx'` range → SSE body only (AsyncIterable)
@@ -96,11 +117,7 @@ type WildcardNonSseEntry<TApiContract extends ApiContract, K extends WildcardSta
96
117
  * are strongly typed; all other headers remain accessible as `string | undefined`.
97
118
  */
98
119
  export type InferSseClientResponse<TApiContract extends ApiContract> = {
99
- [K in keyof TApiContract['responsesByStatusCode'] & HttpStatusCode]: {
100
- statusCode: K;
101
- headers: InferClientResponseHeaders<TApiContract>;
102
- body: K extends SuccessfulHttpStatusCode ? SseInferClientResponseBody<NonNullable<TApiContract['responsesByStatusCode'][K]>> : InferClientResponseBody<NonNullable<TApiContract['responsesByStatusCode'][K]>>;
103
- };
120
+ [K in keyof TApiContract['responsesByStatusCode'] & HttpStatusCode]: ResponseMember<K, InferClientResponseHeaders<TApiContract>, NonNullable<TApiContract['responsesByStatusCode'][K]>, K extends SuccessfulHttpStatusCode ? 'sse' : 'all'>;
104
121
  }[keyof TApiContract['responsesByStatusCode'] & HttpStatusCode] | {
105
122
  [K in keyof TApiContract['responsesByStatusCode'] & WildcardStatusCodeKey]: WildcardSseEntry<TApiContract, K>;
106
123
  }[keyof TApiContract['responsesByStatusCode'] & WildcardStatusCodeKey];
@@ -116,11 +133,7 @@ export type InferSseClientResponse<TApiContract extends ApiContract> = {
116
133
  * are strongly typed; all other headers remain accessible as `string | undefined`.
117
134
  */
118
135
  export type InferNonSseClientResponse<TApiContract extends ApiContract> = {
119
- [K in keyof TApiContract['responsesByStatusCode'] & HttpStatusCode]: {
120
- statusCode: K;
121
- headers: InferClientResponseHeaders<TApiContract>;
122
- body: K extends SuccessfulHttpStatusCode ? NonSseInferClientResponseBody<NonNullable<TApiContract['responsesByStatusCode'][K]>> : InferClientResponseBody<NonNullable<TApiContract['responsesByStatusCode'][K]>>;
123
- };
136
+ [K in keyof TApiContract['responsesByStatusCode'] & HttpStatusCode]: ResponseMember<K, InferClientResponseHeaders<TApiContract>, NonNullable<TApiContract['responsesByStatusCode'][K]>, K extends SuccessfulHttpStatusCode ? 'non-sse' : 'all'>;
124
137
  }[keyof TApiContract['responsesByStatusCode'] & HttpStatusCode] | {
125
138
  [K in keyof TApiContract['responsesByStatusCode'] & WildcardStatusCodeKey]: WildcardNonSseEntry<TApiContract, K>;
126
139
  }[keyof TApiContract['responsesByStatusCode'] & WildcardStatusCodeKey];
@@ -9,6 +9,13 @@ export type TypedTextResponse = {
9
9
  readonly contentType: string;
10
10
  readonly description?: string;
11
11
  };
12
+ /**
13
+ * @deprecated Use {@link blobResponse} instead. `textResponse` and `blobResponse` carry the
14
+ * identical protocol fact (the response `content-type`); they differ only in the JS type the
15
+ * client materializes the body into (`string` vs `Blob`). That decode choice belongs to the
16
+ * consumer, not the shared contract — `blobResponse` defers it to the call site via
17
+ * `.text()` / `.arrayBuffer()` / `.stream()`. Will be removed in a future major release.
18
+ */
12
19
  export declare const textResponse: (contentType: string, options?: ResponseOptions) => TypedTextResponse;
13
20
  export declare const isTextResponse: (value: ApiContractResponse) => value is TypedTextResponse;
14
21
  export type TypedBlobResponse = {
@@ -34,6 +41,13 @@ export type AnyOfResponses<T extends TypedApiContractResponse = TypedApiContract
34
41
  readonly responses: T[];
35
42
  readonly description?: string;
36
43
  };
44
+ /**
45
+ * @deprecated Use a content-map response entry instead — `{ content: { <mediaType>: descriptor } }`
46
+ * (see {@link blobBody} / {@link sseBody}). A flat `anyOfResponses` list can't declare multiple JSON
47
+ * media types, matches content-types by substring (false positives), and is order-dependent;
48
+ * content maps match media types exactly and map 1:1 to the OpenAPI Response Object. Will be removed
49
+ * in a future major release.
50
+ */
37
51
  export declare const anyOfResponses: <T extends TypedApiContractResponse>(responses: T[], options?: ResponseOptions) => AnyOfResponses<T>;
38
52
  export declare const isAnyOfResponses: (value: ApiContractResponse) => value is AnyOfResponses;
39
53
  export type NoBodyResponse = {
@@ -43,7 +57,47 @@ export type NoBodyResponse = {
43
57
  export declare const noBodyResponse: (options?: ResponseOptions) => NoBodyResponse;
44
58
  export declare const isNoBodyResponse: (value: ApiContractResponse) => value is NoBodyResponse;
45
59
  export type ApiContractResponse = typeof ContractNoBody | NoBodyResponse | TypedApiContractResponse | AnyOfResponses;
46
- export type ResponsesByStatusCode = Partial<Record<HttpStatusCode | WildcardStatusCodeKey, ApiContractResponse>>;
60
+ /** Opaque binary body; the media type is supplied by the content-map key. */
61
+ export type BlobBody = {
62
+ readonly _tag: 'BlobBody';
63
+ };
64
+ export declare const blobBody: () => BlobBody;
65
+ export declare const isBlobBody: (value: BodyDescriptor) => value is BlobBody;
66
+ /** Server-Sent Events body; the media type is supplied by the content-map key. */
67
+ export type SseBody<T extends SseSchemaByEventName = SseSchemaByEventName> = {
68
+ readonly _tag: 'SseBody';
69
+ readonly schemaByEventName: T;
70
+ };
71
+ export declare const sseBody: <T extends SseSchemaByEventName>(schemaByEventName: T) => SseBody<T>;
72
+ export declare const isSseBody: (value: BodyDescriptor) => value is SseBody;
73
+ export declare const isJsonBody: (value: BodyDescriptor) => value is z.ZodType;
74
+ /**
75
+ * A value in a {@link ResponseContentMap}; the media type is the map key, so a
76
+ * descriptor never carries a content type itself. A bare Zod schema is JSON.
77
+ */
78
+ export type BodyDescriptor = z.ZodType | BlobBody | SseBody;
79
+ /** Maps a response media type (e.g. `application/json`) to the body it carries. */
80
+ export type ResponseContentMap = Record<string, BodyDescriptor>;
81
+ /** A content-map response carrying a body for one or more media types. */
82
+ export type BodyContentResponseEntry = {
83
+ readonly description?: string;
84
+ readonly content: ResponseContentMap;
85
+ readonly allowNoBody?: boolean;
86
+ };
87
+ /** A content-map response that never carries a body. */
88
+ export type NoBodyContentResponseEntry = {
89
+ readonly description?: string;
90
+ readonly content?: never;
91
+ readonly allowNoBody: true;
92
+ };
93
+ /**
94
+ * A content-map response entry. Either a body response (`content` required,
95
+ * optionally `allowNoBody`) or a no-body response (`allowNoBody: true`, no
96
+ * `content`). The union forces at least one of `content` / `allowNoBody`.
97
+ */
98
+ export type ResponseEntry = BodyContentResponseEntry | NoBodyContentResponseEntry;
99
+ export declare const isContentResponseEntry: (value: ApiContractResponse | ResponseEntry) => value is ResponseEntry;
100
+ export type ResponsesByStatusCode = Partial<Record<HttpStatusCode | WildcardStatusCodeKey, ApiContractResponse | ResponseEntry>>;
47
101
  export type ResponseKind = {
48
102
  kind: 'noContent';
49
103
  } | {
@@ -73,7 +127,7 @@ export type ResponseKind = {
73
127
  * returning `null` — only applies to single-entry responses; `anyOfResponses` always requires a
74
128
  * content-type to disambiguate regardless of this flag.
75
129
  */
76
- export declare const resolveContractResponse: (schemaEntry: ApiContractResponse, contentType: string | undefined, strict?: boolean) => ResponseKind | null;
130
+ export declare const resolveContractResponse: (schemaEntry: ApiContractResponse | ResponseEntry, contentType: string | undefined, strict?: boolean) => ResponseKind | null;
77
131
  /**
78
132
  * Combines status-code lookup and content-type resolution into a single call.
79
133
  * Lookup precedence: exact code → range key (e.g. `'4xx'`) → `'default'`.
@@ -1,4 +1,11 @@
1
1
  import { ContractNoBody } from "./constants.js";
2
+ /**
3
+ * @deprecated Use {@link blobResponse} instead. `textResponse` and `blobResponse` carry the
4
+ * identical protocol fact (the response `content-type`); they differ only in the JS type the
5
+ * client materializes the body into (`string` vs `Blob`). That decode choice belongs to the
6
+ * consumer, not the shared contract — `blobResponse` defers it to the call site via
7
+ * `.text()` / `.arrayBuffer()` / `.stream()`. Will be removed in a future major release.
8
+ */
2
9
  export const textResponse = (contentType, options) => ({
3
10
  _tag: 'TextResponse',
4
11
  contentType,
@@ -18,6 +25,13 @@ export const sseResponse = (schemaByEventName, options) => ({
18
25
  });
19
26
  export const isSseResponse = (value) => typeof value === 'object' && value !== null && '_tag' in value && value._tag === 'SseResponse';
20
27
  export const isJsonResponse = (value) => typeof value === 'object' && value !== null && !('_tag' in value);
28
+ /**
29
+ * @deprecated Use a content-map response entry instead — `{ content: { <mediaType>: descriptor } }`
30
+ * (see {@link blobBody} / {@link sseBody}). A flat `anyOfResponses` list can't declare multiple JSON
31
+ * media types, matches content-types by substring (false positives), and is order-dependent;
32
+ * content maps match media types exactly and map 1:1 to the OpenAPI Response Object. Will be removed
33
+ * in a future major release.
34
+ */
21
35
  export const anyOfResponses = (responses, options) => ({
22
36
  _tag: 'AnyOfResponses',
23
37
  responses,
@@ -29,6 +43,18 @@ export const noBodyResponse = (options) => ({
29
43
  ...(options?.description !== undefined && { description: options.description }),
30
44
  });
31
45
  export const isNoBodyResponse = (value) => typeof value === 'object' && value !== null && '_tag' in value && value._tag === 'NoBodyResponse';
46
+ export const blobBody = () => ({ _tag: 'BlobBody' });
47
+ export const isBlobBody = (value) => typeof value === 'object' && value !== null && '_tag' in value && value._tag === 'BlobBody';
48
+ export const sseBody = (schemaByEventName) => ({
49
+ _tag: 'SseBody',
50
+ schemaByEventName,
51
+ });
52
+ export const isSseBody = (value) => typeof value === 'object' && value !== null && '_tag' in value && value._tag === 'SseBody';
53
+ export const isJsonBody = (value) => typeof value === 'object' && value !== null && !('_tag' in value);
54
+ export const isContentResponseEntry = (value) => typeof value === 'object' &&
55
+ value !== null &&
56
+ !('_tag' in value) &&
57
+ ('content' in value || 'allowNoBody' in value);
32
58
  const matchTypedResponse = (entry, contentType) => {
33
59
  if (isTextResponse(entry)) {
34
60
  return contentType.includes(entry.contentType) ? { kind: 'text' } : null;
@@ -58,6 +84,60 @@ const resolveByKind = (entry) => {
58
84
  }
59
85
  return { kind: 'json', schema: entry };
60
86
  };
87
+ const normalizeMediaType = (contentType) => (contentType.split(';')[0] ?? contentType).trim().toLowerCase();
88
+ const descriptorToKind = (descriptor) => {
89
+ if (isBlobBody(descriptor)) {
90
+ return { kind: 'blob' };
91
+ }
92
+ if (isSseBody(descriptor)) {
93
+ return { kind: 'sse', schemaByEventName: descriptor.schemaByEventName };
94
+ }
95
+ return { kind: 'json', schema: descriptor };
96
+ };
97
+ /**
98
+ * Resolves a content-map {@link ResponseEntry}. Body media types are matched by exact
99
+ * (parameter-stripped, case-insensitive) content-type equality, so e.g. `application/json`
100
+ * and `application/json+01` are kept distinct.
101
+ */
102
+ const resolveContentEntry = (entry, contentType, strict) => {
103
+ if (!entry.content) {
104
+ return { kind: 'noContent' };
105
+ }
106
+ const entries = Object.entries(entry.content);
107
+ if (!contentType) {
108
+ if (entry.allowNoBody) {
109
+ return { kind: 'noContent' };
110
+ }
111
+ }
112
+ else {
113
+ const target = normalizeMediaType(contentType);
114
+ for (const [mediaType, descriptor] of entries) {
115
+ if (normalizeMediaType(mediaType) === target) {
116
+ return descriptorToKind(descriptor);
117
+ }
118
+ }
119
+ }
120
+ // No content-type (without allowNoBody), or no media type matched: in non-strict mode fall
121
+ // back to the sole descriptor when the entry declares exactly one.
122
+ const onlyDescriptor = entries.length === 1 ? entries[0]?.[1] : undefined;
123
+ return !strict && onlyDescriptor ? descriptorToKind(onlyDescriptor) : null;
124
+ };
125
+ /**
126
+ * Resolves a legacy `anyOfResponses` entry. It always requires a content-type to disambiguate,
127
+ * so `strict` has no effect here.
128
+ */
129
+ const resolveAnyOf = (schemaEntry, contentType) => {
130
+ if (!contentType) {
131
+ return null;
132
+ }
133
+ for (const item of schemaEntry.responses) {
134
+ const resolved = matchTypedResponse(item, contentType);
135
+ if (resolved) {
136
+ return resolved;
137
+ }
138
+ }
139
+ return null;
140
+ };
61
141
  /**
62
142
  * Resolves a contract's response entry for a given status code into a concrete `ResponseKind`,
63
143
  * taking the response `content-type` into account.
@@ -75,21 +155,14 @@ const resolveByKind = (entry) => {
75
155
  * content-type to disambiguate regardless of this flag.
76
156
  */
77
157
  export const resolveContractResponse = (schemaEntry, contentType, strict = true) => {
158
+ if (isContentResponseEntry(schemaEntry)) {
159
+ return resolveContentEntry(schemaEntry, contentType, strict);
160
+ }
78
161
  if (schemaEntry === ContractNoBody || isNoBodyResponse(schemaEntry)) {
79
162
  return { kind: 'noContent' };
80
163
  }
81
164
  if (isAnyOfResponses(schemaEntry)) {
82
- // AnyOfResponses always requires content-type to disambiguate — strict mode has no effect here
83
- if (!contentType) {
84
- return null;
85
- }
86
- for (const item of schemaEntry.responses) {
87
- const resolved = matchTypedResponse(item, contentType);
88
- if (resolved) {
89
- return resolved;
90
- }
91
- }
92
- return null;
165
+ return resolveAnyOf(schemaEntry, contentType);
93
166
  }
94
167
  if (!contentType) {
95
168
  return strict ? null : resolveByKind(schemaEntry);
@@ -1 +1 @@
1
- {"version":3,"file":"contractResponse.js","sourceRoot":"","sources":["../../src/new/contractResponse.ts"],"names":[],"mappings":"AAMA,OAAO,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAA;AAY/C,MAAM,CAAC,MAAM,YAAY,GAAG,CAC1B,WAAmB,EACnB,OAAyB,EACN,EAAE,CAAC,CAAC;IACvB,IAAI,EAAE,cAAc;IACpB,WAAW;IACX,GAAG,CAAC,OAAO,EAAE,WAAW,KAAK,SAAS,IAAI,EAAE,WAAW,EAAE,OAAO,CAAC,WAAW,EAAE,CAAC;CAChF,CAAC,CAAA;AAEF,MAAM,CAAC,MAAM,cAAc,GAAG,CAAC,KAA0B,EAA8B,EAAE,CACvF,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,KAAK,IAAI,IAAI,MAAM,IAAI,KAAK,IAAI,KAAK,CAAC,IAAI,KAAK,cAAc,CAAA;AAQjG,MAAM,CAAC,MAAM,YAAY,GAAG,CAC1B,WAAmB,EACnB,OAAyB,EACN,EAAE,CAAC,CAAC;IACvB,IAAI,EAAE,cAAc;IACpB,WAAW;IACX,GAAG,CAAC,OAAO,EAAE,WAAW,KAAK,SAAS,IAAI,EAAE,WAAW,EAAE,OAAO,CAAC,WAAW,EAAE,CAAC;CAChF,CAAC,CAAA;AAEF,MAAM,CAAC,MAAM,cAAc,GAAG,CAAC,KAA0B,EAA8B,EAAE,CACvF,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,KAAK,IAAI,IAAI,MAAM,IAAI,KAAK,IAAI,KAAK,CAAC,IAAI,KAAK,cAAc,CAAA;AAUjG,MAAM,CAAC,MAAM,WAAW,GAAG,CACzB,iBAAoB,EACpB,OAAyB,EACJ,EAAE,CAAC,CAAC;IACzB,IAAI,EAAE,aAAa;IACnB,iBAAiB;IACjB,GAAG,CAAC,OAAO,EAAE,WAAW,KAAK,SAAS,IAAI,EAAE,WAAW,EAAE,OAAO,CAAC,WAAW,EAAE,CAAC;CAChF,CAAC,CAAA;AAEF,MAAM,CAAC,MAAM,aAAa,GAAG,CAAC,KAA0B,EAA6B,EAAE,CACrF,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,KAAK,IAAI,IAAI,MAAM,IAAI,KAAK,IAAI,KAAK,CAAC,IAAI,KAAK,aAAa,CAAA;AAIhG,MAAM,CAAC,MAAM,cAAc,GAAG,CAAC,KAA0B,EAA8B,EAAE,CACvF,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,KAAK,IAAI,IAAI,CAAC,CAAC,MAAM,IAAI,KAAK,CAAC,CAAA;AAcnE,MAAM,CAAC,MAAM,cAAc,GAAG,CAC5B,SAAc,EACd,OAAyB,EACN,EAAE,CAAC,CAAC;IACvB,IAAI,EAAE,gBAAgB;IACtB,SAAS;IACT,GAAG,CAAC,OAAO,EAAE,WAAW,KAAK,SAAS,IAAI,EAAE,WAAW,EAAE,OAAO,CAAC,WAAW,EAAE,CAAC;CAChF,CAAC,CAAA;AAEF,MAAM,CAAC,MAAM,gBAAgB,GAAG,CAAC,KAA0B,EAA2B,EAAE,CACtF,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,KAAK,IAAI,IAAI,MAAM,IAAI,KAAK,IAAI,KAAK,CAAC,IAAI,KAAK,gBAAgB,CAAA;AAOnG,MAAM,CAAC,MAAM,cAAc,GAAG,CAAC,OAAyB,EAAkB,EAAE,CAAC,CAAC;IAC5E,IAAI,EAAE,gBAAgB;IACtB,GAAG,CAAC,OAAO,EAAE,WAAW,KAAK,SAAS,IAAI,EAAE,WAAW,EAAE,OAAO,CAAC,WAAW,EAAE,CAAC;CAChF,CAAC,CAAA;AAEF,MAAM,CAAC,MAAM,gBAAgB,GAAG,CAAC,KAA0B,EAA2B,EAAE,CACtF,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,KAAK,IAAI,IAAI,MAAM,IAAI,KAAK,IAAI,KAAK,CAAC,IAAI,KAAK,gBAAgB,CAAA;AAmBnG,MAAM,kBAAkB,GAAG,CACzB,KAA+B,EAC/B,WAAmB,EACE,EAAE;IACvB,IAAI,cAAc,CAAC,KAAK,CAAC,EAAE,CAAC;QAC1B,OAAO,WAAW,CAAC,QAAQ,CAAC,KAAK,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC,CAAC,CAAC,IAAI,CAAA;IAC1E,CAAC;IAED,IAAI,cAAc,CAAC,KAAK,CAAC,EAAE,CAAC;QAC1B,OAAO,WAAW,CAAC,QAAQ,CAAC,KAAK,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC,CAAC,CAAC,IAAI,CAAA;IAC1E,CAAC;IAED,IAAI,aAAa,CAAC,KAAK,CAAC,EAAE,CAAC;QACzB,OAAO,WAAW,CAAC,QAAQ,CAAC,mBAAmB,CAAC;YAC9C,CAAC,CAAC,EAAE,IAAI,EAAE,KAAK,EAAE,iBAAiB,EAAE,KAAK,CAAC,iBAAiB,EAAE;YAC7D,CAAC,CAAC,IAAI,CAAA;IACV,CAAC;IAED,IAAI,WAAW,CAAC,QAAQ,CAAC,kBAAkB,CAAC,EAAE,CAAC;QAC7C,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,CAAA;IACxC,CAAC;IAED,OAAO,IAAI,CAAA;AACb,CAAC,CAAA;AAED,MAAM,aAAa,GAAG,CAAC,KAA+B,EAAgB,EAAE;IACtE,IAAI,cAAc,CAAC,KAAK,CAAC,EAAE,CAAC;QAC1B,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,CAAA;IACzB,CAAC;IACD,IAAI,cAAc,CAAC,KAAK,CAAC,EAAE,CAAC;QAC1B,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,CAAA;IACzB,CAAC;IACD,IAAI,aAAa,CAAC,KAAK,CAAC,EAAE,CAAC;QACzB,OAAO,EAAE,IAAI,EAAE,KAAK,EAAE,iBAAiB,EAAE,KAAK,CAAC,iBAAiB,EAAE,CAAA;IACpE,CAAC;IACD,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,CAAA;AACxC,CAAC,CAAA;AAED;;;;;;;;;;;;;;;GAeG;AACH,MAAM,CAAC,MAAM,uBAAuB,GAAG,CACrC,WAAgC,EAChC,WAA+B,EAC/B,MAAM,GAAG,IAAI,EACQ,EAAE;IACvB,IAAI,WAAW,KAAK,cAAc,IAAI,gBAAgB,CAAC,WAAW,CAAC,EAAE,CAAC;QACpE,OAAO,EAAE,IAAI,EAAE,WAAW,EAAE,CAAA;IAC9B,CAAC;IAED,IAAI,gBAAgB,CAAC,WAAW,CAAC,EAAE,CAAC;QAClC,+FAA+F;QAC/F,IAAI,CAAC,WAAW,EAAE,CAAC;YACjB,OAAO,IAAI,CAAA;QACb,CAAC;QAED,KAAK,MAAM,IAAI,IAAI,WAAW,CAAC,SAAS,EAAE,CAAC;YACzC,MAAM,QAAQ,GAAG,kBAAkB,CAAC,IAAI,EAAE,WAAW,CAAC,CAAA;YACtD,IAAI,QAAQ,EAAE,CAAC;gBACb,OAAO,QAAQ,CAAA;YACjB,CAAC;QACH,CAAC;QACD,OAAO,IAAI,CAAA;IACb,CAAC;IAED,IAAI,CAAC,WAAW,EAAE,CAAC;QACjB,OAAO,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,aAAa,CAAC,WAAW,CAAC,CAAA;IACnD,CAAC;IAED,MAAM,OAAO,GAAG,kBAAkB,CAAC,WAAW,EAAE,WAAW,CAAC,CAAA;IAE5D,OAAO,OAAO,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,aAAa,CAAC,WAAW,CAAC,CAAC,CAAA;AAChE,CAAC,CAAA;AAED,SAAS,WAAW,CAAC,UAAkB;IACrC,IAAI,UAAU,IAAI,GAAG,IAAI,UAAU,GAAG,GAAG;QAAE,OAAO,KAAK,CAAA;IACvD,IAAI,UAAU,IAAI,GAAG,IAAI,UAAU,GAAG,GAAG;QAAE,OAAO,KAAK,CAAA;IACvD,IAAI,UAAU,IAAI,GAAG,IAAI,UAAU,GAAG,GAAG;QAAE,OAAO,KAAK,CAAA;IACvD,IAAI,UAAU,IAAI,GAAG,IAAI,UAAU,GAAG,GAAG;QAAE,OAAO,KAAK,CAAA;IACvD,IAAI,UAAU,IAAI,GAAG,IAAI,UAAU,GAAG,GAAG;QAAE,OAAO,KAAK,CAAA;IACvD,OAAO,IAAI,CAAA;AACb,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,oBAAoB,CAClC,qBAA4C,EAC5C,UAAkB,EAClB,WAA+B,EAC/B,iBAA0B;IAE1B,MAAM,UAAU,GAAG,qBAAqB,CAAC,UAA4B,CAAC,CAAA;IACtE,IAAI,UAAU,EAAE,CAAC;QACf,OAAO,uBAAuB,CAAC,UAAU,EAAE,WAAW,EAAE,iBAAiB,CAAC,CAAA;IAC5E,CAAC;IAED,MAAM,QAAQ,GAAG,WAAW,CAAC,UAAU,CAAC,CAAA;IACxC,IAAI,QAAQ,EAAE,CAAC;QACb,MAAM,UAAU,GAAG,qBAAqB,CAAC,QAAQ,CAAC,CAAA;QAClD,IAAI,UAAU,EAAE,CAAC;YACf,OAAO,uBAAuB,CAAC,UAAU,EAAE,WAAW,EAAE,iBAAiB,CAAC,CAAA;QAC5E,CAAC;IACH,CAAC;IAED,MAAM,YAAY,GAAG,qBAAqB,CAAC,OAAO,CAAA;IAClD,IAAI,YAAY,EAAE,CAAC;QACjB,OAAO,uBAAuB,CAAC,YAAY,EAAE,WAAW,EAAE,iBAAiB,CAAC,CAAA;IAC9E,CAAC;IAED,OAAO,IAAI,CAAA;AACb,CAAC"}
1
+ {"version":3,"file":"contractResponse.js","sourceRoot":"","sources":["../../src/new/contractResponse.ts"],"names":[],"mappings":"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"}
@@ -1,7 +1,7 @@
1
1
  import { z } from 'zod/v4';
2
2
  import { SUCCESSFUL_HTTP_STATUS_CODES } from "../HttpStatusCodes.js";
3
3
  import { ContractNoBody } from "./constants.js";
4
- import { isAnyOfResponses, isBlobResponse, isNoBodyResponse, isSseResponse, isTextResponse, } from "./contractResponse.js";
4
+ import { isAnyOfResponses, isBlobResponse, isContentResponseEntry, isJsonBody, isNoBodyResponse, isSseBody, isSseResponse, isTextResponse, } from "./contractResponse.js";
5
5
  export const defineApiContract = (contract) => contract;
6
6
  export const mapApiContractToPath = (routeConfig) => {
7
7
  if (!routeConfig.requestPathParamsSchema) {
@@ -16,17 +16,31 @@ export const mapApiContractToPath = (routeConfig) => {
16
16
  export const describeApiContract = (routeConfig) => {
17
17
  return `${routeConfig.method.toUpperCase()} ${mapApiContractToPath(routeConfig)}`;
18
18
  };
19
+ /** Collects every SSE event-schema map declared by a single response entry (legacy or content-map). */
20
+ const collectSseSchemaMaps = (value) => {
21
+ if (isContentResponseEntry(value)) {
22
+ const maps = [];
23
+ for (const descriptor of Object.values(value.content ?? {})) {
24
+ if (isSseBody(descriptor)) {
25
+ maps.push(descriptor.schemaByEventName);
26
+ }
27
+ }
28
+ return maps;
29
+ }
30
+ if (isSseResponse(value)) {
31
+ return [value.schemaByEventName];
32
+ }
33
+ if (isAnyOfResponses(value)) {
34
+ return value.responses.filter(isSseResponse).map((response) => response.schemaByEventName);
35
+ }
36
+ return [];
37
+ };
19
38
  export const getSseSchemaByEventName = (routeConfig) => {
20
39
  const result = {};
21
40
  for (const value of Object.values(routeConfig.responsesByStatusCode)) {
22
- if (isSseResponse(value)) {
23
- Object.assign(result, value.schemaByEventName);
24
- }
25
- else if (isAnyOfResponses(value)) {
26
- for (const response of value.responses) {
27
- if (isSseResponse(response)) {
28
- Object.assign(result, response.schemaByEventName);
29
- }
41
+ if (value) {
42
+ for (const map of collectSseSchemaMaps(value)) {
43
+ Object.assign(result, map);
30
44
  }
31
45
  }
32
46
  }
@@ -35,19 +49,9 @@ export const getSseSchemaByEventName = (routeConfig) => {
35
49
  export const hasAnySuccessSseResponse = (apiContract) => {
36
50
  for (const code of [...SUCCESSFUL_HTTP_STATUS_CODES, '2xx', 'default']) {
37
51
  const value = apiContract.responsesByStatusCode[code];
38
- if (!value) {
39
- continue;
40
- }
41
- if (isSseResponse(value)) {
52
+ if (value && collectSseSchemaMaps(value).length > 0) {
42
53
  return true;
43
54
  }
44
- else if (isAnyOfResponses(value)) {
45
- for (const response of value.responses) {
46
- if (isSseResponse(response)) {
47
- return true;
48
- }
49
- }
50
- }
51
55
  }
52
56
  return false;
53
57
  };
@@ -61,6 +65,21 @@ export const getSuccessResponseSchema = (routeConfig) => {
61
65
  if (!value) {
62
66
  continue;
63
67
  }
68
+ if (isContentResponseEntry(value)) {
69
+ if (!value.content) {
70
+ hasDirectNonJsonEntry = true;
71
+ continue;
72
+ }
73
+ for (const descriptor of Object.values(value.content)) {
74
+ if (isJsonBody(descriptor)) {
75
+ schemas.push(descriptor);
76
+ }
77
+ else {
78
+ hasDirectNonJsonEntry = true;
79
+ }
80
+ }
81
+ continue;
82
+ }
64
83
  if (isAnyOfResponses(value)) {
65
84
  for (const response of value.responses) {
66
85
  if (!isSseResponse(response) && !isTextResponse(response) && !isBlobResponse(response)) {
@@ -93,7 +112,17 @@ export const getIsEmptyResponseExpected = (routeConfig) => {
93
112
  let isEmptyResponseExpected = true;
94
113
  for (const code of SUCCESSFUL_HTTP_STATUS_CODES) {
95
114
  const value = routeConfig.responsesByStatusCode[code];
96
- if (value && value !== ContractNoBody && !isNoBodyResponse(value)) {
115
+ if (!value) {
116
+ continue;
117
+ }
118
+ if (isContentResponseEntry(value)) {
119
+ if (value.content) {
120
+ isEmptyResponseExpected = false;
121
+ break;
122
+ }
123
+ continue;
124
+ }
125
+ if (value !== ContractNoBody && !isNoBodyResponse(value)) {
97
126
  isEmptyResponseExpected = false;
98
127
  break;
99
128
  }
@@ -1 +1 @@
1
- {"version":3,"file":"defineApiContract.js","sourceRoot":"","sources":["../../src/new/defineApiContract.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"defineApiContract.js","sourceRoot":"","sources":["../../src/new/defineApiContract.ts"],"names":[],"mappings":"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"}
@@ -2,22 +2,39 @@ import type { z } from 'zod/v4';
2
2
  import type { SuccessfulHttpStatusCode } from '../HttpStatusCodes.ts';
3
3
  import type { ValueOf } from '../typeUtils.ts';
4
4
  import type { ContractNoBody } from './constants.ts';
5
- import type { NoBodyResponse, ResponsesByStatusCode } from './contractResponse.ts';
5
+ import type { NoBodyResponse, ResponseEntry, ResponsesByStatusCode } from './contractResponse.ts';
6
6
  type ExtractSuccessResponses<T extends ResponsesByStatusCode> = ValueOf<T, Extract<keyof T, SuccessfulHttpStatusCode | '2xx' | 'default'>>;
7
+ type UnpackAnyOf<T> = T extends {
8
+ _tag: 'AnyOfResponses';
9
+ responses: Array<infer Item>;
10
+ } ? Item : T;
11
+ type ContentDescriptorsOf<TEntry> = TEntry extends {
12
+ content: infer TContent;
13
+ } ? TContent[keyof TContent] : never;
14
+ type NormalizeContentDescriptor<TDescriptor> = TDescriptor extends {
15
+ _tag: 'SseBody';
16
+ schemaByEventName: infer S;
17
+ } ? {
18
+ _tag: 'SseResponse';
19
+ schemaByEventName: S;
20
+ } : TDescriptor extends {
21
+ _tag: 'BlobBody';
22
+ } ? {
23
+ _tag: 'BlobResponse';
24
+ } : TDescriptor;
25
+ type FlatContentSuccessResponses<TEntry> = NormalizeContentDescriptor<ContentDescriptorsOf<TEntry>> | (TEntry extends {
26
+ allowNoBody: true;
27
+ } ? typeof ContractNoBody : never);
28
+ type FlatSuccessResponses<T extends ResponsesByStatusCode> = UnpackAnyOf<Exclude<ExtractSuccessResponses<T>, ResponseEntry>> | FlatContentSuccessResponses<Extract<ExtractSuccessResponses<T>, ResponseEntry>>;
7
29
  /**
8
- * Returns true if all success responses have no body
9
- * (ContractNoBody, noBodyResponse(), or no success status codes defined).
30
+ * Returns true if all success responses have no body (ContractNoBody, noBodyResponse(),
31
+ * a no-body content entry, or no success status codes defined).
10
32
  *
11
33
  * @deprecated No known consumers — will be removed in a future release.
12
34
  */
13
35
  export type IsNoBodySuccessResponse<T extends ResponsesByStatusCode> = [
14
- ExtractSuccessResponses<T>
36
+ FlatSuccessResponses<T>
15
37
  ] extends [typeof ContractNoBody | NoBodyResponse | undefined] ? true : false;
16
- type UnpackAnyOf<T> = T extends {
17
- _tag: 'AnyOfResponses';
18
- responses: Array<infer Item>;
19
- } ? Item : T;
20
- type FlatSuccessResponses<T extends ResponsesByStatusCode> = UnpackAnyOf<ExtractSuccessResponses<T>>;
21
38
  type SseSchemaOf<T> = T extends {
22
39
  _tag: 'SseResponse';
23
40
  schemaByEventName: infer S;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lokalise/api-contracts",
3
- "version": "6.13.1",
3
+ "version": "6.15.0",
4
4
  "files": [
5
5
  "dist"
6
6
  ],
@@ -37,14 +37,14 @@
37
37
  },
38
38
  "devDependencies": {
39
39
  "@biomejs/biome": "^2.5.0",
40
- "@lokalise/biome-config": "^3.1.0",
41
- "@lokalise/tsconfig": "^4.0.0",
42
40
  "@types/node": "^25.9.3",
43
41
  "@vitest/coverage-v8": "^4.1.9",
44
42
  "rimraf": "^6.1.2",
45
43
  "typescript": "6.0.3",
46
44
  "vitest": "^4.1.9",
47
- "zod": "^4.3.6"
45
+ "zod": "^4.3.6",
46
+ "@lokalise/biome-config": "^3.1.1",
47
+ "@lokalise/tsconfig": "^5.0.0"
48
48
  },
49
49
  "scripts": {
50
50
  "build": "rimraf dist && tsc --project tsconfig.build.json",