Skip to content

Commit 032cf7c

Browse files
authored
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
2 parents d61506a + beacad4 commit 032cf7c

32 files changed

+1157
-149
lines changed

backend/.env.template

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,4 +29,4 @@ AWS_BUCKET=
2929
EMAIL_DEFAULT_SENDER="[email protected]"
3030
EMAIL_SUPPORT="[email protected]"
3131

32-
CATEGORIES_ACTIVE=false
32+
CATEGORIES_ACTIVE=false

backend/package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
"dev": "nodemon --exec babel-node src/ -e js,gql",
1616
"dev:debug": "nodemon --exec babel-node --inspect=0.0.0.0:9229 src/ -e js,gql",
1717
"lint": "eslint src --config .eslintrc.js",
18-
"test": "cross-env NODE_ENV=test jest --forceExit --detectOpenHandles --runInBand --coverage",
18+
"test": "cross-env NODE_ENV=test NODE_OPTIONS=--max-old-space-size=8192 jest --forceExit --detectOpenHandles --runInBand --coverage",
1919
"db:clean": "babel-node src/db/clean.js",
2020
"db:reset": "yarn run db:clean",
2121
"db:seed": "babel-node src/db/seed.js",
@@ -103,7 +103,7 @@
103103
"mustache": "^4.2.0",
104104
"neo4j-driver": "^4.0.2",
105105
"neo4j-graphql-js": "^2.11.5",
106-
"neode": "^0.4.7",
106+
"neode": "^0.4.8",
107107
"node-fetch": "~2.6.1",
108108
"nodemailer": "^6.4.4",
109109
"nodemailer-html-to-text": "^3.2.0",

backend/src/constants/categories.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
// this file is duplicated in `backend/src/constants/metadata.js` and `webapp/constants/metadata.js`
2+
export const CATEGORIES_MIN = 1
3+
export const CATEGORIES_MAX = 3

backend/src/constants/groups.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
// this file is duplicated in `backend/src/constants/group.js` and `webapp/constants/group.js`
2+
export const DESCRIPTION_WITHOUT_HTML_LENGTH_MIN = 100 // with removed HTML tags
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import gql from 'graphql-tag'
2+
3+
// ------ mutations
4+
5+
export const signupVerificationMutation = gql`
6+
mutation (
7+
$password: String!
8+
$email: String!
9+
$name: String!
10+
$slug: String
11+
$nonce: String!
12+
$termsAndConditionsAgreedVersion: String!
13+
) {
14+
SignupVerification(
15+
email: $email
16+
password: $password
17+
name: $name
18+
slug: $slug
19+
nonce: $nonce
20+
termsAndConditionsAgreedVersion: $termsAndConditionsAgreedVersion
21+
) {
22+
slug
23+
}
24+
}
25+
`
26+
27+
// ------ queries
28+
29+
// fill queries in here

backend/src/db/graphql/groups.js

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
import gql from 'graphql-tag'
2+
3+
// ------ mutations
4+
5+
export const createGroupMutation = gql`
6+
mutation (
7+
$id: ID
8+
$name: String!
9+
$slug: String
10+
$about: String
11+
$description: String!
12+
$groupType: GroupType!
13+
$actionRadius: GroupActionRadius!
14+
$categoryIds: [ID]
15+
) {
16+
CreateGroup(
17+
id: $id
18+
name: $name
19+
slug: $slug
20+
about: $about
21+
description: $description
22+
groupType: $groupType
23+
actionRadius: $actionRadius
24+
categoryIds: $categoryIds
25+
) {
26+
id
27+
name
28+
slug
29+
createdAt
30+
updatedAt
31+
disabled
32+
deleted
33+
about
34+
description
35+
groupType
36+
actionRadius
37+
myRole
38+
}
39+
}
40+
`
41+
42+
// ------ queries
43+
44+
export const groupQuery = gql`
45+
query (
46+
$isMember: Boolean
47+
$id: ID
48+
$name: String
49+
$slug: String
50+
$createdAt: String
51+
$updatedAt: String
52+
$about: String
53+
$description: String
54+
$locationName: String
55+
$first: Int
56+
$offset: Int
57+
$orderBy: [_GroupOrdering]
58+
$filter: _GroupFilter
59+
) {
60+
Group(
61+
isMember: $isMember
62+
id: $id
63+
name: $name
64+
slug: $slug
65+
createdAt: $createdAt
66+
updatedAt: $updatedAt
67+
about: $about
68+
description: $description
69+
locationName: $locationName
70+
first: $first
71+
offset: $offset
72+
orderBy: $orderBy
73+
filter: $filter
74+
) {
75+
id
76+
name
77+
slug
78+
createdAt
79+
updatedAt
80+
disabled
81+
deleted
82+
about
83+
description
84+
groupType
85+
actionRadius
86+
myRole
87+
categories {
88+
id
89+
slug
90+
name
91+
icon
92+
}
93+
}
94+
}
95+
`

