nuxt-modules / apollo

Nuxt.js module to use Vue-Apollo. The Apollo integration for GraphQL.
https://apollo.nuxtjs.org
MIT License
956 stars 198 forks source link

How to configure apollo clients using Nuxt 3 runtime config? #442

Open FernandoArteaga opened 2 years ago

FernandoArteaga commented 2 years ago

For a Nuxt 3 app: Is it possible to change the configuration of httpEndpoint and wsEndpoint once the application is built, using the environment variables and the runtime configuration?

Is there a plugin that allows this behaviour?

sneakylenny commented 1 year ago

~I was about to ask the same for but for headers. I need a dynamic header to manipulate where the data is coming from but I cant find a way to do it right now.~

Solved it by using apollo "links". They work like a middleware and allow you to modify any client attribute before making the request.

guendev commented 1 year ago

463 You can try my solution to rewrite the apollo link

juanmanuelcarrera commented 1 year ago

Based on @nguyenshort comments, by defining the ApolloLinks, including the HttpLink, in the project's plugin configuration, you can overwrite the default links by injecting the environment variable with the NuxtJS runtimeConfig.

/plugins/apollo.ts

import { createHttpLink, from } from '@apollo/client/core';
import { onError } from '@apollo/client/link/error';
import { setContext } from '@apollo/client/link/context';

export default defineNuxtPlugin((nuxtApp) => {
  // Get Nuxt runtimeConfig and apollo instance
  const runtimeConfig = useRuntimeConfig();
  const { $apollo } = useNuxtApp();

  // Create custom links (auth, error, http...)

  // Create an authLink and set authentication token if necessary
  const authLink = setContext(async (_, { headers }) => {
    return {
      headers: {
        ...headers,
        Authorization: `Bearer ${[...]}`,
      },
    };
  });

  const httpLink = authLink.concat(
    createHttpLink({
      uri: runtimeConfig.public.apiGraphqlUrl,
    })
  );

  const errorLink = onError((err) => {
    nuxtApp.callHook('apollo:error', err);
  });

  // Set custom links in the apollo client (in this case, the default apollo client)
  $apollo.defaultClient.setLink(from([errorLink, httpLink]));

  nuxtApp.hook('apollo:error', (error) => {
    console.error(error);
  });
});

This snippet overwrites the ApolloLinks that are created here within the apollo library.

