Refactor to save location in db by 'UpdateUser', 'CreateGroup', and 'UpdateGroup' resolvers

- Refactor, add, and refine tests of location for 'CreateGroup' and 'UpdateGroup' resolvers.
This commit is contained in:
Wolfgang Huß 2022-09-17 20:55:43 +02:00
parent 728eac98ac
commit 0048486a78
10 changed files with 316 additions and 113 deletions

View File

@ -12,7 +12,7 @@ export const createGroupMutation = gql`
$groupType: GroupType! $groupType: GroupType!
$actionRadius: GroupActionRadius! $actionRadius: GroupActionRadius!
$categoryIds: [ID] $categoryIds: [ID]
$locationName: String $locationName: String # empty string '' sets it to null
) { ) {
CreateGroup( CreateGroup(
id: $id id: $id
@ -63,7 +63,7 @@ export const updateGroupMutation = gql`
$actionRadius: GroupActionRadius $actionRadius: GroupActionRadius
$categoryIds: [ID] $categoryIds: [ID]
$avatar: ImageInput $avatar: ImageInput
$locationName: String $locationName: String # empty string '' sets it to null
) { ) {
UpdateGroup( UpdateGroup(
id: $id id: $id

View File

@ -149,9 +149,11 @@ export default {
}, },
UpdateGroup: async (_parent, params, context, _resolveInfo) => { UpdateGroup: async (_parent, params, context, _resolveInfo) => {
const { categoryIds } = params const { categoryIds } = params
const { id: groupId, avatar: avatarInput } = params
delete params.categoryIds delete params.categoryIds
const { id: groupId, avatar: avatarInput } = params
delete params.avatar delete params.avatar
params.locationName = params.locationName === '' ? null : params.locationName
if (CONFIG.CATEGORIES_ACTIVE && categoryIds) { if (CONFIG.CATEGORIES_ACTIVE && categoryIds) {
if (categoryIds.length < CATEGORIES_MIN) { if (categoryIds.length < CATEGORIES_MIN) {
throw new UserInputError('Too view categories!') throw new UserInputError('Too view categories!')
@ -210,7 +212,10 @@ export default {
}) })
try { try {
const group = await writeTxResultPromise const group = await writeTxResultPromise
await createOrUpdateLocations('Group', params.id, params.locationName, session) // TODO: put in a middleware, see "UpdateUser"
if (params.locationName !== undefined) {
await createOrUpdateLocations('Group', params.id, params.locationName, session)
}
return group return group
} catch (error) { } catch (error) {
if (error.code === 'Neo.ClientError.Schema.ConstraintValidationFailed') if (error.code === 'Neo.ClientError.Schema.ConstraintValidationFailed')

View File

@ -553,7 +553,7 @@ describe('in mode', () => {
describe('query groups', () => { describe('query groups', () => {
describe('in general finds only listed groups no hidden groups where user is none or pending member', () => { describe('in general finds only listed groups no hidden groups where user is none or pending member', () => {
describe('without any filters', () => { describe('without any filters', () => {
it('finds all listed groups', async () => { it('finds all listed groups including the set locations', async () => {
const result = await query({ query: groupQuery, variables: {} }) const result = await query({ query: groupQuery, variables: {} })
expect(result).toMatchObject({ expect(result).toMatchObject({
data: { data: {
@ -572,12 +572,16 @@ describe('in mode', () => {
expect.objectContaining({ expect.objectContaining({
id: 'others-group', id: 'others-group',
slug: 'uninteresting-group', slug: 'uninteresting-group',
locationName: null,
location: null,
myRole: null, myRole: null,
}), }),
expect.objectContaining({ expect.objectContaining({
id: 'third-hidden-group', id: 'third-hidden-group',
slug: 'third-investigative-journalism-group', slug: 'third-investigative-journalism-group',
myRole: 'usual', myRole: 'usual',
locationName: null,
location: null,
}), }),
]), ]),
}, },
@ -2617,21 +2621,32 @@ describe('in mode', () => {
}) })
describe('authenticated', () => { describe('authenticated', () => {
let otherUser let noMemberUser
beforeAll(async () => { beforeAll(async () => {
otherUser = await Factory.build( noMemberUser = await Factory.build(
'user', 'user',
{ {
id: 'other-user', id: 'none-member-user',
name: 'Other TestUser', name: 'None Member TestUser',
}, },
{ {
email: 'test2@example.org', email: 'none-member-user@example.org',
password: '1234', password: '1234',
}, },
) )
authenticatedUser = await otherUser.toJson() usualMemberUser = await Factory.build(
'user',
{
id: 'usual-member-user',
name: 'Usual Member TestUser',
},
{
email: 'usual-member-user@example.org',
password: '1234',
},
)
authenticatedUser = await noMemberUser.toJson()
await mutate({ await mutate({
mutation: createGroupMutation, mutation: createGroupMutation,
variables: { variables: {
@ -2655,7 +2670,15 @@ describe('in mode', () => {
groupType: 'public', groupType: 'public',
actionRadius: 'regional', actionRadius: 'regional',
categoryIds, categoryIds,
locationName: 'Hamburg, Germany', locationName: 'Berlin, Germany',
},
})
await mutate({
mutation: changeGroupMemberRoleMutation,
variables: {
groupId: 'my-group',
userId: 'usual-member-user',
roleInGroup: 'usual',
}, },
}) })
}) })
@ -2666,40 +2689,190 @@ describe('in mode', () => {
authenticatedUser = await user.toJson() authenticatedUser = await user.toJson()
}) })
it('has set the settings', async () => { describe('all standard settings excluding location', () => {
await expect( it('has updated the settings', async () => {
mutate({ await expect(
mutation: updateGroupMutation, mutate({
variables: { mutation: updateGroupMutation,
id: 'my-group', variables: {
name: 'The New Group For Our Country', id: 'my-group',
about: 'We will change the land!', name: 'The New Group For Our Country',
description: 'Some country relevant description' + descriptionAdditional100, about: 'We will change the land!',
actionRadius: 'national', description: 'Some country relevant description' + descriptionAdditional100,
// avatar, // test this as result actionRadius: 'national',
locationName: 'Berlin, Germany', // avatar, // test this as result
},
}),
).resolves.toMatchObject({
data: {
UpdateGroup: {
id: 'my-group',
name: 'The New Group For Our Country',
slug: 'the-new-group-for-our-country', // changing the slug is tested in the slugifyMiddleware
about: 'We will change the land!',
description: 'Some country relevant description' + descriptionAdditional100,
actionRadius: 'national',
// avatar, // test this as result
myRole: 'owner',
},
}, },
}), errors: undefined,
).resolves.toMatchObject({ })
data: { })
UpdateGroup: { })
id: 'my-group',
name: 'The New Group For Our Country', describe('location', () => {
slug: 'the-new-group-for-our-country', // changing the slug is tested in the slugifyMiddleware describe('"locationName" is undefined shall not change location', () => {
about: 'We will change the land!', it('has left locaton unchanged as "Berlin"', async () => {
description: 'Some country relevant description' + descriptionAdditional100, await expect(
actionRadius: 'national', mutate({
// avatar, // test this as result mutation: updateGroupMutation,
locationName: 'Berlin, Germany', variables: {
location: expect.objectContaining({ id: 'my-group',
name: 'Berlin', },
nameDE: 'Berlin',
nameEN: 'Berlin',
}), }),
myRole: 'owner', ).resolves.toMatchObject({
}, data: {
}, UpdateGroup: {
errors: undefined, id: 'my-group',
locationName: 'Berlin, Germany',
location: expect.objectContaining({
name: 'Berlin',
nameDE: 'Berlin',
nameEN: 'Berlin',
}),
myRole: 'owner',
},
},
errors: undefined,
})
})
})
describe('"locationName" is null shall change location "Berlin" to unset location', () => {
it('has updated the location to unset location', async () => {
await expect(
mutate({
mutation: updateGroupMutation,
variables: {
id: 'my-group',
// avatar, // test this as result
locationName: null,
},
}),
).resolves.toMatchObject({
data: {
UpdateGroup: {
id: 'my-group',
locationName: null,
location: null,
myRole: 'owner',
},
},
errors: undefined,
})
})
})
describe('change unset location to "Paris"', () => {
it('has updated the location to "Paris"', async () => {
await expect(
mutate({
mutation: updateGroupMutation,
variables: {
id: 'my-group',
// avatar, // test this as result
locationName: 'Paris, France',
},
}),
).resolves.toMatchObject({
data: {
UpdateGroup: {
id: 'my-group',
name: 'The New Group For Our Country',
slug: 'the-new-group-for-our-country', // changing the slug is tested in the slugifyMiddleware
about: 'We will change the land!',
description: 'Some country relevant description' + descriptionAdditional100,
actionRadius: 'national',
// avatar, // test this as result
locationName: 'Paris, France',
location: expect.objectContaining({
name: 'Paris',
nameDE: 'Paris',
nameEN: 'Paris',
}),
myRole: 'owner',
},
},
errors: undefined,
})
})
})
describe('change location "Paris" to "Hamburg"', () => {
it('has updated the location to "Hamburg"', async () => {
await expect(
mutate({
mutation: updateGroupMutation,
variables: {
id: 'my-group',
// avatar, // test this as result
locationName: 'Hamburg, Germany',
},
}),
).resolves.toMatchObject({
data: {
UpdateGroup: {
id: 'my-group',
name: 'The New Group For Our Country',
slug: 'the-new-group-for-our-country', // changing the slug is tested in the slugifyMiddleware
about: 'We will change the land!',
description: 'Some country relevant description' + descriptionAdditional100,
actionRadius: 'national',
// avatar, // test this as result
locationName: 'Hamburg, Germany',
location: expect.objectContaining({
name: 'Hamburg',
nameDE: 'Hamburg',
nameEN: 'Hamburg',
}),
myRole: 'owner',
},
},
errors: undefined,
})
})
})
describe('"locationName" is empty string shall change location "Hamburg" to unset location ', () => {
it('has updated the location to unset', async () => {
await expect(
mutate({
mutation: updateGroupMutation,
variables: {
id: 'my-group',
// avatar, // test this as result
locationName: '', // empty string '' sets it to null
},
}),
).resolves.toMatchObject({
data: {
UpdateGroup: {
id: 'my-group',
name: 'The New Group For Our Country',
slug: 'the-new-group-for-our-country', // changing the slug is tested in the slugifyMiddleware
about: 'We will change the land!',
description: 'Some country relevant description' + descriptionAdditional100,
actionRadius: 'national',
// avatar, // test this as result
locationName: null,
location: null,
myRole: 'owner',
},
},
errors: undefined,
})
})
}) })
}) })
@ -2783,9 +2956,9 @@ describe('in mode', () => {
}) })
}) })
describe('as no(!) owner', () => { describe('as "usual-member-user" member, no(!) owner', () => {
it('throws authorization error', async () => { it('throws authorization error', async () => {
authenticatedUser = await otherUser.toJson() authenticatedUser = await usualMemberUser.toJson()
const { errors } = await mutate({ const { errors } = await mutate({
mutation: updateGroupMutation, mutation: updateGroupMutation,
variables: { variables: {
@ -2794,9 +2967,25 @@ describe('in mode', () => {
about: 'We will change the land!', about: 'We will change the land!',
description: 'Some country relevant description' + descriptionAdditional100, description: 'Some country relevant description' + descriptionAdditional100,
actionRadius: 'national', actionRadius: 'national',
categoryIds: ['cat4', 'cat27'], // test this as result categoryIds: ['cat4', 'cat27'],
// avatar, // test this as result },
// locationName, // test this as result })
expect(errors[0]).toHaveProperty('message', 'Not Authorized!')
})
})
describe('as "none-member-user"', () => {
it('throws authorization error', async () => {
authenticatedUser = await noMemberUser.toJson()
const { errors } = await mutate({
mutation: updateGroupMutation,
variables: {
id: 'my-group',
name: 'The New Group For Our Country',
about: 'We will change the land!',
description: 'Some country relevant description' + descriptionAdditional100,
actionRadius: 'national',
categoryIds: ['cat4', 'cat27'],
}, },
}) })
expect(errors[0]).toHaveProperty('message', 'Not Authorized!') expect(errors[0]).toHaveProperty('message', 'Not Authorized!')

View File

@ -139,9 +139,10 @@ export default {
return blockedUser.toJson() return blockedUser.toJson()
}, },
UpdateUser: async (_parent, params, context, _resolveInfo) => { UpdateUser: async (_parent, params, context, _resolveInfo) => {
const { termsAndConditionsAgreedVersion } = params
const { avatar: avatarInput } = params const { avatar: avatarInput } = params
delete params.avatar delete params.avatar
params.locationName = params.locationName === '' ? null : params.locationName
const { termsAndConditionsAgreedVersion } = params
if (termsAndConditionsAgreedVersion) { if (termsAndConditionsAgreedVersion) {
const regEx = new RegExp(/^[0-9]+\.[0-9]+\.[0-9]+$/g) const regEx = new RegExp(/^[0-9]+\.[0-9]+\.[0-9]+$/g)
if (!regEx.test(termsAndConditionsAgreedVersion)) { if (!regEx.test(termsAndConditionsAgreedVersion)) {
@ -169,7 +170,10 @@ export default {
}) })
try { try {
const user = await writeTxResultPromise const user = await writeTxResultPromise
await createOrUpdateLocations('User', params.id, params.locationName, session) // TODO: put in a middleware, see "CreateGroup, UpdateGroup"
if (params.locationName !== undefined) {
await createOrUpdateLocations('User', params.id, params.locationName, session)
}
return user return user
} catch (error) { } catch (error) {
throw new UserInputError(error.message) throw new UserInputError(error.message)

View File

@ -161,7 +161,7 @@ describe('UpdateUser', () => {
$id: ID! $id: ID!
$name: String $name: String
$termsAndConditionsAgreedVersion: String $termsAndConditionsAgreedVersion: String
$locationName: String $locationName: String # empty string '' sets it to null
) { ) {
UpdateUser( UpdateUser(
id: $id id: $id

View File

@ -1,6 +1,5 @@
import request from 'request' import request from 'request'
import { UserInputError } from 'apollo-server' import { UserInputError } from 'apollo-server'
import isEmpty from 'lodash/isEmpty'
import Debug from 'debug' import Debug from 'debug'
import asyncForEach from '../../../helpers/asyncForEach' import asyncForEach from '../../../helpers/asyncForEach'
import CONFIG from '../../../config' import CONFIG from '../../../config'
@ -63,64 +62,70 @@ const createLocation = async (session, mapboxData) => {
} }
export const createOrUpdateLocations = async (nodeLabel, nodeId, locationName, session) => { export const createOrUpdateLocations = async (nodeLabel, nodeId, locationName, session) => {
if (isEmpty(locationName)) { let locationId
return
}
const res = await fetch(
`https://api.mapbox.com/geocoding/v5/mapbox.places/${encodeURIComponent(
locationName,
)}.json?access_token=${CONFIG.MAPBOX_TOKEN}&types=region,place,country&language=${locales.join(
',',
)}`,
)
debug(res) if (locationName !== null) {
const res = await fetch(
`https://api.mapbox.com/geocoding/v5/mapbox.places/${encodeURIComponent(
locationName,
)}.json?access_token=${
CONFIG.MAPBOX_TOKEN
}&types=region,place,country&language=${locales.join(',')}`,
)
if (!res || !res.features || !res.features[0]) { debug(res)
throw new UserInputError('locationName is invalid')
}
let data if (!res || !res.features || !res.features[0]) {
throw new UserInputError('locationName is invalid')
res.features.forEach((item) => {
if (item.matching_place_name === locationName) {
data = item
} }
})
if (!data) {
data = res.features[0]
}
if (!data || !data.place_type || !data.place_type.length) { let data
throw new UserInputError('locationName is invalid')
}
if (data.place_type.length > 1) { res.features.forEach((item) => {
data.id = 'region.' + data.id.split('.')[1] if (item.matching_place_name === locationName) {
} data = item
await createLocation(session, data) }
let parent = data
if (data.context) {
await asyncForEach(data.context, async (ctx) => {
await createLocation(session, ctx)
await session.writeTransaction((transaction) => {
return transaction.run(
`
MATCH (parent:Location {id: $parentId}), (child:Location {id: $childId})
MERGE (child)<-[:IS_IN]-(parent)
RETURN child.id, parent.id
`,
{
parentId: parent.id,
childId: ctx.id,
},
)
})
parent = ctx
}) })
if (!data) {
data = res.features[0]
}
if (!data || !data.place_type || !data.place_type.length) {
throw new UserInputError('locationName is invalid')
}
if (data.place_type.length > 1) {
data.id = 'region.' + data.id.split('.')[1]
}
await createLocation(session, data)
let parent = data
if (data.context) {
await asyncForEach(data.context, async (ctx) => {
await createLocation(session, ctx)
await session.writeTransaction((transaction) => {
return transaction.run(
`
MATCH (parent:Location {id: $parentId}), (child:Location {id: $childId})
MERGE (child)<-[:IS_IN]-(parent)
RETURN child.id, parent.id
`,
{
parentId: parent.id,
childId: ctx.id,
},
)
})
parent = ctx
})
}
locationId = data.id
} else {
locationId = 'non-existent-id'
} }
// delete all current locations from node and add new location // delete all current locations from node and add new location
await session.writeTransaction((transaction) => { await session.writeTransaction((transaction) => {
return transaction.run( return transaction.run(
@ -133,7 +138,7 @@ export const createOrUpdateLocations = async (nodeLabel, nodeId, locationName, s
MERGE (node)-[:IS_IN]->(location) MERGE (node)-[:IS_IN]->(location)
RETURN location.id, node.id RETURN location.id, node.id
`, `,
{ nodeId, locationId: data.id }, { nodeId, locationId },
) )
}) })
} }

View File

@ -33,8 +33,8 @@ type Group {
groupType: GroupType! groupType: GroupType!
actionRadius: GroupActionRadius! actionRadius: GroupActionRadius!
location: Location @cypher(statement: "MATCH (this)-[:IS_IN]->(l:Location) RETURN l")
locationName: String locationName: String
location: Location @cypher(statement: "MATCH (this)-[:IS_IN]->(l:Location) RETURN l")
categories: [Category] @relation(name: "CATEGORIZED", direction: "OUT") categories: [Category] @relation(name: "CATEGORIZED", direction: "OUT")
@ -95,7 +95,7 @@ type Mutation {
actionRadius: GroupActionRadius! actionRadius: GroupActionRadius!
categoryIds: [ID] categoryIds: [ID]
# avatar: ImageInput # a group can not be created with an avatar # avatar: ImageInput # a group can not be created with an avatar
locationName: String # test this as result locationName: String # empty string '' sets it to null
): Group ): Group
UpdateGroup( UpdateGroup(
@ -108,7 +108,7 @@ type Mutation {
actionRadius: GroupActionRadius actionRadius: GroupActionRadius
categoryIds: [ID] categoryIds: [ID]
avatar: ImageInput # test this as result avatar: ImageInput # test this as result
locationName: String # test this as result locationName: String # empty string '' sets it to null
): Group ): Group
# DeleteGroup(id: ID!): Group # DeleteGroup(id: ID!): Group

View File

@ -33,8 +33,8 @@ type User {
invitedBy: User @relation(name: "INVITED", direction: "IN") invitedBy: User @relation(name: "INVITED", direction: "IN")
invited: [User] @relation(name: "INVITED", direction: "OUT") invited: [User] @relation(name: "INVITED", direction: "OUT")
location: Location @cypher(statement: "MATCH (this)-[:IS_IN]->(l:Location) RETURN l")
locationName: String locationName: String
location: Location @cypher(statement: "MATCH (this)-[:IS_IN]->(l:Location) RETURN l")
about: String about: String
socialMedia: [SocialMedia]! @relation(name: "OWNED_BY", direction: "IN") socialMedia: [SocialMedia]! @relation(name: "OWNED_BY", direction: "IN")
@ -212,7 +212,7 @@ type Mutation {
email: String email: String
slug: String slug: String
avatar: ImageInput avatar: ImageInput
locationName: String locationName: String # empty string '' sets it to null
about: String about: String
termsAndConditionsAgreedVersion: String termsAndConditionsAgreedVersion: String
termsAndConditionsAgreedAt: String termsAndConditionsAgreedAt: String

View File

@ -221,25 +221,25 @@ export const updateUserMutation = () => {
$id: ID! $id: ID!
$slug: String $slug: String
$name: String $name: String
$locationName: String
$about: String $about: String
$allowEmbedIframes: Boolean $allowEmbedIframes: Boolean
$showShoutsPublicly: Boolean $showShoutsPublicly: Boolean
$sendNotificationEmails: Boolean $sendNotificationEmails: Boolean
$termsAndConditionsAgreedVersion: String $termsAndConditionsAgreedVersion: String
$avatar: ImageInput $avatar: ImageInput
$locationName: String # empty string '' sets it to null
) { ) {
UpdateUser( UpdateUser(
id: $id id: $id
slug: $slug slug: $slug
name: $name name: $name
locationName: $locationName
about: $about about: $about
allowEmbedIframes: $allowEmbedIframes allowEmbedIframes: $allowEmbedIframes
showShoutsPublicly: $showShoutsPublicly showShoutsPublicly: $showShoutsPublicly
sendNotificationEmails: $sendNotificationEmails sendNotificationEmails: $sendNotificationEmails
termsAndConditionsAgreedVersion: $termsAndConditionsAgreedVersion termsAndConditionsAgreedVersion: $termsAndConditionsAgreedVersion
avatar: $avatar avatar: $avatar
locationName: $locationName
) { ) {
id id
slug slug

View File

@ -12,7 +12,7 @@ export const createGroupMutation = gql`
$groupType: GroupType! $groupType: GroupType!
$actionRadius: GroupActionRadius! $actionRadius: GroupActionRadius!
$categoryIds: [ID] $categoryIds: [ID]
$locationName: String $locationName: String # empty string '' sets it to null
) { ) {
CreateGroup( CreateGroup(
id: $id id: $id
@ -58,7 +58,7 @@ export const updateGroupMutation = gql`
$actionRadius: GroupActionRadius $actionRadius: GroupActionRadius
$categoryIds: [ID] $categoryIds: [ID]
$avatar: ImageInput $avatar: ImageInput
$locationName: String $locationName: String # empty string '' sets it to null
) { ) {
UpdateGroup( UpdateGroup(
id: $id id: $id