-
Notifications
You must be signed in to change notification settings - Fork 0
65 create map endpoint to get users or clusters corresponding the current coords zoom #66
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -0,0 +1,27 @@ | ||||||
| import { FastifyRequest, FastifyReply, FastifyRequestUser } from 'fastify'; | ||||||
| import { AppError, UnauthorizedError } from '../../../utils/error'; | ||||||
|
|
||||||
| export const getNearUsersHandler = async ( | ||||||
| request: FastifyRequest, | ||||||
| reply: FastifyReply | ||||||
| ) => { | ||||||
| const params = request.query as { level: string, latitude: string, longitude: string, radius: string }; | ||||||
| const level = Number(params.level); | ||||||
| const latitude = Number(params.latitude); | ||||||
| const longitude = Number(params.longitude); | ||||||
| let radius = Number(params.radius); | ||||||
|
|
||||||
| try { | ||||||
| const user = request.user as FastifyRequestUser | undefined; | ||||||
| const userId = user?.id; | ||||||
| if (!userId) | ||||||
| throw new UnauthorizedError(); | ||||||
| const data = await request.server.mapService.getNearUsers(level, latitude, longitude, radius); | ||||||
|
||||||
| const data = await request.server.mapService.getNearUsers(level, latitude, longitude, radius); | |
| const data = await request.server.mapService.getNearUsers(level, latitude, longitude, radius, userId); |
Copilot
AI
Dec 27, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The console.log statement should be removed before merging to production, or replaced with a proper logging mechanism. Console logs in production can expose sensitive information and impact performance.
| console.log(error); | |
| request.log.error(error); |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -27,6 +27,20 @@ type UserLocation = { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| country: string; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| export type MapUserCluster = { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| latitude: number | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| longitude: number | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| count: number | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| export type MapUser = { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| id: number | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| firstName: string | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| profilePictureUrl: string | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| latitude: number | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| longitude: number | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| type BlockedUser = { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| id: number; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| blockerId: number; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -111,6 +125,36 @@ export default class UserModel { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return result.rows[0]; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| getUsersFromLocation = async (lat: number, lgn: number, radius: number): Promise<MapUser[]> => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const result = await this.fastify.pg.query( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| `SELECT u.id, u.first_name, u.profile_pictures[u.profile_picture_index] AS profile_picture, locations.latitude, locations.longitude | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| FROM locations | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| JOIN users AS u ON locations.user_id = u.id | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| WHERE locations.user_id IS NOT NULL | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| AND 6371 * acos(least(1, greatest(-1, cos(radians(locations.latitude)) * cos(radians($1)) * cos(radians($2) - radians(locations.longitude)) + sin(radians(locations.latitude)) * sin(radians($1))))) < $3`, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| [lat, lgn, radius] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return result.rows.map((row: { id: number, first_name: string, profile_picture_url: string, latitude: number, longitude: number }) => { return { id: row.id, firstName: row.first_name, profilePictureUrl: row.profile_picture_url, latitude: row.latitude, longitude: row.longitude }}); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+130
to
+138
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| getUsersCountByLocation = async (level: number, lat: number, lgn: number, radius: number): Promise<MapUserCluster[]> => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const areaName = (level == 1) ? 'city' : (level == 2) ? 'country' : null; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const areaName = (level == 1) ? 'city' : (level == 2) ? 'country' : null; | |
| const areaName = (level === 1) ? 'city' : (level === 2) ? 'country' : null; |
Copilot
AI
Dec 27, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The SQL query uses string interpolation for the column name in the GROUP BY clause, which could potentially lead to SQL injection if the level parameter is not properly validated. Although validation exists in the service layer, it's better to use a safer approach such as a CASE statement or explicit conditional queries rather than string interpolation.
| const areaName = (level == 1) ? 'city' : (level == 2) ? 'country' : null; | |
| if (!areaName) | |
| return []; | |
| const result = await this.fastify.pg.query( | |
| `SELECT count(${areaName}) AS count, avg(latitude) AS latitude, avg(longitude) AS longitude | |
| FROM locations | |
| WHERE ${areaName} IS NOT NULL | |
| AND user_id IS NOT NULL | |
| AND 6371 * acos(least(1, greatest(-1, cos(radians(latitude)) * cos(radians($1)) * cos(radians($2) - radians(longitude)) + sin(radians(latitude)) * sin(radians($1))))) < $3 | |
| GROUP BY ${areaName}`, | |
| [lat, lgn, radius] | |
| ); | |
| return result.rows.map((row: { count: number, latitude: number, longitude: number }) => { return { count: row.count, latitude: row.latitude, longitude: row.longitude }}); | |
| let query: string | null = null; | |
| if (level === 1) { | |
| query = `SELECT count(city) AS count, avg(latitude) AS latitude, avg(longitude) AS longitude | |
| FROM locations | |
| WHERE city IS NOT NULL | |
| AND user_id IS NOT NULL | |
| AND 6371 * acos(least(1, greatest(-1, cos(radians(latitude)) * cos(radians($1)) * cos(radians($2) - radians(longitude)) + sin(radians(latitude)) * sin(radians($1))))) < $3 | |
| GROUP BY city`; | |
| } else if (level === 2) { | |
| query = `SELECT count(country) AS count, avg(latitude) AS latitude, avg(longitude) AS longitude | |
| FROM locations | |
| WHERE country IS NOT NULL | |
| AND user_id IS NOT NULL | |
| AND 6371 * acos(least(1, greatest(-1, cos(radians(latitude)) * cos(radians($1)) * cos(radians($2) - radians(longitude)) + sin(radians(latitude)) * sin(radians($1))))) < $3 | |
| GROUP BY country`; | |
| } | |
| if (!query) { | |
| return []; | |
| } | |
| const result = await this.fastify.pg.query( | |
| query, | |
| [lat, lgn, radius] | |
| ); | |
| return result.rows.map((row: { count: number, latitude: number, longitude: number }) => { | |
| return { count: row.count, latitude: row.latitude, longitude: row.longitude }; | |
| }); |
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -0,0 +1,64 @@ | ||||||
| import { FastifyInstance } from 'fastify'; | ||||||
| import { getNearUsersHandler } from '../../../controllers/private/map'; | ||||||
|
|
||||||
| const mapRoutes = async (fastify: FastifyInstance) => { | ||||||
| fastify.get('/', { | ||||||
| schema: { | ||||||
| debug: true, | ||||||
|
||||||
| debug: true, | |
| debug: false, |
Copilot
AI
Dec 27, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The schema property name 'profilePicture' does not match the MapUser type property name 'profilePictureUrl'. This will cause a mismatch between the returned data and the schema validation. Either update the schema to use 'profilePictureUrl' or change the MapUser type to use 'profilePicture'.
| profilePicture: { type: 'string', nullable: true }, | |
| profilePictureUrl: { type: 'string', nullable: true }, |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,39 @@ | ||
| import UserModel from "../models/User"; | ||
| import fp from 'fastify-plugin'; | ||
| import { MapUser, MapUserCluster } from "../models/User"; | ||
| import { FastifyInstance } from 'fastify'; | ||
| import { BadRequestError } from "../utils/error"; | ||
|
|
||
| class MapService { | ||
| private fastify: FastifyInstance; | ||
| private userModel: UserModel; | ||
|
|
||
| constructor(fastify: FastifyInstance) { | ||
| this.fastify = fastify; | ||
| this.userModel = new UserModel(fastify); | ||
| } | ||
|
|
||
| async getNearUsers(level: number, latitude: number, longitude: number, radius: number): Promise<{ users: MapUser[], clusters: MapUserCluster[] }> { | ||
| if (isNaN(level) || isNaN(latitude) || isNaN(longitude) || isNaN(radius)) | ||
| throw new BadRequestError(); | ||
| if (level !== 0 && level !== 1 && level !== 2) | ||
| throw new BadRequestError(); | ||
| if (latitude < -90 || latitude > 90 || longitude < -180 || longitude > 180 || radius < 0) | ||
| throw new BadRequestError(); | ||
|
|
||
| const data = { | ||
| users: [] as MapUser[], | ||
| clusters: [] as MapUserCluster[] | ||
| } | ||
| if (level == 0) | ||
|
||
| data.users = await this.userModel.getUsersFromLocation(latitude, longitude, radius); | ||
| else | ||
| data.clusters = await this.userModel.getUsersCountByLocation(level, latitude, longitude, radius); | ||
| return data; | ||
| } | ||
| } | ||
|
|
||
| export default fp(async (fastify: FastifyInstance) => { | ||
| const mapService = new MapService(fastify); | ||
| fastify.decorate('mapService', mapService); | ||
| }); | ||
| Original file line number | Diff line number | Diff line change | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,102 @@ | ||||||||||
| import { expect } from 'chai'; | ||||||||||
| import { app } from '../../setup'; | ||||||||||
| import { FastifyInstance } from 'fastify'; | ||||||||||
|
||||||||||
| import { FastifyInstance } from 'fastify'; |
Copilot
AI
Dec 27, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Unused variable data1.
Copilot
AI
Dec 27, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Unused variable data2.
Copilot
AI
Dec 27, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Unused variable data3.
Copilot
AI
Dec 27, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Unused variable data1.
Copilot
AI
Dec 27, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Unused variable data2.
Copilot
AI
Dec 27, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Unused variable data3.
Copilot
AI
Dec 27, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Unused variable data1.
Copilot
AI
Dec 27, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Unused variable data2.
Copilot
AI
Dec 27, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Unused variable data3.
Copilot
AI
Dec 27, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Missing semicolon at the end of the statement. Add a semicolon for consistency with the rest of the codebase.
| expect(data.clusters[0]).to.have.property('count').that.is.above(0) | |
| expect(data.clusters[0]).to.have.property('latitude').that.is.a('number') | |
| expect(data.clusters[0]).to.have.property('count').that.is.above(0); | |
| expect(data.clusters[0]).to.have.property('latitude').that.is.a('number'); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The variable 'radius' is declared with 'let' but is never reassigned. Use 'const' instead for better code clarity and to prevent accidental modifications.