Implement GQL for groups

This commit is contained in:
Wolfgang Huß 2022-08-02 08:26:14 +02:00
parent 7e7bbe769e
commit bf9dd205ca
7 changed files with 422 additions and 27 deletions

View File

@ -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)),
)

View File

@ -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],
// },
}

View File

@ -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,

View File

@ -0,0 +1,6 @@
enum GroupActionRadius {
regional
national
continental
international
}

View File

@ -0,0 +1,5 @@
enum GroupType {
public
closed
hidden
}

View File

@ -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
}

View File

@ -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