@lokalise/api-contracts 6.1.0 → 6.2.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 +143 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +7 -0
- package/dist/index.js.map +1 -1
- package/dist/sse/dualModeContracts.d.ts +63 -0
- package/dist/sse/dualModeContracts.js +2 -0
- package/dist/sse/dualModeContracts.js.map +1 -0
- package/dist/sse/sseContractBuilders.d.ts +146 -0
- package/dist/sse/sseContractBuilders.js +97 -0
- package/dist/sse/sseContractBuilders.js.map +1 -0
- package/dist/sse/sseContracts.d.ts +64 -0
- package/dist/sse/sseContracts.js +2 -0
- package/dist/sse/sseContracts.js.map +1 -0
- package/dist/sse/sseTypes.d.ts +34 -0
- package/dist/sse/sseTypes.js +2 -0
- package/dist/sse/sseTypes.js.map +1 -0
- package/package.json +6 -6
package/README.md
CHANGED
|
@@ -191,3 +191,146 @@ This function is particularly useful for:
|
|
|
191
191
|
- Generating documentation or route summaries
|
|
192
192
|
- Error messages that need to reference specific endpoints
|
|
193
193
|
- Test descriptions and assertions
|
|
194
|
+
|
|
195
|
+
## SSE and Dual-Mode Contracts
|
|
196
|
+
|
|
197
|
+
Use `buildSseContract` for endpoints that support Server-Sent Events (SSE) streaming. This builder supports two contract types:
|
|
198
|
+
|
|
199
|
+
### SSE-Only Contracts
|
|
200
|
+
|
|
201
|
+
Pure streaming endpoints that only return SSE events. Use these for real-time notifications, live feeds, or any endpoint that only streams data.
|
|
202
|
+
|
|
203
|
+
```ts
|
|
204
|
+
import { buildSseContract } from '@lokalise/api-contracts'
|
|
205
|
+
import { z } from 'zod'
|
|
206
|
+
|
|
207
|
+
// GET SSE endpoint for live notifications
|
|
208
|
+
const notificationsStream = buildSseContract({
|
|
209
|
+
pathResolver: () => '/api/notifications/stream',
|
|
210
|
+
params: z.object({}),
|
|
211
|
+
query: z.object({ userId: z.string().optional() }),
|
|
212
|
+
requestHeaders: z.object({}),
|
|
213
|
+
sseEvents: {
|
|
214
|
+
notification: z.object({ id: z.string(), message: z.string() }),
|
|
215
|
+
},
|
|
216
|
+
})
|
|
217
|
+
// Result: isSSE: true
|
|
218
|
+
|
|
219
|
+
// POST SSE endpoint (e.g., streaming file processing)
|
|
220
|
+
const processStream = buildSseContract({
|
|
221
|
+
method: 'post',
|
|
222
|
+
pathResolver: () => '/api/process/stream',
|
|
223
|
+
params: z.object({}),
|
|
224
|
+
query: z.object({}),
|
|
225
|
+
requestHeaders: z.object({}),
|
|
226
|
+
requestBody: z.object({ fileId: z.string() }),
|
|
227
|
+
sseEvents: {
|
|
228
|
+
progress: z.object({ percent: z.number() }),
|
|
229
|
+
done: z.object({ result: z.string() }),
|
|
230
|
+
},
|
|
231
|
+
})
|
|
232
|
+
// Result: isSSE: true
|
|
233
|
+
|
|
234
|
+
// SSE endpoint with error schemas (for errors before streaming starts)
|
|
235
|
+
const channelStream = buildSseContract({
|
|
236
|
+
pathResolver: (params) => `/api/channels/${params.channelId}/stream`,
|
|
237
|
+
params: z.object({ channelId: z.string() }),
|
|
238
|
+
query: z.object({}),
|
|
239
|
+
requestHeaders: z.object({ authorization: z.string() }),
|
|
240
|
+
sseEvents: {
|
|
241
|
+
message: z.object({ text: z.string() }),
|
|
242
|
+
},
|
|
243
|
+
// Errors returned before streaming begins
|
|
244
|
+
responseSchemasByStatusCode: {
|
|
245
|
+
401: z.object({ error: z.literal('Unauthorized') }),
|
|
246
|
+
404: z.object({ error: z.literal('Channel not found') }),
|
|
247
|
+
},
|
|
248
|
+
})
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
### Dual-Mode (Hybrid) Contracts
|
|
252
|
+
|
|
253
|
+
Hybrid endpoints that support **both** synchronous JSON responses **and** SSE streaming from the same URL. The response mode is determined by the client's `Accept` header:
|
|
254
|
+
- `Accept: application/json` → Synchronous JSON response
|
|
255
|
+
- `Accept: text/event-stream` → SSE streaming
|
|
256
|
+
|
|
257
|
+
This pattern is ideal for AI/LLM APIs (like OpenAI's Chat Completions API) where clients can choose between getting the full response at once or streaming it token-by-token.
|
|
258
|
+
|
|
259
|
+
```ts
|
|
260
|
+
import { buildSseContract } from '@lokalise/api-contracts'
|
|
261
|
+
import { z } from 'zod'
|
|
262
|
+
|
|
263
|
+
// OpenAI-style chat completion endpoint
|
|
264
|
+
// - Accept: application/json → returns { reply, usage } immediately
|
|
265
|
+
// - Accept: text/event-stream → streams chunk events, then done event
|
|
266
|
+
const chatCompletion = buildSseContract({
|
|
267
|
+
method: 'post',
|
|
268
|
+
pathResolver: () => '/api/chat/completions',
|
|
269
|
+
params: z.object({}),
|
|
270
|
+
query: z.object({}),
|
|
271
|
+
requestHeaders: z.object({}),
|
|
272
|
+
requestBody: z.object({ message: z.string() }),
|
|
273
|
+
// Adding syncResponseBody makes it dual-mode
|
|
274
|
+
syncResponseBody: z.object({
|
|
275
|
+
reply: z.string(),
|
|
276
|
+
usage: z.object({ tokens: z.number() }),
|
|
277
|
+
}),
|
|
278
|
+
sseEvents: {
|
|
279
|
+
chunk: z.object({ delta: z.string() }),
|
|
280
|
+
done: z.object({ usage: z.object({ totalTokens: z.number() }) }),
|
|
281
|
+
},
|
|
282
|
+
})
|
|
283
|
+
// Result: isDualMode: true
|
|
284
|
+
|
|
285
|
+
// GET dual-mode endpoint for job status (poll or stream)
|
|
286
|
+
const jobStatus = buildSseContract({
|
|
287
|
+
pathResolver: (params) => `/api/jobs/${params.jobId}/status`,
|
|
288
|
+
params: z.object({ jobId: z.string().uuid() }),
|
|
289
|
+
query: z.object({ verbose: z.string().optional() }),
|
|
290
|
+
requestHeaders: z.object({}),
|
|
291
|
+
syncResponseBody: z.object({
|
|
292
|
+
status: z.enum(['pending', 'running', 'completed', 'failed']),
|
|
293
|
+
progress: z.number(),
|
|
294
|
+
}),
|
|
295
|
+
sseEvents: {
|
|
296
|
+
progress: z.object({ percent: z.number() }),
|
|
297
|
+
done: z.object({ result: z.string() }),
|
|
298
|
+
},
|
|
299
|
+
})
|
|
300
|
+
// Result: isDualMode: true
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
### Response Schemas by Status Code
|
|
304
|
+
|
|
305
|
+
Both SSE-only and dual-mode contracts support `responseSchemasByStatusCode` for defining different response shapes for errors that occur **before streaming starts** (e.g., authentication failures, validation errors, resource not found):
|
|
306
|
+
|
|
307
|
+
```ts
|
|
308
|
+
const chatCompletion = buildSseContract({
|
|
309
|
+
method: 'post',
|
|
310
|
+
pathResolver: () => '/api/chat/completions',
|
|
311
|
+
params: z.object({}),
|
|
312
|
+
query: z.object({}),
|
|
313
|
+
requestHeaders: z.object({ authorization: z.string() }),
|
|
314
|
+
requestBody: z.object({ message: z.string() }),
|
|
315
|
+
syncResponseBody: z.object({ reply: z.string() }),
|
|
316
|
+
sseEvents: {
|
|
317
|
+
chunk: z.object({ delta: z.string() }),
|
|
318
|
+
},
|
|
319
|
+
responseSchemasByStatusCode: {
|
|
320
|
+
400: z.object({ error: z.string(), details: z.array(z.string()) }),
|
|
321
|
+
401: z.object({ error: z.literal('Unauthorized') }),
|
|
322
|
+
429: z.object({ error: z.string(), retryAfter: z.number() }),
|
|
323
|
+
},
|
|
324
|
+
})
|
|
325
|
+
```
|
|
326
|
+
|
|
327
|
+
### Contract Type Detection
|
|
328
|
+
|
|
329
|
+
`buildSseContract` automatically determines the contract type based on the presence of `syncResponseBody`:
|
|
330
|
+
|
|
331
|
+
| `syncResponseBody` | `requestBody` | Result |
|
|
332
|
+
|-------------------|---------------|--------|
|
|
333
|
+
| ❌ | ❌ | SSE-only GET |
|
|
334
|
+
| ❌ | ✅ | SSE-only POST/PUT/PATCH |
|
|
335
|
+
| ✅ | ❌ | Dual-mode GET |
|
|
336
|
+
| ✅ | ✅ | Dual-mode POST/PUT/PATCH |
|
package/dist/index.d.ts
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
1
|
export * from './apiContracts.ts';
|
|
2
2
|
export * from './HttpStatusCodes.ts';
|
|
3
3
|
export * from './pathUtils.ts';
|
|
4
|
+
export * from './sse/dualModeContracts.ts';
|
|
5
|
+
export * from './sse/sseContractBuilders.ts';
|
|
6
|
+
export * from './sse/sseContracts.ts';
|
|
7
|
+
export * from './sse/sseTypes.ts';
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,11 @@
|
|
|
1
1
|
export * from "./apiContracts.js";
|
|
2
2
|
export * from "./HttpStatusCodes.js";
|
|
3
3
|
export * from "./pathUtils.js";
|
|
4
|
+
// Dual-mode (hybrid) contracts
|
|
5
|
+
export * from "./sse/dualModeContracts.js";
|
|
6
|
+
// Contract builders
|
|
7
|
+
export * from "./sse/sseContractBuilders.js";
|
|
8
|
+
// SSE contracts
|
|
9
|
+
export * from "./sse/sseContracts.js";
|
|
10
|
+
export * from "./sse/sseTypes.js";
|
|
4
11
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,mBAAmB,CAAA;AACjC,cAAc,sBAAsB,CAAA;AACpC,cAAc,gBAAgB,CAAA"}
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,mBAAmB,CAAA;AACjC,cAAc,sBAAsB,CAAA;AACpC,cAAc,gBAAgB,CAAA;AAC9B,+BAA+B;AAC/B,cAAc,4BAA4B,CAAA;AAC1C,oBAAoB;AACpB,cAAc,8BAA8B,CAAA;AAC5C,gBAAgB;AAChB,cAAc,uBAAuB,CAAA;AACrC,cAAc,mBAAmB,CAAA"}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import type { z } from 'zod/v4';
|
|
2
|
+
import type { RoutePathResolver } from '../apiContracts.ts';
|
|
3
|
+
import type { HttpStatusCode } from '../HttpStatusCodes.ts';
|
|
4
|
+
import type { SSEMethod } from './sseContracts.ts';
|
|
5
|
+
import type { SSEEventSchemas } from './sseTypes.ts';
|
|
6
|
+
/**
|
|
7
|
+
* Definition for a dual-mode route.
|
|
8
|
+
* Use `syncResponseBody` for the non-streaming response schema.
|
|
9
|
+
*
|
|
10
|
+
* @template Method - HTTP method (GET, POST, PUT, PATCH)
|
|
11
|
+
* @template Params - Path parameters schema
|
|
12
|
+
* @template Query - Query string parameters schema
|
|
13
|
+
* @template RequestHeaders - Request headers schema
|
|
14
|
+
* @template Body - Request requestBody schema (for POST/PUT/PATCH)
|
|
15
|
+
* @template SyncResponse - Sync response schema (for Accept: application/json)
|
|
16
|
+
* @template Events - SSE event schemas (for Accept: text/event-stream)
|
|
17
|
+
* @template ResponseHeaders - Response headers schema (for sync mode)
|
|
18
|
+
* @template ResponseSchemasByStatusCode - Alternative response schemas by HTTP status code
|
|
19
|
+
*/
|
|
20
|
+
export type SimplifiedDualModeContractDefinition<Method extends SSEMethod = SSEMethod, Params extends z.ZodTypeAny = z.ZodTypeAny, Query extends z.ZodTypeAny = z.ZodTypeAny, RequestHeaders extends z.ZodTypeAny = z.ZodTypeAny, Body extends z.ZodTypeAny | undefined = undefined, SyncResponse extends z.ZodTypeAny = z.ZodTypeAny, Events extends SSEEventSchemas = SSEEventSchemas, ResponseHeaders extends z.ZodTypeAny | undefined = undefined, ResponseSchemasByStatusCode extends Partial<Record<HttpStatusCode, z.ZodTypeAny>> | undefined = undefined> = {
|
|
21
|
+
method: Method;
|
|
22
|
+
pathResolver: RoutePathResolver<z.infer<Params>>;
|
|
23
|
+
params: Params;
|
|
24
|
+
query: Query;
|
|
25
|
+
requestHeaders: RequestHeaders;
|
|
26
|
+
requestBody: Body;
|
|
27
|
+
/** Sync response schema - use with `sync` handler */
|
|
28
|
+
syncResponseBody: SyncResponse;
|
|
29
|
+
responseHeaders?: ResponseHeaders;
|
|
30
|
+
/**
|
|
31
|
+
* Alternative response schemas by HTTP status code.
|
|
32
|
+
* Used to define different response shapes for error cases (e.g., 400, 404, 500).
|
|
33
|
+
*
|
|
34
|
+
* @example
|
|
35
|
+
* ```ts
|
|
36
|
+
* responseSchemasByStatusCode: {
|
|
37
|
+
* 400: z.object({ error: z.string(), details: z.array(z.string()) }),
|
|
38
|
+
* 404: z.object({ error: z.string() }),
|
|
39
|
+
* }
|
|
40
|
+
* ```
|
|
41
|
+
*/
|
|
42
|
+
responseSchemasByStatusCode?: ResponseSchemasByStatusCode;
|
|
43
|
+
sseEvents: Events;
|
|
44
|
+
isDualMode: true;
|
|
45
|
+
};
|
|
46
|
+
/**
|
|
47
|
+
* Type representing any dual-mode route definition (for use in generic constraints).
|
|
48
|
+
* Uses a manually defined type to avoid pathResolver type incompatibilities.
|
|
49
|
+
*/
|
|
50
|
+
export type AnyDualModeContractDefinition = {
|
|
51
|
+
method: SSEMethod;
|
|
52
|
+
pathResolver: RoutePathResolver<any>;
|
|
53
|
+
params: z.ZodTypeAny;
|
|
54
|
+
query: z.ZodTypeAny;
|
|
55
|
+
requestHeaders: z.ZodTypeAny;
|
|
56
|
+
requestBody: z.ZodTypeAny | undefined;
|
|
57
|
+
/** Sync response schema */
|
|
58
|
+
syncResponseBody: z.ZodTypeAny;
|
|
59
|
+
responseHeaders?: z.ZodTypeAny;
|
|
60
|
+
responseSchemasByStatusCode?: Partial<Record<HttpStatusCode, z.ZodTypeAny>>;
|
|
61
|
+
sseEvents: SSEEventSchemas;
|
|
62
|
+
isDualMode: true;
|
|
63
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dualModeContracts.js","sourceRoot":"","sources":["../../src/sse/dualModeContracts.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import type { z } from 'zod/v4';
|
|
2
|
+
import type { RoutePathResolver } from '../apiContracts.ts';
|
|
3
|
+
import type { HttpStatusCode } from '../HttpStatusCodes.ts';
|
|
4
|
+
import type { SimplifiedDualModeContractDefinition } from './dualModeContracts.ts';
|
|
5
|
+
import type { SSEContractDefinition } from './sseContracts.ts';
|
|
6
|
+
import type { SSEEventSchemas } from './sseTypes.ts';
|
|
7
|
+
/**
|
|
8
|
+
* Configuration for building a GET SSE route.
|
|
9
|
+
* Forbids requestBody for GET variants.
|
|
10
|
+
*/
|
|
11
|
+
export type SSEGetContractConfig<Params extends z.ZodTypeAny, Query extends z.ZodTypeAny, RequestHeaders extends z.ZodTypeAny, Events extends SSEEventSchemas, ResponseSchemasByStatusCode extends Partial<Record<HttpStatusCode, z.ZodTypeAny>> | undefined = undefined> = {
|
|
12
|
+
pathResolver: RoutePathResolver<z.infer<Params>>;
|
|
13
|
+
params: Params;
|
|
14
|
+
query: Query;
|
|
15
|
+
requestHeaders: RequestHeaders;
|
|
16
|
+
sseEvents: Events;
|
|
17
|
+
/**
|
|
18
|
+
* Error response schemas by HTTP status code.
|
|
19
|
+
* Used to define response shapes for errors that occur before streaming starts
|
|
20
|
+
* (e.g., authentication failures, validation errors, not found).
|
|
21
|
+
*
|
|
22
|
+
* @example
|
|
23
|
+
* ```ts
|
|
24
|
+
* responseSchemasByStatusCode: {
|
|
25
|
+
* 401: z.object({ error: z.literal('Unauthorized') }),
|
|
26
|
+
* 404: z.object({ error: z.string() }),
|
|
27
|
+
* }
|
|
28
|
+
* ```
|
|
29
|
+
*/
|
|
30
|
+
responseSchemasByStatusCode?: ResponseSchemasByStatusCode;
|
|
31
|
+
requestBody?: never;
|
|
32
|
+
syncResponseBody?: never;
|
|
33
|
+
};
|
|
34
|
+
/**
|
|
35
|
+
* Configuration for building a POST/PUT/PATCH SSE route with request requestBody.
|
|
36
|
+
* Requires requestBody for payload variants.
|
|
37
|
+
*/
|
|
38
|
+
export type SSEPayloadContractConfig<Params extends z.ZodTypeAny, Query extends z.ZodTypeAny, RequestHeaders extends z.ZodTypeAny, Body extends z.ZodTypeAny, Events extends SSEEventSchemas, ResponseSchemasByStatusCode extends Partial<Record<HttpStatusCode, z.ZodTypeAny>> | undefined = undefined> = {
|
|
39
|
+
method?: 'post' | 'put' | 'patch';
|
|
40
|
+
pathResolver: RoutePathResolver<z.infer<Params>>;
|
|
41
|
+
params: Params;
|
|
42
|
+
query: Query;
|
|
43
|
+
requestHeaders: RequestHeaders;
|
|
44
|
+
requestBody: Body;
|
|
45
|
+
sseEvents: Events;
|
|
46
|
+
/**
|
|
47
|
+
* Error response schemas by HTTP status code.
|
|
48
|
+
* Used to define response shapes for errors that occur before streaming starts
|
|
49
|
+
* (e.g., authentication failures, validation errors, not found).
|
|
50
|
+
*
|
|
51
|
+
* @example
|
|
52
|
+
* ```ts
|
|
53
|
+
* responseSchemasByStatusCode: {
|
|
54
|
+
* 401: z.object({ error: z.literal('Unauthorized') }),
|
|
55
|
+
* 404: z.object({ error: z.string() }),
|
|
56
|
+
* }
|
|
57
|
+
* ```
|
|
58
|
+
*/
|
|
59
|
+
responseSchemasByStatusCode?: ResponseSchemasByStatusCode;
|
|
60
|
+
syncResponseBody?: never;
|
|
61
|
+
};
|
|
62
|
+
/**
|
|
63
|
+
* Configuration for building a GET dual-mode route.
|
|
64
|
+
* Requires syncResponseBody, forbids requestBody.
|
|
65
|
+
*/
|
|
66
|
+
export type DualModeGetContractConfig<Params extends z.ZodTypeAny, Query extends z.ZodTypeAny, RequestHeaders extends z.ZodTypeAny, JsonResponse extends z.ZodTypeAny, Events extends SSEEventSchemas, ResponseHeaders extends z.ZodTypeAny | undefined = undefined, ResponseSchemasByStatusCode extends Partial<Record<HttpStatusCode, z.ZodTypeAny>> | undefined = undefined> = {
|
|
67
|
+
pathResolver: RoutePathResolver<z.infer<Params>>;
|
|
68
|
+
params: Params;
|
|
69
|
+
query: Query;
|
|
70
|
+
requestHeaders: RequestHeaders;
|
|
71
|
+
/** Single sync response schema */
|
|
72
|
+
syncResponseBody: JsonResponse;
|
|
73
|
+
/**
|
|
74
|
+
* Schema for validating response headers (sync mode only).
|
|
75
|
+
* Used to define and validate headers that the server will send in the response.
|
|
76
|
+
*
|
|
77
|
+
* @example
|
|
78
|
+
* ```ts
|
|
79
|
+
* responseHeaders: z.object({
|
|
80
|
+
* 'x-ratelimit-limit': z.string(),
|
|
81
|
+
* 'x-ratelimit-remaining': z.string(),
|
|
82
|
+
* })
|
|
83
|
+
* ```
|
|
84
|
+
*/
|
|
85
|
+
responseHeaders?: ResponseHeaders;
|
|
86
|
+
/**
|
|
87
|
+
* Alternative response schemas by HTTP status code.
|
|
88
|
+
* Used to define different response shapes for error cases.
|
|
89
|
+
*
|
|
90
|
+
* @example
|
|
91
|
+
* ```ts
|
|
92
|
+
* responseSchemasByStatusCode: {
|
|
93
|
+
* 400: z.object({ error: z.string(), details: z.array(z.string()) }),
|
|
94
|
+
* 404: z.object({ error: z.string() }),
|
|
95
|
+
* }
|
|
96
|
+
* ```
|
|
97
|
+
*/
|
|
98
|
+
responseSchemasByStatusCode?: ResponseSchemasByStatusCode;
|
|
99
|
+
sseEvents: Events;
|
|
100
|
+
requestBody?: never;
|
|
101
|
+
};
|
|
102
|
+
/**
|
|
103
|
+
* Configuration for building a POST/PUT/PATCH dual-mode route with request requestBody.
|
|
104
|
+
* Requires both requestBody and syncResponseBody.
|
|
105
|
+
*/
|
|
106
|
+
export type DualModePayloadContractConfig<Params extends z.ZodTypeAny, Query extends z.ZodTypeAny, RequestHeaders extends z.ZodTypeAny, Body extends z.ZodTypeAny, JsonResponse extends z.ZodTypeAny, Events extends SSEEventSchemas, ResponseHeaders extends z.ZodTypeAny | undefined = undefined, ResponseSchemasByStatusCode extends Partial<Record<HttpStatusCode, z.ZodTypeAny>> | undefined = undefined> = {
|
|
107
|
+
method?: 'post' | 'put' | 'patch';
|
|
108
|
+
pathResolver: RoutePathResolver<z.infer<Params>>;
|
|
109
|
+
params: Params;
|
|
110
|
+
query: Query;
|
|
111
|
+
requestHeaders: RequestHeaders;
|
|
112
|
+
requestBody: Body;
|
|
113
|
+
/** Single sync response schema */
|
|
114
|
+
syncResponseBody: JsonResponse;
|
|
115
|
+
/**
|
|
116
|
+
* Schema for validating response headers (sync mode only).
|
|
117
|
+
* Used to define and validate headers that the server will send in the response.
|
|
118
|
+
*
|
|
119
|
+
* @example
|
|
120
|
+
* ```ts
|
|
121
|
+
* responseHeaders: z.object({
|
|
122
|
+
* 'x-ratelimit-limit': z.string(),
|
|
123
|
+
* 'x-ratelimit-remaining': z.string(),
|
|
124
|
+
* })
|
|
125
|
+
* ```
|
|
126
|
+
*/
|
|
127
|
+
responseHeaders?: ResponseHeaders;
|
|
128
|
+
/**
|
|
129
|
+
* Alternative response schemas by HTTP status code.
|
|
130
|
+
* Used to define different response shapes for error cases.
|
|
131
|
+
*
|
|
132
|
+
* @example
|
|
133
|
+
* ```ts
|
|
134
|
+
* responseSchemasByStatusCode: {
|
|
135
|
+
* 400: z.object({ error: z.string(), details: z.array(z.string()) }),
|
|
136
|
+
* 404: z.object({ error: z.string() }),
|
|
137
|
+
* }
|
|
138
|
+
* ```
|
|
139
|
+
*/
|
|
140
|
+
responseSchemasByStatusCode?: ResponseSchemasByStatusCode;
|
|
141
|
+
sseEvents: Events;
|
|
142
|
+
};
|
|
143
|
+
export declare function buildSseContract<Params extends z.ZodTypeAny, Query extends z.ZodTypeAny, RequestHeaders extends z.ZodTypeAny, Body extends z.ZodTypeAny, JsonResponse extends z.ZodTypeAny, Events extends SSEEventSchemas, ResponseHeaders extends z.ZodTypeAny | undefined = undefined, ResponseSchemasByStatusCode extends Partial<Record<HttpStatusCode, z.ZodTypeAny>> | undefined = undefined>(config: DualModePayloadContractConfig<Params, Query, RequestHeaders, Body, JsonResponse, Events, ResponseHeaders, ResponseSchemasByStatusCode>): SimplifiedDualModeContractDefinition<'post' | 'put' | 'patch', Params, Query, RequestHeaders, Body, JsonResponse, Events, ResponseHeaders, ResponseSchemasByStatusCode>;
|
|
144
|
+
export declare function buildSseContract<Params extends z.ZodTypeAny, Query extends z.ZodTypeAny, RequestHeaders extends z.ZodTypeAny, JsonResponse extends z.ZodTypeAny, Events extends SSEEventSchemas, ResponseHeaders extends z.ZodTypeAny | undefined = undefined, ResponseSchemasByStatusCode extends Partial<Record<HttpStatusCode, z.ZodTypeAny>> | undefined = undefined>(config: DualModeGetContractConfig<Params, Query, RequestHeaders, JsonResponse, Events, ResponseHeaders, ResponseSchemasByStatusCode>): SimplifiedDualModeContractDefinition<'get', Params, Query, RequestHeaders, undefined, JsonResponse, Events, ResponseHeaders, ResponseSchemasByStatusCode>;
|
|
145
|
+
export declare function buildSseContract<Params extends z.ZodTypeAny, Query extends z.ZodTypeAny, RequestHeaders extends z.ZodTypeAny, Body extends z.ZodTypeAny, Events extends SSEEventSchemas, ResponseSchemasByStatusCode extends Partial<Record<HttpStatusCode, z.ZodTypeAny>> | undefined = undefined>(config: SSEPayloadContractConfig<Params, Query, RequestHeaders, Body, Events, ResponseSchemasByStatusCode>): SSEContractDefinition<'post' | 'put' | 'patch', Params, Query, RequestHeaders, Body, Events, ResponseSchemasByStatusCode>;
|
|
146
|
+
export declare function buildSseContract<Params extends z.ZodTypeAny, Query extends z.ZodTypeAny, RequestHeaders extends z.ZodTypeAny, Events extends SSEEventSchemas, ResponseSchemasByStatusCode extends Partial<Record<HttpStatusCode, z.ZodTypeAny>> | undefined = undefined>(config: SSEGetContractConfig<Params, Query, RequestHeaders, Events, ResponseSchemasByStatusCode>): SSEContractDefinition<'get', Params, Query, RequestHeaders, undefined, Events, ResponseSchemasByStatusCode>;
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Builds SSE (Server-Sent Events) and dual-mode contracts.
|
|
3
|
+
*
|
|
4
|
+
* This builder supports two contract types:
|
|
5
|
+
*
|
|
6
|
+
* **SSE-only contracts**: Pure streaming endpoints that only return SSE events.
|
|
7
|
+
* Use these for real-time notifications, live feeds, or any endpoint that only streams data.
|
|
8
|
+
*
|
|
9
|
+
* **Dual-mode contracts**: Hybrid endpoints that support both synchronous JSON responses
|
|
10
|
+
* AND SSE streaming from the same URL. The response mode is determined by the client's
|
|
11
|
+
* `Accept` header (`application/json` for sync, `text/event-stream` for SSE).
|
|
12
|
+
* This is ideal for AI/LLM APIs (like OpenAI) where clients can choose between
|
|
13
|
+
* getting the full response at once or streaming it token-by-token.
|
|
14
|
+
*
|
|
15
|
+
* The contract type is automatically determined based on the presence of `syncResponseBody`:
|
|
16
|
+
*
|
|
17
|
+
* | `syncResponseBody` | `requestBody` | Result |
|
|
18
|
+
* |--------------------|---------------|--------|
|
|
19
|
+
* | ❌ | ❌ | SSE-only GET |
|
|
20
|
+
* | ❌ | ✅ | SSE-only POST/PUT/PATCH |
|
|
21
|
+
* | ✅ | ❌ | Dual-mode GET |
|
|
22
|
+
* | ✅ | ✅ | Dual-mode POST/PUT/PATCH |
|
|
23
|
+
*
|
|
24
|
+
* @example
|
|
25
|
+
* ```typescript
|
|
26
|
+
* // SSE-only: Pure streaming endpoint (e.g., live notifications)
|
|
27
|
+
* const notificationsStream = buildSseContract({
|
|
28
|
+
* pathResolver: () => '/api/notifications/stream',
|
|
29
|
+
* params: z.object({}),
|
|
30
|
+
* query: z.object({ userId: z.string().optional() }),
|
|
31
|
+
* requestHeaders: z.object({}),
|
|
32
|
+
* sseEvents: {
|
|
33
|
+
* notification: z.object({ id: z.string(), message: z.string() }),
|
|
34
|
+
* },
|
|
35
|
+
* })
|
|
36
|
+
*
|
|
37
|
+
* // Dual-mode: Same endpoint supports both JSON and SSE (e.g., OpenAI-style API)
|
|
38
|
+
* // - Accept: application/json → returns { reply, usage } immediately
|
|
39
|
+
* // - Accept: text/event-stream → streams chunk events, then done event
|
|
40
|
+
* const chatCompletion = buildSseContract({
|
|
41
|
+
* method: 'POST',
|
|
42
|
+
* pathResolver: () => '/api/chat/completions',
|
|
43
|
+
* params: z.object({}),
|
|
44
|
+
* query: z.object({}),
|
|
45
|
+
* requestHeaders: z.object({}),
|
|
46
|
+
* requestBody: z.object({ message: z.string() }),
|
|
47
|
+
* syncResponseBody: z.object({ reply: z.string(), usage: z.object({ tokens: z.number() }) }),
|
|
48
|
+
* sseEvents: {
|
|
49
|
+
* chunk: z.object({ delta: z.string() }),
|
|
50
|
+
* done: z.object({ usage: z.object({ total: z.number() }) }),
|
|
51
|
+
* },
|
|
52
|
+
* })
|
|
53
|
+
* ```
|
|
54
|
+
*/
|
|
55
|
+
// Helper to build base contract fields
|
|
56
|
+
// biome-ignore lint/suspicious/noExplicitAny: Config union type
|
|
57
|
+
function buildBaseFields(config, hasBody) {
|
|
58
|
+
return {
|
|
59
|
+
pathResolver: config.pathResolver,
|
|
60
|
+
params: config.params,
|
|
61
|
+
query: config.query,
|
|
62
|
+
requestHeaders: config.requestHeaders,
|
|
63
|
+
requestBody: hasBody ? config.requestBody : undefined,
|
|
64
|
+
sseEvents: config.sseEvents,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
// Helper to determine method
|
|
68
|
+
function determineMethod(config, hasBody, defaultMethod) {
|
|
69
|
+
return hasBody ? (config.method ?? defaultMethod) : 'get';
|
|
70
|
+
}
|
|
71
|
+
// Implementation
|
|
72
|
+
export function buildSseContract(config) {
|
|
73
|
+
const hasSyncResponseBody = 'syncResponseBody' in config && config.syncResponseBody !== undefined;
|
|
74
|
+
const hasBody = 'requestBody' in config && config.requestBody !== undefined;
|
|
75
|
+
const base = buildBaseFields(config, hasBody);
|
|
76
|
+
if (hasSyncResponseBody) {
|
|
77
|
+
// Dual-mode contract
|
|
78
|
+
return {
|
|
79
|
+
...base,
|
|
80
|
+
method: determineMethod(config, hasBody, 'post'),
|
|
81
|
+
syncResponseBody: config.syncResponseBody,
|
|
82
|
+
responseHeaders: config.responseHeaders,
|
|
83
|
+
responseSchemasByStatusCode: config
|
|
84
|
+
.responseSchemasByStatusCode,
|
|
85
|
+
isDualMode: true,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
// SSE-only contract
|
|
89
|
+
return {
|
|
90
|
+
...base,
|
|
91
|
+
method: determineMethod(config, hasBody, 'post'),
|
|
92
|
+
responseSchemasByStatusCode: config
|
|
93
|
+
.responseSchemasByStatusCode,
|
|
94
|
+
isSSE: true,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
//# sourceMappingURL=sseContractBuilders.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sseContractBuilders.js","sourceRoot":"","sources":["../../src/sse/sseContractBuilders.ts"],"names":[],"mappings":"AAyLA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqDG;AAEH,uCAAuC;AACvC,gEAAgE;AAChE,SAAS,eAAe,CAAC,MAAW,EAAE,OAAgB;IACpD,OAAO;QACL,YAAY,EAAE,MAAM,CAAC,YAAY;QACjC,MAAM,EAAE,MAAM,CAAC,MAAM;QACrB,KAAK,EAAE,MAAM,CAAC,KAAK;QACnB,cAAc,EAAE,MAAM,CAAC,cAAc;QACrC,WAAW,EAAE,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,WAAW,CAAC,CAAC,CAAC,SAAS;QACrD,SAAS,EAAE,MAAM,CAAC,SAAS;KAC5B,CAAA;AACH,CAAC;AAED,6BAA6B;AAC7B,SAAS,eAAe,CAAC,MAA2B,EAAE,OAAgB,EAAE,aAAqB;IAC3F,OAAO,OAAO,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,MAAM,IAAI,aAAa,CAAC,CAAC,CAAC,CAAC,KAAK,CAAA;AAC3D,CAAC;AAwHD,iBAAiB;AACjB,MAAM,UAAU,gBAAgB,CAC9B,MAOiD;IAGjD,MAAM,mBAAmB,GAAG,kBAAkB,IAAI,MAAM,IAAI,MAAM,CAAC,gBAAgB,KAAK,SAAS,CAAA;IACjG,MAAM,OAAO,GAAG,aAAa,IAAI,MAAM,IAAI,MAAM,CAAC,WAAW,KAAK,SAAS,CAAA;IAC3E,MAAM,IAAI,GAAG,eAAe,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;IAE7C,IAAI,mBAAmB,EAAE,CAAC;QACxB,qBAAqB;QACrB,OAAO;YACL,GAAG,IAAI;YACP,MAAM,EAAE,eAAe,CAAC,MAA6B,EAAE,OAAO,EAAE,MAAM,CAAC;YACvE,gBAAgB,EAAG,MAAwC,CAAC,gBAAgB;YAC5E,eAAe,EAAG,MAAwC,CAAC,eAAe;YAC1E,2BAA2B,EAAG,MAAoD;iBAC/E,2BAA2B;YAC9B,UAAU,EAAE,IAAI;SACjB,CAAA;IACH,CAAC;IAED,oBAAoB;IACpB,OAAO;QACL,GAAG,IAAI;QACP,MAAM,EAAE,eAAe,CAAC,MAA6B,EAAE,OAAO,EAAE,MAAM,CAAC;QACvE,2BAA2B,EAAG,MAAoD;aAC/E,2BAA2B;QAC9B,KAAK,EAAE,IAAI;KACZ,CAAA;AACH,CAAC"}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import type { z } from 'zod/v4';
|
|
2
|
+
import type { RoutePathResolver } from '../apiContracts.ts';
|
|
3
|
+
import type { HttpStatusCode } from '../HttpStatusCodes.ts';
|
|
4
|
+
import type { SSEEventSchemas } from './sseTypes.ts';
|
|
5
|
+
/**
|
|
6
|
+
* Supported HTTP methods for SSE routes.
|
|
7
|
+
* While traditional SSE uses GET, modern APIs (e.g., OpenAI) use POST
|
|
8
|
+
* to send request parameters in the body while streaming responses.
|
|
9
|
+
*/
|
|
10
|
+
export type SSEMethod = 'get' | 'post' | 'put' | 'patch';
|
|
11
|
+
/**
|
|
12
|
+
* Definition for an SSE route with type-safe contracts.
|
|
13
|
+
*
|
|
14
|
+
* @template Method - HTTP method (GET, POST, PUT, PATCH)
|
|
15
|
+
* @template Params - Path parameters schema
|
|
16
|
+
* @template Query - Query string parameters schema
|
|
17
|
+
* @template RequestHeaders - Request headers schema
|
|
18
|
+
* @template Body - Request requestBody schema (for POST/PUT/PATCH)
|
|
19
|
+
* @template Events - Map of event name to event data schema
|
|
20
|
+
* @template ResponseSchemasByStatusCode - Error response schemas by HTTP status code
|
|
21
|
+
*/
|
|
22
|
+
export type SSEContractDefinition<Method extends SSEMethod = SSEMethod, Params extends z.ZodTypeAny = z.ZodTypeAny, Query extends z.ZodTypeAny = z.ZodTypeAny, RequestHeaders extends z.ZodTypeAny = z.ZodTypeAny, Body extends z.ZodTypeAny | undefined = undefined, Events extends SSEEventSchemas = SSEEventSchemas, ResponseSchemasByStatusCode extends Partial<Record<HttpStatusCode, z.ZodTypeAny>> | undefined = undefined> = {
|
|
23
|
+
method: Method;
|
|
24
|
+
/**
|
|
25
|
+
* Type-safe path resolver function.
|
|
26
|
+
* Receives typed params and returns the URL path string.
|
|
27
|
+
*/
|
|
28
|
+
pathResolver: RoutePathResolver<z.infer<Params>>;
|
|
29
|
+
params: Params;
|
|
30
|
+
query: Query;
|
|
31
|
+
requestHeaders: RequestHeaders;
|
|
32
|
+
requestBody: Body;
|
|
33
|
+
sseEvents: Events;
|
|
34
|
+
/**
|
|
35
|
+
* Error response schemas by HTTP status code.
|
|
36
|
+
* Used to define response shapes for errors that occur before streaming starts
|
|
37
|
+
* (e.g., authentication failures, validation errors, not found).
|
|
38
|
+
*
|
|
39
|
+
* @example
|
|
40
|
+
* ```ts
|
|
41
|
+
* responseSchemasByStatusCode: {
|
|
42
|
+
* 401: z.object({ error: z.literal('Unauthorized') }),
|
|
43
|
+
* 404: z.object({ error: z.string() }),
|
|
44
|
+
* }
|
|
45
|
+
* ```
|
|
46
|
+
*/
|
|
47
|
+
responseSchemasByStatusCode?: ResponseSchemasByStatusCode;
|
|
48
|
+
isSSE: true;
|
|
49
|
+
};
|
|
50
|
+
/**
|
|
51
|
+
* Type representing any SSE route definition (for use in generic constraints).
|
|
52
|
+
* Uses a manually defined type to avoid pathResolver type incompatibilities.
|
|
53
|
+
*/
|
|
54
|
+
export type AnySSEContractDefinition = {
|
|
55
|
+
method: SSEMethod;
|
|
56
|
+
pathResolver: RoutePathResolver<any>;
|
|
57
|
+
params: z.ZodTypeAny;
|
|
58
|
+
query: z.ZodTypeAny;
|
|
59
|
+
requestHeaders: z.ZodTypeAny;
|
|
60
|
+
requestBody: z.ZodTypeAny | undefined;
|
|
61
|
+
sseEvents: SSEEventSchemas;
|
|
62
|
+
responseSchemasByStatusCode?: Partial<Record<HttpStatusCode, z.ZodTypeAny>>;
|
|
63
|
+
isSSE: true;
|
|
64
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sseContracts.js","sourceRoot":"","sources":["../../src/sse/sseContracts.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { z } from 'zod/v4';
|
|
2
|
+
import type { AnySSEContractDefinition } from './sseContracts.ts';
|
|
3
|
+
/**
|
|
4
|
+
* Type constraint for SSE event schemas.
|
|
5
|
+
* Maps event names to their Zod schemas for validation.
|
|
6
|
+
*/
|
|
7
|
+
export type SSEEventSchemas = Record<string, z.ZodTypeAny>;
|
|
8
|
+
/**
|
|
9
|
+
* Extract all event names from all contracts as a union of string literals.
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* ```typescript
|
|
13
|
+
* type Contracts = {
|
|
14
|
+
* notifications: { sseEvents: { alert: z.ZodObject<...> } }
|
|
15
|
+
* chat: { sseEvents: { message: z.ZodObject<...>, done: z.ZodObject<...> } }
|
|
16
|
+
* }
|
|
17
|
+
* // AllContractEventNames<Contracts> = 'alert' | 'message' | 'done'
|
|
18
|
+
* ```
|
|
19
|
+
*/
|
|
20
|
+
export type AllContractEventNames<Contracts extends Record<string, AnySSEContractDefinition>> = Contracts[keyof Contracts]['sseEvents'] extends infer E ? E extends SSEEventSchemas ? keyof E & string : never : never;
|
|
21
|
+
/**
|
|
22
|
+
* Extract the schema for a specific event name across all contracts.
|
|
23
|
+
* Returns the Zod schema for the event, or never if not found.
|
|
24
|
+
*/
|
|
25
|
+
export type ExtractEventSchema<Contracts extends Record<string, AnySSEContractDefinition>, EventName extends string> = {
|
|
26
|
+
[K in keyof Contracts]: EventName extends keyof Contracts[K]['sseEvents'] ? Contracts[K]['sseEvents'][EventName] : never;
|
|
27
|
+
}[keyof Contracts];
|
|
28
|
+
/**
|
|
29
|
+
* Flatten all events from all contracts into a single record.
|
|
30
|
+
* Used for type-safe event sending across all controller routes.
|
|
31
|
+
*/
|
|
32
|
+
export type AllContractEvents<Contracts extends Record<string, AnySSEContractDefinition>> = {
|
|
33
|
+
[EventName in AllContractEventNames<Contracts>]: ExtractEventSchema<Contracts, EventName>;
|
|
34
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sseTypes.js","sourceRoot":"","sources":["../../src/sse/sseTypes.ts"],"names":[],"mappings":""}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lokalise/api-contracts",
|
|
3
|
-
"version": "6.
|
|
3
|
+
"version": "6.2.0",
|
|
4
4
|
"files": [
|
|
5
5
|
"dist"
|
|
6
6
|
],
|
|
@@ -45,14 +45,14 @@
|
|
|
45
45
|
"zod": ">=3.25.56"
|
|
46
46
|
},
|
|
47
47
|
"devDependencies": {
|
|
48
|
-
"@biomejs/biome": "^2.3.
|
|
48
|
+
"@biomejs/biome": "^2.3.7",
|
|
49
49
|
"@lokalise/biome-config": "^3.1.0",
|
|
50
|
-
"@lokalise/tsconfig": "^1.
|
|
51
|
-
"@vitest/coverage-v8": "^
|
|
50
|
+
"@lokalise/tsconfig": "^3.1.0",
|
|
51
|
+
"@vitest/coverage-v8": "^4.0.15",
|
|
52
52
|
"rimraf": "^6.0.1",
|
|
53
53
|
"typescript": "5.9.3",
|
|
54
|
-
"vitest": "^
|
|
55
|
-
"zod": "^4.
|
|
54
|
+
"vitest": "^4.0.15",
|
|
55
|
+
"zod": "^4.3.6"
|
|
56
56
|
},
|
|
57
57
|
"dependencies": {}
|
|
58
58
|
}
|