From 5129e4a6e528352b10d30490368542f5910b7ff7 Mon Sep 17 00:00:00 2001 From: Bishwa Thapa <15176360+thapabishwa@users.noreply.github.com> Date: Sun, 7 May 2023 20:18:06 +0545 Subject: [PATCH] feat: Introduce LocalLambdaGroup to manage multiple lambdas in a single server instance (#20) * feat: multiple handlers on same port * feat: Create new file for Lambda Group server * chore: address PR comments --- src/example/multi.ts | 57 ++++++++++++++++++++++++++++++++++++++++++++ src/index.ts | 3 ++- src/lambda.group.ts | 44 ++++++++++++++++++++++++++++++++++ src/local.lambda.ts | 32 ++++++++++++++----------- 4 files changed, 121 insertions(+), 15 deletions(-) create mode 100644 src/example/multi.ts create mode 100644 src/lambda.group.ts diff --git a/src/example/multi.ts b/src/example/multi.ts new file mode 100644 index 0000000..d97abe3 --- /dev/null +++ b/src/example/multi.ts @@ -0,0 +1,57 @@ +import { Context } from 'aws-lambda'; +import { LambdaResponse, RequestEvent } from '../types'; +import { LambdaConfig, LocalLambdaGroupConfig, LocalLambdaGroup } from '../lambda.group'; + +// handler is a function that takes in an event and context and returns a response +const handler = async (req: RequestEvent, context: Context): Promise => ({ statusCode: 200, body: `Hello World !!! My userId is ${req.pathParameters?.id}\n My JWT is ${JSON.stringify(req.requestContext.authorizer.lambda.jwt)}. My queryStringParameter is ${JSON.stringify(req.queryStringParameters)} ` }); +const funnyHandler = async (req: RequestEvent, context: Context): Promise => ({ statusCode: 200, body: `Hello World !!! I'm funny and my userId is ${req.pathParameters?.id}\n My JWT is ${JSON.stringify(req.requestContext.authorizer.lambda.jwt)}. My queryStringParameter is ${JSON.stringify(req.queryStringParameters)} ` }); + +// context is provided as optional field in config. +const config: LambdaConfig[] = [ + { + handler: handler, // if type is not compatible, do `handler: handler as any` + requestContext: { + authorizer: { + lambda: { + jwt: { + claims: { + sub: '1234567890', + name: 'John Doe', + iat: 1516239022, + }, + scopes: ['read', 'write'], + }, + }, + }, + }, + pathParamsPattern: '/user/:id', // optional, default to '/' + }, + { + handler: funnyHandler, // if type is not compatible, do `handler: handler as any` + pathParamsPattern: '/user/:id/funny', // optional, default to '/' + requestContext: { + authorizer: { + lambda: { + jwt: { + claims: { + sub: '1234567890', + name: 'John Doe', + iat: 1516239022, + }, + scopes: ['read', 'write'], + }, + }, + }, + }, + }, +]; + +const multiConfig: LocalLambdaGroupConfig = { + lambdas: config, + port: 8008, // optional, default to 8000 + defaultPath: '/api/v1', // optional, default to '/' +}; + +// visit http://localhost:8008/api/v1/user/1234567890 and http://localhost:8008/api/v1/user/1234567890/funny to see the response +const localLambdaGroup = new LocalLambdaGroup(multiConfig); +localLambdaGroup.run(); \ No newline at end of file diff --git a/src/index.ts b/src/index.ts index 723f654..8579904 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,2 +1,3 @@ export * from './local.lambda'; -export * from './types/index'; \ No newline at end of file +export * from './types/index'; +export * from './lambda.group'; \ No newline at end of file diff --git a/src/lambda.group.ts b/src/lambda.group.ts new file mode 100644 index 0000000..87abc39 --- /dev/null +++ b/src/lambda.group.ts @@ -0,0 +1,44 @@ +import { Context } from 'aws-lambda'; +import express from 'express'; +import { LambdaHandler } from 'index'; +import { DefaultPathParamsPattern, DefaultPort, LocalLambda } from './local.lambda'; + + +export interface LambdaConfig { + handler: LambdaHandler; + context?: Context; + enableCORS?: boolean; + // default binary content-type headers or override + binaryContentTypesOverride?: string[]; + pathParamsPattern ?: string; + requestContext?: Record; +} + + +export interface LocalLambdaGroupConfig { + lambdas: LambdaConfig[]; + port?: number; + defaultPath?: string; +} + +export class LocalLambdaGroup { + lambdas: LambdaConfig[] = []; + app: express.Application; + port: number; + defaultPath: string; + + constructor(config: LocalLambdaGroupConfig) { + this.lambdas = config.lambdas; + this.app = express(); + this.port = config.port ?? DefaultPort; + this.defaultPath = config.defaultPath ?? DefaultPathParamsPattern; + } + + run(): void { + this.lambdas.forEach(lambda => { + const localLambda = new LocalLambda(lambda, this.app, this.defaultPath); + localLambda.createRoute(); + }); + this.app.listen(this.port, () => console.info(`🚀 Lambda Group Server ready at http://localhost:${this.port} at '${new Date().toLocaleString()}'`)); + } +} \ No newline at end of file diff --git a/src/local.lambda.ts b/src/local.lambda.ts index 48a71c4..a4badad 100644 --- a/src/local.lambda.ts +++ b/src/local.lambda.ts @@ -4,7 +4,8 @@ import HTTPMethod from 'http-method-enum'; import { LambdaHandler, RequestEvent } from './types'; import express, { Request, Response } from 'express'; import { flattenArraysInJSON, cloneDeep } from './utils'; -const DefaultPort = 8000; +import { LambdaConfig } from './lambda.group'; +export const DefaultPort = 8000; // binary upload content-type headers const defaultBinaryContentTypeHeaders = [ @@ -16,7 +17,7 @@ const defaultBinaryContentTypeHeaders = [ 'application/zip', ]; -const DefaultPathParamsPattern = '/'; +export const DefaultPathParamsPattern = '/'; export class LocalLambda { handler: LambdaHandler; @@ -25,22 +26,27 @@ export class LocalLambda { enableCORS: boolean; binaryContentTypesOverride: Set; pathParamsPattern: string; + defaultPath: string; app: express.Application; requestContext: Record; - constructor(config: LocalLambdaConfig) { + constructor(config: LocalLambdaConfig, app?: express.Application, defaultPath?: string) { this.handler = config.handler; this.port = config.port ?? DefaultPort; this.context = config.context || {} as Context; this.enableCORS = config.enableCORS ?? true; this.binaryContentTypesOverride = new Set(config.binaryContentTypesOverride ?? defaultBinaryContentTypeHeaders); this.pathParamsPattern = config.pathParamsPattern ?? DefaultPathParamsPattern; - this.app = express(); + this.app = app || express(); + this.defaultPath = defaultPath ?? DefaultPathParamsPattern; this.requestContext = config.requestContext ?? {}; } - run(): void { - this.app.all(`${this.pathParamsPattern}*`,async (request: Request, response: Response) => { + createRoute(): void { + const router = express.Router(); + this.app.use(this.defaultPath, router); + + router.all(`${this.pathParamsPattern}`, async (request: Request, response: Response) => { // create a copy of requestContext to avoid accidental mutation const copyOfRequestContext = cloneDeep(this.requestContext); const data: Buffer[] = []; @@ -84,6 +90,10 @@ export class LocalLambda { }); + } + + run(): void { + this.createRoute(); this.app.listen(this.port, () => console.info(`🚀 Server ready at http://localhost:${this.port} at '${new Date().toLocaleString()}'`)); } @@ -96,13 +106,7 @@ export class LocalLambda { } -export interface LocalLambdaConfig { - handler: LambdaHandler; +// extend the LambdaConfig to add port +export interface LocalLambdaConfig extends LambdaConfig { port?: number; - context?: Context; - enableCORS?: boolean; - // default binary content-type headers or override - binaryContentTypesOverride?: string[]; - pathParamsPattern ?: string; - requestContext?: Record; }