diff --git a/express-zod-api/src/common-helpers.ts b/express-zod-api/src/common-helpers.ts index 584dbf8837..002c316c83 100644 --- a/express-zod-api/src/common-helpers.ts +++ b/express-zod-api/src/common-helpers.ts @@ -4,7 +4,13 @@ import { z } from "zod/v4"; import type { $ZodTransform, $ZodType } from "zod/v4/core"; import { CommonConfig, InputSource, InputSources } from "./config-type"; import { contentTypes } from "./content-type"; -import { AuxMethod, ClientMethod, Method } from "./method"; +import { + ClientMethod, + SomeMethod, + isMethod, + Method, + CORSMethod, +} from "./method"; import { ResponseVariant } from "./api-response"; /** @desc this type does not allow props assignment, but it works for reading them when merged with another interface */ @@ -43,21 +49,26 @@ export const defaultInputSources: InputSources = { patch: ["body", "params"], delete: ["query", "params"], }; -const fallbackInputSource: InputSource[] = ["body", "query", "params"]; +const fallbackInputSources: InputSource[] = ["body", "query", "params"]; -/** @todo consider removing "as" to ensure more constraints and realistic handling */ export const getActualMethod = (request: Request) => - request.method.toLowerCase() as Method | AuxMethod; + request.method.toLowerCase() as SomeMethod; export const getInputSources = ( - actualMethod: ReturnType, + actualMethod: SomeMethod, userDefined: CommonConfig["inputSources"] = {}, ) => { - if (actualMethod === "options") return []; - const method = actualMethod === "head" ? "get" : actualMethod; - return ( - userDefined[method] || defaultInputSources[method] || fallbackInputSource - ); + if (actualMethod === ("options" satisfies CORSMethod)) return []; + const method = + actualMethod === ("head" satisfies ClientMethod) + ? ("get" satisfies Method) + : isMethod(actualMethod) + ? actualMethod + : undefined; + const matchingSources = method + ? userDefined[method] || defaultInputSources[method] + : undefined; + return matchingSources || fallbackInputSources; }; export const getInput = ( diff --git a/express-zod-api/src/endpoint.ts b/express-zod-api/src/endpoint.ts index 8e04755902..d6b31a115a 100644 --- a/express-zod-api/src/endpoint.ts +++ b/express-zod-api/src/endpoint.ts @@ -23,7 +23,7 @@ import { lastResortHandler } from "./last-resort"; import { ActualLogger } from "./logger-helpers"; import { LogicalContainer } from "./logical-container"; import { getBrand, getExamples } from "./metadata"; -import { AuxMethod, ClientMethod, Method } from "./method"; +import { ClientMethod, CORSMethod, Method, SomeMethod } from "./method"; import { AbstractMiddleware, ExpressMiddleware } from "./middleware"; import { ContentType } from "./content-type"; import { ezRawBrand } from "./raw-schema"; @@ -213,7 +213,7 @@ export class Endpoint< response, ...rest }: { - method: Method | AuxMethod; + method: SomeMethod; input: Readonly; // Issue #673: input is immutable, since this.inputSchema is combined with ones of middlewares request: Request; response: Response; @@ -221,7 +221,11 @@ export class Endpoint< options: Partial; }) { for (const mw of this.#def.middlewares || []) { - if (method === "options" && !(mw instanceof ExpressMiddleware)) continue; + if ( + method === ("options" satisfies CORSMethod) && + !(mw instanceof ExpressMiddleware) + ) + continue; Object.assign( options, await mw.execute({ ...rest, options, response, logger }), @@ -302,7 +306,8 @@ export class Endpoint< options, }); if (response.writableEnded) return; - if (method === "options") return void response.status(200).end(); + if (method === ("options" satisfies CORSMethod)) + return void response.status(200).end(); result = { output: await this.#parseOutput( await this.#parseAndRunHandler({ diff --git a/express-zod-api/src/method.ts b/express-zod-api/src/method.ts index 76bd5f5465..1bc02b1af0 100644 --- a/express-zod-api/src/method.ts +++ b/express-zod-api/src/method.ts @@ -1,35 +1,46 @@ import type { IRouter } from "express"; +export type SomeMethod = Lowercase; + +type FamiliarMethod = Exclude< + keyof IRouter, + "param" | "use" | "route" | "stack" +>; + export const methods = [ "get", "post", "put", "delete", "patch", -] satisfies Array; +] satisfies Array; + +export const clientMethods = [ + ...methods, + "head", +] satisfies Array; /** * @desc Methods supported by the framework API to produce Endpoints on EndpointsFactory. * @see BuildProps + * @example "get" | "post" | "put" | "delete" | "patch" * */ export type Method = (typeof methods)[number]; -/** - * @desc Additional methods having some technical handling in the framework - * @see makeCorsHeaders - * @todo consider removing it and introducing CORSMethod = ClientMethod | "options" - * */ -export type AuxMethod = Extract; - -export const clientMethods = [...methods, "head"] satisfies Array< - Method | Extract ->; - /** * @desc Methods usable on the client side, available via generated Integration and Documentation * @see withHead + * @example Method | "head" * */ export type ClientMethod = (typeof clientMethods)[number]; +/** + * @desc Methods supported in CORS headers + * @see makeCorsHeaders + * @see createWrongMethodHandler + * @example ClientMethod | "options" + * */ +export type CORSMethod = ClientMethod | Extract; + export const isMethod = (subject: string): subject is Method => (methods as string[]).includes(subject); diff --git a/express-zod-api/src/routing.ts b/express-zod-api/src/routing.ts index 22726fbb2f..45999652cd 100644 --- a/express-zod-api/src/routing.ts +++ b/express-zod-api/src/routing.ts @@ -6,7 +6,7 @@ import { ContentType } from "./content-type"; import { DependsOnMethod } from "./depends-on-method"; import { Diagnostics } from "./diagnostics"; import { AbstractEndpoint } from "./endpoint"; -import { AuxMethod, isMethod, Method } from "./method"; +import { CORSMethod, isMethod } from "./method"; import { OnEndpoint, walkRouting } from "./routing-walker"; import { ServeStatic } from "./serve-static"; import { GetLogger } from "./server-helpers"; @@ -24,7 +24,7 @@ export interface Routing { export type Parsers = Partial>; -const lineUp = (methods: Array) => +const lineUp = (methods: CORSMethod[]) => methods // auxiliary methods go last .sort((a, b) => +isMethod(b) - +isMethod(a) || a.localeCompare(b)) .join(", ") @@ -32,7 +32,7 @@ const lineUp = (methods: Array) => /** @link https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/405 */ export const createWrongMethodHandler = - (allowedMethods: Array): RequestHandler => + (allowedMethods: CORSMethod[]): RequestHandler => ({ method }, res, next) => { const Allow = lineUp(allowedMethods); res.set({ Allow }); // in case of a custom errorHandler configured that does not care about headers in error @@ -42,13 +42,13 @@ export const createWrongMethodHandler = next(error); }; -const makeCorsHeaders = (accessMethods: Array) => ({ +const makeCorsHeaders = (accessMethods: CORSMethod[]) => ({ "Access-Control-Allow-Origin": "*", "Access-Control-Allow-Methods": lineUp(accessMethods), "Access-Control-Allow-Headers": "content-type", }); -type Siblings = Map; +type Siblings = Map; export const initRouting = ({ app, diff --git a/express-zod-api/tests/method.spec.ts b/express-zod-api/tests/method.spec.ts index a513574586..7bab93da7a 100644 --- a/express-zod-api/tests/method.spec.ts +++ b/express-zod-api/tests/method.spec.ts @@ -3,13 +3,20 @@ import { isMethod, methods, Method, - AuxMethod, clientMethods, ClientMethod, + SomeMethod, + CORSMethod, } from "../src/method"; -import { describe } from "node:test"; describe("Method", () => { + describe("SomeMethod type", () => { + test("should be a lowercase string", () => { + expectTypeOf<"test">().toExtend(); + expectTypeOf<"TEST">().not.toExtend(); + }); + }); + describe("methods array", () => { test("should be the list of selected keys of express router", () => { expect(methods).toEqual(["get", "post", "put", "delete", "patch"]); @@ -29,7 +36,7 @@ describe("Method", () => { }); }); - describe("the type", () => { + describe("Method type", () => { test("should match the entries of the methods array", () => { expectTypeOf<"get">().toExtend(); expectTypeOf<"post">().toExtend(); @@ -37,6 +44,7 @@ describe("Method", () => { expectTypeOf<"delete">().toExtend(); expectTypeOf<"patch">().toExtend(); expectTypeOf<"wrong">().not.toExtend(); + expectTypeOf().toExtend(); }); }); @@ -49,14 +57,21 @@ describe("Method", () => { expectTypeOf<"patch">().toExtend(); expectTypeOf<"head">().toExtend(); expectTypeOf<"wrong">().not.toExtend(); + expectTypeOf().toExtend(); }); }); - describe("AuxMethod", () => { - test("should be options or head", () => { - expectTypeOf<"options">().toExtend(); - expectTypeOf<"head">().toExtend(); - expectTypeOf<"other">().not.toExtend(); + describe("CORSMethod type", () => { + test("should extends ClientMethod with options", () => { + expectTypeOf<"get">().toExtend(); + expectTypeOf<"post">().toExtend(); + expectTypeOf<"put">().toExtend(); + expectTypeOf<"delete">().toExtend(); + expectTypeOf<"patch">().toExtend(); + expectTypeOf<"head">().toExtend(); + expectTypeOf<"options">().toExtend(); + expectTypeOf<"wrong">().not.toExtend(); + expectTypeOf().toExtend(); }); });