Supporting apollo-upload-client

Can we add support for apollo-upload-client? We have a serverful deployment and would like to process files through our server, not having the client talk directly to a third party provider.

Since upload-client required a terminating link we have implemented the following solution:

<RedwoodApolloProvider
    ...
    links: (rwLinks) => [...rwLinks.slice(0, rwLinks.length - 1), apolloUploadClient]
/>

This removes Redwoods http terminating link, replacing it with the upload client. I can see Redwood use split() to switch to the SSE link when using subscriptions however, and would prefer if Redwood could support this natively. Happy to contribute with a PR if interested.

1 Like

I have always felt the way we do links and the terminating links aren’t flexible enough.

Tagging @dthyresson here! Could we start the conversation here about how to go about doing this since @KrasniqiR is happy to contribute?


Something to note though, I refactored how the client gets constructed here: redwood/packages/web/src/apollo/links.tsx at main · redwoodjs/redwood · GitHub

But this code is only used when the streaming-ssr experiment is enabled. My plan was to replace all of the code in apollo/index.js with this refactored version.

Hi @KrasniqiR Absolutely!

As @danny noted, the logic to assemble the links (and terminating link) isn’t a flexible as it could be – and would be great to get some ideas to make it easier to add a link like that for the upload client.

You can see the code starting here: redwood/packages/web/src/apollo/index.tsx at 2d134a8a54f32684f58fa195727cfaf52f761019 · redwoodjs/redwood · GitHub

// 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 })
  }

  // 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 =
    typeof SSELink !== 'undefined'
      ? apolloClient.split(
          ({ query }) => {
            const definition = getMainDefinition(query)

            return (
              definition.kind === 'OperationDefinition' &&
              definition.operation === 'subscription'
            )
          },
          new SSELink({
            url: uri,
            auth: { authProviderType, tokenFn: getToken },
            httpLinkConfig,
            headers,
          }),
          httpLink,
        )
      : httpLink

  /**
   * Use Trusted Documents aka Persisted Operations aka Queries
   *
   * When detecting a meta hash, Apollo Client will send the hash from the document and not the query itself.
   *
   * You must configure your GraphQL server to support this feature with the useTrustedDocuments option.
   *
   * See https://www.apollographql.com/docs/react/api/link/persisted-queries/
   */
  interface DocumentNodeWithMeta extends apolloClient.DocumentNode {
    __meta__?: {
      hash: string
    }
  }

  // Check if the query made includes the hash, and if so then make the request with the persisted query link
  const terminatingLink = apolloClient.split(
    ({ query }) => {
      const documentQuery = query as DocumentNodeWithMeta
      return documentQuery?.['__meta__']?.['hash'] !== undefined
    },
    createPersistedQueryLink({
      generateHash: (document: any) => document['__meta__']['hash'],
    }).concat(httpOrSSELink),
    httpOrSSELink,
  )

  // The order here is important. The last link *must* be a terminating link like HttpLink, SSELink, or the PersistedQueryLink.
  const redwoodApolloLinks: RedwoodApolloLinks = [
    { name: 'withToken', link: withToken },
    { name: 'authMiddleware', link: authMiddleware },
    { name: 'updateDataApolloLink', link: updateDataApolloLink },
    { name: 'httpLink', link: terminatingLink },
  ]

  let link = redwoodApolloLink

  link ??= ApolloLink.from(redwoodApolloLinks.map((l) => l.link))

  if (typeof link === 'function') {
    link = link(redwoodApolloLinks)
  }

  const client = new ApolloClient({
    // Default options for every Cell. Better to specify them here than in `beforeQuery` where it's too easy to overwrite them.
    // See https://www.apollographql.com/docs/react/api/core/ApolloClient/#example-defaultoptions-object.
    defaultOptions: {
      watchQuery: {
        // The `fetchPolicy` we expect:
        //
        // > Apollo Client executes the full query against both the cache and your GraphQL server.
        // > The query automatically updates if the result of the server-side query modifies cached fields.
        //
        // See https://www.apollographql.com/docs/react/data/queries/#cache-and-network.
        fetchPolicy: 'cache-and-network',
        // So that Cells rerender when refetching.
        // See https://www.apollographql.com/docs/react/data/queries/#inspecting-loading-states.
        notifyOnNetworkStatusChange: true,
      },
    },
    link,
    ...rest,
  })

There are like two approaches:

  1. Make the configuration “more flexible” to add other links
  2. Add the upload link to the framework (like SSELink is).

If the latter, then there is some other consideration needed:

  • Make it optional, ie, if “upload” is enabled, the link is added

This requires some vite config as don’t always want to include:

    !realtimeEnabled &&
      removeFromBundle([
        {
          id: /@redwoodjs\/web\/dist\/apollo\/sseLink/,
        },
      ]),

And some documentation on how to use with Yoga: File Uploads (Yoga)

If you want to add a GitHub issue RFC, we can bring in the team to discuss an approach.