Skip to content

Commit

Permalink
Route decorators of TypedQuery.
Browse files Browse the repository at this point in the history
  - `@TypedQuery.Get()`
  - `@TypedQuery.Post()`
  - `@TypedQuery.Put()`
  - `@TypedQuery.Patch()`
  - `@TypedQuery.Delete()`
  • Loading branch information
samchon committed Oct 10, 2023
1 parent 2a15bda commit 569e6a7
Show file tree
Hide file tree
Showing 21 changed files with 718 additions and 87 deletions.
22 changes: 11 additions & 11 deletions packages/core/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@nestia/core",
"version": "2.2.0-dev.20231008",
"version": "2.2.0-dev.20231010",
"description": "Super-fast validation decorators of NestJS",
"main": "lib/index.js",
"typings": "lib/index.d.ts",
Expand Down Expand Up @@ -34,20 +34,20 @@
},
"homepage": "https://nestia.io",
"dependencies": {
"@nestia/fetcher": "^2.2.0-dev.20231008",
"@nestjs/common": ">= 7.0.1",
"@nestjs/core": ">= 7.0.1",
"@nestjs/platform-express": ">= 7.0.1",
"@nestjs/platform-fastify": ">= 7.0.1",
"@nestia/fetcher": "^2.2.0-dev.20231010",
"@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": "^5.2.0"
"raw-body": ">=2.0.0",
"reflect-metadata": ">=0.1.12",
"rxjs": ">=6.0.0",
"typia": ">=5.2.0 <6.0.0"
},
"peerDependencies": {
"@nestia/fetcher": ">=2.2.0-dev.20231008",
"@nestia/fetcher": ">=2.2.0-dev.20231010",
"@nestjs/common": ">=7.0.1",
"@nestjs/core": ">=7.0.1",
"@nestjs/platform-express": ">=7.0.1",
Expand Down
126 changes: 126 additions & 0 deletions packages/core/src/decorators/TypedQuery.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,28 @@
import {
BadRequestException,
CallHandler,
Delete,
ExecutionContext,
Get,
NestInterceptor,
Patch,
Post,
Put,
UseInterceptors,
applyDecorators,
createParamDecorator,
} from "@nestjs/common";
import { HttpArgumentsHost } from "@nestjs/common/interfaces";
import type express from "express";
import type { FastifyRequest } from "fastify";
import { catchError, map } from "rxjs";

import typia from "typia";

import { IRequestQueryValidator } from "../options/IRequestQueryValidator";
import { IResponseBodyQuerifier } from "../options/IResponseBodyQuerifier";
import { get_path_and_querify } from "./internal/get_path_and_querify";
import { route_error } from "./internal/route_error";
import { validate_request_query } from "./internal/validate_request_query";

/**
Expand Down Expand Up @@ -77,6 +91,81 @@ export namespace TypedQuery {
Object.assign(Body, typia.http.assertQuery);
Object.assign(Body, typia.http.isQuery);
Object.assign(Body, typia.http.validateQuery);

/**
* Router decorator function for the GET method.
*
* @param path Path of the HTTP request
* @returns Method decorator
*/
export const Get = Generator("Get");

/**
* Router decorator function for the POST method.
*
* @param path Path of the HTTP request
* @returns Method decorator
*/
export const Post = Generator("Post");

/**
* Router decorator function for the PATH method.
*
* @param path Path of the HTTP request
* @returns Method decorator
*/
export const Patch = Generator("Patch");

/**
* Router decorator function for the PUT method.
*
* @param path Path of the HTTP request
* @returns Method decorator
*/
export const Put = Generator("Put");

/**
* Router decorator function for the DELETE method.
*
* @param path Path of the HTTP request
* @returns Method decorator
*/
export const Delete = Generator("Delete");

/**
* @internal
*/
function Generator(method: "Get" | "Post" | "Put" | "Patch" | "Delete") {
function route(path?: string | string[]): MethodDecorator;
function route<T>(
stringify?: IResponseBodyQuerifier<T>,
): MethodDecorator;
function route<T>(
path: string | string[],
stringify?: IResponseBodyQuerifier<T>,
): MethodDecorator;

function route(...args: any[]): MethodDecorator {
const [path, stringify] = get_path_and_querify(
`TypedQuery.${method}`,
)(...args);
return applyDecorators(
ROUTERS[method](path),
UseInterceptors(new TypedQueryRouteInterceptor(stringify)),
);
}
return route;
}
for (const method of [typia.assert, typia.is, typia.validate])
for (const [key, value] of Object.entries(method))
for (const deco of [
TypedQuery.Get,
TypedQuery.Delete,
TypedQuery.Post,
TypedQuery.Put,
TypedQuery.Patch,
])
(deco as any)[key] = value;
}
Object.assign(TypedQuery, typia.http.assertQuery);
Object.assign(TypedQuery, typia.http.isQuery);
Expand All @@ -90,6 +179,9 @@ function tail(url: string): string {
return index === -1 ? "" : url.substring(index + 1);
}

