@efebia/fastify-zod-reply 1.1.0 → 1.2.1

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.
@@ -1,35 +1,16 @@
1
- export type StatusCode<TCode> = {
2
- statusCode: TCode;
3
- payload: any;
4
- };
5
- export interface FastifyStatusCode {
6
- ok: StatusCode<200>;
7
- created: StatusCode<201>;
8
- accepted: StatusCode<202>;
9
- noContent: StatusCode<204>;
10
- badRequest: StatusCode<400>;
11
- unauthorized: StatusCode<401>;
12
- forbidden: StatusCode<403>;
13
- notFound: StatusCode<404>;
14
- notAcceptable: StatusCode<406>;
15
- conflict: StatusCode<409>;
16
- internalServerError: StatusCode<500>;
17
- }
18
- export type FastifyReplyPluginOptions = {
19
- statusCodes?: {
20
- [key in keyof FastifyStatusCode]?: FastifyStatusCode[key];
21
- };
22
- };
1
+ import { plugin, type FastifyReplyPluginOptions, type StatusCode, type StatusCodeBuilder } from './plugin.js';
2
+ import { StatusCodeKey } from './types.js';
23
3
  export type DecoratedReply = {
24
- [key in keyof FastifyStatusCode]: <T>(val?: T) => T;
4
+ [key in StatusCodeKey]: <T>(val?: T) => T;
25
5
  };
26
6
  declare module 'fastify' {
27
7
  interface FastifyReply extends DecoratedReply {
28
8
  }
29
9
  }
30
- declare const _default: import("fastify").FastifyPluginCallback<FastifyReplyPluginOptions, import("fastify").RawServerDefault, import("fastify").FastifyTypeProviderDefault, import("fastify").FastifyBaseLogger>;
31
- export default _default;
32
10
  export * from './error.js';
11
+ export * from './pluginV4.js';
33
12
  export * from './route.js';
34
13
  export * from './routeV4.js';
35
14
  export * from './types.js';
15
+ export { type FastifyReplyPluginOptions, type StatusCode, type StatusCodeBuilder };
16
+ export default plugin;
package/lib/cjs/index.js CHANGED
@@ -13,38 +13,11 @@ var __createBinding = (this && this.__createBinding) || (Object.create ? (functi
13
13
  var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
14
  for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
15
  };
16
- var __importDefault = (this && this.__importDefault) || function (mod) {
17
- return (mod && mod.__esModule) ? mod : { "default": mod };
18
- };
19
16
  Object.defineProperty(exports, "__esModule", { value: true });
20
- const fastify_plugin_1 = __importDefault(require("fastify-plugin"));
21
- const reply_js_1 = require("./reply.js");
22
- const utils_js_1 = require("./utils.js");
23
- const defaultOptions = {
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: { message: 'noContent' } },
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
- }
37
- };
38
- exports.default = (0, fastify_plugin_1.default)(async (fastify, opts) => {
39
- const finalOptions = (0, utils_js_1.mergeDeep)(defaultOptions, opts);
40
- Object.entries(finalOptions.statusCodes).forEach(([key, value]) => {
41
- fastify.decorateReply(key, (0, reply_js_1.createReply)(value.statusCode, value.payload));
42
- });
43
- }, {
44
- fastify: "5.x",
45
- name: '@efebia/fastify-zod-reply'
46
- });
17
+ const plugin_js_1 = require("./plugin.js");
47
18
  __exportStar(require("./error.js"), exports);
19
+ __exportStar(require("./pluginV4.js"), exports);
48
20
  __exportStar(require("./route.js"), exports);
49
21
  __exportStar(require("./routeV4.js"), exports);
50
22
  __exportStar(require("./types.js"), exports);
