├── .eslintrc ├── .github └── workflows │ ├── ci.yml │ ├── release-please.yaml │ └── release.yaml ├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── bin └── cli.js ├── index.d.ts ├── index.js ├── lib ├── context.js ├── event-handler.js ├── function-loader.js ├── health-check.js ├── invocation-handler.js ├── invoker.js ├── metrics.js ├── request-handler.js ├── types.d.ts └── utils.js ├── package-lock.json ├── package.json ├── sample └── index.js ├── sbom.json ├── test ├── fixtures │ ├── async │ │ └── index.js │ ├── cjs-module │ │ ├── index.js │ │ └── package.json │ ├── cloud-event │ │ ├── binary.js │ │ ├── index.js │ │ └── with-response.js │ ├── content-type │ │ └── index.js │ ├── esm-module-mjs │ │ ├── index.mjs │ │ └── package.json │ ├── esm-module-type-module │ │ ├── index.js │ │ └── package.json │ ├── esm-module-type-up-dir │ │ ├── build │ │ │ └── index.js │ │ └── package.json │ ├── funcyaml │ │ ├── func.yaml │ │ └── index.js │ ├── http-get │ │ ├── .npmrc │ │ ├── index.js │ │ ├── package-lock.json │ │ └── package.json │ ├── http-post │ │ └── index.js │ ├── json-input │ │ └── index.js │ ├── knative.gz │ ├── knative.jpg │ ├── knative.png │ ├── openwhisk-properties │ │ └── index.js │ ├── query-params │ │ └── index.js │ ├── response-code │ │ └── index.js │ ├── response-header │ │ └── index.js │ ├── response-structured │ │ └── index.js │ └── taco.gif ├── test-binary-data.js ├── test-context.js ├── test-errors.js ├── test-function-loading.js ├── test-http-body.js ├── test-lifecycle.js ├── test-metrics.js ├── test-scope.js ├── test.js └── types │ ├── context.test-d.ts │ └── index.test-d.ts └── tsconfig.json /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "@typescript-eslint/parser", 3 | "parserOptions": { 4 | "ecmaVersion": 2020, 5 | "sourceType": "module" 6 | }, 7 | "extends": [ 8 | "prettier" 9 | ], 10 | "env": { 11 | "es2020": true, 12 | "node": true 13 | }, 14 | "rules": { 15 | "standard/no-callback-literal": "off", 16 | "arrow-spacing": "error", 17 | "arrow-parens": ["error", "as-needed"], 18 | "arrow-body-style": ["error", "as-needed"], 19 | "prefer-template": "error", 20 | "max-len": ["warn", { "code": 120 }], 21 | "no-unused-vars": ["warn", { 22 | "argsIgnorePattern": "^_$|^e$|^reject$|^resolve$" 23 | }], 24 | "no-console": ["error", { 25 | "allow": ["warn", "error"] 26 | }], 27 | "valid-jsdoc": "error", 28 | "semi": ["error", "always"], 29 | "quotes": ["error", "single", { "allowTemplateLiterals": true }], 30 | "space-before-function-paren": ["warn", "never"] 31 | } 32 | 33 | } -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: Node.js CI 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | 9 | jobs: 10 | build: 11 | strategy: 12 | matrix: 13 | node-version: [20.x, 22.x, 24.x] 14 | os: [ubuntu-latest, windows-latest] 15 | runs-on: ${{ matrix.os }} 16 | steps: 17 | - uses: actions/checkout@v3 18 | - name: Use Node.js ${{ matrix.node-version }} 19 | uses: actions/setup-node@v3 20 | with: 21 | node-version: ${{ matrix.node-version }} 22 | - run: npm ci 23 | - run: npm run build --if-present 24 | - run: npm test 25 | - name: Coveralls Parallel 26 | uses: coverallsapp/github-action@master 27 | with: 28 | github-token: ${{ secrets.github_token }} 29 | flag-name: run-${{ matrix.node-version }} 30 | parallel: true 31 | finish: 32 | needs: build 33 | runs-on: ubuntu-latest 34 | steps: 35 | - name: Coveralls Finished 36 | uses: coverallsapp/github-action@master 37 | with: 38 | github-token: ${{ secrets.github_token }} 39 | parallel-finished: true -------------------------------------------------------------------------------- /.github/workflows/release-please.yaml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | branches: 4 | - main 5 | name: release-please 6 | jobs: 7 | release-please: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: GoogleCloudPlatform/release-please-action@v3 11 | id: release 12 | with: 13 | token: ${{ secrets.NODESHIFT_RELEASES_TOKEN }} 14 | release-type: node 15 | bump-minor-pre-major: "true" 16 | package-name: faas-js-runtime 17 | changelog-types: '[{"type":"enhancement","section":"Features","hidden":false},{"type":"fix","section":"Bug Fixes","hidden":false},{"type":"chore","section":"Miscellaneous","hidden":false},{"type":"cleanup","section":"Miscellaneous","hidden":false},{"type":"api-change","section":"API Changes","hidden":false},{"type":"documentation","section":"Documentation","hidden":false},{"type":"techdebt","section":"Miscellaneous","hidden":false},{"type":"proposal","section":"Miscellaneous","hidden":false},{"type":"feat","section":"Features","hidden":false}]' 18 | - uses: actions/checkout@v2 19 | with: 20 | ref: release-please--branches--main--components--faas-js-runtime 21 | - name: Update package SBOM 22 | if: ${{steps.release.outputs.release-created}} == 'false' 23 | run: | 24 | git config --global user.name "NodeShift Bot (As Luke Holmquist)" 25 | git config --global user.email "lholmqui@redhat.com" 26 | npm run sbom 27 | git add . 28 | git commit --signoff -m "chore: update SBOM" 29 | git push origin release-please--branches--main--components--faas-js-runtime 30 | -------------------------------------------------------------------------------- /.github/workflows/release.yaml: -------------------------------------------------------------------------------- 1 | name: Publish to npmjs 2 | on: 3 | release: 4 | types: [created] 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | permissions: 9 | contents: read 10 | id-token: write 11 | steps: 12 | - uses: actions/checkout@v3 13 | - uses: actions/setup-node@v3 14 | with: 15 | node-version: '22.x' 16 | registry-url: 'https://registry.npmjs.org' 17 | - run: npm install -g npm 18 | - run: npm ci 19 | - run: npm publish 20 | env: 21 | NODE_AUTH_TOKEN: ${{ secrets.FAAS_JS_RUNTIME_PUBLISH }} 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .nyc_output 3 | coverage 4 | test/fixtures/*/package-lock.json 5 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. 4 | 5 | ## [2.5.0](https://github.com/nodeshift/faas-js-runtime/compare/v2.4.1...v2.5.0) (2025-02-17) 6 | 7 | 8 | ### Features 9 | 10 | * Add configurable request body size limit option ([#380](https://github.com/nodeshift/faas-js-runtime/issues/380)) ([6c8055f](https://github.com/nodeshift/faas-js-runtime/commit/6c8055f7c23a7d94e6424850058b71ee327d5ce0)) 11 | 12 | 13 | ### Miscellaneous 14 | 15 | * update codecoverage to use coveralls instead of codecov ([#381](https://github.com/nodeshift/faas-js-runtime/issues/381)) ([9a7da28](https://github.com/nodeshift/faas-js-runtime/commit/9a7da28290f85b346f334cfe23a89b7c20ec1567)) 16 | 17 | ## [2.4.1](https://github.com/nodeshift/faas-js-runtime/compare/v2.4.0...v2.4.1) (2024-11-18) 18 | 19 | 20 | ### Bug Fixes 21 | 22 | * Fix dereference of undefined ([#363](https://github.com/nodeshift/faas-js-runtime/issues/363)) ([473a23f](https://github.com/nodeshift/faas-js-runtime/commit/473a23f18209a8a4066f664c85e9b8176c898042)) 23 | 24 | 25 | ### Miscellaneous 26 | 27 | * add node 22 to the list of supported versions ([#376](https://github.com/nodeshift/faas-js-runtime/issues/376)) ([0a8b8de](https://github.com/nodeshift/faas-js-runtime/commit/0a8b8de95fe034c355509a9e255f397cb625a6f3)) 28 | 29 | ## [2.4.0](https://github.com/nodeshift/faas-js-runtime/compare/v2.3.0...v2.4.0) (2024-06-20) 30 | 31 | 32 | ### Features 33 | 34 | * Enable OPTIONS preflight and a method to enable cros origins ([#359](https://github.com/nodeshift/faas-js-runtime/issues/359)) ([b3fa2b9](https://github.com/nodeshift/faas-js-runtime/commit/b3fa2b9bb1d53f7c62ef839c01b4c62ecf62aa1c)) 35 | 36 | 37 | ### Bug Fixes 38 | 39 | * upgrade fastify-raw-body from 4.2.1 to 4.3.0 ([#345](https://github.com/nodeshift/faas-js-runtime/issues/345)) ([4756c01](https://github.com/nodeshift/faas-js-runtime/commit/4756c01b95e38d7206d9c1eb96d3b2dae896bc15)) 40 | 41 | ## [2.3.0](https://github.com/nodeshift/faas-js-runtime/compare/v2.2.3...v2.3.0) (2024-02-28) 42 | 43 | 44 | ### Features 45 | 46 | * upgrade @typescript-eslint/parser from 5.61.0 to 6.0.0 ([#293](https://github.com/nodeshift/faas-js-runtime/issues/293)) ([c0497cd](https://github.com/nodeshift/faas-js-runtime/commit/c0497cdd421d9b3bb29acd5af6734f61895a3f83)) 47 | * upgrade cloudevents from 7.0.2 to 8.0.0 ([#304](https://github.com/nodeshift/faas-js-runtime/issues/304)) ([5472036](https://github.com/nodeshift/faas-js-runtime/commit/54720367111d29e4693c1595a8b7cfa7e8c47d0b)) 48 | * upgrade prom-client from 14.1.1 to 15.0.0 ([#326](https://github.com/nodeshift/faas-js-runtime/issues/326)) ([0612b9f](https://github.com/nodeshift/faas-js-runtime/commit/0612b9f825b6f61ddeda4472d26b119aca7ba42f)) 49 | 50 | 51 | ### Bug Fixes 52 | 53 | * Change charset parameter value from "utf8" to "utf-8" in default content-type headers ([#330](https://github.com/nodeshift/faas-js-runtime/issues/330)) ([ed9e7df](https://github.com/nodeshift/faas-js-runtime/commit/ed9e7dfb93a75adda9166b7ac61c94ef77a5cd76)) 54 | * upgrade @cyclonedx/cyclonedx-npm from 1.12.1 to 1.13.0 ([#291](https://github.com/nodeshift/faas-js-runtime/issues/291)) ([656b753](https://github.com/nodeshift/faas-js-runtime/commit/656b753e838c22bffee4e1755dbed1e6f67c56af)) 55 | * upgrade @cyclonedx/cyclonedx-npm from 1.13.0 to 1.16.1 ([#337](https://github.com/nodeshift/faas-js-runtime/issues/337)) ([cbaf6b4](https://github.com/nodeshift/faas-js-runtime/commit/cbaf6b443eacb3f2753d79f04f50d34346ee1d75)) 56 | * upgrade @types/node from 20.3.3 to 20.4.1 ([#294](https://github.com/nodeshift/faas-js-runtime/issues/294)) ([aafa51e](https://github.com/nodeshift/faas-js-runtime/commit/aafa51e8f6a957dbb0f9f8ec93d156dfaad46108)) 57 | * upgrade @types/node from 20.4.1 to 20.4.2 ([#298](https://github.com/nodeshift/faas-js-runtime/issues/298)) ([bc59e1b](https://github.com/nodeshift/faas-js-runtime/commit/bc59e1b0ab1b7bb9975aa7c0b9f13aca113021a5)) 58 | * upgrade @types/node from 20.4.2 to 20.4.4 ([#303](https://github.com/nodeshift/faas-js-runtime/issues/303)) ([a033281](https://github.com/nodeshift/faas-js-runtime/commit/a033281767fbefa8bb224105d6a7fd09daa33d13)) 59 | * upgrade @types/node from 20.4.4 to 20.4.5 ([#305](https://github.com/nodeshift/faas-js-runtime/issues/305)) ([d297261](https://github.com/nodeshift/faas-js-runtime/commit/d2972619172b12cf918dc4c6dc9e327d15b37011)) 60 | * upgrade @types/node from 20.4.5 to 20.4.7 ([#309](https://github.com/nodeshift/faas-js-runtime/issues/309)) ([74b1e22](https://github.com/nodeshift/faas-js-runtime/commit/74b1e22faa11c4f1ae5a7c64aded70cd726a5826)) 61 | * upgrade @typescript-eslint/parser from 6.4.0 to 6.4.1 ([#313](https://github.com/nodeshift/faas-js-runtime/issues/313)) ([4a2d964](https://github.com/nodeshift/faas-js-runtime/commit/4a2d96465afd6feddbdb1e380c454b869239c0e9)) 62 | * upgrade cloudevents from 7.0.1 to 7.0.2 ([#295](https://github.com/nodeshift/faas-js-runtime/issues/295)) ([40869fb](https://github.com/nodeshift/faas-js-runtime/commit/40869fb5f2c160c4f0045eb3d0f88caeb41373e5)) 63 | * upgrade eslint from 8.44.0 to 8.45.0 ([#297](https://github.com/nodeshift/faas-js-runtime/issues/297)) ([1131635](https://github.com/nodeshift/faas-js-runtime/commit/11316354f96a1966bf701b3ea373a48869308223)) 64 | * upgrade eslint from 8.45.0 to 8.46.0 ([#307](https://github.com/nodeshift/faas-js-runtime/issues/307)) ([00658cf](https://github.com/nodeshift/faas-js-runtime/commit/00658cffc4e42d0df9ee7cdc0a22f4a76441da15)) 65 | * upgrade fastify from 4.19.2 to 4.20.0 ([#300](https://github.com/nodeshift/faas-js-runtime/issues/300)) ([9dc620d](https://github.com/nodeshift/faas-js-runtime/commit/9dc620d21c7804b62af211316b5cf382866e94ea)) 66 | * upgrade fastify from 4.20.0 to 4.21.0 ([#308](https://github.com/nodeshift/faas-js-runtime/issues/308)) ([a3f6fe9](https://github.com/nodeshift/faas-js-runtime/commit/a3f6fe9427127d6d2a973c722bb9eb69d74f20a2)) 67 | * upgrade fastify-raw-body from 4.2.0 to 4.2.1 ([#299](https://github.com/nodeshift/faas-js-runtime/issues/299)) ([9e5e5bc](https://github.com/nodeshift/faas-js-runtime/commit/9e5e5bcdb73de8728e46d4e365bbb77cce0c4a52)) 68 | * upgrade tape from 5.6.4 to 5.6.5 ([#296](https://github.com/nodeshift/faas-js-runtime/issues/296)) ([cd0ef07](https://github.com/nodeshift/faas-js-runtime/commit/cd0ef078297189852aa0a8f967bf359d49b02cda)) 69 | * upgrade tape from 5.6.5 to 5.6.6 ([#302](https://github.com/nodeshift/faas-js-runtime/issues/302)) ([ca499c0](https://github.com/nodeshift/faas-js-runtime/commit/ca499c0f82a77a509bc2f5333a36f2494e7ba9ec)) 70 | * upgrade tape from 5.6.6 to 5.7.4 ([#339](https://github.com/nodeshift/faas-js-runtime/issues/339)) ([0ae91e0](https://github.com/nodeshift/faas-js-runtime/commit/0ae91e09fe4c163bcc90f0523399e8bad26d3d84)) 71 | * upgrade typescript from 5.1.6 to 5.2.2 ([#314](https://github.com/nodeshift/faas-js-runtime/issues/314)) ([f602b47](https://github.com/nodeshift/faas-js-runtime/commit/f602b47f66cee71952eafec318a0fe144fc93e4a)) 72 | 73 | ## [2.2.3](https://github.com/nodeshift/faas-js-runtime/compare/v2.2.2...v2.2.3) (2023-07-26) 74 | 75 | 76 | ### Bug Fixes 77 | 78 | * fix Do not overwrite headers of StructuredReturn ([#288](https://github.com/nodeshift/faas-js-runtime/pull/288)) ([a2c37a1](https://github.com/nodeshift/faas-js-runtime/commit/a2c37a115b2ca2be0667549a512e336bba44922e)) 79 | * upgrade @types/node from 20.2.5 to 20.3.2 ([#274](https://github.com/nodeshift/faas-js-runtime/issues/274)) ([cfd1b55](https://github.com/nodeshift/faas-js-runtime/commit/cfd1b552d2191726e5faca0668d1bf57423f3fb8)) 80 | * upgrade @types/node from 20.3.2 to 20.3.3 ([#282](https://github.com/nodeshift/faas-js-runtime/issues/282)) ([fa9ad33](https://github.com/nodeshift/faas-js-runtime/commit/fa9ad33a7160cca9320e1a2f210aa8923c59e857)) 81 | * upgrade @typescript-eslint/parser from 5.60.0 to 5.60.1 ([#275](https://github.com/nodeshift/faas-js-runtime/issues/275)) ([c9dd3ef](https://github.com/nodeshift/faas-js-runtime/commit/c9dd3ef8458fbf936bca2350473610806401fc79)) 82 | * upgrade @typescript-eslint/parser from 5.60.1 to 5.61.0 ([#286](https://github.com/nodeshift/faas-js-runtime/issues/286)) ([97258a0](https://github.com/nodeshift/faas-js-runtime/commit/97258a0d431cd6ec9ae481d9482ff927e1a4d6f7)) 83 | * upgrade eslint from 8.43.0 to 8.44.0 ([#283](https://github.com/nodeshift/faas-js-runtime/issues/283)) ([cbeb454](https://github.com/nodeshift/faas-js-runtime/commit/cbeb45455cc8c8cf688a4d8d0a744e38cf501a52)) 84 | * upgrade fastify from 4.18.0 to 4.19.0 ([#281](https://github.com/nodeshift/faas-js-runtime/issues/281)) ([395287d](https://github.com/nodeshift/faas-js-runtime/commit/395287d2054820f50ad9bc72a2479c5df6fc60e4)) 85 | * upgrade fastify from 4.19.0 to 4.19.2 ([#289](https://github.com/nodeshift/faas-js-runtime/issues/289)) ([6a3b33a](https://github.com/nodeshift/faas-js-runtime/commit/6a3b33a660f84af721c0adae27acab861f1024e5)) 86 | * upgrade tape from 5.6.3 to 5.6.4 ([#284](https://github.com/nodeshift/faas-js-runtime/issues/284)) ([a11c9d7](https://github.com/nodeshift/faas-js-runtime/commit/a11c9d7f7e008c256d0cf617bd3bfa7d42ba6e5f)) 87 | * upgrade typescript from 5.1.3 to 5.1.6 ([#280](https://github.com/nodeshift/faas-js-runtime/issues/280)) ([37f89a3](https://github.com/nodeshift/faas-js-runtime/commit/37f89a3d2427213627cd05576f7ad31edc145ef6)) 88 | 89 | 90 | ### Miscellaneous 91 | 92 | * add sbom generation to release-please action ([#278](https://github.com/nodeshift/faas-js-runtime/issues/278)) ([c8440f5](https://github.com/nodeshift/faas-js-runtime/commit/c8440f5d6b62624e0f8ed69ca392ec4cdd11c41b)) 93 | 94 | ## [2.2.2](https://github.com/nodeshift/faas-js-runtime/compare/v2.2.1...v2.2.2) (2023-07-14) 95 | 96 | 97 | ### Bug Fixes 98 | 99 | * temporarily removes the prePublish step allowing us to publish a new version and for users to use this module. ([#270](https://github.com/nodeshift/faas-js-runtime/issues/270)) ([cea807f](https://github.com/nodeshift/faas-js-runtime/commit/cea807f0a9e7d834226238b304ca2ed0dff52490)) 100 | 101 | ## [2.2.1](https://github.com/nodeshift/faas-js-runtime/compare/v2.2.0...v2.2.1) (2023-07-13) 102 | 103 | 104 | ### Miscellaneous 105 | 106 | * change sbom generation to happen on `prepublishOnly` ([#268](https://github.com/nodeshift/faas-js-runtime/issues/268)) ([9fdef57](https://github.com/nodeshift/faas-js-runtime/commit/9fdef579a8111fa3094f556f33f0159f44ed2492)) 107 | 108 | ## [2.2.0](https://github.com/nodeshift/faas-js-runtime/compare/v2.1.2...v2.2.0) (2023-07-11) 109 | 110 | 111 | ### Features 112 | 113 | * Add option of adding rawBody to context ([#116](https://github.com/nodeshift/faas-js-runtime/issues/116))([c8cfa92](https://github.com/nodeshift/faas-js-runtime/commit/c8cfa92e9792af2fd5ede2f014f709870a1cb6b8)) 114 | 115 | * upgrade commander from 10.0.0 to 11.0.0 ([#261](https://github.com/nodeshift/faas-js-runtime/issues/261)) ([fd53245](https://github.com/nodeshift/faas-js-runtime/commit/fd532451308f41d211a20479520375f3ab3a8151)) 116 | 117 | 118 | ### Bug Fixes 119 | 120 | * upgrade @cyclonedx/cyclonedx-npm from 1.12.0 to 1.12.1 ([#264](https://github.com/nodeshift/faas-js-runtime/issues/264)) ([7c7fefc](https://github.com/nodeshift/faas-js-runtime/commit/7c7fefc83ac51b82767f0d83b2663210e704aa65)) 121 | * upgrade @typescript-eslint/eslint-plugin from 5.59.7 to 5.59.11 ([#257](https://github.com/nodeshift/faas-js-runtime/issues/257)) ([31ec689](https://github.com/nodeshift/faas-js-runtime/commit/31ec689ed16c85e551f35451139ba4f3391b8a48)) 122 | * upgrade @typescript-eslint/parser from 5.59.9 to 5.60.0 ([#266](https://github.com/nodeshift/faas-js-runtime/issues/266)) ([e3ef6ee](https://github.com/nodeshift/faas-js-runtime/commit/e3ef6ee1b7d2d3c7a33dfe3ccd1eefc70368f7b7)) 123 | * upgrade eslint from 8.42.0 to 8.43.0 ([#262](https://github.com/nodeshift/faas-js-runtime/issues/262)) ([675ccf5](https://github.com/nodeshift/faas-js-runtime/commit/675ccf57d4c40c97aa561fb9f3b39ea03026f0cc)) 124 | * upgrade fastify from 4.17.0 to 4.18.0 ([#263](https://github.com/nodeshift/faas-js-runtime/issues/263)) ([520435e](https://github.com/nodeshift/faas-js-runtime/commit/520435e0ad81a17ee1a8f3b2c51e5aeab30e1a02)) 125 | 126 | ## [2.1.2](https://github.com/nodeshift/faas-js-runtime/compare/v2.1.1...v2.1.2) (2023-06-29) 127 | 128 | 129 | ### Miscellaneous 130 | 131 | * update the token that release-please runs with. ([#255](https://github.com/nodeshift/faas-js-runtime/issues/255)) ([16a87c8](https://github.com/nodeshift/faas-js-runtime/commit/16a87c866a3d5150824b4c96c1e73166152967d4)) 132 | 133 | ## [2.1.1](https://github.com/nodeshift/faas-js-runtime/compare/v2.1.0...v2.1.1) (2023-06-29) 134 | 135 | 136 | ### Miscellaneous 137 | 138 | * update the release type to published instead of created ([#253](https://github.com/nodeshift/faas-js-runtime/issues/253)) ([35893c8](https://github.com/nodeshift/faas-js-runtime/commit/35893c8f5e008d7e97560dfbf63c8cd951ce184d)) 139 | 140 | ## [2.1.0](https://github.com/nodeshift/faas-js-runtime/compare/v2.0.0...v2.1.0) (2023-06-29) 141 | 142 | 143 | ### Features 144 | 145 | * upgrade @types/node from 18.14.6 to 20.1.4 ([#236](https://github.com/nodeshift/faas-js-runtime/issues/236)) ([11e616e](https://github.com/nodeshift/faas-js-runtime/commit/11e616e6a08afc5cb0d6e4d0542aa75598730258)) 146 | * upgrade typescript from 4.9.5 to 5.0.4 ([#235](https://github.com/nodeshift/faas-js-runtime/issues/235)) ([26427d4](https://github.com/nodeshift/faas-js-runtime/commit/26427d4cddfa53d0e952fbc8ae6abfd6090ed241)) 147 | 148 | 149 | ### Bug Fixes 150 | 151 | * upgrade @types/node from 20.1.4 to 20.2.5 ([#251](https://github.com/nodeshift/faas-js-runtime/issues/251)) ([6aa97e8](https://github.com/nodeshift/faas-js-runtime/commit/6aa97e8a2b912ab0bb5b207983d7d850d6db6c29)) 152 | * upgrade @typescript-eslint/eslint-plugin from 5.49.0 to 5.59.6 ([#240](https://github.com/nodeshift/faas-js-runtime/issues/240)) ([c7d6284](https://github.com/nodeshift/faas-js-runtime/commit/c7d6284c9dccc30eeb163387666eb7e6fa78a87d)) 153 | * upgrade @typescript-eslint/eslint-plugin from 5.59.6 to 5.59.7 ([#245](https://github.com/nodeshift/faas-js-runtime/issues/245)) ([0e9d9d2](https://github.com/nodeshift/faas-js-runtime/commit/0e9d9d2167011b50e9d16e2017696f2eeb8a7ac1)) 154 | * upgrade @typescript-eslint/parser from 5.58.0 to 5.59.5 ([#237](https://github.com/nodeshift/faas-js-runtime/issues/237)) ([9f85cd8](https://github.com/nodeshift/faas-js-runtime/commit/9f85cd84434c77b6653ce6d7128da8485960ba4e)) 155 | * upgrade @typescript-eslint/parser from 5.59.5 to 5.59.9 ([#249](https://github.com/nodeshift/faas-js-runtime/issues/249)) ([8ca7e29](https://github.com/nodeshift/faas-js-runtime/commit/8ca7e290bc23b43ab9a3d8957ab1f8c7da626307)) 156 | * upgrade cloudevents from 7.0.0 to 7.0.1 ([#246](https://github.com/nodeshift/faas-js-runtime/issues/246)) ([47cbedc](https://github.com/nodeshift/faas-js-runtime/commit/47cbedcc02948a8f4817696ddd73374b29e32e54)) 157 | * upgrade eslint from 8.40.0 to 8.41.0 ([#244](https://github.com/nodeshift/faas-js-runtime/issues/244)) ([e331f46](https://github.com/nodeshift/faas-js-runtime/commit/e331f46eddd27d94862c1722a8fb08fd8bcb22c6)) 158 | * upgrade eslint from 8.41.0 to 8.42.0 ([#252](https://github.com/nodeshift/faas-js-runtime/issues/252)) ([a07a8a2](https://github.com/nodeshift/faas-js-runtime/commit/a07a8a2ca31eb4971841704dd3a4afc5a62903b8)) 159 | * upgrade fastify from 4.16.0 to 4.17.0 ([#238](https://github.com/nodeshift/faas-js-runtime/issues/238)) ([3d239ec](https://github.com/nodeshift/faas-js-runtime/commit/3d239ec4ab27686f2bb82a3fdb745ead5790dff4)) 160 | * upgrade qs from 6.11.1 to 6.11.2 ([#243](https://github.com/nodeshift/faas-js-runtime/issues/243)) ([c8448a5](https://github.com/nodeshift/faas-js-runtime/commit/c8448a5120ff1eaf3b159fdf54a0ca416cd0b72c)) 161 | * upgrade typescript from 5.0.4 to 5.1.3 ([#247](https://github.com/nodeshift/faas-js-runtime/issues/247)) ([6e01336](https://github.com/nodeshift/faas-js-runtime/commit/6e01336880735f32b3e647aa4e2013473d5a5e4f)) 162 | 163 | 164 | ### Miscellaneous 165 | 166 | * move publish to its own workflow and add sigstore provenance ([#248](https://github.com/nodeshift/faas-js-runtime/issues/248)) ([3f34b4f](https://github.com/nodeshift/faas-js-runtime/commit/3f34b4f84af1fe596c190ae4f618de781627e185)) 167 | 168 | ## [2.0.0](https://github.com/nodeshift/faas-js-runtime/compare/v1.1.0...v2.0.0) (2023-06-01) 169 | 170 | 171 | ### ⚠ BREAKING CHANGES 172 | 173 | * removal of node 14 ([#233](https://github.com/nodeshift/faas-js-runtime/issues/233)) 174 | 175 | ### Features 176 | 177 | * adds sbom.json as install artifact ([#229](https://github.com/nodeshift/faas-js-runtime/issues/229)) ([1349641](https://github.com/nodeshift/faas-js-runtime/commit/134964185bab2abe80cc86eaba00439e0a6ca128)) 178 | * removal of node 14 ([#233](https://github.com/nodeshift/faas-js-runtime/issues/233)) ([ae9a27c](https://github.com/nodeshift/faas-js-runtime/commit/ae9a27cc1eb53612ba76da490d834a008f635fc3)) 179 | * upgrade cloudevents from 6.0.4 to 7.0.0 ([#230](https://github.com/nodeshift/faas-js-runtime/issues/230)) ([28ae7e5](https://github.com/nodeshift/faas-js-runtime/commit/28ae7e5894951f319dfc84e70234ff81536fbedd)) 180 | 181 | 182 | ### Bug Fixes 183 | 184 | * upgrade @typescript-eslint/parser from 5.54.0 to 5.54.1 ([#210](https://github.com/nodeshift/faas-js-runtime/issues/210)) ([deb9f99](https://github.com/nodeshift/faas-js-runtime/commit/deb9f99444630eb440ea1d270e73ea47b4c745a1)) 185 | * upgrade @typescript-eslint/parser from 5.54.1 to 5.58.0 ([#225](https://github.com/nodeshift/faas-js-runtime/issues/225)) ([4c30aef](https://github.com/nodeshift/faas-js-runtime/commit/4c30aef02d70c13e71bf74de1c8fbf8012a3cfd2)) 186 | * upgrade eslint from 8.36.0 to 8.37.0 ([#221](https://github.com/nodeshift/faas-js-runtime/issues/221)) ([2007278](https://github.com/nodeshift/faas-js-runtime/commit/20072780959bd0c31e0974b05a2db071a7c433d5)) 187 | * upgrade eslint from 8.37.0 to 8.38.0 ([#224](https://github.com/nodeshift/faas-js-runtime/issues/224)) ([db9fe88](https://github.com/nodeshift/faas-js-runtime/commit/db9fe8893f7a6e4fe5da0208d3e64b90d83427e1)) 188 | * upgrade eslint from 8.38.0 to 8.39.0 ([#227](https://github.com/nodeshift/faas-js-runtime/issues/227)) ([a03e828](https://github.com/nodeshift/faas-js-runtime/commit/a03e8283f99cf0ad504e35a0aa459b7bb3175f87)) 189 | * upgrade eslint from 8.39.0 to 8.40.0 ([#232](https://github.com/nodeshift/faas-js-runtime/issues/232)) ([db8c900](https://github.com/nodeshift/faas-js-runtime/commit/db8c900468d5cad8ab2f743f947c88c0980c0f0b)) 190 | * upgrade fastify from 4.15.0 to 4.16.0 ([#228](https://github.com/nodeshift/faas-js-runtime/issues/228)) ([3d3c8ab](https://github.com/nodeshift/faas-js-runtime/commit/3d3c8ab528febccef3e99484ba3584cc39b94b72)) 191 | * upgrade tsd from 0.26.0 to 0.27.0 ([#213](https://github.com/nodeshift/faas-js-runtime/issues/213)) ([53762a3](https://github.com/nodeshift/faas-js-runtime/commit/53762a36aa650b13d12559679fd85a0fa68a5bac)) 192 | * upgrade tsd from 0.27.0 to 0.28.1 ([#231](https://github.com/nodeshift/faas-js-runtime/issues/231)) ([da15cf4](https://github.com/nodeshift/faas-js-runtime/commit/da15cf4dbcd21e5c57887c81383b2a4b0eef18cb)) 193 | 194 | 195 | ### Miscellaneous 196 | 197 | * improved error handling ([#223](https://github.com/nodeshift/faas-js-runtime/issues/223)) ([ef6bc77](https://github.com/nodeshift/faas-js-runtime/commit/ef6bc7765955b44ca2b2065ae1beafaeeffc49d3)) 198 | 199 | ## [1.1.0](https://github.com/nodeshift/faas-js-runtime/compare/v1.0.0...v1.1.0) (2023-04-17) 200 | 201 | 202 | ### Features 203 | 204 | * add lifecycle and endpoint customization ([#203](https://github.com/nodeshift/faas-js-runtime/issues/203)) ([ff20b40](https://github.com/nodeshift/faas-js-runtime/commit/ff20b404a965cf58edff1a606d8c1b0b48dc5d68)) 205 | 206 | 207 | ### Bug Fixes 208 | 209 | * upgrade @types/node from 18.11.18 to 18.11.19 ([#183](https://github.com/nodeshift/faas-js-runtime/issues/183)) ([edaa3ab](https://github.com/nodeshift/faas-js-runtime/commit/edaa3ab790172377829dd75f906d2524da7132b7)) 210 | * upgrade @types/node from 18.11.19 to 18.13.0 ([#186](https://github.com/nodeshift/faas-js-runtime/issues/186)) ([7eab05c](https://github.com/nodeshift/faas-js-runtime/commit/7eab05c188356614899d10cc52f4791e3e2e3293)) 211 | * upgrade @types/node from 18.13.0 to 18.14.0 ([#192](https://github.com/nodeshift/faas-js-runtime/issues/192)) ([3a689af](https://github.com/nodeshift/faas-js-runtime/commit/3a689af6186a166ca47d35bf360629452c6b1ebd)) 212 | * upgrade @types/node from 18.14.0 to 18.14.1 ([#196](https://github.com/nodeshift/faas-js-runtime/issues/196)) ([878968c](https://github.com/nodeshift/faas-js-runtime/commit/878968c2da5b3bd6696c601a87548ea98fba593f)) 213 | * upgrade @types/node from 18.14.1 to 18.14.2 ([#200](https://github.com/nodeshift/faas-js-runtime/issues/200)) ([f12c148](https://github.com/nodeshift/faas-js-runtime/commit/f12c14892066f9dacbf936e6bc8a2bd5147816f7)) 214 | * upgrade @types/node from 18.14.2 to 18.14.6 ([#204](https://github.com/nodeshift/faas-js-runtime/issues/204)) ([46bd143](https://github.com/nodeshift/faas-js-runtime/commit/46bd1438c87613eecd2701b8622ccbe13eb53002)) 215 | * upgrade @typescript-eslint/parser from 5.49.0 to 5.50.0 ([#180](https://github.com/nodeshift/faas-js-runtime/issues/180)) ([b2c7ca9](https://github.com/nodeshift/faas-js-runtime/commit/b2c7ca93bcfbc0d2dc24d5d70e5585077a25f1a3)) 216 | * upgrade @typescript-eslint/parser from 5.50.0 to 5.51.0 ([#185](https://github.com/nodeshift/faas-js-runtime/issues/185)) ([09cfcf4](https://github.com/nodeshift/faas-js-runtime/commit/09cfcf4a3c30b311ac90c82886c8f0554e7c2073)) 217 | * upgrade @typescript-eslint/parser from 5.51.0 to 5.52.0 ([#190](https://github.com/nodeshift/faas-js-runtime/issues/190)) ([f597f04](https://github.com/nodeshift/faas-js-runtime/commit/f597f049fad19ab42473e7ee0c7ac3f43c024546)) 218 | * upgrade @typescript-eslint/parser from 5.52.0 to 5.53.0 ([#195](https://github.com/nodeshift/faas-js-runtime/issues/195)) ([f529bb1](https://github.com/nodeshift/faas-js-runtime/commit/f529bb163e9346ba2a2cd5cdf5f46b34829279d9)) 219 | * upgrade @typescript-eslint/parser from 5.53.0 to 5.54.0 ([#202](https://github.com/nodeshift/faas-js-runtime/issues/202)) ([974cca3](https://github.com/nodeshift/faas-js-runtime/commit/974cca31184e5858fececca472eb2e874eaee3a9)) 220 | * upgrade cloudevents from 6.0.3 to 6.0.4 ([#191](https://github.com/nodeshift/faas-js-runtime/issues/191)) ([d5f96da](https://github.com/nodeshift/faas-js-runtime/commit/d5f96da4f40ebd095fb7ff990c60799c57047d2e)) 221 | * upgrade eslint from 8.33.0 to 8.34.0 ([#188](https://github.com/nodeshift/faas-js-runtime/issues/188)) ([3f2aa5a](https://github.com/nodeshift/faas-js-runtime/commit/3f2aa5aa51edd1048ec2511f341bf7b6dba83598)) 222 | * upgrade eslint from 8.34.0 to 8.35.0 ([#199](https://github.com/nodeshift/faas-js-runtime/issues/199)) ([3cc8aaf](https://github.com/nodeshift/faas-js-runtime/commit/3cc8aaf313b53bd0d72b59928010c7263713bfc0)) 223 | * upgrade eslint from 8.35.0 to 8.36.0 ([#215](https://github.com/nodeshift/faas-js-runtime/issues/215)) ([c6ddf6d](https://github.com/nodeshift/faas-js-runtime/commit/c6ddf6d4221d83a03db947dc022314c96c4353c9)) 224 | * upgrade eslint-config-prettier from 8.6.0 to 8.7.0 ([#206](https://github.com/nodeshift/faas-js-runtime/issues/206)) ([c3e21da](https://github.com/nodeshift/faas-js-runtime/commit/c3e21da7438362f242e2418a3de6b1f9734f55de)) 225 | * upgrade fastify from 4.12.0 to 4.13.0 ([#187](https://github.com/nodeshift/faas-js-runtime/issues/187)) ([4b1e0eb](https://github.com/nodeshift/faas-js-runtime/commit/4b1e0ebcbdfcac244d356ca300e10da8cd7ada12)) 226 | * upgrade fastify from 4.13.0 to 4.14.0 ([#205](https://github.com/nodeshift/faas-js-runtime/issues/205)) ([e924dbb](https://github.com/nodeshift/faas-js-runtime/commit/e924dbb893941108a15e6d65e8e7a4798681da2e)) 227 | * upgrade fastify from 4.14.0 to 4.14.1 ([#212](https://github.com/nodeshift/faas-js-runtime/issues/212)) ([29fdc65](https://github.com/nodeshift/faas-js-runtime/commit/29fdc65e9f37dc957be2c3013de691233f105350)) 228 | * upgrade fastify from 4.14.1 to 4.15.0 ([#220](https://github.com/nodeshift/faas-js-runtime/issues/220)) ([48ae6e4](https://github.com/nodeshift/faas-js-runtime/commit/48ae6e451644e7a8567f81a69fb3d53be044e214)) 229 | * upgrade qs from 6.11.0 to 6.11.1 ([#211](https://github.com/nodeshift/faas-js-runtime/issues/211)) ([a7321c0](https://github.com/nodeshift/faas-js-runtime/commit/a7321c00f335527e917416572575517413903df8)) 230 | * upgrade tsd from 0.25.0 to 0.26.0 ([#208](https://github.com/nodeshift/faas-js-runtime/issues/208)) ([c0fdfe7](https://github.com/nodeshift/faas-js-runtime/commit/c0fdfe7c98c6ab019e506db1ec43fe5f0caf2c2a)) 231 | 232 | 233 | ### Miscellaneous 234 | 235 | * add function debugging instructions to the readme ([#197](https://github.com/nodeshift/faas-js-runtime/issues/197)) ([8883cc0](https://github.com/nodeshift/faas-js-runtime/commit/8883cc044bedc1e0cff228a928698e42e3df49f8)) 236 | * add the node version to the logger output ([#193](https://github.com/nodeshift/faas-js-runtime/issues/193)) ([8cdb40e](https://github.com/nodeshift/faas-js-runtime/commit/8cdb40e0267e0303a195c740c167e649fc257518)) 237 | * bump checkout and setup-node actions to v3 ([#198](https://github.com/nodeshift/faas-js-runtime/issues/198)) ([5fe62d0](https://github.com/nodeshift/faas-js-runtime/commit/5fe62d0f6482853b2fe3f58b47f0a83f87df10c1)) 238 | * update the publish token name for npm. ([#179](https://github.com/nodeshift/faas-js-runtime/issues/179)) ([6706ac0](https://github.com/nodeshift/faas-js-runtime/commit/6706ac0f32bc5e1e4f5da5ef76d4ff9a3c726d4b)) 239 | 240 | ## [1.0.0](https://github.com/nodeshift/faas-js-runtime/compare/v0.10.0...v1.0.0) (2023-02-21) 241 | 242 | 243 | ### Bug Fixes 244 | 245 | * upgrade eslint from 8.32.0 to 8.33.0 ([#175](https://github.com/nodeshift/faas-js-runtime/issues/175)) ([0e1e964](https://github.com/nodeshift/faas-js-runtime/commit/0e1e96415a05fd89008921a2236cdfddc9aecad1)) 246 | * upgrade typescript from 4.3.5 to 4.9.5 ([#178](https://github.com/nodeshift/faas-js-runtime/issues/178)) ([31b2665](https://github.com/nodeshift/faas-js-runtime/commit/31b266541734c8c00c3688dae7b15d22a4bd2e5d)) 247 | 248 | 249 | ### Miscellaneous 250 | 251 | * release 1.0.0 ([#176](https://github.com/nodeshift/faas-js-runtime/issues/176)) ([14f296f](https://github.com/nodeshift/faas-js-runtime/commit/14f296f2368606b8d70d39ef6567bf07dfdce002)) 252 | 253 | ## [0.10.0](https://github.com/nodeshift/faas-js-runtime/compare/v0.9.7...v0.10.0) (2023-02-16) 254 | 255 | 256 | ### Features 257 | 258 | * upgrade commander from 9.5.0 to 10.0.0 ([#168](https://github.com/nodeshift/faas-js-runtime/issues/168)) ([64a41b2](https://github.com/nodeshift/faas-js-runtime/commit/64a41b2474b16f1aa66081980db023983c98a6cb)) 259 | 260 | 261 | ### Bug Fixes 262 | 263 | * killing the process hangs issue ([#172](https://github.com/nodeshift/faas-js-runtime/issues/172)) ([3ac8b9d](https://github.com/nodeshift/faas-js-runtime/commit/3ac8b9dd6497b173f2208c72884c01ccbd129b6c)), closes [#120](https://github.com/nodeshift/faas-js-runtime/issues/120) 264 | * upgrade commander from 9.4.1 to 9.5.0 ([#164](https://github.com/nodeshift/faas-js-runtime/issues/164)) ([ead35e6](https://github.com/nodeshift/faas-js-runtime/commit/ead35e66f578fedbbafa1c89893c012292f3df1b)) 265 | * upgrade fastify from 4.10.2 to 4.11.0 ([#165](https://github.com/nodeshift/faas-js-runtime/issues/165)) ([f417755](https://github.com/nodeshift/faas-js-runtime/commit/f41775508d03e4bc8e32a8b056ab970a84756bf3)) 266 | * upgrade fastify from 4.11.0 to 4.12.0 ([#171](https://github.com/nodeshift/faas-js-runtime/issues/171)) ([3daf323](https://github.com/nodeshift/faas-js-runtime/commit/3daf3235fa5bd0140e4d31f8b7d5b82df940886b)) 267 | * upgrade prom-client from 14.1.0 to 14.1.1 ([#166](https://github.com/nodeshift/faas-js-runtime/issues/166)) ([9a7b0f3](https://github.com/nodeshift/faas-js-runtime/commit/9a7b0f36ec7206ab5fdf1dac913b01e67d5c885e)) 268 | * upgrade tape from 5.6.1 to 5.6.3 ([#170](https://github.com/nodeshift/faas-js-runtime/issues/170)) ([edc07c5](https://github.com/nodeshift/faas-js-runtime/commit/edc07c586f4458df7455492d042e402686a840f0)) 269 | 270 | 271 | ### Miscellaneous 272 | 273 | * Add the node version support to the readme. ([#174](https://github.com/nodeshift/faas-js-runtime/issues/174)) ([9e14879](https://github.com/nodeshift/faas-js-runtime/commit/9e14879b101c5dccfafe6ecc2d326397ea8846b3)) 274 | * add the type property to the package.json. ([#167](https://github.com/nodeshift/faas-js-runtime/issues/167)) ([9886d87](https://github.com/nodeshift/faas-js-runtime/commit/9886d8735510bc58e2acb5fb00819b55c4aa8774)) 275 | * **deps:** update transitive dependencies ([#148](https://github.com/nodeshift/faas-js-runtime/issues/148)) ([8389cbe](https://github.com/nodeshift/faas-js-runtime/commit/8389cbe9839aa32d8231e79c19b136a160874482)) 276 | * **deps:** updates eslint and friends ([#160](https://github.com/nodeshift/faas-js-runtime/issues/160)) ([880a573](https://github.com/nodeshift/faas-js-runtime/commit/880a57333f4e001e6074ed57cd7751c52707ee5c)) 277 | * **deps:** upgrade eslint-config-prettier from 8.3.0 to 8.6.0 ([#158](https://github.com/nodeshift/faas-js-runtime/issues/158)) ([59390eb](https://github.com/nodeshift/faas-js-runtime/commit/59390ebcace837f0e3e02b7df074042afc44ca24)) 278 | * **deps:** upgrade node-os-utils from 1.3.5 to 1.3.7 ([#159](https://github.com/nodeshift/faas-js-runtime/issues/159)) ([edfe61a](https://github.com/nodeshift/faas-js-runtime/commit/edfe61abf67c76b248104fd561c9f54bc6bbc88c)) 279 | * **deps:** upgrade tape from 5.3.1 to 5.6.1 ([#157](https://github.com/nodeshift/faas-js-runtime/issues/157)) ([3c7acaa](https://github.com/nodeshift/faas-js-runtime/commit/3c7acaaa445bfca447e69bb3e36e7f807687304d)) 280 | * update README.md and package.json for nodeshift ([#173](https://github.com/nodeshift/faas-js-runtime/issues/173)) ([1944025](https://github.com/nodeshift/faas-js-runtime/commit/19440259b7794c71423ef710d40cb6c89221fbcb)) 281 | 282 | ## [0.9.7](https://github.com/boson-project/faas-js-runtime/compare/v0.9.6...v0.9.7) (2023-01-06) 283 | 284 | 285 | ### Bug Fixes 286 | 287 | * modify module loader to look in parent directories ([#144](https://github.com/boson-project/faas-js-runtime/issues/144)) ([2a1e618](https://github.com/boson-project/faas-js-runtime/commit/2a1e618afd0078ceeffa94232c6c55b0584d5842)) 288 | 289 | ## [0.9.6](https://github.com/boson-project/faas-js-runtime/compare/v0.9.5...v0.9.6) (2022-12-09) 290 | 291 | 292 | ### Features 293 | 294 | * Import functions that are written as ES Modules ([#140](https://github.com/boson-project/faas-js-runtime/issues/140)) ([6ac6fff](https://github.com/boson-project/faas-js-runtime/commit/6ac6fffec091e9ed87d74a39f8c8390bc28d8ee6)) 295 | 296 | ## [0.9.5](https://github.com/boson-project/faas-js-runtime/compare/v0.9.4...v0.9.5) (2022-11-09) 297 | 298 | 299 | ### Bug Fixes 300 | 301 | * **types:** use CloudEvent<unknown> for typed events ([#137](https://github.com/boson-project/faas-js-runtime/issues/137)) ([03b1ffe](https://github.com/boson-project/faas-js-runtime/commit/03b1ffe4eb8ffdd3c1070a7a82684f0681bdb65e)) 302 | 303 | ## [0.9.4](https://github.com/boson-project/faas-js-runtime/compare/v0.9.3...v0.9.4) (2022-11-08) 304 | 305 | 306 | ### Bug Fixes 307 | 308 | * **cloudevents:** bump to latest release ([#135](https://github.com/boson-project/faas-js-runtime/issues/135)) ([c7f9331](https://github.com/boson-project/faas-js-runtime/commit/c7f9331de905a17e7b5a7ef1c099a7df690cf91d)) 309 | 310 | ## [0.9.3](https://github.com/boson-project/faas-js-runtime/compare/v0.9.2...v0.9.3) (2022-11-08) 311 | 312 | 313 | ### Miscellaneous 314 | 315 | * **release-please:** bump node version ([#133](https://github.com/boson-project/faas-js-runtime/issues/133)) ([74e231d](https://github.com/boson-project/faas-js-runtime/commit/74e231dde65477e377e73ad009ba86e19445aac0)) 316 | 317 | ## [0.9.2](https://github.com/boson-project/faas-js-runtime/compare/v0.9.1...v0.9.2) (2022-11-03) 318 | 319 | 320 | ### Features 321 | 322 | * **http-functions:** supply HTTP POST body ([#129](https://github.com/boson-project/faas-js-runtime/issues/129)) ([25cdcba](https://github.com/boson-project/faas-js-runtime/commit/25cdcba3d7f58b0d202efc1864455bff855583be)) 323 | 324 | 325 | ### Miscellaneous 326 | 327 | * **coverage:** upload coverage stats to codecov.io ([#127](https://github.com/boson-project/faas-js-runtime/issues/127)) ([4206e31](https://github.com/boson-project/faas-js-runtime/commit/4206e31cbe4a4dcc4a4e322fc7f2b2d66bc2c9e8)) 328 | * **deps:** bump fastify to 4.9.x ([#128](https://github.com/boson-project/faas-js-runtime/issues/128)) ([35403b5](https://github.com/boson-project/faas-js-runtime/commit/35403b54053ea194171ae6fd5afd458926f93ba9)) 329 | * **release-please:** specify changelog headings ([#130](https://github.com/boson-project/faas-js-runtime/issues/130)) ([5ed0cff](https://github.com/boson-project/faas-js-runtime/commit/5ed0cff1f7e8989aa713aa76571f1cbebd6c4830)) 330 | 331 | 332 | ### Documentation 333 | 334 | * **README.md:** update with interfaces and types ([#131](https://github.com/boson-project/faas-js-runtime/issues/131)) ([058fc77](https://github.com/boson-project/faas-js-runtime/commit/058fc77ef0ebf8c74eb48e1f53e7f5e04cfbebe0)) 335 | 336 | ### [0.9.1](https://www.github.com/boson-project/faas-js-runtime/compare/v0.9.0...v0.9.1) (2022-05-27) 337 | 338 | 339 | ### Bug Fixes 340 | 341 | * add an explicit scope to function invocation ([#118](https://www.github.com/boson-project/faas-js-runtime/issues/118)) ([1eb75b3](https://www.github.com/boson-project/faas-js-runtime/commit/1eb75b3cec416cf96509a59a7af98d885f7de6c1)) 342 | 343 | ## [0.9.0](https://www.github.com/boson-project/faas-js-runtime/compare/v0.8.0...v0.9.0) (2022-04-28) 344 | 345 | 346 | ### Features 347 | 348 | * support binary data in cloudevents ([#112](https://www.github.com/boson-project/faas-js-runtime/issues/112)) ([2ce6c05](https://www.github.com/boson-project/faas-js-runtime/commit/2ce6c051f5c3c99269d2db9bf29be61ec3a24e80)) 349 | 350 | ## [0.8.0](https://www.github.com/boson-project/faas-js-runtime/compare/v0.7.1...v0.8.0) (2021-12-09) 351 | 352 | 353 | ### Features 354 | 355 | * add an endpoint for prometheus at /metrics ([#109](https://www.github.com/boson-project/faas-js-runtime/issues/109)) ([559c110](https://www.github.com/boson-project/faas-js-runtime/commit/559c11039bd7a9fbb505fb7689b58c079ce240c4)) 356 | * add limited support for func.yaml via logLevel ([#104](https://www.github.com/boson-project/faas-js-runtime/issues/104)) ([6e376fa](https://www.github.com/boson-project/faas-js-runtime/commit/6e376fa508bd4201c085c5adb8acec880437a91a)) 357 | 358 | 359 | ### Bug Fixes 360 | 361 | * use cloudevent as second param in test fn ([#99](https://www.github.com/boson-project/faas-js-runtime/issues/99)) ([d3dae1d](https://www.github.com/boson-project/faas-js-runtime/commit/d3dae1dee78d9466595c828a169ff964434d28a6)) 362 | 363 | ### [0.7.1](https://www.github.com/boson-project/faas-js-runtime/compare/v0.7.0...v0.7.1) (2021-05-24) 364 | 365 | 366 | ### Bug Fixes 367 | 368 | * fix Invokable type signature ([#97](https://www.github.com/boson-project/faas-js-runtime/issues/97)) ([2c7f66c](https://www.github.com/boson-project/faas-js-runtime/commit/2c7f66c57767d459749c7a543cd06ee0cd76983a)) 369 | 370 | ## [0.7.0](https://www.github.com/boson-project/faas-js-runtime/compare/v0.6.0...v0.7.0) (2021-05-24) 371 | 372 | 373 | ### Features 374 | 375 | * add TypeScript type definitions ([#90](https://www.github.com/boson-project/faas-js-runtime/issues/90)) ([d43fa28](https://www.github.com/boson-project/faas-js-runtime/commit/d43fa28c0114ed7ef24a805f43c50b19cfe7a287)) 376 | * **cli:** pass --logLevel and --port from the cli or as env variables ([d6b32a3](https://www.github.com/boson-project/faas-js-runtime/commit/d6b32a3e32292112c531abe63d5ffbad9c00639e)) 377 | 378 | 379 | ### Bug Fixes 380 | 381 | * change index.js to not have a default export ([#93](https://www.github.com/boson-project/faas-js-runtime/issues/93)) ([d5bfd68](https://www.github.com/boson-project/faas-js-runtime/commit/d5bfd68f4ee731f6a7170ff8978954ff8fd0c100)) 382 | 383 | ## [0.6.0](https://www.github.com/boson-project/faas-js-runtime/compare/v0.5.1...v0.6.0) (2021-04-10) 384 | 385 | 386 | ### Features 387 | 388 | * support configurable log levels ([#82](https://www.github.com/boson-project/faas-js-runtime/issues/82)) ([b6f8be4](https://www.github.com/boson-project/faas-js-runtime/commit/b6f8be4f9e671e2c8be7f85e104400d52219e5ff)) 389 | 390 | 391 | ### Bug Fixes 392 | 393 | * better error handling when network address is already in use ([#74](https://www.github.com/boson-project/faas-js-runtime/issues/74)) ([ae28dcf](https://www.github.com/boson-project/faas-js-runtime/commit/ae28dcfed94dc50fe89a67a1262e78b59c7cbd0a)) 394 | 395 | ### [0.5.1](https://www.github.com/boson-project/faas-js-runtime/compare/v0.5.0...v0.5.1) (2020-11-03) 396 | 397 | 398 | ### Bug Fixes 399 | 400 | * reverse the parameter order when invoking ([#71](https://www.github.com/boson-project/faas-js-runtime/issues/71)) ([df55e8a](https://www.github.com/boson-project/faas-js-runtime/commit/df55e8a9fe4e3f97cb1e82f2d6da6f5d82ec936b)) 401 | 402 | ## [0.5.0](https://www.github.com/boson-project/faas-js-runtime/compare/v0.4.0...v0.5.0) (2020-10-30) 403 | 404 | 405 | ### Features 406 | 407 | * handle CloudEvent and Message responses from function invocation ([#68](https://www.github.com/boson-project/faas-js-runtime/issues/68)) ([351197f](https://www.github.com/boson-project/faas-js-runtime/commit/351197f7258e8612fc4ad1a1d43d3952ba87f7f6)) 408 | 409 | 410 | ### Bug Fixes 411 | 412 | * handle cloudevents that have no data ([#67](https://www.github.com/boson-project/faas-js-runtime/issues/67)) ([84d402d](https://www.github.com/boson-project/faas-js-runtime/commit/84d402d32301ce379f819b670deb7348cc2a7d1b)) 413 | 414 | ## [0.4.0](https://www.github.com/boson-project/faas-js-runtime/compare/v0.3.0...v0.4.0) (2020-10-06) 415 | 416 | 417 | ### Features 418 | 419 | * provide cloudevent data if it exists as first parameter to function ([#61](https://www.github.com/boson-project/faas-js-runtime/issues/61)) ([cdd4d8b](https://www.github.com/boson-project/faas-js-runtime/commit/cdd4d8ba4258a95a1344de90a5940bc629f6cf00)) 420 | 421 | ## [0.3.0](https://www.github.com/boson-project/faas-js-runtime/compare/v0.2.3...v0.3.0) (2020-10-01) 422 | 423 | 424 | ### Features 425 | 426 | * change module name to faas-js-runtime ([#56](https://www.github.com/boson-project/faas-js-runtime/issues/56)) ([304eba6](https://www.github.com/boson-project/faas-js-runtime/commit/304eba608ec3a7c45069ed3092dfb3af13c2456a)) 427 | 428 | 429 | ### Bug Fixes 430 | 431 | * remove openwhisk varaiables from context ([#57](https://www.github.com/boson-project/faas-js-runtime/issues/57)) ([bb07696](https://www.github.com/boson-project/faas-js-runtime/commit/bb076960a6a87afec828336ce5e7e19e92cfc7c6)) 432 | 433 | ### [0.2.3](https://github.com/openshift-cloud-functions/faas-js-runtime/compare/v0.2.2...v0.2.3) (2020-09-04) 434 | 435 | ### [0.2.2](https://github.com/openshift-cloud-functions/faas-js-runtime/compare/v0.2.1...v0.2.2) (2020-09-02) 436 | 437 | ### [0.2.1](https://github.com/openshift-cloud-functions/faas-js-runtime/compare/v0.2.0...v0.2.1) (2020-08-12) 438 | 439 | ## [0.2.0](https://github.com/openshift-cloud-functions/faas-js-runtime/compare/v0.1.0...v0.2.0) (2020-02-12) 440 | 441 | 442 | ### Features 443 | 444 | * add support for 1.0 structured cloud events ([#24](https://github.com/openshift-cloud-functions/faas-js-runtime/issues/24)) ([b246948](https://github.com/openshift-cloud-functions/faas-js-runtime/commit/b24694827ff0de4ebbbf3977cd7ab9fbf6e14391)) 445 | * provide structured logger to functions in context ([#28](https://github.com/openshift-cloud-functions/faas-js-runtime/issues/28)) ([0bec8df](https://github.com/openshift-cloud-functions/faas-js-runtime/commit/0bec8df02425719f0dc5ad974164bee8393d3a9e)) 446 | 447 | 448 | ### Bug Fixes 449 | 450 | * **README:** update sample to use correct namespaced module ([#26](https://github.com/openshift-cloud-functions/faas-js-runtime/issues/26)) ([293e557](https://github.com/openshift-cloud-functions/faas-js-runtime/commit/293e55747ed0ee92b0ea241a29946bab0fdb24ec)) 451 | 452 | ## [0.1.0](https://github.com/openshift-cloud-functions/faas-js-runtime/compare/v0.0.4...v0.1.0) (2019-12-13) 453 | 454 | 455 | ### Features 456 | 457 | * add support for cloud events version 1.0 ([#21](https://github.com/openshift-cloud-functions/faas-js-runtime/issues/21)) ([f282e28](https://github.com/openshift-cloud-functions/faas-js-runtime/commit/f282e28b2927156380b3c408dd156575ad30a3ea)) 458 | 459 | ### [0.0.4](https://github.com/openshift-cloud-functions/faas-js-runtime/compare/v0.0.3...v0.0.4) (2019-11-12) 460 | 461 | 462 | ### Features 463 | 464 | * use error code and message for thrown exceptions ([d1cce37](https://github.com/openshift-cloud-functions/faas-js-runtime/commit/d1cce3728c7edc12465ba1d03cdfb5c84f03ba60)) 465 | 466 | ### [0.0.3](https://github.com/openshift-cloud-functions/faas-js-runtime/compare/v0.0.2...v0.0.3) (2019-11-08) 467 | 468 | 469 | ### Features 470 | 471 | * add openwhisk private properties to context object ([6bea65e](https://github.com/openshift-cloud-functions/faas-js-runtime/commit/6bea65e8bd4921877fd2c2f9e824ee5be9700abc)) 472 | * add query parameters to context object ([74eb5b8](https://github.com/openshift-cloud-functions/faas-js-runtime/commit/74eb5b8e7fca20bce3a3cbc6c8aabcbd4bbe3e99)) 473 | * allow function to set headers ([abff9dc](https://github.com/openshift-cloud-functions/faas-js-runtime/commit/abff9dcf65c40b42936644f092c6178d642292cc)) 474 | * allow function to set the response code in the return object ([5d44398](https://github.com/openshift-cloud-functions/faas-js-runtime/commit/5d44398ced5aadcbe0b98018140482c47b02947a)) 475 | * allow user set content type for text/plain ([b98396e](https://github.com/openshift-cloud-functions/faas-js-runtime/commit/b98396e4fba54c99bf4faf7e8f0696bd11c414b5)) 476 | * parse x-form-urlencoded POST requests ([7996ca4](https://github.com/openshift-cloud-functions/faas-js-runtime/commit/7996ca4c0fdbadd0ebfc65f430582457939e58b6)) 477 | 478 | 479 | ### Bug Fixes 480 | 481 | * add tests and fix bugs for health check URLs ([4220921](https://github.com/openshift-cloud-functions/faas-js-runtime/commit/42209213ebe9a520c424d524211928a36862f23f)) 482 | * set __ow_user to empty string ([1b9ac15](https://github.com/openshift-cloud-functions/faas-js-runtime/commit/1b9ac15f1a93521c52618cc44ac1dc9447ebdc9f)) 483 | 484 | ### 0.0.2 (2019-10-18) 485 | 486 | 487 | ### Features 488 | 489 | * framework should not install function deps ([959e5d9](https://github.com/openshift-cloud-functions/faas-js-runtime/commit/959e5d9cda604587bee8f2f7ce09e5f04873b851)) 490 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2020 Red Hat, Inc. 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Node.js Function Framework 2 | 3 | [![Node.js CI](https://github.com/nodeshift/faas-js-runtime/workflows/Node.js%20CI/badge.svg)](https://github.com/nodeshift/faas-js-runtime/actions?query=workflow%3A%22Node.js+CI%22+branch%3Amaster) 4 | [![codecov](https://codecov.io/gh/nodeshift/faas-js-runtime/branch/main/graph/badge.svg?token=Z72LKANFJI)](https://codecov.io/gh/nodeshift/faas-js-runtime) 5 | 6 | This module provides a Node.js framework for executing a function that 7 | exists in a user-provided directory path as an `index.js` file. The 8 | directory may also contain an optional `package.json` file which can 9 | be used to declare runtime dependencies for the function. You can also 10 | provide a path to an arbitrary JavaScript file instead of a directory 11 | path, allowing you to execute a single file as a function. 12 | 13 | | | Project Info | 14 | | --------------- | ------------- | 15 | | License: | Apache-2.0 | 16 | | Issue tracker: | https://github.com/nodeshift/faas-js-runtime/issues | 17 | | Engines: | Node.js >= 20 | 18 | 19 | The function is loaded and then invoked for incoming HTTP requests 20 | at `localhost:8080`. The incoming request may be a 21 | [Cloud Event](https://github.com/cloudevents/sdk-javascript#readme.) or 22 | just a simple HTTP GET/POST request. The invoked user function can be 23 | `async` but that is not required. 24 | 25 | ## The Function Interface 26 | 27 | The function file that is loaded may export a single function or a `Function` 28 | object. The `Function` object allows developers to add lifecycle hooks for 29 | initialization and shutdown, as well as providing a way to implement custom 30 | health checks. 31 | 32 | The `Function` interface is defined as: 33 | 34 | ```typescript 35 | export interface Function { 36 | // The initialization function, called before the server is started 37 | // This function is optional and should be synchronous. 38 | init?: () => any; 39 | 40 | // The shutdown function, called after the server is stopped 41 | // This function is optional and should be synchronous. 42 | shutdown?: () => any; 43 | 44 | // Function that returns an array of CORS origins 45 | // This function is optional. 46 | cors?: () => string[]; 47 | 48 | // The liveness function, called to check if the server is alive 49 | // This function is optional and should return 200/OK if the server is alive. 50 | liveness?: HealthCheck; 51 | 52 | // The readiness function, called to check if the server is ready to accept requests 53 | // This function is optional and should return 200/OK if the server is ready. 54 | readiness?: HealthCheck; 55 | 56 | logLevel?: LogLevel; 57 | 58 | // The function to handle HTTP requests 59 | handle: CloudEventFunction | HTTPFunction; 60 | } 61 | ``` 62 | 63 | ## Handle Signature 64 | 65 | This module supports two different function signatures: HTTP or CloudEvents. In the type definitions below, we use TypeScript to express interfaces and types, but this module is usable from JavaScript as well. 66 | 67 | ### HTTP Functions 68 | 69 | The HTTP function signature is the simplest. It is invoked for every HTTP request that does not contain a CloudEvent. 70 | 71 | ```typescript 72 | interface HTTPFunction { 73 | (context: Context, body?: IncomingBody): HTTPFunctionReturn; 74 | } 75 | ``` 76 | 77 | Where the `IncomingBody` is either a string, a Buffer, a JavaScript object, or undefined, depending on what was supplied in the HTTP POST message body. The `HTTTPFunctionReturn` type is defined as: 78 | 79 | ```typescript 80 | type HTTPFunctionReturn = Promise | StructuredReturn | ResponseBody | void; 81 | ``` 82 | 83 | Where the `StructuredReturn` is a JavaScript object with the following properties: 84 | 85 | ```typescript 86 | interface StructuredReturn { 87 | statusCode?: number; 88 | headers?: Record; 89 | body?: ResponseBody; 90 | } 91 | ``` 92 | 93 | If the function returns a `StructuredReturn` object, then the `statusCode` and `headers` properties are used to construct the HTTP response. If the `body` property is present, it is used as the response body. If the function returns `void` or `undefined`, then the response body is empty. 94 | 95 | The `ResponseBody` is either a string, a JavaScript object, or a Buffer. JavaScript objects will be serialized as JSON. Buffers will be sent as binary data. 96 | 97 | ### CloudEvent Functions 98 | 99 | CloudEvent functions are used in environments where the incoming HTTP request is a CloudEvent. The function signature is: 100 | 101 | ```typescript 102 | interface CloudEventFunction { 103 | (context: Context, event: CloudEvent): CloudEventFunctionReturn; 104 | } 105 | ``` 106 | 107 | Where the return type is defined as: 108 | 109 | ```typescript 110 | type CloudEventFunctionReturn = Promise | CloudEvent | HTTPFunctionReturn; 111 | ``` 112 | 113 | The function return type can be anything that a simple HTTP function can return or a CloudEvent. Whatever is returned, it will be sent back to the caller as a response. 114 | 115 | ### Health Checks 116 | 117 | The `Function` interface also allows for the addition of a `liveness` and `readiness` function. These functions are used to implement health checks for the function. The `liveness` function is called to check if the function is alive. The `readiness` function is called to check if the function is ready to accept requests. If either of these functions return a non-200 status code, then the function is considered unhealthy. 118 | 119 | A health check function is defined as: 120 | 121 | ```typescript 122 | /** 123 | * The HealthCheck interface describes a health check function, 124 | * including the optional path to which it should be bound. 125 | */ 126 | export interface HealthCheck { 127 | (request: Http2ServerRequest, reply: Http2ServerResponse): any; 128 | path?: string; 129 | } 130 | ``` 131 | 132 | By default, the health checks are bound to the `/health/liveness` and `/health/readiness` paths. You can override this by setting the `path` property on the `HealthCheck` object, or by setting the `LIVENESS_URL` and `READINESS_URL` environment variables. 133 | 134 | ## CLI 135 | 136 | The easiest way to get started is to use the CLI. You can call it 137 | with the path to any JavaScript file which has a default export that 138 | is a function, or that exports a `Function` type. For example, 139 | 140 | ```js 141 | // index.js 142 | function handle(context) { 143 | const event = context.cloudevent; 144 | // business logic 145 | return { 146 | statusCode: 200, 147 | statusMessage: 'OK' 148 | } 149 | } 150 | 151 | module.exports = handle; 152 | ``` 153 | 154 | You can expose this function as an HTTP endpoint at `localhost:8080` 155 | with the CLI. 156 | 157 | ```console 158 | npx faas-js-runtime ./index.js 159 | ``` 160 | 161 | ### --bodyLimit 162 | Sets the maximum allowed payload size for incoming requests. Default is '1mb'. 163 | 164 | Example: 165 | ```console 166 | faas-js-runtime function.js --bodyLimit 2mb 167 | ``` 168 | 169 | ## Debugging Locally 170 | 171 | During local development, it is often necessary to set breakpoints in your code for debugging. Since functions are just javascript files, using any current debugging(VS Code, Chrome Dev Tools) method will work. The linked blog post shows how to setup and use VS Code for development debugging. 172 | 173 | https://developers.redhat.com/articles/2021/07/13/nodejs-serverless-functions-red-hat-openshift-part-2-debugging-locally 174 | 175 | ## Debugging Remotely 176 | 177 | It is also possible to debug your function while it is running on a remote cluster. The linked blog posts shows how to setup and use the Chrome Dev Tools inspector for debugging on a cluster. 178 | 179 | https://developers.redhat.com/articles/2021/12/08/nodejs-serverless-functions-red-hat-openshift-part-3-debugging-cluster 180 | 181 | 182 | 183 | ### Functions as ES Modules 184 | 185 | Functions can be written and imported as ES modules with either the `.mjs` file extension or by adding the `type` property to the functions package.json and setting it to `module`. 186 | 187 | ```js 188 | // index.mjs 189 | const handle = async function(context) => { ... }; 190 | 191 | // Export the function 192 | export { handle }; 193 | ``` 194 | 195 | If using the `type` property, the package.json might look something like this: 196 | ```js 197 | { 198 | "name": "moduleName", 199 | "type": "module" 200 | } 201 | ``` 202 | 203 | 204 | 205 | ## Usage as a Module 206 | 207 | In the current working directory, there is an `index.js` file like this. 208 | 209 | ```js 210 | const { start } = require('faas-js-runtime'); 211 | const options = { 212 | // Pino is used as the logger implementation. Supported log levels are 213 | // documented at this link: 214 | // https://github.com/pinojs/pino/blob/master/docs/api.md#options 215 | logLevel: 'info' 216 | } 217 | 218 | // The function directory is in ./function-dir 219 | start(require(`${__dirname}/function-dir/`), server => { 220 | // The server is now listening on localhost:8080 221 | // and the function defined in `function-dir/index.js` 222 | // will be invoked for each HTTP 223 | // request to this endpoint. 224 | console.log('Server listening'); 225 | 226 | // Whenever you want to shutdown the framework 227 | server.close(); 228 | }, options); 229 | ``` 230 | 231 | In `./function-dir`, there is an `index.js` file that looks 232 | like this. 233 | 234 | ```js 235 | module.exports = async function myFunction(context) { 236 | const ret = 'This is a test for Node.js functions. Success.'; 237 | return new Promise((resolve, reject) => { 238 | setTimeout(_ => { 239 | context.log.info('sending response to client') 240 | resolve(ret); 241 | }, 500); 242 | }); 243 | }; 244 | ``` 245 | 246 | You can use `curl` to `POST` to the endpoint: 247 | ```console 248 | $ curl -X POST -d 'hello=world' \ 249 | -H'Content-type: application/x-www-form-urlencoded' http://localhost:8080 250 | ``` 251 | 252 | You can use `curl` to `POST` JSON data to the endpoint: 253 | ```console 254 | $ curl -X POST -d '{"hello": "world"}' \ 255 | -H'Content-type: application/json' \ 256 | http://localhost:8080 257 | ``` 258 | 259 | You can use `curl` to `POST` an event to the endpoint: 260 | ```console 261 | $ curl -X POST -d '{"hello": "world"}' \ 262 | -H'Content-type: application/json' \ 263 | -H'Ce-id: 1' \ 264 | -H'Ce-source: cloud-event-example' \ 265 | -H'Ce-type: dev.knative.example' \ 266 | -H'Ce-specversion: 1.0' \ 267 | http://localhost:8080 268 | ``` 269 | 270 | ### Sample 271 | 272 | You can see this in action by running `node bin/cli.js sample/index.js`. 273 | -------------------------------------------------------------------------------- /bin/cli.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | const path = require('path'); 3 | 4 | const { start, defaults } = require('../'); 5 | const { loadFunction } = require('../lib/function-loader.js'); 6 | const pkg = require('../package.json'); 7 | const { Command } = require('commander'); 8 | 9 | const program = new Command(); 10 | 11 | program 12 | .version(pkg.version) 13 | .option('--log-level ', 'change the log level of the function', defaults.LOG_LEVEL) 14 | .option('--port ', 'change the port the runtime listens on', defaults.PORT) 15 | .option('--include-raw', 'include the raw body in the request context', defaults.INCLUDE_RAW) 16 | .option('--bodyLimit )', 'maximum size of the request payload in bytes', defaults.BODY_LIMIT) 17 | .arguments('') 18 | .action(runServer); 19 | 20 | program.parse(process.argv); 21 | 22 | async function runServer(file) { 23 | const programOpts = program.opts(); 24 | 25 | try { 26 | let options = { 27 | logLevel: process.env.FUNC_LOG_LEVEL || programOpts['logLevel'] || defaults.LOG_LEVEL, 28 | port: process.env.FUNC_PORT || programOpts.port || defaults.PORT, 29 | includeRaw: process.env.FUNC_INCLUDE_RAW ? true : programOpts.includeRaw || defaults.INCLUDE_RAW, 30 | bodyLimit: process.env.FUNC_BODY_LIMIT || programOpts.bodyLimit || defaults.BODY_LIMIT 31 | }; 32 | 33 | const filePath = extractFullPath(file); 34 | const code = await loadFunction(filePath); 35 | 36 | // The module will extract `handle` and other lifecycle functions 37 | // from `code` if it is an object. If it's just a function, it will 38 | // be used directly. 39 | if (typeof code === 'function' || typeof code === 'object') { 40 | return start(code, options); 41 | } else { 42 | console.error(code); 43 | throw TypeError(`Cannot find Invokable function 'handle' in ${code}`); 44 | } 45 | } catch (error) { 46 | console.error(`⛔ ${error}`); 47 | } 48 | } 49 | 50 | function extractFullPath(file) { 51 | if (path.isAbsolute(file)) return file; 52 | return path.join(process.cwd(), file); 53 | } 54 | -------------------------------------------------------------------------------- /index.d.ts: -------------------------------------------------------------------------------- 1 | import { Server } from 'http'; 2 | import { CloudEventFunction, HTTPFunction, InvokerOptions, Function } from './lib/types'; 3 | import { LogLevel } from 'fastify'; 4 | 5 | // Invokable describes the function signature for a function that can be invoked by the server. 6 | export type Invokable = CloudEventFunction | HTTPFunction; 7 | 8 | export interface Config { 9 | logLevel: LogLevel; 10 | port: number; 11 | includeRaw: boolean; 12 | } 13 | 14 | // start starts the server for the given function. 15 | export declare const start: { 16 | // eslint-disable-next-line no-unused-vars 17 | (func: Invokable | Function, options?: InvokerOptions): Promise 18 | }; 19 | 20 | export declare const defaults: { 21 | LOG_LEVEL: LogLevel, 22 | PORT: number, 23 | INCLUDE_RAW: boolean, 24 | BODY_LIMIT: number, 25 | }; 26 | 27 | // re-export 28 | export * from './lib/types'; 29 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const qs = require('qs'); 2 | const fs = require('fs'); 3 | const path = require('path'); 4 | const yaml = require('js-yaml'); 5 | const requestHandler = require('./lib/request-handler'); 6 | const eventHandler = require('./lib/event-handler'); 7 | const Context = require('./lib/context'); 8 | const shutdown = require('death')({ uncaughtException: true }); 9 | const fastifyRawBody = require('fastify-raw-body'); 10 | const { isPromise } = require('./lib/utils'); 11 | 12 | // HTTP framework 13 | const fastify = require('fastify'); 14 | 15 | // Default log level 16 | const LOG_LEVEL = 'warn'; 17 | 18 | // Default port 19 | const PORT = 8080; 20 | 21 | // Don't Include Raw body by default 22 | const INCLUDE_RAW = false; 23 | 24 | // Default maximum request payload size (1MB in bytes) 25 | const BODY_LIMIT = 1048576; 26 | 27 | /** 28 | * Starts the provided Function. If the function is a module, it will be 29 | * inspected for init, shutdown, cors, liveness, and readiness functions and those 30 | * will be used to configure the server. If it's a function, it will be used 31 | * directly. 32 | * 33 | * @param {Object | function} func The function to start (see the Function type) 34 | * @param {*} options Options to configure the server 35 | * @param {string} options.logLevel The log level to use 36 | * @param {number} options.port The port to listen on 37 | * @returns {Promise} The server that was started 38 | */ 39 | async function start(func, options) { 40 | options = options || {}; 41 | if (typeof func === 'function') { 42 | return __start(func, options); 43 | } 44 | if (typeof func.handle !== 'function') { 45 | throw new TypeError('Function must export a handle function'); 46 | } 47 | if (typeof func.init === 'function') { 48 | const initRet = func.init(); 49 | if (isPromise(initRet)) { 50 | await initRet; 51 | } 52 | } 53 | if (typeof func.shutdown === 'function') { 54 | options.shutdown = func.shutdown; 55 | } 56 | if (typeof func.liveness === 'function') { 57 | options.liveness = func.liveness; 58 | } 59 | if (typeof func.readiness === 'function') { 60 | options.readiness = func.readiness; 61 | } 62 | if (typeof func.cors === 'function') { 63 | options.cors = func.cors; 64 | } 65 | return __start(func.handle, options); 66 | } 67 | 68 | /** 69 | * Internal function to start the server. This is used by the start function. 70 | * 71 | * @param {function} func - The function to start 72 | * @param {*} options - Options to configure the server 73 | * @param {string} options.logLevel - The log level to use 74 | * @param {number} options.port - The port to listen on 75 | * @returns {Promise} The server that was started 76 | */ 77 | async function __start(func, options) { 78 | // Load a func.yaml file if it exists 79 | const config = loadConfig(options); 80 | 81 | // Create and configure the server for the default behavior 82 | const server = initializeServer(config); 83 | 84 | // Configures the server to handle incoming requests to the function itself, 85 | // and also to other endpoints such as telemetry and liveness/readiness 86 | requestHandler(server, { func, funcConfig: config }); 87 | 88 | // Start the server 89 | try { 90 | await server.listen({ 91 | port: config.port, 92 | host: '::', 93 | }); 94 | return server.server; 95 | } catch (err) { 96 | console.error('Error starting server', err); 97 | process.exit(1); 98 | } 99 | } 100 | 101 | /** 102 | * Creates and configures the HTTP server to handle incoming CloudEvents, 103 | * and initializes the Context object. 104 | * @param {Config} config - The configuration object for port and logLevel 105 | * @returns {FastifyInstance} The Fastify server that was created 106 | */ 107 | function initializeServer(config) { 108 | const server = fastify({ 109 | logger: { 110 | level: config.logLevel, 111 | formatters: { 112 | bindings: bindings => ({ 113 | pid: bindings.pid, 114 | hostname: bindings.hostname, 115 | node_version: process.version, 116 | }), 117 | }, 118 | }, 119 | bodyLimit: Number(config.bodyLimit), 120 | }); 121 | 122 | if (config.includeRaw) { 123 | server.register(fastifyRawBody, { 124 | field: 'rawBody', 125 | global: true, 126 | encoding: 'utf8', 127 | runFirst: false, 128 | }); 129 | } 130 | 131 | // Give the Function an opportunity to clean up before the process exits 132 | shutdown(async _ => { 133 | if (typeof config.shutdown === 'function') { 134 | const shutdownRet = config.shutdown(); 135 | if (isPromise(shutdownRet)) { 136 | await shutdownRet; 137 | } 138 | } 139 | server.close(); 140 | process.exit(0); 141 | }); 142 | 143 | // Add a parser for application/x-www-form-urlencoded 144 | server.addContentTypeParser( 145 | 'application/x-www-form-urlencoded', 146 | function(_, payload, done) { 147 | var body = ''; 148 | payload.on('data', data => (body += data)); 149 | payload.on('end', _ => { 150 | try { 151 | const parsed = qs.parse(body); 152 | done(null, parsed); 153 | } catch (e) { 154 | done(e); 155 | } 156 | }); 157 | payload.on('error', done); 158 | } 159 | ); 160 | 161 | // Add a parser for everything else - parse it as a buffer and 162 | // let this framework's router handle it 163 | server.addContentTypeParser( 164 | '*', 165 | { parseAs: 'buffer' }, 166 | function(req, body, done) { 167 | try { 168 | done(null, body); 169 | } catch (err) { 170 | err.statusCode = 500; 171 | done(err, undefined); 172 | } 173 | } 174 | ); 175 | 176 | // Initialize the invocation context 177 | // This is passed as a parameter to the function when it's invoked 178 | server.decorateRequest('fcontext'); 179 | server.addHook('preHandler', (req, reply, done) => { 180 | req.fcontext = new Context(req); 181 | done(); 182 | }); 183 | 184 | // Evaluates the incoming request, parsing any CloudEvents and attaching 185 | // to the request's `fcontext` 186 | eventHandler(server); 187 | 188 | return server; 189 | } 190 | 191 | /** 192 | * loadConfig() loads a func.yaml file if it exists, allowing it to take precedence over the default options 193 | * 194 | * @param {Object} options Server options 195 | * @param {String} options.config Path to a func.yaml file 196 | * @param {String} options.logLevel Log level - one of 'fatal', 'error', 'warn', 'info', 'debug', 'trace', 'silent' 197 | * @param {number} options.port Port to listen on 198 | * @param {number} [options.bodyLimit=1048576] - Maximum request payload size in bytes 199 | * @returns {Config} Configuration object 200 | */ 201 | function loadConfig(options) { 202 | const opts = { ...options, ...readFuncYaml(options.config) }; 203 | opts.logLevel = opts.logLevel || LOG_LEVEL; 204 | opts.port = opts.port || PORT; 205 | opts.includeRaw = opts.includeRaw || INCLUDE_RAW; 206 | opts.bodyLimit = opts.bodyLimit || BODY_LIMIT; 207 | return opts; 208 | } 209 | 210 | /** 211 | * Reads a func.yaml file at path and returns it as a JS object 212 | * @param {string} fileOrDirPath - the path to the func.yaml file or the directory containing it 213 | * @returns {object} the parsed func.yaml file 214 | */ 215 | function readFuncYaml(fileOrDirPath) { 216 | if (!fileOrDirPath) fileOrDirPath = './'; 217 | 218 | let baseDir; 219 | let maybeDir = fs.statSync(fileOrDirPath); 220 | if (maybeDir.isDirectory()) { 221 | baseDir = fileOrDirPath; 222 | } else { 223 | maybeDir = fs.statSync(path.dirname(fileOrDirPath)); 224 | if (maybeDir.isDirectory()) { 225 | baseDir = fileOrDirPath; 226 | } 227 | } 228 | 229 | if (baseDir) { 230 | const yamlFile = path.join(baseDir, 'func.yaml'); 231 | const maybeYaml = fs.statSync(yamlFile, { throwIfNoEntry: false }); 232 | if (!!maybeYaml && maybeYaml.isFile()) { 233 | try { 234 | return yaml.load(fs.readFileSync(yamlFile, 'utf8')); 235 | } catch (err) { 236 | console.warn(err); 237 | } 238 | } 239 | } 240 | } 241 | 242 | module.exports = exports = { 243 | start, 244 | defaults: { LOG_LEVEL, PORT, INCLUDE_RAW, BODY_LIMIT }, 245 | }; 246 | -------------------------------------------------------------------------------- /lib/context.js: -------------------------------------------------------------------------------- 1 | const { CloudEvent } = require('cloudevents'); 2 | 3 | class Context { 4 | constructor(request) { 5 | this.body = request.body; 6 | this.query = { ...request.query }; 7 | this.headers = { ...request.headers }; 8 | this.method = request.raw.method; 9 | this.httpVersion = request.raw.httpVersion; 10 | this.httpVersionMajor = request.raw.httpVersionMajor; 11 | this.httpVersionMinor = request.raw.httpVersionMinor; 12 | this.contentType = request.headers['content-type']; 13 | this.rawBody = request.rawBody; 14 | 15 | Object.assign(this, request.query); 16 | this.log = request.log; 17 | } 18 | 19 | cloudEventResponse(response) { 20 | return new CloudEventResponse(response); 21 | } 22 | } 23 | 24 | class CloudEventResponse { 25 | #response; 26 | 27 | constructor(response) { 28 | this.#response = { data: response }; 29 | } 30 | 31 | version(version) { 32 | this.#response.specversion = version; 33 | return this; 34 | } 35 | 36 | id(id) { 37 | this.#response.id = id; 38 | return this; 39 | } 40 | 41 | type(type) { 42 | this.#response.type = type; 43 | return this; 44 | } 45 | 46 | source(source) { 47 | this.#response.source = source; 48 | return this; 49 | } 50 | 51 | response() { 52 | return new CloudEvent(this.#response); 53 | } 54 | } 55 | 56 | module.exports = Context; 57 | -------------------------------------------------------------------------------- /lib/event-handler.js: -------------------------------------------------------------------------------- 1 | const { HTTP } = require('cloudevents'); 2 | 3 | // Adds a content type parser for cloudevents, decorates 4 | // the request object with an isCloudEvent() function, and 5 | // parses any CloudEvent that is part of the request, 6 | // attaching it to the request's fcontext. 7 | module.exports = exports = function use(fastify) { 8 | fastify.addContentTypeParser('application/cloudevents+json', 9 | { parseAs: 'string' }, function(req, body, done) { 10 | done(null, body); 11 | }); 12 | 13 | fastify.decorateRequest('isCloudEvent', function() { 14 | return HTTP.isEvent(this); 15 | }); 16 | 17 | fastify.addHook('preHandler', function(request, reply, done) { 18 | if (request.isCloudEvent()) { 19 | try { 20 | request.fcontext.cloudevent = HTTP.toEvent(request); 21 | request.fcontext.cloudevent.validate(); 22 | } catch (err) { 23 | if (err.message.startsWith('invalid spec version')) { 24 | reply.code(406); 25 | } 26 | done(err); 27 | } 28 | } 29 | done(); 30 | }); 31 | }; 32 | -------------------------------------------------------------------------------- /lib/function-loader.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | 4 | // This is the way to import es modules inside a CJS module 5 | const dynamicImport = new Function('modulePath', 'return import(modulePath)'); 6 | 7 | async function loadFunction(filePath) { 8 | if (isESM(filePath)) { 9 | if (process.platform === 'win32') { 10 | // Windows requires the file path to be a URL 11 | filePath = `file:///${filePath}`; 12 | } 13 | code = await dynamicImport(filePath); 14 | } else { 15 | code = require(filePath); 16 | } 17 | 18 | return code; 19 | } 20 | 21 | // https://nodejs.org/dist/latest-v18.x/docs/api/packages.html#determining-module-system 22 | // An ESM module can be determined 2 ways 23 | // 1. has the mjs file extention 24 | // 2. type=module in the package.json 25 | function isESM(filePath) { 26 | const pathParsed = path.parse(filePath); 27 | 28 | if (pathParsed.ext === '.mjs') { 29 | return true; 30 | } 31 | 32 | // find the functions package.json and see if it has a type field 33 | // look in the parent directory if the function is in a subdirectory 34 | let dir = pathParsed.dir; 35 | const rootDir = path.parse(process.cwd()).root; 36 | while (!hasPackageJson(dir)) { 37 | if (dir === rootDir) return false; 38 | dir = path.dirname(dir); 39 | } 40 | 41 | // Load the package.json and check the type field 42 | // Put this in a try/catch in case the package.json is invalid 43 | let pkgPath = path.join(dir, 'package.json'); 44 | try { 45 | const functionPkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8')); 46 | return functionPkg.type === 'module'; 47 | } catch(e) { 48 | console.warn(e); 49 | return false; 50 | } 51 | } 52 | 53 | function hasPackageJson(dir) { 54 | const pkgPath = path.join(dir, 'package.json'); 55 | return fs.existsSync(pkgPath); 56 | } 57 | 58 | module.exports = exports = { loadFunction }; 59 | -------------------------------------------------------------------------------- /lib/health-check.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const protection = require('overload-protection'); 4 | 5 | // Default LIVENESS/READINESS urls 6 | const READINESS_URL = '/health/readiness'; 7 | const LIVENESS_URL = '/health/liveness'; 8 | 9 | // Configure protect for liveness/readiness probes 10 | const protectCfg = { 11 | production: process.env.NODE_ENV === 'production', 12 | maxHeapUsedBytes: 0, // Max heap used threshold (0 to disable) [default 0] 13 | maxRssBytes: 0, // Max rss size threshold (0 to disable) [default 0] 14 | errorPropagationMode: false // Don't propagate error 15 | }; 16 | 17 | const readinessURL = process.env.READINESS_URL || READINESS_URL; 18 | const livenessURL = process.env.LIVENESS_URL || LIVENESS_URL; 19 | const protect = protection('http', protectCfg); 20 | 21 | function callProtect(request, reply) { 22 | reply.header('Content-Type', 'text/plain; charset=utf-8'); 23 | protect(request, reply, _ => reply.send('OK')); 24 | } 25 | 26 | module.exports = function healthCheck(opts) { 27 | return (fastify, _, done) => { 28 | // Handle health checks 29 | fastify.get(opts?.readiness?.path || readinessURL, 30 | { logLevel: opts?.logLevel || 'warn' }, opts.readiness || callProtect); 31 | fastify.get(opts?.liveness?.path || livenessURL, 32 | { logLevel: opts?.logLevel || 'warn' }, opts.liveness || callProtect); 33 | done(); 34 | }; 35 | }; 36 | -------------------------------------------------------------------------------- /lib/invocation-handler.js: -------------------------------------------------------------------------------- 1 | const invoker = require('./invoker'); 2 | 3 | // Sets the HTTP endpoints for the function invocation 4 | module.exports = function use(fastify, opts, done) { 5 | fastify.get('/', doGet); 6 | fastify.post('/', doPost); 7 | fastify.options('/', doOptions); 8 | const invokeFunction = invoker(opts); 9 | 10 | // TODO: if we know this is a CloudEvent function, should 11 | // we allow GET requests? 12 | async function doGet(request, reply) { 13 | sendReply(reply, await invokeFunction(request.fcontext, reply.log)); 14 | } 15 | 16 | async function doPost(request, reply) { 17 | sendReply(reply, await invokeFunction(request.fcontext, reply.log)); 18 | } 19 | 20 | async function doOptions(_request, reply) { 21 | reply.code(204).headers({ 22 | 'content-type': 'application/json; charset=utf-8', 23 | 'access-control-allow-methods': 'OPTIONS, GET, DELETE, POST, PUT, HEAD, PATCH', 24 | 'access-control-allow-origin': '*', 25 | 'access-control-allow-headers': '*' 26 | }).send(); 27 | } 28 | done(); 29 | }; 30 | 31 | function sendReply(reply, payload) { 32 | const contentType = payload.headers?.['content-type']; 33 | if (contentType?.startsWith('text/plain') && (typeof payload.response !== 'string')) { 34 | payload.response = JSON.stringify(payload.response); 35 | } 36 | if (payload.code) { 37 | reply = reply.code(payload.code); 38 | } 39 | if (payload.headers) { 40 | reply = reply.headers(payload.headers); 41 | } 42 | return reply.send(payload.response); 43 | } 44 | -------------------------------------------------------------------------------- /lib/invoker.js: -------------------------------------------------------------------------------- 1 | /** 2 | * The invoker module is responsible for invoking the user function. 3 | */ 4 | const { CloudEvent, HTTP } = require('cloudevents'); 5 | 6 | module.exports = function invoker(opts) { 7 | const func = opts.func; 8 | return async function invokeFunction(context, log) { 9 | // Default payload values 10 | const payload = { 11 | code: 200, 12 | response: undefined, 13 | headers: { 14 | 'content-type': 'application/json; charset=utf-8', 15 | 'access-control-allow-methods': 'OPTIONS, GET, DELETE, POST, PUT, HEAD, PATCH', 16 | } 17 | }; 18 | 19 | if (opts.funcConfig.cors) { 20 | const allowedOrigins = opts.funcConfig.cors(); 21 | const requestOrigin = context.headers.origin; 22 | const originIncluded = allowedOrigins.includes(requestOrigin) ? requestOrigin : null; 23 | payload.headers['access-control-allow-origin'] = originIncluded; 24 | } else { 25 | payload.headers['access-control-allow-origin'] = '*'; 26 | } 27 | 28 | let fnReturn; 29 | const scope = Object.freeze({}); 30 | try { 31 | if (context.cloudevent) { 32 | // Invoke the function with the context and the CloudEvent 33 | fnReturn = await func.bind(scope)(context, context.cloudevent); 34 | 35 | // If the response is a CloudEvent, we need to convert it 36 | // to a Message first and respond with the headers/body 37 | if (fnReturn instanceof CloudEvent || fnReturn?.constructor?.name === 'CloudEvent') { 38 | try { 39 | const message = HTTP.binary(fnReturn); 40 | payload.headers = { ...payload.headers, ...message.headers }; 41 | payload.response = message.body; 42 | // In this case, where the function is invoked with a CloudEvent 43 | // and returns a CloudEvent we don't need to continue processing the 44 | // response. Just return it using the HTTP.binary format. 45 | return payload; 46 | } catch (err) { 47 | return handleError(err, log); 48 | } 49 | } 50 | } else { 51 | // It's an HTTP function - extract the request body 52 | let body = context.body; 53 | if (context.contentType === 'application/json' && typeof body === 'string') { 54 | try { 55 | body = JSON.parse(body); 56 | } catch (err) { 57 | console.error('Error parsing JSON body', err); 58 | } 59 | } 60 | // Invoke with context and the raw body 61 | fnReturn = await func.bind(scope)(context, body); 62 | } 63 | } catch (err) { 64 | return handleError(err, log); 65 | } 66 | 67 | // Raw HTTP functions, and CloudEvent functions that return something 68 | // other than a CloudEvent, will end up here. 69 | 70 | // Return 204 No Content if the function returns 71 | // null, undefined or empty string 72 | if (!fnReturn) { 73 | payload.headers['content-type'] = 'text/plain'; 74 | payload.code = 204; 75 | payload.response = ''; 76 | return payload; 77 | } 78 | 79 | // If the function returns a string, set the content type to text/plain 80 | // and return it as the response 81 | if (typeof fnReturn === 'string') { 82 | payload.headers['content-type'] = 'text/plain; charset=utf-8'; 83 | payload.response = fnReturn; 84 | return payload; 85 | } 86 | 87 | // The function returned an object or an array, check for 88 | // user defined headers or datacontenttype 89 | if (typeof fnReturn?.headers === 'object') { 90 | const headers = {}; 91 | // normalize the headers as lowercase 92 | for (const header in fnReturn.headers) { 93 | headers[header.toLocaleLowerCase()] = fnReturn.headers[header]; 94 | } 95 | payload.headers = { ...payload.headers, ...headers }; 96 | } 97 | 98 | // Check for user defined status code 99 | if (fnReturn.statusCode) { 100 | payload.code = fnReturn.statusCode; 101 | } 102 | 103 | // Check for user supplied body 104 | if (fnReturn.body !== undefined) { 105 | // Provide default content-type unless supplied by user 106 | if (!payload.headers || !payload.headers['content-type']) { 107 | if (typeof fnReturn.body === 'string') { 108 | payload.headers['content-type'] = 'text/plain; charset=utf-8'; 109 | } else if (typeof fnReturn.body === 'object') { 110 | payload.headers['content-type'] = 'application/json; charset=utf-8'; 111 | } 112 | } 113 | payload.response = fnReturn.body; 114 | } else if (typeof fnReturn === 'object' && !fnReturn?.body) { 115 | // Finally, the user may have supplied a simple object response 116 | payload.headers['content-type'] = 'application/json; charset=utf-8'; 117 | payload.response = fnReturn; 118 | } 119 | return payload; 120 | }; 121 | }; 122 | 123 | function handleError(err, log) { 124 | log.error(`Error processing user function: "${err}"`); 125 | return { 126 | code: err.code ? err.code : 500, 127 | response: err.message 128 | }; 129 | } 130 | -------------------------------------------------------------------------------- /lib/metrics.js: -------------------------------------------------------------------------------- 1 | const { randomUUID } = require('crypto'); 2 | const { 3 | Registry, 4 | Counter, 5 | Histogram, 6 | Gauge, 7 | collectDefaultMetrics, 8 | } = require('prom-client'); 9 | 10 | const { cpu, mem, netstat } = osu = require('node-os-utils'); 11 | 12 | module.exports = function configure(config) { 13 | // eslint-disable-next-line max-len 14 | // See: https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/resource/semantic_conventions/faas.md 15 | const labels = { 16 | 'faas_name': config.name || 17 | process.env.FAAS_NAME || 18 | process.env.K_SERVICE || 19 | 'anonymous', 20 | 'faas_id': config.id || 21 | process.env.FAAS_ID || 22 | `${process.env.POD_NAME}-${randomUUID()}`, 23 | 'faas_instance': config.instance || 24 | process.env.FAAS_INSTANCE || 25 | process.env.POD_NAME, 26 | 'faas_version': config.version || 27 | process.env.FAAS_VERSION || 28 | process.env.K_REVISION, 29 | 'faas_runtime': config.runtime || 30 | process.env.FAAS_RUNTIME || 31 | 'Node.js', 32 | }; 33 | 34 | // Create a metrics register and add our identifying labels to it 35 | const register = new Registry(); 36 | register.setDefaultLabels(labels); 37 | collectDefaultMetrics({ register }); 38 | 39 | // invoked on GET to /metrics 40 | async function callMetrics(req, res) { 41 | try { 42 | res.headers('Content-Type', register.contentType); 43 | res.send(await register.metrics()); 44 | } catch (ex) { 45 | res.code(500).send(ex); 46 | } 47 | } 48 | 49 | const invocations = new Counter({ 50 | name: 'faas_invocations', 51 | help: 'The number of times this function was invoked', 52 | registers: [register], 53 | }); 54 | 55 | const execLatency = new Histogram({ 56 | name: 'faas_execution_latency', 57 | help: 'The time it took for this function to be invoked and return', 58 | registers: [register], 59 | }); 60 | 61 | const errors = new Counter({ 62 | name: 'faas_errors', 63 | help: 'The number of function invocation failures', 64 | registers: [register], 65 | }); 66 | 67 | const coldStartLatency = new Histogram({ 68 | name: 'faas_cold_start_latency', 69 | help: 'The time it took for this function to be in the ready state after the container started', 70 | registers: [register], 71 | }); 72 | 73 | const queueLatency = new Histogram({ 74 | name: 'faas_queue_latency', 75 | help: 'The time spent in a system queue before the function was invoked', 76 | registers: [register], 77 | }); 78 | 79 | new Gauge({ 80 | name: 'faas_cpu_utilization', 81 | help: 'The CPU utilization of this function instance', 82 | collect() { 83 | cpu.usage() 84 | .then(usage => { 85 | if (supported(usage)) { 86 | this.set(usage); 87 | }}) 88 | .catch(console.error); 89 | }, 90 | registers: [register], 91 | }); 92 | 93 | new Gauge({ 94 | name: 'faas_mem_utilization', 95 | help: 'The memory utilization of this function instance', 96 | collect() { 97 | mem.used() 98 | .then(used => { 99 | if (supported(used)) { 100 | this.set(used.usedMemMb); 101 | }}) 102 | .catch(console.error); 103 | }, 104 | registers: [register], 105 | }); 106 | 107 | new Gauge({ 108 | name: 'faas_netio_utilization', 109 | help: 'The network I/O utilization of this function instance', 110 | collect() { 111 | netstat.inOut() 112 | .then(io => { 113 | if (supported(io)) { 114 | this.set(io.total.inputMb + io.total.outputMb); 115 | }}) 116 | .catch(console.error); 117 | }, 118 | registers: [register], 119 | labels, 120 | }); 121 | 122 | return function handler(fastify) { 123 | // Each request has its own pair of timers 124 | fastify.decorateRequest('queueTimer'); 125 | fastify.decorateRequest('execTimer'); 126 | 127 | // When the server is fully ready, set the 128 | // cold start latency to our total uptime 129 | fastify.addHook('onReady', done => { 130 | coldStartLatency.observe(process.uptime()); 131 | done(); 132 | }); 133 | 134 | // On each request, increment the invocation count 135 | // and start the queueLatency timer 136 | fastify.addHook('onRequest', (req, rep, done) => { 137 | invocations.inc(); 138 | req.queueTimer = queueLatency.startTimer(); 139 | done(); 140 | }); 141 | 142 | // Just before the endpoint's handler is called (i.e. 143 | // the function is invoked), stop the queueTimer and 144 | // start the execution latency timer 145 | fastify.addHook('preHandler', (req, res, done) => { 146 | req.queueTimer(); 147 | req.execTimer = execLatency.startTimer(); 148 | done(); 149 | }); 150 | 151 | // When the response is sent, stop the execution timer 152 | fastify.addHook('onResponse', (req, res, done) => { 153 | req.execTimer(); 154 | done(); 155 | }); 156 | 157 | // If there is an error, count it 158 | fastify.addHook('onError', (req, res, done) => { 159 | errors.inc(); 160 | done(); 161 | }); 162 | 163 | // Return the endpoint handler so that the caller 164 | // can add it to the appropriate invocation context 165 | return callMetrics; 166 | }; 167 | }; 168 | 169 | // Utility function to determine if a given metric 170 | // is not supported 171 | function supported(metric) { 172 | return !osu.isNotSupported(metric); 173 | } 174 | -------------------------------------------------------------------------------- /lib/request-handler.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const healthCheck = require('./health-check'); 3 | const invocationHandler = require('./invocation-handler'); 4 | const metrics = require('./metrics'); 5 | 6 | const METRICS_URL = '/metrics'; 7 | const metricsURL = process.env.METRICS_URL || METRICS_URL; 8 | 9 | // Registers the root endpoint to respond to the user function 10 | module.exports = function use(server, opts) { 11 | // Handle function invocation 12 | let metricsHandler; 13 | server.register(function invocationContext(s, _, done) { 14 | s.register(invocationHandler, opts); 15 | // Initialize the metrics handler in this context 16 | // but register it in its own context 17 | metricsHandler = metrics(opts.funcConfig)(s); 18 | done(); 19 | }); 20 | 21 | // Gather telemetry and metrics for the function 22 | // using the handler initialized above. Register the 23 | // metrics URL and handler on a different context 24 | // so that telemetry is not measured for calls to 25 | // the metrics URL itself. 26 | server.register(function metricContext(s, _, done) { 27 | s.get(metricsURL, { logLevel: 'warn' }, metricsHandler); 28 | done(); 29 | }); 30 | 31 | // Handle health checks 32 | server.register(function healthCheckContext(s, _, done) { 33 | s.register(healthCheck(opts.funcConfig)); 34 | done(); 35 | }); 36 | }; 37 | 38 | -------------------------------------------------------------------------------- /lib/types.d.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-unused-vars */ 2 | import { CloudEvent } from 'cloudevents'; 3 | import { IncomingHttpHeaders, IncomingMessage } from 'http'; 4 | import { Http2ServerRequest, Http2ServerResponse } from 'http2'; 5 | 6 | /** 7 | * The Function interface describes a Function project, including the handler function 8 | * as well as the initialization and shutdown functions, and health checks. 9 | */ 10 | export interface Function { 11 | // The initialization function, called before the server is started 12 | // This function is optional. 13 | init?: () => (any | Promise); 14 | 15 | // The shutdown function, called after the server is stopped 16 | // This function is optional. 17 | shutdown?: () => (any | Promise); 18 | 19 | // Function that returns an array of CORS origins 20 | // This function is optional. 21 | cors?: () => string[]; 22 | 23 | // The liveness function, called to check if the server is alive 24 | // This function is optional and should return 200/OK if the server is alive. 25 | liveness?: HealthCheck; 26 | 27 | // The readiness function, called to check if the server is ready to accept requests 28 | // This function is optional and should return 200/OK if the server is ready. 29 | readiness?: HealthCheck; 30 | 31 | logLevel?: LogLevel; 32 | 33 | // The function to handle HTTP requests 34 | handle: CloudEventFunction | HTTPFunction; 35 | } 36 | 37 | /** 38 | * The HealthCheck interface describes a health check function, 39 | * including the optional path to which it should be bound. 40 | */ 41 | export interface HealthCheck { 42 | (request: Http2ServerRequest, reply: Http2ServerResponse): any; 43 | path?: string; 44 | } 45 | 46 | // InvokerOptions allow the user to configure the server. 47 | export type InvokerOptions = { 48 | 'logLevel'?: LogLevel, 49 | 'port'?: Number, 50 | 'path'?: String, 51 | 'includeRaw'?: Boolean, 52 | } 53 | 54 | /** 55 | * Log level options for the server. 56 | */ 57 | export enum LogLevel { 58 | 'trace', 'debug', 'info', 'warn', 'error', 'fatal' 59 | } 60 | 61 | /** 62 | * CloudEventFunction describes the function signature for a function that accepts CloudEvents. 63 | */ 64 | export interface CloudEventFunction { 65 | (context: Context, event?: CloudEvent): CloudEventFunctionReturn; 66 | } 67 | 68 | // CloudEventFunctionReturn is the return type for a CloudEventFunction. 69 | export type CloudEventFunctionReturn = Promise> | CloudEvent | HTTPFunctionReturn; 70 | 71 | /** 72 | * HTTPFunction describes the function signature for a function that handles 73 | * raw HTTP requests. 74 | */ 75 | export interface HTTPFunction { 76 | (context: Context, body?: IncomingBody): HTTPFunctionReturn; 77 | } 78 | 79 | // IncomingBody is the union of all possible incoming body types for HTTPFunction invocations 80 | export type IncomingBody = string | object | Buffer; 81 | 82 | // HTTPFunctionReturn is the return type for an HTTP Function. 83 | export type HTTPFunctionReturn = Promise | StructuredReturn | ResponseBody | void; 84 | 85 | // Union of the possible return types 86 | export type FunctionReturn = CloudEventFunctionReturn | HTTPFunctionReturn; 87 | 88 | // StructuredReturn is the type of the return value of an HTTP function. 89 | export interface StructuredReturn { 90 | statusCode?: number; 91 | headers?: Record; 92 | body?: ResponseBody; 93 | } 94 | 95 | // ResponseBody is the union of all possible response body types 96 | export type ResponseBody = string | object | Buffer; 97 | 98 | // Context is the request context for HTTP and CloudEvent functions. 99 | export interface Context { 100 | log: Logger; 101 | req: IncomingMessage; 102 | query?: Record; 103 | body?: Record|string; 104 | rawBody?: string; 105 | method: string; 106 | headers: IncomingHttpHeaders; 107 | httpVersion: string; 108 | httpVersionMajor: number; 109 | httpVersionMinor: number; 110 | cloudevent: CloudEvent; 111 | cloudEventResponse(data: string|object): CloudEventResponse; 112 | } 113 | 114 | export interface Logger { 115 | debug: (msg: any) => void, 116 | info: (msg: any) => void, 117 | warn: (msg: any) => void, 118 | error: (msg: any) => void, 119 | fatal: (msg: any) => void, 120 | trace: (msg: any) => void, 121 | } 122 | 123 | export interface CloudEventResponse { 124 | id(id: string): CloudEventResponse; 125 | source(source: string): CloudEventResponse; 126 | type(type: string): CloudEventResponse; 127 | version(version: string): CloudEventResponse; 128 | response(): CloudEvent; 129 | } 130 | -------------------------------------------------------------------------------- /lib/utils.js: -------------------------------------------------------------------------------- 1 | function isPromise(p) { 2 | if (typeof p === 'object' && typeof p.then === 'function') { 3 | return true; 4 | } 5 | 6 | return false; 7 | } 8 | 9 | module.exports = exports = { isPromise }; 10 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "faas-js-runtime", 3 | "version": "2.5.0", 4 | "repository": { 5 | "type": "git", 6 | "url": "https://github.com/nodeshift/faas-js-runtime.git" 7 | }, 8 | "author": "Red Hat, Inc.", 9 | "license": "Apache-2.0", 10 | "engines": { 11 | "node": "^24 || ^22 || ^20" 12 | }, 13 | "type": "commonjs", 14 | "scripts": { 15 | "lint": "eslint --ignore-path .gitignore .", 16 | "fix-lint": "eslint --fix --ignore-path .gitignore .", 17 | "test": "npm run test:source && npm run test:types", 18 | "test:source": "nyc --reporter=lcovonly tape test/test*.js | colortape", 19 | "test:types": "tsd", 20 | "pretest": "npm run lint", 21 | "sbom": "npx @cyclonedx/cyclonedx-npm --omit dev --package-lock-only --output-file sbom.json" 22 | }, 23 | "description": "A Node.js framework for executing arbitrary functions in response to HTTP or cloud events", 24 | "files": [ 25 | "index.d.ts", 26 | "index.js", 27 | "lib", 28 | "bin", 29 | "sbom.json" 30 | ], 31 | "bugs": { 32 | "url": "https://github.com/nodeshift/faas-js-runtime/issues" 33 | }, 34 | "types": "index.d.ts", 35 | "bin": "./bin/cli.js", 36 | "dependencies": { 37 | "cloudevents": "^8.0.0", 38 | "commander": "^13.1.0", 39 | "death": "^1.1.0", 40 | "fastify": "^4.21.0", 41 | "fastify-raw-body": "^4.3.0", 42 | "js-yaml": "^4.1.0", 43 | "node-os-utils": "^1.3.7", 44 | "overload-protection": "^1.2.3", 45 | "prom-client": "^15.0.0", 46 | "qs": "^6.11.2" 47 | }, 48 | "devDependencies": { 49 | "@cyclonedx/cyclonedx-npm": "^3.0.0", 50 | "@types/node": "^20.4.7", 51 | "@typescript-eslint/eslint-plugin": "^6.4.0", 52 | "@typescript-eslint/parser": "^6.4.1", 53 | "colortape": "^0.1.2", 54 | "eslint": "^8.46.0", 55 | "eslint-config-prettier": "^8.7.0", 56 | "nyc": "^15.1.0", 57 | "supertest": "^6.3.1", 58 | "tape": "^5.7.4", 59 | "tsd": "^0.28.1", 60 | "typescript": "^5.7.3" 61 | }, 62 | "tsd": { 63 | "directory": "test/types", 64 | "typingsFile": "index.d.ts" 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /sample/index.js: -------------------------------------------------------------------------------- 1 | // This is a simple Function that illustrates the structure of a Function object. 2 | // Run this Function from the root directory with the following command: 3 | // `node bin/cli.js sample/index.js` 4 | 5 | const Function = { 6 | 7 | // Handle incoming requests. This just echos the request body back to the caller. 8 | handle: (context, receivedBody) => { 9 | process.stdout.write(`In handle. Received:\n${receivedBody}\n`); 10 | return receivedBody; 11 | }, 12 | 13 | // Optional initialization function. This is called once when the Function is 14 | // started up but before it begins handling requests. This should be a 15 | // synchronous function. 16 | init: () => { 17 | process.stdout.write('In init\n'); 18 | }, 19 | 20 | // Optional shutdown function. This is called once when the Function is shut 21 | // down. This should be a synchronous function. 22 | shutdown: () => { 23 | process.stdout.write('In shutdown\n'); 24 | }, 25 | 26 | // Optional liveness function. This is called periodically to determine if the 27 | // Function is still alive. The endpoint is exposed at /alive, which can be 28 | // changed by setting the path property on the function. 29 | liveness: () => { 30 | process.stdout.write('In liveness\n'); 31 | return 'OK from liveness'; 32 | }, 33 | 34 | // Optional readiness function. This is called periodically to determine if the 35 | // Function is ready to handle requests. The endpoint is exposed at /ready, 36 | // which can be changed by setting the path property on the function. 37 | readiness: () => { 38 | process.stdout.write('In readiness\n'); 39 | return 'OK from readiness'; 40 | } 41 | }; 42 | 43 | // Optional path property. This can be used to change the path at which the 44 | // the liveness endpoint can be reached. 45 | Function.liveness.path = '/alive'; 46 | 47 | // Optional path property. This can be used to change the path at which the 48 | // the readiness endpoint can be reached. 49 | Function.readiness.path = '/ready'; 50 | 51 | module.exports = Function; 52 | -------------------------------------------------------------------------------- /test/fixtures/async/index.js: -------------------------------------------------------------------------------- 1 | module.exports = async function testFunc(_) { 2 | const ret = { 3 | message: 'This is the test function for Node.js FaaS. Success.' 4 | }; 5 | return new Promise((resolve, reject) => { 6 | setTimeout(_ => { 7 | resolve(ret); 8 | }, 500); 9 | }); 10 | }; 11 | -------------------------------------------------------------------------------- /test/fixtures/cjs-module/index.js: -------------------------------------------------------------------------------- 1 | module.exports = function testFunc(context) { 2 | return context; 3 | }; 4 | -------------------------------------------------------------------------------- /test/fixtures/cjs-module/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cjs-module", 3 | "version": "1.0.0" 4 | } 5 | -------------------------------------------------------------------------------- /test/fixtures/cloud-event/binary.js: -------------------------------------------------------------------------------- 1 | const { CloudEvent } = require('cloudevents'); 2 | 3 | module.exports = function(context, event) { 4 | return new CloudEvent({ 5 | source: 'image-func', 6 | type: 'response', 7 | datacontenttype: event.datacontenttype, 8 | data: event.data 9 | }); 10 | }; 11 | -------------------------------------------------------------------------------- /test/fixtures/cloud-event/index.js: -------------------------------------------------------------------------------- 1 | module.exports = function testFunc(context, event) { 2 | if (context.cloudevent) return { message: event.data.message }; 3 | else return new Error('No cloud event received'); 4 | }; 5 | -------------------------------------------------------------------------------- /test/fixtures/cloud-event/with-response.js: -------------------------------------------------------------------------------- 1 | module.exports = function testFunc(context, event) { 2 | if (context.cloudevent) { 3 | const response = { 4 | message: event.data.message 5 | }; 6 | return context.cloudEventResponse(response).version('0.3') 7 | .id('dummyid') 8 | .type('dev.ocf.js.type') 9 | .source('dev/ocf/js/service') 10 | .response(); 11 | } 12 | else { 13 | return new Error('No cloud event received'); 14 | } 15 | }; 16 | -------------------------------------------------------------------------------- /test/fixtures/content-type/index.js: -------------------------------------------------------------------------------- 1 | module.exports = function() { 2 | return { 3 | headers: { 4 | 'Content-Type': 'text/plain' 5 | }, 6 | body: 'Well hello there' 7 | }; 8 | }; 9 | -------------------------------------------------------------------------------- /test/fixtures/esm-module-mjs/index.mjs: -------------------------------------------------------------------------------- 1 | const handle = async (context) => { 2 | // YOUR CODE HERE 3 | context.log.info(JSON.stringify(context, null, 2)); 4 | 5 | // If the request is an HTTP POST, the context will contain the request body 6 | if (context.method === 'POST') { 7 | return { 8 | body: context.body, 9 | } 10 | // If the request is an HTTP GET, the context will include a query string, if it exists 11 | } else if (context.method === 'GET') { 12 | return { 13 | query: context.query, 14 | } 15 | } else { 16 | return { statusCode: 405, statusMessage: 'Method not allowed' }; 17 | } 18 | } 19 | 20 | // Export the function 21 | export { handle }; 22 | -------------------------------------------------------------------------------- /test/fixtures/esm-module-mjs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "esm-module-mjs", 3 | "version": "1.0.0" 4 | } 5 | -------------------------------------------------------------------------------- /test/fixtures/esm-module-type-module/index.js: -------------------------------------------------------------------------------- 1 | const handle = async context => { 2 | // YOUR CODE HERE 3 | context.log.info(JSON.stringify(context, null, 2)); 4 | 5 | // If the request is an HTTP POST, the context will contain the request body 6 | if (context.method === 'POST') { 7 | return { 8 | body: context.body, 9 | }; 10 | // If the request is an HTTP GET, the context will include a query string, if it exists 11 | } else if (context.method === 'GET') { 12 | return { 13 | query: context.query, 14 | }; 15 | } else { 16 | return { statusCode: 405, statusMessage: 'Method not allowed' }; 17 | } 18 | }; 19 | 20 | // Export the function 21 | export { handle }; 22 | -------------------------------------------------------------------------------- /test/fixtures/esm-module-type-module/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "esm-module-type-module", 3 | "version": "1.0.0", 4 | "type": "module" 5 | } 6 | -------------------------------------------------------------------------------- /test/fixtures/esm-module-type-up-dir/build/index.js: -------------------------------------------------------------------------------- 1 | const handle = async _ => 'ok'; 2 | export { handle }; 3 | -------------------------------------------------------------------------------- /test/fixtures/esm-module-type-up-dir/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "esm-module-type-module", 3 | "version": "1.0.0", 4 | "type": "module" 5 | } 6 | -------------------------------------------------------------------------------- /test/fixtures/funcyaml/func.yaml: -------------------------------------------------------------------------------- 1 | logLevel: 'info' 2 | -------------------------------------------------------------------------------- /test/fixtures/funcyaml/index.js: -------------------------------------------------------------------------------- 1 | // This is a test function that ensures a func.yaml file 2 | // can set the port and log level 3 | 4 | module.exports = function handle(context) { 5 | // See: https://github.com/pinojs/pino/issues/123 6 | if (context.log.level != 'info') { 7 | console.error(`Expected info but got ${context.log.level}`); 8 | return { statusCode: 417 }; 9 | } 10 | context.log.warn(context.log); 11 | }; 12 | -------------------------------------------------------------------------------- /test/fixtures/http-get/.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false -------------------------------------------------------------------------------- /test/fixtures/http-get/index.js: -------------------------------------------------------------------------------- 1 | // This is a test function that ensures a module specified 2 | // in package.json can be loaded and run during function runtime. 3 | 4 | const isNumber = require('is-number'); 5 | 6 | module.exports = function testFunc(context) { 7 | let ret = { ...context }; 8 | if (isNumber(ret)) throw new Error('Something is wrong with modules'); 9 | if (context.cloudevent) return context.cloudevent.data.message; 10 | 11 | return ret; 12 | }; 13 | -------------------------------------------------------------------------------- /test/fixtures/http-get/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "test", 3 | "version": "1.0.0", 4 | "lockfileVersion": 3, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "test", 9 | "version": "1.0.0", 10 | "license": "MIT", 11 | "dependencies": { 12 | "is-number": "^7.0.0" 13 | } 14 | }, 15 | "node_modules/is-number": { 16 | "version": "7.0.0", 17 | "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", 18 | "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", 19 | "engines": { 20 | "node": ">=0.12.0" 21 | } 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /test/fixtures/http-get/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "test", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "", 10 | "license": "MIT", 11 | "dependencies": { 12 | "is-number": "^7.0.0" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /test/fixtures/http-post/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = context => context.body; 4 | -------------------------------------------------------------------------------- /test/fixtures/json-input/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = context => ({lunch: context.body.lunch}); 4 | -------------------------------------------------------------------------------- /test/fixtures/knative.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nodeshift/faas-js-runtime/63346be054bbe937db911f8ecf2af956259e120a/test/fixtures/knative.gz -------------------------------------------------------------------------------- /test/fixtures/knative.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nodeshift/faas-js-runtime/63346be054bbe937db911f8ecf2af956259e120a/test/fixtures/knative.jpg -------------------------------------------------------------------------------- /test/fixtures/knative.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nodeshift/faas-js-runtime/63346be054bbe937db911f8ecf2af956259e120a/test/fixtures/knative.png -------------------------------------------------------------------------------- /test/fixtures/openwhisk-properties/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = context => { 4 | const ret = {}; 5 | ['__ow_user', '__ow_method', '__ow_headers', '__ow_path', '__ow_query', '__ow_body'] 6 | .map(v => ret[v] = context[v]); 7 | return ret; 8 | }; 9 | -------------------------------------------------------------------------------- /test/fixtures/query-params/index.js: -------------------------------------------------------------------------------- 1 | module.exports = function testFunc(context) { 2 | return { ...context.query }; 3 | }; 4 | -------------------------------------------------------------------------------- /test/fixtures/response-code/index.js: -------------------------------------------------------------------------------- 1 | module.exports = function testFunc() { 2 | return { statusCode: 451, statusMessage: 'Unavailable for Legal Reasons' }; 3 | }; 4 | -------------------------------------------------------------------------------- /test/fixtures/response-header/index.js: -------------------------------------------------------------------------------- 1 | module.exports = function() { 2 | return { 3 | headers: { 4 | 'X-announce-action': 'Saying hello' 5 | }, 6 | message: 'Well hello there' 7 | }; 8 | }; 9 | -------------------------------------------------------------------------------- /test/fixtures/response-structured/index.js: -------------------------------------------------------------------------------- 1 | module.exports = function() { 2 | return { 3 | headers: { 4 | 'Content-Type': 'image/jpeg' 5 | }, 6 | body: Buffer.from([0xd8, 0xff, 0xe0, 0xff, 0x10, 0x00, 0x46]), 7 | statusCode: 201, 8 | }; 9 | }; 10 | -------------------------------------------------------------------------------- /test/fixtures/taco.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nodeshift/faas-js-runtime/63346be054bbe937db911f8ecf2af956259e120a/test/fixtures/taco.gif -------------------------------------------------------------------------------- /test/test-binary-data.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const fs = require('fs'); 3 | const { start } = require('..'); 4 | const test = require('tape'); 5 | const request = require('supertest'); 6 | const { CloudEvent, HTTP } = require('cloudevents'); 7 | 8 | const errHandler = t => err => { 9 | t.error(err); 10 | t.end(); 11 | }; 12 | 13 | const testCases = [ 14 | { content: 'image/png', file: `${__dirname}/fixtures/knative.png` }, 15 | { content: 'image/jpg', file: `${__dirname}/fixtures/knative.jpg` }, 16 | { content: 'image/gif', file: `${__dirname}/fixtures/taco.gif` }, 17 | ]; 18 | 19 | for (const tc of testCases) { 20 | test(`Handles CloudEvents with ${tc.content} data`, t => { 21 | const data = fs.readFileSync(tc.file); 22 | const func = require(`${__dirname}/fixtures/cloud-event/binary.js`); 23 | const event = new CloudEvent({ 24 | source: 'test', 25 | type: 'test', 26 | datacontenttype: tc.content, 27 | data 28 | }); 29 | 30 | const message = HTTP.binary(event); 31 | start(func) 32 | .then(server => { 33 | request(server) 34 | .post('/') 35 | .send(message.body) 36 | .set(message.headers) 37 | .expect(200) 38 | .expect('Content-Type', tc.content) 39 | .end((err, res) => { 40 | t.error(err, 'No error'); 41 | t.deepEqual(res.body, data); 42 | t.end(); 43 | server.close(); 44 | }); 45 | }, errHandler(t)); 46 | }); 47 | } 48 | -------------------------------------------------------------------------------- /test/test-context.js: -------------------------------------------------------------------------------- 1 | const { start } = require('..'); 2 | const test = require('tape'); 3 | const request = require('supertest'); 4 | 5 | const errHandler = t => err => { 6 | t.error(err); 7 | t.end(); 8 | }; 9 | 10 | test('Provides HTTP request headers with the context parameter', t => { 11 | t.plan(2); 12 | start(context => { 13 | t.equal(typeof context.headers, 'object'); 14 | }) 15 | .then(server => { 16 | request(server) 17 | .post('/') 18 | .end((err, _) => { 19 | t.error(err, 'No error'); 20 | t.end(); 21 | server.close(); 22 | }); 23 | }, errHandler(t)); 24 | }); 25 | 26 | test('Provides HTTP request body with the context parameter', t => { 27 | t.plan(2); 28 | start(context => { 29 | t.deepEqual(context.body, { lunch: 'tacos' }); 30 | }) 31 | .then(server => { 32 | request(server) 33 | .post('/') 34 | .send('lunch=tacos') 35 | .end((err, _) => { 36 | t.error(err, 'No error'); 37 | t.end(); 38 | server.close(); 39 | }); 40 | }, errHandler(t)); 41 | }); 42 | 43 | test('Provides HTTP request rawBody with the context parameter', t => { 44 | t.plan(3); 45 | start(context => { 46 | t.deepEqual(context.body, { lunch: 'tacos' }); 47 | t.equal(context.rawBody, 'lunch=tacos'); 48 | }, {includeRaw: true}) 49 | .then(server => { 50 | request(server) 51 | .post('/') 52 | .send('lunch=tacos') 53 | .end((err, _) => { 54 | t.error(err, 'No error'); 55 | t.end(); 56 | server.close(); 57 | }); 58 | }, errHandler(t)); 59 | }); 60 | 61 | test('Provides HTTP request query parameters with the context parameter', t => { 62 | const func = require(`${__dirname}/fixtures/query-params/`); 63 | start(func) 64 | .then(server => { 65 | t.plan(3); 66 | request(server) 67 | .get('/?lunch=tacos&supper=burgers') 68 | .expect(200) 69 | .expect('Content-Type', /json/) 70 | .end((err, res) => { 71 | t.error(err, 'No error'); 72 | t.equal(res.body.lunch, 'tacos'); 73 | t.equal(res.body.supper, 'burgers'); 74 | t.end(); 75 | server.close(); 76 | }); 77 | }, errHandler(t)); 78 | }); 79 | 80 | test('Provides HTTP method information with the context parameter', t => { 81 | t.plan(2); 82 | let context; 83 | start(c => context = c) 84 | .then(server => { 85 | request(server) 86 | .get('/') 87 | .end((err, _) => { 88 | t.error(err, 'No error'); 89 | t.equal(context.method, 'GET'); 90 | t.end(); 91 | server.close(); 92 | }); 93 | }, errHandler(t)); 94 | }); 95 | 96 | test('Provides HTTP version information with the context parameter', t => { 97 | t.plan(4); 98 | let context; 99 | start(c => context = c) 100 | .then(server => { 101 | request(server) 102 | .get('/') 103 | .end((err, _) => { 104 | t.error(err, 'No error'); 105 | t.equal(context.httpVersion, '1.1'); 106 | t.equal(context.httpVersionMajor, 1); 107 | t.equal(context.httpVersionMinor, 1); 108 | t.end(); 109 | server.close(); 110 | }); 111 | }, errHandler(t)); 112 | }); 113 | 114 | -------------------------------------------------------------------------------- /test/test-errors.js: -------------------------------------------------------------------------------- 1 | const { start } = require('..'); 2 | const test = require('tape'); 3 | const request = require('supertest'); 4 | 5 | const errHandler = t => err => { 6 | t.error(err); 7 | t.end(); 8 | }; 9 | 10 | test('Returns HTTP error code if a caught error has one', t => { 11 | t.plan(2); 12 | start(_ => { 13 | const error = new Error('Unavailable for Legal Reasons'); 14 | error.code = 451; 15 | throw error; 16 | }) 17 | .then(server => { 18 | request(server) 19 | .post('/') 20 | .expect(451) 21 | .expect('Content-type', /text/) 22 | .end((err, resp) => { 23 | t.error(err, 'No error'); 24 | t.equal(resp.statusCode, 451, 'Status code'); 25 | t.end(); 26 | server.close(); 27 | }); 28 | }, errHandler(t)); 29 | }); 30 | 31 | test('Prints an error message when an exception is thrown', t => { 32 | t.plan(2); 33 | const func = _ => { throw new Error('This is the error message'); }; 34 | start(func) 35 | .then(server => { 36 | request(server) 37 | .post('/') 38 | .expect(500) 39 | .end((err, resp) => { 40 | t.error(err, 'No error'); 41 | t.equal(resp.text, 'This is the error message', 'Error message'); 42 | t.end(); 43 | server.close(); 44 | }); 45 | }, errHandler(t)); 46 | }); 47 | -------------------------------------------------------------------------------- /test/test-function-loading.js: -------------------------------------------------------------------------------- 1 | const test = require('tape'); 2 | const path = require('path'); 3 | const { loadFunction } = require('../lib/function-loader.js'); 4 | 5 | const fixtureDir = path.join(__dirname, 'fixtures'); 6 | 7 | test('Loads a CJS function', async t => { 8 | const fn = await loadFunction(path.join(fixtureDir, 'cjs-module', 'index.js')); 9 | t.equal(typeof fn, 'function'); 10 | t.pass('CJS function loaded'); 11 | }); 12 | 13 | test('Loads an ESM function with an .mjs extension', async t => { 14 | const fn = await loadFunction(path.join(fixtureDir, 'esm-module-mjs', 'index.mjs')); 15 | t.equal(typeof fn.handle, 'function'); 16 | t.pass('ESM module with a mjs ext loaded'); 17 | }); 18 | 19 | test('Loads an ESM function with the type=module in the package.json', async t => { 20 | const fn = await loadFunction(path.join(fixtureDir, 'esm-module-type-module', 'index.js')); 21 | t.equal(typeof fn.handle, 'function'); 22 | t.pass('ESM module with a type=module in the package.json'); 23 | }); 24 | 25 | test('Loads an ESM function with the type=module in package.json in a parent dir and a .js extension', async t => { 26 | const fn = await loadFunction(path.join(fixtureDir, 'esm-module-type-up-dir', 'build', 'index.js')); 27 | t.equal(typeof fn.handle, 'function'); 28 | t.pass('ESM module with a type=module in a package.json in parent directory'); 29 | }); 30 | -------------------------------------------------------------------------------- /test/test-http-body.js: -------------------------------------------------------------------------------- 1 | const { start } = require('..'); 2 | const test = require('tape'); 3 | const request = require('supertest'); 4 | 5 | const errHandler = t => err => { 6 | t.error(err); 7 | t.end(); 8 | }; 9 | 10 | test('Provides HTTP POST string as string parameter', t => { 11 | const body = 'This is a string'; 12 | t.plan(2); 13 | start((context, receivedBody) => { 14 | t.equal(receivedBody, body); 15 | return receivedBody; 16 | }) 17 | .then(server => { 18 | request(server) 19 | .post('/') 20 | .send(body) 21 | .set({'Content-Type': 'text/plain'}) 22 | .expect('Content-Type', /plain/) 23 | .end((err, _) => { 24 | t.error(err, 'No error'); 25 | t.end(); 26 | server.close(); 27 | }); 28 | }, errHandler(t)); 29 | }); 30 | 31 | test('Provides HTTP POST string as string parameter and rawBody', t => { 32 | const body = 'This is a string'; 33 | t.plan(3); 34 | start((context, receivedBody) => { 35 | t.equal(receivedBody, body); 36 | t.equal(context.rawBody, body); 37 | return receivedBody; 38 | }, {includeRaw: true}) 39 | .then(server => { 40 | request(server) 41 | .post('/') 42 | .send(body) 43 | .set({'Content-Type': 'text/plain'}) 44 | .expect('Content-Type', /plain/) 45 | .end((err, _) => { 46 | t.error(err, 'No error'); 47 | t.end(); 48 | server.close(); 49 | }); 50 | }, errHandler(t)); 51 | }); 52 | 53 | test('Provides HTTP POST JSON as object parameter', t => { 54 | const body = '{"lunch": "pizza"}'; 55 | t.plan(2); 56 | start((context, receivedBody) => { 57 | t.deepEqual(receivedBody, JSON.parse(body)); 58 | return receivedBody; 59 | }) 60 | .then(server => { 61 | request(server) 62 | .post('/') 63 | .send(body) 64 | .set({'Content-Type': 'application/json'}) 65 | .expect('Content-Type', /json/) 66 | .end((err, _) => { 67 | t.error(err, 'No error'); 68 | t.end(); 69 | server.close(); 70 | }); 71 | }, errHandler(t)); 72 | }); 73 | 74 | test('Provides HTTP POST JSON as object parameter with correct rawBody', t => { 75 | const body = '{"lunch": "pizza"}'; 76 | t.plan(3); 77 | start((context, receivedBody) => { 78 | t.deepEqual(receivedBody, JSON.parse(body)); 79 | t.equal(context.rawBody, body); 80 | return receivedBody; 81 | }, {includeRaw: true}) 82 | .then(server => { 83 | request(server) 84 | .post('/') 85 | .send(body) 86 | .set({'Content-Type': 'application/json'}) 87 | .expect('Content-Type', /json/) 88 | .end((err, _) => { 89 | t.error(err, 'No error'); 90 | t.end(); 91 | server.close(); 92 | }); 93 | }, errHandler(t)); 94 | }); 95 | 96 | test('Provides HTTP POST empty body as empty string parameter', t => { 97 | const body = ''; 98 | t.plan(2); 99 | start((context, receivedBody) => { 100 | t.deepEqual(receivedBody, body); 101 | return receivedBody; 102 | }) 103 | .then(server => { 104 | request(server) 105 | .post('/') 106 | .send(body) 107 | .set({'Content-Type': 'text/plain'}) 108 | .expect('Content-Type', /plain/) 109 | .end((err, _) => { 110 | t.error(err, 'No error'); 111 | t.end(); 112 | server.close(); 113 | }); 114 | }, errHandler(t)); 115 | }); 116 | 117 | test('Provides HTTP POST empty body as empty string parameter and rawBody', t => { 118 | const body = ''; 119 | t.plan(3); 120 | start((context, receivedBody) => { 121 | t.equal(receivedBody, body); 122 | t.equal(context.rawBody, body); 123 | return receivedBody; 124 | }, {includeRaw: true}) 125 | .then(server => { 126 | request(server) 127 | .post('/') 128 | .send(body) 129 | .set({'Content-Type': 'text/plain'}) 130 | .expect('Content-Type', /plain/) 131 | .end((err, _) => { 132 | t.error(err, 'No error'); 133 | t.end(); 134 | server.close(); 135 | }); 136 | }, errHandler(t)); 137 | }); 138 | 139 | test('Rejects HTTP POST exceeding bodyLimit', t => { 140 | const largeBody = 'x'.repeat(1048576 + 1); 141 | t.plan(2); 142 | start((_, receivedBody) => receivedBody, { bodyLimit: 1048576 }) 143 | .then(server => { 144 | request(server) 145 | .post('/') 146 | .send(largeBody) 147 | .set({ 'Content-Type': 'text/plain' }) 148 | .expect(413) 149 | .end((err, res) => { 150 | t.error(err, 'No error'); 151 | t.equal(res.status, 413, 'Should reject payload larger than limit'); 152 | t.end(); 153 | server.close(); 154 | }); 155 | }, errHandler(t)); 156 | }); 157 | 158 | test('Accepts HTTP POST within bodyLimit', t => { 159 | const body = 'x'.repeat(1048576); 160 | t.plan(2); 161 | start((_, receivedBody) => { 162 | t.equal(receivedBody, body); 163 | return receivedBody; 164 | }, { bodyLimit: 1048576 }) 165 | .then(server => { 166 | request(server) 167 | .post('/') 168 | .send(body) 169 | .set({ 'Content-Type': 'text/plain' }) 170 | .expect('Content-Type', /plain/) 171 | .expect(200) 172 | .end((err, _) => { 173 | t.error(err, 'No error'); 174 | t.end(); 175 | server.close(); 176 | }); 177 | }, errHandler(t)); 178 | }); 179 | 180 | test('Rejects payload exceeding environment variable bodyLimit', t => { 181 | const bodyLimit = 1048576; 182 | const largeBody = 'x'.repeat(bodyLimit + 1); 183 | process.env.FUNC_BODY_LIMIT = bodyLimit; 184 | t.plan(2); 185 | start((_, receivedBody) => receivedBody) 186 | .then(server => { 187 | request(server) 188 | .post('/') 189 | .send(largeBody) 190 | .set({ 'Content-Type': 'text/plain' }) 191 | .expect(413) 192 | .end((err, res) => { 193 | t.error(err, 'No error'); 194 | t.equal(res.status, 413, 'Should reject payload larger than env var limit'); 195 | delete process.env.FUNC_BODY_LIMIT; 196 | t.end(); 197 | server.close(); 198 | }); 199 | }, errHandler(t)); 200 | }); 201 | 202 | test('Accepts payload within environment variable bodyLimit', t => { 203 | const bodyLimit = 524288; 204 | const body = 'x'.repeat(bodyLimit); 205 | process.env.FUNC_BODY_LIMIT = bodyLimit; 206 | t.plan(2); 207 | start((_, receivedBody) => { 208 | t.equal(receivedBody, body); 209 | return receivedBody; 210 | }) 211 | .then(server => { 212 | request(server) 213 | .post('/') 214 | .send(body) 215 | .set({ 'Content-Type': 'text/plain' }) 216 | .expect('Content-Type', /plain/) 217 | .expect(200) 218 | .end((err, _) => { 219 | t.error(err, 'No error'); 220 | delete process.env.FUNC_BODY_LIMIT; 221 | t.end(); 222 | server.close(); 223 | }); 224 | }, errHandler(t)); 225 | }); -------------------------------------------------------------------------------- /test/test-lifecycle.js: -------------------------------------------------------------------------------- 1 | const { start } = require('..'); 2 | const test = require('tape'); 3 | const request = require('supertest'); 4 | 5 | const defaults = { logLevel: 'silent' }; 6 | 7 | test('Enforces the handle function to exist', async t => { 8 | t.plan(1); 9 | try { 10 | await start({}, defaults); 11 | } catch (err) { 12 | t.ok(err.message.includes('handle'), 'handle function is required'); 13 | } 14 | }); 15 | 16 | test('Calls init before the server has started', async t => { 17 | t.plan(1); 18 | let initCalled = false; 19 | const server = await start( 20 | { 21 | init: () => { 22 | initCalled = true; 23 | }, 24 | handle: () => {}, 25 | }, 26 | defaults 27 | ); 28 | t.ok(initCalled, 'init was called'); 29 | server.close(); 30 | }); 31 | 32 | test('Calls async init before the server has started', async t => { 33 | t.plan(1); 34 | let initCalled = false; 35 | const server = await start( 36 | { 37 | init: async() => { 38 | initCalled = true; 39 | }, 40 | handle: () => {}, 41 | }, 42 | defaults 43 | ); 44 | t.ok(initCalled, 'init was called'); 45 | server.close(); 46 | }); 47 | 48 | test('Bubbles up any exceptions thrown by init()', t => { 49 | t.plan(1); 50 | start( 51 | { 52 | init: () => { 53 | throw new Error('init failed'); 54 | }, 55 | handle: () => {}, 56 | }, 57 | defaults 58 | ) 59 | .then(() => t.fail('should not have resolved')) 60 | .catch(err => t.ok(err.message.includes('init failed'), 'init failed')); 61 | }); 62 | 63 | test('Calls shutdown after the server has stopped', async t => { 64 | t.plan(1); 65 | let shutdownCalled = false; 66 | const server = await start( 67 | { 68 | handle: () => {}, 69 | shutdown: _ => { 70 | shutdownCalled = true; 71 | }, 72 | }, 73 | defaults 74 | ); 75 | t.ok(!shutdownCalled, 'shutdown was not called before server.close()'); 76 | return new Promise(resolve => { 77 | // TODO: It would be nice to check for the shutdown call here 78 | // but it's not clear how to do that if we are only hooking on 79 | // signal interrupts. 80 | server.close(resolve); 81 | }); 82 | }); 83 | 84 | test('Calls async shutdown after the server has stopped', async t => { 85 | t.plan(1); 86 | let shutdownCalled = false; 87 | const server = await start( 88 | { 89 | handle: () => {}, 90 | shutdown: async() => { 91 | shutdownCalled = true; 92 | }, 93 | }, 94 | defaults 95 | ); 96 | t.ok(!shutdownCalled, 'shutdown was not called before server.close()'); 97 | return new Promise(resolve => { 98 | // TODO: It would be nice to check for the shutdown call here 99 | // but it's not clear how to do that if we are only hooking on 100 | // signal interrupts. 101 | server.close(resolve); 102 | }); 103 | }); 104 | 105 | test('Liveness endpoint can be provided', t => { 106 | start({ 107 | handle: _ => 'OK', 108 | liveness: _ => 'I am alive', 109 | }).then(server => { 110 | t.plan(2); 111 | request(server) 112 | .get('/health/liveness') 113 | .expect(200) 114 | .expect('Content-type', /text/) 115 | .end((err, res) => { 116 | t.error(err, 'No error'); 117 | t.equal(res.text, 'I am alive'); 118 | t.end(); 119 | server.close(); 120 | }); 121 | }); 122 | }); 123 | 124 | test('Liveness route can be provided', t => { 125 | function liveness() { 126 | return 'I am alive'; 127 | } 128 | liveness.path = '/alive'; 129 | start({ 130 | handle: _ => 'OK', 131 | liveness, 132 | }).then(server => { 133 | t.plan(2); 134 | request(server) 135 | .get('/alive') 136 | .expect(200) 137 | .expect('Content-type', /text/) 138 | .end((err, res) => { 139 | t.error(err, 'No error'); 140 | t.equal(res.text, 'I am alive'); 141 | t.end(); 142 | server.close(); 143 | }); 144 | }); 145 | }); 146 | 147 | test('Readiness endpoint can be provided', t => { 148 | start({ 149 | handle: _ => 'OK', 150 | readiness: _ => 'I am ready', 151 | }).then(server => { 152 | t.plan(2); 153 | request(server) 154 | .get('/health/readiness') 155 | .expect(200) 156 | .expect('Content-type', /text/) 157 | .end((err, res) => { 158 | t.error(err, 'No error'); 159 | t.equal(res.text, 'I am ready'); 160 | t.end(); 161 | server.close(); 162 | }); 163 | }); 164 | }); 165 | 166 | test('Readiness route can be provided', t => { 167 | function readiness() { 168 | return 'I am ready'; 169 | } 170 | readiness.path = '/ready'; 171 | start({ 172 | handle: _ => 'OK', 173 | readiness, 174 | }).then(server => { 175 | t.plan(2); 176 | request(server) 177 | .get('/ready') 178 | .expect(200) 179 | .expect('Content-type', /text/) 180 | .end((err, res) => { 181 | t.error(err, 'No error'); 182 | t.equal(res.text, 'I am ready'); 183 | t.end(); 184 | server.close(); 185 | }); 186 | }); 187 | }); 188 | -------------------------------------------------------------------------------- /test/test-metrics.js: -------------------------------------------------------------------------------- 1 | const { start } = require('..'); 2 | const test = require('tape'); 3 | const request = require('supertest'); 4 | 5 | process.env.FAAS_ID = 'test'; 6 | 7 | const tests = [ 8 | {test: 'Invocation count', expected: 'faas_invocations counter'}, 9 | {test: 'Error count', expected: 'faas_errors counter'}, 10 | {test: 'Cold start latency', expected: 'faas_cold_start_latency histogram'}, 11 | {test: 'Execution latency', expected: 'faas_execution_latency histogram'}, 12 | {test: 'Queue latency', expected: 'faas_queue_latency histogram'}, 13 | {test: 'CPU utilization', expected: 'faas_cpu_utilization gauge'}, 14 | {test: 'Memory utilization', expected: 'faas_mem_utilization gauge'}, 15 | {test: 'Network I/O utilization', expected: 'faas_netio_utilization gauge'}, 16 | ]; 17 | 18 | for (const tc of tests) { 19 | test(tc.test, async t => testMetricsInclude(`# TYPE ${tc.expected}`, t)); 20 | } 21 | 22 | 23 | test('Exposes a /metrics endpoint', async t => { 24 | const server = await start(_ => _); 25 | try { 26 | await callEndpointNoError(server, '/metrics', t); 27 | } finally { 28 | server.close(); 29 | } 30 | }); 31 | 32 | test('Only cacluates metrics for calls to /', async t => { 33 | // eslint-disable-next-line max-len 34 | const metric = 'faas_invocations{faas_name="anonymous",faas_id="test",faas_instance="undefined",faas_version="undefined",faas_runtime="Node.js"}'; 35 | const expected = `${metric} 1`; 36 | const server = await start(_ => _); 37 | try { 38 | await callEndpointNoError(server, '/', t); 39 | await requestMetricsAndValidate(server, t, expected); 40 | await callEndpointNoError(server, '/health/readiness', t); 41 | await requestMetricsAndValidate(server, t, expected); 42 | await callEndpointNoError(server, '/', t); 43 | // We've now called the function twice 44 | await requestMetricsAndValidate(server, t, `${metric} 2`); 45 | } finally { 46 | server.close(); 47 | } 48 | }); 49 | 50 | 51 | async function requestMetricsAndValidate(server, t, expected) { 52 | const got = await callEndpointNoError(server, '/metrics', t); 53 | if (!got.text.includes(expected)) { 54 | console.error(`EXPECTED: ${expected}\nGOT: ${got.text}`); 55 | } 56 | t.ok(got.text.includes(expected), expected); 57 | } 58 | 59 | // A helper function that calls a server endpoint with a 60 | // GET request and reports any error 61 | async function callEndpointNoError(server, endpoint, t) { 62 | return request(server).get(endpoint).catch(t.error); 63 | } 64 | 65 | // A helper function that will test if the /metrics endpoint 66 | // includes the expected metric in its response 67 | async function testMetricsInclude(metric, t) { 68 | const server = await start(_ => _); 69 | try { 70 | const got = await callEndpointNoError(server, '/metrics', t); 71 | t.ok(got.text.includes(metric), metric); 72 | } finally { 73 | server.close(); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /test/test-scope.js: -------------------------------------------------------------------------------- 1 | 2 | const { start } = require('..'); 3 | const test = require('tape'); 4 | const request = require('supertest'); 5 | 6 | function declaredFunction() { 7 | return { 8 | hasGlobal: this.global !== undefined, 9 | isFrozenThis: Object.isFrozen(this) 10 | }; 11 | } 12 | const arrowFunction = () => ({ 13 | hasGlobal: this.global !== undefined, 14 | isFrozenThis: Object.isFrozen(this) 15 | }); 16 | 17 | const errHandler = t => err => { 18 | t.error(err); 19 | t.end(); 20 | }; 21 | 22 | const tests = { 23 | arrowFunctionTests: [ 24 | { 25 | test: 'Does not have a global object', 26 | func: checkHasGlobal, 27 | expect: false 28 | }, 29 | { 30 | test: 'Has unfrozen this', 31 | func: checkIsFrozen, 32 | expect: false 33 | } 34 | ], 35 | declaredFunctionTests: [ 36 | { 37 | test: 'Does not have a global object', 38 | func: checkHasGlobal, 39 | expect: false 40 | }, 41 | { 42 | test: 'Has frozen this', 43 | func: checkIsFrozen, 44 | expect: true 45 | } 46 | ] 47 | }; 48 | 49 | for (const tt of tests.declaredFunctionTests) { 50 | test(tt.test, t => { 51 | start(declaredFunction) 52 | .then(s => tt.func(s, t, tt.expect), errHandler(t)); 53 | }); 54 | } 55 | 56 | for (const tt of tests.arrowFunctionTests) { 57 | test(tt.test, t => { 58 | start(arrowFunction) 59 | .then(s => tt.func(s, t, tt.expect), errHandler(t)); 60 | }); 61 | } 62 | 63 | function checkHasGlobal(server, t, expected) { 64 | request(server) 65 | .get('/') 66 | .expect(200) 67 | .end((err, res) => { 68 | t.error(err, 'No error'); 69 | t.equal(res.body.hasGlobal, expected); 70 | t.end(); 71 | server.close(); 72 | }); 73 | } 74 | 75 | function checkIsFrozen(server, t, expected) { 76 | request(server) 77 | .get('/') 78 | .expect(200) 79 | .end((err, res) => { 80 | t.error(err, 'No error'); 81 | t.equal(res.body.isFrozenThis, expected); 82 | t.end(); 83 | server.close(); 84 | }); 85 | } 86 | -------------------------------------------------------------------------------- /test/test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { start } = require('..'); 4 | const test = require('tape'); 5 | const request = require('supertest'); 6 | const { CONSTANTS } = require('cloudevents'); 7 | 8 | // package.json handling 9 | const { existsSync, readdirSync } = require('fs'); 10 | const { execSync } = require('child_process'); 11 | const path = require('path'); 12 | const { CloudEvent, HTTP } = require('cloudevents'); 13 | 14 | // Because we are loading so many functions in a single test 15 | // process, and because all of those functions add listeners 16 | // to the same event emitter, we need to make sure that we 17 | // have set the max listeners to a high enough number. 18 | // Otherwise, we will get a warning. 19 | require('events').EventEmitter.defaultMaxListeners = 100; 20 | 21 | // Ensure fixture dependencies are installed 22 | const fixtureDir = path.join(__dirname, 'fixtures'); 23 | const fixtures = readdirSync(fixtureDir); 24 | fixtures.forEach(installDependenciesIfExist); 25 | 26 | function installDependenciesIfExist(functionPath) { 27 | if (path.extname(functionPath) !== '') { 28 | functionPath = path.dirname(functionPath); 29 | } 30 | functionPath = path.join(fixtureDir, functionPath); 31 | if (existsSync(path.join(functionPath, 'package.json'))) { 32 | // eslint-disable-next-line 33 | console.log(`Installing dependencies for ${functionPath}`); 34 | execSync('npm install --omit=dev', { cwd: functionPath }); 35 | } 36 | } 37 | 38 | const errHandler = t => err => { 39 | t.error(err); 40 | t.end(); 41 | }; 42 | 43 | test('Loads a user function with dependencies', t => { 44 | const func = require(`${__dirname}/fixtures/http-get/`); 45 | start(func) 46 | .then(server => { 47 | t.plan(2); 48 | request(server) 49 | .get('/') 50 | .expect(200) 51 | .expect('Content-Type', /json/) 52 | .end((err, res) => { 53 | t.error(err, 'No error'); 54 | t.equal(typeof res.body, 'object'); 55 | t.end(); 56 | server.close(); 57 | }); 58 | }, errHandler(t)); 59 | }); 60 | 61 | test('Can respond via an async function', t => { 62 | const func = require(`${__dirname}/fixtures/async/`); 63 | start(func) 64 | .then(server => { 65 | t.plan(2); 66 | request(server) 67 | .get('/') 68 | .expect(200) 69 | .expect('Content-Type', /json/) 70 | .end((err, res) => { 71 | t.error(err, 'No error'); 72 | t.deepEqual(res.body, 73 | { message: 'This is the test function for Node.js FaaS. Success.' }); 74 | t.end(); 75 | server.close(); 76 | }); 77 | }, errHandler(t)); 78 | }); 79 | 80 | test('Accepts HTTP POST requests', t => { 81 | const func = require(`${__dirname}/fixtures/http-post/`); 82 | start(func) 83 | .then(server => { 84 | request(server) 85 | .post('/') 86 | .send('message=Message body') 87 | .expect(200) 88 | .expect('Content-Type', /json/) 89 | .end((err, res) => { 90 | t.error(err); 91 | t.equal(res.body.message, 'Message body'); 92 | t.end(); 93 | server.close(); 94 | }); 95 | }, errHandler(t)); 96 | }); 97 | 98 | test('Responds to 0.3 binary cloud events', t => { 99 | const func = require(`${__dirname}/fixtures/cloud-event/`); 100 | start(func) 101 | .then(server => { 102 | request(server) 103 | .post('/') 104 | .send({ message: 'hello' }) 105 | .set(CONSTANTS.CE_HEADERS.ID, '1') 106 | .set(CONSTANTS.CE_HEADERS.SOURCE, 'integration-test') 107 | .set(CONSTANTS.CE_HEADERS.TYPE, 'dev.knative.example') 108 | .set(CONSTANTS.CE_HEADERS.SPEC_VERSION, '0.3') 109 | .expect(200) 110 | .expect('Content-Type', /json/) 111 | .end((err, res) => { 112 | t.error(err, 'No error'); 113 | t.deepEqual(res.body, { message: 'hello' }); 114 | t.end(); 115 | server.close(); 116 | }); 117 | }, errHandler(t)); 118 | }); 119 | 120 | test('Responds with 0.3 binary cloud event', t => { 121 | const func = require(`${__dirname}/fixtures/cloud-event/with-response.js`); 122 | start(func) 123 | .then(server => { 124 | request(server) 125 | .post('/') 126 | .send({ message: 'hello' }) 127 | .set(CONSTANTS.CE_HEADERS.ID, '1') 128 | .set(CONSTANTS.CE_HEADERS.SOURCE, 'integration-test') 129 | .set(CONSTANTS.CE_HEADERS.TYPE, 'dev.knative.example') 130 | .set(CONSTANTS.CE_HEADERS.SPEC_VERSION, '0.3') 131 | .expect(200) 132 | .expect('Content-Type', /json/) 133 | .end((err, res) => { 134 | t.error(err, 'No error'); 135 | t.equal(res.body.message, 'hello'); 136 | t.equal(res.headers[CONSTANTS.CE_HEADERS.TYPE], 'dev.ocf.js.type'); 137 | t.equal(res.headers[CONSTANTS.CE_HEADERS.SOURCE], 'dev/ocf/js/service'); 138 | t.equal(res.headers[CONSTANTS.CE_HEADERS.ID], 'dummyid'); 139 | t.equal(res.headers[CONSTANTS.CE_HEADERS.SPEC_VERSION], '0.3'); 140 | t.end(); 141 | server.close(); 142 | }); 143 | }, errHandler(t)); 144 | }); 145 | 146 | test('Responds to 1.0 binary cloud events', t => { 147 | const func = require(`${__dirname}/fixtures/cloud-event/`); 148 | start(func) 149 | .then(server => { 150 | request(server) 151 | .post('/') 152 | .send({ message: 'hello' }) 153 | .set(CONSTANTS.CE_HEADERS.ID, '1') 154 | .set(CONSTANTS.CE_HEADERS.SOURCE, 'integration-test') 155 | .set(CONSTANTS.CE_HEADERS.TYPE, 'dev.knative.example') 156 | .set(CONSTANTS.CE_HEADERS.SPEC_VERSION, '1.0') 157 | .expect(200) 158 | .expect('Content-Type', /json/) 159 | .end((err, res) => { 160 | t.error(err, 'No error'); 161 | t.deepEqual(res.body, { message: 'hello' }); 162 | t.end(); 163 | server.close(); 164 | }); 165 | }, errHandler(t)); 166 | }); 167 | 168 | test('Responds to 1.0 structured cloud events', t => { 169 | const func = require(`${__dirname}/fixtures/cloud-event/`); 170 | start(func) 171 | .then(server => { 172 | request(server) 173 | .post('/') 174 | .send({ 175 | id: '1', 176 | source: 'http://integration-test', 177 | type: 'com.redhat.faas.test', 178 | specversion: '1.0', 179 | data: { 180 | message: 'hello' 181 | } 182 | }) 183 | .set('Content-type', 'application/cloudevents+json; charset=utf-8') 184 | .expect(200) 185 | .expect('Content-Type', /json/) 186 | .end((err, res) => { 187 | t.error(err, 'No error'); 188 | t.deepEqual(res.body, {message: 'hello'}); 189 | t.end(); 190 | server.close(); 191 | }); 192 | }, errHandler(t)); 193 | }); 194 | 195 | test('Handles 1.0 CloudEvent responses', t => { 196 | start(_ => new CloudEvent({ 197 | source: 'test', 198 | type: 'test-type', 199 | data: 'some data', 200 | datacontenttype: 'text/plain' 201 | })) 202 | .then(server => { 203 | request(server) 204 | .post('/') 205 | .send({ message: 'hello' }) 206 | .set(CONSTANTS.CE_HEADERS.ID, '1') 207 | .set(CONSTANTS.CE_HEADERS.SOURCE, 'integration-test') 208 | .set(CONSTANTS.CE_HEADERS.TYPE, 'dev.knative.example') 209 | .set(CONSTANTS.CE_HEADERS.SPEC_VERSION, '1.0') 210 | .expect(200) 211 | .expect('Content-Type', /text/) 212 | .end((err, res) => { 213 | t.error(err, 'No error'); 214 | t.equal(res.text, 'some data'); 215 | t.end(); 216 | server.close(); 217 | }); 218 | }, errHandler(t)); 219 | }); 220 | 221 | test('Handles 1.0 CloudEvent Message responses', t => { 222 | start(_ => HTTP.binary(new CloudEvent({ 223 | source: 'test', 224 | type: 'test-type', 225 | data: 'some data', 226 | datacontenttype: 'text/plain' 227 | }))) 228 | .then(server => { 229 | request(server) 230 | .post('/') 231 | .send({ message: 'hello' }) 232 | .set(CONSTANTS.CE_HEADERS.ID, '1') 233 | .set(CONSTANTS.CE_HEADERS.SOURCE, 'integration-test') 234 | .set(CONSTANTS.CE_HEADERS.TYPE, 'dev.knative.example') 235 | .set(CONSTANTS.CE_HEADERS.SPEC_VERSION, '1.0') 236 | .expect(200) 237 | .expect('Content-Type', /text/) 238 | .end((err, res) => { 239 | t.error(err, 'No error'); 240 | t.equal(res.text, 'some data'); 241 | t.end(); 242 | server.close(); 243 | }); 244 | }, errHandler(t)); 245 | }); 246 | 247 | test('Extracts event data as the second parameter to a function', t => { 248 | const data = { 249 | lunch: 'tacos', 250 | }; 251 | 252 | start((context, menu) => { 253 | t.equal(menu.data.lunch, data.lunch); 254 | return menu; 255 | }) 256 | .then(server => { 257 | request(server) 258 | .post('/') 259 | .send({ 260 | id: '1', 261 | source: 'http://integration-test', 262 | type: 'com.redhat.faas.test', 263 | specversion: '1.0', 264 | data 265 | }) 266 | .set('Content-type', 'application/cloudevents+json; charset=utf-8') 267 | .expect(200) 268 | .expect('Content-Type', /json/) 269 | .end((err, res) => { 270 | t.error(err, 'No error'); 271 | t.deepEqual(res.body, data); 272 | t.end(); 273 | server.close(); 274 | }); 275 | }, errHandler(t)); 276 | }); 277 | 278 | test('Successfully handles events with no data', t => { 279 | start((context, event) => { 280 | t.ok(!event.data); 281 | t.true(context.cloudevent instanceof CloudEvent); 282 | return { status: 'done' }; 283 | }) 284 | .then(server => { 285 | request(server) 286 | .post('/') 287 | .set(CONSTANTS.CE_HEADERS.ID, '1') 288 | .set(CONSTANTS.CE_HEADERS.TYPE, 'test') 289 | .set(CONSTANTS.CE_HEADERS.SOURCE, 'test') 290 | .set(CONSTANTS.CE_HEADERS.SPEC_VERSION, '1.0') 291 | .expect(200) 292 | .expect('Content-Type', /json/) 293 | .end((err, res) => { 294 | t.error(err, 'No error'); 295 | t.deepEqual(res.body, { status: 'done' }); 296 | t.end(); 297 | server.close(); 298 | }); 299 | }, errHandler(t)); 300 | }); 301 | 302 | // @see https://github.com/cloudevents/sdk-javascript/issues/332 303 | // test('Responds with 406 Not Acceptable to unknown cloud event versions', t => { 304 | // const func = require(`${__dirname}/fixtures/cloud-event/`); 305 | // framework(func) 306 | // .then(server => { 307 | // request(server) 308 | // .post('/') 309 | // .send({ message: 'hello' }) 310 | // .set(CONSTANTS.CE_HEADERS.ID, '1') 311 | // .set(CONSTANTS.CE_HEADERS.SOURCE, 'integration-test') 312 | // .set(CONSTANTS.CE_HEADERS.TYPE, 'dev.knative.example') 313 | // .set(CONSTANTS.CE_HEADERS.SPEC_VERSION, '11.0') 314 | // .expect(406) 315 | // .expect('Content-Type', /json/) 316 | // .end((err, res) => { 317 | // t.error(err, 'No error'); 318 | // t.equal(res.body.statusCode, 406); 319 | // t.equal(res.body.message, 'invalid spec version 11.0'); 320 | // t.end(); 321 | // server.close(); 322 | // }); 323 | // }); 324 | // }); 325 | 326 | test('Respects response code set by the function', t => { 327 | const func = require(`${__dirname}/fixtures/response-code/`); 328 | start(func) 329 | .then(server => { 330 | t.plan(1); 331 | request(server) 332 | .get('/') 333 | .expect(451) 334 | .expect('Content-Type', /json/) 335 | .end(err => { 336 | t.error(err, 'No error'); 337 | t.end(); 338 | server.close(); 339 | }); 340 | }, errHandler(t)); 341 | }); 342 | 343 | test('Responds HTTP 204 if response body has no content', t => { 344 | start(_ => '') 345 | .then(server => { 346 | t.plan(2); 347 | request(server) 348 | .get('/') 349 | .expect(204) 350 | .set({'Content-Type': 'text/plain'}) 351 | .expect('Content-Type', /plain/) 352 | .end((err, res) => { 353 | t.error(err, 'No error'); 354 | t.equal(res.text, ''); 355 | t.end(); 356 | server.close(); 357 | }); 358 | }, errHandler(t)); 359 | }); 360 | 361 | test('Sends CORS headers in HTTP response', t => { 362 | start(_ => '') 363 | .then(server => { 364 | t.plan(2); 365 | request(server) 366 | .get('/') 367 | .expect(204) 368 | .expect('Content-Type', /plain/) 369 | .expect('Access-Control-Allow-Origin', '*') 370 | .expect('Access-Control-Allow-Methods', 371 | 'OPTIONS, GET, DELETE, POST, PUT, HEAD, PATCH') 372 | .end((err, res) => { 373 | t.error(err, 'No error'); 374 | t.equal(res.text, ''); 375 | t.end(); 376 | server.close(); 377 | }); 378 | }, errHandler(t)); 379 | }); 380 | 381 | test('Responds to OPTIONS with CORS headers in HTTP response', t => { 382 | start(_ => '') 383 | .then(server => { 384 | t.plan(2); 385 | request(server) 386 | .options('/') 387 | .expect(204) 388 | .expect('Access-Control-Allow-Origin', '*') 389 | .expect('Access-Control-Allow-Methods', 390 | 'OPTIONS, GET, DELETE, POST, PUT, HEAD, PATCH') 391 | .end((err, res) => { 392 | t.error(err, 'No error'); 393 | t.equal(res.text, ''); 394 | t.end(); 395 | server.close(); 396 | }); 397 | }, errHandler(t)); 398 | }); 399 | 400 | test('Responds to GET with CORS headers in HTTP response', t => { 401 | start(_ => '', { cors: () => ['http://example.com', 'http://example2.com'] } ) 402 | .then(server => { 403 | t.plan(2); 404 | request(server) 405 | .get('/') 406 | .expect(204) 407 | .set('Origin', 'http://example.com') 408 | .expect('Access-Control-Allow-Origin', 'http://example.com') 409 | .expect('Access-Control-Allow-Methods', 410 | 'OPTIONS, GET, DELETE, POST, PUT, HEAD, PATCH') 411 | .end((err, res) => { 412 | t.error(err, 'No error'); 413 | t.equal(res.text, ''); 414 | t.end(); 415 | server.close(); 416 | }); 417 | }, errHandler(t)); 418 | }); 419 | 420 | test('Respects headers set by the function', t => { 421 | const func = require(`${__dirname}/fixtures/response-header/`); 422 | start(func) 423 | .then(server => { 424 | t.plan(2); 425 | request(server) 426 | .get('/') 427 | .expect(200) 428 | .expect('X-announce-action', 'Saying hello') 429 | .end((err, res) => { 430 | t.error(err, 'No error'); 431 | t.equal(res.body.message, 'Well hello there'); 432 | t.end(); 433 | server.close(); 434 | }); 435 | }, errHandler(t)); 436 | }); 437 | 438 | test('Respects content type set by the function', t => { 439 | const func = require(`${__dirname}/fixtures/content-type/`); 440 | start(func) 441 | .then(server => { 442 | t.plan(2); 443 | request(server) 444 | .get('/') 445 | .expect(200) 446 | .expect('Content-Type', /text/) 447 | .end((err, res) => { 448 | t.error(err, 'No error'); 449 | t.equal(res.text, 'Well hello there'); 450 | t.end(); 451 | server.close(); 452 | }); 453 | }, errHandler(t)); 454 | }); 455 | 456 | test('Accepts application/json content via HTTP post', t => { 457 | const func = require(`${__dirname}/fixtures/json-input/`); 458 | start(func) 459 | .then(server => { 460 | t.plan(2); 461 | request(server) 462 | .post('/') 463 | .send({ lunch: 'tacos' }) 464 | .set('Accept', 'application/json') 465 | .expect('Content-Type', /json/) 466 | .expect(200) 467 | .end((err, res) => { 468 | t.error(err, 'No error'); 469 | t.deepEqual(res.body, { lunch: 'tacos' }); 470 | t.end(); 471 | server.close(); 472 | }); 473 | }, errHandler(t)); 474 | }); 475 | 476 | test('Accepts x-www-form-urlencoded content via HTTP post', t => { 477 | const func = require(`${__dirname}/fixtures/json-input/`); 478 | start(func) 479 | .then(server => { 480 | t.plan(2); 481 | request(server) 482 | .post('/') 483 | .send('lunch=tacos') 484 | .set('Accept', 'application/json') 485 | .expect('Content-Type', /json/) 486 | .expect(200) 487 | .end((err, res) => { 488 | t.error(err, 'No error'); 489 | t.deepEqual(res.body, { lunch: 'tacos' }); 490 | t.end(); 491 | server.close(); 492 | }); 493 | }, errHandler(t)); 494 | }); 495 | 496 | test('Exposes readiness URL', t => { 497 | start(_ => '') 498 | .then(server => { 499 | t.plan(2); 500 | request(server) 501 | .get('/health/readiness') 502 | .expect(200) 503 | .expect('Content-type', /text/) 504 | .end((err, res) => { 505 | t.error(err, 'No error'); 506 | t.equal(res.text, 'OK'); 507 | t.end(); 508 | server.close(); 509 | }); 510 | }, errHandler(t)); 511 | }); 512 | 513 | test('Exposes liveness URL', t => { 514 | start(_ => '') 515 | .then(server => { 516 | t.plan(2); 517 | request(server) 518 | .get('/health/liveness') 519 | .expect(200) 520 | .expect('Content-type', /text/) 521 | .end((err, res) => { 522 | t.error(err, 'No error'); 523 | t.equal(res.text, 'OK'); 524 | t.end(); 525 | server.close(); 526 | }); 527 | }, errHandler(t)); 528 | }); 529 | 530 | test('Function accepts destructured parameters', t => { 531 | start(function({ lunch }) { return { message: `Yay ${lunch}` }; }) 532 | .then(server => { 533 | t.plan(2); 534 | request(server) 535 | .get('/?lunch=tacos') 536 | .expect(200) 537 | .expect('Content-type', /json/) 538 | .end((err, res) => { 539 | t.error(err, 'No error'); 540 | t.equals(res.body.message, 'Yay tacos'); 541 | t.end(); 542 | server.close(); 543 | }); 544 | }, errHandler(t)); 545 | }); 546 | 547 | test('Provides logger with appropriate log level configured', t => { 548 | var loggerProvided = false; 549 | const logLevel = 'error'; 550 | start(context => { 551 | loggerProvided = (context.log && 552 | typeof context.log.info === 'function' && 553 | typeof context.log.warn === 'function' && 554 | typeof context.log.debug === 'function' && 555 | typeof context.log.trace === 'function' && 556 | typeof context.log.fatal === 'function' && 557 | context.log.level === logLevel); 558 | }, { logLevel }) 559 | .then(server => { 560 | request(server) 561 | .get('/') 562 | .end((err, _) => { 563 | t.error(err, 'No error'); 564 | t.assert(loggerProvided, 'Logger provided'); 565 | t.end(); 566 | server.close(); 567 | }); 568 | }, errHandler(t)); // enable but squelch 569 | }); 570 | 571 | test('Provides logger in context when logging is disabled', t => { 572 | var loggerProvided = false; 573 | start(context => { 574 | loggerProvided = (context.log && typeof context.log.info === 'function'); 575 | }) 576 | .then(server => { 577 | request(server) 578 | .get('/') 579 | .end((err, _) => { 580 | t.error(err, 'No error'); 581 | t.assert(loggerProvided, 'Logger provided'); 582 | t.end(); 583 | server.close(); 584 | }); 585 | }, errHandler(t)); 586 | }); 587 | 588 | test('Accepts CloudEvents with content type of text/plain', t => { 589 | start(_ => new CloudEvent({ 590 | source: 'test', 591 | type: 'test-type', 592 | data: 'some data', 593 | datacontenttype: 'text/plain' 594 | })) 595 | .then(server => { 596 | request(server) 597 | .post('/') 598 | .send('hello') 599 | .set(CONSTANTS.CE_HEADERS.ID, '1') 600 | .set(CONSTANTS.CE_HEADERS.SOURCE, 'integration-test') 601 | .set(CONSTANTS.CE_HEADERS.TYPE, 'dev.knative.example') 602 | .set(CONSTANTS.CE_HEADERS.SPEC_VERSION, '1.0') 603 | .set('ce-datacontenttype', 'text/plain') 604 | .set('content-type', 'text/plain') 605 | .expect(200) 606 | .expect('Content-Type', /text/) 607 | .end((err, res) => { 608 | t.error(err, 'No error'); 609 | t.equal(res.text, 'some data'); 610 | t.end(); 611 | server.close(); 612 | }); 613 | }, errHandler(t)); 614 | }); 615 | 616 | test('Reads a func.yaml file for logLevel', t => { 617 | const funcPath = `${__dirname}/fixtures/funcyaml/`; 618 | const func = require(funcPath); 619 | start(func, { config: funcPath }) 620 | .then(server => { 621 | request(server) 622 | .get('/') 623 | .expect(204) 624 | .end((err, _) => { 625 | t.error(err, 'No error'); 626 | t.end(); 627 | server.close(); 628 | }); 629 | }, errHandler(t)); 630 | }); 631 | 632 | test('Handles HTTP response provided as StructuredReturn', t => { 633 | const func = require(`${__dirname}/fixtures/response-structured/`); 634 | start(func) 635 | .then(server => { 636 | t.plan(3); 637 | request(server) 638 | .get('/') 639 | .expect(201) 640 | .expect('Content-Type', /jpeg/) 641 | .end((err, res) => { 642 | t.error(err, 'No error'); 643 | t.equal(typeof res.body, 'object'); 644 | t.ok(Buffer.isBuffer(res.body), 'body is a buffer'); 645 | t.end(); 646 | server.close(); 647 | }); 648 | }, errHandler(t)); 649 | }); 650 | -------------------------------------------------------------------------------- /test/types/context.test-d.ts: -------------------------------------------------------------------------------- 1 | import { IncomingHttpHeaders, IncomingMessage } from 'http'; 2 | 3 | import { expectAssignable, expectType } from 'tsd'; 4 | import { CloudEvent } from 'cloudevents'; 5 | 6 | import { Context, CloudEventResponse, Logger } from '../../index'; 7 | 8 | const context = {} as Context; 9 | 10 | // Context 11 | expectType(context); 12 | expectType(context.cloudEventResponse('test-data')); 13 | expectType(context.log); 14 | expectType(context.req); 15 | expectType(context.headers); 16 | expectType|undefined>(context.query); 17 | expectType(context.method); 18 | expectType(context.httpVersion); 19 | expectType(context.httpVersionMajor); 20 | expectType(context.httpVersionMinor); 21 | expectType>(context.cloudevent); 22 | expectAssignable|string|undefined>(context.body); 23 | expectAssignable(context.rawBody); 24 | 25 | // CloudEventResponse 26 | expectType(context.cloudEventResponse('').response()); 27 | expectType(context.cloudEventResponse('').id('')); 28 | expectType(context.cloudEventResponse('').source('')); 29 | expectType(context.cloudEventResponse('').type('')); 30 | expectType(context.cloudEventResponse('').version('')); 31 | -------------------------------------------------------------------------------- /test/types/index.test-d.ts: -------------------------------------------------------------------------------- 1 | import { expectType } from 'tsd'; 2 | 3 | import { Server } from 'http'; 4 | import { start, Invokable, Context, LogLevel, InvokerOptions } from '../../index'; 5 | import { CloudEvent } from 'cloudevents'; 6 | 7 | const fn: Invokable = function( 8 | context: Context, 9 | cloudevent?: CloudEvent 10 | ) { 11 | expectType(context); 12 | expectType|undefined>(cloudevent); 13 | return undefined; 14 | }; 15 | 16 | const options: InvokerOptions = { 17 | logLevel: LogLevel.info, 18 | port: 8080, 19 | includeRaw: true, 20 | path: './' 21 | }; 22 | 23 | expectType>(start(fn, options)); 24 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": ["./index.d.ts", "./lib/**/*.ts"], 3 | "exclude": ["node_modules"], 4 | } 5 | --------------------------------------------------------------------------------