UNPKG

opinionated-machine

Version:

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

165 lines 5.9 kB
/** Truncate a long body string for error messages. */ const BODY_TRUNCATE_LIMIT = 500; const truncateBody = (body) => { if (body.length <= BODY_TRUNCATE_LIMIT) { return body; } // Step back one unit if the cut would split a surrogate pair, so the // snippet never ends in a lone (invalid) surrogate. const lastCode = body.charCodeAt(BODY_TRUNCATE_LIMIT - 1); const end = lastCode >= 0xd800 && lastCode <= 0xdbff ? BODY_TRUNCATE_LIMIT - 1 : BODY_TRUNCATE_LIMIT; return `${body.slice(0, end)}…`; }; /** * Build a `bodyForStatus` accessor bound to one inject call. The closure * captures the contract's schemas map so the resulting helper knows which * schemas to parse against; at the type level the caller is constrained to * status codes the contract actually declares. * * @internal Exported only for unit testing — not part of the public API * (the testing barrel re-exports `injectSSE`/`injectPayloadSSE` by name). */ export function bindBodyForStatus(contract, closed) { // A generic arrow function can't be assigned directly to the generic // method signature, so the whole closure is cast once. Keep this // implementation in sync with `InjectSSEResult['bodyForStatus']`. return (async (statusCode) => { const res = await closed; const expected = statusCode; if (res.statusCode !== expected) { throw new Error(`bodyForStatus(${expected}) — actual status ${res.statusCode}, body: ${truncateBody(res.body)}`); } // Widen the generic schemas map to a concrete type so it can be indexed. const schemas = contract.responseBodySchemasByStatusCode; const schema = schemas?.[expected]; if (!schema) { throw new Error(`bodyForStatus(${expected}) — no response body schema declared for status ${expected} in contract.responseBodySchemasByStatusCode`); } let parsedJson; try { parsedJson = JSON.parse(res.body); } catch (err) { throw new Error(`bodyForStatus(${expected}) — body is not valid JSON: ${err.message}; body: ${truncateBody(res.body)}`); } const parsed = schema.safeParse(parsedJson); if (!parsed.success) { throw new Error(`bodyForStatus(${expected}) — body does not match the declared schema: ${parsed.error.message}; body: ${truncateBody(res.body)}`); } return parsed.data; }); } /** * Build query string from query params object. * @internal */ function buildQueryString(query) { const searchParams = new URLSearchParams(); for (const [key, value] of Object.entries(query)) { if (value !== undefined && value !== null) { searchParams.append(key, String(value)); } } return searchParams.toString(); } /** * Build URL from contract pathResolver and params. * @internal */ function buildUrl(contract, params, query) { let url = contract.pathResolver(params ?? {}); // Append query string if present if (query && Object.keys(query).length > 0) { const queryString = buildQueryString(query); if (queryString) { url = `${url}?${queryString}`; } } return url; } /** * Inject a GET SSE request using a contract definition. * * Best for testing SSE endpoints that complete (streaming responses). * For long-lived connections, use `connectSSE` with a real HTTP server. * * @param app - Fastify instance * @param contract - SSE route contract * @param options - Request options (params, query, headers) * * @example * ```typescript * const { closed } = injectSSE(app, streamContract, { * query: { userId: 'user-123' }, * }) * const result = await closed * const events = parseSSEEvents(result.body) * ``` */ export function injectSSE(app, contract, options) { const url = buildUrl(contract, options?.params, options?.query); // Start the request - this promise resolves when connection closes const closed = app .inject({ method: 'GET', url, headers: { accept: 'text/event-stream', ...options?.headers, }, }) .then((res) => ({ statusCode: res.statusCode, headers: res.headers, body: res.body, })); return { closed, bodyForStatus: bindBodyForStatus(contract, closed) }; } /** * Inject a POST/PUT/PATCH SSE request using a contract definition. * * This helper is designed for testing OpenAI-style streaming APIs where * the request includes a body and the response streams events. * * @param app - Fastify instance * @param contract - SSE route contract with body * @param options - Request options (params, query, headers, body) * * @example * ```typescript * // Fire the SSE request * const { closed } = injectPayloadSSE(app, chatCompletionContract, { * body: { message: 'Hello', stream: true }, * headers: { authorization: 'Bearer token' }, * }) * * // Wait for streaming to complete and get full response * const result = await closed * const events = parseSSEEvents(result.body) * * expect(events).toContainEqual( * expect.objectContaining({ event: 'chunk' }) * ) * ``` */ export function injectPayloadSSE(app, contract, options) { const url = buildUrl(contract, options.params, options.query); const closed = app .inject({ method: contract.method, url, headers: { accept: 'text/event-stream', 'content-type': 'application/json', ...options.headers, }, payload: JSON.stringify(options.body), }) .then((res) => ({ statusCode: res.statusCode, headers: res.headers, body: res.body, })); return { closed, bodyForStatus: bindBodyForStatus(contract, closed) }; } //# sourceMappingURL=sseInjectHelpers.js.map