23
+ exports.default = plugin_js_1.plugin;
@@ -0,0 +1,19 @@
1
+ import { z } from "zod";
2
+ import { StatusCodeKey } from "./types.js";
3
+ export type StatusCode<TSchema extends z.ZodType> = {
4
+ statusCode: number;
5
+ payload: z.output<TSchema>;
6
+ schema: TSchema;
7
+ };
8
+ export declare class StatusCodeBuilder<TOpts extends Partial<Record<StatusCodeKey, StatusCode<any>>> = {}> {
9
+ readonly obj: TOpts;
10
+ constructor(obj?: TOpts);
11
+ set<TKey extends keyof TOpts extends never ? StatusCodeKey : Exclude<StatusCodeKey, keyof TOpts>, TSchema extends z.ZodType>(key: TKey, { schema, payload }: {
12
+ schema: TSchema;
13
+ payload: z.output<TSchema>;
14
+ }): StatusCodeBuilder<TOpts & { [key in TKey]: StatusCode<TSchema>; }>;
15
+ }
16
+ export type FastifyReplyPluginOptions = {
17
+ statusCodes?: StatusCodeBuilder;
18
+ };
19
+ export declare const plugin: import("fastify").FastifyPluginCallback<FastifyReplyPluginOptions, import("fastify").RawServerDefault, import("fastify").FastifyTypeProviderDefault, import("fastify").FastifyBaseLogger>;
@@ -0,0 +1,51 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.plugin = exports.StatusCodeBuilder = void 0;
7
+ const fastify_plugin_1 = __importDefault(require("fastify-plugin"));
8
+ const zod_1 = require("zod");
9
+ const reply_js_1 = require("./reply.js");
10
+ const utils_js_1 = require("./utils.js");
11
+ class StatusCodeBuilder {
12
+ constructor(obj = {}) {
13
+ this.obj = obj;
14
+ }
15
+ set(key, { schema, payload }) {
16
+ return new StatusCodeBuilder(Object.assign(Object.assign({}, this.obj), { [key]: { statusCode: utils_js_1.statusByText[key], schema, payload } }));
17
+ }
18
+ }
19
+ exports.StatusCodeBuilder = StatusCodeBuilder;
20
+ const messageSchema = zod_1.z.object({ message: zod_1.z.string() });
21
+ const defaultOptions = {
22
+ statusCodes: new StatusCodeBuilder()
23
+ .set("ok", { schema: messageSchema, payload: { message: "ok" } })
24
+ .set("created", { schema: messageSchema, payload: { message: "created" } })
25
+ .set("accepted", { schema: messageSchema, payload: { message: "accepted" } })
26
+ .set("noContent", { schema: zod_1.z.undefined(), payload: undefined })
27
+ .set("badRequest", { schema: messageSchema, payload: { message: "badRequest" } })
28
+ .set("unauthorized", { schema: messageSchema, payload: { message: "unauthorized" } })
29
+ .set("forbidden", { schema: messageSchema, payload: { message: "forbidden" } })
30
+ .set("notFound", { schema: messageSchema, payload: { message: "notFound" } })
31
+ .set("notAcceptable", { schema: messageSchema, payload: { message: "notAcceptable" } })
32
+ .set("conflict", { schema: messageSchema, payload: { message: "conflict" } })
33
+ .set("internalServerError", { schema: messageSchema, payload: { message: "internalServerError" } })
34
+ };
35
+ exports.plugin = (0, fastify_plugin_1.default)(async (fastify, opts) => {
36
+ var _a, _b, _c;
37
+ const finalOptions = {
38
+ statusCodes: new StatusCodeBuilder(Object.assign(Object.assign({}, (_a = defaultOptions.statusCodes) === null || _a === void 0 ? void 0 : _a.obj), (_b = opts.statusCodes) === null || _b === void 0 ? void 0 : _b.obj)),
39
+ };
40
+ Object.entries(((_c = finalOptions.statusCodes) === null || _c === void 0 ? void 0 : _c.obj) || {}).forEach(([key, value]) => {
41
+ fastify.decorateReply(key, (0, reply_js_1.createReply)(value.statusCode, value.payload));
42
+ fastify.decorateReply(`SCHEMA_${value.statusCode}`, {
43
+ getter() {
44
+ return value.schema;
45
+ }
46
+ });
47
+ });
48
+ }, {
49
+ fastify: "5.x",
50
+ name: "@efebia/fastify-zod-reply",
51
+ });
@@ -0,0 +1,19 @@
1
+ import { z } from "zod/v4";
2
+ import { StatusCodeKey } from "./types.js";
3
+ export type StatusCodeV4<TSchema extends z.ZodType> = {
4
+ statusCode: number;
5
+ payload: z.output<TSchema>;
6
+ schema: TSchema;
7
+ };
8
+ export declare class StatusCodeV4Builder<TOpts extends Partial<Record<StatusCodeKey, StatusCodeV4<any>>> = {}> {
9
+ readonly obj: TOpts;
10
+ constructor(obj?: TOpts);
11
+ set<TKey extends keyof TOpts extends never ? StatusCodeKey : Exclude<StatusCodeKey, keyof TOpts>, TSchema extends z.ZodType>(key: TKey, { schema, payload }: {
12
+ schema: TSchema;
13
+ payload: z.output<TSchema>;
14
+ }): StatusCodeV4Builder<TOpts & { [key in TKey]: StatusCodeV4<TSchema>; }>;
15
+ }
16
+ export type FastifyReplyV4PluginOptions = {
17
+ statusCodes?: StatusCodeV4Builder;
18
+ };
19
+ export declare const pluginV4: import("fastify").FastifyPluginCallback<FastifyReplyV4PluginOptions, import("fastify").RawServerDefault, import("fastify").FastifyTypeProviderDefault, import("fastify").FastifyBaseLogger>;
@@ -0,0 +1,51 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.pluginV4 = exports.StatusCodeV4Builder = void 0;
7
+ const fastify_plugin_1 = __importDefault(require("fastify-plugin"));
8
+ const v4_1 = require("zod/v4");
9
+ const reply_js_1 = require("./reply.js");
10
+ const utils_js_1 = require("./utils.js");
11
+ class StatusCodeV4Builder {
12
+ constructor(obj = {}) {
13
+ this.obj = obj;
14
+ }
15
+ set(key, { schema, payload }) {
16
+ return new StatusCodeV4Builder(Object.assign(Object.assign({}, this.obj), { [key]: { statusCode: utils_js_1.statusByText[key], schema, payload } }));
17
+ }
18
+ }
19
+ exports.StatusCodeV4Builder = StatusCodeV4Builder;
20
+ const messageSchema = v4_1.z.object({ message: v4_1.z.string() });
21
+ const defaultOptions = {
22
+ statusCodes: new StatusCodeV4Builder()
23
+ .set("ok", { schema: messageSchema, payload: { message: "ok" } })
24
+ .set("created", { schema: messageSchema, payload: { message: "created" } })
25
+ .set("accepted", { schema: messageSchema, payload: { message: "accepted" } })
26
+ .set("noContent", { schema: v4_1.z.undefined(), payload: undefined })
27
+ .set("badRequest", { schema: messageSchema, payload: { message: "badRequest" } })
28
+ .set("unauthorized", { schema: messageSchema, payload: { message: "unauthorized" } })
29
+ .set("forbidden", { schema: messageSchema, payload: { message: "forbidden" } })
30
+ .set("notFound", { schema: messageSchema, payload: { message: "notFound" } })
31
+ .set("notAcceptable", { schema: messageSchema, payload: { message: "notAcceptable" } })
32
+ .set("conflict", { schema: messageSchema, payload: { message: "conflict" } })
33
+ .set("internalServerError", { schema: messageSchema, payload: { message: "internalServerError" } })
34
+ };
35
+ exports.pluginV4 = (0, fastify_plugin_1.default)(async (fastify, opts) => {
36
+ var _a, _b, _c;
37
+ const finalOptions = {
38
+ statusCodes: new StatusCodeV4Builder(Object.assign(Object.assign({}, (_a = defaultOptions.statusCodes) === null || _a === void 0 ? void 0 : _a.obj), (_b = opts.statusCodes) === null || _b === void 0 ? void 0 : _b.obj)),
39
+ };
40
+ Object.entries(((_c = finalOptions.statusCodes) === null || _c === void 0 ? void 0 : _c.obj) || {}).forEach(([key, value]) => {
41
+ fastify.decorateReply(key, (0, reply_js_1.createReply)(value.statusCode, value.payload));
42
+ fastify.decorateReply(`SCHEMA_${value.statusCode}`, {
43
+ getter() {
44
+ return value.schema;
45
+ }
46
+ });
47
+ });
48
+ }, {
49
+ fastify: "5.x",
50
+ name: "@efebia/fastify-zod-reply",
51
+ });
package/lib/cjs/route.js CHANGED
@@ -2,7 +2,13 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.route = exports.createRoute = void 0;
4
4
  const zod_to_json_schema_1 = require("zod-to-json-schema");
