├── etc └── .gitkeep ├── .husky ├── .gitignore ├── pre-commit └── prepare-commit-msg ├── www ├── static │ ├── .nojekyll │ ├── CNAME │ └── img │ │ ├── favicon.ico │ │ ├── docusaurus.png │ │ └── tutorial │ │ ├── localeDropdown.png │ │ └── docsVersionDropdown.png ├── docs │ ├── .gitignore │ └── main │ │ ├── adapters │ │ ├── firebase.mdx │ │ ├── aws │ │ │ └── function-url.mdx │ │ └── huawei │ │ │ └── huawei-api-gateway.mdx │ │ ├── getting-started │ │ ├── examples.mdx │ │ ├── installation.mdx │ │ └── usage.mdx │ │ ├── frameworks │ │ ├── koa.mdx │ │ ├── polka.mdx │ │ ├── hapi.mdx │ │ ├── fastify.mdx │ │ ├── express.mdx │ │ └── helpers │ │ │ └── lazy.mdx │ │ ├── resolvers │ │ ├── promise.mdx │ │ ├── callback.mdx │ │ └── aws-context.mdx │ │ └── handlers │ │ ├── gcp.mdx │ │ └── firebase.mdx ├── .tool-versions ├── babel.config.js ├── blog │ ├── first-time-meme-first-time.gif │ └── authors.yml ├── src │ ├── components │ │ ├── HomepageFeatures │ │ │ ├── styles.module.css │ │ │ └── index.tsx │ │ ├── HowToStart │ │ │ ├── styles.module.css │ │ │ └── index.tsx │ │ └── BrowserWindow │ │ │ ├── index.tsx │ │ │ └── styles.module.css │ ├── pages │ │ ├── index.module.css │ │ └── index.tsx │ └── css │ │ └── custom.css ├── tsconfig.json ├── README.md ├── .gitignore └── package.json ├── .tool-versions ├── test ├── issues │ └── alb-express-static │ │ ├── robots.txt │ │ └── alb-express-static.spec.ts ├── handlers │ ├── bitcoin.pdf │ ├── gcp.handler.spec.ts │ └── digital-ocean.handler.spec.ts ├── frameworks │ ├── body-parser.framework.spec.ts │ ├── body-parser-v2.framework.spec.ts │ ├── koa.framework.spec.ts │ ├── fastify.framework.spec.ts │ ├── fastify-v5.framework.spec.ts │ ├── express-v5.framework.spec.ts │ ├── express.framework.spec.ts │ ├── polka.framework.spec.ts │ ├── hapi.framework.spec.ts │ └── http-deepkit.framework.spec.ts ├── adapters │ ├── azure │ │ └── utils │ │ │ └── events.ts │ ├── utils │ │ ├── events.ts │ │ └── can-handle.ts │ ├── huawei │ │ └── utils │ │ │ ├── events.ts │ │ │ └── huawei-api-gateway.ts │ ├── digital-ocean │ │ └── utils │ │ │ ├── event.ts │ │ │ └── http-function.ts │ ├── aws │ │ ├── utils │ │ │ ├── sqs.ts │ │ │ ├── event-bridge.ts │ │ │ ├── s3.ts │ │ │ ├── sns.ts │ │ │ ├── dynamodb.ts │ │ │ └── api-gateway-v2.ts │ │ ├── s3.adapter.spec.ts │ │ ├── sns.adapter.spec.ts │ │ ├── sqs.adapter.spec.ts │ │ └── dynamodb.adapter.spec.ts │ ├── test.example │ └── dummy │ │ └── dummy.adapter.spec.ts ├── core │ ├── no-op.spec.ts │ ├── current-invoke.spec.ts │ ├── event-body.spec.ts │ ├── optional.spec.ts │ ├── utils │ │ └── stream.ts │ └── stream.spec.ts ├── resolvers │ └── dummy.resolver.spec.ts ├── mocks │ └── framework.mock.ts └── network │ └── request.spec.ts ├── .eslintignore ├── .release-please-manifest.json ├── src ├── handlers │ ├── base │ │ └── index.ts │ ├── gcp │ │ ├── index.ts │ │ └── gcp.handler.ts │ ├── aws │ │ └── index.ts │ ├── azure │ │ └── index.ts │ ├── default │ │ └── index.ts │ ├── huawei │ │ └── index.ts │ ├── digital-ocean │ │ ├── index.ts │ │ └── digital-ocean.handler.ts │ └── firebase │ │ ├── index.ts │ │ ├── http-firebase.handler.ts │ │ └── http-firebase-v2.handler.ts ├── adapters │ ├── dummy │ │ ├── index.ts │ │ └── dummy.adapter.ts │ ├── aws │ │ ├── base │ │ │ └── index.ts │ │ ├── index.ts │ │ ├── s3.adapter.ts │ │ ├── sns.adapter.ts │ │ └── sqs.adapter.ts │ ├── azure │ │ └── index.ts │ ├── huawei │ │ └── index.ts │ ├── digital-ocean │ │ └── index.ts │ └── apollo-server │ │ └── index.ts ├── frameworks │ ├── cors │ │ └── index.ts │ ├── hapi │ │ ├── index.ts │ │ └── hapi.framework.ts │ ├── koa │ │ ├── index.ts │ │ └── koa.framework.ts │ ├── lazy │ │ └── index.ts │ ├── polka │ │ ├── index.ts │ │ └── polka.framework.ts │ ├── trpc │ │ └── index.ts │ ├── express │ │ ├── index.ts │ │ └── express.framework.ts │ ├── fastify │ │ ├── index.ts │ │ ├── helpers │ │ │ └── no-op-content-parser.ts │ │ └── fastify.framework.ts │ ├── deepkit │ │ ├── index.ts │ │ └── http-deepkit.framework.ts │ ├── apollo-server │ │ └── index.ts │ └── body-parser │ │ ├── index.ts │ │ ├── text-body-parser.framework.ts │ │ ├── json-body-parser.framework.ts │ │ ├── raw-body-parser.framework.ts │ │ └── urlencoded-body-parser.framework.ts ├── resolvers │ ├── dummy │ │ ├── index.ts │ │ └── dummy.resolver.ts │ ├── callback │ │ ├── index.ts │ │ └── callback.resolver.ts │ ├── promise │ │ ├── index.ts │ │ └── promise.resolver.ts │ └── aws-context │ │ ├── index.ts │ │ └── aws-context.resolver.ts ├── @types │ ├── index.ts │ ├── digital-ocean │ │ ├── index.ts │ │ ├── digital-ocean-http-response.ts │ │ └── digital-ocean-http-event.ts │ ├── huawei │ │ ├── index.ts │ │ └── huawei-api-gateway-response.ts │ ├── helpers.ts │ ├── headers.ts │ └── binary-settings.ts ├── network │ ├── index.ts │ ├── utils.ts │ └── request.ts ├── index.ts ├── contracts │ ├── index.ts │ ├── framework.contract.ts │ └── handler.contract.ts ├── core │ ├── no-op.ts │ ├── index.ts │ ├── optional.ts │ ├── event-body.ts │ ├── constants.ts │ ├── stream.ts │ └── current-invoke.ts └── index.doc.ts ├── benchmark ├── .gitignore ├── .swcrc ├── tsconfig.json ├── README.md ├── package.json └── src │ ├── samples │ ├── clone-headers.ts │ ├── compare-libraries.ts │ └── format-headers.ts │ └── framework.mock.ts ├── .env ├── tsconfig.build.json ├── .npmrc ├── .gitattributes ├── .prettierrc ├── .github ├── ISSUE_TEMPLATE │ ├── config.yml │ ├── bug_report.md │ └── feature_request.md ├── dependabot.yml ├── settings.yml ├── workflows │ ├── release.yml │ ├── docs.yml │ ├── codeql-analysis.yml │ └── pr.yml └── PULL_REQUEST_TEMPLATE.md ├── .vscode ├── settings.json └── launch.json ├── tsconfig.doc.json ├── tsdoc.json ├── vite.config.ts ├── tsconfig.eslint.json ├── scripts ├── parse-docs.ts ├── generate-markdown.ts └── libs │ ├── CustomUtilities.ts │ └── MarkdownEmitter.ts ├── LICENSE ├── tsconfig.json ├── release-please-config.json ├── .tmuxinator.yml ├── tsup.config.ts └── .gitignore /etc/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.husky/.gitignore: -------------------------------------------------------------------------------- 1 | _ 2 | -------------------------------------------------------------------------------- /www/static/.nojekyll: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.tool-versions: -------------------------------------------------------------------------------- 1 | nodejs 18.18.1 2 | -------------------------------------------------------------------------------- /www/docs/.gitignore: -------------------------------------------------------------------------------- 1 | /api/* 2 | -------------------------------------------------------------------------------- /www/.tool-versions: -------------------------------------------------------------------------------- 1 | nodejs 18.18.1 2 | -------------------------------------------------------------------------------- /test/issues/alb-express-static/robots.txt: -------------------------------------------------------------------------------- 1 | test 2 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | src/types/global.d.ts 2 | benchmark/ 3 | -------------------------------------------------------------------------------- /www/static/CNAME: -------------------------------------------------------------------------------- 1 | serverless-adapter.viniciusl.com.br 2 | -------------------------------------------------------------------------------- /.release-please-manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | ".": "4.4.0" 3 | } 4 | -------------------------------------------------------------------------------- /src/handlers/base/index.ts: -------------------------------------------------------------------------------- 1 | export * from './raw-request'; 2 | -------------------------------------------------------------------------------- /src/handlers/gcp/index.ts: -------------------------------------------------------------------------------- 1 | export * from './gcp.handler'; 2 | -------------------------------------------------------------------------------- /benchmark/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | *.cpuprofile 3 | .clinic 4 | -------------------------------------------------------------------------------- /src/adapters/dummy/index.ts: -------------------------------------------------------------------------------- 1 | export * from './dummy.adapter'; 2 | -------------------------------------------------------------------------------- /src/frameworks/cors/index.ts: -------------------------------------------------------------------------------- 1 | export * from './cors.framework'; 2 | -------------------------------------------------------------------------------- /src/frameworks/hapi/index.ts: -------------------------------------------------------------------------------- 1 | export * from './hapi.framework'; 2 | -------------------------------------------------------------------------------- /src/frameworks/koa/index.ts: -------------------------------------------------------------------------------- 1 | export * from './koa.framework'; 2 | -------------------------------------------------------------------------------- /src/frameworks/lazy/index.ts: -------------------------------------------------------------------------------- 1 | export * from './lazy.framework'; 2 | -------------------------------------------------------------------------------- /src/frameworks/polka/index.ts: -------------------------------------------------------------------------------- 1 | export * from './polka.framework'; 2 | -------------------------------------------------------------------------------- /src/frameworks/trpc/index.ts: -------------------------------------------------------------------------------- 1 | export * from './trpc.framework'; 2 | -------------------------------------------------------------------------------- /src/handlers/aws/index.ts: -------------------------------------------------------------------------------- 1 | export * from './aws-stream.handler'; 2 | -------------------------------------------------------------------------------- /src/handlers/azure/index.ts: -------------------------------------------------------------------------------- 1 | export * from './azure.handler'; 2 | -------------------------------------------------------------------------------- /src/handlers/default/index.ts: -------------------------------------------------------------------------------- 1 | export * from './default.handler'; 2 | -------------------------------------------------------------------------------- /src/resolvers/dummy/index.ts: -------------------------------------------------------------------------------- 1 | export * from './dummy.resolver'; 2 | -------------------------------------------------------------------------------- /src/adapters/aws/base/index.ts: -------------------------------------------------------------------------------- 1 | export * from './aws-simple-adapter'; 2 | -------------------------------------------------------------------------------- /src/frameworks/express/index.ts: -------------------------------------------------------------------------------- 1 | export * from './express.framework'; 2 | -------------------------------------------------------------------------------- /src/frameworks/fastify/index.ts: -------------------------------------------------------------------------------- 1 | export * from './fastify.framework'; 2 | -------------------------------------------------------------------------------- /src/handlers/huawei/index.ts: -------------------------------------------------------------------------------- 1 | export * from './http-huawei.handler'; 2 | -------------------------------------------------------------------------------- /src/resolvers/callback/index.ts: -------------------------------------------------------------------------------- 1 | export * from './callback.resolver'; 2 | -------------------------------------------------------------------------------- /src/resolvers/promise/index.ts: -------------------------------------------------------------------------------- 1 | export * from './promise.resolver'; 2 | -------------------------------------------------------------------------------- /src/adapters/azure/index.ts: -------------------------------------------------------------------------------- 1 | export * from './http-trigger-v4.adapter'; 2 | -------------------------------------------------------------------------------- /src/adapters/huawei/index.ts: -------------------------------------------------------------------------------- 1 | export * from './huawei-api-gateway.adapter'; 2 | -------------------------------------------------------------------------------- /src/frameworks/deepkit/index.ts: -------------------------------------------------------------------------------- 1 | export * from './http-deepkit.framework'; 2 | -------------------------------------------------------------------------------- /src/resolvers/aws-context/index.ts: -------------------------------------------------------------------------------- 1 | export * from './aws-context.resolver'; 2 | -------------------------------------------------------------------------------- /src/adapters/digital-ocean/index.ts: -------------------------------------------------------------------------------- 1 | export * from './http-function.adapter'; 2 | -------------------------------------------------------------------------------- /src/handlers/digital-ocean/index.ts: -------------------------------------------------------------------------------- 1 | export * from './digital-ocean.handler'; 2 | -------------------------------------------------------------------------------- /src/frameworks/apollo-server/index.ts: -------------------------------------------------------------------------------- 1 | export * from './apollo-server.framework'; 2 | -------------------------------------------------------------------------------- /src/adapters/apollo-server/index.ts: -------------------------------------------------------------------------------- 1 | export * from './apollo-server-mutation.adapter'; 2 | -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | # Local env vars for debugging 2 | TS_NODE_IGNORE="false" 3 | TS_NODE_FILES="true" 4 | -------------------------------------------------------------------------------- /tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "include": ["src/**/*.ts"] 4 | } 5 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | ; This file is automatically added by @npmcli/template-oss. Do not edit. 2 | 3 | ignore-scripts=true 4 | -------------------------------------------------------------------------------- /test/handlers/bitcoin.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/H4ad/serverless-adapter/HEAD/test/handlers/bitcoin.pdf -------------------------------------------------------------------------------- /www/static/img/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/H4ad/serverless-adapter/HEAD/www/static/img/favicon.ico -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Set the repository to show as TypeScript rather than JS in GitHub 2 | *.js linguist-detectable=false -------------------------------------------------------------------------------- /www/static/img/docusaurus.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/H4ad/serverless-adapter/HEAD/www/static/img/docusaurus.png -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npm run build 5 | npm run test 6 | npm run lint 7 | -------------------------------------------------------------------------------- /src/@types/index.ts: -------------------------------------------------------------------------------- 1 | export * from './binary-settings'; 2 | export * from './headers'; 3 | export * from './helpers'; 4 | -------------------------------------------------------------------------------- /src/handlers/firebase/index.ts: -------------------------------------------------------------------------------- 1 | export * from './http-firebase.handler'; 2 | export * from './http-firebase-v2.handler'; 3 | -------------------------------------------------------------------------------- /www/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [require.resolve('@docusaurus/core/lib/babel/preset')], 3 | }; 4 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "all", 4 | "arrowParens": "avoid", 5 | "printWidth": 80 6 | } 7 | -------------------------------------------------------------------------------- /src/@types/digital-ocean/index.ts: -------------------------------------------------------------------------------- 1 | export * from './digital-ocean-http-event'; 2 | export * from './digital-ocean-http-response'; 3 | -------------------------------------------------------------------------------- /.husky/prepare-commit-msg: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | exec { 5 | createBodyParserTests(); 6 | }); 7 | -------------------------------------------------------------------------------- /src/@types/helpers.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Removes 'optional' attributes from a type's properties 3 | * 4 | * @breadcrumb Types 5 | * @public 6 | */ 7 | export type Concrete = { 8 | [Property in keyof Type]-?: Type[Property]; 9 | }; 10 | -------------------------------------------------------------------------------- /src/core/no-op.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * No operation function is used when we need to pass a function, but we don't want to specify any behavior. 3 | * 4 | * @breadcrumb Core 5 | * @public 6 | */ 7 | export const NO_OP: (...args: any[]) => any = () => void 0; 8 | -------------------------------------------------------------------------------- /www/README.md: -------------------------------------------------------------------------------- 1 | # Documentation 2 | 3 | This website is built using [Docusaurus 2](https://docusaurus.io/), a modern static website generator to create documentation to this library. 4 | 5 | See the website [here](https://viniciusl.com.br/serverless-adapter). 6 | -------------------------------------------------------------------------------- /src/frameworks/body-parser/index.ts: -------------------------------------------------------------------------------- 1 | export * from './base-body-parser.framework'; 2 | export * from './json-body-parser.framework'; 3 | export * from './raw-body-parser.framework'; 4 | export * from './text-body-parser.framework'; 5 | export * from './urlencoded-body-parser.framework'; 6 | -------------------------------------------------------------------------------- /www/docs/main/getting-started/examples.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Examples 3 | position: 4 4 | description: See more about examples of how to use this library. 5 | --- 6 | 7 | You can see some examples of how to use this library [here](https://github.com/H4ad/serverless-adapter-examples). 8 | -------------------------------------------------------------------------------- /tsconfig.doc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "emitDeclarationOnly": true, 5 | "noEmit": false, 6 | "skipDefaultLibCheck": true, 7 | "skipLibCheck": true 8 | }, 9 | "include": ["src/**/*.ts", "src/index.doc.ts"], 10 | "exclude": [] 11 | } 12 | -------------------------------------------------------------------------------- /tsdoc.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://developer.microsoft.com/json-schemas/tsdoc/v0/tsdoc.schema.json", 3 | "tagDefinitions": [ 4 | { 5 | "tagName": "@breadcrumb", 6 | "syntaxKind": "block", 7 | "allowMultiple": false 8 | } 9 | ], 10 | "noStandardTags": false 11 | } 12 | -------------------------------------------------------------------------------- /benchmark/.swcrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "targets": "node >= 18" 4 | }, 5 | "module": { 6 | "type": "commonjs", 7 | "strict": true, 8 | }, 9 | "jsc": { 10 | "target": "es2022", 11 | "parser": { 12 | "syntax": "typescript", 13 | "tsx": false, 14 | "dynamicImport": true 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /test/frameworks/body-parser-v2.framework.spec.ts: -------------------------------------------------------------------------------- 1 | import { describe, vitest } from 'vitest'; 2 | import { createBodyParserTests } from './body-parser.framework.helper'; 3 | 4 | vitest.mock('body-parser', async () => { 5 | return await import('body-parser-v2'); 6 | }); 7 | 8 | describe('Body Parser v2', () => { 9 | createBodyParserTests(); 10 | }); 11 | -------------------------------------------------------------------------------- /src/core/index.ts: -------------------------------------------------------------------------------- 1 | export * from './base-handler'; 2 | export * from './constants'; 3 | export * from './current-invoke'; 4 | export * from './event-body'; 5 | export * from './headers'; 6 | export * from './is-binary'; 7 | export * from './logger'; 8 | export * from './no-op'; 9 | export * from './optional'; 10 | export * from './path'; 11 | export * from './stream'; 12 | -------------------------------------------------------------------------------- /www/.gitignore: -------------------------------------------------------------------------------- 1 | # Dependencies 2 | /node_modules 3 | 4 | # Production 5 | /build 6 | 7 | # Generated files 8 | .docusaurus 9 | .cache-loader 10 | 11 | # Misc 12 | .DS_Store 13 | .env.local 14 | .env.development.local 15 | .env.test.local 16 | .env.production.local 17 | 18 | npm-debug.log* 19 | yarn-debug.log* 20 | yarn-error.log* 21 | 22 | sidebar-api-generated.js 23 | -------------------------------------------------------------------------------- /test/adapters/azure/utils/events.ts: -------------------------------------------------------------------------------- 1 | import { HttpTriggerV4Adapter } from '../../../../src/adapters/azure'; 2 | import { createHttpTriggerEvent } from './http-trigger'; 3 | 4 | export const allAzureEvents: Array<[string, any]> = [ 5 | [HttpTriggerV4Adapter.name, createHttpTriggerEvent('GET', '/')], 6 | [ 7 | HttpTriggerV4Adapter.name, 8 | createHttpTriggerEvent('POST', '/', { name: 'Joga10' }), 9 | ], 10 | ]; 11 | -------------------------------------------------------------------------------- /test/core/no-op.spec.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from 'vitest'; 2 | import { NO_OP } from '../../src'; 3 | 4 | describe('NO_OP', () => { 5 | it('should be a function', () => { 6 | expect(NO_OP).toBeInstanceOf(Function); 7 | }); 8 | 9 | it('should be callable and return undefined', () => { 10 | expect(() => NO_OP()).not.toThrowError(); 11 | 12 | expect(NO_OP()).toBe(undefined); 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /src/adapters/aws/index.ts: -------------------------------------------------------------------------------- 1 | export * from './alb.adapter'; 2 | export * from './api-gateway-v1.adapter'; 3 | export * from './api-gateway-v2.adapter'; 4 | export * from './dynamodb.adapter'; 5 | export * from './event-bridge.adapter'; 6 | export * from './lambda-edge.adapter'; 7 | export * from './s3.adapter'; 8 | export * from './sns.adapter'; 9 | export * from './sqs.adapter'; 10 | export * from './base'; 11 | export * from './request-lambda-edge.adapter'; 12 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # This file is automatically added by @npmcli/template-oss. Do not edit. 2 | 3 | version: 2 4 | 5 | updates: 6 | - package-ecosystem: npm 7 | directory: / 8 | schedule: 9 | interval: daily 10 | allow: 11 | - dependency-type: direct 12 | versioning-strategy: increase-if-necessary 13 | commit-message: 14 | prefix: deps 15 | prefix-development: chore 16 | labels: 17 | - "Dependencies" 18 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line import/no-unresolved 2 | import { defineConfig } from 'vitest/config'; 3 | 4 | export default defineConfig({ 5 | esbuild: { 6 | target: 'es2022', 7 | }, 8 | test: { 9 | coverage: { 10 | provider: 'v8', 11 | include: ['src/**'], 12 | exclude: [ 13 | 'src/**/@types/**/*.ts', 14 | 'src/**/index.doc.ts', 15 | 'src/**/index.ts', 16 | ], 17 | }, 18 | }, 19 | }); 20 | -------------------------------------------------------------------------------- /www/src/components/HowToStart/styles.module.css: -------------------------------------------------------------------------------- 1 | .howto { 2 | padding: 2rem 0; 3 | width: 100%; 4 | } 5 | 6 | .howtoContainer { 7 | display: flex; 8 | flex-direction: column; 9 | align-items: center; 10 | } 11 | 12 | .howto .row { 13 | display: flex; 14 | flex-direction: column; 15 | } 16 | 17 | .exampleContainer { 18 | display: flex; 19 | flex-direction: column; 20 | align-items: center; 21 | } 22 | 23 | .mt-5 { 24 | margin-top: 2rem; 25 | } 26 | -------------------------------------------------------------------------------- /test/resolvers/dummy.resolver.spec.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it, vitest } from 'vitest'; 2 | import { DummyResolver } from '../../src/resolvers/dummy'; 3 | 4 | describe(DummyResolver.name, () => { 5 | it('should do nothing when called and return undefined', () => { 6 | const resolver = new DummyResolver(); 7 | 8 | const task = vitest.fn(); 9 | 10 | resolver.createResolver().run(task); 11 | 12 | expect(task).not.toHaveBeenCalled(); 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /www/src/pages/index.module.css: -------------------------------------------------------------------------------- 1 | /** 2 | * CSS files with the .module.css suffix will be treated as CSS modules 3 | * and scoped locally. 4 | */ 5 | 6 | .heroBanner { 7 | padding: 4rem 0; 8 | text-align: center; 9 | position: relative; 10 | overflow: hidden; 11 | } 12 | 13 | @media screen and (max-width: 996px) { 14 | .heroBanner { 15 | padding: 2rem; 16 | } 17 | } 18 | 19 | .buttons { 20 | display: flex; 21 | align-items: center; 22 | justify-content: center; 23 | } 24 | -------------------------------------------------------------------------------- /tsconfig.eslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "noEmit": true, 5 | "types": [ 6 | "vitest/globals" 7 | ], 8 | "lib": [ 9 | "esnext" 10 | ], 11 | "allowJs": true, 12 | "checkJs": true 13 | }, 14 | "include": [ 15 | "src/**/*.ts", 16 | "test/**/*.ts", 17 | "scripts/**/*.ts", 18 | "vite.config.ts", 19 | "tsup.config.ts" 20 | ], 21 | "exclude": [ 22 | "benchmark/**/*.ts" 23 | ] 24 | } 25 | -------------------------------------------------------------------------------- /test/adapters/utils/events.ts: -------------------------------------------------------------------------------- 1 | import { allAWSEvents } from '../aws/utils/events'; 2 | import { allAzureEvents } from '../azure/utils/events'; 3 | import { allDigitalOceanEvents } from '../digital-ocean/utils/event'; 4 | import { allHuaweiEvents } from '../huawei/utils/events'; 5 | 6 | /** 7 | * Events from all event sources that can be used to test adapters 8 | */ 9 | export const allEvents: [string, any][] = [ 10 | ...allAWSEvents, 11 | ...allHuaweiEvents, 12 | ...allAzureEvents, 13 | ...allDigitalOceanEvents, 14 | ]; 15 | -------------------------------------------------------------------------------- /src/network/utils.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Get the data from a buffer, string, or Uint8Array 3 | * 4 | * @breadcrumb Network 5 | * @param data - The data that was written inside the stream 6 | */ 7 | export function getString(data: Buffer | string | unknown) { 8 | if (Buffer.isBuffer(data)) return data.toString('utf8'); 9 | else if (typeof data === 'string') return data; 10 | else if (data instanceof Uint8Array) return new TextDecoder().decode(data); 11 | else throw new Error(`response.write() of unexpected type: ${typeof data}`); 12 | } 13 | -------------------------------------------------------------------------------- /test/adapters/huawei/utils/events.ts: -------------------------------------------------------------------------------- 1 | import { HuaweiApiGatewayAdapter } from '../../../../src/adapters/huawei'; 2 | import { createHuaweiApiGateway } from './huawei-api-gateway'; 3 | 4 | export const allHuaweiEvents: Array<[string, any]> = [ 5 | [HuaweiApiGatewayAdapter.name, createHuaweiApiGateway('GET', '/users')], 6 | [ 7 | HuaweiApiGatewayAdapter.name, 8 | createHuaweiApiGateway('GET', '/test', undefined, { 'x-batata': 'true' }), 9 | ], 10 | [HuaweiApiGatewayAdapter.name, createHuaweiApiGateway('DELETE', '/test/2')], 11 | ]; 12 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: "🐛 Bug Report" 3 | about: Report a reproducible bug or regression. 4 | title: '' 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | ## Current Behavior 11 | 12 | 13 | 14 | ## Expected Behavior 15 | 16 | 17 | 18 | ## Steps to Reproduce the Problem 19 | 20 | 1. 21 | 1. 22 | 1. 23 | 24 | ## Environment 25 | 26 | - Version: 27 | - Platform: 28 | - Node.js Version: 29 | -------------------------------------------------------------------------------- /.github/settings.yml: -------------------------------------------------------------------------------- 1 | # This file is automatically added by @npmcli/template-oss. Do not edit. 2 | 3 | repository: 4 | allow_merge_commit: false 5 | allow_rebase_merge: true 6 | allow_squash_merge: true 7 | squash_merge_commit_title: PR_TITLE 8 | squash_merge_commit_message: PR_BODY 9 | delete_branch_on_merge: true 10 | enable_automated_security_fixes: true 11 | enable_vulnerability_alerts: true 12 | 13 | branches: 14 | - name: main 15 | protection: 16 | required_status_checks: null 17 | enforce_admins: true 18 | required_pull_request_reviews: 19 | require_last_push_approval: true 20 | dismiss_stale_reviews: true 21 | -------------------------------------------------------------------------------- /benchmark/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2022", 4 | "module": "ESNext", 5 | "moduleResolution": "node", 6 | "jsx": "preserve", 7 | "declaration": true, 8 | "outDir": "dist", 9 | "allowJs": false, 10 | "allowSyntheticDefaultImports": true, 11 | "esModuleInterop": true, 12 | "strict": true, 13 | "skipLibCheck": true, 14 | "noImplicitAny": false, 15 | "noUnusedLocals": true, 16 | "noUnusedParameters": true, 17 | "strictNullChecks": true, 18 | "useUnknownInCatchVariables": false 19 | }, 20 | "include": [ 21 | "src/**/*.ts" 22 | ], 23 | "ts-node": { 24 | "esm": true 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/@types/digital-ocean/digital-ocean-http-response.ts: -------------------------------------------------------------------------------- 1 | //#region Imports 2 | 3 | import type { SingleValueHeaders } from '../headers'; 4 | 5 | //#endregion 6 | 7 | /** 8 | * The interface to represents the response of Digital Ocean Function. 9 | * 10 | * @public 11 | * @breadcrumb Types / Digital Ocean / DigitalOceanHttpResponse 12 | */ 13 | export interface DigitalOceanHttpResponse { 14 | /** 15 | * The HTTP Headers of the response 16 | */ 17 | headers?: SingleValueHeaders; 18 | 19 | /** 20 | * The body of the response 21 | */ 22 | body: unknown; 23 | 24 | /** 25 | * The HTTP Status code of the response 26 | */ 27 | statusCode: number; 28 | } 29 | -------------------------------------------------------------------------------- /test/adapters/digital-ocean/utils/event.ts: -------------------------------------------------------------------------------- 1 | import { HttpFunctionAdapter } from '../../../../src/adapters/digital-ocean'; 2 | import { createHttpFunctionEvent } from './http-function'; 3 | 4 | export const allDigitalOceanEvents: Array<[string, any]> = [ 5 | [ 6 | HttpFunctionAdapter.name, 7 | createHttpFunctionEvent('post', '/users', { name: 'test' }), 8 | ], 9 | [HttpFunctionAdapter.name, createHttpFunctionEvent('get', '/potatos')], 10 | [HttpFunctionAdapter.name, createHttpFunctionEvent('get', '')], 11 | [ 12 | HttpFunctionAdapter.name, 13 | createHttpFunctionEvent('get', '/query', undefined, undefined, { 14 | page: '1', 15 | }), 16 | ], 17 | ]; 18 | -------------------------------------------------------------------------------- /src/frameworks/express/express.framework.ts: -------------------------------------------------------------------------------- 1 | //#region 2 | 3 | import type { IncomingMessage, ServerResponse } from 'http'; 4 | import type { Express } from 'express'; 5 | import type { FrameworkContract } from '../../contracts'; 6 | 7 | //#endregion 8 | 9 | /** 10 | * The framework that forwards requests to express handler 11 | * 12 | * @breadcrumb Frameworks / ExpressFramework 13 | * @public 14 | */ 15 | export class ExpressFramework implements FrameworkContract { 16 | /** 17 | * {@inheritDoc} 18 | */ 19 | public sendRequest( 20 | app: Express, 21 | request: IncomingMessage, 22 | response: ServerResponse, 23 | ): void { 24 | app(request, response); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/frameworks/koa/koa.framework.ts: -------------------------------------------------------------------------------- 1 | //#region Imports 2 | 3 | import type { IncomingMessage, ServerResponse } from 'http'; 4 | import type Application from 'koa'; 5 | import type { FrameworkContract } from '../../contracts'; 6 | 7 | //#endregion 8 | 9 | /** 10 | * The framework that forwards requests to koa handler 11 | * 12 | * @breadcrumb Frameworks / KoaFramework 13 | * @public 14 | */ 15 | export class KoaFramework implements FrameworkContract { 16 | /** 17 | * {@inheritDoc} 18 | */ 19 | public sendRequest( 20 | app: Application, 21 | request: IncomingMessage, 22 | response: ServerResponse, 23 | ): void { 24 | app.callback()(request, response); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/resolvers/dummy/dummy.resolver.ts: -------------------------------------------------------------------------------- 1 | //#region Imports 2 | 3 | import type { Resolver, ResolverContract } from '../../contracts'; 4 | 5 | //#endregion 6 | 7 | /** 8 | * The class that represents a dummy resolver that does nothing and can be used by the cloud that doesn't use resolvers. 9 | * 10 | * @breadcrumb Resolvers / DummyResolver 11 | * @public 12 | */ 13 | export class DummyResolver 14 | implements ResolverContract 15 | { 16 | /** 17 | * {@inheritDoc} 18 | */ 19 | public createResolver(): Resolver { 20 | return { 21 | // eslint-disable-next-line @typescript-eslint/no-misused-promises 22 | run: () => Promise.resolve(), 23 | }; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/frameworks/polka/polka.framework.ts: -------------------------------------------------------------------------------- 1 | //#region Imports 2 | 3 | import type { IncomingMessage, ServerResponse } from 'http'; 4 | import polka, { type Polka } from 'polka'; 5 | import type { FrameworkContract } from '../../contracts'; 6 | 7 | //#endregion 8 | 9 | /** 10 | * The framework that forwards requests to polka handler 11 | * 12 | * @breadcrumb Frameworks / PolkaFramework 13 | * @public 14 | */ 15 | export class PolkaFramework implements FrameworkContract { 16 | /** 17 | * {@inheritDoc} 18 | */ 19 | sendRequest( 20 | app: Polka, 21 | request: IncomingMessage, 22 | response: ServerResponse, 23 | ): void { 24 | app.handler(request as polka.Request, response); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/frameworks/fastify/helpers/no-op-content-parser.ts: -------------------------------------------------------------------------------- 1 | //#region Imports 2 | 3 | import type { FastifyInstance } from 'fastify'; 4 | 5 | //#endregion 6 | 7 | /** 8 | * Just return the current body as it was parsed. 9 | * 10 | * @remarks This function is intended to be used with BodyParserFrameworks. 11 | * 12 | * @param app - The instance of fastify 13 | * @param contentType - The content type to be anuled 14 | * 15 | * @breadcrumb Frameworks / FastifyFramework / Helpers 16 | * @public 17 | */ 18 | export function setNoOpForContentType( 19 | app: FastifyInstance, 20 | contentType: string, 21 | ): void { 22 | app.addContentTypeParser(contentType, (_, req, done) => { 23 | return done(null, (req as any).body); 24 | }); 25 | } 26 | -------------------------------------------------------------------------------- /scripts/parse-docs.ts: -------------------------------------------------------------------------------- 1 | import { resolve } from 'path'; 2 | import { Extractor, ExtractorConfig } from '@microsoft/api-extractor'; 3 | 4 | const apiExtractConfig = resolve('./api-extractor.json'); 5 | 6 | function build(): void { 7 | const config = ExtractorConfig.loadFileAndPrepare(apiExtractConfig); 8 | 9 | const extractorResult = Extractor.invoke(config, { 10 | localBuild: true, 11 | }); 12 | 13 | if (!extractorResult.succeeded) { 14 | console.error( 15 | `API Extractor completed with ${extractorResult.errorCount} errors` + 16 | ` and ${extractorResult.warningCount} warnings`, 17 | ); 18 | process.exitCode = 1; 19 | } 20 | 21 | console.log('API Extractor completed successfully'); 22 | } 23 | 24 | build(); 25 | -------------------------------------------------------------------------------- /src/core/optional.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Return the defaultValue whether the value is undefined, otherwise, return the value. 3 | * 4 | * @example 5 | * ```typescript 6 | * const value1 = getDefaultIfUndefined(undefined, true); 7 | * const value2 = getDefaultIfUndefined(false, true); 8 | * 9 | * console.log(value1); 10 | * // true 11 | * console.log(value2); 12 | * // false 13 | * ``` 14 | * 15 | * @param value - The value to be checked 16 | * @param defaultValue - The default value when value is undefined 17 | * 18 | * @breadcrumb Core 19 | * @public 20 | */ 21 | export function getDefaultIfUndefined( 22 | value: T | undefined, 23 | defaultValue: T, 24 | ): T { 25 | if (value === undefined) return defaultValue; 26 | 27 | return value; 28 | } 29 | -------------------------------------------------------------------------------- /benchmark/README.md: -------------------------------------------------------------------------------- 1 | # Benchmark 2 | 3 | In this folder, we have benchmarks to compare the speed of this library with others. 4 | 5 | ## How to Run 6 | 7 | ```bash 8 | git clone git@github.com:H4ad/serverless-adapter.git 9 | cd serverless-adapter 10 | npm ci 11 | npm run build 12 | npm pack 13 | cd benchmark 14 | npm ci 15 | npm i ../h4ad-serverless-adapter-0.0.0-development.tgz 16 | npm run bench 17 | ``` 18 | 19 | ## Latest Run 20 | 21 | - CPU: Ryzen 2200g 22 | - Memory: 32GB 3200Hz 23 | 24 | ```md 25 | @h4ad/serverless-adapter x 46,463 ops/sec ±10.75% (65 runs sampled) 26 | @vendia/serverless-express x 8,726 ops/sec ±18.64% (82 runs sampled) 27 | serverless-http x 48,246 ops/sec ±8.00% (70 runs sampled) 28 | Fastest is serverless-http,@h4ad/serverless-adapter 29 | ``` 30 | -------------------------------------------------------------------------------- /src/frameworks/hapi/hapi.framework.ts: -------------------------------------------------------------------------------- 1 | //#region Imports 2 | 3 | import type { IncomingMessage, ServerResponse } from 'http'; 4 | import type { Server } from '@hapi/hapi'; 5 | import type { FrameworkContract } from '../../contracts'; 6 | 7 | //#endregion 8 | 9 | /** 10 | * The framework that forwards requests to hapi handler 11 | * 12 | * @breadcrumb Frameworks / HapiFramework 13 | * @public 14 | */ 15 | export class HapiFramework implements FrameworkContract { 16 | /** 17 | * {@inheritDoc} 18 | */ 19 | public sendRequest( 20 | app: Server, 21 | request: IncomingMessage, 22 | response: ServerResponse, 23 | ): void { 24 | const httpServer: any = app.listener; 25 | 26 | httpServer._events.request(request, response); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/contracts/framework.contract.ts: -------------------------------------------------------------------------------- 1 | //#region Imports 2 | 3 | import type { IncomingMessage, ServerResponse } from 'http'; 4 | 5 | //#endregion 6 | 7 | /** 8 | * The interface that represents a contract between the framework and the framework implementation 9 | * 10 | * @breadcrumb Contracts 11 | * @public 12 | */ 13 | export interface FrameworkContract { 14 | /** 15 | * Send the request and response objects to the framework 16 | * 17 | * @param app - The instance of your app (Express, Fastify, Koa, etc...) 18 | * @param request - The request object that will be forward to your app 19 | * @param response - The response object that will be forward to your app to output the response 20 | */ 21 | sendRequest( 22 | app: TApp, 23 | request: IncomingMessage, 24 | response: ServerResponse, 25 | ): void; 26 | } 27 | -------------------------------------------------------------------------------- /src/frameworks/fastify/fastify.framework.ts: -------------------------------------------------------------------------------- 1 | //#region Imports 2 | 3 | import type { IncomingMessage, ServerResponse } from 'http'; 4 | import type { FastifyInstance } from 'fastify'; 5 | import type { FrameworkContract } from '../../contracts'; 6 | 7 | //#endregion 8 | 9 | /** 10 | * The framework that forwards requests to fastify handler 11 | * 12 | * @breadcrumb Frameworks / FastifyFramework 13 | * @public 14 | */ 15 | export class FastifyFramework implements FrameworkContract { 16 | /** 17 | * {@inheritDoc} 18 | */ 19 | public sendRequest( 20 | app: FastifyInstance, 21 | request: IncomingMessage, 22 | response: ServerResponse, 23 | ): void { 24 | // ref: https://www.fastify.io/docs/latest/Guides/Serverless/#implement-and-export-the-function 25 | app.ready().then(() => app.server.emit('request', request, response)); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /www/docs/main/getting-started/installation.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Installation 3 | position: 1 4 | description: See more about how to install this library. 5 | --- 6 | 7 | import TabItem from '@theme/TabItem'; 8 | import Tabs from '@theme/Tabs'; 9 | 10 | The Serverless Adapter package has everything you need to start. 11 | 12 | ## Requirements 13 | 14 | Current versions supported by this library are 18.x and 20.x. 15 | 16 | ## Installing 17 | 18 | First, inside your project, run the following command: 19 | 20 | 21 | 22 | 23 | ```bash 24 | npm i --save @h4ad/serverless-adapter 25 | ``` 26 | 27 | 28 | 29 | 30 | 31 | ```bash 32 | yarn add @h4ad/serverless-adapter 33 | ``` 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /test/core/current-invoke.spec.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from 'vitest'; 2 | import { getCurrentInvoke, setCurrentInvoke } from '../../src'; 3 | 4 | describe('CurrentInvoke', () => { 5 | it('should initial values of getCurrentInvoke be null', () => { 6 | const initial = getCurrentInvoke(); 7 | 8 | expect(initial).toBeDefined(); 9 | 10 | expect(initial).toHaveProperty('event', null); 11 | expect(initial).toHaveProperty('context', null); 12 | }); 13 | 14 | it('should set and get current invoke without problems', () => { 15 | const event = { batata: true }; 16 | const context = { potato: true }; 17 | 18 | expect(() => setCurrentInvoke({ event, context })).not.toThrowError(); 19 | 20 | const currentInvoke = getCurrentInvoke(); 21 | 22 | expect(currentInvoke).toHaveProperty('event', event); 23 | expect(currentInvoke).toHaveProperty('context', context); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/core/event-body.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Get the event body as buffer from body string with content length 3 | * 4 | * @example 5 | * ```typescript 6 | * const body = '{}'; 7 | * const [buffer, contentLength] = getEventBodyAsBuffer(body, false); 8 | * console.log(buffer); 9 | * // 10 | * console.log(contentLength); 11 | * // 2 12 | * ``` 13 | * 14 | * @param body - The body string that can be encoded or not 15 | * @param isBase64Encoded - Tells if body string is encoded in base64 16 | * 17 | * @breadcrumb Core 18 | * @public 19 | */ 20 | export function getEventBodyAsBuffer( 21 | body: string, 22 | isBase64Encoded: boolean, 23 | ): [body: Buffer, contentLength: number] { 24 | const encoding: BufferEncoding = isBase64Encoded ? 'base64' : 'utf8'; 25 | 26 | const buffer = Buffer.from(body, encoding); 27 | const contentLength = Buffer.byteLength(buffer, encoding); 28 | 29 | return [buffer, contentLength]; 30 | } 31 | -------------------------------------------------------------------------------- /test/core/event-body.spec.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from 'vitest'; 2 | import { getEventBodyAsBuffer } from '../../src'; 3 | 4 | describe('getEventBodyAsBuffer', () => { 5 | it('should return correctly the body in utf-8 as buffer', () => { 6 | const body = '{}'; 7 | 8 | const [bodyAsBuffer, contentLength] = getEventBodyAsBuffer(body, false); 9 | 10 | expect(bodyAsBuffer).toBeInstanceOf(Buffer); 11 | expect(contentLength).toBe(2); 12 | 13 | expect(bodyAsBuffer.toString('utf8')).toBe(body); 14 | }); 15 | 16 | it('should return correctly the body in base64 as buffer', () => { 17 | const body = Buffer.from('{}', 'utf8').toString('base64'); 18 | 19 | const [bodyAsBuffer, contentLength] = getEventBodyAsBuffer(body, true); 20 | 21 | expect(bodyAsBuffer).toBeInstanceOf(Buffer); 22 | expect(contentLength).toBe(2); 23 | 24 | expect(bodyAsBuffer.toString('base64')).toBe(body); 25 | }); 26 | }); 27 | -------------------------------------------------------------------------------- /test/adapters/aws/utils/sqs.ts: -------------------------------------------------------------------------------- 1 | import type { SQSEvent } from 'aws-lambda'; 2 | 3 | /** 4 | * Sample event from {@link https://docs.aws.amazon.com/lambda/latest/dg/with-sqs.html} 5 | */ 6 | export function createSQSEvent(): SQSEvent { 7 | return { 8 | Records: [ 9 | { 10 | messageId: '059f36b4-87a3-44ab-83d2-661975830a7d', 11 | receiptHandle: 'AQEBwJnKyrHigUMZj6rYigCgxlaS3SLy0a...', 12 | body: 'Test message.', 13 | attributes: { 14 | ApproximateReceiveCount: '1', 15 | SentTimestamp: '1545082649183', 16 | SenderId: 'AIDAIENQZJOLO23YVJ4VO', 17 | ApproximateFirstReceiveTimestamp: '1545082649185', 18 | }, 19 | messageAttributes: {}, 20 | md5OfBody: 'e4e68fb7bd0e697a0ae8f1bb342846b3', 21 | eventSource: 'aws:sqs', 22 | eventSourceARN: 'arn:aws:sqs:us-east-2:123456789012:my-queue', 23 | awsRegion: 'us-east-2', 24 | }, 25 | ], 26 | }; 27 | } 28 | -------------------------------------------------------------------------------- /src/@types/huawei/huawei-api-gateway-response.ts: -------------------------------------------------------------------------------- 1 | //#region Imports 2 | 3 | import type { MultiValueHeaders } from '../headers'; 4 | 5 | //#endregion 6 | 7 | /** 8 | * The interface that represents the Api Gateway Response of Huawei when integrate with Function Graph of Event Type. 9 | * See more in {@link https://support.huaweicloud.com/intl/en-us/devg-functiongraph/functiongraph_02_0102.html#functiongraph_02_0102__li5178638110137 | Reference}. 10 | * 11 | * @public 12 | * @breadcrumb Types / Huawei / HuaweiApiGatewayResponse 13 | */ 14 | export interface HuaweiApiGatewayResponse { 15 | /** 16 | * Tells if the body was encoded as base64 17 | */ 18 | isBase64Encoded: boolean; 19 | 20 | /** 21 | * The HTTP Status code of this response 22 | */ 23 | statusCode: number; 24 | 25 | /** 26 | * The headers sent with this response 27 | */ 28 | headers: MultiValueHeaders; 29 | 30 | /** 31 | * The body value with the content of this response serialized in JSON 32 | */ 33 | body: string; 34 | } 35 | -------------------------------------------------------------------------------- /test/frameworks/koa.framework.spec.ts: -------------------------------------------------------------------------------- 1 | import Application, { type Context } from 'koa'; 2 | import { describe } from 'vitest'; 3 | import { NO_OP } from '../../src'; 4 | import { KoaFramework } from '../../src/frameworks/koa'; 5 | import { type TestRouteBuilderHandler, createTestSuiteFor } from './utils'; 6 | 7 | function createHandler(): TestRouteBuilderHandler { 8 | return (app, _, handler) => { 9 | app.use((ctx: Context) => { 10 | const [statusCode, resultBody, headers] = handler(ctx.headers, NO_OP); 11 | 12 | for (const header of Object.keys(headers)) 13 | ctx.set(header, headers[header]); 14 | 15 | ctx.status = statusCode; 16 | ctx.body = resultBody; 17 | }); 18 | }; 19 | } 20 | 21 | describe(KoaFramework.name, () => { 22 | createTestSuiteFor( 23 | () => new KoaFramework(), 24 | () => new Application(), 25 | { 26 | get: createHandler(), 27 | delete: createHandler(), 28 | post: createHandler(), 29 | put: createHandler(), 30 | }, 31 | ); 32 | }); 33 | -------------------------------------------------------------------------------- /scripts/generate-markdown.ts: -------------------------------------------------------------------------------- 1 | import { readFileSync, writeFileSync } from 'fs'; 2 | import { join, resolve } from 'path'; 3 | import { ApiModel } from '@microsoft/api-extractor-model'; 4 | import { CustomMarkdownDocumenter } from './libs/CustomMarkdownDocumenter'; 5 | 6 | const apiModelPath = resolve('.', 'temp', 'serverless-adapter.api.json'); 7 | const outputFolder = resolve('.', 'www', 'docs', 'api'); 8 | 9 | function build(): void { 10 | const apiModel = new ApiModel(); 11 | 12 | apiModel.loadPackage(apiModelPath); 13 | 14 | const markdown = new CustomMarkdownDocumenter({ 15 | apiModel, 16 | outputFolder, 17 | documenterConfig: undefined, 18 | }); 19 | 20 | markdown.generateFiles(); 21 | 22 | const filename = join(outputFolder, 'Introduction.md'); 23 | const introductionMarkdownContent = readFileSync(filename); 24 | 25 | const introductionContent = `---\ntitle: Introduction\nsidebar_position: -1\n---\n\n${introductionMarkdownContent}`; 26 | 27 | writeFileSync(filename, introductionContent); 28 | } 29 | 30 | build(); 31 | -------------------------------------------------------------------------------- /test/frameworks/fastify.framework.spec.ts: -------------------------------------------------------------------------------- 1 | import fastify, { type FastifyInstance } from 'fastify'; 2 | import { describe } from 'vitest'; 3 | import { FastifyFramework } from '../../src/frameworks/fastify'; 4 | import { type TestRouteBuilderHandler, createTestSuiteFor } from './utils'; 5 | 6 | function createHandler( 7 | method: 'get' | 'post' | 'delete' | 'put', 8 | ): TestRouteBuilderHandler { 9 | return (app, path, handler) => { 10 | app[method](path, {}, (request, response) => { 11 | const [statusCode, resultBody, headers] = handler( 12 | request.headers, 13 | request.body, 14 | ); 15 | 16 | response.headers(headers).code(statusCode).send(resultBody); 17 | }); 18 | }; 19 | } 20 | 21 | describe(FastifyFramework.name, () => { 22 | createTestSuiteFor( 23 | () => new FastifyFramework(), 24 | () => fastify(), 25 | { 26 | get: createHandler('get'), 27 | delete: createHandler('delete'), 28 | post: createHandler('post'), 29 | put: createHandler('put'), 30 | }, 31 | ); 32 | }); 33 | -------------------------------------------------------------------------------- /test/frameworks/fastify-v5.framework.spec.ts: -------------------------------------------------------------------------------- 1 | import fastify, { type FastifyInstance } from 'fastify-v5'; 2 | import { describe } from 'vitest'; 3 | import { FastifyFramework } from '../../src/frameworks/fastify'; 4 | import { type TestRouteBuilderHandler, createTestSuiteFor } from './utils'; 5 | 6 | function createHandler( 7 | method: 'get' | 'post' | 'delete' | 'put', 8 | ): TestRouteBuilderHandler { 9 | return (app, path, handler) => { 10 | app[method](path, {}, (request, response) => { 11 | const [statusCode, resultBody, headers] = handler( 12 | request.headers, 13 | request.body, 14 | ); 15 | 16 | response.headers(headers).code(statusCode).send(resultBody); 17 | }); 18 | }; 19 | } 20 | 21 | describe(FastifyFramework.name, () => { 22 | createTestSuiteFor( 23 | () => new FastifyFramework(), 24 | () => fastify(), 25 | { 26 | get: createHandler('get'), 27 | delete: createHandler('delete'), 28 | post: createHandler('post'), 29 | put: createHandler('put'), 30 | }, 31 | ); 32 | }); 33 | -------------------------------------------------------------------------------- /src/handlers/firebase/http-firebase.handler.ts: -------------------------------------------------------------------------------- 1 | //#region Imports 2 | 3 | // eslint-disable-next-line import/no-unresolved 4 | import { type HttpsFunction, https } from 'firebase-functions/v1'; 5 | import type { FrameworkContract, HandlerContract } from '../../contracts'; 6 | import { RawRequest } from '../base'; 7 | 8 | //#endregion 9 | /** 10 | * The class that implements a handler for Firebase Https Events 11 | * 12 | * @remarks Read more about Https Events {@link https://firebase.google.com/docs/functions/http-events | here} 13 | * 14 | * @breadcrumb Handlers / HttpFirebaseHandler 15 | * @public 16 | */ 17 | export class HttpFirebaseHandler 18 | extends RawRequest 19 | implements 20 | HandlerContract> 21 | { 22 | //#region Public Methods 23 | 24 | /** 25 | * {@inheritDoc} 26 | */ 27 | public getHandler( 28 | app: TApp, 29 | framework: FrameworkContract, 30 | ): HttpsFunction { 31 | return https.onRequest(this.onRequestCallback(app, framework)); 32 | } 33 | 34 | //#endregion 35 | } 36 | -------------------------------------------------------------------------------- /src/@types/headers.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * The record that represents the headers that doesn't have multiple values in the value 3 | * 4 | * @example 5 | * ```typescript 6 | * { 'Accept-Encoding': 'gzip, deflate, br' } 7 | * ``` 8 | * 9 | * @breadcrumb Types 10 | * @public 11 | */ 12 | export type SingleValueHeaders = Record; 13 | 14 | /** 15 | * The record that represents the headers that have multiple values in the value 16 | * 17 | * @example 18 | * ```typescript 19 | * { 'Accept-Encoding': ['gzip', 'deflate', 'br'] } 20 | * ``` 21 | * 22 | * @breadcrumb Types 23 | * @public 24 | */ 25 | export type MultiValueHeaders = Record; 26 | 27 | /** 28 | * The record that represents the headers that can both single or multiple values in the value 29 | * 30 | * @example 31 | * ```typescript 32 | * { 'Accept-Encoding': ['gzip', 'deflate', 'br'], 'Host': 'xyz.execute-api.us-east-1.amazonaws.com' } 33 | * ``` 34 | * 35 | * @breadcrumb Types 36 | * @public 37 | */ 38 | export type BothValueHeaders = Record; 39 | -------------------------------------------------------------------------------- /benchmark/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "benchmark", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "build": "swc src --out-dir dist", 8 | "bench": "npm run build && node dist/samples/compare-libraries.js" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "git+https://github.com/H4ad/serverless-adapter.git" 13 | }, 14 | "license": "MIT", 15 | "author": { 16 | "name": "Vinícius Lourenço", 17 | "email": "H4ad@users.noreply.github.com", 18 | "url": "https://github.com/H4ad" 19 | }, 20 | "dependencies": { 21 | "@h4ad/serverless-adapter": "file:../h4ad-serverless-adapter-0.0.0-development.tgz", 22 | "@swc/cli": "0.5.1", 23 | "@swc/core": "1.3.101", 24 | "@vendia/serverless-express": "4.12.6", 25 | "benchmark": "2.1.4", 26 | "express": "4.21.1", 27 | "serverless-http": "3.2.0", 28 | "stream-mock": "2.0.5" 29 | }, 30 | "devDependencies": { 31 | "@types/benchmark": "2.1.5", 32 | "aws-lambda": "1.0.7", 33 | "chokidar": "3.5.3", 34 | "ts-node": "10.9.2", 35 | "typescript": "5.3.3" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Vinícius Lourenço 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /benchmark/src/samples/clone-headers.ts: -------------------------------------------------------------------------------- 1 | import benchmark from 'benchmark'; 2 | import { createApiGatewayV1 } from '../events'; 3 | 4 | const eventV1ApiGateway = createApiGatewayV1('GET', '/test'); 5 | 6 | const randomTest = [ 7 | createApiGatewayV1('GET', '/pat2'), 8 | createApiGatewayV1('GET', '/pat3'), 9 | ]; 10 | const headers = createApiGatewayV1('GET', '/pat2').headers; 11 | const suite = new benchmark.Suite(); 12 | 13 | suite.add('{...}', () => { 14 | const result = { ...headers }; 15 | }); 16 | suite.add('structuredClone', () => 17 | structuredClone(headers), 18 | ); 19 | suite.add('JSON.parse + JSON.stringify', () => 20 | JSON.parse(JSON.stringify(headers)), 21 | ); 22 | suite.add('for loop + object.keys', () => { 23 | const headers = {}; 24 | 25 | for (const key of Object.keys([Math.floor(Math.random() * 2)])) 26 | headers[key] = headers[key]; 27 | }); 28 | 29 | suite 30 | .on('cycle', function (event) { 31 | console.log(String(event.target)); 32 | }) 33 | .on('complete', function () { 34 | console.log('Fastest is ' + this.filter('fastest').map('name')); 35 | }) 36 | .run({ 37 | async: false, 38 | }); 39 | -------------------------------------------------------------------------------- /test/frameworks/express-v5.framework.spec.ts: -------------------------------------------------------------------------------- 1 | import express from 'express-v5'; 2 | import { describe } from 'vitest'; 3 | import { ExpressFramework } from '../../src/frameworks/express'; 4 | import { type TestRouteBuilderHandler, createTestSuiteFor } from './utils'; 5 | 6 | function createHandler( 7 | method: 'get' | 'post' | 'delete' | 'put', 8 | ): TestRouteBuilderHandler { 9 | return (app, path, handler) => { 10 | app[method](path, (request, response) => { 11 | const [statusCode, resultBody, headers] = handler( 12 | request.headers, 13 | request.body, 14 | ); 15 | 16 | for (const header of Object.keys(headers)) 17 | response.header(header, headers[header]); 18 | 19 | response.status(statusCode).json(resultBody); 20 | }); 21 | }; 22 | } 23 | 24 | describe(ExpressFramework.name, () => { 25 | createTestSuiteFor( 26 | () => new ExpressFramework(), 27 | () => express(), 28 | { 29 | get: createHandler('get'), 30 | delete: createHandler('delete'), 31 | post: createHandler('post'), 32 | put: createHandler('put'), 33 | }, 34 | ); 35 | }); 36 | -------------------------------------------------------------------------------- /test/frameworks/express.framework.spec.ts: -------------------------------------------------------------------------------- 1 | import express, { type Express } from 'express'; 2 | import { describe } from 'vitest'; 3 | import { ExpressFramework } from '../../src/frameworks/express'; 4 | import { type TestRouteBuilderHandler, createTestSuiteFor } from './utils'; 5 | 6 | function createHandler( 7 | method: 'get' | 'post' | 'delete' | 'put', 8 | ): TestRouteBuilderHandler { 9 | return (app, path, handler) => { 10 | app[method](path, (request, response) => { 11 | const [statusCode, resultBody, headers] = handler( 12 | request.headers, 13 | request.body, 14 | ); 15 | 16 | for (const header of Object.keys(headers)) 17 | response.header(header, headers[header]); 18 | 19 | response.status(statusCode).json(resultBody); 20 | }); 21 | }; 22 | } 23 | 24 | describe(ExpressFramework.name, () => { 25 | createTestSuiteFor( 26 | () => new ExpressFramework(), 27 | () => express(), 28 | { 29 | get: createHandler('get'), 30 | delete: createHandler('delete'), 31 | post: createHandler('post'), 32 | put: createHandler('put'), 33 | }, 34 | ); 35 | }); 36 | -------------------------------------------------------------------------------- /test/frameworks/polka.framework.spec.ts: -------------------------------------------------------------------------------- 1 | import { describe } from 'vitest'; 2 | import polka, { type Polka } from 'polka'; 3 | import { PolkaFramework } from '../../src/frameworks/polka'; 4 | import { type TestRouteBuilderHandler, createTestSuiteFor } from './utils'; 5 | 6 | function createHandler( 7 | method: 'get' | 'post' | 'delete' | 'put', 8 | ): TestRouteBuilderHandler { 9 | return (app, path, handler) => { 10 | app[method](path, (request, response) => { 11 | const [statusCode, resultBody, headers] = handler( 12 | request.headers, 13 | request.body, 14 | ); 15 | 16 | for (const header of Object.keys(headers)) 17 | response.setHeader(header, headers[header]); 18 | 19 | response.statusCode = statusCode; 20 | response.end(JSON.stringify(resultBody)); 21 | }); 22 | }; 23 | } 24 | 25 | describe(PolkaFramework.name, () => { 26 | createTestSuiteFor( 27 | () => new PolkaFramework(), 28 | () => polka(), 29 | { 30 | get: createHandler('get'), 31 | delete: createHandler('delete'), 32 | post: createHandler('post'), 33 | put: createHandler('put'), 34 | }, 35 | ); 36 | }); 37 | -------------------------------------------------------------------------------- /test/core/optional.spec.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from 'vitest'; 2 | import { getDefaultIfUndefined } from '../../src'; 3 | 4 | describe('getDefaultIfUndefined', () => { 5 | it('should return the value when value is not undefined', () => { 6 | const options: [testValue: any, defaultValue: any, expectedValue: any][] = [ 7 | ['batata', 'potato', 'batata'], 8 | [true, false, true], 9 | [false, true, false], 10 | ]; 11 | 12 | for (const [testValue, defaultValue, expectedValue] of options) { 13 | expect(getDefaultIfUndefined(testValue, defaultValue)).toBe( 14 | expectedValue, 15 | ); 16 | } 17 | }); 18 | 19 | it('should return the default value when value is undefined', () => { 20 | const options: [testValue: any, defaultValue: any, expectedValue: any][] = [ 21 | [undefined, true, true], 22 | [undefined, 'text', 'text'], 23 | [void 0, true, true], 24 | [void 0, 'text', 'text'], 25 | ]; 26 | 27 | for (const [testValue, defaultValue, expectedValue] of options) { 28 | expect(getDefaultIfUndefined(testValue, defaultValue)).toBe( 29 | expectedValue, 30 | ); 31 | } 32 | }); 33 | }); 34 | -------------------------------------------------------------------------------- /benchmark/src/framework.mock.ts: -------------------------------------------------------------------------------- 1 | import { FrameworkContract } from '@h4ad/serverless-adapter'; 2 | import type { IncomingMessage, ServerResponse } from 'http'; 3 | import { ObjectReadableMock } from 'stream-mock'; 4 | 5 | /** 6 | * The class that represents a mock for framework that forward the request body to the response. 7 | * 8 | * @internal 9 | */ 10 | export class FrameworkMock implements FrameworkContract { 11 | //#region Constructor 12 | 13 | /** 14 | * Construtor padrão 15 | */ 16 | constructor( 17 | protected readonly statusCode: number, 18 | protected readonly mockedResponseData: object, 19 | ) {} 20 | 21 | //#endregion 22 | 23 | /** 24 | * {@inheritDoc} 25 | */ 26 | public sendRequest( 27 | _: null, 28 | __: IncomingMessage, 29 | response: ServerResponse, 30 | ): void { 31 | const writableOutput = new ObjectReadableMock( 32 | [Buffer.from(JSON.stringify(this.mockedResponseData))], 33 | { 34 | objectMode: true, 35 | }, 36 | ); 37 | 38 | response.statusCode = this.statusCode; 39 | response.setHeader('content-type', 'application/json'); 40 | 41 | writableOutput.pipe(response); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /test/adapters/digital-ocean/utils/http-function.ts: -------------------------------------------------------------------------------- 1 | import { type DigitalOceanHttpEvent } from '../../../../src/@types/digital-ocean'; 2 | 3 | export function createHttpFunctionEvent( 4 | method: string, 5 | path: string, 6 | body?: Record, 7 | headers?: Record, 8 | queryParams?: Record, 9 | ): DigitalOceanHttpEvent { 10 | return { 11 | __ow_method: method, 12 | __ow_query: new URLSearchParams(queryParams).toString(), 13 | __ow_body: JSON.stringify(body), 14 | __ow_headers: { 15 | accept: '*/*', 16 | 'accept-encoding': 'gzip', 17 | 'cdn-loop': 'cloudflare', 18 | 'cf-connecting-ip': '45.444.444.444', 19 | 'cf-ipcountry': 'BR', 20 | 'cf-ray': '4444443444a537-GRU', 21 | 'cf-visitor': '{"scheme":"https"}', 22 | 'content-type': 'application/json', 23 | host: 'ccontroller', 24 | 'user-agent': 'insomnia/2022.4.2', 25 | 'x-custom': 'potato', 26 | 'x-forwarded-for': '45.444.444.444', 27 | 'x-forwarded-proto': 'https', 28 | 'x-request-id': 'xxxxxxxxxxxxxxxxxx', 29 | ...headers, 30 | }, 31 | __ow_path: path, 32 | __ow_isBase64Encoded: false, 33 | }; 34 | } 35 | -------------------------------------------------------------------------------- /www/docs/main/adapters/aws/function-url.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Function URLs 3 | description: See more about how to integrate with AWS Lambda Function URLs 4 | --- 5 | 6 | The [AWS Lambda Function URLs](https://docs.aws.amazon.com/lambda/latest/dg/lambda-urls.html) has the same API interface of [API Gateway V2](./api-gateway-v2), 7 | so you can use the [ApiGatewayV2Adapter](./api-gateway-v2#usage) to support it. 8 | 9 | ```ts title="index.ts" 10 | import { ServerlessAdapter } from '@h4ad/serverless-adapter'; 11 | import { ApiGatewayV2Adapter } from '@h4ad/serverless-adapter/adapters/aws'; 12 | import { DefaultHandler } from '@h4ad/serverless-adapter/handlers/default'; 13 | import app from './app'; 14 | 15 | export const handler = ServerlessAdapter.new(app) 16 | .setHandler(new DefaultHandler()) 17 | // .setFramework(new ExpressFramework()) 18 | // .setResolver(new PromiseResolver()) 19 | .addAdapter(new ApiGatewayV2Adapter()) 20 | // customizing: 21 | // .addAdapter(new ApiGatewayV2Adapter({ stripBasePath: '/prod' })) 22 | .build(); 23 | ``` 24 | 25 | ## AWS Lambda Response Streaming 26 | 27 | To support response streaming, read the docs on [AwsStreamHandler](../../handlers/aws#aws-lambda-response-streaming). 28 | -------------------------------------------------------------------------------- /test/mocks/framework.mock.ts: -------------------------------------------------------------------------------- 1 | //#region Imports 2 | 3 | import type { IncomingMessage, ServerResponse } from 'http'; 4 | import { ObjectReadableMock } from 'stream-mock'; 5 | import { type FrameworkContract } from '../../src'; 6 | 7 | //#endregion 8 | 9 | /** 10 | * The class that represents a mock for framework that forward the request body to the response. 11 | * 12 | * @internal 13 | */ 14 | export class FrameworkMock implements FrameworkContract { 15 | //#region Constructor 16 | 17 | /** 18 | * Construtor padrão 19 | */ 20 | constructor( 21 | protected readonly statusCode: number, 22 | protected readonly mockedResponseData: object, 23 | ) {} 24 | 25 | //#endregion 26 | 27 | /** 28 | * {@inheritDoc} 29 | */ 30 | public sendRequest( 31 | _: null, 32 | _request: IncomingMessage, 33 | response: ServerResponse, 34 | ): void { 35 | const writableOutput = new ObjectReadableMock( 36 | [Buffer.from(JSON.stringify(this.mockedResponseData))], 37 | { 38 | objectMode: true, 39 | }, 40 | ); 41 | 42 | response.statusCode = this.statusCode; 43 | response.setHeader('content-type', 'application/json'); 44 | 45 | writableOutput.pipe(response); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/@types/digital-ocean/digital-ocean-http-event.ts: -------------------------------------------------------------------------------- 1 | //#region Imports 2 | 3 | import type { SingleValueHeaders } from '../headers'; 4 | 5 | //#endregion 6 | 7 | /** 8 | * The interface to represents the values of args send when someone calls a function using HTTP Endpoint. 9 | * To be able to receive this event, inside your `project.yml`, instead of `web: true` change to `web: 'raw'`. 10 | * 11 | * {@link https://www.digitalocean.com/community/questions/digitalocean-functions-how-to-differentiate-query-params-from-body-params | Reference} 12 | * 13 | * @public 14 | * @breadcrumb Types / Digital Ocean / DigitalOceanHttpEvent 15 | */ 16 | export interface DigitalOceanHttpEvent { 17 | /** 18 | * The HTTP Method of the request 19 | */ 20 | __ow_method: string; 21 | 22 | /** 23 | * The query porams of the request 24 | */ 25 | __ow_query: string; 26 | 27 | /** 28 | * The body of the request. 29 | */ 30 | __ow_body?: string; 31 | 32 | /** 33 | * Indicates if body is base64 string 34 | */ 35 | __ow_isBase64Encoded?: boolean; 36 | 37 | /** 38 | * The HTTP Headers of the request 39 | */ 40 | __ow_headers: SingleValueHeaders; 41 | 42 | /** 43 | * The path in the request 44 | */ 45 | __ow_path: string; 46 | } 47 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: 🌈 Feature request 3 | about: Suggest an amazing new idea for this project 4 | title: '' 5 | labels: enhancement 6 | assignees: '' 7 | 8 | --- 9 | 10 | ## Feature Request 11 | 12 | **Is your feature request related to a problem? Please describe.** 13 | 14 | 15 | **Describe the solution you'd like** 16 | 17 | 18 | **Describe alternatives you've considered** 19 | 20 | 21 | ## Are you willing to resolve this issue by submitting a Pull Request? 22 | 23 | 26 | 27 | - [ ] Yes, I have the time, and I know how to start. 28 | - [ ] Yes, I have the time, but I don't know how to start. I would need guidance. 29 | - [ ] No, I don't have the time, although I believe I could do it if I had the time... 30 | - [ ] No, I don't have the time and I wouldn't even know how to start. 31 | 32 | 35 | -------------------------------------------------------------------------------- /test/frameworks/hapi.framework.spec.ts: -------------------------------------------------------------------------------- 1 | import { Server } from '@hapi/hapi'; 2 | import { describe } from 'vitest'; 3 | import { HapiFramework } from '../../src/frameworks/hapi'; 4 | import { type TestRouteBuilderHandler, createTestSuiteFor } from './utils'; 5 | 6 | function createHandler( 7 | method: 'GET' | 'POST' | 'DELETE' | 'PUT', 8 | ): TestRouteBuilderHandler { 9 | return (app, path, handler) => { 10 | app.route({ 11 | method, 12 | path, 13 | handler: (request, h) => { 14 | const [statusCode, resultBody, headers] = handler( 15 | request.headers, 16 | request.payload, 17 | ); 18 | 19 | const response = h.response(resultBody); 20 | 21 | for (const header of Object.keys(headers)) 22 | response?.header(header, headers[header]); 23 | 24 | response.code(statusCode); 25 | 26 | return response; 27 | }, 28 | }); 29 | }; 30 | } 31 | 32 | describe(HapiFramework.name, () => { 33 | createTestSuiteFor( 34 | () => new HapiFramework(), 35 | () => new Server(), 36 | { 37 | get: createHandler('GET'), 38 | delete: createHandler('DELETE'), 39 | post: createHandler('POST'), 40 | put: createHandler('PUT'), 41 | }, 42 | ); 43 | }); 44 | -------------------------------------------------------------------------------- /src/frameworks/body-parser/text-body-parser.framework.ts: -------------------------------------------------------------------------------- 1 | //#region Imports 2 | 3 | import { type OptionsText, text } from 'body-parser'; 4 | import type { FrameworkContract } from '../../contracts'; 5 | import { 6 | BaseBodyParserFramework, 7 | type BodyParserOptions, 8 | } from './base-body-parser.framework'; 9 | 10 | //#endregion 11 | 12 | /** 13 | * The body-parser options for text/plain 14 | * 15 | * @remarks {@link https://github.com/expressjs/body-parser#bodyparsertextoptions} 16 | * 17 | * @breadcrumb Frameworks / BodyParserFramework / TextBodyParserFramework 18 | * @public 19 | */ 20 | export type TextBodyParserFrameworkOptions = OptionsText & BodyParserOptions; 21 | 22 | /** 23 | * The body-parser class used to parse text/plain. 24 | * 25 | * @breadcrumb Frameworks / BodyParserFramework / TextBodyParserFramework 26 | * @public 27 | */ 28 | export class TextBodyParserFramework 29 | extends BaseBodyParserFramework 30 | implements FrameworkContract 31 | { 32 | //#region Constructor 33 | 34 | /** 35 | * Default Constructor 36 | */ 37 | constructor( 38 | framework: FrameworkContract, 39 | options?: TextBodyParserFrameworkOptions, 40 | ) { 41 | super(framework, text(options), options); 42 | } 43 | 44 | //#endregion 45 | } 46 | -------------------------------------------------------------------------------- /src/frameworks/body-parser/json-body-parser.framework.ts: -------------------------------------------------------------------------------- 1 | //#region Imports 2 | 3 | import { type OptionsJson, json } from 'body-parser'; 4 | import { type FrameworkContract } from '../../contracts'; 5 | import { 6 | BaseBodyParserFramework, 7 | type BodyParserOptions, 8 | } from './base-body-parser.framework'; 9 | 10 | //#endregion 11 | 12 | /** 13 | * The body-parser options for application/json 14 | * 15 | * @remarks {@link https://github.com/expressjs/body-parser#bodyparserjsonoptions} 16 | * 17 | * @breadcrumb Frameworks / BodyParserFramework / JsonBodyParserFramework 18 | * @public 19 | */ 20 | export type JsonBodyParserFrameworkOptions = OptionsJson & BodyParserOptions; 21 | 22 | /** 23 | * The body-parser class used to parse application/json. 24 | * 25 | * @breadcrumb Frameworks / BodyParserFramework / JsonBodyParserFramework 26 | * @public 27 | */ 28 | export class JsonBodyParserFramework 29 | extends BaseBodyParserFramework 30 | implements FrameworkContract 31 | { 32 | //#region Constructor 33 | 34 | /** 35 | * Default Constructor 36 | */ 37 | constructor( 38 | framework: FrameworkContract, 39 | options?: JsonBodyParserFrameworkOptions, 40 | ) { 41 | super(framework, json(options), options); 42 | } 43 | 44 | //#endregion 45 | } 46 | -------------------------------------------------------------------------------- /src/frameworks/body-parser/raw-body-parser.framework.ts: -------------------------------------------------------------------------------- 1 | //#region Imports 2 | 3 | import { type Options, raw } from 'body-parser'; 4 | import type { FrameworkContract } from '../../contracts'; 5 | import { 6 | BaseBodyParserFramework, 7 | type BodyParserOptions, 8 | } from './base-body-parser.framework'; 9 | 10 | //#endregion 11 | 12 | /** 13 | * The body-parser options for application/octet-stream 14 | * 15 | * @remarks {@link https://github.com/expressjs/body-parser#bodyparserrawoptions} 16 | * 17 | * @breadcrumb Frameworks / BodyParserFramework / RawBodyParserFramework 18 | * @public 19 | */ 20 | export type RawBodyParserFrameworkOptions = Options & BodyParserOptions; 21 | 22 | /** 23 | * The body-parser class used to parse application/octet-stream. 24 | * 25 | * @breadcrumb Frameworks / BodyParserFramework / RawBodyParserFramework 26 | * @public 27 | */ 28 | export class RawBodyParserFramework 29 | extends BaseBodyParserFramework 30 | implements FrameworkContract 31 | { 32 | //#region Constructor 33 | 34 | /** 35 | * Default Constructor 36 | */ 37 | constructor( 38 | framework: FrameworkContract, 39 | options?: RawBodyParserFrameworkOptions, 40 | ) { 41 | super(framework, raw(options), options); 42 | } 43 | 44 | //#endregion 45 | } 46 | -------------------------------------------------------------------------------- /www/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "docs", 3 | "version": "0.0.0", 4 | "private": true, 5 | "scripts": { 6 | "docusaurus": "docusaurus", 7 | "start": "docusaurus start", 8 | "build": "docusaurus build", 9 | "swizzle": "docusaurus swizzle", 10 | "deploy": "docusaurus deploy", 11 | "clear": "docusaurus clear", 12 | "serve": "docusaurus serve", 13 | "write-translations": "docusaurus write-translations", 14 | "write-heading-ids": "docusaurus write-heading-ids", 15 | "typecheck": "tsc" 16 | }, 17 | "dependencies": { 18 | "@docusaurus/core": "3.0.1", 19 | "@docusaurus/plugin-client-redirects": "3.0.1", 20 | "@docusaurus/preset-classic": "3.0.1", 21 | "@mdx-js/react": "3.0.0", 22 | "clsx": "2.0.0", 23 | "prism-react-renderer": "2.3.1", 24 | "react": "18.2.0", 25 | "react-dom": "18.2.0" 26 | }, 27 | "devDependencies": { 28 | "@docusaurus/module-type-aliases": "3.0.1", 29 | "@tsconfig/docusaurus": "2.0.2", 30 | "typescript": "5.3.3" 31 | }, 32 | "browserslist": { 33 | "production": [ 34 | ">0.5%", 35 | "not dead", 36 | "not op_mini all" 37 | ], 38 | "development": [ 39 | "last 1 chrome version", 40 | "last 1 firefox version", 41 | "last 1 safari version" 42 | ] 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/adapters/dummy/dummy.adapter.ts: -------------------------------------------------------------------------------- 1 | //#region Imports 2 | 3 | import type { 4 | AdapterContract, 5 | AdapterRequest, 6 | OnErrorProps, 7 | } from '../../contracts'; 8 | import { EmptyResponse, type IEmptyResponse } from '../../core'; 9 | 10 | //#endregion 11 | 12 | /** 13 | * The class that represents a dummy adapter that does nothing and can be used by the cloud that doesn't use adapters. 14 | * 15 | * @breadcrumb Adapters / DummyAdapter 16 | * @public 17 | */ 18 | export class DummyAdapter implements AdapterContract { 19 | /** 20 | * {@inheritDoc} 21 | */ 22 | public canHandle(): boolean { 23 | return true; 24 | } 25 | 26 | /** 27 | * {@inheritDoc} 28 | */ 29 | public getAdapterName(): string { 30 | return DummyAdapter.name; 31 | } 32 | 33 | /** 34 | * {@inheritDoc} 35 | */ 36 | public getRequest(): AdapterRequest { 37 | return { 38 | method: 'POST', 39 | body: void 0, 40 | path: '/dummy', 41 | headers: {}, 42 | }; 43 | } 44 | 45 | /** 46 | * {@inheritDoc} 47 | */ 48 | public getResponse(): IEmptyResponse { 49 | return EmptyResponse; 50 | } 51 | 52 | /** 53 | * {@inheritDoc} 54 | */ 55 | public onErrorWhileForwarding(props: OnErrorProps): void { 56 | props.delegatedResolver.succeed(); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json.schemastore.org/tsconfig", 3 | "compilerOptions": { 4 | "outDir": "./lib", 5 | "target": "ES2022", 6 | "module": "ES2022", 7 | "moduleResolution": "Bundler", 8 | "incremental": false, 9 | "noEmit": true, 10 | "noImplicitAny": false, 11 | "verbatimModuleSyntax": true, 12 | "allowUnreachableCode": false, 13 | "allowUnusedLabels": false, 14 | "exactOptionalPropertyTypes": false, 15 | "noImplicitOverride": true, 16 | "allowSyntheticDefaultImports": true, 17 | "alwaysStrict": true, 18 | "declaration": true, 19 | "declarationMap": true, 20 | "esModuleInterop": true, 21 | "importHelpers": false, 22 | "newLine": "lf", 23 | "noEmitHelpers": false, 24 | "noFallthroughCasesInSwitch": true, 25 | "noImplicitReturns": true, 26 | "noUnusedLocals": true, 27 | "noUnusedParameters": true, 28 | "preserveConstEnums": true, 29 | "pretty": true, 30 | "removeComments": false, 31 | "resolveJsonModule": true, 32 | "sourceMap": true, 33 | "strict": true, 34 | "useDefineForClassFields": true 35 | }, 36 | "reflection": true, 37 | "include": [ 38 | "src/**/*.ts", 39 | "test/**/*.ts", 40 | "scripts/**/*.ts" 41 | ], 42 | "exclude": [ 43 | "src/index.doc.ts", 44 | "benchmark/**/*.ts" 45 | ] 46 | } 47 | -------------------------------------------------------------------------------- /test/adapters/test.example: -------------------------------------------------------------------------------- 1 | describe(Adapter.name, () => { 2 | let adapter!: Adapter; 3 | 4 | beforeEach(() => { 5 | adapter = new Adapter(); 6 | }); 7 | 8 | describe('getAdapterName', () => { 9 | it('should be the same name of the class', () => { 10 | expect(adapter.getAdapterName()).toBe(Adapter.name); 11 | }); 12 | }); 13 | 14 | createCanHandleTestsForAdapter(() => new Adapter(), undefined); 15 | 16 | describe('getRequest', () => { 17 | it('should return the correct mapping for the request', () => { 18 | const method = 'PUT'; 19 | const path = '/events'; 20 | const body = { name: 'H4ad Event' }; 21 | }); 22 | }); 23 | 24 | describe('getResponse', () => { 25 | it('should return the correct mapping for the response', () => { 26 | const method = 'PUT'; 27 | const path = '/events'; 28 | const requestBody = { name: 'H4ad Event' }; 29 | 30 | const resultBody = '{"success":true}'; 31 | const resultStatusCode = 200; 32 | const resultIsBase64Encoded = false; 33 | }); 34 | }); 35 | 36 | describe('onErrorWhileForwarding', () => { 37 | it('should resolver call succeed', () => { 38 | const method = 'GET'; 39 | const path = '/events'; 40 | const requestBody = undefined; 41 | }); 42 | }); 43 | }); 44 | -------------------------------------------------------------------------------- /test/core/utils/stream.ts: -------------------------------------------------------------------------------- 1 | // credits to: https://github.com/b4nst/stream-mock/pull/64/files#diff-52aee274967f2fcfa3ffa78ebba2f510dd23d176aa92ccf8c0ad4843373f5ce7 2 | 3 | import { Readable, type ReadableOptions } from 'node:stream'; 4 | import type { IReadableMock } from 'stream-mock'; 5 | 6 | /** 7 | * ErrorReadableMock is a readable stream that mocks error. 8 | * 9 | * @example 10 | * ```typescript 11 | * import { ErrorReadableMock } from 'stream-mock'; 12 | * 13 | * const reader = new ErrorReadble(new Error("mock error")); 14 | * reader.on("data", () => console.log('not called')); 15 | * reader.on("error", e => console.log('called')); 16 | * ``` 17 | * 18 | * @internal 19 | */ 20 | export default class ErrorReadableMock 21 | extends Readable 22 | implements IReadableMock 23 | { 24 | /** 25 | * @param expectedError - error to be passed on callback. 26 | * @param options - Readable stream options. 27 | */ 28 | constructor(expectedError: Error, options: ReadableOptions = {}) { 29 | super(options); 30 | this.expectedError = expectedError; 31 | } 32 | 33 | public it: IterableIterator = [][Symbol.iterator](); 34 | private expectedError: Error; 35 | 36 | // tslint:disable-next-line:function-name Not responsible of this function name 37 | public override _read() { 38 | this.destroy(this.expectedError); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/frameworks/deepkit/http-deepkit.framework.ts: -------------------------------------------------------------------------------- 1 | //#region 2 | 3 | import type { ServerResponse } from 'http'; 4 | import { HttpKernel, HttpResponse, RequestBuilder } from '@deepkit/http'; 5 | import type { FrameworkContract } from '../../contracts'; 6 | import { getFlattenedHeadersMap } from '../../core'; 7 | import { ServerlessRequest } from '../../network'; 8 | 9 | //#endregion 10 | 11 | /** 12 | * The framework that forwards requests to express handler 13 | * 14 | * @breadcrumb Frameworks / HttpDeepkitFramework 15 | * @public 16 | */ 17 | export class HttpDeepkitFramework implements FrameworkContract { 18 | /** 19 | * {@inheritDoc} 20 | */ 21 | public sendRequest( 22 | app: HttpKernel, 23 | request: ServerlessRequest, 24 | response: ServerResponse, 25 | ): void { 26 | const flattenedHeaders = getFlattenedHeadersMap(request.headers); 27 | 28 | let requestBuilder = new RequestBuilder( 29 | request.url!, 30 | request.method, 31 | ).headers(flattenedHeaders); 32 | 33 | if (request.body) { 34 | requestBuilder = Buffer.isBuffer(request.body) 35 | ? requestBuilder.body(request.body) 36 | : requestBuilder.body(Buffer.from(request.body)); 37 | } 38 | 39 | const httpRequest = requestBuilder.build(); 40 | 41 | app.handleRequest(httpRequest, response as HttpResponse); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/index.doc.ts: -------------------------------------------------------------------------------- 1 | export * from './@types'; 2 | export * from './@types/huawei'; 3 | export * from './adapters/apollo-server'; 4 | export * from './adapters/aws'; 5 | export * from './adapters/azure'; 6 | export * from './adapters/digital-ocean'; 7 | export * from './adapters/dummy'; 8 | export * from './adapters/huawei'; 9 | export * from './contracts'; 10 | export * from './core'; 11 | export * from './frameworks/apollo-server'; 12 | export * from './frameworks/body-parser'; 13 | export * from './frameworks/cors'; 14 | export * from './frameworks/deepkit'; 15 | export * from './frameworks/express'; 16 | export * from './frameworks/fastify'; 17 | export * from './frameworks/koa'; 18 | export * from './frameworks/hapi'; 19 | export * from './frameworks/lazy'; 20 | export * from './frameworks/polka'; 21 | export * from './frameworks/trpc'; 22 | export * from './handlers/azure'; 23 | export * from './handlers/aws'; 24 | export * from './handlers/base'; 25 | export * from './handlers/default'; 26 | export * from './handlers/digital-ocean'; 27 | export * from './handlers/firebase'; 28 | export * from './handlers/gcp'; 29 | export * from './handlers/huawei'; 30 | export * from './network'; 31 | export * from './resolvers/aws-context'; 32 | export * from './resolvers/callback'; 33 | export * from './resolvers/promise'; 34 | export * from './resolvers/dummy'; 35 | export * from './serverless-adapter'; 36 | -------------------------------------------------------------------------------- /src/frameworks/body-parser/urlencoded-body-parser.framework.ts: -------------------------------------------------------------------------------- 1 | //#region Imports 2 | 3 | import { type OptionsUrlencoded, urlencoded } from 'body-parser'; 4 | import { type FrameworkContract } from '../../contracts'; 5 | import { 6 | BaseBodyParserFramework, 7 | type BodyParserOptions, 8 | } from './base-body-parser.framework'; 9 | 10 | //#endregion 11 | 12 | /** 13 | * The body parser options for application/x-www-form-urlencoded 14 | * 15 | * @remarks {@link https://github.com/expressjs/body-parser#bodyparserurlencodedoptions} 16 | * 17 | * @breadcrumb Frameworks / BodyParserFramework / UrlencodedBodyParserFramework 18 | * @public 19 | */ 20 | export type UrlencodedBodyParserFrameworkOptions = OptionsUrlencoded & 21 | BodyParserOptions; 22 | 23 | /** 24 | * The body-parser class used to parse application/x-www-form-urlencoded. 25 | * 26 | * @breadcrumb Frameworks / BodyParserFramework / UrlencodedBodyParserFramework 27 | * @public 28 | */ 29 | export class UrlencodedBodyParserFramework 30 | extends BaseBodyParserFramework 31 | implements FrameworkContract 32 | { 33 | //#region Constructor 34 | 35 | /** 36 | * Default Constructor 37 | */ 38 | constructor( 39 | framework: FrameworkContract, 40 | options?: UrlencodedBodyParserFrameworkOptions, 41 | ) { 42 | super(framework, urlencoded(options), options); 43 | } 44 | 45 | //#endregion 46 | } 47 | -------------------------------------------------------------------------------- /www/src/css/custom.css: -------------------------------------------------------------------------------- 1 | /** 2 | * Any CSS included here will be global. The classic template 3 | * bundles Infima by default. Infima is a CSS framework designed to 4 | * work well for content-centric websites. 5 | */ 6 | 7 | /* You can override the default Infima variables here. */ 8 | :root { 9 | --ifm-color-primary: #2e8555; 10 | --ifm-color-primary-dark: #29784c; 11 | --ifm-color-primary-darker: #277148; 12 | --ifm-color-primary-darkest: #205d3b; 13 | --ifm-color-primary-light: #33925d; 14 | --ifm-color-primary-lighter: #359962; 15 | --ifm-color-primary-lightest: #3cad6e; 16 | --ifm-code-font-size: 95%; 17 | } 18 | 19 | /* For readability concerns, you should choose a lighter palette in dark mode. */ 20 | [data-theme='dark'] { 21 | --ifm-color-primary: #25c2a0; 22 | --ifm-color-primary-dark: #21af90; 23 | --ifm-color-primary-darker: #1fa588; 24 | --ifm-color-primary-darkest: #1a8870; 25 | --ifm-color-primary-light: #29d5b0; 26 | --ifm-color-primary-lighter: #32d8b4; 27 | --ifm-color-primary-lightest: #4fddbf; 28 | } 29 | 30 | .docusaurus-highlight-code-line { 31 | background-color: rgba(0, 0, 0, 0.1); 32 | display: block; 33 | margin: 0 calc(-1 * var(--ifm-pre-padding)); 34 | padding: 0 var(--ifm-pre-padding); 35 | } 36 | 37 | [data-theme='dark'] .docusaurus-highlight-code-line { 38 | background-color: rgba(0, 0, 0, 0.3); 39 | } 40 | 41 | html { 42 | scroll-behavior: smooth; 43 | } 44 | -------------------------------------------------------------------------------- /test/handlers/gcp.handler.spec.ts: -------------------------------------------------------------------------------- 1 | import type { IncomingMessage, ServerResponse } from 'http'; 2 | import { describe, expect, it, vitest } from 'vitest'; 3 | import { type FrameworkContract } from '../../src'; 4 | import { GCPHandler } from '../../src/handlers/gcp'; 5 | import { FrameworkMock } from '../mocks/framework.mock'; 6 | 7 | class TestGCPHandler extends GCPHandler { 8 | public override onRequestCallback( 9 | app: TApp, 10 | framework: FrameworkContract, 11 | ): (req: IncomingMessage, res: ServerResponse) => void | Promise { 12 | return super.onRequestCallback(app, framework); 13 | } 14 | } 15 | 16 | describe(GCPHandler.name, () => { 17 | it('should register the callback to the library', () => { 18 | const functionName = 'test'; 19 | const gcpHandler = new TestGCPHandler(functionName); 20 | const mockFramework = new FrameworkMock(204, {}); 21 | 22 | const mockedData = 'Mocked' as any; 23 | const mockedFn = () => mockedData; 24 | 25 | vitest.mock('@google-cloud/functions-framework', () => ({ 26 | http: (name, fn) => { 27 | expect(name).toEqual('test'); 28 | expect(fn).toEqual('Mocked'); 29 | }, 30 | })); 31 | 32 | vitest.spyOn(gcpHandler, 'onRequestCallback').mockImplementation(mockedFn); 33 | 34 | const handler = gcpHandler.getHandler(null, mockFramework); 35 | 36 | expect(handler).toEqual(mockedData); 37 | }); 38 | }); 39 | -------------------------------------------------------------------------------- /src/contracts/handler.contract.ts: -------------------------------------------------------------------------------- 1 | //#region Imports 2 | 3 | import type { BinarySettings } from '../@types'; 4 | import type { ILogger } from '../core'; 5 | import type { AdapterContract } from './adapter.contract'; 6 | import type { FrameworkContract } from './framework.contract'; 7 | import type { ResolverContract } from './resolver.contract'; 8 | 9 | //#endregion 10 | 11 | /** 12 | * The function used to handle serverless requests 13 | * 14 | * @breadcrumb Contracts / HandlerContract 15 | * @public 16 | */ 17 | export type ServerlessHandler = (...args: any[]) => TReturn; 18 | 19 | /** 20 | * The interface that represents the contract between the handler and the real implementation 21 | * 22 | * @breadcrumb Contracts / HandlerContract 23 | * @public 24 | */ 25 | export interface HandlerContract< 26 | TApp, 27 | TEvent, 28 | TContext, 29 | TCallback, 30 | TResponse, 31 | TReturn, 32 | > { 33 | /** 34 | * Get the handler that will handle serverless requests 35 | */ 36 | getHandler( 37 | app: TApp, 38 | framework: FrameworkContract, 39 | adapters: AdapterContract[], 40 | resolverFactory: ResolverContract< 41 | TEvent, 42 | TContext, 43 | TCallback, 44 | TResponse, 45 | TReturn 46 | >, 47 | binarySettings: BinarySettings, 48 | respondWithErrors: boolean, 49 | log: ILogger, 50 | ): ServerlessHandler; 51 | } 52 | -------------------------------------------------------------------------------- /www/docs/main/frameworks/koa.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Koa 3 | description: See more about how to integrate with Koa. 4 | --- 5 | 6 | First, you need to ensure you have the libs installed, so run this code: 7 | 8 | ```bash 9 | npm i --save koa 10 | npm i --save-dev @types/koa 11 | ``` 12 | 13 | Then, you need you just need to use the [KoaFramework](../../api/Frameworks/KoaFramework) when you create your adapter, like: 14 | 15 | ```ts title="index.ts" 16 | import { ServerlessAdapter } from '@h4ad/serverless-adapter'; 17 | import { KoaFramework } from '@h4ad/serverless-adapter/frameworks/koa'; 18 | 19 | const Koa = require('koa'); 20 | 21 | const app = new Koa(); 22 | export const handler = ServerlessAdapter.new(app) 23 | .setFramework(new KoaFramework()) 24 | // continue to set the other options here. 25 | //.setHandler(new DefaultHandler()) 26 | //.setResolver(new PromiseResolver()) 27 | //.addAdapter(new AlbAdapter()) 28 | //.addAdapter(new SQSAdapter()) 29 | //.addAdapter(new SNSAdapter()) 30 | // after put all methods necessary, just call the build method. 31 | .build(); 32 | ``` 33 | 34 | :::tip 35 | 36 | Is your application instance creation asynchronous? Look the [LazyFramework](./helpers/lazy) which helps you in asynchronous startup. 37 | 38 | ::: 39 | 40 | :::tip 41 | 42 | Need to deal with CORS? See [CorsFramework](./helpers/cors) which helps you to add correct headers. 43 | 44 | ::: 45 | -------------------------------------------------------------------------------- /www/docs/main/frameworks/polka.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Polka 3 | description: See more about how to integrate with Polka. 4 | --- 5 | 6 | First, you need to ensure you have the libs installed, so run this code: 7 | 8 | ```bash 9 | npm i --save polka 10 | npm i --save-dev @types/polka 11 | ``` 12 | 13 | Then, you need you just need to use the [PolkaFramework](../../api/Frameworks/PolkaFramework) when you create your adapter, like: 14 | 15 | ```ts title="index.ts" 16 | import { ServerlessAdapter } from '@h4ad/serverless-adapter'; 17 | import { PolkaFramework } from '@h4ad/serverless-adapter/frameworks/polka'; 18 | 19 | const Polka = require('Polka'); 20 | 21 | const app = Polka(); 22 | export const handler = ServerlessAdapter.new(app) 23 | .setFramework(new PolkaFramework()) 24 | // continue to set the other options here. 25 | //.setHandler(new DefaultHandler()) 26 | //.setResolver(new PromiseResolver()) 27 | //.addAdapter(new AlbAdapter()) 28 | //.addAdapter(new SQSAdapter()) 29 | //.addAdapter(new SNSAdapter()) 30 | // after put all methods necessary, just call the build method. 31 | .build(); 32 | ``` 33 | 34 | :::tip 35 | 36 | Is your application instance creation asynchronous? Look the [LazyFramework](./helpers/lazy) which helps you in asynchronous startup. 37 | 38 | ::: 39 | 40 | :::tip 41 | 42 | Need to deal with CORS? See [CorsFramework](./helpers/cors) which helps you to add correct headers. 43 | 44 | ::: 45 | -------------------------------------------------------------------------------- /www/docs/main/frameworks/hapi.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Hapi 3 | description: See more about how to integrate with Hapi. 4 | --- 5 | 6 | First, you need to ensure you have the libs installed, so run this code: 7 | 8 | ```bash 9 | npm i --save @hapi/hapi 10 | npm i --save-dev @types/hapi 11 | ``` 12 | 13 | Then, you need you just need to use the [HapiFramework](../../api/Frameworks/HapiFramework) when you create your adapter, like: 14 | 15 | ```ts title="index.ts" 16 | import { ServerlessAdapter } from '@h4ad/serverless-adapter'; 17 | import { HapiFramework } from '@h4ad/serverless-adapter/frameworks/hapi'; 18 | 19 | const Hapi = require('@hapi/hapi'); 20 | 21 | const app = Hapi.server(); 22 | export const handler = ServerlessAdapter.new(app) 23 | .setFramework(new HapiFramework()) 24 | // continue to set the other options here. 25 | //.setHandler(new DefaultHandler()) 26 | //.setResolver(new PromiseResolver()) 27 | //.addAdapter(new AlbAdapter()) 28 | //.addAdapter(new SQSAdapter()) 29 | //.addAdapter(new SNSAdapter()) 30 | // after put all methods necessary, just call the build method. 31 | .build(); 32 | ``` 33 | 34 | :::tip 35 | 36 | Is your application instance creation asynchronous? Look the [LazyFramework](./helpers/lazy) which helps you in asynchronous startup. 37 | 38 | ::: 39 | 40 | :::tip 41 | 42 | Need to deal with CORS? See [CorsFramework](./helpers/cors) which helps you to add correct headers. 43 | 44 | ::: 45 | -------------------------------------------------------------------------------- /test/issues/alb-express-static/alb-express-static.spec.ts: -------------------------------------------------------------------------------- 1 | import { resolve } from 'path'; 2 | import type { ALBResult } from 'aws-lambda'; 3 | import express from 'express'; 4 | import { describe, expect, it } from 'vitest'; 5 | import { ServerlessAdapter } from '../../../src'; 6 | import { AlbAdapter } from '../../../src/adapters/aws'; 7 | import { ExpressFramework } from '../../../src/frameworks/express'; 8 | import { DefaultHandler } from '../../../src/handlers/default'; 9 | import { PromiseResolver } from '../../../src/resolvers/promise'; 10 | import { createAlbEvent } from '../../adapters/aws/utils/alb-event'; 11 | 12 | describe('ALB rejecting response when uses express.static because', () => { 13 | it('returns some headers that are not string', async () => { 14 | const app = express(); 15 | 16 | app.use(express.static(resolve(__dirname))); 17 | 18 | const handler = ServerlessAdapter.new(app) 19 | .setHandler(new DefaultHandler()) 20 | .setFramework(new ExpressFramework()) 21 | .setResolver(new PromiseResolver()) 22 | .addAdapter(new AlbAdapter()) 23 | .build(); 24 | 25 | const albEvent = createAlbEvent('GET', '/robots.txt'); 26 | 27 | const response = (await handler(albEvent, {})) as ALBResult; 28 | 29 | for (const header of Object.keys(response.headers || {})) { 30 | expect(`typeof ${header}: ${typeof response.headers![header]}`).toBe( 31 | `typeof ${header}: string`, 32 | ); 33 | } 34 | }); 35 | }); 36 | -------------------------------------------------------------------------------- /src/handlers/gcp/gcp.handler.ts: -------------------------------------------------------------------------------- 1 | //#region Imports 2 | 3 | import type { IncomingMessage, ServerResponse } from 'http'; 4 | import { http } from '@google-cloud/functions-framework'; 5 | import type { FrameworkContract, HandlerContract } from '../../contracts'; 6 | import { RawRequest } from '../base'; 7 | 8 | //#endregion 9 | 10 | /** 11 | * The class that implements a handler for GCP Http Functions 12 | * 13 | * @remarks Read more about Http Cloud Function {@link https://cloud.google.com/functions/docs/create-deploy-http-nodejs | here} 14 | * 15 | * @breadcrumb Handlers / GCPHandler 16 | * @public 17 | */ 18 | export class GCPHandler 19 | extends RawRequest 20 | implements 21 | HandlerContract> 22 | { 23 | //#region Constructor 24 | 25 | /** 26 | * Default Constructor 27 | * 28 | * @param name - The name of this function, should be the during deploy. 29 | */ 30 | constructor(protected readonly name: string) { 31 | super(); 32 | } 33 | 34 | //#endregion 35 | 36 | //#region Public Methods 37 | 38 | /** 39 | * {@inheritDoc} 40 | */ 41 | public getHandler( 42 | app: TApp, 43 | framework: FrameworkContract, 44 | ): (req: IncomingMessage, res: ServerResponse) => void | Promise { 45 | const callback = this.onRequestCallback(app, framework); 46 | 47 | http(this.name, callback); 48 | 49 | return callback; 50 | } 51 | 52 | //#endregion 53 | } 54 | -------------------------------------------------------------------------------- /scripts/libs/CustomUtilities.ts: -------------------------------------------------------------------------------- 1 | import { ApiItem, ApiParameterListMixin } from '@microsoft/api-extractor-model'; 2 | 3 | export class CustomUtilities { 4 | private static readonly _badFilenameCharsRegExp: RegExp = /[^a-z0-9_\-.]/gi; 5 | 6 | /** 7 | * Generates a concise signature for a function. Example: "getArea(width, height)" 8 | */ 9 | public static getConciseSignature(apiItem: ApiItem): string { 10 | if (ApiParameterListMixin.isBaseClassOf(apiItem)) { 11 | return ( 12 | apiItem.displayName + 13 | '(' + 14 | apiItem.parameters.map(x => x.name).join(', ') + 15 | ')' 16 | ); 17 | } 18 | return apiItem.displayName; 19 | } 20 | 21 | /** 22 | * Converts bad filename characters to underscores. 23 | */ 24 | public static getSafeFilenameForName(name: string): string { 25 | // TODO: This can introduce naming collisions. 26 | // We will fix that as part of https://github.com/microsoft/rushstack/issues/1308 27 | return name 28 | .replace(CustomUtilities._badFilenameCharsRegExp, '_') 29 | .toLowerCase(); 30 | } 31 | 32 | /** 33 | * Converts bad filename characters to underscores. 34 | */ 35 | public static getSafeFilenameForNameWithCase(name: string): string { 36 | // TODO: This can introduce naming collisions. 37 | // We will fix that as part of https://github.com/microsoft/rushstack/issues/1308 38 | return name.replace(CustomUtilities._badFilenameCharsRegExp, '_'); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/core/constants.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Default encodings that are treated as binary, they are compared with the `Content-Encoding` header. 3 | * 4 | * @breadcrumb Core / Constants 5 | * @defaultValue ['gzip', 'deflate', 'br'] 6 | * @public 7 | */ 8 | export const DEFAULT_BINARY_ENCODINGS: string[] = ['gzip', 'deflate', 'br']; 9 | 10 | /** 11 | * Default content types that are treated as binary, they are compared with the `Content-Type` header. 12 | * 13 | * @breadcrumb Core / Constants 14 | * @defaultValue ['image/png', 'image/jpeg', 'image/jpg', 'image/avif', 'image/bmp', 'image/x-png', 'image/gif', 'image/webp', 'video/mp4', 'application/pdf'] 15 | * @public 16 | */ 17 | export const DEFAULT_BINARY_CONTENT_TYPES: string[] = [ 18 | 'image/png', 19 | 'image/jpeg', 20 | 'image/jpg', 21 | 'image/avif', 22 | 'image/bmp', 23 | 'image/x-png', 24 | 'image/gif', 25 | 'image/webp', 26 | 'video/mp4', 27 | 'application/pdf', 28 | ]; 29 | 30 | /** 31 | * Type alias for empty response and can be used on some adapters when the adapter does not need to return a response. 32 | * 33 | * @breadcrumb Core / Constants 34 | * @public 35 | */ 36 | // eslint-disable-next-line @typescript-eslint/ban-types 37 | export type IEmptyResponse = {}; 38 | 39 | /** 40 | * Constant for empty response and can be used on some adapters when the adapter does not need to return a response. 41 | * 42 | * @breadcrumb Core / Constants 43 | * @public 44 | */ 45 | export const EmptyResponse: IEmptyResponse = {}; 46 | -------------------------------------------------------------------------------- /www/src/pages/index.tsx: -------------------------------------------------------------------------------- 1 | import Link from '@docusaurus/Link'; 2 | import useDocusaurusContext from '@docusaurus/useDocusaurusContext'; 3 | import HomepageFeatures from '@site/src/components/HomepageFeatures'; 4 | import HowToStart from '@site/src/components/HowToStart'; 5 | import Layout from '@theme/Layout'; 6 | import clsx from 'clsx'; 7 | import React from 'react'; 8 | import styles from './index.module.css'; 9 | 10 | function HomepageHeader() { 11 | const { siteConfig } = useDocusaurusContext(); 12 | return ( 13 |
14 |
15 |

{siteConfig.title}

16 |

{siteConfig.tagline}

17 |
18 | 22 | Introduction 23 | 24 |
25 |
26 |
27 | ); 28 | } 29 | 30 | export default function Home(): JSX.Element { 31 | const { siteConfig } = useDocusaurusContext(); 32 | return ( 33 | 37 | 38 |
39 | 40 | 41 |
42 |
43 | ); 44 | } 45 | -------------------------------------------------------------------------------- /www/docs/main/frameworks/fastify.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Fastify 3 | description: See more about how to integrate with Fastify. 4 | --- 5 | 6 | > Supported versions: v4 and v5 7 | 8 | First, you need to ensure you have the libs installed, so run this code: 9 | 10 | ```bash 11 | npm i --save fastify 12 | ``` 13 | 14 | Then, you need you just need to use the [FastifyFramework](../../api/Frameworks/FastifyFramework) when you create your adapter, like: 15 | 16 | ```ts title="index.ts" 17 | import { ServerlessAdapter } from '@h4ad/serverless-adapter'; 18 | import { FastifyFramework } from '@h4ad/serverless-adapter/frameworks/fastify'; 19 | 20 | const Fastify = require('fastify'); 21 | 22 | const app = Fastify({ logger: true }); 23 | export const handler = ServerlessAdapter.new(app) 24 | .setFramework(new FastifyFramework()) 25 | // continue to set the other options here. 26 | //.setHandler(new DefaultHandler()) 27 | //.setResolver(new PromiseResolver()) 28 | //.addAdapter(new AlbAdapter()) 29 | //.addAdapter(new SQSAdapter()) 30 | //.addAdapter(new SNSAdapter()) 31 | // after put all methods necessary, just call the build method. 32 | .build(); 33 | ``` 34 | 35 | :::tip 36 | 37 | Is your application instance creation asynchronous? Look the [LazyFramework](./helpers/lazy) which helps you in asynchronous startup. 38 | 39 | ::: 40 | 41 | :::tip 42 | 43 | Need to deal with CORS? See [CorsFramework](./helpers/cors) which helps you to add correct headers. 44 | 45 | ::: 46 | 47 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | permissions: 9 | contents: write 10 | pull-requests: write 11 | id-token: write 12 | 13 | jobs: 14 | release-please: 15 | runs-on: ubuntu-latest 16 | steps: 17 | - name: Checkout Code 18 | uses: actions/checkout@v4 19 | 20 | - name: Release Please 21 | uses: google-github-actions/release-please-action@v4 22 | id: release 23 | 24 | - name: Use Node 20.x 25 | uses: actions/setup-node@v4 26 | if: ${{ steps.release.outputs.release_created }} 27 | with: 28 | node-version: 20 29 | registry-url: 'https://registry.npmjs.org' 30 | 31 | - name: Install Deps 32 | run: npm ci 33 | if: ${{ steps.release.outputs.release_created }} 34 | 35 | - name: Build 36 | run: npm run build 37 | if: ${{ steps.release.outputs.release_created }} 38 | 39 | - name: Test 40 | run: npm run test 41 | if: ${{ steps.release.outputs.release_created }} 42 | 43 | - name: Get Coverage Info 44 | if: ${{ steps.release.outputs.release_created }} 45 | uses: codecov/codecov-action@v4 46 | env: 47 | CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} 48 | 49 | - name: NPM Publish 50 | run: npm publish --provenance --access public 51 | if: ${{ steps.release.outputs.release_created }} 52 | env: 53 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 54 | -------------------------------------------------------------------------------- /www/docs/main/frameworks/express.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Express 3 | description: See more about how to integrate with Express. 4 | --- 5 | 6 | > Supported versions: v4 and v5 7 | 8 | First, you need to ensure you have the libs installed, so run this code: 9 | 10 | ```bash 11 | npm i --save express 12 | npm i --save-dev @types/express 13 | ``` 14 | 15 | Then, you need you just need to use the [ExpressFramework](../../api/Frameworks/ExpressFramework) when you create your adapter, like: 16 | 17 | ```ts title="index.ts" 18 | import { ServerlessAdapter } from '@h4ad/serverless-adapter'; 19 | import { ExpressFramework } from '@h4ad/serverless-adapter/frameworks/express'; 20 | 21 | const express = require('express'); 22 | 23 | const app = express(); 24 | export const handler = ServerlessAdapter.new(app) 25 | .setFramework(new ExpressFramework()) 26 | // continue to set the other options here. 27 | //.setHandler(new DefaultHandler()) 28 | //.setResolver(new PromiseResolver()) 29 | //.addAdapter(new AlbAdapter()) 30 | //.addAdapter(new SQSAdapter()) 31 | //.addAdapter(new SNSAdapter()) 32 | // after put all methods necessary, just call the build method. 33 | .build(); 34 | ``` 35 | 36 | :::tip 37 | 38 | Is your application instance creation asynchronous? Look the [LazyFramework](./helpers/lazy) which helps you in asynchronous startup. 39 | 40 | ::: 41 | 42 | :::tip 43 | 44 | Need to deal with CORS? See [CorsFramework](./helpers/cors) which helps you to add correct headers. 45 | 46 | ::: 47 | -------------------------------------------------------------------------------- /test/adapters/utils/can-handle.ts: -------------------------------------------------------------------------------- 1 | import { beforeEach, describe, expect, it } from 'vitest'; 2 | import type { AdapterContract, ILogger } from '../../../src'; 3 | import { allEvents } from './events'; 4 | 5 | export function createCanHandleTestsForAdapter< 6 | T, 7 | TContext = any, 8 | TResponse = any, 9 | >( 10 | adapterFactory: () => AdapterContract, 11 | context: TContext, 12 | logger: ILogger = {} as ILogger, 13 | ): void { 14 | let adapter!: AdapterContract; 15 | 16 | beforeEach(() => { 17 | adapter = adapterFactory(); 18 | }); 19 | 20 | describe('canHandle', () => { 21 | it('should return true when is valid event', () => { 22 | const events = allEvents.filter( 23 | ([adapterName]) => adapterName === adapter.getAdapterName(), 24 | )!; 25 | 26 | expect(events.length).toBeGreaterThan(0); 27 | 28 | for (const [, event] of events) 29 | expect(adapter.canHandle(event, context, logger)).toBe(true); 30 | }); 31 | 32 | it('should return false when is not a valid event', () => { 33 | const events = allEvents.filter( 34 | ([adapterName]) => adapterName !== adapter.getAdapterName(), 35 | ); 36 | 37 | expect(events.length).toBeGreaterThan(0); 38 | 39 | for (const [adapterName, event] of events) { 40 | const canHandle = adapter.canHandle(event, context, logger); 41 | 42 | expect(`${adapterName}: ${canHandle}`).toEqual(`${adapterName}: false`); 43 | } 44 | }); 45 | }); 46 | } 47 | -------------------------------------------------------------------------------- /test/adapters/aws/utils/event-bridge.ts: -------------------------------------------------------------------------------- 1 | import type { EventBridgeEvent } from 'aws-lambda'; 2 | 3 | /** 4 | * Sample event from {@link https://docs.aws.amazon.com/lambda/latest/dg/services-cloudwatchevents.html} 5 | */ 6 | export function createEventBridgeEvent(): EventBridgeEvent { 7 | return { 8 | version: '0', 9 | id: 'fe8d3c65-xmpl-c5c3-2c87-81584709a377', 10 | 'detail-type': 'RDS DB Instance Event', 11 | source: 'aws.rds', 12 | account: '123456789012', 13 | time: '2020-04-28T07:20:20Z', 14 | region: 'us-east-2', 15 | resources: ['arn:aws:rds:us-east-2:123456789012:db:rdz6xmpliljlb1'], 16 | detail: { 17 | EventCategories: ['backup'], 18 | SourceType: 'DB_INSTANCE', 19 | SourceArn: 'arn:aws:rds:us-east-2:123456789012:db:rdz6xmpliljlb1', 20 | Date: '2020-04-28T07:20:20.112Z', 21 | Message: 'Finished DB Instance backup', 22 | SourceIdentifier: 'rdz6xmpliljlb1', 23 | }, 24 | }; 25 | } 26 | 27 | /** 28 | * Sample event from {@link https://docs.aws.amazon.com/lambda/latest/dg/services-cloudwatchevents.html} 29 | */ 30 | export function createEventBridgeEventSimple(): EventBridgeEvent { 31 | return { 32 | version: '0', 33 | account: '123456789012', 34 | region: 'us-east-2', 35 | detail: {}, 36 | 'detail-type': 'Scheduled Event', 37 | source: 'aws.events', 38 | time: '2019-03-01T01:23:45Z', 39 | id: 'cdc73f9d-aea9-11e3-9d5a-835b769c0d9c', 40 | resources: ['arn:aws:events:us-east-2:123456789012:rule/my-schedule'], 41 | }; 42 | } 43 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 7 | 8 | ### Description of change 9 | 10 | 23 | 24 | ### Pull-Request Checklist 25 | 26 | 31 | 32 | - [ ] Code is up-to-date with the `main` branch 33 | - [ ] `npm run lint` passes with this change 34 | - [ ] `npm run test` passes with this change 35 | - [ ] This pull request links relevant issues as `Fixes #0000` 36 | - [ ] There are new or updated unit tests validating the change 37 | - [ ] Added documentation inside `www/docs/main` folder. 38 | - [ ] Included new files inside `index.doc.ts`. 39 | - [ ] The new commits follow conventions outlined in the [conventional commit spec](https://www.conventionalcommits.org/en/v1.0.0/) 40 | 41 | 44 | -------------------------------------------------------------------------------- /test/adapters/aws/utils/s3.ts: -------------------------------------------------------------------------------- 1 | import type { S3Event } from 'aws-lambda'; 2 | 3 | /** 4 | * Sample event from {@link https://docs.aws.amazon.com/pt_br/lambda/latest/dg/with-s3.html} 5 | */ 6 | export function createS3Event(): S3Event { 7 | return { 8 | Records: [ 9 | { 10 | eventVersion: '2.1', 11 | eventSource: 'aws:s3', 12 | awsRegion: 'us-east-2', 13 | eventTime: '2019-09-03T19:37:27.192Z', 14 | eventName: 'ObjectCreated:Put', 15 | userIdentity: { 16 | principalId: 'AWS:AIDAINPONIXQXHT3IKHL2', 17 | }, 18 | requestParameters: { 19 | sourceIPAddress: '205.255.255.255', 20 | }, 21 | responseElements: { 22 | 'x-amz-request-id': 'D82B88E5F771F645', 23 | 'x-amz-id-2': 24 | 'vlR7PnpV2Ce81l0PRw6jlUpck7Jo5ZsQjryTjKlc5aLWGVHPZLj5NeC6qMa0emYBDXOo6QBU0Wo=', 25 | }, 26 | s3: { 27 | s3SchemaVersion: '1.0', 28 | configurationId: '828aa6fc-f7b5-4305-8584-487c791949c1', 29 | bucket: { 30 | name: 'DOC-EXAMPLE-BUCKET', 31 | ownerIdentity: { 32 | principalId: 'A3I5XTEXAMAI3E', 33 | }, 34 | arn: 'arn:aws:s3:::lambda-artifacts-deafc19498e3f2df', 35 | }, 36 | object: { 37 | key: 'b21b84d653bb07b05b1e6b33684dc11b', 38 | size: 1305107, 39 | eTag: 'b21b84d653bb07b05b1e6b33684dc11b', 40 | sequencer: '0C0F6F405D6ED209E1', 41 | }, 42 | }, 43 | }, 44 | ], 45 | }; 46 | } 47 | -------------------------------------------------------------------------------- /.github/workflows/docs.yml: -------------------------------------------------------------------------------- 1 | name: Deploy to GitHub Pages 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | paths: 8 | - '.github/workflows/docs.yml' 9 | - 'www/**' 10 | - 'src/**' 11 | - 'scripts/**' 12 | workflow_dispatch: 13 | 14 | jobs: 15 | deploy: 16 | runs-on: ubuntu-latest 17 | permissions: 18 | contents: write 19 | concurrency: 20 | group: ${{ github.workflow }}-${{ github.ref }} 21 | steps: 22 | - uses: actions/checkout@v4 23 | 24 | - name: Setup Node 25 | uses: actions/setup-node@v4 26 | with: 27 | node-version: '18.x' 28 | 29 | - name: Cache dependencies 30 | uses: actions/cache@v3 31 | with: 32 | path: ~/.npm 33 | key: ${{ runner.os }}-node-18-${{ hashFiles('**/package-lock.json') }} 34 | restore-keys: | 35 | ${{ runner.os }}-node-18- 36 | 37 | - name: Install Lib Dependencies 38 | run: npm ci 39 | 40 | - name: Install Docs Dependencies 41 | run: npm ci 42 | working-directory: www 43 | 44 | - name: Parse TSDoc Documentation 45 | run: npm run docs:generate 46 | 47 | - name: Build Docs 48 | run: npm run build 49 | working-directory: www 50 | 51 | - name: Deploy 52 | uses: peaceiris/actions-gh-pages@v3 53 | if: ${{ github.ref == 'refs/heads/main' }} 54 | with: 55 | github_token: ${{ secrets.GITHUB_TOKEN }} 56 | publish_dir: ./www/build 57 | 58 | -------------------------------------------------------------------------------- /src/@types/binary-settings.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * The interface representing the binary settings implementation by function 3 | * 4 | * @breadcrumb Types / BinarySettings 5 | * @public 6 | */ 7 | export interface BinarySettingsFunction { 8 | /** 9 | * This property can be a function that receives the response headers and returns whether that response should be encoded as binary. 10 | * Otherwise, you can specify not to treat any response as binary by putting `false` in this property. 11 | * 12 | * @remarks Setting this property prevents the `contentTypes` and `contentEncodings` properties from being used. 13 | */ 14 | isBinary: 15 | | ((headers: Record) => boolean) 16 | | false; 17 | } 18 | 19 | /** 20 | * The interface representing the binary settings implementation by looking inside the headers 21 | * 22 | * @breadcrumb Types / BinarySettings 23 | * @public 24 | */ 25 | export interface BinarySettingsContentHeaders { 26 | /** 27 | * The list of content types that will be treated as binary 28 | */ 29 | contentTypes: string[]; 30 | 31 | /** 32 | * The list of content encodings that will be treated as binary 33 | */ 34 | contentEncodings: string[]; 35 | } 36 | 37 | /** 38 | * The interface representing the settings for whether the response should be treated as binary or not 39 | * 40 | * @remarks Encoded as binary means the response body will be converted to base64 41 | * 42 | * @breadcrumb Types / BinarySettings 43 | * @public 44 | */ 45 | export type BinarySettings = 46 | | BinarySettingsFunction 47 | | BinarySettingsContentHeaders; 48 | -------------------------------------------------------------------------------- /scripts/libs/MarkdownEmitter.ts: -------------------------------------------------------------------------------- 1 | import { CustomMarkdownEmitter } from '@microsoft/api-documenter/lib/markdown/CustomMarkdownEmitter'; 2 | import type { IMarkdownEmitterContext } from '@microsoft/api-documenter/lib/markdown/MarkdownEmitter'; 3 | import { IndentedWriter } from '@microsoft/api-documenter/lib/utils/IndentedWriter'; 4 | 5 | export class MarkdownEmitter extends CustomMarkdownEmitter { 6 | protected override writePlainText( 7 | text: string, 8 | context: IMarkdownEmitterContext, 9 | ): void { 10 | const writer: IndentedWriter = context.writer; 11 | 12 | // split out the [ leading whitespace, content, trailing whitespace ] 13 | const parts: string[] = text.match(/^(\s*)(.*?)(\s*)$/) || []; 14 | 15 | writer.write(parts[1]); // write leading whitespace 16 | 17 | const middle: string = parts[2]; 18 | 19 | if (middle !== '') { 20 | switch (writer.peekLastCharacter()) { 21 | case '': 22 | case '\n': 23 | case ' ': 24 | case '[': 25 | case '>': 26 | // okay to put a symbol 27 | break; 28 | default: 29 | // This is no problem: "**one** *two* **three**" 30 | // But this is trouble: "**one***two***three**" 31 | // The most general solution: "**one***two***three**" 32 | // but the solution above breaks docusaurus, so, I changed to space 33 | writer.write(' '); 34 | break; 35 | } 36 | 37 | writer.write(this.getEscapedText(middle)); 38 | } 39 | 40 | writer.write(parts[3]); // write trailing whitespace 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/handlers/digital-ocean/digital-ocean.handler.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/unbound-method */ 2 | //#region Imports 3 | 4 | import type { BinarySettings } from '../../@types'; 5 | import type { DigitalOceanHttpEvent } from '../../@types/digital-ocean'; 6 | import type { 7 | AdapterContract, 8 | FrameworkContract, 9 | ResolverContract, 10 | ServerlessHandler, 11 | } from '../../contracts'; 12 | import type { ILogger } from '../../core'; 13 | import { DefaultHandler } from '../default'; 14 | 15 | //#endregion 16 | 17 | /** 18 | * The class that implements a serverless handler for Digital Ocean Functions. 19 | * 20 | * @breadcrumb Handlers / DigitalOceanHandler 21 | * @public 22 | */ 23 | export class DigitalOceanHandler< 24 | TApp, 25 | TEvent, 26 | TResponse, 27 | TReturn, 28 | > extends DefaultHandler { 29 | /** 30 | * {@inheritDoc} 31 | */ 32 | public override getHandler( 33 | app: TApp, 34 | framework: FrameworkContract, 35 | adapters: AdapterContract[], 36 | resolverFactory: ResolverContract, 37 | binarySettings: BinarySettings, 38 | respondWithErrors: boolean, 39 | log: ILogger, 40 | ): ServerlessHandler { 41 | const defaultHandler = super.getHandler( 42 | app, 43 | framework, 44 | adapters, 45 | resolverFactory, 46 | binarySettings, 47 | respondWithErrors, 48 | log, 49 | ); 50 | 51 | return (event: DigitalOceanHttpEvent) => 52 | defaultHandler(event, undefined, undefined); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /test/adapters/aws/utils/sns.ts: -------------------------------------------------------------------------------- 1 | import type { SNSEvent } from 'aws-lambda'; 2 | 3 | /** 4 | * Sample event from {@link https://docs.aws.amazon.com/lambda/latest/dg/with-sns.html} 5 | */ 6 | export function createSNSEvent(): SNSEvent { 7 | return { 8 | Records: [ 9 | { 10 | EventVersion: '1.0', 11 | EventSubscriptionArn: 12 | 'arn:aws:sns:us-east-2:123456789012:sns-lambda:21be56ed-a058-49f5-8c98-aedd2564c486', 13 | EventSource: 'aws:sns', 14 | Sns: { 15 | SignatureVersion: '1', 16 | Timestamp: '2019-01-02T12:45:07.000Z', 17 | Signature: 18 | 'tcc6faL2yUC6dgZdmrwh1Y4cGa/ebXEkAi6RibDsvpi+tE/1+82j...65r==', 19 | SigningCertUrl: 20 | 'https://sns.us-east-2.amazonaws.com/SimpleNotificationService-ac565b8b1a6c5d002d285f9598aa1d9b.pem', 21 | MessageId: '95df01b4-ee98-5cb9-9903-4c221d41eb5e', 22 | Message: 'Hello from SNS!', 23 | MessageAttributes: { 24 | Test: { 25 | Type: 'String', 26 | Value: 'TestString', 27 | }, 28 | TestBinary: { 29 | Type: 'Binary', 30 | Value: 'TestBinary', 31 | }, 32 | }, 33 | Type: 'Notification', 34 | UnsubscribeUrl: 35 | 'https://sns.us-east-2.amazonaws.com/?Action=Unsubscribe&SubscriptionArn=arn:aws:sns:us-east-2:123456789012:test-lambda:21be56ed-a058-49f5-8c98-aedd2564c486', 36 | TopicArn: 'arn:aws:sns:us-east-2:123456789012:sns-lambda', 37 | Subject: 'TestInvoke', 38 | }, 39 | }, 40 | ], 41 | }; 42 | } 43 | -------------------------------------------------------------------------------- /www/src/components/HomepageFeatures/index.tsx: -------------------------------------------------------------------------------- 1 | import clsx from 'clsx'; 2 | import React from 'react'; 3 | import styles from './styles.module.css'; 4 | 5 | type FeatureItem = { 6 | title: string; 7 | description: JSX.Element; 8 | }; 9 | 10 | const FeatureList: FeatureItem[] = [ 11 | { 12 | title: 'Easy to Use & Extensible', 13 | description: ( 14 | <>The library was designed to be very extensible and easy to use. 15 | ), 16 | }, 17 | { 18 | title: 'One library, many Serverless environments', 19 | description: ( 20 | <> 21 | We currently support AWS, Azure, Firebase, Digital Ocean, Google Cloud Functions and Huawei. 22 | 23 | ), 24 | }, 25 | { 26 | title: 'Fully Typed & Tested', 27 | description: ( 28 | <> 29 | The entire library was written with typescript to give the developer the 30 | best experience and we have 100% coverage. 31 | 32 | ), 33 | }, 34 | ]; 35 | 36 | function Feature({ title, description }: FeatureItem) { 37 | return ( 38 |
39 |
40 |

