DGraph Advanced Data Modeling: Part 4 - Likes, Bookmarks, and Votes
Tags: dgraph, graphql, graphdb, firebase
Published: 2021-12-06 03:31:41

Usually when you want to `like` something, `bookmark` something, or `vote` on something, you have a similar data model. You do not want someone to have multiple likes on the same item, in this case.

We think of a `hearts` data model like the following:

### SQL

**Posts Table**

|id|name|...|
|:-|:-|:-|

**User Table**

|id|name|...|
|:-|:-|:-|

**Posts_Likes Table (Junction)**

|posts_fk|users_fk|
|:-|:-|

- You have a foreign key constrain so that the junction table must always match the primary keys
- You enable Cascade Delete on both tables
- You use a COUNT WHERE clause to count the likes on a certain table

### noSQL

```
Posts --> PostID --> Hearts --> UserID
```
OR
```
Users --> UserID --> Hearts --> PostID
```
OR
```
Hearts --> PostID__UserID
```
- You have a Rule that checks this to make sure it matches the postID and userID.
- The ID__ID makes sure you do not have duplicate items
- In Firebase / Firestore you may have to keep up with your counts using increments and decrements, and use rules to enforce this. Mongo will aggregate some of this for you.
- You must create a custom lambda trigger (or firebase function) that will delete all `heart` documents when the post is deleted (if that is your functionality)

But in a Graph Database, it is much much cleaner:

```typescript
type User {
  username: String!
  email: String!
  likedPosts: [Post] @hasInverse(field: likes)
  ...
}
type Post {
  id: ID!
  name: String!
  likes: [User]
  ...
}
```

- You add a user `link` to the `likes` node, and you remove it.
- Simply count the number of 'links' in Post.likes Where ID is that post
- You can query `likedPosts` from the user end, or `likes` from the post end.
- The @hasInverse will enforce both edges, `liked` and `likedPost`, will get deleted with the post or user

## Meta Information
- SQL - store createdAt in the Junction Table
- noSQL - store createdAt in the 'hearts' document
- graphdb
    1. store createdAt in a facet
    2. store createdAt in a nested node

The data model above assumes you do not want the `createdAt` information, but if you do, you would model it this way:

```typescript
type User {
  username: String!
  email: String!
  likedPosts: [Like] @hasInverse(field: user)
  ...
}
type Post {
  id: ID!
  name: String!
  likes: [Like] @hasInverse(field: post)
  ...
}
type Like {
  id: String! @id
  post: Post!
  user: User!
}
```
- Here you could copy the same noSQL pattern and enforce:
  `id = post.id + '__' + user.id` in a custom mutation

### Security

I would recommend you use the first method unless you need meta information. If you don't use GraphQL, also use the first method, and store the `createdAt` information in facets. Currently, you can't query facets in GraphQL yet.

Both versions also will need a [custom mutation](https://dgraph.io/docs/graphql/custom/mutation/) in order for someone to `heart` a post, until Field Level Auth becomes available in the future to prevent users from editing your post as well.

Your custom mutation could have a `toggle` version like so:

```typescript
async function toggleLike({ args, dql, authHeader }: any) {

    const claimsBase64 = authHeader.value.split(".")[1];
    const claims = JSON.parse(atob(claimsBase64));
    const postID = args.id;

    const q = `
    upsert {
        query {
            q1(func: eq(User.email, "${claims.email}")) {
                User as uid
            }
            q2(func: uid("${postID}")) {
                uid
                Post.likes
                @filter(eq(User.email, "${claims.email}")) {
                    r as uid
                    User.email
                }
            }
        }
        mutation @if(eq(len(r), 1)) {
            delete {
                <${postID}> <Post.likes> uid(User) .
                uid(User) <User.likedPosts> <${postID}> .
            }
        }
        mutation @if(eq(len(r), 0)) {
            set {
                <${postID}> <Post.likes> uid(User) .
                uid(User) <User.likedPosts> <${postID}> .
            }
        }
    }`;

    const m = await dql.mutate(q);
    return featureID;
}

(self as any).addGraphQLResolvers({
    "Mutation.toggleLike": toggleLike,
});
```

If you prefer to separate like / unlike, you could just run a basic JSON DQL mutation in the lambda(s) for each:

**like**
```typescript
{
  "uid": postID,
  "Post.likes": {
    "uid": userID,
    "User.likedPost": {
      "uid": postID
    }
  }
}
```
**unlike**
```typescript
{
  "delete": {
    "uid": postID,
    "Post.likes": {
      "uid": userID,
      "User.likedPost": {
        "uid": postID
      }
    }
  }
}
```

If you need the second version, with the extra edge for meta information, join the unofficial [dgraph discord server](https://discord.gg/jmrcFmw6ba) and ask!

However, if security is not a worry, you could stick with the trusty graphQL and avoid the lambda completely:

```typescript
mutation {
  updatePost(input: {
    filter: {
      id: "0x2"
    },
    set: {
      "likes": "0x28"
    }
  }) { id }
}
```

But those of us who care about security, will have an [auth-rule](https://dgraph.io/docs/graphql/authorization/directive/) to make sure our post is secure, and create a **custom lambda mutation** to allow any logged in user to securely like, bookmark, or vote on a post, tweet, or other item.

That is, until DGraph implements more security later in 2022.

Next up, Advanced @auth security...

J