I've followed Apollo's docs for setting up GraphQL subscriptions on both the client and server, and though I'm 90% there, I can't figure out how to set up subscription channels and how to connect mutations to those channels so that whenever a mutation occurs, the server pushes the new data to the client. (For content, I'm making a Reddit clone where people post topics and others comment on it. So when you see "Topics" or "TopicList," think of those as posts.)
So far, I have set up Apollo Client for subscriptions successfully:
const wsClient = new SubscriptionClient('ws://localhost:3001/subscriptions', {
reconnect: true
});
const networkInterface = createNetworkInterface({
uri: '/graphql',
opts: {
credentials: 'same-origin'
}
});
const networkInterfaceWithSubscriptions = addGraphQLSubscriptions(
networkInterface,
wsClient,
);
const client = new ApolloClient({
networkInterface: networkInterfaceWithSubscriptions,
dataIdFromObject: o => o.id
});
And I've set up my back-end for subscriptions as well. Here's my server.js file:
//===========================================================
//Subscription Managaer
//===========================================================
const pubsub = new PubSub();
const subscriptionManager = new SubscriptionManager({
schema: schema,
pubsub: pubsub
});
//=====================================
//WebSocket + Express Server
//=====================================
const server = createServer(app);
//setup listening port
server.listen(3001, ()=>{
new SubscriptionServer(
{
subscriptionManager: subscriptionManager,
onConnect: (connectionParams, webSocket) => {
console.log('Websocket connection established');
},
onSubscribe: (message, params, webSocket) => {
console.log("The client has been subscribed", message, params);
},
onUnsubsribe: (webSocket) => {
console.log("Now unsubscribed");
},
onDisconnect: (webSocket) => {
console.log('Now disconnected');
}
},
{
server: server,
path: '/subscriptions',
});
console.log('Server is hot my man!');
})
I know these are successful, because I get the "Websocket connection established" message logged in my terminal.
Next is the actual subscription - I've created a subscription schema type (just like queries and mutations):
const SubscriptionType = new GraphQLObjectType({
name: 'Subscription',
fields: () => ({
topicAdded: {
type: TopicType,
args: {repoFullName: {type: GraphQLString}}, //I don't understand what repoFullName is - I was trying to follow the Apollo docs, but they never specified that
resolve(parentValue, args){
return parentValue;
}
}
})
});
module.exports = SubscriptionType;
and incorporated it into my root schema. So when I check out GraphiQL, I see: this subscription available in the Docs side menu
My GraphiQIL UI showing the subscriptionSchema successfully
In my React component, I successfully 'subscribe' to it using Apollo's subscribeToMore method:
const TOPICS_SUBSCRIPTION = gql`
subscription OnTopicAdded($repoFullName: String){
topicAdded(repoFullName: $repoFullName){
id
}
}
`;
class TopicList extends Component {
componentDidMount() {
this.createMessageSubscription = this.props.data.subscribeToMore({
document: TOPICS_SUBSCRIPTION,
// updateQuery: (previousState, {subscriptionData}) => {
// const newTopic = subscriptionData.data.Topic.node
// const topics = previousState.findTopics.concat([newTopic])
// return {
// findTopics: topics
// }
// },
onError: (err) => console.error(err)
})
} //...
And I get my "The client has been subscribed" message logged into my terminal. But this is where I'm stuck. I've read about the SetupFunction for the SubscriptionManager, but that isn't included in Apollo's docs. And I can't find how to map a 'createTopic' mutation to this subscription so that whenever someone adds a new topic, it pops up in TopicList.
I realize this is really long, but I've been pulling my hair out tyring to figure out what's the next step. Any help would be much appreciated!! Thank you for reading!
Yes you are missing the setup function. You could take a look at this links GraphQL subscription docu or this example.
How it should work:
First you need the channel on which you publish the changed data. In your case it could look like this:
const manager = new sub.SubscriptionManager({
schema,
pubSub,
setupFunctions: {
topicAdded: (options, args) => ({ // name of your graphQL subscription
topicAddedChannel: { // name of your pubsub publish-tag
filter: (topic) => {
console.log(topic); //should always show you the new topic if there is a subscribed client
return true; // You might want to add a filter. Maybe for guest users or so
},
},
}),
},
});
And here you see the need of the args: {repoFullName: {type: GraphQLString}} argument in the subscription. If you want to filter the subscription dependent on the "repoName". Meaning that only the client with a subscription with the "repoName" as argument gets an update.
Next you need a place where you call the pubsub.publish function. In your case after the add topic mutation has passed. Could look like this:
...
const topic = new Topic(/* args */);
topic.save((error, topic) => {
if (!error) {
pubsub.publish("topicAddedChannel", topic);
}
...
});
....
Related
In Next.js we have a feature of dynamic routing configuration on getStaticPaths method. I need to fetch the data from database based on that data, I need to set dynamic routes. But when ever I try to make a database call, I am getting an error mentioned below.
export async function getStaticPaths() {
const client = await MongoClient.connect('connection string');
const dataBase = client.db();
const collections = dataBase.collection('table name');
let result = await collections .find({}, { id: 1 }).toArray();
client.close();
let paths = result.map((data) => ({
params: { id: data.id.toString() }
}));
console.log(paths);
return {
fallback: false,
paths: paths
};
}
error that I can see,
./node_modules/mongodb/lib/url_parser.js:7:0
Module not found: Can't resolve 'dns'
Please help. Thanks in advance.
I'm trying to set up a basic graphQL subscription to update a list of messages whenever one is crated, based off the recipes in Nader Dabit’s book and in this Medium post, and my subscription is just never firing in the client. What's strange is that in the query editor in Amplify’s Admin UI, the subscription fires as expected. In the app, however, it’s crickets. No errors, nothing.
As far as I can tell, the only thing unusual about my version is the typescript (and the //#ts-ignore’s that are required to account for the SDK’s lack of an Observable type).
Creating the message:
const handleMessageSubmit = async () => {
try {
const response = await API.graphql(
graphqlOperation(mutations.createMessage, {
input: {
authorID: userState.person.id,
text: message,
messageGroupID,
},
}),
);
} catch (err) {
console.log(err);
}
};
The subscription:
useEffect(() => {
const subscription = API.graphql(
graphqlOperation(subscriptions.onCreateMessage),
// #ts-ignore
).subscribe({
next: (event: any) => {
console.log('new message:', event);
},
error: (error: any) => {
console.log(error);
},
});
return () => {
console.log('unsubscribing');
// #ts-ignore
subscription.unsubscribe();
};
}, [messages]);
Turns out it was a problem with my imports.
Incorrect: import API from '#aws-amplify/api'
Correct: import { API } from '#aws-amplify'
The incorrect API worked just fine for other graphQL queries, but it was borking subscriptions.
I should also note that the failure was generating a AWSAppSyncProvider.ts:204 Uncaught (in promise) undefined error in the console, which I hadn't noticed earlier, though that didn't help much toward finding the solution.
While trying to inject prepared statements with MSSQL (tedious) - once I'm trying to execute the request I'm getting the following error:
Database connection failed: Requests can only be made in the LoggedIn state, not the SentClientRequest state
The imports:
import { Connection, Request } from 'tedious';
The hook:
public static async afterConnect(connection, options): Promise<void> {
const client = connection['resource'] as Connection;
let request = new Request('select 42',(err, rowCount, rows) => {
console.log(`${err} ${rowCount} rows`);
});
client.execSql(request);
}
According to tedious I need to chain the request after the previous one, sequelize doesn't seem to pass that data, is there a way to work around this issue?
I've also created an issue on Sequelize GitHub
took some digging inside sequelize code, but found a work around - if anyone has a similar issue.
public static async afterConnect(connection, options): Promise<void> {
const mssql = connection as { resource: Connection; previous: Promise<unknown> };
await new Promise((resolve) => {
let request = new Request('select 42', (err, rowCount) => {
logger.debug(method, `${err} - ${rowCount} rows`);
resolve();
});
await mssql.previous; // failsafe?
mssql.resource.execSql(request);
});
}
I am building a React application with GraphQL using AWS AppSync with DynamoDB. My use case is that I have a table of data that is being pulled from a DynamoDB table and displayed to the user using GraphQL. I have a few fields that are being updated by step functions running on AWS. I need those fields to be automatically updated for the user much like a subscription from GraphQL would do but I found out that subscriptions are tied to mutations and thus an update to the database from step functions will not trigger a subscription update on the frontend. To get around this I am using the following:
useEffect(() => {
setTimeout(getSubmissions, 5 * 1000)
})
Obviously this is a lot of overfetching and will probably incur unnecessary expense. I have looked for a better solution and come across DynamoDB streams but DynamoDB streams can't help me if they can't trigger the frontend to refresh the component. There has to be a better solution than what I have come up with.
Thanks!
You are correct, in AWS AppSync, to trigger a subscription publish you must trigger a GraphQL mutation.
but I found out that subscriptions are tied to mutations and thus an
update to the database from step functions will not trigger a
subscription update on the frontend.
If you update your DynamoDB table directly via step functions or via DynamoDB streams, then AppSync has no way to know the data refreshed.
Why don't you have your step function use an AppSync mutation instead of updating your table directly? That way you can link a subscription to the mutation and have your interested clients get pushed updates when the data is refreshed.
Assuming you are using Cognito as your authentication for your AppSync application, you could set a lambda trigger on the dynamo table that generates a cognito token, and uses that make an authorized request to your mutation endpoint. NOTE: in your cognito userpool>app clients page, you will need to check the Enable username password auth for admin APIs for authentication (ALLOW_ADMIN_USER_PASSWORD_AUTH) box to generate a client secret.
const AWS = require('aws-sdk');
const crypto = require('crypto');
var jwt = require('jsonwebtoken');
const secrets = require('./secrets.js');
var cognitoidentityserviceprovider = new AWS.CognitoIdentityServiceProvider();
var config;
const adminAuth = () => new Promise((res, rej) => {
const digest = crypto.createHmac('SHA256', config.SecretHash)
.update(config.userName + config.ClientId)
.digest('base64');
var params = {
AuthFlow: "ADMIN_NO_SRP_AUTH",
ClientId: config.ClientId, /* required */
UserPoolId: config.UserPoolId, /* required */
AuthParameters: {
'USERNAME': config.userName,
'PASSWORD': config.password,
"SECRET_HASH":digest
},
};
cognitoidentityserviceprovider.adminInitiateAuth(params, function(err, data) {
if (err) {
console.log(err.stack);
rej(err);
}
else {
data.AuthenticationResult ? res(data.AuthenticationResult) : rej("Challenge requested, to verify, login to app using admin credentials");
}
});
});
const decode = auth => new Promise( res => {
const decoded = jwt.decode(auth.AccessToken);
auth.decoded = decoded
res(auth);
});
//example gql query
const testGql = auth => {
const url = config.gqlEndpoint;
const payload = {
query: `
query ListMembers {
listMembers {
items{
firstName
lastName
}
}
}
`
};
console.log(payload);
const options = {
headers: {
"Authorization": auth.AccessToken
},
};
console.log(options);
return axios.post(url, payload, options).then(data => data.data)
.catch(e => console.log(e.response.data));
};
exports.handler = async (event, context, callback) => {
await secrets() //some promise that returns your keys object (i use secrets manager)
.then( keys => {
#keys={ClientId:YOUR_COGNITO_CLIENT,
# UserPoolId:YOUR_USERPOOL_ID,
# SecretHash:(obtained from cognito>userpool>app clients>app client secret),
# gqlEndpoint:YOUR_GRAPHQL_ENDPOINT,
# userName:YOUR_COGNITO_USER,
# password:YOUR_COGNITO_USER_PASSWORD,
# }
config = keys
return adminAuth()
})
.then(auth => {
return decode(auth)
})
.then(auth => {
return testGql(auth)
})
.then( data => {
console.log(data)
callback(null, data)
})
.catch( e => {
callback(e)
})
};
I'm looking for a solution where I can add the subscription to a specific route instead of binding the subscription globally when the app start.
I know the subscription can be acheived with following code
const wsLink = new WebSocketLink({
uri: `ws://localhost:4000`,
options: {
reconnect: true,
connectionParams: {
authToken: localStorage.getItem(AUTH_TOKEN),
}
}
})
const link = split(
({ query }) => {
const { kind, operation } = getMainDefinition(query)
return kind === 'OperationDefinition' && operation === 'subscription'
},
wsLink,
authLink.concat(httpLink)
)
const client = new ApolloClient({
link,
cache: new InMemoryCache()
})
But if I add this, the subscription is directly active upon page load irrespective of what page I'm on.
Is there any solution where I can bind the subscription on a specific page instead of having it binded globally.
If you are passing token in a query parameter and want to reconnect to the link all you need to do is update the link back by reassigning it
wsLink.subscriptionClient.url = `${GraphQLSocketURL}/query?authorization=${newAccessToken}`;
And if you want to establish connection when the page is loaded all you need to do is set lazy flag to true
const wsLink = new WebSocketLink({
uri: `${GraphQLSocketURL}/query?authorization=${accessToken}`,
options: {
reconnect: true,
reconnectionAttempts: 10,
lazy: true,
},
});