From 7fe122bcc3cb483eb517ab95e6f199efb2cfe8da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vin=C3=ADcius=20Louren=C3=A7o?= Date: Sat, 19 Nov 2022 11:56:22 -0300 Subject: [PATCH 1/8] test(deepkit): fixed issues with local testing of deepkit --- .../frameworks/http-deepkit.framework.spec.ts | 16 +-- test/frameworks/utils-deepkit.ts | 98 +++++++++---------- 2 files changed, 56 insertions(+), 58 deletions(-) diff --git a/test/frameworks/http-deepkit.framework.spec.ts b/test/frameworks/http-deepkit.framework.spec.ts index c0955e98..b8ce949f 100644 --- a/test/frameworks/http-deepkit.framework.spec.ts +++ b/test/frameworks/http-deepkit.framework.spec.ts @@ -1,8 +1,8 @@ -// very ugly, i know, but i didn't find any other way, sorry -if (process.env.SKIP_DEEPKIT !== 'true') - require('./utils-deepkit').runDeepkitTest(); -else { - it.skip('when nodejs is 12.x', () => { - expect(true).toEqual(true); - }); -} +describe('HttpDeepkitFramework', () => { + // very ugly, i know, but i didn't find any other way, sorry + if (process.env.SKIP_DEEPKIT === 'true') { + it('when nodejs is 12.x', () => { + expect(true).toEqual(true); + }); + } else require('./utils-deepkit').runDeepkitTest(); +}); diff --git a/test/frameworks/utils-deepkit.ts b/test/frameworks/utils-deepkit.ts index 01c245b1..c783a992 100644 --- a/test/frameworks/utils-deepkit.ts +++ b/test/frameworks/utils-deepkit.ts @@ -40,63 +40,61 @@ export function createDeepkitHandler( } export const runDeepkitTest = () => { - describe(HttpDeepkitFramework.name, () => { - it('should convert correctly when the value is not an buffer', async () => { - const framework = new HttpDeepkitFramework(); - const kernel: Partial = { - handleRequest: jest.fn((request, response) => { - request.pipe(response); + it('should convert correctly when the value is not an buffer', async () => { + const framework = new HttpDeepkitFramework(); + const kernel: Partial = { + handleRequest: jest.fn((request, response) => { + request.pipe(response); - return void 0 as any; - }), - }; - const textCodes = 'test'.split('').map(c => c.charCodeAt(0)); + return void 0 as any; + }), + }; + const textCodes = 'test'.split('').map(c => c.charCodeAt(0)); - const request = new ServerlessRequest({ - body: Uint8Array.of(...textCodes), - url: '/test', - method: 'POST', - headers: {}, - }); - const response = new ServerlessResponse({ - method: 'POST', - }); + const request = new ServerlessRequest({ + body: Uint8Array.of(...textCodes), + url: '/test', + method: 'POST', + headers: {}, + }); + const response = new ServerlessResponse({ + method: 'POST', + }); - framework.sendRequest(kernel as HttpKernel, request, response); + framework.sendRequest(kernel as HttpKernel, request, response); - await waitForStreamComplete(response); + await waitForStreamComplete(response); - const resultBody = ServerlessResponse.body(response); + const resultBody = ServerlessResponse.body(response); - expect(resultBody).toBeInstanceOf(Buffer); - expect(resultBody.toString()).toEqual('test'); - }); + expect(resultBody).toBeInstanceOf(Buffer); + expect(resultBody.toString()).toEqual('test'); + }); - createTestSuiteFor( - () => { - return new HttpDeepkitFramework(); - }, - async () => { - const testingApp = createTestingApp({ - imports: [ - new HttpModule({ debug: true }), - new FrameworkModule({ debug: true, httpLog: true }), - ], - }); + createTestSuiteFor( + () => { + return new HttpDeepkitFramework(); + }, + async () => { + const testingApp = createTestingApp({ + imports: [ + new HttpModule({ debug: true }), + new FrameworkModule({ debug: true, httpLog: true }), + ], + }); - await testingApp.startServer(); + await testingApp.startServer(); - return testingApp; - }, - { - get: createDeepkitHandler('get'), - delete: createDeepkitHandler('delete'), - post: createDeepkitHandler('post'), - put: createDeepkitHandler('put'), - }, - app => app.app.get(HttpKernel), - async app => await app.stopServer(), - process.env.SKIP_DEEPKIT === 'true', - ); - }); + return testingApp; + }, + { + get: createDeepkitHandler('get'), + delete: createDeepkitHandler('delete'), + post: createDeepkitHandler('post'), + put: createDeepkitHandler('put'), + }, + app => app.app.get(HttpKernel), + async app => await app.stopServer(), + process.env.SKIP_DEEPKIT === 'true', + ); }; From cdbae6ed7783ab3d9cdfe0e2a8a4f6eddb532ba1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vin=C3=ADcius=20Louren=C3=A7o?= Date: Sat, 19 Nov 2022 14:05:37 -0300 Subject: [PATCH 2/8] chore(tsconfig): reduce lib size by removing declaration map --- tsconfig.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tsconfig.json b/tsconfig.json index cf8e7ec6..bcb4eaaa 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -12,7 +12,7 @@ // "checkJs": true, /* Report errors in .js files. */ // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', 'react', 'react-jsx' or 'react-jsxdev'. */ "declaration": true, /* Generates corresponding '.d.ts' file. */ - "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ + "declarationMap": false, /* Generates a sourcemap for each corresponding '.d.ts' file. */ "sourceMap": true, /* Generates corresponding '.map' file. */ // "outFile": "./", /* Concatenate and emit output to single file. */ "outDir": "./lib/", /* Redirect output structure to the directory. */ From ea6ca689d14555f284b72920f8fc07786d2b6c7e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vin=C3=ADcius=20Louren=C3=A7o?= Date: Sat, 19 Nov 2022 15:56:43 -0300 Subject: [PATCH 3/8] feat(stream): expose method to validate if stream is ended --- src/core/stream.ts | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/src/core/stream.ts b/src/core/stream.ts index e7c7cfcb..94950f86 100644 --- a/src/core/stream.ts +++ b/src/core/stream.ts @@ -4,6 +4,22 @@ import { Readable, Writable } from 'stream'; //#endregion +/** + * Check if stream already ended + * + * @param stream - The stream + * + * @breadcrumb Core / Stream + * @public + */ +export function isStreamEnded(stream: Readable | Writable): boolean { + if ('readableEnded' in stream && stream.readableEnded) return true; + + if ('writableEnded' in stream && stream.writableEnded) return true; + + return false; +} + /** * Wait asynchronous the stream to complete * @@ -15,11 +31,7 @@ import { Readable, Writable } from 'stream'; export function waitForStreamComplete( stream: TStream, ): Promise { - if ('readableEnded' in stream && stream.readableEnded) - return Promise.resolve(stream); - - if ('writableEnded' in stream && stream.writableEnded) - return Promise.resolve(stream); + if (isStreamEnded(stream)) return Promise.resolve(stream); return new Promise((resolve, reject) => { // Reading the {@link https://github.com/nodejs/node/blob/v12.22.9/lib/events.js#L262 | emit source code}, From 8bf34257dc8de6fbed1d3a5127c1f4d8e75d1c9f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vin=C3=ADcius=20Louren=C3=A7o?= Date: Sat, 19 Nov 2022 16:14:16 -0300 Subject: [PATCH 4/8] feat(cors): added cors framework --- package-lock.json | 4 + package.json | 4 + src/frameworks/cors/cors.framework.ts | 203 +++++++++++++++++ src/frameworks/cors/index.ts | 1 + test/frameworks/cors.framework.spec.ts | 288 +++++++++++++++++++++++++ 5 files changed, 500 insertions(+) create mode 100644 src/frameworks/cors/cors.framework.ts create mode 100644 src/frameworks/cors/index.ts create mode 100644 test/frameworks/cors.framework.spec.ts diff --git a/package-lock.json b/package-lock.json index 404736a3..252b950a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -25,6 +25,7 @@ "@trpc/server": "^9.26.2", "@types/aws-lambda": "^8.10.92", "@types/body-parser": "^1.19.2", + "@types/cors": "*", "@types/express": "^4.17.13", "@types/hapi": "^18.0.7", "@types/jest": "^28.1.6", @@ -36,6 +37,7 @@ "body-parser": "^1.19.1", "codecov": "^3.8.1", "commitizen": "^4.2.4", + "cors": "^2.8.5", "cz-conventional-changelog": "^3.3.0", "ejs": "^3.1.6", "eslint": "^8.9.0", @@ -72,9 +74,11 @@ "@hapi/hapi": ">= 20.0.0", "@trpc/server": ">= 9.0.0", "@types/aws-lambda": ">= 8.10.92", + "@types/cors": "^2.8.12", "@types/express": ">= 4.15.4", "@types/hapi": ">= 18.0.7", "@types/koa": ">= 2.11.2", + "cors": ">= 2.8.5", "express": ">= 4.15.4", "fastify": ">= 3.0.0", "fastify-3": "npm:fastify@3.29.0", diff --git a/package.json b/package.json index 3a426261..e8609e46 100644 --- a/package.json +++ b/package.json @@ -87,6 +87,7 @@ "@trpc/server": "^9.26.2", "@types/aws-lambda": "^8.10.92", "@types/body-parser": "^1.19.2", + "@types/cors": "^2.8.12", "@types/express": "^4.17.13", "@types/hapi": "^18.0.7", "@types/jest": "^28.1.6", @@ -98,6 +99,7 @@ "body-parser": "^1.19.1", "codecov": "^3.8.1", "commitizen": "^4.2.4", + "cors": "^2.8.5", "cz-conventional-changelog": "^3.3.0", "ejs": "^3.1.6", "eslint": "^8.9.0", @@ -131,9 +133,11 @@ "@hapi/hapi": ">= 20.0.0", "@trpc/server": ">= 9.0.0", "@types/aws-lambda": ">= 8.10.92", + "@types/cors": ">= 2.8.12", "@types/express": ">= 4.15.4", "@types/hapi": ">= 18.0.7", "@types/koa": ">= 2.11.2", + "cors": ">= 2.8.5", "express": ">= 4.15.4", "fastify": ">= 3.0.0", "fastify-3": "npm:fastify@3.29.0", diff --git a/src/frameworks/cors/cors.framework.ts b/src/frameworks/cors/cors.framework.ts new file mode 100644 index 00000000..005b2ec6 --- /dev/null +++ b/src/frameworks/cors/cors.framework.ts @@ -0,0 +1,203 @@ +//#region Imports + +import { IncomingMessage, ServerResponse } from 'http'; +import cors, { CorsOptions } from 'cors'; +import { FrameworkContract } from '../../contracts'; +import { getDefaultIfUndefined } from '../../core'; + +//#endregion + +/** + * The options to customize {@link CorsFramework} + * + * @breadcrumb Frameworks / CorsFramework + * @public + */ +export type CorsFrameworkOptions = CorsOptions & { + /** + * Send error 403 when cors is invalid. From what I read in `cors`, `fastify/cors` and [this problem](https://stackoverflow.com/questions/57212248/why-is-http-request-been-processed-in-action-even-when-cors-is-not-enabled) + * it is normal to process the request even if the origin is invalid. + * So this option will respond with error if this method was called from an invalid origin (or not allowed method) like [access control lib](https://github.com/primus/access-control/blob/master/index.js#L95-L115) . + * + * @defaultValue true + */ + forbiddenOnInvalidOriginOrMethod?: boolean; +}; + +/** + * The framework that handles cors for your api without relying on internals of the framework + * + * @example + * ```typescript + * import express from 'express'; + * import { ServerlessAdapter } from '@h4ad/serverless-adapter'; + * import { ExpressFramework } from '@h4ad/serverless-adapter/lib/frameworks/express'; + * import { CorsFramework } from '@h4ad/serverless-adapter/lib/frameworks/cors'; + * + * const expressFramework = new ExpressFramework(); + * const options: CorsOptions = {}; // customize the options + * const framework = new CorsFramework(expressFramework, options); + * + * export const handler = ServerlessAdapter.new(null) + * .setFramework(framework) + * // set other configurations and then build + * .build(); + * ``` + * + * @breadcrumb Frameworks / CorsFramework + * @public + */ +export class CorsFramework implements FrameworkContract { + //#region Constructor + + /** + * Default Constructor + */ + constructor( + protected readonly framework: FrameworkContract, + protected readonly options?: CorsFrameworkOptions, + ) { + this.cachedCorsInstance = cors(this.options); + } + + //#endregion + + /** + * All cors headers that can be added by cors package + */ + protected readonly corsHeaders: string[] = [ + 'Access-Control-Max-Age', + 'Access-Control-Expose-Headers', + 'Access-Control-Allow-Headers', + 'Access-Control-Request-Headers', + 'Access-Control-Allow-Credentials', + 'Access-Control-Allow-Methods', + 'Access-Control-Allow-Origin', + ]; + + /** + * The cached instance of cors + */ + protected readonly cachedCorsInstance: ReturnType; + + //#region Public Methods + + /** + * {@inheritDoc} + */ + public sendRequest( + app: TApp, + request: IncomingMessage, + response: ServerResponse, + ): void { + this.cachedCorsInstance( + request, + response, + this.onCorsNext(app, request, response), + ); + } + + //#endregion + + //#region Protected Methods + + /** + * Handle next execution called by the cors package + */ + protected onCorsNext( + app: TApp, + request: IncomingMessage, + response: ServerResponse, + ): () => void { + return () => { + this.formatHeaderValuesAddedByCorsPackage(response); + + const errorOnInvalidOrigin = getDefaultIfUndefined( + this.options?.forbiddenOnInvalidOriginOrMethod, + true, + ); + + if (errorOnInvalidOrigin) { + const allowedOrigin = response.getHeader('access-control-allow-origin'); + const isInvalidOrigin = this.isInvalidOriginOrMethodIsNotAllowed( + request, + allowedOrigin, + ); + + if (isInvalidOrigin) { + response.statusCode = 403; + response.setHeader('Content-Type', 'text/plain'); + response.end( + [ + 'Invalid HTTP Access Control (CORS) request:', + ` Origin: ${request.headers.origin}`, + ` Method: ${request.method}`, + ].join('\n'), + ); + + return; + } + } + + this.framework.sendRequest(app, request, response); + }; + } + + /** + * Format the headers to be standardized with the rest of the library, such as ApiGatewayV2. + * Also, some frameworks don't support headers as an array, so we need to format the values. + */ + protected formatHeaderValuesAddedByCorsPackage( + response: ServerResponse, + ): void { + for (const corsHeader of this.corsHeaders) { + const value = response.getHeader(corsHeader); + + if (value === undefined) continue; + + response.removeHeader(corsHeader); + response.setHeader( + corsHeader.toLowerCase(), + Array.isArray(value) ? value.join(',') : value, + ); + } + } + + /** + * Check if the origin is invalid or if the method is not allowed. + * Highly inspired by [access-control](https://github.com/primus/access-control/blob/master/index.js#L95-L115) + */ + protected isInvalidOriginOrMethodIsNotAllowed( + request: IncomingMessage, + allowedOrigin: number | string | string[] | undefined, + ): boolean { + if (!allowedOrigin) return true; + + if ( + !!request.headers.origin && + allowedOrigin !== '*' && + request.headers.origin !== allowedOrigin + ) + return true; + + const notPermitedInMethods = + this.options && + Array.isArray(this.options.methods) && + this.options.methods.every( + m => m.toLowerCase() !== request.method?.toLowerCase(), + ); + const differentMethod = + this.options && + typeof this.options.methods === 'string' && + this.options.methods + .split(',') + .every(m => m.trim().toLowerCase() !== request.method?.toLowerCase()); + + if (this.options?.methods && (notPermitedInMethods || differentMethod)) + return true; + + return false; + } + + //#endregion +} diff --git a/src/frameworks/cors/index.ts b/src/frameworks/cors/index.ts new file mode 100644 index 00000000..37ce1c1f --- /dev/null +++ b/src/frameworks/cors/index.ts @@ -0,0 +1 @@ +export * from './cors.framework'; diff --git a/test/frameworks/cors.framework.spec.ts b/test/frameworks/cors.framework.spec.ts new file mode 100644 index 00000000..2786d4fc --- /dev/null +++ b/test/frameworks/cors.framework.spec.ts @@ -0,0 +1,288 @@ +import express from 'express'; +import fastify from 'fastify'; +import Application from 'koa'; +import * as trpc from '@trpc/server'; +import { CorsFramework, CorsFrameworkOptions } from '../../src/frameworks/cors'; +import { + BothValueHeaders, + FrameworkContract, + ServerlessRequest, + ServerlessResponse, + waitForStreamComplete, +} from '../../src'; +import { ExpressFramework } from '../../src/frameworks/express'; +import { FastifyFramework } from '../../src/frameworks/fastify'; +import { KoaFramework } from '../../src/frameworks/koa'; +import { TrpcFramework } from '../../src/frameworks/trpc'; +import SpyInstance = jest.SpyInstance; + +type CorsTest = { + name: string; + method: string; + origin: string; + options: CorsFrameworkOptions; + expectedHeaders: BothValueHeaders; + expectSendRequestOfTheFrameworkToBeCalled: boolean; +}; + +const AllowOrigin = 'access-control-allow-origin'; +const AllowCredentials = 'access-control-allow-credentials'; +const AllowMethods = 'access-control-allow-methods'; +const MaxAge = 'access-control-max-age'; +const AllowHeaders = 'access-control-allow-headers'; + +const corsOptions: CorsTest[] = [ + { + name: 'allow all origins', + method: 'get', + origin: 'http://localhost:3000', + options: { origin: '*' }, + expectedHeaders: { [AllowOrigin]: '*' }, + expectSendRequestOfTheFrameworkToBeCalled: true, + }, + { + name: 'allow localhost origin (string)', + method: 'get', + origin: 'http://localhost:3000', + options: { origin: 'http://localhost:3000' }, + expectedHeaders: { [AllowOrigin]: 'http://localhost:3000', vary: 'Origin' }, + expectSendRequestOfTheFrameworkToBeCalled: true, + }, + { + name: 'allow localhost origin (array)', + method: 'get', + origin: 'http://localhost:3000', + options: { origin: ['http://localhost:3000', 'http://google.com'] }, + expectedHeaders: { [AllowOrigin]: 'http://localhost:3000', vary: 'Origin' }, + expectSendRequestOfTheFrameworkToBeCalled: true, + }, + { + name: 'do not send request on options', + method: 'options', + origin: 'http://localhost:3000', + options: { origin: '*' }, + expectedHeaders: { + [AllowOrigin]: '*', + [AllowMethods]: 'GET,HEAD,PUT,PATCH,POST,DELETE', + vary: 'Access-Control-Request-Headers', + }, + expectSendRequestOfTheFrameworkToBeCalled: false, + }, + { + name: 'do not send request when origin sent is wrong (string)', + method: 'get', + origin: 'http://localhost:3000', + options: { origin: 'http://example.com:3000' }, + expectedHeaders: { [AllowOrigin]: 'http://example.com:3000' }, + expectSendRequestOfTheFrameworkToBeCalled: false, + }, + { + name: 'do not send request when origin sent is wrong (array)', + method: 'get', + origin: 'http://localhost:3000', + options: { origin: ['http://example.com:3000', 'http://google.com'] }, + expectedHeaders: { + vary: 'Origin', + }, + expectSendRequestOfTheFrameworkToBeCalled: false, + }, + { + name: 'do not send request when method sent is wrong (string)', + method: 'get', + origin: 'http://localhost:3000', + options: { origin: '*', methods: 'post' }, + expectedHeaders: { [AllowOrigin]: '*' }, + expectSendRequestOfTheFrameworkToBeCalled: false, + }, + { + name: 'do not send request when method sent is wrong (array)', + method: 'get', + origin: 'http://localhost:3000', + options: { origin: '*', methods: ['post'] }, + expectedHeaders: { [AllowOrigin]: '*' }, + expectSendRequestOfTheFrameworkToBeCalled: false, + }, + { + name: 'force process request when origin is wrong', + method: 'get', + origin: 'http://localhost:3000', + options: { + origin: 'http://example.com', + methods: ['post'], + forbiddenOnInvalidOriginOrMethod: false, + }, + expectedHeaders: { [AllowOrigin]: 'http://example.com' }, + expectSendRequestOfTheFrameworkToBeCalled: true, + }, + { + name: 'when has credentials', + method: 'options', + origin: 'http://localhost:3000', + options: { credentials: true }, + expectedHeaders: { + [AllowCredentials]: 'true', + }, + expectSendRequestOfTheFrameworkToBeCalled: false, + }, + { + name: 'when preflight continue is true', + method: 'options', + origin: 'http://localhost:3000', + options: { origin: '*', preflightContinue: true }, + expectedHeaders: { + [AllowOrigin]: '*', + }, + expectSendRequestOfTheFrameworkToBeCalled: true, + }, + { + name: 'when allowed headers is sent', + method: 'options', + origin: 'http://localhost:3000', + options: { allowedHeaders: ['x-test'] }, + expectedHeaders: { + [AllowHeaders]: 'x-test', + }, + expectSendRequestOfTheFrameworkToBeCalled: false, + }, + { + name: 'when max-age is set', + method: 'options', + origin: 'http://localhost:3000', + options: { maxAge: 60 }, + expectedHeaders: { + [MaxAge]: '60', + }, + expectSendRequestOfTheFrameworkToBeCalled: false, + }, +]; + +function createFramework( + options: CorsFrameworkOptions, + instance: FrameworkContract, +): [ + FrameworkContract, + SpyInstance['sendRequest']>>, +] { + const spy = jest.spyOn(instance, 'sendRequest'); + + return [new CorsFramework(instance, options), spy]; +} + +function createRequest(method: string, origin: string): ServerlessRequest { + return new ServerlessRequest({ + method, + url: '/', + headers: { + origin: origin, + }, + }); +} + +function createResponse(method: string): ServerlessResponse { + return new ServerlessResponse({ + method, + }); +} + +async function handleRestExpects( + app: TApp, + framework: FrameworkContract, + corsTest: CorsTest, +): Promise { + const [corsFramework, spySendRequest] = createFramework( + corsTest.options, + framework, + ); + + const request = createRequest(corsTest.method, corsTest.origin); + const response = createResponse(corsTest.method); + + corsFramework.sendRequest(app, request, response); + + await waitForStreamComplete(response); + + const headers = response.getHeaders(); + + for (const expectHeader in corsTest.expectedHeaders) { + expect(headers).toHaveProperty( + expectHeader, + corsTest.expectedHeaders[expectHeader], + ); + } + + if (corsTest.expectSendRequestOfTheFrameworkToBeCalled) + expect(spySendRequest).toHaveBeenCalled(); + else expect(spySendRequest).not.toHaveBeenCalled(); +} + +describe(CorsFramework.name, () => { + describe('express', () => { + for (const corsTest of corsOptions) { + it(`${corsTest.method}: ${corsTest.name}`, async () => { + const app = express(); + + app.get('/', (_, res) => res.json('ok')); + + await handleRestExpects(app, new ExpressFramework(), corsTest); + }); + } + }); + + describe('fastify', () => { + for (const corsTest of corsOptions) { + it(`${corsTest.method}: ${corsTest.name}`, async () => { + const app = fastify(); + + app.get('/', (_, res) => { + res.send('ok'); + }); + + await handleRestExpects(app, new FastifyFramework(), corsTest); + }); + } + }); + + describe('koa', () => { + for (const corsTest of corsOptions) { + it(`${corsTest.method}: ${corsTest.name}`, async () => { + const app = new Application(); + + app.use(ctx => { + ctx.status = 200; + ctx.body = 'ok'; + }); + + await handleRestExpects(app, new KoaFramework(), corsTest); + }); + } + }); + + describe('hapi', () => { + for (const corsTest of corsOptions) { + it(`${corsTest.method}: ${corsTest.name}`, async () => { + const app = new Application(); + + app.use(ctx => { + ctx.status = 200; + ctx.body = 'ok'; + }); + + await handleRestExpects(app, new KoaFramework(), corsTest); + }); + } + }); + + describe('trpc', () => { + for (const corsTest of corsOptions) { + it(`${corsTest.method}: ${corsTest.name}`, async () => { + const app = trpc.router(); + + app.query('/', { + resolve: () => 'ok', + }); + + await handleRestExpects(app, new TrpcFramework(), corsTest); + }); + } + }); +}); From 2ec822ef402dfb3566cf2146de6342f7b472dcd2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vin=C3=ADcius=20Louren=C3=A7o?= Date: Sat, 19 Nov 2022 16:26:26 -0300 Subject: [PATCH 5/8] docs(lazy): added missing import and removed unused import --- www/docs/main/frameworks/lazy.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/www/docs/main/frameworks/lazy.mdx b/www/docs/main/frameworks/lazy.mdx index 02dff66a..310f5bf8 100644 --- a/www/docs/main/frameworks/lazy.mdx +++ b/www/docs/main/frameworks/lazy.mdx @@ -18,7 +18,7 @@ constructor( The first parameter is the instance of another framework, so if you want to use [Express](./express) for example, you can just use like this: ```ts -import { NestFactory } from '@nestjs/core'; +import { ServerlessAdapter } from '@h4ad/serverless-adapter'; import { LazyFramework } from '@h4ad/serverless-adapter/lib/frameworks/lazy'; import { ExpressFramework } from '@h4ad/serverless-adapter/lib/frameworks/express'; From 01d1a35cb732e7960cdf0e25fbb7dbca0eba41d6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vin=C3=ADcius=20Louren=C3=A7o?= Date: Sat, 19 Nov 2022 16:43:22 -0300 Subject: [PATCH 6/8] docs(cors): added docs about cors --- src/index.doc.ts | 1 + www/docs/main/frameworks/cors.mdx | 71 ++++++++++++++++++++++++++++ www/docs/main/frameworks/deepkit.mdx | 6 +++ www/docs/main/frameworks/express.mdx | 6 +++ www/docs/main/frameworks/fastify.mdx | 6 +++ www/docs/main/frameworks/hapi.mdx | 5 ++ www/docs/main/frameworks/koa.mdx | 4 ++ www/docs/main/frameworks/lazy.mdx | 5 ++ www/docs/main/frameworks/nestjs.mdx | 5 ++ www/docs/main/frameworks/trpc.mdx | 13 ++++- www/docs/main/intro.mdx | 10 +++- www/sidebars.js | 1 + 12 files changed, 129 insertions(+), 4 deletions(-) create mode 100644 www/docs/main/frameworks/cors.mdx diff --git a/src/index.doc.ts b/src/index.doc.ts index c9cc4517..af9c2e68 100644 --- a/src/index.doc.ts +++ b/src/index.doc.ts @@ -7,6 +7,7 @@ export * from './adapters/dummy'; export * from './adapters/huawei'; export * from './contracts'; export * from './core'; +export * from './frameworks/cors'; export * from './frameworks/deepkit'; export * from './frameworks/express'; export * from './frameworks/fastify'; diff --git a/www/docs/main/frameworks/cors.mdx b/www/docs/main/frameworks/cors.mdx new file mode 100644 index 00000000..f4a854e9 --- /dev/null +++ b/www/docs/main/frameworks/cors.mdx @@ -0,0 +1,71 @@ +--- +title: CORS +description: See more about how to deal with CORS in serverless. +--- + +This framework is a helper framework that wraps around another framework and gets some options to configure CORS in your app using the [cors](https://github.com/expressjs/cors) library behind the scenes. + +## Why? + +Why should I use this framework instead of directly using my framework/application's `joe-doe-cors` package? + +Using this framework, you can skip the `request` to your framework, so it's faster to handle it directly in the library instead of leaving it +to your frame. + +Also, frameworks like `trpc`, `deepḱit` and other frameworks don't have a good way of dealing with CORS, so with `CorsFramework` you can do the CORS +works for these frameworks. + +Finally, I added an optimization, inspired by [access control](https://github.com/primus/access-control/blob/master/index.js#L95-L115), which returns the Forbidden error when the origin is invalid +or when the method is not allowed. In other packages, like `fastify/cors`, the `cors` itself used by this library and [even other languages](https://stackoverflow.com/questions/57212248/why-is-http-request-been-processed-in-action-even-when-cors-is-not-enabled), +they process the request if the origin is invalid and to me it sounds like a waste of resources, so we can just return the error to the user instead of processing the request which is sure to return the error in the user's browser. + +> If you want to disable this behaviour, set `{ forbiddenOnInvalidOriginOrMethod: false }` in the options. + +## Requirements + +To be able to use, first you need to install some packages: + +```bash +npm i --save cors +``` + +If you are using TypeScript: + +```bash +npm i --save-dev @types/cors +``` + +## Usage + +The first parameter is the instance of another framework, so if you want to use [Express](./express) for example, you can just use like this: + +```ts +import { ServerlessAdapter } from '@h4ad/serverless-adapter'; +import { CorsFramework, CorsFrameworkOptions } from '@h4ad/serverless-adapter/lib/frameworks/cors'; +import { ExpressFramework } from '@h4ad/serverless-adapter/lib/frameworks/express'; + +const express = require('express'); + +const expressFramework = new ExpressFramework(); +// see docs about the options in the original package: +// https://github.com/expressjs/cors +const options: CorsFrameworkOptions = { origin: '*' }; +const framework = new CorsFramework(expressFramework, options); + +export const handler = ServerlessAdapter.new(express) + .setFramework(framework) + // continue to set the other options here. + //.setHandler(new DefaultHandler()) + //.setResolver(new PromiseResolver()) + //.addAdapter(new AlbAdapter()) + //.addAdapter(new SQSAdapter()) + //.addAdapter(new SNSAdapter()) + // after put all methods necessary, just call the build method. + .build(); +``` + +:::tip + +Is your application instance creation asynchronous? Look the [LazyFramework](./lazy) which helps you in asynchronous startup. + +::: diff --git a/www/docs/main/frameworks/deepkit.mdx b/www/docs/main/frameworks/deepkit.mdx index 4de5a820..62b5be3d 100644 --- a/www/docs/main/frameworks/deepkit.mdx +++ b/www/docs/main/frameworks/deepkit.mdx @@ -91,3 +91,9 @@ new FrameworkModule({ debug: true, httpLog: true }), new HttpModule({ debug: false }), new FrameworkModule({ debug: false, httpLog: false }), ``` + +:::tip + +Need to deal with CORS? See [CorsFramework](./cors) which helps you to add correct headers. + +::: diff --git a/www/docs/main/frameworks/express.mdx b/www/docs/main/frameworks/express.mdx index f5375bf5..37d785a2 100644 --- a/www/docs/main/frameworks/express.mdx +++ b/www/docs/main/frameworks/express.mdx @@ -36,3 +36,9 @@ export const handler = ServerlessAdapter.new(app) Is your application instance creation asynchronous? Look the [LazyFramework](./lazy) which helps you in asynchronous startup. ::: + +:::tip + +Need to deal with CORS? See [CorsFramework](./cors) which helps you to add correct headers. + +::: diff --git a/www/docs/main/frameworks/fastify.mdx b/www/docs/main/frameworks/fastify.mdx index 365e9d92..8fdad0ea 100644 --- a/www/docs/main/frameworks/fastify.mdx +++ b/www/docs/main/frameworks/fastify.mdx @@ -36,3 +36,9 @@ Is your application instance creation asynchronous? Look the [LazyFramework](./l ::: +:::tip + +Need to deal with CORS? See [CorsFramework](./cors) which helps you to add correct headers. + +::: + diff --git a/www/docs/main/frameworks/hapi.mdx b/www/docs/main/frameworks/hapi.mdx index bd041252..f2af0fa7 100644 --- a/www/docs/main/frameworks/hapi.mdx +++ b/www/docs/main/frameworks/hapi.mdx @@ -37,3 +37,8 @@ Is your application instance creation asynchronous? Look the [LazyFramework](./l ::: +:::tip + +Need to deal with CORS? See [CorsFramework](./cors) which helps you to add correct headers. + +::: diff --git a/www/docs/main/frameworks/koa.mdx b/www/docs/main/frameworks/koa.mdx index d6b36b1e..24d476b4 100644 --- a/www/docs/main/frameworks/koa.mdx +++ b/www/docs/main/frameworks/koa.mdx @@ -37,4 +37,8 @@ Is your application instance creation asynchronous? Look the [LazyFramework](./l ::: +:::tip + +Need to deal with CORS? See [CorsFramework](./cors) which helps you to add correct headers. +::: diff --git a/www/docs/main/frameworks/lazy.mdx b/www/docs/main/frameworks/lazy.mdx index 310f5bf8..83366507 100644 --- a/www/docs/main/frameworks/lazy.mdx +++ b/www/docs/main/frameworks/lazy.mdx @@ -57,4 +57,9 @@ The solution above is inspired by [top-level await](https://aws.amazon.com/pt/bl ::: +:::tip + +Need to deal with CORS? See [CorsFramework](./cors) which helps you to add correct headers. + +::: diff --git a/www/docs/main/frameworks/nestjs.mdx b/www/docs/main/frameworks/nestjs.mdx index 4637f8f5..49446a09 100644 --- a/www/docs/main/frameworks/nestjs.mdx +++ b/www/docs/main/frameworks/nestjs.mdx @@ -105,3 +105,8 @@ export const handler = ServerlessAdapter.new(null) +:::tip + +Need to deal with CORS? See [CorsFramework](./cors) which helps you to add correct headers. + +::: diff --git a/www/docs/main/frameworks/trpc.mdx b/www/docs/main/frameworks/trpc.mdx index 0c3aa40e..c7f7d901 100644 --- a/www/docs/main/frameworks/trpc.mdx +++ b/www/docs/main/frameworks/trpc.mdx @@ -21,6 +21,7 @@ Then, you just need to use the [TrpcFramework](../../api/Frameworks/TrpcFramewor import * as trpc from '@trpc/server'; import { ServerlessAdapter } from '@h4ad/serverless-adapter'; import { TrpcFramework, TrpcAdapterContext, TrpcFrameworkOptions, BufferToJSObjectTransformer } from '@h4ad/serverless-adapter/lib/frameworks/trpc'; +import { CorsFramework } from '@h4ad/serverless-adapter/lib/frameworks/cors'; import { z } from 'zod'; type CustomContext = { currentDate: Date }; @@ -52,9 +53,10 @@ const frameworkOptions: TrpcFrameworkOptions = { }; const framework = new TrpcFramework(frameworkOptions); +const corsFramework = new CorsFramework(framework); // see more about: https://serverless-adapter.viniciusl.com.br/docs/main/frameworks/cors export const handler = ServerlessAdapter.new(appRouter) - .setFramework(framework) + .setFramework(corsFramework) // continue to set the other options here. //.setHandler(new DefaultHandler()) //.setResolver(new PromiseResolver()) @@ -165,9 +167,10 @@ const frameworkOptions: TrpcFrameworkOptions = { }; const framework = new TrpcFramework(frameworkOptions); +const corsFramework = new CorsFramework(framework); // see more about: https://serverless-adapter.viniciusl.com.br/docs/main/frameworks/cors export const handler = ServerlessAdapter.new(appRouter) - .setFramework(framework) + .setFramework(corsFramework) // continue to set the other options here. //.setHandler(new DefaultHandler()) //.setResolver(new PromiseResolver()) @@ -183,3 +186,9 @@ export const handler = ServerlessAdapter.new(appRouter) Is your application instance creation asynchronous? Look the [LazyFramework](./lazy) which helps you in asynchronous startup. ::: + +:::tip + +Need to deal with CORS? See [CorsFramework](./cors) which helps you to add correct headers. + +::: diff --git a/www/docs/main/intro.mdx b/www/docs/main/intro.mdx index fd918f41..6535f640 100644 --- a/www/docs/main/intro.mdx +++ b/www/docs/main/intro.mdx @@ -37,11 +37,17 @@ Currently, we support these frameworks: - [Fastify](https://www.fastify.io/) by using ([FastifyFramework](./frameworks/fastify)) - [Hapi](https://hapi.dev/) by using ([HapiFramework](./frameworks/hapi)) - [Koa](https://koajs.com/) by using ([KoaFramework](./frameworks/koa)) +- [NestJS](https://nestjs.com/), see the [documentation about](./frameworks/nestjs) to know how to integrate. +- [tRPC](https://trpc.io/), see the [documentation about](./frameworks/trpc) to know how to integrate. + +Additionally, we have some helper frameworks that you can combine with the above frameworks to give more power to your application: + - Async Initialization by using ([LazyFramework](./frameworks/lazy)) - Use this framework to provide a way to create the instance of your app asynchronously. - With him, you can create an instance of Express or Fastify asynchronously, [see the docs](./frameworks/lazy). -- [NestJS](https://nestjs.com/), see the [documentation about](./frameworks/nestjs) to know how to integrate. -- [tRPC](https://trpc.io/), see the [documentation about](./frameworks/trpc) to know how to integrate. +- CORS support without rely on original framework with ([CorsFramework](./frameworks/cors)) + - Use this framework to provide a way to handle CORS in frameworks like `trpc`. + - Or use to get faster responses when method is `OPTIONS`. We support these event sources: diff --git a/www/sidebars.js b/www/sidebars.js index b1d8ba6f..1692d653 100644 --- a/www/sidebars.js +++ b/www/sidebars.js @@ -134,6 +134,7 @@ const sidebars = { }, collapsed: true, items: [ + 'main/frameworks/cors', 'main/frameworks/deepkit', 'main/frameworks/express', 'main/frameworks/fastify', From 9e450dfbd3f15a36273c76c1f45df68281ad6f81 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vin=C3=ADcius=20Louren=C3=A7o?= Date: Sat, 19 Nov 2022 17:05:29 -0300 Subject: [PATCH 7/8] ci(pr & release): fixed install latest npm on nodejs and fixed nodejs version in release --- .github/workflows/pr.yml | 11 ++++++++--- .github/workflows/release.yml | 2 +- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index 2d82bc45..dd668a34 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -18,8 +18,13 @@ jobs: with: node-version: ${{ matrix.node-version }} + - name: Update NPM Version On NodeJS 12 + run: npm i -g npm@8.x + if: matrix.node-version == '12.x' + - name: Update NPM Version run: npm i -g npm + if: matrix.node-version != '12.x' - name: Cache dependencies uses: actions/cache@v2 @@ -55,7 +60,7 @@ jobs: steps: - uses: actions/checkout@v3 - - name: Use Node.js 14 + - name: Use Node.js 16 uses: actions/setup-node@v3 with: node-version: '16.x' @@ -64,9 +69,9 @@ jobs: uses: actions/cache@v2 with: path: ~/.npm - key: ${{ runner.os }}-node-14.x-${{ hashFiles('**/package-lock.json') }} + key: ${{ runner.os }}-node-16.x-${{ hashFiles('**/package-lock.json') }} restore-keys: | - ${{ runner.os }}-node-14.x- + ${{ runner.os }}-node-16.x- - name: Install Lib Dependencies run: npm ci diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index cdc0f503..051ab1ee 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -15,7 +15,7 @@ jobs: - name: Setup Node.js uses: actions/setup-node@v2 with: - node-version: lts/* + node-version: lts/18 - name: Install dependencies run: npm ci - name: Test From 46acb15001e385c87060724f77567cee8eb7a93a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vin=C3=ADcius=20Louren=C3=A7o?= Date: Sat, 19 Nov 2022 18:14:39 -0300 Subject: [PATCH 8/8] test(fastify & cors): added missing mock fn in nodejs 12.x for cors framework tests --- test/frameworks/cors.framework.spec.ts | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/test/frameworks/cors.framework.spec.ts b/test/frameworks/cors.framework.spec.ts index 2786d4fc..0c246af8 100644 --- a/test/frameworks/cors.framework.spec.ts +++ b/test/frameworks/cors.framework.spec.ts @@ -16,6 +16,22 @@ import { KoaFramework } from '../../src/frameworks/koa'; import { TrpcFramework } from '../../src/frameworks/trpc'; import SpyInstance = jest.SpyInstance; +jest.mock('fastify', () => { + const packages = { + '12.x': 'fastify-3', + latest: 'fastify', + }; + const version = process.env.TEST_NODE_VERSION || 'latest'; + + // Require the original module. + const originalModule = jest.requireActual(packages[version]); + + return { + __esModule: true, + ...originalModule, + }; +}); + type CorsTest = { name: string; method: string;