Skip to content

Commit

Permalink
fix: error mapping
Browse files Browse the repository at this point in the history
  • Loading branch information
tschoffelen committed May 1, 2024
1 parent de2c1ba commit 55ecc6e
Show file tree
Hide file tree
Showing 7 changed files with 158 additions and 156 deletions.
16 changes: 8 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,25 +1,25 @@
# Serverless Middleware

Some helpers for writing API endpoints using AWS Lambda and [Laconia](https://github.com/laconiajs/laconia).
Some helpers for writing API endpoints using AWS Lambda.

---

## Installation

```shell
yarn add @flexible-agency/serverless-middleware
yarn add @includable/serverless-middleware
```

## Example usage

```js
import { middleware, auth } from '@flexible-agency/serverless-middleware';
import { middleware, auth } from '@includable/serverless-middleware';

const dependencies = () => ({
// dependencies for the Laconia dependency injector
});
const dependencies = {
// dependencies for the dependency injector
};

export const app = async({ query, path, body }, { currentUser, /* dependences */ }) => {
export const app = async ({ query, path, body }, { currentUser, /* dependences */ }) => {
// if `auth` is included in the second param of `middleware`, currentUser
// will be an object in the form of `{ id, groups, email, ... }`

Expand Down Expand Up @@ -52,7 +52,7 @@ The middleware will automatically prevent code execution on warmup requests.

<div align="center">
<b>
<a href="https://schof.co/consulting/?utm_source=flexible-agency/serverless-middleware">Get professional support for this package →</a>
<a href="https://includable.com/consultancy/?utm_source=serverless-middleware">Get professional support for this package →</a>
</b>
<br>
<sub>
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
"keywords": [
"serverless",
"middleware",
"laconia"
"apigateway"
],
"author": "Thomas Schoffelen <[email protected]>",
"license": "MIT",
Expand Down
130 changes: 0 additions & 130 deletions src/api-adapter.js

This file was deleted.

42 changes: 25 additions & 17 deletions src/index.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
const currentUser = require("./policies/currentUser");
const createApigatewayAdapter = require("./api-adapter");
const ApiAdapter = require("./lib/ApiAdapter");
const ErrorConverter = require("./lib/ErrorConverter");
const Response = require("./lib/Response");

const errors = (statusCode) => (error) => {
const output = {
Expand All @@ -14,27 +16,33 @@ const errors = (statusCode) => (error) => {
output.debugContext = error;
}

return { statusCode, body: { error: output } };
return new Response({ error: output }, statusCode);
};

const apigateway = createApigatewayAdapter({
errorMappings: {
ValidationError: errors(400),
Invalid: errors(400),
Unauthorized: errors(401),
Forbidden: errors(403),
"no access": errors(401),
"No access": errors(401),
"not found": errors(404),
expired: errors(404),
".*": errors(500),
},
});
const adapter = (app, policies) => {
const adapter = new ApiAdapter(
app,
policies,
new ErrorConverter({
ValidationError: errors(400),
Invalid: errors(400),
Unauthorized: errors(401),
Forbidden: errors(403),
"no access": errors(401),
"No access": errors(401),
"not found": errors(404),
expired: errors(404),
".*": errors(500),
}),
);

return adapter.toFunction();
};

const middleware = (app, policies = []) => {
if (typeof app !== "function")
throw new TypeError(
`middle() expects to be passed a function, you passed: ${JSON.stringify(
`middleware() expects to be passed a function, you passed: ${JSON.stringify(
app,
)}`,
);
Expand All @@ -44,7 +52,7 @@ const middleware = (app, policies = []) => {
context.event = event;
context.context = eventContext;

return apigateway(app, policies)(event, context);
return adapter(app, policies)(event, context);
};

return Object.assign(core, {
Expand Down
75 changes: 75 additions & 0 deletions src/lib/ApiAdapter.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
const Response = require("./Response");
const Request = require("./Request");

class ApiAdapter {
constructor(app, policies, errorConverter) {
this.app = app;
this.policies = policies || [];
this.errorConverter = errorConverter;
this.statusCode = 200;
this.additionalHeaders = {};
}

async handle(event, context) {
if (event && event.source && event.source === "serverless-plugin-warmup") {
return "Lambda is warm";
}

try {
let input = event;

// EventBridge events shouldn't be parsed
if (event.headers || !event["detail-type"] || !event.source) {
// Process events from API Gateway
const { body, headers, params, method } = new Request(event);
const { pathParameters, queryStringParameters } = event;

input = {
method,
...event,
...(params || {}),
headers,
params,
query: queryStringParameters || {},
body: body || {},
path: pathParameters || {},
};
}

for (let i = 0; i < this.policies.length; i++) {
await this.policies[i](input, context);
}

let output = await this.app(input, context);

if (output instanceof Response) {
return {
...output,
headers: Object.assign(this.additionalHeaders, output.headers),
};
}

if (typeof output === "object" && output.statusCode && output.body) {
this.statusCode = output.statusCode;
if (output.headers && typeof output.headers === "object") {
this.additionalHeaders = {
...this.additionalHeaders,
...output.headers,
};
}
output = output.body;
}

return new Response(output, this.statusCode, this.additionalHeaders);
} catch (error) {
console.error(error);
return this.errorConverter.convert(error);
}
}

toFunction() {
return this.handle.bind(this);
}
}

module.exports = ApiAdapter;
33 changes: 33 additions & 0 deletions src/lib/ErrorConverter.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
const Response = require("./Response");

const getMappingEntries = (mappings) =>
mappings instanceof Map ? mappings.entries() : Object.entries(mappings);

const getMappingResponse = (mappings, error) => {
let mappingResponse = {};
for (const [errorRegex, mapping] of getMappingEntries(mappings)) {
if (error.name.match(errorRegex) || error.message.match(errorRegex)) {
mappingResponse = mapping(error);
break;
}
}

return mappingResponse;
};

class ErrorConverter {
constructor(mappings) {
this.mappings = mappings;
}

convert(error) {
const mappingResponse = getMappingResponse(this.mappings, error);
const body = mappingResponse.body || error.message;
const statusCode = mappingResponse.statusCode || error.statusCode || 500;
const headers = mappingResponse.headers || {};

return new Response(body, statusCode, headers);
}
}

module.exports = ErrorConverter;
16 changes: 16 additions & 0 deletions tests/errors.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
const middleware = require("../src");
const event = require("lambda-sample-events/events/aws/apigateway-aws-proxy.json");

it("responds with correct error message and status", async () => {
const exampleApp = middleware(async () => {
throw new Error("page not found");
});

const response = await exampleApp(event);
expect(response.statusCode).toEqual(404);
expect(JSON.parse(response.body).error).toEqual(
expect.objectContaining({
message: "page not found",
}),
);
});

0 comments on commit 55ecc6e

Please sign in to comment.