@efebia/fastify-zod-reply 1.3.0 → 1.4.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.
@@ -23,12 +23,13 @@ export type FastifyReplyPluginOptions = {
23
23
  export type DecoratedReply = {
24
24
  [key in keyof FastifyStatusCode]: <T>(val?: T) => T;
25
25
  };
26
- declare module 'fastify' {
26
+ declare module "fastify" {
27
27
  interface FastifyReply extends DecoratedReply {
28
28
  }
29
29
  }
30
30
  declare const _default: import("fastify").FastifyPluginCallback<FastifyReplyPluginOptions, import("fastify").RawServerDefault, import("fastify").FastifyTypeProviderDefault, import("fastify").FastifyBaseLogger>;
31
31
  export default _default;
32
- export * from './error.js';
33
- export * from './routeV4.js';
34
- export * from './types.js';
32
+ export * from "./error.js";
33
+ export * from "./routeV4.js";
34
+ export * from "./sseRouteV4.js";
35
+ export * from "./types.js";
package/lib/cjs/index.js CHANGED
@@ -22,28 +22,34 @@ const reply_js_1 = require("./reply.js");
22
22
  const utils_js_1 = require("./utils.js");
23
23
  const defaultOptions = {
24
24
  statusCodes: {
25
- ok: { statusCode: 200, payload: { message: 'ok' } },
26
- created: { statusCode: 201, payload: { message: 'created' } },
27
- accepted: { statusCode: 202, payload: { message: 'accepted' } },
28
- noContent: { statusCode: 204, payload: undefined },
29
- badRequest: { statusCode: 400, payload: { message: 'badRequest' } },
30
- unauthorized: { statusCode: 401, payload: { message: 'unauthorized' } },
31
- forbidden: { statusCode: 403, payload: { message: 'forbidden' } },
32
- notFound: { statusCode: 404, payload: { message: 'notFound' } },
33
- notAcceptable: { statusCode: 406, payload: { message: 'notAcceptable' } },
34
- conflict: { statusCode: 409, payload: { message: 'conflict' } },
35
- internalServerError: { statusCode: 500, payload: { message: 'internalServerError' } },
36
- }
25
+ ok: { statusCode: 200, payload: { message: "ok" } },
26
+ created: { statusCode: 201, payload: { message: "created" } },
27
+ accepted: { statusCode: 202, payload: { message: "accepted" } },
28
+ noContent: { statusCode: 204, payload: null },
29
+ badRequest: { statusCode: 400, payload: { message: "badRequest" } },
30
+ unauthorized: { statusCode: 401, payload: { message: "unauthorized" } },
31
+ forbidden: { statusCode: 403, payload: { message: "forbidden" } },
32
+ notFound: { statusCode: 404, payload: { message: "notFound" } },
33
+ notAcceptable: { statusCode: 406, payload: { message: "notAcceptable" } },
34
+ conflict: { statusCode: 409, payload: { message: "conflict" } },
35
+ internalServerError: {
36
+ statusCode: 500,
37
+ payload: { message: "internalServerError" },
38
+ },
39
+ },
37
40
  };
38
41
  exports.default = (0, fastify_plugin_1.default)(async (fastify, opts) => {
39
42
  const finalOptions = (0, utils_js_1.mergeDeep)(defaultOptions, opts);
40
43
  Object.entries(finalOptions.statusCodes).forEach(([key, value]) => {
41
44
  fastify.decorateReply(key, (0, reply_js_1.createReply)(value.statusCode, value.payload));
42
45
  });
46
+ fastify.decorateRequest("abortController");
47
+ fastify.decorateReply("sse");
43
48
  }, {
44
49
  fastify: "5.x",
45
- name: '@efebia/fastify-zod-reply'
50
+ name: "@efebia/fastify-zod-reply",
46
51
  });
47
52
  __exportStar(require("./error.js"), exports);
48
53
  __exportStar(require("./routeV4.js"), exports);
54
+ __exportStar(require("./sseRouteV4.js"), exports);
49
55
  __exportStar(require("./types.js"), exports);
@@ -0,0 +1,17 @@
1
+ import { z } from "zod/v4";
2
+ import { RouteV4Options } from "./routeV4.js";
3
+ export declare const parse: (schema: z.ZodTypeAny, payload: any, tag: string) => Promise<{
4
+ tag: string;
5
+ success: true;
6
+ data: unknown;
7
+ error?: never;
8
+ } | {
9
+ tag: string;
10
+ success: false;
11
+ data?: never;
12
+ error: z.ZodError<unknown>;
13
+ }>;
14
+ export declare const strictifySchema: (schema: z.ZodType, strict: boolean) => any;
15
+ export declare const parseStrict: (tag: keyof Exclude<NonNullable<RouteV4Options["strict"]>, boolean>, value: NonNullable<RouteV4Options["strict"]>) => boolean;
16
+ export declare const findStatusCode: (statusCode: number, availableStatusCodes: [string | number, any][]) => [string | number, any] | undefined;
17
+ export declare const mapZodError: (zodError: z.ZodError, prefix: string) => string;
@@ -0,0 +1,44 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.mapZodError = exports.findStatusCode = exports.parseStrict = exports.strictifySchema = exports.parse = void 0;
4
+ const parse = async (schema, payload, tag) => {
5
+ const result = await schema.safeParseAsync(payload);
6
+ return Object.assign(Object.assign({}, result), { tag });
7
+ };
8
+ exports.parse = parse;
9
+ const strictifySchema = (schema, strict) => {
10
+ if (!strict)
11
+ return schema;
12
+ return "strict" in schema && typeof schema["strict"] === "function"
13
+ ? schema.strict()
14
+ : schema;
15
+ };
16
+ exports.strictifySchema = strictifySchema;
17
+ const parseStrict = (tag, value) => {
18
+ if (typeof value === "boolean")
19
+ return value;
20
+ return value[tag];
21
+ };
22
+ exports.parseStrict = parseStrict;
23
+ const findStatusCode = (statusCode, availableStatusCodes) => {
24
+ return availableStatusCodes.find(([key]) => {
25
+ if (!["number", "string"].includes(typeof key))
26
+ return false;
27
+ if (typeof key === "number")
28
+ return statusCode === key;
29
+ if (/^[0-9]{3}$/.test(key))
30
+ return statusCode === parseInt(key);
31
+ if (/^[0-9]xx$/i.test(key))
32
+ return statusCode.toString()[0] === key[0];
33
+ });
34
+ };
35
+ exports.findStatusCode = findStatusCode;
36
+ const mapZodError = (zodError, prefix) => {
37
+ return zodError.issues
38
+ .map((issue) => {
39
+ const pathStr = `Error at ${prefix}->${issue.path.join("->")}`;
40
+ return issue.message ? `${pathStr}->${issue.message}` : pathStr;
41
+ })
42
+ .join("\n");
43
+ };
44
+ exports.mapZodError = mapZodError;
@@ -1,19 +1,21 @@
1
- import { z } from 'zod/v4';
2
- import { APIHandler, APIOptions, RouteSecurity, RouteTag } from './types.js';
1
+ import { z } from "zod/v4";
2
+ import { APIHandler, APIOptions, RouteSecurity, RouteTag } from "./types.js";
3
3
  export type BaseZodV4Schema = {
4
4
  Body?: z.ZodTypeAny;
5
5
  Params?: z.ZodTypeAny;
6
6
  Query?: z.ZodTypeAny;
7
7
  Headers?: z.ZodTypeAny;
8
- Reply: z.ZodObject;
9
- Security?: (RouteSecurity[keyof RouteSecurity])[];
8
+ Reply: z.ZodObject<{
9
+ [key: string | number]: z.ZodTypeAny;
10
+ }>;
11
+ Security?: RouteSecurity[keyof RouteSecurity][];
10
12
  Tags?: (keyof RouteTag)[];
11
13
  };
12
14
  export type FastifyZodV4Schema<TZodSchema extends BaseZodV4Schema> = {
13
- Body: TZodSchema['Body'] extends z.ZodTypeAny ? z.output<TZodSchema['Body']> : undefined;
14
- Params: TZodSchema['Params'] extends z.ZodTypeAny ? z.output<TZodSchema['Params']> : undefined;
15
- Querystring: TZodSchema['Query'] extends z.ZodTypeAny ? z.output<TZodSchema['Query']> : undefined;
16
- Reply: TZodSchema['Reply'] extends z.ZodTypeAny ? z.input<TZodSchema['Reply']>[keyof z.input<TZodSchema['Reply']>] : undefined;
15
+ Body: TZodSchema["Body"] extends z.ZodTypeAny ? z.output<TZodSchema["Body"]> : undefined;
16
+ Params: TZodSchema["Params"] extends z.ZodTypeAny ? z.output<TZodSchema["Params"]> : undefined;
17
+ Querystring: TZodSchema["Query"] extends z.ZodTypeAny ? z.output<TZodSchema["Query"]> : undefined;
18
+ Reply: TZodSchema["Reply"] extends z.ZodTypeAny ? z.input<TZodSchema["Reply"]>[keyof z.input<TZodSchema["Reply"]>] : undefined;
17
19
  };
18
20
  export type RouteV4Options = {
19
21
  strict?: boolean | {
@@ -23,9 +25,9 @@ export type RouteV4Options = {
23
25
  headers: boolean;
24
26
  };
25
27
  };
26
- export declare const createRouteV4: ({ strict: globalStrict }?: RouteV4Options) => <TSchema extends BaseZodV4Schema, FastifySchema extends FastifyZodV4Schema<TSchema> = FastifyZodV4Schema<TSchema>>(schema: TSchema, handler: APIHandler<FastifySchema>, options?: RouteV4Options) => APIOptions<FastifySchema> & {
28
+ export declare const createRouteV4: <RequestAugmentation extends object = {}, ReplyAugmentation extends object = {}>({ strict: globalStrict }?: RouteV4Options) => <TSchema extends BaseZodV4Schema, FastifySchema extends FastifyZodV4Schema<TSchema> = FastifyZodV4Schema<TSchema>>(schema: TSchema, handler: NoInfer<APIHandler<FastifySchema, RequestAugmentation, ReplyAugmentation>>, options?: RouteV4Options) => APIOptions<FastifySchema> & {
27
29
  handler: APIHandler<FastifySchema>;
28
30
  };
29
- export declare const routeV4: <TSchema extends BaseZodV4Schema, FastifySchema extends FastifyZodV4Schema<TSchema> = FastifyZodV4Schema<TSchema>>(schema: TSchema, handler: APIHandler<FastifySchema>, options?: RouteV4Options) => APIOptions<FastifySchema> & {
31
+ export declare const routeV4: <TSchema extends BaseZodV4Schema, FastifySchema extends FastifyZodV4Schema<TSchema> = FastifyZodV4Schema<TSchema>>(schema: TSchema, handler: NoInfer<APIHandler<FastifySchema, {}, {}>>, options?: RouteV4Options) => APIOptions<FastifySchema> & {
30
32
  handler: APIHandler<FastifySchema>;
31
33
  };
@@ -3,67 +3,54 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.routeV4 = exports.createRouteV4 = void 0;
4
4
  const v4_1 = require("zod/v4");
5
5
  const error_js_1 = require("./error.js");
6
- const mapZodError = (zodError, prefix) => {
7
- return zodError.issues.map(issue => {
8
- const pathStr = `Error at ${prefix}->${issue.path.join('->')}`;
9
- return issue.message ? `${pathStr}->${issue.message}` : pathStr;
10
- }).join('\n');
11
- };
12
- const parse = async (schema, payload, tag) => {
13
- const result = await schema.safeParseAsync(payload);
14
- return Object.assign(Object.assign({}, result), { tag });
15
- };
16
- const findStatusCode = (statusCode, availableStatusCodes) => {
17
- return availableStatusCodes.find(([key]) => {
18
- if (!['number', 'string'].includes(typeof key))
19
- return false;
20
- if (typeof key === 'number')
21
- return statusCode === key;
22
- if (/^[0-9]{3}$/.test(key))
23
- return statusCode === parseInt(key);
24
- if (/^[0-9]xx$/i.test(key))
25
- return statusCode.toString()[0] === key[0];
26
- });
27
- };
28
- const strictifySchema = (schema, strict) => {
29
- if (!strict)
30
- return schema;
31
- return 'strict' in schema && typeof schema['strict'] === 'function' ? schema.strict() : schema;
32
- };
33
- const parseStrict = (tag, value) => {
34
- if (typeof value === 'boolean')
35
- return value;
36
- return value[tag];
37
- };
6
+ const routeHelpers_js_1 = require("./routeHelpers.js");
38
7
  const createRouteV4 = ({ strict: globalStrict = false } = {}) => (schema, handler, options) => {
39
- const strict = typeof (options === null || options === void 0 ? void 0 : options.strict) !== 'undefined' ? options === null || options === void 0 ? void 0 : options.strict : globalStrict;
40
- const finalResult = Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign({}, (schema.Body && { body: v4_1.z.toJSONSchema(strictifySchema(schema.Body, parseStrict('body', strict)), { reused: 'inline', target: "draft-7", io: 'input' }) })), (schema.Params && { params: v4_1.z.toJSONSchema(strictifySchema(schema.Params, parseStrict('params', strict)), { reused: 'inline', target: "draft-7", io: 'input' }) })), (schema.Query && { querystring: v4_1.z.toJSONSchema(strictifySchema(schema.Query, parseStrict('query', strict)), { reused: 'inline', target: "draft-7", io: 'input' }) })), (schema.Headers && { headers: v4_1.z.toJSONSchema(strictifySchema(schema.Headers, parseStrict('headers', strict)), { reused: 'inline', target: "draft-7", io: 'input' }) })), { response: v4_1.z.toJSONSchema(schema.Reply.partial(), { reused: 'inline', target: "draft-7" })['properties'] }), (schema.Security && { security: schema.Security })), (schema.Tags && { tags: schema.Tags }));
8
+ const strict = typeof (options === null || options === void 0 ? void 0 : options.strict) !== "undefined" ? options === null || options === void 0 ? void 0 : options.strict : globalStrict;
9
+ const finalResult = Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign({}, (schema.Body && {
10
+ body: v4_1.z.toJSONSchema((0, routeHelpers_js_1.strictifySchema)(schema.Body, (0, routeHelpers_js_1.parseStrict)("body", strict)), { reused: "inline", target: "draft-7", io: "input" }),
11
+ })), (schema.Params && {
12
+ params: v4_1.z.toJSONSchema((0, routeHelpers_js_1.strictifySchema)(schema.Params, (0, routeHelpers_js_1.parseStrict)("params", strict)), { reused: "inline", target: "draft-7", io: "input" }),
13
+ })), (schema.Query && {
14
+ querystring: v4_1.z.toJSONSchema((0, routeHelpers_js_1.strictifySchema)(schema.Query, (0, routeHelpers_js_1.parseStrict)("query", strict)), { reused: "inline", target: "draft-7", io: "input" }),
15
+ })), (schema.Headers && {
16
+ headers: v4_1.z.toJSONSchema((0, routeHelpers_js_1.strictifySchema)(schema.Headers, (0, routeHelpers_js_1.parseStrict)("headers", strict)), { reused: "inline", target: "draft-7", io: "input" }),
17
+ })), { response: v4_1.z.toJSONSchema(schema.Reply.partial(), {
18
+ reused: "inline",
19
+ target: "draft-7",
20
+ })["properties"] }), (schema.Security && { security: schema.Security })), (schema.Tags && { tags: schema.Tags }));
41
21
  return {
42
22
  schema: finalResult,
43
23
  handler,
44
24
  preHandler: async (request, reply) => {
45
25
  var _a, _b, _c;
46
26
  const results = await Promise.all([
47
- ...(schema.Body ? [parse(schema.Body, request.body, 'body')] : []),
48
- ...(schema.Params ? [parse(schema.Params, request.params, 'params')] : []),
49
- ...(schema.Query ? [parse(schema.Query, request.query, 'query')] : []),
27
+ ...(schema.Body ? [(0, routeHelpers_js_1.parse)(schema.Body, request.body, "body")] : []),
28
+ ...(schema.Params
29
+ ? [(0, routeHelpers_js_1.parse)(schema.Params, request.params, "params")]
30
+ : []),
31
+ ...(schema.Query
32
+ ? [(0, routeHelpers_js_1.parse)(schema.Query, request.query, "query")]
33
+ : []),
50
34
  ]);
51
35
  for (const result of results) {
52
36
  if (!result.success) {
53
37
  return reply
54
38
  .code(400)
55
- .type('application/json')
39
+ .type("application/json")
56
40
  .send({
57
- message: mapZodError(result.error, result.tag),
41
+ message: (0, routeHelpers_js_1.mapZodError)(result.error, result.tag),
58
42
  });
59
43
  }
60
44
  }
61
- request.body = ((_a = results.find(r => r.tag === 'body')) === null || _a === void 0 ? void 0 : _a.data) || {};
62
- request.params = ((_b = results.find(r => r.tag === 'params')) === null || _b === void 0 ? void 0 : _b.data) || {};
63
- request.query = ((_c = results.find(r => r.tag === 'query')) === null || _c === void 0 ? void 0 : _c.data) || {};
45
+ request.body =
46
+ ((_a = results.find((r) => r.tag === "body")) === null || _a === void 0 ? void 0 : _a.data) || {};
47
+ request.params =
48
+ ((_b = results.find((r) => r.tag === "params")) === null || _b === void 0 ? void 0 : _b.data) || {};
49
+ request.query =
50
+ ((_c = results.find((r) => r.tag === "query")) === null || _c === void 0 ? void 0 : _c.data) || {};
64
51
  },
65
52
  preSerialization: (request, reply, payload, done) => {
66
- const foundSchema = findStatusCode(reply.statusCode, Object.entries(schema.Reply.shape));
53
+ const foundSchema = (0, routeHelpers_js_1.findStatusCode)(reply.statusCode, Object.entries(schema.Reply.shape));
67
54
  if (!foundSchema) {
68
55
  if (reply.statusCode >= 400)
69
56
  return done(null, payload);
@@ -74,7 +61,7 @@ const createRouteV4 = ({ strict: globalStrict = false } = {}) => (schema, handle
74
61
  if (serialized.success) {
75
62
  return done(null, serialized.data);
76
63
  }
77
- return done(new error_js_1.FastifyZodReplyError(mapZodError(serialized.error, 'reply'), 500));
64
+ return done(new error_js_1.FastifyZodReplyError((0, routeHelpers_js_1.mapZodError)(serialized.error, "reply"), 500));
78
65
  },
79
66
  };
80
67
  };
@@ -0,0 +1,9 @@
1
+ import { FastifyReply } from "fastify";
2
+ import { z } from "zod/v4";
3
+ import { SSEReplyShape, SSERouteV4Options } from "./sseRouteV4.js";
4
+ export declare function sendSseStream<T extends SSEReplyShape>({ reply, stream, schema, options, }: {
5
+ reply: FastifyReply;
6
+ stream: AsyncGenerator<T>;
7
+ schema: z.ZodType<T>;
8
+ options?: SSERouteV4Options["sse"];
9
+ }): Promise<never>;
@@ -0,0 +1,111 @@
1
+ "use strict";
2
+ var __await = (this && this.__await) || function (v) { return this instanceof __await ? (this.v = v, this) : new __await(v); }
3
+ var __asyncGenerator = (this && this.__asyncGenerator) || function (thisArg, _arguments, generator) {
4
+ if (!Symbol.asyncIterator) throw new TypeError("Symbol.asyncIterator is not defined.");
5
+ var g = generator.apply(thisArg, _arguments || []), i, q = [];
6
+ return i = Object.create((typeof AsyncIterator === "function" ? AsyncIterator : Object).prototype), verb("next"), verb("throw"), verb("return", awaitReturn), i[Symbol.asyncIterator] = function () { return this; }, i;
7
+ function awaitReturn(f) { return function (v) { return Promise.resolve(v).then(f, reject); }; }
8
+ function verb(n, f) { if (g[n]) { i[n] = function (v) { return new Promise(function (a, b) { q.push([n, v, a, b]) > 1 || resume(n, v); }); }; if (f) i[n] = f(i[n]); } }
9
+ function resume(n, v) { try { step(g[n](v)); } catch (e) { settle(q[0][3], e); } }
10
+ function step(r) { r.value instanceof __await ? Promise.resolve(r.value.v).then(fulfill, reject) : settle(q[0][2], r); }
11
+ function fulfill(value) { resume("next", value); }
12
+ function reject(value) { resume("throw", value); }
13
+ function settle(f, v) { if (f(v), q.shift(), q.length) resume(q[0][0], q[0][1]); }
14
+ };
15
+ Object.defineProperty(exports, "__esModule", { value: true });
16
+ exports.sendSseStream = sendSseStream;
17
+ const node_stream_1 = require("node:stream");
18
+ const error_js_1 = require("./error.js");
19
+ const routeHelpers_js_1 = require("./routeHelpers.js");
20
+ async function sendSseStream({ reply, stream, schema, options = {}, }) {
21
+ var _a;
22
+ reply.headers({
23
+ "Content-Type": "text/event-stream",
24
+ "Cache-Control": "no-cache, no-transform",
25
+ Connection: "keep-alive",
26
+ Vary: "Origin",
27
+ "X-Accel-Buffering": "no",
28
+ });
29
+ const sseStream = node_stream_1.Readable.from(createSSEStream({
30
+ stream,
31
+ schema,
32
+ logger: reply.log,
33
+ validate: (_a = options.validateStream) !== null && _a !== void 0 ? _a : true,
34
+ intervalMs: options.keepAliveInterval,
35
+ onError: options.onError,
36
+ }));
37
+ return reply.send(sseStream);
38
+ }
39
+ function createSSEStream(_a) {
40
+ return __asyncGenerator(this, arguments, function* createSSEStream_1({ stream, schema, logger, validate, onError, intervalMs = 30000, }) {
41
+ const streamIterator = stream[Symbol.asyncIterator]();
42
+ let keepAliveTimeout = null;
43
+ const scheduleKeepAlive = () => new Promise((resolve) => {
44
+ if (keepAliveTimeout)
45
+ clearTimeout(keepAliveTimeout);
46
+ keepAliveTimeout = setTimeout(() => resolve({ isKeepAlive: true }), intervalMs);
47
+ });
48
+ let nextChunkPromise = streamIterator.next();
49
+ try {
50
+ while (true) {
51
+ const keepAlivePromise = scheduleKeepAlive();
52
+ const result = yield __await(Promise.race([nextChunkPromise, keepAlivePromise]));
53
+ if ("isKeepAlive" in result && result.isKeepAlive) {
54
+ yield yield __await(": keep-alive\n\n");
55
+ continue;
56
+ }
57
+ if ("done" in result) {
58
+ if (keepAliveTimeout)
59
+ clearTimeout(keepAliveTimeout);
60
+ if (result.done)
61
+ break;
62
+ let itemToSend;
63
+ if (validate) {
64
+ const validation = schema.safeParse(result.value);
65
+ if (!validation.success) {
66
+ const errorMessage = (0, routeHelpers_js_1.mapZodError)(validation.error, "stream-item");
67
+ logger.error(`SSE Stream validation error: ${errorMessage}`);
68
+ if (onError)
69
+ onError(new error_js_1.FastifyZodReplyError(errorMessage, 500));
70
+ yield yield __await(formatSSEMessage({ event: "error", data: errorMessage }));
71
+ nextChunkPromise = streamIterator.next();
72
+ continue;
73
+ }
74
+ itemToSend = validation.data;
75
+ }
76
+ else {
77
+ itemToSend = result.value;
78
+ }
79
+ yield yield __await(formatSSEMessage(itemToSend));
80
+ nextChunkPromise = streamIterator.next();
81
+ }
82
+ }
83
+ }
84
+ catch (error) {
85
+ if (onError)
86
+ onError(error);
87
+ logger.error(error);
88
+ if (error instanceof Error && error.name !== "AbortError") {
89
+ yield yield __await(formatSSEMessage({ event: "error", data: error.message }));
90
+ }
91
+ }
92
+ finally {
93
+ if (keepAliveTimeout)
94
+ clearTimeout(keepAliveTimeout);
95
+ }
96
+ });
97
+ }
98
+ function formatSSEMessage(message) {
99
+ let output = "";
100
+ if (message.id !== undefined) {
101
+ output += `id: ${String(message.id)}\n`;
102
+ }
103
+ if (message.retry !== undefined) {
104
+ output += `retry: ${message.retry}\n`;
105
+ }
106
+ if (message.event) {
107
+ output += `event: ${message.event}\n`;
108
+ }
109
+ output += `data: ${JSON.stringify(message.data)}\n\n`;
110
+ return output;
111
+ }
@@ -0,0 +1,57 @@
1
+ import { z } from "zod/v4";
2
+ import { APIHandler, APIOptions, RouteSecurity, RouteTag } from "./types.js";
3
+ export type SSEReplyShape = {
4
+ data: unknown;
5
+ event?: string | undefined;
6
+ id?: string | number | undefined;
7
+ retry?: number | undefined;
8
+ };
9
+ export type SSEBaseZodV4Schema = {
10
+ Body?: z.ZodTypeAny;
11
+ Params?: z.ZodTypeAny;
12
+ Query?: z.ZodTypeAny;
13
+ Headers?: z.ZodTypeAny;
14
+ Reply: z.ZodObject<{
15
+ SSE: z.ZodType<SSEReplyShape>;
16
+ [key: string | number]: z.ZodTypeAny;
17
+ }>;
18
+ Security?: RouteSecurity[keyof RouteSecurity][];
19
+ Tags?: (keyof RouteTag)[];
20
+ };
21
+ type TransformSSETo200<T> = {
22
+ [K in keyof T as K extends "SSE" ? 200 : K]: T[K];
23
+ };
24
+ export type FastifySSEZodV4Schema<TZodSchema extends SSEBaseZodV4Schema> = {
25
+ Body: TZodSchema["Body"] extends z.ZodTypeAny ? z.output<TZodSchema["Body"]> : undefined;
26
+ Params: TZodSchema["Params"] extends z.ZodTypeAny ? z.output<TZodSchema["Params"]> : undefined;
27
+ Querystring: TZodSchema["Query"] extends z.ZodTypeAny ? z.output<TZodSchema["Query"]> : undefined;
28
+ Reply: TZodSchema["Reply"] extends z.ZodTypeAny ? TransformSSETo200<z.input<TZodSchema["Reply"]>> : undefined;
29
+ };
30
+ export type SSERouteV4Options = {
31
+ strict?: boolean | {
32
+ body: boolean;
33
+ query: boolean;
34
+ params: boolean;
35
+ headers: boolean;
36
+ };
37
+ sse?: {
38
+ keepAliveInterval?: number;
39
+ onError?: undefined | ((error: unknown) => void);
40
+ validateStream?: boolean;
41
+ };
42
+ };
43
+ export type SSEAugmentedAPIHandler<TSchema extends SSEBaseZodV4Schema, FastifySchema extends FastifySSEZodV4Schema<TSchema>, RequestAugmentation extends object = {}, ReplyAugmentation extends object = {}> = APIHandler<FastifySchema, RequestAugmentation & {
44
+ abortController: AbortController;
45
+ }, ReplyAugmentation & {
46
+ sse<T extends z.input<TSchema["Reply"]>["SSE"]>(options: {
47
+ stream: AsyncGenerator<T>;
48
+ onError?: (error: unknown) => void;
49
+ }): Promise<T>;
50
+ }>;
51
+ export declare function createSSERouteV4<RequestAugmentation extends object = {}, ReplyAugmentation extends object = {}>(globalOptions?: SSERouteV4Options): <TSchema extends SSEBaseZodV4Schema, FastifySchema extends FastifySSEZodV4Schema<TSchema> = FastifySSEZodV4Schema<TSchema>>(schema: TSchema, handler: NoInfer<SSEAugmentedAPIHandler<TSchema, FastifySchema, RequestAugmentation, ReplyAugmentation>>, options?: SSERouteV4Options) => APIOptions<FastifySchema> & {
52
+ handler: APIHandler<FastifySchema>;
53
+ };
54
+ export declare const sseRouteV4: <TSchema extends SSEBaseZodV4Schema, FastifySchema extends FastifySSEZodV4Schema<TSchema> = FastifySSEZodV4Schema<TSchema>>(schema: TSchema, handler: NoInfer<SSEAugmentedAPIHandler<TSchema, FastifySchema, {}, {}>>, options?: SSERouteV4Options) => APIOptions<FastifySchema> & {
55
+ handler: APIHandler<FastifySchema>;
56
+ };
57
+ export {};
@@ -0,0 +1,99 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.sseRouteV4 = void 0;
4
+ exports.createSSERouteV4 = createSSERouteV4;
5
+ const v4_1 = require("zod/v4");
6
+ const routeHelpers_js_1 = require("./routeHelpers.js");
7
+ const sseHelpers_js_1 = require("./sseHelpers.js");
8
+ function createSSERouteV4(globalOptions = {}) {
9
+ return (schema, handler, options) => {
10
+ const strict = typeof (options === null || options === void 0 ? void 0 : options.strict) !== "undefined"
11
+ ? options === null || options === void 0 ? void 0 : options.strict
12
+ : typeof globalOptions.strict !== "undefined"
13
+ ? globalOptions.strict
14
+ : false;
15
+ const sseOptions = Object.assign(Object.assign({}, globalOptions.sse), options === null || options === void 0 ? void 0 : options.sse);
16
+ const responseJsonSchema = v4_1.z.toJSONSchema(schema.Reply, {
17
+ reused: "inline",
18
+ target: "draft-7",
19
+ })["properties"];
20
+ const responseSSESchema = responseJsonSchema["SSE"];
21
+ if (!responseJsonSchema || responseSSESchema === undefined) {
22
+ throw new Error("An SSE endpoint must define a schema for the 200 status code.");
23
+ }
24
+ delete responseJsonSchema["SSE"];
25
+ const finalResult = Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign({}, (schema.Body && {
26
+ body: v4_1.z.toJSONSchema((0, routeHelpers_js_1.strictifySchema)(schema.Body, (0, routeHelpers_js_1.parseStrict)("body", strict)), {
27
+ reused: "inline",
28
+ target: "draft-7",
29
+ io: "input",
30
+ }),
31
+ })), (schema.Params && {
32
+ params: v4_1.z.toJSONSchema((0, routeHelpers_js_1.strictifySchema)(schema.Params, (0, routeHelpers_js_1.parseStrict)("params", strict)), {
33
+ reused: "inline",
34
+ target: "draft-7",
35
+ io: "input",
36
+ }),
37
+ })), (schema.Query && {
38
+ querystring: v4_1.z.toJSONSchema((0, routeHelpers_js_1.strictifySchema)(schema.Query, (0, routeHelpers_js_1.parseStrict)("query", strict)), {
39
+ reused: "inline",
40
+ target: "draft-7",
41
+ io: "input",
42
+ }),
43
+ })), (schema.Headers && {
44
+ headers: v4_1.z.toJSONSchema((0, routeHelpers_js_1.strictifySchema)(schema.Headers, (0, routeHelpers_js_1.parseStrict)("headers", strict)), {
45
+ reused: "inline",
46
+ target: "draft-7",
47
+ io: "input",
48
+ }),
49
+ })), { response: Object.assign(Object.assign({}, responseJsonSchema), { 200: {
50
+ content: {
51
+ "text/event-stream": {
52
+ schema: responseSSESchema,
53
+ },
54
+ },
55
+ } }) }), (schema.Security && { security: schema.Security })), (schema.Tags && { tags: schema.Tags }));
56
+ return {
57
+ schema: finalResult,
58
+ handler,
59
+ preHandler: async (request, reply) => {
60
+ var _a, _b, _c;
61
+ const results = await Promise.all([
62
+ ...(schema.Body ? [(0, routeHelpers_js_1.parse)(schema.Body, request.body, "body")] : []),
63
+ ...(schema.Params
64
+ ? [(0, routeHelpers_js_1.parse)(schema.Params, request.params, "params")]
65
+ : []),
66
+ ...(schema.Query
67
+ ? [(0, routeHelpers_js_1.parse)(schema.Query, request.query, "query")]
68
+ : []),
69
+ ]);
70
+ for (const result of results) {
71
+ if (!result.success) {
72
+ return reply
73
+ .code(400)
74
+ .type("application/json")
75
+ .send({
76
+ message: (0, routeHelpers_js_1.mapZodError)(result.error, result.tag),
77
+ });
78
+ }
79
+ }
80
+ request.body =
81
+ ((_a = results.find((r) => r.tag === "body")) === null || _a === void 0 ? void 0 : _a.data) || {};
82
+ request.params =
83
+ ((_b = results.find((r) => r.tag === "params")) === null || _b === void 0 ? void 0 : _b.data) || {};
84
+ request.query =
85
+ ((_c = results.find((r) => r.tag === "query")) === null || _c === void 0 ? void 0 : _c.data) || {};
86
+ const abortController = new AbortController();
87
+ request.socket.on("close", () => abortController.abort());
88
+ request.abortController = abortController;
89
+ reply.sse = async (options) => (0, sseHelpers_js_1.sendSseStream)({
90
+ reply,
91
+ stream: options.stream,
92
+ schema: schema.Reply.shape["SSE"],
93
+ options: sseOptions,
94
+ });
95
+ },
96
+ };
97
+ };
98
+ }
99
+ exports.sseRouteV4 = createSSERouteV4({ strict: false });
@@ -1,6 +1,7 @@
1
- import { type RawReplyDefaultExpression, type RawRequestDefaultExpression, type RawServerDefault, type RouteGenericInterface, type RouteHandlerMethod, type RouteShorthandOptions } from 'fastify';
1
+ import { ContextConfigDefault, FastifyBaseLogger, FastifyInstance, FastifyReply, FastifyRequest, FastifySchema, FastifyTypeProviderDefault, type RawReplyDefaultExpression, type RawRequestDefaultExpression, type RawServerDefault, type RouteGenericInterface, type RouteShorthandOptions } from "fastify";
2
+ import { ResolveFastifyReplyReturnType } from "fastify/types/type-provider.js";
2
3
  export type APIOptions<RouteInterface extends RouteGenericInterface = RouteGenericInterface> = RouteShorthandOptions<RawServerDefault, RawRequestDefaultExpression<RawServerDefault>, RawReplyDefaultExpression<RawServerDefault>, RouteInterface>;
