Skip to content

Commit a20cf01

Browse files
feat: add support for azure function v3 event source (#484)
Co-authored-by: brett-vendia <[email protected]>
1 parent 6baf1c2 commit a20cf01

File tree

20 files changed

+1236
-8
lines changed

20 files changed

+1236
-8
lines changed

README.md

Lines changed: 44 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
</a>
99
</p>
1010

11-
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/).
11+
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/).
1212

1313
```bash
1414
npm install @vendia/serverless-express
@@ -25,7 +25,9 @@ Want to get up and running quickly? [Check out our basic starter example](exampl
2525

2626
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.
2727

28-
## Minimal Lambda handler wrapper
28+
## AWS
29+
30+
### Minimal Lambda handler wrapper
2931

3032
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.
3133

@@ -36,7 +38,7 @@ const app = require('./app')
3638
exports.handler = serverlessExpress({ app })
3739
```
3840

39-
## Async setup Lambda handler
41+
### Async setup Lambda handler
4042

4143
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)):
4244

@@ -70,6 +72,45 @@ function handler (event, context) {
7072
exports.handler = handler
7173
```
7274

75+
## Azure
76+
77+
### Async Azure Function (v3) handler wrapper
78+
79+
The only Azure Function specific code you need to write is a simple `index.js` and a `function.json` like below.
80+
81+
```js
82+
// index.js
83+
const serverlessExpress = require('@vendia/serverless-express')
84+
const app = require('./app')
85+
const cachedServerlessExpress = serverlessExpress({ app })
86+
87+
module.exports = async function (context, req) {
88+
return cachedServerlessExpress(context, req)
89+
}
90+
```
91+
92+
The _out-binding_ parameter `"name": "$return"` is important for Serverless Express to work.
93+
94+
```json
95+
// function.json
96+
{
97+
"bindings": [
98+
{
99+
"authLevel": "anonymous",
100+
"type": "httpTrigger",
101+
"direction": "in",
102+
"name": "req",
103+
"route": "{*segments}"
104+
},
105+
{
106+
"type": "http",
107+
"direction": "out",
108+
"name": "$return"
109+
}
110+
]
111+
}
112+
```
113+
73114
## 4.x
74115

75116
1. Improved API - Simpler for end-user to use and configure.

__tests__/integration.js

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -240,6 +240,14 @@ describe.each(EACH_MATRIX)('%s:%s: integration tests', (eventSourceName, framewo
240240
delete response.multiValueHeaders.etag
241241
delete response.multiValueHeaders['last-modified']
242242
break
243+
case 'azureHttpFunctionV3':
244+
expectedResponse.body = Buffer.from(samLogoBase64, 'base64')
245+
expectedResponse.isBase64Encoded = false
246+
expect(response.headers.etag).toMatch(etagRegex)
247+
expect(response.headers['last-modified']).toMatch(lastModifiedRegex)
248+
delete response.headers.etag
249+
delete response.headers['last-modified']
250+
break
243251
case 'apiGatewayV2':
244252
expect(response.headers.etag).toMatch(etagRegex)
245253
expect(response.headers['last-modified']).toMatch(lastModifiedRegex)
@@ -388,7 +396,7 @@ describe.each(EACH_MATRIX)('%s:%s: integration tests', (eventSourceName, framewo
388396

389397
test('set-cookie', async () => {
390398
router.get('/cookie', (req, res) => {
391-
res.cookie('Foo', 'bar')
399+
res.cookie('Foo', 'bar', { domain: 'example.com', secure: true, httpOnly: true, sameSite: 'Strict' })
392400
res.cookie('Fizz', 'buzz')
393401
res.json({})
394402
})
@@ -400,7 +408,7 @@ describe.each(EACH_MATRIX)('%s:%s: integration tests', (eventSourceName, framewo
400408
const response = await serverlessExpressInstance(event)
401409

402410
const expectedSetCookieHeaders = [
403-
'Foo=bar; Path=/',
411+
'Foo=bar; Domain=example.com; Path=/; HttpOnly; Secure; SameSite=Strict',
404412
'Fizz=buzz; Path=/'
405413
]
406414
const expectedResponse = makeResponse({
@@ -414,6 +422,24 @@ describe.each(EACH_MATRIX)('%s:%s: integration tests', (eventSourceName, framewo
414422
},
415423
statusCode: 200
416424
})
425+
426+
switch (eventSourceName) {
427+
case 'azureHttpFunctionV3':
428+
expectedResponse.cookies = [
429+
{
430+
domain: 'example.com',
431+
httpOnly: true,
432+
name: 'Foo',
433+
path: '/',
434+
sameSite: 'Strict',
435+
secure: true,
436+
value: 'bar'
437+
},
438+
{ name: 'Fizz', path: '/', value: 'buzz' }
439+
]
440+
break
441+
}
442+
417443
expect(response).toEqual(expectedResponse)
418444
})
419445

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
bin
2+
obj
3+
csx
4+
.vs
5+
edge
6+
Publish
7+
8+
*.user
9+
*.suo
10+
*.cscfg
11+
*.Cache
12+
project.lock.json
13+
14+
/packages
15+
/TestResults
16+
17+
/tools/NuGet.exe
18+
/App_Data
19+
/secrets
20+
/data
21+
.secrets
22+
appsettings.json
23+
24+
node_modules
25+
dist
26+
27+
# Local python packages
28+
.python_packages/
29+
30+
# Python Environments
31+
.env
32+
.venv
33+
env/
34+
venv/
35+
ENV/
36+
env.bak/
37+
venv.bak/
38+
39+
# Byte-compiled / optimized / DLL files
40+
__pycache__/
41+
*.py[cod]
42+
*$py.class
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"recommendations": [
3+
"ms-azuretools.vscode-azurefunctions"
4+
]
5+
}
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
const path = require('path')
2+
const express = require('express')
3+
const bodyParser = require('body-parser')
4+
const cors = require('cors')
5+
const compression = require('compression')
6+
const { getCurrentInvoke } = require('../../../src/index') // require('@vendia/serverless-express')
7+
const ejs = require('ejs').__express
8+
const app = express()
9+
const router = express.Router()
10+
11+
app.set('view engine', 'ejs')
12+
app.engine('.ejs', ejs)
13+
14+
router.use(compression())
15+
router.use(cors())
16+
router.use(bodyParser.json())
17+
router.use(bodyParser.urlencoded({ extended: true }))
18+
19+
// NOTE: tests can't find the views directory without this
20+
app.set('views', path.join(__dirname, 'views'))
21+
22+
router.get('/api', (req, res) => {
23+
const currentInvoke = getCurrentInvoke()
24+
const { event = {} } = currentInvoke
25+
const { requestContext = {} } = event
26+
const { domainName = 'localhost:7071' } = requestContext
27+
const apiUrl = `https://${domainName}`
28+
res.render('index', { apiUrl })
29+
})
30+
31+
router.get('/api/vendia', (req, res) => {
32+
res.sendFile(path.join(__dirname, 'vendia-logo.png'))
33+
})
34+
35+
router.get('/api/users', (req, res) => {
36+
res.json(users)
37+
})
38+
39+
router.get('/api/users/:userId', (req, res) => {
40+
const user = getUser(req.params.userId)
41+
42+
if (!user) return res.status(404).json({})
43+
44+
return res.json(user)
45+
})
46+
47+
router.post('/api/users', (req, res) => {
48+
const user = {
49+
id: ++userIdCounter,
50+
name: req.body.name
51+
}
52+
users.push(user)
53+
res.status(201).json(user)
54+
})
55+
56+
router.put('/api/users/:userId', (req, res) => {
57+
const user = getUser(req.params.userId)
58+
59+
if (!user) return res.status(404).json({})
60+
61+
user.name = req.body.name
62+
res.json(user)
63+
})
64+
65+
router.delete('/api/users/:userId', (req, res) => {
66+
const userIndex = getUserIndex(req.params.userId)
67+
68+
if (userIndex === -1) return res.status(404).json({})
69+
70+
users.splice(userIndex, 1)
71+
res.json(users)
72+
})
73+
74+
router.get('/api/cookie', (req, res) => {
75+
res.cookie('Foo', 'bar')
76+
res.cookie('Fizz', 'buzz')
77+
res.json({})
78+
})
79+
80+
const getUser = (userId) => users.find(u => u.id === parseInt(userId))
81+
const getUserIndex = (userId) => users.findIndex(u => u.id === parseInt(userId))
82+
83+
// Ephemeral in-memory data store
84+
const users = [{
85+
id: 1,
86+
name: 'Joe'
87+
}, {
88+
id: 2,
89+
name: 'Jane'
90+
}]
91+
let userIdCounter = users.length
92+
93+
// The serverless-express library creates a server and listens on a Unix
94+
// Domain Socket for you, so you can remove the usual call to app.listen.
95+
// app.listen(3000)
96+
app.use('/', router)
97+
98+
// Export your express server so you can import it in the lambda function.
99+
module.exports = app
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
{
2+
"bindings": [
3+
{
4+
"authLevel": "anonymous",
5+
"type": "httpTrigger",
6+
"direction": "in",
7+
"name": "req",
8+
"route": "{*segments}"
9+
},
10+
{
11+
"type": "http",
12+
"direction": "out",
13+
"name": "$return"
14+
}
15+
]
16+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
const serverlessExpress = require('../../../src/index') // require('@vendia/serverless-express')
2+
const app = require('./app')
3+
const cachedServerlessExpress = serverlessExpress({ app })
4+
5+
module.exports = async function (context, req) {
6+
return cachedServerlessExpress(context, req)
7+
}
8.17 KB
Loading

0 commit comments

Comments
 (0)