diff --git a/.commands/test-types.sh b/.commands/test-types.sh index 11bbad4..f2f3c27 100755 --- a/.commands/test-types.sh +++ b/.commands/test-types.sh @@ -15,6 +15,7 @@ tsc -p tests/plugins/codeGenerator/tsconfig.json tsc -p tests/plugins/openApiGenerator/tsconfig.json tsc -p tests/plugins/cacheController/tsconfig.json tsc -p tests/plugins/static/tsconfig.json +tsc -p tests/plugins/cors/tsconfig.json # integration npm -w integration run test:types # documentation diff --git a/docs/.vitepress/config.ts b/docs/.vitepress/config.ts index 530cac2..805f4b6 100644 --- a/docs/.vitepress/config.ts +++ b/docs/.vitepress/config.ts @@ -108,6 +108,15 @@ export default pipe( "// @filename: @duplojs/http/openApiGenerator.ts", `export * from "@v${namedGroups?.version ?? ""}/openApiGenerator";`, + "// @filename: @duplojs/http/static.ts", + `export * from "@v${namedGroups?.version ?? ""}/static";`, + + "// @filename: @duplojs/http/cors.ts", + `export * from "@v${namedGroups?.version ?? ""}/cors";`, + + "// @filename: @duplojs/http/cacheController.ts", + `export * from "@v${namedGroups?.version ?? ""}/cacheController";`, + "// @filename: index.ts", "// ---cut---", ], @@ -133,6 +142,9 @@ export default pipe( "@v0/node": ["libs/v0/interfaces/node/index"], "@v0/codeGenerator": ["libs/v0/plugins/codeGenerator/index"], "@v0/openApiGenerator": ["libs/v0/plugins/openApiGenerator/index"], + "@v0/static": ["libs/v0/plugins/static/index"], + "@v0/cors": ["libs/v0/plugins/cors/index"], + "@v0/cacheController": ["libs/v0/plugins/cacheController/index"], }, }, }, @@ -264,6 +276,18 @@ export default pipe( text: "Génération OpenAPI", link: "/fr/v0/guide/plugins/openApiGenerator", }, + { + text: "Créer un point d'entrée static", + link: "/fr/v0/guide/plugins/static", + }, + { + text: "Gestion CORS", + link: "/fr/v0/guide/plugins/cors", + }, + { + text: "Contrôle du cache", + link: "/fr/v0/guide/plugins/cacheController", + }, ], }, { @@ -408,6 +432,18 @@ export default pipe( text: "OpenAPI generation", link: "/en/v0/guide/plugins/openApiGenerator", }, + { + text: "Create a static entry point", + link: "/fr/v0/guide/plugins/static", + }, + { + text: "CORS Management", + link: "/fr/v0/guide/plugins/cors", + }, + { + text: "Cache Control", + link: "/fr/v0/guide/plugins/cacheController", + }, ], }, { diff --git a/docs/en/v0/guide/features/formData.md b/docs/en/v0/guide/features/formData.md index f637a92..ca127eb 100644 --- a/docs/en/v0/guide/features/formData.md +++ b/docs/en/v0/guide/features/formData.md @@ -1,8 +1,8 @@ --- description: "Handle complex FormData payloads." prev: - text: "OpenAPI generation" - link: "/en/v0/guide/plugins/openApiGenerator" + text: "Cache control" + link: "/en/v0/guide/plugins/cacheController" next: text: "Server-Sent Events (SSE)" link: "/en/v0/guide/features/SSE" diff --git a/docs/en/v0/guide/plugins/cacheController.md b/docs/en/v0/guide/plugins/cacheController.md new file mode 100644 index 0000000..72bbd77 --- /dev/null +++ b/docs/en/v0/guide/plugins/cacheController.md @@ -0,0 +1,63 @@ +--- +description: "Manage Cache-Control headers." +prev: + text: "CORS handling" + link: "/en/v0/guide/plugins/cors" +next: + text: "Advanced FormData" + link: "/en/v0/guide/features/formData" +--- + +# Cache control + +`@duplojs/http/cacheController` provides `createCacheControllerHooks`, a hook that automatically adds the [`Cache-Control`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Cache-Control) header to matching responses. + +The hook only adds the header to `2xx` and `3xx` responses. + +## On one route + +```ts twoslash {2,12-16} +// @version: 0 + +``` + +In this case, the configuration only applies to the `GET /articles` route. + +- `public: true` allows shared caching. +- `maxAge: 300` allows a 5-minute cache. +- `staleWhileRevalidate: 60` lets clients serve a slightly stale response while revalidating. + +## On all Hub routes + +```ts twoslash {2,5-10} +// @version: 0 + +``` + +Here, the hook is added globally with `addRouteHooks`. Every route registered in this `Hub` inherits the same cache policy. + +## Config interface + +```ts +interface CacheControlDirectives { + maxAge?: number; + sMaxAge?: number; + public?: true; + private?: true | string[]; + noCache?: true | string[]; + noStore?: true; + noTransform?: true; + mustRevalidate?: true; + proxyRevalidate?: true; + immutable?: true; + staleWhileRevalidate?: number; + staleIfError?: number; + mustUnderstand?: true; + extensions?: Record; +} +``` + +## Useful notes + +- `private` and `noCache` can also receive a list of header names. +- `extensions` lets you add custom directives when you need a more specific header. diff --git a/docs/en/v0/guide/plugins/cors.md b/docs/en/v0/guide/plugins/cors.md new file mode 100644 index 0000000..29a3e3a --- /dev/null +++ b/docs/en/v0/guide/plugins/cors.md @@ -0,0 +1,58 @@ +--- +description: "Handle CORS headers." +prev: + text: "Create a static entry point" + link: "/en/v0/guide/plugins/static" +next: + text: "Cache control" + link: "/en/v0/guide/plugins/cacheController" +--- + +# CORS handling + +`@duplojs/http/cors` automatically adds [CORS](https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/CORS) headers to regular responses and creates an `OPTIONS /*` route to handle [preflight requests](https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/CORS#functional_overview). + +## Full example + +```ts twoslash {2,6-16} +// @version: 0 + +``` + +In this example: + +- `allowOrigin` restricts which origins are allowed. +- `allowHeaders` declares which [HTTP headers](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers) are accepted during preflight. +- `exposeHeaders` makes selected headers readable by the browser. +- `allowMethods: true` automatically generates `access-control-allow-methods` from the routes registered in the `Hub`. +- `credentials: true` adds `access-control-allow-credentials: true` for [credentials](https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/CORS#requests_with_credentials). +- `maxAge` defines the preflight cache duration in seconds. + +## Params interface + +```ts +interface CorsPluginParams { + readonly allowOrigin?: + | string + | RegExp + | readonly string[] + | ((origin: string) => boolean | Promise) + | true; + readonly allowHeaders?: string | readonly string[] | true; + readonly exposeHeaders?: string | readonly string[]; + readonly maxAge?: number; + readonly credentials?: boolean; + readonly allowMethods?: RequestMethods | readonly RequestMethods[] | true; +} +``` + +You must provide at least one option to the plugin. + +## Option details + +- `allowOrigin` accepts a single origin, a list, a regular expression, a validation function, or `true` to allow all origins. +- `allowHeaders` accepts a string, a list, or `true` for all headers. +- `exposeHeaders` lists the headers the browser can read after the request. +- `allowMethods` accepts one method, a list, or `true` to compute allowed methods automatically per path. +- `credentials` enables cookies and other authentication credentials to be shared. +- `maxAge` controls how long the `OPTIONS` response can be cached. diff --git a/docs/en/v0/guide/plugins/openApiGenerator.md b/docs/en/v0/guide/plugins/openApiGenerator.md index 241fe23..feca1c6 100644 --- a/docs/en/v0/guide/plugins/openApiGenerator.md +++ b/docs/en/v0/guide/plugins/openApiGenerator.md @@ -4,8 +4,8 @@ prev: text: "Code generation" link: "/en/v0/guide/plugins/codeGenerator" next: - text: "Advanced FormData" - link: "/en/v0/guide/features/formData" + text: "Create a static entry point" + link: "/en/v0/guide/plugins/static" --- # OpenAPI generation diff --git a/docs/en/v0/guide/plugins/static.md b/docs/en/v0/guide/plugins/static.md new file mode 100644 index 0000000..f337988 --- /dev/null +++ b/docs/en/v0/guide/plugins/static.md @@ -0,0 +1,56 @@ +--- +description: "Create a static entry point easily." +prev: + text: "OpenAPI generation" + link: "/en/v0/guide/plugins/openApiGenerator" +next: + text: "CORS handling" + link: "/en/v0/guide/plugins/cors" +--- + +# Create a static entry point + +`@duplojs/http/static` lets you expose a file or a folder in read-only mode. +You can either plug `staticPlugin` directly into the `Hub`, or manually register a route with `makeRouteFile` or `makeRouteFolder`. + +## With `staticPlugin` + +```ts twoslash {3,9-22} +// @version: 0 + +``` + +The plugin automatically chooses the right behavior based on the source you pass. + +- `source` is a `FileInterface` or a `FolderInterface`. +- `path` exposes a single file on a specific route. +- `prefix` exposes a whole folder under a URL prefix. +- `cacheControlConfig` adds `cache-control` headers. +- `directoryFallBackFile` serves a default file for a folder, for example `index.html`. + +## With `makeRouteFile` + +```ts twoslash {5-12} +// @version: 0 + +``` + +Use this function when you want to register a static route yourself for a single file. + +- `source` is the file to serve. +- `path` is the HTTP route to expose. +- `cacheControlConfig` is optional. + +## With `makeRouteFolder` + +```ts twoslash {5-13} +// @version: 0 + +``` + +This version serves every file in a folder from a shared prefix. + +- `source` is the root folder. +- `prefix` is the URL prefix used to resolve files. +- `directoryFallBackFile` serves a default file for a folder, for example `index.html`. +- `cacheControlConfig` is optional. diff --git a/docs/examples/v0/guide/plugins/cacheController/hub.ts b/docs/examples/v0/guide/plugins/cacheController/hub.ts new file mode 100644 index 0000000..566ff0a --- /dev/null +++ b/docs/examples/v0/guide/plugins/cacheController/hub.ts @@ -0,0 +1,10 @@ +import { createHub } from "@duplojs/http"; +import { createCacheControllerHooks } from "@duplojs/http/cacheController"; + +export const hub = createHub({ environment: "DEV" }) + .addRouteHooks( + createCacheControllerHooks({ + private: true, + noCache: true, + }), + ); diff --git a/docs/examples/v0/guide/plugins/cacheController/route.ts b/docs/examples/v0/guide/plugins/cacheController/route.ts new file mode 100644 index 0000000..2a040d7 --- /dev/null +++ b/docs/examples/v0/guide/plugins/cacheController/route.ts @@ -0,0 +1,27 @@ +import { ResponseContract, useRouteBuilder } from "@duplojs/http"; +import { createCacheControllerHooks } from "@duplojs/http/cacheController"; +import { DPE } from "@duplojs/utils"; + +const articleSchema = DPE.object({ + id: DPE.number(), + title: DPE.string(), +}); + +useRouteBuilder("GET", "/articles", { + hooks: [ + createCacheControllerHooks({ + public: true, + maxAge: 300, + staleWhileRevalidate: 60, + }), + ], +}) + .handler( + ResponseContract.ok("articles.list", articleSchema.array()), + (__, { response }) => response("articles.list", [ + { + id: 1, + title: "Hello", + }, + ]), + ); diff --git a/docs/examples/v0/guide/plugins/cors/plugin.ts b/docs/examples/v0/guide/plugins/cors/plugin.ts new file mode 100644 index 0000000..0135ddf --- /dev/null +++ b/docs/examples/v0/guide/plugins/cors/plugin.ts @@ -0,0 +1,17 @@ +import { createHub } from "@duplojs/http"; +import { corsPlugin } from "@duplojs/http/cors"; + +export const hub = createHub({ environment: "DEV" }) + .plug( + corsPlugin({ + allowOrigin: [ + "https://app.example.com", + "https://admin.example.com", + ], + allowHeaders: ["content-type", "authorization"], + exposeHeaders: ["x-request-id", "x-rate-limit-remaining"], + allowMethods: true, + credentials: true, + maxAge: 600, + }), + ); diff --git a/docs/examples/v0/guide/plugins/static/makeRouteFile.ts b/docs/examples/v0/guide/plugins/static/makeRouteFile.ts new file mode 100644 index 0000000..c5e8c39 --- /dev/null +++ b/docs/examples/v0/guide/plugins/static/makeRouteFile.ts @@ -0,0 +1,15 @@ +import { createHub } from "@duplojs/http"; +import { SF } from "@duplojs/server-utils"; +import { makeRouteFile } from "@duplojs/http/static"; + +const faviconRoute = makeRouteFile({ + source: SF.createFileInterface("./public/favicon.ico"), + path: "/favicon.ico", + cacheControlConfig: { + maxAge: 86400, + public: true, + }, +}); + +export const hub = createHub({ environment: "DEV" }) + .register(faviconRoute); diff --git a/docs/examples/v0/guide/plugins/static/makeRouteFolder.ts b/docs/examples/v0/guide/plugins/static/makeRouteFolder.ts new file mode 100644 index 0000000..7855c82 --- /dev/null +++ b/docs/examples/v0/guide/plugins/static/makeRouteFolder.ts @@ -0,0 +1,16 @@ +import { createHub } from "@duplojs/http"; +import { SF } from "@duplojs/server-utils"; +import { makeRouteFolder } from "@duplojs/http/static"; + +const docsRoute = makeRouteFolder({ + source: SF.createFolderInterface("./public/docs"), + prefix: "/docs", + directoryFallBackFile: "index.html", + cacheControlConfig: { + maxAge: 600, + public: true, + }, +}); + +export const hub = createHub({ environment: "DEV" }) + .register(docsRoute); diff --git a/docs/examples/v0/guide/plugins/static/plugin.ts b/docs/examples/v0/guide/plugins/static/plugin.ts new file mode 100644 index 0000000..7c8950c --- /dev/null +++ b/docs/examples/v0/guide/plugins/static/plugin.ts @@ -0,0 +1,22 @@ +import { createHub } from "@duplojs/http"; +import { SF } from "@duplojs/server-utils"; +import { staticPlugin } from "@duplojs/http/static"; + +const logoFile = SF.createFileInterface("./public/logo.svg"); +const assetsFolder = SF.createFolderInterface("./public"); + +export const hub = createHub({ environment: "DEV" }) + .plug( + staticPlugin(logoFile, { + path: "/logo", + cacheControlConfig: { + maxAge: 3600, + public: true, + }, + }), + ) + .plug( + staticPlugin(assetsFolder, { + prefix: "/assets", + }), + ); diff --git a/docs/fr/v0/guide/features/formData.md b/docs/fr/v0/guide/features/formData.md index 0d94cc4..4cc8c7c 100644 --- a/docs/fr/v0/guide/features/formData.md +++ b/docs/fr/v0/guide/features/formData.md @@ -1,8 +1,8 @@ --- description: "Gérer des FormData à structure complexe." prev: - text: "Génération OpenAPI" - link: "/fr/v0/guide/plugins/openApiGenerator" + text: "Contrôle du cache" + link: "/fr/v0/guide/plugins/cacheController" next: text: "Server-Sent Events (SSE)" link: "/fr/v0/guide/features/SSE" diff --git a/docs/fr/v0/guide/plugins/cacheController.md b/docs/fr/v0/guide/plugins/cacheController.md new file mode 100644 index 0000000..8a4662c --- /dev/null +++ b/docs/fr/v0/guide/plugins/cacheController.md @@ -0,0 +1,63 @@ +--- +description: "Gérer les entêtes Cache-Control" +prev: + text: "Gestion CORS" + link: "/fr/v0/guide/plugins/cors" +next: + text: "FormData avancés" + link: "/fr/v0/guide/features/formData" +--- + +# Contrôle du cache + +`@duplojs/http/cacheController` fournit `createCacheControllerHooks`, un hook qui ajoute automatiquement l'entête [`Cache-Control`](https://developer.mozilla.org/fr/docs/Web/HTTP/Reference/Headers/Cache-Control) sur les réponses concernées. + +Le hook n'ajoute l'entête que sur les réponses de statut `2xx` et `3xx`. + +## Sur une route + +```ts twoslash {2,12-16} +// @version: 0 + +``` + +Dans ce cas, la configuration ne s'applique qu'à la route `GET /articles`. + +- `public: true` autorise le cache partagé. +- `maxAge: 300` autorise un cache de 5 minutes. +- `staleWhileRevalidate: 60` permet de servir une réponse légèrement expirée pendant une revalidation. + +## Sur toutes les routes du Hub + +```ts twoslash {2,5-10} +// @version: 0 + +``` + +Ici, le hook est ajouté globalement avec `addRouteHooks`. Toutes les routes enregistrées dans ce `Hub` héritent donc de la même politique de cache. + +## Interface de configuration + +```ts +interface CacheControlDirectives { + maxAge?: number; + sMaxAge?: number; + public?: true; + private?: true | string[]; + noCache?: true | string[]; + noStore?: true; + noTransform?: true; + mustRevalidate?: true; + proxyRevalidate?: true; + immutable?: true; + staleWhileRevalidate?: number; + staleIfError?: number; + mustUnderstand?: true; + extensions?: Record; +} +``` + +## Notes utiles + +- `private` et `noCache` peuvent aussi recevoir une liste de noms d'entêtes. +- `extensions` permet d'ajouter des directives personnalisées si vous devez produire un header plus spécifique. diff --git a/docs/fr/v0/guide/plugins/cors.md b/docs/fr/v0/guide/plugins/cors.md new file mode 100644 index 0000000..4901887 --- /dev/null +++ b/docs/fr/v0/guide/plugins/cors.md @@ -0,0 +1,58 @@ +--- +description: "Gérer les entêtes CORS" +prev: + text: "Créer un point d'entrée static" + link: "/fr/v0/guide/plugins/static" +next: + text: "Contrôle du cache" + link: "/fr/v0/guide/plugins/cacheController" +--- + +# Gestion CORS + +`@duplojs/http/cors` ajoute automatiquement les entêtes [CORS](https://developer.mozilla.org/fr/docs/Web/HTTP/Guides/CORS) sur les réponses classiques et crée une route `OPTIONS /*` pour répondre aux [preflight request](https://developer.mozilla.org/fr/docs/Web/HTTP/Guides/CORS#aper%C3%A7u_fonctionnel). + +## Exemple complet + +```ts twoslash {2,6-16} +// @version: 0 + +``` + +Dans cet exemple : + +- `allowOrigin` limite les origines autorisées. +- `allowHeaders` déclare les [entêtes HTTP](https://developer.mozilla.org/fr/docs/Web/HTTP/Reference/Headers) acceptés pendant la pré-vérification. +- `exposeHeaders` rend certains entêtes lisibles côté navigateur. +- `allowMethods: true` génère automatiquement `access-control-allow-methods` à partir des routes enregistrées dans le `Hub`. +- `credentials: true` ajoute `access-control-allow-credentials: true` pour les [credentials](https://developer.mozilla.org/fr/docs/Web/HTTP/Guides/CORS#requ%C3%AAtes_n%C3%A9cessitant_une_pr%C3%A9-v%C3%A9rification). +- `maxAge` définit la durée de cache de la pré-vérification en secondes. + +## Interface des paramètres + +```ts +interface CorsPluginParams { + readonly allowOrigin?: + | string + | RegExp + | readonly string[] + | ((origin: string) => boolean | Promise) + | true; + readonly allowHeaders?: string | readonly string[] | true; + readonly exposeHeaders?: string | readonly string[]; + readonly maxAge?: number; + readonly credentials?: boolean; + readonly allowMethods?: RequestMethods | readonly RequestMethods[] | true; +} +``` + +Il faut fournir au moins une option au plugin. + +## Détail des options + +- `allowOrigin` accepte une origine unique, une liste, une expression régulière, une fonction de validation, ou `true` pour autoriser toutes les origines. +- `allowHeaders` accepte une chaîne, une liste, ou `true` pour tous les headers. +- `exposeHeaders` liste les entêtes que le navigateur pourra lire après la requête. +- `allowMethods` accepte une méthode, une liste, ou `true` pour calculer automatiquement les méthodes autorisées par chemin. +- `credentials` active le partage des cookies et autres identifiants d'authentification. +- `maxAge` contrôle la mise en cache de la réponse `OPTIONS`. diff --git a/docs/fr/v0/guide/plugins/openApiGenerator.md b/docs/fr/v0/guide/plugins/openApiGenerator.md index 35fa098..8799252 100644 --- a/docs/fr/v0/guide/plugins/openApiGenerator.md +++ b/docs/fr/v0/guide/plugins/openApiGenerator.md @@ -4,8 +4,8 @@ prev: text: "Génération de code" link: "/fr/v0/guide/plugins/codeGenerator" next: - text: "FormData avancés" - link: "/fr/v0/guide/features/formData" + text: "Créer un point d'entrée static" + link: "/fr/v0/guide/plugins/static" --- # Génération OpenAPI diff --git a/docs/fr/v0/guide/plugins/static.md b/docs/fr/v0/guide/plugins/static.md new file mode 100644 index 0000000..d2b66e3 --- /dev/null +++ b/docs/fr/v0/guide/plugins/static.md @@ -0,0 +1,56 @@ +--- +description: "Créer un point d'entrée static facilement" +prev: + text: "Génération OpenAPI" + link: "/fr/v0/guide/plugins/openApiGenerator" +next: + text: "Gestion CORS" + link: "/fr/v0/guide/plugins/cors" +--- + +# Créer un point d'entrée static + +`@duplojs/http/static` permet d'exposer un fichier ou un dossier en lecture seule. +Vous pouvez soit brancher directement le `staticPlugin` dans le `Hub`, soit enregistrer une route manuellement avec `makeRouteFile` ou `makeRouteFolder`. + +## Avec `staticPlugin` + +```ts twoslash {3,9-22} +// @version: 0 + +``` + +Le plugin choisit automatiquement le bon comportement selon la source passée. + +- `source` est un `FileInterface` ou un `FolderInterface`. +- `path` expose un fichier unique sur une route précise. +- `prefix` expose tout un dossier sous un préfixe d'URL. +- `cacheControlConfig` permet d'ajouter les entêtes `cache-control`. +- `directoryFallBackFile` permet de servir un fichier par défaut pour un dossier, par exemple `index.html`. + +## Avec `makeRouteFile` + +```ts twoslash {5-12} +// @version: 0 + +``` + +Utilisez cette fonction quand vous voulez enregistrer vous-même une route static pour un seul fichier. + +- `source` désigne le fichier à servir. +- `path` est la route HTTP à exposer. +- `cacheControlConfig` est optionnel. + +## Avec `makeRouteFolder` + +```ts twoslash {5-13} +// @version: 0 + +``` + +Cette version sert tous les fichiers d'un dossier à partir d'un préfixe commun. + +- `source` désigne le dossier racine. +- `prefix` est le préfixe d'URL utilisé pour résoudre les fichiers. +- `directoryFallBackFile` permet de servir un fichier par défaut pour un dossier, par exemple `index.html`. +- `cacheControlConfig` est optionnel. diff --git a/docs/libs/v0/client/getBody.cjs b/docs/libs/v0/client/getBody.cjs index 1d1daef..e57449e 100644 --- a/docs/libs/v0/client/getBody.cjs +++ b/docs/libs/v0/client/getBody.cjs @@ -14,9 +14,6 @@ function getBody(response) { else if (responseContentType.includes("text")) { return response.text(); } - else if (responseContentType.includes("form-data")) { - return response.formData(); - } else { return Promise.resolve(undefined); } diff --git a/docs/libs/v0/client/getBody.mjs b/docs/libs/v0/client/getBody.mjs index c7e3b81..065feb6 100644 --- a/docs/libs/v0/client/getBody.mjs +++ b/docs/libs/v0/client/getBody.mjs @@ -12,9 +12,6 @@ function getBody(response) { else if (responseContentType.includes("text")) { return response.text(); } - else if (responseContentType.includes("form-data")) { - return response.formData(); - } else { return Promise.resolve(undefined); } diff --git a/docs/libs/v0/client/serverSentEvents.cjs b/docs/libs/v0/client/serverSentEvents.cjs index 0010d67..ece32dc 100644 --- a/docs/libs/v0/client/serverSentEvents.cjs +++ b/docs/libs/v0/client/serverSentEvents.cjs @@ -128,9 +128,6 @@ function makeClientEventsResponse(response, fetchUrl, fetchInitParams) { }, signal: abortController.signal, }); - if (fetchResponse.status === 204) { - return exit(); - } const fetchInformation = fetchResponse.headers.get(response.requestParams.informationHeaderKey); if (fetchResponse.status !== 204 && fetchResponse.headers.get("content-type")?.includes("text/event-stream") diff --git a/docs/libs/v0/client/serverSentEvents.mjs b/docs/libs/v0/client/serverSentEvents.mjs index 4049f8b..b71fec7 100644 --- a/docs/libs/v0/client/serverSentEvents.mjs +++ b/docs/libs/v0/client/serverSentEvents.mjs @@ -105,9 +105,6 @@ function makeClientEventsResponse(response, fetchUrl, fetchInitParams) { }, signal: abortController.signal, }); - if (fetchResponse.status === 204) { - return exit(); - } const fetchInformation = fetchResponse.headers.get(response.requestParams.informationHeaderKey); if (fetchResponse.status !== 204 && fetchResponse.headers.get("content-type")?.includes("text/event-stream") diff --git a/docs/libs/v0/core/builders/preflight/builder.d.ts b/docs/libs/v0/core/builders/preflight/builder.d.ts index d4bb7c2..0785b5a 100644 --- a/docs/libs/v0/core/builders/preflight/builder.d.ts +++ b/docs/libs/v0/core/builders/preflight/builder.d.ts @@ -1,16 +1,15 @@ import { type Floor } from "../../floor"; -import { type MakeRequestFromHooks, type HookRouteLifeCycle, type RoutePreFlightSteps } from "../../route"; -import { type Builder, type NeverCoalescing } from "@duplojs/utils"; -import { type Request } from "../../request"; +import { type HookRouteLifeCycle, type RoutePreFlightSteps } from "../../route"; +import { type Builder } from "@duplojs/utils"; import { type Metadata } from "../../metadata"; export interface PreflightBuilderDefinition { readonly preflightSteps: readonly RoutePreFlightSteps[]; readonly hooks: readonly HookRouteLifeCycle[]; readonly metadata: readonly Metadata[]; } -export interface PreflightBuilder extends Builder { +export interface PreflightBuilder extends Builder { } -export declare const preflightBuilder: import("@duplojs/utils").BuilderHandler>; +export declare const preflightBuilder: import("@duplojs/utils").BuilderHandler>; export declare function usePreflightBuilder(options?: { hooks?: GenericHooks | readonly HookRouteLifeCycle[]; metadata?: GenericMetadata; @@ -18,4 +17,4 @@ export declare function usePreflightBuilder, Request>>; +}, {}>; diff --git a/docs/libs/v0/core/builders/preflight/process.d.ts b/docs/libs/v0/core/builders/preflight/process.d.ts index e3e1692..3a8add1 100644 --- a/docs/libs/v0/core/builders/preflight/process.d.ts +++ b/docs/libs/v0/core/builders/preflight/process.d.ts @@ -1,11 +1,10 @@ import { type Floor } from "../../floor"; import { type ProcessStep } from "../../steps"; import { type O, type NeverCoalescing, type FixDeepFunctionInfer, type Adaptor, type AnyFunction } from "@duplojs/utils"; -import { type GetProcessRequest, type GetProcessExportValue, type Process } from "../../process"; -import { type Request } from "../../request"; +import { type GetProcessExportValue, type Process } from "../../process"; import { type Metadata } from "../../metadata"; declare module "./builder" { - interface PreflightBuilder { + interface PreflightBuilder { exec, const GenericImportation extends readonly Extract[] = never, GenericOptions extends (GenericProcess["definition"]["options"] | ((floor: GenericFloor) => Exclude)) = never, const GenericMetadata extends readonly Metadata[] = readonly []>(process: GenericProcess, params?: { readonly imports?: GenericImportation; readonly options?: FixDeepFunctionInfer GenericProcess["definition"]["options"]), GenericOptions>; @@ -19,6 +18,6 @@ declare module "./builder" { readonly metadata: GenericMetadata; }> ]; - }>, O.AssignObjects>, GenericRequest & NeverCoalescing, Request>>; + }>, O.AssignObjects>>; } } diff --git a/docs/libs/v0/core/builders/preflight/route.d.ts b/docs/libs/v0/core/builders/preflight/route.d.ts index 8a78628..e629e28 100644 --- a/docs/libs/v0/core/builders/preflight/route.d.ts +++ b/docs/libs/v0/core/builders/preflight/route.d.ts @@ -1,11 +1,10 @@ import { type Floor } from "../../floor"; -import { type RequestMethods, type Request, type BodyController } from "../../request"; -import { type MakeRequestFromHooks, type HookRouteLifeCycle, type RoutePath } from "../../route"; +import { type RequestMethods, type BodyController } from "../../request"; +import { 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 { + interface PreflightBuilder { useRouteBuilder(method: GenericMethod, path: GenericPaths, options?: { hooks?: GenericHooks | readonly HookRouteLifeCycle[]; metadata?: GenericMetadata; @@ -24,6 +23,6 @@ declare module "./builder" { ...GenericDefinition["metadata"] ]; readonly bodyController: GenericBodyController; - }, GenericFloor, (GenericRequest & NeverCoalescing, Request>)>; + }, GenericFloor>; } } diff --git a/docs/libs/v0/core/builders/process/builder.d.ts b/docs/libs/v0/core/builders/process/builder.d.ts index 0bfe61b..557686f 100644 --- a/docs/libs/v0/core/builders/process/builder.d.ts +++ b/docs/libs/v0/core/builders/process/builder.d.ts @@ -1,12 +1,11 @@ import { type ProcessDefinition } from "../../process"; import { type Floor } from "../../floor"; import { type Builder, type IsEqual, type NeverCoalescing } from "@duplojs/utils"; -import { type MakeRequestFromHooks, type HookRouteLifeCycle } from "../../route"; -import { type Request } from "../../request"; +import { type HookRouteLifeCycle } from "../../route"; import { type Metadata } from "../../metadata"; -export interface ProcessBuilder extends Builder { +export interface ProcessBuilder extends Builder { } -export declare const processBuilder: import("@duplojs/utils").BuilderHandler>; +export declare const processBuilder: import("@duplojs/utils").BuilderHandler>; export declare function useProcessBuilder(params?: { options?: GenericOptions; hooks?: GenericHooks | readonly HookRouteLifeCycle[]; @@ -18,4 +17,4 @@ export declare function useProcessBuilder extends true ? {} : { options: GenericOptions; -}, NeverCoalescing, Request>>; +}>; diff --git a/docs/libs/v0/core/builders/process/checker.d.ts b/docs/libs/v0/core/builders/process/checker.d.ts index 2387bbd..43bf090 100644 --- a/docs/libs/v0/core/builders/process/checker.d.ts +++ b/docs/libs/v0/core/builders/process/checker.d.ts @@ -4,10 +4,9 @@ import { type O, type MaybeArray, type NeverCoalescing, type FixDeepFunctionInfe import { type GetCheckerResult, type Checker, type GetCheckerInput, type GetCheckerOptions } from "../../checker"; import { type ClientErrorResponseCode, type ResponseContract } from "../../response"; import { type ProcessDefinition } from "../../process"; -import { type Request } from "../../request"; import { type Metadata } from "../../metadata"; declare module "./builder" { - interface ProcessBuilder { + interface ProcessBuilder { check>["information"]>, GenericInput extends GetCheckerInput, GenericResponseContract extends ResponseContract.Contract, GenericIndex extends string = never, GenericOptions extends (GetCheckerOptions | ((floor: GenericFloor) => Exclude, undefined>)) = never, const GenericMetadata extends readonly Metadata[] = readonly []>(checker: GenericChecker, params: { input(floor: GenericFloor): GenericInput; readonly result: GenericResultInformation; @@ -31,6 +30,6 @@ declare module "./builder" { [Prop in GenericIndex]: Extract>, { information: A.ArrayCoalescing[number]; }>["value"]; - }>, GenericRequest>; + }>>; } } diff --git a/docs/libs/v0/core/builders/process/cut.d.ts b/docs/libs/v0/core/builders/process/cut.d.ts index c7ed476..31a2ac0 100644 --- a/docs/libs/v0/core/builders/process/cut.d.ts +++ b/docs/libs/v0/core/builders/process/cut.d.ts @@ -3,19 +3,18 @@ import { type ResponseContract } from "../../response"; import { type CutStepFunctionOutput, type CutStep, type CutStepFunctionParams } from "../../steps"; import { type Unwrap, type O, type MaybePromise, type IsEqual, type A } from "@duplojs/utils"; import { type ProcessDefinition } from "../../process"; -import { type Request } from "../../request"; import { type Metadata } from "../../metadata"; declare module "./builder" { - interface ProcessBuilder { - cut[number]>, GenericOutput extends CutStepFunctionOutput | GenericResponse, const GenericMetadata extends readonly Metadata[] = readonly []>(responseContract: GenericResponseContract, theFunction: (floor: GenericFloor, param: CutStepFunctionParams) => MaybePromise, ...metadata: GenericMetadata): ProcessBuilder { + cut[number]>, GenericOutput extends CutStepFunctionOutput | GenericResponse, const GenericMetadata extends readonly Metadata[] = readonly []>(responseContract: GenericResponseContract, theFunction: (floor: GenericFloor, param: CutStepFunctionParams) => MaybePromise, ...metadata: GenericMetadata): ProcessBuilder): MaybePromise; + theFunction(floor: GenericFloor, param: CutStepFunctionParams): MaybePromise; readonly metadata: GenericMetadata; }> ]; - }>, IsEqual, never> extends true ? GenericFloor : (GenericOutput extends infer InferredOutputData extends CutStepFunctionOutput ? O.AssignObjects> : never), GenericRequest>; + }>, IsEqual, never> extends true ? GenericFloor : (GenericOutput extends infer InferredOutputData extends CutStepFunctionOutput ? O.AssignObjects> : never)>; } } diff --git a/docs/libs/v0/core/builders/process/exports.d.ts b/docs/libs/v0/core/builders/process/exports.d.ts index 05bfd4e..e49948e 100644 --- a/docs/libs/v0/core/builders/process/exports.d.ts +++ b/docs/libs/v0/core/builders/process/exports.d.ts @@ -1,12 +1,11 @@ import { type Floor } from "../../floor"; import { type SimplifyTopLevel, type IsEqual, type Or } from "@duplojs/utils"; -import { type Process, type ProcessExportValue, type ProcessDefinition, type ProcessRequest } from "../../process"; -import { type Request } from "../../request"; +import { type Process, type ProcessExportValue, type ProcessDefinition } from "../../process"; declare module "./builder" { - interface ProcessBuilder { + interface ProcessBuilder { exports(exportedKey?: GenericExportation): Process, IsEqual - ]> extends true ? {} : ProcessExportValue>>) & (IsEqual extends true ? {} : ProcessRequest)>>; + ]> extends true ? {} : ProcessExportValue>>)>>; } } diff --git a/docs/libs/v0/core/builders/process/extract.d.ts b/docs/libs/v0/core/builders/process/extract.d.ts index 308ab34..36bf8e9 100644 --- a/docs/libs/v0/core/builders/process/extract.d.ts +++ b/docs/libs/v0/core/builders/process/extract.d.ts @@ -3,10 +3,9 @@ import { type ExtractShape, type ExtractStep } from "../../steps"; import { type DP, type ObjectEntry, type O, type SimplifyTopLevel, type NeverCoalescing } from "@duplojs/utils"; import { type ClientErrorResponseCode, type ResponseContract } from "../../response"; import { type ProcessDefinition } from "../../process"; -import { type Request } from "../../request"; import { type Metadata } from "../../metadata"; declare module "./builder" { - interface ProcessBuilder { + interface ProcessBuilder { extract | undefined) = never, const GenericMetadata extends readonly Metadata[] = readonly []>(shape: GenericShape, responseContract?: GenericResponseContract, ...metadata: GenericMetadata): ProcessBuilder : never>, GenericRequest>; + }> : never>>; } } diff --git a/docs/libs/v0/core/builders/process/presetChecker.d.ts b/docs/libs/v0/core/builders/process/presetChecker.d.ts index 4a64f2a..6126900 100644 --- a/docs/libs/v0/core/builders/process/presetChecker.d.ts +++ b/docs/libs/v0/core/builders/process/presetChecker.d.ts @@ -1,12 +1,11 @@ import { type Floor } from "../../floor"; import { type PresetCheckerStep } from "../../steps"; import { type O, type A } from "@duplojs/utils"; -import { type Request } from "../../request"; import { type GetPresetCheckerIndex, type GetPresetCheckerInformation, type GetPresetCheckerResult, type GetPresetCheckerInput, type PresetChecker } from "../../presetChecker"; import { type ProcessDefinition } from "../../process"; import { type Metadata } from "../../metadata"; declare module "./builder" { - interface ProcessBuilder { + interface ProcessBuilder { presetCheck, const GenericMetadata extends readonly Metadata[] = readonly []>(presetChecker: GenericPresetChecker, input: (floor: GenericFloor) => GenericInput, ...metadata: GenericMetadata): ProcessBuilder]: Extract>, { information: A.ArrayCoalescing>[number]; }>["value"]; - }>, GenericRequest>; + }>>; } } diff --git a/docs/libs/v0/core/builders/process/process.d.ts b/docs/libs/v0/core/builders/process/process.d.ts index 22f345b..04b4502 100644 --- a/docs/libs/v0/core/builders/process/process.d.ts +++ b/docs/libs/v0/core/builders/process/process.d.ts @@ -1,11 +1,10 @@ import { type Floor } from "../../floor"; import { type ProcessStep } from "../../steps"; import { type O, type NeverCoalescing, type FixDeepFunctionInfer, type Adaptor, type AnyFunction } from "@duplojs/utils"; -import { type ProcessDefinition, type GetProcessExportValue, type Process, type GetProcessRequest } from "../../process"; -import { type Request } from "../../request"; +import { type ProcessDefinition, type GetProcessExportValue, type Process } from "../../process"; import { type Metadata } from "../../metadata"; declare module "./builder" { - interface ProcessBuilder { + interface ProcessBuilder { exec, const GenericImportation extends readonly Extract[] = never, GenericOptions extends (GenericProcess["definition"]["options"] | ((floor: GenericFloor) => Exclude)) = never, const GenericMetadata extends readonly Metadata[] = readonly []>(process: GenericProcess, params?: { readonly imports?: GenericImportation; readonly options?: FixDeepFunctionInfer GenericProcess["definition"]["options"]), GenericOptions>; @@ -19,6 +18,6 @@ declare module "./builder" { readonly metadata: GenericMetadata; }> ]; - }>, O.AssignObjects>, GenericRequest & NeverCoalescing, Request>>; + }>, O.AssignObjects>>; } } diff --git a/docs/libs/v0/core/builders/route/builder.d.ts b/docs/libs/v0/core/builders/route/builder.d.ts index 026f151..6fca2c8 100644 --- a/docs/libs/v0/core/builders/route/builder.d.ts +++ b/docs/libs/v0/core/builders/route/builder.d.ts @@ -1,11 +1,11 @@ -import { type MakeRequestFromHooks, type HookRouteLifeCycle, type RouteDefinition, type RoutePath } from "../../route"; +import { type HookRouteLifeCycle, type RouteDefinition, type RoutePath } from "../../route"; import { type Floor } from "../../floor"; -import { type RequestMethods, type Request, type BodyController } from "../../request"; -import { type Builder, type NeverCoalescing } from "@duplojs/utils"; +import { type RequestMethods, type BodyController } from "../../request"; +import { type Builder } from "@duplojs/utils"; import { type Metadata } from "../../metadata"; -export interface RouteBuilder extends Builder { +export interface RouteBuilder extends Builder { } -export declare const routeBuilderHandler: import("@duplojs/utils").BuilderHandler>; +export declare const routeBuilderHandler: import("@duplojs/utils").BuilderHandler>; export declare function useRouteBuilder(method: GenericMethod, path: GenericPaths, options?: { hooks?: GenericHooks | readonly HookRouteLifeCycle[]; metadata?: GenericMetadata; @@ -18,4 +18,4 @@ export declare function useRouteBuilder, Request>>; +}, {}>; diff --git a/docs/libs/v0/core/builders/route/checker.d.ts b/docs/libs/v0/core/builders/route/checker.d.ts index b6e4d46..7faab44 100644 --- a/docs/libs/v0/core/builders/route/checker.d.ts +++ b/docs/libs/v0/core/builders/route/checker.d.ts @@ -4,10 +4,9 @@ import { type CheckerStep } from "../../steps"; import { type O, type MaybeArray, type NeverCoalescing, type FixDeepFunctionInfer, type Adaptor, type AnyFunction, type DP, type A } from "@duplojs/utils"; import { type GetCheckerInput, type Checker, type GetCheckerResult, type GetCheckerOptions } from "../../checker"; import { type ClientErrorResponseCode, type ResponseContract } from "../../response"; -import { type Request } from "../../request"; import { type Metadata } from "../../metadata"; declare module "./builder" { - interface RouteBuilder { + interface RouteBuilder { check>["information"]>, GenericInput extends GetCheckerInput, GenericResponseContract extends ResponseContract.Contract, GenericIndex extends string = never, GenericOptions extends (GetCheckerOptions | ((floor: GenericFloor) => Exclude, undefined>)) = never, const GenericMetadata extends readonly Metadata[] = readonly []>(checker: GenericChecker, params: { input(floor: GenericFloor): GenericInput; readonly result: GenericResultInformation; @@ -31,6 +30,6 @@ declare module "./builder" { [Prop in GenericIndex]: Extract>, { information: A.ArrayCoalescing[number]; }>["value"]; - }>, GenericRequest>; + }>>; } } diff --git a/docs/libs/v0/core/builders/route/cut.d.ts b/docs/libs/v0/core/builders/route/cut.d.ts index 3e434bf..7f3162d 100644 --- a/docs/libs/v0/core/builders/route/cut.d.ts +++ b/docs/libs/v0/core/builders/route/cut.d.ts @@ -3,19 +3,18 @@ import { type ResponseContract } from "../../response"; import { type RouteDefinition } from "../../route"; import { type CutStepFunctionOutput, type CutStep, type CutStepFunctionParams } from "../../steps"; import { type Unwrap, type O, type MaybePromise, type IsEqual, type A } from "@duplojs/utils"; -import { type Request } from "../../request"; import { type Metadata } from "../../metadata"; declare module "./builder" { - interface RouteBuilder { - cut[number]>, GenericOutput extends CutStepFunctionOutput | GenericResponse, const GenericMetadata extends readonly Metadata[] = readonly []>(responseContract: GenericResponseContract, theFunction: (floor: GenericFloor, params: CutStepFunctionParams) => MaybePromise, ...metadata: GenericMetadata): RouteBuilder { + cut[number]>, GenericOutput extends CutStepFunctionOutput | GenericResponse, const GenericMetadata extends readonly Metadata[] = readonly []>(responseContract: GenericResponseContract, theFunction: (floor: GenericFloor, params: CutStepFunctionParams) => MaybePromise, ...metadata: GenericMetadata): RouteBuilder): MaybePromise; + theFunction(floor: GenericFloor, param: CutStepFunctionParams): MaybePromise; readonly metadata: GenericMetadata; }> ]; - }>, IsEqual, never> extends true ? GenericFloor : (GenericOutput extends infer InferredOutputData extends CutStepFunctionOutput ? O.AssignObjects> : never), GenericRequest>; + }>, IsEqual, never> extends true ? GenericFloor : (GenericOutput extends infer InferredOutputData extends CutStepFunctionOutput ? O.AssignObjects> : never)>; } } diff --git a/docs/libs/v0/core/builders/route/extract.d.ts b/docs/libs/v0/core/builders/route/extract.d.ts index 71616c5..4717732 100644 --- a/docs/libs/v0/core/builders/route/extract.d.ts +++ b/docs/libs/v0/core/builders/route/extract.d.ts @@ -6,8 +6,8 @@ import { type ClientErrorResponseCode, type ResponseContract } from "../../respo import { type Request } from "../../request"; import { type Metadata } from "../../metadata"; declare module "./builder" { - interface RouteBuilder { - extract, GenericResponseContract extends (ResponseContract.Contract | undefined) = never, const GenericMetadata extends readonly Metadata[] = readonly []>(shape: GenericShape, responseContract?: GenericResponseContract, ...metadata: GenericMetadata): RouteBuilder { + extract, GenericResponseContract extends (ResponseContract.Contract | undefined) = never, const GenericMetadata extends readonly Metadata[] = readonly []>(shape: GenericShape, responseContract?: GenericResponseContract, ...metadata: GenericMetadata): RouteBuilder : never>, GenericRequest>; + }> : never>>; } } diff --git a/docs/libs/v0/core/builders/route/handler.d.ts b/docs/libs/v0/core/builders/route/handler.d.ts index d83c2d5..a7950d0 100644 --- a/docs/libs/v0/core/builders/route/handler.d.ts +++ b/docs/libs/v0/core/builders/route/handler.d.ts @@ -3,19 +3,18 @@ import { type ResponseContract } from "../../response"; import { type Route, type RouteDefinition } from "../../route"; import { type HandlerStep, type HandlerStepFunctionParams } from "../../steps"; import { type MaybePromise, type O } from "@duplojs/utils"; -import { type Request } from "../../request"; import { type Metadata } from "../../metadata"; declare module "./builder" { - interface RouteBuilder { + interface RouteBuilder { handler, const GenericMetadata extends readonly Metadata[] = readonly []>(responseContract: GenericResponseContract, theFunction: (floor: GenericFloor, param: HandlerStepFunctionParams) => MaybePromise, ...metadata: GenericMetadata): Route, const GenericMetadata extends readonly Metadata[] = readonly []>(responseContract: GenericResponseContract, theFunction: (floor: GenericFloor, param: HandlerStepFunctionParams) => MaybePromise, ...metadata: GenericMetadata): Route): MaybePromise; + theFunction(floor: GenericFloor, param: HandlerStepFunctionParams): MaybePromise; readonly metadata: GenericMetadata; }> ]; diff --git a/docs/libs/v0/core/builders/route/presetChecker.d.ts b/docs/libs/v0/core/builders/route/presetChecker.d.ts index adeffed..442ed90 100644 --- a/docs/libs/v0/core/builders/route/presetChecker.d.ts +++ b/docs/libs/v0/core/builders/route/presetChecker.d.ts @@ -2,11 +2,10 @@ import { type Floor } from "../../floor"; import { type RouteDefinition } from "../../route"; import { type PresetCheckerStep } from "../../steps"; import { type O, type A } from "@duplojs/utils"; -import { type Request } from "../../request"; import { type GetPresetCheckerIndex, type GetPresetCheckerInformation, type GetPresetCheckerResult, type GetPresetCheckerInput, type PresetChecker } from "../../presetChecker"; import { type Metadata } from "../../metadata"; declare module "./builder" { - interface RouteBuilder { + interface RouteBuilder { presetCheck, const GenericMetadata extends readonly Metadata[] = readonly []>(presetChecker: GenericPresetChecker, input: (floor: GenericFloor) => GenericInput, ...metadata: GenericMetadata): RouteBuilder]: Extract>, { information: A.ArrayCoalescing>[number]; }>["value"]; - }>, GenericRequest>; + }>>; } } diff --git a/docs/libs/v0/core/builders/route/process.d.ts b/docs/libs/v0/core/builders/route/process.d.ts index 3982b12..49d99a0 100644 --- a/docs/libs/v0/core/builders/route/process.d.ts +++ b/docs/libs/v0/core/builders/route/process.d.ts @@ -2,11 +2,10 @@ import { type Floor } from "../../floor"; import { type RouteDefinition } from "../../route"; import { type ProcessStep } from "../../steps"; import { type O, type NeverCoalescing, type FixDeepFunctionInfer, type Adaptor, type AnyFunction } from "@duplojs/utils"; -import { type GetProcessRequest, type GetProcessExportValue, type Process } from "../../process"; -import { type Request } from "../../request"; +import { type GetProcessExportValue, type Process } from "../../process"; import { type Metadata } from "../../metadata"; declare module "./builder" { - interface RouteBuilder { + interface RouteBuilder { exec, const GenericImportation extends readonly Extract[] = never, GenericOptions extends (GenericProcess["definition"]["options"] | ((floor: GenericFloor) => Exclude)) = never, const GenericMetadata extends readonly Metadata[] = readonly []>(process: GenericProcess, params?: { readonly imports?: GenericImportation; readonly options?: FixDeepFunctionInfer GenericProcess["definition"]["options"]), GenericOptions>; @@ -20,6 +19,6 @@ declare module "./builder" { readonly metadata: GenericMetadata; }> ]; - }>, O.AssignObjects>, GenericRequest & NeverCoalescing, Request>>; + }>, O.AssignObjects>>; } } diff --git a/docs/libs/v0/core/router/decodeUrl.cjs b/docs/libs/v0/core/decodeUrl.cjs similarity index 95% rename from docs/libs/v0/core/router/decodeUrl.cjs rename to docs/libs/v0/core/decodeUrl.cjs index 47cbfdd..9c382a2 100644 --- a/docs/libs/v0/core/router/decodeUrl.cjs +++ b/docs/libs/v0/core/decodeUrl.cjs @@ -39,10 +39,7 @@ function decodeUrl(url) { }; } catch { - return { - path: "/", - query: {}, - }; + return null; } } diff --git a/docs/libs/v0/core/router/decodeUrl.d.ts b/docs/libs/v0/core/decodeUrl.d.ts similarity index 74% rename from docs/libs/v0/core/router/decodeUrl.d.ts rename to docs/libs/v0/core/decodeUrl.d.ts index b6a014c..d201938 100644 --- a/docs/libs/v0/core/router/decodeUrl.d.ts +++ b/docs/libs/v0/core/decodeUrl.d.ts @@ -4,4 +4,4 @@ export interface DecodedUrl { path: string; query: Record; } -export declare function decodeUrl(url: string): DecodedUrl; +export declare function decodeUrl(url: string): DecodedUrl | null; diff --git a/docs/libs/v0/core/router/decodeUrl.mjs b/docs/libs/v0/core/decodeUrl.mjs similarity index 94% rename from docs/libs/v0/core/router/decodeUrl.mjs rename to docs/libs/v0/core/decodeUrl.mjs index 7fdee83..0d1aad4 100644 --- a/docs/libs/v0/core/router/decodeUrl.mjs +++ b/docs/libs/v0/core/decodeUrl.mjs @@ -37,10 +37,7 @@ function decodeUrl(url) { }; } catch { - return { - path: "/", - query: {}, - }; + return null; } } diff --git a/docs/libs/v0/core/defaultHooks/index.d.ts b/docs/libs/v0/core/defaultHooks/index.d.ts index fa137ad..912a17e 100644 --- a/docs/libs/v0/core/defaultHooks/index.d.ts +++ b/docs/libs/v0/core/defaultHooks/index.d.ts @@ -1,5 +1,5 @@ import { type Hub } from "../hub"; import { type HttpServerParams } from "../types"; export declare function initDefaultHook(hub: Hub, serverParams: HttpServerParams): { - readonly beforeSendResponse: ({ currentResponse, next }: import("../route").RouteHookParamsAfter) => import("../route").RouteHookNext; + readonly beforeSendResponse: ({ currentResponse, next }: import("../route").RouteHookParamsAfter) => import("../route").RouteHookNext; }; diff --git a/docs/libs/v0/core/functionsBuilders/index.cjs b/docs/libs/v0/core/functionsBuilders/index.cjs index d1869c2..9598b06 100644 --- a/docs/libs/v0/core/functionsBuilders/index.cjs +++ b/docs/libs/v0/core/functionsBuilders/index.cjs @@ -2,4 +2,5 @@ require('./route/index.cjs'); require('./steps/index.cjs'); +require('./router/index.cjs'); diff --git a/docs/libs/v0/core/functionsBuilders/index.d.ts b/docs/libs/v0/core/functionsBuilders/index.d.ts index f4ebfcc..f173e06 100644 --- a/docs/libs/v0/core/functionsBuilders/index.d.ts +++ b/docs/libs/v0/core/functionsBuilders/index.d.ts @@ -1,2 +1,3 @@ export * from "./route"; export * from "./steps"; +export * from "./router"; diff --git a/docs/libs/v0/core/functionsBuilders/index.mjs b/docs/libs/v0/core/functionsBuilders/index.mjs index a9e75c4..a814186 100644 --- a/docs/libs/v0/core/functionsBuilders/index.mjs +++ b/docs/libs/v0/core/functionsBuilders/index.mjs @@ -1,2 +1,3 @@ import './route/index.mjs'; import './steps/index.mjs'; +import './router/index.mjs'; diff --git a/docs/libs/v0/core/functionsBuilders/route/build.d.ts b/docs/libs/v0/core/functionsBuilders/route/build.d.ts index 9bab3f4..4e60e59 100644 --- a/docs/libs/v0/core/functionsBuilders/route/build.d.ts +++ b/docs/libs/v0/core/functionsBuilders/route/build.d.ts @@ -1,10 +1,13 @@ -import { type BuildStepFunctionParams } from "../steps"; +import { type createStepFunctionBuilder } from "../steps"; import { type BuildRouteNotSupportEither, type createRouteFunctionBuilder } from "./create"; import { type HookRouteLifeCycle, type Route } from "../../route"; import { type ResponseContract } from "../../response"; -export interface BuildRouteFunctionParams extends BuildStepFunctionParams { +import { type Environment } from "../../types"; +export interface BuildRouteFunctionParams { readonly routeFunctionBuilders: readonly ReturnType[]; readonly globalHooksRouteLifeCycle: readonly HookRouteLifeCycle[]; + readonly stepFunctionBuilders: readonly ReturnType[]; + readonly environment: Environment; readonly defaultExtractContract: ResponseContract.Contract; } export declare function buildRouteFunction(route: Route, params: BuildRouteFunctionParams): Promise; diff --git a/docs/libs/v0/core/functionsBuilders/route/create.d.ts b/docs/libs/v0/core/functionsBuilders/route/create.d.ts index 9b33af8..35e285b 100644 --- a/docs/libs/v0/core/functionsBuilders/route/create.d.ts +++ b/docs/libs/v0/core/functionsBuilders/route/create.d.ts @@ -1,18 +1,16 @@ import { E, type MaybePromise } from "@duplojs/utils"; -import { type HookRouteLifeCycle, type Route } from "../../route"; -import { type Request } from "../../request"; +import { type BuildedRoute, type HookRouteLifeCycle, type Route } from "../../route"; import { type Environment } from "../../types"; import { type BuildStepSuccessEither, type BuildStepNotSupportEither } from "../steps"; import { type Steps } from "../../steps"; import { type ResponseContract } from "../../response"; -export type BuildedRouteFunction = (request: Request) => Promise; -export type BuildRouteSuccessEither = E.Right<"buildSuccess", BuildedRouteFunction>; +export type BuildRouteSuccessEither = E.Right<"buildSuccess", BuildedRoute>; export type BuildRouteNotSupportEither = E.Left<"routeNotSupport", Route>; export interface RouteFunctionBuilderParams { readonly globalHooksRouteLifeCycle: readonly HookRouteLifeCycle[]; readonly environment: Environment; buildStep(element: Steps): Promise; - success(result: BuildedRouteFunction): BuildRouteSuccessEither; + success(result: BuildedRoute): BuildRouteSuccessEither; readonly defaultExtractContract: ResponseContract.Contract; } export declare function createRouteFunctionBuilder(support: (route: Route) => boolean, builder: (route: Route, params: RouteFunctionBuilderParams) => MaybePromise): (route: Route, params: RouteFunctionBuilderParams) => MaybePromise; diff --git a/docs/libs/v0/core/functionsBuilders/route/default.d.ts b/docs/libs/v0/core/functionsBuilders/route/default.d.ts deleted file mode 100644 index 80709b2..0000000 --- a/docs/libs/v0/core/functionsBuilders/route/default.d.ts +++ /dev/null @@ -1 +0,0 @@ -export declare const defaultRouteFunctionBuilder: (route: import("../../route").Route, params: import("./create").RouteFunctionBuilderParams) => import("@duplojs/utils").MaybePromise; diff --git a/docs/libs/v0/core/functionsBuilders/route/hook.cjs b/docs/libs/v0/core/functionsBuilders/route/default/hook.cjs similarity index 74% rename from docs/libs/v0/core/functionsBuilders/route/hook.cjs rename to docs/libs/v0/core/functionsBuilders/route/default/hook.cjs index 243bc9c..934977c 100644 --- a/docs/libs/v0/core/functionsBuilders/route/hook.cjs +++ b/docs/libs/v0/core/functionsBuilders/route/default/hook.cjs @@ -1,9 +1,9 @@ 'use strict'; -require('../../response/index.cjs'); -require('../../route/index.cjs'); -var hooks = require('../../route/hooks.cjs'); -var hook = require('../../response/hook.cjs'); +require('../../../response/index.cjs'); +require('../../../route/index.cjs'); +var hooks = require('../../../route/hooks.cjs'); +var hook = require('../../../response/hook.cjs'); /* eslint-disable @typescript-eslint/prefer-for-of */ const hookExit = hooks.hookRouteExitKind.setTo({}); @@ -20,7 +20,10 @@ function buildHookBefore(hooks$1) { } return async (params) => { for (let index = 0; index < hooks$1.length; index++) { - const result = await hooks$1[index](params); + let result = hooks$1[index](params); + if (result instanceof Promise) { + result = await result; + } if (hooks.hookRouteExitKind.has(result) || result instanceof hook.HookResponse) { return result; @@ -35,7 +38,10 @@ function buildHookErrorBefore(hooks$1) { } return async (params) => { for (let index = 0; index < hooks$1.length; index++) { - const result = await hooks$1[index](params); + let result = hooks$1[index](params); + if (result instanceof Promise) { + result = await result; + } if (hooks.hookRouteExitKind.has(result) || result instanceof hook.HookResponse) { return result; @@ -50,7 +56,10 @@ function buildHookAfter(hooks$1) { } return async (params) => { for (let index = 0; index < hooks$1.length; index++) { - const result = await hooks$1[index](params); + let result = hooks$1[index](params); + if (result instanceof Promise) { + result = await result; + } if (hooks.hookRouteExitKind.has(result)) { return result; } diff --git a/docs/libs/v0/core/functionsBuilders/route/hook.d.ts b/docs/libs/v0/core/functionsBuilders/route/default/hook.d.ts similarity index 57% rename from docs/libs/v0/core/functionsBuilders/route/hook.d.ts rename to docs/libs/v0/core/functionsBuilders/route/default/hook.d.ts index 9d4c81a..bcb94c1 100644 --- a/docs/libs/v0/core/functionsBuilders/route/hook.d.ts +++ b/docs/libs/v0/core/functionsBuilders/route/default/hook.d.ts @@ -1,8 +1,8 @@ -import { HookResponse } from "../../response"; -import { type HookAfterSendResponse, type HookBeforeRouteExecution, type HookBeforeSendResponse, type HookError, type HookRouteLifeCycle, type HookSendResponse, type RouteHookErrorParams, type RouteHookParams, type RouteHookParamsAfter } from "../../route"; +import { HookResponse } from "../../../response"; +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)[]): 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 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/route/hook.mjs b/docs/libs/v0/core/functionsBuilders/route/default/hook.mjs similarity index 71% rename from docs/libs/v0/core/functionsBuilders/route/hook.mjs rename to docs/libs/v0/core/functionsBuilders/route/default/hook.mjs index 15bcdb3..e5d8a9d 100644 --- a/docs/libs/v0/core/functionsBuilders/route/hook.mjs +++ b/docs/libs/v0/core/functionsBuilders/route/default/hook.mjs @@ -1,7 +1,7 @@ -import '../../response/index.mjs'; -import '../../route/index.mjs'; -import { hookRouteExitKind, hookRouteNextKind } from '../../route/hooks.mjs'; -import { HookResponse } from '../../response/hook.mjs'; +import '../../../response/index.mjs'; +import '../../../route/index.mjs'; +import { hookRouteExitKind, hookRouteNextKind } from '../../../route/hooks.mjs'; +import { HookResponse } from '../../../response/hook.mjs'; /* eslint-disable @typescript-eslint/prefer-for-of */ const hookExit = hookRouteExitKind.setTo({}); @@ -18,7 +18,10 @@ function buildHookBefore(hooks) { } return async (params) => { for (let index = 0; index < hooks.length; index++) { - const result = await hooks[index](params); + let result = hooks[index](params); + if (result instanceof Promise) { + result = await result; + } if (hookRouteExitKind.has(result) || result instanceof HookResponse) { return result; @@ -33,7 +36,10 @@ function buildHookErrorBefore(hooks) { } return async (params) => { for (let index = 0; index < hooks.length; index++) { - const result = await hooks[index](params); + let result = hooks[index](params); + if (result instanceof Promise) { + result = await result; + } if (hookRouteExitKind.has(result) || result instanceof HookResponse) { return result; @@ -48,7 +54,10 @@ function buildHookAfter(hooks) { } return async (params) => { for (let index = 0; index < hooks.length; index++) { - const result = await hooks[index](params); + let result = hooks[index](params); + if (result instanceof Promise) { + result = await result; + } if (hookRouteExitKind.has(result)) { return result; } diff --git a/docs/libs/v0/core/functionsBuilders/route/default.cjs b/docs/libs/v0/core/functionsBuilders/route/default/index.cjs similarity index 76% rename from docs/libs/v0/core/functionsBuilders/route/default.cjs rename to docs/libs/v0/core/functionsBuilders/route/default/index.cjs index e1b95e4..96e2eac 100644 --- a/docs/libs/v0/core/functionsBuilders/route/default.cjs +++ b/docs/libs/v0/core/functionsBuilders/route/default/index.cjs @@ -1,14 +1,14 @@ 'use strict'; -var index = require('../../route/index.cjs'); +var index = require('../../../route/index.cjs'); var utils = require('@duplojs/utils'); -require('../../response/index.cjs'); +require('../../../response/index.cjs'); var hook = require('./hook.cjs'); -var create = require('./create.cjs'); -require('../steps/index.cjs'); -var processStep = require('../steps/defaults/processStep.cjs'); -var base = require('../../response/base.cjs'); -var hook$1 = require('../../response/hook.cjs'); +var create = require('../create.cjs'); +require('../../steps/index.cjs'); +var processStep = require('../../steps/defaults/processStep.cjs'); +var base = require('../../../response/base.cjs'); +var hook$1 = require('../../../response/hook.cjs'); /* eslint-disable @typescript-eslint/prefer-for-of */ const defaultRouteFunctionBuilder = create.createRouteFunctionBuilder(index.routeKind.has, async (route, { success, buildStep, globalHooksRouteLifeCycle, }) => { @@ -32,25 +32,12 @@ const defaultRouteFunctionBuilder = create.createRouteFunctionBuilder(index.rout const hookAfterSendResponse = utils.pipe(allHooks, utils.A.map(({ afterSendResponse }) => afterSendResponse), utils.A.filter(utils.isType("function")), utils.forward); 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 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 = { - afterSendResponse: hook.buildHookAfter(hookAfterSendResponse), beforeRouteExecution: hook.buildHookBefore(hookBeforeRouteExecution), + afterSendResponse: hook.buildHookAfter(hookAfterSendResponse), beforeSendResponse: hook.buildHookAfter(hookBeforeSendResponse), - onConstructRequest: hookOnConstructRequest.length - ? async (params) => { - let newRequest = params.request; - for (let index = 0; index < hookOnConstructRequest.length; index++) { - newRequest = await hookOnConstructRequest[index]({ - ...params, - request: newRequest, - }); - } - return newRequest; - } - : (params) => params.request, error: hook.buildHookErrorBefore(hookError), sendResponse: hook.buildHookAfter(hookSendResponse), }; @@ -97,18 +84,9 @@ const defaultRouteFunctionBuilder = create.createRouteFunctionBuilder(index.rout } } return success(async (request) => { - const currentRequest = await hooks.onConstructRequest({ - request, - addRequestProperties: (newProperties) => { - for (const key in newProperties) { - request[key] = newProperties[key]; - } - return request; - }, - }); - const currentResponse = await routeExecution(currentRequest); + const currentResponse = await routeExecution(request); const afterHookParams = { - request: currentRequest, + request, currentResponse, exit: hook.exitHookFunction, next: hook.nextHookFunction, @@ -119,4 +97,10 @@ const defaultRouteFunctionBuilder = create.createRouteFunctionBuilder(index.rout }); }); +exports.buildHookAfter = hook.buildHookAfter; +exports.buildHookBefore = hook.buildHookBefore; +exports.buildHookErrorBefore = hook.buildHookErrorBefore; +exports.createHookResponse = hook.createHookResponse; +exports.exitHookFunction = hook.exitHookFunction; +exports.nextHookFunction = hook.nextHookFunction; exports.defaultRouteFunctionBuilder = defaultRouteFunctionBuilder; diff --git a/docs/libs/v0/core/functionsBuilders/route/default/index.d.ts b/docs/libs/v0/core/functionsBuilders/route/default/index.d.ts new file mode 100644 index 0000000..2ae37be --- /dev/null +++ b/docs/libs/v0/core/functionsBuilders/route/default/index.d.ts @@ -0,0 +1,2 @@ +export * from "./hook"; +export declare const defaultRouteFunctionBuilder: (route: import("../../../route").Route, params: import("..").RouteFunctionBuilderParams) => import("@duplojs/utils").MaybePromise; diff --git a/docs/libs/v0/core/functionsBuilders/route/default.mjs b/docs/libs/v0/core/functionsBuilders/route/default/index.mjs similarity index 74% rename from docs/libs/v0/core/functionsBuilders/route/default.mjs rename to docs/libs/v0/core/functionsBuilders/route/default/index.mjs index d11273b..77daa58 100644 --- a/docs/libs/v0/core/functionsBuilders/route/default.mjs +++ b/docs/libs/v0/core/functionsBuilders/route/default/index.mjs @@ -1,12 +1,12 @@ -import { routeKind } from '../../route/index.mjs'; +import { routeKind } from '../../../route/index.mjs'; import { E, A, pipe, isType, forward } from '@duplojs/utils'; -import '../../response/index.mjs'; +import '../../../response/index.mjs'; import { buildHookAfter, buildHookErrorBefore, buildHookBefore, createHookResponse, nextHookFunction, exitHookFunction } from './hook.mjs'; -import { createRouteFunctionBuilder } from './create.mjs'; -import '../steps/index.mjs'; -import { buildStepsFunction } from '../steps/defaults/processStep.mjs'; -import { Response } from '../../response/base.mjs'; -import { HookResponse } from '../../response/hook.mjs'; +import { createRouteFunctionBuilder } from '../create.mjs'; +import '../../steps/index.mjs'; +import { buildStepsFunction } from '../../steps/defaults/processStep.mjs'; +import { Response } from '../../../response/base.mjs'; +import { HookResponse } from '../../../response/hook.mjs'; /* eslint-disable @typescript-eslint/prefer-for-of */ const defaultRouteFunctionBuilder = createRouteFunctionBuilder(routeKind.has, async (route, { success, buildStep, globalHooksRouteLifeCycle, }) => { @@ -30,25 +30,12 @@ const defaultRouteFunctionBuilder = createRouteFunctionBuilder(routeKind.has, as const hookAfterSendResponse = pipe(allHooks, A.map(({ afterSendResponse }) => afterSendResponse), A.filter(isType("function")), forward); 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 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 = { - afterSendResponse: buildHookAfter(hookAfterSendResponse), beforeRouteExecution: buildHookBefore(hookBeforeRouteExecution), + afterSendResponse: buildHookAfter(hookAfterSendResponse), beforeSendResponse: buildHookAfter(hookBeforeSendResponse), - onConstructRequest: hookOnConstructRequest.length - ? async (params) => { - let newRequest = params.request; - for (let index = 0; index < hookOnConstructRequest.length; index++) { - newRequest = await hookOnConstructRequest[index]({ - ...params, - request: newRequest, - }); - } - return newRequest; - } - : (params) => params.request, error: buildHookErrorBefore(hookError), sendResponse: buildHookAfter(hookSendResponse), }; @@ -95,18 +82,9 @@ const defaultRouteFunctionBuilder = createRouteFunctionBuilder(routeKind.has, as } } return success(async (request) => { - const currentRequest = await hooks.onConstructRequest({ - request, - addRequestProperties: (newProperties) => { - for (const key in newProperties) { - request[key] = newProperties[key]; - } - return request; - }, - }); - const currentResponse = await routeExecution(currentRequest); + const currentResponse = await routeExecution(request); const afterHookParams = { - request: currentRequest, + request, currentResponse, exit: exitHookFunction, next: nextHookFunction, @@ -117,4 +95,4 @@ const defaultRouteFunctionBuilder = createRouteFunctionBuilder(routeKind.has, as }); }); -export { defaultRouteFunctionBuilder }; +export { buildHookAfter, buildHookBefore, buildHookErrorBefore, createHookResponse, defaultRouteFunctionBuilder, exitHookFunction, nextHookFunction }; diff --git a/docs/libs/v0/core/functionsBuilders/route/index.cjs b/docs/libs/v0/core/functionsBuilders/route/index.cjs index e0ccca0..f00ce53 100644 --- a/docs/libs/v0/core/functionsBuilders/route/index.cjs +++ b/docs/libs/v0/core/functionsBuilders/route/index.cjs @@ -2,17 +2,10 @@ var build = require('./build.cjs'); var create = require('./create.cjs'); -var _default = require('./default.cjs'); -var hook = require('./hook.cjs'); +var index = require('./default/index.cjs'); exports.buildRouteFunction = build.buildRouteFunction; exports.createRouteFunctionBuilder = create.createRouteFunctionBuilder; -exports.defaultRouteFunctionBuilder = _default.defaultRouteFunctionBuilder; -exports.buildHookAfter = hook.buildHookAfter; -exports.buildHookBefore = hook.buildHookBefore; -exports.buildHookErrorBefore = hook.buildHookErrorBefore; -exports.createHookResponse = hook.createHookResponse; -exports.exitHookFunction = hook.exitHookFunction; -exports.nextHookFunction = hook.nextHookFunction; +exports.defaultRouteFunctionBuilder = index.defaultRouteFunctionBuilder; diff --git a/docs/libs/v0/core/functionsBuilders/route/index.d.ts b/docs/libs/v0/core/functionsBuilders/route/index.d.ts index 19ebfd7..622be32 100644 --- a/docs/libs/v0/core/functionsBuilders/route/index.d.ts +++ b/docs/libs/v0/core/functionsBuilders/route/index.d.ts @@ -1,4 +1,3 @@ export * from "./build"; export * from "./create"; export * from "./default"; -export * from "./hook"; diff --git a/docs/libs/v0/core/functionsBuilders/route/index.mjs b/docs/libs/v0/core/functionsBuilders/route/index.mjs index 4ab0ec2..62113ca 100644 --- a/docs/libs/v0/core/functionsBuilders/route/index.mjs +++ b/docs/libs/v0/core/functionsBuilders/route/index.mjs @@ -1,4 +1,3 @@ export { buildRouteFunction } from './build.mjs'; export { createRouteFunctionBuilder } from './create.mjs'; -export { defaultRouteFunctionBuilder } from './default.mjs'; -export { buildHookAfter, buildHookBefore, buildHookErrorBefore, createHookResponse, exitHookFunction, nextHookFunction } from './hook.mjs'; +export { defaultRouteFunctionBuilder } from './default/index.mjs'; diff --git a/docs/libs/v0/core/functionsBuilders/router/build.cjs b/docs/libs/v0/core/functionsBuilders/router/build.cjs new file mode 100644 index 0000000..439e4de --- /dev/null +++ b/docs/libs/v0/core/functionsBuilders/router/build.cjs @@ -0,0 +1,7 @@ +'use strict'; + +async function buildRouterFunction({ routerFunctionBuilder, ...params }) { + return routerFunctionBuilder(params); +} + +exports.buildRouterFunction = buildRouterFunction; diff --git a/docs/libs/v0/core/functionsBuilders/router/build.d.ts b/docs/libs/v0/core/functionsBuilders/router/build.d.ts new file mode 100644 index 0000000..c7bdc1e --- /dev/null +++ b/docs/libs/v0/core/functionsBuilders/router/build.d.ts @@ -0,0 +1,14 @@ +import { type Environment } from "../../types"; +import { type createRouterFunctionBuilder } from "./create"; +import { type Request } from "../../request"; +import { type RouterElementWrapper } from "../../router/types/routerElementWrapper"; +import { type RouterElementSystem } from "../../router/types/routerElementSystem"; +export interface BuildRouterFunctionParams { + readonly routerFunctionBuilder: ReturnType; + readonly environment: Environment; + readonly routerElementWrapper: RouterElementWrapper; + readonly classRequest: typeof Request; + readonly notfoundRouterElement: RouterElementSystem; + readonly malformedUrlRouterElement: RouterElementSystem; +} +export declare function buildRouterFunction({ routerFunctionBuilder, ...params }: BuildRouterFunctionParams): Promise; diff --git a/docs/libs/v0/core/functionsBuilders/router/build.mjs b/docs/libs/v0/core/functionsBuilders/router/build.mjs new file mode 100644 index 0000000..9fad4cb --- /dev/null +++ b/docs/libs/v0/core/functionsBuilders/router/build.mjs @@ -0,0 +1,5 @@ +async function buildRouterFunction({ routerFunctionBuilder, ...params }) { + return routerFunctionBuilder(params); +} + +export { buildRouterFunction }; diff --git a/docs/libs/v0/core/functionsBuilders/router/create.cjs b/docs/libs/v0/core/functionsBuilders/router/create.cjs new file mode 100644 index 0000000..c7129d0 --- /dev/null +++ b/docs/libs/v0/core/functionsBuilders/router/create.cjs @@ -0,0 +1,7 @@ +'use strict'; + +function createRouterFunctionBuilder(theFunction) { + return theFunction; +} + +exports.createRouterFunctionBuilder = createRouterFunctionBuilder; diff --git a/docs/libs/v0/core/functionsBuilders/router/create.d.ts b/docs/libs/v0/core/functionsBuilders/router/create.d.ts new file mode 100644 index 0000000..34d4f2d --- /dev/null +++ b/docs/libs/v0/core/functionsBuilders/router/create.d.ts @@ -0,0 +1,15 @@ +import { MaybePromise } from "@duplojs/utils"; +import { type Request } from "../../request"; +import { type BuildedRouter } from "../../router"; +import { type RouterElementSystem } from "../../router/types/routerElementSystem"; +import { type RouterElementWrapper } from "../../router/types/routerElementWrapper"; +import { type Environment } from "../../types"; +export interface RouterFunctionBuilderParams { + readonly environment: Environment; + readonly routerElementWrapper: RouterElementWrapper; + readonly classRequest: typeof Request; + readonly notfoundRouterElement: RouterElementSystem; + readonly malformedUrlRouterElement: RouterElementSystem; +} +export type RouterFunctionBuilder = (params: RouterFunctionBuilderParams) => MaybePromise; +export declare function createRouterFunctionBuilder(theFunction: RouterFunctionBuilder): RouterFunctionBuilder; diff --git a/docs/libs/v0/core/functionsBuilders/router/create.mjs b/docs/libs/v0/core/functionsBuilders/router/create.mjs new file mode 100644 index 0000000..ac737aa --- /dev/null +++ b/docs/libs/v0/core/functionsBuilders/router/create.mjs @@ -0,0 +1,5 @@ +function createRouterFunctionBuilder(theFunction) { + return theFunction; +} + +export { createRouterFunctionBuilder }; diff --git a/docs/libs/v0/core/functionsBuilders/router/default/index.cjs b/docs/libs/v0/core/functionsBuilders/router/default/index.cjs new file mode 100644 index 0000000..1864ae2 --- /dev/null +++ b/docs/libs/v0/core/functionsBuilders/router/default/index.cjs @@ -0,0 +1,52 @@ +'use strict'; + +var decodeUrl = require('../../../decodeUrl.cjs'); +var create = require('../create.cjs'); + +const defaultRouterFunctionBuilder = create.createRouterFunctionBuilder(({ routerElementWrapper, malformedUrlRouterElement, notfoundRouterElement, classRequest, }) => (initializationData) => { + const routerElements = routerElementWrapper[initializationData.method]; + const decodedUrl = decodeUrl.decodeUrl(initializationData.url); + if (!decodedUrl) { + return malformedUrlRouterElement.buildedRoute(new classRequest({ + ...initializationData, + params: {}, + path: "", + query: {}, + matchedPath: null, + bodyReader: malformedUrlRouterElement.bodyReader, + })); + } + if (!routerElements) { + return notfoundRouterElement.buildedRoute(new classRequest({ + ...initializationData, + ...decodedUrl, + params: {}, + matchedPath: null, + bodyReader: notfoundRouterElement.bodyReader, + })); + } + // eslint-disable-next-line @typescript-eslint/prefer-for-of + for (let index = 0; index < routerElements.length; index++) { + const routerElement = routerElements[index]; + const result = routerElement.pattern.exec(decodedUrl.path); + if (!result) { + continue; + } + return routerElement.buildedRoute(new classRequest({ + ...initializationData, + ...decodedUrl, + params: result.groups ?? {}, + matchedPath: routerElement.matchedPath, + bodyReader: routerElement.bodyReader, + })); + } + return notfoundRouterElement.buildedRoute(new classRequest({ + ...initializationData, + ...decodedUrl, + params: {}, + matchedPath: null, + bodyReader: notfoundRouterElement.bodyReader, + })); +}); + +exports.defaultRouterFunctionBuilder = defaultRouterFunctionBuilder; diff --git a/docs/libs/v0/core/functionsBuilders/router/default/index.d.ts b/docs/libs/v0/core/functionsBuilders/router/default/index.d.ts new file mode 100644 index 0000000..ff8b872 --- /dev/null +++ b/docs/libs/v0/core/functionsBuilders/router/default/index.d.ts @@ -0,0 +1 @@ +export declare const defaultRouterFunctionBuilder: import("..").RouterFunctionBuilder; diff --git a/docs/libs/v0/core/functionsBuilders/router/default/index.mjs b/docs/libs/v0/core/functionsBuilders/router/default/index.mjs new file mode 100644 index 0000000..32dcdc6 --- /dev/null +++ b/docs/libs/v0/core/functionsBuilders/router/default/index.mjs @@ -0,0 +1,50 @@ +import { decodeUrl } from '../../../decodeUrl.mjs'; +import { createRouterFunctionBuilder } from '../create.mjs'; + +const defaultRouterFunctionBuilder = createRouterFunctionBuilder(({ routerElementWrapper, malformedUrlRouterElement, notfoundRouterElement, classRequest, }) => (initializationData) => { + const routerElements = routerElementWrapper[initializationData.method]; + const decodedUrl = decodeUrl(initializationData.url); + if (!decodedUrl) { + return malformedUrlRouterElement.buildedRoute(new classRequest({ + ...initializationData, + params: {}, + path: "", + query: {}, + matchedPath: null, + bodyReader: malformedUrlRouterElement.bodyReader, + })); + } + if (!routerElements) { + return notfoundRouterElement.buildedRoute(new classRequest({ + ...initializationData, + ...decodedUrl, + params: {}, + matchedPath: null, + bodyReader: notfoundRouterElement.bodyReader, + })); + } + // eslint-disable-next-line @typescript-eslint/prefer-for-of + for (let index = 0; index < routerElements.length; index++) { + const routerElement = routerElements[index]; + const result = routerElement.pattern.exec(decodedUrl.path); + if (!result) { + continue; + } + return routerElement.buildedRoute(new classRequest({ + ...initializationData, + ...decodedUrl, + params: result.groups ?? {}, + matchedPath: routerElement.matchedPath, + bodyReader: routerElement.bodyReader, + })); + } + return notfoundRouterElement.buildedRoute(new classRequest({ + ...initializationData, + ...decodedUrl, + params: {}, + matchedPath: null, + bodyReader: notfoundRouterElement.bodyReader, + })); +}); + +export { defaultRouterFunctionBuilder }; diff --git a/docs/libs/v0/core/functionsBuilders/router/index.cjs b/docs/libs/v0/core/functionsBuilders/router/index.cjs new file mode 100644 index 0000000..3f77ee7 --- /dev/null +++ b/docs/libs/v0/core/functionsBuilders/router/index.cjs @@ -0,0 +1,11 @@ +'use strict'; + +var create = require('./create.cjs'); +var index = require('./default/index.cjs'); +var build = require('./build.cjs'); + + + +exports.createRouterFunctionBuilder = create.createRouterFunctionBuilder; +exports.defaultRouterFunctionBuilder = index.defaultRouterFunctionBuilder; +exports.buildRouterFunction = build.buildRouterFunction; diff --git a/docs/libs/v0/core/functionsBuilders/router/index.d.ts b/docs/libs/v0/core/functionsBuilders/router/index.d.ts new file mode 100644 index 0000000..e8da4a9 --- /dev/null +++ b/docs/libs/v0/core/functionsBuilders/router/index.d.ts @@ -0,0 +1,3 @@ +export * from "./create"; +export * from "./default"; +export * from "./build"; diff --git a/docs/libs/v0/core/functionsBuilders/router/index.mjs b/docs/libs/v0/core/functionsBuilders/router/index.mjs new file mode 100644 index 0000000..3eebe27 --- /dev/null +++ b/docs/libs/v0/core/functionsBuilders/router/index.mjs @@ -0,0 +1,3 @@ +export { createRouterFunctionBuilder } from './create.mjs'; +export { defaultRouterFunctionBuilder } from './default/index.mjs'; +export { buildRouterFunction } from './build.mjs'; diff --git a/docs/libs/v0/core/functionsBuilders/steps/create.d.ts b/docs/libs/v0/core/functionsBuilders/steps/create.d.ts index 2d75a9a..8e7f781 100644 --- a/docs/libs/v0/core/functionsBuilders/steps/create.d.ts +++ b/docs/libs/v0/core/functionsBuilders/steps/create.d.ts @@ -1,13 +1,10 @@ import { E, type MaybePromise } from "@duplojs/utils"; -import { type Steps } from "../../steps/types"; -import { type Floor } from "../../floor"; +import { type BuildedStep, type Steps } from "../../steps/types"; import { type HookRouteLifeCycle } from "../../route"; -import { type Request } from "../../request"; -import { type ResponseContract, type Response } from "../../response"; +import { type ResponseContract } from "../../response"; import { type Environment } from "../../types"; -export type BuildedStepFunction = (request: Request, floor: Floor) => MaybePromise; export interface BuildStepResult { - readonly buildedFunction: BuildedStepFunction; + readonly buildedFunction: BuildedStep; readonly hooksRouteLifeCycle: readonly HookRouteLifeCycle[]; } export type BuildStepSuccessEither = E.Right<"buildSuccess", BuildStepResult>; diff --git a/docs/libs/v0/core/hub/defaultEmptyReaderImplementation.cjs b/docs/libs/v0/core/hub/defaultEmptyReaderImplementation.cjs new file mode 100644 index 0000000..4a1fcfd --- /dev/null +++ b/docs/libs/v0/core/hub/defaultEmptyReaderImplementation.cjs @@ -0,0 +1,9 @@ +'use strict'; + +var utils = require('@duplojs/utils'); +require('../request/index.cjs'); +var empty = require('../request/bodyController/empty.cjs'); + +const defaultEmptyReaderImplementation = empty.EmptyBodyController.createReaderImplementation(() => Promise.resolve(utils.E.success(undefined))); + +exports.defaultEmptyReaderImplementation = defaultEmptyReaderImplementation; diff --git a/docs/libs/v0/core/hub/defaultEmptyReaderImplementation.d.ts b/docs/libs/v0/core/hub/defaultEmptyReaderImplementation.d.ts new file mode 100644 index 0000000..08069c8 --- /dev/null +++ b/docs/libs/v0/core/hub/defaultEmptyReaderImplementation.d.ts @@ -0,0 +1 @@ +export declare const defaultEmptyReaderImplementation: import("../request").BodyReaderImplementation<"empty", {}>; diff --git a/docs/libs/v0/core/hub/defaultEmptyReaderImplementation.mjs b/docs/libs/v0/core/hub/defaultEmptyReaderImplementation.mjs new file mode 100644 index 0000000..dd8577d --- /dev/null +++ b/docs/libs/v0/core/hub/defaultEmptyReaderImplementation.mjs @@ -0,0 +1,7 @@ +import { E } from '@duplojs/utils'; +import '../request/index.mjs'; +import { EmptyBodyController } from '../request/bodyController/empty.mjs'; + +const defaultEmptyReaderImplementation = EmptyBodyController.createReaderImplementation(() => Promise.resolve(E.success(undefined))); + +export { defaultEmptyReaderImplementation }; diff --git a/docs/libs/v0/core/hub/defaultMalformedUrlHandler.cjs b/docs/libs/v0/core/hub/defaultMalformedUrlHandler.cjs new file mode 100644 index 0000000..31c74e0 --- /dev/null +++ b/docs/libs/v0/core/hub/defaultMalformedUrlHandler.cjs @@ -0,0 +1,14 @@ +'use strict'; + +require('../response/index.cjs'); +require('../steps/index.cjs'); +var handler = require('../steps/handler.cjs'); +var contract = require('../response/contract.cjs'); + +const defaultMalformedUrlHandler = handler.createHandlerStep({ + responseContract: contract.ResponseContract.badRequest("malformed-url"), + theFunction: (__, { response }) => response("malformed-url", undefined), + metadata: [], +}); + +exports.defaultMalformedUrlHandler = defaultMalformedUrlHandler; diff --git a/docs/libs/v0/core/hub/defaultMalformedUrlHandler.d.ts b/docs/libs/v0/core/hub/defaultMalformedUrlHandler.d.ts new file mode 100644 index 0000000..f218a62 --- /dev/null +++ b/docs/libs/v0/core/hub/defaultMalformedUrlHandler.d.ts @@ -0,0 +1,10 @@ +import { ResponseContract } from "../response"; +export declare const defaultMalformedUrlHandler: import("../steps").HandlerStep<{ + responseContract: NoInfer>>; + theFunction: (__: import("..").Floor, { response }: import("../steps").HandlerStepFunctionParams | import("../response").PredictedResponse>) => never; + metadata: never[]; +}>; diff --git a/docs/libs/v0/core/hub/defaultMalformedUrlHandler.mjs b/docs/libs/v0/core/hub/defaultMalformedUrlHandler.mjs new file mode 100644 index 0000000..63fbdcb --- /dev/null +++ b/docs/libs/v0/core/hub/defaultMalformedUrlHandler.mjs @@ -0,0 +1,12 @@ +import '../response/index.mjs'; +import '../steps/index.mjs'; +import { createHandlerStep } from '../steps/handler.mjs'; +import { ResponseContract } from '../response/contract.mjs'; + +const defaultMalformedUrlHandler = createHandlerStep({ + responseContract: ResponseContract.badRequest("malformed-url"), + theFunction: (__, { response }) => response("malformed-url", undefined), + metadata: [], +}); + +export { defaultMalformedUrlHandler }; diff --git a/docs/libs/v0/core/hub/defaultNotfoundHandler.d.ts b/docs/libs/v0/core/hub/defaultNotfoundHandler.d.ts index df89d65..045f08d 100644 --- a/docs/libs/v0/core/hub/defaultNotfoundHandler.d.ts +++ b/docs/libs/v0/core/hub/defaultNotfoundHandler.d.ts @@ -6,6 +6,6 @@ export declare const defaultNotfoundHandler: import("../steps").HandlerStep<{ readonly coerce: boolean; readonly checkers: readonly []; }>>>; - theFunction: (floor: import("..").Floor, { request, response }: import("../steps").HandlerStepFunctionParams | import("../response").PredictedResponse>) => never; + theFunction: (floor: import("..").Floor, { request, response }: import("../steps").HandlerStepFunctionParams | import("../response").PredictedResponse>) => never; metadata: never[]; }>; diff --git a/docs/libs/v0/core/hub/hooks.d.ts b/docs/libs/v0/core/hub/hooks.d.ts index 72d8808..8702a06 100644 --- a/docs/libs/v0/core/hub/hooks.d.ts +++ b/docs/libs/v0/core/hub/hooks.d.ts @@ -1,7 +1,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 RouterParams } from "../router"; import { type HttpServerParams } from "../types"; export declare const hookServerExitKind: import("@duplojs/utils").KindHandler>; export interface ServerHookExit extends Kind { @@ -19,7 +19,7 @@ export interface HttpServerErrorParams { readonly error: unknown; next(): ServerHookNext; exit(): ServerHookExit; - routerInitializationData: RouterInitializationData; + routerInitializationData: RouterParams; } export type HookServerError = (httpServerErrorParams: HttpServerErrorParams) => MaybePromise; export declare function serverErrorExitHookFunction(): Kind, unknown>; diff --git a/docs/libs/v0/core/hub/index.cjs b/docs/libs/v0/core/hub/index.cjs index 2adc7a6..38a4cf5 100644 --- a/docs/libs/v0/core/hub/index.cjs +++ b/docs/libs/v0/core/hub/index.cjs @@ -8,6 +8,8 @@ var index = require('../request/index.cjs'); var defaultNotfoundHandler = require('./defaultNotfoundHandler.cjs'); var defaultExtractContract = require('./defaultExtractContract.cjs'); var defaultBodyController = require('./defaultBodyController.cjs'); +var defaultMalformedUrlHandler = require('./defaultMalformedUrlHandler.cjs'); +var defaultEmptyReaderImplementation = require('./defaultEmptyReaderImplementation.cjs'); var hooks = require('./hooks.cjs'); var handler = require('../steps/handler.cjs'); @@ -18,13 +20,15 @@ class Hub extends utils.kindHeritage("hub", kind.createCoreLibKind("hub")) { hooksRouteLifeCycle = []; hooksHubLifeCycle = []; routes = new Set(); + routerFunctionBuilder = undefined; routeFunctionBuilders = []; stepFunctionBuilders = []; - bodyReaderImplementations = []; + bodyReaderImplementations = [defaultEmptyReaderImplementation.defaultEmptyReaderImplementation]; classRequest = index.Request; notfoundHandler = defaultNotfoundHandler.defaultNotfoundHandler; defaultExtractContract = defaultExtractContract.defaultExtractContract; defaultBodyController = defaultBodyController.defaultBodyController; + malformedUrlHandler = defaultMalformedUrlHandler.defaultMalformedUrlHandler; constructor(config) { super({}); this.config = config; @@ -33,6 +37,10 @@ class Hub extends utils.kindHeritage("hub", kind.createCoreLibKind("hub")) { 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; } + setRouterFunctionBuilder(functionBuilder) { + this.routerFunctionBuilder = functionBuilder; + return this; + } addRouteFunctionBuilder(functionBuilder) { this.routeFunctionBuilders.push(...utils.A.coalescing(functionBuilder)); return this; @@ -100,6 +108,14 @@ class Hub extends utils.kindHeritage("hub", kind.createCoreLibKind("hub")) { aggregatesHooksRouteLifeCycle(hookName) { return utils.A.flatMap(this.hooksRouteLifeCycle, (hooks) => hooks[hookName] ?? []); } + setMalformedUrlHandler(responseContract, theFunction) { + this.malformedUrlHandler = handler.createHandlerStep({ + responseContract, + theFunction: (__, params) => theFunction(params), + metadata: [], + }); + return this; + } /** * @internal */ @@ -114,6 +130,8 @@ function createHub(config) { exports.defaultNotfoundHandler = defaultNotfoundHandler.defaultNotfoundHandler; exports.defaultExtractContract = defaultExtractContract.defaultExtractContract; exports.defaultBodyController = defaultBodyController.defaultBodyController; +exports.defaultMalformedUrlHandler = defaultMalformedUrlHandler.defaultMalformedUrlHandler; +exports.defaultEmptyReaderImplementation = defaultEmptyReaderImplementation.defaultEmptyReaderImplementation; exports.createHookHubLifeCycle = hooks.createHookHubLifeCycle; exports.hookServerExitKind = hooks.hookServerExitKind; exports.hookServerNextKind = hooks.hookServerNextKind; diff --git a/docs/libs/v0/core/hub/index.d.ts b/docs/libs/v0/core/hub/index.d.ts index f552620..a3ce003 100644 --- a/docs/libs/v0/core/hub/index.d.ts +++ b/docs/libs/v0/core/hub/index.d.ts @@ -7,10 +7,13 @@ import { type ClientErrorResponseCode, type ResponseContract } from "../response import { type Environment } from "../types"; import { type createStepFunctionBuilder } from "../functionsBuilders/steps"; import { type createRouteFunctionBuilder } from "../functionsBuilders/route"; +import { type createRouterFunctionBuilder } from "../functionsBuilders/router"; export * from "./hooks"; export * from "./defaultNotfoundHandler"; export * from "./defaultExtractContract"; export * from "./defaultBodyController"; +export * from "./defaultMalformedUrlHandler"; +export * from "./defaultEmptyReaderImplementation"; export declare const hubKind: import("@duplojs/utils").KindHandler>; export interface HubConfig { readonly environment: Environment; @@ -33,6 +36,7 @@ export declare class Hub extends Hu hooksRouteLifeCycle: HookRouteLifeCycle[]; hooksHubLifeCycle: HookHubLifeCycle[]; routes: Set>; + routerFunctionBuilder: ReturnType | undefined; routeFunctionBuilders: ReturnType[]; stepFunctionBuilders: ReturnType[]; bodyReaderImplementations: BodyReaderImplementation[]; @@ -40,18 +44,21 @@ export declare class Hub extends Hu notfoundHandler: HandlerStep; defaultExtractContract: ResponseContract.Contract; defaultBodyController: BodyController; + malformedUrlHandler: HandlerStep; private constructor(); register(routes: Route | Iterable | Record): this; + setRouterFunctionBuilder(functionBuilder: ReturnType): 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; + 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)[]; + aggregatesHooksRouteLifeCycle(hookName: GenericHookName): (NonNullable extends infer T ? T extends NonNullable ? T extends readonly (infer InnerArr)[] ? InnerArr extends readonly (infer InnerArr)[] ? InnerArr : InnerArr : T : never : never)[]; + setMalformedUrlHandler>(responseContract: GenericResponseContract, theFunction: (param: HandlerStepFunctionParams) => MaybePromise): this; } 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 69ccddd..81c66ce 100644 --- a/docs/libs/v0/core/hub/index.mjs +++ b/docs/libs/v0/core/hub/index.mjs @@ -6,6 +6,8 @@ import { Request } from '../request/index.mjs'; import { defaultNotfoundHandler } from './defaultNotfoundHandler.mjs'; import { defaultExtractContract } from './defaultExtractContract.mjs'; import { defaultBodyController } from './defaultBodyController.mjs'; +import { defaultMalformedUrlHandler } from './defaultMalformedUrlHandler.mjs'; +import { defaultEmptyReaderImplementation } from './defaultEmptyReaderImplementation.mjs'; export { createHookHubLifeCycle, hookServerExitKind, hookServerNextKind, launchHookBeforeBuildRoute, launchHookServer, launchHookServerError, serverErrorExitHookFunction, serverErrorNextHookFunction } from './hooks.mjs'; import { createHandlerStep } from '../steps/handler.mjs'; @@ -16,13 +18,15 @@ class Hub extends kindHeritage("hub", createCoreLibKind("hub")) { hooksRouteLifeCycle = []; hooksHubLifeCycle = []; routes = new Set(); + routerFunctionBuilder = undefined; routeFunctionBuilders = []; stepFunctionBuilders = []; - bodyReaderImplementations = []; + bodyReaderImplementations = [defaultEmptyReaderImplementation]; classRequest = Request; notfoundHandler = defaultNotfoundHandler; defaultExtractContract = defaultExtractContract; defaultBodyController = defaultBodyController; + malformedUrlHandler = defaultMalformedUrlHandler; constructor(config) { super({}); this.config = config; @@ -31,6 +35,10 @@ class Hub extends kindHeritage("hub", createCoreLibKind("hub")) { 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; } + setRouterFunctionBuilder(functionBuilder) { + this.routerFunctionBuilder = functionBuilder; + return this; + } addRouteFunctionBuilder(functionBuilder) { this.routeFunctionBuilders.push(...A.coalescing(functionBuilder)); return this; @@ -98,6 +106,14 @@ class Hub extends kindHeritage("hub", createCoreLibKind("hub")) { aggregatesHooksRouteLifeCycle(hookName) { return A.flatMap(this.hooksRouteLifeCycle, (hooks) => hooks[hookName] ?? []); } + setMalformedUrlHandler(responseContract, theFunction) { + this.malformedUrlHandler = createHandlerStep({ + responseContract, + theFunction: (__, params) => theFunction(params), + metadata: [], + }); + return this; + } /** * @internal */ @@ -109,4 +125,4 @@ function createHub(config) { return Hub.new(config); } -export { Hub, createHub, defaultBodyController, defaultExtractContract, defaultNotfoundHandler, hubKind }; +export { Hub, createHub, defaultBodyController, defaultEmptyReaderImplementation, defaultExtractContract, defaultMalformedUrlHandler, defaultNotfoundHandler, hubKind }; diff --git a/docs/libs/v0/core/implementHttpServer.cjs b/docs/libs/v0/core/implementHttpServer.cjs index ceee359..9103223 100644 --- a/docs/libs/v0/core/implementHttpServer.cjs +++ b/docs/libs/v0/core/implementHttpServer.cjs @@ -1,13 +1,18 @@ 'use strict'; require('./hub/index.cjs'); -var index = require('./router/index.cjs'); +var index$1 = require('./router/index.cjs'); var utils = require('@duplojs/utils'); +var index = require('./defaultHooks/index.cjs'); var hooks = require('./hub/hooks.cjs'); async function implementHttpServer(params, initHttpServer) { await hooks.launchHookServer(params.hub.aggregatesHooksHubLifeCycle("beforeServerBuildRoutes"), params.hub, params.httpServerParams); - const router = await index.buildRouter(params.hub); + params.hub.addRouteHooks([ + index.initDefaultHook(params.hub, params.httpServerParams), + ...params.getInterfaceHooks(params), + ]); + const router = await index$1.createRouter(params.hub); await hooks.launchHookServer(params.hub.aggregatesHooksHubLifeCycle("beforeStartServer"), params.hub, params.httpServerParams); const serverErrorHooks = params.hub.aggregatesHooksHubLifeCycle("serverError"); function catchCriticalError(error) { diff --git a/docs/libs/v0/core/implementHttpServer.d.ts b/docs/libs/v0/core/implementHttpServer.d.ts index 099bdfb..5fe7c11 100644 --- a/docs/libs/v0/core/implementHttpServer.d.ts +++ b/docs/libs/v0/core/implementHttpServer.d.ts @@ -1,12 +1,18 @@ import { type Hub } from "./hub"; -import { type RouterInitializationData } from "./router"; -import { type MaybePromise } from "@duplojs/utils"; +import { type RouterParams } from "./router"; +import { type AnyTuple, type MaybePromise } from "@duplojs/utils"; import { type HttpServerParams } from "./types"; +import { type HookRouteLifeCycle } from "./route"; +export interface GetInterfaceHooksParams { + readonly hub: Hub; + readonly httpServerParams: HttpServerParams; +} export interface ImplementHttpServerParams { readonly hub: Hub; readonly httpServerParams: HttpServerParams; + getInterfaceHooks(params: GetInterfaceHooksParams): AnyTuple; } -export type ExecRouteSystem = (routerInitializationData: RouterInitializationData, whenUncaughtError: (error: unknown, routerInitializationData: RouterInitializationData) => MaybePromise) => Promise; +export type ExecRouteSystem = (routerInitializationData: RouterParams, whenUncaughtError: (error: unknown, routerInitializationData: RouterParams) => MaybePromise) => Promise; export interface InitHttpServerParams { readonly execRouteSystem: ExecRouteSystem; readonly httpServerParams: HttpServerParams; diff --git a/docs/libs/v0/core/implementHttpServer.mjs b/docs/libs/v0/core/implementHttpServer.mjs index df489ef..f4005d2 100644 --- a/docs/libs/v0/core/implementHttpServer.mjs +++ b/docs/libs/v0/core/implementHttpServer.mjs @@ -1,11 +1,16 @@ import './hub/index.mjs'; -import { buildRouter } from './router/index.mjs'; +import { createRouter } from './router/index.mjs'; import { forward } from '@duplojs/utils'; +import { initDefaultHook } from './defaultHooks/index.mjs'; import { launchHookServer, launchHookServerError, serverErrorNextHookFunction, serverErrorExitHookFunction } from './hub/hooks.mjs'; async function implementHttpServer(params, initHttpServer) { await launchHookServer(params.hub.aggregatesHooksHubLifeCycle("beforeServerBuildRoutes"), params.hub, params.httpServerParams); - const router = await buildRouter(params.hub); + params.hub.addRouteHooks([ + initDefaultHook(params.hub, params.httpServerParams), + ...params.getInterfaceHooks(params), + ]); + const router = await createRouter(params.hub); await launchHookServer(params.hub.aggregatesHooksHubLifeCycle("beforeStartServer"), params.hub, params.httpServerParams); const serverErrorHooks = params.hub.aggregatesHooksHubLifeCycle("serverError"); function catchCriticalError(error) { diff --git a/docs/libs/v0/core/index.cjs b/docs/libs/v0/core/index.cjs index def9d93..4db2011 100644 --- a/docs/libs/v0/core/index.cjs +++ b/docs/libs/v0/core/index.cjs @@ -13,13 +13,13 @@ var index$2 = require('./request/index.cjs'); var presetChecker$1 = require('./presetChecker.cjs'); var index$3 = require('./hub/index.cjs'); require('./functionsBuilders/index.cjs'); -var index$4 = require('./router/index.cjs'); +var index$6 = 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'); +var index$7 = require('./defaultHooks/index.cjs'); require('./errors/index.cjs'); var serverSentEvents = require('./serverSentEvents.cjs'); var checker = require('./builders/checker.cjs'); @@ -44,14 +44,17 @@ 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 empty = require('./request/bodyController/empty.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 defaultMalformedUrlHandler = require('./hub/defaultMalformedUrlHandler.cjs'); +var defaultEmptyReaderImplementation = require('./hub/defaultEmptyReaderImplementation.cjs'); var build = require('./functionsBuilders/route/build.cjs'); var create = require('./functionsBuilders/route/create.cjs'); -var _default = require('./functionsBuilders/route/default.cjs'); -var hook$1 = require('./functionsBuilders/route/hook.cjs'); +var hook$1 = require('./functionsBuilders/route/default/hook.cjs'); +var index$4 = require('./functionsBuilders/route/default/index.cjs'); var create$1 = require('./functionsBuilders/steps/create.cjs'); var checkerStep = require('./functionsBuilders/steps/defaults/checkerStep.cjs'); var cutStep = require('./functionsBuilders/steps/defaults/cutStep.cjs'); @@ -59,10 +62,13 @@ var handlerStep = require('./functionsBuilders/steps/defaults/handlerStep.cjs'); var extractStep = require('./functionsBuilders/steps/defaults/extractStep.cjs'); var processStep = require('./functionsBuilders/steps/defaults/processStep.cjs'); var build$1 = require('./functionsBuilders/steps/build.cjs'); +var create$2 = require('./functionsBuilders/router/create.cjs'); +var index$5 = require('./functionsBuilders/router/default/index.cjs'); +var build$2 = require('./functionsBuilders/router/build.cjs'); var pathToRegExp = require('./router/pathToRegExp.cjs'); var buildError = require('./router/buildError.cjs'); -var decodeUrl = require('./router/decodeUrl.cjs'); var notFoundBodyReaderImplementationError = require('./router/notFoundBodyReaderImplementationError.cjs'); +var createRouterElementSystem = require('./router/createRouterElementSystem.cjs'); var base$2 = require('./metadata/base.cjs'); var ignoreByRouteStore = require('./metadata/ignoreByRouteStore.cjs'); var wrongContentTypeError = require('./errors/wrongContentTypeError.cjs'); @@ -86,11 +92,11 @@ exports.presetCheckerKind = presetChecker$1.presetCheckerKind; exports.Hub = index$3.Hub; exports.createHub = index$3.createHub; exports.hubKind = index$3.hubKind; -exports.buildRouter = index$4.buildRouter; +exports.createRouter = index$6.createRouter; exports.createCoreLibStringIdentifier = stringIdentifier.createCoreLibStringIdentifier; exports.implementHttpServer = implementHttpServer.implementHttpServer; exports.createNarrowingInput = narrowingInput.createNarrowingInput; -exports.initDefaultHook = index$5.initDefaultHook; +exports.initDefaultHook = index$7.initDefaultHook; Object.defineProperty(exports, "ServerSentEvents", { enumerable: true, get: function () { return serverSentEvents.ServerSentEvents; } @@ -130,11 +136,14 @@ exports.createProcessStep = process.createProcessStep; exports.processStepKind = process.processStepKind; exports.createPresetCheckerStep = presetChecker.createPresetCheckerStep; exports.presetCheckerStepKind = presetChecker.presetCheckerStepKind; +exports.WrongBodyReaderImplementationError = base$1.WrongBodyReaderImplementationError; exports.createBodyController = base$1.createBodyController; exports.FormDataBodyController = formData.FormDataBodyController; exports.controlBodyAsFormData = formData.controlBodyAsFormData; exports.TextBodyController = text.TextBodyController; exports.controlBodyAsText = text.controlBodyAsText; +exports.EmptyBodyController = empty.EmptyBodyController; +exports.controlBodyAsEmpty = empty.controlBodyAsEmpty; exports.createHookHubLifeCycle = hooks$1.createHookHubLifeCycle; exports.hookServerExitKind = hooks$1.hookServerExitKind; exports.hookServerNextKind = hooks$1.hookServerNextKind; @@ -146,15 +155,17 @@ exports.serverErrorNextHookFunction = hooks$1.serverErrorNextHookFunction; exports.defaultNotfoundHandler = defaultNotfoundHandler.defaultNotfoundHandler; exports.defaultExtractContract = defaultExtractContract.defaultExtractContract; exports.defaultBodyController = defaultBodyController.defaultBodyController; +exports.defaultMalformedUrlHandler = defaultMalformedUrlHandler.defaultMalformedUrlHandler; +exports.defaultEmptyReaderImplementation = defaultEmptyReaderImplementation.defaultEmptyReaderImplementation; exports.buildRouteFunction = build.buildRouteFunction; exports.createRouteFunctionBuilder = create.createRouteFunctionBuilder; -exports.defaultRouteFunctionBuilder = _default.defaultRouteFunctionBuilder; exports.buildHookAfter = hook$1.buildHookAfter; exports.buildHookBefore = hook$1.buildHookBefore; exports.buildHookErrorBefore = hook$1.buildHookErrorBefore; exports.createHookResponse = hook$1.createHookResponse; exports.exitHookFunction = hook$1.exitHookFunction; exports.nextHookFunction = hook$1.nextHookFunction; +exports.defaultRouteFunctionBuilder = index$4.defaultRouteFunctionBuilder; exports.createStepFunctionBuilder = create$1.createStepFunctionBuilder; exports.defaultCheckerStepFunctionBuilder = checkerStep.defaultCheckerStepFunctionBuilder; exports.defaultCutStepFunctionBuilder = cutStep.defaultCutStepFunctionBuilder; @@ -163,12 +174,13 @@ exports.defaultExtractStepFunctionBuilder = extractStep.defaultExtractStepFuncti exports.buildStepsFunction = processStep.buildStepsFunction; exports.defaultProcessStepFunctionBuilder = processStep.defaultProcessStepFunctionBuilder; exports.buildStepFunction = build$1.buildStepFunction; +exports.createRouterFunctionBuilder = create$2.createRouterFunctionBuilder; +exports.defaultRouterFunctionBuilder = index$5.defaultRouterFunctionBuilder; +exports.buildRouterFunction = build$2.buildRouterFunction; exports.pathToRegExp = pathToRegExp.pathToRegExp; exports.RouterBuildError = buildError.RouterBuildError; -exports.decodeUrl = decodeUrl.decodeUrl; -exports.regexQueryAnalyser = decodeUrl.regexQueryAnalyser; -exports.regexUrlAnalyser = decodeUrl.regexUrlAnalyser; exports.NotFoundBodyReaderImplementationError = notFoundBodyReaderImplementationError.NotFoundBodyReaderImplementationError; +exports.createRouterElementSystem = createRouterElementSystem.createRouterElementSystem; exports.createMetadata = base$2.createMetadata; exports.metadataKind = base$2.metadataKind; exports.IgnoreByRouteStoreMetadata = ignoreByRouteStore.IgnoreByRouteStoreMetadata; diff --git a/docs/libs/v0/core/index.mjs b/docs/libs/v0/core/index.mjs index c122424..20d29c2 100644 --- a/docs/libs/v0/core/index.mjs +++ b/docs/libs/v0/core/index.mjs @@ -11,7 +11,7 @@ export { Request } from './request/index.mjs'; export { createPresetChecker, presetCheckerKind } from './presetChecker.mjs'; export { Hub, createHub, hubKind } from './hub/index.mjs'; import './functionsBuilders/index.mjs'; -export { buildRouter } from './router/index.mjs'; +export { createRouter } from './router/index.mjs'; export { createCoreLibStringIdentifier } from './stringIdentifier.mjs'; import './metadata/index.mjs'; export { implementHttpServer } from './implementHttpServer.mjs'; @@ -39,17 +39,20 @@ 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 { WrongBodyReaderImplementationError, createBodyController } from './request/bodyController/base.mjs'; export { FormDataBodyController, controlBodyAsFormData } from './request/bodyController/formData.mjs'; export { TextBodyController, controlBodyAsText } from './request/bodyController/text.mjs'; +export { EmptyBodyController, controlBodyAsEmpty } from './request/bodyController/empty.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 { defaultMalformedUrlHandler } from './hub/defaultMalformedUrlHandler.mjs'; +export { defaultEmptyReaderImplementation } from './hub/defaultEmptyReaderImplementation.mjs'; export { buildRouteFunction } from './functionsBuilders/route/build.mjs'; export { createRouteFunctionBuilder } from './functionsBuilders/route/create.mjs'; -export { defaultRouteFunctionBuilder } from './functionsBuilders/route/default.mjs'; -export { buildHookAfter, buildHookBefore, buildHookErrorBefore, createHookResponse, exitHookFunction, nextHookFunction } from './functionsBuilders/route/hook.mjs'; +export { buildHookAfter, buildHookBefore, buildHookErrorBefore, createHookResponse, exitHookFunction, nextHookFunction } from './functionsBuilders/route/default/hook.mjs'; +export { defaultRouteFunctionBuilder } from './functionsBuilders/route/default/index.mjs'; export { createStepFunctionBuilder } from './functionsBuilders/steps/create.mjs'; export { defaultCheckerStepFunctionBuilder } from './functionsBuilders/steps/defaults/checkerStep.mjs'; export { defaultCutStepFunctionBuilder } from './functionsBuilders/steps/defaults/cutStep.mjs'; @@ -57,10 +60,13 @@ export { defaultHandlerStepFunctionBuilder } from './functionsBuilders/steps/def export { defaultExtractStepFunctionBuilder } from './functionsBuilders/steps/defaults/extractStep.mjs'; export { buildStepsFunction, defaultProcessStepFunctionBuilder } from './functionsBuilders/steps/defaults/processStep.mjs'; export { buildStepFunction } from './functionsBuilders/steps/build.mjs'; +export { createRouterFunctionBuilder } from './functionsBuilders/router/create.mjs'; +export { defaultRouterFunctionBuilder } from './functionsBuilders/router/default/index.mjs'; +export { buildRouterFunction } from './functionsBuilders/router/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 { createRouterElementSystem } from './router/createRouterElementSystem.mjs'; export { createMetadata, metadataKind } from './metadata/base.mjs'; export { IgnoreByRouteStoreMetadata } from './metadata/ignoreByRouteStore.mjs'; export { WrongContentTypeError } from './errors/wrongContentTypeError.mjs'; diff --git a/docs/libs/v0/core/process/index.d.ts b/docs/libs/v0/core/process/index.d.ts index ea42733..0d4ef0e 100644 --- a/docs/libs/v0/core/process/index.d.ts +++ b/docs/libs/v0/core/process/index.d.ts @@ -2,30 +2,23 @@ import { type IsEqual, type Kind, type O } from "@duplojs/utils"; import { type ProcessStep, type CheckerStep, type CutStep, type ExtractStep, type stepKind, type PresetCheckerStep } from "../steps"; import { type Floor } from "../floor"; import { type HookRouteLifeCycle } from "../route"; -import { type Request } from "../request"; import { type Metadata } from "../metadata"; export * from "./types"; export interface ProcessStepsCustom { } export type ProcessSteps = (ProcessStepsCustom[O.GetPropsWithValueExtends>] | CheckerStep | ExtractStep | PresetCheckerStep | CutStep | ProcessStep); declare const SymbolProcessExportValue: unique symbol; -declare const SymbolProcessRequest: unique symbol; export interface ProcessDefinition { steps: readonly ProcessSteps[]; options?: Record; readonly hooks: readonly HookRouteLifeCycle[]; readonly metadata: readonly Metadata[]; [SymbolProcessExportValue]?: Floor; - [SymbolProcessRequest]?: Request; } export interface ProcessExportValue { [SymbolProcessExportValue]: GenericExportValue; } export type GetProcessExportValue = IsEqual extends true ? never : GenericProcess["definition"][typeof SymbolProcessExportValue]; -export interface ProcessRequest { - [SymbolProcessRequest]: GenericRequest; -} -export type GetProcessRequest = IsEqual extends true ? never : GenericProcess["definition"][typeof SymbolProcessRequest]; export declare const processKind: import("@duplojs/utils").KindHandler>; export interface Process extends Kind { definition: GenericDefinition; diff --git a/docs/libs/v0/core/request/bodyController/base.cjs b/docs/libs/v0/core/request/bodyController/base.cjs index 2c382fb..9abc9e2 100644 --- a/docs/libs/v0/core/request/bodyController/base.cjs +++ b/docs/libs/v0/core/request/bodyController/base.cjs @@ -7,20 +7,37 @@ 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"); +class WrongBodyReaderImplementationError extends utils.kindHeritage("wrong-body-reader-implementation", kind.createCoreLibKind("wrong-body-reader-implementation"), Error) { + controllerName; + bodyReaderImplementation; + constructor(controllerName, bodyReaderImplementation) { + super({}, ["Received wrong body reader implementation."]); + this.controllerName = controllerName; + this.bodyReaderImplementation = bodyReaderImplementation; + } +} function createBodyController(name) { return bodyControllerHandlerKind.setTo({ name, create(params) { + function tryToCreateReader(readerImplementation) { + if (bodyReaderImplementationKind.getValue(readerImplementation) !== name) { + return utils.E.fail(); + } + return utils.E.success(bodyReaderKind.setTo({ + read: (request) => readerImplementation.read(request, params), + }, name)); + } return bodyControllerKind.setTo({ name, params, - tryToCreateReader(readerImplementation) { - if (bodyReaderImplementationKind.getValue(readerImplementation) !== name) { - return utils.E.fail(); + tryToCreateReader, + createReaderOrThrow(readerImplementation) { + const result = tryToCreateReader(readerImplementation); + if (utils.E.isLeft(result)) { + throw new WrongBodyReaderImplementationError(name, readerImplementation); } - return utils.E.success(bodyReaderKind.setTo({ - read: (request) => readerImplementation.read(request, params), - }, name)); + return utils.unwrap(result); }, }, name); }, @@ -33,4 +50,5 @@ function createBodyController(name) { }); } +exports.WrongBodyReaderImplementationError = WrongBodyReaderImplementationError; 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 index 849d834..033e529 100644 --- a/docs/libs/v0/core/request/bodyController/base.d.ts +++ b/docs/libs/v0/core/request/bodyController/base.d.ts @@ -16,6 +16,7 @@ export interface BodyController> | E.Fail; + createReaderOrThrow(readerImplementation: BodyReaderImplementation): BodyReader; } declare const bodyControllerHandlerKind: import("@duplojs/utils").KindHandler>; export interface BodyControllerHandler extends Kind { @@ -24,5 +25,13 @@ export interface BodyControllerHandler["read"]): BodyReaderImplementation; is(input: unknown): input is BodyController; } +declare const WrongBodyReaderImplementationError_base: new (params: { + "@DuplojsHttpCore/wrong-body-reader-implementation"?: unknown; +}, parentParams: readonly [message?: string | undefined, options?: ErrorOptions | undefined]) => Error & Kind, unknown> & Kind, unknown>; +export declare class WrongBodyReaderImplementationError extends WrongBodyReaderImplementationError_base { + controllerName: string; + bodyReaderImplementation: BodyReaderImplementation; + constructor(controllerName: string, bodyReaderImplementation: BodyReaderImplementation); +} 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 index d48b105..d87b8bd 100644 --- a/docs/libs/v0/core/request/bodyController/base.mjs +++ b/docs/libs/v0/core/request/bodyController/base.mjs @@ -1,24 +1,41 @@ import { createCoreLibKind } from '../../kind.mjs'; -import { E } from '@duplojs/utils'; +import { kindHeritage, E, unwrap } 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"); +class WrongBodyReaderImplementationError extends kindHeritage("wrong-body-reader-implementation", createCoreLibKind("wrong-body-reader-implementation"), Error) { + controllerName; + bodyReaderImplementation; + constructor(controllerName, bodyReaderImplementation) { + super({}, ["Received wrong body reader implementation."]); + this.controllerName = controllerName; + this.bodyReaderImplementation = bodyReaderImplementation; + } +} function createBodyController(name) { return bodyControllerHandlerKind.setTo({ name, create(params) { + function tryToCreateReader(readerImplementation) { + if (bodyReaderImplementationKind.getValue(readerImplementation) !== name) { + return E.fail(); + } + return E.success(bodyReaderKind.setTo({ + read: (request) => readerImplementation.read(request, params), + }, name)); + } return bodyControllerKind.setTo({ name, params, - tryToCreateReader(readerImplementation) { - if (bodyReaderImplementationKind.getValue(readerImplementation) !== name) { - return E.fail(); + tryToCreateReader, + createReaderOrThrow(readerImplementation) { + const result = tryToCreateReader(readerImplementation); + if (E.isLeft(result)) { + throw new WrongBodyReaderImplementationError(name, readerImplementation); } - return E.success(bodyReaderKind.setTo({ - read: (request) => readerImplementation.read(request, params), - }, name)); + return unwrap(result); }, }, name); }, @@ -31,4 +48,4 @@ function createBodyController(name) { }); } -export { createBodyController }; +export { WrongBodyReaderImplementationError, createBodyController }; diff --git a/docs/libs/v0/core/request/bodyController/empty.cjs b/docs/libs/v0/core/request/bodyController/empty.cjs new file mode 100644 index 0000000..28f369a --- /dev/null +++ b/docs/libs/v0/core/request/bodyController/empty.cjs @@ -0,0 +1,11 @@ +'use strict'; + +var base = require('./base.cjs'); + +const EmptyBodyController = base.createBodyController("empty"); +function controlBodyAsEmpty() { + return EmptyBodyController.create({}); +} + +exports.EmptyBodyController = EmptyBodyController; +exports.controlBodyAsEmpty = controlBodyAsEmpty; diff --git a/docs/libs/v0/core/request/bodyController/empty.d.ts b/docs/libs/v0/core/request/bodyController/empty.d.ts new file mode 100644 index 0000000..b87bac7 --- /dev/null +++ b/docs/libs/v0/core/request/bodyController/empty.d.ts @@ -0,0 +1,3 @@ +export declare const EmptyBodyController: import("./base").BodyControllerHandler<"empty", {}>; +export type EmptyBodyController = typeof EmptyBodyController; +export declare function controlBodyAsEmpty(): import("./base").BodyController<"empty", {}>; diff --git a/docs/libs/v0/core/request/bodyController/empty.mjs b/docs/libs/v0/core/request/bodyController/empty.mjs new file mode 100644 index 0000000..241fd7d --- /dev/null +++ b/docs/libs/v0/core/request/bodyController/empty.mjs @@ -0,0 +1,8 @@ +import { createBodyController } from './base.mjs'; + +const EmptyBodyController = createBodyController("empty"); +function controlBodyAsEmpty() { + return EmptyBodyController.create({}); +} + +export { EmptyBodyController, controlBodyAsEmpty }; diff --git a/docs/libs/v0/core/request/bodyController/formData.cjs b/docs/libs/v0/core/request/bodyController/formData.cjs index 44eb4f5..2d51bd6 100644 --- a/docs/libs/v0/core/request/bodyController/formData.cjs +++ b/docs/libs/v0/core/request/bodyController/formData.cjs @@ -9,6 +9,7 @@ function controlBodyAsFormData(params) { maxFileQuantity: params.maxFileQuantity, bodyMaxSize: params.bodyMaxSize && utils.stringToBytes(params.bodyMaxSize), fileMaxSize: params.fileMaxSize && utils.stringToBytes(params.fileMaxSize), + textFieldMaxSize: params.textFieldMaxSize && utils.stringToBytes(params.textFieldMaxSize), mimeType: params.mimeType !== undefined ? utils.toRegExp(params.mimeType) : undefined, diff --git a/docs/libs/v0/core/request/bodyController/formData.d.ts b/docs/libs/v0/core/request/bodyController/formData.d.ts index e152d1c..fb3fd82 100644 --- a/docs/libs/v0/core/request/bodyController/formData.d.ts +++ b/docs/libs/v0/core/request/bodyController/formData.d.ts @@ -1,9 +1,10 @@ -import { type BytesInString } from "@duplojs/utils"; +import { type AnyTuple, type BytesInString } from "@duplojs/utils"; import { type BodyControllerParams } from "./base"; export interface FormDataBodyReaderParams extends BodyControllerParams { maxFileQuantity: number; mimeType?: RegExp; fileMaxSize?: number; + textFieldMaxSize?: number; maxBufferSize: number; maxIndexArray: number; maxKeyLength: number; @@ -12,9 +13,10 @@ export declare const FormDataBodyController: import("./base").BodyControllerHand export type FormDataBodyController = typeof FormDataBodyController; export interface ControlBodyAsFormDataParams { maxFileQuantity: number; - mimeType?: string | string[] | RegExp; + mimeType?: string | AnyTuple | RegExp; bodyMaxSize?: number | BytesInString; fileMaxSize?: number | BytesInString; + textFieldMaxSize?: number | BytesInString; maxBufferSize?: number | BytesInString; maxIndexArray?: number; maxKeyLength?: number; diff --git a/docs/libs/v0/core/request/bodyController/formData.mjs b/docs/libs/v0/core/request/bodyController/formData.mjs index 2240bd4..70809d0 100644 --- a/docs/libs/v0/core/request/bodyController/formData.mjs +++ b/docs/libs/v0/core/request/bodyController/formData.mjs @@ -7,6 +7,7 @@ function controlBodyAsFormData(params) { maxFileQuantity: params.maxFileQuantity, bodyMaxSize: params.bodyMaxSize && stringToBytes(params.bodyMaxSize), fileMaxSize: params.fileMaxSize && stringToBytes(params.fileMaxSize), + textFieldMaxSize: params.textFieldMaxSize && stringToBytes(params.textFieldMaxSize), mimeType: params.mimeType !== undefined ? toRegExp(params.mimeType) : undefined, diff --git a/docs/libs/v0/core/request/bodyController/index.cjs b/docs/libs/v0/core/request/bodyController/index.cjs index 0bf60bc..0681b66 100644 --- a/docs/libs/v0/core/request/bodyController/index.cjs +++ b/docs/libs/v0/core/request/bodyController/index.cjs @@ -3,11 +3,15 @@ var base = require('./base.cjs'); var formData = require('./formData.cjs'); var text = require('./text.cjs'); +var empty = require('./empty.cjs'); +exports.WrongBodyReaderImplementationError = base.WrongBodyReaderImplementationError; exports.createBodyController = base.createBodyController; exports.FormDataBodyController = formData.FormDataBodyController; exports.controlBodyAsFormData = formData.controlBodyAsFormData; exports.TextBodyController = text.TextBodyController; exports.controlBodyAsText = text.controlBodyAsText; +exports.EmptyBodyController = empty.EmptyBodyController; +exports.controlBodyAsEmpty = empty.controlBodyAsEmpty; diff --git a/docs/libs/v0/core/request/bodyController/index.d.ts b/docs/libs/v0/core/request/bodyController/index.d.ts index b499ebc..d81f708 100644 --- a/docs/libs/v0/core/request/bodyController/index.d.ts +++ b/docs/libs/v0/core/request/bodyController/index.d.ts @@ -1,3 +1,4 @@ export * from "./base"; export * from "./formData"; export * from "./text"; +export * from "./empty"; diff --git a/docs/libs/v0/core/request/bodyController/index.mjs b/docs/libs/v0/core/request/bodyController/index.mjs index f382203..8a88cb2 100644 --- a/docs/libs/v0/core/request/bodyController/index.mjs +++ b/docs/libs/v0/core/request/bodyController/index.mjs @@ -1,3 +1,4 @@ -export { createBodyController } from './base.mjs'; +export { WrongBodyReaderImplementationError, createBodyController } from './base.mjs'; export { FormDataBodyController, controlBodyAsFormData } from './formData.mjs'; export { TextBodyController, controlBodyAsText } from './text.mjs'; +export { EmptyBodyController, controlBodyAsEmpty } from './empty.mjs'; diff --git a/docs/libs/v0/core/route/hooks.cjs b/docs/libs/v0/core/route/hooks.cjs index f43cfd1..77deeab 100644 --- a/docs/libs/v0/core/route/hooks.cjs +++ b/docs/libs/v0/core/route/hooks.cjs @@ -4,14 +4,8 @@ var kind = require('../kind.cjs'); const hookRouteExitKind = kind.createCoreLibKind("route-hook-exit"); const hookRouteNextKind = kind.createCoreLibKind("route-hook-next"); -function createHookRouteLifeCycle(...args) { - if (args.length === 1) { - return args[0]; - } - return { - ...args[1], - onConstructRequest: args[0], - }; +function createHookRouteLifeCycle(hookRouteLifeCycle) { + return hookRouteLifeCycle; } exports.createHookRouteLifeCycle = createHookRouteLifeCycle; diff --git a/docs/libs/v0/core/route/hooks.d.ts b/docs/libs/v0/core/route/hooks.d.ts index 191aa36..ee90580 100644 --- a/docs/libs/v0/core/route/hooks.d.ts +++ b/docs/libs/v0/core/route/hooks.d.ts @@ -1,25 +1,20 @@ import { type Request } from "../request"; -import { type UnionToIntersection, type AnyFunction, type Kind, type MaybePromise, type SimplifyTopLevel, type IsEqual } from "@duplojs/utils"; +import { type Kind, type MaybePromise } from "@duplojs/utils"; import { type HookResponse } from "../response"; import { type ResponseCode, type Response } from "../response"; -export interface HookParamsOnConstructRequest { - request: Request; - addRequestProperties>(newProperties: GenericNewProperties): Request & GenericNewProperties; -} -export type HookOnConstructRequest = (params: HookParamsOnConstructRequest) => MaybePromise; export declare const hookRouteExitKind: import("@duplojs/utils").KindHandler>; export interface RouteHookExit extends Kind { } export declare const hookRouteNextKind: import("@duplojs/utils").KindHandler>; export interface RouteHookNext extends Kind { } -export interface RouteHookParams { - readonly request: GenericRequest; +export interface RouteHookParams { + readonly request: Request; next(): RouteHookNext; exit(): RouteHookExit; response(code: GenericCode, information: GenericInformation, body?: GenericBody): HookResponse; } -export type HookBeforeRouteExecution = (params: RouteHookParams) => MaybePromise; +export type HookBeforeRouteExecution = (params: RouteHookParams) => MaybePromise; export interface RouteHookErrorParams { readonly request: GenericRequest; readonly error: unknown; @@ -28,31 +23,20 @@ export interface RouteHookErrorParams response(code: GenericCode, information: GenericInformation, body?: GenericBody): HookResponse; } export type HookError = (params: RouteHookErrorParams) => MaybePromise; -export interface RouteHookParamsAfter { - readonly request: GenericRequest; +export interface RouteHookParamsAfter { + readonly request: Request; readonly currentResponse: Response; next(): RouteHookNext; exit(): RouteHookExit; } -export type HookBeforeSendResponse = (params: RouteHookParamsAfter) => MaybePromise; -export type HookSendResponse = (params: RouteHookParamsAfter) => MaybePromise; -export type HookAfterSendResponse = (params: RouteHookParamsAfter) => MaybePromise; -export interface HookRouteLifeCycle { - onConstructRequest?: HookOnConstructRequest; - beforeRouteExecution?: HookBeforeRouteExecution; +export type HookBeforeSendResponse = (params: RouteHookParamsAfter) => MaybePromise; +export type HookSendResponse = (params: RouteHookParamsAfter) => MaybePromise; +export type HookAfterSendResponse = (params: RouteHookParamsAfter) => MaybePromise; +export interface HookRouteLifeCycle { + beforeRouteExecution?: HookBeforeRouteExecution; error?: HookError; - beforeSendResponse?: HookBeforeSendResponse; - sendResponse?: HookSendResponse; - afterSendResponse?: HookAfterSendResponse; + beforeSendResponse?: HookBeforeSendResponse; + sendResponse?: HookSendResponse; + afterSendResponse?: HookAfterSendResponse; } -export declare function createHookRouteLifeCycle, "onConstructRequest">>(hookRouteLifeCycle: GenericHookLiveCycle): GenericHookLiveCycle; -export declare function createHookRouteLifeCycle>>, "onConstructRequest">>(onConstructRequest: GenericOnConstructRequest, hookRouteLifeCycle: GenericHookLiveCycle): SimplifyTopLevel<{ - readonly onConstructRequest: GenericOnConstructRequest; -} & GenericHookLiveCycle>; -export type ExtractRequestFromHooks = GenericHooks extends readonly [ - infer InferredFirst, - ...infer InferredRest extends HookRouteLifeCycle[] -] ? (InferredFirst extends { - onConstructRequest: AnyFunction; -} ? Awaited> : never) extends infer InferredResultFirst ? InferredRest extends readonly [] ? InferredResultFirst : ExtractRequestFromHooks extends infer InferredResultRest ? InferredResultFirst | InferredResultRest : never : never : never; -export type MakeRequestFromHooks = ExtractRequestFromHooks extends infer InferredResult extends Request ? IsEqual extends true ? never : UnionToIntersection : never; +export declare function createHookRouteLifeCycle(hookRouteLifeCycle: GenericHookLiveCycle): GenericHookLiveCycle; diff --git a/docs/libs/v0/core/route/hooks.mjs b/docs/libs/v0/core/route/hooks.mjs index e1235c0..db5b580 100644 --- a/docs/libs/v0/core/route/hooks.mjs +++ b/docs/libs/v0/core/route/hooks.mjs @@ -2,14 +2,8 @@ import { createCoreLibKind } from '../kind.mjs'; const hookRouteExitKind = createCoreLibKind("route-hook-exit"); const hookRouteNextKind = createCoreLibKind("route-hook-next"); -function createHookRouteLifeCycle(...args) { - if (args.length === 1) { - return args[0]; - } - return { - ...args[1], - onConstructRequest: args[0], - }; +function createHookRouteLifeCycle(hookRouteLifeCycle) { + return hookRouteLifeCycle; } export { createHookRouteLifeCycle, hookRouteExitKind, hookRouteNextKind }; diff --git a/docs/libs/v0/core/router/createRouterElementSystem.cjs b/docs/libs/v0/core/router/createRouterElementSystem.cjs new file mode 100644 index 0000000..af26be1 --- /dev/null +++ b/docs/libs/v0/core/router/createRouterElementSystem.cjs @@ -0,0 +1,34 @@ +'use strict'; + +var utils = require('@duplojs/utils'); +require('../functionsBuilders/index.cjs'); +require('../request/index.cjs'); +var index = require('../route/index.cjs'); +var buildError = require('./buildError.cjs'); +require('../hub/index.cjs'); +var empty = require('../request/bodyController/empty.cjs'); +var defaultEmptyReaderImplementation = require('../hub/defaultEmptyReaderImplementation.cjs'); +var build = require('../functionsBuilders/route/build.cjs'); + +async function createRouterElementSystem(params) { + const bodyController = empty.controlBodyAsEmpty(); + const bodyReader = bodyController.createReaderOrThrow(defaultEmptyReaderImplementation.defaultEmptyReaderImplementation); + const route = index.createRoute({ + method: "GET", + paths: ["/"], + hooks: [], + preflightSteps: [], + steps: [params.handlerStep], + metadata: [], + bodyController, + }); + const buildedRoute = await utils.asyncPipe(build.buildRouteFunction(route, params.buildParams), utils.E.whenIsLeft((element) => { + throw new buildError.RouterBuildError(route, element); + }), utils.unwrap); + return { + bodyReader, + buildedRoute, + }; +} + +exports.createRouterElementSystem = createRouterElementSystem; diff --git a/docs/libs/v0/core/router/createRouterElementSystem.d.ts b/docs/libs/v0/core/router/createRouterElementSystem.d.ts new file mode 100644 index 0000000..5247d25 --- /dev/null +++ b/docs/libs/v0/core/router/createRouterElementSystem.d.ts @@ -0,0 +1,9 @@ +import { type BuildRouteFunctionParams } from "../functionsBuilders"; +import { type HandlerStep } from "../steps"; +import { type RouterElementSystem } from "./types"; +interface CreateRouterElementSystemParams { + handlerStep: HandlerStep; + buildParams: BuildRouteFunctionParams; +} +export declare function createRouterElementSystem(params: CreateRouterElementSystemParams): Promise; +export {}; diff --git a/docs/libs/v0/core/router/createRouterElementSystem.mjs b/docs/libs/v0/core/router/createRouterElementSystem.mjs new file mode 100644 index 0000000..22351ac --- /dev/null +++ b/docs/libs/v0/core/router/createRouterElementSystem.mjs @@ -0,0 +1,32 @@ +import { asyncPipe, E, unwrap } from '@duplojs/utils'; +import '../functionsBuilders/index.mjs'; +import '../request/index.mjs'; +import { createRoute } from '../route/index.mjs'; +import { RouterBuildError } from './buildError.mjs'; +import '../hub/index.mjs'; +import { controlBodyAsEmpty } from '../request/bodyController/empty.mjs'; +import { defaultEmptyReaderImplementation } from '../hub/defaultEmptyReaderImplementation.mjs'; +import { buildRouteFunction } from '../functionsBuilders/route/build.mjs'; + +async function createRouterElementSystem(params) { + const bodyController = controlBodyAsEmpty(); + const bodyReader = bodyController.createReaderOrThrow(defaultEmptyReaderImplementation); + const route = createRoute({ + method: "GET", + paths: ["/"], + hooks: [], + preflightSteps: [], + steps: [params.handlerStep], + metadata: [], + bodyController, + }); + const buildedRoute = await asyncPipe(buildRouteFunction(route, params.buildParams), E.whenIsLeft((element) => { + throw new RouterBuildError(route, element); + }), unwrap); + return { + bodyReader, + buildedRoute, + }; +} + +export { createRouterElementSystem }; diff --git a/docs/libs/v0/core/router/index.cjs b/docs/libs/v0/core/router/index.cjs index 81093cd..f8b813e 100644 --- a/docs/libs/v0/core/router/index.cjs +++ b/docs/libs/v0/core/router/index.cjs @@ -3,15 +3,12 @@ require('../hub/index.cjs'); var utils = require('@duplojs/utils'); var pathToRegExp = require('./pathToRegExp.cjs'); -var index = require('../route/index.cjs'); var buildError = require('./buildError.cjs'); -require('../functionsBuilders/route/index.cjs'); -var decodeUrl = require('./decodeUrl.cjs'); -var text = require('../request/bodyController/text.cjs'); var notFoundBodyReaderImplementationError = require('./notFoundBodyReaderImplementationError.cjs'); require('../functionsBuilders/index.cjs'); +var createRouterElementSystem = require('./createRouterElementSystem.cjs'); require('./types/index.cjs'); -var _default = require('../functionsBuilders/route/default.cjs'); +var index = require('../functionsBuilders/route/default/index.cjs'); var checkerStep = require('../functionsBuilders/steps/defaults/checkerStep.cjs'); var cutStep = require('../functionsBuilders/steps/defaults/cutStep.cjs'); var handlerStep = require('../functionsBuilders/steps/defaults/handlerStep.cjs'); @@ -19,13 +16,15 @@ var extractStep = require('../functionsBuilders/steps/defaults/extractStep.cjs') var processStep = require('../functionsBuilders/steps/defaults/processStep.cjs'); var hooks = require('../hub/hooks.cjs'); var build = require('../functionsBuilders/route/build.cjs'); +var build$1 = require('../functionsBuilders/router/build.cjs'); +var index$1 = require('../functionsBuilders/router/default/index.cjs'); -async function buildRouter(hub) { +async function createRouter(hub) { const { environment } = hub.config; const { hooksRouteLifeCycle, routes, hooksHubLifeCycle, bodyReaderImplementations, } = hub; const routeFunctionBuilders = [ ...hub.routeFunctionBuilders, - _default.defaultRouteFunctionBuilder, + index.defaultRouteFunctionBuilder, ]; const stepFunctionBuilders = [ ...hub.stepFunctionBuilders, @@ -43,7 +42,7 @@ async function buildRouter(hub) { routeFunctionBuilders, defaultExtractContract: hub.defaultExtractContract, }; - const groupedRoute = await utils.G.asyncReduce(routes, utils.G.reduceFrom({}), async ({ lastValue, element: route, nextWithObject, }) => { + const routerElementWrapper = await utils.G.asyncReduce(routes, utils.G.reduceFrom({}), async ({ lastValue, element: route, nextWithObject, }) => { const routeAfterHook = await hooks.launchHookBeforeBuildRoute(hooksBeforeBuildRoute, route); const buildedRoute = await build.buildRouteFunction(routeAfterHook, buildParams); if (utils.E.isLeft(buildedRoute)) { @@ -63,60 +62,23 @@ async function buildRouter(hub) { }))), }); }); - 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: ["/"], - hooks: [], - preflightSteps: [], - steps: [hub.notfoundHandler], - metadata: [], - bodyController: bodyControllerNotfoundRoute, - }), async (route) => { - const result = await build.buildRouteFunction(route, buildParams); - return utils.E.whenIsLeft(result, (element) => { - throw new buildError.RouterBuildError(route, element); - }); - }, utils.unwrap); - const Request = hub.classRequest; + const notfoundRouterElement = await createRouterElementSystem.createRouterElementSystem({ + handlerStep: hub.notfoundHandler, + buildParams, + }); + const malformedUrlRouterElement = await createRouterElementSystem.createRouterElementSystem({ + handlerStep: hub.malformedUrlHandler, + buildParams, + }); return { - exec: (initializationData) => { - const routerElements = groupedRoute[initializationData.method]; - const decodedUrl = decodeUrl.decodeUrl(initializationData.url); - if (!routerElements) { - return buildedNotfoundRoute(new Request({ - ...initializationData, - ...decodedUrl, - params: {}, - matchedPath: null, - bodyReader: bodyReaderNotFoundRoute, - })); - } - // eslint-disable-next-line @typescript-eslint/prefer-for-of - for (let index = 0; index < routerElements.length; index++) { - const routerElement = routerElements[index]; - const result = routerElement.pattern.exec(decodedUrl.path); - if (!result) { - continue; - } - return routerElement.buildedRoute(new Request({ - ...initializationData, - ...decodedUrl, - params: result.groups ?? {}, - matchedPath: routerElement.matchedPath, - bodyReader: routerElement.bodyReader, - })); - } - return buildedNotfoundRoute(new Request({ - ...initializationData, - ...decodedUrl, - params: {}, - matchedPath: null, - bodyReader: bodyReaderNotFoundRoute, - })); - }, + exec: await build$1.buildRouterFunction({ + environment: hub.config.environment, + routerElementWrapper, + notfoundRouterElement: notfoundRouterElement, + malformedUrlRouterElement: malformedUrlRouterElement, + classRequest: hub.classRequest, + routerFunctionBuilder: hub.routerFunctionBuilder ?? index$1.defaultRouterFunctionBuilder, + }), hooksRouteLifeCycle, routeFunctionBuilders, routes, @@ -127,8 +89,6 @@ async function buildRouter(hub) { exports.pathToRegExp = pathToRegExp.pathToRegExp; exports.RouterBuildError = buildError.RouterBuildError; -exports.decodeUrl = decodeUrl.decodeUrl; -exports.regexQueryAnalyser = decodeUrl.regexQueryAnalyser; -exports.regexUrlAnalyser = decodeUrl.regexUrlAnalyser; exports.NotFoundBodyReaderImplementationError = notFoundBodyReaderImplementationError.NotFoundBodyReaderImplementationError; -exports.buildRouter = buildRouter; +exports.createRouterElementSystem = createRouterElementSystem.createRouterElementSystem; +exports.createRouter = createRouter; diff --git a/docs/libs/v0/core/router/index.d.ts b/docs/libs/v0/core/router/index.d.ts index 3fae40c..6291857 100644 --- a/docs/libs/v0/core/router/index.d.ts +++ b/docs/libs/v0/core/router/index.d.ts @@ -1,8 +1,8 @@ import { type Hub } from "../hub"; -import { type BuildedRouter } from "./types"; +import { type Router } from "./types"; export * from "./types"; export * from "./pathToRegExp"; export * from "./buildError"; -export * from "./decodeUrl"; export * from "./notFoundBodyReaderImplementationError"; -export declare function buildRouter(hub: Hub): Promise; +export * from "./createRouterElementSystem"; +export declare function createRouter(hub: Hub): Promise; diff --git a/docs/libs/v0/core/router/index.mjs b/docs/libs/v0/core/router/index.mjs index 89a71d6..df3bc07 100644 --- a/docs/libs/v0/core/router/index.mjs +++ b/docs/libs/v0/core/router/index.mjs @@ -1,16 +1,12 @@ import '../hub/index.mjs'; -import { pipe, A, isType, G, E, unwrap, justReturn, O, forward, asserts, asyncPipe } from '@duplojs/utils'; +import { pipe, A, isType, G, E, unwrap, justReturn, O, forward } 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 { 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 { createRouterElementSystem } from './createRouterElementSystem.mjs'; import './types/index.mjs'; -import { defaultRouteFunctionBuilder } from '../functionsBuilders/route/default.mjs'; +import { defaultRouteFunctionBuilder } from '../functionsBuilders/route/default/index.mjs'; import { defaultCheckerStepFunctionBuilder } from '../functionsBuilders/steps/defaults/checkerStep.mjs'; import { defaultCutStepFunctionBuilder } from '../functionsBuilders/steps/defaults/cutStep.mjs'; import { defaultHandlerStepFunctionBuilder } from '../functionsBuilders/steps/defaults/handlerStep.mjs'; @@ -18,8 +14,10 @@ import { defaultExtractStepFunctionBuilder } from '../functionsBuilders/steps/de import { defaultProcessStepFunctionBuilder } from '../functionsBuilders/steps/defaults/processStep.mjs'; import { launchHookBeforeBuildRoute } from '../hub/hooks.mjs'; import { buildRouteFunction } from '../functionsBuilders/route/build.mjs'; +import { buildRouterFunction } from '../functionsBuilders/router/build.mjs'; +import { defaultRouterFunctionBuilder } from '../functionsBuilders/router/default/index.mjs'; -async function buildRouter(hub) { +async function createRouter(hub) { const { environment } = hub.config; const { hooksRouteLifeCycle, routes, hooksHubLifeCycle, bodyReaderImplementations, } = hub; const routeFunctionBuilders = [ @@ -42,7 +40,7 @@ async function buildRouter(hub) { routeFunctionBuilders, defaultExtractContract: hub.defaultExtractContract, }; - const groupedRoute = await G.asyncReduce(routes, G.reduceFrom({}), async ({ lastValue, element: route, nextWithObject, }) => { + const routerElementWrapper = await G.asyncReduce(routes, G.reduceFrom({}), async ({ lastValue, element: route, nextWithObject, }) => { const routeAfterHook = await launchHookBeforeBuildRoute(hooksBeforeBuildRoute, route); const buildedRoute = await buildRouteFunction(routeAfterHook, buildParams); if (E.isLeft(buildedRoute)) { @@ -62,60 +60,23 @@ async function buildRouter(hub) { }))), }); }); - 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: ["/"], - hooks: [], - preflightSteps: [], - steps: [hub.notfoundHandler], - metadata: [], - bodyController: bodyControllerNotfoundRoute, - }), async (route) => { - const result = await buildRouteFunction(route, buildParams); - return E.whenIsLeft(result, (element) => { - throw new RouterBuildError(route, element); - }); - }, unwrap); - const Request = hub.classRequest; + const notfoundRouterElement = await createRouterElementSystem({ + handlerStep: hub.notfoundHandler, + buildParams, + }); + const malformedUrlRouterElement = await createRouterElementSystem({ + handlerStep: hub.malformedUrlHandler, + buildParams, + }); return { - exec: (initializationData) => { - const routerElements = groupedRoute[initializationData.method]; - const decodedUrl = decodeUrl(initializationData.url); - if (!routerElements) { - return buildedNotfoundRoute(new Request({ - ...initializationData, - ...decodedUrl, - params: {}, - matchedPath: null, - bodyReader: bodyReaderNotFoundRoute, - })); - } - // eslint-disable-next-line @typescript-eslint/prefer-for-of - for (let index = 0; index < routerElements.length; index++) { - const routerElement = routerElements[index]; - const result = routerElement.pattern.exec(decodedUrl.path); - if (!result) { - continue; - } - return routerElement.buildedRoute(new Request({ - ...initializationData, - ...decodedUrl, - params: result.groups ?? {}, - matchedPath: routerElement.matchedPath, - bodyReader: routerElement.bodyReader, - })); - } - return buildedNotfoundRoute(new Request({ - ...initializationData, - ...decodedUrl, - params: {}, - matchedPath: null, - bodyReader: bodyReaderNotFoundRoute, - })); - }, + exec: await buildRouterFunction({ + environment: hub.config.environment, + routerElementWrapper, + notfoundRouterElement: notfoundRouterElement, + malformedUrlRouterElement: malformedUrlRouterElement, + classRequest: hub.classRequest, + routerFunctionBuilder: hub.routerFunctionBuilder ?? defaultRouterFunctionBuilder, + }), hooksRouteLifeCycle, routeFunctionBuilders, routes, @@ -124,4 +85,4 @@ async function buildRouter(hub) { }; } -export { NotFoundBodyReaderImplementationError, RouterBuildError, buildRouter, decodeUrl, pathToRegExp }; +export { NotFoundBodyReaderImplementationError, RouterBuildError, createRouter, createRouterElementSystem, pathToRegExp }; diff --git a/docs/libs/v0/core/router/types/buildedRouter.d.ts b/docs/libs/v0/core/router/types/buildedRouter.d.ts index ba0162e..c526674 100644 --- a/docs/libs/v0/core/router/types/buildedRouter.d.ts +++ b/docs/libs/v0/core/router/types/buildedRouter.d.ts @@ -1,13 +1,3 @@ -import { type createStepFunctionBuilder, type createRouteFunctionBuilder } from "../../functionsBuilders"; -import { type HookHubLifeCycle } from "../../hub"; import { type RequestInitializationData } from "../../request"; -import { type HookRouteLifeCycle, type Route } from "../../route"; -export type RouterInitializationData = Omit; -export interface BuildedRouter { - exec(initializationData: RouterInitializationData): Promise; - readonly routes: ReadonlySet; - readonly hooksRouteLifeCycle: readonly HookRouteLifeCycle[]; - readonly routeFunctionBuilders: readonly ReturnType[]; - readonly stepFunctionBuilders: readonly ReturnType[]; - readonly hooksHubLifeCycle: readonly HookHubLifeCycle[]; -} +export type RouterParams = Omit; +export type BuildedRouter = (params: RouterParams) => Promise; diff --git a/docs/libs/v0/core/router/types/index.cjs b/docs/libs/v0/core/router/types/index.cjs index e8635fd..059b99c 100644 --- a/docs/libs/v0/core/router/types/index.cjs +++ b/docs/libs/v0/core/router/types/index.cjs @@ -1,4 +1,8 @@ 'use strict'; require('./buildedRouter.cjs'); +require('./router.cjs'); +require('./routerElement.cjs'); +require('./routerElementSystem.cjs'); +require('./routerElementWrapper.cjs'); diff --git a/docs/libs/v0/core/router/types/index.d.ts b/docs/libs/v0/core/router/types/index.d.ts index c2d0e97..9c3567f 100644 --- a/docs/libs/v0/core/router/types/index.d.ts +++ b/docs/libs/v0/core/router/types/index.d.ts @@ -1 +1,5 @@ export * from "./buildedRouter"; +export * from "./router"; +export * from "./routerElement"; +export * from "./routerElementSystem"; +export * from "./routerElementWrapper"; diff --git a/docs/libs/v0/core/router/types/index.mjs b/docs/libs/v0/core/router/types/index.mjs index b5d96d2..83ce180 100644 --- a/docs/libs/v0/core/router/types/index.mjs +++ b/docs/libs/v0/core/router/types/index.mjs @@ -1 +1,5 @@ import './buildedRouter.mjs'; +import './router.mjs'; +import './routerElement.mjs'; +import './routerElementSystem.mjs'; +import './routerElementWrapper.mjs'; diff --git a/docs/libs/v0/core/router/types/router.cjs b/docs/libs/v0/core/router/types/router.cjs new file mode 100644 index 0000000..eb109ab --- /dev/null +++ b/docs/libs/v0/core/router/types/router.cjs @@ -0,0 +1,2 @@ +'use strict'; + diff --git a/docs/libs/v0/core/router/types/router.d.ts b/docs/libs/v0/core/router/types/router.d.ts new file mode 100644 index 0000000..54e8f65 --- /dev/null +++ b/docs/libs/v0/core/router/types/router.d.ts @@ -0,0 +1,12 @@ +import { type createRouteFunctionBuilder, type createStepFunctionBuilder } from "../../functionsBuilders"; +import { type HookHubLifeCycle } from "../../hub"; +import { type HookRouteLifeCycle, type Route } from "../../route"; +import { type BuildedRouter } from "./buildedRouter"; +export interface Router { + exec: BuildedRouter; + readonly routes: ReadonlySet; + readonly hooksRouteLifeCycle: readonly HookRouteLifeCycle[]; + readonly routeFunctionBuilders: readonly ReturnType[]; + readonly stepFunctionBuilders: readonly ReturnType[]; + readonly hooksHubLifeCycle: readonly HookHubLifeCycle[]; +} diff --git a/docs/libs/v0/core/router/types/router.mjs b/docs/libs/v0/core/router/types/router.mjs new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/docs/libs/v0/core/router/types/router.mjs @@ -0,0 +1 @@ + diff --git a/docs/libs/v0/core/router/types/routerElement.cjs b/docs/libs/v0/core/router/types/routerElement.cjs new file mode 100644 index 0000000..eb109ab --- /dev/null +++ b/docs/libs/v0/core/router/types/routerElement.cjs @@ -0,0 +1,2 @@ +'use strict'; + diff --git a/docs/libs/v0/core/router/types/routerElement.d.ts b/docs/libs/v0/core/router/types/routerElement.d.ts new file mode 100644 index 0000000..2c3406c --- /dev/null +++ b/docs/libs/v0/core/router/types/routerElement.d.ts @@ -0,0 +1,8 @@ +import { type BodyReader } from "../../request"; +import { type BuildedRoute } from "../../route/types"; +export interface RouterElement { + readonly pattern: RegExp; + readonly matchedPath: string; + readonly bodyReader: BodyReader; + readonly buildedRoute: BuildedRoute; +} diff --git a/docs/libs/v0/core/router/types/routerElement.mjs b/docs/libs/v0/core/router/types/routerElement.mjs new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/docs/libs/v0/core/router/types/routerElement.mjs @@ -0,0 +1 @@ + diff --git a/docs/libs/v0/core/router/types/routerElementSystem.cjs b/docs/libs/v0/core/router/types/routerElementSystem.cjs new file mode 100644 index 0000000..eb109ab --- /dev/null +++ b/docs/libs/v0/core/router/types/routerElementSystem.cjs @@ -0,0 +1,2 @@ +'use strict'; + diff --git a/docs/libs/v0/core/router/types/routerElementSystem.d.ts b/docs/libs/v0/core/router/types/routerElementSystem.d.ts new file mode 100644 index 0000000..20ee982 --- /dev/null +++ b/docs/libs/v0/core/router/types/routerElementSystem.d.ts @@ -0,0 +1,6 @@ +import { type BodyReader } from "../../request"; +import { type BuildedRoute } from "../../route/types"; +export interface RouterElementSystem { + readonly bodyReader: BodyReader; + readonly buildedRoute: BuildedRoute; +} diff --git a/docs/libs/v0/core/router/types/routerElementSystem.mjs b/docs/libs/v0/core/router/types/routerElementSystem.mjs new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/docs/libs/v0/core/router/types/routerElementSystem.mjs @@ -0,0 +1 @@ + diff --git a/docs/libs/v0/core/router/types/routerElementWrapper.cjs b/docs/libs/v0/core/router/types/routerElementWrapper.cjs new file mode 100644 index 0000000..eb109ab --- /dev/null +++ b/docs/libs/v0/core/router/types/routerElementWrapper.cjs @@ -0,0 +1,2 @@ +'use strict'; + diff --git a/docs/libs/v0/core/router/types/routerElementWrapper.d.ts b/docs/libs/v0/core/router/types/routerElementWrapper.d.ts new file mode 100644 index 0000000..275be09 --- /dev/null +++ b/docs/libs/v0/core/router/types/routerElementWrapper.d.ts @@ -0,0 +1,2 @@ +import { type RouterElement } from "./routerElement"; +export type RouterElementWrapper = Record; diff --git a/docs/libs/v0/core/router/types/routerElementWrapper.mjs b/docs/libs/v0/core/router/types/routerElementWrapper.mjs new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/docs/libs/v0/core/router/types/routerElementWrapper.mjs @@ -0,0 +1 @@ + diff --git a/docs/libs/v0/core/steps/cut.d.ts b/docs/libs/v0/core/steps/cut.d.ts index 717e92e..71016e7 100644 --- a/docs/libs/v0/core/steps/cut.d.ts +++ b/docs/libs/v0/core/steps/cut.d.ts @@ -3,12 +3,11 @@ import { type StepKind } from "./kind"; import { type Floor } from "../floor"; import { type StepFunctionParams } from "./types"; import { type PredictedResponse, type ResponseContract } from "../response"; -import { type Request } from "../request"; import { type Metadata } from "../metadata"; export declare const cutStepOutputKind: import("@duplojs/utils").KindHandler>; export interface CutStepFunctionOutput = Record> extends Kind, WrappedValue { } -export interface CutStepFunctionParams extends StepFunctionParams { +export interface CutStepFunctionParams extends StepFunctionParams { output = never>(data?: GenericData): CutStepFunctionOutput>; } export interface CutStepDefinition { diff --git a/docs/libs/v0/core/steps/handler.d.ts b/docs/libs/v0/core/steps/handler.d.ts index a653088..0beda8c 100644 --- a/docs/libs/v0/core/steps/handler.d.ts +++ b/docs/libs/v0/core/steps/handler.d.ts @@ -2,7 +2,6 @@ import { type MaybePromise, type Kind, type MaybeArray } from "@duplojs/utils"; import { type StepKind } from "./kind"; import { type Floor } from "../floor"; import { type ServerSentEventsPredictedResponse, type PredictedResponse, type ResponseContract } from "../response"; -import { type Request } from "../request"; import { type StepFunctionParams } from "./types"; import { type Metadata } from "../metadata"; interface HandlerStepFunctionParamsServerSentEventsResponse { @@ -10,7 +9,7 @@ interface HandlerStepFunctionParamsServerSentEventsResponse>(information: GenericInformation, startSendingEvents: GenericFilteredResponse["startSendingEvents"]): GenericFilteredResponse; } -export interface HandlerStepFunctionParams extends StepFunctionParams>, HandlerStepFunctionParamsServerSentEventsResponse> { +export interface HandlerStepFunctionParams extends StepFunctionParams>, HandlerStepFunctionParamsServerSentEventsResponse> { } export interface HandlerStepDefinition { theFunction(floor: Floor, params: HandlerStepFunctionParams): MaybePromise; diff --git a/docs/libs/v0/core/steps/types/stepFunctionParams.d.ts b/docs/libs/v0/core/steps/types/stepFunctionParams.d.ts index be24543..d1f8b91 100644 --- a/docs/libs/v0/core/steps/types/stepFunctionParams.d.ts +++ b/docs/libs/v0/core/steps/types/stepFunctionParams.d.ts @@ -1,8 +1,8 @@ import { type Request } from "../../request"; import { type PredictedResponse } from "../../response"; import { type IsEqual, type Or } from "@duplojs/utils"; -export interface StepFunctionParams { - request: GenericRequest; +export interface StepFunctionParams { + request: Request; response>(information: GenericInformation, ...args: Or<[ diff --git a/docs/libs/v0/interfaces/node/bodyReaders/formData/index.cjs b/docs/libs/v0/interfaces/node/bodyReaders/formData/index.cjs index 810227d..8f43703 100644 --- a/docs/libs/v0/interfaces/node/bodyReaders/formData/index.cjs +++ b/docs/libs/v0/interfaces/node/bodyReaders/formData/index.cjs @@ -4,14 +4,39 @@ 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'); +var node_crypto = require('node:crypto'); require('../../../../core/errors/index.cjs'); +var promises = require('node:fs/promises'); 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); + async function createUploadFile(extension, highWaterMark) { + let remainingAttempts = 5; + do { + const path = utils.Path.resolveRelative([ + serverParams.uploadFolder, + `${node_crypto.randomUUID()}${extension}`, + ]); + try { + const handle = await promises.open(path, "wx"); + return { + path, + writeStream: handle.createWriteStream({ + highWaterMark, + autoClose: true, + }), + }; + } + catch (error) { + if (error.code !== "EEXIST" || --remainingAttempts === 0) { + throw error; + } + } + } while (true); + } function addValue(mapResult, fieldName, newValue) { const value = mapResult.get(fieldName); if (value === undefined) { @@ -30,37 +55,31 @@ function createFormDataBodyReaderImplementation(serverParams) { const result = await readRequestFormData.readRequestFormData(request.raw.request, new Map(), { maxBodySize: params.bodyMaxSize ?? serverMaxBodySize, fileMaxSize: params.fileMaxSize ?? Infinity, + textFieldMaxSize: params.textFieldMaxSize ?? Infinity, maxFileQuantity: params.maxFileQuantity, mimeType: params.mimeType, maxBufferSize: params.maxBufferSize, maxKeyLength: params.maxKeyLength, - }, (header) => { + }, async (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, - flags: "wx", - }); + const { path, writeStream } = await createUploadFile(displayExtension, request.raw.request.readableHighWaterMark); + filesAttache.push(path); return { - onReceiveChunk: (chunk) => new Promise((resolve, reject) => void currentFile.write(chunk, (result) => { + onReceiveChunk: (chunk) => new Promise((resolve, reject) => void writeStream.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())); + writeStream.end(); + addValue(valueAccumulator, fieldName, serverUtils.SF.createFileInterface(path)); return valueAccumulator; }, - onError: () => void currentFile.end(), + onError: () => void writeStream.end(), }; } let currentValue = ""; diff --git a/docs/libs/v0/interfaces/node/bodyReaders/formData/index.mjs b/docs/libs/v0/interfaces/node/bodyReaders/formData/index.mjs index 95ca4bb..8831cb6 100644 --- a/docs/libs/v0/interfaces/node/bodyReaders/formData/index.mjs +++ b/docs/libs/v0/interfaces/node/bodyReaders/formData/index.mjs @@ -1,15 +1,40 @@ import '../../../../core/request/index.mjs'; import { SF } from '@duplojs/server-utils'; -import { stringToBytes, A, E, Path, unwrap, TheFormData, O } from '@duplojs/utils'; +import { stringToBytes, Path, A, E, unwrap, TheFormData, O } from '@duplojs/utils'; import { readRequestFormData } from './readRequestFormData.mjs'; -import { createWriteStream } from 'node:fs'; +import { randomUUID } from 'node:crypto'; import '../../../../core/errors/index.mjs'; +import { open } from 'node:fs/promises'; 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); + async function createUploadFile(extension, highWaterMark) { + let remainingAttempts = 5; + do { + const path = Path.resolveRelative([ + serverParams.uploadFolder, + `${randomUUID()}${extension}`, + ]); + try { + const handle = await open(path, "wx"); + return { + path, + writeStream: handle.createWriteStream({ + highWaterMark, + autoClose: true, + }), + }; + } + catch (error) { + if (error.code !== "EEXIST" || --remainingAttempts === 0) { + throw error; + } + } + } while (true); + } function addValue(mapResult, fieldName, newValue) { const value = mapResult.get(fieldName); if (value === undefined) { @@ -28,37 +53,31 @@ function createFormDataBodyReaderImplementation(serverParams) { const result = await readRequestFormData(request.raw.request, new Map(), { maxBodySize: params.bodyMaxSize ?? serverMaxBodySize, fileMaxSize: params.fileMaxSize ?? Infinity, + textFieldMaxSize: params.textFieldMaxSize ?? Infinity, maxFileQuantity: params.maxFileQuantity, mimeType: params.mimeType, maxBufferSize: params.maxBufferSize, maxKeyLength: params.maxKeyLength, - }, (header) => { + }, async (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, - flags: "wx", - }); + const { path, writeStream } = await createUploadFile(displayExtension, request.raw.request.readableHighWaterMark); + filesAttache.push(path); return { - onReceiveChunk: (chunk) => new Promise((resolve, reject) => void currentFile.write(chunk, (result) => { + onReceiveChunk: (chunk) => new Promise((resolve, reject) => void writeStream.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())); + writeStream.end(); + addValue(valueAccumulator, fieldName, SF.createFileInterface(path)); return valueAccumulator; }, - onError: () => void currentFile.end(), + onError: () => void writeStream.end(), }; } let currentValue = ""; diff --git a/docs/libs/v0/interfaces/node/bodyReaders/formData/readRequestFormData.cjs b/docs/libs/v0/interfaces/node/bodyReaders/formData/readRequestFormData.cjs index c869173..d45c2b1 100644 --- a/docs/libs/v0/interfaces/node/bodyReaders/formData/readRequestFormData.cjs +++ b/docs/libs/v0/interfaces/node/bodyReaders/formData/readRequestFormData.cjs @@ -32,6 +32,7 @@ async function readRequestFormData(request, firstValueAccumulator, params, onRec let currentStream = undefined; let fileQuantity = 0; let currentFileSize = undefined; + let currentTextFieldSize = undefined; const checkSize = (receivedChunk) => { size += receivedChunk.length; return size > params.maxBodySize @@ -61,6 +62,7 @@ async function readRequestFormData(request, firstValueAccumulator, params, onRec } if (header.filename !== undefined) { currentFileSize = 0; + currentTextFieldSize = undefined; fileQuantity++; if (fileQuantity > params.maxFileQuantity) { return new error.BodyParseFormDataError("File quantity exceeds limit."); @@ -72,6 +74,7 @@ async function readRequestFormData(request, firstValueAccumulator, params, onRec } else { currentFileSize = undefined; + currentTextFieldSize = 0; } const newStream = await onReceiveHeader(header); if (newStream instanceof Error) { @@ -93,10 +96,16 @@ async function readRequestFormData(request, firstValueAccumulator, params, onRec } if (typeof currentFileSize === "number") { currentFileSize += chunk.length; - if (params.fileMaxSize !== undefined && currentFileSize > params.fileMaxSize) { + if (currentFileSize > params.fileMaxSize) { return new error.BodyParseFormDataError("File size exceeds limit."); } } + if (typeof currentTextFieldSize === "number") { + currentTextFieldSize += chunk.length; + if (currentTextFieldSize > params.textFieldMaxSize) { + return new error.BodyParseFormDataError("Text field size exceeds limit."); + } + } await currentStream.onReceiveChunk(chunk); return true; }; diff --git a/docs/libs/v0/interfaces/node/bodyReaders/formData/readRequestFormData.d.ts b/docs/libs/v0/interfaces/node/bodyReaders/formData/readRequestFormData.d.ts index 468c82b..51a3b5b 100644 --- a/docs/libs/v0/interfaces/node/bodyReaders/formData/readRequestFormData.d.ts +++ b/docs/libs/v0/interfaces/node/bodyReaders/formData/readRequestFormData.d.ts @@ -14,7 +14,8 @@ export interface ReadRequestFormDataParams { maxFileQuantity: number; maxBufferSize: number; maxKeyLength: number; - fileMaxSize?: number; + fileMaxSize: number; + textFieldMaxSize: number; mimeType?: RegExp; } export declare function readRequestFormData(request: http.IncomingMessage, firstValueAccumulator: GenericValueAccumulator, params: ReadRequestFormDataParams, onReceiveHeader: (header: HeaderPartInformation) => MaybePromise | Error>): Promise | GenericOutputHeader | E.Error | GenericValueAccumulator>; diff --git a/docs/libs/v0/interfaces/node/bodyReaders/formData/readRequestFormData.mjs b/docs/libs/v0/interfaces/node/bodyReaders/formData/readRequestFormData.mjs index c65197e..305438f 100644 --- a/docs/libs/v0/interfaces/node/bodyReaders/formData/readRequestFormData.mjs +++ b/docs/libs/v0/interfaces/node/bodyReaders/formData/readRequestFormData.mjs @@ -30,6 +30,7 @@ async function readRequestFormData(request, firstValueAccumulator, params, onRec let currentStream = undefined; let fileQuantity = 0; let currentFileSize = undefined; + let currentTextFieldSize = undefined; const checkSize = (receivedChunk) => { size += receivedChunk.length; return size > params.maxBodySize @@ -59,6 +60,7 @@ async function readRequestFormData(request, firstValueAccumulator, params, onRec } if (header.filename !== undefined) { currentFileSize = 0; + currentTextFieldSize = undefined; fileQuantity++; if (fileQuantity > params.maxFileQuantity) { return new BodyParseFormDataError("File quantity exceeds limit."); @@ -70,6 +72,7 @@ async function readRequestFormData(request, firstValueAccumulator, params, onRec } else { currentFileSize = undefined; + currentTextFieldSize = 0; } const newStream = await onReceiveHeader(header); if (newStream instanceof Error) { @@ -91,10 +94,16 @@ async function readRequestFormData(request, firstValueAccumulator, params, onRec } if (typeof currentFileSize === "number") { currentFileSize += chunk.length; - if (params.fileMaxSize !== undefined && currentFileSize > params.fileMaxSize) { + if (currentFileSize > params.fileMaxSize) { return new BodyParseFormDataError("File size exceeds limit."); } } + if (typeof currentTextFieldSize === "number") { + currentTextFieldSize += chunk.length; + if (currentTextFieldSize > params.textFieldMaxSize) { + return new BodyParseFormDataError("Text field size exceeds limit."); + } + } await currentStream.onReceiveChunk(chunk); return true; }; diff --git a/docs/libs/v0/interfaces/node/createHttpServer.cjs b/docs/libs/v0/interfaces/node/createHttpServer.cjs index 2bec995..b4db796 100644 --- a/docs/libs/v0/interfaces/node/createHttpServer.cjs +++ b/docs/libs/v0/interfaces/node/createHttpServer.cjs @@ -2,10 +2,9 @@ var http = require('http'); var https = require('https'); -var index$3 = require('./hooks/index.cjs'); +var index$2 = 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'); @@ -25,12 +24,8 @@ function createHttpServer(hub, params) { index.createTextBodyReaderImplementation(httpServerParams), index$1.createFormDataBodyReaderImplementation(httpServerParams), ]); - hub.addRouteHooks([ - index$2.initDefaultHook(hub, httpServerParams), - index$3.initNodeHook(hub, httpServerParams), - ]); - function whenUncaughtError(error, routerInitializationData) { - const serverResponse = routerInitializationData.raw.response; + function whenUncaughtError(error, routerParams) { + const serverResponse = routerParams.raw.response; if (!serverResponse.headersSent && !serverResponse.writableEnded) { serverResponse.writeHead(500, { [httpServerParams.informationHeaderKey]: "critical-server-error", @@ -47,6 +42,7 @@ function createHttpServer(hub, params) { return implementHttpServer.implementHttpServer({ hub, httpServerParams, + getInterfaceHooks: ({ hub, httpServerParams }) => [index$2.initNodeHook(hub, httpServerParams)], }, ({ httpServerParams, execRouteSystem }) => { const server = httpServerParams.https ? https.createServer(httpServerParams.https) diff --git a/docs/libs/v0/interfaces/node/createHttpServer.mjs b/docs/libs/v0/interfaces/node/createHttpServer.mjs index a5fec1c..915157a 100644 --- a/docs/libs/v0/interfaces/node/createHttpServer.mjs +++ b/docs/libs/v0/interfaces/node/createHttpServer.mjs @@ -3,7 +3,6 @@ import https from 'https'; import { initNodeHook } 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'; @@ -23,12 +22,8 @@ function createHttpServer(hub, params) { createTextBodyReaderImplementation(httpServerParams), createFormDataBodyReaderImplementation(httpServerParams), ]); - hub.addRouteHooks([ - initDefaultHook(hub, httpServerParams), - initNodeHook(hub, httpServerParams), - ]); - function whenUncaughtError(error, routerInitializationData) { - const serverResponse = routerInitializationData.raw.response; + function whenUncaughtError(error, routerParams) { + const serverResponse = routerParams.raw.response; if (!serverResponse.headersSent && !serverResponse.writableEnded) { serverResponse.writeHead(500, { [httpServerParams.informationHeaderKey]: "critical-server-error", @@ -45,6 +40,7 @@ function createHttpServer(hub, params) { return implementHttpServer({ hub, httpServerParams, + getInterfaceHooks: ({ hub, httpServerParams }) => [initNodeHook(hub, httpServerParams)], }, ({ httpServerParams, execRouteSystem }) => { const server = httpServerParams.https ? https.createServer(httpServerParams.https) diff --git a/docs/libs/v0/interfaces/node/hooks/index.d.ts b/docs/libs/v0/interfaces/node/hooks/index.d.ts index 7cb5fea..cb4482a 100644 --- a/docs/libs/v0/interfaces/node/hooks/index.d.ts +++ b/docs/libs/v0/interfaces/node/hooks/index.d.ts @@ -1,7 +1,7 @@ import { type Hub } from "../../../core/hub"; import { type HttpServerParams } from "../../../core/types"; export declare function initNodeHook(hub: Hub, serverParams: HttpServerParams): { - readonly beforeSendResponse: ({ request, currentResponse, exit }: import("../../../core/route").RouteHookParamsAfter) => import("../../../core/route").RouteHookExit; - readonly sendResponse: ({ request, currentResponse, exit }: import("../../../core/route").RouteHookParamsAfter) => Promise; - readonly afterSendResponse: ({ request, next }: import("../../../core/route").RouteHookParamsAfter) => Promise; + readonly beforeSendResponse: ({ request, currentResponse, exit }: import("../../../core/route").RouteHookParamsAfter) => import("../../../core/route").RouteHookExit; + readonly sendResponse: ({ request, currentResponse, exit }: import("../../../core/route").RouteHookParamsAfter) => Promise; + readonly afterSendResponse: ({ request, next }: import("../../../core/route").RouteHookParamsAfter) => Promise; }; diff --git a/docs/libs/v0/plugins/cacheController/createResponseHeader.cjs b/docs/libs/v0/plugins/cacheController/createResponseHeader.cjs new file mode 100644 index 0000000..413f638 --- /dev/null +++ b/docs/libs/v0/plugins/cacheController/createResponseHeader.cjs @@ -0,0 +1,43 @@ +'use strict'; + +var utils = require('@duplojs/utils'); + +function createCacheControlResponseHeader(directives) { + return utils.pipe([ + utils.O.entry("max-age", directives.maxAge), + utils.O.entry("s-maxage", directives.sMaxAge), + utils.O.entry("public", directives.public), + utils.O.entry("private", directives.private), + utils.O.entry("no-cache", directives.noCache), + utils.O.entry("no-store", directives.noStore), + utils.O.entry("no-transform", directives.noTransform), + utils.O.entry("must-revalidate", directives.mustRevalidate), + utils.O.entry("proxy-revalidate", directives.proxyRevalidate), + utils.O.entry("immutable", directives.immutable), + utils.O.entry("stale-while-revalidate", directives.staleWhileRevalidate), + utils.O.entry("stale-if-error", directives.staleIfError), + utils.O.entry("must-understand", directives.mustUnderstand), + ], utils.A.concat(directives.extensions ? utils.O.entries(directives.extensions) : []), utils.A.reduce(utils.A.reduceFrom([]), ({ element: [key, value], lastValue, nextPush, next }) => { + if (value === true) { + return nextPush(lastValue, key); + } + else if (typeof value === "number" + && Number.isFinite(value) + && value >= 0) { + return nextPush(lastValue, `${key}=${Math.trunc(value)}`); + } + else if (value instanceof Array + && utils.A.minElements(value, 1)) { + return nextPush(lastValue, `${key}="${utils.A.join(value, ",")}"`); + } + else if (value !== "" + && typeof value === "string") { + return nextPush(lastValue, `${key}="${value}"`); + } + else { + return next(lastValue); + } + }), utils.A.join(",")); +} + +exports.createCacheControlResponseHeader = createCacheControlResponseHeader; diff --git a/docs/libs/v0/plugins/cacheController/createResponseHeader.d.ts b/docs/libs/v0/plugins/cacheController/createResponseHeader.d.ts new file mode 100644 index 0000000..7c252dd --- /dev/null +++ b/docs/libs/v0/plugins/cacheController/createResponseHeader.d.ts @@ -0,0 +1,2 @@ +import type { CacheControlDirectives } from "./types"; +export declare function createCacheControlResponseHeader(directives: CacheControlDirectives): string; diff --git a/docs/libs/v0/plugins/cacheController/createResponseHeader.mjs b/docs/libs/v0/plugins/cacheController/createResponseHeader.mjs new file mode 100644 index 0000000..3aa9d1c --- /dev/null +++ b/docs/libs/v0/plugins/cacheController/createResponseHeader.mjs @@ -0,0 +1,41 @@ +import { pipe, O, A } from '@duplojs/utils'; + +function createCacheControlResponseHeader(directives) { + return pipe([ + O.entry("max-age", directives.maxAge), + O.entry("s-maxage", directives.sMaxAge), + O.entry("public", directives.public), + O.entry("private", directives.private), + O.entry("no-cache", directives.noCache), + O.entry("no-store", directives.noStore), + O.entry("no-transform", directives.noTransform), + O.entry("must-revalidate", directives.mustRevalidate), + O.entry("proxy-revalidate", directives.proxyRevalidate), + O.entry("immutable", directives.immutable), + O.entry("stale-while-revalidate", directives.staleWhileRevalidate), + O.entry("stale-if-error", directives.staleIfError), + O.entry("must-understand", directives.mustUnderstand), + ], A.concat(directives.extensions ? O.entries(directives.extensions) : []), A.reduce(A.reduceFrom([]), ({ element: [key, value], lastValue, nextPush, next }) => { + if (value === true) { + return nextPush(lastValue, key); + } + else if (typeof value === "number" + && Number.isFinite(value) + && value >= 0) { + return nextPush(lastValue, `${key}=${Math.trunc(value)}`); + } + else if (value instanceof Array + && A.minElements(value, 1)) { + return nextPush(lastValue, `${key}="${A.join(value, ",")}"`); + } + else if (value !== "" + && typeof value === "string") { + return nextPush(lastValue, `${key}="${value}"`); + } + else { + return next(lastValue); + } + }), A.join(",")); +} + +export { createCacheControlResponseHeader }; diff --git a/docs/libs/v0/plugins/cacheController/hooks.cjs b/docs/libs/v0/plugins/cacheController/hooks.cjs new file mode 100644 index 0000000..adeca96 --- /dev/null +++ b/docs/libs/v0/plugins/cacheController/hooks.cjs @@ -0,0 +1,24 @@ +'use strict'; + +var createResponseHeader = require('./createResponseHeader.cjs'); +require('../../core/route/index.cjs'); +var hooks = require('../../core/route/hooks.cjs'); + +const eligibleCodeRegex = /^(?:2|3)/; +function createCacheControllerHooks(params) { + const cacheControl = params + ? createResponseHeader.createCacheControlResponseHeader(params) + : null; + return hooks.createHookRouteLifeCycle({ + beforeSendResponse: ({ currentResponse, next }) => { + if (cacheControl + && eligibleCodeRegex.test(currentResponse.code) + && currentResponse.headers?.["cache-control"] === undefined) { + currentResponse.setHeader("cache-control", cacheControl); + } + return next(); + }, + }); +} + +exports.createCacheControllerHooks = createCacheControllerHooks; diff --git a/docs/libs/v0/plugins/cacheController/hooks.d.ts b/docs/libs/v0/plugins/cacheController/hooks.d.ts new file mode 100644 index 0000000..821fd5d --- /dev/null +++ b/docs/libs/v0/plugins/cacheController/hooks.d.ts @@ -0,0 +1,4 @@ +import type { CacheControlDirectives } from "./types"; +export declare function createCacheControllerHooks(params?: CacheControlDirectives): { + readonly beforeSendResponse: ({ currentResponse, next }: import("../../core/route").RouteHookParamsAfter) => import("../../core/route").RouteHookNext; +}; diff --git a/docs/libs/v0/plugins/cacheController/hooks.mjs b/docs/libs/v0/plugins/cacheController/hooks.mjs new file mode 100644 index 0000000..84da92b --- /dev/null +++ b/docs/libs/v0/plugins/cacheController/hooks.mjs @@ -0,0 +1,22 @@ +import { createCacheControlResponseHeader } from './createResponseHeader.mjs'; +import '../../core/route/index.mjs'; +import { createHookRouteLifeCycle } from '../../core/route/hooks.mjs'; + +const eligibleCodeRegex = /^(?:2|3)/; +function createCacheControllerHooks(params) { + const cacheControl = params + ? createCacheControlResponseHeader(params) + : null; + return createHookRouteLifeCycle({ + beforeSendResponse: ({ currentResponse, next }) => { + if (cacheControl + && eligibleCodeRegex.test(currentResponse.code) + && currentResponse.headers?.["cache-control"] === undefined) { + currentResponse.setHeader("cache-control", cacheControl); + } + return next(); + }, + }); +} + +export { createCacheControllerHooks }; diff --git a/docs/libs/v0/plugins/cacheController/index.cjs b/docs/libs/v0/plugins/cacheController/index.cjs new file mode 100644 index 0000000..bec091e --- /dev/null +++ b/docs/libs/v0/plugins/cacheController/index.cjs @@ -0,0 +1,10 @@ +'use strict'; + +var hooks = require('./hooks.cjs'); +require('./types/index.cjs'); +var createResponseHeader = require('./createResponseHeader.cjs'); + + + +exports.createCacheControllerHooks = hooks.createCacheControllerHooks; +exports.createCacheControlResponseHeader = createResponseHeader.createCacheControlResponseHeader; diff --git a/docs/libs/v0/plugins/cacheController/index.d.ts b/docs/libs/v0/plugins/cacheController/index.d.ts new file mode 100644 index 0000000..b61d565 --- /dev/null +++ b/docs/libs/v0/plugins/cacheController/index.d.ts @@ -0,0 +1,3 @@ +export * from "./hooks"; +export * from "./types"; +export * from "./createResponseHeader"; diff --git a/docs/libs/v0/plugins/cacheController/index.mjs b/docs/libs/v0/plugins/cacheController/index.mjs new file mode 100644 index 0000000..a5c5073 --- /dev/null +++ b/docs/libs/v0/plugins/cacheController/index.mjs @@ -0,0 +1,3 @@ +export { createCacheControllerHooks } from './hooks.mjs'; +import './types/index.mjs'; +export { createCacheControlResponseHeader } from './createResponseHeader.mjs'; diff --git a/docs/libs/v0/plugins/cacheController/types/cacheControlDirectives.cjs b/docs/libs/v0/plugins/cacheController/types/cacheControlDirectives.cjs new file mode 100644 index 0000000..eb109ab --- /dev/null +++ b/docs/libs/v0/plugins/cacheController/types/cacheControlDirectives.cjs @@ -0,0 +1,2 @@ +'use strict'; + diff --git a/docs/libs/v0/plugins/cacheController/types/cacheControlDirectives.d.ts b/docs/libs/v0/plugins/cacheController/types/cacheControlDirectives.d.ts new file mode 100644 index 0000000..b6a2b2b --- /dev/null +++ b/docs/libs/v0/plugins/cacheController/types/cacheControlDirectives.d.ts @@ -0,0 +1,16 @@ +export interface CacheControlDirectives { + maxAge?: number; + sMaxAge?: number; + public?: true; + private?: true | string[]; + noCache?: true | string[]; + noStore?: true; + noTransform?: true; + mustRevalidate?: true; + proxyRevalidate?: true; + immutable?: true; + staleWhileRevalidate?: number; + staleIfError?: number; + mustUnderstand?: true; + extensions?: Record; +} diff --git a/docs/libs/v0/plugins/cacheController/types/cacheControlDirectives.mjs b/docs/libs/v0/plugins/cacheController/types/cacheControlDirectives.mjs new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/docs/libs/v0/plugins/cacheController/types/cacheControlDirectives.mjs @@ -0,0 +1 @@ + diff --git a/docs/libs/v0/plugins/cacheController/types/index.cjs b/docs/libs/v0/plugins/cacheController/types/index.cjs new file mode 100644 index 0000000..0f0ca33 --- /dev/null +++ b/docs/libs/v0/plugins/cacheController/types/index.cjs @@ -0,0 +1,4 @@ +'use strict'; + +require('./cacheControlDirectives.cjs'); + diff --git a/docs/libs/v0/plugins/cacheController/types/index.d.ts b/docs/libs/v0/plugins/cacheController/types/index.d.ts new file mode 100644 index 0000000..d030668 --- /dev/null +++ b/docs/libs/v0/plugins/cacheController/types/index.d.ts @@ -0,0 +1 @@ +export * from "./cacheControlDirectives"; diff --git a/docs/libs/v0/plugins/cacheController/types/index.mjs b/docs/libs/v0/plugins/cacheController/types/index.mjs new file mode 100644 index 0000000..68926cf --- /dev/null +++ b/docs/libs/v0/plugins/cacheController/types/index.mjs @@ -0,0 +1 @@ +import './cacheControlDirectives.mjs'; diff --git a/docs/libs/v0/plugins/codeGenerator/plugin.cjs b/docs/libs/v0/plugins/codeGenerator/plugin.cjs index bfb0587..05e2afb 100644 --- a/docs/libs/v0/plugins/codeGenerator/plugin.cjs +++ b/docs/libs/v0/plugins/codeGenerator/plugin.cjs @@ -4,7 +4,7 @@ var DataParserToTypescript = require('@duplojs/data-parser-tools/toTypescript'); var utils = require('@duplojs/utils'); var routeToDataParser = require('./routeToDataParser.cjs'); var serverUtils = require('@duplojs/server-utils'); -var typescriptTransfomer = require('./typescriptTransfomer.cjs'); +var typescriptTransformer = require('./typescriptTransformer.cjs'); function _interopNamespaceDefault(e) { var n = Object.create(null); @@ -45,9 +45,9 @@ function codeGeneratorPlugin(pluginParams) { identifier: "Routes", mode: "in", transformers: [ - typescriptTransfomer.fileTransformer, - typescriptTransfomer.dateTransformer, - typescriptTransfomer.timeTransformer, + typescriptTransformer.fileTransformer, + typescriptTransformer.dateTransformer, + typescriptTransformer.timeTransformer, ...DataParserToTypescript__namespace.defaultTransformers, ], }); diff --git a/docs/libs/v0/plugins/codeGenerator/plugin.mjs b/docs/libs/v0/plugins/codeGenerator/plugin.mjs index 1933aeb..b557737 100644 --- a/docs/libs/v0/plugins/codeGenerator/plugin.mjs +++ b/docs/libs/v0/plugins/codeGenerator/plugin.mjs @@ -2,7 +2,7 @@ import * as DataParserToTypescript from '@duplojs/data-parser-tools/toTypescript import { equal, A, DP, asserts, E } from '@duplojs/utils'; import { routeToDataParser } from './routeToDataParser.mjs'; import { SF } from '@duplojs/server-utils'; -import { fileTransformer, dateTransformer, timeTransformer } from './typescriptTransfomer.mjs'; +import { fileTransformer, dateTransformer, timeTransformer } from './typescriptTransformer.mjs'; function codeGeneratorPlugin(pluginParams) { return () => ({ diff --git a/docs/libs/v0/plugins/codeGenerator/typescriptTransfomer.cjs b/docs/libs/v0/plugins/codeGenerator/typescriptTransformer.cjs similarity index 100% rename from docs/libs/v0/plugins/codeGenerator/typescriptTransfomer.cjs rename to docs/libs/v0/plugins/codeGenerator/typescriptTransformer.cjs diff --git a/docs/libs/v0/plugins/codeGenerator/typescriptTransfomer.d.ts b/docs/libs/v0/plugins/codeGenerator/typescriptTransformer.d.ts similarity index 100% rename from docs/libs/v0/plugins/codeGenerator/typescriptTransfomer.d.ts rename to docs/libs/v0/plugins/codeGenerator/typescriptTransformer.d.ts diff --git a/docs/libs/v0/plugins/codeGenerator/typescriptTransfomer.mjs b/docs/libs/v0/plugins/codeGenerator/typescriptTransformer.mjs similarity index 100% rename from docs/libs/v0/plugins/codeGenerator/typescriptTransfomer.mjs rename to docs/libs/v0/plugins/codeGenerator/typescriptTransformer.mjs diff --git a/docs/libs/v0/plugins/cors/headerFunctions/allowHeaders.cjs b/docs/libs/v0/plugins/cors/headerFunctions/allowHeaders.cjs new file mode 100644 index 0000000..bac7d8a --- /dev/null +++ b/docs/libs/v0/plugins/cors/headerFunctions/allowHeaders.cjs @@ -0,0 +1,11 @@ +'use strict'; + +const allowHeadersFunction = { + default(allowHeaders) { + return (request, response) => { + response.setHeader("access-control-allow-headers", allowHeaders); + }; + }, +}; + +exports.allowHeadersFunction = allowHeadersFunction; diff --git a/docs/libs/v0/plugins/cors/headerFunctions/allowHeaders.d.ts b/docs/libs/v0/plugins/cors/headerFunctions/allowHeaders.d.ts new file mode 100644 index 0000000..479e203 --- /dev/null +++ b/docs/libs/v0/plugins/cors/headerFunctions/allowHeaders.d.ts @@ -0,0 +1,5 @@ +import type { Request } from "../../../core/request"; +import type { Response } from "../../../core/response"; +export declare const allowHeadersFunction: { + default(allowHeaders: string): (request: Request, response: Response) => void; +}; diff --git a/docs/libs/v0/plugins/cors/headerFunctions/allowHeaders.mjs b/docs/libs/v0/plugins/cors/headerFunctions/allowHeaders.mjs new file mode 100644 index 0000000..1c46df7 --- /dev/null +++ b/docs/libs/v0/plugins/cors/headerFunctions/allowHeaders.mjs @@ -0,0 +1,9 @@ +const allowHeadersFunction = { + default(allowHeaders) { + return (request, response) => { + response.setHeader("access-control-allow-headers", allowHeaders); + }; + }, +}; + +export { allowHeadersFunction }; diff --git a/docs/libs/v0/plugins/cors/headerFunctions/allowMethods.cjs b/docs/libs/v0/plugins/cors/headerFunctions/allowMethods.cjs new file mode 100644 index 0000000..ae237d2 --- /dev/null +++ b/docs/libs/v0/plugins/cors/headerFunctions/allowMethods.cjs @@ -0,0 +1,16 @@ +'use strict'; + +const allowMethodsFunction = { + default(methods) { + return (request, response) => { + response.setHeader("access-control-allow-methods", methods); + }; + }, + isBool(allowMethods) { + return (request, response) => { + response.setHeader("access-control-allow-methods", allowMethods[request.path]); + }; + }, +}; + +exports.allowMethodsFunction = allowMethodsFunction; diff --git a/docs/libs/v0/plugins/cors/headerFunctions/allowMethods.d.ts b/docs/libs/v0/plugins/cors/headerFunctions/allowMethods.d.ts new file mode 100644 index 0000000..f865430 --- /dev/null +++ b/docs/libs/v0/plugins/cors/headerFunctions/allowMethods.d.ts @@ -0,0 +1,6 @@ +import type { Request } from "../../../core/request"; +import type { Response } from "../../../core/response"; +export declare const allowMethodsFunction: { + default(methods: string): (request: Request, response: Response) => void; + isBool(allowMethods: Record): (request: Request, response: Response) => void; +}; diff --git a/docs/libs/v0/plugins/cors/headerFunctions/allowMethods.mjs b/docs/libs/v0/plugins/cors/headerFunctions/allowMethods.mjs new file mode 100644 index 0000000..e96dc66 --- /dev/null +++ b/docs/libs/v0/plugins/cors/headerFunctions/allowMethods.mjs @@ -0,0 +1,14 @@ +const allowMethodsFunction = { + default(methods) { + return (request, response) => { + response.setHeader("access-control-allow-methods", methods); + }; + }, + isBool(allowMethods) { + return (request, response) => { + response.setHeader("access-control-allow-methods", allowMethods[request.path]); + }; + }, +}; + +export { allowMethodsFunction }; diff --git a/docs/libs/v0/plugins/cors/headerFunctions/allowOrigin.cjs b/docs/libs/v0/plugins/cors/headerFunctions/allowOrigin.cjs new file mode 100644 index 0000000..17c15d1 --- /dev/null +++ b/docs/libs/v0/plugins/cors/headerFunctions/allowOrigin.cjs @@ -0,0 +1,24 @@ +'use strict'; + +const allowOriginFunction = { + default(allowOrigin) { + return (request, response) => { + if (allowOrigin.test(request.origin)) { + response.setHeader("access-control-allow-origin", request.origin); + } + }; + }, + isFunction(allowOrigin) { + return async (request, response) => { + let result = allowOrigin(request.origin); + if (result instanceof Promise) { + result = await result; + } + if (result === true) { + response.setHeader("access-control-allow-origin", request.origin); + } + }; + }, +}; + +exports.allowOriginFunction = allowOriginFunction; diff --git a/docs/libs/v0/plugins/cors/headerFunctions/allowOrigin.d.ts b/docs/libs/v0/plugins/cors/headerFunctions/allowOrigin.d.ts new file mode 100644 index 0000000..048979b --- /dev/null +++ b/docs/libs/v0/plugins/cors/headerFunctions/allowOrigin.d.ts @@ -0,0 +1,7 @@ +import type { Request } from "../../../core/request"; +import type { Response } from "../../../core/response"; +import type { MaybePromise } from "@duplojs/utils"; +export declare const allowOriginFunction: { + default(allowOrigin: RegExp): (request: Request, response: Response) => void; + isFunction(allowOrigin: (origin: string) => MaybePromise): (request: Request, response: Response) => Promise; +}; diff --git a/docs/libs/v0/plugins/cors/headerFunctions/allowOrigin.mjs b/docs/libs/v0/plugins/cors/headerFunctions/allowOrigin.mjs new file mode 100644 index 0000000..40621ca --- /dev/null +++ b/docs/libs/v0/plugins/cors/headerFunctions/allowOrigin.mjs @@ -0,0 +1,22 @@ +const allowOriginFunction = { + default(allowOrigin) { + return (request, response) => { + if (allowOrigin.test(request.origin)) { + response.setHeader("access-control-allow-origin", request.origin); + } + }; + }, + isFunction(allowOrigin) { + return async (request, response) => { + let result = allowOrigin(request.origin); + if (result instanceof Promise) { + result = await result; + } + if (result === true) { + response.setHeader("access-control-allow-origin", request.origin); + } + }; + }, +}; + +export { allowOriginFunction }; diff --git a/docs/libs/v0/plugins/cors/headerFunctions/credentials.cjs b/docs/libs/v0/plugins/cors/headerFunctions/credentials.cjs new file mode 100644 index 0000000..e49c57f --- /dev/null +++ b/docs/libs/v0/plugins/cors/headerFunctions/credentials.cjs @@ -0,0 +1,11 @@ +'use strict'; + +const credentialsFunction = { + default() { + return (request, response) => { + response.setHeader("access-control-allow-credentials", "true"); + }; + }, +}; + +exports.credentialsFunction = credentialsFunction; diff --git a/docs/libs/v0/plugins/cors/headerFunctions/credentials.d.ts b/docs/libs/v0/plugins/cors/headerFunctions/credentials.d.ts new file mode 100644 index 0000000..1c428f2 --- /dev/null +++ b/docs/libs/v0/plugins/cors/headerFunctions/credentials.d.ts @@ -0,0 +1,5 @@ +import type { Request } from "../../../core/request"; +import type { Response } from "../../../core/response"; +export declare const credentialsFunction: { + default(): (request: Request, response: Response) => void; +}; diff --git a/docs/libs/v0/plugins/cors/headerFunctions/credentials.mjs b/docs/libs/v0/plugins/cors/headerFunctions/credentials.mjs new file mode 100644 index 0000000..a8424bd --- /dev/null +++ b/docs/libs/v0/plugins/cors/headerFunctions/credentials.mjs @@ -0,0 +1,9 @@ +const credentialsFunction = { + default() { + return (request, response) => { + response.setHeader("access-control-allow-credentials", "true"); + }; + }, +}; + +export { credentialsFunction }; diff --git a/docs/libs/v0/plugins/cors/headerFunctions/exposeHeaders.cjs b/docs/libs/v0/plugins/cors/headerFunctions/exposeHeaders.cjs new file mode 100644 index 0000000..51451ee --- /dev/null +++ b/docs/libs/v0/plugins/cors/headerFunctions/exposeHeaders.cjs @@ -0,0 +1,11 @@ +'use strict'; + +const exposeHeadersFunction = { + default(exposeHeaders) { + return (request, response) => { + response.setHeader("access-control-expose-headers", exposeHeaders); + }; + }, +}; + +exports.exposeHeadersFunction = exposeHeadersFunction; diff --git a/docs/libs/v0/plugins/cors/headerFunctions/exposeHeaders.d.ts b/docs/libs/v0/plugins/cors/headerFunctions/exposeHeaders.d.ts new file mode 100644 index 0000000..0c0e39c --- /dev/null +++ b/docs/libs/v0/plugins/cors/headerFunctions/exposeHeaders.d.ts @@ -0,0 +1,5 @@ +import type { Request } from "../../../core/request"; +import type { Response } from "../../../core/response"; +export declare const exposeHeadersFunction: { + default(exposeHeaders: string): (request: Request, response: Response) => void; +}; diff --git a/docs/libs/v0/plugins/cors/headerFunctions/exposeHeaders.mjs b/docs/libs/v0/plugins/cors/headerFunctions/exposeHeaders.mjs new file mode 100644 index 0000000..58a324b --- /dev/null +++ b/docs/libs/v0/plugins/cors/headerFunctions/exposeHeaders.mjs @@ -0,0 +1,9 @@ +const exposeHeadersFunction = { + default(exposeHeaders) { + return (request, response) => { + response.setHeader("access-control-expose-headers", exposeHeaders); + }; + }, +}; + +export { exposeHeadersFunction }; diff --git a/docs/libs/v0/plugins/cors/headerFunctions/index.cjs b/docs/libs/v0/plugins/cors/headerFunctions/index.cjs new file mode 100644 index 0000000..d0efcbc --- /dev/null +++ b/docs/libs/v0/plugins/cors/headerFunctions/index.cjs @@ -0,0 +1,19 @@ +'use strict'; + +var allowHeaders = require('./allowHeaders.cjs'); +var allowMethods = require('./allowMethods.cjs'); +var allowOrigin = require('./allowOrigin.cjs'); +var credentials = require('./credentials.cjs'); +var exposeHeaders = require('./exposeHeaders.cjs'); +var maxAge = require('./maxAge.cjs'); +var vary = require('./vary.cjs'); + + + +exports.allowHeadersFunction = allowHeaders.allowHeadersFunction; +exports.allowMethodsFunction = allowMethods.allowMethodsFunction; +exports.allowOriginFunction = allowOrigin.allowOriginFunction; +exports.credentialsFunction = credentials.credentialsFunction; +exports.exposeHeadersFunction = exposeHeaders.exposeHeadersFunction; +exports.maxAgeFunction = maxAge.maxAgeFunction; +exports.varyFunction = vary.varyFunction; diff --git a/docs/libs/v0/plugins/cors/headerFunctions/index.d.ts b/docs/libs/v0/plugins/cors/headerFunctions/index.d.ts new file mode 100644 index 0000000..5a08d2f --- /dev/null +++ b/docs/libs/v0/plugins/cors/headerFunctions/index.d.ts @@ -0,0 +1,7 @@ +export * from "./allowHeaders"; +export * from "./allowMethods"; +export * from "./allowOrigin"; +export * from "./credentials"; +export * from "./exposeHeaders"; +export * from "./maxAge"; +export * from "./vary"; diff --git a/docs/libs/v0/plugins/cors/headerFunctions/index.mjs b/docs/libs/v0/plugins/cors/headerFunctions/index.mjs new file mode 100644 index 0000000..5eb1d18 --- /dev/null +++ b/docs/libs/v0/plugins/cors/headerFunctions/index.mjs @@ -0,0 +1,7 @@ +export { allowHeadersFunction } from './allowHeaders.mjs'; +export { allowMethodsFunction } from './allowMethods.mjs'; +export { allowOriginFunction } from './allowOrigin.mjs'; +export { credentialsFunction } from './credentials.mjs'; +export { exposeHeadersFunction } from './exposeHeaders.mjs'; +export { maxAgeFunction } from './maxAge.mjs'; +export { varyFunction } from './vary.mjs'; diff --git a/docs/libs/v0/plugins/cors/headerFunctions/maxAge.cjs b/docs/libs/v0/plugins/cors/headerFunctions/maxAge.cjs new file mode 100644 index 0000000..7e546a9 --- /dev/null +++ b/docs/libs/v0/plugins/cors/headerFunctions/maxAge.cjs @@ -0,0 +1,11 @@ +'use strict'; + +const maxAgeFunction = { + default(maxAge) { + return (request, response) => { + response.setHeader("access-control-max-age", maxAge); + }; + }, +}; + +exports.maxAgeFunction = maxAgeFunction; diff --git a/docs/libs/v0/plugins/cors/headerFunctions/maxAge.d.ts b/docs/libs/v0/plugins/cors/headerFunctions/maxAge.d.ts new file mode 100644 index 0000000..9f9c77c --- /dev/null +++ b/docs/libs/v0/plugins/cors/headerFunctions/maxAge.d.ts @@ -0,0 +1,5 @@ +import type { Request } from "../../../core/request"; +import type { Response } from "../../../core/response"; +export declare const maxAgeFunction: { + default(maxAge: string): (request: Request, response: Response) => void; +}; diff --git a/docs/libs/v0/plugins/cors/headerFunctions/maxAge.mjs b/docs/libs/v0/plugins/cors/headerFunctions/maxAge.mjs new file mode 100644 index 0000000..f6bd1b6 --- /dev/null +++ b/docs/libs/v0/plugins/cors/headerFunctions/maxAge.mjs @@ -0,0 +1,9 @@ +const maxAgeFunction = { + default(maxAge) { + return (request, response) => { + response.setHeader("access-control-max-age", maxAge); + }; + }, +}; + +export { maxAgeFunction }; diff --git a/docs/libs/v0/plugins/cors/headerFunctions/vary.cjs b/docs/libs/v0/plugins/cors/headerFunctions/vary.cjs new file mode 100644 index 0000000..2ad2d7d --- /dev/null +++ b/docs/libs/v0/plugins/cors/headerFunctions/vary.cjs @@ -0,0 +1,35 @@ +'use strict'; + +const starRegex = /(^|,) *\* *(?=,|$)/; +const originRegex = /(^|,) *origin *(?=,|$)/i; +const varyFunction = { + default() { + const maxStoreSize = 500; + const store = new Map(); + return (request, response) => { + const cachedVary = store.get(request.origin); + if (cachedVary) { + response.setHeader("vary", cachedVary); + return; + } + let varyValue = Array.isArray(response.headers?.Vary) + ? response.headers.Vary.join(", ") + : response.headers?.Vary; + if (varyValue === undefined) { + varyValue = "Origin"; + } + else if (starRegex.test(varyValue)) { + varyValue = "*"; + } + else if (!originRegex.test(varyValue)) { + varyValue = `${varyValue}, Origin`; + } + if (store.size < maxStoreSize) { + store.set(request.origin, varyValue); + } + response.setHeader("vary", varyValue); + }; + }, +}; + +exports.varyFunction = varyFunction; diff --git a/docs/libs/v0/plugins/cors/headerFunctions/vary.d.ts b/docs/libs/v0/plugins/cors/headerFunctions/vary.d.ts new file mode 100644 index 0000000..231e029 --- /dev/null +++ b/docs/libs/v0/plugins/cors/headerFunctions/vary.d.ts @@ -0,0 +1,5 @@ +import type { Request } from "../../../core/request"; +import type { Response } from "../../../core/response"; +export declare const varyFunction: { + default(): (request: Request, response: Response) => void; +}; diff --git a/docs/libs/v0/plugins/cors/headerFunctions/vary.mjs b/docs/libs/v0/plugins/cors/headerFunctions/vary.mjs new file mode 100644 index 0000000..55e17bf --- /dev/null +++ b/docs/libs/v0/plugins/cors/headerFunctions/vary.mjs @@ -0,0 +1,33 @@ +const starRegex = /(^|,) *\* *(?=,|$)/; +const originRegex = /(^|,) *origin *(?=,|$)/i; +const varyFunction = { + default() { + const maxStoreSize = 500; + const store = new Map(); + return (request, response) => { + const cachedVary = store.get(request.origin); + if (cachedVary) { + response.setHeader("vary", cachedVary); + return; + } + let varyValue = Array.isArray(response.headers?.Vary) + ? response.headers.Vary.join(", ") + : response.headers?.Vary; + if (varyValue === undefined) { + varyValue = "Origin"; + } + else if (starRegex.test(varyValue)) { + varyValue = "*"; + } + else if (!originRegex.test(varyValue)) { + varyValue = `${varyValue}, Origin`; + } + if (store.size < maxStoreSize) { + store.set(request.origin, varyValue); + } + response.setHeader("vary", varyValue); + }; + }, +}; + +export { varyFunction }; diff --git a/docs/libs/v0/plugins/cors/index.cjs b/docs/libs/v0/plugins/cors/index.cjs new file mode 100644 index 0000000..8f02380 --- /dev/null +++ b/docs/libs/v0/plugins/cors/index.cjs @@ -0,0 +1,9 @@ +'use strict'; + +var metadata = require('./metadata.cjs'); +var plugin = require('./plugin.cjs'); + + + +exports.IgnoreRouteCorsMetadata = metadata.IgnoreRouteCorsMetadata; +exports.corsPlugin = plugin.corsPlugin; diff --git a/docs/libs/v0/plugins/cors/index.d.ts b/docs/libs/v0/plugins/cors/index.d.ts new file mode 100644 index 0000000..f360fc6 --- /dev/null +++ b/docs/libs/v0/plugins/cors/index.d.ts @@ -0,0 +1,2 @@ +export * from "./metadata"; +export * from "./plugin"; diff --git a/docs/libs/v0/plugins/cors/index.mjs b/docs/libs/v0/plugins/cors/index.mjs new file mode 100644 index 0000000..afb929c --- /dev/null +++ b/docs/libs/v0/plugins/cors/index.mjs @@ -0,0 +1,2 @@ +export { IgnoreRouteCorsMetadata } from './metadata.mjs'; +export { corsPlugin } from './plugin.mjs'; diff --git a/docs/libs/v0/plugins/cors/metadata.cjs b/docs/libs/v0/plugins/cors/metadata.cjs new file mode 100644 index 0000000..9da0f9b --- /dev/null +++ b/docs/libs/v0/plugins/cors/metadata.cjs @@ -0,0 +1,8 @@ +'use strict'; + +require('../../core/metadata/index.cjs'); +var base = require('../../core/metadata/base.cjs'); + +const IgnoreRouteCorsMetadata = base.createMetadata("ignore-by-cors"); + +exports.IgnoreRouteCorsMetadata = IgnoreRouteCorsMetadata; diff --git a/docs/libs/v0/plugins/cors/metadata.d.ts b/docs/libs/v0/plugins/cors/metadata.d.ts new file mode 100644 index 0000000..12ec567 --- /dev/null +++ b/docs/libs/v0/plugins/cors/metadata.d.ts @@ -0,0 +1 @@ +export declare const IgnoreRouteCorsMetadata: import("../../core/metadata").MetadataHandler<"ignore-by-cors", unknown>; diff --git a/docs/libs/v0/plugins/cors/metadata.mjs b/docs/libs/v0/plugins/cors/metadata.mjs new file mode 100644 index 0000000..d4bfc11 --- /dev/null +++ b/docs/libs/v0/plugins/cors/metadata.mjs @@ -0,0 +1,6 @@ +import '../../core/metadata/index.mjs'; +import { createMetadata } from '../../core/metadata/base.mjs'; + +const IgnoreRouteCorsMetadata = createMetadata("ignore-by-cors"); + +export { IgnoreRouteCorsMetadata }; diff --git a/docs/libs/v0/plugins/cors/plugin.cjs b/docs/libs/v0/plugins/cors/plugin.cjs new file mode 100644 index 0000000..ef6f566 --- /dev/null +++ b/docs/libs/v0/plugins/cors/plugin.cjs @@ -0,0 +1,108 @@ +'use strict'; + +var utils = require('@duplojs/utils'); +var metadata = require('./metadata.cjs'); +var index = require('../../core/route/index.cjs'); +require('./headerFunctions/index.cjs'); +var vary = require('./headerFunctions/vary.cjs'); +var allowOrigin = require('./headerFunctions/allowOrigin.cjs'); +var exposeHeaders = require('./headerFunctions/exposeHeaders.cjs'); +var credentials = require('./headerFunctions/credentials.cjs'); +var hooks = require('../../core/route/hooks.cjs'); +var allowMethods = require('./headerFunctions/allowMethods.cjs'); +var allowHeaders = require('./headerFunctions/allowHeaders.cjs'); +var maxAge = require('./headerFunctions/maxAge.cjs'); + +/* eslint-disable @typescript-eslint/prefer-for-of */ +function corsPlugin(params) { + const headerFunctionOtherRoutes = []; + if (params.allowOrigin) { + headerFunctionOtherRoutes.push(vary.varyFunction.default()); + headerFunctionOtherRoutes.push(typeof params.allowOrigin === "function" + ? allowOrigin.allowOriginFunction.isFunction(params.allowOrigin) + : allowOrigin.allowOriginFunction.default(utils.toRegExp(params.allowOrigin === true + ? "*" + : params.allowOrigin))); + } + if (params.exposeHeaders) { + headerFunctionOtherRoutes.push(utils.pipe(params.exposeHeaders, utils.A.coalescing, utils.A.join(","), exposeHeaders.exposeHeadersFunction.default)); + } + if (params.credentials) { + headerFunctionOtherRoutes.push(credentials.credentialsFunction.default()); + } + const hookOtherRoute = hooks.createHookRouteLifeCycle({ + beforeSendResponse: (params) => { + for (let index = 0; index < headerFunctionOtherRoutes.length; index++) { + headerFunctionOtherRoutes[index](params.request, params.currentResponse); + } + return params.next(); + }, + }); + return () => ({ + name: "cors", + hooksHubLifeCycle: [ + { + beforeServerBuildRoutes: (hub) => { + const headerFunctionRouteOptions = []; + if (params.allowMethods === true) { + const allowMethodsFunctionIsBool = utils.pipe(hub.routes, utils.G.filter((route) => !utils.A.some(route.definition.metadata, metadata.IgnoreRouteCorsMetadata.is)), utils.G.map((route) => utils.A.map(route.definition.paths, (path) => ({ + path, + method: route.definition.method, + }))), utils.G.flat, utils.G.reduce(utils.G.reduceFrom({}), ({ element, lastValue, next }) => { + lastValue[element.path] = lastValue[element.path] + ? `${lastValue[element.path]},${element.method}` + : element.method; + return next(lastValue); + }), allowMethods.allowMethodsFunction.isBool); + headerFunctionRouteOptions.push(allowMethodsFunctionIsBool); + } + else if (params.allowMethods) { + headerFunctionRouteOptions.push(utils.pipe(params.allowMethods, utils.A.coalescing, utils.A.join(","), allowMethods.allowMethodsFunction.default)); + } + if (params.allowHeaders) { + headerFunctionRouteOptions.push(allowHeaders.allowHeadersFunction.default(params.allowHeaders === true + ? "*" + : utils.pipe(params.allowHeaders, utils.A.coalescing, utils.A.join(",")))); + } + if (params.maxAge) { + headerFunctionRouteOptions.push(maxAge.maxAgeFunction.default(params.maxAge.toString())); + } + const hookRouteOptions = hooks.createHookRouteLifeCycle({ + beforeRouteExecution: (params) => { + const response = params.response("204", "cors"); + for (let index = 0; index < headerFunctionRouteOptions.length; index++) { + headerFunctionRouteOptions[index](params.request, response); + } + return response; + }, + }); + const routeOptions = index.createRoute({ + paths: ["/*"], + method: "OPTIONS", + hooks: [hookRouteOptions], + metadata: [metadata.IgnoreRouteCorsMetadata()], + steps: [], + preflightSteps: [], + bodyController: null, + }); + hub.register(routeOptions); + return hub; + }, + beforeBuildRoute: (route) => { + if (route.definition.method === "OPTIONS" || utils.A.some(route.definition.metadata, metadata.IgnoreRouteCorsMetadata.is)) { + return route; + } + return { + ...route, + definition: { + ...route.definition, + hooks: [...route.definition.hooks, hookOtherRoute], + }, + }; + }, + }, + ], + }); +} + +exports.corsPlugin = corsPlugin; diff --git a/docs/libs/v0/plugins/cors/plugin.d.ts b/docs/libs/v0/plugins/cors/plugin.d.ts new file mode 100644 index 0000000..94ee794 --- /dev/null +++ b/docs/libs/v0/plugins/cors/plugin.d.ts @@ -0,0 +1,12 @@ +import { type AnyTuple, type MaybePromise, type O } from "@duplojs/utils"; +import { type RequestMethods } from "../../core/request"; +import { type HubPlugin } from "../../core/hub"; +export interface CorsPluginParams { + readonly allowOrigin?: string | RegExp | AnyTuple | ((origin: string) => MaybePromise) | true; + readonly allowHeaders?: string | AnyTuple | true; + readonly exposeHeaders?: string | AnyTuple; + readonly maxAge?: number; + readonly credentials?: boolean; + readonly allowMethods?: RequestMethods | AnyTuple | true; +} +export declare function corsPlugin(params: GenericParams & O.RequireAtLeastOne): () => HubPlugin; diff --git a/docs/libs/v0/plugins/cors/plugin.mjs b/docs/libs/v0/plugins/cors/plugin.mjs new file mode 100644 index 0000000..0e02275 --- /dev/null +++ b/docs/libs/v0/plugins/cors/plugin.mjs @@ -0,0 +1,106 @@ +import { toRegExp, pipe, A, G } from '@duplojs/utils'; +import { IgnoreRouteCorsMetadata } from './metadata.mjs'; +import { createRoute } from '../../core/route/index.mjs'; +import './headerFunctions/index.mjs'; +import { varyFunction } from './headerFunctions/vary.mjs'; +import { allowOriginFunction } from './headerFunctions/allowOrigin.mjs'; +import { exposeHeadersFunction } from './headerFunctions/exposeHeaders.mjs'; +import { credentialsFunction } from './headerFunctions/credentials.mjs'; +import { createHookRouteLifeCycle } from '../../core/route/hooks.mjs'; +import { allowMethodsFunction } from './headerFunctions/allowMethods.mjs'; +import { allowHeadersFunction } from './headerFunctions/allowHeaders.mjs'; +import { maxAgeFunction } from './headerFunctions/maxAge.mjs'; + +/* eslint-disable @typescript-eslint/prefer-for-of */ +function corsPlugin(params) { + const headerFunctionOtherRoutes = []; + if (params.allowOrigin) { + headerFunctionOtherRoutes.push(varyFunction.default()); + headerFunctionOtherRoutes.push(typeof params.allowOrigin === "function" + ? allowOriginFunction.isFunction(params.allowOrigin) + : allowOriginFunction.default(toRegExp(params.allowOrigin === true + ? "*" + : params.allowOrigin))); + } + if (params.exposeHeaders) { + headerFunctionOtherRoutes.push(pipe(params.exposeHeaders, A.coalescing, A.join(","), exposeHeadersFunction.default)); + } + if (params.credentials) { + headerFunctionOtherRoutes.push(credentialsFunction.default()); + } + const hookOtherRoute = createHookRouteLifeCycle({ + beforeSendResponse: (params) => { + for (let index = 0; index < headerFunctionOtherRoutes.length; index++) { + headerFunctionOtherRoutes[index](params.request, params.currentResponse); + } + return params.next(); + }, + }); + return () => ({ + name: "cors", + hooksHubLifeCycle: [ + { + beforeServerBuildRoutes: (hub) => { + const headerFunctionRouteOptions = []; + if (params.allowMethods === true) { + const allowMethodsFunctionIsBool = pipe(hub.routes, G.filter((route) => !A.some(route.definition.metadata, IgnoreRouteCorsMetadata.is)), G.map((route) => A.map(route.definition.paths, (path) => ({ + path, + method: route.definition.method, + }))), G.flat, G.reduce(G.reduceFrom({}), ({ element, lastValue, next }) => { + lastValue[element.path] = lastValue[element.path] + ? `${lastValue[element.path]},${element.method}` + : element.method; + return next(lastValue); + }), allowMethodsFunction.isBool); + headerFunctionRouteOptions.push(allowMethodsFunctionIsBool); + } + else if (params.allowMethods) { + headerFunctionRouteOptions.push(pipe(params.allowMethods, A.coalescing, A.join(","), allowMethodsFunction.default)); + } + if (params.allowHeaders) { + headerFunctionRouteOptions.push(allowHeadersFunction.default(params.allowHeaders === true + ? "*" + : pipe(params.allowHeaders, A.coalescing, A.join(",")))); + } + if (params.maxAge) { + headerFunctionRouteOptions.push(maxAgeFunction.default(params.maxAge.toString())); + } + const hookRouteOptions = createHookRouteLifeCycle({ + beforeRouteExecution: (params) => { + const response = params.response("204", "cors"); + for (let index = 0; index < headerFunctionRouteOptions.length; index++) { + headerFunctionRouteOptions[index](params.request, response); + } + return response; + }, + }); + const routeOptions = createRoute({ + paths: ["/*"], + method: "OPTIONS", + hooks: [hookRouteOptions], + metadata: [IgnoreRouteCorsMetadata()], + steps: [], + preflightSteps: [], + bodyController: null, + }); + hub.register(routeOptions); + return hub; + }, + beforeBuildRoute: (route) => { + if (route.definition.method === "OPTIONS" || A.some(route.definition.metadata, IgnoreRouteCorsMetadata.is)) { + return route; + } + return { + ...route, + definition: { + ...route.definition, + hooks: [...route.definition.hooks, hookOtherRoute], + }, + }; + }, + }, + ], + }); +} + +export { corsPlugin }; diff --git a/docs/libs/v0/plugins/openApiGenerator/makeOpenApiRoute.d.ts b/docs/libs/v0/plugins/openApiGenerator/makeOpenApiRoute.d.ts index f4f0f23..7cdb002 100644 --- a/docs/libs/v0/plugins/openApiGenerator/makeOpenApiRoute.d.ts +++ b/docs/libs/v0/plugins/openApiGenerator/makeOpenApiRoute.d.ts @@ -17,7 +17,7 @@ export declare function makeOpenApiRoute(routePath: RoutePath, openApiPage: stri readonly coerce: boolean; readonly checkers: readonly []; }>>>; - theFunction(floor: {}, param: import("../../core/steps").HandlerStepFunctionParams>): import("@duplojs/utils").MaybePromise>; + theFunction(floor: {}, param: import("../../core/steps").HandlerStepFunctionParams>): import("@duplojs/utils").MaybePromise>; readonly metadata: readonly []; }>]; }>; diff --git a/docs/libs/v0/plugins/static/index.cjs b/docs/libs/v0/plugins/static/index.cjs new file mode 100644 index 0000000..cc1e921 --- /dev/null +++ b/docs/libs/v0/plugins/static/index.cjs @@ -0,0 +1,14 @@ +'use strict'; + +var makeRouteFolder = require('./makeRouteFolder.cjs'); +var makeRouteFile = require('./makeRouteFile.cjs'); +var plugin = require('./plugin.cjs'); + + + +exports.makeRouteFolder = makeRouteFolder.makeRouteFolder; +exports.MissingSelectedStaticFileError = makeRouteFile.MissingSelectedStaticFileError; +exports.SelectedStaticFileIsNotFileError = makeRouteFile.SelectedStaticFileIsNotFileError; +exports.makeRouteFile = makeRouteFile.makeRouteFile; +exports.StaticPluginError = plugin.StaticPluginError; +exports.staticPlugin = plugin.staticPlugin; diff --git a/docs/libs/v0/plugins/static/index.d.ts b/docs/libs/v0/plugins/static/index.d.ts new file mode 100644 index 0000000..a5d981b --- /dev/null +++ b/docs/libs/v0/plugins/static/index.d.ts @@ -0,0 +1,3 @@ +export * from "./makeRouteFolder"; +export * from "./makeRouteFile"; +export * from "./plugin"; diff --git a/docs/libs/v0/plugins/static/index.mjs b/docs/libs/v0/plugins/static/index.mjs new file mode 100644 index 0000000..aa7f032 --- /dev/null +++ b/docs/libs/v0/plugins/static/index.mjs @@ -0,0 +1,3 @@ +export { makeRouteFolder } from './makeRouteFolder.mjs'; +export { MissingSelectedStaticFileError, SelectedStaticFileIsNotFileError, makeRouteFile } from './makeRouteFile.mjs'; +export { StaticPluginError, staticPlugin } from './plugin.mjs'; diff --git a/docs/libs/v0/plugins/static/kind.cjs b/docs/libs/v0/plugins/static/kind.cjs new file mode 100644 index 0000000..fe5eb87 --- /dev/null +++ b/docs/libs/v0/plugins/static/kind.cjs @@ -0,0 +1,9 @@ +'use strict'; + +var utils = require('@duplojs/utils'); + +const createStaticPluginKind = utils.createKindNamespace( +// @ts-expect-error reserved kind namespace +"DuplojsStaticPlugin"); + +exports.createStaticPluginKind = createStaticPluginKind; diff --git a/docs/libs/v0/plugins/static/kind.d.ts b/docs/libs/v0/plugins/static/kind.d.ts new file mode 100644 index 0000000..e59fca3 --- /dev/null +++ b/docs/libs/v0/plugins/static/kind.d.ts @@ -0,0 +1,6 @@ +declare module "@duplojs/utils" { + interface ReservedKindNamespace { + DuplojsStaticPlugin: true; + } +} +export declare const createStaticPluginKind: (name: GenericName & import("@duplojs/utils/string").ForbiddenString) => import("@duplojs/utils").KindHandler>; diff --git a/docs/libs/v0/plugins/static/kind.mjs b/docs/libs/v0/plugins/static/kind.mjs new file mode 100644 index 0000000..6cb3c02 --- /dev/null +++ b/docs/libs/v0/plugins/static/kind.mjs @@ -0,0 +1,7 @@ +import { createKindNamespace } from '@duplojs/utils'; + +const createStaticPluginKind = createKindNamespace( +// @ts-expect-error reserved kind namespace +"DuplojsStaticPlugin"); + +export { createStaticPluginKind }; diff --git a/docs/libs/v0/plugins/static/makeRouteFile.cjs b/docs/libs/v0/plugins/static/makeRouteFile.cjs new file mode 100644 index 0000000..c096923 --- /dev/null +++ b/docs/libs/v0/plugins/static/makeRouteFile.cjs @@ -0,0 +1,62 @@ +'use strict'; + +var serverUtils = require('@duplojs/server-utils'); +var utils = require('@duplojs/utils'); +require('../../core/builders/index.cjs'); +require('../../core/metadata/index.cjs'); +require('../../core/response/index.cjs'); +var hooks = require('../cacheController/hooks.cjs'); +var kind = require('./kind.cjs'); +var builder = require('../../core/builders/route/builder.cjs'); +var ignoreByRouteStore = require('../../core/metadata/ignoreByRouteStore.cjs'); +var contract = require('../../core/response/contract.cjs'); + +class MissingSelectedStaticFileError extends utils.kindHeritage("missing-selected-static-file", kind.createStaticPluginKind("missing-selected-static-file"), Error) { + source; + constructor(source) { + super({}, [`Missing selected static file: ${source.path}.`]); + this.source = source; + } +} +class SelectedStaticFileIsNotFileError extends utils.kindHeritage("selected-static-file-is-not-file", kind.createStaticPluginKind("selected-static-file-is-not-file"), Error) { + source; + constructor(source) { + super({}, [`Selected static file is not file: ${source.path}.`]); + this.source = source; + } +} +function makeRouteFile(params) { + const localPath = utils.A.coalescing(params.path); + return builder.useRouteBuilder("GET", localPath, { + metadata: [ignoreByRouteStore.IgnoreByRouteStoreMetadata()], + hooks: [hooks.createCacheControllerHooks(params.cacheControlConfig)], + }) + .handler([ + contract.ResponseContract.ok("resource.found", serverUtils.SDP.file()), + contract.ResponseContract.notModified("resource.notModified"), + ], async (__, { response, request }) => { + const sourceStatResult = await params.source.stat(); + if (utils.E.isLeft(sourceStatResult)) { + throw new MissingSelectedStaticFileError(params.source); + } + const resourceStat = utils.unwrap(sourceStatResult); + if (!resourceStat.isFile) { + throw new SelectedStaticFileIsNotFileError(params.source); + } + if (request.headers["if-modified-since"] + && typeof request.headers["if-modified-since"] === "string" + && resourceStat.modifiedAt + && new Date(request.headers["if-modified-since"]).getTime() >= resourceStat.modifiedAt.getTime()) { + return response("resource.notModified") + .setHeader("last-modified", resourceStat.modifiedAt.toISOString()); + } + return resourceStat.modifiedAt + ? response("resource.found", params.source) + .setHeader("last-modified", resourceStat.modifiedAt.toISOString()) + : response("resource.found", params.source); + }); +} + +exports.MissingSelectedStaticFileError = MissingSelectedStaticFileError; +exports.SelectedStaticFileIsNotFileError = SelectedStaticFileIsNotFileError; +exports.makeRouteFile = makeRouteFile; diff --git a/docs/libs/v0/plugins/static/makeRouteFile.d.ts b/docs/libs/v0/plugins/static/makeRouteFile.d.ts new file mode 100644 index 0000000..db46a28 --- /dev/null +++ b/docs/libs/v0/plugins/static/makeRouteFile.d.ts @@ -0,0 +1,48 @@ +import { SDP, type SF } from "@duplojs/server-utils"; +import { type AnyTuple } from "@duplojs/utils"; +import { ResponseContract } from "../../core/response"; +import type { RoutePath } from "../../core/route"; +import { type CacheControlDirectives } from "../cacheController/types"; +interface MakeRouteFileParams { + readonly source: SF.FileInterface; + readonly path: RoutePath | AnyTuple; + readonly cacheControlConfig?: CacheControlDirectives; +} +declare const MissingSelectedStaticFileError_base: new (params: { + "@DuplojsStaticPlugin/missing-selected-static-file"?: unknown; +}, parentParams: readonly [message?: string | undefined, options?: ErrorOptions | undefined]) => Error & import("@duplojs/utils").Kind, unknown> & import("@duplojs/utils").Kind, unknown>; +export declare class MissingSelectedStaticFileError extends MissingSelectedStaticFileError_base { + source: SF.FileInterface; + constructor(source: SF.FileInterface); +} +declare const SelectedStaticFileIsNotFileError_base: new (params: { + "@DuplojsStaticPlugin/selected-static-file-is-not-file"?: unknown; +}, parentParams: readonly [message?: string | undefined, options?: ErrorOptions | undefined]) => Error & import("@duplojs/utils").Kind, unknown> & import("@duplojs/utils").Kind, unknown>; +export declare class SelectedStaticFileIsNotFileError extends SelectedStaticFileIsNotFileError_base { + source: SF.FileInterface; + constructor(source: SF.FileInterface); +} +export declare function makeRouteFile(params: MakeRouteFileParams): import("../../core/route").Route<{ + readonly method: "GET"; + readonly metadata: readonly [import("../../core/metadata").Metadata<"ignore-by-route-store", unknown>]; + readonly hooks: readonly [{ + readonly beforeSendResponse: ({ currentResponse, next }: import("../../core/route").RouteHookParamsAfter) => import("../../core/route").RouteHookNext; + }]; + readonly paths: readonly [`/${string}`] | AnyTuple<`/${string}`>; + readonly preflightSteps: readonly []; + readonly bodyController: null; + readonly steps: readonly [import("../../core/steps").HandlerStep<{ + readonly responseContract: [NoInfer>>, NoInfer>>]; + theFunction(floor: {}, param: import("../../core/steps").HandlerStepFunctionParams | import("../../core/response").PredictedResponse<"304", "resource.notModified", undefined>>): import("@duplojs/utils").MaybePromise | import("../../core/response").PredictedResponse<"304", "resource.notModified", undefined>>; + readonly metadata: readonly []; + }>]; +}>; +export {}; diff --git a/docs/libs/v0/plugins/static/makeRouteFile.mjs b/docs/libs/v0/plugins/static/makeRouteFile.mjs new file mode 100644 index 0000000..dd6226d --- /dev/null +++ b/docs/libs/v0/plugins/static/makeRouteFile.mjs @@ -0,0 +1,58 @@ +import { SDP } from '@duplojs/server-utils'; +import { kindHeritage, A, E, unwrap } from '@duplojs/utils'; +import '../../core/builders/index.mjs'; +import '../../core/metadata/index.mjs'; +import '../../core/response/index.mjs'; +import { createCacheControllerHooks } from '../cacheController/hooks.mjs'; +import { createStaticPluginKind } from './kind.mjs'; +import { useRouteBuilder } from '../../core/builders/route/builder.mjs'; +import { IgnoreByRouteStoreMetadata } from '../../core/metadata/ignoreByRouteStore.mjs'; +import { ResponseContract } from '../../core/response/contract.mjs'; + +class MissingSelectedStaticFileError extends kindHeritage("missing-selected-static-file", createStaticPluginKind("missing-selected-static-file"), Error) { + source; + constructor(source) { + super({}, [`Missing selected static file: ${source.path}.`]); + this.source = source; + } +} +class SelectedStaticFileIsNotFileError extends kindHeritage("selected-static-file-is-not-file", createStaticPluginKind("selected-static-file-is-not-file"), Error) { + source; + constructor(source) { + super({}, [`Selected static file is not file: ${source.path}.`]); + this.source = source; + } +} +function makeRouteFile(params) { + const localPath = A.coalescing(params.path); + return useRouteBuilder("GET", localPath, { + metadata: [IgnoreByRouteStoreMetadata()], + hooks: [createCacheControllerHooks(params.cacheControlConfig)], + }) + .handler([ + ResponseContract.ok("resource.found", SDP.file()), + ResponseContract.notModified("resource.notModified"), + ], async (__, { response, request }) => { + const sourceStatResult = await params.source.stat(); + if (E.isLeft(sourceStatResult)) { + throw new MissingSelectedStaticFileError(params.source); + } + const resourceStat = unwrap(sourceStatResult); + if (!resourceStat.isFile) { + throw new SelectedStaticFileIsNotFileError(params.source); + } + if (request.headers["if-modified-since"] + && typeof request.headers["if-modified-since"] === "string" + && resourceStat.modifiedAt + && new Date(request.headers["if-modified-since"]).getTime() >= resourceStat.modifiedAt.getTime()) { + return response("resource.notModified") + .setHeader("last-modified", resourceStat.modifiedAt.toISOString()); + } + return resourceStat.modifiedAt + ? response("resource.found", params.source) + .setHeader("last-modified", resourceStat.modifiedAt.toISOString()) + : response("resource.found", params.source); + }); +} + +export { MissingSelectedStaticFileError, SelectedStaticFileIsNotFileError, makeRouteFile }; diff --git a/docs/libs/v0/plugins/static/makeRouteFolder.cjs b/docs/libs/v0/plugins/static/makeRouteFolder.cjs new file mode 100644 index 0000000..1751ab6 --- /dev/null +++ b/docs/libs/v0/plugins/static/makeRouteFolder.cjs @@ -0,0 +1,67 @@ +'use strict'; + +var serverUtils = require('@duplojs/server-utils'); +var utils = require('@duplojs/utils'); +require('../../core/builders/index.cjs'); +require('../../core/metadata/index.cjs'); +require('../../core/response/index.cjs'); +var hooks = require('../cacheController/hooks.cjs'); +var builder = require('../../core/builders/route/builder.cjs'); +var ignoreByRouteStore = require('../../core/metadata/ignoreByRouteStore.cjs'); +var contract = require('../../core/response/contract.cjs'); + +function makeRouteFolder(params) { + const sourcePath = utils.whenNot(params.source.path, utils.S.endsWith("/"), utils.S.concat("/")); + const localPrefix = utils.A.coalescing(params.prefix); + const routePath = utils.A.mapTuple(localPrefix, (prefix) => `${prefix}/*`); + const prefixRegex = utils.pipe(localPrefix, utils.A.map(utils.escapeRegExp), utils.A.join("|"), (value) => new RegExp(`^(?:${value})(?:/|$)`)); + const getResourcePath = utils.innerPipe(utils.S.replace(prefixRegex, ""), utils.S.prepend(sourcePath), utils.S.replace(/\/+$/, "")); + return builder.useRouteBuilder("GET", routePath, { + metadata: [ignoreByRouteStore.IgnoreByRouteStoreMetadata()], + hooks: [hooks.createCacheControllerHooks(params.cacheControlConfig)], + }) + .handler([ + contract.ResponseContract.ok("resource.found", serverUtils.SDP.file()), + contract.ResponseContract.notFound("resource.notfound"), + contract.ResponseContract.notModified("resource.notModified"), + ], async (__, { request, response }) => { + if (!utils.Path.isAbsolute(request.path)) { + return response("resource.notfound"); + } + const resourcePath = getResourcePath(request.path); + const resultStat = await serverUtils.SF.stat(resourcePath); + if (utils.E.isLeft(resultStat)) { + return response("resource.notfound"); + } + const stat = utils.unwrap(resultStat); + if (stat.isDirectory && !params.directoryFallBackFile) { + return response("resource.notfound"); + } + const resource = serverUtils.SF.createFileInterface(stat.isDirectory && params.directoryFallBackFile + ? `${resourcePath}/${params.directoryFallBackFile}` + : resourcePath); + const resultResourceStat = stat.isFile + ? stat + : await resource.stat(); + if (utils.E.isLeft(resultResourceStat)) { + return response("resource.notfound"); + } + const resourceStat = utils.unwrap(resultResourceStat); + if (!resourceStat.isFile) { + return response("resource.notfound"); + } + if (request.headers["if-modified-since"] + && typeof request.headers["if-modified-since"] === "string" + && resourceStat.modifiedAt + && new Date(request.headers["if-modified-since"]).getTime() >= resourceStat.modifiedAt.getTime()) { + return response("resource.notModified") + .setHeader("last-modified", resourceStat.modifiedAt.toISOString()); + } + return resourceStat.modifiedAt + ? response("resource.found", resource) + .setHeader("last-modified", resourceStat.modifiedAt.toISOString()) + : response("resource.found", resource); + }); +} + +exports.makeRouteFolder = makeRouteFolder; diff --git a/docs/libs/v0/plugins/static/makeRouteFolder.d.ts b/docs/libs/v0/plugins/static/makeRouteFolder.d.ts new file mode 100644 index 0000000..cedd671 --- /dev/null +++ b/docs/libs/v0/plugins/static/makeRouteFolder.d.ts @@ -0,0 +1,39 @@ +import { SDP, SF } from "@duplojs/server-utils"; +import { type AnyTuple } from "@duplojs/utils"; +import { ResponseContract } from "../../core/response"; +import type { RoutePath } from "../../core/route"; +import { type CacheControlDirectives } from "../cacheController/types"; +interface MakeRouteFolderParams { + readonly source: SF.FolderInterface; + readonly prefix: RoutePath | AnyTuple; + readonly cacheControlConfig?: CacheControlDirectives; + readonly directoryFallBackFile?: string; +} +export declare function makeRouteFolder(params: MakeRouteFolderParams): import("../../core/route").Route<{ + readonly method: "GET"; + readonly metadata: readonly [import("../../core/metadata").Metadata<"ignore-by-route-store", unknown>]; + readonly hooks: readonly [{ + readonly beforeSendResponse: ({ currentResponse, next }: import("../../core/route").RouteHookParamsAfter) => import("../../core/route").RouteHookNext; + }]; + readonly paths: [`/${string}/*`] | [`/${string}/*`, ...`/${string}/*`[]]; + readonly preflightSteps: readonly []; + readonly bodyController: null; + readonly steps: readonly [import("../../core/steps").HandlerStep<{ + readonly responseContract: [NoInfer>>, NoInfer>>, NoInfer>>]; + theFunction(floor: {}, param: import("../../core/steps").HandlerStepFunctionParams | import("../../core/response").PredictedResponse<"404", "resource.notfound", undefined> | import("../../core/response").PredictedResponse<"304", "resource.notModified", undefined>>): import("@duplojs/utils").MaybePromise | import("../../core/response").PredictedResponse<"404", "resource.notfound", undefined> | import("../../core/response").PredictedResponse<"304", "resource.notModified", undefined>>; + readonly metadata: readonly []; + }>]; +}>; +export {}; diff --git a/docs/libs/v0/plugins/static/makeRouteFolder.mjs b/docs/libs/v0/plugins/static/makeRouteFolder.mjs new file mode 100644 index 0000000..97ae4c0 --- /dev/null +++ b/docs/libs/v0/plugins/static/makeRouteFolder.mjs @@ -0,0 +1,65 @@ +import { SDP, SF } from '@duplojs/server-utils'; +import { whenNot, S, A, pipe, escapeRegExp, innerPipe, Path, E, unwrap } from '@duplojs/utils'; +import '../../core/builders/index.mjs'; +import '../../core/metadata/index.mjs'; +import '../../core/response/index.mjs'; +import { createCacheControllerHooks } from '../cacheController/hooks.mjs'; +import { useRouteBuilder } from '../../core/builders/route/builder.mjs'; +import { IgnoreByRouteStoreMetadata } from '../../core/metadata/ignoreByRouteStore.mjs'; +import { ResponseContract } from '../../core/response/contract.mjs'; + +function makeRouteFolder(params) { + const sourcePath = whenNot(params.source.path, S.endsWith("/"), S.concat("/")); + const localPrefix = A.coalescing(params.prefix); + const routePath = A.mapTuple(localPrefix, (prefix) => `${prefix}/*`); + const prefixRegex = pipe(localPrefix, A.map(escapeRegExp), A.join("|"), (value) => new RegExp(`^(?:${value})(?:/|$)`)); + const getResourcePath = innerPipe(S.replace(prefixRegex, ""), S.prepend(sourcePath), S.replace(/\/+$/, "")); + return useRouteBuilder("GET", routePath, { + metadata: [IgnoreByRouteStoreMetadata()], + hooks: [createCacheControllerHooks(params.cacheControlConfig)], + }) + .handler([ + ResponseContract.ok("resource.found", SDP.file()), + ResponseContract.notFound("resource.notfound"), + ResponseContract.notModified("resource.notModified"), + ], async (__, { request, response }) => { + if (!Path.isAbsolute(request.path)) { + return response("resource.notfound"); + } + const resourcePath = getResourcePath(request.path); + const resultStat = await SF.stat(resourcePath); + if (E.isLeft(resultStat)) { + return response("resource.notfound"); + } + const stat = unwrap(resultStat); + if (stat.isDirectory && !params.directoryFallBackFile) { + return response("resource.notfound"); + } + const resource = SF.createFileInterface(stat.isDirectory && params.directoryFallBackFile + ? `${resourcePath}/${params.directoryFallBackFile}` + : resourcePath); + const resultResourceStat = stat.isFile + ? stat + : await resource.stat(); + if (E.isLeft(resultResourceStat)) { + return response("resource.notfound"); + } + const resourceStat = unwrap(resultResourceStat); + if (!resourceStat.isFile) { + return response("resource.notfound"); + } + if (request.headers["if-modified-since"] + && typeof request.headers["if-modified-since"] === "string" + && resourceStat.modifiedAt + && new Date(request.headers["if-modified-since"]).getTime() >= resourceStat.modifiedAt.getTime()) { + return response("resource.notModified") + .setHeader("last-modified", resourceStat.modifiedAt.toISOString()); + } + return resourceStat.modifiedAt + ? response("resource.found", resource) + .setHeader("last-modified", resourceStat.modifiedAt.toISOString()) + : response("resource.found", resource); + }); +} + +export { makeRouteFolder }; diff --git a/docs/libs/v0/plugins/static/plugin.cjs b/docs/libs/v0/plugins/static/plugin.cjs new file mode 100644 index 0000000..365c3bf --- /dev/null +++ b/docs/libs/v0/plugins/static/plugin.cjs @@ -0,0 +1,53 @@ +'use strict'; + +var utils = require('@duplojs/utils'); +var serverUtils = require('@duplojs/server-utils'); +var kind = require('./kind.cjs'); +var makeRouteFile = require('./makeRouteFile.cjs'); +var makeRouteFolder = require('./makeRouteFolder.cjs'); + +class StaticPluginError extends utils.kindHeritage("static-plugin", kind.createStaticPluginKind("static-plugin"), Error) { + information; + error; + constructor(information, error) { + super({}, [`Error during registration static route: ${information}`]); + this.information = information; + this.error = error; + } +} +function staticPlugin(...args) { + const route = utils.P.match(args) + .with([utils.toCurriedPredicate(serverUtils.SF.isFolderInterface)], ([source, params]) => makeRouteFolder.makeRouteFolder({ + source, + ...params, + })) + .with([utils.toCurriedPredicate(serverUtils.SF.isFileInterface)], ([source, params]) => makeRouteFile.makeRouteFile({ + source, + ...params, + })) + .exhaustive(); + return { + name: "static", + routes: [route], + hooksHubLifeCycle: [ + { + beforeStartServer: async () => { + const statResult = await args[0].stat(); + if (utils.E.isLeft(statResult)) { + throw new StaticPluginError(`source does not exit (${args[0].path}).`, utils.unwrap(statResult)); + } + const stat = utils.unwrap(statResult); + if (serverUtils.SF.isFileInterface(args[0]) && !stat.isFile) { + throw new StaticPluginError(`source does not file (${args[0].path}).`, stat); + } + else if (serverUtils.SF.isFolderInterface(args[0]) && stat.isFile) { + throw new StaticPluginError(`source does not folder (${args[0].path}).`, stat); + } + }, + }, + ], + }; +} + +exports.StaticPluginError = StaticPluginError; +exports.staticPlugin = staticPlugin; diff --git a/docs/libs/v0/plugins/static/plugin.d.ts b/docs/libs/v0/plugins/static/plugin.d.ts new file mode 100644 index 0000000..cc010a9 --- /dev/null +++ b/docs/libs/v0/plugins/static/plugin.d.ts @@ -0,0 +1,26 @@ +import { type AnyTuple } from "@duplojs/utils"; +import { SF } from "@duplojs/server-utils"; +import type { HubPlugin } from "../../core/hub"; +import type { RoutePath } from "../../core/route"; +import { type CacheControlDirectives } from "../cacheController/types"; +export interface BaseStaticPluginParams { + readonly cacheControlConfig?: CacheControlDirectives; +} +export interface StaticPluginFileParams extends BaseStaticPluginParams { + readonly path: RoutePath | AnyTuple; +} +export interface StaticPluginFolderParams extends BaseStaticPluginParams { + readonly prefix: RoutePath | AnyTuple; + readonly directoryFallBackFile?: string; +} +declare const StaticPluginError_base: new (params: { + "@DuplojsStaticPlugin/static-plugin"?: unknown; +}, parentParams: readonly [message?: string | undefined, options?: ErrorOptions | undefined]) => Error & import("@duplojs/utils").Kind, unknown> & import("@duplojs/utils").Kind, unknown>; +export declare class StaticPluginError extends StaticPluginError_base { + information: string; + error: unknown; + constructor(information: string, error: unknown); +} +export declare function staticPlugin(source: SF.FolderInterface, params: StaticPluginFolderParams): HubPlugin; +export declare function staticPlugin(source: SF.FileInterface, params: StaticPluginFileParams): HubPlugin; +export {}; diff --git a/docs/libs/v0/plugins/static/plugin.mjs b/docs/libs/v0/plugins/static/plugin.mjs new file mode 100644 index 0000000..ee0deb3 --- /dev/null +++ b/docs/libs/v0/plugins/static/plugin.mjs @@ -0,0 +1,50 @@ +import { kindHeritage, P, toCurriedPredicate, E, unwrap } from '@duplojs/utils'; +import { SF } from '@duplojs/server-utils'; +import { createStaticPluginKind } from './kind.mjs'; +import { makeRouteFile } from './makeRouteFile.mjs'; +import { makeRouteFolder } from './makeRouteFolder.mjs'; + +class StaticPluginError extends kindHeritage("static-plugin", createStaticPluginKind("static-plugin"), Error) { + information; + error; + constructor(information, error) { + super({}, [`Error during registration static route: ${information}`]); + this.information = information; + this.error = error; + } +} +function staticPlugin(...args) { + const route = P.match(args) + .with([toCurriedPredicate(SF.isFolderInterface)], ([source, params]) => makeRouteFolder({ + source, + ...params, + })) + .with([toCurriedPredicate(SF.isFileInterface)], ([source, params]) => makeRouteFile({ + source, + ...params, + })) + .exhaustive(); + return { + name: "static", + routes: [route], + hooksHubLifeCycle: [ + { + beforeStartServer: async () => { + const statResult = await args[0].stat(); + if (E.isLeft(statResult)) { + throw new StaticPluginError(`source does not exit (${args[0].path}).`, unwrap(statResult)); + } + const stat = unwrap(statResult); + if (SF.isFileInterface(args[0]) && !stat.isFile) { + throw new StaticPluginError(`source does not file (${args[0].path}).`, stat); + } + else if (SF.isFolderInterface(args[0]) && stat.isFile) { + throw new StaticPluginError(`source does not folder (${args[0].path}).`, stat); + } + }, + }, + ], + }; +} + +export { StaticPluginError, staticPlugin }; diff --git a/docs/tsconfig.v0.json b/docs/tsconfig.v0.json index 3110b5b..e0b650c 100644 --- a/docs/tsconfig.v0.json +++ b/docs/tsconfig.v0.json @@ -8,8 +8,11 @@ "@duplojs/http/node": ["libs/v0/interfaces/node/index"], "@duplojs/http/codeGenerator": ["libs/v0/plugins/codeGenerator/index"], "@duplojs/http/openApiGenerator": ["libs/v0/plugins/openApiGenerator/index"], + "@duplojs/http/static": ["libs/v0/plugins/static/index"], + "@duplojs/http/cors": ["libs/v0/plugins/cors/index"], + "@duplojs/http/cacheController": ["libs/v0/plugins/cacheController/index"], }, - "types": ["@types/web"] + "types": ["web"] }, "include": ["examples/v0/**/*.ts", "libs/v0/**/*.d.ts"], } diff --git a/integration/.commands/test-types.sh b/integration/.commands/test-types.sh index b91078c..c2cff9d 100755 --- a/integration/.commands/test-types.sh +++ b/integration/.commands/test-types.sh @@ -2,10 +2,15 @@ set -e +# core +tsc -p core/tsconfig.json +# client +tsc -p client/tsconfig.json +# interfaces +tsc -p node/tsconfig.json +# plugins tsc -p codeGenerator/tsconfig.json tsc -p openApiGenerator/tsconfig.json -tsc -p core/tsconfig.json -tsc -p node/tsconfig.json -tsc -p client/tsconfig.json tsc -p static/tsconfig.json -tsc -p cacheController/tsconfig.json \ No newline at end of file +tsc -p cacheController/tsconfig.json +tsc -p cors/tsconfig.json \ No newline at end of file diff --git a/integration/codeGenerator/__snapshots__/index.test.ts.snap b/integration/codeGenerator/__snapshots__/index.test.ts.snap index eff535c..6c65805 100644 --- a/integration/codeGenerator/__snapshots__/index.test.ts.snap +++ b/integration/codeGenerator/__snapshots__/index.test.ts.snap @@ -55,6 +55,21 @@ export type Routes = { age: number; }; }; +} | { + method: "DELETE"; + path: "/users"; + body: { + id: number; + }; + responses: { + code: "422"; + information: "extract-error"; + body?: undefined; + } | { + code: "204"; + information: "users.deleted"; + body?: undefined; + }; } | { method: "POST"; path: "/documents"; diff --git a/integration/core/index.ts b/integration/core/index.ts index 241e475..ef56f72 100644 --- a/integration/core/index.ts +++ b/integration/core/index.ts @@ -1,7 +1,9 @@ +import { asserts, E, Path } from "@duplojs/utils"; import { setCurrentWorkingDirectory, SF } from "@duplojs/server-utils"; import { createHub, routeStore } from "@duplojs/http"; import { staticPlugin } from "@duplojs/http/static"; -import { asserts, E, Path } from "@duplojs/utils"; +import { corsPlugin } from "@duplojs/http/cors"; + import "./routes"; const sourceFile = SF.createFileInterface("files/fakeFiles/superTextFile.txt"); @@ -18,5 +20,18 @@ export const hub = createHub({ environment: "DEV" }) staticPlugin(sourceFile, { path: "/static-file" }), ) .plug( - staticPlugin(sourceFolder, { prefix: "/static-folder" }), + staticPlugin(sourceFolder, { + prefix: "/static-folder", + directoryFallBackFile: "1mb.jpg", + }), + ) + .plug( + corsPlugin({ + allowOrigin: "localhost", + allowHeaders: ["content-type", "accept"], + allowMethods: true, + credentials: true, + exposeHeaders: ["info"], + maxAge: 0, + }), ); diff --git a/integration/core/routes/users.ts b/integration/core/routes/users.ts index 4923197..ea8b045 100644 --- a/integration/core/routes/users.ts +++ b/integration/core/routes/users.ts @@ -51,3 +51,14 @@ useRouteBuilder("POST", "/users") ResponseContract.ok("users.create", user), (floor, { response }) => response("users.create", floor.body), ); + +useRouteBuilder("DELETE", "/users") + .extract({ + body: { + id: DPE.coerce.number(), + }, + }) + .handler( + ResponseContract.noContent("users.deleted"), + (__, { response }) => response("users.deleted"), + ); diff --git a/integration/cors/index.test.ts b/integration/cors/index.test.ts new file mode 100644 index 0000000..ea6b5d9 --- /dev/null +++ b/integration/cors/index.test.ts @@ -0,0 +1,145 @@ +import { hub } from "@core"; +import { createHttpServer } from "@duplojs/http/node"; + +describe("corsPlugin", async() => { + const server = await createHttpServer(hub, { + host: "0.0.0.0", + port: 8949, + }); + + afterAll(() => { + server.close(); + }); + + it("simple request", async() => { + await expect( + fetch("http://localhost:8949/users", { + method: "GET", + headers: { + Origin: "localhost", + }, + }) + .then(async(response) => ({ + body: await response.json(), + headers: [...response.headers.entries()], + })), + ).resolves.toStrictEqual({ + body: [ + { + age: 28, + id: 23, + name: "", + }, + ], + headers: expect.arrayContaining([ + [ + "access-control-allow-credentials", + "true", + ], + [ + "access-control-allow-origin", + "localhost", + ], + [ + "access-control-expose-headers", + "info", + ], + [ + "vary", + "Origin", + ], + [ + "information", + "users.findMany", + ], + ]), + }); + }); + + it("preflight request", async() => { + await expect( + fetch("http://localhost:8949/users", { + method: "OPTIONS", + headers: { + Origin: "localhost", + }, + }) + .then(async(response) => ({ + body: await response.text(), + headers: [...response.headers.entries()], + status: response.status, + })), + ).resolves.toStrictEqual({ + body: "", + status: 204, + headers: expect.arrayContaining([ + [ + "access-control-allow-headers", + "content-type,accept", + ], + [ + "access-control-allow-methods", + "GET,POST,DELETE", + ], + ]), + }); + }); + + it("cors flow", async() => { + const preflightResponse = await fetch("http://localhost:8949/users", { + method: "OPTIONS", + headers: { + Origin: "localhost", + }, + }); + + expect(preflightResponse.status).toBe(204); + expect(await preflightResponse.text()).toBe(""); + expect([...preflightResponse.headers.entries()]).toEqual( + expect.arrayContaining([ + [ + "access-control-allow-headers", + "content-type,accept", + ], + ]), + ); + + const deleteResponse = await fetch("http://localhost:8949/users", { + method: "DELETE", + headers: { + "Content-Type": "application/json", + Origin: "localhost", + }, + body: JSON.stringify({ + id: 23, + }), + }); + + expect(deleteResponse.status).toBe(204); + expect(await deleteResponse.text()).toBe(""); + expect([...deleteResponse.headers.entries()]).toEqual( + expect.arrayContaining([ + [ + "access-control-allow-credentials", + "true", + ], + [ + "access-control-allow-origin", + "localhost", + ], + [ + "access-control-expose-headers", + "info", + ], + [ + "information", + "users.deleted", + ], + [ + "vary", + "Origin", + ], + ]), + ); + }); +}); diff --git a/integration/cors/tsconfig.json b/integration/cors/tsconfig.json new file mode 100644 index 0000000..70fe73f --- /dev/null +++ b/integration/cors/tsconfig.json @@ -0,0 +1,11 @@ +{ + "extends": "../../tsconfig.test.json", + "compilerOptions": { + "baseUrl": "./", + "paths": { + "@core": ["../core/index.ts"] + }, + "types": ["vitest/globals", "node"] + }, + "include": ["**/*.ts", "../core/**/*.ts"], +} diff --git a/integration/openApiGenerator/__snapshots__/index.test.ts.snap b/integration/openApiGenerator/__snapshots__/index.test.ts.snap index aa4fe5b..562154f 100644 --- a/integration/openApiGenerator/__snapshots__/index.test.ts.snap +++ b/integration/openApiGenerator/__snapshots__/index.test.ts.snap @@ -75,6 +75,43 @@ exports[`openApiGenerator > correct generate file 1`] = ` } } } + }, + "delete": { + "parameters": [], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NotIdentified7" + } + } + } + }, + "responses": { + "204": { + "headers": { + "information": { + "schema": { + "const": "users.deleted", + "type": "string" + }, + "description": "users.deleted" + } + } + }, + "422": { + "headers": { + "information": { + "schema": { + "const": "extract-error", + "type": "string" + }, + "description": "extract-error" + } + } + } + } } }, "/users/{userId}": { @@ -130,7 +167,7 @@ exports[`openApiGenerator > correct generate file 1`] = ` "content": { "multipart/form-data": { "schema": { - "$ref": "#/components/schemas/NotIdentified7" + "$ref": "#/components/schemas/NotIdentified10" } } } @@ -178,7 +215,7 @@ exports[`openApiGenerator > correct generate file 1`] = ` "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/NotIdentified10" + "$ref": "#/components/schemas/NotIdentified13" } } } @@ -194,7 +231,7 @@ exports[`openApiGenerator > correct generate file 1`] = ` "in": "query", "required": false, "schema": { - "$ref": "#/components/schemas/NotIdentified11" + "$ref": "#/components/schemas/NotIdentified14" } } ], @@ -212,7 +249,7 @@ exports[`openApiGenerator > correct generate file 1`] = ` "content": { "text/event-stream": { "itemSchema": { - "$ref": "#/components/schemas/NotIdentified13" + "$ref": "#/components/schemas/NotIdentified16" } } } @@ -259,7 +296,7 @@ exports[`openApiGenerator > correct generate file 1`] = ` "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/NotIdentified15" + "$ref": "#/components/schemas/NotIdentified18" } } } @@ -295,7 +332,7 @@ exports[`openApiGenerator > correct generate file 1`] = ` "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/NotIdentified17" + "$ref": "#/components/schemas/NotIdentified20" } } } @@ -420,6 +457,26 @@ exports[`openApiGenerator > correct generate file 1`] = ` ] }, "NotIdentified7": { + "type": "object", + "properties": { + "id": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "string" + } + ] + } + }, + "required": [ + "id" + ] + }, + "NotIdentified8": {}, + "NotIdentified9": {}, + "NotIdentified10": { "type": "object", "properties": { "bool": { @@ -467,13 +524,13 @@ exports[`openApiGenerator > correct generate file 1`] = ` "name" ] }, - "NotIdentified8": {}, - "NotIdentified9": {}, - "NotIdentified10": { + "NotIdentified11": {}, + "NotIdentified12": {}, + "NotIdentified13": { "type": "string", "format": "binary" }, - "NotIdentified11": { + "NotIdentified14": { "anyOf": [ { "type": "boolean" @@ -496,8 +553,8 @@ exports[`openApiGenerator > correct generate file 1`] = ` } ] }, - "NotIdentified12": {}, - "NotIdentified13": { + "NotIdentified15": {}, + "NotIdentified16": { "type": "object", "properties": { "event": { @@ -542,18 +599,18 @@ exports[`openApiGenerator > correct generate file 1`] = ` "data" ] }, - "NotIdentified14": {}, - "NotIdentified15": { + "NotIdentified17": {}, + "NotIdentified18": { "type": "string", "format": "binary" }, - "NotIdentified16": {}, - "NotIdentified17": { + "NotIdentified19": {}, + "NotIdentified20": { "type": "string", "format": "binary" }, - "NotIdentified18": {}, - "NotIdentified19": {} + "NotIdentified21": {}, + "NotIdentified22": {} } } }" diff --git a/integration/static/index.test.ts b/integration/static/index.test.ts index 6834435..b53ae3b 100644 --- a/integration/static/index.test.ts +++ b/integration/static/index.test.ts @@ -1,5 +1,5 @@ import { type SF, TESTImplementation, setEnvironment } from "@duplojs/server-utils"; -import { E, D } from "@duplojs/utils"; +import { E, D, mimeType } from "@duplojs/utils"; import { createHttpServer } from "@duplojs/http/node"; import { hub } from "@core"; @@ -28,6 +28,14 @@ describe("static plugin", async() => { } as SF.StatInfo); } + if (path === "files/fakeFiles/1mb.jpg") { + return E.success({ + isFile: true, + isDirectory: false, + modifiedAt: mockedModifiedAt, + } as SF.StatInfo); + } + return E.left("file-system-stat"); }); @@ -184,4 +192,36 @@ describe("static plugin", async() => { ]), }); }); + + it("default file in folder", async() => { + await expect( + fetch("http://localhost:8980/static-folder/", { + method: "GET", + }) + .then((response) => ({ + code: response.status, + headers: [...response.headers.entries()], + })), + ).resolves.toStrictEqual({ + code: 200, + headers: expect.arrayContaining([ + [ + "information", + "resource.found", + ], + [ + "last-modified", + mockedModifiedAt.toISOString(), + ], + [ + "content-type", + "image/jpeg", + ], + [ + "content-disposition", + "attachment; filename=\"1mb.jpg\"", + ], + ]), + }); + }); }); diff --git a/package-lock.json b/package-lock.json index 468c917..3e7201a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -43,7 +43,7 @@ "peerDependencies": { "@duplojs/data-parser-tools": ">=0.2.9 <1.0.0", "@duplojs/server-utils": ">=0.2.2 <1.0.0", - "@duplojs/utils": ">=1.5.13 <2.0.0" + "@duplojs/utils": ">=1.5.15 <2.0.0" } }, "docs": { @@ -1527,9 +1527,9 @@ } }, "node_modules/@duplojs/utils": { - "version": "1.5.13", - "resolved": "https://registry.npmjs.org/@duplojs/utils/-/utils-1.5.13.tgz", - "integrity": "sha512-N4GbxGwQVhMbTu5UsMkgOrCrhRc7biWH8/HSX6w3SDS5NkVz2Rl235d7GZyaJ7NmJfFFm28LW88WM+Ub4a74hA==", + "version": "1.5.15", + "resolved": "https://registry.npmjs.org/@duplojs/utils/-/utils-1.5.15.tgz", + "integrity": "sha512-bhM//4vTj8NkSD8iuNskH2XJny648jAfh1uXf/Wqy3R5uzAkVcEGpXbVQa5Z6AJzmqWpXckFaMHsZHM4+2e2YA==", "license": "MIT", "peer": true, "workspaces": [ diff --git a/package.json b/package.json index e23c23c..286e307 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,10 @@ { "name": "@duplojs/http", "version": "0.0.0", - "author": "mathcovax", + "author": { + "name": "mathcovax", + "url": "https://github.com/mathcovax" + }, "license": "MIT", "type": "module", "repository": { @@ -68,6 +71,11 @@ "import": "./dist/plugins/static/index.mjs", "require": "./dist/plugins/static/index.cjs", "types": "./dist/plugins/static/index.d.ts" + }, + "./cors": { + "import": "./dist/plugins/cors/index.mjs", + "require": "./dist/plugins/cors/index.cjs", + "types": "./dist/plugins/cors/index.d.ts" } }, "files": [ @@ -77,7 +85,7 @@ "peerDependencies": { "@duplojs/data-parser-tools": ">=0.2.9 <1.0.0", "@duplojs/server-utils": ">=0.2.2 <1.0.0", - "@duplojs/utils": ">=1.5.13 <2.0.0" + "@duplojs/utils": ">=1.5.15 <2.0.0" }, "devDependencies": { "@commitlint/cli": "19.8.1", @@ -108,8 +116,25 @@ "integration", "docs" ], - "keywords": [], "engines": { "node": ">=22.15.1" - } + }, + "keywords": [ + "duplojs", + "http", + "client", + "sse", + "static", + "codeGenerator", + "openApiGenerator", + "cors", + "cross-platform" + ], + "homepage": "https://http.duplojs.dev/", + "contributors": [ + { + "name": "ZeRiix", + "url": "https://github.com/ZeRiix" + } + ] } diff --git a/rollup.config.js b/rollup.config.js index 4b8cca6..98095cf 100644 --- a/rollup.config.js +++ b/rollup.config.js @@ -105,6 +105,31 @@ export default defineConfig([ tscAlias({ configFile: "scripts/plugins/cacheController/tsconfig.build.json" }), ], }, + { + input: "scripts/plugins/cors/index.ts", + output: [ + { + dir: "dist", + format: "esm", + preserveModules: true, + preserveModulesRoot: "scripts", + entryFileNames: "[name].mjs" + }, + { + dir: "dist", + format: "cjs", + preserveModules: true, + preserveModulesRoot: "scripts", + entryFileNames: "[name].cjs" + }, + ], + treeshake: false, + plugins: [ + del({ targets: "dist/plugins/cors" }), + typescript({ tsconfig: "scripts/plugins/cors/tsconfig.build.json" }), + tscAlias({ configFile: "scripts/plugins/cors/tsconfig.build.json" }), + ], + }, // interfaces { diff --git a/scripts/core/functionsBuilders/router/create.ts b/scripts/core/functionsBuilders/router/create.ts index 4b76e7d..9327be1 100644 --- a/scripts/core/functionsBuilders/router/create.ts +++ b/scripts/core/functionsBuilders/router/create.ts @@ -3,7 +3,7 @@ import { type BuildedRouter } from "@core/router"; import { type RouterElementSystem } from "@core/router/types/routerElementSystem"; import { type RouterElementWrapper } from "@core/router/types/routerElementWrapper"; import { type Environment } from "@core/types"; -import { type MaybePromise } from "bun"; +import { type MaybePromise } from "@duplojs/utils"; export interface RouterFunctionBuilderParams { readonly environment: Environment; diff --git a/scripts/plugins/cacheController/hooks.ts b/scripts/plugins/cacheController/hooks.ts index f0efb99..a9ddc11 100644 --- a/scripts/plugins/cacheController/hooks.ts +++ b/scripts/plugins/cacheController/hooks.ts @@ -17,6 +17,7 @@ export function createCacheControllerHooks( if ( cacheControl && eligibleCodeRegex.test(currentResponse.code) + && currentResponse.headers?.["cache-control"] === undefined ) { currentResponse.setHeader("cache-control", cacheControl); } diff --git a/scripts/plugins/cors/headerFunctions/allowHeaders.ts b/scripts/plugins/cors/headerFunctions/allowHeaders.ts new file mode 100644 index 0000000..df4db05 --- /dev/null +++ b/scripts/plugins/cors/headerFunctions/allowHeaders.ts @@ -0,0 +1,10 @@ +import type { Request } from "@core/request"; +import type { Response } from "@core/response"; + +export const allowHeadersFunction = { + default(allowHeaders: string) { + return (request: Request, response: Response) => { + response.setHeader("access-control-allow-headers", allowHeaders); + }; + }, +}; diff --git a/scripts/plugins/cors/headerFunctions/allowMethods.ts b/scripts/plugins/cors/headerFunctions/allowMethods.ts new file mode 100644 index 0000000..61636b1 --- /dev/null +++ b/scripts/plugins/cors/headerFunctions/allowMethods.ts @@ -0,0 +1,16 @@ +import type { Request } from "@core/request"; +import type { Response } from "@core/response"; + +export const allowMethodsFunction = { + default(methods: string) { + return (request: Request, response: Response) => { + response.setHeader("access-control-allow-methods", methods); + }; + }, + + isBool(allowMethods: Record) { + return (request: Request, response: Response) => { + response.setHeader("access-control-allow-methods", allowMethods[request.path]); + }; + }, +}; diff --git a/scripts/plugins/cors/headerFunctions/allowOrigin.ts b/scripts/plugins/cors/headerFunctions/allowOrigin.ts new file mode 100644 index 0000000..9fc8099 --- /dev/null +++ b/scripts/plugins/cors/headerFunctions/allowOrigin.ts @@ -0,0 +1,26 @@ +import type { Request } from "@core/request"; +import type { Response } from "@core/response"; +import type { MaybePromise } from "@duplojs/utils"; + +export const allowOriginFunction = { + default(allowOrigin: RegExp) { + return (request: Request, response: Response) => { + if (allowOrigin.test(request.origin)) { + response.setHeader("access-control-allow-origin", request.origin); + } + }; + }, + + isFunction(allowOrigin: (origin: string) => MaybePromise) { + return async(request: Request, response: Response) => { + let result = allowOrigin(request.origin); + if (result instanceof Promise) { + result = await result; + } + + if (result === true) { + response.setHeader("access-control-allow-origin", request.origin); + } + }; + }, +}; diff --git a/scripts/plugins/cors/headerFunctions/credentials.ts b/scripts/plugins/cors/headerFunctions/credentials.ts new file mode 100644 index 0000000..65745ef --- /dev/null +++ b/scripts/plugins/cors/headerFunctions/credentials.ts @@ -0,0 +1,10 @@ +import type { Request } from "@core/request"; +import type { Response } from "@core/response"; + +export const credentialsFunction = { + default() { + return (request: Request, response: Response) => { + response.setHeader("access-control-allow-credentials", "true"); + }; + }, +}; diff --git a/scripts/plugins/cors/headerFunctions/exposeHeaders.ts b/scripts/plugins/cors/headerFunctions/exposeHeaders.ts new file mode 100644 index 0000000..dc21940 --- /dev/null +++ b/scripts/plugins/cors/headerFunctions/exposeHeaders.ts @@ -0,0 +1,10 @@ +import type { Request } from "@core/request"; +import type { Response } from "@core/response"; + +export const exposeHeadersFunction = { + default(exposeHeaders: string) { + return (request: Request, response: Response) => { + response.setHeader("access-control-expose-headers", exposeHeaders); + }; + }, +}; diff --git a/scripts/plugins/cors/headerFunctions/index.ts b/scripts/plugins/cors/headerFunctions/index.ts new file mode 100644 index 0000000..5a08d2f --- /dev/null +++ b/scripts/plugins/cors/headerFunctions/index.ts @@ -0,0 +1,7 @@ +export * from "./allowHeaders"; +export * from "./allowMethods"; +export * from "./allowOrigin"; +export * from "./credentials"; +export * from "./exposeHeaders"; +export * from "./maxAge"; +export * from "./vary"; diff --git a/scripts/plugins/cors/headerFunctions/maxAge.ts b/scripts/plugins/cors/headerFunctions/maxAge.ts new file mode 100644 index 0000000..21c4557 --- /dev/null +++ b/scripts/plugins/cors/headerFunctions/maxAge.ts @@ -0,0 +1,10 @@ +import type { Request } from "@core/request"; +import type { Response } from "@core/response"; + +export const maxAgeFunction = { + default(maxAge: string) { + return (request: Request, response: Response) => { + response.setHeader("access-control-max-age", maxAge); + }; + }, +}; diff --git a/scripts/plugins/cors/headerFunctions/vary.ts b/scripts/plugins/cors/headerFunctions/vary.ts new file mode 100644 index 0000000..f93f9cf --- /dev/null +++ b/scripts/plugins/cors/headerFunctions/vary.ts @@ -0,0 +1,39 @@ +import type { Request } from "@core/request"; +import type { Response } from "@core/response"; + +const starRegex = /(^|,) *\* *(?=,|$)/; +const originRegex = /(^|,) *origin *(?=,|$)/i; + +export const varyFunction = { + default() { + const maxStoreSize = 500; + const store = new Map(); + + return (request: Request, response: Response) => { + const cachedVary = store.get(request.origin); + + if (cachedVary) { + response.setHeader("vary", cachedVary); + return; + } + + let varyValue = Array.isArray(response.headers?.vary) + ? response.headers.vary.join(", ") + : response.headers?.vary; + + if (varyValue === undefined) { + varyValue = "Origin"; + } else if (starRegex.test(varyValue)) { + varyValue = "*"; + } else if (!originRegex.test(varyValue)) { + varyValue = `${varyValue}, Origin`; + } + + if (store.size < maxStoreSize) { + store.set(request.origin, varyValue); + } + + response.setHeader("vary", varyValue); + }; + }, +}; diff --git a/scripts/plugins/cors/index.ts b/scripts/plugins/cors/index.ts new file mode 100644 index 0000000..f360fc6 --- /dev/null +++ b/scripts/plugins/cors/index.ts @@ -0,0 +1,2 @@ +export * from "./metadata"; +export * from "./plugin"; diff --git a/scripts/plugins/cors/metadata.ts b/scripts/plugins/cors/metadata.ts new file mode 100644 index 0000000..e0c9805 --- /dev/null +++ b/scripts/plugins/cors/metadata.ts @@ -0,0 +1,3 @@ +import { createMetadata } from "@core/metadata"; + +export const IgnoreRouteCorsMetadata = createMetadata("ignore-by-cors"); diff --git a/scripts/plugins/cors/plugin.ts b/scripts/plugins/cors/plugin.ts new file mode 100644 index 0000000..b3ec736 --- /dev/null +++ b/scripts/plugins/cors/plugin.ts @@ -0,0 +1,172 @@ +/* eslint-disable @typescript-eslint/prefer-for-of */ +import { type AnyTuple, type MaybePromise, G, pipe, toRegExp, A, type O } from "@duplojs/utils"; +import { type RequestMethods, type Request } from "@core/request"; +import type { Response } from "@core/response"; +import { type HubPlugin } from "@core/hub"; +import { IgnoreRouteCorsMetadata } from "./metadata"; +import { createHookRouteLifeCycle, createRoute } from "@core/route"; +import { allowHeadersFunction, allowMethodsFunction, allowOriginFunction, credentialsFunction, exposeHeadersFunction, maxAgeFunction, varyFunction } from "./headerFunctions"; + +export interface CorsPluginParams { + readonly allowOrigin?: string | RegExp | AnyTuple | ((origin: string) => MaybePromise) | true; + readonly allowHeaders?: string | AnyTuple | true; + readonly exposeHeaders?: string | AnyTuple; + readonly maxAge?: number; + readonly credentials?: boolean; + readonly allowMethods?: RequestMethods | AnyTuple | true; +} + +export function corsPlugin< + GenericParams extends CorsPluginParams, +>(params: GenericParams & O.RequireAtLeastOne) { + const headerFunctionOtherRoutes: ((request: Request, response: Response) => void)[] = []; + + if (params.allowOrigin) { + headerFunctionOtherRoutes.push( + varyFunction.default(), + ); + + headerFunctionOtherRoutes.push( + typeof params.allowOrigin === "function" + ? allowOriginFunction.isFunction(params.allowOrigin) + : allowOriginFunction.default( + toRegExp( + params.allowOrigin === true + ? "*" + : params.allowOrigin, + ), + ), + ); + } + + if (params.exposeHeaders) { + headerFunctionOtherRoutes.push( + pipe( + params.exposeHeaders, + A.coalescing, + A.join(","), + exposeHeadersFunction.default, + ), + ); + } + + if (params.credentials) { + headerFunctionOtherRoutes.push( + credentialsFunction.default(), + ); + } + + const hookOtherRoute = createHookRouteLifeCycle({ + beforeSendResponse: (params) => { + for (let index = 0; index < headerFunctionOtherRoutes.length; index++) { + headerFunctionOtherRoutes[index]!(params.request, params.currentResponse); + } + return params.next(); + }, + }); + + return (): HubPlugin => ({ + name: "cors", + hooksHubLifeCycle: [ + { + beforeServerBuildRoutes: (hub) => { + const headerFunctionRouteOptions: ((request: Request, response: Response) => void)[] = []; + + if (params.allowMethods === true) { + const allowMethodsFunctionIsBool = pipe( + hub.routes, + G.filter((route) => !A.some(route.definition.metadata, IgnoreRouteCorsMetadata.is)), + G.map( + (route) => A.map( + route.definition.paths, + (path) => ({ + path, + method: route.definition.method, + }), + ), + ), + G.flat, + G.reduce( + G.reduceFrom>({}), + ({ element, lastValue, next }) => { + lastValue[element.path] = lastValue[element.path] + ? `${lastValue[element.path]},${element.method}` + : element.method; + return next(lastValue); + }, + ), + allowMethodsFunction.isBool, + ); + + headerFunctionRouteOptions.push(allowMethodsFunctionIsBool); + } else if (params.allowMethods) { + headerFunctionRouteOptions.push( + pipe( + params.allowMethods, + A.coalescing, + A.join(","), + allowMethodsFunction.default, + ), + ); + } + + if (params.allowHeaders) { + headerFunctionRouteOptions.push( + allowHeadersFunction.default( + params.allowHeaders === true + ? "*" + : pipe( + params.allowHeaders, + A.coalescing, + A.join(","), + ), + ), + ); + } + + if (params.maxAge) { + headerFunctionRouteOptions.push( + maxAgeFunction.default(params.maxAge.toString()), + ); + } + + const hookRouteOptions = createHookRouteLifeCycle({ + beforeRouteExecution: (params) => { + const response = params.response("204", "cors"); + for (let index = 0; index < headerFunctionRouteOptions.length; index++) { + headerFunctionRouteOptions[index]!(params.request, response); + } + return response; + }, + }); + + const routeOptions = createRoute({ + paths: ["/*"], + method: "OPTIONS", + hooks: [hookRouteOptions], + metadata: [IgnoreRouteCorsMetadata()], + steps: [], + preflightSteps: [], + bodyController: null, + }); + + hub.register(routeOptions); + + return hub; + }, + beforeBuildRoute: (route) => { + if (route.definition.method === "OPTIONS" || A.some(route.definition.metadata, IgnoreRouteCorsMetadata.is)) { + return route; + } + return { + ...route, + definition: { + ...route.definition, + hooks: [...route.definition.hooks, hookOtherRoute], + }, + }; + }, + }, + ], + }); +} diff --git a/scripts/plugins/cors/tsconfig.build.json b/scripts/plugins/cors/tsconfig.build.json new file mode 100644 index 0000000..97d62f0 --- /dev/null +++ b/scripts/plugins/cors/tsconfig.build.json @@ -0,0 +1,10 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../../dist", + "noEmit": false, + "declaration": true, + "declarationDir": "../../../dist", + "types": null, + }, +} \ No newline at end of file diff --git a/scripts/plugins/cors/tsconfig.json b/scripts/plugins/cors/tsconfig.json new file mode 100644 index 0000000..90362e5 --- /dev/null +++ b/scripts/plugins/cors/tsconfig.json @@ -0,0 +1,12 @@ +{ + "extends": "../../../tsconfig.app.json", + "compilerOptions": { + "baseUrl": "./", + "paths": { + "@core/*": ["../../core/*"], + "@plugin-cors/*": ["./*"], + }, + "types": ["node"], + }, + "include": ["**/*.ts", "../../core/**/*.ts"], +} diff --git a/scripts/plugins/static/makeRouteFolder.ts b/scripts/plugins/static/makeRouteFolder.ts index b2d3b44..6c44b80 100644 --- a/scripts/plugins/static/makeRouteFolder.ts +++ b/scripts/plugins/static/makeRouteFolder.ts @@ -39,6 +39,7 @@ export function makeRouteFolder(params: MakeRouteFolderParams) { const getResourcePath = innerPipe( S.replace(prefixRegex, ""), S.prepend(sourcePath), + S.replace(/\/+$/, ""), ); return useRouteBuilder( diff --git a/scripts/plugins/static/plugin.ts b/scripts/plugins/static/plugin.ts index 8c8b6b7..fc5c1f7 100644 --- a/scripts/plugins/static/plugin.ts +++ b/scripts/plugins/static/plugin.ts @@ -19,7 +19,7 @@ export interface StaticPluginFileParams extends BaseStaticPluginParams { export interface StaticPluginFolderParams extends BaseStaticPluginParams { readonly prefix: RoutePath | AnyTuple; - readonly directoryIndexFilePrefix?: string; + readonly directoryFallBackFile?: string; } export class StaticPluginError extends kindHeritage( diff --git a/tests/plugins/cors/headerFunctions/allowHeaders.test.ts b/tests/plugins/cors/headerFunctions/allowHeaders.test.ts new file mode 100644 index 0000000..b88b2d0 --- /dev/null +++ b/tests/plugins/cors/headerFunctions/allowHeaders.test.ts @@ -0,0 +1,12 @@ +import { Response } from "@core/response"; +import { allowHeadersFunction } from "@plugin-cors/headerFunctions"; + +describe("allowHeadersFunction", () => { + it("expect good", () => { + const response = new Response("204", "cors", undefined); + + allowHeadersFunction.default("Authorization,ETag")(undefined as never, response); + + expect(response.headers!["access-control-allow-headers"]).toStrictEqual("Authorization,ETag"); + }); +}); diff --git a/tests/plugins/cors/headerFunctions/allowMethods.test.ts b/tests/plugins/cors/headerFunctions/allowMethods.test.ts new file mode 100644 index 0000000..4e2ba28 --- /dev/null +++ b/tests/plugins/cors/headerFunctions/allowMethods.test.ts @@ -0,0 +1,46 @@ +import { Response } from "@core/response"; +import { allowMethodsFunction } from "@plugin-cors/headerFunctions"; +import { type RequestInitializationData, Request } from "@core"; +import { createBodyReader } from "@test-utils/bodyReader"; + +function createTestRequest( + input: Partial = {}, +) { + return new Request({ + method: "OPTIONS", + headers: {}, + url: "https://example.com/test", + host: "example.com", + origin: "https://example.com", + matchedPath: null, + params: {}, + path: "/test", + query: {}, + bodyReader: createBodyReader(), + ...input, + }); +} + +function createTestResponse() { + return new Response("204", "cors", undefined); +} + +describe("allowMethodsFunction", () => { + it("sets the allow methods header with the default value", () => { + const response = createTestResponse(); + + allowMethodsFunction.default("GET,POST")(createTestRequest(), response); + + expect(response.headers!["access-control-allow-methods"]).toStrictEqual("GET,POST"); + }); + + it("sets the allow methods header from the request path when matchedPath is set", () => { + const response = createTestResponse(); + + allowMethodsFunction.isBool({ + "/test": "GET,POST,PUT", + })(createTestRequest(), response); + + expect(response.headers!["access-control-allow-methods"]).toStrictEqual("GET,POST,PUT"); + }); +}); diff --git a/tests/plugins/cors/headerFunctions/allowOrigin.test.ts b/tests/plugins/cors/headerFunctions/allowOrigin.test.ts new file mode 100644 index 0000000..2341531 --- /dev/null +++ b/tests/plugins/cors/headerFunctions/allowOrigin.test.ts @@ -0,0 +1,66 @@ +import { Response } from "@core/response"; +import { allowOriginFunction } from "@plugin-cors/headerFunctions"; +import { type RequestInitializationData, Request } from "@core"; +import { createBodyReader } from "@test-utils/bodyReader"; + +function createTestRequest( + input: Partial = {}, +) { + return new Request({ + method: "OPTIONS", + headers: {}, + url: "https://example.com/test", + host: "example.com", + origin: "https://example.com", + matchedPath: null, + params: {}, + path: "/test", + query: {}, + bodyReader: createBodyReader(), + ...input, + }); +} + +function createTestResponse() { + return new Response("204", "cors", undefined); +} + +describe("allowOriginFunction", () => { + it("set allow origin header when regexp matches", () => { + const response = createTestResponse(); + const request = createTestRequest({ + origin: "https://api.example.com", + }); + + allowOriginFunction.default(/^https:\/\/api\.example\.com$/)(request, response); + + expect(response.headers!["access-control-allow-origin"]).toStrictEqual("https://api.example.com"); + }); + + it("does not set allow origin header when regexp does not match", () => { + const response = createTestResponse(); + + allowOriginFunction.default(/^https:\/\/api\.example\.com$/)(createTestRequest(), response); + + expect(response.headers?.["access-control-allow-origin"]).toBeUndefined(); + }); + + it("set allow origin header when async function returns true", async() => { + const response = createTestResponse(); + const request = createTestRequest({ + origin: "https://app.example.com", + }); + + await allowOriginFunction.isFunction((origin) => origin === "https://app.example.com")(request, response); + + expect(response.headers!["access-control-allow-origin"]).toStrictEqual("https://app.example.com"); + }); + + it("does not set allow origin header when async function returns false", async() => { + const response = createTestResponse(); + + await allowOriginFunction.isFunction(() => Promise.resolve(false))(createTestRequest(), response); + + expect(response.headers?.["access-control-allow-origin"]).toBeUndefined(); + }); +}); diff --git a/tests/plugins/cors/headerFunctions/credentials.test.ts b/tests/plugins/cors/headerFunctions/credentials.test.ts new file mode 100644 index 0000000..31bd330 --- /dev/null +++ b/tests/plugins/cors/headerFunctions/credentials.test.ts @@ -0,0 +1,12 @@ +import { Response } from "@core/response"; +import { credentialsFunction } from "@plugin-cors/headerFunctions"; + +describe("credentialsFunction", () => { + it("set allow credentials header", () => { + const response = new Response("204", "cors", undefined); + + credentialsFunction.default()(undefined as never, response); + + expect(response.headers!["access-control-allow-credentials"]).toStrictEqual("true"); + }); +}); diff --git a/tests/plugins/cors/headerFunctions/exposeHeaders.test.ts b/tests/plugins/cors/headerFunctions/exposeHeaders.test.ts new file mode 100644 index 0000000..723a0d8 --- /dev/null +++ b/tests/plugins/cors/headerFunctions/exposeHeaders.test.ts @@ -0,0 +1,12 @@ +import { Response } from "@core/response"; +import { exposeHeadersFunction } from "@plugin-cors/headerFunctions"; + +describe("exposeHeadersFunction", () => { + it("set expose headers header", () => { + const response = new Response("204", "cors", undefined); + + exposeHeadersFunction.default("Authorization,ETag")(undefined as never, response); + + expect(response.headers!["access-control-expose-headers"]).toStrictEqual("Authorization,ETag"); + }); +}); diff --git a/tests/plugins/cors/headerFunctions/maxAge.test.ts b/tests/plugins/cors/headerFunctions/maxAge.test.ts new file mode 100644 index 0000000..72acb04 --- /dev/null +++ b/tests/plugins/cors/headerFunctions/maxAge.test.ts @@ -0,0 +1,12 @@ +import { Response } from "@core/response"; +import { maxAgeFunction } from "@plugin-cors/headerFunctions"; + +describe("maxAgeFunction", () => { + it("set max age header", () => { + const response = new Response("204", "cors", undefined); + + maxAgeFunction.default("600")(undefined as never, response); + + expect(response.headers!["access-control-max-age"]).toStrictEqual("600"); + }); +}); diff --git a/tests/plugins/cors/headerFunctions/vary.test.ts b/tests/plugins/cors/headerFunctions/vary.test.ts new file mode 100644 index 0000000..856fa6a --- /dev/null +++ b/tests/plugins/cors/headerFunctions/vary.test.ts @@ -0,0 +1,75 @@ +import { Response } from "@core/response"; +import { varyFunction } from "@plugin-cors/headerFunctions"; + +describe("varyFunction", () => { + it("basic usage", () => { + const response = new Response("204", "cors", undefined); + + varyFunction.default()({ origin: "https://basic.example.com" } as never, response); + + expect(response.headers!.vary).toStrictEqual("Origin"); + }); + + it("reuses cached vary", () => { + const vary = varyFunction.default(); + const firstResponse = new Response("204", "cors", undefined) + .setHeader("vary", "Accept-Encoding"); + const secondResponse = new Response("204", "cors", undefined) + .setHeader("vary", "X-Test"); + + vary({ origin: "https://cache.example.com" } as never, firstResponse); + vary({ origin: "https://cache.example.com" } as never, secondResponse); + + expect(firstResponse.headers!.vary).toStrictEqual("Accept-Encoding, Origin"); + expect(secondResponse.headers!.vary).toStrictEqual("Accept-Encoding, Origin"); + }); + + it("with array vary", () => { + const response = new Response("204", "cors", undefined) + .setHeader("vary", ["Accept-Encoding", "Accept-Language"]); + + varyFunction.default()({ origin: "https://array.example.com" } as never, response); + + expect(response.headers!.vary).toStrictEqual("Accept-Encoding, Accept-Language, Origin"); + }); + + it("keeps star vary header", () => { + const response = new Response("204", "cors", undefined) + .setHeader("vary", "Accept-Encoding, *"); + + varyFunction.default()({ origin: "https://star.example.com" } as never, response); + + expect(response.headers!.vary).toStrictEqual("*"); + }); + + it("not duplicate", () => { + const response = new Response("204", "cors", undefined) + .setHeader("vary", "Accept-Encoding, Origin"); + + varyFunction.default()({ origin: "https://origin.example.com" } as never, response); + + expect(response.headers!.vary).toStrictEqual("Accept-Encoding, Origin"); + }); + + it("does not cache new origin when store is full", () => { + const vary = varyFunction.default(); + + for (let index = 0; index < 500; index++) { + const response = new Response("204", "cors", undefined) + .setHeader("vary", `X-Test-${index}`); + + vary({ origin: `https://filled-${index}.example.com` } as never, response); + } + + const firstOverflowResponse = new Response("204", "cors", undefined) + .setHeader("vary", "Accept-Encoding"); + const secondOverflowResponse = new Response("204", "cors", undefined) + .setHeader("vary", "X-Second"); + + vary({ origin: "https://overflow.example.com" } as never, firstOverflowResponse); + vary({ origin: "https://overflow.example.com" } as never, secondOverflowResponse); + + expect(firstOverflowResponse.headers!.vary).toStrictEqual("Accept-Encoding, Origin"); + expect(secondOverflowResponse.headers!.vary).toStrictEqual("X-Second, Origin"); + }); +}); diff --git a/tests/plugins/cors/plugin.test.ts b/tests/plugins/cors/plugin.test.ts new file mode 100644 index 0000000..10ea5cb --- /dev/null +++ b/tests/plugins/cors/plugin.test.ts @@ -0,0 +1,282 @@ +import { + createHub, + HookResponse, + launchHookBeforeBuildRoute, + launchHookServer, + Request, + Response, + type RequestInitializationData, + ResponseContract, + useRouteBuilder, + type ResponseCode, +} from "@core"; +import { A, equal } from "@duplojs/utils"; +import { corsPlugin } from "@plugin-cors"; +import { IgnoreRouteCorsMetadata } from "@plugin-cors/metadata"; +import { createBodyReader } from "@test-utils/bodyReader"; + +export const basicRoutes = [ + useRouteBuilder("GET", "/users") + .handler( + ResponseContract.noContent("list-users"), + (__, { response }) => response("list-users"), + ), + useRouteBuilder("POST", "/users") + .handler( + ResponseContract.noContent("create-user"), + (__, { response }) => response("create-user"), + ), + useRouteBuilder("PUT", "/users") + .handler( + ResponseContract.noContent("update-user"), + (__, { response }) => response("update-user"), + ), +] as const; + +function createTestRequest( + input: Partial = {}, +) { + return new Request({ + method: "OPTIONS", + headers: {}, + url: "https://example.com/test", + host: "example.com", + origin: "https://example.com", + matchedPath: null, + params: {}, + path: "/test", + query: {}, + bodyReader: createBodyReader(), + ...input, + }); +} + +describe("cors plugin", () => { + it("when building routes, injects cors options route", async() => { + const hub = createHub({ environment: "DEV" }) + .plug( + corsPlugin({ + allowOrigin: true, + allowMethods: true, + }), + ) + .register(basicRoutes); + + await launchHookServer( + hub.aggregatesHooksHubLifeCycle("beforeServerBuildRoutes"), + hub, + {} as never, + ); + + const routes = A.from(hub.routes); + const optionsRoute = A.find(routes, (route) => route.definition.method === "OPTIONS"); + + expect(routes).toHaveLength(4); + expect(optionsRoute?.definition.paths).toStrictEqual(["/*"]); + expect(optionsRoute?.definition.hooks).toHaveLength(1); + + const routeAfterHook = await launchHookBeforeBuildRoute( + hub.aggregatesHooksHubLifeCycle("beforeBuildRoute"), + basicRoutes[0], + ); + + expect(routeAfterHook.definition.hooks).toHaveLength( + basicRoutes[0].definition.hooks.length + 1, + ); + }); + + it("when sending a standard response, applies cors headers", async() => { + const hub = createHub({ environment: "DEV" }) + .plug( + corsPlugin({ + allowOrigin: "https://example.com", + exposeHeaders: ["x-test", "x-other"], + credentials: true, + }), + ) + .register(basicRoutes); + + const routeAfterHook = await launchHookBeforeBuildRoute( + hub.aggregatesHooksHubLifeCycle("beforeBuildRoute"), + basicRoutes[0], + ); + const response = new Response("204", "cors", undefined); + const nextResult = Symbol("next"); + const beforeSendResponse = routeAfterHook.definition.hooks[0]!.beforeSendResponse!; + + const result = beforeSendResponse({ + request: createTestRequest(), + currentResponse: response, + next: () => nextResult as never, + exit: () => null as never, + } as never); + + expect(result).toBe(nextResult); + expect(response.headers).toStrictEqual({ + vary: "Origin", + "access-control-allow-origin": "https://example.com", + "access-control-expose-headers": "x-test,x-other", + "access-control-allow-credentials": "true", + }); + }); + + it("when allowOrigin is a function", async() => { + const hub = createHub({ environment: "DEV" }) + .plug( + corsPlugin({ + allowOrigin: equal("https://example.com"), + }), + ) + .register(basicRoutes); + + const routeAfterHook = await launchHookBeforeBuildRoute( + hub.aggregatesHooksHubLifeCycle("beforeBuildRoute"), + basicRoutes[0], + ); + const response = new Response("204", "cors", undefined); + const nextResult = Symbol("next"); + const beforeSendResponse = routeAfterHook.definition.hooks[0]!.beforeSendResponse!; + + const result = await beforeSendResponse({ + request: createTestRequest(), + currentResponse: response, + next: () => nextResult as never, + exit: () => null as never, + } as never); + + expect(result).toBe(nextResult); + expect(response.headers).toStrictEqual({ + vary: "Origin", + "access-control-allow-origin": "https://example.com", + }); + }); + + it("when route to the IgnoreRouteCorsMetadata, does not inject cors hook", async() => { + const hub = createHub({ environment: "DEV" }) + .plug( + corsPlugin({ + allowOrigin: true, + }), + ) + .register(basicRoutes); + const ignoredRoute = useRouteBuilder("GET", "/ignored", { + metadata: [IgnoreRouteCorsMetadata()], + }).handler( + ResponseContract.noContent("ignored"), + (__, { response }) => response("ignored"), + ); + + const routeAfterHook = await launchHookBeforeBuildRoute( + hub.aggregatesHooksHubLifeCycle("beforeBuildRoute"), + ignoredRoute, + ); + + expect(routeAfterHook).toBe(ignoredRoute); + }); + + it("i'm out of ideas for 'it message'", async() => { + const hub = createHub({ environment: "DEV" }) + .plug( + corsPlugin({ + allowMethods: ["GET", "POST"], + allowHeaders: ["content-type", "authorization"], + maxAge: 60, + }), + ) + .register(basicRoutes); + + await launchHookServer( + hub.aggregatesHooksHubLifeCycle("beforeServerBuildRoutes"), + hub, + {} as never, + ); + + const optionsRoute = A.find( + A.from(hub.routes), + (route) => route.definition.method === "OPTIONS", + )!; + const beforeRouteExecution = optionsRoute.definition.hooks[0]!.beforeRouteExecution!; + + const response = beforeRouteExecution({ + request: createTestRequest(), + response: (code, information) => new HookResponse("beforeRouteExecution", code, information, undefined), + next: () => null as never, + exit: () => null as never, + }) as HookResponse; + + expect(response.headers).toStrictEqual({ + "access-control-allow-methods": "GET,POST", + "access-control-allow-headers": "content-type,authorization", + "access-control-max-age": "60", + }); + }); + + it("when allowMethods is true, resolves methods from request path", async() => { + const hub = createHub({ environment: "DEV" }) + .plug( + corsPlugin({ + allowMethods: true, + }), + ) + .register(basicRoutes); + + await launchHookServer( + hub.aggregatesHooksHubLifeCycle("beforeServerBuildRoutes"), + hub, + {} as never, + ); + + const optionsRoute = A.find( + A.from(hub.routes), + (route) => route.definition.method === "OPTIONS", + )!; + const beforeRouteExecution = optionsRoute.definition.hooks[0]!.beforeRouteExecution!; + + const response = beforeRouteExecution({ + request: createTestRequest({ + path: "/users", + matchedPath: "/*", + }), + response: (code, information) => new HookResponse("beforeRouteExecution", code, information, undefined), + next: () => null as never, + exit: () => null as never, + }) as HookResponse; + + expect(response.headers).toStrictEqual({ + "access-control-allow-methods": "GET,POST,PUT", + }); + }); + + it("when allowHeaders accepts everything (*)", async() => { + const hub = createHub({ environment: "DEV" }) + .plug( + corsPlugin({ + allowHeaders: true, + }), + ) + .register(basicRoutes); + + await launchHookServer( + hub.aggregatesHooksHubLifeCycle("beforeServerBuildRoutes"), + hub, + {} as never, + ); + + const optionsRoute = A.find( + A.from(hub.routes), + (route) => route.definition.method === "OPTIONS", + )!; + const beforeRouteExecution = optionsRoute.definition.hooks[0]!.beforeRouteExecution!; + + const response = beforeRouteExecution({ + request: createTestRequest(), + response: (code, information) => new HookResponse("beforeRouteExecution", code, information, undefined), + next: () => null as never, + exit: () => null as never, + }) as HookResponse; + + expect(response.headers).toStrictEqual({ + "access-control-allow-headers": "*", + }); + }); +}); diff --git a/tests/plugins/cors/tsconfig.json b/tests/plugins/cors/tsconfig.json new file mode 100644 index 0000000..bdcbbf9 --- /dev/null +++ b/tests/plugins/cors/tsconfig.json @@ -0,0 +1,17 @@ +{ + "extends": "../../../tsconfig.test.json", + "compilerOptions": { + "baseUrl": "./", + "paths": { + "@core": ["../../../scripts/core/index.ts"], + "@core/*": ["../../../scripts/core/*"], + "@plugin-cors": ["../../../scripts/plugins/cors/index.ts"], + "@plugin-cors/*": ["../../../scripts/plugins/cors/*"], + "@test-utils/*": ["../../_utils/*"], + }, + }, + "include": [ + "**/*.ts", + "../../../scripts/plugins/cors/**/*.ts", + ], +} diff --git a/tsconfig.json b/tsconfig.json index 70b6264..6461faa 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -10,6 +10,7 @@ { "path": "./scripts/plugins/openApiGenerator/tsconfig.json" }, { "path": "./scripts/plugins/static/tsconfig.json" }, { "path": "./scripts/plugins/cacheController/tsconfig.json" }, + { "path": "./scripts/plugins/cors/tsconfig.json" }, { "path": "./tests/_utils/tsconfig.json" }, { "path": "./tests/core/tsconfig.json" }, @@ -21,6 +22,7 @@ { "path": "./tests/plugins/openApiGenerator/tsconfig.json" }, { "path": "./tests/plugins/static/tsconfig.json" }, { "path": "./tests/plugins/cacheController/tsconfig.json" }, + { "path": "./tests/plugins/cors/tsconfig.json" }, { "path": "./integration/core/tsconfig.json" }, { "path": "./integration/node/tsconfig.json" }, @@ -28,6 +30,7 @@ { "path": "./integration/openApiGenerator/tsconfig.json" }, { "path": "./integration/client/tsconfig.json" }, { "path": "./integration/static/tsconfig.json" }, - { "path": "./tests/plugins/cacheController/tsconfig.json" }, + { "path": "./integration/cacheController/tsconfig.json" }, + { "path": "./integration/cors/tsconfig.json" }, ], } \ No newline at end of file