3
- export type APIHandler<RouteInterface extends RouteGenericInterface = RouteGenericInterface> = RouteHandlerMethod<RawServerDefault, RawRequestDefaultExpression<RawServerDefault>, RawReplyDefaultExpression<RawServerDefault>, RouteInterface>;
4
+ export type APIHandler<RouteInterface extends RouteGenericInterface = RouteGenericInterface, RequestAugmentation extends object = {}, ReplyAugmentation extends object = {}> = (this: FastifyInstance<RawServerDefault, RawRequestDefaultExpression<RawServerDefault>, RawReplyDefaultExpression<RawServerDefault>, FastifyBaseLogger, FastifyTypeProviderDefault>, request: FastifyRequest<RouteInterface, RawServerDefault, RawRequestDefaultExpression<RawServerDefault>, FastifySchema, FastifyTypeProviderDefault, ContextConfigDefault, FastifyBaseLogger> & RequestAugmentation, reply: FastifyReply<RouteInterface, RawServerDefault, RawRequestDefaultExpression<RawServerDefault>, RawReplyDefaultExpression<RawServerDefault>, ContextConfigDefault, FastifySchema, FastifyTypeProviderDefault> & ReplyAugmentation) => ResolveFastifyReplyReturnType<FastifyTypeProviderDefault, FastifySchema, RouteInterface>;
4
5
  export interface RouteTag {
5
6
  }
