Skip to content

Commit

Permalink
Fix #327 and #335: break change from nest.BadRequestException + `fa…
Browse files Browse the repository at this point in the history
…stify` support

Since `@nestjs/common@9` update, throwing `HttpException` in `createParamDecorator` does not work, and just always be consired as `InternalServerException`. Therefore, every validation decorators just had thrown 500 status error instead of 400. In such reason, I've changed my custom validation decorators to send 400 status error directly through `express.Response` or `FastifyReply` instance.

Also, from now on, `@nestia/core` fully supports `fastify`.
  • Loading branch information
samchon committed May 5, 2023
1 parent f3840de commit adbc5d4
Show file tree
Hide file tree
Showing 79 changed files with 2,018 additions and 118 deletions.
5 changes: 3 additions & 2 deletions packages/core/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@nestia/core",
"version": "1.2.0",
"version": "1.2.1",
"description": "Super-fast validation decorators of NestJS",
"main": "lib/index.js",
"typings": "lib/index.d.ts",
Expand Down Expand Up @@ -33,13 +33,14 @@
},
"homepage": "https://github.com/samchon/nestia",
"dependencies": {
"@nestia/fetcher": "^1.2.0",
"@nestia/fetcher": "^1.2.1",
"@nestjs/common": ">= 7.0.1",
"@nestjs/core": ">= 7.0.1",
"@nestjs/platform-express": ">= 7.0.1",
"@nestjs/platform-fastify": ">= 7.0.1",
"detect-ts-node": "^1.0.5",
"glob": "^7.2.0",
"raw-body": ">= 2.0.0",
"reflect-metadata": ">= 0.1.12",
"rxjs": ">= 6.0.0",
"typia": "^3.8.4"
Expand Down
23 changes: 16 additions & 7 deletions packages/core/src/decorators/EncryptedBody.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { AesPkcs5, IEncryptionPassword } from "@nestia/fetcher";
import {
BadRequestException,
ExecutionContext,
HttpException,
createParamDecorator,
} from "@nestjs/common";
import type express from "express";
Expand All @@ -12,7 +13,9 @@ import { assert, is, validate } from "typia";
import { IRequestBodyValidator } from "../options/IRequestBodyValidator";
import { Singleton } from "../utils/Singleton";
import { ENCRYPTION_METADATA_KEY } from "./internal/EncryptedConstant";
import { get_text_body } from "./internal/get_text_body";
import { headers_to_object } from "./internal/headers_to_object";
import { send_bad_request } from "./internal/send_bad_request";
import { validate_request_body } from "./internal/validate_request_body";

/**
Expand Down Expand Up @@ -42,22 +45,24 @@ export function EncryptedBody<T>(
const checker = validate_request_body("EncryptedBody")(validator);
return createParamDecorator(async function EncryptedBody(
_unknown: any,
ctx: ExecutionContext,
context: ExecutionContext,
) {
const request: express.Request | FastifyRequest = ctx
const request: express.Request | FastifyRequest = context
.switchToHttp()
.getRequest();
if (isTextPlain(request.headers["content-type"]) === false)
throw new BadRequestException(
`Request body type is not "text/plain".`,
return send_bad_request(context)(
new BadRequestException(
`Request body type is not "text/plain".`,
),
);

const param:
| IEncryptionPassword
| IEncryptionPassword.Closure
| undefined = Reflect.getMetadata(
ENCRYPTION_METADATA_KEY,
ctx.getClass(),
context.getClass(),
);
if (!param)
throw new Error(
Expand All @@ -68,15 +73,19 @@ export function EncryptedBody<T>(
const headers: Singleton<Record<string, string>> = new Singleton(() =>
headers_to_object(request.headers),
);
const body: string = request.body;
const body: string = await get_text_body(request);
const password: IEncryptionPassword =
typeof param === "function"
? param({ headers: headers.get(), body }, false)
: param;

// PARSE AND VALIDATE DATA
const data: any = JSON.parse(decrypt(body, password.key, password.iv));
checker(data);
const error: Error | null = checker(data);
if (error !== null)
if (error instanceof HttpException)
return send_bad_request(context)(error);
else throw error;
return data;
})();
}
Expand Down
19 changes: 12 additions & 7 deletions packages/core/src/decorators/PlainBody.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ import {
import type express from "express";
import type { FastifyRequest } from "fastify";

import { get_text_body } from "./internal/get_text_body";
import { send_bad_request } from "./internal/send_bad_request";

/**
* Plain body decorator.
*
Expand All @@ -28,15 +31,17 @@ import type { FastifyRequest } from "fastify";
* @author Jeongho Nam - https://github.com/samchon
*/
export const PlainBody: () => ParameterDecorator = createParamDecorator(
async function PlainBody(_data: any, ctx: ExecutionContext) {
const request: express.Request | FastifyRequest = ctx
async function PlainBody(_data: any, context: ExecutionContext) {
const request: express.Request | FastifyRequest = context
.switchToHttp()
.getRequest();
if (isTextPlain(request.headers["content-type"]) === false)
throw new BadRequestException(
`Request body type is not "text/plain".`,
);
return request.body;
return isTextPlain(request.headers["content-type"])
? get_text_body(request)
: send_bad_request(context)(
new BadRequestException(
`Request body type is not "text/plain".`,
),
);
},
);

Expand Down
21 changes: 12 additions & 9 deletions packages/core/src/decorators/TypedBody.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import {
BadRequestException,
ExecutionContext,
HttpException,
createParamDecorator,
} from "@nestjs/common";
import type express from "express";
Expand All @@ -9,6 +10,7 @@ import type { FastifyRequest } from "fastify";
import { assert, is, validate } from "typia";

import { IRequestBodyValidator } from "../options/IRequestBodyValidator";
import { send_bad_request } from "./internal/send_bad_request";
import { validate_request_body } from "./internal/validate_request_body";

/**
Expand All @@ -29,24 +31,25 @@ export function TypedBody<T>(
validator?: IRequestBodyValidator<T>,
): ParameterDecorator {
const checker = validate_request_body("TypedBody")(validator);
return createParamDecorator(async function TypedBody(
return createParamDecorator(function TypedBody(
_unknown: any,
context: ExecutionContext,
) {
const request: express.Request | FastifyRequest = context
.switchToHttp()
.getRequest();
if (isApplicationJson(request.headers["content-type"]) === false)
throw new BadRequestException(
`Request body type is not "application/json".`,
return send_bad_request(context)(
new BadRequestException(
`Request body type is not "application/json".`,
),
);

try {
checker(request.body);
} catch (exp) {
if (exp instanceof BadRequestException) console.log(exp);
throw exp;
}
const error: Error | null = checker(request.body);
if (error !== null)
if (error instanceof HttpException)
return send_bad_request(context)(error);
else throw error;
return request.body;
})();
}
Expand Down
24 changes: 16 additions & 8 deletions packages/core/src/decorators/TypedParam.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import {
} from "@nestjs/common";
import type express from "express";

import { send_bad_request } from "./internal/send_bad_request";

/**
* Type safe URL parameter decorator.
*
Expand Down Expand Up @@ -40,29 +42,35 @@ export function TypedParam(
type?: "boolean" | "number" | "string" | "uuid",
nullable?: false | true,
): ParameterDecorator {
function TypedParam({}: any, ctx: ExecutionContext) {
const request: express.Request = ctx.switchToHttp().getRequest();
function TypedParam({}: any, context: ExecutionContext) {
const request: express.Request = context.switchToHttp().getRequest();
const str: string = request.params[name];

if (nullable === true && str === "null") return null;
else if (type === "boolean") {
if (str === "true" || str === "1") return true;
else if (str === "false" || str === "0") return false;
else
throw new BadRequestException(
`Value of the URL parameter '${name}' is not a boolean.`,
return send_bad_request(context)(
new BadRequestException(
`Value of the URL parameter '${name}' is not a boolean.`,
),
);
} else if (type === "number") {
const value: number = Number(str);
if (isNaN(value))
throw new BadRequestException(
`Value of the URL parameter "${name}" is not a number.`,
return send_bad_request(context)(
new BadRequestException(
`Value of the URL parameter "${name}" is not a number.`,
),
);
return value;
} else if (type === "uuid") {
if (UUID_PATTERN.test(str) === false)
throw new BadRequestException(
`Value of the URL parameter "${name}" is not a valid UUID.`,
return send_bad_request(context)(
new BadRequestException(
`Value of the URL parameter "${name}" is not a valid UUID.`,
),
);
return str;
} else return str;
Expand Down
36 changes: 21 additions & 15 deletions packages/core/src/decorators/TypedQuery.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,13 @@ import {
ExecutionContext,
createParamDecorator,
} from "@nestjs/common";
import express from "express";
import type express from "express";
import type { FastifyRequest } from "fastify";

import { TypeGuardError, assert } from "typia";
import typia, { TypeGuardError, assert } from "typia";

import { TransformError } from "./internal/TransformError";
import { send_bad_request } from "./internal/send_bad_request";

/**
* Type safe URL query decorator.
Expand All @@ -27,25 +29,29 @@ export function TypedQuery<T>(
): ParameterDecorator {
if (decoder === undefined) throw TransformError("TypedQuery");

return createParamDecorator(async function TypedQuery(
return createParamDecorator(function TypedQuery(
_unknown: any,
ctx: ExecutionContext,
context: ExecutionContext,
) {
const request: express.Request = ctx.switchToHttp().getRequest();
const request: express.Request | FastifyRequest = context
.switchToHttp()
.getRequest();
const params: URLSearchParams = new URLSearchParams(tail(request.url));

try {
return decoder(params);
} catch (exp) {
if (exp instanceof TypeGuardError) {
throw new BadRequestException({
path: exp.path,
reason: exp.message,
expected: exp.expected,
value: exp.value,
message:
"Request query parameters are not following the promised type.",
});
}
if (typia.is<TypeGuardError>(exp))
return send_bad_request(context)(
new BadRequestException({
path: exp.path,
reason: exp.message,
expected: exp.expected,
value: exp.value,
message:
"Request query parameters are not following the promised type.",
}),
);
throw exp;
}
})();
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { InternalServerErrorException } from "@nestjs/common";

import { IValidation, TypeGuardError } from "typia";
import typia, { IValidation, TypeGuardError } from "typia";

import { IResponseBodyStringifier } from "../../options/IResponseBodyStringifier";
import { TransformError } from "./TransformError";
Expand Down Expand Up @@ -41,15 +41,14 @@ const assert =
try {
return closure(data);
} catch (exp) {
if (exp instanceof TypeGuardError) {
if (typia.is<TypeGuardError>(exp))
throw new InternalServerErrorException({
path: exp.path,
reason: exp.message,
expected: exp.expected,
value: exp.value,
message: MESSAGE,
});
}
throw exp;
}
};
Expand Down
14 changes: 14 additions & 0 deletions packages/core/src/decorators/internal/get_text_body.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import type express from "express";
import type { FastifyRequest } from "fastify";
import raw from "raw-body";

export const get_text_body = async (
request: express.Request | FastifyRequest,
): Promise<string> =>
isExpressRequest(request)
? (await raw(request)).toString("utf8")
: (request.body as string);

const isExpressRequest = (
request: express.Request | FastifyRequest,
): request is express.Request => (request as express.Request).app !== undefined;
12 changes: 12 additions & 0 deletions packages/core/src/decorators/internal/send_bad_request.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { BadRequestException, ExecutionContext } from "@nestjs/common";
import type express from "express";
import type { FastifyReply } from "fastify";

export const send_bad_request =
(context: ExecutionContext) =>
(error: BadRequestException): void => {
const response: express.Response | FastifyReply = context
.switchToHttp()
.getResponse();
response.status(error.getStatus()).send(error.getResponse());
};
Loading

0 comments on commit adbc5d4

Please sign in to comment.