opinionated-machine 9.0.0 → 10.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.
Files changed (42) hide show
  1. package/CHANGELOG.md +85 -0
  2. package/README.md +222 -7
  3. package/dist/lib/DIContext.js +33 -27
  4. package/dist/lib/DIContext.js.map +1 -1
  5. package/dist/lib/routes/fastifyRouteBuilder.js +79 -21
  6. package/dist/lib/routes/fastifyRouteBuilder.js.map +1 -1
  7. package/dist/lib/routes/fastifyRouteTypes.d.ts +97 -12
  8. package/dist/lib/routes/fastifyRouteTypes.js.map +1 -1
  9. package/dist/lib/routes/fastifyRouteUtils.js +15 -5
  10. package/dist/lib/routes/fastifyRouteUtils.js.map +1 -1
  11. package/dist/lib/routes/index.d.ts +2 -1
  12. package/dist/lib/routes/index.js +2 -0
  13. package/dist/lib/routes/index.js.map +1 -1
  14. package/dist/lib/routes/sseResponseSchema.d.ts +35 -0
  15. package/dist/lib/routes/sseResponseSchema.js +115 -0
  16. package/dist/lib/routes/sseResponseSchema.js.map +1 -0
  17. package/dist/lib/sse/SSESessionSpy.d.ts +35 -7
  18. package/dist/lib/sse/SSESessionSpy.js +30 -1
  19. package/dist/lib/sse/SSESessionSpy.js.map +1 -1
  20. package/dist/lib/sse/index.d.ts +1 -1
  21. package/dist/lib/sse/index.js.map +1 -1
  22. package/dist/lib/testing/apiSseInjectHelpers.d.ts +64 -0
  23. package/dist/lib/testing/apiSseInjectHelpers.js +212 -0
  24. package/dist/lib/testing/apiSseInjectHelpers.js.map +1 -0
  25. package/dist/lib/testing/apiSseTestTypes.d.ts +202 -0
  26. package/dist/lib/testing/apiSseTestTypes.js +2 -0
  27. package/dist/lib/testing/apiSseTestTypes.js.map +1 -0
  28. package/dist/lib/testing/index.d.ts +4 -1
  29. package/dist/lib/testing/index.js +2 -0
  30. package/dist/lib/testing/index.js.map +1 -1
  31. package/dist/lib/testing/sseHttpClient.d.ts +36 -4
  32. package/dist/lib/testing/sseHttpClient.js +29 -6
  33. package/dist/lib/testing/sseHttpClient.js.map +1 -1
  34. package/dist/lib/testing/sseInjectHelpers.js +1 -12
  35. package/dist/lib/testing/sseInjectHelpers.js.map +1 -1
  36. package/dist/lib/testing/sseInjectShared.d.ts +7 -0
  37. package/dist/lib/testing/sseInjectShared.js +19 -0
  38. package/dist/lib/testing/sseInjectShared.js.map +1 -0
  39. package/dist/lib/testing/sseSessionSpyFactory.d.ts +109 -0
  40. package/dist/lib/testing/sseSessionSpyFactory.js +100 -0
  41. package/dist/lib/testing/sseSessionSpyFactory.js.map +1 -0
  42. package/package.json +1 -1
