@efebia/fastify-zod-reply 0.0.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.
- package/lib/error.d.ts +4 -0
- package/lib/error.js +10 -0
- package/lib/index.d.ts +31 -0
- package/lib/index.js +32 -0
- package/lib/reply.d.ts +8 -0
- package/lib/reply.js +24 -0
- package/lib/route.d.ts +26 -0
- package/lib/route.js +67 -0
- package/lib/utils.d.ts +2 -0
- package/lib/utils.js +25 -0
- package/package.json +21 -0
package/lib/error.d.ts
ADDED
package/lib/error.js
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.FastifyZodReplyError = void 0;
|
|
4
|
+
class FastifyZodReplyError extends Error {
|
|
5
|
+
constructor(message, statusCode) {
|
|
6
|
+
super(message);
|
|
7
|
+
this.statusCode = statusCode !== null && statusCode !== void 0 ? statusCode : -1;
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
exports.FastifyZodReplyError = FastifyZodReplyError;
|
package/lib/index.d.ts
ADDED
|
@@ -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;
|
package/lib/index.js
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
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
|
+
const fastify_plugin_1 = __importDefault(require("fastify-plugin"));
|
|
7
|
+
const reply_1 = require("./reply");
|
|
8
|
+
const utils_1 = require("./utils");
|
|
9
|
+
const defaultOptions = {
|
|
10
|
+
statusCodes: {
|
|
11
|
+
ok: { statusCode: 200, payload: { message: 'ok' } },
|
|
12
|
+
created: { statusCode: 201, payload: { message: 'created' } },
|
|
13
|
+
accepted: { statusCode: 202, payload: { message: 'accepted' } },
|
|
14
|
+
noContent: { statusCode: 204, payload: { message: 'noContent' } },
|
|
15
|
+
badRequest: { statusCode: 400, payload: { message: 'badRequest' } },
|
|
16
|
+
unauthorized: { statusCode: 401, payload: { message: 'unauthorized' } },
|
|
17
|
+
forbidden: { statusCode: 403, payload: { message: 'forbidden' } },
|
|
18
|
+
notFound: { statusCode: 404, payload: { message: 'notFound' } },
|
|
19
|
+
notAcceptable: { statusCode: 406, payload: { message: 'notAcceptable' } },
|
|
20
|
+
conflict: { statusCode: 409, payload: { message: 'conflict' } },
|
|
21
|
+
internalServerError: { statusCode: 500, payload: { message: 'internalServerError' } },
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
exports.default = (0, fastify_plugin_1.default)(async (fastify, opts) => {
|
|
25
|
+
const finalOptions = (0, utils_1.mergeDeep)(defaultOptions, opts);
|
|
26
|
+
Object.entries(finalOptions.statusCodes).forEach(([key, value]) => {
|
|
27
|
+
fastify.decorateReply(key, (0, reply_1.createReply)(value.statusCode, value.payload));
|
|
28
|
+
});
|
|
29
|
+
}, {
|
|
30
|
+
fastify: "4.x",
|
|
31
|
+
name: '@efebia/fastify-zod-reply'
|
|
32
|
+
});
|
package/lib/reply.d.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { FastifyReply } from "fastify";
|
|
2
|
+
import { FastifyZodReplyError } from "./error";
|
|
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 {};
|
package/lib/reply.js
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createError = exports.createReply = void 0;
|
|
4
|
+
const error_1 = require("./error");
|
|
5
|
+
const createReply = (statusCode, defaultPayload) => {
|
|
6
|
+
return function (payload) {
|
|
7
|
+
const finalPayload = payload !== null && payload !== void 0 ? payload : defaultPayload;
|
|
8
|
+
if (typeof finalPayload === 'string')
|
|
9
|
+
throw (0, exports.createError)(statusCode)(finalPayload);
|
|
10
|
+
this.type("application/json");
|
|
11
|
+
this.code(statusCode);
|
|
12
|
+
return finalPayload;
|
|
13
|
+
};
|
|
14
|
+
};
|
|
15
|
+
exports.createReply = createReply;
|
|
16
|
+
const createError = (statusCode) => (message) => {
|
|
17
|
+
const customError = new error_1.FastifyZodReplyError("", statusCode);
|
|
18
|
+
if (typeof message === "string")
|
|
19
|
+
customError.message = message;
|
|
20
|
+
else
|
|
21
|
+
customError.message = message.message;
|
|
22
|
+
return customError;
|
|
23
|
+
};
|
|
24
|
+
exports.createError = createError;
|
package/lib/route.d.ts
ADDED
|
@@ -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
|
+
};
|
package/lib/route.js
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.route = void 0;
|
|
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');
|
|
6
|
+
const parse = async (schema, payload, tag) => {
|
|
7
|
+
const result = await schema.safeParseAsync(payload);
|
|
8
|
+
return Object.assign(Object.assign({}, result), { tag });
|
|
9
|
+
};
|
|
10
|
+
const findStatusCode = (statusCode, availableStatusCodes) => {
|
|
11
|
+
return availableStatusCodes.find(([key]) => {
|
|
12
|
+
if (!['number', 'string'].includes(typeof key))
|
|
13
|
+
return false;
|
|
14
|
+
if (typeof key === 'number')
|
|
15
|
+
return statusCode === key;
|
|
16
|
+
if (/^[0-9]{3}$/.test(key))
|
|
17
|
+
return statusCode === parseInt(key);
|
|
18
|
+
if (/^[0-9]xx$/i.test(key))
|
|
19
|
+
return statusCode.toString()[0] === key[0];
|
|
20
|
+
});
|
|
21
|
+
};
|
|
22
|
+
const route = (schema, handler) => {
|
|
23
|
+
const finalResult = Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign({}, (schema.Body && { body: (0, zod_to_json_schema_1.zodToJsonSchema)(schema.Body) })), (schema.Params && { params: (0, zod_to_json_schema_1.zodToJsonSchema)(schema.Params) })), (schema.Query && { querystring: (0, zod_to_json_schema_1.zodToJsonSchema)(schema.Query) })), (schema.Headers && { headers: (0, zod_to_json_schema_1.zodToJsonSchema)(schema.Headers) })), { response: (0, zod_to_json_schema_1.zodToJsonSchema)(schema.Reply.partial(), {
|
|
24
|
+
$refStrategy: 'none',
|
|
25
|
+
strictUnions: true,
|
|
26
|
+
})['properties'] }), (schema.Security && { security: schema.Security })), (schema.Tags && { tags: schema.Tags }));
|
|
27
|
+
return {
|
|
28
|
+
schema: finalResult,
|
|
29
|
+
handler,
|
|
30
|
+
preHandler: async (request, reply) => {
|
|
31
|
+
var _a, _b, _c;
|
|
32
|
+
const results = await Promise.all([
|
|
33
|
+
...(schema.Body ? [parse(schema.Body, request.body, 'body')] : []),
|
|
34
|
+
...(schema.Params ? [parse(schema.Params, request.params, 'params')] : []),
|
|
35
|
+
...(schema.Query ? [parse(schema.Query, request.query, 'query')] : []),
|
|
36
|
+
]);
|
|
37
|
+
for (const result of results) {
|
|
38
|
+
if (!result.success) {
|
|
39
|
+
return reply
|
|
40
|
+
.code(400)
|
|
41
|
+
.type('application/json')
|
|
42
|
+
.send({
|
|
43
|
+
message: mapZodError(result.error, result.tag),
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
request.body = ((_a = results.find(r => r.tag === 'body')) === null || _a === void 0 ? void 0 : _a.data) || {};
|
|
48
|
+
request.params = ((_b = results.find(r => r.tag === 'params')) === null || _b === void 0 ? void 0 : _b.data) || {};
|
|
49
|
+
request.query = ((_c = results.find(r => r.tag === 'query')) === null || _c === void 0 ? void 0 : _c.data) || {};
|
|
50
|
+
},
|
|
51
|
+
preSerialization: (request, reply, payload, done) => {
|
|
52
|
+
const foundSchema = findStatusCode(reply.statusCode, Object.entries(schema.Reply.shape));
|
|
53
|
+
if (!foundSchema) {
|
|
54
|
+
request.log.warn(`[@efebia/fastify-zod-reply]: Reply schema of: ${request.routeOptions.url} does not have the specified status code: ${reply.statusCode}`);
|
|
55
|
+
return done(null, payload);
|
|
56
|
+
}
|
|
57
|
+
const serialized = foundSchema[1].safeParse(payload);
|
|
58
|
+
if (serialized.success) {
|
|
59
|
+
return done(null, serialized.data);
|
|
60
|
+
}
|
|
61
|
+
return done(null, {
|
|
62
|
+
message: mapZodError(serialized.error, 'reply'),
|
|
63
|
+
});
|
|
64
|
+
},
|
|
65
|
+
};
|
|
66
|
+
};
|
|
67
|
+
exports.route = route;
|
package/lib/utils.d.ts
ADDED
package/lib/utils.js
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.isObject = isObject;
|
|
4
|
+
exports.mergeDeep = mergeDeep;
|
|
5
|
+
function isObject(item) {
|
|
6
|
+
return (item && typeof item === 'object' && !Array.isArray(item));
|
|
7
|
+
}
|
|
8
|
+
function mergeDeep(target, ...sources) {
|
|
9
|
+
if (!sources.length)
|
|
10
|
+
return target;
|
|
11
|
+
const source = sources.shift();
|
|
12
|
+
if (isObject(target) && isObject(source)) {
|
|
13
|
+
for (const key in source) {
|
|
14
|
+
if (isObject(source[key])) {
|
|
15
|
+
if (!target[key])
|
|
16
|
+
Object.assign(target, { [key]: {} });
|
|
17
|
+
mergeDeep(target[key], source[key]);
|
|
18
|
+
}
|
|
19
|
+
else {
|
|
20
|
+
Object.assign(target, { [key]: source[key] });
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
return mergeDeep(target, ...sources);
|
|
25
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@efebia/fastify-zod-reply",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"license": "MIT",
|
|
5
|
+
"dependencies": {
|
|
6
|
+
"fastify-plugin": "^4.5.1",
|
|
7
|
+
"zod": "^3.24.2",
|
|
8
|
+
"zod-to-json-schema": "^3.24.5"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"/lib"
|
|
12
|
+
],
|
|
13
|
+
"scripts": {
|
|
14
|
+
"build": "tsc"
|
|
15
|
+
},
|
|
16
|
+
"main": "./lib",
|
|
17
|
+
"devDependencies": {
|
|
18
|
+
"fastify": "^4.29.0",
|
|
19
|
+
"typescript": "^5.8.3"
|
|
20
|
+
}
|
|
21
|
+
}
|