const authLink = setContext(async (_, { headers }) => {
...

$apollo.defaultClient.setLink(from([errorLink, httpLink]));
toddeTV commented 1 year ago

Based on @juanmanuelcarrera and @manakuro comments, I am using the following setup in Nuxt 3.

Used dependencies:

"devDependencies": {
  "@nuxtjs/apollo": "5.0.0-alpha.5",
  "nuxt": "3.1.1",
  "@types/node": "18.11.17",
  "graphql": "16.6.0",
  "typescript": "4.9.3"
},

File nuxt.config.ts:
Providing a fake endpoint that gets overridden later, but is needed in order to start Nuxt.

export default defineNuxtConfig({
  modules: [
    '@nuxtjs/apollo',
  ],
  runtimeConfig: {
    public: {
      // override with `.env` var `NUXT_PUBLIC_APOLLO_ENDPOINT` (oc do not use `process.env.*`)
      APOLLO_ENDPOINT: '',
    },
  },

  // for `@nuxtjs/apollo`
  apollo: {
    clients: {
      default: {
        httpEndpoint: '', // must be present but will be overridden in the external config TS file (see above)
      },
    },
  },
})

File /plugins/apolloConfig.ts:

import { createHttpLink, from, ApolloLink } from '@apollo/client/core'
import { onError } from '@apollo/client/link/error'
import { setContext } from '@apollo/client/link/context'
import { provideApolloClient } from '@vue/apollo-composable'

/**
 * See example: https://github.com/nuxt-modules/apollo/issues/442
 */
export default defineNuxtPlugin((nuxtApp) => {
  const envVars = useRuntimeConfig()
  const { $apollo } = nuxtApp

  // trigger the error hook on an error
  const errorLink = onError((err) => {
    nuxtApp.callHook('apollo:error', err) // must be called bc `@nuxtjs/apollo` will not do it anymore
  })

  // create an authLink and set authentication token if necessary
  // (Can not use nuxt apollo hook `apollo:auth` anymore bc `@nuxtjs/apollo` has no control anymore.)
  const authLink = setContext(async (_, { headers }) => {
    const someToken = '...'
    return {
      headers: {
        ...headers,
        Authorization: `Bearer ${someToken}`,
      },
    }
  })

  // create an customLink as example for an custom manual link
  const customLink = new ApolloLink((operation, forward) => {
    return forward(operation).map((data) => {
      return data
    })
  })

  // Default httpLink (main communication for apollo)
  const httpLink = createHttpLink({
    uri: envVars.public.APOLLO_ENDPOINT,
    useGETForQueries: true,
  })

  // Set custom links in the apollo client.
  // This is the link chain. Will be walked through from top to bottom. It can only contain 1 terminating
  // Apollo link, see: https://www.apollographql.com/docs/react/api/link/introduction/#the-terminating-link
  $apollo.defaultClient.setLink(from([
    errorLink,
    authLink,
    customLink,
    httpLink,
  ]))

  // For using useQuery in `@vue/apollo-composable`
  provideApolloClient($apollo.defaultClient)
})
gillesgw commented 1 year ago

This solution works, but it seems rather hacky, given that in reality it is almost a matter of configuring apollo "from scratch", and at this point I don't really see the added value of installing nuxt apollo rather than apollo directly. The possibility to be able to change the endpoint(s) at runtime and not during the build seems to be quite common in the case of applications built once for multiple environments.

Can we consider adding this possibility to the plugin?

rmcmk commented 1 year ago

^ Would love to have this feature. Our app maintains multiple deployed subgraphs, being able to hot swap clients at runtime would be a huge benefit

gillesgw commented 1 year ago

I've started looking at how to integrate runtime configuration. Locally with my tests, it seems to work pretty well.

I don't know if this is the best approach. @Diizzayy, could you take a look at my commits here and tell me if this seems like a possible solution? Right now I've added a few @ts-ignore comments because I haven't been able to set the runtimeConfig type.

I'm motivated to improve this solution and evolve the documentation accordingly.

With this implementation, the clients would be configurable via nuxt runtimeConfig as follows:

// nuxt.config.ts

export default defineNuxtConfig({
  modules: ['@nuxt/ui', '@nuxtjs/apollo'],
  runtimeConfig: {
    public: {
      apollo: {
        clients: {
          default: {
            httpEndpoint: 'https://my-custom-endoint.fr',
          }
        }
      }
    }
  }
}

And then if needed via an env variable (.env or system env variable):

NUXT_PUBLIC_APOLLO_CLIENTS_DEFAULT_HTTP_ENDPOINT="http://test.com/graphql"
gillesgw commented 1 year ago

Hello,

Can we have on update on the native implementation of this feature ? @Diizzayy , has said in my previous comment, I would be happy to help.

TheYakuzo commented 1 year ago

This is a sample for Nuxt 3, @nuxtjs/apollo module who need to authenticate WebSocket for Subscription. ( No Bearer token in my case, used "x-hasura-admin-secret" on my server to authenticate ).

Link to apollo documentation for subscription

// plugins/apollo.js

import { createHttpLink, from, split } from "@apollo/client/core";
import { RetryLink } from "@apollo/client/link/retry";
import { getMainDefinition } from "@apollo/client/utilities";
import { GraphQLWsLink } from "@apollo/client/link/subscriptions";
import { onError } from "@apollo/client/link/error";
import { provideApolloClient } from "@vue/apollo-composable";
import { createClient } from "graphql-ws"; // Already import with @nuxtjs/apollo module

export default defineNuxtPlugin((nuxtApp) => {

    // Optional method to get headers 
    const getHeaders = (full = false) => {
        let hasura;
        const token = localStorage.getItem("x-hasura-admin-secret")
        if (token) hasura = atob(token);
        const headers = full
            ? { headers: { "x-hasura-admin-secret": hasura.trim() || null } }
            : { "x-hasura-admin-secret": hasura.trim() || null };

        return headers;
    }

    const envVars = useRuntimeConfig();
    const { $apollo } = nuxtApp;

    // trigger the error hook on an error
    const errorLink = onError((err) => {
        nuxtApp.callHook("apollo:error", err); 
    });

    const retryLink = new RetryLink({
        delay: {
            initial: 300,
            max: 60000,
            jitter: true,
        },
        // eslint-disable-next-line no-unused-vars
        attempts: (count, operation, e) => {
            if (e && e.response && e.response.status === 401) return false;
            return count < 30;
        },
    });

    const httpLink = createHttpLink({
        uri: envVars.public.graphqlApi // http:// ou https://
    });

    const wsLink = new GraphQLWsLink(
        createClient({
            url: envVars.public.graphqlWss, // wss://
            lazy: true,
            connectionParams: () => ({
                headers: getHeaders()
            })
        })
    );

    const splitLink = split(
        ({ query }) => {
            const definition = getMainDefinition(query);
            return (
                definition.kind === "OperationDefinition" &&
                definition.operation === "subscription"
            );
        },
        wsLink,
        httpLink
    );

    $apollo.defaultClient.setLink(
        from([errorLink, retryLink, splitLink])
    );

    provideApolloClient($apollo.defaultClient);
});

It's possible to set a simple configuration for apollo in nuxt.config.ts because main config is in custom plugin apollo.js


// nuxt.config.ts
plugins: [
      { src: '~/plugins/apollo.js', mode: 'client' },
 ],
apollo: {
      clients: {
          default: {
              connectToDevTools: true,
              httpEndpoint: process.env.NUXT_PUBLIC_GRAPHQL_API as string,
              wsEndpoint: process.env.NUXT_PUBLIC_GRAPHQL_WSS as string,
          }
      },
  },
harinlee0803 commented 1 year ago

@TheYakuzo Thanks for the example. It helped me to override the configuration for the default client using Nuxt runtime config variables. Could you give an example for overriding the configuration for another client (not default) when working with multiple clients?