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