Skip to content

Commit

Permalink
Add support for Azure Function v4
Browse files Browse the repository at this point in the history
  • Loading branch information
charlesgardyn authored and aaronpowell committed May 29, 2024
1 parent c57c0ad commit 8a5b3d0
Show file tree
Hide file tree
Showing 13 changed files with 517 additions and 127 deletions.
34 changes: 33 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,12 @@ npm install @as-integrations/azure-functions @apollo/server graphql @azure/funct

## **Usage**


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.
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)
3. Update the `index.ts` to use the Apollo integration:

**v3**
```ts
import { ApolloServer } from '@apollo/server';
import { startServerAndCreateHandler } from '@as-integrations/azure-functions';
Expand Down Expand Up @@ -54,7 +56,37 @@ const server = new ApolloServer({
export default startServerAndCreateHandler(server);
```

4. Update the `function.json` HTTP output binding to use `$return` as the name, as the integration returns from the Function Handler:
**v4**
```ts
import { ApolloServer } from '@apollo/server';
import { v4 } from '@as-integrations/azure-functions';

// The GraphQL schema
const typeDefs = `#graphql
type Query {
hello: String
}
`;

// A map of functions which return data for the schema.
const resolvers = {
Query: {
hello: () => 'world',
},
};

// Set up Apollo Server
const server = new ApolloServer({
typeDefs,
resolvers,
});

app.http('graphql', {
handler: v4.startServerAndCreateHandler(server),
});
```

4. Update the `function.json` HTTP output binding to use `$return` as the name, as the integration returns from the Function Handler **(v3 only)**:

```json
{
Expand Down
105 changes: 95 additions & 10 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,9 @@
"dependencies": {
"@apollo/server": "^4.1.1",
"@azure/functions": "^3.2.0",
"@azure/functions-v4": "npm:@azure/functions@^4.1.0",
"graphql": "^16.6.0",
"graphql-tag": "^2.12.6"
"graphql-tag": "^2.12.6",
"undici": "^5.27.2"
}
}
46 changes: 46 additions & 0 deletions src/__tests__/func-v4.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { ApolloServer, ApolloServerOptions, BaseContext } from '@apollo/server';
import {
CreateServerForIntegrationTestsOptions,
defineIntegrationTestSuite,
} from '@apollo/server-integration-testsuite';
import { createServer } from 'http';
import { v4 } from '..';
import { createMockServer, urlForHttpServer } from './mockServer-v4';

describe('Azure Functions v4', () => {
defineIntegrationTestSuite(
async function (
serverOptions: ApolloServerOptions<BaseContext>,
testOptions?: CreateServerForIntegrationTestsOptions,
) {
const httpServer = createServer();
const server = new ApolloServer({
...serverOptions,
});

const handler = testOptions
? v4.startServerAndCreateHandler(server, testOptions)
: v4.startServerAndCreateHandler(server);

await new Promise<void>((resolve) => {
httpServer.listen({ port: 0 }, resolve);
});

httpServer.addListener('request', createMockServer(handler));

return {
server,
url: urlForHttpServer(httpServer),
async extraCleanup() {
await new Promise<void>((resolve) => {
httpServer.close(() => resolve());
});
},
};
},
{
serverIsStartedInBackground: true,
noIncrementalDelivery: true,
},
);
});
72 changes: 72 additions & 0 deletions src/__tests__/mockServer-v4.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import {
HttpHandler,
InvocationContext,
type HttpMethod,
type HttpRequest,
} from '@azure/functions-v4';
import type { IncomingMessage, Server, ServerResponse } from 'http';
import type { AddressInfo } from 'net';
import { Headers, HeadersInit } from 'undici';

export function urlForHttpServer(httpServer: Server): string {
const { address, port } = httpServer.address() as AddressInfo;

// Convert IPs which mean "any address" (IPv4 or IPv6) into localhost
// corresponding loopback ip. Note that the url field we're setting is
// primarily for consumption by our test suite. If this heuristic is wrong for
// your use case, explicitly specify a frontend host (in the `host` option
// when listening).
const hostname = address === '' || address === '::' ? 'localhost' : address;

return `http://${hostname}:${port}`;
}

export const createMockServer = (handler: HttpHandler) => {
return (req: IncomingMessage, res: ServerResponse) => {
let body = '';
req.on('data', (chunk) => (body += chunk));

req.on('end', async () => {
const azReq: HttpRequest = {
method: (req.method as HttpMethod) || null,
url: new URL(req.url || '', 'http://localhost').toString(),
headers: new Headers(req.headers as HeadersInit),
body,
query: new URLSearchParams(req.url),
params: {},
user: null,
arrayBuffer: async () => {
return Buffer.from(body).buffer;
},
text: async () => {
return body;
},
json: async () => {
return JSON.parse(body);
},
blob: async () => {
throw new Error('Not implemented');
},
bodyUsed: false,
formData: async () => {
throw new Error('Not implemented');
},
};

const context = new InvocationContext({
invocationId: 'mock',
functionName: 'mock',
logHandler: console.log,
});

const azRes = await handler(azReq, context);

res.statusCode = azRes.status || 200;
Object.entries(azRes.headers ?? {}).forEach(([key, value]) => {
res.setHeader(key, value!.toString());
});
res.write(azRes.body);
res.end();
});
};
};
Loading

0 comments on commit 8a5b3d0

Please sign in to comment.