opinionated-machine
Version:
Very opinionated DI framework for fastify, built on top of awilix
135 lines (134 loc) • 5.87 kB
TypeScript
import type { AnySSEContractDefinition, HttpStatusCode } from '@lokalise/api-contracts';
import type { z } from 'zod';
import type { ParsedSSEEvent } from '../sse/sseParser.ts';
/** Safely infer the output type of an optional Zod schema property. */
type InferOptionalSchema<T, Fallback = unknown> = NonNullable<T> extends z.ZodTypeAny ? z.infer<NonNullable<T>> : Fallback;
/**
* Status codes that the given schemas-map declares.
* Resolves to `never` when the map is `undefined`, so `bodyForStatus` is
* uncallable for contracts that declare no response body schemas at all.
*/
export type DeclaredResponseStatus<Schemas extends Partial<Record<HttpStatusCode, z.ZodTypeAny>> | undefined> = Schemas extends Partial<Record<HttpStatusCode, z.ZodTypeAny>> ? keyof Schemas & HttpStatusCode : never;
/** Type of the parsed response body for a declared status. */
export type DeclaredResponseBody<Schemas extends Partial<Record<HttpStatusCode, z.ZodTypeAny>> | undefined, Status extends DeclaredResponseStatus<Schemas>> = Schemas extends Partial<Record<HttpStatusCode, z.ZodTypeAny>> ? Status extends keyof Schemas ? Schemas[Status] extends z.ZodTypeAny ? z.infer<Schemas[Status]> : never : never : never;
/**
* Represents an active SSE test connection (inject-based).
*
* This interface is used with Fastify's inject() for testing SSE endpoints
* synchronously. For long-lived connections, use SSEHttpClient instead.
*/
export interface SSETestConnection {
/**
* Wait for a specific event by name.
* @param eventName - The event name to wait for
* @param timeout - Timeout in milliseconds (default: 5000)
*/
waitForEvent(eventName: string, timeout?: number): Promise<ParsedSSEEvent>;
/**
* Wait for a specific number of events.
* @param count - Number of events to wait for
* @param timeout - Timeout in milliseconds (default: 5000)
*/
waitForEvents(count: number, timeout?: number): Promise<ParsedSSEEvent[]>;
/**
* Get all events received so far.
*/
getReceivedEvents(): ParsedSSEEvent[];
/**
* Close the connection.
*/
close(): void;
/**
* Check if connection is closed.
*/
isClosed(): boolean;
/**
* Get the HTTP response status code.
*/
getStatusCode(): number;
/**
* Get response headers.
*/
getHeaders(): Record<string, string | string[] | undefined>;
}
/**
* Options for establishing an SSE connection.
*/
export type SSEConnectOptions = {
headers?: Record<string, string>;
method?: 'GET' | 'POST' | 'PUT' | 'PATCH';
body?: unknown;
};
/**
* Options for injectSSE (GET SSE routes).
*/
export type InjectSSEOptions<Contract extends AnySSEContractDefinition> = {
params?: InferOptionalSchema<Contract['requestPathParamsSchema']>;
query?: InferOptionalSchema<Contract['requestQuerySchema']>;
headers?: InferOptionalSchema<Contract['requestHeaderSchema']>;
};
/**
* Options for injectPayloadSSE (POST/PUT/PATCH SSE routes).
*/
export type InjectPayloadSSEOptions<Contract extends AnySSEContractDefinition> = {
params?: InferOptionalSchema<Contract['requestPathParamsSchema']>;
query?: InferOptionalSchema<Contract['requestQuerySchema']>;
headers?: InferOptionalSchema<Contract['requestHeaderSchema']>;
body: InferOptionalSchema<Contract['requestBodySchema'], never>;
};
/**
* SSE response data.
*/
export type SSEResponse = {
statusCode: number;
headers: Record<string, string | string[] | undefined>;
body: string;
};
/**
* Result of an SSE inject call.
*
* Note: Fastify's inject() waits for the full response, so these helpers
* work best for streaming that completes (OpenAI-style). For long-lived
* SSE connections, use `SSEHttpClient` with a real HTTP server instead.
*
* When the contract declares `responseBodySchemasByStatusCode`, the result
* exposes `bodyForStatus(status)` — a typed accessor that parses the response
* body against the contract's schema for that status. TS rejects status codes
* the contract doesn't declare.
*/
export type InjectSSEResult<Schemas extends Partial<Record<HttpStatusCode, z.ZodTypeAny>> | undefined = undefined> = {
/**
* Resolves when the response completes with the full SSE body.
* Parse the body with `parseSSEEvents()` to get individual events.
*/
closed: Promise<SSEResponse>;
/**
* Awaits the response, asserts the status code matches, parses the body
* against the contract's schema for that status, and returns the parsed
* object. Useful for asserting on documented error response shapes.
*
* Intended for non-streaming responses emitted via `sse.respond(status, body)`
* before streaming starts (auth failures, validation errors, not-found).
* Calling it for a status served by an actual SSE stream fails at the
* JSON-parse step, since a stream body is `text/event-stream`, not JSON.
*
* Throws (with the offending status and a truncated body snippet) if:
* - the actual status code doesn't match the expected one;
* - the contract declares no schema for that status;
* - the body isn't valid JSON;
* - the body doesn't match the declared Zod schema.
*
* At the type level, `statusCode` is constrained to the keys of the
* contract's `responseBodySchemasByStatusCode`. Contracts without any
* declared schemas can't call this method (`statusCode: never`).
*
* @example
* ```typescript
* const { bodyForStatus } = injectSSE(app, contract, { headers })
* const error = await bodyForStatus(401) // typed as z.infer<401-schema>
* expect(error.message).toBe('Unauthorized')
* ```
*/
bodyForStatus<Status extends DeclaredResponseStatus<Schemas>>(statusCode: Status): Promise<DeclaredResponseBody<Schemas, Status>>;
};
export {};