/**
* @internal
*/
function isApplicationQuery(text?: string): boolean {
return (
text !== undefined &&
Expand All @@ -100,6 +192,9 @@ function isApplicationQuery(text?: string): boolean {
);
}

/**
* @internal
*/
class FakeURLSearchParams {
public constructor(private readonly target: Record<string, string[]>) {}

Expand All @@ -125,3 +220,34 @@ class FakeURLSearchParams {
: [value];
}
}

/**
* @internal
*/
class TypedQueryRouteInterceptor implements NestInterceptor {
public constructor(
private readonly toSearchParams: (input: any) => URLSearchParams,
) {}

public intercept(context: ExecutionContext, next: CallHandler) {
const http: HttpArgumentsHost = context.switchToHttp();
const response: express.Response = http.getResponse();
response.header("Content-Type", "application/x-www-form-urlencoded");

return next.handle().pipe(
map((value) => this.toSearchParams(value).toString()),
catchError((err) => route_error(http.getRequest(), err)),
);
}
}

/**
* @internal
*/
const ROUTERS = {
Get,
Post,
Patch,
Put,
Delete,
};
104 changes: 104 additions & 0 deletions packages/core/src/decorators/internal/get_path_and_querify.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import { InternalServerErrorException } from "@nestjs/common";

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

import { IResponseBodyQuerifier } from "../../options/IResponseBodyQuerifier";
import { NoTransformConfigureError } from "./NoTransformConfigureError";

/**
* @internal
*/
export const get_path_and_querify =
(method: string) =>
(
...args: any[]
): [string | string[] | undefined, (input: any) => URLSearchParams] => {
const path: string | string[] | null | undefined =
args[0] === undefined ||
typeof args[0] === "string" ||
Array.isArray(args[0])
? args[0]
: null;
const functor: IResponseBodyQuerifier<any> | undefined =
path === null ? args[0] : args[1];
return [path ?? undefined, take(method)(functor)];
};

/**
* @internal
*/
const take =
(method: string) =>
<T>(functor?: IResponseBodyQuerifier<T> | null) => {
if (functor === undefined) throw NoTransformConfigureError(method);
else if (functor === null) return querify;
else if (functor.type === "stringify") return functor.stringify;
else if (functor.type === "assert") return assert(functor.assert);
else if (functor.type === "is") return is(functor.is);
else if (functor.type === "validate") return validate(functor.validate);
throw new Error(
`Error on nestia.core.${method}(): invalid typed stringify function.`,
);
};

const querify = (input: Record<string, any>): URLSearchParams => {
const output: URLSearchParams = new URLSearchParams();
for (const [key, value] of Object.entries(input))
if (key === undefined) continue;
else if (Array.isArray(value))
for (const elem of value) output.append(key, String(elem));
else output.append(key, String(value));
return output;
};

/**
* @internal
*/
const assert =
<T>(closure: (data: T) => URLSearchParams) =>
(data: T) => {
try {
return closure(data);
} catch (exp) {
if (typia.is<TypeGuardError>(exp))
throw new InternalServerErrorException({
path: exp.path,
reason: exp.message,
expected: exp.expected,
value: exp.value,
message: MESSAGE,
});
throw exp;
}
};

/**
* @internal
*/
const is =
<T>(closure: (data: T) => URLSearchParams | null) =>
(data: T) => {
const result: URLSearchParams | null = closure(data);
if (result === null) throw new InternalServerErrorException(MESSAGE);
return result;
};

/**
* @internal
*/
const validate =
<T>(closure: (data: T) => IValidation<URLSearchParams>) =>
(data: T) => {
const result: IValidation<URLSearchParams> = closure(data);
if (result.success === false)
throw new InternalServerErrorException({
errors: result.errors,
message: MESSAGE,
});
return result.data;
};

/**
* @internal
*/
const MESSAGE = "Response body data is not following the promised type.";
25 changes: 25 additions & 0 deletions packages/core/src/options/IResponseBodyQuerifier.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { IValidation } from "typia";

export type IResponseBodyQuerifier<T> =
| IResponseBodyquerifier.IStringify<T>
| IResponseBodyquerifier.IIs<T>
| IResponseBodyquerifier.IAssert<T>
| IResponseBodyquerifier.IValidate<T>;
export namespace IResponseBodyquerifier {
export interface IStringify<T> {
type: "stringify";
stringify: (input: T) => URLSearchParams;
}
export interface IIs<T> {
type: "is";
is: (input: T) => URLSearchParams | null;
}
export interface IAssert<T> {
type: "assert";
assert: (input: T) => URLSearchParams;
}
export interface IValidate<T> {
type: "validate";
validate: (input: T) => IValidation<URLSearchParams>;
}
}
55 changes: 55 additions & 0 deletions packages/core/src/programmers/TypedQueryRouteProgrammer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import ts from "typescript";

import { IProject } from "typia/lib/transformers/IProject";

import { INestiaTransformProject } from "../options/INestiaTransformProject";
import { HttpAssertQuerifyProgrammer } from "./http/HttpAssertQuerifyProgrammer";
import { HttpIsQuerifyProgrammer } from "./http/HttpIsQuerifyProgrammer";
import { HttpQuerifyProgrammer } from "./http/HttpQuerifyProgrammer";
import { HttpValidateQuerifyProgrammer } from "./http/HttpValidateQuerifyProgrammer";

export namespace TypedQueryRouteProgrammer {
export const generate =
(project: INestiaTransformProject) =>
(modulo: ts.LeftHandSideExpression) =>
(type: ts.Type): ts.Expression => {
// GENERATE STRINGIFY PLAN
const parameter = (
key: string,
programmer: (
project: IProject,
) => (
modulo: ts.LeftHandSideExpression,
) => (type: ts.Type) => ts.ArrowFunction,
) =>
ts.factory.createObjectLiteralExpression([
ts.factory.createPropertyAssignment(
ts.factory.createIdentifier("type"),
ts.factory.createStringLiteral(key),
),
ts.factory.createPropertyAssignment(
ts.factory.createIdentifier(key),
programmer({
...project,
options: {}, // use default option
})(modulo)(type),
),
]);

// RETURNS
if (project.options.stringify === "is")
return parameter("is", HttpIsQuerifyProgrammer.write);
else if (project.options.stringify === "validate")
return parameter(
"validate",
HttpValidateQuerifyProgrammer.write,
);
else if (project.options.stringify === "stringify")
return parameter("stringify", HttpQuerifyProgrammer.write);
else if (project.options.stringify === null)
return ts.factory.createNull();

// ASSERT IS DEFAULT
return parameter("assert", HttpAssertQuerifyProgrammer.write);
};
}
Loading

0 comments on commit 569e6a7

Please sign in to comment.