@@ -0,0 +1,115 @@
1
+ import { z } from 'zod';
2
+ /**
3
+ * Shape of the error bodies Fastify and the SSE route builders emit themselves, rather than
4
+ * the handler: `FST_ERR_VALIDATION` when a request fails schema validation, whatever an
5
+ * application-level `setErrorHandler` returns, and the `{ statusCode, error, message }`
6
+ * envelope the builders send when a handler throws before streaming starts.
7
+ *
8
+ * A declared status schema is unioned with this so those bodies stay serializable. Without it
9
+ * Fastify serializes them against the handler's schema, fails, and turns a declared 400 or 404
10
+ * into a 500 `FST_ERR_FAILED_ERROR_SERIALIZATION`. Loose so a custom error handler's extra
11
+ * fields survive; `statusCode` and `message` are what keeps it from matching handler bodies.
12
+ * `error` and `code` are listed even though loose mode would carry them anyway, because
13
+ * Fastify defines them as non-enumerable properties that key enumeration would skip.
14
+ */
15
+ const frameworkErrorSchema = z.looseObject({
16
+ statusCode: z.number(),
17
+ message: z.string(),
18
+ error: z.string().optional(),
19
+ code: z.string().optional(),
20
+ });
21
+ /**
22
+ * Combine alternative body schemas for one status.
23
+ *
24
+ * A single schema is returned as-is rather than wrapped, so a status with only one possible
25
+ * body does not pick up a pointless `anyOf` in the generated spec.
26
+ *
27
+ * @returns The combined schema, or `undefined` when there is nothing to describe
28
+ */
29
+ function unionOf(schemas) {
30
+ const [first, ...rest] = schemas;
31
+ if (!first) {
32
+ return undefined;
33
+ }
34
+ return rest.length === 0 ? first : z.union(schemas);
35
+ }
36
+ /**
37
+ * Describe an SSE stream as the union of its event envelopes, following the OpenAPI 3.x
38
+ * convention for `text/event-stream`: one object schema per event type (`{ id?, event, data,
39
+ * retry? }`), discriminated on the `event` name so the contract's event payloads show up in
40
+ * the generated spec as a `oneOf` of envelopes with a `const` event name.
41
+ *
42
+ * Mirrors what `@lokalise/fastify-api-contracts` produces for an `sseBody()` response, so
43
+ * legacy SSE / dual-mode contracts and `ApiContract` ones document the same way.
44
+ *
45
+ * @returns The event envelope schema, or `undefined` when the contract declares no events
46
+ */
47
+ export function buildSseEventSchema(serverSentEventSchemas) {
48
+ const eventSchemas = Object.entries(serverSentEventSchemas).map(([eventName, dataSchema]) => z.object({
49
+ id: z.string().optional(),
50
+ event: z.literal(eventName),
51
+ data: dataSchema,
52
+ retry: z.int().optional(),
53
+ }));
54
+ const [firstEventSchema, ...restEventSchemas] = eventSchemas;
55
+ if (!firstEventSchema) {
56
+ return undefined;
57
+ }
58
+ return restEventSchemas.length === 0
59
+ ? firstEventSchema
60
+ : z.discriminatedUnion('event', [firstEventSchema, ...restEventSchemas]);
61
+ }
62
+ /**
63
+ * Build the JSON schema for one declared status code.
64
+ *
65
+ * A status's schema has to accept every body the runtime can put out at that status,
66
+ * because Fastify serializes against it and rejects anything that does not match:
67
+ *
68
+ * - 2xx on a dual-mode contract: `handleSyncMode` validates the sync body against
69
+ * `successResponseBodySchema` while `processSSEHandlerResult` validates `sse.respond()`
70
+ * against the contract's schema for that status, so both shapes are possible.
71
+ * - Non-2xx: the handler's declared body, or a framework error envelope.
72
+ */
73
+ function buildStatusSchema(statusCode, declaredSchema, syncSuccessSchema) {
74
+ const isSuccessStatus = statusCode >= 200 && statusCode < 300;
75
+ if (isSuccessStatus) {
76
+ return unionOf([syncSuccessSchema, declaredSchema].filter((schema) => schema !== undefined));
77
+ }
78
+ return declaredSchema && z.union([declaredSchema, frameworkErrorSchema]);
79
+ }
80
+ /**
81
+ * Build the `schema.response` map for an SSE or dual-mode route from its contract.
82
+ *
83
+ * The 200 entry uses Fastify's per-media-type form so a single status can describe both the
84
+ * event stream and the JSON body: `text/event-stream` carries the event envelopes, and
85
+ * `application/json` carries the sync body (dual-mode), the body the contract declares for
86
+ * 200 (an SSE route that answers `sse.respond(200, ...)` before streaming starts), or both.
87
+ *
88
+ * Populating this drives both the OpenAPI spec and Fastify's serializer, so a status code the
89
+ * contract declares is now serialized against its schema instead of plain `JSON.stringify`.
90
+ *
91
+ * @param serverSentEventSchemas - Contract's event name to payload schema map
92
+ * @param responseBodySchemasByStatusCode - Contract's per-status response schemas, if any
93
+ * @param syncSuccessSchema - Dual-mode sync 2xx body schema; omit for SSE-only contracts
94
+ */
95
+ export function buildSseResponseSchemas(serverSentEventSchemas, responseBodySchemasByStatusCode, syncSuccessSchema) {
96
+ const { 200: declaredOkSchema, ...otherStatusSchemas } = responseBodySchemasByStatusCode ?? {};
97
+ const jsonOkSchema = buildStatusSchema(200, declaredOkSchema, syncSuccessSchema);
98
+ const eventSchema = buildSseEventSchema(serverSentEventSchemas);
99
+ const okContent = {
100
+ ...(eventSchema && { 'text/event-stream': { schema: eventSchema } }),
101
+ ...(jsonOkSchema && { 'application/json': { schema: jsonOkSchema } }),
102
+ };
103
+ const responseSchemas = {};
104
+ if (Object.keys(okContent).length > 0) {
105
+ responseSchemas[200] = { content: okContent };
106
+ }
107
+ for (const [statusCode, declaredSchema] of Object.entries(otherStatusSchemas)) {
108
+ const schema = buildStatusSchema(Number(statusCode), declaredSchema, syncSuccessSchema);
109
+ if (schema) {
110
+ responseSchemas[statusCode] = schema;
111
+ }
112
+ }
113
+ return responseSchemas;
114
+ }
115
+ //# sourceMappingURL=sseResponseSchema.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sseResponseSchema.js","sourceRoot":"","sources":["../../../lib/routes/sseResponseSchema.ts"],"names":[],"mappings":"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"}
@@ -1,21 +1,39 @@
1
1
  import type { SSESession } from './AbstractSSEController.ts';
