Name: Tsvetoslav Dimov
SocialDeck is currently an API that enables users to manage their posts (Create, Read, Update, Delete, Share). It uses GraphQL, instead of REST because of the following reasons:
- Declarative data fetching: there's no need to call multiple endpoints to access various data, like with traditional REST approach. Instead, you specify the exact data you need and GraphQL gives you exactly what you asked for.
- Improved performance: GraphQL improves performance by helping avoid round trips to the server and reducing payload size. If one want to take advantage of GraphQL, they don't even have to scrape their existing REST API, as GraphQL can be built on top of REST.
- GraphQL was developed by Facebook and it's currently open-sourced. It's quickly gaining popularity. A few of its the most notable users are Airbnb, GitHub, The New York Times, Coursera.
GraphQL is typically served over HTTP via a single endpoint which expresses the full set of capabilities of the service. This is in contrast to REST APIs which expose a suite of URLs each of which expose a single resource.
The GraphQL endpoint for my API is '/graphql'.
GraphQL comes with an in-browser API explorer. If you navigate to /graphql endpoint in your browser, you will see it in action. There you can check all existing queries and mutations if you click on the button DOCS, located on the right side of the page. Underneath it you will see another button SCHEMA, which will show you the full API schema. It’s also possible to write queries and mutations directly on the left tab and check their execution by clicking the PLAY icon in the middle of the screen.
If you'd like to see a video demo of the Explorer and API calls, CLICK HERE.
-
Queries
users: [User!]!
- Gets a list of all users.findUserById(_id: String!): User!
- Finds a user by ID.me: User!
- Gets currently authenticated user.posts: [Post!]!
- Gets a list of all user posts.findPostById(_id: String!): Post!
- Finds a post by ID.
-
Mutations
signUp(email: EmailAddress!, password: String!, firstName: String!, lastName: String!): String!
- Takes in the specified arguments, creates a new user and signs them in.logIn(email: EmailAddress!, password: String!): String!
- Takes in the specified arguments and logs the user in.logOut: String!
- Logs out currently authenticated user, if present. Returns a request status message.deleteUserById(_id: String!): User!
- Deletes a user by its ID. Returns the deleted document.deleteAllUsers: String!
- Deletes all users. Returns a request status message.createPost(message: String, links: [LinkInput!]): Post!
- Takes in the specified arguments and creates a new post. Returns the created document.sharePost(postID: String!): Post!
- Adds the ID of currently authenticated user to the shares array of the specified posts. Returns the shared post document.updatePost(postID: String!, message: String!, links: [LinkInput!]): Post!
- Takes in the specified arguments to update an existing post. Returns the updated document.deletePostById(_id: String!): Post!
- Deletes a post by its ID. Returns the deleted document.deleteAllPosts: String!
- Deletes all posts. Returns a request status message.
Simply put, the database stores users and their posts.
The password
field is an encrypted value of their original password, created by a package called bcrypt.
It is then use to verify if hashed value of the original password matches the provided password argument to mutations
like logIn() and signUp().
The email
field should be an email address of valid format. If it is not, the API will complain and not perform the
creation of User object.
Posts and Users are stored in separate databases.
Using GraphQL I was able to make a connection between a Users and their Posts. I use the User's _id
to fetch their
Posts collection from the database and populate it as the value of posts
array in the User objects.
Each Post object has a field creatorID
, which references a value from the User database.
The createdTime
and updatedTime
fields are generated by a package called 'moment'. It gets the current date and time
and stores it in UTC format, keeping the local time. Initially updateTime
would null, but once a Post gets updated,
this value changes to the exact time when the update happened.
The links
field stores array of URLs and they are checked if they are of valid URL format before being saved to the database.
The shares
field stores an array of Users _id
values. Initially it is null, until the Post object gets shared by a User.
[
{
"_id": "5dbff437f482e01d03fecd4b",
"email": "[email protected]",
"password": "$2b$10$u8JvPqJ3v08S.s9zL6LOy.su65KlcQr3dmYUqhv0rzUXYqtpgV7O2",
"firstName": "Test",
"lastName": "Johnson",
"posts": [
{
"_id": "5dbff437f482e01d03fecd4d",
"creatorID": "5dbff437f482e01d03fecd4b",
"createdTime": "2019-11-04T09:49:43.000Z",
"message": "Test message by user 1...",
"updatedTime": "2019-11-04T10:50:01.000Z",
"links": [
"https://github.com/Urigo/graphql-scalars/",
"https://moodle.wit.ie/"
],
"shares": [
"5dbff437f482e01d03fecd4c"
]
}
]
},
{
"_id": "5dbff437f482e01d03fecd4c",
"email": "[email protected]",
"password": "$2b$10$u8JvPqJ3v08S.s9zL6LOy.su65KlcQr3dmYUqhv0rzUXYqtpgV7O2",
"firstName": "Thomas",
"lastName": "Farrell",
"posts": [
{
"_id": "5dbff437f482e01d03fecd4e",
"creatorID": "5dbff437f482e01d03fecd4c",
"createdTime": "2019-11-04T09:49:43.000Z",
"message": "Test message by user 2...",
"updatedTime": null,
"links": [
"https://mongoosejs.com/docs/api/",
"https://developer.github.com/"
],
"shares": []
}
]
}
]
SocialDeck
GraphQL API
• Authentication
◦ signUp()
✓ should be able to sign up with a valid email (132ms)
✓ should return error if email is not unique
✓ should return error if the user is already logged in (81ms)
◦ logIn()
✓ should be able to log in with valid credentials (70ms)
✓ should return error if the user is already logged in (79ms)
✓ should return error if user's email is not in the db
✓ should return error if user's password does not match the db record (73ms)
◦ logOut()
✓ should be able to log out an authenticated user (74ms)
✓ should return error if the user has not logged in
• Queries
◦ me()
✓ should be able to return currently authenticated user (85ms)
✓ should return error if the user has not logged in
◦ users()
✓ should return all users from the db (83ms)
✓ should return error if the user has not logged in
✓ should return error if there are no users in the db (79ms)
◦ findUserById()
✓ should fetch existing user (83ms)
✓ should return error if the user has not logged in
✓ should return error if the user does not exist (81ms)
◦ posts()
✓ should return all posts from the db (83ms)
✓ should return error if the user has not logged in
✓ should return error if there are no posts in the db (81ms)
◦ findPostById()
✓ should fetch existing post (80ms)
✓ should return error if the user has not logged in
✓ should return error if the post does not exist (77ms)
• Mutations
◦ deleteUserById()
✓ should delete an existing user (79ms)
✓ should return error if the user has not logged in
✓ should return error if the user does not exist (78ms)
◦ deleteAllUsers()
✓ should delete all existing users (78ms)
✓ should return error if the user has not logged in
✓ should return error if 'users' database is empty (81ms)
◦ createPost()
✓ should create a new post (81ms)
✓ should return error if the user has not logged in
✓ should return error if 'links' argument contains a badly formatted URL (79ms)
◦ deletePostById()
✓ should delete an existing post (82ms)
✓ should return error if the user has not logged in
✓ should return error if the post does not exist (86ms)
◦ deleteAllPosts()
✓ should delete all existing posts (83ms)
✓ should return error if the user has not logged in
✓ should return error if 'posts' database is empty (83ms)
◦ sharePost()
✓ should share an existing post (90ms)
✓ should return error if the user has not logged in
✓ should return error if the post does not exist (93ms)
◦ updatePost()
✓ should update an existing post (86ms)
✓ should return error if the user has not logged in
✓ should return error if the post does not exist (84ms)
Other tests
✓ should get index page
45 passing (3s)
The assignment requirements stated that we must create a REST API and write tests for it. Having heard of GraphQL before, I knew about its advantages over REST and was curious to try it out for the first time. After a discussion with my lecturer, we agreed that GraphQL would suit this project with one condition - to provide proper documentation about its usage and pros/cons.
GraphQL comes with built-in scalar types (Int, Float, String, Boolean and ID). While this covers most of the use cases,
often the developer would need custom type like Date, Email, URL and so on. I used graphql-scalars
package, that provides a collection of scalars that can be used out of the box. I used scalar types Email
, Date
and
URL
to support validation in my GraphQL API.
By default Express requests are sequential and no request can be linked
to each other. Which means that users cannot be identified unless using JWT token and Authorization header, or some other
mechanism. That's why I decided to implement express-session. By using
it every user of the API will be assigned a unique session, which allowed me to store the user state across the requests
to the API. The sessions are persisted on MongoDB and I've set them to expire 1 hour after creation. All the API queries
and mutations require the user to authenticate before they get access to the data. However, there are two GraphQL mutations
that don't require authentication. They are logIn()
and signUp()
. After successful authentication, the user doesn't
need to provide credentials as their session is stored in a cookie, accessible in all the requests.
When a user registers, their password gets stored on the database. Storing passwords as plain text is a bad practice.
For that reason I used bcrypt to store a hash of the original user's password.
In future logIn()
requests I compare the hashed password from the database with the password that the user provides on
log in and catch relevant errors, which then I send back to the user.