How to initialize ApolloClient in SvelteKit to work on both SSR and client side - apollo

I tried but didn't work. Got an error: Error when evaluating SSR module /node_modules/cross-fetch/dist/browser-ponyfill.js:
<script lang="ts">
import fetch from 'cross-fetch';
import { ApolloClient, InMemoryCache, HttpLink } from "#apollo/client";
const client = new ApolloClient({
ssrMode: true,
link: new HttpLink({ uri: '/graphql', fetch }),
uri: 'http://localhost:4000/graphql',
cache: new InMemoryCache()
});
</script>

With SvelteKit the subject of CSR vs. SSR and where data fetching should happen is a bit deeper than with other somewhat "similar" solutions. The bellow guide should help you connect some of the dots, but a couple of things need to be stated first.
To define a server side route create a file with the .js extension anywhere in the src/routes directory tree. This .js file can have all the import statements required without the JS bundles that they reference being sent to the web browser.
The #apollo/client is quite huge as it contains the react dependency. Instead, you might wanna consider importing just the #apollo/client/core even if you're setting up the Apollo Client to be used only on the server side, as the demo bellow shows. The #apollo/client is not an ESM package. Notice how it's imported bellow in order for the project to build with the node adapter successfully.
Try going though the following steps.
Create a new SvelteKit app and choose the 'SvelteKit demo app' in the first step of the SvelteKit setup wizard. Answer the "Use TypeScript?" question with N as well as all of the questions afterwards.
npm init svelte#next demo-app
cd demo-app
Modify the package.json accordingly. Optionally check for all packages updates with npx npm-check-updates -u
{
"name": "demo-app",
"version": "0.0.1",
"scripts": {
"dev": "svelte-kit dev",
"build": "svelte-kit build --verbose",
"preview": "svelte-kit preview"
},
"devDependencies": {
"#apollo/client": "^3.3.15",
"#sveltejs/adapter-node": "next",
"#sveltejs/kit": "next",
"graphql": "^15.5.0",
"node-fetch": "^2.6.1",
"svelte": "^3.37.0"
},
"type": "module",
"dependencies": {
"#fontsource/fira-mono": "^4.2.2",
"#lukeed/uuid": "^2.0.0",
"cookie": "^0.4.1"
}
}
Modify the svelte.config.js accordingly.
import node from '#sveltejs/adapter-node';
export default {
kit: {
// By default, `npm run build` will create a standard Node app.
// You can create optimized builds for different platforms by
// specifying a different adapter
adapter: node(),
// hydrate the <div id="svelte"> element in src/app.html
target: '#svelte'
}
};
Create the src/lib/Client.js file with the following contents. This is the Apollo Client setup file.
import fetch from 'node-fetch';
import { ApolloClient, HttpLink } from '#apollo/client/core/core.cjs.js';
import { InMemoryCache } from '#apollo/client/cache/cache.cjs.js';
class Client {
constructor() {
if (Client._instance) {
return Client._instance
}
Client._instance = this;
this.client = this.setupClient();
}
setupClient() {
const link = new HttpLink({
uri: 'http://localhost:4000/graphql',
fetch
});
const client = new ApolloClient({
link,
cache: new InMemoryCache()
});
return client;
}
}
export const client = (new Client()).client;
Create the src/routes/qry/test.js with the following contents. This is the server side route. In case the graphql schema doesn't have the double function specify different query, input(s) and output.
import { client } from '$lib/Client.js';
import { gql } from '#apollo/client/core/core.cjs.js';
export const post = async request => {
const { num } = request.body;
try {
const query = gql`
query Doubled($x: Int) {
double(number: $x)
}
`;
const result = await client.query({
query,
variables: { x: num }
});
return {
status: 200,
body: {
nodes: result.data.double
}
}
} catch (err) {
return {
status: 500,
error: 'Error retrieving data'
}
}
}
Add the following to the load function of routes/todos/index.svelte file within <script context="module">...</script> tag.
try {
const res = await fetch('/qry/test', {
method: 'POST',
credentials: 'same-origin',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
num: 19
})
});
const data = await res.json();
console.log(data);
} catch (err) {
console.error(err);
}
Finally execute npm install and npm run dev commands. Load the site in your web browser and see the server side route being queried from the client whenever you hover over the TODOS link on the navbar. In the console's network tab notice how much quicker is the response from the test route on every second and subsequent request thanks to the Apollo client instance being a singleton.