2
- export type SSESessionEvent = {
2
+ /**
3
+ * Minimal shape of an SSE session the spy needs in order to track it.
4
+ *
5
+ * Both the `SSESession` produced by this package's own SSE routes and the one
6
+ * produced by `@lokalise/fastify-api-contracts` (used by `buildApiRoute`)
7
+ * satisfy it, which lets a single spy be attached to either route style.
8
+ */
9
+ export type SpiedSSESession = {
10
+ id: string;
11
+ request: {
12
+ url: string;
13
+ };
14
+ };
15
+ export type SSESessionEvent<TSession extends SpiedSSESession = SSESession> = {
3
16
  type: 'connect' | 'disconnect';
4
17
  connectionId: string;
5
- connection?: SSESession;
18
+ connection?: TSession;
6
19
  };
7
20
  /**
8
21
  * Connection spy for testing SSE controllers.
9
22
  * Tracks connection and disconnection events separately.
23
+ *
24
+ * @template TSession - The session type the spy observes. Defaults to this
25
+ * package's `SSESession`, which is what `AbstractSSEController` reports.
26
+ * Tests wiring the spy to `buildApiRoute` routes get it parameterized with
27
+ * the contracts package session type via `createSSESessionSpy()`.
10
28
  */
11
- export declare class SSESessionSpy {
29
+ export declare class SSESessionSpy<TSession extends SpiedSSESession = SSESession> {
12
30
  private events;
13
31
  private activeConnections;
14
32
  private claimedConnections;
15
33
  private connectionWaiters;
16
34
  private disconnectionWaiters;
17
35
  /** @internal Called when a connection is established */
18
- addConnection(connection: SSESession): void;
36
+ addConnection(connection: TSession): void;
19
37
  /** @internal Called when a connection is closed */
20
38
  addDisconnection(connectionId: string): void;
21
39
  /**
@@ -40,8 +58,18 @@ export declare class SSESessionSpy {
40
58
  */
41
59
  waitForConnection(options?: {
42
60
  timeout?: number;
43
- predicate?: (connection: SSESession) => boolean;
44
- }): Promise<SSESession>;
61
+ predicate?: (connection: TSession) => boolean;
62
+ }): Promise<TSession>;
63
+ /**
64
+ * Explain a timeout that had matching connections which were already gone.
65
+ *
66
+ * `waitForConnection` only hands back sessions that are still active, since a
67
+ * closed session can no longer be sent events. A route that streams with
68
+ * `autoClose` closes its session as the handler returns, so its connection is
69
+ * registered and closed before a test can claim it — without this hint that
70
+ * shows up as a bare timeout with no indication of what went wrong.
71
+ */
72
+ private describeMissedConnections;
45
73
  /** Wait for a specific connection to disconnect */
46
74
  waitForDisconnection(connectionId: string, options?: {
47
75
  timeout?: number;
@@ -49,7 +77,7 @@ export declare class SSESessionSpy {
49
77
  /** Check if a connection is currently active */
50
78
  isConnected(connectionId: string): boolean;
51
79
  /** Get all connection events in order, optionally filtered by connectionId */
52
- getEvents(connectionId?: string): SSESessionEvent[];
80
+ getEvents(connectionId?: string): SSESessionEvent<TSession>[];
53
81
  /** Clear all events and cancel pending waiters */
54
82
  clear(): void;
55
83
  }
@@ -1,6 +1,11 @@
1
1
  /**
2
2
  * Connection spy for testing SSE controllers.
3
3
  * Tracks connection and disconnection events separately.
4
+ *
5
+ * @template TSession - The session type the spy observes. Defaults to this
6
+ * package's `SSESession`, which is what `AbstractSSEController` reports.
7
+ * Tests wiring the spy to `buildApiRoute` routes get it parameterized with
8
+ * the contracts package session type via `createSSESessionSpy()`.
4
9
  */
5
10
  export class SSESessionSpy {
6
11
  events = [];
@@ -75,11 +80,35 @@ export class SSESessionSpy {
75
80
  if (index !== -1) {
76
81
  this.connectionWaiters.splice(index, 1);
77
82
  }
78
- reject(new Error(`Timeout waiting for connection after ${timeout}ms`));
83
+ reject(new Error(`Timeout waiting for connection after ${timeout}ms${this.describeMissedConnections(predicate)}`));
79
84
  }, timeout);
80
85
  this.connectionWaiters.push({ resolve, reject, timeoutId, predicate });
81
86
  });
82
87
  }
