Repository: RobinTail/express-zod-api
Pull request #675 (2022-10-28 19:12:40)

v7: cherry-pick: fix of #673
Cherry-picking the fix of #673 


Patch:
diff --git a/.github/workflows/npm-publish.yml b/.github/workflows/npm-publish.yml
index 25090d05..21b0936d 100644
--- a/.github/workflows/npm-publish.yml
+++ b/.github/workflows/npm-publish.yml
@@ -14,6 +14,6 @@ jobs:
           node-version: 16
           registry-url: https://registry.npmjs.org/
       - run: yarn install
-      - run: npm publish
+      - run: npm publish --tag v7
         env:
           NODE_AUTH_TOKEN: ${{secrets.NPM_KEY}}
diff --git a/CHANGELOG.md b/CHANGELOG.md
index bd8b7b07..31dd778b 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,16 @@
 
 ## Version 7
 
+### v7.9.3
+
+- This version is a cherry-pick of the fix made in v8.3.2.
+- Fixed the bug #673 found and reported by [@shroudedcode](https://github.com/shroudedcode).
+  - Preventing double parsing of incoming data by input schemas of middlewares containing transformations.
+  - The bug caused inability of using any transforming schema in middlewares.
+  - In particular, but not limited with: using `z.dateIn()` in middlewares.
+    - Sample error message in this case: `Expected string, received date`.
+  - Using `.transform()` method in middlewares was also affected by this bug.
+
 ### v7.9.2
 
 - Fixed issue #585 found and reported along with a suggested solution by [@rayzr522](https://github.com/rayzr522).
diff --git a/src/common-helpers.ts b/src/common-helpers.ts
index 5a06abd7..3cb95c63 100644
--- a/src/common-helpers.ts
+++ b/src/common-helpers.ts
@@ -77,7 +77,7 @@ const fallbackInputSource = defaultInputSources.delete;
 export const getActualMethod = (request: Request) =>
   request.method.toLowerCase() as Method | AuxMethod;
 
-export function getInitialInput(
+export function getInput(
   request: Request,
   inputAssignment: CommonConfig["inputSources"]
 ): any {
diff --git a/src/endpoint.ts b/src/endpoint.ts
index 6616675c..4e755240 100644
--- a/src/endpoint.ts
+++ b/src/endpoint.ts
@@ -7,8 +7,8 @@ import { ResultHandlerError } from "./errors";
 import {
   FlatObject,
   getActualMethod,
-  getInitialInput,
   IOSchema,
+  getInput,
   makeErrorFromAnything,
 } from "./common-helpers";
 import { combineContainers, LogicalContainer } from "./logical-container";
@@ -197,7 +197,7 @@ export class Endpoint<
     logger,
   }: {
     method: Method | AuxMethod;
-    input: any;
+    input: Readonly<any>; // Issue #673: input is immutable, since this.inputSchema is combined with ones of middlewares
     request: Request;
     response: Response;
     logger: Logger;
@@ -208,11 +208,10 @@ export class Endpoint<
       if (method === "options" && def.type === "proprietary") {
         continue;
       }
-      Object.assign(input, await def.input.parseAsync(input)); // middleware can transform the input types
       Object.assign(
         options,
         await def.middleware({
-          input,
+          input: await def.input.parseAsync(input),
           options,
           request,
           response,
@@ -228,7 +227,7 @@ export class Endpoint<
         break;
       }
     }
-    return { input, options, isStreamClosed };
+    return { options, isStreamClosed };
   }
 
   async #parseAndRunHandler({
@@ -236,7 +235,7 @@ export class Endpoint<
     options,
     logger,
   }: {
-    input: any;
+    input: Readonly<any>;
     options: any;
     logger: Logger;
   }) {
@@ -253,14 +252,14 @@ export class Endpoint<
     request,
     response,
     logger,
-    initialInput,
+    input,
     output,
   }: {
     error: Error | null;
     request: Request;
     response: Response;
     logger: Logger;
-    initialInput: any;
+    input: any;
     output: any;
   }) {
     try {
@@ -270,7 +269,7 @@ export class Endpoint<
         request,
         response,
         logger,
-        input: initialInput,
+        input,
       });
     } catch (e) {
       lastResortHandler({
@@ -309,11 +308,11 @@ export class Endpoint<
         response.set(key, headers[key]);
       }
     }
-    const initialInput = getInitialInput(request, config.inputSources);
+    const input = getInput(request, config.inputSources);
     try {
-      const { input, options, isStreamClosed } = await this.#runMiddlewares({
+      const { options, isStreamClosed } = await this.#runMiddlewares({
         method,
-        input: { ...initialInput }, // preserve the initial
+        input,
         request,
         response,
         logger,
@@ -332,7 +331,7 @@ export class Endpoint<
       error = makeErrorFromAnything(e);
     }
     await this.#handleResult({
-      initialInput,
+      input,
       output,
       request,
       response,


Test patch:
diff --git a/tests/unit/common-helpers.spec.ts b/tests/unit/common-helpers.spec.ts
index 2ed0198d..ab522041 100644
--- a/tests/unit/common-helpers.spec.ts
+++ b/tests/unit/common-helpers.spec.ts
@@ -5,7 +5,7 @@ import {
   defaultInputSources,
   getExamples,
   getFinalEndpointInputSchema,
-  getInitialInput,
+  getInput,
   getMessageFromError,
   getRoutePathParams,
   getStatusCodeFromError,
@@ -241,10 +241,10 @@ describe("Common Helpers", () => {
     });
   });
 
