diff --git a/README.md b/README.md
index 1066f1c1..360649ae 100644
--- a/README.md
+++ b/README.md
@@ -8,7 +8,7 @@
-Run REST APIs and other web applications using your existing [Node.js](https://nodejs.org/) application framework (Express, Koa, Hapi, Sails, etc.), on top of [AWS Lambda](https://aws.amazon.com/lambda/) and [Amazon API Gateway](https://aws.amazon.com/api-gateway/).
+Run REST APIs and other web applications using your existing [Node.js](https://nodejs.org/) application framework (Express, Koa, Hapi, Sails, etc.), on top of [AWS Lambda](https://aws.amazon.com/lambda/) and [Amazon API Gateway](https://aws.amazon.com/api-gateway/) or [Azure Function](https://docs.microsoft.com/en-us/azure/azure-functions/).
```bash
npm install @vendia/serverless-express
@@ -25,7 +25,9 @@ Want to get up and running quickly? [Check out our basic starter example](exampl
If you want to migrate an existing application to AWS Lambda, it's advised to get the minimal example up and running first, and then copy your application source in.
-## Minimal Lambda handler wrapper
+## AWS
+
+### Minimal Lambda handler wrapper
The only AWS Lambda specific code you need to write is a simple handler like below. All other code you can write as you normally do.
@@ -36,7 +38,7 @@ const app = require('./app')
exports.handler = serverlessExpress({ app })
```
-## Async setup Lambda handler
+### Async setup Lambda handler
If your application needs to perform some common bootstrap tasks such as connecting to a database before the request is forward to the API, you can use the following pattern (also available in [this example](https://github.com/vendia/serverless-express/blob/mainline/examples/basic-starter-api-gateway-v2/src/lambda-async-setup.js)):
@@ -70,6 +72,45 @@ function handler (event, context) {
exports.handler = handler
```
+## Azure
+
+### Async Azure Function (v3) handler wrapper
+
+The only Azure Function specific code you need to write is a simple `index.js` and a `function.json` like below.
+
+```js
+// index.js
+const serverlessExpress = require('@vendia/serverless-express')
+const app = require('./app')
+const cachedServerlessExpress = serverlessExpress({ app })
+
+module.exports = async function (context, req) {
+ return cachedServerlessExpress(context, req)
+}
+```
+
+The _out-binding_ parameter `"name": "$return"` is important for Serverless Express to work.
+
+```json
+// function.json
+{
+ "bindings": [
+ {
+ "authLevel": "anonymous",
+ "type": "httpTrigger",
+ "direction": "in",
+ "name": "req",
+ "route": "{*segments}"
+ },
+ {
+ "type": "http",
+ "direction": "out",
+ "name": "$return"
+ }
+ ]
+}
+```
+
## 4.x
1. Improved API - Simpler for end-user to use and configure.
diff --git a/__tests__/integration.js b/__tests__/integration.js
index f4eef0ec..580921cf 100644
--- a/__tests__/integration.js
+++ b/__tests__/integration.js
@@ -240,6 +240,14 @@ describe.each(EACH_MATRIX)('%s:%s: integration tests', (eventSourceName, framewo
delete response.multiValueHeaders.etag
delete response.multiValueHeaders['last-modified']
break
+ case 'azureHttpFunctionV3':
+ expectedResponse.body = Buffer.from(samLogoBase64, 'base64')
+ expectedResponse.isBase64Encoded = false
+ expect(response.headers.etag).toMatch(etagRegex)
+ expect(response.headers['last-modified']).toMatch(lastModifiedRegex)
+ delete response.headers.etag
+ delete response.headers['last-modified']
+ break
case 'apiGatewayV2':
expect(response.headers.etag).toMatch(etagRegex)
expect(response.headers['last-modified']).toMatch(lastModifiedRegex)
@@ -388,7 +396,7 @@ describe.each(EACH_MATRIX)('%s:%s: integration tests', (eventSourceName, framewo
test('set-cookie', async () => {
router.get('/cookie', (req, res) => {
- res.cookie('Foo', 'bar')
+ res.cookie('Foo', 'bar', { domain: 'example.com', secure: true, httpOnly: true, sameSite: 'Strict' })
res.cookie('Fizz', 'buzz')
res.json({})
})
@@ -400,7 +408,7 @@ describe.each(EACH_MATRIX)('%s:%s: integration tests', (eventSourceName, framewo
const response = await serverlessExpressInstance(event)
const expectedSetCookieHeaders = [
- 'Foo=bar; Path=/',
+ 'Foo=bar; Domain=example.com; Path=/; HttpOnly; Secure; SameSite=Strict',
'Fizz=buzz; Path=/'
]
const expectedResponse = makeResponse({
@@ -414,6 +422,24 @@ describe.each(EACH_MATRIX)('%s:%s: integration tests', (eventSourceName, framewo
},
statusCode: 200
})
+
+ switch (eventSourceName) {
+ case 'azureHttpFunctionV3':
+ expectedResponse.cookies = [
+ {
+ domain: 'example.com',
+ httpOnly: true,
+ name: 'Foo',
+ path: '/',
+ sameSite: 'Strict',
+ secure: true,
+ value: 'bar'
+ },
+ { name: 'Fizz', path: '/', value: 'buzz' }
+ ]
+ break
+ }
+
expect(response).toEqual(expectedResponse)
})
diff --git a/examples/azure-http-function-v3/.gitignore b/examples/azure-http-function-v3/.gitignore
new file mode 100644
index 00000000..740b89ad
--- /dev/null
+++ b/examples/azure-http-function-v3/.gitignore
@@ -0,0 +1,42 @@
+bin
+obj
+csx
+.vs
+edge
+Publish
+
+*.user
+*.suo
+*.cscfg
+*.Cache
+project.lock.json
+
+/packages
+/TestResults
+
+/tools/NuGet.exe
+/App_Data
+/secrets
+/data
+.secrets
+appsettings.json
+
+node_modules
+dist
+
+# Local python packages
+.python_packages/
+
+# Python Environments
+.env
+.venv
+env/
+venv/
+ENV/
+env.bak/
+venv.bak/
+
+# Byte-compiled / optimized / DLL files
+__pycache__/
+*.py[cod]
+*$py.class
\ No newline at end of file
diff --git a/examples/azure-http-function-v3/.vscode/extensions.json b/examples/azure-http-function-v3/.vscode/extensions.json
new file mode 100644
index 00000000..dde673dc
--- /dev/null
+++ b/examples/azure-http-function-v3/.vscode/extensions.json
@@ -0,0 +1,5 @@
+{
+ "recommendations": [
+ "ms-azuretools.vscode-azurefunctions"
+ ]
+}
\ No newline at end of file
diff --git a/examples/azure-http-function-v3/HttpExample/app.js b/examples/azure-http-function-v3/HttpExample/app.js
new file mode 100644
index 00000000..2e91931d
--- /dev/null
+++ b/examples/azure-http-function-v3/HttpExample/app.js
@@ -0,0 +1,99 @@
+const path = require('path')
+const express = require('express')
+const bodyParser = require('body-parser')
+const cors = require('cors')
+const compression = require('compression')
+const { getCurrentInvoke } = require('../../../src/index') // require('@vendia/serverless-express')
+const ejs = require('ejs').__express
+const app = express()
+const router = express.Router()
+
+app.set('view engine', 'ejs')
+app.engine('.ejs', ejs)
+
+router.use(compression())
+router.use(cors())
+router.use(bodyParser.json())
+router.use(bodyParser.urlencoded({ extended: true }))
+
+// NOTE: tests can't find the views directory without this
+app.set('views', path.join(__dirname, 'views'))
+
+router.get('/api', (req, res) => {
+ const currentInvoke = getCurrentInvoke()
+ const { event = {} } = currentInvoke
+ const { requestContext = {} } = event
+ const { domainName = 'localhost:7071' } = requestContext
+ const apiUrl = `https://${domainName}`
+ res.render('index', { apiUrl })
+})
+
+router.get('/api/vendia', (req, res) => {
+ res.sendFile(path.join(__dirname, 'vendia-logo.png'))
+})
+
+router.get('/api/users', (req, res) => {
+ res.json(users)
+})
+
+router.get('/api/users/:userId', (req, res) => {
+ const user = getUser(req.params.userId)
+
+ if (!user) return res.status(404).json({})
+
+ return res.json(user)
+})
+
+router.post('/api/users', (req, res) => {
+ const user = {
+ id: ++userIdCounter,
+ name: req.body.name
+ }
+ users.push(user)
+ res.status(201).json(user)
+})
+
+router.put('/api/users/:userId', (req, res) => {
+ const user = getUser(req.params.userId)
+
+ if (!user) return res.status(404).json({})
+
+ user.name = req.body.name
+ res.json(user)
+})
+
+router.delete('/api/users/:userId', (req, res) => {
+ const userIndex = getUserIndex(req.params.userId)
+
+ if (userIndex === -1) return res.status(404).json({})
+
+ users.splice(userIndex, 1)
+ res.json(users)
+})
+
+router.get('/api/cookie', (req, res) => {
+ res.cookie('Foo', 'bar')
+ res.cookie('Fizz', 'buzz')
+ res.json({})
+})
+
+const getUser = (userId) => users.find(u => u.id === parseInt(userId))
+const getUserIndex = (userId) => users.findIndex(u => u.id === parseInt(userId))
+
+// Ephemeral in-memory data store
+const users = [{
+ id: 1,
+ name: 'Joe'
+}, {
+ id: 2,
+ name: 'Jane'
+}]
+let userIdCounter = users.length
+
+// The serverless-express library creates a server and listens on a Unix
+// Domain Socket for you, so you can remove the usual call to app.listen.
+// app.listen(3000)
+app.use('/', router)
+
+// Export your express server so you can import it in the lambda function.
+module.exports = app
diff --git a/examples/azure-http-function-v3/HttpExample/function.json b/examples/azure-http-function-v3/HttpExample/function.json
new file mode 100644
index 00000000..13f31dee
--- /dev/null
+++ b/examples/azure-http-function-v3/HttpExample/function.json
@@ -0,0 +1,16 @@
+{
+ "bindings": [
+ {
+ "authLevel": "anonymous",
+ "type": "httpTrigger",
+ "direction": "in",
+ "name": "req",
+ "route": "{*segments}"
+ },
+ {
+ "type": "http",
+ "direction": "out",
+ "name": "$return"
+ }
+ ]
+}
diff --git a/examples/azure-http-function-v3/HttpExample/index.js b/examples/azure-http-function-v3/HttpExample/index.js
new file mode 100644
index 00000000..cd74a8d2
--- /dev/null
+++ b/examples/azure-http-function-v3/HttpExample/index.js
@@ -0,0 +1,7 @@
+const serverlessExpress = require('../../../src/index') // require('@vendia/serverless-express')
+const app = require('./app')
+const cachedServerlessExpress = serverlessExpress({ app })
+
+module.exports = async function (context, req) {
+ return cachedServerlessExpress(context, req)
+}
diff --git a/examples/azure-http-function-v3/HttpExample/vendia-logo.png b/examples/azure-http-function-v3/HttpExample/vendia-logo.png
new file mode 100644
index 00000000..9257542f
Binary files /dev/null and b/examples/azure-http-function-v3/HttpExample/vendia-logo.png differ
diff --git a/examples/azure-http-function-v3/HttpExample/views/index.ejs b/examples/azure-http-function-v3/HttpExample/views/index.ejs
new file mode 100644
index 00000000..cf19f97c
--- /dev/null
+++ b/examples/azure-http-function-v3/HttpExample/views/index.ejs
@@ -0,0 +1,115 @@
+
+
+
+
+ My Serverless Application
+
+
+
+
+ My Serverless Application
+ Welcome to your Azure Function serverless application. This example application has several resources configured for you to explore. State is stored in memory in a given container, and is therefore ephemeral.
+
+ Resources
+
+
+ Returns a list of all users.
curl <%= apiUrl %>/users -H 'accept: application/json'
+
+
+ POST /users
+ Creates a new user.
curl <%= apiUrl %>/api/users -X POST -d '{"name":"Sam"}' -H 'accept: application/json'
+
+
+
+ Returns a single user.
curl <%= apiUrl %>/api/users/1 -H 'accept: application/json'
+
+
+ PUT /users/:userId
+ Updates an existing user.
curl <%= apiUrl %>/api/users/1 -X PUT -d '{"name":"Samantha"}' -H 'accept: application/json'
+
+
+ DELETE /users/:userId
+ Deletes an existing user.
curl <%= apiUrl %>/api/users/1 -X DELETE -H 'accept: application/json'
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/examples/azure-http-function-v3/README.md b/examples/azure-http-function-v3/README.md
new file mode 100644
index 00000000..a91fc485
--- /dev/null
+++ b/examples/azure-http-function-v3/README.md
@@ -0,0 +1,42 @@
+# Example - Azure Function HTTP v3
+
+This example uses the local tools provided by Microsoft to simulate an Azure Function environment. This means there is no Azure Account required to run this example.
+
+## Run local requirements
+
+These prerequisites are required to run and debug your functions locally.
+
+- Visual Studio Code
+- Visual Studio Code - Azure Functions extension
+- Azure Functions Core Tools
+- Node.js
+
+_For further information on how to run an Azure Function locally please check [Develop Azure Functions by using Visual Studio Code](https://docs.microsoft.com/en-us/azure/azure-functions/functions-develop-vs-code?tabs=nodejs)._
+
+
+## Installation & Usage
+
+After all requirements are fulfilled you can install all dependencies for the `azure-http-function-v3` with npm.
+
+```bash
+ cd examples/azure-http-function-v3
+ npm install
+ npm run start
+```
+
+
+
+After you run the last command you should see an output like this:
+
+```bash
+Azure Functions Core Tools
+Core Tools Version: 3.0.3904
+Function Runtime Version: 3.3.1.0
+
+
+Functions:
+
+ HttpExample: http://localhost:7071/api/{*segments}
+```
+
+Now you can navigate to `http://localhost:7071/api/` to display the `My Serverless Application` page.
\ No newline at end of file
diff --git a/examples/azure-http-function-v3/host.json b/examples/azure-http-function-v3/host.json
new file mode 100644
index 00000000..291065f8
--- /dev/null
+++ b/examples/azure-http-function-v3/host.json
@@ -0,0 +1,15 @@
+{
+ "version": "2.0",
+ "logging": {
+ "applicationInsights": {
+ "samplingSettings": {
+ "isEnabled": true,
+ "excludedTypes": "Request"
+ }
+ }
+ },
+ "extensionBundle": {
+ "id": "Microsoft.Azure.Functions.ExtensionBundle",
+ "version": "[2.*, 3.0.0)"
+ }
+}
\ No newline at end of file
diff --git a/examples/azure-http-function-v3/local.settings.json b/examples/azure-http-function-v3/local.settings.json
new file mode 100644
index 00000000..688b46a8
--- /dev/null
+++ b/examples/azure-http-function-v3/local.settings.json
@@ -0,0 +1,8 @@
+{
+ "IsEncrypted": false,
+ "Values": {
+ "FUNCTIONS_WORKER_RUNTIME": "node",
+ "AzureWebJobsStorage": "",
+ "FUNCTIONS_EXTENSION_VERSION": "~3"
+ }
+}
\ No newline at end of file
diff --git a/examples/azure-http-function-v3/package-lock.json b/examples/azure-http-function-v3/package-lock.json
new file mode 100644
index 00000000..aa9d07ee
--- /dev/null
+++ b/examples/azure-http-function-v3/package-lock.json
@@ -0,0 +1,547 @@
+{
+ "requires": true,
+ "lockfileVersion": 1,
+ "dependencies": {
+ "@vendia/serverless-express": {
+ "version": "4.5.2",
+ "resolved": "https://registry.npmjs.org/@vendia/serverless-express/-/serverless-express-4.5.2.tgz",
+ "integrity": "sha512-mekBOPnBxfhIvBYKVwfvjp9NtS+bOs3F08Vudxa3Fb7zkxtdjRn0UMLRT6zwWf6i4V5rjk2aEhzbIDSCV1GQ0w=="
+ },
+ "accepts": {
+ "version": "1.3.7",
+ "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.7.tgz",
+ "integrity": "sha512-Il80Qs2WjYlJIBNzNkK6KYqlVMTbZLXgHx2oT0pU/fjRHyEp+PEfEPY0R3WCwAGVOtauxh1hOxNgIf5bv7dQpA==",
+ "requires": {
+ "mime-types": "~2.1.24",
+ "negotiator": "0.6.2"
+ }
+ },
+ "ansi-styles": {
+ "version": "3.2.1",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
+ "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==",
+ "requires": {
+ "color-convert": "^1.9.0"
+ }
+ },
+ "array-flatten": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
+ "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI="
+ },
+ "async": {
+ "version": "0.9.2",
+ "resolved": "https://registry.npmjs.org/async/-/async-0.9.2.tgz",
+ "integrity": "sha1-rqdNXmHB+JlhO/ZL2mbUx48v0X0="
+ },
+ "balanced-match": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
+ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="
+ },
+ "body-parser": {
+ "version": "1.19.1",
+ "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.19.1.tgz",
+ "integrity": "sha512-8ljfQi5eBk8EJfECMrgqNGWPEY5jWP+1IzkzkGdFFEwFQZZyaZ21UqdaHktgiMlH0xLHqIFtE/u2OYE5dOtViA==",
+ "requires": {
+ "bytes": "3.1.1",
+ "content-type": "~1.0.4",
+ "debug": "2.6.9",
+ "depd": "~1.1.2",
+ "http-errors": "1.8.1",
+ "iconv-lite": "0.4.24",
+ "on-finished": "~2.3.0",
+ "qs": "6.9.6",
+ "raw-body": "2.4.2",
+ "type-is": "~1.6.18"
+ }
+ },
+ "brace-expansion": {
+ "version": "1.1.11",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
+ "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
+ "requires": {
+ "balanced-match": "^1.0.0",
+ "concat-map": "0.0.1"
+ }
+ },
+ "bytes": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.1.tgz",
+ "integrity": "sha512-dWe4nWO/ruEOY7HkUJ5gFt1DCFV9zPRoJr8pV0/ASQermOZjtq8jMjOprC0Kd10GLN+l7xaUPvxzJFWtxGu8Fg=="
+ },
+ "chalk": {
+ "version": "2.4.2",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz",
+ "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==",
+ "requires": {
+ "ansi-styles": "^3.2.1",
+ "escape-string-regexp": "^1.0.5",
+ "supports-color": "^5.3.0"
+ }
+ },
+ "color-convert": {
+ "version": "1.9.3",
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
+ "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==",
+ "requires": {
+ "color-name": "1.1.3"
+ }
+ },
+ "color-name": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
+ "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU="
+ },
+ "compressible": {
+ "version": "2.0.18",
+ "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz",
+ "integrity": "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==",
+ "requires": {
+ "mime-db": ">= 1.43.0 < 2"
+ }
+ },
+ "compression": {
+ "version": "1.7.4",
+ "resolved": "https://registry.npmjs.org/compression/-/compression-1.7.4.tgz",
+ "integrity": "sha512-jaSIDzP9pZVS4ZfQ+TzvtiWhdpFhE2RDHz8QJkpX9SIpLq88VueF5jJw6t+6CUQcAoA6t+x89MLrWAqpfDE8iQ==",
+ "requires": {
+ "accepts": "~1.3.5",
+ "bytes": "3.0.0",
+ "compressible": "~2.0.16",
+ "debug": "2.6.9",
+ "on-headers": "~1.0.2",
+ "safe-buffer": "5.1.2",
+ "vary": "~1.1.2"
+ },
+ "dependencies": {
+ "bytes": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz",
+ "integrity": "sha1-0ygVQE1olpn4Wk6k+odV3ROpYEg="
+ }
+ }
+ },
+ "concat-map": {
+ "version": "0.0.1",
+ "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
+ "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s="
+ },
+ "content-disposition": {
+ "version": "0.5.4",
+ "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz",
+ "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==",
+ "requires": {
+ "safe-buffer": "5.2.1"
+ },
+ "dependencies": {
+ "safe-buffer": {
+ "version": "5.2.1",
+ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
+ "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="
+ }
+ }
+ },
+ "content-type": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz",
+ "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA=="
+ },
+ "cookie": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.1.tgz",
+ "integrity": "sha512-ZwrFkGJxUR3EIoXtO+yVE69Eb7KlixbaeAWfBQB9vVsNn/o+Yw69gBWSSDK825hQNdN+wF8zELf3dFNl/kxkUA=="
+ },
+ "cookie-signature": {
+ "version": "1.0.6",
+ "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz",
+ "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw="
+ },
+ "cors": {
+ "version": "2.8.5",
+ "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz",
+ "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==",
+ "requires": {
+ "object-assign": "^4",
+ "vary": "^1"
+ }
+ },
+ "debug": {
+ "version": "2.6.9",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
+ "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
+ "requires": {
+ "ms": "2.0.0"
+ }
+ },
+ "depd": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz",
+ "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak="
+ },
+ "destroy": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz",
+ "integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA="
+ },
+ "ee-first": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
+ "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0="
+ },
+ "ejs": {
+ "version": "3.1.6",
+ "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.6.tgz",
+ "integrity": "sha512-9lt9Zse4hPucPkoP7FHDF0LQAlGyF9JVpnClFLFH3aSSbxmyoqINRpp/9wePWJTUl4KOQwRL72Iw3InHPDkoGw==",
+ "requires": {
+ "jake": "^10.6.1"
+ }
+ },
+ "encodeurl": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz",
+ "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k="
+ },
+ "escape-html": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
+ "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg="
+ },
+ "escape-string-regexp": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
+ "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ="
+ },
+ "etag": {
+ "version": "1.8.1",
+ "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
+ "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc="
+ },
+ "express": {
+ "version": "4.17.2",
+ "resolved": "https://registry.npmjs.org/express/-/express-4.17.2.tgz",
+ "integrity": "sha512-oxlxJxcQlYwqPWKVJJtvQiwHgosH/LrLSPA+H4UxpyvSS6jC5aH+5MoHFM+KABgTOt0APue4w66Ha8jCUo9QGg==",
+ "requires": {
+ "accepts": "~1.3.7",
+ "array-flatten": "1.1.1",
+ "body-parser": "1.19.1",
+ "content-disposition": "0.5.4",
+ "content-type": "~1.0.4",
+ "cookie": "0.4.1",
+ "cookie-signature": "1.0.6",
+ "debug": "2.6.9",
+ "depd": "~1.1.2",
+ "encodeurl": "~1.0.2",
+ "escape-html": "~1.0.3",
+ "etag": "~1.8.1",
+ "finalhandler": "~1.1.2",
+ "fresh": "0.5.2",
+ "merge-descriptors": "1.0.1",
+ "methods": "~1.1.2",
+ "on-finished": "~2.3.0",
+ "parseurl": "~1.3.3",
+ "path-to-regexp": "0.1.7",
+ "proxy-addr": "~2.0.7",
+ "qs": "6.9.6",
+ "range-parser": "~1.2.1",
+ "safe-buffer": "5.2.1",
+ "send": "0.17.2",
+ "serve-static": "1.14.2",
+ "setprototypeof": "1.2.0",
+ "statuses": "~1.5.0",
+ "type-is": "~1.6.18",
+ "utils-merge": "1.0.1",
+ "vary": "~1.1.2"
+ },
+ "dependencies": {
+ "safe-buffer": {
+ "version": "5.2.1",
+ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
+ "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="
+ }
+ }
+ },
+ "filelist": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.2.tgz",
+ "integrity": "sha512-z7O0IS8Plc39rTCq6i6iHxk43duYOn8uFJiWSewIq0Bww1RNybVHSCjahmcC87ZqAm4OTvFzlzeGu3XAzG1ctQ==",
+ "requires": {
+ "minimatch": "^3.0.4"
+ }
+ },
+ "finalhandler": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz",
+ "integrity": "sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==",
+ "requires": {
+ "debug": "2.6.9",
+ "encodeurl": "~1.0.2",
+ "escape-html": "~1.0.3",
+ "on-finished": "~2.3.0",
+ "parseurl": "~1.3.3",
+ "statuses": "~1.5.0",
+ "unpipe": "~1.0.0"
+ }
+ },
+ "forwarded": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
+ "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow=="
+ },
+ "fresh": {
+ "version": "0.5.2",
+ "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
+ "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac="
+ },
+ "has-flag": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
+ "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0="
+ },
+ "http-errors": {
+ "version": "1.8.1",
+ "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.8.1.tgz",
+ "integrity": "sha512-Kpk9Sm7NmI+RHhnj6OIWDI1d6fIoFAtFt9RLaTMRlg/8w49juAStsrBgp0Dp4OdxdVbRIeKhtCUvoi/RuAhO4g==",
+ "requires": {
+ "depd": "~1.1.2",
+ "inherits": "2.0.4",
+ "setprototypeof": "1.2.0",
+ "statuses": ">= 1.5.0 < 2",
+ "toidentifier": "1.0.1"
+ }
+ },
+ "iconv-lite": {
+ "version": "0.4.24",
+ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
+ "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
+ "requires": {
+ "safer-buffer": ">= 2.1.2 < 3"
+ }
+ },
+ "inherits": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
+ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
+ },
+ "ipaddr.js": {
+ "version": "1.9.1",
+ "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
+ "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g=="
+ },
+ "jake": {
+ "version": "10.8.2",
+ "resolved": "https://registry.npmjs.org/jake/-/jake-10.8.2.tgz",
+ "integrity": "sha512-eLpKyrfG3mzvGE2Du8VoPbeSkRry093+tyNjdYaBbJS9v17knImYGNXQCUV0gLxQtF82m3E8iRb/wdSQZLoq7A==",
+ "requires": {
+ "async": "0.9.x",
+ "chalk": "^2.4.2",
+ "filelist": "^1.0.1",
+ "minimatch": "^3.0.4"
+ }
+ },
+ "media-typer": {
+ "version": "0.3.0",
+ "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
+ "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g="
+ },
+ "merge-descriptors": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz",
+ "integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E="
+ },
+ "methods": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz",
+ "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4="
+ },
+ "mime": {
+ "version": "1.6.0",
+ "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
+ "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg=="
+ },
+ "mime-db": {
+ "version": "1.51.0",
+ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.51.0.tgz",
+ "integrity": "sha512-5y8A56jg7XVQx2mbv1lu49NR4dokRnhZYTtL+KGfaa27uq4pSTXkwQkFJl4pkRMyNFz/EtYDSkiiEHx3F7UN6g=="
+ },
+ "mime-types": {
+ "version": "2.1.34",
+ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.34.tgz",
+ "integrity": "sha512-6cP692WwGIs9XXdOO4++N+7qjqv0rqxxVvJ3VHPh/Sc9mVZcQP+ZGhkKiTvWMQRr2tbHkJP/Yn7Y0npb3ZBs4A==",
+ "requires": {
+ "mime-db": "1.51.0"
+ }
+ },
+ "minimatch": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz",
+ "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==",
+ "requires": {
+ "brace-expansion": "^1.1.7"
+ }
+ },
+ "ms": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+ "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g="
+ },
+ "negotiator": {
+ "version": "0.6.2",
+ "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.2.tgz",
+ "integrity": "sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw=="
+ },
+ "object-assign": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
+ "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM="
+ },
+ "on-finished": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz",
+ "integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=",
+ "requires": {
+ "ee-first": "1.1.1"
+ }
+ },
+ "on-headers": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz",
+ "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA=="
+ },
+ "parseurl": {
+ "version": "1.3.3",
+ "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
+ "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="
+ },
+ "path-to-regexp": {
+ "version": "0.1.7",
+ "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz",
+ "integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w="
+ },
+ "proxy-addr": {
+ "version": "2.0.7",
+ "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
+ "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==",
+ "requires": {
+ "forwarded": "0.2.0",
+ "ipaddr.js": "1.9.1"
+ }
+ },
+ "qs": {
+ "version": "6.9.6",
+ "resolved": "https://registry.npmjs.org/qs/-/qs-6.9.6.tgz",
+ "integrity": "sha512-TIRk4aqYLNoJUbd+g2lEdz5kLWIuTMRagAXxl78Q0RiVjAOugHmeKNGdd3cwo/ktpf9aL9epCfFqWDEKysUlLQ=="
+ },
+ "range-parser": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
+ "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg=="
+ },
+ "raw-body": {
+ "version": "2.4.2",
+ "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.4.2.tgz",
+ "integrity": "sha512-RPMAFUJP19WIet/99ngh6Iv8fzAbqum4Li7AD6DtGaW2RpMB/11xDoalPiJMTbu6I3hkbMVkATvZrqb9EEqeeQ==",
+ "requires": {
+ "bytes": "3.1.1",
+ "http-errors": "1.8.1",
+ "iconv-lite": "0.4.24",
+ "unpipe": "1.0.0"
+ }
+ },
+ "safe-buffer": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
+ "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="
+ },
+ "safer-buffer": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
+ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
+ },
+ "send": {
+ "version": "0.17.2",
+ "resolved": "https://registry.npmjs.org/send/-/send-0.17.2.tgz",
+ "integrity": "sha512-UJYB6wFSJE3G00nEivR5rgWp8c2xXvJ3OPWPhmuteU0IKj8nKbG3DrjiOmLwpnHGYWAVwA69zmTm++YG0Hmwww==",
+ "requires": {
+ "debug": "2.6.9",
+ "depd": "~1.1.2",
+ "destroy": "~1.0.4",
+ "encodeurl": "~1.0.2",
+ "escape-html": "~1.0.3",
+ "etag": "~1.8.1",
+ "fresh": "0.5.2",
+ "http-errors": "1.8.1",
+ "mime": "1.6.0",
+ "ms": "2.1.3",
+ "on-finished": "~2.3.0",
+ "range-parser": "~1.2.1",
+ "statuses": "~1.5.0"
+ },
+ "dependencies": {
+ "ms": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
+ }
+ }
+ },
+ "serve-static": {
+ "version": "1.14.2",
+ "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.14.2.tgz",
+ "integrity": "sha512-+TMNA9AFxUEGuC0z2mevogSnn9MXKb4fa7ngeRMJaaGv8vTwnIEkKi+QGvPt33HSnf8pRS+WGM0EbMtCJLKMBQ==",
+ "requires": {
+ "encodeurl": "~1.0.2",
+ "escape-html": "~1.0.3",
+ "parseurl": "~1.3.3",
+ "send": "0.17.2"
+ }
+ },
+ "setprototypeof": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
+ "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="
+ },
+ "statuses": {
+ "version": "1.5.0",
+ "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz",
+ "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow="
+ },
+ "supports-color": {
+ "version": "5.5.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
+ "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
+ "requires": {
+ "has-flag": "^3.0.0"
+ }
+ },
+ "toidentifier": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
+ "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA=="
+ },
+ "type-is": {
+ "version": "1.6.18",
+ "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
+ "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==",
+ "requires": {
+ "media-typer": "0.3.0",
+ "mime-types": "~2.1.24"
+ }
+ },
+ "unpipe": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
+ "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw="
+ },
+ "utils-merge": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
+ "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM="
+ },
+ "vary": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
+ "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw="
+ }
+ }
+}
diff --git a/examples/azure-http-function-v3/package.json b/examples/azure-http-function-v3/package.json
new file mode 100644
index 00000000..e07a1d99
--- /dev/null
+++ b/examples/azure-http-function-v3/package.json
@@ -0,0 +1,17 @@
+{
+ "name": "@vendia/serverless-express-example",
+ "version": "",
+ "description": "Example application for running a Node Express app with an Azure Function.",
+ "scripts": {
+ "start": "func start"
+ },
+ "dependencies": {
+ "@vendia/serverless-express": "4.5.2",
+ "body-parser": "1.19.1",
+ "compression": "^1.7.4",
+ "cors": "2.8.5",
+ "ejs": "3.1.6",
+ "express": "4.17.2"
+ },
+ "author": ""
+}
diff --git a/jest-helpers/azure-http-function-v3-event.js b/jest-helpers/azure-http-function-v3-event.js
new file mode 100644
index 00000000..976dad45
--- /dev/null
+++ b/jest-helpers/azure-http-function-v3-event.js
@@ -0,0 +1,114 @@
+const { URLSearchParams } = require('url')
+const clone = require('./clone')
+const mergeDeep = require('./merge-deep')
+
+const azureHttpFunctionV3Event = {
+ traceContext: { traceparent: '', tracestate: '', attributes: {} },
+ req: {
+ headers: {
+ accept: 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9',
+ 'accept-encoding': 'gzip, deflate, br',
+ 'accept-language': 'en-US,en;q=0.9',
+ 'cache-control': 'max-age=0',
+ host: '6bwvllq3t2.execute-api.us-east-1.amazonaws.com',
+ 'sec-fetch-dest': 'document',
+ 'sec-fetch-mode': 'navigate',
+ 'sec-fetch-site': 'none',
+ 'sec-fetch-user': '?1',
+ 'upgrade-insecure-requests': '1',
+ 'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36',
+ 'x-amzn-trace-id': 'Root=1-5ff59707-4914805430277a6209549a59',
+ 'x-forwarded-for': '203.123.103.37',
+ 'x-forwarded-port': '443',
+ 'x-forwarded-proto': 'https'
+ }
+ }
+}
+
+function isJsonString (str) {
+ try {
+ JSON.parse(str)
+ } catch (e) {
+ return false
+ }
+ return true
+}
+
+function makeAzureHttpFunctionV3Event (values = {}) {
+ process.env.FUNCTIONS_EXTENSION_VERSION = '~3'
+
+ const baseEvent = clone(azureHttpFunctionV3Event)
+ const mergedEvent = mergeDeep(baseEvent, values)
+
+ mergedEvent.req.method = values.httpMethod
+ mergedEvent.req.url = 'http://localhost:7071'
+ mergedEvent.req.url += values.path
+
+ if (values.multiValueQueryStringParameters) {
+ const multiValueQueryStringParametersToArray = []
+ Object.entries(values.multiValueQueryStringParameters).forEach(([qKey, qValues]) => {
+ qValues.forEach((qValue) => {
+ multiValueQueryStringParametersToArray.push([qKey, qValue])
+ })
+ })
+ const rawQueryString = new URLSearchParams(multiValueQueryStringParametersToArray)
+ mergedEvent.req.url += '?' + rawQueryString.toString()
+ }
+
+ if (values.body && Buffer.from(values.body, 'base64').toString('base64') === values.body) {
+ mergedEvent.req.rawBody = Buffer.from(values.body, 'base64').toString('utf8')
+ } else {
+ mergedEvent.req.rawBody = values.body
+ }
+
+ if (isJsonString(mergedEvent.req.rawBody)) {
+ mergedEvent.req.body = JSON.parse(mergedEvent.req.rawBody)
+ } else {
+ mergedEvent.req.body = mergedEvent.req.rawBody
+ }
+
+ mergedEvent.req.headers = convertMultiValueHeadersToHeaders({ multiValueHeaders: values.multiValueHeaders })
+
+ return mergedEvent
+}
+
+function convertMultiValueHeadersToHeaders ({ multiValueHeaders }) {
+ const headers = {}
+
+ if (!multiValueHeaders) return headers
+
+ Object.entries(multiValueHeaders).forEach(([key, value]) => {
+ headers[key] = value.join(',')
+ })
+
+ return headers
+}
+
+function makeAzureHttpFunctionV3Response (values = {}, { shouldConvertContentLengthToInt = false } = {}) {
+ const baseResponse = {
+ body: '',
+ isBase64Encoded: false,
+ statusCode: 200,
+ headers: {
+ 'content-type': 'application/json; charset=utf-8',
+ 'x-powered-by': 'Express'
+ }
+ }
+
+ values.headers = convertMultiValueHeadersToHeaders({ multiValueHeaders: values.multiValueHeaders })
+ delete values.multiValueHeaders
+ delete values.headers['set-cookie']
+
+ if (shouldConvertContentLengthToInt) {
+ if (values.headers['content-length']) values.headers['content-length'] = parseInt(values.headers['content-length'])
+ }
+
+ const mergedResponse = mergeDeep(baseResponse, values)
+
+ return mergedResponse
+}
+
+module.exports = {
+ makeAzureHttpFunctionV3Event,
+ makeAzureHttpFunctionV3Response
+}
diff --git a/jest-helpers/index.js b/jest-helpers/index.js
index 75123099..9cd71e68 100644
--- a/jest-helpers/index.js
+++ b/jest-helpers/index.js
@@ -2,12 +2,14 @@ const { makeApiGatewayV1Event, makeApiGatewayV1Response } = require('./api-gatew
const { makeApiGatewayV2Event, makeApiGatewayV2Response } = require('./api-gateway-v2-event')
const { makeAlbEvent, makeAlbResponse } = require('./alb-event')
const { makeLambdaEdgeEvent, makeLambdaEdgeResponse } = require('./lambda-edge-event.js')
+const { makeAzureHttpFunctionV3Event, makeAzureHttpFunctionV3Response } = require('./azure-http-function-v3-event')
const EVENT_SOURCE_NAMES = [
'alb',
'apiGatewayV1',
'apiGatewayV2',
- 'lambdaEdge'
+ 'lambdaEdge',
+ 'azureHttpFunctionV3'
]
const FRAMEWORK_NAMES = [
@@ -53,6 +55,8 @@ function makeEvent ({ eventSourceName, ...rest }) {
return makeApiGatewayV2Event(rest)
case 'lambdaEdge':
return makeLambdaEdgeEvent(rest)
+ case 'azureHttpFunctionV3':
+ return makeAzureHttpFunctionV3Event(rest)
default:
throw new Error(`Unknown eventSourceName ${eventSourceName}`)
}
@@ -68,6 +72,8 @@ function makeResponse ({ eventSourceName, ...rest }, { shouldConvertContentLengt
return makeApiGatewayV2Response(rest, { shouldConvertContentLengthToInt })
case 'lambdaEdge':
return makeLambdaEdgeResponse(rest)
+ case 'azureHttpFunctionV3':
+ return makeAzureHttpFunctionV3Response(rest, { shouldConvertContentLengthToInt })
default:
throw new Error(`Unknown eventSourceName ${eventSourceName}`)
}
diff --git a/src/event-sources/azure/http-function-runtime-v3.js b/src/event-sources/azure/http-function-runtime-v3.js
new file mode 100644
index 00000000..de1c7384
--- /dev/null
+++ b/src/event-sources/azure/http-function-runtime-v3.js
@@ -0,0 +1,101 @@
+const url = require('url')
+const { getCommaDelimitedHeaders, parseCookie } = require('../utils')
+
+function getRequestValuesFromHttpFunctionEvent ({ event }) {
+ const context = event.req
+
+ const method = context.method
+ const urlObject = new url.URL(context.url)
+ const path = urlObject.pathname + urlObject.search
+ const headers = { cookies: context.headers.cookie }
+
+ Object.entries(context.headers).forEach(([headerKey, headerValue]) => {
+ headers[headerKey.toLowerCase()] = headerValue
+ })
+
+ const remoteAddress = headers['x-forwarded-for']
+
+ const body = context.rawBody
+ if (body) {
+ headers['content-length'] = Buffer.byteLength(body, 'utf8')
+ }
+
+ return {
+ method,
+ headers,
+ body,
+ remoteAddress,
+ path
+ }
+}
+
+function getResponseToHttpFunction ({ statusCode, body, headers = {}, isBase64Encoded = false, response = {} }) {
+ const responseToHttpFunction = {
+ statusCode,
+ body,
+ isBase64Encoded
+ }
+
+ if (isBase64Encoded) {
+ responseToHttpFunction.body = Buffer.from(body, 'base64')
+ responseToHttpFunction.isBase64Encoded = false
+ }
+
+ const cookies = []
+ const headerCookies = headers['set-cookie']
+
+ // Convert 'set-cookie' to Azure Function 3.x cookie object array
+ // https://github.com/Azure/azure-functions-nodejs-worker/blob/v3.x/types/index.d.ts#L150
+ if (headerCookies) {
+ for (const headerCookie of headerCookies) {
+ const parsedCookie = parseCookie(headerCookie)
+ const nameValueTuple = headerCookie.split(';')[0].split('=')
+
+ const cookie = { name: nameValueTuple[0], value: nameValueTuple[1] }
+
+ if (headerCookie.toLowerCase().includes('httponly')) {
+ cookie.httpOnly = true
+ }
+
+ if (headerCookie.toLowerCase().includes('secure')) {
+ cookie.secure = true
+ }
+
+ if (parsedCookie['max-age']) {
+ cookie.maxAge = parsedCookie['max-age']
+ }
+
+ if (parsedCookie.samesite) {
+ cookie.sameSite = parsedCookie.samesite
+ }
+
+ if (parsedCookie.expires && typeof parsedCookie.expires === 'string') {
+ cookie.expires = new Date(parsedCookie.expires)
+ } else if (parsedCookie.expires && typeof value === 'number') {
+ cookie.expires = parsedCookie.expires
+ }
+
+ if (parsedCookie.path) {
+ cookie.path = parsedCookie.path
+ }
+
+ if (parsedCookie.domain) {
+ cookie.domain = parsedCookie.domain
+ }
+
+ cookies.push(cookie)
+ }
+
+ responseToHttpFunction.cookies = cookies
+ delete headers['set-cookie']
+ }
+
+ responseToHttpFunction.headers = getCommaDelimitedHeaders({ headersMap: headers })
+
+ return responseToHttpFunction
+}
+
+module.exports = {
+ getRequest: getRequestValuesFromHttpFunctionEvent,
+ getResponse: getResponseToHttpFunction
+}
diff --git a/src/event-sources/index.js b/src/event-sources/index.js
index d9046faf..ebe5d118 100644
--- a/src/event-sources/index.js
+++ b/src/event-sources/index.js
@@ -5,6 +5,7 @@ const awsLambdaEdgeEventSource = require('./aws/lambda-edge')
const awsSnsEventSource = require('./aws/sns')
const awsSqsEventSource = require('./aws/sqs')
const awsDynamoDbEventSource = require('./aws/dynamodb')
+const azureHttpFunctionV3EventSource = require('./azure/http-function-runtime-v3')
const awsEventBridgeEventSource = require('./aws/eventbridge')
function getEventSource ({ eventSourceName }) {
@@ -21,6 +22,8 @@ function getEventSource ({ eventSourceName }) {
return awsDynamoDbEventSource
case 'AWS_SNS':
return awsSnsEventSource
+ case 'AZURE_HTTP_FUNCTION_V3':
+ return azureHttpFunctionV3EventSource
case 'AWS_SQS':
return awsSqsEventSource
case 'AWS_EVENTBRIDGE':
diff --git a/src/event-sources/utils.js b/src/event-sources/utils.js
index e47fd6c5..573c3556 100644
--- a/src/event-sources/utils.js
+++ b/src/event-sources/utils.js
@@ -87,6 +87,17 @@ function getEventSourceNameBasedOnEvent ({
if (event.requestContext) {
return event.version === '2.0' ? 'AWS_API_GATEWAY_V2' : 'AWS_API_GATEWAY_V1'
}
+ if (event.traceContext) {
+ const functionsExtensionVersion = process.env.FUNCTIONS_EXTENSION_VERSION
+
+ if (!functionsExtensionVersion) {
+ console.warn('The environment variable \'FUNCTIONS_EXTENSION_VERSION\' is not set. Only the function runtime \'~3\' is supported.')
+ } else if (functionsExtensionVersion !== '~3') {
+ console.warn('Only the function runtime \'~3\' is supported.')
+ }
+
+ return 'AZURE_HTTP_FUNCTION_V3'
+ }
if (
event.version &&
event.version === '0' &&
@@ -129,6 +140,17 @@ function getCommaDelimitedHeaders ({ headersMap, separator = ',', lowerCaseKey =
const emptyResponseMapper = () => {}
+const parseCookie = (str) =>
+ str.split(';')
+ .map((v) => v.split('='))
+ .reduce((acc, v) => {
+ if (!v[1]) {
+ return acc
+ }
+ acc[decodeURIComponent(v[0].trim().toLowerCase())] = decodeURIComponent(v[1].trim())
+ return acc
+ }, {})
+
module.exports = {
getPathWithQueryStringParams,
getRequestValuesFromEvent,
@@ -136,5 +158,6 @@ module.exports = {
getEventSourceNameBasedOnEvent,
getEventBody,
getCommaDelimitedHeaders,
- emptyResponseMapper
+ emptyResponseMapper,
+ parseCookie
}
diff --git a/src/transport.js b/src/transport.js
index c102cb17..a8d5cba9 100644
--- a/src/transport.js
+++ b/src/transport.js
@@ -63,7 +63,8 @@ function respondToEventSourceWithError ({
eventSourceName !== 'AWS_ALB' &&
eventSourceName !== 'AWS_LAMBDA_EDGE' &&
eventSourceName !== 'AWS_API_GATEWAY_V1' &&
- eventSourceName !== 'AWS_API_GATEWAY_V2'
+ eventSourceName !== 'AWS_API_GATEWAY_V2' &&
+ eventSourceName !== 'AZURE_HTTP_FUNCTION_V3'
) {
resolver.fail({ error })
return