Two things to have in mind when using phaleth solution above: caching and authenticated requests.
Since the client is used in the endpoint /qry/test.js, the singleton pattern with the caching behavior makes your server stateful. So if A then B make the same query B could end up seeing some of A data.
Same problem if you need authorization headers in your query. You would need to set this up in the setupClient method like so
setupClient(sometoken) {
...
const authLink = setContext((_, { headers }) => {
return {
headers: {
...headers,
authorization: `Bearer ${sometoken}`
}
};
});
const client = new ApolloClient({
credentials: 'include',
link: authLink.concat(link),
cache: new InMemoryCache()
});
}
But then with the singleton pattern this becomes problematic if you have multiple users.
To keep your server stateless, a work around is to avoid the singleton pattern and create a new Client(sometoken) in the endpoint.
This is not an optimal solution: it recreates the client on each request and basically just erases the cache. But this solves the caching and authorization concerns when you have multiple users.

Related

How would I update the authorization header from a cookie on a graphQL apollo mutation or query

I have the following _app.js for my NextJS app.
I want to change the authorization header on login via a cookie that will be set, I think I can handle the cookie and login functionaility, but I am stuck on how to get the cookie into the ApolloClient headers autorization. Is there a way to pass in a mutation, the headers with a token from the cookie. Any thoughts here???
I have the cookie working, so I have a logged in token, but I need to change the apolloclient Token to the new one via the cookie, in the _app.js. Not sure how this is done.
import "../styles/globals.css";
import { ApolloClient, ApolloProvider, InMemoryCache } from "#apollo/client";
const client = new ApolloClient({
uri: "https://graphql.fauna.com/graphql",
cache: new InMemoryCache(),
headers: {
authorization: `Bearer ${process.env.NEXT_PUBLIC_FAUNA_SECRET}`,
},
});
console.log(client.link.options.headers);
function MyApp({ Component, pageProps }) {
return (
<ApolloProvider client={client}>
<Component {...pageProps} />
</ApolloProvider>
);
}
export default MyApp;
UPDATE:I've read something about setting this to pass the cookie int he apollo docs, but I don't quite understand it.
const link = createHttpLink({
uri: '/graphql',
credentials: 'same-origin'
});
const client = new ApolloClient({
cache: new InMemoryCache(),
link,
});
UPDATE: So I have made good progress with the above, it allows me to pass via the context in useQuery, like below. Now the only problem is the cookieData loads before the use query or something, because if I pass in a api key it works but the fetched cookie gives me invalid db secret and its the same key.
const { data: cookieData, error: cookieError } = useSWR(
"/api/cookie",
fetcher
);
console.log(cookieData);
// const { loading, error, data } = useQuery(FORMS);
const { loading, error, data } = useQuery(FORMS, {
context: {
headers: {
authorization: "Bearer " + cookieData,
},
},
});
Any ideas on this problem would be great.
If you need to run some GraphQL queries after some other data is loaded, then I recommend putting the latter queries in a separate React component with the secret as a prop and only loading it once the former data is available. Or you can use lazy queries.
separate component
const Form = ({ cookieData }) => {
useQuery(FORMS, {
context: {
headers: {
authorization: "Bearer " + cookieData,
},
},
});
return /* ... whatever ... */
}
const FormWrapper = () => {
const { data: cookieData, error: cookieError } = useSWR(
"/api/cookie",
fetcher
);
return cookieData ? <Form cookieData={ cookieData }/> : ...loading
}
I might be missing some nuances with when/how React will mount and unmount the inner component, so I suppose you should be careful with that.
Manual Execution with useLazyQuery
https://www.apollographql.com/docs/react/data/queries/#manual-execution-with-uselazyquery

VueApollo CORS request fails