5
- const mapZodError = (zodError, prefix) => zodError.errors.map(issue => `Error at ${prefix}->${issue.path.join('->')}`).join(';\n');
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
+ };
6
12
  const parse = async (schema, payload, tag) => {
7
13
  const result = await schema.safeParseAsync(payload);
8
14
  return Object.assign(Object.assign({}, result), { tag });
@@ -60,16 +66,21 @@ const createRoute = ({ strict: globalStrict = false } = {}) => (schema, handler,
60
66
  request.query = ((_c = results.find(r => r.tag === 'query')) === null || _c === void 0 ? void 0 : _c.data) || {};
61
67
  },
62
68
  preSerialization: (request, reply, payload, done) => {
63
- const foundSchema = findStatusCode(reply.statusCode, Object.entries(schema.Reply.shape));
69
+ var _a;
70
+ const foundLocalSchema = findStatusCode(reply.statusCode, Object.entries(schema.Reply.shape));
71
+ const foundGlobalSchema = reply[`SCHEMA_${reply.statusCode}`];
72
+ const foundSchema = (_a = foundLocalSchema === null || foundLocalSchema === void 0 ? void 0 : foundLocalSchema[1]) !== null && _a !== void 0 ? _a : foundGlobalSchema;
64
73
  if (!foundSchema) {
65
- request.log.warn(`[@efebia/fastify-zod-reply]: Reply schema of: ${request.routeOptions.url} does not have the specified status code: ${reply.statusCode}`);
66
- return done(null, payload);
74
+ request.log.error(`[@efebia/fastify-zod-reply]: Reply schema of: ${request.routeOptions.url} does not have the specified status code: ${reply.statusCode} nor there is a global schema for this status code.`);
75
+ reply.code(500);
76
+ return done(new error_js_1.FastifyZodReplyError(`Reply schema of: ${request.routeOptions.url} does not have the specified status code: ${reply.statusCode} nor there is a global schema for this status code.`, 500));
67
77
  }
68
- const serialized = foundSchema[1].safeParse(payload);
78
+ const serialized = foundSchema.safeParse(payload);
69
79
  if (serialized.success) {
70
80
  return done(null, serialized.data);
71
81
  }
72
- return done(new Error(mapZodError(serialized.error, 'reply')));
82
+ reply.code(500);
83
+ return done(new error_js_1.FastifyZodReplyError(mapZodError(serialized.error, 'reply'), 500));
73
84
  },
74
85
  };
