From e764ba1e823c5ee2fee4b5698cd5237d5e07d59b Mon Sep 17 00:00:00 2001 From: Danny Choudhury Date: Fri, 9 Aug 2024 13:18:47 +0700 Subject: [PATCH 01/10] feat(uploads): Configure apollo client to do multi-part form uploads --- packages/api-server/package.json | 1 + .../api-server/src/createServerHelpers.ts | 3 +- packages/api-server/src/plugins/graphql.ts | 7 +- packages/web/package.json | 2 + packages/web/src/apollo/index.tsx | 26 ++-- yarn.lock | 111 +++++++++++++++++- 6 files changed, 133 insertions(+), 17 deletions(-) diff --git a/packages/api-server/package.json b/packages/api-server/package.json index e14d47b54f05..9845ca3d41ad 100644 --- a/packages/api-server/package.json +++ b/packages/api-server/package.json @@ -29,6 +29,7 @@ "test:watch": "vitest watch" }, "dependencies": { + "@fastify/multipart": "^8.3.0", "@fastify/url-data": "5.4.0", "@redwoodjs/context": "workspace:*", "@redwoodjs/fastify-web": "workspace:*", diff --git a/packages/api-server/src/createServerHelpers.ts b/packages/api-server/src/createServerHelpers.ts index 978c7dd15f8c..1fa0d2ac49da 100644 --- a/packages/api-server/src/createServerHelpers.ts +++ b/packages/api-server/src/createServerHelpers.ts @@ -46,7 +46,7 @@ export interface CreateServerOptions { type DefaultCreateServerOptions = Required< Omit & { - fastifyServerOptions: Pick + fastifyServerOptions: FastifyServerOptions } > @@ -59,6 +59,7 @@ export const DEFAULT_CREATE_SERVER_OPTIONS: DefaultCreateServerOptions = { }, fastifyServerOptions: { requestTimeout: 15_000, + bodyLimit: 1024 * 1024 * 15, // 15MB }, configureApiServer: () => {}, parseArgs: true, diff --git a/packages/api-server/src/plugins/graphql.ts b/packages/api-server/src/plugins/graphql.ts index 08cdc279e515..68dcacd6acd5 100644 --- a/packages/api-server/src/plugins/graphql.ts +++ b/packages/api-server/src/plugins/graphql.ts @@ -1,3 +1,4 @@ +import fastifyMultiPart from '@fastify/multipart' import fastifyUrlData from '@fastify/url-data' import fg from 'fast-glob' import type { @@ -6,7 +7,6 @@ import type { FastifyReply, FastifyRequest, } from 'fastify' -import fastifyRawBody from 'fastify-raw-body' import type { Plugin } from 'graphql-yoga' import type { GlobalContext } from '@redwoodjs/context' @@ -33,10 +33,13 @@ export async function redwoodFastifyGraphQLServer( redwoodOptions.apiRootPath ??= '/' redwoodOptions.apiRootPath = coerceRootPath(redwoodOptions.apiRootPath) + // @MARK: We need to disable this in order for multipart requests to work + // otherwise you get incomprehensible errors like: 'Missing multipart form field "operations"' + // await fastify.register(fastifyRawBody) fastify.register(fastifyUrlData) + fastify.register(fastifyMultiPart) // Starting in Fastify v4, we have to await the fastifyRawBody plugin's registration // to ensure it's ready - await fastify.register(fastifyRawBody) const method = ['GET', 'POST', 'OPTIONS'] as HTTPMethods[] diff --git a/packages/web/package.json b/packages/web/package.json index 0dfeffb569bc..32733a24c693 100644 --- a/packages/web/package.json +++ b/packages/web/package.json @@ -146,6 +146,7 @@ "@redwoodjs/auth": "workspace:*", "@redwoodjs/server-store": "workspace:*", "@whatwg-node/fetch": "0.9.19", + "apollo-upload-client": "^18.0.1", "core-js": "3.37.1", "graphql": "16.9.0", "graphql-sse": "2.5.3", @@ -167,6 +168,7 @@ "@rollup/plugin-babel": "6.0.4", "@testing-library/jest-dom": "6.4.8", "@testing-library/react": "14.3.1", + "@types/apollo-upload-client": "^18", "@types/react": "^18.2.55", "@types/react-dom": "^18.2.19", "concurrently": "8.2.2", diff --git a/packages/web/src/apollo/index.tsx b/packages/web/src/apollo/index.tsx index 05de0f56bcc4..1834cd871ba0 100644 --- a/packages/web/src/apollo/index.tsx +++ b/packages/web/src/apollo/index.tsx @@ -11,13 +11,13 @@ import type { import { ApolloProvider, ApolloClient, - ApolloLink, InMemoryCache, split, + ApolloLink, } from '@apollo/client' import { setLogVerbosity as apolloSetLogVerbosity } from '@apollo/client/core/core.cjs' import { setContext } from '@apollo/client/link/context/context.cjs' -import { HttpLink } from '@apollo/client/link/http/http.cjs' +import type { HttpLink } from '@apollo/client/link/http/http.cjs' import { createPersistedQueryLink } from '@apollo/client/link/persisted-queries/persisted-queries.cjs' import { useQuery, @@ -28,7 +28,7 @@ import { useSuspenseQuery, } from '@apollo/client/react/hooks/hooks.cjs' import { getMainDefinition } from '@apollo/client/utilities/utilities.cjs' -import { fetch as crossFetch } from '@whatwg-node/fetch' +import createUploadLink from 'apollo-upload-client/createUploadLink.mjs' import { print } from 'graphql/language/printer.js' import type { UseAuth } from '@redwoodjs/auth' @@ -222,14 +222,16 @@ const ApolloProviderWithFetchConfig: React.FunctionComponent<{ // A terminating link. Apollo Client uses this to send GraphQL operations to a server over HTTP. // See https://www.apollographql.com/docs/react/api/link/introduction/#the-terminating-link. - let httpLink = new HttpLink({ uri, ...httpLinkConfig }) - if (globalThis.RWJS_EXP_STREAMING_SSR) { - httpLink = new HttpLink({ uri, fetch: crossFetch, ...httpLinkConfig }) - } + // Internally uploadLink determines whether to use form-data vs http link + const uploadLink: ApolloLink = createUploadLink({ + uri, + ...httpLinkConfig, + // The upload link types don't seem to match the ApolloLink types, even though it comes from Apollo. + }) as unknown as ApolloLink // Our terminating link needs to be smart enough to handle subscriptions, and if the GraphQL query // is subscription it needs to use the SSELink (server sent events link). - const httpOrSSELink = + const uploadOrSSELink = typeof SSELink !== 'undefined' ? split( ({ query }) => { @@ -246,9 +248,9 @@ const ApolloProviderWithFetchConfig: React.FunctionComponent<{ httpLinkConfig, headers, }), - httpLink, + uploadLink, ) - : httpLink + : uploadLink /** * Use Trusted Documents aka Persisted Operations aka Queries @@ -273,8 +275,8 @@ const ApolloProviderWithFetchConfig: React.FunctionComponent<{ }, createPersistedQueryLink({ generateHash: (document: any) => document['__meta__']['hash'], - }).concat(httpOrSSELink), - httpOrSSELink, + }).concat(uploadOrSSELink as any), + uploadOrSSELink as any, ) // The order here is important. The last link *must* be a terminating link like HttpLink, SSELink, or the PersistedQueryLink. diff --git a/yarn.lock b/yarn.lock index 9336878b1c22..ea855a901641 100644 --- a/yarn.lock +++ b/yarn.lock @@ -167,6 +167,43 @@ __metadata: languageName: node linkType: hard +"@apollo/client@npm:^3.8.0": + version: 3.11.4 + resolution: "@apollo/client@npm:3.11.4" + dependencies: + "@graphql-typed-document-node/core": "npm:^3.1.1" + "@wry/caches": "npm:^1.0.0" + "@wry/equality": "npm:^0.5.6" + "@wry/trie": "npm:^0.5.0" + graphql-tag: "npm:^2.12.6" + hoist-non-react-statics: "npm:^3.3.2" + optimism: "npm:^0.18.0" + prop-types: "npm:^15.7.2" + rehackt: "npm:^0.1.0" + response-iterator: "npm:^0.2.6" + symbol-observable: "npm:^4.0.0" + ts-invariant: "npm:^0.10.3" + tslib: "npm:^2.3.0" + zen-observable-ts: "npm:^1.2.5" + peerDependencies: + graphql: ^15.0.0 || ^16.0.0 + graphql-ws: ^5.5.5 + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || >=19.0.0-rc <19.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || >=19.0.0-rc <19.0.0 + subscriptions-transport-ws: ^0.9.0 || ^0.11.0 + peerDependenciesMeta: + graphql-ws: + optional: true + react: + optional: true + react-dom: + optional: true + subscriptions-transport-ws: + optional: true + checksum: 10c0/1d0f848a68803e4987f7bceb6c4d76a1c9c584a66cefbcacfe8aebb342b68e4cca96ecb76e7a5a423f5047da8c35e52d971cad3d532248976976c32336eac7d9 + languageName: node + linkType: hard + "@ardatan/relay-compiler@npm:12.0.0": version: 12.0.0 resolution: "@ardatan/relay-compiler@npm:12.0.0" @@ -3118,7 +3155,7 @@ __metadata: languageName: node linkType: hard -"@fastify/busboy@npm:^2.0.0": +"@fastify/busboy@npm:^2.0.0, @fastify/busboy@npm:^2.1.0": version: 2.1.1 resolution: "@fastify/busboy@npm:2.1.1" checksum: 10c0/6f8027a8cba7f8f7b736718b013f5a38c0476eea67034c94a0d3c375e2b114366ad4419e6a6fa7ffc2ef9c6d3e0435d76dd584a7a1cbac23962fda7650b579e3 @@ -3167,6 +3204,20 @@ __metadata: languageName: node linkType: hard +"@fastify/multipart@npm:^8.3.0": + version: 8.3.0 + resolution: "@fastify/multipart@npm:8.3.0" + dependencies: + "@fastify/busboy": "npm:^2.1.0" + "@fastify/deepmerge": "npm:^1.0.0" + "@fastify/error": "npm:^3.0.0" + fastify-plugin: "npm:^4.0.0" + secure-json-parse: "npm:^2.4.0" + stream-wormhole: "npm:^1.1.0" + checksum: 10c0/1021675af149435b1e585cfcaf8aba848c3799cbc213c18a0e3d74c6d64d21db27572a99295a8da5263f5562869452234dea2680e83e248456d97b560fb627eb + languageName: node + linkType: hard + "@fastify/reply-from@npm:^9.0.0": version: 9.8.0 resolution: "@fastify/reply-from@npm:9.8.0" @@ -7236,6 +7287,7 @@ __metadata: version: 0.0.0-use.local resolution: "@redwoodjs/api-server@workspace:packages/api-server" dependencies: + "@fastify/multipart": "npm:^8.3.0" "@fastify/url-data": "npm:5.4.0" "@redwoodjs/context": "workspace:*" "@redwoodjs/fastify-web": "workspace:*" @@ -8773,9 +8825,11 @@ __metadata: "@rollup/plugin-babel": "npm:6.0.4" "@testing-library/jest-dom": "npm:6.4.8" "@testing-library/react": "npm:14.3.1" + "@types/apollo-upload-client": "npm:^18" "@types/react": "npm:^18.2.55" "@types/react-dom": "npm:^18.2.19" "@whatwg-node/fetch": "npm:0.9.19" + apollo-upload-client: "npm:^18.0.1" concurrently: "npm:8.2.2" core-js: "npm:3.37.1" graphql: "npm:16.9.0" @@ -10346,6 +10400,17 @@ __metadata: languageName: node linkType: hard +"@types/apollo-upload-client@npm:^18": + version: 18.0.0 + resolution: "@types/apollo-upload-client@npm:18.0.0" + dependencies: + "@apollo/client": "npm:^3.8.0" + "@types/extract-files": "npm:*" + graphql: "npm:14 - 16" + checksum: 10c0/e733c3a2abe6286da5557e393c0fcc9946bfdeb9115aabc978d7443f58ada466cb56ba98331d25266664186c6e424aea783eadd4fb7870c3d93f5b3fcafc696a + languageName: node + linkType: hard + "@types/archiver@npm:^6": version: 6.0.2 resolution: "@types/archiver@npm:6.0.2" @@ -10675,6 +10740,13 @@ __metadata: languageName: node linkType: hard +"@types/extract-files@npm:*": + version: 13.0.1 + resolution: "@types/extract-files@npm:13.0.1" + checksum: 10c0/4732f875db36498a4bf3deb6965267d515cc7a82be6a3b0346a6b9a6c9eec173ebae774301070de6c7f9b879121d81bc106498cce9cc7bc521b2e75b84710ed2 + languageName: node + linkType: hard + "@types/find-cache-dir@npm:^3.2.1": version: 3.2.1 resolution: "@types/find-cache-dir@npm:3.2.1" @@ -12122,6 +12194,18 @@ __metadata: languageName: node linkType: hard +"apollo-upload-client@npm:^18.0.1": + version: 18.0.1 + resolution: "apollo-upload-client@npm:18.0.1" + dependencies: + extract-files: "npm:^13.0.0" + peerDependencies: + "@apollo/client": ^3.8.0 + graphql: 14 - 16 + checksum: 10c0/30803d91df5ee32231afc8e15fa52be2a46b11c827255e8028b4f49149bcc8e4b4d4469e33d4331e63e12aa697e9a5e359a32ae3608335c51a76dbe62ad55bcb + languageName: node + linkType: hard + "app-root-dir@npm:^1.0.2": version: 1.0.2 resolution: "app-root-dir@npm:1.0.2" @@ -16974,6 +17058,15 @@ __metadata: languageName: node linkType: hard +"extract-files@npm:^13.0.0": + version: 13.0.0 + resolution: "extract-files@npm:13.0.0" + dependencies: + is-plain-obj: "npm:^4.1.0" + checksum: 10c0/ee1e6e37f24fcb7de5019c0dc054f3e075664f63b5a9bd38c9ba1a32481ed1e77fd237adf826a7f6c7a1db406d9db4428f8ae4395fb2dfb602235b15f8f4bc3e + languageName: node + linkType: hard + "extract-zip@npm:2.0.1": version: 2.0.1 resolution: "extract-zip@npm:2.0.1" @@ -18448,7 +18541,7 @@ __metadata: languageName: node linkType: hard -"graphql@npm:16.9.0, graphql@npm:^16.0.0, graphql@npm:^16.8.1": +"graphql@npm:14 - 16, graphql@npm:16.9.0, graphql@npm:^16.0.0, graphql@npm:^16.8.1": version: 16.9.0 resolution: "graphql@npm:16.9.0" checksum: 10c0/a8850f077ff767377237d1f8b1da2ec70aeb7623cdf1dfc9e1c7ae93accc0c8149c85abe68923be9871a2934b1bce5a2496f846d4d56e1cfb03eaaa7ddba9b6a @@ -19634,6 +19727,13 @@ __metadata: languageName: node linkType: hard +"is-plain-obj@npm:^4.1.0": + version: 4.1.0 + resolution: "is-plain-obj@npm:4.1.0" + checksum: 10c0/32130d651d71d9564dc88ba7e6fda0e91a1010a3694648e9f4f47bb6080438140696d3e3e15c741411d712e47ac9edc1a8a9de1fe76f3487b0d90be06ac9975e + languageName: node + linkType: hard + "is-plain-object@npm:5.0.0, is-plain-object@npm:^5.0.0": version: 5.0.0 resolution: "is-plain-object@npm:5.0.0" @@ -27509,6 +27609,13 @@ __metadata: languageName: node linkType: hard +"stream-wormhole@npm:^1.1.0": + version: 1.1.0 + resolution: "stream-wormhole@npm:1.1.0" + checksum: 10c0/50800bcc919c01085b0bafa175c61a0c0bec27987dcc20aec92f8125bdc8b191102a030e114760d2ac86265eea65627d0145eea3adb8cb4453b3295e4468661a + languageName: node + linkType: hard + "streamsearch@npm:^1.1.0": version: 1.1.0 resolution: "streamsearch@npm:1.1.0" From 6f4a7e079087fc40009297295ed1d8e199bd9f2d Mon Sep 17 00:00:00 2001 From: Danny Choudhury Date: Fri, 9 Aug 2024 15:50:32 +0700 Subject: [PATCH 02/10] Merge config in the way its done before --- packages/api-server/src/createServerHelpers.ts | 1 + packages/api-server/src/plugins/graphql.ts | 2 -- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/api-server/src/createServerHelpers.ts b/packages/api-server/src/createServerHelpers.ts index 1fa0d2ac49da..6b6878878eaf 100644 --- a/packages/api-server/src/createServerHelpers.ts +++ b/packages/api-server/src/createServerHelpers.ts @@ -90,6 +90,7 @@ export function resolveOptions( requestTimeout: DEFAULT_CREATE_SERVER_OPTIONS.fastifyServerOptions.requestTimeout, logger: options.logger ?? DEFAULT_CREATE_SERVER_OPTIONS.logger, + bodyLimit: DEFAULT_CREATE_SERVER_OPTIONS.fastifyServerOptions.bodyLimit, }, configureApiServer: options.configureApiServer ?? diff --git a/packages/api-server/src/plugins/graphql.ts b/packages/api-server/src/plugins/graphql.ts index 68dcacd6acd5..a006a7bd33ef 100644 --- a/packages/api-server/src/plugins/graphql.ts +++ b/packages/api-server/src/plugins/graphql.ts @@ -38,8 +38,6 @@ export async function redwoodFastifyGraphQLServer( // await fastify.register(fastifyRawBody) fastify.register(fastifyUrlData) fastify.register(fastifyMultiPart) - // Starting in Fastify v4, we have to await the fastifyRawBody plugin's registration - // to ensure it's ready const method = ['GET', 'POST', 'OPTIONS'] as HTTPMethods[] From a34a90475d5ac8c1509bf3aa18bf329500eb1a29 Mon Sep 17 00:00:00 2001 From: Danny Choudhury Date: Fri, 9 Aug 2024 17:47:12 +0700 Subject: [PATCH 03/10] Try workaround by bundling apollo-upload in CJS builds only --- packages/web/build.ts | 21 ++++++++++++++++++- packages/web/src/apollo/index.tsx | 4 ++-- .../web/src/bundled/apollo-upload-client.ts | 3 +++ 3 files changed, 25 insertions(+), 3 deletions(-) create mode 100644 packages/web/src/bundled/apollo-upload-client.ts diff --git a/packages/web/build.ts b/packages/web/build.ts index 5b4c9ba50895..ba31305314b0 100644 --- a/packages/web/build.ts +++ b/packages/web/build.ts @@ -1,5 +1,7 @@ import { writeFileSync } from 'node:fs' +import * as esbuild from 'esbuild' + import { build, defaultBuildOptions, @@ -17,7 +19,11 @@ import { */ await build({ entryPointOptions: { - ignore: [...defaultIgnorePatterns, 'src/__typetests__/**'], //, 'src/entry/**'], + ignore: [ + ...defaultIgnorePatterns, + 'src/__typetests__/**', + 'src/bundled/**', // <-- ⭐ + ], }, buildOptions: { ...defaultBuildOptions, @@ -42,6 +48,19 @@ await build({ }, }) +// Workaround for apollo-client-upload being ESM-only +// In ESM version of rwjs/web, we don't actually bundle it, we just reexport. +// In the CJS version (see ⭐ above), we bundle it below. +// This only ever gets used during prerender, so bundle size is not a concern. +await esbuild.build({ + entryPoints: ['src/bundled/*'], + outdir: 'dist/cjs/bundled', + format: 'cjs', + bundle: true, + logLevel: 'info', + tsconfig: 'tsconfig.build.json', +}) + // Place a package.json file with `type: commonjs` in the dist/cjs folder so // that all .js files are treated as CommonJS files. writeFileSync('dist/cjs/package.json', JSON.stringify({ type: 'commonjs' })) diff --git a/packages/web/src/apollo/index.tsx b/packages/web/src/apollo/index.tsx index 1834cd871ba0..acb750983411 100644 --- a/packages/web/src/apollo/index.tsx +++ b/packages/web/src/apollo/index.tsx @@ -28,13 +28,13 @@ import { useSuspenseQuery, } from '@apollo/client/react/hooks/hooks.cjs' import { getMainDefinition } from '@apollo/client/utilities/utilities.cjs' -import createUploadLink from 'apollo-upload-client/createUploadLink.mjs' import { print } from 'graphql/language/printer.js' import type { UseAuth } from '@redwoodjs/auth' import { useNoAuth } from '@redwoodjs/auth' -import './typeOverride.js' +import './typeOverride.js' +import { createUploadLink } from '../bundled/apollo-upload-client.js' import { FetchConfigProvider, useFetchConfig, diff --git a/packages/web/src/bundled/apollo-upload-client.ts b/packages/web/src/bundled/apollo-upload-client.ts new file mode 100644 index 000000000000..ceca04cdba52 --- /dev/null +++ b/packages/web/src/bundled/apollo-upload-client.ts @@ -0,0 +1,3 @@ +import createUploadLink from 'apollo-upload-client/createUploadLink.mjs' + +export { createUploadLink } From 2c3602df24dd3840c228ee6e9a770cdba47d7fe5 Mon Sep 17 00:00:00 2001 From: Danny Choudhury Date: Fri, 9 Aug 2024 17:57:28 +0700 Subject: [PATCH 04/10] Update api server test --- packages/api-server/src/__tests__/createServer.test.ts | 1 + packages/web/src/apollo/index.tsx | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/api-server/src/__tests__/createServer.test.ts b/packages/api-server/src/__tests__/createServer.test.ts index d37e2eef75b4..ea7b6fa98c42 100644 --- a/packages/api-server/src/__tests__/createServer.test.ts +++ b/packages/api-server/src/__tests__/createServer.test.ts @@ -233,6 +233,7 @@ describe('resolveOptions', () => { requestTimeout: DEFAULT_CREATE_SERVER_OPTIONS.fastifyServerOptions.requestTimeout, logger: DEFAULT_CREATE_SERVER_OPTIONS.logger, + bodyLimit: DEFAULT_CREATE_SERVER_OPTIONS.fastifyServerOptions.bodyLimit, }, apiPort: 65501, apiHost: '::', diff --git a/packages/web/src/apollo/index.tsx b/packages/web/src/apollo/index.tsx index acb750983411..a519f6c4ab50 100644 --- a/packages/web/src/apollo/index.tsx +++ b/packages/web/src/apollo/index.tsx @@ -275,8 +275,8 @@ const ApolloProviderWithFetchConfig: React.FunctionComponent<{ }, createPersistedQueryLink({ generateHash: (document: any) => document['__meta__']['hash'], - }).concat(uploadOrSSELink as any), - uploadOrSSELink as any, + }).concat(uploadOrSSELink), + uploadOrSSELink, ) // The order here is important. The last link *must* be a terminating link like HttpLink, SSELink, or the PersistedQueryLink. From b8eac26e08885a434bc62f9f57b811cf1a6f6279 Mon Sep 17 00:00:00 2001 From: Danny Choudhury Date: Fri, 9 Aug 2024 18:16:43 +0700 Subject: [PATCH 05/10] Lock version, update comment --- packages/web/package.json | 2 +- packages/web/src/apollo/index.tsx | 3 ++- yarn.lock | 4 ++-- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/packages/web/package.json b/packages/web/package.json index 32733a24c693..250570396246 100644 --- a/packages/web/package.json +++ b/packages/web/package.json @@ -146,7 +146,7 @@ "@redwoodjs/auth": "workspace:*", "@redwoodjs/server-store": "workspace:*", "@whatwg-node/fetch": "0.9.19", - "apollo-upload-client": "^18.0.1", + "apollo-upload-client": "18.0.1", "core-js": "3.37.1", "graphql": "16.9.0", "graphql-sse": "2.5.3", diff --git a/packages/web/src/apollo/index.tsx b/packages/web/src/apollo/index.tsx index a519f6c4ab50..c98c6d7f8865 100644 --- a/packages/web/src/apollo/index.tsx +++ b/packages/web/src/apollo/index.tsx @@ -226,7 +226,8 @@ const ApolloProviderWithFetchConfig: React.FunctionComponent<{ const uploadLink: ApolloLink = createUploadLink({ uri, ...httpLinkConfig, - // The upload link types don't seem to match the ApolloLink types, even though it comes from Apollo. + // The upload link types don't match the ApolloLink types, even though it comes from Apollo + // because they use ESM imports and we're using the default ones. }) as unknown as ApolloLink // Our terminating link needs to be smart enough to handle subscriptions, and if the GraphQL query diff --git a/yarn.lock b/yarn.lock index ea855a901641..d129a47c60a3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8829,7 +8829,7 @@ __metadata: "@types/react": "npm:^18.2.55" "@types/react-dom": "npm:^18.2.19" "@whatwg-node/fetch": "npm:0.9.19" - apollo-upload-client: "npm:^18.0.1" + apollo-upload-client: "npm:18.0.1" concurrently: "npm:8.2.2" core-js: "npm:3.37.1" graphql: "npm:16.9.0" @@ -12194,7 +12194,7 @@ __metadata: languageName: node linkType: hard -"apollo-upload-client@npm:^18.0.1": +"apollo-upload-client@npm:18.0.1": version: 18.0.1 resolution: "apollo-upload-client@npm:18.0.1" dependencies: From 11c7eb30d65fd7c01ffc0921bfd747e613f096cb Mon Sep 17 00:00:00 2001 From: Danny Choudhury Date: Fri, 9 Aug 2024 18:25:51 +0700 Subject: [PATCH 06/10] Add changeset --- .changesets/11175.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 .changesets/11175.md diff --git a/.changesets/11175.md b/.changesets/11175.md new file mode 100644 index 000000000000..60f9e88a05a6 --- /dev/null +++ b/.changesets/11175.md @@ -0,0 +1,16 @@ +- feat(uploads): Configure apollo client to do multi-part form uploads (#11175) by @dac09 + +a) Configures the Apollo client we export to use upload link - https://github.com/jaydenseric/apollo-upload-client +b) Configures our API side fastify server to accept multipart form data + + +Notes: +1. apollo-upload-client is ESM only. In order to get this working for prerender, I had to bundle it for CJS version only. +Without this change you get errors during prerender like this: +``` +Error [ERR_REQUIRE_ESM]: require() of ES Module /Users/dac09/Experiments/apollo-upload-link/node_modules/apollo-upload-client/createUploadLink.mjs not supported. +``` + +2. Currently the multi-part config only applies when you have a server file (see separate PR with fix: https://github.com/redwoodjs/redwood/pull/11176) + +3. The upload link internally will handle whether to do a regular POST or multipart POST. In order to make use of this on the backend you need to set your graphql schema field to a scalar of `File` From 7c3c4ec8b2197abeeb4f35451ab34e1c1a7382d0 Mon Sep 17 00:00:00 2001 From: Danny Choudhury Date: Wed, 14 Aug 2024 20:54:21 +0700 Subject: [PATCH 07/10] Add File to graphql codegen --- packages/internal/src/generate/graphqlCodeGen.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/internal/src/generate/graphqlCodeGen.ts b/packages/internal/src/generate/graphqlCodeGen.ts index 08b383f0ea36..73f51e42a4d6 100644 --- a/packages/internal/src/generate/graphqlCodeGen.ts +++ b/packages/internal/src/generate/graphqlCodeGen.ts @@ -288,6 +288,7 @@ async function getPluginConfig(side: CodegenSide) { JSONObject: 'Prisma.JsonObject', Time: side === CodegenSide.WEB ? 'string' : 'Date | string', Byte: 'Buffer', + File: 'File', }, // prevent type names being PetQueryQuery, RW generators already append // Query/Mutation/etc From be28c542b6ea899d125d0b38bda64f0b21c49947 Mon Sep 17 00:00:00 2001 From: Danny Choudhury Date: Thu, 15 Aug 2024 12:55:44 +0700 Subject: [PATCH 08/10] Add test for multipart plugin --- .../src/__tests__/graphqlPlugin.test.ts | 59 +++++++++++++++++++ 1 file changed, 59 insertions(+) create mode 100644 packages/api-server/src/__tests__/graphqlPlugin.test.ts diff --git a/packages/api-server/src/__tests__/graphqlPlugin.test.ts b/packages/api-server/src/__tests__/graphqlPlugin.test.ts new file mode 100644 index 000000000000..93e3c70e8050 --- /dev/null +++ b/packages/api-server/src/__tests__/graphqlPlugin.test.ts @@ -0,0 +1,59 @@ +import path from 'path' + +import fastifyMultipart from '@fastify/multipart' +import { + vi, + beforeAll, + afterAll, + describe, + afterEach, + it, + expect, +} from 'vitest' + +import createFastifyInstance from '../fastify' +import { redwoodFastifyGraphQLServer } from '../plugins/graphql' + +// Set up RWJS_CWD. +let original_RWJS_CWD: string | undefined + +beforeAll(async () => { + original_RWJS_CWD = process.env.RWJS_CWD + process.env.RWJS_CWD = path.join(__dirname, './fixtures/redwood-app') +}) + +afterAll(() => { + process.env.RWJS_CWD = original_RWJS_CWD +}) + +describe('RedwoodFastifyGraphqlServer Fastify Plugin', () => { + beforeAll(async () => { + vi.spyOn(console, 'warn').mockImplementation(() => {}) + vi.spyOn(console, 'log').mockImplementation(() => {}) + }) + + afterEach(() => { + vi.clearAllMocks() + }) + + afterAll(async () => { + vi.mocked(console.log).mockRestore() + vi.mocked(console.warn).mockRestore() + }) + + it('registers the fastify multipart plugin to support graphql-uploads', async () => { + const fastifyInstance = await createFastifyInstance() + + const registerSpy = vi.spyOn(fastifyInstance, 'register') + + // Although this is not how you normally register a plugin, we're going to + // doing it this way gives us the ability to spy on the register method + await redwoodFastifyGraphQLServer(fastifyInstance, { + redwood: {}, + }) + + expect(registerSpy).toHaveBeenCalledWith(fastifyMultipart) + + await fastifyInstance.close() + }) +}) From dc4f1be9cd994761869536ff8c4e829bbf7bf2d1 Mon Sep 17 00:00:00 2001 From: Danny Choudhury Date: Mon, 19 Aug 2024 10:14:30 +0700 Subject: [PATCH 09/10] Pin fastify/multipart --- packages/api-server/package.json | 2 +- yarn.lock | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/api-server/package.json b/packages/api-server/package.json index 8f4a4ea2c4a9..89428f495008 100644 --- a/packages/api-server/package.json +++ b/packages/api-server/package.json @@ -29,7 +29,7 @@ "test:watch": "vitest watch" }, "dependencies": { - "@fastify/multipart": "^8.3.0", + "@fastify/multipart": "8.3.0", "@fastify/url-data": "5.4.0", "@redwoodjs/context": "workspace:*", "@redwoodjs/fastify-web": "workspace:*", diff --git a/yarn.lock b/yarn.lock index 19a40bca2805..a90d1e392572 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3186,7 +3186,7 @@ __metadata: languageName: node linkType: hard -"@fastify/multipart@npm:^8.3.0": +"@fastify/multipart@npm:8.3.0": version: 8.3.0 resolution: "@fastify/multipart@npm:8.3.0" dependencies: @@ -7222,7 +7222,7 @@ __metadata: version: 0.0.0-use.local resolution: "@redwoodjs/api-server@workspace:packages/api-server" dependencies: - "@fastify/multipart": "npm:^8.3.0" + "@fastify/multipart": "npm:8.3.0" "@fastify/url-data": "npm:5.4.0" "@redwoodjs/context": "workspace:*" "@redwoodjs/fastify-web": "workspace:*" From 36809e3eb7ccd874ee4bed5899435073ea8dcef1 Mon Sep 17 00:00:00 2001 From: Danny Choudhury Date: Mon, 19 Aug 2024 13:44:54 +0700 Subject: [PATCH 10/10] =?UTF-8?q?Update=20formatting=20in=20changeset=20?= =?UTF-8?q?=F0=9F=A4=B7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .changesets/11175.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/.changesets/11175.md b/.changesets/11175.md index 60f9e88a05a6..96c11ffbc650 100644 --- a/.changesets/11175.md +++ b/.changesets/11175.md @@ -3,10 +3,11 @@ a) Configures the Apollo client we export to use upload link - https://github.com/jaydenseric/apollo-upload-client b) Configures our API side fastify server to accept multipart form data - Notes: -1. apollo-upload-client is ESM only. In order to get this working for prerender, I had to bundle it for CJS version only. -Without this change you get errors during prerender like this: + +1. apollo-upload-client is ESM only. In order to get this working for prerender, I had to bundle it for CJS version only. + Without this change you get errors during prerender like this: + ``` Error [ERR_REQUIRE_ESM]: require() of ES Module /Users/dac09/Experiments/apollo-upload-link/node_modules/apollo-upload-client/createUploadLink.mjs not supported. ```