diff --git a/.github/renovate.json b/.github/renovate.json index 40fe68c78dcd..55aa8c725178 100644 --- a/.github/renovate.json +++ b/.github/renovate.json @@ -22,10 +22,8 @@ "ora", "tempy", "terminal-link", - "@types/node-fetch", "chalk", "pascalcase", - "node-fetch", "@redwoodjs/api", "@redwoodjs/api-server", "@redwoodjs/auth", diff --git a/docs/docs/cookbook/using-a-third-party-api.md b/docs/docs/cookbook/using-a-third-party-api.md index af0ae959fb79..9f0abd0d1f47 100644 --- a/docs/docs/cookbook/using-a-third-party-api.md +++ b/docs/docs/cookbook/using-a-third-party-api.md @@ -349,10 +349,10 @@ We'll enter our query at the top left and the variables (zip) at the lower left. ![image](https://user-images.githubusercontent.com/300/79395014-9cd9d980-7f2d-11ea-83b1-45aaa8506706.png) -Okay lets pull the real data from OpenWeather now. We'll use a package `node-fetch` that mimics the Fetch API in the browser: +Okay lets pull the real data from OpenWeather now. We'll use a package `cross-undici-fetch` that mimics the Fetch API in the browser: ```terminal -yarn workspace api add node-fetch@2 +yarn workspace api add cross-undici-fetch ``` And import that into the service and make the fetch. Note that `fetch` returns a Promise so we're going to convert our service to `async`/`await` to simplify things: @@ -360,7 +360,7 @@ And import that into the service and make the fetch. Note that `fetch` returns a ```javascript // api/src/services/weather/weather.js -import fetch from 'node-fetch' +import { fetch } from 'cross-undici-fetch' export const getWeather = async ({ zip }) => { const response = await fetch( @@ -515,7 +515,7 @@ Okay, let's look for that `cod` and if it's `404` then we know the zip isn't fou ```javascript {4, 12-14} // api/src/services/weather/weather.js -import fetch from 'node-fetch' +import { fetch } from 'cross-undici-fetch' import { UserInputError } from '@redwoodjs/graphql-server' export const getWeather = async ({ zip }) => { diff --git a/packages/api/package.json b/packages/api/package.json index 8094617b6b4b..b73c790ab323 100644 --- a/packages/api/package.json +++ b/packages/api/package.json @@ -32,12 +32,12 @@ "dependencies": { "@babel/runtime-corejs3": "7.16.7", "@prisma/client": "3.11.0", + "cross-undici-fetch": "0.1.25", "crypto-js": "4.1.1", "humanize-string": "2.1.0", "jsonwebtoken": "8.5.1", "jwks-rsa": "2.0.5", "md5": "2.3.0", - "node-fetch": "2.6.7", "pascalcase": "1.0.0", "pino": "7.9.1", "title-case": "3.0.3", diff --git a/packages/api/src/__tests__/normalizeRequest.test.ts b/packages/api/src/__tests__/normalizeRequest.test.ts index c48a7282d100..b68d902f5743 100644 --- a/packages/api/src/__tests__/normalizeRequest.test.ts +++ b/packages/api/src/__tests__/normalizeRequest.test.ts @@ -1,5 +1,5 @@ import type { APIGatewayProxyEvent } from 'aws-lambda' -import { Headers } from 'node-fetch' +import { Headers } from 'cross-undici-fetch' import { normalizeRequest } from '../transforms' diff --git a/packages/api/src/cors.ts b/packages/api/src/cors.ts index 18009b46a84d..f8348bea03e7 100644 --- a/packages/api/src/cors.ts +++ b/packages/api/src/cors.ts @@ -1,5 +1,6 @@ -import type { Request } from 'graphql-helix' -import { Headers } from 'node-fetch' +import { Headers } from 'cross-undici-fetch' + +import type { Request } from './transforms' export type CorsConfig = { origin?: boolean | string | string[] @@ -62,9 +63,7 @@ export function createCorsContext(cors: CorsConfig | undefined) { return request.method === 'OPTIONS' }, getRequestHeaders(request: Request): CorsHeaders { - const eventHeaders = new Headers( - request.headers as Record - ) + const eventHeaders = new Headers(request.headers as HeadersInit) const requestCorsHeaders = new Headers(corsHeaders) if (cors && cors.origin) { diff --git a/packages/api/src/transforms.ts b/packages/api/src/transforms.ts index c38ea7892764..7720121718b3 100644 --- a/packages/api/src/transforms.ts +++ b/packages/api/src/transforms.ts @@ -1,5 +1,5 @@ import type { APIGatewayProxyEvent } from 'aws-lambda' -import { Headers } from 'node-fetch' +import { Headers } from 'cross-undici-fetch' // This is the same interface used by graphql-helix // But not importing here to avoid adding a dependency diff --git a/packages/auth/src/authClients/__tests__/dbAuth.test.jsx b/packages/auth/src/authClients/__tests__/dbAuth.test.jsx index 91ddf77b2952..48bca11b8c73 100644 --- a/packages/auth/src/authClients/__tests__/dbAuth.test.jsx +++ b/packages/auth/src/authClients/__tests__/dbAuth.test.jsx @@ -2,7 +2,7 @@ import { dbAuth } from '../dbAuth' global.RWJS_API_DBAUTH_URL = '/.redwood/functions' -jest.mock('node-fetch', () => { +jest.mock('cross-undici-fetch', () => { return }) diff --git a/packages/cli/package.json b/packages/cli/package.json index 915d533a1dc0..a477bf5f49f1 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -68,7 +68,6 @@ "@babel/cli": "7.16.7", "@babel/core": "7.16.7", "@types/listr": "0.14.4", - "@types/node-fetch": "2.5.12", "jest": "27.5.1", "typescript": "4.6.2" }, diff --git a/packages/codemods/package.json b/packages/codemods/package.json index 62af3b8a41aa..b96e5243cb3c 100644 --- a/packages/codemods/package.json +++ b/packages/codemods/package.json @@ -28,12 +28,12 @@ "@babel/runtime-corejs3": "7.16.7", "@vscode/ripgrep": "1.14.2", "core-js": "3.21.1", + "cross-undici-fetch": "0.1.25", "deepmerge": "4.2.2", "fast-glob": "3.2.11", "findup-sync": "5.0.0", "jest": "27.5.1", "jscodeshift": "0.13.1", - "node-fetch": "2.6.7", "prettier": "2.5.1", "tasuku": "1.0.2", "toml": "3.0.0", diff --git a/packages/codemods/src/codemods/v0.37.x/addDirectives/addDirectives.ts b/packages/codemods/src/codemods/v0.37.x/addDirectives/addDirectives.ts index e12c7d55df28..71e779a5e292 100644 --- a/packages/codemods/src/codemods/v0.37.x/addDirectives/addDirectives.ts +++ b/packages/codemods/src/codemods/v0.37.x/addDirectives/addDirectives.ts @@ -1,8 +1,8 @@ import fs from 'fs' import path from 'path' +import { fetch } from 'cross-undici-fetch' import fg from 'fast-glob' -import fetch from 'node-fetch' import getRWPaths from '../../../lib/getRWPaths' diff --git a/packages/codemods/src/codemods/v0.38.x/updateSeedScript/updateSeedScript.ts b/packages/codemods/src/codemods/v0.38.x/updateSeedScript/updateSeedScript.ts index 0603dcfd776b..64fdb014fbb7 100644 --- a/packages/codemods/src/codemods/v0.38.x/updateSeedScript/updateSeedScript.ts +++ b/packages/codemods/src/codemods/v0.38.x/updateSeedScript/updateSeedScript.ts @@ -1,7 +1,7 @@ import fs from 'fs' import path from 'path' -import fetch from 'node-fetch' +import { fetch } from 'cross-undici-fetch' import getRootPackageJson from '../../../lib/getRootPackageJSON' import getRWPaths from '../../../lib/getRWPaths' diff --git a/packages/codemods/src/codemods/v0.50.x/updateDevFatalErrorPage/updateDevFatalErrorPage.ts b/packages/codemods/src/codemods/v0.50.x/updateDevFatalErrorPage/updateDevFatalErrorPage.ts index fc0e032dd117..8d63cc22bca4 100644 --- a/packages/codemods/src/codemods/v0.50.x/updateDevFatalErrorPage/updateDevFatalErrorPage.ts +++ b/packages/codemods/src/codemods/v0.50.x/updateDevFatalErrorPage/updateDevFatalErrorPage.ts @@ -1,8 +1,8 @@ import fs from 'fs' import path from 'path' +import { fetch } from 'cross-undici-fetch' import fg from 'fast-glob' -import fetch from 'node-fetch' import getRWPaths from '../../../lib/getRWPaths' diff --git a/packages/codemods/src/lib/fetchFileFromTemplate.ts b/packages/codemods/src/lib/fetchFileFromTemplate.ts index 2a2a0b0cb2cd..49a603879f38 100644 --- a/packages/codemods/src/lib/fetchFileFromTemplate.ts +++ b/packages/codemods/src/lib/fetchFileFromTemplate.ts @@ -1,4 +1,4 @@ -import fetch from 'node-fetch' +import { fetch } from 'cross-undici-fetch' /** * @param tag should be something like 'v0.42.1' diff --git a/packages/graphql-server/package.json b/packages/graphql-server/package.json index b4960b2f6c1b..0d2b614a9a6a 100644 --- a/packages/graphql-server/package.json +++ b/packages/graphql-server/package.json @@ -22,7 +22,6 @@ "test:watch": "yarn test --watch" }, "dependencies": { - "@envelop/core": "2.1.0", "@envelop/depth-limit": "1.3.0", "@envelop/disable-introspection": "3.1.0", "@envelop/filter-operation-type": "3.1.0", @@ -31,17 +30,16 @@ "@graphql-tools/merge": "8.2.4", "@graphql-tools/schema": "8.3.3", "@graphql-tools/utils": "8.6.3", + "@graphql-yoga/common": "0.1.0-beta.4", "@prisma/client": "3.11.0", "@redwoodjs/api": "0.49.1", "core-js": "3.21.1", + "cross-undici-fetch": "0.1.25", "graphql": "16.3.0", - "graphql-helix": "1.12.0", - "graphql-playground-html": "1.6.30", "graphql-scalars": "1.15.0", "graphql-tag": "2.12.6", "lodash.merge": "4.6.2", "lodash.omitby": "4.6.0", - "node-fetch": "2.6.7", "uuid": "8.3.2" }, "devDependencies": { diff --git a/packages/graphql-server/src/cors.ts b/packages/graphql-server/src/cors.ts deleted file mode 100644 index ab7c52c1c3e7..000000000000 --- a/packages/graphql-server/src/cors.ts +++ /dev/null @@ -1,98 +0,0 @@ -import { Request } from 'graphql-helix' -import { Headers } from 'node-fetch' - -export type CorsConfig = { - origin?: boolean | string | string[] - methods?: string | string[] - allowedHeaders?: string | string[] - exposedHeaders?: string | string[] - credentials?: boolean - maxAge?: number -} - -export type CorsHeaders = Record -export type CorsContext = ReturnType - -export function createCorsContext(cors: CorsConfig | undefined) { - // Taken from apollo-server-env - // @see: https://github.com/apollographql/apollo-server/blob/9267a79b974e397e87ad9ee408b65c46751e4565/packages/apollo-server-env/src/polyfills/fetch.js#L1 - const corsHeaders = new Headers() - - if (cors) { - if (cors.methods) { - if (typeof cors.methods === 'string') { - corsHeaders.set('access-control-allow-methods', cors.methods) - } else if (Array.isArray(cors.methods)) { - corsHeaders.set('access-control-allow-methods', cors.methods.join(',')) - } - } - - if (cors.allowedHeaders) { - if (typeof cors.allowedHeaders === 'string') { - corsHeaders.set('access-control-allow-headers', cors.allowedHeaders) - } else if (Array.isArray(cors.allowedHeaders)) { - corsHeaders.set( - 'access-control-allow-headers', - cors.allowedHeaders.join(',') - ) - } - } - - if (cors.exposedHeaders) { - if (typeof cors.exposedHeaders === 'string') { - corsHeaders.set('access-control-expose-headers', cors.exposedHeaders) - } else if (Array.isArray(cors.exposedHeaders)) { - corsHeaders.set( - 'access-control-expose-headers', - cors.exposedHeaders.join(',') - ) - } - } - - if (cors.credentials) { - corsHeaders.set('access-control-allow-credentials', 'true') - } - if (typeof cors.maxAge === 'number') { - corsHeaders.set('access-control-max-age', cors.maxAge.toString()) - } - } - - return { - shouldHandleCors(request: Request) { - return request.method === 'OPTIONS' - }, - getRequestHeaders(request: Request): CorsHeaders { - const eventHeaders = new Headers( - request.headers as Record - ) - const requestCorsHeaders = new Headers(corsHeaders) - - if (cors && cors.origin) { - const requestOrigin = eventHeaders.get('origin') - if (typeof cors.origin === 'string') { - requestCorsHeaders.set('access-control-allow-origin', cors.origin) - } else if ( - requestOrigin && - (typeof cors.origin === 'boolean' || - (Array.isArray(cors.origin) && - requestOrigin && - cors.origin.includes(requestOrigin))) - ) { - requestCorsHeaders.set('access-control-allow-origin', requestOrigin) - } - - const requestAccessControlRequestHeaders = eventHeaders.get( - 'access-control-request-headers' - ) - if (!cors.allowedHeaders && requestAccessControlRequestHeaders) { - requestCorsHeaders.set( - 'access-control-allow-headers', - requestAccessControlRequestHeaders - ) - } - } - - return Object.fromEntries(requestCorsHeaders.entries()) - }, - } -} diff --git a/packages/graphql-server/src/errors.ts b/packages/graphql-server/src/errors.ts index bfaa6f08bca3..62ed98782ba1 100644 --- a/packages/graphql-server/src/errors.ts +++ b/packages/graphql-server/src/errors.ts @@ -1,7 +1,7 @@ // based on ApolloError https://github.com/apollographql/apollo-server/blob/main/packages/apollo-server-errors/src/index.ts -import { EnvelopError } from '@envelop/core' +import { GraphQLYogaError } from '@graphql-yoga/common' -export class RedwoodGraphQLError extends EnvelopError { +export class RedwoodGraphQLError extends GraphQLYogaError { constructor(message: string, extensions?: Record) { super(message, extensions) } diff --git a/packages/graphql-server/src/functions/__tests__/graphql.test.ts b/packages/graphql-server/src/functions/__tests__/graphql.test.ts index 0003d1c97f6a..f082672ffbc7 100644 --- a/packages/graphql-server/src/functions/__tests__/graphql.test.ts +++ b/packages/graphql-server/src/functions/__tests__/graphql.test.ts @@ -1,6 +1,6 @@ /* eslint-disable react-hooks/rules-of-hooks */ /* eslint-disable @typescript-eslint/ban-ts-comment */ -import { envelop, Plugin } from '@envelop/core' +import { envelop, Plugin } from '@graphql-yoga/common' import { context, getAsyncStoreInstance } from '../../index' import { useRedwoodGlobalContextSetter } from '../../plugins/useRedwoodGlobalContextSetter' diff --git a/packages/graphql-server/src/functions/__tests__/normalizeRequest.test.ts b/packages/graphql-server/src/functions/__tests__/normalizeRequest.test.ts index b4c7e2e2c41c..bb368b4bbd80 100644 --- a/packages/graphql-server/src/functions/__tests__/normalizeRequest.test.ts +++ b/packages/graphql-server/src/functions/__tests__/normalizeRequest.test.ts @@ -1,5 +1,5 @@ import type { APIGatewayProxyEvent } from 'aws-lambda' -import { Headers } from 'node-fetch' +import { Headers } from 'cross-undici-fetch' import { normalizeRequest } from '@redwoodjs/api' @@ -62,32 +62,38 @@ test('Normalizes an aws event with base64', () => { true ) - expect(normalizeRequest(corsEventB64)).toEqual({ + const normalizedRequest = normalizeRequest(corsEventB64) + const expectedRequest = { headers: new Headers(corsEventB64.headers), method: 'POST', query: null, body: { bazinga: 'hello_world', }, + } + + expect(normalizedRequest.method).toEqual(expectedRequest.method) + expect(normalizedRequest.query).toEqual(expectedRequest.query) + expect(normalizedRequest.body).toEqual(expectedRequest.body) + expectedRequest.headers.forEach((value, key) => { + expect(normalizedRequest.headers.get(key)).toEqual(value) }) }) test('Handles CORS requests with and without b64 encoded', () => { const corsEventB64 = createMockedEvent('OPTIONS', undefined, true) - expect(normalizeRequest(corsEventB64)).toEqual({ - headers: new Headers(corsEventB64.headers), - method: 'OPTIONS', - query: null, - body: undefined, - }) - - const corsEventWithoutB64 = createMockedEvent('OPTIONS', undefined, false) - - expect(normalizeRequest(corsEventWithoutB64)).toEqual({ + const normalizedRequest = normalizeRequest(corsEventB64) + const expectedRequest = { headers: new Headers(corsEventB64.headers), method: 'OPTIONS', query: null, body: undefined, + } + expect(normalizedRequest.method).toEqual(expectedRequest.method) + expect(normalizedRequest.query).toEqual(expectedRequest.query) + expect(normalizedRequest.body).toEqual(expectedRequest.body) + expectedRequest.headers.forEach((value, key) => { + expect(normalizedRequest.headers.get(key)).toEqual(value) }) }) diff --git a/packages/graphql-server/src/functions/graphql.ts b/packages/graphql-server/src/functions/graphql.ts index 9f69e0e337a8..4aa4f0fc19e8 100644 --- a/packages/graphql-server/src/functions/graphql.ts +++ b/packages/graphql-server/src/functions/graphql.ts @@ -1,36 +1,25 @@ /* eslint-disable react-hooks/rules-of-hooks */ import { - envelop, EnvelopError, FormatErrorHandler, - useMaskedErrors, - useSchema, -} from '@envelop/core' -import type { PluginOrDisabledPlugin } from '@envelop/core' + GraphQLYogaError, +} from '@graphql-yoga/common' +import type { PluginOrDisabledPlugin } from '@graphql-yoga/common' import { useDepthLimit } from '@envelop/depth-limit' import { useDisableIntrospection } from '@envelop/disable-introspection' import { useFilterAllowedOperations } from '@envelop/filter-operation-type' -import { useParserCache } from '@envelop/parser-cache' -import { useValidationCache } from '@envelop/validation-cache' -import { normalizeRequest, RedwoodError } from '@redwoodjs/api' +import { RedwoodError } from '@redwoodjs/api' import type { APIGatewayProxyEvent, APIGatewayProxyResult, Context as LambdaContext, } from 'aws-lambda' import { GraphQLError, GraphQLSchema, OperationTypeNode } from 'graphql' -import { - getGraphQLParameters, - processRequest, - shouldRenderGraphiQL, -} from 'graphql-helix' -import { renderPlaygroundPage } from 'graphql-playground-html' +import { CORSOptions, createServer } from '@graphql-yoga/common' -import { createCorsContext } from '@redwoodjs/api' import { makeDirectivesForPlugin } from '../directives/makeDirectives' import { getAsyncStoreInstance } from '../globalContext' -import { createHealthcheckContext } from '../healthcheck' import { makeMergedSchema } from '../makeMergedSchema/makeMergedSchema' import { useRedwoodAuthContext } from '../plugins/useRedwoodAuthContext' import { @@ -44,6 +33,7 @@ import { useRedwoodPopulateContext } from '../plugins/useRedwoodPopulateContext' import { ValidationError } from '../errors' import type { GraphQLHandlerOptions } from './types' +import { Headers, Request } from 'cross-undici-fetch' /* * Prevent unexpected error messages from leaking to the GraphQL clients. @@ -51,7 +41,7 @@ import type { GraphQLHandlerOptions } from './types' * Unexpected errors are those that are not Envelop, GraphQL, or Redwood errors **/ export const formatError: FormatErrorHandler = (err: any, message: string) => { - const allowErrors = [EnvelopError, RedwoodError] + const allowErrors = [GraphQLYogaError, EnvelopError, RedwoodError] // If using graphql-scalars, when validating input // the original TypeError is wrapped in an GraphQLError object. @@ -97,11 +87,10 @@ export const createGraphQLHandler = ({ services, sdls, directives = [], - onHealthCheck, depthLimitOptions, allowedOperations, defaultError = 'Something went wrong.', - graphiQLEndpoint, + graphiQLEndpoint = '/graphql', schemaOptions, }: GraphQLHandlerOptions) => { let schema: GraphQLSchema @@ -142,13 +131,6 @@ export const createGraphQLHandler = ({ plugins.push(useDisableIntrospection()) } - // Simple LRU for caching parse results. - plugins.push(useParserCache()) - // Simple LRU for caching validate results. - plugins.push(useValidationCache()) - // Simplest plugin to provide your GraphQL schema. - plugins.push(useSchema(schema)) - // Custom Redwood plugins plugins.push(useRedwoodAuthContext(getCurrentUser)) plugins.push(useRedwoodGlobalContextSetter()) @@ -182,102 +164,137 @@ export const createGraphQLHandler = ({ // Must be "last" in plugin chain so can process any data added to results and extensions plugins.push(useRedwoodLogger(loggerConfig)) - // Prevent unexpected error messages from leaking to the GraphQL clients. - plugins.push(useMaskedErrors({ formatError, errorMessage: defaultError })) + const yoga = createServer({ + schema, + plugins, + maskedErrors: { + formatError, + errorMessage: defaultError, + }, + logging: logger, + graphiql: isDevEnv ? { + title: 'Redwood GraphQL playground', + endpoint: graphiQLEndpoint, + defaultQuery: `query Redwood { + redwood { + version + } + }`, + headerEditorEnabled: true + } : false, + cors: (request: Request) => { + const yogaCORSOptions: CORSOptions = {} + if (cors?.methods) { + if (typeof cors.methods === 'string') { + yogaCORSOptions.methods = [cors.methods] + } else if (Array.isArray(cors.methods)) { + yogaCORSOptions.methods = cors.methods + } + } + if (cors?.allowedHeaders) { + if (typeof cors.allowedHeaders === 'string') { + yogaCORSOptions.allowedHeaders = [cors.allowedHeaders] + } else if (Array.isArray(cors.allowedHeaders)) { + yogaCORSOptions.allowedHeaders = cors.allowedHeaders + } + } - const corsContext = createCorsContext(cors) + if (cors?.exposedHeaders) { + if (typeof cors.exposedHeaders === 'string') { + yogaCORSOptions.exposedHeaders = [cors.exposedHeaders] + } else if (Array.isArray(cors.exposedHeaders)) { + yogaCORSOptions.exposedHeaders = cors.exposedHeaders + } + } - const healthcheckContext = createHealthcheckContext( - onHealthCheck, - corsContext - ) + if (cors?.credentials) { + yogaCORSOptions.credentials = cors.credentials + } - const createSharedEnvelop = envelop({ - plugins, - enableInternalTracing: loggerConfig.options?.tracing, + if (cors?.maxAge) { + yogaCORSOptions.maxAge = cors.maxAge + } + + if (cors?.origin) { + const requestOrigin = request.headers.get('origin') + if (typeof cors.origin === 'string') { + yogaCORSOptions.origin = [cors.origin] + } else if ( + requestOrigin && + (typeof cors.origin === 'boolean' || + (Array.isArray(cors.origin) && + requestOrigin && + cors.origin.includes(requestOrigin))) + ) { + yogaCORSOptions.origin = [requestOrigin] + } + + const requestAccessControlRequestHeaders = request.headers.get( + 'access-control-request-headers' + ) + if (!cors.allowedHeaders && requestAccessControlRequestHeaders) { + yogaCORSOptions.allowedHeaders = [requestAccessControlRequestHeaders] + } + } + return yogaCORSOptions + }, }) const handlerFn = async ( event: APIGatewayProxyEvent, lambdaContext: LambdaContext ): Promise => { - const enveloped = createSharedEnvelop({ - event, - requestContext: lambdaContext, - }) - - const logger = loggerConfig.logger - // In the future, this could be part of a specific handler for AWS lambdas - lambdaContext.callbackWaitsForEmptyEventLoop = false - - // In the future, the normalizeRequest can take more flexible params, maybe even cloud provider name - // and return a normalized request structure. - const request = normalizeRequest(event) - - if (healthcheckContext.isHealthcheckRequest(event.path)) { - return healthcheckContext.handleHealthCheck(request, event) + const requestHeaders = new Headers() + for (const headerName in event.headers) { + const headerValue = event.headers[headerName] + if (headerValue) { + requestHeaders.append(headerName, headerValue) + } } - - const corsHeaders = cors ? corsContext.getRequestHeaders(request) : {} - - if (corsContext.shouldHandleCors(request)) { - return { - body: '', - statusCode: 200, - headers: corsHeaders, + for (const headerName in event.multiValueHeaders) { + const headerValues = event.multiValueHeaders[headerName] + if (headerValues) { + for (const headerValue of headerValues) { + requestHeaders.append(headerName, headerValue) + } } } - if (isDevEnv && shouldRenderGraphiQL(request)) { - return { - body: renderPlaygroundPage({ - endpoint: graphiQLEndpoint || '/graphql', - }), - statusCode: 200, - headers: { - 'Content-Type': 'text/html', - ...corsHeaders, - }, - } + const requestProtocol = event.requestContext.protocol || 'http' + const requestUrl = new URL(event.path, requestProtocol + '://localhost') + let request: Request + if (event.httpMethod === 'GET' || event.httpMethod === 'HEAD') { + request = new Request(requestUrl.toString(), { + method: event.httpMethod, + headers: requestHeaders, + }) + } else { + request = new Request(requestUrl.toString(), { + method: event.httpMethod, + headers: requestHeaders, + body: event.body, + }) } - const { operationName, query, variables } = getGraphQLParameters(request) + // In the future, this could be part of a specific handler for AWS lambdas + lambdaContext.callbackWaitsForEmptyEventLoop = false let lambdaResponse: APIGatewayProxyResult try { - const result = await processRequest({ - operationName, - query, - variables, - request, - validationRules: undefined, - ...enveloped, - contextFactory: enveloped.contextFactory, + const response = await yoga.handleRequest(request, { + event, + requestContext: lambdaContext, }) - - if (result.type === 'RESPONSE') { - lambdaResponse = { - body: JSON.stringify(result.payload), - statusCode: 200, - headers: { - ...(result.headers || {}).reduce( - (prev, header) => ({ ...prev, [header.name]: header.value }), - {} - ), - ...corsHeaders, - }, - } - } else if (result.type === 'MULTIPART_RESPONSE') { - lambdaResponse = { - body: JSON.stringify({ error: 'Streaming is not supported yet!' }), - statusCode: 500, - } - } else { - lambdaResponse = { - body: JSON.stringify({ error: 'Unexpected flow' }), - statusCode: 500, - } + const multiValueHeaders: APIGatewayProxyResult['multiValueHeaders'] = {} + for (const [key, value] of response.headers) { + multiValueHeaders[key] = multiValueHeaders[key] || [] + multiValueHeaders[key].push(value) + } + lambdaResponse = { + body: await response.text(), + statusCode: response.status, + multiValueHeaders, } } catch (e: any) { logger.error(e) diff --git a/packages/graphql-server/src/functions/types.ts b/packages/graphql-server/src/functions/types.ts index 439deff24c1c..54681d13d02b 100644 --- a/packages/graphql-server/src/functions/types.ts +++ b/packages/graphql-server/src/functions/types.ts @@ -1,7 +1,7 @@ -import type { PluginOrDisabledPlugin } from '@envelop/core' import { DepthLimitConfig } from '@envelop/depth-limit' import type { AllowedOperations } from '@envelop/filter-operation-type' import { IExecutableSchemaDefinition } from '@graphql-tools/schema' +import type { PluginOrDisabledPlugin } from '@graphql-yoga/common' import type { APIGatewayProxyEvent, Context as LambdaContext } from 'aws-lambda' import type { AuthContextPayload } from '@redwoodjs/api' @@ -9,7 +9,6 @@ import { CorsConfig } from '@redwoodjs/api' import { DirectiveGlobImports } from 'src/directives/makeDirectives' -import { OnHealthcheckFn } from '../healthcheck' import { LoggerConfig } from '../plugins/useRedwoodLogger' import { SdlGlobImports, ServicesGlobImports } from '../types' @@ -89,12 +88,7 @@ export interface GraphQLHandlerOptions { cors?: CorsConfig /** - * @description Healthcheck - */ - onHealthCheck?: OnHealthcheckFn - - /** - * @description Limit the complexity of the queries solely by their depth. + * @description Limit the complexity of the queries solely by their depth. * * @see https://www.npmjs.com/package/graphql-depth-limit#documentation */ diff --git a/packages/graphql-server/src/healthcheck.ts b/packages/graphql-server/src/healthcheck.ts deleted file mode 100644 index 360532a8d0cf..000000000000 --- a/packages/graphql-server/src/healthcheck.ts +++ /dev/null @@ -1,48 +0,0 @@ -import type { APIGatewayProxyEvent } from 'aws-lambda' -import { Request } from 'graphql-helix' - -import { CorsContext } from '@redwoodjs/api' - -const HEALTH_CHECK_PATH = '/health' - -export type OnHealthcheckFn = (event: APIGatewayProxyEvent) => Promise - -export function createHealthcheckContext( - onHealthcheckFn?: OnHealthcheckFn, - corsContext?: CorsContext -) { - return { - isHealthcheckRequest(requestPath: string) { - return requestPath.endsWith(HEALTH_CHECK_PATH) - }, - async handleHealthCheck(request: Request, event: APIGatewayProxyEvent) { - const corsHeaders = corsContext - ? corsContext.getRequestHeaders(request) - : {} - - if (onHealthcheckFn) { - try { - await onHealthcheckFn(event) - } catch (_) { - return { - body: JSON.stringify({ status: 'fail' }), - statusCode: 503, - headers: { - 'Content-Type': 'application/json', - ...corsHeaders, - }, - } - } - } - - return { - body: JSON.stringify({ status: 'pass' }), - statusCode: 200, - headers: { - 'Content-Type': 'application/json', - ...corsHeaders, - }, - } - }, - } -} diff --git a/packages/graphql-server/src/plugins/useRedwoodAuthContext.ts b/packages/graphql-server/src/plugins/useRedwoodAuthContext.ts index 7193cb884c59..a76e5b6c206a 100644 --- a/packages/graphql-server/src/plugins/useRedwoodAuthContext.ts +++ b/packages/graphql-server/src/plugins/useRedwoodAuthContext.ts @@ -1,4 +1,4 @@ -import { Plugin } from '@envelop/core' +import { Plugin } from '@graphql-yoga/common' import { getAuthenticationContext } from '@redwoodjs/api' diff --git a/packages/graphql-server/src/plugins/useRedwoodGlobalContextSetter.ts b/packages/graphql-server/src/plugins/useRedwoodGlobalContextSetter.ts index 4c88e851462c..13e14cc59403 100644 --- a/packages/graphql-server/src/plugins/useRedwoodGlobalContextSetter.ts +++ b/packages/graphql-server/src/plugins/useRedwoodGlobalContextSetter.ts @@ -1,4 +1,4 @@ -import { Plugin } from '@envelop/core' +import { Plugin } from '@graphql-yoga/common' import { RedwoodGraphQLContext } from '../functions/types' import { setContext } from '../index' diff --git a/packages/graphql-server/src/plugins/useRedwoodLogger.ts b/packages/graphql-server/src/plugins/useRedwoodLogger.ts index 5fc8af653ed9..e4d3a4c5523f 100644 --- a/packages/graphql-server/src/plugins/useRedwoodLogger.ts +++ b/packages/graphql-server/src/plugins/useRedwoodLogger.ts @@ -1,5 +1,7 @@ -import { Plugin } from '@envelop/core' -import { handleStreamOrSingleExecutionResult } from '@envelop/core' +import { + Plugin, + handleStreamOrSingleExecutionResult, +} from '@graphql-yoga/common' import { ExecutionResult, Kind, OperationDefinitionNode } from 'graphql' import { v4 as uuidv4 } from 'uuid' diff --git a/packages/graphql-server/src/plugins/useRedwoodPopulateContext.ts b/packages/graphql-server/src/plugins/useRedwoodPopulateContext.ts index 6644ccc24ed2..c03598c6a02b 100644 --- a/packages/graphql-server/src/plugins/useRedwoodPopulateContext.ts +++ b/packages/graphql-server/src/plugins/useRedwoodPopulateContext.ts @@ -1,4 +1,4 @@ -import { Plugin } from '@envelop/core' +import { Plugin } from '@graphql-yoga/common' import { RedwoodGraphQLContext, diff --git a/packages/prerender/package.json b/packages/prerender/package.json index e2c3abcdacaf..d956a546709f 100644 --- a/packages/prerender/package.json +++ b/packages/prerender/package.json @@ -31,8 +31,8 @@ "@redwoodjs/web": "0.49.1", "babel-plugin-ignore-html-and-css-imports": "0.1.0", "cheerio": "1.0.0-rc.10", - "mime-types": "2.1.35", - "node-fetch": "2.6.7" + "cross-undici-fetch": "0.1.25", + "mime-types": "2.1.35" }, "devDependencies": { "@babel/cli": "7.16.7", diff --git a/packages/prerender/src/internal.ts b/packages/prerender/src/internal.ts index 6d22571d2138..52814f09c242 100644 --- a/packages/prerender/src/internal.ts +++ b/packages/prerender/src/internal.ts @@ -1,7 +1,7 @@ import fs from 'fs' import path from 'path' -import fetch from 'node-fetch' +import { fetch } from 'cross-undici-fetch' import type { AuthContextInterface } from '@redwoodjs/auth' import { getConfig, getPaths } from '@redwoodjs/internal' diff --git a/packages/telemetry/package.json b/packages/telemetry/package.json index 406976767158..36fd227fa31c 100644 --- a/packages/telemetry/package.json +++ b/packages/telemetry/package.json @@ -30,8 +30,8 @@ "@redwoodjs/internal": "0.49.1", "@redwoodjs/structure": "0.49.1", "ci-info": "3.3.0", + "cross-undici-fetch": "0.1.25", "envinfo": "7.8.1", - "node-fetch": "2.6.7", "systeminformation": "5.11.9", "uuid": "8.3.2", "yargs": "16.2.0" @@ -40,7 +40,6 @@ "@babel/cli": "7.16.7", "@babel/core": "7.16.7", "@types/envinfo": "7.8.1", - "@types/node-fetch": "2.5.12", "@types/uuid": "8.3.4", "@types/yargs": "16.0.4", "jest": "27.5.1" diff --git a/packages/telemetry/src/sendTelemetry.ts b/packages/telemetry/src/sendTelemetry.ts index 1cae062fe022..05361c391f0f 100644 --- a/packages/telemetry/src/sendTelemetry.ts +++ b/packages/telemetry/src/sendTelemetry.ts @@ -2,8 +2,8 @@ import fs from 'fs' import path from 'path' import ci from 'ci-info' +import { fetch } from 'cross-undici-fetch' import envinfo from 'envinfo' -import fetch from 'node-fetch' import system from 'systeminformation' import { v4 as uuidv4 } from 'uuid' diff --git a/yarn.lock b/yarn.lock index b2721a9f1b1d..a88f93a224ab 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2293,7 +2293,7 @@ __metadata: languageName: node linkType: hard -"@envelop/core@npm:2.1.0": +"@envelop/core@npm:^2.0.0": version: 2.1.0 resolution: "@envelop/core@npm:2.1.0" dependencies: @@ -2316,7 +2316,7 @@ __metadata: languageName: node linkType: hard -"@envelop/disable-introspection@npm:3.1.0": +"@envelop/disable-introspection@npm:3.1.0, @envelop/disable-introspection@npm:^3.0.0": version: 3.1.0 resolution: "@envelop/disable-introspection@npm:3.1.0" peerDependencies: @@ -2336,7 +2336,7 @@ __metadata: languageName: node linkType: hard -"@envelop/parser-cache@npm:4.1.0": +"@envelop/parser-cache@npm:4.1.0, @envelop/parser-cache@npm:^4.0.0": version: 4.1.0 resolution: "@envelop/parser-cache@npm:4.1.0" dependencies: @@ -2370,7 +2370,7 @@ __metadata: languageName: node linkType: hard -"@envelop/validation-cache@npm:4.1.0": +"@envelop/validation-cache@npm:4.1.0, @envelop/validation-cache@npm:^4.0.0": version: 4.1.0 resolution: "@envelop/validation-cache@npm:4.1.0" dependencies: @@ -3607,7 +3607,7 @@ __metadata: languageName: node linkType: hard -"@graphql-typed-document-node/core@npm:^3.0.0": +"@graphql-typed-document-node/core@npm:^3.0.0, @graphql-typed-document-node/core@npm:^3.1.1": version: 3.1.1 resolution: "@graphql-typed-document-node/core@npm:3.1.1" peerDependencies: @@ -3616,6 +3616,45 @@ __metadata: languageName: node linkType: hard +"@graphql-yoga/common@npm:0.1.0-beta.4": + version: 0.1.0-beta.4 + resolution: "@graphql-yoga/common@npm:0.1.0-beta.4" + dependencies: + "@envelop/core": ^2.0.0 + "@envelop/disable-introspection": ^3.0.0 + "@envelop/parser-cache": ^4.0.0 + "@envelop/validation-cache": ^4.0.0 + "@graphql-tools/schema": ^8.3.1 + "@graphql-tools/utils": ^8.6.0 + "@graphql-typed-document-node/core": ^3.1.1 + "@graphql-yoga/render-graphiql": 0.1.0-beta.1 + "@graphql-yoga/subscription": 0.0.1-beta.1 + cross-undici-fetch: ^0.1.25 + dset: ^3.1.1 + tslib: ^2.3.1 + peerDependencies: + graphql: ^15.2.0 || ^16.0.0 + checksum: 8591c4fc0a5e36273da6507a819018f5731029048ef3df8703f58a0514c90466b603a30ca110e776e721ee309771a24601b935642be9b52821184feb716f7b65 + languageName: node + linkType: hard + +"@graphql-yoga/render-graphiql@npm:0.1.0-beta.1": + version: 0.1.0-beta.1 + resolution: "@graphql-yoga/render-graphiql@npm:0.1.0-beta.1" + checksum: 40ac74af7feabbe789a24ee40a856f82ad652d826c8f17ace097af4fbe6a38d587649b9fa66ccb246bb67d5123244b1f49460b5c7cc992705ebb6e2b3ba8b7db + languageName: node + linkType: hard + +"@graphql-yoga/subscription@npm:0.0.1-beta.1": + version: 0.0.1-beta.1 + resolution: "@graphql-yoga/subscription@npm:0.0.1-beta.1" + dependencies: + "@repeaterjs/repeater": ^3.0.4 + tslib: ^2.3.1 + checksum: 218cd291f823dd8df2f63a78a38151dc6b459944455dd08920e6160076af69e41bcbe61c7e9b65f3091e684971faeee5ddf9fa5159ed56d0fbd63a826eaffefb + languageName: node + linkType: hard + "@grpc/grpc-js@npm:^1.3.2, @grpc/grpc-js@npm:~1.5.0": version: 1.5.5 resolution: "@grpc/grpc-js@npm:1.5.5" @@ -5737,13 +5776,13 @@ __metadata: "@types/md5": 2.3.2 "@types/split2": 3.2.1 aws-lambda: 1.0.7 + cross-undici-fetch: 0.1.25 crypto-js: 4.1.1 humanize-string: 2.1.0 jest: 27.5.1 jsonwebtoken: 8.5.1 jwks-rsa: 2.0.5 md5: 2.3.0 - node-fetch: 2.6.7 pascalcase: 1.0.0 pino: 7.9.1 split2: 4.1.0 @@ -5809,7 +5848,6 @@ __metadata: "@redwoodjs/structure": 0.49.1 "@redwoodjs/telemetry": 0.49.1 "@types/listr": 0.14.4 - "@types/node-fetch": 2.5.12 boxen: 5.1.2 camelcase: 6.3.0 chalk: 4.1.2 @@ -5862,13 +5900,13 @@ __metadata: "@types/prettier": 2.4.4 "@vscode/ripgrep": 1.14.2 core-js: 3.21.1 + cross-undici-fetch: 0.1.25 deepmerge: 4.2.2 fast-glob: 3.2.11 findup-sync: 5.0.0 fs-extra: 10.0.1 jest: 27.5.1 jscodeshift: 0.13.1 - node-fetch: 2.6.7 prettier: 2.5.1 tasuku: 1.0.2 tempy: 1.0.1 @@ -6014,7 +6052,6 @@ __metadata: dependencies: "@babel/cli": 7.16.7 "@babel/core": 7.16.7 - "@envelop/core": 2.1.0 "@envelop/depth-limit": 1.3.0 "@envelop/disable-introspection": 3.1.0 "@envelop/filter-operation-type": 3.1.0 @@ -6025,6 +6062,7 @@ __metadata: "@graphql-tools/merge": 8.2.4 "@graphql-tools/schema": 8.3.3 "@graphql-tools/utils": 8.6.3 + "@graphql-yoga/common": 0.1.0-beta.4 "@prisma/client": 3.11.0 "@redwoodjs/api": 0.49.1 "@redwoodjs/auth": 0.49.1 @@ -6033,15 +6071,13 @@ __metadata: "@types/uuid": 8.3.4 aws-lambda: 1.0.7 core-js: 3.21.1 + cross-undici-fetch: 0.1.25 graphql: 16.3.0 - graphql-helix: 1.12.0 - graphql-playground-html: 1.6.30 graphql-scalars: 1.15.0 graphql-tag: 2.12.6 jest: 27.5.1 lodash.merge: 4.6.2 lodash.omitby: 4.6.0 - node-fetch: 2.6.7 typescript: 4.6.2 uuid: 8.3.2 languageName: unknown @@ -6113,9 +6149,9 @@ __metadata: babel-plugin-ignore-html-and-css-imports: 0.1.0 babel-plugin-tester: 10.1.0 cheerio: 1.0.0-rc.10 + cross-undici-fetch: 0.1.25 jest: 27.5.1 mime-types: 2.1.35 - node-fetch: 2.6.7 typescript: 4.6.2 peerDependencies: react: 17.0.2 @@ -6203,13 +6239,12 @@ __metadata: "@redwoodjs/internal": 0.49.1 "@redwoodjs/structure": 0.49.1 "@types/envinfo": 7.8.1 - "@types/node-fetch": 2.5.12 "@types/uuid": 8.3.4 "@types/yargs": 16.0.4 ci-info: 3.3.0 + cross-undici-fetch: 0.1.25 envinfo: 7.8.1 jest: 27.5.1 - node-fetch: 2.6.7 systeminformation: 5.11.9 uuid: 8.3.2 yargs: 16.2.0 @@ -6294,6 +6329,13 @@ __metadata: languageName: unknown linkType: soft +"@repeaterjs/repeater@npm:^3.0.4": + version: 3.0.4 + resolution: "@repeaterjs/repeater@npm:3.0.4" + checksum: 9a2928d70f2be4a8f72857f8f7553810015ac970f174b4b20f07289644379af57fa68947601d67e557c1a7c33ddf805e787cf2a1d5e9037ba485d24075a81b6b + languageName: node + linkType: hard + "@samverschueren/stream-to-observable@npm:^0.3.0": version: 0.3.1 resolution: "@samverschueren/stream-to-observable@npm:0.3.1" @@ -8097,16 +8139,6 @@ __metadata: languageName: node linkType: hard -"@types/node-fetch@npm:2.5.12": - version: 2.5.12 - resolution: "@types/node-fetch@npm:2.5.12" - dependencies: - "@types/node": "*" - form-data: ^3.0.0 - checksum: aaa69c354e596f9e293136ac43c9e5d91503415fb4eddfae3a9689153f0f033863bbd627e700b3f419ce14d06303e18e1d61b788d9085411f1fc12fc56afe356 - languageName: node - linkType: hard - "@types/node-fetch@npm:^2.5.7": version: 2.6.1 resolution: "@types/node-fetch@npm:2.6.1" @@ -12024,7 +12056,7 @@ __metadata: languageName: node linkType: hard -"commander@npm:^2.19.0, commander@npm:^2.20.0, commander@npm:^2.20.3": +"commander@npm:^2.19.0, commander@npm:^2.20.0": version: 2.20.3 resolution: "commander@npm:2.20.3" checksum: 74c781a5248c2402a0a3e966a0a2bba3c054aad144f5c023364be83265e796b20565aa9feff624132ff629aa64e16999fa40a743c10c12f7c61e96a794b99288 @@ -12731,9 +12763,9 @@ __metadata: languageName: node linkType: hard -"cross-undici-fetch@npm:^0.1.19": - version: 0.1.24 - resolution: "cross-undici-fetch@npm:0.1.24" +"cross-undici-fetch@npm:0.1.25, cross-undici-fetch@npm:^0.1.19, cross-undici-fetch@npm:^0.1.25": + version: 0.1.25 + resolution: "cross-undici-fetch@npm:0.1.25" dependencies: abort-controller: ^3.0.0 form-data-encoder: ^1.7.1 @@ -12741,7 +12773,7 @@ __metadata: node-fetch: ^2.6.7 undici: ^4.9.3 web-streams-polyfill: ^3.2.0 - checksum: afe312649db875a338a58023194b17e46e8f6e1afac34bfccda747cfcf30718219984338bd322ceaa79d796e7a2f5112bd0ccafaf1d0cff35c01808b5c151dca + checksum: cc3d7203335729da04aa402182ba86e4125221a55b20956f3b9ec7549d8e898cc02b9960554001184310ad177cda9d1595323d9c0672a26c947d4bf1ebea82f8 languageName: node linkType: hard @@ -12968,13 +13000,6 @@ __metadata: languageName: node linkType: hard -"cssfilter@npm:0.0.10": - version: 0.0.10 - resolution: "cssfilter@npm:0.0.10" - checksum: 478a227a616fb6e9bb338eb95f690df141b86231ec737cbea574484f31a09a51db894b4921afc4987459dae08d584355fd689ff2a7a7c7a74de4bb4c072ce553 - languageName: node - linkType: hard - "cssnano-preset-default@npm:^5.1.12": version: 5.1.12 resolution: "cssnano-preset-default@npm:5.1.12" @@ -13937,7 +13962,7 @@ __metadata: languageName: node linkType: hard -"dset@npm:^3.1.0": +"dset@npm:^3.1.0, dset@npm:^3.1.1": version: 3.1.1 resolution: "dset@npm:3.1.1" checksum: b26e14f364b2b849f4a4f779fe0f14d9e3e6224ed0c4139f5592c4f253ba44768ecd689b1cf04b920f0a6f88c64d959ef332745d1564c2d18e8496a1e7262a43 @@ -17008,24 +17033,6 @@ __metadata: languageName: node linkType: hard -"graphql-helix@npm:1.12.0": - version: 1.12.0 - resolution: "graphql-helix@npm:1.12.0" - peerDependencies: - graphql: ^15.3.0 || ^16.0.0 - checksum: a17d46bf054d3e6924f165d80ce1852de0cdbc14cf47dedaf95c6d1d7d65a108bb4378f952a9f3e3378990623eb9e1b0ce1178a7c2a64625e1c519cb22e5c982 - languageName: node - linkType: hard - -"graphql-playground-html@npm:1.6.30": - version: 1.6.30 - resolution: "graphql-playground-html@npm:1.6.30" - dependencies: - xss: ^1.0.6 - checksum: 32c87615b221610e57db41b68788bccf1e548ae66f38489521d4cfd2abf35666c76acf8250ce178f86b2bd825c2a9b083d496684abede1085847ca2dc7e71fb6 - languageName: node - linkType: hard - "graphql-request@npm:^3.3.0": version: 3.7.0 resolution: "graphql-request@npm:3.7.0" @@ -30857,18 +30864,6 @@ __metadata: languageName: node linkType: hard -"xss@npm:^1.0.6": - version: 1.0.10 - resolution: "xss@npm:1.0.10" - dependencies: - commander: ^2.20.3 - cssfilter: 0.0.10 - bin: - xss: bin/xss - checksum: 959bd06ef802c553fb6133bbc70de837c996202cfc292d230aedfaa1bc0c67729b913d884028ae4b1ea0a7566d35b6cadeeb7f73066f97af01596077ae12d066 - languageName: node - linkType: hard - "xtend@npm:^4.0.0, xtend@npm:^4.0.1, xtend@npm:~4.0.1": version: 4.0.2 resolution: "xtend@npm:4.0.2"