From bf9dd205ca6a790c12550182f68ec43e99bf8076 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wolfgang=20Hu=C3=9F?= Date: Tue, 2 Aug 2022 08:26:14 +0200 Subject: [PATCH] Implement GQL for groups --- backend/src/db/migrate/store.js | 3 +- backend/src/models/Groups.js | 133 ++++++++++ backend/src/models/index.js | 1 + .../schema/types/enum/GroupActionRadius.gql | 6 + backend/src/schema/types/enum/GroupType.gql | 5 + backend/src/schema/types/type/Group.gql | 249 ++++++++++++++++++ backend/src/schema/types/type/User.gql | 52 ++-- 7 files changed, 422 insertions(+), 27 deletions(-) create mode 100644 backend/src/models/Groups.js create mode 100644 backend/src/schema/types/enum/GroupActionRadius.gql create mode 100644 backend/src/schema/types/enum/GroupType.gql create mode 100644 backend/src/schema/types/type/Group.gql diff --git a/backend/src/db/migrate/store.js b/backend/src/db/migrate/store.js index 377caf0b0..7a8be0b94 100644 --- a/backend/src/db/migrate/store.js +++ b/backend/src/db/migrate/store.js @@ -62,8 +62,9 @@ class Store { await txc.run('CALL apoc.schema.assert({},{},true)') // drop all indices return Promise.all( [ - 'CALL db.index.fulltext.createNodeIndex("post_fulltext_search",["Post"],["title", "content"])', 'CALL db.index.fulltext.createNodeIndex("user_fulltext_search",["User"],["name", "slug"])', + 'CALL db.index.fulltext.createNodeIndex("user_fulltext_search",["Group"],["name", "slug", "description"])', // Wolle: check for 'name', 'slug', 'description' + 'CALL db.index.fulltext.createNodeIndex("post_fulltext_search",["Post"],["title", "content"])', 'CALL db.index.fulltext.createNodeIndex("tag_fulltext_search",["Tag"],["id"])', ].map((statement) => txc.run(statement)), ) diff --git a/backend/src/models/Groups.js b/backend/src/models/Groups.js new file mode 100644 index 000000000..aa6f5767e --- /dev/null +++ b/backend/src/models/Groups.js @@ -0,0 +1,133 @@ +import { v4 as uuid } from 'uuid' + +export default { + id: { type: 'string', primary: true, default: uuid }, // TODO: should be type: 'uuid' but simplified for our tests + name: { type: 'string', disallow: [null], min: 3 }, + slug: { type: 'string', unique: 'true', regex: /^[a-z0-9_-]+$/, lowercase: true }, + avatar: { + type: 'relationship', + relationship: 'AVATAR_IMAGE', + target: 'Image', + direction: 'out', + }, + deleted: { type: 'boolean', default: false }, + disabled: { type: 'boolean', default: false }, + wasSeeded: 'boolean', // Wolle: used or needed? + locationName: { type: 'string', allow: [null] }, + about: { type: 'string', allow: [null, ''] }, // Wolle: null? + description: { type: 'string', allow: [null, ''] }, // Wolle: null? HTML with Tiptap, similar to post content + // Wolle: followedBy: { + // type: 'relationship', + // relationship: 'FOLLOWS', + // target: 'User', + // direction: 'in', + // properties: { + // createdAt: { type: 'string', isoDate: true, default: () => new Date().toISOString() }, + // }, + // }, + // Wolle: correct this way? + members: { type: 'relationship', + relationship: 'MEMBERS', + target: 'User', + direction: 'out' + }, + // Wolle: needed? lastActiveAt: { type: 'string', isoDate: true }, + createdAt: { + type: 'string', + isoDate: true, + default: () => new Date().toISOString() + }, + updatedAt: { + type: 'string', + isoDate: true, + required: true, + default: () => new Date().toISOString(), + }, + // Wolle: emoted: { + // type: 'relationships', + // relationship: 'EMOTED', + // target: 'Post', + // direction: 'out', + // properties: { + // emotion: { + // type: 'string', + // valid: ['happy', 'cry', 'surprised', 'angry', 'funny'], + // invalid: [null], + // }, + // }, + // eager: true, + // cascade: true, + // }, + // Wolle: blocked: { + // type: 'relationship', + // relationship: 'BLOCKED', + // target: 'User', + // direction: 'out', + // properties: { + // createdAt: { type: 'string', isoDate: true, default: () => new Date().toISOString() }, + // }, + // }, + // Wolle: muted: { + // type: 'relationship', + // relationship: 'MUTED', + // target: 'User', + // direction: 'out', + // properties: { + // createdAt: { type: 'string', isoDate: true, default: () => new Date().toISOString() }, + // }, + // }, + // Wolle: notifications: { + // type: 'relationship', + // relationship: 'NOTIFIED', + // target: 'User', + // direction: 'in', + // }, + // Wolle inviteCodes: { + // type: 'relationship', + // relationship: 'GENERATED', + // target: 'InviteCode', + // direction: 'out', + // }, + // Wolle: redeemedInviteCode: { + // type: 'relationship', + // relationship: 'REDEEMED', + // target: 'InviteCode', + // direction: 'out', + // }, + // Wolle: shouted: { + // type: 'relationship', + // relationship: 'SHOUTED', + // target: 'Post', + // direction: 'out', + // properties: { + // createdAt: { type: 'string', isoDate: true, default: () => new Date().toISOString() }, + // }, + // }, + isIn: { + type: 'relationship', + relationship: 'IS_IN', + target: 'Location', + direction: 'out', + }, + // Wolle: pinned: { + // type: 'relationship', + // relationship: 'PINNED', + // target: 'Post', + // direction: 'out', + // properties: { + // createdAt: { type: 'string', isoDate: true, default: () => new Date().toISOString() }, + // }, + // }, + // Wolle: showShoutsPublicly: { + // type: 'boolean', + // default: false, + // }, + // Wolle: sendNotificationEmails: { + // type: 'boolean', + // default: true, + // }, + // Wolle: locale: { + // type: 'string', + // allow: [null], + // }, +} diff --git a/backend/src/models/index.js b/backend/src/models/index.js index 8d6a021ab..d476e5f9b 100644 --- a/backend/src/models/index.js +++ b/backend/src/models/index.js @@ -4,6 +4,7 @@ export default { Image: require('./Image.js').default, Badge: require('./Badge.js').default, User: require('./User.js').default, + Group: require('./Group.js').default, EmailAddress: require('./EmailAddress.js').default, UnverifiedEmailAddress: require('./UnverifiedEmailAddress.js').default, SocialMedia: require('./SocialMedia.js').default, diff --git a/backend/src/schema/types/enum/GroupActionRadius.gql b/backend/src/schema/types/enum/GroupActionRadius.gql new file mode 100644 index 000000000..afc421133 --- /dev/null +++ b/backend/src/schema/types/enum/GroupActionRadius.gql @@ -0,0 +1,6 @@ +enum GroupActionRadius { + regional + national + continental + international +} diff --git a/backend/src/schema/types/enum/GroupType.gql b/backend/src/schema/types/enum/GroupType.gql new file mode 100644 index 000000000..2cf298474 --- /dev/null +++ b/backend/src/schema/types/enum/GroupType.gql @@ -0,0 +1,5 @@ +enum GroupType { + public + closed + hidden +} diff --git a/backend/src/schema/types/type/Group.gql b/backend/src/schema/types/type/Group.gql new file mode 100644 index 000000000..655a251d6 --- /dev/null +++ b/backend/src/schema/types/type/Group.gql @@ -0,0 +1,249 @@ +enum _GroupOrdering { + id_asc + id_desc + name_asc + name_desc + slug_asc + slug_desc + locationName_asc + locationName_desc + about_asc + about_desc + createdAt_asc + createdAt_desc + updatedAt_asc + updatedAt_desc + # Wolle: needed? locale_asc + # locale_desc +} + +type Group { + id: ID! + name: String # title + slug: String! + + createdAt: String + updatedAt: String + deleted: Boolean + disabled: Boolean + + avatar: Image @relation(name: "AVATAR_IMAGE", direction: "OUT") + + location: Location @cypher(statement: "MATCH (this)-[:IS_IN]->(l:Location) RETURN l") + locationName: String + about: String # goal + description: String + groupType: GroupType + actionRadius: GroupActionRadius + + categories: [Category] @relation(name: "CATEGORIZED", direction: "OUT") + + # Wolle: needed? + socialMedia: [SocialMedia]! @relation(name: "OWNED_BY", direction: "IN") + + # Wolle: showShoutsPublicly: Boolean + # Wolle: sendNotificationEmails: Boolean + # Wolle: needed? locale: String + members: [User]! @relation(name: "MEMBERS", direction: "OUT") + membersCount: Int! + @cypher(statement: "MATCH (this)-[:MEMBERS]->(r:User) RETURN COUNT(DISTINCT r)") + + # Wolle: followedBy: [User]! @relation(name: "FOLLOWS", direction: "IN") + # Wolle: followedByCount: Int! + # @cypher(statement: "MATCH (this)<-[:FOLLOWS]-(r:User) RETURN COUNT(DISTINCT r)") + + # Wolle: inviteCodes: [InviteCode] @relation(name: "GENERATED", direction: "OUT") + # Wolle: redeemedInviteCode: InviteCode @relation(name: "REDEEMED", direction: "OUT") + + # Is the currently logged in user following that user? + # Wolle: followedByCurrentUser: Boolean! + # @cypher( + # statement: """ + # MATCH (this)<-[:FOLLOWS]-(u:User { id: $cypherParams.currentUserId}) + # RETURN COUNT(u) >= 1 + # """ + # ) + + # Wolle: isBlocked: Boolean! + # @cypher( + # statement: """ + # MATCH (this)<-[:BLOCKED]-(user:User {id: $cypherParams.currentUserId}) + # RETURN COUNT(user) >= 1 + # """ + # ) + # Wolle: blocked: Boolean! + # @cypher( + # statement: """ + # MATCH (this)-[:BLOCKED]-(user:User {id: $cypherParams.currentUserId}) + # RETURN COUNT(user) >= 1 + # """ + # ) + + # Wolle: isMuted: Boolean! + # @cypher( + # statement: """ + # MATCH (this)<-[:MUTED]-(user:User { id: $cypherParams.currentUserId}) + # RETURN COUNT(user) >= 1 + # """ + # ) + + # contributions: [WrittenPost]! + # contributions2(first: Int = 10, offset: Int = 0): [WrittenPost2]! + # @cypher( + # statement: "MATCH (this)-[w:WROTE]->(p:Post) RETURN p as Post, w.timestamp as timestamp" + # ) + # Wolle: needed? + # contributions: [Post]! @relation(name: "WROTE", direction: "OUT") + # contributionsCount: Int! + # @cypher( + # statement: """ + # MATCH (this)-[:WROTE]->(r:Post) + # WHERE NOT r.deleted = true AND NOT r.disabled = true + # RETURN COUNT(r) + # """ + # ) + + # Wolle: comments: [Comment]! @relation(name: "WROTE", direction: "OUT") + # commentedCount: Int! + # @cypher( + # statement: "MATCH (this)-[:WROTE]->(:Comment)-[:COMMENTS]->(p:Post) WHERE NOT p.deleted = true AND NOT p.disabled = true RETURN COUNT(DISTINCT(p))" + # ) + + # Wolle: 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)" + # ) + + # Wolle: badges: [Badge]! @relation(name: "REWARDED", direction: "IN") + # badgesCount: Int! @cypher(statement: "MATCH (this)<-[:REWARDED]-(r:Badge) RETURN COUNT(r)") + + # Wolle: emotions: [EMOTED] +} + + +input _GroupFilter { + AND: [_GroupFilter!] + OR: [_GroupFilter!] + name_contains: String + about_contains: String + description_contains: String + slug_contains: String + id: ID + id_not: ID + id_in: [ID!] + id_not_in: [ID!] + # Wolle: + # friends: _GroupFilter + # friends_not: _GroupFilter + # friends_in: [_GroupFilter!] + # friends_not_in: [_GroupFilter!] + # friends_some: _GroupFilter + # friends_none: _GroupFilter + # friends_single: _GroupFilter + # friends_every: _GroupFilter + # following: _GroupFilter + # following_not: _GroupFilter + # following_in: [_GroupFilter!] + # following_not_in: [_GroupFilter!] + # following_some: _GroupFilter + # following_none: _GroupFilter + # following_single: _GroupFilter + # following_every: _GroupFilter + # followedBy: _GroupFilter + # followedBy_not: _GroupFilter + # followedBy_in: [_GroupFilter!] + # followedBy_not_in: [_GroupFilter!] + # followedBy_some: _GroupFilter + # followedBy_none: _GroupFilter + # followedBy_single: _GroupFilter + # followedBy_every: _GroupFilter + # role_in: [UserGroup!] +} + +type Query { + Group( + id: ID + email: String # admins need to search for a user sometimes + name: String + slug: String + locationName: String + about: String + description: String + createdAt: String + updatedAt: String + first: Int + offset: Int + orderBy: [_GroupOrdering] + filter: _GroupFilter + ): [Group] + + availableGroupTypes: [GroupType]! + + # Wolle: + # availableRoles: [UserGroup]! + # mutedUsers: [User] + # blockedUsers: [User] + # isLoggedIn: Boolean! + # currentUser: User + # findUsers(query: String!,limit: Int = 10, filter: _GroupFilter): [User]! + # @cypher( + # statement: """ + # CALL db.index.fulltext.queryNodes('user_fulltext_search', $query) + # YIELD node as post, score + # MATCH (user) + # WHERE score >= 0.2 + # AND NOT user.deleted = true AND NOT user.disabled = true + # RETURN user + # LIMIT $limit + # """ + # ) +} + +# Wolle: enum Deletable { +# Post +# Comment +# } + +type Mutation { + CreateGroup ( + id: ID! + name: String + email: String + slug: String + avatar: ImageInput + locationName: String + about: String + description: String + # Wolle: add group settings + # Wolle: + # showShoutsPublicly: Boolean + # sendNotificationEmails: Boolean + # locale: String + ): Group + + UpdateUser ( + id: ID! + name: String + email: String + slug: String + avatar: ImageInput + locationName: String + about: String + description: String + # Wolle: + # showShoutsPublicly: Boolean + # sendNotificationEmails: Boolean + # locale: String + ): Group + + DeleteGroup(id: ID!): Group + + # Wolle: + # muteUser(id: ID!): User + # unmuteUser(id: ID!): User + # blockUser(id: ID!): User + # unblockUser(id: ID!): User + + # Wolle: switchUserRole(role: UserGroup!, id: ID!): User +} diff --git a/backend/src/schema/types/type/User.gql b/backend/src/schema/types/type/User.gql index 772dedf6b..aca08df0e 100644 --- a/backend/src/schema/types/type/User.gql +++ b/backend/src/schema/types/type/User.gql @@ -156,19 +156,19 @@ input _UserFilter { type Query { User( - id: ID - email: String # admins need to search for a user sometimes - name: String - slug: String - role: UserGroup - locationName: String - about: String - createdAt: String - updatedAt: String - first: Int - offset: Int - orderBy: [_UserOrdering] - filter: _UserFilter + id: ID + email: String # admins need to search for a user sometimes + name: String + slug: String + role: UserGroup + locationName: String + about: String + createdAt: String + updatedAt: String + first: Int + offset: Int + orderBy: [_UserOrdering] + filter: _UserFilter ): [User] availableRoles: [UserGroup]! @@ -197,19 +197,19 @@ enum Deletable { type Mutation { UpdateUser ( - id: ID! - name: String - email: String - slug: String - avatar: ImageInput - locationName: String - about: String - termsAndConditionsAgreedVersion: String - termsAndConditionsAgreedAt: String - allowEmbedIframes: Boolean - showShoutsPublicly: Boolean - sendNotificationEmails: Boolean - locale: String + id: ID! + name: String + email: String + slug: String + avatar: ImageInput + locationName: String + about: String + termsAndConditionsAgreedVersion: String + termsAndConditionsAgreedAt: String + allowEmbedIframes: Boolean + showShoutsPublicly: Boolean + sendNotificationEmails: Boolean + locale: String ): User DeleteUser(id: ID!, resource: [Deletable]): User