88
+ /**
89
+ * Explain a timeout that had matching connections which were already gone.
90
+ *
91
+ * `waitForConnection` only hands back sessions that are still active, since a
92
+ * closed session can no longer be sent events. A route that streams with
93
+ * `autoClose` closes its session as the handler returns, so its connection is
94
+ * registered and closed before a test can claim it — without this hint that
95
+ * shows up as a bare timeout with no indication of what went wrong.
96
+ */
97
+ describeMissedConnections(predicate) {
98
+ const missed = this.events.filter((e) => e.type === 'connect' &&
99
+ e.connection &&
100
+ !this.activeConnections.has(e.connection.id) &&
101
+ (!predicate || predicate(e.connection)));
102
+ if (missed.length === 0) {
103
+ return '';
104
+ }
105
+ const ids = missed.map((e) => e.connectionId).join(', ');
106
+ return (`. ${missed.length} matching connection(s) were registered but had already closed: ${ids}. ` +
107
+ 'Only sessions that are still open can be awaited, since a closed one can no longer be sent ' +
108
+ 'events. A session started with `autoClose` closes as its handler returns, so a route that ' +
109
+ 'uses one cannot be awaited at all - omit `awaitServerConnection` there and assert on the ' +
110
+ 'events the client received instead.');
111
+ }
83
112
  /** Wait for a specific connection to disconnect */
