UNPKG

opinionated-machine

Version:

Very opinionated DI framework for fastify, built on top of awilix

135 lines (134 loc) 5.87 kB
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 {};