diff --git a/docs/en/v0/guide/quickStart.md b/docs/en/v0/guide/quickStart.md index 2042338..7eb1345 100644 --- a/docs/en/v0/guide/quickStart.md +++ b/docs/en/v0/guide/quickStart.md @@ -13,13 +13,13 @@ next: ## Install dependencies ::: code-group ```bash [npm] -npm install @duplojs/http@0 @duplojs/utils@1 +npm install @duplojs/http@0 @duplojs/utils@1 @duplojs/data-parser-tools@0 ``` ```bash [yarn] -yarn add @duplojs/http@0 @duplojs/utils@1 +yarn add @duplojs/http@0 @duplojs/utils@1 @duplojs/data-parser-tools@0 ``` ```bash [pnpm] -pnpm add @duplojs/http@0 @duplojs/utils@1 +pnpm add @duplojs/http@0 @duplojs/utils@1 @duplojs/data-parser-tools@0 ``` ::: diff --git a/docs/en/v0/guide/server/getData.md b/docs/en/v0/guide/server/getData.md index 6f65179..907e658 100644 --- a/docs/en/v0/guide/server/getData.md +++ b/docs/en/v0/guide/server/getData.md @@ -26,3 +26,17 @@ The data is then available in the `Floor`, an object that contains the accumulat ``` Data extraction can be done at two levels. The storage key will be the `dataParser` key. + +## Receive FormData +```ts twoslash {6,11} +// @version: 0 + +``` + +To receive `FormData`, you just need to configure a `bodyController` on your route. This tells the route to prepare for a `FormData` stream during a request. The parameters defined for this `bodyController` are applied while streaming the body, unlike the `dataParser`, which is applied after the body is received. + +::: info +Extraction schemas can be as complex as JSON schemas, because the `FormData` serializer used supports deeper nesting than the default `FormData`. + +File extraction is done with the file `dataParser` (`SDPE.file()`) from `@duplojs/server-utils`. +::: diff --git a/docs/examples/v0/guide/server/getData/formData.ts b/docs/examples/v0/guide/server/getData/formData.ts new file mode 100644 index 0000000..3ed0c53 --- /dev/null +++ b/docs/examples/v0/guide/server/getData/formData.ts @@ -0,0 +1,31 @@ +import { controlBodyAsFormData, ResponseContract, useRouteBuilder } from "@duplojs/http"; +import { SDPE } from "@duplojs/server-utils"; +import { asserts, DPE, E, O, Path } from "@duplojs/utils"; + +useRouteBuilder("POST", "/documents", { + bodyController: controlBodyAsFormData({ maxFileQuantity: 5 }), +}) + .extract({ + body: { + userId: DPE.coerce.number(), + files: SDPE.file().array().max(3), + }, + }) + .handler( + ResponseContract.noContent("files.receive"), + async({ files, userId }, { response }) => { + for (const [index, file] of O.entries(files)) { + asserts( + await file.move( + Path.resolveRelative([ + "new/path/of/file", + `${userId}-${index}${file.getExtension() ?? ""}`, + ]), + ), + E.isRight, + ); + } + + return response("files.receive"); + }, + ); diff --git a/docs/fr/v0/guide/quickStart.md b/docs/fr/v0/guide/quickStart.md index 304df95..045f512 100644 --- a/docs/fr/v0/guide/quickStart.md +++ b/docs/fr/v0/guide/quickStart.md @@ -13,13 +13,13 @@ next: ## Installation des dépendances ::: code-group ```bash [npm] -npm install @duplojs/http@0 @duplojs/utils@1 +npm install @duplojs/http@0 @duplojs/utils@1 @duplojs/data-parser-tools@0 ``` ```bash [yarn] -yarn add @duplojs/http@0 @duplojs/utils@1 +yarn add @duplojs/http@0 @duplojs/utils@1 @duplojs/data-parser-tools@0 ``` ```bash [pnpm] -pnpm add @duplojs/http@0 @duplojs/utils@1 +pnpm add @duplojs/http@0 @duplojs/utils@1 @duplojs/data-parser-tools@0 ``` ::: diff --git a/docs/fr/v0/guide/server/getData.md b/docs/fr/v0/guide/server/getData.md index d6a0cbc..5c6f596 100644 --- a/docs/fr/v0/guide/server/getData.md +++ b/docs/fr/v0/guide/server/getData.md @@ -26,3 +26,17 @@ Les données sont ensuite disponibles dans le `Floor`, qui est un objet qui cont ``` L'extraction de données peut se faire sur deux niveaux. La clé de stockage sera la clé du `dataParser`. + +## Recevoir du FormData +```ts twoslash {6,11} +// @version: 0 + +``` + +Pour recevoir du `FormData`, il vous suffit de configurer un `bodyController` sur votre route. Cela permet d'indiquer à la route qu'elle doit se préparer à recevoir un flux de `FormData` lors d'une requête. Les paramètres définis pour ce `bodyController` s'appliquent pendant le flux du body, contrairement au `dataParser` qui s'applique après la réception du body. + +::: info +Les schémas d'extraction peuvent avoir la même complexité que ceux d'un JSON, car le serializer de `FormData` utilisé permet des niveaux de profondeur supérieurs au `FormData` de base. + +L'extraction de fichiers se fait via le `dataParser` file (`SDPE.file()`) provenant de la librairie `@duplojs/server-utils`. +::: diff --git a/docs/libs/v0/client/getBody.cjs b/docs/libs/v0/client/getBody.cjs index 8dffdfe..14f8475 100644 --- a/docs/libs/v0/client/getBody.cjs +++ b/docs/libs/v0/client/getBody.cjs @@ -15,7 +15,7 @@ function getBody(response) { return response.formData(); } else { - return response.blob(); + return Promise.resolve(undefined); } } diff --git a/docs/libs/v0/client/getBody.mjs b/docs/libs/v0/client/getBody.mjs index cfe4a89..d2e5fbc 100644 --- a/docs/libs/v0/client/getBody.mjs +++ b/docs/libs/v0/client/getBody.mjs @@ -13,7 +13,7 @@ function getBody(response) { return response.formData(); } else { - return response.blob(); + return Promise.resolve(undefined); } } diff --git a/docs/libs/v0/client/hooks.d.ts b/docs/libs/v0/client/hooks.d.ts index d74a213..c01eb77 100644 --- a/docs/libs/v0/client/hooks.d.ts +++ b/docs/libs/v0/client/hooks.d.ts @@ -1,28 +1,4 @@ -import { type MaybePromise } from "@duplojs/utils"; -import { type NotPredictedClientResponse, type ClientResponse } from "./types/clientResponse"; -import { type PromiseRequestParams } from "./promiseRequest"; -export type RequestHook = (requestParams: GenericPromiseRequestParams) => MaybePromise; -export type ResponseHook = (response: ClientResponse) => MaybePromise>; -export type InformationHook = (response: ClientResponse) => MaybePromise; -export type ResponseTypeHook = (response: ClientResponse) => MaybePromise; -export type ExpectedResponseHook = (response: ClientResponse) => MaybePromise; -export type CodeHook = (response: ClientResponse) => MaybePromise; -export type NotPredictedResponseHook = (response: NotPredictedClientResponse) => MaybePromise; -export type ErrorHook = (error: unknown, requestParams: GenericPromiseRequestParams) => MaybePromise; -export interface Hooks { - request: RequestHook[]; - response: ResponseHook[]; - information: Record; - code: Record; - informationalResponseType: ResponseTypeHook[]; - successfulResponseType: ResponseTypeHook[]; - redirectionResponseType: ResponseTypeHook[]; - clientErrorResponseType: ResponseTypeHook[]; - serverErrorResponseType: ResponseTypeHook[]; - expectedResponse: ExpectedResponseHook[]; - notPredictedResponse: NotPredictedResponseHook[]; - error: ErrorHook[]; -} +import { type CodeHook, type ErrorHook, type InformationHook, type NotPredictedResponseHook, type RequestHook, type ResponseHook, type ResponseTypeHook, type PromiseRequestParams, type NotPredictedClientResponse, type ClientResponse } from "./types"; export declare function launchRequestHook(clientHook: readonly RequestHook[], promiseRequestHook: readonly RequestHook[], requestParams: PromiseRequestParams): Promise; export declare function launchResponseHook(clientHook: readonly ResponseHook[], promiseRequestHook: readonly ResponseHook[], response: ClientResponse): Promise; export declare function launchInformationHook(clientHook: readonly InformationHook[], promiseRequestHook: readonly InformationHook[], response: ClientResponse): Promise; diff --git a/docs/libs/v0/client/httpClient.cjs b/docs/libs/v0/client/httpClient.cjs index db9e729..5864b39 100644 --- a/docs/libs/v0/client/httpClient.cjs +++ b/docs/libs/v0/client/httpClient.cjs @@ -140,6 +140,11 @@ function createHttpClient(clientParams) { path, ...params, })), + patch: ((path, params) => self.request({ + method: "PATCH", + path, + ...params, + })), delete: ((path, params) => self.request({ method: "DELETE", path, diff --git a/docs/libs/v0/client/httpClient.d.ts b/docs/libs/v0/client/httpClient.d.ts index 2958523..e18cae9 100644 --- a/docs/libs/v0/client/httpClient.d.ts +++ b/docs/libs/v0/client/httpClient.d.ts @@ -1,17 +1,19 @@ -import { type Kind, type MayBeGetter, type NeverCoalescing, type SimplifyTopLevel } from "@duplojs/utils"; -import { type ClientRequestInitParams, type ServerRoute, type ServerRouteToClientRequestParams, type ServerRouteToClientResponse, type ClientRequestParams } from "./types"; -import { PromiseRequest, type PromiseRequestParams } from "./promiseRequest"; -import { type Hooks, type RequestHook, type ResponseHook, type InformationHook, type CodeHook, type ResponseTypeHook, type ExpectedResponseHook, type ErrorHook, type NotPredictedResponseHook } from "./hooks"; +import { type Kind, type MayBeGetter, type SimplifyTopLevel, type IsEqual } from "@duplojs/utils"; +import { type ClientRequestInitParams, type ServerRoute, type ServerRouteToClientRequestParams, type ServerRouteToClientResponse, type ClientRequestParams, type ClientResponse, type Hooks, type RequestHook, type ResponseHook, type InformationHook, type CodeHook, type ResponseTypeHook, type ExpectedResponseHook, type NotPredictedResponseHook, type ErrorHook, type GetServerRoutePath } from "./types"; +import { PromiseRequest } from "./promiseRequest"; export declare const httpClientKind: import("@duplojs/utils").KindHandler>; type MaybeRequestParams = {} extends GenericRequestParams ? [params?: GenericRequestParams] : [params: GenericRequestParams]; -type HttpClientRequestMethod, GenericMethod extends string> = , GenericMethod extends string> = IsEqual extends true ? (path: string, params?: SimplifyTopLevel, "method" | "path">>) => PromiseRequest> : , GenericHookParams>, ClientRequestParams>, GenericPath extends GenericClientRequestParams["path"], GenericClientRequestRest extends SimplifyTopLevel, ClientRequestParams>, "method" | "path">>>(path: GenericPath, ...args: MaybeRequestParams) => PromiseRequest, ServerRouteToClientResponse["path"], GenericMatchedPath extends GetServerRoutePath, ServerRoute>, GenericHookParams>>; +}>, GenericPath>>(path: GenericPath, ...args: MaybeRequestParams, GenericHookParams>, "method" | "path">>>) => PromiseRequest, GenericHookParams>>; export interface HttpClientConfig { readonly baseUrl: string; readonly informationHeaderKey: string; @@ -26,25 +28,28 @@ export interface HttpClient (string | undefined | null)>; addDefaultHeader(headerName: string, headerValue: MayBeGetter): void; addDefaultHeaders(headers: Record>): void; - addRequestHook(hook: RequestHook>): void; - addResponseHook(hook: ResponseHook>): void; - addInformationHook(information: string, hook: InformationHook>): void; - addCodeHook(code: string, hook: CodeHook>): void; - addInformationalResponseTypeHook(hook: ResponseTypeHook>): void; - addSuccessfulResponseTypeHook(hook: ResponseTypeHook>): void; - addRedirectionResponseTypeHook(hook: ResponseTypeHook>): void; - addClientErrorResponseTypeHook(hook: ResponseTypeHook>): void; - addServerErrorResponseTypeHook(hook: ResponseTypeHook>): void; - addExpectedResponseHook(hook: ExpectedResponseHook>): void; - addNotPredictedResponseHook(hook: NotPredictedResponseHook>): void; - addErrorHook(hook: ErrorHook>): void; - request>(params: GenericClientRequestParams): PromiseRequest, ServerRouteToClientResponse): void; + addResponseHook(hook: ResponseHook): void; + addInformationHook(information: string, hook: InformationHook): void; + addCodeHook(code: string, hook: CodeHook): void; + addInformationalResponseTypeHook(hook: ResponseTypeHook): void; + addSuccessfulResponseTypeHook(hook: ResponseTypeHook): void; + addRedirectionResponseTypeHook(hook: ResponseTypeHook): void; + addClientErrorResponseTypeHook(hook: ResponseTypeHook): void; + addServerErrorResponseTypeHook(hook: ResponseTypeHook): void; + addExpectedResponseHook(hook: ExpectedResponseHook): void; + addNotPredictedResponseHook(hook: NotPredictedResponseHook): void; + addErrorHook(hook: ErrorHook): void; + request, GenericMatchedPath extends GetServerRoutePath, GenericClientRequestParams["path"]>>(params: GenericClientRequestParams): PromiseRequest, GenericHookParams>>; get: HttpClientRequestMethod; post: HttpClientRequestMethod; put: HttpClientRequestMethod; + patch: HttpClientRequestMethod; delete: HttpClientRequestMethod; } export interface CreateHttpClientParams { @@ -56,5 +61,5 @@ export interface CreateHttpClientParams { readonly predictedHeaderKey?: string; readonly disabledPredictedMode?: boolean; } -export declare function createHttpClient = Record>(clientParams: CreateHttpClientParams): HttpClient, GenericHookParams>; +export declare function createHttpClient = Record>(clientParams: CreateHttpClientParams): HttpClient; export {}; diff --git a/docs/libs/v0/client/httpClient.mjs b/docs/libs/v0/client/httpClient.mjs index 170f2a4..78af4ad 100644 --- a/docs/libs/v0/client/httpClient.mjs +++ b/docs/libs/v0/client/httpClient.mjs @@ -118,6 +118,11 @@ function createHttpClient(clientParams) { path, ...params, })), + patch: ((path, params) => self.request({ + method: "PATCH", + path, + ...params, + })), delete: ((path, params) => self.request({ method: "DELETE", path, diff --git a/docs/libs/v0/client/promiseRequest.cjs b/docs/libs/v0/client/promiseRequest.cjs index c9e091c..2b63789 100644 --- a/docs/libs/v0/client/promiseRequest.cjs +++ b/docs/libs/v0/client/promiseRequest.cjs @@ -310,6 +310,9 @@ class PromiseRequest extends Promise { headers["content-type"] = "text/plain; charset=utf-8"; body = body.toString(); } + else if (body instanceof utils.TheFormData) { + headers["content-type-options"] = "advanced"; + } else if ((body && typeof body === "object" && body?.constructor?.name === "Object") diff --git a/docs/libs/v0/client/promiseRequest.d.ts b/docs/libs/v0/client/promiseRequest.d.ts index aa04914..42ae640 100644 --- a/docs/libs/v0/client/promiseRequest.d.ts +++ b/docs/libs/v0/client/promiseRequest.d.ts @@ -1,97 +1,89 @@ import { type NeverCoalescing, type MaybePromise } from "@duplojs/utils"; -import { type Hooks, type ErrorHook, type NotPredictedResponseHook } from "./hooks"; import * as EE from "@duplojs/utils/either"; import { type RequestErrorContent } from "./unexpectedResponseError"; -import { type NotPredictedClientResponse, type ClientRequestParams, type ClientResponse } from "./types"; -type MaybeResponse = (EE.EitherRight<"response", GenericClientResponse> | EE.EitherLeft<"request-error", RequestErrorContent>); -type MaybeWantedResponse = (EE.EitherRight<"response", GenericWantedClientResponse> | EE.EitherLeft<"unexpect-response", GenericUnexpectClientResponse> | EE.EitherLeft<"request-error", RequestErrorContent>); -export interface PromiseRequestParams = Record> extends ClientRequestParams { - baseUrl: string; - hooks: Hooks; - informationHeaderKey: string; - predictedHeaderKey: string; - disabledPredicateMode: boolean; -} -export declare class PromiseRequest extends Promise>> { +import { type NotPredictedClientResponse, type ClientResponse, type PromiseRequestParams, type Hooks, type NotPredictedResponseHook, type ErrorHook } from "./types"; +type MaybeResponse = (EE.Right<"response", GenericClientResponse> | EE.Left<"request-error", RequestErrorContent>); +type MaybeWantedResponse = (EE.Right<"response", GenericWantedClientResponse> | EE.Left<"unexpect-response", GenericUnexpectClientResponse> | EE.Left<"request-error", RequestErrorContent>); +export declare class PromiseRequest = Record, GenericClientResponse extends ClientResponse = ClientResponse> extends Promise>> { params: PromiseRequestParams; readonly hooks: Partial; constructor(params: PromiseRequestParams); - addRequestInterceptor(callback: (requestParams: GenericPromiseRequestParams) => MaybePromise): this; + addRequestInterceptor(callback: (requestParams: GenericClientResponse["requestParams"]) => MaybePromise): this; addResponseInterceptor(callback: (response: GenericClientResponse) => MaybePromise): this; - whenNotPredictedResponse(callback: NotPredictedResponseHook): this; + whenNotPredictedResponse(callback: NotPredictedResponseHook): this; whenInformation>(information: GenericInformation | GenericInformation[], callback: (response: NeverCoalescing, ClientResponse>) => MaybePromise): this; + } : never>, ClientResponse>) => MaybePromise): this; whenCode(code: GenericCode | GenericCode[], callback: (response: NeverCoalescing, ClientResponse>) => MaybePromise): this; + } : never>, ClientResponse>) => MaybePromise): this; whenInformationalResponse(callback: (response: NeverCoalescing, ClientResponse>) => MaybePromise): this; + }>, ClientResponse>) => MaybePromise): this; whenSuccessfulResponse(callback: (response: NeverCoalescing, ClientResponse>) => MaybePromise): this; + }>, ClientResponse>) => MaybePromise): this; whenRedirectionResponse(callback: (response: NeverCoalescing, ClientResponse>) => MaybePromise): this; + }>, ClientResponse>) => MaybePromise): this; whenClientErrorResponse(callback: (response: NeverCoalescing, ClientResponse>) => MaybePromise): this; + }>, ClientResponse>) => MaybePromise): this; whenServerErrorResponse(callback: (response: NeverCoalescing, ClientResponse>) => MaybePromise): this; + }>, ClientResponse>) => MaybePromise): this; whenExpectedResponse(callback: (response: NeverCoalescing, ClientResponse>) => MaybePromise): this; - whenError(callback: ErrorHook): this; + }>, ClientResponse>) => MaybePromise): this; + whenError(callback: ErrorHook): this; iWantInformation, GenericResponse extends NeverCoalescing, ClientResponse>>(information: GenericInformation | GenericInformation[]): Promise, ClientResponse>>>; + } : never>, ClientResponse>>(information: GenericInformation | GenericInformation[]): Promise, ClientResponse>>>; iWantCode, ClientResponse>>(code: GenericCode | GenericCode[]): Promise, ClientResponse>>>; + } : never>, ClientResponse>>(code: GenericCode | GenericCode[]): Promise, ClientResponse>>>; iWantInformationalResponse, ClientResponse>>(): Promise, ClientResponse>>>; + }>, ClientResponse>>(): Promise, ClientResponse>>>; iWantSuccessfulResponse, ClientResponse>>(): Promise, ClientResponse>>>; + }>, ClientResponse>>(): Promise, ClientResponse>>>; iWantRedirectionResponse, ClientResponse>>(): Promise, ClientResponse>>>; + }>, ClientResponse>>(): Promise, ClientResponse>>>; iWantClientErrorResponse, ClientResponse>>(): Promise, ClientResponse>>>; + }>, ClientResponse>>(): Promise, ClientResponse>>>; iWantServerErrorResponse, ClientResponse>>(): Promise, ClientResponse>>>; + }>, ClientResponse>>(): Promise, ClientResponse>>>; iWantExpectedResponse, ClientResponse>>(): Promise, ClientResponse>>>; + }>, ClientResponse>>(): Promise, ClientResponse>>>; iWantInformationOrThrow>(information: GenericInformation | GenericInformation[]): Promise, ClientResponse>>; + } : never>, ClientResponse>>; iWantCodeOrThrow(code: GenericCode | GenericCode[]): Promise, ClientResponse>>; + } : never>, ClientResponse>>; iWantInformationalResponseOrThrow(): Promise, ClientResponse>>; + }>, ClientResponse>>; iWantSuccessfulResponseOrThrow(): Promise, ClientResponse>>; + }>, ClientResponse>>; iWantRedirectionResponseOrThrow(): Promise, ClientResponse>>; + }>, ClientResponse>>; iWantClientErrorResponseOrThrow(): Promise, ClientResponse>>; + }>, ClientResponse>>; iWantServerErrorResponseOrThrow(): Promise, ClientResponse>>; + }>, ClientResponse>>; iWantExpectedResponseOrThrow(): Promise, ClientResponse>>; + }>, ClientResponse>>; static get [Symbol.species](): PromiseConstructor; static fetch(requestParams: GenericPromiseRequestParams): Promise; } diff --git a/docs/libs/v0/client/promiseRequest.mjs b/docs/libs/v0/client/promiseRequest.mjs index 677a93b..34e57cf 100644 --- a/docs/libs/v0/client/promiseRequest.mjs +++ b/docs/libs/v0/client/promiseRequest.mjs @@ -1,4 +1,4 @@ -import { unwrap } from '@duplojs/utils'; +import { unwrap, TheFormData } from '@duplojs/utils'; import { getBody } from './getBody.mjs'; import { insertParamsInPath } from './insertParamsInPath.mjs'; import { queryToString } from './queryToString.mjs'; @@ -287,6 +287,9 @@ class PromiseRequest extends Promise { headers["content-type"] = "text/plain; charset=utf-8"; body = body.toString(); } + else if (body instanceof TheFormData) { + headers["content-type-options"] = "advanced"; + } else if ((body && typeof body === "object" && body?.constructor?.name === "Object") diff --git a/docs/libs/v0/client/queryToString.cjs b/docs/libs/v0/client/queryToString.cjs index 88e06b2..5d833be 100644 --- a/docs/libs/v0/client/queryToString.cjs +++ b/docs/libs/v0/client/queryToString.cjs @@ -9,7 +9,7 @@ function queryToString(query) { if (!value) { return pv; } - if (Array.isArray(value)) { + if (value instanceof Array) { value.forEach((subValue) => { pv.push(`${key}=${subValue}`); }); diff --git a/docs/libs/v0/client/queryToString.mjs b/docs/libs/v0/client/queryToString.mjs index 01dace9..8fa179a 100644 --- a/docs/libs/v0/client/queryToString.mjs +++ b/docs/libs/v0/client/queryToString.mjs @@ -7,7 +7,7 @@ function queryToString(query) { if (!value) { return pv; } - if (Array.isArray(value)) { + if (value instanceof Array) { value.forEach((subValue) => { pv.push(`${key}=${subValue}`); }); diff --git a/docs/libs/v0/client/types/clientResponse.d.ts b/docs/libs/v0/client/types/clientResponse.d.ts index aca5b22..a18c2a4 100644 --- a/docs/libs/v0/client/types/clientResponse.d.ts +++ b/docs/libs/v0/client/types/clientResponse.d.ts @@ -1,9 +1,9 @@ import type * as SS from "@duplojs/utils/string"; import { type ServerRouteResponse, type ServerRoute } from "./serverRoute"; import { type IsEqual, type SimplifyTopLevel } from "@duplojs/utils"; -import { type PromiseRequestParams } from "../promiseRequest"; +import { type PromiseRequestParams } from "./promiseRequestParams"; export type ClientResponseBody = unknown; -export interface ClientResponse { +export interface ClientResponse = Record> { code: SS.Number; information: undefined | string; body: ClientResponseBody; @@ -13,16 +13,16 @@ export interface ClientResponse; predicted: boolean; } -export interface NotPredictedClientResponse extends ClientResponse { +export interface NotPredictedClientResponse = Record> extends ClientResponse { predicted: false; } -export type ServerRouteToClientResponse = Record> = IsEqual extends true ? ClientResponse> : GenericServerRoute extends any ? GenericServerRoute["responses"] extends infer InferredResponse ? InferredResponse extends ServerRouteResponse ? SimplifyTopLevel<{ +export type ServerRouteToClientResponse = Record> = GenericServerRoute extends any ? GenericServerRoute["responses"] extends infer InferredResponse ? InferredResponse extends ServerRouteResponse ? SimplifyTopLevel<{ code: InferredResponse["code"]; information: InferredResponse["information"]; - body: InferredResponse["body"]; + body: IsEqual extends true ? undefined : InferredResponse["body"]; ok: boolean | null; headers: Headers; type: ResponseType; diff --git a/docs/libs/v0/interfaces/node/types/host.cjs b/docs/libs/v0/client/types/hooks.cjs similarity index 100% rename from docs/libs/v0/interfaces/node/types/host.cjs rename to docs/libs/v0/client/types/hooks.cjs diff --git a/docs/libs/v0/client/types/hooks.d.ts b/docs/libs/v0/client/types/hooks.d.ts new file mode 100644 index 0000000..1b53f71 --- /dev/null +++ b/docs/libs/v0/client/types/hooks.d.ts @@ -0,0 +1,25 @@ +import { type MaybePromise } from "@duplojs/utils"; +import { type NotPredictedClientResponse, type ClientResponse } from "./clientResponse"; +import { type PromiseRequestParams } from "./promiseRequestParams"; +export type RequestHook = Record> = (requestParams: PromiseRequestParams) => MaybePromise>; +export type ResponseHook = Record> = (response: ClientResponse) => MaybePromise>; +export type InformationHook = Record> = (response: ClientResponse) => MaybePromise; +export type ResponseTypeHook = Record> = (response: ClientResponse) => MaybePromise; +export type ExpectedResponseHook = Record> = (response: ClientResponse) => MaybePromise; +export type CodeHook = Record> = (response: ClientResponse) => MaybePromise; +export type NotPredictedResponseHook = Record> = (response: NotPredictedClientResponse) => MaybePromise; +export type ErrorHook = Record> = (error: unknown, requestParams: PromiseRequestParams) => MaybePromise; +export interface Hooks { + request: RequestHook[]; + response: ResponseHook[]; + information: Record; + code: Record; + informationalResponseType: ResponseTypeHook[]; + successfulResponseType: ResponseTypeHook[]; + redirectionResponseType: ResponseTypeHook[]; + clientErrorResponseType: ResponseTypeHook[]; + serverErrorResponseType: ResponseTypeHook[]; + expectedResponse: ExpectedResponseHook[]; + notPredictedResponse: NotPredictedResponseHook[]; + error: ErrorHook[]; +} diff --git a/docs/libs/v0/interfaces/node/types/host.mjs b/docs/libs/v0/client/types/hooks.mjs similarity index 100% rename from docs/libs/v0/interfaces/node/types/host.mjs rename to docs/libs/v0/client/types/hooks.mjs diff --git a/docs/libs/v0/client/types/index.cjs b/docs/libs/v0/client/types/index.cjs index 036d4ae..f36a9b1 100644 --- a/docs/libs/v0/client/types/index.cjs +++ b/docs/libs/v0/client/types/index.cjs @@ -4,4 +4,6 @@ require('./clientRequestParams.cjs'); require('./clientResponse.cjs'); require('./serverRoute.cjs'); require('./ObjectCanBeEmpty.cjs'); +require('./promiseRequestParams.cjs'); +require('./hooks.cjs'); diff --git a/docs/libs/v0/client/types/index.d.ts b/docs/libs/v0/client/types/index.d.ts index 872e3d0..1425ca8 100644 --- a/docs/libs/v0/client/types/index.d.ts +++ b/docs/libs/v0/client/types/index.d.ts @@ -2,3 +2,5 @@ export * from "./clientRequestParams"; export * from "./clientResponse"; export * from "./serverRoute"; export * from "./ObjectCanBeEmpty"; +export * from "./promiseRequestParams"; +export * from "./hooks"; diff --git a/docs/libs/v0/client/types/index.mjs b/docs/libs/v0/client/types/index.mjs index e648d9f..0a8ed3b 100644 --- a/docs/libs/v0/client/types/index.mjs +++ b/docs/libs/v0/client/types/index.mjs @@ -2,3 +2,5 @@ import './clientRequestParams.mjs'; import './clientResponse.mjs'; import './serverRoute.mjs'; import './ObjectCanBeEmpty.mjs'; +import './promiseRequestParams.mjs'; +import './hooks.mjs'; diff --git a/docs/libs/v0/client/types/promiseRequestParams.cjs b/docs/libs/v0/client/types/promiseRequestParams.cjs new file mode 100644 index 0000000..eb109ab --- /dev/null +++ b/docs/libs/v0/client/types/promiseRequestParams.cjs @@ -0,0 +1,2 @@ +'use strict'; + diff --git a/docs/libs/v0/client/types/promiseRequestParams.d.ts b/docs/libs/v0/client/types/promiseRequestParams.d.ts new file mode 100644 index 0000000..d64acac --- /dev/null +++ b/docs/libs/v0/client/types/promiseRequestParams.d.ts @@ -0,0 +1,9 @@ +import { type ClientRequestParams } from "./clientRequestParams"; +import { type Hooks } from "./hooks"; +export interface PromiseRequestParams = Record> extends ClientRequestParams { + baseUrl: string; + hooks: Hooks; + informationHeaderKey: string; + predictedHeaderKey: string; + disabledPredicateMode: boolean; +} diff --git a/docs/libs/v0/client/types/promiseRequestParams.mjs b/docs/libs/v0/client/types/promiseRequestParams.mjs new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/docs/libs/v0/client/types/promiseRequestParams.mjs @@ -0,0 +1 @@ + diff --git a/docs/libs/v0/client/types/serverRoute.d.ts b/docs/libs/v0/client/types/serverRoute.d.ts index beac232..df99f23 100644 --- a/docs/libs/v0/client/types/serverRoute.d.ts +++ b/docs/libs/v0/client/types/serverRoute.d.ts @@ -1,4 +1,4 @@ -import { type MaybeArray } from "@duplojs/utils"; +import { type SimplifyTopLevel, type MaybeArray, type IsEqual } from "@duplojs/utils"; import type * as SS from "@duplojs/utils/string"; export type ServerPrimitiveData = string | undefined | number | null | boolean; export type ServerRouteHeaders = Record; @@ -20,3 +20,21 @@ export interface ServerRoute { body?: ServerRouteBody; responses: ServerRouteResponse; } +export type GetServerRoutePath = GenericServerRoute extends ServerRoute ? IsEqual, never> extends true ? never : GenericServerRoute["path"] : never; +export type AddPrefixPathServerRoute = GenericRoute extends ServerRoute ? SimplifyTopLevel<{ + path: `${GenericPrefix}${GenericRoute["path"]}`; +} & Omit> : never; +export type RemovePrefixPathServerRoute = GenericRoute extends ServerRoute ? GenericRoute["path"] extends `${GenericPrefix}${infer InferredPathRest}` ? SimplifyTopLevel<{ + path: InferredPathRest; +} & Omit> : GenericRoute : never; +export type FindServerRoute["path"] = Extract["path"]> = Extract; +export type FindServerRouteResponse = Extract; diff --git a/docs/libs/v0/client/unexpectedResponseError.d.ts b/docs/libs/v0/client/unexpectedResponseError.d.ts index 8e18916..39017e3 100644 --- a/docs/libs/v0/client/unexpectedResponseError.d.ts +++ b/docs/libs/v0/client/unexpectedResponseError.d.ts @@ -1,12 +1,11 @@ -import { type ClientResponse } from "./types"; -import { type PromiseRequestParams } from "./promiseRequest"; +import { type PromiseRequestParams, type ClientResponse } from "./types"; export interface RequestErrorContent { error: unknown; requestParams: PromiseRequestParams; } declare const UnexpectedInformationResponseError_base: new (params: { "@DuplojsHttpClient/unexpected-information-response-error"?: unknown; -}, parentParams: [message?: string | undefined, options?: ErrorOptions | undefined]) => import("@duplojs/utils").Kind, unknown> & import("@duplojs/utils").Kind, unknown> & Error; +}, parentParams: readonly [message?: string | undefined, options?: ErrorOptions | undefined]) => import("@duplojs/utils").Kind, unknown> & import("@duplojs/utils").Kind, unknown> & Error; export declare class UnexpectedInformationResponseError extends UnexpectedInformationResponseError_base { information: string | string[]; response: RequestErrorContent | ClientResponse; @@ -14,7 +13,7 @@ export declare class UnexpectedInformationResponseError extends UnexpectedInform } declare const UnexpectedCodeResponseError_base: new (params: { "@DuplojsHttpClient/unexpected-code-response-error"?: unknown; -}, parentParams: [message?: string | undefined, options?: ErrorOptions | undefined]) => Error & import("@duplojs/utils").Kind, unknown> & import("@duplojs/utils").Kind, unknown>; +}, parentParams: readonly [message?: string | undefined, options?: ErrorOptions | undefined]) => Error & import("@duplojs/utils").Kind, unknown> & import("@duplojs/utils").Kind, unknown>; export declare class UnexpectedCodeResponseError extends UnexpectedCodeResponseError_base { code: string | string[]; response: RequestErrorContent | ClientResponse; @@ -22,7 +21,7 @@ export declare class UnexpectedCodeResponseError extends UnexpectedCodeResponseE } declare const UnexpectedResponseTypeError_base: new (params: { "@DuplojsHttpClient/unexpected-response-type-error"?: unknown; -}, parentParams: [message?: string | undefined, options?: ErrorOptions | undefined]) => Error & import("@duplojs/utils").Kind, unknown> & import("@duplojs/utils").Kind, unknown>; +}, parentParams: readonly [message?: string | undefined, options?: ErrorOptions | undefined]) => Error & import("@duplojs/utils").Kind, unknown> & import("@duplojs/utils").Kind, unknown>; export declare class UnexpectedResponseTypeError extends UnexpectedResponseTypeError_base { expectType: "informational" | "successful" | "redirection" | "clientError" | "serverError"; response: RequestErrorContent | ClientResponse; @@ -30,7 +29,7 @@ export declare class UnexpectedResponseTypeError extends UnexpectedResponseTypeE } declare const UnexpectedResponseError_base: new (params: { "@DuplojsHttpClient/unexpected-response-error"?: unknown; -}, parentParams: [message?: string | undefined, options?: ErrorOptions | undefined]) => Error & import("@duplojs/utils").Kind, unknown> & import("@duplojs/utils").Kind, unknown>; +}, parentParams: readonly [message?: string | undefined, options?: ErrorOptions | undefined]) => Error & import("@duplojs/utils").Kind, unknown> & import("@duplojs/utils").Kind, unknown>; export declare class UnexpectedResponseError extends UnexpectedResponseError_base { response: RequestErrorContent | ClientResponse; constructor(response: RequestErrorContent | ClientResponse); diff --git a/docs/libs/v0/core/builders/preflight/route.cjs b/docs/libs/v0/core/builders/preflight/route.cjs index 643bb63..5c369ab 100644 --- a/docs/libs/v0/core/builders/preflight/route.cjs +++ b/docs/libs/v0/core/builders/preflight/route.cjs @@ -18,4 +18,5 @@ builder.preflightBuilder.set("useRouteBuilder", ({ args: [method, paths, options ...(options?.metadata ?? []), ...accumulator.metadata, ], + bodyController: options?.bodyController ?? null, })); diff --git a/docs/libs/v0/core/builders/preflight/route.d.ts b/docs/libs/v0/core/builders/preflight/route.d.ts index 765cc68..8a78628 100644 --- a/docs/libs/v0/core/builders/preflight/route.d.ts +++ b/docs/libs/v0/core/builders/preflight/route.d.ts @@ -1,27 +1,29 @@ import { type Floor } from "../../floor"; -import { type RequestMethods, type Request } from "../../request"; +import { type RequestMethods, type Request, type BodyController } from "../../request"; import { type MakeRequestFromHooks, type HookRouteLifeCycle, type RoutePath } from "../../route"; import { type RouteBuilder } from "../route"; import { type NeverCoalescing } from "@duplojs/utils"; import { type Metadata } from "../../metadata"; declare module "./builder" { interface PreflightBuilder { - useRouteBuilder(method: GenericMethod, path: GenericPaths, options?: { + useRouteBuilder(method: GenericMethod, path: GenericPaths, options?: { hooks?: GenericHooks | readonly HookRouteLifeCycle[]; metadata?: GenericMetadata; + bodyController?: GenericBodyController; }): RouteBuilder<{ readonly method: GenericMethod; readonly paths: GenericPaths extends string ? readonly [GenericPaths] : GenericPaths; readonly preflightSteps: GenericDefinition["preflightSteps"]; readonly steps: readonly []; readonly hooks: readonly [ - ...GenericDefinition["hooks"], - ...GenericHooks + ...GenericHooks, + ...GenericDefinition["hooks"] ]; readonly metadata: readonly [ - ...GenericDefinition["metadata"], - ...GenericMetadata + ...GenericMetadata, + ...GenericDefinition["metadata"] ]; + readonly bodyController: GenericBodyController; }, GenericFloor, (GenericRequest & NeverCoalescing, Request>)>; } } diff --git a/docs/libs/v0/core/builders/preflight/route.mjs b/docs/libs/v0/core/builders/preflight/route.mjs index 701801f..6c07501 100644 --- a/docs/libs/v0/core/builders/preflight/route.mjs +++ b/docs/libs/v0/core/builders/preflight/route.mjs @@ -16,4 +16,5 @@ preflightBuilder.set("useRouteBuilder", ({ args: [method, paths, options,], accu ...(options?.metadata ?? []), ...accumulator.metadata, ], + bodyController: options?.bodyController ?? null, })); diff --git a/docs/libs/v0/core/builders/route/builder.cjs b/docs/libs/v0/core/builders/route/builder.cjs index c5dd3d3..21f3b0b 100644 --- a/docs/libs/v0/core/builders/route/builder.cjs +++ b/docs/libs/v0/core/builders/route/builder.cjs @@ -12,6 +12,7 @@ function useRouteBuilder(method, path, options) { steps: [], hooks: options?.hooks ?? [], metadata: options?.metadata ?? [], + bodyController: options?.bodyController ?? null, }); } diff --git a/docs/libs/v0/core/builders/route/builder.d.ts b/docs/libs/v0/core/builders/route/builder.d.ts index 046c906..026f151 100644 --- a/docs/libs/v0/core/builders/route/builder.d.ts +++ b/docs/libs/v0/core/builders/route/builder.d.ts @@ -1,14 +1,15 @@ import { type MakeRequestFromHooks, type HookRouteLifeCycle, type RouteDefinition, type RoutePath } from "../../route"; import { type Floor } from "../../floor"; -import { type RequestMethods, type Request } from "../../request"; +import { type RequestMethods, type Request, type BodyController } from "../../request"; import { type Builder, type NeverCoalescing } from "@duplojs/utils"; import { type Metadata } from "../../metadata"; export interface RouteBuilder extends Builder { } export declare const routeBuilderHandler: import("@duplojs/utils").BuilderHandler>; -export declare function useRouteBuilder(method: GenericMethod, path: GenericPaths, options?: { +export declare function useRouteBuilder(method: GenericMethod, path: GenericPaths, options?: { hooks?: GenericHooks | readonly HookRouteLifeCycle[]; metadata?: GenericMetadata; + bodyController?: GenericBodyController; }): RouteBuilder<{ readonly method: GenericMethod; readonly paths: GenericPaths extends string ? readonly [GenericPaths] : GenericPaths; @@ -16,4 +17,5 @@ export declare function useRouteBuilder, Request>>; diff --git a/docs/libs/v0/core/builders/route/builder.mjs b/docs/libs/v0/core/builders/route/builder.mjs index b7016db..8ca5bab 100644 --- a/docs/libs/v0/core/builders/route/builder.mjs +++ b/docs/libs/v0/core/builders/route/builder.mjs @@ -10,6 +10,7 @@ function useRouteBuilder(method, path, options) { steps: [], hooks: options?.hooks ?? [], metadata: options?.metadata ?? [], + bodyController: options?.bodyController ?? null, }); } diff --git a/docs/libs/v0/core/clean/constraint.cjs b/docs/libs/v0/core/clean/constraint.cjs new file mode 100644 index 0000000..1739f93 --- /dev/null +++ b/docs/libs/v0/core/clean/constraint.cjs @@ -0,0 +1,24 @@ +'use strict'; + +var utils = require('@duplojs/utils'); +var clean = require('@duplojs/utils/clean'); + +clean.createConstraint.overrideHandler.setMethod("toExtractParser", (self) => { + const dataParserWithCheckers = self + .primitiveHandler + .dataParser + .addChecker(...self.checkers); + const valueContainer = clean.constrainedTypeKind.setTo({}, { [self.name]: null }); + const dataParser = utils.DPE.transform(dataParserWithCheckers, (input) => ({ + ...valueContainer, + [utils.keyWrappedValue]: input, + })); + return dataParser; +}); +clean.createConstraint.overrideHandler.setMethod("toEndpointSchema", (self) => { + const dataParser = self + .primitiveHandler + .dataParser + .addChecker(...self.checkers); + return utils.DPE.lazy(() => dataParser); +}); diff --git a/docs/libs/v0/core/clean/constraint.d.ts b/docs/libs/v0/core/clean/constraint.d.ts new file mode 100644 index 0000000..050bcf0 --- /dev/null +++ b/docs/libs/v0/core/clean/constraint.d.ts @@ -0,0 +1,8 @@ +import { type DP, DPE } from "@duplojs/utils"; +import { type ConstrainedType, type EligiblePrimitive } from "@duplojs/utils/clean"; +declare module "@duplojs/utils/clean" { + interface ConstraintHandler { + toExtractParser(): DPE.ContractExtended, unknown>; + toEndpointSchema(): DPE.ContractExtended; + } +} diff --git a/docs/libs/v0/core/clean/constraint.mjs b/docs/libs/v0/core/clean/constraint.mjs new file mode 100644 index 0000000..962132c --- /dev/null +++ b/docs/libs/v0/core/clean/constraint.mjs @@ -0,0 +1,22 @@ +import { DPE, keyWrappedValue } from '@duplojs/utils'; +import { createConstraint, constrainedTypeKind } from '@duplojs/utils/clean'; + +createConstraint.overrideHandler.setMethod("toExtractParser", (self) => { + const dataParserWithCheckers = self + .primitiveHandler + .dataParser + .addChecker(...self.checkers); + const valueContainer = constrainedTypeKind.setTo({}, { [self.name]: null }); + const dataParser = DPE.transform(dataParserWithCheckers, (input) => ({ + ...valueContainer, + [keyWrappedValue]: input, + })); + return dataParser; +}); +createConstraint.overrideHandler.setMethod("toEndpointSchema", (self) => { + const dataParser = self + .primitiveHandler + .dataParser + .addChecker(...self.checkers); + return DPE.lazy(() => dataParser); +}); diff --git a/docs/libs/v0/core/clean/constraintsSet.cjs b/docs/libs/v0/core/clean/constraintsSet.cjs new file mode 100644 index 0000000..c9838d4 --- /dev/null +++ b/docs/libs/v0/core/clean/constraintsSet.cjs @@ -0,0 +1,27 @@ +'use strict'; + +var utils = require('@duplojs/utils'); +var clean = require('@duplojs/utils/clean'); + +clean.createConstraintsSet.overrideHandler.setMethod("toExtractParser", (self) => { + const checkers = utils.A.flatMap(self.constraints, ({ checkers }) => checkers); + const dataParserWithCheckers = self + .primitiveHandler + .dataParser + .addChecker(...checkers); + const constraintsKindValue = utils.pipe(self.constraints, utils.A.map(({ name }) => utils.O.entry(name, null)), utils.O.fromEntries); + const valueContainer = clean.constrainedTypeKind.setTo({}, constraintsKindValue); + const dataParser = utils.DPE.transform(dataParserWithCheckers, (input) => ({ + ...valueContainer, + [utils.keyWrappedValue]: input, + })); + return dataParser; +}); +clean.createConstraintsSet.overrideHandler.setMethod("toEndpointSchema", (self) => { + const checkers = utils.A.flatMap(self.constraints, ({ checkers }) => checkers); + const dataParserWithCheckers = self + .primitiveHandler + .dataParser + .addChecker(...checkers); + return utils.DPE.lazy(() => dataParserWithCheckers); +}); diff --git a/docs/libs/v0/core/clean/constraintsSet.d.ts b/docs/libs/v0/core/clean/constraintsSet.d.ts new file mode 100644 index 0000000..6f6aa28 --- /dev/null +++ b/docs/libs/v0/core/clean/constraintsSet.d.ts @@ -0,0 +1,8 @@ +import { DPE, type UnionToIntersection } from "@duplojs/utils"; +import { type EligiblePrimitive, type GetConstraint, type Primitive } from "@duplojs/utils/clean"; +declare module "@duplojs/utils/clean" { + interface ConstraintsSetHandler { + toExtractParser(): DPE.ContractExtended<(Primitive & UnionToIntersection : never : never>), unknown>; + toEndpointSchema(): DPE.ContractExtended; + } +} diff --git a/docs/libs/v0/core/clean/constraintsSet.mjs b/docs/libs/v0/core/clean/constraintsSet.mjs new file mode 100644 index 0000000..dbbe7d4 --- /dev/null +++ b/docs/libs/v0/core/clean/constraintsSet.mjs @@ -0,0 +1,25 @@ +import { A, pipe, O, DPE, keyWrappedValue } from '@duplojs/utils'; +import { createConstraintsSet, constrainedTypeKind } from '@duplojs/utils/clean'; + +createConstraintsSet.overrideHandler.setMethod("toExtractParser", (self) => { + const checkers = A.flatMap(self.constraints, ({ checkers }) => checkers); + const dataParserWithCheckers = self + .primitiveHandler + .dataParser + .addChecker(...checkers); + const constraintsKindValue = pipe(self.constraints, A.map(({ name }) => O.entry(name, null)), O.fromEntries); + const valueContainer = constrainedTypeKind.setTo({}, constraintsKindValue); + const dataParser = DPE.transform(dataParserWithCheckers, (input) => ({ + ...valueContainer, + [keyWrappedValue]: input, + })); + return dataParser; +}); +createConstraintsSet.overrideHandler.setMethod("toEndpointSchema", (self) => { + const checkers = A.flatMap(self.constraints, ({ checkers }) => checkers); + const dataParserWithCheckers = self + .primitiveHandler + .dataParser + .addChecker(...checkers); + return DPE.lazy(() => dataParserWithCheckers); +}); diff --git a/docs/libs/v0/core/clean/entity.cjs b/docs/libs/v0/core/clean/entity.cjs new file mode 100644 index 0000000..cd39edf --- /dev/null +++ b/docs/libs/v0/core/clean/entity.cjs @@ -0,0 +1,33 @@ +'use strict'; + +var utils = require('@duplojs/utils'); +var clean = require('@duplojs/utils/clean'); + +function propertiesDefinitionToSchema(definition, method) { + return utils.pipe(definition, utils.P.when(clean.newTypeHandlerKind.has, (value) => value[method]()), utils.P.when(utils.isType("array"), utils.innerPipe(utils.A.map((element) => element[method]()), (options) => { + utils.asserts(options, utils.A.minElements(1)); + return utils.DP.union(options); + })), utils.P.otherwise((definition) => utils.pipe(definition.type, (subDefinition) => propertiesDefinitionToSchema(subDefinition, method), (dataParser) => { + if (definition.inArray) { + return utils.pipe(dataParser, utils.DP.array, (dataParser) => typeof definition.inArray === "object" + && typeof definition.inArray.min === "number" + ? dataParser.addChecker(utils.DP.checkerArrayMin(definition.inArray.min)) + : dataParser, (dataParser) => typeof definition.inArray === "object" + && typeof definition.inArray.max === "number" + ? dataParser.addChecker(utils.DP.checkerArrayMax(definition.inArray.max)) + : dataParser); + } + return dataParser; + }, (dataParser) => definition.nullable === true + ? utils.DP.nullable(dataParser) + : dataParser))); +} +clean.createEntity.overrideHandler.setMethod("toExtractParser", (self, keys) => utils.pipe(self.propertiesDefinition, utils.O.entries, utils.A.filter(([key]) => keys === undefined || utils.A.includes(keys, key)), utils.A.map(([key, value]) => utils.O.entry(key, propertiesDefinitionToSchema(value, "toExtractParser"))), utils.O.fromEntries, utils.DPE.object)); +clean.createEntity.overrideHandler.setMethod("toEndpointSchema", (self, keys, params) => utils.pipe(self.propertiesDefinition, utils.O.entries, utils.A.filter(([key]) => keys === undefined || utils.A.includes(keys, key)), utils.A.map(([key, value]) => utils.O.entry(key, propertiesDefinitionToSchema(value, "toEndpointSchema"))), utils.O.fromEntries, (shape) => typeof params?.addEntityName !== "undefined" + ? { + ...shape, + _entityName: typeof params.addEntityName === "string" + ? utils.DP.literal(`${self.name}/${params.addEntityName}`) + : utils.DP.literal(self.name), + } + : shape, utils.DPE.object)); diff --git a/docs/libs/v0/core/clean/entity.d.ts b/docs/libs/v0/core/clean/entity.d.ts new file mode 100644 index 0000000..ea11917 --- /dev/null +++ b/docs/libs/v0/core/clean/entity.d.ts @@ -0,0 +1,20 @@ +import { DP, DPE, type IsEqual, type SimplifyTopLevel, type IsExtends } from "@duplojs/utils"; +import { type EntityRawProperties, type EntityProperties, type EntityPropertiesDefinition } from "@duplojs/utils/clean"; +interface ToEndpointSchemaParams { + addEntityName?: boolean | string; +} +declare module "@duplojs/utils/clean" { + interface EntityHandler { + toExtractParser, const GenericKey extends keyof GenericEntityProperties = keyof GenericEntityProperties>(keys?: GenericKey[]): ReturnType; + }>>; + toEndpointSchema, const GenericKey extends keyof GenericEntityRawProperties = keyof GenericEntityRawProperties, const GenericParams extends ToEndpointSchemaParams = {}>(keys?: GenericKey[], params?: GenericParams | ToEndpointSchemaParams): ReturnType; + } & (IsEqual extends true ? { + [Prop in "_entityName"]: DP.Contract; + } : {}) & (IsExtends extends true ? { + [Prop in "_entityName"]: DP.Contract<`${GenericName}/${GenericParams["addEntityName"]}`, unknown>; + } : {})>>>; + } +} +export {}; diff --git a/docs/libs/v0/core/clean/entity.mjs b/docs/libs/v0/core/clean/entity.mjs new file mode 100644 index 0000000..124466f --- /dev/null +++ b/docs/libs/v0/core/clean/entity.mjs @@ -0,0 +1,31 @@ +import { pipe, P, isType, innerPipe, A, asserts, DP, O, DPE } from '@duplojs/utils'; +import { newTypeHandlerKind, createEntity } from '@duplojs/utils/clean'; + +function propertiesDefinitionToSchema(definition, method) { + return pipe(definition, P.when(newTypeHandlerKind.has, (value) => value[method]()), P.when(isType("array"), innerPipe(A.map((element) => element[method]()), (options) => { + asserts(options, A.minElements(1)); + return DP.union(options); + })), P.otherwise((definition) => pipe(definition.type, (subDefinition) => propertiesDefinitionToSchema(subDefinition, method), (dataParser) => { + if (definition.inArray) { + return pipe(dataParser, DP.array, (dataParser) => typeof definition.inArray === "object" + && typeof definition.inArray.min === "number" + ? dataParser.addChecker(DP.checkerArrayMin(definition.inArray.min)) + : dataParser, (dataParser) => typeof definition.inArray === "object" + && typeof definition.inArray.max === "number" + ? dataParser.addChecker(DP.checkerArrayMax(definition.inArray.max)) + : dataParser); + } + return dataParser; + }, (dataParser) => definition.nullable === true + ? DP.nullable(dataParser) + : dataParser))); +} +createEntity.overrideHandler.setMethod("toExtractParser", (self, keys) => pipe(self.propertiesDefinition, O.entries, A.filter(([key]) => keys === undefined || A.includes(keys, key)), A.map(([key, value]) => O.entry(key, propertiesDefinitionToSchema(value, "toExtractParser"))), O.fromEntries, DPE.object)); +createEntity.overrideHandler.setMethod("toEndpointSchema", (self, keys, params) => pipe(self.propertiesDefinition, O.entries, A.filter(([key]) => keys === undefined || A.includes(keys, key)), A.map(([key, value]) => O.entry(key, propertiesDefinitionToSchema(value, "toEndpointSchema"))), O.fromEntries, (shape) => typeof params?.addEntityName !== "undefined" + ? { + ...shape, + _entityName: typeof params.addEntityName === "string" + ? DP.literal(`${self.name}/${params.addEntityName}`) + : DP.literal(self.name), + } + : shape, DPE.object)); diff --git a/docs/libs/v0/core/clean/index.cjs b/docs/libs/v0/core/clean/index.cjs new file mode 100644 index 0000000..405b0c4 --- /dev/null +++ b/docs/libs/v0/core/clean/index.cjs @@ -0,0 +1,8 @@ +'use strict'; + +require('./newType.cjs'); +require('./constraint.cjs'); +require('./constraintsSet.cjs'); +require('./primitive.cjs'); +require('./entity.cjs'); + diff --git a/docs/libs/v0/core/clean/index.d.ts b/docs/libs/v0/core/clean/index.d.ts new file mode 100644 index 0000000..a77d185 --- /dev/null +++ b/docs/libs/v0/core/clean/index.d.ts @@ -0,0 +1,5 @@ +export * from "./newType"; +export * from "./constraint"; +export * from "./constraintsSet"; +export * from "./primitive"; +export * from "./entity"; diff --git a/docs/libs/v0/core/clean/index.mjs b/docs/libs/v0/core/clean/index.mjs new file mode 100644 index 0000000..e3ed0a5 --- /dev/null +++ b/docs/libs/v0/core/clean/index.mjs @@ -0,0 +1,5 @@ +import './newType.mjs'; +import './constraint.mjs'; +import './constraintsSet.mjs'; +import './primitive.mjs'; +import './entity.mjs'; diff --git a/docs/libs/v0/core/clean/newType.cjs b/docs/libs/v0/core/clean/newType.cjs new file mode 100644 index 0000000..135c2b6 --- /dev/null +++ b/docs/libs/v0/core/clean/newType.cjs @@ -0,0 +1,15 @@ +'use strict'; + +var utils = require('@duplojs/utils'); +var clean = require('@duplojs/utils/clean'); + +clean.createNewType.overrideHandler.setMethod("toExtractParser", (self) => { + const constraintsKindValue = utils.pipe(self.constraints, utils.A.map(({ name }) => utils.O.entry(name, null)), utils.O.fromEntries); + const valueContainer = clean.newTypeKind.setTo(clean.constrainedTypeKind.setTo({}, constraintsKindValue), self.name); + const dataParser = utils.DPE.transform(self.dataParser, (input) => ({ + ...valueContainer, + [utils.keyWrappedValue]: input, + })); + return dataParser; +}); +clean.createNewType.overrideHandler.setMethod("toEndpointSchema", (self) => utils.DPE.lazy(() => self.dataParser)); diff --git a/docs/libs/v0/core/clean/newType.d.ts b/docs/libs/v0/core/clean/newType.d.ts new file mode 100644 index 0000000..24a94a8 --- /dev/null +++ b/docs/libs/v0/core/clean/newType.d.ts @@ -0,0 +1,8 @@ +import { DPE } from "@duplojs/utils"; +import { type NewType } from "@duplojs/utils/clean"; +declare module "@duplojs/utils/clean" { + interface NewTypeHandler { + toExtractParser(): DPE.ContractExtended, unknown>; + toEndpointSchema(): DPE.ContractExtended; + } +} diff --git a/docs/libs/v0/core/clean/newType.mjs b/docs/libs/v0/core/clean/newType.mjs new file mode 100644 index 0000000..30c3a55 --- /dev/null +++ b/docs/libs/v0/core/clean/newType.mjs @@ -0,0 +1,13 @@ +import { pipe, A, O, DPE, keyWrappedValue } from '@duplojs/utils'; +import { createNewType, newTypeKind, constrainedTypeKind } from '@duplojs/utils/clean'; + +createNewType.overrideHandler.setMethod("toExtractParser", (self) => { + const constraintsKindValue = pipe(self.constraints, A.map(({ name }) => O.entry(name, null)), O.fromEntries); + const valueContainer = newTypeKind.setTo(constrainedTypeKind.setTo({}, constraintsKindValue), self.name); + const dataParser = DPE.transform(self.dataParser, (input) => ({ + ...valueContainer, + [keyWrappedValue]: input, + })); + return dataParser; +}); +createNewType.overrideHandler.setMethod("toEndpointSchema", (self) => DPE.lazy(() => self.dataParser)); diff --git a/docs/libs/v0/core/clean/primitive.cjs b/docs/libs/v0/core/clean/primitive.cjs new file mode 100644 index 0000000..595d4ea --- /dev/null +++ b/docs/libs/v0/core/clean/primitive.cjs @@ -0,0 +1,12 @@ +'use strict'; + +var utils = require('@duplojs/utils'); +var clean = require('@duplojs/utils/clean'); + +clean.createPrimitive.overrideHandler.setMethod("toExtractParser", (self) => { + const dataParser = utils.DPE.transform(self.dataParser, (input) => ({ + [utils.keyWrappedValue]: input, + })); + return dataParser; +}); +clean.createPrimitive.overrideHandler.setMethod("toEndpointSchema", (self) => utils.DPE.lazy(() => self.dataParser)); diff --git a/docs/libs/v0/core/clean/primitive.d.ts b/docs/libs/v0/core/clean/primitive.d.ts new file mode 100644 index 0000000..2514b88 --- /dev/null +++ b/docs/libs/v0/core/clean/primitive.d.ts @@ -0,0 +1,8 @@ +import { DPE } from "@duplojs/utils"; +import { type EligiblePrimitive, type Primitive } from "@duplojs/utils/clean"; +declare module "@duplojs/utils/clean" { + interface PrimitiveHandler { + toExtractParser(): DPE.ContractExtended, unknown>; + toEndpointSchema(): DPE.ContractExtended; + } +} diff --git a/docs/libs/v0/core/clean/primitive.mjs b/docs/libs/v0/core/clean/primitive.mjs new file mode 100644 index 0000000..2e668c7 --- /dev/null +++ b/docs/libs/v0/core/clean/primitive.mjs @@ -0,0 +1,10 @@ +import { DPE, keyWrappedValue } from '@duplojs/utils'; +import { createPrimitive } from '@duplojs/utils/clean'; + +createPrimitive.overrideHandler.setMethod("toExtractParser", (self) => { + const dataParser = DPE.transform(self.dataParser, (input) => ({ + [keyWrappedValue]: input, + })); + return dataParser; +}); +createPrimitive.overrideHandler.setMethod("toEndpointSchema", (self) => DPE.lazy(() => self.dataParser)); diff --git a/docs/libs/v0/core/defaultHooks/index.cjs b/docs/libs/v0/core/defaultHooks/index.cjs new file mode 100644 index 0000000..7b4286d --- /dev/null +++ b/docs/libs/v0/core/defaultHooks/index.cjs @@ -0,0 +1,50 @@ +'use strict'; + +require('../response/index.cjs'); +require('../route/index.cjs'); +var serverUtils = require('@duplojs/server-utils'); +var hooks = require('../route/hooks.cjs'); +var predicted = require('../response/predicted.cjs'); +var hook = require('../response/hook.cjs'); + +function initDefaultHook(hub, serverParams) { + const informationHeaderKey = serverParams.informationHeaderKey; + const predictedHeaderKey = serverParams.predictedHeaderKey; + const fromHookHeaderKey = serverParams.fromHookHeaderKey; + const isDev = hub.config.environment === "DEV"; + return hooks.createHookRouteLifeCycle({ + beforeSendResponse({ currentResponse, next }) { + if (!currentResponse.headers?.["content-type"]) { + const body = currentResponse.body; + if (typeof body === "string" + || body instanceof Error) { + currentResponse.setHeader("content-type", "text/plain; charset=utf-8"); + } + else if (serverUtils.SF.isFileInterface(body)) { + const filename = body.getName(); + const filenameHeader = filename + ? ` filename="${filename}"` + : ""; + currentResponse + .setHeader("content-type", body.getMimeType() ?? "application/octet-stream") + .setHeader("content-disposition", `attachment;${filenameHeader}`); + } + else if (typeof body === "object" + || typeof body === "number" + || typeof body === "boolean") { + currentResponse.setHeader("content-type", "application/json; charset=utf-8"); + } + } + currentResponse.setHeader(informationHeaderKey, currentResponse.information); + if (currentResponse instanceof predicted.PredictedResponse) { + currentResponse.setHeader(predictedHeaderKey, "1"); + } + else if (currentResponse instanceof hook.HookResponse) { + currentResponse.setHeader(fromHookHeaderKey, currentResponse.fromHook); + } + return next(); + }, + }); +} + +exports.initDefaultHook = initDefaultHook; diff --git a/docs/libs/v0/core/defaultHooks/index.d.ts b/docs/libs/v0/core/defaultHooks/index.d.ts new file mode 100644 index 0000000..ecff70b --- /dev/null +++ b/docs/libs/v0/core/defaultHooks/index.d.ts @@ -0,0 +1,5 @@ +import { type Hub } from "../hub"; +import { type HttpServerParams } from "../types"; +export declare function initDefaultHook(hub: Hub, serverParams: HttpServerParams): { + beforeSendResponse({ currentResponse, next }: import("../route").RouteHookParamsAfter): import("../route").RouteHookNext; +}; diff --git a/docs/libs/v0/core/defaultHooks/index.mjs b/docs/libs/v0/core/defaultHooks/index.mjs new file mode 100644 index 0000000..3e7287f --- /dev/null +++ b/docs/libs/v0/core/defaultHooks/index.mjs @@ -0,0 +1,48 @@ +import '../response/index.mjs'; +import '../route/index.mjs'; +import { SF } from '@duplojs/server-utils'; +import { createHookRouteLifeCycle } from '../route/hooks.mjs'; +import { PredictedResponse } from '../response/predicted.mjs'; +import { HookResponse } from '../response/hook.mjs'; + +function initDefaultHook(hub, serverParams) { + const informationHeaderKey = serverParams.informationHeaderKey; + const predictedHeaderKey = serverParams.predictedHeaderKey; + const fromHookHeaderKey = serverParams.fromHookHeaderKey; + const isDev = hub.config.environment === "DEV"; + return createHookRouteLifeCycle({ + beforeSendResponse({ currentResponse, next }) { + if (!currentResponse.headers?.["content-type"]) { + const body = currentResponse.body; + if (typeof body === "string" + || body instanceof Error) { + currentResponse.setHeader("content-type", "text/plain; charset=utf-8"); + } + else if (SF.isFileInterface(body)) { + const filename = body.getName(); + const filenameHeader = filename + ? ` filename="${filename}"` + : ""; + currentResponse + .setHeader("content-type", body.getMimeType() ?? "application/octet-stream") + .setHeader("content-disposition", `attachment;${filenameHeader}`); + } + else if (typeof body === "object" + || typeof body === "number" + || typeof body === "boolean") { + currentResponse.setHeader("content-type", "application/json; charset=utf-8"); + } + } + currentResponse.setHeader(informationHeaderKey, currentResponse.information); + if (currentResponse instanceof PredictedResponse) { + currentResponse.setHeader(predictedHeaderKey, "1"); + } + else if (currentResponse instanceof HookResponse) { + currentResponse.setHeader(fromHookHeaderKey, currentResponse.fromHook); + } + return next(); + }, + }); +} + +export { initDefaultHook }; diff --git a/docs/libs/v0/interfaces/node/error/bodyParseWrongChunkReceived.cjs b/docs/libs/v0/core/errors/bodyParseWrongChunkReceived.cjs similarity index 52% rename from docs/libs/v0/interfaces/node/error/bodyParseWrongChunkReceived.cjs rename to docs/libs/v0/core/errors/bodyParseWrongChunkReceived.cjs index 6ea3059..43c163a 100644 --- a/docs/libs/v0/interfaces/node/error/bodyParseWrongChunkReceived.cjs +++ b/docs/libs/v0/core/errors/bodyParseWrongChunkReceived.cjs @@ -1,12 +1,14 @@ 'use strict'; -var utils = require('@duplojs/utils'); var kind = require('../kind.cjs'); +var utils = require('@duplojs/utils'); -class BodyParseWrongChunkReceived extends utils.kindHeritage("body-parse-wrong-chunk-received", kind.createInterfacesNodeLibKind("body-parse-wrong-chunk-received"), Error) { +class BodyParseWrongChunkReceived extends utils.kindHeritage("body-parse-wrong-chunk-received", kind.createCoreLibKind("body-parse-wrong-chunk-received"), Error) { + information; wrongChunk; - constructor(wrongChunk) { - super({}, ["Received chunk is not buffer or string."]); + constructor(information, wrongChunk) { + super({}, [`Received chunk is not ${information}`]); + this.information = information; this.wrongChunk = wrongChunk; } } diff --git a/docs/libs/v0/core/errors/bodyParseWrongChunkReceived.d.ts b/docs/libs/v0/core/errors/bodyParseWrongChunkReceived.d.ts new file mode 100644 index 0000000..5d9de72 --- /dev/null +++ b/docs/libs/v0/core/errors/bodyParseWrongChunkReceived.d.ts @@ -0,0 +1,9 @@ +declare const BodyParseWrongChunkReceived_base: new (params: { + "@DuplojsHttpCore/body-parse-wrong-chunk-received"?: unknown; +}, parentParams: readonly [message?: string | undefined, options?: ErrorOptions | undefined]) => Error & import("@duplojs/utils").Kind, unknown> & import("@duplojs/utils").Kind, unknown>; +export declare class BodyParseWrongChunkReceived extends BodyParseWrongChunkReceived_base { + information: string; + wrongChunk: unknown; + constructor(information: string, wrongChunk: unknown); +} +export {}; diff --git a/docs/libs/v0/core/errors/bodyParseWrongChunkReceived.mjs b/docs/libs/v0/core/errors/bodyParseWrongChunkReceived.mjs new file mode 100644 index 0000000..c404d67 --- /dev/null +++ b/docs/libs/v0/core/errors/bodyParseWrongChunkReceived.mjs @@ -0,0 +1,14 @@ +import { createCoreLibKind } from '../kind.mjs'; +import { kindHeritage } from '@duplojs/utils'; + +class BodyParseWrongChunkReceived extends kindHeritage("body-parse-wrong-chunk-received", createCoreLibKind("body-parse-wrong-chunk-received"), Error) { + information; + wrongChunk; + constructor(information, wrongChunk) { + super({}, [`Received chunk is not ${information}`]); + this.information = information; + this.wrongChunk = wrongChunk; + } +} + +export { BodyParseWrongChunkReceived }; diff --git a/docs/libs/v0/interfaces/node/error/bodySizeExceedsLimitError.cjs b/docs/libs/v0/core/errors/bodySizeExceedsLimitError.cjs similarity index 78% rename from docs/libs/v0/interfaces/node/error/bodySizeExceedsLimitError.cjs rename to docs/libs/v0/core/errors/bodySizeExceedsLimitError.cjs index f913096..d4eaa6f 100644 --- a/docs/libs/v0/interfaces/node/error/bodySizeExceedsLimitError.cjs +++ b/docs/libs/v0/core/errors/bodySizeExceedsLimitError.cjs @@ -1,9 +1,9 @@ 'use strict'; -var utils = require('@duplojs/utils'); var kind = require('../kind.cjs'); +var utils = require('@duplojs/utils'); -class BodySizeExceedsLimitError extends utils.kindHeritage("body-size-exceeds-limit-error", kind.createInterfacesNodeLibKind("body-size-exceeds-limit-error"), Error) { +class BodySizeExceedsLimitError extends utils.kindHeritage("body-size-exceeds-limit-error", kind.createCoreLibKind("body-size-exceeds-limit-error"), Error) { bytesInString; constructor(bytesInString) { super({}, [`Body size is bigger than ${bytesInString}.`]); diff --git a/docs/libs/v0/core/errors/bodySizeExceedsLimitError.d.ts b/docs/libs/v0/core/errors/bodySizeExceedsLimitError.d.ts new file mode 100644 index 0000000..83314df --- /dev/null +++ b/docs/libs/v0/core/errors/bodySizeExceedsLimitError.d.ts @@ -0,0 +1,9 @@ +import { type BytesInString } from "@duplojs/utils"; +declare const BodySizeExceedsLimitError_base: new (params: { + "@DuplojsHttpCore/body-size-exceeds-limit-error"?: unknown; +}, parentParams: readonly [message?: string | undefined, options?: ErrorOptions | undefined]) => Error & import("@duplojs/utils").Kind, unknown> & import("@duplojs/utils").Kind, unknown>; +export declare class BodySizeExceedsLimitError extends BodySizeExceedsLimitError_base { + bytesInString: BytesInString | number; + constructor(bytesInString: BytesInString | number); +} +export {}; diff --git a/docs/libs/v0/interfaces/node/error/bodySizeExceedsLimitError.mjs b/docs/libs/v0/core/errors/bodySizeExceedsLimitError.mjs similarity index 67% rename from docs/libs/v0/interfaces/node/error/bodySizeExceedsLimitError.mjs rename to docs/libs/v0/core/errors/bodySizeExceedsLimitError.mjs index 444fd50..2c2caa2 100644 --- a/docs/libs/v0/interfaces/node/error/bodySizeExceedsLimitError.mjs +++ b/docs/libs/v0/core/errors/bodySizeExceedsLimitError.mjs @@ -1,7 +1,7 @@ +import { createCoreLibKind } from '../kind.mjs'; import { kindHeritage } from '@duplojs/utils'; -import { createInterfacesNodeLibKind } from '../kind.mjs'; -class BodySizeExceedsLimitError extends kindHeritage("body-size-exceeds-limit-error", createInterfacesNodeLibKind("body-size-exceeds-limit-error"), Error) { +class BodySizeExceedsLimitError extends kindHeritage("body-size-exceeds-limit-error", createCoreLibKind("body-size-exceeds-limit-error"), Error) { bytesInString; constructor(bytesInString) { super({}, [`Body size is bigger than ${bytesInString}.`]); diff --git a/docs/libs/v0/interfaces/node/error/index.cjs b/docs/libs/v0/core/errors/index.cjs similarity index 59% rename from docs/libs/v0/interfaces/node/error/index.cjs rename to docs/libs/v0/core/errors/index.cjs index 86763e1..bc1609c 100644 --- a/docs/libs/v0/interfaces/node/error/index.cjs +++ b/docs/libs/v0/core/errors/index.cjs @@ -1,11 +1,13 @@ 'use strict'; -var bodySizeExceedsLimitError = require('./bodySizeExceedsLimitError.cjs'); +var wrongContentTypeError = require('./wrongContentTypeError.cjs'); var bodyParseWrongChunkReceived = require('./bodyParseWrongChunkReceived.cjs'); -var bodyParseUnknownError = require('./bodyParseUnknownError.cjs'); +var bodySizeExceedsLimitError = require('./bodySizeExceedsLimitError.cjs'); +var parseJsonError = require('./parseJsonError.cjs'); -exports.BodySizeExceedsLimitError = bodySizeExceedsLimitError.BodySizeExceedsLimitError; +exports.WrongContentTypeError = wrongContentTypeError.WrongContentTypeError; exports.BodyParseWrongChunkReceived = bodyParseWrongChunkReceived.BodyParseWrongChunkReceived; -exports.BodyParseUnknownError = bodyParseUnknownError.BodyParseUnknownError; +exports.BodySizeExceedsLimitError = bodySizeExceedsLimitError.BodySizeExceedsLimitError; +exports.ParseJsonError = parseJsonError.ParseJsonError; diff --git a/docs/libs/v0/interfaces/node/error/index.d.ts b/docs/libs/v0/core/errors/index.d.ts similarity index 55% rename from docs/libs/v0/interfaces/node/error/index.d.ts rename to docs/libs/v0/core/errors/index.d.ts index 7d1788a..29760df 100644 --- a/docs/libs/v0/interfaces/node/error/index.d.ts +++ b/docs/libs/v0/core/errors/index.d.ts @@ -1,3 +1,4 @@ -export * from "./bodySizeExceedsLimitError"; +export * from "./wrongContentTypeError"; export * from "./bodyParseWrongChunkReceived"; -export * from "./bodyParseUnknownError"; +export * from "./bodySizeExceedsLimitError"; +export * from "./parseJsonError"; diff --git a/docs/libs/v0/interfaces/node/error/index.mjs b/docs/libs/v0/core/errors/index.mjs similarity index 57% rename from docs/libs/v0/interfaces/node/error/index.mjs rename to docs/libs/v0/core/errors/index.mjs index 467b86e..7e22180 100644 --- a/docs/libs/v0/interfaces/node/error/index.mjs +++ b/docs/libs/v0/core/errors/index.mjs @@ -1,3 +1,4 @@ -export { BodySizeExceedsLimitError } from './bodySizeExceedsLimitError.mjs'; +export { WrongContentTypeError } from './wrongContentTypeError.mjs'; export { BodyParseWrongChunkReceived } from './bodyParseWrongChunkReceived.mjs'; -export { BodyParseUnknownError } from './bodyParseUnknownError.mjs'; +export { BodySizeExceedsLimitError } from './bodySizeExceedsLimitError.mjs'; +export { ParseJsonError } from './parseJsonError.mjs'; diff --git a/docs/libs/v0/core/errors/parseJsonError.cjs b/docs/libs/v0/core/errors/parseJsonError.cjs new file mode 100644 index 0000000..1b8f345 --- /dev/null +++ b/docs/libs/v0/core/errors/parseJsonError.cjs @@ -0,0 +1,16 @@ +'use strict'; + +var kind = require('../kind.cjs'); +var utils = require('@duplojs/utils'); + +class ParseJsonError extends utils.kindHeritage("parse-json-error", kind.createCoreLibKind("parse-json-error"), Error) { + payload; + error; + constructor(payload, error) { + super({}, ["Error when parse on json."]); + this.payload = payload; + this.error = error; + } +} + +exports.ParseJsonError = ParseJsonError; diff --git a/docs/libs/v0/core/errors/parseJsonError.d.ts b/docs/libs/v0/core/errors/parseJsonError.d.ts new file mode 100644 index 0000000..df23baf --- /dev/null +++ b/docs/libs/v0/core/errors/parseJsonError.d.ts @@ -0,0 +1,9 @@ +declare const ParseJsonError_base: new (params: { + "@DuplojsHttpCore/parse-json-error"?: unknown; +}, parentParams: readonly [message?: string | undefined, options?: ErrorOptions | undefined]) => Error & import("@duplojs/utils").Kind, unknown> & import("@duplojs/utils").Kind, unknown>; +export declare class ParseJsonError extends ParseJsonError_base { + payload: string; + error: unknown; + constructor(payload: string, error: unknown); +} +export {}; diff --git a/docs/libs/v0/core/errors/parseJsonError.mjs b/docs/libs/v0/core/errors/parseJsonError.mjs new file mode 100644 index 0000000..eb5fc70 --- /dev/null +++ b/docs/libs/v0/core/errors/parseJsonError.mjs @@ -0,0 +1,14 @@ +import { createCoreLibKind } from '../kind.mjs'; +import { kindHeritage } from '@duplojs/utils'; + +class ParseJsonError extends kindHeritage("parse-json-error", createCoreLibKind("parse-json-error"), Error) { + payload; + error; + constructor(payload, error) { + super({}, ["Error when parse on json."]); + this.payload = payload; + this.error = error; + } +} + +export { ParseJsonError }; diff --git a/docs/libs/v0/core/errors/wrongContentTypeError.cjs b/docs/libs/v0/core/errors/wrongContentTypeError.cjs new file mode 100644 index 0000000..fe9a6a3 --- /dev/null +++ b/docs/libs/v0/core/errors/wrongContentTypeError.cjs @@ -0,0 +1,16 @@ +'use strict'; + +var kind = require('../kind.cjs'); +var utils = require('@duplojs/utils'); + +class WrongContentTypeError extends utils.kindHeritage("wrong-content-type-error", kind.createCoreLibKind("wrong-content-type-error"), Error) { + expectedContentType; + contentType; + constructor(expectedContentType, contentType) { + super({}, [`expect content-type "${expectedContentType}" but receive "${contentType}".`]); + this.expectedContentType = expectedContentType; + this.contentType = contentType; + } +} + +exports.WrongContentTypeError = WrongContentTypeError; diff --git a/docs/libs/v0/core/errors/wrongContentTypeError.d.ts b/docs/libs/v0/core/errors/wrongContentTypeError.d.ts new file mode 100644 index 0000000..4c7475f --- /dev/null +++ b/docs/libs/v0/core/errors/wrongContentTypeError.d.ts @@ -0,0 +1,9 @@ +declare const WrongContentTypeError_base: new (params: { + "@DuplojsHttpCore/wrong-content-type-error"?: unknown; +}, parentParams: readonly [message?: string | undefined, options?: ErrorOptions | undefined]) => Error & import("@duplojs/utils").Kind, unknown> & import("@duplojs/utils").Kind, unknown>; +export declare class WrongContentTypeError extends WrongContentTypeError_base { + expectedContentType: string; + contentType: string; + constructor(expectedContentType: string, contentType: string); +} +export {}; diff --git a/docs/libs/v0/core/errors/wrongContentTypeError.mjs b/docs/libs/v0/core/errors/wrongContentTypeError.mjs new file mode 100644 index 0000000..b390454 --- /dev/null +++ b/docs/libs/v0/core/errors/wrongContentTypeError.mjs @@ -0,0 +1,14 @@ +import { createCoreLibKind } from '../kind.mjs'; +import { kindHeritage } from '@duplojs/utils'; + +class WrongContentTypeError extends kindHeritage("wrong-content-type-error", createCoreLibKind("wrong-content-type-error"), Error) { + expectedContentType; + contentType; + constructor(expectedContentType, contentType) { + super({}, [`expect content-type "${expectedContentType}" but receive "${contentType}".`]); + this.expectedContentType = expectedContentType; + this.contentType = contentType; + } +} + +export { WrongContentTypeError }; diff --git a/docs/libs/v0/core/functionsBuilders/route/create.d.ts b/docs/libs/v0/core/functionsBuilders/route/create.d.ts index a40b949..9b33af8 100644 --- a/docs/libs/v0/core/functionsBuilders/route/create.d.ts +++ b/docs/libs/v0/core/functionsBuilders/route/create.d.ts @@ -6,8 +6,8 @@ import { type BuildStepSuccessEither, type BuildStepNotSupportEither } from "../ import { type Steps } from "../../steps"; import { type ResponseContract } from "../../response"; export type BuildedRouteFunction = (request: Request) => Promise; -export type BuildRouteSuccessEither = E.EitherRight<"buildSuccess", BuildedRouteFunction>; -export type BuildRouteNotSupportEither = E.EitherLeft<"routeNotSupport", Route>; +export type BuildRouteSuccessEither = E.Right<"buildSuccess", BuildedRouteFunction>; +export type BuildRouteNotSupportEither = E.Left<"routeNotSupport", Route>; export interface RouteFunctionBuilderParams { readonly globalHooksRouteLifeCycle: readonly HookRouteLifeCycle[]; readonly environment: Environment; diff --git a/docs/libs/v0/core/functionsBuilders/route/default.cjs b/docs/libs/v0/core/functionsBuilders/route/default.cjs index 0e89829..e1b95e4 100644 --- a/docs/libs/v0/core/functionsBuilders/route/default.cjs +++ b/docs/libs/v0/core/functionsBuilders/route/default.cjs @@ -33,7 +33,6 @@ const defaultRouteFunctionBuilder = create.createRouteFunctionBuilder(index.rout const hookBeforeRouteExecution = utils.pipe(allHooks, utils.A.map(({ beforeRouteExecution }) => beforeRouteExecution), utils.A.filter(utils.isType("function")), utils.forward); const hookBeforeSendResponse = utils.pipe(allHooks, utils.A.map(({ beforeSendResponse }) => beforeSendResponse), utils.A.filter(utils.isType("function")), utils.forward); const hookOnConstructRequest = utils.pipe(allHooks, utils.A.map(({ onConstructRequest }) => onConstructRequest), utils.A.filter(utils.isType("function")), utils.forward); - const hookParseBody = utils.pipe(allHooks, utils.A.map(({ parseBody }) => parseBody), utils.A.filter(utils.isType("function")), utils.forward); const hookError = utils.pipe(allHooks, utils.A.map(({ error }) => error), utils.A.filter(utils.isType("function")), utils.forward); const hookSendResponse = utils.pipe(allHooks, utils.A.map(({ sendResponse }) => sendResponse), utils.A.filter(utils.isType("function")), utils.forward); const hooks = { @@ -52,7 +51,6 @@ const defaultRouteFunctionBuilder = create.createRouteFunctionBuilder(index.rout return newRequest; } : (params) => params.request, - parseBody: hook.buildHookBefore(hookParseBody), error: hook.buildHookErrorBefore(hookError), sendResponse: hook.buildHookAfter(hookSendResponse), }; @@ -75,15 +73,6 @@ const defaultRouteFunctionBuilder = create.createRouteFunctionBuilder(index.rout } floor = result; } - const parseBodyResult = await hooks.parseBody({ - request, - exit: hook.exitHookFunction, - next: hook.nextHookFunction, - response: hook.createHookResponse("parseBody"), - }); - if (parseBodyResult instanceof base.Response) { - return parseBodyResult; - } for (let index = 0; index < buildedSteps.length; index++) { const result = await buildedSteps[index].buildedFunction(request, floor); if (result instanceof base.Response) { diff --git a/docs/libs/v0/core/functionsBuilders/route/default.mjs b/docs/libs/v0/core/functionsBuilders/route/default.mjs index d75ee3b..d11273b 100644 --- a/docs/libs/v0/core/functionsBuilders/route/default.mjs +++ b/docs/libs/v0/core/functionsBuilders/route/default.mjs @@ -31,7 +31,6 @@ const defaultRouteFunctionBuilder = createRouteFunctionBuilder(routeKind.has, as const hookBeforeRouteExecution = pipe(allHooks, A.map(({ beforeRouteExecution }) => beforeRouteExecution), A.filter(isType("function")), forward); const hookBeforeSendResponse = pipe(allHooks, A.map(({ beforeSendResponse }) => beforeSendResponse), A.filter(isType("function")), forward); const hookOnConstructRequest = pipe(allHooks, A.map(({ onConstructRequest }) => onConstructRequest), A.filter(isType("function")), forward); - const hookParseBody = pipe(allHooks, A.map(({ parseBody }) => parseBody), A.filter(isType("function")), forward); const hookError = pipe(allHooks, A.map(({ error }) => error), A.filter(isType("function")), forward); const hookSendResponse = pipe(allHooks, A.map(({ sendResponse }) => sendResponse), A.filter(isType("function")), forward); const hooks = { @@ -50,7 +49,6 @@ const defaultRouteFunctionBuilder = createRouteFunctionBuilder(routeKind.has, as return newRequest; } : (params) => params.request, - parseBody: buildHookBefore(hookParseBody), error: buildHookErrorBefore(hookError), sendResponse: buildHookAfter(hookSendResponse), }; @@ -73,15 +71,6 @@ const defaultRouteFunctionBuilder = createRouteFunctionBuilder(routeKind.has, as } floor = result; } - const parseBodyResult = await hooks.parseBody({ - request, - exit: exitHookFunction, - next: nextHookFunction, - response: createHookResponse("parseBody"), - }); - if (parseBodyResult instanceof Response) { - return parseBodyResult; - } for (let index = 0; index < buildedSteps.length; index++) { const result = await buildedSteps[index].buildedFunction(request, floor); if (result instanceof Response) { diff --git a/docs/libs/v0/core/functionsBuilders/route/hook.d.ts b/docs/libs/v0/core/functionsBuilders/route/hook.d.ts index e390be2..9d4c81a 100644 --- a/docs/libs/v0/core/functionsBuilders/route/hook.d.ts +++ b/docs/libs/v0/core/functionsBuilders/route/hook.d.ts @@ -1,8 +1,8 @@ import { HookResponse } from "../../response"; -import { type HookAfterSendResponse, type HookBeforeRouteExecution, type HookBeforeSendResponse, type HookError, type HookParseBody, type HookRouteLifeCycle, type HookSendResponse, type RouteHookErrorParams, type RouteHookParams, type RouteHookParamsAfter } from "../../route"; +import { type HookAfterSendResponse, type HookBeforeRouteExecution, type HookBeforeSendResponse, type HookError, type HookRouteLifeCycle, type HookSendResponse, type RouteHookErrorParams, type RouteHookParams, type RouteHookParamsAfter } from "../../route"; export declare function exitHookFunction(): import("@duplojs/utils").Kind, unknown>; export declare function nextHookFunction(): import("@duplojs/utils").Kind, unknown>; -export declare function buildHookBefore(hooks: (HookBeforeRouteExecution | HookParseBody)[]): typeof exitHookFunction | ((params: RouteHookParams) => Promise, unknown> | HookResponse>); +export declare function buildHookBefore(hooks: (HookBeforeRouteExecution)[]): typeof exitHookFunction | ((params: RouteHookParams) => Promise, unknown> | HookResponse>); export declare function buildHookErrorBefore(hooks: HookError[]): typeof exitHookFunction | ((params: RouteHookErrorParams) => Promise, unknown> | HookResponse>); export declare function buildHookAfter(hooks: (HookBeforeSendResponse | HookSendResponse | HookAfterSendResponse)[]): typeof exitHookFunction | ((params: RouteHookParamsAfter) => Promise, unknown>>); export declare function createHookResponse(from: keyof HookRouteLifeCycle): RouteHookParams["response"]; diff --git a/docs/libs/v0/core/functionsBuilders/steps/create.d.ts b/docs/libs/v0/core/functionsBuilders/steps/create.d.ts index f7f2d7d..743ed44 100644 --- a/docs/libs/v0/core/functionsBuilders/steps/create.d.ts +++ b/docs/libs/v0/core/functionsBuilders/steps/create.d.ts @@ -10,8 +10,8 @@ export interface BuildStepResult { readonly buildedFunction: BuildedStepFunction; readonly hooksRouteLifeCycle: readonly HookRouteLifeCycle[]; } -export type BuildStepSuccessEither = E.EitherRight<"buildSuccess", BuildStepResult>; -export type BuildStepNotSupportEither = E.EitherLeft<"stepNotSupport", Steps>; +export type BuildStepSuccessEither = E.Right<"buildSuccess", BuildStepResult>; +export type BuildStepNotSupportEither = E.Left<"stepNotSupport", Steps>; export interface StepFunctionBuilderParams { buildStep(element: Steps): Promise; success(result: BuildStepResult): BuildStepSuccessEither; diff --git a/docs/libs/v0/core/functionsBuilders/steps/defaults/cutStep.cjs b/docs/libs/v0/core/functionsBuilders/steps/defaults/cutStep.cjs index 505f629..ef530d4 100644 --- a/docs/libs/v0/core/functionsBuilders/steps/defaults/cutStep.cjs +++ b/docs/libs/v0/core/functionsBuilders/steps/defaults/cutStep.cjs @@ -19,32 +19,29 @@ const defaultCutStepFunctionBuilder = create.createStepFunctionBuilder(cut.cutSt if (!currentContract) { throw new contract.ResponseContract.Error(information); } - const result = currentContract.body.parse(body); - if (utils.E.isLeft(result)) { - throw new contract.ResponseContract.Error(information, utils.unwrap(result)); - } return new predicted.PredictedResponse(currentContract.code, currentContract.information, body); }; - function treatResult(result, floor) { - if (cut.cutStepOutputKind.has(result)) { - return { - ...floor, - ...utils.unwrap(result), - }; - } - return result; - } return success({ - buildedFunction: (request, floor) => { - const result = cutFunction(floor, { + buildedFunction: async (request, floor) => { + const cutResult = await cutFunction(floor, { request, output, response, }); - if (result instanceof Promise) { - return result.then((awaitedResult) => treatResult(awaitedResult, floor)); + if (cutResult instanceof predicted.PredictedResponse) { + const currentContract = preparedContractResponse[cutResult.information]; + const resultBody = currentContract.body.isAsynchronous() + ? await currentContract.body.asyncParse(cutResult.body) + : currentContract.body.parse(cutResult.body); + if (utils.E.isLeft(resultBody)) { + throw new contract.ResponseContract.Error(cutResult.information, utils.unwrap(resultBody)); + } + return cutResult; } - return treatResult(result, floor); + return { + ...floor, + ...utils.unwrap(cutResult), + }; }, hooksRouteLifeCycle: [], }); diff --git a/docs/libs/v0/core/functionsBuilders/steps/defaults/cutStep.mjs b/docs/libs/v0/core/functionsBuilders/steps/defaults/cutStep.mjs index 909ff14..f0f088f 100644 --- a/docs/libs/v0/core/functionsBuilders/steps/defaults/cutStep.mjs +++ b/docs/libs/v0/core/functionsBuilders/steps/defaults/cutStep.mjs @@ -17,32 +17,29 @@ const defaultCutStepFunctionBuilder = createStepFunctionBuilder(cutStepKind.has, if (!currentContract) { throw new ResponseContract.Error(information); } - const result = currentContract.body.parse(body); - if (E.isLeft(result)) { - throw new ResponseContract.Error(information, unwrap(result)); - } return new PredictedResponse(currentContract.code, currentContract.information, body); }; - function treatResult(result, floor) { - if (cutStepOutputKind.has(result)) { - return { - ...floor, - ...unwrap(result), - }; - } - return result; - } return success({ - buildedFunction: (request, floor) => { - const result = cutFunction(floor, { + buildedFunction: async (request, floor) => { + const cutResult = await cutFunction(floor, { request, output, response, }); - if (result instanceof Promise) { - return result.then((awaitedResult) => treatResult(awaitedResult, floor)); + if (cutResult instanceof PredictedResponse) { + const currentContract = preparedContractResponse[cutResult.information]; + const resultBody = currentContract.body.isAsynchronous() + ? await currentContract.body.asyncParse(cutResult.body) + : currentContract.body.parse(cutResult.body); + if (E.isLeft(resultBody)) { + throw new ResponseContract.Error(cutResult.information, unwrap(resultBody)); + } + return cutResult; } - return treatResult(result, floor); + return { + ...floor, + ...unwrap(cutResult), + }; }, hooksRouteLifeCycle: [], }); diff --git a/docs/libs/v0/core/functionsBuilders/steps/defaults/extractStep.cjs b/docs/libs/v0/core/functionsBuilders/steps/defaults/extractStep.cjs index b278b0e..d0a432f 100644 --- a/docs/libs/v0/core/functionsBuilders/steps/defaults/extractStep.cjs +++ b/docs/libs/v0/core/functionsBuilders/steps/defaults/extractStep.cjs @@ -10,32 +10,56 @@ var predicted = require('../../../response/predicted.cjs'); const defaultExtractStepFunctionBuilder = create.createStepFunctionBuilder(extract.extractStepKind.has, (step, { success, environment, defaultExtractContract }) => { const { shape, responseContract: stepResponseContract, } = step.definition; const responseContract = stepResponseContract ?? defaultExtractContract; - function getResponse(result, key, subKey) { - const response = new predicted.PredictedResponse(responseContract.code, responseContract.information, environment === "DEV" - ? utils.unwrap(result) - : undefined); - return subKey === undefined - ? response.setHeader("extract-key", `request.${key}`) - : response.setHeader("extract-key", `request.${key}.${subKey}`); - } - function treatResult(result, floor, key, subKey) { - if (utils.E.isLeft(result)) { - return getResponse(result, key, subKey); + function createExtractor(parser, key, subKey) { + const createResponse = environment === "DEV" + ? (result) => new predicted.PredictedResponse(responseContract.code, responseContract.information, result) + : () => new predicted.PredictedResponse(responseContract.code, responseContract.information, undefined); + const setHeader = subKey === undefined || key === "body" + ? (response) => response.setHeader("extract-key", `request.${key}`) + : (response) => response.setHeader("extract-key", `request.${key}.${subKey}`); + const getResponse = (result) => setHeader(createResponse(result)); + const treatResult = (result, floor) => utils.E.isLeft(result) + ? getResponse(utils.unwrap(result)) + : { + ...floor, + [subKey ?? key]: utils.unwrap(result), + }; + const getValue = typeof subKey === "string" + ? (value) => value?.[subKey] + : utils.forward; + if (key === "body") { + const parseFunction = parser.isAsynchronous() + ? parser.asyncParse + : parser.parse; + return async (request, floor) => { + const bodyResult = await request.getBody(); + if (utils.E.isLeft(bodyResult)) { + return treatResult(bodyResult, floor); + } + const result = await parseFunction(getValue(utils.unwrap(bodyResult))); + return treatResult(result, floor); + }; + } + if (parser.isAsynchronous()) { + const parseFunction = parser.asyncParse; + return async (request, floor) => { + const result = await parseFunction(getValue(request[key])); + return treatResult(result, floor); + }; } - return { - ...floor, - [subKey ?? key]: utils.unwrap(result), + const parseFunction = parser.parse; + return (request, floor) => { + const result = parseFunction(getValue(request[key])); + return treatResult(result, floor); }; } - const extractors = utils.A.reduce(utils.O.entries(shape), utils.A.reduceFrom([]), ({ lastValue, element: [key, value], next }) => next(utils.DP.dataParserKind.has(value) - ? utils.A.push(lastValue, (request, floor) => treatResult(value.parse(request[key]), floor, key)) - : utils.pipe(value, utils.P.when(utils.isType("undefined"), utils.justReturn(lastValue)), utils.P.otherwise(utils.innerPipe(utils.O.entries, utils.A.map(([subKey, subValue]) => ((request, floor) => treatResult(subValue.parse(request[key]?.[subKey]), floor, key, subKey))), (subExtractor) => utils.A.concat(lastValue, subExtractor)))))); + const extractors = utils.A.reduce(utils.O.entries(shape), utils.A.reduceFrom([]), ({ lastValue, element: [key, value], next, }) => utils.pipe(value, utils.P.when(utils.DP.dataParserKind.has, (value) => utils.A.push(lastValue, createExtractor(value, key, undefined))), utils.P.otherwise((value) => utils.pipe(value, utils.P.when(utils.isType("undefined"), utils.justReturn(lastValue)), utils.P.otherwise(utils.innerPipe(utils.O.entries, utils.A.map(([subKey, subValue]) => createExtractor(subValue, key, subKey)), (subExtractor) => utils.A.concat(lastValue, subExtractor))))), next)); return success({ - buildedFunction: (request, floor) => { + buildedFunction: async (request, floor) => { let newFloor = floor; // eslint-disable-next-line @typescript-eslint/prefer-for-of for (let index = 0; index < extractors.length; index++) { - const result = extractors[index](request, newFloor); + const result = await extractors[index](request, newFloor); if (result instanceof predicted.PredictedResponse) { return result; } diff --git a/docs/libs/v0/core/functionsBuilders/steps/defaults/extractStep.d.ts b/docs/libs/v0/core/functionsBuilders/steps/defaults/extractStep.d.ts index 156fef6..aa2c734 100644 --- a/docs/libs/v0/core/functionsBuilders/steps/defaults/extractStep.d.ts +++ b/docs/libs/v0/core/functionsBuilders/steps/defaults/extractStep.d.ts @@ -1 +1,2 @@ -export declare const defaultExtractStepFunctionBuilder: (step: import("../../../steps").Steps, params: import("..").StepFunctionBuilderParams) => import("@duplojs/utils").MaybePromise; +import { type MaybePromise } from "@duplojs/utils"; +export declare const defaultExtractStepFunctionBuilder: (step: import("../../../steps").Steps, params: import("..").StepFunctionBuilderParams) => MaybePromise; diff --git a/docs/libs/v0/core/functionsBuilders/steps/defaults/extractStep.mjs b/docs/libs/v0/core/functionsBuilders/steps/defaults/extractStep.mjs index 4480cca..5515cc9 100644 --- a/docs/libs/v0/core/functionsBuilders/steps/defaults/extractStep.mjs +++ b/docs/libs/v0/core/functionsBuilders/steps/defaults/extractStep.mjs @@ -1,5 +1,5 @@ import '../../../steps/index.mjs'; -import { unwrap, E, A, O, DP, pipe, P, isType, justReturn, innerPipe } from '@duplojs/utils'; +import { unwrap, E, forward, A, O, pipe, P, DP, isType, justReturn, innerPipe } from '@duplojs/utils'; import '../../../response/index.mjs'; import { createStepFunctionBuilder } from '../create.mjs'; import { extractStepKind } from '../../../steps/extract.mjs'; @@ -8,32 +8,56 @@ import { PredictedResponse } from '../../../response/predicted.mjs'; const defaultExtractStepFunctionBuilder = createStepFunctionBuilder(extractStepKind.has, (step, { success, environment, defaultExtractContract }) => { const { shape, responseContract: stepResponseContract, } = step.definition; const responseContract = stepResponseContract ?? defaultExtractContract; - function getResponse(result, key, subKey) { - const response = new PredictedResponse(responseContract.code, responseContract.information, environment === "DEV" - ? unwrap(result) - : undefined); - return subKey === undefined - ? response.setHeader("extract-key", `request.${key}`) - : response.setHeader("extract-key", `request.${key}.${subKey}`); - } - function treatResult(result, floor, key, subKey) { - if (E.isLeft(result)) { - return getResponse(result, key, subKey); + function createExtractor(parser, key, subKey) { + const createResponse = environment === "DEV" + ? (result) => new PredictedResponse(responseContract.code, responseContract.information, result) + : () => new PredictedResponse(responseContract.code, responseContract.information, undefined); + const setHeader = subKey === undefined || key === "body" + ? (response) => response.setHeader("extract-key", `request.${key}`) + : (response) => response.setHeader("extract-key", `request.${key}.${subKey}`); + const getResponse = (result) => setHeader(createResponse(result)); + const treatResult = (result, floor) => E.isLeft(result) + ? getResponse(unwrap(result)) + : { + ...floor, + [subKey ?? key]: unwrap(result), + }; + const getValue = typeof subKey === "string" + ? (value) => value?.[subKey] + : forward; + if (key === "body") { + const parseFunction = parser.isAsynchronous() + ? parser.asyncParse + : parser.parse; + return async (request, floor) => { + const bodyResult = await request.getBody(); + if (E.isLeft(bodyResult)) { + return treatResult(bodyResult, floor); + } + const result = await parseFunction(getValue(unwrap(bodyResult))); + return treatResult(result, floor); + }; + } + if (parser.isAsynchronous()) { + const parseFunction = parser.asyncParse; + return async (request, floor) => { + const result = await parseFunction(getValue(request[key])); + return treatResult(result, floor); + }; } - return { - ...floor, - [subKey ?? key]: unwrap(result), + const parseFunction = parser.parse; + return (request, floor) => { + const result = parseFunction(getValue(request[key])); + return treatResult(result, floor); }; } - const extractors = A.reduce(O.entries(shape), A.reduceFrom([]), ({ lastValue, element: [key, value], next }) => next(DP.dataParserKind.has(value) - ? A.push(lastValue, (request, floor) => treatResult(value.parse(request[key]), floor, key)) - : pipe(value, P.when(isType("undefined"), justReturn(lastValue)), P.otherwise(innerPipe(O.entries, A.map(([subKey, subValue]) => ((request, floor) => treatResult(subValue.parse(request[key]?.[subKey]), floor, key, subKey))), (subExtractor) => A.concat(lastValue, subExtractor)))))); + const extractors = A.reduce(O.entries(shape), A.reduceFrom([]), ({ lastValue, element: [key, value], next, }) => pipe(value, P.when(DP.dataParserKind.has, (value) => A.push(lastValue, createExtractor(value, key, undefined))), P.otherwise((value) => pipe(value, P.when(isType("undefined"), justReturn(lastValue)), P.otherwise(innerPipe(O.entries, A.map(([subKey, subValue]) => createExtractor(subValue, key, subKey)), (subExtractor) => A.concat(lastValue, subExtractor))))), next)); return success({ - buildedFunction: (request, floor) => { + buildedFunction: async (request, floor) => { let newFloor = floor; // eslint-disable-next-line @typescript-eslint/prefer-for-of for (let index = 0; index < extractors.length; index++) { - const result = extractors[index](request, newFloor); + const result = await extractors[index](request, newFloor); if (result instanceof PredictedResponse) { return result; } diff --git a/docs/libs/v0/core/functionsBuilders/steps/defaults/handlerStep.cjs b/docs/libs/v0/core/functionsBuilders/steps/defaults/handlerStep.cjs index 444015a..3d24304 100644 --- a/docs/libs/v0/core/functionsBuilders/steps/defaults/handlerStep.cjs +++ b/docs/libs/v0/core/functionsBuilders/steps/defaults/handlerStep.cjs @@ -18,17 +18,23 @@ const defaultHandlerStepFunctionBuilder = create.createStepFunctionBuilder(handl if (!currentContract) { throw new contract.ResponseContract.Error(information); } - const result = currentContract.body.parse(body); - if (utils.E.isLeft(result)) { - throw new contract.ResponseContract.Error(information, utils.unwrap(result)); - } return new predicted.PredictedResponse(currentContract.code, currentContract.information, body); }; return success({ - buildedFunction: (request, floor) => handlerFunction(floor, { - request, - response, - }), + buildedFunction: async (request, floor) => { + const predictedResponse = await handlerFunction(floor, { + request, + response, + }); + const currentContract = preparedContractResponse[predictedResponse.information]; + const result = currentContract.body.isAsynchronous() + ? await currentContract.body.asyncParse(predictedResponse.body) + : currentContract.body.parse(predictedResponse.body); + if (utils.E.isLeft(result)) { + throw new contract.ResponseContract.Error(predictedResponse.information, utils.unwrap(result)); + } + return predictedResponse; + }, hooksRouteLifeCycle: [], }); }); diff --git a/docs/libs/v0/core/functionsBuilders/steps/defaults/handlerStep.mjs b/docs/libs/v0/core/functionsBuilders/steps/defaults/handlerStep.mjs index 9d4c9b1..2799a1f 100644 --- a/docs/libs/v0/core/functionsBuilders/steps/defaults/handlerStep.mjs +++ b/docs/libs/v0/core/functionsBuilders/steps/defaults/handlerStep.mjs @@ -16,17 +16,23 @@ const defaultHandlerStepFunctionBuilder = createStepFunctionBuilder(handlerStepK if (!currentContract) { throw new ResponseContract.Error(information); } - const result = currentContract.body.parse(body); - if (E.isLeft(result)) { - throw new ResponseContract.Error(information, unwrap(result)); - } return new PredictedResponse(currentContract.code, currentContract.information, body); }; return success({ - buildedFunction: (request, floor) => handlerFunction(floor, { - request, - response, - }), + buildedFunction: async (request, floor) => { + const predictedResponse = await handlerFunction(floor, { + request, + response, + }); + const currentContract = preparedContractResponse[predictedResponse.information]; + const result = currentContract.body.isAsynchronous() + ? await currentContract.body.asyncParse(predictedResponse.body) + : currentContract.body.parse(predictedResponse.body); + if (E.isLeft(result)) { + throw new ResponseContract.Error(predictedResponse.information, unwrap(result)); + } + return predictedResponse; + }, hooksRouteLifeCycle: [], }); }); diff --git a/docs/libs/v0/core/hub/defaultBodyController.cjs b/docs/libs/v0/core/hub/defaultBodyController.cjs new file mode 100644 index 0000000..0250e1c --- /dev/null +++ b/docs/libs/v0/core/hub/defaultBodyController.cjs @@ -0,0 +1,8 @@ +'use strict'; + +require('../request/index.cjs'); +var text = require('../request/bodyController/text.cjs'); + +const defaultBodyController = text.controlBodyAsText(); + +exports.defaultBodyController = defaultBodyController; diff --git a/docs/libs/v0/core/hub/defaultBodyController.d.ts b/docs/libs/v0/core/hub/defaultBodyController.d.ts new file mode 100644 index 0000000..f1b51b8 --- /dev/null +++ b/docs/libs/v0/core/hub/defaultBodyController.d.ts @@ -0,0 +1 @@ +export declare const defaultBodyController: import("../request").BodyController<"text", import("../request").TextBodyReaderParams>; diff --git a/docs/libs/v0/core/hub/defaultBodyController.mjs b/docs/libs/v0/core/hub/defaultBodyController.mjs new file mode 100644 index 0000000..e106ade --- /dev/null +++ b/docs/libs/v0/core/hub/defaultBodyController.mjs @@ -0,0 +1,6 @@ +import '../request/index.mjs'; +import { controlBodyAsText } from '../request/bodyController/text.mjs'; + +const defaultBodyController = controlBodyAsText(); + +export { defaultBodyController }; diff --git a/docs/libs/v0/core/hub/hooks.cjs b/docs/libs/v0/core/hub/hooks.cjs index 776bed3..b145982 100644 --- a/docs/libs/v0/core/hub/hooks.cjs +++ b/docs/libs/v0/core/hub/hooks.cjs @@ -9,7 +9,9 @@ async function launchHookBeforeBuildRoute(hooks, route) { return utils.G.asyncReduce(hooks, utils.G.reduceFrom(route), async ({ element: hook, lastValue, next, }) => next(await hook(lastValue))); } async function launchHookServer(hooks, hub, httpServerParams) { - return utils.G.asyncReduce(hooks, utils.G.reduceFrom(hub), async ({ element: hook, lastValue, next, }) => next((await hook(lastValue, httpServerParams)) ?? lastValue)); + for (const hook of hooks) { + await hook(hub, httpServerParams); + } } const hookExit = hookServerExitKind.setTo({}); const hookNext = hookServerNextKind.setTo({}); diff --git a/docs/libs/v0/core/hub/hooks.d.ts b/docs/libs/v0/core/hub/hooks.d.ts index c6779f5..72d8808 100644 --- a/docs/libs/v0/core/hub/hooks.d.ts +++ b/docs/libs/v0/core/hub/hooks.d.ts @@ -2,6 +2,7 @@ import { type Route } from "../route"; import { type EscapeVoid, type Kind, type MaybePromise } from "@duplojs/utils"; import { type Hub } from "."; import { type RouterInitializationData } from "../router"; +import { type HttpServerParams } from "../types"; export declare const hookServerExitKind: import("@duplojs/utils").KindHandler>; export interface ServerHookExit extends Kind { } @@ -10,12 +11,10 @@ export interface ServerHookNext extends Kind MaybePromise; export declare function launchHookBeforeBuildRoute(hooks: Iterable, route: Route): Promise>; -export interface HttpServerParams { -} export type HookBeforeServerBuildRoutes = (hub: Hub, httpServerParams: HttpServerParams) => MaybePromise; export type HookBeforeStartServer = (hub: Hub, httpServerParams: HttpServerParams) => MaybePromise; export type HookAfterStartServer = (hub: Hub, httpServerParams: HttpServerParams) => MaybePromise; -export declare function launchHookServer(hooks: Iterable, hub: Hub, httpServerParams: HttpServerParams): Promise>; +export declare function launchHookServer(hooks: Iterable, hub: Hub, httpServerParams: HttpServerParams): Promise; export interface HttpServerErrorParams { readonly error: unknown; next(): ServerHookNext; diff --git a/docs/libs/v0/core/hub/hooks.mjs b/docs/libs/v0/core/hub/hooks.mjs index c6692ea..b4430e1 100644 --- a/docs/libs/v0/core/hub/hooks.mjs +++ b/docs/libs/v0/core/hub/hooks.mjs @@ -7,7 +7,9 @@ async function launchHookBeforeBuildRoute(hooks, route) { return G.asyncReduce(hooks, G.reduceFrom(route), async ({ element: hook, lastValue, next, }) => next(await hook(lastValue))); } async function launchHookServer(hooks, hub, httpServerParams) { - return G.asyncReduce(hooks, G.reduceFrom(hub), async ({ element: hook, lastValue, next, }) => next((await hook(lastValue, httpServerParams)) ?? lastValue)); + for (const hook of hooks) { + await hook(hub, httpServerParams); + } } const hookExit = hookServerExitKind.setTo({}); const hookNext = hookServerNextKind.setTo({}); diff --git a/docs/libs/v0/core/hub/index.cjs b/docs/libs/v0/core/hub/index.cjs index 115f7f9..2adc7a6 100644 --- a/docs/libs/v0/core/hub/index.cjs +++ b/docs/libs/v0/core/hub/index.cjs @@ -1,146 +1,119 @@ 'use strict'; var kind = require('../kind.cjs'); -var index = require('../route/index.cjs'); +var index$1 = require('../route/index.cjs'); var utils = require('@duplojs/utils'); require('../steps/index.cjs'); -var request = require('../request.cjs'); +var index = require('../request/index.cjs'); var defaultNotfoundHandler = require('./defaultNotfoundHandler.cjs'); var defaultExtractContract = require('./defaultExtractContract.cjs'); +var defaultBodyController = require('./defaultBodyController.cjs'); var hooks = require('./hooks.cjs'); var handler = require('../steps/handler.cjs'); const hubKind = kind.createCoreLibKind("hub"); +class Hub extends utils.kindHeritage("hub", kind.createCoreLibKind("hub")) { + config; + plugins = []; + hooksRouteLifeCycle = []; + hooksHubLifeCycle = []; + routes = new Set(); + routeFunctionBuilders = []; + stepFunctionBuilders = []; + bodyReaderImplementations = []; + classRequest = index.Request; + notfoundHandler = defaultNotfoundHandler.defaultNotfoundHandler; + defaultExtractContract = defaultExtractContract.defaultExtractContract; + defaultBodyController = defaultBodyController.defaultBodyController; + constructor(config) { + super({}); + this.config = config; + } + register(routes) { + utils.pipe(routes, utils.P.when(index$1.routeKind.has, utils.A.coalescing), utils.P.when(utils.isType("iterable"), utils.A.from), utils.P.otherwise(utils.O.values), utils.A.map((route) => this.routes.add(route))); + return this; + } + addRouteFunctionBuilder(functionBuilder) { + this.routeFunctionBuilders.push(...utils.A.coalescing(functionBuilder)); + return this; + } + addStepFunctionBuilder(functionBuilder) { + this.stepFunctionBuilders.push(...utils.A.coalescing(functionBuilder)); + return this; + } + addRouteHooks(hook) { + this.hooksRouteLifeCycle.push(...utils.A.coalescing(hook)); + return this; + } + addHubHooks(hook) { + this.hooksHubLifeCycle.push(...utils.A.coalescing(hook)); + return this; + } + addBodyReaderImplementation(bodyReaderImplementation) { + this.bodyReaderImplementations.push(...utils.A.coalescing(bodyReaderImplementation)); + return this; + } + plug(plugin) { + const pluginResult = typeof plugin === "function" + ? plugin(this) + : plugin; + if (pluginResult.bodyReaderImplementations) { + this.addBodyReaderImplementation(pluginResult.bodyReaderImplementations); + } + if (pluginResult.hooksHubLifeCycle) { + this.addHubHooks(pluginResult.hooksHubLifeCycle); + } + if (pluginResult.hooksRouteLifeCycle) { + this.addRouteHooks(pluginResult.hooksRouteLifeCycle); + } + if (pluginResult.routeFunctionBuilders) { + this.addRouteFunctionBuilder(pluginResult.routeFunctionBuilders); + } + if (pluginResult.routes) { + this.register(pluginResult.routes); + } + if (pluginResult.stepFunctionBuilders) { + this.addStepFunctionBuilder(pluginResult.stepFunctionBuilders); + } + this.plugins.push(pluginResult); + return this; + } + setNotfoundHandler(responseContract, theFunction) { + this.notfoundHandler = handler.createHandlerStep({ + responseContract, + theFunction: (floor, params) => theFunction(params), + metadata: [], + }); + return this; + } + setDefaultExtractContract(responseContract) { + this.defaultExtractContract = responseContract; + return this; + } + aggregatesHooksHubLifeCycle(hookName) { + return utils.A.flatMap(this.hooksHubLifeCycle, (hooks) => hooks[hookName] ?? []); + } + setDefaultBodyController(bodyController) { + this.defaultBodyController = bodyController; + return this; + } + aggregatesHooksRouteLifeCycle(hookName) { + return utils.A.flatMap(this.hooksRouteLifeCycle, (hooks) => hooks[hookName] ?? []); + } + /** + * @internal + */ + static "new"(config) { + return new Hub(config); + } +} function createHub(config) { - return { - ...hubKind.addTo({}), - config, - plugins: [], - hooksHubLifeCycle: [], - hooksRouteLifeCycle: [], - routeFunctionBuilders: [], - routes: [], - stepFunctionBuilders: [], - notfoundHandler: defaultNotfoundHandler.defaultNotfoundHandler, - defaultExtractContract: defaultExtractContract.defaultExtractContract, - classRequest: request.Request, - addHubHooks(hook) { - return { - ...this, - hooksHubLifeCycle: utils.A.concat(this.hooksHubLifeCycle, utils.A.coalescing(hook)), - }; - }, - addRouteFunctionBuilder(functionBuilder) { - return { - ...this, - routeFunctionBuilders: utils.A.concat(this.routeFunctionBuilders, utils.A.coalescing(functionBuilder)), - }; - }, - addRouteHooks(hook) { - return { - ...this, - hooksRouteLifeCycle: utils.A.concat(this.hooksRouteLifeCycle, utils.A.coalescing(hook)), - }; - }, - addStepFunctionBuilder(hook) { - return { - ...this, - stepFunctionBuilders: utils.A.concat(this.stepFunctionBuilders, utils.A.coalescing(hook)), - }; - }, - plug(plugin) { - return { - ...this, - plugins: utils.A.push(this.plugins, typeof plugin === "function" - ? plugin(this) - : plugin), - }; - }, - register(route) { - return { - ...this, - routes: utils.A.concat(this.routes, utils.pipe(route, utils.P.when(index.routeKind.has, utils.A.coalescing), utils.P.when(utils.isType("iterable"), utils.A.from), utils.P.otherwise(utils.O.values), utils.A.filter((route) => !utils.A.includes(this.routes, route)))), - }; - }, - setDefaultExtractContract(defaultExtractContract) { - return { - ...this, - defaultExtractContract, - }; - }, - setNotfoundHandler(responseContract, theFunction) { - return { - ...this, - notfoundHandler: handler.createHandlerStep({ - responseContract, - theFunction: (floor, params) => theFunction(params), - metadata: [], - }), - }; - }, - aggregates() { - return utils.A.reduce(this.plugins, utils.A.reduceFrom({ - hooksRouteLifeCycle: this.hooksRouteLifeCycle, - routeFunctionBuilders: this.routeFunctionBuilders, - stepFunctionBuilders: this.stepFunctionBuilders, - routes: this.routes, - hooksHubLifeCycle: this.hooksHubLifeCycle, - }), ({ lastValue, element: plugin, next, }) => next({ - hooksRouteLifeCycle: plugin.hooksRouteLifeCycle - ? utils.A.concat(lastValue.hooksRouteLifeCycle, plugin.hooksRouteLifeCycle) - : lastValue.hooksRouteLifeCycle, - routeFunctionBuilders: plugin.routeFunctionBuilders - ? utils.A.concat(lastValue.routeFunctionBuilders, plugin.routeFunctionBuilders) - : lastValue.routeFunctionBuilders, - stepFunctionBuilders: plugin.stepFunctionBuilders - ? utils.A.concat(lastValue.stepFunctionBuilders, plugin.stepFunctionBuilders) - : lastValue.stepFunctionBuilders, - routes: plugin.routes - ? utils.A.concat(lastValue.routes, plugin.routes) - : lastValue.routes, - hooksHubLifeCycle: plugin.hooksHubLifeCycle - ? utils.A.concat(lastValue.hooksHubLifeCycle, plugin.hooksHubLifeCycle) - : lastValue.hooksHubLifeCycle, - })); - }, - aggregatesRoutes() { - return utils.A.reduce(this.plugins, utils.A.reduceFrom(this.routes), ({ lastValue, element: { routes }, next, }) => routes - ? next(utils.A.concat(lastValue, routes)) - : next(lastValue)); - }, - aggregatesRouteFunctionBuilders() { - return utils.A.reduce(this.plugins, utils.A.reduceFrom(this.routeFunctionBuilders), ({ lastValue, element: { routeFunctionBuilders }, next, }) => routeFunctionBuilders - ? next(utils.A.concat(lastValue, routeFunctionBuilders)) - : next(lastValue)); - }, - aggregatesStepFunctionBuilders() { - return utils.A.reduce(this.plugins, utils.A.reduceFrom(this.stepFunctionBuilders), ({ lastValue, element: { stepFunctionBuilders }, next, }) => stepFunctionBuilders - ? next(utils.A.concat(lastValue, stepFunctionBuilders)) - : next(lastValue)); - }, - aggregatesHooksHubLifeCycle(hookName) { - const hooks = utils.A.flatMap(this.hooksHubLifeCycle, (hooks) => hooks[hookName] ?? []); - return utils.A.reduce(this.plugins, utils.A.reduceFrom(hooks), ({ lastValue, element: { hooksHubLifeCycle }, next, }) => { - if (!hooksHubLifeCycle) { - return next(lastValue); - } - return next(utils.A.concat(lastValue, utils.A.flatMap(hooksHubLifeCycle, (hooks) => hooks[hookName] ?? []))); - }); - }, - aggregatesHooksRouteLifeCycle(hookName) { - const hooks = utils.A.flatMap(this.hooksRouteLifeCycle, (hooks) => hooks[hookName] ?? []); - return utils.A.reduce(this.plugins, utils.A.reduceFrom(hooks), ({ lastValue, element: { hooksRouteLifeCycle }, next, }) => { - if (!hooksRouteLifeCycle) { - return next(lastValue); - } - return next(utils.A.concat(lastValue, utils.A.flatMap(hooksRouteLifeCycle, (hooks) => hooks[hookName] ?? []))); - }); - }, - }; + return Hub.new(config); } exports.defaultNotfoundHandler = defaultNotfoundHandler.defaultNotfoundHandler; exports.defaultExtractContract = defaultExtractContract.defaultExtractContract; +exports.defaultBodyController = defaultBodyController.defaultBodyController; exports.createHookHubLifeCycle = hooks.createHookHubLifeCycle; exports.hookServerExitKind = hooks.hookServerExitKind; exports.hookServerNextKind = hooks.hookServerNextKind; @@ -149,5 +122,6 @@ exports.launchHookServer = hooks.launchHookServer; exports.launchHookServerError = hooks.launchHookServerError; exports.serverErrorExitHookFunction = hooks.serverErrorExitHookFunction; exports.serverErrorNextHookFunction = hooks.serverErrorNextHookFunction; +exports.Hub = Hub; exports.createHub = createHub; exports.hubKind = hubKind; diff --git a/docs/libs/v0/core/hub/index.d.ts b/docs/libs/v0/core/hub/index.d.ts index 2864462..f552620 100644 --- a/docs/libs/v0/core/hub/index.d.ts +++ b/docs/libs/v0/core/hub/index.d.ts @@ -1,8 +1,8 @@ import { type Route, type HookRouteLifeCycle } from "../route"; -import { type Kind, type MaybeArray, type MaybePromise, type DP } from "@duplojs/utils"; +import { type MaybeArray, type MaybePromise, type DP } from "@duplojs/utils"; import { type HookHubLifeCycle } from "./hooks"; import { type HandlerStepFunctionParams, type HandlerStep } from "../steps"; -import { Request } from "../request"; +import { type BodyController, type BodyReaderImplementation, Request } from "../request"; import { type ClientErrorResponseCode, type ResponseContract } from "../response"; import { type Environment } from "../types"; import { type createStepFunctionBuilder } from "../functionsBuilders/steps"; @@ -10,6 +10,7 @@ import { type createRouteFunctionBuilder } from "../functionsBuilders/route"; export * from "./hooks"; export * from "./defaultNotfoundHandler"; export * from "./defaultExtractContract"; +export * from "./defaultBodyController"; export declare const hubKind: import("@duplojs/utils").KindHandler>; export interface HubConfig { readonly environment: Environment; @@ -21,38 +22,36 @@ export interface HubPlugin { readonly routes?: readonly Route[]; readonly routeFunctionBuilders?: readonly ReturnType[]; readonly stepFunctionBuilders?: readonly ReturnType[]; + readonly bodyReaderImplementations?: readonly BodyReaderImplementation[]; } -export interface HubAggregates { - readonly hooksRouteLifeCycle: readonly HookRouteLifeCycle[]; - readonly hooksHubLifeCycle: readonly HookHubLifeCycle[]; - readonly routes: readonly Route[]; - readonly routeFunctionBuilders: readonly ReturnType[]; - readonly stepFunctionBuilders: readonly ReturnType[]; -} -export interface Hub extends Kind { - readonly config: GenericConfig; - readonly plugins: readonly HubPlugin[]; - readonly hooksRouteLifeCycle: readonly HookRouteLifeCycle[]; - readonly hooksHubLifeCycle: readonly HookHubLifeCycle[]; - readonly routes: readonly Route[]; - readonly routeFunctionBuilders: readonly ReturnType[]; - readonly stepFunctionBuilders: readonly ReturnType[]; - readonly classRequest: typeof Request; - readonly notfoundHandler: HandlerStep; - readonly defaultExtractContract: ResponseContract.Contract; - register(routes: Route | Iterable | Record): Hub; - addRouteFunctionBuilder(functionBuilder: MaybeArray>): Hub; - addStepFunctionBuilder(functionBuilder: MaybeArray>): Hub; - addRouteHooks(hook: MaybeArray): Hub; - addHubHooks(hook: MaybeArray): Hub; - plug(plugin: HubPlugin | ((self: this) => HubPlugin)): Hub; - setNotfoundHandler>(responseContract: GenericResponseContract, theFunction: (param: HandlerStepFunctionParams) => MaybePromise): Hub; - setDefaultExtractContract(responseContract: this["defaultExtractContract"]): Hub; - aggregates(): HubAggregates; - aggregatesRoutes(): readonly Route[]; - aggregatesRouteFunctionBuilders(): readonly ReturnType[]; - aggregatesStepFunctionBuilders(): readonly ReturnType[]; - aggregatesHooksHubLifeCycle(hookName: GenericHookName): readonly Exclude[]; - aggregatesHooksRouteLifeCycle(hookName: GenericHookName): readonly Exclude[]; +declare const Hub_base: new (params?: { + "@DuplojsHttpCore/hub"?: unknown; +} | undefined) => import("@duplojs/utils").Kind, unknown> & import("@duplojs/utils").Kind, unknown>; +export declare class Hub extends Hub_base { + config: GenericConfig; + plugins: HubPlugin[]; + hooksRouteLifeCycle: HookRouteLifeCycle[]; + hooksHubLifeCycle: HookHubLifeCycle[]; + routes: Set>; + routeFunctionBuilders: ReturnType[]; + stepFunctionBuilders: ReturnType[]; + bodyReaderImplementations: BodyReaderImplementation[]; + classRequest: typeof Request; + notfoundHandler: HandlerStep; + defaultExtractContract: ResponseContract.Contract; + defaultBodyController: BodyController; + private constructor(); + register(routes: Route | Iterable | Record): this; + addRouteFunctionBuilder(functionBuilder: MaybeArray>): this; + addStepFunctionBuilder(functionBuilder: MaybeArray>): this; + addRouteHooks(hook: MaybeArray): this; + addHubHooks(hook: MaybeArray): this; + addBodyReaderImplementation(bodyReaderImplementation: MaybeArray): this; + plug(plugin: HubPlugin | ((self: this) => HubPlugin)): this; + setNotfoundHandler>(responseContract: GenericResponseContract, theFunction: (param: HandlerStepFunctionParams) => MaybePromise): this; + setDefaultExtractContract(responseContract: this["defaultExtractContract"]): this; + aggregatesHooksHubLifeCycle(hookName: GenericHookName): (NonNullable extends infer T ? T extends NonNullable ? T extends readonly (infer InnerArr)[] ? InnerArr extends readonly (infer InnerArr)[] ? InnerArr : InnerArr : T : never : never)[]; + setDefaultBodyController(bodyController: BodyController): this; + aggregatesHooksRouteLifeCycle(hookName: GenericHookName): (NonNullable[GenericHookName]> extends infer T ? T extends NonNullable[GenericHookName]> ? T extends readonly (infer InnerArr)[] ? InnerArr extends readonly (infer InnerArr)[] ? InnerArr : InnerArr : T : never : never)[]; } export declare function createHub(config: GenericConfig): Hub; diff --git a/docs/libs/v0/core/hub/index.mjs b/docs/libs/v0/core/hub/index.mjs index fc40b1c..69ccddd 100644 --- a/docs/libs/v0/core/hub/index.mjs +++ b/docs/libs/v0/core/hub/index.mjs @@ -1,140 +1,112 @@ import { createCoreLibKind } from '../kind.mjs'; import { routeKind } from '../route/index.mjs'; -import { A, pipe, P, isType, O } from '@duplojs/utils'; +import { kindHeritage, pipe, P, A, isType, O } from '@duplojs/utils'; import '../steps/index.mjs'; -import { Request } from '../request.mjs'; +import { Request } from '../request/index.mjs'; import { defaultNotfoundHandler } from './defaultNotfoundHandler.mjs'; import { defaultExtractContract } from './defaultExtractContract.mjs'; +import { defaultBodyController } from './defaultBodyController.mjs'; export { createHookHubLifeCycle, hookServerExitKind, hookServerNextKind, launchHookBeforeBuildRoute, launchHookServer, launchHookServerError, serverErrorExitHookFunction, serverErrorNextHookFunction } from './hooks.mjs'; import { createHandlerStep } from '../steps/handler.mjs'; const hubKind = createCoreLibKind("hub"); +class Hub extends kindHeritage("hub", createCoreLibKind("hub")) { + config; + plugins = []; + hooksRouteLifeCycle = []; + hooksHubLifeCycle = []; + routes = new Set(); + routeFunctionBuilders = []; + stepFunctionBuilders = []; + bodyReaderImplementations = []; + classRequest = Request; + notfoundHandler = defaultNotfoundHandler; + defaultExtractContract = defaultExtractContract; + defaultBodyController = defaultBodyController; + constructor(config) { + super({}); + this.config = config; + } + register(routes) { + pipe(routes, P.when(routeKind.has, A.coalescing), P.when(isType("iterable"), A.from), P.otherwise(O.values), A.map((route) => this.routes.add(route))); + return this; + } + addRouteFunctionBuilder(functionBuilder) { + this.routeFunctionBuilders.push(...A.coalescing(functionBuilder)); + return this; + } + addStepFunctionBuilder(functionBuilder) { + this.stepFunctionBuilders.push(...A.coalescing(functionBuilder)); + return this; + } + addRouteHooks(hook) { + this.hooksRouteLifeCycle.push(...A.coalescing(hook)); + return this; + } + addHubHooks(hook) { + this.hooksHubLifeCycle.push(...A.coalescing(hook)); + return this; + } + addBodyReaderImplementation(bodyReaderImplementation) { + this.bodyReaderImplementations.push(...A.coalescing(bodyReaderImplementation)); + return this; + } + plug(plugin) { + const pluginResult = typeof plugin === "function" + ? plugin(this) + : plugin; + if (pluginResult.bodyReaderImplementations) { + this.addBodyReaderImplementation(pluginResult.bodyReaderImplementations); + } + if (pluginResult.hooksHubLifeCycle) { + this.addHubHooks(pluginResult.hooksHubLifeCycle); + } + if (pluginResult.hooksRouteLifeCycle) { + this.addRouteHooks(pluginResult.hooksRouteLifeCycle); + } + if (pluginResult.routeFunctionBuilders) { + this.addRouteFunctionBuilder(pluginResult.routeFunctionBuilders); + } + if (pluginResult.routes) { + this.register(pluginResult.routes); + } + if (pluginResult.stepFunctionBuilders) { + this.addStepFunctionBuilder(pluginResult.stepFunctionBuilders); + } + this.plugins.push(pluginResult); + return this; + } + setNotfoundHandler(responseContract, theFunction) { + this.notfoundHandler = createHandlerStep({ + responseContract, + theFunction: (floor, params) => theFunction(params), + metadata: [], + }); + return this; + } + setDefaultExtractContract(responseContract) { + this.defaultExtractContract = responseContract; + return this; + } + aggregatesHooksHubLifeCycle(hookName) { + return A.flatMap(this.hooksHubLifeCycle, (hooks) => hooks[hookName] ?? []); + } + setDefaultBodyController(bodyController) { + this.defaultBodyController = bodyController; + return this; + } + aggregatesHooksRouteLifeCycle(hookName) { + return A.flatMap(this.hooksRouteLifeCycle, (hooks) => hooks[hookName] ?? []); + } + /** + * @internal + */ + static "new"(config) { + return new Hub(config); + } +} function createHub(config) { - return { - ...hubKind.addTo({}), - config, - plugins: [], - hooksHubLifeCycle: [], - hooksRouteLifeCycle: [], - routeFunctionBuilders: [], - routes: [], - stepFunctionBuilders: [], - notfoundHandler: defaultNotfoundHandler, - defaultExtractContract, - classRequest: Request, - addHubHooks(hook) { - return { - ...this, - hooksHubLifeCycle: A.concat(this.hooksHubLifeCycle, A.coalescing(hook)), - }; - }, - addRouteFunctionBuilder(functionBuilder) { - return { - ...this, - routeFunctionBuilders: A.concat(this.routeFunctionBuilders, A.coalescing(functionBuilder)), - }; - }, - addRouteHooks(hook) { - return { - ...this, - hooksRouteLifeCycle: A.concat(this.hooksRouteLifeCycle, A.coalescing(hook)), - }; - }, - addStepFunctionBuilder(hook) { - return { - ...this, - stepFunctionBuilders: A.concat(this.stepFunctionBuilders, A.coalescing(hook)), - }; - }, - plug(plugin) { - return { - ...this, - plugins: A.push(this.plugins, typeof plugin === "function" - ? plugin(this) - : plugin), - }; - }, - register(route) { - return { - ...this, - routes: A.concat(this.routes, pipe(route, P.when(routeKind.has, A.coalescing), P.when(isType("iterable"), A.from), P.otherwise(O.values), A.filter((route) => !A.includes(this.routes, route)))), - }; - }, - setDefaultExtractContract(defaultExtractContract) { - return { - ...this, - defaultExtractContract, - }; - }, - setNotfoundHandler(responseContract, theFunction) { - return { - ...this, - notfoundHandler: createHandlerStep({ - responseContract, - theFunction: (floor, params) => theFunction(params), - metadata: [], - }), - }; - }, - aggregates() { - return A.reduce(this.plugins, A.reduceFrom({ - hooksRouteLifeCycle: this.hooksRouteLifeCycle, - routeFunctionBuilders: this.routeFunctionBuilders, - stepFunctionBuilders: this.stepFunctionBuilders, - routes: this.routes, - hooksHubLifeCycle: this.hooksHubLifeCycle, - }), ({ lastValue, element: plugin, next, }) => next({ - hooksRouteLifeCycle: plugin.hooksRouteLifeCycle - ? A.concat(lastValue.hooksRouteLifeCycle, plugin.hooksRouteLifeCycle) - : lastValue.hooksRouteLifeCycle, - routeFunctionBuilders: plugin.routeFunctionBuilders - ? A.concat(lastValue.routeFunctionBuilders, plugin.routeFunctionBuilders) - : lastValue.routeFunctionBuilders, - stepFunctionBuilders: plugin.stepFunctionBuilders - ? A.concat(lastValue.stepFunctionBuilders, plugin.stepFunctionBuilders) - : lastValue.stepFunctionBuilders, - routes: plugin.routes - ? A.concat(lastValue.routes, plugin.routes) - : lastValue.routes, - hooksHubLifeCycle: plugin.hooksHubLifeCycle - ? A.concat(lastValue.hooksHubLifeCycle, plugin.hooksHubLifeCycle) - : lastValue.hooksHubLifeCycle, - })); - }, - aggregatesRoutes() { - return A.reduce(this.plugins, A.reduceFrom(this.routes), ({ lastValue, element: { routes }, next, }) => routes - ? next(A.concat(lastValue, routes)) - : next(lastValue)); - }, - aggregatesRouteFunctionBuilders() { - return A.reduce(this.plugins, A.reduceFrom(this.routeFunctionBuilders), ({ lastValue, element: { routeFunctionBuilders }, next, }) => routeFunctionBuilders - ? next(A.concat(lastValue, routeFunctionBuilders)) - : next(lastValue)); - }, - aggregatesStepFunctionBuilders() { - return A.reduce(this.plugins, A.reduceFrom(this.stepFunctionBuilders), ({ lastValue, element: { stepFunctionBuilders }, next, }) => stepFunctionBuilders - ? next(A.concat(lastValue, stepFunctionBuilders)) - : next(lastValue)); - }, - aggregatesHooksHubLifeCycle(hookName) { - const hooks = A.flatMap(this.hooksHubLifeCycle, (hooks) => hooks[hookName] ?? []); - return A.reduce(this.plugins, A.reduceFrom(hooks), ({ lastValue, element: { hooksHubLifeCycle }, next, }) => { - if (!hooksHubLifeCycle) { - return next(lastValue); - } - return next(A.concat(lastValue, A.flatMap(hooksHubLifeCycle, (hooks) => hooks[hookName] ?? []))); - }); - }, - aggregatesHooksRouteLifeCycle(hookName) { - const hooks = A.flatMap(this.hooksRouteLifeCycle, (hooks) => hooks[hookName] ?? []); - return A.reduce(this.plugins, A.reduceFrom(hooks), ({ lastValue, element: { hooksRouteLifeCycle }, next, }) => { - if (!hooksRouteLifeCycle) { - return next(lastValue); - } - return next(A.concat(lastValue, A.flatMap(hooksRouteLifeCycle, (hooks) => hooks[hookName] ?? []))); - }); - }, - }; + return Hub.new(config); } -export { createHub, defaultExtractContract, defaultNotfoundHandler, hubKind }; +export { Hub, createHub, defaultBodyController, defaultExtractContract, defaultNotfoundHandler, hubKind }; diff --git a/docs/libs/v0/core/implementHttpServer.cjs b/docs/libs/v0/core/implementHttpServer.cjs index ae57820..ceee359 100644 --- a/docs/libs/v0/core/implementHttpServer.cjs +++ b/docs/libs/v0/core/implementHttpServer.cjs @@ -6,10 +6,10 @@ var utils = require('@duplojs/utils'); var hooks = require('./hub/hooks.cjs'); async function implementHttpServer(params, initHttpServer) { - const newHub1 = await hooks.launchHookServer(params.hub.aggregatesHooksHubLifeCycle("beforeServerBuildRoutes"), params.hub, params.httpServerParams); - const router = await index.buildRouter(newHub1); - const newHub2 = await hooks.launchHookServer(newHub1.aggregatesHooksHubLifeCycle("beforeStartServer"), newHub1, params.httpServerParams); - const serverErrorHooks = newHub1.aggregatesHooksHubLifeCycle("serverError"); + await hooks.launchHookServer(params.hub.aggregatesHooksHubLifeCycle("beforeServerBuildRoutes"), params.hub, params.httpServerParams); + const router = await index.buildRouter(params.hub); + await hooks.launchHookServer(params.hub.aggregatesHooksHubLifeCycle("beforeStartServer"), params.hub, params.httpServerParams); + const serverErrorHooks = params.hub.aggregatesHooksHubLifeCycle("serverError"); function catchCriticalError(error) { console.error("Critical Error :", error); } @@ -29,7 +29,7 @@ async function implementHttpServer(params, initHttpServer) { execRouteSystem: execRouteSystem, httpServerParams: params.httpServerParams, }); - await hooks.launchHookServer(newHub2.aggregatesHooksHubLifeCycle("afterStartServer"), newHub2, params.httpServerParams); + await hooks.launchHookServer(params.hub.aggregatesHooksHubLifeCycle("afterStartServer"), params.hub, params.httpServerParams); return result; } diff --git a/docs/libs/v0/core/implementHttpServer.d.ts b/docs/libs/v0/core/implementHttpServer.d.ts index 27f0211..099bdfb 100644 --- a/docs/libs/v0/core/implementHttpServer.d.ts +++ b/docs/libs/v0/core/implementHttpServer.d.ts @@ -1,6 +1,7 @@ -import { type Hub, type HttpServerParams } from "./hub"; +import { type Hub } from "./hub"; import { type RouterInitializationData } from "./router"; import { type MaybePromise } from "@duplojs/utils"; +import { type HttpServerParams } from "./types"; export interface ImplementHttpServerParams { readonly hub: Hub; readonly httpServerParams: HttpServerParams; diff --git a/docs/libs/v0/core/implementHttpServer.mjs b/docs/libs/v0/core/implementHttpServer.mjs index 87352a2..df489ef 100644 --- a/docs/libs/v0/core/implementHttpServer.mjs +++ b/docs/libs/v0/core/implementHttpServer.mjs @@ -4,10 +4,10 @@ import { forward } from '@duplojs/utils'; import { launchHookServer, launchHookServerError, serverErrorNextHookFunction, serverErrorExitHookFunction } from './hub/hooks.mjs'; async function implementHttpServer(params, initHttpServer) { - const newHub1 = await launchHookServer(params.hub.aggregatesHooksHubLifeCycle("beforeServerBuildRoutes"), params.hub, params.httpServerParams); - const router = await buildRouter(newHub1); - const newHub2 = await launchHookServer(newHub1.aggregatesHooksHubLifeCycle("beforeStartServer"), newHub1, params.httpServerParams); - const serverErrorHooks = newHub1.aggregatesHooksHubLifeCycle("serverError"); + await launchHookServer(params.hub.aggregatesHooksHubLifeCycle("beforeServerBuildRoutes"), params.hub, params.httpServerParams); + const router = await buildRouter(params.hub); + await launchHookServer(params.hub.aggregatesHooksHubLifeCycle("beforeStartServer"), params.hub, params.httpServerParams); + const serverErrorHooks = params.hub.aggregatesHooksHubLifeCycle("serverError"); function catchCriticalError(error) { console.error("Critical Error :", error); } @@ -27,7 +27,7 @@ async function implementHttpServer(params, initHttpServer) { execRouteSystem: execRouteSystem, httpServerParams: params.httpServerParams, }); - await launchHookServer(newHub2.aggregatesHooksHubLifeCycle("afterStartServer"), newHub2, params.httpServerParams); + await launchHookServer(params.hub.aggregatesHooksHubLifeCycle("afterStartServer"), params.hub, params.httpServerParams); return result; } diff --git a/docs/libs/v0/core/index.cjs b/docs/libs/v0/core/index.cjs index 7874cd7..1f5a574 100644 --- a/docs/libs/v0/core/index.cjs +++ b/docs/libs/v0/core/index.cjs @@ -9,15 +9,18 @@ var checker$2 = require('./checker.cjs'); require('./floor.cjs'); var kind$1 = require('./kind.cjs'); var index$1 = require('./process/index.cjs'); -var request = require('./request.cjs'); +var index$2 = require('./request/index.cjs'); var presetChecker$1 = require('./presetChecker.cjs'); -var index$2 = require('./hub/index.cjs'); +var index$3 = require('./hub/index.cjs'); require('./functionsBuilders/index.cjs'); -var index$3 = require('./router/index.cjs'); +var index$4 = require('./router/index.cjs'); var stringIdentifier = require('./stringIdentifier.cjs'); require('./metadata/index.cjs'); var implementHttpServer = require('./implementHttpServer.cjs'); var narrowingInput = require('./narrowingInput.cjs'); +require('./clean/index.cjs'); +var index$5 = require('./defaultHooks/index.cjs'); +require('./errors/index.cjs'); var checker = require('./builders/checker.cjs'); var builder = require('./builders/route/builder.cjs'); var store = require('./builders/route/store.cjs'); @@ -36,9 +39,13 @@ var cut = require('./steps/cut.cjs'); var handler = require('./steps/handler.cjs'); var process = require('./steps/process.cjs'); var presetChecker = require('./steps/presetChecker.cjs'); +var base$1 = require('./request/bodyController/base.cjs'); +var formData = require('./request/bodyController/formData.cjs'); +var text = require('./request/bodyController/text.cjs'); var hooks$1 = require('./hub/hooks.cjs'); var defaultNotfoundHandler = require('./hub/defaultNotfoundHandler.cjs'); var defaultExtractContract = require('./hub/defaultExtractContract.cjs'); +var defaultBodyController = require('./hub/defaultBodyController.cjs'); var build = require('./functionsBuilders/route/build.cjs'); var create = require('./functionsBuilders/route/create.cjs'); var _default = require('./functionsBuilders/route/default.cjs'); @@ -53,8 +60,13 @@ var build$1 = require('./functionsBuilders/steps/build.cjs'); var pathToRegExp = require('./router/pathToRegExp.cjs'); var buildError = require('./router/buildError.cjs'); var decodeUrl = require('./router/decodeUrl.cjs'); -var base$1 = require('./metadata/base.cjs'); +var notFoundBodyReaderImplementationError = require('./router/notFoundBodyReaderImplementationError.cjs'); +var base$2 = require('./metadata/base.cjs'); var ignoreByRouteStore = require('./metadata/ignoreByRouteStore.cjs'); +var wrongContentTypeError = require('./errors/wrongContentTypeError.cjs'); +var bodyParseWrongChunkReceived = require('./errors/bodyParseWrongChunkReceived.cjs'); +var bodySizeExceedsLimitError = require('./errors/bodySizeExceedsLimitError.cjs'); +var parseJsonError = require('./errors/parseJsonError.cjs'); @@ -66,15 +78,17 @@ exports.createChecker = checker$2.createChecker; exports.createCoreLibKind = kind$1.createCoreLibKind; exports.createProcess = index$1.createProcess; exports.processKind = index$1.processKind; -exports.Request = request.Request; +exports.Request = index$2.Request; exports.createPresetChecker = presetChecker$1.createPresetChecker; exports.presetCheckerKind = presetChecker$1.presetCheckerKind; -exports.createHub = index$2.createHub; -exports.hubKind = index$2.hubKind; -exports.buildRouter = index$3.buildRouter; +exports.Hub = index$3.Hub; +exports.createHub = index$3.createHub; +exports.hubKind = index$3.hubKind; +exports.buildRouter = index$4.buildRouter; exports.createCoreLibStringIdentifier = stringIdentifier.createCoreLibStringIdentifier; exports.implementHttpServer = implementHttpServer.implementHttpServer; exports.createNarrowingInput = narrowingInput.createNarrowingInput; +exports.initDefaultHook = index$5.initDefaultHook; exports.checkerBuilder = checker.checkerBuilder; exports.useCheckerBuilder = checker.useCheckerBuilder; exports.routeBuilderHandler = builder.routeBuilderHandler; @@ -109,6 +123,11 @@ exports.createProcessStep = process.createProcessStep; exports.processStepKind = process.processStepKind; exports.createPresetCheckerStep = presetChecker.createPresetCheckerStep; exports.presetCheckerStepKind = presetChecker.presetCheckerStepKind; +exports.createBodyController = base$1.createBodyController; +exports.FormDataBodyController = formData.FormDataBodyController; +exports.controlBodyAsFormData = formData.controlBodyAsFormData; +exports.TextBodyController = text.TextBodyController; +exports.controlBodyAsText = text.controlBodyAsText; exports.createHookHubLifeCycle = hooks$1.createHookHubLifeCycle; exports.hookServerExitKind = hooks$1.hookServerExitKind; exports.hookServerNextKind = hooks$1.hookServerNextKind; @@ -119,6 +138,7 @@ exports.serverErrorExitHookFunction = hooks$1.serverErrorExitHookFunction; exports.serverErrorNextHookFunction = hooks$1.serverErrorNextHookFunction; exports.defaultNotfoundHandler = defaultNotfoundHandler.defaultNotfoundHandler; exports.defaultExtractContract = defaultExtractContract.defaultExtractContract; +exports.defaultBodyController = defaultBodyController.defaultBodyController; exports.buildRouteFunction = build.buildRouteFunction; exports.createRouteFunctionBuilder = create.createRouteFunctionBuilder; exports.defaultRouteFunctionBuilder = _default.defaultRouteFunctionBuilder; @@ -141,6 +161,11 @@ exports.RouterBuildError = buildError.RouterBuildError; exports.decodeUrl = decodeUrl.decodeUrl; exports.regexQueryAnalyser = decodeUrl.regexQueryAnalyser; exports.regexUrlAnalyser = decodeUrl.regexUrlAnalyser; -exports.createMetadata = base$1.createMetadata; -exports.metadataKind = base$1.metadataKind; +exports.NotFoundBodyReaderImplementationError = notFoundBodyReaderImplementationError.NotFoundBodyReaderImplementationError; +exports.createMetadata = base$2.createMetadata; +exports.metadataKind = base$2.metadataKind; exports.IgnoreByRouteStoreMetadata = ignoreByRouteStore.IgnoreByRouteStoreMetadata; +exports.WrongContentTypeError = wrongContentTypeError.WrongContentTypeError; +exports.BodyParseWrongChunkReceived = bodyParseWrongChunkReceived.BodyParseWrongChunkReceived; +exports.BodySizeExceedsLimitError = bodySizeExceedsLimitError.BodySizeExceedsLimitError; +exports.ParseJsonError = parseJsonError.ParseJsonError; diff --git a/docs/libs/v0/core/index.d.ts b/docs/libs/v0/core/index.d.ts index 853e688..2603b4d 100644 --- a/docs/libs/v0/core/index.d.ts +++ b/docs/libs/v0/core/index.d.ts @@ -16,3 +16,6 @@ export * from "./stringIdentifier"; export * from "./metadata"; export * from "./implementHttpServer"; export * from "./narrowingInput"; +export * from "./clean"; +export * from "./defaultHooks"; +export * from "./errors"; diff --git a/docs/libs/v0/core/index.mjs b/docs/libs/v0/core/index.mjs index 8b4fcab..7cb3b5f 100644 --- a/docs/libs/v0/core/index.mjs +++ b/docs/libs/v0/core/index.mjs @@ -7,15 +7,18 @@ export { checkerKind, checkerOutputKind, createChecker } from './checker.mjs'; import './floor.mjs'; export { createCoreLibKind } from './kind.mjs'; export { createProcess, processKind } from './process/index.mjs'; -export { Request } from './request.mjs'; +export { Request } from './request/index.mjs'; export { createPresetChecker, presetCheckerKind } from './presetChecker.mjs'; -export { createHub, hubKind } from './hub/index.mjs'; +export { Hub, createHub, hubKind } from './hub/index.mjs'; import './functionsBuilders/index.mjs'; export { buildRouter } from './router/index.mjs'; export { createCoreLibStringIdentifier } from './stringIdentifier.mjs'; import './metadata/index.mjs'; export { implementHttpServer } from './implementHttpServer.mjs'; export { createNarrowingInput } from './narrowingInput.mjs'; +import './clean/index.mjs'; +export { initDefaultHook } from './defaultHooks/index.mjs'; +import './errors/index.mjs'; export { checkerBuilder, useCheckerBuilder } from './builders/checker.mjs'; export { routeBuilderHandler, useRouteBuilder } from './builders/route/builder.mjs'; export { routeStore } from './builders/route/store.mjs'; @@ -34,9 +37,13 @@ export { createCutStep, cutStepKind, cutStepOutputKind } from './steps/cut.mjs'; export { createHandlerStep, handlerStepKind } from './steps/handler.mjs'; export { createProcessStep, processStepKind } from './steps/process.mjs'; export { createPresetCheckerStep, presetCheckerStepKind } from './steps/presetChecker.mjs'; +export { createBodyController } from './request/bodyController/base.mjs'; +export { FormDataBodyController, controlBodyAsFormData } from './request/bodyController/formData.mjs'; +export { TextBodyController, controlBodyAsText } from './request/bodyController/text.mjs'; export { createHookHubLifeCycle, hookServerExitKind, hookServerNextKind, launchHookBeforeBuildRoute, launchHookServer, launchHookServerError, serverErrorExitHookFunction, serverErrorNextHookFunction } from './hub/hooks.mjs'; export { defaultNotfoundHandler } from './hub/defaultNotfoundHandler.mjs'; export { defaultExtractContract } from './hub/defaultExtractContract.mjs'; +export { defaultBodyController } from './hub/defaultBodyController.mjs'; export { buildRouteFunction } from './functionsBuilders/route/build.mjs'; export { createRouteFunctionBuilder } from './functionsBuilders/route/create.mjs'; export { defaultRouteFunctionBuilder } from './functionsBuilders/route/default.mjs'; @@ -51,5 +58,10 @@ export { buildStepFunction } from './functionsBuilders/steps/build.mjs'; export { pathToRegExp } from './router/pathToRegExp.mjs'; export { RouterBuildError } from './router/buildError.mjs'; export { decodeUrl, regexQueryAnalyser, regexUrlAnalyser } from './router/decodeUrl.mjs'; +export { NotFoundBodyReaderImplementationError } from './router/notFoundBodyReaderImplementationError.mjs'; export { createMetadata, metadataKind } from './metadata/base.mjs'; export { IgnoreByRouteStoreMetadata } from './metadata/ignoreByRouteStore.mjs'; +export { WrongContentTypeError } from './errors/wrongContentTypeError.mjs'; +export { BodyParseWrongChunkReceived } from './errors/bodyParseWrongChunkReceived.mjs'; +export { BodySizeExceedsLimitError } from './errors/bodySizeExceedsLimitError.mjs'; +export { ParseJsonError } from './errors/parseJsonError.mjs'; diff --git a/docs/libs/v0/core/request.mjs b/docs/libs/v0/core/request.mjs deleted file mode 100644 index 9c0ff49..0000000 --- a/docs/libs/v0/core/request.mjs +++ /dev/null @@ -1,32 +0,0 @@ -import { kindHeritage } from '@duplojs/utils'; -import { createCoreLibKind } from './kind.mjs'; - -class Request extends kindHeritage("request", createCoreLibKind("request")) { - method; - headers; - url; - host; - origin; - path; - params; - query; - matchedPath; - body = undefined; - constructor({ method, headers, url, host, origin, path, params, query, matchedPath, ...rest }) { - super(); - this.method = method; - this.headers = headers; - this.url = url; - this.host = host; - this.origin = origin; - this.path = path; - this.params = params; - this.query = query; - this.matchedPath = matchedPath; - for (const key in rest) { - this[key] = rest[key]; - } - } -} - -export { Request }; diff --git a/docs/libs/v0/core/request/bodyController/base.cjs b/docs/libs/v0/core/request/bodyController/base.cjs new file mode 100644 index 0000000..2c382fb --- /dev/null +++ b/docs/libs/v0/core/request/bodyController/base.cjs @@ -0,0 +1,36 @@ +'use strict'; + +var kind = require('../../kind.cjs'); +var utils = require('@duplojs/utils'); + +const bodyReaderKind = kind.createCoreLibKind("body-reader"); +const bodyReaderImplementationKind = kind.createCoreLibKind("body-reader-implementation"); +const bodyControllerKind = kind.createCoreLibKind("body-controller"); +const bodyControllerHandlerKind = kind.createCoreLibKind("body-controller-handler"); +function createBodyController(name) { + return bodyControllerHandlerKind.setTo({ + name, + create(params) { + return bodyControllerKind.setTo({ + name, + params, + tryToCreateReader(readerImplementation) { + if (bodyReaderImplementationKind.getValue(readerImplementation) !== name) { + return utils.E.fail(); + } + return utils.E.success(bodyReaderKind.setTo({ + read: (request) => readerImplementation.read(request, params), + }, name)); + }, + }, name); + }, + createReaderImplementation(read) { + return bodyReaderImplementationKind.setTo({ read }, name); + }, + is(input) { + return bodyControllerKind.has(input) && bodyControllerKind.getValue(input) === name; + }, + }); +} + +exports.createBodyController = createBodyController; diff --git a/docs/libs/v0/core/request/bodyController/base.d.ts b/docs/libs/v0/core/request/bodyController/base.d.ts new file mode 100644 index 0000000..849d834 --- /dev/null +++ b/docs/libs/v0/core/request/bodyController/base.d.ts @@ -0,0 +1,28 @@ +import { type Request } from "../../request"; +import { E, type Kind } from "@duplojs/utils"; +export interface BodyControllerParams { + bodyMaxSize?: number; +} +declare const bodyReaderKind: import("@duplojs/utils").KindHandler>; +export interface BodyReader extends Kind { + read(request: Request): Promise>; +} +declare const bodyReaderImplementationKind: import("@duplojs/utils").KindHandler>; +export interface BodyReaderImplementation extends Kind { + read(request: Request, params: GenericParams): Promise>; +} +declare const bodyControllerKind: import("@duplojs/utils").KindHandler>; +export interface BodyController extends Kind { + readonly name: GenericName; + readonly params: GenericParams; + tryToCreateReader(readerImplementation: BodyReaderImplementation): E.Success> | E.Fail; +} +declare const bodyControllerHandlerKind: import("@duplojs/utils").KindHandler>; +export interface BodyControllerHandler extends Kind { + readonly name: GenericName; + create(params: GenericParams): BodyController; + createReaderImplementation(read: BodyReaderImplementation["read"]): BodyReaderImplementation; + is(input: unknown): input is BodyController; +} +export declare function createBodyController(name: GenericName): BodyControllerHandler; +export {}; diff --git a/docs/libs/v0/core/request/bodyController/base.mjs b/docs/libs/v0/core/request/bodyController/base.mjs new file mode 100644 index 0000000..d48b105 --- /dev/null +++ b/docs/libs/v0/core/request/bodyController/base.mjs @@ -0,0 +1,34 @@ +import { createCoreLibKind } from '../../kind.mjs'; +import { E } from '@duplojs/utils'; + +const bodyReaderKind = createCoreLibKind("body-reader"); +const bodyReaderImplementationKind = createCoreLibKind("body-reader-implementation"); +const bodyControllerKind = createCoreLibKind("body-controller"); +const bodyControllerHandlerKind = createCoreLibKind("body-controller-handler"); +function createBodyController(name) { + return bodyControllerHandlerKind.setTo({ + name, + create(params) { + return bodyControllerKind.setTo({ + name, + params, + tryToCreateReader(readerImplementation) { + if (bodyReaderImplementationKind.getValue(readerImplementation) !== name) { + return E.fail(); + } + return E.success(bodyReaderKind.setTo({ + read: (request) => readerImplementation.read(request, params), + }, name)); + }, + }, name); + }, + createReaderImplementation(read) { + return bodyReaderImplementationKind.setTo({ read }, name); + }, + is(input) { + return bodyControllerKind.has(input) && bodyControllerKind.getValue(input) === name; + }, + }); +} + +export { createBodyController }; diff --git a/docs/libs/v0/core/request/bodyController/formData.cjs b/docs/libs/v0/core/request/bodyController/formData.cjs new file mode 100644 index 0000000..44eb4f5 --- /dev/null +++ b/docs/libs/v0/core/request/bodyController/formData.cjs @@ -0,0 +1,28 @@ +'use strict'; + +var utils = require('@duplojs/utils'); +var base = require('./base.cjs'); + +const FormDataBodyController = base.createBodyController("formData"); +function controlBodyAsFormData(params) { + return FormDataBodyController.create({ + maxFileQuantity: params.maxFileQuantity, + bodyMaxSize: params.bodyMaxSize && utils.stringToBytes(params.bodyMaxSize), + fileMaxSize: params.fileMaxSize && utils.stringToBytes(params.fileMaxSize), + mimeType: params.mimeType !== undefined + ? utils.toRegExp(params.mimeType) + : undefined, + maxBufferSize: params.maxBufferSize !== undefined + ? utils.stringToBytes(params.maxBufferSize) + : utils.stringToBytes("128kb"), + maxIndexArray: params.maxIndexArray !== undefined + ? params.maxIndexArray + : 500, + maxKeyLength: params.maxKeyLength !== undefined + ? params.maxKeyLength + : 500, + }); +} + +exports.FormDataBodyController = FormDataBodyController; +exports.controlBodyAsFormData = controlBodyAsFormData; diff --git a/docs/libs/v0/core/request/bodyController/formData.d.ts b/docs/libs/v0/core/request/bodyController/formData.d.ts new file mode 100644 index 0000000..e152d1c --- /dev/null +++ b/docs/libs/v0/core/request/bodyController/formData.d.ts @@ -0,0 +1,22 @@ +import { type BytesInString } from "@duplojs/utils"; +import { type BodyControllerParams } from "./base"; +export interface FormDataBodyReaderParams extends BodyControllerParams { + maxFileQuantity: number; + mimeType?: RegExp; + fileMaxSize?: number; + maxBufferSize: number; + maxIndexArray: number; + maxKeyLength: number; +} +export declare const FormDataBodyController: import("./base").BodyControllerHandler<"formData", FormDataBodyReaderParams>; +export type FormDataBodyController = typeof FormDataBodyController; +export interface ControlBodyAsFormDataParams { + maxFileQuantity: number; + mimeType?: string | string[] | RegExp; + bodyMaxSize?: number | BytesInString; + fileMaxSize?: number | BytesInString; + maxBufferSize?: number | BytesInString; + maxIndexArray?: number; + maxKeyLength?: number; +} +export declare function controlBodyAsFormData(params: ControlBodyAsFormDataParams): import("./base").BodyController<"formData", FormDataBodyReaderParams>; diff --git a/docs/libs/v0/core/request/bodyController/formData.mjs b/docs/libs/v0/core/request/bodyController/formData.mjs new file mode 100644 index 0000000..2240bd4 --- /dev/null +++ b/docs/libs/v0/core/request/bodyController/formData.mjs @@ -0,0 +1,25 @@ +import { stringToBytes, toRegExp } from '@duplojs/utils'; +import { createBodyController } from './base.mjs'; + +const FormDataBodyController = createBodyController("formData"); +function controlBodyAsFormData(params) { + return FormDataBodyController.create({ + maxFileQuantity: params.maxFileQuantity, + bodyMaxSize: params.bodyMaxSize && stringToBytes(params.bodyMaxSize), + fileMaxSize: params.fileMaxSize && stringToBytes(params.fileMaxSize), + mimeType: params.mimeType !== undefined + ? toRegExp(params.mimeType) + : undefined, + maxBufferSize: params.maxBufferSize !== undefined + ? stringToBytes(params.maxBufferSize) + : stringToBytes("128kb"), + maxIndexArray: params.maxIndexArray !== undefined + ? params.maxIndexArray + : 500, + maxKeyLength: params.maxKeyLength !== undefined + ? params.maxKeyLength + : 500, + }); +} + +export { FormDataBodyController, controlBodyAsFormData }; diff --git a/docs/libs/v0/core/request/bodyController/index.cjs b/docs/libs/v0/core/request/bodyController/index.cjs new file mode 100644 index 0000000..0bf60bc --- /dev/null +++ b/docs/libs/v0/core/request/bodyController/index.cjs @@ -0,0 +1,13 @@ +'use strict'; + +var base = require('./base.cjs'); +var formData = require('./formData.cjs'); +var text = require('./text.cjs'); + + + +exports.createBodyController = base.createBodyController; +exports.FormDataBodyController = formData.FormDataBodyController; +exports.controlBodyAsFormData = formData.controlBodyAsFormData; +exports.TextBodyController = text.TextBodyController; +exports.controlBodyAsText = text.controlBodyAsText; diff --git a/docs/libs/v0/core/request/bodyController/index.d.ts b/docs/libs/v0/core/request/bodyController/index.d.ts new file mode 100644 index 0000000..b499ebc --- /dev/null +++ b/docs/libs/v0/core/request/bodyController/index.d.ts @@ -0,0 +1,3 @@ +export * from "./base"; +export * from "./formData"; +export * from "./text"; diff --git a/docs/libs/v0/core/request/bodyController/index.mjs b/docs/libs/v0/core/request/bodyController/index.mjs new file mode 100644 index 0000000..f382203 --- /dev/null +++ b/docs/libs/v0/core/request/bodyController/index.mjs @@ -0,0 +1,3 @@ +export { createBodyController } from './base.mjs'; +export { FormDataBodyController, controlBodyAsFormData } from './formData.mjs'; +export { TextBodyController, controlBodyAsText } from './text.mjs'; diff --git a/docs/libs/v0/core/request/bodyController/text.cjs b/docs/libs/v0/core/request/bodyController/text.cjs new file mode 100644 index 0000000..cf27558 --- /dev/null +++ b/docs/libs/v0/core/request/bodyController/text.cjs @@ -0,0 +1,14 @@ +'use strict'; + +var utils = require('@duplojs/utils'); +var base = require('./base.cjs'); + +const TextBodyController = base.createBodyController("text"); +function controlBodyAsText(params) { + return TextBodyController.create({ + bodyMaxSize: params?.bodyMaxSize && utils.stringToBytes(params.bodyMaxSize), + }); +} + +exports.TextBodyController = TextBodyController; +exports.controlBodyAsText = controlBodyAsText; diff --git a/docs/libs/v0/core/request/bodyController/text.d.ts b/docs/libs/v0/core/request/bodyController/text.d.ts new file mode 100644 index 0000000..1f09134 --- /dev/null +++ b/docs/libs/v0/core/request/bodyController/text.d.ts @@ -0,0 +1,10 @@ +import { type BytesInString } from "@duplojs/utils"; +import { type BodyControllerParams } from "./base"; +export interface TextBodyReaderParams extends BodyControllerParams { +} +export declare const TextBodyController: import("./base").BodyControllerHandler<"text", TextBodyReaderParams>; +export type TextBodyController = typeof TextBodyController; +export interface ControlBodyAsTextParams { + bodyMaxSize?: number | BytesInString; +} +export declare function controlBodyAsText(params?: ControlBodyAsTextParams): import("./base").BodyController<"text", TextBodyReaderParams>; diff --git a/docs/libs/v0/core/request/bodyController/text.mjs b/docs/libs/v0/core/request/bodyController/text.mjs new file mode 100644 index 0000000..8af1b1d --- /dev/null +++ b/docs/libs/v0/core/request/bodyController/text.mjs @@ -0,0 +1,11 @@ +import { stringToBytes } from '@duplojs/utils'; +import { createBodyController } from './base.mjs'; + +const TextBodyController = createBodyController("text"); +function controlBodyAsText(params) { + return TextBodyController.create({ + bodyMaxSize: params?.bodyMaxSize && stringToBytes(params.bodyMaxSize), + }); +} + +export { TextBodyController, controlBodyAsText }; diff --git a/docs/libs/v0/core/request.cjs b/docs/libs/v0/core/request/index.cjs similarity index 51% rename from docs/libs/v0/core/request.cjs rename to docs/libs/v0/core/request/index.cjs index 891eb25..11f44d9 100644 --- a/docs/libs/v0/core/request.cjs +++ b/docs/libs/v0/core/request/index.cjs @@ -1,7 +1,8 @@ 'use strict'; var utils = require('@duplojs/utils'); -var kind = require('./kind.cjs'); +var kind = require('../kind.cjs'); +require('./bodyController/index.cjs'); class Request extends utils.kindHeritage("request", kind.createCoreLibKind("request")) { method; @@ -13,8 +14,10 @@ class Request extends utils.kindHeritage("request", kind.createCoreLibKind("requ params; query; matchedPath; - body = undefined; - constructor({ method, headers, url, host, origin, path, params, query, matchedPath, ...rest }) { + bodyReader; + bodyResult = undefined; + filesAttache = undefined; + constructor({ method, headers, url, host, origin, path, params, query, matchedPath, bodyReader, ...rest }) { super(); this.method = method; this.headers = headers; @@ -25,10 +28,25 @@ class Request extends utils.kindHeritage("request", kind.createCoreLibKind("requ this.params = params; this.query = query; this.matchedPath = matchedPath; + this.bodyReader = bodyReader; for (const key in rest) { this[key] = rest[key]; } } + getBody() { + if (this.bodyResult !== undefined) { + return this.bodyResult; + } + const externalPromise = utils.createExternalPromise(); + this.bodyResult = externalPromise.promise; + return this.bodyReader + .read(this) + .then((result) => { + externalPromise.resolve(result); + this.bodyResult = result; + return result; + }); + } } exports.Request = Request; diff --git a/docs/libs/v0/core/request.d.ts b/docs/libs/v0/core/request/index.d.ts similarity index 77% rename from docs/libs/v0/core/request.d.ts rename to docs/libs/v0/core/request/index.d.ts index ca79c08..6fc9d17 100644 --- a/docs/libs/v0/core/request.d.ts +++ b/docs/libs/v0/core/request/index.d.ts @@ -1,8 +1,12 @@ +import { type E, type MaybePromise } from "@duplojs/utils"; import { type GetPropsWithValue } from "@duplojs/utils/object"; +import { type BodyReader } from "./bodyController"; +export * from "./bodyController"; export interface RequestMethodsWrapper { GET: true; POST: true; PUT: true; + PATCH: true; DELETE: true; HEAD: true; OPTIONS: true; @@ -20,6 +24,7 @@ export interface RequestInitializationData { readonly path: string; readonly query: Record; readonly url: string; + readonly bodyReader: BodyReader; } declare const Request_base: new (params?: { "@DuplojsHttpCore/request"?: unknown; @@ -34,7 +39,9 @@ export declare class Request extends Request_base implements RequestInitializati params: Record; query: Record; matchedPath: string | null; - body: unknown; - constructor({ method, headers, url, host, origin, path, params, query, matchedPath, ...rest }: RequestInitializationData); + bodyReader: BodyReader; + private bodyResult?; + filesAttache: string[] | undefined; + constructor({ method, headers, url, host, origin, path, params, query, matchedPath, bodyReader, ...rest }: RequestInitializationData); + getBody(): MaybePromise; } -export {}; diff --git a/docs/libs/v0/core/request/index.mjs b/docs/libs/v0/core/request/index.mjs new file mode 100644 index 0000000..8ad0cbf --- /dev/null +++ b/docs/libs/v0/core/request/index.mjs @@ -0,0 +1,50 @@ +import { kindHeritage, createExternalPromise } from '@duplojs/utils'; +import { createCoreLibKind } from '../kind.mjs'; +import './bodyController/index.mjs'; + +class Request extends kindHeritage("request", createCoreLibKind("request")) { + method; + headers; + url; + host; + origin; + path; + params; + query; + matchedPath; + bodyReader; + bodyResult = undefined; + filesAttache = undefined; + constructor({ method, headers, url, host, origin, path, params, query, matchedPath, bodyReader, ...rest }) { + super(); + this.method = method; + this.headers = headers; + this.url = url; + this.host = host; + this.origin = origin; + this.path = path; + this.params = params; + this.query = query; + this.matchedPath = matchedPath; + this.bodyReader = bodyReader; + for (const key in rest) { + this[key] = rest[key]; + } + } + getBody() { + if (this.bodyResult !== undefined) { + return this.bodyResult; + } + const externalPromise = createExternalPromise(); + this.bodyResult = externalPromise.promise; + return this.bodyReader + .read(this) + .then((result) => { + externalPromise.resolve(result); + this.bodyResult = result; + return result; + }); + } +} + +export { Request }; diff --git a/docs/libs/v0/core/response/contract.d.ts b/docs/libs/v0/core/response/contract.d.ts index 950d3f2..a96ddaa 100644 --- a/docs/libs/v0/core/response/contract.d.ts +++ b/docs/libs/v0/core/response/contract.d.ts @@ -283,7 +283,7 @@ export declare namespace ResponseContract { }>>(information: GenericInformation, schema?: (GenericSchema & ForbiddenBigintDataParser) | undefined) => NoInfer>>>; const Error_base: new (params: { "@DuplojsHttpCore/contract-error"?: unknown; - }, parentParams: [message?: string | undefined, options?: ErrorOptions | undefined]) => Kind, unknown> & Kind, unknown> & globalThis.Error; + }, parentParams: readonly [message?: string | undefined, options?: ErrorOptions | undefined]) => globalThis.Error & Kind, unknown> & Kind, unknown>; export class Error extends Error_base { information: string; dataParserError?: DP.DataParserError | undefined; diff --git a/docs/libs/v0/core/response/hook.d.ts b/docs/libs/v0/core/response/hook.d.ts index ea11b0c..ae897b6 100644 --- a/docs/libs/v0/core/response/hook.d.ts +++ b/docs/libs/v0/core/response/hook.d.ts @@ -2,7 +2,7 @@ import { type ResponseCode, Response } from "../response"; import { type HookRouteLifeCycle } from "../route/hooks"; declare const HookResponse_base: new (params: { "@DuplojsHttpCore/hook-response"?: unknown; -}, parentParams: [code: any, information: any, body: any]) => Response & import("@duplojs/utils").Kind, unknown> & import("@duplojs/utils").Kind, unknown>; +}, parentParams: readonly [code: any, information: any, body: any]) => Response & import("@duplojs/utils").Kind, unknown> & import("@duplojs/utils").Kind, unknown>; export declare class HookResponse extends HookResponse_base { code: GenericCode; information: GenericInformation; diff --git a/docs/libs/v0/core/response/predicted.d.ts b/docs/libs/v0/core/response/predicted.d.ts index 069020d..2ac9395 100644 --- a/docs/libs/v0/core/response/predicted.d.ts +++ b/docs/libs/v0/core/response/predicted.d.ts @@ -1,7 +1,7 @@ import { type ResponseCode, Response } from "../response"; declare const PredictedResponse_base: new (params: { "@DuplojsHttpCore/predicted-response"?: unknown; -}, parentParams: [code: any, information: any, body: any]) => Response & import("@duplojs/utils").Kind, unknown> & import("@duplojs/utils").Kind, unknown>; +}, parentParams: readonly [code: any, information: any, body: any]) => Response & import("@duplojs/utils").Kind, unknown> & import("@duplojs/utils").Kind, unknown>; export declare class PredictedResponse extends PredictedResponse_base { code: GenericCode; information: GenericInformation; diff --git a/docs/libs/v0/core/route/hooks.d.ts b/docs/libs/v0/core/route/hooks.d.ts index 72b996b..99b340e 100644 --- a/docs/libs/v0/core/route/hooks.d.ts +++ b/docs/libs/v0/core/route/hooks.d.ts @@ -20,7 +20,6 @@ export interface RouteHookParams { response(code: GenericCode, information: GenericInformation, body?: GenericBody): HookResponse; } export type HookBeforeRouteExecution = (params: RouteHookParams) => MaybePromise; -export type HookParseBody = (params: RouteHookParams) => MaybePromise; export interface RouteHookErrorParams { readonly request: GenericRequest; readonly error: unknown; @@ -41,7 +40,6 @@ export type HookAfterSendResponse = (p export interface HookRouteLifeCycle { onConstructRequest?: HookOnConstructRequest; beforeRouteExecution?: HookBeforeRouteExecution; - parseBody?: HookParseBody; error?: HookError; beforeSendResponse?: HookBeforeSendResponse; sendResponse?: HookSendResponse; diff --git a/docs/libs/v0/core/route/index.d.ts b/docs/libs/v0/core/route/index.d.ts index 838d21a..c413026 100644 --- a/docs/libs/v0/core/route/index.d.ts +++ b/docs/libs/v0/core/route/index.d.ts @@ -1,5 +1,5 @@ import { type O, type Kind } from "@duplojs/utils"; -import { type RequestMethods } from "../request"; +import { type BodyController, type RequestMethods } from "../request"; import { type ExtractStep, type CheckerStep, type CutStep, type HandlerStep, type ProcessStep, type stepKind, type PresetCheckerStep } from "../steps"; import { type HookRouteLifeCycle } from "./hooks"; import { type Metadata } from "../metadata"; @@ -19,6 +19,7 @@ export interface RouteDefinition { readonly steps: readonly RouteSteps[]; readonly hooks: readonly HookRouteLifeCycle[]; readonly metadata: readonly Metadata[]; + readonly bodyController: BodyController | null; } export declare const routeKind: import("@duplojs/utils").KindHandler>; export interface Route extends Kind { diff --git a/docs/libs/v0/core/router/buildError.d.ts b/docs/libs/v0/core/router/buildError.d.ts index 02b11cd..98d7793 100644 --- a/docs/libs/v0/core/router/buildError.d.ts +++ b/docs/libs/v0/core/router/buildError.d.ts @@ -2,7 +2,7 @@ import { type Route } from "../route"; import { type Steps } from "../steps"; declare const RouterBuildError_base: new (params: { "@DuplojsHttpCore/router-build-error"?: unknown; -}, parentParams: [message?: string | undefined, options?: ErrorOptions | undefined]) => Error & import("@duplojs/utils").Kind, unknown> & import("@duplojs/utils").Kind, unknown>; +}, parentParams: readonly [message?: string | undefined, options?: ErrorOptions | undefined]) => Error & import("@duplojs/utils").Kind, unknown> & import("@duplojs/utils").Kind, unknown>; export declare class RouterBuildError extends RouterBuildError_base { route: Route; element: Route | Steps; diff --git a/docs/libs/v0/core/router/index.cjs b/docs/libs/v0/core/router/index.cjs index 9d12518..81093cd 100644 --- a/docs/libs/v0/core/router/index.cjs +++ b/docs/libs/v0/core/router/index.cjs @@ -6,8 +6,10 @@ var pathToRegExp = require('./pathToRegExp.cjs'); var index = require('../route/index.cjs'); var buildError = require('./buildError.cjs'); require('../functionsBuilders/route/index.cjs'); -require('../functionsBuilders/steps/index.cjs'); var decodeUrl = require('./decodeUrl.cjs'); +var text = require('../request/bodyController/text.cjs'); +var notFoundBodyReaderImplementationError = require('./notFoundBodyReaderImplementationError.cjs'); +require('../functionsBuilders/index.cjs'); require('./types/index.cjs'); var _default = require('../functionsBuilders/route/default.cjs'); var checkerStep = require('../functionsBuilders/steps/defaults/checkerStep.cjs'); @@ -18,18 +20,21 @@ var processStep = require('../functionsBuilders/steps/defaults/processStep.cjs') var hooks = require('../hub/hooks.cjs'); var build = require('../functionsBuilders/route/build.cjs'); -async function buildRouter(inputHub) { - const hub = inputHub - .addRouteFunctionBuilder(_default.defaultRouteFunctionBuilder) - .addStepFunctionBuilder([ +async function buildRouter(hub) { + const { environment } = hub.config; + const { hooksRouteLifeCycle, routes, hooksHubLifeCycle, bodyReaderImplementations, } = hub; + const routeFunctionBuilders = [ + ...hub.routeFunctionBuilders, + _default.defaultRouteFunctionBuilder, + ]; + const stepFunctionBuilders = [ + ...hub.stepFunctionBuilders, checkerStep.defaultCheckerStepFunctionBuilder, cutStep.defaultCutStepFunctionBuilder, handlerStep.defaultHandlerStepFunctionBuilder, extractStep.defaultExtractStepFunctionBuilder, processStep.defaultProcessStepFunctionBuilder, - ]); - const { environment } = hub.config; - const { hooksRouteLifeCycle, routeFunctionBuilders, routes, stepFunctionBuilders, hooksHubLifeCycle, } = hub.aggregates(); + ]; const hooksBeforeBuildRoute = utils.pipe(hooksHubLifeCycle, utils.A.map(({ beforeBuildRoute }) => beforeBuildRoute), utils.A.filter(utils.isType("function"))); const buildParams = { environment, @@ -44,14 +49,23 @@ async function buildRouter(inputHub) { if (utils.E.isLeft(buildedRoute)) { throw new buildError.RouterBuildError(route, utils.unwrap(buildedRoute)); } + const routeBodyController = route.definition.bodyController ?? hub.defaultBodyController; + const bodyReader = utils.pipe(bodyReaderImplementations, utils.A.reduce(utils.A.reduceFrom(null), ({ element, next, exit }) => utils.pipe(element, routeBodyController.tryToCreateReader, utils.E.whenIsRight(exit), utils.E.whenIsLeft(utils.justReturn(next(null)))))); + if (!bodyReader) { + throw new notFoundBodyReaderImplementationError.NotFoundBodyReaderImplementationError(route, routeBodyController); + } return nextWithObject(lastValue, { [route.definition.method]: utils.A.concat(lastValue[route.definition.method] ?? [], utils.A.map(route.definition.paths, utils.O.to({ pattern: pathToRegExp.pathToRegExp, buildedRoute: utils.justReturn(utils.unwrap(buildedRoute)), matchedPath: utils.forward, + bodyReader: utils.justReturn(bodyReader), }))), }); }); + const bodyControllerNotfoundRoute = text.controlBodyAsText(); + const bodyReaderNotFoundRoute = utils.unwrap(bodyControllerNotfoundRoute.tryToCreateReader(text.TextBodyController.createReaderImplementation(() => Promise.resolve(utils.E.error(new Error("Inaccessible body in not found route.")))))); + utils.asserts(bodyReaderNotFoundRoute, utils.isType("object")); const buildedNotfoundRoute = await utils.asyncPipe(index.createRoute({ method: "GET", paths: ["/"], @@ -59,6 +73,7 @@ async function buildRouter(inputHub) { preflightSteps: [], steps: [hub.notfoundHandler], metadata: [], + bodyController: bodyControllerNotfoundRoute, }), async (route) => { const result = await build.buildRouteFunction(route, buildParams); return utils.E.whenIsLeft(result, (element) => { @@ -76,6 +91,7 @@ async function buildRouter(inputHub) { ...decodedUrl, params: {}, matchedPath: null, + bodyReader: bodyReaderNotFoundRoute, })); } // eslint-disable-next-line @typescript-eslint/prefer-for-of @@ -90,6 +106,7 @@ async function buildRouter(inputHub) { ...decodedUrl, params: result.groups ?? {}, matchedPath: routerElement.matchedPath, + bodyReader: routerElement.bodyReader, })); } return buildedNotfoundRoute(new Request({ @@ -97,6 +114,7 @@ async function buildRouter(inputHub) { ...decodedUrl, params: {}, matchedPath: null, + bodyReader: bodyReaderNotFoundRoute, })); }, hooksRouteLifeCycle, @@ -112,4 +130,5 @@ exports.RouterBuildError = buildError.RouterBuildError; exports.decodeUrl = decodeUrl.decodeUrl; exports.regexQueryAnalyser = decodeUrl.regexQueryAnalyser; exports.regexUrlAnalyser = decodeUrl.regexUrlAnalyser; +exports.NotFoundBodyReaderImplementationError = notFoundBodyReaderImplementationError.NotFoundBodyReaderImplementationError; exports.buildRouter = buildRouter; diff --git a/docs/libs/v0/core/router/index.d.ts b/docs/libs/v0/core/router/index.d.ts index 44030b9..3fae40c 100644 --- a/docs/libs/v0/core/router/index.d.ts +++ b/docs/libs/v0/core/router/index.d.ts @@ -4,4 +4,5 @@ export * from "./types"; export * from "./pathToRegExp"; export * from "./buildError"; export * from "./decodeUrl"; -export declare function buildRouter(inputHub: Hub): Promise; +export * from "./notFoundBodyReaderImplementationError"; +export declare function buildRouter(hub: Hub): Promise; diff --git a/docs/libs/v0/core/router/index.mjs b/docs/libs/v0/core/router/index.mjs index c0b00a1..89a71d6 100644 --- a/docs/libs/v0/core/router/index.mjs +++ b/docs/libs/v0/core/router/index.mjs @@ -1,12 +1,14 @@ import '../hub/index.mjs'; -import { pipe, A, isType, G, E, unwrap, O, forward, justReturn, asyncPipe } from '@duplojs/utils'; +import { pipe, A, isType, G, E, unwrap, justReturn, O, forward, asserts, asyncPipe } from '@duplojs/utils'; import { pathToRegExp } from './pathToRegExp.mjs'; import { createRoute } from '../route/index.mjs'; import { RouterBuildError } from './buildError.mjs'; import '../functionsBuilders/route/index.mjs'; -import '../functionsBuilders/steps/index.mjs'; import { decodeUrl } from './decodeUrl.mjs'; export { regexQueryAnalyser, regexUrlAnalyser } from './decodeUrl.mjs'; +import { controlBodyAsText, TextBodyController } from '../request/bodyController/text.mjs'; +import { NotFoundBodyReaderImplementationError } from './notFoundBodyReaderImplementationError.mjs'; +import '../functionsBuilders/index.mjs'; import './types/index.mjs'; import { defaultRouteFunctionBuilder } from '../functionsBuilders/route/default.mjs'; import { defaultCheckerStepFunctionBuilder } from '../functionsBuilders/steps/defaults/checkerStep.mjs'; @@ -17,18 +19,21 @@ import { defaultProcessStepFunctionBuilder } from '../functionsBuilders/steps/de import { launchHookBeforeBuildRoute } from '../hub/hooks.mjs'; import { buildRouteFunction } from '../functionsBuilders/route/build.mjs'; -async function buildRouter(inputHub) { - const hub = inputHub - .addRouteFunctionBuilder(defaultRouteFunctionBuilder) - .addStepFunctionBuilder([ +async function buildRouter(hub) { + const { environment } = hub.config; + const { hooksRouteLifeCycle, routes, hooksHubLifeCycle, bodyReaderImplementations, } = hub; + const routeFunctionBuilders = [ + ...hub.routeFunctionBuilders, + defaultRouteFunctionBuilder, + ]; + const stepFunctionBuilders = [ + ...hub.stepFunctionBuilders, defaultCheckerStepFunctionBuilder, defaultCutStepFunctionBuilder, defaultHandlerStepFunctionBuilder, defaultExtractStepFunctionBuilder, defaultProcessStepFunctionBuilder, - ]); - const { environment } = hub.config; - const { hooksRouteLifeCycle, routeFunctionBuilders, routes, stepFunctionBuilders, hooksHubLifeCycle, } = hub.aggregates(); + ]; const hooksBeforeBuildRoute = pipe(hooksHubLifeCycle, A.map(({ beforeBuildRoute }) => beforeBuildRoute), A.filter(isType("function"))); const buildParams = { environment, @@ -43,14 +48,23 @@ async function buildRouter(inputHub) { if (E.isLeft(buildedRoute)) { throw new RouterBuildError(route, unwrap(buildedRoute)); } + const routeBodyController = route.definition.bodyController ?? hub.defaultBodyController; + const bodyReader = pipe(bodyReaderImplementations, A.reduce(A.reduceFrom(null), ({ element, next, exit }) => pipe(element, routeBodyController.tryToCreateReader, E.whenIsRight(exit), E.whenIsLeft(justReturn(next(null)))))); + if (!bodyReader) { + throw new NotFoundBodyReaderImplementationError(route, routeBodyController); + } return nextWithObject(lastValue, { [route.definition.method]: A.concat(lastValue[route.definition.method] ?? [], A.map(route.definition.paths, O.to({ pattern: pathToRegExp, buildedRoute: justReturn(unwrap(buildedRoute)), matchedPath: forward, + bodyReader: justReturn(bodyReader), }))), }); }); + const bodyControllerNotfoundRoute = controlBodyAsText(); + const bodyReaderNotFoundRoute = unwrap(bodyControllerNotfoundRoute.tryToCreateReader(TextBodyController.createReaderImplementation(() => Promise.resolve(E.error(new Error("Inaccessible body in not found route.")))))); + asserts(bodyReaderNotFoundRoute, isType("object")); const buildedNotfoundRoute = await asyncPipe(createRoute({ method: "GET", paths: ["/"], @@ -58,6 +72,7 @@ async function buildRouter(inputHub) { preflightSteps: [], steps: [hub.notfoundHandler], metadata: [], + bodyController: bodyControllerNotfoundRoute, }), async (route) => { const result = await buildRouteFunction(route, buildParams); return E.whenIsLeft(result, (element) => { @@ -75,6 +90,7 @@ async function buildRouter(inputHub) { ...decodedUrl, params: {}, matchedPath: null, + bodyReader: bodyReaderNotFoundRoute, })); } // eslint-disable-next-line @typescript-eslint/prefer-for-of @@ -89,6 +105,7 @@ async function buildRouter(inputHub) { ...decodedUrl, params: result.groups ?? {}, matchedPath: routerElement.matchedPath, + bodyReader: routerElement.bodyReader, })); } return buildedNotfoundRoute(new Request({ @@ -96,6 +113,7 @@ async function buildRouter(inputHub) { ...decodedUrl, params: {}, matchedPath: null, + bodyReader: bodyReaderNotFoundRoute, })); }, hooksRouteLifeCycle, @@ -106,4 +124,4 @@ async function buildRouter(inputHub) { }; } -export { RouterBuildError, buildRouter, decodeUrl, pathToRegExp }; +export { NotFoundBodyReaderImplementationError, RouterBuildError, buildRouter, decodeUrl, pathToRegExp }; diff --git a/docs/libs/v0/core/router/notFoundBodyReaderImplementationError.cjs b/docs/libs/v0/core/router/notFoundBodyReaderImplementationError.cjs new file mode 100644 index 0000000..b6d6db1 --- /dev/null +++ b/docs/libs/v0/core/router/notFoundBodyReaderImplementationError.cjs @@ -0,0 +1,16 @@ +'use strict'; + +var kind = require('../kind.cjs'); +var utils = require('@duplojs/utils'); + +class NotFoundBodyReaderImplementationError extends utils.kindHeritage("not-found-body-reader-implementation-error", kind.createCoreLibKind("not-found-body-reader-implementation-error"), Error) { + route; + bodyController; + constructor(route, bodyController) { + super({}, ["Body reader implementation not found."]); + this.route = route; + this.bodyController = bodyController; + } +} + +exports.NotFoundBodyReaderImplementationError = NotFoundBodyReaderImplementationError; diff --git a/docs/libs/v0/core/router/notFoundBodyReaderImplementationError.d.ts b/docs/libs/v0/core/router/notFoundBodyReaderImplementationError.d.ts new file mode 100644 index 0000000..7f47aba --- /dev/null +++ b/docs/libs/v0/core/router/notFoundBodyReaderImplementationError.d.ts @@ -0,0 +1,11 @@ +import { type BodyController } from "../request"; +import { type Route } from "../route"; +declare const NotFoundBodyReaderImplementationError_base: new (params: { + "@DuplojsHttpCore/not-found-body-reader-implementation-error"?: unknown; +}, parentParams: readonly [message?: string | undefined, options?: ErrorOptions | undefined]) => Error & import("@duplojs/utils").Kind, unknown> & import("@duplojs/utils").Kind, unknown>; +export declare class NotFoundBodyReaderImplementationError extends NotFoundBodyReaderImplementationError_base { + route: Route; + bodyController: BodyController; + constructor(route: Route, bodyController: BodyController); +} +export {}; diff --git a/docs/libs/v0/core/router/notFoundBodyReaderImplementationError.mjs b/docs/libs/v0/core/router/notFoundBodyReaderImplementationError.mjs new file mode 100644 index 0000000..e531b97 --- /dev/null +++ b/docs/libs/v0/core/router/notFoundBodyReaderImplementationError.mjs @@ -0,0 +1,14 @@ +import { createCoreLibKind } from '../kind.mjs'; +import { kindHeritage } from '@duplojs/utils'; + +class NotFoundBodyReaderImplementationError extends kindHeritage("not-found-body-reader-implementation-error", createCoreLibKind("not-found-body-reader-implementation-error"), Error) { + route; + bodyController; + constructor(route, bodyController) { + super({}, ["Body reader implementation not found."]); + this.route = route; + this.bodyController = bodyController; + } +} + +export { NotFoundBodyReaderImplementationError }; diff --git a/docs/libs/v0/core/router/types/buildedRouter.d.ts b/docs/libs/v0/core/router/types/buildedRouter.d.ts index 43783bc..ba0162e 100644 --- a/docs/libs/v0/core/router/types/buildedRouter.d.ts +++ b/docs/libs/v0/core/router/types/buildedRouter.d.ts @@ -1,11 +1,11 @@ import { type createStepFunctionBuilder, type createRouteFunctionBuilder } from "../../functionsBuilders"; import { type HookHubLifeCycle } from "../../hub"; import { type RequestInitializationData } from "../../request"; -import { type HookRouteLifeCycle, type Route, type RouteDefinition } from "../../route"; -export type RouterInitializationData = Omit; +import { type HookRouteLifeCycle, type Route } from "../../route"; +export type RouterInitializationData = Omit; export interface BuildedRouter { exec(initializationData: RouterInitializationData): Promise; - readonly routes: readonly Route[]; + readonly routes: ReadonlySet; readonly hooksRouteLifeCycle: readonly HookRouteLifeCycle[]; readonly routeFunctionBuilders: readonly ReturnType[]; readonly stepFunctionBuilders: readonly ReturnType[]; diff --git a/docs/libs/v0/core/steps/extract.d.ts b/docs/libs/v0/core/steps/extract.d.ts index e7eff8b..6378b26 100644 --- a/docs/libs/v0/core/steps/extract.d.ts +++ b/docs/libs/v0/core/steps/extract.d.ts @@ -6,7 +6,9 @@ import { type Metadata } from "../metadata"; export interface DisabledExtractKeysCustom { } export type DisabledExtractKeys = O.GetPropsWithValue; -export type ExtractShape = Partial | DisabledExtractKeys>, DP.DataParser | Record>>; +export type ExtractShape = Partial | DisabledExtractKeys | "body" | "bodyReader" | symbol>, DP.DataParser | Record> & { + body: (DP.DataParser | Record); +}>; export interface ExtractStepDefinition { readonly shape: ExtractShape; readonly responseContract?: ResponseContract.Contract; diff --git a/docs/libs/v0/core/steps/types/steps.d.ts b/docs/libs/v0/core/steps/types/steps.d.ts index 3fb1ca6..12b373f 100644 --- a/docs/libs/v0/core/steps/types/steps.d.ts +++ b/docs/libs/v0/core/steps/types/steps.d.ts @@ -1,11 +1,9 @@ -import { type Kind, type O } from "@duplojs/utils"; import { type CheckerStep } from "../checker"; import { type CutStep } from "../cut"; import { type ExtractStep } from "../extract"; import { type HandlerStep } from "../handler"; import { type PresetCheckerStep } from "../presetChecker"; import { type ProcessStep } from "../process"; -import { type stepKind } from "../kind"; export interface StepsCustom { } -export type Steps = (StepsCustom[O.GetPropsWithValueExtends>] | CheckerStep | CutStep | ExtractStep | HandlerStep | PresetCheckerStep | ProcessStep); +export type Steps = (StepsCustom[keyof StepsCustom] | CheckerStep | CutStep | ExtractStep | HandlerStep | PresetCheckerStep | ProcessStep); diff --git a/docs/libs/v0/core/types/hosts.cjs b/docs/libs/v0/core/types/hosts.cjs new file mode 100644 index 0000000..eb109ab --- /dev/null +++ b/docs/libs/v0/core/types/hosts.cjs @@ -0,0 +1,2 @@ +'use strict'; + diff --git a/docs/libs/v0/core/types/hosts.d.ts b/docs/libs/v0/core/types/hosts.d.ts new file mode 100644 index 0000000..b8430ec --- /dev/null +++ b/docs/libs/v0/core/types/hosts.d.ts @@ -0,0 +1,4 @@ +import { type O } from "@duplojs/utils"; +export interface HostCustom { +} +export type Hosts = O.GetPropsWithValue; diff --git a/docs/libs/v0/core/types/hosts.mjs b/docs/libs/v0/core/types/hosts.mjs new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/docs/libs/v0/core/types/hosts.mjs @@ -0,0 +1 @@ + diff --git a/docs/libs/v0/core/types/httpServerParams.cjs b/docs/libs/v0/core/types/httpServerParams.cjs new file mode 100644 index 0000000..eb109ab --- /dev/null +++ b/docs/libs/v0/core/types/httpServerParams.cjs @@ -0,0 +1,2 @@ +'use strict'; + diff --git a/docs/libs/v0/core/types/httpServerParams.d.ts b/docs/libs/v0/core/types/httpServerParams.d.ts new file mode 100644 index 0000000..1604cf7 --- /dev/null +++ b/docs/libs/v0/core/types/httpServerParams.d.ts @@ -0,0 +1,11 @@ +import { type BytesInString } from "@duplojs/utils"; +import { type Hosts } from "./hosts"; +export interface HttpServerParams { + readonly host: Hosts; + readonly port: number; + readonly maxBodySize: BytesInString | number; + readonly informationHeaderKey: string; + readonly predictedHeaderKey: string; + readonly fromHookHeaderKey: string; + readonly uploadFolder: string; +} diff --git a/docs/libs/v0/core/types/httpServerParams.mjs b/docs/libs/v0/core/types/httpServerParams.mjs new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/docs/libs/v0/core/types/httpServerParams.mjs @@ -0,0 +1 @@ + diff --git a/docs/libs/v0/core/types/index.cjs b/docs/libs/v0/core/types/index.cjs index 963239b..28e7baf 100644 --- a/docs/libs/v0/core/types/index.cjs +++ b/docs/libs/v0/core/types/index.cjs @@ -2,4 +2,6 @@ require('./environment.cjs'); require('./forbiddenBigintDataParser.cjs'); +require('./httpServerParams.cjs'); +require('./hosts.cjs'); diff --git a/docs/libs/v0/core/types/index.d.ts b/docs/libs/v0/core/types/index.d.ts index 1dea773..3220226 100644 --- a/docs/libs/v0/core/types/index.d.ts +++ b/docs/libs/v0/core/types/index.d.ts @@ -1,2 +1,4 @@ export * from "./environment"; export * from "./forbiddenBigintDataParser"; +export * from "./httpServerParams"; +export * from "./hosts"; diff --git a/docs/libs/v0/core/types/index.mjs b/docs/libs/v0/core/types/index.mjs index a0f3c8c..e524262 100644 --- a/docs/libs/v0/core/types/index.mjs +++ b/docs/libs/v0/core/types/index.mjs @@ -1,2 +1,4 @@ import './environment.mjs'; import './forbiddenBigintDataParser.mjs'; +import './httpServerParams.mjs'; +import './hosts.mjs'; diff --git a/docs/libs/v0/interfaces/bun/types/request.cjs b/docs/libs/v0/interfaces/bun/types/request.cjs index c045973..0841450 100644 --- a/docs/libs/v0/interfaces/bun/types/request.cjs +++ b/docs/libs/v0/interfaces/bun/types/request.cjs @@ -1,5 +1,5 @@ 'use strict'; -require('../../../core/request.cjs'); +require('../../../core/request/index.cjs'); require('../../../core/steps/index.cjs'); diff --git a/docs/libs/v0/interfaces/bun/types/request.mjs b/docs/libs/v0/interfaces/bun/types/request.mjs index d121cb6..8eaee87 100644 --- a/docs/libs/v0/interfaces/bun/types/request.mjs +++ b/docs/libs/v0/interfaces/bun/types/request.mjs @@ -1,2 +1,2 @@ -import '../../../core/request.mjs'; +import '../../../core/request/index.mjs'; import '../../../core/steps/index.mjs'; diff --git a/docs/libs/v0/interfaces/node/bodyReaders/formData/error.cjs b/docs/libs/v0/interfaces/node/bodyReaders/formData/error.cjs new file mode 100644 index 0000000..ac85c20 --- /dev/null +++ b/docs/libs/v0/interfaces/node/bodyReaders/formData/error.cjs @@ -0,0 +1,14 @@ +'use strict'; + +var utils = require('@duplojs/utils'); +var kind = require('../../kind.cjs'); + +class BodyParseFormDataError extends utils.kindHeritage("body-parse-form-data-error", kind.createInterfacesNodeLibKind("body-parse-form-data-error"), Error) { + information; + constructor(information) { + super({}, [`Body parse form date error: ${information}`]); + this.information = information; + } +} + +exports.BodyParseFormDataError = BodyParseFormDataError; diff --git a/docs/libs/v0/interfaces/node/bodyReaders/formData/error.d.ts b/docs/libs/v0/interfaces/node/bodyReaders/formData/error.d.ts new file mode 100644 index 0000000..4ad568e --- /dev/null +++ b/docs/libs/v0/interfaces/node/bodyReaders/formData/error.d.ts @@ -0,0 +1,8 @@ +declare const BodyParseFormDataError_base: new (params: { + "@DuplojsHttpInterfacesNode/body-parse-form-data-error"?: unknown; +}, parentParams: readonly [message?: string | undefined, options?: ErrorOptions | undefined]) => Error & import("@duplojs/utils").Kind, unknown> & import("@duplojs/utils").Kind, unknown>; +export declare class BodyParseFormDataError extends BodyParseFormDataError_base { + information: string; + constructor(information: string); +} +export {}; diff --git a/docs/libs/v0/interfaces/node/bodyReaders/formData/error.mjs b/docs/libs/v0/interfaces/node/bodyReaders/formData/error.mjs new file mode 100644 index 0000000..1079e07 --- /dev/null +++ b/docs/libs/v0/interfaces/node/bodyReaders/formData/error.mjs @@ -0,0 +1,12 @@ +import { kindHeritage } from '@duplojs/utils'; +import { createInterfacesNodeLibKind } from '../../kind.mjs'; + +class BodyParseFormDataError extends kindHeritage("body-parse-form-data-error", createInterfacesNodeLibKind("body-parse-form-data-error"), Error) { + information; + constructor(information) { + super({}, [`Body parse form date error: ${information}`]); + this.information = information; + } +} + +export { BodyParseFormDataError }; diff --git a/docs/libs/v0/interfaces/node/bodyReaders/formData/index.cjs b/docs/libs/v0/interfaces/node/bodyReaders/formData/index.cjs new file mode 100644 index 0000000..27456c3 --- /dev/null +++ b/docs/libs/v0/interfaces/node/bodyReaders/formData/index.cjs @@ -0,0 +1,94 @@ +'use strict'; + +require('../../../../core/request/index.cjs'); +var serverUtils = require('@duplojs/server-utils'); +var utils = require('@duplojs/utils'); +var readRequestFormData = require('./readRequestFormData.cjs'); +var node_fs = require('node:fs'); +require('../../../../core/errors/index.cjs'); +var error = require('./error.cjs'); +var formData = require('../../../../core/request/bodyController/formData.cjs'); +var wrongContentTypeError = require('../../../../core/errors/wrongContentTypeError.cjs'); + +function createFormDataBodyReaderImplementation(serverParams) { + const serverMaxBodySize = utils.stringToBytes(serverParams.maxBodySize); + function addValue(mapResult, fieldName, newValue) { + const value = mapResult.get(fieldName); + if (value === undefined) { + mapResult.set(fieldName, newValue); + } + else { + mapResult.set(fieldName, utils.A.push(utils.A.coalescing(value), newValue)); + } + } + return formData.FormDataBodyController.createReaderImplementation(async (request, params) => { + if (!request.headers["content-type"]?.includes("multipart/form-data")) { + return utils.E.error(new wrongContentTypeError.WrongContentTypeError("multipart/form-data", utils.A.join(utils.A.coalescing(request.headers["content-type"] ?? ""), " "))); + } + const filesAttache = []; + request.filesAttache = filesAttache; + const result = await readRequestFormData.readRequestFormData(request.raw.request, new Map(), { + maxBodySize: params.bodyMaxSize ?? serverMaxBodySize, + fileMaxSize: params.fileMaxSize ?? Infinity, + maxFileQuantity: params.maxFileQuantity, + mimeType: params.mimeType, + maxBufferSize: params.maxBufferSize, + maxKeyLength: params.maxKeyLength, + }, (header) => { + const fieldName = header.name; + if (header.filename) { + const extension = utils.Path.getExtensionName(header.filename); + const displayExtension = extension ? `.${extension}` : ""; + const filePath = utils.Path.resolveRelative([ + serverParams.uploadFolder, + `${Math.random().toString(36).slice(2, 10)}-${Date.now()}${displayExtension}`, + ]); + filesAttache.push(filePath); + const currentFile = node_fs.createWriteStream(filePath, { + highWaterMark: request.raw.request.readableHighWaterMark, + }); + return { + onReceiveChunk: (chunk) => new Promise((resolve, reject) => void currentFile.write(chunk, (result) => { + if (result instanceof Error) { + return void reject(result); + } + return void resolve(); + })), + onEndPart: (valueAccumulator) => { + currentFile.end(); + addValue(valueAccumulator, fieldName, serverUtils.SF.createFileInterface(currentFile.path.toString())); + return valueAccumulator; + }, + onError: () => void currentFile.end(), + }; + } + let currentValue = ""; + return { + onReceiveChunk: (chunk) => { + currentValue += chunk.toString("utf-8"); + }, + onEndPart: (valueAccumulator) => { + addValue(valueAccumulator, fieldName, currentValue); + return valueAccumulator; + }, + onError: null, + }; + }); + if (utils.E.isLeft(result)) { + // mandatory in case of error to avoid monopolizing the client connection if a stream is not finished. + request.raw.response.setHeader("Connection", "close"); + if (utils.E.hasInformation(result, "server-error")) { + throw utils.unwrap(result); + } + return result; + } + if (request.headers["content-type-options"]?.includes("advanced")) { + return utils.E.success(utils.TheFormData.fromEntries(result.entries(), params.maxIndexArray)); + } + return utils.E.success(utils.O.fromEntries(result.entries())); + }); +} + +exports.readRequestFormData = readRequestFormData.readRequestFormData; +exports.BodyParseFormDataError = error.BodyParseFormDataError; +exports.createFormDataBodyReaderImplementation = createFormDataBodyReaderImplementation; diff --git a/docs/libs/v0/interfaces/node/bodyReaders/formData/index.d.ts b/docs/libs/v0/interfaces/node/bodyReaders/formData/index.d.ts new file mode 100644 index 0000000..9bb9a22 --- /dev/null +++ b/docs/libs/v0/interfaces/node/bodyReaders/formData/index.d.ts @@ -0,0 +1,4 @@ +import { type HttpServerParams } from "../../../../core/types"; +export * from "./error"; +export * from "./readRequestFormData"; +export declare function createFormDataBodyReaderImplementation(serverParams: HttpServerParams): import("../../../../core/request").BodyReaderImplementation<"formData", import("../../../../core/request").FormDataBodyReaderParams>; diff --git a/docs/libs/v0/interfaces/node/bodyReaders/formData/index.mjs b/docs/libs/v0/interfaces/node/bodyReaders/formData/index.mjs new file mode 100644 index 0000000..e7d6abc --- /dev/null +++ b/docs/libs/v0/interfaces/node/bodyReaders/formData/index.mjs @@ -0,0 +1,90 @@ +import '../../../../core/request/index.mjs'; +import { SF } from '@duplojs/server-utils'; +import { stringToBytes, A, E, Path, unwrap, TheFormData, O } from '@duplojs/utils'; +import { readRequestFormData } from './readRequestFormData.mjs'; +import { createWriteStream } from 'node:fs'; +import '../../../../core/errors/index.mjs'; +export { BodyParseFormDataError } from './error.mjs'; +import { FormDataBodyController } from '../../../../core/request/bodyController/formData.mjs'; +import { WrongContentTypeError } from '../../../../core/errors/wrongContentTypeError.mjs'; + +function createFormDataBodyReaderImplementation(serverParams) { + const serverMaxBodySize = stringToBytes(serverParams.maxBodySize); + function addValue(mapResult, fieldName, newValue) { + const value = mapResult.get(fieldName); + if (value === undefined) { + mapResult.set(fieldName, newValue); + } + else { + mapResult.set(fieldName, A.push(A.coalescing(value), newValue)); + } + } + return FormDataBodyController.createReaderImplementation(async (request, params) => { + if (!request.headers["content-type"]?.includes("multipart/form-data")) { + return E.error(new WrongContentTypeError("multipart/form-data", A.join(A.coalescing(request.headers["content-type"] ?? ""), " "))); + } + const filesAttache = []; + request.filesAttache = filesAttache; + const result = await readRequestFormData(request.raw.request, new Map(), { + maxBodySize: params.bodyMaxSize ?? serverMaxBodySize, + fileMaxSize: params.fileMaxSize ?? Infinity, + maxFileQuantity: params.maxFileQuantity, + mimeType: params.mimeType, + maxBufferSize: params.maxBufferSize, + maxKeyLength: params.maxKeyLength, + }, (header) => { + const fieldName = header.name; + if (header.filename) { + const extension = Path.getExtensionName(header.filename); + const displayExtension = extension ? `.${extension}` : ""; + const filePath = Path.resolveRelative([ + serverParams.uploadFolder, + `${Math.random().toString(36).slice(2, 10)}-${Date.now()}${displayExtension}`, + ]); + filesAttache.push(filePath); + const currentFile = createWriteStream(filePath, { + highWaterMark: request.raw.request.readableHighWaterMark, + }); + return { + onReceiveChunk: (chunk) => new Promise((resolve, reject) => void currentFile.write(chunk, (result) => { + if (result instanceof Error) { + return void reject(result); + } + return void resolve(); + })), + onEndPart: (valueAccumulator) => { + currentFile.end(); + addValue(valueAccumulator, fieldName, SF.createFileInterface(currentFile.path.toString())); + return valueAccumulator; + }, + onError: () => void currentFile.end(), + }; + } + let currentValue = ""; + return { + onReceiveChunk: (chunk) => { + currentValue += chunk.toString("utf-8"); + }, + onEndPart: (valueAccumulator) => { + addValue(valueAccumulator, fieldName, currentValue); + return valueAccumulator; + }, + onError: null, + }; + }); + if (E.isLeft(result)) { + // mandatory in case of error to avoid monopolizing the client connection if a stream is not finished. + request.raw.response.setHeader("Connection", "close"); + if (E.hasInformation(result, "server-error")) { + throw unwrap(result); + } + return result; + } + if (request.headers["content-type-options"]?.includes("advanced")) { + return E.success(TheFormData.fromEntries(result.entries(), params.maxIndexArray)); + } + return E.success(O.fromEntries(result.entries())); + }); +} + +export { createFormDataBodyReaderImplementation, readRequestFormData }; diff --git a/docs/libs/v0/interfaces/node/bodyReaders/formData/readRequestFormData.cjs b/docs/libs/v0/interfaces/node/bodyReaders/formData/readRequestFormData.cjs new file mode 100644 index 0000000..bb4e8a5 --- /dev/null +++ b/docs/libs/v0/interfaces/node/bodyReaders/formData/readRequestFormData.cjs @@ -0,0 +1,175 @@ +'use strict'; + +var utils = require('@duplojs/utils'); +var error = require('./error.cjs'); +require('../../../../core/errors/index.cjs'); +var bodySizeExceedsLimitError = require('../../../../core/errors/bodySizeExceedsLimitError.cjs'); +var bodyParseWrongChunkReceived = require('../../../../core/errors/bodyParseWrongChunkReceived.cjs'); + +const endHeaderPart = Buffer.from("\r\n\r\n"); +const bufferStart = Buffer.from("\r\n"); +const regexBoundary = /boundary=(?[^; ]+)/i; +const regexHeaderPart = /name="(?(?:\\"|[^"])+)"(?:; filename="(?(?:\\"|[^"])+)")?(?:;\s+filename\*=[^']+'[^']*'(?[^;\r\n\s]+))?/i; +function safeDecode(value) { + try { + return decodeURIComponent(value); + } + catch { + return value; + } +} +async function readRequestFormData(request, firstValueAccumulator, params, onReceiveHeader) { + const boundary = utils.S.extract(request.headers["content-type"] ?? "", regexBoundary)?.namedGroups?.boundary; + if (!boundary) { + return utils.E.error(new error.BodyParseFormDataError("Wrong boundary.")); + } + let valueAccumulator = firstValueAccumulator; + const startPart = Buffer.from(`\r\n--${boundary}`); + const endMultiPart = Buffer.from(`\r\n--${boundary}--`); + let currentBuffer = bufferStart; + let size = 0; + const keep = endMultiPart.length - 1; + let currentStream = undefined; + let fileQuantity = 0; + let currentFileSize = undefined; + const checkSize = (receivedChunk) => { + size += receivedChunk.length; + return size > params.maxBodySize + ? new bodySizeExceedsLimitError.BodySizeExceedsLimitError(params.maxBodySize) + : true; + }; + const flushReceiveHeader = async (headerPart) => { + valueAccumulator = await currentStream?.onEndPart(valueAccumulator) ?? valueAccumulator; + const sizeResult = checkSize(headerPart); + if (sizeResult !== true) { + return sizeResult; + } + const extract = utils.S.extract(headerPart.toString("utf-8"), regexHeaderPart)?.namedGroups; + const header = extract?.name + ? { + name: extract.name.trim(), + filename: (extract.encodedFilename !== undefined + ? safeDecode(extract.encodedFilename) + : extract.filename)?.trim(), + } + : null; + if (!header) { + return new error.BodyParseFormDataError("Bad content header part."); + } + if (header.name.length > params.maxKeyLength) { + return new error.BodyParseFormDataError("key length exceeds limit."); + } + if (header.filename !== undefined) { + currentFileSize = 0; + fileQuantity++; + if (fileQuantity > params.maxFileQuantity) { + return new error.BodyParseFormDataError("File quantity exceeds limit."); + } + else if (params.mimeType !== undefined + && !params.mimeType.test(utils.Path.getExtensionName(header.filename) ?? "")) { + return new error.BodyParseFormDataError("File have wrong mimeType."); + } + } + else { + currentFileSize = undefined; + } + const newStream = await onReceiveHeader(header); + if (newStream instanceof Error) { + return newStream; + } + currentStream = newStream; + return true; + }; + const flushReceiveChunk = async (chunk) => { + if (chunk.length === 0) { + return true; + } + const sizeResult = checkSize(chunk); + if (sizeResult !== true) { + return sizeResult; + } + if (!currentStream) { + return new error.BodyParseFormDataError("Receive chunk before header part."); + } + if (typeof currentFileSize === "number") { + currentFileSize += chunk.length; + if (params.fileMaxSize !== undefined && currentFileSize > params.fileMaxSize) { + return new error.BodyParseFormDataError("File size exceeds limit."); + } + } + await currentStream.onReceiveChunk(chunk); + return true; + }; + const treatError = async (error) => { + await currentStream?.onError?.(error, valueAccumulator); + return utils.E.error(error); + }; + try { + for await (const chunk of request) { + if (!(chunk instanceof Buffer)) { + return await treatError(new bodyParseWrongChunkReceived.BodyParseWrongChunkReceived("Buffer.", chunk)); + } + currentBuffer = Buffer.concat([currentBuffer, chunk]); + if (currentBuffer.length > params.maxBufferSize) { + return await treatError(new error.BodyParseFormDataError("Buffer size exceeds limit.")); + } + while (true) { + const endMultiPartIndex = currentBuffer.indexOf(endMultiPart); + if (endMultiPartIndex !== -1) { + // check if buffer contain end of transmissions + currentBuffer = currentBuffer.subarray(0, endMultiPartIndex); + } + const startPartIndex = currentBuffer.indexOf(startPart); + const endHeaderPartIndex = currentBuffer.indexOf(endHeaderPart); + if (startPartIndex !== -1 && endHeaderPartIndex !== -1) { + // check if buffer contain an entire header of part + const resultChunk = await flushReceiveChunk(currentBuffer.subarray(0, startPartIndex)); + if (resultChunk !== true) { + return await treatError(resultChunk); + } + const endIndex = endHeaderPartIndex + endHeaderPart.length; + const resultHeader = await flushReceiveHeader(currentBuffer.subarray(startPartIndex, endIndex)); + if (resultHeader !== true) { + return await treatError(resultHeader); + } + currentBuffer = currentBuffer.subarray(endIndex); + } + else if (startPartIndex === -1 && endHeaderPartIndex === -1) { + // check if buffer contain only data + if (currentBuffer.length > keep) { + const bufferRestIndex = currentBuffer.length - keep; + const resultChunk = await flushReceiveChunk(currentBuffer.subarray(0, bufferRestIndex)); + if (resultChunk !== true) { + return await treatError(resultChunk); + } + currentBuffer = currentBuffer.subarray(bufferRestIndex); + } + break; + } + else if (startPartIndex !== -1 && endHeaderPartIndex === -1) { + // check if buffer contain start of header but not contain end + break; + } + else { + // check if buffer contain only end of header part + return await treatError(new error.BodyParseFormDataError("Wrong content.")); + } + } + } + const resultChunk = await flushReceiveChunk(currentBuffer); + if (resultChunk !== true) { + return await treatError(resultChunk); + } + valueAccumulator = await currentStream?.onEndPart(valueAccumulator) ?? valueAccumulator; + return valueAccumulator; + } + catch (error) { + await currentStream?.onError?.(error, valueAccumulator); + return utils.E.left("server-error", error); + } + finally { + request.destroy(); + } +} + +exports.readRequestFormData = readRequestFormData; diff --git a/docs/libs/v0/interfaces/node/bodyReaders/formData/readRequestFormData.d.ts b/docs/libs/v0/interfaces/node/bodyReaders/formData/readRequestFormData.d.ts new file mode 100644 index 0000000..468c82b --- /dev/null +++ b/docs/libs/v0/interfaces/node/bodyReaders/formData/readRequestFormData.d.ts @@ -0,0 +1,21 @@ +import { E, type MaybePromise } from "@duplojs/utils"; +import type http from "http"; +interface HeaderPartInformation { + name: string; + filename?: string; +} +export interface ReadRequestFormDataStreamChunkEvent { + onReceiveChunk(chunk: Buffer): MaybePromise; + onEndPart(valueAccumulator: GenericValueAccumulator): MaybePromise; + onError: ((error: unknown, valueAccumulator: GenericValueAccumulator) => MaybePromise) | null; +} +export interface ReadRequestFormDataParams { + maxBodySize: number; + maxFileQuantity: number; + maxBufferSize: number; + maxKeyLength: number; + fileMaxSize?: number; + mimeType?: RegExp; +} +export declare function readRequestFormData(request: http.IncomingMessage, firstValueAccumulator: GenericValueAccumulator, params: ReadRequestFormDataParams, onReceiveHeader: (header: HeaderPartInformation) => MaybePromise | Error>): Promise | GenericOutputHeader | E.Error | GenericValueAccumulator>; +export {}; diff --git a/docs/libs/v0/interfaces/node/bodyReaders/formData/readRequestFormData.mjs b/docs/libs/v0/interfaces/node/bodyReaders/formData/readRequestFormData.mjs new file mode 100644 index 0000000..d70fa7d --- /dev/null +++ b/docs/libs/v0/interfaces/node/bodyReaders/formData/readRequestFormData.mjs @@ -0,0 +1,173 @@ +import { S, E, Path } from '@duplojs/utils'; +import { BodyParseFormDataError } from './error.mjs'; +import '../../../../core/errors/index.mjs'; +import { BodySizeExceedsLimitError } from '../../../../core/errors/bodySizeExceedsLimitError.mjs'; +import { BodyParseWrongChunkReceived } from '../../../../core/errors/bodyParseWrongChunkReceived.mjs'; + +const endHeaderPart = Buffer.from("\r\n\r\n"); +const bufferStart = Buffer.from("\r\n"); +const regexBoundary = /boundary=(?[^; ]+)/i; +const regexHeaderPart = /name="(?(?:\\"|[^"])+)"(?:; filename="(?(?:\\"|[^"])+)")?(?:;\s+filename\*=[^']+'[^']*'(?[^;\r\n\s]+))?/i; +function safeDecode(value) { + try { + return decodeURIComponent(value); + } + catch { + return value; + } +} +async function readRequestFormData(request, firstValueAccumulator, params, onReceiveHeader) { + const boundary = S.extract(request.headers["content-type"] ?? "", regexBoundary)?.namedGroups?.boundary; + if (!boundary) { + return E.error(new BodyParseFormDataError("Wrong boundary.")); + } + let valueAccumulator = firstValueAccumulator; + const startPart = Buffer.from(`\r\n--${boundary}`); + const endMultiPart = Buffer.from(`\r\n--${boundary}--`); + let currentBuffer = bufferStart; + let size = 0; + const keep = endMultiPart.length - 1; + let currentStream = undefined; + let fileQuantity = 0; + let currentFileSize = undefined; + const checkSize = (receivedChunk) => { + size += receivedChunk.length; + return size > params.maxBodySize + ? new BodySizeExceedsLimitError(params.maxBodySize) + : true; + }; + const flushReceiveHeader = async (headerPart) => { + valueAccumulator = await currentStream?.onEndPart(valueAccumulator) ?? valueAccumulator; + const sizeResult = checkSize(headerPart); + if (sizeResult !== true) { + return sizeResult; + } + const extract = S.extract(headerPart.toString("utf-8"), regexHeaderPart)?.namedGroups; + const header = extract?.name + ? { + name: extract.name.trim(), + filename: (extract.encodedFilename !== undefined + ? safeDecode(extract.encodedFilename) + : extract.filename)?.trim(), + } + : null; + if (!header) { + return new BodyParseFormDataError("Bad content header part."); + } + if (header.name.length > params.maxKeyLength) { + return new BodyParseFormDataError("key length exceeds limit."); + } + if (header.filename !== undefined) { + currentFileSize = 0; + fileQuantity++; + if (fileQuantity > params.maxFileQuantity) { + return new BodyParseFormDataError("File quantity exceeds limit."); + } + else if (params.mimeType !== undefined + && !params.mimeType.test(Path.getExtensionName(header.filename) ?? "")) { + return new BodyParseFormDataError("File have wrong mimeType."); + } + } + else { + currentFileSize = undefined; + } + const newStream = await onReceiveHeader(header); + if (newStream instanceof Error) { + return newStream; + } + currentStream = newStream; + return true; + }; + const flushReceiveChunk = async (chunk) => { + if (chunk.length === 0) { + return true; + } + const sizeResult = checkSize(chunk); + if (sizeResult !== true) { + return sizeResult; + } + if (!currentStream) { + return new BodyParseFormDataError("Receive chunk before header part."); + } + if (typeof currentFileSize === "number") { + currentFileSize += chunk.length; + if (params.fileMaxSize !== undefined && currentFileSize > params.fileMaxSize) { + return new BodyParseFormDataError("File size exceeds limit."); + } + } + await currentStream.onReceiveChunk(chunk); + return true; + }; + const treatError = async (error) => { + await currentStream?.onError?.(error, valueAccumulator); + return E.error(error); + }; + try { + for await (const chunk of request) { + if (!(chunk instanceof Buffer)) { + return await treatError(new BodyParseWrongChunkReceived("Buffer.", chunk)); + } + currentBuffer = Buffer.concat([currentBuffer, chunk]); + if (currentBuffer.length > params.maxBufferSize) { + return await treatError(new BodyParseFormDataError("Buffer size exceeds limit.")); + } + while (true) { + const endMultiPartIndex = currentBuffer.indexOf(endMultiPart); + if (endMultiPartIndex !== -1) { + // check if buffer contain end of transmissions + currentBuffer = currentBuffer.subarray(0, endMultiPartIndex); + } + const startPartIndex = currentBuffer.indexOf(startPart); + const endHeaderPartIndex = currentBuffer.indexOf(endHeaderPart); + if (startPartIndex !== -1 && endHeaderPartIndex !== -1) { + // check if buffer contain an entire header of part + const resultChunk = await flushReceiveChunk(currentBuffer.subarray(0, startPartIndex)); + if (resultChunk !== true) { + return await treatError(resultChunk); + } + const endIndex = endHeaderPartIndex + endHeaderPart.length; + const resultHeader = await flushReceiveHeader(currentBuffer.subarray(startPartIndex, endIndex)); + if (resultHeader !== true) { + return await treatError(resultHeader); + } + currentBuffer = currentBuffer.subarray(endIndex); + } + else if (startPartIndex === -1 && endHeaderPartIndex === -1) { + // check if buffer contain only data + if (currentBuffer.length > keep) { + const bufferRestIndex = currentBuffer.length - keep; + const resultChunk = await flushReceiveChunk(currentBuffer.subarray(0, bufferRestIndex)); + if (resultChunk !== true) { + return await treatError(resultChunk); + } + currentBuffer = currentBuffer.subarray(bufferRestIndex); + } + break; + } + else if (startPartIndex !== -1 && endHeaderPartIndex === -1) { + // check if buffer contain start of header but not contain end + break; + } + else { + // check if buffer contain only end of header part + return await treatError(new BodyParseFormDataError("Wrong content.")); + } + } + } + const resultChunk = await flushReceiveChunk(currentBuffer); + if (resultChunk !== true) { + return await treatError(resultChunk); + } + valueAccumulator = await currentStream?.onEndPart(valueAccumulator) ?? valueAccumulator; + return valueAccumulator; + } + catch (error) { + await currentStream?.onError?.(error, valueAccumulator); + return E.left("server-error", error); + } + finally { + request.destroy(); + } +} + +export { readRequestFormData }; diff --git a/docs/libs/v0/interfaces/node/bodyReaders/index.cjs b/docs/libs/v0/interfaces/node/bodyReaders/index.cjs new file mode 100644 index 0000000..8b6b355 --- /dev/null +++ b/docs/libs/v0/interfaces/node/bodyReaders/index.cjs @@ -0,0 +1,9 @@ +'use strict'; + +var index = require('./formData/index.cjs'); +var index$1 = require('./text/index.cjs'); + + + +exports.createFormDataBodyReaderImplementation = index.createFormDataBodyReaderImplementation; +exports.createTextBodyReaderImplementation = index$1.createTextBodyReaderImplementation; diff --git a/docs/libs/v0/interfaces/node/bodyReaders/index.d.ts b/docs/libs/v0/interfaces/node/bodyReaders/index.d.ts new file mode 100644 index 0000000..77cee5e --- /dev/null +++ b/docs/libs/v0/interfaces/node/bodyReaders/index.d.ts @@ -0,0 +1,2 @@ +export * from "./formData"; +export * from "./text"; diff --git a/docs/libs/v0/interfaces/node/bodyReaders/index.mjs b/docs/libs/v0/interfaces/node/bodyReaders/index.mjs new file mode 100644 index 0000000..fadaee1 --- /dev/null +++ b/docs/libs/v0/interfaces/node/bodyReaders/index.mjs @@ -0,0 +1,2 @@ +export { createFormDataBodyReaderImplementation } from './formData/index.mjs'; +export { createTextBodyReaderImplementation } from './text/index.mjs'; diff --git a/docs/libs/v0/interfaces/node/bodyReaders/text/index.cjs b/docs/libs/v0/interfaces/node/bodyReaders/text/index.cjs new file mode 100644 index 0000000..c75e02b --- /dev/null +++ b/docs/libs/v0/interfaces/node/bodyReaders/text/index.cjs @@ -0,0 +1,41 @@ +'use strict'; + +require('../../../../core/request/index.cjs'); +var readRequestText = require('./readRequestText.cjs'); +var utils = require('@duplojs/utils'); +require('../../../../core/errors/index.cjs'); +var text = require('../../../../core/request/bodyController/text.cjs'); +var wrongContentTypeError = require('../../../../core/errors/wrongContentTypeError.cjs'); +var parseJsonError = require('../../../../core/errors/parseJsonError.cjs'); + +function createTextBodyReaderImplementation(serverParams) { + const serverMaxBodySize = utils.stringToBytes(serverParams.maxBodySize); + return text.TextBodyController.createReaderImplementation(async (request, params) => { + if (!request.headers["content-type"]?.includes("application/json") + && !request.headers["content-type"]?.includes("text/plain")) { + return utils.E.error(new wrongContentTypeError.WrongContentTypeError("application/json or text/plain", utils.A.join(utils.A.coalescing(request.headers["content-type"] ?? ""), " "))); + } + const result = await readRequestText.readRequestText(request.raw.request, { maxBodySize: params.bodyMaxSize ?? serverMaxBodySize }, (result) => { + if (request.headers["content-type"]?.includes("application/json")) { + try { + return utils.E.success(JSON.parse(result)); + } + catch (error) { + return utils.E.error(new parseJsonError.ParseJsonError(result, error)); + } + } + return utils.E.success(result); + }); + if (utils.E.isLeft(result)) { + // mandatory in case of error to avoid monopolizing the client connection if a stream is not finished. + request.raw.response.setHeader("Connection", "close"); + } + if (utils.E.hasInformation(result, "server-error")) { + throw utils.unwrap(result); + } + return result; + }); +} + +exports.readRequestText = readRequestText.readRequestText; +exports.createTextBodyReaderImplementation = createTextBodyReaderImplementation; diff --git a/docs/libs/v0/interfaces/node/bodyReaders/text/index.d.ts b/docs/libs/v0/interfaces/node/bodyReaders/text/index.d.ts new file mode 100644 index 0000000..175e1e8 --- /dev/null +++ b/docs/libs/v0/interfaces/node/bodyReaders/text/index.d.ts @@ -0,0 +1,3 @@ +import { type HttpServerParams } from "../../../../core/types"; +export * from "./readRequestText"; +export declare function createTextBodyReaderImplementation(serverParams: HttpServerParams): import("../../../../core/request").BodyReaderImplementation<"text", import("../../../../core/request").TextBodyReaderParams>; diff --git a/docs/libs/v0/interfaces/node/bodyReaders/text/index.mjs b/docs/libs/v0/interfaces/node/bodyReaders/text/index.mjs new file mode 100644 index 0000000..c307d19 --- /dev/null +++ b/docs/libs/v0/interfaces/node/bodyReaders/text/index.mjs @@ -0,0 +1,38 @@ +import '../../../../core/request/index.mjs'; +import { readRequestText } from './readRequestText.mjs'; +import { stringToBytes, E, A, unwrap } from '@duplojs/utils'; +import '../../../../core/errors/index.mjs'; +import { TextBodyController } from '../../../../core/request/bodyController/text.mjs'; +import { WrongContentTypeError } from '../../../../core/errors/wrongContentTypeError.mjs'; +import { ParseJsonError } from '../../../../core/errors/parseJsonError.mjs'; + +function createTextBodyReaderImplementation(serverParams) { + const serverMaxBodySize = stringToBytes(serverParams.maxBodySize); + return TextBodyController.createReaderImplementation(async (request, params) => { + if (!request.headers["content-type"]?.includes("application/json") + && !request.headers["content-type"]?.includes("text/plain")) { + return E.error(new WrongContentTypeError("application/json or text/plain", A.join(A.coalescing(request.headers["content-type"] ?? ""), " "))); + } + const result = await readRequestText(request.raw.request, { maxBodySize: params.bodyMaxSize ?? serverMaxBodySize }, (result) => { + if (request.headers["content-type"]?.includes("application/json")) { + try { + return E.success(JSON.parse(result)); + } + catch (error) { + return E.error(new ParseJsonError(result, error)); + } + } + return E.success(result); + }); + if (E.isLeft(result)) { + // mandatory in case of error to avoid monopolizing the client connection if a stream is not finished. + request.raw.response.setHeader("Connection", "close"); + } + if (E.hasInformation(result, "server-error")) { + throw unwrap(result); + } + return result; + }); +} + +export { createTextBodyReaderImplementation, readRequestText }; diff --git a/docs/libs/v0/interfaces/node/bodyReaders/text/readRequestText.cjs b/docs/libs/v0/interfaces/node/bodyReaders/text/readRequestText.cjs new file mode 100644 index 0000000..f8e3d4a --- /dev/null +++ b/docs/libs/v0/interfaces/node/bodyReaders/text/readRequestText.cjs @@ -0,0 +1,37 @@ +'use strict'; + +require('../../../../core/errors/index.cjs'); +var utils = require('@duplojs/utils'); +var bodyParseWrongChunkReceived = require('../../../../core/errors/bodyParseWrongChunkReceived.cjs'); +var bodySizeExceedsLimitError = require('../../../../core/errors/bodySizeExceedsLimitError.cjs'); + +async function readRequestText(request, params, onEnd) { + let result = ""; + let size = 0; + try { + for await (const chunk of request) { + if (!(chunk instanceof Buffer) && typeof chunk !== "string") { + return utils.E.error(new bodyParseWrongChunkReceived.BodyParseWrongChunkReceived("Buffer or String.", chunk)); + } + size += chunk instanceof Buffer + ? chunk.byteLength + : Buffer.byteLength(chunk); + if (size > params.maxBodySize) { + return utils.E.error(new bodySizeExceedsLimitError.BodySizeExceedsLimitError(params.maxBodySize)); + } + result += chunk.toString(); + } + if (onEnd) { + return await onEnd(result); + } + return result; + } + catch (error) { + return utils.E.left("server-error", error); + } + finally { + request.destroy(); + } +} + +exports.readRequestText = readRequestText; diff --git a/docs/libs/v0/interfaces/node/bodyReaders/text/readRequestText.d.ts b/docs/libs/v0/interfaces/node/bodyReaders/text/readRequestText.d.ts new file mode 100644 index 0000000..6e3cad6 --- /dev/null +++ b/docs/libs/v0/interfaces/node/bodyReaders/text/readRequestText.d.ts @@ -0,0 +1,6 @@ +import { E } from "@duplojs/utils"; +import type http from "http"; +export interface ReadRequestTextParams { + maxBodySize: number; +} +export declare function readRequestText(request: http.IncomingMessage, params: ReadRequestTextParams, onEnd?: (result: string) => GenericOutputValue): Promise | E.Error | GenericOutputValue>; diff --git a/docs/libs/v0/interfaces/node/bodyReaders/text/readRequestText.mjs b/docs/libs/v0/interfaces/node/bodyReaders/text/readRequestText.mjs new file mode 100644 index 0000000..fcd441c --- /dev/null +++ b/docs/libs/v0/interfaces/node/bodyReaders/text/readRequestText.mjs @@ -0,0 +1,35 @@ +import '../../../../core/errors/index.mjs'; +import { E } from '@duplojs/utils'; +import { BodyParseWrongChunkReceived } from '../../../../core/errors/bodyParseWrongChunkReceived.mjs'; +import { BodySizeExceedsLimitError } from '../../../../core/errors/bodySizeExceedsLimitError.mjs'; + +async function readRequestText(request, params, onEnd) { + let result = ""; + let size = 0; + try { + for await (const chunk of request) { + if (!(chunk instanceof Buffer) && typeof chunk !== "string") { + return E.error(new BodyParseWrongChunkReceived("Buffer or String.", chunk)); + } + size += chunk instanceof Buffer + ? chunk.byteLength + : Buffer.byteLength(chunk); + if (size > params.maxBodySize) { + return E.error(new BodySizeExceedsLimitError(params.maxBodySize)); + } + result += chunk.toString(); + } + if (onEnd) { + return await onEnd(result); + } + return result; + } + catch (error) { + return E.left("server-error", error); + } + finally { + request.destroy(); + } +} + +export { readRequestText }; diff --git a/docs/libs/v0/interfaces/node/createHttpServer.cjs b/docs/libs/v0/interfaces/node/createHttpServer.cjs index fe2b448..feebbbe 100644 --- a/docs/libs/v0/interfaces/node/createHttpServer.cjs +++ b/docs/libs/v0/interfaces/node/createHttpServer.cjs @@ -1,12 +1,16 @@ 'use strict'; -var utils = require('@duplojs/utils'); var http = require('http'); var https = require('https'); -var hooks = require('./hooks.cjs'); +var index$3 = require('./hooks/index.cjs'); var implementHttpServer = require('../../core/implementHttpServer.cjs'); +var utils = require('@duplojs/utils'); +var index$2 = require('../../core/defaultHooks/index.cjs'); +require('./bodyReaders/index.cjs'); +var index = require('./bodyReaders/text/index.cjs'); +var index$1 = require('./bodyReaders/formData/index.cjs'); -function createHttpServer(inputHub, params) { +function createHttpServer(hub, params) { const httpServerParams = utils.O.override({ host: "localhost", port: 80, @@ -15,9 +19,13 @@ function createHttpServer(inputHub, params) { predictedHeaderKey: "predicted", fromHookHeaderKey: "from-hook", interface: "node", + uploadFolder: "./upload", }, params); - const hooks$1 = hooks.makeNodeHook(inputHub, httpServerParams); - const hub = inputHub.addRouteHooks(hooks$1); + hub.addBodyReaderImplementation([ + index.createTextBodyReaderImplementation(httpServerParams), + index$1.createFormDataBodyReaderImplementation(httpServerParams), + ]); + hub.addRouteHooks([index$2.initDefaultHook(hub, httpServerParams), index$3.nodeHook]); function whenUncaughtError(error, routerInitializationData) { const serverResponse = routerInitializationData.raw.response; if (!serverResponse.headersSent && !serverResponse.writableEnded) { @@ -51,6 +59,9 @@ function createHttpServer(inputHub, params) { response: serverResponse, }, }, whenUncaughtError)); + if (hub.config.environment === "BUILD") { + return server; + } return new Promise((resolve) => { server.listen({ port: httpServerParams.port, diff --git a/docs/libs/v0/interfaces/node/createHttpServer.d.ts b/docs/libs/v0/interfaces/node/createHttpServer.d.ts index a60bf8e..c4f6c60 100644 --- a/docs/libs/v0/interfaces/node/createHttpServer.d.ts +++ b/docs/libs/v0/interfaces/node/createHttpServer.d.ts @@ -1,20 +1,21 @@ -import { type HttpServerParams, type Hub } from "../../core/hub"; -import { type Hosts } from "./types/host"; -import { type BytesInString, O } from "@duplojs/utils"; +import { type Hub } from "../../core/hub"; import http from "http"; import https from "https"; -declare module "../../core/hub" { +import { O } from "@duplojs/utils"; +import { type HttpServerParams } from "../../core/types"; +declare module "../../core/types" { interface HttpServerParams { readonly interface: "node"; - readonly host: Hosts; - readonly port: number; - readonly maxBodySize: BytesInString | number; - readonly informationHeaderKey: string; - readonly predictedHeaderKey: string; - readonly fromHookHeaderKey: string; readonly http?: http.ServerOptions; readonly https?: https.ServerOptions; } + interface HostCustom { + "::": true; + "0.0.0.0": true; + localhost: true; + "127.0.0.1": true; + "::1": true; + } } -export type CreateHttpServerParams = O.PartialKeys, "maxBodySize" | "informationHeaderKey" | "predictedHeaderKey" | "fromHookHeaderKey">; -export declare function createHttpServer(inputHub: Hub, params: CreateHttpServerParams): Promise | http.Server>; +export type CreateHttpServerParams = O.PartialKeys, "maxBodySize" | "informationHeaderKey" | "predictedHeaderKey" | "fromHookHeaderKey" | "uploadFolder">; +export declare function createHttpServer(hub: Hub, params: CreateHttpServerParams): Promise | http.Server>; diff --git a/docs/libs/v0/interfaces/node/createHttpServer.mjs b/docs/libs/v0/interfaces/node/createHttpServer.mjs index 8ff59c3..540798f 100644 --- a/docs/libs/v0/interfaces/node/createHttpServer.mjs +++ b/docs/libs/v0/interfaces/node/createHttpServer.mjs @@ -1,10 +1,14 @@ -import { O } from '@duplojs/utils'; import http from 'http'; import https from 'https'; -import { makeNodeHook } from './hooks.mjs'; +import { nodeHook } from './hooks/index.mjs'; import { implementHttpServer } from '../../core/implementHttpServer.mjs'; +import { O } from '@duplojs/utils'; +import { initDefaultHook } from '../../core/defaultHooks/index.mjs'; +import './bodyReaders/index.mjs'; +import { createTextBodyReaderImplementation } from './bodyReaders/text/index.mjs'; +import { createFormDataBodyReaderImplementation } from './bodyReaders/formData/index.mjs'; -function createHttpServer(inputHub, params) { +function createHttpServer(hub, params) { const httpServerParams = O.override({ host: "localhost", port: 80, @@ -13,9 +17,13 @@ function createHttpServer(inputHub, params) { predictedHeaderKey: "predicted", fromHookHeaderKey: "from-hook", interface: "node", + uploadFolder: "./upload", }, params); - const hooks = makeNodeHook(inputHub, httpServerParams); - const hub = inputHub.addRouteHooks(hooks); + hub.addBodyReaderImplementation([ + createTextBodyReaderImplementation(httpServerParams), + createFormDataBodyReaderImplementation(httpServerParams), + ]); + hub.addRouteHooks([initDefaultHook(hub, httpServerParams), nodeHook]); function whenUncaughtError(error, routerInitializationData) { const serverResponse = routerInitializationData.raw.response; if (!serverResponse.headersSent && !serverResponse.writableEnded) { @@ -49,6 +57,9 @@ function createHttpServer(inputHub, params) { response: serverResponse, }, }, whenUncaughtError)); + if (hub.config.environment === "BUILD") { + return server; + } return new Promise((resolve) => { server.listen({ port: httpServerParams.port, diff --git a/docs/libs/v0/interfaces/node/error/bodyParseUnknownError.cjs b/docs/libs/v0/interfaces/node/error/bodyParseUnknownError.cjs deleted file mode 100644 index ecf76b6..0000000 --- a/docs/libs/v0/interfaces/node/error/bodyParseUnknownError.cjs +++ /dev/null @@ -1,16 +0,0 @@ -'use strict'; - -var utils = require('@duplojs/utils'); -var kind = require('../kind.cjs'); - -class BodyParseUnknownError extends utils.kindHeritage("body-parse-unknown-error", kind.createInterfacesNodeLibKind("body-parse-unknown-error"), Error) { - contentType; - unknownError; - constructor(contentType, unknownError) { - super({}, [`Error when parsing body with '${contentType}' content-type.`]); - this.contentType = contentType; - this.unknownError = unknownError; - } -} - -exports.BodyParseUnknownError = BodyParseUnknownError; diff --git a/docs/libs/v0/interfaces/node/error/bodyParseUnknownError.d.ts b/docs/libs/v0/interfaces/node/error/bodyParseUnknownError.d.ts deleted file mode 100644 index 4d517d0..0000000 --- a/docs/libs/v0/interfaces/node/error/bodyParseUnknownError.d.ts +++ /dev/null @@ -1,9 +0,0 @@ -declare const BodyParseUnknownError_base: new (params: { - "@DuplojsHttpInterfacesNode/body-parse-unknown-error"?: unknown; -}, parentParams: [message?: string | undefined, options?: ErrorOptions | undefined]) => Error & import("@duplojs/utils").Kind, unknown> & import("@duplojs/utils").Kind, unknown>; -export declare class BodyParseUnknownError extends BodyParseUnknownError_base { - contentType: string; - unknownError: unknown; - constructor(contentType: string, unknownError: unknown); -} -export {}; diff --git a/docs/libs/v0/interfaces/node/error/bodyParseUnknownError.mjs b/docs/libs/v0/interfaces/node/error/bodyParseUnknownError.mjs deleted file mode 100644 index e5cf507..0000000 --- a/docs/libs/v0/interfaces/node/error/bodyParseUnknownError.mjs +++ /dev/null @@ -1,14 +0,0 @@ -import { kindHeritage } from '@duplojs/utils'; -import { createInterfacesNodeLibKind } from '../kind.mjs'; - -class BodyParseUnknownError extends kindHeritage("body-parse-unknown-error", createInterfacesNodeLibKind("body-parse-unknown-error"), Error) { - contentType; - unknownError; - constructor(contentType, unknownError) { - super({}, [`Error when parsing body with '${contentType}' content-type.`]); - this.contentType = contentType; - this.unknownError = unknownError; - } -} - -export { BodyParseUnknownError }; diff --git a/docs/libs/v0/interfaces/node/error/bodyParseWrongChunkReceived.d.ts b/docs/libs/v0/interfaces/node/error/bodyParseWrongChunkReceived.d.ts deleted file mode 100644 index 9522a0b..0000000 --- a/docs/libs/v0/interfaces/node/error/bodyParseWrongChunkReceived.d.ts +++ /dev/null @@ -1,8 +0,0 @@ -declare const BodyParseWrongChunkReceived_base: new (params: { - "@DuplojsHttpInterfacesNode/body-parse-wrong-chunk-received"?: unknown; -}, parentParams: [message?: string | undefined, options?: ErrorOptions | undefined]) => Error & import("@duplojs/utils").Kind, unknown> & import("@duplojs/utils").Kind, unknown>; -export declare class BodyParseWrongChunkReceived extends BodyParseWrongChunkReceived_base { - wrongChunk: unknown; - constructor(wrongChunk: unknown); -} -export {}; diff --git a/docs/libs/v0/interfaces/node/error/bodyParseWrongChunkReceived.mjs b/docs/libs/v0/interfaces/node/error/bodyParseWrongChunkReceived.mjs deleted file mode 100644 index 5c8076f..0000000 --- a/docs/libs/v0/interfaces/node/error/bodyParseWrongChunkReceived.mjs +++ /dev/null @@ -1,12 +0,0 @@ -import { kindHeritage } from '@duplojs/utils'; -import { createInterfacesNodeLibKind } from '../kind.mjs'; - -class BodyParseWrongChunkReceived extends kindHeritage("body-parse-wrong-chunk-received", createInterfacesNodeLibKind("body-parse-wrong-chunk-received"), Error) { - wrongChunk; - constructor(wrongChunk) { - super({}, ["Received chunk is not buffer or string."]); - this.wrongChunk = wrongChunk; - } -} - -export { BodyParseWrongChunkReceived }; diff --git a/docs/libs/v0/interfaces/node/error/bodySizeExceedsLimitError.d.ts b/docs/libs/v0/interfaces/node/error/bodySizeExceedsLimitError.d.ts deleted file mode 100644 index 66b75d6..0000000 --- a/docs/libs/v0/interfaces/node/error/bodySizeExceedsLimitError.d.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { type BytesInString } from "@duplojs/utils"; -declare const BodySizeExceedsLimitError_base: new (params: { - "@DuplojsHttpInterfacesNode/body-size-exceeds-limit-error"?: unknown; -}, parentParams: [message?: string | undefined, options?: ErrorOptions | undefined]) => Error & import("@duplojs/utils").Kind, unknown> & import("@duplojs/utils").Kind, unknown>; -export declare class BodySizeExceedsLimitError extends BodySizeExceedsLimitError_base { - bytesInString: BytesInString | number; - constructor(bytesInString: BytesInString | number); -} -export {}; diff --git a/docs/libs/v0/interfaces/node/hooks.cjs b/docs/libs/v0/interfaces/node/hooks.cjs deleted file mode 100644 index 0157b34..0000000 --- a/docs/libs/v0/interfaces/node/hooks.cjs +++ /dev/null @@ -1,126 +0,0 @@ -'use strict'; - -require('../../core/route/index.cjs'); -var utils = require('@duplojs/utils'); -require('./error/index.cjs'); -require('../../core/response/index.cjs'); -var hooks = require('../../core/route/hooks.cjs'); -var predicted = require('../../core/response/predicted.cjs'); -var hook = require('../../core/response/hook.cjs'); -var bodySizeExceedsLimitError = require('./error/bodySizeExceedsLimitError.cjs'); -var bodyParseWrongChunkReceived = require('./error/bodyParseWrongChunkReceived.cjs'); -var bodyParseUnknownError = require('./error/bodyParseUnknownError.cjs'); - -function makeNodeHook(hub, serverParams) { - const informationHeaderKey = serverParams.informationHeaderKey; - const predictedHeaderKey = serverParams.predictedHeaderKey; - const fromHookHeaderKey = serverParams.fromHookHeaderKey; - const isDev = hub.config.environment === "DEV"; - const maxBodySize = utils.stringToBytes(serverParams.maxBodySize); - return hooks.createHookRouteLifeCycle({ - async parseBody({ request, exit }) { - const contentType = request.headers["content-type"] instanceof Array - ? request.headers["content-type"].join(", ") - : request.headers["content-type"] ?? ""; - const isText = contentType.includes("text/plain"); - const isJson = contentType.includes("application/json"); - if (!isText && !isJson) { - return exit(); - } - const { request: rawRequest } = request.raw; - request.body = await new Promise((resolve, reject) => { - function errorCallback(error) { - if (error instanceof bodySizeExceedsLimitError.BodySizeExceedsLimitError - || error instanceof bodyParseWrongChunkReceived.BodyParseWrongChunkReceived) { - reject(error); - return; - } - reject(new bodyParseUnknownError.BodyParseUnknownError(contentType, error)); - } - let stringBody = ""; - let byteLengthBody = 0; - rawRequest.on("error", errorCallback); - rawRequest.on("data", (chunk) => { - if (!(chunk instanceof Buffer) && typeof chunk !== "string") { - rawRequest.emit("error", new bodyParseWrongChunkReceived.BodyParseWrongChunkReceived(chunk)); - return; - } - byteLengthBody += chunk instanceof Buffer - ? chunk.byteLength - : Buffer.byteLength(chunk); - if (byteLengthBody > maxBodySize) { - rawRequest.emit("error", new bodySizeExceedsLimitError.BodySizeExceedsLimitError(serverParams.maxBodySize)); - return; - } - stringBody += chunk.toString(); - }); - rawRequest.on("end", () => { - try { - resolve(isText - ? stringBody - : JSON.parse(stringBody)); - } - catch (error) { - errorCallback(error); - } - }); - }); - return exit(); - }, - error({ error, response, exit }) { - const displayedError = isDev ? error : undefined; - if (error instanceof bodySizeExceedsLimitError.BodySizeExceedsLimitError) { - return response("400", "body-size-exceeds-limit-error", displayedError); - } - else if (error instanceof bodyParseWrongChunkReceived.BodyParseWrongChunkReceived) { - return response("400", "body-parse-wrong-chunk-received", displayedError); - } - else if (error instanceof bodyParseUnknownError.BodyParseUnknownError) { - return response("400", "body-parse-unknown-error", displayedError); - } - return exit(); - }, - beforeSendResponse({ request, currentResponse, exit }) { - if (!currentResponse.headers?.["content-type"]) { - const body = currentResponse.body; - if (typeof body === "string" - || body instanceof Error) { - currentResponse.setHeader("content-type", "text/plain; charset=utf-8"); - } - else if (typeof body === "object" - || typeof body === "number" - || typeof body === "boolean") { - currentResponse.setHeader("content-type", "application/json; charset=utf-8"); - } - } - currentResponse.setHeader(informationHeaderKey, currentResponse.information); - if (currentResponse instanceof predicted.PredictedResponse) { - currentResponse.setHeader(predictedHeaderKey, "1"); - } - else if (currentResponse instanceof hook.HookResponse) { - currentResponse.setHeader(fromHookHeaderKey, currentResponse.fromHook); - } - request.raw.response.writeHead(Number(currentResponse.code), currentResponse.headers); - return exit(); - }, - sendResponse({ request, currentResponse, exit }) { - const { response: rawResponse } = request.raw; - const body = currentResponse.body; - if (body instanceof Error) { - rawResponse.write(body.toString()); - } - else if (typeof body === "object" - || typeof body === "number" - || typeof body === "boolean") { - rawResponse.write(JSON.stringify(body)); - } - else if (typeof body === "string") { - rawResponse.write(body); - } - rawResponse.end(); - return exit(); - }, - }); -} - -exports.makeNodeHook = makeNodeHook; diff --git a/docs/libs/v0/interfaces/node/hooks.d.ts b/docs/libs/v0/interfaces/node/hooks.d.ts deleted file mode 100644 index ce12857..0000000 --- a/docs/libs/v0/interfaces/node/hooks.d.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { type HttpServerParams, type Hub } from "../../core/hub"; -import { HookResponse } from "../../core/response"; -export declare function makeNodeHook(hub: Hub, serverParams: HttpServerParams): { - parseBody({ request, exit }: import("../../core/route").RouteHookParams): Promise; - error({ error, response, exit }: import("../../core/route").RouteHookErrorParams): import("../../core/route").RouteHookExit | HookResponse<"400", "body-size-exceeds-limit-error", unknown> | HookResponse<"400", "body-parse-wrong-chunk-received", unknown> | HookResponse<"400", "body-parse-unknown-error", unknown>; - beforeSendResponse({ request, currentResponse, exit }: import("../../core/route").RouteHookParamsAfter): import("../../core/route").RouteHookExit; - sendResponse({ request, currentResponse, exit }: import("../../core/route").RouteHookParamsAfter): import("../../core/route").RouteHookExit; -}; diff --git a/docs/libs/v0/interfaces/node/hooks.mjs b/docs/libs/v0/interfaces/node/hooks.mjs deleted file mode 100644 index 66b4adb..0000000 --- a/docs/libs/v0/interfaces/node/hooks.mjs +++ /dev/null @@ -1,124 +0,0 @@ -import '../../core/route/index.mjs'; -import { stringToBytes } from '@duplojs/utils'; -import './error/index.mjs'; -import '../../core/response/index.mjs'; -import { createHookRouteLifeCycle } from '../../core/route/hooks.mjs'; -import { PredictedResponse } from '../../core/response/predicted.mjs'; -import { HookResponse } from '../../core/response/hook.mjs'; -import { BodySizeExceedsLimitError } from './error/bodySizeExceedsLimitError.mjs'; -import { BodyParseWrongChunkReceived } from './error/bodyParseWrongChunkReceived.mjs'; -import { BodyParseUnknownError } from './error/bodyParseUnknownError.mjs'; - -function makeNodeHook(hub, serverParams) { - const informationHeaderKey = serverParams.informationHeaderKey; - const predictedHeaderKey = serverParams.predictedHeaderKey; - const fromHookHeaderKey = serverParams.fromHookHeaderKey; - const isDev = hub.config.environment === "DEV"; - const maxBodySize = stringToBytes(serverParams.maxBodySize); - return createHookRouteLifeCycle({ - async parseBody({ request, exit }) { - const contentType = request.headers["content-type"] instanceof Array - ? request.headers["content-type"].join(", ") - : request.headers["content-type"] ?? ""; - const isText = contentType.includes("text/plain"); - const isJson = contentType.includes("application/json"); - if (!isText && !isJson) { - return exit(); - } - const { request: rawRequest } = request.raw; - request.body = await new Promise((resolve, reject) => { - function errorCallback(error) { - if (error instanceof BodySizeExceedsLimitError - || error instanceof BodyParseWrongChunkReceived) { - reject(error); - return; - } - reject(new BodyParseUnknownError(contentType, error)); - } - let stringBody = ""; - let byteLengthBody = 0; - rawRequest.on("error", errorCallback); - rawRequest.on("data", (chunk) => { - if (!(chunk instanceof Buffer) && typeof chunk !== "string") { - rawRequest.emit("error", new BodyParseWrongChunkReceived(chunk)); - return; - } - byteLengthBody += chunk instanceof Buffer - ? chunk.byteLength - : Buffer.byteLength(chunk); - if (byteLengthBody > maxBodySize) { - rawRequest.emit("error", new BodySizeExceedsLimitError(serverParams.maxBodySize)); - return; - } - stringBody += chunk.toString(); - }); - rawRequest.on("end", () => { - try { - resolve(isText - ? stringBody - : JSON.parse(stringBody)); - } - catch (error) { - errorCallback(error); - } - }); - }); - return exit(); - }, - error({ error, response, exit }) { - const displayedError = isDev ? error : undefined; - if (error instanceof BodySizeExceedsLimitError) { - return response("400", "body-size-exceeds-limit-error", displayedError); - } - else if (error instanceof BodyParseWrongChunkReceived) { - return response("400", "body-parse-wrong-chunk-received", displayedError); - } - else if (error instanceof BodyParseUnknownError) { - return response("400", "body-parse-unknown-error", displayedError); - } - return exit(); - }, - beforeSendResponse({ request, currentResponse, exit }) { - if (!currentResponse.headers?.["content-type"]) { - const body = currentResponse.body; - if (typeof body === "string" - || body instanceof Error) { - currentResponse.setHeader("content-type", "text/plain; charset=utf-8"); - } - else if (typeof body === "object" - || typeof body === "number" - || typeof body === "boolean") { - currentResponse.setHeader("content-type", "application/json; charset=utf-8"); - } - } - currentResponse.setHeader(informationHeaderKey, currentResponse.information); - if (currentResponse instanceof PredictedResponse) { - currentResponse.setHeader(predictedHeaderKey, "1"); - } - else if (currentResponse instanceof HookResponse) { - currentResponse.setHeader(fromHookHeaderKey, currentResponse.fromHook); - } - request.raw.response.writeHead(Number(currentResponse.code), currentResponse.headers); - return exit(); - }, - sendResponse({ request, currentResponse, exit }) { - const { response: rawResponse } = request.raw; - const body = currentResponse.body; - if (body instanceof Error) { - rawResponse.write(body.toString()); - } - else if (typeof body === "object" - || typeof body === "number" - || typeof body === "boolean") { - rawResponse.write(JSON.stringify(body)); - } - else if (typeof body === "string") { - rawResponse.write(body); - } - rawResponse.end(); - return exit(); - }, - }); -} - -export { makeNodeHook }; diff --git a/docs/libs/v0/interfaces/node/hooks/index.cjs b/docs/libs/v0/interfaces/node/hooks/index.cjs new file mode 100644 index 0000000..d06d83e --- /dev/null +++ b/docs/libs/v0/interfaces/node/hooks/index.cjs @@ -0,0 +1,47 @@ +'use strict'; + +require('../../../core/route/index.cjs'); +var serverUtils = require('@duplojs/server-utils'); +var utils = require('@duplojs/utils'); +var node_fs = require('node:fs'); +var hooks = require('../../../core/route/hooks.cjs'); + +const nodeHook = hooks.createHookRouteLifeCycle({ + beforeSendResponse({ request, currentResponse, exit }) { + request.raw.response.writeHead(Number(currentResponse.code), currentResponse.headers); + return exit(); + }, + async sendResponse({ request, currentResponse, exit }) { + const { response: rawResponse } = request.raw; + const body = currentResponse.body; + if (body instanceof Error) { + rawResponse.write(body.toString()); + } + else if (serverUtils.SF.isFileInterface(body)) { + await new Promise((resolve, reject) => { + node_fs.createReadStream(body.path) + .pipe(request.raw.response + .once("error", reject) + .once("close", resolve)); + }); + } + else if (typeof body === "object" + || typeof body === "number" + || typeof body === "boolean") { + rawResponse.write(JSON.stringify(body)); + } + else if (typeof body === "string") { + rawResponse.write(body); + } + rawResponse.end(); + return exit(); + }, + async afterSendResponse({ request, next }) { + if (request.filesAttache) { + await Promise.all(utils.A.map(request.filesAttache, (path) => serverUtils.SF.remove(path))); + } + return next(); + }, +}); + +exports.nodeHook = nodeHook; diff --git a/docs/libs/v0/interfaces/node/hooks/index.d.ts b/docs/libs/v0/interfaces/node/hooks/index.d.ts new file mode 100644 index 0000000..e738e47 --- /dev/null +++ b/docs/libs/v0/interfaces/node/hooks/index.d.ts @@ -0,0 +1,5 @@ +export declare const nodeHook: { + beforeSendResponse({ request, currentResponse, exit }: import("../../../core/route").RouteHookParamsAfter): import("../../../core/route").RouteHookExit; + sendResponse({ request, currentResponse, exit }: import("../../../core/route").RouteHookParamsAfter): Promise; + afterSendResponse({ request, next }: import("../../../core/route").RouteHookParamsAfter): Promise; +}; diff --git a/docs/libs/v0/interfaces/node/hooks/index.mjs b/docs/libs/v0/interfaces/node/hooks/index.mjs new file mode 100644 index 0000000..7cef542 --- /dev/null +++ b/docs/libs/v0/interfaces/node/hooks/index.mjs @@ -0,0 +1,45 @@ +import '../../../core/route/index.mjs'; +import { SF } from '@duplojs/server-utils'; +import { A } from '@duplojs/utils'; +import { createReadStream } from 'node:fs'; +import { createHookRouteLifeCycle } from '../../../core/route/hooks.mjs'; + +const nodeHook = createHookRouteLifeCycle({ + beforeSendResponse({ request, currentResponse, exit }) { + request.raw.response.writeHead(Number(currentResponse.code), currentResponse.headers); + return exit(); + }, + async sendResponse({ request, currentResponse, exit }) { + const { response: rawResponse } = request.raw; + const body = currentResponse.body; + if (body instanceof Error) { + rawResponse.write(body.toString()); + } + else if (SF.isFileInterface(body)) { + await new Promise((resolve, reject) => { + createReadStream(body.path) + .pipe(request.raw.response + .once("error", reject) + .once("close", resolve)); + }); + } + else if (typeof body === "object" + || typeof body === "number" + || typeof body === "boolean") { + rawResponse.write(JSON.stringify(body)); + } + else if (typeof body === "string") { + rawResponse.write(body); + } + rawResponse.end(); + return exit(); + }, + async afterSendResponse({ request, next }) { + if (request.filesAttache) { + await Promise.all(A.map(request.filesAttache, (path) => SF.remove(path))); + } + return next(); + }, +}); + +export { nodeHook }; diff --git a/docs/libs/v0/interfaces/node/index.cjs b/docs/libs/v0/interfaces/node/index.cjs index fbd244c..7d3f628 100644 --- a/docs/libs/v0/interfaces/node/index.cjs +++ b/docs/libs/v0/interfaces/node/index.cjs @@ -2,18 +2,22 @@ require('./types/index.cjs'); var kind = require('./kind.cjs'); -require('./error/index.cjs'); var createHttpServer = require('./createHttpServer.cjs'); -var hooks = require('./hooks.cjs'); -var bodySizeExceedsLimitError = require('./error/bodySizeExceedsLimitError.cjs'); -var bodyParseWrongChunkReceived = require('./error/bodyParseWrongChunkReceived.cjs'); -var bodyParseUnknownError = require('./error/bodyParseUnknownError.cjs'); +var index = require('./hooks/index.cjs'); +require('./bodyReaders/index.cjs'); +var error = require('./bodyReaders/formData/error.cjs'); +var readRequestFormData = require('./bodyReaders/formData/readRequestFormData.cjs'); +var index$1 = require('./bodyReaders/formData/index.cjs'); +var readRequestText = require('./bodyReaders/text/readRequestText.cjs'); +var index$2 = require('./bodyReaders/text/index.cjs'); exports.createInterfacesNodeLibKind = kind.createInterfacesNodeLibKind; exports.createHttpServer = createHttpServer.createHttpServer; -exports.makeNodeHook = hooks.makeNodeHook; -exports.BodySizeExceedsLimitError = bodySizeExceedsLimitError.BodySizeExceedsLimitError; -exports.BodyParseWrongChunkReceived = bodyParseWrongChunkReceived.BodyParseWrongChunkReceived; -exports.BodyParseUnknownError = bodyParseUnknownError.BodyParseUnknownError; +exports.nodeHook = index.nodeHook; +exports.BodyParseFormDataError = error.BodyParseFormDataError; +exports.readRequestFormData = readRequestFormData.readRequestFormData; +exports.createFormDataBodyReaderImplementation = index$1.createFormDataBodyReaderImplementation; +exports.readRequestText = readRequestText.readRequestText; +exports.createTextBodyReaderImplementation = index$2.createTextBodyReaderImplementation; diff --git a/docs/libs/v0/interfaces/node/index.d.ts b/docs/libs/v0/interfaces/node/index.d.ts index af66358..a326d73 100644 --- a/docs/libs/v0/interfaces/node/index.d.ts +++ b/docs/libs/v0/interfaces/node/index.d.ts @@ -1,5 +1,5 @@ export * from "./types"; export * from "./kind"; -export * from "./error"; export * from "./createHttpServer"; export * from "./hooks"; +export * from "./bodyReaders"; diff --git a/docs/libs/v0/interfaces/node/index.mjs b/docs/libs/v0/interfaces/node/index.mjs index f98787a..2638ca2 100644 --- a/docs/libs/v0/interfaces/node/index.mjs +++ b/docs/libs/v0/interfaces/node/index.mjs @@ -1,8 +1,10 @@ import './types/index.mjs'; export { createInterfacesNodeLibKind } from './kind.mjs'; -import './error/index.mjs'; export { createHttpServer } from './createHttpServer.mjs'; -export { makeNodeHook } from './hooks.mjs'; -export { BodySizeExceedsLimitError } from './error/bodySizeExceedsLimitError.mjs'; -export { BodyParseWrongChunkReceived } from './error/bodyParseWrongChunkReceived.mjs'; -export { BodyParseUnknownError } from './error/bodyParseUnknownError.mjs'; +export { nodeHook } from './hooks/index.mjs'; +import './bodyReaders/index.mjs'; +export { BodyParseFormDataError } from './bodyReaders/formData/error.mjs'; +export { readRequestFormData } from './bodyReaders/formData/readRequestFormData.mjs'; +export { createFormDataBodyReaderImplementation } from './bodyReaders/formData/index.mjs'; +export { readRequestText } from './bodyReaders/text/readRequestText.mjs'; +export { createTextBodyReaderImplementation } from './bodyReaders/text/index.mjs'; diff --git a/docs/libs/v0/interfaces/node/types/host.d.ts b/docs/libs/v0/interfaces/node/types/host.d.ts deleted file mode 100644 index 433da70..0000000 --- a/docs/libs/v0/interfaces/node/types/host.d.ts +++ /dev/null @@ -1,4 +0,0 @@ -import { type O } from "@duplojs/utils"; -export interface HostCustom { -} -export type Hosts = (HostCustom[O.GetPropsWithValue] | "::" | "0.0.0.0" | "localhost" | "127.0.0.1" | "::1"); diff --git a/docs/libs/v0/interfaces/node/types/index.cjs b/docs/libs/v0/interfaces/node/types/index.cjs index 1ff1048..e29e776 100644 --- a/docs/libs/v0/interfaces/node/types/index.cjs +++ b/docs/libs/v0/interfaces/node/types/index.cjs @@ -1,5 +1,4 @@ 'use strict'; -require('./host.cjs'); require('./request.cjs'); diff --git a/docs/libs/v0/interfaces/node/types/index.d.ts b/docs/libs/v0/interfaces/node/types/index.d.ts index 7d78289..abe64d0 100644 --- a/docs/libs/v0/interfaces/node/types/index.d.ts +++ b/docs/libs/v0/interfaces/node/types/index.d.ts @@ -1,2 +1 @@ -export * from "./host"; export * from "./request"; diff --git a/docs/libs/v0/interfaces/node/types/index.mjs b/docs/libs/v0/interfaces/node/types/index.mjs index 111694d..2c875f6 100644 --- a/docs/libs/v0/interfaces/node/types/index.mjs +++ b/docs/libs/v0/interfaces/node/types/index.mjs @@ -1,2 +1 @@ -import './host.mjs'; import './request.mjs'; diff --git a/docs/libs/v0/interfaces/node/types/request.cjs b/docs/libs/v0/interfaces/node/types/request.cjs index c045973..0841450 100644 --- a/docs/libs/v0/interfaces/node/types/request.cjs +++ b/docs/libs/v0/interfaces/node/types/request.cjs @@ -1,5 +1,5 @@ 'use strict'; -require('../../../core/request.cjs'); +require('../../../core/request/index.cjs'); require('../../../core/steps/index.cjs'); diff --git a/docs/libs/v0/interfaces/node/types/request.mjs b/docs/libs/v0/interfaces/node/types/request.mjs index d121cb6..8eaee87 100644 --- a/docs/libs/v0/interfaces/node/types/request.mjs +++ b/docs/libs/v0/interfaces/node/types/request.mjs @@ -1,2 +1,2 @@ -import '../../../core/request.mjs'; +import '../../../core/request/index.mjs'; import '../../../core/steps/index.mjs'; diff --git a/docs/libs/v0/plugins/codeGenerator/index.cjs b/docs/libs/v0/plugins/codeGenerator/index.cjs index 6d3b601..c0d186f 100644 --- a/docs/libs/v0/plugins/codeGenerator/index.cjs +++ b/docs/libs/v0/plugins/codeGenerator/index.cjs @@ -10,6 +10,8 @@ var metadata = require('./metadata.cjs'); exports.codeGeneratorPlugin = plugin.codeGeneratorPlugin; +exports.bodyAsFormData = routeToDataParser.bodyAsFormData; +exports.convertRoutePath = routeToDataParser.convertRoutePath; exports.routeToDataParser = routeToDataParser.routeToDataParser; exports.aggregateStepContract = aggregateStepContract.aggregateStepContract; exports.IgnoreByCodeGeneratorMetadata = metadata.IgnoreByCodeGeneratorMetadata; diff --git a/docs/libs/v0/plugins/codeGenerator/index.mjs b/docs/libs/v0/plugins/codeGenerator/index.mjs index 6c12d8a..f0fea22 100644 --- a/docs/libs/v0/plugins/codeGenerator/index.mjs +++ b/docs/libs/v0/plugins/codeGenerator/index.mjs @@ -1,6 +1,6 @@ import '@duplojs/data-parser-tools/toTypescript'; import './types/index.mjs'; export { codeGeneratorPlugin } from './plugin.mjs'; -export { routeToDataParser } from './routeToDataParser.mjs'; +export { bodyAsFormData, convertRoutePath, routeToDataParser } from './routeToDataParser.mjs'; export { aggregateStepContract } from './aggregateStepContract.mjs'; export { IgnoreByCodeGeneratorMetadata } from './metadata.mjs'; diff --git a/docs/libs/v0/plugins/codeGenerator/plugin.cjs b/docs/libs/v0/plugins/codeGenerator/plugin.cjs index d011c7c..440d73d 100644 --- a/docs/libs/v0/plugins/codeGenerator/plugin.cjs +++ b/docs/libs/v0/plugins/codeGenerator/plugin.cjs @@ -3,7 +3,8 @@ var DataParserToTypescript = require('@duplojs/data-parser-tools/toTypescript'); var utils = require('@duplojs/utils'); var routeToDataParser = require('./routeToDataParser.cjs'); -var promises = require('node:fs/promises'); +var serverUtils = require('@duplojs/server-utils'); +var typescriptTransfomer = require('./typescriptTransfomer.cjs'); function _interopNamespaceDefault(e) { var n = Object.create(null); @@ -33,7 +34,7 @@ function codeGeneratorPlugin(pluginParams) { if (!utils.equal(hub.config.environment, ["DEV", "BUILD"])) { return; } - const routes = hub.aggregatesRoutes(); + const routes = utils.A.from(hub.routes); const dataParserRoutes = utils.A.flatMap(routes, (route) => routeToDataParser.routeToDataParser(route, { defaultExtractContract: hub.defaultExtractContract, })); @@ -42,9 +43,12 @@ function codeGeneratorPlugin(pluginParams) { } const output = DataParserToTypescript__namespace.render(utils.DP.union(dataParserRoutes), { identifier: "Routes", - transformers: DataParserToTypescript__namespace.defaultTransformers, + transformers: [ + typescriptTransfomer.fileTransformer, + ...DataParserToTypescript__namespace.defaultTransformers, + ], }); - await promises.writeFile(pluginParams.outputFile, output); + utils.asserts(await serverUtils.SF.writeTextFile(pluginParams.outputFile, output), utils.E.isRight); }, }, ], diff --git a/docs/libs/v0/plugins/codeGenerator/plugin.mjs b/docs/libs/v0/plugins/codeGenerator/plugin.mjs index c6d12b4..09711e9 100644 --- a/docs/libs/v0/plugins/codeGenerator/plugin.mjs +++ b/docs/libs/v0/plugins/codeGenerator/plugin.mjs @@ -1,7 +1,8 @@ import * as DataParserToTypescript from '@duplojs/data-parser-tools/toTypescript'; -import { equal, A, DP } from '@duplojs/utils'; +import { equal, A, DP, asserts, E } from '@duplojs/utils'; import { routeToDataParser } from './routeToDataParser.mjs'; -import { writeFile } from 'node:fs/promises'; +import { SF } from '@duplojs/server-utils'; +import { fileTransformer } from './typescriptTransfomer.mjs'; function codeGeneratorPlugin(pluginParams) { return () => ({ @@ -12,7 +13,7 @@ function codeGeneratorPlugin(pluginParams) { if (!equal(hub.config.environment, ["DEV", "BUILD"])) { return; } - const routes = hub.aggregatesRoutes(); + const routes = A.from(hub.routes); const dataParserRoutes = A.flatMap(routes, (route) => routeToDataParser(route, { defaultExtractContract: hub.defaultExtractContract, })); @@ -21,9 +22,12 @@ function codeGeneratorPlugin(pluginParams) { } const output = DataParserToTypescript.render(DP.union(dataParserRoutes), { identifier: "Routes", - transformers: DataParserToTypescript.defaultTransformers, + transformers: [ + fileTransformer, + ...DataParserToTypescript.defaultTransformers, + ], }); - await writeFile(pluginParams.outputFile, output); + asserts(await SF.writeTextFile(pluginParams.outputFile, output), E.isRight); }, }, ], diff --git a/docs/libs/v0/plugins/codeGenerator/routeToDataParser.cjs b/docs/libs/v0/plugins/codeGenerator/routeToDataParser.cjs index 97fec37..438c242 100644 --- a/docs/libs/v0/plugins/codeGenerator/routeToDataParser.cjs +++ b/docs/libs/v0/plugins/codeGenerator/routeToDataParser.cjs @@ -3,7 +3,21 @@ var aggregateStepContract = require('./aggregateStepContract.cjs'); var utils = require('@duplojs/utils'); var metadata = require('./metadata.cjs'); +require('../../core/request/index.cjs'); +var typescript = require('typescript'); +var formData = require('../../core/request/bodyController/formData.cjs'); +const bodyAsFormData = (dataParser, { transformer, success, addImport }) => { + const result = transformer(dataParser); + if (utils.E.isLeft(result)) { + return result; + } + addImport("@duplojs/utils", "TheFormData"); + return success(typescript.factory.createTypeReferenceNode("TheFormData", [utils.unwrap(result)])); +}; +const convertRoutePath = (path) => utils.pipe(path, utils.S.split("*"), utils.A.flatMap((element, { index, self }) => utils.A.isLastIndex(self, index) + ? element + : [element, utils.DP.string()]), utils.P.when(utils.A.minElements(2), (template) => utils.DP.templateLiteral(template)), utils.P.otherwise(() => utils.DP.literal(path))); function routeToDataParser(route, params) { const isIgnore = utils.A.find(route.definition.metadata, metadata.IgnoreByCodeGeneratorMetadata.is); if (isIgnore) { @@ -24,10 +38,19 @@ function routeToDataParser(route, params) { return skip(); }), utils.O.fromEntries)), ({ endpointContract, entrypointContract }) => utils.A.map(route.definition.paths, (path) => utils.DP.object({ method: utils.DP.literal(route.definition.method), - path: utils.DP.literal(path), + path: convertRoutePath(path), ...entrypointContract, + ...(entrypointContract.body && formData.FormDataBodyController.is(route.definition.bodyController) + ? { + body: entrypointContract + .body + .addOverrideTypescriptTransformer(bodyAsFormData), + } + : {}), responses: utils.DP.union(endpointContract), }))); } +exports.bodyAsFormData = bodyAsFormData; +exports.convertRoutePath = convertRoutePath; exports.routeToDataParser = routeToDataParser; diff --git a/docs/libs/v0/plugins/codeGenerator/routeToDataParser.d.ts b/docs/libs/v0/plugins/codeGenerator/routeToDataParser.d.ts index fca2eeb..0ab8f46 100644 --- a/docs/libs/v0/plugins/codeGenerator/routeToDataParser.d.ts +++ b/docs/libs/v0/plugins/codeGenerator/routeToDataParser.d.ts @@ -1,7 +1,41 @@ import { type Route } from "../../core/route"; import { DP } from "@duplojs/utils"; import { type ResponseContract } from "../../core/response"; +import { type TransformerBuildFunction } from "@duplojs/data-parser-tools/toTypescript"; export interface RouteToDataParserParams { readonly defaultExtractContract: ResponseContract.Contract; } +export declare const bodyAsFormData: TransformerBuildFunction; +export declare const convertRoutePath: (path: string) => DP.DataParserLiteral<{ + readonly value: readonly string[]; + readonly errorMessage?: string | undefined; + readonly identifier?: string | undefined; + readonly overrideTypescriptTransformer?: TransformerBuildFunction | undefined; + readonly checkers: readonly []; +}> | DP.DataParserTemplateLiteral<{ + readonly template: [string | DP.DataParserString<{ + readonly errorMessage?: string | undefined; + readonly identifier?: string | undefined; + readonly overrideTypescriptTransformer?: TransformerBuildFunction | undefined; + readonly coerce: boolean; + readonly checkers: readonly []; + }>, string | DP.DataParserString<{ + readonly errorMessage?: string | undefined; + readonly identifier?: string | undefined; + readonly overrideTypescriptTransformer?: TransformerBuildFunction | undefined; + readonly coerce: boolean; + readonly checkers: readonly []; + }>, ...(string | DP.DataParserString<{ + readonly errorMessage?: string | undefined; + readonly identifier?: string | undefined; + readonly overrideTypescriptTransformer?: TransformerBuildFunction | undefined; + readonly coerce: boolean; + readonly checkers: readonly []; + }>)[]]; + readonly errorMessage?: string | undefined; + readonly identifier?: string | undefined; + readonly overrideTypescriptTransformer?: TransformerBuildFunction | undefined; + readonly pattern: RegExp; + readonly checkers: readonly []; +}>; export declare function routeToDataParser(route: Route, params: RouteToDataParserParams): DP.DataParser[]; diff --git a/docs/libs/v0/plugins/codeGenerator/routeToDataParser.mjs b/docs/libs/v0/plugins/codeGenerator/routeToDataParser.mjs index b70a941..c39ecfb 100644 --- a/docs/libs/v0/plugins/codeGenerator/routeToDataParser.mjs +++ b/docs/libs/v0/plugins/codeGenerator/routeToDataParser.mjs @@ -1,7 +1,21 @@ import { aggregateStepContract } from './aggregateStepContract.mjs'; -import { A, pipe, O, innerPipe, DP } from '@duplojs/utils'; +import { E, unwrap, pipe, S, A, DP, P, O, innerPipe } from '@duplojs/utils'; import { IgnoreByCodeGeneratorMetadata } from './metadata.mjs'; +import '../../core/request/index.mjs'; +import { factory } from 'typescript'; +import { FormDataBodyController } from '../../core/request/bodyController/formData.mjs'; +const bodyAsFormData = (dataParser, { transformer, success, addImport }) => { + const result = transformer(dataParser); + if (E.isLeft(result)) { + return result; + } + addImport("@duplojs/utils", "TheFormData"); + return success(factory.createTypeReferenceNode("TheFormData", [unwrap(result)])); +}; +const convertRoutePath = (path) => pipe(path, S.split("*"), A.flatMap((element, { index, self }) => A.isLastIndex(self, index) + ? element + : [element, DP.string()]), P.when(A.minElements(2), (template) => DP.templateLiteral(template)), P.otherwise(() => DP.literal(path))); function routeToDataParser(route, params) { const isIgnore = A.find(route.definition.metadata, IgnoreByCodeGeneratorMetadata.is); if (isIgnore) { @@ -22,10 +36,17 @@ function routeToDataParser(route, params) { return skip(); }), O.fromEntries)), ({ endpointContract, entrypointContract }) => A.map(route.definition.paths, (path) => DP.object({ method: DP.literal(route.definition.method), - path: DP.literal(path), + path: convertRoutePath(path), ...entrypointContract, + ...(entrypointContract.body && FormDataBodyController.is(route.definition.bodyController) + ? { + body: entrypointContract + .body + .addOverrideTypescriptTransformer(bodyAsFormData), + } + : {}), responses: DP.union(endpointContract), }))); } -export { routeToDataParser }; +export { bodyAsFormData, convertRoutePath, routeToDataParser }; diff --git a/docs/libs/v0/plugins/codeGenerator/typescriptTransfomer.cjs b/docs/libs/v0/plugins/codeGenerator/typescriptTransfomer.cjs new file mode 100644 index 0000000..53cc842 --- /dev/null +++ b/docs/libs/v0/plugins/codeGenerator/typescriptTransfomer.cjs @@ -0,0 +1,9 @@ +'use strict'; + +var DataParserToTypescript = require('@duplojs/data-parser-tools/toTypescript'); +var serverUtils = require('@duplojs/server-utils'); +var typescript = require('typescript'); + +const fileTransformer = DataParserToTypescript.createTransformer(serverUtils.SDP.fileKind.has, (__, { success }) => success(typescript.factory.createTypeReferenceNode("File"))); + +exports.fileTransformer = fileTransformer; diff --git a/docs/libs/v0/plugins/codeGenerator/typescriptTransfomer.d.ts b/docs/libs/v0/plugins/codeGenerator/typescriptTransfomer.d.ts new file mode 100644 index 0000000..1ae78d6 --- /dev/null +++ b/docs/libs/v0/plugins/codeGenerator/typescriptTransfomer.d.ts @@ -0,0 +1 @@ +export declare const fileTransformer: (schema: import("@duplojs/utils/dataParser").DataParsers, params: import("@duplojs/data-parser-tools/toTypescript").TransformerParams) => import("@duplojs/data-parser-tools/toTypescript").MaybeTransformerEither; diff --git a/docs/libs/v0/plugins/codeGenerator/typescriptTransfomer.mjs b/docs/libs/v0/plugins/codeGenerator/typescriptTransfomer.mjs new file mode 100644 index 0000000..33e099e --- /dev/null +++ b/docs/libs/v0/plugins/codeGenerator/typescriptTransfomer.mjs @@ -0,0 +1,7 @@ +import { createTransformer } from '@duplojs/data-parser-tools/toTypescript'; +import { SDP } from '@duplojs/server-utils'; +import { factory } from 'typescript'; + +const fileTransformer = createTransformer(SDP.fileKind.has, (__, { success }) => success(factory.createTypeReferenceNode("File"))); + +export { fileTransformer }; diff --git a/docs/libs/v0/plugins/openApiGenerator/makeOpenApiRoute.cjs b/docs/libs/v0/plugins/openApiGenerator/makeOpenApiRoute.cjs index 1fb4f96..c2a9f14 100644 --- a/docs/libs/v0/plugins/openApiGenerator/makeOpenApiRoute.cjs +++ b/docs/libs/v0/plugins/openApiGenerator/makeOpenApiRoute.cjs @@ -4,12 +4,20 @@ require('../../core/builders/index.cjs'); require('../../core/metadata/index.cjs'); require('../../core/response/index.cjs'); var utils = require('@duplojs/utils'); +var metadata$1 = require('../codeGenerator/metadata.cjs'); +var metadata = require('./metadata.cjs'); var builder = require('../../core/builders/route/builder.cjs'); var ignoreByRouteStore = require('../../core/metadata/ignoreByRouteStore.cjs'); var contract = require('../../core/response/contract.cjs'); function makeOpenApiRoute(routePath, openApiPage) { - return builder.useRouteBuilder("GET", routePath, { metadata: [ignoreByRouteStore.IgnoreByRouteStoreMetadata()] }) + return builder.useRouteBuilder("GET", routePath, { + metadata: [ + ignoreByRouteStore.IgnoreByRouteStoreMetadata(), + metadata.IgnoreByOpenApiGeneratorMetadata(), + metadata$1.IgnoreByCodeGeneratorMetadata(), + ], + }) .handler(contract.ResponseContract.ok("swaggerUi", utils.DP.string()), (__, { response }) => response("swaggerUi", openApiPage) .setHeader("content-type", "text/html")); } diff --git a/docs/libs/v0/plugins/openApiGenerator/makeOpenApiRoute.d.ts b/docs/libs/v0/plugins/openApiGenerator/makeOpenApiRoute.d.ts index 2a2d234..f4f0f23 100644 --- a/docs/libs/v0/plugins/openApiGenerator/makeOpenApiRoute.d.ts +++ b/docs/libs/v0/plugins/openApiGenerator/makeOpenApiRoute.d.ts @@ -3,14 +3,17 @@ import type { RoutePath } from "../../core/route"; import { DP } from "@duplojs/utils"; export declare function makeOpenApiRoute(routePath: RoutePath, openApiPage: string): import("../../core/route").Route<{ readonly method: "GET"; - readonly metadata: readonly [import("../../core/metadata").Metadata<"ignore-by-route-store", unknown>]; + readonly metadata: readonly [import("../../core/metadata").Metadata<"ignore-by-route-store", unknown>, import("../../core/metadata").Metadata<"ignore-by-open-api-generator", unknown>, import("../../core/metadata").Metadata<"ignore-by-code-generator", unknown>]; readonly hooks: readonly []; readonly preflightSteps: readonly []; readonly paths: readonly [`/${string}`]; + readonly bodyController: null; readonly steps: readonly [import("../../core/steps").HandlerStep<{ readonly responseContract: NoInfer>>; diff --git a/docs/libs/v0/plugins/openApiGenerator/makeOpenApiRoute.mjs b/docs/libs/v0/plugins/openApiGenerator/makeOpenApiRoute.mjs index 0ce1034..f0724ec 100644 --- a/docs/libs/v0/plugins/openApiGenerator/makeOpenApiRoute.mjs +++ b/docs/libs/v0/plugins/openApiGenerator/makeOpenApiRoute.mjs @@ -2,12 +2,20 @@ import '../../core/builders/index.mjs'; import '../../core/metadata/index.mjs'; import '../../core/response/index.mjs'; import { DP } from '@duplojs/utils'; +import { IgnoreByCodeGeneratorMetadata } from '../codeGenerator/metadata.mjs'; +import { IgnoreByOpenApiGeneratorMetadata } from './metadata.mjs'; import { useRouteBuilder } from '../../core/builders/route/builder.mjs'; import { IgnoreByRouteStoreMetadata } from '../../core/metadata/ignoreByRouteStore.mjs'; import { ResponseContract } from '../../core/response/contract.mjs'; function makeOpenApiRoute(routePath, openApiPage) { - return useRouteBuilder("GET", routePath, { metadata: [IgnoreByRouteStoreMetadata()] }) + return useRouteBuilder("GET", routePath, { + metadata: [ + IgnoreByRouteStoreMetadata(), + IgnoreByOpenApiGeneratorMetadata(), + IgnoreByCodeGeneratorMetadata(), + ], + }) .handler(ResponseContract.ok("swaggerUi", DP.string()), (__, { response }) => response("swaggerUi", openApiPage) .setHeader("content-type", "text/html")); } diff --git a/docs/libs/v0/plugins/openApiGenerator/plugin.cjs b/docs/libs/v0/plugins/openApiGenerator/plugin.cjs index 4f243b9..881d63d 100644 --- a/docs/libs/v0/plugins/openApiGenerator/plugin.cjs +++ b/docs/libs/v0/plugins/openApiGenerator/plugin.cjs @@ -4,7 +4,7 @@ var routeToOpenApi = require('./routeToOpenApi.cjs'); var utils = require('@duplojs/utils'); var makeOpenApiPage = require('./makeOpenApiPage.cjs'); var makeOpenApiRoute = require('./makeOpenApiRoute.cjs'); -var promises = require('fs/promises'); +var serverUtils = require('@duplojs/server-utils'); function openApiGeneratorPlugin(pluginParams) { return () => ({ @@ -19,7 +19,7 @@ function openApiGeneratorPlugin(pluginParams) { } const contextToJsonSchemaFactory = new Map(); const resultSchemaContext = new Map(); - const routes = hub.aggregatesRoutes(); + const routes = utils.A.from(hub.routes); const openApiRoutes = utils.pipe(routes, utils.A.filter((route) => route.definition.method !== "OPTIONS"), utils.A.flatMap((route) => routeToOpenApi.routeToOpenApi(route, { defaultExtractContract: hub.defaultExtractContract, resultSchemaContext, @@ -77,7 +77,7 @@ function openApiGeneratorPlugin(pluginParams) { }; const openApiDocumentString = JSON.stringify(openApiDocument, null, 2); if (pluginParams.outputFile) { - await promises.writeFile(pluginParams.outputFile, openApiDocumentString); + utils.asserts(await serverUtils.SF.writeTextFile(pluginParams.outputFile, openApiDocumentString), utils.E.isRight); } if (pluginParams.routePath) { const openApiPage = makeOpenApiPage.makeOpenApiPage({ diff --git a/docs/libs/v0/plugins/openApiGenerator/plugin.mjs b/docs/libs/v0/plugins/openApiGenerator/plugin.mjs index 6587274..853c44c 100644 --- a/docs/libs/v0/plugins/openApiGenerator/plugin.mjs +++ b/docs/libs/v0/plugins/openApiGenerator/plugin.mjs @@ -1,8 +1,8 @@ import { routeToOpenApi } from './routeToOpenApi.mjs'; -import { equal, pipe, A, O, G, P, justReturn } from '@duplojs/utils'; +import { equal, A, pipe, O, G, P, justReturn, asserts, E } from '@duplojs/utils'; import { makeOpenApiPage } from './makeOpenApiPage.mjs'; import { makeOpenApiRoute } from './makeOpenApiRoute.mjs'; -import { writeFile } from 'fs/promises'; +import { SF } from '@duplojs/server-utils'; function openApiGeneratorPlugin(pluginParams) { return () => ({ @@ -17,7 +17,7 @@ function openApiGeneratorPlugin(pluginParams) { } const contextToJsonSchemaFactory = new Map(); const resultSchemaContext = new Map(); - const routes = hub.aggregatesRoutes(); + const routes = A.from(hub.routes); const openApiRoutes = pipe(routes, A.filter((route) => route.definition.method !== "OPTIONS"), A.flatMap((route) => routeToOpenApi(route, { defaultExtractContract: hub.defaultExtractContract, resultSchemaContext, @@ -75,7 +75,7 @@ function openApiGeneratorPlugin(pluginParams) { }; const openApiDocumentString = JSON.stringify(openApiDocument, null, 2); if (pluginParams.outputFile) { - await writeFile(pluginParams.outputFile, openApiDocumentString); + asserts(await SF.writeTextFile(pluginParams.outputFile, openApiDocumentString), E.isRight); } if (pluginParams.routePath) { const openApiPage = makeOpenApiPage({ diff --git a/docs/libs/v0/plugins/openApiGenerator/routeToOpenApi.cjs b/docs/libs/v0/plugins/openApiGenerator/routeToOpenApi.cjs index b1afd18..8b86594 100644 --- a/docs/libs/v0/plugins/openApiGenerator/routeToOpenApi.cjs +++ b/docs/libs/v0/plugins/openApiGenerator/routeToOpenApi.cjs @@ -3,7 +3,9 @@ var aggregateStepContract = require('./aggregateStepContract.cjs'); var utils = require('@duplojs/utils'); var toJsonSchema = require('@duplojs/data-parser-tools/toJsonSchema'); +require('../../core/request/index.cjs'); var metadata = require('./metadata.cjs'); +var formData = require('../../core/request/bodyController/formData.cjs'); function factoryJsonSchema(params) { const identifier = params.schema.definition.identifier @@ -32,6 +34,7 @@ const methodMapper = { TRACE: "trace", CONNECT: "connect", OPTIONS: "options", + PATCH: "patch", }; function routeToOpenApi(route, params) { const isIgnore = utils.A.find(route.definition.metadata, metadata.IgnoreByOpenApiGeneratorMetadata.is); @@ -58,7 +61,19 @@ function routeToOpenApi(route, params) { schema, }), })))); - const requestBody = utils.pipe(body, utils.when((value) => utils.O.countKeys(value) === 0, utils.justReturn(utils.DP.empty())), utils.whenNot(utils.DP.dataParserKind.has, utils.DP.object), utils.P.when(utils.DP.identifier(utils.DP.emptyKind), utils.justReturn(undefined)), utils.P.when(utils.DP.identifier(utils.DP.objectKind), (objectSchema) => ({ + const requestBody = utils.pipe(body, utils.when((value) => utils.O.countKeys(value) === 0, utils.justReturn(utils.DP.empty())), utils.whenNot(utils.DP.dataParserKind.has, utils.DP.object), utils.P.when(utils.DP.identifier(utils.DP.emptyKind), utils.justReturn(undefined)), utils.P.when(() => formData.FormDataBodyController.is(route.definition.bodyController), (objectSchema) => ({ + required: true, + content: { + "multipart/form-data": { + schema: factoryJsonSchema({ + context: params.contextToJsonSchemaFactory, + resultSchemaContext: params.resultSchemaContext, + mode: "in", + schema: objectSchema, + }), + }, + }, + })), utils.P.when(utils.DP.identifier(utils.DP.objectKind), (objectSchema) => ({ required: true, content: { "application/json": { diff --git a/docs/libs/v0/plugins/openApiGenerator/routeToOpenApi.d.ts b/docs/libs/v0/plugins/openApiGenerator/routeToOpenApi.d.ts index 353611d..d03e351 100644 --- a/docs/libs/v0/plugins/openApiGenerator/routeToOpenApi.d.ts +++ b/docs/libs/v0/plugins/openApiGenerator/routeToOpenApi.d.ts @@ -10,9 +10,18 @@ export interface RouteToOpenApiParams { } export declare function routeToOpenApi(route: Route, params: RouteToOpenApiParams): { path: `/${string}`; - method: "options" | "get" | "post" | "put" | "delete" | "head" | "trace" | "connect"; + method: "options" | "get" | "post" | "put" | "patch" | "delete" | "head" | "trace" | "connect"; parameters: EntrypointParameter[]; requestBody: { + required: true; + content: { + "multipart/form-data": { + schema: { + $ref: `#/components/schemas/${string}`; + }; + }; + }; + } | { required: true; content: { "application/json": { diff --git a/docs/libs/v0/plugins/openApiGenerator/routeToOpenApi.mjs b/docs/libs/v0/plugins/openApiGenerator/routeToOpenApi.mjs index b8b3b8e..95fce7b 100644 --- a/docs/libs/v0/plugins/openApiGenerator/routeToOpenApi.mjs +++ b/docs/libs/v0/plugins/openApiGenerator/routeToOpenApi.mjs @@ -1,7 +1,9 @@ import { aggregateStepContract } from './aggregateStepContract.mjs'; import { O, A, pipe, DP, when, justReturn, whenNot, P, isType, S } from '@duplojs/utils'; import { render, defaultTransformers } from '@duplojs/data-parser-tools/toJsonSchema'; +import '../../core/request/index.mjs'; import { IgnoreByOpenApiGeneratorMetadata } from './metadata.mjs'; +import { FormDataBodyController } from '../../core/request/bodyController/formData.mjs'; function factoryJsonSchema(params) { const identifier = params.schema.definition.identifier @@ -30,6 +32,7 @@ const methodMapper = { TRACE: "trace", CONNECT: "connect", OPTIONS: "options", + PATCH: "patch", }; function routeToOpenApi(route, params) { const isIgnore = A.find(route.definition.metadata, IgnoreByOpenApiGeneratorMetadata.is); @@ -56,7 +59,19 @@ function routeToOpenApi(route, params) { schema, }), })))); - const requestBody = pipe(body, when((value) => O.countKeys(value) === 0, justReturn(DP.empty())), whenNot(DP.dataParserKind.has, DP.object), P.when(DP.identifier(DP.emptyKind), justReturn(undefined)), P.when(DP.identifier(DP.objectKind), (objectSchema) => ({ + const requestBody = pipe(body, when((value) => O.countKeys(value) === 0, justReturn(DP.empty())), whenNot(DP.dataParserKind.has, DP.object), P.when(DP.identifier(DP.emptyKind), justReturn(undefined)), P.when(() => FormDataBodyController.is(route.definition.bodyController), (objectSchema) => ({ + required: true, + content: { + "multipart/form-data": { + schema: factoryJsonSchema({ + context: params.contextToJsonSchemaFactory, + resultSchemaContext: params.resultSchemaContext, + mode: "in", + schema: objectSchema, + }), + }, + }, + })), P.when(DP.identifier(DP.objectKind), (objectSchema) => ({ required: true, content: { "application/json": { diff --git a/docs/libs/v0/plugins/openApiGenerator/types/entrypoint.d.ts b/docs/libs/v0/plugins/openApiGenerator/types/entrypoint.d.ts index 4aa1fbd..2309594 100644 --- a/docs/libs/v0/plugins/openApiGenerator/types/entrypoint.d.ts +++ b/docs/libs/v0/plugins/openApiGenerator/types/entrypoint.d.ts @@ -10,12 +10,17 @@ export interface EntrypointContentBodyApplicationJson { schema: JsonSchema; }; } +export interface EntrypointContentBodyFormData { + "multipart/form-data": { + schema: JsonSchema; + }; +} export interface EntrypointContentBodyTextPlain { "text/plain": { schema: JsonSchema; }; } -export type EntrypointContentBody = EntrypointContentBodyApplicationJson | EntrypointContentBodyTextPlain; +export type EntrypointContentBody = (EntrypointContentBodyApplicationJson | EntrypointContentBodyTextPlain | EntrypointContentBodyFormData); export interface EntrypointRequestBody { required: true; content: EntrypointContentBody; diff --git a/docs/libs/v0/plugins/openApiGenerator/types/openApiMethod.d.ts b/docs/libs/v0/plugins/openApiGenerator/types/openApiMethod.d.ts index 482bac9..32a1ba6 100644 --- a/docs/libs/v0/plugins/openApiGenerator/types/openApiMethod.d.ts +++ b/docs/libs/v0/plugins/openApiGenerator/types/openApiMethod.d.ts @@ -1 +1 @@ -export type OpenApiMethod = "get" | "post" | "put" | "delete" | "head" | "trace" | "connect" | "options"; +export type OpenApiMethod = "get" | "post" | "put" | "patch" | "delete" | "head" | "trace" | "connect" | "options"; diff --git a/integration/client/clientType.ts b/integration/client/clientType.ts index 7fd7716..6cf06a2 100644 --- a/integration/client/clientType.ts +++ b/integration/client/clientType.ts @@ -1,3 +1,5 @@ +import type { TheFormData } from "@duplojs/utils"; + export type Routes = { method: "GET"; path: "/users"; @@ -50,4 +52,30 @@ export type Routes = { age: number; }; }; +} | { + method: "POST"; + path: "/documents"; + body: TheFormData<{ + bool: boolean; + myFile: [ + File, + ]; + }>; + responses: { + code: "422"; + information: "extract-error"; + body?: undefined; + } | { + code: "204"; + information: "file.receive"; + body?: undefined; + }; +} | { + method: "GET"; + path: `/documents/${string}`; + responses: { + code: "200"; + information: "file.send"; + body: File; + }; }; diff --git a/integration/client/index.test.ts b/integration/client/index.test.ts index 446c247..c44eeb3 100644 --- a/integration/client/index.test.ts +++ b/integration/client/index.test.ts @@ -2,14 +2,19 @@ import { hub } from "@core"; import { createHttpServer } from "@duplojs/http/node"; import { createHttpClient, type NotPredictedClientResponse, type PromiseRequestParams, type RequestErrorContent } from "@duplojs/http/client"; import { type Routes } from "./clientType"; -import { E, S, type ExpectType } from "@duplojs/utils"; +import { createFormData, E, S, type ExpectType } from "@duplojs/utils"; +import { createFileToSend } from "@utils"; +import { resolve } from "path"; describe("node server", async() => { const server = await createHttpServer(hub, { host: "0.0.0.0", port: 8946, + uploadFolder: resolve(import.meta.dirname, "../files/upload"), }); + process.chdir(resolve(import.meta.dirname, "../")); + afterAll(() => { server.close(); }); @@ -23,8 +28,8 @@ describe("node server", async() => { type Check = ExpectType< typeof result, - | E.EitherLeft<"request-error", RequestErrorContent> - | E.EitherRight< + | E.Left<"request-error", RequestErrorContent> + | E.Right< "response", | { code: "200"; @@ -44,7 +49,7 @@ describe("node server", async() => { predicted: boolean; } | NotPredictedClientResponse< - PromiseRequestParams> + Record > >, "strict" @@ -74,11 +79,11 @@ describe("node server", async() => { type Check = ExpectType< typeof result, - | E.EitherLeft<"request-error", RequestErrorContent> - | E.EitherRight< + | E.Left<"request-error", RequestErrorContent> + | E.Right< "response", | NotPredictedClientResponse< - PromiseRequestParams> + Record > | { code: "422"; @@ -131,7 +136,7 @@ describe("node server", async() => { ); }); - it("port user", async() => { + it("post user", async() => { const result = await httpClient.post("/users", { body: { id: 5, @@ -142,8 +147,8 @@ describe("node server", async() => { type Check = ExpectType< typeof result, - | E.EitherLeft<"request-error", RequestErrorContent> - | E.EitherRight< + | E.Left<"request-error", RequestErrorContent> + | E.Right< "response", | { code: "422"; @@ -176,7 +181,7 @@ describe("node server", async() => { predicted: boolean; } | NotPredictedClientResponse< - PromiseRequestParams> + Record > >, "strict" @@ -198,4 +203,62 @@ describe("node server", async() => { ), ); }); + + it("post document", async() => { + const result = await httpClient.post("/documents", { + body: createFormData({ + bool: true, + myFile: [await createFileToSend("files/fakeFiles/1mb.jpg", "//😄.jpg")], + }), + }); + + type Check = ExpectType< + typeof result, + | E.Left<"request-error", RequestErrorContent> + | E.Right< + "response", + | { + code: "422"; + information: "extract-error"; + body: undefined; + ok: boolean | null; + headers: Headers; + type: ResponseType; + url: string; + redirected: boolean; + raw: Response; + requestParams: PromiseRequestParams>; + predicted: boolean; + } + | { + code: "204"; + information: "file.receive"; + body: undefined; + ok: boolean | null; + headers: Headers; + type: ResponseType; + url: string; + redirected: boolean; + raw: Response; + requestParams: PromiseRequestParams>; + predicted: boolean; + } + | NotPredictedClientResponse< + Record + > + >, + "strict" + >; + + expect(result).toStrictEqual( + E.right( + "response", + expect.objectContaining({ + url: "http://localhost:8946/documents", + information: "file.receive", + predicted: true, + }), + ), + ); + }); }); diff --git a/integration/client/tsconfig.json b/integration/client/tsconfig.json index 1ef1811..88b5f76 100644 --- a/integration/client/tsconfig.json +++ b/integration/client/tsconfig.json @@ -3,9 +3,10 @@ "compilerOptions": { "baseUrl": "./", "paths": { - "@core": ["../core/index.ts"] + "@core": ["../core/index.ts"], + "@utils": ["../utils/index.ts"], }, "types": ["vitest/globals", "node", "web"] }, - "include": ["**/*.ts", "../core/**/*.ts"], + "include": ["**/*.ts", "../core/**/*.ts", "../utils/**/*.ts"], } diff --git a/integration/codeGenerator/__snapshots__/index.test.ts.snap b/integration/codeGenerator/__snapshots__/index.test.ts.snap index 998399c..c921a5b 100644 --- a/integration/codeGenerator/__snapshots__/index.test.ts.snap +++ b/integration/codeGenerator/__snapshots__/index.test.ts.snap @@ -1,7 +1,9 @@ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html exports[`codeGenerator > correct generate file 1`] = ` -"export type Routes = { +"import type { TheFormData } from "@duplojs/utils"; + +export type Routes = { method: "GET"; path: "/users"; responses: { @@ -53,5 +55,31 @@ exports[`codeGenerator > correct generate file 1`] = ` age: number; }; }; +} | { + method: "POST"; + path: "/documents"; + body: TheFormData<{ + bool: boolean; + myFile: [ + File + ]; + }>; + responses: { + code: "422"; + information: "extract-error"; + body?: undefined; + } | { + code: "204"; + information: "file.receive"; + body?: undefined; + }; +} | { + method: "GET"; + path: \`/documents/\${string}\`; + responses: { + code: "200"; + information: "file.send"; + body: File; + }; };" `; diff --git a/integration/codeGenerator/index.test.ts b/integration/codeGenerator/index.test.ts index 189d970..a4df839 100644 --- a/integration/codeGenerator/index.test.ts +++ b/integration/codeGenerator/index.test.ts @@ -18,7 +18,7 @@ describe("codeGenerator", () => { await launchHookServer( hubWithPlugins.aggregatesHooksHubLifeCycle("beforeStartServer"), hubWithPlugins, - {}, + {} as any, ); expect(readFileSync(fileName, "utf-8")).toMatchSnapshot(); diff --git a/integration/core/routes/document.ts b/integration/core/routes/document.ts new file mode 100644 index 0000000..42d8971 --- /dev/null +++ b/integration/core/routes/document.ts @@ -0,0 +1,34 @@ +import { controlBodyAsFormData, ResponseContract, useRouteBuilder } from "@duplojs/http"; +import { SDPE } from "@duplojs/server-utils"; +import { createFileInterface } from "@duplojs/server-utils/file"; +import { asserts, DPE, E } from "@duplojs/utils"; + +useRouteBuilder("POST", "/documents", { + bodyController: controlBodyAsFormData({ + maxFileQuantity: 10, + bodyMaxSize: "1.5mb", + }), +}) + .extract({ + body: { + bool: DPE.coerce.boolean(), + myFile: DPE.tuple([SDPE.file()]), + }, + }) + .handler( + ResponseContract.noContent("file.receive"), + async({ myFile: [myFile] }, { response }) => { + asserts( + await myFile.move("files/store/picture.jpg"), + E.isRight, + ); + + return response("file.receive"); + }, + ); + +useRouteBuilder("GET", "/documents/*") + .handler( + ResponseContract.ok("file.send", SDPE.file()), + (__, { response }) => response("file.send", createFileInterface("files/fakeFiles/superTextFile.txt")), + ); diff --git a/integration/core/routes/index.ts b/integration/core/routes/index.ts index 50fcd1e..dd9f685 100644 --- a/integration/core/routes/index.ts +++ b/integration/core/routes/index.ts @@ -1 +1,2 @@ import "./users"; +import "./document"; diff --git a/integration/files/fakeFiles/1mb.jpg b/integration/files/fakeFiles/1mb.jpg new file mode 100644 index 0000000..b31641b Binary files /dev/null and b/integration/files/fakeFiles/1mb.jpg differ diff --git a/integration/files/fakeFiles/2mb.jpg b/integration/files/fakeFiles/2mb.jpg new file mode 100644 index 0000000..ce345fe Binary files /dev/null and b/integration/files/fakeFiles/2mb.jpg differ diff --git a/integration/files/fakeFiles/superTextFile.txt b/integration/files/fakeFiles/superTextFile.txt new file mode 100644 index 0000000..5628841 --- /dev/null +++ b/integration/files/fakeFiles/superTextFile.txt @@ -0,0 +1 @@ +this is super file with super content. \ No newline at end of file diff --git a/integration/files/store/.gitkeep b/integration/files/store/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/integration/files/upload/.gitkeep b/integration/files/upload/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/integration/node/file.test.ts b/integration/node/file.test.ts new file mode 100644 index 0000000..05099e4 --- /dev/null +++ b/integration/node/file.test.ts @@ -0,0 +1,123 @@ +import { hub } from "@core"; +import { createHttpServer } from "@duplojs/http/node"; +import { SF } from "@duplojs/server-utils"; +import { asserts, E, sleep, stringToBytes } from "@duplojs/utils"; +import { createFileToSend } from "@utils"; +import { resolve } from "path"; + +describe("receive file", async() => { + const server = await createHttpServer(hub, { + host: "0.0.0.0", + port: 8961, + uploadFolder: resolve(import.meta.dirname, "../files/upload"), + }); + + process.chdir(resolve(import.meta.dirname, "../")); + + afterAll(() => { + server.close(); + }); + + it("send File", async() => { + const formData = new FormData(); + formData.append("bool", "true"); + formData.append( + "myFile/*\\[0]", + await createFileToSend("files/fakeFiles/1mb.jpg", "//😄.jpg"), + ); + + await expect( + fetch("http://localhost:8961/documents", { + method: "POST", + body: formData, + headers: { "content-type-options": "advanced" }, + }) + .then((response) => ({ + headers: [...response.headers.entries()], + })), + ).resolves.toStrictEqual({ + headers: expect.arrayContaining([ + [ + "information", + "file.receive", + ], + ]), + }); + + await sleep(500); + + expect(await SF.stat("files/store/picture.jpg")).toStrictEqual( + E.success( + expect.objectContaining({ sizeBytes: stringToBytes("1mb") }), + ), + ); + asserts(await SF.remove("files/store/picture.jpg"), E.isRight); + expect(await SF.readDirectory("files/upload")).toStrictEqual( + E.success([".gitkeep"]), + ); + }); + + it("send File witch exceed limit", async() => { + const formData = new FormData(); + formData.append("bool", "true"); + formData.append( + "myFile/*\\[0]", + await createFileToSend("files/fakeFiles/2mb.jpg", "//😄.jpg"), + ); + + await expect( + fetch("http://localhost:8961/documents", { + method: "POST", + body: formData, + }) + .then(async(response) => ({ + code: response.status, + body: await response.text(), + headers: [...response.headers.entries()], + })), + ).resolves.toStrictEqual({ + code: 422, + body: "Error: Body size is bigger than 1572864.", + headers: expect.arrayContaining([ + [ + "information", + "extract-error", + ], + [ + "extract-key", + "request.body", + ], + ]), + }); + + await sleep(500); + + expect(await SF.readDirectory("files/upload")).toStrictEqual( + E.success([".gitkeep"]), + ); + }); + + it("receive file", async() => { + await expect( + fetch("http://localhost:8961/documents/test", { + method: "GET", + }) + .then(async(response) => ({ + body: await response.text(), + headers: [...response.headers.entries()], + })), + ).resolves.toStrictEqual({ + body: "this is super file with super content.", + headers: expect.arrayContaining([ + [ + "information", + "file.send", + ], + [ + "content-type", + "text/plain", + ], + ]), + }); + }); +}); diff --git a/integration/node/tsconfig.json b/integration/node/tsconfig.json index 70fe73f..27be263 100644 --- a/integration/node/tsconfig.json +++ b/integration/node/tsconfig.json @@ -3,9 +3,10 @@ "compilerOptions": { "baseUrl": "./", "paths": { - "@core": ["../core/index.ts"] + "@core": ["../core/index.ts"], + "@utils": ["../utils/index.ts"], }, "types": ["vitest/globals", "node"] }, - "include": ["**/*.ts", "../core/**/*.ts"], + "include": ["**/*.ts", "../core/**/*.ts", "../utils/**/*.ts"], } diff --git a/integration/openApiGenerator/__snapshots__/index.test.ts.snap b/integration/openApiGenerator/__snapshots__/index.test.ts.snap index c3e83cf..5e0456c 100644 --- a/integration/openApiGenerator/__snapshots__/index.test.ts.snap +++ b/integration/openApiGenerator/__snapshots__/index.test.ts.snap @@ -121,6 +121,70 @@ exports[`openApiGenerator > correct generate file 1`] = ` } } } + }, + "/documents": { + "post": { + "parameters": [], + "requestBody": { + "required": true, + "content": { + "multipart/form-data": { + "schema": { + "$ref": "#/components/schemas/NotIdentified5" + } + } + } + }, + "responses": { + "204": { + "headers": { + "information": { + "schema": { + "const": "file.receive", + "type": "string" + }, + "description": "file.receive" + } + } + }, + "422": { + "headers": { + "information": { + "schema": { + "const": "extract-error", + "type": "string" + }, + "description": "extract-error" + } + } + } + } + } + }, + "/documents/*": { + "get": { + "parameters": [], + "responses": { + "200": { + "headers": { + "information": { + "schema": { + "const": "file.send", + "type": "string" + }, + "description": "file.send" + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NotIdentified6" + } + } + } + } + } + } } }, "components": { @@ -213,6 +277,54 @@ exports[`openApiGenerator > correct generate file 1`] = ` "name", "age" ] + }, + "NotIdentified5": { + "type": "object", + "properties": { + "bool": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "enum": [ + "true", + "false", + "0", + "1" + ] + }, + { + "type": "number", + "enum": [ + 0, + 1 + ] + } + ] + }, + "myFile": { + "type": "array", + "items": [ + { + "type": "string", + "format": "binary" + } + ], + "minItems": 1, + "additionalItems": false, + "maxItems": 1 + } + }, + "required": [ + "bool", + "myFile" + ] + }, + "NotIdentified6": { + "type": "string", + "format": "binary" } } } diff --git a/integration/openApiGenerator/index.test.ts b/integration/openApiGenerator/index.test.ts index 9e33350..bbd9b21 100644 --- a/integration/openApiGenerator/index.test.ts +++ b/integration/openApiGenerator/index.test.ts @@ -21,7 +21,7 @@ describe("openApiGenerator", () => { await launchHookServer( hubWithPlugins.aggregatesHooksHubLifeCycle("beforeServerBuildRoutes"), hubWithPlugins, - {}, + {} as any, ); expect(readFileSync(fileName, "utf-8")).toMatchSnapshot(); diff --git a/integration/utils/createFileToSend.ts b/integration/utils/createFileToSend.ts new file mode 100644 index 0000000..909c87c --- /dev/null +++ b/integration/utils/createFileToSend.ts @@ -0,0 +1,11 @@ +import { mimeType, Path } from "@duplojs/utils"; +import { readFile } from "node:fs/promises"; + +export async function createFileToSend(path: string, name?: string) { + const blob = new Blob([await readFile(path) as never]); + + return new File([blob], name ?? Path.getBaseName(path) ?? "", { + type: mimeType.get(Path.getExtensionName(path) ?? ""), + lastModified: Date.now(), + }); +} diff --git a/integration/utils/index.ts b/integration/utils/index.ts new file mode 100644 index 0000000..5c7000b --- /dev/null +++ b/integration/utils/index.ts @@ -0,0 +1 @@ +export * from "./createFileToSend"; diff --git a/package-lock.json b/package-lock.json index 40bb6fa..0a7f0d1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,9 +12,6 @@ "integration", "docs" ], - "dependencies": { - "@types/web": "^0.0.317" - }, "devDependencies": { "@commitlint/cli": "19.8.1", "@commitlint/config-conventional": "19.8.1", @@ -23,6 +20,7 @@ "@types/bun": "^1.3.3", "@types/deno": "^2.5.0", "@types/node": "24.3.0", + "@types/web": "^0.0.317", "@vitest/coverage-istanbul": "3.2.4", "eslint": "9.34.0", "form-data": "^4.0.5", @@ -42,8 +40,9 @@ "node": ">=22.15.1" }, "peerDependencies": { - "@duplojs/data-parser-tools": ">=0.2.4 <1.0.0", - "@duplojs/utils": ">=1.4.57 <2.0.0" + "@duplojs/data-parser-tools": ">=0.2.7 <1.0.0", + "@duplojs/server-utils": ">=0.2.0 <1.0.0", + "@duplojs/utils": ">=1.5.4 <2.0.0" } }, "docs": { @@ -597,13 +596,13 @@ } }, "node_modules/@babel/parser": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.5.tgz", - "integrity": "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==", + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.0.tgz", + "integrity": "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==", "dev": true, "license": "MIT", "dependencies": { - "@babel/types": "^7.28.5" + "@babel/types": "^7.29.0" }, "bin": { "parser": "bin/babel-parser.js" @@ -647,9 +646,9 @@ } }, "node_modules/@babel/types": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.5.tgz", - "integrity": "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==", + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", "dev": true, "license": "MIT", "dependencies": { @@ -668,60 +667,46 @@ "license": "MIT" }, "node_modules/@chevrotain/cst-dts-gen": { - "version": "11.0.3", - "resolved": "https://registry.npmjs.org/@chevrotain/cst-dts-gen/-/cst-dts-gen-11.0.3.tgz", - "integrity": "sha512-BvIKpRLeS/8UbfxXxgC33xOumsacaeCKAjAeLyOn7Pcp95HiRbrpl14S+9vaZLolnbssPIUuiUd8IvgkRyt6NQ==", + "version": "11.1.1", + "resolved": "https://registry.npmjs.org/@chevrotain/cst-dts-gen/-/cst-dts-gen-11.1.1.tgz", + "integrity": "sha512-fRHyv6/f542qQqiRGalrfJl/evD39mAvbJLCekPazhiextEatq1Jx1K/i9gSd5NNO0ds03ek0Cbo/4uVKmOBcw==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@chevrotain/gast": "11.0.3", - "@chevrotain/types": "11.0.3", - "lodash-es": "4.17.21" + "@chevrotain/gast": "11.1.1", + "@chevrotain/types": "11.1.1", + "lodash-es": "4.17.23" } }, - "node_modules/@chevrotain/cst-dts-gen/node_modules/lodash-es": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz", - "integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==", - "dev": true, - "license": "MIT" - }, "node_modules/@chevrotain/gast": { - "version": "11.0.3", - "resolved": "https://registry.npmjs.org/@chevrotain/gast/-/gast-11.0.3.tgz", - "integrity": "sha512-+qNfcoNk70PyS/uxmj3li5NiECO+2YKZZQMbmjTqRI3Qchu8Hig/Q9vgkHpI3alNjr7M+a2St5pw5w5F6NL5/Q==", + "version": "11.1.1", + "resolved": "https://registry.npmjs.org/@chevrotain/gast/-/gast-11.1.1.tgz", + "integrity": "sha512-Ko/5vPEYy1vn5CbCjjvnSO4U7GgxyGm+dfUZZJIWTlQFkXkyym0jFYrWEU10hyCjrA7rQtiHtBr0EaZqvHFZvg==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@chevrotain/types": "11.0.3", - "lodash-es": "4.17.21" + "@chevrotain/types": "11.1.1", + "lodash-es": "4.17.23" } }, - "node_modules/@chevrotain/gast/node_modules/lodash-es": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz", - "integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==", - "dev": true, - "license": "MIT" - }, "node_modules/@chevrotain/regexp-to-ast": { - "version": "11.0.3", - "resolved": "https://registry.npmjs.org/@chevrotain/regexp-to-ast/-/regexp-to-ast-11.0.3.tgz", - "integrity": "sha512-1fMHaBZxLFvWI067AVbGJav1eRY7N8DDvYCTwGBiE/ytKBgP8azTdgyrKyWZ9Mfh09eHWb5PgTSO8wi7U824RA==", + "version": "11.1.1", + "resolved": "https://registry.npmjs.org/@chevrotain/regexp-to-ast/-/regexp-to-ast-11.1.1.tgz", + "integrity": "sha512-ctRw1OKSXkOrR8VTvOxrQ5USEc4sNrfwXHa1NuTcR7wre4YbjPcKw+82C2uylg/TEwFRgwLmbhlln4qkmDyteg==", "dev": true, "license": "Apache-2.0" }, "node_modules/@chevrotain/types": { - "version": "11.0.3", - "resolved": "https://registry.npmjs.org/@chevrotain/types/-/types-11.0.3.tgz", - "integrity": "sha512-gsiM3G8b58kZC2HaWR50gu6Y1440cHiJ+i3JUvcp/35JchYejb2+5MVeJK0iKThYpAa/P2PYFV4hoi44HD+aHQ==", + "version": "11.1.1", + "resolved": "https://registry.npmjs.org/@chevrotain/types/-/types-11.1.1.tgz", + "integrity": "sha512-wb2ToxG8LkgPYnKe9FH8oGn3TMCBdnwiuNC5l5y+CtlaVRbCytU0kbVsk6CGrqTL4ZN4ksJa0TXOYbxpbthtqw==", "dev": true, "license": "Apache-2.0" }, "node_modules/@chevrotain/utils": { - "version": "11.0.3", - "resolved": "https://registry.npmjs.org/@chevrotain/utils/-/utils-11.0.3.tgz", - "integrity": "sha512-YslZMgtJUyuMbZ+aKvfF3x1f5liK4mWNxghFRv7jqRR9C3R3fAOGTTKvxXDa2Y1s9zSbcpuO0cAxDYsc9SrXoQ==", + "version": "11.1.1", + "resolved": "https://registry.npmjs.org/@chevrotain/utils/-/utils-11.1.1.tgz", + "integrity": "sha512-71eTYMzYXYSFPrbg/ZwftSaSDld7UYlS8OQa3lNnn9jzNtpFbaReRRyghzqS7rI3CDaorqpPJJcXGHK+FE1TVQ==", "dev": true, "license": "Apache-2.0" }, @@ -1039,9 +1024,9 @@ "peer": true }, "node_modules/@duplojs/data-parser-tools": { - "version": "0.2.4", - "resolved": "https://registry.npmjs.org/@duplojs/data-parser-tools/-/data-parser-tools-0.2.4.tgz", - "integrity": "sha512-QH8g/6u/Zr5F0IPHI8L+rg0vmZTvgzUsokrEJUZ5/iu5jHZPCjsmO9gYH0y7oJKBX6sHd7K6umH/oYjy1UuC0A==", + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/@duplojs/data-parser-tools/-/data-parser-tools-0.2.7.tgz", + "integrity": "sha512-RstLrT4Q/INGS9i9IKgA34LHxfQcBX38SO1TePBWLcUieZu8tEleSlIymtOr58sT7a9+LsTHfRxO9tLrNT5JTQ==", "license": "MIT", "peer": true, "workspaces": [ @@ -1055,7 +1040,8 @@ "node": ">=22.15.1" }, "peerDependencies": { - "@duplojs/utils": ">=1.4.36 <2.0.0" + "@duplojs/server-utils": ">=0.1.4 < 1.0.0", + "@duplojs/utils": ">=1.5.3 <2.0.0" } }, "node_modules/@duplojs/eslint": { @@ -1514,10 +1500,27 @@ "resolved": "", "link": true }, + "node_modules/@duplojs/server-utils": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/@duplojs/server-utils/-/server-utils-0.2.0.tgz", + "integrity": "sha512-RV2e2Dlezy96nzjikjUVg0rS7hMupXmygqW9e9DW1M1JdLqumuw4OiePp0810sVRn0wnFxJF3zC6W9X7oIfcxg==", + "license": "MIT", + "peer": true, + "workspaces": [ + "integration", + "docs" + ], + "engines": { + "node": ">=22.15.1" + }, + "peerDependencies": { + "@duplojs/utils": ">=1.5.2 <2.0.0" + } + }, "node_modules/@duplojs/utils": { - "version": "1.4.57", - "resolved": "https://registry.npmjs.org/@duplojs/utils/-/utils-1.4.57.tgz", - "integrity": "sha512-5WBW0NvR6l/v2nVKAYtZ+0jux965ni0uTq+aw1N3DXQ1OuRvEd3tZtIJpxSqfYJ9aAY2TtWtJAB85ZuPSSeunQ==", + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/@duplojs/utils/-/utils-1.5.4.tgz", + "integrity": "sha512-aLEathKzhUZJw/rCbPPg/dBvVJxAHMRno9FvS8vR2s87RKB7HtvK/kTgXp4OoNi9Nj+DlY0MyQR3hW4uhvc+0g==", "license": "MIT", "peer": true, "workspaces": [ @@ -1971,9 +1974,9 @@ } }, "node_modules/@eslint-community/eslint-utils": { - "version": "4.9.0", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.0.tgz", - "integrity": "sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==", + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", + "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", "dev": true, "license": "MIT", "dependencies": { @@ -2075,9 +2078,9 @@ } }, "node_modules/@eslint/eslintrc": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.1.tgz", - "integrity": "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==", + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.3.tgz", + "integrity": "sha512-Kr+LPIUVKz2qkx1HAMH8q1q6azbqBAsXJUxBl/ODDuVPX45Z9DfwB8tPjTi6nNZ8BuM3nbJxC5zCAg5elnBUTQ==", "dev": true, "license": "MIT", "dependencies": { @@ -2087,7 +2090,7 @@ "globals": "^14.0.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", - "js-yaml": "^4.1.0", + "js-yaml": "^4.1.1", "minimatch": "^3.1.2", "strip-json-comments": "^3.1.1" }, @@ -2415,13 +2418,13 @@ "optional": true }, "node_modules/@mermaid-js/parser": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/@mermaid-js/parser/-/parser-0.6.3.tgz", - "integrity": "sha512-lnjOhe7zyHjc+If7yT4zoedx2vo4sHaTmtkl1+or8BRTnCtDmcTpAjpzDSfCZrshM5bCoz0GyidzadJAH1xobA==", + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@mermaid-js/parser/-/parser-1.0.0.tgz", + "integrity": "sha512-vvK0Hi/VWndxoh03Mmz6wa1KDriSPjS2XMZL/1l19HFwygiObEEoEwSDxOqyLzzAI6J2PU3261JjTMTO7x+BPw==", "dev": true, "license": "MIT", "dependencies": { - "langium": "3.3.1" + "langium": "^4.0.0" } }, "node_modules/@nodelib/fs.scandir": { @@ -3759,6 +3762,7 @@ "version": "0.0.317", "resolved": "https://registry.npmjs.org/@types/web/-/web-0.0.317.tgz", "integrity": "sha512-Y3WXcQmFPsL6X915ZjtY2fhlKqIryBFj6mGHKOHc9Bk4uHvJxJpuG0j/YXHZ+fu6a8xqtGbv5aHoYPqD7Rkbjw==", + "dev": true, "license": "Apache-2.0" }, "node_modules/@types/web-bluetooth": { @@ -3769,15 +3773,15 @@ "license": "MIT" }, "node_modules/@typescript-eslint/project-service": { - "version": "8.46.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.46.2.tgz", - "integrity": "sha512-PULOLZ9iqwI7hXcmL4fVfIsBi6AN9YxRc0frbvmg8f+4hQAjQ5GYNKK0DIArNo+rOKmR/iBYwkpBmnIwin4wBg==", + "version": "8.56.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.56.0.tgz", + "integrity": "sha512-M3rnyL1vIQOMeWxTWIW096/TtVP+8W3p/XnaFflhmcFp+U4zlxUxWj4XwNs6HbDeTtN4yun0GNTTDBw/SvufKg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.46.2", - "@typescript-eslint/types": "^8.46.2", - "debug": "^4.3.4" + "@typescript-eslint/tsconfig-utils": "^8.56.0", + "@typescript-eslint/types": "^8.56.0", + "debug": "^4.4.3" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -3791,14 +3795,14 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.46.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.46.2.tgz", - "integrity": "sha512-LF4b/NmGvdWEHD2H4MsHD8ny6JpiVNDzrSZr3CsckEgCbAGZbYM4Cqxvi9L+WqDMT+51Ozy7lt2M+d0JLEuBqA==", + "version": "8.56.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.56.0.tgz", + "integrity": "sha512-7UiO/XwMHquH+ZzfVCfUNkIXlp/yQjjnlYUyYz7pfvlK3/EyyN6BK+emDmGNyQLBtLGaYrTAI6KOw8tFucWL2w==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.46.2", - "@typescript-eslint/visitor-keys": "8.46.2" + "@typescript-eslint/types": "8.56.0", + "@typescript-eslint/visitor-keys": "8.56.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -3809,9 +3813,9 @@ } }, "node_modules/@typescript-eslint/tsconfig-utils": { - "version": "8.46.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.46.2.tgz", - "integrity": "sha512-a7QH6fw4S57+F5y2FIxxSDyi5M4UfGF+Jl1bCGd7+L4KsaUY80GsiF/t0UoRFDHAguKlBaACWJRmdrc6Xfkkag==", + "version": "8.56.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.56.0.tgz", + "integrity": "sha512-bSJoIIt4o3lKXD3xmDh9chZcjCz5Lk8xS7Rxn+6l5/pKrDpkCwtQNQQwZ2qRPk7TkUYhrq3WPIHXOXlbXP0itg==", "dev": true, "license": "MIT", "engines": { @@ -3826,9 +3830,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "8.46.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.46.2.tgz", - "integrity": "sha512-lNCWCbq7rpg7qDsQrd3D6NyWYu+gkTENkG5IKYhUIcxSb59SQC/hEQ+MrG4sTgBVghTonNWq42bA/d4yYumldQ==", + "version": "8.56.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.56.0.tgz", + "integrity": "sha512-DBsLPs3GsWhX5HylbP9HNG15U0bnwut55Lx12bHB9MpXxQ+R5GC8MwQe+N1UFXxAeQDvEsEDY6ZYwX03K7Z6HQ==", "dev": true, "license": "MIT", "engines": { @@ -3840,22 +3844,21 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.46.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.46.2.tgz", - "integrity": "sha512-f7rW7LJ2b7Uh2EiQ+7sza6RDZnajbNbemn54Ob6fRwQbgcIn+GWfyuHDHRYgRoZu1P4AayVScrRW+YfbTvPQoQ==", + "version": "8.56.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.56.0.tgz", + "integrity": "sha512-ex1nTUMWrseMltXUHmR2GAQ4d+WjkZCT4f+4bVsps8QEdh0vlBsaCokKTPlnqBFqqGaxilDNJG7b8dolW2m43Q==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/project-service": "8.46.2", - "@typescript-eslint/tsconfig-utils": "8.46.2", - "@typescript-eslint/types": "8.46.2", - "@typescript-eslint/visitor-keys": "8.46.2", - "debug": "^4.3.4", - "fast-glob": "^3.3.2", - "is-glob": "^4.0.3", - "minimatch": "^9.0.4", - "semver": "^7.6.0", - "ts-api-utils": "^2.1.0" + "@typescript-eslint/project-service": "8.56.0", + "@typescript-eslint/tsconfig-utils": "8.56.0", + "@typescript-eslint/types": "8.56.0", + "@typescript-eslint/visitor-keys": "8.56.0", + "debug": "^4.4.3", + "minimatch": "^9.0.5", + "semver": "^7.7.3", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.4.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -3869,16 +3872,16 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "8.46.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.46.2.tgz", - "integrity": "sha512-sExxzucx0Tud5tE0XqR0lT0psBQvEpnpiul9XbGUB1QwpWJJAps1O/Z7hJxLGiZLBKMCutjTzDgmd1muEhBnVg==", + "version": "8.56.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.56.0.tgz", + "integrity": "sha512-RZ3Qsmi2nFGsS+n+kjLAYDPVlrzf7UhTffrDIKr+h2yzAlYP/y5ZulU0yeDEPItos2Ph46JAL5P/On3pe7kDIQ==", "dev": true, "license": "MIT", "dependencies": { - "@eslint-community/eslint-utils": "^4.7.0", - "@typescript-eslint/scope-manager": "8.46.2", - "@typescript-eslint/types": "8.46.2", - "@typescript-eslint/typescript-estree": "8.46.2" + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.56.0", + "@typescript-eslint/types": "8.56.0", + "@typescript-eslint/typescript-estree": "8.56.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -3888,19 +3891,19 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.46.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.46.2.tgz", - "integrity": "sha512-tUFMXI4gxzzMXt4xpGJEsBsTox0XbNQ1y94EwlD/CuZwFcQP79xfQqMhau9HsRc/J0cAPA/HZt1dZPtGn9V/7w==", + "version": "8.56.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.56.0.tgz", + "integrity": "sha512-q+SL+b+05Ud6LbEE35qe4A99P+htKTKVbyiNEe45eCbJFyh/HVK9QXwlrbz+Q4L8SOW4roxSVwXYj4DMBT7Ieg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.46.2", - "eslint-visitor-keys": "^4.2.1" + "@typescript-eslint/types": "8.56.0", + "eslint-visitor-keys": "^5.0.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -3910,6 +3913,19 @@ "url": "https://opencollective.com/typescript-eslint" } }, + "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.0.tgz", + "integrity": "sha512-A0XeIi7CXU7nPlfHS9loMYEKxUaONu/hTEzHTGba9Huu94Cq1hPivf+DE5erJozZOky0LfvXAyrV/tcswpLI0Q==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, "node_modules/@typescript/vfs": { "version": "1.6.2", "resolved": "https://registry.npmjs.org/@typescript/vfs/-/vfs-1.6.2.tgz", @@ -4440,9 +4456,9 @@ } }, "node_modules/ajv": { - "version": "8.17.1", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", - "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", + "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", "dev": true, "license": "MIT", "dependencies": { @@ -4831,18 +4847,18 @@ } }, "node_modules/chevrotain": { - "version": "11.0.3", - "resolved": "https://registry.npmjs.org/chevrotain/-/chevrotain-11.0.3.tgz", - "integrity": "sha512-ci2iJH6LeIkvP9eJW6gpueU8cnZhv85ELY8w8WiFtNjMHA5ad6pQLaJo9mEly/9qUyCpvqX8/POVUTf18/HFdw==", + "version": "11.1.1", + "resolved": "https://registry.npmjs.org/chevrotain/-/chevrotain-11.1.1.tgz", + "integrity": "sha512-f0yv5CPKaFxfsPTBzX7vGuim4oIC1/gcS7LUGdBSwl2dU6+FON6LVUksdOo1qJjoUvXNn45urgh8C+0a24pACQ==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@chevrotain/cst-dts-gen": "11.0.3", - "@chevrotain/gast": "11.0.3", - "@chevrotain/regexp-to-ast": "11.0.3", - "@chevrotain/types": "11.0.3", - "@chevrotain/utils": "11.0.3", - "lodash-es": "4.17.21" + "@chevrotain/cst-dts-gen": "11.1.1", + "@chevrotain/gast": "11.1.1", + "@chevrotain/regexp-to-ast": "11.1.1", + "@chevrotain/types": "11.1.1", + "@chevrotain/utils": "11.1.1", + "lodash-es": "4.17.23" } }, "node_modules/chevrotain-allstar": { @@ -4858,13 +4874,6 @@ "chevrotain": "^11.0.0" } }, - "node_modules/chevrotain/node_modules/lodash-es": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz", - "integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==", - "dev": true, - "license": "MIT" - }, "node_modules/chokidar": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", @@ -6885,6 +6894,7 @@ "version": "10.5.0", "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", "dev": true, "license": "ISC", "dependencies": { @@ -7647,20 +7657,21 @@ "dev": true }, "node_modules/langium": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/langium/-/langium-3.3.1.tgz", - "integrity": "sha512-QJv/h939gDpvT+9SiLVlY7tZC3xB2qK57v0J04Sh9wpMb6MP1q8gB21L3WIo8T5P1MSMg3Ep14L7KkDCFG3y4w==", + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/langium/-/langium-4.2.1.tgz", + "integrity": "sha512-zu9QWmjpzJcomzdJQAHgDVhLGq5bLosVak1KVa40NzQHXfqr4eAHupvnPOVXEoLkg6Ocefvf/93d//SB7du4YQ==", "dev": true, "license": "MIT", "dependencies": { - "chevrotain": "~11.0.3", - "chevrotain-allstar": "~0.3.0", + "chevrotain": "~11.1.1", + "chevrotain-allstar": "~0.3.1", "vscode-languageserver": "~9.0.1", "vscode-languageserver-textdocument": "~1.0.11", - "vscode-uri": "~3.0.8" + "vscode-uri": "~3.1.0" }, "engines": { - "node": ">=16.0.0" + "node": ">=20.10.0", + "npm": ">=10.2.3" } }, "node_modules/layout-base": { @@ -7871,9 +7882,9 @@ "license": "MIT" }, "node_modules/markdown-it": { - "version": "14.1.0", - "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.1.0.tgz", - "integrity": "sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg==", + "version": "14.1.1", + "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.1.1.tgz", + "integrity": "sha512-BuU2qnTti9YKgK5N+IeMubp14ZUKUUw7yeJbkjtosvHiP0AZ5c8IAgEMk79D0eC8F23r4Ac/q8cAIFdm2FtyoA==", "dev": true, "license": "MIT", "dependencies": { @@ -8221,15 +8232,15 @@ } }, "node_modules/mermaid": { - "version": "11.12.2", - "resolved": "https://registry.npmjs.org/mermaid/-/mermaid-11.12.2.tgz", - "integrity": "sha512-n34QPDPEKmaeCG4WDMGy0OT6PSyxKCfy2pJgShP+Qow2KLrvWjclwbc3yXfSIf4BanqWEhQEpngWwNp/XhZt6w==", + "version": "11.12.3", + "resolved": "https://registry.npmjs.org/mermaid/-/mermaid-11.12.3.tgz", + "integrity": "sha512-wN5ZSgJQIC+CHJut9xaKWsknLxaFBwCPwPkGTSUYrTiHORWvpT8RxGk849HPnpUAQ+/9BPRqYb80jTpearrHzQ==", "dev": true, "license": "MIT", "dependencies": { "@braintree/sanitize-url": "^7.1.1", "@iconify/utils": "^3.0.1", - "@mermaid-js/parser": "^0.6.3", + "@mermaid-js/parser": "^1.0.0", "@types/d3": "^7.4.3", "cytoscape": "^3.29.3", "cytoscape-cose-bilkent": "^4.1.0", @@ -8241,7 +8252,7 @@ "dompurify": "^3.2.5", "katex": "^0.16.22", "khroma": "^2.1.0", - "lodash-es": "^4.17.21", + "lodash-es": "^4.17.23", "marked": "^16.2.1", "roughjs": "^4.6.6", "stylis": "^4.3.6", @@ -8812,11 +8823,11 @@ } }, "node_modules/minipass": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", - "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", "dev": true, - "license": "ISC", + "license": "BlueOak-1.0.0", "engines": { "node": ">=16 || 14 >=14.17" } @@ -10259,9 +10270,9 @@ } }, "node_modules/ts-api-utils": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz", - "integrity": "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==", + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.4.0.tgz", + "integrity": "sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA==", "dev": true, "license": "MIT", "engines": { @@ -12299,9 +12310,9 @@ "license": "MIT" }, "node_modules/vscode-uri": { - "version": "3.0.8", - "resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.0.8.tgz", - "integrity": "sha512-AyFQ0EVmsOZOlAnxoFOGOq1SQDWAB7C6aqMGS23svWAllfOaxbuFvcT8D1i8z3Gyn8fraVeZNNmN6e9bxxXkKw==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.1.0.tgz", + "integrity": "sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==", "dev": true, "license": "MIT" }, diff --git a/package.json b/package.json index 4365870..7f71f12 100644 --- a/package.json +++ b/package.json @@ -65,8 +65,9 @@ "README.md" ], "peerDependencies": { - "@duplojs/data-parser-tools": ">=0.2.4 <1.0.0", - "@duplojs/utils": ">=1.4.57 <2.0.0" + "@duplojs/data-parser-tools": ">=0.2.7 <1.0.0", + "@duplojs/server-utils": ">=0.2.0 <1.0.0", + "@duplojs/utils": ">=1.5.4 <2.0.0" }, "devDependencies": { "@commitlint/cli": "19.8.1", @@ -76,6 +77,7 @@ "@types/bun": "^1.3.3", "@types/deno": "^2.5.0", "@types/node": "24.3.0", + "@types/web": "^0.0.317", "@vitest/coverage-istanbul": "3.2.4", "eslint": "9.34.0", "form-data": "^4.0.5", @@ -98,8 +100,5 @@ "keywords": [], "engines": { "node": ">=22.15.1" - }, - "dependencies": { - "@types/web": "^0.0.317" } } diff --git a/scripts/client/getBody.ts b/scripts/client/getBody.ts index 5cb6f1e..e58d21e 100644 --- a/scripts/client/getBody.ts +++ b/scripts/client/getBody.ts @@ -9,6 +9,6 @@ export function getBody(response: Response): Promise { } else if (responseContentType.includes("form-data")) { return response.formData(); } else { - return response.blob(); + return Promise.resolve(undefined); } } diff --git a/scripts/client/hooks.ts b/scripts/client/hooks.ts index f833055..5bb2fdd 100644 --- a/scripts/client/hooks.ts +++ b/scripts/client/hooks.ts @@ -1,66 +1,5 @@ /* eslint-disable @typescript-eslint/prefer-for-of */ -import { type MaybePromise } from "@duplojs/utils"; -import { type NotPredictedClientResponse, type ClientResponse } from "./types/clientResponse"; -import { type PromiseRequestParams } from "./promiseRequest"; - -export type RequestHook< - GenericPromiseRequestParams extends PromiseRequestParams = PromiseRequestParams, -> = (requestParams: GenericPromiseRequestParams) => MaybePromise; - -export type ResponseHook< - GenericPromiseRequestParams extends PromiseRequestParams = PromiseRequestParams, -> = ( - response: ClientResponse -) => MaybePromise>; - -export type InformationHook< - GenericPromiseRequestParams extends PromiseRequestParams = PromiseRequestParams, -> = ( - response: ClientResponse -) => MaybePromise; - -export type ResponseTypeHook< - GenericPromiseRequestParams extends PromiseRequestParams = PromiseRequestParams, -> = ( - response: ClientResponse -) => MaybePromise; - -export type ExpectedResponseHook< - GenericPromiseRequestParams extends PromiseRequestParams = PromiseRequestParams, -> = ( - response: ClientResponse -) => MaybePromise; - -export type CodeHook< - GenericPromiseRequestParams extends PromiseRequestParams = PromiseRequestParams, -> = ( - response: ClientResponse -) => MaybePromise; - -export type NotPredictedResponseHook< - GenericPromiseRequestParams extends PromiseRequestParams = PromiseRequestParams, -> = ( - response: NotPredictedClientResponse -) => MaybePromise; - -export type ErrorHook< - GenericPromiseRequestParams extends PromiseRequestParams = PromiseRequestParams, -> = (error: unknown, requestParams: GenericPromiseRequestParams) => MaybePromise; - -export interface Hooks { - request: RequestHook[]; - response: ResponseHook[]; - information: Record; - code: Record; - informationalResponseType: ResponseTypeHook[]; - successfulResponseType: ResponseTypeHook[]; - redirectionResponseType: ResponseTypeHook[]; - clientErrorResponseType: ResponseTypeHook[]; - serverErrorResponseType: ResponseTypeHook[]; - expectedResponse: ExpectedResponseHook[]; - notPredictedResponse: NotPredictedResponseHook[]; - error: ErrorHook[]; -} +import { type CodeHook, type ErrorHook, type InformationHook, type NotPredictedResponseHook, type RequestHook, type ResponseHook, type ResponseTypeHook, type PromiseRequestParams, type NotPredictedClientResponse, type ClientResponse } from "./types"; export async function launchRequestHook( clientHook: readonly RequestHook[], diff --git a/scripts/client/httpClient.ts b/scripts/client/httpClient.ts index 54542dc..4eb47bf 100644 --- a/scripts/client/httpClient.ts +++ b/scripts/client/httpClient.ts @@ -1,10 +1,9 @@ -import { type RemoveKind, type Kind, type MayBeGetter, type NeverCoalescing, type SimplifyTopLevel } from "@duplojs/utils"; +import { type RemoveKind, type Kind, type MayBeGetter, type SimplifyTopLevel, type IsEqual } from "@duplojs/utils"; import * as OO from "@duplojs/utils/object"; import * as GG from "@duplojs/utils/generator"; import { createClientKind } from "./kind"; -import { type ClientRequestInitParams, type ServerRoute, type ServerRouteToClientRequestParams, type ServerRouteToClientResponse, type ClientRequestParamsHeaders, type ClientRequestParams } from "./types"; -import { PromiseRequest, type PromiseRequestParams } from "./promiseRequest"; -import { type Hooks, type RequestHook, type ResponseHook, type InformationHook, type CodeHook, type ResponseTypeHook, type ExpectedResponseHook, type ErrorHook, type NotPredictedResponseHook } from "./hooks"; +import { type ClientRequestInitParams, type ServerRoute, type ServerRouteToClientRequestParams, type ServerRouteToClientResponse, type ClientRequestParamsHeaders, type ClientRequestParams, type ClientResponse, type Hooks, type RequestHook, type ResponseHook, type InformationHook, type CodeHook, type ResponseTypeHook, type ExpectedResponseHook, type NotPredictedResponseHook, type ErrorHook, type GetServerRoutePath } from "./types"; +import { PromiseRequest } from "./promiseRequest"; export const httpClientKind = createClientKind("http-client"); @@ -18,46 +17,57 @@ type HttpClientRequestMethod< GenericServerRoute extends ServerRoute, GenericHookParams extends Record, GenericMethod extends string, -> = < - GenericClientRequestParams extends NeverCoalescing< - ServerRouteToClientRequestParams< +> = IsEqual extends true + ? ( + path: string, + params?: SimplifyTopLevel< + Omit< + ClientRequestParams, + "method" | "path" + > + > + ) => PromiseRequest< + GenericHookParams, + ClientResponse + > + : < + GenericPath extends Extract["path"], + GenericMatchedPath extends GetServerRoutePath< Extract, - GenericHookParams + GenericPath >, - ClientRequestParams - >, - GenericPath extends GenericClientRequestParams["path"], - GenericClientRequestRest extends SimplifyTopLevel< - Omit< - NeverCoalescing< - Extract< - GenericClientRequestParams, - { path: GenericPath } - >, - ClientRequestParams - >, - "method" | "path" + >( + path: GenericPath, + ...args: MaybeRequestParams< + SimplifyTopLevel< + Omit< + ServerRouteToClientRequestParams< + Extract< + GenericServerRoute, + { + method: GenericMethod; + path: GenericMatchedPath | GenericPath; + } + >, + GenericHookParams + >, + "method" | "path" + > + > > - >, ->( - path: GenericPath, - ...args: MaybeRequestParams -) => PromiseRequest< - PromiseRequestParams, - ServerRouteToClientResponse< - NeverCoalescing< + ) => PromiseRequest< + GenericHookParams, + ServerRouteToClientResponse< Extract< GenericServerRoute, { method: GenericMethod; - path: GenericPath; + path: GenericMatchedPath; } >, - ServerRoute - >, - GenericHookParams - > ->; + GenericHookParams + > + >; export interface HttpClientConfig { readonly baseUrl: string; @@ -87,34 +97,38 @@ export interface HttpClient< > ): void; - addRequestHook(hook: RequestHook>): void; - addResponseHook(hook: ResponseHook>): void; - addInformationHook(information: string, hook: InformationHook>): void; - addCodeHook(code: string, hook: CodeHook>): void; - addInformationalResponseTypeHook(hook: ResponseTypeHook>): void; - addSuccessfulResponseTypeHook(hook: ResponseTypeHook>): void; - addRedirectionResponseTypeHook(hook: ResponseTypeHook>): void; - addClientErrorResponseTypeHook(hook: ResponseTypeHook>): void; - addServerErrorResponseTypeHook(hook: ResponseTypeHook>): void; - addExpectedResponseHook(hook: ExpectedResponseHook>): void; - addNotPredictedResponseHook(hook: NotPredictedResponseHook>): void; - addErrorHook(hook: ErrorHook>): void; + addRequestHook(hook: RequestHook): void; + addResponseHook(hook: ResponseHook): void; + addInformationHook(information: string, hook: InformationHook): void; + addCodeHook(code: string, hook: CodeHook): void; + addInformationalResponseTypeHook(hook: ResponseTypeHook): void; + addSuccessfulResponseTypeHook(hook: ResponseTypeHook): void; + addRedirectionResponseTypeHook(hook: ResponseTypeHook): void; + addClientErrorResponseTypeHook(hook: ResponseTypeHook): void; + addServerErrorResponseTypeHook(hook: ResponseTypeHook): void; + addExpectedResponseHook(hook: ExpectedResponseHook): void; + addNotPredictedResponseHook(hook: NotPredictedResponseHook): void; + addErrorHook(hook: ErrorHook): void; request< GenericClientRequestParams extends ServerRouteToClientRequestParams< GenericServerRoute, GenericHookParams >, + GenericMatchedPath extends GetServerRoutePath< + Extract, + GenericClientRequestParams["path"] + >, >( params: GenericClientRequestParams ): PromiseRequest< - PromiseRequestParams, + GenericHookParams, ServerRouteToClientResponse< Extract< GenericServerRoute, { method: GenericClientRequestParams["method"]; - path: GenericClientRequestParams["path"]; + path: GenericMatchedPath; } >, GenericHookParams @@ -163,12 +177,12 @@ export interface CreateHttpClientParams { } export function createHttpClient< - GenericServerRoute extends ServerRoute = never, + GenericServerRoute extends ServerRoute = ServerRoute, GenericHookParams extends Record = Record, >( clientParams: CreateHttpClientParams, ): HttpClient< - NeverCoalescing, + GenericServerRoute, GenericHookParams > { const hooks = OO.override( diff --git a/scripts/client/promiseRequest.ts b/scripts/client/promiseRequest.ts index 39f9898..3ae1a64 100644 --- a/scripts/client/promiseRequest.ts +++ b/scripts/client/promiseRequest.ts @@ -1,22 +1,22 @@ -import { type NeverCoalescing, type MaybePromise, unwrap } from "@duplojs/utils"; +import { type NeverCoalescing, type MaybePromise, unwrap, TheFormData } from "@duplojs/utils"; import { getBody } from "./getBody"; import { insertParamsInPath } from "./insertParamsInPath"; import { queryToString } from "./queryToString"; -import { type Hooks, launchRequestHook, launchResponseHook, launchInformationHook, launchCodeHook, launchResponseTypeHook, launchExpectedResponseHook, launchErrorHook, type ErrorHook, launchNotPredictedHook, type NotPredictedResponseHook } from "./hooks"; +import { launchRequestHook, launchResponseHook, launchInformationHook, launchCodeHook, launchResponseTypeHook, launchExpectedResponseHook, launchErrorHook, launchNotPredictedHook } from "./hooks"; import * as EE from "@duplojs/utils/either"; import * as SS from "@duplojs/utils/string"; import * as AA from "@duplojs/utils/array"; import { UnexpectedCodeResponseError, UnexpectedInformationResponseError, UnexpectedResponseError, UnexpectedResponseTypeError, type RequestErrorContent } from "./unexpectedResponseError"; -import { type NotPredictedClientResponse, type ClientRequestParams, type ClientResponse } from "./types"; +import { type NotPredictedClientResponse, type ClientResponse, type PromiseRequestParams, type Hooks, type NotPredictedResponseHook, type ErrorHook } from "./types"; type MaybeResponse< GenericClientResponse extends ClientResponse = ClientResponse, > = ( - | EE.EitherRight< + | EE.Right< "response", GenericClientResponse > - | EE.EitherLeft< + | EE.Left< "request-error", RequestErrorContent > @@ -26,37 +26,27 @@ type MaybeWantedResponse< GenericWantedClientResponse extends ClientResponse = ClientResponse, GenericUnexpectClientResponse extends ClientResponse = ClientResponse, > = ( - | EE.EitherRight< + | EE.Right< "response", GenericWantedClientResponse > - | EE.EitherLeft< + | EE.Left< "unexpect-response", GenericUnexpectClientResponse > - | EE.EitherLeft< + | EE.Left< "request-error", RequestErrorContent > ); -export interface PromiseRequestParams< - GenericHookParams extends Record = Record, -> extends ClientRequestParams { - baseUrl: string; - hooks: Hooks; - informationHeaderKey: string; - predictedHeaderKey: string; - disabledPredicateMode: boolean; -} - export class PromiseRequest< - GenericPromiseRequestParams extends PromiseRequestParams = PromiseRequestParams, - GenericClientResponse extends ClientResponse = ClientResponse, + GenericHookParams extends Record = Record, + GenericClientResponse extends ClientResponse = ClientResponse, > extends Promise< MaybeResponse< | GenericClientResponse - | NotPredictedClientResponse + | NotPredictedClientResponse > > { public readonly hooks: Partial = {}; @@ -180,7 +170,9 @@ export class PromiseRequest< } public addRequestInterceptor( - callback: (requestParams: GenericPromiseRequestParams) => MaybePromise, + callback: ( + requestParams: GenericClientResponse["requestParams"] + ) => MaybePromise, ) { this.hooks.request ??= []; this.hooks.request.push(callback as never); @@ -198,7 +190,7 @@ export class PromiseRequest< } public whenNotPredictedResponse( - callback: NotPredictedResponseHook, + callback: NotPredictedResponseHook, ) { this.hooks.notPredictedResponse ??= []; this.hooks.notPredictedResponse.push(callback as never); @@ -221,7 +213,7 @@ export class PromiseRequest< ? { information: GenericInformation } : never >, - ClientResponse + ClientResponse >, ) => MaybePromise, ) { @@ -250,7 +242,7 @@ export class PromiseRequest< ? { code: GenericCode } : never >, - ClientResponse + ClientResponse >, ) => MaybePromise, ) { @@ -274,7 +266,7 @@ export class PromiseRequest< GenericClientResponse, { code: `1${number}` } >, - ClientResponse + ClientResponse >, ) => MaybePromise, ) { @@ -291,7 +283,7 @@ export class PromiseRequest< GenericClientResponse, { code: `2${number}` } >, - ClientResponse + ClientResponse >, ) => MaybePromise, ) { @@ -308,7 +300,7 @@ export class PromiseRequest< GenericClientResponse, { code: `3${number}` } >, - ClientResponse + ClientResponse >, ) => MaybePromise, ) { @@ -325,7 +317,7 @@ export class PromiseRequest< GenericClientResponse, { code: `4${number}` } >, - ClientResponse + ClientResponse >, ) => MaybePromise, ) { @@ -342,7 +334,7 @@ export class PromiseRequest< GenericClientResponse, { code: `5${number}` } >, - ClientResponse + ClientResponse >, ) => MaybePromise, ) { @@ -359,7 +351,7 @@ export class PromiseRequest< GenericClientResponse, { code: `2${number}` | `4${number}` } >, - ClientResponse + ClientResponse >, ) => MaybePromise, ) { @@ -369,7 +361,7 @@ export class PromiseRequest< return this; } - public whenError(callback: ErrorHook) { + public whenError(callback: ErrorHook) { this.hooks.error ??= []; this.hooks.error.push(callback as never); @@ -388,7 +380,7 @@ export class PromiseRequest< ? { information: GenericInformation } : never >, - ClientResponse + ClientResponse >, >( information: GenericInformation | GenericInformation[], @@ -397,7 +389,7 @@ export class PromiseRequest< GenericResponse, NeverCoalescing< Exclude, - ClientResponse + ClientResponse > > > { @@ -434,7 +426,7 @@ export class PromiseRequest< ? { code: GenericCode } : never >, - ClientResponse + ClientResponse >, >( code: GenericCode | GenericCode[], @@ -443,7 +435,7 @@ export class PromiseRequest< GenericResponse, NeverCoalescing< Exclude, - ClientResponse + ClientResponse > > > { @@ -474,14 +466,14 @@ export class PromiseRequest< GenericClientResponse, { code: `1${number}` } >, - ClientResponse + ClientResponse >, >(): Promise< MaybeWantedResponse< GenericResponse, NeverCoalescing< Exclude, - ClientResponse + ClientResponse > > > { @@ -510,14 +502,14 @@ export class PromiseRequest< GenericClientResponse, { code: `2${number}` } >, - ClientResponse + ClientResponse >, >(): Promise< MaybeWantedResponse< GenericResponse, NeverCoalescing< Exclude, - ClientResponse + ClientResponse > > > { @@ -546,14 +538,14 @@ export class PromiseRequest< GenericClientResponse, { code: `3${number}` } >, - ClientResponse + ClientResponse >, >(): Promise< MaybeWantedResponse< GenericResponse, NeverCoalescing< Exclude, - ClientResponse + ClientResponse > > > { @@ -582,14 +574,14 @@ export class PromiseRequest< GenericClientResponse, { code: `4${number}` } >, - ClientResponse + ClientResponse >, >(): Promise< MaybeWantedResponse< GenericResponse, NeverCoalescing< Exclude, - ClientResponse + ClientResponse > > > { @@ -618,14 +610,14 @@ export class PromiseRequest< GenericClientResponse, { code: `5${number}` } >, - ClientResponse + ClientResponse >, >(): Promise< MaybeWantedResponse< GenericResponse, NeverCoalescing< Exclude, - ClientResponse + ClientResponse > > > { @@ -654,14 +646,14 @@ export class PromiseRequest< GenericClientResponse, { code: `2${number}` | `4${number}` } >, - ClientResponse + ClientResponse >, >(): Promise< MaybeWantedResponse< GenericResponse, NeverCoalescing< Exclude, - ClientResponse + ClientResponse > > > { @@ -699,7 +691,7 @@ export class PromiseRequest< ? { information: GenericInformation } : never >, - ClientResponse + ClientResponse > > { return this @@ -730,7 +722,7 @@ export class PromiseRequest< ? { code: GenericCode } : never >, - ClientResponse + ClientResponse > > { return this @@ -755,7 +747,7 @@ export class PromiseRequest< GenericClientResponse, { code: `1${number}` } >, - ClientResponse + ClientResponse > > { return this @@ -780,7 +772,7 @@ export class PromiseRequest< GenericClientResponse, { code: `2${number}` } >, - ClientResponse + ClientResponse > > { return this @@ -805,7 +797,7 @@ export class PromiseRequest< GenericClientResponse, { code: `3${number}` } >, - ClientResponse + ClientResponse > > { return this @@ -830,7 +822,7 @@ export class PromiseRequest< GenericClientResponse, { code: `4${number}` } >, - ClientResponse + ClientResponse > > { return this @@ -855,7 +847,7 @@ export class PromiseRequest< GenericClientResponse, { code: `5${number}` } >, - ClientResponse + ClientResponse > > { return this @@ -880,7 +872,7 @@ export class PromiseRequest< GenericClientResponse, { code: `2${number}` | `4${number}` } >, - ClientResponse + ClientResponse > > { return this @@ -921,6 +913,8 @@ export class PromiseRequest< if (typeof body === "string") { headers["content-type"] = "text/plain; charset=utf-8"; body = body.toString(); + } else if (body instanceof TheFormData) { + headers["content-type-options"] = "advanced"; } else if ( ( body diff --git a/scripts/client/types/clientRequestParams.ts b/scripts/client/types/clientRequestParams.ts index 97f8c62..82767a9 100644 --- a/scripts/client/types/clientRequestParams.ts +++ b/scripts/client/types/clientRequestParams.ts @@ -1,4 +1,4 @@ -import { type SimplifyTopLevel, type IsEqual, type MaybeArray, type AnyTuple, type Json } from "@duplojs/utils"; +import { type SimplifyTopLevel, type IsEqual, type MaybeArray, type AnyTuple } from "@duplojs/utils"; import { type ServerRouteHeaders, type ServerRouteParams, type ServerRouteQuery, type ServerRoute, type ServerPrimitiveData } from "./serverRoute"; import { type ObjectCanBeEmpty } from "./ObjectCanBeEmpty"; import type * as OO from "@duplojs/utils/object"; diff --git a/scripts/client/types/clientResponse.ts b/scripts/client/types/clientResponse.ts index a1a6d75..545aca0 100644 --- a/scripts/client/types/clientResponse.ts +++ b/scripts/client/types/clientResponse.ts @@ -1,12 +1,12 @@ import type * as SS from "@duplojs/utils/string"; import { type ServerRouteResponse, type ServerRoute } from "./serverRoute"; import { type IsEqual, type SimplifyTopLevel } from "@duplojs/utils"; -import { type PromiseRequestParams } from "@client/promiseRequest"; +import { type PromiseRequestParams } from "./promiseRequestParams"; export type ClientResponseBody = unknown; export interface ClientResponse< - GenericPromiseRequestParams extends PromiseRequestParams = PromiseRequestParams, + GenericHookParams extends Record = Record, > { code: SS.Number; information: undefined | string; @@ -17,39 +17,39 @@ export interface ClientResponse< url: string; redirected: boolean; raw: globalThis.Response; - requestParams: GenericPromiseRequestParams; + requestParams: PromiseRequestParams; predicted: boolean; } export interface NotPredictedClientResponse< - GenericPromiseRequestParams extends PromiseRequestParams = PromiseRequestParams, -> extends ClientResponse { + GenericHookParams extends Record = Record, +> extends ClientResponse { predicted: false; } export type ServerRouteToClientResponse< GenericServerRoute extends ServerRoute = ServerRoute, GenericHookParams extends Record = Record, -> = IsEqual extends true - ? ClientResponse> - : GenericServerRoute extends any - ? GenericServerRoute["responses"] extends infer InferredResponse - ? InferredResponse extends ServerRouteResponse - ? SimplifyTopLevel<{ - code: InferredResponse["code"]; - information: InferredResponse["information"]; - body: InferredResponse["body"]; - ok: boolean | null; - headers: Headers; - type: ResponseType; - url: string; - redirected: boolean; - raw: globalThis.Response; - requestParams: PromiseRequestParams; - predicted: boolean; - }> extends infer InferredResult extends ClientResponse - ? InferredResult - : never +> = GenericServerRoute extends any + ? GenericServerRoute["responses"] extends infer InferredResponse + ? InferredResponse extends ServerRouteResponse + ? SimplifyTopLevel<{ + code: InferredResponse["code"]; + information: InferredResponse["information"]; + body: IsEqual extends true + ? undefined + : InferredResponse["body"]; + ok: boolean | null; + headers: Headers; + type: ResponseType; + url: string; + redirected: boolean; + raw: globalThis.Response; + requestParams: PromiseRequestParams; + predicted: boolean; + }> extends infer InferredResult extends ClientResponse + ? InferredResult : never : never - : never; + : never + : never; diff --git a/scripts/client/types/hooks.ts b/scripts/client/types/hooks.ts new file mode 100644 index 0000000..0ac0831 --- /dev/null +++ b/scripts/client/types/hooks.ts @@ -0,0 +1,62 @@ +import { type MaybePromise } from "@duplojs/utils"; +import { type NotPredictedClientResponse, type ClientResponse } from "./clientResponse"; +import { type PromiseRequestParams } from "./promiseRequestParams"; + +export type RequestHook< + GenericHookParams extends Record = Record, +> = (requestParams: PromiseRequestParams) => MaybePromise>; + +export type ResponseHook< + GenericHookParams extends Record = Record, +> = ( + response: ClientResponse +) => MaybePromise>; + +export type InformationHook< + GenericHookParams extends Record = Record, +> = ( + response: ClientResponse +) => MaybePromise; + +export type ResponseTypeHook< + GenericHookParams extends Record = Record, +> = ( + response: ClientResponse +) => MaybePromise; + +export type ExpectedResponseHook< + GenericHookParams extends Record = Record, +> = ( + response: ClientResponse +) => MaybePromise; + +export type CodeHook< + GenericHookParams extends Record = Record, +> = ( + response: ClientResponse +) => MaybePromise; + +export type NotPredictedResponseHook< + GenericHookParams extends Record = Record, +> = ( + response: NotPredictedClientResponse +) => MaybePromise; + +export type ErrorHook< + GenericHookParams extends Record = Record, +> = (error: unknown, requestParams: PromiseRequestParams) => MaybePromise; + +export interface Hooks { + request: RequestHook[]; + response: ResponseHook[]; + information: Record; + code: Record; + informationalResponseType: ResponseTypeHook[]; + successfulResponseType: ResponseTypeHook[]; + redirectionResponseType: ResponseTypeHook[]; + clientErrorResponseType: ResponseTypeHook[]; + serverErrorResponseType: ResponseTypeHook[]; + expectedResponse: ExpectedResponseHook[]; + notPredictedResponse: NotPredictedResponseHook[]; + error: ErrorHook[]; +} diff --git a/scripts/client/types/index.ts b/scripts/client/types/index.ts index 872e3d0..1425ca8 100644 --- a/scripts/client/types/index.ts +++ b/scripts/client/types/index.ts @@ -2,3 +2,5 @@ export * from "./clientRequestParams"; export * from "./clientResponse"; export * from "./serverRoute"; export * from "./ObjectCanBeEmpty"; +export * from "./promiseRequestParams"; +export * from "./hooks"; diff --git a/scripts/client/types/promiseRequestParams.ts b/scripts/client/types/promiseRequestParams.ts new file mode 100644 index 0000000..c236f76 --- /dev/null +++ b/scripts/client/types/promiseRequestParams.ts @@ -0,0 +1,12 @@ +import { type ClientRequestParams } from "./clientRequestParams"; +import { type Hooks } from "./hooks"; + +export interface PromiseRequestParams< + GenericHookParams extends Record = Record, +> extends ClientRequestParams { + baseUrl: string; + hooks: Hooks; + informationHeaderKey: string; + predictedHeaderKey: string; + disabledPredicateMode: boolean; +} diff --git a/scripts/client/types/serverRoute.ts b/scripts/client/types/serverRoute.ts index fdce201..8f238e7 100644 --- a/scripts/client/types/serverRoute.ts +++ b/scripts/client/types/serverRoute.ts @@ -1,4 +1,4 @@ -import { type MaybeArray } from "@duplojs/utils"; +import { type SimplifyTopLevel, type MaybeArray, type IsEqual } from "@duplojs/utils"; import type * as SS from "@duplojs/utils/string"; export type ServerPrimitiveData = string | undefined | number | null | boolean; @@ -28,3 +28,58 @@ export interface ServerRoute { body?: ServerRouteBody; responses: ServerRouteResponse; } + +export type GetServerRoutePath< + GenericServerRoute extends ServerRoute, + GenericPath extends GenericServerRoute["path"], +> = GenericServerRoute extends ServerRoute + ? IsEqual< + Extract, + never + > extends true + ? never + : GenericServerRoute["path"] + : never; + +export type AddPrefixPathServerRoute< + GenericRoute extends ServerRoute, + GenericPrefix extends string, +> = GenericRoute extends ServerRoute + ? SimplifyTopLevel< + { path: `${GenericPrefix}${GenericRoute["path"]}` } + & Omit + > + : never; + +export type RemovePrefixPathServerRoute< + GenericRoute extends ServerRoute, + GenericPrefix extends string, +> = GenericRoute extends ServerRoute + ? GenericRoute["path"] extends `${GenericPrefix}${infer InferredPathRest}` + ? SimplifyTopLevel< + { path: InferredPathRest } + & Omit + > + : GenericRoute + : never; + +export type FindServerRoute< + GenericRoute extends ServerRoute, + GenericMethod extends GenericRoute["method"], + GenericPath extends Extract["path"] = Extract["path"], +> = Extract< + GenericRoute, + { + method: GenericMethod; + path: GenericPath; + } +>; + +export type FindServerRouteResponse< + GenericRoute extends ServerRoute, + GenericKey extends "code" | "information", + GenericValue extends GenericRoute["responses"][GenericKey] = GenericRoute["responses"][GenericKey], +> = Extract< + GenericRoute["responses"], + { [Prop in GenericKey]: GenericValue } +>; diff --git a/scripts/client/unexpectedResponseError.ts b/scripts/client/unexpectedResponseError.ts index 4320864..0522795 100644 --- a/scripts/client/unexpectedResponseError.ts +++ b/scripts/client/unexpectedResponseError.ts @@ -1,7 +1,6 @@ import { kindHeritage } from "@duplojs/utils"; import { createClientKind } from "./kind"; -import { type ClientResponse } from "./types"; -import { type PromiseRequestParams } from "./promiseRequest"; +import { type PromiseRequestParams, type ClientResponse } from "./types"; export interface RequestErrorContent { error: unknown; diff --git a/scripts/core/builders/preflight/route.ts b/scripts/core/builders/preflight/route.ts index ee9ab18..c7e13e2 100644 --- a/scripts/core/builders/preflight/route.ts +++ b/scripts/core/builders/preflight/route.ts @@ -1,5 +1,5 @@ import { type Floor } from "@core/floor"; -import { type RequestMethods, type Request } from "@core/request"; +import { type RequestMethods, type Request, type BodyController } from "@core/request"; import { preflightBuilder } from "./builder"; import { type MakeRequestFromHooks, type HookRouteLifeCycle, type RoutePath } from "@core/route"; import { routeBuilderHandler, type RouteBuilder } from "../route"; @@ -17,12 +17,14 @@ declare module "./builder" { const GenericPaths extends RoutePath | readonly [RoutePath, ...RoutePath[]], const GenericHooks extends readonly HookRouteLifeCycle[] = readonly [], const GenericMetadata extends readonly Metadata[] = readonly [], + const GenericBodyController extends BodyController | null = null, >( method: GenericMethod, path: GenericPaths, options?: { hooks?: GenericHooks | readonly HookRouteLifeCycle[]; metadata?: GenericMetadata; + bodyController?: GenericBodyController; }, ): RouteBuilder< { @@ -40,6 +42,7 @@ declare module "./builder" { ...GenericMetadata, ...GenericDefinition["metadata"], ]; + readonly bodyController: GenericBodyController; }, GenericFloor, ( @@ -75,5 +78,6 @@ preflightBuilder.set( ...(options?.metadata ?? []), ...accumulator.metadata, ], + bodyController: options?.bodyController ?? null, }), ); diff --git a/scripts/core/builders/route/builder.ts b/scripts/core/builders/route/builder.ts index 1b15f2a..dac8e86 100644 --- a/scripts/core/builders/route/builder.ts +++ b/scripts/core/builders/route/builder.ts @@ -1,6 +1,6 @@ import { type MakeRequestFromHooks, type HookRouteLifeCycle, type RouteDefinition, type RoutePath } from "@core/route"; import { type Floor } from "@core/floor"; -import { type RequestMethods, type Request } from "@core/request"; +import { type RequestMethods, type Request, type BodyController } from "@core/request"; import { A, type Builder, createBuilder, type NeverCoalescing } from "@duplojs/utils"; import { createCoreLibStringIdentifier } from "@core/stringIdentifier"; import { type Metadata } from "@core/metadata"; @@ -20,12 +20,14 @@ export function useRouteBuilder< const GenericPaths extends RoutePath | readonly [RoutePath, ...RoutePath[]], const GenericHooks extends readonly HookRouteLifeCycle[] = readonly [], const GenericMetadata extends readonly Metadata[] = readonly [], + const GenericBodyController extends BodyController | null = null, >( method: GenericMethod, path: GenericPaths, options?: { hooks?: GenericHooks | readonly HookRouteLifeCycle[]; metadata?: GenericMetadata; + bodyController?: GenericBodyController; }, ): RouteBuilder< { @@ -37,6 +39,7 @@ export function useRouteBuilder< readonly steps: readonly []; readonly hooks: GenericHooks; readonly metadata: GenericMetadata; + readonly bodyController: GenericBodyController; }, {}, NeverCoalescing< @@ -51,5 +54,6 @@ export function useRouteBuilder< steps: [], hooks: options?.hooks ?? [], metadata: options?.metadata ?? [], + bodyController: options?.bodyController ?? null, }); } diff --git a/scripts/core/clean/newType.ts b/scripts/core/clean/newType.ts index ddaf3e0..a776d36 100644 --- a/scripts/core/clean/newType.ts +++ b/scripts/core/clean/newType.ts @@ -5,7 +5,8 @@ declare module "@duplojs/utils/clean" { interface NewTypeHandler< GenericName extends string = string, GenericValue extends unknown = unknown, - GenericConstraintsHandler extends readonly ConstraintHandler[] = readonly [], + GenericConstraintsHandler extends readonly ConstraintHandler[] = readonly ConstraintHandler[], + GenericInput extends unknown = unknown, > { toExtractParser(): DPE.ContractExtended< NewType< diff --git a/scripts/core/clean/primitive.ts b/scripts/core/clean/primitive.ts index 27c717a..c31b18c 100644 --- a/scripts/core/clean/primitive.ts +++ b/scripts/core/clean/primitive.ts @@ -10,7 +10,10 @@ declare module "@duplojs/utils/clean" { unknown >; - toEndpointSchema(): DPE.ContractExtended; + toEndpointSchema(): DPE.ContractExtended< + GenericValue, + unknown + >; } } diff --git a/scripts/core/defaultHooks/index.ts b/scripts/core/defaultHooks/index.ts new file mode 100644 index 0000000..fdd09b7 --- /dev/null +++ b/scripts/core/defaultHooks/index.ts @@ -0,0 +1,56 @@ +import { type Hub } from "@core/hub"; +import { HookResponse, PredictedResponse } from "@core/response"; +import { createHookRouteLifeCycle } from "@core/route"; +import { type HttpServerParams } from "@core/types"; +import { SF } from "@duplojs/server-utils"; + +export function initDefaultHook( + hub: Hub, + serverParams: HttpServerParams, +) { + const informationHeaderKey = serverParams.informationHeaderKey; + const predictedHeaderKey = serverParams.predictedHeaderKey; + const fromHookHeaderKey = serverParams.fromHookHeaderKey; + const isDev = hub.config.environment === "DEV"; + + return createHookRouteLifeCycle({ + beforeSendResponse({ currentResponse, next }) { + if (!currentResponse.headers?.["content-type"]) { + const body = currentResponse.body; + + if ( + typeof body === "string" + || body instanceof Error + ) { + currentResponse.setHeader("content-type", "text/plain; charset=utf-8"); + } else if (SF.isFileInterface(body)) { + const filename = body.getName(); + const filenameHeader = filename + ? ` filename="${filename}"` + : ""; + currentResponse + .setHeader("content-type", body.getMimeType() ?? "application/octet-stream") + .setHeader("content-disposition", `attachment;${filenameHeader}`); + } else if ( + typeof body === "object" + || typeof body === "number" + || typeof body === "boolean" + + ) { + currentResponse.setHeader("content-type", "application/json; charset=utf-8"); + } + } + + currentResponse.setHeader(informationHeaderKey, currentResponse.information); + + if (currentResponse instanceof PredictedResponse) { + currentResponse.setHeader(predictedHeaderKey, "1"); + } else if (currentResponse instanceof HookResponse) { + currentResponse.setHeader(fromHookHeaderKey, currentResponse.fromHook); + } + + return next(); + }, + + }); +} diff --git a/scripts/interfaces/node/error/bodyParseWrongChunkReceived.ts b/scripts/core/errors/bodyParseWrongChunkReceived.ts similarity index 53% rename from scripts/interfaces/node/error/bodyParseWrongChunkReceived.ts rename to scripts/core/errors/bodyParseWrongChunkReceived.ts index 4ef7125..2e0874e 100644 --- a/scripts/interfaces/node/error/bodyParseWrongChunkReceived.ts +++ b/scripts/core/errors/bodyParseWrongChunkReceived.ts @@ -1,14 +1,15 @@ +import { createCoreLibKind } from "@core/kind"; import { kindHeritage } from "@duplojs/utils"; -import { createInterfacesNodeLibKind } from "@interface-node/kind"; export class BodyParseWrongChunkReceived extends kindHeritage( "body-parse-wrong-chunk-received", - createInterfacesNodeLibKind("body-parse-wrong-chunk-received"), + createCoreLibKind("body-parse-wrong-chunk-received"), Error, ) { public constructor( + public information: string, public wrongChunk: unknown, ) { - super({}, ["Received chunk is not buffer or string."]); + super({}, [`Received chunk is not ${information}`]); } } diff --git a/scripts/interfaces/node/error/bodySizeExceedsLimitError.ts b/scripts/core/errors/bodySizeExceedsLimitError.ts similarity index 70% rename from scripts/interfaces/node/error/bodySizeExceedsLimitError.ts rename to scripts/core/errors/bodySizeExceedsLimitError.ts index a6f19e0..390e2a8 100644 --- a/scripts/interfaces/node/error/bodySizeExceedsLimitError.ts +++ b/scripts/core/errors/bodySizeExceedsLimitError.ts @@ -1,9 +1,9 @@ +import { createCoreLibKind } from "@core/kind"; import { type BytesInString, kindHeritage } from "@duplojs/utils"; -import { createInterfacesNodeLibKind } from "@interface-node/kind"; export class BodySizeExceedsLimitError extends kindHeritage( "body-size-exceeds-limit-error", - createInterfacesNodeLibKind("body-size-exceeds-limit-error"), + createCoreLibKind("body-size-exceeds-limit-error"), Error, ) { public constructor( diff --git a/scripts/interfaces/node/error/index.ts b/scripts/core/errors/index.ts similarity index 55% rename from scripts/interfaces/node/error/index.ts rename to scripts/core/errors/index.ts index 7d1788a..29760df 100644 --- a/scripts/interfaces/node/error/index.ts +++ b/scripts/core/errors/index.ts @@ -1,3 +1,4 @@ -export * from "./bodySizeExceedsLimitError"; +export * from "./wrongContentTypeError"; export * from "./bodyParseWrongChunkReceived"; -export * from "./bodyParseUnknownError"; +export * from "./bodySizeExceedsLimitError"; +export * from "./parseJsonError"; diff --git a/scripts/core/errors/parseJsonError.ts b/scripts/core/errors/parseJsonError.ts new file mode 100644 index 0000000..ce7cc1b --- /dev/null +++ b/scripts/core/errors/parseJsonError.ts @@ -0,0 +1,15 @@ +import { createCoreLibKind } from "@core/kind"; +import { kindHeritage } from "@duplojs/utils"; + +export class ParseJsonError extends kindHeritage( + "parse-json-error", + createCoreLibKind("parse-json-error"), + Error, +) { + public constructor( + public payload: string, + public error: unknown, + ) { + super({}, ["Error when parse on json."]); + } +} diff --git a/scripts/core/errors/wrongContentTypeError.ts b/scripts/core/errors/wrongContentTypeError.ts new file mode 100644 index 0000000..06d4500 --- /dev/null +++ b/scripts/core/errors/wrongContentTypeError.ts @@ -0,0 +1,15 @@ +import { createCoreLibKind } from "@core/kind"; +import { kindHeritage } from "@duplojs/utils"; + +export class WrongContentTypeError extends kindHeritage( + "wrong-content-type-error", + createCoreLibKind("wrong-content-type-error"), + Error, +) { + public constructor( + public expectedContentType: string, + public contentType: string, + ) { + super({}, [`expect content-type "${expectedContentType}" but receive "${contentType}".`]); + } +} diff --git a/scripts/core/functionsBuilders/route/create.ts b/scripts/core/functionsBuilders/route/create.ts index c6fe132..0b718f5 100644 --- a/scripts/core/functionsBuilders/route/create.ts +++ b/scripts/core/functionsBuilders/route/create.ts @@ -11,9 +11,9 @@ export type BuildedRouteFunction = ( ) => Promise; export type BuildRouteSuccessEither< -> = E.EitherRight<"buildSuccess", BuildedRouteFunction>; +> = E.Right<"buildSuccess", BuildedRouteFunction>; -export type BuildRouteNotSupportEither = E.EitherLeft<"routeNotSupport", Route>; +export type BuildRouteNotSupportEither = E.Left<"routeNotSupport", Route>; export interface RouteFunctionBuilderParams { readonly globalHooksRouteLifeCycle: readonly HookRouteLifeCycle[]; diff --git a/scripts/core/functionsBuilders/route/default.ts b/scripts/core/functionsBuilders/route/default.ts index 640f49c..08c956d 100644 --- a/scripts/core/functionsBuilders/route/default.ts +++ b/scripts/core/functionsBuilders/route/default.ts @@ -1,5 +1,5 @@ /* eslint-disable @typescript-eslint/prefer-for-of */ -import { type HookAfterSendResponse, type HookBeforeRouteExecution, type HookBeforeSendResponse, type HookError, type HookOnConstructRequest, type HookParseBody, type HookRouteLifeCycle, type HookSendResponse, routeKind } from "@core/route"; +import { type HookAfterSendResponse, type HookBeforeRouteExecution, type HookBeforeSendResponse, type HookError, type HookOnConstructRequest, type HookRouteLifeCycle, type HookSendResponse, routeKind } from "@core/route"; import { A, E, forward, isType, pipe } from "@duplojs/utils"; import { HookResponse, Response } from "@core/response"; import { type Request } from "@core/request"; @@ -82,12 +82,6 @@ export const defaultRouteFunctionBuilder = createRouteFunctionBuilder( A.filter(isType("function")), forward, ); - const hookParseBody: HookParseBody[] = pipe( - allHooks, - A.map(({ parseBody }) => parseBody), - A.filter(isType("function")), - forward, - ); const hookError: HookError[] = pipe( allHooks, A.map(({ error }) => error), @@ -119,7 +113,6 @@ export const defaultRouteFunctionBuilder = createRouteFunctionBuilder( return newRequest; } : (params) => params.request, - parseBody: buildHookBefore(hookParseBody), error: buildHookErrorBefore(hookError), sendResponse: buildHookAfter(hookSendResponse), }; @@ -149,17 +142,6 @@ export const defaultRouteFunctionBuilder = createRouteFunctionBuilder( floor = result; } - const parseBodyResult = await hooks.parseBody({ - request, - exit: exitHookFunction, - next: nextHookFunction, - response: createHookResponse("parseBody"), - }); - - if (parseBodyResult instanceof Response) { - return parseBodyResult; - } - for (let index = 0; index < buildedSteps.length; index++) { const result = await buildedSteps[index]!.buildedFunction(request, floor); diff --git a/scripts/core/functionsBuilders/route/hook.ts b/scripts/core/functionsBuilders/route/hook.ts index 33f8808..bd9f91a 100644 --- a/scripts/core/functionsBuilders/route/hook.ts +++ b/scripts/core/functionsBuilders/route/hook.ts @@ -1,6 +1,6 @@ /* eslint-disable @typescript-eslint/prefer-for-of */ import { HookResponse } from "@core/response"; -import { type HookAfterSendResponse, type HookBeforeRouteExecution, type HookBeforeSendResponse, type HookError, type HookParseBody, hookRouteExitKind, type HookRouteLifeCycle, hookRouteNextKind, type HookSendResponse, type RouteHookErrorParams, type RouteHookParams, type RouteHookParamsAfter } from "@core/route"; +import { type HookAfterSendResponse, type HookBeforeRouteExecution, type HookBeforeSendResponse, type HookError, hookRouteExitKind, type HookRouteLifeCycle, hookRouteNextKind, type HookSendResponse, type RouteHookErrorParams, type RouteHookParams, type RouteHookParamsAfter } from "@core/route"; const hookExit = hookRouteExitKind.setTo({}); const hookNext = hookRouteNextKind.setTo({}); @@ -16,7 +16,6 @@ export function nextHookFunction() { export function buildHookBefore( hooks: ( | HookBeforeRouteExecution - | HookParseBody )[], ) { if (!hooks.length) { diff --git a/scripts/core/functionsBuilders/steps/create.ts b/scripts/core/functionsBuilders/steps/create.ts index c3be206..5886064 100644 --- a/scripts/core/functionsBuilders/steps/create.ts +++ b/scripts/core/functionsBuilders/steps/create.ts @@ -17,9 +17,9 @@ export interface BuildStepResult { } export type BuildStepSuccessEither< -> = E.EitherRight<"buildSuccess", BuildStepResult>; +> = E.Right<"buildSuccess", BuildStepResult>; -export type BuildStepNotSupportEither = E.EitherLeft<"stepNotSupport", Steps>; +export type BuildStepNotSupportEither = E.Left<"stepNotSupport", Steps>; export interface StepFunctionBuilderParams { buildStep( diff --git a/scripts/core/functionsBuilders/steps/defaults/cutStep.ts b/scripts/core/functionsBuilders/steps/defaults/cutStep.ts index f2fa7dc..26859ba 100644 --- a/scripts/core/functionsBuilders/steps/defaults/cutStep.ts +++ b/scripts/core/functionsBuilders/steps/defaults/cutStep.ts @@ -1,8 +1,7 @@ -import { type CutStepDefinition, type CutStepFunctionParams, cutStepKind, cutStepOutputKind } from "@core/steps"; +import { type CutStepFunctionParams, cutStepKind, cutStepOutputKind } from "@core/steps"; import { createStepFunctionBuilder } from "../create"; import { A, E, unwrap, wrapValue } from "@duplojs/utils"; -import { PredictedResponse, Response, ResponseContract } from "@core/response"; -import { type Floor } from "@core/floor"; +import { PredictedResponse, ResponseContract } from "@core/response"; export const defaultCutStepFunctionBuilder = createStepFunctionBuilder( cutStepKind.has, @@ -39,15 +38,6 @@ export const defaultCutStepFunctionBuilder = createStepFunctionBuilder( throw new ResponseContract.Error(information); } - const result = currentContract.body.parse(body); - - if (E.isLeft(result)) { - throw new ResponseContract.Error( - information, - unwrap(result), - ); - } - return new PredictedResponse( currentContract.code, currentContract.information, @@ -55,23 +45,9 @@ export const defaultCutStepFunctionBuilder = createStepFunctionBuilder( ) as never; }; - function treatResult( - result: Awaited>, - floor: Floor, - ) { - if (cutStepOutputKind.has(result)) { - return { - ...floor, - ...unwrap(result), - }; - } - - return result; - } - return success({ - buildedFunction: (request, floor) => { - const result = cutFunction( + buildedFunction: async(request, floor) => { + const cutResult = await cutFunction( floor, { request, @@ -80,13 +56,26 @@ export const defaultCutStepFunctionBuilder = createStepFunctionBuilder( }, ); - if (result instanceof Promise) { - return result.then( - (awaitedResult) => treatResult(awaitedResult, floor), - ); + if (cutResult instanceof PredictedResponse) { + const currentContract = preparedContractResponse[cutResult.information]!; + const resultBody = currentContract.body.isAsynchronous() + ? await currentContract.body.asyncParse(cutResult.body) + : currentContract.body.parse(cutResult.body); + + if (E.isLeft(resultBody)) { + throw new ResponseContract.Error( + cutResult.information, + unwrap(resultBody), + ); + } + + return cutResult; } - return treatResult(result, floor); + return { + ...floor, + ...unwrap(cutResult), + }; }, hooksRouteLifeCycle: [], }); diff --git a/scripts/core/functionsBuilders/steps/defaults/extractStep.ts b/scripts/core/functionsBuilders/steps/defaults/extractStep.ts index e0f6f4b..a2a219f 100644 --- a/scripts/core/functionsBuilders/steps/defaults/extractStep.ts +++ b/scripts/core/functionsBuilders/steps/defaults/extractStep.ts @@ -1,11 +1,12 @@ -import { extractStepKind } from "@core/steps"; -import { A, DP, E, innerPipe, isType, justReturn, O, P, pipe, unwrap } from "@duplojs/utils"; +import { type ExtractShape, extractStepKind } from "@core/steps"; +import { A, DP, E, forward, innerPipe, isType, justReturn, type MaybePromise, O, P, pipe, unwrap } from "@duplojs/utils"; import { type Request } from "@core/request"; import { PredictedResponse } from "@core/response"; import { type Floor } from "@core/floor"; import { createStepFunctionBuilder } from "../create"; +import { type DataParser } from "@duplojs/utils/dataParser"; -type Extractor = (request: Request, floor: Floor) => PredictedResponse | Floor; +type Extractor = (request: Request, floor: Floor) => MaybePromise; export const defaultExtractStepFunctionBuilder = createStepFunctionBuilder( extractStepKind.has, @@ -17,54 +18,83 @@ export const defaultExtractStepFunctionBuilder = createStepFunctionBuilder( const responseContract = stepResponseContract ?? defaultExtractContract; - function getResponse( - result: E.EitherError, - key: string, - subKey?: string, - ) { - const response = new PredictedResponse( - responseContract.code, - responseContract.information, - environment === "DEV" - ? unwrap(result) - : undefined, - ); + function createExtractor( + parser: DataParser, + key: keyof ExtractShape, + subKey: string | undefined, + ): Extractor { + const createResponse = environment === "DEV" + ? (result: unknown) => new PredictedResponse( + responseContract.code, + responseContract.information, + result, + ) + : () => new PredictedResponse(responseContract.code, responseContract.information, undefined); + const setHeader = subKey === undefined || key === "body" + ? (response: PredictedResponse) => response.setHeader("extract-key", `request.${key}`) + : (response: PredictedResponse) => response.setHeader("extract-key", `request.${key}.${subKey}`); + const getResponse = (result: unknown) => setHeader(createResponse(result)); + const treatResult = (result: E.Left | E.Right, floor: Floor) => E.isLeft(result) + ? getResponse(unwrap(result)) + : { + ...floor, + [subKey ?? key]: unwrap(result), + }; + const getValue = typeof subKey === "string" + ? (value: unknown) => value?.[subKey as never] + : forward; - return subKey === undefined - ? response.setHeader("extract-key", `request.${key}`) - : response.setHeader("extract-key", `request.${key}.${subKey}`); - } + if (key === "body") { + const parseFunction = parser.isAsynchronous() + ? parser.asyncParse + : parser.parse; + return async(request: Request, floor: Floor) => { + const bodyResult = await request.getBody(); + if (E.isLeft(bodyResult)) { + return treatResult(bodyResult, floor); + } + const result = await parseFunction(getValue(unwrap(bodyResult))); + return treatResult(result, floor); + }; + } - function treatResult( - result: E.EitherError | E.EitherSuccess, - floor: Floor, - key: string, - subKey?: string, - ) { - if (E.isLeft(result)) { - return getResponse(result, key, subKey); + if (parser.isAsynchronous()) { + const parseFunction = parser.asyncParse; + return async(request: Request, floor: Floor) => { + const result = await parseFunction(getValue(request[key])); + return treatResult(result, floor); + }; } - return { - ...floor, - [subKey ?? key]: unwrap(result), + const parseFunction = parser.parse; + return (request: Request, floor: Floor) => { + const result = parseFunction(getValue(request[key])); + return treatResult(result, floor); }; } const extractors = A.reduce( O.entries(shape), A.reduceFrom([]), - ({ lastValue, element: [key, value], next }) => next( - DP.dataParserKind.has(value) - ? A.push( + ({ + lastValue, + element: [key, value], + next, + }) => pipe( + value, + P.when( + DP.dataParserKind.has, + (value) => A.push( lastValue, - (request, floor) => treatResult( - value.parse(request[key]), - floor, + createExtractor( + value, key, + undefined, ), - ) - : pipe( + ), + ), + P.otherwise( + (value) => pipe( value, P.when( isType("undefined"), @@ -74,29 +104,28 @@ export const defaultExtractStepFunctionBuilder = createStepFunctionBuilder( innerPipe( O.entries, A.map( - ([subKey, subValue]): Extractor => ( - (request, floor) => treatResult( - subValue.parse(request[key]?.[subKey as never]), - floor, - key, - subKey, - ) + ([subKey, subValue]) => createExtractor( + subValue, + key, + subKey, ), ), (subExtractor) => A.concat(lastValue, subExtractor), ), ), ), + ), + next, ), ); return success({ - buildedFunction: (request, floor) => { + buildedFunction: async(request, floor) => { let newFloor = floor; // eslint-disable-next-line @typescript-eslint/prefer-for-of for (let index = 0; index < extractors.length; index++) { - const result = extractors[index]!(request, newFloor); + const result = await extractors[index]!(request, newFloor); if (result instanceof PredictedResponse) { return result; diff --git a/scripts/core/functionsBuilders/steps/defaults/handlerStep.ts b/scripts/core/functionsBuilders/steps/defaults/handlerStep.ts index 53ce6bf..9193c1d 100644 --- a/scripts/core/functionsBuilders/steps/defaults/handlerStep.ts +++ b/scripts/core/functionsBuilders/steps/defaults/handlerStep.ts @@ -1,7 +1,7 @@ import { type HandlerStepFunctionParams, handlerStepKind } from "@core/steps"; import { createStepFunctionBuilder } from "../create"; import { A, E, unwrap } from "@duplojs/utils"; -import { PredictedResponse, Response, ResponseContract } from "@core/response"; +import { PredictedResponse, ResponseContract } from "@core/response"; export const defaultHandlerStepFunctionBuilder = createStepFunctionBuilder( handlerStepKind.has, @@ -32,15 +32,6 @@ export const defaultHandlerStepFunctionBuilder = createStepFunctionBuilder( throw new ResponseContract.Error(information); } - const result = currentContract.body.parse(body); - - if (E.isLeft(result)) { - throw new ResponseContract.Error( - information, - unwrap(result), - ); - } - return new PredictedResponse( currentContract.code, currentContract.information, @@ -49,13 +40,29 @@ export const defaultHandlerStepFunctionBuilder = createStepFunctionBuilder( }; return success({ - buildedFunction: (request, floor) => handlerFunction( - floor, - { - request, - response, - }, - ), + buildedFunction: async(request, floor) => { + const predictedResponse = await handlerFunction( + floor, + { + request, + response, + }, + ); + + const currentContract = preparedContractResponse[predictedResponse.information]!; + const result = currentContract.body.isAsynchronous() + ? await currentContract.body.asyncParse(predictedResponse.body) + : currentContract.body.parse(predictedResponse.body); + + if (E.isLeft(result)) { + throw new ResponseContract.Error( + predictedResponse.information, + unwrap(result), + ); + } + + return predictedResponse; + }, hooksRouteLifeCycle: [], }); }, diff --git a/scripts/core/hub/defaultBodyController.ts b/scripts/core/hub/defaultBodyController.ts new file mode 100644 index 0000000..323c648 --- /dev/null +++ b/scripts/core/hub/defaultBodyController.ts @@ -0,0 +1,3 @@ +import { controlBodyAsText } from "@core/request"; + +export const defaultBodyController = controlBodyAsText(); diff --git a/scripts/core/hub/hooks.ts b/scripts/core/hub/hooks.ts index 4e73d1b..9df6759 100644 --- a/scripts/core/hub/hooks.ts +++ b/scripts/core/hub/hooks.ts @@ -3,6 +3,7 @@ import { type EscapeVoid, G, type Kind, type MaybePromise } from "@duplojs/utils import { type Hub } from "."; import { createCoreLibKind } from "@core/kind"; import { type RouterInitializationData } from "@core/router"; +import { type HttpServerParams } from "@core/types"; export const hookServerExitKind = createCoreLibKind("server-hook-exit"); @@ -35,10 +36,6 @@ export async function launchHookBeforeBuildRoute( ); } -export interface HttpServerParams { - -} - export type HookBeforeServerBuildRoutes = ( hub: Hub, httpServerParams: HttpServerParams @@ -59,17 +56,9 @@ export async function launchHookServer( hub: Hub, httpServerParams: HttpServerParams, ) { - return G.asyncReduce( - hooks, - G.reduceFrom(hub), - async({ - element: hook, - lastValue, - next, - }) => next( - (await hook(lastValue, httpServerParams)) ?? lastValue, - ), - ); + for (const hook of hooks) { + await hook(hub, httpServerParams); + } } export interface HttpServerErrorParams { diff --git a/scripts/core/hub/index.ts b/scripts/core/hub/index.ts index efc0e71..6b47426 100644 --- a/scripts/core/hub/index.ts +++ b/scripts/core/hub/index.ts @@ -1,19 +1,21 @@ import { createCoreLibKind } from "@core/kind"; import { type Route, type HookRouteLifeCycle, routeKind } from "@core/route"; -import { A, O, pipe, type Kind, type MaybeArray, type MaybePromise, type DP, isType, P } from "@duplojs/utils"; +import { A, O, pipe, type MaybeArray, type MaybePromise, type DP, isType, P, kindHeritage } from "@duplojs/utils"; import { type HookHubLifeCycle } from "./hooks"; import { type HandlerStepFunctionParams, type HandlerStep, createHandlerStep } from "@core/steps"; -import { Request } from "@core/request"; +import { type BodyController, type BodyReaderImplementation, Request } from "@core/request"; import { type ClientErrorResponseCode, type ResponseContract } from "@core/response"; import { defaultNotfoundHandler } from "./defaultNotfoundHandler"; import { type Environment } from "@core/types"; import { defaultExtractContract } from "./defaultExtractContract"; import { type createStepFunctionBuilder } from "@core/functionsBuilders/steps"; import { type createRouteFunctionBuilder } from "@core/functionsBuilders/route"; +import { defaultBodyController } from "./defaultBodyController"; export * from "./hooks"; export * from "./defaultNotfoundHandler"; export * from "./defaultExtractContract"; +export * from "./defaultBodyController"; export const hubKind = createCoreLibKind("hub"); @@ -28,68 +30,139 @@ export interface HubPlugin { readonly routes?: readonly Route[]; readonly routeFunctionBuilders?: readonly ReturnType[]; readonly stepFunctionBuilders?: readonly ReturnType[]; + readonly bodyReaderImplementations?: readonly BodyReaderImplementation[]; } -export interface HubAggregates { - readonly hooksRouteLifeCycle: readonly HookRouteLifeCycle[]; - readonly hooksHubLifeCycle: readonly HookHubLifeCycle[]; - readonly routes: readonly Route[]; - readonly routeFunctionBuilders: readonly ReturnType[]; - readonly stepFunctionBuilders: readonly ReturnType[]; -} - -export interface Hub< +export class Hub< GenericConfig extends HubConfig = HubConfig, -> extends Kind { - readonly config: GenericConfig; +> extends kindHeritage( + "hub", + createCoreLibKind("hub"), + ) { + public plugins: HubPlugin[] = []; - readonly plugins: readonly HubPlugin[]; + public hooksRouteLifeCycle: HookRouteLifeCycle[] = []; - readonly hooksRouteLifeCycle: readonly HookRouteLifeCycle[]; + public hooksHubLifeCycle: HookHubLifeCycle[] = []; - readonly hooksHubLifeCycle: readonly HookHubLifeCycle[]; + public routes = new Set(); - readonly routes: readonly Route[]; + public routeFunctionBuilders: ReturnType[] = []; - readonly routeFunctionBuilders: readonly ReturnType[]; + public stepFunctionBuilders: ReturnType[] = []; - readonly stepFunctionBuilders: readonly ReturnType[]; + public bodyReaderImplementations: BodyReaderImplementation[] = []; - readonly classRequest: typeof Request; + public classRequest = Request; - readonly notfoundHandler: HandlerStep; + public notfoundHandler: HandlerStep = defaultNotfoundHandler; - readonly defaultExtractContract: ResponseContract.Contract< + public defaultExtractContract: ResponseContract.Contract< ClientErrorResponseCode, string, DP.DataParserEmpty - >; - - register( - routes: Route | Iterable | Record - ): Hub; - - addRouteFunctionBuilder( - functionBuilder: MaybeArray> - ): Hub; - - addStepFunctionBuilder( - functionBuilder: MaybeArray> - ): Hub; - - addRouteHooks( - hook: MaybeArray - ): Hub; - - addHubHooks( - hook: MaybeArray - ): Hub; - - plug( - plugin: HubPlugin | ((self: this) => HubPlugin) - ): Hub; - - setNotfoundHandler< + > = defaultExtractContract; + + public defaultBodyController: BodyController = defaultBodyController; + + private constructor( + public config: GenericConfig, + ) { + super({}); + } + + public register( + routes: Route | Iterable | Record, + ) { + pipe( + routes, + P.when( + routeKind.has, + A.coalescing, + ), + P.when( + isType("iterable"), + A.from, + ), + P.otherwise(O.values), + A.map((route) => this.routes.add(route)), + ); + + return this; + } + + public addRouteFunctionBuilder( + functionBuilder: MaybeArray>, + ) { + this.routeFunctionBuilders.push(...A.coalescing(functionBuilder)); + return this; + } + + public addStepFunctionBuilder( + functionBuilder: MaybeArray>, + ) { + this.stepFunctionBuilders.push(...A.coalescing(functionBuilder)); + return this; + } + + public addRouteHooks( + hook: MaybeArray, + ) { + this.hooksRouteLifeCycle.push(...A.coalescing(hook)); + return this; + } + + public addHubHooks( + hook: MaybeArray, + ) { + this.hooksHubLifeCycle.push(...A.coalescing(hook)); + return this; + } + + public addBodyReaderImplementation( + bodyReaderImplementation: MaybeArray, + ) { + this.bodyReaderImplementations.push(...A.coalescing(bodyReaderImplementation)); + return this; + } + + public plug( + plugin: HubPlugin | ((self: this) => HubPlugin), + ) { + const pluginResult = typeof plugin === "function" + ? plugin(this) + : plugin; + + if (pluginResult.bodyReaderImplementations) { + this.addBodyReaderImplementation(pluginResult.bodyReaderImplementations); + } + + if (pluginResult.hooksHubLifeCycle) { + this.addHubHooks(pluginResult.hooksHubLifeCycle); + } + + if (pluginResult.hooksRouteLifeCycle) { + this.addRouteHooks(pluginResult.hooksRouteLifeCycle); + } + + if (pluginResult.routeFunctionBuilders) { + this.addRouteFunctionBuilder(pluginResult.routeFunctionBuilders); + } + + if (pluginResult.routes) { + this.register(pluginResult.routes); + } + + if (pluginResult.stepFunctionBuilders) { + this.addStepFunctionBuilder(pluginResult.stepFunctionBuilders); + } + + this.plugins.push(pluginResult); + + return this; + } + + public setNotfoundHandler< GenericResponseContract extends ResponseContract.Contract, GenericResponse extends ResponseContract.Convert< GenericResponseContract @@ -101,250 +174,63 @@ export interface Hub< Request, GenericResponse > - ) => MaybePromise - ): Hub; - - setDefaultExtractContract( - responseContract: this["defaultExtractContract"] - ): Hub; - - aggregates(): HubAggregates; - - aggregatesRoutes(): readonly Route[]; - - aggregatesRouteFunctionBuilders(): readonly ReturnType[]; + ) => MaybePromise, + ) { + this.notfoundHandler = createHandlerStep({ + responseContract, + theFunction: (floor, params) => theFunction(params), + metadata: [], + }); + + return this; + } + + public setDefaultExtractContract( + responseContract: this["defaultExtractContract"], + ) { + this.defaultExtractContract = responseContract; + + return this; + } + + public aggregatesHooksHubLifeCycle< + GenericHookName extends keyof HookHubLifeCycle, + >(hookName: GenericHookName) { + return A.flatMap( + this.hooksHubLifeCycle, + (hooks) => hooks[hookName] ?? [], + ); + } - aggregatesStepFunctionBuilders(): readonly ReturnType[]; + public setDefaultBodyController(bodyController: BodyController) { + this.defaultBodyController = bodyController; - aggregatesHooksHubLifeCycle< - GenericHookName extends keyof HookHubLifeCycle, - >(hookName: GenericHookName): readonly Exclude[]; + return this; + } - aggregatesHooksRouteLifeCycle< + public aggregatesHooksRouteLifeCycle< GenericHookName extends keyof HookRouteLifeCycle, - >(hookName: GenericHookName): readonly Exclude[]; + >(hookName: GenericHookName) { + return A.flatMap( + this.hooksRouteLifeCycle, + (hooks) => hooks[hookName] ?? [], + ); + } + + /** + * @internal + */ + public static "new"< + GenericConfig extends HubConfig, + >(config: GenericConfig) { + return new Hub(config); + } } export function createHub< const GenericConfig extends HubConfig, >( config: GenericConfig, -): Hub { - return { - ...hubKind.addTo({}), - config, - plugins: [], - hooksHubLifeCycle: [], - hooksRouteLifeCycle: [], - routeFunctionBuilders: [], - routes: [], - stepFunctionBuilders: [], - notfoundHandler: defaultNotfoundHandler, - defaultExtractContract, - classRequest: Request, - addHubHooks(hook) { - return { - ...this, - hooksHubLifeCycle: A.concat(this.hooksHubLifeCycle, A.coalescing(hook)), - }; - }, - addRouteFunctionBuilder(functionBuilder) { - return { - ...this, - routeFunctionBuilders: A.concat(this.routeFunctionBuilders, A.coalescing(functionBuilder)), - }; - }, - addRouteHooks(hook) { - return { - ...this, - hooksRouteLifeCycle: A.concat(this.hooksRouteLifeCycle, A.coalescing(hook)), - }; - }, - addStepFunctionBuilder(hook) { - return { - ...this, - stepFunctionBuilders: A.concat(this.stepFunctionBuilders, A.coalescing(hook)), - }; - }, - plug(plugin) { - return { - ...this, - plugins: A.push( - this.plugins, - typeof plugin === "function" - ? plugin(this) - : plugin, - ), - }; - }, - register(route) { - return { - ...this, - routes: A.concat( - this.routes, - pipe( - route, - P.when( - routeKind.has, - A.coalescing, - ), - P.when( - isType("iterable"), - A.from, - ), - P.otherwise(O.values), - A.filter((route) => !A.includes(this.routes, route)), - ), - ), - }; - }, - setDefaultExtractContract(defaultExtractContract) { - return { - ...this, - defaultExtractContract, - }; - }, - setNotfoundHandler(responseContract, theFunction) { - return { - ...this, - notfoundHandler: createHandlerStep({ - responseContract, - theFunction: (floor, params) => theFunction(params), - metadata: [], - }), - }; - }, - aggregates() { - return A.reduce( - this.plugins, - A.reduceFrom({ - hooksRouteLifeCycle: this.hooksRouteLifeCycle, - routeFunctionBuilders: this.routeFunctionBuilders, - stepFunctionBuilders: this.stepFunctionBuilders, - routes: this.routes, - hooksHubLifeCycle: this.hooksHubLifeCycle, - }), - ({ - lastValue, - element: plugin, - next, - }) => next({ - hooksRouteLifeCycle: plugin.hooksRouteLifeCycle - ? A.concat(lastValue.hooksRouteLifeCycle, plugin.hooksRouteLifeCycle) - : lastValue.hooksRouteLifeCycle, - routeFunctionBuilders: plugin.routeFunctionBuilders - ? A.concat(lastValue.routeFunctionBuilders, plugin.routeFunctionBuilders) - : lastValue.routeFunctionBuilders, - stepFunctionBuilders: plugin.stepFunctionBuilders - ? A.concat(lastValue.stepFunctionBuilders, plugin.stepFunctionBuilders) - : lastValue.stepFunctionBuilders, - routes: plugin.routes - ? A.concat(lastValue.routes, plugin.routes) - : lastValue.routes, - hooksHubLifeCycle: plugin.hooksHubLifeCycle - ? A.concat(lastValue.hooksHubLifeCycle, plugin.hooksHubLifeCycle) - : lastValue.hooksHubLifeCycle, - }), - ); - }, - aggregatesRoutes() { - return A.reduce( - this.plugins, - A.reduceFrom(this.routes), - ({ - lastValue, - element: { routes }, - next, - }) => routes - ? next(A.concat(lastValue, routes)) - : next(lastValue), - ); - }, - aggregatesRouteFunctionBuilders() { - return A.reduce( - this.plugins, - A.reduceFrom(this.routeFunctionBuilders), - ({ - lastValue, - element: { routeFunctionBuilders }, - next, - }) => routeFunctionBuilders - ? next(A.concat(lastValue, routeFunctionBuilders)) - : next(lastValue), - ); - }, - aggregatesStepFunctionBuilders() { - return A.reduce( - this.plugins, - A.reduceFrom(this.stepFunctionBuilders), - ({ - lastValue, - element: { stepFunctionBuilders }, - next, - }) => stepFunctionBuilders - ? next(A.concat(lastValue, stepFunctionBuilders)) - : next(lastValue), - ); - }, - aggregatesHooksHubLifeCycle(hookName) { - const hooks = A.flatMap( - this.hooksHubLifeCycle, - (hooks) => hooks[hookName] ?? [], - ); - - return A.reduce( - this.plugins, - A.reduceFrom(hooks), - ({ - lastValue, - element: { hooksHubLifeCycle }, - next, - }) => { - if (!hooksHubLifeCycle) { - return next(lastValue); - } - - return next( - A.concat( - lastValue, - A.flatMap( - hooksHubLifeCycle, - (hooks) => hooks[hookName] ?? [], - ), - ), - ); - }, - ) as never; - }, - aggregatesHooksRouteLifeCycle(hookName) { - const hooks = A.flatMap( - this.hooksRouteLifeCycle, - (hooks) => hooks[hookName] ?? [], - ); - - return A.reduce( - this.plugins, - A.reduceFrom(hooks), - ({ - lastValue, - element: { hooksRouteLifeCycle }, - next, - }) => { - if (!hooksRouteLifeCycle) { - return next(lastValue); - } - - return next( - A.concat( - lastValue, - A.flatMap( - hooksRouteLifeCycle, - (hooks) => hooks[hookName] ?? [], - ), - ), - ); - }, - ) as never; - }, - }; +) { + return Hub.new(config); } diff --git a/scripts/core/implementHttpServer.ts b/scripts/core/implementHttpServer.ts index cf2c3bc..f6d796b 100644 --- a/scripts/core/implementHttpServer.ts +++ b/scripts/core/implementHttpServer.ts @@ -1,7 +1,9 @@ -import { type Hub, type HttpServerParams, launchHookServer, launchHookServerError, serverErrorExitHookFunction, serverErrorNextHookFunction } from "./hub"; +import { type Hub, launchHookServer, launchHookServerError, serverErrorExitHookFunction, serverErrorNextHookFunction } from "./hub"; import { buildRouter, type RouterInitializationData } from "./router"; import { forward, type MaybePromise } from "@duplojs/utils"; +import { type HttpServerParams } from "./types"; +import { initDefaultHook } from "./defaultHooks"; export interface ImplementHttpServerParams { readonly hub: Hub; @@ -27,23 +29,23 @@ export async function implementHttpServer< params: ImplementHttpServerParams, initHttpServer: (params: InitHttpServerParams) => MaybePromise, ): Promise { - const newHub1 = await launchHookServer( + await launchHookServer( params.hub.aggregatesHooksHubLifeCycle("beforeServerBuildRoutes"), params.hub, params.httpServerParams, ); const router = await buildRouter( - newHub1, + params.hub, ); - const newHub2 = await launchHookServer( - newHub1.aggregatesHooksHubLifeCycle("beforeStartServer"), - newHub1, + await launchHookServer( + params.hub.aggregatesHooksHubLifeCycle("beforeStartServer"), + params.hub, params.httpServerParams, ); - const serverErrorHooks = newHub1.aggregatesHooksHubLifeCycle("serverError"); + const serverErrorHooks = params.hub.aggregatesHooksHubLifeCycle("serverError"); function catchCriticalError(error: unknown) { console.error("Critical Error :", error); @@ -72,8 +74,8 @@ export async function implementHttpServer< }); await launchHookServer( - newHub2.aggregatesHooksHubLifeCycle("afterStartServer"), - newHub2, + params.hub.aggregatesHooksHubLifeCycle("afterStartServer"), + params.hub, params.httpServerParams, ); diff --git a/scripts/core/index.ts b/scripts/core/index.ts index fce492a..d2a3966 100644 --- a/scripts/core/index.ts +++ b/scripts/core/index.ts @@ -18,3 +18,5 @@ export * from "./metadata"; export * from "./implementHttpServer"; export * from "./narrowingInput"; export * from "./clean"; +export * from "./defaultHooks"; +export * from "./errors"; diff --git a/scripts/core/request/bodyController/base.ts b/scripts/core/request/bodyController/base.ts new file mode 100644 index 0000000..49792c2 --- /dev/null +++ b/scripts/core/request/bodyController/base.ts @@ -0,0 +1,104 @@ +import { createCoreLibKind } from "@core/kind"; +import { type Request } from "@core/request"; +import { E, type RemoveKind, type Kind } from "@duplojs/utils"; + +export interface BodyControllerParams { + bodyMaxSize?: number; +} + +const bodyReaderKind = createCoreLibKind<"body-reader", string>("body-reader"); + +export interface BodyReader< + GenericName extends string = string, +> extends Kind { + read( + request: Request, + ): Promise>; +} + +const bodyReaderImplementationKind = createCoreLibKind<"body-reader-implementation", string>("body-reader-implementation"); + +export interface BodyReaderImplementation< + GenericName extends string = string, + GenericParams extends BodyControllerParams = BodyControllerParams, +> extends Kind { + read( + request: Request, + params: GenericParams + ): Promise>; +} + +const bodyControllerKind = createCoreLibKind<"body-controller", string>("body-controller"); + +export interface BodyController< + GenericName extends string = string, + GenericParams extends BodyControllerParams = BodyControllerParams, +> extends Kind< + typeof bodyControllerKind.definition, + GenericName + > { + readonly name: GenericName; + readonly params: GenericParams; + tryToCreateReader( + readerImplementation: BodyReaderImplementation + ): E.Success> | E.Fail; +} + +const bodyControllerHandlerKind = createCoreLibKind("body-controller-handler"); + +export interface BodyControllerHandler< + GenericName extends string = string, + GenericParams extends BodyControllerParams = BodyControllerParams, +> extends Kind { + readonly name: GenericName; + create(params: GenericParams): BodyController; + createReaderImplementation( + read: BodyReaderImplementation["read"] + ): BodyReaderImplementation; + is(input: unknown): input is BodyController; +} + +export function createBodyController< + GenericName extends string, + GenericParams extends BodyControllerParams, +>(name: GenericName): BodyControllerHandler { + return bodyControllerHandlerKind.setTo( + { + name, + create(params) { + return bodyControllerKind.setTo< + RemoveKind>, + GenericName + >( + { + name, + params, + tryToCreateReader(readerImplementation) { + if (bodyReaderImplementationKind.getValue(readerImplementation) !== name) { + return E.fail(); + } + return E.success( + bodyReaderKind.setTo( + { + read: (request) => readerImplementation.read(request, params), + } satisfies RemoveKind>, + name, + ), + ); + }, + }, + name, + ); + }, + createReaderImplementation(read) { + return bodyReaderImplementationKind.setTo( + { read }, + name, + ); + }, + is(input) { + return bodyControllerKind.has(input) && bodyControllerKind.getValue(input) === name; + }, + } satisfies RemoveKind>, + ); +} diff --git a/scripts/core/request/bodyController/formData.ts b/scripts/core/request/bodyController/formData.ts new file mode 100644 index 0000000..e87647c --- /dev/null +++ b/scripts/core/request/bodyController/formData.ts @@ -0,0 +1,49 @@ +import { stringToBytes, toRegExp, type BytesInString } from "@duplojs/utils"; +import { type BodyControllerParams, createBodyController } from "./base"; + +export interface FormDataBodyReaderParams extends BodyControllerParams { + maxFileQuantity: number; + mimeType?: RegExp; + fileMaxSize?: number; + maxBufferSize: number; + maxIndexArray: number; + maxKeyLength: number; +} + +export const FormDataBodyController = createBodyController< + "formData", + FormDataBodyReaderParams +>("formData"); +export type FormDataBodyController = typeof FormDataBodyController; + +export interface ControlBodyAsFormDataParams { + maxFileQuantity: number; + mimeType?: string | string[] | RegExp; + bodyMaxSize?: number | BytesInString; + fileMaxSize?: number | BytesInString; + maxBufferSize?: number | BytesInString; + maxIndexArray?: number; + maxKeyLength?: number; +} + +export function controlBodyAsFormData( + params: ControlBodyAsFormDataParams, +) { + return FormDataBodyController.create({ + maxFileQuantity: params.maxFileQuantity, + bodyMaxSize: params.bodyMaxSize && stringToBytes(params.bodyMaxSize), + fileMaxSize: params.fileMaxSize && stringToBytes(params.fileMaxSize), + mimeType: params.mimeType !== undefined + ? toRegExp(params.mimeType) + : undefined, + maxBufferSize: params.maxBufferSize !== undefined + ? stringToBytes(params.maxBufferSize) + : stringToBytes("128kb"), + maxIndexArray: params.maxIndexArray !== undefined + ? params.maxIndexArray + : 500, + maxKeyLength: params.maxKeyLength !== undefined + ? params.maxKeyLength + : 500, + }); +} diff --git a/scripts/core/request/bodyController/index.ts b/scripts/core/request/bodyController/index.ts new file mode 100644 index 0000000..b499ebc --- /dev/null +++ b/scripts/core/request/bodyController/index.ts @@ -0,0 +1,3 @@ +export * from "./base"; +export * from "./formData"; +export * from "./text"; diff --git a/scripts/core/request/bodyController/text.ts b/scripts/core/request/bodyController/text.ts new file mode 100644 index 0000000..31ec820 --- /dev/null +++ b/scripts/core/request/bodyController/text.ts @@ -0,0 +1,24 @@ +import { stringToBytes, type BytesInString } from "@duplojs/utils"; +import { type BodyControllerParams, createBodyController } from "./base"; + +export interface TextBodyReaderParams extends BodyControllerParams { + +} + +export const TextBodyController = createBodyController< + "text", + TextBodyReaderParams +>("text"); +export type TextBodyController = typeof TextBodyController; + +export interface ControlBodyAsTextParams { + bodyMaxSize?: number | BytesInString; +} + +export function controlBodyAsText( + params?: ControlBodyAsTextParams, +) { + return TextBodyController.create({ + bodyMaxSize: params?.bodyMaxSize && stringToBytes(params.bodyMaxSize), + }); +} diff --git a/scripts/core/request.ts b/scripts/core/request/index.ts similarity index 64% rename from scripts/core/request.ts rename to scripts/core/request/index.ts index fe96c6e..1acc9a6 100644 --- a/scripts/core/request.ts +++ b/scripts/core/request/index.ts @@ -1,6 +1,9 @@ -import { kindHeritage } from "@duplojs/utils"; +import { createExternalPromise, type E, kindHeritage, type MaybePromise } from "@duplojs/utils"; import { type GetPropsWithValue } from "@duplojs/utils/object"; -import { createCoreLibKind } from "./kind"; +import { createCoreLibKind } from "../kind"; +import { type BodyReader } from "./bodyController"; + +export * from "./bodyController"; export interface RequestMethodsWrapper { GET: true; @@ -26,6 +29,7 @@ export interface RequestInitializationData { readonly path: string; readonly query: Record; readonly url: string; + readonly bodyReader: BodyReader; } export class Request extends kindHeritage( @@ -50,7 +54,11 @@ export class Request extends kindHeritage( public matchedPath: string | null; - public body: unknown = undefined; + public bodyReader: BodyReader; + + private bodyResult?: MaybePromise = undefined; + + public filesAttache: string[] | undefined = undefined; public constructor( { @@ -63,6 +71,7 @@ export class Request extends kindHeritage( params, query, matchedPath, + bodyReader, ...rest }: RequestInitializationData, ) { @@ -77,9 +86,33 @@ export class Request extends kindHeritage( this.params = params; this.query = query; this.matchedPath = matchedPath; + this.bodyReader = bodyReader; for (const key in rest) { this[key as never] = rest[key as never]; } } + + public getBody(): MaybePromise< + | E.Success + | E.Error + > { + if (this.bodyResult !== undefined) { + return this.bodyResult; + } + const externalPromise = createExternalPromise< + | E.Success + | E.Error + >(); + + this.bodyResult = externalPromise.promise; + + return this.bodyReader + .read(this) + .then((result) => { + externalPromise.resolve(result); + this.bodyResult = result; + return result; + }); + } } diff --git a/scripts/core/route/hooks.ts b/scripts/core/route/hooks.ts index 78f9040..221f9a2 100644 --- a/scripts/core/route/hooks.ts +++ b/scripts/core/route/hooks.ts @@ -58,12 +58,6 @@ export type HookBeforeRouteExecution< params: RouteHookParams ) => MaybePromise; -export type HookParseBody< - GenericRequest extends Request = Request, -> = ( - params: RouteHookParams -) => MaybePromise; - export interface RouteHookErrorParams< GenericRequest extends Request = Request, > { @@ -122,7 +116,6 @@ export interface HookRouteLifeCycle< > { onConstructRequest?: HookOnConstructRequest; beforeRouteExecution?: HookBeforeRouteExecution; - parseBody?: HookParseBody; error?: HookError; beforeSendResponse?: HookBeforeSendResponse; sendResponse?: HookSendResponse; diff --git a/scripts/core/route/index.ts b/scripts/core/route/index.ts index 63f493f..4c43799 100644 --- a/scripts/core/route/index.ts +++ b/scripts/core/route/index.ts @@ -1,6 +1,6 @@ import { type O, pipe, type Kind } from "@duplojs/utils"; import { createCoreLibKind } from "../kind"; -import { type RequestMethods } from "../request"; +import { type BodyController, type RequestMethods } from "../request"; import { type ExtractStep, type CheckerStep, type CutStep, type HandlerStep, type ProcessStep, type stepKind, type PresetCheckerStep } from "../steps"; import { type HookRouteLifeCycle } from "./hooks"; import { type Metadata } from "@core/metadata"; @@ -48,6 +48,7 @@ export interface RouteDefinition { readonly steps: readonly RouteSteps[]; readonly hooks: readonly HookRouteLifeCycle[]; readonly metadata: readonly Metadata[]; + readonly bodyController: BodyController | null; } export const routeKind = createCoreLibKind("route"); diff --git a/scripts/core/router/index.ts b/scripts/core/router/index.ts index 357dd79..627ad66 100644 --- a/scripts/core/router/index.ts +++ b/scripts/core/router/index.ts @@ -1,22 +1,27 @@ import { type Hub, launchHookBeforeBuildRoute } from "@core/hub"; import { type BuildedRouter } from "./types"; -import { A, asyncPipe, E, forward, G, isType, justReturn, O, pipe, unwrap } from "@duplojs/utils"; +import { A, asserts, asyncPipe, E, forward, G, isType, justReturn, O, pipe, unwrap } from "@duplojs/utils"; import { type BuildedRoute } from "@core/route/types"; import { pathToRegExp } from "./pathToRegExp"; import { createRoute } from "@core/route"; import { RouterBuildError } from "./buildError"; -import { buildRouteFunction, type BuildRouteFunctionParams, defaultRouteFunctionBuilder } from "@core/functionsBuilders/route"; -import { defaultCheckerStepFunctionBuilder, defaultCutStepFunctionBuilder, defaultExtractStepFunctionBuilder, defaultHandlerStepFunctionBuilder, defaultProcessStepFunctionBuilder } from "@core/functionsBuilders/steps"; +import { buildRouteFunction, type createRouteFunctionBuilder, defaultRouteFunctionBuilder, type BuildRouteFunctionParams } from "@core/functionsBuilders/route"; import { decodeUrl } from "./decodeUrl"; +import { type BodyReader } from "@core/request"; +import { controlBodyAsText, TextBodyController } from "@core/request/bodyController/text"; +import { NotFoundBodyReaderImplementationError } from "./notFoundBodyReaderImplementationError"; +import { type createStepFunctionBuilder, defaultCheckerStepFunctionBuilder, defaultCutStepFunctionBuilder, defaultExtractStepFunctionBuilder, defaultHandlerStepFunctionBuilder, defaultProcessStepFunctionBuilder } from "@core/functionsBuilders"; export * from "./types"; export * from "./pathToRegExp"; export * from "./buildError"; export * from "./decodeUrl"; +export * from "./notFoundBodyReaderImplementationError"; interface RouterElement { pattern: RegExp; matchedPath: string; + bodyReader: BodyReader; buildedRoute: BuildedRoute; } @@ -25,25 +30,27 @@ type GroupedRoute = Record< RouterElement[] >; -export async function buildRouter(inputHub: Hub): Promise { - const hub = inputHub - .addRouteFunctionBuilder(defaultRouteFunctionBuilder) - .addStepFunctionBuilder([ - defaultCheckerStepFunctionBuilder, - defaultCutStepFunctionBuilder, - defaultHandlerStepFunctionBuilder, - defaultExtractStepFunctionBuilder, - defaultProcessStepFunctionBuilder, - ]); - +export async function buildRouter(hub: Hub): Promise { const { environment } = hub.config; const { hooksRouteLifeCycle, - routeFunctionBuilders, routes, - stepFunctionBuilders, hooksHubLifeCycle, - } = hub.aggregates(); + bodyReaderImplementations, + } = hub; + + const routeFunctionBuilders: readonly ReturnType[] = [ + ...hub.routeFunctionBuilders, + defaultRouteFunctionBuilder, + ]; + const stepFunctionBuilders: readonly ReturnType[] = [ + ...hub.stepFunctionBuilders, + defaultCheckerStepFunctionBuilder, + defaultCutStepFunctionBuilder, + defaultHandlerStepFunctionBuilder, + defaultExtractStepFunctionBuilder, + defaultProcessStepFunctionBuilder, + ]; const hooksBeforeBuildRoute = pipe( hooksHubLifeCycle, @@ -84,6 +91,28 @@ export async function buildRouter(inputHub: Hub): Promise { ); } + const routeBodyController = route.definition.bodyController ?? hub.defaultBodyController; + + const bodyReader = pipe( + bodyReaderImplementations, + A.reduce( + A.reduceFrom(null), + ({ element, next, exit }) => pipe( + element, + routeBodyController.tryToCreateReader, + E.whenIsRight(exit), + E.whenIsLeft(justReturn(next(null))), + ), + ), + ); + + if (!bodyReader) { + throw new NotFoundBodyReaderImplementationError( + route, + routeBodyController, + ); + } + return nextWithObject( lastValue, { @@ -95,6 +124,7 @@ export async function buildRouter(inputHub: Hub): Promise { pattern: pathToRegExp, buildedRoute: justReturn(unwrap(buildedRoute)), matchedPath: forward, + bodyReader: justReturn(bodyReader), }), ), ), @@ -103,6 +133,18 @@ export async function buildRouter(inputHub: Hub): Promise { }, ); + const bodyControllerNotfoundRoute = controlBodyAsText(); + const bodyReaderNotFoundRoute = unwrap( + bodyControllerNotfoundRoute.tryToCreateReader( + TextBodyController.createReaderImplementation( + () => Promise.resolve( + E.error(new Error("Inaccessible body in not found route.")), + ), + ), + ), + ); + asserts(bodyReaderNotFoundRoute, isType("object")); + const buildedNotfoundRoute = await asyncPipe( createRoute({ method: "GET", @@ -111,6 +153,7 @@ export async function buildRouter(inputHub: Hub): Promise { preflightSteps: [], steps: [hub.notfoundHandler], metadata: [], + bodyController: bodyControllerNotfoundRoute, }), async(route) => { const result = await buildRouteFunction( @@ -142,6 +185,7 @@ export async function buildRouter(inputHub: Hub): Promise { ...decodedUrl, params: {}, matchedPath: null, + bodyReader: bodyReaderNotFoundRoute, }), ); } @@ -161,6 +205,7 @@ export async function buildRouter(inputHub: Hub): Promise { ...decodedUrl, params: result.groups ?? {}, matchedPath: routerElement.matchedPath, + bodyReader: routerElement.bodyReader, }), ); } @@ -171,6 +216,7 @@ export async function buildRouter(inputHub: Hub): Promise { ...decodedUrl, params: {}, matchedPath: null, + bodyReader: bodyReaderNotFoundRoute, }), ); }, diff --git a/scripts/core/router/notFoundBodyReaderImplementationError.ts b/scripts/core/router/notFoundBodyReaderImplementationError.ts new file mode 100644 index 0000000..ae246c5 --- /dev/null +++ b/scripts/core/router/notFoundBodyReaderImplementationError.ts @@ -0,0 +1,17 @@ +import { createCoreLibKind } from "@core/kind"; +import { type BodyController } from "@core/request"; +import { type Route } from "@core/route"; +import { kindHeritage } from "@duplojs/utils"; + +export class NotFoundBodyReaderImplementationError extends kindHeritage( + "not-found-body-reader-implementation-error", + createCoreLibKind("not-found-body-reader-implementation-error"), + Error, +) { + public constructor( + public route: Route, + public bodyController: BodyController, + ) { + super({}, ["Body reader implementation not found."]); + } +} diff --git a/scripts/core/router/types/buildedRouter.ts b/scripts/core/router/types/buildedRouter.ts index a20aba0..730f3d9 100644 --- a/scripts/core/router/types/buildedRouter.ts +++ b/scripts/core/router/types/buildedRouter.ts @@ -1,11 +1,12 @@ import { type createStepFunctionBuilder, type createRouteFunctionBuilder } from "@core/functionsBuilders"; import { type HookHubLifeCycle } from "@core/hub"; import { type RequestInitializationData } from "@core/request"; -import { type HookRouteLifeCycle, type Route, type RouteDefinition } from "@core/route"; +import { type HookRouteLifeCycle, type Route } from "@core/route"; export type RouterInitializationData = Omit< RequestInitializationData, | "matchedPath" + | "bodyReader" | "params" | "path" | "query" @@ -15,7 +16,7 @@ export interface BuildedRouter { exec( initializationData: RouterInitializationData ): Promise; - readonly routes: readonly Route[]; + readonly routes: ReadonlySet; readonly hooksRouteLifeCycle: readonly HookRouteLifeCycle[]; readonly routeFunctionBuilders: readonly ReturnType[]; readonly stepFunctionBuilders: readonly ReturnType[]; diff --git a/scripts/core/steps/extract.ts b/scripts/core/steps/extract.ts index d434848..741e8cb 100644 --- a/scripts/core/steps/extract.ts +++ b/scripts/core/steps/extract.ts @@ -17,7 +17,7 @@ export type DisabledExtractKeys = O.GetPropsWithValue< export type ExtractShape< GenericRequest extends Request = Request, > = Partial< - Record< + & Record< Exclude< keyof GenericRequest, | O.GetPropsWithValueExtends< @@ -26,10 +26,19 @@ export type ExtractShape< > // eslint-disable-next-line @typescript-eslint/no-redundant-type-constituents | DisabledExtractKeys + | "body" + | "bodyReader" + | symbol >, | DP.DataParser | Record > + & { + body: ( + | DP.DataParser + | Record + ); + } >; export interface ExtractStepDefinition { diff --git a/scripts/core/steps/types/steps.ts b/scripts/core/steps/types/steps.ts index 300860f..dcec382 100644 --- a/scripts/core/steps/types/steps.ts +++ b/scripts/core/steps/types/steps.ts @@ -1,11 +1,9 @@ -import { type Kind, type O } from "@duplojs/utils"; import { type CheckerStep } from "../checker"; import { type CutStep } from "../cut"; import { type ExtractStep } from "../extract"; import { type HandlerStep } from "../handler"; import { type PresetCheckerStep } from "../presetChecker"; import { type ProcessStep } from "../process"; -import { type stepKind } from "../kind"; export interface StepsCustom { @@ -14,10 +12,7 @@ export interface StepsCustom { export type Steps = ( // eslint-disable-next-line @typescript-eslint/no-redundant-type-constituents | StepsCustom[ - O.GetPropsWithValueExtends< - StepsCustom, - Kind - > + keyof StepsCustom ] | CheckerStep | CutStep diff --git a/scripts/core/tsconfig.build.json b/scripts/core/tsconfig.build.json index 509f198..6ba419d 100644 --- a/scripts/core/tsconfig.build.json +++ b/scripts/core/tsconfig.build.json @@ -6,5 +6,6 @@ "declaration": true, "declarationDir": "../../dist/core", "types": null, + "stripInternal": true, }, } \ No newline at end of file diff --git a/scripts/core/types/hosts.ts b/scripts/core/types/hosts.ts new file mode 100644 index 0000000..4817621 --- /dev/null +++ b/scripts/core/types/hosts.ts @@ -0,0 +1,8 @@ +import { type O } from "@duplojs/utils"; + +export interface HostCustom {} + +export type Hosts = O.GetPropsWithValue< + HostCustom, + true +>; diff --git a/scripts/core/types/httpServerParams.ts b/scripts/core/types/httpServerParams.ts new file mode 100644 index 0000000..0aeed66 --- /dev/null +++ b/scripts/core/types/httpServerParams.ts @@ -0,0 +1,12 @@ +import { type BytesInString } from "@duplojs/utils"; +import { type Hosts } from "./hosts"; + +export interface HttpServerParams { + readonly host: Hosts; + readonly port: number; + readonly maxBodySize: BytesInString | number; + readonly informationHeaderKey: string; + readonly predictedHeaderKey: string; + readonly fromHookHeaderKey: string; + readonly uploadFolder: string; +} diff --git a/scripts/core/types/index.ts b/scripts/core/types/index.ts index 1dea773..3220226 100644 --- a/scripts/core/types/index.ts +++ b/scripts/core/types/index.ts @@ -1,2 +1,4 @@ export * from "./environment"; export * from "./forbiddenBigintDataParser"; +export * from "./httpServerParams"; +export * from "./hosts"; diff --git a/scripts/interfaces/node/bodyReaders/formData/error.ts b/scripts/interfaces/node/bodyReaders/formData/error.ts new file mode 100644 index 0000000..dc3590c --- /dev/null +++ b/scripts/interfaces/node/bodyReaders/formData/error.ts @@ -0,0 +1,14 @@ +import { kindHeritage } from "@duplojs/utils"; +import { createInterfacesNodeLibKind } from "@interface-node/kind"; + +export class BodyParseFormDataError extends kindHeritage( + "body-parse-form-data-error", + createInterfacesNodeLibKind("body-parse-form-data-error"), + Error, +) { + public constructor( + public information: string, + ) { + super({}, [`Body parse form date error: ${information}`]); + } +} diff --git a/scripts/interfaces/node/bodyReaders/formData/index.ts b/scripts/interfaces/node/bodyReaders/formData/index.ts new file mode 100644 index 0000000..7b71428 --- /dev/null +++ b/scripts/interfaces/node/bodyReaders/formData/index.ts @@ -0,0 +1,141 @@ +import { FormDataBodyController } from "@core/request"; +import { type HttpServerParams } from "@core/types"; +import { SF } from "@duplojs/server-utils"; +import { A, E, type MaybeArray, O, Path, stringToBytes, TheFormData, unwrap } from "@duplojs/utils"; +import { readRequestFormData } from "./readRequestFormData"; +import { createWriteStream } from "node:fs"; +import { WrongContentTypeError } from "@core/errors"; + +export * from "./error"; +export * from "./readRequestFormData"; + +export function createFormDataBodyReaderImplementation(serverParams: HttpServerParams) { + const serverMaxBodySize = stringToBytes(serverParams.maxBodySize); + + function addValue( + mapResult: Map>, + fieldName: string, + newValue: SF.FileInterface | string, + ) { + const value = mapResult.get(fieldName); + + if (value === undefined) { + mapResult.set(fieldName, newValue); + } else { + mapResult.set( + fieldName, + A.push(A.coalescing(value), newValue), + ); + } + } + + return FormDataBodyController.createReaderImplementation( + async(request, params) => { + if (!request.headers["content-type"]?.includes("multipart/form-data")) { + return E.error( + new WrongContentTypeError( + "multipart/form-data", + A.join(A.coalescing(request.headers["content-type"] ?? ""), " "), + ), + ); + } + + const filesAttache: string[] = []; + request.filesAttache = filesAttache; + + const result = await readRequestFormData( + request.raw.request, + new Map>(), + { + maxBodySize: params.bodyMaxSize ?? serverMaxBodySize, + fileMaxSize: params.fileMaxSize ?? Infinity, + maxFileQuantity: params.maxFileQuantity, + mimeType: params.mimeType, + maxBufferSize: params.maxBufferSize, + maxKeyLength: params.maxKeyLength, + }, + (header) => { + const fieldName = header.name; + if (header.filename) { + const extension = Path.getExtensionName(header.filename); + const displayExtension = extension ? `.${extension}` : ""; + const filePath = Path.resolveRelative([ + serverParams.uploadFolder, + `${Math.random().toString(36).slice(2, 10)}-${Date.now()}${displayExtension}`, + ]); + filesAttache.push(filePath); + + const currentFile = createWriteStream( + filePath, + { + highWaterMark: request.raw.request.readableHighWaterMark, + }, + ); + + return { + onReceiveChunk: (chunk) => new Promise( + (resolve, reject) => void currentFile.write( + chunk, + (result) => { + if (result instanceof Error) { + return void reject(result); + } + + return void resolve(); + }, + ), + ), + onEndPart: (valueAccumulator) => { + currentFile.end(); + + addValue( + valueAccumulator, + fieldName, + SF.createFileInterface(currentFile.path.toString()), + ); + + return valueAccumulator; + }, + onError: () => void currentFile.end(), + }; + } + + let currentValue = ""; + return { + onReceiveChunk: (chunk) => { + currentValue += chunk.toString("utf-8"); + }, + onEndPart: (valueAccumulator) => { + addValue( + valueAccumulator, + fieldName, + currentValue, + ); + + return valueAccumulator; + }, + onError: null, + }; + }, + ); + + if (E.isLeft(result)) { + // mandatory in case of error to avoid monopolizing the client connection if a stream is not finished. + request.raw.response.setHeader("Connection", "close"); + if (E.hasInformation(result, "server-error")) { + throw unwrap(result); + } + + return result; + } + + if (request.headers["content-type-options"]?.includes("advanced")) { + return E.success( + TheFormData.fromEntries(result.entries(), params.maxIndexArray), + ); + } + + return E.success(O.fromEntries(result.entries())); + }, + ); +} diff --git a/scripts/interfaces/node/bodyReaders/formData/readRequestFormData.ts b/scripts/interfaces/node/bodyReaders/formData/readRequestFormData.ts new file mode 100644 index 0000000..f9a855a --- /dev/null +++ b/scripts/interfaces/node/bodyReaders/formData/readRequestFormData.ts @@ -0,0 +1,260 @@ + +import { E, Path, S, type MaybePromise } from "@duplojs/utils"; +import type http from "http"; +import { BodyParseFormDataError } from "./error"; +import { BodyParseWrongChunkReceived, BodySizeExceedsLimitError } from "@core/errors"; + +const endHeaderPart = Buffer.from("\r\n\r\n"); +const bufferStart = Buffer.from("\r\n"); +const regexBoundary = /boundary=(?[^; ]+)/i; +const regexHeaderPart = /name="(?(?:\\"|[^"])+)"(?:; filename="(?(?:\\"|[^"])+)")?(?:;\s+filename\*=[^']+'[^']*'(?[^;\r\n\s]+))?/i; + +function safeDecode(value: string) { + try { + return decodeURIComponent(value); + } catch { + return value; + } +} + +interface HeaderPartInformation { + name: string; + filename?: string; +} + +export interface ReadRequestFormDataStreamChunkEvent< + GenericValueAccumulator extends unknown = unknown, +> { + onReceiveChunk(chunk: Buffer): MaybePromise; + onEndPart(valueAccumulator: GenericValueAccumulator): MaybePromise; + onError: ((error: unknown, valueAccumulator: GenericValueAccumulator) => MaybePromise) | null ; +} + +export interface ReadRequestFormDataParams { + maxBodySize: number; + maxFileQuantity: number; + maxBufferSize: number; + maxKeyLength: number; + fileMaxSize?: number; + mimeType?: RegExp; +} + +export async function readRequestFormData< + GenericValueAccumulator extends unknown, + GenericOutputHeader extends E.Left = never, +>( + request: http.IncomingMessage, + firstValueAccumulator: GenericValueAccumulator, + params: ReadRequestFormDataParams, + onReceiveHeader: (header: HeaderPartInformation) => MaybePromise< + | ReadRequestFormDataStreamChunkEvent + | Error + >, +): Promise< + | E.Left<"server-error", unknown> + | GenericOutputHeader + | E.Error + | GenericValueAccumulator + > { + const boundary = S.extract( + request.headers["content-type"] ?? "", + regexBoundary, + )?.namedGroups?.boundary; + + if (!boundary) { + return E.error(new BodyParseFormDataError("Wrong boundary.")); + } + + let valueAccumulator: GenericValueAccumulator = firstValueAccumulator; + + const startPart = Buffer.from(`\r\n--${boundary}`); + const endMultiPart = Buffer.from(`\r\n--${boundary}--`); + + let currentBuffer = bufferStart; + let size = 0; + const keep = endMultiPart.length - 1; + + let currentStream = undefined as ReadRequestFormDataStreamChunkEvent | undefined; + let fileQuantity = 0; + let currentFileSize = undefined as undefined | number; + + const checkSize = (receivedChunk: Buffer): true | Error => { + size += receivedChunk.length; + + return size > params.maxBodySize + ? new BodySizeExceedsLimitError(params.maxBodySize) + : true; + }; + + const flushReceiveHeader = async(headerPart: Buffer): Promise => { + valueAccumulator = await currentStream?.onEndPart(valueAccumulator) ?? valueAccumulator; + + const sizeResult = checkSize(headerPart); + if (sizeResult !== true) { + return sizeResult; + } + + const extract = S.extract( + headerPart.toString("utf-8"), + regexHeaderPart, + )?.namedGroups; + + const header = extract?.name + ? { + name: extract.name.trim(), + filename: ( + extract.encodedFilename !== undefined + ? safeDecode(extract.encodedFilename) + : extract.filename + )?.trim(), + } + : null; + + if (!header) { + return new BodyParseFormDataError("Bad content header part."); + } + + if (header.name.length > params.maxKeyLength) { + return new BodyParseFormDataError("key length exceeds limit."); + } + + if (header.filename !== undefined) { + currentFileSize = 0; + fileQuantity++; + if (fileQuantity > params.maxFileQuantity) { + return new BodyParseFormDataError("File quantity exceeds limit."); + } else if ( + params.mimeType !== undefined + && !params.mimeType.test(Path.getExtensionName(header.filename) ?? "") + ) { + return new BodyParseFormDataError("File have wrong mimeType."); + } + } else { + currentFileSize = undefined; + } + + const newStream = await onReceiveHeader(header); + + if (newStream instanceof Error) { + return newStream; + } + + currentStream = newStream; + + return true; + }; + + const flushReceiveChunk = async(chunk: Buffer): Promise => { + if (chunk.length === 0) { + return true; + } + + const sizeResult = checkSize(chunk); + if (sizeResult !== true) { + return sizeResult; + } + + if (!currentStream) { + return new BodyParseFormDataError("Receive chunk before header part."); + } + + if (typeof currentFileSize === "number") { + currentFileSize += chunk.length; + if (params.fileMaxSize !== undefined && currentFileSize > params.fileMaxSize) { + return new BodyParseFormDataError("File size exceeds limit."); + } + } + + await currentStream.onReceiveChunk(chunk); + + return true; + }; + + const treatError = async(error: Error): Promise> => { + await currentStream?.onError?.(error, valueAccumulator); + return E.error(error); + }; + + try { + for await (const chunk of request) { + if (!(chunk instanceof Buffer)) { + return await treatError(new BodyParseWrongChunkReceived("Buffer.", chunk)); + } + + currentBuffer = Buffer.concat([currentBuffer, chunk]); + + if (currentBuffer.length > params.maxBufferSize) { + return await treatError(new BodyParseFormDataError("Buffer size exceeds limit.")); + } + + while (true) { + const endMultiPartIndex = currentBuffer.indexOf(endMultiPart); + if (endMultiPartIndex !== -1) { + // check if buffer contain end of transmissions + currentBuffer = currentBuffer.subarray(0, endMultiPartIndex); + } + + const startPartIndex = currentBuffer.indexOf(startPart); + const endHeaderPartIndex = currentBuffer.indexOf(endHeaderPart); + if (startPartIndex !== -1 && endHeaderPartIndex !== -1) { + // check if buffer contain an entire header of part + const resultChunk = await flushReceiveChunk( + currentBuffer.subarray(0, startPartIndex), + ); + + if (resultChunk !== true) { + return await treatError(resultChunk); + } + + const endIndex = endHeaderPartIndex + endHeaderPart.length; + + const resultHeader = await flushReceiveHeader( + currentBuffer.subarray(startPartIndex, endIndex), + ); + + if (resultHeader !== true) { + return await treatError(resultHeader); + } + currentBuffer = currentBuffer.subarray(endIndex); + } else if (startPartIndex === -1 && endHeaderPartIndex === -1) { + // check if buffer contain only data + if (currentBuffer.length > keep) { + const bufferRestIndex = currentBuffer.length - keep; + const resultChunk = await flushReceiveChunk( + currentBuffer.subarray(0, bufferRestIndex), + ); + + if (resultChunk !== true) { + return await treatError(resultChunk); + } + + currentBuffer = currentBuffer.subarray(bufferRestIndex); + } + + break; + } else if (startPartIndex !== -1 && endHeaderPartIndex === -1) { + // check if buffer contain start of header but not contain end + break; + } else { + // check if buffer contain only end of header part + return await treatError(new BodyParseFormDataError("Wrong content.")); + } + } + } + + const resultChunk = await flushReceiveChunk(currentBuffer); + + if (resultChunk !== true) { + return await treatError(resultChunk); + } + + valueAccumulator = await currentStream?.onEndPart(valueAccumulator) ?? valueAccumulator; + + return valueAccumulator; + } catch (error) { + await currentStream?.onError?.(error, valueAccumulator); + return E.left("server-error", error); + } finally { + request.destroy(); + } +} diff --git a/scripts/interfaces/node/bodyReaders/index.ts b/scripts/interfaces/node/bodyReaders/index.ts new file mode 100644 index 0000000..77cee5e --- /dev/null +++ b/scripts/interfaces/node/bodyReaders/index.ts @@ -0,0 +1,2 @@ +export * from "./formData"; +export * from "./text"; diff --git a/scripts/interfaces/node/bodyReaders/text/index.ts b/scripts/interfaces/node/bodyReaders/text/index.ts new file mode 100644 index 0000000..9534a11 --- /dev/null +++ b/scripts/interfaces/node/bodyReaders/text/index.ts @@ -0,0 +1,57 @@ +import { TextBodyController } from "@core/request"; +import { readRequestText } from "./readRequestText"; +import { A, E, type Json, stringToBytes, unwrap } from "@duplojs/utils"; +import { type HttpServerParams } from "@core/types"; +import { ParseJsonError, WrongContentTypeError } from "@core/errors"; +export * from "./readRequestText"; + +export function createTextBodyReaderImplementation(serverParams: HttpServerParams) { + const serverMaxBodySize = stringToBytes(serverParams.maxBodySize); + + return TextBodyController.createReaderImplementation( + async(request, params) => { + if ( + !request.headers["content-type"]?.includes("application/json") + && !request.headers["content-type"]?.includes("text/plain") + ) { + return E.error( + new WrongContentTypeError( + "application/json or text/plain", + A.join(A.coalescing(request.headers["content-type"] ?? ""), " "), + ), + ); + } + + const result = await readRequestText( + request.raw.request, + { maxBodySize: params.bodyMaxSize ?? serverMaxBodySize }, + (result) => { + if (request.headers["content-type"]?.includes("application/json")) { + try { + return E.success( + JSON.parse(result) as Json, + ); + } catch (error) { + return E.error( + new ParseJsonError(result, error), + ); + } + } + + return E.success(result); + }, + ); + + if (E.isLeft(result)) { + // mandatory in case of error to avoid monopolizing the client connection if a stream is not finished. + request.raw.response.setHeader("Connection", "close"); + } + + if (E.hasInformation(result, "server-error")) { + throw unwrap(result); + } + + return result; + }, + ); +} diff --git a/scripts/interfaces/node/bodyReaders/text/readRequestText.ts b/scripts/interfaces/node/bodyReaders/text/readRequestText.ts new file mode 100644 index 0000000..20649d0 --- /dev/null +++ b/scripts/interfaces/node/bodyReaders/text/readRequestText.ts @@ -0,0 +1,50 @@ +import { BodyParseWrongChunkReceived, BodySizeExceedsLimitError } from "@core/errors"; +import { E } from "@duplojs/utils"; +import type http from "http"; + +export interface ReadRequestTextParams { + maxBodySize: number; +} + +export async function readRequestText< + GenericOutputValue extends unknown = string, +>( + request: http.IncomingMessage, + params: ReadRequestTextParams, + onEnd?: (result: string) => GenericOutputValue, +): Promise< + | E.Left<"server-error", unknown> + | E.Error + | GenericOutputValue + > { + let result = ""; + let size = 0; + + try { + for await (const chunk of request) { + if (!(chunk instanceof Buffer) && typeof chunk !== "string") { + return E.error(new BodyParseWrongChunkReceived("Buffer or String.", chunk)); + } + + size += chunk instanceof Buffer + ? chunk.byteLength + : Buffer.byteLength(chunk); + + if (size > params.maxBodySize) { + return E.error(new BodySizeExceedsLimitError(params.maxBodySize)); + } + + result += chunk.toString(); + } + + if (onEnd) { + return await onEnd(result); + } + + return result as GenericOutputValue; + } catch (error) { + return E.left("server-error", error); + } finally { + request.destroy(); + } +} diff --git a/scripts/interfaces/node/createHttpServer.ts b/scripts/interfaces/node/createHttpServer.ts index 3d2ac2a..64919ac 100644 --- a/scripts/interfaces/node/createHttpServer.ts +++ b/scripts/interfaces/node/createHttpServer.ts @@ -1,24 +1,28 @@ -import { type HttpServerParams, type Hub } from "@core/hub"; +import { type Hub } from "@core/hub"; import { type RouterInitializationData } from "@core/router"; -import { type Hosts } from "./types/host"; -import { type BytesInString, O } from "@duplojs/utils"; import http from "http"; import https from "https"; -import { makeNodeHook } from "./hooks"; +import { nodeHook } from "./hooks"; import { implementHttpServer } from "@core/implementHttpServer"; +import { O } from "@duplojs/utils"; +import { type HttpServerParams } from "@core/types"; +import { initDefaultHook } from "@core/defaultHooks"; +import { createFormDataBodyReaderImplementation, createTextBodyReaderImplementation } from "./bodyReaders"; -declare module "@core/hub" { +declare module "@core/types" { interface HttpServerParams { readonly interface: "node"; - readonly host: Hosts; - readonly port: number; - readonly maxBodySize: BytesInString | number; - readonly informationHeaderKey: string; - readonly predictedHeaderKey: string; - readonly fromHookHeaderKey: string; readonly http?: http.ServerOptions; readonly https?: https.ServerOptions; } + + interface HostCustom { + "::": true; + "0.0.0.0": true; + localhost: true; + "127.0.0.1": true; + "::1": true; + } } export type CreateHttpServerParams = O.PartialKeys< @@ -27,10 +31,11 @@ export type CreateHttpServerParams = O.PartialKeys< | "informationHeaderKey" | "predictedHeaderKey" | "fromHookHeaderKey" + | "uploadFolder" >; export function createHttpServer( - inputHub: Hub, + hub: Hub, params: CreateHttpServerParams, ) { const httpServerParams: HttpServerParams = O.override( @@ -42,13 +47,16 @@ export function createHttpServer( predictedHeaderKey: "predicted", fromHookHeaderKey: "from-hook", interface: "node", + uploadFolder: "./upload", }, params, ); - const hooks = makeNodeHook(inputHub, httpServerParams); - - const hub = inputHub.addRouteHooks(hooks); + hub.addBodyReaderImplementation([ + createTextBodyReaderImplementation(httpServerParams), + createFormDataBodyReaderImplementation(httpServerParams), + ]); + hub.addRouteHooks([initDefaultHook(hub, httpServerParams), nodeHook]); function whenUncaughtError( error: unknown, diff --git a/scripts/interfaces/node/error/bodyParseUnknownError.ts b/scripts/interfaces/node/error/bodyParseUnknownError.ts deleted file mode 100644 index d12de27..0000000 --- a/scripts/interfaces/node/error/bodyParseUnknownError.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { kindHeritage } from "@duplojs/utils"; -import { createInterfacesNodeLibKind } from "@interface-node/kind"; - -export class BodyParseUnknownError extends kindHeritage( - "body-parse-unknown-error", - createInterfacesNodeLibKind("body-parse-unknown-error"), - Error, -) { - public constructor( - public contentType: string, - public unknownError: unknown, - ) { - super({}, [`Error when parsing body with '${contentType}' content-type.`]); - } -} diff --git a/scripts/interfaces/node/hooks.ts b/scripts/interfaces/node/hooks.ts deleted file mode 100644 index 41cea78..0000000 --- a/scripts/interfaces/node/hooks.ts +++ /dev/null @@ -1,172 +0,0 @@ -import { type HttpServerParams, type Hub } from "@core/hub"; -import { createHookRouteLifeCycle } from "@core/route"; -import { stringToBytes } from "@duplojs/utils"; -import { BodyParseUnknownError, BodyParseWrongChunkReceived, BodySizeExceedsLimitError } from "./error"; -import { HookResponse, PredictedResponse } from "@core/response"; - -export function makeNodeHook(hub: Hub, serverParams: HttpServerParams) { - const informationHeaderKey = serverParams.informationHeaderKey; - const predictedHeaderKey = serverParams.predictedHeaderKey; - const fromHookHeaderKey = serverParams.fromHookHeaderKey; - const isDev = hub.config.environment === "DEV"; - const maxBodySize = stringToBytes(serverParams.maxBodySize); - - return createHookRouteLifeCycle({ - async parseBody({ request, exit }) { - const contentType = request.headers["content-type"] instanceof Array - ? request.headers["content-type"].join(", ") - : request.headers["content-type"] ?? ""; - - const isText = contentType.includes("text/plain"); - const isJson = contentType.includes("application/json"); - - if (!isText && !isJson) { - return exit(); - } - - const { request: rawRequest } = request.raw; - - request.body = await new Promise( - (resolve, reject) => { - function errorCallback(error: unknown) { - if ( - error instanceof BodySizeExceedsLimitError - || error instanceof BodyParseWrongChunkReceived - ) { - reject(error); - return; - } - - reject(new BodyParseUnknownError(contentType, error)); - } - - let stringBody = ""; - let byteLengthBody = 0; - - rawRequest.on("error", errorCallback); - - rawRequest.on("data", (chunk: unknown) => { - if (!(chunk instanceof Buffer) && typeof chunk !== "string") { - rawRequest.emit( - "error", - new BodyParseWrongChunkReceived(chunk), - ); - return; - } - - byteLengthBody += chunk instanceof Buffer - ? chunk.byteLength - : Buffer.byteLength(chunk); - - if (byteLengthBody > maxBodySize) { - rawRequest.emit( - "error", - new BodySizeExceedsLimitError(serverParams.maxBodySize), - ); - return; - } - stringBody += chunk.toString(); - }); - - rawRequest.on("end", () => { - try { - resolve( - isText - ? stringBody - : JSON.parse(stringBody), - ); - } catch (error) { - errorCallback(error); - } - }); - }, - ); - - return exit(); - }, - error({ error, response, exit }) { - const displayedError = isDev ? error : undefined; - - if (error instanceof BodySizeExceedsLimitError) { - return response( - "400", - "body-size-exceeds-limit-error", - displayedError, - ); - } else if (error instanceof BodyParseWrongChunkReceived) { - return response( - "400", - "body-parse-wrong-chunk-received", - displayedError, - ); - } else if (error instanceof BodyParseUnknownError) { - return response( - "400", - "body-parse-unknown-error", - displayedError, - ); - } - - return exit(); - }, - beforeSendResponse({ request, currentResponse, exit }) { - if (!currentResponse.headers?.["content-type"]) { - const body = currentResponse.body; - - if ( - typeof body === "string" - || body instanceof Error - ) { - currentResponse.setHeader("content-type", "text/plain; charset=utf-8"); - } else if ( - typeof body === "object" - || typeof body === "number" - || typeof body === "boolean" - - ) { - currentResponse.setHeader("content-type", "application/json; charset=utf-8"); - } - } - - currentResponse.setHeader(informationHeaderKey, currentResponse.information); - - if (currentResponse instanceof PredictedResponse) { - currentResponse.setHeader(predictedHeaderKey, "1"); - } else if (currentResponse instanceof HookResponse) { - currentResponse.setHeader(fromHookHeaderKey, currentResponse.fromHook); - } - - request.raw.response.writeHead( - Number(currentResponse.code), - currentResponse.headers, - ); - - return exit(); - }, - sendResponse({ request, currentResponse, exit }) { - const { response: rawResponse } = request.raw; - - const body = currentResponse.body; - - if (body instanceof Error) { - rawResponse.write( - body.toString(), - ); - } else if ( - typeof body === "object" - || typeof body === "number" - || typeof body === "boolean" - ) { - rawResponse.write( - JSON.stringify(body), - ); - } else if (typeof body === "string") { - rawResponse.write(body); - } - - rawResponse.end(); - - return exit(); - }, - }); -} diff --git a/scripts/interfaces/node/hooks/index.ts b/scripts/interfaces/node/hooks/index.ts new file mode 100644 index 0000000..3840d1b --- /dev/null +++ b/scripts/interfaces/node/hooks/index.ts @@ -0,0 +1,61 @@ +import { createHookRouteLifeCycle } from "@core/route"; +import { SF } from "@duplojs/server-utils"; +import { A } from "@duplojs/utils"; +import { createReadStream } from "node:fs"; + +export const nodeHook = createHookRouteLifeCycle({ + beforeSendResponse({ request, currentResponse, exit }) { + request.raw.response.writeHead( + Number(currentResponse.code), + currentResponse.headers, + ); + + return exit(); + }, + async sendResponse({ request, currentResponse, exit }) { + const { response: rawResponse } = request.raw; + + const body = currentResponse.body; + + if (body instanceof Error) { + rawResponse.write( + body.toString(), + ); + } else if (SF.isFileInterface(body)) { + await new Promise((resolve, reject) => { + createReadStream(body.path) + .pipe( + request.raw.response + .once("error", reject) + .once("close", resolve), + ); + }); + } else if ( + typeof body === "object" + || typeof body === "number" + || typeof body === "boolean" + ) { + rawResponse.write( + JSON.stringify(body), + ); + } else if (typeof body === "string") { + rawResponse.write(body); + } + + rawResponse.end(); + + return exit(); + }, + async afterSendResponse({ request, next }) { + if (request.filesAttache) { + await Promise.all( + A.map( + request.filesAttache, + (path) => SF.remove(path), + ), + ); + } + + return next(); + }, +}); diff --git a/scripts/interfaces/node/index.ts b/scripts/interfaces/node/index.ts index fc6b9be..f3a4810 100644 --- a/scripts/interfaces/node/index.ts +++ b/scripts/interfaces/node/index.ts @@ -1,6 +1,6 @@ export * from "./types"; export * from "./kind"; -export * from "./error"; export * from "./createHttpServer"; export * from "./hooks"; +export * from "./bodyReaders"; diff --git a/scripts/interfaces/node/types/host.ts b/scripts/interfaces/node/types/host.ts deleted file mode 100644 index 5ad3846..0000000 --- a/scripts/interfaces/node/types/host.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { type O } from "@duplojs/utils"; - -export interface HostCustom {} - -export type Hosts = ( - // eslint-disable-next-line @typescript-eslint/no-redundant-type-constituents - | HostCustom[ - O.GetPropsWithValue< - HostCustom, - true - > - ] - | "::" - | "0.0.0.0" - | "localhost" - | "127.0.0.1" - | "::1" -); diff --git a/scripts/interfaces/node/types/index.ts b/scripts/interfaces/node/types/index.ts index 7d78289..abe64d0 100644 --- a/scripts/interfaces/node/types/index.ts +++ b/scripts/interfaces/node/types/index.ts @@ -1,2 +1 @@ -export * from "./host"; export * from "./request"; diff --git a/scripts/plugins/codeGenerator/plugin.ts b/scripts/plugins/codeGenerator/plugin.ts index 13d9bf1..dfd9678 100644 --- a/scripts/plugins/codeGenerator/plugin.ts +++ b/scripts/plugins/codeGenerator/plugin.ts @@ -1,8 +1,9 @@ import * as DataParserToTypescript from "@duplojs/data-parser-tools/toTypescript"; import { type HubPlugin } from "@core/hub"; -import { A, DP, equal, S } from "@duplojs/utils"; +import { A, asserts, DP, E, equal } from "@duplojs/utils"; import { routeToDataParser } from "./routeToDataParser"; -import { writeFile } from "node:fs/promises"; +import { SF } from "@duplojs/server-utils"; +import { fileTransformer } from "./typescriptTransfomer"; export interface CodeGeneratorPluginParams { outputFile: string; @@ -18,7 +19,7 @@ export function codeGeneratorPlugin(pluginParams: CodeGeneratorPluginParams) { return; } - const routes = hub.aggregatesRoutes(); + const routes = A.from(hub.routes); const dataParserRoutes = A.flatMap( routes, @@ -35,12 +36,17 @@ export function codeGeneratorPlugin(pluginParams: CodeGeneratorPluginParams) { DP.union(dataParserRoutes), { identifier: "Routes", - transformers: DataParserToTypescript.defaultTransformers, - + transformers: [ + fileTransformer, + ...DataParserToTypescript.defaultTransformers, + ], }, ); - await writeFile(pluginParams.outputFile, output); + asserts( + await SF.writeTextFile(pluginParams.outputFile, output), + E.isRight, + ); }, }, ], diff --git a/scripts/plugins/codeGenerator/routeToDataParser.ts b/scripts/plugins/codeGenerator/routeToDataParser.ts index ffa698e..c38610f 100644 --- a/scripts/plugins/codeGenerator/routeToDataParser.ts +++ b/scripts/plugins/codeGenerator/routeToDataParser.ts @@ -1,13 +1,48 @@ import { type Route } from "@core/route"; import { aggregateStepContract } from "./aggregateStepContract"; -import { A, DP, innerPipe, O, pipe } from "@duplojs/utils"; +import { A, DP, E, innerPipe, O, P, pipe, S, unwrap } from "@duplojs/utils"; import { type ResponseContract } from "@core/response"; import { IgnoreByCodeGeneratorMetadata } from "./metadata"; +import { FormDataBodyController } from "@core/request"; +import { factory } from "typescript"; +import { type TransformerBuildFunction } from "@duplojs/data-parser-tools/toTypescript"; export interface RouteToDataParserParams { readonly defaultExtractContract: ResponseContract.Contract; } +export const bodyAsFormData: TransformerBuildFunction = (dataParser, { transformer, success, addImport }) => { + const result = transformer(dataParser); + + if (E.isLeft(result)) { + return result; + } + + addImport("@duplojs/utils", "TheFormData"); + + return success( + factory.createTypeReferenceNode( + "TheFormData", + [unwrap(result)], + ), + ); +}; + +export const convertRoutePath = (path: string) => pipe( + path, + S.split("*"), + A.flatMap( + (element, { index, self }) => A.isLastIndex(self, index) + ? element + : [element, DP.string()], + ), + P.when( + A.minElements(2), + (template) => DP.templateLiteral(template), + ), + P.otherwise(() => DP.literal(path)), +); + export function routeToDataParser( route: Route, params: RouteToDataParserParams, @@ -55,8 +90,17 @@ export function routeToDataParser( route.definition.paths, (path) => DP.object({ method: DP.literal(route.definition.method), - path: DP.literal(path), + path: convertRoutePath(path), ...entrypointContract, + ...( + entrypointContract.body && FormDataBodyController.is(route.definition.bodyController) + ? { + body: entrypointContract + .body + .addOverrideTypescriptTransformer(bodyAsFormData), + } + : {} + ), responses: DP.union(endpointContract as never), }), ), diff --git a/scripts/plugins/codeGenerator/typescriptTransfomer.ts b/scripts/plugins/codeGenerator/typescriptTransfomer.ts new file mode 100644 index 0000000..4bb6815 --- /dev/null +++ b/scripts/plugins/codeGenerator/typescriptTransfomer.ts @@ -0,0 +1,8 @@ +import { createTransformer } from "@duplojs/data-parser-tools/toTypescript"; +import { SDP } from "@duplojs/server-utils"; +import { factory } from "typescript"; + +export const fileTransformer = createTransformer( + SDP.fileKind.has, + (__, { success }) => success(factory.createTypeReferenceNode("File")), +); diff --git a/scripts/plugins/openApiGenerator/plugin.ts b/scripts/plugins/openApiGenerator/plugin.ts index 3fecf5b..33a12ae 100644 --- a/scripts/plugins/openApiGenerator/plugin.ts +++ b/scripts/plugins/openApiGenerator/plugin.ts @@ -1,13 +1,13 @@ import type { HubPlugin } from "@core/hub"; import type { JsonSchema, MapContext } from "@duplojs/data-parser-tools/toJsonSchema"; import { routeToOpenApi, type ResultSchemaContext } from "./routeToOpenApi"; -import { A, equal, G, justReturn, O, P, pipe } from "@duplojs/utils"; +import { A, asserts, E, equal, G, justReturn, O, P, pipe } from "@duplojs/utils"; import { makeOpenApiPage } from "./makeOpenApiPage"; import { makeOpenApiRoute } from "./makeOpenApiRoute"; -import { writeFile } from "fs/promises"; import type { RoutePath } from "@core/route"; import type { OpenApiDocument } from "./types/openApiDocument"; import type { OpenApiSecuritySchema, SupportedBearerFormat } from "./types"; +import { SF } from "@duplojs/server-utils"; interface OpenApiSecurityOptionBearer { type: "bearer"; @@ -81,7 +81,7 @@ export function openApiGeneratorPlugin(pluginParams: OpenApiGeneratorPluginParam const contextToJsonSchemaFactory: MapContext = new Map(); const resultSchemaContext: ResultSchemaContext = new Map(); - const routes = hub.aggregatesRoutes(); + const routes = A.from(hub.routes); const openApiRoutes = pipe( routes, @@ -203,9 +203,9 @@ export function openApiGeneratorPlugin(pluginParams: OpenApiGeneratorPluginParam const openApiDocumentString = JSON.stringify(openApiDocument, null, 2); if (pluginParams.outputFile) { - await writeFile( - pluginParams.outputFile, - openApiDocumentString, + asserts( + await SF.writeTextFile(pluginParams.outputFile, openApiDocumentString), + E.isRight, ); } diff --git a/scripts/plugins/openApiGenerator/routeToOpenApi.ts b/scripts/plugins/openApiGenerator/routeToOpenApi.ts index fbdc565..5aeb143 100644 --- a/scripts/plugins/openApiGenerator/routeToOpenApi.ts +++ b/scripts/plugins/openApiGenerator/routeToOpenApi.ts @@ -3,7 +3,7 @@ import { aggregateStepContract } from "./aggregateStepContract"; import { A, DP, isType, justReturn, O, P, pipe, S, when, whenNot } from "@duplojs/utils"; import type { ResponseCode, ResponseContract } from "@core/response"; import { type MapContext, type JsonSchema, render, defaultTransformers } from "@duplojs/data-parser-tools/toJsonSchema"; -import type { RequestMethods } from "@core/request"; +import { FormDataBodyController, type RequestMethods } from "@core/request"; import type { EndpointResponse, EntrypointParameter, OpenApiMethod } from "./types"; import { IgnoreByOpenApiGeneratorMetadata } from "./metadata"; @@ -127,6 +127,22 @@ export function routeToOpenApi( DP.identifier(DP.emptyKind), justReturn(undefined), ), + P.when( + () => FormDataBodyController.is(route.definition.bodyController), + (objectSchema) => ({ + required: true, + content: { + "multipart/form-data": { + schema: factoryJsonSchema({ + context: params.contextToJsonSchemaFactory, + resultSchemaContext: params.resultSchemaContext, + mode: "in", + schema: objectSchema, + }), + }, + }, + }), + ), P.when( DP.identifier(DP.objectKind), (objectSchema) => ({ diff --git a/scripts/plugins/openApiGenerator/types/entrypoint.ts b/scripts/plugins/openApiGenerator/types/entrypoint.ts index 491000f..43e286b 100644 --- a/scripts/plugins/openApiGenerator/types/entrypoint.ts +++ b/scripts/plugins/openApiGenerator/types/entrypoint.ts @@ -13,13 +13,23 @@ export interface EntrypointContentBodyApplicationJson { }; } +export interface EntrypointContentBodyFormData { + "multipart/form-data": { + schema: JsonSchema; + }; +} + export interface EntrypointContentBodyTextPlain { "text/plain": { schema: JsonSchema; }; } -export type EntrypointContentBody = EntrypointContentBodyApplicationJson | EntrypointContentBodyTextPlain; +export type EntrypointContentBody = ( + | EntrypointContentBodyApplicationJson + | EntrypointContentBodyTextPlain + | EntrypointContentBodyFormData +); export interface EntrypointRequestBody { required: true; diff --git a/tests/_utils/bodyReader.ts b/tests/_utils/bodyReader.ts new file mode 100644 index 0000000..0c29df9 --- /dev/null +++ b/tests/_utils/bodyReader.ts @@ -0,0 +1,25 @@ +import { createBodyController } from "@core"; +import { asserts, E, isType, unwrap } from "@duplojs/utils"; + +export function createBodyReader(theFunction: () => unknown = () => undefined) { + const BodyController = createBodyController("test"); + const bodyController = BodyController.create({}); + const bodyReader = unwrap( + bodyController.tryToCreateReader( + BodyController.createReaderImplementation( + async() => { + const result = await theFunction(); + + if (result instanceof Error) { + return E.error(result); + } + + return E.success(result); + }, + ), + ), + ); + asserts(bodyReader, isType("object")); + + return bodyReader; +} diff --git a/tests/_utils/route.ts b/tests/_utils/route.ts index 7f23439..e78f6f2 100644 --- a/tests/_utils/route.ts +++ b/tests/_utils/route.ts @@ -7,4 +7,5 @@ export const testRoute = createRoute({ preflightSteps: [], steps: [], metadata: [], + bodyController: null, }); diff --git a/tests/client/getBody.test.ts b/tests/client/getBody.test.ts index e895ac9..d2fffdc 100644 --- a/tests/client/getBody.test.ts +++ b/tests/client/getBody.test.ts @@ -43,13 +43,10 @@ describe("getBody", () => { }); it("parses blob for other content-types", async() => { - const blob = vi.fn().mockResolvedValue(new Blob(["data"])); const response = { headers: new Headers({ "content-type": "application/octet-stream" }), - blob, } as unknown as Response; - await expect(getBody(response)).resolves.toBeInstanceOf(Blob); - expect(blob).toHaveBeenCalledTimes(1); + await expect(getBody(response)).resolves.toBe(undefined); }); }); diff --git a/tests/client/httpClient/index.test.ts b/tests/client/httpClient/index.test.ts index 2d85a4e..7eb441a 100644 --- a/tests/client/httpClient/index.test.ts +++ b/tests/client/httpClient/index.test.ts @@ -3,8 +3,7 @@ vi.mock("@client/promiseRequest", () => ({ })); import { PromiseRequest } from "@client/promiseRequest"; -import { createHttpClient, httpClientKind } from "@client"; -import { type Hooks } from "@client/hooks"; +import { createHttpClient, type Hooks, httpClientKind } from "@client"; import { forward } from "@duplojs/utils"; describe("httpClient", () => { diff --git a/tests/client/httpClient/withType.ts b/tests/client/httpClient/withType.ts index 592409e..a9e75f6 100644 --- a/tests/client/httpClient/withType.ts +++ b/tests/client/httpClient/withType.ts @@ -1,5 +1,5 @@ -import { type ClientResponse, createHttpClient, type RequestErrorContent, type NotPredictedClientResponse, type PromiseRequest, type PromiseRequestParams } from "@client"; -import { E, S, type ExpectType } from "@duplojs/utils"; +import { type ClientResponse, createHttpClient, type RequestErrorContent, type NotPredictedClientResponse, type PromiseRequest, type PromiseRequestParams, type GetServerRoutePath, type FindServerRoute, type AddPrefixPathServerRoute, type RemovePrefixPathServerRoute, type FindServerRouteResponse } from "@client"; +import { E, S, type TheFormData, type ExpectType, createFormData } from "@duplojs/utils"; type Routes = { method: "GET"; @@ -53,6 +53,30 @@ type Routes = { age: number; }; }; +} | { + method: "POST"; + path: "/documents"; + body: TheFormData<{ + bool: boolean; + myFile: File; + }>; + responses: { + code: "422"; + information: "extract-error"; + body?: undefined; + } | { + code: "204"; + information: "file.receive"; + body?: undefined; + }; +} | { + method: "GET"; + path: `/documents/${string}`; + responses: { + code: "200"; + information: "file.send"; + body: File; + }; }; const httpClient = createHttpClient({ @@ -71,9 +95,9 @@ const promiseRequest = httpClient type Check = ExpectType< typeof promiseRequest, PromiseRequest< - PromiseRequestParams<{ + { params1: string; - }>, + }, | { code: "422"; information: "extract-error"; @@ -166,9 +190,9 @@ void promiseRequest .whenInformationalResponse((value) => { type Check = ExpectType< typeof value, - ClientResponse>, + }>, "strict" >; }) @@ -200,9 +224,9 @@ void promiseRequest .whenRedirectionResponse((value) => { type Check = ExpectType< typeof value, - ClientResponse>, + }>, "strict" >; }) @@ -230,9 +254,9 @@ void promiseRequest .whenServerErrorResponse((value) => { type Check = ExpectType< typeof value, - ClientResponse>, + }>, "strict" >; }) @@ -278,9 +302,9 @@ void promiseRequest .whenNotPredictedResponse((value) => { type Check = ExpectType< typeof value, - NotPredictedClientResponse>, + }>, "strict" >; }) @@ -306,7 +330,7 @@ void promiseRequest if (E.isRight(value)) { type Check = ExpectType< typeof value, - E.EitherRight<"response", { + E.Right<"response", { code: "422"; information: "extract-error"; body: undefined; @@ -324,7 +348,7 @@ void promiseRequest } else { type Check = ExpectType< typeof value, - E.EitherLeft<"request-error", RequestErrorContent> | E.EitherLeft<"unexpect-response", { + E.Left<"request-error", RequestErrorContent> | E.Left<"unexpect-response", { code: "200"; information: "users.find"; body: { @@ -354,7 +378,7 @@ void promiseRequest if (E.isRight(value)) { type Check = ExpectType< typeof value, - E.EitherRight<"response", { + E.Right<"response", { code: "422"; information: "extract-error"; body: undefined; @@ -372,7 +396,7 @@ void promiseRequest } else { type Check = ExpectType< typeof value, - E.EitherLeft<"request-error", RequestErrorContent> | E.EitherLeft<"unexpect-response", { + E.Left<"request-error", RequestErrorContent> | E.Left<"unexpect-response", { code: "200"; information: "users.find"; body: { @@ -402,17 +426,17 @@ void promiseRequest if (E.isRight(value)) { type Check = ExpectType< typeof value, - E.EitherRight<"response", ClientResponse>>, + }>>, "strict" >; } else { type Check = ExpectType< typeof value, - E.EitherLeft<"request-error", RequestErrorContent> | E.EitherLeft<"unexpect-response", ClientResponse | E.Left<"unexpect-response", ClientResponse<{ params1: string; - }>>>, + }>>, "strict" >; } @@ -424,7 +448,7 @@ void promiseRequest if (E.isRight(value)) { type Check = ExpectType< typeof value, - E.EitherRight<"response", { + E.Right<"response", { code: "200"; information: "users.find"; body: { @@ -448,7 +472,7 @@ void promiseRequest } else { type Check = ExpectType< typeof value, - E.EitherLeft<"request-error", RequestErrorContent> | E.EitherLeft<"unexpect-response", { + E.Left<"request-error", RequestErrorContent> | E.Left<"unexpect-response", { code: "422"; information: "extract-error"; body: undefined; @@ -472,17 +496,17 @@ void promiseRequest if (E.isRight(value)) { type Check = ExpectType< typeof value, - E.EitherRight<"response", ClientResponse>>, + }>>, "strict" >; } else { type Check = ExpectType< typeof value, - E.EitherLeft<"request-error", RequestErrorContent> | E.EitherLeft<"unexpect-response", ClientResponse | E.Left<"unexpect-response", ClientResponse<{ params1: string; - }>>>, + }>>, "strict" >; } @@ -494,7 +518,7 @@ void promiseRequest if (E.isRight(value)) { type Check = ExpectType< typeof value, - E.EitherRight<"response", { + E.Right<"response", { code: "422"; information: "extract-error"; body: undefined; @@ -512,7 +536,7 @@ void promiseRequest } else { type Check = ExpectType< typeof value, - E.EitherLeft<"request-error", RequestErrorContent> | E.EitherLeft<"unexpect-response", { + E.Left<"request-error", RequestErrorContent> | E.Left<"unexpect-response", { code: "200"; information: "users.find"; body: { @@ -542,17 +566,17 @@ void promiseRequest if (E.isRight(value)) { type Check = ExpectType< typeof value, - E.EitherRight<"response", ClientResponse>>, + }>>, "strict" >; } else { type Check = ExpectType< typeof value, - E.EitherLeft<"request-error", RequestErrorContent> | E.EitherLeft<"unexpect-response", ClientResponse | E.Left<"unexpect-response", ClientResponse<{ params1: string; - }>>>, + }>>, "strict" >; } @@ -564,7 +588,7 @@ void promiseRequest if (E.isRight(value)) { type Check = ExpectType< typeof value, - E.EitherRight<"response", { + E.Right<"response", { code: "422"; information: "extract-error"; body: undefined; @@ -602,9 +626,9 @@ void promiseRequest } else { type Check = ExpectType< typeof value, - E.EitherLeft<"request-error", RequestErrorContent> | E.EitherLeft<"unexpect-response", ClientResponse | E.Left<"unexpect-response", ClientResponse<{ params1: string; - }>>>, + }>>, "strict" >; } @@ -671,9 +695,9 @@ void promiseRequest .then((value) => { type Check = ExpectType< typeof value, - ClientResponse>, + }>, "strict" >; }); @@ -711,9 +735,9 @@ void promiseRequest .then((value) => { type Check = ExpectType< typeof value, - ClientResponse>, + }>, "strict" >; }); @@ -747,9 +771,9 @@ void promiseRequest .then((value) => { type Check = ExpectType< typeof value, - ClientResponse>, + }>, "strict" >; }); @@ -801,7 +825,7 @@ void promiseRequest if (E.isRight(value)) { type Check = ExpectType< typeof value, - E.EitherRight< + E.Right< "response", | { code: "422"; @@ -833,14 +857,14 @@ void promiseRequest requestParams: PromiseRequestParams<{ params1: string }>; predicted: boolean; } - | NotPredictedClientResponse> + | NotPredictedClientResponse<{ params1: string }> >, "strict" >; } else { type Check = ExpectType< typeof value, - E.EitherLeft<"request-error", RequestErrorContent>, + E.Left<"request-error", RequestErrorContent>, "strict" >; } @@ -875,10 +899,10 @@ void httpClient.get("/users") .then((value) => { type Check = ExpectType< typeof value, - | E.EitherLeft<"request-error", RequestErrorContent> - | E.EitherRight< + | E.Left<"request-error", RequestErrorContent> + | E.Right< "response", - | NotPredictedClientResponse> + | NotPredictedClientResponse<{ params1: string }> | { code: "200"; information: "users.findMany"; @@ -934,10 +958,10 @@ void httpClient.post("/users", { .then((value) => { type Check = ExpectType< typeof value, - | E.EitherLeft<"request-error", RequestErrorContent> - | E.EitherRight< + | E.Left<"request-error", RequestErrorContent> + | E.Right< "response", - | NotPredictedClientResponse> + | NotPredictedClientResponse<{ params1: string }> | { code: "422"; information: "extract-error"; @@ -972,3 +996,77 @@ void httpClient.post("/users", { "strict" >; }); + +void httpClient.post( + "/documents", + { + body: createFormData({ + bool: true, + myFile: new File([], "test"), + }), + }, +); + +void httpClient + .get( + "/documents/test", + { + hookParams: { params1: "" }, + }, + ) + .whenInformation("file.send", ({ body }) => { + type Check = ExpectType< + typeof body, + undefined, + "strict" + >; + }); + +type Check1 = ExpectType< + RemovePrefixPathServerRoute< + AddPrefixPathServerRoute< + FindServerRoute, + "/titi/toto" + >, + "/titi/" + >, + { + method: "GET"; + path: "toto/users/{userId}"; + params: { + userId: number; + }; + responses: { + code: "422"; + information: "extract-error"; + body?: undefined; + } | { + code: "200"; + information: "users.find"; + body: { + id: number; + name: string; + age: number; + }; + }; + }, + "strict" +>; + +type Check2 = ExpectType< + FindServerRouteResponse< + FindServerRoute, + "information", + "users.find" + >, + { + code: "200"; + information: "users.find"; + body: { + id: number; + name: string; + age: number; + }; + }, + "strict" +>; diff --git a/tests/client/httpClient/withoutType.ts b/tests/client/httpClient/withoutType.ts index eb5125c..02dbfe3 100644 --- a/tests/client/httpClient/withoutType.ts +++ b/tests/client/httpClient/withoutType.ts @@ -1,7 +1,7 @@ -import { type ClientResponse, createHttpClient, type RequestErrorContent, type NotPredictedClientResponse, type PromiseRequest, type PromiseRequestParams } from "@client"; -import { E, type ExpectType } from "@duplojs/utils"; +import { type ClientResponse, createHttpClient, type RequestErrorContent, type NotPredictedClientResponse, type PromiseRequest, type PromiseRequestParams, type ServerRoute } from "@client"; +import { createFormData, E, type ExpectType } from "@duplojs/utils"; -const httpClient = createHttpClient({ +const httpClient = createHttpClient({ baseUrl: "http://test.com", }); @@ -14,24 +14,14 @@ const promiseRequest = httpClient type Check = ExpectType< typeof promiseRequest, PromiseRequest< - PromiseRequestParams<{ - params1: string; - }>, { - code: `${number}`; - information: string | undefined; - body: unknown; - ok: boolean | null; - headers: Headers; - type: ResponseType; - url: string; - redirected: boolean; - raw: globalThis.Response; - predicted: boolean; - requestParams: PromiseRequestParams<{ + params1: string; + }, + ClientResponse< + { params1: string; - }>; - } + } + > >, "strict" >; @@ -40,81 +30,81 @@ void promiseRequest .whenInformation("test", (value) => { type Check = ExpectType< typeof value, - ClientResponse>, + }>, "strict" >; }) .whenCode("200", (value) => { type Check = ExpectType< typeof value, - ClientResponse>, + }>, "strict" >; }) .whenInformationalResponse((value) => { type Check = ExpectType< typeof value, - ClientResponse>, + }>, "strict" >; }) .whenSuccessfulResponse((value) => { type Check = ExpectType< typeof value, - ClientResponse>, + }>, "strict" >; }) .whenRedirectionResponse((value) => { type Check = ExpectType< typeof value, - ClientResponse>, + }>, "strict" >; }) .whenClientErrorResponse((value) => { type Check = ExpectType< typeof value, - ClientResponse>, + }>, "strict" >; }) .whenServerErrorResponse((value) => { type Check = ExpectType< typeof value, - ClientResponse>, + }>, "strict" >; }) .whenExpectedResponse((value) => { type Check = ExpectType< typeof value, - ClientResponse>, + }>, "strict" >; }) .whenNotPredictedResponse((value) => { type Check = ExpectType< typeof value, - NotPredictedClientResponse>, + }>, "strict" >; }) @@ -140,17 +130,17 @@ void promiseRequest if (E.isRight(value)) { type Check = ExpectType< typeof value, - E.EitherRight<"response", ClientResponse>>, + }>>, "strict" >; } else { type Check = ExpectType< typeof value, - E.EitherLeft<"request-error", RequestErrorContent> | E.EitherLeft<"unexpect-response", ClientResponse | E.Left<"unexpect-response", ClientResponse<{ params1: string; - }>>>, + }>>, "strict" >; } @@ -162,17 +152,17 @@ void promiseRequest if (E.isRight(value)) { type Check = ExpectType< typeof value, - E.EitherRight<"response", ClientResponse>>, + }>>, "strict" >; } else { type Check = ExpectType< typeof value, - E.EitherLeft<"request-error", RequestErrorContent> | E.EitherLeft<"unexpect-response", ClientResponse | E.Left<"unexpect-response", ClientResponse<{ params1: string; - }>>>, + }>>, "strict" >; } @@ -184,17 +174,17 @@ void promiseRequest if (E.isRight(value)) { type Check = ExpectType< typeof value, - E.EitherRight<"response", ClientResponse>>, + }>>, "strict" >; } else { type Check = ExpectType< typeof value, - E.EitherLeft<"request-error", RequestErrorContent> | E.EitherLeft<"unexpect-response", ClientResponse | E.Left<"unexpect-response", ClientResponse<{ params1: string; - }>>>, + }>>, "strict" >; } @@ -206,17 +196,17 @@ void promiseRequest if (E.isRight(value)) { type Check = ExpectType< typeof value, - E.EitherRight<"response", ClientResponse>>, + }>>, "strict" >; } else { type Check = ExpectType< typeof value, - E.EitherLeft<"request-error", RequestErrorContent> | E.EitherLeft<"unexpect-response", ClientResponse | E.Left<"unexpect-response", ClientResponse<{ params1: string; - }>>>, + }>>, "strict" >; } @@ -228,17 +218,17 @@ void promiseRequest if (E.isRight(value)) { type Check = ExpectType< typeof value, - E.EitherRight<"response", ClientResponse>>, + }>>, "strict" >; } else { type Check = ExpectType< typeof value, - E.EitherLeft<"request-error", RequestErrorContent> | E.EitherLeft<"unexpect-response", ClientResponse | E.Left<"unexpect-response", ClientResponse<{ params1: string; - }>>>, + }>>, "strict" >; } @@ -250,17 +240,17 @@ void promiseRequest if (E.isRight(value)) { type Check = ExpectType< typeof value, - E.EitherRight<"response", ClientResponse>>, + }>>, "strict" >; } else { type Check = ExpectType< typeof value, - E.EitherLeft<"request-error", RequestErrorContent> | E.EitherLeft<"unexpect-response", ClientResponse | E.Left<"unexpect-response", ClientResponse<{ params1: string; - }>>>, + }>>, "strict" >; } @@ -272,17 +262,17 @@ void promiseRequest if (E.isRight(value)) { type Check = ExpectType< typeof value, - E.EitherRight<"response", ClientResponse>>, + }>>, "strict" >; } else { type Check = ExpectType< typeof value, - E.EitherLeft<"request-error", RequestErrorContent> | E.EitherLeft<"unexpect-response", ClientResponse | E.Left<"unexpect-response", ClientResponse<{ params1: string; - }>>>, + }>>, "strict" >; } @@ -294,17 +284,17 @@ void promiseRequest if (E.isRight(value)) { type Check = ExpectType< typeof value, - E.EitherRight<"response", ClientResponse>>, + }>>, "strict" >; } else { type Check = ExpectType< typeof value, - E.EitherLeft<"request-error", RequestErrorContent> | E.EitherLeft<"unexpect-response", ClientResponse | E.Left<"unexpect-response", ClientResponse<{ params1: string; - }>>>, + }>>, "strict" >; } @@ -315,9 +305,9 @@ void promiseRequest .then((value) => { type Check = ExpectType< typeof value, - ClientResponse>, + }>, "strict" >; }); @@ -327,9 +317,9 @@ void promiseRequest .then((value) => { type Check = ExpectType< typeof value, - ClientResponse>, + }>, "strict" >; }); @@ -339,9 +329,9 @@ void promiseRequest .then((value) => { type Check = ExpectType< typeof value, - ClientResponse>, + }>, "strict" >; }); @@ -351,9 +341,9 @@ void promiseRequest .then((value) => { type Check = ExpectType< typeof value, - ClientResponse>, + }>, "strict" >; }); @@ -363,9 +353,9 @@ void promiseRequest .then((value) => { type Check = ExpectType< typeof value, - ClientResponse>, + }>, "strict" >; }); @@ -375,9 +365,9 @@ void promiseRequest .then((value) => { type Check = ExpectType< typeof value, - ClientResponse>, + }>, "strict" >; }); @@ -387,9 +377,9 @@ void promiseRequest .then((value) => { type Check = ExpectType< typeof value, - ClientResponse>, + }>, "strict" >; }); @@ -399,9 +389,9 @@ void promiseRequest .then((value) => { type Check = ExpectType< typeof value, - ClientResponse>, + }>, "strict" >; }); @@ -411,21 +401,21 @@ void promiseRequest if (E.isRight(value)) { type Check = ExpectType< typeof value, - E.EitherRight< + E.Right< "response", - | ClientResponse> - | NotPredictedClientResponse + | NotPredictedClientResponse<{ params1: string; - }>> + }> >, "strict" >; } else { type Check = ExpectType< typeof value, - E.EitherLeft<"request-error", RequestErrorContent>, + E.Left<"request-error", RequestErrorContent>, "strict" >; } @@ -435,20 +425,20 @@ void httpClient.get("/test") .whenInformation("test", (value) => { type Check = ExpectType< typeof value, - ClientResponse>, + }>, "strict" >; }) .then((value) => { type Check = ExpectType< typeof value, - | E.EitherLeft<"request-error", RequestErrorContent> - | E.EitherRight< + | E.Left<"request-error", RequestErrorContent> + | E.Right< "response", - | ClientResponse> - | NotPredictedClientResponse> + | ClientResponse<{ params1: string }> + | NotPredictedClientResponse<{ params1: string }> >, "strict" >; @@ -458,21 +448,31 @@ void httpClient.get("/test", { body: "" }) .whenInformation("test", (value) => { type Check = ExpectType< typeof value, - ClientResponse>, + }>, "strict" >; }) .then((value) => { type Check = ExpectType< typeof value, - | E.EitherLeft<"request-error", RequestErrorContent> - | E.EitherRight< + | E.Left<"request-error", RequestErrorContent> + | E.Right< "response", - | ClientResponse> - | NotPredictedClientResponse> + | ClientResponse<{ params1: string }> + | NotPredictedClientResponse<{ params1: string }> >, "strict" >; }); + +void httpClient.post( + "/documents", + { + body: createFormData({ + bool: true, + myFile: new File([], "test"), + }), + }, +); diff --git a/tests/client/promiseRequest.test.ts b/tests/client/promiseRequest.test.ts index b7c3782..6a30082 100644 --- a/tests/client/promiseRequest.test.ts +++ b/tests/client/promiseRequest.test.ts @@ -1,8 +1,8 @@ -import { PromiseRequest, type PromiseRequestParams } from "@client/promiseRequest"; -import { type Hooks } from "@client/hooks"; +import { type Hooks, type PromiseRequestParams } from "@client"; +import { PromiseRequest } from "@client/promiseRequest"; import { type ClientResponse } from "@client/types/clientResponse"; import { UnexpectedCodeResponseError, UnexpectedInformationResponseError, UnexpectedResponseError, UnexpectedResponseTypeError } from "@client/unexpectedResponseError"; -import { asserts, unwrap, E } from "@duplojs/utils"; +import { asserts, unwrap, E, createFormData } from "@duplojs/utils"; describe("PromiseRequest", () => { const createHooks = (): Hooks => ({ @@ -96,6 +96,7 @@ describe("PromiseRequest", () => { .mockResolvedValueOnce(jsonResponse) .mockResolvedValueOnce(jsonResponse) .mockResolvedValueOnce(jsonResponse) + .mockResolvedValueOnce(jsonResponse) .mockRejectedValueOnce(new Error("network")); vi.stubGlobal("fetch", fetchMock); @@ -128,6 +129,10 @@ describe("PromiseRequest", () => { body: new FormData(), }); + const paramsWithTheFormData = createParams({ + body: createFormData({}), + }); + const paramsWithArray = createParams({ body: ["test"], }); @@ -138,6 +143,7 @@ describe("PromiseRequest", () => { const resultNumber = await PromiseRequest.fetch(paramsWithNumber); const resultHeader = await PromiseRequest.fetch(paramsWithHeader); const resultFormData = await PromiseRequest.fetch(paramsWithFormData); + const resultTheFormData = await PromiseRequest.fetch(paramsWithTheFormData); const resultArray = await PromiseRequest.fetch(paramsWithArray); const resultError = await PromiseRequest.fetch(createParams()); @@ -148,6 +154,16 @@ describe("PromiseRequest", () => { method: "GET", }), ); + expect(fetchMock).toHaveBeenNthCalledWith( + 7, + "http://test.local/resource", + expect.objectContaining({ + method: "GET", + headers: { + "content-type-options": "advanced", + }, + }), + ); asserts(result, E.isRight); asserts(resultObject, E.isRight); @@ -155,6 +171,7 @@ describe("PromiseRequest", () => { asserts(resultNumber, E.isRight); asserts(resultHeader, E.isRight); asserts(resultFormData, E.isRight); + asserts(resultTheFormData, E.isRight); asserts(resultArray, E.isRight); asserts(resultError, E.isLeft); diff --git a/tests/core/builders/preflight/route.test.ts b/tests/core/builders/preflight/route.test.ts index 16548d5..df3ba29 100644 --- a/tests/core/builders/preflight/route.test.ts +++ b/tests/core/builders/preflight/route.test.ts @@ -13,6 +13,7 @@ describe("preflight builder use route builder", () => { method: "GET", paths: ["/test"], preflightSteps: [], + bodyController: null, metadata: [IgnoreByRouteStoreMetadata(), IgnoreByRouteStoreMetadata()], steps: [], }, @@ -26,6 +27,7 @@ describe("preflight builder use route builder", () => { readonly method: "GET"; readonly paths: readonly ["/test"]; readonly preflightSteps: readonly []; + readonly bodyController: null; readonly steps: readonly []; readonly hooks: readonly []; readonly metadata: readonly [ @@ -52,6 +54,7 @@ describe("preflight builder use route builder", () => { paths: ["/test", "/toto"], preflightSteps: [], metadata: [], + bodyController: null, steps: [], }, }), @@ -65,6 +68,7 @@ describe("preflight builder use route builder", () => { readonly paths: readonly ["/test", "/toto"]; readonly preflightSteps: readonly []; readonly steps: readonly []; + readonly bodyController: null; readonly hooks: readonly []; readonly metadata: readonly []; }, @@ -94,6 +98,7 @@ describe("preflight builder use route builder", () => { method: "GET", paths: ["/test"], preflightSteps: [], + bodyController: null, metadata: [], steps: [], }, @@ -107,6 +112,7 @@ describe("preflight builder use route builder", () => { readonly method: "GET"; readonly paths: readonly ["/test"]; readonly preflightSteps: readonly []; + readonly bodyController: null; readonly steps: readonly []; readonly hooks: readonly [ { @@ -150,6 +156,7 @@ describe("preflight builder use route builder", () => { [builderKind.runTimeKey]: { hooks: [], method: "GET", + bodyController: null, paths: ["/toto"], preflightSteps: [ { @@ -174,6 +181,7 @@ describe("preflight builder use route builder", () => { { readonly method: "GET"; readonly paths: readonly ["/toto"]; + readonly bodyController: null; readonly preflightSteps: readonly [ ProcessStep<{ readonly process: typeof process; @@ -210,6 +218,7 @@ describe("preflight builder use route builder", () => { { onConstructRequest: expect.any(Function) }, ], method: "GET", + bodyController: null, paths: ["/toto"], preflightSteps: [], metadata: [], @@ -224,6 +233,7 @@ describe("preflight builder use route builder", () => { { readonly method: "GET"; readonly paths: readonly ["/toto"]; + readonly bodyController: null; readonly preflightSteps: readonly []; readonly steps: readonly []; readonly hooks: readonly [ diff --git a/tests/core/builders/route/builder.test.ts b/tests/core/builders/route/builder.test.ts index f40bdad..9be55a5 100644 --- a/tests/core/builders/route/builder.test.ts +++ b/tests/core/builders/route/builder.test.ts @@ -1,4 +1,4 @@ -import { type RouteBuilder, useRouteBuilder, type Request, type HookParamsOnConstructRequest, type Metadata, IgnoreByRouteStoreMetadata } from "@core"; +import { type RouteBuilder, useRouteBuilder, type Request, type HookParamsOnConstructRequest, type Metadata, IgnoreByRouteStoreMetadata, controlBodyAsFormData, type BodyController, type FormDataBodyReaderParams } from "@core"; import { builderKind, type ExpectType } from "@duplojs/utils"; describe("route builder", () => { @@ -14,6 +14,7 @@ describe("route builder", () => { preflightSteps: [], steps: [], metadata: [IgnoreByRouteStoreMetadata()], + bodyController: null, }, }), ); @@ -28,6 +29,7 @@ describe("route builder", () => { readonly steps: readonly []; readonly hooks: readonly []; readonly metadata: readonly [Metadata<"ignore-by-route-store", unknown>]; + readonly bodyController: null; }, {}, Request @@ -48,6 +50,7 @@ describe("route builder", () => { preflightSteps: [], steps: [], metadata: [], + bodyController: null, }, }), ); @@ -62,6 +65,7 @@ describe("route builder", () => { readonly steps: readonly []; readonly hooks: readonly []; readonly metadata: readonly []; + readonly bodyController: null; }, {}, Request @@ -90,6 +94,7 @@ describe("route builder", () => { preflightSteps: [], steps: [], metadata: [], + bodyController: null, }, }), ); @@ -117,6 +122,7 @@ describe("route builder", () => { }, ]; readonly metadata: readonly []; + readonly bodyController: null; }, {}, & Request @@ -126,4 +132,41 @@ describe("route builder", () => { "strict" >; }); + + it("useRouteBuilder with Custom bodyController", () => { + const bodyController = controlBodyAsFormData({ maxFileQuantity: 10 }); + const routeBuilder = useRouteBuilder("GET", "/test", { bodyController }); + + expect({ ...routeBuilder }).toStrictEqual( + expect.objectContaining({ + [builderKind.runTimeKey]: { + hooks: [], + method: "GET", + paths: ["/test"], + preflightSteps: [], + steps: [], + metadata: [], + bodyController: bodyController, + }, + }), + ); + + type Check = ExpectType< + typeof routeBuilder, + RouteBuilder< + { + readonly method: "GET"; + readonly paths: readonly ["/test"]; + readonly preflightSteps: readonly []; + readonly steps: readonly []; + readonly hooks: readonly []; + readonly metadata: readonly []; + readonly bodyController: BodyController<"formData", FormDataBodyReaderParams>; + }, + {}, + Request + >, + "strict" + >; + }); }); diff --git a/tests/core/builders/route/checker.test.ts b/tests/core/builders/route/checker.test.ts index 8a28c6c..a293d3a 100644 --- a/tests/core/builders/route/checker.test.ts +++ b/tests/core/builders/route/checker.test.ts @@ -36,6 +36,7 @@ describe("route builder checker method", () => { paths: ["/test"], preflightSteps: [], metadata: [], + bodyController: null, steps: [ expect.objectContaining({ [extractStepKind.runTimeKey]: null, @@ -67,6 +68,7 @@ describe("route builder checker method", () => { readonly paths: readonly ["/test"]; readonly preflightSteps: readonly []; readonly hooks: readonly []; + readonly bodyController: null; readonly steps: readonly [ ExtractStep<{ readonly shape: { @@ -133,6 +135,7 @@ describe("route builder checker method", () => { method: "GET", paths: ["/test"], preflightSteps: [], + bodyController: null, metadata: [], steps: [ expect.objectContaining({ @@ -168,6 +171,7 @@ describe("route builder checker method", () => { readonly paths: readonly ["/test"]; readonly preflightSteps: readonly []; readonly hooks: readonly []; + readonly bodyController: null; readonly steps: readonly [ ExtractStep<{ readonly shape: { @@ -244,6 +248,7 @@ describe("route builder checker method", () => { method: "GET", paths: ["/test"], preflightSteps: [], + bodyController: null, metadata: [], steps: [ expect.objectContaining({ @@ -277,6 +282,7 @@ describe("route builder checker method", () => { readonly paths: readonly ["/test"]; readonly preflightSteps: readonly []; readonly hooks: readonly []; + readonly bodyController: null; readonly steps: readonly [ ExtractStep<{ readonly shape: { @@ -342,6 +348,7 @@ describe("route builder checker method", () => { method: "GET", paths: ["/test"], preflightSteps: [], + bodyController: null, metadata: [], steps: [ expect.objectContaining({ @@ -375,6 +382,7 @@ describe("route builder checker method", () => { readonly paths: readonly ["/test"]; readonly preflightSteps: readonly []; readonly hooks: readonly []; + readonly bodyController: null; readonly steps: readonly [ ExtractStep<{ readonly shape: { diff --git a/tests/core/builders/route/cut.test.ts b/tests/core/builders/route/cut.test.ts index c190ec8..cfaea73 100644 --- a/tests/core/builders/route/cut.test.ts +++ b/tests/core/builders/route/cut.test.ts @@ -31,6 +31,7 @@ describe("route builder cut method", () => { method: "GET", paths: ["/test"], preflightSteps: [], + bodyController: null, metadata: [], steps: [ expect.objectContaining({ @@ -61,6 +62,7 @@ describe("route builder cut method", () => { readonly paths: readonly ["/test"]; readonly preflightSteps: readonly []; readonly hooks: readonly []; + readonly bodyController: null; readonly steps: readonly [ ExtractStep<{ readonly shape: { @@ -127,6 +129,7 @@ describe("route builder cut method", () => { method: "GET", paths: ["/test"], preflightSteps: [], + bodyController: null, metadata: [], steps: [ { @@ -160,6 +163,7 @@ describe("route builder cut method", () => { readonly method: "GET"; readonly preflightSteps: readonly []; readonly hooks: readonly []; + readonly bodyController: null; readonly steps: readonly [ CutStep<{ readonly responseContract: readonly [ @@ -227,6 +231,7 @@ describe("route builder cut method", () => { method: "GET", paths: ["/test"], preflightSteps: [], + bodyController: null, metadata: [], steps: [ expect.objectContaining({ @@ -257,6 +262,7 @@ describe("route builder cut method", () => { readonly method: "GET"; readonly preflightSteps: readonly []; readonly hooks: readonly []; + readonly bodyController: null; readonly steps: readonly [ ExtractStep<{ readonly shape: { @@ -327,6 +333,7 @@ describe("route builder cut method", () => { method: "GET", paths: ["/test"], preflightSteps: [], + bodyController: null, metadata: [], steps: [ expect.objectContaining({ @@ -357,6 +364,7 @@ describe("route builder cut method", () => { readonly method: "GET"; readonly preflightSteps: readonly []; readonly hooks: readonly []; + readonly bodyController: null; readonly steps: readonly [ ExtractStep<{ readonly shape: { @@ -423,6 +431,7 @@ describe("route builder cut method", () => { method: "GET", paths: ["/test"], preflightSteps: [], + bodyController: null, metadata: [], steps: [ expect.objectContaining({ @@ -453,6 +462,7 @@ describe("route builder cut method", () => { readonly method: "GET"; readonly preflightSteps: readonly []; readonly hooks: readonly []; + readonly bodyController: null; readonly steps: readonly [ ExtractStep<{ readonly shape: { diff --git a/tests/core/builders/route/extract.test.ts b/tests/core/builders/route/extract.test.ts index 1259766..4d0c63b 100644 --- a/tests/core/builders/route/extract.test.ts +++ b/tests/core/builders/route/extract.test.ts @@ -14,6 +14,7 @@ describe("route builder extract method", () => { method: "GET", paths: ["/test"], preflightSteps: [], + bodyController: null, metadata: [], steps: [ { @@ -42,6 +43,7 @@ describe("route builder extract method", () => { readonly paths: readonly ["/test"]; readonly preflightSteps: readonly []; readonly hooks: readonly []; + readonly bodyController: null; readonly steps: readonly [ ExtractStep<{ readonly shape: { @@ -78,6 +80,7 @@ describe("route builder extract method", () => { hooks: [], method: "GET", paths: ["/test"], + bodyController: null, preflightSteps: [], metadata: [], steps: [ @@ -110,6 +113,7 @@ describe("route builder extract method", () => { readonly paths: readonly ["/test"]; readonly preflightSteps: readonly []; readonly hooks: readonly []; + readonly bodyController: null; readonly steps: readonly [ ExtractStep<{ readonly shape: { @@ -151,6 +155,7 @@ describe("route builder extract method", () => { hooks: [], method: "GET", paths: ["/test"], + bodyController: null, preflightSteps: [], metadata: [], steps: [ @@ -181,6 +186,7 @@ describe("route builder extract method", () => { readonly method: "GET"; readonly paths: readonly ["/test"]; readonly preflightSteps: readonly []; + readonly bodyController: null; readonly hooks: readonly []; readonly steps: readonly [ ExtractStep<{ @@ -241,6 +247,7 @@ describe("route builder extract method", () => { hooks: [], method: "GET", paths: ["/test"], + bodyController: null, preflightSteps: [], metadata: [], steps: [ diff --git a/tests/core/builders/route/handler.test.ts b/tests/core/builders/route/handler.test.ts index fa0592d..fb04cd9 100644 --- a/tests/core/builders/route/handler.test.ts +++ b/tests/core/builders/route/handler.test.ts @@ -27,6 +27,7 @@ describe("route builder handler method", () => { method: "GET", paths: ["/test"], preflightSteps: [], + bodyController: null, metadata: [IgnoreByRouteStoreMetadata()], steps: [ expect.objectContaining({ @@ -57,6 +58,7 @@ describe("route builder handler method", () => { readonly method: "GET"; readonly paths: readonly ["/test"]; readonly preflightSteps: readonly []; + readonly bodyController: null; readonly hooks: readonly []; readonly steps: readonly [ ExtractStep<{ @@ -113,6 +115,7 @@ describe("route builder handler method", () => { method: "GET", paths: ["/test"], preflightSteps: [], + bodyController: null, metadata: [], steps: [ { @@ -145,6 +148,7 @@ describe("route builder handler method", () => { readonly paths: readonly ["/test"]; readonly preflightSteps: readonly []; readonly hooks: readonly []; + readonly bodyController: null; readonly steps: readonly [ HandlerStep<{ readonly responseContract: [ @@ -210,6 +214,7 @@ describe("route builder handler method", () => { method: "GET", paths: ["/test"], preflightSteps: [], + bodyController: null, metadata: [], steps: [ { @@ -235,6 +240,7 @@ describe("route builder handler method", () => { readonly method: "GET"; readonly paths: readonly ["/test"]; readonly preflightSteps: readonly []; + readonly bodyController: null; readonly hooks: readonly [ { // eslint-disable-next-line @typescript-eslint/method-signature-style diff --git a/tests/core/builders/route/presetChecker.test.ts b/tests/core/builders/route/presetChecker.test.ts index a3e6714..7dcf359 100644 --- a/tests/core/builders/route/presetChecker.test.ts +++ b/tests/core/builders/route/presetChecker.test.ts @@ -40,6 +40,7 @@ describe("route builder preset checker method", () => { paths: ["/test"], preflightSteps: [], metadata: [], + bodyController: null, steps: [ expect.objectContaining({ [extractStepKind.runTimeKey]: null, @@ -85,6 +86,7 @@ describe("route builder preset checker method", () => { }>, ]; readonly metadata: readonly []; + readonly bodyController: null; }, { body: string }, Request @@ -123,6 +125,7 @@ describe("route builder preset checker method", () => { paths: ["/test"], preflightSteps: [], metadata: [], + bodyController: null, steps: [ expect.objectContaining({ [extractStepKind.runTimeKey]: null, @@ -168,6 +171,7 @@ describe("route builder preset checker method", () => { }>, ]; readonly metadata: readonly []; + readonly bodyController: null; }, { body: string; diff --git a/tests/core/builders/route/process.test.ts b/tests/core/builders/route/process.test.ts index 3df94c1..656b9e9 100644 --- a/tests/core/builders/route/process.test.ts +++ b/tests/core/builders/route/process.test.ts @@ -16,6 +16,7 @@ describe("route builder process method", () => { method: "GET", paths: ["/test"], preflightSteps: [], + bodyController: null, steps: [ { [processStepKind.runTimeKey]: null, @@ -39,6 +40,7 @@ describe("route builder process method", () => { readonly paths: readonly ["/test"]; readonly method: "GET"; readonly preflightSteps: readonly []; + readonly bodyController: null; readonly steps: readonly [ ProcessStep<{ readonly process: typeof process; @@ -73,6 +75,7 @@ describe("route builder process method", () => { hooks: [], method: "GET", paths: ["/test"], + bodyController: null, preflightSteps: [], steps: [ { @@ -98,6 +101,7 @@ describe("route builder process method", () => { readonly paths: readonly ["/test"]; readonly method: "GET"; readonly preflightSteps: readonly []; + readonly bodyController: null; readonly steps: readonly [ ProcessStep<{ readonly process: typeof process; @@ -150,6 +154,7 @@ describe("route builder process method", () => { hooks: [], method: "GET", paths: ["/test"], + bodyController: null, preflightSteps: [], steps: [ expect.objectContaining({ @@ -178,6 +183,7 @@ describe("route builder process method", () => { readonly paths: readonly ["/test"]; readonly method: "GET"; readonly preflightSteps: readonly []; + readonly bodyController: null; readonly steps: readonly [ ExtractStep<{ readonly shape: { @@ -224,6 +230,7 @@ describe("route builder process method", () => { hooks: [], method: "GET", paths: ["/test"], + bodyController: null, preflightSteps: [], steps: [ { @@ -249,6 +256,7 @@ describe("route builder process method", () => { readonly paths: readonly ["/test"]; readonly method: "GET"; readonly preflightSteps: readonly []; + readonly bodyController: null; readonly steps: readonly [ ProcessStep<{ readonly process: typeof process; @@ -283,6 +291,7 @@ describe("route builder process method", () => { hooks: [], method: "GET", paths: ["/test"], + bodyController: null, preflightSteps: [], steps: [ { @@ -308,6 +317,7 @@ describe("route builder process method", () => { readonly paths: readonly ["/test"]; readonly method: "GET"; readonly preflightSteps: readonly []; + readonly bodyController: null; readonly steps: readonly [ ProcessStep<{ readonly process: typeof process; @@ -343,6 +353,7 @@ describe("route builder process method", () => { hooks: [], method: "GET", paths: ["/test"], + bodyController: null, preflightSteps: [], steps: [ { @@ -365,6 +376,7 @@ describe("route builder process method", () => { { readonly hooks: readonly []; readonly paths: readonly ["/test"]; + readonly bodyController: null; readonly method: "GET"; readonly preflightSteps: readonly []; readonly steps: readonly [ diff --git a/tests/core/builders/route/store.test.ts b/tests/core/builders/route/store.test.ts index 14a96a5..a11ca92 100644 --- a/tests/core/builders/route/store.test.ts +++ b/tests/core/builders/route/store.test.ts @@ -9,6 +9,7 @@ describe("route store", () => { preflightSteps: [], steps: [], metadata: [], + bodyController: null, }); const route2 = createRoute({ hooks: [], @@ -17,6 +18,7 @@ describe("route store", () => { preflightSteps: [], steps: [], metadata: [], + bodyController: null, }); it("adds routes and returns them in the store", () => { diff --git a/tests/core/defaultHooks/index.test.ts b/tests/core/defaultHooks/index.test.ts new file mode 100644 index 0000000..1cd5e79 --- /dev/null +++ b/tests/core/defaultHooks/index.test.ts @@ -0,0 +1,198 @@ +import { createHub, HookResponse, type HttpServerParams, initDefaultHook, PredictedResponse, Response } from "@core"; +import { SF } from "@duplojs/server-utils"; + +describe("defaultHook", () => { + const defaultHook = initDefaultHook( + createHub({ environment: "DEV" }), + { + fromHookHeaderKey: "from-hook", + informationHeaderKey: "information", + predictedHeaderKey: "predicted", + } as HttpServerParams, + ); + + it("beforeSendResponse set information et predicted headers", () => { + const response = new PredictedResponse("100", "superInfo", undefined); + defaultHook.beforeSendResponse({ + currentResponse: response, + next: () => ({}) as any, + request: {} as any, + exit: {} as any, + }); + expect(response.headers).toStrictEqual({ + information: "superInfo", + predicted: "1", + }); + }); + + it("beforeSendResponse send string", () => { + const response = new PredictedResponse("100", "superInfo", "superBody"); + defaultHook.beforeSendResponse({ + currentResponse: response, + next: () => ({}) as any, + request: {} as any, + exit: {} as any, + }); + expect(response.headers).toStrictEqual({ + information: "superInfo", + predicted: "1", + "content-type": "text/plain; charset=utf-8", + }); + }); + + it("beforeSendResponse send error", () => { + const response = new PredictedResponse("100", "superInfo", new Error("super error")); + defaultHook.beforeSendResponse({ + currentResponse: response, + next: () => ({}) as any, + request: {} as any, + exit: {} as any, + }); + expect(response.headers).toStrictEqual({ + information: "superInfo", + predicted: "1", + "content-type": "text/plain; charset=utf-8", + }); + }); + + it("beforeSendResponse send html file", () => { + const response = new PredictedResponse("100", "superInfo", SF.createFileInterface("test.html")); + + defaultHook.beforeSendResponse({ + currentResponse: response, + next: () => ({}) as any, + request: {} as any, + exit: {} as any, + }); + + expect(response.headers).toStrictEqual({ + information: "superInfo", + predicted: "1", + "content-disposition": "attachment; filename=\"test.html\"", + "content-type": "text/html", + }); + }); + + it("beforeSendResponse send unknown file", () => { + const response = new PredictedResponse("100", "superInfo", SF.createFileInterface("test")); + + defaultHook.beforeSendResponse({ + currentResponse: response, + next: () => ({}) as any, + request: {} as any, + exit: {} as any, + }); + + expect(response.headers).toStrictEqual({ + information: "superInfo", + predicted: "1", + "content-disposition": "attachment; filename=\"test\"", + "content-type": "application/octet-stream", + }); + }); + + it("beforeSendResponse send file with unknown name", () => { + const response = new PredictedResponse("100", "superInfo", SF.createFileInterface("test/")); + + defaultHook.beforeSendResponse({ + currentResponse: response, + next: () => ({}) as any, + request: {} as any, + exit: {} as any, + }); + + expect(response.headers).toStrictEqual({ + information: "superInfo", + predicted: "1", + "content-disposition": "attachment;", + "content-type": "application/octet-stream", + }); + }); + + it("beforeSendResponse expect application/json content-type", () => { + const responseNull = new PredictedResponse("100", "superInfo", null); + defaultHook.beforeSendResponse({ + currentResponse: responseNull, + next: () => ({}) as any, + request: {} as any, + exit: {} as any, + }); + expect(responseNull.headers).toStrictEqual({ + information: "superInfo", + predicted: "1", + "content-type": "application/json; charset=utf-8", + }); + + const responseObject = new PredictedResponse("100", "superInfo", {}); + defaultHook.beforeSendResponse({ + currentResponse: responseObject, + next: () => ({}) as any, + request: {} as any, + exit: {} as any, + }); + expect(responseObject.headers).toStrictEqual({ + information: "superInfo", + predicted: "1", + "content-type": "application/json; charset=utf-8", + }); + + const responseBoolean = new PredictedResponse("100", "superInfo", true); + defaultHook.beforeSendResponse({ + currentResponse: responseBoolean, + next: () => ({}) as any, + request: {} as any, + exit: {} as any, + }); + expect(responseBoolean.headers).toStrictEqual({ + information: "superInfo", + predicted: "1", + "content-type": "application/json; charset=utf-8", + }); + + const responseNumber = new PredictedResponse("100", "superInfo", 10); + defaultHook.beforeSendResponse({ + currentResponse: responseNumber, + next: () => ({}) as any, + request: {} as any, + exit: {} as any, + }); + expect(responseNumber.headers).toStrictEqual({ + information: "superInfo", + predicted: "1", + "content-type": "application/json; charset=utf-8", + }); + }); + + it("beforeSendResponse send HookResponse", () => { + const response = new HookResponse("afterSendResponse", "100", "superInfo", null); + + defaultHook.beforeSendResponse({ + currentResponse: response, + next: () => ({}) as any, + request: {} as any, + exit: {} as any, + }); + + expect(response.headers).toStrictEqual({ + information: "superInfo", + "content-type": "application/json; charset=utf-8", + "from-hook": "afterSendResponse", + }); + }); + + it("beforeSendResponse not redefine content-type", () => { + const response = new Response("100", "superInfo", null).setHeader("content-type", "test"); + + defaultHook.beforeSendResponse({ + currentResponse: response, + next: () => ({}) as any, + request: {} as any, + exit: {} as any, + }); + + expect(response.headers).toStrictEqual({ + information: "superInfo", + "content-type": "test", + }); + }); +}); diff --git a/tests/core/errors/bodyParseWrongChunkReceived.test.ts b/tests/core/errors/bodyParseWrongChunkReceived.test.ts new file mode 100644 index 0000000..5262525 --- /dev/null +++ b/tests/core/errors/bodyParseWrongChunkReceived.test.ts @@ -0,0 +1,5 @@ +import { BodyParseWrongChunkReceived } from "@core"; + +it("BodyParseWrongChunkReceived", () => { + expect(new BodyParseWrongChunkReceived("test", 1212)).instanceOf(Error); +}); diff --git a/tests/core/errors/bodySizeExceedsLimitError.test.ts b/tests/core/errors/bodySizeExceedsLimitError.test.ts new file mode 100644 index 0000000..79b7a5e --- /dev/null +++ b/tests/core/errors/bodySizeExceedsLimitError.test.ts @@ -0,0 +1,5 @@ +import { BodySizeExceedsLimitError } from "@core"; + +it("BodySizeExceedsLimitError", () => { + expect(new BodySizeExceedsLimitError(1000)).instanceOf(Error); +}); diff --git a/tests/core/errors/parseJsonError.test.ts b/tests/core/errors/parseJsonError.test.ts new file mode 100644 index 0000000..fb257f2 --- /dev/null +++ b/tests/core/errors/parseJsonError.test.ts @@ -0,0 +1,5 @@ +import { ParseJsonError } from "@core"; + +it("ParseJsonError", () => { + expect(new ParseJsonError("", "")).instanceOf(Error); +}); diff --git a/tests/core/errors/wrongContentTypeError.test.ts b/tests/core/errors/wrongContentTypeError.test.ts new file mode 100644 index 0000000..048d7c8 --- /dev/null +++ b/tests/core/errors/wrongContentTypeError.test.ts @@ -0,0 +1,5 @@ +import { WrongContentTypeError } from "@core"; + +it("WrongContentTypeError", () => { + expect(new WrongContentTypeError("", "")).instanceOf(Error); +}); diff --git a/tests/core/functionsBuilders/route/create.test.ts b/tests/core/functionsBuilders/route/create.test.ts index 100d9e5..930bcf2 100644 --- a/tests/core/functionsBuilders/route/create.test.ts +++ b/tests/core/functionsBuilders/route/create.test.ts @@ -47,6 +47,7 @@ describe("createFunctionBuilder", () => { preflightSteps: [], steps: [], metadata: [], + bodyController: null, }), { success: (element) => E.right("buildSuccess", element), diff --git a/tests/core/functionsBuilders/route/default.test.ts b/tests/core/functionsBuilders/route/default.test.ts index 1568f50..13cd1eb 100644 --- a/tests/core/functionsBuilders/route/default.test.ts +++ b/tests/core/functionsBuilders/route/default.test.ts @@ -1,5 +1,6 @@ import { ResponseContract, useRouteBuilder, Request, Response, usePreflightBuilder, useProcessBuilder, defaultExtractStepFunctionBuilder, defaultHandlerStepFunctionBuilder, HookResponse, type HookRouteLifeCycle, PredictedResponse } from "@core"; import { DPE } from "@duplojs/utils"; +import { createBodyReader } from "@test-utils/bodyReader"; import { useTestRouteFunctionBuilder } from "@test-utils/useTestRouteFunctionBuilder"; describe("route function builder", () => { @@ -68,6 +69,7 @@ describe("route function builder", () => { params: { value: "test" }, query: {}, url: "", + bodyReader: createBodyReader(), }), ); @@ -108,6 +110,7 @@ describe("route function builder", () => { params: { value: "test" }, query: {}, url: "", + bodyReader: createBodyReader(), }), ); @@ -122,8 +125,8 @@ describe("route function builder", () => { const route = useRouteBuilder("GET", "/test", { hooks: [{ afterSendResponse: spyResponse }] }) .extract({ params: { value: DPE.string() } }) .handler( - ResponseContract.ok("good", DPE.string()), - (floor, { response }) => ({}) as never, + ResponseContract.ok("good", DPE.empty()), + (floor, { response }) => ({ information: "good" }) as never, ); const buildedRoute = await useTestRouteFunctionBuilder(route); @@ -139,6 +142,7 @@ describe("route function builder", () => { params: { value: "test" }, query: {}, url: "", + bodyReader: createBodyReader(), }), ); @@ -179,6 +183,7 @@ describe("route function builder", () => { params: { }, query: {}, url: "", + bodyReader: createBodyReader(), }), ); @@ -216,6 +221,7 @@ describe("route function builder", () => { params: { }, query: {}, url: "", + bodyReader: createBodyReader(), }), ); @@ -226,43 +232,6 @@ describe("route function builder", () => { ); }); - it("parseBody", async() => { - const route = useRouteBuilder("GET", "/test", { - hooks: [ - { - afterSendResponse: spyResponse, - parseBody: ({ response }) => response("400", "info"), - }, - ], - }) - .handler( - ResponseContract.noContent("good"), - (floor, { response }) => response("good"), - ); - - const buildedRoute = await useTestRouteFunctionBuilder(route); - - await buildedRoute( - new Request({ - headers: {}, - host: "", - matchedPath: "", - method: "", - origin: "", - path: "", - params: { }, - query: {}, - url: "", - }), - ); - - expect(spyResponse).toHaveBeenCalledWith( - expect.objectContaining({ - currentResponse: new HookResponse("parseBody", "400", "info", undefined), - }), - ); - }); - it("error", async() => { const route = useRouteBuilder("GET", "/test", { hooks: [ @@ -292,6 +261,7 @@ describe("route function builder", () => { params: { }, query: {}, url: "", + bodyReader: createBodyReader(), }), ); @@ -306,7 +276,7 @@ describe("route function builder", () => { const route = useRouteBuilder("GET", "/test", { hooks: [ { - parseBody: ({ next }) => next(), + beforeRouteExecution: ({ next }) => next(), afterSendResponse: spyResponse, sendResponse: ({ exit }) => exit(), error: ({ next }) => next(), @@ -333,6 +303,7 @@ describe("route function builder", () => { params: { }, query: {}, url: "", + bodyReader: createBodyReader(), }), ); @@ -368,10 +339,6 @@ describe("route function builder", () => { checkpoint.push(`onConstructRequest ${value}`); return request; }, - parseBody: ({ next }) => { - checkpoint.push(`parseBody ${value}`); - return next(); - }, sendResponse: ({ next }) => { checkpoint.push(`sendResponse ${value}`); return next(); @@ -417,6 +384,7 @@ describe("route function builder", () => { params: { }, query: {}, url: "", + bodyReader: createBodyReader(), }), ); @@ -437,14 +405,6 @@ describe("route function builder", () => { "beforeRouteExecution deep process", "beforeRouteExecution global", - "parseBody route", - "parseBody builder preflight process", - "parseBody preflight process", - "parseBody preflight deep process", - "parseBody process", - "parseBody deep process", - "parseBody global", - "beforeSendResponse route", "beforeSendResponse builder preflight process", "beforeSendResponse preflight process", diff --git a/tests/core/functionsBuilders/steps/defaults/checkerStep.test.ts b/tests/core/functionsBuilders/steps/defaults/checkerStep.test.ts index ff988d2..8acd9c8 100644 --- a/tests/core/functionsBuilders/steps/defaults/checkerStep.test.ts +++ b/tests/core/functionsBuilders/steps/defaults/checkerStep.test.ts @@ -1,5 +1,6 @@ import { ResponseContract, useCheckerBuilder, useRouteBuilder, Request, Response, createPresetChecker, PredictedResponse } from "@core"; import { DPE } from "@duplojs/utils"; +import { createBodyReader } from "@test-utils/bodyReader"; import { useTestRouteFunctionBuilder } from "@test-utils/useTestRouteFunctionBuilder"; describe("checker step function builder", () => { @@ -45,6 +46,7 @@ describe("checker step function builder", () => { params: { value: "" }, query: {}, url: "", + bodyReader: createBodyReader(), }), ); @@ -92,6 +94,7 @@ describe("checker step function builder", () => { params: { value: "test" }, query: {}, url: "", + bodyReader: createBodyReader(), }), ); @@ -139,6 +142,7 @@ describe("checker step function builder", () => { params: { value: "test" }, query: {}, url: "", + bodyReader: createBodyReader(), }), ); @@ -189,6 +193,7 @@ describe("checker step function builder", () => { params: { value: "test" }, query: {}, url: "", + bodyReader: createBodyReader(), }), ); @@ -237,6 +242,7 @@ describe("checker step function builder", () => { params: { value: "test" }, query: {}, url: "", + bodyReader: createBodyReader(), }), ); @@ -285,6 +291,7 @@ describe("checker step function builder", () => { params: { value: "test" }, query: {}, url: "", + bodyReader: createBodyReader(), }), ); @@ -335,6 +342,7 @@ describe("checker step function builder", () => { params: { value: "" }, query: {}, url: "", + bodyReader: createBodyReader(), }), ); diff --git a/tests/core/functionsBuilders/steps/defaults/cutStep.test.ts b/tests/core/functionsBuilders/steps/defaults/cutStep.test.ts index 01c3605..06f41a2 100644 --- a/tests/core/functionsBuilders/steps/defaults/cutStep.test.ts +++ b/tests/core/functionsBuilders/steps/defaults/cutStep.test.ts @@ -1,5 +1,6 @@ import { ResponseContract, useRouteBuilder, Request, Response, PredictedResponse } from "@core"; import { DP, DPE } from "@duplojs/utils"; +import { createBodyReader } from "@test-utils/bodyReader"; import { useTestRouteFunctionBuilder } from "@test-utils/useTestRouteFunctionBuilder"; describe("cut step function builder", () => { @@ -13,7 +14,7 @@ describe("cut step function builder", () => { const route = useRouteBuilder("GET", "/test", { hooks: [{ afterSendResponse: spyResponse }] }) .extract({ params: { value: DPE.string() } }) .cut( - [ResponseContract.ok("goodCut", DPE.string())], + [ResponseContract.ok("goodCut", DPE.string().transform(async(value) => Promise.resolve(value)))], (floor, { response }) => response("goodCut", floor.value), ) .handler( @@ -34,6 +35,8 @@ describe("cut step function builder", () => { params: { value: "test" }, query: {}, url: "", + bodyReader: createBodyReader(), + }), ); @@ -68,6 +71,8 @@ describe("cut step function builder", () => { params: {}, query: {}, url: "", + bodyReader: createBodyReader(), + }), ); @@ -102,6 +107,8 @@ describe("cut step function builder", () => { params: {}, query: {}, url: "", + bodyReader: createBodyReader(), + }), ); @@ -140,6 +147,8 @@ describe("cut step function builder", () => { params: {}, query: {}, url: "", + bodyReader: createBodyReader(), + }), ); @@ -171,6 +180,8 @@ describe("cut step function builder", () => { params: {}, query: {}, url: "", + bodyReader: createBodyReader(), + }), ); diff --git a/tests/core/functionsBuilders/steps/defaults/extractStep.test.ts b/tests/core/functionsBuilders/steps/defaults/extractStep.test.ts index c33f945..bac814b 100644 --- a/tests/core/functionsBuilders/steps/defaults/extractStep.test.ts +++ b/tests/core/functionsBuilders/steps/defaults/extractStep.test.ts @@ -1,5 +1,6 @@ -import { ResponseContract, useRouteBuilder, Request, Response, PredictedResponse } from "@core"; +import { ResponseContract, useRouteBuilder, Request, PredictedResponse } from "@core"; import { DP, DPE } from "@duplojs/utils"; +import { createBodyReader } from "@test-utils/bodyReader"; import { useTestRouteFunctionBuilder } from "@test-utils/useTestRouteFunctionBuilder"; describe("extract step function builder", () => { @@ -30,6 +31,7 @@ describe("extract step function builder", () => { params: { value: "test" }, query: {}, url: "", + bodyReader: createBodyReader(), }), ); @@ -61,6 +63,7 @@ describe("extract step function builder", () => { params: {}, query: {}, url: "", + bodyReader: createBodyReader(), }), ); @@ -92,6 +95,7 @@ describe("extract step function builder", () => { params: {}, query: {}, url: "", + bodyReader: createBodyReader(), }), ); @@ -128,6 +132,7 @@ describe("extract step function builder", () => { params: {}, query: {}, url: "", + bodyReader: createBodyReader(), }), ); @@ -164,6 +169,7 @@ describe("extract step function builder", () => { params: {}, query: {}, url: "", + bodyReader: createBodyReader(), }), ); @@ -200,6 +206,7 @@ describe("extract step function builder", () => { params: {}, query: {}, url: "", + bodyReader: createBodyReader(), }), ); @@ -214,4 +221,145 @@ describe("extract step function builder", () => { }), ); }); + + it("extract body", async() => { + const route = useRouteBuilder("GET", "/test", { hooks: [{ afterSendResponse: spyResponse }] }) + .extract({ body: DPE.number() }) + .handler( + ResponseContract.ok("good", DPE.number()), + (floor, { response }) => response("good", floor.body), + ); + + const buildedRoute = await useTestRouteFunctionBuilder(route, { environment: "PROD" }); + + await buildedRoute( + new Request({ + headers: {}, + host: "", + matchedPath: "", + method: "", + origin: "test1", + path: "", + params: {}, + query: {}, + url: "", + bodyReader: createBodyReader(() => 12), + }), + ); + + expect(spyResponse).toHaveBeenCalledWith( + expect.objectContaining({ + currentResponse: new PredictedResponse( + "200", + "good", + 12, + ), + }), + ); + }); + + it("extract body whit async schema", async() => { + const route = useRouteBuilder("GET", "/test", { hooks: [{ afterSendResponse: spyResponse }] }) + .extract({ body: DPE.number().transform(async(value) => Promise.resolve(value + 1)) }) + .handler( + ResponseContract.ok("good", DPE.number()), + (floor, { response }) => response("good", floor.body), + ); + + const buildedRoute = await useTestRouteFunctionBuilder(route, { environment: "DEV" }); + + await buildedRoute( + new Request({ + headers: {}, + host: "", + matchedPath: "", + method: "", + origin: "test1", + path: "", + params: {}, + query: {}, + url: "", + bodyReader: createBodyReader(() => 1), + }), + ); + + expect(spyResponse).toHaveBeenCalledWith( + expect.objectContaining({ + currentResponse: new PredictedResponse( + "200", + "good", + 2, + ), + }), + ); + }); + + it("fail extract body", async() => { + const route = useRouteBuilder("GET", "/test", { hooks: [{ afterSendResponse: spyResponse }] }) + .extract({ body: DPE.number() }) + .handler( + ResponseContract.ok("good", DPE.number()), + (floor, { response }) => response("good", floor.body), + ); + + const buildedRoute = await useTestRouteFunctionBuilder(route, { environment: "DEV" }); + + await buildedRoute( + new Request({ + headers: {}, + host: "", + matchedPath: "", + method: "", + origin: "test1", + path: "", + params: {}, + query: {}, + url: "", + bodyReader: createBodyReader(() => new Error("fail")), + }), + ); + + expect(spyResponse).toHaveBeenCalledWith( + expect.objectContaining({ + currentResponse: new PredictedResponse( + "422", + "extract-error", + new Error("fail"), + ) + .setHeader("extract-key", "request.body"), + }), + ); + }); + + it("extract with async dataParser", async() => { + const route = useRouteBuilder("GET", "/test", { hooks: [{ afterSendResponse: spyResponse }] }) + .extract({ origin: DPE.string().transform(async(value) => Promise.resolve(`${value}tt`)) }) + .handler( + ResponseContract.ok("good", DPE.string()), + (floor, { response }) => response("good", floor.origin), + ); + + const buildedRoute = await useTestRouteFunctionBuilder(route); + + await buildedRoute( + new Request({ + headers: {}, + host: "", + matchedPath: "", + method: "", + origin: "test1", + path: "", + params: {}, + query: {}, + url: "", + bodyReader: createBodyReader(), + }), + ); + + expect(spyResponse).toHaveBeenCalledWith( + expect.objectContaining({ + currentResponse: new PredictedResponse("200", "good", "test1tt"), + }), + ); + }); }); diff --git a/tests/core/functionsBuilders/steps/defaults/handlerStep.test.ts b/tests/core/functionsBuilders/steps/defaults/handlerStep.test.ts index 8d07148..2afe3c1 100644 --- a/tests/core/functionsBuilders/steps/defaults/handlerStep.test.ts +++ b/tests/core/functionsBuilders/steps/defaults/handlerStep.test.ts @@ -1,6 +1,7 @@ import { ResponseContract, useRouteBuilder, Request, Response, PredictedResponse } from "@core"; import { DP, DPE } from "@duplojs/utils"; import { useTestRouteFunctionBuilder } from "@test-utils/useTestRouteFunctionBuilder"; +import { createBodyReader } from "@test-utils/bodyReader"; describe("handler step function builder", () => { const spyResponse = vi.fn(); @@ -13,7 +14,7 @@ describe("handler step function builder", () => { const route = useRouteBuilder("GET", "/test", { hooks: [{ afterSendResponse: spyResponse }] }) .extract({ params: { value: DPE.string() } }) .handler( - ResponseContract.ok("good", DPE.string()), + ResponseContract.ok("good", DPE.string().transform(async(value) => Promise.resolve(value))), (floor, { response }) => response("good", floor.value), ); @@ -30,6 +31,7 @@ describe("handler step function builder", () => { params: { value: "test" }, query: {}, url: "", + bodyReader: createBodyReader(), }), ); @@ -60,6 +62,7 @@ describe("handler step function builder", () => { params: {}, query: {}, url: "", + bodyReader: createBodyReader(), }), ); @@ -90,6 +93,7 @@ describe("handler step function builder", () => { params: {}, query: {}, url: "", + bodyReader: createBodyReader(), }), ); diff --git a/tests/core/functionsBuilders/steps/defaults/processStep.test.ts b/tests/core/functionsBuilders/steps/defaults/processStep.test.ts index f25a849..07633a0 100644 --- a/tests/core/functionsBuilders/steps/defaults/processStep.test.ts +++ b/tests/core/functionsBuilders/steps/defaults/processStep.test.ts @@ -1,5 +1,6 @@ import { ResponseContract, useProcessBuilder, useRouteBuilder, Request, defaultHandlerStepFunctionBuilder, defaultProcessStepFunctionBuilder, PredictedResponse } from "@core"; import { DPE } from "@duplojs/utils"; +import { createBodyReader } from "@test-utils/bodyReader"; import { useTestRouteFunctionBuilder } from "@test-utils/useTestRouteFunctionBuilder"; describe("process function builder", () => { @@ -56,6 +57,8 @@ describe("process function builder", () => { params: {}, query: {}, url: "", + bodyReader: createBodyReader(), + }), ); @@ -92,6 +95,8 @@ describe("process function builder", () => { params: {}, query: {}, url: "", + bodyReader: createBodyReader(), + }), ); @@ -127,6 +132,8 @@ describe("process function builder", () => { params: {}, query: {}, url: "", + bodyReader: createBodyReader(), + }), ); diff --git a/tests/core/hub/hooks.test.ts b/tests/core/hub/hooks.test.ts index 2ea226e..537ca6b 100644 --- a/tests/core/hub/hooks.test.ts +++ b/tests/core/hub/hooks.test.ts @@ -27,7 +27,7 @@ describe("hub hooks", () => { await launchHookServer( [fakeHook, fakeHook2], hub, - {}, + {} as any, ); expect(fakeHook).toBeCalledWith(hub, {}); diff --git a/tests/core/hub/index.test.ts b/tests/core/hub/index.test.ts index ac222c0..86288de 100644 --- a/tests/core/hub/index.test.ts +++ b/tests/core/hub/index.test.ts @@ -1,102 +1,150 @@ -import { createHub, defaultCheckerStepFunctionBuilder, defaultExtractContract, defaultNotfoundHandler, defaultRouteFunctionBuilder, hubKind, Request, ResponseContract } from "@core"; -import { type HookHubLifeCycle } from "@core/hub"; -import { type HookRouteLifeCycle } from "@core/route"; -import { type ExpectType } from "@duplojs/utils"; +import { type HookRouteLifeCycle, defaultBodyController, createHub, defaultCheckerStepFunctionBuilder, defaultExtractContract, defaultNotfoundHandler, defaultRouteFunctionBuilder, hubKind, Request, ResponseContract, type HookHubLifeCycle, TextBodyController, controlBodyAsText } from "@core"; import { testRoute } from "@test-utils/route"; describe("hub", () => { - const hub = createHub({ - environment: "DEV", - }); - const baseHub = { [hubKind.runTimeKey]: null, - addHubHooks: expect.any(Function), - addRouteFunctionBuilder: expect.any(Function), - addRouteHooks: expect.any(Function), - addStepFunctionBuilder: expect.any(Function), - aggregates: expect.any(Function), - aggregatesHooksHubLifeCycle: expect.any(Function), - aggregatesHooksRouteLifeCycle: expect.any(Function), - aggregatesRouteFunctionBuilders: expect.any(Function), - aggregatesRoutes: expect.any(Function), - aggregatesStepFunctionBuilders: expect.any(Function), - register: expect.any(Function), - plug: expect.any(Function), - setDefaultExtractContract: expect.any(Function), - setNotfoundHandler: expect.any(Function), classRequest: Request, config: { environment: "DEV" }, defaultExtractContract, hooksHubLifeCycle: [], hooksRouteLifeCycle: [], notfoundHandler: defaultNotfoundHandler, + defaultBodyController: defaultBodyController, plugins: [], routeFunctionBuilders: [], - routes: [], + routes: new Set(), stepFunctionBuilders: [], + bodyReaderImplementations: [], }; it("hub shape", () => { - expect(hub).toStrictEqual(baseHub); + const hub = createHub({ + environment: "DEV", + }); + + expect({ ...hub }).toStrictEqual(baseHub); }); it("hub register", () => { - const newHub = hub.register(testRoute); + const hub = createHub({ + environment: "DEV", + }) + .register(testRoute); - expect(newHub).toStrictEqual({ + expect({ ...hub }).toStrictEqual({ ...baseHub, - routes: [testRoute], + routes: new Set([testRoute]), }); - expect(hub.register([testRoute])).toStrictEqual({ + const otherRoute = { ...testRoute }; + hub.register([otherRoute]); + + expect({ ...hub }).toStrictEqual({ ...baseHub, - routes: [testRoute], + routes: new Set([testRoute, otherRoute]), }); - expect(hub.register({ testRoute })).toStrictEqual({ + const otherOtherRoute = { ...testRoute }; + hub.register({ otherOtherRoute }); + + expect({ ...hub }).toStrictEqual({ ...baseHub, - routes: [testRoute], + routes: new Set([testRoute, otherRoute, otherOtherRoute]), }); }); it("hub plug", () => { - const newHub = hub.plug({ name: "test" }); + const hub = createHub({ + environment: "DEV", + }) + .plug({ name: "test" }); - expect(newHub).toStrictEqual({ + expect({ ...hub }).toStrictEqual({ ...baseHub, plugins: [{ name: "test" }], }); - const newHub1 = hub.plug((hub) => ({ + hub.plug((hub) => ({ name: "test", hub, })); - expect(newHub1).toStrictEqual({ + expect({ ...hub }).toStrictEqual({ ...baseHub, plugins: [ + { name: "test" }, { name: "test", hub, }, ], }); + + const bodyReaderImplementation = TextBodyController.createReaderImplementation(() => void 0 as never); + + hub.plug({ + name: "1", + bodyReaderImplementations: [bodyReaderImplementation], + }); + + expect(hub.bodyReaderImplementations).toStrictEqual([bodyReaderImplementation]); + + hub.plug({ + name: "1", + hooksHubLifeCycle: [{}], + }); + + expect(hub.hooksHubLifeCycle).toStrictEqual([{}]); + + hub.plug({ + name: "1", + hooksRouteLifeCycle: [{}], + }); + + expect(hub.hooksRouteLifeCycle).toStrictEqual([{}]); + + hub.plug({ + name: "1", + routeFunctionBuilders: [defaultRouteFunctionBuilder], + }); + + expect(hub.routeFunctionBuilders).toStrictEqual([defaultRouteFunctionBuilder]); + + hub.plug({ + name: "1", + routes: [testRoute], + }); + + expect(hub.routes).toStrictEqual(new Set([testRoute])); + + hub.plug({ + name: "1", + stepFunctionBuilders: [defaultCheckerStepFunctionBuilder], + }); + + expect(hub.stepFunctionBuilders).toStrictEqual([defaultCheckerStepFunctionBuilder]); }); it("hub add route function builder", () => { - const newHub = hub.addRouteFunctionBuilder(defaultRouteFunctionBuilder); + const hub = createHub({ + environment: "DEV", + }) + .addRouteFunctionBuilder(defaultRouteFunctionBuilder); - expect(newHub).toStrictEqual({ + expect({ ...hub }).toStrictEqual({ ...baseHub, routeFunctionBuilders: [defaultRouteFunctionBuilder], }); }); it("hub add step function builder", () => { - const newHub = hub.addStepFunctionBuilder(defaultCheckerStepFunctionBuilder); + const hub = createHub({ + environment: "DEV", + }) + .addStepFunctionBuilder(defaultCheckerStepFunctionBuilder); - expect(newHub).toStrictEqual({ + expect({ ...hub }).toStrictEqual({ ...baseHub, stepFunctionBuilders: [defaultCheckerStepFunctionBuilder], }); @@ -104,25 +152,25 @@ describe("hub", () => { it("hub add route hooks", () => { const routeHook: HookRouteLifeCycle = {}; - const newHub = hub.addRouteHooks(routeHook); + const hub = createHub({ + environment: "DEV", + }) + .addRouteHooks(routeHook); - expect(newHub).toStrictEqual({ + expect({ ...hub }).toStrictEqual({ ...baseHub, hooksRouteLifeCycle: [routeHook], }); - - type Check = ExpectType< - typeof newHub, - typeof hub, - "strict" - >; }); it("hub add hub hooks", () => { const hubHook: HookHubLifeCycle = {}; - const newHub = hub.addHubHooks(hubHook); + const hub = createHub({ + environment: "DEV", + }) + .addHubHooks(hubHook); - expect(newHub).toStrictEqual({ + expect({ ...hub }).toStrictEqual({ ...baseHub, hooksHubLifeCycle: [hubHook], }); @@ -136,34 +184,18 @@ describe("hub", () => { beforeStartServer: (hub) => hub, }; - const aggregatedHub = hub + const aggregatedHub = createHub({ + environment: "DEV", + }) .addRouteHooks([routeHook, {}]) .addHubHooks([hubHook, {}]) - .addRouteFunctionBuilder(defaultRouteFunctionBuilder) - .addStepFunctionBuilder(defaultCheckerStepFunctionBuilder) - .register(testRoute) .plug({ name: "test", hooksRouteLifeCycle: [routeHook, {}], hooksHubLifeCycle: [hubHook, {}], - routes: [testRoute], - routeFunctionBuilders: [defaultRouteFunctionBuilder], - stepFunctionBuilders: [defaultCheckerStepFunctionBuilder], }) .plug({ name: "empty" }); - expect(aggregatedHub.aggregatesRoutes()).toStrictEqual([ - testRoute, - testRoute, - ]); - expect(aggregatedHub.aggregatesRouteFunctionBuilders()).toStrictEqual([ - defaultRouteFunctionBuilder, - defaultRouteFunctionBuilder, - ]); - expect(aggregatedHub.aggregatesStepFunctionBuilders()).toStrictEqual([ - defaultCheckerStepFunctionBuilder, - defaultCheckerStepFunctionBuilder, - ]); expect(aggregatedHub.aggregatesHooksHubLifeCycle("beforeStartServer")).toStrictEqual([ hubHook.beforeStartServer, hubHook.beforeStartServer, @@ -172,30 +204,20 @@ describe("hub", () => { routeHook.beforeRouteExecution, routeHook.beforeRouteExecution, ]); - expect(aggregatedHub.aggregates()).toStrictEqual({ - hooksRouteLifeCycle: [routeHook, {}, routeHook, {}], - hooksHubLifeCycle: [hubHook, {}, hubHook, {}], - routes: [testRoute, testRoute], - routeFunctionBuilders: [ - defaultRouteFunctionBuilder, - defaultRouteFunctionBuilder, - ], - stepFunctionBuilders: [ - defaultCheckerStepFunctionBuilder, - defaultCheckerStepFunctionBuilder, - ], - }); }); it("hub set not found handler", () => { const contract = ResponseContract.notFound("test"); - const newHub = hub.setNotfoundHandler( - contract, - ({ response }) => response("test"), - ); + const hub = createHub({ + environment: "DEV", + }) + .setNotfoundHandler( + contract, + ({ response }) => response("test"), + ); - expect(newHub).toStrictEqual({ + expect({ ...hub }).toStrictEqual({ ...baseHub, notfoundHandler: expect.objectContaining({ definition: expect.objectContaining({ @@ -208,11 +230,28 @@ describe("hub", () => { it("hub set default extract contract", () => { const contract = ResponseContract.notFound("test"); - const newHub = hub.setDefaultExtractContract(contract); + const hub = createHub({ + environment: "DEV", + }) + .setDefaultExtractContract(contract); - expect(newHub).toStrictEqual({ + expect({ ...hub }).toStrictEqual({ ...baseHub, defaultExtractContract: contract, }); }); + + it("add body reader", () => { + const bodyController = controlBodyAsText(); + + const hub = createHub({ + environment: "DEV", + }) + .setDefaultBodyController(bodyController); + + expect({ ...hub }).toStrictEqual({ + ...baseHub, + defaultBodyController: bodyController, + }); + }); }); diff --git a/tests/core/implementHttpServer.test.ts b/tests/core/implementHttpServer.test.ts index c72841a..64a7bfc 100644 --- a/tests/core/implementHttpServer.test.ts +++ b/tests/core/implementHttpServer.test.ts @@ -1,12 +1,15 @@ -import { ResponseContract, createHub, implementHttpServer, serverErrorExitHookFunction, serverErrorNextHookFunction, useRouteBuilder } from "@core"; +import { type HttpServerParams, ResponseContract, TextBodyController, createHub, implementHttpServer, serverErrorExitHookFunction, serverErrorNextHookFunction, useRouteBuilder } from "@core"; import { type RouterInitializationData } from "@core/router"; -import { type AnyFunction } from "@duplojs/utils"; +import { E, type AnyFunction } from "@duplojs/utils"; describe("implementHttpServer", () => { + const bodyReaderImplementation = TextBodyController.createReaderImplementation( + () => Promise.resolve(E.success(undefined)), + ); it("runs lifecycle hooks in order and executes route", async() => { const calls: string[] = []; const routeHandler = vi.fn(); - const httpServerParams = { marker: "http-server-params" }; + const httpServerParams = {} as HttpServerParams; const route = useRouteBuilder("GET", "/") .handler( @@ -42,7 +45,8 @@ describe("implementHttpServer", () => { beforeServerBuildRoutes: beforeServerBuildRoutesHook, beforeStartServer: beforeStartServerHook, afterStartServer: afterStartServerHook, - }), + }) + .addBodyReaderImplementation(bodyReaderImplementation), httpServerParams, }, async({ execRouteSystem, httpServerParams: receivedParams }) => { @@ -108,8 +112,9 @@ describe("implementHttpServer", () => { .register(route) .addHubHooks({ serverError: serverErrorHook, - }), - httpServerParams: {}, + }) + .addBodyReaderImplementation(bodyReaderImplementation), + httpServerParams: {} as HttpServerParams, }, ({ execRouteSystem: receivedExecRouteSystem }) => { execRouteSystem = receivedExecRouteSystem; diff --git a/tests/core/request/bodyController/base.test.ts b/tests/core/request/bodyController/base.test.ts new file mode 100644 index 0000000..900a51e --- /dev/null +++ b/tests/core/request/bodyController/base.test.ts @@ -0,0 +1,50 @@ +import { type BodyControllerParams, controlBodyAsText, createBodyController } from "@core"; +import { asserts, E, unwrap } from "@duplojs/utils"; + +describe("createBodyController", () => { + interface TestParams extends BodyControllerParams { + test: string; + } + + const BodyController = createBodyController<"test", TestParams>("test"); + + it("check name", () => { + expect(BodyController.name).toStrictEqual("test"); + }); + + it("create bodyController", () => { + const bodyController = BodyController.create({ test: "" }); + expect(bodyController.name).toStrictEqual("test"); + expect(bodyController.params).toStrictEqual({ test: "" }); + }); + + it("create createReaderImplementation", () => { + const spy = vi.fn(); + const bodyReaderImplementation = BodyController.createReaderImplementation(spy); + expect(bodyReaderImplementation.read).toStrictEqual(spy); + }); + + it("bodyController tryToCreateReader", () => { + const bodyController = BodyController.create({ test: "" }); + expect(bodyController.tryToCreateReader({} as never)).toStrictEqual(E.fail()); + const bodyReaderImplementation = BodyController.createReaderImplementation(vi.fn()); + expect(bodyController.tryToCreateReader(bodyReaderImplementation)).toStrictEqual(E.success(expect.any(Object))); + }); + + it("reader", async() => { + const bodyController = BodyController.create({ test: "" }); + const spy = vi.fn(); + const bodyReaderImplementation = BodyController.createReaderImplementation(spy); + const reader = bodyController.tryToCreateReader(bodyReaderImplementation); + asserts(reader, E.isRight); + await unwrap(reader).read({} as never); + expect(spy).toHaveBeenCalledTimes(1); + }); + + it("is", () => { + const bodyController = BodyController.create({ test: "" }); + + expect(BodyController.is(bodyController)).toStrictEqual(true); + expect(BodyController.is(controlBodyAsText())).toStrictEqual(false); + }); +}); diff --git a/tests/core/request/bodyController/formData.test.ts b/tests/core/request/bodyController/formData.test.ts new file mode 100644 index 0000000..47851c5 --- /dev/null +++ b/tests/core/request/bodyController/formData.test.ts @@ -0,0 +1,42 @@ +import { controlBodyAsFormData } from "@core"; + +it("controlBodyAsFormData", () => { + expect( + controlBodyAsFormData({ + maxFileQuantity: 10, + bodyMaxSize: "50b", + fileMaxSize: "50b", + mimeType: "test", + }).params, + ) + .toStrictEqual({ + maxFileQuantity: 10, + bodyMaxSize: 50, + fileMaxSize: 50, + maxBufferSize: 131072, + maxIndexArray: 500, + maxKeyLength: 500, + mimeType: /^test$/, + }); + + expect( + controlBodyAsFormData({ + maxFileQuantity: 10, + bodyMaxSize: "50b", + fileMaxSize: "50b", + mimeType: "test", + maxBufferSize: "10kb", + maxIndexArray: 50, + maxKeyLength: 1, + }).params, + ) + .toStrictEqual({ + maxFileQuantity: 10, + bodyMaxSize: 50, + fileMaxSize: 50, + maxBufferSize: 10240, + maxIndexArray: 50, + maxKeyLength: 1, + mimeType: /^test$/, + }); +}); diff --git a/tests/core/request/bodyController/text.test.ts b/tests/core/request/bodyController/text.test.ts new file mode 100644 index 0000000..3007088 --- /dev/null +++ b/tests/core/request/bodyController/text.test.ts @@ -0,0 +1,6 @@ +import { controlBodyAsText } from "@core"; + +it("controlBodyAsText", () => { + expect(controlBodyAsText({ bodyMaxSize: "10mb" }).params) + .toStrictEqual({ bodyMaxSize: 10485760 }); +}); diff --git a/tests/core/request.test.ts b/tests/core/request/index.test.ts similarity index 50% rename from tests/core/request.test.ts rename to tests/core/request/index.test.ts index c17ae90..2f84533 100644 --- a/tests/core/request.test.ts +++ b/tests/core/request/index.test.ts @@ -1,8 +1,11 @@ import { createCoreLibKind, Request } from "@core"; -import { kindHeritage } from "@duplojs/utils"; +import { E, kindHeritage } from "@duplojs/utils"; +import { createBodyReader } from "@test-utils/bodyReader"; +import { visitNode } from "typescript"; describe("Request", () => { it("construct", () => { + const bodyReader = createBodyReader(); expect({ ...new Request({ method: "GET", @@ -14,6 +17,7 @@ describe("Request", () => { params: {}, path: "/path", query: { query: "1" }, + bodyReader, ...({ test: "value", }), @@ -21,7 +25,6 @@ describe("Request", () => { }).toStrictEqual({ "@duplojs/utils/kind/@DuplojsHttpCore/request": null, method: "GET", - body: undefined, headers: { host: "example.com" }, url: "https://example.com/path?query=1", host: "example.com", @@ -30,7 +33,10 @@ describe("Request", () => { params: {}, path: "/path", query: { query: "1" }, + bodyReader, test: "value", + bodyResult: undefined, + filesAttache: undefined, }); }); @@ -43,4 +49,28 @@ describe("Request", () => { expect((new CloneRequest()) instanceof Request).toBe(true); expect((new Request({} as any)) instanceof CloneRequest).toBe(true); }); + + it("getBody", async() => { + const spy = vi.fn(() => "superBody"); + const bodyRequest = new Request({ + method: "GET", + headers: { host: "example.com" }, + url: "https://example.com/path?query=1", + host: "example.com", + origin: "https://example.com", + matchedPath: null, + params: {}, + path: "/path", + query: { query: "1" }, + bodyReader: createBodyReader(spy), + }); + + const body = bodyRequest.getBody(); + void bodyRequest.getBody(); + void bodyRequest.getBody(); + await expect(bodyRequest.getBody()).resolves.toStrictEqual(E.success("superBody")); + await expect(body).resolves.toStrictEqual(E.success("superBody")); + expect(bodyRequest.getBody()).toStrictEqual(E.success("superBody")); + expect(spy).toHaveBeenCalledTimes(1); + }); }); diff --git a/tests/core/route/index.test.ts b/tests/core/route/index.test.ts index 1dc2f0a..5c6e01c 100644 --- a/tests/core/route/index.test.ts +++ b/tests/core/route/index.test.ts @@ -10,6 +10,7 @@ describe("route", () => { steps: [], hooks: [], metadata: [], + bodyController: null, }), ).toStrictEqual({ definition: { @@ -19,6 +20,7 @@ describe("route", () => { steps: [], hooks: [], metadata: [], + bodyController: null, }, [routeKind.runTimeKey]: null, }); diff --git a/tests/core/router/index.test.ts b/tests/core/router/index.test.ts index b8a168b..9a8fe67 100644 --- a/tests/core/router/index.test.ts +++ b/tests/core/router/index.test.ts @@ -1,9 +1,12 @@ -import { buildRouter, createHub, defaultCheckerStepFunctionBuilder, defaultCutStepFunctionBuilder, defaultExtractStepFunctionBuilder, defaultHandlerStepFunctionBuilder, defaultProcessStepFunctionBuilder, defaultRouteFunctionBuilder, ResponseContract, routeKind, RouterBuildError, stepKind, useRouteBuilder } from "@core"; -import { DP } from "@duplojs/utils"; +import { buildRouter, createHub, defaultCheckerStepFunctionBuilder, defaultCutStepFunctionBuilder, defaultExtractStepFunctionBuilder, defaultHandlerStepFunctionBuilder, defaultProcessStepFunctionBuilder, defaultRouteFunctionBuilder, NotFoundBodyReaderImplementationError, ResponseContract, RouterBuildError, stepKind, TextBodyController, useRouteBuilder } from "@core"; +import { DP, E } from "@duplojs/utils"; import { testRoute } from "@test-utils/route"; describe("buildRouter", () => { + const textBodyReaderImplementation = TextBodyController + .createReaderImplementation(() => Promise.resolve(E.success(null))); it("correct build router", async() => { + const otherRoute = { ...testRoute }; const router = await buildRouter( createHub({ environment: "DEV", @@ -19,8 +22,9 @@ describe("buildRouter", () => { hooksRouteLifeCycle: [{}], routeFunctionBuilders: [defaultRouteFunctionBuilder], stepFunctionBuilders: [defaultCutStepFunctionBuilder], - routes: [testRoute], - }), + routes: [otherRoute], + }) + .addBodyReaderImplementation(textBodyReaderImplementation), ); expect(router).toStrictEqual({ @@ -32,15 +36,15 @@ describe("buildRouter", () => { defaultRouteFunctionBuilder, defaultRouteFunctionBuilder, ], - routes: [testRoute, testRoute], + routes: new Set([testRoute, otherRoute]), stepFunctionBuilders: [ defaultCheckerStepFunctionBuilder, + defaultCutStepFunctionBuilder, defaultCheckerStepFunctionBuilder, defaultCutStepFunctionBuilder, defaultHandlerStepFunctionBuilder, defaultExtractStepFunctionBuilder, defaultProcessStepFunctionBuilder, - defaultCutStepFunctionBuilder, ], }); }); @@ -51,13 +55,26 @@ describe("buildRouter", () => { createHub({ environment: "DEV", }) - .register([{}] as any), + .register([{}] as any) + .addBodyReaderImplementation(textBodyReaderImplementation), ), ).rejects.instanceof(RouterBuildError); }); + it("throw error when not found body reader implementation", async() => { + await expect( + buildRouter( + createHub({ + environment: "DEV", + }) + .register(testRoute), + ), + ).rejects.instanceof(NotFoundBodyReaderImplementationError); + }); + it("throw error when build notfound route", async() => { - const hub = createHub({ environment: "DEV" }); + const hub = createHub({ environment: "DEV" }) + .addBodyReaderImplementation(textBodyReaderImplementation); (hub as any).notfoundHandler = stepKind.setTo({}) as any; @@ -74,11 +91,12 @@ describe("buildRouter", () => { createHub({ environment: "DEV" }) .setNotfoundHandler( ResponseContract.notFound("notfound"), - ({ response }) => { - spy(); + async({ response, request }) => { + spy(await request.getBody()); return response("notfound"); }, - ), + ) + .addBodyReaderImplementation(textBodyReaderImplementation), ); await buildedRouter.exec({ @@ -89,7 +107,7 @@ describe("buildRouter", () => { url: "/test", }); - expect(spy).toHaveBeenCalled(); + expect(spy).toHaveBeenCalledWith(E.error(new Error("Inaccessible body in not found route."))); }); it("buildedRouter use notfound route after search route", async() => { @@ -115,7 +133,8 @@ describe("buildRouter", () => { spy(); return response("notfound"); }, - ), + ) + .addBodyReaderImplementation(textBodyReaderImplementation), ); await buildedRouter.exec({ @@ -153,7 +172,8 @@ describe("buildRouter", () => { spyNotfound(); return response("notfound"); }, - ), + ) + .addBodyReaderImplementation(textBodyReaderImplementation), ); await buildedRouter.exec({ @@ -201,7 +221,8 @@ describe("buildRouter", () => { spyNotfound(); return response("notfound"); }, - ), + ) + .addBodyReaderImplementation(textBodyReaderImplementation), ); await buildedRouter.exec({ @@ -245,7 +266,8 @@ describe("buildRouter", () => { spyNotfound(); return response("notfound"); }, - ), + ) + .addBodyReaderImplementation(textBodyReaderImplementation), ); await buildedRouter.exec({ diff --git a/tests/core/steps/extract.test.ts b/tests/core/steps/extract/index.test.ts similarity index 100% rename from tests/core/steps/extract.test.ts rename to tests/core/steps/extract/index.test.ts diff --git a/tests/interfaces/bun/_utils/request.ts b/tests/interfaces/bun/_utils/request.ts index 766588f..b23ac84 100644 --- a/tests/interfaces/bun/_utils/request.ts +++ b/tests/interfaces/bun/_utils/request.ts @@ -1,5 +1,6 @@ import { type RequestInitializationData, Request } from "@core"; import { type SimplifyTopLevel } from "@duplojs/utils"; +import { createBodyReader } from "@test-utils/bodyReader"; type InitializationData = SimplifyTopLevel< & Omit, "raw"> @@ -19,6 +20,7 @@ export function createFakeRequest({ raw, ...initializationData }: Initialization origin: "", url: "", raw: {} as any, + bodyReader: createBodyReader(), ...initializationData, }); } diff --git a/tests/interfaces/deno/_utils/request.ts b/tests/interfaces/deno/_utils/request.ts index 766588f..b23ac84 100644 --- a/tests/interfaces/deno/_utils/request.ts +++ b/tests/interfaces/deno/_utils/request.ts @@ -1,5 +1,6 @@ import { type RequestInitializationData, Request } from "@core"; import { type SimplifyTopLevel } from "@duplojs/utils"; +import { createBodyReader } from "@test-utils/bodyReader"; type InitializationData = SimplifyTopLevel< & Omit, "raw"> @@ -19,6 +20,7 @@ export function createFakeRequest({ raw, ...initializationData }: Initialization origin: "", url: "", raw: {} as any, + bodyReader: createBodyReader(), ...initializationData, }); } diff --git a/tests/interfaces/node/_utils/fs.ts b/tests/interfaces/node/_utils/fs.ts new file mode 100644 index 0000000..64db02b --- /dev/null +++ b/tests/interfaces/node/_utils/fs.ts @@ -0,0 +1,50 @@ +import type * as fileSystem from "fs"; +import type * as fileSystemPromise from "fs/promises"; +import type { Mock } from "vitest"; + +const original = Symbol("original"); + +vi.mock( + "fs", + async(importOriginal) => { + const fs = await importOriginal() as object; + + return Object.fromEntries([ + ...Object + .keys(fs) + .map((key) => [key, vi.fn()]), + [original, fs], + ]); + }, +); + +vi.mock( + "fs/promises", + async(importOriginal) => { + const fsPromise = await importOriginal() as object; + + return Object.fromEntries([ + ...Object + .keys(fsPromise) + .map((key) => [key, vi.fn()]), + [original, fsPromise], + ]); + }, +); + +export const fsSpy: Record = await import("fs") as any; +export const fspSpy: Record = await import("fs/promises") as any; +export function fsSpyResetMock() { + Object.values(fsSpy).forEach((value) => { + value.mockReset(); + }); +} + +export function fspSpyResetMock() { + Object.values(fspSpy).forEach((value) => { + value.mockReset(); + }); +} + +export const fs: typeof fileSystem = (fsSpy as never)[original as never] as any; +export const fsp: typeof fileSystemPromise = (fspSpy as never)[original as never] as any; diff --git a/tests/interfaces/node/_utils/request.ts b/tests/interfaces/node/_utils/request.ts index f336e2b..50013c1 100644 --- a/tests/interfaces/node/_utils/request.ts +++ b/tests/interfaces/node/_utils/request.ts @@ -1,15 +1,22 @@ +/* eslint-disable require-yield */ +/* eslint-disable @typescript-eslint/only-throw-error */ +/* eslint-disable @typescript-eslint/require-await */ import { type RequestInitializationData, Request } from "@core"; import httpMocks from "node-mocks-http"; import type http from "http"; -import FormData from "form-data"; import { type SimplifyTopLevel } from "@duplojs/utils"; +import { createBodyReader } from "@test-utils/bodyReader"; type InitializationData = SimplifyTopLevel< & Omit, "raw"> & { body?: unknown } & { raw?: { - request?: httpMocks.RequestOptions; + request?: httpMocks.RequestOptions & { + bodyChunks?: unknown; + bodyIteratorError?: unknown; + readableHighWaterMark?: number; + }; response?: httpMocks.ResponseOptions; }; } @@ -24,14 +31,36 @@ export function createFakeRequest({ raw, ...initializationData }: Initialization raw?.response, ); - request.pipe = (writable) => { - const body = raw?.request?.body; - if (body instanceof FormData) { - body.pipe(writable); - } + if (raw?.request?.readableHighWaterMark) { + (request.readableHighWaterMark as any) = raw?.request?.readableHighWaterMark; + } - return writable; - }; + const bodyIteratorError = raw?.request?.bodyIteratorError; + const explicitBodyChunks = raw?.request?.bodyChunks; + const bodyChunks = explicitBodyChunks ?? raw?.request?.body; + + if (bodyIteratorError) { + request[Symbol.asyncIterator] = async function *() { + throw bodyIteratorError; + }; + } else if (typeof (bodyChunks as never)?.[Symbol.asyncIterator] === "function") { + request[Symbol.asyncIterator as never] = (bodyChunks as AsyncIterable)[Symbol.asyncIterator] + .bind(bodyChunks); + } else if (typeof bodyChunks === "string" || Buffer.isBuffer(bodyChunks)) { + request[Symbol.asyncIterator as never] = async function *() { + yield await bodyChunks as never; + }; + } else if (typeof (bodyChunks as never)?.[Symbol.iterator] === "function") { + request[Symbol.asyncIterator as never] = async function *() { + for (const chunk of bodyChunks as Iterable) { + yield chunk as never; + } + }; + } else if (bodyChunks !== undefined) { + request[Symbol.asyncIterator as never] = async function *() { + yield bodyChunks as any; + }; + } return new Request({ method: "GET", @@ -47,6 +76,7 @@ export function createFakeRequest({ raw, ...initializationData }: Initialization request, response, }, + bodyReader: createBodyReader(), ...initializationData, }) as Request & { raw: { diff --git a/tests/interfaces/node/bodyReader/formData/error.test.ts b/tests/interfaces/node/bodyReader/formData/error.test.ts new file mode 100644 index 0000000..5d36e04 --- /dev/null +++ b/tests/interfaces/node/bodyReader/formData/error.test.ts @@ -0,0 +1,5 @@ +import { BodyParseFormDataError } from "@interface-node"; + +it("BodyParseFormDataError", () => { + expect(new BodyParseFormDataError("")).instanceOf(Error); +}); diff --git a/tests/interfaces/node/bodyReader/formData/index.test.ts b/tests/interfaces/node/bodyReader/formData/index.test.ts new file mode 100644 index 0000000..f6da91b --- /dev/null +++ b/tests/interfaces/node/bodyReader/formData/index.test.ts @@ -0,0 +1,441 @@ +import { fsSpy, fsSpyResetMock } from "@test-utils/fs"; +import { WrongContentTypeError } from "@core/errors"; +import { createFormDataBodyReaderImplementation } from "@interface-node/bodyReaders/formData"; +import { E, unwrap } from "@duplojs/utils"; +import { createFakeRequest } from "@test-utils/request"; +import { SF } from "@duplojs/server-utils"; +import type { HttpServerParams } from "@core"; + +describe("createFormDataBodyReaderImplementation", () => { + const boundary = "duplo-boundary"; + const contentType = `multipart/form-data; boundary=${boundary}`; + const serverParams: HttpServerParams = { + interface: "node", + host: "localhost", + port: 3000, + maxBodySize: "10mb", + informationHeaderKey: "information", + predictedHeaderKey: "predicted", + fromHookHeaderKey: "from-hook", + uploadFolder: "./upload", + }; + + beforeEach(() => { + fsSpyResetMock(); + }); + + it("returns WrongContentTypeError when content-type is unsupported", async() => { + const request = createFakeRequest({ + headers: { + "content-type": "application/xml", + }, + raw: { + request: { + headers: { + "content-type": "application/xml", + }, + bodyChunks: [], + }, + }, + }); + const reader = createFormDataBodyReaderImplementation(serverParams); + + const result = await reader.read(request, { + maxFileQuantity: 1, + maxBufferSize: 10000, + maxKeyLength: 100, + maxIndexArray: 2500, + }); + + expect(E.hasInformation(result, "error")).toBe(true); + expect(unwrap(result)).toBeInstanceOf(WrongContentTypeError); + }); + + it("returns WrongContentTypeError when content-type is missing", async() => { + const request = createFakeRequest({ + headers: {}, + raw: { + request: { + headers: {}, + bodyChunks: [], + }, + }, + }); + const reader = createFormDataBodyReaderImplementation(serverParams); + + const result = await reader.read(request, { + maxFileQuantity: 1, + maxBufferSize: 10000, + maxKeyLength: 100, + maxIndexArray: 2500, + }); + + expect(E.hasInformation(result, "error")).toBe(true); + expect(unwrap(result)).toBeInstanceOf(WrongContentTypeError); + }); + + it("returns WrongContentTypeError when content-type is an array", async() => { + const request = createFakeRequest({ + headers: { + "content-type": ["application/xml", "text/plain"], + } as any, + raw: { + request: { + headers: { + "content-type": ["application/xml", "text/plain"], + } as any, + bodyChunks: [], + }, + }, + }); + const reader = createFormDataBodyReaderImplementation(serverParams); + + const result = await reader.read(request, { + maxFileQuantity: 1, + maxBufferSize: 10000, + maxKeyLength: 100, + maxIndexArray: 2500, + }); + + expect(E.hasInformation(result, "error")).toBe(true); + expect(unwrap(result)).toBeInstanceOf(WrongContentTypeError); + }); + + it("parses text field and returns file interface values", async() => { + const request = createFakeRequest({ + headers: { + "content-type": contentType, + }, + raw: { + request: { + headers: { + "content-type": contentType, + }, + bodyChunks: [ + Buffer.from(`--${boundary}\r\nContent-Disposition: form-data; name="field"\r\n\r\n`), + Buffer.from("hello"), + Buffer.from(`\r\n--${boundary}--`), + ], + }, + }, + }); + const reader = createFormDataBodyReaderImplementation(serverParams); + + const result = await reader.read(request, { + maxFileQuantity: 1, + maxBufferSize: 10000, + maxKeyLength: 100, + maxIndexArray: 2500, + }); + + expect(E.hasInformation(result, "success")).toBe(true); + const body = unwrap(result) as Record; + expect(body.field).toBe("hello"); + }); + + it("merges values when the same field appears multiple times", async() => { + const request = createFakeRequest({ + headers: { + "content-type": contentType, + }, + raw: { + request: { + headers: { + "content-type": contentType, + }, + bodyChunks: [ + Buffer.from(`--${boundary}\r\nContent-Disposition: form-data; name="field"\r\n\r\n`), + Buffer.from("one"), + Buffer.from(`\r\n--${boundary}\r\nContent-Disposition: form-data; name="field"\r\n\r\n`), + Buffer.from("two"), + Buffer.from(`\r\n--${boundary}--`), + ], + }, + }, + }); + const reader = createFormDataBodyReaderImplementation(serverParams); + + const result = await reader.read(request, { + maxFileQuantity: 1, + maxBufferSize: 10000, + maxKeyLength: 100, + maxIndexArray: 2500, + }); + + expect(E.hasInformation(result, "success")).toBe(true); + const body = unwrap(result) as Record; + expect(Array.isArray(body.field)).toBe(true); + const values = body.field as SF.FileInterface[]; + expect(values).toHaveLength(2); + expect(values[0]).toBe("one"); + expect(values[1]).toBe("two"); + }); + + it("parses file field and writes file stream", async() => { + const nowSpy = vi.spyOn(Date, "now").mockReturnValue(1234567890); + const writeSpy = vi.fn((__: Buffer, cb?: (err?: Error | null) => void) => { + cb?.(null); + }); + const endSpy = vi.fn(); + const streamMock = { + path: "", + write: writeSpy, + end: endSpy, + on: vi.fn(), + once: vi.fn(), + emit: vi.fn(), + }; + fsSpy.createWriteStream.mockImplementation((path: string) => { + streamMock.path = path; + return streamMock as any; + }); + + const request = createFakeRequest({ + headers: { + "content-type": contentType, + }, + raw: { + request: { + headers: { + "content-type": contentType, + }, + bodyChunks: [ + Buffer.from(`--${boundary}\r\nContent-Disposition: form-data; name="file"; filename="a.txt"\r\n\r\n`), + Buffer.from("DATA"), + Buffer.from(`\r\n--${boundary}--`), + ], + readableHighWaterMark: 16, + }, + }, + }); + const reader = createFormDataBodyReaderImplementation(serverParams); + + const result = await reader.read(request, { + maxFileQuantity: 1, + maxBufferSize: 10000, + maxKeyLength: 100, + maxIndexArray: 2500, + }); + + expect(fsSpy.createWriteStream).toHaveBeenCalledTimes(1); + const filePath = (fsSpy.createWriteStream.mock.calls[0] as [string])[0]; + expect(request.filesAttache).toStrictEqual([filePath]); + expect(writeSpy).toHaveBeenCalled(); + expect(endSpy).toHaveBeenCalled(); + + expect(E.hasInformation(result, "success")).toBe(true); + const body = unwrap(result) as Record; + expect(SF.isFileInterface(body.file)).toBe(true); + expect((body.file as SF.FileInterface).path).toBe(filePath); + + nowSpy.mockRestore(); + }); + + it("writes file stream without extension in filename", async() => { + const nowSpy = vi.spyOn(Date, "now").mockReturnValue(1234567890); + const writeSpy = vi.fn((__: Buffer, cb?: (err?: Error | null) => void) => { + cb?.(null); + }); + const endSpy = vi.fn(); + const streamMock = { + path: "", + write: writeSpy, + end: endSpy, + on: vi.fn(), + once: vi.fn(), + emit: vi.fn(), + }; + fsSpy.createWriteStream.mockImplementation((path: string) => { + streamMock.path = path; + return streamMock as any; + }); + + const request = createFakeRequest({ + headers: { + "content-type": contentType, + }, + raw: { + request: { + headers: { + "content-type": contentType, + }, + bodyChunks: [ + Buffer.from(`--${boundary}\r\nContent-Disposition: form-data; name="file"; filename="noext"\r\n\r\n`), + Buffer.from("DATA"), + Buffer.from(`\r\n--${boundary}--`), + ], + readableHighWaterMark: 16, + }, + }, + }); + const reader = createFormDataBodyReaderImplementation(serverParams); + + const result = await reader.read(request, { + maxFileQuantity: 1, + maxBufferSize: 10000, + maxKeyLength: 100, + maxIndexArray: 2500, + }); + + const filePath = (fsSpy.createWriteStream.mock.calls.at(-1) as [string])[0]; + expect(filePath.endsWith("1234567890")).toBe(true); + expect(E.hasInformation(result, "success")).toBe(true); + + nowSpy.mockRestore(); + }); + + it("returns error when file size exceeds limit and closes stream", async() => { + const writeSpy = vi.fn((__: Buffer, cb?: (err?: Error | null) => void) => { + cb?.(null); + }); + const endSpy = vi.fn(); + const streamMock = { + path: "", + write: writeSpy, + end: endSpy, + on: vi.fn(), + once: vi.fn(), + emit: vi.fn(), + }; + fsSpy.createWriteStream.mockImplementation((path: string) => { + streamMock.path = path; + return streamMock as any; + }); + + const request = createFakeRequest({ + headers: { + "content-type": contentType, + }, + raw: { + request: { + headers: { + "content-type": contentType, + }, + bodyChunks: [ + Buffer.from(`--${boundary}\r\nContent-Disposition: form-data; name="file"; filename="a.txt"\r\n\r\n`), + Buffer.from("AB"), + Buffer.from(`\r\n--${boundary}--`), + ], + }, + }, + }); + const reader = createFormDataBodyReaderImplementation(serverParams); + + const result = await reader.read(request, { + maxFileQuantity: 1, + fileMaxSize: 1, + maxBufferSize: 10000, + maxKeyLength: 100, + maxIndexArray: 2500, + }); + + expect(E.hasInformation(result, "error")).toBe(true); + expect(endSpy).toHaveBeenCalled(); + }); + + it("throws when file write callback returns error", async() => { + const writeSpy = vi.fn((__: Buffer, cb?: (err?: Error | null) => void) => { + cb?.(new Error("fail")); + }); + const endSpy = vi.fn(); + const streamMock = { + path: "", + write: writeSpy, + end: endSpy, + on: vi.fn(), + once: vi.fn(), + emit: vi.fn(), + }; + fsSpy.createWriteStream.mockImplementation((path: string) => { + streamMock.path = path; + return streamMock as any; + }); + + const request = createFakeRequest({ + headers: { + "content-type": contentType, + }, + raw: { + request: { + headers: { + "content-type": contentType, + }, + bodyChunks: [ + Buffer.from(`--${boundary}\r\nContent-Disposition: form-data; name="file"; filename="a.txt"\r\n\r\n`), + Buffer.from("DATA"), + Buffer.from(`\r\n--${boundary}--`), + ], + }, + }, + }); + const reader = createFormDataBodyReaderImplementation(serverParams); + + await expect(reader.read(request, { + maxFileQuantity: 1, + maxBufferSize: 10000, + maxKeyLength: 100, + maxIndexArray: 2500, + })).rejects.toBeInstanceOf(Error); + + expect(endSpy).toHaveBeenCalled(); + }); + + it("returns advanced object when content-type-options includes advanced", async() => { + const request = createFakeRequest({ + headers: { + "content-type": contentType, + "content-type-options": "advanced", + }, + raw: { + request: { + headers: { + "content-type": contentType, + }, + bodyChunks: [ + Buffer.from(`--${boundary}\r\nContent-Disposition: form-data; name="user/*\\[0]/*\\name"\r\n\r\n`), + Buffer.from("bob"), + Buffer.from(`\r\n--${boundary}--`), + ], + }, + }, + }); + const reader = createFormDataBodyReaderImplementation(serverParams); + + const result = await reader.read(request, { + maxFileQuantity: 1, + maxBufferSize: 10000, + maxKeyLength: 100, + maxIndexArray: 2500, + }); + + expect(E.hasInformation(result, "success")).toBe(true); + const body = unwrap(result) as any; + expect(body.user[0].name).toBeDefined(); + expect(body.user[0].name).toBe("bob"); + }); + + it("throws when readRequestFormData returns server-error", async() => { + const error = new Error("boom"); + const request = createFakeRequest({ + headers: { + "content-type": contentType, + }, + raw: { + request: { + headers: { + "content-type": contentType, + }, + bodyIteratorError: error, + }, + }, + }); + const reader = createFormDataBodyReaderImplementation(serverParams); + + await expect(reader.read(request, { + maxFileQuantity: 1, + maxBufferSize: 10000, + maxKeyLength: 100, + maxIndexArray: 2500, + })).rejects.toBe(error); + }); +}); diff --git a/tests/interfaces/node/bodyReader/formData/readRequestFormData.test.ts b/tests/interfaces/node/bodyReader/formData/readRequestFormData.test.ts new file mode 100644 index 0000000..9655ba1 --- /dev/null +++ b/tests/interfaces/node/bodyReader/formData/readRequestFormData.test.ts @@ -0,0 +1,682 @@ +import { BodyParseWrongChunkReceived, BodySizeExceedsLimitError } from "@core/errors"; +import { BodyParseFormDataError, readRequestFormData } from "@interface-node"; +import { E, unwrap } from "@duplojs/utils"; +import { createFakeRequest } from "@test-utils/request"; + +describe("readRequestFormData", () => { + const boundary = "duplo-boundary"; + const contentType = `multipart/form-data; boundary=${boundary}`; + + it("returns error when boundary is missing", async() => { + const request = createFakeRequest({ + raw: { + request: { + headers: { + "content-type": "multipart/form-data", + }, + bodyChunks: [], + }, + }, + }); + + const result = await readRequestFormData( + request.raw.request, + {}, + { + maxBodySize: 1000, + maxBufferSize: 10000, + maxKeyLength: 100, + maxFileQuantity: 1, + }, + () => ({ + onReceiveChunk: () => {}, + onEndPart: (value) => value, + onError: () => {}, + }), + ); + + expect(E.hasInformation(result, "error")).toBe(true); + expect(unwrap(result)).toBeInstanceOf(BodyParseFormDataError); + }); + + it("returns error when content-type is missing", async() => { + const request = createFakeRequest({ + raw: { + request: { + bodyChunks: [], + }, + }, + }); + + const result = await readRequestFormData( + request.raw.request, + {}, + { + maxBodySize: 1000, + maxBufferSize: 10000, + maxKeyLength: 100, + maxFileQuantity: 1, + }, + () => ({ + onReceiveChunk: () => {}, + onEndPart: (value) => value, + onError: () => {}, + }), + ); + + expect(E.hasInformation(result, "error")).toBe(true); + expect(unwrap(result)).toBeInstanceOf(BodyParseFormDataError); + }); + + it("parses field and file parts with heavily split header and streaming data", async() => { + const request = createFakeRequest({ + raw: { + request: { + headers: { + "content-type": contentType, + }, + bodyChunks: [ + Buffer.from(`--${boundary}\r\nContent-`), + Buffer.from("Disposition: form-data; name=\"fi"), + Buffer.from("eld\"\r\n\r\nva"), + Buffer.from(`lue\r\n--${boundary}\r\nContent-`), + Buffer.from("Disposition: form-data; name=\"fi"), + Buffer.from("le\"; filename=\"ignored"), + Buffer.from(".txt\"; filename*=utf-8''file%20"), + Buffer.from("name.txt\r\nContent-Type: text/"), + Buffer.from("plain\r\n\r\n"), + Buffer.from("A".repeat(40)), + Buffer.from(`\r\n--${boundary}--`), + ], + }, + }, + }); + const destroySpy = vi.spyOn(request.raw.request, "destroy"); + const accumulator = { + fields: {} as Record, + files: [] as { + name: string; + filename: string; + data: string; + }[], + }; + + const result = await readRequestFormData( + request.raw.request, + accumulator, + { + maxBodySize: 10000, + maxBufferSize: 20000, + maxKeyLength: 100, + maxFileQuantity: 2, + mimeType: /txt/, + }, + (header) => { + if (header.filename) { + let fileData = ""; + return { + onReceiveChunk: (chunk) => { + fileData += chunk.toString("utf-8"); + }, + onEndPart: (value) => { + value.files.push({ + name: header.name, + filename: header.filename ?? "", + data: fileData, + }); + return value; + }, + onError: () => {}, + }; + } + + let fieldValue = ""; + return { + onReceiveChunk: (chunk) => { + fieldValue += chunk.toString("utf-8"); + }, + onEndPart: (value) => { + value.fields[header.name] = fieldValue; + return value; + }, + onError: () => {}, + }; + }, + ); + + expect(result).toStrictEqual({ + fields: { field: "value" }, + files: [ + { + name: "file", + filename: "file name.txt", + data: "A".repeat(40), + }, + ], + }); + expect(destroySpy).toHaveBeenCalled(); + }); + + it("returns error for wrong chunk type and calls onError", async() => { + const onError = vi.fn(); + const request = createFakeRequest({ + raw: { + request: { + headers: { + "content-type": contentType, + }, + bodyChunks: [ + Buffer.from( + `--${boundary}\r\nContent-Disposition: form-data; name="field"\r\n\r\n`, + ), + "not-a-buffer", + ], + }, + }, + }); + + const result = await readRequestFormData( + request.raw.request, + {}, + { + maxBodySize: 1000, + maxBufferSize: 10000, + maxKeyLength: 100, + maxFileQuantity: 1, + }, + () => ({ + onReceiveChunk: () => {}, + onEndPart: (value) => value, + onError, + }), + ); + + expect(E.hasInformation(result, "error")).toBe(true); + expect(unwrap(result)).toBeInstanceOf(BodyParseWrongChunkReceived); + expect(onError).toHaveBeenCalled(); + }); + + it("returns error when body size exceeds limit", async() => { + const request = createFakeRequest({ + raw: { + request: { + headers: { + "content-type": contentType, + }, + bodyChunks: [ + Buffer.from(`--${boundary}\r\nContent-`), + Buffer.from("Disposition: form-data; name=\"fi"), + Buffer.from("eld\"\r\n\r\n"), + Buffer.from("val"), + Buffer.from(`ue\r\n--${boundary}`), + Buffer.from("--"), + ], + }, + }, + }); + + const result = await readRequestFormData( + request.raw.request, + {}, + { + maxBodySize: 5, + maxBufferSize: 10000, + maxKeyLength: 100, + maxFileQuantity: 1, + }, + () => ({ + onReceiveChunk: () => {}, + onEndPart: (value) => value, + onError: () => {}, + }), + ); + + expect(E.hasInformation(result, "error")).toBe(true); + expect(unwrap(result)).toBeInstanceOf(BodySizeExceedsLimitError); + }); + + it("returns error when streaming data exceeds max body size", async() => { + const request = createFakeRequest({ + raw: { + request: { + headers: { + "content-type": contentType, + }, + bodyChunks: [ + Buffer.from(`--${boundary}\r\nContent-Disposition: form-data; name="field"\r\n\r\n`), + Buffer.from("A".repeat(2000)), + ], + }, + }, + }); + const onError = vi.fn(); + + const result = await readRequestFormData( + request.raw.request, + {}, + { + maxBodySize: 1800, + maxBufferSize: 2000, + maxKeyLength: 100, + maxFileQuantity: 1, + }, + () => ({ + onReceiveChunk: () => {}, + onEndPart: (value) => value, + onError, + }), + ); + + expect(E.hasInformation(result, "error")).toBe(true); + expect(unwrap(result)).toBeInstanceOf(BodySizeExceedsLimitError); + expect(onError).toHaveBeenCalled(); + }); + + it("returns error when buffer size exceeds limit", async() => { + const request = createFakeRequest({ + raw: { + request: { + headers: { + "content-type": contentType, + }, + bodyChunks: [Buffer.from("A".repeat(20))], + }, + }, + }); + + const result = await readRequestFormData( + request.raw.request, + {}, + { + maxBodySize: 1000, + maxBufferSize: 5, + maxKeyLength: 100, + maxFileQuantity: 1, + }, + () => ({ + onReceiveChunk: () => {}, + onEndPart: (value) => value, + onError: () => {}, + }), + ); + + expect(E.hasInformation(result, "error")).toBe(true); + expect(unwrap(result)).toBeInstanceOf(BodyParseFormDataError); + }); + + it("returns error for invalid header part", async() => { + const request = createFakeRequest({ + raw: { + request: { + headers: { + "content-type": contentType, + }, + bodyChunks: [ + Buffer.from(`--${boundary}\r\nContent-`), + Buffer.from("Disposition: form-data\r\n"), + Buffer.from("\r\nval"), + Buffer.from(`ue\r\n--${boundary}`), + Buffer.from("--"), + ], + }, + }, + }); + + const result = await readRequestFormData( + request.raw.request, + {}, + { + maxBodySize: 1000, + maxBufferSize: 10000, + maxKeyLength: 100, + maxFileQuantity: 1, + }, + () => ({ + onReceiveChunk: () => {}, + onEndPart: (value) => value, + onError: () => {}, + }), + ); + + expect(E.hasInformation(result, "error")).toBe(true); + expect(unwrap(result)).toBeInstanceOf(BodyParseFormDataError); + }); + + it("returns error when key length exceeds limit", async() => { + const longKey = "a".repeat(6); + const request = createFakeRequest({ + raw: { + request: { + headers: { + "content-type": contentType, + }, + bodyChunks: [ + Buffer.from(`--${boundary}\r\nContent-`), + Buffer.from(`Disposition: form-data; name="${longKey}"\r\n\r\n`), + Buffer.from(`value\r\n--${boundary}`), + Buffer.from("--"), + ], + }, + }, + }); + + const result = await readRequestFormData( + request.raw.request, + {}, + { + maxBodySize: 1000, + maxBufferSize: 10000, + maxFileQuantity: 1, + maxKeyLength: 5, + }, + () => ({ + onReceiveChunk: () => {}, + onEndPart: (value) => value, + onError: () => {}, + }), + ); + + expect(E.hasInformation(result, "error")).toBe(true); + expect(unwrap(result)).toBeInstanceOf(BodyParseFormDataError); + }); + + it("returns error when file quantity exceeds limit", async() => { + const request = createFakeRequest({ + raw: { + request: { + headers: { + "content-type": contentType, + }, + bodyChunks: [ + Buffer.from(`--${boundary}\r\nContent-`), + Buffer.from("Disposition: form-data; name=\"file1\"; fil"), + Buffer.from(`ename="a.txt"\r\n\r\nA\r\n--${boundary}\r\nContent-`), + Buffer.from("Disposition: form-data; name=\"file2\"; fil"), + Buffer.from(`ename="b.txt"\r\n\r\nB\r\n--${boundary}`), + Buffer.from("--"), + ], + }, + }, + }); + + const result = await readRequestFormData( + request.raw.request, + {}, + { + maxBodySize: 1000, + maxBufferSize: 10000, + maxKeyLength: 100, + maxFileQuantity: 1, + }, + () => ({ + onReceiveChunk: () => {}, + onEndPart: (value) => value, + onError: () => {}, + }), + ); + + expect(E.hasInformation(result, "error")).toBe(true); + expect(unwrap(result)).toBeInstanceOf(BodyParseFormDataError); + }); + + it("returns error when file mimeType is wrong and decode fails", async() => { + const request = createFakeRequest({ + raw: { + request: { + headers: { + "content-type": contentType, + }, + bodyChunks: [ + Buffer.from(`--${boundary}\r\nContent-`), + Buffer.from("Disposition: form-data; name=\"file\"; filename=\"fallback"), + Buffer.from(".txt\"; filename*=utf-8''%E0%A4\r\n"), + Buffer.from(`\r\nA\r\n--${boundary}`), + Buffer.from("--"), + ], + }, + }, + }); + + const result = await readRequestFormData( + request.raw.request, + {}, + { + maxBodySize: 1000, + maxBufferSize: 10000, + maxKeyLength: 100, + maxFileQuantity: 1, + mimeType: /txt/, + }, + () => ({ + onReceiveChunk: () => {}, + onEndPart: (value) => value, + onError: () => {}, + }), + ); + + expect(E.hasInformation(result, "error")).toBe(true); + expect(unwrap(result)).toBeInstanceOf(BodyParseFormDataError); + }); + + it("returns error when file size exceeds limit and calls onError", async() => { + const onError = vi.fn(); + const request = createFakeRequest({ + raw: { + request: { + headers: { + "content-type": contentType, + }, + bodyChunks: [ + Buffer.from(`--${boundary}\r\nContent-`), + Buffer.from("Disposition: form-data; name=\"file\"; filename=\"a.txt\""), + Buffer.from("\r\n\r\n"), + Buffer.from("AB"), + Buffer.from("CDE"), + Buffer.from(`\r\n--${boundary}`), + Buffer.from("--"), + ], + }, + }, + }); + + const result = await readRequestFormData( + request.raw.request, + {}, + { + maxBodySize: 1000, + maxBufferSize: 10000, + maxKeyLength: 100, + maxFileQuantity: 1, + fileMaxSize: 3, + }, + () => ({ + onReceiveChunk: () => {}, + onEndPart: (value) => value, + onError, + }), + ); + + expect(E.hasInformation(result, "error")).toBe(true); + expect(unwrap(result)).toBeInstanceOf(BodyParseFormDataError); + expect(onError).toHaveBeenCalled(); + }); + + it("returns error when chunk arrives before header", async() => { + const request = createFakeRequest({ + raw: { + request: { + headers: { + "content-type": contentType, + }, + bodyChunks: [ + Buffer.from("ab"), + Buffer.from(`c\r\n--${boundary}\r\nContent-`), + Buffer.from("Disposition: form-data; name=\"field\"\r\n\r\n"), + Buffer.from(`value\r\n--${boundary}`), + Buffer.from("--"), + ], + }, + }, + }); + + const result = await readRequestFormData( + request.raw.request, + {}, + { + maxBodySize: 1000, + maxBufferSize: 10000, + maxKeyLength: 100, + maxFileQuantity: 1, + }, + () => ({ + onReceiveChunk: () => {}, + onEndPart: (value) => value, + onError: () => {}, + }), + ); + + expect(E.hasInformation(result, "error")).toBe(true); + expect(unwrap(result)).toBeInstanceOf(BodyParseFormDataError); + }); + + it("returns error for wrong content without boundary", async() => { + const request = createFakeRequest({ + raw: { + request: { + headers: { + "content-type": contentType, + }, + bodyChunks: [Buffer.from("\r\n"), Buffer.from("\r\n")], + }, + }, + }); + + const result = await readRequestFormData( + request.raw.request, + {}, + { + maxBodySize: 1000, + maxBufferSize: 10000, + maxKeyLength: 100, + maxFileQuantity: 1, + }, + () => ({ + onReceiveChunk: () => {}, + onEndPart: (value) => value, + onError: () => {}, + }), + ); + + expect(E.hasInformation(result, "error")).toBe(true); + expect(unwrap(result)).toBeInstanceOf(BodyParseFormDataError); + }); + + it("returns accumulator when payload contains only end boundary", async() => { + const localBoundary = "end-only"; + const localContentType = `multipart/form-data; boundary=${localBoundary}`; + const request = createFakeRequest({ + raw: { + request: { + headers: { + "content-type": localContentType, + }, + bodyChunks: [Buffer.from(`--${localBoundary}--`)], + }, + }, + }); + const accumulator = { ok: true }; + + const result = await readRequestFormData( + request.raw.request, + accumulator, + { + maxBodySize: 1000, + maxBufferSize: 10000, + maxKeyLength: 100, + maxFileQuantity: 1, + }, + () => ({ + onReceiveChunk: () => {}, + onEndPart: (value) => value, + onError: () => {}, + }), + ); + + expect(result).toBe(accumulator); + }); + + it("returns error when onReceiveHeader returns Error", async() => { + const request = createFakeRequest({ + raw: { + request: { + headers: { + "content-type": contentType, + }, + bodyChunks: [ + Buffer.from(`--${boundary}\r\nContent-`), + Buffer.from("Disposition: form-data; name=\"field\"\r\n\r\n"), + Buffer.from(`value\r\n--${boundary}`), + Buffer.from("--"), + ], + }, + }, + }); + + const result = await readRequestFormData( + request.raw.request, + {}, + { + maxBodySize: 1000, + maxBufferSize: 10000, + maxKeyLength: 100, + maxFileQuantity: 1, + }, + () => new Error("nope"), + ); + + expect(E.hasInformation(result, "error")).toBe(true); + expect(unwrap(result)).toStrictEqual(new Error("nope")); + }); + + it("returns server-error when stream processing throws and calls onError", async() => { + const error = new Error("boom"); + const onError = vi.fn(); + const request = createFakeRequest({ + raw: { + request: { + headers: { + "content-type": contentType, + }, + bodyChunks: [ + Buffer.from(`--${boundary}\r\nContent-`), + Buffer.from("Disposition: form-data; name=\"field\"\r\n\r\n"), + Buffer.from(`value\r\n--${boundary}`), + Buffer.from("--"), + ], + }, + }, + }); + + const result = await readRequestFormData( + request.raw.request, + {}, + { + maxBodySize: 1000, + maxBufferSize: 10000, + maxKeyLength: 100, + maxFileQuantity: 1, + }, + () => ({ + onReceiveChunk: () => { + throw error; + }, + onEndPart: (value) => value, + onError, + }), + ); + + expect(E.hasInformation(result, "server-error")).toBe(true); + expect(unwrap(result)).toBe(error); + expect(onError).toHaveBeenCalled(); + }); +}); diff --git a/tests/interfaces/node/bodyReader/text/index.test.ts b/tests/interfaces/node/bodyReader/text/index.test.ts new file mode 100644 index 0000000..450af8a --- /dev/null +++ b/tests/interfaces/node/bodyReader/text/index.test.ts @@ -0,0 +1,138 @@ +import { + BodySizeExceedsLimitError, + ParseJsonError, + WrongContentTypeError, +} from "@core/errors"; +import { createTextBodyReaderImplementation } from "@interface-node/bodyReaders/text"; +import { E, unwrap } from "@duplojs/utils"; +import { createFakeRequest } from "@test-utils/request"; +import { type HttpServerParams } from "@core"; + +describe("createTextBodyReaderImplementation", () => { + const serverParams: HttpServerParams = { + interface: "node", + host: "localhost", + port: 3000, + maxBodySize: "10mb", + informationHeaderKey: "information", + predictedHeaderKey: "predicted", + fromHookHeaderKey: "from-hook", + uploadFolder: "./upload", + }; + + it("returns WrongContentTypeError when content-type is unsupported", async() => { + const request = createFakeRequest({ + headers: { + "content-type": "application/xml", + }, + }); + const reader = createTextBodyReaderImplementation(serverParams); + + const result = await reader.read(request, {}); + + expect(E.hasInformation(result, "error")).toBe(true); + expect(unwrap(result)).toBeInstanceOf(WrongContentTypeError); + }); + + it("returns WrongContentTypeError when content-type is missing", async() => { + const request = createFakeRequest(); + const reader = createTextBodyReaderImplementation(serverParams); + + const result = await reader.read(request, {}); + + expect(E.hasInformation(result, "error")).toBe(true); + expect(unwrap(result)).toBeInstanceOf(WrongContentTypeError); + }); + + it("parses json body when content-type is application/json", async() => { + const request = createFakeRequest({ + headers: { + "content-type": "application/json; charset=utf-8", + }, + raw: { + request: { + bodyChunks: ["{\"ok\":true}"], + }, + }, + }); + const reader = createTextBodyReaderImplementation(serverParams); + + const result = await reader.read(request, {}); + + expect(result).toStrictEqual(E.success({ ok: true })); + }); + + it("returns ParseJsonError when json body is invalid", async() => { + const request = createFakeRequest({ + headers: { + "content-type": "application/json", + }, + raw: { + request: { + bodyChunks: ["{"], + }, + }, + }); + const reader = createTextBodyReaderImplementation(serverParams); + + const result = await reader.read(request, {}); + + expect(E.hasInformation(result, "error")).toBe(true); + expect(unwrap(result)).toBeInstanceOf(ParseJsonError); + }); + + it("returns text body when content-type is text/plain", async() => { + const request = createFakeRequest({ + headers: { + "content-type": "text/plain", + }, + raw: { + request: { + bodyChunks: ["hello"], + }, + }, + }); + const reader = createTextBodyReaderImplementation(serverParams); + + const result = await reader.read(request, {}); + + expect(result).toStrictEqual(E.success("hello")); + }); + + it("uses bodyMaxSize from params when provided", async() => { + const request = createFakeRequest({ + headers: { + "content-type": "text/plain", + }, + raw: { + request: { + bodyChunks: ["abcd"], + }, + }, + }); + const reader = createTextBodyReaderImplementation(serverParams); + + const result = await reader.read(request, { bodyMaxSize: 3 }); + + expect(E.hasInformation(result, "error")).toBe(true); + expect(unwrap(result)).toBeInstanceOf(BodySizeExceedsLimitError); + expect((unwrap(result) as BodySizeExceedsLimitError).bytesInString).toBe(3); + }); + + it("throws when readRequestText returns server-error", async() => { + const error = new Error("boom"); + const request = createFakeRequest({ + headers: { + "content-type": "text/plain", + }, + raw: { + request: { + bodyIteratorError: error, + }, + }, + }); + const reader = createTextBodyReaderImplementation(serverParams); + + await expect(reader.read(request, {})).rejects.toBe(error); + }); +}); diff --git a/tests/interfaces/node/bodyReader/text/readRequestText.test.ts b/tests/interfaces/node/bodyReader/text/readRequestText.test.ts new file mode 100644 index 0000000..ab0e6b0 --- /dev/null +++ b/tests/interfaces/node/bodyReader/text/readRequestText.test.ts @@ -0,0 +1,98 @@ +import { BodyParseWrongChunkReceived, BodySizeExceedsLimitError } from "@core/errors"; +import { readRequestText } from "@interface-node/bodyReaders/text/readRequestText"; +import { E, unwrap } from "@duplojs/utils"; +import { createFakeRequest } from "@test-utils/request"; + +describe("readRequestText", () => { + it("reads buffer and string chunks", async() => { + const request = createFakeRequest({ + raw: { + request: { + bodyChunks: [Buffer.from("hello "), "world"], + }, + }, + }); + const destroySpy = vi.spyOn(request.raw.request, "destroy"); + + const result = await readRequestText(request.raw.request, { maxBodySize: 100 }); + + expect(result).toBe("hello world"); + expect(destroySpy).toHaveBeenCalled(); + }); + + it("uses onEnd and returns its result", async() => { + const request = createFakeRequest({ + raw: { + request: { + bodyChunks: ["duplo"], + }, + }, + }); + const destroySpy = vi.spyOn(request.raw.request, "destroy"); + const onEnd = vi.fn((value: string) => ({ + ok: true, + value, + })); + + const result = await readRequestText(request.raw.request, { maxBodySize: 100 }, onEnd); + + expect(onEnd).toHaveBeenCalledWith("duplo"); + expect(result).toStrictEqual({ + ok: true, + value: "duplo", + }); + expect(destroySpy).toHaveBeenCalled(); + }); + + it("returns error for wrong chunk type", async() => { + const request = createFakeRequest({ + raw: { + request: { + bodyChunks: [123], + }, + }, + }); + const destroySpy = vi.spyOn(request.raw.request, "destroy"); + + const result = await readRequestText(request.raw.request, { maxBodySize: 100 }); + + expect(E.hasInformation(result, "error")).toBe(true); + expect(unwrap(result)).toBeInstanceOf(BodyParseWrongChunkReceived); + expect(destroySpy).toHaveBeenCalled(); + }); + + it("returns error when body size exceeds limit", async() => { + const request = createFakeRequest({ + raw: { + request: { + bodyChunks: ["abcd"], + }, + }, + }); + const destroySpy = vi.spyOn(request.raw.request, "destroy"); + + const result = await readRequestText(request.raw.request, { maxBodySize: 3 }); + + expect(E.hasInformation(result, "error")).toBe(true); + expect(unwrap(result)).toBeInstanceOf(BodySizeExceedsLimitError); + expect(destroySpy).toHaveBeenCalled(); + }); + + it("returns server-error when stream fails", async() => { + const error = new Error("boom"); + const request = createFakeRequest({ + raw: { + request: { + bodyIteratorError: error, + }, + }, + }); + const destroySpy = vi.spyOn(request.raw.request, "destroy"); + + const result = await readRequestText(request.raw.request, { maxBodySize: 100 }); + + expect(E.hasInformation(result, "server-error")).toBe(true); + expect(unwrap(result)).toBe(error); + expect(destroySpy).toHaveBeenCalled(); + }); +}); diff --git a/tests/interfaces/node/createHttpServer.test.ts b/tests/interfaces/node/createHttpServer.test.ts index 3257772..96d4f6e 100644 --- a/tests/interfaces/node/createHttpServer.test.ts +++ b/tests/interfaces/node/createHttpServer.test.ts @@ -74,9 +74,6 @@ describe("createHttpServer", () => { }, ); - expect(receivedHub?.hooksRouteLifeCycle.length).toBe( - baseHub.hooksRouteLifeCycle.length + 1, - ); expect(receivedHttpServerParams).toStrictEqual({ host: "localhost", port: 3000, @@ -85,6 +82,7 @@ describe("createHttpServer", () => { predictedHeaderKey: "predicted", fromHookHeaderKey: "from-hook", interface: "node", + uploadFolder: "./upload", }); expect(httpCreateServer).toHaveBeenCalledWith({}); expect(httpsCreateServer).not.toHaveBeenCalled(); diff --git a/tests/interfaces/node/error/bodyParseUnknownError.test.ts b/tests/interfaces/node/error/bodyParseUnknownError.test.ts deleted file mode 100644 index d2a1f08..0000000 --- a/tests/interfaces/node/error/bodyParseUnknownError.test.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { BodyParseUnknownError } from "@interface-node"; - -describe("BodyParseUnknownError", () => { - it("stores content type, unknown error and message", () => { - const unknownError = new Error("parse failed"); - const error = new BodyParseUnknownError("application/json", unknownError); - - expect({ - ...error, - message: error.message, - }).toStrictEqual({ - "@duplojs/utils/kind/@DuplojsHttpInterfacesNode/body-parse-unknown-error": null, - contentType: "application/json", - unknownError, - message: "Error when parsing body with 'application/json' content-type.", - }); - - expect(error).toBeInstanceOf(Error); - }); -}); diff --git a/tests/interfaces/node/error/bodyParseWrongChunkReceived.test.ts b/tests/interfaces/node/error/bodyParseWrongChunkReceived.test.ts deleted file mode 100644 index 9aa3f5e..0000000 --- a/tests/interfaces/node/error/bodyParseWrongChunkReceived.test.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { BodyParseWrongChunkReceived } from "@interface-node"; - -describe("BodyParseWrongChunkReceived", () => { - it("stores wrong chunk and message", () => { - const chunk = { some: "value" }; - const error = new BodyParseWrongChunkReceived(chunk); - - expect({ - ...error, - message: error.message, - }).toStrictEqual({ - "@duplojs/utils/kind/@DuplojsHttpInterfacesNode/body-parse-wrong-chunk-received": null, - wrongChunk: chunk, - message: "Received chunk is not buffer or string.", - }); - - expect(error).toBeInstanceOf(Error); - }); -}); diff --git a/tests/interfaces/node/error/bodySizeExceedsLimitError.test.ts b/tests/interfaces/node/error/bodySizeExceedsLimitError.test.ts deleted file mode 100644 index 49d9be3..0000000 --- a/tests/interfaces/node/error/bodySizeExceedsLimitError.test.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { BodySizeExceedsLimitError } from "@interface-node"; - -describe("BodySizeExceedsLimitError", () => { - it("stores size and message", () => { - const error = new BodySizeExceedsLimitError(1024); - - expect({ - ...error, - message: error.message, - }).toStrictEqual({ - "@duplojs/utils/kind/@DuplojsHttpInterfacesNode/body-size-exceeds-limit-error": null, - bytesInString: 1024, - message: "Body size is bigger than 1024.", - }); - - expect(error).toBeInstanceOf(Error); - }); -}); diff --git a/tests/interfaces/node/hooks.test.ts b/tests/interfaces/node/hooks.test.ts deleted file mode 100644 index 6bddccd..0000000 --- a/tests/interfaces/node/hooks.test.ts +++ /dev/null @@ -1,382 +0,0 @@ -import { makeNodeHook, BodyParseUnknownError, BodyParseWrongChunkReceived, BodySizeExceedsLimitError } from "@interface-node"; -import { HookResponse, type HttpServerParams, PredictedResponse, Response, createHub, exitHookFunction } from "@core"; -import { createFakeRequest } from "@test-utils/request"; -import { testHub } from "@test-utils/hub"; - -describe("makeNodeHook", () => { - const baseServerParams: HttpServerParams = { - interface: "node", - host: "localhost", - port: 3000, - maxBodySize: 100, - informationHeaderKey: "information", - fromHookHeaderKey: "from-hook", - predictedHeaderKey: "predicted", - }; - - const hooks = makeNodeHook(testHub, baseServerParams); - - describe("parseBody", () => { - it("exits when content-type is unsupported", async() => { - const request = createFakeRequest({ - headers: { "content-type": "application/xml" }, - }); - - await hooks.parseBody({ - request, - exit: exitHookFunction, - } as any); - - expect(request.body).toBeUndefined(); - }); - - it("parses text/plain body", async() => { - const request = createFakeRequest({ - headers: { "content-type": "text/plain" }, - }); - - const promise = hooks.parseBody({ - request, - exit: exitHookFunction, - } as any); - - request.raw.request.emit("data", "hello"); - request.raw.request.emit("end"); - - await promise; - - expect(request.body).toBe("hello"); - }); - - it("parses text/plain body when content-type is an array", async() => { - const request = createFakeRequest({ - headers: { "content-type": ["text/plain"] as any }, - }); - - const promise = hooks.parseBody({ - request, - exit: exitHookFunction, - } as any); - - request.raw.request.emit("data", "hello"); - request.raw.request.emit("end"); - - await promise; - - expect(request.body).toBe("hello"); - }); - - it("parses application/json body", async() => { - const request = createFakeRequest({ - headers: { "content-type": "application/json" }, - }); - - const promise = hooks.parseBody({ - request, - exit: exitHookFunction, - } as any); - - request.raw.request.emit("data", Buffer.from(JSON.stringify({ aa: 1 }))); - request.raw.request.emit("end"); - - await promise; - - expect(request.body).toStrictEqual({ aa: 1 }); - }); - - it("rejects when chunk type is invalid", async() => { - const request = createFakeRequest({ - headers: { "content-type": "text/plain" }, - }); - - const promise = hooks.parseBody!({ - request, - exit: exitHookFunction, - } as any); - - request.raw.request.emit("data", 12); - - await expect(promise).rejects.toBeInstanceOf(BodyParseWrongChunkReceived); - }); - - it("rejects when body exceeds max size", async() => { - const hook = makeNodeHook(testHub, { - ...baseServerParams, - maxBodySize: 5, - }); - - const request = createFakeRequest({ - headers: { "content-type": "text/plain" }, - }); - - const promise = hook.parseBody!({ - request, - exit: exitHookFunction, - } as any); - - request.raw.request.emit("data", "123456"); - - await expect(promise).rejects.toBeInstanceOf(BodySizeExceedsLimitError); - }); - - it("wraps unknown parse error", async() => { - const request = createFakeRequest({ - headers: { "content-type": "application/json" }, - }); - - const promise = hooks.parseBody!({ - request, - exit: vi.fn(), - } as any); - - request.raw.request.emit("data", "{invalid-json"); - request.raw.request.emit("end"); - - await expect(promise).rejects.toBeInstanceOf(BodyParseUnknownError); - }); - - it("exits immediately when content-type is missing", async() => { - const request = createFakeRequest(); - - await hooks.parseBody({ - request, - exit: exitHookFunction, - } as any); - - expect(request.body).toBeUndefined(); - }); - }); - - describe("error hook", () => { - it("handle BodySizeExceedsLimitError", () => { - const response = vi.fn(); - const exit = vi.fn(); - - hooks.error({ - error: new BodySizeExceedsLimitError(10), - response, - exit, - } as any); - - expect(response).toHaveBeenLastCalledWith( - "400", - "body-size-exceeds-limit-error", - expect.any(BodySizeExceedsLimitError), - ); - }); - - it("handle BodyParseWrongChunkReceived", () => { - const response = vi.fn(); - const exit = vi.fn(); - - hooks.error({ - error: new BodyParseWrongChunkReceived("oops"), - response, - exit, - } as any); - - expect(response).toHaveBeenLastCalledWith( - "400", - "body-parse-wrong-chunk-received", - expect.any(BodyParseWrongChunkReceived), - ); - - hooks.error({ - error: new BodyParseUnknownError("application/json", new Error("parse")), - response, - exit, - } as any); - expect(response).toHaveBeenLastCalledWith( - "400", - "body-parse-unknown-error", - expect.any(BodyParseUnknownError), - ); - }); - - it("omits error body when not in dev and exits for unknown errors", () => { - const hub = createHub({ environment: "PROD" }); - const prodHooks = makeNodeHook( - hub, - baseServerParams, - ); - const response = vi.fn(); - const exit = vi.fn(); - - prodHooks.error({ - error: new BodyParseUnknownError("application/json", new Error("parse")), - response, - exit, - } as any); - - expect(response).toHaveBeenLastCalledWith( - "400", - "body-parse-unknown-error", - undefined, - ); - - prodHooks.error({ - error: new Error("other"), - response, - exit, - } as any); - - expect(exit).toHaveBeenCalled(); - }); - }); - - describe("beforeSendResponse", () => { - it("sets headers and calls writeHead", () => { - const request = createFakeRequest(); - const writeHeadSpy = vi.spyOn(request.raw.response, "writeHead"); - const currentResponse = new PredictedResponse("200", "ok", { ok: true }); - - hooks.beforeSendResponse({ - request, - currentResponse, - exit: exitHookFunction, - } as any); - - expect(currentResponse.headers).toStrictEqual({ - "content-type": "application/json; charset=utf-8", - [baseServerParams.informationHeaderKey]: "ok", - [baseServerParams.predictedHeaderKey]: "1", - }); - expect(writeHeadSpy).toHaveBeenCalledWith( - 200, - currentResponse.headers, - ); - }); - - it("adds from-hook header for HookResponse", () => { - const request = createFakeRequest(); - const currentResponse = new HookResponse("parseBody", "200", "info", "data"); - - hooks.beforeSendResponse({ - request, - currentResponse, - exit: exitHookFunction, - } as any); - - expect(currentResponse.headers).toStrictEqual({ - "content-type": "text/plain; charset=utf-8", - [baseServerParams.informationHeaderKey]: "info", - [baseServerParams.fromHookHeaderKey]: "parseBody", - }); - }); - - it("sets text/plain for string body", () => { - const request = createFakeRequest(); - const currentResponse = new Response("200", "str", "value"); - - hooks.beforeSendResponse!({ - request, - currentResponse, - exit: exitHookFunction, - } as any); - - expect(currentResponse.headers).toStrictEqual({ - "content-type": "text/plain; charset=utf-8", - [baseServerParams.informationHeaderKey]: "str", - }); - }); - - it("not sets text/plain for string body", () => { - const request = createFakeRequest(); - const currentResponse = new Response("200", "str", "value") - .setHeader("content-type", "text/html"); - - hooks.beforeSendResponse!({ - request, - currentResponse, - exit: exitHookFunction, - } as any); - - expect(currentResponse.headers).toStrictEqual({ - "content-type": "text/html", - [baseServerParams.informationHeaderKey]: "str", - }); - }); - - it("un support body value", () => { - const request = createFakeRequest(); - const currentResponse = new Response("200", "fnc", (() => {})); - - hooks.beforeSendResponse!({ - request, - currentResponse, - exit: exitHookFunction, - } as any); - - expect(currentResponse.headers).toStrictEqual({ - [baseServerParams.informationHeaderKey]: "fnc", - }); - }); - }); - - describe("sendResponse", () => { - it("writes object body", () => { - const request = createFakeRequest(); - - hooks.sendResponse!({ - request, - currentResponse: { body: { ok: true } }, - exit: exitHookFunction, - } as any); - - expect(request.raw.response._getData()).toBe(JSON.stringify({ ok: true })); - expect(request.raw.response._isEndCalled()).toBe(true); - }); - - it("writes string body", () => { - const request = createFakeRequest(); - - hooks.sendResponse!({ - request, - currentResponse: { body: "text" }, - exit: exitHookFunction, - } as any); - - expect(request.raw.response._getData()).toBe("text"); - expect(request.raw.response._isEndCalled()).toBe(true); - }); - - it("writes number body", () => { - const request = createFakeRequest(); - - hooks.sendResponse!({ - request, - currentResponse: { body: 42 }, - exit: exitHookFunction, - } as any); - - expect(request.raw.response._getData()).toBe("42"); - expect(request.raw.response._isEndCalled()).toBe(true); - }); - - it("writes error body", () => { - const request = createFakeRequest(); - - hooks.sendResponse!({ - request, - currentResponse: { body: new Error("boom") }, - exit: exitHookFunction, - } as any); - - expect(request.raw.response._getData()).toBe("Error: boom"); - expect(request.raw.response._isEndCalled()).toBe(true); - }); - - it("writes nothing for unsupported types", () => { - const request = createFakeRequest(); - - hooks.sendResponse!({ - request, - currentResponse: { body: (() => {}) }, - exit: exitHookFunction, - } as any); - - expect(request.raw.response._getData()).toBe(""); - expect(request.raw.response._isEndCalled()).toBe(true); - }); - }); -}); diff --git a/tests/interfaces/node/hooks/index.test.ts b/tests/interfaces/node/hooks/index.test.ts new file mode 100644 index 0000000..ae56f6c --- /dev/null +++ b/tests/interfaces/node/hooks/index.test.ts @@ -0,0 +1,177 @@ +import { fsSpy } from "@test-utils/fs"; +import { type HttpServerParams, Response, exitHookFunction, nextHookFunction } from "@core"; +import { createFakeRequest } from "@test-utils/request"; +import { nodeHook } from "@interface-node"; +import { setEnvironment, SF, TESTImplementation } from "@duplojs/server-utils"; +import { EventEmitter } from "stream"; + +describe("makeNodeHook", () => { + beforeEach(() => { + setEnvironment("TEST"); + TESTImplementation.clear(); + }); + const baseServerParams: HttpServerParams = { + interface: "node", + host: "localhost", + port: 3000, + maxBodySize: 100, + informationHeaderKey: "information", + fromHookHeaderKey: "from-hook", + predictedHeaderKey: "predicted", + uploadFolder: "./upload", + }; + + const hooks = nodeHook; + + describe("beforeSendResponse", () => { + it("sets headers and calls writeHead", () => { + const request = createFakeRequest(); + const writeHeadSpy = vi.spyOn(request.raw.response, "writeHead"); + const currentResponse = new Response("205", "ok", { ok: true }) + .setHeader("content-type", "application/json; charset=utf-8"); + + hooks.beforeSendResponse({ + request, + currentResponse, + exit: exitHookFunction, + } as any); + + expect(writeHeadSpy).toHaveBeenCalledWith( + 205, + { + "content-type": "application/json; charset=utf-8", + }, + ); + }); + }); + + describe("sendResponse", () => { + it("writes object body", async() => { + const request = createFakeRequest(); + + await hooks.sendResponse!({ + request, + currentResponse: { body: { ok: true } }, + exit: exitHookFunction, + } as any); + + expect(request.raw.response._getData()).toBe(JSON.stringify({ ok: true })); + expect(request.raw.response._isEndCalled()).toBe(true); + }); + + it("writes string body", async() => { + const request = createFakeRequest(); + + await hooks.sendResponse!({ + request, + currentResponse: { body: "text" }, + exit: exitHookFunction, + } as any); + + expect(request.raw.response._getData()).toBe("text"); + expect(request.raw.response._isEndCalled()).toBe(true); + }); + + it("writes number body", async() => { + const request = createFakeRequest(); + + await hooks.sendResponse!({ + request, + currentResponse: { body: 42 }, + exit: exitHookFunction, + } as any); + + expect(request.raw.response._getData()).toBe("42"); + expect(request.raw.response._isEndCalled()).toBe(true); + }); + + it("writes error body", async() => { + const request = createFakeRequest(); + + await hooks.sendResponse!({ + request, + currentResponse: { body: new Error("boom") }, + exit: exitHookFunction, + } as any); + + expect(request.raw.response._getData()).toBe("Error: boom"); + expect(request.raw.response._isEndCalled()).toBe(true); + }); + + it("writes nothing for unsupported types", async() => { + const request = createFakeRequest(); + + await hooks.sendResponse!({ + request, + currentResponse: { body: (() => {}) }, + exit: exitHookFunction, + } as any); + + expect(request.raw.response._getData()).toBe(""); + expect(request.raw.response._isEndCalled()).toBe(true); + }); + + it("writes file body", async() => { + const request = createFakeRequest({ + raw: { + response: { + eventEmitter: EventEmitter, + }, + }, + }); + + fsSpy.createReadStream.mockImplementation(() => ({ + pipe: (arg: any) => { + expect(arg).toBe(request.raw.response); + }, + })); + + setTimeout(() => { + request.raw.response.emit("close"); + }); + + await hooks.sendResponse!({ + request, + currentResponse: { body: SF.createFileInterface("test.txt") }, + exit: exitHookFunction, + } as any); + + expect(fsSpy.createReadStream).toBeCalledWith("test.txt"); + }); + }); + + describe("afterSendResponse", () => { + it("delete filesAttache", async() => { + const spy = TESTImplementation.set("remove", vi.fn()); + const request = createFakeRequest(); + + request.filesAttache = ["test.txt", "tot.png"]; + + await hooks.afterSendResponse!({ + request, + next: nextHookFunction, + } as any); + + expect(spy).toHaveBeenNthCalledWith( + 1, + "test.txt", + ); + expect(spy).toHaveBeenNthCalledWith( + 2, + "tot.png", + ); + }); + + it("not delete filesAttache", async() => { + const spy = TESTImplementation.set("remove", vi.fn()); + const request = createFakeRequest(); + + await hooks.afterSendResponse!({ + request, + next: nextHookFunction, + } as any); + + expect(spy).not.toHaveBeenCalled(); + }); + }); +}); diff --git a/tests/interfaces/node/tsconfig.json b/tests/interfaces/node/tsconfig.json index 921691a..18278ba 100644 --- a/tests/interfaces/node/tsconfig.json +++ b/tests/interfaces/node/tsconfig.json @@ -12,6 +12,6 @@ }, "include": [ "**/*.ts", - "../../../scripts/interfaces/node/**/*.ts", + "../../../scripts/interfaces/node/**/*.ts", "../../../scripts/core/errors/parseJsonError.ts", ], } diff --git a/tests/plugins/codeGenerator/__snapshots__/routeToDataParser.test.ts.snap b/tests/plugins/codeGenerator/__snapshots__/routeToDataParser.test.ts.snap new file mode 100644 index 0000000..4446418 --- /dev/null +++ b/tests/plugins/codeGenerator/__snapshots__/routeToDataParser.test.ts.snap @@ -0,0 +1,7 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`routeToDataParser > bodyAsFormData 1`] = ` +"import type { TheFormData } from "@duplojs/utils"; + +export type ArrayString = TheFormData;" +`; diff --git a/tests/plugins/codeGenerator/__snapshots__/typescriptTransfomer.test.ts.snap b/tests/plugins/codeGenerator/__snapshots__/typescriptTransfomer.test.ts.snap new file mode 100644 index 0000000..9540c07 --- /dev/null +++ b/tests/plugins/codeGenerator/__snapshots__/typescriptTransfomer.test.ts.snap @@ -0,0 +1,3 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`typescript transformer > file 1`] = `"export type ArrayString = File[];"`; diff --git a/tests/plugins/codeGenerator/plugin.test.ts b/tests/plugins/codeGenerator/plugin.test.ts index 2489c8a..cdaa6fb 100644 --- a/tests/plugins/codeGenerator/plugin.test.ts +++ b/tests/plugins/codeGenerator/plugin.test.ts @@ -1,18 +1,15 @@ import { createHub, launchHookServer, ResponseContract, useRouteBuilder } from "@core"; -import { DPE } from "@duplojs/utils"; +import { DPE, E } from "@duplojs/utils"; +import { TESTImplementation, setEnvironment } from "@duplojs/server-utils"; import { codeGeneratorPlugin } from "@plugin-codeGenerator"; -import { testHub } from "@test-utils/hub"; -import { type Mock } from "vitest"; - -vi.mock("node:fs/promises", () => ({ - writeFile: vi.fn(), -})); - -const { writeFile } = await import("node:fs/promises"); describe("plugin implementation", () => { + setEnvironment("TEST"); + const spy = vi.fn((path: string, content: string) => Promise.resolve(E.ok())); + TESTImplementation.set("writeTextFile", spy); + beforeEach(() => { - (writeFile as Mock).mockClear(); + spy.mockClear(); }); const route = useRouteBuilder("GET", "/user") @@ -36,31 +33,31 @@ describe("plugin implementation", () => { ); it("generate API type", async() => { - const hub = testHub + const hub = createHub({ environment: "DEV" }) .plug(codeGeneratorPlugin({ outputFile: "test.d.ts" })) .register(route); await launchHookServer( hub.aggregatesHooksHubLifeCycle("beforeStartServer"), hub, - {}, + {} as any, ); - expect((writeFile as Mock).mock.lastCall?.at(0)).toBe("test.d.ts"); - expect((writeFile as Mock).mock.lastCall?.at(1)).toMatchSnapshot(); + expect(spy.mock.lastCall?.at(0)).toBe("test.d.ts"); + expect(spy.mock.lastCall?.at(1)).toMatchSnapshot(); }); it("not generate API type", async() => { - const hub = testHub + const hub = createHub({ environment: "DEV" }) .plug(codeGeneratorPlugin({ outputFile: "test.d.ts" })); await launchHookServer( hub.aggregatesHooksHubLifeCycle("beforeStartServer"), hub, - {}, + {} as any, ); - expect(writeFile).not.toHaveBeenCalled(); + expect(spy).not.toHaveBeenCalled(); }); it("not generate in PROD env", async() => { @@ -71,9 +68,9 @@ describe("plugin implementation", () => { await launchHookServer( hub.aggregatesHooksHubLifeCycle("beforeStartServer"), hub, - {}, + {} as any, ); - expect(writeFile).not.toHaveBeenCalled(); + expect(spy).not.toHaveBeenCalled(); }); }); diff --git a/tests/plugins/codeGenerator/routeToDataParser.test.ts b/tests/plugins/codeGenerator/routeToDataParser.test.ts index 08a38b1..8b97445 100644 --- a/tests/plugins/codeGenerator/routeToDataParser.test.ts +++ b/tests/plugins/codeGenerator/routeToDataParser.test.ts @@ -1,8 +1,11 @@ -import { defaultExtractContract, ResponseContract, useProcessBuilder, useRouteBuilder } from "@core"; -import { DP, DPE } from "@duplojs/utils"; +import { controlBodyAsFormData, defaultExtractContract, ResponseContract, useProcessBuilder, useRouteBuilder } from "@core"; +import { DP, DPE, E } from "@duplojs/utils"; import { testPresetChecker } from "@test-utils/presetChecker"; import { omitFunctions } from "@test-utils/omitFunction"; -import { IgnoreByCodeGeneratorMetadata, routeToDataParser } from "@plugin-codeGenerator"; +import { bodyAsFormData, convertRoutePath, IgnoreByCodeGeneratorMetadata, routeToDataParser } from "@plugin-codeGenerator"; +import { SDPE } from "@duplojs/server-utils"; +import { defaultTransformers, render } from "@duplojs/data-parser-tools/toTypescript"; +import { fileTransformer } from "@plugin-codeGenerator/typescriptTransfomer"; describe("routeToDataParser", () => { const process1 = useProcessBuilder() @@ -50,7 +53,6 @@ describe("routeToDataParser", () => { body: DP.object({ prop1: DPE.string(), prop2: DPE.number(), - }), headers: DP.object({ header3: DPE.string(), @@ -105,4 +107,61 @@ describe("routeToDataParser", () => { expect(result).toStrictEqual([]); }); + + it("convertRoutePath", () => { + expect( + omitFunctions( + [ + convertRoutePath("/test/*"), + convertRoutePath("/test"), + convertRoutePath("/test-*/ok"), + ], + ), + ).toStrictEqual( + omitFunctions([ + DP.templateLiteral(["/test/", DP.string(), ""]), + DP.literal("/test"), + DP.templateLiteral(["/test-", DP.string(), "/ok"]), + ]), + ); + }); + + it("", () => { + const route = useRouteBuilder("GET", "/test", { bodyController: controlBodyAsFormData({ maxFileQuantity: 1 }) }) + .extract({ + body: DP.object({ + test: DP.string(), + }), + }) + .handler( + ResponseContract.noContent("test"), + (__, { response }) => response("test"), + ); + + const result = routeToDataParser( + route, + { defaultExtractContract }, + ); + + expect( + (result as any)[0]?.definition.shape.body.definition.overrideTypescriptTransformer, + ).toStrictEqual(bodyAsFormData); + }); + + it("bodyAsFormData", () => { + expect( + render( + DPE.array(SDPE.file()).addOverrideTypescriptTransformer(bodyAsFormData), + { + identifier: "ArrayString", + transformers: [fileTransformer, ...defaultTransformers], + mode: "out", + }, + ), + ).toMatchSnapshot(); + + expect( + bodyAsFormData(SDPE.file(), { transformer: () => E.left("test") } as never), + ).toStrictEqual(E.left("test")); + }); }); diff --git a/tests/plugins/codeGenerator/typescriptTransfomer.test.ts b/tests/plugins/codeGenerator/typescriptTransfomer.test.ts new file mode 100644 index 0000000..371a2de --- /dev/null +++ b/tests/plugins/codeGenerator/typescriptTransfomer.test.ts @@ -0,0 +1,19 @@ +import { defaultTransformers, render } from "@duplojs/data-parser-tools/toTypescript"; +import { SDPE } from "@duplojs/server-utils"; +import { DPE } from "@duplojs/utils"; +import { fileTransformer } from "@plugin-codeGenerator/typescriptTransfomer"; + +describe("typescript transformer", () => { + it("file", () => { + expect( + render( + DPE.array(SDPE.file()), + { + identifier: "ArrayString", + transformers: [fileTransformer, ...defaultTransformers], + mode: "out", + }, + ), + ).toMatchSnapshot(); + }); +}); diff --git a/tests/plugins/openApiGenerator/makeOpenApiRoute.test.ts b/tests/plugins/openApiGenerator/makeOpenApiRoute.test.ts index f043c17..dfc36b7 100644 --- a/tests/plugins/openApiGenerator/makeOpenApiRoute.test.ts +++ b/tests/plugins/openApiGenerator/makeOpenApiRoute.test.ts @@ -1,6 +1,7 @@ import { makeOpenApiPage, makeOpenApiRoute } from "@plugin-openApiGenerator"; import { useTestRouteFunctionBuilder } from "@test-utils/useTestRouteFunctionBuilder"; -import { PredictedResponse, Request, Response } from "@core"; +import { PredictedResponse, Request } from "@core"; +import { createBodyReader } from "@test-utils/bodyReader"; describe("makeOpenApiRoute", () => { const spyResponse = vi.fn(); @@ -47,6 +48,7 @@ describe("makeOpenApiRoute", () => { params: {}, query: {}, url: "", + bodyReader: createBodyReader(), }), ); diff --git a/tests/plugins/openApiGenerator/plugin.test.ts b/tests/plugins/openApiGenerator/plugin.test.ts index 55b0d3f..a10c61c 100644 --- a/tests/plugins/openApiGenerator/plugin.test.ts +++ b/tests/plugins/openApiGenerator/plugin.test.ts @@ -1,18 +1,15 @@ import { createHub, launchHookServer, ResponseContract, useRouteBuilder } from "@core"; -import { DPE } from "@duplojs/utils"; +import { setEnvironment, TESTImplementation } from "@duplojs/server-utils"; +import { DPE, E } from "@duplojs/utils"; import { openApiGeneratorPlugin } from "@plugin-openApiGenerator"; -import { testHub } from "@test-utils/hub"; -import { type Mock } from "vitest"; - -vi.mock("node:fs/promises", () => ({ - writeFile: vi.fn(), -})); - -const { writeFile } = await import("node:fs/promises"); describe("plugin implementation", () => { + setEnvironment("TEST"); + const spy = vi.fn((path: string, content: string) => Promise.resolve(E.ok())); + TESTImplementation.set("writeTextFile", spy); + beforeEach(() => { - (writeFile as Mock).mockClear(); + spy.mockClear(); }); const route = useRouteBuilder("GET", "/user") @@ -36,7 +33,7 @@ describe("plugin implementation", () => { ); it("generate OpenApi file", async() => { - const hub = testHub + const hub = createHub({ environment: "DEV" }) .plug(openApiGeneratorPlugin({ outputFile: "swagger.json", })) @@ -45,15 +42,15 @@ describe("plugin implementation", () => { await launchHookServer( hub.aggregatesHooksHubLifeCycle("beforeServerBuildRoutes"), hub, - {}, + {} as any, ); - expect((writeFile as Mock).mock.lastCall?.at(0)).toBe("swagger.json"); - expect((writeFile as Mock).mock.lastCall?.at(1)).toMatchSnapshot(); + expect(spy.mock.lastCall?.at(0)).toBe("swagger.json"); + expect(spy.mock.lastCall?.at(1)).toMatchSnapshot(); }); it("generate OpenApi file with type bearer ok security option", async() => { - const hub = testHub + const hub = createHub({ environment: "DEV" }) .plug( openApiGeneratorPlugin({ outputFile: "swagger.json", @@ -66,15 +63,15 @@ describe("plugin implementation", () => { await launchHookServer( hub.aggregatesHooksHubLifeCycle("beforeServerBuildRoutes"), hub, - {}, + {} as any, ); - expect((writeFile as Mock).mock.lastCall?.at(0)).toBe("swagger.json"); - expect((writeFile as Mock).mock.lastCall?.at(1)).toMatchSnapshot(); + expect(spy.mock.lastCall?.at(0)).toBe("swagger.json"); + expect(spy.mock.lastCall?.at(1)).toMatchSnapshot(); }); it("generate OpenApi file with type apiKey ok security option", async() => { - const hub = testHub + const hub = createHub({ environment: "DEV" }) .plug(openApiGeneratorPlugin({ outputFile: "swagger.json", routePath: "/swagger", @@ -89,15 +86,15 @@ describe("plugin implementation", () => { await launchHookServer( hub.aggregatesHooksHubLifeCycle("beforeServerBuildRoutes"), hub, - {}, + {} as any, ); - expect((writeFile as Mock).mock.lastCall?.at(0)).toBe("swagger.json"); - expect((writeFile as Mock).mock.lastCall?.at(1)).toMatchSnapshot(); + expect(spy.mock.lastCall?.at(0)).toBe("swagger.json"); + expect(spy.mock.lastCall?.at(1)).toMatchSnapshot(); }); it("not generate OpenApi file", async() => { - const hub = testHub + const hub = createHub({ environment: "DEV" }) .plug(openApiGeneratorPlugin({ routePath: "/swagger", })) @@ -106,14 +103,14 @@ describe("plugin implementation", () => { await launchHookServer( hub.aggregatesHooksHubLifeCycle("beforeServerBuildRoutes"), hub, - {}, + {} as any, ); - expect(writeFile).not.toHaveBeenCalled(); + expect(spy).not.toHaveBeenCalled(); }); it("empty route", async() => { - const hub = testHub + const hub = createHub({ environment: "DEV" }) .plug(openApiGeneratorPlugin({ outputFile: "./swagger.json", })); @@ -121,36 +118,23 @@ describe("plugin implementation", () => { await launchHookServer( hub.aggregatesHooksHubLifeCycle("beforeServerBuildRoutes"), hub, - {}, - ); - - expect(writeFile).not.toHaveBeenCalled(); - }); - - it("empty params", async() => { - const hub = testHub - .plug(openApiGeneratorPlugin({})); - - await launchHookServer( - hub.aggregatesHooksHubLifeCycle("beforeServerBuildRoutes"), - hub, - {}, + {} as any, ); - expect(writeFile).not.toHaveBeenCalled(); + expect(spy).not.toHaveBeenCalled(); }); it("empty params", async() => { - const hub = testHub + const hub = createHub({ environment: "DEV" }) .plug(openApiGeneratorPlugin({})); await launchHookServer( hub.aggregatesHooksHubLifeCycle("beforeServerBuildRoutes"), hub, - {}, + {} as any, ); - expect(writeFile).not.toHaveBeenCalled(); + expect(spy).not.toHaveBeenCalled(); }); it("not generate in PROD env", async() => { @@ -160,9 +144,9 @@ describe("plugin implementation", () => { await launchHookServer( hub.aggregatesHooksHubLifeCycle("beforeServerBuildRoutes"), hub, - {}, + {} as any, ); - expect(writeFile).not.toHaveBeenCalled(); + expect(spy).not.toHaveBeenCalled(); }); }); diff --git a/tests/plugins/openApiGenerator/routeToOpenApi.test.ts b/tests/plugins/openApiGenerator/routeToOpenApi.test.ts index f40f657..d248c4b 100644 --- a/tests/plugins/openApiGenerator/routeToOpenApi.test.ts +++ b/tests/plugins/openApiGenerator/routeToOpenApi.test.ts @@ -1,11 +1,12 @@ -import { defaultExtractContract, ResponseContract, useProcessBuilder, useRouteBuilder } from "@core"; +import { controlBodyAsFormData, defaultExtractContract, ResponseContract, useProcessBuilder, useRouteBuilder } from "@core"; import { DP, DPE } from "@duplojs/utils"; import { testPresetChecker } from "@test-utils/presetChecker"; import { omitFunctions } from "@test-utils/omitFunction"; import { IgnoreByOpenApiGeneratorMetadata, routeToOpenApi } from "@plugin-openApiGenerator"; +import { SDPE } from "@duplojs/server-utils"; describe("routeToOpenApi", () => { - it("empty result", () => { + it("request body application/json", () => { const process1 = useProcessBuilder() .extract({ headers: { @@ -282,6 +283,73 @@ describe("routeToOpenApi", () => { ); }); + it("request body multipart/form-data", () => { + const route = useRouteBuilder("GET", "/test", { bodyController: controlBodyAsFormData({ maxFileQuantity: 10 }) }) + .extract({ + body: { + superFile: SDPE.file(), + field: DPE.coerce.boolean(), + }, + }) + .handler( + ResponseContract.ok("handler", DP.string()), + (__, { response }) => response("handler", "handler"), + ); + + const result = routeToOpenApi( + route, + { + defaultExtractContract, + contextToJsonSchemaFactory: new Map(), + resultSchemaContext: new Map(), + }, + ); + + expect(result).toStrictEqual( + [ + { + path: "/test", + method: "get", + parameters: [], + requestBody: { + required: true, + content: { + "multipart/form-data": { schema: { $ref: "#/components/schemas/NotIdentified0" } }, + }, + }, + responses: { + 200: { + headers: { + information: { + schema: { + const: "handler", + type: "string", + }, + description: "handler", + }, + }, + content: { + "plain/text": { schema: { $ref: "#/components/schemas/NotIdentified1" } }, + }, + }, + 422: { + headers: { + information: { + schema: { + const: "extract-error", + type: "string", + }, + description: "extract-error", + }, + }, + content: undefined, + }, + }, + }, + ], + ); + }); + it("request body text/plain", () => { const route = useRouteBuilder("GET", "/test") .extract({