Best Practices for Upvote/Downvote


#1

What is the preferred way to implement upvote/downvote relations?

The method I am using is resulting in error. I don’t know why it doesn’t work.

async downvoteBill(parent, args, ctx, info) {
		const { userId } = ctx.request;
		if (!userId) {
			throw new Error('You must be signed in');
		}

		downvote = await ctx.db.mutation.updateBill({
			data: {
				upvotes: {
					disconnect: {
						id: userId
					}
				},
				downvotes: {
					connect: {
						id: userId
					}
				}
			},
			where: {
				id: args.id
			}
		});

		return downvote;
	},

but I get the error:

Error: GraphQL error: The relation UpvoteBill has no node for the model Bill with the value ‘cjqmpuu941bi20a12chsy8o2l’ for the field ‘id’ connected to a node for the model User with the value ‘cjq626r53owk40a98hzqzflyy’ for the field ‘id’

It seems as though it gets hung on disconnecting a node that doesn’t exist, but is there any advantage to throwing an error here? Seems like it’d make more sense to pass on things like this.

here’s the relevant datamodel:

type Bill {
        id: ID! @unique
        upvotes: [User] @relation(name: "UpvoteBill")
        downvotes: [User] @relation(name: "DownvoteBill")
        comments: [Comment] @relation(name: "BillComment")
} 

type User {
        upvotedBills: [Bill] @relation(name: UpvoteBill)
        downvotedBills: [Bill} @relation(name: DownvoteBIll)
}

The HowToGraphQL tutorial for making the hackernews site has its own type for votes:

type Vote {
  id: ID!
  link: Link!
  user: User!
}

type Link {
  id: ID!
  createdAt: DateTime!
  description: String!
  url: String!
  postedBy: User
  votes: [Vote!]!
}

I however am not sure why this is necessary. Why not create a direct connection from User to Link?

But my way doesn’t work, and the HowToGraphQL tutorial does, so I’m inclined to try that. Only to follow this convention while also implementing downvotes would require an Upvote type and Downvote type, which seems superfluous. It gets even worse when you start adding up/downvotes for other types such as comments or replies to comments.

I’m curious if and how anyone else is doing this…


#2

Hi,

Currently, disconnect throw this error when no connected node exists. There is already a pending FR about this problem: https://github.com/prisma/prisma/issues/3312

For now, the best bet would be to check the existence of upvote first:

async function downvoteBill(parent, args, ctx, info) {
  const { userId } = ctx.request;
  if (!userId) {
    throw new Error("You must be signed in");
  }

  const alreadyUpvoted = prisma.exists.Bill({
    id: args.id,
    upvotes: {
      id: userId
    }
  });
  let downvote;
  if (alreadyUpvotes) {
    downvote = await ctx.db.mutation.updateBill({
      data: {
        upvotes: {
          disconnect: {
            id: userId
          }
        },
        downvotes: {
          connect: {
            id: userId
          }
        }
      },
      where: {
        id: args.id
      }
    });
  } else {
    downvote = await ctx.db.mutation.updateBill({
      data: {
        downvotes: {
          connect: {
            id: userId
          }
        }
      },
      where: {
        id: args.id
      }
    });
  }

  return downvote;
}


#4

seems as though prisma-client’s ctx.prisma.$exist is not working… I suppose this would be a separate issue. Thanks for pointing out the FR!


#5

Are you using prisma client or bindings? The code you have shared above uses prisma bindings. Please try not to mix them :slightly_smiling_face:


#6

prisma-client doesn’t seem to work with my nested mutations :frowning:

Working on fixing a build error so I can retry with the client. Didn’t know they couldn’t be mixed though, is this stated in documentation and I missed it?


#7

I am getting this error with prisma-client:

Uncaught (in promise) Error: GraphQL error: Variable ‘$where’ expected value of type ‘BillWhereInput’ but got: {“id”:“cjra0zduw9ip70a686owupfif”,“upvotes”:{“id”:“cjq626r53owk40a98hzqzflyy”}}. Reason: ‘upvotes’ Field ‘upvotes’ is not defined in the input type ‘BillWhereInput’. (line 1, column 8):
query ($where: BillWhereInput) {
^
at new ApolloError (ApolloError.js:37)
at Object.next (QueryManager.js:179)
at notifySubscription (Observable.js:126)
at onNotify (Observable.js:161)
at SubscriptionObserver.next (Observable.js:215)
at Object.next (index.js:41)
at notifySubscription (Observable.js:126)
at onNotify (Observable.js:161)
at SubscriptionObserver.next (Observable.js:215)
at notifySubscription (Observable.js:126)
at onNotify (Observable.js:161)
at SubscriptionObserver.next (Observable.js:215)
at httpLink.js:123

Here is the code updated using prisma-client:

async upvoteBill(parent, args, ctx, info) {
		const { userId } = ctx.request;
		if (!userId) {
			throw new Error('You must be logged in');
		}
		const upvoted = ctx.prisma.$exists.bill({
			id: args.id,
			upvotes: {
				id: userId
			}
		});
		const downvoted = ctx.prisma.$exists.bill({
			id: args.id,
			downvotes: {
				id: userId
			}
		});
		if (upvoted && downvoted) {
			console.log('found upvote and downvote. disconnecting one.');
			upvote = await ctx.prisma.updateBill({
				data: {
					downvotes: {
						disconnect: {
							id: userId
						}
					}
				},
				where: {
					id: args.id
				}
			});
		} else if (upvoted) {
			console.log('undoing previous upvote');
			upvote = await ctx.prisma.updateBill({
				upvotes: {
					disconnect: {
						id: userId
					}
				},
				where: {
					id: args.id
				}
			});
		} else if (downvoted) {
			console.log('previously downvoted. changing your mind...');
			upvote = await ctx.prisma.updateBill({
				data: {
					downvotes: {
						disconnect: {
							id: userId
						}
					},
					upvotes: {
						connect: {
							id: userId
						}
					}
				},
				where: { id: args.id }
			});
		} else {
			console.log('default');
			upvote = await ctx.prisma.updateBill({
				data: {
					upvotes: {
						connect: { id: userId }
					}
				},
				where: { id: args.id }
			});
		}
		return upvote;
	},

	async downvoteBill(parent, args, ctx, info) {
		const { userId } = ctx.request;
		if (!userId) {
			throw new Error('You must be logged in');
		}

		const upvoted = ctx.prisma.$exists.bill({
			id: args.id,
			upvotes: {
				id: userId
			}
		});

		const downvoted = ctx.prisma.$exists.bill({
			id: args.id,
			downvotes: {
				id: userId
			}
		});

		let downvote;
		if (upvoted && downvoted) {
			downvote = await ctx.prisma.updateBill({
				data: {
					upvotes: {
						disconnect: {
							id: userId
						}
					}
				},
				where: {
					id: args.id
				}
			});
		} else if (downvoted) {
			downvote = await ctx.prisma.updateBill({
				data: {
					downvotes: {
						disconnect: {
							id: userId
						}
					}
				},
				where: {
					id: args.id
				}
			});
		}
		if (upvoted) {
			downvote = await ctx.prisma.updateBill({
				data: {
					upvotes: {
						disconnect: {
							id: userId
						}
					},
					downvotes: {
						connect: {
							id: userId
						}
					}
				},
				where: {
					id: args.id
				}
			});
		} else {
			downvote = await ctx.prisma.updateBill({
				data: {
					downvotes: {
						connect: {
							id: userId
						}
					}
				},
				where: { id: args.id }
			});
		}
		return downvote;
	},

Using Prisma-client docs as a reference.

Is anything wrong with my syntax?