opinionated-machine 7.0.0 → 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 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(UserController.contracts.streamUpdates, async (_req, sse) => {
24
- * sse.start('keepAlive')
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(UserController.contracts.streamUpdates, async (_req, sse) => {
21
- * sse.start('keepAlive')
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;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,MAAM,OAAgB,qBAAqB;IAGzC;;;;;;;OAOG;IACa,eAAe,CAAuB;CACvD"}
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 { type ApiContract } from '@lokalise/api-contracts';
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 { ApiRouteOptions, InferApiHandler } from './apiHandlerTypes.ts';
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
- * The handler shape is inferred from the contract's response mode:
8
- * - `'non-sse'` — bare async function returning `{ status, body }`
9
- * - `'sse'` — bare async function calling `sse.start(...)` / `sse.respond(...)`
10
- * - `'dual'` — `{ nonSse, sse }` object branched by the `Accept` header
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
- * The optional `options` argument carries:
13
- * - any Fastify route field (`preHandler`, `onRequest`, `config`, `bodyLimit`, …)
14
- * minus the ones the contract provides (`method`, `url`, `schema`, `handler`, `sse`),
15
- * - SSE lifecycle hooks (`onConnect`, `onClose`, `onReconnect`, `serializer`,
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,358 +1,25 @@
1
- import { randomUUID } from 'node:crypto';
2
- import { ContractNoBody, getSseSchemaByEventName, hasAnySuccessSseResponse, isContentResponseEntry, isSseBody, 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
- // A bare Zod schema is a JSON response, which always has a non-SSE representation.
16
- return true;
17
- }
18
- function getContractResponseMode(contract) {
19
- if (!hasAnySuccessSseResponse(contract))
20
- return 'non-sse';
21
- for (const code of SUCCESSFUL_HTTP_STATUS_CODES) {
22
- const value = contract.responsesByStatusCode[code];
23
- if (value && isSuccessResponseDual(value))
24
- return 'dual';
25
- }
26
- return 'sse';
27
- }
28
- function buildSSERouteConfig(options) {
29
- if (!options?.serializer && options?.heartbeatInterval === undefined)
30
- return true;
31
- const sseConfig = {};
32
- if (options.serializer)
33
- sseConfig.serializer = options.serializer;
34
- if (options.heartbeatInterval !== undefined)
35
- sseConfig.heartbeatInterval = options.heartbeatInterval;
36
- return sseConfig;
37
- }
38
- // ============================================================================
39
- // Internal Helpers — Sync Route
40
- // ============================================================================
41
- function getSchemaForStatusCode(contract, status) {
42
- const entry = contract.responsesByStatusCode[status];
43
- if (!entry)
44
- return null;
45
- // Resolve the JSON representation for this status code, covering both bare Zod
46
- // schemas and content-map entries. Non-JSON responses (blob, SSE, no-body) are
47
- // not validated here.
48
- const resolved = resolveContractResponse(entry, 'application/json', false);
49
- return resolved?.kind === 'json' ? resolved.schema : null;
50
- }
51
- function validateApiResponseHeaders(contract, reply) {
52
- const schema = contract.responseHeaderSchema;
53
- if (!schema) {
54
- return;
55
- }
56
- const result = schema.safeParse(reply.getHeaders());
57
- if (!result.success) {
58
- throw new InternalError({
59
- message: 'Internal Server Error',
60
- errorCode: 'RESPONSE_HEADERS_VALIDATION_FAILED',
61
- details: { validationError: result.error.message },
62
- });
63
- }
64
- }
65
- async function handleApiSyncRoute(contract,
66
- // biome-ignore lint/suspicious/noExplicitAny: Handler types are validated by InferApiHandler at the call site
67
- handler,
68
- // biome-ignore lint/suspicious/noExplicitAny: Request types are validated by Fastify schema
69
- request, reply) {
70
- const { status, body } = await handler(request, reply);
71
- if (reply.sent) {
72
- request.log.warn({
73
- msg: 'Sync handler sent response directly, bypassing response validation',
74
- tag: 'response_sent_directly',
75
- method: request.method,
76
- url: request.url,
77
- });
78
- return;
79
- }
80
- try {
81
- const schema = getSchemaForStatusCode(contract, status);
82
- if (schema) {
83
- const result = schema.safeParse(body);
84
- if (!result.success) {
85
- throw new InternalError({
86
- message: 'Internal Server Error',
87
- errorCode: 'RESPONSE_VALIDATION_FAILED',
88
- details: { validationError: result.error.message },
89
- });
90
- }
91
- }
92
- }
93
- catch (err) {
94
- reply.code(500);
95
- throw err;
96
- }
97
- validateApiResponseHeaders(contract, reply);
98
- if (!reply.hasHeader('content-type')) {
99
- reply.type('application/json');
100
- }
101
- return reply.code(status).send(body);
102
- }
103
- // ============================================================================
104
- // Internal Helpers — SSE Route (no controller, uses reply.sse directly)
105
- // ============================================================================
106
- function buildApiSSEContext(
107
- // biome-ignore lint/suspicious/noExplicitAny: Request types are validated by Fastify schema
108
- request, reply, eventSchemas, options) {
109
- let started = false;
110
- let responseData;
111
- const sseReply = reply;
112
- const sseContext = {
113
- start: (mode, startOptions) => {
114
- started = true;
115
- if (mode === 'keepAlive') {
116
- sseReply.sse.keepAlive();
117
- }
118
- // sendHeaders() calls writeHead(200) but only queues headers in the buffer.
119
- // flushHeaders() forces them onto the wire so the client's fetch() returns.
120
- sseReply.sse.sendHeaders();
121
- reply.raw.flushHeaders();
122
- const connectionId = randomUUID();
123
- const send = async (eventName, data, sendOptions) => {
124
- const schema = eventSchemas[eventName];
125
- if (schema) {
126
- const result = schema.safeParse(data);
127
- if (!result.success) {
128
- throw new InternalError({
129
- message: `SSE event validation failed for event "${eventName}": ${result.error.message}`,
130
- errorCode: 'RESPONSE_VALIDATION_FAILED',
131
- });
132
- }
133
- }
134
- try {
135
- await sseReply.sse.send({
136
- event: eventName,
137
- data,
138
- id: sendOptions?.id,
139
- retry: sendOptions?.retry,
140
- });
141
- return true;
142
- }
143
- catch {
144
- return false;
145
- }
146
- };
147
- const session = {
148
- id: connectionId,
149
- request,
150
- reply,
151
- context: (startOptions?.context ?? {}),
152
- connectedAt: new Date(),
153
- // biome-ignore lint/suspicious/noExplicitAny: SSEEventSender generic is satisfied at handler call site
154
- send: send,
155
- isConnected: () => sseReply.sse.isConnected,
156
- getStream: () => sseReply.sse.stream(),
157
- sendStream: async (messages) => {
158
- for await (const message of messages) {
159
- await send(message.event, message.data, { id: message.id, retry: message.retry });
160
- }
161
- },
162
- rooms: { join: () => { }, leave: () => { } },
163
- eventSchemas,
164
- };
165
- if (options?.onConnect) {
166
- void Promise.resolve(options.onConnect(session)).catch(() => { });
167
- }
168
- if (options?.onClose) {
169
- const onClose = options.onClose;
170
- sseReply.sse.onClose(() => {
171
- void Promise.resolve(onClose(session, 'client')).catch(() => { });
172
- });
173
- }
174
- if (options?.onReconnect && sseReply.sse.lastEventId) {
175
- const onReconnect = options.onReconnect;
176
- const lastEventId = sseReply.sse.lastEventId;
177
- void sseReply.sse.replay(async () => {
178
- const replay = await onReconnect(session, lastEventId);
179
- if (replay) {
180
- for await (const msg of replay) {
181
- await sseReply.sse.send(msg);
182
- }
183
- }
184
- });
185
- }
186
- return session;
187
- },
188
- respond: ((code, body) => {
189
- if (started) {
190
- throw new Error('Cannot call sse.respond() after sse.start() — the SSE stream is already open.');
191
- }
192
- responseData = { code, body };
193
- return { _type: 'respond', code, body };
194
- // biome-ignore lint/suspicious/noExplicitAny: respond typing is enforced by contract at call site
195
- }),
196
- sendHeaders: () => {
197
- sseReply.sse.sendHeaders();
198
- },
199
- reply,
200
- };
201
- return {
202
- sseContext,
203
- isStarted: () => started,
204
- hasResponse: () => responseData !== undefined,
205
- getResponseData: () => responseData,
206
- };
207
- }
208
- // biome-ignore lint/complexity/noExcessiveCognitiveComplexity: Core SSE handler coordinates context, error handling, and lifecycle
209
- async function handleApiSseRoute(
210
- // biome-ignore lint/suspicious/noExplicitAny: SSE handler types are validated by InferApiHandler at call site
211
- sseHandler, eventSchemas, options,
212
- // biome-ignore lint/suspicious/noExplicitAny: Request types are validated by Fastify schema
213
- request, reply) {
214
- const { sseContext, isStarted, hasResponse, getResponseData } = buildApiSSEContext(request, reply, eventSchemas, options);
215
- try {
216
- await sseHandler(request, sseContext);
217
- if (!isStarted() && !hasResponse()) {
218
- throw new Error('SSE handler must either send a response (sse.respond()) ' +
219
- 'or start streaming (sse.start()). Handler returned without doing either.');
220
- }
221
- const responseData = getResponseData();
222
- if (responseData) {
223
- // Early HTTP response (sse.respond() was called before streaming)
224
- reply.removeHeader('cache-control');
225
- reply.removeHeader('x-accel-buffering');
226
- reply.type('application/json').code(responseData.code).send(responseData.body);
227
- }
228
- // If started, @fastify/sse manages the rest of the connection lifecycle
229
- }
230
- catch (err) {
231
- if (isStarted()) {
232
- // Headers already sent — can't change status code; try to send error event
233
- const sseReply = reply;
234
- if (sseReply.sse.isConnected) {
235
- try {
236
- await sseReply.sse.send({
237
- event: 'error',
238
- data: { message: isErrorLike(err) ? err.message : 'Internal Server Error' },
239
- });
240
- }
241
- catch {
242
- // Ignore send failures during error handling
243
- }
244
- }
245
- throw err;
246
- }
247
- // Streaming not started — send HTTP error response
248
- const message = isErrorLike(err) ? err.message : 'Internal Server Error';
249
- const statusCode = hasHttpStatusCode(err) ? err.httpStatusCode : 500;
250
- const statusText = statusCode >= 500 ? 'Internal Server Error' : 'Error';
251
- reply.code(statusCode).type('application/json').send({ statusCode, error: statusText, message });
252
- }
253
- }
254
- // ============================================================================
255
- // Internal Helpers — Schema
256
- // ============================================================================
257
- function buildResponseSchemas(contract) {
258
- return Object.keys(contract.responsesByStatusCode).reduce((acc, statusCode) => {
259
- const schema = getSchemaForStatusCode(contract, Number(statusCode));
260
- if (schema) {
261
- acc[Number(statusCode)] = schema;
262
- }
263
- return acc;
264
- }, {});
265
- }
266
- function buildBaseSchema(contract) {
267
- const schema = {};
268
- if (contract.requestPathParamsSchema)
269
- schema.params = contract.requestPathParamsSchema;
270
- if (contract.requestQuerySchema)
271
- schema.querystring = contract.requestQuerySchema;
272
- if (contract.requestHeaderSchema)
273
- schema.headers = contract.requestHeaderSchema;
274
- if (contract.requestBodySchema !== undefined && contract.requestBodySchema !== ContractNoBody) {
275
- schema.body = contract.requestBodySchema;
276
- }
277
- schema.response = buildResponseSchemas(contract);
278
- return schema;
279
- }
280
- // ============================================================================
281
- // Public API
282
- // ============================================================================
283
3
  /**
284
4
  * Build a Fastify `RouteOptions` object from an `ApiContract` + handler.
285
5
  *
286
- * The handler shape is inferred from the contract's response mode:
287
- * - `'non-sse'` — bare async function returning `{ status, body }`
288
- * - `'sse'` — bare async function calling `sse.start(...)` / `sse.respond(...)`
289
- * - `'dual'` — `{ nonSse, sse }` object branched by the `Accept` header
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.
290
11
  *
291
- * The optional `options` argument carries:
292
- * - any Fastify route field (`preHandler`, `onRequest`, `config`, `bodyLimit`, …)
293
- * minus the ones the contract provides (`method`, `url`, `schema`, `handler`, `sse`),
294
- * - SSE lifecycle hooks (`onConnect`, `onClose`, `onReconnect`, `serializer`,
295
- * `heartbeatInterval`) — applied for `'sse'` and `'dual'` contracts only,
296
- * - `defaultMode` for `'dual'` contracts when the `Accept` header is ambiguous,
297
- * - `gatewayMetadata` — per-route gateway policy with header / query keys
298
- * narrowed to the contract; equivalent to wrapping the result with
299
- * `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.
300
16
  *
301
17
  * @returns Fastify `RouteOptions` ready to pass to `app.route()`
302
18
  */
303
19
  export function buildApiRoute(contract, handler, options) {
304
- // Separate SSE-specific options (not in Fastify RouteOptions) and gateway
305
- // metadata (stamped via Symbol, not spread) from passthrough options.
306
- const { defaultMode, contractMetadataToRouteMapper, gatewayMetadata, serializer: _serializer, heartbeatInterval: _heartbeatInterval, onConnect: _onConnect, onClose: _onClose, onReconnect: _onReconnect, logger: _logger, ...fastifyOptions } = options ?? {};
307
- const url = mapApiContractToPath(contract);
308
- const mode = getContractResponseMode(contract);
309
- const eventSchemas = getSseSchemaByEventName(contract) ?? {};
310
- const baseSchema = buildBaseSchema(contract);
311
- const contractMetadata = contractMetadataToRouteMapper?.(contract.metadata) ?? {};
312
- const finalize = (route) => gatewayMetadata !== undefined ? attachGatewayMetadata(route, gatewayMetadata) : route;
313
- if (mode === 'non-sse') {
314
- // biome-ignore lint/suspicious/noExplicitAny: handler shape validated by InferApiHandler at call site
315
- const syncHandler = handler;
316
- return finalize({
317
- ...fastifyOptions,
318
- ...contractMetadata,
319
- method: contract.method,
320
- url,
321
- schema: baseSchema,
322
- handler: async (request, reply) => handleApiSyncRoute(contract, syncHandler, request, reply),
323
- });
324
- }
325
- if (mode === 'dual') {
326
- const resolvedDefaultMode = defaultMode ?? 'json';
327
- // biome-ignore lint/suspicious/noExplicitAny: handler shape validated by InferApiHandler at call site
328
- const dualHandlers = handler;
329
- return finalize({
330
- ...fastifyOptions,
331
- ...contractMetadata,
332
- method: contract.method,
333
- url,
334
- sse: buildSSERouteConfig(options),
335
- schema: baseSchema,
336
- handler: (request, reply) => {
337
- const responseMode = determineMode(request.headers.accept, resolvedDefaultMode);
338
- if (responseMode === 'json') {
339
- return handleApiSyncRoute(contract, dualHandlers.nonSse, request, reply);
340
- }
341
- return handleApiSseRoute(dualHandlers.sse, eventSchemas, options, request, reply);
342
- },
343
- });
344
- }
345
- // SSE-only
346
- // biome-ignore lint/suspicious/noExplicitAny: handler shape validated by InferApiHandler at call site
347
- const sseHandler = handler;
348
- return finalize({
349
- ...fastifyOptions,
350
- ...contractMetadata,
351
- method: contract.method,
352
- url,
353
- sse: buildSSERouteConfig(options),
354
- schema: baseSchema,
355
- handler: async (request, reply) => handleApiSseRoute(sseHandler, eventSchemas, options, request, reply),
356
- });
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;
357
24
  }
358
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 type { ApiNonSseHandler, ApiRouteOptions, ApiSseHandler, InferApiHandler, InferApiRequest, InferApiStatusResponse, } from './apiHandlerTypes.ts';
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":"AAAA,OAAO,EAAE,qBAAqB,EAAE,MAAM,4BAA4B,CAAA;AASlE,OAAO,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAA;AACpD,OAAO,EAAE,oBAAoB,EAAE,MAAM,2BAA2B,CAAA"}
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
- "name": "opinionated-machine",
3
- "version": "7.0.0",
4
- "description": "Very opinionated DI framework for fastify, built on top of awilix ",
5
- "type": "module",
6
- "license": "MIT",
7
- "workspaces": [
8
- ".",
9
- "packages/*"
10
- ],
11
- "main": "dist/index.js",
12
- "types": "dist/index.d.ts",
13
- "exports": {
14
- ".": "./dist/index.js",
15
- "./package.json": "./package.json"
16
- },
17
- "maintainers": [
18
- {
19
- "name": "Igor Savin",
20
- "email": "kibertoad@gmail.com"
21
- }
22
- ],
23
- "scripts": {
24
- "build": "rimraf dist && tsc -p tsconfig.build.json",
25
- "lint": "biome check . && tsc",
26
- "lint:fix": "biome check --write .",
27
- "test": "vitest --typecheck",
28
- "test:ci": "npm run test -- --coverage",
29
- "prepublishOnly": "npm run build",
30
- "changeset": "changeset",
31
- "ci:publish": "changeset publish"
32
- },
33
- "repository": {
34
- "type": "git",
35
- "url": "git+https://github.com/kibertoad/opinionated-machine.git"
36
- },
37
- "dependencies": {
38
- "@fastify/sse": "^0.4.0",
39
- "fast-querystring": "^1.1.2",
40
- "fastify-plugin": "^6.0.0",
41
- "ts-deepmerge": "^8.0.0"
42
- },
43
- "peerDependencies": {
44
- "@lokalise/api-contracts": ">=7.0.0",
45
- "@lokalise/fastify-api-contracts": ">=5.4.1",
46
- "@lokalise/node-core": ">=14.7.4",
47
- "awilix": ">=13.0.0",
48
- "awilix-manager": ">=6.0.0",
49
- "fastify": ">=5.0.0",
50
- "fastify-type-provider-zod": ">=6.1.0",
51
- "zod": ">=4.1.12"
52
- },
53
- "devDependencies": {
54
- "@biomejs/biome": "2.4.4",
55
- "@changesets/cli": "^2.31.0",
56
- "@lokalise/api-contracts": "^7.0.0",
57
- "@lokalise/biome-config": "^3.1.1",
58
- "@lokalise/fastify-api-contracts": "^5.4.1",
59
- "@lokalise/node-core": "^14.7.4",
60
- "@lokalise/tsconfig": "^3.1.0",
61
- "@types/node": "^22.19.7",
62
- "@vitest/coverage-v8": "^4.0.18",
63
- "awilix": "^13.0.0",
64
- "awilix-manager": "^7.0.0",
65
- "fastify": "^5.7.4",
66
- "fastify-type-provider-zod": "^7.0.0",
67
- "rimraf": "^6.1.2",
68
- "typescript": "^5.9.3",
69
- "vitest": "^4.0.18",
70
- "zod": "^4.3.6"
71
- },
72
- "private": false,
73
- "publishConfig": {
74
- "access": "public"
75
- },
76
- "keywords": [
77
- "dependency",
78
- "injection",
79
- "opinionated",
80
- "awilix",
81
- "di",
82
- "fastify"
83
- ],
84
- "homepage": "https://github.com/kibertoad/opinionated-machine",
85
- "files": [
86
- "README.md",
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,161 +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 NonSseBodyDescriptor<D> = D extends {
8
- _tag: 'SseBody';
9
- } ? never : D extends {
10
- _tag: 'BlobBody';
11
- } ? Blob : D extends z.ZodType ? z.output<D> : never;
12
- type NonSseBodyEntry<T> = T extends undefined ? never : T extends {
13
- content: infer TContent;
14
- } ? NonSseBodyDescriptor<TContent[keyof TContent]> | (T extends {
15
- allowNoBody: true;
16
- } ? undefined : never) : T extends {
17
- allowNoBody: true;
18
- } ? undefined : T extends z.ZodType ? z.output<T> : undefined;
19
- /**
20
- * Discriminated union of `{ status, body }` pairs for all non-SSE responses in a contract.
21
- *
22
- * Allows non-SSE handlers to return a specific status code and body together without
23
- * calling `reply.code()` separately.
24
- *
25
- * @example
26
- * ```typescript
27
- * async (request) => {
28
- * if (!valid) return { status: 400, body: { error: 'Bad Request' } }
29
- * return { id: request.params.id }
30
- * }
31
- * ```
32
- */
33
- export type InferApiStatusResponse<Contract extends ApiContract> = {
34
- [K in keyof Contract['responsesByStatusCode']]: NonSseBodyEntry<Contract['responsesByStatusCode'][K]> extends never ? never : {
35
- status: K;
36
- body: NonSseBodyEntry<Contract['responsesByStatusCode'][K]>;
37
- };
38
- }[keyof Contract['responsesByStatusCode']];
39
- type InferOptSchema<T, Fallback = unknown> = NonNullable<T> extends z.ZodType ? z.output<NonNullable<T>> : Fallback;
40
- 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;
41
- /**
42
- * Infer the FastifyRequest type from an ApiContract.
43
- *
44
- * Provides properly typed params, querystring, headers, and body.
45
- *
46
- * @example
47
- * ```typescript
48
- * const handler = async (request: InferApiRequest<typeof myContract>) => {
49
- * request.params.userId // typed
50
- * request.body.name // typed
51
- * }
52
- * ```
53
- */
54
- export type InferApiRequest<Contract extends ApiContract> = FastifyRequest<{
55
- Params: InferOptSchema<Contract['requestPathParamsSchema']>;
56
- Querystring: InferOptSchema<Contract['requestQuerySchema']>;
57
- Headers: InferOptSchema<Contract['requestHeaderSchema']>;
58
- Body: InferApiBodyType<Contract>;
59
- }>;
60
- /**
61
- * Handler for non-SSE responses from an ApiContract.
62
- *
63
- * Always return `{ status, body }` — the framework validates the body against the
64
- * contract's schema for that status code and sends it.
65
- *
66
- * Use `reply.header()` to set response headers when needed.
67
- *
68
- * @example
69
- * ```typescript
70
- * async (request) => ({ status: 200, body: { id: request.params.userId } })
71
- * ```
72
- *
73
- * @example With multiple status codes
74
- * ```typescript
75
- * async (request) => {
76
- * if (!valid) return { status: 400, body: { error: 'Bad Request' } }
77
- * return { status: 200, body: { id: request.params.userId } }
78
- * }
79
- * ```
80
- */
81
- export type ApiNonSseHandler<Contract extends ApiContract> = (request: InferApiRequest<Contract>, reply: SyncModeReply) => InferApiStatusResponse<Contract> | Promise<InferApiStatusResponse<Contract>>;
82
- /**
83
- * Handler for SSE responses from an ApiContract.
84
- *
85
- * Call `sse.start(mode)` to begin streaming or `sse.respond(code, body)` for
86
- * early HTTP returns before streaming starts.
87
- */
88
- export type ApiSseHandler<Contract extends ApiContract> = (request: InferApiRequest<Contract>, sse: SSEContext<EnsureSseEventSchemas<InferSseSuccessResponses<Contract['responsesByStatusCode']>>>) => SSEHandlerResult | Promise<SSEHandlerResult>;
89
- /**
90
- * `InferSseSuccessResponses` resolves to the contract's SSE event schema map, but for a
91
- * generic `Contract` the content-map response path can widen it beyond `SSEEventSchemas`.
92
- * This narrows it back so it satisfies the `SSEContext` constraint, falling back to the
93
- * base `SSEEventSchemas` when the inferred type is not a valid schema map.
94
- */
95
- export type EnsureSseEventSchemas<TEvents> = [TEvents] extends [SSEEventSchemas] ? TEvents : SSEEventSchemas;
96
- /**
97
- * Infer the handler shape based on the contract's response mode:
98
- * - `'non-sse'` — bare `ApiNonSseHandler` function
99
- * - `'sse'` — bare `ApiSseHandler` function
100
- * - `'dual'` — `{ nonSse, sse }` object, branched by `Accept` header
101
- */
102
- export type InferApiHandler<Contract extends ApiContract> = [
103
- ContractResponseMode<Contract['responsesByStatusCode']>
104
- ] extends ['dual'] ? {
105
- nonSse: ApiNonSseHandler<Contract>;
106
- sse: ApiSseHandler<Contract>;
107
- } : [ContractResponseMode<Contract['responsesByStatusCode']>] extends ['sse'] ? ApiSseHandler<Contract> : ApiNonSseHandler<Contract>;
108
- /**
109
- * Options for configuring an ApiContract route.
110
- *
111
- * Extends Fastify's `RouteOptions` minus the fields the contract provides
112
- * (`method`, `url`, `schema`, `handler`, `sse`), so any Fastify hook or config
113
- * (`preHandler`, `onRequest`, `config`, `bodyLimit`, etc.) can be passed directly.
114
- *
115
- * SSE lifecycle options (`onConnect`, `onClose`, `onReconnect`) are only
116
- * relevant for SSE and dual-mode contracts and are ignored for non-SSE routes.
117
- *
118
- * Generic in `Contract` so `gatewayMetadata.match.headers` / `match.query`
119
- * keys are narrowed to the contract's request schemas. The generic is always
120
- * inferred from the contract argument at the `buildApiRoute` call site, so
121
- * direct references should write `ApiRouteOptions<typeof myContract>` when
122
- * gateway metadata typing is needed.
123
- */
124
- export type ApiRouteOptions<Contract extends ApiContract> = Omit<RouteOptions, 'method' | 'url' | 'schema' | 'handler' | 'sse'> & Omit<FastifySSERouteOptions, 'preHandler'> & {
125
- /**
126
- * Default response mode for dual-mode routes when the `Accept` header
127
- * does not express a preference.
128
- * @default 'json'
129
- */
130
- defaultMode?: DualModeType;
131
- /**
132
- * Per-route gateway metadata. `match.headers` / `match.query` keys are
133
- * narrowed to the contract's request schemas; `customHeaders` /
134
- * `customQuery` remain the escape hatch for headers and params not
135
- * declared on the contract. Validated at runtime against the same Zod
136
- * schema used by `withGatewayMetadata` and stamped on the route via the
137
- * shared `GATEWAY_METADATA_SYMBOL`.
138
- *
139
- * Equivalent to wrapping the result with `withGatewayMetadata` — keep
140
- * to one form per route. If both are used on the same route, the later
141
- * call (typically `withGatewayMetadata`) overwrites the inline value;
142
- * there is no merge.
143
- *
144
- * @example
145
- * ```ts
146
- * buildApiRoute(MyController.contracts.getItem, this.getItem, {
147
- * gatewayMetadata: {
148
- * cache: { ttl: '60s' },
149
- * match: {
150
- * // narrowed to keys of the contract's requestHeaderSchema:
151
- * headers: { 'x-trace-id': { regex: '^[a-f0-9]+$' } },
152
- * // escape hatch for headers not declared on the contract:
153
- * customHeaders: { 'x-tenant-id': { regex: '^t_' } },
154
- * },
155
- * },
156
- * })
157
- * ```
158
- */
159
- gatewayMetadata?: GatewayMetadata<Contract>;
160
- };
161
- export {};
@@ -1,2 +0,0 @@
1
- export {};
2
- //# sourceMappingURL=apiHandlerTypes.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"apiHandlerTypes.js","sourceRoot":"","sources":["../../../lib/api-contracts/apiHandlerTypes.ts"],"names":[],"mappings":""}