75
86
  };
@@ -2,6 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.routeV4 = exports.createRouteV4 = void 0;
4
4
  const v4_1 = require("zod/v4");
5
+ const error_js_1 = require("./error.js");
5
6
  const mapZodError = (zodError, prefix) => {
6
7
  return zodError.issues.map(issue => {
7
8
  const pathStr = `Error at ${prefix}->${issue.path.join('->')}`;
@@ -62,16 +63,21 @@ const createRouteV4 = ({ strict: globalStrict = false } = {}) => (schema, handle
62
63
  request.query = ((_c = results.find(r => r.tag === 'query')) === null || _c === void 0 ? void 0 : _c.data) || {};
63
64
  },
64
65
  preSerialization: (request, reply, payload, done) => {
65
- const foundSchema = findStatusCode(reply.statusCode, Object.entries(schema.Reply.shape));
66
+ var _a;
67
+ const foundLocalSchema = findStatusCode(reply.statusCode, Object.entries(schema.Reply.shape));
68
+ const foundGlobalSchema = reply[`SCHEMA_${reply.statusCode}`];
69
+ const foundSchema = (_a = foundLocalSchema === null || foundLocalSchema === void 0 ? void 0 : foundLocalSchema[1]) !== null && _a !== void 0 ? _a : foundGlobalSchema;
66
70
  if (!foundSchema) {
67
- request.log.warn(`[@efebia/fastify-zod-reply]: Reply schema of: ${request.routeOptions.url} does not have the specified status code: ${reply.statusCode}`);
68
- return done(null, payload);
71
+ request.log.error(`[@efebia/fastify-zod-reply]: Reply schema of: ${request.routeOptions.url} does not have the specified status code: ${reply.statusCode} nor there is a global schema for this status code.`);
72
+ reply.code(500);
73
+ return done(new error_js_1.FastifyZodReplyError(`Reply schema of: ${request.routeOptions.url} does not have the specified status code: ${reply.statusCode} nor there is a global schema for this status code.`, 500));
69
74
  }
70
- const serialized = foundSchema[1].safeParse(payload);
75
+ const serialized = foundSchema.safeParse(payload);
71
76
  if (serialized.success) {
72
77
  return done(null, serialized.data);
73
78
  }
74
- return done(new Error(mapZodError(serialized.error, 'reply')));
79
+ reply.code(500);
80
+ return done(new error_js_1.FastifyZodReplyError(mapZodError(serialized.error, 'reply'), 500));
75
81
  },
76
82
  };
77
83
  };
@@ -5,3 +5,4 @@ export interface RouteTag {
5
5
  }
6
6
  export interface RouteSecurity {
7
7
  }
8
+ export type StatusCodeKey = 'ok' | 'created' | 'accepted' | 'noContent' | 'badRequest' | 'unauthorized' | 'forbidden' | 'notFound' | 'notAcceptable' | 'conflict' | 'internalServerError';
@@ -1,2 +1,4 @@
1
+ import { StatusCodeKey } from "./types.js";
1
2
  export declare function isObject(item: any): any;
2
3
  export declare function mergeDeep<TResult extends object>(target: TResult, ...sources: any[]): TResult;
4
+ export declare const statusByText: Record<StatusCodeKey, number>;
package/lib/cjs/utils.js CHANGED
@@ -1,9 +1,10 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.statusByText = void 0;
3
4
  exports.isObject = isObject;
4
5
  exports.mergeDeep = mergeDeep;
5
6
  function isObject(item) {
6
- return (item && typeof item === 'object' && !Array.isArray(item));
7
+ return item && typeof item === "object" && !Array.isArray(item);
7
8
  }
8
9
  function mergeDeep(target, ...sources) {
9
10
  if (!sources.length)
@@ -23,3 +24,16 @@ function mergeDeep(target, ...sources) {
23
24
  }
24
25
  return mergeDeep(target, ...sources);
25
26
  }
27
+ exports.statusByText = {
28
+ ok: 200,
29
+ created: 201,
30
+ accepted: 202,
31
+ noContent: 204,
32
+ badRequest: 400,
33
+ unauthorized: 401,
34
+ forbidden: 403,
35
+ notFound: 404,
36
+ notAcceptable: 406,
37
+ conflict: 409,
38
+ internalServerError: 500,
39
+ };
@@ -1,35 +1,16 @@
1
- export type StatusCode<TCode> = {
2
- statusCode: TCode;
3
- payload: any;
4
- };
5
- export interface FastifyStatusCode {
6
- ok: StatusCode<200>;
7
- created: StatusCode<201>;
8
- accepted: StatusCode<202>;
9
- noContent: StatusCode<204>;
10
- badRequest: StatusCode<400>;
11
- unauthorized: StatusCode<401>;
12
- forbidden: StatusCode<403>;
13
- notFound: StatusCode<404>;
14
- notAcceptable: StatusCode<406>;
15
- conflict: StatusCode<409>;
16
- internalServerError: StatusCode<500>;
17
- }
18
- export type FastifyReplyPluginOptions = {
19
- statusCodes?: {
20
- [key in keyof FastifyStatusCode]?: FastifyStatusCode[key];
21
- };
22
- };
1
+ import { plugin, type FastifyReplyPluginOptions, type StatusCode, type StatusCodeBuilder } from './plugin.js';
2
+ import { StatusCodeKey } from './types.js';
23
3
  export type DecoratedReply = {
24
- [key in keyof FastifyStatusCode]: <T>(val?: T) => T;
4
+ [key in StatusCodeKey]: <T>(val?: T) => T;
25
5
  };
26
6
  declare module 'fastify' {
27
7
  interface FastifyReply extends DecoratedReply {
28
8
  }
29
9
  }
30
- declare const _default: import("fastify").FastifyPluginCallback<FastifyReplyPluginOptions, import("fastify").RawServerDefault, import("fastify").FastifyTypeProviderDefault, import("fastify").FastifyBaseLogger>;
31
- export default _default;
32
10
  export * from './error.js';
11
+ export * from './pluginV4.js';
33
12
  export * from './route.js';
34
13
  export * from './routeV4.js';
35
14
  export * from './types.js';
15
+ export { type FastifyReplyPluginOptions, type StatusCode, type StatusCodeBuilder };
16
+ export default plugin;
package/lib/esm/index.js CHANGED
@@ -1,31 +1,7 @@
1
- import fp from "fastify-plugin";
2
- import { createReply } from "./reply.js";
3
- import { mergeDeep } from "./utils.js";
4
- const defaultOptions = {
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: { message: 'noContent' } },
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
- }
18
- };
19
- export default fp(async (fastify, opts) => {
20
- const finalOptions = mergeDeep(defaultOptions, opts);
21
- Object.entries(finalOptions.statusCodes).forEach(([key, value]) => {
22
- fastify.decorateReply(key, createReply(value.statusCode, value.payload));
23
- });
24
- }, {
25
- fastify: "5.x",
26
- name: '@efebia/fastify-zod-reply'
27
- });
1
+ import { plugin } from './plugin.js';
28
2
  export * from './error.js';
