GraphQL subscriptions: Error on calling apolloClient.subscribe - graphql

I think I have the backend subscription setup correctly. I am using angular on the client side, when I try to call subscribe I got an error
passwordUpdatedSubscription = gql`
subscription passwordUpdated{passwordUpdated{name password}}
`;
// Apollo Subscription
var subscription = this.apollo.subscribe({
query: this.passwordUpdatedSubscription
});
subscription.subscribe(
{
next(data) {
console.log(data);
},
error(err) { console.error('err', err); },
}
);
And then this is the error appears in the console
{"type":"subscription_fail","id":0,"payload":{"errors":[{"message":"Cannot read property 'subscribe' of undefined"}]}}
Maybe I am missing something on the backend? Do I need to define the setupFunctions in the SubscriptionManager?
This is my SubscriptionManager
const sub = require('graphql-subscriptions');
const pubSub = new sub.PubSub();
const manager = new sub.SubscriptionManager({
schema,
pubSub
});
This is my schema in graphQL
const graphql = require('graphql');
var schema = graphql.buildSchema(`
type Subscription {
passwordUpdated: User
}
type Mutation {
setMessage(message: String): String,
updateUserPassword(userName: String, password: String): User!
}
type Query {
getMessage: String,
getUsers: [User],
findUsers(userName: String): [User]
}
type User {
name: String,
password: String
}
`);

Yes you are missing the setup function. You could take a look at this links graphql subscription docu or example.
Your subscription manager could look like this:
const manager = new sub.SubscriptionManager({
schema,
pubSub,
setupFunctions: {
passwordUpdated: (options, args) => ({ // name of your graphQL subscription
passwordUpdatedChannel: { // name of your pubsub publish-tag
filter: () => {
return true
},
},
}),
},
});
When you call the pubsub publish function you have to write it like this pubsub.publish("passwordUpdatedChannel").
Sidenode: You might want to add the id of the user that has the password changed to the subscription. If you do that you can add it to the filter option, could look like this filter: (user) => {return user.id === args.userId}

Related

Pubsub publish multiple events Apollo Server

I am using Apollo Server and I want to publish 2 events in the row from same resolver. Both subscriptions are working fine but only if I dispatch only one event. If I try to dispatch both, second subscription resolver never gets called. If I comment out the first event dispatch second works normally.
const publishMessageNotification = async (message, me, action) => {
const notification = await models.Notification.create({
ownerId: message.userId,
messageId: message.id,
userId: me.id,
action,
});
// if I comment out this one, second pubsub.publish starts firing
pubsub.publish(EVENTS.NOTIFICATION.CREATED, {
notificationCreated: { notification },
});
const unseenNotificationsCount = await models.Notification.find({
ownerId: notification.ownerId,
isSeen: false,
}).countDocuments();
console.log('unseenNotificationsCount', unseenNotificationsCount);// logs correct value
// this one is not working if first one is present
pubsub.publish(EVENTS.NOTIFICATION.NOT_SEEN_UPDATED, {
notSeenUpdated: unseenNotificationsCount,
});
};
I am using default pubsub implementation. There are no errors in the console.
import { PubSub } from 'apollo-server';
import * as MESSAGE_EVENTS from './message';
import * as NOTIFICATION_EVENTS from './notification';
export const EVENTS = {
MESSAGE: MESSAGE_EVENTS,
NOTIFICATION: NOTIFICATION_EVENTS,
};
export default new PubSub();
Make sure, that you use pubsub from context of apollo server, for example:
Server:
const server = new ApolloServer({
schema: schemaWithMiddleware,
subscriptions: {
path: PATH,
...subscriptionOptions,
},
context: http => ({
http,
pubsub,
redisCache,
}),
engine: {
apiKey: ENGINE_API_KEY,
schemaTag: process.env.NODE_ENV,
},
playground: process.env.NODE_ENV === 'DEV',
tracing: process.env.NODE_ENV === 'DEV',
debug: process.env.NODE_ENV === 'DEV',
});
and example use in resolver, by context:
...
const Mutation = {
async createOrder(parent, { input }, context) {
...
try {
...
context.pubsub.publish(CHANNEL_NAME, {
newMessage: {
messageCount: 0,
},
participants,
});
dialog.lastMessage = `{ "orderID": ${parentID}, "text": "created" }`;
context.pubsub.publish(NOTIFICATION_CHANNEL_NAME, {
notification: { messageCount: 0, dialogID: dialog.id },
participants,
});
...
}
return result;
} catch (err) {
log.error(err);
return sendError(err);
}
},
};
...
It has been a while since this moment.
I have also been a struggle with pubsub not working problem.
and I would like to see your ApolloClient setup code.
I changed my configurations with regard to graphql version and client-side setup.
graphql version : 14.xx.xx -> 15.3.0
const client = new ApolloClient({
uri: 'http://localhost:8001/graphql',
cache: cache,
credentials: 'include',
link: ApolloLink.from([wsLink, httpLink])
});
I want you to clarify link order, especially about httpLink, if you use in your case, "HttpLink is a terminating Link.", according to Apollo official site.
At first, I used link order [httpLink, wsLink].
Therefore, pubsub.publish didn't work.
I hope this answer will help some of graphql users.

