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:
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 thelikes
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, orlikes
from the post end. - The @hasInverse will enforce both edges,
liked
andlikedPost
, 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
- store createdAt in a facet
- 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:
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 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:
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
{
"uid": postID,
"Post.likes": {
"uid": userID,
"User.likedPost": {
"uid": postID
}
}
}
unlike
{
"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 and ask!
However, if security is not a worry, you could stick with the trusty graphQL and avoid the lambda completely:
mutation {
updatePost(input: {
filter: {
id: "0x2"
},
set: {
"likes": "0x28"
}
}) { id }
}
But those of us who care about security, will have an auth-rule 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