Repository: swarmion/swarmion
Pull request #640 (2023-04-23 17:46:49)

docs: add clementmarcilhacy as a contributor for code, and doc
Adds @clementmarcilhacy as a contributor for code, doc.

This was requested by adriencaccia [in this comment](https://github.com/swarmion/swarmion/pull/630#issuecomment-1519119433)

[skip ci]


Patch:
diff --git a/.all-contributorsrc b/.all-contributorsrc
index 53bca25f..e6d5800d 100644
--- a/.all-contributorsrc
+++ b/.all-contributorsrc
@@ -213,6 +213,16 @@
       "contributions": [
         "code"
       ]
+    },
+    {
+      "login": "clementmarcilhacy",
+      "name": "Clément Marcilhacy",
+      "avatar_url": "https://avatars.githubusercontent.com/u/28105242?v=4",
+      "profile": "https://github.com/clementmarcilhacy",
+      "contributions": [
+        "code",
+        "doc"
+      ]
     }
   ],
   "contributorsPerLine": 7,
diff --git a/README.md b/README.md
index 252f924b..408c46cb 100644
--- a/README.md
+++ b/README.md
@@ -5,7 +5,7 @@
 </p>
 
 <!-- ALL-CONTRIBUTORS-BADGE:START - Do not remove or modify this section -->