3
+ export * from './pluginV4.js';
29
4
  export * from './route.js';
30
5
  export * from './routeV4.js';
31
6
  export * from './types.js';
7
+ export default plugin;
@@ -0,0 +1,19 @@
1
+ import { z } from "zod";
2
+ import { StatusCodeKey } from "./types.js";
3
+ export type StatusCode<TSchema extends z.ZodType> = {
4
+ statusCode: number;
5
+ payload: z.output<TSchema>;
6
+ schema: TSchema;
7
+ };
8
+ export declare class StatusCodeBuilder<TOpts extends Partial<Record<StatusCodeKey, StatusCode<any>>> = {}> {
9
+ readonly obj: TOpts;
10
+ constructor(obj?: TOpts);
11
+ set<TKey extends keyof TOpts extends never ? StatusCodeKey : Exclude<StatusCodeKey, keyof TOpts>, TSchema extends z.ZodType>(key: TKey, { schema, payload }: {
12
+ schema: TSchema;
13
+ payload: z.output<TSchema>;
14
+ }): StatusCodeBuilder<TOpts & { [key in TKey]: StatusCode<TSchema>; }>;
15
+ }
16
+ export type FastifyReplyPluginOptions = {
17
+ statusCodes?: StatusCodeBuilder;
18
+ };
19
+ export declare const plugin: import("fastify").FastifyPluginCallback<FastifyReplyPluginOptions, import("fastify").RawServerDefault, import("fastify").FastifyTypeProviderDefault, import("fastify").FastifyBaseLogger>;
@@ -0,0 +1,44 @@
1
+ import fp from "fastify-plugin";
2
+ import { z } from "zod";
3
+ import { createReply } from "./reply.js";
4
+ import { statusByText } from "./utils.js";
5
+ export class StatusCodeBuilder {
6
+ constructor(obj = {}) {
7
+ this.obj = obj;
8
+ }
9
+ set(key, { schema, payload }) {
10
+ return new StatusCodeBuilder(Object.assign(Object.assign({}, this.obj), { [key]: { statusCode: statusByText[key], schema, payload } }));
11
+ }
12
+ }
13
+ const messageSchema = z.object({ message: z.string() });
14
+ const defaultOptions = {
15
+ statusCodes: new StatusCodeBuilder()
16
+ .set("ok", { schema: messageSchema, payload: { message: "ok" } })
17
+ .set("created", { schema: messageSchema, payload: { message: "created" } })
18
+ .set("accepted", { schema: messageSchema, payload: { message: "accepted" } })
19
+ .set("noContent", { schema: z.undefined(), payload: undefined })
20
+ .set("badRequest", { schema: messageSchema, payload: { message: "badRequest" } })
21
+ .set("unauthorized", { schema: messageSchema, payload: { message: "unauthorized" } })
22
+ .set("forbidden", { schema: messageSchema, payload: { message: "forbidden" } })
23
+ .set("notFound", { schema: messageSchema, payload: { message: "notFound" } })
24
+ .set("notAcceptable", { schema: messageSchema, payload: { message: "notAcceptable" } })
25
+ .set("conflict", { schema: messageSchema, payload: { message: "conflict" } })
26
+ .set("internalServerError", { schema: messageSchema, payload: { message: "internalServerError" } })
27
+ };
28
+ export const plugin = fp(async (fastify, opts) => {
29
+ var _a, _b, _c;
30
+ const finalOptions = {
31
+ statusCodes: new StatusCodeBuilder(Object.assign(Object.assign({}, (_a = defaultOptions.statusCodes) === null || _a === void 0 ? void 0 : _a.obj), (_b = opts.statusCodes) === null || _b === void 0 ? void 0 : _b.obj)),
32
+ };
33
+ Object.entries(((_c = finalOptions.statusCodes) === null || _c === void 0 ? void 0 : _c.obj) || {}).forEach(([key, value]) => {
34
+ fastify.decorateReply(key, createReply(value.statusCode, value.payload));
35
+ fastify.decorateReply(`SCHEMA_${value.statusCode}`, {
36
+ getter() {
37
+ return value.schema;
38
+ }
39
+ });
40
+ });
41
+ }, {
42
+ fastify: "5.x",
43
+ name: "@efebia/fastify-zod-reply",
44
+ });
@@ -0,0 +1,19 @@
1
+ import { z } from "zod/v4";
2
+ import { StatusCodeKey } from "./types.js";
3
+ export type StatusCodeV4<TSchema extends z.ZodType> = {
4
+ statusCode: number;
5
+ payload: z.output<TSchema>;
6
+ schema: TSchema;
7
+ };
8
+ export declare class StatusCodeV4Builder<TOpts extends Partial<Record<StatusCodeKey, StatusCodeV4<any>>> = {}> {
9
+ readonly obj: TOpts;
10
+ constructor(obj?: TOpts);
11
+ set<TKey extends keyof TOpts extends never ? StatusCodeKey : Exclude<StatusCodeKey, keyof TOpts>, TSchema extends z.ZodType>(key: TKey, { schema, payload }: {
12
+ schema: TSchema;
13
+ payload: z.output<TSchema>;
14
+ }): StatusCodeV4Builder<TOpts & { [key in TKey]: StatusCodeV4<TSchema>; }>;
15
+ }
16
+ export type FastifyReplyV4PluginOptions = {
17
+ statusCodes?: StatusCodeV4Builder;
18
+ };
19
+ export declare const pluginV4: import("fastify").FastifyPluginCallback<FastifyReplyV4PluginOptions, import("fastify").RawServerDefault, import("fastify").FastifyTypeProviderDefault, import("fastify").FastifyBaseLogger>;
@@ -0,0 +1,44 @@
1
+ import fp from "fastify-plugin";
2
+ import { z } from "zod/v4";
3
+ import { createReply } from "./reply.js";
4
+ import { statusByText } from "./utils.js";
5
+ export class StatusCodeV4Builder {
6
+ constructor(obj = {}) {
7
+ this.obj = obj;
8
+ }
9
+ set(key, { schema, payload }) {
10
+ return new StatusCodeV4Builder(Object.assign(Object.assign({}, this.obj), { [key]: { statusCode: statusByText[key], schema, payload } }));
11
+ }
12
+ }
13
+ const messageSchema = z.object({ message: z.string() });
14
+ const defaultOptions = {
15
+ statusCodes: new StatusCodeV4Builder()
16
+ .set("ok", { schema: messageSchema, payload: { message: "ok" } })
17
+ .set("created", { schema: messageSchema, payload: { message: "created" } })
18
+ .set("accepted", { schema: messageSchema, payload: { message: "accepted" } })
19
+ .set("noContent", { schema: z.undefined(), payload: undefined })
20
+ .set("badRequest", { schema: messageSchema, payload: { message: "badRequest" } })
21
+ .set("unauthorized", { schema: messageSchema, payload: { message: "unauthorized" } })
22
+ .set("forbidden", { schema: messageSchema, payload: { message: "forbidden" } })
23
+ .set("notFound", { schema: messageSchema, payload: { message: "notFound" } })
24
+ .set("notAcceptable", { schema: messageSchema, payload: { message: "notAcceptable" } })
25
+ .set("conflict", { schema: messageSchema, payload: { message: "conflict" } })
26
+ .set("internalServerError", { schema: messageSchema, payload: { message: "internalServerError" } })
27
+ };
28
+ export const pluginV4 = fp(async (fastify, opts) => {
29
+ var _a, _b, _c;
30
+ const finalOptions = {
31
+ statusCodes: new StatusCodeV4Builder(Object.assign(Object.assign({}, (_a = defaultOptions.statusCodes) === null || _a === void 0 ? void 0 : _a.obj), (_b = opts.statusCodes) === null || _b === void 0 ? void 0 : _b.obj)),
32
+ };
33
+ Object.entries(((_c = finalOptions.statusCodes) === null || _c === void 0 ? void 0 : _c.obj) || {}).forEach(([key, value]) => {
34
+ fastify.decorateReply(key, createReply(value.statusCode, value.payload));
35
+ fastify.decorateReply(`SCHEMA_${value.statusCode}`, {
36
+ getter() {
37
+ return value.schema;
38
+ }
39
+ });
40
+ });
41
+ }, {
42
+ fastify: "5.x",
43
+ name: "@efebia/fastify-zod-reply",
44
+ });
package/lib/esm/route.js CHANGED
@@ -1,5 +1,11 @@
1
1
  import { zodToJsonSchema } from 'zod-to-json-schema';