-  describe("getInitialInput()", () => {
+  describe("getInput()", () => {
     test("should return body for POST, PUT and PATCH requests by default", () => {
       expect(
-        getInitialInput(
+        getInput(
           {
             body: {
               param: 123,
@@ -258,7 +258,7 @@ describe("Common Helpers", () => {
         param: 123,
       });
       expect(
-        getInitialInput(
+        getInput(
           {
             body: {
               param: 123,
@@ -271,7 +271,7 @@ describe("Common Helpers", () => {
         param: 123,
       });
       expect(
-        getInitialInput(
+        getInput(
           {
             body: {
               param: 123,
@@ -286,7 +286,7 @@ describe("Common Helpers", () => {
     });
     test("should return query for GET requests by default", () => {
       expect(
-        getInitialInput(
+        getInput(
           {
             query: {
               param: 123,
@@ -301,7 +301,7 @@ describe("Common Helpers", () => {
     });
     test("should return both body and query for DELETE and unknown requests by default", () => {
       expect(
-        getInitialInput(
+        getInput(
           {
             query: { a: "query" },
             body: { b: "body" },
@@ -316,7 +316,7 @@ describe("Common Helpers", () => {
     });
     test("should return body and files on demand for POST by default", () => {
       expect(
-        getInitialInput(
+        getInput(
           {
             body: {
               param: 123,
@@ -336,7 +336,7 @@ describe("Common Helpers", () => {
     });
     test("Issue 158: should return query and body for POST on demand", () => {
       expect(
-        getInitialInput(
+        getInput(
           {
             body: {
               a: "body",
@@ -358,7 +358,7 @@ describe("Common Helpers", () => {
     });
     test("URL params: should also be taken, with a higher priority by default", () => {
       expect(
-        getInitialInput(
+        getInput(
           {
             body: {
               a: "body",
@@ -382,7 +382,7 @@ describe("Common Helpers", () => {
     });
     test("Issue 514: should return empty object for OPTIONS", () => {
       expect(
-        getInitialInput({ method: "OPTIONS" } as unknown as Request, undefined)
+        getInput({ method: "OPTIONS" } as unknown as Request, undefined)
       ).toEqual({});
     });
   });
diff --git a/tests/unit/endpoint.spec.ts b/tests/unit/endpoint.spec.ts
index 2cf99b1e..5e714ce5 100644
--- a/tests/unit/endpoint.spec.ts
+++ b/tests/unit/endpoint.spec.ts
@@ -543,4 +543,48 @@ describe("Endpoint", () => {
       });
     });
   });
+
+  describe("Issue #673: transformations in middlewares", () => {
+    test("should avoid double parsing, should not mutate input", async () => {
+      const dateInputMiddleware = createMiddleware({
+        input: z.object({
+          middleware_date_input: z.dateIn().optional(),
+        }),
+        middleware: async ({ input: { middleware_date_input }, logger }) => {
+          logger.debug("date in mw handler", typeof middleware_date_input);
+          return {};
+        },
+      });
+
+      const endpoint = defaultEndpointsFactory
+        .addMiddleware(dateInputMiddleware)
+        .build({
+          method: "get",
+          input: z.object({}),
+          output: z.object({}),
+          handler: async ({ input: { middleware_date_input }, logger }) => {
+            logger.debug(
+              "date in endpoint handler",
+              typeof middleware_date_input
+            );
+            return {};
+          },
+        });
+
+      const { loggerMock, responseMock } = await testEndpoint({
+        endpoint,
+        requestProps: {
+          query: {
+            middleware_date_input: "2022-09-28",
+          },
+        },
+      });
+
+      expect(loggerMock.debug.mock.calls).toEqual([
+        ["date in mw handler", "object"],
+        ["date in endpoint handler", "object"],
+      ]);
+      expect(responseMock.status).toHaveBeenCalledWith(200);
+    });
+  });
 });