diff --git a/backend/src/middleware/permissionsMiddleware.js b/backend/src/middleware/permissionsMiddleware.js index a803adb64..afc38a65c 100644 --- a/backend/src/middleware/permissionsMiddleware.js +++ b/backend/src/middleware/permissionsMiddleware.js @@ -1,4 +1,7 @@ import { rule, shield, deny, allow, and, or, not } from 'graphql-shield' +import { neode } from '../bootstrap/neo4j' + +const instance = neode() /* * TODO: implement @@ -7,7 +10,7 @@ import { rule, shield, deny, allow, and, or, not } from 'graphql-shield' const isAuthenticated = rule({ cache: 'contextual', })(async (_parent, _args, ctx, _info) => { - return ctx.user !== null + return ctx.user != null }) const isModerator = rule()(async (parent, args, { user }, info) => { @@ -30,6 +33,14 @@ const isMyOwn = rule({ return context.user.id === parent.id }) +const isMySocialMedia = rule({ + cache: 'no_cache', +})(async (_, args, { user }) => { + let socialMedia = await instance.find('SocialMedia', args.id) + socialMedia = await socialMedia.toJson() + return socialMedia.ownedBy.node.id === user.id +}) + const belongsToMe = rule({ cache: 'no_cache', })(async (_, args, context) => { @@ -163,8 +174,8 @@ const permissions = shield( DeletePost: isAuthor, report: isAuthenticated, CreateSocialMedia: isAuthenticated, - UpdateSocialMedia: isAuthenticated, - DeleteSocialMedia: isAuthenticated, + UpdateSocialMedia: isMySocialMedia, + DeleteSocialMedia: isMySocialMedia, // AddBadgeRewarded: isAdmin, // RemoveBadgeRewarded: isAdmin, reward: isAdmin, diff --git a/backend/src/middleware/validation/validationMiddleware.js b/backend/src/middleware/validation/validationMiddleware.js index 319a9a6c0..2d354ad2b 100644 --- a/backend/src/middleware/validation/validationMiddleware.js +++ b/backend/src/middleware/validation/validationMiddleware.js @@ -1,23 +1,8 @@ import { UserInputError } from 'apollo-server' -import Joi from '@hapi/joi' const COMMENT_MIN_LENGTH = 1 const NO_POST_ERR_MESSAGE = 'Comment cannot be created without a post!' -const validate = schema => { - return async (resolve, root, args, context, info) => { - const validation = schema.validate(args) - if (validation.error) throw new UserInputError(validation.error) - return resolve(root, args, context, info) - } -} - -const socialMediaSchema = Joi.object().keys({ - url: Joi.string() - .uri() - .required(), -}) - const validateCommentCreation = async (resolve, root, args, context, info) => { const content = args.content.replace(/<(?:.|\n)*?>/gm, '').trim() const { postId } = args @@ -57,7 +42,6 @@ const validateUpdateComment = async (resolve, root, args, context, info) => { export default { Mutation: { - CreateSocialMedia: validate(socialMediaSchema), CreateComment: validateCommentCreation, UpdateComment: validateUpdateComment, }, diff --git a/backend/src/models/SocialMedia.js b/backend/src/models/SocialMedia.js new file mode 100644 index 000000000..d41391ec1 --- /dev/null +++ b/backend/src/models/SocialMedia.js @@ -0,0 +1,15 @@ +import uuid from 'uuid/v4' + +module.exports = { + id: { type: 'string', primary: true, default: uuid }, + url: { type: 'string', uri: true, required: true }, + createdAt: { type: 'string', isoDate: true, default: () => new Date().toISOString() }, + ownedBy: { + type: 'relationship', + relationship: 'OWNED_BY', + target: 'User', + direction: 'in', + eager: true, + cascade: 'detach', + }, +} diff --git a/backend/src/models/index.js b/backend/src/models/index.js index 09d1dbbeb..b468dedf2 100644 --- a/backend/src/models/index.js +++ b/backend/src/models/index.js @@ -5,4 +5,5 @@ export default { User: require('./User.js'), InvitationCode: require('./InvitationCode.js'), EmailAddress: require('./EmailAddress.js'), + SocialMedia: require('./SocialMedia.js'), } 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 d28bc3fe1..d67a41636 100644 --- a/backend/src/schema/resolvers/socialMedia.js +++ b/backend/src/schema/resolvers/socialMedia.js @@ -1,43 +1,38 @@ -import { neo4jgraphql } from 'neo4j-graphql-js' +import { neode } from '../../bootstrap/neo4j' +import Resolver from './helpers/Resolver' + +const instance = neode() export default { Mutation: { CreateSocialMedia: async (object, params, context, resolveInfo) => { - const socialMedia = await neo4jgraphql(object, params, context, resolveInfo, false) - const session = context.driver.session() - await session.run( - `MATCH (owner:User {id: $userId}), (socialMedia:SocialMedia {id: $socialMediaId}) - MERGE (socialMedia)<-[:OWNED]-(owner) - RETURN owner`, - { - userId: context.user.id, - socialMediaId: socialMedia.id, - }, - ) - session.close() + const [user, socialMedia] = await Promise.all([ + instance.find('User', context.user.id), + instance.create('SocialMedia', params), + ]) + await socialMedia.relateTo(user, 'ownedBy') + const response = await socialMedia.toJson() - return socialMedia - }, - DeleteSocialMedia: async (object, params, context, resolveInfo) => { - const socialMedia = await neo4jgraphql(object, params, context, resolveInfo, false) - - return socialMedia + return response }, UpdateSocialMedia: async (object, params, context, resolveInfo) => { - const session = context.driver.session() - await session.run( - `MATCH (owner: User { id: $userId })-[:OWNED]->(socialMedia: SocialMedia { id: $socialMediaId }) - SET socialMedia.url = $socialMediaUrl - RETURN owner`, - { - userId: context.user.id, - socialMediaId: params.id, - socialMediaUrl: params.url, - }, - ) - session.close() + const socialMedia = await instance.find('SocialMedia', params.id) + await socialMedia.update({ url: params.url }) + const response = await socialMedia.toJson() - return params + 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 af17dbb43..1bbcb8d5b 100644 --- a/backend/src/schema/resolvers/socialMedia.spec.js +++ b/backend/src/schema/resolvers/socialMedia.spec.js @@ -1,56 +1,59 @@ -import { GraphQLClient } from 'graphql-request' +import { createTestClient } from 'apollo-server-testing' +import createServer from '../../server' import Factory from '../../seed/factories' -import { host, login, gql } from '../../jest/helpers' +import { gql } from '../../jest/helpers' +import { neode, getDriver } from '../../bootstrap/neo4j' +const driver = getDriver() const factory = Factory() +const instance = neode() describe('SocialMedia', () => { - let client, headers, variables, mutation + let socialMediaAction, someUser, ownerNode, owner const ownerParams = { - email: 'owner@example.com', + email: 'pippi@example.com', password: '1234', - id: '1234', name: 'Pippi Langstrumpf', } const userParams = { - email: 'someuser@example.com', + email: 'kalle@example.com', password: 'abcd', - id: 'abcd', name: 'Kalle Blomqvist', } const url = 'https://twitter.com/pippi-langstrumpf' const newUrl = 'https://twitter.com/bullerby' - const createSocialMediaMutation = gql` - mutation($url: String!) { - CreateSocialMedia(url: $url) { - id - url - } - } - ` - const updateSocialMediaMutation = gql` - mutation($id: ID!, $url: String!) { - UpdateSocialMedia(id: $id, url: $url) { - id - url - } - } - ` - const deleteSocialMediaMutation = gql` - mutation($id: ID!) { - DeleteSocialMedia(id: $id) { - id - url - } - } - ` + const setUpSocialMedia = async () => { + const socialMediaNode = await instance.create('SocialMedia', { url }) + await socialMediaNode.relateTo(ownerNode, 'ownedBy') + return socialMediaNode.toJson() + } + beforeEach(async () => { - await factory.create('User', userParams) - await factory.create('User', ownerParams) + const someUserNode = await instance.create('User', userParams) + someUser = await someUserNode.toJson() + ownerNode = await instance.create('User', ownerParams) + owner = await ownerNode.toJson() + + socialMediaAction = async (user, mutation, variables) => { + const { server } = createServer({ + context: () => { + return { + user, + driver, + } + }, + }) + const { mutate } = createTestClient(server) + + return mutate({ + mutation, + variables, + }) + } }) afterEach(async () => { @@ -58,129 +61,213 @@ describe('SocialMedia', () => { }) describe('create social media', () => { + let mutation, variables + beforeEach(() => { + mutation = gql` + mutation($url: String!) { + CreateSocialMedia(url: $url) { + id + url + } + } + ` variables = { url } - mutation = createSocialMediaMutation }) describe('unauthenticated', () => { it('throws authorization error', async () => { - client = new GraphQLClient(host) - await expect(client.request(mutation, variables)).rejects.toThrow('Not Authorised') + const user = null + const result = await socialMediaAction(user, mutation, variables) + + expect(result.errors[0]).toHaveProperty('message', 'Not Authorised!') }) }) describe('authenticated', () => { - beforeEach(async () => { - headers = await login(userParams) - client = new GraphQLClient(host, { headers }) + let user + + beforeEach(() => { + user = owner }) - it('creates social media with correct URL', async () => { - await expect(client.request(mutation, variables)).resolves.toEqual( + it('creates social media with the given url', async () => { + await expect(socialMediaAction(user, mutation, variables)).resolves.toEqual( expect.objectContaining({ - CreateSocialMedia: { - id: expect.any(String), - url: url, + data: { + CreateSocialMedia: { + id: expect.any(String), + url, + }, }, }), ) }) - it('rejects empty string', async () => { + it('rejects an empty string as url', async () => { variables = { url: '' } + const result = await socialMediaAction(user, mutation, variables) - await expect(client.request(mutation, variables)).rejects.toThrow( - '"url" is not allowed to be empty', + expect(result.errors[0].message).toEqual( + expect.stringContaining('"url" is not allowed to be empty'), ) }) - it('rejects invalid URLs', async () => { + it('rejects invalid urls', async () => { variables = { url: 'not-a-url' } + const result = await socialMediaAction(user, mutation, variables) - await expect(client.request(createSocialMediaMutation, variables)).rejects.toThrow( - '"url" must be a valid uri', + expect(result.errors[0].message).toEqual( + expect.stringContaining('"url" must be a valid uri'), + ) + }) + }) + + 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', () => { + let mutation, variables + beforeEach(async () => { - headers = await login(ownerParams) - client = new GraphQLClient(host, { headers }) + const socialMedia = await setUpSocialMedia() - const { CreateSocialMedia } = await client.request(createSocialMediaMutation, { url }) - const { id } = CreateSocialMedia - - variables = { url: newUrl, id } - mutation = updateSocialMediaMutation + mutation = gql` + mutation($id: ID!, $url: String!) { + UpdateSocialMedia(id: $id, url: $url) { + id + url + } + } + ` + variables = { url: newUrl, id: socialMedia.id } }) describe('unauthenticated', () => { it('throws authorization error', async () => { - client = new GraphQLClient(host) - await expect(client.request(mutation, variables)).rejects.toThrow('Not Authorised') + const user = null + const result = await socialMediaAction(user, mutation, variables) + + expect(result.errors[0]).toHaveProperty('message', 'Not Authorised!') }) }) describe('authenticated as other user', () => { - // TODO: make sure it throws an authorization error + it('throws authorization error', async () => { + const user = someUser + const result = await socialMediaAction(user, mutation, variables) + + expect(result.errors[0]).toHaveProperty('message', 'Not Authorised!') + }) }) describe('authenticated as owner', () => { - it('updates social media', async () => { - const expected = { UpdateSocialMedia: { ...variables } } + let user - await expect(client.request(mutation, variables)).resolves.toEqual( + beforeEach(() => { + user = owner + }) + + it('updates social media with the given id', async () => { + const expected = { + data: { + UpdateSocialMedia: { ...variables }, + }, + } + + await expect(socialMediaAction(user, mutation, variables)).resolves.toEqual( expect.objectContaining(expected), ) }) - describe('given a non-existent id', () => { - // TODO: make sure it throws an error + it('does not update if the the given id does not exist', async () => { + variables.id = 'some-id' + const result = await socialMediaAction(user, mutation, variables) + + expect(result.errors[0]).toHaveProperty('message', 'Not Authorised!') }) }) }) describe('delete social media', () => { + let mutation, variables + beforeEach(async () => { - headers = await login(ownerParams) - client = new GraphQLClient(host, { headers }) + const socialMedia = await setUpSocialMedia() - const { CreateSocialMedia } = await client.request(createSocialMediaMutation, { url }) - const { id } = CreateSocialMedia - - variables = { id } - mutation = deleteSocialMediaMutation + mutation = gql` + mutation($id: ID!) { + DeleteSocialMedia(id: $id) { + id + url + } + } + ` + variables = { url: newUrl, id: socialMedia.id } }) describe('unauthenticated', () => { it('throws authorization error', async () => { - client = new GraphQLClient(host) + const user = null + const result = await socialMediaAction(user, mutation, variables) - await expect(client.request(mutation, variables)).rejects.toThrow('Not Authorised') + expect(result.errors[0]).toHaveProperty('message', 'Not Authorised!') }) }) describe('authenticated as other user', () => { - // TODO: make sure it throws an authorization error + it('throws authorization error', async () => { + const user = someUser + const result = await socialMediaAction(user, mutation, variables) + + expect(result.errors[0]).toHaveProperty('message', 'Not Authorised!') + }) }) describe('authenticated as owner', () => { + let user + beforeEach(async () => { - headers = await login(ownerParams) - client = new GraphQLClient(host, { headers }) + user = owner }) - it('deletes social media', async () => { + it('deletes social media with the given id', async () => { const expected = { - DeleteSocialMedia: { - id: variables.id, - url: url, + data: { + DeleteSocialMedia: { + id: variables.id, + url, + }, }, } - await expect(client.request(mutation, variables)).resolves.toEqual(expected) + + await expect(socialMediaAction(user, mutation, variables)).resolves.toEqual( + expect.objectContaining(expected), + ) }) }) }) diff --git a/backend/src/schema/resolvers/users.js b/backend/src/schema/resolvers/users.js index 610f84ae1..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]->(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 492dd3966..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", 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 +} diff --git a/backend/src/schema/types/type/User.gql b/backend/src/schema/types/type/User.gql index 81bf3e782..2534463d1 100644 --- a/backend/src/schema/types/type/User.gql +++ b/backend/src/schema/types/type/User.gql @@ -17,7 +17,7 @@ type User { location: Location @cypher(statement: "MATCH (this)-[:IS_IN]->(l:Location) RETURN l") locationName: String about: String - socialMedia: [SocialMedia]! @relation(name: "OWNED", direction: "OUT") + socialMedia: [SocialMedia]! @relation(name: "OWNED_BY", direction: "OUT") #createdAt: DateTime #updatedAt: DateTime