Repository: H4ad/serverless-adapter Branch: main Commit: 3abab65593ba Files: 317 Total size: 888.5 KB Directory structure: gitextract_42shtyvg/ ├── .eslintignore ├── .eslintrc ├── .gitattributes ├── .github/ │ ├── ISSUE_TEMPLATE/ │ │ ├── bug_report.md │ │ ├── config.yml │ │ └── feature_request.md │ ├── PULL_REQUEST_TEMPLATE.md │ ├── dependabot.yml │ ├── settings.yml │ └── workflows/ │ ├── codeql-analysis.yml │ ├── docs.yml │ ├── pr.yml │ └── release.yml ├── .gitignore ├── .husky/ │ ├── .gitignore │ ├── pre-commit │ └── prepare-commit-msg ├── .npmrc ├── .prettierrc ├── .release-please-manifest.json ├── .tmuxinator.yml ├── .tool-versions ├── .vscode/ │ ├── launch.json │ └── settings.json ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── api-extractor.json ├── benchmark/ │ ├── .gitignore │ ├── .swcrc │ ├── README.md │ ├── package.json │ ├── src/ │ │ ├── events.ts │ │ ├── framework.mock.ts │ │ └── samples/ │ │ ├── clone-headers.ts │ │ ├── compare-libraries.ts │ │ └── format-headers.ts │ └── tsconfig.json ├── package.json ├── release-please-config.json ├── scripts/ │ ├── generate-api-pages.ts │ ├── generate-markdown.ts │ ├── libs/ │ │ ├── CustomMarkdownDocumenter.ts │ │ ├── CustomUtilities.ts │ │ └── MarkdownEmitter.ts │ ├── models/ │ │ └── apidoc.types.ts │ └── parse-docs.ts ├── src/ │ ├── @types/ │ │ ├── binary-settings.ts │ │ ├── digital-ocean/ │ │ │ ├── digital-ocean-http-event.ts │ │ │ ├── digital-ocean-http-response.ts │ │ │ └── index.ts │ │ ├── headers.ts │ │ ├── helpers.ts │ │ ├── huawei/ │ │ │ ├── huawei-api-gateway-event.ts │ │ │ ├── huawei-api-gateway-response.ts │ │ │ ├── huawei-context.ts │ │ │ └── index.ts │ │ └── index.ts │ ├── adapters/ │ │ ├── apollo-server/ │ │ │ ├── apollo-server-mutation.adapter.ts │ │ │ └── index.ts │ │ ├── aws/ │ │ │ ├── alb.adapter.ts │ │ │ ├── api-gateway-v1.adapter.ts │ │ │ ├── api-gateway-v2.adapter.ts │ │ │ ├── base/ │ │ │ │ ├── aws-simple-adapter.ts │ │ │ │ └── index.ts │ │ │ ├── dynamodb.adapter.ts │ │ │ ├── event-bridge.adapter.ts │ │ │ ├── index.ts │ │ │ ├── lambda-edge.adapter.ts │ │ │ ├── request-lambda-edge.adapter.ts │ │ │ ├── s3.adapter.ts │ │ │ ├── sns.adapter.ts │ │ │ └── sqs.adapter.ts │ │ ├── azure/ │ │ │ ├── http-trigger-v4.adapter.ts │ │ │ └── index.ts │ │ ├── digital-ocean/ │ │ │ ├── http-function.adapter.ts │ │ │ └── index.ts │ │ ├── dummy/ │ │ │ ├── dummy.adapter.ts │ │ │ └── index.ts │ │ └── huawei/ │ │ ├── huawei-api-gateway.adapter.ts │ │ └── index.ts │ ├── contracts/ │ │ ├── adapter.contract.ts │ │ ├── framework.contract.ts │ │ ├── handler.contract.ts │ │ ├── index.ts │ │ └── resolver.contract.ts │ ├── core/ │ │ ├── base-handler.ts │ │ ├── constants.ts │ │ ├── current-invoke.ts │ │ ├── event-body.ts │ │ ├── headers.ts │ │ ├── index.ts │ │ ├── is-binary.ts │ │ ├── logger.ts │ │ ├── no-op.ts │ │ ├── optional.ts │ │ ├── path.ts │ │ └── stream.ts │ ├── frameworks/ │ │ ├── apollo-server/ │ │ │ ├── apollo-server.framework.ts │ │ │ └── index.ts │ │ ├── body-parser/ │ │ │ ├── base-body-parser.framework.ts │ │ │ ├── index.ts │ │ │ ├── json-body-parser.framework.ts │ │ │ ├── raw-body-parser.framework.ts │ │ │ ├── text-body-parser.framework.ts │ │ │ └── urlencoded-body-parser.framework.ts │ │ ├── cors/ │ │ │ ├── cors.framework.ts │ │ │ └── index.ts │ │ ├── deepkit/ │ │ │ ├── http-deepkit.framework.ts │ │ │ └── index.ts │ │ ├── express/ │ │ │ ├── express.framework.ts │ │ │ └── index.ts │ │ ├── fastify/ │ │ │ ├── fastify.framework.ts │ │ │ ├── helpers/ │ │ │ │ └── no-op-content-parser.ts │ │ │ └── index.ts │ │ ├── hapi/ │ │ │ ├── hapi.framework.ts │ │ │ └── index.ts │ │ ├── koa/ │ │ │ ├── index.ts │ │ │ └── koa.framework.ts │ │ ├── lazy/ │ │ │ ├── index.ts │ │ │ └── lazy.framework.ts │ │ ├── polka/ │ │ │ ├── index.ts │ │ │ └── polka.framework.ts │ │ └── trpc/ │ │ ├── index.ts │ │ └── trpc.framework.ts │ ├── handlers/ │ │ ├── aws/ │ │ │ ├── aws-stream.handler.ts │ │ │ └── index.ts │ │ ├── azure/ │ │ │ ├── azure.handler.ts │ │ │ └── index.ts │ │ ├── base/ │ │ │ ├── index.ts │ │ │ └── raw-request.ts │ │ ├── default/ │ │ │ ├── default.handler.ts │ │ │ └── index.ts │ │ ├── digital-ocean/ │ │ │ ├── digital-ocean.handler.ts │ │ │ └── index.ts │ │ ├── firebase/ │ │ │ ├── http-firebase-v2.handler.ts │ │ │ ├── http-firebase.handler.ts │ │ │ └── index.ts │ │ ├── gcp/ │ │ │ ├── gcp.handler.ts │ │ │ └── index.ts │ │ └── huawei/ │ │ ├── http-huawei.handler.ts │ │ └── index.ts │ ├── index.doc.ts │ ├── index.ts │ ├── network/ │ │ ├── index.ts │ │ ├── request.ts │ │ ├── response-stream.ts │ │ ├── response.ts │ │ └── utils.ts │ ├── resolvers/ │ │ ├── aws-context/ │ │ │ ├── aws-context.resolver.ts │ │ │ └── index.ts │ │ ├── callback/ │ │ │ ├── callback.resolver.ts │ │ │ └── index.ts │ │ ├── dummy/ │ │ │ ├── dummy.resolver.ts │ │ │ └── index.ts │ │ └── promise/ │ │ ├── index.ts │ │ └── promise.resolver.ts │ └── serverless-adapter.ts ├── test/ │ ├── adapters/ │ │ ├── apollo-server/ │ │ │ └── apollo-mutation.adapter.spec.ts │ │ ├── aws/ │ │ │ ├── alb.adapter.spec.ts │ │ │ ├── api-gateway-v1.adapter.spec.ts │ │ │ ├── api-gateway-v2.adapter.spec.ts │ │ │ ├── aws-simple-adapter.spec.ts │ │ │ ├── dynamodb.adapter.spec.ts │ │ │ ├── event-bridge.adapter.spec.ts │ │ │ ├── lambda-edge.adapter.spec.ts │ │ │ ├── request-lambda-edge.adapter.spec.ts │ │ │ ├── s3.adapter.spec.ts │ │ │ ├── sns.adapter.spec.ts │ │ │ ├── sqs.adapter.spec.ts │ │ │ └── utils/ │ │ │ ├── alb-event.ts │ │ │ ├── api-gateway-v1.ts │ │ │ ├── api-gateway-v2.ts │ │ │ ├── dynamodb.ts │ │ │ ├── event-bridge.ts │ │ │ ├── events.ts │ │ │ ├── lambda-edge.ts │ │ │ ├── s3.ts │ │ │ ├── sns.ts │ │ │ └── sqs.ts │ │ ├── azure/ │ │ │ ├── http-trigger.adapter.spec.ts │ │ │ └── utils/ │ │ │ ├── events.ts │ │ │ └── http-trigger.ts │ │ ├── digital-ocean/ │ │ │ ├── http-function.adapter.spec.ts │ │ │ └── utils/ │ │ │ ├── event.ts │ │ │ └── http-function.ts │ │ ├── dummy/ │ │ │ └── dummy.adapter.spec.ts │ │ ├── huawei/ │ │ │ ├── huawei-api-gateway.adapter.spec.ts │ │ │ └── utils/ │ │ │ ├── events.ts │ │ │ └── huawei-api-gateway.ts │ │ ├── test.example │ │ └── utils/ │ │ ├── can-handle.ts │ │ └── events.ts │ ├── core/ │ │ ├── base-handler.spec.ts │ │ ├── current-invoke.spec.ts │ │ ├── event-body.spec.ts │ │ ├── headers.spec.ts │ │ ├── is-binary.spec.ts │ │ ├── logger.spec.ts │ │ ├── no-op.spec.ts │ │ ├── optional.spec.ts │ │ ├── path.spec.ts │ │ ├── stream.spec.ts │ │ └── utils/ │ │ └── stream.ts │ ├── frameworks/ │ │ ├── apollo-server.framework.spec.ts │ │ ├── body-parser-v2.framework.spec.ts │ │ ├── body-parser.framework.helper.ts │ │ ├── body-parser.framework.spec.ts │ │ ├── cors.framework.spec.ts │ │ ├── express-v5.framework.spec.ts │ │ ├── express.framework.spec.ts │ │ ├── fastify-v5.framework.spec.ts │ │ ├── fastify.framework.spec.ts │ │ ├── hapi.framework.spec.ts │ │ ├── http-deepkit.framework.spec.ts │ │ ├── koa.framework.spec.ts │ │ ├── lazy.framework.spec.ts │ │ ├── polka.framework.spec.ts │ │ ├── trpc.framework.spec.ts │ │ └── utils.ts │ ├── handlers/ │ │ ├── aws-stream.handler.spec.ts │ │ ├── azure.handler.spec.ts │ │ ├── default.handler.spec.ts │ │ ├── digital-ocean.handler.spec.ts │ │ ├── gcp.handler.spec.ts │ │ ├── http-firebase-v2.handler.spec.ts │ │ ├── http-firebase-v2.sdk-v5.handler.spec.ts │ │ ├── http-firebase-v2.sdk-v6.handler.spec.ts │ │ ├── http-firebase.handler.spec.ts │ │ └── huawei.handler.spec.ts │ ├── issues/ │ │ ├── alb-express-static/ │ │ │ ├── alb-express-static.spec.ts │ │ │ └── robots.txt │ │ └── issue-165/ │ │ └── transfer-encoding-chunked-support.spec.ts │ ├── mocks/ │ │ └── framework.mock.ts │ ├── network/ │ │ ├── request.spec.ts │ │ └── response.spec.ts │ ├── resolvers/ │ │ ├── aws-context.resolver.spec.ts │ │ ├── callback.resolver.spec.ts │ │ ├── dummy.resolver.spec.ts │ │ └── promise.resolver.spec.ts │ └── serverless-adapter.spec.ts ├── tsconfig.build.json ├── tsconfig.doc.json ├── tsconfig.eslint.json ├── tsconfig.json ├── tsdoc.json ├── tsup.config.ts ├── vite.config.ts └── www/ ├── .gitignore ├── .tool-versions ├── README.md ├── babel.config.js ├── blog/ │ ├── 2022-06-17-the-beginning.mdx │ ├── 2022-07-17-updates-and-releases.mdx │ ├── 2023-04-28-aws-lambda-response-streaming.mdx │ ├── 2023-12-25-dual-package-publish.mdx │ └── authors.yml ├── docs/ │ ├── .gitignore │ └── main/ │ ├── adapters/ │ │ ├── aws/ │ │ │ ├── alb.mdx │ │ │ ├── api-gateway-v1.mdx │ │ │ ├── api-gateway-v2.mdx │ │ │ ├── dynamodb.mdx │ │ │ ├── event-bridge.mdx │ │ │ ├── function-url.mdx │ │ │ ├── lambda-edge.mdx │ │ │ ├── s3.mdx │ │ │ ├── sns.mdx │ │ │ └── sqs.mdx │ │ ├── azure/ │ │ │ └── http-trigger-v4.mdx │ │ ├── digital-ocean/ │ │ │ └── http-function.mdx │ │ ├── firebase.mdx │ │ └── huawei/ │ │ └── huawei-api-gateway.mdx │ ├── advanced/ │ │ └── adapters/ │ │ ├── creating-an-adapter.mdx │ │ └── introduction.mdx │ ├── architecture.mdx │ ├── frameworks/ │ │ ├── apollo-server.mdx │ │ ├── deepkit.mdx │ │ ├── express.mdx │ │ ├── fastify.mdx │ │ ├── hapi.mdx │ │ ├── helpers/ │ │ │ ├── body-parser.mdx │ │ │ ├── cors.mdx │ │ │ └── lazy.mdx │ │ ├── koa.mdx │ │ ├── nestjs.mdx │ │ ├── polka.mdx │ │ └── trpc.mdx │ ├── getting-started/ │ │ ├── customizing.mdx │ │ ├── examples.mdx │ │ ├── installation.mdx │ │ └── usage.mdx │ ├── handlers/ │ │ ├── aws.mdx │ │ ├── azure.mdx │ │ ├── digital-ocean.mdx │ │ ├── firebase.mdx │ │ ├── gcp.mdx │ │ └── huawei.mdx │ ├── intro.mdx │ └── resolvers/ │ ├── aws-context.mdx │ ├── callback.mdx │ └── promise.mdx ├── docusaurus.config.js ├── package.json ├── sidebars.js ├── src/ │ ├── components/ │ │ ├── BrowserWindow/ │ │ │ ├── index.tsx │ │ │ └── styles.module.css │ │ ├── HomepageFeatures/ │ │ │ ├── index.tsx │ │ │ └── styles.module.css │ │ └── HowToStart/ │ │ ├── index.tsx │ │ └── styles.module.css │ ├── css/ │ │ └── custom.css │ └── pages/ │ ├── index.module.css │ └── index.tsx ├── static/ │ ├── .nojekyll │ └── CNAME └── tsconfig.json ================================================ FILE CONTENTS ================================================ ================================================ FILE: .eslintignore ================================================ src/types/global.d.ts benchmark/ ================================================ FILE: .eslintrc ================================================ { "root": true, "parser": "@typescript-eslint/parser", "plugins": [ "@typescript-eslint", "eslint-plugin-tsdoc" ], "parserOptions": { "project": [ "./tsconfig.eslint.json" ] }, "extends": [ "eslint:recommended", "plugin:node/recommended", "plugin:import/recommended", "plugin:import/typescript", "plugin:@typescript-eslint/eslint-recommended", "plugin:@typescript-eslint/recommended", "plugin:@typescript-eslint/recommended-requiring-type-checking", "plugin:prettier/recommended" ], "rules": { "tsdoc/syntax": "error", "prettier/prettier": "warn", "comma-dangle": [ "error", "always-multiline" ], "node/no-missing-import": "off", "node/no-empty-function": "off", "node/no-unsupported-features/es-syntax": "off", "node/no-missing-require": "off", "node/shebang": "off", "import/order": [ "error", { "newlines-between": "never" } ], "sort-imports": [ "error", { "ignoreDeclarationSort": true } ], "@typescript-eslint/no-use-before-define": "off", "quotes": [ "warn", "single", { "avoidEscape": true } ], "curly": [ "error", "multi-or-nest" ], "max-len": [ "warn", { "code": 140, "tabWidth": 2, "ignoreStrings": true, "ignoreTemplateLiterals": true, "ignoreUrls": true, "ignoreComments": true, "ignorePattern": "^import\\s.+\\sfrom\\s.+;$" } ], "no-case-declarations": "warn", "no-control-regex": "off", "node/no-unpublished-import": "off", "@typescript-eslint/no-namespace": "off", "@typescript-eslint/no-unsafe-argument": "off", "@typescript-eslint/no-unsafe-assignment": "off", "@typescript-eslint/no-unsafe-member-access": "off", "@typescript-eslint/no-unsafe-call": "off", "@typescript-eslint/no-unsafe-return": "off", "@typescript-eslint/no-var-requires": "off", "@typescript-eslint/ban-ts-comment": "off", "@typescript-eslint/no-explicit-any": "off", "@typescript-eslint/no-non-null-assertion": "off", "@typescript-eslint/restrict-template-expressions": "off", "@typescript-eslint/no-floating-promises": "off", "@typescript-eslint/explicit-module-boundary-types": "off", "@typescript-eslint/no-inferrable-types": "off", "@typescript-eslint/no-unnecessary-type-assertion": "off", "@typescript-eslint/no-redundant-type-constituents": "off", "@typescript-eslint/no-unused-vars": [ "error", { "varsIgnorePattern": "^_", "argsIgnorePattern": "^_" } ], "@typescript-eslint/member-ordering": [ "error", { "default": [ // Constructors "public-constructor", "protected-constructor", "private-constructor", // Index signature "signature", // Fields "protected-abstract-field", "public-abstract-field", "protected-static-field", "public-static-field", "private-static-field", "protected-decorated-field", "public-decorated-field", "private-decorated-field", "protected-instance-field", "public-instance-field", "private-instance-field", // Getters "protected-decorated-get", "public-decorated-get", "private-decorated-get", "protected-static-get", "public-static-get", "private-static-get", "protected-instance-get", "public-instance-get", "private-instance-get", "protected-abstract-get", "public-abstract-get", "decorated-get", "abstract-get", "protected-get", "public-get", "private-get", "static-get", "instance-get", "get", // Setters "protected-abstract-set", "public-abstract-set", "abstract-set", "protected-decorated-set", "public-decorated-set", "private-decorated-set", "protected-static-set", "public-static-set", "private-static-set", "protected-instance-set", "public-instance-set", "private-instance-set", "protected-set", "public-set", "private-set", "decorated-set", "static-set", "instance-set", "set", // Methods "public-static-method", "protected-static-method", "private-static-method", "public-decorated-method", "protected-decorated-method", "private-decorated-method", "public-abstract-method", "protected-abstract-method", "public-instance-method", "protected-instance-method", "private-instance-method" ] } ] } } ================================================ FILE: .gitattributes ================================================ # Set the repository to show as TypeScript rather than JS in GitHub *.js linguist-detectable=false ================================================ FILE: .github/ISSUE_TEMPLATE/bug_report.md ================================================ --- name: "🐛 Bug Report" about: Report a reproducible bug or regression. title: '' labels: bug assignees: '' --- ## Current Behavior ## Expected Behavior ## Steps to Reproduce the Problem 1. 1. 1. ## Environment - Version: - Platform: - Node.js Version: ================================================ FILE: .github/ISSUE_TEMPLATE/config.yml ================================================ # This file is automatically added by @npmcli/template-oss. Do not edit. blank_issues_enabled: true ================================================ FILE: .github/ISSUE_TEMPLATE/feature_request.md ================================================ --- name: 🌈 Feature request about: Suggest an amazing new idea for this project title: '' labels: enhancement assignees: '' --- ## Feature Request **Is your feature request related to a problem? Please describe.** **Describe the solution you'd like** **Describe alternatives you've considered** ## Are you willing to resolve this issue by submitting a Pull Request? - [ ] Yes, I have the time, and I know how to start. - [ ] Yes, I have the time, but I don't know how to start. I would need guidance. - [ ] No, I don't have the time, although I believe I could do it if I had the time... - [ ] No, I don't have the time and I wouldn't even know how to start. ================================================ FILE: .github/PULL_REQUEST_TEMPLATE.md ================================================ ### Description of change ### Pull-Request Checklist - [ ] Code is up-to-date with the `main` branch - [ ] `npm run lint` passes with this change - [ ] `npm run test` passes with this change - [ ] This pull request links relevant issues as `Fixes #0000` - [ ] There are new or updated unit tests validating the change - [ ] Added documentation inside `www/docs/main` folder. - [ ] Included new files inside `index.doc.ts`. - [ ] The new commits follow conventions outlined in the [conventional commit spec](https://www.conventionalcommits.org/en/v1.0.0/) ================================================ FILE: .github/dependabot.yml ================================================ # This file is automatically added by @npmcli/template-oss. Do not edit. version: 2 updates: - package-ecosystem: npm directory: / schedule: interval: daily allow: - dependency-type: direct versioning-strategy: increase-if-necessary commit-message: prefix: deps prefix-development: chore labels: - "Dependencies" ================================================ FILE: .github/settings.yml ================================================ # This file is automatically added by @npmcli/template-oss. Do not edit. repository: allow_merge_commit: false allow_rebase_merge: true allow_squash_merge: true squash_merge_commit_title: PR_TITLE squash_merge_commit_message: PR_BODY delete_branch_on_merge: true enable_automated_security_fixes: true enable_vulnerability_alerts: true branches: - name: main protection: required_status_checks: null enforce_admins: true required_pull_request_reviews: require_last_push_approval: true dismiss_stale_reviews: true ================================================ FILE: .github/workflows/codeql-analysis.yml ================================================ name: CodeQL on: push: paths: - 'src/**' - 'package-lock.json' - 'package.json' - 'tsconfig.json' - 'tsconfig.*.json' - 'vite.config.ts' branches: - main pull_request: paths: - 'src/**' - 'package-lock.json' - 'package.json' - 'tsconfig.json' - 'tsconfig.*.json' - 'vite.config.ts' branches: - main schedule: # "At 10:00 UTC (03:00 PT) on Monday" https://crontab.guru/#0_10_*_*_1 - cron: "0 10 * * 1" jobs: analyze: name: Analyze runs-on: ubuntu-latest timeout-minutes: 120 permissions: actions: read contents: read security-events: write strategy: fail-fast: false matrix: language: [ 'javascript-typescript' ] steps: - name: Checkout uses: actions/checkout@v4 - name: Setup Git User run: | git config --global user.email "h4ad+bot@viniciusl.com.br" git config --global user.name "H4ad CLI robot" - name: Initialize CodeQL uses: github/codeql-action/init@v3 with: languages: ${{ matrix.language }} - name: Autobuild uses: github/codeql-action/autobuild@v3 - name: Perform CodeQL Analysis uses: github/codeql-action/analyze@v3 with: category: "/language:${{matrix.language}}" ================================================ FILE: .github/workflows/docs.yml ================================================ name: Deploy to GitHub Pages on: push: branches: - main paths: - '.github/workflows/docs.yml' - 'www/**' - 'src/**' - 'scripts/**' workflow_dispatch: jobs: deploy: runs-on: ubuntu-latest permissions: contents: write concurrency: group: ${{ github.workflow }}-${{ github.ref }} steps: - uses: actions/checkout@v4 - name: Setup Node uses: actions/setup-node@v4 with: node-version: '18.x' - name: Cache dependencies uses: actions/cache@v3 with: path: ~/.npm key: ${{ runner.os }}-node-18-${{ hashFiles('**/package-lock.json') }} restore-keys: | ${{ runner.os }}-node-18- - name: Install Lib Dependencies run: npm ci - name: Install Docs Dependencies run: npm ci working-directory: www - name: Parse TSDoc Documentation run: npm run docs:generate - name: Build Docs run: npm run build working-directory: www - name: Deploy uses: peaceiris/actions-gh-pages@v3 if: ${{ github.ref == 'refs/heads/main' }} with: github_token: ${{ secrets.GITHUB_TOKEN }} publish_dir: ./www/build ================================================ FILE: .github/workflows/pr.yml ================================================ name: Pull Request on: workflow_dispatch: pull_request: paths: - 'src/**' - 'package-lock.json' - 'package.json' - 'tsconfig.json' - 'tsconfig.*.json' - 'vite.config.ts' jobs: build: runs-on: ubuntu-latest strategy: matrix: node-version: [18.x, 20.x] steps: - uses: actions/checkout@v4 - name: Use Node.js ${{ matrix.node-version }} uses: actions/setup-node@v4 with: node-version: ${{ matrix.node-version }} - name: Update NPM Version run: npm i -g npm - name: Cache dependencies uses: actions/cache@v3 with: path: ~/.npm key: ${{ runner.os }}-node-${{ matrix.node-version }}-${{ hashFiles('**/package-lock.json') }} restore-keys: | ${{ runner.os }}-node-${{ matrix.node-version }}- - name: Install Lib Dependencies run: npm ci - name: Build run: npm run build - name: Run tests run: npm test - name: Get Coverage Info uses: codecov/codecov-action@v4 with: fail_ci_if_error: true env: CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} docs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Use Node.js 18 uses: actions/setup-node@v4 with: node-version: '18.x' - name: Cache dependencies uses: actions/cache@v3 with: path: ~/.npm key: ${{ runner.os }}-node-18.x-${{ hashFiles('**/package-lock.json') }} restore-keys: | ${{ runner.os }}-node-18.x- - name: Install Lib Dependencies run: npm ci - name: Install Docs Dependencies run: npm ci working-directory: www - name: Parse TSDoc Documentation run: npm run docs:generate - name: Build Docs run: npm run build working-directory: www ================================================ FILE: .github/workflows/release.yml ================================================ name: Release on: push: branches: - main permissions: contents: write pull-requests: write id-token: write jobs: release-please: runs-on: ubuntu-latest steps: - name: Checkout Code uses: actions/checkout@v4 - name: Release Please uses: google-github-actions/release-please-action@v4 id: release - name: Use Node 20.x uses: actions/setup-node@v4 if: ${{ steps.release.outputs.release_created }} with: node-version: 20 registry-url: 'https://registry.npmjs.org' - name: Install Deps run: npm ci if: ${{ steps.release.outputs.release_created }} - name: Build run: npm run build if: ${{ steps.release.outputs.release_created }} - name: Test run: npm run test if: ${{ steps.release.outputs.release_created }} - name: Get Coverage Info if: ${{ steps.release.outputs.release_created }} uses: codecov/codecov-action@v4 env: CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} - name: NPM Publish run: npm publish --provenance --access public if: ${{ steps.release.outputs.release_created }} env: NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} ================================================ FILE: .gitignore ================================================ # Logs logs *.log npm-debug.log* yarn-debug.log* yarn-error.log* lerna-debug.log* var # Diagnostic reports (https://nodejs.org/api/report.html) report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json # Runtime data pids *.pid *.seed *.pid.lock # Directory for instrumented libs generated by jscoverage/JSCover lib-cov # Coverage directory used by tools like istanbul coverage *.lcov # nyc test coverage .nyc_output # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) .grunt # Bower dependency directory (https://bower.io/) bower_components # node-waf configuration .lock-wscript # Compiled binary addons (https://nodejs.org/api/addons.html) build/Release # Dependency directories node_modules/ jspm_packages/ # Snowpack dependency directory (https://snowpack.dev/) web_modules/ # TypeScript cache *.tsbuildinfo # Optional npm cache directory .npm # Optional eslint cache .eslintcache # Microbundle cache .rpt2_cache/ .rts2_cache_cjs/ .rts2_cache_es/ .rts2_cache_umd/ # Optional REPL history .node_repl_history # Output of 'npm pack' *.tgz # Yarn Integrity file .yarn-integrity # dotenv environment variables file .env.test .env.local # parcel-bundler cache (https://parceljs.org/) .cache .parcel-cache # Next.js build output .next out # Nuxt.js build / generate output .nuxt dist # Gatsby files .cache/ # Comment in the public line in if your project uses Gatsby and not Next.js # https://nextjs.org/blog/next-9-1#public-directory-support # public # vuepress build output .vuepress/dist # Serverless directories .serverless/ # FuseBox cache .fusebox/ # DynamoDB Local files .dynamodb/ # TernJS port file .tern-port # Stores VSCode versions used for testing VSCode extensions .vscode-test # yarn v2 .yarn/cache .yarn/unplugged .yarn/build-state.yml .yarn/install-state.gz .pnp.* # Compiled code lib/ # IDEs .idea # tsdoc temp /etc !etc/.gitkeep # docs www/api/* !www/api/.gitkeep .env perf/ ================================================ FILE: .husky/.gitignore ================================================ _ ================================================ FILE: .husky/pre-commit ================================================ #!/bin/sh . "$(dirname "$0")/_/husky.sh" npm run build npm run test npm run lint ================================================ FILE: .husky/prepare-commit-msg ================================================ #!/bin/sh . "$(dirname "$0")/_/husky.sh" exec /**", "node_modules/**"] }, { "name": "Debug Jest Tests", "type": "node", "request": "launch", "runtimeArgs": [ "--inspect-brk", "${workspaceRoot}/node_modules/.bin/jest", "--runInBand" ], "envFile": "${workspaceFolder}/.env", "console": "integratedTerminal", "internalConsoleOptions": "neverOpen", "port": 9229, "disableOptimisticBPs": true, "windows": { "program": "${workspaceFolder}/node_modules/jest/bin/jest" } }, { "name": "Debug Jest Current File", "type": "node", "request": "launch", "program": "${workspaceFolder}/node_modules/.bin/jest", "args": ["${relativeFile}", "--config", "jest.config.js"], "console": "integratedTerminal", "internalConsoleOptions": "neverOpen", "port": 9229, "disableOptimisticBPs": true, "windows": { "program": "${workspaceFolder}/node_modules/jest/bin/jest" } } ] } ================================================ FILE: .vscode/settings.json ================================================ { "editor.formatOnSave": true, "[typescript]": { "editor.defaultFormatter": "esbenp.prettier-vscode" } } ================================================ FILE: CHANGELOG.md ================================================ # Changelog ## [4.4.0](https://github.com/H4ad/serverless-adapter/compare/v4.3.2...v4.4.0) (2024-12-01) ### Features * **express-v5:** added support to express v5 and body-parser v2 ([e435b97](https://github.com/H4ad/serverless-adapter/commit/e435b97e09783de62801374a1fb365c553ed4833)) * **fastify-v5:** ensure support to fastify v5 ([bd09e46](https://github.com/H4ad/serverless-adapter/commit/bd09e4685a78f12851ab0f99fab19dae26c52d8d)) * **firebase:** ensure support to v5 and v6 sdk ([bc6886e](https://github.com/H4ad/serverless-adapter/commit/bc6886eadab75c7f18e3e9c2bcc886ec3b7f714c)) ### Miscellaneous Chores * bump cross-spawn and @swc/cli in /benchmark ([0e4db68](https://github.com/H4ad/serverless-adapter/commit/0e4db6820ab0836f1e2dfbe60dc692288f0c7157)) * bump cross-spawn from 7.0.3 to 7.0.6 in /www ([ce1d0cd](https://github.com/H4ad/serverless-adapter/commit/ce1d0cdb27133402ed624fb3fba96caed95fa905)) * bump micromatch from 4.0.5 to 4.0.8 in /benchmark ([1ddc2a9](https://github.com/H4ad/serverless-adapter/commit/1ddc2a990f868e63d6f8c29b65e4f7a0056736f4)) * bump webpack from 5.89.0 to 5.96.1 in /www ([47392b0](https://github.com/H4ad/serverless-adapter/commit/47392b0bbd973ad06a30e364e334444144559944)) * **http-deepkit:** ensure is working with version 1.0.1-alpha-155 ([b1ee6d4](https://github.com/H4ad/serverless-adapter/commit/b1ee6d4beb3219b7d63350502073530c637c826b)) * **packages:** bump lib package versions ([15bc6ad](https://github.com/H4ad/serverless-adapter/commit/15bc6ad7baae905971c04c88337c563fe65e913c)) ## [4.3.2](https://github.com/H4ad/serverless-adapter/compare/v4.3.1...v4.3.2) (2024-11-09) ### Continuous Integration * **coverage:** do not fail when fail coverage & fix issue with test ([ff9702b](https://github.com/H4ad/serverless-adapter/commit/ff9702b38da650b94935481be54618a0fd765e6b)) ## [4.3.1](https://github.com/H4ad/serverless-adapter/compare/v4.3.0...v4.3.1) (2024-11-09) ### Miscellaneous Chores * bump cookie and express in /www ([22d850f](https://github.com/H4ad/serverless-adapter/commit/22d850fab96240c110ee6fbc75473c8bbefd11c2)) * bump serve-static and express in /benchmark ([249af0c](https://github.com/H4ad/serverless-adapter/commit/249af0c9d3dfa1f5fda23f8a49f720f3c96c4f07)) * **codecov:** fix issues with upload coverage ([4b5b39e](https://github.com/H4ad/serverless-adapter/commit/4b5b39e7c54d5b37913a9d7866a626515b8c06b6)) * **reflect-metadata:** allow more general versions for reflect metadata ([816cc51](https://github.com/H4ad/serverless-adapter/commit/816cc51afee8024907eb399ed9fe625f828c98ad)) ## [4.3.0](https://github.com/H4ad/serverless-adapter/compare/v4.2.3...v4.3.0) (2024-09-18) ### Features * **aws-stream-handler:** add flag to customize callbackWaitsForEmptyEventLoop ([#264](https://github.com/H4ad/serverless-adapter/issues/264)) ([30a59f9](https://github.com/H4ad/serverless-adapter/commit/30a59f99e83cd16f5a7c37bc4296f7501f59fd36)) ## [4.2.3](https://github.com/H4ad/serverless-adapter/compare/v4.2.2...v4.2.3) (2024-09-09) ### Bug Fixes * **response-stream:** improve chunk identification (fixes [#260](https://github.com/H4ad/serverless-adapter/issues/260)) ([2aa474e](https://github.com/H4ad/serverless-adapter/commit/2aa474e02c533d31b5086866a78afdedd3058f04)) ### Documentation * **response-stream:** add comments and references explaining implementation ([d39db53](https://github.com/H4ad/serverless-adapter/commit/d39db532a2ca9ebd7754dd364f88cf8bf0ed0a18)) ### Tests * **response-stream:** test eagerly flushed headers ([0f33c29](https://github.com/H4ad/serverless-adapter/commit/0f33c29e2bfc99194c61887bb908763625e357b7)) ## [4.2.2](https://github.com/H4ad/serverless-adapter/compare/v4.2.1...v4.2.2) (2024-09-06) ### Bug Fixes * **apig-v1-adapter:** lowercase request headers ([4fbb588](https://github.com/H4ad/serverless-adapter/commit/4fbb5880283ba0fad54374baeb572ca706b804e6)) ### Documentation * fix Apollo Server package name in npm command ([4d4cece](https://github.com/H4ad/serverless-adapter/commit/4d4ceced9d2f882f7431830134d293c187aff4f3)) * **getting-started:** update npm install command ([ee4661f](https://github.com/H4ad/serverless-adapter/commit/ee4661fc3a7a3d518d4ff3f4ff033ce5a01066ba)) ### Miscellaneous Chores * bump express from 4.18.2 to 4.19.2 in /benchmark ([98e84d1](https://github.com/H4ad/serverless-adapter/commit/98e84d1a6cb7d05bab6506bf2225573c8bd6e50d)) ## [4.2.1](https://github.com/H4ad/serverless-adapter/compare/v4.2.0...v4.2.1) (2024-02-29) ### Bug Fixes * **response-stream:** fix response with no content doesn't correctly end the writable stream ([bded8cf](https://github.com/H4ad/serverless-adapter/commit/bded8cfe32a853529fea7334658709b12b2971e1)) ### Code Refactoring * **apollo-server-mutation:** better types for adapter ([79f3383](https://github.com/H4ad/serverless-adapter/commit/79f33833c4368282d421495b6f7a70a700bd06bb)) * **response-stream:** avoid creating object on log while parsing headers ([1effcae](https://github.com/H4ad/serverless-adapter/commit/1effcaebf23e83188d4836693428f1953d0403be)) ### Tests * **all:** cleaning tests and fixing ts issues ([c3dcfff](https://github.com/H4ad/serverless-adapter/commit/c3dcfff58ac3bc29294337abd074c567724a8198)) * **aws-stream:** add tests to cover [#206](https://github.com/H4ad/serverless-adapter/issues/206) ([c853149](https://github.com/H4ad/serverless-adapter/commit/c853149a6295f015b467825f20a60790cb346f65)) ## [4.2.0](https://github.com/H4ad/serverless-adapter/compare/v4.1.0...v4.2.0) (2024-01-08) ### Features * **frameworks:** added support for polka ([39377cb](https://github.com/H4ad/serverless-adapter/commit/39377cb16b20bdba7b724663b8076a6a394851a6)) ### Miscellaneous Chores * bump @apollo/server from 4.9.5 to 4.10.0 ([cf4e1d9](https://github.com/H4ad/serverless-adapter/commit/cf4e1d9485fe174c68ae1b70dc2c6d6e7a220c02)) * bump @rushstack/node-core-library from 3.62.0 to 3.63.0 ([19c88e0](https://github.com/H4ad/serverless-adapter/commit/19c88e01c74a8e4735ba66f6b5b77b2cbd579897)) * bump @vitest/coverage-v8 from 1.1.0 to 1.1.3 ([3e67b23](https://github.com/H4ad/serverless-adapter/commit/3e67b23dba80b00c65667c312578f07d39111919)) * bump eslint-plugin-prettier from 5.1.1 to 5.1.2 ([83fc5e5](https://github.com/H4ad/serverless-adapter/commit/83fc5e5cac5a08701815a6c73563a51866fdcbf9)) * bump fastify from 4.25.1 to 4.25.2 ([e048b11](https://github.com/H4ad/serverless-adapter/commit/e048b117034c2f4e7f3b25467dd24fa3b754e684)) * bump follow-redirects from 1.15.3 to 1.15.4 in /www ([af12bbd](https://github.com/H4ad/serverless-adapter/commit/af12bbd55d264dc2be668e24cb1f551e333f33ba)) * bump koa from 2.14.2 to 2.15.0 ([164c97b](https://github.com/H4ad/serverless-adapter/commit/164c97ba10e6d0ee4661bc980d279262f4a982c1)) * bump vite from 5.0.10 to 5.0.11 ([0478492](https://github.com/H4ad/serverless-adapter/commit/04784922a49429a57f9bbbd4773e42c069ce2cca)) ## [4.1.0](https://github.com/H4ad/serverless-adapter/compare/v4.0.1...v4.1.0) (2024-01-03) ### Features * **network:** support buffering transfer-encoding: chunked ([f19ffd1](https://github.com/H4ad/serverless-adapter/commit/f19ffd1f6b2da4cccbd2be6e48429c566719ade6)) ## [4.0.1](https://github.com/H4ad/serverless-adapter/compare/v4.0.0...v4.0.1) (2023-12-26) ### Bug Fixes * **ci:** missing build part while releasing new version ([5b7d184](https://github.com/H4ad/serverless-adapter/commit/5b7d18410acdc0aa547de2db63cf6347a5715b58)) ### Documentation * **blog:** added note about bug related to missing package files ([1d75d91](https://github.com/H4ad/serverless-adapter/commit/1d75d91fe8863c45ae2a7abe44aec6d51d96e44d)) ## [4.0.0](https://github.com/H4ad/serverless-adapter/compare/v3.2.0...v4.0.0) (2023-12-26) ### ⚠ BREAKING CHANGES * Now we support dual package publish, and the import can fail. ### Features * added support for dual package publish ([dd0803f](https://github.com/H4ad/serverless-adapter/commit/dd0803ff5ebcabf22120da88b74a720c3661f846)) ### Bug Fixes * **dual-package-publish:** issue with imports lib when moduleResolution is node ([4dac8aa](https://github.com/H4ad/serverless-adapter/commit/4dac8aa07ef015f3b0fd8f8d766705271e93c111)) ### Documentation * **blog:** added blogpost about dual package publish ([006e8a9](https://github.com/H4ad/serverless-adapter/commit/006e8a94b02152e4857cda7951e285ff2b449430)) * updated documentation for dual package publish ([03ee217](https://github.com/H4ad/serverless-adapter/commit/03ee21746bee785d840ab26a1ec5ddf2bd6dea90)) ### Continuous Integration * **release:** fixed issue with release-please skipping release ([8dfb582](https://github.com/H4ad/serverless-adapter/commit/8dfb582742481f7e37e076f00c51d32907f401fd)) ## [3.2.0](https://github.com/H4ad/serverless-adapter/compare/v3.1.0...v3.2.0) (2023-12-22) ### Features * **firebase:** bump supported firebase functions to 4.x ([b717240](https://github.com/H4ad/serverless-adapter/commit/b717240a808d7d81905745347b17969e7caaf6f5)) ### Documentation * **readme:** removed semantic release badge ([fe85304](https://github.com/H4ad/serverless-adapter/commit/fe8530439df4ed48d3542127227ae98954fd84a5)) ### Miscellaneous Chores * **benchmark:** bump package versions ([b6aa539](https://github.com/H4ad/serverless-adapter/commit/b6aa539bb499fcadd2393c7bf010dfe6d726f2d5)) * bootstrap releases for path: . ([e68506e](https://github.com/H4ad/serverless-adapter/commit/e68506ea9c5a5fb8492b7cc7bb03400c95700668)) * bump @apollo/server from 4.7.4 to 4.9.3 ([52c8b83](https://github.com/H4ad/serverless-adapter/commit/52c8b83db4d8b80120aea6ccb32e8b4580466168)) * bump semver from 5.7.1 to 5.7.2 in /benchmark ([0a6a3e0](https://github.com/H4ad/serverless-adapter/commit/0a6a3e0a0e536f43e2c61f307f955b01a97e1169)) * bump semver from 5.7.1 to 5.7.2 in /www ([49c7baf](https://github.com/H4ad/serverless-adapter/commit/49c7baf364e251d78da4349ab35c6b69837a003d)) * bump vite from 4.3.5 to 4.4.9 ([ecd1252](https://github.com/H4ad/serverless-adapter/commit/ecd125253229ed032f21606238fbc27fc74d5e95)) * bump vite from 4.4.9 to 5.0.10 ([8eadf40](https://github.com/H4ad/serverless-adapter/commit/8eadf405eea86facff1268b9fb4d5d153a873fbb)) * bump word-wrap from 1.2.3 to 1.2.4 ([218d3a9](https://github.com/H4ad/serverless-adapter/commit/218d3a906c0b18156110c4c8fe155d0f183fca29)) * **docs:** update to docusaurus v3 ([51a104e](https://github.com/H4ad/serverless-adapter/commit/51a104e000e867ae3601a70408cec8d0ab2d8cc3)) * **package:** bump package versions ([fe0a0fc](https://github.com/H4ad/serverless-adapter/commit/fe0a0fc35c687037dfa172dbb667c4451d539ad8)) * **release-please:** set latest version ([69110ec](https://github.com/H4ad/serverless-adapter/commit/69110ec1f418831ac4a49545d1bf40c291212293)) * **semantic-release:** removed unused package ([2c60275](https://github.com/H4ad/serverless-adapter/commit/2c602753ecd3fcdff23567ec8a77e317ebd7f9fe)) ### Continuous Integration * **codeql:** run only when changing code files ([93d8f1c](https://github.com/H4ad/serverless-adapter/commit/93d8f1c029e2c84e5c4b1366ecddc9b1b11c6fa5)) * **codeql:** updated configuration ([9ffa3e8](https://github.com/H4ad/serverless-adapter/commit/9ffa3e8b5f4c7df8772cd64ef8640646879f713f)) * **docs:** only trigger when update workflows of docs ([c1e7f8a](https://github.com/H4ad/serverless-adapter/commit/c1e7f8aefdaf18a12f5a26c2b0cbc94f4c830322)) * **pr:** only run when update specific files ([0085520](https://github.com/H4ad/serverless-adapter/commit/0085520b20edb0a33111bdb7780195805d31b0af)) * **pr:** stop running the pr on main ([7c7a05a](https://github.com/H4ad/serverless-adapter/commit/7c7a05a78928a2ef96d67dae38f6f56b25361575)) * **release-please:** try fix issues with release please config ([46577f2](https://github.com/H4ad/serverless-adapter/commit/46577f2c79bcc9f20b9925f6fa629f534d63a4f9)) * **release:** added coverage ([57f1e09](https://github.com/H4ad/serverless-adapter/commit/57f1e09d63936546764708880e5dd5e799c332b6)) * **release:** added provenance during publish ([1161e42](https://github.com/H4ad/serverless-adapter/commit/1161e4227fb63ad272ba740ba186de63d40955c3)) * **release:** include all commits on release ([9185a0b](https://github.com/H4ad/serverless-adapter/commit/9185a0b6ab34174905669cfdd084b2cc9afe54bb)) * **release:** moved configuration to the correct place ([b8c6156](https://github.com/H4ad/serverless-adapter/commit/b8c6156eb3d7df06f1c370965e91abc850217adc)) * **release:** use release manager instead of merge-and-release ([ef278e6](https://github.com/H4ad/serverless-adapter/commit/ef278e6efc2732e5e21d2e3ae9d32fb96ac1edc2)) * **workflows:** bump action versions ([647e694](https://github.com/H4ad/serverless-adapter/commit/647e694ce6919925c5df4188450e49faa5ec3fc8)) CHANGES: ## [3.1.0](https://github.com/H4ad/serverless-adapter/compare/v3.0.0...v3.1.0) (2023-07-01) ### Bug Fixes * **build:** disable minify identifiers ([0a285a6](https://github.com/H4ad/serverless-adapter/commit/0a285a6b2249d56ce39a762872bcc7cfb4515f8c)) * **package:** types not being emitted ([2bc1244](https://github.com/H4ad/serverless-adapter/commit/2bc124456f41855798ed7d5c4a132b2d83bf16fd)) ### Features * **aws:** added adapter for request lambda edge ([b8791da](https://github.com/H4ad/serverless-adapter/commit/b8791da9c4718a837d9ae01d89bba7b30067dc52)) ## [3.0.0](https://github.com/H4ad/serverless-adapter/compare/v2.17.0...v3.0.0) (2023-06-09) ### Bug Fixes * **api-gateway-v1:** probably missing query string value when multiple ([78b9f18](https://github.com/H4ad/serverless-adapter/commit/78b9f18dfb8a459f0c1557fdf702f68a078c098b)) * perf(api-gateway-v2)!: single pass when collecting headers and cookies on response ([3d65895](https://github.com/H4ad/serverless-adapter/commit/3d65895f174db00e2e45b3626223874a2d71f40a)) * refactor(core)!: removed support for regex on binary validation and case-sensitive headers ([4fb3a39](https://github.com/H4ad/serverless-adapter/commit/4fb3a39f0434d29d66b018f451698954ecbf3ed4)) * chore(nodejs)!: deprecate node 12.x, 14.x and 16.x ([4c734d4](https://github.com/H4ad/serverless-adapter/commit/4c734d4fed3bb9384b514ccf28d41f34c5360b76)) ### Features * **trpc:** bump support for 10.x ([5d3124a](https://github.com/H4ad/serverless-adapter/commit/5d3124a115dc44099fc681eec9592636374f85b8)) ### Performance Improvements * **api-gateway-v1:** faster getRequest ([70f7020](https://github.com/H4ad/serverless-adapter/commit/70f7020e347e57417920b32eb68d4456df7db246)) * **api-gateway-v2:** faster getRequest method ([3b08708](https://github.com/H4ad/serverless-adapter/commit/3b087087af1873414c85c8db09b5867c0752ab56)) * **aws:** optimized strip base path ([f72967a](https://github.com/H4ad/serverless-adapter/commit/f72967afaa1cdc4dbc95cc2298d560dc21b27884)) * **default-handler:** always log using fn ([36950b3](https://github.com/H4ad/serverless-adapter/commit/36950b36e246a43dc45d2d9ef2d989402eef916b)) * **headers:** use object.keys + reduce instead of entries ([41339c6](https://github.com/H4ad/serverless-adapter/commit/41339c681f52b05328097a8b4cbb9cf27e704a84)) * **logger:** faster logger ([103817c](https://github.com/H4ad/serverless-adapter/commit/103817c7a284dabedb78807d6db7fbf2ed42ed75)) * **optional:** use strict equal instead of typeof ([1fba12c](https://github.com/H4ad/serverless-adapter/commit/1fba12c6e22089376437a8976971ad30df1283e1)) * **tsconfig:** do not use define because is slower ([35ce7c7](https://github.com/H4ad/serverless-adapter/commit/35ce7c738b69a39b7179f1d8cae40924967ad0cd)) ### Tests * **vitest:** replaced jest for vitest ([7505fad](https://github.com/H4ad/serverless-adapter/commit/7505fad2b3078aabbc72c105033043c597842933)) ### BREAKING CHANGES * now we don't flatten the headers * now regex is not support anymore due the slow performance and we don't lower case all the headers, so the content-encoding and content-type must be lowercase * **vitest:** removed support for fastify 3.0.0 & hapi 20.x & firebase-admin < 11 * Now we will no longer support old nodejs versions ================================================ FILE: CONTRIBUTING.md ================================================ # Contributing When contributing to this repository, please first discuss the change you wish to make via issue, email, or any other method with the owners of this repository before making a change. Please note we have a code of conduct, please follow it in all your interactions with the project. ## Pull Request Process 1. Install the dependencies with `npm ci` and then go into the `www` folder and install the dependencies again. 2. After creating your resource or fixing a bug, verify that the tests are correct with `npm run test`. 3. Be sure to update the documentation if you add a new feature, the docs are inside `www/docs/main`, to see your changes, run `npm run start`. 4. Once we've reviewed and ensured that all of these things are correct, we'll merge your Pull-Request. ## Code of Conduct ### Our Pledge In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. ### Our Standards Examples of behavior that contributes to creating a positive environment include: * Using welcoming and inclusive language * Being respectful of differing viewpoints and experiences * Gracefully accepting constructive criticism * Focusing on what is best for the community * Showing empathy towards other community members Examples of unacceptable behavior by participants include: * The use of sexualized language or imagery and unwelcome sexual attention or advances * Trolling, insulting/derogatory comments, and personal or political attacks * Public or private harassment * Publishing others' private information, such as a physical or electronic address, without explicit permission * Other conduct which could reasonably be considered inappropriate in a professional setting ### Our Responsibilities Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. ### Scope This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. ### Enforcement Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at [@vinii_joga10](https://twitter.com/vinii_joga10). All complaints will be reviewed and investigated and will result in a response that is deemed necessary and appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. ### Attribution This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] [homepage]: http://contributor-covenant.org [version]: http://contributor-covenant.org/version/1/4/ ================================================ FILE: LICENSE ================================================ MIT License Copyright (c) 2022 Vinícius Lourenço Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================

🚀 Serverless Adapter

Install   |    Usage   |    Support   |    Examples   |    Benchmark   |    Architecture   |    Credits

[![npm package][npm-img]][npm-url] [![Build Status][build-img]][build-url] [![Downloads][downloads-img]][downloads-url] [![Issues][issues-img]][issues-url] [![Code Coverage][codecov-img]][codecov-url] [![Commitizen Friendly][commitizen-img]][commitizen-url] Run REST APIs and other web applications using your existing Node.js application framework (NestJS, Deepkit, Express (v4 and v5), Koa, Hapi, Fastify, tRPC and Apollo Server), on top of AWS Lambda, Azure, Digital Ocean and many other clouds. This library was a refactored version of [@vendia/serverless-express](https://github.com/vendia/serverless-express), I create a new way to interact and extend event sources by creating contracts to abstract the integrations between each library layer. Why you would use this libray instead of [@vendia/serverless-express](https://github.com/vendia/serverless-express)? - Better APIs to extend library functionality. - You don't need me to release a new version to integrate with the new event source, you can create an adapter and just call the `addAdapter` method when building your handler. - All code can be extended, if you want to modify the current behavior you can. - This is important because if you find a bug, you can quickly resolve it by extending the class, _and then you can submit a PR to fix the bug_. - All code was written in Typescript. - Well documented, any method, class, or interface has comments to explain the behavior. - We have >99% coverage. # Installing To be able to use, first install the library: ```bash npm i --save @h4ad/serverless-adapter ``` # Usage To start to use, first you need to know what you need to import, let's start showing the [ServerlessAdapter](/docs/api/ServerlessAdapter). ```tsx import { ServerlessAdapter } from '@h4ad/serverless-adapter'; ``` We need to pass to [Serverless Adapter](/docs/api/ServerlessAdapter) the instance of your api, let's look an example with: - Framework: [Express](../frameworks/express). - Adapters: [AWS Api Gateway V2 Adapter](../adapters/aws/api-gateway-v2). - Handler: [Default Handler](../handlers/default). - Resolver: [Promise Resolver](../resolvers/promise). ```ts import { ServerlessAdapter } from '@h4ad/serverless-adapter'; import { ExpressFramework } from '@h4ad/serverless-adapter/lib/frameworks/express'; import { DefaultHandler } from '@h4ad/serverless-adapter/lib/handlers/default'; import { PromiseResolver } from '@h4ad/serverless-adapter/lib/resolvers/promise'; import { ApiGatewayV2Adapter } from '@h4ad/serverless-adapter/lib/adapters/aws'; const express = require('express'); const app = express(); export const handler = ServerlessAdapter.new(app) .setFramework(new ExpressFramework()) .setHandler(new DefaultHandler()) .setResolver(new PromiseResolver()) .addAdapter(new ApiGatewayV2Adapter()) // if you need more adapters // just append more `addAdapter` calls .build(); ``` # Documentation See how to use this library [here](https://viniciusl.com.br/serverless-adapter/docs/category/getting-started). # Breaking Changes I will not consider updating/breaking compatibility of a NodeJS framework as a breaking change, because I had a lot of supported frameworks and if I created a major version for each one it would be a mess. So if you want predictability, pin the version with `~` instead of `^`. # Examples You can see some examples of how to use this library [here](https://github.com/H4ad/serverless-adapter-examples). # Benchmark See the speed comparison between other libraries that have the same purpose in the [Benchmark Section](./benchmark). # Credits Honestly, I just refactored all the code that the @vendia team and many other contributors wrote, thanks so much to them for existing and giving us a brilliant library that is the core of my current company. # Sponsors | | |------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| [build-img]:https://github.com/H4ad/serverless-adapter/actions/workflows/release.yml/badge.svg [build-url]:https://github.com/H4ad/serverless-adapter/actions/workflows/release.yml [downloads-img]:https://img.shields.io/npm/dt/serverless-adapter [downloads-url]:https://www.npmtrends.com/@h4ad/serverless-adapter [npm-img]:https://img.shields.io/npm/v/@h4ad/serverless-adapter [npm-url]:https://www.npmjs.com/package/@h4ad/serverless-adapter [issues-img]:https://img.shields.io/github/issues/H4ad/serverless-adapter [issues-url]:https://github.com/H4ad/serverless-adapter/issues [codecov-img]:https://codecov.io/gh/H4ad/serverless-adapter/branch/main/graph/badge.svg [codecov-url]:https://codecov.io/gh/H4ad/serverless-adapter [commitizen-img]:https://img.shields.io/badge/commitizen-friendly-brightgreen.svg [commitizen-url]:http://commitizen.github.io/cz-cli/ ================================================ FILE: api-extractor.json ================================================ /** * Config file for API Extractor. For more info, please visit: https://api-extractor.com */ { "$schema": "https://developer.microsoft.com/json-schemas/api-extractor/v7/api-extractor.schema.json", /** * Optionally specifies another JSON config file that this file extends from. This provides a way for * standard settings to be shared across multiple projects. * * If the path starts with "./" or "../", the path is resolved relative to the folder of the file that contains * the "extends" field. Otherwise, the first path segment is interpreted as an NPM package name, and will be * resolved using NodeJS require(). * * SUPPORTED TOKENS: none * DEFAULT VALUE: "" */ // "extends": "./shared/api-extractor-base.json" // "extends": "my-package/include/api-extractor-base.json" /** * Determines the "" token that can be used with other config file settings. The project folder * typically contains the tsconfig.json and package.json config files, but the path is user-defined. * * The path is resolved relative to the folder of the config file that contains the setting. * * The default value for "projectFolder" is the token "", which means the folder is determined by traversing * parent folders, starting from the folder containing api-extractor.json, and stopping at the first folder * that contains a tsconfig.json file. If a tsconfig.json file cannot be found in this way, then an error * will be reported. * * SUPPORTED TOKENS: * DEFAULT VALUE: "" */ // "projectFolder": "..", /** * (REQUIRED) Specifies the .d.ts file to be used as the starting point for analysis. API Extractor * analyzes the symbols exported by this module. * * The file extension must be ".d.ts" and not ".ts". * * The path is resolved relative to the folder of the config file that contains the setting; to change this, * prepend a folder token such as "". * * SUPPORTED TOKENS: , , */ "mainEntryPointFilePath": "/lib/index.doc.d.ts", /** * A list of NPM package names whose exports should be treated as part of this package. * * For example, suppose that Webpack is used to generate a distributed bundle for the project "library1", * and another NPM package "library2" is embedded in this bundle. Some types from library2 may become part * of the exported API for library1, but by default API Extractor would generate a .d.ts rollup that explicitly * imports library2. To avoid this, we can specify: * * "bundledPackages": [ "library2" ], * * This would direct API Extractor to embed those types directly in the .d.ts rollup, as if they had been * local files for library1. */ "bundledPackages": [], /** * Determines how the TypeScript compiler engine will be invoked by API Extractor. */ "compiler": { /** * Specifies the path to the tsconfig.json file to be used by API Extractor when analyzing the project. * * The path is resolved relative to the folder of the config file that contains the setting; to change this, * prepend a folder token such as "". * * Note: This setting will be ignored if "overrideTsconfig" is used. * * SUPPORTED TOKENS: , , * DEFAULT VALUE: "/tsconfig.json" */ // "tsconfigFilePath": "/tsconfig.json", /** * Provides a compiler configuration that will be used instead of reading the tsconfig.json file from disk. * The object must conform to the TypeScript tsconfig schema: * * http://json.schemastore.org/tsconfig * * If omitted, then the tsconfig.json file will be read from the "projectFolder". * * DEFAULT VALUE: no overrideTsconfig section */ // "overrideTsconfig": { // . . . // } /** * This option causes the compiler to be invoked with the --skipLibCheck option. This option is not recommended * and may cause API Extractor to produce incomplete or incorrect declarations, but it may be required when * dependencies contain declarations that are incompatible with the TypeScript engine that API Extractor uses * for its analysis. Where possible, the underlying issue should be fixed rather than relying on skipLibCheck. * * DEFAULT VALUE: false */ // "skipLibCheck": true, }, /** * Configures how the API report file (*.api.md) will be generated. */ "apiReport": { /** * (REQUIRED) Whether to generate an API report. */ "enabled": true /** * The filename for the API report files. It will be combined with "reportFolder" or "reportTempFolder" to produce * a full file path. * * The file extension should be ".api.md", and the string should not contain a path separator such as "\" or "/". * * SUPPORTED TOKENS: , * DEFAULT VALUE: ".api.md" */ // "reportFileName": ".api.md", /** * Specifies the folder where the API report file is written. The file name portion is determined by * the "reportFileName" setting. * * The API report file is normally tracked by Git. Changes to it can be used to trigger a branch policy, * e.g. for an API review. * * The path is resolved relative to the folder of the config file that contains the setting; to change this, * prepend a folder token such as "". * * SUPPORTED TOKENS: , , * DEFAULT VALUE: "/etc/" */ // "reportFolder": "/etc/", /** * Specifies the folder where the temporary report file is written. The file name portion is determined by * the "reportFileName" setting. * * After the temporary file is written to disk, it is compared with the file in the "reportFolder". * If they are different, a production build will fail. * * The path is resolved relative to the folder of the config file that contains the setting; to change this, * prepend a folder token such as "". * * SUPPORTED TOKENS: , , * DEFAULT VALUE: "/temp/" */ // "reportTempFolder": "/temp/" }, /** * Configures how the doc model file (*.api.json) will be generated. */ "docModel": { /** * (REQUIRED) Whether to generate a doc model file. */ "enabled": true /** * The output path for the doc model file. The file extension should be ".api.json". * * The path is resolved relative to the folder of the config file that contains the setting; to change this, * prepend a folder token such as "". * * SUPPORTED TOKENS: , , * DEFAULT VALUE: "/temp/.api.json" */ // "apiJsonFilePath": "/temp/.api.json" }, /** * Configures how the .d.ts rollup file will be generated. */ "dtsRollup": { /** * (REQUIRED) Whether to generate the .d.ts rollup file. */ "enabled": true /** * Specifies the output path for a .d.ts rollup file to be generated without any trimming. * This file will include all declarations that are exported by the main entry point. * * If the path is an empty string, then this file will not be written. * * The path is resolved relative to the folder of the config file that contains the setting; to change this, * prepend a folder token such as "". * * SUPPORTED TOKENS: , , * DEFAULT VALUE: "/dist/.d.ts" */ // "untrimmedFilePath": "/dist/.d.ts", /** * Specifies the output path for a .d.ts rollup file to be generated with trimming for a "beta" release. * This file will include only declarations that are marked as "@public" or "@beta". * * The path is resolved relative to the folder of the config file that contains the setting; to change this, * prepend a folder token such as "". * * SUPPORTED TOKENS: , , * DEFAULT VALUE: "" */ // "betaTrimmedFilePath": "/dist/-beta.d.ts", /** * Specifies the output path for a .d.ts rollup file to be generated with trimming for a "public" release. * This file will include only declarations that are marked as "@public". * * If the path is an empty string, then this file will not be written. * * The path is resolved relative to the folder of the config file that contains the setting; to change this, * prepend a folder token such as "". * * SUPPORTED TOKENS: , , * DEFAULT VALUE: "" */ // "publicTrimmedFilePath": "/dist/-public.d.ts", /** * When a declaration is trimmed, by default it will be replaced by a code comment such as * "Excluded from this release type: exampleMember". Set "omitTrimmingComments" to true to remove the * declaration completely. * * DEFAULT VALUE: false */ // "omitTrimmingComments": true }, /** * Configures how the tsdoc-metadata.json file will be generated. */ "tsdocMetadata": { /** * Whether to generate the tsdoc-metadata.json file. * * DEFAULT VALUE: true */ // "enabled": true, /** * Specifies where the TSDoc metadata file should be written. * * The path is resolved relative to the folder of the config file that contains the setting; to change this, * prepend a folder token such as "". * * The default value is "", which causes the path to be automatically inferred from the "tsdocMetadata", * "typings" or "main" fields of the project's package.json. If none of these fields are set, the lookup * falls back to "tsdoc-metadata.json" in the package folder. * * SUPPORTED TOKENS: , , * DEFAULT VALUE: "" */ // "tsdocMetadataFilePath": "/dist/tsdoc-metadata.json" }, /** * Specifies what type of newlines API Extractor should use when writing output files. By default, the output files * will be written with Windows-style newlines. To use POSIX-style newlines, specify "lf" instead. * To use the OS's default newline kind, specify "os". * * DEFAULT VALUE: "crlf" */ // "newlineKind": "crlf", /** * Configures how API Extractor reports error and warning messages produced during analysis. * * There are three sources of messages: compiler messages, API Extractor messages, and TSDoc messages. */ "messages": { /** * Configures handling of diagnostic messages reported by the TypeScript compiler engine while analyzing * the input .d.ts files. * * TypeScript message identifiers start with "TS" followed by an integer. For example: "TS2551" * * DEFAULT VALUE: A single "default" entry with logLevel=warning. */ "compilerMessageReporting": { /** * Configures the default routing for messages that don't match an explicit rule in this table. */ "default": { /** * Specifies whether the message should be written to the the tool's output log. Note that * the "addToApiReportFile" property may supersede this option. * * Possible values: "error", "warning", "none" * * Errors cause the build to fail and return a nonzero exit code. Warnings cause a production build fail * and return a nonzero exit code. For a non-production build (e.g. when "api-extractor run" includes * the "--local" option), the warning is displayed but the build will not fail. * * DEFAULT VALUE: "warning" */ "logLevel": "warning" /** * When addToApiReportFile is true: If API Extractor is configured to write an API report file (.api.md), * then the message will be written inside that file; otherwise, the message is instead logged according to * the "logLevel" option. * * DEFAULT VALUE: false */ // "addToApiReportFile": false } // "TS2551": { // "logLevel": "warning", // "addToApiReportFile": true // }, // // . . . }, /** * Configures handling of messages reported by API Extractor during its analysis. * * API Extractor message identifiers start with "ae-". For example: "ae-extra-release-tag" * * DEFAULT VALUE: See api-extractor-defaults.json for the complete table of extractorMessageReporting mappings */ "extractorMessageReporting": { "default": { "logLevel": "warning" // "addToApiReportFile": false } // "ae-extra-release-tag": { // "logLevel": "warning", // "addToApiReportFile": true // }, // // . . . }, /** * Configures handling of messages reported by the TSDoc parser when analyzing code comments. * * TSDoc message identifiers start with "tsdoc-". For example: "tsdoc-link-tag-unescaped-text" * * DEFAULT VALUE: A single "default" entry with logLevel=warning. */ "tsdocMessageReporting": { "default": { "logLevel": "warning" // "addToApiReportFile": false } // "tsdoc-link-tag-unescaped-text": { // "logLevel": "warning", // "addToApiReportFile": true // }, // // . . . } } } ================================================ FILE: benchmark/.gitignore ================================================ node_modules *.cpuprofile .clinic ================================================ FILE: benchmark/.swcrc ================================================ { "env": { "targets": "node >= 18" }, "module": { "type": "commonjs", "strict": true, }, "jsc": { "target": "es2022", "parser": { "syntax": "typescript", "tsx": false, "dynamicImport": true } } } ================================================ FILE: benchmark/README.md ================================================ # Benchmark In this folder, we have benchmarks to compare the speed of this library with others. ## How to Run ```bash git clone git@github.com:H4ad/serverless-adapter.git cd serverless-adapter npm ci npm run build npm pack cd benchmark npm ci npm i ../h4ad-serverless-adapter-0.0.0-development.tgz npm run bench ``` ## Latest Run - CPU: Ryzen 2200g - Memory: 32GB 3200Hz ```md @h4ad/serverless-adapter x 46,463 ops/sec ±10.75% (65 runs sampled) @vendia/serverless-express x 8,726 ops/sec ±18.64% (82 runs sampled) serverless-http x 48,246 ops/sec ±8.00% (70 runs sampled) Fastest is serverless-http,@h4ad/serverless-adapter ``` ================================================ FILE: benchmark/package.json ================================================ { "name": "benchmark", "version": "1.0.0", "description": "", "main": "index.js", "scripts": { "build": "swc src --out-dir dist", "bench": "npm run build && node dist/samples/compare-libraries.js" }, "repository": { "type": "git", "url": "git+https://github.com/H4ad/serverless-adapter.git" }, "license": "MIT", "author": { "name": "Vinícius Lourenço", "email": "H4ad@users.noreply.github.com", "url": "https://github.com/H4ad" }, "dependencies": { "@h4ad/serverless-adapter": "file:../h4ad-serverless-adapter-0.0.0-development.tgz", "@swc/cli": "0.5.1", "@swc/core": "1.3.101", "@vendia/serverless-express": "4.12.6", "benchmark": "2.1.4", "express": "4.21.1", "serverless-http": "3.2.0", "stream-mock": "2.0.5" }, "devDependencies": { "@types/benchmark": "2.1.5", "aws-lambda": "1.0.7", "chokidar": "3.5.3", "ts-node": "10.9.2", "typescript": "5.3.3" } } ================================================ FILE: benchmark/src/events.ts ================================================ import { getMultiValueHeadersMap } from '@h4ad/serverless-adapter'; import type { APIGatewayProxyEvent, APIGatewayProxyEventQueryStringParameters, } from 'aws-lambda/trigger/api-gateway-proxy'; export function createApiGatewayV1( httpMethod: string, path: string, body?: Record, headers?: Record, queryParams?: APIGatewayProxyEventQueryStringParameters, ): APIGatewayProxyEvent { return { resource: '/{proxy+}', path, httpMethod, headers: { Accept: '*/*', 'Accept-Encoding': 'gzip, deflate, br', 'Accept-Language': 'en-US,en;q=0.9', 'cache-control': 'no-cache', 'CloudFront-Forwarded-Proto': 'https', 'CloudFront-Is-Desktop-Viewer': 'true', 'CloudFront-Is-Mobile-Viewer': 'false', 'CloudFront-Is-SmartTV-Viewer': 'false', 'CloudFront-Is-Tablet-Viewer': 'false', 'CloudFront-Viewer-Country': 'US', 'content-type': '', Host: 'xxxxxx.execute-api.us-east-1.amazonaws.com', origin: 'https://xxxxxx.execute-api.us-east-1.amazonaws.com', pragma: 'no-cache', Referer: 'https://xxxxxx.execute-api.us-east-1.amazonaws.com/prod/', 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/74.0.3729.131 Safari/537.36', Via: '2.0 00f0a41f749793b9dd653153037c957e.cloudfront.net (CloudFront)', 'X-Amz-Cf-Id': '2D5N65SYHJdnJfEmAV_hC0Mw3QvkbUXDumJKAL786IGHRdq_MggPtA==', 'X-Amzn-Trace-Id': 'Root=1-5cdf30d0-31a428004abe13807f9445b0', 'X-Forwarded-For': '11.111.111.111, 11.111.111.111', 'X-Forwarded-Port': '443', 'X-Forwarded-Proto': 'https', ...headers, }, multiValueHeaders: { Accept: ['*/*'], 'Accept-Encoding': ['gzip, deflate, br'], 'Accept-Language': ['en-US,en;q=0.9'], 'cache-control': ['no-cache'], 'CloudFront-Forwarded-Proto': ['https'], 'CloudFront-Is-Desktop-Viewer': ['true'], 'CloudFront-Is-Mobile-Viewer': ['false'], 'CloudFront-Is-SmartTV-Viewer': ['false'], 'CloudFront-Is-Tablet-Viewer': ['false'], 'CloudFront-Viewer-Country': ['US'], 'content-type': [], Host: ['xxxxxx.execute-api.us-east-1.amazonaws.com'], origin: ['https://xxxxxx.execute-api.us-east-1.amazonaws.com'], pragma: ['no-cache'], Referer: ['https://xxxxxx.execute-api.us-east-1.amazonaws.com/prod/'], 'User-Agent': [ 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/74.0.3729.131 Safari/537.36', ], Via: ['2.0 00f0a41f749793b9dd653153037c957e.cloudfront.net (CloudFront)'], 'X-Amz-Cf-Id': [ '2D5N65SYHJdnJfEmAV_hC0Mw3QvkbUXDumJKAL786IGHRdq_MggPtA==', ], 'X-Amzn-Trace-Id': ['Root=1-5cdf30d0-31a428004abe13807f9445b0'], 'X-Forwarded-For': ['11.111.111.111, 11.111.111.111'], 'X-Forwarded-Port': ['443'], 'X-Forwarded-Proto': ['https'], ...(headers && getMultiValueHeadersMap(headers)), }, queryStringParameters: queryParams || null, multiValueQueryStringParameters: (queryParams && getMultiValueHeadersMap(queryParams)) || null, pathParameters: { path: path.replace(/^\//, ''), }, stageVariables: {}, requestContext: { authorizer: { claims: null, scopes: null, }, resourceId: 'xxxxx', resourcePath: '/{proxy+}', httpMethod: 'POST', extendedRequestId: 'Z2SQlEORIAMFjpA=', requestTime: '17/May/2019:22:08:16 +0000', path, accountId: 'xxxxxxxx', protocol: 'HTTP/1.1', stage: 'prod', domainPrefix: 'xxxxxx', requestTimeEpoch: 1558130896565, requestId: '4589cf16-78f0-11e9-9c65-816a9b037cec', identity: { apiKey: null, apiKeyId: null, clientCert: null, cognitoIdentityPoolId: null, accountId: null, cognitoIdentityId: null, caller: null, sourceIp: '11.111.111.111', principalOrgId: null, accessKey: null, cognitoAuthenticationType: null, cognitoAuthenticationProvider: null, userArn: null, userAgent: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/74.0.3729.131 Safari/537.36', user: null, }, domainName: 'xxxxxx.execute-api.us-east-1.amazonaws.com', apiId: 'xxxxxx', }, body: (body && JSON.stringify(body)) || null, isBase64Encoded: false, }; } ================================================ FILE: benchmark/src/framework.mock.ts ================================================ import { FrameworkContract } from '@h4ad/serverless-adapter'; import type { IncomingMessage, ServerResponse } from 'http'; import { ObjectReadableMock } from 'stream-mock'; /** * The class that represents a mock for framework that forward the request body to the response. * * @internal */ export class FrameworkMock implements FrameworkContract { //#region Constructor /** * Construtor padrão */ constructor( protected readonly statusCode: number, protected readonly mockedResponseData: object, ) {} //#endregion /** * {@inheritDoc} */ public sendRequest( _: null, __: IncomingMessage, response: ServerResponse, ): void { const writableOutput = new ObjectReadableMock( [Buffer.from(JSON.stringify(this.mockedResponseData))], { objectMode: true, }, ); response.statusCode = this.statusCode; response.setHeader('content-type', 'application/json'); writableOutput.pipe(response); } } ================================================ FILE: benchmark/src/samples/clone-headers.ts ================================================ import benchmark from 'benchmark'; import { createApiGatewayV1 } from '../events'; const eventV1ApiGateway = createApiGatewayV1('GET', '/test'); const randomTest = [ createApiGatewayV1('GET', '/pat2'), createApiGatewayV1('GET', '/pat3'), ]; const headers = createApiGatewayV1('GET', '/pat2').headers; const suite = new benchmark.Suite(); suite.add('{...}', () => { const result = { ...headers }; }); suite.add('structuredClone', () => structuredClone(headers), ); suite.add('JSON.parse + JSON.stringify', () => JSON.parse(JSON.stringify(headers)), ); suite.add('for loop + object.keys', () => { const headers = {}; for (const key of Object.keys([Math.floor(Math.random() * 2)])) headers[key] = headers[key]; }); suite .on('cycle', function (event) { console.log(String(event.target)); }) .on('complete', function () { console.log('Fastest is ' + this.filter('fastest').map('name')); }) .run({ async: false, }); ================================================ FILE: benchmark/src/samples/compare-libraries.ts ================================================ import { ServerlessAdapter } from '@h4ad/serverless-adapter/lib'; import { ApiGatewayV1Adapter } from '@h4ad/serverless-adapter/lib/adapters/aws'; import { DefaultHandler } from '@h4ad/serverless-adapter/lib/handlers/default'; import { PromiseResolver } from '@h4ad/serverless-adapter/lib/resolvers/promise'; import vendia from '@vendia/serverless-express'; import benchmark from 'benchmark'; import serverlessHttp from 'serverless-http'; import { createApiGatewayV1 } from '../events'; import { FrameworkMock } from '../framework.mock'; console.log('Running simply-forward.ts'); const framework = new FrameworkMock(200, { message: 'Hello world' }); const handler = ServerlessAdapter.new(null) .setHandler(new DefaultHandler()) .setResolver(new PromiseResolver()) .setFramework(framework) .addAdapter(new ApiGatewayV1Adapter()) .build(); const falseApp = (req, res) => framework.sendRequest(null, req, res); const vendiaHandler = vendia({ app: falseApp, }); const serverlessHttpHandler = serverlessHttp(falseApp); const context = {} as any; const callback = {} as any; const eventV1ApiGateway = createApiGatewayV1('GET', '/test'); const suite = new benchmark.Suite(); suite.add( '@h4ad/serverless-adapter', async () => await handler(eventV1ApiGateway, context, callback), ); suite.add( '@vendia/serverless-express', async () => await vendiaHandler(eventV1ApiGateway, context, callback), ); suite.add( 'serverless-http', async () => await serverlessHttpHandler(eventV1ApiGateway, context), ); suite .on('cycle', function (event) { console.log(String(event.target)); }) .on('complete', function () { console.log('Fastest is ' + this.filter('fastest').map('name')); }) .run({ async: false, }); ================================================ FILE: benchmark/src/samples/format-headers.ts ================================================ import benchmark from 'benchmark'; import { BothValueHeaders } from '../../../src'; import { createApiGatewayV1 } from '../events'; function getFlattenedHeadersMap( headersMap: BothValueHeaders, separator: string = ',', lowerCaseKey: boolean = false, ): Record { const commaDelimitedHeaders: Record = {}; const headersMapEntries = Object.entries(headersMap); for (const [headerKey, headerValue] of headersMapEntries) { const newKey = lowerCaseKey ? headerKey.toLowerCase() : headerKey; if (Array.isArray(headerValue)) commaDelimitedHeaders[newKey] = headerValue.join(separator); else commaDelimitedHeaders[newKey] = String(headerValue ?? ''); } return commaDelimitedHeaders; } function getFlattenedHeadersV2( headersMap: BothValueHeaders, separator: string = ',', lowerCaseKey: boolean = false, ): Record { const commaDelimitedHeaders: Record = {}; for (const [headerKey, headerValue] of Object.entries(headersMap)) { const newKey = lowerCaseKey ? headerKey.toLowerCase() : headerKey; if (Array.isArray(headerValue)) commaDelimitedHeaders[newKey] = headerValue.join(separator); else commaDelimitedHeaders[newKey] = (headerValue ?? '') + ''; } return commaDelimitedHeaders; } function getFlattenedHeadersV3( headersMap: BothValueHeaders, separator: string = ',', lowerCaseKey: boolean = false, ): Record { return Object.keys(headersMap).reduce((acc, headerKey) => { const newKey = lowerCaseKey ? headerKey.toLowerCase() : headerKey; const headerValue = headersMap[headerKey]; if (Array.isArray(headerValue)) acc[newKey] = headerValue.join(separator); else acc[newKey] = (headerValue ?? '') + ''; return acc; }, {}); } const eventV1ApiGateway = createApiGatewayV1('GET', '/test'); const suite = new benchmark.Suite(); suite.add('getFlattenedHeadersMap', () => getFlattenedHeadersMap(eventV1ApiGateway.headers), ); suite.add('getFlattenedHeadersV2', () => getFlattenedHeadersV2(eventV1ApiGateway.headers), ); suite.add('getFlattenedHeadersV3', () => getFlattenedHeadersV3(eventV1ApiGateway.headers), ); suite .on('cycle', function (event) { console.log(String(event.target)); }) .on('complete', function () { console.log('Fastest is ' + this.filter('fastest').map('name')); }) .run({ async: false, }); ================================================ FILE: benchmark/tsconfig.json ================================================ { "compilerOptions": { "target": "ES2022", "module": "ESNext", "moduleResolution": "node", "jsx": "preserve", "declaration": true, "outDir": "dist", "allowJs": false, "allowSyntheticDefaultImports": true, "esModuleInterop": true, "strict": true, "skipLibCheck": true, "noImplicitAny": false, "noUnusedLocals": true, "noUnusedParameters": true, "strictNullChecks": true, "useUnknownInCatchVariables": false }, "include": [ "src/**/*.ts" ], "ts-node": { "esm": true } } ================================================ FILE: package.json ================================================ { "name": "@h4ad/serverless-adapter", "version": "4.4.0", "description": "Run REST APIs and other web applications using your existing Node.js application framework (NestJS, Express, Koa, Hapi, Fastify and many others), on top of AWS, Azure, Digital Ocean and many other clouds.", "type": "module", "main": "./lib/index.cjs", "module": "./lib/index.mjs", "types": "./lib/index.d.ts", "files": [ "lib/**/*" ], "scripts": { "prepare": "husky install", "build": "tsup", "build:docs": "tsc -p tsconfig.doc.json", "clean": "rm -rf ./lib/", "cm": "cz", "lint": "eslint ./src/ ./test/ --fix", "docs:generate": "npm run docs:generate:parsing && npm run docs:generate:markdown && npm run docs:generate:api-pages", "docs:generate:parsing": "npm run build:docs && npx tsx scripts/parse-docs.ts", "docs:generate:markdown": "npx tsx ./scripts/generate-markdown.ts", "docs:generate:api-pages": "npx tsx ./scripts/generate-api-pages.ts", "test:watch": "vitest --watch", "test": "vitest --run --coverage", "typecheck": "tsc --noEmit" }, "repository": { "type": "git", "url": "git+https://github.com/H4ad/serverless-adapter.git" }, "license": "MIT", "author": { "name": "Vinícius Lourenço", "email": "H4ad@users.noreply.github.com", "url": "https://github.com/H4ad" }, "engines": { "node": ">=18.0.0" }, "keywords": [ "aws", "serverless", "api gateway", "sqs", "sns", "lambda edge", "alb", "lambda", "lambda streaming", "response streaming", "apollo server", "express", "koa", "hapi", "fastify", "node.js", "http", "huawei", "functiongraph", "trpc", "azure", "azure functions", "http trigger v4", "firebase", "firebase functions", "firebase http events", "deepkit", "deepkit http", "digital ocean", "digital ocean functions", "digital ocean serverless", "gcp", "google cloud functions", "polka" ], "bugs": { "url": "https://github.com/H4ad/serverless-adapter/issues" }, "homepage": "https://github.com/H4ad/serverless-adapter#readme", "devDependencies": { "@apollo/server": "4.10.0", "@azure/functions": "3.5.1", "@deepkit/app": "1.0.1-alpha.155", "@deepkit/core": "1.0.1-alpha.154", "@deepkit/framework": "1.0.1-alpha.155", "@deepkit/http": "1.0.1-alpha.155", "@deepkit/injector": "1.0.1-alpha.155", "@deepkit/logger": "1.0.1-alpha.155", "@deepkit/stopwatch": "1.0.1-alpha.155", "@deepkit/type": "1.0.1-alpha.155", "@deepkit/workflow": "1.0.1-alpha.155", "@google-cloud/functions-framework": "3.4.2", "@hapi/hapi": "21.3.2", "@microsoft/api-documenter": "7.26.0", "@microsoft/api-extractor": "7.48.0", "@microsoft/api-extractor-model": "7.30.0", "@microsoft/tsdoc": "0.15.1", "@rushstack/node-core-library": "5.10.0", "@trpc/server": "10.45.2", "@types/aws-lambda": "8.10.146", "@types/body-parser": "1.19.5", "@types/cors": "2.8.17", "@types/express": "4.17.21", "@types/koa": "2.15.0", "@types/node": "18.19.67", "@types/polka": "0.5.7", "@types/supertest": "6.0.2", "@typescript-eslint/eslint-plugin": "6.21.0", "@typescript-eslint/parser": "6.21.0", "@vitest/coverage-v8": "2.1.6", "body-parser": "1.20.3", "body-parser-v2": "npm:body-parser@2", "commitizen": "4.3.0", "cors": "2.8.5", "cz-conventional-changelog": "3.3.0", "esbuild": "0.24.0", "eslint": "8.57.1", "eslint-config-prettier": "9.1.0", "eslint-plugin-import": "2.31.0", "eslint-plugin-node": "11.1.0", "eslint-plugin-prettier": "5.2.1", "eslint-plugin-tsdoc": "0.4.0", "express": "4.21.1", "express-v5": "npm:express@5", "fastify": "4.28.1", "fastify-v5": "npm:fastify@5", "firebase-admin": "11.11.1", "firebase-functions": "4.5.0", "firebase-functions-v5": "npm:firebase-functions@5", "firebase-functions-v6": "npm:firebase-functions@6", "glob": "10.4.5", "husky": "8.0.3", "koa": "2.15.3", "polka": "0.5.2", "prettier": "3.4.1", "stream-mock": "2.0.5", "supertest": "6.3.4", "ts-node": "10.9.2", "tsup": "8.3.5", "typedoc": "0.27.2", "typescript": "~5.3.3", "vite": "6.0.1", "vitest": "2.1.6" }, "peerDependencies": { "@apollo/server": ">= 4.0.0", "@azure/functions": ">= 2.0.0", "@deepkit/http": ">= 1.0.1-alpha.94", "@google-cloud/functions-framework": ">= 3.0.0", "@hapi/hapi": ">= 21.0.0", "@trpc/server": ">= 10.0.0", "@types/aws-lambda": ">= 8.10.92", "@types/body-parser": ">= 1.19.2", "@types/cors": ">= 2.8.12", "@types/express": ">= 4.15.4", "@types/koa": ">= 2.11.2", "body-parser": ">= 1.20.0", "cors": ">= 2.8.5", "express": ">= 4.15.4", "fastify": ">= 4.0.0", "firebase-admin": ">= 11.0.0", "firebase-functions": ">= 4.0.0", "http-errors": ">= 2.0.0", "koa": ">= 2.5.1", "reflect-metadata": ">= 0.1.13" }, "peerDependenciesMeta": { "@apollo/server": { "optional": true }, "@azure/functions": { "optional": true }, "@deepkit/http": { "optional": true }, "@google-cloud/functions-framework": { "optional": true }, "@hapi/hapi": { "optional": true }, "@trpc/server": { "optional": true }, "@types/aws-lambda": { "optional": true }, "@types/express": { "optional": true }, "@types/hapi": { "optional": true }, "@types/koa": { "optional": true }, "body-parser": { "optional": true }, "cors": { "optional": true }, "express": { "optional": true }, "fastify": { "optional": true }, "firebase-admin": { "optional": true }, "firebase-functions": { "optional": true }, "http-errors": { "optional": true }, "koa": { "optional": true }, "reflect-metadata": { "optional": true } }, "config": { "commitizen": { "path": "./node_modules/cz-conventional-changelog" } }, "publishConfig": { "access": "public" }, "exports": { ".": { "import": { "types": "./lib/index.d.ts", "default": "./lib/index.mjs" }, "require": { "types": "./lib/index.d.cts", "default": "./lib/index.cjs" } }, "./adapters/apollo-server": { "import": { "types": "./lib/adapters/apollo-server/index.d.ts", "default": "./lib/adapters/apollo-server/index.mjs" }, "require": { "types": "./lib/adapters/apollo-server/index.d.cts", "default": "./lib/adapters/apollo-server/index.cjs" } }, "./lib/adapters/apollo-server": { "import": { "types": "./lib/adapters/apollo-server/index.d.ts", "default": "./lib/adapters/apollo-server/index.mjs" }, "require": { "types": "./lib/adapters/apollo-server/index.d.cts", "default": "./lib/adapters/apollo-server/index.cjs" } }, "./adapters/aws": { "import": { "types": "./lib/adapters/aws/index.d.ts", "default": "./lib/adapters/aws/index.mjs" }, "require": { "types": "./lib/adapters/aws/index.d.cts", "default": "./lib/adapters/aws/index.cjs" } }, "./lib/adapters/aws": { "import": { "types": "./lib/adapters/aws/index.d.ts", "default": "./lib/adapters/aws/index.mjs" }, "require": { "types": "./lib/adapters/aws/index.d.cts", "default": "./lib/adapters/aws/index.cjs" } }, "./adapters/azure": { "import": { "types": "./lib/adapters/azure/index.d.ts", "default": "./lib/adapters/azure/index.mjs" }, "require": { "types": "./lib/adapters/azure/index.d.cts", "default": "./lib/adapters/azure/index.cjs" } }, "./lib/adapters/azure": { "import": { "types": "./lib/adapters/azure/index.d.ts", "default": "./lib/adapters/azure/index.mjs" }, "require": { "types": "./lib/adapters/azure/index.d.cts", "default": "./lib/adapters/azure/index.cjs" } }, "./adapters/digital-ocean": { "import": { "types": "./lib/adapters/digital-ocean/index.d.ts", "default": "./lib/adapters/digital-ocean/index.mjs" }, "require": { "types": "./lib/adapters/digital-ocean/index.d.cts", "default": "./lib/adapters/digital-ocean/index.cjs" } }, "./lib/adapters/digital-ocean": { "import": { "types": "./lib/adapters/digital-ocean/index.d.ts", "default": "./lib/adapters/digital-ocean/index.mjs" }, "require": { "types": "./lib/adapters/digital-ocean/index.d.cts", "default": "./lib/adapters/digital-ocean/index.cjs" } }, "./adapters/dummy": { "import": { "types": "./lib/adapters/dummy/index.d.ts", "default": "./lib/adapters/dummy/index.mjs" }, "require": { "types": "./lib/adapters/dummy/index.d.cts", "default": "./lib/adapters/dummy/index.cjs" } }, "./lib/adapters/dummy": { "import": { "types": "./lib/adapters/dummy/index.d.ts", "default": "./lib/adapters/dummy/index.mjs" }, "require": { "types": "./lib/adapters/dummy/index.d.cts", "default": "./lib/adapters/dummy/index.cjs" } }, "./adapters/huawei": { "import": { "types": "./lib/adapters/huawei/index.d.ts", "default": "./lib/adapters/huawei/index.mjs" }, "require": { "types": "./lib/adapters/huawei/index.d.cts", "default": "./lib/adapters/huawei/index.cjs" } }, "./lib/adapters/huawei": { "import": { "types": "./lib/adapters/huawei/index.d.ts", "default": "./lib/adapters/huawei/index.mjs" }, "require": { "types": "./lib/adapters/huawei/index.d.cts", "default": "./lib/adapters/huawei/index.cjs" } }, "./frameworks/apollo-server": { "import": { "types": "./lib/frameworks/apollo-server/index.d.ts", "default": "./lib/frameworks/apollo-server/index.mjs" }, "require": { "types": "./lib/frameworks/apollo-server/index.d.cts", "default": "./lib/frameworks/apollo-server/index.cjs" } }, "./lib/frameworks/apollo-server": { "import": { "types": "./lib/frameworks/apollo-server/index.d.ts", "default": "./lib/frameworks/apollo-server/index.mjs" }, "require": { "types": "./lib/frameworks/apollo-server/index.d.cts", "default": "./lib/frameworks/apollo-server/index.cjs" } }, "./frameworks/body-parser": { "import": { "types": "./lib/frameworks/body-parser/index.d.ts", "default": "./lib/frameworks/body-parser/index.mjs" }, "require": { "types": "./lib/frameworks/body-parser/index.d.cts", "default": "./lib/frameworks/body-parser/index.cjs" } }, "./lib/frameworks/body-parser": { "import": { "types": "./lib/frameworks/body-parser/index.d.ts", "default": "./lib/frameworks/body-parser/index.mjs" }, "require": { "types": "./lib/frameworks/body-parser/index.d.cts", "default": "./lib/frameworks/body-parser/index.cjs" } }, "./frameworks/cors": { "import": { "types": "./lib/frameworks/cors/index.d.ts", "default": "./lib/frameworks/cors/index.mjs" }, "require": { "types": "./lib/frameworks/cors/index.d.cts", "default": "./lib/frameworks/cors/index.cjs" } }, "./lib/frameworks/cors": { "import": { "types": "./lib/frameworks/cors/index.d.ts", "default": "./lib/frameworks/cors/index.mjs" }, "require": { "types": "./lib/frameworks/cors/index.d.cts", "default": "./lib/frameworks/cors/index.cjs" } }, "./frameworks/deepkit": { "import": { "types": "./lib/frameworks/deepkit/index.d.ts", "default": "./lib/frameworks/deepkit/index.mjs" }, "require": { "types": "./lib/frameworks/deepkit/index.d.cts", "default": "./lib/frameworks/deepkit/index.cjs" } }, "./lib/frameworks/deepkit": { "import": { "types": "./lib/frameworks/deepkit/index.d.ts", "default": "./lib/frameworks/deepkit/index.mjs" }, "require": { "types": "./lib/frameworks/deepkit/index.d.cts", "default": "./lib/frameworks/deepkit/index.cjs" } }, "./frameworks/express": { "import": { "types": "./lib/frameworks/express/index.d.ts", "default": "./lib/frameworks/express/index.mjs" }, "require": { "types": "./lib/frameworks/express/index.d.cts", "default": "./lib/frameworks/express/index.cjs" } }, "./lib/frameworks/express": { "import": { "types": "./lib/frameworks/express/index.d.ts", "default": "./lib/frameworks/express/index.mjs" }, "require": { "types": "./lib/frameworks/express/index.d.cts", "default": "./lib/frameworks/express/index.cjs" } }, "./frameworks/fastify": { "import": { "types": "./lib/frameworks/fastify/index.d.ts", "default": "./lib/frameworks/fastify/index.mjs" }, "require": { "types": "./lib/frameworks/fastify/index.d.cts", "default": "./lib/frameworks/fastify/index.cjs" } }, "./lib/frameworks/fastify": { "import": { "types": "./lib/frameworks/fastify/index.d.ts", "default": "./lib/frameworks/fastify/index.mjs" }, "require": { "types": "./lib/frameworks/fastify/index.d.cts", "default": "./lib/frameworks/fastify/index.cjs" } }, "./frameworks/hapi": { "import": { "types": "./lib/frameworks/hapi/index.d.ts", "default": "./lib/frameworks/hapi/index.mjs" }, "require": { "types": "./lib/frameworks/hapi/index.d.cts", "default": "./lib/frameworks/hapi/index.cjs" } }, "./lib/frameworks/hapi": { "import": { "types": "./lib/frameworks/hapi/index.d.ts", "default": "./lib/frameworks/hapi/index.mjs" }, "require": { "types": "./lib/frameworks/hapi/index.d.cts", "default": "./lib/frameworks/hapi/index.cjs" } }, "./frameworks/koa": { "import": { "types": "./lib/frameworks/koa/index.d.ts", "default": "./lib/frameworks/koa/index.mjs" }, "require": { "types": "./lib/frameworks/koa/index.d.cts", "default": "./lib/frameworks/koa/index.cjs" } }, "./lib/frameworks/koa": { "import": { "types": "./lib/frameworks/koa/index.d.ts", "default": "./lib/frameworks/koa/index.mjs" }, "require": { "types": "./lib/frameworks/koa/index.d.cts", "default": "./lib/frameworks/koa/index.cjs" } }, "./frameworks/lazy": { "import": { "types": "./lib/frameworks/lazy/index.d.ts", "default": "./lib/frameworks/lazy/index.mjs" }, "require": { "types": "./lib/frameworks/lazy/index.d.cts", "default": "./lib/frameworks/lazy/index.cjs" } }, "./lib/frameworks/lazy": { "import": { "types": "./lib/frameworks/lazy/index.d.ts", "default": "./lib/frameworks/lazy/index.mjs" }, "require": { "types": "./lib/frameworks/lazy/index.d.cts", "default": "./lib/frameworks/lazy/index.cjs" } }, "./frameworks/polka": { "import": { "types": "./lib/frameworks/polka/index.d.ts", "default": "./lib/frameworks/polka/index.mjs" }, "require": { "types": "./lib/frameworks/polka/index.d.cts", "default": "./lib/frameworks/polka/index.cjs" } }, "./lib/frameworks/polka": { "import": { "types": "./lib/frameworks/polka/index.d.ts", "default": "./lib/frameworks/polka/index.mjs" }, "require": { "types": "./lib/frameworks/polka/index.d.cts", "default": "./lib/frameworks/polka/index.cjs" } }, "./frameworks/trpc": { "import": { "types": "./lib/frameworks/trpc/index.d.ts", "default": "./lib/frameworks/trpc/index.mjs" }, "require": { "types": "./lib/frameworks/trpc/index.d.cts", "default": "./lib/frameworks/trpc/index.cjs" } }, "./lib/frameworks/trpc": { "import": { "types": "./lib/frameworks/trpc/index.d.ts", "default": "./lib/frameworks/trpc/index.mjs" }, "require": { "types": "./lib/frameworks/trpc/index.d.cts", "default": "./lib/frameworks/trpc/index.cjs" } }, "./handlers/aws": { "import": { "types": "./lib/handlers/aws/index.d.ts", "default": "./lib/handlers/aws/index.mjs" }, "require": { "types": "./lib/handlers/aws/index.d.cts", "default": "./lib/handlers/aws/index.cjs" } }, "./lib/handlers/aws": { "import": { "types": "./lib/handlers/aws/index.d.ts", "default": "./lib/handlers/aws/index.mjs" }, "require": { "types": "./lib/handlers/aws/index.d.cts", "default": "./lib/handlers/aws/index.cjs" } }, "./handlers/azure": { "import": { "types": "./lib/handlers/azure/index.d.ts", "default": "./lib/handlers/azure/index.mjs" }, "require": { "types": "./lib/handlers/azure/index.d.cts", "default": "./lib/handlers/azure/index.cjs" } }, "./lib/handlers/azure": { "import": { "types": "./lib/handlers/azure/index.d.ts", "default": "./lib/handlers/azure/index.mjs" }, "require": { "types": "./lib/handlers/azure/index.d.cts", "default": "./lib/handlers/azure/index.cjs" } }, "./handlers/default": { "import": { "types": "./lib/handlers/default/index.d.ts", "default": "./lib/handlers/default/index.mjs" }, "require": { "types": "./lib/handlers/default/index.d.cts", "default": "./lib/handlers/default/index.cjs" } }, "./lib/handlers/default": { "import": { "types": "./lib/handlers/default/index.d.ts", "default": "./lib/handlers/default/index.mjs" }, "require": { "types": "./lib/handlers/default/index.d.cts", "default": "./lib/handlers/default/index.cjs" } }, "./handlers/digital-ocean": { "import": { "types": "./lib/handlers/digital-ocean/index.d.ts", "default": "./lib/handlers/digital-ocean/index.mjs" }, "require": { "types": "./lib/handlers/digital-ocean/index.d.cts", "default": "./lib/handlers/digital-ocean/index.cjs" } }, "./lib/handlers/digital-ocean": { "import": { "types": "./lib/handlers/digital-ocean/index.d.ts", "default": "./lib/handlers/digital-ocean/index.mjs" }, "require": { "types": "./lib/handlers/digital-ocean/index.d.cts", "default": "./lib/handlers/digital-ocean/index.cjs" } }, "./handlers/firebase": { "import": { "types": "./lib/handlers/firebase/index.d.ts", "default": "./lib/handlers/firebase/index.mjs" }, "require": { "types": "./lib/handlers/firebase/index.d.cts", "default": "./lib/handlers/firebase/index.cjs" } }, "./lib/handlers/firebase": { "import": { "types": "./lib/handlers/firebase/index.d.ts", "default": "./lib/handlers/firebase/index.mjs" }, "require": { "types": "./lib/handlers/firebase/index.d.cts", "default": "./lib/handlers/firebase/index.cjs" } }, "./handlers/gcp": { "import": { "types": "./lib/handlers/gcp/index.d.ts", "default": "./lib/handlers/gcp/index.mjs" }, "require": { "types": "./lib/handlers/gcp/index.d.cts", "default": "./lib/handlers/gcp/index.cjs" } }, "./lib/handlers/gcp": { "import": { "types": "./lib/handlers/gcp/index.d.ts", "default": "./lib/handlers/gcp/index.mjs" }, "require": { "types": "./lib/handlers/gcp/index.d.cts", "default": "./lib/handlers/gcp/index.cjs" } }, "./handlers/huawei": { "import": { "types": "./lib/handlers/huawei/index.d.ts", "default": "./lib/handlers/huawei/index.mjs" }, "require": { "types": "./lib/handlers/huawei/index.d.cts", "default": "./lib/handlers/huawei/index.cjs" } }, "./lib/handlers/huawei": { "import": { "types": "./lib/handlers/huawei/index.d.ts", "default": "./lib/handlers/huawei/index.mjs" }, "require": { "types": "./lib/handlers/huawei/index.d.cts", "default": "./lib/handlers/huawei/index.cjs" } }, "./resolvers/aws-context": { "import": { "types": "./lib/resolvers/aws-context/index.d.ts", "default": "./lib/resolvers/aws-context/index.mjs" }, "require": { "types": "./lib/resolvers/aws-context/index.d.cts", "default": "./lib/resolvers/aws-context/index.cjs" } }, "./lib/resolvers/aws-context": { "import": { "types": "./lib/resolvers/aws-context/index.d.ts", "default": "./lib/resolvers/aws-context/index.mjs" }, "require": { "types": "./lib/resolvers/aws-context/index.d.cts", "default": "./lib/resolvers/aws-context/index.cjs" } }, "./resolvers/callback": { "import": { "types": "./lib/resolvers/callback/index.d.ts", "default": "./lib/resolvers/callback/index.mjs" }, "require": { "types": "./lib/resolvers/callback/index.d.cts", "default": "./lib/resolvers/callback/index.cjs" } }, "./lib/resolvers/callback": { "import": { "types": "./lib/resolvers/callback/index.d.ts", "default": "./lib/resolvers/callback/index.mjs" }, "require": { "types": "./lib/resolvers/callback/index.d.cts", "default": "./lib/resolvers/callback/index.cjs" } }, "./resolvers/dummy": { "import": { "types": "./lib/resolvers/dummy/index.d.ts", "default": "./lib/resolvers/dummy/index.mjs" }, "require": { "types": "./lib/resolvers/dummy/index.d.cts", "default": "./lib/resolvers/dummy/index.cjs" } }, "./lib/resolvers/dummy": { "import": { "types": "./lib/resolvers/dummy/index.d.ts", "default": "./lib/resolvers/dummy/index.mjs" }, "require": { "types": "./lib/resolvers/dummy/index.d.cts", "default": "./lib/resolvers/dummy/index.cjs" } }, "./resolvers/promise": { "import": { "types": "./lib/resolvers/promise/index.d.ts", "default": "./lib/resolvers/promise/index.mjs" }, "require": { "types": "./lib/resolvers/promise/index.d.cts", "default": "./lib/resolvers/promise/index.cjs" } }, "./lib/resolvers/promise": { "import": { "types": "./lib/resolvers/promise/index.d.ts", "default": "./lib/resolvers/promise/index.mjs" }, "require": { "types": "./lib/resolvers/promise/index.d.cts", "default": "./lib/resolvers/promise/index.cjs" } } } } ================================================ FILE: release-please-config.json ================================================ { "packages": { ".": { "release-type": "node", "bump-minor-pre-major": false, "bump-patch-for-minor-pre-major": false, "draft": false, "prerelease": false, "draft-pull-request": true, "include-component-in-tag": false, "include-v-in-tag": true, "separate-pull-requests": false, "skip-github-release": false, "versioning": "default", "pull-request-header": ":robot: I have created a release *beep* *boop*", "pull-request-title-pattern": "chore${scope}: release${component} ${version}", "changelog-path": "CHANGELOG.md", "changelog-host": "https://github.com", "changelog-type": "default", "changelog-sections": [ { "type": "feat", "section": "Features" }, { "type": "feature", "section": "Features" }, { "type": "fix", "section": "Bug Fixes" }, { "type": "perf", "section": "Performance Improvements" }, { "type": "revert", "section": "Reverts" }, { "type": "docs", "section": "Documentation" }, { "type": "style", "section": "Styles" }, { "type": "chore", "section": "Miscellaneous Chores" }, { "type": "refactor", "section": "Code Refactoring" }, { "type": "test", "section": "Tests" }, { "type": "build", "section": "Build System" }, { "type": "ci", "section": "Continuous Integration" } ] } }, "$schema": "https://raw.githubusercontent.com/googleapis/release-please/main/schemas/config.json" } ================================================ FILE: scripts/generate-api-pages.ts ================================================ import { writeFileSync } from 'fs'; import { resolve } from 'path'; import { ApiDocumentedItem, ApiItem, ApiModel, } from '@microsoft/api-extractor-model'; import { DocNode, DocNodeKind, DocPlainText } from '@microsoft/tsdoc'; const apiModelPath = resolve('.', 'temp', 'serverless-adapter.api.json'); const outputFile = resolve('.', 'www', 'sidebar-api-generated.js'); type BreadcrumbItem = { breadcrumbs: string[]; apiMember: ApiItem; }; function isPlainTextNode(block: DocNode): block is DocPlainText { // eslint-disable-next-line @typescript-eslint/no-unsafe-enum-comparison return block.kind === DocNodeKind.PlainText; } function getBreadcrumbsWithApiItem(apiModel: ApiModel): BreadcrumbItem[] { const breadcrumbs: BreadcrumbItem[] = []; for (const apiPackage of apiModel.members) { for (const apiEntrypoint of apiPackage.members) { for (const apiMember of apiEntrypoint.members) { const apiDocumentedItem = apiMember as ApiDocumentedItem; if (!apiDocumentedItem.tsdocComment) continue; const breadcrumb = apiDocumentedItem.tsdocComment.customBlocks.find( block => block.blockTag.tagName === '@breadcrumb', ); if (!breadcrumb) continue; const breadcrumbContent = breadcrumb.content .getChildNodes() // eslint-disable-next-line @typescript-eslint/no-unsafe-enum-comparison .filter(block => block.kind === DocNodeKind.Paragraph) // @ts-ignore .reduce((acc, block) => [...acc, ...block.getChildNodes()], []) // @ts-ignore .filter(isPlainTextNode) .map((plainText: DocPlainText) => plainText.text) .join(''); breadcrumbs.push({ breadcrumbs: breadcrumbContent .split('/') .map(section => section.trimLeft().trimRight()), apiMember, }); } } } return breadcrumbs; } type SidebarItem = { type: string; label: string; link?: { type: string; id: string; }; items: Sidebar[]; }; type Sidebar = SidebarItem; function build(): void { const apiModel = new ApiModel(); apiModel.loadPackage(apiModelPath); const breadcrumbsWithApiItems = getBreadcrumbsWithApiItem(apiModel); const pages: Sidebar[] = []; for (const breadcrumbWithApiItem of breadcrumbsWithApiItems) { let lastPage: SidebarItem | undefined; for (const breadcrumb of breadcrumbWithApiItem.breadcrumbs) { const newPage: SidebarItem = { type: 'category', label: breadcrumb, items: [], link: { id: `./api/${breadcrumb}`, type: 'doc', }, }; if (lastPage) { let subpage = lastPage.items.find(page => page.label === breadcrumb); if (!subpage) { subpage = newPage; lastPage.items.push(subpage); } lastPage = subpage; } else { const oldPage = pages.find(page => page.label === breadcrumb); if (oldPage) lastPage = oldPage; else { lastPage = newPage; pages.push(lastPage); } } } if (!lastPage) { throw new Error( `Breadcrumb was configured incorrectly. Error found in ${breadcrumbWithApiItem.apiMember.displayName}.`, ); } lastPage.items.push({ type: 'category', label: breadcrumbWithApiItem.apiMember.displayName, items: [], }); } writeFileSync(outputFile, `export default ${JSON.stringify(pages)}`); } build(); ================================================ FILE: scripts/generate-markdown.ts ================================================ import { readFileSync, writeFileSync } from 'fs'; import { join, resolve } from 'path'; import { ApiModel } from '@microsoft/api-extractor-model'; import { CustomMarkdownDocumenter } from './libs/CustomMarkdownDocumenter'; const apiModelPath = resolve('.', 'temp', 'serverless-adapter.api.json'); const outputFolder = resolve('.', 'www', 'docs', 'api'); function build(): void { const apiModel = new ApiModel(); apiModel.loadPackage(apiModelPath); const markdown = new CustomMarkdownDocumenter({ apiModel, outputFolder, documenterConfig: undefined, }); markdown.generateFiles(); const filename = join(outputFolder, 'Introduction.md'); const introductionMarkdownContent = readFileSync(filename); const introductionContent = `---\ntitle: Introduction\nsidebar_position: -1\n---\n\n${introductionMarkdownContent}`; writeFileSync(filename, introductionContent); } build(); ================================================ FILE: scripts/libs/CustomMarkdownDocumenter.ts ================================================ import * as path from 'path'; import { type IMarkdownDocumenterFeatureOnBeforeWritePageArgs, MarkdownDocumenterAccessor, MarkdownDocumenterFeatureContext, } from '@microsoft/api-documenter/lib'; import { DocumenterConfig } from '@microsoft/api-documenter/lib/documenters/DocumenterConfig'; import { CustomDocNodes } from '@microsoft/api-documenter/lib/nodes/CustomDocNodeKind'; import { DocEmphasisSpan } from '@microsoft/api-documenter/lib/nodes/DocEmphasisSpan'; import { DocHeading } from '@microsoft/api-documenter/lib/nodes/DocHeading'; import { DocNoteBox } from '@microsoft/api-documenter/lib/nodes/DocNoteBox'; import { DocTable } from '@microsoft/api-documenter/lib/nodes/DocTable'; import { DocTableCell } from '@microsoft/api-documenter/lib/nodes/DocTableCell'; import { DocTableRow } from '@microsoft/api-documenter/lib/nodes/DocTableRow'; import { PluginLoader } from '@microsoft/api-documenter/lib/plugin/PluginLoader'; import { Utilities } from '@microsoft/api-documenter/lib/utils/Utilities'; import { ApiClass, ApiDeclaredItem, ApiDocumentedItem, ApiEnum, ApiInterface, ApiItem, ApiItemKind, ApiModel, ApiNamespace, ApiOptionalMixin, ApiPackage, ApiParameterListMixin, ApiReleaseTagMixin, ApiReturnTypeMixin, ApiTypeAlias, Excerpt, ExcerptToken, ExcerptTokenKind, type IResolveDeclarationReferenceResult, ReleaseTag, } from '@microsoft/api-extractor-model'; import { DocBlock, DocCodeSpan, DocComment, DocFencedCode, DocLinkTag, DocNodeContainer, DocNodeKind, DocParagraph, DocPlainText, DocSection, StandardTags, StringBuilder, TSDocConfiguration, } from '@microsoft/tsdoc'; import { FileSystem, NewlineKind, PackageName, } from '@rushstack/node-core-library'; import { CustomUtilities } from './CustomUtilities'; import { MarkdownEmitter } from './MarkdownEmitter'; export interface IMarkdownDocumenterOptions { apiModel: ApiModel; documenterConfig: DocumenterConfig | undefined; outputFolder: string; } /** * Renders API documentation in the Markdown file format. * For more info: https://en.wikipedia.org/wiki/Markdown */ export class CustomMarkdownDocumenter { public constructor(options: IMarkdownDocumenterOptions) { this._apiModel = options.apiModel; this._documenterConfig = options.documenterConfig; this._outputFolder = options.outputFolder; this._tsdocConfiguration = CustomDocNodes.configuration; this._markdownEmitter = new MarkdownEmitter(this._apiModel); this._pluginLoader = new PluginLoader(); } private readonly _apiModel: ApiModel; private readonly _documenterConfig: DocumenterConfig | undefined; private readonly _tsdocConfiguration: TSDocConfiguration; private readonly _markdownEmitter: MarkdownEmitter; private readonly _outputFolder: string; private readonly _pluginLoader: PluginLoader; public generateFiles(): void { if (this._documenterConfig) { this._pluginLoader.load(this._documenterConfig, () => { return new MarkdownDocumenterFeatureContext({ apiModel: this._apiModel, outputFolder: this._outputFolder, documenter: new MarkdownDocumenterAccessor({ getLinkForApiItem: (apiItem: ApiItem) => { return this._getLinkFilenameForApiItem(apiItem); }, }), }); }); } this._deleteOldOutputFiles(); this._writeApiItemPage(this._apiModel); if (this._pluginLoader.markdownDocumenterFeature) this._pluginLoader.markdownDocumenterFeature.onFinished({}); } private _writeApiItemPage(apiItem: ApiItem, parentOutput?: DocSection): void { const configuration: TSDocConfiguration = this._tsdocConfiguration; const output: DocSection = parentOutput || new DocSection({ configuration: this._tsdocConfiguration, }); if (!parentOutput) this._writeBreadcrumb(output, apiItem); const scopedName: string = apiItem.displayName; switch (apiItem.kind) { case ApiItemKind.Class: output.appendNode( new DocHeading({ configuration, title: `(class) ${scopedName}` }), ); break; case ApiItemKind.Enum: output.appendNode( new DocHeading({ configuration, title: `(enum) ${scopedName}` }), ); break; case ApiItemKind.Interface: output.appendNode( new DocHeading({ configuration, title: `(interface) ${scopedName}` }), ); break; case ApiItemKind.Constructor: case ApiItemKind.ConstructSignature: output.appendNode(new DocHeading({ configuration, title: scopedName })); break; case ApiItemKind.Method: case ApiItemKind.MethodSignature: output.appendNode( new DocHeading({ configuration, title: `(method) ${scopedName}` }), ); break; case ApiItemKind.Function: output.appendNode( new DocHeading({ configuration, title: `(function) ${scopedName}` }), ); break; case ApiItemKind.Model: output.appendNode( new DocHeading({ configuration, title: 'API Reference' }), ); break; case ApiItemKind.Namespace: output.appendNode( new DocHeading({ configuration, title: `(namespace) ${scopedName}` }), ); break; case ApiItemKind.Package: console.log(`Writing ${apiItem.displayName} package`); const unscopedPackageName: string = PackageName.getUnscopedName( apiItem.displayName, ); output.appendNode( new DocHeading({ configuration, title: `(package) ${unscopedPackageName}`, }), ); break; case ApiItemKind.Property: case ApiItemKind.PropertySignature: output.appendNode( new DocHeading({ configuration, title: `(property) ${scopedName}` }), ); break; case ApiItemKind.TypeAlias: output.appendNode( new DocHeading({ configuration, title: `(type) ${scopedName}` }), ); break; case ApiItemKind.Variable: output.appendNode( new DocHeading({ configuration, title: `(variable) ${scopedName}` }), ); break; default: throw new Error('Unsupported API item kind: ' + apiItem.kind); } if (ApiReleaseTagMixin.isBaseClassOf(apiItem)) { if (apiItem.releaseTag === ReleaseTag.Beta) this._writeBetaWarning(output); } const decoratorBlocks: DocBlock[] = []; if (apiItem instanceof ApiDocumentedItem) { const tsdocComment: DocComment | undefined = apiItem.tsdocComment; if (tsdocComment) { decoratorBlocks.push( ...tsdocComment.customBlocks.filter( block => block.blockTag.tagNameWithUpperCase === StandardTags.decorator.tagNameWithUpperCase, ), ); if (tsdocComment.deprecatedBlock) { output.appendNode( new DocNoteBox({ configuration: this._tsdocConfiguration }, [ new DocParagraph({ configuration: this._tsdocConfiguration }, [ new DocPlainText({ configuration: this._tsdocConfiguration, text: 'Warning: This API is now obsolete. ', }), ]), ...tsdocComment.deprecatedBlock.content.nodes, ]), ); } this._appendSection(output, tsdocComment.summarySection); } } if (apiItem instanceof ApiDeclaredItem) { if (apiItem.excerpt.text.length > 0) { output.appendNode( new DocParagraph({ configuration }, [ new DocEmphasisSpan({ configuration, bold: true }, [ new DocPlainText({ configuration, text: 'Signature:' }), ]), ]), ); output.appendNode( new DocFencedCode({ configuration, code: apiItem.getExcerptWithModifiers(), language: 'typescript', }), ); } this._writeHeritageTypes(output, apiItem); } if (decoratorBlocks.length > 0) { output.appendNode( new DocParagraph({ configuration }, [ new DocEmphasisSpan({ configuration, bold: true }, [ new DocPlainText({ configuration, text: 'Decorators:' }), ]), ]), ); for (const decoratorBlock of decoratorBlocks) output.appendNodes(decoratorBlock.content.nodes); } let appendRemarks: boolean = true; switch (apiItem.kind) { case ApiItemKind.Class: case ApiItemKind.Interface: case ApiItemKind.Namespace: case ApiItemKind.Package: this._writeRemarksSection(output, apiItem); appendRemarks = false; break; } switch (apiItem.kind) { case ApiItemKind.Class: this._writeClassTables(output, apiItem as ApiClass); break; case ApiItemKind.Enum: this._writeEnumTables(output, apiItem as ApiEnum); break; case ApiItemKind.Interface: this._writeInterfaceTables(output, apiItem as ApiInterface); break; case ApiItemKind.Constructor: case ApiItemKind.ConstructSignature: case ApiItemKind.Method: case ApiItemKind.MethodSignature: case ApiItemKind.Function: this._writeParameterTables(output, apiItem as ApiParameterListMixin); this._writeThrowsSection(output, apiItem); break; case ApiItemKind.Namespace: this._writePackageOrNamespaceTables(output, apiItem as ApiNamespace); break; case ApiItemKind.Model: this._writeModelTable(output, apiItem as ApiModel); break; case ApiItemKind.Package: this._writePackageOrNamespaceTables(output, apiItem as ApiPackage); break; case ApiItemKind.Property: case ApiItemKind.PropertySignature: break; case ApiItemKind.TypeAlias: break; case ApiItemKind.Variable: break; default: throw new Error(`Unsupported API item kind: ${apiItem.kind}`); } if (appendRemarks) this._writeRemarksSection(output, apiItem); const filename: string = path.join( this._outputFolder, this._getFilenameForApiItem(apiItem, true), ); const stringBuilder: StringBuilder = new StringBuilder(); stringBuilder.append( '\n\n', ); this._markdownEmitter.emit(stringBuilder, output, { contextApiItem: apiItem, onGetFilenameForApiItem: (apiItemForFilename: ApiItem) => { return this._getLinkFilenameForApiItem(apiItemForFilename); }, }); let pageContent: string = stringBuilder.toString(); if (this._pluginLoader.markdownDocumenterFeature) { // Allow the plugin to customize the pageContent const eventArgs: IMarkdownDocumenterFeatureOnBeforeWritePageArgs = { apiItem: apiItem, outputFilename: filename, pageContent: pageContent, }; this._pluginLoader.markdownDocumenterFeature.onBeforeWritePage(eventArgs); pageContent = eventArgs.pageContent; } if (apiItem.kind == ApiItemKind.Model) return; if (parentOutput) return; if (filename.includes('ignored.md')) return; FileSystem.writeFile(filename, pageContent.replaceAll('{', '\\{'), { ensureFolderExists: true, convertLineEndings: this._documenterConfig ? this._documenterConfig.newlineKind : NewlineKind.CrLf, }); } private _writeHeritageTypes( output: DocSection, apiItem: ApiDeclaredItem, ): void { const configuration: TSDocConfiguration = this._tsdocConfiguration; if (apiItem instanceof ApiClass) { if (apiItem.extendsType) { const extendsParagraph: DocParagraph = new DocParagraph( { configuration }, [ new DocEmphasisSpan({ configuration, bold: true }, [ new DocPlainText({ configuration, text: 'Extends: ' }), ]), ], ); this._appendExcerptWithHyperlinks( extendsParagraph, apiItem.extendsType.excerpt, ); output.appendNode(extendsParagraph); } if (apiItem.implementsTypes.length > 0) { const extendsParagraph: DocParagraph = new DocParagraph( { configuration }, [ new DocEmphasisSpan({ configuration, bold: true }, [ new DocPlainText({ configuration, text: 'Implements: ' }), ]), ], ); let needsComma: boolean = false; for (const implementsType of apiItem.implementsTypes) { if (needsComma) { extendsParagraph.appendNode( new DocPlainText({ configuration, text: ', ' }), ); } this._appendExcerptWithHyperlinks( extendsParagraph, implementsType.excerpt, ); needsComma = true; } output.appendNode(extendsParagraph); } } if (apiItem instanceof ApiInterface) { if (apiItem.extendsTypes.length > 0) { const extendsParagraph: DocParagraph = new DocParagraph( { configuration }, [ new DocEmphasisSpan({ configuration, bold: true }, [ new DocPlainText({ configuration, text: 'Extends: ' }), ]), ], ); let needsComma: boolean = false; for (const extendsType of apiItem.extendsTypes) { if (needsComma) { extendsParagraph.appendNode( new DocPlainText({ configuration, text: ', ' }), ); } this._appendExcerptWithHyperlinks( extendsParagraph, extendsType.excerpt, ); needsComma = true; } output.appendNode(extendsParagraph); } } if (apiItem instanceof ApiTypeAlias) { const refs: ExcerptToken[] = apiItem.excerptTokens.filter( token => token.kind === ExcerptTokenKind.Reference && token.canonicalReference && this._apiModel.resolveDeclarationReference( token.canonicalReference, undefined, ).resolvedApiItem, ); if (refs.length > 0) { const referencesParagraph: DocParagraph = new DocParagraph( { configuration }, [ new DocEmphasisSpan({ configuration, bold: true }, [ new DocPlainText({ configuration, text: 'References: ' }), ]), ], ); let needsComma: boolean = false; const visited: Set = new Set(); for (const ref of refs) { if (visited.has(ref.text)) continue; visited.add(ref.text); if (needsComma) { referencesParagraph.appendNode( new DocPlainText({ configuration, text: ', ' }), ); } this._appendExcerptTokenWithHyperlinks(referencesParagraph, ref); needsComma = true; } output.appendNode(referencesParagraph); } } } private _writeRemarksSection(output: DocSection, apiItem: ApiItem): void { if (apiItem instanceof ApiDocumentedItem) { const tsdocComment: DocComment | undefined = apiItem.tsdocComment; if (tsdocComment) { // Write the @remarks block if (tsdocComment.remarksBlock) { output.appendNode( new DocHeading({ configuration: this._tsdocConfiguration, title: 'Remarks', }), ); this._appendSection(output, tsdocComment.remarksBlock.content); } // Write the @example blocks const exampleBlocks: DocBlock[] = tsdocComment.customBlocks.filter( x => x.blockTag.tagNameWithUpperCase === StandardTags.example.tagNameWithUpperCase, ); let exampleNumber: number = 1; for (const exampleBlock of exampleBlocks) { const heading: string = exampleBlocks.length > 1 ? `Example ${exampleNumber}` : 'Example'; output.appendNode( new DocHeading({ configuration: this._tsdocConfiguration, title: heading, }), ); this._appendSection(output, exampleBlock.content); ++exampleNumber; } } } } private _writeThrowsSection(output: DocSection, apiItem: ApiItem): void { if (apiItem instanceof ApiDocumentedItem) { const tsdocComment: DocComment | undefined = apiItem.tsdocComment; if (tsdocComment) { // Write the @throws blocks const throwsBlocks: DocBlock[] = tsdocComment.customBlocks.filter( x => x.blockTag.tagNameWithUpperCase === StandardTags.throws.tagNameWithUpperCase, ); if (throwsBlocks.length > 0) { const heading: string = 'Exceptions'; output.appendNode( new DocHeading({ configuration: this._tsdocConfiguration, title: heading, }), ); for (const throwsBlock of throwsBlocks) this._appendSection(output, throwsBlock.content); } } } } /** * GENERATE PAGE: MODEL */ private _writeModelTable(output: DocSection, apiModel: ApiModel): void { const configuration: TSDocConfiguration = this._tsdocConfiguration; const packagesTable: DocTable = new DocTable({ configuration, headerTitles: ['Package', 'Description'], }); for (const apiMember of apiModel.members) { const row: DocTableRow = new DocTableRow({ configuration }, [ this._createTitleCell(apiMember), this._createDescriptionCell(apiMember), ]); switch (apiMember.kind) { case ApiItemKind.Package: packagesTable.addRow(row); this._writeApiItemPage(apiMember); break; } } if (packagesTable.rows.length > 0) { output.appendNode( new DocHeading({ configuration: this._tsdocConfiguration, title: 'Packages', }), ); output.appendNode(packagesTable); } } /** * GENERATE PAGE: PACKAGE or NAMESPACE */ private _writePackageOrNamespaceTables( output: DocSection, apiContainer: ApiPackage | ApiNamespace, ): void { const configuration: TSDocConfiguration = this._tsdocConfiguration; const classesTable: DocTable = new DocTable({ configuration, headerTitles: ['Class', 'Description'], }); const enumerationsTable: DocTable = new DocTable({ configuration, headerTitles: ['Enumeration', 'Description'], }); const functionsTable: DocTable = new DocTable({ configuration, headerTitles: ['Function', 'Description'], }); const interfacesTable: DocTable = new DocTable({ configuration, headerTitles: ['Interface', 'Description'], }); const namespacesTable: DocTable = new DocTable({ configuration, headerTitles: ['Namespace', 'Description'], }); const variablesTable: DocTable = new DocTable({ configuration, headerTitles: ['Variable', 'Description'], }); const typeAliasesTable: DocTable = new DocTable({ configuration, headerTitles: ['Type Alias', 'Description'], }); const apiMembers: ReadonlyArray = apiContainer.kind === ApiItemKind.Package ? (apiContainer as ApiPackage).entryPoints[0].members : (apiContainer as ApiNamespace).members; for (const apiMember of apiMembers) { const name = Utilities.getConciseSignature(apiMember); // ignore types generated by deepkit if (name.startsWith('__')) continue; const row: DocTableRow = new DocTableRow({ configuration }, [ this._createTitleCell(apiMember), this._createDescriptionCell(apiMember), ]); switch (apiMember.kind) { case ApiItemKind.Class: classesTable.addRow(row); this._writeApiItemPage(apiMember); break; case ApiItemKind.Enum: enumerationsTable.addRow(row); this._writeApiItemPage(apiMember); break; case ApiItemKind.Interface: interfacesTable.addRow(row); this._writeApiItemPage(apiMember); break; case ApiItemKind.Namespace: namespacesTable.addRow(row); this._writeApiItemPage(apiMember); break; case ApiItemKind.Function: functionsTable.addRow(row); this._writeApiItemPage(apiMember); break; case ApiItemKind.TypeAlias: typeAliasesTable.addRow(row); this._writeApiItemPage(apiMember); break; case ApiItemKind.Variable: variablesTable.addRow(row); this._writeApiItemPage(apiMember); break; } } if (classesTable.rows.length > 0) { output.appendNode( new DocHeading({ configuration: this._tsdocConfiguration, title: 'Classes', }), ); output.appendNode(classesTable); } if (enumerationsTable.rows.length > 0) { output.appendNode( new DocHeading({ configuration: this._tsdocConfiguration, title: 'Enumerations', }), ); output.appendNode(enumerationsTable); } if (functionsTable.rows.length > 0) { output.appendNode( new DocHeading({ configuration: this._tsdocConfiguration, title: 'Functions', }), ); output.appendNode(functionsTable); } if (interfacesTable.rows.length > 0) { output.appendNode( new DocHeading({ configuration: this._tsdocConfiguration, title: 'Interfaces', }), ); output.appendNode(interfacesTable); } if (namespacesTable.rows.length > 0) { output.appendNode( new DocHeading({ configuration: this._tsdocConfiguration, title: 'Namespaces', }), ); output.appendNode(namespacesTable); } if (variablesTable.rows.length > 0) { output.appendNode( new DocHeading({ configuration: this._tsdocConfiguration, title: 'Variables', }), ); output.appendNode(variablesTable); } if (typeAliasesTable.rows.length > 0) { output.appendNode( new DocHeading({ configuration: this._tsdocConfiguration, title: 'Type Aliases', }), ); output.appendNode(typeAliasesTable); } } /** * GENERATE PAGE: CLASS */ private _writeClassTables(output: DocSection, apiClass: ApiClass): void { const postApiMembers: ApiItem[] = []; for (const apiMember of apiClass.members) { switch (apiMember.kind) { case ApiItemKind.Constructor: case ApiItemKind.Method: case ApiItemKind.Property: postApiMembers.push(apiMember); break; } } if (postApiMembers.length > 0) { postApiMembers.forEach(postApiMember => this._writeApiItemPage(postApiMember, output), ); } } /** * GENERATE PAGE: ENUM */ private _writeEnumTables(output: DocSection, apiEnum: ApiEnum): void { const configuration: TSDocConfiguration = this._tsdocConfiguration; const enumMembersTable: DocTable = new DocTable({ configuration, headerTitles: ['Member', 'Value', 'Description'], }); for (const apiEnumMember of apiEnum.members) { enumMembersTable.addRow( new DocTableRow({ configuration }, [ new DocTableCell({ configuration }, [ new DocParagraph({ configuration }, [ new DocPlainText({ configuration, text: Utilities.getConciseSignature(apiEnumMember), }), ]), ]), new DocTableCell({ configuration }, [ new DocParagraph({ configuration }, [ new DocCodeSpan({ configuration, code: apiEnumMember.initializerExcerpt?.text || '', }), ]), ]), this._createDescriptionCell(apiEnumMember), ]), ); } if (enumMembersTable.rows.length > 0) { output.appendNode( new DocHeading({ configuration: this._tsdocConfiguration, title: 'Enumeration Members', }), ); output.appendNode(enumMembersTable); } } /** * GENERATE PAGE: INTERFACE */ private _writeInterfaceTables( output: DocSection, apiClass: ApiInterface, ): void { const postApiMembers: ApiItem[] = []; for (const apiMember of apiClass.members) { switch (apiMember.kind) { case ApiItemKind.ConstructSignature: case ApiItemKind.MethodSignature: case ApiItemKind.PropertySignature: postApiMembers.push(apiMember); break; } } if (postApiMembers.length > 0) { postApiMembers.forEach(postApiItem => this._writeApiItemPage(postApiItem, output), ); } } /** * GENERATE PAGE: FUNCTION-LIKE */ private _writeParameterTables( output: DocSection, apiParameterListMixin: ApiParameterListMixin, ): void { const configuration: TSDocConfiguration = this._tsdocConfiguration; const parametersTable: DocTable = new DocTable({ configuration, headerTitles: ['Parameter', 'Type', 'Description'], }); for (const apiParameter of apiParameterListMixin.parameters) { const parameterDescription: DocSection = new DocSection({ configuration, }); if (apiParameter.isOptional) { parameterDescription.appendNodesInParagraph([ new DocEmphasisSpan({ configuration, italic: true }, [ new DocPlainText({ configuration, text: '(Optional)' }), ]), new DocPlainText({ configuration, text: ' ' }), ]); } if (apiParameter.tsdocParamBlock) { this._appendAndMergeSection( parameterDescription, apiParameter.tsdocParamBlock.content, ); } parametersTable.addRow( new DocTableRow({ configuration }, [ new DocTableCell({ configuration }, [ new DocParagraph({ configuration }, [ new DocPlainText({ configuration, text: apiParameter.name }), ]), ]), new DocTableCell({ configuration }, [ this._createParagraphForTypeExcerpt( apiParameter.parameterTypeExcerpt, ), ]), new DocTableCell({ configuration }, parameterDescription.nodes), ]), ); } if (parametersTable.rows.length > 0) { output.appendNode( new DocHeading({ configuration: this._tsdocConfiguration, title: 'Parameters', level: 3, }), ); output.appendNode(parametersTable); } if (ApiReturnTypeMixin.isBaseClassOf(apiParameterListMixin)) { const returnTypeExcerpt: Excerpt = apiParameterListMixin.returnTypeExcerpt; output.appendNode( new DocParagraph({ configuration }, [ new DocEmphasisSpan({ configuration, bold: true }, [ new DocPlainText({ configuration, text: 'Returns:' }), ]), ]), ); output.appendNode(this._createParagraphForTypeExcerpt(returnTypeExcerpt)); if (apiParameterListMixin instanceof ApiDocumentedItem) { if ( apiParameterListMixin.tsdocComment && apiParameterListMixin.tsdocComment.returnsBlock ) { this._appendSection( output, apiParameterListMixin.tsdocComment.returnsBlock.content, ); } } } } private _createParagraphForTypeExcerpt(excerpt: Excerpt): DocParagraph { const configuration: TSDocConfiguration = this._tsdocConfiguration; const paragraph: DocParagraph = new DocParagraph({ configuration }); if (!excerpt.text.trim()) { paragraph.appendNode( new DocPlainText({ configuration, text: '(not declared)' }), ); } else this._appendExcerptWithHyperlinks(paragraph, excerpt); return paragraph; } private _appendExcerptWithHyperlinks( docNodeContainer: DocNodeContainer, excerpt: Excerpt, ): void { for (const token of excerpt.spannedTokens) this._appendExcerptTokenWithHyperlinks(docNodeContainer, token); } private _appendExcerptTokenWithHyperlinks( docNodeContainer: DocNodeContainer, token: ExcerptToken, ): void { const configuration: TSDocConfiguration = this._tsdocConfiguration; // Markdown doesn't provide a standardized syntax for hyperlinks inside code spans, so we will render // the type expression as DocPlainText. Instead of creating multiple DocParagraphs, we can simply // discard any newlines and let the renderer do normal word-wrapping. const unwrappedTokenText: string = token.text.replace(/[\r\n]+/g, ' '); // If it's hyperlinkable, then append a DocLinkTag if (token.kind === ExcerptTokenKind.Reference && token.canonicalReference) { const apiItemResult: IResolveDeclarationReferenceResult = this._apiModel.resolveDeclarationReference( token.canonicalReference, undefined, ); if (apiItemResult.resolvedApiItem) { docNodeContainer.appendNode( new DocLinkTag({ configuration, tagName: '@link', linkText: unwrappedTokenText, urlDestination: this._getLinkFilenameForApiItem( apiItemResult.resolvedApiItem, ), }), ); return; } } // Otherwise append non-hyperlinked text docNodeContainer.appendNode( new DocPlainText({ configuration, text: unwrappedTokenText }), ); } private _createTitleCell(apiItem: ApiItem): DocTableCell { const configuration: TSDocConfiguration = this._tsdocConfiguration; let linkText: string = Utilities.getConciseSignature(apiItem); if (ApiOptionalMixin.isBaseClassOf(apiItem) && apiItem.isOptional) linkText += '?'; return new DocTableCell({ configuration }, [ new DocParagraph({ configuration }, [ new DocLinkTag({ configuration, tagName: '@link', linkText: linkText, urlDestination: this._getLinkFilenameForApiItem(apiItem), }), ]), ]); } /** * This generates a DocTableCell for an ApiItem including the summary section and "(BETA)" annotation. * * @remarks * We mostly assume that the input is an ApiDocumentedItem, but it's easier to perform this as a runtime * check than to have each caller perform a type cast. */ private _createDescriptionCell(apiItem: ApiItem): DocTableCell { const configuration: TSDocConfiguration = this._tsdocConfiguration; const section: DocSection = new DocSection({ configuration }); if (ApiReleaseTagMixin.isBaseClassOf(apiItem)) { if (apiItem.releaseTag === ReleaseTag.Beta) { section.appendNodesInParagraph([ new DocEmphasisSpan({ configuration, bold: true, italic: true }, [ new DocPlainText({ configuration, text: '(BETA)' }), ]), new DocPlainText({ configuration, text: ' ' }), ]); } } if (ApiOptionalMixin.isBaseClassOf(apiItem) && apiItem.isOptional) { section.appendNodesInParagraph([ new DocEmphasisSpan({ configuration, italic: true }, [ new DocPlainText({ configuration, text: '(Optional)' }), ]), new DocPlainText({ configuration, text: ' ' }), ]); } if (apiItem instanceof ApiDocumentedItem) { if (apiItem.tsdocComment !== undefined) { this._appendAndMergeSection( section, apiItem.tsdocComment.summarySection, ); } } return new DocTableCell({ configuration }, section.nodes); } private _writeBreadcrumb(output: DocSection, apiItem: ApiItem): void { const breadcrumbDivider = [ new DocPlainText({ configuration: this._tsdocConfiguration, text: ' > ', }), ]; for (const hierarchyItem of apiItem.getHierarchy()) { const isFirst = hierarchyItem.kind === ApiItemKind.Package; switch (hierarchyItem.kind) { case ApiItemKind.Model: case ApiItemKind.EntryPoint: // We don't show the model as part of the breadcrumb because it is the root-level container. // We don't show the entry point because today API Extractor doesn't support multiple entry points; // this may change in the future. break; default: output.appendNodesInParagraph([ ...(isFirst ? [] : breadcrumbDivider), new DocLinkTag({ configuration: this._tsdocConfiguration, tagName: '@link', linkText: hierarchyItem.displayName, urlDestination: this._getLinkFilenameForApiItem(hierarchyItem), }), ]); } } } private _writeBetaWarning(output: DocSection): void { const configuration: TSDocConfiguration = this._tsdocConfiguration; const betaWarning: string = 'This API is provided as a preview for developers and may change' + ' based on feedback that we receive. Do not use this API in a production environment.'; output.appendNode( new DocNoteBox({ configuration }, [ new DocParagraph({ configuration }, [ new DocPlainText({ configuration, text: betaWarning }), ]), ]), ); } private _appendSection(output: DocSection, docSection: DocSection): void { for (const node of docSection.nodes) output.appendNode(node); } private _appendAndMergeSection( output: DocSection, docSection: DocSection, ): void { let firstNode: boolean = true; for (const node of docSection.nodes) { if (firstNode) { // eslint-disable-next-line @typescript-eslint/no-unsafe-enum-comparison if (node.kind === DocNodeKind.Paragraph) { output.appendNodesInParagraph(node.getChildNodes()); firstNode = false; continue; } } firstNode = false; output.appendNode(node); } } private _getFilenameForApiItem( apiItem: ApiItem, nestedWhenSameName: boolean = false, ): string { if (apiItem.kind === ApiItemKind.Model) { // this file will be ignored, we don't like the old index file. return 'ignored.md'; } const apiDocumentedItem = apiItem as ApiDocumentedItem; if (apiDocumentedItem.tsdocComment) { const breadcrumb = apiDocumentedItem.tsdocComment.customBlocks.find( block => block.blockTag.tagName === '@breadcrumb', ); if (breadcrumb) { const breadcrumbContent = breadcrumb.content .getChildNodes() // eslint-disable-next-line @typescript-eslint/no-unsafe-enum-comparison .filter(block => block.kind === DocNodeKind.Paragraph) // @ts-ignore .reduce((acc, block) => [...acc, ...block.getChildNodes()], []) // @ts-ignore .filter(block => block.kind === DocNodeKind.PlainText) .map((plainText: DocPlainText) => plainText.text) .join(''); const breadcrumbs = breadcrumbContent .split('/') .map(section => section.trimLeft().trimRight()); const safeName = CustomUtilities.getSafeFilenameForNameWithCase( apiItem.displayName, ); if (breadcrumbs.length === 0) return safeName + '.md'; const filename = !nestedWhenSameName && breadcrumbs[breadcrumbs.length - 1] === safeName ? '' : safeName + '.md'; if (!filename) return breadcrumbs.join('/') + '.md'; return [...breadcrumbs, filename].join('/'); } } let baseName: string = ''; for (const hierarchyItem of apiItem.getHierarchy()) { // For overloaded methods, add a suffix such as "MyClass.myMethod_2". let qualifiedName: string = CustomUtilities.getSafeFilenameForNameWithCase( hierarchyItem.displayName, ); if (ApiParameterListMixin.isBaseClassOf(hierarchyItem)) { if (hierarchyItem.overloadIndex > 1) { // Subtract one for compatibility with earlier releases of API Documenter. // (This will get revamped when we fix GitHub issue #1308) qualifiedName += `_${hierarchyItem.overloadIndex - 1}`; } } switch (hierarchyItem.kind) { case ApiItemKind.Model: case ApiItemKind.EntryPoint: case ApiItemKind.EnumMember: case ApiItemKind.Package: break; default: baseName = baseName ? baseName + '.' + qualifiedName : qualifiedName; } } // when we don't have name, usually is the package documentation if (!baseName) return 'Introduction.md'; if (baseName.startsWith('___')) { // ignore files from deepkit return 'ignored.md'; } return baseName + '.md'; } private _getLinkFilenameForApiItem(apiItem: ApiItem): string { return ( '/docs/api/' + this._getFilenameForApiItem(apiItem) .replace(/\.md/g, '') .replace(/ /g, '%20') ); } private _deleteOldOutputFiles(): void { console.log('Deleting old output from ' + this._outputFolder); FileSystem.ensureEmptyFolder(this._outputFolder); } } ================================================ FILE: scripts/libs/CustomUtilities.ts ================================================ import { ApiItem, ApiParameterListMixin } from '@microsoft/api-extractor-model'; export class CustomUtilities { private static readonly _badFilenameCharsRegExp: RegExp = /[^a-z0-9_\-.]/gi; /** * Generates a concise signature for a function. Example: "getArea(width, height)" */ public static getConciseSignature(apiItem: ApiItem): string { if (ApiParameterListMixin.isBaseClassOf(apiItem)) { return ( apiItem.displayName + '(' + apiItem.parameters.map(x => x.name).join(', ') + ')' ); } return apiItem.displayName; } /** * Converts bad filename characters to underscores. */ public static getSafeFilenameForName(name: string): string { // TODO: This can introduce naming collisions. // We will fix that as part of https://github.com/microsoft/rushstack/issues/1308 return name .replace(CustomUtilities._badFilenameCharsRegExp, '_') .toLowerCase(); } /** * Converts bad filename characters to underscores. */ public static getSafeFilenameForNameWithCase(name: string): string { // TODO: This can introduce naming collisions. // We will fix that as part of https://github.com/microsoft/rushstack/issues/1308 return name.replace(CustomUtilities._badFilenameCharsRegExp, '_'); } } ================================================ FILE: scripts/libs/MarkdownEmitter.ts ================================================ import { CustomMarkdownEmitter } from '@microsoft/api-documenter/lib/markdown/CustomMarkdownEmitter'; import type { IMarkdownEmitterContext } from '@microsoft/api-documenter/lib/markdown/MarkdownEmitter'; import { IndentedWriter } from '@microsoft/api-documenter/lib/utils/IndentedWriter'; export class MarkdownEmitter extends CustomMarkdownEmitter { protected override writePlainText( text: string, context: IMarkdownEmitterContext, ): void { const writer: IndentedWriter = context.writer; // split out the [ leading whitespace, content, trailing whitespace ] const parts: string[] = text.match(/^(\s*)(.*?)(\s*)$/) || []; writer.write(parts[1]); // write leading whitespace const middle: string = parts[2]; if (middle !== '') { switch (writer.peekLastCharacter()) { case '': case '\n': case ' ': case '[': case '>': // okay to put a symbol break; default: // This is no problem: "**one** *two* **three**" // But this is trouble: "**one***two***three**" // The most general solution: "**one***two***three**" // but the solution above breaks docusaurus, so, I changed to space writer.write(' '); break; } writer.write(this.getEscapedText(middle)); } writer.write(parts[3]); // write trailing whitespace } } ================================================ FILE: scripts/models/apidoc.types.ts ================================================ export interface APIDoc { metadata: Metadata; kind: string; canonicalReference: string; docComment: string; name: string; members: APIDocMember[]; } export interface APIDocMember { kind: string; canonicalReference: string; name: string; members: PurpleMember[]; } export interface PurpleMember { kind: PurpleKind; canonicalReference: string; docComment: string; excerptTokens: ExcerptToken[]; releaseTag: ReleaseTag; typeParameters?: TypeParameter[]; name: string; members?: FluffyMember[]; extendsTokenRanges?: any[]; implementsTokenRanges?: TokenRange[]; typeTokenRange?: TokenRange; returnTypeTokenRange?: TokenRange; overloadIndex?: number; parameters?: Parameter[]; variableTypeTokenRange?: TokenRange; extendsTokenRange?: TokenRange; } export interface ExcerptToken { kind: ExcerptTokenKind; text: string; canonicalReference?: string; } export enum ExcerptTokenKind { Content = 'Content', Reference = 'Reference', } export interface TokenRange { startIndex: number; endIndex: number; } export enum PurpleKind { Class = 'Class', Function = 'Function', Interface = 'Interface', TypeAlias = 'TypeAlias', Variable = 'Variable', } export interface FluffyMember { kind: FluffyKind; canonicalReference: string; docComment: string; excerptTokens: ExcerptToken[]; isOptional?: boolean; returnTypeTokenRange?: TokenRange; releaseTag: ReleaseTag; overloadIndex?: number; parameters?: Parameter[]; name?: string; propertyTypeTokenRange?: TokenRange; isStatic?: boolean; typeParameters?: TypeParameter[]; } export enum FluffyKind { Constructor = 'Constructor', Method = 'Method', MethodSignature = 'MethodSignature', Property = 'Property', PropertySignature = 'PropertySignature', } export interface Parameter { parameterName: string; parameterTypeTokenRange: TokenRange; isOptional: boolean; } export enum ReleaseTag { Public = 'Public', } export interface TypeParameter { typeParameterName: TypeParameterName; constraintTokenRange: TokenRange; defaultTypeTokenRange: TokenRange; } export enum TypeParameterName { T = 'T', TApp = 'TApp', TCallback = 'TCallback', TContext = 'TContext', TEvent = 'TEvent', TResponse = 'TResponse', TReturn = 'TReturn', TStream = 'TStream', } export interface Metadata { toolPackage: string; toolVersion: string; schemaVersion: number; oldestForwardsCompatibleVersion: number; tsdocConfig: TsdocConfig; } export interface TsdocConfig { $schema: string; noStandardTags: boolean; tagDefinitions: TagDefinition[]; supportForTags: { [key: string]: boolean }; } export interface TagDefinition { tagName: string; syntaxKind: SyntaxKind; allowMultiple?: boolean; } export enum SyntaxKind { Block = 'block', Inline = 'inline', Modifier = 'modifier', } ================================================ FILE: scripts/parse-docs.ts ================================================ import { resolve } from 'path'; import { Extractor, ExtractorConfig } from '@microsoft/api-extractor'; const apiExtractConfig = resolve('./api-extractor.json'); function build(): void { const config = ExtractorConfig.loadFileAndPrepare(apiExtractConfig); const extractorResult = Extractor.invoke(config, { localBuild: true, }); if (!extractorResult.succeeded) { console.error( `API Extractor completed with ${extractorResult.errorCount} errors` + ` and ${extractorResult.warningCount} warnings`, ); process.exitCode = 1; } console.log('API Extractor completed successfully'); } build(); ================================================ FILE: src/@types/binary-settings.ts ================================================ /** * The interface representing the binary settings implementation by function * * @breadcrumb Types / BinarySettings * @public */ export interface BinarySettingsFunction { /** * This property can be a function that receives the response headers and returns whether that response should be encoded as binary. * Otherwise, you can specify not to treat any response as binary by putting `false` in this property. * * @remarks Setting this property prevents the `contentTypes` and `contentEncodings` properties from being used. */ isBinary: | ((headers: Record) => boolean) | false; } /** * The interface representing the binary settings implementation by looking inside the headers * * @breadcrumb Types / BinarySettings * @public */ export interface BinarySettingsContentHeaders { /** * The list of content types that will be treated as binary */ contentTypes: string[]; /** * The list of content encodings that will be treated as binary */ contentEncodings: string[]; } /** * The interface representing the settings for whether the response should be treated as binary or not * * @remarks Encoded as binary means the response body will be converted to base64 * * @breadcrumb Types / BinarySettings * @public */ export type BinarySettings = | BinarySettingsFunction | BinarySettingsContentHeaders; ================================================ FILE: src/@types/digital-ocean/digital-ocean-http-event.ts ================================================ //#region Imports import type { SingleValueHeaders } from '../headers'; //#endregion /** * The interface to represents the values of args send when someone calls a function using HTTP Endpoint. * To be able to receive this event, inside your `project.yml`, instead of `web: true` change to `web: 'raw'`. * * {@link https://www.digitalocean.com/community/questions/digitalocean-functions-how-to-differentiate-query-params-from-body-params | Reference} * * @public * @breadcrumb Types / Digital Ocean / DigitalOceanHttpEvent */ export interface DigitalOceanHttpEvent { /** * The HTTP Method of the request */ __ow_method: string; /** * The query porams of the request */ __ow_query: string; /** * The body of the request. */ __ow_body?: string; /** * Indicates if body is base64 string */ __ow_isBase64Encoded?: boolean; /** * The HTTP Headers of the request */ __ow_headers: SingleValueHeaders; /** * The path in the request */ __ow_path: string; } ================================================ FILE: src/@types/digital-ocean/digital-ocean-http-response.ts ================================================ //#region Imports import type { SingleValueHeaders } from '../headers'; //#endregion /** * The interface to represents the response of Digital Ocean Function. * * @public * @breadcrumb Types / Digital Ocean / DigitalOceanHttpResponse */ export interface DigitalOceanHttpResponse { /** * The HTTP Headers of the response */ headers?: SingleValueHeaders; /** * The body of the response */ body: unknown; /** * The HTTP Status code of the response */ statusCode: number; } ================================================ FILE: src/@types/digital-ocean/index.ts ================================================ export * from './digital-ocean-http-event'; export * from './digital-ocean-http-response'; ================================================ FILE: src/@types/headers.ts ================================================ /** * The record that represents the headers that doesn't have multiple values in the value * * @example * ```typescript * { 'Accept-Encoding': 'gzip, deflate, br' } * ``` * * @breadcrumb Types * @public */ export type SingleValueHeaders = Record; /** * The record that represents the headers that have multiple values in the value * * @example * ```typescript * { 'Accept-Encoding': ['gzip', 'deflate', 'br'] } * ``` * * @breadcrumb Types * @public */ export type MultiValueHeaders = Record; /** * The record that represents the headers that can both single or multiple values in the value * * @example * ```typescript * { 'Accept-Encoding': ['gzip', 'deflate', 'br'], 'Host': 'xyz.execute-api.us-east-1.amazonaws.com' } * ``` * * @breadcrumb Types * @public */ export type BothValueHeaders = Record; ================================================ FILE: src/@types/helpers.ts ================================================ /** * Removes 'optional' attributes from a type's properties * * @breadcrumb Types * @public */ export type Concrete = { [Property in keyof Type]-?: Type[Property]; }; ================================================ FILE: src/@types/huawei/huawei-api-gateway-event.ts ================================================ //#region Imports import type { BothValueHeaders } from '../index'; //#endregion /** * The interface that represents the Api Gateway Event of Huawei when integrate with Function Graph of Event Type. * See more in {@link https://support.huaweicloud.com/intl/en-us/devg-functiongraph/functiongraph_02_0102.html#functiongraph_02_0102__li5178638110137 | Reference}. * * @public * @breadcrumb Types / Huawei / HuaweiApiGatewayEvent */ export interface HuaweiApiGatewayEvent { /** * The body value with the content of this event serialized in JSON */ body: string; /** * The headers of the request which this event represents */ headers: BothValueHeaders; /** * The HTTP Method of the request which this event represents */ httpMethod: string; /** * Tells if the body is base64 encoded */ isBase64Encoded: boolean; /** * The path of the request which this event represents */ path: string; /** * The path parameters of the request which this event represents */ pathParameters: HuaweiRequestPathParameters; /** * The query strings of the request which this event represents */ queryStringParameters: HuaweiRequestQueryStringParameters; /** * The request context with information about the stage, api and requestId */ requestContext: HuaweiRequestContext; /** * It can have more properties that I could not discover yet */ [key: string]: any; } /** * The path parameters of the request, usually is the name of the wildcard you create in FunctionGraph, such as /\{proxy\}. * * @public * @breadcrumb Types / Huawei / HuaweiApiGatewayEvent */ export type HuaweiRequestPathParameters = Record; /** * The query strings of the request * * @public * @breadcrumb Types / Huawei / HuaweiApiGatewayEvent */ export type HuaweiRequestQueryStringParameters = Record< string, string | string[] >; /** * The interface that represents the values you can get inside request context. * * @public * @breadcrumb Types / Huawei / HuaweiApiGatewayEvent */ export interface HuaweiRequestContext { /** * The ID of your API inside Api Gateway */ apiId: string; /** * The ID of this request */ requestId: string; /** * The name of the stage running this Function Graph */ stage: string; } ================================================ FILE: src/@types/huawei/huawei-api-gateway-response.ts ================================================ //#region Imports import type { MultiValueHeaders } from '../headers'; //#endregion /** * The interface that represents the Api Gateway Response of Huawei when integrate with Function Graph of Event Type. * See more in {@link https://support.huaweicloud.com/intl/en-us/devg-functiongraph/functiongraph_02_0102.html#functiongraph_02_0102__li5178638110137 | Reference}. * * @public * @breadcrumb Types / Huawei / HuaweiApiGatewayResponse */ export interface HuaweiApiGatewayResponse { /** * Tells if the body was encoded as base64 */ isBase64Encoded: boolean; /** * The HTTP Status code of this response */ statusCode: number; /** * The headers sent with this response */ headers: MultiValueHeaders; /** * The body value with the content of this response serialized in JSON */ body: string; } ================================================ FILE: src/@types/huawei/huawei-context.ts ================================================ /** * The return value of {@link HuaweiContext} getRequestID * * @public * @breadcrumb Types / Huawei / HuaweiContext */ export type GetRequestIDSecondsReturn = string; /** * The return value of {@link HuaweiContext} getRemainingTimeInMilliSeconds * * @public * @breadcrumb Types / Huawei / HuaweiContext */ export type GetRemainingTimeInMilliSecondsReturn = number; /** * The return value of {@link HuaweiContext} getAccessKey * * @public * @breadcrumb Types / Huawei / HuaweiContext */ export type GetAccessKeyReturn = string; /** * The return value of {@link HuaweiContext} getSecretKey * * @public * @breadcrumb Types / Huawei / HuaweiContext */ export type GetSecretKeyReturn = string; /** * The parameters of the method {@link HuaweiContext} getUserData * * @public * @breadcrumb Types / Huawei / HuaweiContext */ export type GetUserDataKeyParameter = string; /** * The return value of {@link HuaweiContext} getUserData * * @public * @breadcrumb Types / Huawei / HuaweiContext */ export type GetUserDataReturn = any; /** * The return value of {@link HuaweiContext} getFunctionName * * @public * @breadcrumb Types / Huawei / HuaweiContext */ export type GetFunctionNameReturn = string; /** * The return value of {@link HuaweiContext} getRunningTimeInSeconds * * @public * @breadcrumb Types / Huawei / HuaweiContext */ export type GetRunningTimeInSecondsReturn = number; /** * The return value of {@link HuaweiContext} getVersion * * @public * @breadcrumb Types / Huawei / HuaweiContext */ export type GetVersionReturn = string; /** * The return value of {@link HuaweiContext} getMemorySize * * @public * @breadcrumb Types / Huawei / HuaweiContext */ export type GetMemorySizeReturn = number; /** * The return value of {@link HuaweiContext} getCPUNumber * * @public * @breadcrumb Types / Huawei / HuaweiContext */ export type GetCPUNumberReturn = number; /** * The return value of {@link HuaweiContext} getProjectID * * @public * @breadcrumb Types / Huawei / HuaweiContext */ export type GetProjectIdReturn = number; /** * The return value of {@link HuaweiContext} getPackage * * @public * @breadcrumb Types / Huawei / HuaweiContext */ export type GetPackageReturn = string; /** * The return value of {@link HuaweiContext} getToken * * @public * @breadcrumb Types / Huawei / HuaweiContext */ export type GetTokenReturn = string; /** * The return value of {@link HuaweiContext} getLogger * * Is the instance of logger that can be used to send logs to * * @public * @breadcrumb Types / Huawei / HuaweiContext */ export type GetLoggerReturn = { info(message: string): void; }; /** * The interface that represents methods sent by huawei to get information about the function graph. * See more in {@link https://support.huaweicloud.com/intl/en-us/devg-functiongraph/functiongraph_02_0410.html#section1 | Context Methods} * * @public * @breadcrumb Types / Huawei / HuaweiContext */ export interface HuaweiContext { /** * Obtains a request ID. */ getRequestID(): GetRequestIDSecondsReturn; /** * Obtains the remaining running time of a function. */ getRemainingTimeInMilliSeconds(): GetRemainingTimeInMilliSecondsReturn; /** * Obtains the AK (valid for 24 hours) of an agency. If you use this method, you need to configure an agency for the function. */ getAccessKey(): GetAccessKeyReturn; /** * Obtains the SK (valid for 24 hours) of an agency. If you use this method, you need to configure an agency for the function. */ getSecretKey(): GetSecretKeyReturn; /** * Uses keys to obtain the values passed by environment variables. * * @param key - The key to get environment variables values */ getUserData(key: GetUserDataKeyParameter): GetUserDataReturn; /** * Obtains the name of a function. */ getFunctionName(): GetFunctionNameReturn; /** * Obtains the timeout of a function. */ getRunningTimeInSeconds(): GetRunningTimeInSecondsReturn; /** * Obtains the version of a function. */ getVersion(): GetVersionReturn; /** * Obtains the allocated memory. */ getMemorySize(): GetMemorySizeReturn; /** * Number of CPU millicores used by the function (1 core = 1000 millicores). * * The value of this field is proportional to that of MemorySize. By default, 100 CPU millicores are required for 128 MB memory. The number of CPU millicores is calculated as follows: Memory/128 x 100 + 200 (basic CPU millicores). */ getCPUNumber(): GetCPUNumberReturn; /** * Obtains a project ID. */ getProjectID(): GetProjectIdReturn; /** * Obtains a function group, that is, an app. */ getPackage(): GetPackageReturn; /** * Obtains the token (valid for 24 hours) of an agency. If you use this method, you need to configure an agency for the function. */ getToken(): GetTokenReturn; /** * Obtains the logger method provided by the context and returns a log output class. Logs are output in the format of Time-Request ID-Content by using the info method. * * For example, use the info method to output logs: * * @example * ```typescript * logg = context.getLogger() * * logg.info("hello") * ``` */ getLogger(): GetLoggerReturn; } ================================================ FILE: src/@types/huawei/index.ts ================================================ export * from './huawei-context'; export * from './huawei-api-gateway-event'; export * from './huawei-api-gateway-response'; ================================================ FILE: src/@types/index.ts ================================================ export * from './binary-settings'; export * from './headers'; export * from './helpers'; ================================================ FILE: src/adapters/apollo-server/apollo-server-mutation.adapter.ts ================================================ //#region Imports import type { AdapterContract, AdapterRequest, GetResponseAdapterProps, OnErrorProps, } from '../../contracts'; import { type ILogger, getDefaultIfUndefined, getEventBodyAsBuffer, } from '../../core'; //#endregion /** * The options for {@link ApolloServerMutationAdapter} * * @breadcrumb Adapters / Apollo Server / ApolloServerMutationAdapter * @public */ export type ApolloServerMutationAdapterOptions = { /** * Specify the name of mutation that will be called when an event was received */ mutationName: string; /** * Specify the mutation result schema. * Use this to customize the behavior when you need to return a specific object to be handled by the Adapter, like SQS with Batch Mode. * * @defaultValue `{ __typename }` */ mutationResultQuery?: string; }; /** * The adapter that wraps another adapter to force a transformation of the event data as a mutation to Apollo Server be able to handle. * * @breadcrumb Adapters / Apollo Server / ApolloServerMutationAdapter * @public */ export class ApolloServerMutationAdapter implements AdapterContract { //#region Constructor /** * The default constructor */ constructor( protected readonly baseAdapter: AdapterContract< TEvent, TContext, TResponse >, protected readonly options: ApolloServerMutationAdapterOptions, ) {} //#endregion //#region Public Methods /** * {@inheritDoc} */ public canHandle(event: unknown, context: TContext, log: ILogger): boolean { return this.baseAdapter.canHandle(event, context, log); } /** * {@inheritDoc} */ public getAdapterName(): string { return this.baseAdapter.getAdapterName() + 'Mutation'; } /** * {@inheritDoc} */ public getRequest( event: TEvent, context: TContext, log: ILogger, ): AdapterRequest { const request = this.baseAdapter.getRequest(event, context, log); request.method = 'POST'; const operationName = this.options.mutationName; const mutationResultQuery = getDefaultIfUndefined( this.options.mutationResultQuery, '{ __typename }', ); const mutationBody = JSON.stringify({ operationName, query: `mutation ${operationName} ($event: String) { ${operationName} (event: $event) ${mutationResultQuery} }`, variables: { event: request.body?.toString() || '', }, }); const [buffer, contentLength] = getEventBodyAsBuffer(mutationBody, false); request.body = buffer; request.headers['content-type'] = 'application/json'; request.headers['content-length'] = String(contentLength); return request; } /** * {@inheritDoc} */ public getResponse(props: GetResponseAdapterProps): TResponse { const { data, errors } = JSON.parse(props.body); if (!errors) { return this.baseAdapter.getResponse({ ...props, body: JSON.stringify(data[this.options.mutationName]), }); } // when error happens, is the responsability of base adapter // to deal with error status code. return this.baseAdapter.getResponse(props); } /** * {@inheritDoc} */ public onErrorWhileForwarding(props: OnErrorProps): void { return this.baseAdapter.onErrorWhileForwarding(props); } //#endregion } ================================================ FILE: src/adapters/apollo-server/index.ts ================================================ export * from './apollo-server-mutation.adapter'; ================================================ FILE: src/adapters/aws/alb.adapter.ts ================================================ //#region Imports import type { ALBEvent, ALBResult, Context } from 'aws-lambda'; import type { AdapterContract, AdapterRequest, GetResponseAdapterProps, OnErrorProps, } from '../../contracts'; import { type StripBasePathFn, buildStripBasePath, getEventBodyAsBuffer, getFlattenedHeadersMap, getMultiValueHeadersMap, getPathWithQueryStringParams, } from '../../core'; //#endregion /** * The options to customize the {@link AlbAdapter} * * @breadcrumb Adapters / AWS / AlbAdapter * @public */ export interface AlbAdapterOptions { /** * Strip base path for custom domains * * @defaultValue '' */ stripBasePath?: string; } /** * The adapter to handle requests from AWS ALB * * @example * ```typescript * const stripBasePath = '/any/custom/base/path'; // default '' * const adapter = new AlbAdapter({ stripBasePath }); * ``` * * {@link https://docs.aws.amazon.com/lambda/latest/dg/services-alb.html | Event Reference} * * @breadcrumb Adapters / AWS / AlbAdapter * @public */ export class AlbAdapter implements AdapterContract { //#region Constructor /** * Default constructor * * @param options - The options to customize the {@link AlbAdapter} */ constructor(protected readonly options?: AlbAdapterOptions) { this.stripPathFn = buildStripBasePath(this.options?.stripBasePath); } //#endregion //#region Protected Properties /** * Strip base path function */ protected stripPathFn: StripBasePathFn; //#endregion //#region Public Methods /** * {@inheritDoc} */ public getAdapterName(): string { return AlbAdapter.name; } /** * {@inheritDoc} */ public canHandle(event: unknown): event is ALBEvent { const albEvent = event as Partial; return !!(albEvent?.requestContext && albEvent.requestContext.elb); } /** * {@inheritDoc} */ public getRequest(event: ALBEvent): AdapterRequest { const method = event.httpMethod; const path = this.getPathFromEvent(event); const headers = event.multiValueHeaders ? getFlattenedHeadersMap(event.multiValueHeaders, ',', true) : event.headers!; let body: Buffer | undefined; if (event.body) { const [bufferBody, contentLength] = getEventBodyAsBuffer( event.body, event.isBase64Encoded, ); body = bufferBody; headers['content-length'] = String(contentLength); } let remoteAddress = ''; // ref: https://docs.aws.amazon.com/elasticloadbalancing/latest/application/x-forwarded-headers.html#x-forwarded-for if (headers['x-forwarded-for']) remoteAddress = headers['x-forwarded-for']; return { method, headers, body, remoteAddress, path, }; } /** * {@inheritDoc} */ public getResponse({ event, headers: responseHeaders, body, isBase64Encoded, statusCode, }: GetResponseAdapterProps): ALBResult { const multiValueHeaders = !event.headers ? getMultiValueHeadersMap(responseHeaders) : undefined; const headers = event.headers ? getFlattenedHeadersMap(responseHeaders) : undefined; if (headers && headers['transfer-encoding'] === 'chunked') delete headers['transfer-encoding']; if ( multiValueHeaders && multiValueHeaders['transfer-encoding']?.includes('chunked') ) delete multiValueHeaders['transfer-encoding']; return { statusCode, body, headers, multiValueHeaders, isBase64Encoded, }; } /** * {@inheritDoc} */ public onErrorWhileForwarding({ error, delegatedResolver, respondWithErrors, event, log, }: OnErrorProps): void { const body = respondWithErrors ? error.stack || '' : ''; const errorResponse = this.getResponse({ event, statusCode: 500, body, headers: {}, isBase64Encoded: false, log, }); delegatedResolver.succeed(errorResponse); } //#endregion //#region Protected Methods /** * Get path from event with query strings * * @param event - The event sent by serverless */ protected getPathFromEvent(event: ALBEvent): string { const path = this.stripPathFn(event.path); const queryParams = event.headers ? event.queryStringParameters : event.multiValueQueryStringParameters; return getPathWithQueryStringParams(path, queryParams || {}); } //#endregion } ================================================ FILE: src/adapters/aws/api-gateway-v1.adapter.ts ================================================ //#region Imports import type { APIGatewayProxyResult, Context } from 'aws-lambda'; import type { APIGatewayProxyEvent } from 'aws-lambda/trigger/api-gateway-proxy'; import type { AdapterContract, AdapterRequest, GetResponseAdapterProps, OnErrorProps, } from '../../contracts'; import { keysToLowercase } from '../../core'; import { type StripBasePathFn, buildStripBasePath, getDefaultIfUndefined, getEventBodyAsBuffer, getMultiValueHeadersMap, getPathWithQueryStringParams, } from '../../core'; //#endregion /** * The options to customize the {@link ApiGatewayV1Adapter} * * @breadcrumb Adapters / AWS / ApiGatewayV1Adapter * @public */ export interface ApiGatewayV1Options { /** * Strip base path for custom domains * * @defaultValue '' */ stripBasePath?: string; /** * Throw an exception when you send the `transfer-encoding=chunked`, currently, API Gateway doesn't support chunked transfer. * If this is set to `false`, we will remove the `transfer-encoding` header from the response and buffer the response body * while we remove the special characters inserted by the chunked encoding. * * @remarks To learn more https://github.com/H4ad/serverless-adapter/issues/165 * @defaultValue true */ throwOnChunkedTransferEncoding?: boolean; /** * Emulates the behavior of Node.js `http` module by ensuring all request headers are lowercase. * * @defaultValue false */ lowercaseRequestHeaders?: boolean; } /** * The adapter to handle requests from AWS Api Gateway V1 * * As per {@link https://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-known-issues.html | know issues}, we throw an exception when you send the `transfer-encoding=chunked`, currently, API Gateway doesn't support chunked transfer. * * @remarks This adapter is not fully compatible with \@vendia/serverless-express, on \@vendia they filter `transfer-encoding=chunked` but we throw an exception. * * @example * ```typescript * const stripBasePath = '/any/custom/base/path'; // default '' * const adapter = new ApiGatewayV1Adapter({ stripBasePath }); * ``` * * {@link https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-develop-integrations-lambda.html | Event Reference} * * @breadcrumb Adapters / AWS / ApiGatewayV1Adapter * @public */ export class ApiGatewayV1Adapter implements AdapterContract { //#region Constructor /** * Default constructor * * @param options - The options to customize the {@link ApiGatewayV1Adapter} */ constructor(protected readonly options?: ApiGatewayV1Options) { this.stripPathFn = buildStripBasePath(this.options?.stripBasePath); } //#endregion //#region Protected Properties /** * Strip base path function */ protected stripPathFn: StripBasePathFn; //#endregion //#region Public Methods /** * {@inheritDoc} */ public getAdapterName(): string { return ApiGatewayV1Adapter.name; } /** * {@inheritDoc} */ public canHandle(event: unknown): event is APIGatewayProxyEvent { const partialEventV1 = event as Partial & { version?: '2.0'; }; return !!( partialEventV1?.requestContext && partialEventV1.version !== '2.0' && partialEventV1.headers && partialEventV1.multiValueHeaders && ((partialEventV1.queryStringParameters === null && partialEventV1.multiValueQueryStringParameters === null) || (partialEventV1.queryStringParameters && partialEventV1.multiValueQueryStringParameters)) ); } /** * {@inheritDoc} */ public getRequest(event: APIGatewayProxyEvent): AdapterRequest { const method = event.httpMethod; const headers = this.options?.lowercaseRequestHeaders ? keysToLowercase(event.headers) : { ...event.headers }; for (const multiValueHeaderKey of Object.keys( event.multiValueHeaders || {}, )) { const headerValue = event.multiValueHeaders[multiValueHeaderKey]; // event.headers by default only stick with first value if they see multiple headers // the other values will only appear on multiValueHeaderKey, in this case // we look for headers with more than 1 length which is the wrong values on event.headers // https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-develop-integrations-lambda.html if (!headerValue || headerValue?.length <= 1) continue; headers[multiValueHeaderKey] = headerValue.join(','); } const path = this.getPathFromEvent(event); let body: Buffer | undefined; if (event.body) { const [bufferBody, contentLength] = getEventBodyAsBuffer( event.body, event.isBase64Encoded, ); body = bufferBody; // eslint-disable-next-line @typescript-eslint/restrict-plus-operands headers['content-length'] = contentLength + ''; } const remoteAddress = event.requestContext.identity.sourceIp; return { method, headers, body, remoteAddress, path, }; } /** * {@inheritDoc} */ public getResponse({ headers: responseHeaders, body, isBase64Encoded, statusCode, response, }: GetResponseAdapterProps): APIGatewayProxyResult { const multiValueHeaders = getMultiValueHeadersMap(responseHeaders); const shouldThrowOnChunkedTransferEncoding = getDefaultIfUndefined( this.options?.throwOnChunkedTransferEncoding, true, ); const transferEncodingHeader = multiValueHeaders['transfer-encoding']; const hasTransferEncodingChunked = transferEncodingHeader?.some(value => value.includes('chunked'), ); if (hasTransferEncodingChunked || response?.chunkedEncoding) { if (shouldThrowOnChunkedTransferEncoding) { throw new Error( 'chunked encoding in headers is not supported by API Gateway V1', ); } else delete multiValueHeaders['transfer-encoding']; } return { statusCode, body, multiValueHeaders, isBase64Encoded, }; } /** * {@inheritDoc} */ public onErrorWhileForwarding({ error, delegatedResolver, respondWithErrors, event, log, }: OnErrorProps): void { const body = respondWithErrors ? error.stack : ''; const errorResponse = this.getResponse({ event, statusCode: 500, body: body || '', headers: {}, isBase64Encoded: false, log, }); delegatedResolver.succeed(errorResponse); } //#endregion //#region Protected Methods /** * Get path from event with query strings * * @param event - The event sent by serverless */ protected getPathFromEvent(event: APIGatewayProxyEvent): string { const path = this.stripPathFn(event.path); const queryParams = event.multiValueQueryStringParameters || {}; if (event.queryStringParameters) { for (const queryStringKey of Object.keys(event.queryStringParameters)) { const queryStringValue = event.queryStringParameters[queryStringKey]; if (queryStringValue === undefined) continue; if (!Array.isArray(queryParams[queryStringKey])) queryParams[queryStringKey] = []; if (queryParams[queryStringKey]!.includes(queryStringValue)) continue; queryParams[queryStringKey]!.push(queryStringValue); } } return getPathWithQueryStringParams(path, queryParams); } //#endregion } ================================================ FILE: src/adapters/aws/api-gateway-v2.adapter.ts ================================================ //#region Imports import type { APIGatewayProxyEventV2, Context } from 'aws-lambda'; import type { APIGatewayProxyStructuredResultV2 } from 'aws-lambda/trigger/api-gateway-proxy'; import type { AdapterContract, AdapterRequest, GetResponseAdapterProps, OnErrorProps, } from '../../contracts'; import { type StripBasePathFn, buildStripBasePath, getDefaultIfUndefined, getEventBodyAsBuffer, getFlattenedHeadersMapAndCookies, getPathWithQueryStringParams, } from '../../core'; //#endregion /** * The options to customize the {@link ApiGatewayV2Adapter} * * @breadcrumb Adapters / AWS / ApiGatewayV2Adapter * @public */ export interface ApiGatewayV2Options { /** * Strip base path for custom domains * * @defaultValue '' */ stripBasePath?: string; /** * Throw an exception when you send the `transfer-encoding=chunked`, currently, API Gateway doesn't support chunked transfer. * If this is set to `false`, we will remove the `transfer-encoding` header from the response and buffer the response body * while we remove the special characters inserted by the chunked encoding. * * @remarks To learn more https://github.com/H4ad/serverless-adapter/issues/165 * @defaultValue true */ throwOnChunkedTransferEncoding?: boolean; } /** * The adapter to handle requests from AWS Api Gateway V2 * * As per {@link https://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-known-issues.html | know issues}, we throw an exception when you send the `transfer-encoding=chunked`. * But, if you use this adapter to accept requests from Function URL, you can accept the `transfer-encoding=chunked` changing the method of invocation from `BUFFERED` to `RESPONSE_STREAM`. * * @example * ```typescript * const stripBasePath = '/any/custom/base/path'; // default '' * const adapter = new ApiGatewayV2Adapter({ stripBasePath }); * ``` * * {@link https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-develop-integrations-lambda.html | Event Reference} * * @breadcrumb Adapters / AWS / ApiGatewayV2Adapter * @public */ export class ApiGatewayV2Adapter implements AdapterContract< APIGatewayProxyEventV2, Context, APIGatewayProxyStructuredResultV2 > { //#region Constructor /** * Default constructor * * @param options - The options to customize the {@link ApiGatewayV2Adapter} */ constructor(protected readonly options?: ApiGatewayV2Options) { this.stripPathFn = buildStripBasePath(this.options?.stripBasePath); } //#endregion //#region Protected Properties /** * Strip base path function */ protected stripPathFn: StripBasePathFn; //#endregion //#region Public Methods /** * {@inheritDoc} */ public getAdapterName(): string { return ApiGatewayV2Adapter.name; } /** * {@inheritDoc} */ public canHandle(event: unknown): event is APIGatewayProxyEventV2 { const apiGatewayEvent = event as Partial & { version?: string; }; return !!( apiGatewayEvent?.requestContext && apiGatewayEvent.version === '2.0' ); } /** * {@inheritDoc} */ public getRequest(event: APIGatewayProxyEventV2): AdapterRequest { const method = event.requestContext.http.method; const path = this.getPathFromEvent(event); // accords https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-develop-integrations-lambda.html // all headers are lowercased and cannot be array // so no need to format, just a shallow copy will work here const headers = { ...event.headers }; if (event.cookies) headers.cookie = event.cookies.join('; '); let body: Buffer | undefined; if (event.body) { const [bufferBody, contentLength] = getEventBodyAsBuffer( event.body, event.isBase64Encoded, ); body = bufferBody; // eslint-disable-next-line @typescript-eslint/restrict-plus-operands headers['content-length'] = contentLength + ''; } const remoteAddress = event.requestContext.http.sourceIp; return { method, headers, body, remoteAddress, path, }; } /** * {@inheritDoc} */ public getResponse({ headers: responseHeaders, body, isBase64Encoded, statusCode, response, }: GetResponseAdapterProps): APIGatewayProxyStructuredResultV2 { const { cookies, headers } = getFlattenedHeadersMapAndCookies(responseHeaders); const shouldThrowOnChunkedTransferEncoding = getDefaultIfUndefined( this.options?.throwOnChunkedTransferEncoding, true, ); const transferEncodingHeader: string | undefined = headers['transfer-encoding']; const hasTransferEncodingChunked = transferEncodingHeader && transferEncodingHeader.includes('chunked'); if (hasTransferEncodingChunked || response?.chunkedEncoding) { if (shouldThrowOnChunkedTransferEncoding) { throw new Error( 'chunked encoding in headers is not supported by API Gateway V2', ); } else delete headers['transfer-encoding']; } return { statusCode, body, headers, isBase64Encoded, cookies, }; } /** * {@inheritDoc} */ public onErrorWhileForwarding({ error, delegatedResolver, respondWithErrors, event, log, }: OnErrorProps< APIGatewayProxyEventV2, APIGatewayProxyStructuredResultV2 >): void { const body = respondWithErrors ? error.stack : ''; const errorResponse = this.getResponse({ event, statusCode: 500, body: body || '', headers: {}, isBase64Encoded: false, log, }); delegatedResolver.succeed(errorResponse); } //#endregion //#region Protected Methods /** * Get path from event with query strings * * @param event - The event sent by serverless */ protected getPathFromEvent(event: APIGatewayProxyEventV2): string { const path = this.stripPathFn(event.rawPath); const queryParams = event.rawQueryString; return getPathWithQueryStringParams(path, queryParams || {}); } //#endregion } ================================================ FILE: src/adapters/aws/base/aws-simple-adapter.ts ================================================ //#region Imports import type { Context, SQSBatchItemFailure } from 'aws-lambda'; import type { AdapterContract, AdapterRequest, GetResponseAdapterProps, OnErrorProps, } from '../../../contracts'; import { EmptyResponse, type IEmptyResponse, getEventBodyAsBuffer, } from '../../../core'; //#endregion /** * The options to customize the {@link AwsSimpleAdapter} * * @breadcrumb Adapters / AWS / AWS Simple Adapter * @public */ export interface AWSSimpleAdapterOptions { /** * The path that will be used to create a request to be forwarded to the framework. */ forwardPath: string; /** * The http method that will be used to create a request to be forwarded to the framework. */ forwardMethod: string; /** * The AWS Service host that will be injected inside headers to developer being able to validate if request originate from the library. */ host: string; /** * Tells if this adapter should support batch item failures. */ batch?: true | false; } /** * The batch item failure response expected from the API server * * @breadcrumb Adapters / AWS / AWS Simple Adapter * @public */ export type BatchItemFailureResponse = SQSBatchItemFailure; /** * The possible options of response for {@link AwsSimpleAdapter} * * @breadcrumb Adapters / AWS / AWS Simple Adapter * @public */ export type AWSSimpleAdapterResponseType = | BatchItemFailureResponse | IEmptyResponse; /** * The abstract adapter to use to implement other simple AWS adapters * * @breadcrumb Adapters / AWS / AWS Simple Adapter * @public */ export abstract class AwsSimpleAdapter implements AdapterContract { //#region Constructor /** * Default constructor * * @param options - The options to customize the {@link AwsSimpleAdapter} */ constructor(protected readonly options: AWSSimpleAdapterOptions) {} //#endregion //#region Public Methods /** * {@inheritDoc} */ public getAdapterName(): string { throw new Error('not implemented.'); } /** * {@inheritDoc} */ public canHandle(_: unknown): _ is TEvent { throw new Error('not implemented.'); } /** * {@inheritDoc} */ public getRequest(event: TEvent): AdapterRequest { const path = this.options.forwardPath; const method = this.options.forwardMethod; const [body, contentLength] = getEventBodyAsBuffer( JSON.stringify(event), false, ); const headers = { host: this.options.host, 'content-type': 'application/json', 'content-length': String(contentLength), }; return { method, headers, body, path, }; } /** * {@inheritDoc} */ public getResponse({ body, headers, isBase64Encoded, event, statusCode, }: GetResponseAdapterProps): AWSSimpleAdapterResponseType { if (this.hasInvalidStatusCode(statusCode)) { throw new Error( JSON.stringify({ body, headers, isBase64Encoded, event, statusCode }), ); } if (!this.options.batch) return EmptyResponse; if (isBase64Encoded) { throw new Error( 'SERVERLESS_ADAPTER: The response could not be base64 encoded when you set batch: true, the response should be a JSON.', ); } if (!body) return EmptyResponse; return JSON.parse(body); } /** * {@inheritDoc} */ public onErrorWhileForwarding({ error, delegatedResolver, }: OnErrorProps): void { delegatedResolver.fail(error); } //#endregion //#region Protected Methods /** * Check if the status code is invalid * * @param statusCode - The status code */ protected hasInvalidStatusCode(statusCode: number): boolean { return statusCode < 200 || statusCode >= 400; } //#endregion } ================================================ FILE: src/adapters/aws/base/index.ts ================================================ export * from './aws-simple-adapter'; ================================================ FILE: src/adapters/aws/dynamodb.adapter.ts ================================================ //#region Imports import type { DynamoDBStreamEvent } from 'aws-lambda'; import { getDefaultIfUndefined } from '../../core'; import { type AWSSimpleAdapterOptions, AwsSimpleAdapter } from './base/index'; //#endregion /** * The options to customize the {@link DynamoDBAdapter} * * @breadcrumb Adapters / AWS / DynamoDBAdapter * @public */ export interface DynamoDBAdapterOptions extends Pick { /** * The path that will be used to create a request to be forwarded to the framework. * * @defaultValue /dynamo */ dynamoDBForwardPath?: string; /** * The http method that will be used to create a request to be forwarded to the framework. * * @defaultValue POST */ dynamoDBForwardMethod?: string; } /** * The adapter to handle requests from AWS DynamoDB. * * The option of `responseWithErrors` is ignored by this adapter and we always call `resolver.fail` with the error. * * {@link https://docs.aws.amazon.com/lambda/latest/dg/with-ddb.html | Event Reference} * * @example * ```typescript * const dynamoDBForwardPath = '/your/route/dynamo'; // default /dynamo * const dynamoDBForwardMethod = 'POST'; // default POST * const adapter = new DynamoDBAdapter({ dynamoDBForwardPath, dynamoDBForwardMethod }); * ``` * * @breadcrumb Adapters / AWS / DynamoDBAdapter * @public */ export class DynamoDBAdapter extends AwsSimpleAdapter { //#region Constructor /** * Default constructor * * @param options - The options to customize the {@link DynamoDBAdapter} */ constructor(options?: DynamoDBAdapterOptions) { super({ forwardPath: getDefaultIfUndefined( options?.dynamoDBForwardPath, '/dynamo', ), forwardMethod: getDefaultIfUndefined( options?.dynamoDBForwardMethod, 'POST', ), batch: options?.batch, host: 'dynamodb.amazonaws.com', }); } //#endregion //#region Public Methods /** * {@inheritDoc} */ public override getAdapterName(): string { return DynamoDBAdapter.name; } /** * {@inheritDoc} */ public override canHandle(event: unknown): event is DynamoDBStreamEvent { const dynamoDBevent = event as Partial; if (!Array.isArray(dynamoDBevent?.Records)) return false; const eventSource = dynamoDBevent.Records[0]?.eventSource; return eventSource === 'aws:dynamodb'; } //#endregion } ================================================ FILE: src/adapters/aws/event-bridge.adapter.ts ================================================ //#region Imports import type { EventBridgeEvent } from 'aws-lambda'; import { getDefaultIfUndefined } from '../../core'; import { AwsSimpleAdapter } from './base'; //#endregion /** * The options to customize the {@link EventBridgeAdapter} * * @breadcrumb Adapters / AWS / EventBridgeAdapter * @public */ export interface EventBridgeOptions { /** * The path that will be used to create a request to be forwarded to the framework. * * @defaultValue /eventbridge */ eventBridgeForwardPath?: string; /** * The http method that will be used to create a request to be forwarded to the framework. * * @defaultValue POST */ eventBridgeForwardMethod?: string; } /** * Just a type alias to ignore generic types in the event * * @breadcrumb Adapters / AWS / EventBridgeAdapter * @public */ export type EventBridgeEventAll = EventBridgeEvent; /** * The adapter to handle requests from AWS EventBridge (Cloudwatch Events). * * The option of `responseWithErrors` is ignored by this adapter and we always call `resolver.fail` with the error. * * {@link https://docs.aws.amazon.com/lambda/latest/dg/services-cloudwatchevents.html | Event Reference} * * @example * ```typescript * const eventBridgeForwardPath = '/your/route/eventbridge'; // default /eventbridge * const eventBridgeForwardMethod = 'POST'; // default POST * const adapter = new EventBridgeAdapter({ eventBridgeForwardPath, eventBridgeForwardMethod }); * ``` * * @breadcrumb Adapters / AWS / EventBridgeAdapter * @public */ export class EventBridgeAdapter extends AwsSimpleAdapter { //#region Constructor /** * Default constructor * * @param options - The options to customize the {@link EventBridgeAdapter} */ constructor(options?: EventBridgeOptions) { super({ forwardPath: getDefaultIfUndefined( options?.eventBridgeForwardPath, '/eventbridge', ), forwardMethod: getDefaultIfUndefined( options?.eventBridgeForwardMethod, 'POST', ), batch: false, host: 'events.amazonaws.com', }); } //#endregion //#region Public Methods /** * {@inheritDoc} */ public override getAdapterName(): string { return EventBridgeAdapter.name; } /** * {@inheritDoc} */ public override canHandle(event: unknown): event is EventBridgeEventAll { const eventBridgeEvent = event as Partial; // thanks to @cnuss in https://github.com/vendia/serverless-express/blob/b5da6070b8dd2fb674c1f7035dd7edfef1dc83a2/src/event-sources/utils.js#L87 return !!( eventBridgeEvent && eventBridgeEvent.version && eventBridgeEvent.version === '0' && eventBridgeEvent.id && eventBridgeEvent['detail-type'] && eventBridgeEvent.source && eventBridgeEvent.account && eventBridgeEvent.time && eventBridgeEvent.region && eventBridgeEvent.resources && Array.isArray(eventBridgeEvent.resources) && eventBridgeEvent.detail && typeof eventBridgeEvent.detail === 'object' && !Array.isArray(eventBridgeEvent.detail) ); } //#endregion } ================================================ FILE: src/adapters/aws/index.ts ================================================ export * from './alb.adapter'; export * from './api-gateway-v1.adapter'; export * from './api-gateway-v2.adapter'; export * from './dynamodb.adapter'; export * from './event-bridge.adapter'; export * from './lambda-edge.adapter'; export * from './s3.adapter'; export * from './sns.adapter'; export * from './sqs.adapter'; export * from './base'; export * from './request-lambda-edge.adapter'; ================================================ FILE: src/adapters/aws/lambda-edge.adapter.ts ================================================ //#region Imports import type { CloudFrontRequest, Context } from 'aws-lambda'; import type { CloudFrontEvent, CloudFrontHeaders, CloudFrontResultResponse, } from 'aws-lambda/common/cloudfront'; import type { CloudFrontRequestEvent, CloudFrontRequestResult, } from 'aws-lambda/trigger/cloudfront-request'; import type { BothValueHeaders, Concrete, SingleValueHeaders, } from '../../@types'; import type { AdapterContract, AdapterRequest, GetResponseAdapterProps, OnErrorProps, } from '../../contracts'; import { getDefaultIfUndefined, getEventBodyAsBuffer, getPathWithQueryStringParams, } from '../../core'; //#endregion /** * The type alias to indicate where we get the default value of query string to create the request. * * @breadcrumb Adapters / AWS / LambdaEdgeAdapter * @public */ export type DefaultQueryString = CloudFrontRequestEvent['Records'][number]['cf']['request']['querystring']; /** * The type alias to indicate where we get the default value of path to create the request. * * @breadcrumb Adapters / AWS / LambdaEdgeAdapter * @public */ export type DefaultForwardPath = CloudFrontRequestEvent['Records'][number]['cf']['request']['uri']; /** * Represents the body of the new version of Lambda\@edge, which uses the `body` property inside `request` as the body (library) of the request. * * @breadcrumb Adapters / AWS / LambdaEdgeAdapter * @public */ export type NewLambdaEdgeBody = CloudFrontRequestEvent['Records'][number]['cf']['request']['body']; /** * Represents the body of the old version of Lambda\@edge supported by \@vendia/serverless-express which returns the `data` property within `body` for the body (library) of the request. * * @breadcrumb Adapters / AWS / LambdaEdgeAdapter * @public */ export type OldLambdaEdgeBody = Concrete< CloudFrontRequestEvent['Records'][number]['cf']['request'] >['body']['data']; /** * The list was created based on {@link https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/edge-functions-restrictions.html | these docs} in the "Disallowed Headers" section. * * @breadcrumb Adapters / AWS / LambdaEdgeAdapter / Constants * @public */ export const DEFAULT_LAMBDA_EDGE_DISALLOWED_HEADERS: (string | RegExp)[] = [ 'Connection', 'Expect', 'Keep-Alive', 'Proxy-Authenticate', 'Proxy-Authorization', 'Proxy-Connection', 'Trailer', 'Upgrade', 'X-Accel-Buffering', 'X-Accel-Charset', 'X-Accel-Limit-Rate', 'X-Accel-Redirect', /(X-Amz-Cf-)(.*)/gim, 'X-Cache', /(X-Edge-)(.*)/gim, 'X-Forwarded-Proto', 'X-Real-IP', ]; /** * The default max response size in bytes of viewer request and viewer response. * * @defaultValue 1024 * 40 = 40960 = 40KB * * {@link https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/cloudfront-limits.html | Reference} * * @breadcrumb Adapters / AWS / LambdaEdgeAdapter / Constants * @public */ export const DEFAULT_VIEWER_MAX_RESPONSE_SIZE_IN_BYTES = 1024 * 40; /** * The default max response size in bytes of origin request and origin response. * * @defaultValue 1024 * 1024 = 1048576 = 1MB * * {@link https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/cloudfront-limits.html | Reference} * * @breadcrumb Adapters / AWS / LambdaEdgeAdapter / Constants * @public */ export const DEFAULT_ORIGIN_MAX_RESPONSE_SIZE_IN_BYTES = 1024 * 1024; /** * The options to customize the {@link LambdaEdgeAdapter}. * * @breadcrumb Adapters / AWS / LambdaEdgeAdapter * @public */ export interface LambdaEdgeAdapterOptions { /** * The max response size in bytes of viewer request and viewer response. * * {@link https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/cloudfront-limits.html | Reference} * * @defaultValue {@link DEFAULT_VIEWER_MAX_RESPONSE_SIZE_IN_BYTES} */ viewerMaxResponseSizeInBytes?: number; /** * The max response size in bytes of origin request and origin response. * * {@link https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/cloudfront-limits.html | Reference} * * @defaultValue {@link DEFAULT_ORIGIN_MAX_RESPONSE_SIZE_IN_BYTES} */ originMaxResponseSizeInBytes?: number; /** * The function called when the response size exceed the max limits of the Lambda\@edge * * @param response - The response from framework that exceed the limit of Lambda\@edge * @defaultValue undefined */ onResponseSizeExceedLimit?: ( response: CloudFrontRequestResult, ) => CloudFrontRequestResult; /** * Return the path to be used to create a request to the framework * * @remarks You MUST append the query params from {@link DefaultQueryString}, you can use the helper {@link getPathWithQueryStringParams}. * * @param event - The event sent by the serverless * @defaultValue The value from {@link DefaultForwardPath} */ getPathFromEvent?: ( event: CloudFrontRequestEvent['Records'][number], ) => string; /** * The headers that will be stripped from the headers object because Lambda\@edge will fail if these headers are passed in the response. * * @remarks All headers will be compared with other headers using toLowerCase, but for the RegExp, if you modify this list, you must put the flag `/gmi` at the end of the RegExp (ex: `/(X-Amz-Cf-)(.*)/gim`) * * @defaultValue To get the full list, see {@link DEFAULT_LAMBDA_EDGE_DISALLOWED_HEADERS}. */ disallowedHeaders?: (string | RegExp)[]; /** * If you want to change how we check against the header if it should be stripped, you can pass a function to this property. * * @param header - The header of the response * @defaultValue The default method is implemented to test the header against the list {@link LambdaEdgeAdapterOptions.disallowedHeaders}. */ shouldStripHeader?: (header: string) => boolean; /** * By default, the {@link aws-lambda#CloudFrontRequestResult} has the `headers` property, but we also have the headers sent by the framework too. * So this setting tells us how to handle this case, if you pass `true` to this property, we will use the framework headers. * Otherwise, we will forward the body back to cloudfront without modifying or trying to set the `headers` property inside {@link aws-lambda#CloudFrontRequestResult}. * * @defaultValue false */ shouldUseHeadersFromFramework?: boolean; } /** * The adapter to handle requests from AWS Lambda\@Edge. * * This adapter is not fully compatible with Lambda\@edge supported by \@vendia/serverless-express, the request body was modified to return {@link NewLambdaEdgeBody} instead {@link OldLambdaEdgeBody}. * Also, the response has been modified to return entire body sent by the framework, in this form you MUST return the body from the framework in the format of {@link aws-lambda#CloudFrontRequestResult}. * And when we get an error during the forwarding to the framework, we call `resolver.fail` instead of trying to return status 500 like the old implementation was. * * {@link https://docs.aws.amazon.com/lambda/latest/dg/lambda-edge.html | Lambda edge docs} * {@link https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/lambda-event-structure.html | Event Reference} * * @example * ```typescript * const getPathFromEvent = () => '/lambda/edge'; // will forward all requests to the same endpoint * const adapter = new LambdaEdgeAdapter({ getPathFromEvent }); * ``` * * @breadcrumb Adapters / AWS / LambdaEdgeAdapter * @public */ export class LambdaEdgeAdapter implements AdapterContract { //#region Constructor /** * Default constructor * * @param options - The options to customize the {@link LambdaEdgeAdapter} */ constructor(protected readonly options?: LambdaEdgeAdapterOptions) { const disallowedHeaders = getDefaultIfUndefined( this.options?.disallowedHeaders, DEFAULT_LAMBDA_EDGE_DISALLOWED_HEADERS, ); this.cachedDisallowedHeaders = disallowedHeaders.map(disallowedHeader => { if (disallowedHeader instanceof RegExp) return disallowedHeader; return new RegExp(`(${disallowedHeader})`, 'gim'); }); } //#endregion //#region Protected Properties /** * This property is used to cache the disallowed headers in `RegExp` version, even if you provide a string in `disallowedHeader`, we will cache it in an instance of `RegExp`. */ protected readonly cachedDisallowedHeaders: RegExp[]; //#endregion //#region Public Methods /** * {@inheritDoc} */ public getAdapterName(): string { return LambdaEdgeAdapter.name; } /** * {@inheritDoc} */ public canHandle(event: unknown): event is CloudFrontRequestEvent { const lambdaEdgeEvent = event as Partial; if (!Array.isArray(lambdaEdgeEvent?.Records)) return false; const eventType = lambdaEdgeEvent.Records[0]?.cf?.config?.eventType; const validEventTypes: CloudFrontEvent['config']['eventType'][] = [ 'origin-response', 'origin-request', 'viewer-response', 'viewer-request', ]; return validEventTypes.includes(eventType); } /** * {@inheritDoc} */ public getRequest(event: CloudFrontRequestEvent): AdapterRequest { const request = event.Records[0]; const cloudFrontRequest = request.cf.request; const method = cloudFrontRequest.method; const pathFromOptions = this.options?.getPathFromEvent ? this.options.getPathFromEvent(request) : undefined; const defaultPath = getPathWithQueryStringParams( cloudFrontRequest.uri, cloudFrontRequest.querystring, ); const path = getDefaultIfUndefined(pathFromOptions, defaultPath); const remoteAddress = cloudFrontRequest.clientIp; const headers = this.getFlattenedHeadersFromCloudfrontRequest(cloudFrontRequest); let body: Buffer | undefined; if (cloudFrontRequest.body) { const [buffer, contentLength] = getEventBodyAsBuffer( JSON.stringify(cloudFrontRequest.body), false, ); body = buffer; headers['content-length'] = contentLength.toString(); } const { host } = headers; return { method, path, headers, body, remoteAddress, host, hostname: host, }; } /** * {@inheritDoc} */ public getResponse( props: GetResponseAdapterProps, ): CloudFrontRequestResult { const response = this.getResponseToLambdaEdge(props); const responseToServiceBytes = new TextEncoder().encode( JSON.stringify(response), ).length; const isOriginRequestOrResponse = this.isEventTypeOrigin( props.event.Records[0].cf.config, ); const maxSizeInBytes = isOriginRequestOrResponse ? getDefaultIfUndefined( this.options?.originMaxResponseSizeInBytes, DEFAULT_ORIGIN_MAX_RESPONSE_SIZE_IN_BYTES, ) : getDefaultIfUndefined( this.options?.viewerMaxResponseSizeInBytes, DEFAULT_VIEWER_MAX_RESPONSE_SIZE_IN_BYTES, ); if (responseToServiceBytes <= maxSizeInBytes) return response; if (this.options?.onResponseSizeExceedLimit) this.options.onResponseSizeExceedLimit(response); else { props.log.error( `SERVERLESS_ADAPTER:LAMBDA_EDGE_ADAPTER: Max response size exceeded: ${responseToServiceBytes} of the max of ${maxSizeInBytes}.`, ); } return response; } /** * {@inheritDoc} */ public onErrorWhileForwarding({ error, delegatedResolver, }: OnErrorProps): void { delegatedResolver.fail(error); } //#endregion //#region Protected Methods /** * Returns the headers with the flattened (non-list) values of the cloudfront request headers * * @param cloudFrontRequest - The cloudfront request */ protected getFlattenedHeadersFromCloudfrontRequest( cloudFrontRequest: CloudFrontRequest, ): SingleValueHeaders { return Object.keys(cloudFrontRequest.headers).reduce((acc, headerKey) => { const headerValue = cloudFrontRequest.headers[headerKey]; acc[headerKey] = headerValue.map(header => header.value).join(','); return acc; }, {} as SingleValueHeaders); } /** * Returns the framework response in the format required by the Lambda\@edge. * * @param body - The body of the response * @param frameworkHeaders - The headers from the framework */ protected getResponseToLambdaEdge({ body, headers: frameworkHeaders, }: GetResponseAdapterProps): CloudFrontRequestResult { const shouldUseHeadersFromFramework = getDefaultIfUndefined( this.options?.shouldUseHeadersFromFramework, false, ); const parsedBody: CloudFrontResultResponse | CloudFrontRequest = JSON.parse(body); if (parsedBody.headers) { parsedBody.headers = Object.keys(parsedBody.headers).reduce( (acc, header) => { if (this.shouldStripHeader(header)) return acc; acc[header] = parsedBody.headers![header]; return acc; }, {} as CloudFrontHeaders, ); } if (!shouldUseHeadersFromFramework) return parsedBody; parsedBody.headers = this.getHeadersForCloudfrontResponse(frameworkHeaders); return parsedBody; } /** * Returns headers in Cloudfront Response format. * * @param originalHeaders - The original version of the request sent by the framework */ protected getHeadersForCloudfrontResponse( originalHeaders: BothValueHeaders, ): CloudFrontHeaders { return Object.keys(originalHeaders).reduce((acc, headerKey) => { if (this.shouldStripHeader(headerKey)) return acc; if (!acc[headerKey]) acc[headerKey] = []; const headerValue = originalHeaders[headerKey]; if (!Array.isArray(headerValue)) { acc[headerKey].push({ key: headerKey, value: headerValue || '', }); return acc; } const headersArray = headerValue.map(value => ({ key: headerKey, value: value, })); acc[headerKey].push(...headersArray); return acc; }, {} as CloudFrontHeaders); } /** * Returns the information if we should remove the response header * * @param headerKey - The header that will be tested */ protected shouldStripHeader(headerKey: string): boolean { if (this.options?.shouldStripHeader) return this.options.shouldStripHeader(headerKey); const headerKeyLowerCase = headerKey.toLowerCase(); for (const stripHeaderIf of this.cachedDisallowedHeaders) { if (!stripHeaderIf.test(headerKeyLowerCase)) continue; return true; } return false; } /** * Determines whether the event is from origin or is from viewer. * * @param content - The event sent by AWS or the response sent by the framework */ protected isEventTypeOrigin(content: CloudFrontEvent['config']): boolean { return content.eventType.includes('origin'); } //#endregion } ================================================ FILE: src/adapters/aws/request-lambda-edge.adapter.ts ================================================ //#region Imports import type { CloudFrontRequest, Context } from 'aws-lambda'; import type { CloudFrontHeaders, CloudFrontResultResponse, } from 'aws-lambda/common/cloudfront'; import type { CloudFrontRequestEvent, CloudFrontRequestResult, } from 'aws-lambda/trigger/cloudfront-request'; import type { BothValueHeaders, SingleValueHeaders } from '../../@types'; import type { AdapterContract, AdapterRequest, GetResponseAdapterProps, OnErrorProps, } from '../../contracts'; import { type StripBasePathFn, buildStripBasePath, getDefaultIfUndefined, getEventBodyAsBuffer, getPathWithQueryStringParams, } from '../../core'; import { DEFAULT_LAMBDA_EDGE_DISALLOWED_HEADERS, DEFAULT_ORIGIN_MAX_RESPONSE_SIZE_IN_BYTES, DEFAULT_VIEWER_MAX_RESPONSE_SIZE_IN_BYTES, } from './lambda-edge.adapter'; //#endregion //#endregion /** * The options to customize the {@link RequestLambdaEdgeAdapter}. * * @breadcrumb Adapters / AWS / RequestLambdaEdgeAdapter * @public */ export interface RequestLambdaEdgeAdapterOptions { /** * Strip base path for custom paths, like `/api`. * * @defaultValue '' */ stripBasePath?: string; /** * The max response size in bytes of viewer request and viewer response. * * {@link https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/cloudfront-limits.html | Reference} * * @defaultValue {@link DEFAULT_VIEWER_MAX_RESPONSE_SIZE_IN_BYTES} */ viewerMaxResponseSizeInBytes?: number; /** * The max response size in bytes of origin request and origin response. * * {@link https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/cloudfront-limits.html | Reference} * * @defaultValue {@link DEFAULT_ORIGIN_MAX_RESPONSE_SIZE_IN_BYTES} */ originMaxResponseSizeInBytes?: number; /** * The function called when the response size exceed the max limits of the Lambda\@edge * * @param response - The response from framework that exceed the limit of Lambda\@edge * @defaultValue undefined */ onResponseSizeExceedLimit?: ( response: CloudFrontRequestResult, ) => CloudFrontRequestResult; /** * The headers that will be stripped from the headers object because Lambda\@edge will fail if these headers are passed in the response. * * @remarks All headers will be compared with other headers using toLowerCase, but for the RegExp, if you modify this list, you must put the flag `/gmi` at the end of the RegExp (ex: `/(X-Amz-Cf-)(.*)/gim`) * * @defaultValue To get the full list, see {@link DEFAULT_LAMBDA_EDGE_DISALLOWED_HEADERS}. */ disallowedHeaders?: (string | RegExp)[]; /** * If you want to change how we check against the header if it should be stripped, you can pass a function to this property. * * @param header - The header of the response * @defaultValue The default method is implemented to test the header against the list {@link RequestLambdaEdgeAdapterOptions.disallowedHeaders}. */ shouldStripHeader?: (header: string) => boolean; } /** * The adapter to handle requests from AWS Lambda\@Edge of the type Viewer Request. * * The idea of this Adapter is to you be able to expose your framework to the Edge, like when you build for Cloudfront. * * {@link https://docs.aws.amazon.com/lambda/latest/dg/lambda-edge.html | Lambda edge docs} * {@link https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/lambda-event-structure.html | Event Reference} * * @example * ```typescript * const stripBasePath = '/api'; // in case you have configure the cloudfront to forward the path /api to your lambda * const adapter = new RequestLambdaEdgeAdapter({ stripBasePath }); * ``` * * @breadcrumb Adapters / AWS / RequestLambdaEdgeAdapter * @public */ export class RequestLambdaEdgeAdapter implements AdapterContract { //#region Constructor /** * Default constructor * * @param options - The options to customize the {@link RequestLambdaEdgeAdapter} */ constructor(protected readonly options?: RequestLambdaEdgeAdapterOptions) { this.stripPathFn = buildStripBasePath(this.options?.stripBasePath); const disallowedHeaders = getDefaultIfUndefined( this.options?.disallowedHeaders, DEFAULT_LAMBDA_EDGE_DISALLOWED_HEADERS, ); this.cachedDisallowedHeaders = disallowedHeaders.map(disallowedHeader => { if (disallowedHeader instanceof RegExp) return disallowedHeader; return new RegExp(`(${disallowedHeader})`, 'gim'); }); } //#endregion //#region Protected Properties /** * Strip base path function */ protected readonly stripPathFn: StripBasePathFn; /** * This property is used to cache the disallowed headers in `RegExp` version, even if you provide a string in `disallowedHeader`, we will cache it in an instance of `RegExp`. */ protected readonly cachedDisallowedHeaders: RegExp[]; //#endregion //#region Public Methods /** * {@inheritDoc} */ public getAdapterName(): string { return RequestLambdaEdgeAdapter.name; } /** * {@inheritDoc} */ public canHandle(event: unknown): event is CloudFrontRequestEvent { const lambdaEdgeEvent = event as Partial; if (!Array.isArray(lambdaEdgeEvent?.Records)) return false; const eventType = lambdaEdgeEvent.Records[0]?.cf?.config?.eventType; return eventType === 'viewer-request' || eventType === 'origin-request'; } /** * {@inheritDoc} */ public getRequest(event: CloudFrontRequestEvent): AdapterRequest { const request = event.Records[0]; const cloudFrontRequest = request.cf.request; const method = cloudFrontRequest.method; const path = this.stripPathFn( getPathWithQueryStringParams( cloudFrontRequest.uri, cloudFrontRequest.querystring, ), ); const remoteAddress = cloudFrontRequest.clientIp; const headers = this.getFlattenedHeadersFromCloudfrontRequest(cloudFrontRequest); let body: Buffer | undefined; if (cloudFrontRequest.body) { const [buffer, contentLength] = getEventBodyAsBuffer( cloudFrontRequest.body.data, cloudFrontRequest.body.encoding === 'base64', ); body = buffer; headers['content-length'] = contentLength.toString(); } const { host } = headers; return { method, path, headers, body, remoteAddress, host, hostname: host, }; } /** * {@inheritDoc} */ public getResponse({ body, headers: frameworkHeaders, isBase64Encoded, statusCode, log, event, }: GetResponseAdapterProps): CloudFrontResultResponse { const headers = this.getHeadersForCloudfrontResponse(frameworkHeaders); const maxSizeInBytes = event.Records[0].cf.config.eventType === 'origin-request' ? getDefaultIfUndefined( this.options?.originMaxResponseSizeInBytes, DEFAULT_ORIGIN_MAX_RESPONSE_SIZE_IN_BYTES, ) : getDefaultIfUndefined( this.options?.viewerMaxResponseSizeInBytes, DEFAULT_VIEWER_MAX_RESPONSE_SIZE_IN_BYTES, ); const response: CloudFrontResultResponse = { body, status: statusCode.toString(), bodyEncoding: isBase64Encoded ? 'base64' : 'text', headers, }; // probably is not correctly accurate, but it's a good approximation const bodyLength = body.length; if (bodyLength <= maxSizeInBytes) return response; if (this.options?.onResponseSizeExceedLimit) this.options.onResponseSizeExceedLimit(response); else { log.error( `SERVERLESS_ADAPTER:LAMBDA_EDGE_ADAPTER: Max response size exceeded: ${bodyLength} of the max of ${maxSizeInBytes}.`, ); } return response; } /** * {@inheritDoc} */ public onErrorWhileForwarding({ error, delegatedResolver, respondWithErrors, log, event, }: OnErrorProps): void { const body = respondWithErrors ? error.stack : ''; const errorResponse = this.getResponse({ event, statusCode: 500, body: body || '', headers: {}, isBase64Encoded: false, log, }); delegatedResolver.succeed(errorResponse); } //#endregion //#region Protected Methods /** * Returns the headers with the flattened (non-list) values of the cloudfront request headers * * @param cloudFrontRequest - The cloudfront request */ protected getFlattenedHeadersFromCloudfrontRequest( cloudFrontRequest: CloudFrontRequest, ): SingleValueHeaders { return Object.keys(cloudFrontRequest.headers).reduce((acc, headerKey) => { const headerValue = cloudFrontRequest.headers[headerKey]; if (headerValue.length === 1) acc[headerKey] = headerValue[0].value; else acc[headerKey] = headerValue.map(header => header.value).join(','); return acc; }, {} as SingleValueHeaders); } /** * Returns headers in Cloudfront Response format. * * @param originalHeaders - The original version of the request sent by the framework */ protected getHeadersForCloudfrontResponse( originalHeaders: BothValueHeaders, ): CloudFrontHeaders { return Object.keys(originalHeaders).reduce((acc, headerKey) => { if (this.shouldStripHeader(headerKey)) return acc; const lowercaseHeaderKey = headerKey.toLowerCase(); if (!acc[lowercaseHeaderKey]) acc[lowercaseHeaderKey] = []; const headerValue = originalHeaders[headerKey]; if (!Array.isArray(headerValue)) { acc[lowercaseHeaderKey].push({ key: headerKey, value: headerValue || '', }); return acc; } const headersArray = headerValue.map(value => ({ key: headerKey, value: value, })); acc[lowercaseHeaderKey].push(...headersArray); return acc; }, {} as CloudFrontHeaders); } /** * Returns the information if we should remove the response header * * @param headerKey - The header that will be tested */ protected shouldStripHeader(headerKey: string): boolean { if (this.options?.shouldStripHeader) return this.options.shouldStripHeader(headerKey); const headerKeyLowerCase = headerKey.toLowerCase(); for (const stripHeaderIf of this.cachedDisallowedHeaders) { if (!stripHeaderIf.test(headerKeyLowerCase)) continue; return true; } return false; } //#endregion } ================================================ FILE: src/adapters/aws/s3.adapter.ts ================================================ //#region Imports import type { S3Event } from 'aws-lambda'; import { getDefaultIfUndefined } from '../../core'; import { AwsSimpleAdapter } from './base/index'; //#endregion /** * The options to customize the {@link S3Adapter} * * @breadcrumb Adapters / AWS / S3Adapter * @public */ export interface S3AdapterOptions { /** * The path that will be used to create a request to be forwarded to the framework. * * @defaultValue /s3 */ s3ForwardPath?: string; /** * The http method that will be used to create a request to be forwarded to the framework. * * @defaultValue POST */ s3ForwardMethod?: string; } /** * The adapter to handle requests from AWS S3. * * The option of `responseWithErrors` is ignored by this adapter and we always call `resolver.fail` with the error. * * {@link https://docs.aws.amazon.com/lambda/latest/dg/with-s3.html | Event Reference} * * @example * ```typescript * const s3ForwardPath = '/your/route/s3'; // default /s3 * const s3ForwardMethod = 'POST'; // default POST * const adapter = new S3Adapter({ s3ForwardPath, s3ForwardMethod }); * ``` * * @breadcrumb Adapters / AWS / S3Adapter * @public */ export class S3Adapter extends AwsSimpleAdapter { //#region Constructor /** * Default constructor * * @param options - The options to customize the {@link SNSAdapter} */ constructor(options?: S3AdapterOptions) { super({ forwardPath: getDefaultIfUndefined(options?.s3ForwardPath, '/s3'), forwardMethod: getDefaultIfUndefined(options?.s3ForwardMethod, 'POST'), batch: false, host: 's3.amazonaws.com', }); } //#endregion //#region Public Methods /** * {@inheritDoc} */ public override getAdapterName(): string { return S3Adapter.name; } /** * {@inheritDoc} */ public override canHandle(event: unknown): event is S3Event { const s3Event = event as Partial; if (!Array.isArray(s3Event?.Records)) return false; const eventSource = s3Event.Records[0]?.eventSource; return eventSource === 'aws:s3'; } //#endregion } ================================================ FILE: src/adapters/aws/sns.adapter.ts ================================================ //#region Imports import type { SNSEvent } from 'aws-lambda'; import { getDefaultIfUndefined } from '../../core'; import { AwsSimpleAdapter } from './base'; //#endregion /** * The options to customize the {@link SNSAdapter} * * @breadcrumb Adapters / AWS / SNSAdapter * @public */ export interface SNSAdapterOptions { /** * The path that will be used to create a request to be forwarded to the framework. * * @defaultValue /sns */ snsForwardPath?: string; /** * The http method that will be used to create a request to be forwarded to the framework. * * @defaultValue POST */ snsForwardMethod?: string; } /** * The adapter to handle requests from AWS SNS. * * The option of `responseWithErrors` is ignored by this adapter and we always call `resolver.fail` with the error. * * {@link https://docs.aws.amazon.com/lambda/latest/dg/with-sns.html | Event Reference} * * @example * ```typescript * const snsForwardPath = '/your/route/sns'; // default /sns * const snsForwardMethod = 'POST'; // default POST * const adapter = new SNSAdapter({ snsForwardPath, snsForwardMethod }); * ``` * * @breadcrumb Adapters / AWS / SNSAdapter * @public */ export class SNSAdapter extends AwsSimpleAdapter { //#region Constructor /** * Default constructor * * @param options - The options to customize the {@link SNSAdapter} */ constructor(options?: SNSAdapterOptions) { super({ forwardPath: getDefaultIfUndefined(options?.snsForwardPath, '/sns'), forwardMethod: getDefaultIfUndefined(options?.snsForwardMethod, 'POST'), batch: false, host: 'sns.amazonaws.com', }); } //#endregion //#region Public Methods /** * {@inheritDoc} */ public override getAdapterName(): string { return SNSAdapter.name; } /** * {@inheritDoc} */ public override canHandle(event: unknown): event is SNSEvent { const snsEvent = event as Partial; if (!Array.isArray(snsEvent?.Records)) return false; const eventSource = snsEvent.Records[0]?.EventSource; return eventSource === 'aws:sns'; } //#endregion } ================================================ FILE: src/adapters/aws/sqs.adapter.ts ================================================ //#region Imports import type { SQSEvent } from 'aws-lambda'; import { getDefaultIfUndefined } from '../../core'; import { type AWSSimpleAdapterOptions, AwsSimpleAdapter } from './base/index'; //#endregion /** * The options to customize the {@link SQSAdapter} * * @breadcrumb Adapters / AWS / SQSAdapter * @public */ export interface SQSAdapterOptions extends Pick { /** * The path that will be used to create a request to be forwarded to the framework. * * @defaultValue /sqs */ sqsForwardPath?: string; /** * The http method that will be used to create a request to be forwarded to the framework. * * @defaultValue POST */ sqsForwardMethod?: string; } /** * The adapter to handle requests from AWS SQS. * * The option of `responseWithErrors` is ignored by this adapter and we always call `resolver.fail` with the error. * * {@link https://docs.aws.amazon.com/lambda/latest/dg/with-sqs.html | Event Reference} * * @example * ```typescript * const sqsForwardPath = '/your/route/sqs'; // default /sqs * const sqsForwardMethod = 'POST'; // default POST * const adapter = new SQSAdapter({ sqsForwardPath, sqsForwardMethod }); * ``` * * @breadcrumb Adapters / AWS / SQSAdapter * @public */ export class SQSAdapter extends AwsSimpleAdapter { //#region Constructor /** * Default constructor * * @param options - The options to customize the {@link SNSAdapter} */ constructor(options?: SQSAdapterOptions) { super({ forwardPath: getDefaultIfUndefined(options?.sqsForwardPath, '/sqs'), forwardMethod: getDefaultIfUndefined(options?.sqsForwardMethod, 'POST'), batch: options?.batch, host: 'sqs.amazonaws.com', }); } //#endregion //#region Public Methods /** * {@inheritDoc} */ public override getAdapterName(): string { return SQSAdapter.name; } /** * {@inheritDoc} */ public override canHandle(event: unknown): event is SQSEvent { const sqsEvent = event as Partial; if (!Array.isArray(sqsEvent?.Records)) return false; const eventSource = sqsEvent.Records[0]?.eventSource; return eventSource === 'aws:sqs'; } //#endregion } ================================================ FILE: src/adapters/azure/http-trigger-v4.adapter.ts ================================================ //#region Imports import { URL } from 'node:url'; import type { Context, Cookie, HttpRequest, HttpResponseSimple, } from '@azure/functions'; import type { BothValueHeaders } from '../../@types'; import type { AdapterContract, AdapterRequest, GetResponseAdapterProps, OnErrorProps, } from '../../contracts'; import { getDefaultIfUndefined, getEventBodyAsBuffer, getFlattenedHeadersMap, getPathWithQueryStringParams, } from '../../core'; //#endregion /** * The options to customize the {@link HttpTriggerV4Adapter} * * @breadcrumb Adapters / Azure / HttpTriggerV4Adapter * @public */ export interface HttpTriggerV4AdapterOptions { /** * Strip base path for custom domains * * @defaultValue '' */ stripBasePath?: string; } /** * The adapter to handle requests from Http Trigger on Azure Function V4. * * @example * ```typescript * const stripBasePath = '/any/custom/base/path'; // default '' * const adapter = new HttpTriggerV4Adapter({ stripBasePath }); * ``` * * @see {@link https://docs.microsoft.com/en-us/azure/azure-functions/functions-reference-node | Reference} * * @breadcrumb Adapters / Azure / HttpTriggerV4Adapter * @public */ export class HttpTriggerV4Adapter implements AdapterContract { //#region Constructor /** * Default constructor * * @param options - The options to customize the {@link HttpTriggerV4Adapter} */ constructor(protected readonly options?: HttpTriggerV4AdapterOptions) {} //#endregion //#region Public Methods /** * {@inheritDoc} */ public getAdapterName(): string { return HttpTriggerV4Adapter.name; } /** * {@inheritDoc} */ public canHandle(event: unknown, context: unknown): boolean { const maybeEvent = event as Partial | undefined; const maybeContext = context as Partial | undefined; return !!( maybeEvent && maybeEvent.method && maybeEvent.headers && maybeEvent.url && maybeEvent.query && maybeContext && maybeContext.traceContext && maybeContext.bindingDefinitions && maybeContext.log && !!maybeContext.log.info && maybeContext.bindingData ); } /** * {@inheritDoc} */ public getRequest(event: HttpRequest): AdapterRequest { const path = this.getPathFromEvent(event); const method = event.method!; const headers = getFlattenedHeadersMap(event.headers, ',', true); let body: Buffer | undefined; if (event.body) { const [bufferBody, contentLength] = getEventBodyAsBuffer( event.rawBody, false, ); body = bufferBody; headers['content-length'] = String(contentLength); } const remoteAddress = headers['x-forwarded-for']; return { method, path, headers, remoteAddress, body, }; } /** * {@inheritDoc} */ public getResponse({ body, statusCode, headers: originalHeaders, }: GetResponseAdapterProps): HttpResponseSimple { const headers = getFlattenedHeadersMap(originalHeaders, ',', true); const cookies = this.getAzureCookiesFromHeaders(originalHeaders); if (headers['set-cookie']) delete headers['set-cookie']; return { body, statusCode, headers, // I tried to understand this property with // https://docs.microsoft.com/en-us/aspnet/web-api/overview/formats-and-model-binding/content-negotiation // but I don't know if it's worth implementing this guy as an option // I found out when this guy is set to true and the framework sets content-type, azure returns 500 // So I'll leave it as is and hope no one has any problems. enableContentNegotiation: false, cookies, }; } /** * {@inheritDoc} */ public onErrorWhileForwarding({ error, respondWithErrors, event, delegatedResolver, log, }: OnErrorProps): void { const body = respondWithErrors ? error.stack : ''; const errorResponse = this.getResponse({ event, statusCode: 500, body: body || '', headers: {}, isBase64Encoded: false, log, }); delegatedResolver.succeed(errorResponse); } //#endregion //#region Protected Methods /** * Get path from event with query strings * * @param event - The event sent by serverless */ protected getPathFromEvent(event: HttpRequest): string { const stripBasePath = getDefaultIfUndefined( this.options?.stripBasePath, '', ); const url = new URL(event.url); const originalPath = url.pathname; const replaceRegex = new RegExp(`^${stripBasePath}`); const path = originalPath.replace(replaceRegex, ''); const queryParams = event.query; return getPathWithQueryStringParams(path, queryParams); } /** * Get the Azure Cookie list parsed from set-cookie header. * * @param headers - The headers object */ protected getAzureCookiesFromHeaders(headers: BothValueHeaders): Cookie[] { const setCookie = headers['set-cookie']; const headerCookies = Array.isArray(setCookie) ? setCookie : setCookie ? [setCookie] : []; return headerCookies.map(cookie => this.parseCookie(cookie)); } /** * Parse the string cookie to the Azure Cookie Object. * This code was written by {@link https://github.com/zachabney | @zachabney} * on {@link https://github.com/zachabney/azure-aws-serverless-express/blob/241d2d5c4d5906e4817662cad6426ec2cbbf9ca7/src/index.js#L4-L49 | this library}. * * @param cookie - The cookie */ protected parseCookie(cookie: string): Cookie { return cookie.split(';').reduce( (azureCookieObject, cookieProperty, index) => { const [key, value] = cookieProperty.split('='); const sanitizedKey = key.toLowerCase().trim(); const sanitizedValue = value && value.trim(); if (index === 0) { azureCookieObject.name = key; azureCookieObject.value = sanitizedValue; return azureCookieObject; } switch (sanitizedKey) { case 'domain': azureCookieObject.domain = sanitizedValue; break; case 'path': azureCookieObject.path = sanitizedValue; break; case 'expires': azureCookieObject.expires = new Date(sanitizedValue); break; case 'secure': azureCookieObject.secure = true; break; case 'httponly': azureCookieObject.httpOnly = true; break; case 'samesite': azureCookieObject.sameSite = sanitizedValue as Cookie['sameSite']; break; case 'max-age': azureCookieObject.maxAge = Number(sanitizedValue); break; } return azureCookieObject; }, { name: '', value: '' } as Cookie, ); } //#endregion } ================================================ FILE: src/adapters/azure/index.ts ================================================ export * from './http-trigger-v4.adapter'; ================================================ FILE: src/adapters/digital-ocean/http-function.adapter.ts ================================================ //#region Imports //#region Imports import type { DigitalOceanHttpEvent, DigitalOceanHttpResponse, } from '../../@types/digital-ocean'; import type { AdapterContract, AdapterRequest, GetResponseAdapterProps, OnErrorProps, } from '../../contracts'; import { getDefaultIfUndefined, getEventBodyAsBuffer, getFlattenedHeadersMap, getPathWithQueryStringParams, } from '../../core'; //#endregion /** * The options to customize the {@link HttpFunctionAdapter} * * @breadcrumb Adapters / Digital Ocean / HttpFunctionAdapter * @public */ export interface HttpFunctionAdapterOptions { /** * Strip base path for custom domains * * @defaultValue '' */ stripBasePath?: string; } /** * The adapter to handle requests from Digital Ocean Functions when called from HTTP Endpoint. * * @example * ```typescript * const stripBasePath = '/any/custom/base/path'; // default '' * const adapter = new HttpFunctionAdapter({ stripBasePath }); * ``` * * @breadcrumb Adapters / Digital Ocean / HttpFunctionAdapter * @public */ export class HttpFunctionAdapter implements AdapterContract { //#region Constructor /** * Default constructor * * @param options - The options to customize the {@link HttpFunctionAdapter} */ constructor(protected readonly options?: HttpFunctionAdapterOptions) {} //#endregion //#region Public Methods /** * {@inheritDoc} */ public getAdapterName(): string { return HttpFunctionAdapter.name; } /** * {@inheritDoc} */ public canHandle(event: unknown): event is DigitalOceanHttpEvent { const digitalOceanHttpEvent = event as DigitalOceanHttpEvent; return ( !!digitalOceanHttpEvent && digitalOceanHttpEvent.__ow_path !== undefined && digitalOceanHttpEvent.__ow_method !== undefined && digitalOceanHttpEvent.__ow_headers !== undefined ); } /** * {@inheritDoc} */ public getRequest(event: DigitalOceanHttpEvent): AdapterRequest { const headers = event.__ow_headers; const method = event.__ow_method.toUpperCase(); const path = this.getPathFromEvent(event); let body: Buffer | undefined; if (event.__ow_body) { const [bufferBody, contentLength] = getEventBodyAsBuffer( event.__ow_body, !!event.__ow_isBase64Encoded, ); body = bufferBody; headers['content-length'] = String(contentLength); } const remoteAddress = headers['x-forwarded-for']; return { method, headers, body, remoteAddress, path, }; } /** * {@inheritDoc} */ public getResponse({ headers: responseHeaders, body, statusCode, }: GetResponseAdapterProps): DigitalOceanHttpResponse { const headers = getFlattenedHeadersMap(responseHeaders); return { statusCode, body, headers, }; } /** * {@inheritDoc} */ public onErrorWhileForwarding({ error, delegatedResolver, respondWithErrors, event, log, }: OnErrorProps): void { const body = respondWithErrors ? error.stack : ''; const errorResponse = this.getResponse({ event, statusCode: 500, body: body || '', headers: {}, isBase64Encoded: false, log, }); delegatedResolver.succeed(errorResponse); } //#endregion //#region Protected Methods /** * Get path from event with query strings * * @param event - The event sent by digital ocean */ protected getPathFromEvent(event: DigitalOceanHttpEvent): string { const stripBasePath = getDefaultIfUndefined( this.options?.stripBasePath, '', ); const replaceRegex = new RegExp(`^${stripBasePath}`); const path = event.__ow_path.replace(replaceRegex, ''); const queryParams = event.__ow_query; return getPathWithQueryStringParams(path, queryParams || {}); } //#endregion } ================================================ FILE: src/adapters/digital-ocean/index.ts ================================================ export * from './http-function.adapter'; ================================================ FILE: src/adapters/dummy/dummy.adapter.ts ================================================ //#region Imports import type { AdapterContract, AdapterRequest, OnErrorProps, } from '../../contracts'; import { EmptyResponse, type IEmptyResponse } from '../../core'; //#endregion /** * The class that represents a dummy adapter that does nothing and can be used by the cloud that doesn't use adapters. * * @breadcrumb Adapters / DummyAdapter * @public */ export class DummyAdapter implements AdapterContract { /** * {@inheritDoc} */ public canHandle(): boolean { return true; } /** * {@inheritDoc} */ public getAdapterName(): string { return DummyAdapter.name; } /** * {@inheritDoc} */ public getRequest(): AdapterRequest { return { method: 'POST', body: void 0, path: '/dummy', headers: {}, }; } /** * {@inheritDoc} */ public getResponse(): IEmptyResponse { return EmptyResponse; } /** * {@inheritDoc} */ public onErrorWhileForwarding(props: OnErrorProps): void { props.delegatedResolver.succeed(); } } ================================================ FILE: src/adapters/dummy/index.ts ================================================ export * from './dummy.adapter'; ================================================ FILE: src/adapters/huawei/huawei-api-gateway.adapter.ts ================================================ //#region Imports import type { HuaweiApiGatewayEvent, HuaweiApiGatewayResponse, HuaweiContext, } from '../../@types/huawei'; import type { AdapterContract, AdapterRequest, GetResponseAdapterProps, OnErrorProps, } from '../../contracts'; import { getDefaultIfUndefined, getEventBodyAsBuffer, getFlattenedHeadersMap, getMultiValueHeadersMap, getPathWithQueryStringParams, } from '../../core'; //#endregion /** * The options to customize the {@link HuaweiApiGatewayAdapter} * * @breadcrumb Adapters / Huawei / HuaweiApiGatewayAdapter * @public */ export interface HuaweiApiGatewayOptions { /** * Strip base path for custom domains * * @defaultValue '' */ stripBasePath?: string; } /** * The adapter to handle requests from Huawei Api Gateway * * @example * ```typescript * const stripBasePath = '/any/custom/base/path'; // default '' * const adapter = new ApiGatewayAdapter({ stripBasePath }); * ``` * * @breadcrumb Adapters / Huawei / HuaweiApiGatewayAdapter * @public * * {@link https://support.huaweicloud.com/intl/en-us/devg-functiongraph/functiongraph_02_0102.html#functiongraph_02_0102__li5178638110137 | Event Reference} */ export class HuaweiApiGatewayAdapter implements AdapterContract< HuaweiApiGatewayEvent, HuaweiContext, HuaweiApiGatewayResponse > { //#region Constructor /** * Default constructor * * @param options - The options to customize the {@link HuaweiApiGatewayAdapter} */ constructor(protected readonly options?: HuaweiApiGatewayOptions) {} //#endregion //#region Public Methods /** * {@inheritDoc} */ public getAdapterName(): string { return HuaweiApiGatewayAdapter.name; } /** * {@inheritDoc} */ public canHandle(event: unknown): event is HuaweiApiGatewayEvent { const apiGatewayEvent = event as Partial; return !!( apiGatewayEvent && apiGatewayEvent.httpMethod && apiGatewayEvent.requestContext && apiGatewayEvent.requestContext.apiId && apiGatewayEvent.requestContext.stage && apiGatewayEvent.requestContext.requestId && // to avoid conflict with api gateway v1 of aws !('multiValueQueryStringParameters' in apiGatewayEvent) ); } /** * {@inheritDoc} */ public getRequest(event: HuaweiApiGatewayEvent): AdapterRequest { const method = event.httpMethod; const path = this.getPathFromEvent(event); const headers = getFlattenedHeadersMap(event.headers, ',', true); let body: Buffer | undefined; if (event.body) { const [bufferBody, contentLength] = getEventBodyAsBuffer( event.body, event.isBase64Encoded, ); body = bufferBody; headers['content-length'] = String(contentLength); } const remoteAddress = headers['x-real-ip']; return { method, headers, body, remoteAddress, path, }; } /** * {@inheritDoc} */ public getResponse({ headers: responseHeaders, body, isBase64Encoded, statusCode, }: GetResponseAdapterProps): HuaweiApiGatewayResponse { const headers = getMultiValueHeadersMap(responseHeaders); return { statusCode, body, headers, isBase64Encoded, }; } /** * {@inheritDoc} */ public onErrorWhileForwarding({ error, delegatedResolver, respondWithErrors, event, log, }: OnErrorProps): void { const body = respondWithErrors ? error.stack : ''; const errorResponse = this.getResponse({ event, statusCode: 500, body: body || '', headers: {}, isBase64Encoded: false, log, }); delegatedResolver.succeed(errorResponse); } //#endregion //#region Protected Methods /** * Get path from event with query strings * * @param event - The event sent by serverless */ protected getPathFromEvent(event: HuaweiApiGatewayEvent): string { const stripBasePath = getDefaultIfUndefined( this.options?.stripBasePath, '', ); const replaceRegex = new RegExp(`^${stripBasePath}`); const path = event.path.replace(replaceRegex, ''); const queryParams = event.queryStringParameters; return getPathWithQueryStringParams(path, queryParams); } //#endregion } ================================================ FILE: src/adapters/huawei/index.ts ================================================ export * from './huawei-api-gateway.adapter'; ================================================ FILE: src/contracts/adapter.contract.ts ================================================ //#region Imports import type { BothValueHeaders, SingleValueHeaders } from '../@types'; import type { ILogger } from '../core'; import { ServerlessResponse } from '../network'; import type { DelegatedResolver } from './resolver.contract'; //#endregion /** * The request interface used to bridge any event source to the framework. * * @breadcrumb Contracts / AdapterContract * @public */ export interface AdapterRequest { /** * The HTTP Method to use to create the request to the framework */ method: string; /** * The path to use to create the request to the framework */ path: string; /** * The headers to use to create the request to the framework */ headers: SingleValueHeaders; /** * The body as buffer to use to create the request to the framework */ body?: Buffer; /** * The remote address (client ip) to use to create the request to the framework */ remoteAddress?: string; /** * The address of the event source (used in Lambda\@edge) * * @deprecated It is no longer used in the library and will be removed in the next major release. */ host?: string; /** * The address of the event source (used in Lambda\@edge) * * @deprecated It is no longer used in the library and will be removed in the next major release. */ hostname?: string; } /** * The props of the method that get the response from the framework and transform it into a format that the event source can handle * * @breadcrumb Contracts / AdapterContract * @public */ export interface GetResponseAdapterProps { /** * The event sent by the serverless */ event: TEvent; /** * The framework {@link ServerlessResponse | response}. * * @remarks It can be optional, as this method can be used when an error occurs during the handling of the request by the framework. */ response?: ServerlessResponse; /** * The framework response status code */ statusCode: number; /** * The framework response body */ body: string; /** * The framework response headers */ headers: BothValueHeaders; /** * Indicates whether the response is base64 encoded or not */ isBase64Encoded: boolean; /** * The instance of the logger */ log: ILogger; } /** * The props of the method that handle the response when an error occurs while forwarding the request to the framework * * @breadcrumb Contracts / AdapterContract * @public */ export interface OnErrorProps { /** * The event sent by the serverless */ event: TEvent; /** * The error throwed during forwarding */ error: Error; /** * The instance of the resolver */ delegatedResolver: DelegatedResolver; /** * Indicates whether to forward the (error.stack) or not to the client */ respondWithErrors: boolean; /** * The instance of the logger */ log: ILogger; } /** * The interface that represents a contract between the adapter and the actual implementation of the adapter. * * @breadcrumb Contracts / AdapterContract * @public */ export interface AdapterContract { /** * Get the adapter name */ getAdapterName(): string; /** * Decide if this adapter can handle a request based in the event payload * * @param event - The event sent by serverless * @param context - The context sent by the serverless * @param log - The instance of logger */ canHandle(event: unknown, context: TContext, log: ILogger): boolean; /** * Maps the serverless payload to an actual request that a framework can handle * * @param event - The event sent by serverless * @param context - The context sent by the serverless * @param log - The instance of logger */ getRequest(event: TEvent, context: TContext, log: ILogger): AdapterRequest; /** * Maps the response of the framework to a payload that serverless can handle * * @param props - The props sent by serverless */ getResponse(props: GetResponseAdapterProps): TResponse; /** * When an error occurs while forwarding the request to the framework * * @remarks You must call resolver.fail or resolver.succeed when implementing this method. */ onErrorWhileForwarding(props: OnErrorProps): void; } ================================================ FILE: src/contracts/framework.contract.ts ================================================ //#region Imports import type { IncomingMessage, ServerResponse } from 'http'; //#endregion /** * The interface that represents a contract between the framework and the framework implementation * * @breadcrumb Contracts * @public */ export interface FrameworkContract { /** * Send the request and response objects to the framework * * @param app - The instance of your app (Express, Fastify, Koa, etc...) * @param request - The request object that will be forward to your app * @param response - The response object that will be forward to your app to output the response */ sendRequest( app: TApp, request: IncomingMessage, response: ServerResponse, ): void; } ================================================ FILE: src/contracts/handler.contract.ts ================================================ //#region Imports import type { BinarySettings } from '../@types'; import type { ILogger } from '../core'; import type { AdapterContract } from './adapter.contract'; import type { FrameworkContract } from './framework.contract'; import type { ResolverContract } from './resolver.contract'; //#endregion /** * The function used to handle serverless requests * * @breadcrumb Contracts / HandlerContract * @public */ export type ServerlessHandler = (...args: any[]) => TReturn; /** * The interface that represents the contract between the handler and the real implementation * * @breadcrumb Contracts / HandlerContract * @public */ export interface HandlerContract< TApp, TEvent, TContext, TCallback, TResponse, TReturn, > { /** * Get the handler that will handle serverless requests */ getHandler( app: TApp, framework: FrameworkContract, adapters: AdapterContract[], resolverFactory: ResolverContract< TEvent, TContext, TCallback, TResponse, TReturn >, binarySettings: BinarySettings, respondWithErrors: boolean, log: ILogger, ): ServerlessHandler; } ================================================ FILE: src/contracts/index.ts ================================================ export * from './adapter.contract'; export * from './framework.contract'; export * from './handler.contract'; export * from './resolver.contract'; ================================================ FILE: src/contracts/resolver.contract.ts ================================================ //#region Imports import type { ILogger } from '../core'; import type { AdapterContract } from './adapter.contract'; //#endregion /** * The type that represents a resolver used to send the response, error or success, to the client * * @breadcrumb Contracts / ResolverContract * @public */ export type Resolver = { /** * The method that will perform the task of forwarding the request to the framework and waiting for the promise to be resolved with the response * * @param task - The task to be executed */ run(task: () => Promise): TReturn; }; /** * The type that represents a delegate resolver that is passed to the adapter to handle what to do when an error occurs during forwarding. * * @breadcrumb Contracts / ResolverContract * @public */ export type DelegatedResolver = { /** * Send the success response to the client * * @param success - The serverless response */ succeed: (response: TResponse) => void; /** * Send the error response to the client * * @param error - The error object */ fail: (error: Error) => void; }; /** * The createResolver contract props * * @breadcrumb Contracts / ResolverContract * @public */ export type ResolverProps = { /** * The event sent by the serverless environment */ event: TEvent; /** * Indicates whether to forward the (error.stack) or not to the client */ respondWithErrors: boolean; /** * The instance of the logger */ log: ILogger; /** * The instance of the adapter */ adapter: AdapterContract; /** * The context sent by serverless */ context?: TContext; /** * The callback sent by serverless */ callback?: TCallback; }; /** * The interface that represents the contract used to send the response to the client * * @breadcrumb Contracts / ResolverContract * @public */ export interface ResolverContract< TEvent, TContext, TCallback, TResponse, TReturn, > { /** * Create the resolver based on the context, callback or promise * * @param props - The props used to create the resolver */ createResolver( props: ResolverProps, ): Resolver; } ================================================ FILE: src/core/base-handler.ts ================================================ //#region Imports import type { BinarySettings } from '../@types'; import type { AdapterContract, AdapterRequest, FrameworkContract, HandlerContract, ResolverContract, ServerlessHandler, } from '../contracts'; import { ServerlessRequest, ServerlessResponse } from '../network'; import type { ILogger } from './index'; //#endregion /** * The abstract class that represents the base class for a handler * * @breadcrumb Core * @public */ export abstract class BaseHandler< TApp, TEvent, TContext, TCallback, TResponse, TReturn, > implements HandlerContract { //#region Public Methods /** * Get the handler that will handle serverless requests */ public abstract getHandler( app: TApp, framework: FrameworkContract, adapters: AdapterContract[], resolverFactory: ResolverContract< TEvent, TContext, TCallback, TResponse, TReturn >, binarySettings: BinarySettings, respondWithErrors: boolean, log: ILogger, ): ServerlessHandler; //#endregion //#region Protected Methods /** * Get the adapter to handle a specific event and context * * @param event - The event sent by serverless * @param context - The context sent by serverless * @param adapters - The list of adapters * @param log - The instance of logger */ protected getAdapterByEventAndContext( event: TEvent, context: TContext, adapters: AdapterContract[], log: ILogger, ): AdapterContract { const resolvedAdapters = adapters.filter(adapter => adapter.canHandle(event, context, log), ); if (resolvedAdapters.length === 0) { throw new Error( "SERVERLESS_ADAPTER: Couldn't find adapter to handle this event.", ); } if (resolvedAdapters.length > 1) { throw new Error( `SERVERLESS_ADAPTER: Two or more adapters was resolved by the event, the adapters are: ${adapters .map(adapter => adapter.getAdapterName()) .join(', ')}.`, ); } return resolvedAdapters[0]; } /** * Get serverless request and response frmo the adapter request * * @param requestValues - The request values from adapter */ protected getServerlessRequestResponseFromAdapterRequest( requestValues: AdapterRequest, ): [request: ServerlessRequest, response: ServerlessResponse] { const request = new ServerlessRequest({ method: requestValues.method, headers: requestValues.headers, body: requestValues.body, remoteAddress: requestValues.remoteAddress, url: requestValues.path, }); const response = new ServerlessResponse({ method: requestValues.method, }); return [request, response]; } //#endregion } ================================================ FILE: src/core/constants.ts ================================================ /** * Default encodings that are treated as binary, they are compared with the `Content-Encoding` header. * * @breadcrumb Core / Constants * @defaultValue ['gzip', 'deflate', 'br'] * @public */ export const DEFAULT_BINARY_ENCODINGS: string[] = ['gzip', 'deflate', 'br']; /** * Default content types that are treated as binary, they are compared with the `Content-Type` header. * * @breadcrumb Core / Constants * @defaultValue ['image/png', 'image/jpeg', 'image/jpg', 'image/avif', 'image/bmp', 'image/x-png', 'image/gif', 'image/webp', 'video/mp4', 'application/pdf'] * @public */ export const DEFAULT_BINARY_CONTENT_TYPES: string[] = [ 'image/png', 'image/jpeg', 'image/jpg', 'image/avif', 'image/bmp', 'image/x-png', 'image/gif', 'image/webp', 'video/mp4', 'application/pdf', ]; /** * Type alias for empty response and can be used on some adapters when the adapter does not need to return a response. * * @breadcrumb Core / Constants * @public */ // eslint-disable-next-line @typescript-eslint/ban-types export type IEmptyResponse = {}; /** * Constant for empty response and can be used on some adapters when the adapter does not need to return a response. * * @breadcrumb Core / Constants * @public */ export const EmptyResponse: IEmptyResponse = {}; ================================================ FILE: src/core/current-invoke.ts ================================================ /** * 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. * * @breadcrumb Core / Current Invoke * @public */ export type CurrentInvoke = { /** * The event created by the serverless trigger * * @remarks It's only null when you call {@link getCurrentInvoke} outside this library's pipeline. */ event: TEvent | null; /** * The context created by the serverless environment * * @remarks It's only null when you call {@link getCurrentInvoke} outside this library's pipeline. */ context: TContext | null; }; const currentInvoke: CurrentInvoke = { context: null, event: null, }; /** * Get the reference to the event created by the serverless trigger or context created by the serverless environment. * * @example * ```typescript * import type { ALBEvent, Context } from 'aws-lambda'; * * // inside the method that handles the aws alb request. * const { event, context } = getCurrentInvoke(); * ``` * * @breadcrumb Core / Current Invoke * @public */ export function getCurrentInvoke(): CurrentInvoke< TEvent, TContext > { return currentInvoke; } /** * Method that saves to the event created by the serverless trigger or context created by the serverless environment. * * @remarks This method MUST NOT be called by you, this method MUST only be used internally in this library. * * @param event - The event created by the serverless trigger * @param context - The context created by the serverless environment * * @breadcrumb Core / Current Invoke * @public */ export function setCurrentInvoke({ event, context, }: CurrentInvoke) { currentInvoke.event = event; currentInvoke.context = context; } ================================================ FILE: src/core/event-body.ts ================================================ /** * Get the event body as buffer from body string with content length * * @example * ```typescript * const body = '{}'; * const [buffer, contentLength] = getEventBodyAsBuffer(body, false); * console.log(buffer); * // * console.log(contentLength); * // 2 * ``` * * @param body - The body string that can be encoded or not * @param isBase64Encoded - Tells if body string is encoded in base64 * * @breadcrumb Core * @public */ export function getEventBodyAsBuffer( body: string, isBase64Encoded: boolean, ): [body: Buffer, contentLength: number] { const encoding: BufferEncoding = isBase64Encoded ? 'base64' : 'utf8'; const buffer = Buffer.from(body, encoding); const contentLength = Buffer.byteLength(buffer, encoding); return [buffer, contentLength]; } ================================================ FILE: src/core/headers.ts ================================================ //#region Imports import type { BothValueHeaders } from '../@types'; //#endregion /** * Transform a header map and make sure the value is not an array * * @example * ```typescript * const headers = { 'accept-encoding': 'gzip', 'accept-language': ['en-US', 'en;q=0.9'] }; * const flattenedHeaders = getFlattenedHeadersMap(headers, ',', true); * console.log(flattenedHeaders); * // { 'accept-encoding': 'gzip', 'accept-language': 'en-US,en;q=0.9' } * ``` * * @param headersMap - The initial headers * @param separator - The separator used when we join the array of header's value * @param lowerCaseKey - Should put all keys in lowercase * * @breadcrumb Core / Headers * @public */ export function getFlattenedHeadersMap( headersMap: BothValueHeaders, separator: string = ',', lowerCaseKey: boolean = false, ): Record { return Object.keys(headersMap).reduce((acc, headerKey) => { const newKey = lowerCaseKey ? headerKey.toLowerCase() : headerKey; const headerValue = headersMap[headerKey]; if (Array.isArray(headerValue)) acc[newKey] = headerValue.join(separator); else acc[newKey] = (headerValue ?? '') + ''; return acc; }, {}); } /** * Transforms a header map into a multi-value map header. * * @example * ```typescript * const headers = { 'accept-encoding': 'gzip', 'connection': ['keep-alive'] }; * const multiValueHeaders = getMultiValueHeadersMap(headers); * console.log(multiValueHeaders); * // { 'accept-encoding': ['gzip'], 'connection': ['keep-alive'] } * ``` * * @param headersMap - The initial headers * * @breadcrumb Core / Headers * @public */ export function getMultiValueHeadersMap( headersMap: BothValueHeaders, ): Record { return Object.keys(headersMap).reduce((acc, headerKey) => { const headerValue = headersMap[headerKey]; acc[headerKey.toLowerCase()] = Array.isArray(headerValue) ? headerValue.map(String) : [String(headerValue)]; return acc; }, {}); } /** * The wrapper that holds the information about single value headers and cookies * * @breadcrumb Core / Headers * @public */ export type FlattenedHeadersAndCookies = { /** * Just the single value headers */ headers: Record; /** * The list of cookies */ cookies: string[]; }; /** * Transforms a header map into a single value headers and cookies * * @param headersMap - The initial headers * * @breadcrumb Core / Headers * @public */ export function getFlattenedHeadersMapAndCookies( headersMap: BothValueHeaders, ): FlattenedHeadersAndCookies { return Object.keys(headersMap).reduce( (acc, headerKey) => { const headerValue = headersMap[headerKey]; const lowerHeaderKey = headerKey.toLowerCase(); if (Array.isArray(headerValue)) { if (lowerHeaderKey !== 'set-cookie') acc.headers[headerKey] = headerValue.join(','); else acc.cookies.push(...headerValue); } else { if (lowerHeaderKey === 'set-cookie' && headerValue !== undefined) acc.cookies.push(headerValue ?? ''); else acc.headers[headerKey] = String(headerValue ?? ''); } return acc; }, { cookies: [], headers: {}, } as FlattenedHeadersAndCookies, ); } /** * Parse HTTP Raw Headers * Attribution to {@link https://github.com/kesla/parse-headers/blob/master/parse-headers.js} * * @param headers - The raw headers * * @breadcrumb Core / Headers * @public */ export function parseHeaders( headers: string, ): Record { if (!headers) return {}; const result = {}; const headersArr = headers.trim().split('\n'); for (let i = 0; i < headersArr.length; i++) { const row = headersArr[i]; const index = row.indexOf(':'); const key = row.slice(0, index).trim().toLowerCase(); const value = row.slice(index + 1).trim(); if (typeof result[key] === 'undefined') result[key] = value; else if (Array.isArray(result[key])) result[key].push(value); else result[key] = [result[key], value]; } return result; } export function keysToLowercase>( obj: T, ): { [K in keyof T as Lowercase]: T[K] } { const result: any = {}; for (const [k, v] of Object.entries(obj)) result[k.toLowerCase()] = v; return result as { [K in keyof T as Lowercase]: T[K] }; } ================================================ FILE: src/core/index.ts ================================================ export * from './base-handler'; export * from './constants'; export * from './current-invoke'; export * from './event-body'; export * from './headers'; export * from './is-binary'; export * from './logger'; export * from './no-op'; export * from './optional'; export * from './path'; export * from './stream'; ================================================ FILE: src/core/is-binary.ts ================================================ // ATTRIBUTION: https://github.com/dougmoscrop/serverless-http //#region Imports import type { BinarySettings, BothValueHeaders } from '../@types'; //#endregion /** * The function that determines by the content encoding whether the response should be treated as binary * * @example * ```typescript * const headers = { 'content-encoding': 'gzip' }; * const isBinary = isContentEncodingBinary(headers, ['gzip']); * console.log(isBinary); * // true * ``` * * @param headers - The headers of the response * @param binaryEncodingTypes - The list of content encodings that will be treated as binary * * @breadcrumb Core / isBinary * @public */ export function isContentEncodingBinary( headers: BothValueHeaders, binaryEncodingTypes: string[], ): boolean { let contentEncodings = headers['content-encoding']; if (!contentEncodings) return false; if (!Array.isArray(contentEncodings)) contentEncodings = contentEncodings.split(','); return contentEncodings.some(value => binaryEncodingTypes.includes(value.trim()), ); } /** * The function that returns the content type of headers * * @example * ```typescript * const headers = { 'content-type': 'application/json' }; * const contentType = getContentType(headers); * console.log(contentType); * // application/json * ``` * * @param headers - The headers of the response * * @breadcrumb Core / isBinary * @public */ export function getContentType(headers: BothValueHeaders): string { const contentTypeHeaderRaw = headers['content-type']; const contentTypeHeader = Array.isArray(contentTypeHeaderRaw) ? contentTypeHeaderRaw[0] || '' : contentTypeHeaderRaw || ''; if (!contentTypeHeaderRaw) return ''; // only compare mime type; ignore encoding part const contentTypeStart = contentTypeHeader.indexOf(';'); if (contentTypeStart === -1) return contentTypeHeader; return contentTypeHeader.slice(0, contentTypeStart); } /** * The function that determines by the content type whether the response should be treated as binary * * @example * ```typescript * const headers = { 'content-type': 'image/png' }; * const isBinary = isContentTypeBinary(headers, new Map([['image/png', true]])); * console.log(isBinary); * // true * ``` * * @param headers - The headers of the response * @param binaryContentTypes - The list of content types that will be treated as binary * * @breadcrumb Core / isBinary * @public */ export function isContentTypeBinary( headers: BothValueHeaders, binaryContentTypes: string[], ) { const contentType = getContentType(headers); if (!contentType) return false; return binaryContentTypes.includes(contentType.trim()); } /** * The function used to determine from the headers and the binary settings if a response should be encoded or not * * @example * ```typescript * const headers = { 'content-type': 'image/png', 'content-encoding': 'gzip' }; * const isContentBinary = isBinary(headers, { contentEncodings: ['gzip'], contentTypes: ['image/png'] }); * console.log(isContentBinary); * // true * ``` * * @param headers - The headers of the response * @param binarySettings - The settings for the validation * * @breadcrumb Core / isBinary * @public */ export function isBinary( headers: BothValueHeaders, binarySettings: BinarySettings, ): boolean { if ('isBinary' in binarySettings) { if (binarySettings.isBinary === false) return false; return binarySettings.isBinary(headers); } return ( isContentEncodingBinary(headers, binarySettings.contentEncodings) || isContentTypeBinary(headers, binarySettings.contentTypes) ); } ================================================ FILE: src/core/logger.ts ================================================ import { NO_OP } from './no-op'; /** * The type representing the possible log levels to choose from. * * @breadcrumb Core / Logger * @public */ export type LogLevels = | 'debug' | 'verbose' | 'info' | 'warn' | 'error' | 'none'; /** * The options to customize {@link ILogger} * * @breadcrumb Core / Logger * @public */ export type LoggerOptions = { /** * Select the log level, {@link LogLevels | see more}. * * @defaultValue error */ level: LogLevels; }; /** * The log function used in any level. * * @breadcrumb Core / Logger * @public */ export type LoggerFN = (message: any, ...additional: any[]) => void; /** * The interface representing the logger, you can provide a custom logger by implementing this interface. * * @breadcrumb Core / Logger * @public */ export type ILogger = Record, LoggerFN>; /** * The symbol used to check against an ILogger instace to verify if that ILogger was created by this library * * @breadcrumb Core / Logger * @public */ const InternalLoggerSymbol = Symbol('InternalLogger'); const logLevels: Record< LogLevels, [level: LogLevels, consoleMethod: keyof Console][] > = { debug: [ ['debug', 'debug'], ['verbose', 'debug'], ['info', 'info'], ['error', 'error'], ['warn', 'warn'], ], verbose: [ ['verbose', 'debug'], ['info', 'info'], ['error', 'error'], ['warn', 'warn'], ], info: [ ['info', 'info'], ['error', 'error'], ['warn', 'warn'], ], warn: [ ['warn', 'warn'], ['error', 'error'], ], error: [['error', 'error']], none: [], }; const lazyPrint = (value: () => any | unknown) => { if (typeof value === 'function') return value(); return value; }; const print = (fn: string) => (message: any, ...additional: (() => any)[]) => console[fn](message, ...additional.map(lazyPrint)); /** * The method used to create a simple logger instance to use in this library. * * @remarks Behind the scenes, this simple logger sends the message to the `console` methods. * * @example * ```typescript * const logger = createDefaultLogger(); * * logger.error('An error happens.'); * // An error happens. * ``` * * @param level - Select the level of the log * * @breadcrumb Core / Logger * @public */ export function createDefaultLogger( { level }: LoggerOptions = { level: 'error' }, ): ILogger { const levels = logLevels[level]; if (!levels) throw new Error('Invalid log level'); const logger = { [InternalLoggerSymbol]: true, error: NO_OP, debug: NO_OP, info: NO_OP, verbose: NO_OP, warn: NO_OP, } as ILogger; for (const [level, consoleMethod] of levels) logger[level] = print(consoleMethod); return logger; } /** * The method used to chck if logger was created by this library, or it was defined by the user. * * @param logger - The instance of the logger to check * * @breadcrumb Core / Logger * @public */ export function isInternalLogger(logger: ILogger): boolean { return !!logger[InternalLoggerSymbol]; } ================================================ FILE: src/core/no-op.ts ================================================ /** * No operation function is used when we need to pass a function, but we don't want to specify any behavior. * * @breadcrumb Core * @public */ export const NO_OP: (...args: any[]) => any = () => void 0; ================================================ FILE: src/core/optional.ts ================================================ /** * Return the defaultValue whether the value is undefined, otherwise, return the value. * * @example * ```typescript * const value1 = getDefaultIfUndefined(undefined, true); * const value2 = getDefaultIfUndefined(false, true); * * console.log(value1); * // true * console.log(value2); * // false * ``` * * @param value - The value to be checked * @param defaultValue - The default value when value is undefined * * @breadcrumb Core * @public */ export function getDefaultIfUndefined( value: T | undefined, defaultValue: T, ): T { if (value === undefined) return defaultValue; return value; } ================================================ FILE: src/core/path.ts ================================================ /** * Transform the path and a map of query params to a string with formatted query params * * @example * ```typescript * const path = '/pets/search'; * const queryParams = { batata: undefined, petType: [ 'dog', 'fish' ] }; * const result = getPathWithQueryStringParams(path, queryParams); * console.log(result); * // /pets/search?batata=&petType=dog&petType=fish * ``` * * @param path - The path * @param queryParams - The query params * * @breadcrumb Core / Path * @public */ export function getPathWithQueryStringParams( path: string, queryParams: | string | Record | undefined | null, ): string { if (String(queryParams || '').length === 0) return path; if (typeof queryParams === 'string') return `${path}?${queryParams}`; const queryParamsString = getQueryParamsStringFromRecord(queryParams); if (!queryParamsString) return path; return `${path}?${queryParamsString}`; } /** * Map query params to a string with formatted query params * * @example * ```typescript * const queryParams = { batata: undefined, petType: [ 'dog', 'fish' ] }; * const result = getQueryParamsStringFromRecord(queryParams); * console.log(result); * // batata=&petType=dog&petType=fish * ``` * * @param queryParamsRecord - The query params record * * @breadcrumb Core / Path * @public */ export function getQueryParamsStringFromRecord( queryParamsRecord: | Record | undefined | null, ): string { const searchParams = new URLSearchParams(); const multiValueHeadersEntries: [string, string | string[] | undefined][] = Object.entries(queryParamsRecord || {}); if (multiValueHeadersEntries.length === 0) return ''; for (const [key, value] of multiValueHeadersEntries) { if (!Array.isArray(value)) { searchParams.append(key, value || ''); continue; } for (const arrayValue of value) searchParams.append(key, arrayValue); } return searchParams.toString(); } /** * Type of the function to strip base path * * @breadcrumb Core / Path * @public */ export type StripBasePathFn = (path: string) => string; const NOOPBasePath: StripBasePathFn = (path: string) => path; /** * Get the strip base path function * * @param basePath - The base path * * @breadcrumb Core / Path * @public */ export function buildStripBasePath( basePath: string | undefined, ): StripBasePathFn { if (!basePath) return NOOPBasePath; const length = basePath.length; return (path: string) => { if (path.startsWith(basePath)) return path.slice(path.indexOf(basePath) + length, path.length) || '/'; return path; }; } ================================================ FILE: src/core/stream.ts ================================================ //#region Imports import { Readable, Writable } from 'node:stream'; //#endregion /** * Check if stream already ended * * @param stream - The stream * * @breadcrumb Core / Stream * @public */ export function isStreamEnded(stream: Readable | Writable): boolean { if ('readableEnded' in stream && stream.readableEnded) return true; if ('writableEnded' in stream && stream.writableEnded) return true; return false; } /** * Wait asynchronous the stream to complete * * @param stream - The stream * * @breadcrumb Core / Stream * @public */ export function waitForStreamComplete( stream: TStream, ): Promise { if (isStreamEnded(stream)) return Promise.resolve(stream); return new Promise((resolve, reject) => { // Reading the {@link https://github.com/nodejs/node/blob/v12.22.9/lib/events.js#L262 | emit source code}, // it's almost impossible to complete being called twice because the emit function runs synchronously and removes the other listeners, // 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. let isComplete = false; function complete(err: any) { /* istanbul ignore next */ if (isComplete) return; isComplete = true; stream.removeListener('error', complete); stream.removeListener('end', complete); stream.removeListener('finish', complete); if (err) reject(err); else resolve(stream); } stream.once('error', complete); stream.once('end', complete); stream.once('finish', complete); }); } ================================================ FILE: src/frameworks/apollo-server/apollo-server.framework.ts ================================================ //#region import type { IncomingMessage, ServerResponse } from 'http'; import { type ApolloServer, type BaseContext, HeaderMap } from '@apollo/server'; import type { FrameworkContract } from '../../contracts'; import { ServerlessRequest } from '../../network'; import { getDefaultIfUndefined } from '../../core'; //#endregion /** * The default context of Apollo Server when you integrate and don't pass any context. * * @breadcrumb Frameworks / ApolloServerFramework * @public */ export interface DefaultServerlessApolloServerContext extends BaseContext { /** * The request reference */ request: IncomingMessage; /** * The response reference */ response: ServerResponse; } /** * The arguments used to create a Context inside {@link ApolloServerOptions} * * @breadcrumb Frameworks / ApolloServerFramework * @public */ export type ApolloServerContextArguments = { /** * The request reference */ request: IncomingMessage; /** * The response reference */ response: ServerResponse; }; /** * The options to customize {@link ApolloServerFramework} * * @breadcrumb Frameworks / ApolloServerFramework * @public */ export interface ApolloServerOptions { /** * Define a function to create the context of Apollo Server * * @param options - Default options passed by library */ context?: (options: ApolloServerContextArguments) => Promise; } /** * The framework that forwards requests to Apollo Server * * @breadcrumb Frameworks / ApolloServerFramework * @public */ export class ApolloServerFramework implements FrameworkContract> { //#region Constructor /** * Construtor padrão */ constructor(protected readonly options?: ApolloServerOptions) {} //#endregion /** * {@inheritDoc} */ public sendRequest( app: ApolloServer, request: ServerlessRequest, response: ServerResponse, ): void { const headers = new HeaderMap(); for (const [key, value] of Object.entries(request.headers)) { if (value === undefined) continue; headers.set( key, Array.isArray(value) ? value.join(', ') : value.toString(), ); } const defaultContext: ApolloServerOptions['context'] = context => Promise.resolve(context); const context = () => getDefaultIfUndefined( this.options?.context, defaultContext, )({ request, response }); const search = request.url?.startsWith('http') ? (new URL(request.url).search ?? '') : request.url?.split('?')[1] || ''; // we don't need to handle catch because of https://www.apollographql.com/docs/apollo-server/integrations/building-integrations/#handle-errors app .executeHTTPGraphQLRequest({ httpGraphQLRequest: { method: request.method!.toUpperCase(), headers, body: request.body, search, }, context, }) .then(async httpGraphQLResponse => { // this section was copy and pasted from https://github.com/apollographql/apollo-server/blob/main/packages/server/src/express4/index.ts#L95 for (const [key, value] of httpGraphQLResponse.headers) response.setHeader(key, value); response.statusCode = httpGraphQLResponse.status || 200; if (httpGraphQLResponse.body.kind === 'complete') { response.end(httpGraphQLResponse.body.string); return; } for await (const chunk of httpGraphQLResponse.body.asyncIterator) { response.write(chunk); // Express/Node doesn't define a way of saying "it's time to send this // data over the wire"... but the popular `compression` middleware // (which implements `accept-encoding: gzip` and friends) does, by // monkey-patching a `flush` method onto the response. So we call it // if it's there. if (typeof (response as any).flush === 'function') (response as any).flush(); } response.end(); }); } } ================================================ FILE: src/frameworks/apollo-server/index.ts ================================================ export * from './apollo-server.framework'; ================================================ FILE: src/frameworks/body-parser/base-body-parser.framework.ts ================================================ //#region Imports import type { IncomingMessage, ServerResponse } from 'http'; import type { NextHandleFunction } from 'connect'; import type { HttpError } from 'http-errors'; import type { FrameworkContract } from '../../contracts'; import { getDefaultIfUndefined } from '../../core'; //#endregion /** * The options for {@link BaseBodyParserFramework} * * @breadcrumb Frameworks / BodyParserFramework * @public */ export type BodyParserOptions = { /** * Implements a custom way of handling error. * * @defaultValue {@link BaseBodyParserFramework.defaultHandleOnError} * * @example * ```typescript * customErrorHandler: (req: IncomingMessage, response: ServerResponse, error: HttpError) => { * response.setHeader('content-type', 'text/plain'); * response.statusCode = error.statusCode; * // always call end to return the error * response.end(error.message); * } * ``` * * @param request - The referecene for request * @param response - The reference for response * @param error - The error throwed by body-parser */ customErrorHandler?: ( request: IncomingMessage, response: ServerResponse, error: HttpError, ) => void; }; /** * The base class used by other body-parser functions to parse a specific content-type * * @breadcrumb Frameworks / BodyParserFramework * @public */ export class BaseBodyParserFramework implements FrameworkContract { //#region Constructor /** * Default Constructor */ protected constructor( protected readonly framework: FrameworkContract, protected readonly middleware: NextHandleFunction, protected readonly options?: BodyParserOptions, ) { this.errorHandler = getDefaultIfUndefined( this.options?.customErrorHandler, this.defaultHandleOnError.bind(this), ); } //#endregion //#region Protected Properties /** * The selected error handler */ protected readonly errorHandler: NonNullable< BodyParserOptions['customErrorHandler'] >; //#endregion //#region Public Methods /** * {@inheritDoc} */ public sendRequest( app: TApp, request: IncomingMessage, response: ServerResponse, ): void { this.middleware( request, response, this.onBodyParserFinished(app, request, response), ); } //#endregion //#region Protected Methods /** * Handle next execution called by the cors package */ protected onBodyParserFinished( app: TApp, request: IncomingMessage, response: ServerResponse, ): () => void { return (err?: any) => { if (err) return this.errorHandler(request, response, err); this.framework.sendRequest(app, request, response); }; } /** * The default function to handle errors * * @param _request - The referecene for request * @param response - The reference for response * @param error - The error throwed by body-parser */ protected defaultHandleOnError( _request: IncomingMessage, response: ServerResponse, error: HttpError, ): void { response.setHeader('content-type', 'text/plain'); response.statusCode = error.statusCode; response.end(error.message); } //#endregion } ================================================ FILE: src/frameworks/body-parser/index.ts ================================================ export * from './base-body-parser.framework'; export * from './json-body-parser.framework'; export * from './raw-body-parser.framework'; export * from './text-body-parser.framework'; export * from './urlencoded-body-parser.framework'; ================================================ FILE: src/frameworks/body-parser/json-body-parser.framework.ts ================================================ //#region Imports import { type OptionsJson, json } from 'body-parser'; import { type FrameworkContract } from '../../contracts'; import { BaseBodyParserFramework, type BodyParserOptions, } from './base-body-parser.framework'; //#endregion /** * The body-parser options for application/json * * @remarks {@link https://github.com/expressjs/body-parser#bodyparserjsonoptions} * * @breadcrumb Frameworks / BodyParserFramework / JsonBodyParserFramework * @public */ export type JsonBodyParserFrameworkOptions = OptionsJson & BodyParserOptions; /** * The body-parser class used to parse application/json. * * @breadcrumb Frameworks / BodyParserFramework / JsonBodyParserFramework * @public */ export class JsonBodyParserFramework extends BaseBodyParserFramework implements FrameworkContract { //#region Constructor /** * Default Constructor */ constructor( framework: FrameworkContract, options?: JsonBodyParserFrameworkOptions, ) { super(framework, json(options), options); } //#endregion } ================================================ FILE: src/frameworks/body-parser/raw-body-parser.framework.ts ================================================ //#region Imports import { type Options, raw } from 'body-parser'; import type { FrameworkContract } from '../../contracts'; import { BaseBodyParserFramework, type BodyParserOptions, } from './base-body-parser.framework'; //#endregion /** * The body-parser options for application/octet-stream * * @remarks {@link https://github.com/expressjs/body-parser#bodyparserrawoptions} * * @breadcrumb Frameworks / BodyParserFramework / RawBodyParserFramework * @public */ export type RawBodyParserFrameworkOptions = Options & BodyParserOptions; /** * The body-parser class used to parse application/octet-stream. * * @breadcrumb Frameworks / BodyParserFramework / RawBodyParserFramework * @public */ export class RawBodyParserFramework extends BaseBodyParserFramework implements FrameworkContract { //#region Constructor /** * Default Constructor */ constructor( framework: FrameworkContract, options?: RawBodyParserFrameworkOptions, ) { super(framework, raw(options), options); } //#endregion } ================================================ FILE: src/frameworks/body-parser/text-body-parser.framework.ts ================================================ //#region Imports import { type OptionsText, text } from 'body-parser'; import type { FrameworkContract } from '../../contracts'; import { BaseBodyParserFramework, type BodyParserOptions, } from './base-body-parser.framework'; //#endregion /** * The body-parser options for text/plain * * @remarks {@link https://github.com/expressjs/body-parser#bodyparsertextoptions} * * @breadcrumb Frameworks / BodyParserFramework / TextBodyParserFramework * @public */ export type TextBodyParserFrameworkOptions = OptionsText & BodyParserOptions; /** * The body-parser class used to parse text/plain. * * @breadcrumb Frameworks / BodyParserFramework / TextBodyParserFramework * @public */ export class TextBodyParserFramework extends BaseBodyParserFramework implements FrameworkContract { //#region Constructor /** * Default Constructor */ constructor( framework: FrameworkContract, options?: TextBodyParserFrameworkOptions, ) { super(framework, text(options), options); } //#endregion } ================================================ FILE: src/frameworks/body-parser/urlencoded-body-parser.framework.ts ================================================ //#region Imports import { type OptionsUrlencoded, urlencoded } from 'body-parser'; import { type FrameworkContract } from '../../contracts'; import { BaseBodyParserFramework, type BodyParserOptions, } from './base-body-parser.framework'; //#endregion /** * The body parser options for application/x-www-form-urlencoded * * @remarks {@link https://github.com/expressjs/body-parser#bodyparserurlencodedoptions} * * @breadcrumb Frameworks / BodyParserFramework / UrlencodedBodyParserFramework * @public */ export type UrlencodedBodyParserFrameworkOptions = OptionsUrlencoded & BodyParserOptions; /** * The body-parser class used to parse application/x-www-form-urlencoded. * * @breadcrumb Frameworks / BodyParserFramework / UrlencodedBodyParserFramework * @public */ export class UrlencodedBodyParserFramework extends BaseBodyParserFramework implements FrameworkContract { //#region Constructor /** * Default Constructor */ constructor( framework: FrameworkContract, options?: UrlencodedBodyParserFrameworkOptions, ) { super(framework, urlencoded(options), options); } //#endregion } ================================================ FILE: src/frameworks/cors/cors.framework.ts ================================================ //#region Imports import type { IncomingMessage, ServerResponse } from 'http'; import cors, { type CorsOptions } from 'cors'; import type { FrameworkContract } from '../../contracts'; import { getDefaultIfUndefined } from '../../core'; //#endregion /** * The options to customize {@link CorsFramework} * * @breadcrumb Frameworks / CorsFramework * @public */ export type CorsFrameworkOptions = CorsOptions & { /** * Send error 403 when cors is invalid. From what I read in `cors`, `fastify/cors` and [this problem](https://stackoverflow.com/questions/57212248/why-is-http-request-been-processed-in-action-even-when-cors-is-not-enabled) * it is normal to process the request even if the origin is invalid. * So this option will respond with error if this method was called from an invalid origin (or not allowed method) like [access control lib](https://github.com/primus/access-control/blob/master/index.js#L95-L115) . * * @defaultValue true */ forbiddenOnInvalidOriginOrMethod?: boolean; }; /** * The framework that handles cors for your api without relying on internals of the framework * * @example * ```typescript * import express from 'express'; * import { ServerlessAdapter } from '@h4ad/serverless-adapter'; * import { ExpressFramework } from '@h4ad/serverless-adapter/lib/frameworks/express'; * import { CorsFramework } from '@h4ad/serverless-adapter/lib/frameworks/cors'; * * const expressFramework = new ExpressFramework(); * const options: CorsOptions = {}; // customize the options * const framework = new CorsFramework(expressFramework, options); * * export const handler = ServerlessAdapter.new(null) * .setFramework(framework) * // set other configurations and then build * .build(); * ``` * * @breadcrumb Frameworks / CorsFramework * @public */ export class CorsFramework implements FrameworkContract { //#region Constructor /** * Default Constructor */ constructor( protected readonly framework: FrameworkContract, protected readonly options?: CorsFrameworkOptions, ) { this.cachedCorsInstance = cors(this.options); } //#endregion /** * All cors headers that can be added by cors package */ protected readonly corsHeaders: string[] = [ 'Access-Control-Max-Age', 'Access-Control-Expose-Headers', 'Access-Control-Allow-Headers', 'Access-Control-Request-Headers', 'Access-Control-Allow-Credentials', 'Access-Control-Allow-Methods', 'Access-Control-Allow-Origin', ]; /** * The cached instance of cors */ protected readonly cachedCorsInstance: ReturnType; //#region Public Methods /** * {@inheritDoc} */ public sendRequest( app: TApp, request: IncomingMessage, response: ServerResponse, ): void { this.cachedCorsInstance( request, response, this.onCorsNext(app, request, response), ); } //#endregion //#region Protected Methods /** * Handle next execution called by the cors package */ protected onCorsNext( app: TApp, request: IncomingMessage, response: ServerResponse, ): () => void { return () => { this.formatHeaderValuesAddedByCorsPackage(response); const errorOnInvalidOrigin = getDefaultIfUndefined( this.options?.forbiddenOnInvalidOriginOrMethod, true, ); if (errorOnInvalidOrigin) { const allowedOrigin = response.getHeader('access-control-allow-origin'); const isInvalidOrigin = this.isInvalidOriginOrMethodIsNotAllowed( request, allowedOrigin, ); if (isInvalidOrigin) { response.statusCode = 403; response.setHeader('Content-Type', 'text/plain'); response.end( [ 'Invalid HTTP Access Control (CORS) request:', ` Origin: ${request.headers.origin}`, ` Method: ${request.method}`, ].join('\n'), ); return; } } this.framework.sendRequest(app, request, response); }; } /** * Format the headers to be standardized with the rest of the library, such as ApiGatewayV2. * Also, some frameworks don't support headers as an array, so we need to format the values. */ protected formatHeaderValuesAddedByCorsPackage( response: ServerResponse, ): void { for (const corsHeader of this.corsHeaders) { const value = response.getHeader(corsHeader); if (value === undefined) continue; response.removeHeader(corsHeader); response.setHeader( corsHeader.toLowerCase(), Array.isArray(value) ? value.join(',') : value, ); } } /** * Check if the origin is invalid or if the method is not allowed. * Highly inspired by [access-control](https://github.com/primus/access-control/blob/master/index.js#L95-L115) */ protected isInvalidOriginOrMethodIsNotAllowed( request: IncomingMessage, allowedOrigin: number | string | string[] | undefined, ): boolean { if (!allowedOrigin) return true; if ( !!request.headers.origin && allowedOrigin !== '*' && request.headers.origin !== allowedOrigin ) return true; const notPermitedInMethods = this.options && Array.isArray(this.options.methods) && this.options.methods.every( m => m.toLowerCase() !== request.method?.toLowerCase(), ); const differentMethod = this.options && typeof this.options.methods === 'string' && this.options.methods .split(',') .every(m => m.trim().toLowerCase() !== request.method?.toLowerCase()); if (this.options?.methods && (notPermitedInMethods || differentMethod)) return true; return false; } //#endregion } ================================================ FILE: src/frameworks/cors/index.ts ================================================ export * from './cors.framework'; ================================================ FILE: src/frameworks/deepkit/http-deepkit.framework.ts ================================================ //#region import type { ServerResponse } from 'http'; import { HttpKernel, HttpResponse, RequestBuilder } from '@deepkit/http'; import type { FrameworkContract } from '../../contracts'; import { getFlattenedHeadersMap } from '../../core'; import { ServerlessRequest } from '../../network'; //#endregion /** * The framework that forwards requests to express handler * * @breadcrumb Frameworks / HttpDeepkitFramework * @public */ export class HttpDeepkitFramework implements FrameworkContract { /** * {@inheritDoc} */ public sendRequest( app: HttpKernel, request: ServerlessRequest, response: ServerResponse, ): void { const flattenedHeaders = getFlattenedHeadersMap(request.headers); let requestBuilder = new RequestBuilder( request.url!, request.method, ).headers(flattenedHeaders); if (request.body) { requestBuilder = Buffer.isBuffer(request.body) ? requestBuilder.body(request.body) : requestBuilder.body(Buffer.from(request.body)); } const httpRequest = requestBuilder.build(); app.handleRequest(httpRequest, response as HttpResponse); } } ================================================ FILE: src/frameworks/deepkit/index.ts ================================================ export * from './http-deepkit.framework'; ================================================ FILE: src/frameworks/express/express.framework.ts ================================================ //#region import type { IncomingMessage, ServerResponse } from 'http'; import type { Express } from 'express'; import type { FrameworkContract } from '../../contracts'; //#endregion /** * The framework that forwards requests to express handler * * @breadcrumb Frameworks / ExpressFramework * @public */ export class ExpressFramework implements FrameworkContract { /** * {@inheritDoc} */ public sendRequest( app: Express, request: IncomingMessage, response: ServerResponse, ): void { app(request, response); } } ================================================ FILE: src/frameworks/express/index.ts ================================================ export * from './express.framework'; ================================================ FILE: src/frameworks/fastify/fastify.framework.ts ================================================ //#region Imports import type { IncomingMessage, ServerResponse } from 'http'; import type { FastifyInstance } from 'fastify'; import type { FrameworkContract } from '../../contracts'; //#endregion /** * The framework that forwards requests to fastify handler * * @breadcrumb Frameworks / FastifyFramework * @public */ export class FastifyFramework implements FrameworkContract { /** * {@inheritDoc} */ public sendRequest( app: FastifyInstance, request: IncomingMessage, response: ServerResponse, ): void { // ref: https://www.fastify.io/docs/latest/Guides/Serverless/#implement-and-export-the-function app.ready().then(() => app.server.emit('request', request, response)); } } ================================================ FILE: src/frameworks/fastify/helpers/no-op-content-parser.ts ================================================ //#region Imports import type { FastifyInstance } from 'fastify'; //#endregion /** * Just return the current body as it was parsed. * * @remarks This function is intended to be used with BodyParserFrameworks. * * @param app - The instance of fastify * @param contentType - The content type to be anuled * * @breadcrumb Frameworks / FastifyFramework / Helpers * @public */ export function setNoOpForContentType( app: FastifyInstance, contentType: string, ): void { app.addContentTypeParser(contentType, (_, req, done) => { return done(null, (req as any).body); }); } ================================================ FILE: src/frameworks/fastify/index.ts ================================================ export * from './fastify.framework'; ================================================ FILE: src/frameworks/hapi/hapi.framework.ts ================================================ //#region Imports import type { IncomingMessage, ServerResponse } from 'http'; import type { Server } from '@hapi/hapi'; import type { FrameworkContract } from '../../contracts'; //#endregion /** * The framework that forwards requests to hapi handler * * @breadcrumb Frameworks / HapiFramework * @public */ export class HapiFramework implements FrameworkContract { /** * {@inheritDoc} */ public sendRequest( app: Server, request: IncomingMessage, response: ServerResponse, ): void { const httpServer: any = app.listener; httpServer._events.request(request, response); } } ================================================ FILE: src/frameworks/hapi/index.ts ================================================ export * from './hapi.framework'; ================================================ FILE: src/frameworks/koa/index.ts ================================================ export * from './koa.framework'; ================================================ FILE: src/frameworks/koa/koa.framework.ts ================================================ //#region Imports import type { IncomingMessage, ServerResponse } from 'http'; import type Application from 'koa'; import type { FrameworkContract } from '../../contracts'; //#endregion /** * The framework that forwards requests to koa handler * * @breadcrumb Frameworks / KoaFramework * @public */ export class KoaFramework implements FrameworkContract { /** * {@inheritDoc} */ public sendRequest( app: Application, request: IncomingMessage, response: ServerResponse, ): void { app.callback()(request, response); } } ================================================ FILE: src/frameworks/lazy/index.ts ================================================ export * from './lazy.framework'; ================================================ FILE: src/frameworks/lazy/lazy.framework.ts ================================================ //#region Imports import type { IncomingMessage, ServerResponse } from 'http'; import type { FrameworkContract } from '../../contracts'; import { type ILogger, createDefaultLogger } from '../../core'; //#endregion /** * The framework that asynchronously instantiates your application and forwards the request to the framework as quickly as possible. * * @example * ```typescript * import express from 'express'; * import { ServerlessAdapter } from '@h4ad/serverless-adapter'; * import { ExpressFramework } from '@h4ad/serverless-adapter/lib/frameworks/express'; * import { LazyFramework } from '@h4ad/serverless-adapter/lib/frameworks/lazy'; * * const expressFramework = new ExpressFramework(); * const factory = async () => { * const app = express(); * * // do some asynchronous stuffs like create the database; * await new Promise(resolve => setTimeout(resolve, 100); * * return app; * }; * const framework = new LazyFramework(expressFramework, factory); * * export const handler = ServerlessAdapter.new(null) * .setFramework(framework) * // set other configurations and then build * .build(); * ``` * * @breadcrumb Frameworks / LazyFramework * @public */ export class LazyFramework implements FrameworkContract { //#region Constructor /** * Default Constructor */ constructor( protected readonly framework: FrameworkContract, protected readonly factory: () => Promise, protected readonly logger: ILogger = createDefaultLogger(), ) { this.delayedFactory = Promise.resolve() .then(() => factory()) .then(app => { this.cachedApp = app; }) .catch((error: Error) => { // deal with the error only when receive some request // to be able to return some message to user this.logger.error( 'SERVERLESS_ADAPTER:LAZY_FRAMEWORK: An error occours during the creation of your app.', ); this.logger.error(error); }); } //#endregion //#region Protected Properties /** * The cached version of the app */ protected cachedApp?: TApp; /** * The delayed factory to create an instance of the app */ protected readonly delayedFactory: Promise; //#endregion //#region Public Methods /** * {@inheritDoc} */ public sendRequest( _app: null, request: IncomingMessage, response: ServerResponse, ): void { if (this.cachedApp) return this.framework.sendRequest(this.cachedApp, request, response); this.delayedFactory.then(() => { if (!this.cachedApp) { return response.emit( 'error', new Error( 'SERVERLESS_ADAPTER:LAZY_FRAMEWORK: The instance of the app returned by the factory is not valid, see the logs to learn more.', ), ); } return this.framework.sendRequest(this.cachedApp, request, response); }); } //#endregion } ================================================ FILE: src/frameworks/polka/index.ts ================================================ export * from './polka.framework'; ================================================ FILE: src/frameworks/polka/polka.framework.ts ================================================ //#region Imports import type { IncomingMessage, ServerResponse } from 'http'; import polka, { type Polka } from 'polka'; import type { FrameworkContract } from '../../contracts'; //#endregion /** * The framework that forwards requests to polka handler * * @breadcrumb Frameworks / PolkaFramework * @public */ export class PolkaFramework implements FrameworkContract { /** * {@inheritDoc} */ sendRequest( app: Polka, request: IncomingMessage, response: ServerResponse, ): void { app.handler(request as polka.Request, response); } } ================================================ FILE: src/frameworks/trpc/index.ts ================================================ export * from './trpc.framework'; ================================================ FILE: src/frameworks/trpc/trpc.framework.ts ================================================ //#region import type { IncomingMessage, ServerResponse } from 'http'; import type { AnyRouter, DataTransformer } from '@trpc/server'; import { type NodeHTTPCreateContextFn, type NodeHTTPCreateContextFnOptions, type NodeHTTPHandlerOptions, nodeHTTPRequestHandler, } from '@trpc/server/adapters/node-http'; import type { SingleValueHeaders } from '../../@types'; import type { FrameworkContract } from '../../contracts'; import { getDefaultIfUndefined, getFlattenedHeadersMap } from '../../core'; //#endregion /** * The transformer that is responsible to transform buffer's input to javascript objects * * @deprecated You should use {@link JsonBodyParserFramework} instead, is more reliable and enable you to use transformer of trpc to other things. * @breadcrumb Frameworks / TrpcFramework * @public */ export class BufferToJSObjectTransformer implements DataTransformer { /** * Deserialize unknown values to javascript objects * * @param value - The value to be deserialized */ public deserialize(value?: unknown): any { if (value instanceof Buffer) return JSON.parse(value.toString('utf-8')); return value; } /** * The value to be serialized, do nothing because tRPC can handle. * * @param value - The value to be serialized */ public serialize(value: any): any { return value; } } /** * The context created by this library that allows getting some information from the request and setting the status and header of the response. * * @breadcrumb Frameworks / TrpcFramework * @public */ export interface TrpcAdapterBaseContext { /** * The request object that will be forward to your app */ request: IncomingMessage; /** * The response object that will be forward to your app to output the response */ response: ServerResponse; /** * The method to set response status. * * @param statusCode - The response status that you want */ setStatus(statusCode: number): void; /** * The method to set some header in the response * * @param name - The name of the header * @param value - The value to be set in the header */ setHeader(name: string, value: number | string): void; /** * The method to remove some header from the response * * @param name - The name of the header */ removeHeader(name: string): void; /** * The method to return the value of some header from the request * * @param name - The name of the header */ getHeader(name: string): string | undefined; /** * The method to return the request headers */ getHeaders(): SingleValueHeaders; /** * The method to return user's address */ getIp(): string | undefined; /** * The method to return the URL called */ getUrl(): string | undefined; /** * The method to return the HTTP Method in the request */ getMethod(): string | undefined; } /** * This is the context merged between {@link TrpcAdapterBaseContext} and the {@link TContext} that you provided. * * This context will be merged with the context you created with `createContext` inside {@link TrpcFrameworkOptions}. * So to make the type work, just send the properties you've added inside {@link TContext}. * * @example * ```typescript * type MyCustomContext = { user: { name: string } }; * type TrpcContext = TrpcAdapterContext; // your final context type to put inside trpc.router * ``` * * @breadcrumb Frameworks / TrpcFramework * @public */ export type TrpcAdapterContext = TContext & TrpcAdapterBaseContext; /** * The options to customize the {@link TrpcFramework} * * @breadcrumb Frameworks / TrpcFramework * @public */ export type TrpcFrameworkOptions = Omit< NodeHTTPHandlerOptions, 'router' | 'createContext' > & { createContext?: ( opts: NodeHTTPCreateContextFnOptions, ) => | Omit | Promise>; }; /** * The framework that forwards requests to TRPC handler * * @breadcrumb Frameworks / TrpcFramework * @public */ export class TrpcFramework< TContext extends TrpcAdapterBaseContext, TRouter extends AnyRouter = AnyRouter, > implements FrameworkContract { //#region Constructor /** * Default constructor */ constructor(protected readonly options?: TrpcFrameworkOptions) {} //#endregion //#region Public Methods /** * {@inheritDoc} */ public sendRequest( app: TRouter, request: IncomingMessage, response: ServerResponse, ): void { const endpoint = this.getSafeUrlForTrpc(request); nodeHTTPRequestHandler({ req: request, res: response, path: endpoint, router: app, ...this.options, createContext: createContextOptions => this.mergeDefaultContextWithOptionsContext(createContextOptions), }); } //#endregion //#region Protected Methods /** * Get safe url that can be used inside Trpc. * * @example * ```typescript * const url = getSafeUrlForTrpc('/users?input=hello'); * console.log(url); // users * ``` * * @param request - The request object that will be forward to your app */ protected getSafeUrlForTrpc(request: IncomingMessage): string { let url = request.url!; if (url.startsWith('/')) url = url.slice(1); if (url.includes('?')) url = url.split('?')[0]; return url; } /** * Merge the default context ({@link TrpcAdapterContext}) with the context created by the user. * * @param createContextOptions - The options sent by trpc to create the context */ protected mergeDefaultContextWithOptionsContext( createContextOptions: NodeHTTPCreateContextFnOptions< IncomingMessage, ServerResponse >, ): TContext | Promise { const createContextFromOptions: NodeHTTPCreateContextFn< AnyRouter, IncomingMessage, ServerResponse > = getDefaultIfUndefined( this.options?.createContext, () => undefined as unknown as Omit, ); const resolvedContext = createContextFromOptions(createContextOptions); if (resolvedContext && resolvedContext.then) { return resolvedContext.then(context => this.wrapResolvedContextWithDefaultContext( context, createContextOptions, ), ); } return this.wrapResolvedContextWithDefaultContext( resolvedContext, createContextOptions, ); } /** * Wraps the resolved context from the {@link TrpcFrameworkOptions} created with `createContext` and merge * with the {@link TrpcAdapterContext} generated by the library. * * @param resolvedContext - The context created with `createContext` inside {@link TrpcFrameworkOptions} * @param createContextOptions - The options sent by trpc to create the context */ protected wrapResolvedContextWithDefaultContext( resolvedContext: TContext, createContextOptions: NodeHTTPCreateContextFnOptions< IncomingMessage, ServerResponse >, ): TContext { const request = createContextOptions.req; const response = createContextOptions.res; return { ...resolvedContext, request, response, getUrl: () => request.url, getMethod: () => request.method, getHeaders: () => getFlattenedHeadersMap(request.headers, ',', true), setHeader: (header, value) => { response.setHeader(header, value); }, removeHeader: header => { response.removeHeader(header); }, getHeader: (header: string) => { return getFlattenedHeadersMap(request.headers, ',', true)[ header.toLowerCase() ]; }, setStatus: (statusCode: number) => { response.statusCode = statusCode; // force undefined to get default message for the status code // ref: https://nodejs.org/dist/latest-v16.x/docs/api/http.html#responsestatusmessage response.statusMessage = undefined as any; }, getIp: () => request.connection.remoteAddress, }; } //#endregion } ================================================ FILE: src/handlers/aws/aws-stream.handler.ts ================================================ //#region Imports import { Writable } from 'node:stream'; import { inspect } from 'node:util'; import type { APIGatewayProxyEventV2, Context } from 'aws-lambda'; import type { APIGatewayProxyStructuredResultV2 } from 'aws-lambda/trigger/api-gateway-proxy'; import type { BinarySettings } from '../../@types'; import type { AdapterContract, AdapterRequest, FrameworkContract, ResolverContract, ServerlessHandler, } from '../../contracts'; import { BaseHandler, type ILogger, getFlattenedHeadersMap, setCurrentInvoke, waitForStreamComplete, } from '../../core'; import { ServerlessRequest, ServerlessStreamResponse } from '../../network'; //#endregion /** * @breadcrumb Handlers / AwsStreamHandler * @public */ export type AWSResponseStream = Writable; /** * @breadcrumb Handlers / AwsStreamHandler * @public */ export type AWSStreamResponseMetadata = Pick< APIGatewayProxyStructuredResultV2, 'statusCode' | 'headers' | 'cookies' >; /** * @breadcrumb Handlers / AwsStreamHandler * @public */ declare const awslambda: { streamifyResponse: ( handler: ( event: APIGatewayProxyEventV2, response: AWSResponseStream, context: Context, ) => Promise, ) => any; HttpResponseStream: { from: ( stream: AWSResponseStream, httpResponseMetadata: AWSStreamResponseMetadata, ) => AWSResponseStream; }; }; /** * The interface that customizes the {@link AwsStreamHandler} * * @breadcrumb Handlers / AwsStreamHandler * @public */ export type AwsStreamHandlerOptions = { /** * Set the value of the property `callbackWaitsForEmptyEventLoop`, you can set to `false` to fix issues with long execution due to not cleaning the event loop ([ref](https://github.com/H4ad/serverless-adapter/issues/264)). * In the next release, this value will be changed to `false`. * * @defaultValue undefined */ callbackWaitsForEmptyEventLoop?: boolean; }; /** * The interface that describes the internal context used by the {@link AwsStreamHandler} * * @breadcrumb Handlers / AwsStreamHandler * @public */ export type AWSStreamContext = { /** * The response stream provided by the serverless */ response: AWSResponseStream; /** * The context provided by the serverless */ context: Context; }; /** * The class that implements a default serverless handler consisting of a function with event, context and callback parameters respectively * * @breadcrumb Handlers / AwsStreamHandler * @public */ export class AwsStreamHandler extends BaseHandler< TApp, APIGatewayProxyEventV2, AWSStreamContext, void, AWSStreamResponseMetadata, void > { //#region Constructor /** * Construtor padrão */ constructor(private readonly options?: AwsStreamHandlerOptions) { super(); } //#endregion //#region Public Methods /** * {@inheritDoc} */ public getHandler( app: TApp, framework: FrameworkContract, adapters: AdapterContract< APIGatewayProxyEventV2, AWSStreamContext, AWSStreamResponseMetadata >[], _resolverFactory: ResolverContract< unknown, unknown, unknown, unknown, unknown >, binarySettings: BinarySettings, respondWithErrors: boolean, log: ILogger, ): ServerlessHandler> { return awslambda.streamifyResponse(async (event, response, context) => { if (this.options?.callbackWaitsForEmptyEventLoop !== undefined) { // TODO(h4ad): Set the following property to false by default context.callbackWaitsForEmptyEventLoop = this.options.callbackWaitsForEmptyEventLoop; } const streamContext = { response, context }; this.onReceiveRequest( log, event, streamContext, binarySettings, respondWithErrors, ); const adapter = this.getAdapterByEventAndContext( event, streamContext, adapters, log, ); this.onResolveAdapter(log, adapter); setCurrentInvoke({ event, context }); await this.forwardRequestToFramework( app, framework, event, streamContext, adapter, binarySettings, log, ); }); } //#endregion //#region Hooks /** * The hook executed on receive a request, before the request is being processed * * @param log - The instance of logger * @param event - The event sent by serverless * @param context - The context sent by serverless * @param binarySettings - The binary settings * @param respondWithErrors - Indicates whether the error stack should be included in the response or not */ protected onReceiveRequest( log: ILogger, event: APIGatewayProxyEventV2, context: AWSStreamContext, binarySettings: BinarySettings, respondWithErrors: boolean, ): void { log.debug('SERVERLESS_ADAPTER:PROXY', () => ({ event, context: inspect(context, { depth: null }), binarySettings, respondWithErrors, })); } /** * The hook executed after resolve the adapter that will be used to handle the request and response * * @param log - The instance of logger * @param adapter - The adapter resolved */ protected onResolveAdapter( log: ILogger, adapter: AdapterContract< APIGatewayProxyEventV2, AWSStreamContext, AWSStreamResponseMetadata >, ): void { log.debug( 'SERVERLESS_ADAPTER:RESOLVED_ADAPTER_NAME: ', adapter.getAdapterName(), ); } /** * The hook executed after resolves the request values that will be sent to the framework * * @param log - The instance of logger * @param requestValues - The request values returned by the adapter */ protected onResolveRequestValues( log: ILogger, requestValues: AdapterRequest, ): void { log.debug( 'SERVERLESS_ADAPTER:FORWARD_REQUEST_TO_FRAMEWORK:REQUEST_VALUES', () => ({ requestValues: { ...requestValues, body: requestValues.body?.toString(), }, }), ); } /** * The hook executed before sending response to the serverless with response from adapter * * @param log - The instance of logger * @param successResponse - The success response resolved by the adapter */ protected onForwardResponseAdapterResponse( log: ILogger, successResponse: AWSStreamResponseMetadata, ) { log.debug('SERVERLESS_ADAPTER:FORWARD_RESPONSE:EVENT_SOURCE_RESPONSE', { successResponse, }); } //#endregion //#region Protected Methods /** * The function to forward the event to the framework * * @param app - The instance of the app (express, hapi, etc...) * @param framework - The framework that will process requests * @param event - The event sent by serverless * @param context - The context sent by serverless * @param adapter - The adapter resolved to this event * @param _binarySettings - The binary settings * @param log - The instance of logger */ protected async forwardRequestToFramework( app: TApp, framework: FrameworkContract, event: APIGatewayProxyEventV2, context: AWSStreamContext, adapter: AdapterContract< APIGatewayProxyEventV2, AWSStreamContext, AWSStreamResponseMetadata >, _binarySettings: BinarySettings, log: ILogger, ): Promise { const requestValues = adapter.getRequest(event, context, log); this.onResolveRequestValues(log, requestValues); const request = new ServerlessRequest({ method: requestValues.method, headers: requestValues.headers, body: requestValues.body, remoteAddress: requestValues.remoteAddress, url: requestValues.path, }); const response = new ServerlessStreamResponse({ method: requestValues.method, onReceiveHeaders: (status, headers) => { const flattenedHeaders = getFlattenedHeadersMap(headers); const awsMetadata: AWSStreamResponseMetadata = { statusCode: status, headers: flattenedHeaders, }; const cookies = headers['set-cookie']; if (cookies) { awsMetadata.cookies = Array.isArray(cookies) ? cookies : [cookies]; delete headers['set-cookie']; delete flattenedHeaders['set-cookie']; } this.onForwardResponseAdapterResponse(log, awsMetadata); const finalResponse = awslambda.HttpResponseStream.from( context.response, awsMetadata, ); // We must call write with an empty string to trigger the awsMetadata to be sent // https://github.com/aws/aws-lambda-nodejs-runtime-interface-client/blob/2ce88619fd176a5823bc5f38c5484d1cbdf95717/src/HttpResponseStream.js#L22 finalResponse.write(''); return finalResponse; }, log, }); framework.sendRequest(app, request, response); log.debug( 'SERVERLESS_ADAPTER:FORWARD_REQUEST_TO_FRAMEWORK:WAITING_STREAM_COMPLETE', ); await waitForStreamComplete(response); log.debug( 'SERVERLESS_ADAPTER:FORWARD_REQUEST_TO_FRAMEWORK:STREAM_COMPLETE', ); context.response.end(); } //#endregion } ================================================ FILE: src/handlers/aws/index.ts ================================================ export * from './aws-stream.handler'; ================================================ FILE: src/handlers/azure/azure.handler.ts ================================================ /* eslint-disable @typescript-eslint/unbound-method */ //#region Imports import type { Context } from '@azure/functions'; import type { BinarySettings } from '../../@types'; import type { AdapterContract, FrameworkContract, ResolverContract, ServerlessHandler, } from '../../contracts'; import { type ILogger, getDefaultIfUndefined, isInternalLogger, } from '../../core'; import { DefaultHandler } from '../default'; //#endregion /** * The options to customize {@link AzureHandler} * * @breadcrumb Handlers / AzureHandler * @public */ export interface AzureHandlerOptions { /** * Indicates to use the context log instead console.log when logger is internal (created by the library) * * @defaultValue true */ useContextLogWhenInternalLogger: boolean; } /** * The class that implements a serverless handler for Azure Function. * * When you don't specify a custom logger, the {@link Context} logger is used instead. * * @breadcrumb Handlers / AzureHandler * @public */ export class AzureHandler< TApp, TEvent, TCallback, TResponse, TReturn, > extends DefaultHandler { //#region Constructor /** * Default Constructor */ constructor(protected readonly options?: AzureHandlerOptions) { super(); } //#endregion //#region Public Methods /** * {@inheritDoc} */ public override getHandler( app: TApp, framework: FrameworkContract, adapters: AdapterContract[], resolverFactory: ResolverContract< TEvent, Context, TCallback, TResponse, TReturn >, binarySettings: BinarySettings, respondWithErrors: boolean, log: ILogger, ): ServerlessHandler { return (context: Context, event: TEvent) => { const useContextLogWhenInternalLogger = getDefaultIfUndefined( this.options?.useContextLogWhenInternalLogger, true, ); if (isInternalLogger(log) && useContextLogWhenInternalLogger) log = this.createLoggerFromContext(context); const defaultHandler = super.getHandler( app, framework, adapters, resolverFactory, binarySettings, respondWithErrors, log, ); // remove this from context // because user can mess it-up the things // @ts-ignore delete context.done; delete context.res; return defaultHandler(event, context, undefined); }; } //#endregion //#region Protected Methods /** * Get the {@link ILogger} instance from logger of the context * * @param context - The Azure Context */ protected createLoggerFromContext(context: Context): ILogger { return { error: context.log.error, debug: context.log.verbose, verbose: context.log.verbose, info: context.log.info, warn: context.log.warn, }; } //#endregion } ================================================ FILE: src/handlers/azure/index.ts ================================================ export * from './azure.handler'; ================================================ FILE: src/handlers/base/index.ts ================================================ export * from './raw-request'; ================================================ FILE: src/handlers/base/raw-request.ts ================================================ //#region Imports import type { IncomingMessage, ServerResponse } from 'http'; import type { FrameworkContract } from '../../contracts'; import { ServerlessRequest } from '../../network'; import { getEventBodyAsBuffer, getFlattenedHeadersMap } from '../../core'; //#endregion /** * The class that expose some methods to be used to get raw request from Express HTTP Request * * @breadcrumb Handlers / Base / RawRequest * @public */ export abstract class RawRequest { //#region Protected Methods /** * The callback to when receive some request from external source * * @param app - The instance of the app * @param framework - The framework for the app */ protected onRequestCallback( app: TApp, framework: FrameworkContract, ): (req: IncomingMessage, res: ServerResponse) => void | Promise { return (request: IncomingMessage, response: ServerResponse) => { const customRequest = this.getRequestFromExpressRequest(request); return framework.sendRequest(app, customRequest, response); }; } /** * Not sure why they think using Express instance with prebuilt middlewares was a good idea, but Firebase/GCP * decides to use `Express` and `body-parser` by default, so you don't get a raw request, instead, you get a modified version by * Express and also with the body parsed by `body-parser`. * If you use NestJS or Express it's awesome, but for the rest of the frameworks it's terrible! * That's why I have this method, just to try and create a raw request to be used and passed to the frameworks so they can handle the request * as if they received the request from the native http module. * * @param request - The Express request */ protected getRequestFromExpressRequest( request: IncomingMessage, ): ServerlessRequest { const expressRequestParsed = request as unknown as { body: object | Buffer; }; const headers = getFlattenedHeadersMap(request.headers, ',', true); const remoteAddress = headers['x-forwarded-for']; let body: Buffer | undefined; if ( expressRequestParsed.body && typeof expressRequestParsed.body === 'object' ) { const jsonContent = JSON.stringify(expressRequestParsed.body); const [bufferBody, contentLength] = getEventBodyAsBuffer( jsonContent, false, ); body = bufferBody; headers['content-length'] = String(contentLength); } return new ServerlessRequest({ method: request.method!, url: request.url!, body, headers, remoteAddress, }); } //#endregion } ================================================ FILE: src/handlers/default/default.handler.ts ================================================ //#region Imports import util from 'node:util'; import type { BinarySettings, SingleValueHeaders } from '../../@types'; import type { AdapterContract, AdapterRequest, FrameworkContract, ResolverContract, ServerlessHandler, } from '../../contracts'; import { BaseHandler, type ILogger, isBinary, setCurrentInvoke, waitForStreamComplete, } from '../../core'; import { ServerlessResponse } from '../../network'; //#endregion /** * The class that implements a default serverless handler consisting of a function with event, context and callback parameters respectively * * @breadcrumb Handlers / DefaultHandler * @public */ export class DefaultHandler< TApp, TEvent, TContext, TCallback, TResponse, TReturn, > extends BaseHandler { //#region Public Methods /** * {@inheritDoc} */ public getHandler( app: TApp, framework: FrameworkContract, adapters: AdapterContract[], resolverFactory: ResolverContract< TEvent, TContext, TCallback, TResponse, TReturn >, binarySettings: BinarySettings, respondWithErrors: boolean, log: ILogger, ): ServerlessHandler { return (event: TEvent, context: TContext, callback?: TCallback) => { this.onReceiveRequest( log, event, context, binarySettings, respondWithErrors, ); const adapter = this.getAdapterByEventAndContext( event, context, adapters, log, ); this.onResolveAdapter(log, adapter); setCurrentInvoke({ event, context }); const resolver = resolverFactory.createResolver({ event, context, callback, log, respondWithErrors, adapter, }); return resolver.run(() => this.forwardRequestToFramework( app, framework, event, context, adapter, binarySettings, log, ), ); }; } //#endregion //#region Hooks /** * The hook executed on receive a request, before the request is being processed * * @param log - The instance of logger * @param event - The event sent by serverless * @param context - The context sent by serverless * @param binarySettings - The binary settings * @param respondWithErrors - Indicates whether the error stack should be included in the response or not */ protected onReceiveRequest( log: ILogger, event: TEvent, context: TContext, binarySettings: BinarySettings, respondWithErrors: boolean, ): void { log.debug('SERVERLESS_ADAPTER:PROXY', () => ({ event: util.inspect(event, { depth: null }), context: util.inspect(context, { depth: null }), binarySettings, respondWithErrors, })); } /** * The hook executed after resolve the adapter that will be used to handle the request and response * * @param log - The instance of logger * @param adapter - The adapter resolved */ protected onResolveAdapter( log: ILogger, adapter: AdapterContract, ): void { log.debug( 'SERVERLESS_ADAPTER:RESOLVED_ADAPTER_NAME: ', adapter.getAdapterName(), ); } /** * The hook executed after resolves the request values that will be sent to the framework * * @param log - The instance of logger * @param requestValues - The request values returned by the adapter */ protected onResolveRequestValues( log: ILogger, requestValues: AdapterRequest, ): void { log.debug( 'SERVERLESS_ADAPTER:FORWARD_REQUEST_TO_FRAMEWORK:REQUEST_VALUES', () => ({ requestValues: { ...requestValues, body: requestValues.body?.toString(), }, }), ); } /** * The hook executed after handling the response sent by the framework * * @param log - The instance of logger * @param response - The response sent by the framework */ protected onResolveForwardedResponseToFramework( log: ILogger, response: ServerlessResponse, ): void { log.debug( 'SERVERLESS_ADAPTER:FORWARD_REQUEST_TO_FRAMEWORK:RESPONSE', () => ({ response, }), ); } /** * The hook executed before sending response to the serverless * * @param log - The instance of logger * @param statusCode - The status code of the response * @param body - The body of the response * @param headers - The headers of the response * @param isBase64Encoded - Indicates whether the response was encoded as binary or not */ protected onForwardResponse( log: ILogger, statusCode: number, body: string, headers: SingleValueHeaders, isBase64Encoded: boolean, ) { log.debug( 'SERVERLESS_ADAPTER:FORWARD_RESPONSE:EVENT_SOURCE_RESPONSE_PARAMS', () => ({ statusCode, body, headers, isBase64Encoded, }), ); } /** * The hook executed before sending response to the serverless with response from adapter * * @param log - The instance of logger * @param successResponse - The success response resolved by the adapter * @param body - The body of the response sent by the framework */ protected onForwardResponseAdapterResponse( log: ILogger, successResponse: TResponse, body: string, ) { log.debug( 'SERVERLESS_ADAPTER:FORWARD_RESPONSE:EVENT_SOURCE_RESPONSE', () => ({ successResponse: util.inspect(successResponse, { depth: null }), body, }), ); } //#endregion //#region Protected Methods /** * The function to forward the event to the framework * * @param app - The instance of the app (express, hapi, etc...) * @param framework - The framework that will process requests * @param event - The event sent by serverless * @param context - The context sent by serverless * @param adapter - The adapter resolved to this event * @param log - The instance of logger * @param binarySettings - The binary settings */ protected async forwardRequestToFramework( app: TApp, framework: FrameworkContract, event: TEvent, context: TContext, adapter: AdapterContract, binarySettings: BinarySettings, log: ILogger, ): Promise { const requestValues = adapter.getRequest(event, context, log); this.onResolveRequestValues(log, requestValues); const [request, response] = this.getServerlessRequestResponseFromAdapterRequest(requestValues); framework.sendRequest(app, request, response); await waitForStreamComplete(response); this.onResolveForwardedResponseToFramework(log, response); return this.forwardResponse(event, response, adapter, binarySettings, log); } /** * The function to forward the response back to the serverless * * @param event - The event sent by serverless * @param response - The response of the framework * @param adapter - The adapter resolved to this event * @param binarySettings - The binary settings * @param log - The instance of logger */ protected forwardResponse( event: TEvent, response: ServerlessResponse, adapter: AdapterContract, binarySettings: BinarySettings, log: ILogger, ): TResponse { const statusCode = response.statusCode; const headers = ServerlessResponse.headers(response); const isBase64Encoded = isBinary(headers, binarySettings); const encoding = isBase64Encoded ? 'base64' : 'utf8'; const body = ServerlessResponse.body(response).toString(encoding); const logBody = isBase64Encoded ? '[BASE64_ENCODED]' : body; this.onForwardResponse(log, statusCode, logBody, headers, isBase64Encoded); const successResponse = adapter.getResponse({ event, statusCode, body, headers, isBase64Encoded, response, log, }); this.onForwardResponseAdapterResponse(log, successResponse, logBody); return successResponse; } //#endregion } ================================================ FILE: src/handlers/default/index.ts ================================================ export * from './default.handler'; ================================================ FILE: src/handlers/digital-ocean/digital-ocean.handler.ts ================================================ /* eslint-disable @typescript-eslint/unbound-method */ //#region Imports import type { BinarySettings } from '../../@types'; import type { DigitalOceanHttpEvent } from '../../@types/digital-ocean'; import type { AdapterContract, FrameworkContract, ResolverContract, ServerlessHandler, } from '../../contracts'; import type { ILogger } from '../../core'; import { DefaultHandler } from '../default'; //#endregion /** * The class that implements a serverless handler for Digital Ocean Functions. * * @breadcrumb Handlers / DigitalOceanHandler * @public */ export class DigitalOceanHandler< TApp, TEvent, TResponse, TReturn, > extends DefaultHandler { /** * {@inheritDoc} */ public override getHandler( app: TApp, framework: FrameworkContract, adapters: AdapterContract[], resolverFactory: ResolverContract, binarySettings: BinarySettings, respondWithErrors: boolean, log: ILogger, ): ServerlessHandler { const defaultHandler = super.getHandler( app, framework, adapters, resolverFactory, binarySettings, respondWithErrors, log, ); return (event: DigitalOceanHttpEvent) => defaultHandler(event, undefined, undefined); } } ================================================ FILE: src/handlers/digital-ocean/index.ts ================================================ export * from './digital-ocean.handler'; ================================================ FILE: src/handlers/firebase/http-firebase-v2.handler.ts ================================================ //#region Imports import { IncomingMessage, ServerResponse } from 'node:http'; // eslint-disable-next-line import/no-unresolved import { https } from 'firebase-functions/v2'; import type { FrameworkContract, HandlerContract } from '../../contracts'; import { RawRequest } from '../base'; //#endregion /** * The HTTP handler that is exposed when you use {@link HttpFirebaseV2Handler}. * * @breadcrumb Handlers / HttpFirebaseHandler * @public */ export type FirebaseHttpHandler = ( request: IncomingMessage, response: ServerResponse, ) => void | Promise; /** * The class that implements a handler for Firebase Https Events * * @remarks Read more about Https Events {@link https://firebase.google.com/docs/functions/http-events | here} * * @breadcrumb Handlers / HttpFirebaseHandler * @public */ export class HttpFirebaseV2Handler extends RawRequest implements HandlerContract> { //#region Constructor /** * Construtor padrão */ constructor(protected readonly options?: https.HttpsOptions) { super(); } //#endregion //#region Public Methods /** * {@inheritDoc} */ public getHandler( app: TApp, framework: FrameworkContract, ): FirebaseHttpHandler { if (this.options) { return this.onRequestWithOptions( this.options, this.onRequestCallback(app, framework), ); } return https.onRequest( this.onRequestCallback(app, framework), ) as unknown as FirebaseHttpHandler; } //#endregion //#region Protected Method /** * Wrapper method around onRequest for better testability */ protected onRequestWithOptions( options: https.HttpsOptions, callback: ReturnType['onRequestCallback']>, ): FirebaseHttpHandler { return https.onRequest(options, callback) as unknown as FirebaseHttpHandler; } //#endregion } ================================================ FILE: src/handlers/firebase/http-firebase.handler.ts ================================================ //#region Imports // eslint-disable-next-line import/no-unresolved import { type HttpsFunction, https } from 'firebase-functions/v1'; import type { FrameworkContract, HandlerContract } from '../../contracts'; import { RawRequest } from '../base'; //#endregion /** * The class that implements a handler for Firebase Https Events * * @remarks Read more about Https Events {@link https://firebase.google.com/docs/functions/http-events | here} * * @breadcrumb Handlers / HttpFirebaseHandler * @public */ export class HttpFirebaseHandler extends RawRequest implements HandlerContract> { //#region Public Methods /** * {@inheritDoc} */ public getHandler( app: TApp, framework: FrameworkContract, ): HttpsFunction { return https.onRequest(this.onRequestCallback(app, framework)); } //#endregion } ================================================ FILE: src/handlers/firebase/index.ts ================================================ export * from './http-firebase.handler'; export * from './http-firebase-v2.handler'; ================================================ FILE: src/handlers/gcp/gcp.handler.ts ================================================ //#region Imports import type { IncomingMessage, ServerResponse } from 'http'; import { http } from '@google-cloud/functions-framework'; import type { FrameworkContract, HandlerContract } from '../../contracts'; import { RawRequest } from '../base'; //#endregion /** * The class that implements a handler for GCP Http Functions * * @remarks Read more about Http Cloud Function {@link https://cloud.google.com/functions/docs/create-deploy-http-nodejs | here} * * @breadcrumb Handlers / GCPHandler * @public */ export class GCPHandler extends RawRequest implements HandlerContract> { //#region Constructor /** * Default Constructor * * @param name - The name of this function, should be the during deploy. */ constructor(protected readonly name: string) { super(); } //#endregion //#region Public Methods /** * {@inheritDoc} */ public getHandler( app: TApp, framework: FrameworkContract, ): (req: IncomingMessage, res: ServerResponse) => void | Promise { const callback = this.onRequestCallback(app, framework); http(this.name, callback); return callback; } //#endregion } ================================================ FILE: src/handlers/gcp/index.ts ================================================ export * from './gcp.handler'; ================================================ FILE: src/handlers/huawei/http-huawei.handler.ts ================================================ //#region Imports import type { RequestListener } from 'http'; import { type Server, createServer } from 'node:http'; import type { BinarySettings } from '../../@types'; import type { AdapterContract, FrameworkContract, HandlerContract, ResolverContract, ServerlessHandler, } from '../../contracts'; import { type ILogger, getDefaultIfUndefined } from '../../core'; //#endregion /** * The default port that huawei will proxy the request to your framework * * {@link https://support.huaweicloud.com/intl/en-us/ae-ad-1-usermanual-functiongraph/functiongraph_01_1442.html#functiongraph_01_1442__li194597302096 | Reference} * * @breadcrumb Handlers / HttpHuaweiHandler * @public */ export const DEFAULT_HUAWEI_LISTEN_PORT: number = 8000; /** * The options to customize {@link HttpHuaweiHandler} * * @breadcrumb Handlers / HttpHuaweiHandler * @public */ export type HttpHuaweiHandlerOptions = { /** * @defaultValue {@link DEFAULT_HUAWEI_LISTEN_PORT} */ port?: number; /** * The factory to create a http server to use to listen huawei requests */ httpServerFactory?: (requestListener: RequestListener) => Server; }; /** * The class that implements a huawei serverless handler with http function that exposes a http server in specific port. * * In this Handler, you don't need to specific resolver and adapter, so you can use DummyAdapter and DummyResolver instead. * * @see https://support.huaweicloud.com/intl/en-us/ae-ad-1-usermanual-functiongraph/functiongraph_01_1442.html#functiongraph_01_1442__li194597302096 * * @breadcrumb Handlers / HttpHuaweiHandler * @public */ export class HttpHuaweiHandler implements HandlerContract> { //#region Constructor /** * Construtor padrão */ constructor(protected readonly options?: HttpHuaweiHandlerOptions) {} //#endregion //#region Public Methods /** * {@inheritDoc} */ public getHandler( app: TApp, framework: FrameworkContract, _adapters: AdapterContract[], _resolver: ResolverContract, _binarySettings: BinarySettings, _respondWithErrors: boolean, log: ILogger, ): ServerlessHandler> { const requestListener: RequestListener = (req, res) => { framework.sendRequest(app, req, res); }; const server = getDefaultIfUndefined( this.options?.httpServerFactory, this.createHttpServer.bind(this), )(requestListener); const port = getDefaultIfUndefined( this.options?.port, DEFAULT_HUAWEI_LISTEN_PORT, ); server.listen(port, () => { log.debug('SERVERLESS_ADAPTER:PROXY: Server started.'); log.debug( `SERVERLESS_ADAPTER:PROXY: Listening the Huawei Requests in port ${port}`, ); }); return () => { log.debug('SERVERLESS_ADAPTER:PROXY: Disposing the server'); return new Promise((resolve, reject) => { server.close(err => (err ? reject(err) : resolve())); }); }; } //#endregion //#region Protected Methods /** * The method that creates the instance of http server * * @param requestListener - O método que lidará com as requisições recebidas */ protected createHttpServer(requestListener: RequestListener): Server { return createServer(requestListener); } //#endregion } ================================================ FILE: src/handlers/huawei/index.ts ================================================ export * from './http-huawei.handler'; ================================================ FILE: src/index.doc.ts ================================================ export * from './@types'; export * from './@types/huawei'; export * from './adapters/apollo-server'; export * from './adapters/aws'; export * from './adapters/azure'; export * from './adapters/digital-ocean'; export * from './adapters/dummy'; export * from './adapters/huawei'; export * from './contracts'; export * from './core'; export * from './frameworks/apollo-server'; export * from './frameworks/body-parser'; export * from './frameworks/cors'; export * from './frameworks/deepkit'; export * from './frameworks/express'; export * from './frameworks/fastify'; export * from './frameworks/koa'; export * from './frameworks/hapi'; export * from './frameworks/lazy'; export * from './frameworks/polka'; export * from './frameworks/trpc'; export * from './handlers/azure'; export * from './handlers/aws'; export * from './handlers/base'; export * from './handlers/default'; export * from './handlers/digital-ocean'; export * from './handlers/firebase'; export * from './handlers/gcp'; export * from './handlers/huawei'; export * from './network'; export * from './resolvers/aws-context'; export * from './resolvers/callback'; export * from './resolvers/promise'; export * from './resolvers/dummy'; export * from './serverless-adapter'; ================================================ FILE: src/index.ts ================================================ export * from './@types'; export * from './contracts'; export * from './core'; export * from './network'; export * from './serverless-adapter'; ================================================ FILE: src/network/index.ts ================================================ export * from './request'; export * from './response'; export * from './response-stream'; export * from './utils'; ================================================ FILE: src/network/request.ts ================================================ // ATTRIBUTION: https://github.com/dougmoscrop/serverless-http import { IncomingMessage } from 'node:http'; import type { AddressInfo } from 'node:net'; import type { SingleValueHeaders } from '../@types'; import { NO_OP } from '../core'; const HTTPS_PORT = 443; /** * The properties to create a {@link ServerlessRequest} * * @breadcrumb Network / ServerlessRequest * @public */ export interface ServerlessRequestProps { /** * The HTTP Method of the request */ method: string; /** * The URL requested */ url: string; /** * The headers from the event source */ headers: SingleValueHeaders; /** * The body from the event source */ body?: Buffer | Uint8Array; /** * The IP Address from caller */ remoteAddress?: string; } /** * The class that represents an {@link http#IncomingMessage} created by the library to represent an actual request to the framework. * * @breadcrumb Network / ServerlessRequest * @public */ export class ServerlessRequest extends IncomingMessage { constructor({ method, url, headers, body, remoteAddress, }: ServerlessRequestProps) { super({ encrypted: true, readable: true, // credits to @pnkp at https://github.com/CodeGenieApp/serverless-express/pull/692 remoteAddress, address: () => ({ port: HTTPS_PORT }) as AddressInfo, on: NO_OP, removeListener: NO_OP, removeEventListener: NO_OP, end: NO_OP, destroy: NO_OP, } as any); this.statusCode = 200; this.statusMessage = 'OK'; this.complete = true; this.httpVersion = '1.1'; this.httpVersionMajor = 1; this.httpVersionMinor = 1; this.method = method; this.headers = headers; this.body = body; this.url = url; this.ip = remoteAddress; this._read = () => { this.push(body); this.push(null); }; } ip?: string; body?: Buffer | Uint8Array; } ================================================ FILE: src/network/response-stream.ts ================================================ import { ServerResponse } from 'node:http'; import type { Socket } from 'node:net'; import type { Writable } from 'node:stream'; import type { BothValueHeaders } from '../@types'; import { type ILogger, NO_OP, parseHeaders } from '../core'; import { getString } from './utils'; // header or data crlf const crlfBuffer = Buffer.from('\r\n'); const endChunked = '0\r\n\r\n'; const headerEnd = '\r\n\r\n'; const endStatusSeparator = '\r\n'; /** * The properties to create a {@link ServerlessStreamResponse}. * * @breadcrumb Network / ServerlessStreamResponse * @public */ export interface ServerlessStreamResponseProps { /** * The HTTP Method from request */ method?: string; /** * The callback to receive the headers when they are written to the stream * You need to return a writable stream be able to continue writing the response * * @param statusCode - The status code of the response * @param headers - The headers of the response */ onReceiveHeaders: (statusCode: number, headers: BothValueHeaders) => Writable; /** * Instance of the logger */ log: ILogger; } /** * The class that represents a response instance used to send to the framework and wait until the framework finishes processing the request. * This response is specially built to deal with transfer-encoding: chunked * * @breadcrumb Network / ServerlessStreamResponse * @public */ export class ServerlessStreamResponse extends ServerResponse { constructor({ method, onReceiveHeaders, log, }: ServerlessStreamResponseProps) { super({ method } as any); this.useChunkedEncodingByDefault = true; this.chunkedEncoding = true; let internalWritable: Writable | null = null; let firstCrlfBufferEncountered = false; let chunkEncountered = false; const socket: Partial & { _writableState: any } = { _writableState: {}, writable: true, on: NO_OP, removeListener: NO_OP, destroy: NO_OP, cork: NO_OP, uncork: NO_OP, write: ( data: Uint8Array | string, encoding?: string | null | (() => void), cb?: () => void, ): any => { // very unlikely, I don't even know how to reproduce this, but exist on types // istanbul ignore if if (typeof encoding === 'function') { cb = encoding; encoding = null; } log.debug('SERVERLESS_ADAPTER:RESPONSE_STREAM:DATA', () => ({ data: Buffer.isBuffer(data) ? data.toString('utf8') : data, encoding, })); if (!internalWritable) { const stringData = getString(data); const endStatusIndex = stringData.indexOf(endStatusSeparator); const status = +stringData.slice(0, endStatusIndex).split(' ')[1]; const endHeaderIndex = stringData.indexOf(headerEnd); const headerData = stringData.slice( endStatusIndex + 2, endHeaderIndex, ); const headers = parseHeaders(headerData); log.debug( 'SERVERLESS_ADAPTER:RESPONSE_STREAM:FRAMEWORK_HEADERS', () => ({ headers, }), ); internalWritable = onReceiveHeaders(status, headers); // If we get an endChunked right after header which means the response body is empty, we need to immediately end the writable if (stringData.substring(endHeaderIndex + 4) === endChunked) internalWritable.end(); return true; } // node sends the last chunk crlf as a string: // https://github.com/nodejs/node/blob/v22.8.0/lib/_http_outgoing.js#L1131 if (data === endChunked) { internalWritable.end(cb); return true; } // check for header or data crlf // node sends the header and data crlf as a buffer // below code is aligned to following node implementation of the HTTP/1.1 chunked transfer coding: // https://github.com/nodejs/node/blob/v22.8.0/lib/_http_outgoing.js#L1012-L1015 // for reference: https://datatracker.ietf.org/doc/html/rfc9112#section-7 if (Buffer.isBuffer(data) && crlfBuffer.equals(data)) { const isHeaderCrlf = !firstCrlfBufferEncountered; if (isHeaderCrlf) { firstCrlfBufferEncountered = true; return true; } const isDataCrlf = firstCrlfBufferEncountered && chunkEncountered; if (isDataCrlf) { // done with chunk firstCrlfBufferEncountered = false; chunkEncountered = false; return true; } // the crlf *is* the chunk } const isContentLength = !firstCrlfBufferEncountered; if (isContentLength) { // discard content length return true; } // write chunk chunkEncountered = true; internalWritable.write(data, cb); return true; }, }; this.assignSocket(socket as unknown as Socket); } } ================================================ FILE: src/network/response.ts ================================================ // ATTRIBUTION: https://github.com/dougmoscrop/serverless-http import { IncomingMessage, ServerResponse } from 'node:http'; import type { Socket } from 'node:net'; import { NO_OP } from '../core'; import { getString } from './utils'; const headerEnd = '\r\n\r\n'; const endChunked = '0\r\n\r\n'; const BODY = Symbol('Response body'); const HEADERS = Symbol('Response headers'); function addData(stream: ServerlessResponse, data: Uint8Array | string) { if ( Buffer.isBuffer(data) || typeof data === 'string' || data instanceof Uint8Array ) stream[BODY].push(Buffer.from(data)); else throw new Error(`response.write() of unexpected type: ${typeof data}`); } /** * The properties to create a {@link ServerlessResponse}. * * @breadcrumb Network / ServerlessResponse * @public */ export interface ServerlessResponseProps { /** * The HTTP Method from request */ method?: string; } /** * The class that represents a response instance used to send to the framework and wait until the framework finishes processing the request. * Once it's happens, we use the properties from this response to built the response to the cloud. * * @breadcrumb Network / ServerlessResponse * @public */ export class ServerlessResponse extends ServerResponse { constructor({ method }: ServerlessResponseProps) { super({ method } as any); this[BODY] = []; this[HEADERS] = {}; this.useChunkedEncodingByDefault = false; this.chunkedEncoding = false; this._header = ''; // this ignore is used because I need to ignore these write calls: // https://github.com/nodejs/node/blob/main/lib/_http_outgoing.js#L934-L935 // https://github.com/nodejs/node/blob/main/lib/_http_outgoing.js#L937 let writesToIgnore = 1; const socket: Partial & { _writableState: any } = { _writableState: {}, writable: true, on: NO_OP, removeListener: NO_OP, destroy: NO_OP, cork: NO_OP, uncork: NO_OP, write: ( data: Uint8Array | string, encoding?: string | null | (() => void), cb?: () => void, ): any => { if (typeof encoding === 'function') { cb = encoding; encoding = null; } if (this._header === '' || this._wroteHeader) { if (!this.chunkedEncoding) addData(this, data); else { if (writesToIgnore > 0) writesToIgnore--; else if (data !== endChunked) { addData(this, data); writesToIgnore = 3; } } } else { const string = getString(data); const index = string.indexOf(headerEnd); if (index !== -1) { const remainder = string.slice(index + headerEnd.length); if (remainder && !this.chunkedEncoding) addData(this, remainder); this._wroteHeader = true; } } if (typeof cb === 'function') cb(); }, }; this.assignSocket(socket as unknown as Socket); } _header: string; _headers?: Record; _wroteHeader?: boolean; [BODY]: any[]; [HEADERS]: Record; get headers(): Record { return this[HEADERS]; } static from(res: IncomingMessage) { const response = new ServerlessResponse({ method: res.method }); response.statusCode = res.statusCode || 0; response[HEADERS] = res.headers; response[BODY] = (res as any).body ? [Buffer.from((res as any).body)] : []; response.end(); return response; } static body(res: ServerlessResponse): Buffer { return Buffer.concat(res[BODY]); } static headers(res: ServerlessResponse) { const headers = res.getHeaders(); return Object.assign(headers, res[HEADERS]); } override setHeader( key: string, value: number | string | readonly string[], ): any { if (this._wroteHeader) this[HEADERS][key] = value; else super.setHeader(key, value); } override writeHead( statusCode: number, statusMessage?: string | any | any[], obj?: any | any[], ): any { const headersObjOrArray = typeof statusMessage === 'string' ? obj : statusMessage; const arrayHeaders = Array.isArray(headersObjOrArray) ? headersObjOrArray : [headersObjOrArray || {}]; for (const headers of arrayHeaders) { for (const name in headers) { this.setHeader(name, headers[name]!); if (!this._wroteHeader) { // we only need to initiate super.headers once // writeHead will add the other headers itself break; } } } return this.callNativeWriteHead(statusCode, statusMessage, obj); } /** * I use ignore here because in nodejs 12.x, statusMessage can be string | OutgoingHttpHeaders * But in nodejs \>=14.x, statusMessage can also be OutgoingHttpHeaders[] * I take care of these cases above, but here I can't handle it well, so I give up * nodejs 12.x ref: https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/node/v12/http.d.ts#L229 * nodejs 14.x ref: https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/node/v14/http.d.ts#L263 */ protected callNativeWriteHead( statusCode: number, statusMessage?: string | any | any[], obj?: any | any[], ): this { return super.writeHead(statusCode, statusMessage, obj); } } ================================================ FILE: src/network/utils.ts ================================================ /** * Get the data from a buffer, string, or Uint8Array * * @breadcrumb Network * @param data - The data that was written inside the stream */ export function getString(data: Buffer | string | unknown) { if (Buffer.isBuffer(data)) return data.toString('utf8'); else if (typeof data === 'string') return data; else if (data instanceof Uint8Array) return new TextDecoder().decode(data); else throw new Error(`response.write() of unexpected type: ${typeof data}`); } ================================================ FILE: src/resolvers/aws-context/aws-context.resolver.ts ================================================ //#region Imports import type { Context } from 'aws-lambda'; import type { DelegatedResolver, Resolver, ResolverContract, ResolverProps, } from '../../contracts'; //#endregion /** * The class that implements the resolver by using the AWS Context object. * * @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. * * @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. * * @breadcrumb Resolvers / AwsContextResolver * @public */ export class AwsContextResolver implements ResolverContract { /** * {@inheritDoc} */ public createResolver({ context, event, log, respondWithErrors, adapter, }: ResolverProps): Resolver { if (!context) { throw new Error( 'Could not figure out how to create the resolver because the "context" argument was not sent.', ); } if (!context.succeed) { throw new Error( 'Could not figure out how to create the resolver because the "context" argument didn\'t have the "succeed" function.', ); } if (!context.fail) { throw new Error( 'Could not figure out how to create the resolver because the "context" argument didn\'t have the "fail" function.', ); } const delegatedResolver: DelegatedResolver = { succeed: response => context.succeed(response), fail: error => context.fail(error), }; return { run: task => { task() .then(response => delegatedResolver.succeed(response)) .catch(error => { log.error( 'SERVERLESS_ADAPTER:RESPOND_TO_EVENT_SOURCE_WITH_ERROR', error, ); adapter.onErrorWhileForwarding({ delegatedResolver, error, log, event, respondWithErrors, }); }); }, }; } } ================================================ FILE: src/resolvers/aws-context/index.ts ================================================ export * from './aws-context.resolver'; ================================================ FILE: src/resolvers/callback/callback.resolver.ts ================================================ //#region Imports import type { DelegatedResolver, Resolver, ResolverContract, ResolverProps, } from '../../contracts'; //#endregion /** * The default signature of the callback sent by serverless * * @breadcrumb Resolvers / CallbackResolver * @public */ export type ServerlessCallback = ( error: Error | null, success: TResponse | null, ) => void; /** * The class that implements the resolver using the callback function sent by serverless * * @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. * * @breadcrumb Resolvers / CallbackResolver * @public */ export class CallbackResolver implements ResolverContract, TResponse, void> { /** * {@inheritDoc} */ public createResolver({ callback, event, log, respondWithErrors, adapter, }: ResolverProps< TEvent, TContext, ServerlessCallback, TResponse >): Resolver { if (!callback) { throw new Error( 'Could not figure out how to create the resolver because the "callback" argument was not sent.', ); } const delegatedResolver: DelegatedResolver = { succeed: response => callback(null, response), fail: error => callback(error, null), }; return { run: task => { task() .then(response => delegatedResolver.succeed(response)) .catch(error => { log.error( 'SERVERLESS_ADAPTER:RESPOND_TO_EVENT_SOURCE_WITH_ERROR', error, ); adapter.onErrorWhileForwarding({ delegatedResolver, error, log, event, respondWithErrors, }); }); }, }; } } ================================================ FILE: src/resolvers/callback/index.ts ================================================ export * from './callback.resolver'; ================================================ FILE: src/resolvers/dummy/dummy.resolver.ts ================================================ //#region Imports import type { Resolver, ResolverContract } from '../../contracts'; //#endregion /** * The class that represents a dummy resolver that does nothing and can be used by the cloud that doesn't use resolvers. * * @breadcrumb Resolvers / DummyResolver * @public */ export class DummyResolver implements ResolverContract { /** * {@inheritDoc} */ public createResolver(): Resolver { return { // eslint-disable-next-line @typescript-eslint/no-misused-promises run: () => Promise.resolve(), }; } } ================================================ FILE: src/resolvers/dummy/index.ts ================================================ export * from './dummy.resolver'; ================================================ FILE: src/resolvers/promise/index.ts ================================================ export * from './promise.resolver'; ================================================ FILE: src/resolvers/promise/promise.resolver.ts ================================================ //#region Imports import type { DelegatedResolver, Resolver, ResolverContract, ResolverProps, } from '../../contracts'; //#endregion /** * The class that implements the resolver using the promise object sent by this library * * @breadcrumb Resolvers / PromiseResolver * @public */ export class PromiseResolver implements ResolverContract> { /** * {@inheritDoc} */ public createResolver({ event, log, respondWithErrors, adapter, }: ResolverProps): Resolver< TResponse, Promise > { return { run: task => { return new Promise((resolve, reject) => { const delegatedResolver: DelegatedResolver = { succeed: response => resolve(response), fail: error => reject(error), }; task() .then(response => delegatedResolver.succeed(response)) .catch(error => { log.error( 'SERVERLESS_ADAPTER:RESPOND_TO_EVENT_SOURCE_WITH_ERROR', error, ); adapter.onErrorWhileForwarding({ delegatedResolver, error, log, event, respondWithErrors, }); }); }); }, }; } } ================================================ FILE: src/serverless-adapter.ts ================================================ //#region Imports import type { BinarySettings } from './@types'; import type { AdapterContract, FrameworkContract, HandlerContract, ResolverContract, ServerlessHandler, } from './contracts'; import { DEFAULT_BINARY_CONTENT_TYPES, DEFAULT_BINARY_ENCODINGS, type ILogger, createDefaultLogger, } from './core'; //#endregion /** * The class used to build the serverless handler. * * @example * ```typescript * const app = express(); * export const handler = ServerlessAdapter.new(app) * .setFramework(new ExpressFramework()) * .setHandler(new DefaultHandler()) * .setResolver(new PromiseResolver()) * .setRespondWithErrors(true) * .addAdapter(new AlbAdapter()) * .addAdapter(new SQSAdapter()) * .addAdapter(new SNSAdapter()) * .build(); * ``` * * @breadcrumb ServerlessAdapter * @public */ export class ServerlessAdapter< TApp, TEvent, TContext, TCallback, TResponse, TReturn, > { //#region Constructor /** * Default constructor */ private constructor(app: TApp) { this.app = app; } //#endregion //#region Protected Properties /** * The instance of the app (express, hapi, koa, etc...) */ protected app: TApp; //#endregion //#region Protected Properties /** * Settings for whether the response should be treated as binary or not * * @defaultValue `contentEncodings` and `contentTypes` are set with {@link DEFAULT_BINARY_ENCODINGS} and {@link DEFAULT_BINARY_CONTENT_TYPES}, respectively. */ protected binarySettings: BinarySettings = { contentEncodings: DEFAULT_BINARY_ENCODINGS, contentTypes: DEFAULT_BINARY_CONTENT_TYPES, }; /** * Indicates whether the error stack should be included in the response or not * * @remarks These errors will only be included when an error occurs while forwarding the event to the framework * @defaultValue True when NODE_ENV is equal to `development` */ protected respondWithErrors: boolean = process.env.NODE_ENV === 'development'; /** * The instance of the logger service */ protected log: ILogger = createDefaultLogger(); /** * The list of adapters used to handle an event's request and response */ protected adapters: AdapterContract[] = []; /** * The framework that will process requests */ protected framework?: FrameworkContract; /** * The resolver that aims to resolve the response to serverless and stop its execution when the request ends */ protected resolver?: ResolverContract< TEvent, TContext, TCallback, TResponse, TReturn >; /** * The handler that will get the event, context and callback and pass it to the adapter and framework */ protected handler?: HandlerContract< TApp, TEvent, TContext, TCallback, TResponse, TReturn >; //#endregion //#region Static Methods /** * Creates a new instance of the builder with app (express, hapi, koa, etc...) * * @param app - The instance of the app */ public static new< TApp, TEvent, TContext = any, TCallback = any, TResponse = any, TReturn = any, >( app: TApp, ): ServerlessAdapter { return new ServerlessAdapter(app); } //#endregion //#region Builder Methods /** * Defines the handler that will get the event, context and callback and pass it to the adapter and framework * * @param handler - The implementation of the handler contract */ public setHandler( handler: HandlerContract< TApp, TEvent, TContext, TCallback, TResponse, TReturn >, ): Omit { if (this.handler) throw new Error('SERVERLESS_ADAPTER: The handler should not set twice.'); this.handler = handler; return this; } /** * Defines the resolver that aims to resolve the response to serverless and stop its execution when the request ends * * @param resolver - The implementation of the resolver contract */ public setResolver( resolver: ResolverContract, ): Omit { if (this.resolver) throw new Error('SERVERLESS_ADAPTER: The resolver should not set twice.'); this.resolver = resolver; return this; } /** * Defines the framework that will process requests * * @param framework - The implementation of the framework contract */ public setFramework( framework: FrameworkContract, ): Omit { if (this.framework) { throw new Error( 'SERVERLESS_ADAPTER: The framework should not set twice.', ); } this.framework = framework; return this; } /** * Defines the logger service used during the execution of the handler * * @param logger - The implementation of the logger */ public setLogger(logger: ILogger): Omit { this.log = logger; return this; } /** * Defines the binary settings for whether the response should be treated as binary or not * * @param binarySettings - The binary settings */ public setBinarySettings( binarySettings: BinarySettings, ): Omit { this.binarySettings = { ...this.binarySettings, ...binarySettings, }; return this; } /** * Defines the responseWithErrors, a property that indicates whether the error stack should be included in the response or not * * @param respondWithErrors - Should include or not the errors in response */ public setRespondWithErrors( respondWithErrors: boolean, ): Omit { this.respondWithErrors = respondWithErrors; return this; } /** * Add an adapter to the adapters list to handle the event coming from any serverless event source * * @param adapter - The implementation of the adapter contract */ public addAdapter( adapter: AdapterContract, ): Pick { this.adapters.push(adapter); return this; } /** * The builder method that returns the handler function to be exported for serverless consumption */ public build(): ServerlessHandler { if (!this.resolver) { throw new Error( 'SERVERLESS_ADAPTER: Is required to set a resolver before build.', ); } if (!this.framework) { throw new Error( 'SERVERLESS_ADAPTER: Is required to set a framework before build.', ); } if (!this.handler) { throw new Error( 'SERVERLESS_ADAPTER: Is required to set a handler before build.', ); } if (this.adapters.length === 0) { throw new Error( 'SERVERLESS_ADAPTER: Is required to set at least one adapter.', ); } return this.handler.getHandler( this.app, this.framework, this.adapters, this.resolver, this.binarySettings, this.respondWithErrors, this.log, ); } //#endregion } ================================================ FILE: test/adapters/apollo-server/apollo-mutation.adapter.spec.ts ================================================ import { ApolloServer } from '@apollo/server'; import { describe, expect, it, vitest } from 'vitest'; import type { SQSEvent } from 'aws-lambda'; import { type AdapterContract, EmptyResponse, type GetResponseAdapterProps, ServerlessRequest, ServerlessResponse, createDefaultLogger, waitForStreamComplete, } from '../../../src'; import { ApolloServerMutationAdapter, type ApolloServerMutationAdapterOptions, } from '../../../src/adapters/apollo-server'; import { DynamoDBAdapter, SNSAdapter, SQSAdapter, } from '../../../src/adapters/aws'; import { ApolloServerFramework } from '../../../src/frameworks/apollo-server'; import { JsonBodyParserFramework } from '../../../src/frameworks/body-parser'; import { createDynamoDBEvent } from '../aws/utils/dynamodb'; import { createSNSEvent } from '../aws/utils/sns'; import { createSQSEvent } from '../aws/utils/sqs'; function createApolloServer({ mutationName, queryDefinition, }: { mutationName: string; queryDefinition: string; }): ApolloServer { const schema = ` type Query { message: String } type AWSResult ${queryDefinition} type Mutation { ${mutationName} (event: String): AWSResult } `; const app = new ApolloServer({ typeDefs: schema, resolvers: { Query: { message: () => 'Hello World!', }, Mutation: { [mutationName]: (_, data) => { return { result: data.event, }; }, }, }, }); app.startInBackgroundHandlingStartupErrorsByLoggingAndFailingAllRequests(); return app; } const options: [adapter: AdapterContract, eventData: any][] = [ [new SQSAdapter(), createSQSEvent()], [new SNSAdapter(), createSNSEvent()], [new DynamoDBAdapter(), createDynamoDBEvent()], ]; describe('integration: should be able to convert', () => { for (const [adapter, eventData] of options) { it(`${adapter.getAdapterName()}: should convert correctly`, async () => { const options: ApolloServerMutationAdapterOptions = { mutationName: 'aws', mutationResultQuery: '{ result }', }; const app = createApolloServer({ mutationName: 'aws', queryDefinition: '{ result: String }', }); const mutationAdapter = new ApolloServerMutationAdapter(adapter, options); expect( mutationAdapter.canHandle(eventData, null, createDefaultLogger()), ).toEqual(true); const request = mutationAdapter.getRequest( eventData, null, createDefaultLogger(), ); expect(request.method).toEqual('POST'); const serverlessRequest = new ServerlessRequest({ method: request.method, headers: request.headers, body: request.body, remoteAddress: request.remoteAddress, url: request.path, }); const serverlessResponse = new ServerlessResponse({ method: request.method, }); const framework = new JsonBodyParserFramework( new ApolloServerFramework(), ); framework.sendRequest(app, serverlessRequest, serverlessResponse); await waitForStreamComplete(serverlessResponse); expect(ServerlessResponse.body(serverlessResponse).toString()).toContain( JSON.stringify({ data: { aws: { result: JSON.stringify(eventData), }, }, }), ); const sqsAdapterSpy = vitest.spyOn(adapter, 'getResponse'); const props = { response: serverlessResponse, body: ServerlessResponse.body(serverlessResponse).toString(), headers: ServerlessResponse.headers(serverlessResponse), log: createDefaultLogger(), event: eventData, statusCode: serverlessResponse.statusCode, isBase64Encoded: false, }; const response = mutationAdapter.getResponse(props); expect(sqsAdapterSpy).toHaveBeenNthCalledWith( 1, expect.objectContaining({ ...props, body: JSON.stringify({ result: JSON.stringify(eventData), }), }), ); expect(response).toEqual(EmptyResponse); }); } it('to __typename: when result query is not passed', async () => { const options: ApolloServerMutationAdapterOptions = { mutationName: 'aws', }; const eventData = createSQSEvent(); const adapter: AdapterContract = new SQSAdapter(); const app = createApolloServer({ mutationName: 'aws', queryDefinition: '{_: Boolean}', }); const mutationAdapter = new ApolloServerMutationAdapter(adapter, options); const request = mutationAdapter.getRequest( eventData, null, createDefaultLogger(), ); const serverlessRequest = new ServerlessRequest({ method: request.method, headers: request.headers, body: request.body, remoteAddress: request.remoteAddress, url: request.path, }); const serverlessResponse = new ServerlessResponse({ method: request.method, }); const framework = new JsonBodyParserFramework(new ApolloServerFramework()); framework.sendRequest(app, serverlessRequest, serverlessResponse); await waitForStreamComplete(serverlessResponse); const sqsAdapterSpy = vitest.spyOn(adapter, 'getResponse'); const props = { response: serverlessResponse, body: ServerlessResponse.body(serverlessResponse).toString(), headers: ServerlessResponse.headers(serverlessResponse), log: createDefaultLogger(), event: eventData, statusCode: serverlessResponse.statusCode, isBase64Encoded: false, }; const response = mutationAdapter.getResponse(props); expect(sqsAdapterSpy).toHaveBeenNthCalledWith( 1, expect.objectContaining({ ...props, body: JSON.stringify({ __typename: 'AWSResult' }), }), ); expect(response).toEqual(EmptyResponse); }); it('to base adapter: when mutation does not exist', async () => { const options: ApolloServerMutationAdapterOptions = { mutationName: 'aws', }; const eventData = createSQSEvent(); const adapter: AdapterContract = new SQSAdapter(); const app = createApolloServer({ mutationName: 'potato', queryDefinition: '{_: Boolean}', }); const mutationAdapter = new ApolloServerMutationAdapter(adapter, options); const request = mutationAdapter.getRequest( eventData, null, createDefaultLogger(), ); const serverlessRequest = new ServerlessRequest({ method: request.method, headers: request.headers, body: request.body, remoteAddress: request.remoteAddress, url: request.path, }); const serverlessResponse = new ServerlessResponse({ method: request.method, }); const framework = new JsonBodyParserFramework(new ApolloServerFramework()); framework.sendRequest(app, serverlessRequest, serverlessResponse); await waitForStreamComplete(serverlessResponse); const sqsAdapterSpy = vitest.spyOn(adapter, 'getResponse'); const props = { response: serverlessResponse, body: ServerlessResponse.body(serverlessResponse).toString(), headers: ServerlessResponse.headers(serverlessResponse), log: createDefaultLogger(), event: eventData, statusCode: serverlessResponse.statusCode, isBase64Encoded: false, }; expect(() => mutationAdapter.getResponse(props)).toThrow( 'Cannot query field', ); expect(sqsAdapterSpy).toHaveBeenNthCalledWith(1, props); }); }); it('getAdapterName: should mutate the name of adapter', () => { const sqs = new SQSAdapter(); const mutation = new ApolloServerMutationAdapter(sqs, { mutationName: 'aws', }); expect(mutation.getAdapterName()).toEqual(`${sqs.getAdapterName()}Mutation`); }); it('onErrorWhileForwarding: should forward error dealing to base adapter', () => { const sqs = new SQSAdapter(); const spyedOnError = vitest.spyOn(sqs, 'onErrorWhileForwarding'); const mutation = new ApolloServerMutationAdapter(sqs, { mutationName: 'aws', }); const props = { event: {} as SQSEvent, log: createDefaultLogger(), error: new Error(), delegatedResolver: { fail: vitest.fn(), succeed: vitest.fn() }, respondWithErrors: true, }; mutation.onErrorWhileForwarding(props); expect(spyedOnError).toHaveBeenNthCalledWith(1, props); }); it('getResponse: should forward props to base adapter when response has errors', () => { const sqs = new SQSAdapter(); const spyedOnError = vitest.spyOn(sqs, 'getResponse'); const mutation = new ApolloServerMutationAdapter(sqs, { mutationName: 'aws', }); const props: GetResponseAdapterProps = { log: createDefaultLogger(), event: {}, body: JSON.stringify({ errors: { message: 'Wrong data' }, data: {} }), isBase64Encoded: false, statusCode: 400, headers: {}, }; expect(() => mutation.getResponse(props)).toThrow('"statusCode":400'); expect(spyedOnError).toHaveBeenNthCalledWith(1, props); }); ================================================ FILE: test/adapters/aws/alb.adapter.spec.ts ================================================ import type { ALBEvent, ALBResult } from 'aws-lambda'; import { beforeEach, describe, expect, it, vitest } from 'vitest'; import { type DelegatedResolver, type GetResponseAdapterProps, type ILogger, getEventBodyAsBuffer, getFlattenedHeadersMap, getMultiValueHeadersMap, getPathWithQueryStringParams, } from '../../../src'; import { AlbAdapter } from '../../../src/adapters/aws'; import { createCanHandleTestsForAdapter } from '../utils/can-handle'; import { createAlbEvent, createAlbEventWithMultiValueHeaders, } from './utils/alb-event'; describe(AlbAdapter.name, () => { let adapter!: AlbAdapter; beforeEach(() => { adapter = new AlbAdapter(); }); describe('getAdapterName', () => { it('should be the same name of the class', () => { expect(adapter.getAdapterName()).toBe(AlbAdapter.name); }); }); createCanHandleTestsForAdapter(() => new AlbAdapter(), undefined); describe('getRequest', () => { it('should return the correct mapping for the request', () => { const method = 'PUT'; const path = '/events'; const body = { name: 'H4ad Event' }; const event = createAlbEvent(method, path, body); expect(event.headers).toHaveProperty('x-forwarded-for'); expect(event.headers!['x-forwarded-for']).not.toBeInstanceOf(Array); const result = adapter.getRequest(event); const remoteAddress = event.headers!['x-forwarded-for']; expect(result).toHaveProperty('method', method); expect(result).toHaveProperty('headers'); expect(result.headers).toHaveProperty('x-forwarded-for'); expect(result.headers['x-forwarded-for']).not.toBeInstanceOf(Array); expect(result).toHaveProperty('body'); expect(result.body).toBeInstanceOf(Buffer); const [bodyBuffer, contentLength] = getEventBodyAsBuffer( JSON.stringify(body), false, ); expect(result.body).toStrictEqual(bodyBuffer); expect(result.headers).toHaveProperty('content-length'); expect(result.headers['content-length']).toBe(String(contentLength)); expect(result).toHaveProperty('remoteAddress', remoteAddress); const resultPath = getPathWithQueryStringParams( path, event.queryStringParameters, ); expect(result).toHaveProperty('path', resultPath); }); it('should return the correct mapping for the request with multi value headers', () => { const method = 'POST'; const path = '/events'; const body = { name: 'H4ad Event' }; const event = createAlbEventWithMultiValueHeaders(method, path, body); expect(event.multiValueHeaders).toHaveProperty('x-forwarded-for'); expect(event.multiValueHeaders!['x-forwarded-for']).toBeInstanceOf(Array); const result = adapter.getRequest(event); const remoteAddress = event.multiValueHeaders!['x-forwarded-for']![0]; expect(result).toHaveProperty('method', method); expect(result).toHaveProperty('headers'); expect(result.headers).toHaveProperty('x-forwarded-for'); expect(result.headers['x-forwarded-for']).not.toBeInstanceOf(Array); expect(result).toHaveProperty('body'); expect(result.body).toBeInstanceOf(Buffer); const [bodyBuffer, contentLength] = getEventBodyAsBuffer( JSON.stringify(body), false, ); expect(result.body).toStrictEqual(bodyBuffer); expect(result.headers).toHaveProperty('content-length'); expect(result.headers['content-length']).toBe(String(contentLength)); expect(result).toHaveProperty('remoteAddress', remoteAddress); const resultPath = getPathWithQueryStringParams( path, event.multiValueQueryStringParameters, ); expect(result).toHaveProperty('path', resultPath); }); it('should return the correct mapping for the request when it has no body', () => { const method = 'POST'; const path = '/events'; const body = undefined; const event = createAlbEvent(method, path, body); const result = adapter.getRequest(event); const remoteAddress = event.headers!['x-forwarded-for']; expect(result).toHaveProperty('method', method); expect(result).toHaveProperty('headers'); expect(result.headers).toHaveProperty('x-forwarded-for'); expect(result.headers['x-forwarded-for']).not.toBeInstanceOf(Array); expect(result).toHaveProperty('body'); expect(result.body).not.toBeInstanceOf(Buffer); expect(result.body).toBeUndefined(); expect(result.headers).toHaveProperty('content-length'); expect(result.headers['content-length']).toBe( event.headers!['content-length'], ); expect(result).toHaveProperty('remoteAddress', remoteAddress); const resultPath = getPathWithQueryStringParams( path, event.queryStringParameters, ); expect(result).toHaveProperty('path', resultPath); }); it('should return the correct mapping for the request when send stripBasePath', () => { const stripBasePath = '/prod'; const method = 'PUT'; const path = '/prod/events'; const body = { name: 'H4ad Event' }; const strippedAdapter = new AlbAdapter({ stripBasePath }); const event = createAlbEvent(method, path, body); expect(event.headers).toHaveProperty('x-forwarded-for'); expect(event.headers!['x-forwarded-for']).not.toBeInstanceOf(Array); const result = strippedAdapter.getRequest(event); const remoteAddress = event.headers!['x-forwarded-for']; expect(result).toHaveProperty('method', method); expect(result).toHaveProperty('headers'); expect(result.headers).toHaveProperty('x-forwarded-for'); expect(result.headers['x-forwarded-for']).not.toBeInstanceOf(Array); expect(result).toHaveProperty('body'); expect(result.body).toBeInstanceOf(Buffer); const [bodyBuffer, contentLength] = getEventBodyAsBuffer( JSON.stringify(body), false, ); expect(result.body).toStrictEqual(bodyBuffer); expect(result.headers).toHaveProperty('content-length'); expect(result.headers['content-length']).toBe(String(contentLength)); expect(result).toHaveProperty('remoteAddress', remoteAddress); const resultPath = getPathWithQueryStringParams( path.replace(stripBasePath, ''), event.queryStringParameters, ); expect(result).toHaveProperty('path', resultPath); }); }); describe('getResponse', () => { it('should return the correct mapping for the response', () => { const method = 'PUT'; const path = '/events'; const requestBody = { name: 'H4ad Event' }; const resultBody = '{"success":true}'; const resultStatusCode = 200; const resultIsBase64Encoded = false; const event = createAlbEvent(method, path, requestBody); const responseHeaders = getFlattenedHeadersMap(event.headers!); const result = adapter.getResponse({ event, headers: responseHeaders, body: resultBody, log: {} as ILogger, isBase64Encoded: resultIsBase64Encoded, statusCode: resultStatusCode, }); expect(result).toHaveProperty('statusCode', 200); expect(result).toHaveProperty('body', resultBody); expect(result).toHaveProperty('headers', responseHeaders); expect(result).toHaveProperty('multiValueHeaders', undefined); expect(result).toHaveProperty('isBase64Encoded', resultIsBase64Encoded); }); it('should return the correct mapping for the response with multi value headers', () => { const method = 'PUT'; const path = '/events'; const requestBody = { name: 'H4ad Event' }; const resultBody = '{"success":true}'; const resultStatusCode = 200; const resultIsBase64Encoded = false; const event = createAlbEventWithMultiValueHeaders( method, path, requestBody, ); const responseHeaders = getFlattenedHeadersMap(event.multiValueHeaders!); const responseMultiValueHeaders = getMultiValueHeadersMap(responseHeaders); const result = adapter.getResponse({ event, headers: responseHeaders, body: resultBody, log: {} as ILogger, isBase64Encoded: resultIsBase64Encoded, statusCode: resultStatusCode, }); expect(result).toHaveProperty('statusCode', 200); expect(result).toHaveProperty('body', resultBody); expect(result).toHaveProperty('headers', undefined); expect(result).toHaveProperty( 'multiValueHeaders', responseMultiValueHeaders, ); expect(result).toHaveProperty('isBase64Encoded', resultIsBase64Encoded); }); it('should remove the transfer-encoding header if it is chunked', () => { const event = createAlbEventWithMultiValueHeaders('GET', '/events'); const responseHeaders = getFlattenedHeadersMap(event.multiValueHeaders!); const responseMultiValueHeaders = getMultiValueHeadersMap(responseHeaders); responseHeaders['transfer-encoding'] = 'chunked'; const result = adapter.getResponse({ event, headers: responseHeaders, body: '', log: {} as ILogger, isBase64Encoded: false, statusCode: 200, }); expect(result).toHaveProperty('headers', undefined); expect(result).toHaveProperty( 'multiValueHeaders', responseMultiValueHeaders, ); responseMultiValueHeaders['transfer-encoding'] = ['chunked']; const event2 = createAlbEvent('GET', '/events'); const responseHeaders2 = getFlattenedHeadersMap(event2.headers!); const result2 = adapter.getResponse({ event: event2, headers: responseHeaders2, body: '', log: {} as ILogger, isBase64Encoded: false, statusCode: 200, }); expect(result2).toHaveProperty('headers', responseHeaders2); }); }); describe('onErrorWhileForwarding', () => { it('should resolver call succeed', () => { const method = 'GET'; const path = '/events'; const requestBody = undefined; const event = createAlbEventWithMultiValueHeaders( method, path, requestBody, ); const log = {} as ILogger; const resolver: DelegatedResolver = { fail: vitest.fn(), succeed: vitest.fn(), }; const respondWithErrors = true; const error = new Error('Test error'); const oldGetResponse = adapter.getResponse.bind(adapter); let getResponseResult: ALBResult | undefined; adapter.getResponse = vitest.fn( (params: GetResponseAdapterProps) => { expect(params.event).toBe(event); expect(params.statusCode).toBe(500); expect(params.body).toBe(error.stack); expect(params.isBase64Encoded).toBe(false); expect(params.log).toBe(log); expect(params.headers).toStrictEqual({}); getResponseResult = oldGetResponse(params); return getResponseResult; }, ); adapter.onErrorWhileForwarding({ event, log, delegatedResolver: resolver, respondWithErrors, error, }); // eslint-disable-next-line @typescript-eslint/unbound-method expect(adapter.getResponse).toHaveBeenCalledTimes(1); expect(resolver.fail).toHaveBeenCalledTimes(0); expect(resolver.succeed).toHaveBeenCalledTimes(1); expect(resolver.succeed).toHaveBeenCalledWith(getResponseResult); }); it('should resolver call succeed but without sending errors', () => { const method = 'GET'; const path = '/events'; const requestBody = undefined; const event = createAlbEventWithMultiValueHeaders( method, path, requestBody, ); const log = {} as ILogger; const resolver: DelegatedResolver = { fail: vitest.fn(), succeed: vitest.fn(), }; const respondWithErrors = false; const error = new Error('Test error without sending this error'); const oldGetResponse = adapter.getResponse.bind(adapter); let getResponseResult: ALBResult | undefined; adapter.getResponse = vitest.fn( (params: GetResponseAdapterProps) => { expect(params.event).toBe(event); expect(params.statusCode).toBe(500); expect(params.body).not.toBe(error.stack); expect(params.body).toStrictEqual(''); expect(params.isBase64Encoded).toBe(false); expect(params.log).toBe(log); expect(params.headers).toStrictEqual({}); getResponseResult = oldGetResponse(params); return getResponseResult; }, ); adapter.onErrorWhileForwarding({ event, log, delegatedResolver: resolver, respondWithErrors, error, }); // eslint-disable-next-line @typescript-eslint/unbound-method expect(adapter.getResponse).toHaveBeenCalledTimes(1); expect(resolver.fail).toHaveBeenCalledTimes(0); expect(resolver.succeed).toHaveBeenCalledTimes(1); expect(resolver.succeed).toHaveBeenCalledWith(getResponseResult); }); }); }); ================================================ FILE: test/adapters/aws/api-gateway-v1.adapter.spec.ts ================================================ import type { APIGatewayProxyResult } from 'aws-lambda'; import type { APIGatewayProxyEvent } from 'aws-lambda/trigger/api-gateway-proxy'; import { beforeEach, describe, expect, it, vitest } from 'vitest'; import { type DelegatedResolver, type GetResponseAdapterProps, type ILogger, ServerlessResponse, getEventBodyAsBuffer, getFlattenedHeadersMap, getMultiValueHeadersMap, getPathWithQueryStringParams, } from '../../../src'; import { ApiGatewayV1Adapter } from '../../../src/adapters/aws'; import { createCanHandleTestsForAdapter } from '../utils/can-handle'; import { createApiGatewayV1 } from './utils/api-gateway-v1'; describe(ApiGatewayV1Adapter.name, () => { let adapter!: ApiGatewayV1Adapter; beforeEach(() => { adapter = new ApiGatewayV1Adapter(); }); describe('getAdapterName', () => { it('should be the same name of the class', () => { expect(adapter.getAdapterName()).toBe(ApiGatewayV1Adapter.name); }); }); createCanHandleTestsForAdapter(() => new ApiGatewayV1Adapter(), undefined); describe('getRequest', () => { it('should return the correct mapping for the request', () => { const method = 'PUT'; const path = '/events'; const body = { name: 'H4ad Event' }; const event = createApiGatewayV1(method, path, body); event.queryStringParameters = { potato: 'v1', nanana: 'oh nanana', unkown: 'oi', ignore: undefined, }; event.multiValueQueryStringParameters = { potato: ['v1', 'v2'], nanana: ['oh nanana'], }; const result = adapter.getRequest(event); const remoteAddress = event.requestContext.identity.sourceIp; expect(result).toHaveProperty('method', method); expect(result).toHaveProperty('headers'); expect(result.headers).toHaveProperty('Accept'); expect(result).toHaveProperty('body'); expect(result.body).toBeInstanceOf(Buffer); const [bodyBuffer, contentLength] = getEventBodyAsBuffer( JSON.stringify(body), false, ); expect(result.body).toStrictEqual(bodyBuffer); expect(result.headers).toHaveProperty('content-length'); expect(result.headers['content-length']).toBe(String(contentLength)); expect(result).toHaveProperty('remoteAddress', remoteAddress); const resultPath = getPathWithQueryStringParams( path, event.multiValueQueryStringParameters, ); expect(result).toHaveProperty('path', resultPath); }); it('should return lowercase request headers if option `lowercaseRequestHeaders` is `true`', () => { const method = 'GET'; const path = '/events'; adapter = new ApiGatewayV1Adapter({ lowercaseRequestHeaders: true }); const event = createApiGatewayV1(method, path); event.headers['Cookie'] = 'test=test;'; const { headers } = adapter.getRequest(event); expect(headers).not.toHaveProperty('Cookie'); expect(headers).toHaveProperty('cookie'); expect(headers).not.toHaveProperty('Accept'); expect(headers).toHaveProperty('accept'); }); it('should return the correct mapping for the request when it has no body', () => { const method = 'GET'; const path = '/users'; const body = undefined; const event = createApiGatewayV1(method, path, body, {}, { page: '2' }); const result = adapter.getRequest(event); const remoteAddress = event.requestContext.identity.sourceIp; expect(result).toHaveProperty('method', method); expect(result).toHaveProperty('headers'); expect(result).toHaveProperty('body'); expect(result.body).not.toBeInstanceOf(Buffer); expect(result.body).toBeUndefined(); expect(result).toHaveProperty('remoteAddress', remoteAddress); const resultPath = getPathWithQueryStringParams( path, event.queryStringParameters, ); expect(result).toHaveProperty('path', resultPath); }); it('should return the correct mapping for the request when send stripBasePath', () => { const stripBasePath = '/prod'; const method = 'GET'; const path = '/prod/posts'; const body = undefined; const strippedAdapter = new ApiGatewayV1Adapter({ stripBasePath }); const event = createApiGatewayV1(method, path, body); const result = strippedAdapter.getRequest(event); const remoteAddress = event.requestContext.identity.sourceIp; expect(result).toHaveProperty('method', method); expect(result).toHaveProperty('headers'); expect(result).toHaveProperty('body'); expect(result.body).not.toBeInstanceOf(Buffer); expect(result.body).toBeUndefined(); expect(result).toHaveProperty('remoteAddress', remoteAddress); const resultPath = getPathWithQueryStringParams( path.replace('/prod', ''), event.queryStringParameters, ); expect(result).toHaveProperty('path', resultPath); }); }); describe('getResponse', () => { it('should return the correct mapping for the response', () => { const method = 'PUT'; const path = '/events'; const requestBody = { name: 'H4ad Event' }; const resultBody = '{"success":true}'; const resultStatusCode = 200; const resultIsBase64Encoded = false; const event = createApiGatewayV1(method, path, requestBody); const resultHeaders = getFlattenedHeadersMap(event.headers); const result = adapter.getResponse({ event, log: {} as ILogger, body: resultBody, isBase64Encoded: resultIsBase64Encoded, statusCode: resultStatusCode, headers: resultHeaders, }); const responseHeaders = getMultiValueHeadersMap(resultHeaders); expect(result).toHaveProperty('statusCode', 200); expect(result).toHaveProperty('body', resultBody); expect(result).not.toHaveProperty('headers'); expect(result).toHaveProperty('multiValueHeaders', responseHeaders); expect(result).toHaveProperty('isBase64Encoded', resultIsBase64Encoded); }); it('should throw an error when framework send transfer-encoding=chunked in headers', () => { const method = 'GET'; const path = '/events/stream'; const requestBody = undefined; const resultBody = '{"success":true}'; const resultStatusCode = 200; const resultIsBase64Encoded = false; const event = createApiGatewayV1(method, path, requestBody); const resultHeaders = getFlattenedHeadersMap(event.headers); resultHeaders['transfer-encoding'] = 'gzip,chunked'; expect(() => adapter.getResponse({ event, log: {} as ILogger, body: resultBody, isBase64Encoded: resultIsBase64Encoded, statusCode: resultStatusCode, headers: resultHeaders, }), ).toThrowError('is not supported'); }); it('should throw an error when framework send chunkedEncoding=true in response', () => { const method = 'GET'; const path = '/events/stream'; const requestBody = undefined; const resultBody = '{"success":true}'; const resultStatusCode = 200; const resultIsBase64Encoded = false; const event = createApiGatewayV1(method, path, requestBody); const resultHeaders = getFlattenedHeadersMap(event.headers); const fakeChunkedResponse = new ServerlessResponse({ method }); fakeChunkedResponse.chunkedEncoding = true; expect(() => adapter.getResponse({ event, log: {} as ILogger, body: resultBody, isBase64Encoded: resultIsBase64Encoded, statusCode: resultStatusCode, headers: resultHeaders, response: fakeChunkedResponse, }), ).toThrowError('is not supported'); }); describe('when throwOnChunkedTransferEncoding=false', () => { it('should NOT throw an error when framework send chunkedEncoding=true in response', () => { const customAdapter = new ApiGatewayV1Adapter({ throwOnChunkedTransferEncoding: false, }); const method = 'GET'; const path = '/events/stream'; const requestBody = undefined; const resultBody = '{"success":true}'; const resultStatusCode = 200; const resultIsBase64Encoded = false; const event = createApiGatewayV1(method, path, requestBody); const resultHeaders = getFlattenedHeadersMap(event.headers); const fakeChunkedResponse = new ServerlessResponse({ method }); fakeChunkedResponse.chunkedEncoding = true; const result = customAdapter.getResponse({ event, log: {} as ILogger, body: resultBody, isBase64Encoded: resultIsBase64Encoded, statusCode: resultStatusCode, headers: resultHeaders, response: fakeChunkedResponse, }); expect(result.multiValueHeaders!['transfer-encoding']).toBeUndefined(); }); it('should NOT throw an error when framework send transfer-encoding=chunked in headers', () => { const customAdapter = new ApiGatewayV1Adapter({ throwOnChunkedTransferEncoding: false, }); const method = 'GET'; const path = '/events/stream'; const requestBody = undefined; const resultBody = '{"success":true}'; const resultStatusCode = 200; const resultIsBase64Encoded = false; const event = createApiGatewayV1(method, path, requestBody); const resultHeaders = getFlattenedHeadersMap(event.headers); resultHeaders['transfer-encoding'] = 'gzip,chunked'; const result = customAdapter.getResponse({ event, log: {} as ILogger, body: resultBody, isBase64Encoded: resultIsBase64Encoded, statusCode: resultStatusCode, headers: resultHeaders, }); expect(result.multiValueHeaders!['transfer-encoding']).toBeUndefined(); }); }); }); describe('onErrorWhileForwarding', () => { it('should resolver call succeed', () => { const method = 'GET'; const path = '/events'; const requestBody = undefined; const event = createApiGatewayV1(method, path, requestBody); const log = {} as ILogger; const resolver: DelegatedResolver = { fail: vitest.fn(), succeed: vitest.fn(), }; const respondWithErrors = true; const error = new Error('Test error'); const oldGetResponse = adapter.getResponse.bind(adapter); let getResponseResult: APIGatewayProxyResult | undefined; adapter.getResponse = vitest.fn( (params: GetResponseAdapterProps) => { expect(params.event).toBe(event); expect(params.statusCode).toBe(500); expect(params.body).toBe(error.stack); expect(params.isBase64Encoded).toBe(false); expect(params.log).toBe(log); expect(params.headers).toStrictEqual({}); getResponseResult = oldGetResponse(params); return getResponseResult; }, ); adapter.onErrorWhileForwarding({ event, log, delegatedResolver: resolver, respondWithErrors, error, }); // eslint-disable-next-line @typescript-eslint/unbound-method expect(adapter.getResponse).toHaveBeenCalledTimes(1); expect(resolver.fail).toHaveBeenCalledTimes(0); expect(resolver.succeed).toHaveBeenCalledTimes(1); expect(resolver.succeed).toHaveBeenCalledWith(getResponseResult); }); it('should resolver call succeed but without sending errors', () => { const method = 'GET'; const path = '/users'; const requestBody = undefined; const event = createApiGatewayV1(method, path, requestBody); const log = {} as ILogger; const resolver: DelegatedResolver = { fail: vitest.fn(), succeed: vitest.fn(), }; const respondWithErrors = false; const error = new Error('Test error'); const oldGetResponse = adapter.getResponse.bind(adapter); let getResponseResult: APIGatewayProxyResult | undefined; adapter.getResponse = vitest.fn( (params: GetResponseAdapterProps) => { expect(params.event).toBe(event); expect(params.statusCode).toBe(500); expect(params.body).toBe(''); expect(params.isBase64Encoded).toBe(false); expect(params.log).toBe(log); expect(params.headers).toStrictEqual({}); getResponseResult = oldGetResponse(params); return getResponseResult; }, ); adapter.onErrorWhileForwarding({ event, log, delegatedResolver: resolver, respondWithErrors, error, }); // eslint-disable-next-line @typescript-eslint/unbound-method expect(adapter.getResponse).toHaveBeenCalledTimes(1); expect(resolver.fail).toHaveBeenCalledTimes(0); expect(resolver.succeed).toHaveBeenCalledTimes(1); expect(resolver.succeed).toHaveBeenCalledWith(getResponseResult); }); }); }); ================================================ FILE: test/adapters/aws/api-gateway-v2.adapter.spec.ts ================================================ import type { APIGatewayProxyEventV2 } from 'aws-lambda'; import type { APIGatewayProxyStructuredResultV2 } from 'aws-lambda/trigger/api-gateway-proxy'; import { beforeEach, describe, expect, it, vitest } from 'vitest'; import { type DelegatedResolver, type GetResponseAdapterProps, type ILogger, ServerlessResponse, getEventBodyAsBuffer, getFlattenedHeadersMap, getMultiValueHeadersMap, getPathWithQueryStringParams, } from '../../../src'; import { ApiGatewayV2Adapter } from '../../../src/adapters/aws'; import { createCanHandleTestsForAdapter } from '../utils/can-handle'; import { createApiGatewayV2 } from './utils/api-gateway-v2'; describe(ApiGatewayV2Adapter.name, () => { let adapter!: ApiGatewayV2Adapter; beforeEach(() => { adapter = new ApiGatewayV2Adapter(); }); describe('getAdapterName', () => { it('should be the same name of the class', () => { expect(adapter.getAdapterName()).toBe(ApiGatewayV2Adapter.name); }); }); createCanHandleTestsForAdapter(() => new ApiGatewayV2Adapter(), undefined); describe('getRequest', () => { it('should return the correct mapping for the request', () => { const method = 'PUT'; const path = '/collaborators'; const body = { name: 'H4ad Collaborator' }; const queryParams = { page: '2' }; const cookies = ['batata', 'joga10']; const event = createApiGatewayV2( method, path, body, {}, queryParams, cookies, ); const result = adapter.getRequest(event); const remoteAddress = event.requestContext.http.sourceIp; expect(result).toHaveProperty('method', method); expect(result).toHaveProperty('headers'); expect(result).toHaveProperty('body'); expect(result.body).toBeInstanceOf(Buffer); const [bodyBuffer, contentLength] = getEventBodyAsBuffer( JSON.stringify(body), false, ); expect(result.body).toStrictEqual(bodyBuffer); expect(result.headers).toHaveProperty('content-length'); expect(result.headers['content-length']).toBe(String(contentLength)); expect(result.headers['cookie']).toBe(cookies.join('; ')); expect(result).toHaveProperty('remoteAddress', remoteAddress); const resultPath = getPathWithQueryStringParams( path, event.rawQueryString, ); expect(result).toHaveProperty('path', resultPath); }); it('should return the correct mapping for the request when it has no body', () => { const method = 'GET'; const path = '/collaborators'; const body = undefined; const event = createApiGatewayV2(method, path, body, {}, { page: '2' }); const result = adapter.getRequest(event); const remoteAddress = event.requestContext.http.sourceIp; expect(result).toHaveProperty('method', method); expect(result).toHaveProperty('headers'); expect(result.headers).not.toHaveProperty('cookie'); expect(result).toHaveProperty('body'); expect(result.body).not.toBeInstanceOf(Buffer); expect(result.body).toBeUndefined(); expect(result).toHaveProperty('remoteAddress', remoteAddress); const resultPath = getPathWithQueryStringParams( path, event.queryStringParameters, ); expect(result).toHaveProperty('path', resultPath); }); it('should return the correct mapping for the request when send stripBasePath', () => { const stripBasePath = '/prod'; const method = 'GET'; const path = '/prod/collaborators'; const body = undefined; const strippedAdapter = new ApiGatewayV2Adapter({ stripBasePath }); const event = createApiGatewayV2(method, path, body); const result = strippedAdapter.getRequest(event); const remoteAddress = event.requestContext.http.sourceIp; expect(result).toHaveProperty('method', method); expect(result).toHaveProperty('headers'); expect(result).toHaveProperty('body'); expect(result.body).not.toBeInstanceOf(Buffer); expect(result.body).toBeUndefined(); expect(result).toHaveProperty('remoteAddress', remoteAddress); const resultPath = getPathWithQueryStringParams( path.replace('/prod', ''), event.queryStringParameters, ); expect(result.path).toBe(resultPath); }); }); describe('getResponse', () => { it('should return the correct mapping for the response', () => { const method = 'PUT'; const path = '/collaborators'; const requestBody = { name: 'H4ad Collaborator V2' }; const queryParams = { page: '2' }; const cookies = ['batata', 'joga10']; const resultCookies = 'batata; joga10'; const resultBody = '{"success":true}'; const resultStatusCode = 200; const resultIsBase64Encoded = false; const event = createApiGatewayV2( method, path, requestBody, {}, queryParams, cookies, ); const resultHeaders = getFlattenedHeadersMap(event.headers); const result = adapter.getResponse({ event, log: {} as ILogger, body: resultBody, isBase64Encoded: resultIsBase64Encoded, statusCode: resultStatusCode, headers: { ...resultHeaders, 'set-cookie': resultCookies, }, }); expect(result).toHaveProperty('statusCode', 200); expect(result).toHaveProperty('body', resultBody); expect(result).toHaveProperty('headers'); expect(result.headers).not.toHaveProperty('set-cookie'); expect(result).toHaveProperty('cookies', [resultCookies]); expect(result).toHaveProperty('isBase64Encoded', resultIsBase64Encoded); }); it('should return the correct mapping for the response when set-cookie is array', () => { const method = 'PUT'; const path = '/collaborators'; const requestBody = { name: 'H4ad Collaborator V2' }; const queryParams = { page: '2' }; const cookies = ['batata', 'joga10']; const resultCookies = ['batata', 'joga10']; const resultBody = '{"success":true}'; const resultStatusCode = 200; const resultIsBase64Encoded = false; const event = createApiGatewayV2( method, path, requestBody, {}, queryParams, cookies, ); const resultHeaders = getFlattenedHeadersMap(event.headers); const result = adapter.getResponse({ event, log: {} as ILogger, body: resultBody, isBase64Encoded: resultIsBase64Encoded, statusCode: resultStatusCode, headers: { ...resultHeaders, 'set-cookie': resultCookies, }, }); expect(result).toHaveProperty('statusCode', 200); expect(result).toHaveProperty('body', resultBody); expect(result).toHaveProperty('headers'); expect(result.headers).not.toHaveProperty('set-cookie'); expect(result).toHaveProperty('cookies', resultCookies); expect(result).toHaveProperty('isBase64Encoded', resultIsBase64Encoded); }); it('should throw an error when framework send transfer-encoding=chunked in headers', () => { const method = 'GET'; const path = '/collaborators/stream'; const requestBody = undefined; const resultBody = '{"success":true}'; const resultStatusCode = 200; const resultIsBase64Encoded = false; const event = createApiGatewayV2(method, path, requestBody); const resultHeaders = getFlattenedHeadersMap(event.headers); resultHeaders['transfer-encoding'] = 'gzip,chunked'; expect(() => adapter.getResponse({ event, log: {} as ILogger, body: resultBody, isBase64Encoded: resultIsBase64Encoded, statusCode: resultStatusCode, headers: resultHeaders, }), ).toThrowError('is not supported'); const resultMultiValueHeaders = getMultiValueHeadersMap(event.headers); resultMultiValueHeaders['transfer-encoding'] = ['gzip', 'chunked']; expect(() => adapter.getResponse({ event, log: {} as ILogger, body: resultBody, isBase64Encoded: resultIsBase64Encoded, statusCode: resultStatusCode, headers: resultMultiValueHeaders, }), ).toThrowError('is not supported'); }); it('should throw an error when framework send chunkedEncoding=true in response', () => { const method = 'GET'; const path = '/collaborators/stream'; const requestBody = undefined; const resultBody = '{"success":true}'; const resultStatusCode = 200; const resultIsBase64Encoded = false; const event = createApiGatewayV2(method, path, requestBody); const resultHeaders = getFlattenedHeadersMap(event.headers); const fakeChunkedResponse = new ServerlessResponse({ method }); fakeChunkedResponse.chunkedEncoding = true; expect(() => adapter.getResponse({ event, log: {} as ILogger, body: resultBody, isBase64Encoded: resultIsBase64Encoded, statusCode: resultStatusCode, headers: resultHeaders, response: fakeChunkedResponse, }), ).toThrowError('is not supported'); }); describe('when throwOnChunkedTransferEncoding=false', () => { it('should NOT throw an error when framework send transfer-encoding=chunked in headers', () => { const customAdapter = new ApiGatewayV2Adapter({ throwOnChunkedTransferEncoding: false, }); const method = 'GET'; const path = '/collaborators/stream'; const requestBody = undefined; const resultBody = '{"success":true}'; const resultStatusCode = 200; const resultIsBase64Encoded = false; const event = createApiGatewayV2(method, path, requestBody); const resultHeaders = getFlattenedHeadersMap(event.headers); resultHeaders['transfer-encoding'] = 'gzip,chunked'; const result1 = customAdapter.getResponse({ event, log: {} as ILogger, body: resultBody, isBase64Encoded: resultIsBase64Encoded, statusCode: resultStatusCode, headers: resultHeaders, }); expect(result1.headers!['transfer-encoding']).toBeUndefined(); const resultMultiValueHeaders = getMultiValueHeadersMap(event.headers); resultMultiValueHeaders['transfer-encoding'] = ['gzip', 'chunked']; const result2 = customAdapter.getResponse({ event, log: {} as ILogger, body: resultBody, isBase64Encoded: resultIsBase64Encoded, statusCode: resultStatusCode, headers: resultMultiValueHeaders, }); expect(result2.headers!['transfer-encoding']).toBeUndefined(); }); it('should NOT throw an error when framework send chunkedEncoding=true in response', () => { const customAdapter = new ApiGatewayV2Adapter({ throwOnChunkedTransferEncoding: false, }); const method = 'GET'; const path = '/collaborators/stream'; const requestBody = undefined; const resultBody = '{"success":true}'; const resultStatusCode = 200; const resultIsBase64Encoded = false; const event = createApiGatewayV2(method, path, requestBody); const resultHeaders = getFlattenedHeadersMap(event.headers); const fakeChunkedResponse = new ServerlessResponse({ method }); fakeChunkedResponse.chunkedEncoding = true; const result = customAdapter.getResponse({ event, log: {} as ILogger, body: resultBody, isBase64Encoded: resultIsBase64Encoded, statusCode: resultStatusCode, headers: resultHeaders, response: fakeChunkedResponse, }); expect(result.headers!['transfer-encoding']).toBeUndefined(); }); }); }); describe('onErrorWhileForwarding', () => { it('should resolver call succeed', () => { const method = 'GET'; const path = '/events'; const requestBody = undefined; const event = createApiGatewayV2(method, path, requestBody); const log = {} as ILogger; const resolver: DelegatedResolver = { fail: vitest.fn(), succeed: vitest.fn(), }; const respondWithErrors = true; const error = new Error('Test error'); const oldGetResponse = adapter.getResponse.bind(adapter); let getResponseResult: APIGatewayProxyStructuredResultV2 | undefined; adapter.getResponse = vitest.fn( (params: GetResponseAdapterProps) => { expect(params.event).toBe(event); expect(params.statusCode).toBe(500); expect(params.body).toBe(error.stack); expect(params.isBase64Encoded).toBe(false); expect(params.log).toBe(log); expect(params.headers).toStrictEqual({}); getResponseResult = oldGetResponse(params); return getResponseResult; }, ); adapter.onErrorWhileForwarding({ event, log, delegatedResolver: resolver, respondWithErrors, error, }); // eslint-disable-next-line @typescript-eslint/unbound-method expect(adapter.getResponse).toHaveBeenCalledTimes(1); expect(resolver.fail).toHaveBeenCalledTimes(0); expect(resolver.succeed).toHaveBeenCalledTimes(1); expect(resolver.succeed).toHaveBeenCalledWith(getResponseResult); }); it('should resolver call succeed but without sending errors', () => { const method = 'GET'; const path = '/users'; const requestBody = undefined; const event = createApiGatewayV2(method, path, requestBody); const log = {} as ILogger; const resolver: DelegatedResolver = { fail: vitest.fn(), succeed: vitest.fn(), }; const respondWithErrors = false; const error = new Error('Test error'); const oldGetResponse = adapter.getResponse.bind(adapter); let getResponseResult: APIGatewayProxyStructuredResultV2 | undefined; adapter.getResponse = vitest.fn( (params: GetResponseAdapterProps) => { expect(params.event).toBe(event); expect(params.statusCode).toBe(500); expect(params.body).toBe(''); expect(params.isBase64Encoded).toBe(false); expect(params.log).toBe(log); expect(params.headers).toStrictEqual({}); getResponseResult = oldGetResponse(params); return getResponseResult; }, ); adapter.onErrorWhileForwarding({ event, log, delegatedResolver: resolver, respondWithErrors, error, }); // eslint-disable-next-line @typescript-eslint/unbound-method expect(adapter.getResponse).toHaveBeenCalledTimes(1); expect(resolver.fail).toHaveBeenCalledTimes(0); expect(resolver.succeed).toHaveBeenCalledTimes(1); expect(resolver.succeed).toHaveBeenCalledWith(getResponseResult); }); }); }); ================================================ FILE: test/adapters/aws/aws-simple-adapter.spec.ts ================================================ import { describe, expect, it, vitest } from 'vitest'; import { type DelegatedResolver, EmptyResponse, type ILogger, createDefaultLogger, getEventBodyAsBuffer, } from '../../../src'; import { AwsSimpleAdapter } from '../../../src/adapters/aws'; import { createDynamoDBEvent } from './utils/dynamodb'; import { createSNSEvent } from './utils/sns'; import { createSQSEvent } from './utils/sqs'; const sampleEvents = [ createSQSEvent(), createSNSEvent(), createDynamoDBEvent(), ]; class TestAdapter extends AwsSimpleAdapter {} describe(AwsSimpleAdapter.name, () => { describe('getAdapterName', () => { it('should throw not implemented error', () => { const adapter = new TestAdapter({ forwardPath: '/test', forwardMethod: 'POST', host: 'test.com.com', }); expect(() => adapter.getAdapterName()).toThrow('not implemented'); }); }); describe('canHandle', () => { it('should throw not implemented error', () => { const adapter = new TestAdapter({ forwardPath: '/test', forwardMethod: 'POST', host: 'test.com.com', }); expect(() => adapter.canHandle(null)).toThrow('not implemented'); }); }); describe('getRequest', () => { it('should return the correct mapping for the request', () => { for (const event of sampleEvents) { const adapter = new TestAdapter({ forwardPath: '/test', forwardMethod: 'POST', host: 'test.amazonaws.com', batch: false, }); const result = adapter.getRequest(event); expect(result.method).toBe('POST'); expect(result.path).toBe('/test'); expect(result.headers).toHaveProperty('host', 'test.amazonaws.com'); expect(result.headers).toHaveProperty( 'content-type', 'application/json', ); const [bodyBuffer, contentLength] = getEventBodyAsBuffer( JSON.stringify(event), false, ); expect(result.body).toBeInstanceOf(Buffer); expect(result.body).toStrictEqual(bodyBuffer); expect(result.headers).toHaveProperty( 'content-length', String(contentLength), ); } }); it('should return the correct mapping for the request with custom path and method', () => { const event = createSQSEvent(); const method = 'PUT'; const path = '/custom/test'; const customAdapter = new TestAdapter({ forwardMethod: method, forwardPath: path, host: 'test.amazonaws.com', }); const result = customAdapter.getRequest(event); expect(result.method).toBe(method); expect(result.path).toBe(path); expect(result.headers).toHaveProperty('host', 'test.amazonaws.com'); expect(result.headers).toHaveProperty('content-type', 'application/json'); const [bodyBuffer, contentLength] = getEventBodyAsBuffer( JSON.stringify(event), false, ); expect(result.body).toBeInstanceOf(Buffer); expect(result.body).toStrictEqual(bodyBuffer); expect(result.headers).toHaveProperty( 'content-length', String(contentLength), ); }); }); describe('getResponse', () => { it('should throw for invalid status', () => { const options: [status: number, error: boolean][] = [ [101, true], [200, false], [204, false], [301, false], [303, false], [400, true], [401, true], [404, true], [500, true], [503, true], ]; const adapter = new TestAdapter({ forwardPath: '/test', forwardMethod: 'POST', host: 'test.amazonaws.com', batch: false, }); for (const [status, shouldThrowError] of options) { if (shouldThrowError) { expect(() => adapter.getResponse({ event: null, body: JSON.stringify({ ok: true }), log: createDefaultLogger(), headers: {}, statusCode: status, isBase64Encoded: false, }), ).toThrowError(`"statusCode":${status}`); } else { expect(() => adapter.getResponse({ event: null, body: JSON.stringify({ ok: true }), log: createDefaultLogger(), headers: {}, statusCode: status, isBase64Encoded: false, }), ).not.toThrowError(`"statusCode":${status}`); } } }); describe('batch: false', () => { it('should not throw when body is base64', () => { const adapter = new TestAdapter({ forwardPath: '/test', forwardMethod: 'POST', host: 'test.amazonaws.com', batch: false, }); expect(() => adapter.getResponse({ event: null, body: JSON.stringify({ ok: true }), log: createDefaultLogger(), headers: {}, statusCode: 200, isBase64Encoded: true, }), ).not.toThrowError('could not be base64 encoded'); }); it('should return the correct mapping for the response', () => { const adapter = new TestAdapter({ forwardPath: '/test', forwardMethod: 'POST', host: 'test.amazonaws.com', batch: false, }); const result = adapter.getResponse({ event: null, body: JSON.stringify({ ok: true }), log: createDefaultLogger(), headers: {}, statusCode: 200, isBase64Encoded: false, }); expect(result).toBe(EmptyResponse); }); }); }); describe('batch: true', () => { it('should throw when body is base64', () => { const adapter = new TestAdapter({ forwardPath: '/test', forwardMethod: 'POST', host: 'test.amazonaws.com', batch: true, }); expect(() => adapter.getResponse({ event: null, body: JSON.stringify({ ok: true }), log: createDefaultLogger(), headers: {}, statusCode: 200, isBase64Encoded: true, }), ).toThrowError('could not be base64 encoded'); }); it('should return the body when response is correct', () => { const adapter = new TestAdapter({ forwardPath: '/test', forwardMethod: 'POST', host: 'test.amazonaws.com', batch: true, }); const body = { ok: true }; const response = adapter.getResponse({ event: null, body: JSON.stringify(body), log: createDefaultLogger(), headers: {}, statusCode: 200, isBase64Encoded: false, }); expect(response).toStrictEqual(body); }); it('should return empty when body is also empty', () => { const adapter = new TestAdapter({ forwardPath: '/test', forwardMethod: 'POST', host: 'test.amazonaws.com', batch: true, }); const body = ''; const response = adapter.getResponse({ event: null, body, log: createDefaultLogger(), headers: {}, statusCode: 200, isBase64Encoded: false, }); expect(response).toStrictEqual(EmptyResponse); }); }); describe('onErrorWhileForwarding', () => { it('should resolver just call fail without get response', () => { const adapter = new TestAdapter({ forwardPath: '/test', forwardMethod: 'POST', host: 'test.amazonaws.com', batch: true, }); const error = new Error('fail because I need to test.'); const resolver: DelegatedResolver = { fail: vitest.fn(), succeed: vitest.fn(), }; adapter.getResponse = vitest.fn(); adapter.onErrorWhileForwarding({ event: {}, error, delegatedResolver: resolver, log: {} as ILogger, respondWithErrors: false, }); // eslint-disable-next-line @typescript-eslint/unbound-method expect(adapter.getResponse).toHaveBeenCalledTimes(0); expect(resolver.fail).toHaveBeenCalledWith(error); expect(resolver.succeed).toHaveBeenCalledTimes(0); }); }); }); ================================================ FILE: test/adapters/aws/dynamodb.adapter.spec.ts ================================================ import { beforeEach, describe, expect, it } from 'vitest'; import { getEventBodyAsBuffer } from '../../../src'; import { DynamoDBAdapter } from '../../../src/adapters/aws'; import { createCanHandleTestsForAdapter } from '../utils/can-handle'; import { createDynamoDBEvent } from './utils/dynamodb'; describe(DynamoDBAdapter.name, () => { let adapter!: DynamoDBAdapter; beforeEach(() => { adapter = new DynamoDBAdapter(); }); describe('getAdapterName', () => { it('should be the same name of the class', () => { expect(adapter.getAdapterName()).toBe(DynamoDBAdapter.name); }); }); createCanHandleTestsForAdapter(() => new DynamoDBAdapter(), undefined); describe('getRequest', () => { it('should return the correct mapping for the request', () => { const event = createDynamoDBEvent(); const result = adapter.getRequest(event); expect(result.method).toBe('POST'); expect(result.path).toBe('/dynamo'); expect(result.headers).toHaveProperty('host', 'dynamodb.amazonaws.com'); expect(result.headers).toHaveProperty('content-type', 'application/json'); const [bodyBuffer, contentLength] = getEventBodyAsBuffer( JSON.stringify(event), false, ); expect(result.body).toBeInstanceOf(Buffer); expect(result.body).toStrictEqual(bodyBuffer); expect(result.headers).toHaveProperty( 'content-length', String(contentLength), ); }); it('should return the correct mapping for the request with custom path and method', () => { const event = createDynamoDBEvent(); const method = 'PUT'; const path = '/custom/dynamo'; const customAdapter = new DynamoDBAdapter({ dynamoDBForwardMethod: method, dynamoDBForwardPath: path, }); const result = customAdapter.getRequest(event); expect(result.method).toBe(method); expect(result.path).toBe(path); expect(result.headers).toHaveProperty('host', 'dynamodb.amazonaws.com'); expect(result.headers).toHaveProperty('content-type', 'application/json'); const [bodyBuffer, contentLength] = getEventBodyAsBuffer( JSON.stringify(event), false, ); expect(result.body).toBeInstanceOf(Buffer); expect(result.body).toStrictEqual(bodyBuffer); expect(result.headers).toHaveProperty( 'content-length', String(contentLength), ); }); }); }); ================================================ FILE: test/adapters/aws/event-bridge.adapter.spec.ts ================================================ import { beforeEach, describe, expect, it } from 'vitest'; import { getEventBodyAsBuffer } from '../../../src'; import { EventBridgeAdapter } from '../../../src/adapters/aws'; import { createCanHandleTestsForAdapter } from '../utils/can-handle'; import { createEventBridgeEvent } from './utils/event-bridge'; describe(EventBridgeAdapter.name, () => { let adapter!: EventBridgeAdapter; beforeEach(() => { adapter = new EventBridgeAdapter(); }); describe('getAdapterName', () => { it('should be the same name of the class', () => { expect(adapter.getAdapterName()).toBe(EventBridgeAdapter.name); }); }); createCanHandleTestsForAdapter(() => new EventBridgeAdapter(), undefined); describe('getRequest', () => { it('should return the correct mapping for the request', () => { const event = createEventBridgeEvent(); const result = adapter.getRequest(event); expect(result.method).toBe('POST'); expect(result.path).toBe('/eventbridge'); expect(result.headers).toHaveProperty('host', 'events.amazonaws.com'); expect(result.headers).toHaveProperty('content-type', 'application/json'); const [bodyBuffer, contentLength] = getEventBodyAsBuffer( JSON.stringify(event), false, ); expect(result.body).toBeInstanceOf(Buffer); expect(result.body).toStrictEqual(bodyBuffer); expect(result.headers).toHaveProperty( 'content-length', String(contentLength), ); }); it('should return the correct mapping for the request with custom path and method', () => { const method = 'PUT'; const path = '/prod/eventbridge'; const customAdapter = new EventBridgeAdapter({ eventBridgeForwardMethod: method, eventBridgeForwardPath: path, }); const event = createEventBridgeEvent(); const result = customAdapter.getRequest(event); expect(result.method).toBe(method); expect(result.path).toBe(path); expect(result.headers).toHaveProperty('host', 'events.amazonaws.com'); expect(result.headers).toHaveProperty('content-type', 'application/json'); const [bodyBuffer, contentLength] = getEventBodyAsBuffer( JSON.stringify(event), false, ); expect(result.body).toBeInstanceOf(Buffer); expect(result.body).toStrictEqual(bodyBuffer); expect(result.headers).toHaveProperty( 'content-length', String(contentLength), ); }); }); }); ================================================ FILE: test/adapters/aws/lambda-edge.adapter.spec.ts ================================================ import { join } from 'path'; import type { CloudFrontHeaders, CloudFrontRequest, } from 'aws-lambda/common/cloudfront'; import type { CloudFrontRequestEvent, CloudFrontRequestResult, } from 'aws-lambda/trigger/cloudfront-request'; import { beforeEach, describe, expect, it, vitest } from 'vitest'; import { type BothValueHeaders, type DelegatedResolver, type ILogger, type MultiValueHeaders, type SingleValueHeaders, } from '../../../src'; import { DEFAULT_LAMBDA_EDGE_DISALLOWED_HEADERS, DEFAULT_ORIGIN_MAX_RESPONSE_SIZE_IN_BYTES, DEFAULT_VIEWER_MAX_RESPONSE_SIZE_IN_BYTES, LambdaEdgeAdapter, } from '../../../src/adapters/aws'; import { createCanHandleTestsForAdapter } from '../utils/can-handle'; import { createLambdaEdgeOriginEvent, createLambdaEdgeViewerEvent, } from './utils/lambda-edge'; describe(LambdaEdgeAdapter.name, () => { let adapter!: LambdaEdgeAdapter; beforeEach(() => { adapter = new LambdaEdgeAdapter(); }); describe('getAdapterName', () => { it('should be the same name of the class', () => { expect(adapter.getAdapterName()).toBe(LambdaEdgeAdapter.name); }); }); createCanHandleTestsForAdapter(() => new LambdaEdgeAdapter(), undefined); describe('getRequest', () => { it('should return the correct mapping for the request', () => { const events: [ factory: | typeof createLambdaEdgeOriginEvent | typeof createLambdaEdgeViewerEvent, method: string, path: string, body?: any, ][] = [ [createLambdaEdgeOriginEvent, 'GET', '/image.png', undefined], [ createLambdaEdgeOriginEvent, 'POST', 'batata.png', { base64: Buffer.from('batata', 'utf-8').toString('base64'), }, ], [createLambdaEdgeViewerEvent, 'GET', '/image4343.png', undefined], [ createLambdaEdgeViewerEvent, 'PUT', 'banana.png', { base64: Buffer.from('batata', 'utf-8').toString('base64'), }, ], ]; for (const [createEvent, method, path, body] of events) { const lambdaEdgeEvent = createEvent(method, path, body); const cloudfrontRequest = lambdaEdgeEvent.Records[0].cf.request; const result = adapter.getRequest(lambdaEdgeEvent); const keys = Object.keys(result); const expectedKeys = [ 'method', 'path', 'headers', 'body', 'remoteAddress', 'host', 'hostname', ]; expect(keys.length === expectedKeys.length).toBe(true); expect(keys.every(key => expectedKeys.includes(key))).toBe(true); expect(result.method).toBe(method); expect(result.path).toBe(path); const someHeaderValueIsArray = Object.values(result.headers).some( Array.isArray, ); expect(someHeaderValueIsArray).toBe(false); const headerKeys = Object.keys(result.headers); const expectedHeaderKeys = Object.keys(cloudfrontRequest.headers); if (result.body) expectedHeaderKeys.push('content-length'); expect(headerKeys.length === expectedHeaderKeys.length).toBe(true); expect(headerKeys.every(key => expectedHeaderKeys.includes(key))).toBe( true, ); if (result.body === undefined) expect(result.body).toBeUndefined(); else { const dataAsBase64 = Buffer.from( JSON.stringify(body), 'utf-8', ).toString('base64'); const jsonString = JSON.stringify({ action: 'read-only', encoding: 'base64', inputTruncated: false, data: dataAsBase64, }); expect(result.body.toString('utf-8')).toBe(jsonString); } expect(result.remoteAddress).toBe(cloudfrontRequest.clientIp); const host = cloudfrontRequest.headers['host'][0].value; expect(result.host).toBe(host); expect(result.hostname).toBe(host); } }); it('should return the correct mapping for the request with query params', () => { const lambdaEvent = createLambdaEdgeOriginEvent( 'GET', '/image_of_apple.png', undefined, undefined, 'pretty=true', ); const result = adapter.getRequest(lambdaEvent); expect(result.path).toBe('/image_of_apple.png?pretty=true'); }); it('should return the correct mapping for the request with custom path function', () => { const lambdaEvent = createLambdaEdgeOriginEvent( 'GET', '/image2.png', undefined, undefined, 'potato=true', ); const customAdapter = new LambdaEdgeAdapter({ getPathFromEvent: event => join('/prod', event.cf.request.uri), }); const result = customAdapter.getRequest(lambdaEvent); expect(result.path).toBe('/prod/image2.png'); // certifies the behavior described in the comments of `getPathFromEvent`. expect(result.path).not.toBe('/prod/image2.png?potato=true'); }); }); describe('getResponse', () => { it('should return the correct mapping for the response', () => { const options: CloudFrontRequestEvent[] = [ createLambdaEdgeOriginEvent('GET', '/potato.png'), createLambdaEdgeViewerEvent('GET', '/apple.png'), ]; for (const event of options) { const cloudFrontRequest = event.Records[0].cf.request; const body = JSON.stringify(cloudFrontRequest); const result = adapter.getResponse({ event, body, headers: {}, log: {} as ILogger, statusCode: 200, isBase64Encoded: false, }) as CloudFrontRequest; expect(result).toBeDefined(); expect(result).toHaveProperty('headers'); expect(result.headers).toHaveProperty( 'host', cloudFrontRequest.headers['host'], ); expect(result).toHaveProperty('clientIp', cloudFrontRequest.clientIp); expect(result).toHaveProperty('method', cloudFrontRequest.method); expect(result.origin).toEqual(cloudFrontRequest.origin); expect(result).toHaveProperty( 'querystring', cloudFrontRequest.querystring, ); expect(result).toHaveProperty('uri', cloudFrontRequest.uri); expect(result).not.toHaveProperty('body'); } }); it('should return the correct mapping for the response even if we reach the max response size', () => { const bigResponseForOrigin = new Array( DEFAULT_ORIGIN_MAX_RESPONSE_SIZE_IN_BYTES + 1, ).map(() => 'a'); const bigResponseForView = new Array( DEFAULT_VIEWER_MAX_RESPONSE_SIZE_IN_BYTES + 1, ).map(() => 'b'); const options: CloudFrontRequestEvent[] = [ createLambdaEdgeOriginEvent('GET', '/potato.png', { bigResponseForOrigin, }), createLambdaEdgeViewerEvent('GET', '/apple.png', { bigResponseForView, }), ]; for (const event of options) { const cloudFrontRequest = event.Records[0].cf.request; const body = JSON.stringify(cloudFrontRequest); const log = { error: vitest.fn(message => expect(message).toContain('Max response size exceeded'), ) as any, } as ILogger; adapter.getResponse({ event, body, headers: {}, log, statusCode: 200, isBase64Encoded: false, }); expect(log.error).toHaveBeenCalledTimes(1); } }); it('should return the correct mapping for the response with option "shouldUseHeadersFromFramework"', () => { const event = createLambdaEdgeViewerEvent('GET', '/potato.png'); const cloudFrontRequest = event.Records[0].cf.request; const body = JSON.stringify(cloudFrontRequest); const customAdapter = new LambdaEdgeAdapter({ shouldUseHeadersFromFramework: true, }); const options: BothValueHeaders[] = [ { batata: 'true' }, { batata: ['true'] }, ]; for (const headers of options) { const result = customAdapter.getResponse({ event, body, headers, log: {} as ILogger, statusCode: 200, isBase64Encoded: false, }); expect(result!.headers!['batata']).toStrictEqual([ { key: 'batata', value: 'true' }, ]); expect(result!.headers!['batata']).not.toStrictEqual([ { key: 'batata', value: Math.random().toString() }, ]); } }); it('should return the correct mapping for the response with option "disallowedHeaders"', () => { const disallowedHeadersList = DEFAULT_LAMBDA_EDGE_DISALLOWED_HEADERS.filter( header => typeof header === 'string', ) as string[]; expect(disallowedHeadersList.length > 0).toBe(true); const allDisallowedHeadersMap: SingleValueHeaders = {}; const allDisallowedMultiValueHeadersMap: MultiValueHeaders = {}; const allDisallowedCloudfrontHeaders: CloudFrontHeaders = {}; for (const header of disallowedHeadersList) { allDisallowedHeadersMap[header] = Math.random().toString(); allDisallowedMultiValueHeadersMap[header] = [Math.random().toString()]; allDisallowedCloudfrontHeaders[header] = [ { key: header, value: Math.random().toString() }, ]; } const options: [ adapter: LambdaEdgeAdapter, event: CloudFrontRequestEvent, headers: BothValueHeaders, ][] = [ [ new LambdaEdgeAdapter({ disallowedHeaders: disallowedHeadersList, }), createLambdaEdgeViewerEvent( 'GET', '/potato.png', undefined, allDisallowedCloudfrontHeaders, ), {}, ], [ new LambdaEdgeAdapter({ disallowedHeaders: disallowedHeadersList, }), createLambdaEdgeOriginEvent( 'GET', '/potato.png', undefined, allDisallowedCloudfrontHeaders, ), {}, ], [ new LambdaEdgeAdapter({ shouldUseHeadersFromFramework: true, disallowedHeaders: disallowedHeadersList, }), createLambdaEdgeViewerEvent('GET', '/potato.png'), allDisallowedHeadersMap, ], [ new LambdaEdgeAdapter({ shouldUseHeadersFromFramework: true, disallowedHeaders: disallowedHeadersList, }), createLambdaEdgeViewerEvent('GET', '/apple.png'), allDisallowedMultiValueHeadersMap, ], [ new LambdaEdgeAdapter({ shouldUseHeadersFromFramework: true, disallowedHeaders: disallowedHeadersList, }), createLambdaEdgeOriginEvent('GET', '/apple.png'), allDisallowedHeadersMap, ], [ new LambdaEdgeAdapter({ shouldUseHeadersFromFramework: true, disallowedHeaders: disallowedHeadersList, }), createLambdaEdgeOriginEvent('GET', '/apple.png'), allDisallowedMultiValueHeadersMap, ], ]; for (const [customAdapter, event, headers] of options) { const cloudFrontRequest = event.Records[0].cf.request; const body = JSON.stringify(cloudFrontRequest); const result = customAdapter.getResponse({ event, body, headers, log: {} as ILogger, statusCode: 200, isBase64Encoded: false, }); expect(Object.keys(result!.headers!)).toHaveLength(0); } }); it('should return the correct mapping for the response with option "shouldStripHeader"', () => { const customAdapter = new LambdaEdgeAdapter({ shouldStripHeader: () => true, }); const options: CloudFrontRequestEvent[] = [ createLambdaEdgeViewerEvent('GET', '/potato.png'), createLambdaEdgeOriginEvent('GET', '/apple.png'), ]; for (const event of options) { const cloudFrontRequest = event.Records[0].cf.request; const body = JSON.stringify(cloudFrontRequest); const result = customAdapter.getResponse({ event, body, headers: {}, log: {} as ILogger, statusCode: 200, isBase64Encoded: false, }); expect(Object.keys(result!.headers!)).toHaveLength(0); } }); it('should return the correct mapping for the response with option "onResponseSizeExceedLimit"', () => { const customAdapter = new LambdaEdgeAdapter({ originMaxResponseSizeInBytes: 0, viewerMaxResponseSizeInBytes: 0, }); const options: CloudFrontRequestEvent[] = [ createLambdaEdgeViewerEvent('GET', '/potato.png', { potato: true }), createLambdaEdgeOriginEvent('GET', '/apple.png', { apple: true }), ]; for (const event of options) { const cloudFrontRequest = event.Records[0].cf.request; const body = JSON.stringify(cloudFrontRequest); const log = { error: vitest.fn(message => expect(message).toContain('Max response size exceeded'), ) as any, } as ILogger; const result = customAdapter.getResponse({ event, body, headers: {}, log, statusCode: 200, isBase64Encoded: false, }); expect(result).toBeDefined(); expect(log.error).toHaveBeenCalledTimes(1); const onResponseSizeExceedLimit = vitest.fn(); const customAdapter2 = new LambdaEdgeAdapter({ originMaxResponseSizeInBytes: 0, viewerMaxResponseSizeInBytes: 0, onResponseSizeExceedLimit, }); customAdapter2.getResponse({ event, body, headers: {}, log: {} as ILogger, statusCode: 200, isBase64Encoded: false, }); expect(onResponseSizeExceedLimit).toHaveBeenCalled(); } }); }); describe('onErrorWhileForwarding', () => { it('should resolver call succeed', () => { const options: [ event: CloudFrontRequestEvent, respondWithError: boolean, ][] = [ [ createLambdaEdgeViewerEvent('GET', '/potato.png', { potato: true }), false, ], [ createLambdaEdgeOriginEvent('GET', '/apple.png', { apple: true }), false, ], [createLambdaEdgeViewerEvent('GET', '/mapple.png'), true], [createLambdaEdgeOriginEvent('GET', '/juice.png'), true], ]; for (const [event, respondWithErrors] of options) { const log = {} as ILogger; const resolver: DelegatedResolver = { fail: vitest.fn(), succeed: vitest.fn(), }; const error = new Error('Test error'); adapter.onErrorWhileForwarding({ event, log, delegatedResolver: resolver, respondWithErrors, error, }); expect(resolver.fail).toHaveBeenCalledTimes(1); expect(resolver.succeed).toHaveBeenCalledTimes(0); } }); }); }); ================================================ FILE: test/adapters/aws/request-lambda-edge.adapter.spec.ts ================================================ import type { CloudFrontHeaders } from 'aws-lambda/common/cloudfront'; import type { CloudFrontRequestEvent, CloudFrontRequestResult, } from 'aws-lambda/trigger/cloudfront-request'; import { beforeEach, describe, expect, it, vitest } from 'vitest'; import { type BothValueHeaders, type DelegatedResolver, type ILogger, type MultiValueHeaders, type SingleValueHeaders, } from '../../../src'; import { DEFAULT_LAMBDA_EDGE_DISALLOWED_HEADERS, DEFAULT_ORIGIN_MAX_RESPONSE_SIZE_IN_BYTES, DEFAULT_VIEWER_MAX_RESPONSE_SIZE_IN_BYTES, RequestLambdaEdgeAdapter, } from '../../../src/adapters/aws'; import { createLambdaEdgeOriginEvent, createLambdaEdgeViewerEvent, } from './utils/lambda-edge'; describe(RequestLambdaEdgeAdapter.name, () => { let adapter!: RequestLambdaEdgeAdapter; beforeEach(() => { adapter = new RequestLambdaEdgeAdapter(); }); describe('getAdapterName', () => { it('should be the same name of the class', () => { expect(adapter.getAdapterName()).toBe(RequestLambdaEdgeAdapter.name); }); }); describe('canHandle', () => { it('should handle origin-request and viewer-request', () => { expect( adapter.canHandle(createLambdaEdgeViewerEvent('GET', '/users')), ).toBe(true); expect( adapter.canHandle(createLambdaEdgeOriginEvent('GET', '/users')), ).toBe(true); expect(adapter.canHandle({})).toBe(false); }); }); describe('getRequest', () => { it('should return the correct mapping for the request', () => { const events: [ factory: | typeof createLambdaEdgeOriginEvent | typeof createLambdaEdgeViewerEvent, method: string, path: string, body?: any, headers?: CloudFrontHeaders, ][] = [ [createLambdaEdgeOriginEvent, 'GET', '/projects', undefined], [ createLambdaEdgeOriginEvent, 'GET', '/users', undefined, { test: [{ key: 'test', value: '1' }], test2: [ { key: 'Test2', value: '1' }, { key: 'Test2', value: '2' }, ], }, ], [ createLambdaEdgeOriginEvent, 'POST', 'batata.png', { base64: Buffer.from('batata', 'utf-8').toString('base64'), }, ], [createLambdaEdgeViewerEvent, 'GET', '/products', undefined], [ createLambdaEdgeViewerEvent, 'PUT', '/tests', { base64: Buffer.from('batata', 'utf-8').toString('base64'), }, ], ]; for (const [createEvent, method, path, body, headers] of events) { const lambdaEdgeEvent = createEvent(method, path, body, headers); const cloudfrontRequest = lambdaEdgeEvent.Records[0].cf.request; const result = adapter.getRequest(lambdaEdgeEvent); const keys = Object.keys(result); const expectedKeys = [ 'method', 'path', 'headers', 'body', 'remoteAddress', 'host', 'hostname', ]; expect(keys.length === expectedKeys.length).toBe(true); expect(keys.every(key => expectedKeys.includes(key))).toBe(true); expect(result.method).toBe(method); expect(result.path).toBe(path); const someHeaderValueIsArray = Object.values(result.headers).some( Array.isArray, ); expect(someHeaderValueIsArray).toBe(false); const headerKeys = Object.keys(result.headers); const expectedHeaderKeys = Object.keys(cloudfrontRequest.headers); if (result.body) expectedHeaderKeys.push('content-length'); expect(headerKeys.length === expectedHeaderKeys.length).toBe(true); expect(headerKeys.every(key => expectedHeaderKeys.includes(key))).toBe( true, ); if (result.body === undefined) expect(result.body).toBeUndefined(); else expect(result.body.toString('utf-8')).toBe(JSON.stringify(body)); expect(result.remoteAddress).toBe(cloudfrontRequest.clientIp); if (cloudfrontRequest.headers['host']) { const host = cloudfrontRequest.headers['host'][0].value; expect(result.host).toBe(host); expect(result.hostname).toBe(host); } else { expect(result.host).toBeUndefined(); expect(result.hostname).toBeUndefined(); } } }); it('should return the correct mapping for the request with query params', () => { const lambdaEvent = createLambdaEdgeOriginEvent( 'GET', '/image_of_apple.png', undefined, undefined, 'pretty=true', ); const result = adapter.getRequest(lambdaEvent); expect(result.path).toBe('/image_of_apple.png?pretty=true'); }); it('should return the correct path for the request with stripBasePath', () => { const lambdaEvent = createLambdaEdgeOriginEvent( 'GET', '/api/users', undefined, undefined, 'potato=true', ); const customAdapter = new RequestLambdaEdgeAdapter({ stripBasePath: '/api', }); const result = customAdapter.getRequest(lambdaEvent); expect(result.path).toBe('/users?potato=true'); }); }); describe('getResponse', () => { it('should return the correct mapping for the response', () => { const options: CloudFrontRequestEvent[] = [ createLambdaEdgeOriginEvent('GET', '/users'), createLambdaEdgeViewerEvent('GET', '/products'), ]; for (const event of options) { const cloudFrontRequest = event.Records[0].cf.request; const body = JSON.stringify(cloudFrontRequest); const result = adapter.getResponse({ event, body, headers: { Test: 'value', }, log: {} as ILogger, statusCode: 200, isBase64Encoded: false, }); expect(result).toBeDefined(); expect(result.headers).toEqual({ test: [{ key: 'Test', value: 'value' }], }); expect(result.bodyEncoding).toEqual('text'); expect(result.status).toEqual('200'); expect(result.body).toEqual(body); } }); it('should return the correct mapping for the response when base64', () => { const options: CloudFrontRequestEvent[] = [ createLambdaEdgeOriginEvent('GET', '/users'), createLambdaEdgeViewerEvent('GET', '/products'), ]; for (const event of options) { const cloudFrontRequest = event.Records[0].cf.request; const body = Buffer.from(JSON.stringify(cloudFrontRequest)).toString( 'base64', ); const result = adapter.getResponse({ event, body, headers: { Test: 'value', Single: ['2'], Test2: ['1', '2'], }, log: {} as ILogger, statusCode: 200, isBase64Encoded: true, }); expect(result).toBeDefined(); expect(result.headers).toEqual({ test: [{ key: 'Test', value: 'value' }], single: [{ key: 'Single', value: '2' }], test2: [ { key: 'Test2', value: '1' }, { key: 'Test2', value: '2' }, ], }); expect(result.bodyEncoding).toEqual('base64'); expect(result.status).toEqual('200'); expect(result.body).toEqual(body); } }); it('should return the correct mapping for the response even if we reach the max response size', () => { const bigResponseForOrigin = new Array( DEFAULT_ORIGIN_MAX_RESPONSE_SIZE_IN_BYTES + 1, ).map(() => 'a'); const bigResponseForView = new Array( DEFAULT_VIEWER_MAX_RESPONSE_SIZE_IN_BYTES + 1, ).map(() => 'b'); const options: CloudFrontRequestEvent[] = [ createLambdaEdgeOriginEvent('GET', '/users', { bigResponseForOrigin, }), createLambdaEdgeViewerEvent('GET', '/products', { bigResponseForView, }), ]; for (const event of options) { const cloudFrontRequest = event.Records[0].cf.request; const body = JSON.stringify(cloudFrontRequest); const log = { error: vitest.fn(message => expect(message).toContain('Max response size exceeded'), ) as any, } as ILogger; adapter.getResponse({ event, body, headers: {}, log, statusCode: 200, isBase64Encoded: false, }); expect(log.error).toHaveBeenCalledTimes(1); } }); it('should return the correct mapping for the response with option "disallowedHeaders"', () => { const disallowedHeadersList = DEFAULT_LAMBDA_EDGE_DISALLOWED_HEADERS.filter( header => typeof header === 'string', ) as string[]; expect(disallowedHeadersList.length > 0).toBe(true); const allDisallowedHeadersMap: SingleValueHeaders = {}; const allDisallowedMultiValueHeadersMap: MultiValueHeaders = {}; for (const header of disallowedHeadersList) { allDisallowedHeadersMap[header] = Math.random().toString(); allDisallowedMultiValueHeadersMap[header] = [Math.random().toString()]; } const options: [ adapter: RequestLambdaEdgeAdapter, event: CloudFrontRequestEvent, headers: BothValueHeaders, ][] = [ [ new RequestLambdaEdgeAdapter({ disallowedHeaders: disallowedHeadersList, }), createLambdaEdgeOriginEvent('GET', '/products', undefined), allDisallowedHeadersMap, ], [ new RequestLambdaEdgeAdapter({ disallowedHeaders: disallowedHeadersList, }), createLambdaEdgeOriginEvent('GET', '/products', undefined), allDisallowedMultiValueHeadersMap, ], ]; for (const [customAdapter, event, headers] of options) { const cloudFrontRequest = event.Records[0].cf.request; const body = JSON.stringify(cloudFrontRequest); const result = customAdapter.getResponse({ event, body, headers, log: {} as ILogger, statusCode: 200, isBase64Encoded: false, }); expect(Object.keys(result.headers!)).toHaveLength(0); } }); it('should return the correct mapping for the response with option "shouldStripHeader"', () => { const customAdapter = new RequestLambdaEdgeAdapter({ shouldStripHeader: () => true, }); const options: [ event: CloudFrontRequestEvent, headers: BothValueHeaders, ][] = [ [createLambdaEdgeViewerEvent('GET', '/users'), { test: '1' }], [ createLambdaEdgeOriginEvent('GET', '/products'), { test: '2', potato: ['3', '4'] }, ], ]; for (const [event, headers] of options) { const cloudFrontRequest = event.Records[0].cf.request; const body = JSON.stringify(cloudFrontRequest); const result = customAdapter.getResponse({ event, body, headers, log: {} as ILogger, statusCode: 200, isBase64Encoded: false, }); expect(Object.keys(result.headers!)).toHaveLength(0); } }); it('should return the correct mapping for the response with option "onResponseSizeExceedLimit"', () => { const customAdapter = new RequestLambdaEdgeAdapter({ originMaxResponseSizeInBytes: 0, viewerMaxResponseSizeInBytes: 0, }); const options: CloudFrontRequestEvent[] = [ createLambdaEdgeViewerEvent('GET', '/users', { potato: true }), createLambdaEdgeOriginEvent('GET', '/products', { apple: true }), ]; for (const event of options) { const cloudFrontRequest = event.Records[0].cf.request; const body = JSON.stringify(cloudFrontRequest); const log = { error: vitest.fn(message => expect(message).toContain('Max response size exceeded'), ) as any, } as ILogger; const result = customAdapter.getResponse({ event, body, headers: {}, log, statusCode: 200, isBase64Encoded: false, }); expect(result).toBeDefined(); expect(log.error).toHaveBeenCalledTimes(1); const onResponseSizeExceedLimit = vitest.fn(); const customAdapter2 = new RequestLambdaEdgeAdapter({ originMaxResponseSizeInBytes: 0, viewerMaxResponseSizeInBytes: 0, onResponseSizeExceedLimit, }); customAdapter2.getResponse({ event, body, headers: {}, log: {} as ILogger, statusCode: 200, isBase64Encoded: false, }); expect(onResponseSizeExceedLimit).toHaveBeenCalled(); } }); }); describe('onErrorWhileForwarding', () => { it('should resolver call succeed', () => { const options: [ event: CloudFrontRequestEvent, respondWithError: boolean, ][] = [ [createLambdaEdgeViewerEvent('GET', '/users', { potato: true }), false], [createLambdaEdgeOriginEvent('GET', '/users', { apple: true }), false], [createLambdaEdgeViewerEvent('GET', '/products'), true], [createLambdaEdgeOriginEvent('GET', '/juices'), true], ]; for (const [event, respondWithErrors] of options) { const log = {} as ILogger; const resolver: DelegatedResolver = { fail: vitest.fn(), succeed: vitest.fn(), }; const error = new Error('Test error'); adapter.onErrorWhileForwarding({ event, log, delegatedResolver: resolver, respondWithErrors, error, }); expect(resolver.fail).toHaveBeenCalledTimes(0); expect(resolver.succeed).toHaveBeenCalledTimes(1); } }); }); }); ================================================ FILE: test/adapters/aws/s3.adapter.spec.ts ================================================ import { beforeEach, describe, expect, it } from 'vitest'; import { getEventBodyAsBuffer } from '../../../src'; import { S3Adapter } from '../../../src/adapters/aws'; import { createCanHandleTestsForAdapter } from '../utils/can-handle'; import { createS3Event } from './utils/s3'; describe(S3Adapter.name, () => { let adapter!: S3Adapter; beforeEach(() => { adapter = new S3Adapter(); }); describe('getAdapterName', () => { it('should be the same name of the class', () => { expect(adapter.getAdapterName()).toBe(S3Adapter.name); }); }); createCanHandleTestsForAdapter(() => new S3Adapter(), undefined); describe('getRequest', () => { it('should return the correct mapping for the request', () => { const event = createS3Event(); const result = adapter.getRequest(event); expect(result.method).toBe('POST'); expect(result.path).toBe('/s3'); expect(result.headers).toHaveProperty('host', 's3.amazonaws.com'); expect(result.headers).toHaveProperty('content-type', 'application/json'); const [bodyBuffer, contentLength] = getEventBodyAsBuffer( JSON.stringify(event), false, ); expect(result.body).toBeInstanceOf(Buffer); expect(result.body).toStrictEqual(bodyBuffer); expect(result.headers).toHaveProperty( 'content-length', String(contentLength), ); }); it('should return the correct mapping for the request with custom path and method', () => { const event = createS3Event(); const method = 'PUT'; const path = '/custom/s3'; const customAdapter = new S3Adapter({ s3ForwardMethod: method, s3ForwardPath: path, }); const result = customAdapter.getRequest(event); expect(result.method).toBe(method); expect(result.path).toBe(path); expect(result.headers).toHaveProperty('host', 's3.amazonaws.com'); expect(result.headers).toHaveProperty('content-type', 'application/json'); const [bodyBuffer, contentLength] = getEventBodyAsBuffer( JSON.stringify(event), false, ); expect(result.body).toBeInstanceOf(Buffer); expect(result.body).toStrictEqual(bodyBuffer); expect(result.headers).toHaveProperty( 'content-length', String(contentLength), ); }); }); }); ================================================ FILE: test/adapters/aws/sns.adapter.spec.ts ================================================ import { beforeEach, describe, expect, it } from 'vitest'; import { getEventBodyAsBuffer } from '../../../src'; import { SNSAdapter } from '../../../src/adapters/aws'; import { createCanHandleTestsForAdapter } from '../utils/can-handle'; import { createSNSEvent } from './utils/sns'; describe(SNSAdapter.name, () => { let adapter!: SNSAdapter; beforeEach(() => { adapter = new SNSAdapter(); }); describe('getAdapterName', () => { it('should be the same name of the class', () => { expect(adapter.getAdapterName()).toBe(SNSAdapter.name); }); }); createCanHandleTestsForAdapter(() => new SNSAdapter(), undefined); describe('getRequest', () => { it('should return the correct mapping for the request', () => { const event = createSNSEvent(); const result = adapter.getRequest(event); expect(result.method).toBe('POST'); expect(result.path).toBe('/sns'); expect(result.headers).toHaveProperty('host', 'sns.amazonaws.com'); expect(result.headers).toHaveProperty('content-type', 'application/json'); const [bodyBuffer, contentLength] = getEventBodyAsBuffer( JSON.stringify(event), false, ); expect(result.body).toBeInstanceOf(Buffer); expect(result.body).toStrictEqual(bodyBuffer); expect(result.headers).toHaveProperty( 'content-length', String(contentLength), ); }); it('should return the correct mapping for the request with custom path and method', () => { const event = createSNSEvent(); const method = 'PUT'; const path = '/custom/sns'; const customAdapter = new SNSAdapter({ snsForwardMethod: method, snsForwardPath: path, }); const result = customAdapter.getRequest(event); expect(result.method).toBe(method); expect(result.path).toBe(path); expect(result.headers).toHaveProperty('host', 'sns.amazonaws.com'); expect(result.headers).toHaveProperty('content-type', 'application/json'); const [bodyBuffer, contentLength] = getEventBodyAsBuffer( JSON.stringify(event), false, ); expect(result.body).toBeInstanceOf(Buffer); expect(result.body).toStrictEqual(bodyBuffer); expect(result.headers).toHaveProperty( 'content-length', String(contentLength), ); }); }); }); ================================================ FILE: test/adapters/aws/sqs.adapter.spec.ts ================================================ import { beforeEach, describe, expect, it } from 'vitest'; import { getEventBodyAsBuffer } from '../../../src'; import { SQSAdapter } from '../../../src/adapters/aws'; import { createCanHandleTestsForAdapter } from '../utils/can-handle'; import { createSQSEvent } from './utils/sqs'; describe(SQSAdapter.name, () => { let adapter!: SQSAdapter; beforeEach(() => { adapter = new SQSAdapter(); }); describe('getAdapterName', () => { it('should be the same name of the class', () => { expect(adapter.getAdapterName()).toBe(SQSAdapter.name); }); }); createCanHandleTestsForAdapter(() => new SQSAdapter(), undefined); describe('getRequest', () => { it('should return the correct mapping for the request', () => { const event = createSQSEvent(); const result = adapter.getRequest(event); expect(result.method).toBe('POST'); expect(result.path).toBe('/sqs'); expect(result.headers).toHaveProperty('host', 'sqs.amazonaws.com'); expect(result.headers).toHaveProperty('content-type', 'application/json'); const [bodyBuffer, contentLength] = getEventBodyAsBuffer( JSON.stringify(event), false, ); expect(result.body).toBeInstanceOf(Buffer); expect(result.body).toStrictEqual(bodyBuffer); expect(result.headers).toHaveProperty( 'content-length', String(contentLength), ); }); it('should return the correct mapping for the request with custom path and method', () => { const event = createSQSEvent(); const method = 'PUT'; const path = '/custom/sqs'; const customAdapter = new SQSAdapter({ sqsForwardMethod: method, sqsForwardPath: path, }); const result = customAdapter.getRequest(event); expect(result.method).toBe(method); expect(result.path).toBe(path); expect(result.headers).toHaveProperty('host', 'sqs.amazonaws.com'); expect(result.headers).toHaveProperty('content-type', 'application/json'); const [bodyBuffer, contentLength] = getEventBodyAsBuffer( JSON.stringify(event), false, ); expect(result.body).toBeInstanceOf(Buffer); expect(result.body).toStrictEqual(bodyBuffer); expect(result.headers).toHaveProperty( 'content-length', String(contentLength), ); }); }); }); ================================================ FILE: test/adapters/aws/utils/alb-event.ts ================================================ import type { ALBEvent } from 'aws-lambda'; /** * Sample event from {@link https://docs.aws.amazon.com/lambda/latest/dg/services-alb.html} */ export function createAlbEvent( httpMethod: string, path: string, body?: Record, headers?: Record, ): ALBEvent { return { requestContext: { elb: { targetGroupArn: 'arn:aws:elasticloadbalancing:us-east-1:347971939225:targetgroup/aws-s-Targe-RJF5FKWHX6Y8/29425aed99131fd0', }, }, httpMethod, path, queryStringParameters: { cache: 'true', }, headers: { accept: '*/*', 'accept-encoding': 'gzip, deflate', 'accept-language': 'en-US,en;q=0.9', 'cache-control': 'no-cache', connection: 'keep-alive', 'content-length': '', 'content-type': '', host: 'aws-ser-alb-p9y7dvwm0r42-2135869912.us-east-1.elb.amazonaws.com', origin: 'http://aws-ser-alb-p9y7dvwm0r42-2135869912.us-east-1.elb.amazonaws.com', pragma: 'no-cache', referer: 'http://aws-ser-alb-p9y7dvwm0r42-2135869912.us-east-1.elb.amazonaws.com/', 'user-agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/74.0.3729.131 Safari/537.36', 'x-amzn-trace-id': 'Root=1-5cdf3407-76d73870a87c746001f27090', 'x-forwarded-for': '72.21.198.66', 'x-forwarded-port': '80', 'x-forwarded-proto': 'http', ...headers, }, body: body ? JSON.stringify(body) : null, isBase64Encoded: false, }; } export function createAlbEventWithMultiValueHeaders( httpMethod: string, path: string, body?: Record, headers?: Record, ): ALBEvent { return { requestContext: { elb: { targetGroupArn: 'arn:aws:elasticloadbalancing:us-east-1:347971939225:targetgroup/aws-s-Targe-RJF5FKWHX6Y8/29425aed99131fd0', }, }, httpMethod, path, multiValueQueryStringParameters: { cache: ['true'], }, multiValueHeaders: { accept: ['*/*'], 'accept-encoding': ['gzip, deflate'], 'accept-language': ['en-US,en;q=0.9'], 'cache-control': ['no-cache'], connection: ['keep-alive'], 'content-length': [], 'content-type': [], host: ['aws-ser-alb-p9y7dvwm0r42-2135869912.us-east-1.elb.amazonaws.com'], origin: [ 'http://aws-ser-alb-p9y7dvwm0r42-2135869912.us-east-1.elb.amazonaws.com', ], pragma: ['no-cache'], referer: [ 'http://aws-ser-alb-p9y7dvwm0r42-2135869912.us-east-1.elb.amazonaws.com/', ], 'user-agent': [ 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/74.0.3729.131 Safari/537.36', ], 'x-amzn-trace-id': ['Root=1-5cdf3407-76d73870a87c746001f27090'], 'x-forwarded-for': ['72.21.198.66'], 'x-forwarded-port': ['80'], 'x-forwarded-proto': ['http'], ...headers, }, body: body ? JSON.stringify(body) : null, isBase64Encoded: false, }; } ================================================ FILE: test/adapters/aws/utils/api-gateway-v1.ts ================================================ import type { APIGatewayProxyEvent, APIGatewayProxyEventQueryStringParameters, } from 'aws-lambda/trigger/api-gateway-proxy'; import { getMultiValueHeadersMap } from '../../../../src'; export function createApiGatewayV1( httpMethod: string, path: string, body?: Record, headers?: Record, queryParams?: APIGatewayProxyEventQueryStringParameters, ): APIGatewayProxyEvent { return { resource: '/{proxy+}', path, httpMethod, headers: { Accept: '*/*', 'Accept-Encoding': 'gzip', 'Accept-Language': 'en-US,en;q=0.9', 'cache-control': 'no-cache', 'CloudFront-Forwarded-Proto': 'https', 'CloudFront-Is-Desktop-Viewer': 'true', 'CloudFront-Is-Mobile-Viewer': 'false', 'CloudFront-Is-SmartTV-Viewer': 'false', 'CloudFront-Is-Tablet-Viewer': 'false', 'CloudFront-Viewer-Country': 'US', 'content-type': '', Host: 'xxxxxx.execute-api.us-east-1.amazonaws.com', origin: 'https://xxxxxx.execute-api.us-east-1.amazonaws.com', pragma: 'no-cache', Referer: 'https://xxxxxx.execute-api.us-east-1.amazonaws.com/prod/', 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/74.0.3729.131 Safari/537.36', Via: '2.0 00f0a41f749793b9dd653153037c957e.cloudfront.net (CloudFront)', 'X-Amz-Cf-Id': '2D5N65SYHJdnJfEmAV_hC0Mw3QvkbUXDumJKAL786IGHRdq_MggPtA==', 'X-Amzn-Trace-Id': 'Root=1-5cdf30d0-31a428004abe13807f9445b0', 'X-Forwarded-For': '11.111.111.111, 11.111.111.111', 'X-Forwarded-Port': '443', 'X-Forwarded-Proto': 'https', ...headers, }, multiValueHeaders: { Accept: ['*/*'], 'Accept-Encoding': ['gzip', 'deflate', 'br'], 'Accept-Language': ['en-US,en;q=0.9'], 'cache-control': ['no-cache'], 'CloudFront-Forwarded-Proto': ['https'], 'CloudFront-Is-Desktop-Viewer': ['true'], 'CloudFront-Is-Mobile-Viewer': ['false'], 'CloudFront-Is-SmartTV-Viewer': ['false'], 'CloudFront-Is-Tablet-Viewer': ['false'], 'CloudFront-Viewer-Country': ['US'], 'content-type': [], Host: ['xxxxxx.execute-api.us-east-1.amazonaws.com'], origin: ['https://xxxxxx.execute-api.us-east-1.amazonaws.com'], pragma: ['no-cache'], Referer: ['https://xxxxxx.execute-api.us-east-1.amazonaws.com/prod/'], 'User-Agent': [ 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/74.0.3729.131 Safari/537.36', ], Via: ['2.0 00f0a41f749793b9dd653153037c957e.cloudfront.net (CloudFront)'], 'X-Amz-Cf-Id': [ '2D5N65SYHJdnJfEmAV_hC0Mw3QvkbUXDumJKAL786IGHRdq_MggPtA==', ], 'X-Amzn-Trace-Id': ['Root=1-5cdf30d0-31a428004abe13807f9445b0'], 'X-Forwarded-For': ['11.111.111.111, 11.111.111.111'], 'X-Forwarded-Port': ['443'], 'X-Forwarded-Proto': ['https'], ...(headers && getMultiValueHeadersMap(headers)), }, queryStringParameters: queryParams || null, multiValueQueryStringParameters: (queryParams && getMultiValueHeadersMap(queryParams)) || null, pathParameters: { path: path.replace(/^\//, ''), }, stageVariables: {}, requestContext: { authorizer: { claims: null, scopes: null, }, resourceId: 'xxxxx', resourcePath: '/{proxy+}', httpMethod: 'POST', extendedRequestId: 'Z2SQlEORIAMFjpA=', requestTime: '17/May/2019:22:08:16 +0000', path, accountId: 'xxxxxxxx', protocol: 'HTTP/1.1', stage: 'prod', domainPrefix: 'xxxxxx', requestTimeEpoch: 1558130896565, requestId: '4589cf16-78f0-11e9-9c65-816a9b037cec', identity: { apiKey: null, apiKeyId: null, clientCert: null, cognitoIdentityPoolId: null, accountId: null, cognitoIdentityId: null, caller: null, sourceIp: '11.111.111.111', principalOrgId: null, accessKey: null, cognitoAuthenticationType: null, cognitoAuthenticationProvider: null, userArn: null, userAgent: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/74.0.3729.131 Safari/537.36', user: null, }, domainName: 'xxxxxx.execute-api.us-east-1.amazonaws.com', apiId: 'xxxxxx', }, body: (body && JSON.stringify(body)) || null, isBase64Encoded: false, }; } ================================================ FILE: test/adapters/aws/utils/api-gateway-v2.ts ================================================ import type { APIGatewayProxyEventV2 } from 'aws-lambda'; import type { APIGatewayProxyEventQueryStringParameters } from 'aws-lambda/trigger/api-gateway-proxy'; import { getQueryParamsStringFromRecord } from '../../../../src'; export function createApiGatewayV2( method: string, path: string, body?: Record, headers?: Record, queryParams?: APIGatewayProxyEventQueryStringParameters, cookies?: APIGatewayProxyEventV2['cookies'], ): APIGatewayProxyEventV2 { return { version: '2.0', routeKey: '$default', rawPath: path, rawQueryString: getQueryParamsStringFromRecord(queryParams || {}), queryStringParameters: queryParams, headers: { accept: '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', 'accept-encoding': 'gzip, deflate, br', 'accept-language': 'en-US,en;q=0.9', 'cache-control': 'max-age=0', 'content-length': '0', host: '6bwvllq3t2.execute-api.us-east-1.amazonaws.com', 'sec-fetch-dest': 'document', 'sec-fetch-mode': 'navigate', 'sec-fetch-site': 'none', 'sec-fetch-user': '?1', 'upgrade-insecure-requests': '1', 'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36', 'x-amzn-trace-id': 'Root=1-5ff59707-4914805430277a6209549a59', 'x-forwarded-for': '203.123.103.37', 'x-forwarded-port': '443', 'x-forwarded-proto': 'https', ...headers, }, cookies, requestContext: { accountId: '347971939225', apiId: '6bwvllq3t2', domainName: '6bwvllq3t2.execute-api.us-east-1.amazonaws.com', domainPrefix: '6bwvllq3t2', http: { method, path, protocol: 'HTTP/1.1', sourceIp: '203.123.103.37', userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36', }, requestId: 'YuSJQjZfoAMESbg=', routeKey: '$default', stage: '$default', time: '06/Jan/2021:10:55:03 +0000', timeEpoch: 1609930503973, }, body: (body && JSON.stringify(body)) || undefined, isBase64Encoded: false, }; } ================================================ FILE: test/adapters/aws/utils/dynamodb.ts ================================================ import type { DynamoDBStreamEvent } from 'aws-lambda'; /** * Sample event from {@link https://docs.aws.amazon.com/lambda/latest/dg/with-ddb.html} */ export function createDynamoDBEvent(): DynamoDBStreamEvent { return { Records: [ { eventID: '1', eventVersion: '1.0', dynamodb: { Keys: { Id: { N: '101', }, }, NewImage: { Message: { S: 'New item!', }, Id: { N: '101', }, }, StreamViewType: 'NEW_AND_OLD_IMAGES', SequenceNumber: '111', SizeBytes: 26, }, awsRegion: 'us-west-2', eventName: 'INSERT', eventSourceARN: 'arn:aws:dynamodb:us-east-1:0000000000:mytable', eventSource: 'aws:dynamodb', }, { eventID: '2', eventVersion: '1.0', dynamodb: { OldImage: { Message: { S: 'New item!', }, Id: { N: '101', }, }, SequenceNumber: '222', Keys: { Id: { N: '101', }, }, SizeBytes: 59, NewImage: { Message: { S: 'This item has changed', }, Id: { N: '101', }, }, StreamViewType: 'NEW_AND_OLD_IMAGES', }, awsRegion: 'us-west-2', eventName: 'MODIFY', eventSourceARN: 'arn:aws:dynamodb:us-east-1:0000000000:mytable', eventSource: 'aws:dynamodb', }, ], }; } ================================================ FILE: test/adapters/aws/utils/event-bridge.ts ================================================ import type { EventBridgeEvent } from 'aws-lambda'; /** * Sample event from {@link https://docs.aws.amazon.com/lambda/latest/dg/services-cloudwatchevents.html} */ export function createEventBridgeEvent(): EventBridgeEvent { return { version: '0', id: 'fe8d3c65-xmpl-c5c3-2c87-81584709a377', 'detail-type': 'RDS DB Instance Event', source: 'aws.rds', account: '123456789012', time: '2020-04-28T07:20:20Z', region: 'us-east-2', resources: ['arn:aws:rds:us-east-2:123456789012:db:rdz6xmpliljlb1'], detail: { EventCategories: ['backup'], SourceType: 'DB_INSTANCE', SourceArn: 'arn:aws:rds:us-east-2:123456789012:db:rdz6xmpliljlb1', Date: '2020-04-28T07:20:20.112Z', Message: 'Finished DB Instance backup', SourceIdentifier: 'rdz6xmpliljlb1', }, }; } /** * Sample event from {@link https://docs.aws.amazon.com/lambda/latest/dg/services-cloudwatchevents.html} */ export function createEventBridgeEventSimple(): EventBridgeEvent { return { version: '0', account: '123456789012', region: 'us-east-2', detail: {}, 'detail-type': 'Scheduled Event', source: 'aws.events', time: '2019-03-01T01:23:45Z', id: 'cdc73f9d-aea9-11e3-9d5a-835b769c0d9c', resources: ['arn:aws:events:us-east-2:123456789012:rule/my-schedule'], }; } ================================================ FILE: test/adapters/aws/utils/events.ts ================================================ import { AlbAdapter, ApiGatewayV1Adapter, ApiGatewayV2Adapter, DynamoDBAdapter, EventBridgeAdapter, LambdaEdgeAdapter, S3Adapter, SNSAdapter, SQSAdapter, } from '../../../../src/adapters/aws'; import { createAlbEvent, createAlbEventWithMultiValueHeaders, } from './alb-event'; import { createApiGatewayV1 } from './api-gateway-v1'; import { createApiGatewayV2 } from './api-gateway-v2'; import { createDynamoDBEvent } from './dynamodb'; import { createEventBridgeEvent, createEventBridgeEventSimple, } from './event-bridge'; import { createLambdaEdgeOriginEvent, createLambdaEdgeViewerEvent, } from './lambda-edge'; import { createS3Event } from './s3'; import { createSNSEvent } from './sns'; import { createSQSEvent } from './sqs'; export const allAWSEvents: Array<[string, any]> = [ ['fake-to-test-undefined-event', undefined], ['fake-to-test-records-empty-event', { Records: [] }], [ AlbAdapter.name, createAlbEvent('POST', '/users', { name: 'potato with banana' }), ], [ AlbAdapter.name, createAlbEventWithMultiValueHeaders('PUT', '/users', { name: 'batata' }), ], [AlbAdapter.name, createAlbEvent('GET', '/users')], [AlbAdapter.name, createAlbEventWithMultiValueHeaders('GET', '/users')], [ ApiGatewayV1Adapter.name, createApiGatewayV1('POST', '/users', { name: 'Fake' }), ], [ ApiGatewayV1Adapter.name, createApiGatewayV1('PUT', '/users', { name: 'Fake v2' }), ], [ ApiGatewayV1Adapter.name, createApiGatewayV1('GET', '/users', undefined, {}, { page: '2' }), ], [ApiGatewayV2Adapter.name, createApiGatewayV2('GET', '/collaborators')], [ ApiGatewayV2Adapter.name, createApiGatewayV2('POST', '/collaborators', { name: 'Fake' }), ], [ ApiGatewayV2Adapter.name, createApiGatewayV2('PUT', '/collaborators', { name: 'Fake v2' }), ], [ ApiGatewayV2Adapter.name, createApiGatewayV2('GET', '/collaborators', undefined, {}, { page: '2' }), ], [ApiGatewayV2Adapter.name, createApiGatewayV2('collaborators', '/users')], [DynamoDBAdapter.name, createDynamoDBEvent()], [EventBridgeAdapter.name, createEventBridgeEvent()], [EventBridgeAdapter.name, createEventBridgeEventSimple()], [SQSAdapter.name, createSQSEvent()], [SNSAdapter.name, createSNSEvent()], [LambdaEdgeAdapter.name, createLambdaEdgeViewerEvent('GET', '/image.png')], [ LambdaEdgeAdapter.name, createLambdaEdgeViewerEvent('POST', '/image.png', { base64: Buffer.from('batata', 'utf-8').toString('base64'), }), ], [LambdaEdgeAdapter.name, createLambdaEdgeOriginEvent('GET', '/image.png')], [ LambdaEdgeAdapter.name, createLambdaEdgeOriginEvent('POST', '/image.png', { base64: Buffer.from('batata', 'utf-8').toString('base64'), }), ], [S3Adapter.name, createS3Event()], ]; ================================================ FILE: test/adapters/aws/utils/lambda-edge.ts ================================================ import type { CloudFrontHeaders } from 'aws-lambda/common/cloudfront'; import type { CloudFrontRequestEvent } from 'aws-lambda/trigger/cloudfront-request'; export function createLambdaEdgeViewerEvent( httpMethod: string, path: string, body?: Record, headers?: CloudFrontHeaders, queryParams?: string, ): CloudFrontRequestEvent { return { Records: [ { cf: { config: { distributionDomainName: 'd3qj9vk9486y6c.cloudfront.net', distributionId: 'E2I5C7O4FEQEKZ', eventType: 'viewer-request', requestId: 'BKXC0kFgBfWSEgribSo9EwziZB1FztiXQ96VRvTfFNHYCBv7Ko-RBQ==', }, request: { clientIp: '203.123.103.37', headers: headers ?? { host: [ { key: 'Host', value: 'd3qj9vk9486y6c.cloudfront.net', }, ], 'user-agent': [ { key: 'User-Agent', value: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.141 Safari/537.36', }, ], 'cache-control': [ { key: 'Cache-Control', value: 'max-age=0', }, ], accept: [ { key: 'accept', value: 'application/json,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', }, ], 'if-none-match': [ { key: 'if-none-match', value: 'W/"2e-Lu6qxFOQSPFulDAGUFiiK6QgREo"', }, ], 'accept-language': [ { key: 'accept-language', value: 'en-US,en;q=0.9', }, ], 'upgrade-insecure-requests': [ { key: 'upgrade-insecure-requests', value: '1', }, ], origin: [ { key: 'Origin', value: 'https://d3qj9vk9486y6c.cloudfront.net', }, ], 'sec-fetch-site': [ { key: 'Sec-Fetch-Site', value: 'same-origin', }, ], 'sec-fetch-mode': [ { key: 'Sec-Fetch-Mode', value: 'cors', }, ], 'sec-fetch-dest': [ { key: 'Sec-Fetch-Dest', value: 'empty', }, ], referer: [ { key: 'Referer', value: 'https://d3qj9vk9486y6c.cloudfront.net/users', }, ], 'accept-encoding': [ { key: 'Accept-Encoding', value: 'gzip, deflate, br', }, ], }, body: body ? { action: 'read-only', encoding: 'base64', inputTruncated: false, data: Buffer.from(JSON.stringify(body), 'utf-8').toString( 'base64', ), } : undefined, method: httpMethod, querystring: queryParams || '', uri: path, }, }, }, ], }; } export function createLambdaEdgeOriginEvent( httpMethod: string, path: string, body?: Record, headers?: CloudFrontHeaders, queryParams?: string, ): CloudFrontRequestEvent { return { Records: [ { cf: { config: { distributionDomainName: 'd111111abcdef8.cloudfront.net', distributionId: 'EDFDVBD6EXAMPLE', eventType: 'origin-request', requestId: '4TyzHTaYWb1GX1qTfsHhEqV6HUDd_BzoBZnwfnvQc_1oF26ClkoUSEQ==', }, request: { body: body ? { action: 'read-only', encoding: 'base64', inputTruncated: false, data: Buffer.from(JSON.stringify(body), 'utf-8').toString( 'base64', ), } : undefined, clientIp: '203.0.113.178', headers: headers ?? { 'x-forwarded-for': [ { key: 'X-Forwarded-For', value: '203.0.113.178', }, ], 'user-agent': [ { key: 'User-Agent', value: 'Amazon CloudFront', }, ], via: [ { key: 'Via', value: '2.0 2afae0d44e2540f472c0635ab62c232b.cloudfront.net (CloudFront)', }, ], host: [ { key: 'Host', value: 'example.org', }, ], 'cache-control': [ { key: 'Cache-Control', value: 'no-cache, cf-no-cache', }, ], }, method: httpMethod, origin: { custom: { customHeaders: {}, domainName: 'example.org', keepaliveTimeout: 5, path: '', port: 443, protocol: 'https', readTimeout: 30, sslProtocols: ['TLSv1', 'TLSv1.1', 'TLSv1.2'], }, }, querystring: queryParams || '', uri: path, }, }, }, ], }; } ================================================ FILE: test/adapters/aws/utils/s3.ts ================================================ import type { S3Event } from 'aws-lambda'; /** * Sample event from {@link https://docs.aws.amazon.com/pt_br/lambda/latest/dg/with-s3.html} */ export function createS3Event(): S3Event { return { Records: [ { eventVersion: '2.1', eventSource: 'aws:s3', awsRegion: 'us-east-2', eventTime: '2019-09-03T19:37:27.192Z', eventName: 'ObjectCreated:Put', userIdentity: { principalId: 'AWS:AIDAINPONIXQXHT3IKHL2', }, requestParameters: { sourceIPAddress: '205.255.255.255', }, responseElements: { 'x-amz-request-id': 'D82B88E5F771F645', 'x-amz-id-2': 'vlR7PnpV2Ce81l0PRw6jlUpck7Jo5ZsQjryTjKlc5aLWGVHPZLj5NeC6qMa0emYBDXOo6QBU0Wo=', }, s3: { s3SchemaVersion: '1.0', configurationId: '828aa6fc-f7b5-4305-8584-487c791949c1', bucket: { name: 'DOC-EXAMPLE-BUCKET', ownerIdentity: { principalId: 'A3I5XTEXAMAI3E', }, arn: 'arn:aws:s3:::lambda-artifacts-deafc19498e3f2df', }, object: { key: 'b21b84d653bb07b05b1e6b33684dc11b', size: 1305107, eTag: 'b21b84d653bb07b05b1e6b33684dc11b', sequencer: '0C0F6F405D6ED209E1', }, }, }, ], }; } ================================================ FILE: test/adapters/aws/utils/sns.ts ================================================ import type { SNSEvent } from 'aws-lambda'; /** * Sample event from {@link https://docs.aws.amazon.com/lambda/latest/dg/with-sns.html} */ export function createSNSEvent(): SNSEvent { return { Records: [ { EventVersion: '1.0', EventSubscriptionArn: 'arn:aws:sns:us-east-2:123456789012:sns-lambda:21be56ed-a058-49f5-8c98-aedd2564c486', EventSource: 'aws:sns', Sns: { SignatureVersion: '1', Timestamp: '2019-01-02T12:45:07.000Z', Signature: 'tcc6faL2yUC6dgZdmrwh1Y4cGa/ebXEkAi6RibDsvpi+tE/1+82j...65r==', SigningCertUrl: 'https://sns.us-east-2.amazonaws.com/SimpleNotificationService-ac565b8b1a6c5d002d285f9598aa1d9b.pem', MessageId: '95df01b4-ee98-5cb9-9903-4c221d41eb5e', Message: 'Hello from SNS!', MessageAttributes: { Test: { Type: 'String', Value: 'TestString', }, TestBinary: { Type: 'Binary', Value: 'TestBinary', }, }, Type: 'Notification', UnsubscribeUrl: 'https://sns.us-east-2.amazonaws.com/?Action=Unsubscribe&SubscriptionArn=arn:aws:sns:us-east-2:123456789012:test-lambda:21be56ed-a058-49f5-8c98-aedd2564c486', TopicArn: 'arn:aws:sns:us-east-2:123456789012:sns-lambda', Subject: 'TestInvoke', }, }, ], }; } ================================================ FILE: test/adapters/aws/utils/sqs.ts ================================================ import type { SQSEvent } from 'aws-lambda'; /** * Sample event from {@link https://docs.aws.amazon.com/lambda/latest/dg/with-sqs.html} */ export function createSQSEvent(): SQSEvent { return { Records: [ { messageId: '059f36b4-87a3-44ab-83d2-661975830a7d', receiptHandle: 'AQEBwJnKyrHigUMZj6rYigCgxlaS3SLy0a...', body: 'Test message.', attributes: { ApproximateReceiveCount: '1', SentTimestamp: '1545082649183', SenderId: 'AIDAIENQZJOLO23YVJ4VO', ApproximateFirstReceiveTimestamp: '1545082649185', }, messageAttributes: {}, md5OfBody: 'e4e68fb7bd0e697a0ae8f1bb342846b3', eventSource: 'aws:sqs', eventSourceARN: 'arn:aws:sqs:us-east-2:123456789012:my-queue', awsRegion: 'us-east-2', }, ], }; } ================================================ FILE: test/adapters/azure/http-trigger.adapter.spec.ts ================================================ import type { Cookie, HttpRequest, HttpResponseSimple } from '@azure/functions'; import { beforeEach, describe, expect, it, vitest } from 'vitest'; import { type DelegatedResolver, type GetResponseAdapterProps, type ILogger, getEventBodyAsBuffer, getFlattenedHeadersMap, getPathWithQueryStringParams, } from '../../../src'; import { HttpTriggerV4Adapter } from '../../../src/adapters/azure'; import { createCanHandleTestsForAdapter } from '../utils/can-handle'; import { createHttpTriggerContext, createHttpTriggerEvent, } from './utils/http-trigger'; describe(HttpTriggerV4Adapter.name, () => { let adapter!: HttpTriggerV4Adapter; beforeEach(() => { adapter = new HttpTriggerV4Adapter(); }); describe('getAdapterName', () => { it('should be the same name of the class', () => { expect(adapter.getAdapterName()).toBe(HttpTriggerV4Adapter.name); }); }); createCanHandleTestsForAdapter( () => new HttpTriggerV4Adapter(), createHttpTriggerContext('GET', '/'), ); describe('getRequest', () => { it('should return the correct mapping for the request', () => { const method = 'PUT'; const path = '/events'; const body = { name: 'H4ad Event' }; const event = createHttpTriggerEvent(method, path, body); expect(event.headers).toHaveProperty('x-forwarded-for'); expect(event.headers['x-forwarded-for']).not.toBeInstanceOf(Array); const result = adapter.getRequest(event); const remoteAddress = event.headers['x-forwarded-for']; expect(result).toHaveProperty('method', method); expect(result).toHaveProperty('headers'); expect(result.headers).toHaveProperty('x-forwarded-for'); expect(result.headers['x-forwarded-for']).not.toBeInstanceOf(Array); expect(result).toHaveProperty('body'); expect(result.body).toBeInstanceOf(Buffer); const [bodyBuffer, contentLength] = getEventBodyAsBuffer( JSON.stringify(body), false, ); expect(result.body).toStrictEqual(bodyBuffer); expect(result.headers).toHaveProperty('content-length'); expect(result.headers['content-length']).toBe(String(contentLength)); expect(result).toHaveProperty('remoteAddress', remoteAddress); const resultPath = getPathWithQueryStringParams(path, event.query); expect(result.path.replace('/api/test-serverless-adapter', '')).toEqual( resultPath, ); }); it('should return the correct mapping for the request when it has no body', () => { const method = 'POST'; const path = '/events'; const body = undefined; const event = createHttpTriggerEvent(method, path, body); const result = adapter.getRequest(event); const remoteAddress = event.headers['x-forwarded-for']; expect(result).toHaveProperty('method', method); expect(result).toHaveProperty('headers'); expect(result.headers).toHaveProperty('x-forwarded-for'); expect(result.headers['x-forwarded-for']).not.toBeInstanceOf(Array); expect(result).toHaveProperty('body'); expect(result.body).not.toBeInstanceOf(Buffer); expect(result.body).toBeUndefined(); expect(result.headers).not.toHaveProperty('content-length'); expect(result).toHaveProperty('remoteAddress', remoteAddress); const resultPath = getPathWithQueryStringParams(path, event.query); expect(result.path.replace('/api/test-serverless-adapter', '')).toEqual( resultPath, ); }); it('should return the correct mapping for the request when send stripBasePath', () => { const stripBasePath = '/api/test-serverless-adapter'; const method = 'PUT'; const path = '/events'; const body = { name: 'H4ad Event' }; const strippedAdapter = new HttpTriggerV4Adapter({ stripBasePath }); const event = createHttpTriggerEvent(method, path, body); expect(event.headers).toHaveProperty('x-forwarded-for'); expect(event.headers['x-forwarded-for']).not.toBeInstanceOf(Array); const result = strippedAdapter.getRequest(event); const remoteAddress = event.headers['x-forwarded-for']; expect(result).toHaveProperty('method', method); expect(result).toHaveProperty('headers'); expect(result.headers).toHaveProperty('x-forwarded-for'); expect(result.headers['x-forwarded-for']).not.toBeInstanceOf(Array); expect(result).toHaveProperty('body'); expect(result.body).toBeInstanceOf(Buffer); const [bodyBuffer, contentLength] = getEventBodyAsBuffer( JSON.stringify(body), false, ); expect(result.body).toStrictEqual(bodyBuffer); expect(result.headers).toHaveProperty('content-length'); expect(result.headers['content-length']).toBe(String(contentLength)); expect(result).toHaveProperty('remoteAddress', remoteAddress); const resultPath = getPathWithQueryStringParams(path, event.query); expect(result).toHaveProperty('path', resultPath); }); }); describe('getResponse', () => { it('should return the correct mapping for the response', () => { const method = 'PUT'; const path = '/events'; const requestBody = { name: 'H4ad Event' }; const resultBody = '{"success":true}'; const resultStatusCode = 200; const resultIsBase64Encoded = false; const event = createHttpTriggerEvent(method, path, requestBody); const responseHeaders = getFlattenedHeadersMap(event.headers); const result = adapter.getResponse({ event, headers: responseHeaders, body: resultBody, log: {} as ILogger, isBase64Encoded: resultIsBase64Encoded, statusCode: resultStatusCode, }); expect(result).toHaveProperty('statusCode', 200); expect(result).toHaveProperty('body', resultBody); expect(result).toHaveProperty('headers', responseHeaders); expect(result).toHaveProperty('enableContentNegotiation', false); expect(result).toHaveProperty('cookies', []); }); it('should return the correct mapping for the response with multiple set-cookie', () => { const method = 'PUT'; const path = '/events'; const requestBody = { name: 'H4ad Event' }; const resultBody = '{"success":true}'; const resultStatusCode = 200; const resultIsBase64Encoded = false; const event = createHttpTriggerEvent(method, path, requestBody, { 'set-cookie': [ 'Id=a3fWa; Expires=Thu, 31 Oct 2021 07:28:00 GMT;', 'id=a3fWa; Expires=Thu, 21 Oct 2021 07:28:00 GMT; Secure; HttpOnly', 'MyKey=myvalue; SameSite=Strict', 'lu=Rg3vHJZnehYLjVg7qi3bZjzg; Expires=Tue, 15 Jan 2013 21:47:38 GMT; Path=/; Domain=.example.com; HttpOnly', 'made_write_conn=1295214458; Path=/; Domain=.example.com; Max-Age=1209600', 'reg_fb_gate=deleted; Expires=Thu, 01 Jan 1970 00:00:01 GMT; Path=/; Domain=.example.com; HttpOnly', ], }); const result = adapter.getResponse({ event, headers: event.headers, body: resultBody, log: {} as ILogger, isBase64Encoded: resultIsBase64Encoded, statusCode: resultStatusCode, }); delete event.headers['set-cookie']; expect(result).toHaveProperty('statusCode', 200); expect(result).toHaveProperty('body', resultBody); expect(result).toHaveProperty('headers', event.headers); expect(result).toHaveProperty('enableContentNegotiation', false); expect(result.cookies).toStrictEqual([ { name: 'Id', value: 'a3fWa', expires: new Date('Thu, 31 Oct 2021 07:28:00 GMT'), }, { name: 'id', value: 'a3fWa', expires: new Date('Thu, 21 Oct 2021 07:28:00 GMT'), secure: true, httpOnly: true, }, { name: 'MyKey', value: 'myvalue', sameSite: 'Strict', }, { name: 'lu', value: 'Rg3vHJZnehYLjVg7qi3bZjzg', expires: new Date('Tue, 15 Jan 2013 21:47:38 GMT'), path: '/', domain: '.example.com', httpOnly: true, }, { name: 'made_write_conn', value: '1295214458', path: '/', domain: '.example.com', maxAge: 1209600, }, { name: 'reg_fb_gate', value: 'deleted', expires: new Date('Thu, 01 Jan 1970 00:00:01 GMT'), path: '/', domain: '.example.com', httpOnly: true, }, ] as Cookie[]); }); it('should return the correct mapping for the response with set-cookie', () => { const method = 'PUT'; const path = '/events'; const requestBody = { name: 'H4ad Event' }; const resultBody = '{"success":true}'; const resultStatusCode = 200; const resultIsBase64Encoded = false; const event = createHttpTriggerEvent(method, path, requestBody, { 'set-cookie': 'id=a3fWa; Expires=Thu, 31 Oct 2021 07:28:00 GMT;', }); const result = adapter.getResponse({ event, headers: event.headers, body: resultBody, log: {} as ILogger, isBase64Encoded: resultIsBase64Encoded, statusCode: resultStatusCode, }); delete event.headers['set-cookie']; expect(result).toHaveProperty('statusCode', 200); expect(result).toHaveProperty('body', resultBody); expect(result).toHaveProperty('headers', event.headers); expect(result).toHaveProperty('enableContentNegotiation', false); expect(result.cookies).toStrictEqual([ { name: 'id', value: 'a3fWa', expires: new Date('Thu, 31 Oct 2021 07:28:00 GMT'), }, ] as Cookie[]); }); }); describe('onErrorWhileForwarding', () => { it('should resolver call succeed', () => { const method = 'GET'; const path = '/events'; const requestBody = undefined; const event = createHttpTriggerEvent(method, path, requestBody); const log = {} as ILogger; const resolver: DelegatedResolver = { fail: vitest.fn(), succeed: vitest.fn(), }; const respondWithErrors = true; const error = new Error('Test error'); const oldGetResponse = adapter.getResponse.bind(adapter); let getResponseResult: HttpResponseSimple | undefined; adapter.getResponse = vitest.fn( (params: GetResponseAdapterProps) => { expect(params.event).toBe(event); expect(params.statusCode).toBe(500); expect(params.body).toBe(error.stack); expect(params.isBase64Encoded).toBe(false); expect(params.log).toBe(log); expect(params.headers).toStrictEqual({}); getResponseResult = oldGetResponse(params); return getResponseResult; }, ); adapter.onErrorWhileForwarding({ event, log, delegatedResolver: resolver, respondWithErrors, error, }); // eslint-disable-next-line @typescript-eslint/unbound-method expect(adapter.getResponse).toHaveBeenCalledTimes(1); expect(resolver.fail).toHaveBeenCalledTimes(0); expect(resolver.succeed).toHaveBeenCalledTimes(1); expect(resolver.succeed).toHaveBeenCalledWith(getResponseResult); }); it('should resolver call succeed but without sending errors', () => { const method = 'GET'; const path = '/events'; const requestBody = undefined; const event = createHttpTriggerEvent(method, path, requestBody); const log = {} as ILogger; const resolver: DelegatedResolver = { fail: vitest.fn(), succeed: vitest.fn(), }; const respondWithErrors = false; const error = new Error('Test error without sending this error'); const oldGetResponse = adapter.getResponse.bind(adapter); let getResponseResult: HttpResponseSimple | undefined; adapter.getResponse = vitest.fn( (params: GetResponseAdapterProps) => { expect(params.event).toBe(event); expect(params.statusCode).toBe(500); expect(params.body).not.toBe(error.stack); expect(params.body).toStrictEqual(''); expect(params.isBase64Encoded).toBe(false); expect(params.log).toBe(log); expect(params.headers).toStrictEqual({}); getResponseResult = oldGetResponse(params); return getResponseResult; }, ); adapter.onErrorWhileForwarding({ event, log, delegatedResolver: resolver, respondWithErrors, error, }); // eslint-disable-next-line @typescript-eslint/unbound-method expect(adapter.getResponse).toHaveBeenCalledTimes(1); expect(resolver.fail).toHaveBeenCalledTimes(0); expect(resolver.succeed).toHaveBeenCalledTimes(1); expect(resolver.succeed).toHaveBeenCalledWith(getResponseResult); }); }); }); ================================================ FILE: test/adapters/azure/utils/events.ts ================================================ import { HttpTriggerV4Adapter } from '../../../../src/adapters/azure'; import { createHttpTriggerEvent } from './http-trigger'; export const allAzureEvents: Array<[string, any]> = [ [HttpTriggerV4Adapter.name, createHttpTriggerEvent('GET', '/')], [ HttpTriggerV4Adapter.name, createHttpTriggerEvent('POST', '/', { name: 'Joga10' }), ], ]; ================================================ FILE: test/adapters/azure/utils/http-trigger.ts ================================================ import { URL } from 'url'; import type { Context, Form, HttpRequest } from '@azure/functions'; import { vitest } from 'vitest'; import { type BothValueHeaders } from '../../../../src'; export function createHttpTriggerEvent( method: HttpRequest['method'], path: string, body?: Record, headers?: BothValueHeaders, ): HttpRequest { const url = new URL(`http://localhost${path}`); url.searchParams.set('code', 'sE_d8h7XJ4YYsGJ7mgVta_t-32323%3D%3D'); return { get: () => '', method, url: `https://serverless-adapter.azurewebsites.net/api/test-serverless-adapter${ path || '' }?${url.searchParams.toString()}`, headers: { accept: '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', 'accept-encoding': 'gzip, deflate, br', 'accept-language': 'pt-BR,pt;q=0.9,en-US;q=0.8,en;q=0.7,bg;q=0.6', 'cache-control': 'max-age=0', host: 'serverless-adapter.azurewebsites.net', 'max-forwards': '9', 'user-agent': 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.0.0 Safari/537.36', 'sec-ch-ua': '".Not/A)Brand";v="99", "Google Chrome";v="103", "Chromium";v="103"', 'sec-ch-ua-mobile': '?0', 'sec-ch-ua-platform': '"Linux"', dnt: '1', 'sec-fetch-site': 'none', 'sec-fetch-mode': 'navigate', 'sec-fetch-user': '?1', 'sec-fetch-dest': 'document', 'x-arr-log-id': '09a0a10e-eaba-487f-9e6b-ae6ce6f1d333', 'client-ip': '2.3.3.3:30750', 'x-site-deployment-id': 'serverless-adapter', 'was-default-hostname': 'serverless-adapter.azurewebsites.net', 'x-forwarded-proto': 'https', 'x-appservice-proto': 'https', 'x-arr-ssl': '2048|256|CN=Microsoft Azure TLS Issuing CA 01, O=Microsoft Corporation, C=US|CN=*.azurewebsites.net, O=Microsoft Corporation, L=Redmond, S=WA, C=US', 'x-forwarded-tlsversion': '1.2', 'x-forwarded-for': '3.3.3.3:49196', 'x-original-url': '/api/test-serverless-adapter?code=sE_d8h7XJ4YYsGJ7mgVta_t-32323%3D%3D', 'x-waws-unencoded-url': '/api/test-serverless-adapter?code=sE_d8h7XJ4YYsGJ7mgVta_t-32323%3D%3D', 'disguised-host': 'serverless-adapter.azurewebsites.net', ...headers, }, query: Object.fromEntries(url.searchParams.entries()), params: {}, body: body || undefined, rawBody: body ? JSON.stringify(body) : undefined, user: null, parseFormBody(): Form { throw new Error('test'); }, }; } export function createHttpTriggerContext( method: HttpRequest['method'], path: string, body?: Record, headers?: BothValueHeaders, ): Context { const req = createHttpTriggerEvent(method, path, body, headers); const log = vitest.fn(); Object.assign(log, { error: vitest.fn(), warn: vitest.fn(), info: vitest.fn(), verbose: vitest.fn(), }); return { invocationId: '6947db6b-98f6-406b-a1ce-e5bd7244ff66', traceContext: { traceparent: '00-7d1ba80dfba92a27453561f5844346c9-684d236d619d3234-00', tracestate: '', attributes: {}, }, executionContext: { invocationId: '6947db6b-98f6-406b-a1ce-e5bd7244ff66', functionName: 'test-serverless-adapter', functionDirectory: 'C:\\home\\site\\wwwroot\\test-serverless-adapter', retryContext: null, }, bindings: { req, }, log: log as unknown as Context['log'], bindingData: { invocationId: '6947db6b-98f6-406b-a1ce-32323', query: req.query, headers: req.headers, sys: { methodName: 'test-serverless-adapter', utcNow: '2022-07-10T20:48:24.113Z', randGuid: '5a5a0bfd-9774-4e5a-875d-bb8444d595b3', }, }, bindingDefinitions: [ { name: 'req', type: 'httpTrigger', direction: 'in' }, { name: 'res', type: 'http', direction: 'out' }, ], done: vitest.fn(), req: req, res: { headers: {}, cookies: [], send: vitest.fn(), header: vitest.fn(), set: vitest.fn(), get: vitest.fn(), _done: vitest.fn(), }, }; } ================================================ FILE: test/adapters/digital-ocean/http-function.adapter.spec.ts ================================================ import { beforeEach, describe, expect, it, vitest } from 'vitest'; import { type DelegatedResolver, type GetResponseAdapterProps, type ILogger, getEventBodyAsBuffer, getPathWithQueryStringParams, } from '../../../src'; import { type DigitalOceanHttpEvent, type DigitalOceanHttpResponse, } from '../../../src/@types/digital-ocean'; import { HttpFunctionAdapter } from '../../../src/adapters/digital-ocean'; import { createCanHandleTestsForAdapter } from '../utils/can-handle'; import { createHttpFunctionEvent } from './utils/http-function'; describe(HttpFunctionAdapter.name, () => { let adapter!: HttpFunctionAdapter; beforeEach(() => { adapter = new HttpFunctionAdapter(); }); describe('getAdapterName', () => { it('should be the same name of the class', () => { expect(adapter.getAdapterName()).toBe(HttpFunctionAdapter.name); }); }); createCanHandleTestsForAdapter(() => new HttpFunctionAdapter(), undefined); describe('getRequest', () => { it('should return the correct mapping for the request', () => { const method = 'PUT'; const path = '/collaborators'; const body = { name: 'H4ad Collaborator' }; const queryParams = { page: '2' }; const event = createHttpFunctionEvent( method, path, body, {}, queryParams, ); const result = adapter.getRequest(event); const remoteAddress = event.__ow_headers['x-forwarded-for']; expect(result).toHaveProperty('method', method); expect(result).toHaveProperty('headers'); expect(result).toHaveProperty('body'); expect(result.body).toBeInstanceOf(Buffer); const [bodyBuffer, contentLength] = getEventBodyAsBuffer( JSON.stringify(body), false, ); expect(result.body).toStrictEqual(bodyBuffer); expect(result.headers).toHaveProperty('content-length'); expect(result.headers['content-length']).toBe(String(contentLength)); expect(result).toHaveProperty('remoteAddress', remoteAddress); const resultPath = getPathWithQueryStringParams(path, event.__ow_query); expect(result).toHaveProperty('path', resultPath); }); it('should method be always uppercase', () => { const method = 'get'; const path = '/test'; const event = createHttpFunctionEvent(method, path); const result = adapter.getRequest(event); expect(result).toHaveProperty('method', method.toUpperCase()); }); it('should return the correct mapping for the request when it has no body', () => { const method = 'GET'; const path = '/collaborators'; const body = undefined; const event = createHttpFunctionEvent( method, path, body, {}, { page: '2' }, ); const result = adapter.getRequest(event); const remoteAddress = event.__ow_headers['x-forwarded-for']; expect(result).toHaveProperty('method', method); expect(result).toHaveProperty('headers'); expect(result).toHaveProperty('body'); expect(result.body).not.toBeInstanceOf(Buffer); expect(result.body).toBeUndefined(); expect(result).toHaveProperty('remoteAddress', remoteAddress); const resultPath = getPathWithQueryStringParams(path, event.__ow_query); expect(result).toHaveProperty('path', resultPath); }); it('should return the correct mapping for the request when send stripBasePath', () => { const stripBasePath = '/prod'; const method = 'GET'; const path = '/prod/collaborators'; const body = undefined; const strippedAdapter = new HttpFunctionAdapter({ stripBasePath }); const event = createHttpFunctionEvent(method, path, body); const result = strippedAdapter.getRequest(event); const remoteAddress = event.__ow_headers['x-forwarded-for']; expect(result).toHaveProperty('method', method); expect(result).toHaveProperty('headers'); expect(result).toHaveProperty('body'); expect(result.body).not.toBeInstanceOf(Buffer); expect(result.body).toBeUndefined(); expect(result).toHaveProperty('remoteAddress', remoteAddress); const resultPath = getPathWithQueryStringParams( path.replace('/prod', ''), event.__ow_query, ); expect(result).toHaveProperty('path', resultPath); }); }); describe('getResponse', () => { it('should return the correct mapping for the response', () => { const method = 'PUT'; const path = '/collaborators'; const requestBody = { name: 'H4ad Collaborator V2' }; const queryParams = { page: '2' }; const resultBody = '{"success":true}'; const resultStatusCode = 200; const resultIsBase64Encoded = false; const event = createHttpFunctionEvent( method, path, requestBody, {}, queryParams, ); const resultHeaders = event.__ow_headers; const result = adapter.getResponse({ event, log: {} as ILogger, body: resultBody, isBase64Encoded: resultIsBase64Encoded, statusCode: resultStatusCode, headers: resultHeaders, }); expect(result).toHaveProperty('statusCode', 200); expect(result).toHaveProperty('body', resultBody); expect(result).toHaveProperty('headers'); }); }); describe('onErrorWhileForwarding', () => { it('should resolver call succeed', () => { const method = 'GET'; const path = '/events'; const requestBody = undefined; const event = createHttpFunctionEvent(method, path, requestBody); const log = {} as ILogger; const resolver: DelegatedResolver = { fail: vitest.fn(), succeed: vitest.fn(), }; const respondWithErrors = true; const error = new Error('Test error'); const oldGetResponse = adapter.getResponse.bind(adapter); let getResponseResult: DigitalOceanHttpResponse | undefined; adapter.getResponse = vitest.fn( (params: GetResponseAdapterProps) => { expect(params.event).toBe(event); expect(params.statusCode).toBe(500); expect(params.body).toBe(error.stack); expect(params.isBase64Encoded).toBe(false); expect(params.log).toBe(log); expect(params.headers).toStrictEqual({}); getResponseResult = oldGetResponse(params); return getResponseResult; }, ); adapter.onErrorWhileForwarding({ event, log, delegatedResolver: resolver, respondWithErrors, error, }); // eslint-disable-next-line @typescript-eslint/unbound-method expect(adapter.getResponse).toHaveBeenCalledTimes(1); expect(resolver.fail).toHaveBeenCalledTimes(0); expect(resolver.succeed).toHaveBeenCalledTimes(1); expect(resolver.succeed).toHaveBeenCalledWith(getResponseResult); }); it('should resolver call succeed but without sending errors', () => { const method = 'GET'; const path = '/users'; const requestBody = undefined; const event = createHttpFunctionEvent(method, path, requestBody); const log = {} as ILogger; const resolver: DelegatedResolver = { fail: vitest.fn(), succeed: vitest.fn(), }; const respondWithErrors = false; const error = new Error('Test error'); const oldGetResponse = adapter.getResponse.bind(adapter); let getResponseResult: DigitalOceanHttpResponse | undefined; adapter.getResponse = vitest.fn( (params: GetResponseAdapterProps) => { expect(params.event).toBe(event); expect(params.statusCode).toBe(500); expect(params.body).toBe(''); expect(params.isBase64Encoded).toBe(false); expect(params.log).toBe(log); expect(params.headers).toStrictEqual({}); getResponseResult = oldGetResponse(params); return getResponseResult; }, ); adapter.onErrorWhileForwarding({ event, log, delegatedResolver: resolver, respondWithErrors, error, }); // eslint-disable-next-line @typescript-eslint/unbound-method expect(adapter.getResponse).toHaveBeenCalledTimes(1); expect(resolver.fail).toHaveBeenCalledTimes(0); expect(resolver.succeed).toHaveBeenCalledTimes(1); expect(resolver.succeed).toHaveBeenCalledWith(getResponseResult); }); }); }); ================================================ FILE: test/adapters/digital-ocean/utils/event.ts ================================================ import { HttpFunctionAdapter } from '../../../../src/adapters/digital-ocean'; import { createHttpFunctionEvent } from './http-function'; export const allDigitalOceanEvents: Array<[string, any]> = [ [ HttpFunctionAdapter.name, createHttpFunctionEvent('post', '/users', { name: 'test' }), ], [HttpFunctionAdapter.name, createHttpFunctionEvent('get', '/potatos')], [HttpFunctionAdapter.name, createHttpFunctionEvent('get', '')], [ HttpFunctionAdapter.name, createHttpFunctionEvent('get', '/query', undefined, undefined, { page: '1', }), ], ]; ================================================ FILE: test/adapters/digital-ocean/utils/http-function.ts ================================================ import { type DigitalOceanHttpEvent } from '../../../../src/@types/digital-ocean'; export function createHttpFunctionEvent( method: string, path: string, body?: Record, headers?: Record, queryParams?: Record, ): DigitalOceanHttpEvent { return { __ow_method: method, __ow_query: new URLSearchParams(queryParams).toString(), __ow_body: JSON.stringify(body), __ow_headers: { accept: '*/*', 'accept-encoding': 'gzip', 'cdn-loop': 'cloudflare', 'cf-connecting-ip': '45.444.444.444', 'cf-ipcountry': 'BR', 'cf-ray': '4444443444a537-GRU', 'cf-visitor': '{"scheme":"https"}', 'content-type': 'application/json', host: 'ccontroller', 'user-agent': 'insomnia/2022.4.2', 'x-custom': 'potato', 'x-forwarded-for': '45.444.444.444', 'x-forwarded-proto': 'https', 'x-request-id': 'xxxxxxxxxxxxxxxxxx', ...headers, }, __ow_path: path, __ow_isBase64Encoded: false, }; } ================================================ FILE: test/adapters/dummy/dummy.adapter.spec.ts ================================================ import { beforeEach, describe, expect, it, vitest } from 'vitest'; import { type DelegatedResolver, EmptyResponse, createDefaultLogger, } from '../../../src'; import { DummyAdapter } from '../../../src/adapters/dummy'; describe(DummyAdapter.name, () => { let adapter!: DummyAdapter; beforeEach(() => { adapter = new DummyAdapter(); }); describe('getAdapterName', () => { it('should be the same name of the class', () => { expect(adapter.getAdapterName()).toBe(DummyAdapter.name); }); }); describe('canHandle', () => { it('should always return true', () => { expect(adapter.canHandle()).toBe(true); }); }); describe('getRequest', () => { it('should always create the same request', () => { const request = adapter.getRequest(); expect(request).toHaveProperty('body', undefined); expect(request).toHaveProperty('method', 'POST'); expect(request).toHaveProperty('path', '/dummy'); expect(request.headers).toStrictEqual({}); }); }); describe('getResponse', () => { it('should always return empty response', () => { expect(adapter.getResponse()).toBe(EmptyResponse); }); }); describe('onErrorWhileForwarding', () => { it('should always resolve with success', () => { const resolver: DelegatedResolver = { fail: vitest.fn(), succeed: vitest.fn(), }; adapter.onErrorWhileForwarding({ event: void 0, log: createDefaultLogger(), respondWithErrors: true, error: new Error('test'), delegatedResolver: resolver, }); expect(resolver.succeed).toHaveBeenCalled(); }); }); }); ================================================ FILE: test/adapters/huawei/huawei-api-gateway.adapter.spec.ts ================================================ import { beforeEach, describe, expect, it, vitest } from 'vitest'; import { type DelegatedResolver, type GetResponseAdapterProps, type ILogger, getEventBodyAsBuffer, getFlattenedHeadersMap, getPathWithQueryStringParams, } from '../../../src'; import type { HuaweiApiGatewayEvent, HuaweiApiGatewayResponse, } from '../../../src/@types/huawei'; import { HuaweiApiGatewayAdapter } from '../../../src/adapters/huawei'; import { createCanHandleTestsForAdapter } from '../utils/can-handle'; import { createHuaweiApiGateway } from './utils/huawei-api-gateway'; describe(HuaweiApiGatewayAdapter.name, () => { let adapter!: HuaweiApiGatewayAdapter; beforeEach(() => { adapter = new HuaweiApiGatewayAdapter(); }); describe('getAdapterName', () => { it('should be the same name of the class', () => { expect(adapter.getAdapterName()).toBe(HuaweiApiGatewayAdapter.name); }); }); createCanHandleTestsForAdapter( () => new HuaweiApiGatewayAdapter(), undefined, ); describe('getRequest', () => { it('should return the correct mapping for the request', () => { const method = 'PUT'; const path = '/collaborators'; const body = { name: 'H4ad Collaborator' }; const queryParams = { page: '44' }; const event = createHuaweiApiGateway(method, path, body, {}, queryParams); const result = adapter.getRequest(event); expect(result).toHaveProperty('method', method); expect(result).toHaveProperty('headers'); expect(result).toHaveProperty('body'); expect(result.body).toBeInstanceOf(Buffer); const [bodyBuffer, contentLength] = getEventBodyAsBuffer( JSON.stringify(body), false, ); expect(result.body).toStrictEqual(bodyBuffer); expect(result.headers).toHaveProperty('content-length'); expect(result.headers['content-length']).toBe(String(contentLength)); const remoteAddress = event.headers['x-real-ip']; expect(result).toHaveProperty('remoteAddress', remoteAddress); const resultPath = getPathWithQueryStringParams( path, event.queryStringParameters, ); expect(result).toHaveProperty('path', resultPath); }); it('should return the correct mapping for the request when it has no body', () => { const method = 'GET'; const path = '/potatos'; const body = undefined; const event = createHuaweiApiGateway(method, path, body, undefined, { page: '2', }); const result = adapter.getRequest(event); const remoteAddress = event.headers['x-real-ip']; expect(result).toHaveProperty('method', method); expect(result).toHaveProperty('headers'); expect(result).toHaveProperty('body'); expect(result.body).not.toBeInstanceOf(Buffer); expect(result.body).toBeUndefined(); expect(result).toHaveProperty('remoteAddress', remoteAddress); const resultPath = getPathWithQueryStringParams( path, event.queryStringParameters, ); expect(result).toHaveProperty('path', resultPath); }); it('should return the correct mapping for the request when send stripBasePath', () => { const stripBasePath = '/prod'; const method = 'GET'; const path = '/prod/collaborators'; const body = undefined; const strippedAdapter = new HuaweiApiGatewayAdapter({ stripBasePath }); const event = createHuaweiApiGateway(method, path, body); const result = strippedAdapter.getRequest(event); const remoteAddress = event.headers['x-real-ip']; expect(result).toHaveProperty('method', method); expect(result).toHaveProperty('headers'); expect(result).toHaveProperty('body'); expect(result.body).not.toBeInstanceOf(Buffer); expect(result.body).toBeUndefined(); expect(result).toHaveProperty('remoteAddress', remoteAddress); const resultPath = getPathWithQueryStringParams( path.replace('/prod', ''), event.queryStringParameters, ); expect(result).toHaveProperty('path', resultPath); }); }); describe('getResponse', () => { it('should return the correct mapping for the response', () => { const method = 'PUT'; const path = '/collaborators'; const requestBody = { name: 'H4ad Collaborator V2' }; const queryParams = { page: '2' }; const resultBody = '{"success":true}'; const resultStatusCode = 200; const resultIsBase64Encoded = false; const event = createHuaweiApiGateway( method, path, requestBody, {}, queryParams, ); const resultHeaders = getFlattenedHeadersMap(event.headers); const result = adapter.getResponse({ event, log: {} as ILogger, body: resultBody, isBase64Encoded: resultIsBase64Encoded, statusCode: resultStatusCode, headers: { ...resultHeaders, }, }); expect(result).toHaveProperty('statusCode', 200); expect(result).toHaveProperty('body', resultBody); expect(result).toHaveProperty('headers'); expect(result).toHaveProperty('isBase64Encoded', resultIsBase64Encoded); }); it('should return the correct mapping for the response when set-cookie is array', () => { const method = 'PUT'; const path = '/collaborators'; const requestBody = { name: 'H4ad Collaborator V2' }; const queryParams = { page: '2' }; const resultBody = '{"success":true}'; const resultStatusCode = 200; const resultIsBase64Encoded = false; const event = createHuaweiApiGateway( method, path, requestBody, {}, queryParams, ); const resultHeaders = getFlattenedHeadersMap(event.headers); const result = adapter.getResponse({ event, log: {} as ILogger, body: resultBody, isBase64Encoded: resultIsBase64Encoded, statusCode: resultStatusCode, headers: { ...resultHeaders, }, }); expect(result).toHaveProperty('statusCode', 200); expect(result).toHaveProperty('body', resultBody); expect(result).toHaveProperty('headers'); expect(result).toHaveProperty('isBase64Encoded', resultIsBase64Encoded); }); }); describe('onErrorWhileForwarding', () => { it('should resolver call succeed', () => { const method = 'GET'; const path = '/events'; const requestBody = undefined; const event = createHuaweiApiGateway(method, path, requestBody); const log = {} as ILogger; const resolver: DelegatedResolver = { fail: vitest.fn(), succeed: vitest.fn(), }; const respondWithErrors = true; const error = new Error('Test error'); const oldGetResponse = adapter.getResponse.bind(adapter); let getResponseResult: HuaweiApiGatewayResponse | undefined; adapter.getResponse = vitest.fn( (params: GetResponseAdapterProps) => { expect(params.event).toBe(event); expect(params.statusCode).toBe(500); expect(params.body).toBe(error.stack); expect(params.isBase64Encoded).toBe(false); expect(params.log).toBe(log); expect(params.headers).toStrictEqual({}); getResponseResult = oldGetResponse(params); return getResponseResult; }, ); adapter.onErrorWhileForwarding({ event, log, delegatedResolver: resolver, respondWithErrors, error, }); // eslint-disable-next-line @typescript-eslint/unbound-method expect(adapter.getResponse).toHaveBeenCalledTimes(1); expect(resolver.fail).toHaveBeenCalledTimes(0); expect(resolver.succeed).toHaveBeenCalledTimes(1); expect(resolver.succeed).toHaveBeenCalledWith(getResponseResult); }); it('should resolver call succeed but without sending errors', () => { const method = 'GET'; const path = '/users'; const requestBody = undefined; const event = createHuaweiApiGateway(method, path, requestBody); const log = {} as ILogger; const resolver: DelegatedResolver = { fail: vitest.fn(), succeed: vitest.fn(), }; const respondWithErrors = false; const error = new Error('Test error'); const oldGetResponse = adapter.getResponse.bind(adapter); let getResponseResult: HuaweiApiGatewayResponse | undefined; adapter.getResponse = vitest.fn( (params: GetResponseAdapterProps) => { expect(params.event).toBe(event); expect(params.statusCode).toBe(500); expect(params.body).toBe(''); expect(params.isBase64Encoded).toBe(false); expect(params.log).toBe(log); expect(params.headers).toStrictEqual({}); getResponseResult = oldGetResponse(params); return getResponseResult; }, ); adapter.onErrorWhileForwarding({ event, log, delegatedResolver: resolver, respondWithErrors, error, }); // eslint-disable-next-line @typescript-eslint/unbound-method expect(adapter.getResponse).toHaveBeenCalledTimes(1); expect(resolver.fail).toHaveBeenCalledTimes(0); expect(resolver.succeed).toHaveBeenCalledTimes(1); expect(resolver.succeed).toHaveBeenCalledWith(getResponseResult); }); }); }); ================================================ FILE: test/adapters/huawei/utils/events.ts ================================================ import { HuaweiApiGatewayAdapter } from '../../../../src/adapters/huawei'; import { createHuaweiApiGateway } from './huawei-api-gateway'; export const allHuaweiEvents: Array<[string, any]> = [ [HuaweiApiGatewayAdapter.name, createHuaweiApiGateway('GET', '/users')], [ HuaweiApiGatewayAdapter.name, createHuaweiApiGateway('GET', '/test', undefined, { 'x-batata': 'true' }), ], [HuaweiApiGatewayAdapter.name, createHuaweiApiGateway('DELETE', '/test/2')], ]; ================================================ FILE: test/adapters/huawei/utils/huawei-api-gateway.ts ================================================ import type { BothValueHeaders } from '../../../../src'; import type { HuaweiApiGatewayEvent, HuaweiRequestQueryStringParameters, } from '../../../../src/@types/huawei'; export function createHuaweiApiGateway( method: string, path: string, body?: object, headers?: BothValueHeaders, queryParams?: HuaweiRequestQueryStringParameters, ): HuaweiApiGatewayEvent { const bodyBuffer = Buffer.from(JSON.stringify(body || ''), 'utf-8'); return { body: body ? bodyBuffer.toString('base64') : '', headers: { accept: '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', 'accept-encoding': 'gzip, deflate, br', 'accept-language': 'pt-BR,pt;q=0.9,en-US;q=0.8,en;q=0.7,bg;q=0.6', 'cache-control': 'max-age=0', 'content-length': Buffer.byteLength(bodyBuffer).toString(), connection: 'keep-alive', dnt: '1', host: 'test.apig.la-south-2.huaweicloudapis.com', 'sec-ch-ua': '" Not A;Brand";v="99", "Chromium";v="101", "Google Chrome";v="101"', 'sec-ch-ua-mobile': '?0', 'sec-ch-ua-platform': '"Windows"', 'sec-fetch-dest': 'document', 'sec-fetch-mode': 'navigate', 'sec-fetch-site': 'none', 'sec-fetch-user': '?1', 'upgrade-insecure-requests': '1', 'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/101.0.4951.67 Safari/537.36', 'x-forwarded-for': '33.33.33.33', 'x-forwarded-host': 'test.apig.la-south-2.huaweicloudapis.com', 'x-forwarded-port': '443', 'x-forwarded-proto': 'https', 'x-real-ip': '33.33.33.33', 'x-request-id': 'eb6f50b5922fd574175f8115ba22c168', ...headers, }, httpMethod: method, isBase64Encoded: true, 'lubanops-gtrace-id': '', 'lubanops-ndomain-id': '', 'lubanops-nenv-id': '', 'lubanops-nspan-id': '', 'lubanops-ntrace-id': '', 'lubanops-sevent-id': '', path, pathParameters: {}, queryStringParameters: { ...queryParams, }, requestContext: { apiId: '863aad9dd5dd4043b7f6745b34922323', requestId: 'eb6f50b5922fd574175f8115ba943222', stage: 'RELEASE', }, }; } ================================================ FILE: test/adapters/test.example ================================================ describe(Adapter.name, () => { let adapter!: Adapter; beforeEach(() => { adapter = new Adapter(); }); describe('getAdapterName', () => { it('should be the same name of the class', () => { expect(adapter.getAdapterName()).toBe(Adapter.name); }); }); createCanHandleTestsForAdapter(() => new Adapter(), undefined); describe('getRequest', () => { it('should return the correct mapping for the request', () => { const method = 'PUT'; const path = '/events'; const body = { name: 'H4ad Event' }; }); }); describe('getResponse', () => { it('should return the correct mapping for the response', () => { const method = 'PUT'; const path = '/events'; const requestBody = { name: 'H4ad Event' }; const resultBody = '{"success":true}'; const resultStatusCode = 200; const resultIsBase64Encoded = false; }); }); describe('onErrorWhileForwarding', () => { it('should resolver call succeed', () => { const method = 'GET'; const path = '/events'; const requestBody = undefined; }); }); }); ================================================ FILE: test/adapters/utils/can-handle.ts ================================================ import { beforeEach, describe, expect, it } from 'vitest'; import type { AdapterContract, ILogger } from '../../../src'; import { allEvents } from './events'; export function createCanHandleTestsForAdapter< T, TContext = any, TResponse = any, >( adapterFactory: () => AdapterContract, context: TContext, logger: ILogger = {} as ILogger, ): void { let adapter!: AdapterContract; beforeEach(() => { adapter = adapterFactory(); }); describe('canHandle', () => { it('should return true when is valid event', () => { const events = allEvents.filter( ([adapterName]) => adapterName === adapter.getAdapterName(), )!; expect(events.length).toBeGreaterThan(0); for (const [, event] of events) expect(adapter.canHandle(event, context, logger)).toBe(true); }); it('should return false when is not a valid event', () => { const events = allEvents.filter( ([adapterName]) => adapterName !== adapter.getAdapterName(), ); expect(events.length).toBeGreaterThan(0); for (const [adapterName, event] of events) { const canHandle = adapter.canHandle(event, context, logger); expect(`${adapterName}: ${canHandle}`).toEqual(`${adapterName}: false`); } }); }); } ================================================ FILE: test/adapters/utils/events.ts ================================================ import { allAWSEvents } from '../aws/utils/events'; import { allAzureEvents } from '../azure/utils/events'; import { allDigitalOceanEvents } from '../digital-ocean/utils/event'; import { allHuaweiEvents } from '../huawei/utils/events'; /** * Events from all event sources that can be used to test adapters */ export const allEvents: [string, any][] = [ ...allAWSEvents, ...allHuaweiEvents, ...allAzureEvents, ...allDigitalOceanEvents, ]; ================================================ FILE: test/core/base-handler.spec.ts ================================================ import { describe, expect, it, vitest } from 'vitest'; import { type AdapterContract, type AdapterRequest, BaseHandler, type ILogger, ServerlessRequest, ServerlessResponse, createDefaultLogger, } from '../../src'; import { AlbAdapter, SQSAdapter } from '../../src/adapters/aws'; import { createSQSEvent } from '../adapters/aws/utils/sqs'; class TestHandler extends BaseHandler< TApp, unknown, TContext, TCallback, unknown, TReturn > { getHandler = vitest.fn(); /** * {@inheritDoc} */ public override getAdapterByEventAndContext( event: any, context: any, adapters: AdapterContract[], log: ILogger, ): AdapterContract { return super.getAdapterByEventAndContext(event, context, adapters, log); } /** * {@inheritDoc} */ public override getServerlessRequestResponseFromAdapterRequest( requestValues: AdapterRequest, ): [request: ServerlessRequest, response: ServerlessResponse] { return super.getServerlessRequestResponseFromAdapterRequest(requestValues); } } describe(BaseHandler.name, () => { it('should can resolve adapter by event and context', () => { const handler = new TestHandler(); const testEvent = createSQSEvent(); const eventAdapter = new SQSAdapter(); const adapters = [eventAdapter, new AlbAdapter()] as AdapterContract< any, any, any >[]; const context = {}; const logger = createDefaultLogger(); adapters.forEach(adapter => { // @ts-ignore adapter.canHandle = vitest.fn(adapter.canHandle.bind(adapter)); }); expect( handler.getAdapterByEventAndContext(testEvent, context, adapters, logger), ).toBe(eventAdapter); adapters.forEach(adapter => { // eslint-disable-next-line @typescript-eslint/unbound-method expect(adapter.canHandle).toHaveBeenCalledWith( testEvent, context, logger, ); }); }); it('should throw error when could not resolve the adapter', () => { const handler = new TestHandler(); const testEvent = {}; const adapters = []; expect(() => handler.getAdapterByEventAndContext( testEvent, {}, adapters, createDefaultLogger(), ), ).toThrowError("Couldn't find adapter"); }); it('should throw error when resolve more than one adapter', () => { const handler = new TestHandler(); const testEvent = createSQSEvent(); const adapters = [new SQSAdapter(), new SQSAdapter()] as AdapterContract< any, any, any >[]; const adapterNames = adapters .map(adapter => adapter.getAdapterName()) .join(', '); expect(() => handler.getAdapterByEventAndContext( testEvent, {}, adapters, createDefaultLogger(), ), ).toThrowError(adapterNames); }); it('should can create correctly request and response from adapter request', () => { const handler = new TestHandler(); const testEvent = createSQSEvent(); const adapter = new SQSAdapter({ sqsForwardPath: '/sqs', sqsForwardMethod: 'POST', }); const adapterRequest = adapter.getRequest(testEvent); const [request, response] = handler.getServerlessRequestResponseFromAdapterRequest(adapterRequest); expect(request).toBeInstanceOf(ServerlessRequest); expect(request).toHaveProperty('method', adapterRequest.method); expect(request).toHaveProperty('url', adapterRequest.path); expect(request).toHaveProperty('headers', adapterRequest.headers); expect(request).toHaveProperty('body', adapterRequest.body); expect(request.socket).toHaveProperty( 'remoteAddress', adapterRequest.remoteAddress, ); expect(response).toBeInstanceOf(ServerlessResponse); }); }); ================================================ FILE: test/core/current-invoke.spec.ts ================================================ import { describe, expect, it } from 'vitest'; import { getCurrentInvoke, setCurrentInvoke } from '../../src'; describe('CurrentInvoke', () => { it('should initial values of getCurrentInvoke be null', () => { const initial = getCurrentInvoke(); expect(initial).toBeDefined(); expect(initial).toHaveProperty('event', null); expect(initial).toHaveProperty('context', null); }); it('should set and get current invoke without problems', () => { const event = { batata: true }; const context = { potato: true }; expect(() => setCurrentInvoke({ event, context })).not.toThrowError(); const currentInvoke = getCurrentInvoke(); expect(currentInvoke).toHaveProperty('event', event); expect(currentInvoke).toHaveProperty('context', context); }); }); ================================================ FILE: test/core/event-body.spec.ts ================================================ import { describe, expect, it } from 'vitest'; import { getEventBodyAsBuffer } from '../../src'; describe('getEventBodyAsBuffer', () => { it('should return correctly the body in utf-8 as buffer', () => { const body = '{}'; const [bodyAsBuffer, contentLength] = getEventBodyAsBuffer(body, false); expect(bodyAsBuffer).toBeInstanceOf(Buffer); expect(contentLength).toBe(2); expect(bodyAsBuffer.toString('utf8')).toBe(body); }); it('should return correctly the body in base64 as buffer', () => { const body = Buffer.from('{}', 'utf8').toString('base64'); const [bodyAsBuffer, contentLength] = getEventBodyAsBuffer(body, true); expect(bodyAsBuffer).toBeInstanceOf(Buffer); expect(contentLength).toBe(2); expect(bodyAsBuffer.toString('base64')).toBe(body); }); }); ================================================ FILE: test/core/headers.spec.ts ================================================ import { describe, expect, it } from 'vitest'; import { type BothValueHeaders, getFlattenedHeadersMap, getFlattenedHeadersMapAndCookies, getMultiValueHeadersMap, } from '../../src'; describe('getFlattenedHeadersMap', () => { it('should return headers flattened', () => { const headerLists: BothValueHeaders[] = [ { 'Accept-Encoding': 'gzip', 'Accept-Language': 'en-US,en;q=0.9', Host: undefined, 'Content-Type': '', 'Content-Length': 40 as unknown as string, }, { 'Accept-Encoding': ['gzip'], 'Accept-Language': ['en-US', 'en;q=0.9'], Host: undefined, 'Content-Type': '', 'Content-Length': [40] as unknown as string[], }, ]; for (const headers of headerLists) { const flattenedHeaders = getFlattenedHeadersMap(headers); expect(Object.keys(flattenedHeaders).length).toEqual( Object.keys(headers).length, ); expect(flattenedHeaders).toHaveProperty('Accept-Encoding'); expect(flattenedHeaders['Accept-Encoding']).toEqual('gzip'); expect(flattenedHeaders['Accept-Language']).toEqual('en-US,en;q=0.9'); expect(flattenedHeaders['Content-Length']).toEqual('40'); } }); it('should return headers flattened with custom options', () => { const defaultSingleValueHeaders = { 'Accept-Encoding': 'gzip', 'Accept-Language': 'en-US,en;q=0.9', }; const defaultMultiValueHeaders = { 'Accept-Encoding': ['gzip'], 'Accept-Language': ['en-US', 'en;q=0.9'], }; const headerLists: [ headers: BothValueHeaders, separator: string, lowerCase: boolean, ][] = [ [defaultSingleValueHeaders, ',', false], [defaultMultiValueHeaders, ',', false], [defaultSingleValueHeaders, '|', false], [defaultMultiValueHeaders, '|', false], [defaultSingleValueHeaders, ',', true], [defaultMultiValueHeaders, ',', true], [defaultSingleValueHeaders, '|', true], [defaultMultiValueHeaders, '|', true], ]; for (const [headers, separator, lowerCase] of headerLists) { const flattenedHeaders = getFlattenedHeadersMap( headers, separator, lowerCase, ); expect(Object.keys(flattenedHeaders).length).toEqual( Object.keys(headers).length, ); const checkedHeader = lowerCase ? 'accept-encoding' : 'Accept-Encoding'; const arrayCheckedHeader = lowerCase ? 'accept-language' : 'Accept-Language'; expect(flattenedHeaders).toHaveProperty(checkedHeader); expect(flattenedHeaders[checkedHeader]).toEqual('gzip'); if (Array.isArray(headers['Accept-Language'])) { expect(headers['Accept-Language']).toStrictEqual( flattenedHeaders[arrayCheckedHeader].split(separator), ); } else { expect(headers['Accept-Language']).toStrictEqual( flattenedHeaders[arrayCheckedHeader], ); } } }); }); describe('getMultiValueHeadersMap', () => { it('should return headers flattened', () => { const headerLists: BothValueHeaders[] = [ { 'Accept-Encoding': 'gzip', 'Accept-Language': 'en-US,en;q=0.9', Host: undefined, 'Content-Type': '', }, { 'Accept-Encoding': ['gzip'], 'Accept-Language': ['en-US', 'en;q=0.9'], Host: undefined, 'Content-Type': '', }, ]; for (const headers of headerLists) { const multiValueHeadersMap = getMultiValueHeadersMap(headers); expect(Object.keys(multiValueHeadersMap).length).toEqual( Object.keys(headers).length, ); expect(multiValueHeadersMap).toHaveProperty('accept-encoding', ['gzip']); expect( Object.keys(multiValueHeadersMap).every(key => Array.isArray(multiValueHeadersMap[key]), ), ); } }); }); describe('getFlattenedHeadersMapAndCookies', () => { it('should return headers flattened', () => { const headerLists: BothValueHeaders[] = [ { 'Accept-Encoding': 'gzip', 'Accept-Language': 'en-US,en;q=0.9', Host: undefined, 'Content-Type': '', 'Content-Length': 40 as unknown as string, 'Set-Cookie': 'blabla', }, { 'Accept-Encoding': ['gzip'], 'Accept-Language': ['en-US', 'en;q=0.9'], Host: undefined, 'Content-Type': '', 'Content-Length': [40] as unknown as string[], 'Set-Cookie': ['blabla'], }, ]; for (const headers of headerLists) { const { headers: flattenedHeaders, cookies } = getFlattenedHeadersMapAndCookies(headers); expect(Object.keys(flattenedHeaders).length).toEqual( Object.keys(headers).length - 1, ); expect(flattenedHeaders).toHaveProperty('Accept-Encoding'); expect(flattenedHeaders['Accept-Encoding']).toEqual('gzip'); expect(flattenedHeaders['Accept-Language']).toEqual('en-US,en;q=0.9'); expect(flattenedHeaders['Content-Length']).toEqual('40'); expect(flattenedHeaders['Content-Length']).toEqual('40'); expect(cookies[0]).toEqual('blabla'); } }); }); ================================================ FILE: test/core/is-binary.spec.ts ================================================ import { describe, expect, it } from 'vitest'; import { type BothValueHeaders, DEFAULT_BINARY_CONTENT_TYPES, DEFAULT_BINARY_ENCODINGS, getContentType, isBinary, isContentEncodingBinary, isContentTypeBinary, } from '../../src'; type HeaderListJest = [headers: BothValueHeaders, expectedValue: boolean][]; const headerListForContentEncodings: HeaderListJest = [ [{ 'content-encoding': undefined }, false], [{ 'content-encoding': [] }, false], [{ 'content-encoding': 'non-standard' }, false], [{ 'content-encoding': ['non-standard'] }, false], [{ 'content-encoding': 'gzip' }, true], [{ 'content-encoding': 'deflate' }, true], [{ 'content-encoding': 'br' }, true], [{ 'content-encoding': 'gzip,non-standard' }, true], [{ 'content-encoding': ['gzip'] }, true], [{ 'content-encoding': ['gzip', 'non-standard'] }, true], [{ 'content-encoding': ['deflate'] }, true], [{ 'content-encoding': ['deflate', 'non-standard'] }, true], [{ 'content-encoding': ['br'] }, true], [{ 'content-encoding': ['br', 'non-standard'] }, true], ]; const headerListForContentTypes: HeaderListJest = [ [{ 'content-type': undefined }, false], [{ 'content-type': [] }, false], [{ 'content-type': 'application/json' }, false], [{ 'content-type': ['application/json'] }, false], [{ 'content-type': 'application/json,image/png' }, false], [{ 'content-type': 'application/json;image/png' }, false], [{ 'content-type': ['application/json', 'image/png'] }, false], [{ 'content-type': 'image/png' }, true], [{ 'content-type': ['image/png'] }, true], [{ 'content-type': 'video/mp4' }, true], [{ 'content-type': ['video/mp4'] }, true], [{ 'content-type': 'application/pdf' }, true], [{ 'content-type': ['application/pdf'] }, true], ]; describe('isContentEncodingBinary', () => { it('should correctly check if content encoding is binary', () => { const headersList: HeaderListJest = [ [{ 'content-type': 'application/json' }, false], ...headerListForContentEncodings, ]; const binaryEncodings = DEFAULT_BINARY_ENCODINGS; for (const [headers, expectedValue] of headersList) { const isBinary = isContentEncodingBinary(headers, binaryEncodings); expect(isBinary).toBe(expectedValue); } }); }); describe('getContentType', () => { it('should correctly return the content type from headers', () => { const headersList: [headers: BothValueHeaders, expectedValue: string][] = [ [{ 'content-encoding': 'gzip' }, ''], [{ 'content-type': 'application/json' }, 'application/json'], [{ 'content-type': ['application/json'] }, 'application/json'], [ { 'content-type': 'application/json,image/png' }, 'application/json,image/png', ], [{ 'content-type': 'application/json;image/png' }, 'application/json'], [ { 'content-type': ['application/json', 'image/png'] }, 'application/json', ], [{ 'content-type': ['image/png', 'application/json'] }, 'image/png'], ]; for (const [headers, expectedValue] of headersList) { const isBinary = getContentType(headers); expect(isBinary).toBe(expectedValue); } }); }); describe('isContentTypeBinary', () => { it('should correctly check if content type is binary', () => { const headersList: [headers: BothValueHeaders, expectedValue: boolean][] = [ [{ 'content-encoding': 'gzip' }, false], ...headerListForContentTypes, ]; const binaryEncodings = DEFAULT_BINARY_CONTENT_TYPES; for (const [headers, expectedValue] of headersList) { const isBinary = isContentTypeBinary(headers, binaryEncodings); expect(isBinary).toBe(expectedValue); } }); }); describe('isBinary', () => { it('should correctly return if content is binary', () => { const headersList: [headers: BothValueHeaders, expectedValue: boolean][] = [ [{ Host: 'blablabla.com' }, false], ...headerListForContentEncodings, ...headerListForContentTypes, ]; const contentTypes = DEFAULT_BINARY_CONTENT_TYPES; const contentEncodings = DEFAULT_BINARY_ENCODINGS; for (const [headers, expectedValue] of headersList) { const isContentBinary = isBinary(headers, { contentTypes, contentEncodings, }); expect( isContentBinary, `contentTypes: ${contentTypes.join( ';', )}, contentEncodings: ${contentEncodings.join( ';', )}: has ${expectedValue} inside ${JSON.stringify(headers)}`, ).toBe(expectedValue); } }); it('should correctly return if content is binary with custom "isBinary" option', () => { const headersList: [headers: BothValueHeaders, expectedValue: boolean][] = [ [{ Host: 'blablabla.com' }, false], ...headerListForContentEncodings, ...headerListForContentTypes, ]; for (const [headers] of headersList) { const isContentBinary = isBinary(headers, { isBinary: () => true, }); expect(isContentBinary).toBe(true); } for (const [headers] of headersList) { const isContentBinary = isBinary(headers, { isBinary: false, }); expect(isContentBinary).toBe(false); } }); }); ================================================ FILE: test/core/logger.spec.ts ================================================ /* eslint-disable @typescript-eslint/unbound-method */ import { type MockInstance, afterEach, beforeEach, describe, expect, it, vitest, } from 'vitest'; import { type LogLevels, NO_OP, createDefaultLogger, isInternalLogger, } from '../../src'; describe('createDefaultLogger', () => { const mocks: MockInstance[] = []; beforeEach(() => { const mockMethods: (keyof Console)[] = [ 'error', 'info', 'warn', 'log', 'debug', ]; for (const method of mockMethods) { mocks.push( vitest.spyOn(global.console, method).mockImplementation(NO_OP), ); } }); afterEach(() => { for (const mock of mocks) mock.mockRestore(); }); it('should create correctly the logger instance', () => { expect(createDefaultLogger()).toBeDefined(); }); it('should lazy log when we pass a function', () => { const logger = createDefaultLogger({ level: 'debug' }); logger.debug('debug', () => '=true', ' works'); expect(global.console.debug).not.toHaveBeenNthCalledWith( 1, 'debug=true works', ); }); it('should log correctly with log level as none', () => { const logger = createDefaultLogger({ level: 'none' }); logger.error('error'); logger.warn('warn'); logger.info('info'); logger.verbose('verbose'); logger.debug('debug'); expect(global.console.error).not.toHaveBeenCalledWith('error'); expect(global.console.warn).not.toHaveBeenCalledWith('warn'); expect(global.console.info).not.toHaveBeenCalledWith('info'); expect(global.console.debug).not.toHaveBeenNthCalledWith(1, 'verbose'); expect(global.console.debug).not.toHaveBeenNthCalledWith(2, 'debug'); }); it('should log correctly with log level as error', () => { const logger = createDefaultLogger({ level: 'error' }); logger.error('error'); logger.warn('warn'); logger.info('info'); logger.verbose('verbose'); logger.debug('debug'); expect(global.console.error).toHaveBeenCalledWith('error'); expect(global.console.warn).not.toHaveBeenCalledWith('warn'); expect(global.console.info).not.toHaveBeenCalledWith('info'); expect(global.console.debug).not.toHaveBeenNthCalledWith(1, 'verbose'); expect(global.console.debug).not.toHaveBeenNthCalledWith(2, 'debug'); }); it('should log correctly with log level as warn', () => { const logger = createDefaultLogger({ level: 'warn' }); logger.error('error'); logger.warn('warn'); logger.info('info'); logger.verbose('verbose'); logger.debug('debug'); expect(global.console.error).toHaveBeenCalledWith('error'); expect(global.console.warn).toHaveBeenCalledWith('warn'); expect(global.console.info).not.toHaveBeenCalledWith('info'); expect(global.console.debug).not.toHaveBeenNthCalledWith(1, 'verbose'); expect(global.console.debug).not.toHaveBeenNthCalledWith(2, 'debug'); }); it('should log correctly with log level as info', () => { const logger = createDefaultLogger({ level: 'info' }); logger.error('error'); logger.warn('warn'); logger.info('info'); logger.verbose('verbose'); logger.debug('debug'); expect(global.console.error).toHaveBeenCalledWith('error'); expect(global.console.warn).toHaveBeenCalledWith('warn'); expect(global.console.info).toHaveBeenCalledWith('info'); expect(global.console.debug).not.toHaveBeenNthCalledWith(1, 'verbose'); expect(global.console.debug).not.toHaveBeenNthCalledWith(2, 'debug'); }); it('should log correctly with log level as verbose', () => { const logger = createDefaultLogger({ level: 'verbose' }); logger.error('error'); logger.warn('warn'); logger.info('info'); logger.verbose('verbose'); logger.debug('debug'); expect(global.console.error).toHaveBeenCalledWith('error'); expect(global.console.warn).toHaveBeenCalledWith('warn'); expect(global.console.info).toHaveBeenCalledWith('info'); expect(global.console.debug).toHaveBeenNthCalledWith(1, 'verbose'); expect(global.console.debug).not.toHaveBeenNthCalledWith(2, 'debug'); }); it('should log correctly with log level as debug', () => { const logger = createDefaultLogger({ level: 'debug' }); logger.error('error'); logger.warn('warn'); logger.info('info'); logger.verbose('verbose'); logger.debug('debug'); expect(global.console.error).toHaveBeenCalledWith('error'); expect(global.console.warn).toHaveBeenCalledWith('warn'); expect(global.console.info).toHaveBeenCalledWith('info'); expect(global.console.debug).toHaveBeenNthCalledWith(1, 'verbose'); expect(global.console.debug).toHaveBeenNthCalledWith(2, 'debug'); }); it('should throw error with invalid log level', () => { expect(() => createDefaultLogger({ level: 'random' as unknown as LogLevels }), ).toThrowError('Invalid'); }); }); describe('isInternalLogger', () => { const logLevelRecord: Record, true> = { debug: true, info: true, verbose: true, warn: true, error: true, }; const logLevels = Object.keys(logLevelRecord) as LogLevels[]; for (const logLevel of logLevels) { it(`instance created by createDefaultLogger with logLevel: ${logLevel} should return true`, () => { const logger = createDefaultLogger({ level: logLevel, }); expect(isInternalLogger(logger)).toBe(true); }); } it('random instance of ILogger should not return true', () => { expect( isInternalLogger({ debug: NO_OP, info: NO_OP, verbose: NO_OP, warn: NO_OP, error: NO_OP, }), ).toBe(false); }); }); ================================================ FILE: test/core/no-op.spec.ts ================================================ import { describe, expect, it } from 'vitest'; import { NO_OP } from '../../src'; describe('NO_OP', () => { it('should be a function', () => { expect(NO_OP).toBeInstanceOf(Function); }); it('should be callable and return undefined', () => { expect(() => NO_OP()).not.toThrowError(); expect(NO_OP()).toBe(undefined); }); }); ================================================ FILE: test/core/optional.spec.ts ================================================ import { describe, expect, it } from 'vitest'; import { getDefaultIfUndefined } from '../../src'; describe('getDefaultIfUndefined', () => { it('should return the value when value is not undefined', () => { const options: [testValue: any, defaultValue: any, expectedValue: any][] = [ ['batata', 'potato', 'batata'], [true, false, true], [false, true, false], ]; for (const [testValue, defaultValue, expectedValue] of options) { expect(getDefaultIfUndefined(testValue, defaultValue)).toBe( expectedValue, ); } }); it('should return the default value when value is undefined', () => { const options: [testValue: any, defaultValue: any, expectedValue: any][] = [ [undefined, true, true], [undefined, 'text', 'text'], [void 0, true, true], [void 0, 'text', 'text'], ]; for (const [testValue, defaultValue, expectedValue] of options) { expect(getDefaultIfUndefined(testValue, defaultValue)).toBe( expectedValue, ); } }); }); ================================================ FILE: test/core/path.spec.ts ================================================ import { describe, expect, it } from 'vitest'; import { buildStripBasePath, getPathWithQueryStringParams, getQueryParamsStringFromRecord, } from '../../src'; describe('getPathWithQueryStringParams', () => { it('should correctly return path and query string concaneted', () => { const options: [ path: string, queryParams: | string | Record | undefined | null, expectedValue: string, ][] = [ ['/users', undefined, '/users'], ['/users', null, '/users'], ['/users', 'limit=100', '/users?limit=100'], ['/users', {}, '/users'], ['/users', { page: '1' }, '/users?page=1'], ['/users', { page: '1', limit: '100' }, '/users?page=1&limit=100'], [ '/users', { page: '1', limit: '100', s: undefined }, '/users?page=1&limit=100&s=', ], ['/users', { joins: ['details'] }, '/users?joins=details'], [ '/users', { joins: ['details', 'address'] }, '/users?joins=details&joins=address', ], [ '/users', { page: '1', limit: '100', s: undefined, joins: ['details', 'address'], }, '/users?page=1&limit=100&s=&joins=details&joins=address', ], ]; for (const [path, queryParams, expectedValue] of options) { expect(getPathWithQueryStringParams(path, queryParams)).toBe( expectedValue, ); } }); }); describe('getQueryParamsStringFromRecord', () => { it('should correctly return query string from values', () => { const options: [ queryParams: | Record | undefined | null, expectedValue: string, ][] = [ [undefined, ''], [null, ''], [{ page: '1' }, 'page=1'], [{ page: '1', limit: '100' }, 'page=1&limit=100'], [{ page: '1', limit: '100', s: undefined }, 'page=1&limit=100&s='], [{ joins: ['details'] }, 'joins=details'], [{ joins: ['details', 'address'] }, 'joins=details&joins=address'], [ { page: '1', limit: '100', s: undefined, joins: ['details', 'address'], }, 'page=1&limit=100&s=&joins=details&joins=address', ], ]; for (const [queryParams, expectedValue] of options) expect(getQueryParamsStringFromRecord(queryParams)).toBe(expectedValue); }); }); describe('buildStripBasePath', () => { it('should correctly return query string from values', () => { const options: [ basePath: string | undefined, path: string, expectedValue: string, ][] = [ ['/prod', '/prod/users', '/users'], ['/v1', '/v1/potato', '/potato'], ['', '/v1/users', '/v1/users'], [undefined, '/v1/courses', '/v1/courses'], ['/prod', '/prod', '/'], ['/prod', '/ignore-path', '/ignore-path'], ['/v1', '/prod/v1/ignore-path', '/prod/v1/ignore-path'], ]; for (const [basePath, path, expectedValue] of options) expect(buildStripBasePath(basePath)(path)).toBe(expectedValue); }); }); ================================================ FILE: test/core/stream.spec.ts ================================================ import { ObjectReadableMock, ObjectWritableMock } from 'stream-mock'; import { describe, expect, it } from 'vitest'; import { NO_OP, waitForStreamComplete } from '../../src'; import ErrorReadableMock from './utils/stream'; describe('waitForStreamComplete', () => { it('should wait for the writable stream to complete', async () => { const testedData = 'test'; const read = new ObjectReadableMock(testedData); const writer = new ObjectWritableMock(); read.pipe(writer); const waitedStream = await waitForStreamComplete(writer); expect(waitedStream).toBe(writer); expect(writer.data.join('')).toBe(testedData); const waitedStream2 = await waitForStreamComplete(writer); expect(waitedStream2).toBe(writer); expect(writer.data.join('')).toBe(testedData); }); it('should wait for the readable stream to complete', async () => { const testedData: number[] = [0, 1, 2, 3, 4]; const read = new ObjectReadableMock(testedData); const resultData: number[] = []; read.on('data', value => resultData.push(value)); const waitedStream = await waitForStreamComplete(read); expect(waitedStream).toBe(read); expect(resultData).toStrictEqual(testedData); const waitedStream2 = await waitForStreamComplete(read); expect(waitedStream2).toBe(read); expect(resultData).toStrictEqual(testedData); }); it('should throw error when error occours', async () => { const error = new Error('error on read'); const read = new ErrorReadableMock(error, { objectMode: true }); read.on('data', NO_OP); await expect(waitForStreamComplete(read)).rejects.toThrowError(error); }); it('should handle correctly if events emit end and finish', async () => { const testedData: number[] = [0, 1, 2, 3, 4]; const read = new ObjectReadableMock(testedData); setTimeout(() => { read.pause(); read.emit('error'); read.emit('end'); read.emit('finish'); read.resume(); }, 100); await expect(waitForStreamComplete(read)).resolves.not.toThrowError(); }); }); ================================================ FILE: test/core/utils/stream.ts ================================================ // credits to: https://github.com/b4nst/stream-mock/pull/64/files#diff-52aee274967f2fcfa3ffa78ebba2f510dd23d176aa92ccf8c0ad4843373f5ce7 import { Readable, type ReadableOptions } from 'node:stream'; import type { IReadableMock } from 'stream-mock'; /** * ErrorReadableMock is a readable stream that mocks error. * * @example * ```typescript * import { ErrorReadableMock } from 'stream-mock'; * * const reader = new ErrorReadble(new Error("mock error")); * reader.on("data", () => console.log('not called')); * reader.on("error", e => console.log('called')); * ``` * * @internal */ export default class ErrorReadableMock extends Readable implements IReadableMock { /** * @param expectedError - error to be passed on callback. * @param options - Readable stream options. */ constructor(expectedError: Error, options: ReadableOptions = {}) { super(options); this.expectedError = expectedError; } public it: IterableIterator = [][Symbol.iterator](); private expectedError: Error; // tslint:disable-next-line:function-name Not responsible of this function name public override _read() { this.destroy(this.expectedError); } } ================================================ FILE: test/frameworks/apollo-server.framework.spec.ts ================================================ import type { OutgoingHttpHeaders } from 'http'; import { ApolloServer, type BaseContext, HeaderMap } from '@apollo/server'; import { describe, expect, it, vitest } from 'vitest'; import { ServerlessRequest, ServerlessResponse, getEventBodyAsBuffer, waitForStreamComplete, } from '../../src'; import { type ApolloServerContextArguments, ApolloServerFramework, type DefaultServerlessApolloServerContext, } from '../../src/frameworks/apollo-server'; import { JsonBodyParserFramework } from '../../src/frameworks/body-parser'; import { type TestRouteBuilderMethods } from './utils'; export const frameworkTestOptions: [ method: TestRouteBuilderMethods, path: string, query: string, statusCode: number, expectedValue: string, expectHeaderSet: boolean, ][] = [ ['post', 'GetUser', 'message', 200, 'Joga10', true], ['post', 'ListUser', 'message', 200, 'Unkownn', true], ['post', 'UserCreated', 'message', 200, 'Created', true], ['post', 'WrongQuery', 'nonexist', 400, 'Cannot query field', false], ]; describe(ApolloServerFramework.name, () => { describe('test requests', () => { for (const [ method, queryName, query, statusCode, expectedValue, expectHeaderSet, ] of frameworkTestOptions) { it(`${method}${queryName}: should forward request and receive response correctly`, async () => { const app = new ApolloServer({ typeDefs: 'type Query { message: String }', resolvers: { Query: { message: (_, __, context: ApolloServerContextArguments) => { context.response.setHeader('response-header', 'true'); return expectedValue; }, }, }, }); app.startInBackgroundHandlingStartupErrorsByLoggingAndFailingAllRequests(); const stringBody = JSON.stringify({ query: ` query Query { ${query} } `, }); const [bufferBody, bodyLength] = stringBody ? getEventBodyAsBuffer(stringBody, false) : [undefined, 0]; const framework = new JsonBodyParserFramework( new ApolloServerFramework(), ); const request = new ServerlessRequest({ method: method.toUpperCase(), url: '/', headers: { 'content-length': String(bodyLength), 'request-header': 'true', 'content-type': 'application/json', }, body: bufferBody, }); const response = new ServerlessResponse({ method: method.toUpperCase(), }); framework.sendRequest(app, request, response); await waitForStreamComplete(response); const resultBody = ServerlessResponse.body(response); expect(resultBody.toString('utf-8')).toContain( expectedValue !== undefined ? expectedValue : expectedValue, ); if (expectHeaderSet) { expect(ServerlessResponse.headers(response)).toHaveProperty( 'response-header', 'true', ); } else { expect(ServerlessResponse.headers(response)).not.toHaveProperty( 'response-header', 'true', ); } expect(response.statusCode).toBe(statusCode); }); } }); describe('async iterator', () => { it('should handle well async iterator', async () => { const app = new ApolloServer({ typeDefs: 'type Query { message: String }', resolvers: { Query: { message: (_, __, context: ApolloServerContextArguments) => { context.response.setHeader('response-header', 'true'); return 'ok'; }, }, }, }); const asyncContent = ['hello', 'world', '!']; // eslint-disable-next-line @typescript-eslint/require-await async function* iterator(values) { for (let i = 0; i < values.length; i++) yield values[i]; } vitest.spyOn(app, 'executeHTTPGraphQLRequest').mockImplementation(() => Promise.resolve({ status: 200, headers: new HeaderMap(), body: { kind: 'chunked' as const, asyncIterator: iterator(asyncContent), }, }), ); app.startInBackgroundHandlingStartupErrorsByLoggingAndFailingAllRequests(); const stringBody = JSON.stringify({ query: ` query Query { message } `, }); const [bufferBody, bodyLength] = stringBody ? getEventBodyAsBuffer(stringBody, false) : [undefined, 0]; const framework = new JsonBodyParserFramework( new ApolloServerFramework(), ); const request = new ServerlessRequest({ method: 'POST', url: '/', headers: { 'content-length': String(bodyLength), 'request-header': 'true', 'content-type': 'application/json', }, body: bufferBody, }); const response: ServerlessResponse & { flush?: () => void } = new ServerlessResponse({ method: 'POST', }); response.flush = vitest.fn(() => void 0); framework.sendRequest(app, request, response); await waitForStreamComplete(response); const resultBody = ServerlessResponse.body(response); expect(resultBody.toString()).toEqual(asyncContent.join('')); expect(response.flush).toHaveBeenNthCalledWith(asyncContent.length); }); }); describe('possible headers', () => { it('should handle all types of OutgoingHttpHeaders', () => { const headers: OutgoingHttpHeaders = { test: 'test', foo: ['bar', 'boe'], bar: undefined, doe: 10, }; const app = new ApolloServer({ typeDefs: 'type Query { doe: String }', resolvers: {}, }); vitest .spyOn(app, 'executeHTTPGraphQLRequest') .mockImplementation(({ httpGraphQLRequest: { headers } }) => { const objHeaders = Object.fromEntries(headers.entries()); expect(objHeaders).toHaveProperty('test', 'test'); expect(objHeaders).toHaveProperty('foo', 'bar, boe'); expect(objHeaders).toHaveProperty('doe', '10'); expect(objHeaders).not.toHaveProperty('bar'); return Promise.resolve({ status: 200, body: { kind: 'complete', string: 'ok' }, headers: new HeaderMap(), }); }); const request = new ServerlessRequest({ method: 'POST', url: '/', headers: headers as any, }); const response: ServerlessResponse = new ServerlessResponse({ method: 'POST', }); const framework = new ApolloServerFramework(); framework.sendRequest(app, request, response); }); }); }); ================================================ FILE: test/frameworks/body-parser-v2.framework.spec.ts ================================================ import { describe, vitest } from 'vitest'; import { createBodyParserTests } from './body-parser.framework.helper'; vitest.mock('body-parser', async () => { return await import('body-parser-v2'); }); describe('Body Parser v2', () => { createBodyParserTests(); }); ================================================ FILE: test/frameworks/body-parser.framework.helper.ts ================================================ import * as trpc from '@trpc/server'; import type { Options } from 'body-parser'; import express, { type Express } from 'express'; import express_v5 from 'express-v5'; import fastify, { type FastifyInstance } from 'fastify'; import fastify_v5 from 'fastify-v5'; import Application from 'koa'; import polka from 'polka'; import { type SpyInstance, describe, expect, it, vitest } from 'vitest'; import { type FrameworkContract, ServerlessRequest, ServerlessResponse, waitForStreamComplete, } from '../../src'; import { type BodyParserOptions, JsonBodyParserFramework, RawBodyParserFramework, TextBodyParserFramework, UrlencodedBodyParserFramework, } from '../../src/frameworks/body-parser'; import { ExpressFramework } from '../../src/frameworks/express'; import { FastifyFramework } from '../../src/frameworks/fastify'; import { setNoOpForContentType } from '../../src/frameworks/fastify/helpers/no-op-content-parser'; import { KoaFramework } from '../../src/frameworks/koa'; import { PolkaFramework } from '../../src/frameworks/polka'; import { type TrpcAdapterContext, TrpcFramework, } from '../../src/frameworks/trpc'; type BodyParserTest = { name: string; createFramework: ( framework: FrameworkContract, ) => FrameworkContract; body: Buffer; contentType: string; expectedBody?: any; notExpectedBody?: any; status: number; expectSendRequestOfTheFrameworkToBeCalled: boolean; skipFrameworks?: ( | 'express' | 'fastify' | 'koa' | 'hapi' | 'trpc' | 'polka' )[]; }; const bodyParserOptions: BodyParserTest[] = [ { name: 'json: default behavior', createFramework: framework => framework, body: Buffer.from(JSON.stringify({ message: 'ok' }), 'utf-8'), contentType: 'application/json', expectedBody: Buffer.from(JSON.stringify({ message: 'ok' }), 'utf-8'), status: 200, expectSendRequestOfTheFrameworkToBeCalled: true, }, { name: 'json: parse successfuly json', createFramework: framework => new JsonBodyParserFramework(framework), body: Buffer.from(JSON.stringify({ message: 'ok' }), 'utf-8'), contentType: 'application/json', expectedBody: { message: 'ok' }, status: 200, expectSendRequestOfTheFrameworkToBeCalled: true, }, { name: 'json: error on parse, limit', createFramework: framework => new JsonBodyParserFramework(framework, { limit: 4 }), body: Buffer.from(JSON.stringify({ message: 'ok' }), 'utf-8'), contentType: 'application/json', notExpectedBody: JSON.stringify({ message: 'ok' }), status: 413, expectSendRequestOfTheFrameworkToBeCalled: false, }, { name: 'json: error on parse, invalid json', createFramework: framework => new JsonBodyParserFramework(framework), body: Buffer.from('{"potato":true', 'utf-8'), contentType: 'application/json', notExpectedBody: '{"potato":true', status: 400, expectSendRequestOfTheFrameworkToBeCalled: false, }, { name: 'json: error on parse, invalid json and strict syntax', createFramework: framework => new JsonBodyParserFramework(framework, { strict: true }), body: Buffer.from('"potato":true}', 'utf-8'), contentType: 'application/json', notExpectedBody: '{"potato":true', status: 400, expectSendRequestOfTheFrameworkToBeCalled: false, }, { name: 'text: default behavior', createFramework: framework => framework, body: Buffer.from('potato=cool', 'utf-8'), contentType: 'text/plain', expectedBody: Buffer.from('potato=cool', 'utf-8'), status: 200, expectSendRequestOfTheFrameworkToBeCalled: true, }, { name: 'text: parse text', createFramework: framework => new TextBodyParserFramework(framework), body: Buffer.from('potato=cool', 'utf-8'), contentType: 'text/plain', expectedBody: 'potato=cool', status: 200, expectSendRequestOfTheFrameworkToBeCalled: true, skipFrameworks: ['trpc'], }, { name: 'text: error on size limit', createFramework: framework => new TextBodyParserFramework(framework, { limit: 4 }), body: Buffer.from('potato=cool', 'utf-8'), contentType: 'text/plain', notExpectedBody: 'potato=cool', status: 413, expectSendRequestOfTheFrameworkToBeCalled: false, }, { name: 'raw: default behavior', createFramework: framework => framework, body: Buffer.from('potato=cool', 'utf-8'), contentType: 'application/octet-stream', expectedBody: Buffer.from('potato=cool', 'utf-8'), status: 200, expectSendRequestOfTheFrameworkToBeCalled: true, }, { name: 'raw: parse raw successfuly', createFramework: framework => new RawBodyParserFramework(framework), body: Buffer.from('potato=cool', 'utf-8'), contentType: 'application/octet-stream', expectedBody: Buffer.from('potato=cool', 'utf-8'), status: 200, expectSendRequestOfTheFrameworkToBeCalled: true, }, { name: 'raw: error on size limit', createFramework: framework => new RawBodyParserFramework(framework, { limit: 4 }), body: Buffer.from('potato=cool', 'utf-8'), contentType: 'application/octet-stream', notExpectedBody: Buffer.from('potato=cool', 'utf-8'), status: 413, expectSendRequestOfTheFrameworkToBeCalled: false, }, { name: 'urlencoded: default behavior', createFramework: framework => framework, body: Buffer.from('foo=bar', 'utf-8'), contentType: 'application/x-www-form-urlencoded', expectedBody: Buffer.from('foo=bar', 'utf-8'), status: 200, expectSendRequestOfTheFrameworkToBeCalled: true, }, { name: 'urlencoded: parse urlencoded', createFramework: framework => new UrlencodedBodyParserFramework(framework), body: Buffer.from('foo=bar', 'utf-8'), contentType: 'application/x-www-form-urlencoded', expectedBody: { foo: 'bar' }, status: 200, expectSendRequestOfTheFrameworkToBeCalled: true, }, { name: 'urlencoded: parse urlencoded extended', createFramework: framework => new UrlencodedBodyParserFramework(framework, { extended: true }), body: Buffer.from('foo[bar]=test', 'utf-8'), contentType: 'application/x-www-form-urlencoded', expectedBody: { foo: { bar: 'test' } }, status: 200, expectSendRequestOfTheFrameworkToBeCalled: true, }, { name: 'urlencoded: error on max size', createFramework: framework => new UrlencodedBodyParserFramework(framework, { limit: 3 }), body: Buffer.from('foo=bar', 'utf-8'), contentType: 'application/x-www-form-urlencoded', notExpectedBody: { foo: 'bar' }, status: 413, expectSendRequestOfTheFrameworkToBeCalled: false, }, ]; function createFramework( options: BodyParserTest, instance: FrameworkContract, ): [ FrameworkContract, SpyInstance['sendRequest']>>, ] { const spy = vitest.spyOn(instance, 'sendRequest'); return [options.createFramework(instance), spy]; } function createRequest(body: Buffer, contentType: string): ServerlessRequest { return new ServerlessRequest({ method: 'POST', url: '/body', body, headers: { 'content-type': contentType, 'content-length': Buffer.byteLength(body, 'utf-8').toString(), accept: contentType, }, }); } function createResponse(method: string): ServerlessResponse { return new ServerlessResponse({ method, }); } async function handleRestExpects( app: TApp, framework: FrameworkContract, bodyParserTestOptions: BodyParserTest, ): Promise { const [bodyParserFramework, spySendRequest] = createFramework( bodyParserTestOptions, framework, ); const request = createRequest( bodyParserTestOptions.body, bodyParserTestOptions.contentType, ); const response = createResponse(request.method!); bodyParserFramework.sendRequest(app, request, response); await waitForStreamComplete(response); const returnedBody = ServerlessResponse.body(response); expect( response.statusCode, `Got status ${response.statusCode} instead of ${ bodyParserTestOptions.status }. Response Body: ${returnedBody.toString()}`, ).toEqual(bodyParserTestOptions.status); if (bodyParserTestOptions.expectSendRequestOfTheFrameworkToBeCalled) expect(spySendRequest).toHaveBeenCalled(); else expect(spySendRequest).not.toHaveBeenCalled(); } export function createBodyParserTests() { describe('BodyParserFramework', () => { describe('express', () => { for (const bodyParserTest of bodyParserOptions) { const itFn = bodyParserTest?.skipFrameworks?.includes('express') ? it.skip : it; itFn(bodyParserTest.name, async () => { const app = express(); app.post('/body', (req, res) => { if (bodyParserTest.expectedBody) expect(req.body).toEqual(bodyParserTest.expectedBody); else expect(req.body).not.toEqual(bodyParserTest.notExpectedBody); res.send('ok'); }); app.use((err, __, res, _) => { res.emit('error', err); }); await handleRestExpects(app, new ExpressFramework(), bodyParserTest); }); } }); describe('express-v5', () => { for (const bodyParserTest of bodyParserOptions) { const itFn = bodyParserTest?.skipFrameworks?.includes('express') ? it.skip : it; itFn(bodyParserTest.name, async () => { const app = express_v5(); app.post('/body', (req, res) => { if (bodyParserTest.expectedBody) expect(req.body).toEqual(bodyParserTest.expectedBody); else expect(req.body).not.toEqual(bodyParserTest.notExpectedBody); res.send('ok'); }); app.use((err, __, res, _) => { res.emit('error', err); }); await handleRestExpects(app, new ExpressFramework(), bodyParserTest); }); } }); describe('fastify', () => { for (const bodyParserTest of bodyParserOptions) { const itFn = bodyParserTest?.skipFrameworks?.includes('fastify') ? it.skip : it; itFn(bodyParserTest.name, async () => { const app = fastify(); setNoOpForContentType(app, 'application/json'); setNoOpForContentType(app, 'text/plain'); setNoOpForContentType(app, 'application/octet-stream'); setNoOpForContentType(app, 'application/x-www-form-urlencoded'); app.post('/body', (req, res) => { if (bodyParserTest.expectedBody) expect(req.body).toEqual(bodyParserTest.expectedBody); else expect(req.body).not.toEqual(bodyParserTest.notExpectedBody); res.send('ok'); }); app.setErrorHandler((err, _req, reply) => { reply.raw.emit('error', err); }); await handleRestExpects(app, new FastifyFramework(), bodyParserTest); }); } }); describe('fastify-v5', () => { for (const bodyParserTest of bodyParserOptions) { const itFn = bodyParserTest?.skipFrameworks?.includes('fastify') ? it.skip : it; itFn(bodyParserTest.name, async () => { const app = fastify_v5() as unknown as FastifyInstance; setNoOpForContentType(app, 'application/json'); setNoOpForContentType(app, 'text/plain'); setNoOpForContentType(app, 'application/octet-stream'); setNoOpForContentType(app, 'application/x-www-form-urlencoded'); app.post('/body', (req, res) => { if (bodyParserTest.expectedBody) expect(req.body).toEqual(bodyParserTest.expectedBody); else expect(req.body).not.toEqual(bodyParserTest.notExpectedBody); res.send('ok'); }); app.setErrorHandler((err, _req, reply) => { reply.raw.emit('error', err); }); await handleRestExpects(app, new FastifyFramework(), bodyParserTest); }); } }); describe('koa', () => { for (const bodyParserTest of bodyParserOptions) { const itFn = bodyParserTest?.skipFrameworks?.includes('koa') ? it.skip : it; itFn(bodyParserTest.name, async () => { const app = new Application(); app.onerror = e => { throw e; }; const next = vitest.fn(ctx => { const body = ctx.req.body; if (bodyParserTest.expectedBody) expect(body).toEqual(bodyParserTest.expectedBody); else expect(body).not.toEqual(bodyParserTest.notExpectedBody); ctx.status = 200; ctx.body = 'ok'; }); app.use(next); await handleRestExpects(app, new KoaFramework(), bodyParserTest); }); } }); describe('hapi', () => { for (const bodyParserTest of bodyParserOptions) { const itFn = bodyParserTest?.skipFrameworks?.includes('hapi') ? it.skip : it; itFn(bodyParserTest.name, async () => { const app = new Application(); app.use(ctx => { const body = (ctx.req as any).body; if (bodyParserTest.expectedBody) expect(body).toEqual(bodyParserTest.expectedBody); else expect(body).not.toEqual(bodyParserTest.notExpectedBody); ctx.status = 200; ctx.body = 'ok'; }); app.onerror = e => { throw e; }; await handleRestExpects(app, new KoaFramework(), bodyParserTest); }); } }); describe('trpc', () => { for (const bodyParserTest of bodyParserOptions) { const itFn = bodyParserTest?.skipFrameworks?.includes('trpc') ? it.skip : it; itFn(bodyParserTest.name, async () => { const t = trpc.initTRPC .context>() .create(); const app = t.router({ body: t.procedure .input(inp => inp) .mutation(ctx => { const body = (ctx.ctx.request as any).body; if (bodyParserTest.expectedBody) expect(body).toEqual(bodyParserTest.expectedBody); else expect(body).not.toEqual(bodyParserTest.notExpectedBody); return 'ok'; }), }); await handleRestExpects(app, new TrpcFramework(), bodyParserTest); }); } }); describe('polka', () => { for (const bodyParserTest of bodyParserOptions) { const itFn = bodyParserTest?.skipFrameworks?.includes('polka') ? it.skip : it; itFn(bodyParserTest.name, async () => { const app = polka(); app.post('/body', (req, res) => { if (bodyParserTest.expectedBody) expect(req.body).toEqual(bodyParserTest.expectedBody); else expect(req.body).not.toEqual(bodyParserTest.notExpectedBody); res.end('ok'); }); await handleRestExpects(app, new PolkaFramework(), bodyParserTest); }); } }); it('should handle correctly on wrong content-encoding', async () => { const app = express(); const expressFramework = new ExpressFramework(); const bodyParserFramework = new TextBodyParserFramework(expressFramework); const request = createRequest( Buffer.from('testrandomdata'), 'text/plain', ); request.headers['content-encoding'] = 'random'; const response = createResponse('POST'); bodyParserFramework.sendRequest(app, request, response); await waitForStreamComplete(response); expect(response.statusCode).toEqual(415); }); describe('customErrorHandler', () => { it('should be able to set custom error handler', async () => { const app = express(); const customOptions: Options & BodyParserOptions = { limit: 4, customErrorHandler: (__, response, _) => { response.statusCode = 400; response.end('ok'); }, }; const expressFramework = new ExpressFramework(); const bodyParserFrameworks: [ framework: FrameworkContract, contentType: string, ][] = [ [ new TextBodyParserFramework(expressFramework, customOptions), 'text/plain', ], [ new JsonBodyParserFramework(expressFramework, customOptions), 'application/json', ], [ new UrlencodedBodyParserFramework(expressFramework, customOptions), 'application/x-www-form-urlencoded', ], ]; for (const [bodyParserFramework, contentType] of bodyParserFrameworks) { const request = createRequest( Buffer.from('testrandomdata'), contentType, ); const response = createResponse('POST'); bodyParserFramework.sendRequest(app, request, response); await waitForStreamComplete(response); const result = ServerlessResponse.body(response); expect(result).toEqual(Buffer.from('ok')); expect(response.statusCode).toEqual(400); } }); }); }); } ================================================ FILE: test/frameworks/body-parser.framework.spec.ts ================================================ import { describe } from 'vitest'; import { createBodyParserTests } from './body-parser.framework.helper'; describe('Body Parser v1', () => { createBodyParserTests(); }); ================================================ FILE: test/frameworks/cors.framework.spec.ts ================================================ import * as trpc from '@trpc/server'; import express from 'express'; import fastify from 'fastify'; import Application from 'koa'; import { type SpyInstance, describe, expect, it, vitest } from 'vitest'; import polka from 'polka'; import { type BothValueHeaders, type FrameworkContract, ServerlessRequest, ServerlessResponse, waitForStreamComplete, } from '../../src'; import { CorsFramework, type CorsFrameworkOptions, } from '../../src/frameworks/cors'; import { ExpressFramework } from '../../src/frameworks/express'; import { FastifyFramework } from '../../src/frameworks/fastify'; import { KoaFramework } from '../../src/frameworks/koa'; import { TrpcFramework } from '../../src/frameworks/trpc'; import { PolkaFramework } from '../../src/frameworks/polka'; type CorsTest = { name: string; method: string; origin: string; options: CorsFrameworkOptions; expectedHeaders: BothValueHeaders; expectSendRequestOfTheFrameworkToBeCalled: boolean; }; const AllowOrigin = 'access-control-allow-origin'; const AllowCredentials = 'access-control-allow-credentials'; const AllowMethods = 'access-control-allow-methods'; const MaxAge = 'access-control-max-age'; const AllowHeaders = 'access-control-allow-headers'; const corsOptions: CorsTest[] = [ { name: 'allow all origins', method: 'get', origin: 'http://localhost:3000', options: { origin: '*' }, expectedHeaders: { [AllowOrigin]: '*' }, expectSendRequestOfTheFrameworkToBeCalled: true, }, { name: 'allow localhost origin (string)', method: 'get', origin: 'http://localhost:3000', options: { origin: 'http://localhost:3000' }, expectedHeaders: { [AllowOrigin]: 'http://localhost:3000', vary: 'Origin' }, expectSendRequestOfTheFrameworkToBeCalled: true, }, { name: 'allow localhost origin (array)', method: 'get', origin: 'http://localhost:3000', options: { origin: ['http://localhost:3000', 'http://google.com'] }, expectedHeaders: { [AllowOrigin]: 'http://localhost:3000', vary: 'Origin' }, expectSendRequestOfTheFrameworkToBeCalled: true, }, { name: 'do not send request on options', method: 'options', origin: 'http://localhost:3000', options: { origin: '*' }, expectedHeaders: { [AllowOrigin]: '*', [AllowMethods]: 'GET,HEAD,PUT,PATCH,POST,DELETE', vary: 'Access-Control-Request-Headers', }, expectSendRequestOfTheFrameworkToBeCalled: false, }, { name: 'do not send request when origin sent is wrong (string)', method: 'get', origin: 'http://localhost:3000', options: { origin: 'http://example.com:3000' }, expectedHeaders: { [AllowOrigin]: 'http://example.com:3000' }, expectSendRequestOfTheFrameworkToBeCalled: false, }, { name: 'do not send request when origin sent is wrong (array)', method: 'get', origin: 'http://localhost:3000', options: { origin: ['http://example.com:3000', 'http://google.com'] }, expectedHeaders: { vary: 'Origin', }, expectSendRequestOfTheFrameworkToBeCalled: false, }, { name: 'do not send request when method sent is wrong (string)', method: 'get', origin: 'http://localhost:3000', options: { origin: '*', methods: 'post' }, expectedHeaders: { [AllowOrigin]: '*' }, expectSendRequestOfTheFrameworkToBeCalled: false, }, { name: 'do not send request when method sent is wrong (array)', method: 'get', origin: 'http://localhost:3000', options: { origin: '*', methods: ['post'] }, expectedHeaders: { [AllowOrigin]: '*' }, expectSendRequestOfTheFrameworkToBeCalled: false, }, { name: 'force process request when origin is wrong', method: 'get', origin: 'http://localhost:3000', options: { origin: 'http://example.com', methods: ['post'], forbiddenOnInvalidOriginOrMethod: false, }, expectedHeaders: { [AllowOrigin]: 'http://example.com' }, expectSendRequestOfTheFrameworkToBeCalled: true, }, { name: 'when has credentials', method: 'options', origin: 'http://localhost:3000', options: { credentials: true }, expectedHeaders: { [AllowCredentials]: 'true', }, expectSendRequestOfTheFrameworkToBeCalled: false, }, { name: 'when preflight continue is true', method: 'options', origin: 'http://localhost:3000', options: { origin: '*', preflightContinue: true }, expectedHeaders: { [AllowOrigin]: '*', }, expectSendRequestOfTheFrameworkToBeCalled: true, }, { name: 'when allowed headers is sent', method: 'options', origin: 'http://localhost:3000', options: { allowedHeaders: ['x-test'] }, expectedHeaders: { [AllowHeaders]: 'x-test', }, expectSendRequestOfTheFrameworkToBeCalled: false, }, { name: 'when max-age is set', method: 'options', origin: 'http://localhost:3000', options: { maxAge: 60 }, expectedHeaders: { [MaxAge]: '60', }, expectSendRequestOfTheFrameworkToBeCalled: false, }, ]; function createFramework( options: CorsFrameworkOptions, instance: FrameworkContract, ): [ FrameworkContract, SpyInstance['sendRequest']>>, ] { const spy = vitest.spyOn(instance, 'sendRequest'); return [new CorsFramework(instance, options), spy]; } function createRequest(method: string, origin: string): ServerlessRequest { return new ServerlessRequest({ method, url: '/', headers: { origin: origin, }, }); } function createResponse(method: string): ServerlessResponse { return new ServerlessResponse({ method, }); } async function handleRestExpects( app: TApp, framework: FrameworkContract, corsTest: CorsTest, ): Promise { const [corsFramework, spySendRequest] = createFramework( corsTest.options, framework, ); const request = createRequest(corsTest.method, corsTest.origin); const response = createResponse(corsTest.method); corsFramework.sendRequest(app, request, response); await waitForStreamComplete(response); const headers = response.getHeaders(); for (const expectHeader in corsTest.expectedHeaders) { expect(headers).toHaveProperty( expectHeader, corsTest.expectedHeaders[expectHeader], ); } if (corsTest.expectSendRequestOfTheFrameworkToBeCalled) expect(spySendRequest).toHaveBeenCalled(); else expect(spySendRequest).not.toHaveBeenCalled(); } describe('CorsFramework', () => { describe('express', () => { for (const corsTest of corsOptions) { it(`${corsTest.method}: ${corsTest.name}`, async () => { const app = express(); app.get('/', (_, res) => res.json('ok')); await handleRestExpects(app, new ExpressFramework(), corsTest); }); } }); describe('fastify', () => { for (const corsTest of corsOptions) { it(`${corsTest.method}: ${corsTest.name}`, async () => { const app = fastify(); app.get('/', (_, res) => { res.send('ok'); }); await handleRestExpects(app, new FastifyFramework(), corsTest); }); } }); describe('koa', () => { for (const corsTest of corsOptions) { it(`${corsTest.method}: ${corsTest.name}`, async () => { const app = new Application(); app.use(ctx => { ctx.status = 200; ctx.body = 'ok'; }); await handleRestExpects(app, new KoaFramework(), corsTest); }); } }); describe('hapi', () => { for (const corsTest of corsOptions) { it(`${corsTest.method}: ${corsTest.name}`, async () => { const app = new Application(); app.use(ctx => { ctx.status = 200; ctx.body = 'ok'; }); await handleRestExpects(app, new KoaFramework(), corsTest); }); } }); describe('trpc', () => { for (const corsTest of corsOptions) { it(`${corsTest.method}: ${corsTest.name}`, async () => { const t = trpc.initTRPC.create(); const app = t.router({ ['/']: t.procedure.query(() => { return 'ok'; }), }); await handleRestExpects(app, new TrpcFramework(), corsTest); }); } }); describe('polka', () => { for (const corsTest of corsOptions) { it(`${corsTest.method}: ${corsTest.name}`, async () => { const app = polka(); app.get('/', (_, res) => res.end('ok')); await handleRestExpects(app, new PolkaFramework(), corsTest); }); } }); }); ================================================ FILE: test/frameworks/express-v5.framework.spec.ts ================================================ import express from 'express-v5'; import { describe } from 'vitest'; import { ExpressFramework } from '../../src/frameworks/express'; import { type TestRouteBuilderHandler, createTestSuiteFor } from './utils'; function createHandler( method: 'get' | 'post' | 'delete' | 'put', ): TestRouteBuilderHandler { return (app, path, handler) => { app[method](path, (request, response) => { const [statusCode, resultBody, headers] = handler( request.headers, request.body, ); for (const header of Object.keys(headers)) response.header(header, headers[header]); response.status(statusCode).json(resultBody); }); }; } describe(ExpressFramework.name, () => { createTestSuiteFor( () => new ExpressFramework(), () => express(), { get: createHandler('get'), delete: createHandler('delete'), post: createHandler('post'), put: createHandler('put'), }, ); }); ================================================ FILE: test/frameworks/express.framework.spec.ts ================================================ import express, { type Express } from 'express'; import { describe } from 'vitest'; import { ExpressFramework } from '../../src/frameworks/express'; import { type TestRouteBuilderHandler, createTestSuiteFor } from './utils'; function createHandler( method: 'get' | 'post' | 'delete' | 'put', ): TestRouteBuilderHandler { return (app, path, handler) => { app[method](path, (request, response) => { const [statusCode, resultBody, headers] = handler( request.headers, request.body, ); for (const header of Object.keys(headers)) response.header(header, headers[header]); response.status(statusCode).json(resultBody); }); }; } describe(ExpressFramework.name, () => { createTestSuiteFor( () => new ExpressFramework(), () => express(), { get: createHandler('get'), delete: createHandler('delete'), post: createHandler('post'), put: createHandler('put'), }, ); }); ================================================ FILE: test/frameworks/fastify-v5.framework.spec.ts ================================================ import fastify, { type FastifyInstance } from 'fastify-v5'; import { describe } from 'vitest'; import { FastifyFramework } from '../../src/frameworks/fastify'; import { type TestRouteBuilderHandler, createTestSuiteFor } from './utils'; function createHandler( method: 'get' | 'post' | 'delete' | 'put', ): TestRouteBuilderHandler { return (app, path, handler) => { app[method](path, {}, (request, response) => { const [statusCode, resultBody, headers] = handler( request.headers, request.body, ); response.headers(headers).code(statusCode).send(resultBody); }); }; } describe(FastifyFramework.name, () => { createTestSuiteFor( () => new FastifyFramework(), () => fastify(), { get: createHandler('get'), delete: createHandler('delete'), post: createHandler('post'), put: createHandler('put'), }, ); }); ================================================ FILE: test/frameworks/fastify.framework.spec.ts ================================================ import fastify, { type FastifyInstance } from 'fastify'; import { describe } from 'vitest'; import { FastifyFramework } from '../../src/frameworks/fastify'; import { type TestRouteBuilderHandler, createTestSuiteFor } from './utils'; function createHandler( method: 'get' | 'post' | 'delete' | 'put', ): TestRouteBuilderHandler { return (app, path, handler) => { app[method](path, {}, (request, response) => { const [statusCode, resultBody, headers] = handler( request.headers, request.body, ); response.headers(headers).code(statusCode).send(resultBody); }); }; } describe(FastifyFramework.name, () => { createTestSuiteFor( () => new FastifyFramework(), () => fastify(), { get: createHandler('get'), delete: createHandler('delete'), post: createHandler('post'), put: createHandler('put'), }, ); }); ================================================ FILE: test/frameworks/hapi.framework.spec.ts ================================================ import { Server } from '@hapi/hapi'; import { describe } from 'vitest'; import { HapiFramework } from '../../src/frameworks/hapi'; import { type TestRouteBuilderHandler, createTestSuiteFor } from './utils'; function createHandler( method: 'GET' | 'POST' | 'DELETE' | 'PUT', ): TestRouteBuilderHandler { return (app, path, handler) => { app.route({ method, path, handler: (request, h) => { const [statusCode, resultBody, headers] = handler( request.headers, request.payload, ); const response = h.response(resultBody); for (const header of Object.keys(headers)) response?.header(header, headers[header]); response.code(statusCode); return response; }, }); }; } describe(HapiFramework.name, () => { createTestSuiteFor( () => new HapiFramework(), () => new Server(), { get: createHandler('GET'), delete: createHandler('DELETE'), post: createHandler('POST'), put: createHandler('PUT'), }, ); }); ================================================ FILE: test/frameworks/http-deepkit.framework.spec.ts ================================================ import { App } from '@deepkit/app'; import { HttpKernel, HttpModule, HttpRouterRegistry, JSONResponse, } from '@deepkit/http'; import { describe, expect, it, vitest } from 'vitest'; import { ServerlessRequest, ServerlessResponse, waitForStreamComplete, } from '../../src'; import { HttpDeepkitFramework } from '../../src/frameworks/deepkit'; it('should convert correctly when the value is not an buffer', async () => { const framework = new HttpDeepkitFramework(); const kernel: Partial = { handleRequest: vitest.fn((request, response) => { request.pipe(response); return void 0 as any; }), }; const textCodes = 'test'.split('').map(c => c.charCodeAt(0)); const request = new ServerlessRequest({ body: Uint8Array.of(...textCodes), url: '/test', method: 'POST', headers: {}, }); const response = new ServerlessResponse({ method: 'POST', }); framework.sendRequest(kernel as HttpKernel, request, response); await waitForStreamComplete(response); const resultBody = ServerlessResponse.body(response); expect(resultBody).toBeInstanceOf(Buffer); expect(resultBody.toString()).toEqual('test'); }); describe('deepkit', () => { it('should return valid json on get request', async () => { const app = new App({ imports: [new HttpModule()], }); const body = { test: 'ok' }; app.get(HttpRouterRegistry).get('/', () => { return new JSONResponse(body, 200).header('response-header', 'true'); }); const request = new ServerlessRequest({ method: 'GET', url: '/', headers: {}, }); const response = new ServerlessResponse({ method: 'GET', }); const framework = new HttpDeepkitFramework(); const httpKernel = app.get(HttpKernel); framework.sendRequest(httpKernel, request, response); await waitForStreamComplete(response); const resultBody = ServerlessResponse.body(response); expect(resultBody.toString('utf-8')).toEqual(JSON.stringify(body)); expect(response.statusCode).toBe(200); expect(ServerlessResponse.headers(response)).toHaveProperty( 'response-header', 'true', ); }); }); ================================================ FILE: test/frameworks/koa.framework.spec.ts ================================================ import Application, { type Context } from 'koa'; import { describe } from 'vitest'; import { NO_OP } from '../../src'; import { KoaFramework } from '../../src/frameworks/koa'; import { type TestRouteBuilderHandler, createTestSuiteFor } from './utils'; function createHandler(): TestRouteBuilderHandler { return (app, _, handler) => { app.use((ctx: Context) => { const [statusCode, resultBody, headers] = handler(ctx.headers, NO_OP); for (const header of Object.keys(headers)) ctx.set(header, headers[header]); ctx.status = statusCode; ctx.body = resultBody; }); }; } describe(KoaFramework.name, () => { createTestSuiteFor( () => new KoaFramework(), () => new Application(), { get: createHandler(), delete: createHandler(), post: createHandler(), put: createHandler(), }, ); }); ================================================ FILE: test/frameworks/lazy.framework.spec.ts ================================================ import { describe, expect, it, vitest } from 'vitest'; import { type ILogger, ServerlessRequest, ServerlessResponse, waitForStreamComplete, } from '../../src'; import { LazyFramework } from '../../src/frameworks/lazy'; import { FrameworkMock } from '../mocks/framework.mock'; describe(LazyFramework.name, () => { it('should can lazy create an instance of any app and return the cached version', async () => { const appInstance = Symbol('Your app'); const mockFramework = new FrameworkMock(200, { data: true, }); // eslint-disable-next-line @typescript-eslint/unbound-method mockFramework.sendRequest = vitest.fn(mockFramework.sendRequest); const factory = vitest.fn( () => new Promise(resolve => setTimeout(() => resolve(appInstance), 100)), ); const framework = new LazyFramework(mockFramework, factory); const firstRequest = new ServerlessRequest({ method: 'POST', headers: {}, url: '/users', }); const firstResponse = new ServerlessResponse({ method: 'POST', }); framework.sendRequest(null, firstRequest, firstResponse); await waitForStreamComplete(firstResponse); expect(framework['factory']).toHaveBeenCalledTimes(1); // eslint-disable-next-line @typescript-eslint/unbound-method expect(mockFramework.sendRequest).toHaveBeenLastCalledWith( appInstance, firstRequest, firstResponse, ); const secondRequest = new ServerlessRequest({ method: 'GET', headers: {}, url: '/users', }); const secondResponse = new ServerlessResponse({ method: 'GET', }); framework.sendRequest(null, secondRequest, secondResponse); await waitForStreamComplete(secondResponse); expect(factory).toHaveBeenCalledTimes(1); // eslint-disable-next-line @typescript-eslint/unbound-method expect(mockFramework.sendRequest).toHaveBeenCalledTimes(2); // eslint-disable-next-line @typescript-eslint/unbound-method expect(mockFramework.sendRequest).toHaveBeenLastCalledWith( appInstance, secondRequest, secondResponse, ); }); it('should throw error if error occours in factory function', async () => { const mockFramework = new FrameworkMock(200, { data: true, }); const mockLogger = { error: vitest.fn() } as unknown as ILogger; const error = new Error('Something Wrong Occours'); const framework = new LazyFramework( mockFramework, () => Promise.reject(error), mockLogger, ); const request = new ServerlessRequest({ method: 'GET', headers: {}, url: '/users', }); const response = new ServerlessResponse({ method: 'GET', }); framework.sendRequest(null, request, response); await expect( async () => await waitForStreamComplete(response), ).rejects.toThrowError('factory is not valid,'); }); }); ================================================ FILE: test/frameworks/polka.framework.spec.ts ================================================ import { describe } from 'vitest'; import polka, { type Polka } from 'polka'; import { PolkaFramework } from '../../src/frameworks/polka'; import { type TestRouteBuilderHandler, createTestSuiteFor } from './utils'; function createHandler( method: 'get' | 'post' | 'delete' | 'put', ): TestRouteBuilderHandler { return (app, path, handler) => { app[method](path, (request, response) => { const [statusCode, resultBody, headers] = handler( request.headers, request.body, ); for (const header of Object.keys(headers)) response.setHeader(header, headers[header]); response.statusCode = statusCode; response.end(JSON.stringify(resultBody)); }); }; } describe(PolkaFramework.name, () => { createTestSuiteFor( () => new PolkaFramework(), () => polka(), { get: createHandler('get'), delete: createHandler('delete'), post: createHandler('post'), put: createHandler('put'), }, ); }); ================================================ FILE: test/frameworks/trpc.framework.spec.ts ================================================ import * as trpc from '@trpc/server'; import { type AnyRouter } from '@trpc/server'; import { describe, expect, it } from 'vitest'; import { NO_OP, ServerlessRequest, ServerlessResponse, getEventBodyAsBuffer, waitForStreamComplete, } from '../../src'; import { BufferToJSObjectTransformer, type TrpcAdapterContext, TrpcFramework, } from '../../src/frameworks/trpc'; import { type TestRouteBuilderHandler, frameworkTestOptions } from './utils'; type TrpcContext = TrpcAdapterContext; function createHandler( method: 'get' | 'post' | 'delete' | 'put', ): TestRouteBuilderHandler, AnyRouter> { return (app, path, handler) => { if (method === 'get') { return app.router({ [path]: app.procedure.query(({ ctx, input }) => { const [statusCode, resultBody, headers] = handler( ctx.getHeaders(), input, ); for (const header of Object.keys(headers)) ctx.setHeader(header, headers[header]); ctx.setStatus(statusCode); return resultBody; }), }); } else { return app.router({ [path]: app.procedure .input(inp => inp) .mutation(({ ctx, input }) => { const [statusCode, resultBody, headers] = handler( ctx.getHeaders(), input, ); for (const header of Object.keys(headers)) ctx.setHeader(header, headers[header]); ctx.setStatus(statusCode); return resultBody; }), }); } }; } function createRouter() { return trpc.initTRPC.context().create(); } const validTestOptions = frameworkTestOptions.filter( ([method]) => method === 'post' || method === 'get', ); describe('TrpcFramework', () => { for (const [ method, path, statusCode, body, expectedValue, ] of validTestOptions) { const formattedPath = path.replace('/', '').replace(/\//g, '.'); const requestUrl = '/' + formattedPath; it(`${method}:${formattedPath}: should forward request and receive response correctly`, async () => { const app = createRouter(); const handler = createHandler(method); const resolvedApp = handler( app, formattedPath, (requestHeaders, requestBody) => { expect(requestHeaders).toHaveProperty('request-header', 'true'); if ( (method === 'post' || method === 'put') && requestBody !== NO_OP ) { const parsedRequestBody = requestBody instanceof Buffer ? JSON.parse(requestBody.toString('utf-8')) : requestBody; expect(parsedRequestBody).toEqual(body); } return [statusCode, body, { 'response-header': 'true' }]; }, ); const framework = new TrpcFramework>(); const stringBody = body ? JSON.stringify(body) : body; const [bufferBody, bodyLength] = stringBody ? getEventBodyAsBuffer(stringBody, false) : [undefined, 0]; const request = new ServerlessRequest({ method: method.toUpperCase(), url: method !== 'get' ? requestUrl : requestUrl + `?input=${encodeURIComponent(JSON.stringify(body))}`, headers: { 'content-length': String(bodyLength), 'request-header': 'true', ...(body && { 'content-type': 'application/json', }), }, body: bufferBody, remoteAddress: '1.1.1.1', }); const response = new ServerlessResponse({ method: method.toUpperCase(), }); framework.sendRequest(resolvedApp, request, response); await waitForStreamComplete(response); const resultBody = ServerlessResponse.body(response); expect( expectedValue !== undefined ? expectedValue : JSON.stringify({ result: { data: body } }), ).toEqual(resultBody.toString('utf-8')); expect(response.statusCode).toBe(statusCode); expect(ServerlessResponse.headers(response)).toHaveProperty( 'response-header', 'true', ); }); } it('should enable create custom contexts', async () => { type Context = { currentDate: Date }; type CustomContext = TrpcAdapterContext; const currentDate = new Date(); const t = trpc.initTRPC.context().create(); const app = t.router({ test: t.procedure.query(function ({ ctx }) { expect(ctx).toHaveProperty('currentDate'); ctx.setStatus(201); }), }); const request = new ServerlessRequest({ method: 'GET', url: '/test', headers: { test: 'header', }, body: undefined, remoteAddress: '1.1.1.1', }); const firstResponse = new ServerlessResponse({ method: 'get', }); const secondResponse = new ServerlessResponse({ method: 'get', }); const firstFramework = new TrpcFramework>({ createContext: async () => Promise.resolve({ currentDate }), }); const secondFramework = new TrpcFramework>({ createContext: () => ({ currentDate }), }); firstFramework.sendRequest(app, request, firstResponse); secondFramework.sendRequest(app, request, secondResponse); await waitForStreamComplete(firstResponse); await waitForStreamComplete(secondResponse); const firstResultBody = ServerlessResponse.body(firstResponse); const secondResultBody = ServerlessResponse.body(secondResponse); const emptyResponse = JSON.stringify({ result: {}, }); expect(firstResultBody.toString('utf-8')).toEqual(emptyResponse); expect(secondResultBody.toString('utf-8')).toEqual(emptyResponse); }); it('should correctly send default methods inside context', async () => { const t = createRouter(); const app = t.router({ test: t.procedure.query(({ ctx }) => { expect(ctx.request).toBeDefined(); expect(ctx.response).toBeDefined(); expect(ctx.getMethod()).toEqual('GET'); expect(ctx.getUrl()).toEqual('/test'); expect(ctx.getIp()).toEqual('1.1.1.1'); expect(ctx.getHeaders()).toHaveProperty('test', 'header'); expect(ctx.getHeader('test')).toEqual('header'); expect(ctx.getMethod()).toEqual('GET'); ctx.setStatus(204); ctx.setHeader('test2', 'batata'); ctx.removeHeader('test2'); }), }); const framework = new TrpcFramework>(); const request = new ServerlessRequest({ method: 'GET', url: '/test', headers: { test: 'header', }, body: undefined, remoteAddress: '1.1.1.1', }); const response = new ServerlessResponse({ method: 'get', }); framework.sendRequest(app, request, response); await waitForStreamComplete(response); expect(response.statusCode).toEqual(204); expect(response.headers).not.toHaveProperty('test2'); }); }); describe(BufferToJSObjectTransformer.name, () => { it('should correctly parse json when came from buffer', () => { const jsonObject = { batata: true }; const testJson = JSON.stringify(jsonObject); const transformer = new BufferToJSObjectTransformer(); const buffer = Buffer.from(testJson, 'utf-8'); expect(transformer.deserialize(buffer)).toEqual(jsonObject); }); it('should dont deserialize the value when is not an buffer', () => { const values = [Symbol('do nothing'), 'test', 434]; const transformer = new BufferToJSObjectTransformer(); for (const value of values) expect(transformer.deserialize(value)).toEqual(value); }); it('should dont modify the value when serialize', () => { const symbol = Symbol('do nothing'); const transformer = new BufferToJSObjectTransformer(); expect(transformer.serialize(symbol)).toEqual(symbol); }); it('should throw error when buffer is not an JSON', () => { const xml = 'true'; const transformer = new BufferToJSObjectTransformer(); const buffer = Buffer.from(xml, 'utf-8'); expect(() => transformer.deserialize(buffer)).toThrow(); }); }); ================================================ FILE: test/frameworks/utils.ts ================================================ import { expect, it } from 'vitest'; import { type FrameworkContract, NO_OP, ServerlessRequest, ServerlessResponse, getEventBodyAsBuffer, waitForStreamComplete, } from '../../src'; export type TestRouteBuilderHandler = ( app: TApp, path: string, handler: ( headers: any, body: any, ) => [statusCode: number, body: any, headers: any], ) => TOutput; export type TestRouteBuilderMethods = 'get' | 'post' | 'delete' | 'put'; export type TestRouteBuilder = Record< TestRouteBuilderMethods, TestRouteBuilderHandler >; export type TestOptions = [ method: TestRouteBuilderMethods, path: string, statusCode: number, body: any, expectedValue?: string, ]; export const frameworkTestOptions: TestOptions[] = [ ['get', '/', 200, [{ name: 'Joga10' }]], ['get', '/', 200, [{ name: 'Joga10' }]], ['get', '/users', 200, [{ name: 'Joga10' }]], ['get', '/users/list', 200, []], ['get', '/users/1', 404, { didntFind: 'entity' }], ['get', '/users/2', 404, { notFound: true }], ['post', '/users/error', 401, { unathorized: true }], ['post', '/users', 201, { success: true }], ['put', '/users/1', 201, { updated: true }], ['put', '/users/2', 404, { notFound: true }], ['put', '/users/3', 404, { didntFind: 'entity' }], ['delete', '/users/1', 200, { deleted: true }], ['delete', '/users/2', 401, { unathorized: true }], ['get', '/bad-gateway', 503, { error: true }], ]; export function createTestSuiteFor( frameworkFactory: () => FrameworkContract, appFactory: () => TApp | Promise, routeBuilder: TestRouteBuilder, appToFrameworkApp?: (TApp) => TFrameworkApp, tearDown?: (app: TApp) => Promise, skip?: boolean, testOptions: TestOptions[] = frameworkTestOptions, ): void { for (const [method, path, statusCode, body, expectedValue] of testOptions) { const itFn = skip ? it.skip : it; itFn( `${method}${path}: should forward request and receive response correctly`, async () => { const app = await appFactory(); routeBuilder[method]( app, path || '/', (requestHeaders, requestBody) => { expect(requestHeaders).toHaveProperty('request-header', 'true'); if ( (method === 'post' || method === 'put') && requestBody !== NO_OP ) { const parsedRequestBody = requestBody instanceof Buffer ? JSON.parse(requestBody.toString('utf-8')) : requestBody; expect(parsedRequestBody || null).toEqual(body || null); } return [statusCode, body, { 'response-header': 'true' }]; }, ); const stringBody = body ? JSON.stringify(body) : body; const [bufferBody, bodyLength] = stringBody ? getEventBodyAsBuffer(stringBody, false) : [undefined, 0]; const framework = frameworkFactory(); const request = new ServerlessRequest({ method: method.toUpperCase(), url: path, headers: { 'content-length': String(bodyLength), 'request-header': 'true', ...(body && { 'content-type': 'application/json', }), }, body: bufferBody, }); const response = new ServerlessResponse({ method: method.toUpperCase(), }); const frameworkApp = appToFrameworkApp ? appToFrameworkApp(app) : (app as unknown as TFrameworkApp); framework.sendRequest(frameworkApp, request, response); await waitForStreamComplete(response); if (tearDown) await tearDown(app); const resultBody = ServerlessResponse.body(response); expect(resultBody.toString('utf-8')).toEqual( expectedValue !== undefined ? expectedValue : JSON.stringify(body), ); expect(response.statusCode).toBe(statusCode); expect(ServerlessResponse.headers(response)).toHaveProperty( 'response-header', 'true', ); }, ); } } ================================================ FILE: test/handlers/aws-stream.handler.spec.ts ================================================ import { createReadStream, readFileSync } from 'fs'; import { join } from 'path'; import express from 'express'; import { WritableMock } from 'stream-mock/lib/writable'; import { afterEach, beforeEach, describe, expect, it, vitest } from 'vitest'; import { type ILogger, getCurrentInvoke } from '../../src'; import { ApiGatewayV2Adapter } from '../../src/adapters/aws'; import { ExpressFramework } from '../../src/frameworks/express'; import { AwsStreamHandler } from '../../src/handlers/aws'; import { DummyResolver } from '../../src/resolvers/dummy'; import { createApiGatewayV2 } from '../adapters/aws/utils/api-gateway-v2'; describe('AwsStreamHandler', () => { const awsStreamHandler = new AwsStreamHandler(); const apiGatewayAdapter = new ApiGatewayV2Adapter(); const adapters = [apiGatewayAdapter]; const resolver = new DummyResolver(); const binarySettings = { contentEncodings: [], contentTypes: [] }; const respondWithErrors = true; const logger: ILogger = { debug: vitest.fn((m, callbackOrString) => { expect(typeof m === 'string').toBeTruthy(); const content = typeof callbackOrString === 'function' ? callbackOrString() : callbackOrString || 'no-second-arg'; expect(content).toBeTruthy(); }), error: vitest.fn(), verbose: vitest.fn(), info: vitest.fn(), warn: vitest.fn(), }; beforeEach(() => { (global as any).awslambda = { streamifyResponse: vitest.fn(fn => fn), HttpResponseStream: { from: vitest.fn(r => r) }, }; }); afterEach(() => { (global as any).awslambda = undefined; }); it('should return the correct bytes of chunked stream', async () => { const app = express(); const file = readFileSync(join(__dirname, 'bitcoin.pdf')); app.get('/', (_, res) => { const readable = createReadStream(join(__dirname, 'bitcoin.pdf')); res.statusCode = 200; res.setHeader('content-type', 'application/pdf'); readable.pipe(res); }); const expressFramework = new ExpressFramework(); const handler = awsStreamHandler.getHandler( app, expressFramework, adapters, resolver, binarySettings, respondWithErrors, logger, ); const event = createApiGatewayV2('GET', '/', {}, { test: 'true' }); const context = { test: Symbol('unique') }; const writable = new WritableMock(); await handler(event, writable, context); expect(getCurrentInvoke()).toHaveProperty('event', event); expect(getCurrentInvoke()).toHaveProperty('context', context); const finalBuffer = Buffer.concat(writable.data); expect(Buffer.byteLength(finalBuffer)).toBe(Buffer.byteLength(file)); }); it('should return the correct bytes of chunked stream with eagerly flushed headers', async () => { const app = express(); const file = readFileSync(join(__dirname, 'bitcoin.pdf')); app.get('/', (_, res) => { const readable = createReadStream(join(__dirname, 'bitcoin.pdf')); res.statusCode = 200; res.setHeader('content-type', 'application/pdf'); res.flushHeaders(); readable.pipe(res); }); const expressFramework = new ExpressFramework(); const handler = awsStreamHandler.getHandler( app, expressFramework, adapters, resolver, binarySettings, respondWithErrors, logger, ); const event = createApiGatewayV2('GET', '/', {}, { test: 'true' }); const context = { test: Symbol('unique') }; const writable = new WritableMock(); await handler(event, writable, context); expect(getCurrentInvoke()).toHaveProperty('event', event); expect(getCurrentInvoke()).toHaveProperty('context', context); const finalBuffer = Buffer.concat(writable.data); expect(Buffer.byteLength(finalBuffer)).toBe(Buffer.byteLength(file)); }); it('should return the correct bytes of json', async () => { const app = express(); app.get('/', (_, res) => { return res.json({ test: 'true' }); }); const expressFramework = new ExpressFramework(); const handler = awsStreamHandler.getHandler( app, expressFramework, adapters, resolver, binarySettings, respondWithErrors, logger, ); const event = createApiGatewayV2('GET', '/', {}, { test: 'true' }); const context = { test: Symbol('unique') }; const writable = new WritableMock(); await handler(event, writable, context); expect(getCurrentInvoke()).toHaveProperty('event', event); expect(getCurrentInvoke()).toHaveProperty('context', context); const finalBuffer = Buffer.concat(writable.data); expect(finalBuffer.toString()).toBe(JSON.stringify({ test: 'true' })); }); it('should handle redirect with status 304', async () => { const app = express(); app.get('/', (_, res) => { return res.redirect(304, '/test'); }); const expressFramework = new ExpressFramework(); const handler = awsStreamHandler.getHandler( app, expressFramework, adapters, resolver, binarySettings, respondWithErrors, logger, ); const event = createApiGatewayV2('GET', '/', undefined); const context = { test: Symbol('unique') }; const writable = new WritableMock(); const write = vitest.spyOn(writable, 'write'); await handler(event, writable, context); expect(getCurrentInvoke()).toHaveProperty('event', event); expect(getCurrentInvoke()).toHaveProperty('context', context); expect(write).toHaveBeenCalledWith(''); const finalBuffer = Buffer.concat(writable.data); expect(finalBuffer.toString()).toBe(''); }); for (const statusCode of [300, 301, 302, 303, 305, 306, 307, 308]) { it(`should handle redirect with status ${statusCode}`, async () => { const app = express(); app.get('/', (_, res) => { return res.redirect(statusCode, '/test'); }); const expressFramework = new ExpressFramework(); const handler = awsStreamHandler.getHandler( app, expressFramework, adapters, resolver, binarySettings, respondWithErrors, logger, ); const event = createApiGatewayV2('GET', '/', undefined); const context = { test: Symbol('unique') }; const writable = new WritableMock(); const write = vitest.spyOn(writable, 'write'); await handler(event, writable, context); expect(getCurrentInvoke()).toHaveProperty('event', event); expect(getCurrentInvoke()).toHaveProperty('context', context); expect(write).toHaveBeenCalled(); const finalBuffer = Buffer.concat(writable.data); expect(finalBuffer.toString()).toContain('Redirecting to /test'); }); } for (const statusCode of [200, 201, 202, 203, 204, 400, 401, 403, 404]) { it(`should handle no content with status ${statusCode}`, async () => { const app = express(); app.get('/', (_, res) => { return res.status(statusCode).end(); }); const expressFramework = new ExpressFramework(); const handler = awsStreamHandler.getHandler( app, expressFramework, adapters, resolver, binarySettings, respondWithErrors, logger, ); const event = createApiGatewayV2('GET', '/', undefined); const context = { test: Symbol('unique') }; const writable = new WritableMock(); const write = vitest.spyOn(writable, 'write'); await handler(event, writable, context); expect(getCurrentInvoke()).toHaveProperty('event', event); expect(getCurrentInvoke()).toHaveProperty('context', context); expect(write).toHaveBeenCalledWith(''); const finalBuffer = Buffer.concat(writable.data); expect(finalBuffer.toString()).toBe(''); }); } for (const statusCode of [200, 201, 202, 203, 204, 400, 401, 403, 404]) { it(`should handle writeHead with no content and status ${statusCode}`, async () => { const app = express(); app.get('/', (_, res) => { return res.writeHead(statusCode).end(); }); const expressFramework = new ExpressFramework(); const handler = awsStreamHandler.getHandler( app, expressFramework, adapters, resolver, binarySettings, respondWithErrors, logger, ); const event = createApiGatewayV2('GET', '/', undefined); const context = { test: Symbol('unique') }; const writable = new WritableMock(); const write = vitest.spyOn(writable, 'write'); await handler(event, writable, context); expect(getCurrentInvoke()).toHaveProperty('event', event); expect(getCurrentInvoke()).toHaveProperty('context', context); expect(write).toHaveBeenCalledWith(''); const finalBuffer = Buffer.concat(writable.data); expect(finalBuffer.toString()).toBe(''); }); } it('should handle HEAD requests', async () => { const app = express(); app.head('/', (_, res) => { return res.set(200).end(); }); const expressFramework = new ExpressFramework(); const handler = awsStreamHandler.getHandler( app, expressFramework, adapters, resolver, binarySettings, respondWithErrors, logger, ); const event = createApiGatewayV2('HEAD', '/', undefined); const context = { test: Symbol('unique') }; const writable = new WritableMock(); const write = vitest.spyOn(writable, 'write'); await handler(event, writable, context); expect(getCurrentInvoke()).toHaveProperty('event', event); expect(getCurrentInvoke()).toHaveProperty('context', context); expect(write).toHaveBeenCalledWith(''); const finalBuffer = Buffer.concat(writable.data); expect(finalBuffer.toString()).toBe(''); }); it('should handle correctly the cookies', async () => { const app = express(); app.get('/', (_, res) => { res.setHeader('set-cookie', 'test=1'); res.json({ ok: true }); }); const expressFramework = new ExpressFramework(); const handler = awsStreamHandler.getHandler( app, expressFramework, adapters, resolver, binarySettings, respondWithErrors, logger, ); const event = createApiGatewayV2('GET', '/', {}, { test: 'true' }); const context = { test: Symbol('unique') }; const writable = new WritableMock(); await handler(event, writable, context); expect(getCurrentInvoke()).toHaveProperty('event', event); expect(getCurrentInvoke()).toHaveProperty('context', context); expect( (global as any).awslambda.HttpResponseStream.from, ).toHaveBeenCalledWith( expect.anything(), expect.objectContaining({ headers: expect.not.objectContaining({ 'set-cookie': 'test=1', }), cookies: ['test=1'], }), ); }); it('should handle correctly the cookies array', async () => { const app = express(); app.get('/', (_, res) => { res.setHeader('set-cookie', ['test=1', 'test2=3']); res.json({ ok: true }); }); const expressFramework = new ExpressFramework(); const handler = awsStreamHandler.getHandler( app, expressFramework, adapters, resolver, binarySettings, respondWithErrors, logger, ); const event = createApiGatewayV2('GET', '/', {}, { test: 'true' }); const context = { test: Symbol('unique') }; const writable = new WritableMock(); await handler(event, writable, context); expect(getCurrentInvoke()).toHaveProperty('event', event); expect(getCurrentInvoke()).toHaveProperty('context', context); expect( (global as any).awslambda.HttpResponseStream.from, ).toHaveBeenCalledWith( expect.anything(), expect.objectContaining({ headers: expect.not.objectContaining({ 'set-cookie': 'test=1', }), cookies: ['test=1', 'test2=3'], }), ); }); it('callbackWaitsForEmptyEventLoop should not be modified', async () => { const app = express(); app.get('/', (_, res) => { res.json({ ok: true }); }); const expressFramework = new ExpressFramework(); const handler = awsStreamHandler.getHandler( app, expressFramework, adapters, resolver, binarySettings, respondWithErrors, logger, ); const event = createApiGatewayV2('GET', '/', {}, { test: 'true' }); const defaultValueForCallback = Symbol('1'); const context = { test: Symbol('unique'), callbackWaitsForEmptyEventLoop: defaultValueForCallback, }; const writable = new WritableMock(); await handler(event, writable, context); expect(context).toHaveProperty( 'callbackWaitsForEmptyEventLoop', defaultValueForCallback, ); }); describe('callbackWaitsForEmptyEventLoop should be changed', () => { for (const value of [true, false]) { it(`to ${value}`, async () => { const app = express(); app.get('/', (_, res) => { res.json({ ok: true }); }); const expressFramework = new ExpressFramework(); const customAwsHandler = new AwsStreamHandler({ callbackWaitsForEmptyEventLoop: value, }); const handler = customAwsHandler.getHandler( app, expressFramework, adapters, resolver, binarySettings, respondWithErrors, logger, ); const event = createApiGatewayV2('GET', '/', {}, { test: 'true' }); const defaultValueForCallback = Symbol('test'); const context = { test: Symbol('unique'), callbackWaitsForEmptyEventLoop: defaultValueForCallback, }; const writable = new WritableMock(); await handler(event, writable, context); expect(context).toHaveProperty('callbackWaitsForEmptyEventLoop', value); }); } }); }); ================================================ FILE: test/handlers/azure.handler.spec.ts ================================================ import { describe, expect, it, vitest } from 'vitest'; import type { HttpRequest } from '@azure/functions'; import { type ILogger, createDefaultLogger } from '../../src'; import { HttpTriggerV4Adapter } from '../../src/adapters/azure'; import { AzureHandler } from '../../src/handlers/azure'; import { DefaultHandler } from '../../src/handlers/default'; import { PromiseResolver } from '../../src/resolvers/promise'; import { createHttpTriggerContext, createHttpTriggerEvent, } from '../adapters/azure/utils/http-trigger'; import { FrameworkMock } from '../mocks/framework.mock'; describe(AzureHandler.name, () => { const azureHandlerFactory = new AzureHandler< null, HttpRequest, any, any, any >(); const app = null; const response = { batata: true }; const mockFramework = new FrameworkMock(200, response); const httpTriggerAdapter = new HttpTriggerV4Adapter(); const adapters = [httpTriggerAdapter]; const resolver = new PromiseResolver(); const binarySettings = { contentEncodings: [], contentTypes: [] }; const respondWithErrors = true; const logger: ILogger = { debug: vitest.fn(), error: vitest.fn(), verbose: vitest.fn(), info: vitest.fn(), warn: vitest.fn(), }; it('should call default handler with correct params', () => { const defaultServerlessHandler = vitest.fn(() => Promise.resolve(response)); const defaultGetHandler = vitest .spyOn(DefaultHandler.prototype, 'getHandler') .mockImplementation(() => defaultServerlessHandler); const getHandlerArguments = [ app, mockFramework, adapters, resolver, binarySettings, respondWithErrors, logger, ] as const; const azureHandler = azureHandlerFactory.getHandler(...getHandlerArguments); const event = createHttpTriggerEvent('GET', '/'); const context = createHttpTriggerContext('GET', '/'); expect(azureHandler(context, event)).resolves.toBe(response); expect(defaultGetHandler).toHaveBeenCalledWith(...getHandlerArguments); expect(defaultServerlessHandler).toHaveBeenCalledWith( event, context, undefined, ); // eslint-disable-next-line @typescript-eslint/unbound-method expect(context.done).toBeUndefined(); expect(context.res).toBeUndefined(); }); it('should prefer context log method when send default logger', () => { const event = createHttpTriggerEvent('GET', '/'); const context = createHttpTriggerContext('GET', '/'); const defaultServerlessHandler = vitest.fn(() => Promise.resolve(response)); const defaultGetHandler = vitest .spyOn(DefaultHandler.prototype, 'getHandler') .mockImplementation(() => defaultServerlessHandler); const getHandlerArguments = [ app, mockFramework, adapters, resolver, binarySettings, respondWithErrors, ] as const; const azureHandler = azureHandlerFactory.getHandler( ...getHandlerArguments, createDefaultLogger(), ); expect(azureHandler(context, event)).resolves.toBe(response); expect(defaultGetHandler).toHaveBeenCalledWith( ...getHandlerArguments, // eslint-disable-next-line @typescript-eslint/unbound-method expect.objectContaining({ error: context.log.error }), ); expect(defaultServerlessHandler).toHaveBeenCalledWith( event, context, undefined, ); }); it('should prefer default log method when send false to useContextLogWhenInternalLogger option', () => { const event = createHttpTriggerEvent('GET', '/'); const context = createHttpTriggerContext('GET', '/'); const defaultServerlessHandler = vitest.fn(() => Promise.resolve(response)); const defaultGetHandler = vitest .spyOn(DefaultHandler.prototype, 'getHandler') .mockImplementation(() => defaultServerlessHandler); const log = createDefaultLogger(); const getHandlerArguments = [ app, mockFramework, adapters, resolver, binarySettings, respondWithErrors, log, ] as const; const azureHandler = new AzureHandler({ useContextLogWhenInternalLogger: false, }).getHandler(...getHandlerArguments); expect(azureHandler(context, event)).resolves.toBe(response); expect(defaultGetHandler).toHaveBeenCalledWith(...getHandlerArguments); expect(defaultServerlessHandler).toHaveBeenCalledWith( event, context, undefined, ); }); }); ================================================ FILE: test/handlers/default.handler.spec.ts ================================================ import { describe, expect, it, vitest } from 'vitest'; import { type AdapterContract, type ILogger, NO_OP, getCurrentInvoke, } from '../../src'; import { ApiGatewayV2Adapter } from '../../src/adapters/aws'; import { DefaultHandler } from '../../src/handlers/default'; import { PromiseResolver } from '../../src/resolvers/promise'; import { createApiGatewayV2 } from '../adapters/aws/utils/api-gateway-v2'; import { FrameworkMock } from '../mocks/framework.mock'; describe('DefaultHandler', () => { const defaultHandler = new DefaultHandler(); const app = null; const response = { batata: true }; const apiGatewayAdapter = new ApiGatewayV2Adapter(); const adapters = [apiGatewayAdapter] as AdapterContract[]; const resolver = new PromiseResolver(); const binarySettings = { contentEncodings: [], contentTypes: [] }; const respondWithErrors = true; const executeLog = (_, fn) => typeof fn === 'function' && fn(); const logger: ILogger = { debug: vitest.fn(executeLog), error: vitest.fn(executeLog), verbose: vitest.fn(executeLog), info: vitest.fn(executeLog), warn: vitest.fn(executeLog), }; it('should forward and return the response from a request with different status', async () => { const options: [statusCode: number][] = [[200], [400]]; for (const [statusCode] of options) { const framework = new FrameworkMock(statusCode, response); const handler = defaultHandler.getHandler( app, framework, adapters, resolver, binarySettings, respondWithErrors, logger, ); const event = createApiGatewayV2('GET', '/users', {}, { test: 'true' }); const context = { test: Symbol('unique') }; const result = await handler(event, context, NO_OP); expect(getCurrentInvoke()).toHaveProperty('event', event); expect(getCurrentInvoke()).toHaveProperty('context', context); expect(logger.debug).toHaveBeenNthCalledWith( 1, 'SERVERLESS_ADAPTER:PROXY', expect.any(Function), ); expect(logger.debug).toHaveBeenNthCalledWith( 2, 'SERVERLESS_ADAPTER:RESOLVED_ADAPTER_NAME: ', apiGatewayAdapter.getAdapterName(), ); expect(logger.debug).toHaveBeenNthCalledWith( 3, 'SERVERLESS_ADAPTER:FORWARD_REQUEST_TO_FRAMEWORK:REQUEST_VALUES', expect.any(Function), ); expect(logger.debug).toHaveBeenNthCalledWith( 4, 'SERVERLESS_ADAPTER:FORWARD_REQUEST_TO_FRAMEWORK:RESPONSE', expect.any(Function), ); expect(logger.debug).toHaveBeenNthCalledWith( 5, 'SERVERLESS_ADAPTER:FORWARD_RESPONSE:EVENT_SOURCE_RESPONSE_PARAMS', expect.any(Function), ); expect(logger.debug).toHaveBeenNthCalledWith( 6, 'SERVERLESS_ADAPTER:FORWARD_RESPONSE:EVENT_SOURCE_RESPONSE', expect.any(Function), ); expect(result).toHaveProperty('headers', { 'content-type': 'application/json', }); expect(result).toHaveProperty('isBase64Encoded', false); expect(result).toHaveProperty('statusCode', statusCode); expect(result).toHaveProperty('body', JSON.stringify(response)); } }); it('should forward and return the response from a request with base64 encoding', async () => { const framework = new FrameworkMock(200, response); const handler = defaultHandler.getHandler( app, framework, adapters, resolver, { isBinary: () => true }, respondWithErrors, logger, ); const event = createApiGatewayV2('GET', '/users', {}, { test: 'true' }); const context = { test: Symbol('unique') }; const result = await handler(event, context, NO_OP); expect(result).toHaveProperty('headers', { 'content-type': 'application/json', }); expect(result).toHaveProperty('isBase64Encoded', true); expect(result).toHaveProperty('statusCode', 200); expect(result).toHaveProperty( 'body', Buffer.from(JSON.stringify(response)).toString('base64'), ); }); it('should forward and return the response from a request with empty body', async () => { const framework = new FrameworkMock(200, response); const handler = defaultHandler.getHandler( app, framework, adapters, resolver, { isBinary: () => true }, respondWithErrors, logger, ); const event = createApiGatewayV2('GET', '/users', undefined, { test: 'true', }); const context = { test: Symbol('unique') }; const result = await handler(event, context, NO_OP); expect(result).toHaveProperty('headers', { 'content-type': 'application/json', }); expect(result).toHaveProperty('isBase64Encoded', true); expect(result).toHaveProperty('statusCode', 200); expect(result).toHaveProperty( 'body', Buffer.from(JSON.stringify(response)).toString('base64'), ); }); }); ================================================ FILE: test/handlers/digital-ocean.handler.spec.ts ================================================ import { describe, expect, it, vitest } from 'vitest'; import { type ILogger } from '../../src'; import { HttpFunctionAdapter } from '../../src/adapters/digital-ocean'; import { DefaultHandler } from '../../src/handlers/default'; import { DigitalOceanHandler } from '../../src/handlers/digital-ocean'; import { PromiseResolver } from '../../src/resolvers/promise'; import { FrameworkMock } from '../mocks/framework.mock'; import { createHttpFunctionEvent } from '../adapters/digital-ocean/utils/http-function'; import type { DigitalOceanHttpEvent } from '../../src/@types/digital-ocean'; describe(DigitalOceanHandler.name, () => { const azureHandlerFactory = new DigitalOceanHandler< null, DigitalOceanHttpEvent, any, any >(); const app = null; const response = { batata: true }; const mockFramework = new FrameworkMock(200, response); const adapter = new HttpFunctionAdapter(); const adapters = [adapter]; const resolver = new PromiseResolver(); const binarySettings = { contentEncodings: [], contentTypes: [] }; const respondWithErrors = true; const logger: ILogger = { debug: vitest.fn(), error: vitest.fn(), verbose: vitest.fn(), info: vitest.fn(), warn: vitest.fn(), }; it('should call default handler with correct params', () => { const defaultServerlessHandler = vitest.fn(() => Promise.resolve(response)); const defaultGetHandler = vitest .spyOn(DefaultHandler.prototype, 'getHandler') .mockImplementation(() => defaultServerlessHandler); const getHandlerArguments = [ app, mockFramework, adapters, resolver, binarySettings, respondWithErrors, logger, ] as const; const azureHandler = azureHandlerFactory.getHandler(...getHandlerArguments); const event = createHttpFunctionEvent('GET', '/'); expect(azureHandler(event)).resolves.toBe(response); expect(defaultGetHandler).toHaveBeenCalledWith(...getHandlerArguments); expect(defaultServerlessHandler).toHaveBeenCalledWith( event, undefined, undefined, ); }); }); ================================================ FILE: test/handlers/gcp.handler.spec.ts ================================================ import type { IncomingMessage, ServerResponse } from 'http'; import { describe, expect, it, vitest } from 'vitest'; import { type FrameworkContract } from '../../src'; import { GCPHandler } from '../../src/handlers/gcp'; import { FrameworkMock } from '../mocks/framework.mock'; class TestGCPHandler extends GCPHandler { public override onRequestCallback( app: TApp, framework: FrameworkContract, ): (req: IncomingMessage, res: ServerResponse) => void | Promise { return super.onRequestCallback(app, framework); } } describe(GCPHandler.name, () => { it('should register the callback to the library', () => { const functionName = 'test'; const gcpHandler = new TestGCPHandler(functionName); const mockFramework = new FrameworkMock(204, {}); const mockedData = 'Mocked' as any; const mockedFn = () => mockedData; vitest.mock('@google-cloud/functions-framework', () => ({ http: (name, fn) => { expect(name).toEqual('test'); expect(fn).toEqual('Mocked'); }, })); vitest.spyOn(gcpHandler, 'onRequestCallback').mockImplementation(mockedFn); const handler = gcpHandler.getHandler(null, mockFramework); expect(handler).toEqual(mockedData); }); }); ================================================ FILE: test/handlers/http-firebase-v2.handler.spec.ts ================================================ import type { HttpsOptions } from 'firebase-functions/v2/https'; import { describe, expect, it, vitest } from 'vitest'; import { type FrameworkContract, ServerlessRequest, ServerlessResponse, waitForStreamComplete, } from '../../src'; import { HttpFirebaseV2Handler } from '../../src/handlers/firebase'; import { FrameworkMock } from '../mocks/framework.mock'; describe(HttpFirebaseV2Handler.name, () => { it('should forward correctly the request to framework', async () => { const handlerFactory = new HttpFirebaseV2Handler(); const method = 'POST'; const url = '/users/batata'; const headers = { 'Content-Type': 'application/json' }; const remoteAddress = '168.16.0.1'; const body = Buffer.from('{"test": true}', 'utf-8'); const request = new ServerlessRequest({ method, url, headers, remoteAddress, body, }); const response = new ServerlessResponse({ method, }); const responseBody = { batata: true }; const responseStatus = 200; const framework = new FrameworkMock(responseStatus, responseBody); const handler = handlerFactory.getHandler(null, framework); handler(request, response); await waitForStreamComplete(response); expect(response.statusCode).toBe(responseStatus); expect(ServerlessResponse.body(response).toString()).toStrictEqual( JSON.stringify(responseBody), ); }); it('should handle weird body types', () => { const handlerFactory = new HttpFirebaseV2Handler(); const method = 'POST'; const url = '/users/batata'; const headers = { 'Content-Type': 'application/json' }; const remoteAddress = '168.16.0.1'; const options = [{ potato: true }, [{ test: true }]]; for (const option of options) { const request = new ServerlessRequest({ method, url, headers, remoteAddress, body: option as any, }); const response = new ServerlessResponse({ method, }); const framework: FrameworkContract = { // eslint-disable-next-line @typescript-eslint/no-misused-promises sendRequest: vitest.fn( async ( _app: null, req: ServerlessRequest, res: ServerlessResponse, ) => { expect(req.body?.toString()).toEqual(JSON.stringify(option)); expect(req.headers['content-length']).toEqual( Buffer.byteLength(JSON.stringify(option)).toString(), ); req.pipe(res); await waitForStreamComplete(res); expect(ServerlessResponse.body(res).toString()).toEqual( JSON.stringify(option), ); }, ), }; const handler = handlerFactory.getHandler(null, framework); handler(request, response); } }); it('should forward the properties to https.onRequest', () => { const options: HttpsOptions = { concurrency: 400, }; const factory = new HttpFirebaseV2Handler(options); const spyMethod = vitest.spyOn(factory, 'onRequestWithOptions' as any); factory.getHandler(null, new FrameworkMock(200, {})); expect(spyMethod).toHaveBeenCalledWith(options, expect.any(Function)); }); }); ================================================ FILE: test/handlers/http-firebase-v2.sdk-v5.handler.spec.ts ================================================ import type { HttpsOptions } from 'firebase-functions/v2/https'; import { describe, expect, it, vitest } from 'vitest'; import { type FrameworkContract, ServerlessRequest, ServerlessResponse, waitForStreamComplete, } from '../../src'; import { HttpFirebaseV2Handler } from '../../src/handlers/firebase'; import { FrameworkMock } from '../mocks/framework.mock'; vitest.mock('firebase-functions/v2', async () => { // eslint-disable-next-line import/no-unresolved return await import('firebase-functions-v5/v2'); }); describe(HttpFirebaseV2Handler.name, () => { it('should forward correctly the request to framework', async () => { const handlerFactory = new HttpFirebaseV2Handler(); const method = 'POST'; const url = '/users/batata'; const headers = { 'Content-Type': 'application/json' }; const remoteAddress = '168.16.0.1'; const body = Buffer.from('{"test": true}', 'utf-8'); const request = new ServerlessRequest({ method, url, headers, remoteAddress, body, }); const response = new ServerlessResponse({ method, }); const responseBody = { batata: true }; const responseStatus = 200; const framework = new FrameworkMock(responseStatus, responseBody); const handler = handlerFactory.getHandler(null, framework); handler(request, response); await waitForStreamComplete(response); expect(response.statusCode).toBe(responseStatus); expect(ServerlessResponse.body(response).toString()).toStrictEqual( JSON.stringify(responseBody), ); }); it('should handle weird body types', () => { const handlerFactory = new HttpFirebaseV2Handler(); const method = 'POST'; const url = '/users/batata'; const headers = { 'Content-Type': 'application/json' }; const remoteAddress = '168.16.0.1'; const options = [{ potato: true }, [{ test: true }]]; for (const option of options) { const request = new ServerlessRequest({ method, url, headers, remoteAddress, body: option as any, }); const response = new ServerlessResponse({ method, }); const framework: FrameworkContract = { // eslint-disable-next-line @typescript-eslint/no-misused-promises sendRequest: vitest.fn( async ( _app: null, req: ServerlessRequest, res: ServerlessResponse, ) => { expect(req.body?.toString()).toEqual(JSON.stringify(option)); expect(req.headers['content-length']).toEqual( Buffer.byteLength(JSON.stringify(option)).toString(), ); req.pipe(res); await waitForStreamComplete(res); expect(ServerlessResponse.body(res).toString()).toEqual( JSON.stringify(option), ); }, ), }; const handler = handlerFactory.getHandler(null, framework); handler(request, response); } }); it('should forward the properties to https.onRequest', () => { const options: HttpsOptions = { concurrency: 400, }; const factory = new HttpFirebaseV2Handler(options); const spyMethod = vitest.spyOn(factory, 'onRequestWithOptions' as any); factory.getHandler(null, new FrameworkMock(200, {})); expect(spyMethod).toHaveBeenCalledWith(options, expect.any(Function)); }); }); ================================================ FILE: test/handlers/http-firebase-v2.sdk-v6.handler.spec.ts ================================================ import type { HttpsOptions } from 'firebase-functions/v2/https'; import { describe, expect, it, vitest } from 'vitest'; import { type FrameworkContract, ServerlessRequest, ServerlessResponse, waitForStreamComplete, } from '../../src'; import { HttpFirebaseV2Handler } from '../../src/handlers/firebase'; import { FrameworkMock } from '../mocks/framework.mock'; vitest.mock('firebase-functions/v2', async () => { // eslint-disable-next-line import/no-unresolved return await import('firebase-functions-v6/v2'); }); describe(HttpFirebaseV2Handler.name, () => { it('should forward correctly the request to framework', async () => { const handlerFactory = new HttpFirebaseV2Handler(); const method = 'POST'; const url = '/users/batata'; const headers = { 'Content-Type': 'application/json' }; const remoteAddress = '168.16.0.1'; const body = Buffer.from('{"test": true}', 'utf-8'); const request = new ServerlessRequest({ method, url, headers, remoteAddress, body, }); const response = new ServerlessResponse({ method, }); const responseBody = { batata: true }; const responseStatus = 200; const framework = new FrameworkMock(responseStatus, responseBody); const handler = handlerFactory.getHandler(null, framework); handler(request, response); await waitForStreamComplete(response); expect(response.statusCode).toBe(responseStatus); expect(ServerlessResponse.body(response).toString()).toStrictEqual( JSON.stringify(responseBody), ); }); it('should handle weird body types', () => { const handlerFactory = new HttpFirebaseV2Handler(); const method = 'POST'; const url = '/users/batata'; const headers = { 'Content-Type': 'application/json' }; const remoteAddress = '168.16.0.1'; const options = [{ potato: true }, [{ test: true }]]; for (const option of options) { const request = new ServerlessRequest({ method, url, headers, remoteAddress, body: option as any, }); const response = new ServerlessResponse({ method, }); const framework: FrameworkContract = { // eslint-disable-next-line @typescript-eslint/no-misused-promises sendRequest: vitest.fn( async ( _app: null, req: ServerlessRequest, res: ServerlessResponse, ) => { expect(req.body?.toString()).toEqual(JSON.stringify(option)); expect(req.headers['content-length']).toEqual( Buffer.byteLength(JSON.stringify(option)).toString(), ); req.pipe(res); await waitForStreamComplete(res); expect(ServerlessResponse.body(res).toString()).toEqual( JSON.stringify(option), ); }, ), }; const handler = handlerFactory.getHandler(null, framework); handler(request, response); } }); it('should forward the properties to https.onRequest', () => { const options: HttpsOptions = { concurrency: 400, }; const factory = new HttpFirebaseV2Handler(options); const spyMethod = vitest.spyOn(factory, 'onRequestWithOptions' as any); factory.getHandler(null, new FrameworkMock(200, {})); expect(spyMethod).toHaveBeenCalledWith(options, expect.any(Function)); }); }); ================================================ FILE: test/handlers/http-firebase.handler.spec.ts ================================================ import { describe, expect, it, vitest } from 'vitest'; import type { Request, Response } from 'express'; import { type FrameworkContract, ServerlessRequest, ServerlessResponse, waitForStreamComplete, } from '../../src'; import { HttpFirebaseHandler } from '../../src/handlers/firebase'; import { FrameworkMock } from '../mocks/framework.mock'; describe(HttpFirebaseHandler.name, () => { it('should forward correctly the request to framework', async () => { const handlerFactory = new HttpFirebaseHandler(); const method = 'POST'; const url = '/users/batata'; const headers = { 'Content-Type': 'application/json' }; const remoteAddress = '168.16.0.1'; const body = Buffer.from('{"test": true}', 'utf-8'); const request = new ServerlessRequest({ method, url, headers, remoteAddress, body, }); const response = new ServerlessResponse({ method, }); const responseBody = { batata: true }; const responseStatus = 200; const framework = new FrameworkMock(responseStatus, responseBody); const handler = handlerFactory.getHandler(null, framework); handler(request as Request, response as unknown as Response); await waitForStreamComplete(response); expect(response.statusCode).toBe(responseStatus); expect(ServerlessResponse.body(response).toString()).toStrictEqual( JSON.stringify(responseBody), ); }); it('should handle weird body types', () => { const handlerFactory = new HttpFirebaseHandler(); const method = 'POST'; const url = '/users/batata'; const headers = { 'Content-Type': 'application/json' }; const remoteAddress = '168.16.0.1'; const options = [{ potato: true }, [{ test: true }]]; for (const option of options) { const request = new ServerlessRequest({ method, url, headers, remoteAddress, body: option as any, }); const response = new ServerlessResponse({ method, }); const framework: FrameworkContract = { // eslint-disable-next-line @typescript-eslint/no-misused-promises sendRequest: vitest.fn( async ( _app: null, req: ServerlessRequest, res: ServerlessResponse, ) => { expect(req.body?.toString()).toEqual(JSON.stringify(option)); expect(req.headers['content-length']).toEqual( Buffer.byteLength(JSON.stringify(option)).toString(), ); req.pipe(res); await waitForStreamComplete(res); expect(ServerlessResponse.body(res).toString()).toEqual( JSON.stringify(option), ); }, ), }; const handler = handlerFactory.getHandler(null, framework); handler(request as Request, response as unknown as Response); } }); }); ================================================ FILE: test/handlers/huawei.handler.spec.ts ================================================ import { type Server, createServer } from 'node:http'; import supertest from 'supertest'; import { describe, expect, it, vitest } from 'vitest'; import { type ILogger } from '../../src'; import { DummyAdapter } from '../../src/adapters/dummy'; import { DEFAULT_HUAWEI_LISTEN_PORT, HttpHuaweiHandler, } from '../../src/handlers/huawei'; import { DummyResolver } from '../../src/resolvers/dummy'; import { FrameworkMock } from '../mocks/framework.mock'; describe('HttpHuaweiHandler', () => { const app = null; const response = { batata: true }; const adapters = [new DummyAdapter()]; const resolver = new DummyResolver(); const binarySettings = { contentEncodings: [], contentTypes: [] }; const respondWithErrors = true; const logger: ILogger = { debug: vitest.fn(), error: vitest.fn(), verbose: vitest.fn(), info: vitest.fn(), warn: vitest.fn(), }; it('should create correctly mocked server and test default constants', async () => { const listenMock = vitest.fn(); const closeMock = vitest.fn(callback => callback()); const addEventListenerMock = vitest.fn(); const createServerMock = vitest.fn( () => ({ listen: listenMock, close: closeMock, addEventListener: addEventListenerMock, }) as unknown as Server, ); const handlerFactory = new HttpHuaweiHandler({ httpServerFactory: createServerMock, }); const framework = new FrameworkMock(200, response); const dispose = handlerFactory.getHandler( app, framework, adapters, resolver, binarySettings, respondWithErrors, logger, ); expect(createServerMock).toHaveBeenCalledWith(expect.any(Function)); expect(listenMock).toHaveBeenCalledWith( DEFAULT_HUAWEI_LISTEN_PORT, expect.any(Function), ); await expect(dispose()).resolves.toBeUndefined(); expect(closeMock).toHaveBeenCalled(); expect(logger.debug).toHaveBeenCalled(); }); async function waitUntilServerStarted(): Promise { await new Promise(resolve => setTimeout(resolve, 100)); } it('should create correctly http server', async () => { const handlerFactory = new HttpHuaweiHandler({ port: 0, }); const framework = new FrameworkMock(200, response); const dispose = handlerFactory.getHandler( app, framework, adapters, resolver, binarySettings, respondWithErrors, logger, ); await waitUntilServerStarted(); expect(logger.debug).toHaveBeenCalledWith( expect.stringContaining('Server started'), ); await expect(dispose()).resolves.toBeUndefined(); expect(logger.debug).toHaveBeenCalledWith( expect.stringContaining('Disposing'), ); }); it('should forward correctly the request to framework', async () => { let httpServer!: Server; const handlerFactory = new HttpHuaweiHandler({ port: 0, httpServerFactory: requestListener => { const server = createServer(requestListener); httpServer = server; return server; }, }); const responseStatus = 200; const framework = new FrameworkMock(responseStatus, response); const dispose = handlerFactory.getHandler( app, framework, adapters, resolver, binarySettings, respondWithErrors, logger, ); const httpResponse = await supertest(httpServer).get('/').send(); expect(httpResponse.status).toBe(responseStatus); expect(httpResponse.body).toStrictEqual(response); await expect(dispose()).resolves.toBeUndefined(); }); it('should throw error if something wrong occours on dispose', async () => { const error = new Error('something wrong occours'); const mockServer = { listen: vitest.fn(), close: vitest.fn(cb => cb(error)), } as unknown as Server; const handlerFactory = new HttpHuaweiHandler({ httpServerFactory: () => { return mockServer; }, }); const framework = new FrameworkMock(200, response); const dispose = handlerFactory.getHandler( app, framework, adapters, resolver, binarySettings, respondWithErrors, logger, ); await waitUntilServerStarted(); await expect(async () => await dispose()).rejects.toThrow(error); // eslint-disable-next-line @typescript-eslint/unbound-method expect(mockServer.close).toHaveBeenCalledWith(expect.any(Function)); }); }); ================================================ FILE: test/issues/alb-express-static/alb-express-static.spec.ts ================================================ import { resolve } from 'path'; import type { ALBResult } from 'aws-lambda'; import express from 'express'; import { describe, expect, it } from 'vitest'; import { ServerlessAdapter } from '../../../src'; import { AlbAdapter } from '../../../src/adapters/aws'; import { ExpressFramework } from '../../../src/frameworks/express'; import { DefaultHandler } from '../../../src/handlers/default'; import { PromiseResolver } from '../../../src/resolvers/promise'; import { createAlbEvent } from '../../adapters/aws/utils/alb-event'; describe('ALB rejecting response when uses express.static because', () => { it('returns some headers that are not string', async () => { const app = express(); app.use(express.static(resolve(__dirname))); const handler = ServerlessAdapter.new(app) .setHandler(new DefaultHandler()) .setFramework(new ExpressFramework()) .setResolver(new PromiseResolver()) .addAdapter(new AlbAdapter()) .build(); const albEvent = createAlbEvent('GET', '/robots.txt'); const response = (await handler(albEvent, {})) as ALBResult; for (const header of Object.keys(response.headers || {})) { expect(`typeof ${header}: ${typeof response.headers![header]}`).toBe( `typeof ${header}: string`, ); } }); }); ================================================ FILE: test/issues/alb-express-static/robots.txt ================================================ test ================================================ FILE: test/issues/issue-165/transfer-encoding-chunked-support.spec.ts ================================================ import { setTimeout } from 'timers/promises'; import { Readable } from 'node:stream'; import { describe, expect, it } from 'vitest'; import express from 'express'; import fastify from 'fastify'; import polka from 'polka'; import { ServerlessAdapter } from '../../../src'; import { DefaultHandler } from '../../../src/handlers/default'; import { PromiseResolver } from '../../../src/resolvers/promise'; import { ExpressFramework } from '../../../src/frameworks/express'; import { AlbAdapter } from '../../../src/adapters/aws'; import { createAlbEvent } from '../../adapters/aws/utils/alb-event'; import { FastifyFramework } from '../../../src/frameworks/fastify'; import { PolkaFramework } from '../../../src/frameworks/polka'; const expectedResult = 'INITIAL PAYLOAD RESPONSE\nFINAL PAYLOAD RESPONSE\n'; describe('Issue 165: cannot handle transfer-encoding: chunked', () => { it('express: should handle transfer-encoding: chunked', async () => { const app = express(); // eslint-disable-next-line @typescript-eslint/no-misused-promises app.get('/chunked-response', async (_req, res) => { // Send headers right away res.setHeader('Content-Type', 'text/plain'); res.setHeader('Transfer-Encoding', 'chunked'); res.status(200); res.write('INITIAL PAYLOAD RESPONSE\n'); await setTimeout(50); res.end('FINAL PAYLOAD RESPONSE\n'); }); const albEvent = createAlbEvent('GET', '/chunked-response'); const handler = ServerlessAdapter.new(app) .setHandler(new DefaultHandler()) .setFramework(new ExpressFramework()) .setResolver(new PromiseResolver()) .addAdapter(new AlbAdapter()) .build(); const result = await handler(albEvent, {}); expect(result.body).toEqual(expectedResult); expect(result.headers['content-length'], expectedResult.length.toString()); }); it('fastify: should handle transfer-encoding: chunked', async () => { const app = fastify(); // eslint-disable-next-line @typescript-eslint/no-misused-promises app.get('/chunked-response', async (_req, res) => { // Send headers right away res.type('text/plain'); res.header('Transfer-Encoding', 'chunked'); res.status(200); const buffer = new Readable(); buffer._read = () => {}; res.send(buffer); buffer.push('INITIAL PAYLOAD RESPONSE\n'); await setTimeout(50); buffer.push('FINAL PAYLOAD RESPONSE\n'); buffer.push(null); }); const albEvent = createAlbEvent('GET', '/chunked-response'); const handler = ServerlessAdapter.new(app) .setHandler(new DefaultHandler()) .setFramework(new FastifyFramework()) .setResolver(new PromiseResolver()) .addAdapter(new AlbAdapter()) .build(); const result = await handler(albEvent, {}); expect(result.body).toEqual(expectedResult); expect(result.headers['content-length'], expectedResult.length.toString()); }); it('polka: should handle transfer-encoding: chunked', async () => { const app = polka(); // eslint-disable-next-line @typescript-eslint/no-misused-promises app.get('/chunked-response', async (_req, res) => { // Send headers right away res.setHeader('Content-Type', 'text/plain'); res.setHeader('Transfer-Encoding', 'chunked'); res.statusCode = 200; res.write('INITIAL PAYLOAD RESPONSE\n'); await setTimeout(50); res.end('FINAL PAYLOAD RESPONSE\n'); }); const albEvent = createAlbEvent('GET', '/chunked-response'); const handler = ServerlessAdapter.new(app) .setHandler(new DefaultHandler()) .setFramework(new PolkaFramework()) .setResolver(new PromiseResolver()) .addAdapter(new AlbAdapter()) .build(); const result = await handler(albEvent, {}); expect(result.body).toEqual(expectedResult); expect(result.headers['content-length'], expectedResult.length.toString()); }); }); ================================================ FILE: test/mocks/framework.mock.ts ================================================ //#region Imports import type { IncomingMessage, ServerResponse } from 'http'; import { ObjectReadableMock } from 'stream-mock'; import { type FrameworkContract } from '../../src'; //#endregion /** * The class that represents a mock for framework that forward the request body to the response. * * @internal */ export class FrameworkMock implements FrameworkContract { //#region Constructor /** * Construtor padrão */ constructor( protected readonly statusCode: number, protected readonly mockedResponseData: object, ) {} //#endregion /** * {@inheritDoc} */ public sendRequest( _: null, _request: IncomingMessage, response: ServerResponse, ): void { const writableOutput = new ObjectReadableMock( [Buffer.from(JSON.stringify(this.mockedResponseData))], { objectMode: true, }, ); response.statusCode = this.statusCode; response.setHeader('content-type', 'application/json'); writableOutput.pipe(response); } } ================================================ FILE: test/network/request.spec.ts ================================================ import { describe, expect, it, vitest } from 'vitest'; import { NO_OP, ServerlessRequest } from '../../src'; describe('ServerlessRequest', () => { it('should can create serverless request from parameters of constructor', () => { const method = 'POST'; const url = '/users/batata'; const headers = { 'Content-Type': 'application/json' }; const remoteAddress = '168.16.0.1'; const body = Buffer.from('{"test": true}', 'utf-8'); const request = new ServerlessRequest({ method, url, headers, remoteAddress, body, }); expect(request).toHaveProperty('statusCode', 200); expect(request).toHaveProperty('statusMessage', 'OK'); expect(request).toHaveProperty('url', url); expect(request).toHaveProperty('headers', headers); expect(request).toHaveProperty('ip', remoteAddress); expect(request).toHaveProperty('body', body); expect(request).toHaveProperty('complete', true); expect(request).toHaveProperty('httpVersion', '1.1'); expect(request).toHaveProperty('httpVersionMajor', 1); expect(request).toHaveProperty('httpVersionMinor', 1); expect(request.socket).toHaveProperty('encrypted', true); expect(request.socket).toHaveProperty('readable', true); expect(request.socket).toHaveProperty('remoteAddress', remoteAddress); expect(request.socket).toHaveProperty('end', NO_OP); expect(request.socket).toHaveProperty('destroy', NO_OP); expect(request.socket.address()).toHaveProperty('port', 443); }); it('should push body property when call _read', () => { const method = 'POST'; const url = '/users/batata'; const headers = { 'Content-Type': 'application/json' }; const remoteAddress = '168.16.0.1'; const body = Buffer.from('{"random": 2323}', 'utf-8'); const request = new ServerlessRequest({ method, url, headers, remoteAddress, body, }); // eslint-disable-next-line @typescript-eslint/unbound-method request.push = vitest.fn(request.push); // eslint-disable-next-line @typescript-eslint/unbound-method request._read = vitest.fn(request._read); request._read(Math.random()); // eslint-disable-next-line @typescript-eslint/unbound-method expect(request.push).toHaveBeenNthCalledWith(1, body); // eslint-disable-next-line @typescript-eslint/unbound-method expect(request.push).toHaveBeenNthCalledWith(2, null); }); }); ================================================ FILE: test/network/response.spec.ts ================================================ import { ObjectReadableMock } from 'stream-mock'; import { describe, expect, it, vitest } from 'vitest'; import { ServerlessRequest, ServerlessResponse, waitForStreamComplete, } from '../../src'; describe('ServerlessResponse', () => { it('should can create serverless response from parameters of constructor', () => { const method = 'POST'; const response = new ServerlessResponse({ method, }); expect(response).toHaveProperty('useChunkedEncodingByDefault', false); expect(response).toHaveProperty('chunkedEncoding', false); expect(response).toHaveProperty('writable', true); expect(response).toHaveProperty('writableEnded', false); const responseBody = ServerlessResponse.body(response); expect(responseBody).toBeInstanceOf(Buffer); expect(responseBody.length).toEqual(0); const responseHeaders = ServerlessResponse.headers(response); expect(Object.keys(responseHeaders).length).toEqual(0); response.setHeader('test', 'testedValue'); const responseHeadersWithValue = ServerlessResponse.headers(response); expect(responseHeadersWithValue).toHaveProperty('test', 'testedValue'); // we didn't have the header that we set before because this method // get the value from headers inside the response instead from original implementation expect(response.headers).not.toHaveProperty('test', 'testedValue'); }); it('should can create response from request', () => { const defaultParams = { method: 'GET', headers: { test2: 'value2' }, url: '/values', }; const request = new ServerlessRequest({ ...defaultParams, }); const requestWithBody = new ServerlessRequest({ ...defaultParams, body: Buffer.from('{"test": true}', 'utf-8'), }); const requestWithBodyString = new ServerlessRequest({ ...defaultParams, body: '{"test": true}' as any, }); const requestWithUintArray = new ServerlessRequest({ ...defaultParams, body: Uint8Array.from( Array.from('{"test": true}').map(c => c.charCodeAt(0)), ), }); const requestHead = new ServerlessRequest({ ...defaultParams, method: 'HEAD', }); const requests: [ request: ServerlessRequest, expectedLength: number, hasBody: boolean, ][] = [ [request, 0, true], [requestWithBody, requestWithBody.body!.length, true], [requestWithBodyString, requestWithBodyString.body!.length, true], [requestWithUintArray, requestWithUintArray.body!.length, true], [requestHead, 0, false], ]; for (const [request, expectedLength, hasBody] of requests) { const responseFrom = ServerlessResponse.from(request); // why I have this test? Because of this line: https://github.com/nodejs/node/blob/master/lib/_http_server.js#L181 // the only way that I can use to check if method passed in request is working. expect(responseFrom).toHaveProperty('_hasBody', hasBody); expect(responseFrom).toHaveProperty( 'statusCode', responseFrom.statusCode, ); expect(responseFrom.headers).toHaveProperty( 'test2', defaultParams.headers.test2, ); const body = ServerlessResponse.body(responseFrom); expect(body).toHaveLength(expectedLength); expect(responseFrom).toHaveProperty('writable', true); expect(responseFrom).toHaveProperty('writableEnded', true); // In this case, when we use ServerlessResponse.from we set // response headers instead setting original implementation, // so, we have access to the headers by calling get headers property expect(responseFrom.headers).toHaveProperty( 'test2', defaultParams.headers.test2, ); } }); it('should can pipe response and return correct data', async () => { const options: [ value: string | Buffer | Uint8Array, expectedValue: string, ][] = [ ['test', 'test'], [Buffer.from('{"yo": true}', 'utf-8'), '{"yo": true}'], [ Uint8Array.from(Array.from('{"test": true}').map(c => c.charCodeAt(0))), '{"test": true}', ], ]; for (const [testedData, expectedValue] of options) { const response = new ServerlessResponse({ method: 'GET', }); const read = new ObjectReadableMock([testedData], { objectMode: true, }); read.pipe(response); const waitedStream = await waitForStreamComplete(response); expect(waitedStream).toBe(response); expect(ServerlessResponse.body(response).toString('utf-8')).toBe( expectedValue, ); } }); it('should cannot pipe response with invalid data', () => { const options: any[] = [void 0, null]; for (const testedData of options) { const response = new ServerlessResponse({ method: 'GET', }); expect(() => response.connection!.write(testedData)).toThrowError( 'response.write()', ); expect(() => response.connection!.write(testedData)).toThrowError( 'response.write()', ); } for (const testedData of options) { const response = new ServerlessResponse({ method: 'GET', }); response._header = null as any; expect(() => response.connection!.write(testedData)).toThrowError( 'response.write()', ); } }); it('should call correctly the callback with valid data in response', () => { const options = [ 'test', Buffer.from('testB', 'utf-8'), Uint8Array.from(Array.from('{"test": true}').map(c => c.charCodeAt(0))), ]; for (const testedData of options) { const response = new ServerlessResponse({ method: 'GET', }); response._header = null as any; const callback = vitest.fn(); expect(() => response.connection!.write(testedData, callback), ).not.toThrowError(); expect(callback).toHaveBeenCalled(); } }); it('should write headers correctly in object when call writeHead', () => { class MockServerlessResponse extends ServerlessResponse { public override callNativeWriteHead( statusCode: number, statusMessage?: string | any | any[], obj?: any | any[], ): this { return super.callNativeWriteHead(statusCode, statusMessage, obj); } } const response = new MockServerlessResponse({ method: 'GET', }); response.callNativeWriteHead = vitest.fn(); response.setHeader = vitest.fn(); expect(() => response.writeHead(200, { test1: 'true' })).not.toThrowError(); expect(() => response.writeHead(200, [{ test2: 'true' }, { test3: 'true' }]), ).not.toThrowError(); expect(() => response.writeHead(200, 'test', { test4: 'true' }), ).not.toThrowError(); // eslint-disable-next-line @typescript-eslint/unbound-method expect(response.setHeader).toHaveBeenCalledTimes(4); // eslint-disable-next-line @typescript-eslint/unbound-method expect(response.setHeader).toHaveBeenNthCalledWith(1, 'test1', 'true'); // eslint-disable-next-line @typescript-eslint/unbound-method expect(response.setHeader).toHaveBeenNthCalledWith(2, 'test2', 'true'); // eslint-disable-next-line @typescript-eslint/unbound-method expect(response.setHeader).toHaveBeenNthCalledWith(3, 'test3', 'true'); // eslint-disable-next-line @typescript-eslint/unbound-method expect(response.setHeader).toHaveBeenNthCalledWith(4, 'test4', 'true'); }); it('should write headers correctly in object when call setHeader', () => { const response = new ServerlessResponse({ method: 'GET', }); response.setHeader('test', 'value'); expect(response.getHeaders()).toHaveProperty('test', 'value'); response._wroteHeader = true; response.setHeader('test2', 'value'); expect(response.getHeaders()).not.toHaveProperty('test2', 'value'); expect(response.headers).toHaveProperty('test2', 'value'); }); }); ================================================ FILE: test/resolvers/aws-context.resolver.spec.ts ================================================ import type { Context } from 'aws-lambda'; import { beforeEach, describe, expect, it, vitest } from 'vitest'; import type { AdapterContract, ILogger, OnErrorProps, ResolverProps, } from '../../src'; import { AwsContextResolver } from '../../src/resolvers/aws-context'; describe(AwsContextResolver.name, () => { let resolverFactory!: AwsContextResolver; let mockedContext!: Context; let mockedLogger!: ILogger; let mockedAdapter!: AdapterContract; function onContextResolve(task: () => void): void { setTimeout(task, 200); } beforeEach(() => { resolverFactory = new AwsContextResolver(); mockedContext = { succeed: vitest.fn(), fail: vitest.fn(), } as unknown as Context; mockedLogger = { error: vitest.fn(), } as unknown as ILogger; mockedAdapter = { onErrorWhileForwarding: vitest.fn( ({ error, delegatedResolver, respondWithErrors, log, event, }: OnErrorProps) => { expect(error).toBeInstanceOf(Error); expect(delegatedResolver).toHaveProperty('succeed'); expect(delegatedResolver).toHaveProperty('fail'); expect(typeof respondWithErrors).toBe('boolean'); expect(log).toBe(mockedLogger); expect(event).toBeDefined(); delegatedResolver.fail(error); }, ), } as unknown as AdapterContract; }); it('should call correctly the context when succeed', async () => { const resolverProps: ResolverProps = { log: mockedLogger, respondWithErrors: false, context: mockedContext, event: {}, adapter: mockedAdapter, }; const resolver = resolverFactory.createResolver(resolverProps); const result = resolver.run(() => Promise.resolve(true)); expect(result).toBeUndefined(); await new Promise(resolve => { onContextResolve(() => { expect(resolverProps.context.succeed).toHaveBeenCalledWith(true); resolve(); }); }); }); it('should call correctly the context when fail', async () => { const resolverProps: ResolverProps = { log: mockedLogger, respondWithErrors: false, context: mockedContext, event: {}, adapter: mockedAdapter, }; const error = new Error('error on test'); const resolver = resolverFactory.createResolver(resolverProps); const result = resolver.run(() => Promise.reject(error)); expect(result).toBeUndefined(); await new Promise(resolve => { onContextResolve(() => { expect(resolverProps.log.error).toHaveBeenCalled(); // eslint-disable-next-line @typescript-eslint/unbound-method expect(resolverProps.adapter.onErrorWhileForwarding).toHaveBeenCalled(); expect(resolverProps.context.fail).toHaveBeenCalledWith(error); resolve(); }); }); }); it('should return error when sending wrong arguments to build resolver', () => { expect(() => resolverFactory.createResolver({ log: mockedLogger, respondWithErrors: false, adapter: mockedAdapter, event: {}, }), ).toThrowError(); expect(() => resolverFactory.createResolver({ log: mockedLogger, respondWithErrors: false, adapter: mockedAdapter, context: {} as unknown as Context, event: {}, }), ).toThrowError(); expect(() => resolverFactory.createResolver({ log: mockedLogger, respondWithErrors: false, adapter: mockedAdapter, context: { succeed: undefined, fail: vitest.fn(), } as unknown as Context, event: {}, }), ).toThrowError(); expect(() => resolverFactory.createResolver({ log: mockedLogger, respondWithErrors: false, adapter: mockedAdapter, context: { succeed: vitest.fn(), fail: undefined, } as unknown as Context, event: {}, }), ).toThrowError(); }); }); ================================================ FILE: test/resolvers/callback.resolver.spec.ts ================================================ import { beforeEach, describe, expect, it, vitest } from 'vitest'; import type { AdapterContract, ILogger, OnErrorProps, ResolverProps, } from '../../src'; import { CallbackResolver, type ServerlessCallback, } from '../../src/resolvers/callback'; describe(CallbackResolver.name, () => { let resolverFactory!: CallbackResolver; let mockedLogger!: ILogger; let mockedAdapter!: AdapterContract; function onCallbackResolve(task: () => void): void { setTimeout(task, 200); } beforeEach(() => { resolverFactory = new CallbackResolver(); mockedLogger = { error: vitest.fn(), } as unknown as ILogger; mockedAdapter = { onErrorWhileForwarding: vitest.fn( ({ error, delegatedResolver, respondWithErrors, log, event, }: OnErrorProps) => { expect(error).toBeInstanceOf(Error); expect(delegatedResolver).toHaveProperty('succeed'); expect(delegatedResolver).toHaveProperty('fail'); expect(typeof respondWithErrors).toBe('boolean'); expect(log).toBe(mockedLogger); expect(event).toBeDefined(); delegatedResolver.fail(error); }, ), } as unknown as AdapterContract; }); it('should call correctly the callback when succeed', async () => { const resolverProps: ResolverProps< any, any, ServerlessCallback, any > = { log: mockedLogger, respondWithErrors: false, callback: vitest.fn(), event: {}, adapter: mockedAdapter, }; const resolver = resolverFactory.createResolver(resolverProps); const result = resolver.run(() => Promise.resolve(true)); expect(result).toBeUndefined(); await new Promise(resolve => { onCallbackResolve(() => { expect(resolverProps.callback).toHaveBeenCalledWith(null, true); resolve(); }); }); }); it('should call correctly the callback when fail', async () => { const resolverProps: ResolverProps< any, any, ServerlessCallback, any > = { log: mockedLogger, respondWithErrors: false, callback: vitest.fn(), event: {}, adapter: mockedAdapter, }; const error = new Error('error on test'); const resolver = resolverFactory.createResolver(resolverProps); const result = resolver.run(() => Promise.reject(error)); expect(result).toBeUndefined(); await new Promise(resolve => { onCallbackResolve(() => { expect(resolverProps.log.error).toHaveBeenCalled(); // eslint-disable-next-line @typescript-eslint/unbound-method expect(resolverProps.adapter.onErrorWhileForwarding).toHaveBeenCalled(); expect(resolverProps.callback).toHaveBeenCalledWith(error, null); resolve(); }); }); }); it('should return error when sending wrong arguments to build resolver', () => { expect(() => resolverFactory.createResolver({ log: mockedLogger, respondWithErrors: false, adapter: mockedAdapter, event: {}, }), ).toThrowError(); }); }); ================================================ FILE: test/resolvers/dummy.resolver.spec.ts ================================================ import { describe, expect, it, vitest } from 'vitest'; import { DummyResolver } from '../../src/resolvers/dummy'; describe(DummyResolver.name, () => { it('should do nothing when called and return undefined', () => { const resolver = new DummyResolver(); const task = vitest.fn(); resolver.createResolver().run(task); expect(task).not.toHaveBeenCalled(); }); }); ================================================ FILE: test/resolvers/promise.resolver.spec.ts ================================================ import { beforeEach, describe, expect, it, vitest } from 'vitest'; import type { Context } from 'aws-lambda'; import type { AdapterContract, ILogger, OnErrorProps, ResolverProps, } from '../../src'; import { PromiseResolver } from '../../src/resolvers/promise'; describe(PromiseResolver.name, () => { let resolverFactory!: PromiseResolver< unknown, unknown, unknown, unknown, unknown >; let mockedContext!: Context; let mockedLogger!: ILogger; let mockedAdapter!: AdapterContract; beforeEach(() => { resolverFactory = new PromiseResolver(); mockedContext = { succeed: vitest.fn(), fail: vitest.fn(), } as unknown as Context; mockedLogger = { error: vitest.fn(), } as unknown as ILogger; mockedAdapter = { onErrorWhileForwarding: vitest.fn( ({ error, delegatedResolver, respondWithErrors, log, event, }: OnErrorProps) => { expect(error).toBeInstanceOf(Error); expect(delegatedResolver).toHaveProperty('succeed'); expect(delegatedResolver).toHaveProperty('fail'); expect(typeof respondWithErrors).toBe('boolean'); expect(log).toBe(mockedLogger); expect(event).toBeDefined(); delegatedResolver.fail(error); }, ), } as unknown as AdapterContract; }); it('should call correctly the promise when succeed', async () => { const resolverProps: ResolverProps = { log: mockedLogger, respondWithErrors: false, event: {}, adapter: mockedAdapter, }; const resolver = resolverFactory.createResolver(resolverProps); const result = await resolver.run(() => Promise.resolve(true)); expect(result).toBe(true); }); it('should call correctly the promise when fail', async () => { const resolverProps: ResolverProps = { log: mockedLogger, respondWithErrors: false, context: mockedContext, event: {}, adapter: mockedAdapter, }; const error = new Error('error on test'); const resolver = resolverFactory.createResolver(resolverProps); await expect(resolver.run(() => Promise.reject(error))).rejects.toBe(error); expect(resolverProps.log.error).toHaveBeenCalled(); // eslint-disable-next-line @typescript-eslint/unbound-method expect(resolverProps.adapter.onErrorWhileForwarding).toHaveBeenCalled(); }); }); ================================================ FILE: test/serverless-adapter.spec.ts ================================================ import { describe, expect, it, vitest } from 'vitest'; import { type BinarySettings, DEFAULT_BINARY_CONTENT_TYPES, DEFAULT_BINARY_ENCODINGS, type HandlerContract, NO_OP, ServerlessAdapter, createDefaultLogger, } from '../src'; import { ApiGatewayV2Adapter } from '../src/adapters/aws'; import * as logger from '../src/core/logger'; import { DefaultHandler } from '../src/handlers/default'; import { PromiseResolver } from '../src/resolvers/promise'; import { FrameworkMock } from './mocks/framework.mock'; describe('ServerlessAdapter', () => { it('should have correct default values', () => { const defaultLoggerSymbol = Symbol('createDefaultLogger'); vitest .spyOn(logger, 'createDefaultLogger') .mockReturnValue(defaultLoggerSymbol as any); const oldEnv = process.env; vitest.resetModules(); process.env = { ...oldEnv, NODE_ENV: 'test' }; const adapter = ServerlessAdapter.new(null); expect(adapter['binarySettings']).toHaveProperty( 'contentEncodings', DEFAULT_BINARY_ENCODINGS, ); expect(adapter['binarySettings']).toHaveProperty( 'contentTypes', DEFAULT_BINARY_CONTENT_TYPES, ); expect(adapter['respondWithErrors']).toEqual(false); expect(adapter['log']).toEqual(defaultLoggerSymbol); expect(adapter['adapters']).toHaveLength(0); expect(adapter['framework']).toBeUndefined(); expect(adapter['resolver']).toBeUndefined(); expect(adapter['handler']).toBeUndefined(); expect(adapter['app']).toEqual(null); vitest.resetModules(); process.env = { ...oldEnv, NODE_ENV: 'development' }; const developmentAdapter = ServerlessAdapter.new(null); expect(developmentAdapter['respondWithErrors']).toEqual(true); }); it('should can create a pipeline of handlers', () => { const statusCode = 200; const response = { body: true }; const app = null; const mockedHandler: HandlerContract = { getHandler: vitest.fn(() => NO_OP), }; const adapter = new ApiGatewayV2Adapter(); const logger = createDefaultLogger(); const respondWithErrors = false; const resolver = new PromiseResolver(); const framework = new FrameworkMock(statusCode, response); const binarySettings: BinarySettings = { isBinary: () => true }; const handler = ServerlessAdapter.new(app) .setHandler(mockedHandler) .setLogger(logger) .setRespondWithErrors(respondWithErrors) .setResolver(resolver) .setFramework(framework) .setBinarySettings(binarySettings) .addAdapter(adapter) .build(); expect(handler).toBe(NO_OP); // eslint-disable-next-line @typescript-eslint/unbound-method expect(mockedHandler.getHandler).toHaveBeenCalledWith( app, framework, [adapter], resolver, expect.objectContaining(binarySettings), respondWithErrors, logger, ); }); it('should CANNOT set handler twice', () => { const handler = new DefaultHandler(); expect(() => ServerlessAdapter.new(null) .setHandler(handler) .setRespondWithErrors(true) .setHandler(handler), ).toThrow('twice'); }); it('should CANNOT set framework twice', () => { const framework = new FrameworkMock(200, {}); expect(() => ServerlessAdapter.new(null) .setFramework(framework) .setRespondWithErrors(true) .setFramework(framework), ).toThrow('twice'); }); it('should CANNOT set resolver twice', () => { const resolver = new PromiseResolver(); expect(() => ServerlessAdapter.new(null) .setResolver(resolver) .setRespondWithErrors(true) .setResolver(resolver), ).toThrow('twice'); }); it('should CANNOT build without set resolver', () => { expect(() => ServerlessAdapter.new(null).build()).toThrow('set a resolver'); }); it('should CANNOT build without set framework', () => { expect(() => ServerlessAdapter.new(null).setResolver(new PromiseResolver()).build(), ).toThrow('set a framework'); }); it('should CANNOT build without set handler', () => { expect(() => ServerlessAdapter.new(null) .setResolver(new PromiseResolver()) .setFramework(new FrameworkMock(200, {})) .build(), ).toThrow('set a handler'); }); it('should CANNOT build without set at least one adapter', () => { expect(() => ServerlessAdapter.new(null) .setResolver(new PromiseResolver()) .setFramework(new FrameworkMock(200, {})) .setHandler(new DefaultHandler()) .build(), ).toThrow('one adapter'); }); }); ================================================ FILE: tsconfig.build.json ================================================ { "extends": "./tsconfig.json", "include": ["src/**/*.ts"] } ================================================ FILE: tsconfig.doc.json ================================================ { "extends": "./tsconfig.json", "compilerOptions": { "emitDeclarationOnly": true, "noEmit": false, "skipDefaultLibCheck": true, "skipLibCheck": true }, "include": ["src/**/*.ts", "src/index.doc.ts"], "exclude": [] } ================================================ FILE: tsconfig.eslint.json ================================================ { "extends": "./tsconfig.json", "compilerOptions": { "noEmit": true, "types": [ "vitest/globals" ], "lib": [ "esnext" ], "allowJs": true, "checkJs": true }, "include": [ "src/**/*.ts", "test/**/*.ts", "scripts/**/*.ts", "vite.config.ts", "tsup.config.ts" ], "exclude": [ "benchmark/**/*.ts" ] } ================================================ FILE: tsconfig.json ================================================ { "$schema": "http://json.schemastore.org/tsconfig", "compilerOptions": { "outDir": "./lib", "target": "ES2022", "module": "ES2022", "moduleResolution": "Bundler", "incremental": false, "noEmit": true, "noImplicitAny": false, "verbatimModuleSyntax": true, "allowUnreachableCode": false, "allowUnusedLabels": false, "exactOptionalPropertyTypes": false, "noImplicitOverride": true, "allowSyntheticDefaultImports": true, "alwaysStrict": true, "declaration": true, "declarationMap": true, "esModuleInterop": true, "importHelpers": false, "newLine": "lf", "noEmitHelpers": false, "noFallthroughCasesInSwitch": true, "noImplicitReturns": true, "noUnusedLocals": true, "noUnusedParameters": true, "preserveConstEnums": true, "pretty": true, "removeComments": false, "resolveJsonModule": true, "sourceMap": true, "strict": true, "useDefineForClassFields": true }, "reflection": true, "include": [ "src/**/*.ts", "test/**/*.ts", "scripts/**/*.ts" ], "exclude": [ "src/index.doc.ts", "benchmark/**/*.ts" ] } ================================================ FILE: tsdoc.json ================================================ { "$schema": "https://developer.microsoft.com/json-schemas/tsdoc/v0/tsdoc.schema.json", "tagDefinitions": [ { "tagName": "@breadcrumb", "syntaxKind": "block", "allowMultiple": false } ], "noStandardTags": false } ================================================ FILE: tsup.config.ts ================================================ import { execSync } from 'node:child_process'; import { defineConfig } from 'tsup'; const adapters = [ 'apollo-server', 'aws', 'azure', 'digital-ocean', 'dummy', 'huawei', ]; const frameworks = [ 'apollo-server', 'body-parser', 'cors', 'deepkit', 'express', 'fastify', 'hapi', 'koa', 'lazy', 'polka', 'trpc', ]; const handlers = [ 'aws', 'azure', 'default', 'digital-ocean', 'firebase', 'gcp', 'huawei', ]; const resolvers = ['aws-context', 'callback', 'dummy', 'promise']; const libEntries = [ ...adapters.map(adapter => `src/adapters/${adapter}/index.ts`), ...frameworks.map(framework => `src/frameworks/${framework}/index.ts`), ...handlers.map(handler => `src/handlers/${handler}/index.ts`), ...resolvers.map(resolver => `src/resolvers/${resolver}/index.ts`), ]; const createExport = (filePath: string) => ({ import: { types: `./lib/${filePath}.d.ts`, default: `./lib/${filePath}.mjs`, }, require: { types: `./lib/${filePath}.d.cts`, default: `./lib/${filePath}.cjs`, }, }); const createExportReducer = (initialPath: string) => (acc: object, name: string) => { acc[`./${initialPath}/${name}`] = createExport( `${initialPath}/${name}/index`, ); acc[`./lib/${initialPath}/${name}`] = createExport( `${initialPath}/${name}/index`, ); return acc; }; const packageExports = { '.': createExport('index'), ...adapters.reduce(createExportReducer('adapters'), {}), ...frameworks.reduce(createExportReducer('frameworks'), {}), ...handlers.reduce(createExportReducer('handlers'), {}), ...resolvers.reduce(createExportReducer('resolvers'), {}), }; execSync(`npm pkg set exports='${JSON.stringify(packageExports)}' --json`); export default defineConfig({ outDir: './lib', clean: true, dts: true, format: ['esm', 'cjs'], outExtension: ({ format }) => ({ js: format === 'cjs' ? '.cjs' : '.mjs', }), cjsInterop: true, entry: ['src/index.ts', ...libEntries], sourcemap: true, skipNodeModulesBundle: true, minify: true, target: 'es2022', tsconfig: './tsconfig.build.json', keepNames: true, bundle: true, }); ================================================ FILE: vite.config.ts ================================================ // eslint-disable-next-line import/no-unresolved import { defineConfig } from 'vitest/config'; export default defineConfig({ esbuild: { target: 'es2022', }, test: { coverage: { provider: 'v8', include: ['src/**'], exclude: [ 'src/**/@types/**/*.ts', 'src/**/index.doc.ts', 'src/**/index.ts', ], }, }, }); ================================================ FILE: www/.gitignore ================================================ # Dependencies /node_modules # Production /build # Generated files .docusaurus .cache-loader # Misc .DS_Store .env.local .env.development.local .env.test.local .env.production.local npm-debug.log* yarn-debug.log* yarn-error.log* sidebar-api-generated.js ================================================ FILE: www/.tool-versions ================================================ nodejs 18.18.1 ================================================ FILE: www/README.md ================================================ # Documentation This website is built using [Docusaurus 2](https://docusaurus.io/), a modern static website generator to create documentation to this library. See the website [here](https://viniciusl.com.br/serverless-adapter). ================================================ FILE: www/babel.config.js ================================================ module.exports = { presets: [require.resolve('@docusaurus/core/lib/babel/preset')], }; ================================================ FILE: www/blog/2022-06-17-the-beginning.mdx ================================================ --- slug: the-beginning title: The Beginning authors: [h4ad] tags: [serverless-adapter] --- Hello, welcome to my new library to help you integrate your API with the serverless world. ## The development It took me almost 5 months to build this library, refactoring was easy and testing was challenging, but documenting this library was the hardest part. It took me almost 2 weeks to refactor [@vendia/serverless-express](https://github.com/vendia/serverless-express), about 1 and a half month to create tests with 99% coverage and the rest of the time I spent creating documentation for this library. I currently added support for: - AWS - [AWS Load Balancer](https://docs.aws.amazon.com/lambda/latest/dg/services-alb.html) by using ([AlbAdapter](/docs/main/adapters/aws/alb)) - [AWS Api Gateway V1](https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-develop-integrations-lambda.html) by using ([ApiGatewayV1Adapter](/docs/main/adapters/aws/api-gateway-v1)) - [AWS Api Gateway V2](https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-develop-integrations-lambda.html) by using ([ApiGatewayV2Adapter](/docs/main/adapters/aws/api-gateway-v2)) - [AWS DynamoDB](https://docs.aws.amazon.com/lambda/latest/dg/with-ddb.html) by using ([DynamoDBAdapter](/docs/main/adapters/aws/dynamodb)) - [AWS Event Bridge / CloudWatch Events](https://docs.aws.amazon.com/lambda/latest/dg/services-cloudwatchevents.html) by using ([EventBridgeAdapter](/docs/main/adapters/aws/event-bridge)) - [AWS Lambda Edge](https://docs.aws.amazon.com/lambda/latest/dg/lambda-edge.html) by using ([LambdaEdgeAdapter](/docs/main/adapters/aws/lambda-edge)) - [AWS SNS](https://docs.aws.amazon.com/lambda/latest/dg/with-sns.html) by using ([SNSAdapter](/docs/main/adapters/aws/sns)) - [AWS SQS](https://docs.aws.amazon.com/lambda/latest/dg/with-sqs.html) by using ([SQSAdapter](/docs/main/adapters/aws/sqs)) - Huawei - [Http Function](https://support.huaweicloud.com/intl/en-us/usermanual-functiongraph/functiongraph_01_1442.html): Look [this section](/docs/main/handlers/huawei#http-function). - [Event Function](https://support.huaweicloud.com/intl/en-us/usermanual-functiongraph/functiongraph_01_1441.html): Look [this section](/docs/main/handlers/huawei#event-function). - [Api Gateway](https://support.huaweicloud.com/intl/en-us/devg-functiongraph/functiongraph_02_0102.html#functiongraph_02_0102__li5178638110137) by using ([HuaweiApiGatewayAdapter](/docs/main/adapters/huawei/huawei-api-gateway)). But it's just the beginning, I'm going to build more adapters to integrate with as much of the cloud as possible, just to be able to deploy my APIs on any cloud. ## About me I am a student at [Facens](https://facens.br/) university and I work for [Liga](https://liga.facens.br/), which is a sector within Facens that develops applications, websites, games and much more fun stuff. I currently work on this library only in my spare time and I need to balance my Final Theses and my overtime projects so it was very challenging but I am happy with the end result of this library. ## Inspiration This library was originally created to help my company reduce costs with AWS SQS, but it has since turned into something I can spend my time developing and learning English because I'm not a native speaker (as typing problems might suggest) writing all the documentation in English. ## Credits I need to thank [@vendia](https://vendia.net/) for developing [@vendia/serverless-express](https://github.com/vendia/serverless-express), all logic and code I finished to refactor from the code I read on serverless-express. I also have many thanks to [Chaguri](https://github.com/guichaguri), [Liga](https://liga.facens.br/) and many other people who gave me time and insights to create this library. ## You can use it right now! See the [Introduction](/docs/main/intro) section to know more about the library. ================================================ FILE: www/blog/2022-07-17-updates-and-releases.mdx ================================================ --- slug: updates-and-releases title: Updates and Releases authors: [h4ad] tags: [serverless-adapter, trpc, azure, firebase] image: https://images.unsplash.com/photo-1636819488524-1f019c4e1c44 --- import BrowserWindow from '@site/src/components/BrowserWindow'; ![To the moon!](https://images.unsplash.com/photo-1636819488524-1f019c4e1c44) Now we have more Handlers, Frameworks and Adapters, let's see what's new. > From [v2.3.2](https://github.com/H4ad/serverless-adapter/tree/v2.3.2) to [v2.6.0](https://github.com/H4ad/serverless-adapter/tree/v2.6.0), compare the changes [here](https://github.com/H4ad/serverless-adapter/compare/v2.3.2...v2.6.0). ## Changes 42 commits, 6905 lines added, 601 lines deleted, that's the size of the changes since [The Beginning](/blog/the-beginning). I'm very proud of how things are going, I learned a lot by studying to implement these new things. But, let's learn what's new in all these releases. ## Azure and Firebase You can now use this library to deploy your apps to [Azure Functions](https://docs.microsoft.com/en-us/azure/azure-functions/create-first-function-vs-code-node) and [Firebase Functions](https://firebase.google.com/docs/functions/http-events). More specifically, you can integrate with Http Trigger V4 on Azure and Http Events on Firebase. These integrations are just to open the door of possibilities, in the future I want to add support for more triggers in these clouds. Check out the [Azure](/docs/main/handlers/azure) and [Firebase](/docs/main/handlers/firebase) docs for how to integrate. I also added examples for the cloud in the [serverless-adapter-examples](https://github.com/H4ad/serverless-adapter-examples) repository. ## tRPC tRPC allows you to easily build & consume fully typesafe APIs, without schemas or code generation. tRPC is a framework that brings a new way of thinking about APIs, instead of REST or GraphQL, you can build typesafe APIs and easily can integrate with the client, seems to be very promising. So now you can deploy applications developed with tRPC to any cloud that this library supports, have a look at [docs](/docs/main/frameworks/trpc) to learn more about how to use it. ## That's all folks! I have two more weeks to work in this library without worrying because I'm on vacation at the university, so probably my next efforts will be to bring more articles to this blog to show the full power of this library. Giving some spoilers for those of you that make it this far, I'll start by showing you the benefits of using AWS Lambda integrated with API Gateway and SQS, I used it in a project of my company and I managed to reduce a lot of stress on the database and now we are able to process 500k votes in minutes without spending 15% CPU using a PostgreSQL database on a t2.micro instance. That's all for today, thank you! ================================================ FILE: www/blog/2023-04-28-aws-lambda-response-streaming.mdx ================================================ --- slug: aws-lambda-response-streaming title: AWS Lambda Response Streaming authors: [h4ad] tags: [serverless-adapter, aws, aws-lambda, function-url] image: https://images.unsplash.com/photo-1527489377706-5bf97e608852 --- ![A beautiful stream!](https://images.unsplash.com/photo-1527489377706-5bf97e608852) > Image by [Hendrik Cornelissen](https://unsplash.com/@the_bracketeer) on [Unsplash](https://unsplash.com) It's been a long time since I wrote a post here, but I'm happy to share this new announcement. ## First, are you new to this library? ![First time?](first-time-meme-first-time.gif) Let me introduce the library first, I named [Serverless Adapter](/docs/main/intro) because my goal is connect any serverless environment to any NodeJS framework. So you could just plug your [framework](/docs/main/architecture#framework), use the correct [handler](/docs/main/architecture#handler) for your serverless environment, choose the [adapters](/docs/main/architecture#adapter) and then you can deploy your application! ### What does this library support? Currently, we support [8 NodeJS frameworks](/docs/category/frameworks): [Express](/docs/main/frameworks/express), [Fastify](/docs/main/frameworks/fastify), [tRPC](/docs/main/frameworks/trpc), [Apollo Server](/docs/main/frameworks/apollo-server), [NestJS](/docs/main/frameworks/nestjs), [Deepkit](/docs/main/frameworks/deepkit), [Koa](/docs/main/frameworks/koa) and [Hapi](/docs/main/frameworks/Hapi). We also support [6 serverless environments](/docs/category/handlers): [AWS](/docs/main/handlers/aws), [Azure](/docs/main/handlers/azure), [Google Cloud](/docs/main/handlers/gcp), [Digital Ocean](/docs/main/handlers/digital-ocean), [Firebase](/docs/main/handlers/firebase) and [Huawei](/docs/main/handlers/huawei). Talking about AWS, we support [10 different services](/docs/category/aws) like API Gateway [V1](/docs/main/adapters/aws/api-gateway-v1) and [V2](/docs/main/adapters/aws/api-gateway-v2), [SQS](/docs/main/adapters/aws/sqs), [SNS](/docs/main/adapters/aws/sns), etc... and you can combine them to use the same codebase and lambda to handle them all. :::tip To learn understand the power of this composability, check this article I wrote about how I went [From a million invocations to a thousand with correct caching](https://viniciusl.com.br/posts/2022/12/08-from-million-invocations-to-thousand-with-correct-caching/). ::: But okay, enough self-marketing, let's get to the main point of this article. ## AWS Lambda Response Streaming Today I'm rolling out support for [AWS Lambda Streaming Response](https://aws.amazon.com/blogs/compute/introducing-aws-lambda-response-streaming/) using [AwsStreamHandler](/docs/main/handlers/aws#aws-lambda-response-streaming). If you already use this library, just change [DefaultHandler](/docs/main/handlers/aws#usage) to [AwsStreamHandler](/docs/main/handlers/aws#aws-lambda-response-streaming), and make sure you're using [DummyResolver](/docs/api/Resolvers/DummyResolver) and [ApiGatewayV2Adapter](/docs/main/adapters/aws/api-gateway-v2): ```ts title="index.ts" import { ServerlessAdapter } from '@h4ad/serverless-adapter'; import { AwsStreamHandler } from '@h4ad/serverless-adapter/handlers/aws'; import { DummyResolver } from '@h4ad/serverless-adapter/resolvers/dummy'; import { ApiGatewayV2Adapter } from '@h4ad/serverless-adapter/adapters/aws'; import app from './app'; export const handler = ServerlessAdapter.new(app) // .setHandler(new DefaultHandler()) .setHandler(new AwsStreamHandler()) .setResolver(new DummyResolver()) .setAdapter(new ApiGatewayV2Adapter()) // more options... //.setFramework(new ExpressFramework()) .build(); ``` > Despite its name, `ApiGatewayV2Adapter` can be used to support API Gateway V2 and function URLs. :::caution Response streaming currently is only available for Function URLs. ::: That's it :) Now you can use Function URLs and stream your content to the world! :::information Don't forget to enable the feature in your AWS Lambda function by changing `BUFFERED` TO `RESPONSE_STREAM.` ::: ### I NEED CODE!!! Well, if you're the type of person who, like me, needs to see the code working, here's a repository with several example projects using this library: [serverless-adapter-examples](https://github.com/H4ad/serverless-adapter-examples). ## Beyond HTTP Requests Furthermore, not only can you receive HTTP requests using `Function URLs`, but you can combine your `SQS` queue and use the same codebase to process everything. I haven't spent a lot of time testing it, but so far, any AWS service that supports this library can be hooked up to your Lambda function with `RESPONSE_STREAM` enabled. The only thing you need to know is: the answer didn't work as expected, I couldn't get the [SQS Partial Response](/docs/main/adapters/aws/sqs#batch-item-failures) to work for example . But you can give it a try anyway, share your results with me on [twitter](https://twitter.com/vinii_joga10) and I'll be happy to help if I can. ## Conclusion Well, I don't have much to say, but I hope you enjoy this new feature and use it to build amazing things. I've spent the last 3 weeks trying to figure out how to make this work and I'm happy with the result. If you're curious enough to learn more about how I implement it, you can see [this PR](https://github.com/H4ad/serverless-adapter/pull/90) with all my struggles and thoughts over the weeks. ================================================ FILE: www/blog/2023-12-25-dual-package-publish.mdx ================================================ --- slug: dual-package-publish title: Support for Dual Package Publish authors: [h4ad] tags: [serverless-adapter, cjs, esm, npm, package, publish] image: https://images.unsplash.com/photo-1429743305873-d4065c15f93e --- import BrowserWindow from '@site/src/components/BrowserWindow'; ![Two paths inside a forest!](https://images.unsplash.com/photo-1429743305873-d4065c15f93e) > Image by [Jens Lelie](https://unsplash.com/@madebyjens) on [Unsplash](https://unsplash.com) This feature was initially asked by [ClementCornut](https://github.com/ClementCornut) on issue [#127](https://github.com/H4ad/serverless-adapter/issues/127). Initially I was a little unsure whether to publish `esm` and `cjs`, but then I started to like the idea of exporting my packages as `@h4ad/serverless-adapter/adapters/aws`. You can use it by installing the new version: ```bash npm i @h4ad/serverless-adapter@4.0.1 ``` > The version 4.0.0 was released with a bug that didn't include the package files, so I released the version 4.0.1 to fix this issue. In the previous version, since I only export to `commonjs`, you need to import the files as `/lib/adapters/aws`, which is not bad, but not exactly good. This was necessary because I can't export all files in the default `export` as this will lead you to install all frameworks supported by this library. But ok, I had some problems while adding support for dual-package publishing which I want to share with you only, and especially for my future version if you want to add support for dual-package publishing in your modules. ## Vite I already use `vitest` test my package and to build the previous versions, it works great and is specially fast to run the tests (5.82s to run 456 tests across 52 files). But the configuration to build was a little bit nightmare: ```ts title="vite.config.ts" // some initial configuration lines... ...(!isTest && { esbuild: { format: 'cjs', platform: 'node', target: 'node18', sourcemap: 'external', minifyIdentifiers: false, }, build: { outDir: 'lib', emptyOutDir: true, sourcemap: true, lib: { entry: path.resolve(__dirname, 'src/index.ts'), formats: ['cjs'], }, rollupOptions: { external: ['yeoman-generator'], input: glob.sync(path.resolve(__dirname, 'src/**/*.ts')), output: { preserveModules: true, entryFileNames: entry => { const { name } = entry; const fileName = `${name}.js`; return fileName; }, }, }, }, }), ``` All of this configuration was necessary as I need to build my package to match exactly the same structure as `src`, which was needed for users to import as `/lib/adapters/aws`. On the first attempt, I just tried to extend this configuration, but I spent a few hours and managed to generate a good package output, but it was missing some details that were very painful to bear, such as correctly emitting `d.cts`. If you want to see how it turned out, if you want to try doing this using `vite` directly, [here is vite.config.ts](https://github.com/H4ad/serverless-adapter/blob/f642739334687b0a22312074a6e225e9f8ac8124/vite.config.ts). But then I started to give up and [tweeted about it](https://twitter.com/vinii_joga10/status/1738954683853451760). ## Suggestions from Twitter I got some incredible helpful messages on twitter and I will cover those suggestions that I applied to be able to finally support dual package publish. ### Re-exporting on mjs This was a suggestion from [Matteo Collina](https://twitter.com/matteocollina), he also sent me the package [snap](https://github.com/mcollina/snap) which does this re-export, which I also saw being used in [Orama](https://github.com/oramasearch/orama/blob/main/packages/orama/src/cjs/index.cts), is basically doing this: Your state: ```js title="./node_modules/pkg/state.cjs" import Date from 'date'; const someDate = new Date(); ``` Define/export on `cjs`. ```js title="./node_modules/pkg/index.cjs" const state = require('./state.cjs'); module.exports.state = state; ``` Re-export on `mjs`. ```js title="./node_modules/pkg/index.mjs" import state from './state.cjs'; export { state, }; ``` This way, I solve the problem of state isolation, but I will need: - or manually export all these files - or use a tool to automate this process Both ways will be a bit painful to maintain, so I didn't go that route. This approach can be fine if your library maintains state and the codebase is pure javascript. ### tsup Instead of trying to go through this configuration hell in `vite`, [Michele Riva](https://twitter.com/MicheleRivaCode) suggested [tsup](https://tsup.egoist.dev/) which is incredibly easy to use. I spent less than 10 minutes to generate almost the same output as the previous configuration with `vite`, but this time the output was correct, with `d.cts` files being generated. My configuration file now looks like this: ```ts title="tsup.config.ts" export default defineConfig({ outDir: './lib', clean: true, dts: true, format: ['esm', 'cjs'], outExtension: ({ format }) => ({ js: format === 'cjs' ? '.cjs' : '.mjs', }), cjsInterop: true, // the libEntries is basically all the entries I need to export, // like: adapters/aws, frameworks/fastify, etc... entry: ['src/index.ts', ...libEntries], sourcemap: true, skipNodeModulesBundle: true, minify: true, target: 'es2022', tsconfig: './tsconfig.build.json', keepNames: true, bundle: true, }); ``` ### package.json exports Since I have a lot of things to export, I also automate the configuration of `exports` in `package.json` with: ```ts title="tsup.config.ts" // I do the same for adapters, frameworks and handlers const resolvers = ['aws-context', 'callback', 'dummy', 'promise']; const libEntries = [ ...resolvers.map(resolver => `src/resolvers/${resolver}/index.ts`), ]; const createExport = (filePath: string) => ({ import: { types: `./lib/${filePath}.d.ts`, default: `./lib/${filePath}.mjs`, }, require: { types: `./lib/${filePath}.d.cts`, default: `./lib/${filePath}.cjs`, }, }); const createExportReducer = (initialPath: string) => (acc: object, name: string) => { acc[`./${initialPath}/${name}`] = createExport( `${initialPath}/${name}/index`, ); acc[`./lib/${initialPath}/${name}`] = createExport( `${initialPath}/${name}/index`, ); return acc; }; const packageExports = { '.': createExport('index'), ...resolvers.reduce(createExportReducer('resolvers'), {}), // and I also do the same for adapters, frameworks and handlers. }; // this command does the magic to update my package.json execSync(`npm pkg set exports='${JSON.stringify(packageExports)}' --json`); ``` This works incredible and also keep my `package.json` updated. ### Module not found on moduleResolution `node` But there is one thing you should pay attention to, did you see that I export files with the prefix `./lib`? ```json title="package.json" "exports": { "./adapters/apollo-server": {...}, "./lib/adapters/apollo-server": {...}, } ``` The content of both is the same, but I do this to be compatible with the resolution of `node`. Without this configuration, the `IDE` will show the import to `@h4ad/serverless-adapter/adapters/apollo-server` as resolved, but `node` will not be able to find the file during the runtime. And the interesting part of doing this is that people who import this package with `/lib` will still be able to import the code, and they won't need any code modifications. Maybe I could release this feature without it being a breaking change with this change, but to be on the safe side, I released it as a breaking change anyway. ### publint The [publint](https://publint.dev/) is a tool that I learned on [ESM Modernization Lessons](https://blog.isquaredsoftware.com/2023/08/esm-modernization-lessons/#early-attempts) inside `Early Attempts`, this article was a suggestion by [Luca Micieli](https://twitter.com/LucaRams23). With this tool, I detect several problems with the `exports` configuration and this will make your life a lot easier. But this tool has a problem that they didn't catch, and this problem was pointed out by [Michele Riva](https://twitter.com/MicheleRivaCode), instead: ```json title="package.json" "exports": { "resolvers/promise": { ``` I should export it with the prefix `./`: ```json title="package.json" "exports": { "./resolvers/promise": { ``` This small detail can make your configuration fail. ### verdaccio The [verdaccio](https://verdaccio.org/) was suggested by [Abhijeet Prasad](https://twitter.com/imabhiprasad), with this tool you can have your private registry that you can use to test, Abhijeet use this tool on [Sentry](https://sentry.io/) SDKs to do e2e tests. With this tool, I was able to make sure the package was working correctly with the new dual package publish. ### Wrong `moduleResolution` It took me a while to realize, but while testing the changes in a sample project, the imports still failed because TypeScript couldn't find the files. So I remember the suggestion from [sami](https://twitter.com/samijaber_) who gave me some examples of projects he uses in [BuilderIO/hidration-overlay](https://github.com/BuilderIO/hydration-overlay/tree/main/tests), and I understand the difference between `moduleResolution`, in my project it was configured for `node`, in his project it was configured for `bundler`. When I changed this setting, all imports started working and imports using `/lib/adapters/aws` started failing. If you're like me and have no idea what this setting is about, the documentation says: Specify the module resolution strategy: 'node16' or 'nodenext' for modern versions of Node.js. Node.js v12 and later supports both ECMAScript imports and CommonJS require, which resolve using different algorithms. These moduleResolution values, when combined with the corresponding module values, picks the right algorithm for each resolution based on whether Node.js will see an import or require in the output JavaScript code. 'node10' (previously called 'node') for Node.js versions older than v10, which only support CommonJS require. You probably won’t need to use node10 in modern code. 'bundler' for use with bundlers. Like node16 and nodenext, this mode supports package.json "imports" and "exports", but unlike the Node.js resolution modes, bundler never requires file extensions on relative paths in imports. Make sense why it was not working at all, `node` was not built to support `exports`, and only `nodenext` and `bundler` should work correctly. ## Doubling the package size Something I saw that made me a little worried was the size of this package, since I need to export the code, types and source maps twice, the package went from `~600Kb` to `~1.5Mb`. I enabled minification to try to reduce the amount of code shipped, but if you use this library and don't have any kind of minification/bundling during your build, I highly recommend you look into these libraries to help you with the size of your zip file being uploaded: - [@h4ad/node-modules-packer](https://github.com/H4ad/node-modules-packer) - [ncc](https://github.com/vercel/ncc) - [zip-it-and-ship-it](https://github.com/netlify/zip-it-and-ship-it) ## Conclusions The `esm` packages was a nightmare to support some time ago but the ecosystem is starting solving problems with new tools to bundle your project instead of having to fight with your own configuration files. The `cjs` is a no-brain solution, almost no configuration and it works great but maybe is not ideal for your consumers/clients, some of them can have issues like [ClementCornut](https://github.com/ClementCornut) that needed to import the files with the full path `import awsPkg from "@h4ad/serverless-adapter/lib/adapters/aws/index.js";`. When I started adding this feature, I had no knowledge of how to publish double packages, I basically go-horse in the early hours of my implementation and then I started learning more about how it works and how to properly configure the package. This makes me realize that dual-package publishing isn't the nightmare I initially thought, I just didn't learn from the previous mistakes other people made and I should have read more articles about it before I started implementing it. My sincere thanks to: - [Abhijeet Prasad](https://twitter.com/imabhiprasad) - [Luca Micieli](https://twitter.com/LucaRams23) - [Matteo Collina](https://twitter.com/matteocollina) - [Michele Riva](https://twitter.com/MicheleRivaCode) - [sami](https://twitter.com/samijaber_) Without you, it will probably take me a lot longer to be able to convince myself to go ahead and try again to add support for dual-package publish after the first failures. ================================================ FILE: www/blog/authors.yml ================================================ h4ad: name: Vinícius Lourenço title: Maintainer of Serverless Adapter url: https://github.com/h4ad image_url: https://github.com/h4ad.png ================================================ FILE: www/docs/.gitignore ================================================ /api/* ================================================ FILE: www/docs/main/adapters/aws/alb.mdx ================================================ --- title: ALB description: See more about how to integrate with AWS Application Load Balancer. --- The adapter to handle requests from [AWS Application Load Balancer](https://docs.aws.amazon.com/lambda/latest/dg/services-alb.html). :::info When an error is thrown during forwarding and the `responseWithErrors` option is `true`, we return a 500 status WITH error stack in the response. ::: :::tip Reducing Costs Not sure when to use AWS ALB instead of API Gateway? See [this article](https://serverless-training.com/articles/save-money-by-replaceing-api-gateway-with-application-load-balancer/) from Serverless Training to learn more. ::: ## About the adapter This adapter turns every request coming from AWS ALB into an HTTP request to your framework. ```json title="alb-event-example.json" { "requestContext": { "elb": { "targetGroupArn": "arn:aws:elasticloadbalancing:us-east-2:123456789012:targetgroup/lambda-279XGJDqGZ5rsrHC2Fjr/49e9d65c45c6791a" } }, "httpMethod": "POST", "path": "/lambda", "queryStringParameters": { "query": "1234ABCD" }, "headers": { "accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8", "accept-encoding": "gzip", "accept-language": "en-US,en;q=0.9", "connection": "keep-alive", "host": "lambda-alb-123578498.us-east-2.elb.amazonaws.com", "upgrade-insecure-requests": "1", "user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/71.0.3578.98 Safari/537.36", "x-amzn-trace-id": "Root=1-5c536348-3d683b8b04734faae651f476", "x-forwarded-for": "72.12.164.125", "x-forwarded-port": "80", "x-forwarded-proto": "http", "x-imforwards": "20" }, "body": "Banana", "isBase64Encoded": false } ``` So, to add support to the above request, we must have registered the `/lambda` route as `POST` and when API Gateway sends this event, you will get: - `body`: `Banana` - `queryString`: `query=1234ABCD` ## Customizing You can remove some base path with the `stripBasePath` option inside [AlbAdapterOptions](/docs/api/Adapters/AWS/AlbAdapter/AlbAdapterOptions). :::caution When you configure your API with some `basePath` like `/prod`, you should either send the request in the path `/prod/lambda` or set `stripBasePath` to `/prod`. ::: ## Usage To add support to AWS ALB you do the following: ```ts title="index.ts" import { ServerlessAdapter } from '@h4ad/serverless-adapter'; import { AlbAdapter } from '@h4ad/serverless-adapter/adapters/aws'; import { DefaultHandler } from '@h4ad/serverless-adapter/handlers/default'; import app from './app'; export const handler = ServerlessAdapter.new(app) .setHandler(new DefaultHandler()) // .setFramework(new ExpressFramework()) // .setResolver(new PromiseResolver()) .addAdapter(new AlbAdapter()) // customizing: // .addAdapter(new AlbAdapter({ stripBasePath: '/prod' })) .build(); ``` ### Transfer Encoding Chunked ALB currently didn't support chunked transfer, so the response body will be buffered without the special characters introduced by the chunked transfer keeping the body complete. ================================================ FILE: www/docs/main/adapters/aws/api-gateway-v1.mdx ================================================ --- title: Api Gateway V1 description: See more about how to integrate with AWS API Gateway V1. --- The adapter to handle requests from [AWS Api Gateway V1](https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-develop-integrations-lambda.html). :::info When an error is thrown during forwarding and the `responseWithErrors` option is `true`, we return a 500 status WITH error stack in the response. ::: :::tip Reducing Costs Not sure when to use AWS ALB instead of API Gateway? See [this article](https://serverless-training.com/articles/save-money-by-replaceing-api-gateway-with-application-load-balancer/) from Serverless Training to learn more. ::: ## About the adapter This adapter turns every request coming from API Gateway V1 into an HTTP request to your framework. ```json title="api-gateway-v1-event-example.json" { "version": "1.0", "resource": "/my/path", "path": "/my/path", "httpMethod": "GET", "headers": { "header1": "value1", "header2": "value2" }, "multiValueHeaders": { "header1": [ "value1" ], "header2": [ "value1", "value2" ] }, "queryStringParameters": { "parameter1": "value1", "parameter2": "value" }, "multiValueQueryStringParameters": { "parameter1": [ "value1", "value2" ], "parameter2": [ "value" ] }, "requestContext": { "accountId": "123456789012", "apiId": "id", "authorizer": { "claims": null, "scopes": null }, "domainName": "id.execute-api.us-east-1.amazonaws.com", "domainPrefix": "id", "extendedRequestId": "request-id", "httpMethod": "GET", "identity": { "accessKey": null, "accountId": null, "caller": null, "cognitoAuthenticationProvider": null, "cognitoAuthenticationType": null, "cognitoIdentityId": null, "cognitoIdentityPoolId": null, "principalOrgId": null, "sourceIp": "IP", "user": null, "userAgent": "user-agent", "userArn": null, "clientCert": { "clientCertPem": "CERT_CONTENT", "subjectDN": "www.example.com", "issuerDN": "Example issuer", "serialNumber": "a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1", "validity": { "notBefore": "May 28 12:30:02 2019 GMT", "notAfter": "Aug 5 09:36:04 2021 GMT" } } }, "path": "/my/path", "protocol": "HTTP/1.1", "requestId": "id=", "requestTime": "04/Mar/2020:19:15:17 +0000", "requestTimeEpoch": 1583349317135, "resourceId": null, "resourcePath": "/my/path", "stage": "$default" }, "pathParameters": null, "stageVariables": null, "body": "Hello from Lambda!", "isBase64Encoded": false } ``` So, to add support to the above request, we must have registered the `/my/path` route as `POST` and when API Gateway sends this event, you will get: - `body`: `Hello from Lambda` - `queryString`: `parameter1=value1¶meter1=value2¶meter2=value` ## Customizing You can remove some base path with the `stripBasePath` option inside [ApiGatewayV1Options](/docs/api/Adapters/AWS/ApiGatewayV1Adapter/ApiGatewayV1Options). :::caution When you configure your API with some `basePath` like `/prod`, you should either send the request in the path `/prod/my/path` or set `stripBasePath` to `/prod`. ::: You can also ensure request headers are lowercase like the default behavior of Node.js `http` module by setting the `lowercaseRequestHeaders` option to `true`. :::caution ApiGateway may already be ensuring your headers are lowercase but it may be worth confirming in your own system as many libraries in the Node.js ecosystem will assume this lowercasing. ::: ## Usage To add support to AWS API Gateway V1 you do the following: ```ts title="index.ts" import { ServerlessAdapter } from '@h4ad/serverless-adapter'; import { ApiGatewayV1Adapter } from '@h4ad/serverless-adapter/adapters/aws'; import { DefaultHandler } from '@h4ad/serverless-adapter/handlers/default'; import app from './app'; export const handler = ServerlessAdapter.new(app) .setHandler(new DefaultHandler()) // .setFramework(new ExpressFramework()) // .setResolver(new PromiseResolver()) .addAdapter(new ApiGatewayV1Adapter()) // customizing: // .addAdapter(new ApiGatewayV1Adapter({ stripBasePath: '/prod' })) .build(); ``` ### Transfer Encoding Chunked API Gateway V1 currently didn't support chunked transfer, so we throw an exception when you send the `transfer-encoding=chunked`. But, you can disable the exception by setting the `throwOnChunkedTransferEncoding` to `false` in the [ApiGatewayV1Options](/docs/api/Adapters/AWS/ApiGatewayV1Adapter/ApiGatewayV1Options). ```ts title="index.ts" new ApiGatewayV1Adapter({ throwOnChunkedTransferEncoding: false }) ``` The response body will be buffered without the special characters introduced by the chunked transfer keeping the body complete. ================================================ FILE: www/docs/main/adapters/aws/api-gateway-v2.mdx ================================================ --- title: Api Gateway V2 description: See more about how to integrate with AWS API Gateway V2. --- The adapter to handle requests from [AWS Api Gateway V2](https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-develop-integrations-lambda.html) and from [AWS Lambda Function URLs](https://docs.aws.amazon.com/lambda/latest/dg/lambda-urls.html). :::info When an error is thrown during forwarding and the `responseWithErrors` option is `true`, we return a 500 status WITH error stack in the response. ::: :::tip Reducing Costs Not sure when to use AWS ALB instead of API Gateway? See [this article](https://serverless-training.com/articles/save-money-by-replaceing-api-gateway-with-application-load-balancer/) from Serverless Training to learn more. ::: ## About the adapter This adapter transforms every request coming from API Gateway V2 into an HTTP request to your framework. ```json title="api-gateway-v2-event-example.json" { "version": "2.0", "routeKey": "$default", "rawPath": "/my/path", "rawQueryString": "parameter1=value1¶meter1=value2¶meter2=value", "cookies": [ "cookie1", "cookie2" ], "headers": { "header1": "value1", "header2": "value1,value2" }, "queryStringParameters": { "parameter1": "value1,value2", "parameter2": "value" }, "requestContext": { "accountId": "123456789012", "apiId": "api-id", "authentication": { "clientCert": { "clientCertPem": "CERT_CONTENT", "subjectDN": "www.example.com", "issuerDN": "Example issuer", "serialNumber": "a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1", "validity": { "notBefore": "May 28 12:30:02 2019 GMT", "notAfter": "Aug 5 09:36:04 2021 GMT" } } }, "authorizer": { "jwt": { "claims": { "claim1": "value1", "claim2": "value2" }, "scopes": [ "scope1", "scope2" ] } }, "domainName": "id.execute-api.us-east-1.amazonaws.com", "domainPrefix": "id", "http": { "method": "POST", "path": "/my/path", "protocol": "HTTP/1.1", "sourceIp": "IP", "userAgent": "agent" }, "requestId": "id", "routeKey": "$default", "stage": "$default", "time": "12/Mar/2020:19:03:58 +0000", "timeEpoch": 1583348638390 }, "body": "Hello from Lambda", "pathParameters": { "parameter1": "value1" }, "isBase64Encoded": false, "stageVariables": { "stageVariable1": "value1", "stageVariable2": "value2" } } ``` So, to add support to the above request, we must have registered the `/my/path` route as `POST` and when API Gateway sends this event, you will get: - `body`: `Hello from Lambda` - `queryString`: `parameter1=value1¶meter1=value2¶meter2=value` ## Customizing You can strip base path with the option `stripBasePath` inside [ApiGatewayV2Options](/docs/api/Adapters/AWS/ApiGatewayV2Adapter/ApiGatewayV2Options). :::caution When you configure your API with some `basePath` like `/prod`, you should either send the request in the path `/prod/my/path` or set `stripBasePath` to `/prod`. ::: ## Usage To add support to AWS API Gateway V2 you do the following: ```ts title="index.ts" import { ServerlessAdapter } from '@h4ad/serverless-adapter'; import { ApiGatewayV2Adapter } from '@h4ad/serverless-adapter/adapters/aws'; import { DefaultHandler } from '@h4ad/serverless-adapter/handlers/default'; import app from './app'; export const handler = ServerlessAdapter.new(app) .setHandler(new DefaultHandler()) // .setFramework(new ExpressFramework()) // .setResolver(new PromiseResolver()) .addAdapter(new ApiGatewayV2Adapter()) // customizing: // .addAdapter(new ApiGatewayV2Adapter({ stripBasePath: '/prod' })) .build(); ``` ### Transfer Encoding Chunked API Gateway V2 currently didn't support chunked transfer, so we throw an exception when you send the `transfer-encoding=chunked`. But, you can disable the exception by setting the `throwOnChunkedTransferEncoding` to `false` in the [ApiGatewayV2Options](/docs/api/Adapters/AWS/ApiGatewayV2Adapter/ApiGatewayV2Options). ```ts title="index.ts" new ApiGatewayV1Adapter({ throwOnChunkedTransferEncoding: false }) ``` The response body will be buffered without the special characters introduced by the chunked transfer keeping the body complete. ================================================ FILE: www/docs/main/adapters/aws/dynamodb.mdx ================================================ --- title: DynamoDB description: See more about how to integrate with AWS DynamoDB. --- The adapter to handle requests from [AWS DynamoDB](https://docs.aws.amazon.com/lambda/latest/dg/with-ddb.html). :::info The option of `responseWithErrors` is ignored by this adapter and we always call `resolver.fail` with the error. ::: ## Typescript To correctly type your `body` when receiving the AWS DynamoDB request, you must install `aws-lambda`: ```bash npm i --save-dev @types/aws-lambda ``` So when getting the `body` you should use this type: ```ts title="dynamodb.controller.ts" import type { DynamoDBStreamEvent } from 'aws-lambda'; ``` ## About the adapter In AWS DynamoDB, you don't have requests, you just receive the records in the `event` property of the handler. So, in order to handle this adapter, by default we create a `POST` request to `/dynamo` with the `body` being the `event` property as JSON. ```json title="dynamodb-event-example.json" { "Records": [ { "eventID": "1", "eventVersion": "1.0", "dynamodb": { "Keys": { "Id": { "N": "101" } }, "NewImage": { "Message": { "S": "New item!" }, "Id": { "N": "101" } }, "StreamViewType": "NEW_AND_OLD_IMAGES", "SequenceNumber": "111", "SizeBytes": 26 }, "awsRegion": "us-west-2", "eventName": "INSERT", "eventSourceARN": "arn:aws:dynamodb:us-east-1:111122223333:table/EventSourceTable", "eventSource": "aws:dynamodb" } ] } ``` Normally, your framework will parse this JSON and return the parsed values as javascript objects. ## Customizing You can change the HTTP Method and Path that will be used to create the request by sending `dynamoDBForwardMethod` and `dynamoDBForwardPath` inside [DynamoDBAdapterOptions](/docs/api/Adapters/AWS/DynamoDBAdapter/DynamoDBAdapterOptions). ## Usage To add support to AWS DynamoDB you do the following: ```ts title="index.ts" import { ServerlessAdapter } from '@h4ad/serverless-adapter'; import { DynamoDBAdapter } from '@h4ad/serverless-adapter/adapters/aws'; import { DefaultHandler } from '@h4ad/serverless-adapter/handlers/default'; import app from './app'; export const handler = ServerlessAdapter.new(app) .setHandler(new DefaultHandler()) // .setFramework(new ExpressFramework()) // .setResolver(new PromiseResolver()) .addAdapter(new DynamoDBAdapter()) // customizing: // .addAdapter(new DynamoDBAdapter({ dynamoDBForwardPath: '/prod/dynamo', dynamoDBForwardMethod: 'PUT' })) .build(); ``` :::caution When you configure your API with some `basePath` like `/prod`, you should set `dynamoDBForwardPath` as `/prod/dynamo` instead leave as default `/dynamo`. ::: ## Security You **MUST** check if the header `Host` contains the value of `dynamodb.amazonaws.com`. Without checking this header, if you add this adapter and [AWS API Gateway V2](./api-gateway-v2) adapter, you will be vulnerable to attacks because anyone can create a `POST` request to `/dynamo`. ## What happens when my response status is different from 2xx or 3xx? Well, this library will throw an error. In previous versions of this library, the behavior was different, but now we throw an error if the status does not indicate success. When it throws an error, the request will simply fail to process the event, and depending on how you set up your dead-letter queue or your retry police, can be sent to dead-letter queue for you to check what happens or try again. ## Batch Item Failures If you enable this batch item failure option, to be able to partially return that some items failed to process, first configure your Adapter: ```ts const adapter = new DynamoDBAdapter({ batch: true, }); ``` And then, just return the following JSON in the route that processes the DynamoDB event. ```json { "batchItemFailures": [ { "itemIdentifier": "id2" }, { "itemIdentifier": "id4" } ] } ``` > [Reference](https://docs.aws.amazon.com/lambda/latest/dg/with-ddb.html#services-ddb-batchfailurereporting) ================================================ FILE: www/docs/main/adapters/aws/event-bridge.mdx ================================================ --- title: EventBridge (CloudWatch Events) description: See more about how to integrate with AWS EventBridge. --- The adapter to handle requests from [AWS EventBridge](https://docs.aws.amazon.com/lambda/latest/dg/services-cloudwatchevents.html). :::info The option of `responseWithErrors` is ignored by this adapter and we always call `resolver.fail` with the error. ::: ## Typescript To correctly type your `body` when receiving the AWS EventBridge request, you must install `aws-lambda`: ```bash npm i --save-dev @types/aws-lambda ``` So when getting the `body` you should use this type: ```ts title="eventbridge.controller.ts" import type { EventBridgeEvent } from 'aws-lambda'; ``` If you want to integrate with Scheduled Expression, you can use this type: ```ts title="eventbridge.controller.ts" import type { ScheduledEvent } from 'aws-lambda'; ``` ## About the adapter In AWS EventBridge, you don't have requests, you just receive the info from Cloudwatch Events within `event` property of the handler. So, in order to handle this adapter, by default we create a `POST` request to `/eventbridge` with the `body` being the `event` property as JSON. ```json title="rds-eventbridge-event-example.json" { "version": "0", "id": "fe8d3c65-xmpl-c5c3-2c87-81584709a377", "detail-type": "RDS DB Instance Event", "source": "aws.rds", "account": "123456789012", "time": "2020-04-28T07:20:20Z", "region": "us-east-2", "resources": [ "arn:aws:rds:us-east-2:123456789012:db:rdz6xmpliljlb1" ], "detail": { "EventCategories": [ "backup" ], "SourceType": "DB_INSTANCE", "SourceArn": "arn:aws:rds:us-east-2:123456789012:db:rdz6xmpliljlb1", "Date": "2020-04-28T07:20:20.112Z", "Message": "Finished DB Instance backup", "SourceIdentifier": "rdz6xmpliljlb1" } } ``` Normally, your framework will parse this JSON and return the parsed values as javascript objects. ### Schedule Expression With [Schedule Expression](https://docs.aws.amazon.com/lambda/latest/dg/services-cloudwatchevents-expressions.html), you have the following JSON when the event is triggered: ```json title="scheduled-eventbridge-event-example.json" { "version": "0", "account": "123456789012", "region": "us-east-2", "detail": {}, "detail-type": "Scheduled Event", "source": "aws.events", "time": "2019-03-01T01:23:45Z", "id": "cdc73f9d-aea9-11e3-9d5a-835b769c0d9c", "resources": [ "arn:aws:events:us-east-2:123456789012:rule/my-schedule" ] } ``` It's good enough if you want to integrate with just one cron job, but what if you want more? One option is to check the `resources` property, but I don't like that solution, so I'll introduce it to you in a way. When selecting the target as AWS Lambda, you can configure in `Additional Settings` the `input target` as [Input Transformer](https://docs.aws.amazon.com/eventbridge/latest/userguide/eb-transform-target-input.html), with this option you can modify the above JSON into something different or add new properties. After clicking `Configure Input Transformer`, you can choose the `Scheduled Event` in the sample event to get an idea of what the event will look like after the transformation. In the `Input Path` inside the Target Input Transformer you will put this json: ```json title="input-path.json" { "account": "$.account", "detail-type": "$.detail-type", "id": "$.id", "region": "$.region", "resources": "$.resources", "source": "$.source", "time": "$.time", "version": "$.version" } ``` And inside `Template`, you will put this json: ```json { "version": "", "id": "", "detail-type": "", "source": "", "account": "", "time": "