Implement+test SocialMedia.ownedBy resolver

Explicitly define `SocialMedia` graphql type and exclude it from
`neo4j-graphql-js`.
This commit is contained in:
Robert Schäfer 2019-07-31 18:07:48 +02:00
parent 448706ab18
commit c65cc3e0e5
8 changed files with 168 additions and 98 deletions

View File

@ -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
},

View File

@ -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',

View File

@ -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
}

View File

@ -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)',
},
}),
}

View File

@ -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', () => {

View File

@ -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)',
},
}),
},
}

View File

@ -131,8 +131,3 @@ type SharedInboxEndpoint {
uri: String
}
type SocialMedia {
id: ID!
url: String
ownedBy: [User]! @relation(name: "OWNED_BY", direction: "IN")
}

View File

@ -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
}