diff --git a/backend/src/graphql/types/type/Location.gql b/backend/src/graphql/types/type/Location.gql index 9cb5c970a..d9c0ec1cc 100644 --- a/backend/src/graphql/types/type/Location.gql +++ b/backend/src/graphql/types/type/Location.gql @@ -14,6 +14,7 @@ type Location { lat: Float lng: Float parent: Location @cypher(statement: "MATCH (this)-[:IS_IN]->(l:Location) RETURN l") + distanceToMe: Int } # This is not smart - we need one location for everything - use the same type everywhere! diff --git a/backend/src/graphql/types/type/User.gql b/backend/src/graphql/types/type/User.gql index c2f067f12..81dd9cf5b 100644 --- a/backend/src/graphql/types/type/User.gql +++ b/backend/src/graphql/types/type/User.gql @@ -50,7 +50,6 @@ type User { locationName: String location: Location @cypher(statement: "MATCH (this)-[:IS_IN]->(l:Location) RETURN l") - distanceToMe: Int about: String socialMedia: [SocialMedia]! @relation(name: "OWNED_BY", direction: "IN") diff --git a/backend/src/middleware/permissionsMiddleware.ts b/backend/src/middleware/permissionsMiddleware.ts index 3897a61e9..7eaed8a84 100644 --- a/backend/src/middleware/permissionsMiddleware.ts +++ b/backend/src/middleware/permissionsMiddleware.ts @@ -483,6 +483,9 @@ export default shield( email: or(isMyOwn, isAdmin), emailNotificationSettings: isMyOwn, }, + Location: { + distanceToMe: isAuthenticated, + }, Report: isModerator, }, { diff --git a/backend/src/schema/resolvers/locations.spec.ts b/backend/src/schema/resolvers/locations.spec.ts index aed85da54..8b3c5b779 100644 --- a/backend/src/schema/resolvers/locations.spec.ts +++ b/backend/src/schema/resolvers/locations.spec.ts @@ -8,7 +8,7 @@ import Factory, { cleanDatabase } from '@db/factories' import { getNeode, getDriver } from '@db/neo4j' import createServer from '@src/server' -let mutate, authenticatedUser +let query, mutate, authenticatedUser const driver = getDriver() const neode = getNeode() @@ -25,6 +25,7 @@ beforeAll(async () => { } }, }) + query = createTestClient(server).query mutate = createTestClient(server).mutate }) @@ -93,3 +94,209 @@ describe('resolvers', () => { }) }) }) + +const distanceToMeQuery = gql` + query ($id: ID!) { + User(id: $id) { + location { + distanceToMe + } + } + } +` +let user, myPlaceUser, otherPlaceUser, noCordsPlaceUser, noPlaceUser + +describe('distanceToMe', () => { + beforeEach(async () => { + const Hamburg = await Factory.build('location', { + id: 'region.5127278006398860', + name: 'Hamburg', + type: 'region', + lng: 10.0, + lat: 53.55, + nameES: 'Hamburgo', + nameFR: 'Hambourg', + nameIT: 'Amburgo', + nameEN: 'Hamburg', + namePT: 'Hamburgo', + nameDE: 'Hamburg', + nameNL: 'Hamburg', + namePL: 'Hamburg', + nameRU: 'Гамбург', + }) + const Germany = await Factory.build('location', { + id: 'country.10743216036480410', + name: 'Germany', + type: 'country', + namePT: 'Alemanha', + nameDE: 'Deutschland', + nameES: 'Alemania', + nameNL: 'Duitsland', + namePL: 'Niemcy', + nameFR: 'Allemagne', + nameIT: 'Germania', + nameEN: 'Germany', + nameRU: 'Германия', + }) + const Paris = await Factory.build('location', { + id: 'region.9397217726497330', + name: 'Paris', + type: 'region', + lng: 2.35183, + lat: 48.85658, + nameES: 'París', + nameFR: 'Paris', + nameIT: 'Parigi', + nameEN: 'Paris', + namePT: 'Paris', + nameDE: 'Paris', + nameNL: 'Parijs', + namePL: 'Paryż', + nameRU: 'Париж', + }) + + user = await Factory.build('user', { + id: 'user', + role: 'user', + }) + await user.relateTo(Hamburg, 'isIn') + + myPlaceUser = await Factory.build('user', { + id: 'myPlaceUser', + role: 'user', + }) + await myPlaceUser.relateTo(Hamburg, 'isIn') + + otherPlaceUser = await Factory.build('user', { + id: 'otherPlaceUser', + role: 'user', + }) + await otherPlaceUser.relateTo(Paris, 'isIn') + + noCordsPlaceUser = await Factory.build('user', { + id: 'noCordsPlaceUser', + role: 'user', + }) + await noCordsPlaceUser.relateTo(Germany, 'isIn') + + noPlaceUser = await Factory.build('user', { + id: 'noPlaceUser', + role: 'user', + }) + }) + + describe('query the field', () => { + describe('for self user', () => { + it('returns 0', async () => { + authenticatedUser = await user.toJson() + const targetUser = await user.toJson() + await expect( + query({ query: distanceToMeQuery, variables: { id: targetUser.id } }), + ).resolves.toEqual( + expect.objectContaining({ + data: { + User: [ + { + location: { + distanceToMe: 0, + }, + }, + ], + }, + errors: undefined, + }), + ) + }) + }) + + describe('for myPlaceUser', () => { + it('returns 0', async () => { + authenticatedUser = await user.toJson() + const targetUser = await myPlaceUser.toJson() + await expect( + query({ query: distanceToMeQuery, variables: { id: targetUser.id } }), + ).resolves.toEqual( + expect.objectContaining({ + data: { + User: [ + { + location: { + distanceToMe: 0, + }, + }, + ], + }, + errors: undefined, + }), + ) + }) + }) + + describe('for otherPlaceUser', () => { + it('returns a number', async () => { + authenticatedUser = await user.toJson() + const targetUser = await otherPlaceUser.toJson() + await expect( + query({ query: distanceToMeQuery, variables: { id: targetUser.id } }), + ).resolves.toEqual( + expect.objectContaining({ + data: { + User: [ + { + location: { + distanceToMe: 746, + }, + }, + ], + }, + errors: undefined, + }), + ) + }) + }) + + describe('for noCordsPlaceUser', () => { + it('returns null', async () => { + authenticatedUser = await user.toJson() + const targetUser = await noCordsPlaceUser.toJson() + await expect( + query({ query: distanceToMeQuery, variables: { id: targetUser.id } }), + ).resolves.toEqual( + expect.objectContaining({ + data: { + User: [ + { + location: { + distanceToMe: null, + }, + }, + ], + }, + errors: undefined, + }), + ) + }) + }) + + describe('for noPlaceUser', () => { + it('returns null location', async () => { + authenticatedUser = await user.toJson() + const targetUser = await noPlaceUser.toJson() + await expect( + query({ query: distanceToMeQuery, variables: { id: targetUser.id } }), + ).resolves.toEqual( + expect.objectContaining({ + data: { + User: [ + { + location: null, + }, + ], + }, + errors: undefined, + }), + ) + }) + }) + }) +}) diff --git a/backend/src/schema/resolvers/locations.ts b/backend/src/schema/resolvers/locations.ts index bcefa2337..f375f287f 100644 --- a/backend/src/schema/resolvers/locations.ts +++ b/backend/src/schema/resolvers/locations.ts @@ -1,3 +1,6 @@ +/* eslint-disable @typescript-eslint/no-unsafe-return */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ /* eslint-disable @typescript-eslint/no-unsafe-member-access */ /* eslint-disable @typescript-eslint/no-unsafe-argument */ import { UserInputError } from 'apollo-server' @@ -20,6 +23,31 @@ export default { 'nameRU', ], }), + distanceToMe: async (parent, _params, context, _resolveInfo) => { + const session = context.driver.session() + + const query = session.readTransaction(async (transaction) => { + const result = await transaction.run( + ` + MATCH (loc:Location {id: $parent.id}) + MATCH (me:User {id: $user.id})-[:IS_IN]->(meLoc:Location) + WITH + point({latitude: loc.lat, longitude: loc.lng}) as locPoint, + point({latitude: meLoc.lat, longitude: meLoc.lng}) as mePoint + RETURN round(point.distance(locPoint, mePoint) / 1000) as distance + `, + { parent, user: context.user }, + ) + + return result.records.map((record) => record.get('distance'))[0] + }) + + try { + return await query + } finally { + await session.close() + } + }, }, Query: { queryLocations: async (_object, args, _context, _resolveInfo) => { diff --git a/backend/src/schema/resolvers/users.spec.ts b/backend/src/schema/resolvers/users.spec.ts index d666e5fc4..ad37e2024 100644 --- a/backend/src/schema/resolvers/users.spec.ts +++ b/backend/src/schema/resolvers/users.spec.ts @@ -660,202 +660,6 @@ const emailNotificationSettingsMutation = gql` } ` -const distanceToMeQuery = gql` - query ($id: ID!) { - User(id: $id) { - distanceToMe - } - } -` -let myPlaceUser, otherPlaceUser, noCordsPlaceUser, noPlaceUser - -describe('distanceToMe', () => { - beforeEach(async () => { - const Hamburg = await Factory.build('location', { - id: 'region.5127278006398860', - name: 'Hamburg', - type: 'region', - lng: 10.0, - lat: 53.55, - nameES: 'Hamburgo', - nameFR: 'Hambourg', - nameIT: 'Amburgo', - nameEN: 'Hamburg', - namePT: 'Hamburgo', - nameDE: 'Hamburg', - nameNL: 'Hamburg', - namePL: 'Hamburg', - nameRU: 'Гамбург', - }) - const Germany = await Factory.build('location', { - id: 'country.10743216036480410', - name: 'Germany', - type: 'country', - namePT: 'Alemanha', - nameDE: 'Deutschland', - nameES: 'Alemania', - nameNL: 'Duitsland', - namePL: 'Niemcy', - nameFR: 'Allemagne', - nameIT: 'Germania', - nameEN: 'Germany', - nameRU: 'Германия', - }) - const Paris = await Factory.build('location', { - id: 'region.9397217726497330', - name: 'Paris', - type: 'region', - lng: 2.35183, - lat: 48.85658, - nameES: 'París', - nameFR: 'Paris', - nameIT: 'Parigi', - nameEN: 'Paris', - namePT: 'Paris', - nameDE: 'Paris', - nameNL: 'Parijs', - namePL: 'Paryż', - nameRU: 'Париж', - }) - - user = await Factory.build('user', { - id: 'user', - role: 'user', - }) - await user.relateTo(Hamburg, 'isIn') - - myPlaceUser = await Factory.build('user', { - id: 'myPlaceUser', - role: 'user', - }) - await myPlaceUser.relateTo(Hamburg, 'isIn') - - otherPlaceUser = await Factory.build('user', { - id: 'otherPlaceUser', - role: 'user', - }) - await otherPlaceUser.relateTo(Paris, 'isIn') - - noCordsPlaceUser = await Factory.build('user', { - id: 'noCordsPlaceUser', - role: 'user', - }) - await noCordsPlaceUser.relateTo(Germany, 'isIn') - - noPlaceUser = await Factory.build('user', { - id: 'noPlaceUser', - role: 'user', - }) - }) - - describe('query the field', () => { - describe('for self user', () => { - it('returns null', async () => { - authenticatedUser = await user.toJson() - const targetUser = await user.toJson() - await expect( - query({ query: distanceToMeQuery, variables: { id: targetUser.id } }), - ).resolves.toEqual( - expect.objectContaining({ - data: { - User: [ - { - distanceToMe: null, - }, - ], - }, - errors: undefined, - }), - ) - }) - }) - - describe('for myPlaceUser', () => { - it('returns 0', async () => { - authenticatedUser = await user.toJson() - const targetUser = await myPlaceUser.toJson() - await expect( - query({ query: distanceToMeQuery, variables: { id: targetUser.id } }), - ).resolves.toEqual( - expect.objectContaining({ - data: { - User: [ - { - distanceToMe: 0, - }, - ], - }, - errors: undefined, - }), - ) - }) - }) - - describe('for otherPlaceUser', () => { - it('returns a number', async () => { - authenticatedUser = await user.toJson() - const targetUser = await otherPlaceUser.toJson() - await expect( - query({ query: distanceToMeQuery, variables: { id: targetUser.id } }), - ).resolves.toEqual( - expect.objectContaining({ - data: { - User: [ - { - distanceToMe: 746, - }, - ], - }, - errors: undefined, - }), - ) - }) - }) - - describe('for noCordsPlaceUser', () => { - it('returns null', async () => { - authenticatedUser = await user.toJson() - const targetUser = await noCordsPlaceUser.toJson() - await expect( - query({ query: distanceToMeQuery, variables: { id: targetUser.id } }), - ).resolves.toEqual( - expect.objectContaining({ - data: { - User: [ - { - distanceToMe: null, - }, - ], - }, - errors: undefined, - }), - ) - }) - }) - - describe('for noPlaceUser', () => { - it('returns null', async () => { - authenticatedUser = await user.toJson() - const targetUser = await noPlaceUser.toJson() - await expect( - query({ query: distanceToMeQuery, variables: { id: targetUser.id } }), - ).resolves.toEqual( - expect.objectContaining({ - data: { - User: [ - { - distanceToMe: null, - }, - ], - }, - errors: undefined, - }), - ) - }) - }) - }) -}) - describe('emailNotificationSettings', () => { beforeEach(async () => { user = await Factory.build('user', { diff --git a/backend/src/schema/resolvers/users.ts b/backend/src/schema/resolvers/users.ts index 7c906d9a9..f549e79a3 100644 --- a/backend/src/schema/resolvers/users.ts +++ b/backend/src/schema/resolvers/users.ts @@ -467,36 +467,6 @@ export default { }, }, User: { - distanceToMe: async (parent, _params, context, _resolveInfo) => { - // is it myself? - if (parent.id === context.user.id) { - return null - } - - const session = context.driver.session() - - const query = session.readTransaction(async (transaction) => { - const result = await transaction.run( - ` - MATCH (user:User {id: $parent.id})-[:IS_IN]->(userLoc:Location) - MATCH (me:User {id: $user.id})-[:IS_IN]->(meLoc:Location) - WITH - point({latitude: userLoc.lat, longitude: userLoc.lng}) as userPoint, - point({latitude: meLoc.lat, longitude: meLoc.lng}) as mePoint - RETURN round(point.distance(userPoint, mePoint) / 1000) as distance - `, - { parent, user: context.user }, - ) - - return result.records.map((record) => record.get('distance'))[0] - }) - - try { - return await query - } finally { - session.close() - } - }, emailNotificationSettings: async (parent, _params, _context, _resolveInfo) => { return [ {