mirror of
https://github.com/IT4Change/Ocelot-Social.git
synced 2025-12-13 07:45:56 +00:00
Merge pull request #5139 from Ocelot-Social-Community/5059-groups/5131-implement-group-gql-model-and-crud
feat: 🍰 Implement Group GQL Model And CRUD Resolvers – First Step
This commit is contained in:
commit
032cf7c182
@ -29,4 +29,4 @@ AWS_BUCKET=
|
||||
EMAIL_DEFAULT_SENDER="devops@ocelot.social"
|
||||
EMAIL_SUPPORT="devops@ocelot.social"
|
||||
|
||||
CATEGORIES_ACTIVE=false
|
||||
CATEGORIES_ACTIVE=false
|
||||
|
||||
@ -15,7 +15,7 @@
|
||||
"dev": "nodemon --exec babel-node src/ -e js,gql",
|
||||
"dev:debug": "nodemon --exec babel-node --inspect=0.0.0.0:9229 src/ -e js,gql",
|
||||
"lint": "eslint src --config .eslintrc.js",
|
||||
"test": "cross-env NODE_ENV=test jest --forceExit --detectOpenHandles --runInBand --coverage",
|
||||
"test": "cross-env NODE_ENV=test NODE_OPTIONS=--max-old-space-size=8192 jest --forceExit --detectOpenHandles --runInBand --coverage",
|
||||
"db:clean": "babel-node src/db/clean.js",
|
||||
"db:reset": "yarn run db:clean",
|
||||
"db:seed": "babel-node src/db/seed.js",
|
||||
@ -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",
|
||||
|
||||
3
backend/src/constants/categories.js
Normal file
3
backend/src/constants/categories.js
Normal 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
|
||||
2
backend/src/constants/groups.js
Normal file
2
backend/src/constants/groups.js
Normal 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
|
||||
29
backend/src/db/graphql/authentications.js
Normal file
29
backend/src/db/graphql/authentications.js
Normal file
@ -0,0 +1,29 @@
|
||||
import gql from 'graphql-tag'
|
||||
|
||||
// ------ mutations
|
||||
|
||||
export const signupVerificationMutation = gql`
|
||||
mutation (
|
||||
$password: String!
|
||||
$email: String!
|
||||
$name: String!
|
||||
$slug: String
|
||||
$nonce: String!
|
||||
$termsAndConditionsAgreedVersion: String!
|
||||
) {
|
||||
SignupVerification(
|
||||
email: $email
|
||||
password: $password
|
||||
name: $name
|
||||
slug: $slug
|
||||
nonce: $nonce
|
||||
termsAndConditionsAgreedVersion: $termsAndConditionsAgreedVersion
|
||||
) {
|
||||
slug
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
// ------ queries
|
||||
|
||||
// fill queries in here
|
||||
95
backend/src/db/graphql/groups.js
Normal file
95
backend/src/db/graphql/groups.js
Normal file
@ -0,0 +1,95 @@
|
||||
import gql from 'graphql-tag'
|
||||
|
||||
// ------ mutations
|
||||
|
||||
export const createGroupMutation = gql`
|
||||
mutation (
|
||||
$id: ID
|
||||
$name: String!
|
||||
$slug: String
|
||||
$about: String
|
||||
$description: String!
|
||||
$groupType: GroupType!
|
||||
$actionRadius: GroupActionRadius!
|
||||
$categoryIds: [ID]
|
||||
) {
|
||||
CreateGroup(
|
||||
id: $id
|
||||
name: $name
|
||||
slug: $slug
|
||||
about: $about
|
||||
description: $description
|
||||
groupType: $groupType
|
||||
actionRadius: $actionRadius
|
||||
categoryIds: $categoryIds
|
||||
) {
|
||||
id
|
||||
name
|
||||
slug
|
||||
createdAt
|
||||
updatedAt
|
||||
disabled
|
||||
deleted
|
||||
about
|
||||
description
|
||||
groupType
|
||||
actionRadius
|
||||
myRole
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
// ------ queries
|
||||
|
||||
export const groupQuery = gql`
|
||||
query (
|
||||
$isMember: Boolean
|
||||
$id: ID
|
||||
$name: String
|
||||
$slug: String
|
||||
$createdAt: String
|
||||
$updatedAt: String
|
||||
$about: String
|
||||
$description: String
|
||||
$locationName: String
|
||||
$first: Int
|
||||
$offset: Int
|
||||
$orderBy: [_GroupOrdering]
|
||||
$filter: _GroupFilter
|
||||
) {
|
||||
Group(
|
||||
isMember: $isMember
|
||||
id: $id
|
||||
name: $name
|
||||
slug: $slug
|
||||
createdAt: $createdAt
|
||||
updatedAt: $updatedAt
|
||||
about: $about
|
||||
description: $description
|
||||
locationName: $locationName
|
||||
first: $first
|
||||
offset: $offset
|
||||
orderBy: $orderBy
|
||||
filter: $filter
|
||||
) {
|
||||
id
|
||||
name
|
||||
slug
|
||||
createdAt
|
||||
updatedAt
|
||||
disabled
|
||||
deleted
|
||||
about
|
||||
description
|
||||
groupType
|
||||
actionRadius
|
||||
myRole
|
||||
categories {
|
||||
id
|
||||
slug
|
||||
name
|
||||
icon
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
15
backend/src/db/graphql/posts.js
Normal file
15
backend/src/db/graphql/posts.js
Normal file
@ -0,0 +1,15 @@
|
||||
import gql from 'graphql-tag'
|
||||
|
||||
// ------ mutations
|
||||
|
||||
export const createPostMutation = gql`
|
||||
mutation ($title: String!, $content: String!, $categoryIds: [ID]!, $slug: String) {
|
||||
CreatePost(title: $title, content: $content, categoryIds: $categoryIds, slug: $slug) {
|
||||
slug
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
// ------ queries
|
||||
|
||||
// fill queries in here
|
||||
@ -59,11 +59,11 @@ class Store {
|
||||
const session = driver.session()
|
||||
await createDefaultAdminUser(session)
|
||||
const writeTxResultPromise = session.writeTransaction(async (txc) => {
|
||||
await txc.run('CALL apoc.schema.assert({},{},true)') // drop all indices
|
||||
await txc.run('CALL apoc.schema.assert({},{},true)') // drop all indices and contraints
|
||||
return Promise.all(
|
||||
[
|
||||
'CALL db.index.fulltext.createNodeIndex("post_fulltext_search",["Post"],["title", "content"])',
|
||||
'CALL db.index.fulltext.createNodeIndex("user_fulltext_search",["User"],["name", "slug"])',
|
||||
'CALL db.index.fulltext.createNodeIndex("post_fulltext_search",["Post"],["title", "content"])',
|
||||
'CALL db.index.fulltext.createNodeIndex("tag_fulltext_search",["Tag"],["id"])',
|
||||
].map((statement) => txc.run(statement)),
|
||||
)
|
||||
|
||||
@ -0,0 +1,66 @@
|
||||
import { getDriver } from '../../db/neo4j'
|
||||
|
||||
export const description = `
|
||||
We introduced a new node label 'Group' and we need two primary keys 'id' and 'slug' for it.
|
||||
Additional we like to have fulltext indices the keys 'name', 'slug', 'about', and 'description'.
|
||||
`
|
||||
|
||||
export async function up(next) {
|
||||
const driver = getDriver()
|
||||
const session = driver.session()
|
||||
const transaction = session.beginTransaction()
|
||||
|
||||
try {
|
||||
// Implement your migration here.
|
||||
await transaction.run(`
|
||||
CREATE CONSTRAINT ON ( group:Group ) ASSERT group.id IS UNIQUE
|
||||
`)
|
||||
await transaction.run(`
|
||||
CREATE CONSTRAINT ON ( group:Group ) ASSERT group.slug IS UNIQUE
|
||||
`)
|
||||
await transaction.run(`
|
||||
CALL db.index.fulltext.createNodeIndex("group_fulltext_search",["Group"],["name", "slug", "about", "description"])
|
||||
`)
|
||||
await transaction.commit()
|
||||
next()
|
||||
} catch (error) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(error)
|
||||
await transaction.rollback()
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('rolled back')
|
||||
throw new Error(error)
|
||||
} finally {
|
||||
session.close()
|
||||
}
|
||||
}
|
||||
|
||||
export async function down(next) {
|
||||
const driver = getDriver()
|
||||
const session = driver.session()
|
||||
const transaction = session.beginTransaction()
|
||||
|
||||
try {
|
||||
// Implement your migration here.
|
||||
await transaction.run(`
|
||||
DROP CONSTRAINT ON ( group:Group ) ASSERT group.id IS UNIQUE
|
||||
`)
|
||||
await transaction.run(`
|
||||
DROP CONSTRAINT ON ( group:Group ) ASSERT group.slug IS UNIQUE
|
||||
`)
|
||||
await transaction.run(`
|
||||
CALL db.index.fulltext.drop("group_fulltext_search")
|
||||
`)
|
||||
await transaction.commit()
|
||||
next()
|
||||
} catch (error) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(error)
|
||||
await transaction.rollback()
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('rolled back')
|
||||
throw new Error(error)
|
||||
} finally {
|
||||
session.close()
|
||||
}
|
||||
}
|
||||
@ -1,3 +1,7 @@
|
||||
// TODO: can be replaced with: (which is no a fake)
|
||||
// import gql from 'graphql-tag'
|
||||
// See issue: https://github.com/Ocelot-Social-Community/Ocelot-Social/issues/5152
|
||||
|
||||
//* This is a fake ES2015 template string, just to benefit of syntax
|
||||
// highlighting of `gql` template strings in certain editors.
|
||||
export function gql(strings) {
|
||||
|
||||
@ -2,25 +2,25 @@ import trunc from 'trunc-html'
|
||||
|
||||
export default {
|
||||
Mutation: {
|
||||
CreateGroup: async (resolve, root, args, context, info) => {
|
||||
args.descriptionExcerpt = trunc(args.description, 120).html
|
||||
return resolve(root, args, context, info)
|
||||
},
|
||||
CreatePost: async (resolve, root, args, context, info) => {
|
||||
args.contentExcerpt = trunc(args.content, 120).html
|
||||
const result = await resolve(root, args, context, info)
|
||||
return result
|
||||
return resolve(root, args, context, info)
|
||||
},
|
||||
UpdatePost: async (resolve, root, args, context, info) => {
|
||||
args.contentExcerpt = trunc(args.content, 120).html
|
||||
const result = await resolve(root, args, context, info)
|
||||
return result
|
||||
return resolve(root, args, context, info)
|
||||
},
|
||||
CreateComment: async (resolve, root, args, context, info) => {
|
||||
args.contentExcerpt = trunc(args.content, 180).html
|
||||
const result = await resolve(root, args, context, info)
|
||||
return result
|
||||
return resolve(root, args, context, info)
|
||||
},
|
||||
UpdateComment: async (resolve, root, args, context, info) => {
|
||||
args.contentExcerpt = trunc(args.content, 180).html
|
||||
const result = await resolve(root, args, context, info)
|
||||
return result
|
||||
return resolve(root, args, context, info)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@ -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',
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -114,6 +114,7 @@ export default shield(
|
||||
reports: isModerator,
|
||||
statistics: allow,
|
||||
currentUser: allow,
|
||||
Group: isAuthenticated,
|
||||
Post: allow,
|
||||
profilePagePosts: allow,
|
||||
Comment: allow,
|
||||
@ -140,6 +141,7 @@ export default shield(
|
||||
Signup: or(publicRegistration, inviteRegistration, isAdmin),
|
||||
SignupVerification: allow,
|
||||
UpdateUser: onlyYourself,
|
||||
CreateGroup: isAuthenticated,
|
||||
CreatePost: isAuthenticated,
|
||||
UpdatePost: isAuthor,
|
||||
DeletePost: isAuthor,
|
||||
|
||||
@ -26,6 +26,10 @@ export default {
|
||||
args.slug = args.slug || (await uniqueSlug(args.name, isUniqueFor(context, 'User')))
|
||||
return resolve(root, args, context, info)
|
||||
},
|
||||
CreateGroup: 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)
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import slugify from 'slug'
|
||||
|
||||
export default async function uniqueSlug(string, isUnique) {
|
||||
const slug = slugify(string || 'anonymous', {
|
||||
lower: true,
|
||||
|
||||
@ -1,29 +1,33 @@
|
||||
import Factory, { cleanDatabase } from '../db/factories'
|
||||
import { gql } from '../helpers/jest'
|
||||
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 { createPostMutation } from '../db/graphql/posts'
|
||||
import { signupVerificationMutation } from '../db/graphql/authentications'
|
||||
|
||||
let mutate
|
||||
let authenticatedUser
|
||||
let variables
|
||||
|
||||
const driver = getDriver()
|
||||
const neode = getNeode()
|
||||
const descriptionAdditional100 =
|
||||
' 123456789-123456789-123456789-123456789-123456789-123456789-123456789-123456789-123456789-123456789'
|
||||
|
||||
const { server } = createServer({
|
||||
context: () => {
|
||||
return {
|
||||
driver,
|
||||
neode,
|
||||
user: authenticatedUser,
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
const { mutate } = createTestClient(server)
|
||||
|
||||
beforeAll(async () => {
|
||||
await cleanDatabase()
|
||||
|
||||
const { server } = createServer({
|
||||
context: () => {
|
||||
return {
|
||||
driver,
|
||||
neode,
|
||||
user: authenticatedUser,
|
||||
}
|
||||
},
|
||||
})
|
||||
mutate = createTestClient(server).mutate
|
||||
})
|
||||
|
||||
afterAll(async () => {
|
||||
@ -57,15 +61,136 @@ afterEach(async () => {
|
||||
})
|
||||
|
||||
describe('slugifyMiddleware', () => {
|
||||
describe('CreateGroup', () => {
|
||||
const categoryIds = ['cat9']
|
||||
|
||||
beforeEach(() => {
|
||||
variables = {
|
||||
...variables,
|
||||
name: 'The Best Group',
|
||||
about: 'Some about',
|
||||
description: 'Some description' + descriptionAdditional100,
|
||||
groupType: 'closed',
|
||||
actionRadius: 'national',
|
||||
categoryIds,
|
||||
}
|
||||
})
|
||||
|
||||
describe('if slug not exists', () => {
|
||||
it('generates a slug based on name', async () => {
|
||||
await expect(
|
||||
mutate({
|
||||
mutation: createGroupMutation,
|
||||
variables,
|
||||
}),
|
||||
).resolves.toMatchObject({
|
||||
data: {
|
||||
CreateGroup: {
|
||||
name: 'The Best Group',
|
||||
slug: 'the-best-group',
|
||||
about: 'Some about',
|
||||
description: 'Some description' + descriptionAdditional100,
|
||||
groupType: 'closed',
|
||||
actionRadius: 'national',
|
||||
},
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it('generates a slug based on given slug', async () => {
|
||||
await expect(
|
||||
mutate({
|
||||
mutation: createGroupMutation,
|
||||
variables: {
|
||||
...variables,
|
||||
slug: 'the-group',
|
||||
},
|
||||
}),
|
||||
).resolves.toMatchObject({
|
||||
data: {
|
||||
CreateGroup: {
|
||||
slug: 'the-group',
|
||||
},
|
||||
},
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('if slug exists', () => {
|
||||
beforeEach(async () => {
|
||||
await mutate({
|
||||
mutation: createGroupMutation,
|
||||
variables: {
|
||||
...variables,
|
||||
name: 'Pre-Existing Group',
|
||||
slug: 'pre-existing-group',
|
||||
about: 'As an about',
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it('chooses another slug', async () => {
|
||||
variables = {
|
||||
...variables,
|
||||
name: 'Pre-Existing Group',
|
||||
about: 'As an about',
|
||||
}
|
||||
await expect(
|
||||
mutate({
|
||||
mutation: createGroupMutation,
|
||||
variables,
|
||||
}),
|
||||
).resolves.toMatchObject({
|
||||
data: {
|
||||
CreateGroup: {
|
||||
slug: 'pre-existing-group-1',
|
||||
},
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
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 }),
|
||||
).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', () => {
|
||||
const categoryIds = ['cat9']
|
||||
const createPostMutation = gql`
|
||||
mutation ($title: String!, $content: String!, $categoryIds: [ID]!, $slug: String) {
|
||||
CreatePost(title: $title, content: $content, categoryIds: $categoryIds, slug: $slug) {
|
||||
slug
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
beforeEach(() => {
|
||||
variables = {
|
||||
@ -76,18 +201,38 @@ describe('slugifyMiddleware', () => {
|
||||
}
|
||||
})
|
||||
|
||||
it('generates a slug based on title', async () => {
|
||||
await expect(
|
||||
mutate({
|
||||
mutation: createPostMutation,
|
||||
variables,
|
||||
}),
|
||||
).resolves.toMatchObject({
|
||||
data: {
|
||||
CreatePost: {
|
||||
slug: 'i-am-a-brand-new-post',
|
||||
describe('if slug not exists', () => {
|
||||
it('generates a slug based on title', async () => {
|
||||
await expect(
|
||||
mutate({
|
||||
mutation: createPostMutation,
|
||||
variables,
|
||||
}),
|
||||
).resolves.toMatchObject({
|
||||
data: {
|
||||
CreatePost: {
|
||||
slug: 'i-am-a-brand-new-post',
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it('generates a slug based on given slug', async () => {
|
||||
await expect(
|
||||
mutate({
|
||||
mutation: createPostMutation,
|
||||
variables: {
|
||||
...variables,
|
||||
slug: 'the-post',
|
||||
},
|
||||
}),
|
||||
).resolves.toMatchObject({
|
||||
data: {
|
||||
CreatePost: {
|
||||
slug: 'the-post',
|
||||
},
|
||||
},
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -160,7 +305,7 @@ describe('slugifyMiddleware', () => {
|
||||
\`\`\`
|
||||
|
||||
Learn how to setup the database here:
|
||||
https://docs.human-connection.org/human-connection/backend#database-indices-and-constraints
|
||||
https://github.com/Ocelot-Social-Community/Ocelot-Social/blob/master/backend/README.md#database-indices-and-constraints
|
||||
`)
|
||||
}
|
||||
})
|
||||
@ -169,28 +314,6 @@ describe('slugifyMiddleware', () => {
|
||||
})
|
||||
|
||||
describe('SignupVerification', () => {
|
||||
const mutation = gql`
|
||||
mutation (
|
||||
$password: String!
|
||||
$email: String!
|
||||
$name: String!
|
||||
$slug: String
|
||||
$nonce: String!
|
||||
$termsAndConditionsAgreedVersion: String!
|
||||
) {
|
||||
SignupVerification(
|
||||
email: $email
|
||||
password: $password
|
||||
name: $name
|
||||
slug: $slug
|
||||
nonce: $nonce
|
||||
termsAndConditionsAgreedVersion: $termsAndConditionsAgreedVersion
|
||||
) {
|
||||
slug
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
beforeEach(() => {
|
||||
variables = {
|
||||
...variables,
|
||||
@ -211,18 +334,38 @@ describe('slugifyMiddleware', () => {
|
||||
})
|
||||
})
|
||||
|
||||
it('generates a slug based on name', async () => {
|
||||
await expect(
|
||||
mutate({
|
||||
mutation,
|
||||
variables,
|
||||
}),
|
||||
).resolves.toMatchObject({
|
||||
data: {
|
||||
SignupVerification: {
|
||||
slug: 'i-am-a-user',
|
||||
describe('if slug not exists', () => {
|
||||
it('generates a slug based on name', async () => {
|
||||
await expect(
|
||||
mutate({
|
||||
mutation: signupVerificationMutation,
|
||||
variables,
|
||||
}),
|
||||
).resolves.toMatchObject({
|
||||
data: {
|
||||
SignupVerification: {
|
||||
slug: 'i-am-a-user',
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it('generates a slug based on given slug', async () => {
|
||||
await expect(
|
||||
mutate({
|
||||
mutation: signupVerificationMutation,
|
||||
variables: {
|
||||
...variables,
|
||||
slug: 'the-user',
|
||||
},
|
||||
}),
|
||||
).resolves.toMatchObject({
|
||||
data: {
|
||||
SignupVerification: {
|
||||
slug: 'the-user',
|
||||
},
|
||||
},
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -237,7 +380,7 @@ describe('slugifyMiddleware', () => {
|
||||
it('chooses another slug', async () => {
|
||||
await expect(
|
||||
mutate({
|
||||
mutation,
|
||||
mutation: signupVerificationMutation,
|
||||
variables,
|
||||
}),
|
||||
).resolves.toMatchObject({
|
||||
@ -260,7 +403,7 @@ describe('slugifyMiddleware', () => {
|
||||
it('rejects SignupVerification (on FAIL Neo4j constraints may not defined in database)', async () => {
|
||||
await expect(
|
||||
mutate({
|
||||
mutation,
|
||||
mutation: signupVerificationMutation,
|
||||
variables,
|
||||
}),
|
||||
).resolves.toMatchObject({
|
||||
|
||||
46
backend/src/models/Group.js
Normal file
46
backend/src/models/Group.js
Normal file
@ -0,0 +1,46 @@
|
||||
import { v4 as uuid } from 'uuid'
|
||||
|
||||
export default {
|
||||
id: { type: 'string', primary: true, default: uuid }, // TODO: should be type: 'uuid' but simplified for our tests
|
||||
name: { type: 'string', disallow: [null], min: 3 },
|
||||
slug: { type: 'string', unique: 'true', regex: /^[a-z0-9_-]+$/, lowercase: true },
|
||||
|
||||
createdAt: {
|
||||
type: 'string',
|
||||
isoDate: true,
|
||||
required: true,
|
||||
default: () => new Date().toISOString(),
|
||||
},
|
||||
updatedAt: {
|
||||
type: 'string',
|
||||
isoDate: true,
|
||||
required: true,
|
||||
default: () => new Date().toISOString(),
|
||||
},
|
||||
deleted: { type: 'boolean', default: false },
|
||||
disabled: { type: 'boolean', default: false },
|
||||
|
||||
avatar: {
|
||||
type: 'relationship',
|
||||
relationship: 'AVATAR_IMAGE',
|
||||
target: 'Image',
|
||||
direction: 'out',
|
||||
},
|
||||
|
||||
about: { type: 'string', allow: [null, ''] },
|
||||
description: { type: 'string', disallow: [null], min: 100 },
|
||||
descriptionExcerpt: { type: 'string', allow: [null] },
|
||||
groupType: { type: 'string', default: 'public' },
|
||||
actionRadius: { type: 'string', default: 'regional' },
|
||||
|
||||
myRole: { type: 'string', default: 'pending' },
|
||||
|
||||
locationName: { type: 'string', allow: [null] },
|
||||
|
||||
isIn: {
|
||||
type: 'relationship',
|
||||
relationship: 'IS_IN',
|
||||
target: 'Location',
|
||||
direction: 'out',
|
||||
},
|
||||
}
|
||||
@ -55,7 +55,7 @@ describe('slug', () => {
|
||||
\`\`\`
|
||||
|
||||
Learn how to setup the database here:
|
||||
https://docs.human-connection.org/human-connection/backend#database-indices-and-constraints
|
||||
https://github.com/Ocelot-Social-Community/Ocelot-Social/blob/master/backend/README.md#database-indices-and-constraints
|
||||
`)
|
||||
}
|
||||
})
|
||||
|
||||
@ -4,6 +4,7 @@ export default {
|
||||
Image: require('./Image.js').default,
|
||||
Badge: require('./Badge.js').default,
|
||||
User: require('./User.js').default,
|
||||
Group: require('./Group.js').default,
|
||||
EmailAddress: require('./EmailAddress.js').default,
|
||||
UnverifiedEmailAddress: require('./UnverifiedEmailAddress.js').default,
|
||||
SocialMedia: require('./SocialMedia.js').default,
|
||||
|
||||
121
backend/src/schema/resolvers/groups.js
Normal file
121
backend/src/schema/resolvers/groups.js
Normal file
@ -0,0 +1,121 @@
|
||||
import { v4 as uuid } from 'uuid'
|
||||
import { UserInputError } from 'apollo-server'
|
||||
import CONFIG from '../../config'
|
||||
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'
|
||||
|
||||
export default {
|
||||
Query: {
|
||||
Group: async (_object, params, context, _resolveInfo) => {
|
||||
const { isMember } = params
|
||||
const session = context.driver.session()
|
||||
const readTxResultPromise = session.readTransaction(async (txc) => {
|
||||
let groupCypher
|
||||
if (isMember === true) {
|
||||
groupCypher = `
|
||||
MATCH (:User {id: $userId})-[membership:MEMBER_OF]->(group:Group)
|
||||
RETURN group {.*, myRole: membership.role}
|
||||
`
|
||||
} else {
|
||||
if (isMember === false) {
|
||||
groupCypher = `
|
||||
MATCH (group:Group)
|
||||
WHERE NOT (:User {id: $userId})-[:MEMBER_OF]->(group)
|
||||
RETURN group {.*, myRole: NULL}
|
||||
`
|
||||
} else {
|
||||
groupCypher = `
|
||||
MATCH (group:Group)
|
||||
OPTIONAL MATCH (:User {id: $userId})-[membership:MEMBER_OF]->(group)
|
||||
RETURN group {.*, myRole: membership.role}
|
||||
`
|
||||
}
|
||||
}
|
||||
const result = await txc.run(groupCypher, {
|
||||
userId: context.user.id,
|
||||
})
|
||||
return result.records.map((record) => record.get('group'))
|
||||
})
|
||||
try {
|
||||
return await readTxResultPromise
|
||||
} catch (error) {
|
||||
throw new Error(error)
|
||||
} finally {
|
||||
session.close()
|
||||
}
|
||||
},
|
||||
},
|
||||
Mutation: {
|
||||
CreateGroup: async (_parent, params, context, _resolveInfo) => {
|
||||
const { categoryIds } = params
|
||||
delete params.categoryIds
|
||||
if (CONFIG.CATEGORIES_ACTIVE && (!categoryIds || categoryIds.length < CATEGORIES_MIN)) {
|
||||
throw new UserInputError('Too view categories!')
|
||||
}
|
||||
if (CONFIG.CATEGORIES_ACTIVE && 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()
|
||||
const writeTxResultPromise = session.writeTransaction(async (transaction) => {
|
||||
const categoriesCypher =
|
||||
CONFIG.CATEGORIES_ACTIVE && categoryIds
|
||||
? `
|
||||
WITH group, membership
|
||||
UNWIND $categoryIds AS categoryId
|
||||
MATCH (category:Category {id: categoryId})
|
||||
MERGE (group)-[:CATEGORIZED]->(category)
|
||||
`
|
||||
: ''
|
||||
const ownerCreateGroupTransactionResponse = await transaction.run(
|
||||
`
|
||||
CREATE (group:Group)
|
||||
SET group += $params
|
||||
SET group.createdAt = toString(datetime())
|
||||
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())
|
||||
SET membership.role = 'owner'
|
||||
${categoriesCypher}
|
||||
RETURN group {.*, myRole: membership.role}
|
||||
`,
|
||||
{ userId: context.user.id, categoryIds, params },
|
||||
)
|
||||
const [group] = await ownerCreateGroupTransactionResponse.records.map((record) =>
|
||||
record.get('group'),
|
||||
)
|
||||
return group
|
||||
})
|
||||
try {
|
||||
const group = await writeTxResultPromise
|
||||
return group
|
||||
} 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()
|
||||
}
|
||||
},
|
||||
},
|
||||
Group: {
|
||||
...Resolver('Group', {
|
||||
hasMany: {
|
||||
categories: '-[:CATEGORIZED]->(related:Category)',
|
||||
},
|
||||
}),
|
||||
},
|
||||
}
|
||||
320
backend/src/schema/resolvers/groups.spec.js
Normal file
320
backend/src/schema/resolvers/groups.spec.js
Normal file
@ -0,0 +1,320 @@
|
||||
import { createTestClient } from 'apollo-server-testing'
|
||||
import Factory, { cleanDatabase } from '../../db/factories'
|
||||
import { createGroupMutation, groupQuery } from '../../db/graphql/groups'
|
||||
import { getNeode, getDriver } from '../../db/neo4j'
|
||||
import createServer from '../../server'
|
||||
import CONFIG from '../../config'
|
||||
|
||||
const driver = getDriver()
|
||||
const neode = getNeode()
|
||||
|
||||
let query
|
||||
let mutate
|
||||
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 () => {
|
||||
await cleanDatabase()
|
||||
|
||||
const { server } = createServer({
|
||||
context: () => {
|
||||
return {
|
||||
driver,
|
||||
neode,
|
||||
user: authenticatedUser,
|
||||
}
|
||||
},
|
||||
})
|
||||
query = createTestClient(server).query
|
||||
mutate = createTestClient(server).mutate
|
||||
})
|
||||
|
||||
afterAll(async () => {
|
||||
await cleanDatabase()
|
||||
})
|
||||
|
||||
beforeEach(async () => {
|
||||
variables = {}
|
||||
user = await Factory.build(
|
||||
'user',
|
||||
{
|
||||
id: 'current-user',
|
||||
name: 'TestUser',
|
||||
},
|
||||
{
|
||||
email: 'test@example.org',
|
||||
password: '1234',
|
||||
},
|
||||
)
|
||||
await Promise.all([
|
||||
neode.create('Category', {
|
||||
id: 'cat9',
|
||||
name: 'Democracy & Politics',
|
||||
slug: 'democracy-politics',
|
||||
icon: 'university',
|
||||
}),
|
||||
neode.create('Category', {
|
||||
id: 'cat4',
|
||||
name: 'Environment & Nature',
|
||||
slug: 'environment-nature',
|
||||
icon: 'tree',
|
||||
}),
|
||||
neode.create('Category', {
|
||||
id: 'cat15',
|
||||
name: 'Consumption & Sustainability',
|
||||
slug: 'consumption-sustainability',
|
||||
icon: 'shopping-cart',
|
||||
}),
|
||||
neode.create('Category', {
|
||||
id: 'cat27',
|
||||
name: 'Animal Protection',
|
||||
slug: 'animal-protection',
|
||||
icon: 'paw',
|
||||
}),
|
||||
])
|
||||
authenticatedUser = null
|
||||
})
|
||||
|
||||
// TODO: avoid database clean after each test in the future if possible for performance and flakyness reasons by filling the database step by step, see issue https://github.com/Ocelot-Social-Community/Ocelot-Social/issues/4543
|
||||
afterEach(async () => {
|
||||
await cleanDatabase()
|
||||
})
|
||||
|
||||
describe('Group', () => {
|
||||
describe('unauthenticated', () => {
|
||||
it('throws authorization error', async () => {
|
||||
const { errors } = await query({ query: groupQuery, variables: {} })
|
||||
expect(errors[0]).toHaveProperty('message', 'Not Authorised!')
|
||||
})
|
||||
})
|
||||
|
||||
describe('authenticated', () => {
|
||||
beforeEach(async () => {
|
||||
authenticatedUser = await user.toJson()
|
||||
})
|
||||
|
||||
let otherUser
|
||||
|
||||
beforeEach(async () => {
|
||||
otherUser = await Factory.build(
|
||||
'user',
|
||||
{
|
||||
id: 'other-user',
|
||||
name: 'Other TestUser',
|
||||
},
|
||||
{
|
||||
email: 'test2@example.org',
|
||||
password: '1234',
|
||||
},
|
||||
)
|
||||
authenticatedUser = await otherUser.toJson()
|
||||
await mutate({
|
||||
mutation: createGroupMutation,
|
||||
variables: {
|
||||
id: 'others-group',
|
||||
name: 'Uninteresting Group',
|
||||
about: 'We will change nothing!',
|
||||
description: 'We love it like it is!?' + descriptionAdditional100,
|
||||
groupType: 'closed',
|
||||
actionRadius: 'global',
|
||||
categoryIds,
|
||||
},
|
||||
})
|
||||
authenticatedUser = await user.toJson()
|
||||
await mutate({
|
||||
mutation: createGroupMutation,
|
||||
variables: {
|
||||
id: 'my-group',
|
||||
name: 'The Best Group',
|
||||
about: 'We will change the world!',
|
||||
description: 'Some description' + descriptionAdditional100,
|
||||
groupType: 'public',
|
||||
actionRadius: 'regional',
|
||||
categoryIds,
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
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)
|
||||
})
|
||||
})
|
||||
|
||||
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)
|
||||
})
|
||||
})
|
||||
|
||||
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('CreateGroup', () => {
|
||||
beforeEach(() => {
|
||||
variables = {
|
||||
...variables,
|
||||
id: 'g589',
|
||||
name: 'The Best Group',
|
||||
slug: 'the-group',
|
||||
about: 'We will change the world!',
|
||||
description: 'Some description' + descriptionAdditional100,
|
||||
groupType: 'public',
|
||||
actionRadius: 'regional',
|
||||
categoryIds,
|
||||
}
|
||||
})
|
||||
|
||||
describe('unauthenticated', () => {
|
||||
it('throws authorization error', async () => {
|
||||
const { errors } = await mutate({ mutation: createGroupMutation, variables })
|
||||
expect(errors[0]).toHaveProperty('message', 'Not Authorised!')
|
||||
})
|
||||
})
|
||||
|
||||
describe('authenticated', () => {
|
||||
beforeEach(async () => {
|
||||
authenticatedUser = await user.toJson()
|
||||
})
|
||||
|
||||
it('creates a group', async () => {
|
||||
const expected = {
|
||||
data: {
|
||||
CreateGroup: {
|
||||
name: 'The Best Group',
|
||||
slug: 'the-group',
|
||||
about: 'We will change the world!',
|
||||
},
|
||||
},
|
||||
errors: undefined,
|
||||
}
|
||||
await expect(mutate({ mutation: createGroupMutation, variables })).resolves.toMatchObject(
|
||||
expected,
|
||||
)
|
||||
})
|
||||
|
||||
it('assigns the authenticated user as owner', async () => {
|
||||
const expected = {
|
||||
data: {
|
||||
CreateGroup: {
|
||||
name: 'The Best Group',
|
||||
myRole: 'owner',
|
||||
},
|
||||
},
|
||||
errors: undefined,
|
||||
}
|
||||
await expect(mutate({ mutation: createGroupMutation, variables })).resolves.toMatchObject(
|
||||
expected,
|
||||
)
|
||||
})
|
||||
|
||||
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', () => {
|
||||
beforeEach(() => {
|
||||
CONFIG.CATEGORIES_ACTIVE = true
|
||||
})
|
||||
|
||||
describe('not even one', () => {
|
||||
it('throws error: "Too view categories!"', async () => {
|
||||
const { errors } = await mutate({
|
||||
mutation: createGroupMutation,
|
||||
variables: { ...variables, categoryIds: null },
|
||||
})
|
||||
expect(errors[0]).toHaveProperty('message', 'Too view categories!')
|
||||
})
|
||||
})
|
||||
|
||||
describe('four', () => {
|
||||
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', 'Too many categories!')
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
7
backend/src/schema/types/enum/GroupActionRadius.gql
Normal file
7
backend/src/schema/types/enum/GroupActionRadius.gql
Normal file
@ -0,0 +1,7 @@
|
||||
enum GroupActionRadius {
|
||||
regional
|
||||
national
|
||||
continental
|
||||
global
|
||||
interplanetary
|
||||
}
|
||||
6
backend/src/schema/types/enum/GroupMemberRole.gql
Normal file
6
backend/src/schema/types/enum/GroupMemberRole.gql
Normal file
@ -0,0 +1,6 @@
|
||||
enum GroupMemberRole {
|
||||
pending
|
||||
usual
|
||||
admin
|
||||
owner
|
||||
}
|
||||
5
backend/src/schema/types/enum/GroupType.gql
Normal file
5
backend/src/schema/types/enum/GroupType.gql
Normal file
@ -0,0 +1,5 @@
|
||||
enum GroupType {
|
||||
public
|
||||
closed
|
||||
hidden
|
||||
}
|
||||
109
backend/src/schema/types/type/Group.gql
Normal file
109
backend/src/schema/types/type/Group.gql
Normal file
@ -0,0 +1,109 @@
|
||||
enum _GroupOrdering {
|
||||
id_asc
|
||||
id_desc
|
||||
name_asc
|
||||
name_desc
|
||||
slug_asc
|
||||
slug_desc
|
||||
locationName_asc
|
||||
locationName_desc
|
||||
about_asc
|
||||
about_desc
|
||||
createdAt_asc
|
||||
createdAt_desc
|
||||
updatedAt_asc
|
||||
updatedAt_desc
|
||||
}
|
||||
|
||||
type Group {
|
||||
id: ID!
|
||||
name: String! # title
|
||||
slug: String!
|
||||
|
||||
createdAt: String!
|
||||
updatedAt: String!
|
||||
deleted: Boolean
|
||||
disabled: Boolean
|
||||
|
||||
avatar: Image @relation(name: "AVATAR_IMAGE", direction: "OUT")
|
||||
|
||||
about: String # goal
|
||||
description: String!
|
||||
groupType: GroupType!
|
||||
actionRadius: GroupActionRadius!
|
||||
|
||||
location: Location @cypher(statement: "MATCH (this)-[:IS_IN]->(l:Location) RETURN l")
|
||||
locationName: String
|
||||
|
||||
categories: [Category] @relation(name: "CATEGORIZED", direction: "OUT")
|
||||
|
||||
myRole: GroupMemberRole # if 'null' then the current user is no member
|
||||
}
|
||||
|
||||
|
||||
input _GroupFilter {
|
||||
AND: [_GroupFilter!]
|
||||
OR: [_GroupFilter!]
|
||||
name_contains: String
|
||||
slug_contains: String
|
||||
about_contains: String
|
||||
description_contains: String
|
||||
groupType_in: [GroupType!]
|
||||
actionRadius_in: [GroupActionRadius!]
|
||||
myRole_in: [GroupMemberRole!]
|
||||
id: ID
|
||||
id_not: ID
|
||||
id_in: [ID!]
|
||||
id_not_in: [ID!]
|
||||
}
|
||||
|
||||
type Query {
|
||||
Group(
|
||||
isMember: Boolean # if 'undefined' or 'null' then all groups
|
||||
id: ID
|
||||
name: String
|
||||
slug: String
|
||||
createdAt: String
|
||||
updatedAt: String
|
||||
about: String
|
||||
description: String
|
||||
locationName: String
|
||||
first: Int
|
||||
offset: Int
|
||||
orderBy: [_GroupOrdering]
|
||||
filter: _GroupFilter
|
||||
): [Group]
|
||||
|
||||
AvailableGroupTypes: [GroupType]!
|
||||
|
||||
AvailableGroupActionRadii: [GroupActionRadius]!
|
||||
|
||||
AvailableGroupMemberRoles: [GroupMemberRole]!
|
||||
}
|
||||
|
||||
type Mutation {
|
||||
CreateGroup(
|
||||
id: ID
|
||||
name: String!
|
||||
slug: String
|
||||
avatar: ImageInput
|
||||
about: String
|
||||
description: String!
|
||||
groupType: GroupType!
|
||||
actionRadius: GroupActionRadius!
|
||||
categoryIds: [ID]
|
||||
locationName: String
|
||||
): Group
|
||||
|
||||
UpdateGroup(
|
||||
id: ID!
|
||||
name: String
|
||||
slug: String
|
||||
avatar: ImageInput
|
||||
locationName: String
|
||||
about: String
|
||||
description: String
|
||||
): Group
|
||||
|
||||
DeleteGroup(id: ID!): Group
|
||||
}
|
||||
5
backend/src/schema/types/type/MEMBER_OF.gql
Normal file
5
backend/src/schema/types/type/MEMBER_OF.gql
Normal file
@ -0,0 +1,5 @@
|
||||
type MEMBER_OF {
|
||||
createdAt: String!
|
||||
updatedAt: String!
|
||||
role: GroupMemberRole!
|
||||
}
|
||||
@ -156,19 +156,19 @@ input _UserFilter {
|
||||
|
||||
type Query {
|
||||
User(
|
||||
id: ID
|
||||
email: String # admins need to search for a user sometimes
|
||||
name: String
|
||||
slug: String
|
||||
role: UserRole
|
||||
locationName: String
|
||||
about: String
|
||||
createdAt: String
|
||||
updatedAt: String
|
||||
first: Int
|
||||
offset: Int
|
||||
orderBy: [_UserOrdering]
|
||||
filter: _UserFilter
|
||||
id: ID
|
||||
email: String # admins need to search for a user sometimes
|
||||
name: String
|
||||
slug: String
|
||||
role: UserRole
|
||||
locationName: String
|
||||
about: String
|
||||
createdAt: String
|
||||
updatedAt: String
|
||||
first: Int
|
||||
offset: Int
|
||||
orderBy: [_UserOrdering]
|
||||
filter: _UserFilter
|
||||
): [User]
|
||||
|
||||
availableRoles: [UserRole]!
|
||||
@ -197,19 +197,19 @@ enum Deletable {
|
||||
|
||||
type Mutation {
|
||||
UpdateUser (
|
||||
id: ID!
|
||||
name: String
|
||||
email: String
|
||||
slug: String
|
||||
avatar: ImageInput
|
||||
locationName: String
|
||||
about: String
|
||||
termsAndConditionsAgreedVersion: String
|
||||
termsAndConditionsAgreedAt: String
|
||||
allowEmbedIframes: Boolean
|
||||
showShoutsPublicly: Boolean
|
||||
sendNotificationEmails: Boolean
|
||||
locale: String
|
||||
id: ID!
|
||||
name: String
|
||||
email: String
|
||||
slug: String
|
||||
avatar: ImageInput
|
||||
locationName: String
|
||||
about: String
|
||||
termsAndConditionsAgreedVersion: String
|
||||
termsAndConditionsAgreedAt: String
|
||||
allowEmbedIframes: Boolean
|
||||
showShoutsPublicly: Boolean
|
||||
sendNotificationEmails: Boolean
|
||||
locale: String
|
||||
): User
|
||||
|
||||
DeleteUser(id: ID!, resource: [Deletable]): User
|
||||
|
||||
@ -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==
|
||||
|
||||
@ -4,4 +4,4 @@ PUBLIC_REGISTRATION=false
|
||||
INVITE_REGISTRATION=true
|
||||
WEBSOCKETS_URI=ws://localhost:3000/api/graphql
|
||||
GRAPHQL_URI=http://localhost:4000/
|
||||
CATEGORIES_ACTIVE=false
|
||||
CATEGORIES_ACTIVE=false
|
||||
|
||||
3
webapp/constants/categories.js
Normal file
3
webapp/constants/categories.js
Normal 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
|
||||
2
webapp/constants/groups.js
Normal file
2
webapp/constants/groups.js
Normal 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
|
||||
Loading…
x
Reference in New Issue
Block a user