i'm facing a problem trying to make a request to API with address different against client.
client app lives at http://localhost:8080
server app lives at http://localhost:4000
in main.js i'm creating apollo client
const apolloClient = new ApolloClient({
uri: 'http://localhost:4000/v1/graphql',
})
const apolloProvider = new VueApollo({
defaultClient: apolloClient,
})
and feed the apolloProvider variable to Vue.
in component code that's calling API endpoint is looking like this
<template>
<div>{{ categories }}</div>
</template>
<script>
import gql from 'graphql-tag'
export default {
apollo: {
categories: gql`query {
categories {
name
_id
}
}`
}
}
</script>
my GraphQL server that should accept the query from VueApollo is looking like this
// apollo
const { ApolloServer, makeExecutableSchema } = require('apollo-server')
const typeDefs = require('./schema')
const resolvers = require('./resolvers')
const schema = makeExecutableSchema({
typeDefs,
resolvers,
})
const server = new ApolloServer({
schema,
cors: {
origin: 'http://localhost:8080',
methods: 'POST',
optionsSuccessStatus: 204,
preflightContinue: false,
},
})
server.listen({ port: process.env.PORT || 4000 }).then(({ url }) => {
console.log(`🚀 app running at ${url}`)
})
in Chrome browser requests from VueApollo accepted and response returned appropriately, but in FireFox i'm getting a CORS errors like this
am i missing anything guys? please help!
i'm not sure what was wrong, but while i was trying to find out a solution i've noticed that my vue-cli module out of date. for me it was 3.1.1. so i updated vue cli to 4.5.9 and it got worked.

How can I set a cookie with Relay?

I've got express js server code:
...
const server = new GraphQLServer({
typeDefs: `schema.graphql`,
resolvers,
context: context => {
let cookie = get(context, 'request.headers.cookie');
return { ...context, cookie, pubsub };
},
});
such that I can attach cookie to resolvers' requests:
...
method: 'GET',
headers: {
cookie: context.cookie,
},
Now I want to be able to use Relay (as a GraphQL client) and I want to be able to attach a cookie to Relay's requests as well.
I've found a similar question but it's not clear to me where can I insert that code:
Relay.injectNetworkLayer(
new Relay.DefaultNetworkLayer('/graphql', {
credentials: 'same-origin',
})
);
since I don't import Relay in Environment.js.
Update: I tried to add
import { Relay, graphql, QueryRenderer } from 'react-relay';
Relay.injectNetworkLayer(
new Relay.DefaultNetworkLayer('http://example.com/graphql', {
credentials: 'same-origin',
})
);
to a file where I send GraphQL queries (e.g., client.js), but it says that Relay is undefined.
Update #2: this repo looks interesting.

Found #client directives in a query but no ApolloClient resolvers were specified

OS: Windows 10 Pro
apollo-client: 2.6.3
apollo-boost: 0.1.16
Can anyone explain why I'm getting the following error message?:
Found #client directives in a query but no ApolloClient resolvers were
specified. This means ApolloClient local resolver handling has been
disabled, and #client directives will be passed through to your link
chain.
when I've defined my ApolloClient as follows:
return new ApolloClient({
uri: process.env.NODE_ENV === 'development' ? endpoint : prodEndpoint,
request: operation => {
operation.setContext({
fetchOptions: {
credentials: 'include',
},
headers: { cookie: headers && headers.cookie },
});
},
// local data
clientState: {
resolvers: {
Mutation: {
toggleCart(_, variables, { cache }) {
// Read the cartOpen value from the cache
const { cartOpen } = cache.readQuery({
query: LOCAL_STATE_QUERY,
});
// Write the cart State to the opposite
const data = {
data: { cartOpen: !cartOpen },
};
cache.writeData(data);
return data;
},
},
},
defaults: {
cartOpen: false,
},
},
});
From the docs:
If you're interested in integrating local state handling capabilities with Apollo Client < 2.5, please refer to our (now deprecated) apollo-link-state project. As of Apollo Client 2.5, local state handling is baked into the core, which means it is no longer necessary to use apollo-link-state
The clientState config option was only used with apollo-link-state. You need to add the resolvers directly to the config as shown in the docs:
new ApolloClient({
uri: '/graphql',
resolvers: { ... },
})
Also note that there is no defaults option anymore -- the cache should be initialized by calling writeData directly on the cache instance (see here).
I would suggest going through the latest docs and avoiding any examples from external sources (like existing repos or tutorials) since these may be outdated.
Note: As of version 3.0, writeData was removed in favor of writeFragment and writeQuery.

