From dafc139c03d5dd89de1596db905ccd02836271b7 Mon Sep 17 00:00:00 2001 From: Naor Peled Date: Thu, 6 Feb 2025 20:53:16 +0200 Subject: [PATCH 01/18] . --- index.d.ts | 398 ++++++++++++++++++++++++++++++++++++++++-------- index.test-d.ts | 274 ++++++++++++++++++++++++++++++--- 2 files changed, 586 insertions(+), 86 deletions(-) diff --git a/index.d.ts b/index.d.ts index c23ae10..0690239 100644 --- a/index.d.ts +++ b/index.d.ts @@ -2,6 +2,7 @@ import { APIGatewayEventRequestContext, APIGatewayProxyEvent, APIGatewayProxyEventV2, + ALBEvent, Context, } from 'aws-lambda'; import { S3ClientConfig } from '@aws-sdk/client-s3'; @@ -44,23 +45,74 @@ export declare interface App { [namespace: string]: Package; } -export declare type Middleware = ( - req: Request, - res: Response, +export type ALBContext = ALBEvent['requestContext']; +export type APIGatewayV2Context = APIGatewayProxyEventV2['requestContext']; +export type APIGatewayContext = APIGatewayEventRequestContext; + +export declare type RequestContext = + | APIGatewayContext + | APIGatewayV2Context + | ALBContext; + +export declare type Event = + | APIGatewayProxyEvent + | APIGatewayProxyEventV2 + | ALBEvent; + +export declare type Middleware< + TResponse = any, + TContext extends RequestContext = APIGatewayContext, + TQuery extends Record = Record< + string, + string | undefined + >, + TParams extends Record = Record< + string, + string | undefined + >, + TBody = any +> = ( + req: Request, + res: Response, next: NextFunction ) => void; -export declare type ErrorHandlingMiddleware = ( + +export declare type ErrorHandlingMiddleware< + TResponse = any, + TContext extends RequestContext = APIGatewayContext, + TQuery extends Record = Record< + string, + string | undefined + >, + TParams extends Record = Record< + string, + string | undefined + >, + TBody = any +> = ( error: Error, - req: Request, - res: Response, + req: Request, + res: Response, next: NextFunction ) => void; + export declare type ErrorCallback = (error?: Error) => void; -export declare type HandlerFunction = ( - req: Request, - res: Response, - next?: NextFunction -) => void | any | Promise; +export declare type HandlerFunction< + TResponse = any, + TContext extends RequestContext = APIGatewayContext, + TQuery extends Record = Record< + string, + string | undefined + >, + TParams extends Record = Record< + string, + string | undefined + >, + TBody = any +> = ( + req: Request, + res: Response +) => void | TResponse | Promise; export declare type LoggerFunction = ( message?: any, @@ -136,20 +188,27 @@ export declare interface Options { s3Config?: S3ClientConfig; } -export declare class Request { +export declare class Request< + TContext extends RequestContext = APIGatewayContext, + TQuery extends Record = Record< + string, + string | undefined + >, + TParams extends Record = Record< + string, + string | undefined + >, + TBody = any +> { app: API; version: string; id: string; - params: { - [key: string]: string | undefined; - }; + params: TParams; method: string; path: string; - query: { - [key: string]: string | undefined; - }; + query: TQuery; multiValueQuery: { - [key: string]: string[] | undefined; + [K in keyof TQuery]: string[] | undefined; }; headers: { [key: string]: string | undefined; @@ -160,10 +219,10 @@ export declare class Request { rawHeaders?: { [key: string]: string | undefined; }; - body: any; + body: TBody; rawBody: string; route: ''; - requestContext: APIGatewayEventRequestContext; + requestContext: TContext; isBase64Encoded: boolean; pathParameters: { [name: string]: string } | null; stageVariables: { [name: string]: string } | null; @@ -196,7 +255,7 @@ export declare class Request { [key: string]: any; } -export declare class Response { +export declare class Response { status(code: number): this; sendStatus(code: number): void; @@ -219,13 +278,13 @@ export declare class Response { callback?: ErrorCallback ): Promise; - send(body: any): void; + send(body: TResponse): void; - json(body: any): void; + json(body: TResponse): void; - jsonp(body: any): void; + jsonp(body: TResponse): void; - html(body: any): void; + html(body: string): void; type(type: string): this; @@ -269,62 +328,237 @@ export declare class API { app(namespace: string, package: Package): App; app(packages: App): App; - get( + get< + TResponse = any, + TContext extends RequestContext = APIGatewayContext, + TQuery extends Record = Record< + string, + string | undefined + >, + TParams extends Record = Record< + string, + string | undefined + >, + TBody = any + >( path: string, - ...middlewaresAndHandler: (Middleware | HandlerFunction)[] + ...middlewaresAndHandler: ( + | Middleware + | HandlerFunction + )[] + ): void; + get( + ...middlewaresAndHandler: ( + | Middleware + | HandlerFunction + )[] ): void; - get(...middlewaresAndHandler: (Middleware | HandlerFunction)[]): void; - post( + post< + TResponse = any, + TContext extends RequestContext = APIGatewayContext, + TQuery extends Record = Record< + string, + string | undefined + >, + TParams extends Record = Record< + string, + string | undefined + >, + TBody = any + >( path: string, - ...middlewaresAndHandler: (Middleware | HandlerFunction)[] + ...middlewaresAndHandler: ( + | Middleware + | HandlerFunction + )[] + ): void; + post( + ...middlewaresAndHandler: ( + | Middleware + | HandlerFunction + )[] ): void; - post(...middlewaresAndHandler: (Middleware | HandlerFunction)[]): void; - put( + put< + TResponse = any, + TContext extends RequestContext = APIGatewayContext, + TQuery extends Record = Record< + string, + string | undefined + >, + TParams extends Record = Record< + string, + string | undefined + >, + TBody = any + >( path: string, - ...middlewaresAndHandler: (Middleware | HandlerFunction)[] + ...middlewaresAndHandler: ( + | Middleware + | HandlerFunction + )[] + ): void; + put( + ...middlewaresAndHandler: ( + | Middleware + | HandlerFunction + )[] ): void; - put(...middlewaresAndHandler: (Middleware | HandlerFunction)[]): void; - patch( + patch< + TResponse = any, + TContext extends RequestContext = APIGatewayContext, + TQuery extends Record = Record< + string, + string | undefined + >, + TParams extends Record = Record< + string, + string | undefined + >, + TBody = any + >( path: string, - ...middlewaresAndHandler: (Middleware | HandlerFunction)[] + ...middlewaresAndHandler: ( + | Middleware + | HandlerFunction + )[] + ): void; + patch( + ...middlewaresAndHandler: ( + | Middleware + | HandlerFunction + )[] ): void; - patch(...middlewaresAndHandler: (Middleware | HandlerFunction)[]): void; - delete( + delete< + TResponse = any, + TContext extends RequestContext = APIGatewayContext, + TQuery extends Record = Record< + string, + string | undefined + >, + TParams extends Record = Record< + string, + string | undefined + >, + TBody = any + >( path: string, - ...middlewaresAndHandler: (Middleware | HandlerFunction)[] + ...middlewaresAndHandler: ( + | Middleware + | HandlerFunction + )[] + ): void; + delete( + ...middlewaresAndHandler: HandlerFunction[] ): void; - delete(...middlewaresAndHandler: HandlerFunction[]): void; - options( + options< + TResponse = any, + TContext extends RequestContext = APIGatewayContext, + TQuery extends Record = Record< + string, + string | undefined + >, + TParams extends Record = Record< + string, + string | undefined + >, + TBody = any + >( path: string, - ...middlewaresAndHandler: (Middleware | HandlerFunction)[] + ...middlewaresAndHandler: ( + | Middleware + | HandlerFunction + )[] + ): void; + options( + ...middlewaresAndHandler: ( + | Middleware + | HandlerFunction + )[] ): void; - options(...middlewaresAndHandler: (Middleware | HandlerFunction)[]): void; - head( + head< + TResponse = any, + TContext extends RequestContext = APIGatewayContext, + TQuery extends Record = Record< + string, + string | undefined + >, + TParams extends Record = Record< + string, + string | undefined + >, + TBody = any + >( path: string, - ...middlewaresAndHandler: (Middleware | HandlerFunction)[] + ...middlewaresAndHandler: ( + | Middleware + | HandlerFunction + )[] + ): void; + head( + ...middlewaresAndHandler: ( + | Middleware + | HandlerFunction + )[] ): void; - head(...middlewaresAndHandler: (Middleware | HandlerFunction)[]): void; - any( + any< + TResponse = any, + TContext extends RequestContext = APIGatewayContext, + TQuery extends Record = Record< + string, + string | undefined + >, + TParams extends Record = Record< + string, + string | undefined + >, + TBody = any + >( path: string, - ...middlewaresAndHandler: (Middleware | HandlerFunction)[] + ...middlewaresAndHandler: ( + | Middleware + | HandlerFunction + )[] + ): void; + any( + ...middlewaresAndHandler: ( + | Middleware + | HandlerFunction + )[] ): void; - any(...middlewaresAndHandler: (Middleware | HandlerFunction)[]): void; - METHOD( + METHOD< + TResponse = any, + TContext extends RequestContext = APIGatewayContext, + TQuery extends Record = Record< + string, + string | undefined + >, + TParams extends Record = Record< + string, + string | undefined + >, + TBody = any + >( method: METHODS | METHODS[], path: string, - ...middlewaresAndHandler: (Middleware | HandlerFunction)[] + ...middlewaresAndHandler: ( + | Middleware + | HandlerFunction + )[] ): void; - METHOD( + METHOD( method: METHODS | METHODS[], - ...middlewaresAndHandler: (Middleware | HandlerFunction)[] + ...middlewaresAndHandler: ( + | Middleware + | HandlerFunction + )[] ): void; register( @@ -336,21 +570,55 @@ export declare class API { routes(format: false): string[][]; routes(): string[][]; - use(path: string, ...middleware: Middleware[]): void; - use(paths: string[], ...middleware: Middleware[]): void; - use(...middleware: (Middleware | ErrorHandlingMiddleware)[]): void; + use< + TResponse = any, + TContext extends RequestContext = APIGatewayContext, + TQuery extends Record = Record< + string, + string | undefined + >, + TParams extends Record = Record< + string, + string | undefined + >, + TBody = any + >( + path: string, + ...middleware: Middleware[] + ): void; + use< + TResponse = any, + TContext extends RequestContext = APIGatewayContext, + TQuery extends Record = Record< + string, + string | undefined + >, + TParams extends Record = Record< + string, + string | undefined + >, + TBody = any + >( + paths: string[], + ...middleware: Middleware[] + ): void; + use( + ...middleware: ( + | Middleware + | ErrorHandlingMiddleware + )[] + ): void; - finally(callback: FinallyFunction): void; + finally( + callback: (req: Request, res: Response) => void + ): void; - run( - event: APIGatewayProxyEvent | APIGatewayProxyEventV2, + run( + event: Event, context: Context, - cb: (err: Error, result: any) => void + cb: (err: Error, result: TResponse) => void ): void; - run( - event: APIGatewayProxyEvent | APIGatewayProxyEventV2, - context: Context - ): Promise; + run(event: Event, context: Context): Promise; } export declare class RouteError extends Error { diff --git a/index.test-d.ts b/index.test-d.ts index 782501b..5a3ea9e 100644 --- a/index.test-d.ts +++ b/index.test-d.ts @@ -1,4 +1,4 @@ -import { expectType, expectError } from 'tsd'; +import { expectType } from 'tsd'; import { API, Request, @@ -17,6 +17,11 @@ import { ConfigurationError, ResponseError, FileError, + Event, + ALBContext, + APIGatewayV2Context, + APIGatewayContext, + RegisterOptions, } from './index'; import { APIGatewayProxyEvent, @@ -40,12 +45,71 @@ expectType(options); const req = {} as Request; expectType(req.method); expectType(req.path); -expectType<{ [key: string]: string | undefined }>(req.params); -expectType<{ [key: string]: string | undefined }>(req.query); +expectType>(req.params); +expectType>(req.query); expectType<{ [key: string]: string | undefined }>(req.headers); expectType(req.body); expectType<{ [key: string]: string }>(req.cookies); +expectType(req.requestContext); +type ALBParams = { userId: string }; +type ALBQuery = { filter: string }; +type ALBBody = { data: string }; + +const albReq = {} as Request; +expectType(albReq.requestContext); +expectType<{ filter: string | undefined }>(albReq.query); +expectType<{ userId: string | undefined }>(albReq.params); +expectType<{ data: string }>(albReq.body); + +const typedMiddleware: Middleware< + UserResponse, + ALBContext, + ALBQuery, + ALBParams, + ALBBody +> = (req, res, next) => { + expectType(req.requestContext); + expectType<{ filter: string | undefined }>(req.query); + expectType<{ userId: string | undefined }>(req.params); + expectType<{ data: string }>(req.body); + next(); +}; + +const typedHandler: HandlerFunction< + UserResponse, + ALBContext, + ALBQuery, + ALBParams, + ALBBody +> = (req, res) => { + expectType(req.requestContext); + expectType<{ filter: string | undefined }>(req.query); + expectType<{ userId: string | undefined }>(req.params); + expectType<{ data: string }>(req.body); + res.json({ + id: '123', + name: 'John', + email: 'john@example.com', + }); +}; + +type ApiGwV2Params = { id: string }; +type ApiGwV2Query = { page: string }; +type ApiGwV2Body = { name: string }; + +const apiGwV2Req = {} as Request< + APIGatewayV2Context, + ApiGwV2Query, + ApiGwV2Params, + ApiGwV2Body +>; +expectType(apiGwV2Req.requestContext); +expectType<{ page: string | undefined }>(apiGwV2Req.query); +expectType<{ id: string | undefined }>(apiGwV2Req.params); +expectType<{ name: string }>(apiGwV2Req.body); + +const api = new API(); const apiGwV1Event: APIGatewayProxyEvent = { body: '{"test":"body"}', headers: { 'content-type': 'application/json' }, @@ -146,11 +210,83 @@ const context: Context = { succeed: () => {}, }; -const api = new API(); expectType>(api.run(apiGwV1Event, context)); expectType>(api.run(apiGwV2Event, context)); -// @ts-expect-error ALB events are not supported -expectType>(api.run(albEvent, context)); +expectType>(api.run(albEvent, context)); + +expectType(apiGwV1Event); +expectType(apiGwV2Event); +expectType(albEvent); + +interface UserResponse { + id: string; + name: string; + email: string; +} + +interface ErrorResponse { + code: number; + message: string; +} + +interface UserParams extends Record { + id: string; +} + +interface UserQuery extends Record { + fields: string; +} + +interface UserBody { + name: string; + email: string; +} + +const testApi = new API(); + +const albHandler: HandlerFunction< + UserResponse, + ALBContext, + UserQuery, + UserParams, + UserBody +> = (req, res) => { + const { id } = req.params; + const { fields } = req.query; + const { name, email } = req.body; + const { elb } = req.requestContext; + + res.json({ + id: id || 'new', + name, + email, + }); +}; + +const apiGwV2Handler: HandlerFunction = ( + req, + res +) => { + const { requestContext } = req; + const { domainName, domainPrefix } = requestContext; + + res.json({ + id: req.params.id || '', + name: 'John', + email: 'john@example.com', + }); +}; + +testApi.post('/users/:id', albHandler); +testApi.get('/users/:id', apiGwV2Handler); + +expectType>( + testApi.run(apiGwV1Event, context) +); + +testApi.run(apiGwV1Event, context, (err, result) => { + expectType(result); +}); const res = {} as Response; expectType(res.status(200)); @@ -173,21 +309,6 @@ expectType( expectType(res.redirect('/new-path')); -const middleware: Middleware = (req, res, next) => { - next(); -}; -expectType(middleware); - -const errorMiddleware: ErrorHandlingMiddleware = (error, req, res, next) => { - res.status(500).json({ error: error.message }); -}; -expectType(errorMiddleware); - -const handler: HandlerFunction = (req, res) => { - res.json({ success: true }); -}; -expectType(handler); - const cookieOptions: CookieOptions = { domain: 'example.com', httpOnly: true, @@ -255,3 +376,114 @@ const fileError = new FileError('File not found', { syscall: 'open', }); expectType(fileError); + +const defaultReq = {} as Request; +expectType(defaultReq.requestContext); +expectType>(defaultReq.query); +expectType>(defaultReq.params); +expectType(defaultReq.body); + +const defaultMiddleware: Middleware = (req, res, next) => { + expectType(req.requestContext); + expectType>(req.query); + expectType>(req.params); + expectType(req.body); + expectType>(res); + next(); +}; + +const errorMiddleware: ErrorHandlingMiddleware = ( + error, + req, + res, + next +) => { + expectType(req.requestContext); + expectType>(req.query); + expectType>(req.params); + expectType(req.body); + expectType>(res); + res.status(500).json({ code: 500, message: error.message }); + next(); +}; + +const partialContextReq = {} as Request; +expectType(partialContextReq.requestContext); +expectType>(partialContextReq.query); +expectType>(partialContextReq.params); +expectType(partialContextReq.body); + +const partialContextAndQueryReq = {} as Request; +expectType(partialContextAndQueryReq.requestContext); +expectType<{ filter: string | undefined }>(partialContextAndQueryReq.query); +expectType>( + partialContextAndQueryReq.params +); +expectType(partialContextAndQueryReq.body); + +const stringResponse = {} as Response; +expectType(stringResponse.json('test')); +expectType(stringResponse.send('test')); + +const numberResponse = {} as Response; +expectType(numberResponse.json(42)); +expectType(numberResponse.send(42)); + +testApi.get('/echo', (req, res) => res.send('hello')); + +testApi.post('/count', (req, res) => res.json(42)); + +testApi.put>( + '/flag', + (req, res) => res.json(true) +); + +testApi.use((req, res, next) => { + expectType(req.requestContext); + next(); +}); + +testApi.use('/users', (req, res, next) => { + expectType(req.requestContext); + next(); +}); + +testApi.use( + ['/users', '/admin'], + (req, res, next) => { + expectType(req.requestContext); + expectType<{ fields: string | undefined }>(req.query); + next(); + } +); + +testApi.finally((req, res) => { + expectType(req); + expectType>(res); +}); + +testApi.METHOD('GET', '/users', (req, res) => { + res.json({ id: '1', name: 'John', email: 'john@example.com' }); +}); + +testApi.METHOD( + ['GET', 'POST'], + '/users', + (req, res) => { + expectType(req.requestContext); + res.json({ id: '1', name: 'John', email: 'john@example.com' }); + } +); + +testApi.register( + (api, options) => { + expectType(api); + expectType(options); + }, + { prefix: '/api' } +); + +const routesArray = testApi.routes(false); +expectType(routesArray); + +testApi.routes(true); From 57e59e10a77d99b802b89d2ad26a124f9be99d20 Mon Sep 17 00:00:00 2001 From: Naor Peled Date: Thu, 6 Feb 2025 21:25:16 +0200 Subject: [PATCH 02/18] . --- index.test-d.ts | 514 ++++++++---------------------------------------- 1 file changed, 87 insertions(+), 427 deletions(-) diff --git a/index.test-d.ts b/index.test-d.ts index 5a3ea9e..46e6668 100644 --- a/index.test-d.ts +++ b/index.test-d.ts @@ -3,220 +3,17 @@ import { API, Request, Response, - CookieOptions, - CorsOptions, - FileOptions, - LoggerOptions, - Options, - Middleware, - ErrorHandlingMiddleware, - HandlerFunction, - METHODS, RouteError, MethodError, - ConfigurationError, - ResponseError, - FileError, - Event, ALBContext, APIGatewayV2Context, APIGatewayContext, - RegisterOptions, + METHODS, + ErrorHandlingMiddleware, + HandlerFunction, + Middleware, } from './index'; -import { - APIGatewayProxyEvent, - APIGatewayProxyEventV2, - Context, - ALBEvent, -} from 'aws-lambda'; - -const options: Options = { - base: '/api', - version: 'v1', - logger: { - level: 'info', - access: true, - timestamp: true, - }, - compression: true, -}; -expectType(options); - -const req = {} as Request; -expectType(req.method); -expectType(req.path); -expectType>(req.params); -expectType>(req.query); -expectType<{ [key: string]: string | undefined }>(req.headers); -expectType(req.body); -expectType<{ [key: string]: string }>(req.cookies); -expectType(req.requestContext); - -type ALBParams = { userId: string }; -type ALBQuery = { filter: string }; -type ALBBody = { data: string }; - -const albReq = {} as Request; -expectType(albReq.requestContext); -expectType<{ filter: string | undefined }>(albReq.query); -expectType<{ userId: string | undefined }>(albReq.params); -expectType<{ data: string }>(albReq.body); - -const typedMiddleware: Middleware< - UserResponse, - ALBContext, - ALBQuery, - ALBParams, - ALBBody -> = (req, res, next) => { - expectType(req.requestContext); - expectType<{ filter: string | undefined }>(req.query); - expectType<{ userId: string | undefined }>(req.params); - expectType<{ data: string }>(req.body); - next(); -}; - -const typedHandler: HandlerFunction< - UserResponse, - ALBContext, - ALBQuery, - ALBParams, - ALBBody -> = (req, res) => { - expectType(req.requestContext); - expectType<{ filter: string | undefined }>(req.query); - expectType<{ userId: string | undefined }>(req.params); - expectType<{ data: string }>(req.body); - res.json({ - id: '123', - name: 'John', - email: 'john@example.com', - }); -}; - -type ApiGwV2Params = { id: string }; -type ApiGwV2Query = { page: string }; -type ApiGwV2Body = { name: string }; - -const apiGwV2Req = {} as Request< - APIGatewayV2Context, - ApiGwV2Query, - ApiGwV2Params, - ApiGwV2Body ->; -expectType(apiGwV2Req.requestContext); -expectType<{ page: string | undefined }>(apiGwV2Req.query); -expectType<{ id: string | undefined }>(apiGwV2Req.params); -expectType<{ name: string }>(apiGwV2Req.body); - -const api = new API(); -const apiGwV1Event: APIGatewayProxyEvent = { - body: '{"test":"body"}', - headers: { 'content-type': 'application/json' }, - multiValueHeaders: { 'content-type': ['application/json'] }, - httpMethod: 'POST', - isBase64Encoded: false, - path: '/test', - pathParameters: { id: '123' }, - queryStringParameters: { query: 'test' }, - multiValueQueryStringParameters: { query: ['test'] }, - stageVariables: { stage: 'dev' }, - requestContext: { - accountId: '', - apiId: '', - authorizer: {}, - protocol: '', - httpMethod: 'POST', - identity: { - accessKey: null, - accountId: null, - apiKey: null, - apiKeyId: null, - caller: null, - cognitoAuthenticationProvider: null, - cognitoAuthenticationType: null, - cognitoIdentityId: null, - cognitoIdentityPoolId: null, - principalOrgId: null, - sourceIp: '', - user: null, - userAgent: null, - userArn: null, - }, - path: '/test', - stage: 'dev', - requestId: '', - requestTimeEpoch: 0, - resourceId: '', - resourcePath: '', - }, - resource: '', -}; - -const apiGwV2Event: APIGatewayProxyEventV2 = { - version: '2.0', - routeKey: 'POST /test', - rawPath: '/test', - rawQueryString: 'query=test', - headers: { 'content-type': 'application/json' }, - requestContext: { - accountId: '', - apiId: '', - domainName: '', - domainPrefix: '', - http: { - method: 'POST', - path: '/test', - protocol: 'HTTP/1.1', - sourceIp: '', - userAgent: '', - }, - requestId: '', - routeKey: 'POST /test', - stage: 'dev', - time: '', - timeEpoch: 0, - }, - body: '{"test":"body"}', - isBase64Encoded: false, -}; - -const albEvent: ALBEvent = { - requestContext: { - elb: { - targetGroupArn: '', - }, - }, - httpMethod: 'GET', - path: '/test', - queryStringParameters: {}, - headers: {}, - body: '', - isBase64Encoded: false, -}; - -const context: Context = { - callbackWaitsForEmptyEventLoop: true, - functionName: '', - functionVersion: '', - invokedFunctionArn: '', - memoryLimitInMB: '', - awsRequestId: '', - logGroupName: '', - logStreamName: '', - getRemainingTimeInMillis: () => 0, - done: () => {}, - fail: () => {}, - succeed: () => {}, -}; - -expectType>(api.run(apiGwV1Event, context)); -expectType>(api.run(apiGwV2Event, context)); -expectType>(api.run(albEvent, context)); - -expectType(apiGwV1Event); -expectType(apiGwV2Event); -expectType(albEvent); +import { APIGatewayProxyEvent, Context } from 'aws-lambda'; interface UserResponse { id: string; @@ -224,266 +21,129 @@ interface UserResponse { email: string; } -interface ErrorResponse { - code: number; - message: string; +interface UserQuery extends Record { + fields: string; } interface UserParams extends Record { id: string; } -interface UserQuery extends Record { - fields: string; -} - interface UserBody { name: string; email: string; } -const testApi = new API(); - -const albHandler: HandlerFunction< - UserResponse, - ALBContext, - UserQuery, - UserParams, - UserBody -> = (req, res) => { - const { id } = req.params; - const { fields } = req.query; - const { name, email } = req.body; - const { elb } = req.requestContext; - - res.json({ - id: id || 'new', - name, - email, - }); -}; - -const apiGwV2Handler: HandlerFunction = ( - req, - res -) => { - const { requestContext } = req; - const { domainName, domainPrefix } = requestContext; +const api = new API(); - res.json({ - id: req.params.id || '', - name: 'John', - email: 'john@example.com', - }); -}; +const defaultReq = {} as Request; +expectType(defaultReq.requestContext); +expectType>(defaultReq.query); +expectType>(defaultReq.params); +expectType(defaultReq.body); -testApi.post('/users/:id', albHandler); -testApi.get('/users/:id', apiGwV2Handler); +const albReq = {} as Request; +expectType(albReq.requestContext); +expectType(albReq.query); +expectType(albReq.params); +expectType(albReq.body); -expectType>( - testApi.run(apiGwV1Event, context) -); +const apiGwV2Req = {} as Request; +expectType(apiGwV2Req.requestContext); -testApi.run(apiGwV1Event, context, (err, result) => { - expectType(result); -}); +const stringResponse = {} as Response; +expectType(stringResponse.json('test')); +expectType(stringResponse.send('test')); -const res = {} as Response; -expectType(res.status(200)); -expectType(res.header('Content-Type', 'application/json')); -expectType( - res.cookie('session', 'value', { - httpOnly: true, - secure: true, +const userResponse = {} as Response; +expectType( + userResponse.json({ + id: '1', + name: 'John', + email: 'test@test.com', }) ); -expectType(res.send({ message: 'test' })); -expectType(res.json({ message: 'test' })); -expectType(res.html('
test
')); +const errorHandler: ErrorHandlingMiddleware = ( + error, + req, + res, + next +) => { + expectType(error); + expectType(req); + expectType>(res); + next(); +}; -expectType(res.error('Test error')); -expectType( - res.error(500, 'Server error', { details: 'Additional info' }) -); +const routeError = new RouteError('Not found', '/users'); +expectType(routeError); -expectType(res.redirect('/new-path')); +const methodError = new MethodError('Method not allowed', 'POST', '/users'); +expectType(methodError); -const cookieOptions: CookieOptions = { - domain: 'example.com', - httpOnly: true, - secure: true, - sameSite: 'Strict', +const getHandler: HandlerFunction = (req, res) => { + res.json({ id: '1', name: 'John', email: 'test@test.com' }); }; -expectType(cookieOptions); +api.METHOD('GET', '/users', getHandler); -const corsOptions: CorsOptions = { - origin: '*', - methods: 'GET,POST', - headers: 'Content-Type,Authorization', - credentials: true, +const multiHandler: HandlerFunction = (req, res) => { + res.json({ id: '1', name: 'John', email: 'test@test.com' }); }; -expectType(corsOptions); +api.METHOD(['GET', 'POST'], '/users', multiHandler); -const fileOptions: FileOptions = { - maxAge: 3600, - root: '/public', - lastModified: true, - headers: { 'Cache-Control': 'public' }, +const getUserHandler: HandlerFunction = (req, res) => { + expectType>(req); + expectType(req.requestContext); + res.json({ id: '1', name: 'John', email: 'test@test.com' }); }; -expectType(fileOptions); +api.get('/users', getUserHandler); -const loggerOptions: LoggerOptions = { - level: 'info', - access: true, - timestamp: true, - sampling: { - target: 10, - rate: 0.1, - }, +const postUserHandler: HandlerFunction< + UserResponse, + ALBContext, + UserQuery, + UserParams, + UserBody +> = (req, res) => { + expectType(req.requestContext); + expectType(req.query); + expectType(req.params); + expectType(req.body); + res.json({ id: '1', name: req.body.name, email: req.body.email }); }; -expectType(loggerOptions); - -const methods: METHODS[] = [ - 'GET', - 'POST', - 'PUT', - 'DELETE', - 'OPTIONS', - 'HEAD', - 'ANY', -]; -expectType(methods); - -const routeError = new RouteError('Route not found', '/api/test'); -expectType(routeError); - -const methodError = new MethodError( - 'Method not allowed', - 'POST' as METHODS, - '/api/test' +api.post( + '/users', + postUserHandler ); -expectType(methodError); - -const configError = new ConfigurationError('Invalid configuration'); -expectType(configError); - -const responseError = new ResponseError('Response error', 500); -expectType(responseError); -const fileError = new FileError('File not found', { - code: 'ENOENT', - syscall: 'open', -}); -expectType(fileError); - -const defaultReq = {} as Request; -expectType(defaultReq.requestContext); -expectType>(defaultReq.query); -expectType>(defaultReq.params); -expectType(defaultReq.body); - -const defaultMiddleware: Middleware = (req, res, next) => { - expectType(req.requestContext); - expectType>(req.query); - expectType>(req.params); - expectType(req.body); - expectType>(res); +const userMiddleware: Middleware = (req, res, next) => { + expectType>(res); next(); }; +api.use(userMiddleware); -const errorMiddleware: ErrorHandlingMiddleware = ( - error, +const albMiddleware: Middleware = ( req, res, next ) => { - expectType(req.requestContext); - expectType>(req.query); - expectType>(req.params); - expectType(req.body); - expectType>(res); - res.status(500).json({ code: 500, message: error.message }); - next(); -}; - -const partialContextReq = {} as Request; -expectType(partialContextReq.requestContext); -expectType>(partialContextReq.query); -expectType>(partialContextReq.params); -expectType(partialContextReq.body); - -const partialContextAndQueryReq = {} as Request; -expectType(partialContextAndQueryReq.requestContext); -expectType<{ filter: string | undefined }>(partialContextAndQueryReq.query); -expectType>( - partialContextAndQueryReq.params -); -expectType(partialContextAndQueryReq.body); - -const stringResponse = {} as Response; -expectType(stringResponse.json('test')); -expectType(stringResponse.send('test')); - -const numberResponse = {} as Response; -expectType(numberResponse.json(42)); -expectType(numberResponse.send(42)); - -testApi.get('/echo', (req, res) => res.send('hello')); - -testApi.post('/count', (req, res) => res.json(42)); - -testApi.put>( - '/flag', - (req, res) => res.json(true) -); - -testApi.use((req, res, next) => { - expectType(req.requestContext); - next(); -}); - -testApi.use('/users', (req, res, next) => { expectType(req.requestContext); + expectType>(res); next(); -}); - -testApi.use( - ['/users', '/admin'], - (req, res, next) => { - expectType(req.requestContext); - expectType<{ fields: string | undefined }>(req.query); - next(); - } -); +}; +api.use('/users', albMiddleware); -testApi.finally((req, res) => { +const finallyHandler: HandlerFunction = (req, res) => { expectType(req); - expectType>(res); -}); - -testApi.METHOD('GET', '/users', (req, res) => { - res.json({ id: '1', name: 'John', email: 'john@example.com' }); -}); - -testApi.METHOD( - ['GET', 'POST'], - '/users', - (req, res) => { - expectType(req.requestContext); - res.json({ id: '1', name: 'John', email: 'john@example.com' }); - } -); - -testApi.register( - (api, options) => { - expectType(api); - expectType(options); - }, - { prefix: '/api' } -); + expectType(res); +}; +api.finally(finallyHandler); -const routesArray = testApi.routes(false); -expectType(routesArray); +const result = api.run({} as APIGatewayProxyEvent, {} as Context); +expectType>(result); -testApi.routes(true); +api.run({} as APIGatewayProxyEvent, {} as Context, (err, res) => { + expectType(err); + expectType(res); +}); From 0f35ebacf166c25022c092522bffd7d1faab74a4 Mon Sep 17 00:00:00 2001 From: Naor Peled Date: Sat, 8 Feb 2025 12:23:30 +0200 Subject: [PATCH 03/18] . --- index.d.ts | 5 +- index.test-d.ts | 148 ++++++++++++++++++++++++++++++++++++++---------- 2 files changed, 122 insertions(+), 31 deletions(-) diff --git a/index.d.ts b/index.d.ts index 0690239..5dde660 100644 --- a/index.d.ts +++ b/index.d.ts @@ -452,7 +452,10 @@ export declare class API { )[] ): void; delete( - ...middlewaresAndHandler: HandlerFunction[] + ...middlewaresAndHandler: ( + | Middleware + | HandlerFunction + )[] ): void; options< diff --git a/index.test-d.ts b/index.test-d.ts index 46e6668..78b8fab 100644 --- a/index.test-d.ts +++ b/index.test-d.ts @@ -82,48 +82,137 @@ expectType(routeError); const methodError = new MethodError('Method not allowed', 'POST', '/users'); expectType(methodError); -const getHandler: HandlerFunction = (req, res) => { - res.json({ id: '1', name: 'John', email: 'test@test.com' }); -}; -api.METHOD('GET', '/users', getHandler); - -const multiHandler: HandlerFunction = (req, res) => { - res.json({ id: '1', name: 'John', email: 'test@test.com' }); +const authMiddleware: Middleware = (req, res, next) => { + expectType>(res); + next(); }; -api.METHOD(['GET', 'POST'], '/users', multiHandler); -const getUserHandler: HandlerFunction = (req, res) => { - expectType>(req); - expectType(req.requestContext); - res.json({ id: '1', name: 'John', email: 'test@test.com' }); +const validationMiddleware: Middleware = (req, res, next) => { + expectType>(res); + next(); }; -api.get('/users', getUserHandler); -const postUserHandler: HandlerFunction< +const albAuthMiddleware: Middleware< UserResponse, ALBContext, UserQuery, UserParams, UserBody -> = (req, res) => { +> = (req, res, next) => { expectType(req.requestContext); - expectType(req.query); - expectType(req.params); - expectType(req.body); - res.json({ id: '1', name: req.body.name, email: req.body.email }); + expectType>(res); + next(); }; + +const handler: HandlerFunction = (req, res) => { + res.json({ id: '1', name: 'John', email: 'test@test.com' }); +}; + +api.get( + '/users', + authMiddleware, + validationMiddleware, + (req, res) => { + expectType>(req); + res.json({ id: '1', name: 'John', email: 'test@test.com' }); + } +); + +api.post( + '/users', + authMiddleware, + validationMiddleware, + (req, res) => { + expectType>(req); + res.json({ id: '1', name: 'John', email: 'test@test.com' }); + } +); + +api.put( + '/users/:id', + authMiddleware, + validationMiddleware, + (req, res) => { + expectType>(req); + res.json({ id: '1', name: 'John', email: 'test@test.com' }); + } +); + +api.patch( + '/users/:id', + authMiddleware, + validationMiddleware, + (req, res) => { + expectType>(req); + res.json({ id: '1', name: 'John', email: 'test@test.com' }); + } +); + +api.delete( + '/users/:id', + authMiddleware, + validationMiddleware, + (req, res) => { + expectType>(req); + res.json({ id: '1', name: 'John', email: 'test@test.com' }); + } +); + +api.options( + '/users', + authMiddleware, + validationMiddleware, + (req, res) => { + expectType>(req); + res.json({ id: '1', name: 'John', email: 'test@test.com' }); + } +); + +api.head( + '/users', + authMiddleware, + validationMiddleware, + (req, res) => { + expectType>(req); + res.json({ id: '1', name: 'John', email: 'test@test.com' }); + } +); + +api.any( + '/users', + authMiddleware, + validationMiddleware, + (req, res) => { + expectType>(req); + res.json({ id: '1', name: 'John', email: 'test@test.com' }); + } +); + api.post( '/users', - postUserHandler + albAuthMiddleware, + (req, res) => { + expectType(req.requestContext); + expectType(req.query); + expectType(req.params); + expectType(req.body); + res.json({ id: '1', name: req.body.name, email: req.body.email }); + } ); -const userMiddleware: Middleware = (req, res, next) => { - expectType>(res); - next(); -}; -api.use(userMiddleware); +api.METHOD( + ['GET', 'POST'], + '/users', + authMiddleware, + validationMiddleware, + handler +); + +// Test middleware without path +api.use(authMiddleware); -const albMiddleware: Middleware = ( +// Test middleware with path and ALB context +const albRouteMiddleware: Middleware = ( req, res, next @@ -132,13 +221,12 @@ const albMiddleware: Middleware = ( expectType>(res); next(); }; -api.use('/users', albMiddleware); +api.use('/users', albRouteMiddleware); -const finallyHandler: HandlerFunction = (req, res) => { +api.finally((req, res) => { expectType(req); expectType(res); -}; -api.finally(finallyHandler); +}); const result = api.run({} as APIGatewayProxyEvent, {} as Context); expectType>(result); From 2ba4c9992af123adad22d12e03f9b19056f3286a Mon Sep 17 00:00:00 2001 From: Naor Peled Date: Sat, 8 Feb 2025 12:27:11 +0200 Subject: [PATCH 04/18] . --- README.md | 105 ++++++++++++++++++++++++++++++++++++++++-------------- 1 file changed, 78 insertions(+), 27 deletions(-) diff --git a/README.md b/README.md index a08c981..b171577 100644 --- a/README.md +++ b/README.md @@ -34,6 +34,84 @@ exports.handler = async (event, context) => { For a full tutorial see [How To: Build a Serverless API with Serverless, AWS Lambda and Lambda API](https://www.jeremydaly.com/build-serverless-api-serverless-aws-lambda-lambda-api/). +## TypeScript Support + +Lambda API includes comprehensive TypeScript definitions out of the box. You can leverage type safety across your entire API: + +```typescript +import { API, Request, Response } from 'lambda-api'; + +// Define your response type +interface UserResponse { + id: string; + name: string; + email: string; +} + +// Create a typed API instance +const api = new API(); + +// Routes with type-safe request/response +api.get('/users/:id', (req, res) => { + res.json({ + id: req.params.id, + name: 'John', + email: 'john@example.com' + }); +}); + +// Middleware with type checking +const authMiddleware: Middleware = (req, res, next) => { + // TypeScript will ensure type safety + next(); +}; + +// Full type support for complex scenarios +interface UserQuery { fields: string } +interface UserParams { id: string } +interface UserBody { name: string; email: string } + +api.post( + '/users', + (req, res) => { + // Full type safety for: + req.query.fields; // UserQuery + req.params.id; // UserParams + req.body.name; // UserBody + req.requestContext; // ALBContext + + res.json({ + id: '1', + name: req.body.name, + email: req.body.email + }); + } +); + +// Error handling with types +const errorHandler: ErrorHandlingMiddleware = ( + error, + req, + res, + next +) => { + res.status(500).json({ + id: 'error', + name: error.name, + email: error.message + }); +}; +``` + +Key TypeScript Features: +- Full type inference for request and response objects +- Generic type parameters for response types +- Support for API Gateway and ALB contexts +- Type-safe query parameters, path parameters, and request body +- Middleware and error handler type definitions +- Automatic type inference for all HTTP methods +- Type safety for cookies, headers, and other API features + ## Why Another Web Framework? Express.js, Fastify, Koa, Restify, and Hapi are just a few of the many amazing web frameworks out there for Node.js. So why build yet another one when there are so many great options already? One word: **DEPENDENCIES**. @@ -1479,33 +1557,6 @@ Simply create a `{proxy+}` route that uses the `ANY` method and all requests wil If you are using persistent connections in your function routes (such as AWS RDS or Elasticache), be sure to set `context.callbackWaitsForEmptyEventLoop = false;` in your main handler. This will allow the freezing of connections and will prevent Lambda from hanging on open connections. See [here](https://www.jeremydaly.com/reuse-database-connections-aws-lambda/) for more information. -## TypeScript Support - -An `index.d.ts` declaration file has been included for use with your TypeScript projects (thanks @hassankhan). Please feel free to make suggestions and contributions to keep this up-to-date with future releases. - -**TypeScript Example** - -```typescript -// import AWS Lambda types -import { APIGatewayEvent, Context } from 'aws-lambda'; -// import Lambda API default function -import createAPI from 'lambda-api'; - -// instantiate framework -const api = createAPI(); - -// Define a route -api.get('/status', async (req, res) => { - return { status: 'ok' }; -}); - -// Declare your Lambda handler -exports.run = async (event: APIGatewayEvent, context: Context) => { - // Run the request - return await api.run(event, context); -}; -``` - ## Contributions Contributions, ideas and bug reports are welcome and greatly appreciated. Please add [issues](https://github.com/jeremydaly/lambda-api/issues) for suggestions and bug reports or create a pull request. From 464c9825f7abb66a83dc4fc0fa14e8c43994d4f0 Mon Sep 17 00:00:00 2001 From: Naor Peled Date: Sat, 8 Feb 2025 12:33:57 +0200 Subject: [PATCH 05/18] . --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index b171577..017c3eb 100644 --- a/README.md +++ b/README.md @@ -1541,7 +1541,7 @@ Lambda API automatically parses this information to create a normalized `REQUEST ## ALB Integration -AWS recently added support for Lambda functions as targets for Application Load Balancers. While the events from ALBs are similar to API Gateway, there are a number of differences that would require code changes based on implementation. Lambda API detects the event `interface` and automatically normalizes the `REQUEST` object. It also correctly formats the `RESPONSE` (supporting both multi-header and non-multi-header mode) for you. This allows you to call your Lambda function from API Gateway, ALB, or both, without requiring any code changes. +AWS supports Lambda functions as targets for Application Load Balancers. While the events from ALBs are similar to API Gateway, there are a number of differences that would require code changes based on implementation. Lambda API detects the event `interface` and automatically normalizes the `REQUEST` object. It also correctly formats the `RESPONSE` (supporting both multi-header and non-multi-header mode) for you. This allows you to call your Lambda function from API Gateway, ALB, or both, without requiring any code changes. Please note that ALB events do not contain all of the same headers as API Gateway (such as `clientType`), but Lambda API provides defaults for seamless integration between the interfaces. ALB also automatically enables binary support, giving you the ability to serve images and other binary file types. Lambda API reads the `path` parameter supplied by the ALB event and uses that to route your requests. If you specify a wildcard in your listener rule, then all matching paths will be forwarded to your Lambda function. Lambda API's routing system can be used to process these routes just like with API Gateway. This includes static paths, parameterized paths, wildcards, middleware, etc. From 03f3c29b3530b562135a4ed20ff5cf28e04a55f2 Mon Sep 17 00:00:00 2001 From: Naor Peled Date: Sat, 8 Feb 2025 15:05:28 +0200 Subject: [PATCH 06/18] . --- README.md | 156 +++++++++++++++++++++++ index.d.ts | 209 +++++++------------------------ index.js | 21 ++-- index.test-d.ts | 311 +++++++++++++++++++++------------------------- lib/typeguards.js | 19 +++ 5 files changed, 374 insertions(+), 342 deletions(-) create mode 100644 lib/typeguards.js diff --git a/README.md b/README.md index 017c3eb..26eb0af 100644 --- a/README.md +++ b/README.md @@ -205,6 +205,7 @@ Whatever you decide is best for your use case, **Lambda API** is there to suppor - [TypeScript Support](#typescript-support) - [Contributions](#contributions) - [Are you using Lambda API?](#are-you-using-lambda-api) +- [Handling Multiple Request Sources](#handling-multiple-request-sources) ## Installation @@ -1564,3 +1565,158 @@ Contributions, ideas and bug reports are welcome and greatly appreciated. Please ## Are you using Lambda API? If you're using Lambda API and finding it useful, hit me up on [Twitter](https://twitter.com/jeremy_daly) or email me at contact[at]jeremydaly.com. I'd love to hear your stories, ideas, and even your complaints! + +## Type-Safe Middleware and Extensions + +Lambda API provides full TypeScript support with type-safe middleware and request/response extensions. Here are the recommended patterns: + +### Extending Request and Response Types + +```typescript +declare module 'lambda-api' { + interface Request { + user?: { + id: string; + roles: string[]; + email: string; + }; + } +} + +function hasUser(req: Request): req is Request & { user: { id: string; roles: string[]; email: string; } } { + return 'user' in req && req.user !== undefined; +} + +const authMiddleware: Middleware = (req, res, next) => { + req.user = { + id: '123', + roles: ['admin'], + email: 'user@example.com' + }; + next(); +}; + +api.get('/protected', (req, res) => { + if (hasUser(req)) { + const { id, roles, email } = req.user; + res.json({ message: `Hello ${email}` }); + } +}); +``` + +### Response Extensions + +```typescript +declare module 'lambda-api' { + interface Response { + sendWithTimestamp?: (data: any) => void; + } +} + +const responseEnhancer: Middleware = (req, res, next) => { + res.sendWithTimestamp = (data: any) => { + res.json({ + ...data, + timestamp: Date.now() + }); + }; + next(); +}; + +api.get('/users', (req, res) => { + res.sendWithTimestamp({ name: 'John' }); +}); +``` + +### Using Built-in Auth Property + +```typescript +interface AuthInfo { + userId: string; + roles: string[]; + type: 'Bearer' | 'Basic' | 'OAuth' | 'Digest' | 'none'; + value: string | null; +} + +function hasAuth(req: Request): req is Request & { auth: AuthInfo } { + return 'auth' in req && req.auth?.type !== undefined; +} + +const authMiddleware: Middleware = (req, res, next) => { + req.auth = { + userId: '123', + roles: ['user'], + type: 'Bearer', + value: 'token123' + }; + next(); +}; +``` + +### Type Safety Examples + +```typescript +function hasUser(req: Request): req is Request & { user: UserType } { + return 'user' in req && req.user !== undefined; +} + +interface QueryParams { + limit?: string; + offset?: string; +} + +api.get( + '/users', + (req, res) => { + const { limit, offset } = req.query; + res.json({ /* ... */ }); + } +); + +interface CreateUserBody { + name: string; + email: string; +} + +api.post( + '/users', + (req, res) => { + const { name, email } = req.body; + res.json({ /* ... */ }); + } +); + +const withUser = (handler: HandlerFunction): HandlerFunction => { + return (req, res) => { + if (!hasUser(req)) { + return res.status(401).json({ error: 'Unauthorized' }); + } + return handler(req, res); + }; +}; + +api.get('/protected', withUser(handler)); +``` + +## Handling Multiple Request Sources + +```typescript +import { isApiGatewayContext, isApiGatewayV2Context, isAlbContext } from 'lambda-api'; + +api.get('/api-gateway', (req, res) => { + console.log(req.requestContext.identity); +}); + +api.get('/alb', (req, res) => { + console.log(req.requestContext.elb); +}); + +api.get('/any', (req, res) => { + if (isApiGatewayContext(req.requestContext)) { + console.log(req.requestContext.identity); + } else if (isAlbContext(req.requestContext)) { + console.log(req.requestContext.elb); + } +}); +``` + diff --git a/index.d.ts b/index.d.ts index 5dde660..908538b 100644 --- a/index.d.ts +++ b/index.d.ts @@ -49,7 +49,7 @@ export type ALBContext = ALBEvent['requestContext']; export type APIGatewayV2Context = APIGatewayProxyEventV2['requestContext']; export type APIGatewayContext = APIGatewayEventRequestContext; -export declare type RequestContext = +export type RequestContext = | APIGatewayContext | APIGatewayV2Context | ALBContext; @@ -77,6 +77,8 @@ export declare type Middleware< next: NextFunction ) => void; +export declare type NextFunction = (error?: Error) => void; + export declare type ErrorHandlingMiddleware< TResponse = any, TContext extends RequestContext = APIGatewayContext, @@ -96,8 +98,18 @@ export declare type ErrorHandlingMiddleware< next: NextFunction ) => void; -export declare type ErrorCallback = (error?: Error) => void; -export declare type HandlerFunction< +export type METHODS = + | 'GET' + | 'POST' + | 'PUT' + | 'PATCH' + | 'DELETE' + | 'HEAD' + | 'OPTIONS'; + +export type LoggerFunction = (message: string, ...args: any[]) => void; + +export type HandlerFunction< TResponse = any, TContext extends RequestContext = APIGatewayContext, TQuery extends Record = Record< @@ -112,80 +124,15 @@ export declare type HandlerFunction< > = ( req: Request, res: Response -) => void | TResponse | Promise; - -export declare type LoggerFunction = ( - message?: any, - additionalInfo?: LoggerFunctionAdditionalInfo ) => void; -export declare type LoggerFunctionAdditionalInfo = - | string - | number - | boolean - | null - | LoggerFunctionAdditionalInfo[] - | { [key: string]: LoggerFunctionAdditionalInfo }; - -export declare type NextFunction = () => void; -export declare type TimestampFunction = () => string; -export declare type SerializerFunction = (body: object) => string; -export declare type FinallyFunction = (req: Request, res: Response) => void; -export declare type METHODS = - | 'GET' - | 'POST' - | 'PUT' - | 'PATCH' - | 'DELETE' - | 'OPTIONS' - | 'HEAD' - | 'ANY'; - -export declare interface SamplingOptions { - route?: string; - target?: number; - rate?: number; - period?: number; - method?: string | string[]; -} - -export declare interface LoggerOptions { - access?: boolean | string; - customKey?: string; - errorLogging?: boolean; - detail?: boolean; - level?: string; - levels?: { - [key: string]: string; - }; - messageKey?: string; - nested?: boolean; - timestamp?: boolean | TimestampFunction; - sampling?: { - target?: number; - rate?: number; - period?: number; - rules?: SamplingOptions[]; - }; - serializers?: { - [name: string]: (prop: any) => any; - }; - stack?: boolean; -} -export declare interface Options { - base?: string; - callbackName?: string; - logger?: boolean | LoggerOptions; - mimeTypes?: { - [key: string]: string; +export interface Options { + logger?: { + level?: string; + format?: string; + [key: string]: any; }; - serializer?: SerializerFunction; - version?: string; - errorHeaderWhitelist?: string[]; - isBase64?: boolean; - compression?: boolean; - headers?: object; - s3Config?: S3ClientConfig; + [key: string]: any; } export declare class Request< @@ -257,71 +204,48 @@ export declare class Request< export declare class Response { status(code: number): this; - sendStatus(code: number): void; - header(key: string, value?: string | Array, append?: boolean): this; - getHeader(key: string): string; - getHeaders(): { [key: string]: string }; - setHeader(...args: Parameters): this; - hasHeader(key: string): boolean; - removeHeader(key: string): this; - getLink( s3Path: string, expires?: number, callback?: ErrorCallback ): Promise; - send(body: TResponse): void; - json(body: TResponse): void; - jsonp(body: TResponse): void; - html(body: string): void; - type(type: string): this; - location(path: string): this; - redirect(status: number, path: string): void; redirect(path: string): void; - cors(options: CorsOptions): this; - error(message: string, detail?: any): void; error(code: number, message: string, detail?: any): void; - cookie(name: string, value: string, options?: CookieOptions): this; - clearCookie(name: string, options?: CookieOptions): this; - etag(enable?: boolean): this; - cache(age?: boolean | number | string, private?: boolean): this; - modified(date: boolean | string | Date): this; - attachment(fileName?: string): this; - download( file: string | Buffer, fileName?: string, options?: FileOptions, callback?: ErrorCallback ): void; - sendFile( file: string | Buffer, options?: FileOptions, callback?: ErrorCallback ): Promise; + + [key: string]: any; } export declare class API { @@ -347,12 +271,6 @@ export declare class API { | HandlerFunction )[] ): void; - get( - ...middlewaresAndHandler: ( - | Middleware - | HandlerFunction - )[] - ): void; post< TResponse = any, @@ -373,12 +291,6 @@ export declare class API { | HandlerFunction )[] ): void; - post( - ...middlewaresAndHandler: ( - | Middleware - | HandlerFunction - )[] - ): void; put< TResponse = any, @@ -399,12 +311,6 @@ export declare class API { | HandlerFunction )[] ): void; - put( - ...middlewaresAndHandler: ( - | Middleware - | HandlerFunction - )[] - ): void; patch< TResponse = any, @@ -425,12 +331,6 @@ export declare class API { | HandlerFunction )[] ): void; - patch( - ...middlewaresAndHandler: ( - | Middleware - | HandlerFunction - )[] - ): void; delete< TResponse = any, @@ -451,14 +351,8 @@ export declare class API { | HandlerFunction )[] ): void; - delete( - ...middlewaresAndHandler: ( - | Middleware - | HandlerFunction - )[] - ): void; - options< + head< TResponse = any, TContext extends RequestContext = APIGatewayContext, TQuery extends Record = Record< @@ -477,14 +371,8 @@ export declare class API { | HandlerFunction )[] ): void; - options( - ...middlewaresAndHandler: ( - | Middleware - | HandlerFunction - )[] - ): void; - head< + options< TResponse = any, TContext extends RequestContext = APIGatewayContext, TQuery extends Record = Record< @@ -503,12 +391,6 @@ export declare class API { | HandlerFunction )[] ): void; - head( - ...middlewaresAndHandler: ( - | Middleware - | HandlerFunction - )[] - ): void; any< TResponse = any, @@ -529,12 +411,6 @@ export declare class API { | HandlerFunction )[] ): void; - any( - ...middlewaresAndHandler: ( - | Middleware - | HandlerFunction - )[] - ): void; METHOD< TResponse = any, @@ -556,13 +432,6 @@ export declare class API { | HandlerFunction )[] ): void; - METHOD( - method: METHODS | METHODS[], - ...middlewaresAndHandler: ( - | Middleware - | HandlerFunction - )[] - ): void; register( routes: (api: API, options?: RegisterOptions) => void, @@ -586,9 +455,13 @@ export declare class API { >, TBody = any >( - path: string, - ...middleware: Middleware[] + path: string | string[], + ...middleware: ( + | Middleware + | ErrorHandlingMiddleware + )[] ): void; + use< TResponse = any, TContext extends RequestContext = APIGatewayContext, @@ -602,13 +475,9 @@ export declare class API { >, TBody = any >( - paths: string[], - ...middleware: Middleware[] - ): void; - use( ...middleware: ( - | Middleware - | ErrorHandlingMiddleware + | Middleware + | ErrorHandlingMiddleware )[] ): void; @@ -647,3 +516,15 @@ export declare class FileError extends Error { declare function createAPI(options?: Options): API; export default createAPI; + +export declare function isApiGatewayContext( + context: RequestContext +): context is APIGatewayContext; + +export declare function isApiGatewayV2Context( + context: RequestContext +): context is APIGatewayV2Context; + +export declare function isAlbContext( + context: RequestContext +): context is ALBContext; diff --git a/index.js b/index.js index 86e3439..399912d 100644 --- a/index.js +++ b/index.js @@ -12,6 +12,7 @@ const LOGGER = require('./lib/logger'); const S3 = () => require('./lib/s3-service'); const prettyPrint = require('./lib/prettyPrint'); const { ConfigurationError } = require('./lib/errors'); +const { isApiGatewayContext, isApiGatewayV2Context, isAlbContext } = require('./lib/typeguards'); class API { constructor(props) { @@ -42,8 +43,8 @@ class API { : {}; this._compression = props && - (typeof props.compression === 'boolean' || - Array.isArray(props.compression)) + (typeof props.compression === 'boolean' || + Array.isArray(props.compression)) ? props.compression : false; @@ -84,7 +85,7 @@ class API { this._app = {}; // Executed after the callback - this._finally = () => {}; + this._finally = () => { }; // Global error status (used for response parsing errors) this._errorStatus = 500; @@ -213,8 +214,8 @@ class API { stack: _stack['m'][method] ? _stack['m'][method].concat(stack) : _stack['*'][method] - ? _stack['*'][method].concat(stack) - : stack, + ? _stack['*'][method].concat(stack) + : stack, // inherited: _stack[method] ? _stack[method] : [], route: '/' + parsedPath.join('/'), path: '/' + this._prefix.concat(parsedPath).join('/'), @@ -450,8 +451,8 @@ class API { typeof args[0] === 'string' ? Array.of(args.shift()) : Array.isArray(args[0]) - ? args.shift() - : ['/*']; + ? args.shift() + : ['/*']; // Init middleware stack let middleware = []; @@ -552,6 +553,10 @@ class API { } // end API class // Export the API class as a new instance -module.exports = (opts) => new API(opts); +module.exports = (options) => new API(options); // Add createAPI as default export (to match index.d.ts) module.exports.default = module.exports; + +module.exports.isApiGatewayContext = isApiGatewayContext; +module.exports.isApiGatewayV2Context = isApiGatewayV2Context; +module.exports.isAlbContext = isAlbContext; diff --git a/index.test-d.ts b/index.test-d.ts index 78b8fab..9a71f31 100644 --- a/index.test-d.ts +++ b/index.test-d.ts @@ -12,222 +12,193 @@ import { ErrorHandlingMiddleware, HandlerFunction, Middleware, + RequestExtensions, + NextFunction, + RequestContext, + isApiGatewayRequest, + isApiGatewayV2Request, + isAlbRequest, } from './index'; import { APIGatewayProxyEvent, Context } from 'aws-lambda'; +// Response type for user endpoints interface UserResponse { id: string; name: string; email: string; } +// Query parameters for user endpoints interface UserQuery extends Record { - fields: string; + fields?: string; } +// URL parameters for user endpoints interface UserParams extends Record { - id: string; + id?: string; } +// Request body for user endpoints interface UserBody { name: string; email: string; } -const api = new API(); - -const defaultReq = {} as Request; -expectType(defaultReq.requestContext); -expectType>(defaultReq.query); -expectType>(defaultReq.params); -expectType(defaultReq.body); - -const albReq = {} as Request; -expectType(albReq.requestContext); -expectType(albReq.query); -expectType(albReq.params); -expectType(albReq.body); +// Auth info type for request extensions +interface AuthInfo { + userId: string; + roles: string[]; + type: 'Bearer' | 'Basic' | 'OAuth' | 'Digest' | 'none'; + value: string | null; +} -const apiGwV2Req = {} as Request; -expectType(apiGwV2Req.requestContext); +// Type guard for auth info +function hasAuth(req: Request): req is Request & { auth: AuthInfo } { + return 'auth' in req && req.auth?.type !== undefined; +} -const stringResponse = {} as Response; -expectType(stringResponse.json('test')); -expectType(stringResponse.send('test')); +const api = new API(); -const userResponse = {} as Response; -expectType( - userResponse.json({ - id: '1', - name: 'John', - email: 'test@test.com', - }) -); - -const errorHandler: ErrorHandlingMiddleware = ( - error, - req, - res, - next -) => { - expectType(error); - expectType(req); - expectType>(res); +// Test source-agnostic middleware +const sourceAgnosticMiddleware: Middleware = (req, res, next) => { + // Common properties available across all sources + expectType(req.requestContext.requestId); + if (isApiGatewayRequest(req.requestContext)) { + const sourceIp = req.requestContext.identity.sourceIp; + if (sourceIp) { + expectType(sourceIp); + } + } else if (isApiGatewayV2Request(req.requestContext)) { + const sourceIp = req.requestContext.http.sourceIp; + if (sourceIp) { + expectType(sourceIp); + } + } else if (isAlbRequest(req.requestContext)) { + const sourceIp = req.requestContext.sourceIp; + if (sourceIp) { + expectType(sourceIp); + } + } next(); }; -const routeError = new RouteError('Not found', '/users'); -expectType(routeError); - -const methodError = new MethodError('Method not allowed', 'POST', '/users'); -expectType(methodError); - -const authMiddleware: Middleware = (req, res, next) => { - expectType>(res); +// Test source-specific middleware for ALB +const albMiddleware: Middleware< + UserResponse, + ALBContext & { sourceType: 'alb' }, + UserQuery, + UserParams, + UserBody +> = (req, res, next) => { + expectType<{ targetGroupArn: string }>(req.requestContext.elb); next(); }; -const validationMiddleware: Middleware = (req, res, next) => { - expectType>(res); +// Test source-specific middleware for API Gateway v2 +const apiGwV2Middleware: Middleware< + UserResponse, + APIGatewayV2Context & { sourceType: 'apigatewayv2' }, + UserQuery, + UserParams, + UserBody +> = (req, res, next) => { + expectType(req.requestContext.accountId); next(); }; -const albAuthMiddleware: Middleware< +// Test ALB-specific handler +const albHandler: HandlerFunction< UserResponse, - ALBContext, + ALBContext & { sourceType: 'alb' }, UserQuery, UserParams, UserBody -> = (req, res, next) => { - expectType(req.requestContext); - expectType>(res); - next(); +> = (req, res) => { + expectType<{ targetGroupArn: string }>(req.requestContext.elb); + res.json({ + id: '1', + name: req.body.name, + email: req.body.email, + }); }; -const handler: HandlerFunction = (req, res) => { - res.json({ id: '1', name: 'John', email: 'test@test.com' }); +// Test API Gateway v2 handler +const apiGwV2Handler: HandlerFunction< + UserResponse, + APIGatewayV2Context & { sourceType: 'apigatewayv2' }, + UserQuery, + UserParams, + UserBody +> = (req, res) => { + expectType(req.requestContext.accountId); + res.json({ + id: '1', + name: req.body.name, + email: req.body.email, + }); }; -api.get( - '/users', - authMiddleware, - validationMiddleware, - (req, res) => { - expectType>(req); - res.json({ id: '1', name: 'John', email: 'test@test.com' }); - } -); - -api.post( - '/users', - authMiddleware, - validationMiddleware, - (req, res) => { - expectType>(req); - res.json({ id: '1', name: 'John', email: 'test@test.com' }); - } -); - -api.put( - '/users/:id', - authMiddleware, - validationMiddleware, - (req, res) => { - expectType>(req); - res.json({ id: '1', name: 'John', email: 'test@test.com' }); - } -); - -api.patch( - '/users/:id', - authMiddleware, - validationMiddleware, - (req, res) => { - expectType>(req); - res.json({ id: '1', name: 'John', email: 'test@test.com' }); - } -); - -api.delete( - '/users/:id', - authMiddleware, - validationMiddleware, - (req, res) => { - expectType>(req); - res.json({ id: '1', name: 'John', email: 'test@test.com' }); - } -); - -api.options( - '/users', - authMiddleware, - validationMiddleware, - (req, res) => { - expectType>(req); - res.json({ id: '1', name: 'John', email: 'test@test.com' }); - } -); - -api.head( - '/users', - authMiddleware, - validationMiddleware, - (req, res) => { - expectType>(req); - res.json({ id: '1', name: 'John', email: 'test@test.com' }); - } -); - -api.any( - '/users', - authMiddleware, - validationMiddleware, - (req, res) => { - expectType>(req); - res.json({ id: '1', name: 'John', email: 'test@test.com' }); - } -); - -api.post( - '/users', - albAuthMiddleware, - (req, res) => { - expectType(req.requestContext); - expectType(req.query); - expectType(req.params); - expectType(req.body); - res.json({ id: '1', name: req.body.name, email: req.body.email }); +// Test routes with multiple source support +api.post('/users', sourceAgnosticMiddleware, (req, res) => { + res.json({ + id: '1', + name: 'John', + email: 'john@example.com', + }); +}); + +// Test ALB-specific route +api.post< + UserResponse, + ALBContext & { sourceType: 'alb' }, + UserQuery, + UserParams, + UserBody +>('/alb-users', albMiddleware, albHandler); + +// Test API Gateway v2 specific route +api.post< + UserResponse, + APIGatewayV2Context & { sourceType: 'apigatewayv2' }, + UserQuery, + UserParams, + UserBody +>('/v2-users', apiGwV2Middleware, apiGwV2Handler); + +// Test error handling for multiple sources +const errorHandler: ErrorHandlingMiddleware = (error, req, res, next) => { + if (isAlbRequest(req.requestContext)) { + // ALB-specific error handling + res.status(500).json({ + id: 'alb-error', + name: error.name, + email: error.message, + }); + } else { + // Default error handling + res.status(500).json({ + id: 'error', + name: error.name, + email: error.message, + }); } -); - -api.METHOD( - ['GET', 'POST'], - '/users', - authMiddleware, - validationMiddleware, - handler -); - -// Test middleware without path -api.use(authMiddleware); - -// Test middleware with path and ALB context -const albRouteMiddleware: Middleware = ( - req, - res, - next -) => { - expectType(req.requestContext); - expectType>(res); - next(); }; -api.use('/users', albRouteMiddleware); +// Register error handler +api.use(errorHandler); + +// Test finally handler with multiple sources api.finally((req, res) => { - expectType(req); - expectType(res); + if (isApiGatewayRequest(req.requestContext)) { + console.log('API Gateway request completed'); + } else if (isApiGatewayV2Request(req.requestContext)) { + console.log('API Gateway v2 request completed'); + } else if (isAlbRequest(req.requestContext)) { + console.log('ALB request completed'); + } }); +// Test run method const result = api.run({} as APIGatewayProxyEvent, {} as Context); expectType>(result); diff --git a/lib/typeguards.js b/lib/typeguards.js new file mode 100644 index 0000000..f807aa6 --- /dev/null +++ b/lib/typeguards.js @@ -0,0 +1,19 @@ +'use strict'; + +const isApiGatewayContext = (context) => { + return 'identity' in context; +}; + +const isApiGatewayV2Context = (context) => { + return 'http' in context; +}; + +const isAlbContext = (context) => { + return 'elb' in context; +}; + +module.exports = { + isApiGatewayContext, + isApiGatewayV2Context, + isAlbContext +}; \ No newline at end of file From 56e4ce4837552f869d2a474a4a1b331f83afe90b Mon Sep 17 00:00:00 2001 From: Naor Peled Date: Sat, 8 Feb 2025 15:31:07 +0200 Subject: [PATCH 07/18] . --- index.d.ts | 52 +++++++ index.test-d.ts | 379 ++++++++++++++++++++++++++++++++++++++++-------- 2 files changed, 370 insertions(+), 61 deletions(-) diff --git a/index.d.ts b/index.d.ts index 908538b..0a8fb3d 100644 --- a/index.d.ts +++ b/index.d.ts @@ -528,3 +528,55 @@ export declare function isApiGatewayV2Context( export declare function isAlbContext( context: RequestContext ): context is ALBContext; + +export declare function isApiGatewayEvent( + event: Event +): event is APIGatewayProxyEvent; + +export declare function isApiGatewayV2Event( + event: Event +): event is APIGatewayProxyEventV2; + +export declare function isAlbEvent(event: Event): event is ALBEvent; + +export declare function isApiGatewayRequest< + TQuery extends Record = Record< + string, + string | undefined + >, + TParams extends Record = Record< + string, + string | undefined + >, + TBody = any +>( + req: Request +): req is Request; + +export declare function isApiGatewayV2Request< + TQuery extends Record = Record< + string, + string | undefined + >, + TParams extends Record = Record< + string, + string | undefined + >, + TBody = any +>( + req: Request +): req is Request; + +export declare function isAlbRequest< + TQuery extends Record = Record< + string, + string | undefined + >, + TParams extends Record = Record< + string, + string | undefined + >, + TBody = any +>( + req: Request +): req is Request; diff --git a/index.test-d.ts b/index.test-d.ts index 9a71f31..8dc4591 100644 --- a/index.test-d.ts +++ b/index.test-d.ts @@ -1,4 +1,5 @@ import { expectType } from 'tsd'; + import { API, Request, @@ -12,39 +13,44 @@ import { ErrorHandlingMiddleware, HandlerFunction, Middleware, - RequestExtensions, NextFunction, RequestContext, + isApiGatewayContext, + isApiGatewayV2Context, + isAlbContext, isApiGatewayRequest, isApiGatewayV2Request, isAlbRequest, + isApiGatewayEvent, + isApiGatewayV2Event, + isAlbEvent, } from './index'; -import { APIGatewayProxyEvent, Context } from 'aws-lambda'; +import { + APIGatewayProxyEvent, + APIGatewayProxyEventV2, + ALBEvent, + Context, +} from 'aws-lambda'; -// Response type for user endpoints interface UserResponse { id: string; name: string; email: string; } -// Query parameters for user endpoints interface UserQuery extends Record { fields?: string; } -// URL parameters for user endpoints interface UserParams extends Record { id?: string; } -// Request body for user endpoints interface UserBody { name: string; email: string; } -// Auth info type for request extensions interface AuthInfo { userId: string; roles: string[]; @@ -52,40 +58,75 @@ interface AuthInfo { value: string | null; } -// Type guard for auth info function hasAuth(req: Request): req is Request & { auth: AuthInfo } { return 'auth' in req && req.auth?.type !== undefined; } const api = new API(); -// Test source-agnostic middleware -const sourceAgnosticMiddleware: Middleware = (req, res, next) => { - // Common properties available across all sources - expectType(req.requestContext.requestId); - if (isApiGatewayRequest(req.requestContext)) { +const testContextTypeGuards = () => { + const apiGatewayContext: APIGatewayContext = {} as APIGatewayContext; + const apiGatewayV2Context: APIGatewayV2Context = {} as APIGatewayV2Context; + const albContext: ALBContext = {} as ALBContext; + + if (isApiGatewayContext(apiGatewayContext)) { + expectType(apiGatewayContext); + } + + if (isApiGatewayV2Context(apiGatewayV2Context)) { + expectType(apiGatewayV2Context); + } + + if (isAlbContext(albContext)) { + expectType(albContext); + } +}; + +const testEventTypeGuards = () => { + const apiGatewayEvent: APIGatewayProxyEvent = {} as APIGatewayProxyEvent; + const apiGatewayV2Event: APIGatewayProxyEventV2 = + {} as APIGatewayProxyEventV2; + const albEvent: ALBEvent = {} as ALBEvent; + + if (isApiGatewayEvent(apiGatewayEvent)) { + expectType(apiGatewayEvent); + } + + if (isApiGatewayV2Event(apiGatewayV2Event)) { + expectType(apiGatewayV2Event); + } + + if (isAlbEvent(albEvent)) { + expectType(albEvent); + } +}; + +const sourceAgnosticMiddleware: Middleware = ( + req, + res, + next +) => { + if (isApiGatewayContext(req.requestContext)) { + expectType(req.requestContext.requestId); const sourceIp = req.requestContext.identity.sourceIp; if (sourceIp) { expectType(sourceIp); } - } else if (isApiGatewayV2Request(req.requestContext)) { + } else if (isApiGatewayV2Context(req.requestContext)) { + expectType(req.requestContext.requestId); const sourceIp = req.requestContext.http.sourceIp; if (sourceIp) { expectType(sourceIp); } - } else if (isAlbRequest(req.requestContext)) { - const sourceIp = req.requestContext.sourceIp; - if (sourceIp) { - expectType(sourceIp); - } + } else if (isAlbContext(req.requestContext)) { + expectType<{ targetGroupArn: string }>(req.requestContext.elb); } next(); }; -// Test source-specific middleware for ALB const albMiddleware: Middleware< UserResponse, - ALBContext & { sourceType: 'alb' }, + ALBContext, UserQuery, UserParams, UserBody @@ -94,10 +135,9 @@ const albMiddleware: Middleware< next(); }; -// Test source-specific middleware for API Gateway v2 const apiGwV2Middleware: Middleware< UserResponse, - APIGatewayV2Context & { sourceType: 'apigatewayv2' }, + APIGatewayV2Context, UserQuery, UserParams, UserBody @@ -106,10 +146,9 @@ const apiGwV2Middleware: Middleware< next(); }; -// Test ALB-specific handler const albHandler: HandlerFunction< UserResponse, - ALBContext & { sourceType: 'alb' }, + ALBContext, UserQuery, UserParams, UserBody @@ -122,10 +161,9 @@ const albHandler: HandlerFunction< }); }; -// Test API Gateway v2 handler const apiGwV2Handler: HandlerFunction< UserResponse, - APIGatewayV2Context & { sourceType: 'apigatewayv2' }, + APIGatewayV2Context, UserQuery, UserParams, UserBody @@ -138,44 +176,97 @@ const apiGwV2Handler: HandlerFunction< }); }; -// Test routes with multiple source support -api.post('/users', sourceAgnosticMiddleware, (req, res) => { - res.json({ - id: '1', - name: 'John', - email: 'john@example.com', - }); -}); +const testRequestTypeGuards = () => { + const req = {} as Request; -// Test ALB-specific route -api.post< - UserResponse, - ALBContext & { sourceType: 'alb' }, - UserQuery, - UserParams, - UserBody ->('/alb-users', albMiddleware, albHandler); + if (isApiGatewayRequest(req)) { + expectType>(req); + } -// Test API Gateway v2 specific route -api.post< - UserResponse, - APIGatewayV2Context & { sourceType: 'apigatewayv2' }, - UserQuery, - UserParams, - UserBody ->('/v2-users', apiGwV2Middleware, apiGwV2Handler); + if (isApiGatewayV2Request(req)) { + expectType>(req); + } + + if (isAlbRequest(req)) { + expectType>(req); + } +}; + +const sourceAgnosticHandler: HandlerFunction = ( + req, + res +) => { + expectType(req.method); + expectType(req.path); + expectType>(req.query); + expectType>(req.headers); + expectType(req.ip); + + if (isApiGatewayContext(req.requestContext)) { + expectType(req.requestContext.requestId); + expectType(req.requestContext.identity.sourceIp); + res.json({ + id: req.requestContext.requestId, + name: 'API Gateway User', + email: 'user@apigateway.com', + }); + } else if (isApiGatewayV2Context(req.requestContext)) { + expectType(req.requestContext.requestId); + expectType(req.requestContext.http.sourceIp); + res.json({ + id: req.requestContext.requestId, + name: 'API Gateway V2 User', + email: 'user@apigatewayv2.com', + }); + } else if (isAlbContext(req.requestContext)) { + expectType<{ targetGroupArn: string }>(req.requestContext.elb); + res.json({ + id: req.requestContext.elb.targetGroupArn, + name: 'ALB User', + email: 'user@alb.com', + }); + } +}; + +api.get('/source-agnostic', sourceAgnosticHandler); +api.post( + '/source-agnostic', + sourceAgnosticMiddleware, + sourceAgnosticHandler +); + +api.post( + '/users', + sourceAgnosticMiddleware, + (req: Request, res: Response) => { + res.json({ + id: '1', + name: 'John', + email: 'john@example.com', + }); + } +); + +api.post( + '/alb-users', + albMiddleware, + albHandler +); + +api.post( + '/v2-users', + apiGwV2Middleware, + apiGwV2Handler +); -// Test error handling for multiple sources const errorHandler: ErrorHandlingMiddleware = (error, req, res, next) => { - if (isAlbRequest(req.requestContext)) { - // ALB-specific error handling + if (isAlbContext(req.requestContext)) { res.status(500).json({ id: 'alb-error', name: error.name, email: error.message, }); } else { - // Default error handling res.status(500).json({ id: 'error', name: error.name, @@ -184,21 +275,18 @@ const errorHandler: ErrorHandlingMiddleware = (error, req, res, next) => { } }; -// Register error handler api.use(errorHandler); -// Test finally handler with multiple sources api.finally((req, res) => { - if (isApiGatewayRequest(req.requestContext)) { + if (isApiGatewayContext(req.requestContext)) { console.log('API Gateway request completed'); - } else if (isApiGatewayV2Request(req.requestContext)) { + } else if (isApiGatewayV2Context(req.requestContext)) { console.log('API Gateway v2 request completed'); - } else if (isAlbRequest(req.requestContext)) { + } else if (isAlbContext(req.requestContext)) { console.log('ALB request completed'); } }); -// Test run method const result = api.run({} as APIGatewayProxyEvent, {} as Context); expectType>(result); @@ -206,3 +294,172 @@ api.run({} as APIGatewayProxyEvent, {} as Context, (err, res) => { expectType(err); expectType(res); }); + +const testHttpMethods = () => { + api.get( + '/users/:id', + ( + req: Request, + res: Response + ) => { + expectType(req.params.id); + res.json({ id: '1', name: 'John', email: 'test@example.com' }); + } + ); + + api.put( + '/users/:id', + ( + req: Request, + res: Response + ) => { + expectType(req.body); + res + .status(200) + .json({ id: '1', name: req.body.name, email: req.body.email }); + } + ); + + api.patch< + UserResponse, + APIGatewayContext, + UserQuery, + UserParams, + Partial + >( + '/users/:id', + ( + req: Request>, + res: Response + ) => { + expectType>(req.body); + res.json({ id: '1', name: 'John', email: 'test@example.com' }); + } + ); + + api.delete( + '/users/:id', + (req: Request, res: Response) => { + res.status(204).send(); + } + ); + + api.head( + '/users', + (req: Request, res: Response) => { + res.status(200).send(); + } + ); + + api.options( + '/users', + (req: Request, res: Response) => { + res.header('Allow', 'GET, POST, PUT, DELETE').status(204).send(); + } + ); + + api.any<{ method: string }, APIGatewayContext>( + '/wildcard', + (req: Request, res: Response<{ method: string }>) => { + expectType(req.method); + res.send({ method: req.method }); + } + ); +}; + +const pathSpecificMiddleware: Middleware = ( + req, + res, + next +) => { + req.log.info('Path-specific middleware'); + next(); +}; + +api.use('/specific-path', pathSpecificMiddleware); +api.use(['/path1', '/path2'], pathSpecificMiddleware); + +interface RequestWithCustom1 extends Request { + custom1: string; +} + +interface RequestWithCustom2 extends Request { + custom2: string; +} + +const middleware1: Middleware = (req, res, next) => { + (req as RequestWithCustom1).custom1 = 'value1'; + next(); +}; + +const middleware2: Middleware = (req, res, next) => { + (req as RequestWithCustom2).custom2 = 'value2'; + next(); +}; + +api.use(middleware1, middleware2); + +const handlerWithCustomProps: HandlerFunction = ( + req, + res +) => { + if ('custom1' in req) { + expectType((req as RequestWithCustom1).custom1); + } + if ('custom2' in req) { + expectType((req as RequestWithCustom2).custom2); + } + res.send({ status: 'ok' }); +}; + +const testResponseMethods: HandlerFunction = ( + req, + res +) => { + res.status(201); + res.header('X-Custom', 'value'); + res.getHeader('X-Custom'); + res.hasHeader('X-Custom'); + res.removeHeader('X-Custom'); + res.send({ data: 'raw' }); + res.json({ data: 'json' }); + res.html('
html
'); + res.redirect('/new-location'); + res.redirect(301, '/permanent-location'); + res.type('json'); + res.cookie('session', 'value', { httpOnly: true }); + res.clearCookie('session'); + res.cache(3600); + res.cache(true, true); + res.cors({ + origin: '*', + methods: 'GET, POST', + headers: 'Content-Type', + }); + res.error(400, 'Bad Request'); +}; + +const testRequestProperties: HandlerFunction = ( + req, + res +) => { + expectType(req.id); + expectType(req.method); + expectType(req.path); + expectType>(req.query); + expectType>(req.headers); + expectType(req.ip); + expectType(req.userAgent); + expectType<'desktop' | 'mobile' | 'tv' | 'tablet' | 'unknown'>( + req.clientType + ); + expectType(req.clientCountry); + expectType(req.coldStart); + expectType(req.requestCount); + req.log.trace('trace message'); + req.log.debug('debug message'); + req.log.info('info message'); + req.log.warn('warn message'); + req.log.error('error message'); + req.log.fatal('fatal message'); +}; From 216ad8484505b28c08b44cf9f7ed4cd89599c5ed Mon Sep 17 00:00:00 2001 From: Naor Peled Date: Sat, 8 Feb 2025 15:34:24 +0200 Subject: [PATCH 08/18] . --- index.d.ts | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/index.d.ts b/index.d.ts index 0a8fb3d..d886944 100644 --- a/index.d.ts +++ b/index.d.ts @@ -127,12 +127,25 @@ export type HandlerFunction< ) => void; export interface Options { + base?: string; + callbackName?: string; logger?: { level?: string; format?: string; [key: string]: any; }; - [key: string]: any; + mimeTypes?: { + [key: string]: string; + }; + serializer?: (data: any) => string; + version?: string; + errorHeaderWhitelist?: string[]; + isBase64?: boolean; + compression?: boolean | string[]; + headers?: { + [key: string]: string; + }; + s3Config?: S3ClientConfig; } export declare class Request< From 7a94e930f71e3e6e066722507bf3579d207332dd Mon Sep 17 00:00:00 2001 From: Naor Peled Date: Sat, 8 Feb 2025 15:41:52 +0200 Subject: [PATCH 09/18] . --- index.d.ts | 43 +++++++++++++++++++++++++++++++++++++------ 1 file changed, 37 insertions(+), 6 deletions(-) diff --git a/index.d.ts b/index.d.ts index d886944..361608d 100644 --- a/index.d.ts +++ b/index.d.ts @@ -105,7 +105,8 @@ export type METHODS = | 'PATCH' | 'DELETE' | 'HEAD' - | 'OPTIONS'; + | 'OPTIONS' + | 'ANY'; export type LoggerFunction = (message: string, ...args: any[]) => void; @@ -126,14 +127,44 @@ export type HandlerFunction< res: Response ) => void; +export type TimestampFunction = () => string; + +export declare interface SamplingOptions { + route?: string; + target?: number; + rate?: number; + period?: number; + method?: string | string[]; +} + +export declare interface LoggerOptions { + access?: boolean | string; + customKey?: string; + errorLogging?: boolean; + detail?: boolean; + level?: string; + levels?: { + [key: string]: string; + }; + messageKey?: string; + nested?: boolean; + timestamp?: boolean | TimestampFunction; + sampling?: { + target?: number; + rate?: number; + period?: number; + rules?: SamplingOptions[]; + }; + serializers?: { + [name: string]: (prop: any) => any; + }; + stack?: boolean; +} + export interface Options { base?: string; callbackName?: string; - logger?: { - level?: string; - format?: string; - [key: string]: any; - }; + logger?: boolean | LoggerOptions; mimeTypes?: { [key: string]: string; }; From 3654a2fb0879cddaac45573b28e50df8a649dc2d Mon Sep 17 00:00:00 2001 From: Naor Peled Date: Sat, 8 Feb 2025 15:49:48 +0200 Subject: [PATCH 10/18] . --- index.d.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/index.d.ts b/index.d.ts index 361608d..3e6b0da 100644 --- a/index.d.ts +++ b/index.d.ts @@ -77,7 +77,7 @@ export declare type Middleware< next: NextFunction ) => void; -export declare type NextFunction = (error?: Error) => void; +export declare type NextFunction = () => void; export declare type ErrorHandlingMiddleware< TResponse = any, @@ -168,7 +168,7 @@ export interface Options { mimeTypes?: { [key: string]: string; }; - serializer?: (data: any) => string; + serializer?: SerializerFunction; version?: string; errorHeaderWhitelist?: string[]; isBase64?: boolean; @@ -179,6 +179,8 @@ export interface Options { s3Config?: S3ClientConfig; } +export declare type SerializerFunction = (body: object) => string; + export declare class Request< TContext extends RequestContext = APIGatewayContext, TQuery extends Record = Record< From 3019064e26de42d85988fa8e535ecf59f501a975 Mon Sep 17 00:00:00 2001 From: Naor Peled Date: Sat, 8 Feb 2025 15:59:22 +0200 Subject: [PATCH 11/18] . --- index.d.ts | 53 +++++++++++++++++++++++++++++++++++++++---------- index.test-d.ts | 34 +++++++++++++++++++++++++++++++ 2 files changed, 76 insertions(+), 11 deletions(-) diff --git a/index.d.ts b/index.d.ts index 3e6b0da..a47f2be 100644 --- a/index.d.ts +++ b/index.d.ts @@ -92,11 +92,25 @@ export declare type ErrorHandlingMiddleware< >, TBody = any > = ( + /** + * The error that was thrown or passed to res.error() + */ error: Error, + /** + * The request object + */ req: Request, + /** + * The response object. Call res.send() or return a value to send a response. + * The error will continue through the error middleware chain until a response is sent. + */ res: Response, + /** + * Call next() to continue to the next error middleware. + * Note: next(error) is not supported - the error parameter will be ignored. + */ next: NextFunction -) => void; +) => void | Promise | TResponse; export type METHODS = | 'GET' @@ -142,23 +156,32 @@ export declare interface LoggerOptions { customKey?: string; errorLogging?: boolean; detail?: boolean; - level?: string; + level?: 'trace' | 'debug' | 'info' | 'warn' | 'error' | 'fatal' | 'none'; levels?: { - [key: string]: string; + [key: string]: number; }; messageKey?: string; nested?: boolean; timestamp?: boolean | TimestampFunction; - sampling?: { - target?: number; - rate?: number; - period?: number; - rules?: SamplingOptions[]; - }; + sampling?: + | boolean + | { + target?: number; + rate?: number; + period?: number; + rules?: SamplingOptions[]; + }; serializers?: { - [name: string]: (prop: any) => any; + main?: (req: Request) => object; + req?: (req: Request) => object; + res?: (res: Response) => object; + context?: (context: Context) => object; + custom?: (custom: any) => object; }; stack?: boolean; + timer?: boolean; + multiValue?: boolean; + log?: (message: string) => void; } export interface Options { @@ -214,7 +237,7 @@ export declare class Request< }; body: TBody; rawBody: string; - route: ''; + route: string; requestContext: TContext; isBase64Encoded: boolean; pathParameters: { [name: string]: string } | null; @@ -235,6 +258,12 @@ export declare class Request< clientType: 'desktop' | 'mobile' | 'tv' | 'tablet' | 'unknown'; clientCountry: string; namespace: App; + /** + * Alias for namespace + */ + ns: App; + interface: 'apigateway' | 'alb'; + payloadVersion?: string; log: { trace: LoggerFunction; @@ -249,6 +278,8 @@ export declare class Request< } export declare class Response { + app: API; + status(code: number): this; sendStatus(code: number): void; header(key: string, value?: string | Array, append?: boolean): this; diff --git a/index.test-d.ts b/index.test-d.ts index 8dc4591..44166ac 100644 --- a/index.test-d.ts +++ b/index.test-d.ts @@ -24,6 +24,8 @@ import { isApiGatewayEvent, isApiGatewayV2Event, isAlbEvent, + App, + SerializerFunction, } from './index'; import { APIGatewayProxyEvent, @@ -456,6 +458,9 @@ const testRequestProperties: HandlerFunction = ( expectType(req.clientCountry); expectType(req.coldStart); expectType(req.requestCount); + expectType<'apigateway' | 'alb'>(req.interface); + expectType(req.payloadVersion); + expectType(req.ns); req.log.trace('trace message'); req.log.debug('debug message'); req.log.info('info message'); @@ -463,3 +468,32 @@ const testRequestProperties: HandlerFunction = ( req.log.error('error message'); req.log.fatal('fatal message'); }; + +const testErrorHandlingMiddleware: ErrorHandlingMiddleware = async ( + error, + req, + res, + next +) => { + // Test that we can return different types + if (error.message === 'sync') { + return { message: 'handled synchronously' }; + } + + if (error.message === 'async') { + return Promise.resolve({ message: 'handled asynchronously' }); + } + + if (error.message === 'void') { + res.json({ message: 'handled with void' }); + return; + } + + if (error.message === 'promise-void') { + await Promise.resolve(); + res.json({ message: 'handled with promise void' }); + return; + } + + next(); +}; From fa38eb59c5abd5a4cdb20558598c6bf3eeca4972 Mon Sep 17 00:00:00 2001 From: Naor Peled Date: Sat, 8 Feb 2025 16:01:35 +0200 Subject: [PATCH 12/18] . --- index.d.ts | 4 --- index.test-d.ts | 66 ++++++++++++++++++++++++++++--------------------- 2 files changed, 38 insertions(+), 32 deletions(-) diff --git a/index.d.ts b/index.d.ts index a47f2be..7cc9a78 100644 --- a/index.d.ts +++ b/index.d.ts @@ -278,8 +278,6 @@ export declare class Request< } export declare class Response { - app: API; - status(code: number): this; sendStatus(code: number): void; header(key: string, value?: string | Array, append?: boolean): this; @@ -321,8 +319,6 @@ export declare class Response { options?: FileOptions, callback?: ErrorCallback ): Promise; - - [key: string]: any; } export declare class API { diff --git a/index.test-d.ts b/index.test-d.ts index 44166ac..7f0ed8a 100644 --- a/index.test-d.ts +++ b/index.test-d.ts @@ -414,33 +414,6 @@ const handlerWithCustomProps: HandlerFunction = ( res.send({ status: 'ok' }); }; -const testResponseMethods: HandlerFunction = ( - req, - res -) => { - res.status(201); - res.header('X-Custom', 'value'); - res.getHeader('X-Custom'); - res.hasHeader('X-Custom'); - res.removeHeader('X-Custom'); - res.send({ data: 'raw' }); - res.json({ data: 'json' }); - res.html('
html
'); - res.redirect('/new-location'); - res.redirect(301, '/permanent-location'); - res.type('json'); - res.cookie('session', 'value', { httpOnly: true }); - res.clearCookie('session'); - res.cache(3600); - res.cache(true, true); - res.cors({ - origin: '*', - methods: 'GET, POST', - headers: 'Content-Type', - }); - res.error(400, 'Bad Request'); -}; - const testRequestProperties: HandlerFunction = ( req, res @@ -469,13 +442,50 @@ const testRequestProperties: HandlerFunction = ( req.log.fatal('fatal message'); }; +const testResponseMethods: HandlerFunction = ( + req, + res +) => { + res + .status(201) + .header('X-Custom', 'value') + .type('json') + .cors({ + origin: '*', + methods: 'GET, POST', + headers: 'Content-Type', + }) + .cookie('session', 'value', { httpOnly: true }) + .cache(3600) + .etag(true) + .modified(new Date()); + + expectType(res.getHeader('X-Custom')); + expectType<{ [key: string]: string }>(res.getHeaders()); + expectType(res.hasHeader('X-Custom')); + res.removeHeader('X-Custom'); + + res.send({ data: 'raw' }); + res.json({ data: 'json' }); + res.jsonp({ data: 'jsonp' }); + res.html('
html
'); + res.sendStatus(204); + + res.redirect('/new-location'); + res.redirect(301, '/permanent-location'); + + res.clearCookie('session'); + + res.error(400, 'Bad Request'); + res.error('Error message'); +}; + const testErrorHandlingMiddleware: ErrorHandlingMiddleware = async ( error, req, res, next ) => { - // Test that we can return different types if (error.message === 'sync') { return { message: 'handled synchronously' }; } From 18d419f75109d2122be70ed04a14dbcc55e3e750 Mon Sep 17 00:00:00 2001 From: Naor Peled Date: Sat, 8 Feb 2025 16:08:28 +0200 Subject: [PATCH 13/18] . --- index.test-d.ts | 95 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 95 insertions(+) diff --git a/index.test-d.ts b/index.test-d.ts index 7f0ed8a..b53ed5b 100644 --- a/index.test-d.ts +++ b/index.test-d.ts @@ -507,3 +507,98 @@ const testErrorHandlingMiddleware: ErrorHandlingMiddleware = async ( next(); }; + +const testDefaultTypes = () => { + api.get('/simple', (req, res) => { + expectType(req); + expectType(res); + expectType(req.requestContext); + expectType>(req.query); + expectType>(req.params); + expectType(req.body); + res.json({ message: 'ok' }); + }); + + const simpleMiddleware: Middleware = (req, res, next) => { + expectType(req); + expectType(res); + expectType(req.requestContext); + next(); + }; + + const simpleErrorHandler: ErrorHandlingMiddleware = ( + error, + req, + res, + next + ) => { + expectType(req); + expectType(res); + expectType(req.requestContext); + res.status(500).json({ error: error.message }); + }; + + api.post('/simple-chain', simpleMiddleware, (req, res) => { + expectType(req); + expectType(res); + res.json({ status: 'ok' }); + }); + + api.use((req, res, next) => { + expectType(req); + expectType(res); + next(); + }); + + api.use('/path', (req, res, next) => { + expectType(req); + expectType(res); + next(); + }); + + api.finally((req, res) => { + expectType(req); + expectType(res); + }); + + const runResult = api.run({} as APIGatewayProxyEvent, {} as Context); + expectType>(runResult); + + api.run({} as APIGatewayProxyEvent, {} as Context, (err, res) => { + expectType(err); + expectType(res); + }); + + const albApi = new API(); + albApi.get('/alb-default', (req, res) => { + if (isAlbContext(req.requestContext)) { + expectType(req.requestContext); + expectType<{ targetGroupArn: string }>(req.requestContext.elb); + expectType>(req.query); + expectType>(req.params); + expectType(req.body); + res.json({ message: 'ALB response' }); + } + }); + + const albResult = albApi.run({} as ALBEvent, {} as Context); + expectType>(albResult); + + const apiGwV2Api = new API(); + apiGwV2Api.get('/apigw-v2-default', (req, res) => { + if (isApiGatewayV2Context(req.requestContext)) { + expectType(req.requestContext); + expectType(req.requestContext.accountId); + expectType>(req.query); + expectType>(req.params); + expectType(req.body); + res.json({ message: 'API Gateway V2 response' }); + } + }); + + const apiGwV2Result = apiGwV2Api.run( + {} as APIGatewayProxyEventV2, + {} as Context + ); + expectType>(apiGwV2Result); +}; From 08443a0dbf9ee3d2574270f31db4fb91f2840f55 Mon Sep 17 00:00:00 2001 From: Naor Peled Date: Sat, 8 Feb 2025 16:20:17 +0200 Subject: [PATCH 14/18] . --- index.test-d.ts | 90 +++++++++++++++++++++++++++++-------------------- 1 file changed, 53 insertions(+), 37 deletions(-) diff --git a/index.test-d.ts b/index.test-d.ts index b53ed5b..8a4739c 100644 --- a/index.test-d.ts +++ b/index.test-d.ts @@ -509,8 +509,8 @@ const testErrorHandlingMiddleware: ErrorHandlingMiddleware = async ( }; const testDefaultTypes = () => { - api.get('/simple', (req, res) => { - expectType(req); + api.get('/simple', (req: Request, res: Response) => { + expectType>(req); expectType(res); expectType(req.requestContext); expectType>(req.query); @@ -519,58 +519,71 @@ const testDefaultTypes = () => { res.json({ message: 'ok' }); }); - const simpleMiddleware: Middleware = (req, res, next) => { - expectType(req); + const simpleMiddleware: Middleware = ( + req: Request, + res: Response, + next: NextFunction + ) => { + expectType>(req); expectType(res); expectType(req.requestContext); next(); }; const simpleErrorHandler: ErrorHandlingMiddleware = ( - error, - req, - res, - next + error: Error, + req: Request, + res: Response, + next: NextFunction ) => { - expectType(req); + expectType>(req); expectType(res); expectType(req.requestContext); res.status(500).json({ error: error.message }); }; - api.post('/simple-chain', simpleMiddleware, (req, res) => { - expectType(req); - expectType(res); - res.json({ status: 'ok' }); - }); + api.post( + '/simple-chain', + simpleMiddleware, + (req: Request, res: Response) => { + expectType>(req); + expectType(res); + res.json({ status: 'ok' }); + } + ); - api.use((req, res, next) => { - expectType(req); - expectType(res); - next(); - }); + api.use( + (req: Request, res: Response, next: NextFunction) => { + expectType>(req); + expectType(res); + next(); + } + ); - api.use('/path', (req, res, next) => { - expectType(req); - expectType(res); - next(); - }); + api.use( + '/path', + (req: Request, res: Response, next: NextFunction) => { + expectType>(req); + expectType(res); + next(); + } + ); - api.finally((req, res) => { - expectType(req); + api.finally((req: Request, res: Response) => { + expectType>(req); expectType(res); }); const runResult = api.run({} as APIGatewayProxyEvent, {} as Context); expectType>(runResult); - api.run({} as APIGatewayProxyEvent, {} as Context, (err, res) => { + api.run({} as APIGatewayProxyEvent, {} as Context, (err: Error, res: any) => { expectType(err); expectType(res); }); const albApi = new API(); - albApi.get('/alb-default', (req, res) => { + albApi.get('/alb-default', (req: Request, res: Response) => { if (isAlbContext(req.requestContext)) { expectType(req.requestContext); expectType<{ targetGroupArn: string }>(req.requestContext.elb); @@ -585,16 +598,19 @@ const testDefaultTypes = () => { expectType>(albResult); const apiGwV2Api = new API(); - apiGwV2Api.get('/apigw-v2-default', (req, res) => { - if (isApiGatewayV2Context(req.requestContext)) { - expectType(req.requestContext); - expectType(req.requestContext.accountId); - expectType>(req.query); - expectType>(req.params); - expectType(req.body); - res.json({ message: 'API Gateway V2 response' }); + apiGwV2Api.get( + '/apigw-v2-default', + (req: Request, res: Response) => { + if (isApiGatewayV2Context(req.requestContext)) { + expectType(req.requestContext); + expectType(req.requestContext.accountId); + expectType>(req.query); + expectType>(req.params); + expectType(req.body); + res.json({ message: 'API Gateway V2 response' }); + } } - }); + ); const apiGwV2Result = apiGwV2Api.run( {} as APIGatewayProxyEventV2, From 7668fe8071b5b88b042b9075315678d9e421598b Mon Sep 17 00:00:00 2001 From: Naor Peled Date: Sat, 8 Feb 2025 16:20:47 +0200 Subject: [PATCH 15/18] . --- index.test-d.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/index.test-d.ts b/index.test-d.ts index 8a4739c..a7c8fe6 100644 --- a/index.test-d.ts +++ b/index.test-d.ts @@ -4,12 +4,9 @@ import { API, Request, Response, - RouteError, - MethodError, ALBContext, APIGatewayV2Context, APIGatewayContext, - METHODS, ErrorHandlingMiddleware, HandlerFunction, Middleware, @@ -25,7 +22,6 @@ import { isApiGatewayV2Event, isAlbEvent, App, - SerializerFunction, } from './index'; import { APIGatewayProxyEvent, From ca1aadf93e351fe66a950d06570de4be23c137ee Mon Sep 17 00:00:00 2001 From: Naor Peled Date: Sat, 8 Feb 2025 16:25:37 +0200 Subject: [PATCH 16/18] . --- index.d.ts | 63 +++++++++++++++++++++++++++++++++++++++++++++++++ index.test-d.ts | 61 ++++++++++++++++++++++++++++++++++++++++++----- 2 files changed, 118 insertions(+), 6 deletions(-) diff --git a/index.d.ts b/index.d.ts index 7cc9a78..7e31fe7 100644 --- a/index.d.ts +++ b/index.d.ts @@ -653,3 +653,66 @@ export declare function isAlbRequest< >( req: Request ): req is Request; + +/** + * Source-agnostic request type that works with any AWS Lambda trigger + */ +export type SourceAgnosticRequest< + TQuery extends Record = Record< + string, + string | undefined + >, + TParams extends Record = Record< + string, + string | undefined + >, + TBody = any +> = Request; + +/** + * Source-agnostic middleware type that works with any AWS Lambda trigger + */ +export type SourceAgnosticMiddleware< + TResponse = any, + TQuery extends Record = Record< + string, + string | undefined + >, + TParams extends Record = Record< + string, + string | undefined + >, + TBody = any +> = Middleware; + +/** + * Source-agnostic handler function type that works with any AWS Lambda trigger + */ +export type SourceAgnosticHandler< + TResponse = any, + TQuery extends Record = Record< + string, + string | undefined + >, + TParams extends Record = Record< + string, + string | undefined + >, + TBody = any +> = HandlerFunction; + +/** + * Source-agnostic error handling middleware type that works with any AWS Lambda trigger + */ +export type SourceAgnosticErrorHandler< + TResponse = any, + TQuery extends Record = Record< + string, + string | undefined + >, + TParams extends Record = Record< + string, + string | undefined + >, + TBody = any +> = ErrorHandlingMiddleware; diff --git a/index.test-d.ts b/index.test-d.ts index a7c8fe6..1515bbe 100644 --- a/index.test-d.ts +++ b/index.test-d.ts @@ -22,6 +22,9 @@ import { isApiGatewayV2Event, isAlbEvent, App, + SourceAgnosticMiddleware, + SourceAgnosticHandler, + SourceAgnosticErrorHandler, } from './index'; import { APIGatewayProxyEvent, @@ -99,11 +102,7 @@ const testEventTypeGuards = () => { } }; -const sourceAgnosticMiddleware: Middleware = ( - req, - res, - next -) => { +const sourceAgnosticMiddleware: SourceAgnosticMiddleware = (req, res, next) => { if (isApiGatewayContext(req.requestContext)) { expectType(req.requestContext.requestId); const sourceIp = req.requestContext.identity.sourceIp; @@ -190,7 +189,7 @@ const testRequestTypeGuards = () => { } }; -const sourceAgnosticHandler: HandlerFunction = ( +const sourceAgnosticHandler: SourceAgnosticHandler = ( req, res ) => { @@ -614,3 +613,53 @@ const testDefaultTypes = () => { ); expectType>(apiGwV2Result); }; + +const testSourceAgnosticTypes = () => { + // Test source-agnostic handler with minimal type parameters + const simpleSourceAgnosticHandler: SourceAgnosticHandler = (req, res) => { + expectType(req.requestContext); + res.json({ message: 'ok' }); + }; + + // Test source-agnostic handler with response type + const typedSourceAgnosticHandler: SourceAgnosticHandler = ( + req, + res + ) => { + res.json({ + id: '1', + name: 'John', + email: 'john@example.com', + }); + }; + + // Test source-agnostic middleware with minimal type parameters + const simpleSourceAgnosticMiddleware: SourceAgnosticMiddleware = ( + req, + res, + next + ) => { + expectType(req.requestContext); + next(); + }; + + // Test source-agnostic error handler + const sourceAgnosticErrorHandler: SourceAgnosticErrorHandler = ( + error, + req, + res, + next + ) => { + expectType(req.requestContext); + res.status(500).json({ error: error.message }); + }; + + // Test using source-agnostic types with API + api.get('/source-agnostic', simpleSourceAgnosticHandler); + api.post( + '/source-agnostic', + simpleSourceAgnosticMiddleware, + typedSourceAgnosticHandler + ); + api.use(sourceAgnosticErrorHandler); +}; From 0d8fd88e89d176fa61c938db5962cd7eab8b31fc Mon Sep 17 00:00:00 2001 From: Naor Peled Date: Sat, 8 Feb 2025 16:36:21 +0200 Subject: [PATCH 17/18] . --- README.md | 128 +++++++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 103 insertions(+), 25 deletions(-) diff --git a/README.md b/README.md index 26eb0af..c8bb718 100644 --- a/README.md +++ b/README.md @@ -39,7 +39,15 @@ For a full tutorial see [How To: Build a Serverless API with Serverless, AWS Lam Lambda API includes comprehensive TypeScript definitions out of the box. You can leverage type safety across your entire API: ```typescript -import { API, Request, Response } from 'lambda-api'; +import { + API, + Request, + Response, + SourceAgnosticHandler, + SourceAgnosticMiddleware, + isApiGatewayContext, + isAlbContext +} from 'lambda-api'; // Define your response type interface UserResponse { @@ -51,34 +59,48 @@ interface UserResponse { // Create a typed API instance const api = new API(); -// Routes with type-safe request/response -api.get('/users/:id', (req, res) => { +// Source-agnostic handler that works with any Lambda trigger +const handler: SourceAgnosticHandler = (req, res) => { + // Common properties are always available + console.log(req.method, req.path); + + // Type-safe access to source-specific features + if (isApiGatewayContext(req.requestContext)) { + console.log(req.requestContext.identity); + } else if (isAlbContext(req.requestContext)) { + console.log(req.requestContext.elb); + } + res.json({ - id: req.params.id, + id: '1', name: 'John', email: 'john@example.com' }); -}); +}; -// Middleware with type checking -const authMiddleware: Middleware = (req, res, next) => { - // TypeScript will ensure type safety +// Source-agnostic middleware +const middleware: SourceAgnosticMiddleware = (req, res, next) => { + // Works with any Lambda trigger + console.log(`${req.method} ${req.path}`); next(); }; -// Full type support for complex scenarios +// Use with API methods +api.get('/users', middleware, handler); + +// For source-specific handlers, you can specify the context type interface UserQuery { fields: string } interface UserParams { id: string } interface UserBody { name: string; email: string } -api.post( +api.post( '/users', (req, res) => { // Full type safety for: req.query.fields; // UserQuery req.params.id; // UserParams req.body.name; // UserBody - req.requestContext; // ALBContext + req.requestContext; // APIGatewayContext res.json({ id: '1', @@ -87,23 +109,11 @@ api.post( }); } ); - -// Error handling with types -const errorHandler: ErrorHandlingMiddleware = ( - error, - req, - res, - next -) => { - res.status(500).json({ - id: 'error', - name: error.name, - email: error.message - }); -}; ``` Key TypeScript Features: +- Source-agnostic types that work with any Lambda trigger +- Type guards for safe context type checking - Full type inference for request and response objects - Generic type parameters for response types - Support for API Gateway and ALB contexts @@ -112,6 +122,74 @@ Key TypeScript Features: - Automatic type inference for all HTTP methods - Type safety for cookies, headers, and other API features +## Type Guards + +Lambda API provides type guards to safely work with different request sources: + +```typescript +import { + isApiGatewayContext, + isApiGatewayV2Context, + isAlbContext, + isApiGatewayRequest, + isApiGatewayV2Request, + isAlbRequest +} from 'lambda-api'; + +// Check request context type +if (isApiGatewayContext(req.requestContext)) { + // TypeScript knows this is APIGatewayContext + console.log(req.requestContext.identity); +} + +// Check entire request type +if (isApiGatewayRequest(req)) { + // TypeScript knows this is Request + console.log(req.requestContext.identity); +} +``` + +## Handling Multiple Request Sources + +Lambda API provides type-safe support for different AWS Lambda triggers. You can write source-specific handlers or use source-agnostic handlers that work with any trigger: + +```typescript +import { + isApiGatewayContext, + isApiGatewayV2Context, + isAlbContext, + SourceAgnosticHandler +} from 'lambda-api'; + +// Source-specific handler +api.get('/api-gateway', (req, res) => { + console.log(req.requestContext.identity); +}); + +api.get('/alb', (req, res) => { + console.log(req.requestContext.elb); +}); + +// Source-agnostic handler (works with any trigger) +const handler: SourceAgnosticHandler = (req, res) => { + if (isApiGatewayContext(req.requestContext)) { + console.log(req.requestContext.identity); + } else if (isAlbContext(req.requestContext)) { + console.log(req.requestContext.elb); + } + + res.json({ status: 'ok' }); +}; + +api.get('/any', handler); +``` + +Key features for handling multiple sources: +- Type guards for safe context type checking +- Source-agnostic types that work with any trigger +- Full type safety for source-specific properties +- Automatic payload format detection + ## Why Another Web Framework? Express.js, Fastify, Koa, Restify, and Hapi are just a few of the many amazing web frameworks out there for Node.js. So why build yet another one when there are so many great options already? One word: **DEPENDENCIES**. From 18216775fb9773e8397528075bccefdd19510d5a Mon Sep 17 00:00:00 2001 From: Naor Peled Date: Sat, 8 Feb 2025 16:40:11 +0200 Subject: [PATCH 18/18] . --- README.md | 91 +++++++++++++++++++++++++++-------------------- index.js | 20 ++++++----- lib/typeguards.js | 14 ++++---- 3 files changed, 72 insertions(+), 53 deletions(-) diff --git a/README.md b/README.md index c8bb718..331ee1d 100644 --- a/README.md +++ b/README.md @@ -39,14 +39,14 @@ For a full tutorial see [How To: Build a Serverless API with Serverless, AWS Lam Lambda API includes comprehensive TypeScript definitions out of the box. You can leverage type safety across your entire API: ```typescript -import { - API, - Request, +import { + API, + Request, Response, SourceAgnosticHandler, SourceAgnosticMiddleware, isApiGatewayContext, - isAlbContext + isAlbContext, } from 'lambda-api'; // Define your response type @@ -63,18 +63,18 @@ const api = new API(); const handler: SourceAgnosticHandler = (req, res) => { // Common properties are always available console.log(req.method, req.path); - + // Type-safe access to source-specific features if (isApiGatewayContext(req.requestContext)) { console.log(req.requestContext.identity); } else if (isAlbContext(req.requestContext)) { console.log(req.requestContext.elb); } - + res.json({ id: '1', name: 'John', - email: 'john@example.com' + email: 'john@example.com', }); }; @@ -89,29 +89,37 @@ const middleware: SourceAgnosticMiddleware = (req, res, next) => { api.get('/users', middleware, handler); // For source-specific handlers, you can specify the context type -interface UserQuery { fields: string } -interface UserParams { id: string } -interface UserBody { name: string; email: string } +interface UserQuery { + fields: string; +} +interface UserParams { + id: string; +} +interface UserBody { + name: string; + email: string; +} api.post( '/users', (req, res) => { // Full type safety for: - req.query.fields; // UserQuery - req.params.id; // UserParams - req.body.name; // UserBody - req.requestContext; // APIGatewayContext - + req.query.fields; // UserQuery + req.params.id; // UserParams + req.body.name; // UserBody + req.requestContext; // APIGatewayContext + res.json({ id: '1', name: req.body.name, - email: req.body.email + email: req.body.email, }); } ); ``` Key TypeScript Features: + - Source-agnostic types that work with any Lambda trigger - Type guards for safe context type checking - Full type inference for request and response objects @@ -127,13 +135,13 @@ Key TypeScript Features: Lambda API provides type guards to safely work with different request sources: ```typescript -import { +import { isApiGatewayContext, - isApiGatewayV2Context, + isApiGatewayV2Context, isAlbContext, isApiGatewayRequest, isApiGatewayV2Request, - isAlbRequest + isAlbRequest, } from 'lambda-api'; // Check request context type @@ -154,11 +162,11 @@ if (isApiGatewayRequest(req)) { Lambda API provides type-safe support for different AWS Lambda triggers. You can write source-specific handlers or use source-agnostic handlers that work with any trigger: ```typescript -import { - isApiGatewayContext, - isApiGatewayV2Context, +import { + isApiGatewayContext, + isApiGatewayV2Context, isAlbContext, - SourceAgnosticHandler + SourceAgnosticHandler, } from 'lambda-api'; // Source-specific handler @@ -177,7 +185,7 @@ const handler: SourceAgnosticHandler = (req, res) => { } else if (isAlbContext(req.requestContext)) { console.log(req.requestContext.elb); } - + res.json({ status: 'ok' }); }; @@ -185,6 +193,7 @@ api.get('/any', handler); ``` Key features for handling multiple sources: + - Type guards for safe context type checking - Source-agnostic types that work with any trigger - Full type safety for source-specific properties @@ -1661,7 +1670,9 @@ declare module 'lambda-api' { } } -function hasUser(req: Request): req is Request & { user: { id: string; roles: string[]; email: string; } } { +function hasUser( + req: Request +): req is Request & { user: { id: string; roles: string[]; email: string } } { return 'user' in req && req.user !== undefined; } @@ -1669,7 +1680,7 @@ const authMiddleware: Middleware = (req, res, next) => { req.user = { id: '123', roles: ['admin'], - email: 'user@example.com' + email: 'user@example.com', }; next(); }; @@ -1695,7 +1706,7 @@ const responseEnhancer: Middleware = (req, res, next) => { res.sendWithTimestamp = (data: any) => { res.json({ ...data, - timestamp: Date.now() + timestamp: Date.now(), }); }; next(); @@ -1725,7 +1736,7 @@ const authMiddleware: Middleware = (req, res, next) => { userId: '123', roles: ['user'], type: 'Bearer', - value: 'token123' + value: 'token123', }; next(); }; @@ -1743,13 +1754,12 @@ interface QueryParams { offset?: string; } -api.get( - '/users', - (req, res) => { - const { limit, offset } = req.query; - res.json({ /* ... */ }); - } -); +api.get('/users', (req, res) => { + const { limit, offset } = req.query; + res.json({ + /* ... */ + }); +}); interface CreateUserBody { name: string; @@ -1760,7 +1770,9 @@ api.post( '/users', (req, res) => { const { name, email } = req.body; - res.json({ /* ... */ }); + res.json({ + /* ... */ + }); } ); @@ -1779,7 +1791,11 @@ api.get('/protected', withUser(handler)); ## Handling Multiple Request Sources ```typescript -import { isApiGatewayContext, isApiGatewayV2Context, isAlbContext } from 'lambda-api'; +import { + isApiGatewayContext, + isApiGatewayV2Context, + isAlbContext, +} from 'lambda-api'; api.get('/api-gateway', (req, res) => { console.log(req.requestContext.identity); @@ -1797,4 +1813,3 @@ api.get('/any', (req, res) => { } }); ``` - diff --git a/index.js b/index.js index 399912d..c8a217b 100644 --- a/index.js +++ b/index.js @@ -12,7 +12,11 @@ const LOGGER = require('./lib/logger'); const S3 = () => require('./lib/s3-service'); const prettyPrint = require('./lib/prettyPrint'); const { ConfigurationError } = require('./lib/errors'); -const { isApiGatewayContext, isApiGatewayV2Context, isAlbContext } = require('./lib/typeguards'); +const { + isApiGatewayContext, + isApiGatewayV2Context, + isAlbContext, +} = require('./lib/typeguards'); class API { constructor(props) { @@ -43,8 +47,8 @@ class API { : {}; this._compression = props && - (typeof props.compression === 'boolean' || - Array.isArray(props.compression)) + (typeof props.compression === 'boolean' || + Array.isArray(props.compression)) ? props.compression : false; @@ -85,7 +89,7 @@ class API { this._app = {}; // Executed after the callback - this._finally = () => { }; + this._finally = () => {}; // Global error status (used for response parsing errors) this._errorStatus = 500; @@ -214,8 +218,8 @@ class API { stack: _stack['m'][method] ? _stack['m'][method].concat(stack) : _stack['*'][method] - ? _stack['*'][method].concat(stack) - : stack, + ? _stack['*'][method].concat(stack) + : stack, // inherited: _stack[method] ? _stack[method] : [], route: '/' + parsedPath.join('/'), path: '/' + this._prefix.concat(parsedPath).join('/'), @@ -451,8 +455,8 @@ class API { typeof args[0] === 'string' ? Array.of(args.shift()) : Array.isArray(args[0]) - ? args.shift() - : ['/*']; + ? args.shift() + : ['/*']; // Init middleware stack let middleware = []; diff --git a/lib/typeguards.js b/lib/typeguards.js index f807aa6..7188004 100644 --- a/lib/typeguards.js +++ b/lib/typeguards.js @@ -1,19 +1,19 @@ 'use strict'; const isApiGatewayContext = (context) => { - return 'identity' in context; + return 'identity' in context; }; const isApiGatewayV2Context = (context) => { - return 'http' in context; + return 'http' in context; }; const isAlbContext = (context) => { - return 'elb' in context; + return 'elb' in context; }; module.exports = { - isApiGatewayContext, - isApiGatewayV2Context, - isAlbContext -}; \ No newline at end of file + isApiGatewayContext, + isApiGatewayV2Context, + isAlbContext, +};