Merge branch 'master' of github.com:Human-Connection/Human-Connection into 1724-block-users

This commit is contained in:
mattwr18 2020-01-30 10:47:50 +01:00
commit 60cd593826
105 changed files with 3631 additions and 3081 deletions

View File

@ -18,7 +18,7 @@ before_script:
- docker-compose -f docker-compose.yml -f docker-compose.build-and-test.yml build # just tagging, just be quite fast
- docker-compose -f docker-compose.yml -f docker-compose.build-and-test.yml up -d
- wait-on http://localhost:7474
- docker-compose -f docker-compose.yml -f docker-compose.build-and-test.yml exec neo4j db_setup
- docker-compose -f docker-compose.yml -f docker-compose.build-and-test.yml exec backend yarn run db:migrate init
script:
- export CYPRESS_RETRIES=1

View File

@ -64,7 +64,7 @@ Regular pair programming sessions
* we team up and work on an issue together (often using Visual Studio live sharing sessions)
Open-Source Community Meeting
* every Thursday 13:00
* bi-weekly on Mondays 13:00 (when there is no sprint retrospective)
* the link will be posted in the [discord chat](https://discord.gg/6ub73U3) and on the [Agile Ventures website](https://www.agileventures.org/events?utf8=%E2%9C%93&project_id=220&commit=Filter+by+Project)
* all contributors welcome!
@ -99,3 +99,34 @@ We believe in open source contributions as a learning experience everyone is
We use pair programming sessions as a tool for knowledge sharing. We can learn a lot from each other and only by sharing what we know and overcoming challenges together can we grow as a team and truly own this project collectively.
As a volunteeer you have no commitment except your own self development and your awesomeness by contributing to this free and open-source software project. Cheers to you!
## Open-Source Bounties
There are so many good reasons to contribute to Human Connection
* You learn state-of-the-art technologies
* You build your portfolio
* You contribute to a good cause
Now there is one more good reason: You can receive a small fincancial
compensation for your contribution! :tada:
### How it works
Before you can benefit from the Open-Source bounty program you **must get one
pull request approved and merged for free**. You can choose something really
quick and easy. What's important is starting a working relationship with the
team, learning the workflow, and understanding this contribution guide. You can
filter issues by 'good first issue', to get an idea where to start. Please join
our our [community chat](https://human-connection.org/discord), too.
You can filter Github issues with label [bounty](https://github.com/Human-Connection/Human-Connection/issues?q=is%3Aopen+is%3Aissue+label%3Abounty). These issues should have a second label `€<amount>`
which indicate their respective financial compensation in Euros.
You can bill us after your pull request got approved and merged into `master`.
Payment methods are up to you: Bank transfer or PayPal is fine for us. Just send
us your invoice as .pdf file attached to an E-Mail once you are done.
Our Open-Source bounty program is a work-in-progress. Based on our future
experience we will make changes and improvements. So keep an eye on this
contribution guide.

View File

@ -52,6 +52,10 @@ Check out the [contribution guideline](./CONTRIBUTING.md), too!
[![](https://sourcerer.io/fame/roschaefer/Human-Connection/Human-Connection/images/0)](https://sourcerer.io/fame/roschaefer/Human-Connection/Human-Connection/links/0)[![](https://sourcerer.io/fame/roschaefer/Human-Connection/Human-Connection/images/1)](https://sourcerer.io/fame/roschaefer/Human-Connection/Human-Connection/links/1)[![](https://sourcerer.io/fame/roschaefer/Human-Connection/Human-Connection/images/2)](https://sourcerer.io/fame/roschaefer/Human-Connection/Human-Connection/links/2)[![](https://sourcerer.io/fame/roschaefer/Human-Connection/Human-Connection/images/3)](https://sourcerer.io/fame/roschaefer/Human-Connection/Human-Connection/links/3)[![](https://sourcerer.io/fame/roschaefer/Human-Connection/Human-Connection/images/4)](https://sourcerer.io/fame/roschaefer/Human-Connection/Human-Connection/links/4)[![](https://sourcerer.io/fame/roschaefer/Human-Connection/Human-Connection/images/5)](https://sourcerer.io/fame/roschaefer/Human-Connection/Human-Connection/links/5)[![](https://sourcerer.io/fame/roschaefer/Human-Connection/Human-Connection/images/6)](https://sourcerer.io/fame/roschaefer/Human-Connection/Human-Connection/links/6)[![](https://sourcerer.io/fame/roschaefer/Human-Connection/Human-Connection/images/7)](https://sourcerer.io/fame/roschaefer/Human-Connection/Human-Connection/links/7)
## Open-Source Bounties
You can get a small financial compensation for your contribution :moneybag: See
details in our [Contribution Guidelines](./CONTRIBUTING.md#open-source-bounties).
## Attributions

View File

@ -32,6 +32,7 @@
* [Volume Snapshots](deployment/volumes/volume-snapshots/README.md)
* [Reclaim Policy](deployment/volumes/reclaim-policy/README.md)
* [Velero](deployment/volumes/velero/README.md)
* [Metrics](deployment/monitoring/README.md)
* [Legacy Migration](deployment/legacy-migration/README.md)
* [Feature Specification](cypress/features.md)
* [Code of conduct](CODE_OF_CONDUCT.md)

View File

@ -53,6 +53,27 @@ can issue GraphQL requests or access GraphQL Playground in the browser.
![GraphQL Playground](../.gitbook/assets/graphql-playground.png)
### Database Indices and Constraints
Database indices and constraints need to be created when the database and the
backend is running:
{% tabs %}
{% tab title="Docker" %}
```bash
docker-compose exec backend yarn run db:migrate init
```
{% endtab %}
{% tab title="Without Docker" %}
```bash
# in folder backend/
# make sure your database is running on http://localhost:7474/browser/
yarn run db:migrate init
```
{% endtab %}
{% endtabs %}
#### Seed Database
@ -73,7 +94,7 @@ $ docker-compose exec backend yarn run db:reset
# you could also wipe out your neo4j database and delete all volumes with:
$ docker-compose down -v
# if container is not running, run this command to set up your database indeces and contstraints
$ docker-compose run neo4j db_setup
$ docker-compose run backend yarn run db:migrate init
```
{% endtab %}
@ -90,6 +111,38 @@ $ yarn run db:reset
{% endtab %}
{% endtabs %}
### Data migrations
Although Neo4J is schema-less,you might find yourself in a situation in which
you have to migrate your data e.g. because your data modeling has changed.
{% tabs %}
{% tab title="Docker" %}
Generate a data migration file:
```bash
$ docker-compose exec backend yarn run db:migrate:create your_data_migration
# Edit the file in ./src/db/migrations/
```
To run the migration:
```bash
$ docker-compose exec backend yarn run db:migrate up
```
{% endtab %}
{% tab title="Without Docker" %}
Generate a data migration file:
```bash
$ yarn run db:migrate:create your_data_migration
# Edit the file in ./src/db/migrations/
```
To run the migration:
```bash
$ yarn run db:migrate up
```
{% endtab %}
{% endtabs %}
# Testing
**Beware**: We have no multiple database setup at the moment. We clean the

View File

@ -4,14 +4,19 @@
"description": "GraphQL Backend for Human Connection",
"main": "src/index.js",
"scripts": {
"build": "babel src/ -d dist/ --copy-files",
"__migrate": "migrate --compiler 'js:@babel/register' --migrations-dir ./src/db/migrations",
"prod:migrate": "migrate --migrations-dir ./dist/db/migrations --store ./dist/db/migrate/store.js",
"start": "node dist/",
"build": "babel src/ -d dist/ --copy-files",
"dev": "nodemon --exec babel-node src/ -e js,gql",
"dev:debug": "nodemon --exec babel-node --inspect=0.0.0.0:9229 src/ -e js,gql",
"lint": "eslint src --config .eslintrc.js",
"test": "jest --forceExit --detectOpenHandles --runInBand",
"db:reset": "babel-node src/seed/reset-db.js",
"db:seed": "babel-node src/seed/seed-db.js"
"db:clean": "babel-node src/db/clean.js",
"db:reset": "yarn run db:clean",
"db:seed": "babel-node src/db/seed.js",
"db:migrate": "yarn run __migrate --store ./src/db/migrate/store.js",
"db:migrate:create": "yarn run __migrate --template-file ./src/db/migrate/template.js --date-format 'yyyymmddHHmmss' create"
},
"author": "Human Connection gGmbH",
"license": "MIT",
@ -44,40 +49,41 @@
"bcryptjs": "~2.4.3",
"cheerio": "~1.0.0-rc.3",
"cors": "~2.8.5",
"cross-env": "~6.0.3",
"cross-env": "~7.0.0",
"date-fns": "2.9.0",
"debug": "~4.1.1",
"dotenv": "~8.2.0",
"express": "^4.17.1",
"faker": "Marak/faker.js#master",
"graphql": "^14.5.8",
"graphql": "^14.6.0",
"graphql-custom-directives": "~0.2.14",
"graphql-iso-date": "~3.6.1",
"graphql-middleware": "~4.0.2",
"graphql-middleware-sentry": "^3.2.1",
"graphql-shield": "~7.0.7",
"graphql-shield": "~7.0.9",
"graphql-tag": "~2.10.1",
"helmet": "~3.21.2",
"jsonwebtoken": "~8.5.1",
"linkifyjs": "~2.1.8",
"lodash": "~4.17.14",
"merge-graphql-schemas": "^1.7.6",
"metascraper": "^5.10.5",
"metascraper-audio": "^5.10.5",
"metascraper-author": "^5.10.5",
"metascraper": "^5.10.6",
"metascraper-audio": "^5.10.6",
"metascraper-author": "^5.10.6",
"metascraper-clearbit-logo": "^5.3.0",
"metascraper-date": "^5.10.5",
"metascraper-description": "^5.10.5",
"metascraper-image": "^5.10.5",
"metascraper-lang": "^5.10.5",
"metascraper-date": "^5.10.6",
"metascraper-description": "^5.10.6",
"metascraper-image": "^5.10.6",
"metascraper-lang": "^5.10.6",
"metascraper-lang-detector": "^4.10.2",
"metascraper-logo": "^5.10.5",
"metascraper-publisher": "^5.10.5",
"metascraper-soundcloud": "^5.10.5",
"metascraper-title": "^5.10.5",
"metascraper-url": "^5.10.5",
"metascraper-video": "^5.10.5",
"metascraper-youtube": "^5.10.5",
"metascraper-logo": "^5.10.6",
"metascraper-publisher": "^5.10.6",
"metascraper-soundcloud": "^5.10.6",
"metascraper-title": "^5.10.6",
"metascraper-url": "^5.10.6",
"metascraper-video": "^5.10.6",
"metascraper-youtube": "^5.10.6",
"migrate": "^1.6.2",
"minimatch": "^3.0.4",
"mustache": "^4.0.0",
"neo4j-driver": "^4.0.1",
@ -89,10 +95,10 @@
"npm-run-all": "~4.1.5",
"request": "~2.88.0",
"sanitize-html": "~1.21.1",
"slug": "~2.1.0",
"slug": "~2.1.1",
"trunc-html": "~1.1.2",
"uuid": "~3.4.0",
"validator": "^12.1.0",
"validator": "^12.2.0",
"wait-on": "~4.0.0",
"xregexp": "^4.2.4"
},
@ -102,15 +108,15 @@
"@babel/node": "~7.8.3",
"@babel/plugin-proposal-throw-expressions": "^7.8.3",
"@babel/preset-env": "~7.8.3",
"@babel/register": "~7.8.3",
"@babel/register": "^7.8.3",
"apollo-server-testing": "~2.9.16",
"babel-core": "~7.0.0-0",
"babel-eslint": "~10.0.3",
"babel-jest": "~24.9.0",
"babel-jest": "~25.1.0",
"chai": "~4.2.0",
"cucumber": "~6.0.5",
"eslint": "~6.8.0",
"eslint-config-prettier": "~6.9.0",
"eslint-config-prettier": "~6.10.0",
"eslint-config-standard": "~14.1.0",
"eslint-plugin-import": "~2.20.0",
"eslint-plugin-jest": "~23.6.0",
@ -118,7 +124,7 @@
"eslint-plugin-prettier": "~3.1.2",
"eslint-plugin-promise": "~4.2.1",
"eslint-plugin-standard": "~4.0.1",
"jest": "~24.9.0",
"jest": "~25.1.0",
"nodemon": "~2.0.2",
"prettier": "~1.19.1",
"supertest": "~4.0.2"

View File

@ -1,6 +1,6 @@
import { handler } from './webfinger'
import Factory from '../../seed/factories'
import { getDriver } from '../../bootstrap/neo4j'
import Factory from '../../factories'
import { getDriver } from '../../db/neo4j'
let resource, res, json, status, contentType

View File

@ -1,4 +1,4 @@
import { cleanDatabase } from './factories'
import { cleanDatabase } from '../factories'
if (process.env.NODE_ENV === 'production') {
throw new Error(`You cannot clean the database in production environment!`)

View File

@ -0,0 +1,97 @@
import { getDriver, getNeode } from '../../db/neo4j'
class Store {
async init(next) {
const neode = getNeode()
const { driver } = neode
const session = driver.session()
// eslint-disable-next-line no-console
const writeTxResultPromise = session.writeTransaction(async txc => {
await txc.run('CALL apoc.schema.assert({},{},true)') // drop all indices
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"])',
].map(statement => txc.run(statement)),
)
})
try {
await writeTxResultPromise
await getNeode().schema.install()
// eslint-disable-next-line no-console
console.log('Successfully created database indices and constraints!')
next()
} catch (error) {
console.log(error) // eslint-disable-line no-console
next(error, null)
} finally {
session.close()
driver.close()
}
}
async load(next) {
const driver = getDriver()
const session = driver.session()
const readTxResultPromise = session.readTransaction(async txc => {
const result = await txc.run(
'MATCH (migration:Migration) RETURN migration {.*} ORDER BY migration.timestamp DESC',
)
return result.records.map(r => r.get('migration'))
})
try {
const migrations = await readTxResultPromise
if (migrations.length <= 0) {
// eslint-disable-next-line no-console
console.log(
"No migrations found in database. If it's the first time you run migrations, then this is normal.",
)
return next(null, {})
}
const [{ title: lastRun }] = migrations
next(null, { lastRun, migrations })
} catch (error) {
console.log(error) // eslint-disable-line no-console
next(error)
} finally {
session.close()
}
}
async save(set, next) {
const driver = getDriver()
const session = driver.session()
const { migrations } = set
const writeTxResultPromise = session.writeTransaction(txc => {
return Promise.all(
migrations.map(async migration => {
const { title, description, timestamp } = migration
const properties = { title, description, timestamp }
const migrationResult = await txc.run(
`
MERGE (migration:Migration { title: $properties.title })
ON MATCH SET
migration += $properties
ON CREATE SET
migration += $properties,
migration.migratedAt = toString(datetime())
`,
{ properties },
)
return migrationResult
}),
)
})
try {
await writeTxResultPromise
next()
} catch (error) {
console.log(error) // eslint-disable-line no-console
next(error)
} finally {
session.close()
}
}
}
module.exports = Store

View File

@ -0,0 +1,45 @@
import { getDriver } from '../../db/neo4j'
export const 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(``)
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')
} 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(``)
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')
} finally {
session.close()
}
}

View File

@ -0,0 +1,84 @@
import { throwError, concat } from 'rxjs'
import { flatMap, mergeMap, map, catchError, filter } from 'rxjs/operators'
import { getDriver } from '../neo4j'
import normalizeEmail from '../../schema/resolvers//helpers/normalizeEmail'
export const description = `
This migration merges duplicate :User and :EmailAddress nodes. It became
necessary after we implemented the email normalization but forgot to migrate
the existing data. Some (40) users decided to just register with a new account
but the same email address. On signup our backend would normalize the email,
which is good, but would also keep the existing unnormalized email address.
This led to about 40 duplicate user and email address nodes in our database.
`
export function up(next) {
const driver = getDriver()
const rxSession = driver.rxSession()
rxSession
.beginTransaction()
.pipe(
flatMap(txc =>
concat(
txc
.run('MATCH (email:EmailAddress) RETURN email {.email}')
.records()
.pipe(
map(record => {
const { email } = record.get('email')
const normalizedEmail = normalizeEmail(email)
return { email, normalizedEmail }
}),
filter(({ email, normalizedEmail }) => email !== normalizedEmail),
mergeMap(({ email, normalizedEmail }) => {
return txc
.run(
`
MATCH (oldUser:User)-[:PRIMARY_EMAIL]->(oldEmail:EmailAddress {email: $email}), (oldUser)-[previousRelationship]-(oldEmail)
MATCH (user:User)-[:PRIMARY_EMAIL]->(email:EmailAddress {email: $normalizedEmail})
DELETE previousRelationship
WITH oldUser, oldEmail, user, email
CALL apoc.refactor.mergeNodes([user, oldUser], { properties: 'discard', mergeRels: true }) YIELD node as mergedUser
CALL apoc.refactor.mergeNodes([email, oldEmail], { properties: 'discard', mergeRels: true }) YIELD node as mergedEmail
RETURN user {.*}, email {.*}
`,
{ email, normalizedEmail },
)
.records()
.pipe(
map(r => ({
oldEmail: email,
email: r.get('email'),
user: r.get('user'),
})),
)
}),
),
txc.commit(),
).pipe(catchError(err => txc.rollback().pipe(throwError(err)))),
),
)
.subscribe({
next: ({ user, email, oldUser, oldEmail }) =>
// eslint-disable-next-line no-console
console.log(`
Merged:
=============================
userId: ${user.id}
email: ${oldEmail} => ${email.email}
=============================
`),
complete: () => {
// eslint-disable-next-line no-console
console.log('Merging of duplicate users completed')
next()
},
error: error => {
next(new Error(error), null)
},
})
}
export function down(next) {
next(new Error('Irreversible migration'))
}

View File

@ -0,0 +1,77 @@
import { throwError, concat } from 'rxjs'
import { flatMap, mergeMap, map, catchError } from 'rxjs/operators'
import { getDriver } from '../neo4j'
export const description = `
This migration merges duplicate :Location nodes. It became
necessary after we realized that we had not set up constraints for Location.id in production.
`
export function up(next) {
const driver = getDriver()
const rxSession = driver.rxSession()
rxSession
.beginTransaction()
.pipe(
flatMap(transaction =>
concat(
transaction
.run(
`
MATCH (location:Location)
RETURN location {.id}
`,
)
.records()
.pipe(
map(record => {
const { id: locationId } = record.get('location')
return { locationId }
}),
mergeMap(({ locationId }) => {
return transaction
.run(
`
MATCH(location:Location {id: $locationId}), (location2:Location {id: $locationId})
WHERE location.id = location2.id AND id(location) < id(location2)
CALL apoc.refactor.mergeNodes([location, location2], { properties: 'combine', mergeRels: true }) YIELD node as updatedLocation
RETURN location {.*},updatedLocation {.*}
`,
{ locationId },
)
.records()
.pipe(
map(record => ({
location: record.get('location'),
updatedLocation: record.get('updatedLocation'),
})),
)
}),
),
transaction.commit(),
).pipe(catchError(error => transaction.rollback().pipe(throwError(error)))),
),
)
.subscribe({
next: ({ updatedLocation, location }) =>
// eslint-disable-next-line no-console
console.log(`
Merged:
=============================
locationId: ${location.id}
updatedLocation: ${location.id} => ${updatedLocation.id}
=============================
`),
complete: () => {
// eslint-disable-next-line no-console
console.log('Merging of duplicate locations completed')
next()
},
error: error => {
next(new Error(error), null)
},
})
}
export function down(next) {
next(new Error('Irreversible migration'))
}

View File

@ -0,0 +1,46 @@
import { getDriver } from '../../db/neo4j'
export const description = `
This migration creates a MUTED relationship between two edges(:User) that have a pre-existing BLOCKED relationship.
It also sets the createdAt date for the BLOCKED relationship to the datetime the migration was run. This became
necessary after we redefined what it means to block someone, and what it means to mute them. Muting is about filtering
another user's content, whereas blocking means preventing that user from interacting with you/your contributions.
A blocked user will still be able to see your contributions, but will not be able to interact with them and vice versa.
`
export async function up(next) {
const driver = getDriver()
const session = driver.session()
const transaction = session.beginTransaction()
try {
await transaction.run(
`
MATCH (blocker:User)-[blocked:BLOCKED]->(blockee:User)
MERGE (blocker)-[muted:MUTED]->(blockee)
SET muted.createdAt = toString(datetime()), blocked.createdAt = toString(datetime())
`,
)
await transaction.commit()
} catch (error) {
// eslint-disable-next-line no-console
console.log(error)
await transaction.rollback()
// eslint-disable-next-line no-console
console.log('rolled back')
} finally {
session.close()
}
}
export function down(next) {
const driver = getDriver()
const session = driver.session()
try {
// Rollback your migration here.
next()
} catch (err) {
next(err)
} finally {
session.close()
}
}

View File

@ -2,8 +2,8 @@ import faker from 'faker'
import sample from 'lodash/sample'
import { createTestClient } from 'apollo-server-testing'
import createServer from '../server'
import Factory from './factories'
import { getNeode, getDriver } from '../bootstrap/neo4j'
import Factory from '../factories'
import { getNeode, getDriver } from '../db/neo4j'
import { gql } from '../helpers/jest'
const languages = ['de', 'en', 'es', 'fr', 'it', 'pt', 'pl']

View File

@ -1,4 +1,4 @@
import { getDriver, getNeode } from '../../bootstrap/neo4j'
import { getDriver, getNeode } from '../db/neo4j'
const factories = {
Badge: require('./badges.js').default,

View File

@ -21,11 +21,15 @@ export default function create() {
categoryIds: [],
imageBlurred: false,
imageAspectRatio: 1.333,
pinned: null,
}
args = {
...defaults,
...args,
}
// Convert false to null
args.pinned = args.pinned || null
args.slug = args.slug || slugify(args.title, { lower: true })
args.contentExcerpt = args.contentExcerpt || args.content
@ -50,9 +54,21 @@ export default function create() {
if (author && authorId) throw new Error('You provided both author and authorId')
if (authorId) author = await neodeInstance.find('User', authorId)
author = author || (await factoryInstance.create('User'))
const post = await neodeInstance.create('Post', args)
await post.relateTo(author, 'author')
if (args.pinned) {
args.pinnedAt = args.pinnedAt || new Date().toISOString()
if (!args.pinnedBy) {
const admin = await factoryInstance.create('User', {
role: 'admin',
updatedAt: new Date().toISOString(),
})
await admin.relateTo(post, 'pinned')
args.pinnedBy = admin
}
}
await Promise.all(categories.map(c => c.relateTo(post, 'post')))
await Promise.all(tags.map(t => t.relateTo(post, 'post')))
return post

View File

@ -1,6 +1,6 @@
import faker from 'faker'
import uuid from 'uuid/v4'
import encryptPassword from '../../helpers/encryptPassword'
import encryptPassword from '../helpers/encryptPassword'
import slugify from 'slug'
export default function create() {

View File

@ -1,5 +0,0 @@
//* This is a fake ES2015 template string, just to benefit of syntax
// highlighting of `gql` template strings in certain editors.
export function gql(strings) {
return strings.join('')
}

View File

@ -1,5 +1,5 @@
import Factory from '../seed/factories/index'
import { getDriver, getNeode } from '../bootstrap/neo4j'
import Factory from '../factories/index'
import { getDriver, getNeode } from '../db/neo4j'
import decode from './decode'
const factory = Factory()

View File

@ -1,7 +1,7 @@
import { gql } from '../../helpers/jest'
import Factory from '../../seed/factories'
import Factory from '../../factories'
import { createTestClient } from 'apollo-server-testing'
import { getNeode, getDriver } from '../../bootstrap/neo4j'
import { getNeode, getDriver } from '../../db/neo4j'
import createServer from '../../server'
let server

View File

@ -1,7 +1,7 @@
import { gql } from '../../helpers/jest'
import Factory from '../../seed/factories'
import Factory from '../../factories'
import { createTestClient } from 'apollo-server-testing'
import { getNeode, getDriver } from '../../bootstrap/neo4j'
import { getNeode, getDriver } from '../../db/neo4j'
import createServer from '../../server'
let server, query, mutate, notifiedUser, authenticatedUser

View File

@ -1,6 +1,6 @@
import { gql } from '../helpers/jest'
import Factory from '../seed/factories'
import { getNeode, getDriver } from '../bootstrap/neo4j'
import Factory from '../factories'
import { getNeode, getDriver } from '../db/neo4j'
import { createTestClient } from 'apollo-server-testing'
import createServer from '../server'

View File

@ -1,5 +1,5 @@
import { rule, shield, deny, allow, or } from 'graphql-shield'
import { getNeode } from '../bootstrap/neo4j'
import { getNeode } from '../db/neo4j'
import CONFIG from '../config'
const debug = !!CONFIG.DEBUG

View File

@ -1,8 +1,8 @@
import { createTestClient } from 'apollo-server-testing'
import createServer from '../server'
import Factory from '../seed/factories'
import Factory from '../factories'
import { gql } from '../helpers/jest'
import { getDriver, getNeode } from '../bootstrap/neo4j'
import { getDriver, getNeode } from '../db/neo4j'
const factory = Factory()
const instance = getNeode()

View File

@ -1,6 +1,6 @@
import Factory from '../seed/factories'
import Factory from '../factories'
import { gql } from '../helpers/jest'
import { getNeode, getDriver } from '../bootstrap/neo4j'
import { getNeode, getDriver } from '../db/neo4j'
import createServer from '../server'
import { createTestClient } from 'apollo-server-testing'

View File

@ -1,6 +1,6 @@
import Factory from '../../seed/factories'
import Factory from '../../factories'
import { gql } from '../../helpers/jest'
import { getNeode, getDriver } from '../../bootstrap/neo4j'
import { getNeode, getDriver } from '../../db/neo4j'
import createServer from '../../server'
import { createTestClient } from 'apollo-server-testing'

View File

@ -1,6 +1,6 @@
import { gql } from '../../helpers/jest'
import Factory from '../../seed/factories'
import { getNeode, getDriver } from '../../bootstrap/neo4j'
import Factory from '../../factories'
import { getNeode, getDriver } from '../../db/neo4j'
import { createTestClient } from 'apollo-server-testing'
import createServer from '../../server'

View File

@ -20,6 +20,7 @@ function clean(dirty) {
'hr',
'b',
'i',
'u',
'em',
'strong',
'a',

View File

@ -3,7 +3,7 @@ import uuid from 'uuid/v4'
export default {
id: { type: 'string', primary: true, default: uuid },
name: { type: 'string', required: true, default: false },
slug: { type: 'string' },
slug: { type: 'string', unique: 'true' },
icon: { type: 'string', required: true, default: false },
createdAt: { type: 'string', isoDate: true, default: () => new Date().toISOString() },
updatedAt: {

View File

@ -0,0 +1,5 @@
export default {
title: { type: 'string', primary: true, token: true },
description: { type: 'string' },
migratedAt: { type: 'string', isoDate: true, default: () => new Date().toISOString() },
}

View File

@ -11,7 +11,7 @@ export default {
direction: 'in',
},
title: { type: 'string', disallow: [null], min: 3 },
slug: { type: 'string', allow: [null] },
slug: { type: 'string', allow: [null], unique: 'true' },
content: { type: 'string', disallow: [null], min: 3 },
contentExcerpt: { type: 'string', allow: [null] },
image: { type: 'string', allow: [null] },
@ -41,4 +41,6 @@ export default {
language: { type: 'string', allow: [null] },
imageBlurred: { type: 'boolean', default: false },
imageAspectRatio: { type: 'float', default: 1.0 },
pinned: { type: 'boolean', default: null, valid: [null, true] },
pinnedAt: { type: 'string', isoDate: true },
}

View File

@ -1,5 +1,5 @@
export default {
email: { type: 'string', primary: true, lowercase: true, email: true },
email: { type: 'string', lowercase: true, email: true },
createdAt: { type: 'string', isoDate: true, default: () => new Date().toISOString() },
nonce: { type: 'string', token: true },
belongsTo: {

View File

@ -4,7 +4,7 @@ export default {
id: { type: 'string', primary: true, default: uuid }, // TODO: should be type: 'uuid' but simplified for our tests
actorId: { type: 'string', allow: [null] },
name: { type: 'string', disallow: [null], min: 3 },
slug: { type: 'string', regex: /^[a-z0-9_-]+$/, lowercase: true },
slug: { type: 'string', unique: 'true', regex: /^[a-z0-9_-]+$/, lowercase: true },
encryptedPassword: 'string',
avatar: { type: 'string', allow: [null] },
coverImg: { type: 'string', allow: [null] },
@ -77,12 +77,18 @@ export default {
relationship: 'BLOCKED',
target: 'User',
direction: 'out',
properties: {
createdAt: { type: 'string', isoDate: true, default: () => new Date().toISOString() },
},
},
muted: {
type: 'relationship',
relationship: 'MUTED',
target: 'User',
direction: 'out',
properties: {
createdAt: { type: 'string', isoDate: true, default: () => new Date().toISOString() },
},
},
notifications: {
type: 'relationship',

View File

@ -1,5 +1,5 @@
import Factory from '../seed/factories'
import { getNeode } from '../bootstrap/neo4j'
import Factory from '../factories'
import { getNeode } from '../db/neo4j'
const factory = Factory()
const neode = getNeode()

View File

@ -13,4 +13,5 @@ export default {
Location: require('./Location.js').default,
Donations: require('./Donations.js').default,
Report: require('./Report.js').default,
Migration: require('./Migration.js').default,
}

View File

@ -1,8 +1,8 @@
import Factory from '../../seed/factories'
import Factory from '../../factories'
import { gql } from '../../helpers/jest'
import { createTestClient } from 'apollo-server-testing'
import createServer from '../../server'
import { getNeode, getDriver } from '../../bootstrap/neo4j'
import { getNeode, getDriver } from '../../db/neo4j'
const driver = getDriver()
const neode = getNeode()

View File

@ -1,7 +1,7 @@
import { createTestClient } from 'apollo-server-testing'
import Factory from '../../seed/factories'
import Factory from '../../factories'
import { gql } from '../../helpers/jest'
import { getNeode, getDriver } from '../../bootstrap/neo4j'
import { getNeode, getDriver } from '../../db/neo4j'
import createServer from '../../server'
let mutate, query, authenticatedUser, variables

View File

@ -1,6 +1,6 @@
import Factory from '../../seed/factories'
import Factory from '../../factories'
import { gql } from '../../helpers/jest'
import { getDriver, getNeode } from '../../bootstrap/neo4j'
import { getDriver, getNeode } from '../../db/neo4j'
import createServer from '../../server'
import { createTestClient } from 'apollo-server-testing'

View File

@ -1,4 +1,4 @@
import { getNeode } from '../../bootstrap/neo4j'
import { getNeode } from '../../db/neo4j'
const neode = getNeode()

View File

@ -1,6 +1,6 @@
import { createTestClient } from 'apollo-server-testing'
import Factory from '../../seed/factories'
import { getDriver, getNeode } from '../../bootstrap/neo4j'
import Factory from '../../factories'
import { getDriver, getNeode } from '../../db/neo4j'
import createServer from '../../server'
import { gql } from '../../helpers/jest'

View File

@ -1,6 +1,6 @@
import Factory from '../../seed/factories'
import Factory from '../../factories'
import { gql } from '../../helpers/jest'
import { getNeode, getDriver } from '../../bootstrap/neo4j'
import { getNeode, getDriver } from '../../db/neo4j'
import createServer from '../../server'
import { createTestClient } from 'apollo-server-testing'

View File

@ -1,7 +1,7 @@
import { createTestClient } from 'apollo-server-testing'
import Factory from '../../seed/factories'
import Factory from '../../factories'
import { gql } from '../../helpers/jest'
import { getNeode, getDriver } from '../../bootstrap/neo4j'
import { getNeode, getDriver } from '../../db/neo4j'
import createServer from '../../server'
const factory = Factory()

View File

@ -1,6 +1,6 @@
import Factory from '../../seed/factories'
import Factory from '../../factories'
import { gql } from '../../helpers/jest'
import { getDriver } from '../../bootstrap/neo4j'
import { getDriver } from '../../db/neo4j'
import { createTestClient } from 'apollo-server-testing'
import createServer from '../.././server'

View File

@ -1,6 +1,6 @@
import Factory from '../../seed/factories'
import Factory from '../../factories'
import { gql } from '../../helpers/jest'
import { getNeode, getDriver } from '../../bootstrap/neo4j'
import { getNeode, getDriver } from '../../db/neo4j'
import createPasswordReset from './helpers/createPasswordReset'
import createServer from '../../server'
import { createTestClient } from 'apollo-server-testing'

View File

@ -1,7 +1,7 @@
import { createTestClient } from 'apollo-server-testing'
import Factory from '../../seed/factories'
import Factory from '../../factories'
import { gql } from '../../helpers/jest'
import { getNeode, getDriver } from '../../bootstrap/neo4j'
import { getNeode, getDriver } from '../../db/neo4j'
import createServer from '../../server'
const driver = getDriver()
@ -682,58 +682,62 @@ describe('UpdatePost', () => {
})
describe('PostOrdering', () => {
let pinnedPost, admin
beforeEach(async () => {
;[pinnedPost] = await Promise.all([
neode.create('Post', {
id: 'im-a-pinned-post',
pinned: true,
}),
neode.create('Post', {
id: 'i-was-created-after-pinned-post',
createdAt: '2019-10-22T17:26:29.070Z', // this should always be 3rd
}),
])
admin = await user.update({
role: 'admin',
name: 'Admin',
updatedAt: new Date().toISOString(),
await factory.create('Post', {
id: 'im-a-pinned-post',
createdAt: '2019-11-22T17:26:29.070Z',
pinned: true,
})
await factory.create('Post', {
id: 'i-was-created-before-pinned-post',
// fairly old, so this should be 3rd
createdAt: '2019-10-22T17:26:29.070Z',
})
await admin.relateTo(pinnedPost, 'pinned')
})
it('pinned post appear first even when created before other posts', async () => {
const postOrderingQuery = gql`
query($orderBy: [_PostOrdering]) {
Post(orderBy: $orderBy) {
id
pinnedAt
describe('order by `pinned_asc` and `createdAt_desc`', () => {
beforeEach(() => {
// this is the ordering in the frontend
variables = { orderBy: ['pinned_asc', 'createdAt_desc'] }
})
it('pinned post appear first even when created before other posts', async () => {
const postOrderingQuery = gql`
query($orderBy: [_PostOrdering]) {
Post(orderBy: $orderBy) {
id
pinned
createdAt
pinnedAt
}
}
}
`
const expected = {
data: {
Post: [
{
id: 'im-a-pinned-post',
pinnedAt: expect.any(String),
},
{
id: 'p9876',
pinnedAt: null,
},
{
id: 'i-was-created-after-pinned-post',
pinnedAt: null,
},
],
},
errors: undefined,
}
variables = { orderBy: ['pinned_desc', 'createdAt_desc'] }
await expect(query({ query: postOrderingQuery, variables })).resolves.toMatchObject(
expected,
)
`
await expect(query({ query: postOrderingQuery, variables })).resolves.toMatchObject({
data: {
Post: [
{
id: 'im-a-pinned-post',
pinned: true,
createdAt: '2019-11-22T17:26:29.070Z',
pinnedAt: expect.any(String),
},
{
id: 'p9876',
pinned: null,
createdAt: expect.any(String),
pinnedAt: null,
},
{
id: 'i-was-created-before-pinned-post',
pinned: null,
createdAt: '2019-10-22T17:26:29.070Z',
pinnedAt: null,
},
],
},
errors: undefined,
})
})
})
})
})

View File

@ -1,5 +1,5 @@
import { UserInputError } from 'apollo-server'
import { getNeode } from '../../bootstrap/neo4j'
import { getNeode } from '../../db/neo4j'
import fileUpload from './fileUpload'
import encryptPassword from '../../helpers/encryptPassword'
import generateNonce from './helpers/generateNonce'

View File

@ -1,6 +1,6 @@
import Factory from '../../seed/factories'
import Factory from '../../factories'
import { gql } from '../../helpers/jest'
import { getDriver, getNeode } from '../../bootstrap/neo4j'
import { getDriver, getNeode } from '../../db/neo4j'
import createServer from '../../server'
import { createTestClient } from 'apollo-server-testing'

View File

@ -1,8 +1,8 @@
import { createTestClient } from 'apollo-server-testing'
import createServer from '../.././server'
import Factory from '../../seed/factories'
import Factory from '../../factories'
import { gql } from '../../helpers/jest'
import { getDriver, getNeode } from '../../bootstrap/neo4j'
import { getDriver, getNeode } from '../../db/neo4j'
const factory = Factory()
const instance = getNeode()

View File

@ -1,4 +1,4 @@
import { getNeode } from '../../bootstrap/neo4j'
import { getNeode } from '../../db/neo4j'
import { UserInputError } from 'apollo-server'
const neode = getNeode()

View File

@ -1,7 +1,7 @@
import { createTestClient } from 'apollo-server-testing'
import Factory from '../../seed/factories'
import Factory from '../../factories'
import { gql } from '../../helpers/jest'
import { getNeode, getDriver } from '../../bootstrap/neo4j'
import { getNeode, getDriver } from '../../db/neo4j'
import createServer from '../../server'
const factory = Factory()

View File

@ -1,7 +1,7 @@
import { createTestClient } from 'apollo-server-testing'
import Factory from '../../seed/factories'
import Factory from '../../factories'
import { gql } from '../../helpers/jest'
import { getNeode, getDriver } from '../../bootstrap/neo4j'
import { getNeode, getDriver } from '../../db/neo4j'
import createServer from '../../server'
let mutate, query, authenticatedUser, variables

View File

@ -1,4 +1,4 @@
import { getNeode } from '../../bootstrap/neo4j'
import { getNeode } from '../../db/neo4j'
import Resolver from './helpers/Resolver'
const neode = getNeode()

View File

@ -1,8 +1,8 @@
import { createTestClient } from 'apollo-server-testing'
import createServer from '../../server'
import Factory from '../../seed/factories'
import Factory from '../../factories'
import { gql } from '../../helpers/jest'
import { getNeode, getDriver } from '../../bootstrap/neo4j'
import { getNeode, getDriver } from '../../db/neo4j'
const driver = getDriver()
const factory = Factory()

View File

@ -1,7 +1,7 @@
import { createTestClient } from 'apollo-server-testing'
import Factory from '../../seed/factories'
import Factory from '../../factories'
import { gql } from '../../helpers/jest'
import { getNeode, getDriver } from '../../bootstrap/neo4j'
import { getNeode, getDriver } from '../../db/neo4j'
import createServer from '../../server'
let query, authenticatedUser

View File

@ -1,7 +1,7 @@
import encode from '../../jwt/encode'
import bcrypt from 'bcryptjs'
import { AuthenticationError } from 'apollo-server'
import { getNeode } from '../../bootstrap/neo4j'
import { getNeode } from '../../db/neo4j'
import normalizeEmail from './helpers/normalizeEmail'
import log from './helpers/databaseLogger'

View File

@ -1,11 +1,11 @@
import jwt from 'jsonwebtoken'
import CONFIG from './../../config'
import Factory from '../../seed/factories'
import Factory from '../../factories'
import { gql } from '../../helpers/jest'
import { createTestClient } from 'apollo-server-testing'
import createServer, { context } from '../../server'
import encode from '../../jwt/encode'
import { getNeode } from '../../bootstrap/neo4j'
import { getNeode } from '../../db/neo4j'
const factory = Factory()
const neode = getNeode()

View File

@ -1,6 +1,6 @@
import { neo4jgraphql } from 'neo4j-graphql-js'
import fileUpload from './fileUpload'
import { getNeode } from '../../bootstrap/neo4j'
import { getNeode } from '../../db/neo4j'
import { UserInputError, ForbiddenError } from 'apollo-server'
import Resolver from './helpers/Resolver'
import log from './helpers/databaseLogger'

View File

@ -1,6 +1,6 @@
import Factory from '../../seed/factories'
import Factory from '../../factories'
import { gql } from '../../helpers/jest'
import { getNeode, getDriver } from '../../bootstrap/neo4j'
import { getNeode, getDriver } from '../../db/neo4j'
import createServer from '../../server'
import { createTestClient } from 'apollo-server-testing'

View File

@ -1,6 +1,6 @@
import { gql } from '../../../helpers/jest'
import Factory from '../../../seed/factories'
import { getNeode, getDriver } from '../../../bootstrap/neo4j'
import Factory from '../../../factories'
import { getNeode, getDriver } from '../../../db/neo4j'
import { createTestClient } from 'apollo-server-testing'
import createServer from '../../../server'

View File

@ -1,8 +1,8 @@
import { createTestClient } from 'apollo-server-testing'
import createServer from '../../../server'
import Factory from '../../../seed/factories'
import Factory from '../../../factories'
import { gql } from '../../../helpers/jest'
import { getNeode, getDriver } from '../../../bootstrap/neo4j'
import { getNeode, getDriver } from '../../../db/neo4j'
const driver = getDriver()
const factory = Factory()

View File

@ -1,134 +0,0 @@
const _ = require('lodash')
const faker = require('faker')
const unsplashTopics = [
'love',
'family',
'spring',
'business',
'nature',
'travel',
'happy',
'landscape',
'health',
'friends',
'computer',
'autumn',
'space',
'animal',
'smile',
'face',
'people',
'portrait',
'amazing',
]
let unsplashTopicsTmp = []
const ngoLogos = [
'http://www.fetchlogos.com/wp-content/uploads/2015/11/Girl-Scouts-Of-The-Usa-Logo.jpg',
'http://logos.textgiraffe.com/logos/logo-name/Ngo-designstyle-friday-m.png',
'http://seeklogo.com/images/N/ngo-logo-BD53A3E024-seeklogo.com.png',
'https://dcassetcdn.com/design_img/10133/25833/25833_303600_10133_image.jpg',
'https://cdn.tutsplus.com/vector/uploads/legacy/articles/08bad_ngologos/20.jpg',
'https://cdn.tutsplus.com/vector/uploads/legacy/articles/08bad_ngologos/33.jpg',
null,
]
const difficulties = ['easy', 'medium', 'hard']
export default {
randomItem: (items, filter) => {
const ids = filter
? Object.keys(items).filter(id => {
return filter(items[id])
})
: _.keys(items)
const randomIds = _.shuffle(ids)
return items[randomIds.pop()]
},
randomItems: (items, key = 'id', min = 1, max = 1) => {
const randomIds = _.shuffle(_.keys(items))
const res = []
const count = _.random(min, max)
for (let i = 0; i < count; i++) {
let r = items[randomIds.pop()][key]
if (key === 'id') {
r = r.toString()
}
res.push(r)
}
return res
},
random: items => {
return _.shuffle(items).pop()
},
randomDifficulty: () => {
return _.shuffle(difficulties).pop()
},
randomLogo: () => {
return _.shuffle(ngoLogos).pop()
},
randomUnsplashUrl: () => {
if (Math.random() < 0.6) {
// do not attach images in 60 percent of the cases (faster seeding)
return
}
if (unsplashTopicsTmp.length < 2) {
unsplashTopicsTmp = _.shuffle(unsplashTopics)
}
return (
'https://source.unsplash.com/daily?' + unsplashTopicsTmp.pop() + ',' + unsplashTopicsTmp.pop()
)
},
randomCategories: (seederstore, allowEmpty = false) => {
let count = Math.round(Math.random() * 3)
if (allowEmpty === false && count === 0) {
count = 1
}
const categorieIds = _.shuffle(_.keys(seederstore.categories))
const ids = []
for (let i = 0; i < count; i++) {
ids.push(categorieIds.pop())
}
return ids
},
randomAddresses: () => {
const count = Math.round(Math.random() * 3)
const addresses = []
for (let i = 0; i < count; i++) {
addresses.push({
city: faker.address.city(),
zipCode: faker.address.zipCode(),
street: faker.address.streetAddress(),
country: faker.address.countryCode(),
lat: 54.032726 - Math.random() * 10,
lng: 6.558838 + Math.random() * 10,
})
}
return addresses
},
/**
* Get array of ids from the given seederstore items after mapping them by the key in the values
*
* @param items items from the seederstore
* @param values values for which you need the ids
* @param key the field key that is represented in the values (slug, name, etc.)
*/
mapIdsByKey: (items, values, key) => {
const res = []
values.forEach(value => {
res.push(_.find(items, [key, value]).id.toString())
})
return res
},
genInviteCode: () => {
const chars = '23456789abcdefghkmnpqrstuvwxyzABCDEFGHJKLMNPRSTUVWXYZ'
let code = ''
for (let i = 0; i < 8; i++) {
const n = _.random(0, chars.length - 1)
code += chars.substr(n, 1)
}
return code
},
}

View File

@ -3,7 +3,7 @@ import helmet from 'helmet'
import { ApolloServer } from 'apollo-server-express'
import CONFIG from './config'
import middleware from './middleware'
import { getNeode, getDriver } from './bootstrap/neo4j'
import { getNeode, getDriver } from './db/neo4j'
import decode from './jwt/decode'
import schema from './schema'
import webfinger from './activitypub/routes/webfinger'

View File

@ -3,7 +3,7 @@ import { Given, When, Then, AfterAll } from 'cucumber'
import { expect } from 'chai'
// import { client } from '../../../src/activitypub/apollo-client'
import { GraphQLClient } from 'graphql-request'
import Factory from '../../../src/seed/factories'
import Factory from '../../../src/factories'
const debug = require('debug')('ea:test:steps')
const factory = Factory()

File diff suppressed because it is too large Load Diff

View File

@ -249,10 +249,12 @@ Shows automatically related actions for existing post.
### Administration
[Cucumber Features](https://github.com/Human-Connection/Human-Connection/tree/master/cypress/integration/administration)
* Provide Admin-Interface to send Users Invite Code
* Static Pages for Data Privacy Statement ...
* Create, edit and delete Announcements
* Show Announcements on top of User Interface
* Pin a post to inform users
### Invitation

View File

@ -0,0 +1,36 @@
Feature: Pin a post
As an admin
I want to pin a post so that it always appears at the top
In order to make sure all network users read it - e.g. notify people about security incidents, maintenance downtimes
Background:
Given we have the following posts in our database:
| id | title | pinned | createdAt |
| p1 | Some other post | | 2020-01-21 |
| p2 | Houston we have a problem | x | 2020-01-20 |
| p3 | Yet another post | | 2020-01-19 |
Scenario: Pinned post always appears on the top of the newsfeed
Given I am logged in with a "user" role
Then the first post on the landing page has the title:
"""
Houston we have a problem
"""
And the post with title "Houston we have a problem" has a ribbon for pinned posts
Scenario: Ordinary users cannot pin a post
Given I am logged in with a "user" role
When I open the content menu of post "Yet another post"
Then there is no button to pin a post
Scenario: Admins are allowed to pin a post
Given I am logged in with a "admin" role
And I open the content menu of post "Yet another post"
When I click on 'Pin post'
Then I see a toaster with "Post pinned successfully"
And the first post on the landing page has the title:
"""
Yet another post
"""
And the post with title "Yet another post" has a ribbon for pinned posts

View File

@ -44,3 +44,31 @@ Then("I should see an abreviated version of my comment", () => {
Then("the editor should be cleared", () => {
cy.get(".ProseMirror p").should("have.class", "is-empty");
});
When("I open the content menu of post {string}", (title)=> {
cy.contains('.post-card', title)
.find('.content-menu .base-button')
.click()
})
When("I click on 'Pin post'", (string)=> {
cy.get("a.ds-menu-item-link").contains("Pin post")
.click()
})
Then("there is no button to pin a post", () => {
cy.get("a.ds-menu-item-link")
.should('contain', "Report Post") // sanity check
.should('not.contain', "Pin post")
})
And("the post with title {string} has a ribbon for pinned posts", (title) => {
cy.get("article.post-card").contains(title)
.parent()
.find("div.ribbon.ribbon--pinned")
.should("contain", "Announcement")
})
Then("I see a toaster with {string}", (title) => {
cy.get(".iziToast-message").should("contain", title);
})

View File

@ -170,5 +170,4 @@ When("they have a post someone has reported", () => {
authorId: 'annnoying-user',
title,
});
})

View File

@ -223,6 +223,7 @@ Given("we have the following posts in our database:", table => {
...postAttributes,
deleted: Boolean(postAttributes.deleted),
disabled: Boolean(postAttributes.disabled),
pinned: Boolean(postAttributes.pinned),
categoryIds: ['cat-456']
}
cy.factory().create("Post", postAttributes);

View File

@ -9,10 +9,10 @@ Feature: Report and Moderate
Background:
Given we have the following user accounts:
| id | name |
| u67 | David Irving |
| id | name |
| u67 | David Irving |
| annoying-user | I'm gonna mute Moderators and Admins HA HA HA |
Given we have the following posts in our database:
| authorId | id | title | content |
| u67 | p1 | The Truth about the Holocaust | It never existed! |

View File

@ -1,5 +1,5 @@
import Factory from '../../backend/src/seed/factories'
import { getDriver, getNeode } from '../../backend/src/bootstrap/neo4j'
import Factory from '../../backend/src/factories'
import { getDriver, getNeode } from '../../backend/src/db/neo4j'
const neo4jConfigs = {
uri: Cypress.env('NEO4J_URI'),

View File

@ -0,0 +1,43 @@
# Metrics
You can optionally setup [prometheus](https://prometheus.io/) and
[grafana](https://grafana.com/) for metrics.
We follow this tutorial [here](https://medium.com/@chris_linguine/how-to-monitor-your-kubernetes-cluster-with-prometheus-and-grafana-2d5704187fc8):
```bash
kubectl proxy # proxy to your kubernetes dashboard
helm repo list
# If using helm v3, the stable repository is not set, so you need to manually add it.
helm repo add stable https://kubernetes-charts.storage.googleapis.com
# Create a monitoring namespace for your cluster
kubectl create namespace monitoring
helm --namespace monitoring install prometheus stable/prometheus
kubectl -n monitoring get pods # look for 'server'
kubectl port-forward -n monitoring <PROMETHEUS_SERVER_ID> 9090
# You can now see your prometheus server on: http://localhost:9090
# Make sure you are in folder `deployment/`
kubectl apply -f monitoring/grafana/config.yml
helm --namespace monitoring install grafana stable/grafana -f monitoring/grafana/values.yml
# Get the admin password for grafana from your kubernetes dashboard.
kubectl --namespace monitoring port-forward <POD_NAME> 3000
# You can now see your grafana dashboard on: http://localhost:3000
# Login with user 'admin' and the password you just looked up.
# In your dashboard import this dashboard:
# https://grafana.com/grafana/dashboards/1860
# Enter ID 180 and choose "Prometheus" as datasource.
# You got metrics!
```
Now you should see something like this:
![Grafana dashboard](./grafana/metrics.png)
You can set up a grafana dashboard, by visiting https://grafana.com/dashboards, finding one that is suitable and copying it's id.
You then go to the left hand menu in localhost, choose `Dashboard` > `Manage` > `Import`
Paste in the id, click `Load`, select `Prometheus` for the data source, and click `Import`
When you just installed prometheus and grafana, the data will not be available
immediately, so wait for a couple of minutes and reload.

View File

@ -0,0 +1,16 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: prometheus-grafana-datasource
namespace: monitoring
labels:
grafana_datasource: '1'
data:
datasource.yaml: |-
apiVersion: 1
datasources:
- name: Prometheus
type: prometheus
access: proxy
orgId: 1
url: http://prometheus-server.monitoring.svc.cluster.local

Binary file not shown.

After

Width:  |  Height:  |  Size: 206 KiB

View File

@ -0,0 +1,4 @@
sidecar:
datasources:
enabled: true
label: grafana_datasource

View File

@ -1,6 +1,6 @@
// features/support/steps.js
import { Given, When, Then, After, AfterAll } from 'cucumber'
import Factory from '../../backend/src/seed/factories'
import Factory from '../../backend/src/factories'
import dotenv from 'dotenv'
import expect from 'expect'

View File

@ -23,7 +23,6 @@ DELETE disabled
CREATE (moderator)-[review:REVIEWED]->(report:Report)-[:BELONGS_TO]->(disabledResource)
SET review.createdAt = toString(datetime()), review.updatedAt = review.createdAt, review.disable = true
SET report.id = randomUUID(), report.createdAt = toString(datetime()), report.updatedAt = report.createdAt, report.rule = 'latestReviewUpdatedAtRules', report.closed = false
// if disabledResource has no filed report, then create a moderators default filed report
WITH moderator, disabledResource, report
OPTIONAL MATCH (disabledResourceReporter:User)-[existingFiledReport:FILED]->(disabledResource)
@ -36,7 +35,6 @@ FOREACH(disabledResource IN CASE WHEN existingFiledReport IS NOT NULL THEN [1] E
SET moveModeratorReport = existingFiledReport
DELETE existingFiledReport
)
RETURN disabledResource {.id};
" | cypher-shell
@ -49,7 +47,5 @@ ON CREATE SET report.id = randomUUID(), report.createdAt = toString(datetime()),
CREATE (reporter)-[filed:FILED]->(report)
SET report = oldReport
DELETE oldReport
RETURN notDisabledResource {.id};
" | cypher-shell

View File

@ -4,7 +4,5 @@ LABEL Description="Neo4J database of the Social Network Human-Connection.org wit
ARG BUILD_COMMIT
ENV BUILD_COMMIT=$BUILD_COMMIT
COPY db_setup.sh /usr/local/bin/db_setup
RUN apt-get update && apt-get -y install wget htop
RUN wget https://github.com/neo4j-contrib/neo4j-apoc-procedures/releases/download/3.5.0.4/apoc-3.5.0.4-all.jar -P plugins/

View File

@ -18,15 +18,6 @@ docker-compose up
You can access Neo4J through [http://localhost:7474/](http://localhost:7474/)
for an interactive cypher shell and a visualization of the graph.
### Database Indices and Constraints
Database indices and constraints need to be created when the database is
running. So start the container with the command above and run:
```bash
docker-compose exec neo4j db_setup
```
## Installation without Docker
@ -45,20 +36,6 @@ Then make sure to allow Apoc procedures by adding the following line to your Neo
```
dbms.security.procedures.unrestricted=apoc.*
```
### Database Indices and Constraints
If you have `cypher-shell` available with your local installation of neo4j you
can run:
```bash
# in folder neo4j/
$ cp .env.template .env
$ ./db_setup.sh
```
Otherwise, if you don't have `cypher-shell` available, copy the cypher
statements [from the `db_setup.sh` script](https://github.com/Human-Connection/Human-Connection/blob/master/neo4j/db_setup.sh) and paste the scripts into your
[database browser frontend](http://localhost:7474).
### Alternatives

View File

@ -1,51 +0,0 @@
#!/usr/bin/env bash
ENV_FILE=$(dirname "$0")/.env
[[ -f "$ENV_FILE" ]] && source "$ENV_FILE"
if [ -z "$NEO4J_USERNAME" ] || [ -z "$NEO4J_PASSWORD" ]; then
echo "Please set NEO4J_USERNAME and NEO4J_PASSWORD environment variables."
echo "Database manipulation is not possible without connecting to the database."
echo "E.g. you could \`cp .env.template .env\` unless you run the script in a docker container"
fi
until echo 'RETURN "Connection successful" as info;' | cypher-shell
do
echo "Connecting to neo4j failed, trying again..."
sleep 1
done
echo "
:begin
MATCH(user)-[reported:REPORTED]->(resource)
WITH reported, resource, COLLECT(user) as users
MERGE(report:Report)-[:BELONGS_TO]->(resource)
SET report.id = randomUUID(), report.createdAt = toString(datetime()), report.updatedAt = report.createdAt, report.rule = 'latestReviewUpdatedAtRules', report.closed = false
WITH report, users, reported
UNWIND users as user
MERGE (user)-[filed:FILED]->(report)
SET filed = reported
DELETE reported;
MATCH(moderator)-[disabled:DISABLED]->(resource)
MATCH(report:Report)-[:BELONGS_TO]->(resource)
WITH disabled, resource, COLLECT(moderator) as moderators, report
DELETE disabled
WITH report, moderators, disabled
UNWIND moderators as moderator
MERGE (moderator)-[review:REVIEWED {disable: true}]->(report)
SET review.createdAt = toString(datetime()), review.updatedAt = review.createdAt, review.disable = true;
MATCH(moderator)-[disabled:DISABLED]->(resource)
WITH disabled, resource, COLLECT(moderator) as moderators
MERGE(report:Report)-[:BELONGS_TO]->(resource)
SET report.id = randomUUID(), report.createdAt = toString(datetime()), report.updatedAt = report.createdAt, report.rule = 'latestReviewUpdatedAtRules', report.closed = false
DELETE disabled
WITH report, moderators, disabled
UNWIND moderators as moderator
MERGE(moderator)-[filed:FILED]->(report)
SET filed.createdAt = toString(datetime()), filed.reasonCategory = 'other', filed.reasonDescription = 'Old DISABLED relations didn\'t enforce mandatory reporting !!! Created automatically to ensure database consistency! Creation date is when the database manipulation happened.'
MERGE (moderator)-[review:REVIEWED {disable: true}]->(report)
SET review.createdAt = toString(datetime()), review.updatedAt = review.createdAt, review.disable = true;
:commit
" | cypher-shell

View File

@ -1,41 +0,0 @@
#!/usr/bin/env bash
ENV_FILE=$(dirname "$0")/.env
[[ -f "$ENV_FILE" ]] && source "$ENV_FILE"
if [ -z "$NEO4J_USERNAME" ] || [ -z "$NEO4J_PASSWORD" ]; then
echo "Please set NEO4J_USERNAME and NEO4J_PASSWORD environment variables."
echo "Setting up database constraints and indexes will probably fail because of authentication errors."
echo "E.g. you could \`cp .env.template .env\` unless you run the script in a docker container"
fi
until echo 'RETURN "Connection successful" as info;' | cypher-shell
do
echo "Connecting to neo4j failed, trying again..."
sleep 1
done
echo '
RETURN "Here is a list of indexes and constraints BEFORE THE SETUP:" as info;
CALL db.indexes();
' | cypher-shell
echo '
CALL db.index.fulltext.createNodeIndex("post_fulltext_search",["Post"],["title", "content"]);
CALL db.index.fulltext.createNodeIndex("user_fulltext_search",["User"],["name", "slug"]);
CREATE CONSTRAINT ON (p:Post) ASSERT p.id IS UNIQUE;
CREATE CONSTRAINT ON (c:Comment) ASSERT c.id IS UNIQUE;
CREATE CONSTRAINT ON (c:Category) ASSERT c.id IS UNIQUE;
CREATE CONSTRAINT ON (u:User) ASSERT u.id IS UNIQUE;
CREATE CONSTRAINT ON (t:Tag) ASSERT t.id IS UNIQUE;
CREATE CONSTRAINT ON (p:Post) ASSERT p.slug IS UNIQUE;
CREATE CONSTRAINT ON (c:Category) ASSERT c.slug IS UNIQUE;
CREATE CONSTRAINT ON (u:User) ASSERT u.slug IS UNIQUE;
CREATE CONSTRAINT ON (e:EmailAddress) ASSERT e.email IS UNIQUE;
' | cypher-shell
echo '
RETURN "Setting up all the indexes and constraints seems to have been successful. Here is a list AFTER THE SETUP:" as info;
CALL db.indexes();
' | cypher-shell

View File

@ -30,22 +30,22 @@
"@babel/register": "^7.8.3",
"auto-changelog": "^1.16.2",
"bcryptjs": "^2.4.3",
"codecov": "^3.6.1",
"codecov": "^3.6.2",
"cross-env": "^6.0.3",
"cucumber": "^6.0.5",
"cypress": "^3.8.2",
"cypress": "^3.8.3",
"cypress-cucumber-preprocessor": "^2.0.1",
"cypress-file-upload": "^3.5.3",
"cypress-plugin-retries": "^1.5.2",
"date-fns": "^2.9.0",
"dotenv": "^8.2.0",
"expect": "^24.9.0",
"expect": "^25.1.0",
"faker": "Marak/faker.js#master",
"graphql-request": "^1.8.2",
"neo4j-driver": "^4.0.1",
"neode": "^0.3.7",
"npm-run-all": "^4.1.5",
"slug": "^2.1.0",
"slug": "^2.1.1",
"standard-version": "^7.1.0"
},
"resolutions": {

View File

@ -0,0 +1,5 @@
<!-- Generated by IcoMoon.io -->
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">
<title>underline</title>
<path d="M7 4h2v12c0 3.37 2.63 6 6 6s6-2.63 6-6v-12h2v12c0 4.43-3.57 8-8 8s-8-3.57-8-8v-12zM5 26h20v2h-20v-2z"></path>
</svg>

After

Width:  |  Height:  |  Size: 282 B

View File

@ -11,7 +11,7 @@
"
@click.prevent="toggleMenu"
>
<user-avatar :user="user" />
<user-avatar :user="user" size="small" />
<base-icon class="dropdown-arrow" name="angle-down" />
</a>
</template>
@ -127,6 +127,10 @@ export default {
display: flex;
align-items: center;
padding-left: $space-xx-small;
> .user-avatar {
margin-right: $space-xx-small;
}
}
.avatar-menu-popover {
padding-top: $space-x-small;

View File

@ -5,6 +5,12 @@
<menu-bar-button :isActive="isActive.italic()" :onClick="commands.italic" icon="italic" />
<menu-bar-button
:isActive="isActive.underline()"
:onClick="commands.underline"
icon="underline"
/>
<menu-bar-button
ref="linkButton"
:isActive="isActive.link()"

View File

@ -132,7 +132,7 @@ export default {
)
},
isPinned() {
return this.post && this.post.pinnedBy
return this.post && this.post.pinned
},
},
methods: {
@ -193,6 +193,8 @@ export default {
/* workaround to avoid jumping layout when user-teaser is rendered */
.user-wrapper {
height: 36px;
position: relative;
z-index: $z-index-post-card-link;
}
.content-menu {

View File

@ -1,6 +1,6 @@
<template>
<div class="user-teaser" v-if="displayAnonymous">
<user-avatar v-if="showAvatar" />
<user-avatar v-if="showAvatar" size="small" />
<span class="info anonymous">{{ $t('profile.userAnonym') }}</span>
</div>
<dropdown
@ -158,8 +158,6 @@ export default {
.user-teaser {
display: flex;
flex-wrap: nowrap;
z-index: $z-index-post-card-link;
position: relative;
> .user-avatar {
flex-shrink: 0;

View File

@ -6,7 +6,7 @@
v-else
:src="user.avatar | proxyApiUrl"
class="image"
@error="event.target.style.display = 'none'"
@error="$event.target.style.display = 'none'"
/>
</div>
</template>
@ -75,7 +75,6 @@ export default {
> .image {
position: relative;
z-index: 5;
width: 100%;
object-fit: cover;
object-position: center;

Some files were not shown because too many files have changed in this diff Show More