6
7
  export interface RouteSecurity {
@@ -1,2 +1,3 @@
1
1
  export declare function isObject(item: any): any;
2
2
  export declare function mergeDeep<TResult extends object>(target: TResult, ...sources: any[]): TResult;
3
+ export declare function sleep(ms: number): Promise<unknown>;
package/lib/cjs/utils.js CHANGED
@@ -2,6 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.isObject = isObject;
4
4
  exports.mergeDeep = mergeDeep;
5
+ exports.sleep = sleep;
5
6
  function isObject(item) {
6
7
  return (item && typeof item === 'object' && !Array.isArray(item));
7
8
  }
@@ -23,3 +24,6 @@ function mergeDeep(target, ...sources) {
23
24
  }
24
25
  return mergeDeep(target, ...sources);
25
26
  }
27
+ async function sleep(ms) {
28
+ return new Promise((resolve) => { setTimeout(resolve, ms); });
29
+ }
@@ -23,12 +23,13 @@ export type FastifyReplyPluginOptions = {
23
23
  export type DecoratedReply = {
24
24
  [key in keyof FastifyStatusCode]: <T>(val?: T) => T;
25
25
  };
26
- declare module 'fastify' {
26
+ declare module "fastify" {
27
27
  interface FastifyReply extends DecoratedReply {
28
28
  }
29
29
  }
30
30
  declare const _default: import("fastify").FastifyPluginCallback<FastifyReplyPluginOptions, import("fastify").RawServerDefault, import("fastify").FastifyTypeProviderDefault, import("fastify").FastifyBaseLogger>;
31
31
  export default _default;
32
- export * from './error.js';
33
- export * from './routeV4.js';
34
- export * from './types.js';
32
+ export * from "./error.js";
33
+ export * from "./routeV4.js";
34
+ export * from "./sseRouteV4.js";
35
+ export * from "./types.js";
package/lib/esm/index.js CHANGED
@@ -3,28 +3,34 @@ import { createReply } from "./reply.js";
3
3
  import { mergeDeep } from "./utils.js";
4
4
  const defaultOptions = {
5
5
  statusCodes: {
6
- ok: { statusCode: 200, payload: { message: 'ok' } },
7
- created: { statusCode: 201, payload: { message: 'created' } },
8
- accepted: { statusCode: 202, payload: { message: 'accepted' } },
9
- noContent: { statusCode: 204, payload: undefined },
10
- badRequest: { statusCode: 400, payload: { message: 'badRequest' } },
11
- unauthorized: { statusCode: 401, payload: { message: 'unauthorized' } },
12
- forbidden: { statusCode: 403, payload: { message: 'forbidden' } },
13
- notFound: { statusCode: 404, payload: { message: 'notFound' } },
14
- notAcceptable: { statusCode: 406, payload: { message: 'notAcceptable' } },
15
- conflict: { statusCode: 409, payload: { message: 'conflict' } },
16
- internalServerError: { statusCode: 500, payload: { message: 'internalServerError' } },
17
- }
6
+ ok: { statusCode: 200, payload: { message: "ok" } },
7
+ created: { statusCode: 201, payload: { message: "created" } },
8
+ accepted: { statusCode: 202, payload: { message: "accepted" } },
9
+ noContent: { statusCode: 204, payload: null },
10
+ badRequest: { statusCode: 400, payload: { message: "badRequest" } },
11
+ unauthorized: { statusCode: 401, payload: { message: "unauthorized" } },
12
+ forbidden: { statusCode: 403, payload: { message: "forbidden" } },
13
+ notFound: { statusCode: 404, payload: { message: "notFound" } },
14
+ notAcceptable: { statusCode: 406, payload: { message: "notAcceptable" } },
15
+ conflict: { statusCode: 409, payload: { message: "conflict" } },
16
+ internalServerError: {
17
+ statusCode: 500,
18
+ payload: { message: "internalServerError" },
19
+ },
20
+ },
18
21
  };
19
22
  export default fp(async (fastify, opts) => {
20
23
  const finalOptions = mergeDeep(defaultOptions, opts);
21
24
  Object.entries(finalOptions.statusCodes).forEach(([key, value]) => {
22
25
  fastify.decorateReply(key, createReply(value.statusCode, value.payload));
23
26
  });
27
+ fastify.decorateRequest("abortController");
28
+ fastify.decorateReply("sse");
24
29
  }, {
25
30
  fastify: "5.x",
26
- name: '@efebia/fastify-zod-reply'
31
+ name: "@efebia/fastify-zod-reply",
27
32
  });
28
- export * from './error.js';
29
- export * from './routeV4.js';
30
- export * from './types.js';
33
+ export * from "./error.js";
34
+ export * from "./routeV4.js";
35
+ export * from "./sseRouteV4.js";
36
+ export * from "./types.js";
@@ -0,0 +1,17 @@
1
+ import { z } from "zod/v4";
2
+ import { RouteV4Options } from "./routeV4.js";
3
+ export declare const parse: (schema: z.ZodTypeAny, payload: any, tag: string) => Promise<{
4
+ tag: string;
5
+ success: true;
6
+ data: unknown;
7
+ error?: never;
8
+ } | {
9
+ tag: string;
10
+ success: false;
11
+ data?: never;
12
+ error: z.ZodError<unknown>;
13
+ }>;
14
+ export declare const strictifySchema: (schema: z.ZodType, strict: boolean) => any;
15
+ export declare const parseStrict: (tag: keyof Exclude<NonNullable<RouteV4Options["strict"]>, boolean>, value: NonNullable<RouteV4Options["strict"]>) => boolean;
16
+ export declare const findStatusCode: (statusCode: number, availableStatusCodes: [string | number, any][]) => [string | number, any] | undefined;
17
+ export declare const mapZodError: (zodError: z.ZodError, prefix: string) => string;
@@ -0,0 +1,36 @@
1
+ export const parse = async (schema, payload, tag) => {
2
+ const result = await schema.safeParseAsync(payload);
3
+ return Object.assign(Object.assign({}, result), { tag });
4
+ };
5
+ export const strictifySchema = (schema, strict) => {
6
+ if (!strict)
7
+ return schema;
8
+ return "strict" in schema && typeof schema["strict"] === "function"
9
+ ? schema.strict()
10
+ : schema;
11
+ };
12
+ export const parseStrict = (tag, value) => {
13
+ if (typeof value === "boolean")
14
+ return value;
15
+ return value[tag];
16
+ };
17
+ export const findStatusCode = (statusCode, availableStatusCodes) => {
18
+ return availableStatusCodes.find(([key]) => {
19
+ if (!["number", "string"].includes(typeof key))
20
+ return false;
21
+ if (typeof key === "number")
22
+ return statusCode === key;
23
+ if (/^[0-9]{3}$/.test(key))
24
+ return statusCode === parseInt(key);
25
+ if (/^[0-9]xx$/i.test(key))
26
+ return statusCode.toString()[0] === key[0];
27
+ });
28
+ };
29
+ export const mapZodError = (zodError, prefix) => {
30
+ return zodError.issues
31
+ .map((issue) => {
32
+ const pathStr = `Error at ${prefix}->${issue.path.join("->")}`;
33
+ return issue.message ? `${pathStr}->${issue.message}` : pathStr;
34
+ })
35
+ .join("\n");
36
+ };
@@ -1,19 +1,21 @@
1
- import { z } from 'zod/v4';
2
- import { APIHandler, APIOptions, RouteSecurity, RouteTag } from './types.js';
1
+ import { z } from "zod/v4";
2
+ import { APIHandler, APIOptions, RouteSecurity, RouteTag } from "./types.js";
3
3
  export type BaseZodV4Schema = {
4
4
  Body?: z.ZodTypeAny;
5
5
  Params?: z.ZodTypeAny;
6
6
  Query?: z.ZodTypeAny;
7
7
  Headers?: z.ZodTypeAny;
8
- Reply: z.ZodObject;
9
- Security?: (RouteSecurity[keyof RouteSecurity])[];
8
+ Reply: z.ZodObject<{
9
+ [key: string | number]: z.ZodTypeAny;
10
+ }>;
11
+ Security?: RouteSecurity[keyof RouteSecurity][];
10
12
  Tags?: (keyof RouteTag)[];
11
13
  };
12
14
  export type FastifyZodV4Schema<TZodSchema extends BaseZodV4Schema> = {
13
- Body: TZodSchema['Body'] extends z.ZodTypeAny ? z.output<TZodSchema['Body']> : undefined;
14
- Params: TZodSchema['Params'] extends z.ZodTypeAny ? z.output<TZodSchema['Params']> : undefined;
15
- Querystring: TZodSchema['Query'] extends z.ZodTypeAny ? z.output<TZodSchema['Query']> : undefined;
16
- Reply: TZodSchema['Reply'] extends z.ZodTypeAny ? z.input<TZodSchema['Reply']>[keyof z.input<TZodSchema['Reply']>] : undefined;
15
+ Body: TZodSchema["Body"] extends z.ZodTypeAny ? z.output<TZodSchema["Body"]> : undefined;
16
+ Params: TZodSchema["Params"] extends z.ZodTypeAny ? z.output<TZodSchema["Params"]> : undefined;
17
+ Querystring: TZodSchema["Query"] extends z.ZodTypeAny ? z.output<TZodSchema["Query"]> : undefined;
18
+ Reply: TZodSchema["Reply"] extends z.ZodTypeAny ? z.input<TZodSchema["Reply"]>[keyof z.input<TZodSchema["Reply"]>] : undefined;
17
19
  };
18
20
  export type RouteV4Options = {
19
21
  strict?: boolean | {
@@ -23,9 +25,9 @@ export type RouteV4Options = {
23
25
  headers: boolean;
24
26
  };
25
27
  };
26
- export declare const createRouteV4: ({ strict: globalStrict }?: RouteV4Options) => <TSchema extends BaseZodV4Schema, FastifySchema extends FastifyZodV4Schema<TSchema> = FastifyZodV4Schema<TSchema>>(schema: TSchema, handler: APIHandler<FastifySchema>, options?: RouteV4Options) => APIOptions<FastifySchema> & {
28
+ export declare const createRouteV4: <RequestAugmentation extends object = {}, ReplyAugmentation extends object = {}>({ strict: globalStrict }?: RouteV4Options) => <TSchema extends BaseZodV4Schema, FastifySchema extends FastifyZodV4Schema<TSchema> = FastifyZodV4Schema<TSchema>>(schema: TSchema, handler: NoInfer<APIHandler<FastifySchema, RequestAugmentation, ReplyAugmentation>>, options?: RouteV4Options) => APIOptions<FastifySchema> & {
27
29
  handler: APIHandler<FastifySchema>;
28
30
  };
29
- export declare const routeV4: <TSchema extends BaseZodV4Schema, FastifySchema extends FastifyZodV4Schema<TSchema> = FastifyZodV4Schema<TSchema>>(schema: TSchema, handler: APIHandler<FastifySchema>, options?: RouteV4Options) => APIOptions<FastifySchema> & {
31
+ export declare const routeV4: <TSchema extends BaseZodV4Schema, FastifySchema extends FastifyZodV4Schema<TSchema> = FastifyZodV4Schema<TSchema>>(schema: TSchema, handler: NoInfer<APIHandler<FastifySchema, {}, {}>>, options?: RouteV4Options) => APIOptions<FastifySchema> & {
30
32
  handler: APIHandler<FastifySchema>;
31
33
  };
@@ -1,63 +1,50 @@
1
- import { z } from 'zod/v4';
2
- import { FastifyZodReplyError } from './error.js';
3
- const mapZodError = (zodError, prefix) => {
4
- return zodError.issues.map(issue => {
5
- const pathStr = `Error at ${prefix}->${issue.path.join('->')}`;
6
- return issue.message ? `${pathStr}->${issue.message}` : pathStr;
7
- }).join('\n');
8
- };
9
- const parse = async (schema, payload, tag) => {
10
- const result = await schema.safeParseAsync(payload);
11
- return Object.assign(Object.assign({}, result), { tag });
12
- };
13
- const findStatusCode = (statusCode, availableStatusCodes) => {
14
- return availableStatusCodes.find(([key]) => {
15
- if (!['number', 'string'].includes(typeof key))
16
- return false;
17
- if (typeof key === 'number')
18
- return statusCode === key;
19
- if (/^[0-9]{3}$/.test(key))
20
- return statusCode === parseInt(key);
21
- if (/^[0-9]xx$/i.test(key))
22
- return statusCode.toString()[0] === key[0];
23
- });
24
- };
25
- const strictifySchema = (schema, strict) => {
26
- if (!strict)
27
- return schema;
28
- return 'strict' in schema && typeof schema['strict'] === 'function' ? schema.strict() : schema;
29
- };
30
- const parseStrict = (tag, value) => {
31
- if (typeof value === 'boolean')
32
- return value;
33
- return value[tag];
34
- };
1
+ import { z } from "zod/v4";
2
+ import { FastifyZodReplyError } from "./error.js";
3
+ import { findStatusCode, mapZodError, parse, parseStrict, strictifySchema, } from "./routeHelpers.js";
35
4
  export const createRouteV4 = ({ strict: globalStrict = false } = {}) => (schema, handler, options) => {
36
- const strict = typeof (options === null || options === void 0 ? void 0 : options.strict) !== 'undefined' ? options === null || options === void 0 ? void 0 : options.strict : globalStrict;
37
- const finalResult = Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign({}, (schema.Body && { body: z.toJSONSchema(strictifySchema(schema.Body, parseStrict('body', strict)), { reused: 'inline', target: "draft-7", io: 'input' }) })), (schema.Params && { params: z.toJSONSchema(strictifySchema(schema.Params, parseStrict('params', strict)), { reused: 'inline', target: "draft-7", io: 'input' }) })), (schema.Query && { querystring: z.toJSONSchema(strictifySchema(schema.Query, parseStrict('query', strict)), { reused: 'inline', target: "draft-7", io: 'input' }) })), (schema.Headers && { headers: z.toJSONSchema(strictifySchema(schema.Headers, parseStrict('headers', strict)), { reused: 'inline', target: "draft-7", io: 'input' }) })), { response: z.toJSONSchema(schema.Reply.partial(), { reused: 'inline', target: "draft-7" })['properties'] }), (schema.Security && { security: schema.Security })), (schema.Tags && { tags: schema.Tags }));
5
+ const strict = typeof (options === null || options === void 0 ? void 0 : options.strict) !== "undefined" ? options === null || options === void 0 ? void 0 : options.strict : globalStrict;
6
+ const finalResult = Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign({}, (schema.Body && {
7
+ body: z.toJSONSchema(strictifySchema(schema.Body, parseStrict("body", strict)), { reused: "inline", target: "draft-7", io: "input" }),
8
+ })), (schema.Params && {
9
+ params: z.toJSONSchema(strictifySchema(schema.Params, parseStrict("params", strict)), { reused: "inline", target: "draft-7", io: "input" }),
10
+ })), (schema.Query && {
11
+ querystring: z.toJSONSchema(strictifySchema(schema.Query, parseStrict("query", strict)), { reused: "inline", target: "draft-7", io: "input" }),
12
+ })), (schema.Headers && {
13
+ headers: z.toJSONSchema(strictifySchema(schema.Headers, parseStrict("headers", strict)), { reused: "inline", target: "draft-7", io: "input" }),
14
+ })), { response: z.toJSONSchema(schema.Reply.partial(), {
15
+ reused: "inline",
16
+ target: "draft-7",
17
+ })["properties"] }), (schema.Security && { security: schema.Security })), (schema.Tags && { tags: schema.Tags }));
38
18
  return {
39
19
  schema: finalResult,
40
20
  handler,
41
21
  preHandler: async (request, reply) => {
42
22
  var _a, _b, _c;
43
23
  const results = await Promise.all([
44
- ...(schema.Body ? [parse(schema.Body, request.body, 'body')] : []),
45
- ...(schema.Params ? [parse(schema.Params, request.params, 'params')] : []),
46
- ...(schema.Query ? [parse(schema.Query, request.query, 'query')] : []),
24
+ ...(schema.Body ? [parse(schema.Body, request.body, "body")] : []),
25
+ ...(schema.Params
26
+ ? [parse(schema.Params, request.params, "params")]
27
+ : []),
28
+ ...(schema.Query
29
+ ? [parse(schema.Query, request.query, "query")]
30
+ : []),
47
31
  ]);
48
32
  for (const result of results) {
49
33
  if (!result.success) {
50
34
  return reply
51
35
  .code(400)
52
- .type('application/json')
36
+ .type("application/json")
53
37
  .send({
54
38
  message: mapZodError(result.error, result.tag),
55
39
  });
56
40
  }
57
41
  }
58
- request.body = ((_a = results.find(r => r.tag === 'body')) === null || _a === void 0 ? void 0 : _a.data) || {};
59
- request.params = ((_b = results.find(r => r.tag === 'params')) === null || _b === void 0 ? void 0 : _b.data) || {};
60
- request.query = ((_c = results.find(r => r.tag === 'query')) === null || _c === void 0 ? void 0 : _c.data) || {};
42
+ request.body =
43
+ ((_a = results.find((r) => r.tag === "body")) === null || _a === void 0 ? void 0 : _a.data) || {};
44
+ request.params =
45
+ ((_b = results.find((r) => r.tag === "params")) === null || _b === void 0 ? void 0 : _b.data) || {};
46
+ request.query =
47
+ ((_c = results.find((r) => r.tag === "query")) === null || _c === void 0 ? void 0 : _c.data) || {};
61
48
  },
62
49
  preSerialization: (request, reply, payload, done) => {
63
50
  const foundSchema = findStatusCode(reply.statusCode, Object.entries(schema.Reply.shape));
@@ -71,7 +58,7 @@ export const createRouteV4 = ({ strict: globalStrict = false } = {}) => (schema,
71
58
  if (serialized.success) {
72
59
  return done(null, serialized.data);
73
60
  }
74
- return done(new FastifyZodReplyError(mapZodError(serialized.error, 'reply'), 500));
61
+ return done(new FastifyZodReplyError(mapZodError(serialized.error, "reply"), 500));
75
62
  },
76
63
  };
77
64
  };
