How to delete from cache in Apollo Client ReactJS? - node.js

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?

Related

Trouble with return statement in Node Async Await

Trying to write a query for DynamoDB and learn promises etc. The console.log(resp.Items) returns the object that I am looking for so I think my query is formatted correctly. I get a status 200 back with an empty object.
I have read up for a few days and tried to implement various changes to the code by nothing is returning the object resp.Items. I am guessing the function is returning before the const is updated with the data but I am not sure why the console.log works.
const AWS = require('aws-sdk')
const dynamodb = new AWS.DynamoDB()
const getTechs = async () => {
try {
const resp = await dynamodb
.query({
ExpressionAttributeValues: {
':tech': { S: 'TECH#' },
},
KeyConditionExpression: 'PK = :tech',
TableName: process.env.TABLE_NAME,
ScanIndexForward: true,
})
.promise()
console.log(resp.Items)
if (!resp.Items) {
return {
error: 'No Techs in the DB',
}
}
return {
tech: resp.Items,
}
} catch (error) {
console.log('Error retrieving Tech List')
console.log(error)
return {
error: 'Could not retrieve Tech List',
}
}
}
handler func
const { makeHandler } = require('./utils')
const { getTechs } = require('../data')
// const { Tech } = require('../entities')
const inputSchema = {
type: 'object',
properties: {
pathParameters: {
type: 'object',
properties: {
tech: { type: 'string' },
},
required: ['tech'],
},
},
required: ['pathParameters'],
}
const handler = async (event) => {
const { techs, error } = await getTechs()
const statusCode = error ? 500 : 200
const body = error ? JSON.stringify({ error }) : JSON.stringify({ techs })
return {
statusCode,
body,
}
}
module.exports.handler = makeHandler({ handler })
executeTransactWrite func
const executeTransactWrite = async ({ tech, params }) => {
const transactionRequest = tech.transactWriteItems(params)
let cancellationReasons
transactionRequest.on('extractError', (response) => {
try {
cancellationReasons = JSON.parse(
response.httpResponse.body.toString()
).CancellationReasons
} catch (err) {
// suppress this just in case some types of errors aren't JSON parseable
console.error('Error extracting cancellation error', err)
}
})
return new Promise((resolve, reject) => {
transactionRequest.send((err, response) => {
if (err) {
err.cancellationReasons = cancellationReasons
return reject(err)
}
return resolve(response)
})
})
}
module.exports = {
executeTransactWrite,
makehandler func
const middy = require('middy')
const {
jsonBodyParser,
validator,
httpErrorHandler,
} = require('middy/middlewares')
const makeHandler = ({ handler, inputSchema }) =>
middy(handler)
.use(jsonBodyParser())
.use(validator({ inputSchema }))
.use(httpErrorHandler())
module.exports = { makeHandler }

Unable to get initial data using graphql-ws subscription

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

Learn about apollo-graphql Subscription

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!

passing object to graphql resolver shows object as undefined

I'm new to graphql and am working on this issue for a while. My frontend uses React.js and Apollo Client, and backend is Node.js with Apollo GraphQL and Prisma. I am trying to create a simple mutation that will add a recipe.
Here is my react component code:
const [addRecipe] = useMutation(CREATE_RECIPE)
const addRecipeClick = () => {
const recipeData = {
title: 'Crinkle Cookies',
author: 'Adina', ingredients: [{ quantity: '1 c', name: 'cocoa' }], link: '', imageURL: ''
}
addRecipe({ variables: { recipe: recipeData } })
.then(() => {
enqueueSnackbar('Recipe Added!', { variant: 'success' })
})
.catch(() => enqueueSnackbar(`Unable to add recipe`, { variant: 'error' }))
}
apollo client query:
export const CREATE_RECIPE = gql`
mutation($recipe: RecipeInput!) {
createRecipe(recipe: $recipe){
recipes {
title
author
ingredients {
quantity
name
}
}
}
}`
backend schema:
type Mutation {
createRecipe(recipe: RecipeInput!): RecipeResults
}
input RecipeInput {
title: String!,
author: String!,
ingredients: [Ingredient]!,
link: String,
imageURL: String
}
backend resolver:
const createRecipe = async (recipe: RecipeInput) => {
console.log('createRecipe resolver: recipe ', recipe) // undefined
try {
const result = await prisma.recipe.create({
data: recipe
})
const recipes = await prisma.recipe.findMany()
return { recipes }
}
catch (e) {
console.log('error: ', e)
return {}
}
}
When I log out 'recipeData' on the frontend, console shows its value is set.
However, when I log out 'recipe' in createRecipe() resolver, it's undefined. Can't figure out why.

How to use Redux to dispatch data to the backend (and consequently mongoDB)?

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.

Resources