Skip to content

Commit 8a5b3d0

Browse files
charlesgardynaaronpowell
authored andcommitted
Add support for Azure Function v4
1 parent c57c0ad commit 8a5b3d0

File tree

13 files changed

+517
-127
lines changed

13 files changed

+517
-127
lines changed

README.md

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,10 +23,12 @@ npm install @as-integrations/azure-functions @apollo/server graphql @azure/funct
2323

2424
## **Usage**
2525

26+
2627
1. Setup an [Azure Function with TypeScript](https://learn.microsoft.com/azure/azure-functions/create-first-function-vs-code-typescript) (or [JavaScript](https://learn.microsoft.com/azure/azure-functions/create-first-function-vs-code-node)) as per normal.
2728
2. Create a new [HTTP Trigger](https://learn.microsoft.com/azure/azure-functions/functions-bindings-http-webhook-trigger?tabs=in-process%2Cfunctionsv2&pivots=programming-language-javascript)
2829
3. Update the `index.ts` to use the Apollo integration:
2930

31+
**v3**
3032
```ts
3133
import { ApolloServer } from '@apollo/server';
3234
import { startServerAndCreateHandler } from '@as-integrations/azure-functions';
@@ -54,7 +56,37 @@ const server = new ApolloServer({
5456
export default startServerAndCreateHandler(server);
5557
```
5658

57-
4. Update the `function.json` HTTP output binding to use `$return` as the name, as the integration returns from the Function Handler:
59+
**v4**
60+
```ts
61+
import { ApolloServer } from '@apollo/server';
62+
import { v4 } from '@as-integrations/azure-functions';
63+
64+
// The GraphQL schema
65+
const typeDefs = `#graphql
66+
type Query {
67+
hello: String
68+
}
69+
`;
70+
71+
// A map of functions which return data for the schema.
72+
const resolvers = {
73+
Query: {
74+
hello: () => 'world',
75+
},
76+
};
77+
78+
// Set up Apollo Server
79+
const server = new ApolloServer({
80+
typeDefs,
81+
resolvers,
82+
});
83+
84+
app.http('graphql', {
85+
handler: v4.startServerAndCreateHandler(server),
86+
});
87+
```
88+
89+
4. Update the `function.json` HTTP output binding to use `$return` as the name, as the integration returns from the Function Handler **(v3 only)**:
5890

5991
```json
6092
{

package-lock.json

Lines changed: 95 additions & 10 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,9 @@
5555
"dependencies": {
5656
"@apollo/server": "^4.1.1",
5757
"@azure/functions": "^3.2.0",
58+
"@azure/functions-v4": "npm:@azure/functions@^4.1.0",
5859
"graphql": "^16.6.0",
59-
"graphql-tag": "^2.12.6"
60+
"graphql-tag": "^2.12.6",
61+
"undici": "^5.27.2"
6062
}
6163
}

src/__tests__/func-v4.test.ts

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import { ApolloServer, ApolloServerOptions, BaseContext } from '@apollo/server';
2+
import {
3+
CreateServerForIntegrationTestsOptions,
4+
defineIntegrationTestSuite,
5+
} from '@apollo/server-integration-testsuite';
6+
import { createServer } from 'http';
7+
import { v4 } from '..';
8+
import { createMockServer, urlForHttpServer } from './mockServer-v4';
9+
10+
describe('Azure Functions v4', () => {
11+
defineIntegrationTestSuite(
12+
async function (
13+
serverOptions: ApolloServerOptions<BaseContext>,
14+
testOptions?: CreateServerForIntegrationTestsOptions,
15+
) {
16+
const httpServer = createServer();
17+
const server = new ApolloServer({
18+
...serverOptions,
19+
});
20+
21+
const handler = testOptions
22+
? v4.startServerAndCreateHandler(server, testOptions)
23+
: v4.startServerAndCreateHandler(server);
24+
25+
await new Promise<void>((resolve) => {
26+
httpServer.listen({ port: 0 }, resolve);
27+
});
28+
29+
httpServer.addListener('request', createMockServer(handler));
30+
31+
return {
32+
server,
33+
url: urlForHttpServer(httpServer),
34+
async extraCleanup() {
35+
await new Promise<void>((resolve) => {
36+
httpServer.close(() => resolve());
37+
});
38+
},
39+
};
40+
},
41+
{
42+
serverIsStartedInBackground: true,
43+
noIncrementalDelivery: true,
44+
},
45+
);
46+
});

src/__tests__/mockServer-v4.ts

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
import {
2+
HttpHandler,
3+
InvocationContext,
4+
type HttpMethod,
5+
type HttpRequest,
6+
} from '@azure/functions-v4';
7+
import type { IncomingMessage, Server, ServerResponse } from 'http';
8+
import type { AddressInfo } from 'net';
9+
import { Headers, HeadersInit } from 'undici';
10+
11+
export function urlForHttpServer(httpServer: Server): string {
12+
const { address, port } = httpServer.address() as AddressInfo;
13+
14+
// Convert IPs which mean "any address" (IPv4 or IPv6) into localhost
15+
// corresponding loopback ip. Note that the url field we're setting is
16+
// primarily for consumption by our test suite. If this heuristic is wrong for
17+
// your use case, explicitly specify a frontend host (in the `host` option
18+
// when listening).
19+
const hostname = address === '' || address === '::' ? 'localhost' : address;
20+
21+
return `http://${hostname}:${port}`;
22+
}
23+
24+
export const createMockServer = (handler: HttpHandler) => {
25+
return (req: IncomingMessage, res: ServerResponse) => {
26+
let body = '';
27+
req.on('data', (chunk) => (body += chunk));
28+
29+
req.on('end', async () => {
30+
const azReq: HttpRequest = {
31+
method: (req.method as HttpMethod) || null,
32+
url: new URL(req.url || '', 'http://localhost').toString(),
33+
headers: new Headers(req.headers as HeadersInit),
34+
body,
35+
query: new URLSearchParams(req.url),
36+
params: {},
37+
user: null,
38+
arrayBuffer: async () => {
39+
return Buffer.from(body).buffer;
40+
},
41+
text: async () => {
42+
return body;
43+
},
44+
json: async () => {
45+
return JSON.parse(body);
46+
},
47+
blob: async () => {
48+
throw new Error('Not implemented');
49+
},
50+
bodyUsed: false,
51+
formData: async () => {
52+
throw new Error('Not implemented');
53+
},
54+
};
55+
56+
const context = new InvocationContext({
57+
invocationId: 'mock',
58+
functionName: 'mock',
59+
logHandler: console.log,
60+
});
61+
62+
const azRes = await handler(azReq, context);
63+
64+
res.statusCode = azRes.status || 200;
65+
Object.entries(azRes.headers ?? {}).forEach(([key, value]) => {
66+
res.setHeader(key, value!.toString());
67+
});
68+
res.write(azRes.body);
69+
res.end();
70+
});
71+
};
72+
};

0 commit comments

Comments
 (0)