Skip to content

Commit 2455267

Browse files
authored
Improving methods inheritance (#2833)
Due to #2816 <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Refactor** * Improved and unified type definitions for HTTP methods, enhancing consistency and clarity in method handling across the application. * Updated method handling logic to better support CORS and HTTP method distinctions. * **Tests** * Removed obsolete tests related to deprecated method types. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
1 parent b1762bf commit 2455267

File tree

5 files changed

+81
-39
lines changed

5 files changed

+81
-39
lines changed

express-zod-api/src/common-helpers.ts

Lines changed: 21 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,13 @@ import { z } from "zod/v4";
44
import type { $ZodTransform, $ZodType } from "zod/v4/core";
55
import { CommonConfig, InputSource, InputSources } from "./config-type";
66
import { contentTypes } from "./content-type";
7-
import { AuxMethod, ClientMethod, Method } from "./method";
7+
import {
8+
ClientMethod,
9+
SomeMethod,
10+
isMethod,
11+
Method,
12+
CORSMethod,
13+
} from "./method";
814
import { ResponseVariant } from "./api-response";
915

1016
/** @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 = {
4349
patch: ["body", "params"],
4450
delete: ["query", "params"],
4551
};
46-
const fallbackInputSource: InputSource[] = ["body", "query", "params"];
52+
const fallbackInputSources: InputSource[] = ["body", "query", "params"];
4753

48-
/** @todo consider removing "as" to ensure more constraints and realistic handling */
4954
export const getActualMethod = (request: Request) =>
50-
request.method.toLowerCase() as Method | AuxMethod;
55+
request.method.toLowerCase() as SomeMethod;
5156

5257
export const getInputSources = (
53-
actualMethod: ReturnType<typeof getActualMethod>,
58+
actualMethod: SomeMethod,
5459
userDefined: CommonConfig["inputSources"] = {},
5560
) => {
56-
if (actualMethod === "options") return [];
57-
const method = actualMethod === "head" ? "get" : actualMethod;
58-
return (
59-
userDefined[method] || defaultInputSources[method] || fallbackInputSource
60-
);
61+
if (actualMethod === ("options" satisfies CORSMethod)) return [];
62+
const method =
63+
actualMethod === ("head" satisfies ClientMethod)
64+
? ("get" satisfies Method)
65+
: isMethod(actualMethod)
66+
? actualMethod
67+
: undefined;
68+
const matchingSources = method
69+
? userDefined[method] || defaultInputSources[method]
70+
: undefined;
71+
return matchingSources || fallbackInputSources;
6172
};
6273

6374
export const getInput = (

express-zod-api/src/endpoint.ts

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ import { lastResortHandler } from "./last-resort";
2323
import { ActualLogger } from "./logger-helpers";
2424
import { LogicalContainer } from "./logical-container";
2525
import { getBrand, getExamples } from "./metadata";
26-
import { AuxMethod, ClientMethod, Method } from "./method";
26+
import { ClientMethod, CORSMethod, Method, SomeMethod } from "./method";
2727
import { AbstractMiddleware, ExpressMiddleware } from "./middleware";
2828
import { ContentType } from "./content-type";
2929
import { ezRawBrand } from "./raw-schema";
@@ -213,15 +213,19 @@ export class Endpoint<
213213
response,
214214
...rest
215215
}: {
216-
method: Method | AuxMethod;
216+
method: SomeMethod;
217217
input: Readonly<FlatObject>; // Issue #673: input is immutable, since this.inputSchema is combined with ones of middlewares
218218
request: Request;
219219
response: Response;
220220
logger: ActualLogger;
221221
options: Partial<OPT>;
222222
}) {
223223
for (const mw of this.#def.middlewares || []) {
224-
if (method === "options" && !(mw instanceof ExpressMiddleware)) continue;
224+
if (
225+
method === ("options" satisfies CORSMethod) &&
226+
!(mw instanceof ExpressMiddleware)
227+
)
228+
continue;
225229
Object.assign(
226230
options,
227231
await mw.execute({ ...rest, options, response, logger }),
@@ -302,7 +306,8 @@ export class Endpoint<
302306
options,
303307
});
304308
if (response.writableEnded) return;
305-
if (method === "options") return void response.status(200).end();
309+
if (method === ("options" satisfies CORSMethod))
310+
return void response.status(200).end();
306311
result = {
307312
output: await this.#parseOutput(
308313
await this.#parseAndRunHandler({

express-zod-api/src/method.ts

Lines changed: 23 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,46 @@
11
import type { IRouter } from "express";
22

3+
export type SomeMethod = Lowercase<string>;
4+
5+
type FamiliarMethod = Exclude<
6+
keyof IRouter,
7+
"param" | "use" | "route" | "stack"
8+
>;
9+
310
export const methods = [
411
"get",
512
"post",
613
"put",
714
"delete",
815
"patch",
9-
] satisfies Array<keyof IRouter>;
16+
] satisfies Array<FamiliarMethod>;
17+
18+
export const clientMethods = [
19+
...methods,
20+
"head",
21+
] satisfies Array<FamiliarMethod>;
1022

1123
/**
1224
* @desc Methods supported by the framework API to produce Endpoints on EndpointsFactory.
1325
* @see BuildProps
26+
* @example "get" | "post" | "put" | "delete" | "patch"
1427
* */
1528
export type Method = (typeof methods)[number];
1629

17-
/**
18-
* @desc Additional methods having some technical handling in the framework
19-
* @see makeCorsHeaders
20-
* @todo consider removing it and introducing CORSMethod = ClientMethod | "options"
21-
* */
22-
export type AuxMethod = Extract<keyof IRouter, "options" | "head">;
23-
24-
export const clientMethods = [...methods, "head"] satisfies Array<
25-
Method | Extract<AuxMethod, "head">
26-
>;
27-
2830
/**
2931
* @desc Methods usable on the client side, available via generated Integration and Documentation
3032
* @see withHead
33+
* @example Method | "head"
3134
* */
3235
export type ClientMethod = (typeof clientMethods)[number];
3336

37+
/**
38+
* @desc Methods supported in CORS headers
39+
* @see makeCorsHeaders
40+
* @see createWrongMethodHandler
41+
* @example ClientMethod | "options"
42+
* */
43+
export type CORSMethod = ClientMethod | Extract<FamiliarMethod, "options">;
44+
3445
export const isMethod = (subject: string): subject is Method =>
3546
(methods as string[]).includes(subject);

express-zod-api/src/routing.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { ContentType } from "./content-type";
66
import { DependsOnMethod } from "./depends-on-method";
77
import { Diagnostics } from "./diagnostics";
88
import { AbstractEndpoint } from "./endpoint";
9-
import { AuxMethod, isMethod, Method } from "./method";
9+
import { CORSMethod, isMethod } from "./method";
1010
import { OnEndpoint, walkRouting } from "./routing-walker";
1111
import { ServeStatic } from "./serve-static";
1212
import { GetLogger } from "./server-helpers";
@@ -24,15 +24,15 @@ export interface Routing {
2424

2525
export type Parsers = Partial<Record<ContentType, RequestHandler[]>>;
2626

27-
const lineUp = (methods: Array<Method | AuxMethod>) =>
27+
const lineUp = (methods: CORSMethod[]) =>
2828
methods // auxiliary methods go last
2929
.sort((a, b) => +isMethod(b) - +isMethod(a) || a.localeCompare(b))
3030
.join(", ")
3131
.toUpperCase();
3232

3333
/** @link https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/405 */
3434
export const createWrongMethodHandler =
35-
(allowedMethods: Array<Method | AuxMethod>): RequestHandler =>
35+
(allowedMethods: CORSMethod[]): RequestHandler =>
3636
({ method }, res, next) => {
3737
const Allow = lineUp(allowedMethods);
3838
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 =
4242
next(error);
4343
};
4444

45-
const makeCorsHeaders = (accessMethods: Array<Method | AuxMethod>) => ({
45+
const makeCorsHeaders = (accessMethods: CORSMethod[]) => ({
4646
"Access-Control-Allow-Origin": "*",
4747
"Access-Control-Allow-Methods": lineUp(accessMethods),
4848
"Access-Control-Allow-Headers": "content-type",
4949
});
5050

51-
type Siblings = Map<Method | AuxMethod, [RequestHandler[], AbstractEndpoint]>;
51+
type Siblings = Map<CORSMethod, [RequestHandler[], AbstractEndpoint]>;
5252

5353
export const initRouting = ({
5454
app,

express-zod-api/tests/method.spec.ts

Lines changed: 23 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,20 @@ import {
33
isMethod,
44
methods,
55
Method,
6-
AuxMethod,
76
clientMethods,
87
ClientMethod,
8+
SomeMethod,
9+
CORSMethod,
910
} from "../src/method";
10-
import { describe } from "node:test";
1111

1212
describe("Method", () => {
13+
describe("SomeMethod type", () => {
14+
test("should be a lowercase string", () => {
15+
expectTypeOf<"test">().toExtend<SomeMethod>();
16+
expectTypeOf<"TEST">().not.toExtend<SomeMethod>();
17+
});
18+
});
19+
1320
describe("methods array", () => {
1421
test("should be the list of selected keys of express router", () => {
1522
expect(methods).toEqual(["get", "post", "put", "delete", "patch"]);
@@ -29,14 +36,15 @@ describe("Method", () => {
2936
});
3037
});
3138

32-
describe("the type", () => {
39+
describe("Method type", () => {
3340
test("should match the entries of the methods array", () => {
3441
expectTypeOf<"get">().toExtend<Method>();
3542
expectTypeOf<"post">().toExtend<Method>();
3643
expectTypeOf<"put">().toExtend<Method>();
3744
expectTypeOf<"delete">().toExtend<Method>();
3845
expectTypeOf<"patch">().toExtend<Method>();
3946
expectTypeOf<"wrong">().not.toExtend<Method>();
47+
expectTypeOf<Method>().toExtend<SomeMethod>();
4048
});
4149
});
4250

@@ -49,14 +57,21 @@ describe("Method", () => {
4957
expectTypeOf<"patch">().toExtend<ClientMethod>();
5058
expectTypeOf<"head">().toExtend<ClientMethod>();
5159
expectTypeOf<"wrong">().not.toExtend<ClientMethod>();
60+
expectTypeOf<ClientMethod>().toExtend<SomeMethod>();
5261
});
5362
});
5463

55-
describe("AuxMethod", () => {
56-
test("should be options or head", () => {
57-
expectTypeOf<"options">().toExtend<AuxMethod>();
58-
expectTypeOf<"head">().toExtend<AuxMethod>();
59-
expectTypeOf<"other">().not.toExtend<AuxMethod>();
64+
describe("CORSMethod type", () => {
65+
test("should extends ClientMethod with options", () => {
66+
expectTypeOf<"get">().toExtend<CORSMethod>();
67+
expectTypeOf<"post">().toExtend<CORSMethod>();
68+
expectTypeOf<"put">().toExtend<CORSMethod>();
69+
expectTypeOf<"delete">().toExtend<CORSMethod>();
70+
expectTypeOf<"patch">().toExtend<CORSMethod>();
71+
expectTypeOf<"head">().toExtend<CORSMethod>();
72+
expectTypeOf<"options">().toExtend<CORSMethod>();
73+
expectTypeOf<"wrong">().not.toExtend<CORSMethod>();
74+
expectTypeOf<CORSMethod>().toExtend<SomeMethod>();
6075
});
6176
});
6277

0 commit comments

Comments
 (0)