Apollo-client returns "400 (Bad Request) Error" on sending mutation to server

I am currently using the vue-apollo package for Apollo client with VueJs stack with django and graphene-python for my GraphQl API.
I have a simple setup with vue-apollo below:
import Vue from 'vue'
import { ApolloClient } from 'apollo-client'
import { HttpLink } from 'apollo-link-http'
import { InMemoryCache } from 'apollo-cache-inmemory'
import VueApollo from 'vue-apollo'
import Cookies from 'js-cookie'
const httpLink = new HttpLink({
credentials: 'same-origin',
uri: 'http://localhost:8000/api/',
})
// Create the apollo client
const apolloClient = new ApolloClient({
link: httpLink,
cache: new InMemoryCache(),
connectToDevTools: true,
})
export const apolloProvider = new VueApollo({
defaultClient: apolloClient,
})
// Install the vue plugin
Vue.use(VueApollo)
I also have CORS setup on my Django settings.py with the django-cors-headers package. All queries and mutations resolve fine when I use graphiQL or the Insomnia API client for chrome, but trying the mutation below from my vue app:
'''
import gql from "graphql-tag";
import CREATE_USER from "#/graphql/NewUser.gql";
export default {
data() {
return {
test: ""
};
},
methods: {
authenticateUser() {
this.$apollo.mutate({
mutation: CREATE_USER,
variables: {
email: "test#example.com",
password: "pa$$word",
username: "testuser"
}
}).then(data => {
console.log(result)
})
}
}
};
NewUser.gql
mutation createUser($email: String!, $password: String!, $username: String!) {
createUser (username: $name, password: $password, email: $email)
user {
id
username
email
password
}
}
returns with the error response below:
POST http://localhost:8000/api/ 400 (Bad Request)
ApolloError.js?d4ec:37 Uncaught (in promise) Error: Network error: Response not successful: Received status code 400
Regular queries in my vue app, however, work fine resolving the right response, except mutations, so this has me really baffled
400 errors generally mean there's something off with the query itself. In this instance, you've defined (and you're passing in) a variable called $username -- however, your query references it as $name on line 2.
In addition to graphiQL, I would like to add that apollo-link-error package would also had been of great help.
By importing its error handler { onError }, you can obtain great detail through the console about errors produced at network and application(graphql) level :
import { onError } from 'apollo-link-error';
import { ApolloLink } from 'apollo-link';
const errorLink = onError(({ graphQLErrors, networkError }) => {
if (graphQLErrors) {
console.log('graphQLErrors', graphQLErrors);
}
if (networkError) {
console.log('networkError', networkError);
}
});
const httpLink = ...
const link = ApolloLink.from([errorLink, httpLink]);
const client = new ApolloClient({
...,
link,
...
});
By adding this configuration where you instantiate your Apollo Client, you would have obtained an error similar to this one:
GraphQLError{message: "Syntax Error: Expected {, found Name "createUser""}
Further information can be found in Apollo Doc - Error handling: https://www.apollographql.com/docs/react/features/error-handling.
Hope it helps in the future.
For me, it was the fact that I was using a field not defined in the GraphQL schema. Always be careful!
For sure the mutation is not formatted correctly if that is exactly what you are sending. You need an opening bracket in the mutation
mutation createUser($email: String!, $password: String!, $username: String!) {
createUser (username: $name, password: $password, email: $email) {
user {
id
username
email
password
}
}
}
With any of these queries when i run into bugs i paste it into either graphiql or graphql playground to identify what the formatting errors is in order to isolate what is wrong.
For people using laravel for backend, this helped me solve the problem
In the laravel project find file config/cors.php and change line 'paths' => ['api/*', 'sanctum/csrf-cookie'], to 'paths' => ['api/*', 'graphql', 'sanctum/csrf-cookie'],
Also in your vue app ensure that you're not using the no-cors mode in apollo config
Regards