Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,16 @@
### v28.0.0

- Supported Node.js versions: `^22.19.0 || ^24.0.0`;
- Breaking change to the `createConfig()` argument (object):
- property `wrongMethodBehavior` (number) changed to `hintAllowedMethods` (boolean);
- Consider using [the automated migration](https://www.npmjs.com/package/@express-zod-api/migration).

```diff
createConfig({
- wrongMethodBehavior: 404,
+ hintAllowedMethods: false,
});
```

## Version 27

Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -920,7 +920,7 @@ it normalizes errors into consistent HTTP responses with sensible status codes.
- Routing, parsing and upload issues:
- Handled by `ResultHandler` configured as `errorHandler` (the defaults is `defaultResultHandler`);
- Parsing errors: passed through as-is (typically `HttpError` with `4XX` code used for response by default);
- Routing errors: `404` or `405`, based on `wrongMethodBehavior` configuration;
- Routing errors: `404` or `405`, based on `hintAllowedMethods` configuration;
- Upload issues: thrown only if `upload.limitError` is configured (`HttpError::statusCode` can be used for response);
- For other errors the default status code is `500`;
- `ResultHandler` failures:
Expand Down
9 changes: 4 additions & 5 deletions compat-test/migration.spec.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
// import { readFile } from "node:fs/promises";
import { describe, test } from "vitest";
import { readFile } from "node:fs/promises";
import { describe, test, expect } from "vitest";

/** @todo update when migration ready */
describe("Migration", () => {
test("should migrate", async () => {
// const fixed = await readFile("./sample.ts", "utf-8");
// expect(fixed).toBe();
const fixed = await readFile("./sample.ts", "utf-8");
expect(fixed.split("\n")[0]).toBe(`createConfig({ hintAllowedMethods: false });`);
});
});
2 changes: 1 addition & 1 deletion compat-test/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"type": "module",
"private": true,
"scripts": {
"pretest": "echo 'new Integration();' > sample.ts",
"pretest": "echo 'createConfig({ wrongMethodBehavior: 404 });' > sample.ts",
"test": "eslint --fix && vitest --run",
"posttest": "rm sample.ts"
},
Expand Down
12 changes: 6 additions & 6 deletions express-zod-api/src/config-type.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,12 +40,12 @@ export interface CommonConfig {
*/
cors: boolean | HeadersProvider;
/**
* @desc How to respond to a request that uses a wrong method to an existing endpoint
* @example 404Not found
* @example 405Method not allowed, incl. the "Allow" header with a list of methods
* @default 405
* */
wrongMethodBehavior?: 404 | 405;
* @desc Controls how to respond to a request to an existing endpoint with an invalid HTTP method.
* @example truerespond with status code 405 and "Allow" header containing a list of valid methods
* @example falserespond with status code 404 (Not found)
* @default true
*/
hintAllowedMethods?: boolean;
/**
* @desc How to treat Routing keys that look like methods (when assigned with an Endpoint)
* @see Method
Expand Down
2 changes: 1 addition & 1 deletion express-zod-api/src/routing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ export const initRouting = ({ app, config, getLogger, ...rest }: InitProps) => {
}
app[method](path, ...handlers);
}
if (config.wrongMethodBehavior === 404) continue;
if (config.hintAllowedMethods === false) continue;
deprioritized.set(path, createWrongMethodHandler(accessMethods));
}
for (const [path, handler] of deprioritized) app.all(path, handler);
Expand Down
8 changes: 4 additions & 4 deletions express-zod-api/tests/routing.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,14 +31,14 @@ describe("Routing", () => {
vi.clearAllMocks(); // resets call counters on mocked methods
});

test.each([404, 405] as const)(
test.each([true, false, undefined])(
"Should set right methods %#",
(wrongMethodBehavior) => {
(hintAllowedMethods) => {
const handlerMock = vi.fn();
const configMock = {
cors: true,
startupLogo: false,
wrongMethodBehavior,
hintAllowedMethods,
methodLikeRouteBehavior: "path" as const,
};
const factory = new EndpointsFactory(defaultResultHandler);
Expand Down Expand Up @@ -85,7 +85,7 @@ describe("Routing", () => {
expect(appMock.options.mock.calls[0][0]).toBe("/v1/user/get");
expect(appMock.options.mock.calls[1][0]).toBe("/v1/user/set");
expect(appMock.options.mock.calls[2][0]).toBe("/v1/user/universal");
if (wrongMethodBehavior !== 405) return;
if (hintAllowedMethods === false) return;
expect(appMock.all).toHaveBeenCalledTimes(3);
expect(appMock.all.mock.calls[0][0]).toBe("/v1/user/get");
expect(appMock.all.mock.calls[1][0]).toBe("/v1/user/set");
Expand Down
50 changes: 48 additions & 2 deletions migration/index.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,53 @@ describe("Migration", async () => {
});

tester.run(ruleName, theRule, {
valid: [`new Integration({ typescript, config, routing });`],
invalid: [],
valid: [`createConfig({ hintAllowedMethods: false });`],
invalid: [
{
name: "wrongMethodBehavior=404",
code: `createConfig({ wrongMethodBehavior: 404 });`,
output: `createConfig({ hintAllowedMethods: false });`,
errors: [
{
messageId: "change",
data: {
subject: "property",
from: "wrongMethodBehavior",
to: "hintAllowedMethods",
},
},
],
},
{
name: "wrongMethodBehavior=405",
code: `createConfig({ wrongMethodBehavior: 405 });`,
output: `createConfig({ hintAllowedMethods: true });`,
errors: [
{
messageId: "change",
data: {
subject: "property",
from: "wrongMethodBehavior",
to: "hintAllowedMethods",
},
},
],
},
{
name: "wrongMethodBehavior=undefined",
code: `createConfig({ wrongMethodBehavior: undefined });`,
output: `createConfig({ hintAllowedMethods: undefined });`,
errors: [
{
messageId: "change",
data: {
subject: "property",
from: "wrongMethodBehavior",
to: "hintAllowedMethods",
},
},
],
},
],
});
});
47 changes: 40 additions & 7 deletions migration/index.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,28 @@
import {
ESLintUtils,
// AST_NODE_TYPES as NT,
AST_NODE_TYPES as NT,
type TSESLint,
// type TSESTree,
type TSESTree,
} from "@typescript-eslint/utils"; // eslint-disable-line allowed/dependencies -- assumed transitive dependency

/*
type NamedProp = TSESTree.PropertyNonComputedName & {
key: TSESTree.Identifier | TSESTree.StringLiteral;
};

interface Queries {}
interface Queries {
wrongMethodBehavior: NamedProp;
}

type Listener = keyof Queries;

const queries: Record<Listener, string> = {};
const queries: Record<Listener, string> = {
wrongMethodBehavior:
`${NT.CallExpression}[callee.name="createConfig"] > ` +
`${NT.ObjectExpression} > ` +
`${NT.Property}[key.name="wrongMethodBehavior"]`,
};

/*
const isNamedProp = (prop: TSESTree.ObjectLiteralElement): prop is NamedProp =>
prop.type === NT.Property &&
!prop.computed &&
Expand All @@ -24,6 +31,8 @@ const isNamedProp = (prop: TSESTree.ObjectLiteralElement): prop is NamedProp =>

const getPropName = (prop: NamedProp): string =>
prop.key.type === NT.Identifier ? prop.key.name : prop.key.value;
*/

const listen = <
S extends { [K in Listener]: TSESLint.RuleFunction<Queries[K]> },
>(
Expand All @@ -36,7 +45,6 @@ const listen = <
}),
{},
);
*/

const ruleName = `v${process.env.TSDOWN_VERSION?.split(".")[0] ?? "0"}`; // fail-safe for bumpp

Expand All @@ -54,7 +62,32 @@ const theRule = ESLintUtils.RuleCreator.withoutDocs({
},
defaultOptions: [],
},
create: () => ({}), // (ctx) => listen({}),
create: (ctx) =>
listen({
wrongMethodBehavior: (node) => {
const value = node.value;
const newKey = "hintAllowedMethods";
let newValue: string;
if (value.type === NT.Literal && typeof value.value === "number")
newValue = value.value === 405 ? "true" : "false";
else if (value.type === NT.Identifier && value.name === "undefined")
newValue = "undefined";
else return;
ctx.report({
node,
messageId: "change",
data: {
subject: "property",
from: "wrongMethodBehavior",
to: newKey,
},
fix: (fixer) => [
fixer.replaceText(node.key, newKey),
fixer.replaceText(value, newValue),
],
});
},
}),
});

export default {
Expand Down
Loading