{title}

41 |

{description}

42 |
43 |
44 | ); 45 | } 46 | 47 | export default function HomepageFeatures(): JSX.Element { 48 | return ( 49 |
50 |
51 |
52 | {FeatureList.map((props, idx) => ( 53 | 54 | ))} 55 |
56 |
57 |
58 | ); 59 | } 60 | -------------------------------------------------------------------------------- /www/src/components/BrowserWindow/index.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Facebook, Inc. and its affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | import clsx from 'clsx'; 9 | import React, { type ReactNode } from 'react'; 10 | 11 | import styles from './styles.module.css'; 12 | 13 | interface Props { 14 | children: ReactNode; 15 | minHeight: number; 16 | url: string; 17 | } 18 | 19 | export default function BrowserWindow({ 20 | children, 21 | minHeight, 22 | url = 'http://localhost:3000', 23 | }: Props): JSX.Element { 24 | return ( 25 |
26 |
27 |
28 | 29 | 30 | 31 |
32 | 37 |
38 |
39 | 40 | 41 | 42 |
43 |
44 |
45 | 46 |
{children}
47 |
48 | ); 49 | } 50 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | name: CodeQL 2 | 3 | on: 4 | push: 5 | paths: 6 | - 'src/**' 7 | - 'package-lock.json' 8 | - 'package.json' 9 | - 'tsconfig.json' 10 | - 'tsconfig.*.json' 11 | - 'vite.config.ts' 12 | branches: 13 | - main 14 | pull_request: 15 | paths: 16 | - 'src/**' 17 | - 'package-lock.json' 18 | - 'package.json' 19 | - 'tsconfig.json' 20 | - 'tsconfig.*.json' 21 | - 'vite.config.ts' 22 | branches: 23 | - main 24 | schedule: 25 | # "At 10:00 UTC (03:00 PT) on Monday" https://crontab.guru/#0_10_*_*_1 26 | - cron: "0 10 * * 1" 27 | 28 | jobs: 29 | analyze: 30 | name: Analyze 31 | runs-on: ubuntu-latest 32 | timeout-minutes: 120 33 | permissions: 34 | actions: read 35 | contents: read 36 | security-events: write 37 | strategy: 38 | fail-fast: false 39 | matrix: 40 | language: [ 'javascript-typescript' ] 41 | steps: 42 | - name: Checkout 43 | uses: actions/checkout@v4 44 | 45 | - name: Setup Git User 46 | run: | 47 | git config --global user.email "h4ad+bot@viniciusl.com.br" 48 | git config --global user.name "H4ad CLI robot" 49 | 50 | - name: Initialize CodeQL 51 | uses: github/codeql-action/init@v3 52 | with: 53 | languages: ${{ matrix.language }} 54 | 55 | - name: Autobuild 56 | uses: github/codeql-action/autobuild@v3 57 | 58 | - name: Perform CodeQL Analysis 59 | uses: github/codeql-action/analyze@v3 60 | with: 61 | category: "/language:${{matrix.language}}" 62 | -------------------------------------------------------------------------------- /src/resolvers/promise/promise.resolver.ts: -------------------------------------------------------------------------------- 1 | //#region Imports 2 | 3 | import type { 4 | DelegatedResolver, 5 | Resolver, 6 | ResolverContract, 7 | ResolverProps, 8 | } from '../../contracts'; 9 | 10 | //#endregion 11 | 12 | /** 13 | * The class that implements the resolver using the promise object sent by this library 14 | * 15 | * @breadcrumb Resolvers / PromiseResolver 16 | * @public 17 | */ 18 | export class PromiseResolver 19 | implements 20 | ResolverContract> 21 | { 22 | /** 23 | * {@inheritDoc} 24 | */ 25 | public createResolver({ 26 | event, 27 | log, 28 | respondWithErrors, 29 | adapter, 30 | }: ResolverProps): Resolver< 31 | TResponse, 32 | Promise 33 | > { 34 | return { 35 | run: task => { 36 | return new Promise((resolve, reject) => { 37 | const delegatedResolver: DelegatedResolver = { 38 | succeed: response => resolve(response), 39 | fail: error => reject(error), 40 | }; 41 | 42 | task() 43 | .then(response => delegatedResolver.succeed(response)) 44 | .catch(error => { 45 | log.error( 46 | 'SERVERLESS_ADAPTER:RESPOND_TO_EVENT_SOURCE_WITH_ERROR', 47 | error, 48 | ); 49 | 50 | adapter.onErrorWhileForwarding({ 51 | delegatedResolver, 52 | error, 53 | log, 54 | event, 55 | respondWithErrors, 56 | }); 57 | }); 58 | }); 59 | }, 60 | }; 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": "Current TS File", 9 | "type": "node", 10 | "request": "launch", 11 | "runtimeExecutable": "node", 12 | "args": ["${relativeFile}"], 13 | "runtimeArgs": ["--nolazy", "-r", "ts-node/register/transpile-only"], 14 | "envFile": "${workspaceFolder}/.env", 15 | "cwd": "${workspaceRoot}", 16 | "internalConsoleOptions": "openOnSessionStart", 17 | "skipFiles": ["/**", "node_modules/**"] 18 | }, 19 | { 20 | "name": "Debug Jest Tests", 21 | "type": "node", 22 | "request": "launch", 23 | "runtimeArgs": [ 24 | "--inspect-brk", 25 | "${workspaceRoot}/node_modules/.bin/jest", 26 | "--runInBand" 27 | ], 28 | "envFile": "${workspaceFolder}/.env", 29 | "console": "integratedTerminal", 30 | "internalConsoleOptions": "neverOpen", 31 | "port": 9229, 32 | "disableOptimisticBPs": true, 33 | "windows": { 34 | "program": "${workspaceFolder}/node_modules/jest/bin/jest" 35 | } 36 | }, 37 | { 38 | "name": "Debug Jest Current File", 39 | "type": "node", 40 | "request": "launch", 41 | "program": "${workspaceFolder}/node_modules/.bin/jest", 42 | "args": ["${relativeFile}", "--config", "jest.config.js"], 43 | "console": "integratedTerminal", 44 | "internalConsoleOptions": "neverOpen", 45 | "port": 9229, 46 | "disableOptimisticBPs": true, 47 | "windows": { 48 | "program": "${workspaceFolder}/node_modules/jest/bin/jest" 49 | } 50 | } 51 | ] 52 | } 53 | -------------------------------------------------------------------------------- /src/core/stream.ts: -------------------------------------------------------------------------------- 1 | //#region Imports 2 | 3 | import { Readable, Writable } from 'node:stream'; 4 | 5 | //#endregion 6 | 7 | /** 8 | * Check if stream already ended 9 | * 10 | * @param stream - The stream 11 | * 12 | * @breadcrumb Core / Stream 13 | * @public 14 | */ 15 | export function isStreamEnded(stream: Readable | Writable): boolean { 16 | if ('readableEnded' in stream && stream.readableEnded) return true; 17 | 18 | if ('writableEnded' in stream && stream.writableEnded) return true; 19 | 20 | return false; 21 | } 22 | 23 | /** 24 | * Wait asynchronous the stream to complete 25 | * 26 | * @param stream - The stream 27 | * 28 | * @breadcrumb Core / Stream 29 | * @public 30 | */ 31 | export function waitForStreamComplete( 32 | stream: TStream, 33 | ): Promise { 34 | if (isStreamEnded(stream)) return Promise.resolve(stream); 35 | 36 | return new Promise((resolve, reject) => { 37 | // Reading the {@link https://github.com/nodejs/node/blob/v12.22.9/lib/events.js#L262 | emit source code}, 38 | // it's almost impossible to complete being called twice because the emit function runs synchronously and removes the other listeners, 39 | // but I'll leave it at that because I didn't write that code, so I couldn't figure out what the author thought when he wrote this. 40 | let isComplete = false; 41 | 42 | function complete(err: any) { 43 | /* istanbul ignore next */ 44 | if (isComplete) return; 45 | 46 | isComplete = true; 47 | 48 | stream.removeListener('error', complete); 49 | stream.removeListener('end', complete); 50 | stream.removeListener('finish', complete); 51 | 52 | if (err) reject(err); 53 | else resolve(stream); 54 | } 55 | 56 | stream.once('error', complete); 57 | stream.once('end', complete); 58 | stream.once('finish', complete); 59 | }); 60 | } 61 | -------------------------------------------------------------------------------- /www/docs/main/adapters/huawei/huawei-api-gateway.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Huawei Api Gateway 3 | description: See more about how to integrate with Api Gateway. 4 | --- 5 | 6 | This adapter add support to [Huawei Api Gateway](https://support.huaweicloud.com/intl/en-us/devg-functiongraph/functiongraph_02_0102.html#functiongraph_02_0102__li5178638110137) inside [Huawei Function Graph](https://support.huaweicloud.com/intl/en-us/devg-functiongraph/functiongraph_02_0101.html). 7 | 8 | ## About the Adapter 9 | 10 | When you receive an `POST` request inside path `/users`, this adapter will forward that request to your framework, 11 | so you can just plug this adapter and doesn't need any configuration to work. 12 | 13 | To see which options you can customize, see the [HuaweiApiGatewayOptions](/docs/api/Adapters/Huawei/HuaweiApiGatewayAdapter/HuaweiApiGatewayOptions) interface. 14 | 15 | ## Usage 16 | 17 | To add support to Api Gateway you do the following: 18 | 19 | ```ts title="index.ts" 20 | import { ServerlessAdapter } from '@h4ad/serverless-adapter'; 21 | import { HuaweiApiGatewayAdapter } from '@h4ad/serverless-adapter/adapters/huawei'; 22 | import { ExpressFramework } from '@h4ad/serverless-adapter/frameworks/express'; 23 | import { DefaultHandler } from '@h4ad/serverless-adapter/handlers/default'; 24 | import { CallbackResolver } from '@h4ad/serverless-adapter/resolvers/callback'; 25 | import app from './app'; 26 | 27 | export const handler = ServerlessAdapter.new(app) 28 | .setFramework(new ExpressFramework()) 29 | .setHandler(new DefaultHandler()) 30 | .setResolver(new CallbackResolver()) 31 | .addAdapter(new HuaweiApiGatewayAdapter()) 32 | .build(); 33 | ``` 34 | 35 | :::caution One important thing 36 | 37 | You must use the callback resolver because I couldn't get it to work with the PromiseResolver. 38 | Maybe it's a bug in the library or something specific in Huawei, if you have a hint please create an issue. 39 | 40 | ::: 41 | -------------------------------------------------------------------------------- /test/adapters/dummy/dummy.adapter.spec.ts: -------------------------------------------------------------------------------- 1 | import { beforeEach, describe, expect, it, vitest } from 'vitest'; 2 | import { 3 | type DelegatedResolver, 4 | EmptyResponse, 5 | createDefaultLogger, 6 | } from '../../../src'; 7 | import { DummyAdapter } from '../../../src/adapters/dummy'; 8 | 9 | describe(DummyAdapter.name, () => { 10 | let adapter!: DummyAdapter; 11 | 12 | beforeEach(() => { 13 | adapter = new DummyAdapter(); 14 | }); 15 | 16 | describe('getAdapterName', () => { 17 | it('should be the same name of the class', () => { 18 | expect(adapter.getAdapterName()).toBe(DummyAdapter.name); 19 | }); 20 | }); 21 | 22 | describe('canHandle', () => { 23 | it('should always return true', () => { 24 | expect(adapter.canHandle()).toBe(true); 25 | }); 26 | }); 27 | 28 | describe('getRequest', () => { 29 | it('should always create the same request', () => { 30 | const request = adapter.getRequest(); 31 | 32 | expect(request).toHaveProperty('body', undefined); 33 | expect(request).toHaveProperty('method', 'POST'); 34 | expect(request).toHaveProperty('path', '/dummy'); 35 | expect(request.headers).toStrictEqual({}); 36 | }); 37 | }); 38 | 39 | describe('getResponse', () => { 40 | it('should always return empty response', () => { 41 | expect(adapter.getResponse()).toBe(EmptyResponse); 42 | }); 43 | }); 44 | 45 | describe('onErrorWhileForwarding', () => { 46 | it('should always resolve with success', () => { 47 | const resolver: DelegatedResolver = { 48 | fail: vitest.fn(), 49 | succeed: vitest.fn(), 50 | }; 51 | 52 | adapter.onErrorWhileForwarding({ 53 | event: void 0, 54 | log: createDefaultLogger(), 55 | respondWithErrors: true, 56 | error: new Error('test'), 57 | delegatedResolver: resolver, 58 | }); 59 | 60 | expect(resolver.succeed).toHaveBeenCalled(); 61 | }); 62 | }); 63 | }); 64 | -------------------------------------------------------------------------------- /test/adapters/aws/utils/dynamodb.ts: -------------------------------------------------------------------------------- 1 | import type { DynamoDBStreamEvent } from 'aws-lambda'; 2 | 3 | /** 4 | * Sample event from {@link https://docs.aws.amazon.com/lambda/latest/dg/with-ddb.html} 5 | */ 6 | export function createDynamoDBEvent(): DynamoDBStreamEvent { 7 | return { 8 | Records: [ 9 | { 10 | eventID: '1', 11 | eventVersion: '1.0', 12 | dynamodb: { 13 | Keys: { 14 | Id: { 15 | N: '101', 16 | }, 17 | }, 18 | NewImage: { 19 | Message: { 20 | S: 'New item!', 21 | }, 22 | Id: { 23 | N: '101', 24 | }, 25 | }, 26 | StreamViewType: 'NEW_AND_OLD_IMAGES', 27 | SequenceNumber: '111', 28 | SizeBytes: 26, 29 | }, 30 | awsRegion: 'us-west-2', 31 | eventName: 'INSERT', 32 | eventSourceARN: 'arn:aws:dynamodb:us-east-1:0000000000:mytable', 33 | eventSource: 'aws:dynamodb', 34 | }, 35 | { 36 | eventID: '2', 37 | eventVersion: '1.0', 38 | dynamodb: { 39 | OldImage: { 40 | Message: { 41 | S: 'New item!', 42 | }, 43 | Id: { 44 | N: '101', 45 | }, 46 | }, 47 | SequenceNumber: '222', 48 | Keys: { 49 | Id: { 50 | N: '101', 51 | }, 52 | }, 53 | SizeBytes: 59, 54 | NewImage: { 55 | Message: { 56 | S: 'This item has changed', 57 | }, 58 | Id: { 59 | N: '101', 60 | }, 61 | }, 62 | StreamViewType: 'NEW_AND_OLD_IMAGES', 63 | }, 64 | awsRegion: 'us-west-2', 65 | eventName: 'MODIFY', 66 | eventSourceARN: 'arn:aws:dynamodb:us-east-1:0000000000:mytable', 67 | eventSource: 'aws:dynamodb', 68 | }, 69 | ], 70 | }; 71 | } 72 | -------------------------------------------------------------------------------- /www/src/components/BrowserWindow/styles.module.css: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Facebook, Inc. and its affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | .browserWindow { 9 | border: 3px solid #444950; 10 | border-radius: 0.4rem; 11 | box-shadow: 0 1px 2px 0 rgba(0, 0, 0, .1); 12 | margin-bottom: 1.25rem; 13 | } 14 | 15 | .browserWindowHeader { 16 | align-items: center; 17 | background: #444950; 18 | display: flex; 19 | padding: 0.5rem 1rem; 20 | } 21 | 22 | .row::after { 23 | content: ''; 24 | display: table; 25 | clear: both; 26 | } 27 | 28 | .buttons { 29 | white-space: nowrap; 30 | } 31 | 32 | .right { 33 | align-self: center; 34 | width: 10%; 35 | } 36 | 37 | [data-theme='light'] { 38 | --ifm-background-color: #fff; 39 | } 40 | 41 | .browserWindowAddressBar { 42 | flex: 1 0; 43 | margin: 0 1rem 0 0.5rem; 44 | border-radius: 12.5px; 45 | background-color: var(--ifm-background-color); 46 | color: #444950; 47 | padding: 5px 15px; 48 | font: 400 13px Arial, sans-serif; 49 | user-select: none; 50 | } 51 | 52 | [data-theme='dark'] .browserWindowAddressBar { 53 | color: #dadde1; 54 | } 55 | 56 | .dot { 57 | margin-right: 6px; 58 | margin-top: 4px; 59 | height: 12px; 60 | width: 12px; 61 | background-color: #bbb; 62 | border-radius: 50%; 63 | display: inline-block; 64 | } 65 | 66 | .browserWindowMenuIcon { 67 | margin-left: auto; 68 | } 69 | 70 | .bar { 71 | width: 17px; 72 | height: 3px; 73 | background-color: #aaa; 74 | margin: 3px 0; 75 | display: block; 76 | } 77 | 78 | .browserWindowBody { 79 | background-color: var(--ifm-background-color); 80 | border-bottom-left-radius: inherit; 81 | border-bottom-right-radius: inherit; 82 | padding: 1rem; 83 | } 84 | 85 | .browserWindowBody *:last-child { 86 | margin-bottom: 0; 87 | } 88 | -------------------------------------------------------------------------------- /benchmark/src/samples/compare-libraries.ts: -------------------------------------------------------------------------------- 1 | import { ServerlessAdapter } from '@h4ad/serverless-adapter/lib'; 2 | import { ApiGatewayV1Adapter } from '@h4ad/serverless-adapter/lib/adapters/aws'; 3 | import { DefaultHandler } from '@h4ad/serverless-adapter/lib/handlers/default'; 4 | import { PromiseResolver } from '@h4ad/serverless-adapter/lib/resolvers/promise'; 5 | import vendia from '@vendia/serverless-express'; 6 | import benchmark from 'benchmark'; 7 | import serverlessHttp from 'serverless-http'; 8 | import { createApiGatewayV1 } from '../events'; 9 | import { FrameworkMock } from '../framework.mock'; 10 | 11 | console.log('Running simply-forward.ts'); 12 | 13 | const framework = new FrameworkMock(200, { message: 'Hello world' }); 14 | const handler = ServerlessAdapter.new(null) 15 | .setHandler(new DefaultHandler()) 16 | .setResolver(new PromiseResolver()) 17 | .setFramework(framework) 18 | .addAdapter(new ApiGatewayV1Adapter()) 19 | .build(); 20 | 21 | const falseApp = (req, res) => framework.sendRequest(null, req, res); 22 | const vendiaHandler = vendia({ 23 | app: falseApp, 24 | }); 25 | 26 | const serverlessHttpHandler = serverlessHttp(falseApp); 27 | 28 | const context = {} as any; 29 | const callback = {} as any; 30 | 31 | const eventV1ApiGateway = createApiGatewayV1('GET', '/test'); 32 | 33 | const suite = new benchmark.Suite(); 34 | 35 | suite.add( 36 | '@h4ad/serverless-adapter', 37 | async () => await handler(eventV1ApiGateway, context, callback), 38 | ); 39 | suite.add( 40 | '@vendia/serverless-express', 41 | async () => await vendiaHandler(eventV1ApiGateway, context, callback), 42 | ); 43 | suite.add( 44 | 'serverless-http', 45 | async () => await serverlessHttpHandler(eventV1ApiGateway, context), 46 | ); 47 | 48 | suite 49 | .on('cycle', function (event) { 50 | console.log(String(event.target)); 51 | }) 52 | .on('complete', function () { 53 | console.log('Fastest is ' + this.filter('fastest').map('name')); 54 | }) 55 | .run({ 56 | async: false, 57 | }); 58 | -------------------------------------------------------------------------------- /www/docs/main/resolvers/promise.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Promise 3 | position: 1 4 | description: See more about the Promise Resolver. 5 | --- 6 | 7 | :::tip 8 | 9 | Don't know what a resolver is? See the [Architecture](../architecture#resolver) section. 10 | 11 | ::: 12 | 13 | The best and most agnostic resolver is using promise, generally every serverless environment supports asynchronous handlers. 14 | 15 | When the handler is created with [getHandler](../../api/Contracts/HandlerContract#method-gethandler), it will return an promise 16 | which is resolved when your framework send the response and the adapter transform the response in the way of your cloud can handle. 17 | 18 | You can use this resolver with any cloud (except Huawei), with any framework or any adapter. 19 | 20 | :::caution 21 | 22 | Only Huawei doesn't support Promise, or it was buggy in my time, so I suggest you use [Callback Resolver](./callback). 23 | 24 | ::: 25 | 26 | # Usage 27 | 28 | To use, you can import and call the method [setResolver](../../api/ServerlessAdapter#method-setresolver), as per the code below: 29 | 30 | ```ts title="index.ts" 31 | import { ServerlessAdapter } from '@h4ad/serverless-adapter'; 32 | import { PromiseResolver } from '@h4ad/serverless-adapter/resolvers/promise'; 33 | 34 | const express = require('express'); 35 | 36 | const app = express(); 37 | export const handler = ServerlessAdapter.new(app) 38 | .setResolver(new PromiseResolver()) 39 | // continue to set the other options here. 40 | //.setFramework(new ExpressFramework()) 41 | //.setHandler(new DefaultHandler()) 42 | //.addAdapter(new AlbAdapter()) 43 | //.addAdapter(new SQSAdapter()) 44 | //.addAdapter(new SNSAdapter()) 45 | // after put all methods necessary, just call the build method. 46 | .build(); 47 | ``` 48 | 49 | :::tip 50 | 51 | To know more about how AWS deals with async handlers, see [NodeJS Handler](https://docs.aws.amazon.com/lambda/latest/dg/nodejs-handler.html) section. 52 | 53 | ::: 54 | -------------------------------------------------------------------------------- /www/src/components/HowToStart/index.tsx: -------------------------------------------------------------------------------- 1 | import Link from '@docusaurus/Link'; 2 | import CodeBlock from '@theme/CodeBlock'; 3 | import React from 'react'; 4 | import styles from './styles.module.css'; 5 | 6 | export default function HowToStart(): JSX.Element { 7 | return ( 8 |
9 |
10 |
11 |

