diff --git a/package.json b/package.json index 254b55e..3d1f6d1 100644 --- a/package.json +++ b/package.json @@ -9,8 +9,8 @@ "scripts": { "build": "babel src/ -d dist/ --copy-files", "start": "node dist/", - "dev": "nodemon --exec babel-node src/ -e js,graphql", - "dev:debug": "nodemon --exec babel-node --inspect=0.0.0.0:9229 src/index.js -e js,graphql", + "dev": "nodemon --exec babel-node src/ -e js,gql", + "dev:debug": "nodemon --exec babel-node --inspect=0.0.0.0:9229 src/index.js -e js,gql", "lint": "eslint src --config .eslintrc.js", "test": "nyc --reporter=text-lcov yarn run test:jest", "test:cypress": "run-p --race test:before:*", @@ -54,6 +54,7 @@ "jsonwebtoken": "~8.5.0", "linkifyjs": "~2.1.8", "lodash": "~4.17.11", + "merge-graphql-schemas": "^1.5.8", "ms": "~2.1.1", "neo4j-driver": "~1.7.3", "neo4j-graphql-js": "~2.4.2", diff --git a/src/graphql-schema.js b/src/graphql-schema.js index 6832d2a..4e475a4 100644 --- a/src/graphql-schema.js +++ b/src/graphql-schema.js @@ -1,16 +1,11 @@ -import fs from 'fs' -import path from 'path' import userManagement from './resolvers/user_management.js' import statistics from './resolvers/statistics.js' import reports from './resolvers/reports.js' import posts from './resolvers/posts.js' import moderation from './resolvers/moderation.js' +import types from './types' -export const typeDefs = fs - .readFileSync( - process.env.GRAPHQL_SCHEMA || path.join(__dirname, 'schema.graphql') - ) - .toString('utf-8') +export const typeDefs = types export const resolvers = { Query: { diff --git a/src/schema.graphql b/src/schema.graphql index 801eb45..ca67b66 100644 --- a/src/schema.graphql +++ b/src/schema.graphql @@ -1,3 +1,13 @@ +########################################################## +# +# ATTENTION! Please use the files inside src/types/ +# +# This document is only used for merging +# and will not be processed by the system! +# It will be removed soon. +# +########################################################## + type Query { isLoggedIn: Boolean! "Get the currently logged in User based on the given JWT Token" diff --git a/src/types/badge.gql b/src/types/badge.gql new file mode 100644 index 0000000..6031865 --- /dev/null +++ b/src/types/badge.gql @@ -0,0 +1,23 @@ +enum BadgeTypeEnum { + role + crowdfunding +} +enum BadgeStatusEnum { + permanent + temporary +} + +type Badge { + id: ID! + key: String! + type: BadgeTypeEnum! + status: BadgeStatusEnum! + icon: String! + + rewarded: [User]! @relation(name: "REWARDED", direction: "OUT") +} + +type User { + badges: [Badge]! @relation(name: "REWARDED", direction: "IN") + badgesCount: Int! @cypher(statement: "MATCH (this)<-[:REWARDED]-(r:Badge) RETURN COUNT(r)") +} diff --git a/src/types/blacklist.gql b/src/types/blacklist.gql new file mode 100644 index 0000000..af71a6c --- /dev/null +++ b/src/types/blacklist.gql @@ -0,0 +1,3 @@ +type User { + blacklisted: [User]! @relation(name: "BLACKLISTED", direction: "OUT") +} diff --git a/src/types/category.gql b/src/types/category.gql new file mode 100644 index 0000000..696b58e --- /dev/null +++ b/src/types/category.gql @@ -0,0 +1,20 @@ +type Category { + id: ID! + name: String! + slug: String + icon: String! + posts: [Post]! @relation(name: "CATEGORIZED", direction: "IN") + postCount: Int! @cypher(statement: "MATCH (this)<-[:CATEGORIZED]-(r:Post) RETURN COUNT(r)") +} + +type Post { + categories: [Category]! @relation(name: "CATEGORIZED", direction: "OUT") +} + +type Organization { + categories: [Category]! @relation(name: "CATEGORIZED", direction: "OUT") +} + +type User { + categories: [Category]! @relation(name: "CATEGORIZED", direction: "OUT") +} diff --git a/src/types/comment.gql b/src/types/comment.gql new file mode 100644 index 0000000..f06071c --- /dev/null +++ b/src/types/comment.gql @@ -0,0 +1,21 @@ +type Comment { + id: ID! + author: User @relation(name: "WROTE", direction: "IN") + content: String! + contentExcerpt: String + createdAt: String + updatedAt: String + deleted: Boolean + disabled: Boolean + disabledBy: User @relation(name: "DISABLED", direction: "IN") +} + +type User { + comments: [Comment]! @relation(name: "WROTE", direction: "OUT") + commentsCount: Int! @cypher(statement: "MATCH (this)-[:WROTE]->(r:Comment) WHERE NOT r.deleted = true RETURN COUNT(r)") +} + +type Post { + comments: [Comment]! @relation(name: "COMMENTS", direction: "IN") + commentsCount: Int! @cypher(statement: "MATCH (this)<-[:COMMENTS]-(r:Comment) RETURN COUNT(r)") +} diff --git a/src/types/disable.gql b/src/types/disable.gql new file mode 100644 index 0000000..b43db5e --- /dev/null +++ b/src/types/disable.gql @@ -0,0 +1,4 @@ +type Mutation { + disable(id: ID!): ID + enable(id: ID!): ID +} diff --git a/src/types/follow.gql b/src/types/follow.gql new file mode 100644 index 0000000..bd3f2ca --- /dev/null +++ b/src/types/follow.gql @@ -0,0 +1,37 @@ + +enum FollowTypeEnum { + User + Organization + Project +} + +type Mutation { + "Follow the given Type and ID" + follow(id: ID!, type: FollowTypeEnum): Boolean! @cypher(statement: """ + MATCH (n {id: $id}), (u:User {id: $cypherParams.currentUserId}) + WHERE $type IN labels(n) AND NOT $id = $cypherParams.currentUserId + MERGE (u)-[r:FOLLOWS]->(n) + RETURN COUNT(r) > 0 + """) + "Unfollow the given Type and ID" + unfollow(id: ID!, type: FollowTypeEnum): Boolean! @cypher(statement: """ + MATCH (:User {id: $cypherParams.currentUserId})-[r:FOLLOWS]->(n {id: $id}) + WHERE $type IN labels(n) + DELETE r + RETURN COUNT(r) > 0 + """) +} + +type User { + following: [User]! @relation(name: "FOLLOWS", direction: "OUT") + followingCount: Int! @cypher(statement: "MATCH (this)-[:FOLLOWS]->(r:User) RETURN COUNT(DISTINCT r)") + + followedBy: [User]! @relation(name: "FOLLOWS", direction: "IN") + followedByCount: Int! @cypher(statement: "MATCH (this)<-[:FOLLOWS]-(r:User) RETURN COUNT(DISTINCT r)") + + "Is the currently logged in user following that user?" + followedByCurrentUser: Boolean! @cypher(statement: """ + MATCH (this)<-[:FOLLOWS]-(u:User {id: $cypherParams.currentUserId}) + RETURN COUNT(u) >= 1 + """) +} diff --git a/src/types/friends.gql b/src/types/friends.gql new file mode 100644 index 0000000..8ed8bcd --- /dev/null +++ b/src/types/friends.gql @@ -0,0 +1,4 @@ +type User { + friends: [User]! @relation(name: "FRIENDS", direction: "BOTH") + friendsCount: Int! @cypher(statement: "MATCH (this)<-[:FRIENDS]->(r:User) RETURN COUNT(DISTINCT r)") +} diff --git a/src/types/index.js b/src/types/index.js new file mode 100644 index 0000000..e25d4fa --- /dev/null +++ b/src/types/index.js @@ -0,0 +1,17 @@ +import fs from 'fs' +import path from 'path' +import { mergeTypes } from 'merge-graphql-schemas' + +const loadSchema = file => { + return fs.readFileSync(path.join(__dirname, file)).toString('utf-8') +} + +let typeDefs = [] + +fs.readdirSync(__dirname).forEach(file => { + if (file.split('.').pop() === 'gql') { + typeDefs.push(loadSchema(file)) + } +}) + +export default mergeTypes(typeDefs, { all: true }) diff --git a/src/types/location.gql b/src/types/location.gql new file mode 100644 index 0000000..268d99e --- /dev/null +++ b/src/types/location.gql @@ -0,0 +1,16 @@ +type Location { + id: ID! + name: String! + nameEN: String + nameDE: String + nameFR: String + nameNL: String + nameIT: String + nameES: String + namePT: String + namePL: String + type: String! + lat: Float + lng: Float + parent: Location @cypher(statement: "MATCH (this)-[:IS_IN]->(l:Location) RETURN l") +} diff --git a/src/types/organization.gql b/src/types/organization.gql new file mode 100644 index 0000000..c8384e2 --- /dev/null +++ b/src/types/organization.gql @@ -0,0 +1,16 @@ +type Organization { + id: ID! + createdBy: User @relation(name: "CREATED_ORGA", direction: "IN") + ownedBy: [User] @relation(name: "OWNING_ORGA", direction: "IN") + name: String! + slug: String + description: String! + descriptionExcerpt: String + deleted: Boolean + disabled: Boolean +} + +type User { + organizationsCreated: [Organization] @relation(name: "CREATED_ORGA", direction: "OUT") + organizationsOwned: [Organization] @relation(name: "OWNING_ORGA", direction: "OUT") +} diff --git a/src/types/post.gql b/src/types/post.gql new file mode 100644 index 0000000..fc2b3e2 --- /dev/null +++ b/src/types/post.gql @@ -0,0 +1,43 @@ +enum VisibilityEnum { + public + friends + private +} + +type Post { + id: ID! + author: User @relation(name: "WROTE", direction: "IN") + title: String! + slug: String + content: String! + contentExcerpt: String + image: String + visibility: VisibilityEnum + deleted: Boolean + disabled: Boolean + disabledBy: User @relation(name: "DISABLED", direction: "IN") + createdAt: String + updatedAt: String + + relatedContributions: [Post]! @cypher(statement: """ + MATCH (this)-[:TAGGED|CATEGORIZED]->(categoryOrTag)<-[:TAGGED|CATEGORIZED]-(post:Post) + WHERE (NOT exists(post.deleted) OR post.deleted = false) + AND (NOT exists(post.disabled) OR post.disabled = false) + RETURN DISTINCT post + LIMIT 10 + """) +} + +type User { + contributions: [Post]! @relation(name: "WROTE", direction: "OUT") + contributionsCount: Int! @cypher(statement: """ + MATCH (this)-[:WROTE]->(r:Post) + WHERE (NOT exists(r.deleted) OR r.deleted = false) + AND (NOT exists(r.disabled) OR r.disabled = false) + RETURN COUNT(r)""" + ) +} + +type Comment { + post: Post @relation(name: "COMMENTS", direction: "OUT") +} diff --git a/src/types/report.gql b/src/types/report.gql new file mode 100644 index 0000000..e1f31d0 --- /dev/null +++ b/src/types/report.gql @@ -0,0 +1,14 @@ +type Mutation { + report(id: ID!, description: String): Report +} + +type Report { + id: ID! + submitter: User @relation(name: "REPORTED", direction: "IN") + description: String + type: String! @cypher(statement: "MATCH (resource)<-[:REPORTED]-(this) RETURN labels(resource)[0]") + createdAt: String + comment: Comment @relation(name: "REPORTED", direction: "OUT") + post: Post @relation(name: "REPORTED", direction: "OUT") + user: User @relation(name: "REPORTED", direction: "OUT") +} diff --git a/src/types/shout.gql b/src/types/shout.gql new file mode 100644 index 0000000..4154107 --- /dev/null +++ b/src/types/shout.gql @@ -0,0 +1,38 @@ +enum ShoutTypeEnum { + Post + Organization + Project +} + +type Mutation { + "Shout the given Type and ID" + shout(id: ID!, type: ShoutTypeEnum): Boolean! @cypher(statement: """ + MATCH (n {id: $id})<-[:WROTE]-(wu:User), (u:User {id: $cypherParams.currentUserId}) + WHERE $type IN labels(n) AND NOT wu.id = $cypherParams.currentUserId + MERGE (u)-[r:SHOUTED]->(n) + RETURN COUNT(r) > 0 + """) + + "Unshout the given Type and ID" + unshout(id: ID!, type: ShoutTypeEnum): Boolean! @cypher(statement: """ + MATCH (:User {id: $cypherParams.currentUserId})-[r:SHOUTED]->(n {id: $id}) + WHERE $type IN labels(n) + DELETE r + RETURN COUNT(r) > 0 + """) +} + +type User { + shouted: [Post]! @relation(name: "SHOUTED", direction: "OUT") + shoutedCount: Int! @cypher(statement: "MATCH (this)-[:SHOUTED]->(r:Post) WHERE NOT r.deleted = true AND NOT r.disabled = true RETURN COUNT(DISTINCT r)") +} +type Post { + shoutedBy: [User]! @relation(name: "SHOUTED", direction: "IN") + shoutedCount: Int! @cypher(statement: "MATCH (this)<-[:SHOUTED]-(r:User) WHERE NOT r.deleted = true AND NOT r.disabled = true RETURN COUNT(DISTINCT r)") + + "Has the currently logged in user shouted that post?" + shoutedByCurrentUser: Boolean! @cypher(statement: """ + MATCH (this)<-[:SHOUTED]-(u:User {id: $cypherParams.currentUserId}) + RETURN COUNT(u) >= 1 + """) +} diff --git a/src/types/statistics.gql b/src/types/statistics.gql new file mode 100644 index 0000000..b2f489d --- /dev/null +++ b/src/types/statistics.gql @@ -0,0 +1,16 @@ +type Query { + "Get the latest Network Statistics" + statistics: Statistics! +} + +type Statistics { + countUsers: Int! + countPosts: Int! + countComments: Int! + countNotifications: Int! + countOrganizations: Int! + countProjects: Int! + countInvites: Int! + countFollows: Int! + countShouts: Int! +} diff --git a/src/types/tag.gql b/src/types/tag.gql new file mode 100644 index 0000000..94db5ad --- /dev/null +++ b/src/types/tag.gql @@ -0,0 +1,18 @@ +type Tag { + id: ID! + name: String! + taggedPosts: [Post]! @relation(name: "TAGGED", direction: "IN") + taggedOrganizations: [Organization]! @relation(name: "TAGGED", direction: "IN") + taggedCount: Int! @cypher(statement: "MATCH (this)<-[:TAGGED]-(p) RETURN COUNT(DISTINCT p)") + taggedCountUnique: Int! @cypher(statement: "MATCH (this)<-[:TAGGED]-(p)<-[:WROTE]-(u:User) RETURN COUNT(DISTINCT u)") + deleted: Boolean + disabled: Boolean +} + +type Post { + tags: [Tag]! @relation(name: "TAGGED", direction: "OUT") +} + +type Organization { + tags: [Tag]! @relation(name: "TAGGED", direction: "OUT") +} diff --git a/src/types/user.gql b/src/types/user.gql new file mode 100644 index 0000000..65cf806 --- /dev/null +++ b/src/types/user.gql @@ -0,0 +1,44 @@ +type Query { + "Check if the current user is logged in" + isLoggedIn: Boolean! + + "Get the currently logged in User based on the given JWT Token" + currentUser: User +} +type Mutation { + "Get a JWT Token for the given Email and password" + login(email: String!, password: String!): String! + signup(email: String!, password: String!): Boolean! +} + +enum UserGroupEnum { + admin + moderator + user +} + +type User { + id: ID! + name: String + email: String + slug: String + password: String! + avatar: String + deleted: Boolean + disabled: Boolean + disabledBy: User @relation(name: "DISABLED", direction: "IN") + role: UserGroupEnum + + location: Location @cypher(statement: "MATCH (this)-[:IS_IN]->(l:Location) RETURN l") + locationName: String + about: String + + createdAt: String + updatedAt: String +} + +enum UserGroupEnum { + admin + moderator + user +} diff --git a/yarn.lock b/yarn.lock index 24b54ea..95b1fcb 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2421,6 +2421,11 @@ deep-is@~0.1.3: resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.3.tgz#b369d6fb5dbc13eecf524f91b070feedc357cf34" integrity sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ= +deepmerge@^2.2.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-2.2.1.tgz#5d3ff22a01c00f645405a2fbc17d0778a1801170" + integrity sha512-R9hc1Xa/NOBi9WRVUWg19rl1UB7Tt4kuPd+thNJgFZoxXsTz7ncaPaeIm+40oSGuP33DfMb4sZt1QIGiJzC4EA== + default-require-extensions@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/default-require-extensions/-/default-require-extensions-2.0.0.tgz#f5f8fbb18a7d6d50b21f641f649ebb522cfe24f7" @@ -4992,6 +4997,15 @@ merge-descriptors@1.0.1: resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.1.tgz#b00aaa556dd8b44568150ec9d1b953f3f90cbb61" integrity sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E= +merge-graphql-schemas@^1.5.8: + version "1.5.8" + resolved "https://registry.yarnpkg.com/merge-graphql-schemas/-/merge-graphql-schemas-1.5.8.tgz#89457b60312aabead44d5b2b7625643f8ab9e369" + integrity sha512-0TGOKebltvmWR9h9dPYS2vAqMPThXwJ6gVz7O5MtpBp2sunAg/M25iMSNI7YhU6PDJVtGtldTfqV9a+55YhB+A== + dependencies: + deepmerge "^2.2.1" + glob "^7.1.3" + is-glob "^4.0.0" + merge-source-map@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/merge-source-map/-/merge-source-map-1.1.0.tgz#2fdde7e6020939f70906a68f2d7ae685e4c8c646"