npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

thirty

v2.0.0-beta.3

Published

Lightweight extensions that makes AWS Lambda functions easy to develop, testable and type safe.

Downloads

4,050

Readme

Installation

npm install thirty

Getting started

import { APIGatewayProxyEvent } from 'aws-lambda';
import { compose, eventType } from 'thirty/core';
import { parseJson } from 'thirty/parseJson';
import { serializeJson } from 'thirty/serializeJson';
import { verifyJwt, tokenFromHeaderFactory } from 'thirty/verifyJwt';
import { registerHttpErrorHandler } from 'thirty/registerHttpErrorHandler';
import { inject } from 'thirty/inject';
import { APIGatewayProxyResult } from 'thrirty/types/APIGatewayProxyResult';

export const handler = compose(
  types<APIGatewayProxyEvent, Promise<APIGatewayProxyResult>>(),
  inject({
    authService: authServiceFactory,
    userService: userServiceFactory,
  }),
  registerHttpErrorHandler(),
  parseJson(),
  serializeJson(),
)(async event => {
  const { userService } = event.deps;
  const user = await userService.createUser(event.jsonBody);
  return {
    statusCode: 201,
    body: user,
  };
});

Testing

The composed handler function exposes a reference to the actual handler via the actual property:

// handler.spec.ts
import { handler } from './handler';

it('should return created user', async () => {
  const user = {
    /*...*/
  };
  const eventMock = {
    deps: { userService: userServiceMock /* ..*/ },
    /* ..*/
  };
  const { statusCode, body } = await handler.actual(eventMock);
  expect(statusCode).toBe(201);
  expect(body).toEqual(user);
});

This makes it possible to easily unit test the business code without retesting middleware-functionality again.

compose

compose is a common implementation of Function_composition and the heart of thirty.

On top of that compose provides typings so that the event type, which is extended by middlewares, can be inferred.

export const handler = compose(
  types<{ inputA: number; inputB: number }, string>(),
  serializeJson(),
)(async event => {
  return event.inputA + event.inputB;
});

It also exposes a reference to the argument of the composed function:

const actual = async () => {};
export const handler = compose()(actual);
// ...
handler.actual === actual; // true

Middlewares

inject

inject is a middleware that provides lightweight dependency injection.

In order to create a dependency injection container, just define an object, where its properties refer to factory methods.

import { inject } from 'thirty/inject';

export const handler = compose(
  eventType<APIGatewayProxyEvent>(),
  inject({
    authService: authServiceFactory,
    userService: userServiceFactory,
  }),
)(async event => {
  const { userService } = event.deps;
  // ...
});

Each factory gets access all dependencies defined in the container:

export type AuthServiceDeps = { userService: UserService };
export type AuthService = ReturnType<typeof authServiceFactory>;

export const authServiceFactory = ({ userService }: AuthServiceDeps) => ({
  authenticate() {
    const user = userService.getUser();
    // ...
  },
});

This makes it easy to mock and test the actual handler:

// handler.spec.ts
it('should return created user', async () => {
  const eventMock = {
    deps: { authService: authServiceMock, userService: userServiceMock },
    /* ..*/
  };
  const result = await handler.actual(eventMock);
  // assertion goes here
});

doNotWaitForEmptyEventLoop

Sets context.callbackWaitsForEmptyEventLoop to false.

From official documentation:

callbackWaitsForEmptyEventLoop – Set to false to send the response right away when the callback runs, instead of waiting for the Node.js event loop to be empty. If this is false, any outstanding events continue to run during the next invocation.

const handler = compose(
  types<APIGatewayEvent, Promise<APIGatewayProxyResult>>(), 
  doNotWaitForEmptyEventLoop(),
)(async event => {

});

parseJson

parseJson is a middleware that parses the request body and extends the event object by a jsonBody object:

import { compose, types, of } from 'thirty/core';
import { parseJson } from 'thirty/parseJson';
import { APIGatewayProxyEvent, APIGatewayProxyResult } from 'aws-lambda';

type SomeDto = { description: string };

export const handler = compose(
  types<APIGatewayProxyEvent, Promise<APIGatewayProxyResult>>(),
  parseJson(of<SomeDto>),
)(async event => {
  const { description } = event.jsonBody;
    
  return {
    statusCode: 200,
    body: JSON.stringify({ id: uuid(), description }),
  };
});

serializeJson

Before that middleware you had to serialize your response body's manually and parse it back again in your tests in order to assert response bodys - especially partially.

type User = {id: string; name: string};
const handler = compose(
  types<APIGatewayEvent, Promise<APIGatewayProxyResult>>(),
)(async event => {
  return {
    statusCode: 200,
    body: JSON.stringify({id: 'USER_1', name: 'Marty'} as User),
  };
});

const response = await handler.actual({...});
expect(response.statusCode).toEqual(200);
expect(JSON.parse(response.body)).toEqual(expect.objectContaining({
     id: 'USER_1'
  }));

But serializeJson makes type-safe and testing less verbose:

const handler = compose(
  types<APIGatewayEvent, Promise<APIGatewayProxyResult>>(),
  serializeJson(of<User>),
)(async event => {
  return {
    statusCode: 200,
    body: {id: 'USER_1', name: 'Marty'},
  };
});

const response = await handler.actual({...});
expect(response).toEqual({
   statusCode: 200,
   body: expect.objectContaining({ id: 'USER_1' })
});

registerHttpErrorHandler

registerHttpErrorHandler is a middleware that wraps the actual handler, catches all errors and creates an error response:

import { registerHttpErrorHandler } from 'thirty/registerHttpErrorHandler';
import { BadRequestError } from 'thirty/errors';