-[![All Contributors](https://img.shields.io/badge/all_contributors-22-orange.svg?style=flat-square)](#contributors-)
+[![All Contributors](https://img.shields.io/badge/all_contributors-23-orange.svg?style=flat-square)](#contributors-)
 <!-- ALL-CONTRIBUTORS-BADGE:END -->
 
 A set of tools to build and deploy type-safe microservices. In order to see working examples of these tools, check out the [examples folder](https://github.com/swarmion/swarmion/tree/main/examples).
@@ -60,6 +60,7 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d
     </tr>
     <tr>
       <td align="center" valign="top" width="14.28%"><a href="https://github.com/adelego"><img src="https://avatars.githubusercontent.com/u/28386375?v=4?s=100" width="100px;" alt="Adèle Gauvrit"/><br /><sub><b>Adèle Gauvrit</b></sub></a><br /><a href="https://github.com/swarmion/swarmion/commits?author=adelego" title="Code">💻</a></td>
+      <td align="center" valign="top" width="14.28%"><a href="https://github.com/clementmarcilhacy"><img src="https://avatars.githubusercontent.com/u/28105242?v=4?s=100" width="100px;" alt="Clément Marcilhacy"/><br /><sub><b>Clément Marcilhacy</b></sub></a><br /><a href="https://github.com/swarmion/swarmion/commits?author=clementmarcilhacy" title="Code">💻</a> <a href="https://github.com/swarmion/swarmion/commits?author=clementmarcilhacy" title="Documentation">📖</a></td>
     </tr>
   </tbody>
 </table>
diff --git a/packages/serverless-contracts/src/contracts/apiGateway/features/lambdaHandler.ts b/packages/serverless-contracts/src/contracts/apiGateway/features/lambdaHandler.ts
index 9bb4f4da..ab2ed24e 100644
--- a/packages/serverless-contracts/src/contracts/apiGateway/features/lambdaHandler.ts
+++ b/packages/serverless-contracts/src/contracts/apiGateway/features/lambdaHandler.ts
@@ -1,3 +1,4 @@
+/* eslint-disable max-lines */
 import Ajv, { ValidateFunction } from 'ajv';
 import createHttpError, { isHttpError } from 'http-errors';
 
@@ -28,11 +29,13 @@ export type GetApiGatewayHandlerOptions =
       ajv: Ajv;
       validateInput?: boolean;
       validateOutput?: boolean;
+      returnValidationErrors?: boolean;
     }
   | {
       ajv?: Ajv;
       validateInput: false;
       validateOutput: false;
+      returnValidationErrors: false;
     };
 
 const defaultOptions = {
@@ -102,6 +105,7 @@ export const getApiGatewayHandler =
       }
     }
 
+    // eslint-disable-next-line complexity
     return async (event, context, callback, ...additionalArgs) => {
       try {
         const parsedEvent = proxyEventToHandlerEvent<
@@ -118,6 +122,15 @@ export const getApiGatewayHandler =
           if (!inputValidator(parsedEvent)) {
             console.error('Error: Invalid input');
             console.error(JSON.stringify(inputValidator.errors, null, 2));
+            // eslint-disable-next-line max-depth
+            if (options.returnValidationErrors === true)
+              throw createHttpError(
+                400,
+                JSON.stringify({
+                  message: 'Invalid input',
+                  errors: inputValidator.errors,
+                }),
+              );
             throw createHttpError(400, 'Invalid input');
           }
         }
diff --git a/user-docs/documentation/docs/how-to-guides/1-use-serverless-contracts/1-api-gateway.md b/user-docs/documentation/docs/how-to-guides/1-use-serverless-contracts/1-api-gateway.md
index c5b2ea94..50172639 100644
--- a/user-docs/documentation/docs/how-to-guides/1-use-serverless-contracts/1-api-gateway.md
+++ b/user-docs/documentation/docs/how-to-guides/1-use-serverless-contracts/1-api-gateway.md
@@ -257,7 +257,8 @@ Regarding the `ajv` option, we advise you to use a singleton instance of ajv tha
 [See an example](../../how-to-guides/migration-guides/ajv-dependency-injection#share-a-singleton-ajv-instance-across-the-whole-project)
 :::
 
-:::info
+### Input and output validation
+
 By default, the `getHandler` feature will validate both the input and the output of your lambda. If you wish to disable one of those, you can use the optional second argument in the `getHandler` feature.
 If you do so, you can omit the `ajv` option.
 
@@ -272,7 +273,50 @@ const handler = getHandler(myContract, {
 });
 ```
 
-:::
+You can also choose to return the ajv validation errors in the response body:
+
+```ts
+import { getHandler } from '@swarmion/serverless-contracts';
+import { ajv } from 'libs/ajv';
+
+const handler = getHandler(myContract, {
+  ajv,
+  returnValidationErrors: true,
+})(async event => {
+  // ...
+});
+```
+
+An exemple with this flag would be:
+
+```json
+{
+  "statusCode": 400,
+  "body": {
+    "message": "Invalid input",
+    "errors": [
+      {
+        "keyword": "type",
+        "dataPath": ".id",
+        "schemaPath": "#/properties/id/type",
+        "params": {
+          "type": "string"
+        },
+        "message": "should be string"
+      }
+    ]
+  }
+}
+```
+
+and without:
+
+```json
+{
+  "statusCode": 400,
+  "body": "Invalid input"
+}
+```
 
 ### Use Middy middlewares
 


Test patch:
diff --git a/packages/serverless-contracts/src/contracts/apiGateway/__tests__/httpLambdaHandler.test.ts b/packages/serverless-contracts/src/contracts/apiGateway/__tests__/httpLambdaHandler.test.ts
index a4469c80..a404e31e 100644
--- a/packages/serverless-contracts/src/contracts/apiGateway/__tests__/httpLambdaHandler.test.ts
+++ b/packages/serverless-contracts/src/contracts/apiGateway/__tests__/httpLambdaHandler.test.ts
@@ -194,6 +194,71 @@ describe('apiGateway lambda handler', () => {
       });
     });
 
+    it('should return a error response with ajv error details when input is invalid', async () => {
+      const fakeContext = getHandlerContextMock();
+
+      const httpHandler = getHandler(httpApiContract, {
+        ajv,
+        returnValidationErrors: true,
+      })(
+        async ({
+          body,
+          pathParameters,
+          queryStringParameters,
+          headers,
+          requestContext,
+        }) => {
+          await Promise.resolve();
+          const myCustomClaim = requestContext.authorizer.claims.foo;
+
+          const name =
+            body.foo +
+            pathParameters.pageNumber +
+            queryStringParameters.testId +
+            headers.myHeader +
+            myCustomClaim;
+
+          throw createHttpError(500, name);
+        },
+      );
+
+      const result = await httpHandler(
+        {
+          pathParameters: { userId: 'toto', pageNumber: '15' },
+          body: JSON.stringify({ bar: 'foo' }),
+          headers: {
+            myHeader: 'MyCustomHeader',
+            anotherHeader: 'anotherHeader',
+          },
+          queryStringParameters: { testId: 'myTestId' },
+          requestContext: fakeRequestContext,
+          version: '',
+          routeKey: '',
+          rawPath: '',
+          rawQueryString: '',
+          isBase64Encoded: false,
+        },
+        fakeContext,
+        () => null,
+      );
+
+      expect(result).toEqual({
+        body: JSON.stringify({
+          message: 'Invalid input',
+          errors: [
+            {
+              instancePath: '/body',
+              schemaPath: '#/properties/body/required',
+              keyword: 'required',
+              params: { missingProperty: 'foo' },
+              message: "must have required property 'foo'",
+            },
+          ],
+        }),
+        statusCode: 400,
+      });
+    });
+
     it('should return a error response when output is invalid', async () => {
       const fakeContext = getHandlerContextMock();