dbartholomae / lambda-middleware Goto Github PK
View Code? Open in Web Editor NEWA collection of middleware for AWS lambda functions.
Home Page: https://dbartholomae.github.io/lambda-middleware/
License: MIT License
A collection of middleware for AWS lambda functions.
Home Page: https://dbartholomae.github.io/lambda-middleware/
License: MIT License
The json serializer works fine for object literals, however if passed a type with optional members (I generally return models from my handlers, which usually have optional fields), it doesn't typecheck.
describe("with a handler returning a type with optional members", () => {
let response: any;
type Thing = {
id?: string
foo: string
}
beforeEach(async () => {
const handlerResponse: Thing = { foo: "bar" };
const handler = async () => handlerResponse;
const handlerWithMiddleware = jsonSerializer()(handler);
response = await handlerWithMiddleware(createEvent({}), createContext());
});
it("returns 200", async () => {
expect(response).toMatchObject({ statusCode: 200});
});
it("returns serialized non-optional members", async () => {
expect(response.body).toEqual("{\"foo\":\"bar\"}");
});
})
yields
src/JsonSerializer.test.ts:38:54 - error TS2345: Argument of type '() => Promise<Thing>' is not assignable to parameter of type 'PromiseHandler<APIGatewayProxyEvent, { [key: string]: JSONPrimitive; } | JSONObject[] | undefined>'.
Type 'Promise<Thing>' is not assignable to type 'Promise<{ [key: string]: JSONPrimitive; } | JSONObject[] | undefined>'.
Type 'Thing' is not assignable to type '{ [key: string]: JSONPrimitive; } | JSONObject[] | undefined'.
Type 'Thing' is not assignable to type '{ [key: string]: JSONPrimitive; }'.
Property 'id' is incompatible with index signature.
Type 'string | undefined' is not assignable to type 'JSONPrimitive'.
Type 'undefined' is not assignable to type 'JSONPrimitive'.
Type check passes as the optional key is still of the right type.
This appears to be a bit of a typescript wart, as optional members are treated as equivalent to having | undefined
under strict null checks, which doesn't really make sense to me given that even in underlying JS hasOwnProperty can distinguish the two cases.
This seems like it can be fixed by redefining type JSONPrimitive = string | number | boolean | JSONObject | undefined;
, though this isn't technically valid JSON, JSON.stringify has a relatively defined behavior for it.
I think there is a way to do this cleanly with keyof/Pick, trying to work it out...
EDIT: I should also note that this doesn't work at all if the returned value is typed as interface
; that seems to really break the index signature inference
Is your feature request related to a problem? Please describe.
I'm trying to add the cors
middleware to a compose chain, and can't seem to find the right combination. Here's my current attempt:
const corsConfig: CorsMiddlewareOptions = {
allowedHeaders: ['*'],
cacheControl: 'max-age: 300',
allowCredentials: true,
exposedHeaders: ['X-Custom-Header'],
maxAge: 300,
allowedMethods: ['GET', 'HEAD', 'PUT', 'PATCH', 'POST', 'DELETE'],
optionsSuccessStatus: 204,
allowedOrigins: ['https://mysite.com'],
preflightContinue: false,
}
export const standardHttp = compose<APIGatewayEvent, APIGatewayProxyResult>(
createJWTForLocal,
typeormInitializer,
doNotWait(),
jsonSerializer(),
jsonDeserializer(),
timingLogMiddleware(logDuration),
errorHandler,
cors(corsConfig)
)
** Would you be willing to help with a PR? **
Ideally, I'd like to add cors
to this chain to add to all lambda functions
None
Add any other context or screenshots about the feature request here.
Thanks for this libary.
Can we use a different lambda event type?
I would like to have the event as APIGatewayProxyEventV2WithJWTAuthorizer
for example.
Thanks
I might be missing something here, but what do you think about adding Context as a generic to the ProxyHandler. I seem to hit a wall when altering it and passing those alterations to the next middleware.
I followed the instructions to creating middleware, but didn't see any references on how to alter the context. As a result, I explored with adding a context generic to the PromiseHandler
New PromiseHandler
export type ContextPromiseHandler<
TEvent = any,
TResult = any,
TContext = Context
> = (event: TEvent, context: TContext) => Promise<TResult>;
And an example of altering the context
// Add `foo` to context
const fooContextMiddleware = () => <
E extends APIGatewayProxyEvent,
C extends Context
>(
handler: ContextPromiseHandler<E, APIGatewayProxyResult, C & { foo: string }>
): ContextPromiseHandler<E, APIGatewayProxyResult, C> => async (
event: E,
context: C
) => {
return handler(event, { ...context, foo: "bar" });
};
// Add `bar` to context
const barContextMiddleware = () => <
E extends APIGatewayProxyEvent,
C extends Context
>(
handler: ContextPromiseHandler<E, APIGatewayProxyResult, C & { bar: string }>
): ContextPromiseHandler<E, APIGatewayProxyResult, C> => async (
event: E,
context: C
) => {
return handler(event, { ...context, bar: "foo" });
};
// Business
const helloWorldContext = async (
event: APIGatewayProxyEvent,
context: Context & { foo: string; bar: string }
) => {
console.log("Foo", context.foo);
console.log("Bar", context.bar);
return {
body: "Hello World",
statusCode: 200,
};
};
// In strict mode so using
export const handler = composeHandler(
fooMiddleware(),
barMiddleware(),
helloWorldContext
);
Building should include:
Add a middleware for server-timing inspired by server-timing.
Inspired by the corresponding middy middleware.
This is a continuation of #4 .
helmet has a list of middlewares to improve server security. Let's mirror those that make sense:
Is your feature request related to a problem? Please describe.
I've been using AJV to validate my request payloads. I could see this being easily adapted to an official lambda-middleware.
JSONSchema is a well documented spec. AJV can return very detailed errors. It also doesn't require adapting your code to use classes.
** Would you be willing to help with a PR? **
Turn the following into middleware.
// JSONSchema format
const bodySchema = {
type: 'object',
properties: {
body: { type: 'string' },
header: { type: 'string' },
footer: { type: 'string' },
encoding: { type: 'string' },
options: { type: 'object', additionalProperties: true },
},
required: ['body'],
additionalProperties: false,
}
const ajv = new Ajv()
const validate = ajv.compile(bodySchema)
const payload = JSON.parse(event.body)
const valid = validate(payload)
if (!valid) {
console.log(validate.errors)
}
What I'm doing now is already the alternative.
I'm having a hard time determining the return type from a composeHandler. Likely due to a lack TypeScript knowleged.
Everything seems great when using out of the box handlers:
import { ProxyHandler } from "aws-lambda";
import { PromiseHandler } from "@lambda-middleware/utils";
import { composeHandler } from "@lambda-middleware/compose";
import { cors } from "@lambda-middleware/cors";
import { errorHandler } from "@lambda-middleware/http-error-handler";
const helloWorld: PromiseHandler = async () => {
return {
body: "Hello World",
statusCode: 200,
};
};
export const handler: ProxyHandler = composeHandler(
errorHandler(),
cors(),
helloWorld
);
But with middleware which alter the event, something isn't right:
import {
ProxyHandler,
Context,
APIGatewayProxyEvent,
APIGatewayProxyResult,
} from "aws-lambda";
import { PromiseHandler } from "@lambda-middleware/utils";
import { composeHandler } from "@lambda-middleware/compose";
const fooMiddleware = () => <E extends APIGatewayProxyEvent>(
handler: PromiseHandler<E & { foo: string }, APIGatewayProxyResult>
): PromiseHandler<E, APIGatewayProxyResult> => async (
event: E,
context: Context
) => {
return handler({ ...event, foo: "bar" }, context);
};
const barMiddleware = () => <E extends APIGatewayProxyEvent>(
handler: PromiseHandler<E & { bar: string }, APIGatewayProxyResult>
): PromiseHandler<E, APIGatewayProxyResult> => async (
event: E,
context: Context
) => {
return handler({ ...event, bar: "foo" }, context);
};
const helloWorldEvent = async (
event: APIGatewayProxyEvent & { foo: string; bar: string }
) => {
console.log("Foo", event.foo);
console.log("Bar", event.bar);
return {
body: "Hello World",
statusCode: 200,
};
};
// Not happy w/ ProxyHandler
export const handler: ProxyHandler = composeHandler(
fooMiddleware(),
barMiddleware(),
helloWorldEvent
);
Is your feature request related to a problem? Please describe.
Many of our lambdas need to connect to a relational database with help of typeorm. This logic could be abstacted out similar to middy's db-manager. This should also allow to wire the database into a dependency injection framework.
Describe the solution you'd like
A minimal version should work like this, reading the setup needed from the typeorm-typical env vars.
export const handler = connectDatabase({
container: Container
})
( ..)
Potentially should allow to get credentials via AWS.
Describe alternatives you've considered
Potentially this should be more abstract, allowing other kinds of ORMs like knex. Currently I think a simple middleware focussing only on typeorm makes more sense, with individual middlewares for individual ORMs.
Inspired by the corresponding middy middleware.
Is your feature request related to a problem? Please describe.
Certain lambda function hosts (netlify functions in particular) do not provide a path parser/do not populate pathParameters, nor offer a way to pack path params into query params using rewrites. I wrote a very simple path parser middleware that addresses this.
Would you be willing to help with a PR?
The code is very simple, most of it is here:
https://gist.github.com/parkan/f0b95f343e18aaadd1bb93b4ca67806d
I'm using this in production (ish), only thing I see being needed is e2e tests.
The netlify dev (local testing) env provides packing of path params into query params, however this doesn't work in production. The only other alternative is manually unpacking them in the handler.
The downside here is that the handler wrapper needs to know the path expression to parse out the params, but I don't see a way to work around it, aside from parsing _redirects at build time.
Hello it's me again 🙂
jsonSerializer (and I think some of the other middlewares as well) don't respect the pattern of generically passing the event from the previous middleware described in the README so the type information about the modified event is lost, replaced with base APIGatewayEvent
compose(
errorHandler(),
jwtAuth(...),
jsonSerializer()
)(handler) // the handler gets APIGatewayEvent without AuthorizedEvent
Type information is preserved in the chain
This should be a simple fix, replace
export const jsonSerializer = () => (
handler: PromiseHandler<APIGatewayEvent, JSONObject | undefined>
) =>
...
with
export const jsonSerializer = () => (
<E extends APIGatewayProxyEvent>(handler: PromiseHandler<APIGatewayEvent, JSONObject | undefined>): PromiseHandler<E, APIGatewayProxyResult> =>
...
happy to PR this!
I tried to follow the bodyParser example and build my own middleware which alters the event. I was also adding the first party cors middleware to the routine which produced some type errors I cannot figure out.
In the first scenario, the first middleware is adding 'foo' to the event -> cors -> business logic which doesn't hold the type reference:
import {
Context,
APIGatewayProxyEvent,
APIGatewayProxyResult,
} from "aws-lambda";
import { PromiseHandler } from "@lambda-middleware/utils";
import { composeHandler } from "@lambda-middleware/compose";
import { cors } from "@lambda-middleware/cors";
const fooMiddleware = () => <E extends APIGatewayProxyEvent>(
handler: PromiseHandler<E & { foo: string }, APIGatewayProxyResult>
): PromiseHandler<E, APIGatewayProxyResult> => async (
event: E,
context: Context
) => {
return handler({ ...event, foo: "bar" }, context);
};
const helloWorldEvent = async (
event: APIGatewayProxyEvent & { foo: string }
) => {
console.log("Foo", event.foo);
return {
body: "Hello World",
statusCode: 200,
};
};
export const handlerComposed = composeHandler(
fooMiddleware(),
cors(),
helloWorldEvent // <- not happy
);
Anything stick out?
It's not a really bug, but a type issue, and even that I'm not really sure.
A clear and concise description of what the bug is.
When doNotWait
middleware is used in composeHandler
, the type for context.callbackWaitsForEmptyEventLoop
should be true
, not boolean
.
The following minimal code reproduces the error:
import { composeHandler } from "@lambda-middleware/compose"
import { doNotWait } from "@lambda-middleware/do-not-wait"
export const handler = composeHandler(
doNotWait(),
async (evt, ctx) => {
ctx.callbackWaitsForEmptyEventLoop // It is typed as boolean, but should be true
return {
body: 'OK',
statusCode: 200
}
}
)
A clear and concise description of what you expected to happen.
It to be typed true.
Add any other context about the problem here.
The @lambda-middleware/utils
is indirectly including the entire aws-sdk
package via the aws-lambda
package which is increasing my bundle size to ~350kb
❯ yarn init -y
❯ yarn add @lambda-middleware/utils
❯ yarn why aws-sdk
yarn why v1.22.10
[1/4] �🤔 Why do we have the module "aws-sdk"...?
[2/4] �🚚 Initialising dependency graph...
[3/4] �🔍 Finding dependency...
[4/4] �🚡 Calculating file sizes...
=> Found "[email protected]"
info Reasons this module exists
- "@lambda-middleware#utils#aws-lambda" depends on it
- Hoisted from "@lambda-middleware#utils#aws-lambda#aws-sdk"
info Disk size without dependencies: "59.79MB"
info Disk size with unique dependencies: "61.05MB"
info Disk size with transitive dependencies: "61.3MB"
info Number of shared dependencies: 9
The aws-sdk
not be included transitively
I believe the core issue is that this package imports both @types/aws-lambda
and aws-lambda
even though only the type information is needed.
You should be able to just remove the aws-lambda
import
Is your feature request related to a problem? Please describe.
I spend a lot of time writing and rewriting deserialization and type validation code in my lambdas. I've just started a project where we have a large number of endpoints (roughly 40 of them) and I really didn't fancy rewriting this code again and again.
As a result, I've written two middlewares compatible with your lambda-middleware library and I'd like to see if you'd like them as part of the main package before I publish them as separate packages, this feature request is for the JSON deserializer.
Would you be willing to help with a PR?
This takes incoming event with a JSON payload, deserializes it to an object and replaces the body property with it's object equivalent and throws a custom error if JSON serialization fails (so it can be picked up by a global error handler middleware).
The implementation types would be something like this:
<E extends APIGatewayProxyEvent, R>() => PromiseHandler<Omit<E, "body"> & { body: {} }, R> => R
It does the following:
RequestBodyNotJsonError
.body
property on the request with the object and passes it to the handler.Currently I'm stripping the body and replacing it with an object, this does mean it could have negative effects on a downstream middleware, but I think it is cleaner for my code and reflects the object being transformed into something my handler will want to work with.
If you don't want that, I could add another bodyObject
property in addition to the original body but seeing as you used a similar method in the class-validator
middleware, I hope you don't have any issues with this? :)
I like the idea of "typed" errors, as such my middleware will probably use two examples of this (and my implementation uses these).
I extend Error and add any relevant properties to the extended error class (so if it's logged out I would get useful semantic logging).
You can then use these with http-error-handler
middleware (switching on error name) or I could use an app-specific error handler middleware at the top level (as I'm doing in my code at-present).
Add a middleware for session management inspired by express as an alternative to using Cognito and authorizers.
I'm trying to expose a wrapper function so that all my lambda handlers have the same middleware applied to them.
const wrapper = (handler) =>
compose(
// add cors headers last so even error responses from the
// errorHandler middleware have cors headers applied
cors(),
errorHandler(),
jsonSerializer(),
classValidator(),
)(handler)
However I'm struggling to get the type information to line up.
Is it possible to chain these middlewares like this as you can do using middy?
It should take as input an object and then serialize it into an AWS lambda response object with body
, statusCode
(200, or 204 if the input is undefined
) and set the right headers (content-type). It should not spring into action if the event headers ask for a different content-type.
When classValidator
is used in composeHandler
, the types for event
, and context
becomes any
.
The return type is lost as well.
The following minimal code reproduces the error:
import "reflect-metadata"
import { composeHandler } from "@lambda-middleware/compose"
import { jsonSerializer } from "@lambda-middleware/json-serializer"
import { errorHandler } from "@lambda-middleware/http-error-handler"
import { classValidator } from '@lambda-middleware/class-validator'
import { jsonDeserializer } from "@lambda-middleware/json-deserializer"
import { doNotWait } from "@lambda-middleware/do-not-wait"
import { IsString } from "class-validator"
class NameBody {
constructor(firstName: string, lastName: string) {
this.firstName = firstName;
this.lastName = lastName;
}
@IsString()
public firstName: string;
@IsString()
public lastName: string;
}
export const handler = composeHandler(
errorHandler(),
doNotWait(),
jsonDeserializer(),
jsonSerializer(),
classValidator({
bodyType: NameBody,
}),
async (evt, ctx) => {
return {
}
}
)
A clear and concise description of what you expected to happen.
Infer event
, context
, and return
type.
Here is a screenshot from the code snippet, it weirdly expects a then:
JsonDeserializer does not accept content-type header with charset
use header: Content-Type: application/json; charset=utf-8
bodyObject
should not be null
nope
Is your feature request related to a problem? Please describe.
deserializeBody()
currently assumes an event type APIGatewayProxyEvent
. It would be helpful if it could also support APIGatewayProxyEventV2
** Would you be willing to help with a PR? **
Allow deserializeBody()
to be used with handlers using events of type APIGatewayProxyEventV2
I would be happy to extend this PR to include all APIGateway Proxy Event types (e.g., APIGatewayProxyEventV2WithJWTAuthorizer
) as well (perhaps by creating a union type that's reused throughout the library), or to create a follow-on PR that accomplishes this
At the moment, cors
middleware seems to not support a version 2.0 payload of the HTTP API for AWS Lambda. There, the HTTP method is available under event.requestContext.http.method
not event.httpMethod
.
Here's the existing code:
if (event.httpMethod.toLowerCase() === "options") {
return handlePreflightRequest(runHandler, event, fullOptions);
}
** Would you be willing to help with a PR? **
The handler should be able to dynamically understand an HTTP API payload.
I have removed the dedicated middleware config for the APIs in question and have instead set cors
to true
at the API Gateway level. This works in my case but will not allow fine grained control that might be needed for sophisticated cases.
None
Add a middleware for identifying the useragent inspired by express-useragent.
Is your feature request related to a problem? Please describe.
I spend a lot of time writing and rewriting deserialization and type validation code in my lambdas. I've just started a project where we have a large number of endpoints (roughly 40 of them) and I really didn't fancy rewriting this code again and again.
As a result, I've written two middlewares compatible with your lambda-middleware library and I'd like to see if you'd like them as part of the main package before I publish them as separate packages, this feature request is for the io-ts validator.
** Would you be willing to help with a PR? **
The io-ts validator is loosely based on ideas from the class validator middleware, I decided to build it as I wanted to type validate a payload without having to add it a class and I didn't want to have to enable experimental typescript features.
It takes an io-ts codec and a type alias (to report errors in a human-readable fashion) and returns the middleware that will validate that specific type of object.
It replaces the body of the incoming event for the wrapped handler with the typed object (much like the JSON Deserializer middleware).
Class validator requires classes, and I've already written validators in io-ts. Why io-ts vs zod? I have experience with both, but zod explicitly does NOT map the validated object meaning that you have to write a separate mapper and it clutters things up a bit more than you might otherwise need. Passing io-ts codecs in seemed like a nice clean solution (despite all the annoying functional programming shenanigans in using io-ts).
Currently, in my app's implementation of this middleware, it expects the body to already be an object (as I control the middleware order and I didn't need to deserialize as part of this middleware) - if this is ok, I can make this an expectation of using the middleware, or I could add deserialization to make it a bit more generic but it would mean we have some repeated functionality with the JSON Deserializer middleware.
Currently I'm stripping the body and replacing it with the typed object, this does mean it could have negative effects on a downstream middleware, but I think it is cleaner for my code and reflects the object being transformed into something my handler will want to work with.
If you don't want that, I could add another distinct property in addition to the original body but seeing as you used a similar method in the class-validator
middleware, I hope you don't have any issues with this? :)
I like the idea of "typed" errors, as such my middleware will probably throw a distinct ValidationError
which will contain validation information that can be returned to the user in a BadRequest output.
I extend Error and add any relevant properties to the extended error class (so if it's logged out I would get useful semantic logging).
You can then use these with http-error-handler
middleware (switching on error name) or you could use an app-specific error handler middleware at the top level (as I'm doing in my code at-present).
Add middleware inspired by the corresponding middy middleware. Potentially allow to directly set values via dependency injection.
Inspired by the corresponding middy middleware.
Inspired by the corresponding middy middleware.
A declarative, efficient, and flexible JavaScript library for building user interfaces.
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google ❤️ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.