Skip to content

Commit

Permalink
feat: Introduce LocalLambdaGroup to manage multiple lambdas in a sing…
Browse files Browse the repository at this point in the history
…le server instance (#20)

* feat: multiple handlers on same port

* feat: Create new file for Lambda Group server

* chore: address PR comments
  • Loading branch information
thapabishwa authored May 7, 2023
1 parent 5570064 commit 5129e4a
Show file tree
Hide file tree
Showing 4 changed files with 121 additions and 15 deletions.
57 changes: 57 additions & 0 deletions src/example/multi.ts
Original file line number Diff line number Diff line change
@@ -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<LambdaResponse> => ({ 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<LambdaResponse> => ({ 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();
3 changes: 2 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export * from './local.lambda';
export * from './types/index';
export * from './types/index';
export * from './lambda.group';
44 changes: 44 additions & 0 deletions src/lambda.group.ts
Original file line number Diff line number Diff line change
@@ -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<string, any>;
}


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()}'`));
}
}
32 changes: 18 additions & 14 deletions src/local.lambda.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = [
Expand All @@ -16,7 +17,7 @@ const defaultBinaryContentTypeHeaders = [
'application/zip',
];

const DefaultPathParamsPattern = '/';
export const DefaultPathParamsPattern = '/';

export class LocalLambda {
handler: LambdaHandler;
Expand All @@ -25,22 +26,27 @@ export class LocalLambda {
enableCORS: boolean;
binaryContentTypesOverride: Set<string>;
pathParamsPattern: string;
defaultPath: string;
app: express.Application;
requestContext: Record<string, any>;

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[] = [];
Expand Down Expand Up @@ -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()}'`));
}

Expand All @@ -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<string, any>;
}

0 comments on commit 5129e4a

Please sign in to comment.