@lokalise/api-contracts 6.14.0 → 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
@@ -68,13 +68,55 @@ const downloadPhoto = defineApiContract({
68
68
 
69
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
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.
77
+
78
+ ```ts
79
+ import { defineApiContract, blobBody, sseBody } from '@lokalise/api-contracts'
80
+ import { z } from 'zod/v4'
81
+
82
+ const downloadReport = defineApiContract({
83
+ method: 'get',
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
+ },
98
+ })
99
+ ```
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
+
71
112
  ### SSE and dual-mode routes
72
113
 
73
- Use `sseResponse()` inside `responsesByStatusCode` to define SSE event schemas.
74
- 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.
75
117
 
76
118
  ```ts
77
- import { defineApiContract, sseResponse, anyOfResponses } from '@lokalise/api-contracts'
119
+ import { defineApiContract, sseResponse, sseBody } from '@lokalise/api-contracts'
78
120
  import { z } from 'zod/v4'
79
121
 
80
122
  // SSE-only
@@ -94,13 +136,15 @@ const chatCompletion = defineApiContract({
94
136
  pathResolver: () => '/chat/completions',
95
137
  requestBodySchema: z.object({ message: z.string() }),
96
138
  responsesByStatusCode: {
97
- 200: anyOfResponses([
98
- sseResponse({
99
- chunk: z.object({ delta: z.string() }),
100
- done: z.object({ finish_reason: z.string() }),
101
- }),
102
- z.object({ text: z.string() }),
103
- ]),
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
+ },
104
148
  },
105
149
  })
106
150
  ```
@@ -156,13 +200,7 @@ The `'2xx'` range key participates in SSE detection and success/error type narro
156
200
  All response factories accept an optional `ResponseOptions` object as their last argument.
157
201
 
158
202
  ```ts
159
- import {
160
- defineApiContract,
161
- noBodyResponse,
162
- blobResponse,
163
- sseResponse,
164
- anyOfResponses,
165
- } from '@lokalise/api-contracts'
203
+ import { defineApiContract, noBodyResponse, blobBody, sseBody } from '@lokalise/api-contracts'
166
204
  import { z } from 'zod/v4'
167
205
 
168
206
  const contract = defineApiContract({
@@ -172,19 +210,22 @@ const contract = defineApiContract({
172
210
  responsesByStatusCode: {
173
211
  201: z.object({ id: z.string() }).describe('Created resource'),
174
212
  204: noBodyResponse({ description: 'Deleted — no content returned' }),
175
- 200: anyOfResponses(
176
- [
177
- z.object({ id: z.string() }).describe('JSON representation'),
178
- blobResponse('text/csv', { description: 'CSV export' }),
179
- blobResponse('application/pdf', { description: 'PDF report' }),
180
- sseResponse({ update: z.object({ id: z.string() }) }, { description: 'Live update stream' }),
181
- ],
182
- { description: 'Multiple response formats available' },
183
- ),
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
+ },
184
222
  },
185
223
  })
186
224
  ```
187
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
+
188
229
  `getSseSchemaByEventName(contract)` extracts SSE event schemas from a contract:
189
230
 
190
231
  ```ts
@@ -254,7 +295,7 @@ const contract = defineApiContract({
254
295
 
255
296
  ### Type utilities
256
297
 
257
- **`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.
258
299
 
259
300
  ```ts
260
301
  import type { InferNonSseSuccessResponses } from '@lokalise/api-contracts'
@@ -358,7 +399,7 @@ getIsEmptyResponseExpected(deleteUser) // true
358
399
  getIsEmptyResponseExpected(getUser) // false
359
400
  ```
360
401
 
361
- **`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).
362
403
 
363
404
  ```ts
364
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];
@@ -41,6 +41,13 @@ export type AnyOfResponses<T extends TypedApiContractResponse = TypedApiContract
41
41
  readonly responses: T[];
42
42
  readonly description?: string;
43
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
+ */
44
51
  export declare const anyOfResponses: <T extends TypedApiContractResponse>(responses: T[], options?: ResponseOptions) => AnyOfResponses<T>;
45
52
  export declare const isAnyOfResponses: (value: ApiContractResponse) => value is AnyOfResponses;
46
53
  export type NoBodyResponse = {
@@ -50,7 +57,47 @@ export type NoBodyResponse = {
50
57
  export declare const noBodyResponse: (options?: ResponseOptions) => NoBodyResponse;
51
58
  export declare const isNoBodyResponse: (value: ApiContractResponse) => value is NoBodyResponse;
52
59
  export type ApiContractResponse = typeof ContractNoBody | NoBodyResponse | TypedApiContractResponse | AnyOfResponses;
53
- 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>>;
54
101
  export type ResponseKind = {
55
102
  kind: 'noContent';
56
103
  } | {
@@ -80,7 +127,7 @@ export type ResponseKind = {
80
127
  * returning `null` — only applies to single-entry responses; `anyOfResponses` always requires a
81
128
  * content-type to disambiguate regardless of this flag.
82
129
  */
83
- 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;
84
131
  /**
85
132
  * Combines status-code lookup and content-type resolution into a single call.
86
133
  * Lookup precedence: exact code → range key (e.g. `'4xx'`) → `'default'`.
@@ -25,6 +25,13 @@ export const sseResponse = (schemaByEventName, options) => ({
25
25
  });
26
26
  export const isSseResponse = (value) => typeof value === 'object' && value !== null && '_tag' in value && value._tag === 'SseResponse';
27
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
+ */
28
35
  export const anyOfResponses = (responses, options) => ({
29
36
  _tag: 'AnyOfResponses',
30
37
  responses,
@@ -36,6 +43,18 @@ export const noBodyResponse = (options) => ({
36
43
  ...(options?.description !== undefined && { description: options.description }),
37
44
  });
38
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);
39
58
  const matchTypedResponse = (entry, contentType) => {
40
59
  if (isTextResponse(entry)) {
41
60
  return contentType.includes(entry.contentType) ? { kind: 'text' } : null;
@@ -65,6 +84,60 @@ const resolveByKind = (entry) => {
65
84
  }
66
85
  return { kind: 'json', schema: entry };
67
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
+ };
68
141
  /**
69
142
  * Resolves a contract's response entry for a given status code into a concrete `ResponseKind`,
70
143
  * taking the response `content-type` into account.
@@ -82,21 +155,14 @@ const resolveByKind = (entry) => {
82
155
  * content-type to disambiguate regardless of this flag.
83
156
  */
84
157
  export const resolveContractResponse = (schemaEntry, contentType, strict = true) => {
158
+ if (isContentResponseEntry(schemaEntry)) {
159
+ return resolveContentEntry(schemaEntry, contentType, strict);
160
+ }
85
161
  if (schemaEntry === ContractNoBody || isNoBodyResponse(schemaEntry)) {
86
162
  return { kind: 'noContent' };
87
163
  }
88
164
  if (isAnyOfResponses(schemaEntry)) {
89
- // AnyOfResponses always requires content-type to disambiguate — strict mode has no effect here
90
- if (!contentType) {
91
- return null;
92
- }
93
- for (const item of schemaEntry.responses) {
94
- const resolved = matchTypedResponse(item, contentType);
95
- if (resolved) {
96
- return resolved;
97
- }
98
- }
99
- return null;
165
+ return resolveAnyOf(schemaEntry, contentType);
100
166
  }
101
167
  if (!contentType) {
102
168
  return strict ? null : resolveByKind(schemaEntry);
@@ -1 +1 @@
1
- {"version":3,"file":"contractResponse.js","sourceRoot":"","sources":["../../src/new/contractResponse.ts"],"names":[],"mappings":"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"}
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.14.0",
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",