Giter Site home page Giter Site logo

dbartholomae / lambda-middleware Goto Github PK

View Code? Open in Web Editor NEW
151.0 151.0 18.0 1.38 MB

A collection of middleware for AWS lambda functions.

Home Page: https://dbartholomae.github.io/lambda-middleware/

License: MIT License

TypeScript 84.70% JavaScript 10.05% Raku 2.66% Perl 2.58%
aws hacktoberfest lambda lambda-middleware middleware

lambda-middleware's Issues

Unable to JSON serialize type with optional keys

Describe the bug

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.

To Reproduce

  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'.

Expected behavior

Type check passes as the optional key is still of the right type.

Additional context

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

Can you please show how to add cors() to a compose chain?

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? **

  • Yes, absolutely
  • Yes, with some guidance
  • Unfortunately no time :'-(

Describe the solution you'd like

Ideally, I'd like to add cors to this chain to add to all lambda functions

Describe alternatives you've considered

None

Additional context

Add any other context or screenshots about the feature request here.

Allowing PromiseHandler to have generic Context

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
);

Enable Github Actions to automatically build

Building should include:

  • audit dependencies
  • install dependencies via rush
  • run linting
  • run unit tests
  • report test coverage to codecov
  • run integration tests
  • publish via rush and/or semantic release both to Github and npm

JSONSchema validator with AJV

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? **

  • Yes, absolutely
  • Yes, with some guidance
  • Unfortunately no time :'-(

Describe the solution you'd like

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)
}

Describe alternatives you've considered

What I'm doing now is already the alternative.

Help with composeHandler response

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
);

Add typeorm middleware for connecting to a database

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.

Interest in a path parser 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?

  • Yes, absolutely
  • Yes, with some guidance
  • Unfortunately no time :'-(

Describe the solution you'd like

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.

Describe alternatives you've considered

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.

Additional context

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.

jsonSerializer does not pass through correct event type

Describe the bug

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

To Reproduce

compose(
  errorHandler(),
  jwtAuth(...),
  jsonSerializer()
)(handler) // the handler gets APIGatewayEvent without AuthorizedEvent

Expected behavior

Type information is preserved in the chain

Additional context

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!

Middleware ordering causes type conflicts on remaining middleware

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?

Should doNotWait middleware make context.callbackWaitsForEmptyEventLoop true instead of boolean?

It's not a really bug, but a type issue, and even that I'm not really sure.

Describe the bug

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.

To Reproduce

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
            }
      }
)

Expected behavior

A clear and concise description of what you expected to happen.
It to be typed true.

Additional context

Add any other context about the problem here.

@lambda-middleware/utils package is pulling in the entire aws-sdk package

Describe the bug

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

To Reproduce

❯ 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

Expected behavior

The aws-sdk not be included transitively

Additional context

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

Add a JSON Deserializer Middleware

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?

  • Yes, absolutely (already written, writing tests now)
  • Yes, with some guidance
  • Unfortunately no time :'-(

Describe the solution you'd like

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:

  • Asserts that the mime type on the request is JSON - if not it should throw a BadRequestError (if we're setting up a middleware to deserialize json, a non-json request should be considered "bad" in my opinion?).
  • Attempts to deserialize the payload - if it fails it should throw a RequestBodyNotJsonError.
  • Switches the body property on the request with the object and passes it to the handler.

Describe alternatives you've considered

  • Writing the above functionality within the handler.
  • Writing a compatible middleware and keeping it within my code.
  • Releasing this as a separate project and package (but it seemed to make more sense and provide more visibility to add it to the main project though, and a nice way to give back to a really useful project).

Additional implementation notes/considerations

Changing Properties vs Adding New Properties to the APIGatewayProxyEvent

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? :)

Error Handling

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).

Question - Composing multiple middleware

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?

Add middleware to serialize json data into an http response

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.

class-validator seems to break the type inference for the handler

Describe the bug

When classValidator is used in composeHandler, the types for event, and context becomes any.
The return type is lost as well.

To Reproduce

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 {

            }
      }
)

Expected behavior

A clear and concise description of what you expected to happen.
Infer event, context, and return type.

Additional context

Here is a screenshot from the code snippet, it weirdly expects a then:
image

Allow deserializeBody to work with APIGatewayProxyEventV2

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? **

  • Yes, absolutely
  • Yes, with some guidance
  • Unfortunately no time :'-(

Describe the solution you'd like

Allow deserializeBody() to be used with handlers using events of type APIGatewayProxyEventV2

Describe alternatives you've considered

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

Compatibility with Http API

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? **

  • Yes, absolutely
  • Yes, with some guidance
  • Unfortunately no time :'-(

Describe the solution you'd like

The handler should be able to dynamically understand an HTTP API payload.

Describe alternatives you've considered

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.

Additional context

None

Add io-ts validator middleware

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? **

  • Yes, absolutely (already written and working, separating from existing project at-present)
  • Yes, with some guidance
  • Unfortunately no time :'-(

Describe the solution you'd like

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).

Describe alternatives you've considered

  • Writing the above functionality within the handler.
  • Using class validator - I don't want to enable experimental functionality, and it restricts me to using classes
  • Writing a compatible middleware and keeping it within my code.
  • Releasing this as a separate project and package (but it seemed to make more sense and provide more visibility to add it to the main project though, and a nice way to give back to a really useful project).

Additional implementation notes

Why io-ts? (or rather.... why not Zod or Class Validator?)

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).

Dependencies on other Middlewares (ordering)

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.

Changing Properties vs Adding New Properties to the APIGatewayProxyEvent

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? :)

Error Handling

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).

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.