I am fairly new to using graphql-ws and graphql-yoga server, so forgive me if this is a naive question or mistake from my side.
I went through graphql-ws documentation. It has written the schema as a parameter. Unfortunately, the schema definition used in the documentation is missing a reference.
After adding a new todo (using addTodo) it shows two todo items. So I believe it is unable to return the initial todo list whenever running subscribe on Yoga Graphiql explorer.
It should show the initial todo item as soon as it has been subscribed and published in the schema definition.
My understanding is there is something I am missing in the schema definition which is not showing the todo list when tried accessing Yoga Graphiql explorer.
Has anyone had a similar experience and been able to resolve it? What I am missing?
Libraries used
Backend
graphql-yoga
ws
graphql-ws
Frontend
solid-js
wonka
Todo item - declared in schema
{
id: "1",
title: "Learn GraphQL + Solidjs",
completed: false
}
Screenshot
Code Snippets
Schema definition
import { createPubSub } from 'graphql-yoga';
import { Todo } from "./types";
let todos = [
{
id: "1",
title: "Learn GraphQL + Solidjs",
completed: false
}
];
// channel
const TODOS_CHANNEL = "TODOS_CHANNEL";
// pubsub
const pubSub = createPubSub();
const publishToChannel = (data: any) => pubSub.publish(TODOS_CHANNEL, data);
// Type def
const typeDefs = [`
type Todo {
id: ID!
title: String!
completed: Boolean!
}
type Query {
getTodos: [Todo]!
}
type Mutation {
addTodo(title: String!): Todo!
}
type Subscription {
todos: [Todo!]
}
`];
// Resolvers
const resolvers = {
Query: {
getTodos: () => todos
},
Mutation: {
addTodo: (_: unknown, { title }: Todo) => {
const newTodo = {
id: "" + (todos.length + 1),
title,
completed: false
};
todos.push(newTodo);
publishToChannel({ todos });
return newTodo;
},
Subscription: {
todos: {
subscribe: () => {
const res = pubSub.subscribe(TODOS_CHANNEL);
publishToChannel({ todos });
return res;
}
},
},
};
export const schema = {
resolvers,
typeDefs
};
Server backend
import { createServer } from "graphql-yoga";
import { WebSocketServer } from "ws";
import { useServer } from "graphql-ws/lib/use/ws";
import { schema } from "./src/schema";
import { execute, ExecutionArgs, subscribe } from "graphql";
async function main() {
const yogaApp = createServer({
schema,
graphiql: {
subscriptionsProtocol: 'WS', // use WebSockets instead of SSE
},
});
const server = await yogaApp.start();
const wsServer = new WebSocketServer({
server,
path: yogaApp.getAddressInfo().endpoint
});
type EnvelopedExecutionArgs = ExecutionArgs & {
rootValue: {
execute: typeof execute;
subscribe: typeof subscribe;
};
};
useServer(
{
execute: (args: any) => (args as EnvelopedExecutionArgs).rootValue.execute(args),
subscribe: (args: any) => (args as EnvelopedExecutionArgs).rootValue.subscribe(args),
onSubscribe: async (ctx, msg) => {
const { schema, execute, subscribe, contextFactory, parse, validate } =
yogaApp.getEnveloped(ctx);
const args: EnvelopedExecutionArgs = {
schema,
operationName: msg.payload.operationName,
document: parse(msg.payload.query),
variableValues: msg.payload.variables,
contextValue: await contextFactory(),
rootValue: {
execute,
subscribe,
},
};
const errors = validate(args.schema, args.document);
if (errors.length) return errors;
return args;
},
},
wsServer,
);
}
main().catch((e) => {
console.error(e);
process.exit(1);
});
apply these changes
Mutation: {
addTodo: (_: unknown, { title }: Todo) => {
const newTodo = {
id: "" + (todos.length + 1),
title,
completed: false
};
todos.push(newTodo);
publishToChannel({ todos });
return newTodo;
},
Subscription: {
todos: {
subscribe: () => {
return Repeater.merge(
[
new Repeater(async (push, stop) => {
push({ todos });
await stop;
}),
pubSub.subscribe(TODOS_CHANNEL),
]
)
}
},
},
first, npm i #repeaterjs/repeater then import Repeater
I want to delete a post from my apollo cache
const DELETE_POST = gql`
mutation deletePost($id: Int) {
deletePost(id: $id) {
id
}
}
`;
const deleteFromCache = (e: any, id: any) => {
e.preventDefault();
console.log('id', id, typeof id);
deletePost({
variables: { id },
onError: (err) => alert(err.message),
update(cache) {
cache.modify({
fields: {
posts(existingPosts: any[], { readField }) {
const newPosts = existingPosts.filter((post: any) => id !== readField('id', post));
console.log('newPosts', newPosts);
return newPosts;
},
},
});
},
});
};
However I am getting an error from Apollo
Schema is not configured to execute mutation operation.
Even though in my backend typeDefs I have post.id as ID! and my mongoose schema for post id is Number
When I fetch Posts, id is somehow a String
What am I doing wrong?
I'm presently studying Graphql(apollo) and utilising Node.js for the backend.
I've made some progress with queries and mutations, but I'm stuck at subscription. I've seen several videos and read some blogs, but I'm having trouble grasping it since they're utilising front-end frameworks, such as react, and I'm not familiar with react or any other front-end javascript frameworks.
I'm solely interested in learning it for the backend.
Is there anyone who can assist me with this?
I've got three separate queries (or whatever they're called) that I'm working on.
User,
Post and
Comment
Now I want to generate a subscription whenever someone adds a new comment or creates a new post.
For users, comments, and posts, I already have a mutation for add, update and remove.
There is currently no authentication or authorization in place.
Your question is more to help me implement this rather than actually asking an issue or problem you faced after trying out some steps in series of order.
This might help you when learning and implementing GraphQL subscription;
https://www.apollographql.com/docs/apollo-server/data/subscriptions/
https://www.youtube.com/watch?v=R2IrBI4-7Kg&t=698s
If you have tried something and faced a wall, please add your code and what you have done, so I could help you on where you are wrong or what you could do hopefully. Cheers.
The solutions I'm providing are crafted on #apollo/server v.4, with expressMiddleware and mongodb/mongoose on the backend and subscribeToMore with updateQuery on the client-side instead of the useSubscription hook.
The graphql-transport-ws transport library is no longer supported; instead, use graphql-ws.
The implementation differentiates three main collections: User, Post, and Comment, as well as subscription for post creation, post modification, and user authentication.
Likewise, as of 12.2022, the following setup and configuration apply.
Subscription on the backend:
Install the following dependencies:
$ npm i #apollo/server #graphql-tools/schema graphql-subscriptions graphql-ws ws cors body-parser mongoose graphql express
I'm assuming you've already configured your MongoDB models; if not, you might want to look at this repo for a basic setup.
Set up schema types and resolvers, such as this one.
// typeDefs.js
const typeDefs = `#graphql
type User {
id: ID!
email: String!
posts: [Post]!
commentsMade: [Comment]!
}
type Token {
value: String!
}
input UpdatePostInput {
title: String!
}
type Post {
id: ID!
title: String!
postedBy: User
comments: [Comment]!
}
input CommentInput {
text: String!
}
type Comment {
id: ID!
text: String!
commenter: User!
commentFor: Post!
}
type Query {
users: [User]!
user(id: ID!): User!
posts: [Post!]!
post(id: ID!): Post!
comment(id: ID!): Comment!
comments: [Comment!]!
}
type Mutation {
signup(email: String!, password: String!): User
signin(email: String!, password: String!): Token
createPost(title: String): Post
createComment(postId: String!, commentInput: CommentInput!): Comment
updatePost(postId: ID!, updatePostInput: UpdatePostInput!): Post
}
type Subscription {
postAdded: Post
commentAdded: Comment
postUpdated: Post
}
`
export default typeDefs
// resolvers.js
import dotenv from 'dotenv'
import { PubSub } from 'graphql-subscriptions'
import mongoose from 'mongoose'
import { GraphQLError } from 'graphql'
import bcrypt from 'bcrypt'
import UserModel from '../models/User.js'
import PostModel from '../models/Post.js'
import CommentModel from '../models/Comment.js'
dotenv.config()
...
const pubsub = new PubSub()
const User = UserModel
const Post = PostModel
const Comment = CommentModel
const secret = process.env.TOKEN_SECRET
const resolvers = {
Query: {...},
Mutation: {
...
createPost: async (_, args, contextValue) => {
const authUser = contextValue.authUser
if (!authUser) {
throw new GraphQLError('User is not authenticated', {
extensions: {
code: 'UNAUTHENTICATED',
http: { status: 401 },
},
})
}
const post = new Post({
...args,
postedBy: mongoose.Types.ObjectId(authUser.id),
})
try {
const savedPost = await post.save()
authUser.posts = authUser.posts.concat(post._id)
await authUser.save()
const addedPost = {
id: savedPost.id,
title: savedPost.title,
postedBy: savedPost.postedBy,
comments: savedPost.comments,
}
// subscription postAdded with object iterator POST_ADDED
pubsub.publish('POST_ADDED', { postAdded: addedPost })
return post
} catch (error) {
throw new GraphQLError(`Error: ${error.message}`, {
extensions: {
code: 'BAD_USER_INPUT',
http: { status: 400 },
argumentName: args,
},
})
}
},
updatePost: async (_, args, contextValue) => {
const authUser = contextValue.authUser
if (!authUser) {
throw new GraphQLError('User is not authenticated', {
extensions: {
code: 'UNAUTHENTICATED',
http: { status: 401 },
},
})
}
try {
const post = await Post.findByIdAndUpdate(
args.postId,
args.updatePostInput,
{ new: true }
)
.populate('comments')
.populate('postedBy')
const updatedPost = {
id: post.id,
title: post.title,
postedBy: post.postedBy,
comments: post.comments,
}
// subscription postUpdated with object iterator POST_UPDATED
pubsub.publish('POST_UPDATED', { postUpdated: updatedPost })
return post
} catch (error) {
throw new GraphQLError(`Error: ${error.message}`, {
extensions: {
code: 'BAD_REQUEST',
http: { status: 400 },
argumentName: args,
},
})
}
},
},
// resolvers for post addition and post modification using subscribe function
Subscription: {
postAdded: {
subscribe: () => pubsub.asyncIterator('POST_ADDED'),
},
commentAdded: {
subscribe: () => pubsub.asyncIterator('COMMENT_ADDED'),
},
postUpdated: {
subscribe: () => pubsub.asyncIterator('POST_UPDATED'),
},
},
//Hard-coding the default resolvers is appropriate in some cases,
// but I think it is required in fields with references to other
//database models to avoid returning null field values.
Post: {
id: async (parent, args, contextValue, info) => {
return parent.id
},
title: async (parent) => {
return parent.title
},
postedBy: async (parent) => {
const user = await User.findById(parent.postedBy)
.populate('posts', { id: 1, title: 1, comments: 1, postedBy: 1 })
.populate('commentsMade')
//console.log('id', user.id)
//console.log('email', user.email)
return user
},
comments: async (parent) => {
return parent.comments
},
},
Comment: {
id: async (parent, args, contextValue, info) => {
return parent.id
},
text: async (parent, args, contextValue, info) => {
return parent.text
},
commenter: async (parent, args, contextValue, info) => {
const user = await User.findById(parent.commenter)
.populate('posts')
.populate('commentsMade')
return user
},
commentFor: async (parent, args, contextValue, info) => {
const post = await Post.findById(parent.commentFor)
.populate('comments')
.populate('postedBy')
return post
},
},
...
}
export default resolvers
The code in the main entry server file (e.g. index.js) may look like this, e.g.
import dotenv from 'dotenv'
import { ApolloServer } from '#apollo/server'
import { expressMiddleware } from '#apollo/server/express4'
import { ApolloServerPluginDrainHttpServer } from '#apollo/server/plugin/drainHttpServer'
import { makeExecutableSchema } from '#graphql-tools/schema'
import { WebSocketServer } from 'ws'
import { useServer } from 'graphql-ws/lib/use/ws'
import express from 'express'
import http from 'http'
import cors from 'cors'
import bodyParser from 'body-parser'
import jwt from 'jsonwebtoken'
import UserModel from './models/User.js'
import typeDefs from './tpeDefs.js'
import resolvers from './resolvers.js'
import mongoose from 'mongoose'
dotenv.config()
mongoose.set('strictQuery', false)
let db_uri
if (process.env.NODE_ENV === 'development') {
db_uri = process.env.MONGO_DEV
}
mongoose.connect(db_uri).then(
() => {
console.log('Database connected')
},
(err) => {
console.log(err)
}
)
const startGraphQLServer = async () => {
const app = express()
const httpServer = http.createServer(app)
const schema = makeExecutableSchema({ typeDefs, resolvers })
const wsServer = new WebSocketServer({
server: httpServer,
path: '/',
})
const serverCleanup = useServer({ schema }, wsServer)
const server = new ApolloServer({
schema,
context: async ({ req }) => {
const authHeader = req.headers['authorization']
const token = authHeader && authHeader.split(' ')[1]
if (token) {
const decoded = jwt.verify(token, process.env.TOKEN_SECRET)
const authUser = await UserModel.findById(decoded.id)
.populate('commentsMade')
.populate('posts')
return { authUser }
}
},
plugins: [
ApolloServerPluginDrainHttpServer({ httpServer }),
{
async serverWillStart() {
return {
async drainServer() {
await serverCleanup.dispose()
},
}
},
},
],
})
await server.start()
app.use(
'/',
cors(),
bodyParser.json(),
expressMiddleware(server, {
context: async ({ req }) => {
const authHeader = req.headers['authorization']
const token = authHeader && authHeader.split(' ')[1]
if (token) {
const decoded = jwt.verify(token, process.env.TOKEN_SECRET)
const authUser = await UserModel.findById(decoded.id)
.populate('commentsMade')
.populate('posts')
return { authUser }
}
},
})
)
const PORT = 4000
httpServer.listen(PORT, () =>
console.log(`Server is now running on http://localhost:${PORT}`)
)
}
startGraphQLServer()
END. It's time to run some tests and checks in the Apollo Explorer sandbox. Be conscientious about defining the required default resolvers to avert the Apollo server from sending null values on your behalf.
To view the code and implementation, go to this repository.
Happy coding!
I recently created a simple MERN application that is supposed to use a form to send data to the backend using Redux to maintain state management. I'm new to Redux (as you will see in my code) and I believe I must have messed up the dispatching.
Below are the functions in my Form component:
const [landlordData, setLandlordData] = useState({name: '', type: '', rating: '', details: ''});
const dispatch = useDispatch();
const handleSubmit = (e) => {
e.preventDefault();
console.log(landlordData);
dispatch(createLandlord(landlordData));
}
Which console log the data from the form normally. When I submit though the new entry in the MongoDB only includes the time created and the UUID of the entry due to the Schema of the database:
import mongoose from 'mongoose';
const landlordSchema = mongoose.Schema({
name: String,
type: String,
rating: Number,
details: String,
createdAt: {
type: Date,
default: new Date()
}
});
var landlordDetails = mongoose.model('Landlords', landlordSchema);
export default landlordDetails;
To provide more context on the backend operations here is the controller script I made:
import landlordDetails from '../models/landlords.js';
export const getLandlords = async (req, res) => {
try {
const getDetails = await landlordDetails.find();
console.log(getDetails);
res.status(200).json(getDetails);
} catch (error) {
res.status(404).json({ message: error.message });
}
}
export const createLandlords = async (req, res) => {
const details = req.body;
const newLandlord = new landlordDetails(details);
try {
await newLandlord.save();
res.status(201).json(newLandlord);
console.log("New landlord added!");
} catch (error) {
res.status(409).json({ message: error.message })
}
}
Please let me know if any more information is needed or if I am completely oblivious to something obvious. Thank you.
EDIT: To provide more context, here are my api calls and my action script:
API:
import axios from 'axios';
const url = 'http://localhost:5000/landlords';
export const fetchLandlords = () => axios.get(url);
export const createLandlord = (landlordData) => axios.post(url, landlordData);
Actions JS file:
import * as api from '../api/index.js';
//Action creators
export const getLandlords = () => async (dispatch) => {
try {
const { data } = await api.fetchLandlords();
dispatch({ type: 'FETCH_ALL', payload: data });
} catch (error) {
console.log(error.message);
}
};
export const createLandlord = (landlord) => async (dispatch) => {
try {
const { data } = await api.createLandlord(landlord);
dispatch({ type: 'CREATE', payload: data });
} catch (error){
console.log(error);
}
};
When I click the submit button, a new database entry is made with the createdAt field but nothing else.
I am trying to create a collection in mongodb where a field named lists will contain an array of link and linkName. I am successfully able to create a two seperate field link and linkName, however not able to store the value inside lists.
Model code for mongodb :-
const socialSchema = new Schema({
lists: [{
link:{ formType: String},
linkName: { formType: String}
}]
})
API code :-(this code is for creating only, will later on try to use findOneAndUpdate to update the existing field
router.route('/', [auth]).post(async (req, res) => {
const {linkName, link } = req.body
try {
console.log(req.body)//Ex. { linkName: 'facebook', link: 'www.facebook.com'}
const social = new Social({
//Stuck here!!!
})
await social.save()
res.json(social)
} catch (err) {
console.error(err.message);
res.status(500).send('Server Errors')
}
}
)
Part of frontend Code(React)
const [formData, setFormData] = useState({
linkName: '',
link: ''
});
const {linkName, link} = formData
const onChange = e =>
setFormData({ ...formData, [e.target.name]: e.target.value });
const handleSubmit = async e => {
e.preventDefault()
const socialList = {
linkName,
link
}
try {
const config = {
headers: {
'Content-Type': 'application/json'
}
};
const body = JSON.stringify(socialList)
const res = await Axios.post('/api/social', body, config)
console.log(res)
} catch (err) {
console.error(err);
}
}
In your schema change from {formType: String} to {type: String}.
const data = {link: req.body.link, linkName: req.body.linkName};
Social.create({
links: [data]
});
This should work.
MY FULL WORKING CODE THAT I TESTED
const schema = new mongoose.Schema({
links: [
{
link: { type: String },
linkName: { type: String }
}
]
});
const Model = mongoose.model("test", schema);
const doc = { link: "link", linkName: "linkname" };
Model.create({
links: [doc]
});