To start, first, install the library with:

12 |
13 |
14 |
15 | 16 | npm i --save @h4ad/serverless-adapter 17 | 18 |
19 |
20 |
21 |

22 | And then you can add support, for example, to{' '} 23 | 24 | AWS Api Gateway V2 25 | 26 | {' and '} 27 | AWS SQS to your 28 | Express App with: 29 |

30 |
31 | {`import { ServerlessAdapter } from '@h4ad/serverless-adapter'; 35 | import { ApiGatewayV2Adapter, SQSAdapter } from '@h4ad/serverless-adapter/lib/adapters/aws'; 36 | import { ExpressFramework } from '@h4ad/serverless-adapter/lib/frameworks/express'; 37 | import { DefaultHandler } from '@h4ad/serverless-adapter/lib/handlers/default'; 38 | import { PromiseResolver } from '@h4ad/serverless-adapter/lib/resolvers/promise'; 39 | import app from './app'; 40 | 41 | export const handler = ServerlessAdapter.new(app) 42 | .setFramework(new ExpressFramework()) 43 | .setHandler(new DefaultHandler()) 44 | .setResolver(new PromiseResolver()) 45 | .addAdapter(new ApiGatewayV2Adapter()) 46 | .addAdapter(new SQSAdapter()) 47 | .build(); 48 | `} 49 |
50 |
51 |
52 |
53 | ); 54 | } 55 | -------------------------------------------------------------------------------- /release-please-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "packages": { 3 | ".": { 4 | "release-type": "node", 5 | "bump-minor-pre-major": false, 6 | "bump-patch-for-minor-pre-major": false, 7 | "draft": false, 8 | "prerelease": false, 9 | "draft-pull-request": true, 10 | "include-component-in-tag": false, 11 | "include-v-in-tag": true, 12 | "separate-pull-requests": false, 13 | "skip-github-release": false, 14 | "versioning": "default", 15 | "pull-request-header": ":robot: I have created a release *beep* *boop*", 16 | "pull-request-title-pattern": "chore${scope}: release${component} ${version}", 17 | "changelog-path": "CHANGELOG.md", 18 | "changelog-host": "https://github.com", 19 | "changelog-type": "default", 20 | "changelog-sections": [ 21 | { 22 | "type": "feat", 23 | "section": "Features" 24 | }, 25 | { 26 | "type": "feature", 27 | "section": "Features" 28 | }, 29 | { 30 | "type": "fix", 31 | "section": "Bug Fixes" 32 | }, 33 | { 34 | "type": "perf", 35 | "section": "Performance Improvements" 36 | }, 37 | { 38 | "type": "revert", 39 | "section": "Reverts" 40 | }, 41 | { 42 | "type": "docs", 43 | "section": "Documentation" 44 | }, 45 | { 46 | "type": "style", 47 | "section": "Styles" 48 | }, 49 | { 50 | "type": "chore", 51 | "section": "Miscellaneous Chores" 52 | }, 53 | { 54 | "type": "refactor", 55 | "section": "Code Refactoring" 56 | }, 57 | { 58 | "type": "test", 59 | "section": "Tests" 60 | }, 61 | { 62 | "type": "build", 63 | "section": "Build System" 64 | }, 65 | { 66 | "type": "ci", 67 | "section": "Continuous Integration" 68 | } 69 | ] 70 | } 71 | }, 72 | "$schema": "https://raw.githubusercontent.com/googleapis/release-please/main/schemas/config.json" 73 | } 74 | -------------------------------------------------------------------------------- /src/core/current-invoke.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * The type that represents the object that handles the references to the event created by the serverless trigger or context created by the serverless environment. 3 | * 4 | * @breadcrumb Core / Current Invoke 5 | * @public 6 | */ 7 | export type CurrentInvoke = { 8 | /** 9 | * The event created by the serverless trigger 10 | * 11 | * @remarks It's only null when you call {@link getCurrentInvoke} outside this library's pipeline. 12 | */ 13 | event: TEvent | null; 14 | 15 | /** 16 | * The context created by the serverless environment 17 | * 18 | * @remarks It's only null when you call {@link getCurrentInvoke} outside this library's pipeline. 19 | */ 20 | context: TContext | null; 21 | }; 22 | 23 | const currentInvoke: CurrentInvoke = { 24 | context: null, 25 | event: null, 26 | }; 27 | 28 | /** 29 | * Get the reference to the event created by the serverless trigger or context created by the serverless environment. 30 | * 31 | * @example 32 | * ```typescript 33 | * import type { ALBEvent, Context } from 'aws-lambda'; 34 | * 35 | * // inside the method that handles the aws alb request. 36 | * const { event, context } = getCurrentInvoke(); 37 | * ``` 38 | * 39 | * @breadcrumb Core / Current Invoke 40 | * @public 41 | */ 42 | export function getCurrentInvoke(): CurrentInvoke< 43 | TEvent, 44 | TContext 45 | > { 46 | return currentInvoke; 47 | } 48 | 49 | /** 50 | * Method that saves to the event created by the serverless trigger or context created by the serverless environment. 51 | * 52 | * @remarks This method MUST NOT be called by you, this method MUST only be used internally in this library. 53 | * 54 | * @param event - The event created by the serverless trigger 55 | * @param context - The context created by the serverless environment 56 | * 57 | * @breadcrumb Core / Current Invoke 58 | * @public 59 | */ 60 | export function setCurrentInvoke({ 61 | event, 62 | context, 63 | }: CurrentInvoke) { 64 | currentInvoke.event = event; 65 | currentInvoke.context = context; 66 | } 67 | -------------------------------------------------------------------------------- /www/docs/main/resolvers/callback.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Callback 3 | position: 2 4 | description: See more about the Callback Resolver. 5 | --- 6 | 7 | :::tip 8 | 9 | Don't know what a resolver is? See the [Architecture](../architecture#resolver) section. 10 | 11 | ::: 12 | 13 | The best and most agnostic resolver (for sure) is using callback, generally every serverless environment supports callback as the third argument of the handler. 14 | 15 | When the handler is created with [getHandler](../../api/Contracts/HandlerContract#method-gethandler), it will return void 16 | and the cloud will wait until the event loop is empty. 17 | This happens when your framework sends the response and the adapter transforms the response the way your cloud can handle, 18 | at this point the response will be passed to the callback and then the event loop will be empty. 19 | 20 | You can use this resolver with any cloud, with any framework or any adapter. 21 | 22 | :::caution AWS 23 | 24 | To use this resolver on AWS, you MUST leave [callbackWaitsForEmptyEventLoop](https://docs.aws.amazon.com/lambda/latest/dg/nodejs-context.html) as true, otherwise, AWS will not wait for this resolver to resolve. 25 | 26 | ::: 27 | 28 | # Usage 29 | 30 | To use, you can import and call the method [setResolver](../../api/ServerlessAdapter#method-setresolver), as per the code below: 31 | 32 | ```ts title="index.ts" 33 | import { ServerlessAdapter } from '@h4ad/serverless-adapter'; 34 | import { CallbackResolver } from '@h4ad/serverless-adapter/resolvers/callback'; 35 | 36 | const express = require('express'); 37 | 38 | const app = express(); 39 | export const handler = ServerlessAdapter.new(app) 40 | .setResolver(new CallbackResolver()) 41 | // continue to set the other options here. 42 | //.setFramework(new ExpressFramework()) 43 | //.setHandler(new DefaultHandler()) 44 | //.addAdapter(new AlbAdapter()) 45 | //.addAdapter(new SQSAdapter()) 46 | //.addAdapter(new SNSAdapter()) 47 | // after put all methods necessary, just call the build method. 48 | .build(); 49 | ``` 50 | 51 | :::tip 52 | 53 | To know more about how AWS deals with callback handlers, see [NodeJS Handler](https://docs.aws.amazon.com/lambda/latest/dg/nodejs-handler.html) section. 54 | 55 | ::: 56 | -------------------------------------------------------------------------------- /src/network/request.ts: -------------------------------------------------------------------------------- 1 | // ATTRIBUTION: https://github.com/dougmoscrop/serverless-http 2 | import { IncomingMessage } from 'node:http'; 3 | import type { AddressInfo } from 'node:net'; 4 | import type { SingleValueHeaders } from '../@types'; 5 | import { NO_OP } from '../core'; 6 | 7 | const HTTPS_PORT = 443; 8 | 9 | /** 10 | * The properties to create a {@link ServerlessRequest} 11 | * 12 | * @breadcrumb Network / ServerlessRequest 13 | * @public 14 | */ 15 | export interface ServerlessRequestProps { 16 | /** 17 | * The HTTP Method of the request 18 | */ 19 | method: string; 20 | 21 | /** 22 | * The URL requested 23 | */ 24 | url: string; 25 | 26 | /** 27 | * The headers from the event source 28 | */ 29 | headers: SingleValueHeaders; 30 | 31 | /** 32 | * The body from the event source 33 | */ 34 | body?: Buffer | Uint8Array; 35 | 36 | /** 37 | * The IP Address from caller 38 | */ 39 | remoteAddress?: string; 40 | } 41 | 42 | /** 43 | * The class that represents an {@link http#IncomingMessage} created by the library to represent an actual request to the framework. 44 | * 45 | * @breadcrumb Network / ServerlessRequest 46 | * @public 47 | */ 48 | export class ServerlessRequest extends IncomingMessage { 49 | constructor({ 50 | method, 51 | url, 52 | headers, 53 | body, 54 | remoteAddress, 55 | }: ServerlessRequestProps) { 56 | super({ 57 | encrypted: true, 58 | readable: true, // credits to @pnkp at https://github.com/CodeGenieApp/serverless-express/pull/692 59 | remoteAddress, 60 | address: () => ({ port: HTTPS_PORT }) as AddressInfo, 61 | on: NO_OP, 62 | removeListener: NO_OP, 63 | removeEventListener: NO_OP, 64 | end: NO_OP, 65 | destroy: NO_OP, 66 | } as any); 67 | 68 | this.statusCode = 200; 69 | this.statusMessage = 'OK'; 70 | this.complete = true; 71 | this.httpVersion = '1.1'; 72 | this.httpVersionMajor = 1; 73 | this.httpVersionMinor = 1; 74 | this.method = method; 75 | this.headers = headers; 76 | this.body = body; 77 | this.url = url; 78 | this.ip = remoteAddress; 79 | 80 | this._read = () => { 81 | this.push(body); 82 | this.push(null); 83 | }; 84 | } 85 | 86 | ip?: string; 87 | body?: Buffer | Uint8Array; 88 | } 89 | -------------------------------------------------------------------------------- /www/docs/main/frameworks/helpers/lazy.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Lazy 3 | description: See more about how to use Lazy Framework. 4 | --- 5 | 6 | This framework is a helper framework that wraps another framework and receives a function to asynchronously initialize the application. 7 | 8 | The constructor looks like: 9 | 10 | ```ts title="lazy.framework.ts" 11 | constructor( 12 | protected readonly framework: FrameworkContract, 13 | protected readonly factory: () => Promise, 14 | protected readonly logger: ILogger = createDefaultLogger(), 15 | ) 16 | ``` 17 | 18 | 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: 19 | 20 | ```ts 21 | import { ServerlessAdapter } from '@h4ad/serverless-adapter'; 22 | import { LazyFramework } from '@h4ad/serverless-adapter/frameworks/lazy'; 23 | import { ExpressFramework } from '@h4ad/serverless-adapter/frameworks/express'; 24 | 25 | const express = require('express'); 26 | 27 | async function bootstrap() { 28 | const app = express(); 29 | 30 | // do the things asynchronously 31 | 32 | return app; 33 | } 34 | 35 | const expressFramework = new ExpressFramework(); 36 | const framework = new LazyFramework(expressFramework, bootstrap); 37 | 38 | export const handler = ServerlessAdapter.new(null) 39 | .setFramework(framework) 40 | // continue to set the other options here. 41 | //.setHandler(new DefaultHandler()) 42 | //.setResolver(new PromiseResolver()) 43 | //.addAdapter(new AlbAdapter()) 44 | //.addAdapter(new SQSAdapter()) 45 | //.addAdapter(new SNSAdapter()) 46 | // after put all methods necessary, just call the build method. 47 | .build(); 48 | ``` 49 | 50 | ## Benefits 51 | 52 | This is an optimized version that could save almost 1s (in my projects) because we initialize our app while the lambda warms up and before the lambda receives the first request. 53 | 54 | :::tip 55 | 56 | The solution above is inspired by [top-level await](https://aws.amazon.com/pt/blogs/compute/using-node-js-es-modules-and-top-level-await-in-aws-lambda/), it is very much worth reading. 57 | 58 | ::: 59 | 60 | :::tip 61 | 62 | Need to deal with CORS? See [CorsFramework](./cors) which helps you to add correct headers. 63 | 64 | ::: 65 | -------------------------------------------------------------------------------- /src/handlers/firebase/http-firebase-v2.handler.ts: -------------------------------------------------------------------------------- 1 | //#region Imports 2 | 3 | import { IncomingMessage, ServerResponse } from 'node:http'; 4 | // eslint-disable-next-line import/no-unresolved 5 | import { https } from 'firebase-functions/v2'; 6 | import type { FrameworkContract, HandlerContract } from '../../contracts'; 7 | import { RawRequest } from '../base'; 8 | 9 | //#endregion 10 | 11 | /** 12 | * The HTTP handler that is exposed when you use {@link HttpFirebaseV2Handler}. 13 | * 14 | * @breadcrumb Handlers / HttpFirebaseHandler 15 | * @public 16 | */ 17 | export type FirebaseHttpHandler = ( 18 | request: IncomingMessage, 19 | response: ServerResponse, 20 | ) => void | Promise; 21 | 22 | /** 23 | * The class that implements a handler for Firebase Https Events 24 | * 25 | * @remarks Read more about Https Events {@link https://firebase.google.com/docs/functions/http-events | here} 26 | * 27 | * @breadcrumb Handlers / HttpFirebaseHandler 28 | * @public 29 | */ 30 | export class HttpFirebaseV2Handler 31 | extends RawRequest 32 | implements 33 | HandlerContract> 34 | { 35 | //#region Constructor 36 | 37 | /** 38 | * Construtor padrão 39 | */ 40 | constructor(protected readonly options?: https.HttpsOptions) { 41 | super(); 42 | } 43 | 44 | //#endregion 45 | 46 | //#region Public Methods 47 | 48 | /** 49 | * {@inheritDoc} 50 | */ 51 | public getHandler( 52 | app: TApp, 53 | framework: FrameworkContract, 54 | ): FirebaseHttpHandler { 55 | if (this.options) { 56 | return this.onRequestWithOptions( 57 | this.options, 58 | this.onRequestCallback(app, framework), 59 | ); 60 | } 61 | 62 | return https.onRequest( 63 | this.onRequestCallback(app, framework), 64 | ) as unknown as FirebaseHttpHandler; 65 | } 66 | 67 | //#endregion 68 | 69 | //#region Protected Method 70 | 71 | /** 72 | * Wrapper method around onRequest for better testability 73 | */ 74 | protected onRequestWithOptions( 75 | options: https.HttpsOptions, 76 | callback: ReturnType['onRequestCallback']>, 77 | ): FirebaseHttpHandler { 78 | return https.onRequest(options, callback) as unknown as FirebaseHttpHandler; 79 | } 80 | 81 | //#endregion 82 | } 83 | -------------------------------------------------------------------------------- /.github/workflows/pr.yml: -------------------------------------------------------------------------------- 1 | name: Pull Request 2 | 3 | on: 4 | workflow_dispatch: 5 | pull_request: 6 | paths: 7 | - 'src/**' 8 | - 'package-lock.json' 9 | - 'package.json' 10 | - 'tsconfig.json' 11 | - 'tsconfig.*.json' 12 | - 'vite.config.ts' 13 | 14 | jobs: 15 | build: 16 | runs-on: ubuntu-latest 17 | 18 | strategy: 19 | matrix: 20 | node-version: [18.x, 20.x] 21 | 22 | steps: 23 | - uses: actions/checkout@v4 24 | 25 | - name: Use Node.js ${{ matrix.node-version }} 26 | uses: actions/setup-node@v4 27 | with: 28 | node-version: ${{ matrix.node-version }} 29 | 30 | - name: Update NPM Version 31 | run: npm i -g npm 32 | 33 | - name: Cache dependencies 34 | uses: actions/cache@v3 35 | with: 36 | path: ~/.npm 37 | key: ${{ runner.os }}-node-${{ matrix.node-version }}-${{ hashFiles('**/package-lock.json') }} 38 | restore-keys: | 39 | ${{ runner.os }}-node-${{ matrix.node-version }}- 40 | 41 | - name: Install Lib Dependencies 42 | run: npm ci 43 | 44 | - name: Build 45 | run: npm run build 46 | 47 | - name: Run tests 48 | run: npm test 49 | 50 | - name: Get Coverage Info 51 | uses: codecov/codecov-action@v4 52 | with: 53 | fail_ci_if_error: true 54 | env: 55 | CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} 56 | 57 | docs: 58 | runs-on: ubuntu-latest 59 | 60 | steps: 61 | - uses: actions/checkout@v4 62 | 63 | - name: Use Node.js 18 64 | uses: actions/setup-node@v4 65 | with: 66 | node-version: '18.x' 67 | 68 | - name: Cache dependencies 69 | uses: actions/cache@v3 70 | with: 71 | path: ~/.npm 72 | key: ${{ runner.os }}-node-18.x-${{ hashFiles('**/package-lock.json') }} 73 | restore-keys: | 74 | ${{ runner.os }}-node-18.x- 75 | 76 | - name: Install Lib Dependencies 77 | run: npm ci 78 | 79 | - name: Install Docs Dependencies 80 | run: npm ci 81 | working-directory: www 82 | 83 | - name: Parse TSDoc Documentation 84 | run: npm run docs:generate 85 | 86 | - name: Build Docs 87 | run: npm run build 88 | working-directory: www 89 | -------------------------------------------------------------------------------- /src/resolvers/callback/callback.resolver.ts: -------------------------------------------------------------------------------- 1 | //#region Imports 2 | 3 | import type { 4 | DelegatedResolver, 5 | Resolver, 6 | ResolverContract, 7 | ResolverProps, 8 | } from '../../contracts'; 9 | 10 | //#endregion 11 | 12 | /** 13 | * The default signature of the callback sent by serverless 14 | * 15 | * @breadcrumb Resolvers / CallbackResolver 16 | * @public 17 | */ 18 | export type ServerlessCallback = ( 19 | error: Error | null, 20 | success: TResponse | null, 21 | ) => void; 22 | 23 | /** 24 | * The class that implements the resolver using the callback function sent by serverless 25 | * 26 | * @remarks To use this resolver on AWS, you MUST leave `{@link https://docs.aws.amazon.com/lambda/latest/dg/nodejs-context.html | callbackWaitsForEmptyEventLoop}` as true, otherwise, AWS will not wait for this resolver to resolve. 27 | * 28 | * @breadcrumb Resolvers / CallbackResolver 29 | * @public 30 | */ 31 | export class CallbackResolver 32 | implements 33 | ResolverContract, TResponse, void> 34 | { 35 | /** 36 | * {@inheritDoc} 37 | */ 38 | public createResolver({ 39 | callback, 40 | event, 41 | log, 42 | respondWithErrors, 43 | adapter, 44 | }: ResolverProps< 45 | TEvent, 46 | TContext, 47 | ServerlessCallback, 48 | TResponse 49 | >): Resolver { 50 | if (!callback) { 51 | throw new Error( 52 | 'Could not figure out how to create the resolver because the "callback" argument was not sent.', 53 | ); 54 | } 55 | 56 | const delegatedResolver: DelegatedResolver = { 57 | succeed: response => callback(null, response), 58 | fail: error => callback(error, null), 59 | }; 60 | 61 | return { 62 | run: task => { 63 | task() 64 | .then(response => delegatedResolver.succeed(response)) 65 | .catch(error => { 66 | log.error( 67 | 'SERVERLESS_ADAPTER:RESPOND_TO_EVENT_SOURCE_WITH_ERROR', 68 | error, 69 | ); 70 | 71 | adapter.onErrorWhileForwarding({ 72 | delegatedResolver, 73 | error, 74 | log, 75 | event, 76 | respondWithErrors, 77 | }); 78 | }); 79 | }, 80 | }; 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /test/handlers/digital-ocean.handler.spec.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it, vitest } from 'vitest'; 2 | import { type ILogger } from '../../src'; 3 | import { HttpFunctionAdapter } from '../../src/adapters/digital-ocean'; 4 | import { DefaultHandler } from '../../src/handlers/default'; 5 | import { DigitalOceanHandler } from '../../src/handlers/digital-ocean'; 6 | import { PromiseResolver } from '../../src/resolvers/promise'; 7 | import { FrameworkMock } from '../mocks/framework.mock'; 8 | import { createHttpFunctionEvent } from '../adapters/digital-ocean/utils/http-function'; 9 | import type { DigitalOceanHttpEvent } from '../../src/@types/digital-ocean'; 10 | 11 | describe(DigitalOceanHandler.name, () => { 12 | const azureHandlerFactory = new DigitalOceanHandler< 13 | null, 14 | DigitalOceanHttpEvent, 15 | any, 16 | any 17 | >(); 18 | 19 | const app = null; 20 | const response = { batata: true }; 21 | const mockFramework = new FrameworkMock(200, response); 22 | const adapter = new HttpFunctionAdapter(); 23 | const adapters = [adapter]; 24 | const resolver = new PromiseResolver(); 25 | const binarySettings = { contentEncodings: [], contentTypes: [] }; 26 | const respondWithErrors = true; 27 | const logger: ILogger = { 28 | debug: vitest.fn(), 29 | error: vitest.fn(), 30 | verbose: vitest.fn(), 31 | info: vitest.fn(), 32 | warn: vitest.fn(), 33 | }; 34 | 35 | it('should call default handler with correct params', () => { 36 | const defaultServerlessHandler = vitest.fn(() => Promise.resolve(response)); 37 | const defaultGetHandler = vitest 38 | .spyOn(DefaultHandler.prototype, 'getHandler') 39 | .mockImplementation(() => defaultServerlessHandler); 40 | 41 | const getHandlerArguments = [ 42 | app, 43 | mockFramework, 44 | adapters, 45 | resolver, 46 | binarySettings, 47 | respondWithErrors, 48 | logger, 49 | ] as const; 50 | 51 | const azureHandler = azureHandlerFactory.getHandler(...getHandlerArguments); 52 | 53 | const event = createHttpFunctionEvent('GET', '/'); 54 | 55 | expect(azureHandler(event)).resolves.toBe(response); 56 | 57 | expect(defaultGetHandler).toHaveBeenCalledWith(...getHandlerArguments); 58 | expect(defaultServerlessHandler).toHaveBeenCalledWith( 59 | event, 60 | undefined, 61 | undefined, 62 | ); 63 | }); 64 | }); 65 | -------------------------------------------------------------------------------- /test/core/stream.spec.ts: -------------------------------------------------------------------------------- 1 | import { ObjectReadableMock, ObjectWritableMock } from 'stream-mock'; 2 | import { describe, expect, it } from 'vitest'; 3 | import { NO_OP, waitForStreamComplete } from '../../src'; 4 | import ErrorReadableMock from './utils/stream'; 5 | 6 | describe('waitForStreamComplete', () => { 7 | it('should wait for the writable stream to complete', async () => { 8 | const testedData = 'test'; 9 | 10 | const read = new ObjectReadableMock(testedData); 11 | const writer = new ObjectWritableMock(); 12 | 13 | read.pipe(writer); 14 | 15 | const waitedStream = await waitForStreamComplete(writer); 16 | 17 | expect(waitedStream).toBe(writer); 18 | expect(writer.data.join('')).toBe(testedData); 19 | 20 | const waitedStream2 = await waitForStreamComplete(writer); 21 | 22 | expect(waitedStream2).toBe(writer); 23 | expect(writer.data.join('')).toBe(testedData); 24 | }); 25 | 26 | it('should wait for the readable stream to complete', async () => { 27 | const testedData: number[] = [0, 1, 2, 3, 4]; 28 | 29 | const read = new ObjectReadableMock(testedData); 30 | const resultData: number[] = []; 31 | 32 | read.on('data', value => resultData.push(value)); 33 | 34 | const waitedStream = await waitForStreamComplete(read); 35 | 36 | expect(waitedStream).toBe(read); 37 | expect(resultData).toStrictEqual(testedData); 38 | 39 | const waitedStream2 = await waitForStreamComplete(read); 40 | 41 | expect(waitedStream2).toBe(read); 42 | expect(resultData).toStrictEqual(testedData); 43 | }); 44 | 45 | it('should throw error when error occours', async () => { 46 | const error = new Error('error on read'); 47 | 48 | const read = new ErrorReadableMock(error, { objectMode: true }); 49 | 50 | read.on('data', NO_OP); 51 | 52 | await expect(waitForStreamComplete(read)).rejects.toThrowError(error); 53 | }); 54 | 55 | it('should handle correctly if events emit end and finish', async () => { 56 | const testedData: number[] = [0, 1, 2, 3, 4]; 57 | const read = new ObjectReadableMock(testedData); 58 | 59 | setTimeout(() => { 60 | read.pause(); 61 | 62 | read.emit('error'); 63 | read.emit('end'); 64 | read.emit('finish'); 65 | 66 | read.resume(); 67 | }, 100); 68 | 69 | await expect(waitForStreamComplete(read)).resolves.not.toThrowError(); 70 | }); 71 | }); 72 | -------------------------------------------------------------------------------- /.tmuxinator.yml: -------------------------------------------------------------------------------- 1 | # ./.tmuxinator.yml 2 | 3 | name: serverless-adapter 4 | root: ./ 5 | 6 | # Optional tmux socket 7 | # socket_name: foo 8 | 9 | # Note that the pre and post options have been deprecated and will be replaced by 10 | # project hooks. 11 | 12 | # Project hooks 13 | 14 | # Runs on project start, always 15 | # on_project_start: command 16 | 17 | # Run on project start, the first time 18 | # on_project_first_start: command 19 | 20 | # Run on project start, after the first time 21 | # on_project_restart: command 22 | 23 | # Run on project exit ( detaching from tmux session ) 24 | # on_project_exit: command 25 | 26 | # Run on project stop 27 | # on_project_stop: cd liga.api.nestjs.eng && docker-compose stop postgres 28 | 29 | # Runs in each window and pane before window/pane specific commands. Useful for setting up interpreter versions. 30 | # pre_window: rbenv shell 2.0.0-p247 31 | 32 | # Pass command line options to tmux. Useful for specifying a different tmux.conf. 33 | # tmux_options: -f ~/.tmux.mac.conf 34 | 35 | # Change the command to call tmux. This can be used by derivatives/wrappers like byobu. 36 | # tmux_command: byobu 37 | 38 | # Specifies (by name or index) which window will be selected on project startup. If not set, the first window is used. 39 | # startup_window: editor 40 | 41 | # Specifies (by index) which pane of the specified window will be selected on project startup. If not set, the first pane is used. 42 | # startup_pane: 1 43 | 44 | # Controls whether the tmux session should be attached to automatically. Defaults to true. 45 | # attach: false 46 | 47 | windows: 48 | - lib: 49 | layout: tilled 50 | # Synchronize all panes of this window, can be enabled before or after the pane commands run. 51 | # 'before' represents legacy functionality and will be deprecated in a future release, in favour of 'after' 52 | # synchronize: after 53 | panes: 54 | - npm run test:watch 55 | - clear 56 | - docs: 57 | layout: tilled 58 | # Synchronize all panes of this window, can be enabled before or after the pane commands run. 59 | # 'before' represents legacy functionality and will be deprecated in a future release, in favour of 'after' 60 | # synchronize: after 61 | panes: 62 | - cd www && npm run start 63 | - cd www 64 | # - api: cd backend && npm run start:debug 65 | # - app: cd app && ng serve 66 | -------------------------------------------------------------------------------- /www/docs/main/handlers/gcp.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Google Cloud Functions 3 | description: See more about how to integrate with Google Cloud Functions. 4 | --- 5 | 6 | :::tip 7 | 8 | Don't know what a handler is? See the [Architecture](../architecture#handler) section. 9 | 10 | ::: 11 | 12 | ## Requirements 13 | 14 | First, install the types for this adapter: 15 | 16 | ```bash 17 | npm i --save @google-cloud/functions-framework 18 | ``` 19 | 20 | ## Integrating with Http Events 21 | 22 | To use, you can import [GCPHandler](../../api/Handlers/GCPHandler) and call the method [setHandler](../../api/ServerlessAdapter#method-sethandler), as per the code below: 23 | 24 | ```ts title="index.ts" 25 | import { ServerlessAdapter } from '@h4ad/serverless-adapter'; 26 | import { DummyAdapter } from '@h4ad/serverless-adapter/adapters/dummy'; 27 | import { GCPHandler } from '@h4ad/serverless-adapter/handlers/gcp'; 28 | import { DummyResolver } from '@h4ad/serverless-adapter/resolvers/dummy'; 29 | import app from './app'; 30 | 31 | const functionName = 'helloWorld'; 32 | 33 | // this handler don't need to export 34 | ServerlessAdapter.new(app) 35 | .setHandler(new GCPHandler(functionName)) 36 | .setResolver(new DummyResolver()) 37 | // choose the framework of your app 38 | // .setFramework(new ExpressFramework()) 39 | .addAdapter(new DummyAdapter()) 40 | .build(); 41 | ``` 42 | 43 | When defining `functionName`, on deploy, you MUST set the `--entry-point` flag when running `gcloud deploy` to the same value. 44 | 45 | :::danger Function Version 46 | 47 | You MUST use `--gen2` flag when running `gcloud deploy`, this library was designed to only work with `gen2` of Google Cloud Function. 48 | 49 | ::: 50 | 51 | :::info About Resolver and Adapter 52 | 53 | You should use `DummyResolver` and `DummyAdapter` because it's a requirement for the library, but `GCPHandler` doesn't do anything with them, 54 | so you use those dummy versions just to satisfy the library requirements. 55 | 56 | ::: 57 | 58 | ## What about Pub/Sub, Storage, etc? 59 | 60 | I could not think yet in an API to handle those cases well, 61 | so currently I didn't add support to these type of Google Cloud Functions. 62 | 63 | If you have some idea about a design for those APIs, [create an issue](https://github.com/H4ad/serverless-adapter/issues/new/choose). 64 | 65 | ## Examples 66 | 67 | You can see examples of how to use [here](https://github.com/H4ad/serverless-adapter-examples). 68 | 69 | -------------------------------------------------------------------------------- /www/docs/main/resolvers/aws-context.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: AWS Context 3 | position: 3 4 | description: See more about the AWS Context Resolver. 5 | --- 6 | 7 | :::tip 8 | 9 | Don't know what a resolver is? See the [Architecture](../architecture#resolver) section. 10 | 11 | ::: 12 | 13 | :::note Deprecated 14 | 15 | From the AWS Documentation, describing the functions used in this resolver: Functions for compatibility with earlier Node.js Runtime v0.10.42. No longer documented, so they are deprecated, but they still work as of the 12.x runtime, so they are not removed from the types. 16 | 17 | ::: 18 | 19 | This resolver only works in AWS and it was created just to be fully compatible with the resolution mode of [@vendia/serverless-express](https://github.com/vendia/serverless-express/blob/mainline/src/make-resolver.js#L9). 20 | 21 | When the handler is created with [getHandler](../../api/Contracts/HandlerContract#method-gethandler), it will return void 22 | and the cloud will wait until the event loop is empty. 23 | This happens when your framework sends the response and the adapter transforms the response the way your cloud can handle, 24 | at this point the response will be passed to the functions `succeed` or `fail` that is exposed by `context` object. 25 | 26 | You can use this resolver only with AWS. 27 | 28 | :::caution AWS 29 | 30 | To use this resolver on AWS, you MUST leave [callbackWaitsForEmptyEventLoop](https://docs.aws.amazon.com/lambda/latest/dg/nodejs-context.html) as true, otherwise, AWS will not wait for this resolver to resolve. 31 | 32 | ::: 33 | 34 | # Usage 35 | 36 | To use, you can import and call the method [setResolver](../../api/ServerlessAdapter#method-setresolver), as per the code below: 37 | 38 | ```ts title="index.ts" 39 | import { ServerlessAdapter } from '@h4ad/serverless-adapter'; 40 | import { AwsContextResolver } from '@h4ad/serverless-adapter/resolvers/aws-context'; 41 | 42 | const express = require('express'); 43 | 44 | const app = express(); 45 | export const handler = ServerlessAdapter.new(app) 46 | .setResolver(new AwsContextResolver()) 47 | // continue to set the other options here. 48 | //.setFramework(new ExpressFramework()) 49 | //.setHandler(new DefaultHandler()) 50 | //.addAdapter(new AlbAdapter()) 51 | //.addAdapter(new SQSAdapter()) 52 | //.addAdapter(new SNSAdapter()) 53 | // after put all methods necessary, just call the build method. 54 | .build(); 55 | ``` 56 | 57 | -------------------------------------------------------------------------------- /src/adapters/aws/s3.adapter.ts: -------------------------------------------------------------------------------- 1 | //#region Imports 2 | 3 | import type { S3Event } from 'aws-lambda'; 4 | import { getDefaultIfUndefined } from '../../core'; 5 | import { AwsSimpleAdapter } from './base/index'; 6 | 7 | //#endregion 8 | 9 | /** 10 | * The options to customize the {@link S3Adapter} 11 | * 12 | * @breadcrumb Adapters / AWS / S3Adapter 13 | * @public 14 | */ 15 | export interface S3AdapterOptions { 16 | /** 17 | * The path that will be used to create a request to be forwarded to the framework. 18 | * 19 | * @defaultValue /s3 20 | */ 21 | s3ForwardPath?: string; 22 | 23 | /** 24 | * The http method that will be used to create a request to be forwarded to the framework. 25 | * 26 | * @defaultValue POST 27 | */ 28 | s3ForwardMethod?: string; 29 | } 30 | 31 | /** 32 | * The adapter to handle requests from AWS S3. 33 | * 34 | * The option of `responseWithErrors` is ignored by this adapter and we always call `resolver.fail` with the error. 35 | * 36 | * {@link https://docs.aws.amazon.com/lambda/latest/dg/with-s3.html | Event Reference} 37 | * 38 | * @example 39 | * ```typescript 40 | * const s3ForwardPath = '/your/route/s3'; // default /s3 41 | * const s3ForwardMethod = 'POST'; // default POST 42 | * const adapter = new S3Adapter({ s3ForwardPath, s3ForwardMethod }); 43 | * ``` 44 | * 45 | * @breadcrumb Adapters / AWS / S3Adapter 46 | * @public 47 | */ 48 | export class S3Adapter extends AwsSimpleAdapter { 49 | //#region Constructor 50 | 51 | /** 52 | * Default constructor 53 | * 54 | * @param options - The options to customize the {@link SNSAdapter} 55 | */ 56 | constructor(options?: S3AdapterOptions) { 57 | super({ 58 | forwardPath: getDefaultIfUndefined(options?.s3ForwardPath, '/s3'), 59 | forwardMethod: getDefaultIfUndefined(options?.s3ForwardMethod, 'POST'), 60 | batch: false, 61 | host: 's3.amazonaws.com', 62 | }); 63 | } 64 | 65 | //#endregion 66 | 67 | //#region Public Methods 68 | 69 | /** 70 | * {@inheritDoc} 71 | */ 72 | public override getAdapterName(): string { 73 | return S3Adapter.name; 74 | } 75 | 76 | /** 77 | * {@inheritDoc} 78 | */ 79 | public override canHandle(event: unknown): event is S3Event { 80 | const s3Event = event as Partial; 81 | 82 | if (!Array.isArray(s3Event?.Records)) return false; 83 | 84 | const eventSource = s3Event.Records[0]?.eventSource; 85 | 86 | return eventSource === 'aws:s3'; 87 | } 88 | 89 | //#endregion 90 | } 91 | -------------------------------------------------------------------------------- /src/adapters/aws/sns.adapter.ts: -------------------------------------------------------------------------------- 1 | //#region Imports 2 | 3 | import type { SNSEvent } from 'aws-lambda'; 4 | import { getDefaultIfUndefined } from '../../core'; 5 | import { AwsSimpleAdapter } from './base'; 6 | 7 | //#endregion 8 | 9 | /** 10 | * The options to customize the {@link SNSAdapter} 11 | * 12 | * @breadcrumb Adapters / AWS / SNSAdapter 13 | * @public 14 | */ 15 | export interface SNSAdapterOptions { 16 | /** 17 | * The path that will be used to create a request to be forwarded to the framework. 18 | * 19 | * @defaultValue /sns 20 | */ 21 | snsForwardPath?: string; 22 | 23 | /** 24 | * The http method that will be used to create a request to be forwarded to the framework. 25 | * 26 | * @defaultValue POST 27 | */ 28 | snsForwardMethod?: string; 29 | } 30 | 31 | /** 32 | * The adapter to handle requests from AWS SNS. 33 | * 34 | * The option of `responseWithErrors` is ignored by this adapter and we always call `resolver.fail` with the error. 35 | * 36 | * {@link https://docs.aws.amazon.com/lambda/latest/dg/with-sns.html | Event Reference} 37 | * 38 | * @example 39 | * ```typescript 40 | * const snsForwardPath = '/your/route/sns'; // default /sns 41 | * const snsForwardMethod = 'POST'; // default POST 42 | * const adapter = new SNSAdapter({ snsForwardPath, snsForwardMethod }); 43 | * ``` 44 | * 45 | * @breadcrumb Adapters / AWS / SNSAdapter 46 | * @public 47 | */ 48 | export class SNSAdapter extends AwsSimpleAdapter { 49 | //#region Constructor 50 | 51 | /** 52 | * Default constructor 53 | * 54 | * @param options - The options to customize the {@link SNSAdapter} 55 | */ 56 | constructor(options?: SNSAdapterOptions) { 57 | super({ 58 | forwardPath: getDefaultIfUndefined(options?.snsForwardPath, '/sns'), 59 | forwardMethod: getDefaultIfUndefined(options?.snsForwardMethod, 'POST'), 60 | batch: false, 61 | host: 'sns.amazonaws.com', 62 | }); 63 | } 64 | 65 | //#endregion 66 | 67 | //#region Public Methods 68 | 69 | /** 70 | * {@inheritDoc} 71 | */ 72 | public override getAdapterName(): string { 73 | return SNSAdapter.name; 74 | } 75 | 76 | /** 77 | * {@inheritDoc} 78 | */ 79 | public override canHandle(event: unknown): event is SNSEvent { 80 | const snsEvent = event as Partial; 81 | 82 | if (!Array.isArray(snsEvent?.Records)) return false; 83 | 84 | const eventSource = snsEvent.Records[0]?.EventSource; 85 | 86 | return eventSource === 'aws:sns'; 87 | } 88 | 89 | //#endregion 90 | } 91 | -------------------------------------------------------------------------------- /www/docs/main/getting-started/usage.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Usage 3 | position: 2 4 | description: See more about how to use this library. 5 | --- 6 | 7 | To start to use, first you need to know what you need to import, let's start showing the [ServerlessAdapter](/docs/api/ServerlessAdapter). 8 | 9 | ```tsx 10 | import { ServerlessAdapter } from '@h4ad/serverless-adapter'; 11 | ``` 12 | 13 | We need to pass to [Serverless Adapter](/docs/api/ServerlessAdapter) the instance of your api, let's look an example with: 14 | 15 | - Framework: [Express](../frameworks/express). 16 | - Adapters: [AWS Api Gateway V2 Adapter](../adapters/aws/api-gateway-v2). 17 | - Handler: [Default Handler](../handlers/aws). 18 | - Resolver: [Promise Resolver](../resolvers/promise). 19 | 20 | :::warning 21 | If you are using `Typescript`, imports will only work exactly as in the example 22 | if you set your `moduleResolution` to `nodenext` or `bundler` in your `tsconfig.json`. 23 | 24 | If you don't want to change this, you should append `/lib` after the package name, 25 | like `@h4ad/serverless-adapter/lib/frameworks/express/index`. 26 | ::: 27 | 28 | ```ts 29 | import { ServerlessAdapter } from '@h4ad/serverless-adapter'; 30 | import { ExpressFramework } from '@h4ad/serverless-adapter/frameworks/express'; 31 | import { DefaultHandler } from '@h4ad/serverless-adapter/handlers/default'; 32 | import { PromiseResolver } from '@h4ad/serverless-adapter/resolvers/promise'; 33 | import { ApiGatewayV2Adapter } from '@h4ad/serverless-adapter/adapters/aws'; 34 | 35 | const express = require('express'); 36 | 37 | const app = express(); 38 | export const handler = ServerlessAdapter.new(app) 39 | .setFramework(new ExpressFramework()) 40 | .setHandler(new DefaultHandler()) 41 | .setResolver(new PromiseResolver()) 42 | .addAdapter(new ApiGatewayV2Adapter()) 43 | // if you need more adapters 44 | // just append more `addAdapter` calls 45 | .build(); 46 | ``` 47 | 48 | :::tip 49 | 50 | To know more about the frameworks that you can use, see [Frameworks Section](../../category/frameworks). 51 | 52 | ::: 53 | 54 | :::tip 55 | 56 | To know more about the handlers that you can use, see [Handlers Section](../../category/handlers). 57 | 58 | ::: 59 | 60 | 61 | :::tip 62 | 63 | To know more about the resolvers that you can use, see [Resolvers Section](../../category/resolvers). 64 | 65 | ::: 66 | 67 | 68 | :::tip 69 | 70 | To know more about the adapters that you can use, see [Adapters Section](../../category/adapters). 71 | 72 | ::: 73 | -------------------------------------------------------------------------------- /test/adapters/huawei/utils/huawei-api-gateway.ts: -------------------------------------------------------------------------------- 1 | import type { BothValueHeaders } from '../../../../src'; 2 | import type { 3 | HuaweiApiGatewayEvent, 4 | HuaweiRequestQueryStringParameters, 5 | } from '../../../../src/@types/huawei'; 6 | 7 | export function createHuaweiApiGateway( 8 | method: string, 9 | path: string, 10 | body?: object, 11 | headers?: BothValueHeaders, 12 | queryParams?: HuaweiRequestQueryStringParameters, 13 | ): HuaweiApiGatewayEvent { 14 | const bodyBuffer = Buffer.from(JSON.stringify(body || ''), 'utf-8'); 15 | 16 | return { 17 | body: body ? bodyBuffer.toString('base64') : '', 18 | headers: { 19 | accept: 20 | 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9', 21 | 'accept-encoding': 'gzip, deflate, br', 22 | 'accept-language': 'pt-BR,pt;q=0.9,en-US;q=0.8,en;q=0.7,bg;q=0.6', 23 | 'cache-control': 'max-age=0', 24 | 'content-length': Buffer.byteLength(bodyBuffer).toString(), 25 | connection: 'keep-alive', 26 | dnt: '1', 27 | host: 'test.apig.la-south-2.huaweicloudapis.com', 28 | 'sec-ch-ua': 29 | '" Not A;Brand";v="99", "Chromium";v="101", "Google Chrome";v="101"', 30 | 'sec-ch-ua-mobile': '?0', 31 | 'sec-ch-ua-platform': '"Windows"', 32 | 'sec-fetch-dest': 'document', 33 | 'sec-fetch-mode': 'navigate', 34 | 'sec-fetch-site': 'none', 35 | 'sec-fetch-user': '?1', 36 | 'upgrade-insecure-requests': '1', 37 | 'user-agent': 38 | 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/101.0.4951.67 Safari/537.36', 39 | 'x-forwarded-for': '33.33.33.33', 40 | 'x-forwarded-host': 'test.apig.la-south-2.huaweicloudapis.com', 41 | 'x-forwarded-port': '443', 42 | 'x-forwarded-proto': 'https', 43 | 'x-real-ip': '33.33.33.33', 44 | 'x-request-id': 'eb6f50b5922fd574175f8115ba22c168', 45 | ...headers, 46 | }, 47 | httpMethod: method, 48 | isBase64Encoded: true, 49 | 'lubanops-gtrace-id': '', 50 | 'lubanops-ndomain-id': '', 51 | 'lubanops-nenv-id': '', 52 | 'lubanops-nspan-id': '', 53 | 'lubanops-ntrace-id': '', 54 | 'lubanops-sevent-id': '', 55 | path, 56 | pathParameters: {}, 57 | queryStringParameters: { 58 | ...queryParams, 59 | }, 60 | requestContext: { 61 | apiId: '863aad9dd5dd4043b7f6745b34922323', 62 | requestId: 'eb6f50b5922fd574175f8115ba943222', 63 | stage: 'RELEASE', 64 | }, 65 | }; 66 | } 67 | -------------------------------------------------------------------------------- /test/frameworks/http-deepkit.framework.spec.ts: -------------------------------------------------------------------------------- 1 | import { App } from '@deepkit/app'; 2 | import { 3 | HttpKernel, 4 | HttpModule, 5 | HttpRouterRegistry, 6 | JSONResponse, 7 | } from '@deepkit/http'; 8 | import { describe, expect, it, vitest } from 'vitest'; 9 | import { 10 | ServerlessRequest, 11 | ServerlessResponse, 12 | waitForStreamComplete, 13 | } from '../../src'; 14 | import { HttpDeepkitFramework } from '../../src/frameworks/deepkit'; 15 | 16 | it('should convert correctly when the value is not an buffer', async () => { 17 | const framework = new HttpDeepkitFramework(); 18 | const kernel: Partial = { 19 | handleRequest: vitest.fn((request, response) => { 20 | request.pipe(response); 21 | 22 | return void 0 as any; 23 | }), 24 | }; 25 | const textCodes = 'test'.split('').map(c => c.charCodeAt(0)); 26 | 27 | const request = new ServerlessRequest({ 28 | body: Uint8Array.of(...textCodes), 29 | url: '/test', 30 | method: 'POST', 31 | headers: {}, 32 | }); 33 | const response = new ServerlessResponse({ 34 | method: 'POST', 35 | }); 36 | 37 | framework.sendRequest(kernel as HttpKernel, request, response); 38 | 39 | await waitForStreamComplete(response); 40 | 41 | const resultBody = ServerlessResponse.body(response); 42 | 43 | expect(resultBody).toBeInstanceOf(Buffer); 44 | expect(resultBody.toString()).toEqual('test'); 45 | }); 46 | 47 | describe('deepkit', () => { 48 | it('should return valid json on get request', async () => { 49 | const app = new App({ 50 | imports: [new HttpModule()], 51 | }); 52 | 53 | const body = { test: 'ok' }; 54 | 55 | app.get(HttpRouterRegistry).get('/', () => { 56 | return new JSONResponse(body, 200).header('response-header', 'true'); 57 | }); 58 | 59 | const request = new ServerlessRequest({ 60 | method: 'GET', 61 | url: '/', 62 | headers: {}, 63 | }); 64 | 65 | const response = new ServerlessResponse({ 66 | method: 'GET', 67 | }); 68 | const framework = new HttpDeepkitFramework(); 69 | const httpKernel = app.get(HttpKernel); 70 | 71 | framework.sendRequest(httpKernel, request, response); 72 | 73 | await waitForStreamComplete(response); 74 | 75 | const resultBody = ServerlessResponse.body(response); 76 | 77 | expect(resultBody.toString('utf-8')).toEqual(JSON.stringify(body)); 78 | expect(response.statusCode).toBe(200); 79 | expect(ServerlessResponse.headers(response)).toHaveProperty( 80 | 'response-header', 81 | 'true', 82 | ); 83 | }); 84 | }); 85 | -------------------------------------------------------------------------------- /tsup.config.ts: -------------------------------------------------------------------------------- 1 | import { execSync } from 'node:child_process'; 2 | import { defineConfig } from 'tsup'; 3 | 4 | const adapters = [ 5 | 'apollo-server', 6 | 'aws', 7 | 'azure', 8 | 'digital-ocean', 9 | 'dummy', 10 | 'huawei', 11 | ]; 12 | 13 | const frameworks = [ 14 | 'apollo-server', 15 | 'body-parser', 16 | 'cors', 17 | 'deepkit', 18 | 'express', 19 | 'fastify', 20 | 'hapi', 21 | 'koa', 22 | 'lazy', 23 | 'polka', 24 | 'trpc', 25 | ]; 26 | 27 | const handlers = [ 28 | 'aws', 29 | 'azure', 30 | 'default', 31 | 'digital-ocean', 32 | 'firebase', 33 | 'gcp', 34 | 'huawei', 35 | ]; 36 | 37 | const resolvers = ['aws-context', 'callback', 'dummy', 'promise']; 38 | 39 | const libEntries = [ 40 | ...adapters.map(adapter => `src/adapters/${adapter}/index.ts`), 41 | ...frameworks.map(framework => `src/frameworks/${framework}/index.ts`), 42 | ...handlers.map(handler => `src/handlers/${handler}/index.ts`), 43 | ...resolvers.map(resolver => `src/resolvers/${resolver}/index.ts`), 44 | ]; 45 | 46 | const createExport = (filePath: string) => ({ 47 | import: { 48 | types: `./lib/${filePath}.d.ts`, 49 | default: `./lib/${filePath}.mjs`, 50 | }, 51 | require: { 52 | types: `./lib/${filePath}.d.cts`, 53 | default: `./lib/${filePath}.cjs`, 54 | }, 55 | }); 56 | 57 | const createExportReducer = 58 | (initialPath: string) => (acc: object, name: string) => { 59 | acc[`./${initialPath}/${name}`] = createExport( 60 | `${initialPath}/${name}/index`, 61 | ); 62 | 63 | acc[`./lib/${initialPath}/${name}`] = createExport( 64 | `${initialPath}/${name}/index`, 65 | ); 66 | 67 | return acc; 68 | }; 69 | 70 | const packageExports = { 71 | '.': createExport('index'), 72 | ...adapters.reduce(createExportReducer('adapters'), {}), 73 | ...frameworks.reduce(createExportReducer('frameworks'), {}), 74 | ...handlers.reduce(createExportReducer('handlers'), {}), 75 | ...resolvers.reduce(createExportReducer('resolvers'), {}), 76 | }; 77 | 78 | execSync(`npm pkg set exports='${JSON.stringify(packageExports)}' --json`); 79 | 80 | export default defineConfig({ 81 | outDir: './lib', 82 | clean: true, 83 | dts: true, 84 | format: ['esm', 'cjs'], 85 | outExtension: ({ format }) => ({ 86 | js: format === 'cjs' ? '.cjs' : '.mjs', 87 | }), 88 | cjsInterop: true, 89 | entry: ['src/index.ts', ...libEntries], 90 | sourcemap: true, 91 | skipNodeModulesBundle: true, 92 | minify: true, 93 | target: 'es2022', 94 | tsconfig: './tsconfig.build.json', 95 | keepNames: true, 96 | bundle: true, 97 | }); 98 | -------------------------------------------------------------------------------- /test/adapters/aws/utils/api-gateway-v2.ts: -------------------------------------------------------------------------------- 1 | import type { APIGatewayProxyEventV2 } from 'aws-lambda'; 2 | import type { APIGatewayProxyEventQueryStringParameters } from 'aws-lambda/trigger/api-gateway-proxy'; 3 | import { getQueryParamsStringFromRecord } from '../../../../src'; 4 | 5 | export function createApiGatewayV2( 6 | method: string, 7 | path: string, 8 | body?: Record, 9 | headers?: Record, 10 | queryParams?: APIGatewayProxyEventQueryStringParameters, 11 | cookies?: APIGatewayProxyEventV2['cookies'], 12 | ): APIGatewayProxyEventV2 { 13 | return { 14 | version: '2.0', 15 | routeKey: '$default', 16 | rawPath: path, 17 | rawQueryString: getQueryParamsStringFromRecord(queryParams || {}), 18 | queryStringParameters: queryParams, 19 | headers: { 20 | accept: 21 | 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9', 22 | 'accept-encoding': 'gzip, deflate, br', 23 | 'accept-language': 'en-US,en;q=0.9', 24 | 'cache-control': 'max-age=0', 25 | 'content-length': '0', 26 | host: '6bwvllq3t2.execute-api.us-east-1.amazonaws.com', 27 | 'sec-fetch-dest': 'document', 28 | 'sec-fetch-mode': 'navigate', 29 | 'sec-fetch-site': 'none', 30 | 'sec-fetch-user': '?1', 31 | 'upgrade-insecure-requests': '1', 32 | 'user-agent': 33 | 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36', 34 | 'x-amzn-trace-id': 'Root=1-5ff59707-4914805430277a6209549a59', 35 | 'x-forwarded-for': '203.123.103.37', 36 | 'x-forwarded-port': '443', 37 | 'x-forwarded-proto': 'https', 38 | ...headers, 39 | }, 40 | cookies, 41 | requestContext: { 42 | accountId: '347971939225', 43 | apiId: '6bwvllq3t2', 44 | domainName: '6bwvllq3t2.execute-api.us-east-1.amazonaws.com', 45 | domainPrefix: '6bwvllq3t2', 46 | http: { 47 | method, 48 | path, 49 | protocol: 'HTTP/1.1', 50 | sourceIp: '203.123.103.37', 51 | userAgent: 52 | 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36', 53 | }, 54 | requestId: 'YuSJQjZfoAMESbg=', 55 | routeKey: '$default', 56 | stage: '$default', 57 | time: '06/Jan/2021:10:55:03 +0000', 58 | timeEpoch: 1609930503973, 59 | }, 60 | body: (body && JSON.stringify(body)) || undefined, 61 | isBase64Encoded: false, 62 | }; 63 | } 64 | -------------------------------------------------------------------------------- /src/adapters/aws/sqs.adapter.ts: -------------------------------------------------------------------------------- 1 | //#region Imports 2 | 3 | import type { SQSEvent } from 'aws-lambda'; 4 | import { getDefaultIfUndefined } from '../../core'; 5 | import { type AWSSimpleAdapterOptions, AwsSimpleAdapter } from './base/index'; 6 | 7 | //#endregion 8 | 9 | /** 10 | * The options to customize the {@link SQSAdapter} 11 | * 12 | * @breadcrumb Adapters / AWS / SQSAdapter 13 | * @public 14 | */ 15 | export interface SQSAdapterOptions 16 | extends Pick { 17 | /** 18 | * The path that will be used to create a request to be forwarded to the framework. 19 | * 20 | * @defaultValue /sqs 21 | */ 22 | sqsForwardPath?: string; 23 | 24 | /** 25 | * The http method that will be used to create a request to be forwarded to the framework. 26 | * 27 | * @defaultValue POST 28 | */ 29 | sqsForwardMethod?: string; 30 | } 31 | 32 | /** 33 | * The adapter to handle requests from AWS SQS. 34 | * 35 | * The option of `responseWithErrors` is ignored by this adapter and we always call `resolver.fail` with the error. 36 | * 37 | * {@link https://docs.aws.amazon.com/lambda/latest/dg/with-sqs.html | Event Reference} 38 | * 39 | * @example 40 | * ```typescript 41 | * const sqsForwardPath = '/your/route/sqs'; // default /sqs 42 | * const sqsForwardMethod = 'POST'; // default POST 43 | * const adapter = new SQSAdapter({ sqsForwardPath, sqsForwardMethod }); 44 | * ``` 45 | * 46 | * @breadcrumb Adapters / AWS / SQSAdapter 47 | * @public 48 | */ 49 | export class SQSAdapter extends AwsSimpleAdapter { 50 | //#region Constructor 51 | 52 | /** 53 | * Default constructor 54 | * 55 | * @param options - The options to customize the {@link SNSAdapter} 56 | */ 57 | constructor(options?: SQSAdapterOptions) { 58 | super({ 59 | forwardPath: getDefaultIfUndefined(options?.sqsForwardPath, '/sqs'), 60 | forwardMethod: getDefaultIfUndefined(options?.sqsForwardMethod, 'POST'), 61 | batch: options?.batch, 62 | host: 'sqs.amazonaws.com', 63 | }); 64 | } 65 | 66 | //#endregion 67 | 68 | //#region Public Methods 69 | 70 | /** 71 | * {@inheritDoc} 72 | */ 73 | public override getAdapterName(): string { 74 | return SQSAdapter.name; 75 | } 76 | 77 | /** 78 | * {@inheritDoc} 79 | */ 80 | public override canHandle(event: unknown): event is SQSEvent { 81 | const sqsEvent = event as Partial; 82 | 83 | if (!Array.isArray(sqsEvent?.Records)) return false; 84 | 85 | const eventSource = sqsEvent.Records[0]?.eventSource; 86 | 87 | return eventSource === 'aws:sqs'; 88 | } 89 | 90 | //#endregion 91 | } 92 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | var 9 | 10 | # Diagnostic reports (https://nodejs.org/api/report.html) 11 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 12 | 13 | # Runtime data 14 | pids 15 | *.pid 16 | *.seed 17 | *.pid.lock 18 | 19 | # Directory for instrumented libs generated by jscoverage/JSCover 20 | lib-cov 21 | 22 | # Coverage directory used by tools like istanbul 23 | coverage 24 | *.lcov 25 | 26 | # nyc test coverage 27 | .nyc_output 28 | 29 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 30 | .grunt 31 | 32 | # Bower dependency directory (https://bower.io/) 33 | bower_components 34 | 35 | # node-waf configuration 36 | .lock-wscript 37 | 38 | # Compiled binary addons (https://nodejs.org/api/addons.html) 39 | build/Release 40 | 41 | # Dependency directories 42 | node_modules/ 43 | jspm_packages/ 44 | 45 | # Snowpack dependency directory (https://snowpack.dev/) 46 | web_modules/ 47 | 48 | # TypeScript cache 49 | *.tsbuildinfo 50 | 51 | # Optional npm cache directory 52 | .npm 53 | 54 | # Optional eslint cache 55 | .eslintcache 56 | 57 | # Microbundle cache 58 | .rpt2_cache/ 59 | .rts2_cache_cjs/ 60 | .rts2_cache_es/ 61 | .rts2_cache_umd/ 62 | 63 | # Optional REPL history 64 | .node_repl_history 65 | 66 | # Output of 'npm pack' 67 | *.tgz 68 | 69 | # Yarn Integrity file 70 | .yarn-integrity 71 | 72 | # dotenv environment variables file 73 | .env.test 74 | .env.local 75 | 76 | # parcel-bundler cache (https://parceljs.org/) 77 | .cache 78 | .parcel-cache 79 | 80 | # Next.js build output 81 | .next 82 | out 83 | 84 | # Nuxt.js build / generate output 85 | .nuxt 86 | dist 87 | 88 | # Gatsby files 89 | .cache/ 90 | # Comment in the public line in if your project uses Gatsby and not Next.js 91 | # https://nextjs.org/blog/next-9-1#public-directory-support 92 | # public 93 | 94 | # vuepress build output 95 | .vuepress/dist 96 | 97 | # Serverless directories 98 | .serverless/ 99 | 100 | # FuseBox cache 101 | .fusebox/ 102 | 103 | # DynamoDB Local files 104 | .dynamodb/ 105 | 106 | # TernJS port file 107 | .tern-port 108 | 109 | # Stores VSCode versions used for testing VSCode extensions 110 | .vscode-test 111 | 112 | # yarn v2 113 | .yarn/cache 114 | .yarn/unplugged 115 | .yarn/build-state.yml 116 | .yarn/install-state.gz 117 | .pnp.* 118 | 119 | # Compiled code 120 | lib/ 121 | 122 | # IDEs 123 | .idea 124 | 125 | # tsdoc 126 | temp 127 | /etc 128 | !etc/.gitkeep 129 | 130 | # docs 131 | www/api/* 132 | !www/api/.gitkeep 133 | 134 | .env 135 | perf/ 136 | -------------------------------------------------------------------------------- /test/adapters/aws/s3.adapter.spec.ts: -------------------------------------------------------------------------------- 1 | import { beforeEach, describe, expect, it } from 'vitest'; 2 | import { getEventBodyAsBuffer } from '../../../src'; 3 | import { S3Adapter } from '../../../src/adapters/aws'; 4 | import { createCanHandleTestsForAdapter } from '../utils/can-handle'; 5 | import { createS3Event } from './utils/s3'; 6 | 7 | describe(S3Adapter.name, () => { 8 | let adapter!: S3Adapter; 9 | 10 | beforeEach(() => { 11 | adapter = new S3Adapter(); 12 | }); 13 | 14 | describe('getAdapterName', () => { 15 | it('should be the same name of the class', () => { 16 | expect(adapter.getAdapterName()).toBe(S3Adapter.name); 17 | }); 18 | }); 19 | 20 | createCanHandleTestsForAdapter(() => new S3Adapter(), undefined); 21 | 22 | describe('getRequest', () => { 23 | it('should return the correct mapping for the request', () => { 24 | const event = createS3Event(); 25 | 26 | const result = adapter.getRequest(event); 27 | 28 | expect(result.method).toBe('POST'); 29 | expect(result.path).toBe('/s3'); 30 | expect(result.headers).toHaveProperty('host', 's3.amazonaws.com'); 31 | expect(result.headers).toHaveProperty('content-type', 'application/json'); 32 | 33 | const [bodyBuffer, contentLength] = getEventBodyAsBuffer( 34 | JSON.stringify(event), 35 | false, 36 | ); 37 | 38 | expect(result.body).toBeInstanceOf(Buffer); 39 | expect(result.body).toStrictEqual(bodyBuffer); 40 | 41 | expect(result.headers).toHaveProperty( 42 | 'content-length', 43 | String(contentLength), 44 | ); 45 | }); 46 | 47 | it('should return the correct mapping for the request with custom path and method', () => { 48 | const event = createS3Event(); 49 | 50 | const method = 'PUT'; 51 | const path = '/custom/s3'; 52 | 53 | const customAdapter = new S3Adapter({ 54 | s3ForwardMethod: method, 55 | s3ForwardPath: path, 56 | }); 57 | 58 | const result = customAdapter.getRequest(event); 59 | 60 | expect(result.method).toBe(method); 61 | expect(result.path).toBe(path); 62 | expect(result.headers).toHaveProperty('host', 's3.amazonaws.com'); 63 | expect(result.headers).toHaveProperty('content-type', 'application/json'); 64 | 65 | const [bodyBuffer, contentLength] = getEventBodyAsBuffer( 66 | JSON.stringify(event), 67 | false, 68 | ); 69 | 70 | expect(result.body).toBeInstanceOf(Buffer); 71 | expect(result.body).toStrictEqual(bodyBuffer); 72 | 73 | expect(result.headers).toHaveProperty( 74 | 'content-length', 75 | String(contentLength), 76 | ); 77 | }); 78 | }); 79 | }); 80 | -------------------------------------------------------------------------------- /test/network/request.spec.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it, vitest } from 'vitest'; 2 | import { NO_OP, ServerlessRequest } from '../../src'; 3 | 4 | describe('ServerlessRequest', () => { 5 | it('should can create serverless request from parameters of constructor', () => { 6 | const method = 'POST'; 7 | const url = '/users/batata'; 8 | const headers = { 'Content-Type': 'application/json' }; 9 | const remoteAddress = '168.16.0.1'; 10 | const body = Buffer.from('{"test": true}', 'utf-8'); 11 | 12 | const request = new ServerlessRequest({ 13 | method, 14 | url, 15 | headers, 16 | remoteAddress, 17 | body, 18 | }); 19 | 20 | expect(request).toHaveProperty('statusCode', 200); 21 | expect(request).toHaveProperty('statusMessage', 'OK'); 22 | expect(request).toHaveProperty('url', url); 23 | expect(request).toHaveProperty('headers', headers); 24 | expect(request).toHaveProperty('ip', remoteAddress); 25 | expect(request).toHaveProperty('body', body); 26 | expect(request).toHaveProperty('complete', true); 27 | expect(request).toHaveProperty('httpVersion', '1.1'); 28 | expect(request).toHaveProperty('httpVersionMajor', 1); 29 | expect(request).toHaveProperty('httpVersionMinor', 1); 30 | expect(request.socket).toHaveProperty('encrypted', true); 31 | expect(request.socket).toHaveProperty('readable', true); 32 | expect(request.socket).toHaveProperty('remoteAddress', remoteAddress); 33 | expect(request.socket).toHaveProperty('end', NO_OP); 34 | expect(request.socket).toHaveProperty('destroy', NO_OP); 35 | expect(request.socket.address()).toHaveProperty('port', 443); 36 | }); 37 | 38 | it('should push body property when call _read', () => { 39 | const method = 'POST'; 40 | const url = '/users/batata'; 41 | const headers = { 'Content-Type': 'application/json' }; 42 | const remoteAddress = '168.16.0.1'; 43 | const body = Buffer.from('{"random": 2323}', 'utf-8'); 44 | 45 | const request = new ServerlessRequest({ 46 | method, 47 | url, 48 | headers, 49 | remoteAddress, 50 | body, 51 | }); 52 | 53 | // eslint-disable-next-line @typescript-eslint/unbound-method 54 | request.push = vitest.fn(request.push); 55 | 56 | // eslint-disable-next-line @typescript-eslint/unbound-method 57 | request._read = vitest.fn(request._read); 58 | 59 | request._read(Math.random()); 60 | 61 | // eslint-disable-next-line @typescript-eslint/unbound-method 62 | expect(request.push).toHaveBeenNthCalledWith(1, body); 63 | // eslint-disable-next-line @typescript-eslint/unbound-method 64 | expect(request.push).toHaveBeenNthCalledWith(2, null); 65 | }); 66 | }); 67 | -------------------------------------------------------------------------------- /test/adapters/aws/sns.adapter.spec.ts: -------------------------------------------------------------------------------- 1 | import { beforeEach, describe, expect, it } from 'vitest'; 2 | import { getEventBodyAsBuffer } from '../../../src'; 3 | import { SNSAdapter } from '../../../src/adapters/aws'; 4 | import { createCanHandleTestsForAdapter } from '../utils/can-handle'; 5 | import { createSNSEvent } from './utils/sns'; 6 | 7 | describe(SNSAdapter.name, () => { 8 | let adapter!: SNSAdapter; 9 | 10 | beforeEach(() => { 11 | adapter = new SNSAdapter(); 12 | }); 13 | 14 | describe('getAdapterName', () => { 15 | it('should be the same name of the class', () => { 16 | expect(adapter.getAdapterName()).toBe(SNSAdapter.name); 17 | }); 18 | }); 19 | 20 | createCanHandleTestsForAdapter(() => new SNSAdapter(), undefined); 21 | 22 | describe('getRequest', () => { 23 | it('should return the correct mapping for the request', () => { 24 | const event = createSNSEvent(); 25 | 26 | const result = adapter.getRequest(event); 27 | 28 | expect(result.method).toBe('POST'); 29 | expect(result.path).toBe('/sns'); 30 | expect(result.headers).toHaveProperty('host', 'sns.amazonaws.com'); 31 | expect(result.headers).toHaveProperty('content-type', 'application/json'); 32 | 33 | const [bodyBuffer, contentLength] = getEventBodyAsBuffer( 34 | JSON.stringify(event), 35 | false, 36 | ); 37 | 38 | expect(result.body).toBeInstanceOf(Buffer); 39 | expect(result.body).toStrictEqual(bodyBuffer); 40 | 41 | expect(result.headers).toHaveProperty( 42 | 'content-length', 43 | String(contentLength), 44 | ); 45 | }); 46 | 47 | it('should return the correct mapping for the request with custom path and method', () => { 48 | const event = createSNSEvent(); 49 | 50 | const method = 'PUT'; 51 | const path = '/custom/sns'; 52 | 53 | const customAdapter = new SNSAdapter({ 54 | snsForwardMethod: method, 55 | snsForwardPath: path, 56 | }); 57 | 58 | const result = customAdapter.getRequest(event); 59 | 60 | expect(result.method).toBe(method); 61 | expect(result.path).toBe(path); 62 | expect(result.headers).toHaveProperty('host', 'sns.amazonaws.com'); 63 | expect(result.headers).toHaveProperty('content-type', 'application/json'); 64 | 65 | const [bodyBuffer, contentLength] = getEventBodyAsBuffer( 66 | JSON.stringify(event), 67 | false, 68 | ); 69 | 70 | expect(result.body).toBeInstanceOf(Buffer); 71 | expect(result.body).toStrictEqual(bodyBuffer); 72 | 73 | expect(result.headers).toHaveProperty( 74 | 'content-length', 75 | String(contentLength), 76 | ); 77 | }); 78 | }); 79 | }); 80 | -------------------------------------------------------------------------------- /test/adapters/aws/sqs.adapter.spec.ts: -------------------------------------------------------------------------------- 1 | import { beforeEach, describe, expect, it } from 'vitest'; 2 | import { getEventBodyAsBuffer } from '../../../src'; 3 | import { SQSAdapter } from '../../../src/adapters/aws'; 4 | import { createCanHandleTestsForAdapter } from '../utils/can-handle'; 5 | import { createSQSEvent } from './utils/sqs'; 6 | 7 | describe(SQSAdapter.name, () => { 8 | let adapter!: SQSAdapter; 9 | 10 | beforeEach(() => { 11 | adapter = new SQSAdapter(); 12 | }); 13 | 14 | describe('getAdapterName', () => { 15 | it('should be the same name of the class', () => { 16 | expect(adapter.getAdapterName()).toBe(SQSAdapter.name); 17 | }); 18 | }); 19 | 20 | createCanHandleTestsForAdapter(() => new SQSAdapter(), undefined); 21 | 22 | describe('getRequest', () => { 23 | it('should return the correct mapping for the request', () => { 24 | const event = createSQSEvent(); 25 | 26 | const result = adapter.getRequest(event); 27 | 28 | expect(result.method).toBe('POST'); 29 | expect(result.path).toBe('/sqs'); 30 | expect(result.headers).toHaveProperty('host', 'sqs.amazonaws.com'); 31 | expect(result.headers).toHaveProperty('content-type', 'application/json'); 32 | 33 | const [bodyBuffer, contentLength] = getEventBodyAsBuffer( 34 | JSON.stringify(event), 35 | false, 36 | ); 37 | 38 | expect(result.body).toBeInstanceOf(Buffer); 39 | expect(result.body).toStrictEqual(bodyBuffer); 40 | 41 | expect(result.headers).toHaveProperty( 42 | 'content-length', 43 | String(contentLength), 44 | ); 45 | }); 46 | 47 | it('should return the correct mapping for the request with custom path and method', () => { 48 | const event = createSQSEvent(); 49 | 50 | const method = 'PUT'; 51 | const path = '/custom/sqs'; 52 | 53 | const customAdapter = new SQSAdapter({ 54 | sqsForwardMethod: method, 55 | sqsForwardPath: path, 56 | }); 57 | 58 | const result = customAdapter.getRequest(event); 59 | 60 | expect(result.method).toBe(method); 61 | expect(result.path).toBe(path); 62 | expect(result.headers).toHaveProperty('host', 'sqs.amazonaws.com'); 63 | expect(result.headers).toHaveProperty('content-type', 'application/json'); 64 | 65 | const [bodyBuffer, contentLength] = getEventBodyAsBuffer( 66 | JSON.stringify(event), 67 | false, 68 | ); 69 | 70 | expect(result.body).toBeInstanceOf(Buffer); 71 | expect(result.body).toStrictEqual(bodyBuffer); 72 | 73 | expect(result.headers).toHaveProperty( 74 | 'content-length', 75 | String(contentLength), 76 | ); 77 | }); 78 | }); 79 | }); 80 | -------------------------------------------------------------------------------- /src/resolvers/aws-context/aws-context.resolver.ts: -------------------------------------------------------------------------------- 1 | //#region Imports 2 | 3 | import type { Context } from 'aws-lambda'; 4 | import type { 5 | DelegatedResolver, 6 | Resolver, 7 | ResolverContract, 8 | ResolverProps, 9 | } from '../../contracts'; 10 | 11 | //#endregion 12 | 13 | /** 14 | * The class that implements the resolver by using the AWS Context object. 15 | * 16 | * @remarks To use this resolver, you MUST leave `{@link https://docs.aws.amazon.com/lambda/latest/dg/nodejs-context.html | callbackWaitsForEmptyEventLoop}` as true, otherwise, AWS will not wait for this resolver to resolve. 17 | * 18 | * @deprecated From the AWS Documentation, describing the functions used in this resolver: Functions for compatibility with earlier Node.js Runtime v0.10.42. No longer documented, so they are deprecated, but they still work as of the 12.x runtime, so they are not removed from the types. 19 | * 20 | * @breadcrumb Resolvers / AwsContextResolver 21 | * @public 22 | */ 23 | export class AwsContextResolver 24 | implements ResolverContract 25 | { 26 | /** 27 | * {@inheritDoc} 28 | */ 29 | public createResolver({ 30 | context, 31 | event, 32 | log, 33 | respondWithErrors, 34 | adapter, 35 | }: ResolverProps): Resolver { 36 | if (!context) { 37 | throw new Error( 38 | 'Could not figure out how to create the resolver because the "context" argument was not sent.', 39 | ); 40 | } 41 | 42 | if (!context.succeed) { 43 | throw new Error( 44 | 'Could not figure out how to create the resolver because the "context" argument didn\'t have the "succeed" function.', 45 | ); 46 | } 47 | 48 | if (!context.fail) { 49 | throw new Error( 50 | 'Could not figure out how to create the resolver because the "context" argument didn\'t have the "fail" function.', 51 | ); 52 | } 53 | 54 | const delegatedResolver: DelegatedResolver = { 55 | succeed: response => context.succeed(response), 56 | fail: error => context.fail(error), 57 | }; 58 | 59 | return { 60 | run: task => { 61 | task() 62 | .then(response => delegatedResolver.succeed(response)) 63 | .catch(error => { 64 | log.error( 65 | 'SERVERLESS_ADAPTER:RESPOND_TO_EVENT_SOURCE_WITH_ERROR', 66 | error, 67 | ); 68 | 69 | adapter.onErrorWhileForwarding({ 70 | delegatedResolver, 71 | error, 72 | log, 73 | event, 74 | respondWithErrors, 75 | }); 76 | }); 77 | }, 78 | }; 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /benchmark/src/samples/format-headers.ts: -------------------------------------------------------------------------------- 1 | import benchmark from 'benchmark'; 2 | import { BothValueHeaders } from '../../../src'; 3 | import { createApiGatewayV1 } from '../events'; 4 | 5 | function getFlattenedHeadersMap( 6 | headersMap: BothValueHeaders, 7 | separator: string = ',', 8 | lowerCaseKey: boolean = false, 9 | ): Record { 10 | const commaDelimitedHeaders: Record = {}; 11 | const headersMapEntries = Object.entries(headersMap); 12 | 13 | for (const [headerKey, headerValue] of headersMapEntries) { 14 | const newKey = lowerCaseKey ? headerKey.toLowerCase() : headerKey; 15 | 16 | if (Array.isArray(headerValue)) 17 | commaDelimitedHeaders[newKey] = headerValue.join(separator); 18 | else commaDelimitedHeaders[newKey] = String(headerValue ?? ''); 19 | } 20 | 21 | return commaDelimitedHeaders; 22 | } 23 | 24 | function getFlattenedHeadersV2( 25 | headersMap: BothValueHeaders, 26 | separator: string = ',', 27 | lowerCaseKey: boolean = false, 28 | ): Record { 29 | const commaDelimitedHeaders: Record = {}; 30 | 31 | for (const [headerKey, headerValue] of Object.entries(headersMap)) { 32 | const newKey = lowerCaseKey ? headerKey.toLowerCase() : headerKey; 33 | 34 | if (Array.isArray(headerValue)) 35 | commaDelimitedHeaders[newKey] = headerValue.join(separator); 36 | else commaDelimitedHeaders[newKey] = (headerValue ?? '') + ''; 37 | } 38 | 39 | return commaDelimitedHeaders; 40 | } 41 | 42 | function getFlattenedHeadersV3( 43 | headersMap: BothValueHeaders, 44 | separator: string = ',', 45 | lowerCaseKey: boolean = false, 46 | ): Record { 47 | return Object.keys(headersMap).reduce((acc, headerKey) => { 48 | const newKey = lowerCaseKey ? headerKey.toLowerCase() : headerKey; 49 | const headerValue = headersMap[headerKey]; 50 | 51 | if (Array.isArray(headerValue)) acc[newKey] = headerValue.join(separator); 52 | else acc[newKey] = (headerValue ?? '') + ''; 53 | 54 | return acc; 55 | }, {}); 56 | } 57 | 58 | const eventV1ApiGateway = createApiGatewayV1('GET', '/test'); 59 | 60 | const suite = new benchmark.Suite(); 61 | 62 | suite.add('getFlattenedHeadersMap', () => 63 | getFlattenedHeadersMap(eventV1ApiGateway.headers), 64 | ); 65 | suite.add('getFlattenedHeadersV2', () => 66 | getFlattenedHeadersV2(eventV1ApiGateway.headers), 67 | ); 68 | suite.add('getFlattenedHeadersV3', () => 69 | getFlattenedHeadersV3(eventV1ApiGateway.headers), 70 | ); 71 | 72 | suite 73 | .on('cycle', function (event) { 74 | console.log(String(event.target)); 75 | }) 76 | .on('complete', function () { 77 | console.log('Fastest is ' + this.filter('fastest').map('name')); 78 | }) 79 | .run({ 80 | async: false, 81 | }); 82 | -------------------------------------------------------------------------------- /www/docs/main/handlers/firebase.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Firebase 3 | description: See more about how to integrate with Firebase Functions. 4 | --- 5 | 6 | :::tip 7 | 8 | Don't know what a handler is? See the [Architecture](../architecture#handler) section. 9 | 10 | ::: 11 | 12 | ## Requirements 13 | 14 | First, install the types for this adapter: 15 | 16 | ```bash 17 | npm i --save firebase-functions firebase-admin 18 | ``` 19 | 20 | ## V1 and V2 21 | 22 | If you want to use Firebase Functions V1, use [HttpFirebaseHandler](../../api/Handlers/HttpFirebaseHandler). 23 | 24 | But if you want to add support for V2, then use [HttpFirebaseV2Handler](../../api/Handlers/HttpFirebaseHandler/HttpFirebaseV2Handler). 25 | 26 | ## Integrating with Http Events 27 | 28 | To use, you can import [HttpFirebaseV2Handler](../../api/Handlers/HttpFirebaseHandler/HttpFirebaseV2Handler) and call the method [setHandler](../../api/ServerlessAdapter#method-sethandler), as per the code below: 29 | 30 | ```ts title="index.ts" 31 | import { ServerlessAdapter } from '@h4ad/serverless-adapter'; 32 | import { DummyAdapter } from '@h4ad/serverless-adapter/adapters/dummy'; 33 | import { HttpFirebaseV2Handler } from '@h4ad/serverless-adapter/handlers/firebase'; 34 | import { DummyResolver } from '@h4ad/serverless-adapter/resolvers/dummy'; 35 | import app from './app'; 36 | 37 | export const helloWorld = ServerlessAdapter.new(app) 38 | .setHandler(new HttpFirebaseV2Handler({ 39 | // you can pass custom properties here, like: concurrency. 40 | })) 41 | .setResolver(new DummyResolver()) 42 | // choose the framework of your app 43 | // .setFramework(new ExpressFramework()) 44 | .addAdapter(new DummyAdapter()) 45 | .build(); 46 | 47 | // you can export more than one if you want 48 | export const test = ServerlessAdapter.new(app) 49 | .setHandler(new HttpFirebaseV2Handler()) 50 | .setResolver(new DummyResolver()) 51 | // choose the framework of your app 52 | // .setFramework(new ExpressFramework()) 53 | .addAdapter(new DummyAdapter()) 54 | .build(); 55 | ``` 56 | 57 | :::info About Resolver and Adapter 58 | 59 | You should use `DummyResolver` and `DummyAdapter` because it's a requirement for the library, but `HttpFirebaseV2Handler` doesn't do anything with them, 60 | so you use those dummy versions just to satisfy the library requirements. 61 | 62 | ::: 63 | 64 | ## What about Cron, Pub/Sub, etc? 65 | 66 | I could not think yet in an API to handle those cases well, 67 | so currently I didn't add support to these type of Firebase Functions. 68 | 69 | If you have some idea about a design for those APIs, [create an issue](https://github.com/H4ad/serverless-adapter/issues/new/choose). 70 | 71 | ## Examples 72 | 73 | You can see examples of how to use [here](https://github.com/H4ad/serverless-adapter-examples). 74 | 75 | -------------------------------------------------------------------------------- /test/adapters/aws/dynamodb.adapter.spec.ts: -------------------------------------------------------------------------------- 1 | import { beforeEach, describe, expect, it } from 'vitest'; 2 | import { getEventBodyAsBuffer } from '../../../src'; 3 | import { DynamoDBAdapter } from '../../../src/adapters/aws'; 4 | import { createCanHandleTestsForAdapter } from '../utils/can-handle'; 5 | import { createDynamoDBEvent } from './utils/dynamodb'; 6 | 7 | describe(DynamoDBAdapter.name, () => { 8 | let adapter!: DynamoDBAdapter; 9 | 10 | beforeEach(() => { 11 | adapter = new DynamoDBAdapter(); 12 | }); 13 | 14 | describe('getAdapterName', () => { 15 | it('should be the same name of the class', () => { 16 | expect(adapter.getAdapterName()).toBe(DynamoDBAdapter.name); 17 | }); 18 | }); 19 | 20 | createCanHandleTestsForAdapter(() => new DynamoDBAdapter(), undefined); 21 | 22 | describe('getRequest', () => { 23 | it('should return the correct mapping for the request', () => { 24 | const event = createDynamoDBEvent(); 25 | 26 | const result = adapter.getRequest(event); 27 | 28 | expect(result.method).toBe('POST'); 29 | expect(result.path).toBe('/dynamo'); 30 | expect(result.headers).toHaveProperty('host', 'dynamodb.amazonaws.com'); 31 | expect(result.headers).toHaveProperty('content-type', 'application/json'); 32 | 33 | const [bodyBuffer, contentLength] = getEventBodyAsBuffer( 34 | JSON.stringify(event), 35 | false, 36 | ); 37 | 38 | expect(result.body).toBeInstanceOf(Buffer); 39 | expect(result.body).toStrictEqual(bodyBuffer); 40 | 41 | expect(result.headers).toHaveProperty( 42 | 'content-length', 43 | String(contentLength), 44 | ); 45 | }); 46 | 47 | it('should return the correct mapping for the request with custom path and method', () => { 48 | const event = createDynamoDBEvent(); 49 | 50 | const method = 'PUT'; 51 | const path = '/custom/dynamo'; 52 | 53 | const customAdapter = new DynamoDBAdapter({ 54 | dynamoDBForwardMethod: method, 55 | dynamoDBForwardPath: path, 56 | }); 57 | 58 | const result = customAdapter.getRequest(event); 59 | 60 | expect(result.method).toBe(method); 61 | expect(result.path).toBe(path); 62 | expect(result.headers).toHaveProperty('host', 'dynamodb.amazonaws.com'); 63 | expect(result.headers).toHaveProperty('content-type', 'application/json'); 64 | 65 | const [bodyBuffer, contentLength] = getEventBodyAsBuffer( 66 | JSON.stringify(event), 67 | false, 68 | ); 69 | 70 | expect(result.body).toBeInstanceOf(Buffer); 71 | expect(result.body).toStrictEqual(bodyBuffer); 72 | 73 | expect(result.headers).toHaveProperty( 74 | 'content-length', 75 | String(contentLength), 76 | ); 77 | }); 78 | }); 79 | }); 80 | --------------------------------------------------------------------------------