@efebia/fastify-zod-reply 0.0.1 → 0.0.2

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.
@@ -4,8 +4,8 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  const fastify_plugin_1 = __importDefault(require("fastify-plugin"));
7
- const reply_1 = require("./reply");
8
- const utils_1 = require("./utils");
7
+ const reply_js_1 = require("./reply.js");
8
+ const utils_js_1 = require("./utils.js");
9
9
  const defaultOptions = {
10
10
  statusCodes: {
11
11
  ok: { statusCode: 200, payload: { message: 'ok' } },
@@ -22,9 +22,9 @@ const defaultOptions = {
22
22
  }
23
23
  };
24
24
  exports.default = (0, fastify_plugin_1.default)(async (fastify, opts) => {
25
- const finalOptions = (0, utils_1.mergeDeep)(defaultOptions, opts);
25
+ const finalOptions = (0, utils_js_1.mergeDeep)(defaultOptions, opts);
26
26
  Object.entries(finalOptions.statusCodes).forEach(([key, value]) => {
27
- fastify.decorateReply(key, (0, reply_1.createReply)(value.statusCode, value.payload));
27
+ fastify.decorateReply(key, (0, reply_js_1.createReply)(value.statusCode, value.payload));
28
28
  });
29
29
  }, {
30
30
  fastify: "4.x",
@@ -1,5 +1,5 @@
1
1
  import { FastifyReply } from "fastify";
2
- import { FastifyZodReplyError } from "./error";
2
+ import { FastifyZodReplyError } from "./error.js";
3
3
  type ReplyFunction<T> = T extends (...args: any[]) => any ? (this: FastifyReply, ...args: Parameters<T>) => ReturnType<T> : never;
4
4
  export declare const createReply: (statusCode: number, defaultPayload: object) => ReplyFunction<any>;
5
5
  export declare const createError: (statusCode: number) => (message: string | {
@@ -1,7 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.createError = exports.createReply = void 0;
4
- const error_1 = require("./error");
4
+ const error_js_1 = require("./error.js");
5
5
  const createReply = (statusCode, defaultPayload) => {
6
6
  return function (payload) {
7
7
  const finalPayload = payload !== null && payload !== void 0 ? payload : defaultPayload;
@@ -14,7 +14,7 @@ const createReply = (statusCode, defaultPayload) => {
14
14
  };
15
15
  exports.createReply = createReply;
16
16
  const createError = (statusCode) => (message) => {
17
- const customError = new error_1.FastifyZodReplyError("", statusCode);
17
+ const customError = new error_js_1.FastifyZodReplyError("", statusCode);
18
18
  if (typeof message === "string")
19
19
  customError.message = message;
20
20
  else
@@ -0,0 +1,4 @@
1
+ export declare class FastifyZodReplyError extends Error {
2
+ statusCode: number;
3
+ constructor(message?: string, statusCode?: number);
4
+ }
@@ -0,0 +1,6 @@
1
+ export class FastifyZodReplyError extends Error {
2
+ constructor(message, statusCode) {
3
+ super(message);
4
+ this.statusCode = statusCode !== null && statusCode !== void 0 ? statusCode : -1;
5
+ }
6
+ }
@@ -0,0 +1,31 @@
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
+ };
23
+ export type DecoratedReply = {
24
+ [key in keyof FastifyStatusCode]: <T>(val?: T) => T;
25
+ };
26
+ declare module 'fastify' {
27
+ interface FastifyReply extends DecoratedReply {
28
+ }
29
+ }
30
+ declare const _default: import("fastify").FastifyPluginCallback<FastifyReplyPluginOptions, import("fastify").RawServerDefault, import("fastify").FastifyTypeProviderDefault, import("fastify").FastifyBaseLogger>;
31
+ export default _default;
@@ -0,0 +1,27 @@
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: "4.x",
26
+ name: '@efebia/fastify-zod-reply'
27
+ });
@@ -0,0 +1,8 @@
1
+ import { FastifyReply } from "fastify";
2
+ import { FastifyZodReplyError } from "./error.js";
3
+ type ReplyFunction<T> = T extends (...args: any[]) => any ? (this: FastifyReply, ...args: Parameters<T>) => ReturnType<T> : never;
4
+ export declare const createReply: (statusCode: number, defaultPayload: object) => ReplyFunction<any>;
5
+ export declare const createError: (statusCode: number) => (message: string | {
6
+ message: string;
7
+ }) => FastifyZodReplyError;
8
+ export {};
@@ -0,0 +1,19 @@
1
+ import { FastifyZodReplyError } from "./error.js";
2
+ export const createReply = (statusCode, defaultPayload) => {
3
+ return function (payload) {
4
+ const finalPayload = payload !== null && payload !== void 0 ? payload : defaultPayload;
5
+ if (typeof finalPayload === 'string')
6
+ throw createError(statusCode)(finalPayload);
7
+ this.type("application/json");
8
+ this.code(statusCode);
9
+ return finalPayload;
10
+ };
11
+ };
12
+ export const createError = (statusCode) => (message) => {
13
+ const customError = new FastifyZodReplyError("", statusCode);
14
+ if (typeof message === "string")
15
+ customError.message = message;
16
+ else
17
+ customError.message = message.message;
18
+ return customError;
19
+ };
@@ -0,0 +1,26 @@
1
+ import { type RawReplyDefaultExpression, type RawRequestDefaultExpression, type RawServerDefault, type RouteGenericInterface, type RouteHandlerMethod, type RouteShorthandOptions } from 'fastify';
2
+ import { z } from 'zod';
3
+ export type APIOptions<RouteInterface extends RouteGenericInterface = RouteGenericInterface> = RouteShorthandOptions<RawServerDefault, RawRequestDefaultExpression<RawServerDefault>, RawReplyDefaultExpression<RawServerDefault>, RouteInterface>;
4
+ export type APIHandler<RouteInterface extends RouteGenericInterface = RouteGenericInterface> = RouteHandlerMethod<RawServerDefault, RawRequestDefaultExpression<RawServerDefault>, RawReplyDefaultExpression<RawServerDefault>, RouteInterface>;
5
+ export interface RouteTag {
6
+ }
7
+ export interface RouteSecurity {
8
+ }
9
+ export type BaseZodSchema = {
10
+ Body?: z.ZodTypeAny;
11
+ Params?: z.ZodTypeAny;
12
+ Query?: z.ZodTypeAny;
13
+ Headers?: z.ZodTypeAny;
14
+ Reply: z.AnyZodObject;
15
+ Security?: (RouteSecurity[keyof RouteSecurity])[];
16
+ Tags?: (keyof RouteTag)[];
17
+ };
18
+ export type FastifyZodSchema<TZodSchema extends BaseZodSchema> = {
19
+ Body: TZodSchema['Body'] extends z.ZodTypeAny ? z.output<TZodSchema['Body']> : undefined;
20
+ Params: TZodSchema['Params'] extends z.ZodTypeAny ? z.output<TZodSchema['Params']> : undefined;
21
+ Querystring: TZodSchema['Query'] extends z.ZodTypeAny ? z.output<TZodSchema['Query']> : undefined;
22
+ Reply: TZodSchema['Reply'] extends z.ZodTypeAny ? z.input<TZodSchema['Reply']>[keyof z.infer<TZodSchema['Reply']>] : undefined;
23
+ };
24
+ export declare const route: <TSchema extends BaseZodSchema, FastifySchema extends FastifyZodSchema<TSchema> = FastifyZodSchema<TSchema>>(schema: TSchema, handler: APIHandler<FastifySchema>) => APIOptions<FastifySchema> & {
25
+ handler: APIHandler<FastifySchema>;
26
+ };
@@ -0,0 +1,63 @@
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');
3
+ const parse = async (schema, payload, tag) => {
4
+ const result = await schema.safeParseAsync(payload);
5
+ return Object.assign(Object.assign({}, result), { tag });
6
+ };
7
+ const findStatusCode = (statusCode, availableStatusCodes) => {
8
+ return availableStatusCodes.find(([key]) => {
9
+ if (!['number', 'string'].includes(typeof key))
10
+ return false;
11
+ if (typeof key === 'number')
12
+ return statusCode === key;
13
+ if (/^[0-9]{3}$/.test(key))
14
+ return statusCode === parseInt(key);
15
+ if (/^[0-9]xx$/i.test(key))
16
+ return statusCode.toString()[0] === key[0];
17
+ });
18
+ };
19
+ export const route = (schema, handler) => {
20
+ const finalResult = Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign({}, (schema.Body && { body: zodToJsonSchema(schema.Body) })), (schema.Params && { params: zodToJsonSchema(schema.Params) })), (schema.Query && { querystring: zodToJsonSchema(schema.Query) })), (schema.Headers && { headers: zodToJsonSchema(schema.Headers) })), { response: zodToJsonSchema(schema.Reply.partial(), {
21
+ $refStrategy: 'none',
22
+ strictUnions: true,
23
+ })['properties'] }), (schema.Security && { security: schema.Security })), (schema.Tags && { tags: schema.Tags }));
24
+ return {
25
+ schema: finalResult,
26
+ handler,
27
+ preHandler: async (request, reply) => {
28
+ var _a, _b, _c;
29
+ const results = await Promise.all([
30
+ ...(schema.Body ? [parse(schema.Body, request.body, 'body')] : []),
31
+ ...(schema.Params ? [parse(schema.Params, request.params, 'params')] : []),
32
+ ...(schema.Query ? [parse(schema.Query, request.query, 'query')] : []),
33
+ ]);
34
+ for (const result of results) {
35
+ if (!result.success) {
36
+ return reply
37
+ .code(400)
38
+ .type('application/json')
39
+ .send({
40
+ message: mapZodError(result.error, result.tag),
41
+ });
42
+ }
43
+ }
44
+ request.body = ((_a = results.find(r => r.tag === 'body')) === null || _a === void 0 ? void 0 : _a.data) || {};
45
+ request.params = ((_b = results.find(r => r.tag === 'params')) === null || _b === void 0 ? void 0 : _b.data) || {};
46
+ request.query = ((_c = results.find(r => r.tag === 'query')) === null || _c === void 0 ? void 0 : _c.data) || {};
47
+ },
48
+ preSerialization: (request, reply, payload, done) => {
49
+ const foundSchema = findStatusCode(reply.statusCode, Object.entries(schema.Reply.shape));
50
+ if (!foundSchema) {
51
+ request.log.warn(`[@efebia/fastify-zod-reply]: Reply schema of: ${request.routeOptions.url} does not have the specified status code: ${reply.statusCode}`);
52
+ return done(null, payload);
53
+ }
54
+ const serialized = foundSchema[1].safeParse(payload);
55
+ if (serialized.success) {
56
+ return done(null, serialized.data);
57
+ }
58
+ return done(null, {
59
+ message: mapZodError(serialized.error, 'reply'),
60
+ });
61
+ },
62
+ };
63
+ };
@@ -0,0 +1,2 @@
1
+ export declare function isObject(item: any): any;
2
+ export declare function mergeDeep<TResult extends object>(target: TResult, ...sources: any[]): TResult;
@@ -0,0 +1,21 @@
1
+ export function isObject(item) {
2
+ return (item && typeof item === 'object' && !Array.isArray(item));
3
+ }
4
+ export function mergeDeep(target, ...sources) {
5
+ if (!sources.length)
6
+ return target;
7
+ const source = sources.shift();
8
+ if (isObject(target) && isObject(source)) {
9
+ for (const key in source) {
10
+ if (isObject(source[key])) {
11
+ if (!target[key])
12
+ Object.assign(target, { [key]: {} });
13
+ mergeDeep(target[key], source[key]);
14
+ }
15
+ else {
16
+ Object.assign(target, { [key]: source[key] });
17
+ }
18
+ }
19
+ }
20
+ return mergeDeep(target, ...sources);
21
+ }
package/package.json CHANGED
@@ -1,8 +1,9 @@
1
1
  {
2
2
  "name": "@efebia/fastify-zod-reply",
3
- "version": "0.0.1",
3
+ "version": "0.0.2",
4
4
  "license": "MIT",
5
5
  "dependencies": {
6
+ "fastify": "^4.29.0",
6
7
  "fastify-plugin": "^4.5.1",
7
8
  "zod": "^3.24.2",
8
9
  "zod-to-json-schema": "^3.24.5"
@@ -11,11 +12,20 @@
11
12
  "/lib"
12
13
  ],
13
14
  "scripts": {
14
- "build": "tsc"
15
+ "build:esm": "tsc --module nodenext --moduleResolution nodenext --outDir lib/esm",
16
+ "build:cjs": "tsc --module commonjs --moduleResolution node --outDir lib/cjs",
17
+ "build": "yarn build:esm && yarn build:cjs"
15
18
  },
16
- "main": "./lib",
19
+ "main": "./lib/esm/index.js",
20
+ "types": "./lib/esm/index.d.ts",
21
+ "type": "module",
17
22
  "devDependencies": {
18
- "fastify": "^4.29.0",
19
23
  "typescript": "^5.8.3"
24
+ },
25
+ "exports": {
26
+ ".": {
27
+ "require": "./lib/cjs/index.js",
28
+ "import": "./lib/esm/index.js"
29
+ }
20
30
  }
21
31
  }
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes