mirror of
https://github.com/IT4Change/Ocelot-Social.git
synced 2025-12-13 07:45:56 +00:00
Merge branch '5059-groups/5131-implement-group-gql-model-and-crud' into 5140-My-Groups-Page
This commit is contained in:
commit
fbf1ced7ba
6
.github/ISSUE_TEMPLATE/bug_report.md
vendored
6
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@ -1,8 +1,10 @@
|
|||||||
---
|
---
|
||||||
name: 🐛 Bug Report
|
name: "\U0001F41B Bug Report"
|
||||||
about: Create a report to help us to improve.
|
about: Create a report to help us to improve.
|
||||||
|
title: "\U0001F41B [Bug] XXX"
|
||||||
labels: bug
|
labels: bug
|
||||||
title: 🐛 [Bug]
|
assignees: ''
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## :bug: Bug Report
|
## :bug: Bug Report
|
||||||
|
|||||||
6
.github/ISSUE_TEMPLATE/devops_ticket.md
vendored
6
.github/ISSUE_TEMPLATE/devops_ticket.md
vendored
@ -1,8 +1,10 @@
|
|||||||
---
|
---
|
||||||
name: 💥 DevOps Ticket
|
name: "\U0001F4A5 DevOps Ticket"
|
||||||
about: Help us manage our deployed app.
|
about: Help us manage our deployed app.
|
||||||
|
title: "\U0001F4A5 [DevOps] XXX"
|
||||||
labels: devops
|
labels: devops
|
||||||
title: 💥 [DevOps]
|
assignees: ''
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 💥 DevOps Ticket
|
## 💥 DevOps Ticket
|
||||||
|
|||||||
7
.github/ISSUE_TEMPLATE/epic.md
vendored
7
.github/ISSUE_TEMPLATE/epic.md
vendored
@ -1,9 +1,12 @@
|
|||||||
---
|
---
|
||||||
name: 🌟 Epic
|
name: "\U0001F31F Epic"
|
||||||
about: Define a big development step.
|
about: Define a big development step.
|
||||||
|
title: "\U0001F31F [EPIC] XXX"
|
||||||
labels: epic
|
labels: epic
|
||||||
title: 🌟 [EPIC]
|
assignees: ''
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
<!-- THIS ISSUE-TYPE IS NOT FOR YOU! -->
|
<!-- THIS ISSUE-TYPE IS NOT FOR YOU! -->
|
||||||
<!-- If you need an answer right away, visit the ocelot.social Discord:
|
<!-- If you need an answer right away, visit the ocelot.social Discord:
|
||||||
https://discord.gg/AJSX9DCSUA -->
|
https://discord.gg/AJSX9DCSUA -->
|
||||||
|
|||||||
6
.github/ISSUE_TEMPLATE/feature_request.md
vendored
6
.github/ISSUE_TEMPLATE/feature_request.md
vendored
@ -1,8 +1,10 @@
|
|||||||
---
|
---
|
||||||
name: 🚀 Feature Request
|
name: "\U0001F680 Feature Request"
|
||||||
about: Suggest an idea for this project.
|
about: Suggest an idea for this project.
|
||||||
|
title: "\U0001F680 [Feature] XXX"
|
||||||
labels: feature
|
labels: feature
|
||||||
title: 🚀 [Feature]
|
assignees: ''
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## :rocket: Feature Request
|
## :rocket: Feature Request
|
||||||
|
|||||||
7
.github/ISSUE_TEMPLATE/question.md
vendored
7
.github/ISSUE_TEMPLATE/question.md
vendored
@ -1,9 +1,12 @@
|
|||||||
---
|
---
|
||||||
name: 💬 Question
|
name: "\U0001F4AC Question"
|
||||||
about: If you need help understanding ocelot.social.
|
about: If you need help understanding ocelot.social.
|
||||||
|
title: "\U0001F4AC [Question] XXX"
|
||||||
labels: question
|
labels: question
|
||||||
title: 💬 [Question]
|
assignees: ''
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
<!-- Chat with ocelot.social team -->
|
<!-- Chat with ocelot.social team -->
|
||||||
<!-- If you need an answer right away, visit the ocelot.social Discord:
|
<!-- If you need an answer right away, visit the ocelot.social Discord:
|
||||||
https://discord.gg/AJSX9DCSUA -->
|
https://discord.gg/AJSX9DCSUA -->
|
||||||
|
|||||||
7
.github/ISSUE_TEMPLATE/refactor_tickets.md
vendored
7
.github/ISSUE_TEMPLATE/refactor_tickets.md
vendored
@ -1,10 +1,11 @@
|
|||||||
---
|
---
|
||||||
name: 🔧 Refactor
|
name: "\U0001F527 Refactor"
|
||||||
about: Help us improve our code by refactoring it.
|
about: Help us improve our code by refactoring it.
|
||||||
|
title: "\U0001F527 [Refactor] XXX"
|
||||||
labels: refactor
|
labels: refactor
|
||||||
title: 🔧 [Refactor]
|
assignees: ''
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 🔧 Refactor
|
## 🔧 Refactor
|
||||||
<!-- Describe your issue in detail. Include screenshots if needed. Give us as much information as possible. Use a clear and concise description of what the problem is.-->
|
<!-- Describe your issue in detail. Include screenshots if needed. Give us as much information as possible. Use a clear and concise description of what the problem is.-->
|
||||||
|
|
||||||
|
|||||||
@ -4,8 +4,12 @@ All notable changes to this project will be documented in this file. Dates are d
|
|||||||
|
|
||||||
Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
|
Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
|
||||||
|
|
||||||
#### [1.0.9](https://github.com/Ocelot-Social-Community/Ocelot-Social/compare/1.0.8...1.0.9)
|
#### [1.1.0](https://github.com/Ocelot-Social-Community/Ocelot-Social/compare/1.0.8...1.1.0)
|
||||||
|
|
||||||
|
- feat: Make Categories Optional [`#5102`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/5102)
|
||||||
|
- Update issue templates [`#5101`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/5101)
|
||||||
|
- chore: 🍰 Betters Automatic Deployment To `stage.ocelot.social` On Push To `master` Branch [`#5097`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/5097)
|
||||||
|
- chore: 🍰 Release v1.0.9 [`#5095`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/5095)
|
||||||
- chore: 🍰 Automatic Deployment To `stage.ocelot.social` On Push To `master` Branch [`#5080`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/5080)
|
- chore: 🍰 Automatic Deployment To `stage.ocelot.social` On Push To `master` Branch [`#5080`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/5080)
|
||||||
- change footer version-link [`#5091`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/5091)
|
- change footer version-link [`#5091`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/5091)
|
||||||
- docs: 🍰 Add Neo4j Docu For Important Commands [`#5090`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/5090)
|
- docs: 🍰 Add Neo4j Docu For Important Commands [`#5090`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/5090)
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "ocelot-social-backend",
|
"name": "ocelot-social-backend",
|
||||||
"version": "1.0.9",
|
"version": "1.1.0",
|
||||||
"description": "GraphQL Backend for ocelot.social",
|
"description": "GraphQL Backend for ocelot.social",
|
||||||
"repository": "https://github.com/Ocelot-Social-Community/Ocelot-Social",
|
"repository": "https://github.com/Ocelot-Social-Community/Ocelot-Social",
|
||||||
"author": "ocelot.social Community",
|
"author": "ocelot.social Community",
|
||||||
|
|||||||
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
|
||||||
101
backend/src/db/graphql/groups.js
Normal file
101
backend/src/db/graphql/groups.js
Normal file
@ -0,0 +1,101 @@
|
|||||||
|
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
|
||||||
|
# Wolle: owner {
|
||||||
|
# name
|
||||||
|
# }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
// ------ queries
|
||||||
|
|
||||||
|
export const groupQuery = gql`
|
||||||
|
query (
|
||||||
|
$isMember: Boolean
|
||||||
|
$id: ID,
|
||||||
|
$name: String,
|
||||||
|
$slug: String,
|
||||||
|
$createdAt: String
|
||||||
|
$updatedAt: String
|
||||||
|
$about: String,
|
||||||
|
$description: String,
|
||||||
|
# $groupType: GroupType!,
|
||||||
|
# $actionRadius: GroupActionRadius!,
|
||||||
|
# $categoryIds: [ID]
|
||||||
|
$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
|
||||||
|
# groupType: $groupType
|
||||||
|
# actionRadius: $actionRadius
|
||||||
|
# categoryIds: $categoryIds
|
||||||
|
locationName: $locationName
|
||||||
|
first: $first
|
||||||
|
offset: $offset
|
||||||
|
orderBy: $orderBy
|
||||||
|
filter: $filter
|
||||||
|
) {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
slug
|
||||||
|
createdAt
|
||||||
|
updatedAt
|
||||||
|
disabled
|
||||||
|
deleted
|
||||||
|
about
|
||||||
|
description
|
||||||
|
groupType
|
||||||
|
actionRadius
|
||||||
|
myRole
|
||||||
|
# Wolle: owner {
|
||||||
|
# name
|
||||||
|
# }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
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()
|
const session = driver.session()
|
||||||
await createDefaultAdminUser(session)
|
await createDefaultAdminUser(session)
|
||||||
const writeTxResultPromise = session.writeTransaction(async (txc) => {
|
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(
|
return Promise.all(
|
||||||
[
|
[
|
||||||
'CALL db.index.fulltext.createNodeIndex("post_fulltext_search",["Post"],["title", "content"])',
|
|
||||||
'CALL db.index.fulltext.createNodeIndex("user_fulltext_search",["User"],["name", "slug"])',
|
'CALL db.index.fulltext.createNodeIndex("user_fulltext_search",["User"],["name", "slug"])',
|
||||||
|
'CALL db.index.fulltext.createNodeIndex("post_fulltext_search",["Post"],["title", "content"])',
|
||||||
'CALL db.index.fulltext.createNodeIndex("tag_fulltext_search",["Tag"],["id"])',
|
'CALL db.index.fulltext.createNodeIndex("tag_fulltext_search",["Tag"],["id"])',
|
||||||
].map((statement) => txc.run(statement)),
|
].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,6 @@
|
|||||||
|
// TODO: can be replaced with, which is no a fake:
|
||||||
|
// import gql from 'graphql-tag'
|
||||||
|
|
||||||
//* This is a fake ES2015 template string, just to benefit of syntax
|
//* This is a fake ES2015 template string, just to benefit of syntax
|
||||||
// highlighting of `gql` template strings in certain editors.
|
// highlighting of `gql` template strings in certain editors.
|
||||||
export function gql(strings) {
|
export function gql(strings) {
|
||||||
|
|||||||
@ -2,6 +2,11 @@ import trunc from 'trunc-html'
|
|||||||
|
|
||||||
export default {
|
export default {
|
||||||
Mutation: {
|
Mutation: {
|
||||||
|
CreateGroup: async (resolve, root, args, context, info) => {
|
||||||
|
args.descriptionExcerpt = trunc(args.description, 120).html
|
||||||
|
const result = await resolve(root, args, context, info)
|
||||||
|
return result
|
||||||
|
},
|
||||||
CreatePost: async (resolve, root, args, context, info) => {
|
CreatePost: async (resolve, root, args, context, info) => {
|
||||||
args.contentExcerpt = trunc(args.content, 120).html
|
args.contentExcerpt = trunc(args.content, 120).html
|
||||||
const result = await resolve(root, args, context, info)
|
const result = await resolve(root, args, context, info)
|
||||||
|
|||||||
@ -114,6 +114,7 @@ export default shield(
|
|||||||
reports: isModerator,
|
reports: isModerator,
|
||||||
statistics: allow,
|
statistics: allow,
|
||||||
currentUser: allow,
|
currentUser: allow,
|
||||||
|
Group: isAuthenticated,
|
||||||
Post: allow,
|
Post: allow,
|
||||||
profilePagePosts: allow,
|
profilePagePosts: allow,
|
||||||
Comment: allow,
|
Comment: allow,
|
||||||
@ -140,6 +141,7 @@ export default shield(
|
|||||||
Signup: or(publicRegistration, inviteRegistration, isAdmin),
|
Signup: or(publicRegistration, inviteRegistration, isAdmin),
|
||||||
SignupVerification: allow,
|
SignupVerification: allow,
|
||||||
UpdateUser: onlyYourself,
|
UpdateUser: onlyYourself,
|
||||||
|
CreateGroup: isAuthenticated,
|
||||||
CreatePost: isAuthenticated,
|
CreatePost: isAuthenticated,
|
||||||
UpdatePost: isAuthor,
|
UpdatePost: isAuthor,
|
||||||
DeletePost: isAuthor,
|
DeletePost: isAuthor,
|
||||||
|
|||||||
@ -26,6 +26,10 @@ export default {
|
|||||||
args.slug = args.slug || (await uniqueSlug(args.name, isUniqueFor(context, 'User')))
|
args.slug = args.slug || (await uniqueSlug(args.name, isUniqueFor(context, 'User')))
|
||||||
return resolve(root, args, context, info)
|
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) => {
|
CreatePost: async (resolve, root, args, context, info) => {
|
||||||
args.slug = args.slug || (await uniqueSlug(args.title, isUniqueFor(context, 'Post')))
|
args.slug = args.slug || (await uniqueSlug(args.title, isUniqueFor(context, 'Post')))
|
||||||
return resolve(root, args, context, info)
|
return resolve(root, args, context, info)
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
import slugify from 'slug'
|
import slugify from 'slug'
|
||||||
|
|
||||||
export default async function uniqueSlug(string, isUnique) {
|
export default async function uniqueSlug(string, isUnique) {
|
||||||
const slug = slugify(string || 'anonymous', {
|
const slug = slugify(string || 'anonymous', {
|
||||||
lower: true,
|
lower: true,
|
||||||
|
|||||||
@ -1,8 +1,10 @@
|
|||||||
import Factory, { cleanDatabase } from '../db/factories'
|
|
||||||
import { gql } from '../helpers/jest'
|
|
||||||
import { getNeode, getDriver } from '../db/neo4j'
|
import { getNeode, getDriver } from '../db/neo4j'
|
||||||
import createServer from '../server'
|
import createServer from '../server'
|
||||||
import { createTestClient } from 'apollo-server-testing'
|
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 mutate
|
||||||
let authenticatedUser
|
let authenticatedUser
|
||||||
@ -57,15 +59,136 @@ afterEach(async () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
describe('slugifyMiddleware', () => {
|
describe('slugifyMiddleware', () => {
|
||||||
|
describe('CreateGroup', () => {
|
||||||
|
const categoryIds = ['cat9']
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
variables = {
|
||||||
|
...variables,
|
||||||
|
name: 'The Best Group',
|
||||||
|
about: 'Some about',
|
||||||
|
description: 'Some description',
|
||||||
|
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',
|
||||||
|
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', () => {
|
describe('CreatePost', () => {
|
||||||
const categoryIds = ['cat9']
|
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(() => {
|
beforeEach(() => {
|
||||||
variables = {
|
variables = {
|
||||||
@ -76,18 +199,38 @@ describe('slugifyMiddleware', () => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
it('generates a slug based on title', async () => {
|
describe('if slug not exists', () => {
|
||||||
await expect(
|
it('generates a slug based on title', async () => {
|
||||||
mutate({
|
await expect(
|
||||||
mutation: createPostMutation,
|
mutate({
|
||||||
variables,
|
mutation: createPostMutation,
|
||||||
}),
|
variables,
|
||||||
).resolves.toMatchObject({
|
}),
|
||||||
data: {
|
).resolves.toMatchObject({
|
||||||
CreatePost: {
|
data: {
|
||||||
slug: 'i-am-a-brand-new-post',
|
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 +303,7 @@ describe('slugifyMiddleware', () => {
|
|||||||
\`\`\`
|
\`\`\`
|
||||||
|
|
||||||
Learn how to setup the database here:
|
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 +312,6 @@ describe('slugifyMiddleware', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
describe('SignupVerification', () => {
|
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(() => {
|
beforeEach(() => {
|
||||||
variables = {
|
variables = {
|
||||||
...variables,
|
...variables,
|
||||||
@ -211,18 +332,38 @@ describe('slugifyMiddleware', () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
it('generates a slug based on name', async () => {
|
describe('if slug not exists', () => {
|
||||||
await expect(
|
it('generates a slug based on name', async () => {
|
||||||
mutate({
|
await expect(
|
||||||
mutation,
|
mutate({
|
||||||
variables,
|
mutation: signupVerificationMutation,
|
||||||
}),
|
variables,
|
||||||
).resolves.toMatchObject({
|
}),
|
||||||
data: {
|
).resolves.toMatchObject({
|
||||||
SignupVerification: {
|
data: {
|
||||||
slug: 'i-am-a-user',
|
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 +378,7 @@ describe('slugifyMiddleware', () => {
|
|||||||
it('chooses another slug', async () => {
|
it('chooses another slug', async () => {
|
||||||
await expect(
|
await expect(
|
||||||
mutate({
|
mutate({
|
||||||
mutation,
|
mutation: signupVerificationMutation,
|
||||||
variables,
|
variables,
|
||||||
}),
|
}),
|
||||||
).resolves.toMatchObject({
|
).resolves.toMatchObject({
|
||||||
@ -260,7 +401,7 @@ describe('slugifyMiddleware', () => {
|
|||||||
it('rejects SignupVerification (on FAIL Neo4j constraints may not defined in database)', async () => {
|
it('rejects SignupVerification (on FAIL Neo4j constraints may not defined in database)', async () => {
|
||||||
await expect(
|
await expect(
|
||||||
mutate({
|
mutate({
|
||||||
mutation,
|
mutation: signupVerificationMutation,
|
||||||
variables,
|
variables,
|
||||||
}),
|
}),
|
||||||
).resolves.toMatchObject({
|
).resolves.toMatchObject({
|
||||||
|
|||||||
146
backend/src/models/Group.js
Normal file
146
backend/src/models/Group.js
Normal file
@ -0,0 +1,146 @@
|
|||||||
|
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] },
|
||||||
|
|
||||||
|
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],
|
||||||
|
// },
|
||||||
|
}
|
||||||
@ -55,7 +55,7 @@ describe('slug', () => {
|
|||||||
\`\`\`
|
\`\`\`
|
||||||
|
|
||||||
Learn how to setup the database here:
|
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,
|
Image: require('./Image.js').default,
|
||||||
Badge: require('./Badge.js').default,
|
Badge: require('./Badge.js').default,
|
||||||
User: require('./User.js').default,
|
User: require('./User.js').default,
|
||||||
|
Group: require('./Group.js').default,
|
||||||
EmailAddress: require('./EmailAddress.js').default,
|
EmailAddress: require('./EmailAddress.js').default,
|
||||||
UnverifiedEmailAddress: require('./UnverifiedEmailAddress.js').default,
|
UnverifiedEmailAddress: require('./UnverifiedEmailAddress.js').default,
|
||||||
SocialMedia: require('./SocialMedia.js').default,
|
SocialMedia: require('./SocialMedia.js').default,
|
||||||
|
|||||||
218
backend/src/schema/resolvers/groups.js
Normal file
218
backend/src/schema/resolvers/groups.js
Normal file
@ -0,0 +1,218 @@
|
|||||||
|
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'
|
||||||
|
// Wolle: import { mergeImage, deleteImage } from './images/images'
|
||||||
|
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()
|
||||||
|
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,
|
||||||
|
})
|
||||||
|
const group = result.records.map((record) => record.get('group'))
|
||||||
|
return group
|
||||||
|
})
|
||||||
|
try {
|
||||||
|
const group = await readTxResultPromise
|
||||||
|
return group
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error(error)
|
||||||
|
} finally {
|
||||||
|
session.close()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Mutation: {
|
||||||
|
CreateGroup: async (_parent, params, context, _resolveInfo) => {
|
||||||
|
const { categoryIds } = params
|
||||||
|
delete params.categoryIds
|
||||||
|
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)-[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] = 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()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
// 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',
|
||||||
|
// },
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
}
|
||||||
720
backend/src/schema/resolvers/groups.spec.js
Normal file
720
backend/src/schema/resolvers/groups.spec.js
Normal file
@ -0,0 +1,720 @@
|
|||||||
|
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'
|
||||||
|
|
||||||
|
const driver = getDriver()
|
||||||
|
const neode = getNeode()
|
||||||
|
|
||||||
|
let query
|
||||||
|
let mutate
|
||||||
|
let authenticatedUser
|
||||||
|
let user
|
||||||
|
|
||||||
|
const categoryIds = ['cat9', 'cat4', 'cat15']
|
||||||
|
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',
|
||||||
|
icon: 'university',
|
||||||
|
}),
|
||||||
|
neode.create('Category', {
|
||||||
|
id: 'cat4',
|
||||||
|
name: 'Environment & Nature',
|
||||||
|
icon: 'tree',
|
||||||
|
}),
|
||||||
|
neode.create('Category', {
|
||||||
|
id: 'cat15',
|
||||||
|
name: 'Consumption & Sustainability',
|
||||||
|
icon: 'shopping-cart',
|
||||||
|
}),
|
||||||
|
neode.create('Category', {
|
||||||
|
id: 'cat27',
|
||||||
|
name: '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!?',
|
||||||
|
groupType: 'closed',
|
||||||
|
actionRadius: 'international',
|
||||||
|
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',
|
||||||
|
groupType: 'public',
|
||||||
|
actionRadius: 'regional',
|
||||||
|
categoryIds,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('query can fetch', () => {
|
||||||
|
it('groups 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)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('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)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('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)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
// Wolle: describe('can be filtered', () => {
|
||||||
|
// 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),
|
||||||
|
// },
|
||||||
|
// })
|
||||||
|
// })
|
||||||
|
// })
|
||||||
|
// /* 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)
|
||||||
|
// }) */
|
||||||
|
// 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,
|
||||||
|
// })
|
||||||
|
// })
|
||||||
|
// })
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('CreateGroup', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
variables = {
|
||||||
|
...variables,
|
||||||
|
id: 'g589',
|
||||||
|
name: 'The Best Group',
|
||||||
|
slug: 'the-group',
|
||||||
|
about: 'We will change the world!',
|
||||||
|
description: 'Some description',
|
||||||
|
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',
|
||||||
|
// Wolle: owner: {
|
||||||
|
// name: 'TestUser',
|
||||||
|
// },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
errors: undefined,
|
||||||
|
}
|
||||||
|
await expect(mutate({ mutation: createGroupMutation, variables })).resolves.toMatchObject(
|
||||||
|
expected,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('`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('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,
|
||||||
|
// )
|
||||||
|
// })
|
||||||
|
// })
|
||||||
|
// })
|
||||||
|
// })
|
||||||
6
backend/src/schema/types/enum/GroupActionRadius.gql
Normal file
6
backend/src/schema/types/enum/GroupActionRadius.gql
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
enum GroupActionRadius {
|
||||||
|
regional
|
||||||
|
national
|
||||||
|
continental
|
||||||
|
international
|
||||||
|
}
|
||||||
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
|
||||||
|
}
|
||||||
257
backend/src/schema/types/type/Group.gql
Normal file
257
backend/src/schema/types/type/Group.gql
Normal file
@ -0,0 +1,257 @@
|
|||||||
|
enum _GroupOrdering {
|
||||||
|
id_asc
|
||||||
|
id_desc
|
||||||
|
name_asc
|
||||||
|
name_desc
|
||||||
|
slug_asc
|
||||||
|
slug_desc
|
||||||
|
locationName_asc
|
||||||
|
locationName_desc
|
||||||
|
about_asc
|
||||||
|
about_desc
|
||||||
|
createdAt_asc
|
||||||
|
createdAt_desc
|
||||||
|
updatedAt_asc
|
||||||
|
updatedAt_desc
|
||||||
|
# Wolle: needed? locale_asc
|
||||||
|
# locale_desc
|
||||||
|
}
|
||||||
|
|
||||||
|
type Group {
|
||||||
|
id: ID!
|
||||||
|
name: String! # title
|
||||||
|
slug: String!
|
||||||
|
|
||||||
|
createdAt: String!
|
||||||
|
updatedAt: String!
|
||||||
|
deleted: Boolean
|
||||||
|
disabled: Boolean
|
||||||
|
|
||||||
|
avatar: Image @relation(name: "AVATAR_IMAGE", direction: "OUT")
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
# 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]
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
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!]
|
||||||
|
# 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 {
|
||||||
|
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]!
|
||||||
|
|
||||||
|
# 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
|
||||||
|
name: String!
|
||||||
|
slug: String
|
||||||
|
avatar: ImageInput
|
||||||
|
about: String
|
||||||
|
description: String!
|
||||||
|
groupType: GroupType!
|
||||||
|
actionRadius: GroupActionRadius!
|
||||||
|
categoryIds: [ID]
|
||||||
|
locationName: String
|
||||||
|
): # Wolle: add group settings
|
||||||
|
# Wolle:
|
||||||
|
# showShoutsPublicly: Boolean
|
||||||
|
# sendNotificationEmails: Boolean
|
||||||
|
# locale: String
|
||||||
|
Group
|
||||||
|
|
||||||
|
UpdateGroup(
|
||||||
|
id: ID!
|
||||||
|
name: String
|
||||||
|
slug: String
|
||||||
|
avatar: ImageInput
|
||||||
|
locationName: String
|
||||||
|
about: String
|
||||||
|
description: String
|
||||||
|
): # Wolle:
|
||||||
|
# showShoutsPublicly: Boolean
|
||||||
|
# sendNotificationEmails: Boolean
|
||||||
|
# locale: String
|
||||||
|
Group
|
||||||
|
|
||||||
|
DeleteGroup(id: ID!): Group
|
||||||
|
|
||||||
|
# Wolle:
|
||||||
|
# muteUser(id: ID!): User
|
||||||
|
# unmuteUser(id: ID!): User
|
||||||
|
# blockUser(id: ID!): User
|
||||||
|
# unblockUser(id: ID!): User
|
||||||
|
|
||||||
|
# Wolle: switchUserRole(role: UserRole!, id: ID!): User
|
||||||
|
}
|
||||||
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 {
|
type Query {
|
||||||
User(
|
User(
|
||||||
id: ID
|
id: ID
|
||||||
email: String # admins need to search for a user sometimes
|
email: String # admins need to search for a user sometimes
|
||||||
name: String
|
name: String
|
||||||
slug: String
|
slug: String
|
||||||
role: UserGroup
|
role: UserGroup
|
||||||
locationName: String
|
locationName: String
|
||||||
about: String
|
about: String
|
||||||
createdAt: String
|
createdAt: String
|
||||||
updatedAt: String
|
updatedAt: String
|
||||||
first: Int
|
first: Int
|
||||||
offset: Int
|
offset: Int
|
||||||
orderBy: [_UserOrdering]
|
orderBy: [_UserOrdering]
|
||||||
filter: _UserFilter
|
filter: _UserFilter
|
||||||
): [User]
|
): [User]
|
||||||
|
|
||||||
availableRoles: [UserGroup]!
|
availableRoles: [UserGroup]!
|
||||||
@ -197,19 +197,19 @@ enum Deletable {
|
|||||||
|
|
||||||
type Mutation {
|
type Mutation {
|
||||||
UpdateUser (
|
UpdateUser (
|
||||||
id: ID!
|
id: ID!
|
||||||
name: String
|
name: String
|
||||||
email: String
|
email: String
|
||||||
slug: String
|
slug: String
|
||||||
avatar: ImageInput
|
avatar: ImageInput
|
||||||
locationName: String
|
locationName: String
|
||||||
about: String
|
about: String
|
||||||
termsAndConditionsAgreedVersion: String
|
termsAndConditionsAgreedVersion: String
|
||||||
termsAndConditionsAgreedAt: String
|
termsAndConditionsAgreedAt: String
|
||||||
allowEmbedIframes: Boolean
|
allowEmbedIframes: Boolean
|
||||||
showShoutsPublicly: Boolean
|
showShoutsPublicly: Boolean
|
||||||
sendNotificationEmails: Boolean
|
sendNotificationEmails: Boolean
|
||||||
locale: String
|
locale: String
|
||||||
): User
|
): User
|
||||||
|
|
||||||
DeleteUser(id: ID!, resource: [Deletable]): User
|
DeleteUser(id: ID!, resource: [Deletable]): User
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "ocelot-social",
|
"name": "ocelot-social",
|
||||||
"version": "1.0.9",
|
"version": "1.1.0",
|
||||||
"description": "Free and open source software program code available to run social networks.",
|
"description": "Free and open source software program code available to run social networks.",
|
||||||
"author": "ocelot.social Community",
|
"author": "ocelot.social Community",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@ocelot-social/maintenance",
|
"name": "@ocelot-social/maintenance",
|
||||||
"version": "1.0.9",
|
"version": "1.1.0",
|
||||||
"description": "Maintenance page for ocelot.social",
|
"description": "Maintenance page for ocelot.social",
|
||||||
"repository": "https://github.com/Ocelot-Social-Community/Ocelot-Social",
|
"repository": "https://github.com/Ocelot-Social-Community/Ocelot-Social",
|
||||||
"author": "ocelot.social Community",
|
"author": "ocelot.social Community",
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "ocelot-social-webapp",
|
"name": "ocelot-social-webapp",
|
||||||
"version": "1.0.9",
|
"version": "1.1.0",
|
||||||
"description": "ocelot.social Frontend",
|
"description": "ocelot.social Frontend",
|
||||||
"repository": "https://github.com/Ocelot-Social-Community/Ocelot-Social",
|
"repository": "https://github.com/Ocelot-Social-Community/Ocelot-Social",
|
||||||
"author": "ocelot.social Community",
|
"author": "ocelot.social Community",
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user