diff --git a/README.md b/README.md index 42dfd2e..e5ee28c 100644 --- a/README.md +++ b/README.md @@ -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'; @@ -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 { diff --git a/package-lock.json b/package-lock.json index 26a00c7..580482d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,8 +11,10 @@ "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" }, "devDependencies": { "@apollo/server-integration-testsuite": "4.1.1", @@ -385,9 +387,38 @@ } }, "node_modules/@azure/functions": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/@azure/functions/-/functions-3.2.0.tgz", - "integrity": "sha512-HbE7iORnYcjLzKNf5mIQRJQDTsVxhoXHRWEZ6KWdGh4e7+F9xTloiBicavbSoVmlAYivenIVpryHanVwsQaHUw==" + "version": "3.5.1", + "resolved": "https://registry.npmjs.org/@azure/functions/-/functions-3.5.1.tgz", + "integrity": "sha512-6UltvJiuVpvHSwLcK/Zc6NfUwlkDLOFFx97BHCJzlWNsfiWwzwmTsxJXg4kE/LemKTHxPpfoPE+kOJ8hAdiKFQ==", + "dependencies": { + "iconv-lite": "^0.6.3", + "long": "^4.0.0", + "uuid": "^8.3.0" + } + }, + "node_modules/@azure/functions-v4": { + "name": "@azure/functions", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@azure/functions/-/functions-4.1.0.tgz", + "integrity": "sha512-45WDaJZiTmvaIOPSdWWKL5NgzgUWsNzXDNlF7oCMLS43lE602qG7XE6Hdg9ewPWBj55URHRU7UWTHk4uDVqBGg==", + "dependencies": { + "long": "^4.0.0", + "undici": "^5.13.0" + }, + "engines": { + "node": ">=18.0" + } + }, + "node_modules/@azure/functions/node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } }, "node_modules/@babel/code-frame": { "version": "7.18.6", @@ -1515,6 +1546,14 @@ "@jridgewell/sourcemap-codec": "^1.4.10" } }, + "node_modules/@fastify/busboy": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-2.1.0.tgz", + "integrity": "sha512-+KpH+QxZU7O4675t3mnkQKcZZg56u+K/Ct2K+N2AZYNVK8kyeo/bI18tI8aPm3tvNNRyTWfj6s5tnGNlcbQRsA==", + "engines": { + "node": ">=14" + } + }, "node_modules/@graphql-tools/merge": { "version": "8.3.12", "resolved": "https://registry.npmjs.org/@graphql-tools/merge/-/merge-8.3.12.tgz", @@ -10622,6 +10661,17 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/undici": { + "version": "5.27.2", + "resolved": "https://registry.npmjs.org/undici/-/undici-5.27.2.tgz", + "integrity": "sha512-iS857PdOEy/y3wlM3yRp+6SNQQ6xU0mmZcwRSriqk+et/cwWAtwmIGf6WkoDN2EK/AMdCO/dfXzIwi+rFMrjjQ==", + "dependencies": { + "@fastify/busboy": "^2.0.0" + }, + "engines": { + "node": ">=14.0" + } + }, "node_modules/unique-string": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/unique-string/-/unique-string-2.0.0.tgz", @@ -10689,7 +10739,6 @@ "version": "8.3.2", "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", - "dev": true, "bin": { "uuid": "dist/bin/uuid" } @@ -11300,9 +11349,33 @@ } }, "@azure/functions": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/@azure/functions/-/functions-3.2.0.tgz", - "integrity": "sha512-HbE7iORnYcjLzKNf5mIQRJQDTsVxhoXHRWEZ6KWdGh4e7+F9xTloiBicavbSoVmlAYivenIVpryHanVwsQaHUw==" + "version": "3.5.1", + "resolved": "https://registry.npmjs.org/@azure/functions/-/functions-3.5.1.tgz", + "integrity": "sha512-6UltvJiuVpvHSwLcK/Zc6NfUwlkDLOFFx97BHCJzlWNsfiWwzwmTsxJXg4kE/LemKTHxPpfoPE+kOJ8hAdiKFQ==", + "requires": { + "iconv-lite": "^0.6.3", + "long": "^4.0.0", + "uuid": "^8.3.0" + }, + "dependencies": { + "iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "requires": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + } + } + } + }, + "@azure/functions-v4": { + "version": "npm:@azure/functions@4.1.0", + "resolved": "https://registry.npmjs.org/@azure/functions/-/functions-4.1.0.tgz", + "integrity": "sha512-45WDaJZiTmvaIOPSdWWKL5NgzgUWsNzXDNlF7oCMLS43lE602qG7XE6Hdg9ewPWBj55URHRU7UWTHk4uDVqBGg==", + "requires": { + "long": "^4.0.0", + "undici": "^5.13.0" + } }, "@babel/code-frame": { "version": "7.18.6", @@ -12281,6 +12354,11 @@ } } }, + "@fastify/busboy": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-2.1.0.tgz", + "integrity": "sha512-+KpH+QxZU7O4675t3mnkQKcZZg56u+K/Ct2K+N2AZYNVK8kyeo/bI18tI8aPm3tvNNRyTWfj6s5tnGNlcbQRsA==" + }, "@graphql-tools/merge": { "version": "8.3.12", "resolved": "https://registry.npmjs.org/@graphql-tools/merge/-/merge-8.3.12.tgz", @@ -19196,6 +19274,14 @@ "which-boxed-primitive": "^1.0.2" } }, + "undici": { + "version": "5.27.2", + "resolved": "https://registry.npmjs.org/undici/-/undici-5.27.2.tgz", + "integrity": "sha512-iS857PdOEy/y3wlM3yRp+6SNQQ6xU0mmZcwRSriqk+et/cwWAtwmIGf6WkoDN2EK/AMdCO/dfXzIwi+rFMrjjQ==", + "requires": { + "@fastify/busboy": "^2.0.0" + } + }, "unique-string": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/unique-string/-/unique-string-2.0.0.tgz", @@ -19234,8 +19320,7 @@ "uuid": { "version": "8.3.2", "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", - "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", - "dev": true + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==" }, "v8-compile-cache-lib": { "version": "3.0.1", diff --git a/package.json b/package.json index 7d879d7..ee03fab 100644 --- a/package.json +++ b/package.json @@ -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" } } diff --git a/src/__tests__/func-v4.test.ts b/src/__tests__/func-v4.test.ts new file mode 100644 index 0000000..8492842 --- /dev/null +++ b/src/__tests__/func-v4.test.ts @@ -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, + testOptions?: CreateServerForIntegrationTestsOptions, + ) { + const httpServer = createServer(); + const server = new ApolloServer({ + ...serverOptions, + }); + + const handler = testOptions + ? v4.startServerAndCreateHandler(server, testOptions) + : v4.startServerAndCreateHandler(server); + + await new Promise((resolve) => { + httpServer.listen({ port: 0 }, resolve); + }); + + httpServer.addListener('request', createMockServer(handler)); + + return { + server, + url: urlForHttpServer(httpServer), + async extraCleanup() { + await new Promise((resolve) => { + httpServer.close(() => resolve()); + }); + }, + }; + }, + { + serverIsStartedInBackground: true, + noIncrementalDelivery: true, + }, + ); +}); diff --git a/src/__tests__/mockServer-v4.ts b/src/__tests__/mockServer-v4.ts new file mode 100644 index 0000000..c12e79f --- /dev/null +++ b/src/__tests__/mockServer-v4.ts @@ -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(); + }); + }; +}; diff --git a/src/func-v3.ts b/src/func-v3.ts new file mode 100644 index 0000000..bf3c3cf --- /dev/null +++ b/src/func-v3.ts @@ -0,0 +1,115 @@ +import { + ApolloServer, + BaseContext, + ContextFunction, + HTTPGraphQLRequest, + HeaderMap, +} from '@apollo/server'; +import type { + AzureFunction, + Context, + HttpRequest, + HttpRequestHeaders, +} from '@azure/functions'; + +import type { WithRequired } from '@apollo/utils.withrequired'; + +export interface AzureFunctionsContextFunctionArgument { + context: Context; + req: HttpRequest; +} + +export interface AzureFunctionsMiddlewareOptions { + context?: ContextFunction<[AzureFunctionsContextFunctionArgument], TContext>; +} + +const defaultContext: ContextFunction< + [AzureFunctionsContextFunctionArgument], + any +> = async () => ({}); + +export function startServerAndCreateHandler( + server: ApolloServer, + options?: AzureFunctionsMiddlewareOptions, +): AzureFunction; +export function startServerAndCreateHandler( + server: ApolloServer, + options: WithRequired, 'context'>, +): AzureFunction; +export function startServerAndCreateHandler( + server: ApolloServer, + options?: AzureFunctionsMiddlewareOptions, +): AzureFunction { + server.startInBackgroundHandlingStartupErrorsByLoggingAndFailingAllRequests(); + return async (context: Context, req: HttpRequest) => { + const contextFunction = options?.context ?? defaultContext; + try { + const normalizedRequest = normalizeRequest(req); + + const { body, headers, status } = await server.executeHTTPGraphQLRequest({ + httpGraphQLRequest: normalizedRequest, + context: () => contextFunction({ context, req }), + }); + + if (body.kind === 'chunked') { + throw Error('Incremental delivery not implemented'); + } + + return { + status: status || 200, + headers: { + ...Object.fromEntries(headers), + 'content-length': Buffer.byteLength(body.string).toString(), + }, + body: body.string, + }; + } catch (e) { + context.log.error('Failure processing GraphQL request', e); + return { + status: 400, + body: (e as Error).message, + }; + } + }; +} + +function normalizeRequest(req: HttpRequest): HTTPGraphQLRequest { + if (!req.method) { + throw new Error('No method'); + } + + return { + method: req.method, + headers: normalizeHeaders(req.headers), + search: new URL(req.url).search, + body: parseBody(req.method, req.body, req.headers['content-type']), + }; +} + +function parseBody( + method: string | undefined, + body: string | null | undefined, + contentType: string | undefined, +): object | null { + const isValidContentType = contentType?.startsWith('application/json'); + const isValidPostRequest = method === 'POST' && isValidContentType; + + if (isValidPostRequest) { + if (typeof body === 'string') { + return JSON.parse(body); + } + if (typeof body === 'object') { + return body; + } + } + + return null; +} + +function normalizeHeaders(headers: HttpRequestHeaders): HeaderMap { + const headerMap = new HeaderMap(); + for (const [key, value] of Object.entries(headers)) { + headerMap.set(key, value ?? ''); + } + return headerMap; +} diff --git a/src/func-v4.ts b/src/func-v4.ts new file mode 100644 index 0000000..82fe7f3 --- /dev/null +++ b/src/func-v4.ts @@ -0,0 +1,108 @@ +import { + ApolloServer, + BaseContext, + ContextFunction, + HTTPGraphQLRequest, + HeaderMap, +} from '@apollo/server'; +import type { + HttpHandler, + HttpRequest, + InvocationContext, +} from '@azure/functions-v4'; + +import type { WithRequired } from '@apollo/utils.withrequired'; + +export interface AzureFunctionsContextFunctionArgument { + context: InvocationContext; + req: HttpRequest; +} + +export interface AzureFunctionsMiddlewareOptions { + context?: ContextFunction<[AzureFunctionsContextFunctionArgument], TContext>; +} + +const defaultContext: ContextFunction< + [AzureFunctionsContextFunctionArgument], + any +> = async () => ({}); + +export function startServerAndCreateHandler( + server: ApolloServer, + options?: AzureFunctionsMiddlewareOptions, +): HttpHandler; +export function startServerAndCreateHandler( + server: ApolloServer, + options: WithRequired, 'context'>, +): HttpHandler; +export function startServerAndCreateHandler( + server: ApolloServer, + options?: AzureFunctionsMiddlewareOptions, +): HttpHandler { + server.startInBackgroundHandlingStartupErrorsByLoggingAndFailingAllRequests(); + return async (req: HttpRequest, context: InvocationContext) => { + const contextFunction = options?.context ?? defaultContext; + try { + const normalizedRequest = await normalizeRequest(req); + + const { body, headers, status } = await server.executeHTTPGraphQLRequest({ + httpGraphQLRequest: normalizedRequest, + context: () => contextFunction({ context, req }), + }); + + if (body.kind === 'chunked') { + throw Error('Incremental delivery not implemented'); + } + + return { + status: status || 200, + headers: { + ...Object.fromEntries(headers), + 'content-length': Buffer.byteLength(body.string).toString(), + }, + body: body.string, + }; + } catch (e) { + context.error('Failure processing GraphQL request', e); + return { + status: 400, + body: (e as Error).message, + }; + } + }; +} + +async function normalizeRequest(req: HttpRequest): Promise { + if (!req.method) { + throw new Error('No method'); + } + + return { + method: req.method, + headers: normalizeHeaders(req), + search: new URL(req.url).search, + body: await parseBody(req), + }; +} + +async function parseBody(req: HttpRequest): Promise { + const isValidContentType = req.headers + .get('content-type') + ?.startsWith('application/json'); + const isValidPostRequest = req.method === 'POST' && isValidContentType; + + if (isValidPostRequest) { + return req.json(); + } + + return null; +} + +function normalizeHeaders(req: HttpRequest): HeaderMap { + const headerMap = new HeaderMap(); + + for (const [key, value] of req.headers.entries()) { + headerMap.set(key, value ?? ''); + } + return headerMap; +} diff --git a/src/index.ts b/src/index.ts index 6570822..d8e2618 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,114 +1,2 @@ -import type { - AzureFunction, - Context, - HttpRequest, - HttpRequestHeaders, -} from '@azure/functions'; -import { - ApolloServer, - BaseContext, - ContextFunction, - HeaderMap, - HTTPGraphQLRequest, -} from '@apollo/server'; -import type { WithRequired } from '@apollo/utils.withrequired'; - -export interface AzureFunctionsContextFunctionArgument { - context: Context; - req: HttpRequest; -} - -export interface AzureFunctionsMiddlewareOptions { - context?: ContextFunction<[AzureFunctionsContextFunctionArgument], TContext>; -} - -const defaultContext: ContextFunction< - [AzureFunctionsContextFunctionArgument], - any -> = async () => ({}); - -export function startServerAndCreateHandler( - server: ApolloServer, - options?: AzureFunctionsMiddlewareOptions, -): AzureFunction; -export function startServerAndCreateHandler( - server: ApolloServer, - options: WithRequired, 'context'>, -): AzureFunction; -export function startServerAndCreateHandler( - server: ApolloServer, - options?: AzureFunctionsMiddlewareOptions, -): AzureFunction { - server.startInBackgroundHandlingStartupErrorsByLoggingAndFailingAllRequests(); - return async (context: Context, req: HttpRequest) => { - const contextFunction = options?.context ?? defaultContext; - try { - const normalizedRequest = normalizeRequest(req); - - const { body, headers, status } = await server.executeHTTPGraphQLRequest({ - httpGraphQLRequest: normalizedRequest, - context: () => contextFunction({ context, req }), - }); - - if (body.kind === 'chunked') { - throw Error('Incremental delivery not implemented'); - } - - return { - status: status || 200, - headers: { - ...Object.fromEntries(headers), - 'content-length': Buffer.byteLength(body.string).toString(), - }, - body: body.string, - }; - } catch (e) { - context.log.error('Failure processing GraphQL request', e); - return { - status: 400, - body: (e as Error).message, - }; - } - }; -} - -function normalizeRequest(req: HttpRequest): HTTPGraphQLRequest { - if (!req.method) { - throw new Error('No method'); - } - - return { - method: req.method, - headers: normalizeHeaders(req.headers), - search: new URL(req.url).search, - body: parseBody(req.method, req.body, req.headers['content-type']), - }; -} - -function parseBody( - method: string | undefined, - body: string | null | undefined, - contentType: string | undefined, -): object | null { - const isValidContentType = contentType?.startsWith('application/json'); - const isValidPostRequest = method === 'POST' && isValidContentType; - - if (isValidPostRequest) { - if (typeof body === 'string') { - return JSON.parse(body); - } - if (typeof body === 'object') { - return body; - } - } - - return null; -} - -function normalizeHeaders(headers: HttpRequestHeaders): HeaderMap { - const headerMap = new HeaderMap(); - for (const [key, value] of Object.entries(headers)) { - headerMap.set(key, value ?? ''); - } - return headerMap; -} +export * from './func-v3'; +export * as v4 from './func-v4'; diff --git a/src/sample/graphql/function.json b/src/samples/v3/graphql/function.json similarity index 100% rename from src/sample/graphql/function.json rename to src/samples/v3/graphql/function.json diff --git a/src/sample/graphql/index.ts b/src/samples/v3/graphql/index.ts similarity index 87% rename from src/sample/graphql/index.ts rename to src/samples/v3/graphql/index.ts index d1d4dd7..2c61929 100644 --- a/src/sample/graphql/index.ts +++ b/src/samples/v3/graphql/index.ts @@ -1,5 +1,5 @@ import { ApolloServer } from '@apollo/server'; -import { startServerAndCreateHandler } from '../..'; +import { startServerAndCreateHandler } from '../../..'; // The GraphQL schema const typeDefs = `#graphql diff --git a/src/sample/host.json b/src/samples/v3/host.json similarity index 100% rename from src/sample/host.json rename to src/samples/v3/host.json diff --git a/src/samples/v4/host.json b/src/samples/v4/host.json new file mode 100644 index 0000000..9df9136 --- /dev/null +++ b/src/samples/v4/host.json @@ -0,0 +1,15 @@ +{ + "version": "2.0", + "logging": { + "applicationInsights": { + "samplingSettings": { + "isEnabled": true, + "excludedTypes": "Request" + } + } + }, + "extensionBundle": { + "id": "Microsoft.Azure.Functions.ExtensionBundle", + "version": "[4.*, 5.0.0)" + } +} \ No newline at end of file diff --git a/src/samples/v4/src/functions/graphql.ts b/src/samples/v4/src/functions/graphql.ts new file mode 100644 index 0000000..ab3d642 --- /dev/null +++ b/src/samples/v4/src/functions/graphql.ts @@ -0,0 +1,27 @@ +import { ApolloServer } from '@apollo/server'; +import { app } from '@azure/functions-v4'; +import { v4 } from '../../../../'; + +// 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), +});