From 07d02af2886ef551cc6a273cbafc96feb3dfbb0d Mon Sep 17 00:00:00 2001 From: Blayne Chard Date: Fri, 8 Jul 2022 10:11:14 +1200 Subject: [PATCH 1/5] fix: lambda function urls do not decode rawPath --- src/__test__/examples.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/__test__/examples.ts b/src/__test__/examples.ts index 98b1205..db3062d 100644 --- a/src/__test__/examples.ts +++ b/src/__test__/examples.ts @@ -157,7 +157,7 @@ export const AlbExample: ALBEvent = { export const UrlExample: UrlEvent = { version: '2.0', routeKey: '$default', - rawPath: '/v1/🦄/🌈/🦄.json', + rawPath: '/v1/%F0%9F%A6%84/%F0%9F%8C%88/%F0%9F%A6%84.json', rawQueryString: '%F0%9F%A6%84=abc123', headers: { 'x-amzn-trace-id': 'Root=1-624e71a0-114297900a437c050c74f1fe', From 025f7114b1cb5342e933494dcc655cbd542d6707 Mon Sep 17 00:00:00 2001 From: Blayne Chard Date: Fri, 8 Jul 2022 12:02:49 +1200 Subject: [PATCH 2/5] feat: switch to a more complete routing engine find-my-way --- package.json | 1 + src/__test__/examples.ts | 40 +++++++++++++++++++-- src/__test__/url.test.ts | 2 +- src/__test__/wrap.test.ts | 2 +- src/function.ts | 8 +++-- src/http/__test__/router.test.ts | 47 ++++++++++++++++++++++++ src/http/request.http.ts | 5 +++ src/http/router.ts | 61 +++++++++++++++----------------- yarn.lock | 20 +++++++++++ 9 files changed, 148 insertions(+), 38 deletions(-) create mode 100644 src/http/__test__/router.test.ts diff --git a/package.json b/package.json index c09e812..3a89d6e 100644 --- a/package.json +++ b/package.json @@ -41,6 +41,7 @@ ], "dependencies": { "@linzjs/metrics": "^6.21.1", + "find-my-way": "^7.0.0", "pino": "^7.9.1", "ulid": "^2.3.0" } diff --git a/src/__test__/examples.ts b/src/__test__/examples.ts index db3062d..17b947b 100644 --- a/src/__test__/examples.ts +++ b/src/__test__/examples.ts @@ -1,5 +1,8 @@ -import { ALBEvent, APIGatewayProxyEvent, CloudFrontRequestEvent } from 'aws-lambda'; -import { UrlEvent } from '../http/request.url'; +import { ALBEvent, APIGatewayProxyEvent, CloudFrontRequestEvent, Context } from 'aws-lambda'; +import { LambdaAlbRequest } from '../http/request.alb.js'; +import { LambdaApiGatewayRequest } from '../http/request.api.gateway.js'; +import { LambdaUrlRequest, UrlEvent } from '../http/request.url.js'; +import { fakeLog } from './log.js'; export const ApiGatewayExample: APIGatewayProxyEvent = { body: 'eyJ0ZXN0IjoiYm9keSJ9', @@ -194,3 +197,36 @@ export const UrlExample: UrlEvent = { export function clone(c: T): T { return JSON.parse(JSON.stringify(c)); } + +const fakeContext = {} as Context; + +export function newRequestUrl>(path: string, query: string): LambdaUrlRequest { + const example = clone(UrlExample); + example.rawPath = encodeURI(path); + example.rawQueryString = encodeURI(query); + example.requestContext.http.path = path; + return new LambdaUrlRequest(example, fakeContext, fakeLog) as LambdaUrlRequest; +} + +export function newRequestAlb>(path: string, query: string): LambdaAlbRequest { + const example = clone(AlbExample); + example.path = encodeURI(path); + example.queryStringParameters = {}; + for (const [key, value] of new URLSearchParams(query).entries()) { + example.queryStringParameters[key] = value; + } + return new LambdaAlbRequest(example, fakeContext, fakeLog) as LambdaAlbRequest; +} + +export function newRequestApi>( + path: string, + query: string, +): LambdaApiGatewayRequest { + const example = clone(ApiGatewayExample); + example.path = encodeURI(path); + example.multiValueQueryStringParameters = {}; + for (const [key, value] of new URLSearchParams(query).entries()) { + example.multiValueQueryStringParameters[key] = [value]; + } + return new LambdaApiGatewayRequest(example, fakeContext, fakeLog) as LambdaApiGatewayRequest; +} diff --git a/src/__test__/url.test.ts b/src/__test__/url.test.ts index ea33037..efaf0b7 100644 --- a/src/__test__/url.test.ts +++ b/src/__test__/url.test.ts @@ -44,7 +44,7 @@ o.spec('FunctionUrl', () => { o('should support utf8 paths and query', () => { const req = new LambdaUrlRequest(UrlExample, fakeContext, fakeLog); - o(req.path).equals('/v1/🦄/🌈/🦄.json'); + o(req.path).equals('/v1/%F0%9F%A6%84/%F0%9F%8C%88/%F0%9F%A6%84.json'); o(req.query.get('🦄')).equals('abc123'); o(req.body).equals(null); }); diff --git a/src/__test__/wrap.test.ts b/src/__test__/wrap.test.ts index 1393957..47bce75 100644 --- a/src/__test__/wrap.test.ts +++ b/src/__test__/wrap.test.ts @@ -72,7 +72,7 @@ o.spec('LambdaWrap', () => { const headers: Record = { DELETE: 1, - ALL: 0, + // ALL: 0, GET: 1, OPTIONS: 1, HEAD: 1, diff --git a/src/function.ts b/src/function.ts index 1e22f06..df4a85b 100644 --- a/src/function.ts +++ b/src/function.ts @@ -64,7 +64,9 @@ export async function execute( } req.set('status', status); - req.set('metrics', req.timer.metrics); + if (req.timer.timers.size > 0) { + req.set('metrics', req.timer.metrics); + } if (versionInfo.hash) req.set('package', versionInfo); @@ -179,7 +181,9 @@ export class lf { const cloudFrontId = req.header(HttpHeaderAmazon.CloudfrontId); const traceId = req.header(HttpHeaderAmazon.TraceId); const lambdaId = context.awsRequestId; - req.set('aws', { cloudFrontId, traceId, lambdaId }); + if (cloudFrontId || traceId || lambdaId) { + req.set('aws', { cloudFrontId, traceId, lambdaId }); + } req.set('method', req.method); req.set('path', req.path); diff --git a/src/http/__test__/router.test.ts b/src/http/__test__/router.test.ts new file mode 100644 index 0000000..4175c15 --- /dev/null +++ b/src/http/__test__/router.test.ts @@ -0,0 +1,47 @@ +import o from 'ospec'; +import { newRequestAlb, newRequestApi, newRequestUrl } from '../../__test__/examples.js'; +import { LambdaHttpRequest } from '../request.http.js'; +import { LambdaHttpResponse } from '../response.http.js'; +import { Router } from '../router.js'; + +o.spec('Router', () => { + const router = new Router(); + router.get('/v1/🦄/🌈/:fileName', (req: LambdaHttpRequest<{ Params: { fileName: string } }>) => { + return LambdaHttpResponse.ok().json({ + fileName: req.params.fileName, + path: req.path, + query: [...req.query.entries()], + }); + }); + const expectedResult = { fileName: '🦄.json', path: encodeURI('/v1/🦄/🌈/🦄.json'), query: [['🌈', '🦄']] }; + + o('should route rainbows LambdaUrl', async () => { + const urlRoute = newRequestUrl('/v1/🦄/🌈/🦄.json', '🌈=🦄'); + const res = await router.handle(urlRoute); + o(res.status).equals(200); + o(res.body).deepEquals(JSON.stringify(expectedResult)); + + const resb = await router.handle(newRequestUrl('/v2/🦄/🌈/🦄.json', '🌈=🦄')); + o(resb.status).equals(404); + }); + + o('should route rainbows LambdaAlb', async () => { + const urlRoute = newRequestAlb('/v1/🦄/🌈/🦄.json', '🌈=🦄'); + const res = await router.handle(urlRoute); + o(res.status).equals(200); + o(res.body).deepEquals(JSON.stringify(expectedResult)); + + const resb = await router.handle(newRequestUrl('/v2/🦄/🌈/🦄.json', '🌈=🦄')); + o(resb.status).equals(404); + }); + + o('should route rainbows LambdaApi', async () => { + const urlRoute = newRequestApi('/v1/🦄/🌈/🦄.json', '🌈=🦄'); + const res = await router.handle(urlRoute); + o(res.status).equals(200); + o(res.body).deepEquals(JSON.stringify(expectedResult)); + + const resb = await router.handle(newRequestUrl('/v2/🦄/🌈/🦄.json', '🌈=🦄')); + o(resb.status).equals(404); + }); +}); diff --git a/src/http/request.http.ts b/src/http/request.http.ts index df6b521..9a1bb8d 100644 --- a/src/http/request.http.ts +++ b/src/http/request.http.ts @@ -98,4 +98,9 @@ export abstract class LambdaHttpRequest< if (this._query == null) this._query = this.loadQueryString(); return this._query; } + + /** This is used by the router and is just the path */ + protected get url(): string { + return this.path; + } } diff --git a/src/http/router.ts b/src/http/router.ts index 15d229d..8569e52 100644 --- a/src/http/router.ts +++ b/src/http/router.ts @@ -1,6 +1,7 @@ import { execute, runFunction } from '../function.js'; import { LambdaHttpRequest, RequestTypes } from './request.http.js'; import { LambdaHttpResponse } from './response.http.js'; +import FindMyWay from 'find-my-way'; export type Route = ( req: LambdaHttpRequest, @@ -22,36 +23,32 @@ export type HookRecord = { [K in keyof T]: T[K][]; }; -export type HttpMethods = 'DELETE' | 'GET' | 'HEAD' | 'PATCH' | 'POST' | 'PUT' | 'OPTIONS' | 'ALL'; +export type HttpMethods = 'DELETE' | 'GET' | 'HEAD' | 'PATCH' | 'POST' | 'PUT' | 'OPTIONS'; export class Router { - routes: { - path: RegExp; - method: HttpMethods; - // TODO is there a better way to model this route list - // eslint-disable-next-line @typescript-eslint/no-explicit-any - fn: Route; - }[] = []; - hooks: HookRecord = { /** Hooks to be run before every request */ request: [], /** Hooks to be run after every request */ response: [], }; + router: FindMyWay.Instance; + + constructor() { + this.router = FindMyWay(); + } register(method: HttpMethods, path: string, fn: Route): void { - // Stolen from https://github.com/kwhitley/itty-router - const regex = RegExp( - `^${ - path - .replace(/(\/?)\*/g, '($1.*)?') - .replace(/\/$/, '') - .replace(/:(\w+)(\?)?(\.)?/g, '$2(?<$1>[^/]+)$2$3') - .replace(/\.(?=[\w(])/, '\\.') - .replace(/\)\.\?\(([^\[]+)\[\^/g, '?)\\.?($1(?<=\\.)[^\\.') // RIP all the bytes lost :'( - }/*$`, - ); - this.routes.push({ path: regex, method, fn }); + this.router.on(method, path, async (req: unknown, res, params) => { + if (req instanceof LambdaHttpRequest) { + req.params = params; + console.log({ params: req.params, query: [...req.query.entries()] }); + const ret = await fn(req); + if (ret != null) { + res.statusCode = ret.status; + res.end(ret); + } + } + }); } /** @@ -65,7 +62,7 @@ export class Router { /** Register a route for all HTTP types, GET, POST, HEAD, etc... */ all(path: string, fn: Route): void { - return this.register('ALL', path, fn); + // return this.register('ALL', path, fn); } get(path: string, fn: Route): void { return this.register('GET', path, fn); @@ -114,16 +111,16 @@ export class Router { if (res) return this.after(req, res); } - for (const r of this.routes) { - if (r.method !== 'ALL' && req.method !== r.method) continue; - const m = req.path.match(r.path); - if (m) { - // TODO this should ideally be validated - req.params = m.groups; - const res = await execute(req, r.fn); - if (res) return this.after(req, res); - } - } + const routeRes = { + end(value: unknown): unknown { + return null; + }, + }; + const routePromise = new Promise((resolve) => (routeRes.end = resolve)); + this.router.lookup(req as any, routeRes as any); + + const result = await routePromise; + if (result) return this.after(req, result); return this.after(req, new LambdaHttpResponse(404, 'Not found')); } diff --git a/yarn.lock b/yarn.lock index 211bf36..8e062eb 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1069,6 +1069,14 @@ fill-range@^7.0.1: dependencies: to-regex-range "^5.0.1" +find-my-way@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/find-my-way/-/find-my-way-7.0.0.tgz#8e79fde2606624af61775e3d097da4f1872e58d9" + integrity sha512-NHVohYPYRXgj6jxXVRwm4iMQjA2ggJpyewHz7Nq7hvBnHoYJJIyHuxNzs8QLPTLQfoqxZzls2g6Zm79XMbhXjA== + dependencies: + fast-deep-equal "^3.1.3" + safe-regex2 "^2.0.0" + find-up@^1.0.0: version "1.1.2" resolved "https://registry.yarnpkg.com/find-up/-/find-up-1.1.2.tgz#6b2e9822b1a2ce0a60ab64d610eccad53cb24d0f" @@ -2378,6 +2386,11 @@ responselike@1.0.2: dependencies: lowercase-keys "^1.0.0" +ret@~0.2.0: + version "0.2.2" + resolved "https://registry.yarnpkg.com/ret/-/ret-0.2.2.tgz#b6861782a1f4762dce43402a71eb7a283f44573c" + integrity sha512-M0b3YWQs7R3Z917WRQy1HHA7Ba7D8hvZg6UE5mLykJxQVE2ju0IXbGlaHPPlkY+WN7wFP+wUMXmBFA0aV6vYGQ== + reusify@^1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76" @@ -2407,6 +2420,13 @@ safe-buffer@~5.1.0, safe-buffer@~5.1.1: resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== +safe-regex2@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/safe-regex2/-/safe-regex2-2.0.0.tgz#b287524c397c7a2994470367e0185e1916b1f5b9" + integrity sha512-PaUSFsUaNNuKwkBijoAPHAK6/eM6VirvyPWlZ7BAQy4D+hCvh4B6lIG+nPdhbFfIbP+gTGBcrdsOaUs0F+ZBOQ== + dependencies: + ret "~0.2.0" + safe-stable-stringify@^2.1.0: version "2.3.1" resolved "https://registry.yarnpkg.com/safe-stable-stringify/-/safe-stable-stringify-2.3.1.tgz#ab67cbe1fe7d40603ca641c5e765cb942d04fc73" From ed6239fcd75ab89bedcfd7027267555cfb97d91e Mon Sep 17 00:00:00 2001 From: Blayne Chard Date: Fri, 8 Jul 2022 12:05:34 +1200 Subject: [PATCH 3/5] wip: test cloudfront with rainbows --- src/__test__/examples.ts | 11 +++++++++++ src/http/__test__/router.test.ts | 18 ++++++++++++++---- 2 files changed, 25 insertions(+), 4 deletions(-) diff --git a/src/__test__/examples.ts b/src/__test__/examples.ts index 17b947b..400d913 100644 --- a/src/__test__/examples.ts +++ b/src/__test__/examples.ts @@ -1,6 +1,7 @@ import { ALBEvent, APIGatewayProxyEvent, CloudFrontRequestEvent, Context } from 'aws-lambda'; import { LambdaAlbRequest } from '../http/request.alb.js'; import { LambdaApiGatewayRequest } from '../http/request.api.gateway.js'; +import { LambdaCloudFrontRequest } from '../http/request.cloudfront.js'; import { LambdaUrlRequest, UrlEvent } from '../http/request.url.js'; import { fakeLog } from './log.js'; @@ -230,3 +231,13 @@ export function newRequestApi>( } return new LambdaApiGatewayRequest(example, fakeContext, fakeLog) as LambdaApiGatewayRequest; } + +export function newRequestCloudFront>( + path: string, + query: string, +): LambdaCloudFrontRequest { + const example = clone(CloudfrontExample); + example.Records[0].cf.request.uri = encodeURI(path); + example.Records[0].cf.request.querystring = '?' + query; + return new LambdaCloudFrontRequest(example, fakeContext, fakeLog) as LambdaCloudFrontRequest; +} diff --git a/src/http/__test__/router.test.ts b/src/http/__test__/router.test.ts index 4175c15..221c929 100644 --- a/src/http/__test__/router.test.ts +++ b/src/http/__test__/router.test.ts @@ -1,5 +1,5 @@ import o from 'ospec'; -import { newRequestAlb, newRequestApi, newRequestUrl } from '../../__test__/examples.js'; +import { newRequestAlb, newRequestApi, newRequestCloudFront, newRequestUrl } from '../../__test__/examples.js'; import { LambdaHttpRequest } from '../request.http.js'; import { LambdaHttpResponse } from '../response.http.js'; import { Router } from '../router.js'; @@ -15,7 +15,7 @@ o.spec('Router', () => { }); const expectedResult = { fileName: '🦄.json', path: encodeURI('/v1/🦄/🌈/🦄.json'), query: [['🌈', '🦄']] }; - o('should route rainbows LambdaUrl', async () => { + o('should route rainbows and unicorns LambdaUrl', async () => { const urlRoute = newRequestUrl('/v1/🦄/🌈/🦄.json', '🌈=🦄'); const res = await router.handle(urlRoute); o(res.status).equals(200); @@ -25,7 +25,7 @@ o.spec('Router', () => { o(resb.status).equals(404); }); - o('should route rainbows LambdaAlb', async () => { + o('should route rainbows and unicorns LambdaAlb', async () => { const urlRoute = newRequestAlb('/v1/🦄/🌈/🦄.json', '🌈=🦄'); const res = await router.handle(urlRoute); o(res.status).equals(200); @@ -35,7 +35,7 @@ o.spec('Router', () => { o(resb.status).equals(404); }); - o('should route rainbows LambdaApi', async () => { + o('should route rainbows and unicorns LambdaApi', async () => { const urlRoute = newRequestApi('/v1/🦄/🌈/🦄.json', '🌈=🦄'); const res = await router.handle(urlRoute); o(res.status).equals(200); @@ -44,4 +44,14 @@ o.spec('Router', () => { const resb = await router.handle(newRequestUrl('/v2/🦄/🌈/🦄.json', '🌈=🦄')); o(resb.status).equals(404); }); + + o('should route rainbows and unicorns LambdaCloudFront', async () => { + const urlRoute = newRequestCloudFront('/v1/🦄/🌈/🦄.json', '🌈=🦄'); + const res = await router.handle(urlRoute); + o(res.status).equals(200); + o(res.body).deepEquals(JSON.stringify(expectedResult)); + + const resb = await router.handle(newRequestUrl('/v2/🦄/🌈/🦄.json', '🌈=🦄')); + o(resb.status).equals(404); + }); }); From a2338284f4c6e614525613ddfc15dcf2967cc122 Mon Sep 17 00:00:00 2001 From: Blayne Chard Date: Sat, 9 Jul 2022 13:27:13 +1200 Subject: [PATCH 4/5] refactor: cleanup types --- README.md | 4 +-- src/__test__/examples.ts | 7 ++++ src/__test__/readme.example.ts | 4 +-- src/__test__/wrap.test.ts | 13 ++++--- src/http/__test__/router.test.ts | 61 +++++++++++--------------------- src/http/router.ts | 36 +++++++------------ 6 files changed, 50 insertions(+), 75 deletions(-) diff --git a/README.md b/README.md index cb5e67f..b7c21f8 100644 --- a/README.md +++ b/README.md @@ -26,11 +26,11 @@ handler.router.get<{ Params: { style: string } }>( ); // Handle all requests -handler.router.all('*', () => new LambdaHttpResponse(404, 'Not found')); +handler.router.get('*', () => new LambdaHttpResponse(404, 'Not found')); // create middleware to validate api key on all requests -handler.router.all('*', (req) => { +handler.router.hook('request', (req) => { const isApiValid = validateApiKey(req.query.get('api')); // Bail early if (!isApiValid) return new LambdaHttpResponse(400, 'Invalid api key'); diff --git a/src/__test__/examples.ts b/src/__test__/examples.ts index 400d913..7f04429 100644 --- a/src/__test__/examples.ts +++ b/src/__test__/examples.ts @@ -201,6 +201,13 @@ export function clone(c: T): T { const fakeContext = {} as Context; +export const RequestTypes = [ + { type: 'FunctionUrl', create: newRequestUrl }, + { type: 'Alb', create: newRequestAlb }, + { type: 'ApiGateway', create: newRequestApi }, + { type: 'CloudFront', create: newRequestCloudFront }, +]; + export function newRequestUrl>(path: string, query: string): LambdaUrlRequest { const example = clone(UrlExample); example.rawPath = encodeURI(path); diff --git a/src/__test__/readme.example.ts b/src/__test__/readme.example.ts index 4ba4cbe..e3b6be2 100644 --- a/src/__test__/readme.example.ts +++ b/src/__test__/readme.example.ts @@ -27,13 +27,13 @@ handler.router.get<{ Params: { style: string } }>( ); // Handle all requests -handler.router.all('*', () => new LambdaHttpResponse(404, 'Not found')); +handler.router.get('*', () => new LambdaHttpResponse(404, 'Not found')); function validateApiKey(s?: string | null): boolean { return s != null; } // create middleware to validate api key on all requests -handler.router.all('*', (req) => { +handler.router.hook('request', (req) => { const isApiValid = validateApiKey(req.query.get('api')); // Bail early if (!isApiValid) return new LambdaHttpResponse(400, 'Invalid api key'); diff --git a/src/__test__/wrap.test.ts b/src/__test__/wrap.test.ts index 47bce75..06e0bb8 100644 --- a/src/__test__/wrap.test.ts +++ b/src/__test__/wrap.test.ts @@ -34,13 +34,13 @@ o.spec('LambdaWrap', () => { o('should handle middleware', async () => { const fn = lf.http(fakeLog); - fn.router.all('*', (req): LambdaHttpResponse | void => { + fn.router.hook('request', (req): LambdaHttpResponse | void => { if (req.path.includes('fail')) return new LambdaHttpResponse(500, 'Failed'); }); - fn.router.get('/v1/ping', () => new LambdaHttpResponse(200, 'Ok')); + fn.router.get('/v1/ping/:message', () => new LambdaHttpResponse(200, 'Ok')); const newReq = clone(ApiGatewayExample); - newReq.path = '/v1/ping'; + newReq.path = '/v1/ping/ok'; const ret = await new Promise((resolve) => fn(newReq, fakeContext, (a, b) => resolve(b))); assertAlbResult(ret); @@ -98,7 +98,6 @@ o.spec('LambdaWrap', () => { const fn = lf.http(fakeLog); fn.router.get('/v1/tiles/:tileSet/:projection/:z/:x/:y.json', fakeLambda); await new Promise((resolve) => fn(AlbExample, fakeContext, (a, b) => resolve(b))); - o(fakeLog.logs.length).equals(1); const firstLog = fakeLog.logs[0]; @@ -182,7 +181,7 @@ o.spec('LambdaWrap', () => { o('should handle thrown http responses', async () => { const fn = lf.http(fakeLog); - fn.router.all('*', () => { + fn.router.get('*', () => { throw new LambdaHttpResponse(400, 'Error'); }); const ret = await new Promise((resolve) => fn(ApiGatewayExample, fakeContext, (a, b) => resolve(b))); @@ -193,7 +192,7 @@ o.spec('LambdaWrap', () => { o('should handle http exceptions', async () => { const fn = lf.http(fakeLog); - fn.router.all('*', () => { + fn.router.get('*', () => { throw new Error('Error'); }); const ret = await new Promise((resolve) => fn(ApiGatewayExample, fakeContext, (a, b) => resolve(b))); @@ -252,7 +251,7 @@ o.spec('LambdaWrap', () => { const serverName = lf.ServerName; lf.ServerName = null; const fn = lf.http(); - fn.router.all('*', fakeLambda); + fn.router.get('*', fakeLambda); const ret = await new Promise((resolve) => fn(ApiGatewayExample, fakeContext, (a, b) => resolve(b))); lf.ServerName = serverName; diff --git a/src/http/__test__/router.test.ts b/src/http/__test__/router.test.ts index 221c929..7899cd8 100644 --- a/src/http/__test__/router.test.ts +++ b/src/http/__test__/router.test.ts @@ -1,5 +1,5 @@ import o from 'ospec'; -import { newRequestAlb, newRequestApi, newRequestCloudFront, newRequestUrl } from '../../__test__/examples.js'; +import { RequestTypes } from '../../__test__/examples.js'; import { LambdaHttpRequest } from '../request.http.js'; import { LambdaHttpResponse } from '../response.http.js'; import { Router } from '../router.js'; @@ -15,43 +15,24 @@ o.spec('Router', () => { }); const expectedResult = { fileName: '🦄.json', path: encodeURI('/v1/🦄/🌈/🦄.json'), query: [['🌈', '🦄']] }; - o('should route rainbows and unicorns LambdaUrl', async () => { - const urlRoute = newRequestUrl('/v1/🦄/🌈/🦄.json', '🌈=🦄'); - const res = await router.handle(urlRoute); - o(res.status).equals(200); - o(res.body).deepEquals(JSON.stringify(expectedResult)); - - const resb = await router.handle(newRequestUrl('/v2/🦄/🌈/🦄.json', '🌈=🦄')); - o(resb.status).equals(404); - }); - - o('should route rainbows and unicorns LambdaAlb', async () => { - const urlRoute = newRequestAlb('/v1/🦄/🌈/🦄.json', '🌈=🦄'); - const res = await router.handle(urlRoute); - o(res.status).equals(200); - o(res.body).deepEquals(JSON.stringify(expectedResult)); - - const resb = await router.handle(newRequestUrl('/v2/🦄/🌈/🦄.json', '🌈=🦄')); - o(resb.status).equals(404); - }); - - o('should route rainbows and unicorns LambdaApi', async () => { - const urlRoute = newRequestApi('/v1/🦄/🌈/🦄.json', '🌈=🦄'); - const res = await router.handle(urlRoute); - o(res.status).equals(200); - o(res.body).deepEquals(JSON.stringify(expectedResult)); - - const resb = await router.handle(newRequestUrl('/v2/🦄/🌈/🦄.json', '🌈=🦄')); - o(resb.status).equals(404); - }); - - o('should route rainbows and unicorns LambdaCloudFront', async () => { - const urlRoute = newRequestCloudFront('/v1/🦄/🌈/🦄.json', '🌈=🦄'); - const res = await router.handle(urlRoute); - o(res.status).equals(200); - o(res.body).deepEquals(JSON.stringify(expectedResult)); - - const resb = await router.handle(newRequestUrl('/v2/🦄/🌈/🦄.json', '🌈=🦄')); - o(resb.status).equals(404); - }); + for (const rt of RequestTypes) { + o.spec(rt.type, () => { + o(`should route rainbows and unicorns`, async () => { + const urlRoute = rt.create('/v1/🦄/🌈/🦄.json', '🌈=🦄'); + const res = await router.handle(urlRoute); + o(res.status).equals(200); + o(res.body).deepEquals(JSON.stringify(expectedResult)); + }); + + o('path should be url encoded', () => { + const req = rt.create('/v1/🦄/🌈/🦄.json', ''); + o(req.path).equals('/v1/%F0%9F%A6%84/%F0%9F%8C%88/%F0%9F%A6%84.json'); + }); + + o('should 404 on invalid routes', async () => { + const res = await router.handle(rt.create('/v1/🦄/🦄/🦄.json', '🌈=🦄')); + o(res.status).equals(404); + }); + }); + } }); diff --git a/src/http/router.ts b/src/http/router.ts index 8569e52..bfc5ff7 100644 --- a/src/http/router.ts +++ b/src/http/router.ts @@ -34,20 +34,14 @@ export class Router { router: FindMyWay.Instance; constructor() { - this.router = FindMyWay(); + this.router = FindMyWay({ defaultRoute: () => new LambdaHttpResponse(404, 'Not found') }); } register(method: HttpMethods, path: string, fn: Route): void { this.router.on(method, path, async (req: unknown, res, params) => { - if (req instanceof LambdaHttpRequest) { - req.params = params; - console.log({ params: req.params, query: [...req.query.entries()] }); - const ret = await fn(req); - if (ret != null) { - res.statusCode = ret.status; - res.end(ret); - } - } + if (!(req instanceof LambdaHttpRequest)) return new LambdaHttpResponse(500, 'Internal server error'); + req.params = params; + return execute(req, fn); }); } @@ -60,10 +54,6 @@ export class Router { this.hooks[name].push(cb); } - /** Register a route for all HTTP types, GET, POST, HEAD, etc... */ - all(path: string, fn: Route): void { - // return this.register('ALL', path, fn); - } get(path: string, fn: Route): void { return this.register('GET', path, fn); } @@ -110,16 +100,14 @@ export class Router { // If a hook returns a response return the response to the user if (res) return this.after(req, res); } - - const routeRes = { - end(value: unknown): unknown { - return null; - }, - }; - const routePromise = new Promise((resolve) => (routeRes.end = resolve)); - this.router.lookup(req as any, routeRes as any); - - const result = await routePromise; + /** + * Work around the very strict typings of find-my-way + * It expects everything to be some sort of http request, + * but internally it only ever uses `req.url` and `req.method` + * it also does not ever do anything with the response + */ + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const result = await this.router.lookup(req as any, null as any); if (result) return this.after(req, result); return this.after(req, new LambdaHttpResponse(404, 'Not found')); From 222b1df89b66b85336d8f7079e52146c768d94cc Mon Sep 17 00:00:00 2001 From: Blayne Chard Date: Mon, 18 Jul 2022 11:35:35 +1200 Subject: [PATCH 5/5] refactor: remove commented out code --- src/__test__/wrap.test.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/__test__/wrap.test.ts b/src/__test__/wrap.test.ts index 06e0bb8..bc39233 100644 --- a/src/__test__/wrap.test.ts +++ b/src/__test__/wrap.test.ts @@ -72,7 +72,6 @@ o.spec('LambdaWrap', () => { const headers: Record = { DELETE: 1, - // ALL: 0, GET: 1, OPTIONS: 1, HEAD: 1,