From dcc98d4561cc8d80ef101f105bcd3af09c6e9020 Mon Sep 17 00:00:00 2001 From: YoungMame Date: Thu, 13 Nov 2025 15:27:12 +0100 Subject: [PATCH 01/10] add:(idk) --- fastify/assets/@types/fastify.d.ts | 4 ++ fastify/assets/srcs/app.ts | 3 + .../assets/srcs/services/BrowsingService.ts | 65 +++++++++++++++++++ .../test/integration/ws/browsing.test.ts | 54 +++++++++++++++ 4 files changed, 126 insertions(+) create mode 100644 fastify/assets/srcs/services/BrowsingService.ts create mode 100644 fastify/assets/test/integration/ws/browsing.test.ts diff --git a/fastify/assets/@types/fastify.d.ts b/fastify/assets/@types/fastify.d.ts index 1a2ea98..b5095f2 100644 --- a/fastify/assets/@types/fastify.d.ts +++ b/fastify/assets/@types/fastify.d.ts @@ -104,6 +104,10 @@ declare module 'fastify' { reportUser(reportedId: number, reporterId: number): Promise; }; + browsingService: { + browseUsers(userId: number, limit?: number, offset?: number, radius?: number): Promise>; + }; + nodemailer: any; authenticate(request: any, reply: any): Promise; diff --git a/fastify/assets/srcs/app.ts b/fastify/assets/srcs/app.ts index 167e27d..34aeaf6 100644 --- a/fastify/assets/srcs/app.ts +++ b/fastify/assets/srcs/app.ts @@ -15,6 +15,7 @@ import chatServicePlugin from './services/ChatService' import mailServicePlugin from './services/MailService' import notificationService from './services/NotificationsServices' import reportService from './services/ReportService' +import browsingService from './services/BrowsingService' // import custom plugins import authenticate from './plugins/authenticate' import checkImageConformity from './plugins/checkImageConformity' @@ -73,6 +74,8 @@ export const buildApp = () => { app.register(reportService); + app.register(browsingService); + app.register(pg, { connectionString: process.env.PG }); diff --git a/fastify/assets/srcs/services/BrowsingService.ts b/fastify/assets/srcs/services/BrowsingService.ts new file mode 100644 index 0000000..9e82efe --- /dev/null +++ b/fastify/assets/srcs/services/BrowsingService.ts @@ -0,0 +1,65 @@ +import PasswordManager from "../utils/password"; +import User from "../classes/User"; +import UserModel from "../models/User"; +import LikeModel from "../models/Like"; +import ViewModel from "../models/View" +// import ChatModel from "../models/Chat"; +import fp from 'fastify-plugin'; +import { FastifyInstance } from 'fastify'; +import { UnauthorizedError, NotFoundError, BadRequestError, InternalServerError, ForbiddenError, ConflictError } from "../utils/error"; + +class BrowsingService { + private fastify: FastifyInstance; + private userModel: UserModel; + private likeModel: LikeModel; + private viewModel: ViewModel + // private chatModel: ChatModel; + UsersCache: Map; + + constructor(fastify: FastifyInstance) { + this.fastify = fastify; + this.userModel = new UserModel(fastify); + this.likeModel = new LikeModel(fastify); + this.viewModel = new ViewModel(fastify); + // this.chatModel = new ChatModel(fastify); + this.UsersCache = new Map(); + } + + private async getUsersFromCoordsAndRadius(userId: number, lat: number, lgn: number, limit: number, offset: number, radius: number) { + const rows = await this.fastify.pg.query( + ` + SELECT u.id, u.first_name, u.gender, u.profile_pictures, u.profile_picture_index, distances.distance + FROM users u + JOIN + ( + SELECT user_id, 6371 * acos(least(1, greatest(-1, cos(radians(latitude)) * cos(radians($1)) * cos(radians($2) - radians(longitude)) + sin(radians(latitude)) * sin(radians($1))))) AS distance + FROM locations + WHERE 6371 * acos(least(1, greatest(-1, cos(radians(latitude)) * cos(radians($1)) * cos(radians($2) - radians(longitude)) + sin(radians(latitude)) * sin(radians($1))))) < $3 + AND user_id != $4 + ORDER BY distance + LIMIT $5 OFFSET $6 + ) AS distances ON u.id = distances.user_id + WHERE u.is_profile_completed = TRUE + `, + [lat, lgn, radius, userId, limit, offset] + ); + // console.log(rows.rows); + return rows.rows; + } + + public async browseUsers(userId: number, limit: number = 5, offset: number = 0, radius: number = 25): Promise> { + const user = await this.fastify.userService.getMe(userId); + const lat = user.location?.latitude; + const lgn = user.location?.longitude; + + if (lat === undefined || lgn === undefined) + throw new BadRequestError(); + const userRows = await this.getUsersFromCoordsAndRadius(userId,lat, lgn, limit, offset, radius); + return userRows; + } +} + +export default fp(async (fastify: FastifyInstance) => { + const browsingService = new BrowsingService(fastify); + fastify.decorate('browsingService', browsingService); +}); \ No newline at end of file diff --git a/fastify/assets/test/integration/ws/browsing.test.ts b/fastify/assets/test/integration/ws/browsing.test.ts new file mode 100644 index 0000000..30aa7dd --- /dev/null +++ b/fastify/assets/test/integration/ws/browsing.test.ts @@ -0,0 +1,54 @@ +import chai from 'chai'; +import { expect } from 'chai'; +import { buildApp } from '../../../srcs/app'; +import { FastifyInstance } from 'fastify'; + +// import fixtures +import { quickUser } from '../fixtures/auth.fixtures'; + +describe('Block users test', () => { + let app: FastifyInstance; + + beforeEach(async () => { + app = buildApp(); + await app.ready(); + }); + + it('should be able get near users', async function (this: any) { + this.timeout(5000); + + const { userData: data1, token: token1 } = await quickUser(app); + const { userData: data2, token: token2 } = await quickUser(app); + + await app.inject({ + method: 'PUT', + url: `/private/user/me/profile`, + headers: { + 'Cookie': `jwt=${token1}` + }, + payload: { + location: { + latitude: 48.8566, + longitude: 2.3522 + } + } + }); + + await app.inject({ + method: 'PUT', + url: `/private/user/me/profile`, + headers: { + 'Cookie': `jwt=${token2}` + }, + payload: { + location: { + latitude: 48.8566, + longitude: 2.3522 + } + } + }); + + const rows = await app.browsingService.browseUsers(data1.id as number, 10, 0, 50); + console.log('Browsed users:', rows); + }); +}); \ No newline at end of file From 46504995f6f879f478f2bfce30b626fab97e0a03 Mon Sep 17 00:00:00 2001 From: Mame <134452452+YoungMame@users.noreply.github.com> Date: Thu, 13 Nov 2025 18:32:11 +0100 Subject: [PATCH 02/10] add:(filter and sort type) --- .../assets/srcs/services/BrowsingService.ts | 39 +++++++++++++++---- .../test/integration/ws/browsing.test.ts | 10 ++--- 2 files changed, 37 insertions(+), 12 deletions(-) diff --git a/fastify/assets/srcs/services/BrowsingService.ts b/fastify/assets/srcs/services/BrowsingService.ts index 9e82efe..b6b4bad 100644 --- a/fastify/assets/srcs/services/BrowsingService.ts +++ b/fastify/assets/srcs/services/BrowsingService.ts @@ -8,6 +8,24 @@ import fp from 'fastify-plugin'; import { FastifyInstance } from 'fastify'; import { UnauthorizedError, NotFoundError, BadRequestError, InternalServerError, ForbiddenError, ConflictError } from "../utils/error"; +type BrowsingFilter = { + age?: { + min: number; + max: number; + }; + location?: { + latitude: number; + longitude: number; + } + fameRate?: { // between 0 and 1000 + min: number; + max: number; + } + tags?: Array; +} + +type BrowsingSort = 'distance' | 'age' | 'fameRate' | 'tags'; + class BrowsingService { private fastify: FastifyInstance; private userModel: UserModel; @@ -25,7 +43,11 @@ class BrowsingService { this.UsersCache = new Map(); } - private async getUsersFromCoordsAndRadius(userId: number, lat: number, lgn: number, limit: number, offset: number, radius: number) { + private async getUsersFromCoordsAndRadius(userId: number, lat: number, lgn: number, limit: number, offset: number, radius: number, filters?: BrowsingFilter): Promise> { + let parameters: Array> = [lat, lgn, radius, userId, limit, offset]; + if (filters?.tags && filters.tags.length > 0) { + parameters.push(filters.tags); + } const rows = await this.fastify.pg.query( ` SELECT u.id, u.first_name, u.gender, u.profile_pictures, u.profile_picture_index, distances.distance @@ -37,24 +59,27 @@ class BrowsingService { WHERE 6371 * acos(least(1, greatest(-1, cos(radians(latitude)) * cos(radians($1)) * cos(radians($2) - radians(longitude)) + sin(radians(latitude)) * sin(radians($1))))) < $3 AND user_id != $4 ORDER BY distance - LIMIT $5 OFFSET $6 ) AS distances ON u.id = distances.user_id WHERE u.is_profile_completed = TRUE + ${filters?.age ? `AND u.age BETWEEN ${filters.age.min} AND ${filters.age.max}` : ''} + ${filters?.fameRate ? `AND u.fame_rate BETWEEN ${filters.fameRate.min} AND ${filters.fameRate.max}` : ''} + ${filters?.tags && filters.tags.length > 0 ? `FROM users WHERE tags @> $7::text[]` : ''} + LIMIT $5 OFFSET $6 `, - [lat, lgn, radius, userId, limit, offset] + parameters ); // console.log(rows.rows); return rows.rows; } - public async browseUsers(userId: number, limit: number = 5, offset: number = 0, radius: number = 25): Promise> { + public async browseUsers(userId: number, limit: number = 5, offset: number = 0, radius: number = 25, filters?: BrowsingFilter, sort?: BrowsingSort): Promise> { const user = await this.fastify.userService.getMe(userId); - const lat = user.location?.latitude; - const lgn = user.location?.longitude; + const lat = filters?.location?.latitude ?? user.location?.latitude; + const lgn = filters?.location?.longitude ?? user.location?.longitude; if (lat === undefined || lgn === undefined) throw new BadRequestError(); - const userRows = await this.getUsersFromCoordsAndRadius(userId,lat, lgn, limit, offset, radius); + const userRows = await this.getUsersFromCoordsAndRadius(userId, lat, lgn, limit, offset, radius); return userRows; } } diff --git a/fastify/assets/test/integration/ws/browsing.test.ts b/fastify/assets/test/integration/ws/browsing.test.ts index 30aa7dd..437b354 100644 --- a/fastify/assets/test/integration/ws/browsing.test.ts +++ b/fastify/assets/test/integration/ws/browsing.test.ts @@ -38,13 +38,13 @@ describe('Block users test', () => { method: 'PUT', url: `/private/user/me/profile`, headers: { - 'Cookie': `jwt=${token2}` + 'Cookie': `jwt=${token2}` }, payload: { - location: { - latitude: 48.8566, - longitude: 2.3522 - } + location: { + latitude: 48.94705, + longitude: 2.3522 + } } }); From 7527f4896293a30c014ed4f9b5f4207eb73ff0b0 Mon Sep 17 00:00:00 2001 From: Mame <134452452+YoungMame@users.noreply.github.com> Date: Thu, 13 Nov 2025 20:06:56 +0100 Subject: [PATCH 03/10] add:(sorting [NOT TESTED]) --- fastify/assets/@types/fastify.d.ts | 3 +- .../assets/srcs/services/BrowsingService.ts | 44 +++++++++++- .../test/integration/ws/browsing.test.ts | 72 ++++++++++++------- 3 files changed, 88 insertions(+), 31 deletions(-) diff --git a/fastify/assets/@types/fastify.d.ts b/fastify/assets/@types/fastify.d.ts index b5095f2..a233db4 100644 --- a/fastify/assets/@types/fastify.d.ts +++ b/fastify/assets/@types/fastify.d.ts @@ -1,4 +1,5 @@ import 'fastify'; +import { BrowsingFilter, BrowsingSort } from '../srcs/services/BrowsingService'; declare module 'fastify' { interface FastifyInstance { @@ -105,7 +106,7 @@ declare module 'fastify' { }; browsingService: { - browseUsers(userId: number, limit?: number, offset?: number, radius?: number): Promise>; + browseUsers(userId: number, limit: number = 5, offset: number = 0, radius: number = 25, filters?: BrowsingFilter, sort?: BrowsingSort): Promise> }; nodemailer: any; diff --git a/fastify/assets/srcs/services/BrowsingService.ts b/fastify/assets/srcs/services/BrowsingService.ts index b6b4bad..be48fcd 100644 --- a/fastify/assets/srcs/services/BrowsingService.ts +++ b/fastify/assets/srcs/services/BrowsingService.ts @@ -8,7 +8,7 @@ import fp from 'fastify-plugin'; import { FastifyInstance } from 'fastify'; import { UnauthorizedError, NotFoundError, BadRequestError, InternalServerError, ForbiddenError, ConflictError } from "../utils/error"; -type BrowsingFilter = { +export type BrowsingFilter = { age?: { min: number; max: number; @@ -24,7 +24,7 @@ type BrowsingFilter = { tags?: Array; } -type BrowsingSort = 'distance' | 'age' | 'fameRate' | 'tags'; +export type BrowsingSort = 'distance' | 'age' | 'fameRate' | 'tags'; class BrowsingService { private fastify: FastifyInstance; @@ -72,6 +72,33 @@ class BrowsingService { return rows.rows; } + private getSimilarTagsCount(userTags: Array, otherUserTags: Array): number { + let count = 0; + + for (const tag of userTags) { + if (otherUserTags.includes(tag)) { + count++; + } + } + return count; + } + + private sortByDistance(userRows: Array): Array { + return userRows.sort((a, b) => a.distance - b.distance); + } + + private sortByAge(userRows: Array): Array { + return userRows.sort((a, b) => a.age - b.age); + } + + private sortByFameRate(userRows: Array): Array { + return userRows.sort((a, b) => a.fame_rate - b.fame_rate); + } + + private sortByTags(userRows: Array, userTags: Array): Array { + return userRows.sort((a, b) => this.getSimilarTagsCount(userTags, b.tags || []) - this.getSimilarTagsCount(userTags, a.tags || [])); + } + public async browseUsers(userId: number, limit: number = 5, offset: number = 0, radius: number = 25, filters?: BrowsingFilter, sort?: BrowsingSort): Promise> { const user = await this.fastify.userService.getMe(userId); const lat = filters?.location?.latitude ?? user.location?.latitude; @@ -80,7 +107,18 @@ class BrowsingService { if (lat === undefined || lgn === undefined) throw new BadRequestError(); const userRows = await this.getUsersFromCoordsAndRadius(userId, lat, lgn, limit, offset, radius); - return userRows; + switch (sort) { + case 'distance': + return this.sortByDistance(userRows); + case 'age': + return this.sortByAge(userRows); + case 'fameRate': + return this.sortByFameRate(userRows); + case 'tags': + return this.sortByTags(userRows, user.tags || []); + default: + return userRows; + } } } diff --git a/fastify/assets/test/integration/ws/browsing.test.ts b/fastify/assets/test/integration/ws/browsing.test.ts index 437b354..edc7e97 100644 --- a/fastify/assets/test/integration/ws/browsing.test.ts +++ b/fastify/assets/test/integration/ws/browsing.test.ts @@ -6,6 +6,22 @@ import { FastifyInstance } from 'fastify'; // import fixtures import { quickUser } from '../fixtures/auth.fixtures'; +const setLocalisation = async (app: FastifyInstance, token: string, lat: number, lgn: number) => { + await app.inject({ + method: 'PUT', + url: `/private/user/me/profile`, + headers: { + 'Cookie': `jwt=${token}` + }, + payload: { + location: { + latitude: lat, + longitude: lgn + } + } + }); +}; + describe('Block users test', () => { let app: FastifyInstance; @@ -20,35 +36,37 @@ describe('Block users test', () => { const { userData: data1, token: token1 } = await quickUser(app); const { userData: data2, token: token2 } = await quickUser(app); - await app.inject({ - method: 'PUT', - url: `/private/user/me/profile`, - headers: { - 'Cookie': `jwt=${token1}` - }, - payload: { - location: { - latitude: 48.8566, - longitude: 2.3522 - } - } - }); - - await app.inject({ - method: 'PUT', - url: `/private/user/me/profile`, - headers: { - 'Cookie': `jwt=${token2}` - }, - payload: { - location: { - latitude: 48.94705, - longitude: 2.3522 - } - } - }); + await setLocalisation(app, token1, 48.8566, 2.3522); + + await setLocalisation(app, token2, 48.94705, 2.3522); const rows = await app.browsingService.browseUsers(data1.id as number, 10, 0, 50); console.log('Browsed users:', rows); }); + + it('should be able to sort users by distance', async function (this: any) { + this.timeout(5000); + + const { userData: data1, token: token1 } = await quickUser(app); + const { userData: data2, token: token2 } = await quickUser(app); + const { userData: data3, token: token3 } = await quickUser(app); + const { userData: data4, token: token4 } = await quickUser(app); + const { userData: data5, token: token5 } = await quickUser(app); + + await setLocalisation(app, token1, 48.8566, 2.3522); + + await setLocalisation(app, token2, 48.94705, 2.3522); + + await setLocalisation(app, token3, 48.8516, 2.4525); + + await setLocalisation(app, token4, 48.8566, 2.3622); + + await setLocalisation(app, token5, 48.8566, 2.5522); + + const rows = await app.browsingService.browseUsers(data1.id as number, 10, 0, 50, undefined, 'distance'); + + for (let i = 0; i < rows.length - 1; i++) { + expect(rows[i].distance).to.be.greaterThan(rows[i + 1].distance); + } + }); }); \ No newline at end of file From eae2966ed6981b344aeb6e40bef465969ba4f94a Mon Sep 17 00:00:00 2001 From: YoungMame Date: Fri, 14 Nov 2025 10:29:55 +0100 Subject: [PATCH 04/10] add:(sort by age) --- .../srcs/controllers/private/me/profile.ts | 32 +++++++--- .../srcs/routes/private/user/me/profile.ts | 2 +- .../assets/srcs/services/BrowsingService.ts | 53 +++++++++++----- .../test/integration/ws/browsing.test.ts | 62 ++++++++++++++++++- 4 files changed, 126 insertions(+), 23 deletions(-) diff --git a/fastify/assets/srcs/controllers/private/me/profile.ts b/fastify/assets/srcs/controllers/private/me/profile.ts index 65cd59e..87e69bd 100644 --- a/fastify/assets/srcs/controllers/private/me/profile.ts +++ b/fastify/assets/srcs/controllers/private/me/profile.ts @@ -5,13 +5,31 @@ export const setProfileHandler = async ( request: FastifyRequest, reply: FastifyReply ) => { - const body = request.body as any; - const updateObject: any = {}; - Object.entries(body).forEach(([key, value]) => { - if (value !== undefined && key !== 'location') { - updateObject[key] = value; - } - }); + const body = request.body as { + bio?: string; + tags?: Array; + gender?: string; + orientation?: string; + bornAt?: string; + location?: { + latitude?: number; + longitude?: number; + }; + }; + + let updateObject: { + bio?: string; + tags?: string[]; + gender?: string; + orientation?: string; + bornAt?: Date; + } = {}; + + if (body.bio !== undefined) updateObject.bio = body.bio; + if (body.tags !== undefined) updateObject.tags = body.tags; + if (body.gender !== undefined) updateObject.gender = body.gender; + if (body.orientation !== undefined) updateObject.orientation = body.orientation; + if (body.bornAt) updateObject.bornAt = new Date(body.bornAt); try { const user = request.user as FastifyRequestUser | undefined; diff --git a/fastify/assets/srcs/routes/private/user/me/profile.ts b/fastify/assets/srcs/routes/private/user/me/profile.ts index 156d558..d2abbc8 100644 --- a/fastify/assets/srcs/routes/private/user/me/profile.ts +++ b/fastify/assets/srcs/routes/private/user/me/profile.ts @@ -4,6 +4,7 @@ import { setProfileHandler, getProfileHandler } from "../../../../controllers/pr const profileRoutes = async (fastify: FastifyInstance) => { fastify.put('/', { schema: { + body: { type: 'object', properties: { @@ -18,7 +19,6 @@ const profileRoutes = async (fastify: FastifyInstance) => { latitude: { type: 'number' }, longitude: { type: 'number' } }, - required: ['latitude', 'longitude'], additionalProperties: false } }, diff --git a/fastify/assets/srcs/services/BrowsingService.ts b/fastify/assets/srcs/services/BrowsingService.ts index be48fcd..30951f9 100644 --- a/fastify/assets/srcs/services/BrowsingService.ts +++ b/fastify/assets/srcs/services/BrowsingService.ts @@ -26,6 +26,17 @@ export type BrowsingFilter = { export type BrowsingSort = 'distance' | 'age' | 'fameRate' | 'tags'; +export type BrowsingUser = { + id: number; + firstName: string; + gender: string; + tags: Array; + fameRate: number; + profilePicture: string; + bornAt: string; + distance: number; +} + class BrowsingService { private fastify: FastifyInstance; private userModel: UserModel; @@ -43,14 +54,14 @@ class BrowsingService { this.UsersCache = new Map(); } - private async getUsersFromCoordsAndRadius(userId: number, lat: number, lgn: number, limit: number, offset: number, radius: number, filters?: BrowsingFilter): Promise> { + private async getUsersFromCoordsAndRadius(userId: number, lat: number, lgn: number, limit: number, offset: number, radius: number, filters?: BrowsingFilter): Promise> { let parameters: Array> = [lat, lgn, radius, userId, limit, offset]; if (filters?.tags && filters.tags.length > 0) { parameters.push(filters.tags); } - const rows = await this.fastify.pg.query( + const result = await this.fastify.pg.query( ` - SELECT u.id, u.first_name, u.gender, u.profile_pictures, u.profile_picture_index, distances.distance + SELECT u.id, u.first_name, u.gender, u.profile_pictures, u.profile_picture_index, u.born_at, u.tags, u.fame_rate, distances.distance FROM users u JOIN ( @@ -61,6 +72,7 @@ class BrowsingService { ORDER BY distance ) AS distances ON u.id = distances.user_id WHERE u.is_profile_completed = TRUE + AND u.profile_picture_index IS NOT NULL ${filters?.age ? `AND u.age BETWEEN ${filters.age.min} AND ${filters.age.max}` : ''} ${filters?.fameRate ? `AND u.fame_rate BETWEEN ${filters.fameRate.min} AND ${filters.fameRate.max}` : ''} ${filters?.tags && filters.tags.length > 0 ? `FROM users WHERE tags @> $7::text[]` : ''} @@ -68,8 +80,19 @@ class BrowsingService { `, parameters ); - // console.log(rows.rows); - return rows.rows; + return result.rows.map(row => { + const user = { + id: row.id as number, + firstName: row.first_name as string, + gender: row.gender as string, + tags: row.tags as Array, + fameRate: row.fame_rate as number, + profilePicture: row.profile_pictures[row.profile_picture_index] ?? '', + bornAt: row.born_at as string, + distance: row.distance as number + } + return user; + }); } private getSimilarTagsCount(userTags: Array, otherUserTags: Array): number { @@ -83,26 +106,28 @@ class BrowsingService { return count; } - private sortByDistance(userRows: Array): Array { + private sortByDistance(userRows: Array): Array { return userRows.sort((a, b) => a.distance - b.distance); } - private sortByAge(userRows: Array): Array { - return userRows.sort((a, b) => a.age - b.age); + private sortByAge(userRows: Array, bornAt: Date): Array { + return userRows.sort((a, b) => Math.abs(bornAt.getTime() - new Date(a.bornAt).getTime()) - Math.abs(bornAt.getTime() - new Date(b.bornAt).getTime())); } - private sortByFameRate(userRows: Array): Array { - return userRows.sort((a, b) => a.fame_rate - b.fame_rate); + private sortByFameRate(userRows: Array): Array { + return userRows.sort((a, b) => a.fameRate - b.fameRate); } - private sortByTags(userRows: Array, userTags: Array): Array { + private sortByTags(userRows: Array, userTags: Array): Array { return userRows.sort((a, b) => this.getSimilarTagsCount(userTags, b.tags || []) - this.getSimilarTagsCount(userTags, a.tags || [])); } - public async browseUsers(userId: number, limit: number = 5, offset: number = 0, radius: number = 25, filters?: BrowsingFilter, sort?: BrowsingSort): Promise> { + public async browseUsers(userId: number, limit: number = 5, offset: number = 0, radius: number = 25, filters?: BrowsingFilter, sort?: BrowsingSort): Promise> { const user = await this.fastify.userService.getMe(userId); const lat = filters?.location?.latitude ?? user.location?.latitude; const lgn = filters?.location?.longitude ?? user.location?.longitude; + const bornAt = user.bornAt; + const tags = user.tags; if (lat === undefined || lgn === undefined) throw new BadRequestError(); @@ -111,11 +136,11 @@ class BrowsingService { case 'distance': return this.sortByDistance(userRows); case 'age': - return this.sortByAge(userRows); + return this.sortByAge(userRows, bornAt); case 'fameRate': return this.sortByFameRate(userRows); case 'tags': - return this.sortByTags(userRows, user.tags || []); + return this.sortByTags(userRows, tags); default: return userRows; } diff --git a/fastify/assets/test/integration/ws/browsing.test.ts b/fastify/assets/test/integration/ws/browsing.test.ts index edc7e97..a19d234 100644 --- a/fastify/assets/test/integration/ws/browsing.test.ts +++ b/fastify/assets/test/integration/ws/browsing.test.ts @@ -22,6 +22,37 @@ const setLocalisation = async (app: FastifyInstance, token: string, lat: number, }); }; +const setBirthDate = async (app: FastifyInstance, token: string, birthdate: string) => { + const response = await app.inject({ + method: 'PUT', + url: `/private/user/me/profile`, + headers: { + 'Cookie': `jwt=${token}` + }, + payload: { + bornAt: new Date(birthdate).toISOString() + } + }); + console.log('Set birthdate response status:', response.statusCode); +} + +const getAgeDifference = (birthdate1: string, birthdate2: string): number => { + console.log('Birthdate1:', birthdate1, 'Birthdate2:', birthdate2); + const date1 = new Date(birthdate1); + const date2 = new Date(birthdate2); + + console.log('Date1:', date1, 'Date2:', date2); + + const age1 = new Date().getTime() - date1.getTime(); + const age2 = new Date().getTime() - date2.getTime(); + + console.log('Age1:', age1, 'Age2:', age2); + + console.log('Age Difference:', Math.abs(age1 - age2)); + + return Math.abs(age1 - age2); +} + describe('Block users test', () => { let app: FastifyInstance; @@ -66,7 +97,36 @@ describe('Block users test', () => { const rows = await app.browsingService.browseUsers(data1.id as number, 10, 0, 50, undefined, 'distance'); for (let i = 0; i < rows.length - 1; i++) { - expect(rows[i].distance).to.be.greaterThan(rows[i + 1].distance); + expect(rows[i].distance).to.be.lessThanOrEqual(rows[i + 1].distance); + } + }); + + it('should be able to sort users by age', async function (this: any) { + this.timeout(5000); + + const userBirthdate = '1992-06-15'; + const { userData: data1, token: token1 } = await quickUser(app); + const { userData: data2, token: token2 } = await quickUser(app); + const { userData: data3, token: token3 } = await quickUser(app); + const { userData: data4, token: token4 } = await quickUser(app); + const { userData: data5, token: token5 } = await quickUser(app); + + await setLocalisation(app, token1, 48.8566, 2.3522); + await setLocalisation(app, token2, 48.94705, 2.3522); + await setLocalisation(app, token3, 48.8566, 2.3525); + await setLocalisation(app, token4, 48.8566, 2.3622); + await setLocalisation(app, token5, 48.8566, 2.5522); + + await setBirthDate(app, token1, userBirthdate); + await setBirthDate(app, token2, '1987-02-02'); + await setBirthDate(app, token3, '1994-03-03'); + await setBirthDate(app, token4, '1996-04-04'); + await setBirthDate(app, token5, '1980-05-05'); + + const rows = await app.browsingService.browseUsers(data1.id as number, 10, 0, 50, undefined, 'age'); + + for (let i = 0; i < rows.length - 1; i++) { + expect(getAgeDifference(userBirthdate, rows[i].bornAt)).to.be.lessThanOrEqual(getAgeDifference(userBirthdate, rows[i + 1].bornAt)); } }); }); \ No newline at end of file From 0764ab6e618f6efcb7ef4e3b159d6c6262ff5e0b Mon Sep 17 00:00:00 2001 From: YoungMame Date: Fri, 14 Nov 2025 13:12:20 +0100 Subject: [PATCH 05/10] add:(sorting and filtering [TEST}) --- .../assets/srcs/services/BrowsingService.ts | 11 +- .../ws/browsing.filtersort.test.ts | 446 ++++++++++++++++++ .../test/integration/ws/browsing.test.ts | 132 ------ 3 files changed, 453 insertions(+), 136 deletions(-) create mode 100644 fastify/assets/test/integration/ws/browsing.filtersort.test.ts delete mode 100644 fastify/assets/test/integration/ws/browsing.test.ts diff --git a/fastify/assets/srcs/services/BrowsingService.ts b/fastify/assets/srcs/services/BrowsingService.ts index 30951f9..9a84e67 100644 --- a/fastify/assets/srcs/services/BrowsingService.ts +++ b/fastify/assets/srcs/services/BrowsingService.ts @@ -73,9 +73,11 @@ class BrowsingService { ) AS distances ON u.id = distances.user_id WHERE u.is_profile_completed = TRUE AND u.profile_picture_index IS NOT NULL - ${filters?.age ? `AND u.age BETWEEN ${filters.age.min} AND ${filters.age.max}` : ''} + ${filters?.age ? `AND (CURRENT_DATE - u.born_at) / 365 +BETWEEN ${filters.age.min} AND ${filters.age.max} + ` : ''} ${filters?.fameRate ? `AND u.fame_rate BETWEEN ${filters.fameRate.min} AND ${filters.fameRate.max}` : ''} - ${filters?.tags && filters.tags.length > 0 ? `FROM users WHERE tags @> $7::text[]` : ''} + ${filters?.tags && filters.tags.length > 0 ? `AND u.tags @> $7::text[]` : ''} LIMIT $5 OFFSET $6 `, parameters @@ -115,7 +117,8 @@ class BrowsingService { } private sortByFameRate(userRows: Array): Array { - return userRows.sort((a, b) => a.fameRate - b.fameRate); + console.log('sortByFameRate called with:', userRows); + return userRows.sort((a, b) => b.fameRate - a.fameRate); } private sortByTags(userRows: Array, userTags: Array): Array { @@ -131,7 +134,7 @@ class BrowsingService { if (lat === undefined || lgn === undefined) throw new BadRequestError(); - const userRows = await this.getUsersFromCoordsAndRadius(userId, lat, lgn, limit, offset, radius); + const userRows = await this.getUsersFromCoordsAndRadius(userId, lat, lgn, limit, offset, radius, filters); switch (sort) { case 'distance': return this.sortByDistance(userRows); diff --git a/fastify/assets/test/integration/ws/browsing.filtersort.test.ts b/fastify/assets/test/integration/ws/browsing.filtersort.test.ts new file mode 100644 index 0000000..6d95e70 --- /dev/null +++ b/fastify/assets/test/integration/ws/browsing.filtersort.test.ts @@ -0,0 +1,446 @@ +import chai from 'chai'; +import { expect } from 'chai'; +import { buildApp } from '../../../srcs/app'; +import { FastifyInstance } from 'fastify'; + +// import fixtures +import { quickUser } from '../fixtures/auth.fixtures'; + +const setTags = async (app: FastifyInstance, token: string, tags: Array) => { + await app.inject({ + method: 'PUT', + url: `/private/user/me/profile`, + headers: { + 'Cookie': `jwt=${token}` + }, + payload: { + tags: tags + } + }); +} + +const viewUser = async (app: FastifyInstance, token: string, likedUserId: number) => { + await app.inject({ + method: 'GET', + url: `/private/user/view/${likedUserId}`, + headers: { + 'Cookie': `jwt=${token}` + } + }); +}; + +const likeUser = async (app: FastifyInstance, token: string, likedUserId: number) => { + await app.inject({ + method: 'POST', + url: `/private/user/like/${likedUserId}`, + headers: { + 'Cookie': `jwt=${token}` + } + }); +}; + +const setLocalisation = async (app: FastifyInstance, token: string, lat: number, lgn: number) => { + await app.inject({ + method: 'PUT', + url: `/private/user/me/profile`, + headers: { + 'Cookie': `jwt=${token}` + }, + payload: { + location: { + latitude: lat, + longitude: lgn + } + } + }); +}; + +const setBirthDate = async (app: FastifyInstance, token: string, birthdate: string) => { + const response = await app.inject({ + method: 'PUT', + url: `/private/user/me/profile`, + headers: { + 'Cookie': `jwt=${token}` + }, + payload: { + bornAt: new Date(birthdate).toISOString() + } + }); + console.log(response.statusCode); +} + +const getAgeDifference = (birthdate1: string, birthdate2: string): number => { + const date1 = new Date(birthdate1); + const date2 = new Date(birthdate2); + + const age1 = new Date().getTime() - date1.getTime(); + const age2 = new Date().getTime() - date2.getTime(); + + return Math.abs(age1 - age2); +} + +describe('Browsing filters and sorting', () => { + let app: FastifyInstance; + + beforeEach(async () => { + app = buildApp(); + await app.ready(); + }); + + it('should be able get near users', async function (this: any) { + this.timeout(5000); + + const { userData: data1, token: token1 } = await quickUser(app); + const { userData: data2, token: token2 } = await quickUser(app); + + await setLocalisation(app, token1, 48.8566, 2.3522); + + await setLocalisation(app, token2, 48.94705, 2.3522); + + const rows = await app.browsingService.browseUsers(data1.id as number, 10, 0, 50); + + expect(rows.length).to.be.greaterThan(0); + }); + + it('should be able to sort users by distance', async function (this: any) { + this.timeout(5000); + + const { userData: data1, token: token1 } = await quickUser(app); + const { userData: data2, token: token2 } = await quickUser(app); + const { userData: data3, token: token3 } = await quickUser(app); + const { userData: data4, token: token4 } = await quickUser(app); + const { userData: data5, token: token5 } = await quickUser(app); + + await setLocalisation(app, token1, 48.8566, 2.3522); + + await setLocalisation(app, token2, 48.94705, 2.3522); + + await setLocalisation(app, token3, 48.8516, 2.4525); + + await setLocalisation(app, token4, 48.8566, 2.3622); + + await setLocalisation(app, token5, 48.8566, 2.5522); + + const rows = await app.browsingService.browseUsers(data1.id as number, 10, 0, 50, undefined, 'distance'); + + for (let i = 0; i < rows.length - 1; i++) { + expect(rows[i].distance).to.be.lessThanOrEqual(rows[i + 1].distance); + } + }); + + it('should be able to sort users by age', async function (this: any) { + this.timeout(5000); + + const userBirthdate = '1992-06-15'; + const { userData: data1, token: token1 } = await quickUser(app); + const { userData: data2, token: token2 } = await quickUser(app); + const { userData: data3, token: token3 } = await quickUser(app); + const { userData: data4, token: token4 } = await quickUser(app); + const { userData: data5, token: token5 } = await quickUser(app); + + await setLocalisation(app, token1, 48.8566, 2.3522); + await setLocalisation(app, token2, 48.94705, 2.3522); + await setLocalisation(app, token3, 48.8566, 2.3525); + await setLocalisation(app, token4, 48.8566, 2.3622); + await setLocalisation(app, token5, 48.8566, 2.5522); + + await setBirthDate(app, token1, userBirthdate); + await setBirthDate(app, token2, '1987-02-02'); + await setBirthDate(app, token3, '1994-03-03'); + await setBirthDate(app, token4, '1996-04-04'); + await setBirthDate(app, token5, '1980-05-05'); + + const rows = await app.browsingService.browseUsers(data1.id as number, 10, 0, 50, undefined, 'age'); + + for (let i = 0; i < rows.length - 1; i++) { + expect(getAgeDifference(userBirthdate, rows[i].bornAt)).to.be.lessThanOrEqual(getAgeDifference(userBirthdate, rows[i + 1].bornAt)); + } + }); + + it('should be able to sort users by fame rate', async function (this: any) { + this.timeout(5000); + + const { userData: data1, token: token1 } = await quickUser(app); + const { userData: data2, token: token2 } = await quickUser(app); + const { userData: data3, token: token3 } = await quickUser(app); + const { userData: data4, token: token4 } = await quickUser(app); + const { userData: data5, token: token5 } = await quickUser(app); + + await setLocalisation(app, token1, 34.3453, 2.325); + await setLocalisation(app, token2, 34.3453, 2.323); + await setLocalisation(app, token3, 34.3453, 2.3212); + await setLocalisation(app, token4, 34.3453, 2.3232); + await setLocalisation(app, token5, 34.3453, 2.321); + + await viewUser(app, token1, data2.id as number); + await viewUser(app, token1, data3.id as number); + await viewUser(app, token1, data4.id as number); + await viewUser(app, token1, data5.id as number); + await viewUser(app, token2, data3.id as number); + await viewUser(app, token2, data4.id as number); + await viewUser(app, token2, data5.id as number); + await viewUser(app, token3, data2.id as number); + await viewUser(app, token3, data4.id as number); + await viewUser(app, token3, data5.id as number); + await viewUser(app, token4, data2.id as number); + await viewUser(app, token4, data3.id as number); + await viewUser(app, token4, data5.id as number); + await viewUser(app, token5, data2.id as number); + await viewUser(app, token5, data3.id as number); + await viewUser(app, token5, data4.id as number); + + await likeUser(app, token1, data2.id as number); + await likeUser(app, token1, data3.id as number); + await likeUser(app, token2, data4.id as number); + await likeUser(app, token3, data4.id as number); + await likeUser(app, token3, data5.id as number); + await likeUser(app, token4, data5.id as number); + + const rows = await app.browsingService.browseUsers(data1.id as number, 10, 0, 50, undefined, 'fameRate'); + + for (let i = 0; i < rows.length - 1; i++) { + expect(rows[i].fameRate).to.be.greaterThanOrEqual(rows[i + 1].fameRate); + } + }); + + it('should be able to sort users by fame rate', async function (this: any) { + this.timeout(5000); + + const { userData: data1, token: token1 } = await quickUser(app); + const { userData: data2, token: token2 } = await quickUser(app); + const { userData: data3, token: token3 } = await quickUser(app); + const { userData: data4, token: token4 } = await quickUser(app); + const { userData: data5, token: token5 } = await quickUser(app); + + await setLocalisation(app, token1, 34.3453, 2.325); + await setLocalisation(app, token2, 34.3453, 2.323); + await setLocalisation(app, token3, 34.3453, 2.3212); + await setLocalisation(app, token4, 34.3453, 2.3232); + await setLocalisation(app, token5, 34.3453, 2.321); + + await setTags(app, token1, ['music', 'sports', 'art']); + await setTags(app, token2, ['music', 'sports', 'art']); + await setTags(app, token3, ['music', 'sports', 'reading']); + await setTags(app, token4, ['gambling', 'cooking', 'gaming']); + await setTags(app, token5, ['gaming', 'photography', 'art']); + + const rows = await app.browsingService.browseUsers(data1.id as number, 10, 0, 50, undefined, 'tags'); + console.log(rows); + + for (let i = 0; i < rows.length - 1; i++) { + const countCurrent = rows[i].tags.filter((tag: string) => ['music', 'sports', 'art'].includes(tag)).length; + const countNext = rows[i + 1].tags.filter((tag: string) => ['music', 'sports', 'art'].includes(tag)).length; + expect(countCurrent).to.be.greaterThanOrEqual(countNext); + } + }); + + it('should be able to filter users by tags', async function (this: any) { + this.timeout(5000); + + const { userData: data1, token: token1 } = await quickUser(app); + const { userData: data2, token: token2 } = await quickUser(app); + const { userData: data3, token: token3 } = await quickUser(app); + const { userData: data4, token: token4 } = await quickUser(app); + const { userData: data5, token: token5 } = await quickUser(app); + + await setLocalisation(app, token1, 34.3453, 2.325); + await setLocalisation(app, token2, 34.3453, 2.323); + await setLocalisation(app, token3, 34.3453, 2.3212); + await setLocalisation(app, token4, 34.3453, 2.3232); + await setLocalisation(app, token5, 34.3453, 2.321); + + await setTags(app, token1, ['music', 'sports', 'art']); + await setTags(app, token2, ['music', 'sports', 'art']); + await setTags(app, token3, ['music', 'sports', 'reading']); + await setTags(app, token4, ['music', 'cooking', 'gaming']); + await setTags(app, token5, ['gaming', 'photography', 'gambling']); + + const rows = await app.browsingService.browseUsers(data1.id as number, 10, 0, 50, { tags: ['music'] }); + console.log(rows); + + for (let i = 0; i < rows.length - 1; i++) { + expect(rows[i].tags).to.include('music'); + } + }); + + it('should be able to filter users by age', async function (this: any) { + this.timeout(5000); + + const { userData: data1, token: token1 } = await quickUser(app); + const { userData: data2, token: token2 } = await quickUser(app); + const { userData: data3, token: token3 } = await quickUser(app); + const { userData: data4, token: token4 } = await quickUser(app); + const { userData: data5, token: token5 } = await quickUser(app); + + await setLocalisation(app, token1, 38.3853, 2.325); + await setLocalisation(app, token2, 38.3853, 2.323); + await setLocalisation(app, token3, 38.3853, 2.3212); + await setLocalisation(app, token4, 38.3853, 2.3232); + await setLocalisation(app, token5, 38.3853, 2.321); + + const less40YearsAgo = new Date(); + less40YearsAgo.setFullYear(less40YearsAgo.getFullYear() - 40); + await setBirthDate(app, token2, less40YearsAgo.toISOString()); + + const less37YearsAgo = new Date(); + less37YearsAgo.setFullYear(less37YearsAgo.getFullYear() - 37); + await setBirthDate(app, token3, less37YearsAgo.toISOString()); + + const less35YearsAgo = new Date(); + less35YearsAgo.setFullYear(less35YearsAgo.getFullYear() - 35); + await setBirthDate(app, token4, less35YearsAgo.toISOString()); + + const less33YearsAgo = new Date(); + less33YearsAgo.setFullYear(less33YearsAgo.getFullYear() - 33); + await setBirthDate(app, token5, less33YearsAgo.toISOString()); + + const rows = await app.browsingService.browseUsers(data1.id as number, 10, 0, 50, { age: { min: 34, max: 39 } }); + console.log(rows); + + for (let i = 0; i < rows.length - 1; i++) { + const age = new Date().getFullYear() - new Date(rows[i].bornAt).getFullYear(); + expect(age).to.be.at.least(34); + expect(age).to.be.at.most(39); + } + }); + + it('should be able to filter users by age', async function (this: any) { + this.timeout(5000); + + const { userData: data1, token: token1 } = await quickUser(app); + const { userData: data2, token: token2 } = await quickUser(app); + const { userData: data3, token: token3 } = await quickUser(app); + const { userData: data4, token: token4 } = await quickUser(app); + const { userData: data5, token: token5 } = await quickUser(app); + + await setLocalisation(app, token1, 38.3853, 2.325); + await setLocalisation(app, token2, 38.3853, 2.323); + await setLocalisation(app, token3, 38.3853, 2.3212); + await setLocalisation(app, token4, 38.3853, 2.3232); + await setLocalisation(app, token5, 38.3853, 2.321); + + const less40YearsAgo = new Date(); + less40YearsAgo.setFullYear(less40YearsAgo.getFullYear() - 40); + await setBirthDate(app, token2, less40YearsAgo.toISOString()); + + const less37YearsAgo = new Date(); + less37YearsAgo.setFullYear(less37YearsAgo.getFullYear() - 37); + await setBirthDate(app, token3, less37YearsAgo.toISOString()); + + const less35YearsAgo = new Date(); + less35YearsAgo.setFullYear(less35YearsAgo.getFullYear() - 35); + await setBirthDate(app, token4, less35YearsAgo.toISOString()); + + const less33YearsAgo = new Date(); + less33YearsAgo.setFullYear(less33YearsAgo.getFullYear() - 33); + await setBirthDate(app, token5, less33YearsAgo.toISOString()); + + const rows = await app.browsingService.browseUsers(data1.id as number, 10, 0, 50, { age: { min: 34, max: 39 } }); + console.log(rows); + + for (let i = 0; i < rows.length - 1; i++) { + const age = new Date().getFullYear() - new Date(rows[i].bornAt).getFullYear(); + expect(age).to.be.at.least(34); + expect(age).to.be.at.most(39); + } + }); + + it('should be able to filter users by fame rate', async function (this: any) { + this.timeout(5000); + + const { userData: data1, token: token1 } = await quickUser(app); + const { userData: data2, token: token2 } = await quickUser(app); + const { userData: data3, token: token3 } = await quickUser(app); + const { userData: data4, token: token4 } = await quickUser(app); + const { userData: data5, token: token5 } = await quickUser(app); + + await setLocalisation(app, token1, 40.4053, 2.325); + await setLocalisation(app, token2, 40.4053, 2.323); + await setLocalisation(app, token3, 40.4053, 2.3212); + await setLocalisation(app, token4, 40.4053, 2.3232); + await setLocalisation(app, token5, 40.4053, 2.321); + + await viewUser(app, token1, data2.id as number); + await viewUser(app, token1, data3.id as number); + await viewUser(app, token1, data4.id as number); + await viewUser(app, token1, data5.id as number); + await viewUser(app, token2, data3.id as number); + await viewUser(app, token2, data4.id as number); + await viewUser(app, token2, data5.id as number); + await viewUser(app, token3, data2.id as number); + await viewUser(app, token3, data4.id as number); + await viewUser(app, token3, data5.id as number); + await viewUser(app, token4, data2.id as number); + await viewUser(app, token4, data3.id as number); + await viewUser(app, token4, data5.id as number); + await viewUser(app, token5, data2.id as number); + await viewUser(app, token5, data3.id as number); + await viewUser(app, token5, data4.id as number); + + await likeUser(app, token1, data2.id as number); + await likeUser(app, token1, data3.id as number); + await likeUser(app, token2, data4.id as number); + await likeUser(app, token3, data4.id as number); + await likeUser(app, token3, data5.id as number); + await likeUser(app, token4, data5.id as number); + + const rows = await app.browsingService.browseUsers(data1.id as number, 10, 0, 50, { fameRate: { min: 300, max: 800 } }); + + for (let i = 0; i < rows.length - 1; i++) { + expect(rows[i].fameRate).to.be.at.least(300); + expect(rows[i].fameRate).to.be.at.most(800); + } + + const rows1 = await app.browsingService.browseUsers(data1.id as number, 10, 0, 50, { fameRate: { min: 600, max: 1000 } }); + for (let i = 0; i < rows1.length - 1; i++) { + expect(rows1[i].fameRate).to.be.at.least(600); + expect(rows1[i].fameRate).to.be.at.most(1000); + } + + const rows2 = await app.browsingService.browseUsers(data1.id as number, 10, 0, 50, { fameRate: { min: 0, max: 0 } }); + for (let i = 0; i < rows2.length - 1; i++) { + expect(rows2[i].fameRate).to.be.at.least(0); + expect(rows2[i].fameRate).to.be.at.most(0); + } + }); + + it('should be able to filter users by location', async function (this: any) { + this.timeout(5000); + + const { userData: data1, token: token1 } = await quickUser(app); + const { userData: data2, token: token2 } = await quickUser(app); + const { userData: data3, token: token3 } = await quickUser(app); + const { userData: data4, token: token4 } = await quickUser(app); + const { userData: data5, token: token5 } = await quickUser(app); + + await setLocalisation(app, token1, 1.4053, 1.325); + await setLocalisation(app, token2, 40.4053, 2.323); + await setLocalisation(app, token3, 40.4053, 2.3212); + await setLocalisation(app, token4, 40.4053, 2.3232); + await setLocalisation(app, token5, 40.4053, 2.321); + + const rows = await app.browsingService.browseUsers(data1.id as number, 10, 0, 50, { + location: { + latitude: 40.4053, + longitude: 2.325 + } + , }, 'distance'); + expect(rows.length).to.be.greaterThan(0); + + for (let i = 0; i < rows.length - 1; i++) { + expect(rows[i].distance).to.be.at.most(50); + } + + const rows1 = await app.browsingService.browseUsers(data1.id as number, 10, 0, 25, { + location: { + latitude: 40.4053, + longitude: 2.325 + } + , }, 'distance'); + expect(rows1.length).to.be.greaterThan(0); + + for (let i = 0; i < rows1.length - 1; i++) { + expect(rows1[i].distance).to.be.at.most(25); + } + }); +}); \ No newline at end of file diff --git a/fastify/assets/test/integration/ws/browsing.test.ts b/fastify/assets/test/integration/ws/browsing.test.ts deleted file mode 100644 index a19d234..0000000 --- a/fastify/assets/test/integration/ws/browsing.test.ts +++ /dev/null @@ -1,132 +0,0 @@ -import chai from 'chai'; -import { expect } from 'chai'; -import { buildApp } from '../../../srcs/app'; -import { FastifyInstance } from 'fastify'; - -// import fixtures -import { quickUser } from '../fixtures/auth.fixtures'; - -const setLocalisation = async (app: FastifyInstance, token: string, lat: number, lgn: number) => { - await app.inject({ - method: 'PUT', - url: `/private/user/me/profile`, - headers: { - 'Cookie': `jwt=${token}` - }, - payload: { - location: { - latitude: lat, - longitude: lgn - } - } - }); -}; - -const setBirthDate = async (app: FastifyInstance, token: string, birthdate: string) => { - const response = await app.inject({ - method: 'PUT', - url: `/private/user/me/profile`, - headers: { - 'Cookie': `jwt=${token}` - }, - payload: { - bornAt: new Date(birthdate).toISOString() - } - }); - console.log('Set birthdate response status:', response.statusCode); -} - -const getAgeDifference = (birthdate1: string, birthdate2: string): number => { - console.log('Birthdate1:', birthdate1, 'Birthdate2:', birthdate2); - const date1 = new Date(birthdate1); - const date2 = new Date(birthdate2); - - console.log('Date1:', date1, 'Date2:', date2); - - const age1 = new Date().getTime() - date1.getTime(); - const age2 = new Date().getTime() - date2.getTime(); - - console.log('Age1:', age1, 'Age2:', age2); - - console.log('Age Difference:', Math.abs(age1 - age2)); - - return Math.abs(age1 - age2); -} - -describe('Block users test', () => { - let app: FastifyInstance; - - beforeEach(async () => { - app = buildApp(); - await app.ready(); - }); - - it('should be able get near users', async function (this: any) { - this.timeout(5000); - - const { userData: data1, token: token1 } = await quickUser(app); - const { userData: data2, token: token2 } = await quickUser(app); - - await setLocalisation(app, token1, 48.8566, 2.3522); - - await setLocalisation(app, token2, 48.94705, 2.3522); - - const rows = await app.browsingService.browseUsers(data1.id as number, 10, 0, 50); - console.log('Browsed users:', rows); - }); - - it('should be able to sort users by distance', async function (this: any) { - this.timeout(5000); - - const { userData: data1, token: token1 } = await quickUser(app); - const { userData: data2, token: token2 } = await quickUser(app); - const { userData: data3, token: token3 } = await quickUser(app); - const { userData: data4, token: token4 } = await quickUser(app); - const { userData: data5, token: token5 } = await quickUser(app); - - await setLocalisation(app, token1, 48.8566, 2.3522); - - await setLocalisation(app, token2, 48.94705, 2.3522); - - await setLocalisation(app, token3, 48.8516, 2.4525); - - await setLocalisation(app, token4, 48.8566, 2.3622); - - await setLocalisation(app, token5, 48.8566, 2.5522); - - const rows = await app.browsingService.browseUsers(data1.id as number, 10, 0, 50, undefined, 'distance'); - - for (let i = 0; i < rows.length - 1; i++) { - expect(rows[i].distance).to.be.lessThanOrEqual(rows[i + 1].distance); - } - }); - - it('should be able to sort users by age', async function (this: any) { - this.timeout(5000); - - const userBirthdate = '1992-06-15'; - const { userData: data1, token: token1 } = await quickUser(app); - const { userData: data2, token: token2 } = await quickUser(app); - const { userData: data3, token: token3 } = await quickUser(app); - const { userData: data4, token: token4 } = await quickUser(app); - const { userData: data5, token: token5 } = await quickUser(app); - - await setLocalisation(app, token1, 48.8566, 2.3522); - await setLocalisation(app, token2, 48.94705, 2.3522); - await setLocalisation(app, token3, 48.8566, 2.3525); - await setLocalisation(app, token4, 48.8566, 2.3622); - await setLocalisation(app, token5, 48.8566, 2.5522); - - await setBirthDate(app, token1, userBirthdate); - await setBirthDate(app, token2, '1987-02-02'); - await setBirthDate(app, token3, '1994-03-03'); - await setBirthDate(app, token4, '1996-04-04'); - await setBirthDate(app, token5, '1980-05-05'); - - const rows = await app.browsingService.browseUsers(data1.id as number, 10, 0, 50, undefined, 'age'); - - for (let i = 0; i < rows.length - 1; i++) { - expect(getAgeDifference(userBirthdate, rows[i].bornAt)).to.be.lessThanOrEqual(getAgeDifference(userBirthdate, rows[i + 1].bornAt)); - } - }); -}); \ No newline at end of file From 3d7e16506bffdffd0680b7913485e1aad68e643a Mon Sep 17 00:00:00 2001 From: YoungMame Date: Fri, 14 Nov 2025 14:12:48 +0100 Subject: [PATCH 06/10] add:(classic sort function) --- .../assets/srcs/services/BrowsingService.ts | 29 ++++++- .../assets/test/integration/utils/browsing.ts | 74 +++++++++++++++++ .../ws/browsing.filtersort.test.ts | 74 +---------------- .../test/integration/ws/browsing.test.ts | 83 +++++++++++++++++++ 4 files changed, 187 insertions(+), 73 deletions(-) create mode 100644 fastify/assets/test/integration/utils/browsing.ts create mode 100644 fastify/assets/test/integration/ws/browsing.test.ts diff --git a/fastify/assets/srcs/services/BrowsingService.ts b/fastify/assets/srcs/services/BrowsingService.ts index 9a84e67..07ee684 100644 --- a/fastify/assets/srcs/services/BrowsingService.ts +++ b/fastify/assets/srcs/services/BrowsingService.ts @@ -125,12 +125,39 @@ BETWEEN ${filters.age.min} AND ${filters.age.max} return userRows.sort((a, b) => this.getSimilarTagsCount(userTags, b.tags || []) - this.getSimilarTagsCount(userTags, a.tags || [])); } + private sortByAll(userRows: Array, bornAt: Date, userTags: Array, fameRate: number): Array { + const ageWeight = 0.3; + const tagsWeight = 0.4; + const fameRateWeight = 0.3; + const maxAgeDiff = 10; // years + const maxFameDiff = 400; + + let scoreMap = new Map(); // index to score + + userRows.forEach(user => { + const ageDiff = Math.abs(bornAt.getFullYear() - new Date(user.bornAt).getFullYear()); + const ageScore = 100 - (ageDiff / maxAgeDiff) * maxAgeDiff; + + const similarTagsCount = this.getSimilarTagsCount(userTags, user.tags || []); + const tagsScore = userTags.length > 0 ? (similarTagsCount / userTags.length) * 100 : 0; + + const fameRateDiff = Math.abs(fameRate - user.fameRate); + const fameRateScore = 100 - (fameRateDiff / maxFameDiff) * maxFameDiff; + + const totalScore = (ageScore * ageWeight) + (tagsScore * tagsWeight) + (fameRateScore * fameRateWeight); + console.log(`User ${user.id} - Age Score: ${ageScore.toFixed(2)}, Tags Score: ${tagsScore.toFixed(2)}, Fame Rate Score: ${fameRateScore.toFixed(2)}, Total Score: ${totalScore.toFixed(2)}`); + scoreMap.set(user.id, totalScore); + }) + return userRows.sort((a, b) => (scoreMap.get(b.id) ?? 0) - (scoreMap.get(a.id) ?? 0)); + } + public async browseUsers(userId: number, limit: number = 5, offset: number = 0, radius: number = 25, filters?: BrowsingFilter, sort?: BrowsingSort): Promise> { const user = await this.fastify.userService.getMe(userId); const lat = filters?.location?.latitude ?? user.location?.latitude; const lgn = filters?.location?.longitude ?? user.location?.longitude; const bornAt = user.bornAt; const tags = user.tags; + const fameRate = user.fameRate; if (lat === undefined || lgn === undefined) throw new BadRequestError(); @@ -145,7 +172,7 @@ BETWEEN ${filters.age.min} AND ${filters.age.max} case 'tags': return this.sortByTags(userRows, tags); default: - return userRows; + return this.sortByAll(userRows, bornAt, tags, fameRate ?? 0); } } } diff --git a/fastify/assets/test/integration/utils/browsing.ts b/fastify/assets/test/integration/utils/browsing.ts new file mode 100644 index 0000000..da2322c --- /dev/null +++ b/fastify/assets/test/integration/utils/browsing.ts @@ -0,0 +1,74 @@ +import { FastifyInstance } from "fastify"; + +export const setTags = async (app: FastifyInstance, token: string, tags: Array) => { + await app.inject({ + method: 'PUT', + url: `/private/user/me/profile`, + headers: { + 'Cookie': `jwt=${token}` + }, + payload: { + tags: tags + } + }); +} + +export const viewUser = async (app: FastifyInstance, token: string, likedUserId: number) => { + await app.inject({ + method: 'GET', + url: `/private/user/view/${likedUserId}`, + headers: { + 'Cookie': `jwt=${token}` + } + }); +}; + +export const likeUser = async (app: FastifyInstance, token: string, likedUserId: number) => { + await app.inject({ + method: 'POST', + url: `/private/user/like/${likedUserId}`, + headers: { + 'Cookie': `jwt=${token}` + } + }); +}; + +export const setLocalisation = async (app: FastifyInstance, token: string, lat: number, lgn: number) => { + await app.inject({ + method: 'PUT', + url: `/private/user/me/profile`, + headers: { + 'Cookie': `jwt=${token}` + }, + payload: { + location: { + latitude: lat, + longitude: lgn + } + } + }); +}; + +export const setBirthDate = async (app: FastifyInstance, token: string, birthdate: string) => { + const response = await app.inject({ + method: 'PUT', + url: `/private/user/me/profile`, + headers: { + 'Cookie': `jwt=${token}` + }, + payload: { + bornAt: new Date(birthdate).toISOString() + } + }); + console.log(response.statusCode); +} + +export const getAgeDifference = (birthdate1: string, birthdate2: string): number => { + const date1 = new Date(birthdate1); + const date2 = new Date(birthdate2); + + const age1 = new Date().getTime() - date1.getTime(); + const age2 = new Date().getTime() - date2.getTime(); + + return Math.abs(age1 - age2); +} \ No newline at end of file diff --git a/fastify/assets/test/integration/ws/browsing.filtersort.test.ts b/fastify/assets/test/integration/ws/browsing.filtersort.test.ts index 6d95e70..ced6b85 100644 --- a/fastify/assets/test/integration/ws/browsing.filtersort.test.ts +++ b/fastify/assets/test/integration/ws/browsing.filtersort.test.ts @@ -6,78 +6,8 @@ import { FastifyInstance } from 'fastify'; // import fixtures import { quickUser } from '../fixtures/auth.fixtures'; -const setTags = async (app: FastifyInstance, token: string, tags: Array) => { - await app.inject({ - method: 'PUT', - url: `/private/user/me/profile`, - headers: { - 'Cookie': `jwt=${token}` - }, - payload: { - tags: tags - } - }); -} - -const viewUser = async (app: FastifyInstance, token: string, likedUserId: number) => { - await app.inject({ - method: 'GET', - url: `/private/user/view/${likedUserId}`, - headers: { - 'Cookie': `jwt=${token}` - } - }); -}; - -const likeUser = async (app: FastifyInstance, token: string, likedUserId: number) => { - await app.inject({ - method: 'POST', - url: `/private/user/like/${likedUserId}`, - headers: { - 'Cookie': `jwt=${token}` - } - }); -}; - -const setLocalisation = async (app: FastifyInstance, token: string, lat: number, lgn: number) => { - await app.inject({ - method: 'PUT', - url: `/private/user/me/profile`, - headers: { - 'Cookie': `jwt=${token}` - }, - payload: { - location: { - latitude: lat, - longitude: lgn - } - } - }); -}; - -const setBirthDate = async (app: FastifyInstance, token: string, birthdate: string) => { - const response = await app.inject({ - method: 'PUT', - url: `/private/user/me/profile`, - headers: { - 'Cookie': `jwt=${token}` - }, - payload: { - bornAt: new Date(birthdate).toISOString() - } - }); - console.log(response.statusCode); -} - -const getAgeDifference = (birthdate1: string, birthdate2: string): number => { - const date1 = new Date(birthdate1); - const date2 = new Date(birthdate2); - - const age1 = new Date().getTime() - date1.getTime(); - const age2 = new Date().getTime() - date2.getTime(); - - return Math.abs(age1 - age2); -} +// import utils +import { setTags, setLocalisation, setBirthDate, likeUser, viewUser, getAgeDifference } from '../utils/browsing'; describe('Browsing filters and sorting', () => { let app: FastifyInstance; diff --git a/fastify/assets/test/integration/ws/browsing.test.ts b/fastify/assets/test/integration/ws/browsing.test.ts new file mode 100644 index 0000000..27fde47 --- /dev/null +++ b/fastify/assets/test/integration/ws/browsing.test.ts @@ -0,0 +1,83 @@ +import chai from 'chai'; +import { expect } from 'chai'; +import { buildApp } from '../../../srcs/app'; +import { FastifyInstance } from 'fastify'; + +// import fixtures +import { quickUser } from '../fixtures/auth.fixtures'; + +// import utils +import { setTags, setLocalisation, setBirthDate, likeUser, viewUser, getAgeDifference } from '../utils/browsing'; + +describe('Browsing filters and sorting', async () => { + let app: FastifyInstance; + + beforeEach(async () => { + app = buildApp(); + await app.ready(); + }); + + it('should be able get near users', async function (this: any) { + this.timeout(5000); + + const { userData: data1, token: token1 } = await quickUser(app); + const { userData: data2, token: token2 } = await quickUser(app); + + await setLocalisation(app, token1, 48.8566, 2.3522); + + await setLocalisation(app, token2, 48.94705, 2.3522); + + const rows = await app.browsingService.browseUsers(data1.id as number, 10, 0, 50); + + expect(rows.length).to.be.greaterThan(0); + }); + + it('should be able to give the best corresponding users', async function (this: any) { + this.timeout(5000); + const { userData: data1, token: token1 } = await quickUser(app); + + // Build my user profile + await setTags(app, token1, ['music', 'sport', 'travel', 'art']); + await setBirthDate(app, token1, '1995-06-15'); + await setLocalisation(app, token1, 69.8566, 2.3522); + + // Build corresponding users + const { userData: data2, token: token2 } = await quickUser(app); // #1 Perfect match + await setTags(app, token2, ['music', 'sport', 'travel']); + await setBirthDate(app, token2, '1994-08-20'); + await setLocalisation(app, token2, 69.8566, 2.3622); + + const { userData: data3, token: token3 } = await quickUser(app); // #2 Almost perfect match + await setTags(app, token3, ['music', 'art']); + await setBirthDate(app, token3, '1994-08-20'); + await setLocalisation(app, token3, 69.854, 2.5522); + + const { userData: data4, token: token4 } = await quickUser(app); // #3 Less tags + await setTags(app, token4, ['cooking']); + await setBirthDate(app, token4, '1996-11-05'); + await setLocalisation(app, token4, 69.9566, 2.3522); + + const { userData: data5, token: token5 } = await quickUser(app); // #4 Too far + await setTags(app, token5, ['music', 'sport', 'travel', 'art']); + await setBirthDate(app, token5, '1995-05-30'); + await setLocalisation(app, token5, 68.8566, 2.355); + + const { userData: data6, token: token6 } = await quickUser(app); // #6 No matching tags + await setTags(app, token6, ['cooking', 'reading', 'drug', 'gambling', 'alcohol']); + await setBirthDate(app, token6, '1960-03-22'); + await setLocalisation(app, token6, 69.86, 2.3522); + + const { userData: data7, token: token7 } = await quickUser(app); // #7 Too old + await setTags(app, token7, ['music', 'sport', 'travel', 'art']); + await setBirthDate(app, token7, '1970-03-22'); + await setLocalisation(app, token7, 69.8577, 2.3522); + + const rows = await app.browsingService.browseUsers(data1.id as number, 10, 0, 200, undefined); + console.log('Rows :', rows); + + expect(rows[0].id).to.equal(data2.id); + expect(rows[1].id).to.equal(data3.id); + expect(rows[2].id).to.equal(data4.id); + expect(rows[3].id).to.equal(data4.id); + }); +}); \ No newline at end of file From 69488ffbdab62afd0c94cf2a1460df29293f8813 Mon Sep 17 00:00:00 2001 From: YoungMame Date: Mon, 24 Nov 2025 13:22:19 +0100 Subject: [PATCH 07/10] fix:(classic usert borwsing) --- .../assets/srcs/services/BrowsingService.ts | 29 ++++++++++++++----- .../test/integration/ws/browsing.test.ts | 29 ++++++++++--------- 2 files changed, 38 insertions(+), 20 deletions(-) diff --git a/fastify/assets/srcs/services/BrowsingService.ts b/fastify/assets/srcs/services/BrowsingService.ts index 07ee684..56e7312 100644 --- a/fastify/assets/srcs/services/BrowsingService.ts +++ b/fastify/assets/srcs/services/BrowsingService.ts @@ -82,7 +82,17 @@ BETWEEN ${filters.age.min} AND ${filters.age.max} `, parameters ); - return result.rows.map(row => { + return result.rows.map((row: { + id: number; + first_name: string; + gender: string; + tags: Array; + fame_rate: number; + profile_pictures: Array; + profile_picture_index: number; + born_at: string; + distance: number; + }) => { const user = { id: row.id as number, firstName: row.first_name as string, @@ -127,16 +137,19 @@ BETWEEN ${filters.age.min} AND ${filters.age.max} private sortByAll(userRows: Array, bornAt: Date, userTags: Array, fameRate: number): Array { const ageWeight = 0.3; - const tagsWeight = 0.4; - const fameRateWeight = 0.3; + const tagsWeight = 0.2; + const fameRateWeight = 0.2; + const distanceWeight = 0.3; const maxAgeDiff = 10; // years const maxFameDiff = 400; + const maxDistance = 100; let scoreMap = new Map(); // index to score userRows.forEach(user => { const ageDiff = Math.abs(bornAt.getFullYear() - new Date(user.bornAt).getFullYear()); - const ageScore = 100 - (ageDiff / maxAgeDiff) * maxAgeDiff; + + const ageScore = 100 - (Math.min(ageDiff, maxAgeDiff) / maxAgeDiff) * 100; const similarTagsCount = this.getSimilarTagsCount(userTags, user.tags || []); const tagsScore = userTags.length > 0 ? (similarTagsCount / userTags.length) * 100 : 0; @@ -144,11 +157,13 @@ BETWEEN ${filters.age.min} AND ${filters.age.max} const fameRateDiff = Math.abs(fameRate - user.fameRate); const fameRateScore = 100 - (fameRateDiff / maxFameDiff) * maxFameDiff; - const totalScore = (ageScore * ageWeight) + (tagsScore * tagsWeight) + (fameRateScore * fameRateWeight); - console.log(`User ${user.id} - Age Score: ${ageScore.toFixed(2)}, Tags Score: ${tagsScore.toFixed(2)}, Fame Rate Score: ${fameRateScore.toFixed(2)}, Total Score: ${totalScore.toFixed(2)}`); + const distanceScore = 100 - (Math.min(user.distance, maxDistance) / maxDistance) * 100; + + const totalScore = (ageScore * ageWeight) + (tagsScore * tagsWeight) + (fameRateScore * fameRateWeight) + (distanceScore * distanceWeight); + console.log(`User ${user.id} - Age Score: ${ageScore.toFixed(2)}, Tags Score: ${tagsScore.toFixed(2)}, Fame Rate Score: ${fameRateScore.toFixed(2)}, Total Score: ${totalScore.toFixed(2)}, Distance Score: ${distanceScore.toFixed(2)}`); scoreMap.set(user.id, totalScore); }) - return userRows.sort((a, b) => (scoreMap.get(b.id) ?? 0) - (scoreMap.get(a.id) ?? 0)); + return userRows.sort((a, b) => (scoreMap.get(b.id) || 0) - (scoreMap.get(a.id) || 0)); } public async browseUsers(userId: number, limit: number = 5, offset: number = 0, radius: number = 25, filters?: BrowsingFilter, sort?: BrowsingSort): Promise> { diff --git a/fastify/assets/test/integration/ws/browsing.test.ts b/fastify/assets/test/integration/ws/browsing.test.ts index 27fde47..03f4142 100644 --- a/fastify/assets/test/integration/ws/browsing.test.ts +++ b/fastify/assets/test/integration/ws/browsing.test.ts @@ -53,24 +53,24 @@ describe('Browsing filters and sorting', async () => { await setLocalisation(app, token3, 69.854, 2.5522); const { userData: data4, token: token4 } = await quickUser(app); // #3 Less tags - await setTags(app, token4, ['cooking']); + await setTags(app, token4, ['music', 'dshsh', 'sdgfgd']); await setBirthDate(app, token4, '1996-11-05'); await setLocalisation(app, token4, 69.9566, 2.3522); - const { userData: data5, token: token5 } = await quickUser(app); // #4 Too far + const { userData: data5, token: token5 } = await quickUser(app); // #7 Too old await setTags(app, token5, ['music', 'sport', 'travel', 'art']); - await setBirthDate(app, token5, '1995-05-30'); - await setLocalisation(app, token5, 68.8566, 2.355); + await setBirthDate(app, token5, '1970-03-22'); + await setLocalisation(app, token5, 69.8577, 2.3522); - const { userData: data6, token: token6 } = await quickUser(app); // #6 No matching tags - await setTags(app, token6, ['cooking', 'reading', 'drug', 'gambling', 'alcohol']); - await setBirthDate(app, token6, '1960-03-22'); - await setLocalisation(app, token6, 69.86, 2.3522); + const { userData: data6, token: token6 } = await quickUser(app); // #4 Too far + await setTags(app, token6, ['cooking', 'reading', 'music']); + await setBirthDate(app, token6, '1995-05-30'); + await setLocalisation(app, token6, 68.8566, 2.955); - const { userData: data7, token: token7 } = await quickUser(app); // #7 Too old - await setTags(app, token7, ['music', 'sport', 'travel', 'art']); - await setBirthDate(app, token7, '1970-03-22'); - await setLocalisation(app, token7, 69.8577, 2.3522); + const { userData: data7, token: token7 } = await quickUser(app); // #5 No matching tags + far + old + await setTags(app, token7, ['cooking', 'reading', 'drug', 'gambling', 'alcohol']); + await setBirthDate(app, token7, '1960-03-22'); + await setLocalisation(app, token7, 69.86, 2.3522); const rows = await app.browsingService.browseUsers(data1.id as number, 10, 0, 200, undefined); console.log('Rows :', rows); @@ -78,6 +78,9 @@ describe('Browsing filters and sorting', async () => { expect(rows[0].id).to.equal(data2.id); expect(rows[1].id).to.equal(data3.id); expect(rows[2].id).to.equal(data4.id); - expect(rows[3].id).to.equal(data4.id); + expect(rows[3].id).to.equal(data5.id); + expect(rows[4].id).to.equal(data6.id); + expect(rows[4].id).to.equal(data6.id); + }); }); \ No newline at end of file From d06bb1ef0ad3665fa20c1a328da8f73779ccd1c3 Mon Sep 17 00:00:00 2001 From: YoungMame Date: Mon, 24 Nov 2025 14:50:32 +0100 Subject: [PATCH 08/10] add:([WIP]: browse users route) --- .../controllers/private/browsing/index.ts | 45 +++++++++++++ .../srcs/routes/private/browsing/index.ts | 65 +++++++++++++++++++ fastify/assets/srcs/routes/private/index.ts | 2 + .../assets/srcs/services/BrowsingService.ts | 1 - .../assets/test/integration/utils/browsing.ts | 1 - .../ws/browsing.filtersort.test.ts | 4 -- .../test/integration/ws/browsing.test.ts | 43 +++++++++--- 7 files changed, 145 insertions(+), 16 deletions(-) create mode 100644 fastify/assets/srcs/controllers/private/browsing/index.ts create mode 100644 fastify/assets/srcs/routes/private/browsing/index.ts diff --git a/fastify/assets/srcs/controllers/private/browsing/index.ts b/fastify/assets/srcs/controllers/private/browsing/index.ts new file mode 100644 index 0000000..0669972 --- /dev/null +++ b/fastify/assets/srcs/controllers/private/browsing/index.ts @@ -0,0 +1,45 @@ +import { FastifyRequest, FastifyReply } from 'fastify'; +import { AppError, UnauthorizedError } from '../../../utils/error'; +import { BrowsingFilter, BrowsingUser, BrowsingSort } from '../../../services/BrowsingService'; + +export const browseUsersHandler = async (request: FastifyRequest, reply: FastifyReply) => { + try { + const { + minAge, + maxAge, + minFame, + maxFame, + tags, + lat, + lng, + radius, + sortBy, + offset, + limit + } = request.params as { minAge: number, maxAge: number, minFame: number, maxFame: number, tags: string[], lat: number, lng: number, radius: number, sortBy: string, offset: number, limit: number }; + + if (!request.user?.id) + throw new UnauthorizedError(); + let requestFilters: BrowsingFilter = { + age: { min: minAge, max: maxAge }, + fameRate: { min: minFame, max: maxFame }, + tags: tags + }; + if (!(lat < -90 || lat > 90 || lng < -180 || lng > 180)) + requestFilters.location = { latitude: lat, longitude: lng }; + const users: BrowsingUser[] = await request.server.browsingService.browseUsers( + request.user.id, + limit || 20, + offset || 0, + radius, + requestFilters, + (sortBy ? sortBy as BrowsingSort : undefined) + ); + return reply.status(200).send({ users }); + } catch (error) { + if (error instanceof AppError) { + return reply.status(error.statusCode).send({ error: error.message }); + } + return reply.status(500).send({ error: 'Internal Server Error' }); + } +} \ No newline at end of file diff --git a/fastify/assets/srcs/routes/private/browsing/index.ts b/fastify/assets/srcs/routes/private/browsing/index.ts new file mode 100644 index 0000000..7790b1e --- /dev/null +++ b/fastify/assets/srcs/routes/private/browsing/index.ts @@ -0,0 +1,65 @@ +import { FastifyInstance } from 'fastify'; +import { browseUsersHandler } from '../../../controllers/private/browsing' + +export default async function browsingRoutes(fastify: FastifyInstance) { + fastify.get('/:minAge/:maxAge/:minFame/:maxFame/:tags/:lat/:lng/:radius/:sortBy', { + schema: { + params: { + type: 'object', + properties: { + minAge: { type: 'number', minimum: 18, maximum: 100 }, + maxAge: { type: 'number', minimum: 18, maximum: 100 }, + minFame: { type: 'number', minimum: 0, maximum: 1000 }, + maxFame: { type: 'number', minimum: 0, maximum: 1000 }, + tags: { type: 'array', items: { type: 'string' } }, + lat: { type: 'number' }, + lng: { type: 'number' }, + radius: { type: 'number', minimum: 0 }, + sortBy: { type: 'string', enum: ['distance', 'age', 'fameRate', 'tags', 'default'] }, + offset: { type: 'number', minimum: 0 }, + limit: { type: 'number', minimum: 1, maximum: 30 } + }, + required: ['minAge', 'maxAge', 'minFame', 'maxFame', 'tags', 'lat', 'lng', 'radius', 'sortBy'], + additionalProperties: false + }, + response: { + 200: { + type: 'object', + properties: { + users: { type: 'array', items: { + type: 'object', + properties: { + id: { type: 'number' }, + firstName: { type: 'string' }, + gender: { type: 'string' }, + tags: { type: 'array', items: { type: 'string' } }, + fameRate: { type: 'number' }, + profilePicture: { type: 'string' }, + bornAt: { type: 'string' }, + distance: { type: 'number' } + }, + required: ['id', 'firstName', 'gender', 'tags', 'fameRate', 'profilePicture', 'bornAt', 'distance'], + additionalProperties: false + } } + }, + additionalProperties: false + }, + 400: { + type: 'object', + properties: { + error: { type: 'string', } + }, + additionalProperties: false + }, + 500: { + type: 'object', + properties: { + error: { type: 'string' } + }, + additionalProperties: false + } + } + }, + handler: browseUsersHandler + }); +} \ No newline at end of file diff --git a/fastify/assets/srcs/routes/private/index.ts b/fastify/assets/srcs/routes/private/index.ts index eefbd08..3326b84 100644 --- a/fastify/assets/srcs/routes/private/index.ts +++ b/fastify/assets/srcs/routes/private/index.ts @@ -4,6 +4,7 @@ import userRoutes from './user'; import wsRoutes from './ws'; import notificationsRoutes from './notifications'; import reportRoutes from './report'; +import browsingRoutes from './browsing'; import statics from '@fastify/static'; import path from 'path'; @@ -17,6 +18,7 @@ export default async function privateRoutes(fastify: FastifyInstance, options: F fastify.register(userRoutes, { prefix: '/user' }); fastify.register(notificationsRoutes, { prefix: '/notifications' }); fastify.register(reportRoutes, { prefix: '/report', preHandler: fastify.checkIsCompleted }); + fastify.register(browsingRoutes, { prefix: '/browsing', preHandler: fastify.checkIsCompleted }); fastify.register(wsRoutes, { prefix: '/ws', preHandler: fastify.checkIsCompleted }); fastify.register(statics, { root: path.join(__dirname, '../../../uploads'), diff --git a/fastify/assets/srcs/services/BrowsingService.ts b/fastify/assets/srcs/services/BrowsingService.ts index 56e7312..a9599ed 100644 --- a/fastify/assets/srcs/services/BrowsingService.ts +++ b/fastify/assets/srcs/services/BrowsingService.ts @@ -127,7 +127,6 @@ BETWEEN ${filters.age.min} AND ${filters.age.max} } private sortByFameRate(userRows: Array): Array { - console.log('sortByFameRate called with:', userRows); return userRows.sort((a, b) => b.fameRate - a.fameRate); } diff --git a/fastify/assets/test/integration/utils/browsing.ts b/fastify/assets/test/integration/utils/browsing.ts index da2322c..de3212c 100644 --- a/fastify/assets/test/integration/utils/browsing.ts +++ b/fastify/assets/test/integration/utils/browsing.ts @@ -60,7 +60,6 @@ export const setBirthDate = async (app: FastifyInstance, token: string, birthdat bornAt: new Date(birthdate).toISOString() } }); - console.log(response.statusCode); } export const getAgeDifference = (birthdate1: string, birthdate2: string): number => { diff --git a/fastify/assets/test/integration/ws/browsing.filtersort.test.ts b/fastify/assets/test/integration/ws/browsing.filtersort.test.ts index ced6b85..b4c5e7f 100644 --- a/fastify/assets/test/integration/ws/browsing.filtersort.test.ts +++ b/fastify/assets/test/integration/ws/browsing.filtersort.test.ts @@ -155,7 +155,6 @@ describe('Browsing filters and sorting', () => { await setTags(app, token5, ['gaming', 'photography', 'art']); const rows = await app.browsingService.browseUsers(data1.id as number, 10, 0, 50, undefined, 'tags'); - console.log(rows); for (let i = 0; i < rows.length - 1; i++) { const countCurrent = rows[i].tags.filter((tag: string) => ['music', 'sports', 'art'].includes(tag)).length; @@ -186,7 +185,6 @@ describe('Browsing filters and sorting', () => { await setTags(app, token5, ['gaming', 'photography', 'gambling']); const rows = await app.browsingService.browseUsers(data1.id as number, 10, 0, 50, { tags: ['music'] }); - console.log(rows); for (let i = 0; i < rows.length - 1; i++) { expect(rows[i].tags).to.include('music'); @@ -225,7 +223,6 @@ describe('Browsing filters and sorting', () => { await setBirthDate(app, token5, less33YearsAgo.toISOString()); const rows = await app.browsingService.browseUsers(data1.id as number, 10, 0, 50, { age: { min: 34, max: 39 } }); - console.log(rows); for (let i = 0; i < rows.length - 1; i++) { const age = new Date().getFullYear() - new Date(rows[i].bornAt).getFullYear(); @@ -266,7 +263,6 @@ describe('Browsing filters and sorting', () => { await setBirthDate(app, token5, less33YearsAgo.toISOString()); const rows = await app.browsingService.browseUsers(data1.id as number, 10, 0, 50, { age: { min: 34, max: 39 } }); - console.log(rows); for (let i = 0; i < rows.length - 1; i++) { const age = new Date().getFullYear() - new Date(rows[i].bornAt).getFullYear(); diff --git a/fastify/assets/test/integration/ws/browsing.test.ts b/fastify/assets/test/integration/ws/browsing.test.ts index 03f4142..ff8b580 100644 --- a/fastify/assets/test/integration/ws/browsing.test.ts +++ b/fastify/assets/test/integration/ws/browsing.test.ts @@ -9,6 +9,20 @@ import { quickUser } from '../fixtures/auth.fixtures'; // import utils import { setTags, setLocalisation, setBirthDate, likeUser, viewUser, getAgeDifference } from '../utils/browsing'; +async function browseUsers(app: FastifyInstance, token: string, params: any) { + const url = `/private/browsing/${params.minAge || 18}/${params.maxAge || 100}/${params.minFame || 0}/${params.maxFame || 1000}/${(params.tags || []).join(',')}/${params.lat || 1000}/${params.lng || 1000}/${params.radius || 30}/${params.sortBy || 'default'}`; + const response = await app.inject({ + method: 'GET', + headers: { + Cookie: `jwt=${token}` + }, + url: url, + }); + console.log('Url:', url); + console.log('Browse users response:', response.body); + return JSON.parse(response.body).users; +} + describe('Browsing filters and sorting', async () => { let app: FastifyInstance; @@ -72,15 +86,24 @@ describe('Browsing filters and sorting', async () => { await setBirthDate(app, token7, '1960-03-22'); await setLocalisation(app, token7, 69.86, 2.3522); - const rows = await app.browsingService.browseUsers(data1.id as number, 10, 0, 200, undefined); - console.log('Rows :', rows); - - expect(rows[0].id).to.equal(data2.id); - expect(rows[1].id).to.equal(data3.id); - expect(rows[2].id).to.equal(data4.id); - expect(rows[3].id).to.equal(data5.id); - expect(rows[4].id).to.equal(data6.id); - expect(rows[4].id).to.equal(data6.id); - + const users = await browseUsers(app, token1, { + minAge: 18, + maxAge: 40, + minFame: 0, + maxFame: 1000, + tags: ['music', 'sport', 'travel', 'art'], + lat: 69.8566, + lng: 2.3522, + radius: 100, + sortBy: 'default' + }); + console.log('Rows :', users); + + expect(users[0].id).to.equal(data2.id); + expect(users[1].id).to.equal(data3.id); + expect(users[2].id).to.equal(data4.id); + expect(users[3].id).to.equal(data5.id); + expect(users[4].id).to.equal(data6.id); + expect(users[5].id).to.equal(data7.id); }); }); \ No newline at end of file From 34a89abc772e77a27b525c92015eaee4c00a2f61 Mon Sep 17 00:00:00 2001 From: YoungMame Date: Mon, 24 Nov 2025 15:25:40 +0100 Subject: [PATCH 09/10] + --- .../srcs/routes/private/browsing/index.ts | 2 +- .../assets/srcs/services/BrowsingService.ts | 18 +++++++++++++----- 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/fastify/assets/srcs/routes/private/browsing/index.ts b/fastify/assets/srcs/routes/private/browsing/index.ts index 7790b1e..99a79cd 100644 --- a/fastify/assets/srcs/routes/private/browsing/index.ts +++ b/fastify/assets/srcs/routes/private/browsing/index.ts @@ -11,7 +11,7 @@ export default async function browsingRoutes(fastify: FastifyInstance) { maxAge: { type: 'number', minimum: 18, maximum: 100 }, minFame: { type: 'number', minimum: 0, maximum: 1000 }, maxFame: { type: 'number', minimum: 0, maximum: 1000 }, - tags: { type: 'array', items: { type: 'string' } }, + tags: { type: 'string' }, // comma separated tags lat: { type: 'number' }, lng: { type: 'number' }, radius: { type: 'number', minimum: 0 }, diff --git a/fastify/assets/srcs/services/BrowsingService.ts b/fastify/assets/srcs/services/BrowsingService.ts index a9599ed..56adef8 100644 --- a/fastify/assets/srcs/services/BrowsingService.ts +++ b/fastify/assets/srcs/services/BrowsingService.ts @@ -168,14 +168,22 @@ BETWEEN ${filters.age.min} AND ${filters.age.max} public async browseUsers(userId: number, limit: number = 5, offset: number = 0, radius: number = 25, filters?: BrowsingFilter, sort?: BrowsingSort): Promise> { const user = await this.fastify.userService.getMe(userId); const lat = filters?.location?.latitude ?? user.location?.latitude; - const lgn = filters?.location?.longitude ?? user.location?.longitude; + const lng = filters?.location?.longitude ?? user.location?.longitude; + if (filters?.tags && filters.tags.length === 0) { + delete filters.tags; + } const bornAt = user.bornAt; - const tags = user.tags; const fameRate = user.fameRate; - - if (lat === undefined || lgn === undefined) + const tags = user.tags; + + if (lat === undefined || lng === undefined) throw new BadRequestError(); - const userRows = await this.getUsersFromCoordsAndRadius(userId, lat, lgn, limit, offset, radius, filters); + console.log('Browsing users with filters:', filters,); + console.log('Lat and Lgn:', lat, lng); + console.log('Radius:', radius); + console.log('Sort by:', sort); + console.log('Limit and Offset:', limit, offset); + const userRows = await this.getUsersFromCoordsAndRadius(userId, lat, lng, limit, offset, radius, filters); switch (sort) { case 'distance': return this.sortByDistance(userRows); From 7da608152925c2094e56e0703484ba2d7bb80046 Mon Sep 17 00:00:00 2001 From: YoungMame Date: Tue, 25 Nov 2025 07:17:10 +0100 Subject: [PATCH 10/10] add:(orientation specification in browsing) --- .../controllers/private/browsing/index.ts | 5 +- .../assets/srcs/services/BrowsingService.ts | 18 +++--- .../integration/fixtures/auth.fixtures.ts | 2 +- .../test/integration/ws/browsing.test.ts | 57 ++++++++++++++----- 4 files changed, 58 insertions(+), 24 deletions(-) diff --git a/fastify/assets/srcs/controllers/private/browsing/index.ts b/fastify/assets/srcs/controllers/private/browsing/index.ts index 0669972..97b2f32 100644 --- a/fastify/assets/srcs/controllers/private/browsing/index.ts +++ b/fastify/assets/srcs/controllers/private/browsing/index.ts @@ -16,14 +16,15 @@ export const browseUsersHandler = async (request: FastifyRequest, reply: Fastify sortBy, offset, limit - } = request.params as { minAge: number, maxAge: number, minFame: number, maxFame: number, tags: string[], lat: number, lng: number, radius: number, sortBy: string, offset: number, limit: number }; + } = request.params as { minAge: number, maxAge: number, minFame: number, maxFame: number, tags: string, lat: number, lng: number, radius: number, sortBy: string, offset: number, limit: number }; if (!request.user?.id) throw new UnauthorizedError(); + const tagsArray = tags ? tags.split(',') : []; let requestFilters: BrowsingFilter = { age: { min: minAge, max: maxAge }, fameRate: { min: minFame, max: maxFame }, - tags: tags + tags: tagsArray }; if (!(lat < -90 || lat > 90 || lng < -180 || lng > 180)) requestFilters.location = { latitude: lat, longitude: lng }; diff --git a/fastify/assets/srcs/services/BrowsingService.ts b/fastify/assets/srcs/services/BrowsingService.ts index 56adef8..4583d09 100644 --- a/fastify/assets/srcs/services/BrowsingService.ts +++ b/fastify/assets/srcs/services/BrowsingService.ts @@ -54,7 +54,7 @@ class BrowsingService { this.UsersCache = new Map(); } - private async getUsersFromCoordsAndRadius(userId: number, lat: number, lgn: number, limit: number, offset: number, radius: number, filters?: BrowsingFilter): Promise> { + private async getUsersFromCoordsAndRadius(userId: number, lat: number, lgn: number, limit: number, offset: number, radius: number, gender?: string, filters?: BrowsingFilter): Promise> { let parameters: Array> = [lat, lgn, radius, userId, limit, offset]; if (filters?.tags && filters.tags.length > 0) { parameters.push(filters.tags); @@ -78,6 +78,7 @@ BETWEEN ${filters.age.min} AND ${filters.age.max} ` : ''} ${filters?.fameRate ? `AND u.fame_rate BETWEEN ${filters.fameRate.min} AND ${filters.fameRate.max}` : ''} ${filters?.tags && filters.tags.length > 0 ? `AND u.tags @> $7::text[]` : ''} + ${gender ? `AND u.gender = '${gender}'` : ''} LIMIT $5 OFFSET $6 `, parameters @@ -159,7 +160,7 @@ BETWEEN ${filters.age.min} AND ${filters.age.max} const distanceScore = 100 - (Math.min(user.distance, maxDistance) / maxDistance) * 100; const totalScore = (ageScore * ageWeight) + (tagsScore * tagsWeight) + (fameRateScore * fameRateWeight) + (distanceScore * distanceWeight); - console.log(`User ${user.id} - Age Score: ${ageScore.toFixed(2)}, Tags Score: ${tagsScore.toFixed(2)}, Fame Rate Score: ${fameRateScore.toFixed(2)}, Total Score: ${totalScore.toFixed(2)}, Distance Score: ${distanceScore.toFixed(2)}`); + // console.log(`User ${user.id} - Age Score: ${ageScore.toFixed(2)}, Tags Score: ${tagsScore.toFixed(2)}, Fame Rate Score: ${fameRateScore.toFixed(2)}, Total Score: ${totalScore.toFixed(2)}, Distance Score: ${distanceScore.toFixed(2)}`); scoreMap.set(user.id, totalScore); }) return userRows.sort((a, b) => (scoreMap.get(b.id) || 0) - (scoreMap.get(a.id) || 0)); @@ -178,12 +179,13 @@ BETWEEN ${filters.age.min} AND ${filters.age.max} if (lat === undefined || lng === undefined) throw new BadRequestError(); - console.log('Browsing users with filters:', filters,); - console.log('Lat and Lgn:', lat, lng); - console.log('Radius:', radius); - console.log('Sort by:', sort); - console.log('Limit and Offset:', limit, offset); - const userRows = await this.getUsersFromCoordsAndRadius(userId, lat, lng, limit, offset, radius, filters); + let gender: string | undefined = undefined; + if (user.orientation === 'heterosexual') { + gender = user.gender === 'men' ? 'women' : 'men'; + } else if (user.orientation === 'homosexual') { + gender = user.gender; + } + const userRows = await this.getUsersFromCoordsAndRadius(userId, lat, lng, limit, offset, radius, gender, filters); switch (sort) { case 'distance': return this.sortByDistance(userRows); diff --git a/fastify/assets/test/integration/fixtures/auth.fixtures.ts b/fastify/assets/test/integration/fixtures/auth.fixtures.ts index 8b618df..58b6af2 100644 --- a/fastify/assets/test/integration/fixtures/auth.fixtures.ts +++ b/fastify/assets/test/integration/fixtures/auth.fixtures.ts @@ -94,7 +94,7 @@ export const quickUser = async (app: FastifyInstance): Promise<{ userData: UserD bio: `Nice description for quick user ${concat}`, tags: ['quick', 'test', 'user'], bornAt: '2000-01-01', - orientation: 'heterosexual', + orientation: 'bisexual', gender: 'men' }; const token = await signUpAndGetToken(app, userData); diff --git a/fastify/assets/test/integration/ws/browsing.test.ts b/fastify/assets/test/integration/ws/browsing.test.ts index ff8b580..713773d 100644 --- a/fastify/assets/test/integration/ws/browsing.test.ts +++ b/fastify/assets/test/integration/ws/browsing.test.ts @@ -4,10 +4,14 @@ import { buildApp } from '../../../srcs/app'; import { FastifyInstance } from 'fastify'; // import fixtures -import { quickUser } from '../fixtures/auth.fixtures'; +import { quickUser, signUpAndGetToken } from '../fixtures/auth.fixtures'; // import utils import { setTags, setLocalisation, setBirthDate, likeUser, viewUser, getAgeDifference } from '../utils/browsing'; +import path from 'node:path'; +import fs from 'node:fs'; +import FormData from 'form-data'; +import { create } from 'node:domain'; async function browseUsers(app: FastifyInstance, token: string, params: any) { const url = `/private/browsing/${params.minAge || 18}/${params.maxAge || 100}/${params.minFame || 0}/${params.maxFame || 1000}/${(params.tags || []).join(',')}/${params.lat || 1000}/${params.lng || 1000}/${params.radius || 30}/${params.sortBy || 'default'}`; @@ -18,11 +22,45 @@ async function browseUsers(app: FastifyInstance, token: string, params: any) { }, url: url, }); - console.log('Url:', url); - console.log('Browse users response:', response.body); return JSON.parse(response.body).users; } +async function createUserWithProfile(app: FastifyInstance, username: string, email: string, password: string, firstName: string, lastName: string, bio: string, tags: string[], bornAt: string, orientation: string, gender: string): Promise { + const token = await signUpAndGetToken(app, { + username: username, + email: email, + password: password, + firstName: firstName, + lastName: lastName, + bio: bio, + tags: tags, + bornAt: bornAt, + orientation: orientation, + gender: gender + }); + if (!token) throw new Error('Failed to create browsing test user 1'); + await setLocalisation(app, token, 69.8566, 2.3522); + + const form = new FormData(); + const filePath = path.join(__dirname, '../../files/test.jpg'); + form.append('file', fs.createReadStream(filePath), { + filename: 'test.jpg', + contentType: 'image/jpeg' + }); + + const headers = form.getHeaders(); + headers['Cookie'] = `jwt=${token}`; + + await app.inject({ + method: 'POST', + url: '/private/user/me/profile-picture', + headers, + payload: form + }); + + return token; +} + describe('Browsing filters and sorting', async () => { let app: FastifyInstance; @@ -48,12 +86,7 @@ describe('Browsing filters and sorting', async () => { it('should be able to give the best corresponding users', async function (this: any) { this.timeout(5000); - const { userData: data1, token: token1 } = await quickUser(app); - - // Build my user profile - await setTags(app, token1, ['music', 'sport', 'travel', 'art']); - await setBirthDate(app, token1, '1995-06-15'); - await setLocalisation(app, token1, 69.8566, 2.3522); + const token1 = await createUserWithProfile(app, 'browsingtestuser1', 'browsingtestuser1@gmail.com', 'Test@1234!fjfsfas', 'Browsing', 'TestUser1', 'I am browsing test user 1', ['music', 'sport', 'travel', 'art'], '1995-06-15', 'heterosexual', 'women'); // Build corresponding users const { userData: data2, token: token2 } = await quickUser(app); // #1 Perfect match @@ -88,16 +121,14 @@ describe('Browsing filters and sorting', async () => { const users = await browseUsers(app, token1, { minAge: 18, - maxAge: 40, + maxAge: 100, minFame: 0, maxFame: 1000, - tags: ['music', 'sport', 'travel', 'art'], lat: 69.8566, lng: 2.3522, - radius: 100, + radius: 300, sortBy: 'default' }); - console.log('Rows :', users); expect(users[0].id).to.equal(data2.id); expect(users[1].id).to.equal(data3.id);