Merge branch '5059-groups/5131-implement-group-gql-model-and-crud' into 5140-My-Groups-Page

This commit is contained in:
ogerly 2022-08-15 09:43:39 +02:00
commit 267534e95b
19 changed files with 191 additions and 971 deletions

View File

@ -103,7 +103,7 @@
"mustache": "^4.2.0",
"neo4j-driver": "^4.0.2",
"neo4j-graphql-js": "^2.11.5",
"neode": "^0.4.7",
"neode": "^0.4.8",
"node-fetch": "~2.6.1",
"nodemailer": "^6.4.4",
"nodemailer-html-to-text": "^3.2.0",

View File

@ -1,5 +1,3 @@
// this file is duplicated in `backend/src/config/metadata.js` and `webapp/constants/metadata.js`
export default {
CATEGORIES_MIN: 1,
CATEGORIES_MAX: 3,
}
// 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

View File

@ -0,0 +1,2 @@
// 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

View File

@ -35,9 +35,6 @@ export const createGroupMutation = gql`
groupType
actionRadius
myRole
# Wolle: owner {
# name
# }
}
}
`
@ -54,9 +51,6 @@ export const groupQuery = gql`
$updatedAt: String
$about: String
$description: String
# $groupType: GroupType!,
# $actionRadius: GroupActionRadius!,
# $categoryIds: [ID]
$locationName: String
$first: Int
$offset: Int
@ -72,9 +66,6 @@ export const groupQuery = gql`
updatedAt: $updatedAt
about: $about
description: $description
# groupType: $groupType
# actionRadius: $actionRadius
# categoryIds: $categoryIds
locationName: $locationName
first: $first
offset: $offset
@ -99,9 +90,6 @@ export const groupQuery = gql`
name
icon
}
# Wolle: owner {
# name
# }
}
}
`

View File

