Merge branch '5059-groups/5131-implement-update-group-resolver' of github.com:Ocelot-Social-Community/Ocelot-Social into 5059-groups/5190-group-profile
# Conflicts: # backend/src/schema/resolvers/groups.spec.js
@ -1,3 +1,102 @@
|
||||
// this file is duplicated in `backend/src/constants/metadata.js` and `webapp/constants/metadata.js`
|
||||
export const CATEGORIES_MIN = 1
|
||||
export const CATEGORIES_MAX = 3
|
||||
|
||||
export const categories = [
|
||||
{
|
||||
icon: 'users',
|
||||
name: 'networking',
|
||||
description: 'Kooperation, Aktionsbündnisse, Solidarität, Hilfe',
|
||||
},
|
||||
{
|
||||
icon: 'home',
|
||||
name: 'home',
|
||||
description: 'Bauen, Lebensgemeinschaften, Tiny Houses, Gemüsegarten',
|
||||
},
|
||||
{
|
||||
icon: 'lightbulb',
|
||||
name: 'energy',
|
||||
description: 'Öl, Gas, Kohle, Wind, Wasserkraft, Biogas, Atomenergie, ...',
|
||||
},
|
||||
{
|
||||
icon: 'smile',
|
||||
name: 'psyche',
|
||||
description: 'Seele, Gefühle, Glück',
|
||||
},
|
||||
{
|
||||
icon: 'movement',
|
||||
name: 'body-and-excercise',
|
||||
description: 'Sport, Yoga, Massage, Tanzen, Entspannung',
|
||||
},
|
||||
{
|
||||
icon: 'balance-scale',
|
||||
name: 'law',
|
||||
description: 'Menschenrechte, Gesetze, Verordnungen',
|
||||
},
|
||||
{
|
||||
icon: 'money',
|
||||
name: 'finance',
|
||||
description: 'Geld, Finanzsystem, Alternativwährungen, ...',
|
||||
},
|
||||
{
|
||||
icon: 'child',
|
||||
name: 'children',
|
||||
description: 'Familie, Pädagogik, Schule, Prägung',
|
||||
},
|
||||
{
|
||||
icon: 'suitcase',
|
||||
name: 'mobility',
|
||||
description: 'Reise, Verkehr, Elektromobilität',
|
||||
},
|
||||
{
|
||||
icon: 'shopping-cart',
|
||||
name: 'economy',
|
||||
description: 'Handel, Konsum, Marketing, Lebensmittel, Lieferketten, ...',
|
||||
},
|
||||
{
|
||||
icon: 'angellist',
|
||||
name: 'peace',
|
||||
description: 'Krieg, Militär, soziale Verteidigung, Waffen, Cyberattacken',
|
||||
},
|
||||
{
|
||||
icon: 'university',
|
||||
name: 'politics',
|
||||
description: 'Demokratie, Mitbestimmung, Wahlen, Korruption, Parteien',
|
||||
},
|
||||
{
|
||||
icon: 'tree',
|
||||
name: 'nature',
|
||||
description: 'Tiere, Pflanzen, Landwirtschaft, Ökologie, Artenvielfalt',
|
||||
},
|
||||
{
|
||||
icon: 'graduation-cap',
|
||||
name: 'science',
|
||||
description: 'Bildung, Hochschule, Publikationen, ...',
|
||||
},
|
||||
{
|
||||
icon: 'medkit',
|
||||
name: 'health',
|
||||
description: 'Medizin, Ernährung, WHO, Impfungen, Schadstoffe, ...',
|
||||
},
|
||||
{
|
||||
icon: 'desktop',
|
||||
name: 'it-and-media',
|
||||
description:
|
||||
'Nachrichten, Manipulation, Datenschutz, Überwachung, Datenkraken, AI, Software, Apps',
|
||||
},
|
||||
{
|
||||
icon: 'heart-o',
|
||||
name: 'spirituality',
|
||||
description: 'Religion, Werte, Ethik',
|
||||
},
|
||||
{
|
||||
icon: 'music',
|
||||
name: 'culture',
|
||||
description: 'Kunst, Theater, Musik, Fotografie, Film',
|
||||
},
|
||||
{
|
||||
icon: 'ellipsis-h',
|
||||
name: 'miscellaneous',
|
||||
description: '',
|
||||
},
|
||||
]
|
||||
|
||||
@ -1,2 +1,3 @@
|
||||
// this file is duplicated in `backend/src/constants/group.js` and `webapp/constants/group.js`
|
||||
export const DESCRIPTION_WITHOUT_HTML_LENGTH_MIN = 100 // with removed HTML tags
|
||||
export const DESCRIPTION_EXCERPT_HTML_LENGTH = 120 // with removed HTML tags
|
||||
|
||||
@ -12,6 +12,7 @@ export const createGroupMutation = gql`
|
||||
$groupType: GroupType!
|
||||
$actionRadius: GroupActionRadius!
|
||||
$categoryIds: [ID]
|
||||
$locationName: String
|
||||
) {
|
||||
CreateGroup(
|
||||
id: $id
|
||||
@ -22,6 +23,7 @@ export const createGroupMutation = gql`
|
||||
groupType: $groupType
|
||||
actionRadius: $actionRadius
|
||||
categoryIds: $categoryIds
|
||||
locationName: $locationName
|
||||
) {
|
||||
id
|
||||
name
|
||||
@ -34,11 +36,87 @@ export const createGroupMutation = gql`
|
||||
description
|
||||
groupType
|
||||
actionRadius
|
||||
categories {
|
||||
id
|
||||
slug
|
||||
name
|
||||
icon
|
||||
}
|
||||
# locationName # test this as result
|
||||
myRole
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
export const updateGroupMutation = gql`
|
||||
mutation (
|
||||
$id: ID!
|
||||
$name: String
|
||||
$slug: String
|
||||
$about: String
|
||||
$description: String
|
||||
$actionRadius: GroupActionRadius
|
||||
$categoryIds: [ID]
|
||||
$avatar: ImageInput
|
||||
$locationName: String
|
||||
) {
|
||||
UpdateGroup(
|
||||
id: $id
|
||||
name: $name
|
||||
slug: $slug
|
||||
about: $about
|
||||
description: $description
|
||||
actionRadius: $actionRadius
|
||||
categoryIds: $categoryIds
|
||||
avatar: $avatar
|
||||
locationName: $locationName
|
||||
) {
|
||||
id
|
||||
name
|
||||
slug
|
||||
createdAt
|
||||
updatedAt
|
||||
disabled
|
||||
deleted
|
||||
about
|
||||
description
|
||||
groupType
|
||||
actionRadius
|
||||
categories {
|
||||
id
|
||||
slug
|
||||
name
|
||||
icon
|
||||
}
|
||||
# avatar # test this as result
|
||||
# locationName # test this as result
|
||||
myRole
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
export const joinGroupMutation = gql`
|
||||
mutation ($groupId: ID!, $userId: ID!) {
|
||||
JoinGroup(groupId: $groupId, userId: $userId) {
|
||||
id
|
||||
name
|
||||
slug
|
||||
myRoleInGroup
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
export const changeGroupMemberRoleMutation = gql`
|
||||
mutation ($groupId: ID!, $userId: ID!, $roleInGroup: GroupMemberRole!) {
|
||||
ChangeGroupMemberRole(groupId: $groupId, userId: $userId, roleInGroup: $roleInGroup) {
|
||||
id
|
||||
name
|
||||
slug
|
||||
myRoleInGroup
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
// ------ queries
|
||||
|
||||
export const groupQuery = gql`
|
||||
@ -90,6 +168,19 @@ export const groupQuery = gql`
|
||||
name
|
||||
icon
|
||||
}
|
||||
# avatar # test this as result
|
||||
# locationName # test this as result
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
export const groupMembersQuery = gql`
|
||||
query ($id: ID!, $first: Int, $offset: Int, $orderBy: [_UserOrdering], $filter: _UserFilter) {
|
||||
GroupMembers(id: $id, first: $first, offset: $offset, orderBy: $orderBy, filter: $filter) {
|
||||
id
|
||||
name
|
||||
slug
|
||||
myRoleInGroup
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
@ -1,6 +1,8 @@
|
||||
import { getDriver, getNeode } from '../../db/neo4j'
|
||||
import { hashSync } from 'bcryptjs'
|
||||
import { v4 as uuid } from 'uuid'
|
||||
import { categories } from '../../constants/categories'
|
||||
import CONFIG from '../../config'
|
||||
|
||||
const defaultAdmin = {
|
||||
email: 'admin@example.org',
|
||||
@ -10,6 +12,29 @@ const defaultAdmin = {
|
||||
slug: 'admin',
|
||||
}
|
||||
|
||||
const createCategories = async (session) => {
|
||||
const createCategoriesTxResultPromise = session.writeTransaction(async (txc) => {
|
||||
categories.forEach(({ icon, name }, index) => {
|
||||
const id = `cat${index + 1}`
|
||||
txc.run(
|
||||
`MERGE (c:Category {
|
||||
icon: "${icon}",
|
||||
slug: "${name}",
|
||||
name: "${name}",
|
||||
id: "${id}",
|
||||
createdAt: toString(datetime())
|
||||
})`,
|
||||
)
|
||||
})
|
||||
})
|
||||
try {
|
||||
await createCategoriesTxResultPromise
|
||||
console.log('Successfully created categories!') // eslint-disable-line no-console
|
||||
} catch (error) {
|
||||
console.log(`Error creating categories: ${error}`) // eslint-disable-line no-console
|
||||
}
|
||||
}
|
||||
|
||||
const createDefaultAdminUser = async (session) => {
|
||||
const readTxResultPromise = session.readTransaction(async (txc) => {
|
||||
const result = await txc.run('MATCH (user:User) RETURN count(user) AS userCount')
|
||||
@ -45,7 +70,7 @@ const createDefaultAdminUser = async (session) => {
|
||||
})
|
||||
try {
|
||||
await createAdminTxResultPromise
|
||||
console.log('Successfully created default admin user') // eslint-disable-line no-console
|
||||
console.log('Successfully created default admin user!') // eslint-disable-line no-console
|
||||
} catch (error) {
|
||||
console.log(error) // eslint-disable-line no-console
|
||||
}
|
||||
@ -58,6 +83,7 @@ class Store {
|
||||
const { driver } = neode
|
||||
const session = driver.session()
|
||||
await createDefaultAdminUser(session)
|
||||
if (CONFIG.CATEGORIES_ACTIVE) await createCategories(session)
|
||||
const writeTxResultPromise = session.writeTransaction(async (txc) => {
|
||||
await txc.run('CALL apoc.schema.assert({},{},true)') // drop all indices and contraints
|
||||
return Promise.all(
|
||||
|
||||
@ -5,9 +5,14 @@ import createServer from '../server'
|
||||
import faker from '@faker-js/faker'
|
||||
import Factory from '../db/factories'
|
||||
import { getNeode, getDriver } from '../db/neo4j'
|
||||
import { createGroupMutation } from './graphql/groups'
|
||||
import {
|
||||
createGroupMutation,
|
||||
joinGroupMutation,
|
||||
changeGroupMemberRoleMutation,
|
||||
} from './graphql/groups'
|
||||
import { createPostMutation } from './graphql/posts'
|
||||
import { createCommentMutation } from './graphql/comments'
|
||||
import { categories } from '../constants/categories'
|
||||
|
||||
if (CONFIG.PRODUCTION && !CONFIG.PRODUCTION_DB_CLEAN_ALLOW) {
|
||||
throw new Error(`You cannot seed the database in a non-staging and real production environment!`)
|
||||
@ -269,104 +274,16 @@ const languages = ['de', 'en', 'es', 'fr', 'it', 'pt', 'pl']
|
||||
dagobert.relateTo(louie, 'blocked'),
|
||||
])
|
||||
|
||||
await Promise.all([
|
||||
Factory.build('category', {
|
||||
id: 'cat1',
|
||||
name: 'Just For Fun',
|
||||
slug: 'just-for-fun',
|
||||
icon: 'smile',
|
||||
await Promise.all(
|
||||
categories.map(({ icon, name }, index) => {
|
||||
Factory.build('category', {
|
||||
id: `cat${index + 1}`,
|
||||
slug: name,
|
||||
name,
|
||||
icon,
|
||||
})
|
||||
}),
|
||||
Factory.build('category', {
|
||||
id: 'cat2',
|
||||
name: 'Happiness & Values',
|
||||
slug: 'happiness-values',
|
||||
icon: 'heart-o',
|
||||
}),
|
||||
Factory.build('category', {
|
||||
id: 'cat3',
|
||||
name: 'Health & Wellbeing',
|
||||
slug: 'health-wellbeing',
|
||||
icon: 'medkit',
|
||||
}),
|
||||
Factory.build('category', {
|
||||
id: 'cat4',
|
||||
name: 'Environment & Nature',
|
||||
slug: 'environment-nature',
|
||||
icon: 'tree',
|
||||
}),
|
||||
Factory.build('category', {
|
||||
id: 'cat5',
|
||||
name: 'Animal Protection',
|
||||
slug: 'animal-protection',
|
||||
icon: 'paw',
|
||||
}),
|
||||
Factory.build('category', {
|
||||
id: 'cat6',
|
||||
name: 'Human Rights & Justice',
|
||||
slug: 'human-rights-justice',
|
||||
icon: 'balance-scale',
|
||||
}),
|
||||
Factory.build('category', {
|
||||
id: 'cat7',
|
||||
name: 'Education & Sciences',
|
||||
slug: 'education-sciences',
|
||||
icon: 'graduation-cap',
|
||||
}),
|
||||
Factory.build('category', {
|
||||
id: 'cat8',
|
||||
name: 'Cooperation & Development',
|
||||
slug: 'cooperation-development',
|
||||
icon: 'users',
|
||||
}),
|
||||
Factory.build('category', {
|
||||
id: 'cat9',
|
||||
name: 'Democracy & Politics',
|
||||
slug: 'democracy-politics',
|
||||
icon: 'university',
|
||||
}),
|
||||
Factory.build('category', {
|
||||
id: 'cat10',
|
||||
name: 'Economy & Finances',
|
||||
slug: 'economy-finances',
|
||||
icon: 'money',
|
||||
}),
|
||||
Factory.build('category', {
|
||||
id: 'cat11',
|
||||
name: 'Energy & Technology',
|
||||
slug: 'energy-technology',
|
||||
icon: 'flash',
|
||||
}),
|
||||
Factory.build('category', {
|
||||
id: 'cat12',
|
||||
name: 'IT, Internet & Data Privacy',
|
||||
slug: 'it-internet-data-privacy',
|
||||
icon: 'mouse-pointer',
|
||||
}),
|
||||
Factory.build('category', {
|
||||
id: 'cat13',
|
||||
name: 'Art, Culture & Sport',
|
||||
slug: 'art-culture-sport',
|
||||
icon: 'paint-brush',
|
||||
}),
|
||||
Factory.build('category', {
|
||||
id: 'cat14',
|
||||
name: 'Freedom of Speech',
|
||||
slug: 'freedom-of-speech',
|
||||
icon: 'bullhorn',
|
||||
}),
|
||||
Factory.build('category', {
|
||||
id: 'cat15',
|
||||
name: 'Consumption & Sustainability',
|
||||
slug: 'consumption-sustainability',
|
||||
icon: 'shopping-cart',
|
||||
}),
|
||||
Factory.build('category', {
|
||||
id: 'cat16',
|
||||
name: 'Global Peace & Nonviolence',
|
||||
slug: 'global-peace-nonviolence',
|
||||
icon: 'angellist',
|
||||
}),
|
||||
])
|
||||
)
|
||||
|
||||
const [environment, nature, democracy, freedom] = await Promise.all([
|
||||
Factory.build('tag', {
|
||||
@ -400,6 +317,62 @@ const languages = ['de', 'en', 'es', 'fr', 'it', 'pt', 'pl']
|
||||
},
|
||||
}),
|
||||
])
|
||||
await Promise.all([
|
||||
mutate({
|
||||
mutation: joinGroupMutation,
|
||||
variables: {
|
||||
groupId: 'g0',
|
||||
userId: 'u2',
|
||||
},
|
||||
}),
|
||||
mutate({
|
||||
mutation: joinGroupMutation,
|
||||
variables: {
|
||||
groupId: 'g0',
|
||||
userId: 'u3',
|
||||
},
|
||||
}),
|
||||
mutate({
|
||||
mutation: joinGroupMutation,
|
||||
variables: {
|
||||
groupId: 'g0',
|
||||
userId: 'u4',
|
||||
},
|
||||
}),
|
||||
mutate({
|
||||
mutation: joinGroupMutation,
|
||||
variables: {
|
||||
groupId: 'g0',
|
||||
userId: 'u6',
|
||||
},
|
||||
}),
|
||||
])
|
||||
await Promise.all([
|
||||
mutate({
|
||||
mutation: changeGroupMemberRoleMutation,
|
||||
variables: {
|
||||
groupId: 'g0',
|
||||
userId: 'u2',
|
||||
roleInGroup: 'usual',
|
||||
},
|
||||
}),
|
||||
mutate({
|
||||
mutation: changeGroupMemberRoleMutation,
|
||||
variables: {
|
||||
groupId: 'g0',
|
||||
userId: 'u4',
|
||||
roleInGroup: 'admin',
|
||||
},
|
||||
}),
|
||||
mutate({
|
||||
mutation: changeGroupMemberRoleMutation,
|
||||
variables: {
|
||||
groupId: 'g0',
|
||||
userId: 'u3',
|
||||
roleInGroup: 'owner',
|
||||
},
|
||||
}),
|
||||
])
|
||||
|
||||
authenticatedUser = await jennyRostock.toJson()
|
||||
await Promise.all([
|
||||
@ -416,6 +389,77 @@ const languages = ['de', 'en', 'es', 'fr', 'it', 'pt', 'pl']
|
||||
},
|
||||
}),
|
||||
])
|
||||
await Promise.all([
|
||||
mutate({
|
||||
mutation: joinGroupMutation,
|
||||
variables: {
|
||||
groupId: 'g1',
|
||||
userId: 'u1',
|
||||
},
|
||||
}),
|
||||
mutate({
|
||||
mutation: joinGroupMutation,
|
||||
variables: {
|
||||
groupId: 'g1',
|
||||
userId: 'u2',
|
||||
},
|
||||
}),
|
||||
mutate({
|
||||
mutation: joinGroupMutation,
|
||||
variables: {
|
||||
groupId: 'g1',
|
||||
userId: 'u5',
|
||||
},
|
||||
}),
|
||||
mutate({
|
||||
mutation: joinGroupMutation,
|
||||
variables: {
|
||||
groupId: 'g1',
|
||||
userId: 'u6',
|
||||
},
|
||||
}),
|
||||
mutate({
|
||||
mutation: joinGroupMutation,
|
||||
variables: {
|
||||
groupId: 'g1',
|
||||
userId: 'u7',
|
||||
},
|
||||
}),
|
||||
])
|
||||
await Promise.all([
|
||||
mutate({
|
||||
mutation: changeGroupMemberRoleMutation,
|
||||
variables: {
|
||||
groupId: 'g0',
|
||||
userId: 'u1',
|
||||
roleInGroup: 'usual',
|
||||
},
|
||||
}),
|
||||
mutate({
|
||||
mutation: changeGroupMemberRoleMutation,
|
||||
variables: {
|
||||
groupId: 'g0',
|
||||
userId: 'u2',
|
||||
roleInGroup: 'usual',
|
||||
},
|
||||
}),
|
||||
mutate({
|
||||
mutation: changeGroupMemberRoleMutation,
|
||||
variables: {
|
||||
groupId: 'g0',
|
||||
userId: 'u5',
|
||||
roleInGroup: 'admin',
|
||||
},
|
||||
}),
|
||||
mutate({
|
||||
mutation: changeGroupMemberRoleMutation,
|
||||
variables: {
|
||||
groupId: 'g0',
|
||||
userId: 'u6',
|
||||
roleInGroup: 'owner',
|
||||
},
|
||||
}),
|
||||
])
|
||||
|
||||
authenticatedUser = await bobDerBaumeister.toJson()
|
||||
await Promise.all([
|
||||
@ -432,6 +476,62 @@ const languages = ['de', 'en', 'es', 'fr', 'it', 'pt', 'pl']
|
||||
},
|
||||
}),
|
||||
])
|
||||
await Promise.all([
|
||||
mutate({
|
||||
mutation: joinGroupMutation,
|
||||
variables: {
|
||||
groupId: 'g2',
|
||||
userId: 'u4',
|
||||
},
|
||||
}),
|
||||
mutate({
|
||||
mutation: joinGroupMutation,
|
||||
variables: {
|
||||
groupId: 'g2',
|
||||
userId: 'u5',
|
||||
},
|
||||
}),
|
||||
mutate({
|
||||
mutation: joinGroupMutation,
|
||||
variables: {
|
||||
groupId: 'g2',
|
||||
userId: 'u6',
|
||||
},
|
||||
}),
|
||||
mutate({
|
||||
mutation: joinGroupMutation,
|
||||
variables: {
|
||||
groupId: 'g2',
|
||||
userId: 'u7',
|
||||
},
|
||||
}),
|
||||
])
|
||||
await Promise.all([
|
||||
mutate({
|
||||
mutation: changeGroupMemberRoleMutation,
|
||||
variables: {
|
||||
groupId: 'g0',
|
||||
userId: 'u4',
|
||||
roleInGroup: 'usual',
|
||||
},
|
||||
}),
|
||||
mutate({
|
||||
mutation: changeGroupMemberRoleMutation,
|
||||
variables: {
|
||||
groupId: 'g0',
|
||||
userId: 'u5',
|
||||
roleInGroup: 'usual',
|
||||
},
|
||||
}),
|
||||
mutate({
|
||||
mutation: changeGroupMemberRoleMutation,
|
||||
variables: {
|
||||
groupId: 'g0',
|
||||
userId: 'u6',
|
||||
roleInGroup: 'usual',
|
||||
},
|
||||
}),
|
||||
])
|
||||
|
||||
// Create Posts
|
||||
|
||||
|
||||
@ -7,3 +7,12 @@
|
||||
export function gql(strings) {
|
||||
return strings.join('')
|
||||
}
|
||||
|
||||
// sometime we have to wait to check a db state by having a look into the db in a certain moment
|
||||
// or we wait a bit to check if we missed to set an await somewhere
|
||||
// see: https://www.sitepoint.com/delay-sleep-pause-wait/
|
||||
export function sleep(ms) {
|
||||
return new Promise((resolve) => setTimeout(resolve, ms))
|
||||
}
|
||||
// usage – 4 seconds for example
|
||||
// await sleep(4 * 1000)
|
||||
|
||||
@ -1,9 +1,14 @@
|
||||
import trunc from 'trunc-html'
|
||||
import { DESCRIPTION_EXCERPT_HTML_LENGTH } from '../constants/groups'
|
||||
|
||||
export default {
|
||||
Mutation: {
|
||||
CreateGroup: async (resolve, root, args, context, info) => {
|
||||
args.descriptionExcerpt = trunc(args.description, 120).html
|
||||
args.descriptionExcerpt = trunc(args.description, DESCRIPTION_EXCERPT_HTML_LENGTH).html
|
||||
return resolve(root, args, context, info)
|
||||
},
|
||||
UpdateGroup: async (resolve, root, args, context, info) => {
|
||||
args.descriptionExcerpt = trunc(args.description, DESCRIPTION_EXCERPT_HTML_LENGTH).html
|
||||
return resolve(root, args, context, info)
|
||||
},
|
||||
CreatePost: async (resolve, root, args, context, info) => {
|
||||
|
||||
@ -52,6 +52,145 @@ const isMySocialMedia = rule({
|
||||
return socialMedia.ownedBy.node.id === user.id
|
||||
})
|
||||
|
||||
const isAllowedToChangeGroupSettings = rule({
|
||||
cache: 'no_cache',
|
||||
})(async (_parent, args, { user, driver }) => {
|
||||
if (!(user && user.id)) return false
|
||||
const ownerId = user.id
|
||||
const { id: groupId } = args
|
||||
const session = driver.session()
|
||||
const readTxPromise = session.readTransaction(async (transaction) => {
|
||||
const transactionResponse = await transaction.run(
|
||||
`
|
||||
MATCH (owner:User {id: $ownerId})-[membership:MEMBER_OF]->(group:Group {id: $groupId})
|
||||
RETURN group {.*}, owner {.*, myRoleInGroup: membership.role}
|
||||
`,
|
||||
{ groupId, ownerId },
|
||||
)
|
||||
return {
|
||||
owner: transactionResponse.records.map((record) => record.get('owner'))[0],
|
||||
group: transactionResponse.records.map((record) => record.get('group'))[0],
|
||||
}
|
||||
})
|
||||
try {
|
||||
const { owner, group } = await readTxPromise
|
||||
return !!group && !!owner && ['owner'].includes(owner.myRoleInGroup)
|
||||
} catch (error) {
|
||||
throw new Error(error)
|
||||
} finally {
|
||||
session.close()
|
||||
}
|
||||
})
|
||||
|
||||
const isAllowedSeeingMembersOfGroup = rule({
|
||||
cache: 'no_cache',
|
||||
})(async (_parent, args, { user, driver }) => {
|
||||
if (!(user && user.id)) return false
|
||||
const { id: groupId } = args
|
||||
const session = driver.session()
|
||||
const readTxPromise = session.readTransaction(async (transaction) => {
|
||||
const transactionResponse = await transaction.run(
|
||||
`
|
||||
MATCH (group:Group {id: $groupId})
|
||||
OPTIONAL MATCH (member:User {id: $userId})-[membership:MEMBER_OF]->(group)
|
||||
RETURN group {.*}, member {.*, myRoleInGroup: membership.role}
|
||||
`,
|
||||
{ groupId, userId: user.id },
|
||||
)
|
||||
return {
|
||||
member: transactionResponse.records.map((record) => record.get('member'))[0],
|
||||
group: transactionResponse.records.map((record) => record.get('group'))[0],
|
||||
}
|
||||
})
|
||||
try {
|
||||
const { member, group } = await readTxPromise
|
||||
return (
|
||||
!!group &&
|
||||
(group.groupType === 'public' ||
|
||||
(['closed', 'hidden'].includes(group.groupType) &&
|
||||
!!member &&
|
||||
['usual', 'admin', 'owner'].includes(member.myRoleInGroup)))
|
||||
)
|
||||
} catch (error) {
|
||||
throw new Error(error)
|
||||
} finally {
|
||||
session.close()
|
||||
}
|
||||
})
|
||||
|
||||
const isAllowedToChangeGroupMemberRole = rule({
|
||||
cache: 'no_cache',
|
||||
})(async (_parent, args, { user, driver }) => {
|
||||
if (!(user && user.id)) return false
|
||||
const adminId = user.id
|
||||
const { groupId, userId, roleInGroup } = args
|
||||
if (adminId === userId) return false
|
||||
const session = driver.session()
|
||||
const readTxPromise = session.readTransaction(async (transaction) => {
|
||||
const transactionResponse = await transaction.run(
|
||||
`
|
||||
MATCH (admin:User {id: $adminId})-[adminMembership:MEMBER_OF]->(group:Group {id: $groupId})
|
||||
OPTIONAL MATCH (group)<-[userMembership:MEMBER_OF]-(member:User {id: $userId})
|
||||
RETURN group {.*}, admin {.*, myRoleInGroup: adminMembership.role}, member {.*, myRoleInGroup: userMembership.role}
|
||||
`,
|
||||
{ groupId, adminId, userId },
|
||||
)
|
||||
return {
|
||||
admin: transactionResponse.records.map((record) => record.get('admin'))[0],
|
||||
group: transactionResponse.records.map((record) => record.get('group'))[0],
|
||||
member: transactionResponse.records.map((record) => record.get('member'))[0],
|
||||
}
|
||||
})
|
||||
try {
|
||||
const { admin, group, member } = await readTxPromise
|
||||
return (
|
||||
!!group &&
|
||||
!!admin &&
|
||||
(!member ||
|
||||
(!!member &&
|
||||
(member.myRoleInGroup === roleInGroup || !['owner'].includes(member.myRoleInGroup)))) &&
|
||||
((['admin'].includes(admin.myRoleInGroup) &&
|
||||
['pending', 'usual', 'admin'].includes(roleInGroup)) ||
|
||||
(['owner'].includes(admin.myRoleInGroup) &&
|
||||
['pending', 'usual', 'admin', 'owner'].includes(roleInGroup)))
|
||||
)
|
||||
} catch (error) {
|
||||
throw new Error(error)
|
||||
} finally {
|
||||
session.close()
|
||||
}
|
||||
})
|
||||
|
||||
const isAllowedToJoinGroup = rule({
|
||||
cache: 'no_cache',
|
||||
})(async (_parent, args, { user, driver }) => {
|
||||
if (!(user && user.id)) return false
|
||||
const { groupId, userId } = args
|
||||
const session = driver.session()
|
||||
const readTxPromise = session.readTransaction(async (transaction) => {
|
||||
const transactionResponse = await transaction.run(
|
||||
`
|
||||
MATCH (group:Group {id: $groupId})
|
||||
OPTIONAL MATCH (group)<-[membership:MEMBER_OF]-(member:User {id: $userId})
|
||||
RETURN group {.*}, member {.*, myRoleInGroup: membership.role}
|
||||
`,
|
||||
{ groupId, userId },
|
||||
)
|
||||
return {
|
||||
group: transactionResponse.records.map((record) => record.get('group'))[0],
|
||||
member: transactionResponse.records.map((record) => record.get('member'))[0],
|
||||
}
|
||||
})
|
||||
try {
|
||||
const { group, member } = await readTxPromise
|
||||
return !!group && (group.groupType !== 'hidden' || (!!member && !!member.myRoleInGroup))
|
||||
} catch (error) {
|
||||
throw new Error(error)
|
||||
} finally {
|
||||
session.close()
|
||||
}
|
||||
})
|
||||
|
||||
const isAuthor = rule({
|
||||
cache: 'no_cache',
|
||||
})(async (_parent, args, { user, driver }) => {
|
||||
@ -78,7 +217,7 @@ const isAuthor = rule({
|
||||
|
||||
const isDeletingOwnAccount = rule({
|
||||
cache: 'no_cache',
|
||||
})(async (parent, args, context, info) => {
|
||||
})(async (parent, args, context, _info) => {
|
||||
return context.user.id === args.id
|
||||
})
|
||||
|
||||
@ -115,6 +254,7 @@ export default shield(
|
||||
statistics: allow,
|
||||
currentUser: allow,
|
||||
Group: isAuthenticated,
|
||||
GroupMembers: isAllowedSeeingMembersOfGroup,
|
||||
Post: allow,
|
||||
profilePagePosts: allow,
|
||||
Comment: allow,
|
||||
@ -142,6 +282,9 @@ export default shield(
|
||||
SignupVerification: allow,
|
||||
UpdateUser: onlyYourself,
|
||||
CreateGroup: isAuthenticated,
|
||||
UpdateGroup: isAllowedToChangeGroupSettings,
|
||||
JoinGroup: isAllowedToJoinGroup,
|
||||
ChangeGroupMemberRole: isAllowedToChangeGroupMemberRole,
|
||||
CreatePost: isAuthenticated,
|
||||
UpdatePost: isAuthor,
|
||||
DeletePost: isAuthor,
|
||||
|
||||
@ -30,6 +30,10 @@ export default {
|
||||
args.slug = args.slug || (await uniqueSlug(args.name, isUniqueFor(context, 'Group')))
|
||||
return resolve(root, args, context, info)
|
||||
},
|
||||
UpdateGroup: async (resolve, root, args, context, info) => {
|
||||
args.slug = args.slug || (await uniqueSlug(args.name, isUniqueFor(context, 'Group')))
|
||||
return resolve(root, args, context, info)
|
||||
},
|
||||
CreatePost: async (resolve, root, args, context, info) => {
|
||||
args.slug = args.slug || (await uniqueSlug(args.title, isUniqueFor(context, 'Post')))
|
||||
return resolve(root, args, context, info)
|
||||
|
||||
@ -2,12 +2,13 @@ import { getNeode, getDriver } from '../db/neo4j'
|
||||
import createServer from '../server'
|
||||
import { createTestClient } from 'apollo-server-testing'
|
||||
import Factory, { cleanDatabase } from '../db/factories'
|
||||
import { createGroupMutation } from '../db/graphql/groups'
|
||||
import { createGroupMutation, updateGroupMutation } from '../db/graphql/groups'
|
||||
import { createPostMutation } from '../db/graphql/posts'
|
||||
import { signupVerificationMutation } from '../db/graphql/authentications'
|
||||
|
||||
let authenticatedUser
|
||||
let variables
|
||||
const categoryIds = ['cat9']
|
||||
|
||||
const driver = getDriver()
|
||||
const neode = getNeode()
|
||||
@ -62,8 +63,6 @@ afterEach(async () => {
|
||||
|
||||
describe('slugifyMiddleware', () => {
|
||||
describe('CreateGroup', () => {
|
||||
const categoryIds = ['cat9']
|
||||
|
||||
beforeEach(() => {
|
||||
variables = {
|
||||
...variables,
|
||||
@ -130,15 +129,14 @@ describe('slugifyMiddleware', () => {
|
||||
})
|
||||
|
||||
it('chooses another slug', async () => {
|
||||
variables = {
|
||||
...variables,
|
||||
name: 'Pre-Existing Group',
|
||||
about: 'As an about',
|
||||
}
|
||||
await expect(
|
||||
mutate({
|
||||
mutation: createGroupMutation,
|
||||
variables,
|
||||
variables: {
|
||||
...variables,
|
||||
name: 'Pre-Existing Group',
|
||||
about: 'As an about',
|
||||
},
|
||||
}),
|
||||
).resolves.toMatchObject({
|
||||
data: {
|
||||
@ -151,15 +149,17 @@ describe('slugifyMiddleware', () => {
|
||||
|
||||
describe('but if the client specifies a slug', () => {
|
||||
it('rejects CreateGroup', async (done) => {
|
||||
variables = {
|
||||
...variables,
|
||||
name: 'Pre-Existing Group',
|
||||
about: 'As an about',
|
||||
slug: 'pre-existing-group',
|
||||
}
|
||||
try {
|
||||
await expect(
|
||||
mutate({ mutation: createGroupMutation, variables }),
|
||||
mutate({
|
||||
mutation: createGroupMutation,
|
||||
variables: {
|
||||
...variables,
|
||||
name: 'Pre-Existing Group',
|
||||
about: 'As an about',
|
||||
slug: 'pre-existing-group',
|
||||
},
|
||||
}),
|
||||
).resolves.toMatchObject({
|
||||
errors: [
|
||||
{
|
||||
@ -189,9 +189,163 @@ describe('slugifyMiddleware', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('CreatePost', () => {
|
||||
const categoryIds = ['cat9']
|
||||
describe('UpdateGroup', () => {
|
||||
let createGroupResult
|
||||
|
||||
beforeEach(async () => {
|
||||
createGroupResult = await mutate({
|
||||
mutation: createGroupMutation,
|
||||
variables: {
|
||||
name: 'The Best Group',
|
||||
slug: 'the-best-group',
|
||||
about: 'Some about',
|
||||
description: 'Some description' + descriptionAdditional100,
|
||||
groupType: 'closed',
|
||||
actionRadius: 'national',
|
||||
categoryIds,
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
describe('if group exists', () => {
|
||||
describe('if new slug not(!) exists', () => {
|
||||
describe('setting slug by group name', () => {
|
||||
it('has the new slug', async () => {
|
||||
await expect(
|
||||
mutate({
|
||||
mutation: updateGroupMutation,
|
||||
variables: {
|
||||
id: createGroupResult.data.CreateGroup.id,
|
||||
name: 'My Best Group',
|
||||
},
|
||||
}),
|
||||
).resolves.toMatchObject({
|
||||
data: {
|
||||
UpdateGroup: {
|
||||
name: 'My Best Group',
|
||||
slug: 'my-best-group',
|
||||
about: 'Some about',
|
||||
description: 'Some description' + descriptionAdditional100,
|
||||
groupType: 'closed',
|
||||
actionRadius: 'national',
|
||||
myRole: 'owner',
|
||||
},
|
||||
},
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('setting slug explicitly', () => {
|
||||
it('has the new slug', async () => {
|
||||
await expect(
|
||||
mutate({
|
||||
mutation: updateGroupMutation,
|
||||
variables: {
|
||||
id: createGroupResult.data.CreateGroup.id,
|
||||
slug: 'my-best-group',
|
||||
},
|
||||
}),
|
||||
).resolves.toMatchObject({
|
||||
data: {
|
||||
UpdateGroup: {
|
||||
name: 'The Best Group',
|
||||
slug: 'my-best-group',
|
||||
about: 'Some about',
|
||||
description: 'Some description' + descriptionAdditional100,
|
||||
groupType: 'closed',
|
||||
actionRadius: 'national',
|
||||
myRole: 'owner',
|
||||
},
|
||||
},
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('if new slug exists in another group', () => {
|
||||
beforeEach(async () => {
|
||||
await mutate({
|
||||
mutation: createGroupMutation,
|
||||
variables: {
|
||||
name: 'Pre-Existing Group',
|
||||
slug: 'pre-existing-group',
|
||||
about: 'Some about',
|
||||
description: 'Some description' + descriptionAdditional100,
|
||||
groupType: 'closed',
|
||||
actionRadius: 'national',
|
||||
categoryIds,
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
describe('setting slug by group name', () => {
|
||||
it('has unique slug "*-1"', async () => {
|
||||
await expect(
|
||||
mutate({
|
||||
mutation: updateGroupMutation,
|
||||
variables: {
|
||||
id: createGroupResult.data.CreateGroup.id,
|
||||
name: 'Pre-Existing Group',
|
||||
},
|
||||
}),
|
||||
).resolves.toMatchObject({
|
||||
data: {
|
||||
UpdateGroup: {
|
||||
name: 'Pre-Existing Group',
|
||||
slug: 'pre-existing-group-1',
|
||||
about: 'Some about',
|
||||
description: 'Some description' + descriptionAdditional100,
|
||||
groupType: 'closed',
|
||||
actionRadius: 'national',
|
||||
myRole: 'owner',
|
||||
},
|
||||
},
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('setting slug explicitly', () => {
|
||||
it('rejects UpdateGroup', async (done) => {
|
||||
try {
|
||||
await expect(
|
||||
mutate({
|
||||
mutation: updateGroupMutation,
|
||||
variables: {
|
||||
id: createGroupResult.data.CreateGroup.id,
|
||||
slug: 'pre-existing-group',
|
||||
},
|
||||
}),
|
||||
).resolves.toMatchObject({
|
||||
errors: [
|
||||
{
|
||||
message: 'Group with this slug already exists!',
|
||||
},
|
||||
],
|
||||
})
|
||||
done()
|
||||
} catch (error) {
|
||||
throw new Error(`
|
||||
${error}
|
||||
|
||||
Probably your database has no unique constraints!
|
||||
|
||||
To see all constraints go to http://localhost:7474/browser/ and
|
||||
paste the following:
|
||||
\`\`\`
|
||||
CALL db.constraints();
|
||||
\`\`\`
|
||||
|
||||
Learn how to setup the database here:
|
||||
https://github.com/Ocelot-Social-Community/Ocelot-Social/blob/master/backend/README.md#database-indices-and-constraints
|
||||
`)
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('CreatePost', () => {
|
||||
beforeEach(() => {
|
||||
variables = {
|
||||
...variables,
|
||||
@ -252,16 +406,15 @@ describe('slugifyMiddleware', () => {
|
||||
})
|
||||
|
||||
it('chooses another slug', async () => {
|
||||
variables = {
|
||||
...variables,
|
||||
title: 'Pre-existing post',
|
||||
content: 'Some content',
|
||||
categoryIds,
|
||||
}
|
||||
await expect(
|
||||
mutate({
|
||||
mutation: createPostMutation,
|
||||
variables,
|
||||
variables: {
|
||||
...variables,
|
||||
title: 'Pre-existing post',
|
||||
content: 'Some content',
|
||||
categoryIds,
|
||||
},
|
||||
}),
|
||||
).resolves.toMatchObject({
|
||||
data: {
|
||||
@ -274,16 +427,18 @@ describe('slugifyMiddleware', () => {
|
||||
|
||||
describe('but if the client specifies a slug', () => {
|
||||
it('rejects CreatePost', async (done) => {
|
||||
variables = {
|
||||
...variables,
|
||||
title: 'Pre-existing post',
|
||||
content: 'Some content',
|
||||
slug: 'pre-existing-post',
|
||||
categoryIds,
|
||||
}
|
||||
try {
|
||||
await expect(
|
||||
mutate({ mutation: createPostMutation, variables }),
|
||||
mutate({
|
||||
mutation: createPostMutation,
|
||||
variables: {
|
||||
...variables,
|
||||
title: 'Pre-existing post',
|
||||
content: 'Some content',
|
||||
slug: 'pre-existing-post',
|
||||
categoryIds,
|
||||
},
|
||||
}),
|
||||
).resolves.toMatchObject({
|
||||
errors: [
|
||||
{
|
||||
@ -313,6 +468,8 @@ describe('slugifyMiddleware', () => {
|
||||
})
|
||||
})
|
||||
|
||||
it.todo('UpdatePost')
|
||||
|
||||
describe('SignupVerification', () => {
|
||||
beforeEach(() => {
|
||||
variables = {
|
||||
|
||||
@ -9,8 +9,7 @@ export default {
|
||||
updatedAt: {
|
||||
type: 'string',
|
||||
isoDate: true,
|
||||
required: true,
|
||||
default: () => new Date().toISOString(),
|
||||
required: false,
|
||||
},
|
||||
post: {
|
||||
type: 'relationship',
|
||||
|
||||
@ -5,6 +5,7 @@ import { CATEGORIES_MIN, CATEGORIES_MAX } from '../../constants/categories'
|
||||
import { DESCRIPTION_WITHOUT_HTML_LENGTH_MIN } from '../../constants/groups'
|
||||
import { removeHtmlTags } from '../../middleware/helpers/cleanHtml.js'
|
||||
import Resolver from './helpers/Resolver'
|
||||
import { mergeImage } from './images/images'
|
||||
|
||||
export default {
|
||||
Query: {
|
||||
@ -34,10 +35,31 @@ export default {
|
||||
`
|
||||
}
|
||||
}
|
||||
const result = await txc.run(groupCypher, {
|
||||
const transactionResponse = await txc.run(groupCypher, {
|
||||
userId: context.user.id,
|
||||
})
|
||||
return result.records.map((record) => record.get('group'))
|
||||
return transactionResponse.records.map((record) => record.get('group'))
|
||||
})
|
||||
try {
|
||||
return await readTxResultPromise
|
||||
} catch (error) {
|
||||
throw new Error(error)
|
||||
} finally {
|
||||
session.close()
|
||||
}
|
||||
},
|
||||
GroupMembers: async (_object, params, context, _resolveInfo) => {
|
||||
const { id: groupId } = params
|
||||
const session = context.driver.session()
|
||||
const readTxResultPromise = session.readTransaction(async (txc) => {
|
||||
const groupMemberCypher = `
|
||||
MATCH (user:User)-[membership:MEMBER_OF]->(:Group {id: $groupId})
|
||||
RETURN user {.*, myRoleInGroup: membership.role}
|
||||
`
|
||||
const transactionResponse = await txc.run(groupMemberCypher, {
|
||||
groupId,
|
||||
})
|
||||
return transactionResponse.records.map((record) => record.get('user'))
|
||||
})
|
||||
try {
|
||||
return await readTxResultPromise
|
||||
@ -87,9 +109,10 @@ export default {
|
||||
MATCH (owner:User {id: $userId})
|
||||
MERGE (owner)-[:CREATED]->(group)
|
||||
MERGE (owner)-[membership:MEMBER_OF]->(group)
|
||||
SET membership.createdAt = toString(datetime())
|
||||
SET membership.updatedAt = toString(datetime())
|
||||
SET membership.role = 'owner'
|
||||
SET
|
||||
membership.createdAt = toString(datetime()),
|
||||
membership.updatedAt = null,
|
||||
membership.role = 'owner'
|
||||
${categoriesCypher}
|
||||
RETURN group {.*, myRole: membership.role}
|
||||
`,
|
||||
@ -101,8 +124,7 @@ export default {
|
||||
return group
|
||||
})
|
||||
try {
|
||||
const group = await writeTxResultPromise
|
||||
return group
|
||||
return await writeTxResultPromise
|
||||
} catch (error) {
|
||||
if (error.code === 'Neo.ClientError.Schema.ConstraintValidationFailed')
|
||||
throw new UserInputError('Group with this slug already exists!')
|
||||
@ -111,6 +133,137 @@ export default {
|
||||
session.close()
|
||||
}
|
||||
},
|
||||
UpdateGroup: async (_parent, params, context, _resolveInfo) => {
|
||||
const { categoryIds } = params
|
||||
const { id: groupId, avatar: avatarInput } = params
|
||||
delete params.categoryIds
|
||||
if (CONFIG.CATEGORIES_ACTIVE && categoryIds) {
|
||||
if (categoryIds.length < CATEGORIES_MIN) {
|
||||
throw new UserInputError('Too view categories!')
|
||||
}
|
||||
if (categoryIds.length > CATEGORIES_MAX) {
|
||||
throw new UserInputError('Too many categories!')
|
||||
}
|
||||
}
|
||||
if (
|
||||
params.description &&
|
||||
removeHtmlTags(params.description).length < DESCRIPTION_WITHOUT_HTML_LENGTH_MIN
|
||||
) {
|
||||
throw new UserInputError('Description too short!')
|
||||
}
|
||||
const session = context.driver.session()
|
||||
if (CONFIG.CATEGORIES_ACTIVE && categoryIds && categoryIds.length) {
|
||||
const cypherDeletePreviousRelations = `
|
||||
MATCH (group:Group {id: $groupId})-[previousRelations:CATEGORIZED]->(category:Category)
|
||||
DELETE previousRelations
|
||||
RETURN group, category
|
||||
`
|
||||
await session.writeTransaction((transaction) => {
|
||||
return transaction.run(cypherDeletePreviousRelations, { groupId })
|
||||
})
|
||||
}
|
||||
const writeTxResultPromise = session.writeTransaction(async (transaction) => {
|
||||
let updateGroupCypher = `
|
||||
MATCH (group:Group {id: $groupId})
|
||||
SET group += $params
|
||||
SET group.updatedAt = toString(datetime())
|
||||
WITH group
|
||||
`
|
||||
if (CONFIG.CATEGORIES_ACTIVE && categoryIds && categoryIds.length) {
|
||||
updateGroupCypher += `
|
||||
UNWIND $categoryIds AS categoryId
|
||||
MATCH (category:Category {id: categoryId})
|
||||
MERGE (group)-[:CATEGORIZED]->(category)
|
||||
WITH group
|
||||
`
|
||||
}
|
||||
updateGroupCypher += `
|
||||
OPTIONAL MATCH (:User {id: $userId})-[membership:MEMBER_OF]->(group)
|
||||
RETURN group {.*, myRole: membership.role}
|
||||
`
|
||||
const transactionResponse = await transaction.run(updateGroupCypher, {
|
||||
groupId,
|
||||
userId: context.user.id,
|
||||
categoryIds,
|
||||
params,
|
||||
})
|
||||
const [group] = await transactionResponse.records.map((record) => record.get('group'))
|
||||
if (avatarInput) {
|
||||
await mergeImage(group, 'AVATAR_IMAGE', avatarInput, { transaction })
|
||||
}
|
||||
return group
|
||||
})
|
||||
try {
|
||||
return await writeTxResultPromise
|
||||
} catch (error) {
|
||||
if (error.code === 'Neo.ClientError.Schema.ConstraintValidationFailed')
|
||||
throw new UserInputError('Group with this slug already exists!')
|
||||
throw new Error(error)
|
||||
} finally {
|
||||
session.close()
|
||||
}
|
||||
},
|
||||
JoinGroup: async (_parent, params, context, _resolveInfo) => {
|
||||
const { groupId, userId } = params
|
||||
const session = context.driver.session()
|
||||
const writeTxResultPromise = session.writeTransaction(async (transaction) => {
|
||||
const joinGroupCypher = `
|
||||
MATCH (member:User {id: $userId}), (group:Group {id: $groupId})
|
||||
MERGE (member)-[membership:MEMBER_OF]->(group)
|
||||
ON CREATE SET
|
||||
membership.createdAt = toString(datetime()),
|
||||
membership.updatedAt = null,
|
||||
membership.role =
|
||||
CASE WHEN group.groupType = 'public'
|
||||
THEN 'usual'
|
||||
ELSE 'pending'
|
||||
END
|
||||
RETURN member {.*, myRoleInGroup: membership.role}
|
||||
`
|
||||
const transactionResponse = await transaction.run(joinGroupCypher, { groupId, userId })
|
||||
const [member] = await transactionResponse.records.map((record) => record.get('member'))
|
||||
return member
|
||||
})
|
||||
try {
|
||||
return await writeTxResultPromise
|
||||
} catch (error) {
|
||||
throw new Error(error)
|
||||
} finally {
|
||||
session.close()
|
||||
}
|
||||
},
|
||||
ChangeGroupMemberRole: async (_parent, params, context, _resolveInfo) => {
|
||||
const { groupId, userId, roleInGroup } = params
|
||||
const session = context.driver.session()
|
||||
const writeTxResultPromise = session.writeTransaction(async (transaction) => {
|
||||
const joinGroupCypher = `
|
||||
MATCH (member:User {id: $userId}), (group:Group {id: $groupId})
|
||||
MERGE (member)-[membership:MEMBER_OF]->(group)
|
||||
ON CREATE SET
|
||||
membership.createdAt = toString(datetime()),
|
||||
membership.updatedAt = null,
|
||||
membership.role = $roleInGroup
|
||||
ON MATCH SET
|
||||
membership.updatedAt = toString(datetime()),
|
||||
membership.role = $roleInGroup
|
||||
RETURN member {.*, myRoleInGroup: membership.role}
|
||||
`
|
||||
const transactionResponse = await transaction.run(joinGroupCypher, {
|
||||
groupId,
|
||||
userId,
|
||||
roleInGroup,
|
||||
})
|
||||
const [member] = await transactionResponse.records.map((record) => record.get('member'))
|
||||
return member
|
||||
})
|
||||
try {
|
||||
return await writeTxResultPromise
|
||||
} catch (error) {
|
||||
throw new Error(error)
|
||||
} finally {
|
||||
session.close()
|
||||
}
|
||||
},
|
||||
},
|
||||
Group: {
|
||||
...Resolver('Group', {
|
||||
|
||||
@ -131,11 +131,11 @@ export default {
|
||||
delete params.image
|
||||
const session = context.driver.session()
|
||||
let updatePostCypher = `
|
||||
MATCH (post:Post {id: $params.id})
|
||||
SET post += $params
|
||||
SET post.updatedAt = toString(datetime())
|
||||
WITH post
|
||||
`
|
||||
MATCH (post:Post {id: $params.id})
|
||||
SET post += $params
|
||||
SET post.updatedAt = toString(datetime())
|
||||
WITH post
|
||||
`
|
||||
|
||||
if (CONFIG.CATEGORIES_ACTIVE && categoryIds && categoryIds.length) {
|
||||
const cypherDeletePreviousRelations = `
|
||||
@ -358,7 +358,7 @@ export default {
|
||||
undefinedToNull: ['activityId', 'objectId', 'language', 'pinnedAt', 'pinned'],
|
||||
hasMany: {
|
||||
tags: '-[:TAGGED]->(related:Tag)',
|
||||
// categories: '-[:CATEGORIZED]->(related:Category)',
|
||||
categories: '-[:CATEGORIZED]->(related:Category)',
|
||||
comments: '<-[:COMMENTS]-(related:Comment)',
|
||||
shoutedBy: '<-[:SHOUTED]-(related:User)',
|
||||
emotions: '<-[related:EMOTED]',
|
||||
|
||||
@ -368,7 +368,7 @@ describe('UpdatePost', () => {
|
||||
describe('unauthenticated', () => {
|
||||
it('throws authorization error', async () => {
|
||||
authenticatedUser = null
|
||||
expect(mutate({ mutation: updatePostMutation, variables })).resolves.toMatchObject({
|
||||
await expect(mutate({ mutation: updatePostMutation, variables })).resolves.toMatchObject({
|
||||
errors: [{ message: 'Not Authorized!' }],
|
||||
data: { UpdatePost: null },
|
||||
})
|
||||
|
||||
@ -59,7 +59,7 @@ input _GroupFilter {
|
||||
|
||||
type Query {
|
||||
Group(
|
||||
isMember: Boolean # if 'undefined' or 'null' then all groups
|
||||
isMember: Boolean # if 'undefined' or 'null' then get all groups
|
||||
id: ID
|
||||
name: String
|
||||
slug: String
|
||||
@ -67,18 +67,28 @@ type Query {
|
||||
updatedAt: String
|
||||
about: String
|
||||
description: String
|
||||
# groupType: GroupType # test this
|
||||
# actionRadius: GroupActionRadius # test this
|
||||
# avatar: ImageInput # test this
|
||||
locationName: String
|
||||
first: Int
|
||||
offset: Int
|
||||
orderBy: [_GroupOrdering]
|
||||
filter: _GroupFilter
|
||||
): [Group]
|
||||
|
||||
AvailableGroupTypes: [GroupType]!
|
||||
GroupMembers(
|
||||
id: ID!
|
||||
first: Int
|
||||
offset: Int
|
||||
orderBy: [_UserOrdering]
|
||||
filter: _UserFilter
|
||||
): [User]
|
||||
|
||||
AvailableGroupActionRadii: [GroupActionRadius]!
|
||||
# AvailableGroupTypes: [GroupType]!
|
||||
|
||||
AvailableGroupMemberRoles: [GroupMemberRole]!
|
||||
# AvailableGroupActionRadii: [GroupActionRadius]!
|
||||
|
||||
# AvailableGroupMemberRoles: [GroupMemberRole]!
|
||||
}
|
||||
|
||||
type Mutation {
|
||||
@ -86,24 +96,38 @@ type Mutation {
|
||||
id: ID
|
||||
name: String!
|
||||
slug: String
|
||||
avatar: ImageInput
|
||||
about: String
|
||||
description: String!
|
||||
groupType: GroupType!
|
||||
actionRadius: GroupActionRadius!
|
||||
categoryIds: [ID]
|
||||
locationName: String
|
||||
# avatar: ImageInput # a group can not be created with an avatar
|
||||
locationName: String # test this as result
|
||||
): Group
|
||||
|
||||
UpdateGroup(
|
||||
id: ID!
|
||||
name: String
|
||||
slug: String
|
||||
avatar: ImageInput
|
||||
locationName: String
|
||||
about: String
|
||||
description: String
|
||||
# groupType: GroupType # is not possible at the moment and has to be discussed. may be in the stronger direction: public → closed → hidden
|
||||
actionRadius: GroupActionRadius
|
||||
categoryIds: [ID]
|
||||
avatar: ImageInput # test this as result
|
||||
locationName: String # test this as result
|
||||
): Group
|
||||
|
||||
DeleteGroup(id: ID!): Group
|
||||
# DeleteGroup(id: ID!): Group
|
||||
|
||||
JoinGroup(
|
||||
groupId: ID!
|
||||
userId: ID!
|
||||
): User
|
||||
|
||||
ChangeGroupMemberRole(
|
||||
groupId: ID!
|
||||
userId: ID!
|
||||
roleInGroup: GroupMemberRole!
|
||||
): User
|
||||
}
|
||||
|
||||
@ -114,6 +114,8 @@ type User {
|
||||
badgesCount: Int! @cypher(statement: "MATCH (this)<-[:REWARDED]-(r:Badge) RETURN COUNT(r)")
|
||||
|
||||
emotions: [EMOTED]
|
||||
|
||||
myRoleInGroup: GroupMemberRole
|
||||
}
|
||||
|
||||
|
||||
|
||||
5
webapp/assets/_new/icons/svgs/child.svg
Normal file
@ -0,0 +1,5 @@
|
||||
<!-- Generated by IcoMoon.io -->
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">
|
||||
<title>child</title>
|
||||
<path d="M12 3c2.202 0 3.791 1.007 4.531 2.313 0.026-0.041 0.034-0.084 0.063-0.125 0.453-0.641 1.315-1.188 2.406-1.188v2c-0.453 0-0.588 0.111-0.719 0.281 3.845 0.921 6.812 4.105 7.563 8.063 1.193 0.397 2.156 1.337 2.156 2.656 0 1.365-1.024 2.33-2.281 2.688-0.816 4.701-4.82 8.313-9.719 8.313s-8.903-3.611-9.719-8.313c-1.257-0.357-2.281-1.323-2.281-2.688s1.024-2.33 2.281-2.688c0.741-4.271 4.122-7.637 8.406-8.219-0.39-0.574-1.192-1.094-2.688-1.094v-2zM16 8c-4.093 0-7.461 3.121-7.906 7.125l-0.094 0.875h-1c-0.555 0-1 0.445-1 1s0.445 1 1 1h1l0.094 0.875c0.445 4.004 3.813 7.125 7.906 7.125s7.461-3.121 7.906-7.125l0.094-0.875h1c0.555 0 1-0.445 1-1s-0.445-1-1-1h-0.875l-0.125-0.875c-0.536-4.019-3.907-7.125-8-7.125zM12.5 16c0.828 0 1.5 0.672 1.5 1.5s-0.672 1.5-1.5 1.5-1.5-0.672-1.5-1.5 0.672-1.5 1.5-1.5zM19.5 16c0.828 0 1.5 0.672 1.5 1.5s-0.672 1.5-1.5 1.5-1.5-0.672-1.5-1.5 0.672-1.5 1.5-1.5z"></path>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.0 KiB |
5
webapp/assets/_new/icons/svgs/desktop.svg
Normal file
@ -0,0 +1,5 @@
|
||||
<!-- Generated by IcoMoon.io -->
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">
|
||||
<title>desktop</title>
|
||||
<path d="M2 6h28v18h-13v2h5v2h-12v-2h5v-2h-13v-18zM4 8v14h24v-14h-24z"></path>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 240 B |
5
webapp/assets/_new/icons/svgs/ellipsis-h.svg
Normal file
@ -0,0 +1,5 @@
|
||||
<!-- Generated by IcoMoon.io -->
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">
|
||||
<title>ellipsis-h</title>
|
||||
<path d="M6 14c1.105 0 2 0.895 2 2s-0.895 2-2 2-2-0.895-2-2 0.895-2 2-2zM16 14c1.105 0 2 0.895 2 2s-0.895 2-2 2-2-0.895-2-2 0.895-2 2-2zM26 14c1.105 0 2 0.895 2 2s-0.895 2-2 2-2-0.895-2-2 0.895-2 2-2z"></path>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 374 B |
5
webapp/assets/_new/icons/svgs/home.svg
Normal file
@ -0,0 +1,5 @@
|
||||
<!-- Generated by IcoMoon.io -->
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">
|
||||
<title>home</title>
|
||||
<path d="M16 2.594l0.719 0.688 13 13-1.438 1.438-1.281-1.281v11.563h-9v-10h-4v10h-9v-11.563l-1.281 1.281-1.438-1.438 13-13zM16 5.438l-9 9v11.563h5v-10h8v10h5v-11.563z"></path>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 334 B |
5
webapp/assets/_new/icons/svgs/lightbulb.svg
Normal file
@ -0,0 +1,5 @@
|
||||
<!-- Generated by IcoMoon.io -->
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">
|
||||
<title>lightbulb-o</title>
|
||||
<path d="M6.813 2.406l2.094 2.094-1.406 1.406-2.094-2.094zM25.188 2.406l1.406 1.406-2.094 2.094-1.406-1.406zM16 3.031c4.934-0.047 9 4.027 9 8.969 0 2.706-1.249 5.062-2.906 6.719-1.238 1.15-2 2.627-2 4.094v1.188h-0.094v4h-2.281c-0.347 0.597-0.982 1-1.719 1s-1.372-0.403-1.719-1h-2.281v-6c-0.203-1.117-0.794-2.212-1.75-3.031-2.233-1.898-3.573-4.845-3.125-8.094 0.561-4.039 3.789-7.316 7.844-7.781 0.011-0.001 0.020 0.001 0.031 0 0.336-0.041 0.671-0.059 1-0.063zM16 5.031c-0.258 0.004-0.518 0.030-0.781 0.063-3.131 0.348-5.687 2.881-6.125 6.031-0.352 2.552 0.702 4.811 2.469 6.313 1.388 1.19 2.124 2.848 2.344 4.563h4.375c0.236-1.792 1.094-3.434 2.438-4.688l-0.031-0.031c1.343-1.343 2.313-3.187 2.313-5.281 0-3.861-3.135-7.024-7-6.969zM2 12h3v2h-3v-2zM27 12h3v2h-3v-2zM7.5 20.094l1.406 1.406-2.094 2.094-1.406-1.406zM24.5 20.094l2.094 2.094-1.406 1.406-2.094-2.094zM14 24v2h4v-2h-4z"></path>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.0 KiB |
20
webapp/assets/_new/icons/svgs/movement.svg
Normal file
@ -0,0 +1,20 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 26.4.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
width="24px" height="24.3px" viewBox="0 0 24 24.3" style="enable-background:new 0 0 24 24.3;" xml:space="preserve">
|
||||
<style type="text/css">
|
||||
.st0{fill:#333333;}
|
||||
</style>
|
||||
<g>
|
||||
<circle class="st0" cx="17.1" cy="2.9" r="2.9"/>
|
||||
<path class="st0" d="M23.5,5.6L23.5,5.6c-0.6-0.5-1.4-0.5-2,0.1l-2.7,3l-2.2-1.4c0.1,0.3,0.1,0.6,0.1,0.9c0,0.6-0.2,1.1-0.5,1.6
|
||||
L16,10.2l1.5,1c0.4,0.3,0.9,0.4,1.4,0.4c0.1,0,0.2,0,0.4,0c0.6-0.1,1.2-0.4,1.6-0.9l2.7-3.1C24.2,7,24.1,6.1,23.5,5.6z"/>
|
||||
<path class="st0" d="M6.5,17c-0.3-0.2-0.5-0.5-0.7-0.8c0,0,0,0,0,0L4.6,19l-4,2.7C0,22.1-0.2,23,0.2,23.6l0,0
|
||||
c0.4,0.6,1.3,0.8,1.9,0.4l4.7-3.2l1.4-3.3l-0.7-0.1C7.2,17.4,6.8,17.2,6.5,17z"/>
|
||||
<path class="st0" d="M16.3,15.1l-4.4-0.5l3.5-4.7l0.3-0.4C16,9,16.2,8.6,16.2,8.2c0-0.7-0.4-1.4-1-1.7c-0.1,0-0.1-0.1-0.2-0.1
|
||||
l-1.8-0.7L9.4,4.4C9.1,4.3,8.8,4.3,8.5,4.3c-0.3,0-0.5,0-0.8,0.1c-0.5,0.2-1,0.5-1.3,0.9L3.7,8.5C3.2,9.1,3.3,10,3.9,10.5l0,0
|
||||
c0.3,0.2,0.6,0.3,0.9,0.3c0.4,0,0.8-0.2,1.1-0.5L8.6,7L11.4,8l-4.8,6.5l0,0c-0.1,0.1-0.1,0.3-0.2,0.4c-0.2,0.8,0.4,1.7,1.3,1.8
|
||||
l0.9,0.1l6.9,0.9l-1.7,2.7c-0.4,0.7-0.2,1.5,0.5,1.9h0c0.2,0.1,0.5,0.2,0.7,0.2c0.5,0,0.9-0.2,1.2-0.7l2-3.2
|
||||
c0.4-0.7,0.5-1.6,0.1-2.3C17.8,15.7,17.1,15.2,16.3,15.1z"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.4 KiB |
5
webapp/assets/_new/icons/svgs/music.svg
Normal file
@ -0,0 +1,5 @@
|
||||
<!-- Generated by IcoMoon.io -->
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">
|
||||
<title>music</title>
|
||||
<path d="M27 3.781v17.219c0 2.197-1.803 4-4 4s-4-1.803-4-4 1.803-4 4-4c0.732 0 1.407 0.214 2 0.563v-7.375l-14 2.625v11.188c0 2.197-1.803 4-4 4s-4-1.803-4-4 1.803-4 4-4c0.732 0 1.407 0.214 2 0.563v-13.406l0.813-0.125 16-3zM25 6.188l-14 2.625v2l14-2.625v-2zM23 19c-1.116 0-2 0.884-2 2s0.884 2 2 2 2-0.884 2-2-0.884-2-2-2zM7 22c-1.116 0-2 0.884-2 2s0.884 2 2 2 2-0.884 2-2-0.884-2-2-2z"></path>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 551 B |
5
webapp/assets/_new/icons/svgs/suitcase.svg
Normal file
@ -0,0 +1,5 @@
|
||||
<!-- Generated by IcoMoon.io -->
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">
|
||||
<title>suitcase</title>
|
||||
<path d="M14 3h4c1.093 0 2 0.907 2 2v1h3v-1h2v1h2c1.093 0 2 0.907 2 2v16c0 1.093-0.907 2-2 2h-22c-1.093 0-2-0.907-2-2v-16c0-1.093 0.907-2 2-2h2v-1h2v1h3v-1c0-1.093 0.907-2 2-2zM14 5v1h4v-1h-4zM5 8v16h2v-15h2v15h14v-15h2v15h2v-16h-22z"></path>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 405 B |
@ -8,6 +8,9 @@ let wrapper
|
||||
describe('FilterMenu.vue', () => {
|
||||
const mocks = {
|
||||
$t: jest.fn((string) => string),
|
||||
$env: {
|
||||
CATEGORIES_ACTIVE: true,
|
||||
},
|
||||
}
|
||||
|
||||
const getters = {
|
||||
|
||||
@ -14,6 +14,7 @@
|
||||
<div class="filter-menu-options">
|
||||
<h2 class="title">{{ $t('filter-menu.filter-by') }}</h2>
|
||||
<following-filter />
|
||||
<categories-filter v-if="categoriesActive" />
|
||||
</div>
|
||||
<div class="filter-menu-options">
|
||||
<h2 class="title">{{ $t('filter-menu.order-by') }}</h2>
|
||||
@ -28,17 +29,24 @@ import Dropdown from '~/components/Dropdown'
|
||||
import { mapGetters } from 'vuex'
|
||||
import FollowingFilter from './FollowingFilter'
|
||||
import OrderByFilter from './OrderByFilter'
|
||||
import CategoriesFilter from './CategoriesFilter'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Dropdown,
|
||||
FollowingFilter,
|
||||
CategoriesFilter,
|
||||
OrderByFilter,
|
||||
},
|
||||
props: {
|
||||
placement: { type: String },
|
||||
offset: { type: [String, Number] },
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
categoriesActive: this.$env.CATEGORIES_ACTIVE,
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({
|
||||
filterActive: 'posts/isActive',
|
||||
|
||||
@ -218,22 +218,25 @@
|
||||
},
|
||||
"category": {
|
||||
"name": {
|
||||
"animal-protection": "Schutz der Tiere",
|
||||
"art-culture-sport": "Kunst, Kultur & Sport",
|
||||
"consumption-sustainability": "Verbrauch & Nachhaltigkeit",
|
||||
"cooperation-development": "Zusammenarbeit & Entwicklung",
|
||||
"democracy-politics": "Demokratie & Politik",
|
||||
"economy-finances": "Wirtschaft & Finanzen",
|
||||
"education-sciences": "Bildung & Wissenschaft",
|
||||
"energy-technology": "Energie & Technologie",
|
||||
"environment-nature": "Umwelt & Natur",
|
||||
"freedom-of-speech": "Redefreiheit",
|
||||
"global-peace-nonviolence": "Globaler Frieden & Gewaltlosigkeit",
|
||||
"happiness-values": "Glück & Werte",
|
||||
"health-wellbeing": "Gesundheit & Wohlbefinden",
|
||||
"human-rights-justice": "Menschenrechte & Gerechtigkeit",
|
||||
"it-internet-data-privacy": "IT, Internet & Datenschutz",
|
||||
"just-for-fun": "Nur zum Spaß"
|
||||
"body-and-excercise": "Körper & Bewegung",
|
||||
"children": "Kinder",
|
||||
"culture": "Kultur",
|
||||
"economy": "Wirtschaft",
|
||||
"energy": "Energie",
|
||||
"finance": "Finanzen",
|
||||
"health": "Gesundheit",
|
||||
"home": "Wohnen",
|
||||
"it-and-media": "IT & Medien",
|
||||
"law": "Recht",
|
||||
"miscellaneous": "Sonstiges",
|
||||
"mobility": "Mobilität",
|
||||
"nature": "Natur",
|
||||
"networking": "Vernetzung",
|
||||
"peace": "Frieden",
|
||||
"politics": "Politik",
|
||||
"psyche": "Psyche",
|
||||
"science": "Wissenschaft",
|
||||
"spirituality": "Spiritualität"
|
||||
}
|
||||
},
|
||||
"emotions-label": {
|
||||
|
||||
@ -218,22 +218,25 @@
|
||||
},
|
||||
"category": {
|
||||
"name": {
|
||||
"animal-protection": "Animal Protection",
|
||||
"art-culture-sport": "Art, Culture, & Sport",
|
||||
"consumption-sustainability": "Consumption & Sustainability",
|
||||
"cooperation-development": "Cooperation & Development",
|
||||
"democracy-politics": "Democracy & Politics",
|
||||
"economy-finances": "Economy & Finances",
|
||||
"education-sciences": "Education & Sciences",
|
||||
"energy-technology": "Energy & Technology",
|
||||
"environment-nature": "Environment & Nature",
|
||||
"freedom-of-speech": "Freedom of Speech",
|
||||
"global-peace-nonviolence": "Global Peace & Nonviolence",
|
||||
"happiness-values": "Happiness & Values",
|
||||
"health-wellbeing": "Health & Wellbeing",
|
||||
"human-rights-justice": "Human Rights & Justice",
|
||||
"it-internet-data-privacy": "IT, Internet & Data Privacy",
|
||||
"just-for-fun": "Just for Fun"
|
||||
"body-and-excercise": "Body & Excercise",
|
||||
"children": "Children",
|
||||
"culture": "Culture",
|
||||
"economy": "Economy",
|
||||
"energy": "Energy",
|
||||
"finance": "Finance",
|
||||
"health": "Health",
|
||||
"home": "Home",
|
||||
"it-and-media": "IT & Media",
|
||||
"law": "Law",
|
||||
"miscellaneous": "Miscellaneous",
|
||||
"mobility": "Mobility",
|
||||
"nature": "Nature",
|
||||
"networking": "Networking",
|
||||
"peace": "Peace",
|
||||
"politics": "Politics",
|
||||
"psyche": "Psyche",
|
||||
"science": "Science",
|
||||
"spirituality": "Spirituality"
|
||||
}
|
||||
},
|
||||
"emotions-label": {
|
||||
|
||||