@@ -0,0 +1,9 @@
1
+ import { FastifyReply } from "fastify";
2
+ import { z } from "zod/v4";
3
+ import { SSEReplyShape, SSERouteV4Options } from "./sseRouteV4.js";
4
+ export declare function sendSseStream<T extends SSEReplyShape>({ reply, stream, schema, options, }: {
5
+ reply: FastifyReply;
6
+ stream: AsyncGenerator<T>;
7
+ schema: z.ZodType<T>;
8
+ options?: SSERouteV4Options["sse"];
9
+ }): Promise<never>;
@@ -0,0 +1,108 @@
1
+ var __await = (this && this.__await) || function (v) { return this instanceof __await ? (this.v = v, this) : new __await(v); }
2
+ var __asyncGenerator = (this && this.__asyncGenerator) || function (thisArg, _arguments, generator) {
3
+ if (!Symbol.asyncIterator) throw new TypeError("Symbol.asyncIterator is not defined.");
4
+ var g = generator.apply(thisArg, _arguments || []), i, q = [];
5
+ return i = Object.create((typeof AsyncIterator === "function" ? AsyncIterator : Object).prototype), verb("next"), verb("throw"), verb("return", awaitReturn), i[Symbol.asyncIterator] = function () { return this; }, i;
6
+ function awaitReturn(f) { return function (v) { return Promise.resolve(v).then(f, reject); }; }
7
+ function verb(n, f) { if (g[n]) { i[n] = function (v) { return new Promise(function (a, b) { q.push([n, v, a, b]) > 1 || resume(n, v); }); }; if (f) i[n] = f(i[n]); } }
8
+ function resume(n, v) { try { step(g[n](v)); } catch (e) { settle(q[0][3], e); } }
9
+ function step(r) { r.value instanceof __await ? Promise.resolve(r.value.v).then(fulfill, reject) : settle(q[0][2], r); }
10
+ function fulfill(value) { resume("next", value); }
11
+ function reject(value) { resume("throw", value); }
12
+ function settle(f, v) { if (f(v), q.shift(), q.length) resume(q[0][0], q[0][1]); }
13
+ };
14
+ import { Readable } from "node:stream";
15
+ import { FastifyZodReplyError } from "./error.js";
16
+ import { mapZodError } from "./routeHelpers.js";
17
+ export async function sendSseStream({ reply, stream, schema, options = {}, }) {
18
+ var _a;
19
+ reply.headers({
20
+ "Content-Type": "text/event-stream",
21
+ "Cache-Control": "no-cache, no-transform",
22
+ Connection: "keep-alive",
23
+ Vary: "Origin",
24
+ "X-Accel-Buffering": "no",
25
+ });
26
+ const sseStream = Readable.from(createSSEStream({
27
+ stream,
28
+ schema,
29
+ logger: reply.log,
30
+ validate: (_a = options.validateStream) !== null && _a !== void 0 ? _a : true,
31
+ intervalMs: options.keepAliveInterval,
32
+ onError: options.onError,
33
+ }));
34
+ return reply.send(sseStream);
35
+ }
36
+ function createSSEStream(_a) {
37
+ return __asyncGenerator(this, arguments, function* createSSEStream_1({ stream, schema, logger, validate, onError, intervalMs = 30000, }) {
38
+ const streamIterator = stream[Symbol.asyncIterator]();
39
+ let keepAliveTimeout = null;
40
+ const scheduleKeepAlive = () => new Promise((resolve) => {
41
+ if (keepAliveTimeout)
42
+ clearTimeout(keepAliveTimeout);
43
+ keepAliveTimeout = setTimeout(() => resolve({ isKeepAlive: true }), intervalMs);
44
+ });
45
+ let nextChunkPromise = streamIterator.next();
46
+ try {
47
+ while (true) {
48
+ const keepAlivePromise = scheduleKeepAlive();
49
+ const result = yield __await(Promise.race([nextChunkPromise, keepAlivePromise]));
50
+ if ("isKeepAlive" in result && result.isKeepAlive) {
51
+ yield yield __await(": keep-alive\n\n");
52
+ continue;
53
+ }
54
+ if ("done" in result) {
55
+ if (keepAliveTimeout)
56
+ clearTimeout(keepAliveTimeout);
57
+ if (result.done)
58
+ break;
59
+ let itemToSend;
60
+ if (validate) {
61
+ const validation = schema.safeParse(result.value);
62
+ if (!validation.success) {
63
+ const errorMessage = mapZodError(validation.error, "stream-item");
64
+ logger.error(`SSE Stream validation error: ${errorMessage}`);
65
+ if (onError)
66
+ onError(new FastifyZodReplyError(errorMessage, 500));
67
+ yield yield __await(formatSSEMessage({ event: "error", data: errorMessage }));
68
+ nextChunkPromise = streamIterator.next();
69
+ continue;
70
+ }
71
+ itemToSend = validation.data;
72
+ }
73
+ else {
74
+ itemToSend = result.value;
75
+ }
76
+ yield yield __await(formatSSEMessage(itemToSend));
77
+ nextChunkPromise = streamIterator.next();
78
+ }
79
+ }
80
+ }
81
+ catch (error) {
82
+ if (onError)
83
+ onError(error);
84
+ logger.error(error);
85
+ if (error instanceof Error && error.name !== "AbortError") {
86
+ yield yield __await(formatSSEMessage({ event: "error", data: error.message }));
87
+ }
88
+ }
89
+ finally {
90
+ if (keepAliveTimeout)
91
+ clearTimeout(keepAliveTimeout);
92
+ }
93
+ });
94
+ }
95
+ function formatSSEMessage(message) {
96
+ let output = "";
97
+ if (message.id !== undefined) {
98
+ output += `id: ${String(message.id)}\n`;
99
+ }
100
+ if (message.retry !== undefined) {
101
+ output += `retry: ${message.retry}\n`;
102
+ }
103
+ if (message.event) {
104
+ output += `event: ${message.event}\n`;
105
+ }
106
+ output += `data: ${JSON.stringify(message.data)}\n\n`;
107
+ return output;
108
+ }
@@ -0,0 +1,57 @@
1
+ import { z } from "zod/v4";
2
+ import { APIHandler, APIOptions, RouteSecurity, RouteTag } from "./types.js";
3
+ export type SSEReplyShape = {
4
+ data: unknown;
5
+ event?: string | undefined;
6
+ id?: string | number | undefined;
7
+ retry?: number | undefined;
8
+ };
9
+ export type SSEBaseZodV4Schema = {
10
+ Body?: z.ZodTypeAny;
11
+ Params?: z.ZodTypeAny;
12
+ Query?: z.ZodTypeAny;
13
+ Headers?: z.ZodTypeAny;
14
+ Reply: z.ZodObject<{
15
+ SSE: z.ZodType<SSEReplyShape>;
16
+ [key: string | number]: z.ZodTypeAny;
17
+ }>;
18
+ Security?: RouteSecurity[keyof RouteSecurity][];
19
+ Tags?: (keyof RouteTag)[];
20
+ };
21
+ type TransformSSETo200<T> = {
22
+ [K in keyof T as K extends "SSE" ? 200 : K]: T[K];
23
+ };
24
+ export type FastifySSEZodV4Schema<TZodSchema extends SSEBaseZodV4Schema> = {
25
+ Body: TZodSchema["Body"] extends z.ZodTypeAny ? z.output<TZodSchema["Body"]> : undefined;
26
+ Params: TZodSchema["Params"] extends z.ZodTypeAny ? z.output<TZodSchema["Params"]> : undefined;
27
+ Querystring: TZodSchema["Query"] extends z.ZodTypeAny ? z.output<TZodSchema["Query"]> : undefined;
28
+ Reply: TZodSchema["Reply"] extends z.ZodTypeAny ? TransformSSETo200<z.input<TZodSchema["Reply"]>> : undefined;
29
+ };
30
+ export type SSERouteV4Options = {
31
+ strict?: boolean | {
32
+ body: boolean;
33
+ query: boolean;
34
+ params: boolean;
35
+ headers: boolean;
36
+ };
37
+ sse?: {
38
+ keepAliveInterval?: number;
39
+ onError?: undefined | ((error: unknown) => void);
40
+ validateStream?: boolean;
41
+ };
42
+ };
43
+ export type SSEAugmentedAPIHandler<TSchema extends SSEBaseZodV4Schema, FastifySchema extends FastifySSEZodV4Schema<TSchema>, RequestAugmentation extends object = {}, ReplyAugmentation extends object = {}> = APIHandler<FastifySchema, RequestAugmentation & {
44
+ abortController: AbortController;
45
+ }, ReplyAugmentation & {
46
+ sse<T extends z.input<TSchema["Reply"]>["SSE"]>(options: {
47
+ stream: AsyncGenerator<T>;
48
+ onError?: (error: unknown) => void;
49
+ }): Promise<T>;
50
+ }>;
51
+ export declare function createSSERouteV4<RequestAugmentation extends object = {}, ReplyAugmentation extends object = {}>(globalOptions?: SSERouteV4Options): <TSchema extends SSEBaseZodV4Schema, FastifySchema extends FastifySSEZodV4Schema<TSchema> = FastifySSEZodV4Schema<TSchema>>(schema: TSchema, handler: NoInfer<SSEAugmentedAPIHandler<TSchema, FastifySchema, RequestAugmentation, ReplyAugmentation>>, options?: SSERouteV4Options) => APIOptions<FastifySchema> & {
52
+ handler: APIHandler<FastifySchema>;
53
+ };
54
+ export declare const sseRouteV4: <TSchema extends SSEBaseZodV4Schema, FastifySchema extends FastifySSEZodV4Schema<TSchema> = FastifySSEZodV4Schema<TSchema>>(schema: TSchema, handler: NoInfer<SSEAugmentedAPIHandler<TSchema, FastifySchema, {}, {}>>, options?: SSERouteV4Options) => APIOptions<FastifySchema> & {
55
+ handler: APIHandler<FastifySchema>;
56
+ };
57
+ export {};
@@ -0,0 +1,95 @@
1
+ import { z } from "zod/v4";
2
+ import { mapZodError, parse, parseStrict, strictifySchema, } from "./routeHelpers.js";
3
+ import { sendSseStream } from "./sseHelpers.js";
4
+ export function createSSERouteV4(globalOptions = {}) {
5
+ return (schema, handler, options) => {
6
+ const strict = typeof (options === null || options === void 0 ? void 0 : options.strict) !== "undefined"
7
+ ? options === null || options === void 0 ? void 0 : options.strict
8
+ : typeof globalOptions.strict !== "undefined"
9
+ ? globalOptions.strict
10
+ : false;
11
+ const sseOptions = Object.assign(Object.assign({}, globalOptions.sse), options === null || options === void 0 ? void 0 : options.sse);
12
+ const responseJsonSchema = z.toJSONSchema(schema.Reply, {
13
+ reused: "inline",
14
+ target: "draft-7",
15
+ })["properties"];
16
+ const responseSSESchema = responseJsonSchema["SSE"];
17
+ if (!responseJsonSchema || responseSSESchema === undefined) {
18
+ throw new Error("An SSE endpoint must define a schema for the 200 status code.");
19
+ }
20
+ delete responseJsonSchema["SSE"];
21
+ const finalResult = Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign({}, (schema.Body && {
22
+ body: z.toJSONSchema(strictifySchema(schema.Body, parseStrict("body", strict)), {
23
+ reused: "inline",
24
+ target: "draft-7",
25
+ io: "input",
26
+ }),
27
+ })), (schema.Params && {
28
+ params: z.toJSONSchema(strictifySchema(schema.Params, parseStrict("params", strict)), {
29
+ reused: "inline",
30
+ target: "draft-7",
31
+ io: "input",
32
+ }),
33
+ })), (schema.Query && {
34
+ querystring: z.toJSONSchema(strictifySchema(schema.Query, parseStrict("query", strict)), {
35
+ reused: "inline",
36
+ target: "draft-7",
37
+ io: "input",
38
+ }),
39
+ })), (schema.Headers && {
40
+ headers: z.toJSONSchema(strictifySchema(schema.Headers, parseStrict("headers", strict)), {
41
+ reused: "inline",
42
+ target: "draft-7",
43
+ io: "input",
44
+ }),
45
+ })), { response: Object.assign(Object.assign({}, responseJsonSchema), { 200: {
46
+ content: {
47
+ "text/event-stream": {
48
+ schema: responseSSESchema,
49
+ },
50
+ },
51
+ } }) }), (schema.Security && { security: schema.Security })), (schema.Tags && { tags: schema.Tags }));
52
+ return {
53
+ schema: finalResult,
54
+ handler,
55
+ preHandler: async (request, reply) => {
56
+ var _a, _b, _c;
57
+ const results = await Promise.all([
58
+ ...(schema.Body ? [parse(schema.Body, request.body, "body")] : []),
59
+ ...(schema.Params
60
+ ? [parse(schema.Params, request.params, "params")]
61
+ : []),
62
+ ...(schema.Query
63
+ ? [parse(schema.Query, request.query, "query")]
64
+ : []),
65
+ ]);
66
+ for (const result of results) {
67
+ if (!result.success) {
68
+ return reply
69
+ .code(400)
70
+ .type("application/json")
71
+ .send({
72
+ message: mapZodError(result.error, result.tag),
73
+ });
74
+ }
75
+ }
76
+ request.body =
77
+ ((_a = results.find((r) => r.tag === "body")) === null || _a === void 0 ? void 0 : _a.data) || {};
78
+ request.params =
79
+ ((_b = results.find((r) => r.tag === "params")) === null || _b === void 0 ? void 0 : _b.data) || {};
80
+ request.query =
81
+ ((_c = results.find((r) => r.tag === "query")) === null || _c === void 0 ? void 0 : _c.data) || {};
82
+ const abortController = new AbortController();
83
+ request.socket.on("close", () => abortController.abort());
84
+ request.abortController = abortController;
85
+ reply.sse = async (options) => sendSseStream({
86
+ reply,
87
+ stream: options.stream,
88
+ schema: schema.Reply.shape["SSE"],
89
+ options: sseOptions,
90
+ });
91
+ },
92
+ };
93
+ };
94
+ }
95
+ export const sseRouteV4 = createSSERouteV4({ strict: false });
@@ -1,6 +1,7 @@
1
- import { type RawReplyDefaultExpression, type RawRequestDefaultExpression, type RawServerDefault, type RouteGenericInterface, type RouteHandlerMethod, type RouteShorthandOptions } from 'fastify';
1
+ import { ContextConfigDefault, FastifyBaseLogger, FastifyInstance, FastifyReply, FastifyRequest, FastifySchema, FastifyTypeProviderDefault, type RawReplyDefaultExpression, type RawRequestDefaultExpression, type RawServerDefault, type RouteGenericInterface, type RouteShorthandOptions } from "fastify";
2
+ import { ResolveFastifyReplyReturnType } from "fastify/types/type-provider.js";
2
3
  export type APIOptions<RouteInterface extends RouteGenericInterface = RouteGenericInterface> = RouteShorthandOptions<RawServerDefault, RawRequestDefaultExpression<RawServerDefault>, RawReplyDefaultExpression<RawServerDefault>, RouteInterface>;
