Example blog site built with Neo4j GraphQL & React.js. This application showcases features of Neo4j GraphQL such as;
- Nested Mutations
- @auth directive
- OGM(Object Graph Mapper)
There are only two custom resolvers in the server; sign up plus sign in. The lack of custom logic is showcasing how quickly developers can build, both powerful and secure, applications ontop of Neo4j. Its worth nothing this entire application contains zero 'raw' cypher. All interaction's with the database are done through the generated GraphQL Schema via either the OGM or Apollo Server.
Diagram from https://arrows.app/
type User {
id: ID! @id
email: String!
password: String!
createdBlogs: [Blog!]! @relationship(type: "HAS_BLOG", direction: OUT)
authorsBlogs: [Blog!]! @relationship(type: "CAN_POST", direction: OUT)
password: String! @private
createdAt: DateTime @timestamp(operations: [CREATE])
updatedAt: DateTime @timestamp(operations: [UPDATE])
}
type Blog {
id: ID! @id
name: String!
creator: User! @relationship(type: "HAS_BLOG", direction: IN)
authors: [User!]! @relationship(type: "CAN_POST", direction: IN)
posts: [Post!]! @relationship(type: "HAS_POST", direction: OUT)
createdAt: DateTime @timestamp(operations: [CREATE])
updatedAt: DateTime @timestamp(operations: [UPDATE])
}
type Post {
id: ID! @id
title: String!
content: String!
blog: Blog! @relationship(type: "HAS_POST", direction: IN)
comments: [Comment!]! @relationship(type: "HAS_COMMENT", direction: OUT)
author: User! @relationship(type: "WROTE", direction: IN)
createdAt: DateTime @timestamp(operations: [CREATE])
updatedAt: DateTime @timestamp(operations: [UPDATE])
}
type Comment {
id: ID! @id
author: User! @relationship(type: "COMMENTED", direction: IN)
content: String!
post: Post! @relationship(type: "HAS_COMMENT", direction: IN)
createdAt: DateTime @timestamp(operations: [CREATE])
updatedAt: DateTime @timestamp(operations: [UPDATE])
}
Schema above simplified for clarity.
If you want to run this Blog locally follow the steps below. When it comes to Configure environment variables you will need a running Neo4j instance to point to.
Each client and server folders contains a ./env.example
file. Copy this file, to the same directory, at ./.env
and adjust configuration to suit your local machine although the defaults may be fine.
There are many ways to get started with neo4j such as; Neo4j Sandbox, Neo4j Desktop or Docker.
Clone the repo;
$ git clone https://github.com/neo4j/graphql
Install
$ cd graphql && yarn install
=> Configure environment variables <=
Run Seeder on;
$ yarn run neo-push:seed
Checkout the seeder its using the OGM
Once seeded used the default credentials to log in
- Email: [email protected]
- Password: password
Run the webpack and graphql servers with;
$ yarn run neo-push
Navigate to http://localhost:4000 and sign up!
This application has two custom resolvers; sign in and sign up. In the resolvers we return a JWT. This JWT is stored in local storage on the client. The contents of the JWT is something like;
{
"sub": "1234-4321-abcd-dcba", # user.id
"iat": { ... }
}
the .sub
property is the users id. We use NEO4J_GRAPHQL_JWT_SECRET
env var on the sever to configure the secret.
Note to keep things simple... This application has no JWT expiry or refreshing mechanism. Patterns you would implement outside of
@neo4j/graphql
so we deemed it less important in this showcase.
When the client is making a request to server we attach the JWT in the authorization
header of the request, the same header @neo4j/graphql
looks at.
At the core of the app but to keep things simple the UI doesn't have any profile page ect ect.
Before you can create a post you must create a blog. Users can have many blogs with many post. Each blog can have an array of authors, whom can post to the blog.
Once logged in users are directed to the dashboard page;
query myBlogs($id: ID, $offset: Int, $limit: Int, $hasNextBlogsoffset: Int) {
myBlogs: blogs(
where: { OR: [{ creator: { id: $id } }, { authors: { id: $id } }] }
options: { limit: $limit, offset: $offset, sort: { createdAt: DESC } }
) {
id
name
creator {
id
email
}
createdAt
}
hasNextBlogs: blogs(
where: { OR: [{ creator: { id: $id } }, { authors: { id: $id } }] }
options: { limit: 1, offset: $hasNextBlogsoffset, sort: { createdAt: DESC } }
) {
id
createdAt
}
}
⚠ Page info such as Relay spec is not supported in the current version of @neo4j/graphql
so with the My Blogs and Recently Updated Blogs we query twice asking for the next item, to determine if there is a next page. Using this technique we can paginate the blog lists.
Image showing pagination with limit of 1, in the app its default to 10.
From the dashboard you can create a blog.
mutation($name: String!, $sub: ID) {
createBlogs(input: [{ name: $name, creator: { connect: { where: { node: { id: $sub } } } } }]) {
blogs {
id
name
createdAt
}
}
}
If your the creator of a blog you can edit its name.
mutation editBlog($id: ID, $name: String) {
updateBlogs(where: { id: $id }, update: { name: $name }) {
blogs {
id
}
}
}
If you are the creator of a blog you can assign other users as an author. You can also revoke too!
mutation assignBlogAuthor($blog: ID, $authorEmail: String) {
updateBlogs(where: { id: $blog }, connect: { authors: { where: { node: { email: $authorEmail } } } }) {
blogs {
authors {
email
}
}
}
}
mutation revokeBlogAuthor($blog: ID, $authorEmail: String) {
updateBlogs(where: { id: $blog }, disconnect: { authors: { where: { email: $authorEmail } } }) {
blogs {
authors {
email
}
}
}
}
If you are the creator of a blog you can delete it.
mutation deleteBlog($id: ID) {
deleteComments(where: { post: { blog: { id: $id } } }) {
nodesDeleted
}
deletePosts(where: { blog: { id: $id } }) {
nodesDeleted
}
deleteBlogs(where: { id: $id }) {
nodesDeleted
}
}
Users can have many posts with many comments. Blog creators and authors can edit the post. Blog creators and authors of the post can delete it.
Once you have a blog. Either the creator or authors can create a post.
Posts support markdown
mutation createPost($title: String!, $content: String!, $user: ID, $blog: ID) {
createPosts(
input: [
{
title: $title
content: $content
blog: { connect: { where: { node: { id: $blog } } } }
author: { connect: { where: { node: { id: $user } } } }
}
]
) {
posts {
id
}
}
}
Authors and creators can edit posts belonging to the blog.
mutation editPost($id: ID, $content: String, $title: String) {
updatePosts(where: { id: $id }, update: { content: $content, title: $title }) {
posts {
id
}
}
}
Creators of the blog and authors of the post can delete.
mutation deletePost($id: ID) {
deleteComments(where: { post: { id: $id } }) {
nodesDeleted
}
deletePosts(where: { id: $id }) {
nodesDeleted
}
}
Any user can comment on any post. The Author of the comment can edit. Blog creators and post authors can delete comments on a related post.
Any user can comment on any post.
Comments support markdown
mutation commentOnPost($post: ID, $content: String!, $user: ID) {
commentOnPost: createComments(
input: [
{
content: $content
post: { connect: { where: { node: { id: $post } } } }
author: { connect: { where: { node: { id: $user } } } }
}
]
) {
comments {
id
content
author {
id
email
}
createdAt
}
}
}
Users can edit there own comments.
mutation updateComment($id: ID, $content: String) {
updateComments(where: { id: $id }, update: { content: $content }) {
comments {
id
}
}
}
Authors of the comment, authors of the post plus creators of the blog can delete a comment.
mutation deleteComment($id: ID) {
deleteComments(where: { id: $id }) {
nodesDeleted
}
}