Type Resolvers Issue


#1

Hello,

I really appreciate any help provided and am willing to pay for assistance with the following implementation:

I am looking to prohibit users downvoting a post if they have already downvoted. I was looking to keep my codebase clean as I’ll have very similar logic for upvoting. When I try to use type resolvers for the Post type to determine if a user has currently downvoted, this information isn’t available to to the parent resolver… i.e. When I run a downvotePost mutation, I cannot access the currentUserDownvoted field to check the boolean if the user has already voted and reject the mutation.

Is the only option I have is: implement the logic in the parent resolvers and ensure I return the currentUserDownvoted field in every resolver that returns a post object?

Here are my (very shortened) resolvers:
const resolvers = {

    Query: {

        async post(parent, args, ctx, info) {
             const post = await ctx.db.query.post({}, info)
             if (!post) throw new Error('This post could not be found.')
             return post
        },
        async feedPosts(parent, args, ctx, info) {
             const posts = await ctx.db.query.posts({ orderBy: 'upvoteCount_DESC' }, info)
             if (!post) throw new Error('This post could not be found.')
             return posts
        }

    },

    Mutation: {

        async downvotePost(parent, args, ctx, info) {
            const post = await ctx.db.query.post({ where: { id: args.id } }, info)
            if (!post) throw new Error('This post no longer seems to exist.')

            // ISSUE: these are not defined as Post resolver is hit after this.
            // Only option currently can think is to make the below check in this resolver instead
            // of Post resolver? -> How do I solve this?

            if (post.currentUserDownvoted) {
               console.log('ALREADY DOWNVOTE')
            } else if (post.currentUserUpvoted) {
                console.log('ALREADY UPVOTE')
            } else {
                console.log('NO UPVOTE OR DOWNVOTE')
            }

            return post

        }

      },

    Post: {
       title(parent, args, ctx, info) {
          return parent.title
        },

       async currentUserDownvoted(parent, args, ctx, info) {
            if (ctx.request.user) {
                 const downvotes = await ctx.db.query.downvotesConnection(
                    { where: {
                          AND: [ { post: { id: parent.id } }, { user: { id: ctx.request.user.id } } ]
                     } }, `{ aggregate { count } }` )

                    return downvotes.aggregate.count > 0
             }
             return false
        }
    }
}

Typedefs -> backend schema, not prisma (shortened again)

const typeDefs = `
    type User {
        id: ID!
        email: String!
        username: String!
        name: String!
        cognitoID: String!
        posts: [Post]!
    }

    type Post {
        id: ID!
        createdAt: DateTime!
        updatedAt: DateTime!
        title: String!
        tags: [Tag]
        # make this required
        description: String
        content: String!
        entity: Entity!
        author: User!
        upvoteCount: Int!
        downvoteCount: Int!
        currentUserUpvoted: Boolean!
        currentUserDownvoted: Boolean!
    }

    type Upvote {
        id: ID!
        user: User!
        post: Post
        comment: Comment
    }
    
    type Downvote {
        id: ID!
        user: User!
        post: Post
        comment: Comment
    }

`

Prisma Datamodel:

type User {
id: ID! @unique
name: String
email: String! @unique
username: String! @unique
cognitoID: String! @unique
comments: [Comment]!
posts: [Post]! @relation(name: "PostByUser")
upvotes: [Upvote]!
downvotes: [Downvote]!
}

type Post {
id: ID! @unique
title: String!
comments: [Comment]!
content: String
description: String
entity: Entity!
author: User! @relation(name: "PostByUser")
tags: [Tag] @relation(name: "TagsRelatedToPost")
upvotes: [Upvote]!
downvotes: [Downvote]!
upvoteCount: Int @default(value: "0")
downvoteCount: Int @default(value: "0")
#
createdAt: DateTime!
updatedAt: DateTime!
}

type Upvote {
id: ID! @unique
user: User!
post: Post
comment: Comment
}

type Downvote {
id: ID! @unique
user: User!
post: Post
comment: Comment
}

Perhaps I should change my prisma schema to track upvoters:[User], downvoters:[User]?


#2

What you probably want is $exists

in your case, something like this:

const downVoteExists = await context.prisma.$exists.downvote({ user: { id: userId }, post: { id: postId } })

Regarding your datamodel, I would suggest the following changes to avoid redundant data and simplify operations:

    type User {
        id: ID!
        email: String!
        username: String!
        name: String!
        cognitoID: String!
        posts: [Post]!
    }

    type Post {
        id: ID!
        createdAt: DateTime!
        updatedAt: DateTime!
        title: String!
        tags: [Tag]
        # make this required
        description: String
        content: String!
        entity: Entity!
        author: User!
        upvoteCount: Int!
        downvoteCount: Int!
        currentUserUpvoted: Boolean!
        currentUserDownvoted: Boolean!
    }

    type Vote {
        id: ID!
        user: User!
        post: Post!
        comment: Comment
        type: VoteTypeEnum
    }

    enum VoteTypeEnum {
        DOWNVOTE
        UPVOTE
    }

Next, our vote mutation resolver:

const Mutation = {
    async vote (root, { postId, type }, context, info) {
         // note this resolver would run just fine if the user tried to downvote again
         //  after already downvoting. Nothing would change and no error would be be thrown
         // if this behavior is not desired, we could query to check the type of the existing vote
        const userId = context.request.user.id
        const userVote = await context.prisma.vote({ user: { id: userid }, post: { id: postId } })
        if (userVote) {
            // type is one of 'UPVOTE' or 'DOWNVOTE'
            if (userVote.type === type) {
              return userVote
            }
            return context.prisma.updateVote({
              where: { id: userVote.id },
              data: { type }
            })
        } else {
            return context.prisma.createVote({
              post: { id: postId },
              user: { id: userId },
              type
            })
        }
    }
}

Finally or Post resolvers:

const Post = {
    userDownVoted: ({ id }, args, context, info) => context.prisma.$exists.vote({ post: { id }, user: { id: context.request.user.id }, type 'DOWNVOTE' }),
    userUpVoted: ({ id }, args, context, info) => context.prisma.$exists.vote({ post: { id }, user: { id: context.request.user.id }, type 'UPVOTE' })
}

#3

@BenoitRanque

I truly appreciate your help and reply! The only problem is I’m not using bindings, not client (at least I’m 90% sure) so I don’t believe I have access to $exists. It’s showing undefined. Same concept with vote just use voteConnection.aggregate.count? or a better solution with bindings?

prisma.yml

endpoint: ${env:PRISMA_ENDPOINT}
datamodel: datamodel.graphql
# secret: ${env:PRISMA_SECRET}
hooks:
  post-deploy:
      - graphql get-schema -p prisma

createServer.js

return new GraphQLServer({
  typeDefs: 'src/schema.graphql',
  resolvers: {
    Mutation,
    Query,
    Post
   },
   resolverValidationOptions: {
      requireResolversForResolveType: false
   },
   context: req => ({ ...req, db })
})

db.js

const { Prisma } = require('prisma-binding')

const db = new Prisma({
   typeDefs: 'src/generated/prisma.graphql',
   endpoint: process.env.PRISMA_ENDPOINT,
   debug: false
})

#4

Apologies about the mixup
You do have exists, docs here :wink:

Sidenote: I really don’t know why they dropped bindings from the docs, I find bindings awesome!

Edit: use would look something like this:

const voteExists = await prisma.exists.Vote({ user: { id: userId }, post: { id: postId } })

#5

@BenoitRanque

Awesome, I really appreciate your help!

Let me know if there’s anyway I can repay you for the help.