opinionated-machine 6.20.3 → 8.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +28 -0
- package/dist/lib/api-contracts/AbstractApiController.d.ts +6 -3
- package/dist/lib/api-contracts/AbstractApiController.js +6 -3
- package/dist/lib/api-contracts/AbstractApiController.js.map +1 -1
- package/dist/lib/api-contracts/apiRouteBuilder.d.ts +57 -15
- package/dist/lib/api-contracts/apiRouteBuilder.js +14 -353
- package/dist/lib/api-contracts/apiRouteBuilder.js.map +1 -1
- package/dist/lib/api-contracts/index.d.ts +2 -2
- package/dist/lib/api-contracts/index.js.map +1 -1
- package/package.json +85 -89
- package/dist/lib/api-contracts/apiHandlerTypes.d.ts +0 -159
- package/dist/lib/api-contracts/apiHandlerTypes.js +0 -2
- package/dist/lib/api-contracts/apiHandlerTypes.js.map +0 -1
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# opinionated-machine
|
|
2
|
+
|
|
3
|
+
## 8.0.0
|
|
4
|
+
|
|
5
|
+
### Major Changes
|
|
6
|
+
|
|
7
|
+
- de12db0: Replace the local ApiContract route builder with `buildFastifyApiRoute` from `@lokalise/fastify-api-contracts` (>=6.0.0, now the source of the route-building, handler inference, response validation, and SSE streaming logic). `buildApiRoute` remains as a thin wrapper that only adds the contract-narrowed `gatewayMetadata` option.
|
|
8
|
+
|
|
9
|
+
Breaking changes:
|
|
10
|
+
|
|
11
|
+
- Handlers now use the package's unified `(request, reply, context) => { status, body }` shape for every response mode. SSE streaming is driven via `context.sse.start(...)` (or by returning an `AsyncIterable` body for an SSE status); dual-mode `{ nonSse, sse }` handler objects and `sse.respond()` / `sse.sendHeaders()` are gone — branch on `context.expectedContentType` and return `{ status, body }` for early HTTP responses.
|
|
12
|
+
- `ApiNonSseHandler`, `ApiSseHandler`, `InferApiRequest`, and `InferApiStatusResponse` are removed; `InferApiHandler`, `InferApiHandlerRequest`, `InferApiHandlerResult`, `ApiHandlerContext`, and `ApiHandlerReply` are re-exported from `@lokalise/fastify-api-contracts` instead.
|
|
13
|
+
- `buildApiRoute` options: `defaultMode` is removed (use `context.expectedContentType`). `heartbeatInterval` is removed — per-route heartbeat intervals are no longer possible: the package's `heartbeat` option is a boolean that only enables/disables the heartbeat for a route, and the interval itself is configured once for all routes at `@fastify/sse` plugin registration (`heartbeatInterval`, default 30000 ms).
|
|
14
|
+
- Response body validation is delegated to the `fastify-type-provider-zod` serializer compiler — apps must register `validatorCompiler` / `serializerCompiler`. This applies to every ApiContract route, not just JSON ones: the package emits a Zod response schema for every declared status (including `sseBody()` and `noBodyResponse()`, which previously produced no response schema), so an app missing `setSerializerCompiler` now fails at boot during route registration — even for SSE-only contracts — with `FST_ERR_SCH_SERIALIZATION_BUILD: Failed building the serialization schema … schema is invalid: data/required must be array`. If you hit that error, register the zod compilers. Peer ranges bumped: `@lokalise/fastify-api-contracts` >=6.0.0, `fastify-type-provider-zod` >=7.0.0.
|
|
15
|
+
- Error handling is delegated to the app's global `fastify.setErrorHandler` (per the `@lokalise/fastify-api-contracts` README), including for SSE routes: the route builder no longer maps the node-core `httpStatusCode` error convention (`PublicNonRecoverableError`, `InternalError`, …) onto responses — such errors now reach the error handler unmapped and default to 500 — and no longer emits a terminal SSE `error` event when a handler throws after `sse.start()`. Note the resulting cross-system split: legacy `buildFastifyRoute` / SSE / dual-mode routes still honor `httpStatusCode` internally, so an app mixing both route families maps the same error to different statuses. Install a global error handler that maps `httpStatusCode` (and, if your clients rely on it, emits the terminal SSE `error` event) before upgrading.
|
|
16
|
+
- The `SSESession` / `SSEContext` / `SSESessionMode` / `FastifySSERouteOptions` types exported from the package root still resolve to the legacy `lib/routes` types and no longer match what `buildApiRoute` handlers and lifecycle hooks (`onConnect` / `onClose`) actually receive — the package's session has no `rooms` / `eventSchemas`, an optional `context`, adds `close()`, and its context has no `respond()` / `sendHeaders()`. When typing `buildApiRoute` sessions, contexts, or hooks explicitly, import these types from `@lokalise/fastify-api-contracts` directly; the root exports keep typing the legacy `AbstractSSEController` / `AbstractDualModeController` routes.
|
|
17
|
+
|
|
18
|
+
## 7.0.0
|
|
19
|
+
|
|
20
|
+
### Major Changes
|
|
21
|
+
|
|
22
|
+
- e3a05b6: Require `@lokalise/api-contracts` >= 7.0.0. The route builder no longer handles the legacy response entries removed in api-contracts 7 (`anyOfResponses`, `sseResponse`/`blobResponse`/`textResponse` tagged objects, `ContractNoBody` as a response) — declare responses with bare Zod schemas, `noBodyResponse()`, or content maps (`{ content: { 'text/event-stream': sseBody(...) } }`). Handler body types are now also inferred from content-map entries (JSON media types resolve to their Zod output type, blob to `Blob`, `allowNoBody` to `undefined`).
|
|
23
|
+
|
|
24
|
+
## 6.20.3
|
|
25
|
+
|
|
26
|
+
### Patch Changes
|
|
27
|
+
|
|
28
|
+
- 22a290b: Migrate release automation to Changesets.
|
|
@@ -20,9 +20,12 @@ import type { GatewayMetadataValue } from '../gateway/gatewayMetadata.ts';
|
|
|
20
20
|
* status: 200,
|
|
21
21
|
* body: { id: req.params.id },
|
|
22
22
|
* })),
|
|
23
|
-
* streamUpdates: buildApiRoute(
|
|
24
|
-
*
|
|
25
|
-
*
|
|
23
|
+
* streamUpdates: buildApiRoute(
|
|
24
|
+
* UserController.contracts.streamUpdates,
|
|
25
|
+
* async (_req, _reply, { sse }) => {
|
|
26
|
+
* sse.start('keepAlive')
|
|
27
|
+
* },
|
|
28
|
+
* ),
|
|
26
29
|
* }
|
|
27
30
|
* }
|
|
28
31
|
* ```
|
|
@@ -17,9 +17,12 @@
|
|
|
17
17
|
* status: 200,
|
|
18
18
|
* body: { id: req.params.id },
|
|
19
19
|
* })),
|
|
20
|
-
* streamUpdates: buildApiRoute(
|
|
21
|
-
*
|
|
22
|
-
*
|
|
20
|
+
* streamUpdates: buildApiRoute(
|
|
21
|
+
* UserController.contracts.streamUpdates,
|
|
22
|
+
* async (_req, _reply, { sse }) => {
|
|
23
|
+
* sse.start('keepAlive')
|
|
24
|
+
* },
|
|
25
|
+
* ),
|
|
23
26
|
* }
|
|
24
27
|
* }
|
|
25
28
|
* ```
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AbstractApiController.js","sourceRoot":"","sources":["../../../lib/api-contracts/AbstractApiController.ts"],"names":[],"mappings":"AAIA
|
|
1
|
+
{"version":3,"file":"AbstractApiController.js","sourceRoot":"","sources":["../../../lib/api-contracts/AbstractApiController.ts"],"names":[],"mappings":"AAIA;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,MAAM,OAAgB,qBAAqB;IAGzC;;;;;;;OAOG;IACa,eAAe,CAAuB;CACvD"}
|
|
@@ -1,23 +1,65 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type { ApiContract } from '@lokalise/api-contracts';
|
|
2
|
+
import { type ApiRouteOptions as FastifyApiRouteOptions, type InferApiHandler } from '@lokalise/fastify-api-contracts';
|
|
2
3
|
import type { RouteOptions } from 'fastify';
|
|
3
|
-
import type {
|
|
4
|
+
import type { GatewayMetadata } from '../gateway/gatewayTypes.ts';
|
|
5
|
+
/**
|
|
6
|
+
* Options for configuring an ApiContract route.
|
|
7
|
+
*
|
|
8
|
+
* All options from `@lokalise/fastify-api-contracts` (any Fastify route field
|
|
9
|
+
* minus the ones the contract provides, SSE lifecycle hooks, and
|
|
10
|
+
* `contractMetadataToRouteMapper`) pass through to `buildFastifyApiRoute`
|
|
11
|
+
* unchanged.
|
|
12
|
+
*
|
|
13
|
+
* Generic in `Contract` so `gatewayMetadata.match.headers` / `match.query`
|
|
14
|
+
* keys are narrowed to the contract's request schemas. The generic is always
|
|
15
|
+
* inferred from the contract argument at the `buildApiRoute` call site, so
|
|
16
|
+
* direct references should write `ApiRouteOptions<typeof myContract>` when
|
|
17
|
+
* gateway metadata typing is needed.
|
|
18
|
+
*/
|
|
19
|
+
export type ApiRouteOptions<Contract extends ApiContract> = FastifyApiRouteOptions & {
|
|
20
|
+
/**
|
|
21
|
+
* Per-route gateway metadata. `match.headers` / `match.query` keys are
|
|
22
|
+
* narrowed to the contract's request schemas; `customHeaders` /
|
|
23
|
+
* `customQuery` remain the escape hatch for headers and params not
|
|
24
|
+
* declared on the contract. Validated at runtime against the same Zod
|
|
25
|
+
* schema used by `withGatewayMetadata` and stamped on the route via the
|
|
26
|
+
* shared `GATEWAY_METADATA_SYMBOL`.
|
|
27
|
+
*
|
|
28
|
+
* Equivalent to wrapping the result with `withGatewayMetadata` — keep
|
|
29
|
+
* to one form per route. If both are used on the same route, the later
|
|
30
|
+
* call (typically `withGatewayMetadata`) overwrites the inline value;
|
|
31
|
+
* there is no merge.
|
|
32
|
+
*
|
|
33
|
+
* @example
|
|
34
|
+
* ```ts
|
|
35
|
+
* buildApiRoute(MyController.contracts.getItem, this.getItem, {
|
|
36
|
+
* gatewayMetadata: {
|
|
37
|
+
* cache: { ttl: '60s' },
|
|
38
|
+
* match: {
|
|
39
|
+
* // narrowed to keys of the contract's requestHeaderSchema:
|
|
40
|
+
* headers: { 'x-trace-id': { regex: '^[a-f0-9]+$' } },
|
|
41
|
+
* // escape hatch for headers not declared on the contract:
|
|
42
|
+
* customHeaders: { 'x-tenant-id': { regex: '^t_' } },
|
|
43
|
+
* },
|
|
44
|
+
* },
|
|
45
|
+
* })
|
|
46
|
+
* ```
|
|
47
|
+
*/
|
|
48
|
+
gatewayMetadata?: GatewayMetadata<Contract>;
|
|
49
|
+
};
|
|
4
50
|
/**
|
|
5
51
|
* Build a Fastify `RouteOptions` object from an `ApiContract` + handler.
|
|
6
52
|
*
|
|
7
|
-
*
|
|
8
|
-
* -
|
|
9
|
-
*
|
|
10
|
-
*
|
|
53
|
+
* Thin wrapper around `buildFastifyApiRoute` from
|
|
54
|
+
* `@lokalise/fastify-api-contracts` — the handler shape, response mode
|
|
55
|
+
* inference, SSE streaming, and validation semantics are all the package's.
|
|
56
|
+
* See its docs for the `(request, reply, context) => { status, body }`
|
|
57
|
+
* handler model and `context.sse` streaming.
|
|
11
58
|
*
|
|
12
|
-
*
|
|
13
|
-
* -
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
* `heartbeatInterval`) — applied for `'sse'` and `'dual'` contracts only,
|
|
17
|
-
* - `defaultMode` for `'dual'` contracts when the `Accept` header is ambiguous,
|
|
18
|
-
* - `gatewayMetadata` — per-route gateway policy with header / query keys
|
|
19
|
-
* narrowed to the contract; equivalent to wrapping the result with
|
|
20
|
-
* `withGatewayMetadata`. See `ApiRouteOptions` for full details.
|
|
59
|
+
* On top of the package builder this adds one option: `gatewayMetadata` —
|
|
60
|
+
* per-route gateway policy with header / query keys narrowed to the
|
|
61
|
+
* contract; equivalent to wrapping the result with `withGatewayMetadata`.
|
|
62
|
+
* See `ApiRouteOptions` for full details.
|
|
21
63
|
*
|
|
22
64
|
* @returns Fastify `RouteOptions` ready to pass to `app.route()`
|
|
23
65
|
*/
|
|
@@ -1,364 +1,25 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { ContractNoBody, getSseSchemaByEventName, hasAnySuccessSseResponse, isAnyOfResponses, isBlobResponse, isContentResponseEntry, isSseBody, isSseResponse, isTextResponse, mapApiContractToPath, resolveContractResponse, SUCCESSFUL_HTTP_STATUS_CODES, } from '@lokalise/api-contracts';
|
|
3
|
-
import { InternalError } from '@lokalise/node-core';
|
|
4
|
-
import { isErrorLike } from "../errorUtils.js";
|
|
1
|
+
import { buildFastifyApiRoute, } from '@lokalise/fastify-api-contracts';
|
|
5
2
|
import { attachGatewayMetadata } from "../gateway/withGatewayMetadata.js";
|
|
6
|
-
import { determineMode, hasHttpStatusCode } from "../routes/fastifyRouteUtils.js";
|
|
7
|
-
function isSuccessResponseDual(value) {
|
|
8
|
-
if (isContentResponseEntry(value)) {
|
|
9
|
-
// A content-map entry offers a non-SSE representation when it allows an empty
|
|
10
|
-
// body or declares any non-SSE media type descriptor.
|
|
11
|
-
if (value.allowNoBody || !value.content)
|
|
12
|
-
return true;
|
|
13
|
-
return Object.values(value.content).some((descriptor) => !isSseBody(descriptor));
|
|
14
|
-
}
|
|
15
|
-
if (value === ContractNoBody || isTextResponse(value) || isBlobResponse(value))
|
|
16
|
-
return true;
|
|
17
|
-
if (!isSseResponse(value) && !isAnyOfResponses(value))
|
|
18
|
-
return true;
|
|
19
|
-
if (isAnyOfResponses(value)) {
|
|
20
|
-
return value.responses.some((response) => !isSseResponse(response));
|
|
21
|
-
}
|
|
22
|
-
return false;
|
|
23
|
-
}
|
|
24
|
-
function getContractResponseMode(contract) {
|
|
25
|
-
if (!hasAnySuccessSseResponse(contract))
|
|
26
|
-
return 'non-sse';
|
|
27
|
-
for (const code of SUCCESSFUL_HTTP_STATUS_CODES) {
|
|
28
|
-
const value = contract.responsesByStatusCode[code];
|
|
29
|
-
if (value && isSuccessResponseDual(value))
|
|
30
|
-
return 'dual';
|
|
31
|
-
}
|
|
32
|
-
return 'sse';
|
|
33
|
-
}
|
|
34
|
-
function buildSSERouteConfig(options) {
|
|
35
|
-
if (!options?.serializer && options?.heartbeatInterval === undefined)
|
|
36
|
-
return true;
|
|
37
|
-
const sseConfig = {};
|
|
38
|
-
if (options.serializer)
|
|
39
|
-
sseConfig.serializer = options.serializer;
|
|
40
|
-
if (options.heartbeatInterval !== undefined)
|
|
41
|
-
sseConfig.heartbeatInterval = options.heartbeatInterval;
|
|
42
|
-
return sseConfig;
|
|
43
|
-
}
|
|
44
|
-
// ============================================================================
|
|
45
|
-
// Internal Helpers — Sync Route
|
|
46
|
-
// ============================================================================
|
|
47
|
-
function getSchemaForStatusCode(contract, status) {
|
|
48
|
-
const entry = contract.responsesByStatusCode[status];
|
|
49
|
-
if (!entry)
|
|
50
|
-
return null;
|
|
51
|
-
// Resolve the JSON representation for this status code, covering both legacy
|
|
52
|
-
// response entries (bare Zod schema, anyOfResponses, …) and the new content-map
|
|
53
|
-
// entries. Non-JSON responses (text, blob, SSE, no-body) are not validated here.
|
|
54
|
-
const resolved = resolveContractResponse(entry, 'application/json', false);
|
|
55
|
-
return resolved?.kind === 'json' ? resolved.schema : null;
|
|
56
|
-
}
|
|
57
|
-
function validateApiResponseHeaders(contract, reply) {
|
|
58
|
-
const schema = contract.responseHeaderSchema;
|
|
59
|
-
if (!schema) {
|
|
60
|
-
return;
|
|
61
|
-
}
|
|
62
|
-
const result = schema.safeParse(reply.getHeaders());
|
|
63
|
-
if (!result.success) {
|
|
64
|
-
throw new InternalError({
|
|
65
|
-
message: 'Internal Server Error',
|
|
66
|
-
errorCode: 'RESPONSE_HEADERS_VALIDATION_FAILED',
|
|
67
|
-
details: { validationError: result.error.message },
|
|
68
|
-
});
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
async function handleApiSyncRoute(contract,
|
|
72
|
-
// biome-ignore lint/suspicious/noExplicitAny: Handler types are validated by InferApiHandler at the call site
|
|
73
|
-
handler,
|
|
74
|
-
// biome-ignore lint/suspicious/noExplicitAny: Request types are validated by Fastify schema
|
|
75
|
-
request, reply) {
|
|
76
|
-
const { status, body } = await handler(request, reply);
|
|
77
|
-
if (reply.sent) {
|
|
78
|
-
request.log.warn({
|
|
79
|
-
msg: 'Sync handler sent response directly, bypassing response validation',
|
|
80
|
-
tag: 'response_sent_directly',
|
|
81
|
-
method: request.method,
|
|
82
|
-
url: request.url,
|
|
83
|
-
});
|
|
84
|
-
return;
|
|
85
|
-
}
|
|
86
|
-
try {
|
|
87
|
-
const schema = getSchemaForStatusCode(contract, status);
|
|
88
|
-
if (schema) {
|
|
89
|
-
const result = schema.safeParse(body);
|
|
90
|
-
if (!result.success) {
|
|
91
|
-
throw new InternalError({
|
|
92
|
-
message: 'Internal Server Error',
|
|
93
|
-
errorCode: 'RESPONSE_VALIDATION_FAILED',
|
|
94
|
-
details: { validationError: result.error.message },
|
|
95
|
-
});
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
catch (err) {
|
|
100
|
-
reply.code(500);
|
|
101
|
-
throw err;
|
|
102
|
-
}
|
|
103
|
-
validateApiResponseHeaders(contract, reply);
|
|
104
|
-
if (!reply.hasHeader('content-type')) {
|
|
105
|
-
reply.type('application/json');
|
|
106
|
-
}
|
|
107
|
-
return reply.code(status).send(body);
|
|
108
|
-
}
|
|
109
|
-
// ============================================================================
|
|
110
|
-
// Internal Helpers — SSE Route (no controller, uses reply.sse directly)
|
|
111
|
-
// ============================================================================
|
|
112
|
-
function buildApiSSEContext(
|
|
113
|
-
// biome-ignore lint/suspicious/noExplicitAny: Request types are validated by Fastify schema
|
|
114
|
-
request, reply, eventSchemas, options) {
|
|
115
|
-
let started = false;
|
|
116
|
-
let responseData;
|
|
117
|
-
const sseReply = reply;
|
|
118
|
-
const sseContext = {
|
|
119
|
-
start: (mode, startOptions) => {
|
|
120
|
-
started = true;
|
|
121
|
-
if (mode === 'keepAlive') {
|
|
122
|
-
sseReply.sse.keepAlive();
|
|
123
|
-
}
|
|
124
|
-
// sendHeaders() calls writeHead(200) but only queues headers in the buffer.
|
|
125
|
-
// flushHeaders() forces them onto the wire so the client's fetch() returns.
|
|
126
|
-
sseReply.sse.sendHeaders();
|
|
127
|
-
reply.raw.flushHeaders();
|
|
128
|
-
const connectionId = randomUUID();
|
|
129
|
-
const send = async (eventName, data, sendOptions) => {
|
|
130
|
-
const schema = eventSchemas[eventName];
|
|
131
|
-
if (schema) {
|
|
132
|
-
const result = schema.safeParse(data);
|
|
133
|
-
if (!result.success) {
|
|
134
|
-
throw new InternalError({
|
|
135
|
-
message: `SSE event validation failed for event "${eventName}": ${result.error.message}`,
|
|
136
|
-
errorCode: 'RESPONSE_VALIDATION_FAILED',
|
|
137
|
-
});
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
try {
|
|
141
|
-
await sseReply.sse.send({
|
|
142
|
-
event: eventName,
|
|
143
|
-
data,
|
|
144
|
-
id: sendOptions?.id,
|
|
145
|
-
retry: sendOptions?.retry,
|
|
146
|
-
});
|
|
147
|
-
return true;
|
|
148
|
-
}
|
|
149
|
-
catch {
|
|
150
|
-
return false;
|
|
151
|
-
}
|
|
152
|
-
};
|
|
153
|
-
const session = {
|
|
154
|
-
id: connectionId,
|
|
155
|
-
request,
|
|
156
|
-
reply,
|
|
157
|
-
context: (startOptions?.context ?? {}),
|
|
158
|
-
connectedAt: new Date(),
|
|
159
|
-
// biome-ignore lint/suspicious/noExplicitAny: SSEEventSender generic is satisfied at handler call site
|
|
160
|
-
send: send,
|
|
161
|
-
isConnected: () => sseReply.sse.isConnected,
|
|
162
|
-
getStream: () => sseReply.sse.stream(),
|
|
163
|
-
sendStream: async (messages) => {
|
|
164
|
-
for await (const message of messages) {
|
|
165
|
-
await send(message.event, message.data, { id: message.id, retry: message.retry });
|
|
166
|
-
}
|
|
167
|
-
},
|
|
168
|
-
rooms: { join: () => { }, leave: () => { } },
|
|
169
|
-
eventSchemas,
|
|
170
|
-
};
|
|
171
|
-
if (options?.onConnect) {
|
|
172
|
-
void Promise.resolve(options.onConnect(session)).catch(() => { });
|
|
173
|
-
}
|
|
174
|
-
if (options?.onClose) {
|
|
175
|
-
const onClose = options.onClose;
|
|
176
|
-
sseReply.sse.onClose(() => {
|
|
177
|
-
void Promise.resolve(onClose(session, 'client')).catch(() => { });
|
|
178
|
-
});
|
|
179
|
-
}
|
|
180
|
-
if (options?.onReconnect && sseReply.sse.lastEventId) {
|
|
181
|
-
const onReconnect = options.onReconnect;
|
|
182
|
-
const lastEventId = sseReply.sse.lastEventId;
|
|
183
|
-
void sseReply.sse.replay(async () => {
|
|
184
|
-
const replay = await onReconnect(session, lastEventId);
|
|
185
|
-
if (replay) {
|
|
186
|
-
for await (const msg of replay) {
|
|
187
|
-
await sseReply.sse.send(msg);
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
});
|
|
191
|
-
}
|
|
192
|
-
return session;
|
|
193
|
-
},
|
|
194
|
-
respond: ((code, body) => {
|
|
195
|
-
if (started) {
|
|
196
|
-
throw new Error('Cannot call sse.respond() after sse.start() — the SSE stream is already open.');
|
|
197
|
-
}
|
|
198
|
-
responseData = { code, body };
|
|
199
|
-
return { _type: 'respond', code, body };
|
|
200
|
-
// biome-ignore lint/suspicious/noExplicitAny: respond typing is enforced by contract at call site
|
|
201
|
-
}),
|
|
202
|
-
sendHeaders: () => {
|
|
203
|
-
sseReply.sse.sendHeaders();
|
|
204
|
-
},
|
|
205
|
-
reply,
|
|
206
|
-
};
|
|
207
|
-
return {
|
|
208
|
-
sseContext,
|
|
209
|
-
isStarted: () => started,
|
|
210
|
-
hasResponse: () => responseData !== undefined,
|
|
211
|
-
getResponseData: () => responseData,
|
|
212
|
-
};
|
|
213
|
-
}
|
|
214
|
-
// biome-ignore lint/complexity/noExcessiveCognitiveComplexity: Core SSE handler coordinates context, error handling, and lifecycle
|
|
215
|
-
async function handleApiSseRoute(
|
|
216
|
-
// biome-ignore lint/suspicious/noExplicitAny: SSE handler types are validated by InferApiHandler at call site
|
|
217
|
-
sseHandler, eventSchemas, options,
|
|
218
|
-
// biome-ignore lint/suspicious/noExplicitAny: Request types are validated by Fastify schema
|
|
219
|
-
request, reply) {
|
|
220
|
-
const { sseContext, isStarted, hasResponse, getResponseData } = buildApiSSEContext(request, reply, eventSchemas, options);
|
|
221
|
-
try {
|
|
222
|
-
await sseHandler(request, sseContext);
|
|
223
|
-
if (!isStarted() && !hasResponse()) {
|
|
224
|
-
throw new Error('SSE handler must either send a response (sse.respond()) ' +
|
|
225
|
-
'or start streaming (sse.start()). Handler returned without doing either.');
|
|
226
|
-
}
|
|
227
|
-
const responseData = getResponseData();
|
|
228
|
-
if (responseData) {
|
|
229
|
-
// Early HTTP response (sse.respond() was called before streaming)
|
|
230
|
-
reply.removeHeader('cache-control');
|
|
231
|
-
reply.removeHeader('x-accel-buffering');
|
|
232
|
-
reply.type('application/json').code(responseData.code).send(responseData.body);
|
|
233
|
-
}
|
|
234
|
-
// If started, @fastify/sse manages the rest of the connection lifecycle
|
|
235
|
-
}
|
|
236
|
-
catch (err) {
|
|
237
|
-
if (isStarted()) {
|
|
238
|
-
// Headers already sent — can't change status code; try to send error event
|
|
239
|
-
const sseReply = reply;
|
|
240
|
-
if (sseReply.sse.isConnected) {
|
|
241
|
-
try {
|
|
242
|
-
await sseReply.sse.send({
|
|
243
|
-
event: 'error',
|
|
244
|
-
data: { message: isErrorLike(err) ? err.message : 'Internal Server Error' },
|
|
245
|
-
});
|
|
246
|
-
}
|
|
247
|
-
catch {
|
|
248
|
-
// Ignore send failures during error handling
|
|
249
|
-
}
|
|
250
|
-
}
|
|
251
|
-
throw err;
|
|
252
|
-
}
|
|
253
|
-
// Streaming not started — send HTTP error response
|
|
254
|
-
const message = isErrorLike(err) ? err.message : 'Internal Server Error';
|
|
255
|
-
const statusCode = hasHttpStatusCode(err) ? err.httpStatusCode : 500;
|
|
256
|
-
const statusText = statusCode >= 500 ? 'Internal Server Error' : 'Error';
|
|
257
|
-
reply.code(statusCode).type('application/json').send({ statusCode, error: statusText, message });
|
|
258
|
-
}
|
|
259
|
-
}
|
|
260
|
-
// ============================================================================
|
|
261
|
-
// Internal Helpers — Schema
|
|
262
|
-
// ============================================================================
|
|
263
|
-
function buildResponseSchemas(contract) {
|
|
264
|
-
return Object.keys(contract.responsesByStatusCode).reduce((acc, statusCode) => {
|
|
265
|
-
const schema = getSchemaForStatusCode(contract, Number(statusCode));
|
|
266
|
-
if (schema) {
|
|
267
|
-
acc[Number(statusCode)] = schema;
|
|
268
|
-
}
|
|
269
|
-
return acc;
|
|
270
|
-
}, {});
|
|
271
|
-
}
|
|
272
|
-
function buildBaseSchema(contract) {
|
|
273
|
-
const schema = {};
|
|
274
|
-
if (contract.requestPathParamsSchema)
|
|
275
|
-
schema.params = contract.requestPathParamsSchema;
|
|
276
|
-
if (contract.requestQuerySchema)
|
|
277
|
-
schema.querystring = contract.requestQuerySchema;
|
|
278
|
-
if (contract.requestHeaderSchema)
|
|
279
|
-
schema.headers = contract.requestHeaderSchema;
|
|
280
|
-
if (contract.requestBodySchema !== undefined && contract.requestBodySchema !== ContractNoBody) {
|
|
281
|
-
schema.body = contract.requestBodySchema;
|
|
282
|
-
}
|
|
283
|
-
schema.response = buildResponseSchemas(contract);
|
|
284
|
-
return schema;
|
|
285
|
-
}
|
|
286
|
-
// ============================================================================
|
|
287
|
-
// Public API
|
|
288
|
-
// ============================================================================
|
|
289
3
|
/**
|
|
290
4
|
* Build a Fastify `RouteOptions` object from an `ApiContract` + handler.
|
|
291
5
|
*
|
|
292
|
-
*
|
|
293
|
-
* -
|
|
294
|
-
*
|
|
295
|
-
*
|
|
6
|
+
* Thin wrapper around `buildFastifyApiRoute` from
|
|
7
|
+
* `@lokalise/fastify-api-contracts` — the handler shape, response mode
|
|
8
|
+
* inference, SSE streaming, and validation semantics are all the package's.
|
|
9
|
+
* See its docs for the `(request, reply, context) => { status, body }`
|
|
10
|
+
* handler model and `context.sse` streaming.
|
|
296
11
|
*
|
|
297
|
-
*
|
|
298
|
-
* -
|
|
299
|
-
*
|
|
300
|
-
*
|
|
301
|
-
* `heartbeatInterval`) — applied for `'sse'` and `'dual'` contracts only,
|
|
302
|
-
* - `defaultMode` for `'dual'` contracts when the `Accept` header is ambiguous,
|
|
303
|
-
* - `gatewayMetadata` — per-route gateway policy with header / query keys
|
|
304
|
-
* narrowed to the contract; equivalent to wrapping the result with
|
|
305
|
-
* `withGatewayMetadata`. See `ApiRouteOptions` for full details.
|
|
12
|
+
* On top of the package builder this adds one option: `gatewayMetadata` —
|
|
13
|
+
* per-route gateway policy with header / query keys narrowed to the
|
|
14
|
+
* contract; equivalent to wrapping the result with `withGatewayMetadata`.
|
|
15
|
+
* See `ApiRouteOptions` for full details.
|
|
306
16
|
*
|
|
307
17
|
* @returns Fastify `RouteOptions` ready to pass to `app.route()`
|
|
308
18
|
*/
|
|
309
19
|
export function buildApiRoute(contract, handler, options) {
|
|
310
|
-
//
|
|
311
|
-
|
|
312
|
-
const
|
|
313
|
-
|
|
314
|
-
const mode = getContractResponseMode(contract);
|
|
315
|
-
const eventSchemas = getSseSchemaByEventName(contract) ?? {};
|
|
316
|
-
const baseSchema = buildBaseSchema(contract);
|
|
317
|
-
const contractMetadata = contractMetadataToRouteMapper?.(contract.metadata) ?? {};
|
|
318
|
-
const finalize = (route) => gatewayMetadata !== undefined ? attachGatewayMetadata(route, gatewayMetadata) : route;
|
|
319
|
-
if (mode === 'non-sse') {
|
|
320
|
-
// biome-ignore lint/suspicious/noExplicitAny: handler shape validated by InferApiHandler at call site
|
|
321
|
-
const syncHandler = handler;
|
|
322
|
-
return finalize({
|
|
323
|
-
...fastifyOptions,
|
|
324
|
-
...contractMetadata,
|
|
325
|
-
method: contract.method,
|
|
326
|
-
url,
|
|
327
|
-
schema: baseSchema,
|
|
328
|
-
handler: async (request, reply) => handleApiSyncRoute(contract, syncHandler, request, reply),
|
|
329
|
-
});
|
|
330
|
-
}
|
|
331
|
-
if (mode === 'dual') {
|
|
332
|
-
const resolvedDefaultMode = defaultMode ?? 'json';
|
|
333
|
-
// biome-ignore lint/suspicious/noExplicitAny: handler shape validated by InferApiHandler at call site
|
|
334
|
-
const dualHandlers = handler;
|
|
335
|
-
return finalize({
|
|
336
|
-
...fastifyOptions,
|
|
337
|
-
...contractMetadata,
|
|
338
|
-
method: contract.method,
|
|
339
|
-
url,
|
|
340
|
-
sse: buildSSERouteConfig(options),
|
|
341
|
-
schema: baseSchema,
|
|
342
|
-
handler: (request, reply) => {
|
|
343
|
-
const responseMode = determineMode(request.headers.accept, resolvedDefaultMode);
|
|
344
|
-
if (responseMode === 'json') {
|
|
345
|
-
return handleApiSyncRoute(contract, dualHandlers.nonSse, request, reply);
|
|
346
|
-
}
|
|
347
|
-
return handleApiSseRoute(dualHandlers.sse, eventSchemas, options, request, reply);
|
|
348
|
-
},
|
|
349
|
-
});
|
|
350
|
-
}
|
|
351
|
-
// SSE-only
|
|
352
|
-
// biome-ignore lint/suspicious/noExplicitAny: handler shape validated by InferApiHandler at call site
|
|
353
|
-
const sseHandler = handler;
|
|
354
|
-
return finalize({
|
|
355
|
-
...fastifyOptions,
|
|
356
|
-
...contractMetadata,
|
|
357
|
-
method: contract.method,
|
|
358
|
-
url,
|
|
359
|
-
sse: buildSSERouteConfig(options),
|
|
360
|
-
schema: baseSchema,
|
|
361
|
-
handler: async (request, reply) => handleApiSseRoute(sseHandler, eventSchemas, options, request, reply),
|
|
362
|
-
});
|
|
20
|
+
// Gateway metadata is stamped via Symbol, not spread into Fastify options.
|
|
21
|
+
const { gatewayMetadata, ...fastifyOptions } = options ?? {};
|
|
22
|
+
const route = buildFastifyApiRoute(contract, handler, fastifyOptions);
|
|
23
|
+
return gatewayMetadata !== undefined ? attachGatewayMetadata(route, gatewayMetadata) : route;
|
|
363
24
|
}
|
|
364
25
|
//# sourceMappingURL=apiRouteBuilder.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"apiRouteBuilder.js","sourceRoot":"","sources":["../../../lib/api-contracts/apiRouteBuilder.ts"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"apiRouteBuilder.js","sourceRoot":"","sources":["../../../lib/api-contracts/apiRouteBuilder.ts"],"names":[],"mappings":"AACA,OAAO,EACL,oBAAoB,GAGrB,MAAM,iCAAiC,CAAA;AAGxC,OAAO,EAAE,qBAAqB,EAAE,MAAM,mCAAmC,CAAA;AAgDzE;;;;;;;;;;;;;;;GAeG;AACH,MAAM,UAAU,aAAa,CAC3B,QAAkB,EAClB,OAAkC,EAClC,OAAmC;IAEnC,2EAA2E;IAC3E,MAAM,EAAE,eAAe,EAAE,GAAG,cAAc,EAAE,GAAG,OAAO,IAAI,EAAE,CAAA;IAC5D,MAAM,KAAK,GAAG,oBAAoB,CAAC,QAAQ,EAAE,OAAO,EAAE,cAAc,CAAC,CAAA;IACrE,OAAO,eAAe,KAAK,SAAS,CAAC,CAAC,CAAC,qBAAqB,CAAC,KAAK,EAAE,eAAe,CAAC,CAAC,CAAC,CAAC,KAAK,CAAA;AAC9F,CAAC"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
+
export type { ApiHandlerContext, ApiHandlerReply, InferApiHandler, InferApiHandlerRequest, InferApiHandlerResult, InferContractResponseContentTypes, } from '@lokalise/fastify-api-contracts';
|
|
1
2
|
export { AbstractApiController } from './AbstractApiController.ts';
|
|
2
|
-
export
|
|
3
|
-
export { buildApiRoute } from './apiRouteBuilder.ts';
|
|
3
|
+
export { type ApiRouteOptions, buildApiRoute } from './apiRouteBuilder.ts';
|
|
4
4
|
export { asApiControllerClass } from './asApiControllerClass.ts';
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../lib/api-contracts/index.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../lib/api-contracts/index.ts"],"names":[],"mappings":"AAUA,OAAO,EAAE,qBAAqB,EAAE,MAAM,4BAA4B,CAAA;AAClE,OAAO,EAAwB,aAAa,EAAE,MAAM,sBAAsB,CAAA;AAC1E,OAAO,EAAE,oBAAoB,EAAE,MAAM,2BAA2B,CAAA"}
|
package/package.json
CHANGED
|
@@ -1,90 +1,86 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
"
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
"
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
"
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
"
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
"
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
"
|
|
73
|
-
"
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
"
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
"LICENSE",
|
|
88
|
-
"dist/*"
|
|
89
|
-
]
|
|
90
|
-
}
|
|
2
|
+
"name": "opinionated-machine",
|
|
3
|
+
"version": "8.0.0",
|
|
4
|
+
"description": "Very opinionated DI framework for fastify, built on top of awilix ",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"main": "dist/index.js",
|
|
8
|
+
"types": "dist/index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": "./dist/index.js",
|
|
11
|
+
"./package.json": "./package.json"
|
|
12
|
+
},
|
|
13
|
+
"maintainers": [
|
|
14
|
+
{
|
|
15
|
+
"name": "Igor Savin",
|
|
16
|
+
"email": "kibertoad@gmail.com"
|
|
17
|
+
}
|
|
18
|
+
],
|
|
19
|
+
"repository": {
|
|
20
|
+
"type": "git",
|
|
21
|
+
"url": "git+https://github.com/kibertoad/opinionated-machine.git"
|
|
22
|
+
},
|
|
23
|
+
"dependencies": {
|
|
24
|
+
"@fastify/sse": "^0.6.0",
|
|
25
|
+
"fast-querystring": "^1.1.2",
|
|
26
|
+
"fastify-plugin": "^6.0.0",
|
|
27
|
+
"ts-deepmerge": "^8.0.0"
|
|
28
|
+
},
|
|
29
|
+
"peerDependencies": {
|
|
30
|
+
"@lokalise/api-contracts": ">=7.0.0",
|
|
31
|
+
"@lokalise/fastify-api-contracts": ">=6.0.0",
|
|
32
|
+
"@lokalise/node-core": ">=14.7.4",
|
|
33
|
+
"awilix": ">=13.0.0",
|
|
34
|
+
"awilix-manager": ">=6.0.0",
|
|
35
|
+
"fastify": ">=5.0.0",
|
|
36
|
+
"fastify-type-provider-zod": ">=7.0.0",
|
|
37
|
+
"zod": ">=4.1.12"
|
|
38
|
+
},
|
|
39
|
+
"devDependencies": {
|
|
40
|
+
"@biomejs/biome": "2.4.4",
|
|
41
|
+
"@changesets/cli": "^3.0.0",
|
|
42
|
+
"@lokalise/api-contracts": "^7.0.0",
|
|
43
|
+
"@lokalise/biome-config": "^3.1.1",
|
|
44
|
+
"@lokalise/fastify-api-contracts": "^6.0.0",
|
|
45
|
+
"@lokalise/node-core": "^14.7.4",
|
|
46
|
+
"@lokalise/tsconfig": "^3.1.0",
|
|
47
|
+
"@types/node": "^22.19.7",
|
|
48
|
+
"@vitest/coverage-v8": "^4.0.18",
|
|
49
|
+
"awilix": "^13.0.0",
|
|
50
|
+
"awilix-manager": "^7.0.0",
|
|
51
|
+
"fastify": "^5.7.4",
|
|
52
|
+
"fastify-type-provider-zod": "^7.0.0",
|
|
53
|
+
"rimraf": "^6.1.2",
|
|
54
|
+
"typescript": "^5.9.3",
|
|
55
|
+
"vitest": "^4.0.18",
|
|
56
|
+
"zod": "^4.3.6"
|
|
57
|
+
},
|
|
58
|
+
"private": false,
|
|
59
|
+
"publishConfig": {
|
|
60
|
+
"access": "public"
|
|
61
|
+
},
|
|
62
|
+
"keywords": [
|
|
63
|
+
"dependency",
|
|
64
|
+
"injection",
|
|
65
|
+
"opinionated",
|
|
66
|
+
"awilix",
|
|
67
|
+
"di",
|
|
68
|
+
"fastify"
|
|
69
|
+
],
|
|
70
|
+
"homepage": "https://github.com/kibertoad/opinionated-machine",
|
|
71
|
+
"files": [
|
|
72
|
+
"README.md",
|
|
73
|
+
"CHANGELOG.md",
|
|
74
|
+
"LICENSE",
|
|
75
|
+
"dist/*"
|
|
76
|
+
],
|
|
77
|
+
"scripts": {
|
|
78
|
+
"build": "rimraf dist && tsc -p tsconfig.build.json",
|
|
79
|
+
"lint": "biome check . && tsc",
|
|
80
|
+
"lint:fix": "biome check --write .",
|
|
81
|
+
"test": "vitest --typecheck",
|
|
82
|
+
"test:ci": "pnpm run test --coverage",
|
|
83
|
+
"changeset": "changeset",
|
|
84
|
+
"ci:publish": "changeset publish"
|
|
85
|
+
}
|
|
86
|
+
}
|
|
@@ -1,159 +0,0 @@
|
|
|
1
|
-
import type { ApiContract, ContractNoBody, ContractResponseMode, InferSseSuccessResponses, PayloadApiContract, SSEEventSchemas } from '@lokalise/api-contracts';
|
|
2
|
-
import type { FastifyRequest, RouteOptions } from 'fastify';
|
|
3
|
-
import type { z } from 'zod/v4';
|
|
4
|
-
import type { DualModeType } from '../dualmode/dualModeTypes.ts';
|
|
5
|
-
import type { GatewayMetadata } from '../gateway/gatewayTypes.ts';
|
|
6
|
-
import type { FastifySSERouteOptions, SSEContext, SSEHandlerResult, SyncModeReply } from '../routes/fastifyRouteTypes.ts';
|
|
7
|
-
type NonSseBodyEntry<T> = T extends undefined ? never : T extends {
|
|
8
|
-
_tag: 'SseResponse';
|
|
9
|
-
} ? never : T extends {
|
|
10
|
-
_tag: 'BlobResponse';
|
|
11
|
-
} ? Blob : T extends {
|
|
12
|
-
_tag: 'TextResponse';
|
|
13
|
-
} ? string : T extends {
|
|
14
|
-
_tag: 'AnyOfResponses';
|
|
15
|
-
responses: Array<infer R>;
|
|
16
|
-
} ? NonSseBodyEntry<R> : T extends z.ZodType ? z.output<T> : undefined;
|
|
17
|
-
/**
|
|
18
|
-
* Discriminated union of `{ status, body }` pairs for all non-SSE responses in a contract.
|
|
19
|
-
*
|
|
20
|
-
* Allows non-SSE handlers to return a specific status code and body together without
|
|
21
|
-
* calling `reply.code()` separately.
|
|
22
|
-
*
|
|
23
|
-
* @example
|
|
24
|
-
* ```typescript
|
|
25
|
-
* async (request) => {
|
|
26
|
-
* if (!valid) return { status: 400, body: { error: 'Bad Request' } }
|
|
27
|
-
* return { id: request.params.id }
|
|
28
|
-
* }
|
|
29
|
-
* ```
|
|
30
|
-
*/
|
|
31
|
-
export type InferApiStatusResponse<Contract extends ApiContract> = {
|
|
32
|
-
[K in keyof Contract['responsesByStatusCode']]: NonSseBodyEntry<Contract['responsesByStatusCode'][K]> extends never ? never : {
|
|
33
|
-
status: K;
|
|
34
|
-
body: NonSseBodyEntry<Contract['responsesByStatusCode'][K]>;
|
|
35
|
-
};
|
|
36
|
-
}[keyof Contract['responsesByStatusCode']];
|
|
37
|
-
type InferOptSchema<T, Fallback = unknown> = NonNullable<T> extends z.ZodType ? z.output<NonNullable<T>> : Fallback;
|
|
38
|
-
type InferApiBodyType<Contract extends ApiContract> = Contract extends PayloadApiContract ? Contract['requestBodySchema'] extends typeof ContractNoBody ? undefined : NonNullable<Contract['requestBodySchema']> extends z.ZodType ? z.output<NonNullable<Contract['requestBodySchema']>> : undefined : undefined;
|
|
39
|
-
/**
|
|
40
|
-
* Infer the FastifyRequest type from an ApiContract.
|
|
41
|
-
*
|
|
42
|
-
* Provides properly typed params, querystring, headers, and body.
|
|
43
|
-
*
|
|
44
|
-
* @example
|
|
45
|
-
* ```typescript
|
|
46
|
-
* const handler = async (request: InferApiRequest<typeof myContract>) => {
|
|
47
|
-
* request.params.userId // typed
|
|
48
|
-
* request.body.name // typed
|
|
49
|
-
* }
|
|
50
|
-
* ```
|
|
51
|
-
*/
|
|
52
|
-
export type InferApiRequest<Contract extends ApiContract> = FastifyRequest<{
|
|
53
|
-
Params: InferOptSchema<Contract['requestPathParamsSchema']>;
|
|
54
|
-
Querystring: InferOptSchema<Contract['requestQuerySchema']>;
|
|
55
|
-
Headers: InferOptSchema<Contract['requestHeaderSchema']>;
|
|
56
|
-
Body: InferApiBodyType<Contract>;
|
|
57
|
-
}>;
|
|
58
|
-
/**
|
|
59
|
-
* Handler for non-SSE responses from an ApiContract.
|
|
60
|
-
*
|
|
61
|
-
* Always return `{ status, body }` — the framework validates the body against the
|
|
62
|
-
* contract's schema for that status code and sends it.
|
|
63
|
-
*
|
|
64
|
-
* Use `reply.header()` to set response headers when needed.
|
|
65
|
-
*
|
|
66
|
-
* @example
|
|
67
|
-
* ```typescript
|
|
68
|
-
* async (request) => ({ status: 200, body: { id: request.params.userId } })
|
|
69
|
-
* ```
|
|
70
|
-
*
|
|
71
|
-
* @example With multiple status codes
|
|
72
|
-
* ```typescript
|
|
73
|
-
* async (request) => {
|
|
74
|
-
* if (!valid) return { status: 400, body: { error: 'Bad Request' } }
|
|
75
|
-
* return { status: 200, body: { id: request.params.userId } }
|
|
76
|
-
* }
|
|
77
|
-
* ```
|
|
78
|
-
*/
|
|
79
|
-
export type ApiNonSseHandler<Contract extends ApiContract> = (request: InferApiRequest<Contract>, reply: SyncModeReply) => InferApiStatusResponse<Contract> | Promise<InferApiStatusResponse<Contract>>;
|
|
80
|
-
/**
|
|
81
|
-
* Handler for SSE responses from an ApiContract.
|
|
82
|
-
*
|
|
83
|
-
* Call `sse.start(mode)` to begin streaming or `sse.respond(code, body)` for
|
|
84
|
-
* early HTTP returns before streaming starts.
|
|
85
|
-
*/
|
|
86
|
-
export type ApiSseHandler<Contract extends ApiContract> = (request: InferApiRequest<Contract>, sse: SSEContext<EnsureSseEventSchemas<InferSseSuccessResponses<Contract['responsesByStatusCode']>>>) => SSEHandlerResult | Promise<SSEHandlerResult>;
|
|
87
|
-
/**
|
|
88
|
-
* `InferSseSuccessResponses` resolves to the contract's SSE event schema map, but for a
|
|
89
|
-
* generic `Contract` the content-map response path can widen it beyond `SSEEventSchemas`.
|
|
90
|
-
* This narrows it back so it satisfies the `SSEContext` constraint, falling back to the
|
|
91
|
-
* base `SSEEventSchemas` when the inferred type is not a valid schema map.
|
|
92
|
-
*/
|
|
93
|
-
export type EnsureSseEventSchemas<TEvents> = [TEvents] extends [SSEEventSchemas] ? TEvents : SSEEventSchemas;
|
|
94
|
-
/**
|
|
95
|
-
* Infer the handler shape based on the contract's response mode:
|
|
96
|
-
* - `'non-sse'` — bare `ApiNonSseHandler` function
|
|
97
|
-
* - `'sse'` — bare `ApiSseHandler` function
|
|
98
|
-
* - `'dual'` — `{ nonSse, sse }` object, branched by `Accept` header
|
|
99
|
-
*/
|
|
100
|
-
export type InferApiHandler<Contract extends ApiContract> = [
|
|
101
|
-
ContractResponseMode<Contract['responsesByStatusCode']>
|
|
102
|
-
] extends ['dual'] ? {
|
|
103
|
-
nonSse: ApiNonSseHandler<Contract>;
|
|
104
|
-
sse: ApiSseHandler<Contract>;
|
|
105
|
-
} : [ContractResponseMode<Contract['responsesByStatusCode']>] extends ['sse'] ? ApiSseHandler<Contract> : ApiNonSseHandler<Contract>;
|
|
106
|
-
/**
|
|
107
|
-
* Options for configuring an ApiContract route.
|
|
108
|
-
*
|
|
109
|
-
* Extends Fastify's `RouteOptions` minus the fields the contract provides
|
|
110
|
-
* (`method`, `url`, `schema`, `handler`, `sse`), so any Fastify hook or config
|
|
111
|
-
* (`preHandler`, `onRequest`, `config`, `bodyLimit`, etc.) can be passed directly.
|
|
112
|
-
*
|
|
113
|
-
* SSE lifecycle options (`onConnect`, `onClose`, `onReconnect`) are only
|
|
114
|
-
* relevant for SSE and dual-mode contracts and are ignored for non-SSE routes.
|
|
115
|
-
*
|
|
116
|
-
* Generic in `Contract` so `gatewayMetadata.match.headers` / `match.query`
|
|
117
|
-
* keys are narrowed to the contract's request schemas. The generic is always
|
|
118
|
-
* inferred from the contract argument at the `buildApiRoute` call site, so
|
|
119
|
-
* direct references should write `ApiRouteOptions<typeof myContract>` when
|
|
120
|
-
* gateway metadata typing is needed.
|
|
121
|
-
*/
|
|
122
|
-
export type ApiRouteOptions<Contract extends ApiContract> = Omit<RouteOptions, 'method' | 'url' | 'schema' | 'handler' | 'sse'> & Omit<FastifySSERouteOptions, 'preHandler'> & {
|
|
123
|
-
/**
|
|
124
|
-
* Default response mode for dual-mode routes when the `Accept` header
|
|
125
|
-
* does not express a preference.
|
|
126
|
-
* @default 'json'
|
|
127
|
-
*/
|
|
128
|
-
defaultMode?: DualModeType;
|
|
129
|
-
/**
|
|
130
|
-
* Per-route gateway metadata. `match.headers` / `match.query` keys are
|
|
131
|
-
* narrowed to the contract's request schemas; `customHeaders` /
|
|
132
|
-
* `customQuery` remain the escape hatch for headers and params not
|
|
133
|
-
* declared on the contract. Validated at runtime against the same Zod
|
|
134
|
-
* schema used by `withGatewayMetadata` and stamped on the route via the
|
|
135
|
-
* shared `GATEWAY_METADATA_SYMBOL`.
|
|
136
|
-
*
|
|
137
|
-
* Equivalent to wrapping the result with `withGatewayMetadata` — keep
|
|
138
|
-
* to one form per route. If both are used on the same route, the later
|
|
139
|
-
* call (typically `withGatewayMetadata`) overwrites the inline value;
|
|
140
|
-
* there is no merge.
|
|
141
|
-
*
|
|
142
|
-
* @example
|
|
143
|
-
* ```ts
|
|
144
|
-
* buildApiRoute(MyController.contracts.getItem, this.getItem, {
|
|
145
|
-
* gatewayMetadata: {
|
|
146
|
-
* cache: { ttl: '60s' },
|
|
147
|
-
* match: {
|
|
148
|
-
* // narrowed to keys of the contract's requestHeaderSchema:
|
|
149
|
-
* headers: { 'x-trace-id': { regex: '^[a-f0-9]+$' } },
|
|
150
|
-
* // escape hatch for headers not declared on the contract:
|
|
151
|
-
* customHeaders: { 'x-tenant-id': { regex: '^t_' } },
|
|
152
|
-
* },
|
|
153
|
-
* },
|
|
154
|
-
* })
|
|
155
|
-
* ```
|
|
156
|
-
*/
|
|
157
|
-
gatewayMetadata?: GatewayMetadata<Contract>;
|
|
158
|
-
};
|
|
159
|
-
export {};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"apiHandlerTypes.js","sourceRoot":"","sources":["../../../lib/api-contracts/apiHandlerTypes.ts"],"names":[],"mappings":""}
|