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

Issue:
Middlewares do not run for the OPTIONS request
Doing `.addExpressMiddleware(cors())` has no effect since it's not run for the OPTIONS request. This example is also in Readme, so it should be fixed.

_Originally posted by @HardCoreQual in https://github.com/RobinTail/express-zod-api/discussions/500#discussioncomment-3186884_

> Can be bug that
`ExpressZodEndpoidFactory.addExpressMiddleware(cors()) `  don't  work         // createServer
and is necessary to use `expressApp.use(cors())`                                               // attachRouting
or it work like expected ?


Pull request description:
Run native express middlewares for OPTIONS request
Fixes #514 

Patch:
diff --git a/src/common-helpers.ts b/src/common-helpers.ts
index bbeedbb1..d0bef2d9 100644
--- a/src/common-helpers.ts
+++ b/src/common-helpers.ts
@@ -8,7 +8,7 @@ import {
   loggerLevels,
 } from "./config-type";
 import { copyMeta, getMeta } from "./metadata";
-import { Method } from "./method";
+import { AuxMethod, Method } from "./method";
 import { AnyMiddlewareDef } from "./middleware";
 import { mimeMultipart } from "./mime";
 import { ZodUpload } from "./upload-schema";
@@ -74,11 +74,17 @@ export const defaultInputSources: InputSources = {
 };
 const fallbackInputSource = defaultInputSources.delete;
 
+export const getActualMethod = (request: Request) =>
+  request.method.toLowerCase() as Method | AuxMethod;
+
 export function getInitialInput(
   request: Request,
   inputAssignment: CommonConfig["inputSources"]
 ): any {
-  const method = request.method.toLowerCase() as Method;
+  const method = getActualMethod(request);
+  if (method === "options") {
+    return {};
+  }
   let props = fallbackInputSource;
   if (method in defaultInputSources) {
     props = defaultInputSources[method];
diff --git a/src/endpoint.ts b/src/endpoint.ts
index a6ef6199..6246c63d 100644
--- a/src/endpoint.ts
+++ b/src/endpoint.ts
@@ -4,7 +4,12 @@ import { z } from "zod";
 import { ApiResponse } from "./api-response";
 import { CommonConfig } from "./config-type";
 import { ResultHandlerError } from "./errors";
-import { FlatObject, getInitialInput, IOSchema } from "./common-helpers";
+import {
+  FlatObject,
+  getActualMethod,
+  getInitialInput,
+  IOSchema,
+} from "./common-helpers";
 import { AuxMethod, Method, MethodsDefinition } from "./method";
 import { AnyMiddlewareDef } from "./middleware";
 import { lastResortHandler, ResultHandlerDefinition } from "./result-handler";
@@ -162,11 +167,13 @@ export class Endpoint<
   }
 
   async #runMiddlewares({
+    method,
     input,
     request,
     response,
     logger,
   }: {
+    method: Method | AuxMethod;
     input: any;
     request: Request;
     response: Response;
@@ -175,6 +182,9 @@ export class Endpoint<
     const options: any = {};
     let isStreamClosed = false;
     for (const def of this.middlewares) {
+      if (method === "options" && def.type === "proprietary") {
+        continue;
+      }
       Object.assign(input, await def.input.parseAsync(input)); // middleware can transform the input types
       Object.assign(
         options,
@@ -261,6 +271,7 @@ export class Endpoint<
     logger: Logger;
     config: CommonConfig;
   }) {
+    const method = getActualMethod(request);
     let output: any;
     let error: Error | null = null;
     if (config.cors) {
@@ -277,13 +288,10 @@ export class Endpoint<
         response.set(key, headers[key]);
       }
     }
-    if (request.method === "OPTIONS") {
-      response.status(200).end();
-      return;
-    }
     const initialInput = getInitialInput(request, config.inputSources);
     try {
       const { input, options, isStreamClosed } = await this.#runMiddlewares({
+        method,
         input: { ...initialInput }, // preserve the initial
         request,
         response,
@@ -292,6 +300,10 @@ export class Endpoint<
       if (isStreamClosed) {
         return;
       }
+      if (method === "options") {
+        response.status(200).end();
+        return;
+      }
       output = await this.#parseOutput(
         await this.#parseAndRunHandler({ input, options, logger })
       );
diff --git a/src/endpoints-factory.ts b/src/endpoints-factory.ts
index ebe19358..789fe78e 100644
--- a/src/endpoints-factory.ts
+++ b/src/endpoints-factory.ts
@@ -15,6 +15,7 @@ import {
   createMiddleware,
   ExpressMiddleware,
   ExpressMiddlewareFeatures,
+  MiddlewareCreationProps,
   MiddlewareDefinition,
 } from "./middleware";
 import { mimeJson, mimeMultipart } from "./mime";
@@ -62,6 +63,15 @@ export class EndpointsFactory<
 
   public addMiddleware<AIN extends IOSchema<"strip">, AOUT extends FlatObject>(
     definition: MiddlewareDefinition<AIN, OUT, AOUT>
+  ): EndpointsFactory<POS, NEG, ProbableIntersection<IN, AIN>, OUT & AOUT>;
+  /** @deprecated please use createMiddleware() for the argument */
+  public addMiddleware<AIN extends IOSchema<"strip">, AOUT extends FlatObject>(
+    props: MiddlewareCreationProps<AIN, OUT, AOUT>
+  ): EndpointsFactory<POS, NEG, ProbableIntersection<IN, AIN>, OUT & AOUT>;
+  public addMiddleware<AIN extends IOSchema<"strip">, AOUT extends FlatObject>(
+    subject:
+      | MiddlewareDefinition<AIN, OUT, AOUT>
+      | MiddlewareCreationProps<AIN, OUT, AOUT>
   ) {
     return EndpointsFactory.#create<
       POS,
@@ -69,7 +79,9 @@ export class EndpointsFactory<
       ProbableIntersection<IN, AIN>,
       OUT & AOUT
     >(
-      this.middlewares.concat(definition as unknown as AnyMiddlewareDef),
+      this.middlewares.concat(
+        "type" in subject ? subject : createMiddleware(subject)
+      ),
       this.resultHandler
     );
   }
@@ -86,7 +98,8 @@ export class EndpointsFactory<
   ) {
     const transformer = features?.transformer || ((err: Error) => err);
     const provider = features?.provider || (() => ({} as AOUT));
-    const definition = createMiddleware({
+    const definition: AnyMiddlewareDef = {
+      type: "express",
       input: z.object({}),
       middleware: async ({ request, response }) =>
         new Promise<AOUT>((resolve, reject) => {
@@ -98,9 +111,9 @@ export class EndpointsFactory<
           };
           middleware(request as R, response as S, next);
         }),
-    });
+    };
     return EndpointsFactory.#create<POS, NEG, IN, OUT & AOUT>(
