I'm having trouble with GraphQL subscriptions in Apollo. I want to subscribe to added "perspectives" on topics (basically added comments on posts), and I'm pretty sure I have the server set up correctly. The client is what's giving me trouble. (If this question looks familiar, I asked it before and thought I got an answer, but no go). Here is my subscription schema:
type Subscription {
perspectiveAdded: Perspective
}
schema {
query: RootQuery
mutation: Mutation
subscription: Subscription
}
My subscription resolver:
Subscription: {
perspectiveAdded(perspective) {
return perspective;
}
}
My subscriptionManager:
const pubsub = new PubSub();
const subscriptionManager = new SubscriptionManager({
schema,
pubsub,
setupFunctions: {
perspectiveAdded: (options, args) => {
perspectiveAdded: {
filter: (topic) => {
return topic
}
}
},
}
});
export { subscriptionManager, pubsub };
The last part of my addPerspective mutation that is (the event trigger for the subscription):
//...
return perspective.save((error, perspective) => {
if(error){
console.log(error);
}
//Publish it to Subscription channel
pubsub.publish('perspectiveAdded', perspective);
});
And then I've wired up the actual server to support subscriptions:
const PORT = process.env.PORT || 4000;
const server = createServer(app);
server.listen(PORT, ()=>{
new SubscriptionServer(
{
subscriptionManager: subscriptionManager,
onConnect: (connectionParams, webSocket) => {
console.log('Websocket connection established Lord Commander');
},
onSubscribe: (message, params, webSocket) => {
console.log("The client has been subscribed, Lord Commander", message, params);
},
onUnsubsribe: (webSocket) => {
console.log("Now unsubscribed, Lord Commander");
},
onDisconnect: (webSocket) => {
console.log('Now disconnected, Lord Commander');
}
},
{
server: server,
path: '/subscriptions',
});
console.log('Server is hot my Lord Commander!');
});
I've wired up the client correctly as well, because in my terminal I see the "Websocket connection established" message. The part I'm stumped about is how to actually call the subscription. According to the Apollo blog, I should be able to test the subscription in GraphiQL (since I'm using an apollo server, now graphql-server-express), but it says "Resolve function for \"Subscription.perspectiveAdded\" returned undefined".
For my component, I've tried to wire up 'subscribeToMore' but in the browser console, I'm getting an error object that says "Invalid params returned from onSubscribe! return values must be an object!" I'm not sure which object it is referring to.
Here's my subscription query called perspectiveSubscription:
export default gql`
subscription {
perspectiveAdded {
id
content
}
}
`;
And the wired up component:
constructor(props){
super(props);
this.state = {};
this.subscription = null;
}
componentWillReceiveProps(nextProps) {
if (!this.subscription && !nextProps.data.loading) {
let { subscribeToMore } = this.props.data
this.subscription = subscribeToMore(
{
document: perspectiveSubscription,
updateQuery: (previousResult, { subscriptionData }) => {
if(!subscriptionData.data){
console.log('no new subscription data');
return previousResult;
}
const newPerspective = subscriptionData.data.perspectiveAdded;
console.log(newPerspective);
return Object.assign({}, previousResult, newPerspective);
}
}
)
}
From here, I get a message in my terminal saying the client has been subscribed, but still I get the error object mentioned above. I've been pulling my hair out about this for days - do you guys see what I am missing here? Specifically, any ideas on the client side? Thanks everyone!
It seems like the server side is not correct, because the subscription is added and graphiql also does not deliver a correct result.
One thing that i suggest is that you check the channel definition:
const pubsub = new PubSub();
const subscriptionManager = new SubscriptionManager({
schema,
pubsub,
setupFunctions: {
perspectiveAdded: (options, args) => {
perspectiveAdded: {
filter: (perspective) => {
console.log(perspective); // check if object is correct
return true; // return true and not the object as long as you do not want to filter
}
}
},
}
});
export { subscriptionManager, pubsub };
And also check if the perspective object is saved and defined before the pubsub call.
And i think you also want to add a comment id for which the subscription should be working. On my side it looks more or less like in this post
Related
I am writing a CRUD app with React Query and I created some custom hooks as described here: https://react-query.tanstack.com/examples/custom-hooks
In the docs I see that there are basically two ways to update the cache after a mutation:
Query invalidation (https://react-query.tanstack.com/guides/query-invalidation)
onSuccess: () => {
queryClient.invalidateQueries("posts");
}
Updating the cache manually (https://react-query.tanstack.com/guides/invalidations-from-mutations)
// Update post example
// I get the updated post data for onSuccess
onSuccess: (data) => {
queryClient.setQueryData("posts", (oldData) => {
const index = oldData.findIndex((post) => post.id === data.id);
if (index > -1) {
return [
...oldData.slice(0, index),
data,
...oldData.slice(index + 1),
];
}
});
},
I understand that manual update has the advantage of not doing an extra call for fetching the 'posts', but I wonder if there is any advantage of invalidating cache over the manual update. For example:
import { useMutation, useQueryClient } from "react-query";
const { API_URL } = process.env;
const createPost = async (payload) => {
const options = {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(payload),
};
if (API_URL) {
try {
const response = await fetch(API_URL, options);
if (!response.ok) {
throw new Error(response.statusText);
}
return response.json();
} catch (error) {
throw new Error(error);
}
} else {
throw new Error("No api url is set");
}
};
export default function useCreatePost() {
const queryClient = useQueryClient();
return useMutation((payload) => createPost(payload), {
// DOES INVALIDATING HAVE ANY ADVANTAGE OVER MANUAL UPDATE IN THIS CASE?
// onSuccess: () => {
// queryClient.invalidateQueries("posts");
// },
onSuccess: (data) => {
queryClient.setQueryData("posts", (oldData) => {
return [...oldData, data];
});
},
});
}
Thanks for your time!
As you state it yourself, the only advantage is that you don't waste another network call to update data we already have.
Here we have a create and delete example.
import { useMutation, useQueryClient } from 'react-query'
const queryClient = useQueryClient()
// createPost(post: PostT) {
// const { data } = await http.post<{ post: PostT >('/posts', { post });
// return data.post;
// }
const mutation = useMutation(createPost, {
onSuccess: (post) => {
queryClient.setQueryData<PostT[]>(['posts'], (oldData || []) => [ ...oldData, post])
},
})
// deletePost(id: string) {
// await http.delete(`/posts/${id}`);
// }
const mutation = useMutation(deletePost, {
onSuccess: (_, id) => {
queryClient.setQueryData<PostT[]>(['posts'], (oldData || []) => oldData.filter((post) => id !== post.id)
},
})
Invalidating the query can also be an option is some cases. The query will be invalidated and the data will be marked as stale. This will trigger a refetching in the background. So you know for a fact that the data will be as fresh as possible.
This can be handy if you got:
multiple queries to update with data from a mutation
have a (difficult) nested data structure to update
import { useMutation, useQueryClient } from 'react-query'
const queryClient = useQueryClient()
const mutation = useMutation(createPost, {
onSuccess: () => {
queryClient.invalidateQueries('posts')
queryClient.invalidateQueries('meta')
queryClient.invalidateQueries('headers')
},
})
But it really is up to you.
The main advantage of using manual updates comes from the fact that you can do it before the request is sent to the server; so if you manually update after the request is successful, then there's not much of an advantage if the data that you get from the server doesn't need to be immediately present to the user & in those cases (which I have found to be the majority) you better off invalidating. when you use optimistic updates, you assume the request is successful before you send it to server & then if the request fails you just roll back your update. this way your action happens instantly which is a better UX than doing the action, showing a loading spinner or something & then showing the updated state. so I have found it more useful for giving instantaneous feedback to the user than saving an extra request to the server. in most cases (as in yours) you still need to invalidate the query after, because your manually added post doesn't have an id, so you should sync it with the list of posts from the server. so be very careful about that because if you reading from that id somewhere else in that page, it would be undefined & would throw an error. so at the end of the day your mutation is not a great candidate for optimistic update & you should be careful to handle all the problems that can come up with your posts value having a post with no id in it (as opposed to something like a follow action which is just changing a boolean value in your database & you can confidently mutate the cache & undo it if request was not successful). so if we assume that you can handle that problem your useMutation hook would be something like this:
return useMutation(
(payload) => {
queryClient.setQueryData("posts", (oldData) => {
return [...oldData, payload];
});
return createPost(payload);
},
{
onSettled: () => {
queryClient.invalidateQueries("posts");
},
}
);
This is my first time using useSub and I noticed that although my backend sends it's responses to the client (at least the console.log shows it is). The client using useSub doesn't do anything. I usually use subscribeToMore with query, but for this job I want to only get the most updated info. Is there a way to check if useSub connects correctly? Or is it broken in "#apollo/react-hooks": "^3.1.3"
Query
export const INCOMING_VIDEO_CHAT = gql`
subscription {
incomingVideoChat {
rn
p
}
}
`;
Client
const { data, loading } = useSubscription(INCOMING\_VIDEO\_CHAT, {
onSubscriptionData: ({ subscriptionData }) => {
console.log(subscriptionData);
}
});
Server:
module.exports = {
type: chatInfoType,
subscribe: () => pubsub.asyncIterator(INCOMING\_VIDEO\_CHAT),
async resolve(payload, { }, req) {
if (auth.isAuthenticated(req)) {
if (!payload) {
return;
}
const { userID, rn, p } = payload;
try {
if (req.id === userID) {
return { rn, p };
} else {
return;
}
} catch (e) {
throw new Error(e);
}
}
}
};
You can use Chrome Dev Tools to check if useSubscription connects correctly. In the Network tab of the Chrome DevTools, you should switch the filter to WS to see and debug your Apollo GraphQL subscription. You should read it.
I have followed the subscription tutorial on How to GraphQL React + Relay (https://relay.dev/docs/en/subscriptions) but still not working.
I'm using Relay Modern in my app and have successfully integrated query but not working the requestSubscription function.
Any help would be awesome.
My environment.js file:
function setupSubscription(
config,
variables,
cacheConfig,
observer,
) {
const query = config.text
const subscriptionClient = new SubscriptionClient('ws://192.168.1.19:8000/subscriptions', {reconnect: true});
const id = subscriptionClient.on({query, variables}, (error, result) => {
console.log(result,'result');
observer.onNext({data: result})
})
}
const network = Network.create(fetchQuery, setupSubscription)
const environment = new Environment({
network,
store
});
export default environment;
- My Subscription.js file:
const subscription = graphql`
subscription newVoteSubscription {
leaderboardUpdate {
id,
game_id,
event_id,
colarr,
rowarr
}
}
`;
function newVoteSubscription(callback) {
const variables = {};
return requestSubscription(environment, {
subscription: subscription,
variables: variables,
onError: (error)=> {
console.log(error, "error");
},
onNext: (res) => {
console.log(res,'onNext');
// callback();
},
updater: proxyStore => {
console.log(proxyStore,'proxyStore');
},
onCompleted: () => {
console.log('test');
},
});
}
export default newVoteSubscription;
I had trouble with the network as well. On Relay 7 using an Observable worked for me. This also handles error cases and the server closing the subscription.
const subscriptionClient = new SubscriptionClient('ws://192.168.1.19:8000/subscriptions', {reconnect: true})
function setupSubscription(
request,
variables,
cacheConfig,
) {
const query = request.text;
// Observable is imported from the relay-runtime package
return Observable.create(sink => {
const c = subscriptionClient.request({ query, variables }).subscribe(sink);
return c.unsubscribe;
});
}
I'm not sure why i've gone with the sink / unsubscribe approach, but this is what worked for me. As far as i remember the observable types used by relay and subscriptions-transport-ws were not compatible.
Also i'd advise you to hoist the new SubscriptionClient() call outside of the setupSubscription function as otherwise you'll open a new WebSocket for each subscription request.
I got the response, but now observer.onNext is undefined.
My updated code environment.js:
const setupSubscription = (config, variables, cacheConfig, observer) => {
const query = config.text
const subscriptionClient = new SubscriptionClient('ws://192.168.1.19:8000/subscriptions', {reconnect: true})
subscriptionClient.request({ query, variables }).subscribe((result) => {
observer.onNext({data: result})
});
return Observable.create(() => {
return subscriptionClient;
});
}
const environment = new Environment({
network: Network.create(fetchQuery, setupSubscription),
store: new Store(new RecordSource())
});
Let's say I have a Post and Comment model. A Post hasMany Comments. In React Apollo I'm using subscribeToMore on a Query for a particular post.
The query appears as follows:
query getPost(id: ID!){
id, title, comments { id }
}
And the subscription which returns the post with any new comments:
subscription commentAdded(postId: ID!){
id, title, comments { id }
}
The query works. It returns all of the associated Comments, which I can then render as in list on the page.
Yet when using the subscribeToMore helper for the query, I get the follow error whenever the event subscription is dispatched.
Cannot read property 'Comment' of undefined.
The strange thing is that if I remove the Comment, such that the subscription looks like...
subscription commentAdded(postId: ID!){
id, title
}
...it works perfectly. I'm confused why it seems to treat the Comments as associating with an undefined model.
This isn't just a Comments -> Posts issue, this happens on any model that tries to return a subscription with an association.
post query:
post: async (parent, {id}, {models}) => {
return await models.Post.findByPk(id);
}
saveComment resolver:
saveComment: async (parent, {postId, comment}, {models, me}) => {
let post = await models.Post.findByPk(postId);
let comment = await models.Comment.create({comment, postId});
await pubsub.publish("COMMENT_CREATED", {
commentCreated: post,
postId
})
}
commentCreated subscription:
commentCreated: {
subscribe: withFilter(
() => pubsub.asyncIterator(["COMMENT_CREATED"]),
(payload, variables) => {
return payload.postId == variables.postId
}
)
}
Post type resolver
Post: {
comments: async (post, args, {models}) => {
return await models.Comment.findAll({where:{postId: post.id}});
}
}
Server initialization:
const server = new ApolloServer({
typeDefs: schema,
resolvers,
subscriptions: {
onConnect: (connectionParams, webSocket) => {
return true
},
},
context: async ({ req, connection }) => {
if(connection){
return connection.context;
}else{
const me = await getMe(req);
return {
models,
me,
secret: process.env.SECRET,
};
}
}
});
Your context function only returns connection.context, which will not include any of the custom properties you want to include (me, models, etc.). Doing something like this should fix the problem:
context: async ({ req, connection }) => {
const context = {
models,
secret: process.env.SECRET,
};
if(connection){
return { ...connection.context, ...context };
} else {
const me = await getMe(req);
return { ...context, me };
}
}
What I'm you doing?:
I would like to thrown an error on apollo-server and process it on client.
Note: I'm using apollo-link-error middleware on apollo-client.
Server:
import { UserInputError } from "apollo-server";
Mutation: {
someMutation : {
try {
// here is some code which failed
} catch (error) {
// base Error class has message property by default
// response just hold some additional informations
throw new UserInputError(error.message, { response });
}
}
}
Client:
simplified implementation of my mutation on client
<Mutation
mutation={CREATE_ORDER}
>
{(createOrder, { loading, error }) => (
....
try {
await createOrder({ variables: {...}});
} catch (createOrderError) {
// here I get Cannot read property 'data' of undefined
console.log(createOrderError);
}
)}
</Mutation>
I receive following error on client (in catch clause in the code above):
TypeError: Cannot read property 'data' of undefined
at Mutation._this.onMutationCompleted (react-apollo.browser.umd.js:631)
at react-apollo.browser.umd.js:586
This error looks like problem with httpLink.
Response: (from network tab in chrome dev tools)
From graphql spec :
If an error was encountered during the execution that prevented a
valid response, the data entry in the response should be null.
So I assume that my response from server is valid. The data object should be null.
What do I expect to happen?:
I would like to access to response from apollo server How could I achieve this?
Thanks for help!
Things to check before accessing the data returned from query or mutation
If loading -> return some loader component
If error is present -> display some error component
If not any of above two conditions matched then for sure you have the data.
Apart from that you need to have
1.On Apollo Client "errorPolicy"
const client = new ApolloClient({
defaultOptions: {
watchQuery: {
errorPolicy: 'all'
},
query: {
errorPolicy: 'all'
},
mutate: {
errorPolicy: 'all'
}
},
link,
cache,
connectToDevTools: true,
})
2.For customising error sent from server -
You can use formatError
const server = new ApolloServer({
...root,
resolverValidationOptions: {
requireResolversForResolveType: false,
},
formatError, <---------- send custom error
formatResponse: (response, query) => formatResponse({ response, query }),
dataSources,
context: async ({ req, res }) => {
const user = req.user;
return { user, req, res };
}
});
e.g
const formatError = (error) => {
const { extensions } = error;
logger.error(error);
const exception = extensions.exception ? extensions.exception : {};
logger.error('\nStackTrace');
logger.error(exception.stacktrace);
exception.stacktrace = null;
const extractedError = extractErrorFromExtention(extensions);
return extractedError || { message: error.message, code: extensions.code, exception };
};