From d14850345deb81cd21a5666d8c828f8766ee8eee Mon Sep 17 00:00:00 2001 From: Anna Bocharova Date: Sun, 20 Jul 2025 12:36:06 +0200 Subject: [PATCH 1/8] Ref: rm AuxMethod, add FamiliarMethod and CORSMethod (unfinished). --- express-zod-api/src/common-helpers.ts | 4 ++-- express-zod-api/src/endpoint.ts | 4 ++-- express-zod-api/src/method.ts | 31 ++++++++++++++++----------- express-zod-api/src/routing.ts | 10 ++++----- express-zod-api/tests/method.spec.ts | 9 -------- 5 files changed, 28 insertions(+), 30 deletions(-) diff --git a/express-zod-api/src/common-helpers.ts b/express-zod-api/src/common-helpers.ts index 584dbf8837..ea44e701d7 100644 --- a/express-zod-api/src/common-helpers.ts +++ b/express-zod-api/src/common-helpers.ts @@ -4,7 +4,7 @@ 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, FamiliarMethod } 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 */ @@ -47,7 +47,7 @@ const fallbackInputSource: 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 FamiliarMethod; export const getInputSources = ( actualMethod: ReturnType, diff --git a/express-zod-api/src/endpoint.ts b/express-zod-api/src/endpoint.ts index 8e04755902..01a2d4c1fe 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, FamiliarMethod, Method } 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: FamiliarMethod /** @todo string */; input: Readonly; // Issue #673: input is immutable, since this.inputSchema is combined with ones of middlewares request: Request; response: Response; diff --git a/express-zod-api/src/method.ts b/express-zod-api/src/method.ts index 76bd5f5465..3eb14a75aa 100644 --- a/express-zod-api/src/method.ts +++ b/express-zod-api/src/method.ts @@ -1,12 +1,23 @@ import type { IRouter } from "express"; +/** @todo rm export */ +export 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. @@ -14,22 +25,18 @@ export const methods = [ * */ 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 * */ export type ClientMethod = (typeof clientMethods)[number]; +/** + * @desc Methods supported in CORS headers + * @see makeCorsHeaders + * @see createWrongMethodHandler + * */ +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..e64444dd05 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: Array) => 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: Array): 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: Array) => ({ "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..a5d8c2c751 100644 --- a/express-zod-api/tests/method.spec.ts +++ b/express-zod-api/tests/method.spec.ts @@ -3,7 +3,6 @@ import { isMethod, methods, Method, - AuxMethod, clientMethods, ClientMethod, } from "../src/method"; @@ -52,14 +51,6 @@ describe("Method", () => { }); }); - describe("AuxMethod", () => { - test("should be options or head", () => { - expectTypeOf<"options">().toExtend(); - expectTypeOf<"head">().toExtend(); - expectTypeOf<"other">().not.toExtend(); - }); - }); - describe("isMethod", () => { test.each(methods)("should validate %s", (one) => { expect(isMethod(one)).toBe(true); From 56d75b691123dd21d589852b4a810e98805e7e26 Mon Sep 17 00:00:00 2001 From: Anna Bocharova Date: Sun, 20 Jul 2025 12:48:55 +0200 Subject: [PATCH 2/8] Adjusting implementation to use SomeMethod (string) where applicable. --- express-zod-api/src/common-helpers.ts | 22 ++++++++++++++-------- express-zod-api/src/endpoint.ts | 4 ++-- express-zod-api/src/method.ts | 5 +++-- express-zod-api/tests/method.spec.ts | 1 - 4 files changed, 19 insertions(+), 13 deletions(-) diff --git a/express-zod-api/src/common-helpers.ts b/express-zod-api/src/common-helpers.ts index ea44e701d7..2eb39cfa9f 100644 --- a/express-zod-api/src/common-helpers.ts +++ b/express-zod-api/src/common-helpers.ts @@ -4,7 +4,7 @@ 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 { ClientMethod, FamiliarMethod } from "./method"; +import { ClientMethod, SomeMethod, isMethod, Method } 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 +43,27 @@ export const defaultInputSources: InputSources = { patch: ["body", "params"], delete: ["query", "params"], }; +/** @todo pluralize name */ const fallbackInputSource: InputSource[] = ["body", "query", "params"]; -/** @todo consider removing "as" to ensure more constraints and realistic handling */ export const getActualMethod = (request: Request) => - request.method.toLowerCase() as FamiliarMethod; + 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 - ); + const method = + actualMethod === "head" + ? ("get" satisfies Method) + : isMethod(actualMethod) + ? actualMethod + : undefined; + const matchingSources = method + ? userDefined[method] || defaultInputSources[method] + : undefined; + return matchingSources || fallbackInputSource; }; export const getInput = ( diff --git a/express-zod-api/src/endpoint.ts b/express-zod-api/src/endpoint.ts index 01a2d4c1fe..eba8d1b50e 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 { ClientMethod, FamiliarMethod, Method } from "./method"; +import { ClientMethod, 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: FamiliarMethod /** @todo string */; + method: SomeMethod; input: Readonly; // Issue #673: input is immutable, since this.inputSchema is combined with ones of middlewares request: Request; response: Response; diff --git a/express-zod-api/src/method.ts b/express-zod-api/src/method.ts index 3eb14a75aa..2109420439 100644 --- a/express-zod-api/src/method.ts +++ b/express-zod-api/src/method.ts @@ -1,7 +1,8 @@ import type { IRouter } from "express"; -/** @todo rm export */ -export type FamiliarMethod = Exclude< +export type SomeMethod = Lowercase; + +type FamiliarMethod = Exclude< keyof IRouter, "param" | "use" | "route" | "stack" >; diff --git a/express-zod-api/tests/method.spec.ts b/express-zod-api/tests/method.spec.ts index a5d8c2c751..56575ea93b 100644 --- a/express-zod-api/tests/method.spec.ts +++ b/express-zod-api/tests/method.spec.ts @@ -6,7 +6,6 @@ import { clientMethods, ClientMethod, } from "../src/method"; -import { describe } from "node:test"; describe("Method", () => { describe("methods array", () => { From b4e5cd34872425a1787d1fd2668c279966fbb7cb Mon Sep 17 00:00:00 2001 From: Anna Bocharova Date: Sun, 20 Jul 2025 12:51:18 +0200 Subject: [PATCH 3/8] Ref: naming, fallbackInputSources. --- express-zod-api/src/common-helpers.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/express-zod-api/src/common-helpers.ts b/express-zod-api/src/common-helpers.ts index 2eb39cfa9f..c5964b1131 100644 --- a/express-zod-api/src/common-helpers.ts +++ b/express-zod-api/src/common-helpers.ts @@ -43,8 +43,7 @@ export const defaultInputSources: InputSources = { patch: ["body", "params"], delete: ["query", "params"], }; -/** @todo pluralize name */ -const fallbackInputSource: InputSource[] = ["body", "query", "params"]; +const fallbackInputSources: InputSource[] = ["body", "query", "params"]; export const getActualMethod = (request: Request) => request.method.toLowerCase() as SomeMethod; @@ -63,7 +62,7 @@ export const getInputSources = ( const matchingSources = method ? userDefined[method] || defaultInputSources[method] : undefined; - return matchingSources || fallbackInputSource; + return matchingSources || fallbackInputSources; }; export const getInput = ( From 5d47dae57d7f121e77412e7706b3583bad313a21 Mon Sep 17 00:00:00 2001 From: Anna Bocharova Date: Sun, 20 Jul 2025 12:52:46 +0200 Subject: [PATCH 4/8] Minor: jsdoc. --- express-zod-api/src/method.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/express-zod-api/src/method.ts b/express-zod-api/src/method.ts index 2109420439..1bc02b1af0 100644 --- a/express-zod-api/src/method.ts +++ b/express-zod-api/src/method.ts @@ -23,12 +23,14 @@ export const clientMethods = [ /** * @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 Methods usable on the client side, available via generated Integration and Documentation * @see withHead + * @example Method | "head" * */ export type ClientMethod = (typeof clientMethods)[number]; @@ -36,6 +38,7 @@ export type ClientMethod = (typeof clientMethods)[number]; * @desc Methods supported in CORS headers * @see makeCorsHeaders * @see createWrongMethodHandler + * @example ClientMethod | "options" * */ export type CORSMethod = ClientMethod | Extract; From 1d29db9230875b841b6c4459a4d0f0f9fbf8efd9 Mon Sep 17 00:00:00 2001 From: Anna Bocharova Date: Sun, 20 Jul 2025 13:03:37 +0200 Subject: [PATCH 5/8] More constraints where using comparison to SomeMethod. --- express-zod-api/src/common-helpers.ts | 12 +++++++++--- express-zod-api/src/endpoint.ts | 11 ++++++++--- 2 files changed, 17 insertions(+), 6 deletions(-) diff --git a/express-zod-api/src/common-helpers.ts b/express-zod-api/src/common-helpers.ts index c5964b1131..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 { ClientMethod, SomeMethod, isMethod, 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 */ @@ -52,9 +58,9 @@ export const getInputSources = ( actualMethod: SomeMethod, userDefined: CommonConfig["inputSources"] = {}, ) => { - if (actualMethod === "options") return []; + if (actualMethod === ("options" satisfies CORSMethod)) return []; const method = - actualMethod === "head" + actualMethod === ("head" satisfies ClientMethod) ? ("get" satisfies Method) : isMethod(actualMethod) ? actualMethod diff --git a/express-zod-api/src/endpoint.ts b/express-zod-api/src/endpoint.ts index eba8d1b50e..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 { ClientMethod, Method, SomeMethod } from "./method"; +import { ClientMethod, CORSMethod, Method, SomeMethod } from "./method"; import { AbstractMiddleware, ExpressMiddleware } from "./middleware"; import { ContentType } from "./content-type"; import { ezRawBrand } from "./raw-schema"; @@ -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({ From 67295a9180bebeec3bef2c25e3dbab2cc1256225 Mon Sep 17 00:00:00 2001 From: Anna Bocharova Date: Sun, 20 Jul 2025 13:12:14 +0200 Subject: [PATCH 6/8] Ref: minor, shortening. --- express-zod-api/src/routing.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/express-zod-api/src/routing.ts b/express-zod-api/src/routing.ts index e64444dd05..45999652cd 100644 --- a/express-zod-api/src/routing.ts +++ b/express-zod-api/src/routing.ts @@ -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,7 +42,7 @@ 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", From d7b97697f2c08c9648d1e7d0498ce592d89b5332 Mon Sep 17 00:00:00 2001 From: Anna Bocharova Date: Sun, 20 Jul 2025 13:17:35 +0200 Subject: [PATCH 7/8] Test for new types. --- express-zod-api/tests/method.spec.ts | 28 +++++++++++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/express-zod-api/tests/method.spec.ts b/express-zod-api/tests/method.spec.ts index 56575ea93b..eb30e4388c 100644 --- a/express-zod-api/tests/method.spec.ts +++ b/express-zod-api/tests/method.spec.ts @@ -5,9 +5,19 @@ import { Method, clientMethods, ClientMethod, + SomeMethod, + CORSMethod, } from "../src/method"; +import { expectTypeOf } from "vitest"; 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"]); @@ -27,7 +37,7 @@ describe("Method", () => { }); }); - describe("the type", () => { + describe("Method type", () => { test("should match the entries of the methods array", () => { expectTypeOf<"get">().toExtend(); expectTypeOf<"post">().toExtend(); @@ -35,6 +45,7 @@ describe("Method", () => { expectTypeOf<"delete">().toExtend(); expectTypeOf<"patch">().toExtend(); expectTypeOf<"wrong">().not.toExtend(); + expectTypeOf().toExtend(); }); }); @@ -47,6 +58,21 @@ describe("Method", () => { expectTypeOf<"patch">().toExtend(); expectTypeOf<"head">().toExtend(); expectTypeOf<"wrong">().not.toExtend(); + expectTypeOf().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(); }); }); From 75aa342fc8a7f89244f07e5ad2991911bb69bd33 Mon Sep 17 00:00:00 2001 From: Anna Bocharova Date: Sun, 20 Jul 2025 13:18:05 +0200 Subject: [PATCH 8/8] rm redundant import. --- express-zod-api/tests/method.spec.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/express-zod-api/tests/method.spec.ts b/express-zod-api/tests/method.spec.ts index eb30e4388c..7bab93da7a 100644 --- a/express-zod-api/tests/method.spec.ts +++ b/express-zod-api/tests/method.spec.ts @@ -8,7 +8,6 @@ import { SomeMethod, CORSMethod, } from "../src/method"; -import { expectTypeOf } from "vitest"; describe("Method", () => { describe("SomeMethod type", () => {