-      this.middlewares.concat(definition as AnyMiddlewareDef),
+      this.middlewares.concat(definition),
       this.resultHandler
     );
   }
@@ -111,7 +124,7 @@ export class EndpointsFactory<
         createMiddleware({
           input: z.object({}),
           middleware: async () => options,
-        }) as AnyMiddlewareDef
+        })
       ),
       this.resultHandler
     );
diff --git a/src/middleware.ts b/src/middleware.ts
index 654a2865..4ad42fc8 100644
--- a/src/middleware.ts
+++ b/src/middleware.ts
@@ -16,7 +16,7 @@ type Middleware<IN, OPT, OUT> = (
   params: MiddlewareParams<IN, OPT>
 ) => Promise<OUT>;
 
-export interface MiddlewareDefinition<
+export interface MiddlewareCreationProps<
   IN extends IOSchema<"strip">,
   OPT,
   OUT extends FlatObject
@@ -25,19 +25,26 @@ export interface MiddlewareDefinition<
   middleware: Middleware<z.output<IN>, OPT, OUT>;
 }
 
-export type AnyMiddlewareDef = MiddlewareDefinition<
-  IOSchema<"strip">,
-  any,
-  any
->;
+export interface MiddlewareDefinition<
+  IN extends IOSchema<"strip">,
+  OPT,
+  OUT extends FlatObject
+> extends MiddlewareCreationProps<IN, OPT, OUT> {
+  type: "proprietary" | "express";
+}
+
+export type AnyMiddlewareDef = MiddlewareDefinition<any, any, any>;
 
 export const createMiddleware = <
   IN extends IOSchema<"strip">,
   OPT,
   OUT extends FlatObject
 >(
-  definition: MiddlewareDefinition<IN, OPT, OUT>
-) => definition;
+  props: MiddlewareCreationProps<IN, OPT, OUT>
+): MiddlewareDefinition<IN, OPT, OUT> => ({
+  ...props,
+  type: "proprietary",
+});
 
 export type ExpressMiddleware<R extends Request, S extends Response> = (
   request: R,


Test patch:
diff --git a/tests/system/system.spec.ts b/tests/system/system.spec.ts
index 14ba52a2..c7b0aeb8 100644
--- a/tests/system/system.spec.ts
+++ b/tests/system/system.spec.ts
@@ -9,6 +9,7 @@ import {
   defaultResultHandler,
   createResultHandler,
   createApiResponse,
+  createMiddleware,
 } from "../../src";
 import { waitFor } from "../helpers";
 
@@ -55,23 +56,27 @@ describe("App", () => {
           }),
         }),
         test: new EndpointsFactory(defaultResultHandler)
