Merge pull request #1016 from Human-Connection/277_reward_badges

Refactor reward/unreward Badges in backend
This commit is contained in:
mattwr18 2019-07-13 10:08:52 -03:00 committed by GitHub
commit 4c91d8fbc5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
30 changed files with 266 additions and 752 deletions

View File

@ -146,6 +146,7 @@ const permissions = shield(
Comment: allow, Comment: allow,
User: or(noEmailFilter, isAdmin), User: or(noEmailFilter, isAdmin),
isLoggedIn: allow, isLoggedIn: allow,
Badge: allow,
}, },
Mutation: { Mutation: {
'*': deny, '*': deny,
@ -160,9 +161,6 @@ const permissions = shield(
UpdatePost: isAuthor, UpdatePost: isAuthor,
DeletePost: isAuthor, DeletePost: isAuthor,
report: isAuthenticated, report: isAuthenticated,
CreateBadge: isAdmin,
UpdateBadge: isAdmin,
DeleteBadge: isAdmin,
CreateSocialMedia: isAuthenticated, CreateSocialMedia: isAuthenticated,
DeleteSocialMedia: isAuthenticated, DeleteSocialMedia: isAuthenticated,
// AddBadgeRewarded: isAdmin, // AddBadgeRewarded: isAdmin,

View File

@ -0,0 +1,7 @@
module.exports = {
id: { type: 'string', primary: true, lowercase: true },
status: { type: 'string', valid: ['permanent', 'temporary'] },
type: { type: 'string', valid: ['role', 'crowdfunding'] },
icon: { type: 'string', required: true },
createdAt: { type: 'string', isoDate: true, default: () => new Date().toISOString() },
}

View File

@ -43,6 +43,12 @@ module.exports = {
target: 'User', target: 'User',
direction: 'in', direction: 'in',
}, },
rewarded: {
type: 'relationship',
relationship: 'REWARDED',
target: 'Badge',
direction: 'in',
},
invitedBy: { type: 'relationship', relationship: 'INVITED', target: 'User', direction: 'in' }, invitedBy: { type: 'relationship', relationship: 'INVITED', target: 'User', direction: 'in' },
createdAt: { type: 'string', isoDate: true, default: () => new Date().toISOString() }, createdAt: { type: 'string', isoDate: true, default: () => new Date().toISOString() },
updatedAt: { updatedAt: {

View File

@ -1,6 +1,7 @@
// NOTE: We cannot use `fs` here to clean up the code. Cypress breaks on any npm // NOTE: We cannot use `fs` here to clean up the code. Cypress breaks on any npm
// module that is not browser-compatible. Node's `fs` module is server-side only // module that is not browser-compatible. Node's `fs` module is server-side only
export default { export default {
Badge: require('./Badge.js'),
User: require('./User.js'), User: require('./User.js'),
InvitationCode: require('./InvitationCode.js'), InvitationCode: require('./InvitationCode.js'),
EmailAddress: require('./EmailAddress.js'), EmailAddress: require('./EmailAddress.js'),

View File

@ -12,11 +12,25 @@ export default applyScalars(
resolvers, resolvers,
config: { config: {
query: { query: {
exclude: ['InvitationCode', 'EmailAddress', 'Notfication', 'Statistics', 'LoggedInUser'], exclude: [
'Badge',
'InvitationCode',
'EmailAddress',
'Notfication',
'Statistics',
'LoggedInUser',
],
// add 'User' here as soon as possible // add 'User' here as soon as possible
}, },
mutation: { mutation: {
exclude: ['InvitationCode', 'EmailAddress', 'Notfication', 'Statistics', 'LoggedInUser'], exclude: [
'Badge',
'InvitationCode',
'EmailAddress',
'Notfication',
'Statistics',
'LoggedInUser',
],
// add 'User' here as soon as possible // add 'User' here as soon as possible
}, },
debug: CONFIG.DEBUG, debug: CONFIG.DEBUG,

View File

@ -0,0 +1,9 @@
import { neo4jgraphql } from 'neo4j-graphql-js'
export default {
Query: {
Badge: async (object, args, context, resolveInfo) => {
return neo4jgraphql(object, args, context, resolveInfo, false)
},
},
}

View File

@ -1,200 +0,0 @@
import { GraphQLClient } from 'graphql-request'
import Factory from '../../seed/factories'
import { host, login } from '../../jest/helpers'
const factory = Factory()
let client
describe('badges', () => {
beforeEach(async () => {
await factory.create('User', {
email: 'user@example.org',
role: 'user',
password: '1234',
})
await factory.create('User', {
id: 'u2',
role: 'moderator',
email: 'moderator@example.org',
})
await factory.create('User', {
id: 'u3',
role: 'admin',
email: 'admin@example.org',
})
})
afterEach(async () => {
await factory.cleanDatabase()
})
describe('CreateBadge', () => {
const variables = {
id: 'b1',
key: 'indiegogo_en_racoon',
type: 'crowdfunding',
status: 'permanent',
icon: '/img/badges/indiegogo_en_racoon.svg',
}
const mutation = `
mutation(
$id: ID
$key: String!
$type: BadgeType!
$status: BadgeStatus!
$icon: String!
) {
CreateBadge(id: $id, key: $key, type: $type, status: $status, icon: $icon) {
id,
key,
type,
status,
icon
}
}
`
describe('unauthenticated', () => {
it('throws authorization error', async () => {
client = new GraphQLClient(host)
await expect(client.request(mutation, variables)).rejects.toThrow('Not Authorised')
})
})
describe('authenticated admin', () => {
beforeEach(async () => {
const headers = await login({ email: 'admin@example.org', password: '1234' })
client = new GraphQLClient(host, { headers })
})
it('creates a badge', async () => {
const expected = {
CreateBadge: {
icon: '/img/badges/indiegogo_en_racoon.svg',
id: 'b1',
key: 'indiegogo_en_racoon',
status: 'permanent',
type: 'crowdfunding',
},
}
await expect(client.request(mutation, variables)).resolves.toEqual(expected)
})
})
describe('authenticated moderator', () => {
beforeEach(async () => {
const headers = await login({ email: 'moderator@example.org', password: '1234' })
client = new GraphQLClient(host, { headers })
})
it('throws authorization error', async () => {
await expect(client.request(mutation, variables)).rejects.toThrow('Not Authorised')
})
})
})
describe('UpdateBadge', () => {
beforeEach(async () => {
await factory.authenticateAs({ email: 'admin@example.org', password: '1234' })
await factory.create('Badge', { id: 'b1' })
})
const variables = {
id: 'b1',
key: 'whatever',
}
const mutation = `
mutation($id: ID!, $key: String!) {
UpdateBadge(id: $id, key: $key) {
id
key
}
}
`
describe('unauthenticated', () => {
it('throws authorization error', async () => {
client = new GraphQLClient(host)
await expect(client.request(mutation, variables)).rejects.toThrow('Not Authorised')
})
})
describe('authenticated moderator', () => {
beforeEach(async () => {
const headers = await login({ email: 'moderator@example.org', password: '1234' })
client = new GraphQLClient(host, { headers })
})
it('throws authorization error', async () => {
await expect(client.request(mutation, variables)).rejects.toThrow('Not Authorised')
})
})
describe('authenticated admin', () => {
beforeEach(async () => {
const headers = await login({ email: 'admin@example.org', password: '1234' })
client = new GraphQLClient(host, { headers })
})
it('updates a badge', async () => {
const expected = {
UpdateBadge: {
id: 'b1',
key: 'whatever',
},
}
await expect(client.request(mutation, variables)).resolves.toEqual(expected)
})
})
})
describe('DeleteBadge', () => {
beforeEach(async () => {
await factory.authenticateAs({ email: 'admin@example.org', password: '1234' })
await factory.create('Badge', { id: 'b1' })
})
const variables = {
id: 'b1',
}
const mutation = `
mutation($id: ID!) {
DeleteBadge(id: $id) {
id
}
}
`
describe('unauthenticated', () => {
it('throws authorization error', async () => {
client = new GraphQLClient(host)
await expect(client.request(mutation, variables)).rejects.toThrow('Not Authorised')
})
})
describe('authenticated moderator', () => {
beforeEach(async () => {
const headers = await login({ email: 'moderator@example.org', password: '1234' })
client = new GraphQLClient(host, { headers })
})
it('throws authorization error', async () => {
await expect(client.request(mutation, variables)).rejects.toThrow('Not Authorised')
})
})
describe('authenticated admin', () => {
beforeEach(async () => {
const headers = await login({ email: 'admin@example.org', password: '1234' })
client = new GraphQLClient(host, { headers })
})
it('deletes a badge', async () => {
const expected = {
DeleteBadge: {
id: 'b1',
},
}
await expect(client.request(mutation, variables)).resolves.toEqual(expected)
})
})
})
})

View File

@ -1,47 +1,47 @@
import { neode } from '../../bootstrap/neo4j'
import { UserInputError } from 'apollo-server'
const instance = neode()
const getUserAndBadge = async ({ badgeKey, userId }) => {
let user = await instance.first('User', 'id', userId)
const badge = await instance.first('Badge', 'id', badgeKey)
if (!user) throw new UserInputError("Couldn't find a user with that id")
if (!badge) throw new UserInputError("Couldn't find a badge with that id")
return { user, badge }
}
export default { export default {
Mutation: { Mutation: {
reward: async (_object, params, context, _resolveInfo) => { reward: async (_object, params, context, _resolveInfo) => {
const { fromBadgeId, toUserId } = params const { user, badge } = await getUserAndBadge(params)
const session = context.driver.session() await user.relateTo(badge, 'rewarded')
return user.toJson()
let transactionRes = await session.run(
`MATCH (badge:Badge {id: $badgeId}), (rewardedUser:User {id: $rewardedUserId})
MERGE (badge)-[:REWARDED]->(rewardedUser)
RETURN rewardedUser {.id}`,
{
badgeId: fromBadgeId,
rewardedUserId: toUserId,
},
)
const [rewardedUser] = transactionRes.records.map(record => {
return record.get('rewardedUser')
})
session.close()
return rewardedUser.id
}, },
unreward: async (_object, params, context, _resolveInfo) => { unreward: async (_object, params, context, _resolveInfo) => {
const { fromBadgeId, toUserId } = params const { badgeKey, userId } = params
const { user } = await getUserAndBadge(params)
const session = context.driver.session() const session = context.driver.session()
try {
let transactionRes = await session.run( // silly neode cannot remove relationships
`MATCH (badge:Badge {id: $badgeId})-[reward:REWARDED]->(rewardedUser:User {id: $rewardedUserId}) await session.run(
DELETE reward `
RETURN rewardedUser {.id}`, MATCH (badge:Badge {id: $badgeKey})-[reward:REWARDED]->(rewardedUser:User {id: $userId})
{ DELETE reward
badgeId: fromBadgeId, RETURN rewardedUser
rewardedUserId: toUserId, `,
}, {
) badgeKey,
const [rewardedUser] = transactionRes.records.map(record => { userId,
return record.get('rewardedUser') },
}) )
session.close() } catch (err) {
throw err
return rewardedUser.id } finally {
session.close()
}
return user.toJson()
}, },
}, },
} }

View File

@ -1,12 +1,20 @@
import { GraphQLClient } from 'graphql-request' import { GraphQLClient } from 'graphql-request'
import Factory from '../../seed/factories' import Factory from '../../seed/factories'
import { host, login } from '../../jest/helpers' import { host, login } from '../../jest/helpers'
import gql from 'graphql-tag'
const factory = Factory() const factory = Factory()
let user
let badge
describe('rewards', () => { describe('rewards', () => {
const variables = {
from: 'indiegogo_en_rhino',
to: 'u1',
}
beforeEach(async () => { beforeEach(async () => {
await factory.create('User', { user = await factory.create('User', {
id: 'u1', id: 'u1',
role: 'user', role: 'user',
email: 'user@example.org', email: 'user@example.org',
@ -22,9 +30,8 @@ describe('rewards', () => {
role: 'admin', role: 'admin',
email: 'admin@example.org', email: 'admin@example.org',
}) })
await factory.create('Badge', { badge = await factory.create('Badge', {
id: 'b6', id: 'indiegogo_en_rhino',
key: 'indiegogo_en_rhino',
type: 'crowdfunding', type: 'crowdfunding',
status: 'permanent', status: 'permanent',
icon: '/img/badges/indiegogo_en_rhino.svg', icon: '/img/badges/indiegogo_en_rhino.svg',
@ -35,21 +42,19 @@ describe('rewards', () => {
await factory.cleanDatabase() await factory.cleanDatabase()
}) })
describe('RewardBadge', () => { describe('reward', () => {
const mutation = ` const mutation = gql`
mutation( mutation($from: ID!, $to: ID!) {
$from: ID! reward(badgeKey: $from, userId: $to) {
$to: ID! id
) { badges {
reward(fromBadgeId: $from, toUserId: $to) id
}
}
} }
` `
describe('unauthenticated', () => { describe('unauthenticated', () => {
const variables = {
from: 'b6',
to: 'u1',
}
let client let client
it('throws authorization error', async () => { it('throws authorization error', async () => {
@ -65,74 +70,94 @@ describe('rewards', () => {
client = new GraphQLClient(host, { headers }) client = new GraphQLClient(host, { headers })
}) })
describe('badge for id does not exist', () => {
it('rejects with a telling error message', async () => {
await expect(
client.request(mutation, {
...variables,
from: 'bullshit',
}),
).rejects.toThrow("Couldn't find a badge with that id")
})
})
describe('user for id does not exist', () => {
it('rejects with a telling error message', async () => {
await expect(
client.request(mutation, {
...variables,
to: 'bullshit',
}),
).rejects.toThrow("Couldn't find a user with that id")
})
})
it('rewards a badge to user', async () => { it('rewards a badge to user', async () => {
const variables = {
from: 'b6',
to: 'u1',
}
const expected = { const expected = {
reward: 'u1', reward: {
id: 'u1',
badges: [{ id: 'indiegogo_en_rhino' }],
},
} }
await expect(client.request(mutation, variables)).resolves.toEqual(expected) await expect(client.request(mutation, variables)).resolves.toEqual(expected)
}) })
it('rewards a second different badge to same user', async () => { it('rewards a second different badge to same user', async () => {
await factory.create('Badge', { await factory.create('Badge', {
id: 'b1', id: 'indiegogo_en_racoon',
key: 'indiegogo_en_racoon',
type: 'crowdfunding',
status: 'permanent',
icon: '/img/badges/indiegogo_en_racoon.svg', icon: '/img/badges/indiegogo_en_racoon.svg',
}) })
const variables = {
from: 'b1',
to: 'u1',
}
const expected = { const expected = {
reward: 'u1', reward: {
id: 'u1',
badges: [{ id: 'indiegogo_en_racoon' }, { id: 'indiegogo_en_rhino' }],
},
} }
await expect(client.request(mutation, variables)).resolves.toEqual(expected) await client.request(mutation, variables)
await expect(
client.request(mutation, {
...variables,
from: 'indiegogo_en_racoon',
}),
).resolves.toEqual(expected)
}) })
it('rewards the same badge as well to another user', async () => { it('rewards the same badge as well to another user', async () => {
const variables1 = {
from: 'b6',
to: 'u1',
}
await client.request(mutation, variables1)
const variables2 = {
from: 'b6',
to: 'u2',
}
const expected = { const expected = {
reward: 'u2', reward: {
id: 'u2',
badges: [{ id: 'indiegogo_en_rhino' }],
},
} }
await expect(client.request(mutation, variables2)).resolves.toEqual(expected) await expect(
client.request(mutation, {
...variables,
to: 'u2',
}),
).resolves.toEqual(expected)
}) })
it('returns the original reward if a reward is attempted a second time', async () => {
const variables = { it('creates no duplicate reward relationships', async () => {
from: 'b6',
to: 'u1',
}
await client.request(mutation, variables) await client.request(mutation, variables)
await client.request(mutation, variables) await client.request(mutation, variables)
const query = `{ const query = gql`
User( id: "u1" ) { {
badgesCount User(id: "u1") {
badgesCount
badges {
id
}
}
} }
}
` `
const expected = { User: [{ badgesCount: 1 }] } const expected = { User: [{ badgesCount: 1, badges: [{ id: 'indiegogo_en_rhino' }] }] }
await expect(client.request(query)).resolves.toEqual(expected) await expect(client.request(query)).resolves.toEqual(expected)
}) })
}) })
describe('authenticated moderator', () => { describe('authenticated moderator', () => {
const variables = {
from: 'b6',
to: 'u1',
}
let client let client
beforeEach(async () => { beforeEach(async () => {
const headers = await login({ email: 'moderator@example.org', password: '1234' }) const headers = await login({ email: 'moderator@example.org', password: '1234' })
@ -147,27 +172,41 @@ describe('rewards', () => {
}) })
}) })
describe('RemoveReward', () => { describe('unreward', () => {
beforeEach(async () => { beforeEach(async () => {
await factory.relate('User', 'Badges', { from: 'b6', to: 'u1' }) await user.relateTo(badge, 'rewarded')
}) })
const variables = { const expected = { unreward: { id: 'u1', badges: [] } }
from: 'b6',
to: 'u1',
}
const expected = {
unreward: 'u1',
}
const mutation = ` const mutation = gql`
mutation( mutation($from: ID!, $to: ID!) {
$from: ID! unreward(badgeKey: $from, userId: $to) {
$to: ID! id
) { badges {
unreward(fromBadgeId: $from, toUserId: $to) id
}
}
} }
` `
describe('check test setup', () => {
it('user has one badge', async () => {
const query = gql`
{
User(id: "u1") {
badgesCount
badges {
id
}
}
}
`
const expected = { User: [{ badgesCount: 1, badges: [{ id: 'indiegogo_en_rhino' }] }] }
const client = new GraphQLClient(host)
await expect(client.request(query)).resolves.toEqual(expected)
})
})
describe('unauthenticated', () => { describe('unauthenticated', () => {
let client let client
@ -188,12 +227,9 @@ describe('rewards', () => {
await expect(client.request(mutation, variables)).resolves.toEqual(expected) await expect(client.request(mutation, variables)).resolves.toEqual(expected)
}) })
it('fails to remove a not existing badge from user', async () => { it('does not crash when unrewarding multiple times', async () => {
await client.request(mutation, variables) await client.request(mutation, variables)
await expect(client.request(mutation, variables)).resolves.toEqual(expected)
await expect(client.request(mutation, variables)).rejects.toThrow(
"Cannot read property 'id' of undefined",
)
}) })
}) })

View File

@ -139,7 +139,7 @@ export default {
organizationsCreated: '-[:CREATED_ORGA]->(related:Organization)', organizationsCreated: '-[:CREATED_ORGA]->(related:Organization)',
organizationsOwned: '-[:OWNING_ORGA]->(related:Organization)', organizationsOwned: '-[:OWNING_ORGA]->(related:Organization)',
categories: '-[:CATEGORIZED]->(related:Category)', categories: '-[:CATEGORIZED]->(related:Category)',
badges: '-[:REWARDED]->(related:Badge)', badges: '<-[:REWARDED]-(related:Badge)',
}), }),
}, },
} }

View File

@ -147,7 +147,7 @@ describe('users', () => {
} }
` `
beforeEach(async () => { beforeEach(async () => {
asAuthor = await factory.create('User', { await factory.create('User', {
email: 'test@example.org', email: 'test@example.org',
password: '1234', password: '1234',
id: 'u343', id: 'u343',
@ -191,6 +191,7 @@ describe('users', () => {
describe('attempting to delete my own account', () => { describe('attempting to delete my own account', () => {
let expectedResponse let expectedResponse
beforeEach(async () => { beforeEach(async () => {
asAuthor = Factory()
await asAuthor.authenticateAs({ await asAuthor.authenticateAs({
email: 'test@example.org', email: 'test@example.org',
password: '1234', password: '1234',

View File

@ -1,4 +0,0 @@
enum BadgeStatus {
permanent
temporary
}

View File

@ -1,4 +0,0 @@
enum BadgeType {
role
crowdfunding
}

View File

@ -28,8 +28,6 @@ type Mutation {
report(id: ID!, description: String): Report report(id: ID!, description: String): Report
disable(id: ID!): ID disable(id: ID!): ID
enable(id: ID!): ID enable(id: ID!): ID
reward(fromBadgeId: ID!, toUserId: ID!): ID
unreward(fromBadgeId: ID!, toUserId: ID!): ID
# Shout the given Type and ID # Shout the given Type and ID
shout(id: ID!, type: ShoutTypeEnum): Boolean! shout(id: ID!, type: ShoutTypeEnum): Boolean!
# Unshout the given Type and ID # Unshout the given Type and ID

View File

@ -1,324 +0,0 @@
scalar Upload
type Query {
isLoggedIn: Boolean!
# Get the currently logged in User based on the given JWT Token
currentUser: User
# Get the latest Network Statistics
statistics: Statistics!
findPosts(filter: String!, limit: Int = 10): [Post]! @cypher(
statement: """
CALL db.index.fulltext.queryNodes('full_text_search', $filter)
YIELD node as post, score
MATCH (post)<-[:WROTE]-(user:User)
WHERE score >= 0.2
AND NOT user.deleted = true AND NOT user.disabled = true
AND NOT post.deleted = true AND NOT post.disabled = true
RETURN post
LIMIT $limit
"""
)
CommentByPost(postId: ID!): [Comment]!
}
type Mutation {
# Get a JWT Token for the given Email and password
login(email: String!, password: String!): String!
signup(email: String!, password: String!): Boolean!
changePassword(oldPassword:String!, newPassword: String!): String!
report(id: ID!, description: String): Report
disable(id: ID!): ID
enable(id: ID!): ID
reward(fromBadgeId: ID!, toUserId: ID!): ID
unreward(fromBadgeId: ID!, toUserId: ID!): ID
# Shout the given Type and ID
shout(id: ID!, type: ShoutTypeEnum): Boolean!
# Unshout the given Type and ID
unshout(id: ID!, type: ShoutTypeEnum): Boolean!
# Follow the given Type and ID
follow(id: ID!, type: FollowTypeEnum): Boolean!
# Unfollow the given Type and ID
unfollow(id: ID!, type: FollowTypeEnum): Boolean!
}
type Statistics {
countUsers: Int!
countPosts: Int!
countComments: Int!
countNotifications: Int!
countOrganizations: Int!
countProjects: Int!
countInvites: Int!
countFollows: Int!
countShouts: Int!
}
type Notification {
id: ID!
read: Boolean,
user: User @relation(name: "NOTIFIED", direction: "OUT")
post: Post @relation(name: "NOTIFIED", direction: "IN")
createdAt: String
}
scalar Date
scalar Time
scalar DateTime
enum VisibilityEnum {
public
friends
private
}
enum UserGroupEnum {
admin
moderator
user
}
type Location {
id: ID!
name: String!
nameEN: String
nameDE: String
nameFR: String
nameNL: String
nameIT: String
nameES: String
namePT: String
namePL: String
type: String!
lat: Float
lng: Float
parent: Location @cypher(statement: "MATCH (this)-[:IS_IN]->(l:Location) RETURN l")
}
type User {
id: ID!
actorId: String
name: String
email: String!
slug: String
password: String!
avatar: String
avatarUpload: Upload
deleted: Boolean
disabled: Boolean
disabledBy: User @relation(name: "DISABLED", direction: "IN")
role: UserGroupEnum
publicKey: String
privateKey: String
location: Location @cypher(statement: "MATCH (this)-[:IS_IN]->(l:Location) RETURN l")
locationName: String
about: String
socialMedia: [SocialMedia]! @relation(name: "OWNED", direction: "OUT")
createdAt: String
updatedAt: String
notifications(read: Boolean): [Notification]! @relation(name: "NOTIFIED", direction: "IN")
friends: [User]! @relation(name: "FRIENDS", direction: "BOTH")
friendsCount: Int! @cypher(statement: "MATCH (this)<-[:FRIENDS]->(r:User) RETURN COUNT(DISTINCT r)")
following: [User]! @relation(name: "FOLLOWS", direction: "OUT")
followingCount: Int! @cypher(statement: "MATCH (this)-[:FOLLOWS]->(r:User) RETURN COUNT(DISTINCT r)")
followedBy: [User]! @relation(name: "FOLLOWS", direction: "IN")
followedByCount: Int! @cypher(statement: "MATCH (this)<-[:FOLLOWS]-(r:User) RETURN COUNT(DISTINCT r)")
# Is the currently logged in user following that user?
followedByCurrentUser: Boolean! @cypher(
statement: """
MATCH (this)<-[:FOLLOWS]-(u:User {id: $cypherParams.currentUserId})
RETURN COUNT(u) >= 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"
# )
contributions: [Post]! @relation(name: "WROTE", direction: "OUT")
contributionsCount: Int! @cypher(
statement: """
MATCH (this)-[:WROTE]->(r:Post)
WHERE (NOT exists(r.deleted) OR r.deleted = false)
AND (NOT exists(r.disabled) OR r.disabled = false)
RETURN COUNT(r)
"""
)
comments: [Comment]! @relation(name: "WROTE", direction: "OUT")
commentsCount: Int! @cypher(statement: "MATCH (this)-[:WROTE]->(r:Comment) WHERE NOT r.deleted = true AND NOT r.disabled = true RETURN COUNT(r)")
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)")
organizationsCreated: [Organization] @relation(name: "CREATED_ORGA", direction: "OUT")
organizationsOwned: [Organization] @relation(name: "OWNING_ORGA", direction: "OUT")
blacklisted: [User]! @relation(name: "BLACKLISTED", direction: "OUT")
categories: [Category]! @relation(name: "CATEGORIZED", direction: "OUT")
badges: [Badge]! @relation(name: "REWARDED", direction: "IN")
badgesCount: Int! @cypher(statement: "MATCH (this)<-[:REWARDED]-(r:Badge) RETURN COUNT(r)")
}
type Post {
id: ID!
activityId: String
objectId: String
author: User @relation(name: "WROTE", direction: "IN")
title: String!
slug: String
content: String!
contentExcerpt: String
image: String
imageUpload: Upload
visibility: VisibilityEnum
deleted: Boolean
disabled: Boolean
disabledBy: User @relation(name: "DISABLED", direction: "IN")
createdAt: String
updatedAt: String
relatedContributions: [Post]! @cypher(
statement: """
MATCH (this)-[:TAGGED|CATEGORIZED]->(categoryOrTag)<-[:TAGGED|CATEGORIZED]-(post:Post)
RETURN DISTINCT post
LIMIT 10
"""
)
tags: [Tag]! @relation(name: "TAGGED", direction: "OUT")
categories: [Category]! @relation(name: "CATEGORIZED", direction: "OUT")
comments: [Comment]! @relation(name: "COMMENTS", direction: "IN")
commentsCount: Int! @cypher(statement: "MATCH (this)<-[:COMMENTS]-(r:Comment) WHERE NOT r.deleted = true AND NOT r.disabled = true RETURN COUNT(r)")
shoutedBy: [User]! @relation(name: "SHOUTED", direction: "IN")
shoutedCount: Int! @cypher(statement: "MATCH (this)<-[:SHOUTED]-(r:User) WHERE NOT r.deleted = true AND NOT r.disabled = true RETURN COUNT(DISTINCT r)")
# Has the currently logged in user shouted that post?
shoutedByCurrentUser: Boolean! @cypher(
statement: """
MATCH (this)<-[:SHOUTED]-(u:User {id: $cypherParams.currentUserId})
RETURN COUNT(u) >= 1
"""
)
}
type Comment {
id: ID!
activityId: String
postId: ID
author: User @relation(name: "WROTE", direction: "IN")
content: String!
contentExcerpt: String
post: Post @relation(name: "COMMENTS", direction: "OUT")
createdAt: String
updatedAt: String
deleted: Boolean
disabled: Boolean
disabledBy: User @relation(name: "DISABLED", direction: "IN")
}
type Report {
id: ID!
submitter: User @relation(name: "REPORTED", direction: "IN")
description: String
type: String! @cypher(statement: "MATCH (resource)<-[:REPORTED]-(this) RETURN labels(resource)[0]")
createdAt: String
comment: Comment @relation(name: "REPORTED", direction: "OUT")
post: Post @relation(name: "REPORTED", direction: "OUT")
user: User @relation(name: "REPORTED", direction: "OUT")
}
type Category {
id: ID!
name: String!
slug: String
icon: String!
posts: [Post]! @relation(name: "CATEGORIZED", direction: "IN")
postCount: Int! @cypher(statement: "MATCH (this)<-[:CATEGORIZED]-(r:Post) RETURN COUNT(r)")
}
type Badge {
id: ID!
key: String!
type: BadgeTypeEnum!
status: BadgeStatusEnum!
icon: String!
rewarded: [User]! @relation(name: "REWARDED", direction: "OUT")
}
enum BadgeTypeEnum {
role
crowdfunding
}
enum BadgeStatusEnum {
permanent
temporary
}
enum ShoutTypeEnum {
Post
Organization
Project
}
enum FollowTypeEnum {
User
Organization
Project
}
type Reward {
id: ID!
user: User @relation(name: "REWARDED", direction: "IN")
rewarderId: ID
createdAt: String
badge: Badge @relation(name: "REWARDED", direction: "OUT")
}
type Organization {
id: ID!
createdBy: User @relation(name: "CREATED_ORGA", direction: "IN")
ownedBy: [User] @relation(name: "OWNING_ORGA", direction: "IN")
name: String!
slug: String
description: String!
descriptionExcerpt: String
deleted: Boolean
disabled: Boolean
tags: [Tag]! @relation(name: "TAGGED", direction: "OUT")
categories: [Category]! @relation(name: "CATEGORIZED", direction: "OUT")
}
type Tag {
id: ID!
name: String!
taggedPosts: [Post]! @relation(name: "TAGGED", direction: "IN")
taggedOrganizations: [Organization]! @relation(name: "TAGGED", direction: "IN")
taggedCount: Int! @cypher(statement: "MATCH (this)<-[:TAGGED]-(p) RETURN COUNT(DISTINCT p)")
taggedCountUnique: Int! @cypher(statement: "MATCH (this)<-[:TAGGED]-(p)<-[:WROTE]-(u:User) RETURN COUNT(DISTINCT u)")
deleted: Boolean
disabled: Boolean
}
type SharedInboxEndpoint {
id: ID!
uri: String
}
type SocialMedia {
id: ID!
url: String
ownedBy: [User]! @relation(name: "OWNED", direction: "IN")
}

View File

@ -1,6 +1,5 @@
type Badge { type Badge {
id: ID! id: ID!
key: String!
type: BadgeType! type: BadgeType!
status: BadgeStatus! status: BadgeStatus!
icon: String! icon: String!
@ -10,4 +9,23 @@ type Badge {
updatedAt: String updatedAt: String
rewarded: [User]! @relation(name: "REWARDED", direction: "OUT") rewarded: [User]! @relation(name: "REWARDED", direction: "OUT")
} }
enum BadgeStatus {
permanent
temporary
}
enum BadgeType {
role
crowdfunding
}
type Query {
Badge: [Badge]
}
type Mutation {
reward(badgeKey: ID!, userId: ID!): User
unreward(badgeKey: ID!, userId: ID!): User
}

View File

@ -1,28 +1,15 @@
import uuid from 'uuid/v4' export default function create() {
export default function(params) {
const {
id = uuid(),
key = '',
type = 'crowdfunding',
status = 'permanent',
icon = '/img/badges/indiegogo_en_panda.svg',
} = params
return { return {
mutation: ` factory: async ({ args, neodeInstance }) => {
mutation( const defaults = {
$id: ID type: 'crowdfunding',
$key: String! status: 'permanent',
$type: BadgeType!
$status: BadgeStatus!
$icon: String!
) {
CreateBadge(id: $id, key: $key, type: $type, status: $status, icon: $icon) {
id
}
} }
`, args = {
variables: { id, key, type, status, icon }, ...defaults,
...args,
}
return neodeInstance.create('Badge', args)
},
} }
} }

View File

@ -73,6 +73,7 @@ export default function Factory(options = {}) {
const { factory, mutation, variables } = this.factories[node](args) const { factory, mutation, variables } = this.factories[node](args)
if (factory) { if (factory) {
this.lastResponse = await factory({ args, neodeInstance }) this.lastResponse = await factory({ args, neodeInstance })
return this.lastResponse
} else { } else {
this.lastResponse = await this.graphQLClient.request(mutation, variables) this.lastResponse = await this.graphQLClient.request(mutation, variables)
} }

View File

@ -3,7 +3,7 @@ import uuid from 'uuid/v4'
import encryptPassword from '../../helpers/encryptPassword' import encryptPassword from '../../helpers/encryptPassword'
import slugify from 'slug' import slugify from 'slug'
export default function create(params) { export default function create() {
return { return {
factory: async ({ args, neodeInstance }) => { factory: async ({ args, neodeInstance }) => {
const defaults = { const defaults = {
@ -21,8 +21,7 @@ export default function create(params) {
...args, ...args,
} }
args = await encryptPassword(args) args = await encryptPassword(args)
const user = await neodeInstance.create('User', args) return neodeInstance.create('User', args)
return user.toJson()
}, },
} }
} }

View File

@ -5,52 +5,42 @@ import Factory from './factories'
;(async function() { ;(async function() {
try { try {
const f = Factory() const f = Factory()
await Promise.all([ const [racoon, rabbit, wolf, bear, turtle, rhino] = await Promise.all([
f.create('Badge', { f.create('Badge', {
id: 'b1', id: 'indiegogo_en_racoon',
key: 'indiegogo_en_racoon',
type: 'crowdfunding',
status: 'permanent',
icon: '/img/badges/indiegogo_en_racoon.svg', icon: '/img/badges/indiegogo_en_racoon.svg',
}), }),
f.create('Badge', { f.create('Badge', {
id: 'b2', id: 'indiegogo_en_rabbit',
key: 'indiegogo_en_rabbit',
type: 'crowdfunding',
status: 'permanent',
icon: '/img/badges/indiegogo_en_rabbit.svg', icon: '/img/badges/indiegogo_en_rabbit.svg',
}), }),
f.create('Badge', { f.create('Badge', {
id: 'b3', id: 'indiegogo_en_wolf',
key: 'indiegogo_en_wolf',
type: 'crowdfunding',
status: 'permanent',
icon: '/img/badges/indiegogo_en_wolf.svg', icon: '/img/badges/indiegogo_en_wolf.svg',
}), }),
f.create('Badge', { f.create('Badge', {
id: 'b4', id: 'indiegogo_en_bear',
key: 'indiegogo_en_bear',
type: 'crowdfunding',
status: 'permanent',
icon: '/img/badges/indiegogo_en_bear.svg', icon: '/img/badges/indiegogo_en_bear.svg',
}), }),
f.create('Badge', { f.create('Badge', {
id: 'b5', id: 'indiegogo_en_turtle',
key: 'indiegogo_en_turtle',
type: 'crowdfunding',
status: 'permanent',
icon: '/img/badges/indiegogo_en_turtle.svg', icon: '/img/badges/indiegogo_en_turtle.svg',
}), }),
f.create('Badge', { f.create('Badge', {
id: 'b6', id: 'indiegogo_en_rhino',
key: 'indiegogo_en_rhino',
type: 'crowdfunding',
status: 'permanent',
icon: '/img/badges/indiegogo_en_rhino.svg', icon: '/img/badges/indiegogo_en_rhino.svg',
}), }),
]) ])
await Promise.all([ const [
peterLustig,
bobDerBaumeister,
jennyRostock,
tick, // eslint-disable-line no-unused-vars
trick, // eslint-disable-line no-unused-vars
track, // eslint-disable-line no-unused-vars
dagobert,
] = await Promise.all([
f.create('User', { f.create('User', {
id: 'u1', id: 'u1',
name: 'Peter Lustig', name: 'Peter Lustig',
@ -123,30 +113,16 @@ import Factory from './factories'
]) ])
await Promise.all([ await Promise.all([
f.relate('User', 'Badges', { peterLustig.relateTo(racoon, 'rewarded'),
from: 'b6', peterLustig.relateTo(rhino, 'rewarded'),
to: 'u1', peterLustig.relateTo(wolf, 'rewarded'),
}), bobDerBaumeister.relateTo(racoon, 'rewarded'),
f.relate('User', 'Badges', { bobDerBaumeister.relateTo(turtle, 'rewarded'),
from: 'b5', jennyRostock.relateTo(bear, 'rewarded'),
to: 'u2', dagobert.relateTo(rabbit, 'rewarded'),
}), ])
f.relate('User', 'Badges', {
from: 'b4', await Promise.all([
to: 'u3',
}),
f.relate('User', 'Badges', {
from: 'b3',
to: 'u4',
}),
f.relate('User', 'Badges', {
from: 'b2',
to: 'u5',
}),
f.relate('User', 'Badges', {
from: 'b1',
to: 'u6',
}),
f.relate('User', 'Friends', { f.relate('User', 'Friends', {
from: 'u1', from: 'u1',
to: 'u2', to: 'u2',

View File

@ -23,24 +23,27 @@ Cypress.Commands.add('factory', () => {
Cypress.Commands.add( Cypress.Commands.add(
'create', 'create',
{ prevSubject: true }, { prevSubject: true },
(factory, node, properties) => { async (factory, node, properties) => {
return factory.create(node, properties) await factory.create(node, properties)
return factory
} }
) )
Cypress.Commands.add( Cypress.Commands.add(
'relate', 'relate',
{ prevSubject: true }, { prevSubject: true },
(factory, node, relationship, properties) => { async (factory, node, relationship, properties) => {
return factory.relate(node, relationship, properties) await factory.relate(node, relationship, properties)
return factory
} }
) )
Cypress.Commands.add( Cypress.Commands.add(
'mutate', 'mutate',
{ prevSubject: true }, { prevSubject: true },
(factory, mutation, variables) => { async (factory, mutation, variables) => {
return factory.mutate(mutation, variables) await factory.mutate(mutation, variables)
return factory
} }
) )

View File

@ -25,7 +25,7 @@
[?] type: String, // in nitro this is a defined enum - seems good for now [?] type: String, // in nitro this is a defined enum - seems good for now
[X] required: true [X] required: true
}, },
[X] key: { [X] id: {
[X] type: String, [X] type: String,
[X] required: true [X] required: true
}, },
@ -43,7 +43,7 @@
CALL apoc.load.json("file:${IMPORT_CHUNK_PATH_CQL_FILE}") YIELD value as badge CALL apoc.load.json("file:${IMPORT_CHUNK_PATH_CQL_FILE}") YIELD value as badge
MERGE(b:Badge {id: badge._id["$oid"]}) MERGE(b:Badge {id: badge._id["$oid"]})
ON CREATE SET ON CREATE SET
b.key = badge.key, b.id = badge.key,
b.type = badge.type, b.type = badge.type,
b.icon = replace(badge.image.path, 'https://api-alpha.human-connection.org', ''), b.icon = replace(badge.image.path, 'https://api-alpha.human-connection.org', ''),
b.status = badge.status, b.status = badge.status,

View File

@ -1,6 +1,6 @@
<template> <template>
<div :class="[badges.length === 2 && 'hc-badges-dual']" class="hc-badges"> <div :class="[badges.length === 2 && 'hc-badges-dual']" class="hc-badges">
<div v-for="badge in badges" :key="badge.key" class="hc-badge-container"> <div v-for="badge in badges" :key="badge.id" class="hc-badge-container">
<img :title="badge.key" :src="badge.icon | proxyApiUrl" class="hc-badge" /> <img :title="badge.key" :src="badge.icon | proxyApiUrl" class="hc-badge" />
</div> </div>
</div> </div>

View File

@ -47,7 +47,8 @@ export default {
}, },
watch: { watch: {
Post(post) { Post(post) {
this.comments = post[0].comments || [] const [first] = post
this.comments = (first && first.comments) || []
}, },
}, },
apollo: { apollo: {

View File

@ -25,7 +25,6 @@ export default app => {
} }
badges { badges {
id id
key
icon icon
} }
} }

View File

@ -29,7 +29,6 @@ export default i18n => {
} }
badges { badges {
id id
key
icon icon
} }
} }

View File

@ -30,7 +30,6 @@ export default i18n => {
} }
badges { badges {
id id
key
icon icon
} }
} }
@ -61,7 +60,6 @@ export default i18n => {
} }
badges { badges {
id id
key
icon icon
} }
} }

View File

@ -19,7 +19,6 @@ export default i18n => {
createdAt createdAt
badges { badges {
id id
key
icon icon
} }
badgesCount badgesCount
@ -39,7 +38,6 @@ export default i18n => {
commentsCount commentsCount
badges { badges {
id id
key
icon icon
} }
location { location {
@ -61,7 +59,6 @@ export default i18n => {
commentsCount commentsCount
badges { badges {
id id
key
icon icon
} }
location { location {

View File

@ -157,7 +157,6 @@ export default {
} }
badges { badges {
id id
key
icon icon
} }
} }

View File

@ -111,7 +111,6 @@ export default {
} }
badges { badges {
id id
key
icon icon
} }
} }