3
- export type APIHandler<RouteInterface extends RouteGenericInterface = RouteGenericInterface> = RouteHandlerMethod<RawServerDefault, RawRequestDefaultExpression<RawServerDefault>, RawReplyDefaultExpression<RawServerDefault>, RouteInterface>;
4
+ export type APIHandler<RouteInterface extends RouteGenericInterface = RouteGenericInterface, RequestAugmentation extends object = {}, ReplyAugmentation extends object = {}> = (this: FastifyInstance<RawServerDefault, RawRequestDefaultExpression<RawServerDefault>, RawReplyDefaultExpression<RawServerDefault>, FastifyBaseLogger, FastifyTypeProviderDefault>, request: FastifyRequest<RouteInterface, RawServerDefault, RawRequestDefaultExpression<RawServerDefault>, FastifySchema, FastifyTypeProviderDefault, ContextConfigDefault, FastifyBaseLogger> & RequestAugmentation, reply: FastifyReply<RouteInterface, RawServerDefault, RawRequestDefaultExpression<RawServerDefault>, RawReplyDefaultExpression<RawServerDefault>, ContextConfigDefault, FastifySchema, FastifyTypeProviderDefault> & ReplyAugmentation) => ResolveFastifyReplyReturnType<FastifyTypeProviderDefault, FastifySchema, RouteInterface>;
4
5
  export interface RouteTag {
5
6
  }
6
7
  export interface RouteSecurity {
@@ -1,2 +1,3 @@
1
1
  export declare function isObject(item: any): any;
2
2
  export declare function mergeDeep<TResult extends object>(target: TResult, ...sources: any[]): TResult;
3
+ export declare function sleep(ms: number): Promise<unknown>;
package/lib/esm/utils.js CHANGED
@@ -19,3 +19,6 @@ export function mergeDeep(target, ...sources) {
19
19
  }
20
20
  return mergeDeep(target, ...sources);
21
21
  }
22
+ export async function sleep(ms) {
23
+ return new Promise((resolve) => { setTimeout(resolve, ms); });
24
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@efebia/fastify-zod-reply",
3
- "version": "1.3.0",
3
+ "version": "1.4.0",
4
4
  "license": "MIT",
5
5
  "dependencies": {
6
6
  "fastify": "^5.3.0",