backend/src/db/graphql/posts.js

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import gql from 'graphql-tag'
2+
3+
// ------ mutations
4+
5+
export const createPostMutation = gql`
6+
mutation ($title: String!, $content: String!, $categoryIds: [ID]!, $slug: String) {
7+
CreatePost(title: $title, content: $content, categoryIds: $categoryIds, slug: $slug) {
8+
slug
9+
}
10+
}
11+
`
12+
13+
// ------ queries
14+
15+
// fill queries in here

backend/src/db/migrate/store.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,11 +59,11 @@ class Store {
5959
const session = driver.session()
6060
await createDefaultAdminUser(session)
6161
const writeTxResultPromise = session.writeTransaction(async (txc) => {
62-
await txc.run('CALL apoc.schema.assert({},{},true)') // drop all indices
62+
await txc.run('CALL apoc.schema.assert({},{},true)') // drop all indices and contraints
6363
return Promise.all(
6464
[
65-
'CALL db.index.fulltext.createNodeIndex("post_fulltext_search",["Post"],["title", "content"])',
6665
'CALL db.index.fulltext.createNodeIndex("user_fulltext_search",["User"],["name", "slug"])',
66+
'CALL db.index.fulltext.createNodeIndex("post_fulltext_search",["Post"],["title", "content"])',
6767
'CALL db.index.fulltext.createNodeIndex("tag_fulltext_search",["Tag"],["id"])',
6868
].map((statement) => txc.run(statement)),
6969
)
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import { getDriver } from '../../db/neo4j'
2+
3+
export const description = `
4+
We introduced a new node label 'Group' and we need two primary keys 'id' and 'slug' for it.
5+
Additional we like to have fulltext indices the keys 'name', 'slug', 'about', and 'description'.
6+
`
7+
8+
export async function up(next) {
9+
const driver = getDriver()
10+
const session = driver.session()
11+
const transaction = session.beginTransaction()
12+
13+
try {
14+
// Implement your migration here.
15+
await transaction.run(`
16+
CREATE CONSTRAINT ON ( group:Group ) ASSERT group.id IS UNIQUE
17+
`)
18+
await transaction.run(`
19+
CREATE CONSTRAINT ON ( group:Group ) ASSERT group.slug IS UNIQUE
20+
`)
21+
await transaction.run(`
22+
CALL db.index.fulltext.createNodeIndex("group_fulltext_search",["Group"],["name", "slug", "about", "description"])
23+
`)
24+
await transaction.commit()
25+
next()
26+
} catch (error) {
27+
// eslint-disable-next-line no-console
28+
console.log(error)
29+
await transaction.rollback()
30+
// eslint-disable-next-line no-console
31+
console.log('rolled back')
32+
throw new Error(error)
33+
} finally {
34+
session.close()
35+
}
36+
}
37+
38+
export async function down(next) {
39+
const driver = getDriver()
40+
const session = driver.session()
41+
const transaction = session.beginTransaction()
42+
43+
try {
44+
// Implement your migration here.
45+
await transaction.run(`
46+
DROP CONSTRAINT ON ( group:Group ) ASSERT group.id IS UNIQUE
47+
`)
48+
await transaction.run(`
49+
DROP CONSTRAINT ON ( group:Group ) ASSERT group.slug IS UNIQUE
50+
`)
51+
await transaction.run(`
52+
CALL db.index.fulltext.drop("group_fulltext_search")
53+
`)
54+
await transaction.commit()
55+
next()
56+
} catch (error) {
57+
// eslint-disable-next-line no-console
58+
console.log(error)
59+
await transaction.rollback()
60+
// eslint-disable-next-line no-console
61+
console.log('rolled back')
62+
throw new Error(error)
63+
} finally {
64+
session.close()
65+
}
66+
}

backend/src/helpers/jest.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
// TODO: can be replaced with: (which is no a fake)
2+
// import gql from 'graphql-tag'
3+
// See issue: https://github.com/Ocelot-Social-Community/Ocelot-Social/issues/5152
4+
15
//* This is a fake ES2015 template string, just to benefit of syntax
26
// highlighting of `gql` template strings in certain editors.
37
export function gql(strings) {

backend/src/middleware/excerptMiddleware.js

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,25 +2,25 @@ import trunc from 'trunc-html'
22

33
export default {
44
Mutation: {
5+
CreateGroup: async (resolve, root, args, context, info) => {
6+
args.descriptionExcerpt = trunc(args.description, 120).html
7+
return resolve(root, args, context, info)
8+
},
59
CreatePost: async (resolve, root, args, context, info) => {
610
args.contentExcerpt = trunc(args.content, 120).html
7-
const result = await resolve(root, args, context, info)
8-
return result
11+
return resolve(root, args, context, info)
912
},
1013
UpdatePost: async (resolve, root, args, context, info) => {
1114
args.contentExcerpt = trunc(args.content, 120).html
12-
const result = await resolve(root, args, context, info)
13-
return result
15+
return resolve(root, args, context, info)
1416
},
1517
CreateComment: async (resolve, root, args, context, info) => {
1618
args.contentExcerpt = trunc(args.content, 180).html
17-
const result = await resolve(root, args, context, info)
18-
return result
19+
return resolve(root, args, context, info)
1920
},
2021
UpdateComment: async (resolve, root, args, context, info) => {
2122
args.contentExcerpt = trunc(args.content, 180).html
22-
const result = await resolve(root, args, context, info)
23-
return result
23+
return resolve(root, args, context, info)
2424
},
2525
},
2626
}

backend/src/middleware/helpers/cleanHtml.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,13 @@
11
import sanitizeHtml from 'sanitize-html'
22
import linkifyHtml from 'linkifyjs/html'
33

4+
export const removeHtmlTags = (input) => {
5+
return sanitizeHtml(input, {
6+
allowedTags: [],
7+
allowedAttributes: {},
8+
})
9+
}
10+
411
const standardSanitizeHtmlOptions = {
512
allowedTags: [
613
'img',

backend/src/middleware/languages/languages.js

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,5 @@
11
import LanguageDetect from 'languagedetect'
2-
import sanitizeHtml from 'sanitize-html'
3-
4-
const removeHtmlTags = (input) => {
5-
return sanitizeHtml(input, {
6-
allowedTags: [],
7-
allowedAttributes: {},
8-
})
9-
}
2+
import { removeHtmlTags } from '../helpers/cleanHtml.js'
103

114
const setPostLanguage = (text) => {
125
const lngDetector = new LanguageDetect()

backend/src/middleware/permissionsMiddleware.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,7 @@ export default shield(
114114
reports: isModerator,
115115
statistics: allow,
116116
currentUser: allow,
117+
Group: isAuthenticated,
117118
Post: allow,
118119
profilePagePosts: allow,
119120
Comment: allow,
@@ -140,6 +141,7 @@ export default shield(
140141
Signup: or(publicRegistration, inviteRegistration, isAdmin),
141142
SignupVerification: allow,
142143
UpdateUser: onlyYourself,
144+
CreateGroup: isAuthenticated,
143145
CreatePost: isAuthenticated,
144146
UpdatePost: isAuthor,
145147
DeletePost: isAuthor,

backend/src/middleware/sluggifyMiddleware.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,10 @@ export default {
2626
args.slug = args.slug || (await uniqueSlug(args.name, isUniqueFor(context, 'User')))
2727
return resolve(root, args, context, info)
2828
},
29+
CreateGroup: async (resolve, root, args, context, info) => {
30+
args.slug = args.slug || (await uniqueSlug(args.name, isUniqueFor(context, 'Group')))
31+
return resolve(root, args, context, info)
32+
},
2933
CreatePost: async (resolve, root, args, context, info) => {
3034
args.slug = args.slug || (await uniqueSlug(args.title, isUniqueFor(context, 'Post')))
3135
return resolve(root, args, context, info)

backend/src/middleware/slugify/uniqueSlug.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import slugify from 'slug'
2+
23
export default async function uniqueSlug(string, isUnique) {
34
const slug = slugify(string || 'anonymous', {
45
lower: true,

0 commit comments

Comments
 (0)