-          .addMiddleware({
-            input: z.object({
-              key: z.string().refine((v) => v === "123", "Invalid key"),
-            }),
-            middleware: async () => ({
-              user: {
-                id: 354,
-              },
-            }),
-          })
-          .addMiddleware({
-            input: z.object({}),
-            middleware: async ({ request, options: { user } }) => ({
-              method: request.method.toLowerCase() as Method,
-              permissions: user.id === 354 ? ["any"] : [],
-            }),
-          })
+          .addMiddleware(
+            createMiddleware({
+              input: z.object({
+                key: z.string().refine((v) => v === "123", "Invalid key"),
+              }),
+              middleware: async () => ({
+                user: {
+                  id: 354,
+                },
+              }),
+            })
+          )
+          .addMiddleware(
+            createMiddleware({
+              input: z.object({}),
+              middleware: async ({ request, options: { user } }) => ({
+                method: request.method.toLowerCase() as Method,
+                permissions: user.id === 354 ? ["any"] : [],
+              }),
+            })
+          )
           .build({
             methods: ["get", "post"],
             input: z.object({
diff --git a/tests/unit/common-helpers.spec.ts b/tests/unit/common-helpers.spec.ts
index 7f725461..6bb8d664 100644
--- a/tests/unit/common-helpers.spec.ts
+++ b/tests/unit/common-helpers.spec.ts
@@ -31,7 +31,7 @@ describe("Common Helpers", () => {
     });
 
     test("Should merge input object schemas", () => {
-      const middlewares = [
+      const middlewares: AnyMiddlewareDef[] = [
         createMiddleware({
           input: z.object({
             one: z.string(),
@@ -50,7 +50,7 @@ describe("Common Helpers", () => {
           }),
           middleware: jest.fn(),
         }),
-      ] as unknown as AnyMiddlewareDef[];
+      ];
       const endpointInput = z.object({
         four: z.boolean(),
       });
@@ -60,7 +60,7 @@ describe("Common Helpers", () => {
     });
 
     test("Should merge union object schemas", () => {
-      const middlewares = [
+      const middlewares: AnyMiddlewareDef[] = [
         createMiddleware({
           input: z
             .object({
@@ -85,7 +85,7 @@ describe("Common Helpers", () => {
             ),
           middleware: jest.fn(),
         }),
-      ] as unknown as AnyMiddlewareDef[];
+      ];
       const endpointInput = z
         .object({
           five: z.string(),
@@ -101,7 +101,7 @@ describe("Common Helpers", () => {
     });
 
     test("Should merge intersection object schemas", () => {
-      const middlewares = [
+      const middlewares: AnyMiddlewareDef[] = [
         createMiddleware({
           input: z
             .object({
@@ -126,7 +126,7 @@ describe("Common Helpers", () => {
             ),
           middleware: jest.fn(),
         }),
-      ] as unknown as AnyMiddlewareDef[];
+      ];
       const endpointInput = z
         .object({
           five: z.string(),
@@ -142,7 +142,7 @@ describe("Common Helpers", () => {
     });
 
     test("Should merge mixed object schemas", () => {
-      const middlewares = [
+      const middlewares: AnyMiddlewareDef[] = [
         createMiddleware({
           input: z
             .object({
@@ -167,7 +167,7 @@ describe("Common Helpers", () => {
             ),
           middleware: jest.fn(),
         }),
-      ] as unknown as AnyMiddlewareDef[];
+      ];
       const endpointInput = z.object({
         five: z.string(),
       });
@@ -177,7 +177,7 @@ describe("Common Helpers", () => {
     });
 
     test("Should merge examples in case of using withMeta()", () => {
-      const middlewares = [
+      const middlewares: AnyMiddlewareDef[] = [
         createMiddleware({
           input: withMeta(
             z
@@ -212,7 +212,7 @@ describe("Common Helpers", () => {
           }),
           middleware: jest.fn(),
         }),
-      ] as unknown as AnyMiddlewareDef[];
+      ];
       const endpointInput = withMeta(
         z.object({
           five: z.string(),
@@ -378,6 +378,11 @@ describe("Common Helpers", () => {
         b: "url param",
       });
     });
+    test("Issue 514: should return empty object for OPTIONS", () => {
+      expect(
+        getInitialInput({ method: "OPTIONS" } as unknown as Request, undefined)
+      ).toEqual({});
+    });
   });
 
   describe("isLoggerConfig()", () => {
diff --git a/tests/unit/endpoint.spec.ts b/tests/unit/endpoint.spec.ts
index c0b68b65..c3fb981d 100644
--- a/tests/unit/endpoint.spec.ts
+++ b/tests/unit/endpoint.spec.ts
@@ -376,12 +376,14 @@ describe("Endpoint", () => {
   describe("Issue #269: Async refinements", () => {
     test("should handle async refinements in input, output and middleware", async () => {
       const endpoint = new EndpointsFactory(defaultResultHandler)
-        .addMiddleware({
-          input: z.object({
-            m: z.number().refine(async (m) => m < 10),
-          }),
-          middleware: async () => ({}),
-        })
+        .addMiddleware(
+          createMiddleware({
+            input: z.object({
+              m: z.number().refine(async (m) => m < 10),
+            }),
+            middleware: async () => ({}),
+          })
+        )
         .build({
           methods: ["post"],
           input: z.object({
@@ -409,4 +411,42 @@ describe("Endpoint", () => {
       });
     });
   });
+
+  describe("Issue #514: Express native middlewares for OPTIONS request", () => {
+    test("should skip proprietary ones", async () => {
+      const endpoint = new EndpointsFactory(defaultResultHandler)
+        .addMiddleware({
+          // testing also the backward compatibility (without createMiddleware)
+          input: z.object({
+            shouldNotBeHere: z.boolean(),
+          }),
+          middleware: async () => {
+            throw new Error("Should not be here");
+          },
+        })
+        .addExpressMiddleware((req, res, next) => {
+          res.set("X-Custom-Header", "test");
+          next();
+        })
+        .build({
+          methods: ["post"],
+          input: z.object({
+            shouldNotBeThere: z.boolean(),
+          }),
+          output: z.object({
+            shouldNotComeHereAsWell: z.boolean(),
+          }),
+          handler: async () => ({ shouldNotComeHereAsWell: true }),
+        });
+      const { responseMock } = await testEndpoint({
+        endpoint,
+        requestProps: {
+          method: "OPTIONS",
+        },
+      });
+      expect(responseMock.status).toHaveBeenCalledWith(200);
+      expect(responseMock.json).toHaveBeenCalledTimes(0);
+      expect(responseMock.set).toHaveBeenCalledWith("X-Custom-Header", "test");
+    });
+  });
 });
diff --git a/tests/unit/middleware.spec.ts b/tests/unit/middleware.spec.ts
index 7e767be9..b4ba0482 100644
--- a/tests/unit/middleware.spec.ts
+++ b/tests/unit/middleware.spec.ts
@@ -2,7 +2,7 @@ import { createMiddleware, z } from "../../src";
 
 describe("Middleware", () => {
   describe("createMiddleware()", () => {
-    test("Should simply return its argument", () => {
+    test("Should simply return the middleware of the proprietary type", () => {
       const definition = {
         input: z.object({
           something: z.number(),
@@ -10,7 +10,7 @@ describe("Middleware", () => {
         middleware: jest.fn(),
       };
       const middleware = createMiddleware(definition);
-      expect(middleware).toStrictEqual(definition);
+      expect(middleware).toStrictEqual({ ...definition, type: "proprietary" });
     });
   });
 });
diff --git a/tests/unit/open-api.spec.ts b/tests/unit/open-api.spec.ts
index cbbe1d8d..07ce98ac 100644
--- a/tests/unit/open-api.spec.ts
+++ b/tests/unit/open-api.spec.ts
@@ -5,6 +5,7 @@ import {
   defaultEndpointsFactory,
   withMeta,
   createConfig,
+  createMiddleware,
 } from "../../src";
 import { expectType } from "tsd";
 
@@ -752,16 +753,18 @@ describe("Open API generator", () => {
         routing: {
           v1: {
             getSomething: defaultEndpointsFactory
-              .addMiddleware({
-                input: withMeta(
-                  z.object({
-                    key: z.string(),
-                  })
-                ).example({
-                  key: "1234-56789-01",
-                }),
-                middleware: jest.fn(),
-              })
+              .addMiddleware(
+                createMiddleware({
+                  input: withMeta(
+                    z.object({
+                      key: z.string(),
+                    })
+                  ).example({
+                    key: "1234-56789-01",
+                  }),
+                  middleware: jest.fn(),
+                })
+              )
               .build({
                 method: "post",
                 input: withMeta(