@ -1,6 +1,13 @@
import sanitizeHtml from 'sanitize-html'
import linkifyHtml from 'linkifyjs/html'
export const removeHtmlTags = (input) => {
return sanitizeHtml(input, {
allowedTags: [],
allowedAttributes: {},
})
}
const standardSanitizeHtmlOptions = {
allowedTags: [
'img',

View File

@ -1,12 +1,5 @@
import LanguageDetect from 'languagedetect'
import sanitizeHtml from 'sanitize-html'
const removeHtmlTags = (input) => {
return sanitizeHtml(input, {
allowedTags: [],
allowedAttributes: {},
})
}
import { removeHtmlTags } from '../helpers/cleanHtml.js'
const setPostLanguage = (text) => {
const lngDetector = new LanguageDetect()

View File

@ -12,6 +12,8 @@ let variables
const driver = getDriver()
const neode = getNeode()
const descriptionAdditional100 =
' 123456789-123456789-123456789-123456789-123456789-123456789-123456789-123456789-123456789-123456789'
beforeAll(async () => {
await cleanDatabase()
@ -67,7 +69,7 @@ describe('slugifyMiddleware', () => {
...variables,
name: 'The Best Group',
about: 'Some about',
description: 'Some description',
description: 'Some description' + descriptionAdditional100,
groupType: 'closed',
actionRadius: 'national',
categoryIds,
@ -87,7 +89,7 @@ describe('slugifyMiddleware', () => {
name: 'The Best Group',
slug: 'the-best-group',
about: 'Some about',
description: 'Some description',
description: 'Some description' + descriptionAdditional100,
groupType: 'closed',
actionRadius: 'national',
},

View File

@ -37,110 +37,10 @@ export default {
locationName: { type: 'string', allow: [null] },
wasSeeded: 'boolean', // Wolle: used or needed?
// Wolle: owner: {
// type: 'relationship',
// relationship: 'OWNS',
// target: 'User',
// direction: 'in',
// },
// 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 },
// 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

@ -1,30 +1,13 @@
import { v4 as uuid } from 'uuid'
// Wolle: import { neo4jgraphql } from 'neo4j-graphql-js'
// Wolle: import { isEmpty } from 'lodash'
import { UserInputError } from 'apollo-server'
import CONFIG from '../../config'
import categories from '../../constants/categories'
// Wolle: import { mergeImage, deleteImage } from './images/images'
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'
// Wolle: import { filterForMutedUsers } from './helpers/filterForMutedUsers'
// Wolle: const maintainPinnedPosts = (params) => {
// const pinnedPostFilter = { pinned: true }
// if (isEmpty(params.filter)) {
// params.filter = { OR: [pinnedPostFilter, {}] }
// } else {
// params.filter = { OR: [pinnedPostFilter, { ...params.filter }] }
// }
// return params
// }
export default {
Query: {
// Wolle: Post: async (object, params, context, resolveInfo) => {
// params = await filterForMutedUsers(params, context)
// // params = await maintainPinnedPosts(params)
// return neo4jgraphql(object, params, context, resolveInfo)
// },
Group: async (_object, params, context, _resolveInfo) => {
const { isMember } = params
const session = context.driver.session()
@ -53,12 +36,10 @@ export default {
const result = await txc.run(groupCypher, {
userId: context.user.id,
})
const group = result.records.map((record) => record.get('group'))
return group
return result.records.map((record) => record.get('group'))
})
try {
const group = await readTxResultPromise
return group
return await readTxResultPromise
} catch (error) {
throw new Error(error)
} finally {
@ -70,11 +51,18 @@ export default {
CreateGroup: async (_parent, params, context, _resolveInfo) => {
const { categoryIds } = params
delete params.categoryIds
if (!categoryIds || categoryIds.length < categories.CATEGORIES_MIN) {
throw new UserInputError('To Less Categories!')
if (!categoryIds || categoryIds.length < CATEGORIES_MIN) {
throw new UserInputError('Too view categories!')
}
if (categoryIds && categoryIds.length > categories.CATEGORIES_MAX) {
throw new UserInputError('To Many Categories!')
if (categoryIds && categoryIds.length > CATEGORIES_MAX) {
throw new UserInputError('Too many categories!')
}
if (
params.description === undefined ||
params.description === null ||
removeHtmlTags(params.description).length < DESCRIPTION_WITHOUT_HTML_LENGTH_MIN
) {
throw new UserInputError('Description too short!')
}
params.id = params.id || uuid()
const session = context.driver.session()
@ -96,6 +84,7 @@ export default {
SET group.updatedAt = toString(datetime())
WITH group
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())
@ -121,105 +110,12 @@ export default {
session.close()
}
},
// UpdatePost: async (_parent, params, context, _resolveInfo) => {
// const { categoryIds } = params
// const { image: imageInput } = params
// delete params.categoryIds
// 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
// `
// if (categoryIds && categoryIds.length) {
// const cypherDeletePreviousRelations = `
// MATCH (post:Post { id: $params.id })-[previousRelations:CATEGORIZED]->(category:Category)
// DELETE previousRelations
// RETURN post, category
// `
// await session.writeTransaction((transaction) => {
// return transaction.run(cypherDeletePreviousRelations, { params })
// })
// updatePostCypher += `
// UNWIND $categoryIds AS categoryId
// MATCH (category:Category {id: categoryId})
// MERGE (post)-[:CATEGORIZED]->(category)
// WITH post
// `
// }
// updatePostCypher += `RETURN post {.*}`
// const updatePostVariables = { categoryIds, params }
// try {
// const writeTxResultPromise = session.writeTransaction(async (transaction) => {
// const updatePostTransactionResponse = await transaction.run(
// updatePostCypher,
// updatePostVariables,
// )
// const [post] = updatePostTransactionResponse.records.map((record) => record.get('post'))
// await mergeImage(post, 'HERO_IMAGE', imageInput, { transaction })
// return post
// })
// const post = await writeTxResultPromise
// return post
// } finally {
// session.close()
// }
// },
// DeletePost: async (object, args, context, resolveInfo) => {
// const session = context.driver.session()
// const writeTxResultPromise = session.writeTransaction(async (transaction) => {
// const deletePostTransactionResponse = await transaction.run(
// `
// MATCH (post:Post {id: $postId})
// OPTIONAL MATCH (post)<-[:COMMENTS]-(comment:Comment)
// SET post.deleted = TRUE
// SET post.content = 'UNAVAILABLE'
// SET post.contentExcerpt = 'UNAVAILABLE'
// SET post.title = 'UNAVAILABLE'
// SET comment.deleted = TRUE
// RETURN post {.*}
// `,
// { postId: args.id },
// )
// const [post] = deletePostTransactionResponse.records.map((record) => record.get('post'))
// await deleteImage(post, 'HERO_IMAGE', { transaction })
// return post
// })
// try {
// const post = await writeTxResultPromise
// return post
// } finally {
// session.close()
// }
},
Group: {
...Resolver('Group', {
// Wolle: undefinedToNull: ['activityId', 'objectId', 'language', 'pinnedAt', 'pinned'],
hasMany: {
// Wolle: tags: '-[:TAGGED]->(related:Tag)',
categories: '-[:CATEGORIZED]->(related:Category)',
},
// hasOne: {
// owner: '<-[:OWNS]-(related:User)',
// // Wolle: image: '-[:HERO_IMAGE]->(related:Image)',
// },
// Wolle: count: {
// contributionsCount:
// '-[:WROTE]->(related:Post) WHERE NOT related.disabled = true AND NOT related.deleted = true',
// },
// Wolle: boolean: {
// shoutedByCurrentUser:
// 'MATCH(this)<-[:SHOUTED]-(related:User {id: $cypherParams.currentUserId}) RETURN COUNT(related) >= 1',
// viewedTeaserByCurrentUser:
// 'MATCH (this)<-[:VIEWED_TEASER]-(u:User {id: $cypherParams.currentUserId}) RETURN COUNT(u) >= 1',
// },
}),
},
}

View File

@ -13,6 +13,8 @@ let authenticatedUser
let user
const categoryIds = ['cat9', 'cat4', 'cat15']
const descriptionAdditional100 =
' 123456789-123456789-123456789-123456789-123456789-123456789-123456789-123456789-123456789-123456789'
let variables = {}
beforeAll(async () => {
@ -116,9 +118,9 @@ describe('Group', () => {
id: 'others-group',
name: 'Uninteresting Group',
about: 'We will change nothing!',
description: 'We love it like it is!?',
description: 'We love it like it is!?' + descriptionAdditional100,
groupType: 'closed',
actionRadius: 'international',
actionRadius: 'global',
categoryIds,
},
})
@ -129,7 +131,7 @@ describe('Group', () => {
id: 'my-group',
name: 'The Best Group',
about: 'We will change the world!',
description: 'Some description',
description: 'Some description' + descriptionAdditional100,
groupType: 'public',
actionRadius: 'regional',
categoryIds,
@ -137,221 +139,70 @@ describe('Group', () => {
})
})
describe('can find', () => {
it('all', async () => {
const expected = {
data: {
Group: expect.arrayContaining([
expect.objectContaining({
id: 'my-group',
slug: 'the-best-group',
myRole: 'owner',
}),
expect.objectContaining({
id: 'others-group',
slug: 'uninteresting-group',
myRole: null,
}),
]),
},
errors: undefined,
}
await expect(query({ query: groupQuery, variables: {} })).resolves.toMatchObject(expected)
describe('query groups', () => {
describe('without any filters', () => {
it('finds all groups', async () => {
const expected = {
data: {
Group: expect.arrayContaining([
expect.objectContaining({
id: 'my-group',
slug: 'the-best-group',
myRole: 'owner',
}),
expect.objectContaining({
id: 'others-group',
slug: 'uninteresting-group',
myRole: null,
}),
]),
},
errors: undefined,
}
await expect(query({ query: groupQuery, variables: {} })).resolves.toMatchObject(expected)
})
})
it('where user is member (or owner in this case)', async () => {
const expected = {
data: {
Group: [
{
id: 'my-group',
slug: 'the-best-group',
myRole: 'owner',
},
],
},
errors: undefined,
}
await expect(
query({ query: groupQuery, variables: { isMember: true } }),
).resolves.toMatchObject(expected)
describe('isMember = true', () => {
it('finds only groups where user is member', async () => {
const expected = {
data: {
Group: [
{
id: 'my-group',
slug: 'the-best-group',
myRole: 'owner',
},
],
},
errors: undefined,
}
await expect(
query({ query: groupQuery, variables: { isMember: true } }),
).resolves.toMatchObject(expected)
})
})
it('where user is not(!) member', async () => {
const expected = {
data: {
Group: expect.arrayContaining([
expect.objectContaining({
id: 'others-group',
slug: 'uninteresting-group',
myRole: null,
}),
]),
},
errors: undefined,
}
await expect(
query({ query: groupQuery, variables: { isMember: false } }),
).resolves.toMatchObject(expected)
describe('isMember = false', () => {
it('finds only groups where user is not(!) member', async () => {
const expected = {
data: {
Group: expect.arrayContaining([
expect.objectContaining({
id: 'others-group',
slug: 'uninteresting-group',
myRole: null,
}),
]),
},
errors: undefined,
}
await expect(
query({ query: groupQuery, variables: { isMember: false } }),
).resolves.toMatchObject(expected)
})
})
})
// describe('can be filtered', () => {
// Wolle: it('by categories', async () => {
// const postQueryFilteredByCategories = gql`
// query Post($filter: _PostFilter) {
// Post(filter: $filter) {
// id
// categories {
// id
// }
// }
// }
// `
// const expected = {
// data: {
// Post: [
// {
// id: 'post-by-followed-user',
// categories: [{ id: 'cat9' }],
// },
// ],
// },
// }
// variables = { ...variables, filter: { categories_some: { id_in: ['cat9'] } } }
// await expect(
// query({ query: postQueryFilteredByCategories, variables }),
// ).resolves.toMatchObject(expected)
// })
// Wolle: let followedUser, happyPost, cryPost
// beforeEach(async () => {
// ;[followedUser] = await Promise.all([
// Factory.build(
// 'user',
// {
// id: 'followed-by-me',
// name: 'Followed User',
// },
// {
// email: 'followed@example.org',
// password: '1234',
// },
// ),
// ])
// ;[happyPost, cryPost] = await Promise.all([
// Factory.build('post', { id: 'happy-post' }, { categoryIds: ['cat4'] }),
// Factory.build('post', { id: 'cry-post' }, { categoryIds: ['cat15'] }),
// Factory.build(
// 'post',
// {
// id: 'post-by-followed-user',
// },
// {
// categoryIds: ['cat9'],
// author: followedUser,
// },
// ),
// ])
// })
// describe('no filter', () => {
// it('returns all posts', async () => {
// const postQueryNoFilters = gql`
// query Post($filter: _PostFilter) {
// Post(filter: $filter) {
// id
// }
// }
// `
// const expected = [{ id: 'happy-post' }, { id: 'cry-post' }, { id: 'post-by-followed-user' }]
// variables = { filter: {} }
// await expect(query({ query: postQueryNoFilters, variables })).resolves.toMatchObject({
// data: {
// Post: expect.arrayContaining(expected),
// },
// })
// })
// })
// describe('by emotions', () => {
// const postQueryFilteredByEmotions = gql`
// query Post($filter: _PostFilter) {
// Post(filter: $filter) {
// id
// emotions {
// emotion
// }
// }
// }
// `
// it('filters by single emotion', async () => {
// const expected = {
// data: {
// Post: [
// {
// id: 'happy-post',
// emotions: [{ emotion: 'happy' }],
// },
// ],
// },
// }
// await user.relateTo(happyPost, 'emoted', { emotion: 'happy' })
// variables = { ...variables, filter: { emotions_some: { emotion_in: ['happy'] } } }
// await expect(
// query({ query: postQueryFilteredByEmotions, variables }),
// ).resolves.toMatchObject(expected)
// })
// it('filters by multiple emotions', async () => {
// const expected = [
// {
// id: 'happy-post',
// emotions: [{ emotion: 'happy' }],
// },
// {
// id: 'cry-post',
// emotions: [{ emotion: 'cry' }],
// },
// ]
// await user.relateTo(happyPost, 'emoted', { emotion: 'happy' })
// await user.relateTo(cryPost, 'emoted', { emotion: 'cry' })
// variables = { ...variables, filter: { emotions_some: { emotion_in: ['happy', 'cry'] } } }
// await expect(
// query({ query: postQueryFilteredByEmotions, variables }),
// ).resolves.toMatchObject({
// data: {
// Post: expect.arrayContaining(expected),
// },
// errors: undefined,
// })
// })
// })
// it('by followed-by', async () => {
// const postQueryFilteredByUsersFollowed = gql`
// query Post($filter: _PostFilter) {
// Post(filter: $filter) {
// id
// author {
// id
// name
// }
// }
// }
// `
// await user.relateTo(followedUser, 'following')
// variables = { filter: { author: { followedBy_some: { id: 'current-user' } } } }
// await expect(
// query({ query: postQueryFilteredByUsersFollowed, variables }),
// ).resolves.toMatchObject({
// data: {
// Post: [
// {
// id: 'post-by-followed-user',
// author: { id: 'followed-by-me', name: 'Followed User' },
// },
// ],
// },
// errors: undefined,
// })
// })
// })
})
})
@ -363,7 +214,7 @@ describe('CreateGroup', () => {
name: 'The Best Group',
slug: 'the-group',
about: 'We will change the world!',
description: 'Some description',
description: 'Some description' + descriptionAdditional100,
groupType: 'public',
actionRadius: 'regional',
categoryIds,
@ -404,9 +255,6 @@ describe('CreateGroup', () => {
CreateGroup: {
name: 'The Best Group',
myRole: 'owner',
// Wolle: owner: {
// name: 'TestUser',
// },
},
},
errors: undefined,
@ -416,331 +264,52 @@ describe('CreateGroup', () => {
)
})
it('"disabled" and "deleted" default to "false"', async () => {
it('has "disabled" and "deleted" default to "false"', async () => {
const expected = { data: { CreateGroup: { disabled: false, deleted: false } } }
await expect(mutate({ mutation: createGroupMutation, variables })).resolves.toMatchObject(
expected,
)
})
describe('description', () => {
describe('length without HTML', () => {
describe('less then 100 chars', () => {
it('throws error: "Too view categories!"', async () => {
const { errors } = await mutate({
mutation: createGroupMutation,
variables: {
...variables,
description:
'0123456789' +
'<a href="https://domain.org/0123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789">0123456789</a>',
},
})
expect(errors[0]).toHaveProperty('message', 'Description too short!')
})
})
})
})
describe('categories', () => {
describe('not even one', () => {
it('throws error: "To Less Categories!"', async () => {
it('throws error: "Too view categories!"', async () => {
const { errors } = await mutate({
mutation: createGroupMutation,
variables: { ...variables, categoryIds: null },
})
expect(errors[0]).toHaveProperty('message', 'To Less Categories!')
expect(errors[0]).toHaveProperty('message', 'Too view categories!')
})
})
describe('four', () => {
it('throws error: "To Many Categories!"', async () => {
it('throws error: "Too many categories!"', async () => {
const { errors } = await mutate({
mutation: createGroupMutation,
variables: { ...variables, categoryIds: ['cat9', 'cat4', 'cat15', 'cat27'] },
})
expect(errors[0]).toHaveProperty('message', 'To Many Categories!')
expect(errors[0]).toHaveProperty('message', 'Too many categories!')
})
})
})
})
})
// describe('UpdatePost', () => {
// let author, newlyCreatedPost
// const updatePostMutation = gql`
// mutation ($id: ID!, $title: String!, $content: String!, $image: ImageInput) {
// UpdatePost(id: $id, title: $title, content: $content, image: $image) {
// id
// title
// content
// author {
// name
// slug
// }
// createdAt
// updatedAt
// }
// }
// `
// beforeEach(async () => {
// author = await Factory.build('user', { slug: 'the-author' })
// newlyCreatedPost = await Factory.build(
// 'post',
// {
// id: 'p9876',
// title: 'Old title',
// content: 'Old content',
// },
// {
// author,
// categoryIds,
// },
// )
// variables = {
// id: 'p9876',
// title: 'New title',
// content: 'New content',
// }
// })
// describe('unauthenticated', () => {
// it('throws authorization error', async () => {
// authenticatedUser = null
// expect(mutate({ mutation: updatePostMutation, variables })).resolves.toMatchObject({
// errors: [{ message: 'Not Authorised!' }],
// data: { UpdatePost: null },
// })
// })
// })
// describe('authenticated but not the author', () => {
// beforeEach(async () => {
// authenticatedUser = await user.toJson()
// })
// it('throws authorization error', async () => {
// const { errors } = await mutate({ mutation: updatePostMutation, variables })
// expect(errors[0]).toHaveProperty('message', 'Not Authorised!')
// })
// })
// describe('authenticated as author', () => {
// beforeEach(async () => {
// authenticatedUser = await author.toJson()
// })
// it('updates a post', async () => {
// const expected = {
// data: { UpdatePost: { id: 'p9876', content: 'New content' } },
// errors: undefined,
// }
// await expect(mutate({ mutation: updatePostMutation, variables })).resolves.toMatchObject(
// expected,
// )
// })
// it('updates a post, but maintains non-updated attributes', async () => {
// const expected = {
// data: {
// UpdatePost: { id: 'p9876', content: 'New content', createdAt: expect.any(String) },
// },
// errors: undefined,
// }
// await expect(mutate({ mutation: updatePostMutation, variables })).resolves.toMatchObject(
// expected,
// )
// })
// it('updates the updatedAt attribute', async () => {
// newlyCreatedPost = await newlyCreatedPost.toJson()
// const {
// data: { UpdatePost },
// } = await mutate({ mutation: updatePostMutation, variables })
// expect(newlyCreatedPost.updatedAt).toBeTruthy()
// expect(Date.parse(newlyCreatedPost.updatedAt)).toEqual(expect.any(Number))
// expect(UpdatePost.updatedAt).toBeTruthy()
// expect(Date.parse(UpdatePost.updatedAt)).toEqual(expect.any(Number))
// expect(newlyCreatedPost.updatedAt).not.toEqual(UpdatePost.updatedAt)
// })
// /* describe('no new category ids provided for update', () => {
// it('resolves and keeps current categories', async () => {
// const expected = {
// data: {
// UpdatePost: {
// id: 'p9876',
// categories: expect.arrayContaining([{ id: 'cat9' }, { id: 'cat4' }, { id: 'cat15' }]),
// },
// },
// errors: undefined,
// }
// await expect(mutate({ mutation: updatePostMutation, variables })).resolves.toMatchObject(
// expected,
// )
// })
// }) */
// /* describe('given category ids', () => {
// beforeEach(() => {
// variables = { ...variables, categoryIds: ['cat27'] }
// })
// it('updates categories of a post', async () => {
// const expected = {
// data: {
// UpdatePost: {
// id: 'p9876',
// categories: expect.arrayContaining([{ id: 'cat27' }]),
// },
// },
// errors: undefined,
// }
// await expect(mutate({ mutation: updatePostMutation, variables })).resolves.toMatchObject(
// expected,
// )
// })
// }) */
// describe('params.image', () => {
// describe('is object', () => {
// beforeEach(() => {
// variables = { ...variables, image: { sensitive: true } }
// })
// it('updates the image', async () => {
// await expect(neode.first('Image', { sensitive: true })).resolves.toBeFalsy()
// await mutate({ mutation: updatePostMutation, variables })
// await expect(neode.first('Image', { sensitive: true })).resolves.toBeTruthy()
// })
// })
// describe('is null', () => {
// beforeEach(() => {
// variables = { ...variables, image: null }
// })
// it('deletes the image', async () => {
// await expect(neode.all('Image')).resolves.toHaveLength(6)
// await mutate({ mutation: updatePostMutation, variables })
// await expect(neode.all('Image')).resolves.toHaveLength(5)
// })
// })
// describe('is undefined', () => {
// beforeEach(() => {
// delete variables.image
// })
// it('keeps the image unchanged', async () => {
// await expect(neode.first('Image', { sensitive: true })).resolves.toBeFalsy()
// await mutate({ mutation: updatePostMutation, variables })
// await expect(neode.first('Image', { sensitive: true })).resolves.toBeFalsy()
// })
// })
// })
// })
// })
// describe('DeletePost', () => {
// let author
// const deletePostMutation = gql`
// mutation ($id: ID!) {
// DeletePost(id: $id) {
// id
// deleted
// content
// contentExcerpt
// image {
// url
// }
// comments {
// deleted
// content
// contentExcerpt
// }
// }
// }
// `
// beforeEach(async () => {
// author = await Factory.build('user')
// await Factory.build(
// 'post',
// {
// id: 'p4711',
// title: 'I will be deleted',
// content: 'To be deleted',
// },
// {
// image: Factory.build('image', {
// url: 'path/to/some/image',
// }),
// author,
// categoryIds,
// },
// )
// variables = { ...variables, id: 'p4711' }
// })
// describe('unauthenticated', () => {
// it('throws authorization error', async () => {
// const { errors } = await mutate({ mutation: deletePostMutation, variables })
// expect(errors[0]).toHaveProperty('message', 'Not Authorised!')
// })
// })
// describe('authenticated but not the author', () => {
// beforeEach(async () => {
// authenticatedUser = await user.toJson()
// })
// it('throws authorization error', async () => {
// const { errors } = await mutate({ mutation: deletePostMutation, variables })
// expect(errors[0]).toHaveProperty('message', 'Not Authorised!')
// })
// })
// describe('authenticated as author', () => {
// beforeEach(async () => {
// authenticatedUser = await author.toJson()
// })
// it('marks the post as deleted and blacks out attributes', async () => {
// const expected = {
// data: {
// DeletePost: {
// id: 'p4711',
// deleted: true,
// content: 'UNAVAILABLE',
// contentExcerpt: 'UNAVAILABLE',
// image: null,
// comments: [],
// },
// },
// }
// await expect(mutate({ mutation: deletePostMutation, variables })).resolves.toMatchObject(
// expected,
// )
// })
// describe('if there are comments on the post', () => {
// beforeEach(async () => {
// await Factory.build(
// 'comment',
// {
// content: 'to be deleted comment content',
// contentExcerpt: 'to be deleted comment content',
// },
// {
// postId: 'p4711',
// },
// )
// })
// it('marks the comments as deleted', async () => {
// const expected = {
// data: {
// DeletePost: {
// id: 'p4711',
// deleted: true,
// content: 'UNAVAILABLE',
// contentExcerpt: 'UNAVAILABLE',
// image: null,
// comments: [
// {
// deleted: true,
// // Should we black out the comment content in the database, too?
// content: 'UNAVAILABLE',
// contentExcerpt: 'UNAVAILABLE',
// },
// ],
// },
// },
// }
// await expect(mutate({ mutation: deletePostMutation, variables })).resolves.toMatchObject(
// expected,
// )
// })
// })
// })
// })

View File

@ -45,7 +45,7 @@ const deleteUserMutation = gql`
}
`
const switchUserRoleMutation = gql`
mutation ($role: UserGroup!, $id: ID!) {
mutation ($role: UserRole!, $id: ID!) {
switchUserRole(role: $role, id: $id) {
name
role

View File

@ -2,5 +2,6 @@ enum GroupActionRadius {
regional
national
continental
international
global
interplanetary
}

View File

@ -1,4 +1,4 @@
enum UserGroup {
enum UserRole {
admin
moderator
user

View File

@ -13,8 +13,6 @@ enum _GroupOrdering {
createdAt_desc
updatedAt_asc
updatedAt_desc
# Wolle: needed? locale_asc
# locale_desc
}
type Group {
@ -40,90 +38,6 @@ type Group {
categories: [Category] @relation(name: "CATEGORIZED", direction: "OUT")
myRole: GroupMemberRole # if 'null' then the current user is no member
# Wolle: needed?
# socialMedia: [SocialMedia]! @relation(name: "OWNED_BY", direction: "IN")
# Wolle: owner: User @relation(name: "OWNS", 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]
}
@ -141,39 +55,6 @@ input _GroupFilter {
id_not: ID
id_in: [ID!]
id_not_in: [ID!]
# categories: _CategoryFilter
# categories_not: _CategoryFilter
# categories_in: [_CategoryFilter!]
# categories_not_in: [_CategoryFilter!]
# categories_some: _CategoryFilter
# categories_none: _CategoryFilter
# categories_single: _CategoryFilter
# categories_every: _CategoryFilter
# 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
}
type Query {
@ -198,32 +79,8 @@ type Query {
AvailableGroupActionRadii: [GroupActionRadius]!
AvailableGroupMemberRoles: [GroupMemberRole]!
# Wolle:
# availableRoles: [UserRole]!
# 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
@ -236,12 +93,7 @@ type Mutation {
actionRadius: GroupActionRadius!
categoryIds: [ID]
locationName: String
): # Wolle: add group settings
# Wolle:
# showShoutsPublicly: Boolean
# sendNotificationEmails: Boolean
# locale: String
Group
): Group
UpdateGroup(
id: ID!
@ -251,19 +103,7 @@ type Mutation {
locationName: String
about: String
description: String
): # Wolle:
# showShoutsPublicly: Boolean
# sendNotificationEmails: Boolean
# locale: String
Group
): 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: UserRole!, id: ID!): User
}

View File

@ -28,7 +28,7 @@ type User {
avatar: Image @relation(name: "AVATAR_IMAGE", direction: "OUT")
deleted: Boolean
disabled: Boolean
role: UserGroup!
role: UserRole!
publicKey: String
invitedBy: User @relation(name: "INVITED", direction: "IN")
invited: [User] @relation(name: "INVITED", direction: "OUT")
@ -151,7 +151,7 @@ input _UserFilter {
followedBy_none: _UserFilter
followedBy_single: _UserFilter
followedBy_every: _UserFilter
role_in: [UserGroup!]
role_in: [UserRole!]
}
type Query {
@ -160,7 +160,7 @@ type Query {
email: String # admins need to search for a user sometimes
name: String
slug: String
role: UserGroup
role: UserRole
locationName: String
about: String
createdAt: String
@ -171,7 +171,7 @@ type Query {
filter: _UserFilter
): [User]
availableRoles: [UserGroup]!
availableRoles: [UserRole]!
mutedUsers: [User]
blockedUsers: [User]
isLoggedIn: Boolean!
@ -219,5 +219,5 @@ type Mutation {
blockUser(id: ID!): User
unblockUser(id: ID!): User
switchUserRole(role: UserGroup!, id: ID!): User
switchUserRole(role: UserRole!, id: ID!): User
}

View File

@ -997,9 +997,9 @@
tslib "1.11.1"
"@hapi/address@2.x.x":
version "2.1.2"
resolved "https://registry.yarnpkg.com/@hapi/address/-/address-2.1.2.tgz#1c794cd6dbf2354d1eb1ef10e0303f573e1c7222"
integrity sha512-O4QDrx+JoGKZc6aN64L04vqa7e41tIiLU+OvKdcYaEMP97UttL0f9GIi9/0A4WAMx0uBd6SidDIhktZhgOcN8Q==
version "2.1.4"
resolved "https://registry.yarnpkg.com/@hapi/address/-/address-2.1.4.tgz#5d67ed43f3fd41a69d4b9ff7b56e7c0d1d0a81e5"
integrity sha512-QD1PhQk+s31P1ixsX0H0Suoupp3VMXzIVMSwobR3F3MSUO2YCV0B7xqLcUw/Bh8yuvd3LhpyqLQWTNcRmp6IdQ==
"@hapi/address@^4.0.1":
version "4.0.1"
@ -1018,10 +1018,10 @@
resolved "https://registry.yarnpkg.com/@hapi/formula/-/formula-2.0.0.tgz#edade0619ed58c8e4f164f233cda70211e787128"
integrity sha512-V87P8fv7PI0LH7LiVi8Lkf3x+KCO7pQozXRssAHNXXL9L1K+uyu4XypLXwxqVDKgyQai6qj3/KteNlrqDx4W5A==
"@hapi/hoek@8.x.x":
version "8.2.4"
resolved "https://registry.yarnpkg.com/@hapi/hoek/-/hoek-8.2.4.tgz#684a14f4ca35d46f44abc87dfc696e5e4fe8a020"
integrity sha512-Ze5SDNt325yZvNO7s5C4fXDscjJ6dcqLFXJQ/M7dZRQCewuDj2iDUuBi6jLQt+APbW9RjjVEvLr35FXuOEqjow==
"@hapi/hoek@8.x.x", "@hapi/hoek@^8.3.0":
version "8.5.1"
resolved "https://registry.yarnpkg.com/@hapi/hoek/-/hoek-8.5.1.tgz#fde96064ca446dec8c55a8c2f130957b070c6e06"
integrity sha512-yN7kbciD87WzLGc5539Tn0sApjyiGHAJgKvG9W8C7O+6c7qmoQMfVs0W4bX17eqz6C78QJqqFrtgdK5EWf6Qow==
"@hapi/hoek@^9.0.0":
version "9.0.0"
@ -1055,11 +1055,11 @@
integrity sha512-vzXR5MY7n4XeIvLpfl3HtE3coZYO4raKXW766R6DZw/6aLqR26iuZ109K7a0NtF2Db0jxqh7xz2AxkUwpUFybw==
"@hapi/topo@3.x.x":
version "3.1.3"
resolved "https://registry.yarnpkg.com/@hapi/topo/-/topo-3.1.3.tgz#c7a02e0d936596d29f184e6d7fdc07e8b5efce11"
integrity sha512-JmS9/vQK6dcUYn7wc2YZTqzIKubAQcJKu2KCKAru6es482U5RT5fP1EXCPtlXpiK7PR0On/kpQKI4fRKkzpZBQ==
version "3.1.6"
resolved "https://registry.yarnpkg.com/@hapi/topo/-/topo-3.1.6.tgz#68d935fa3eae7fdd5ab0d7f953f3205d8b2bfc29"
integrity sha512-tAag0jEcjwH+P2quUfipd7liWCNX2F8NvYjQp2wtInsZxnMlypdw0FtAOLxtvvkO+GSRRbmNi8m/5y42PQJYCQ==
dependencies:
"@hapi/hoek" "8.x.x"
"@hapi/hoek" "^8.3.0"
"@hapi/topo@^5.0.0":
version "5.0.0"
@ -2681,6 +2681,11 @@ base64-js@^1.0.2:
resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.3.1.tgz#58ece8cb75dd07e71ed08c736abc5fac4dbf8df1"
integrity sha512-mLQ4i2QO1ytvGWFWmcngKO//JXAQueZvwEKtjgQFM4jIK0kU+ytMfplL8j+n5mspOfjHwoAg+9yhb7BwAHm36g==
base64-js@^1.3.1:
version "1.5.1"
resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a"
integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==
base@^0.11.1:
version "0.11.2"
resolved "https://registry.yarnpkg.com/base/-/base-0.11.2.tgz#7bde5ced145b6d551a90db87f83c558b4eb48a8f"
@ -2850,6 +2855,14 @@ buffer@4.9.1:
ieee754 "^1.1.4"
isarray "^1.0.0"
buffer@^6.0.3:
version "6.0.3"
resolved "https://registry.yarnpkg.com/buffer/-/buffer-6.0.3.tgz#2ace578459cc8fbe2a70aaa8f52ee63b6a74c6c6"
integrity sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==
dependencies:
base64-js "^1.3.1"
ieee754 "^1.2.1"
busboy@^0.3.1:
version "0.3.1"
resolved "https://registry.yarnpkg.com/busboy/-/busboy-0.3.1.tgz#170899274c5bf38aae27d5c62b71268cd585fd1b"
@ -3929,7 +3942,7 @@ dot-prop@^4.1.0:
dotenv@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-4.0.0.tgz#864ef1379aced55ce6f95debecdce179f7a0cd1d"
integrity sha1-hk7xN5rO1Vzm+V3r7NzhefegzR0=
integrity sha512-XcaMACOr3JMVcEv0Y/iUM2XaOsATRZ3U1In41/1jjK6vJZ2PZbQ1bzCG8uvaByfaBpl9gqc9QWJovpUGBXLLYQ==
dotenv@^6.1.0:
version "6.2.0"
@ -5516,6 +5529,11 @@ ieee754@1.1.13, ieee754@^1.1.4:
resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.1.13.tgz#ec168558e95aa181fd87d37f55c32bbcb6708b84"
integrity sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg==
ieee754@^1.2.1:
version "1.2.1"
resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352"
integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==
ienoopen@1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/ienoopen/-/ienoopen-1.1.0.tgz#411e5d530c982287dbdc3bb31e7a9c9e32630974"
@ -7528,18 +7546,19 @@ negotiator@0.6.2:
resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.2.tgz#feacf7ccf525a77ae9634436a64883ffeca346fb"
integrity sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw==
neo4j-driver-bolt-connection@^4.3.4:
version "4.3.4"
resolved "https://registry.yarnpkg.com/neo4j-driver-bolt-connection/-/neo4j-driver-bolt-connection-4.3.4.tgz#de642bb6a62ffc6ae2e280dccf21395b4d1705a2"
integrity sha512-yxbvwGav+N7EYjcEAINqL6D3CZV+ee2qLInpAhx+iNurwbl3zqtBGiVP79SZ+7tU++y3Q1fW5ofikH06yc+LqQ==
neo4j-driver-bolt-connection@^4.4.7:
version "4.4.7"
resolved "https://registry.yarnpkg.com/neo4j-driver-bolt-connection/-/neo4j-driver-bolt-connection-4.4.7.tgz#0582d54de1f213e60c374209193d1f645ba523ea"
integrity sha512-6Q4hCtvWE6gzN64N09UqZqf/3rDl7FUWZZXiVQL0ZRbaMkJpZNC2NmrDIgGXYE05XEEbRBexf2tVv5OTYZYrow==
dependencies:
neo4j-driver-core "^4.3.4"
text-encoding-utf-8 "^1.0.2"
buffer "^6.0.3"
neo4j-driver-core "^4.4.7"
string_decoder "^1.3.0"
neo4j-driver-core@^4.3.4:
version "4.3.4"
resolved "https://registry.yarnpkg.com/neo4j-driver-core/-/neo4j-driver-core-4.3.4.tgz#b445a4fbf94dce8441075099bd6ac3133c1cf5ee"
integrity sha512-3tn3j6IRUNlpXeehZ9Xv7dLTZPB4a7APaoJ+xhQyMmYQO3ujDM4RFHc0pZcG+GokmaltT5pUCIPTDYx6ODdhcA==
neo4j-driver-core@^4.4.7:
version "4.4.7"
resolved "https://registry.yarnpkg.com/neo4j-driver-core/-/neo4j-driver-core-4.4.7.tgz#d2475e107b3fea2b9d1c36b0c273da5c5a291c37"
integrity sha512-NhvVuQYgG7eO/vXxRaoJfkWUNkjvIpmCIS9UWU9Bbhb4V+wCOyX/MVOXqD0Yizhs4eyIkD7x90OXb79q+vi+oA==
neo4j-driver@^4.0.1, neo4j-driver@^4.0.2:
version "4.0.2"
@ -7552,13 +7571,13 @@ neo4j-driver@^4.0.1, neo4j-driver@^4.0.2:
uri-js "^4.2.2"
neo4j-driver@^4.2.2:
version "4.3.4"
resolved "https://registry.yarnpkg.com/neo4j-driver/-/neo4j-driver-4.3.4.tgz#a54f0562f868ee94dff7509df74e3eb2c1f95a85"
integrity sha512-AGrsFFqnoZv4KhJdmKt4mOBV5mnxmV3+/t8KJTOM68jQuEWoy+RlmAaRRaCSU4eY586OFN/R8lg9MrJpZdSFjw==
version "4.4.7"
resolved "https://registry.yarnpkg.com/neo4j-driver/-/neo4j-driver-4.4.7.tgz#51b3fb48241e66eb3be94e90032cc494c44e59f3"
integrity sha512-N7GddPhp12gVJe4eB84u5ik5SmrtRv8nH3rK47Qy7IUKnJkVEos/F1QjOJN6zt1jLnDXwDcGzCKK8XklYpzogw==
dependencies:
"@babel/runtime" "^7.5.5"
neo4j-driver-bolt-connection "^4.3.4"
neo4j-driver-core "^4.3.4"
neo4j-driver-bolt-connection "^4.4.7"
neo4j-driver-core "^4.4.7"
rxjs "^6.6.3"
neo4j-graphql-js@^2.11.5:
@ -7574,10 +7593,10 @@ neo4j-graphql-js@^2.11.5:
lodash "^4.17.15"
neo4j-driver "^4.0.1"
neode@^0.4.7:
version "0.4.7"
resolved "https://registry.yarnpkg.com/neode/-/neode-0.4.7.tgz#033007b57a2ee167e9ee5537493086db08d005eb"
integrity sha512-YXlc187JRpeKCBcUIkY6nimXXG+Tvlopfe71/FPno2THrwmYt5mm0RPHZ+mXF2O1Xg6zvjKvOpCpDz2vHBfroQ==
neode@^0.4.8:
version "0.4.8"
resolved "https://registry.yarnpkg.com/neode/-/neode-0.4.8.tgz#0889b4fc7f1bf0b470b01fa5b8870373b5d47ad6"
integrity sha512-pb91NfCOg4Fj5o+98H+S2XYC+ByQfbdhwcc1UVuzuUQ0Ezzj+jWz8NmKWU8ZfCH6l4plk71yDAPd2eTwpt+Xvg==
dependencies:
"@hapi/joi" "^15.1.1"
dotenv "^4.0.0"
@ -9603,7 +9622,7 @@ string.prototype.trimstart@^1.0.1:
define-properties "^1.1.3"
es-abstract "^1.17.5"
string_decoder@^1.1.1:
string_decoder@^1.1.1, string_decoder@^1.3.0:
version "1.3.0"
resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.3.0.tgz#42f114594a46cf1a8e30b0a84f56c78c3edac21e"
integrity sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==

View File

@ -0,0 +1,3 @@
// 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

View File

@ -0,0 +1,2 @@
// 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

View File

@ -10,7 +10,7 @@ export const FetchAllRoles = () => {
export const updateUserRole = (role, id) => {
return gql`
mutation($role: UserGroup!, $id: ID!) {
mutation($role: UserRole!, $id: ID!) {
switchUserRole(role: $role, id: $id) {
name
role