Repository: RobinTail/express-zod-api (ts)

Issue:
`DELETE` endpoints with `requestBody` leads to an OpenAPI semantic error
It looks like OpenAPI spec forbids a request body for `DELETE` operations:
- According to https://www.rfc-editor.org/rfc/rfc9110.html#name-delete:
> Although request message framing is independent of the method used, content received in a DELETE request has no generally defined semantics, cannot alter the meaning or target of the request, and might lead some implementations to reject the request and close the connection because of its potential as a request smuggling attack
- According to https://stackoverflow.com/a/54980985:
> No, you cannot use the OpenAPI 3.0 Specification and Swagger tools to implement DELETE requests with a request body

---
### Steps to reproduce:
- add endpoint with `delete` method like this:
```
defaultEndpointsFactory.build({
  methods: ["delete"],
  input: z.object({}),
...
```
- call `new OpenAPI({...}).getSpecAsYaml()`
- validate generated spec (e.g. on https://editor.swagger.io/)

### Actual behaviour:
There is a semantic error `DELETE operations cannot have a requestBody` in OpenAPI spec

### Expected behaviour
OpenApi spec generates without `requestBody` for DELETE operations


Pull request:
Remove request body from `DELETE` operations in OpenAPI spec
This PR fixes #820


Patch:
diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml
index 15aa773b..9e660c9f 100644
--- a/.github/workflows/codeql-analysis.yml
+++ b/.github/workflows/codeql-analysis.yml
@@ -13,10 +13,10 @@ name: "CodeQL"
 
 on:
   push:
-    branches: [ master, v7 ]
+    branches: [ master, v9-beta ]
   pull_request:
     # The branches below must be a subset of the branches above
-    branches: [ master, v7 ]
+    branches: [ master, v9-beta ]
   schedule:
     - cron: '26 8 * * 1'
 
diff --git a/.github/workflows/node.js.yml b/.github/workflows/node.js.yml
index 083f85fb..86b97490 100644
--- a/.github/workflows/node.js.yml
+++ b/.github/workflows/node.js.yml
@@ -5,9 +5,9 @@ name: Node.js CI
 
 on:
   push:
-    branches: [ master, v7 ]
+    branches: [ master, v9-beta ]
   pull_request:
-    branches: [ master, v7 ]
+    branches: [ master, v9-beta ]
 
 jobs:
   build:
diff --git a/.github/workflows/swagger.yml b/.github/workflows/swagger.yml
index 1f0f6b20..20a028f1 100644
--- a/.github/workflows/swagger.yml
+++ b/.github/workflows/swagger.yml
@@ -2,9 +2,9 @@ name: OpenAPI Validation
 
 on:
   push:
-    branches: [ master, v7 ]
+    branches: [ master, v9-beta ]
   pull_request:
-    branches: [ master, v7 ]
+    branches: [ master, v9-beta ]
 
 
 jobs:
diff --git a/src/common-helpers.ts b/src/common-helpers.ts
index 0600d5f7..3da71b5e 100644
--- a/src/common-helpers.ts
+++ b/src/common-helpers.ts
@@ -3,6 +3,7 @@ import { HttpError } from "http-errors";
 import { z } from "zod";
 import {
   CommonConfig,
+  InputSource,
   InputSources,
   LoggerConfig,
   loggerLevels,
@@ -33,9 +34,9 @@ export const defaultInputSources: InputSources = {
   post: ["body", "params", "files"],
   put: ["body", "params"],
   patch: ["body", "params"],
-  delete: ["body", "query", "params"],
+  delete: ["query", "params"],
 };
-const fallbackInputSource = defaultInputSources.delete;
+const fallbackInputSource: InputSource[] = ["body", "query", "params"];
 
 export const getActualMethod = (request: Request) =>
   request.method.toLowerCase() as Method | AuxMethod;
diff --git a/src/config-type.ts b/src/config-type.ts
index 97b72dc9..812393ff 100644
--- a/src/config-type.ts
+++ b/src/config-type.ts
@@ -55,7 +55,10 @@ export interface AppConfig {
   app: Express; // or your custom express app
 }
 
-type InputSource = keyof Pick<Request, "query" | "body" | "files" | "params">;
+export type InputSource = keyof Pick<
+  Request,
+  "query" | "body" | "files" | "params"
+>;
 export type InputSources = Record<Method, InputSource[]>;
 
 type Headers = Record<string, string>;
diff --git a/src/open-api-helpers.ts b/src/open-api-helpers.ts
index 1b756cd6..938b228e 100644
--- a/src/open-api-helpers.ts
+++ b/src/open-api-helpers.ts
@@ -24,7 +24,7 @@ import {
   routePathParamsRegex,
   tryToTransform,
 } from "./common-helpers";
-import { InputSources, TagsConfig } from "./config-type";
+import { InputSource, TagsConfig } from "./config-type";
 import { ZodDateIn, isoDateRegex } from "./date-in-schema";
 import { ZodDateOut } from "./date-out-schema";
 import { AbstractEndpoint } from "./endpoint";
@@ -575,7 +575,7 @@ export const depictRequestParams = ({
   endpoint,
   inputSources,
 }: ReqResDepictHelperCommonProps & {
-  inputSources: InputSources[Method];
+  inputSources: InputSource[];
 }): ParameterObject[] => {
   const schema = endpoint.getInputSchema();
   const shape = extractObjectSchema(schema).shape;


Test patch:
diff --git a/tests/unit/__snapshots__/common-helpers.spec.ts.snap b/tests/unit/__snapshots__/common-helpers.spec.ts.snap
index e89b0c61..4d182225 100644
--- a/tests/unit/__snapshots__/common-helpers.spec.ts.snap
+++ b/tests/unit/__snapshots__/common-helpers.spec.ts.snap
@@ -3,7 +3,6 @@
 exports[`Common Helpers defaultInputSources should be declared in a certain way 1`] = `
 {
   "delete": [
-    "body",
     "query",
     "params",
   ],
diff --git a/tests/unit/__snapshots__/open-api.spec.ts.snap b/tests/unit/__snapshots__/open-api.spec.ts.snap
index c70afc58..99631825 100644
--- a/tests/unit/__snapshots__/open-api.spec.ts.snap
+++ b/tests/unit/__snapshots__/open-api.spec.ts.snap
@@ -1145,6 +1145,84 @@ servers:
 "
 `;
 
+exports[`Open API generator generateOpenApi() should generate the correct schema for DELETE request without body 1`] = `
+"openapi: 3.0.0
+info:
+  title: Testing DELETE request without body
+  version: 3.4.5
+paths:
+  /v1/deleteSomething:
+    delete:
+      responses:
+        "200":
+          description: DELETE /v1/deleteSomething Successful response
+          content:
+            application/json:
+              schema:
+                type: object
+                properties:
+                  status:
+                    type: string
+                    enum:
+                      - success
+                  data:
+                    type: object
+                    properties:
+                      whatever:
+                        type: number
+                        format: double
+                        minimum: 5e-324
+                        exclusiveMinimum: false
+                        maximum: 1.7976931348623157e+308
+                        exclusiveMaximum: false
+                    required:
+                      - whatever
+                required:
+                  - status
+                  - data
+        "400":
+          description: DELETE /v1/deleteSomething Error response
+          content:
+            application/json:
+              schema:
+                type: object
+                properties:
+                  status:
+                    type: string
+                    enum:
+                      - error
+                  error:
+                    type: object
+                    properties:
+                      message:
+                        type: string
+                    required:
+                      - message
+                required:
+                  - status
+                  - error
+              examples:
+                example1:
+                  value:
+                    status: error
+                    error:
+                      message: Sample error message
+components:
+  schemas: {}
+  responses: {}
+  parameters: {}
+  examples: {}
+  requestBodies: {}
+  headers: {}
+  securitySchemes: {}
+  links: {}
+  callbacks: {}
+tags: []
+servers:
+  - url: http://example.com
+"
+`;
+
 exports[`Open API generator generateOpenApi() should generate the correct schema for complex types 1`] = `
 "openapi: 3.0.0
 info:
diff --git a/tests/unit/common-helpers.spec.ts b/tests/unit/common-helpers.spec.ts
index fddd8feb..3aa6033b 100644
--- a/tests/unit/common-helpers.spec.ts
+++ b/tests/unit/common-helpers.spec.ts
@@ -83,7 +83,7 @@ describe("Common Helpers", () => {
         param: 123,
       });
     });
-    test("should return both body and query for DELETE and unknown requests by default", () => {
+    test("should return only query for DELETE requests by default", () => {
       expect(
         getInput(
           {
@@ -93,6 +93,20 @@ describe("Common Helpers", () => {
           } as unknown as Request,
           undefined
         )
+      ).toEqual({
+        a: "query",
+      });
+    });
+    test("should return body and query for unknown requests by default", () => {
+      expect(
+        getInput(
+          {
+            query: { a: "query" },
+            body: { b: "body" },
+            method: "UNSUPPORTED",
+          } as unknown as Request,
+          undefined
+        )
       ).toEqual({
         a: "query",
         b: "body",
diff --git a/tests/unit/open-api.spec.ts b/tests/unit/open-api.spec.ts
index 5344d536..f58f6905 100644
--- a/tests/unit/open-api.spec.ts
+++ b/tests/unit/open-api.spec.ts
@@ -31,6 +31,30 @@ describe("Open API generator", () => {
       expect(spec).toMatchSnapshot();
     });
 
+    test("should generate the correct schema for DELETE request without body", () => {
+      const spec = new OpenAPI({
+        routing: {
+          v1: {
+            deleteSomething: defaultEndpointsFactory.build({
+              methods: ["delete"],
+              input: z.object({}),
+              output: z.object({
+                whatever: z.number(),
+              }),
+              handler: async () => ({
+                whatever: 42,
+              }),
+            }),
+          },
+        },
+        config: sampleConfig,
+        version: "3.4.5",
+        title: "Testing DELETE request without body",
+        serverUrl: "http://example.com",
+      }).getSpecAsYaml();
+      expect(spec).toMatchSnapshot();
+    });
+
     test("should generate the correct schema for complex types", () => {
       const literalValue = "something" as const;
       const spec = new OpenAPI({