export const handler = compose(
  eventType<{ someType: string }>(),
  registerHttpErrorHandler({
    logger: console,
    backlist: [{ statusCode: 401, message: 'Alternative message' }],
  }),
)(async event => {
  throw new BadRequestError('Parameter x missing');
});

The above example would create an error response that would look like:

{
  "statusCode": 400,
  "headers": {
    "Content-Type": "application/json"
  },
  "body": "{\"error\":\"Parameter x missing\"}"
}

HttpErrorHandlerOptions

sanitizeHeaders

sanitizeHeaders is a middleware that lower cases all header properties and stores them in a new event.sanitizedHeaders object. This is necessary because the header properties in event.headers aren't consolidated. Which means they are deserialized as set in the header request.

import { sanitizeHeaders } from 'thirty/sanitizeHeaders';

export const handler = compose(
  eventType<{ someType: string }>(),
  sanitizeHeaders(),
)(async event => {
  event.sanitizedHeaders;
});

handleCors

handleCors is a middleware that creates a preflight response to OPTIONS requests and adds CORS headers to any other request.

Requires sanitizeHeaders middleware

import { sanitizeHeaders } from 'thirty/sanitizeHeaders';
import { handleCors } from 'thirty/handleCors';

export const handler = compose(
  eventType<APIGatewayProxyEvent>(),
  sanitizeHeaders(),
  handleCors(),
)(async event => {
  // ...
});

CorsOptions

decodeParameters

decodeParameters is a middleware that decodes all parameter values with decodeURIComponent and stores them in event.decodedPathParameters,event.decodedQueryParameters, event.decodedMultiValueQueryParameters.

import { decodeParameters } from 'thirty/decodeParameters';

export const handler = compose(
  eventType<{ someType: string }>(),
  decodeParameters(),
)(async event => {
  event.decodeParameters;
  event.decodedQueryParameters;
  event.decodedMultiValueQueryParameters;
});

verifyJwt

verifyJwt is a authentication middleware, which extends the event object by a user object and throws an UnauthorizedError if the client is not authorized. Under the hood it uses the jsonwebtoken library.

import { verifyJwt } from 'thirty/verifyJwt';

export const handler = compose(
  eventType<{ someType: string }>(),
  verifyJwt({
    getToken: event => event.headers.Authorization.split(' ')[1],
    getSecretOrPublic: ({ deps, event, decodedJwt }) => someSecretOrPublic,
  }),
)(async event => {
  event.user;
});

thirty/verifyJwt already provides factory functions to retrieve the token from headers or cookie:

  • tokenFromHeaderFactory expects a header name (default is 'Authorization').

    Requires sanitizeHeaders middleware

    import { tokenFromHeaderFactory } from 'thirty/verifyJwt';
    
    {
      getToken: tokenFromHeaderFactory();
    }
  • tokenFromCookieFactory requires parseCookie middleware and expects a key for cookie entry (default is 'authentication').

    import { tokenFromCookieFactory } from 'thirty/verifyJwt';
    
    {
      getToken: tokenFromCookieFactory();
    }

Options API

  • getToken - Function that expects the token that should be validated.
  • getSecretOrPublic - Secret or public key provider for verifying token.
  • All options that can be passed to jsonwebtoken's verify

verifyXsrfToken

verifyXsrfToken is a middleware that checks the XSRF Token provided in the request headers. It uses the csrf library.

Requires sanitizeHeaders middlware

import { verifyXsrfToken } from 'thirty/verifyXsrfToken';

export const handler = compose(
  eventType<{ someType: string }>(),
  verifyXsrfToken({
    getSecret: ({ event }) => secret,
  }),
)(async event => {
  // ...
});

parseCookie

parseCookie is a middleware that parses the event cookie header and extends the event object by a cookie object:

import { parseCookie } from 'thirty/parseCookie';

export const handler = compose(
  eventType<{ someType: string }>(),
  parseCookie(),
)(async event => {
  event.cookie;
});

forEachSqsRecord

Consider the following setup not using that middleware:

type SomeMesssage = {id: string; text: string};
const handler = compose(
  types<SQSEvent, Promise<SQSBatchResponse>>(),
)(async event => {
  return {
    batchItemFailures: (
      await Promise.all(
        event.Records.map((record) => {
          try {
            const message: SomeMessage = JSON.parse(record.body);
            // process message
          } catch (e) {
            return {
              itemIdentifier: record.messageId,
            };
          }
        }),
      )
    ).filter((maybeItemFailure): maybeItemFailure is SQSBatchItemFailure => !!maybeItemFailure),
  };
});

You have to do a lot of boilerplate code, which makes the actual business code of processing one message hard to read. forEachSqsRecord lets you process one message without any of that boilerplate:

const handler = compose(
  types<SQSEvent, Promise<SQSBatchResponse>>(),
  forEachSqsRecord({
    batchItemFailures: true,
    bodyType: of<SomeMessage>,
  })
)(async event => {
  const message = event.record.body;
  // process message
});

Use sequential set to true in order to iterate over the records in order. If one record fails to be processed, the processing of any upcoming records will be stopped, stopped too. If batchItemFailures is also set to true, all unprocessed records will be added to list of batchItemFailures.

Publish

In order to publish a new version to npm, create a new release on github.

  1. Create a tag. The tag needs to follow semver (Don't prefix the version number with "v" as suggested by github). e.g. 1.7.0
  2. Define a release title
  3. Generate release notes by clicking "Generate release notes"
  4. Click "Publish release"

ℹ️ The package will automatically bundled and published to npm via the publish.yml workflow.