diff --git a/backend/src/schema/index.js b/backend/src/schema/index.js index 0f724d9b5..2b24178e1 100644 --- a/backend/src/schema/index.js +++ b/backend/src/schema/index.js @@ -19,6 +19,7 @@ export default applyScalars( 'Notfication', 'Statistics', 'LoggedInUser', + 'SocialMedia', ], // add 'User' here as soon as possible }, @@ -30,6 +31,7 @@ export default applyScalars( 'Notfication', 'Statistics', 'LoggedInUser', + 'SocialMedia', ], // add 'User' here as soon as possible }, diff --git a/backend/src/schema/resolvers/embeds.js b/backend/src/schema/resolvers/embeds.js index ba27f77b2..016352950 100644 --- a/backend/src/schema/resolvers/embeds.js +++ b/backend/src/schema/resolvers/embeds.js @@ -1,5 +1,5 @@ import scrape from './embeds/scraper.js' -import { undefinedToNull } from '../helpers' +import { undefinedToNullResolver } from './helpers/Resolver' export default { Query: { @@ -8,7 +8,7 @@ export default { }, }, Embed: { - ...undefinedToNull([ + ...undefinedToNullResolver([ 'type', 'title', 'author', diff --git a/backend/src/schema/resolvers/helpers/Resolver.js b/backend/src/schema/resolvers/helpers/Resolver.js new file mode 100644 index 000000000..655cf08a0 --- /dev/null +++ b/backend/src/schema/resolvers/helpers/Resolver.js @@ -0,0 +1,75 @@ +import { neode } from '../../../bootstrap/neo4j' + +export const undefinedToNullResolver = list => { + const resolvers = {} + list.forEach(key => { + resolvers[key] = async (parent, params, context, resolveInfo) => { + return typeof parent[key] === 'undefined' ? null : parent[key] + } + }) + return resolvers +} + +export default function Resolver(type, options = {}) { + const instance = neode() + const { + idAttribute = 'id', + undefinedToNull = [], + count = {}, + hasOne = {}, + hasMany = {}, + } = options + const _hasResolver = (resolvers, { key, connection }, { returnType }) => { + return async (parent, params, context, resolveInfo) => { + if (typeof parent[key] !== 'undefined') return parent[key] + const id = parent[idAttribute] + const statement = `MATCH(:${type} {${idAttribute}: {id}})${connection} RETURN related` + const result = await instance.cypher(statement, { id }) + let response = result.records.map(r => r.get('related').properties) + if (returnType === 'object') response = response[0] || null + return response + } + } + + const countResolver = obj => { + const resolvers = {} + for (const [key, connection] of Object.entries(obj)) { + resolvers[key] = async (parent, params, context, resolveInfo) => { + if (typeof parent[key] !== 'undefined') return parent[key] + const id = parent[idAttribute] + const statement = ` + MATCH(u:${type} {${idAttribute}: {id}})${connection} + WHERE NOT related.deleted = true AND NOT related.disabled = true + RETURN COUNT(DISTINCT(related)) as count + ` + const result = await instance.cypher(statement, { id }) + const [response] = result.records.map(r => r.get('count').toNumber()) + return response + } + } + return resolvers + } + + const hasManyResolver = obj => { + const resolvers = {} + for (const [key, connection] of Object.entries(obj)) { + resolvers[key] = _hasResolver(resolvers, { key, connection }, { returnType: 'iterable' }) + } + return resolvers + } + + const hasOneResolver = obj => { + const resolvers = {} + for (const [key, connection] of Object.entries(obj)) { + resolvers[key] = _hasResolver(resolvers, { key, connection }, { returnType: 'object' }) + } + return resolvers + } + const result = { + ...undefinedToNullResolver(undefinedToNull), + ...countResolver(count), + ...hasOneResolver(hasOne), + ...hasManyResolver(hasMany), + } + return result +} diff --git a/backend/src/schema/resolvers/socialMedia.js b/backend/src/schema/resolvers/socialMedia.js index 56d74271a..d67a41636 100644 --- a/backend/src/schema/resolvers/socialMedia.js +++ b/backend/src/schema/resolvers/socialMedia.js @@ -1,4 +1,5 @@ import { neode } from '../../bootstrap/neo4j' +import Resolver from './helpers/Resolver' const instance = neode() @@ -14,13 +15,6 @@ export default { return response }, - DeleteSocialMedia: async (object, params, context, resolveInfo) => { - const socialMedia = await instance.find('SocialMedia', params.id) - const response = await socialMedia.toJson() - await socialMedia.delete() - - return response - }, UpdateSocialMedia: async (object, params, context, resolveInfo) => { const socialMedia = await instance.find('SocialMedia', params.id) await socialMedia.update({ url: params.url }) @@ -28,5 +22,17 @@ export default { return response }, + DeleteSocialMedia: async (object, { id }, context, resolveInfo) => { + const socialMedia = await instance.find('SocialMedia', id) + if (!socialMedia) return null + await socialMedia.delete() + return socialMedia.toJson() + }, }, + SocialMedia: Resolver('SocialMedia', { + idAttribute: 'url', + hasOne: { + ownedBy: '<-[:OWNED_BY]-(related:User)', + }, + }), } diff --git a/backend/src/schema/resolvers/socialMedia.spec.js b/backend/src/schema/resolvers/socialMedia.spec.js index 4c09c7ab9..1bbcb8d5b 100644 --- a/backend/src/schema/resolvers/socialMedia.spec.js +++ b/backend/src/schema/resolvers/socialMedia.spec.js @@ -122,6 +122,32 @@ describe('SocialMedia', () => { ) }) }) + + describe('ownedBy', () => { + beforeEach(() => { + mutation = gql` + mutation($url: String!) { + CreateSocialMedia(url: $url) { + url + ownedBy { + name + } + } + } + ` + }) + + it('resolves', async () => { + const user = someUser + await expect(socialMediaAction(user, mutation, variables)).resolves.toEqual( + expect.objectContaining({ + data: { + CreateSocialMedia: { url, ownedBy: { name: 'Kalle Blomqvist' } }, + }, + }), + ) + }) + }) }) describe('update social media', () => { diff --git a/backend/src/schema/resolvers/users.js b/backend/src/schema/resolvers/users.js index ab74acacd..c0826c6c8 100644 --- a/backend/src/schema/resolvers/users.js +++ b/backend/src/schema/resolvers/users.js @@ -2,57 +2,10 @@ import { neo4jgraphql } from 'neo4j-graphql-js' import fileUpload from './fileUpload' import { neode } from '../../bootstrap/neo4j' import { UserInputError } from 'apollo-server' -import { undefinedToNull } from '../helpers' +import Resolver from './helpers/Resolver' const instance = neode() -const _has = (resolvers, { key, connection }, { returnType }) => { - return async (parent, params, context, resolveInfo) => { - if (typeof parent[key] !== 'undefined') return parent[key] - const { id } = parent - const statement = `MATCH(u:User {id: {id}})${connection} RETURN related` - const result = await instance.cypher(statement, { id }) - let response = result.records.map(r => r.get('related').properties) - if (returnType === 'object') response = response[0] || null - return response - } -} - -const count = obj => { - const resolvers = {} - for (const [key, connection] of Object.entries(obj)) { - resolvers[key] = async (parent, params, context, resolveInfo) => { - if (typeof parent[key] !== 'undefined') return parent[key] - const { id } = parent - const statement = ` - MATCH(u:User {id: {id}})${connection} - WHERE NOT related.deleted = true AND NOT related.disabled = true - RETURN COUNT(DISTINCT(related)) as count - ` - const result = await instance.cypher(statement, { id }) - const [response] = result.records.map(r => r.get('count').toNumber()) - return response - } - } - return resolvers -} - -export const hasMany = obj => { - const resolvers = {} - for (const [key, connection] of Object.entries(obj)) { - resolvers[key] = _has(resolvers, { key, connection }, { returnType: 'iterable' }) - } - return resolvers -} - -export const hasOne = obj => { - const resolvers = {} - for (const [key, connection] of Object.entries(obj)) { - resolvers[key] = _has(resolvers, { key, connection }, { returnType: 'object' }) - } - return resolvers -} - export default { Query: { User: async (object, args, context, resolveInfo) => { @@ -110,42 +63,44 @@ export default { let [{ email }] = result.records.map(r => r.get('e').properties) return email }, - ...undefinedToNull([ - 'actorId', - 'avatar', - 'coverImg', - 'deleted', - 'disabled', - 'locationName', - 'about', - ]), - ...count({ - contributionsCount: '-[:WROTE]->(related:Post)', - friendsCount: '<-[:FRIENDS]->(related:User)', - followingCount: '-[:FOLLOWS]->(related:User)', - followedByCount: '<-[:FOLLOWS]-(related:User)', - commentsCount: '-[:WROTE]->(r:Comment)', - commentedCount: '-[:WROTE]->(:Comment)-[:COMMENTS]->(related:Post)', - shoutedCount: '-[:SHOUTED]->(related:Post)', - badgesCount: '<-[:REWARDED]-(related:Badge)', - }), - ...hasOne({ - invitedBy: '<-[:INVITED]-(related:User)', - disabledBy: '<-[:DISABLED]-(related:User)', - }), - ...hasMany({ - followedBy: '<-[:FOLLOWS]-(related:User)', - following: '-[:FOLLOWS]->(related:User)', - friends: '-[:FRIENDS]-(related:User)', - blacklisted: '-[:BLACKLISTED]->(related:User)', - socialMedia: '-[:OWNED_BY]->(related:SocialMedia', - contributions: '-[:WROTE]->(related:Post)', - comments: '-[:WROTE]->(related:Comment)', - shouted: '-[:SHOUTED]->(related:Post)', - organizationsCreated: '-[:CREATED_ORGA]->(related:Organization)', - organizationsOwned: '-[:OWNING_ORGA]->(related:Organization)', - categories: '-[:CATEGORIZED]->(related:Category)', - badges: '<-[:REWARDED]-(related:Badge)', + ...Resolver('User', { + undefinedToNull: [ + 'actorId', + 'avatar', + 'coverImg', + 'deleted', + 'disabled', + 'locationName', + 'about', + ], + count: { + contributionsCount: '-[:WROTE]->(related:Post)', + friendsCount: '<-[:FRIENDS]->(related:User)', + followingCount: '-[:FOLLOWS]->(related:User)', + followedByCount: '<-[:FOLLOWS]-(related:User)', + commentsCount: '-[:WROTE]->(r:Comment)', + commentedCount: '-[:WROTE]->(:Comment)-[:COMMENTS]->(related:Post)', + shoutedCount: '-[:SHOUTED]->(related:Post)', + badgesCount: '<-[:REWARDED]-(related:Badge)', + }, + hasOne: { + invitedBy: '<-[:INVITED]-(related:User)', + disabledBy: '<-[:DISABLED]-(related:User)', + }, + hasMany: { + followedBy: '<-[:FOLLOWS]-(related:User)', + following: '-[:FOLLOWS]->(related:User)', + friends: '-[:FRIENDS]-(related:User)', + blacklisted: '-[:BLACKLISTED]->(related:User)', + socialMedia: '-[:OWNED_BY]->(related:SocialMedia', + contributions: '-[:WROTE]->(related:Post)', + comments: '-[:WROTE]->(related:Comment)', + shouted: '-[:SHOUTED]->(related:Post)', + organizationsCreated: '-[:CREATED_ORGA]->(related:Organization)', + organizationsOwned: '-[:OWNING_ORGA]->(related:Organization)', + categories: '-[:CATEGORIZED]->(related:Category)', + badges: '<-[:REWARDED]-(related:Badge)', + }, }), }, } diff --git a/backend/src/schema/types/schema.gql b/backend/src/schema/types/schema.gql index 82c02ab7e..41fe8f4e6 100644 --- a/backend/src/schema/types/schema.gql +++ b/backend/src/schema/types/schema.gql @@ -131,8 +131,3 @@ type SharedInboxEndpoint { uri: String } -type SocialMedia { - id: ID! - url: String - ownedBy: [User]! @relation(name: "OWNED_BY", direction: "IN") -} diff --git a/backend/src/schema/types/type/SocialMedia.gql b/backend/src/schema/types/type/SocialMedia.gql new file mode 100644 index 000000000..230938d95 --- /dev/null +++ b/backend/src/schema/types/type/SocialMedia.gql @@ -0,0 +1,11 @@ +type SocialMedia { + id: ID! + url: String + ownedBy: User! @relation(name: "OWNED_BY", direction: "IN") +} + +type Mutation { + CreateSocialMedia(id: ID, url: String!): SocialMedia + UpdateSocialMedia(id: ID!, url: String!): SocialMedia + DeleteSocialMedia(id: ID!): SocialMedia +}