Query variables not being passed down from vue component in apollo

I have a simple query which takes in an ID parameter, but it is not working. It says "TypeError: Cannot read property 'taskId' of undefined" . So I think it does not recognize the 'this' keyword for some reason.
Please take a look:
Apollo query from frontend component:
getCommentsByTask: {
query: GET_COMMENTS_BY_TASK,
variables: {
taskId: this.taskId
},
result({ data }) {
this.getComments = data;
console.log("data", data);
}
}
Defined the query in frontend:
query GET_COMMENTS_BY_TASK($taskId: ID!) {
getCommentsByTask(taskId: $taskId) {
id
parentId
ownerId
text
}
}
Resolver in server:
async getCommentsByTask (_, {taskId}, context) {
const userId = getUserId(context)
const user = await User.findById(userId)
if (!user) return
const comments = await Comment.findById(taskId)
return comments
}
Schema:
type Query {
getCommentsByTask(taskId: ID!): [Comment]
}
Assuming that's a smart query, variables should be a (regular, non-arrow) function if you need access to this.

TypeORM - Retrieve data through relation query in resolver

So I'm trying to retrieve elements out of a join relation in an Apollo resolver.
I've got a User table, a Notification table. And a Relation table named:
notification_recipients_user defined by a ManyToMany relation on both entity but hosted on Notification :
//entity/Notification.ts
#ManyToMany(type => User, recipient => recipient.notifications)
#JoinTable()
recipients: User[];
I can create relation without problem through this mutation :
addNotificationForUser: async (_, { id, data }) => {
const user = await User.findOne({ id });
if (user) {
const notification = await Notification.create({
template: data,
recipients: [user]
}).save()
return notification;
} else {
throw new Error("User does not exists!");
}
}
However I'm totally not succeeding in retrieving data for a specific User.
allNotificationsOfUser: (_, { user }, ___) => {
return Notification.find({
where: { userId: user },
relations: ['recipients'],
})
The method find is one of TypeORM native methods.
However I must be doing something wrong because it react as if there wasn't any filter.
Okay so the best way of doing it is by using relation between entity.
So to get Notification you'll go by User.notifications
allUnseenNotificationsOfUser: async (_, { userId }, __) => {
const user = await User.findOne({
relations: ["notifications"],
where: { id: userId }
});
if (user) {
const notifications = user.notifications.filter(notification => !notification.seen)
return notifications;
}
return null;
}
And for the record for anyone stumbeling upon this you can use filter on your result to do a query like resolver.
const notifications = user.notifications.filter(notification => !notification.seen)
It feels tricky but works like a charm.

apollo client 2.0 graphql react auth

I'm new to apollo/graphql and I'm trying to get my authentication done properly in a greenfield project. My authentication provider is AWS cognito. I wrote a cognito helper module to interact with it.
Though I'm not quite sure how to sync my apollo client with my auth state.
export const authenticate = (username: string, password: string) => {
const authDetails = new AuthenticationDetails({
Username: username,
Password: password,
})
const cognitoUser = getCognitoUser(username)
return new Promise((resolve, reject) => {
cognitoUser.authenticateUser(authDetails, {
onSuccess: result => {
resolve(result)
},
onFailure: err => {
reject(err)
},
})
})
}
export const getCurrentUserToken = () => {
return new Promise((resolve, reject) => {
const currentUser = userPool.getCurrentUser()
if (currentUser) {
currentUser.getSession((error, session) => {
if (error) {
reject(error)
}
resolve(session.getIdToken().getJwtToken())
})
} else {
resolve(null)
}
})
}
export const logout = () => {
const currentUser = userPool.getCurrentUser()
if (currentUser) {
currentUser.signOut()
}
}
Right now I'm just using these function to handle my login by calling them in my react component handlers. I configured an apollo-link for adding the auth header. Inject my JWT token data into context at the backend and implemented a currentUser query resolver in the backend.
const resolvers = {
RootQuery: {
currentUser: (obj, args, context) =>
context.tokenData
? {
id: context.tokenData.sub,
name: context.tokenData.name,
email: context.tokenData.email,
username: context.tokenData['cognito:username'],
}
: null,
},
}
In my react App layout i got a component UserPanel which queries that currentUser query.
const CURRENT_USER_QUERY = gql`
query {
currentUser {
name
}
}
`
export default graphql(CURRENT_USER_QUERY)(UserPanel)
When i am logging in now obviously the UserPanel does not update its currentUser query except I'm reloading the page ofc. Though im also having troubles finding a good solution to sync them.
I was thinking about implementing my login via graphql mutation using apollo-link-state to do it locally and watch these to refetch if someone logged in/out. I'm not sure if this is fine since it seems to me that this link cannot resolve async stuff (e.g. promises) in its mutation resolvers.
Another option I was thinking about was to decouple the auth process from the apollo client completely and implement some auth pubsub system maybe with Observables and let the react components refetch the queries if the authentication state changes.
I'm very uncertain how to continue and every solution I'm thinking about doesn't feel like the recommended way to go.
I don't have the full picture with regards to your React setup but here I go. It might be that Apollo-client is caching CURRENT_USER_QUERY locally and is showing you the results of a previous query. You could try the network-only option on the query:
export default graphql(CURRENT_USER_QUERY, { options: {fetchPolicy: 'network-only' }})(UserPanel)
What I have in React is an AppContainer which is my parent component. It checks if the user is logged in:
const loggedInUser = gql`
query loggedInUser{
user {
id
role
}
}`
export default graphql(loggedInUser, { options: {fetchPolicy: 'network-only' }})(AppContainer)
Then on my UserProfile page, I use a data container to fetch the data before passing it down to the UserProfile child component. I think the loggedInUser query automatically updates the user in the apollo store. With it apollo-client realizes that it needs to refetch userQuery. Does that help?
const userQuery = gql`
query userQuery {
user {
id
name
email
role
company
}
}
`
export default graphql(userQuery, {name: 'userQuery'})(UserDataContainer);

Relay commitUpdate callback with follow-up mutation and missing fragment

I have two GraphQL/Relay mutations that work fine separately. The first one creates an item. The second one runs a procedure for connecting two items.
GraphQL
createOrganization(
input: CreateOrganizationInput!
): CreateOrganizationPayload
createOrganizationMember(
input: CreateOrganizationMemberInput!
): CreateOrganizationMemberPayload
input CreateOrganizationInput {
clientMutationId: String
organization: OrganizationInput!
}
input CreateOrganizationMemberInput {
clientMutationId: String
organizationMember: OrganizationMemberInput!
}
# Represents a user’s membership in an organization.
input OrganizationMemberInput {
# The organization which the user is a part of.
organizationId: Uuid!
# The user who is a member of the given organization.
memberId: Uuid!
}
type CreateOrganizationPayload {
clientMutationId: String
# The `Organization` that was created by this mutation.
organization: Organization
# An edge for our `Organization`. May be used by Relay 1.
organizationEdge(
orderBy: OrganizationsOrderBy = PRIMARY_KEY_ASC
): OrganizationsEdge
# Our root query field type. Allows us to run any query from our mutation payload.
query: Query
}
I would like to be able to run the createOrganization mutation and then connect the user to the organization with the createOrganizationMember mutation. The second mutation takes two arguments, one of which is the newly created edge.
I tried passing the edge into the mutation, but it expects the mutation to be able to getFragment. How can I get the fragment for the payload edge so it can be passed into a mutation?
React-Relay
Relay.Store.commitUpdate(
new CreateOrganizationMutation({
organizationData: data,
user,
query,
}), {
onSuccess: response => {
Relay.Store.commitUpdate(
new CreateOrganizationMemberMutation({
organization: response.createOrganization.organizationEdge.node,
user,
})
);
},
}
);
fragments: {
user: () => Relay.QL`
fragment on User {
${CreateOrganizationMutation.getFragment('user')},
${CreateOrganizationMemberMutation.getFragment('user')},
}
`,
I solved this problem without changing any GraphQL:
I created a new Relay container, route, and queries object. It is configured as a
child route for the container where the first of two mutation occurs. The id for
the new edge is passed as a parameter via the route pathname. A router state
variable is also passed.
Routes
import {Route} from 'react-router';
function prepareProfileParams (params, {location}) {
return {
...params,
userId: localStorage.getItem('user_uuid'),
};
}
// ProfileContainer has the component CreateOrganizationForm, which calls
// the createOrganization mutation
<Route
path={'profile'}
component={ProfileContainer}
queries={ProfileQueries}
prepareParams={prepareProfileParams}
onEnter={loginBouncer}
renderLoading={renderLoading}
>
<Route path={'join-organization'}>
<Route
path={':organizationId'}
component={JoinOrganizationContainer}
queries={JoinOrganizationQueries}
renderLoading={renderLoading}
/>
</Route>
</Route>
CreateOrganizationForm.js
Relay.Store.commitUpdate(
new CreateOrganizationMutation({
organizationData: data,
user,
query,
}), {
onSuccess: response => {
const organizationId = response.createOrganization.organizationEdge.node.rowId;
router.push({
pathname: `/profile/join-organization/${organizationId}`,
state: {
isAdmin: true,
},
});
},
}
);
The new Relay container JoinOrganizationContainer will hook into a lifecycle
method to call the second mutation that we needed. The second mutation has an
onSuccess callback which does router.push to the page for the new object we
created with the first mutation.
JoinOrganizationContainer.js
import React from 'react';
import Relay from 'react-relay';
import CreateOrganizationMemberMutation from './mutations/CreateOrganizationMemberMutation';
class JoinOrganizationContainer extends React.Component {
static propTypes = {
user: React.PropTypes.object,
organization: React.PropTypes.object,
};
static contextTypes = {
router: React.PropTypes.object,
location: React.PropTypes.object,
};
componentWillMount () {
const {user, organization} = this.props;
const {router, location} = this.context;
Relay.Store.commitUpdate(
new CreateOrganizationMemberMutation({
user,
organization,
isAdmin: location.state.isAdmin,
}), {
onSuccess: response => {
router.replace(`/organization/${organization.id}`);
},
}
);
}
render () {
console.log('Joining organization...');
return null;
}
}
export default Relay.createContainer(JoinOrganizationContainer, {
initialVariables: {
userId: null,
organizationId: null,
},
fragments: {
user: () => Relay.QL`
fragment on User {
${CreateOrganizationMemberMutation.getFragment('user')},
}
`,
organization: () => Relay.QL`
fragment on Organization {
id,
${CreateOrganizationMemberMutation.getFragment('organization')},
}
`,
},
});
JoinOrganizationQueries.js
import Relay from 'react-relay';
export default {
user: () => Relay.QL`
query { userByRowId(rowId: $userId) }
`,
organization: () => Relay.QL`
query { organizationByRowId(rowId: $organizationId) }
`,
};
One unexpected benefit of doing things this way is that there is now a shareable url that can be used as an invite link for joining an organization in this app. If the user is logged in and goes to the link: <host>/profile/join-organization/<organizationRowId>, the mutation will run that joins the person as a member. In this use case, router.state.isAdmin is false, so the new membership will be disabled as an admin.

Resources