2
- const mapZodError = (zodError, prefix) => zodError.errors.map(issue => `Error at ${prefix}->${issue.path.join('->')}`).join(';\n');
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
+ };
3
9
  const parse = async (schema, payload, tag) => {
4
10
  const result = await schema.safeParseAsync(payload);
5
11
  return Object.assign(Object.assign({}, result), { tag });
@@ -57,16 +63,21 @@ export const createRoute = ({ strict: globalStrict = false } = {}) => (schema, h
57
63
  request.query = ((_c = results.find(r => r.tag === 'query')) === null || _c === void 0 ? void 0 : _c.data) || {};
58
64
  },
59
65
  preSerialization: (request, reply, payload, done) => {
60
- const foundSchema = findStatusCode(reply.statusCode, Object.entries(schema.Reply.shape));
66
+ var _a;
67
+ const foundLocalSchema = findStatusCode(reply.statusCode, Object.entries(schema.Reply.shape));
68
+ const foundGlobalSchema = reply[`SCHEMA_${reply.statusCode}`];
69
+ const foundSchema = (_a = foundLocalSchema === null || foundLocalSchema === void 0 ? void 0 : foundLocalSchema[1]) !== null && _a !== void 0 ? _a : foundGlobalSchema;
61
70
  if (!foundSchema) {
62
- request.log.warn(`[@efebia/fastify-zod-reply]: Reply schema of: ${request.routeOptions.url} does not have the specified status code: ${reply.statusCode}`);
63
- return done(null, payload);
71
+ request.log.error(`[@efebia/fastify-zod-reply]: Reply schema of: ${request.routeOptions.url} does not have the specified status code: ${reply.statusCode} nor there is a global schema for this status code.`);
72
+ reply.code(500);
73
+ return done(new FastifyZodReplyError(`Reply schema of: ${request.routeOptions.url} does not have the specified status code: ${reply.statusCode} nor there is a global schema for this status code.`, 500));
64
74
  }
65
- const serialized = foundSchema[1].safeParse(payload);
75
+ const serialized = foundSchema.safeParse(payload);
66
76
  if (serialized.success) {
67
77
  return done(null, serialized.data);
68
78
  }
69
- return done(new Error(mapZodError(serialized.error, 'reply')));
79
+ reply.code(500);
80
+ return done(new FastifyZodReplyError(mapZodError(serialized.error, 'reply'), 500));
70
81
  },
71
82
  };
