@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 +71 -47
- package/dist/new/clientTypes.d.ts +51 -38
- package/dist/new/contractResponse.d.ts +56 -2
- package/dist/new/contractResponse.js +84 -11
- package/dist/new/contractResponse.js.map +1 -1
- package/dist/new/defineApiContract.js +50 -21
- package/dist/new/defineApiContract.js.map +1 -1
- package/dist/new/inferTypes.d.ts +26 -9
- package/package.json +4 -4
package/README.md
CHANGED
|
@@ -48,49 +48,75 @@ const deleteUser = defineApiContract({
|
|
|
48
48
|
|
|
49
49
|
### Non-JSON responses
|
|
50
50
|
|
|
51
|
-
Use `
|
|
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,
|
|
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:
|
|
59
|
+
responsesByStatusCode: { 200: blobResponse('text/csv') },
|
|
60
60
|
})
|
|
61
61
|
|
|
62
|
-
const
|
|
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: () => '/
|
|
71
|
-
responsesByStatusCode: { 200:
|
|
64
|
+
pathResolver: () => '/photo.png',
|
|
65
|
+
responsesByStatusCode: { 200: blobResponse('image/png') },
|
|
72
66
|
})
|
|
73
67
|
```
|
|
74
68
|
|
|
75
|
-
|
|
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,
|
|
79
|
+
import { defineApiContract, blobBody, sseBody } from '@lokalise/api-contracts'
|
|
80
|
+
import { z } from 'zod/v4'
|
|
79
81
|
|
|
80
|
-
const
|
|
82
|
+
const downloadReport = defineApiContract({
|
|
81
83
|
method: 'get',
|
|
82
|
-
pathResolver: () => '/
|
|
83
|
-
responsesByStatusCode: {
|
|
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`
|
|
90
|
-
|
|
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,
|
|
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:
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
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:
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
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
|
-
//
|
|
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
|
|
54
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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;
|
|
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 (
|
|
23
|
-
|
|
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 (
|
|
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 (
|
|
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":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,QAAQ,CAAA;AAM1B,OAAO,EAAE,4BAA4B,EAAE,MAAM,uBAAuB,CAAA;AAEpE,OAAO,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAA;AAC/C,OAAO,
|
|
1
|
+
{"version":3,"file":"defineApiContract.js","sourceRoot":"","sources":["../../src/new/defineApiContract.ts"],"names":[],"mappings":"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"}
|
package/dist/new/inferTypes.d.ts
CHANGED
|
@@ -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
|
-
*
|
|
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
|
-
|
|
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.
|
|
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",
|