84
113
  waitForDisconnection(connectionId, options) {
85
114
  const timeout = options?.timeout ?? 5000;
@@ -1 +1 @@
1
- {"version":3,"file":"SSESessionSpy.js","sourceRoot":"","sources":["../../../lib/sse/SSESessionSpy.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"SSESessionSpy.js","sourceRoot":"","sources":["../../../lib/sse/SSESessionSpy.ts"],"names":[],"mappings":"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"}
@@ -3,6 +3,6 @@ export { type BuildFastifySSERoutesReturnType, buildFastifyRoute, buildHandler,
3
3
  export { AbstractSSEController, type SSEControllerConfig, type SSEEventSender, type SSELogger, type SSEMessage, } from './AbstractSSEController.js';
4
4
  export { defineEvent, type SSEEventDefinition } from './defineEvent.js';
5
5
  export { defineRoom, InMemoryAdapter, type PreDeliveryFilter, type RoomBroadcastOptions, type RoomNameResolver, type SSERoomAdapter, SSERoomBroadcaster, SSERoomManager, type SSERoomManagerConfig, type SSERoomMessageHandler, type SSERoomOperations, } from './rooms/index.js';
6
- export { type SSESessionEvent, SSESessionSpy } from './SSESessionSpy.js';
6
+ export { type SpiedSSESession, type SSESessionEvent, SSESessionSpy } from './SSESessionSpy.js';
7
7
  export { type ParsedSSEEvent, type ParseSSEBufferResult, parseSSEBuffer, parseSSEEvents, } from './sseParser.js';
8
8
  export { defineEventMetadata, type ExtractMetadata, type FilterVerdict, type IncomingEvent, type MetadataGuard, type MetadataGuards, type PublishResult, type ResolverResult, SSESubscriptionManager, type SSESubscriptionManagerConfig, type SubscriptionContext, type SubscriptionPolicy, type SubscriptionResolver, } from './subscriptions/index.js';
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../lib/sse/index.ts"],"names":[],"mappings":"AAUA,2CAA2C;AAC3C,OAAO,EAEL,iBAAiB,EACjB,YAAY,GAUb,MAAM,oBAAoB,CAAA;AAC3B,OAAO,EACL,qBAAqB,GAKtB,MAAM,4BAA4B,CAAA;AACnC,OAAO,EAAE,WAAW,EAA2B,MAAM,kBAAkB,CAAA;AACvE,mCAAmC;AACnC,OAAO,EACL,UAAU,EACV,eAAe,EAKf,kBAAkB,EAClB,cAAc,GAIf,MAAM,kBAAkB,CAAA;AACzB,OAAO,EAAwB,aAAa,EAAE,MAAM,oBAAoB,CAAA;AACxE,OAAO,EAGL,cAAc,EACd,cAAc,GACf,MAAM,gBAAgB,CAAA;AACvB,oBAAoB;AACpB,OAAO,EACL,mBAAmB,EAQnB,sBAAsB,GAKvB,MAAM,0BAA0B,CAAA"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../lib/sse/index.ts"],"names":[],"mappings":"AAUA,2CAA2C;AAC3C,OAAO,EAEL,iBAAiB,EACjB,YAAY,GAUb,MAAM,oBAAoB,CAAA;AAC3B,OAAO,EACL,qBAAqB,GAKtB,MAAM,4BAA4B,CAAA;AACnC,OAAO,EAAE,WAAW,EAA2B,MAAM,kBAAkB,CAAA;AACvE,mCAAmC;AACnC,OAAO,EACL,UAAU,EACV,eAAe,EAKf,kBAAkB,EAClB,cAAc,GAIf,MAAM,kBAAkB,CAAA;AACzB,OAAO,EAA8C,aAAa,EAAE,MAAM,oBAAoB,CAAA;AAC9F,OAAO,EAGL,cAAc,EACd,cAAc,GACf,MAAM,gBAAgB,CAAA;AACvB,oBAAoB;AACpB,OAAO,EACL,mBAAmB,EAQnB,sBAAsB,GAKvB,MAAM,0BAA0B,CAAA"}
@@ -0,0 +1,64 @@
1
+ import { type ApiContract } from '@lokalise/api-contracts';
2
+ import type { AnyFastifyInstance } from './AnyFastifyInstance.ts';
3
+ import type { ApiSSEEventReader, InjectApiSSEParams, InjectApiSSEResult } from './apiSseTestTypes.ts';
4
+ import type { SSEResponse } from './sseTestTypes.ts';
5
+ /**
6
+ * Build a `bodyForStatus` accessor bound to one `injectApiSSE` call.
7
+ *
8
+ * @internal Exported only for unit testing — not part of the public API
9
+ * (the testing barrel re-exports `injectApiSSE` by name).
10
+ */
11
+ export declare function bindApiBodyForStatus<Contract extends ApiContract>(contract: Contract, closed: Promise<SSEResponse>): InjectApiSSEResult<Contract>['bodyForStatus'];
12
+ /**
13
+ * Build an `events` accessor bound to one `injectApiSSE` call: parses the SSE body and
14
+ * validates every event against the contract's `sseBody` schemas.
15
+ *
16
+ * @internal Exported only for unit testing — not part of the public API.
17
+ */
18
+ export declare function bindApiEvents<Contract extends ApiContract>(contract: Contract, closed: Promise<SSEResponse>): ApiSSEEventReader<Contract>;
19
+ /**
20
+ * Inject an SSE request using a contract built with `defineApiContract` + `sseResponse` /
21
+ * `sseBody` (the newer `@lokalise/api-contracts` API).
22
+ *
23
+ * The `defineApiContract` counterpart of `injectSSE` / `injectPayloadSSE`, which are typed
24
+ * against the legacy `SSEContractDefinition`. One function covers every method: the HTTP verb
25
+ * comes from the contract, and `params` (`pathParams` / `queryParams` / `headers` / `body` /
26
+ * `pathPrefix`) is the same shape `injectByApiContract` takes, so a body is required exactly
27
+ * when the contract declares `requestBodySchema`.
28
+ *
29
+ * The request always carries `accept: text/event-stream` (a caller-supplied `accept` still
30
+ * wins), so a status declaring a stream answers with it — dual-mode statuses included. Those
31
+ * statuses expose no JSON body through `bodyForStatus`; read them with `events()`, or use
32
+ * `injectByApiContract` when you want the JSON side.
33
+ *
34
+ * Best for SSE endpoints that complete — Fastify's `inject()` waits for the whole response.
35
+ * For long-lived connections, use `SSEHttpClient` against a real HTTP server.
36
+ *
37
+ * @param app - Fastify instance
38
+ * @param contract - Contract built with `defineApiContract`
39
+ * @param params - Request params derived from the contract
40
+ *
41
+ * @example
42
+ * ```typescript
43
+ * const { closed, bodyForStatus, events } = injectApiSSE(app, lqaTextSegmentContract, {
44
+ * body: { segment: 'hello' },
45
+ * })
46
+ *
47
+ * // Typed events, validated against the contract's sseResponse schemas
48
+ * for (const event of await events()) {
49
+ * if (event.event === 'review') expect(event.data.score).toBeGreaterThan(0)
50
+ * }
51
+ *
52
+ * // Or the raw body, for assertions the typed accessors don't cover
53
+ * expect((await closed).statusCode).toBe(200)
54
+ * ```
55
+ *
56
+ * @example
57
+ * ```typescript
58
+ * // A documented pre-stream error response, typed by the contract's 400 schema
59
+ * const { bodyForStatus } = injectApiSSE(app, lqaTextSegmentContract, { body: { segment: '' } })
60
+ * const error = await bodyForStatus(400)
61
+ * expect(error.message).toBe('segment must not be empty')
62
+ * ```
63
+ */
64
+ export declare function injectApiSSE<const Contract extends ApiContract>(app: AnyFastifyInstance, contract: Contract, params: InjectApiSSEParams<Contract>): InjectApiSSEResult<Contract>;
@@ -0,0 +1,212 @@
1
+ import { getSseSchemaByEventName, resolveResponseEntry, } from '@lokalise/api-contracts';
2
+ import { injectByApiContract } from '@lokalise/fastify-api-contracts';
3
+ import { parseSSEEvents } from "../sse/sseParser.js";
4
+ import { truncateBody } from "./sseInjectShared.js";
5
+ const SSE_CONTENT_TYPE = 'text/event-stream';
6
+ /** Read a response header that light-my-request may expose as an array. */
7
+ function readHeader(value) {
8
+ return Array.isArray(value) ? value[0] : value;
9
+ }
10
+ /** Strip `; charset=…` style parameters from a media type. */
11
+ function mediaTypeOf(contentType) {
12
+ return contentType?.split(';')[0]?.trim().toLowerCase();
13
+ }
14
+ const STATUS_RANGE_KEYS = ['1xx', '2xx', '3xx', '4xx', '5xx'];
15
+ /**
16
+ * The `responsesByStatusCode` key that serves a status, following the same
17
+ * exact → range → `'default'` precedence as `resolveResponseEntry`.
18
+ *
19
+ * Diagnostics only. `resolveResponseEntry` collapses "no entry for this status" and "an entry
20
+ * exists but none of its content-map descriptors matched the response's content-type" into a
21
+ * single `null`; this tells the two apart so the error names the actual problem.
22
+ */
23
+ function findResponseKeyForStatus(responsesByStatusCode, statusCode) {
24
+ if (responsesByStatusCode[statusCode]) {
25
+ return String(statusCode);
26
+ }
27
+ const rangeKey = STATUS_RANGE_KEYS[Math.floor(statusCode / 100) - 1];
28
+ if (rangeKey && responsesByStatusCode[rangeKey]) {
29
+ return rangeKey;
30
+ }
31
+ return responsesByStatusCode.default ? 'default' : undefined;
32
+ }
33
+ /**
34
+ * The JSON schema the contract declares for the status a response actually carries, or a
35
+ * thrown error naming why there isn't one.
36
+ *
37
+ * Resolution follows the same exact → range → `'default'` precedence (and content-type
38
+ * matching) the contract client uses, so a stream on one status and JSON bodies on the others
39
+ * resolve independently.
40
+ */
41
+ function resolveJsonSchemaForStatus(responsesByStatusCode, statusCode, res) {
42
+ const contentType = readHeader(res.headers['content-type']);
43
+ // Non-strict resolution: a response without a content-type still resolves to the entry's
44
+ // declared kind, which keeps hand-rolled test handlers working.
45
+ const resolved = resolveResponseEntry(responsesByStatusCode, statusCode, contentType, false);
46
+ if (!resolved) {
47
+ const declaredKey = findResponseKeyForStatus(responsesByStatusCode, statusCode);
48
+ throw new Error(declaredKey === undefined
49
+ ? `bodyForStatus(${statusCode}) — no response declared for status ${statusCode} in contract.responsesByStatusCode`
50
+ : `bodyForStatus(${statusCode}) — the '${declaredKey}' entry of contract.responsesByStatusCode declares no body for content-type '${mediaTypeOf(contentType) ?? 'absent'}'; body: ${truncateBody(res.body)}`);
51
+ }
52
+ if (resolved.kind !== 'json') {
53
+ // A dual-mode status lands here: `injectApiSSE` asks for the stream, so that is what the
54
+ // status resolved to. The type layer rules this out, so reaching it means a cast.
55
+ const hint = resolved.kind === 'sse'
56
+ ? ` — injectApiSSE requests '${SSE_CONTENT_TYPE}', so a status declaring a stream always answers with it; read it with events()`
57
+ : '';
58
+ throw new Error(`bodyForStatus(${statusCode}) — the contract declares a '${resolved.kind}' response for status ${statusCode}, not a JSON body${hint}`);
59
+ }
60
+ return resolved.schema;
61
+ }
62
+ /** JSON-parse and schema-validate a response body, reporting either failure in context. */
63
+ function parseJsonBody(schema, statusCode, body) {
64
+ let parsedJson;
65
+ try {
66
+ parsedJson = JSON.parse(body);
67
+ }
68
+ catch (err) {
69
+ throw new Error(`bodyForStatus(${statusCode}) — body is not valid JSON: ${err.message}; body: ${truncateBody(body)}`);
70
+ }
71
+ const parsed = schema.safeParse(parsedJson);
72
+ if (!parsed.success) {
73
+ throw new Error(`bodyForStatus(${statusCode}) — body does not match the declared schema: ${parsed.error.message}; body: ${truncateBody(body)}`);
74
+ }
75
+ return parsed.data;
76
+ }
77
+ /**
78
+ * Build a `bodyForStatus` accessor bound to one `injectApiSSE` call.
79
+ *
80
+ * @internal Exported only for unit testing — not part of the public API
81
+ * (the testing barrel re-exports `injectApiSSE` by name).
82
+ */
83
+ export function bindApiBodyForStatus(contract, closed) {
84
+ // A generic arrow function can't be assigned directly to the generic method signature,
85
+ // so the whole closure is cast once. Keep in sync with `InjectApiSSEResult['bodyForStatus']`.
86
+ return (async (statusCode) => {
87
+ const res = await closed;
88
+ const expected = statusCode;
89
+ if (res.statusCode !== expected) {
90
+ throw new Error(`bodyForStatus(${expected}) — actual status ${res.statusCode}, body: ${truncateBody(res.body)}`);
91
+ }
92
+ const schema = resolveJsonSchemaForStatus(contract.responsesByStatusCode, expected, res);
93
+ return parseJsonBody(schema, expected, res.body);
94
+ });
95
+ }
96
+ /**
97
+ * Build an `events` accessor bound to one `injectApiSSE` call: parses the SSE body and
98
+ * validates every event against the contract's `sseBody` schemas.
99
+ *
100
+ * @internal Exported only for unit testing — not part of the public API.
101
+ */
102
+ export function bindApiEvents(contract, closed) {
103
+ return async () => {
104
+ const res = await closed;
105
+ // Merges the SSE schemas of every declared status, not just the successful ones.
106
+ const schemaByEventName = getSseSchemaByEventName(contract);
107
+ if (!schemaByEventName) {
108
+ throw new Error('events() — the contract declares no SSE response');
109
+ }
110
+ const contentType = mediaTypeOf(readHeader(res.headers['content-type']));
111
+ if (contentType !== SSE_CONTENT_TYPE) {
112
+ throw new Error(`events() — response is not an SSE stream (status ${res.statusCode}, content-type ${contentType ?? 'absent'}); use bodyForStatus(${res.statusCode}) for declared error responses. Body: ${truncateBody(res.body)}`);
113
+ }
114
+ return parseSSEEvents(res.body).map((event) => {
115
+ // An SSE event without an `event:` field is a `message` event per the spec.
116
+ const name = event.event ?? 'message';
117
+ const schema = schemaByEventName[name];
118
+ if (!schema) {
119
+ throw new Error(`events() — the contract declares no schema for event "${name}"`);
120
+ }
121
+ let parsedJson;
122
+ try {
123
+ parsedJson = JSON.parse(event.data);
124
+ }
125
+ catch (err) {
126
+ throw new Error(`events() — data of event "${name}" is not valid JSON: ${err.message}; data: ${truncateBody(event.data)}`);
127
+ }
128
+ const parsed = schema.safeParse(parsedJson);
129
+ if (!parsed.success) {
130
+ throw new Error(`events() — data of event "${name}" does not match the declared schema: ${parsed.error.message}; data: ${truncateBody(event.data)}`);
131
+ }
132
+ return {
133
+ ...(event.id !== undefined && { id: event.id }),
134
+ ...(event.retry !== undefined && { retry: event.retry }),
135
+ event: name,
136
+ data: parsed.data,
137
+ };
138
+ });
139
+ };
140
+ }
141
+ /**
142
+ * Inject an SSE request using a contract built with `defineApiContract` + `sseResponse` /
143
+ * `sseBody` (the newer `@lokalise/api-contracts` API).
144
+ *
145
+ * The `defineApiContract` counterpart of `injectSSE` / `injectPayloadSSE`, which are typed
146
+ * against the legacy `SSEContractDefinition`. One function covers every method: the HTTP verb
147
+ * comes from the contract, and `params` (`pathParams` / `queryParams` / `headers` / `body` /
148
+ * `pathPrefix`) is the same shape `injectByApiContract` takes, so a body is required exactly
149
+ * when the contract declares `requestBodySchema`.
150
+ *
151
+ * The request always carries `accept: text/event-stream` (a caller-supplied `accept` still
152
+ * wins), so a status declaring a stream answers with it — dual-mode statuses included. Those
153
+ * statuses expose no JSON body through `bodyForStatus`; read them with `events()`, or use
154
+ * `injectByApiContract` when you want the JSON side.
155
+ *
156
+ * Best for SSE endpoints that complete — Fastify's `inject()` waits for the whole response.
157
+ * For long-lived connections, use `SSEHttpClient` against a real HTTP server.
158
+ *
159
+ * @param app - Fastify instance
160
+ * @param contract - Contract built with `defineApiContract`
161
+ * @param params - Request params derived from the contract
162
+ *
163
+ * @example
164
+ * ```typescript
165
+ * const { closed, bodyForStatus, events } = injectApiSSE(app, lqaTextSegmentContract, {
166
+ * body: { segment: 'hello' },
167
+ * })
168
+ *
169
+ * // Typed events, validated against the contract's sseResponse schemas
170
+ * for (const event of await events()) {
171
+ * if (event.event === 'review') expect(event.data.score).toBeGreaterThan(0)
172
+ * }
173
+ *
174
+ * // Or the raw body, for assertions the typed accessors don't cover
175
+ * expect((await closed).statusCode).toBe(200)
176
+ * ```
177
+ *
178
+ * @example
179
+ * ```typescript
180
+ * // A documented pre-stream error response, typed by the contract's 400 schema
181
+ * const { bodyForStatus } = injectApiSSE(app, lqaTextSegmentContract, { body: { segment: '' } })
182
+ * const error = await bodyForStatus(400)
183
+ * expect(error.message).toBe('segment must not be empty')
184
+ * ```
185
+ */
186
+ export function injectApiSSE(app, contract, params) {
187
+ // biome-ignore lint/suspicious/noExplicitAny: params shape depends on the contract
188
+ const requestParams = params;
189
+ const closed = injectByApiContract(app, contract, {
190
+ ...requestParams,
191
+ // `accept` first so an explicit caller header still wins; headers may be a factory,
192
+ // which `injectByApiContract` resolves for us — resolve the caller's here too.
193
+ headers: async () => ({
194
+ accept: SSE_CONTENT_TYPE,
195
+ ...(typeof requestParams.headers === 'function'
196
+ ? await requestParams.headers()
197
+ : requestParams.headers),
198
+ }),
199
+ }).then((res) => ({
200
+ statusCode: res.statusCode,
201
+ headers: res.headers,
202
+ body: res.body,
203
+ }));
204
+ return {
205
+ closed,
206
+ bodyForStatus: bindApiBodyForStatus(contract, closed),
207
+ // `events` is typed `never` for contracts that declare no SSE response, which no concrete
208
+ // function satisfies — the binder returns the callable form and it is narrowed here.
209
+ events: bindApiEvents(contract, closed),
210
+ };
211
+ }
212
+ //# sourceMappingURL=apiSseInjectHelpers.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"apiSseInjectHelpers.js","sourceRoot":"","sources":["../../../lib/testing/apiSseInjectHelpers.ts"],"names":[],"mappings":"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"}