Skip to content

Commit

Permalink
feat: add support for azure function v3 event source (#484)
Browse files Browse the repository at this point in the history
Co-authored-by: brett-vendia <[email protected]>
  • Loading branch information
Shamshiel and brett-vendia authored Apr 20, 2022
1 parent 6baf1c2 commit a20cf01
Show file tree
Hide file tree
Showing 20 changed files with 1,236 additions and 8 deletions.
47 changes: 44 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
</a>
</p>

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
Expand All @@ -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.

Expand All @@ -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)):

Expand Down Expand Up @@ -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.
Expand Down
30 changes: 28 additions & 2 deletions __tests__/integration.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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({})
})
Expand All @@ -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({
Expand All @@ -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)
})

Expand Down
42 changes: 42 additions & 0 deletions examples/azure-http-function-v3/.gitignore
Original file line number Diff line number Diff line change
@@ -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
5 changes: 5 additions & 0 deletions examples/azure-http-function-v3/.vscode/extensions.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"recommendations": [
"ms-azuretools.vscode-azurefunctions"
]
}
99 changes: 99 additions & 0 deletions examples/azure-http-function-v3/HttpExample/app.js
Original file line number Diff line number Diff line change
@@ -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
16 changes: 16 additions & 0 deletions examples/azure-http-function-v3/HttpExample/function.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"bindings": [
{
"authLevel": "anonymous",
"type": "httpTrigger",
"direction": "in",
"name": "req",
"route": "{*segments}"
},
{
"type": "http",
"direction": "out",
"name": "$return"
}
]
}
7 changes: 7 additions & 0 deletions examples/azure-http-function-v3/HttpExample/index.js
Original file line number Diff line number Diff line change
@@ -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)
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading

0 comments on commit a20cf01

Please sign in to comment.