72
83
  };
@@ -1,4 +1,5 @@
1
1
  import { z } from 'zod/v4';
2
+ import { FastifyZodReplyError } from './error.js';
2
3
  const mapZodError = (zodError, prefix) => {
3
4
  return zodError.issues.map(issue => {
4
5
  const pathStr = `Error at ${prefix}->${issue.path.join('->')}`;
@@ -59,16 +60,21 @@ export const createRouteV4 = ({ strict: globalStrict = false } = {}) => (schema,
59
60
  request.query = ((_c = results.find(r => r.tag === 'query')) === null || _c === void 0 ? void 0 : _c.data) || {};
60
61
  },
61
62
  preSerialization: (request, reply, payload, done) => {
62
- const foundSchema = findStatusCode(reply.statusCode, Object.entries(schema.Reply.shape));
63
+ var _a;
64
+ const foundLocalSchema = findStatusCode(reply.statusCode, Object.entries(schema.Reply.shape));
65
+ const foundGlobalSchema = reply[`SCHEMA_${reply.statusCode}`];
66
+ const foundSchema = (_a = foundLocalSchema === null || foundLocalSchema === void 0 ? void 0 : foundLocalSchema[1]) !== null && _a !== void 0 ? _a : foundGlobalSchema;
63
67
  if (!foundSchema) {
64
- request.log.warn(`[@efebia/fastify-zod-reply]: Reply schema of: ${request.routeOptions.url} does not have the specified status code: ${reply.statusCode}`);
65
- return done(null, payload);
68
+ request.log.error(`[@efebia/fastify-zod-reply]: Reply schema of: ${request.routeOptions.url} does not have the specified status code: ${reply.statusCode} nor there is a global schema for this status code.`);
69
+ reply.code(500);
70
+ return done(new FastifyZodReplyError(`Reply schema of: ${request.routeOptions.url} does not have the specified status code: ${reply.statusCode} nor there is a global schema for this status code.`, 500));
66
71
  }
67
- const serialized = foundSchema[1].safeParse(payload);
72
+ const serialized = foundSchema.safeParse(payload);
68
73
  if (serialized.success) {
69
74
  return done(null, serialized.data);
70
75
  }
71
- return done(new Error(mapZodError(serialized.error, 'reply')));
76
+ reply.code(500);
77
+ return done(new FastifyZodReplyError(mapZodError(serialized.error, 'reply'), 500));
72
78
  },
73
79
  };
74
80
  };
@@ -5,3 +5,4 @@ export interface RouteTag {
5
5
  }
6
6
  export interface RouteSecurity {
7
7
  }
8
+ export type StatusCodeKey = 'ok' | 'created' | 'accepted' | 'noContent' | 'badRequest' | 'unauthorized' | 'forbidden' | 'notFound' | 'notAcceptable' | 'conflict' | 'internalServerError';
@@ -1,2 +1,4 @@
1
+ import { StatusCodeKey } from "./types.js";
1
2
  export declare function isObject(item: any): any;
2
3
  export declare function mergeDeep<TResult extends object>(target: TResult, ...sources: any[]): TResult;
4
+ export declare const statusByText: Record<StatusCodeKey, number>;
package/lib/esm/utils.js CHANGED
@@ -1,5 +1,5 @@
1
1
  export function isObject(item) {
2
- return (item && typeof item === 'object' && !Array.isArray(item));
2
+ return item && typeof item === "object" && !Array.isArray(item);
3
3
  }
4
4
  export function mergeDeep(target, ...sources) {
5
5
  if (!sources.length)
@@ -19,3 +19,16 @@ export function mergeDeep(target, ...sources) {
19
19
  }
20
20
  return mergeDeep(target, ...sources);
21
21
  }
22
+ export const statusByText = {
23
+ ok: 200,
24
+ created: 201,
25
+ accepted: 202,
26
+ noContent: 204,
27
+ badRequest: 400,
28
+ unauthorized: 401,
29
+ forbidden: 403,
30
+ notFound: 404,
31
+ notAcceptable: 406,
32
+ conflict: 409,
33
+ internalServerError: 500,
34
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@efebia/fastify-zod-reply",
3
- "version": "1.1.0",
3
+ "version": "1.2.1",
4
4
  "license": "MIT",
5
5
  "dependencies": {
6
6
  "fastify": "^5.3.0",
@@ -15,7 +15,8 @@
15
15
  "build:esm": "tsc -p ./tsconfig.build.json --module nodenext --moduleResolution nodenext --outDir lib/esm",
16
16
  "build:cjs": "tsc -p ./tsconfig.build.json --module commonjs --moduleResolution node --outDir lib/cjs",
17
17
  "build": "yarn build:esm && yarn build:cjs",
18
- "test": "node --import tsx --test ./**/*.test.ts"
18
+ "test": "node --import tsx --test ./**/*.test.ts",
19
+ "test:single": "node --import tsx --test"
19
20
  },
20
21
  "main": "./lib/esm/index.js",
21
22
  "types": "./lib/esm/index.d.ts",