580 lines
25 KiB
TypeScript
580 lines
25 KiB
TypeScript
import { v4 as uuidv4 } from 'uuid';
|
||
import { query, transaction } from '../database/connection';
|
||
import { Swipe, SwipeData, SwipeType } from '../models/Swipe';
|
||
import { Match, MatchData } from '../models/Match';
|
||
import { Profile } from '../models/Profile';
|
||
import { ProfileService } from './profileService';
|
||
import { NotificationService } from './notificationService';
|
||
import { BotError } from '../types';
|
||
|
||
export class MatchingService {
|
||
private profileService: ProfileService;
|
||
private notificationService: NotificationService;
|
||
|
||
constructor() {
|
||
this.profileService = new ProfileService();
|
||
this.notificationService = new NotificationService();
|
||
}
|
||
|
||
// Выполнить свайп
|
||
|
||
async performSwipe(telegramId: string, targetTelegramId: string, swipeType: SwipeType): Promise<{
|
||
swipe: Swipe;
|
||
isMatch: boolean;
|
||
match?: Match;
|
||
}> {
|
||
|
||
// Получить профили пользователей
|
||
const userProfile = await this.profileService.getProfileByTelegramId(telegramId);
|
||
const targetProfile = await this.profileService.getProfileByTelegramId(targetTelegramId); if (!userProfile || !targetProfile) {
|
||
throw new BotError('Profile not found', 'PROFILE_NOT_FOUND', 400);
|
||
}
|
||
|
||
const userId = userProfile.userId;
|
||
const targetUserId = targetProfile.userId;
|
||
|
||
// Проверяем, что пользователь не свайпает сам себя
|
||
if (userId === targetUserId) {
|
||
throw new BotError('Cannot swipe yourself', 'INVALID_SWIPE');
|
||
}
|
||
|
||
// Проверяем, что свайп еще не был сделан
|
||
const existingSwipe = await this.getSwipe(userId, targetUserId);
|
||
if (existingSwipe) {
|
||
throw new BotError('Already swiped this profile', 'ALREADY_SWIPED');
|
||
}
|
||
|
||
const swipeId = uuidv4();
|
||
let isMatch = false;
|
||
let match: Match | undefined;
|
||
|
||
await transaction(async (client) => {
|
||
// Создаем свайп
|
||
await client.query(`
|
||
INSERT INTO swipes (id, user_id, target_user_id, type, created_at)
|
||
VALUES ($1, $2, $3, $4, $5)
|
||
`, [swipeId, userId, targetUserId, swipeType, new Date()]);
|
||
|
||
// Если это лайк или суперлайк, проверяем взаимность
|
||
if (swipeType === 'like' || swipeType === 'superlike') {
|
||
const reciprocalSwipe = await client.query(`
|
||
SELECT * FROM swipes
|
||
WHERE user_id = $1 AND target_user_id = $2 AND type IN ('like', 'superlike')
|
||
`, [targetUserId, userId]);
|
||
|
||
if (reciprocalSwipe.rows.length > 0) {
|
||
// Проверяем, что матч еще не существует
|
||
const existingMatch = await client.query(`
|
||
SELECT * FROM matches
|
||
WHERE (user_id_1 = $1 AND user_id_2 = $2) OR (user_id_1 = $2 AND user_id_2 = $1)
|
||
`, [userId, targetUserId]);
|
||
|
||
if (existingMatch.rows.length === 0) {
|
||
isMatch = true;
|
||
const matchId = uuidv4();
|
||
const isSuperMatch = swipeType === 'superlike' || reciprocalSwipe.rows[0].type === 'superlike';
|
||
|
||
// Упорядочиваем пользователей для консистентности
|
||
const [user1Id, user2Id] = userId < targetUserId ? [userId, targetUserId] : [targetUserId, userId];
|
||
|
||
// Создаем матч
|
||
await client.query(`
|
||
INSERT INTO matches (id, user_id_1, user_id_2, created_at, is_active)
|
||
VALUES ($1, $2, $3, $4, $5)
|
||
`, [matchId, user1Id, user2Id, new Date(), true]);
|
||
|
||
match = new Match({
|
||
id: matchId,
|
||
userId1: user1Id,
|
||
userId2: user2Id,
|
||
createdAt: new Date(),
|
||
isActive: true,
|
||
isSuperMatch: isSuperMatch,
|
||
unreadCount1: 0,
|
||
unreadCount2: 0
|
||
});
|
||
}
|
||
}
|
||
}
|
||
});
|
||
|
||
const swipe = new Swipe({
|
||
id: swipeId,
|
||
userId,
|
||
targetUserId,
|
||
type: swipeType,
|
||
timestamp: new Date(),
|
||
isMatch
|
||
});
|
||
|
||
// Отправляем уведомления
|
||
if (swipeType === 'like' || swipeType === 'superlike') {
|
||
this.notificationService.sendLikeNotification(targetTelegramId, telegramId, swipeType === 'superlike');
|
||
}
|
||
|
||
if (isMatch && match) {
|
||
this.notificationService.sendMatchNotification(userId, targetUserId);
|
||
this.notificationService.sendMatchNotification(targetUserId, userId);
|
||
}
|
||
|
||
return { swipe, isMatch, match };
|
||
}
|
||
|
||
// Получить свайп между двумя пользователями
|
||
async getSwipe(userId: string, targetUserId: string): Promise<Swipe | null> {
|
||
const result = await query(`
|
||
SELECT * FROM swipes
|
||
WHERE user_id = $1 AND target_user_id = $2
|
||
`, [userId, targetUserId]);
|
||
|
||
if (result.rows.length === 0) {
|
||
return null;
|
||
}
|
||
|
||
return this.mapEntityToSwipe(result.rows[0]);
|
||
}
|
||
|
||
// Получить свайп между двумя пользователями (псевдоним для getSwipe)
|
||
async getSwipeBetweenUsers(userId: string, targetUserId: string): Promise<Swipe | null> {
|
||
return this.getSwipe(userId, targetUserId);
|
||
}
|
||
|
||
// Создать свайп (лайк, дислайк или суперлайк)
|
||
async createSwipe(userId: string, targetUserId: string, swipeType: SwipeType): Promise<{
|
||
swipe: Swipe;
|
||
isMatch: boolean;
|
||
match?: Match;
|
||
}> {
|
||
const swipeId = uuidv4();
|
||
let isMatch = false;
|
||
let match: Match | undefined;
|
||
|
||
await transaction(async (client) => {
|
||
// Создаем свайп
|
||
await client.query(`
|
||
INSERT INTO swipes (id, user_id, target_user_id, type, created_at)
|
||
VALUES ($1, $2, $3, $4, $5)
|
||
`, [swipeId, userId, targetUserId, swipeType, new Date()]);
|
||
|
||
// Если это лайк или суперлайк, проверяем взаимность
|
||
if (swipeType === 'like' || swipeType === 'superlike') {
|
||
const reciprocalSwipe = await client.query(`
|
||
SELECT * FROM swipes
|
||
WHERE user_id = $1 AND target_user_id = $2 AND type IN ('like', 'superlike')
|
||
`, [targetUserId, userId]);
|
||
|
||
if (reciprocalSwipe.rows.length > 0) {
|
||
// Проверяем, что матч еще не существует
|
||
const existingMatch = await client.query(`
|
||
SELECT * FROM matches
|
||
WHERE (user_id_1 = $1 AND user_id_2 = $2) OR (user_id_1 = $2 AND user_id_2 = $1)
|
||
`, [userId, targetUserId]);
|
||
|
||
if (existingMatch.rows.length === 0) {
|
||
isMatch = true;
|
||
const matchId = uuidv4();
|
||
const isSuperMatch = swipeType === 'superlike' || reciprocalSwipe.rows[0].type === 'superlike';
|
||
|
||
// Упорядочиваем пользователей для консистентности
|
||
const [user1Id, user2Id] = userId < targetUserId ? [userId, targetUserId] : [targetUserId, userId];
|
||
|
||
// Создаем матч
|
||
await client.query(`
|
||
INSERT INTO matches (id, user_id_1, user_id_2, created_at, is_active, is_super_match)
|
||
VALUES ($1, $2, $3, $4, $5, $6)
|
||
`, [matchId, user1Id, user2Id, new Date(), true, isSuperMatch]);
|
||
|
||
match = new Match({
|
||
id: matchId,
|
||
userId1: user1Id,
|
||
userId2: user2Id,
|
||
createdAt: new Date(),
|
||
isActive: true,
|
||
isSuperMatch: isSuperMatch,
|
||
unreadCount1: 0,
|
||
unreadCount2: 0
|
||
});
|
||
|
||
// Обновляем свайпы, отмечая что они образуют матч
|
||
await client.query(`
|
||
UPDATE swipes SET is_match = true
|
||
WHERE (user_id = $1 AND target_user_id = $2) OR (user_id = $2 AND target_user_id = $1)
|
||
`, [userId, targetUserId]);
|
||
}
|
||
}
|
||
}
|
||
});
|
||
|
||
const swipe = new Swipe({
|
||
id: swipeId,
|
||
userId,
|
||
targetUserId,
|
||
type: swipeType,
|
||
timestamp: new Date(),
|
||
isMatch
|
||
});
|
||
|
||
return { swipe, isMatch, match };
|
||
}
|
||
|
||
// Получить все матчи пользователя по telegram ID
|
||
async getUserMatches(telegramId: string, limit: number = 50): Promise<Match[]> {
|
||
// Сначала получаем userId по telegramId
|
||
const userId = await this.profileService.getUserIdByTelegramId(telegramId);
|
||
if (!userId) {
|
||
return [];
|
||
}
|
||
|
||
const result = await query(`
|
||
SELECT * FROM matches
|
||
WHERE (user_id_1 = $1 OR user_id_2 = $1) AND is_active = true
|
||
ORDER BY created_at DESC
|
||
LIMIT $2
|
||
`, [userId, limit]);
|
||
|
||
return result.rows.map((row: any) => this.mapEntityToMatch(row));
|
||
}
|
||
|
||
// Получить матч по ID
|
||
async getMatchById(matchId: string): Promise<Match | null> {
|
||
const result = await query(`
|
||
SELECT * FROM matches WHERE id = $1
|
||
`, [matchId]);
|
||
|
||
if (result.rows.length === 0) {
|
||
return null;
|
||
}
|
||
|
||
return this.mapEntityToMatch(result.rows[0]);
|
||
}
|
||
|
||
// Получить матч между двумя пользователями
|
||
async getMatchBetweenUsers(userId1: string, userId2: string): Promise<Match | null> {
|
||
const result = await query(`
|
||
SELECT * FROM matches
|
||
WHERE ((user_id_1 = $1 AND user_id_2 = $2) OR (user_id_1 = $2 AND user_id_2 = $1))
|
||
AND is_active = true
|
||
`, [userId1, userId2]);
|
||
|
||
if (result.rows.length === 0) {
|
||
return null;
|
||
}
|
||
|
||
return this.mapEntityToMatch(result.rows[0]);
|
||
}
|
||
|
||
// Размэтчить (деактивировать матч)
|
||
async unmatch(userId: string, matchId: string): Promise<boolean> {
|
||
const match = await this.getMatchById(matchId);
|
||
if (!match || !match.includesUser(userId)) {
|
||
throw new BotError('Match not found or access denied', 'MATCH_NOT_FOUND');
|
||
}
|
||
|
||
await query(`
|
||
UPDATE matches SET is_active = false WHERE id = $1
|
||
`, [matchId]);
|
||
|
||
return true;
|
||
}
|
||
|
||
// Получить недавние лайки
|
||
async getRecentLikes(userId: string, limit: number = 20): Promise<Swipe[]> {
|
||
const result = await query(`
|
||
SELECT * FROM swipes
|
||
WHERE target_user_id = $1 AND type IN ('like', 'superlike') AND is_match = false
|
||
ORDER BY created_at DESC
|
||
LIMIT $2
|
||
`, [userId, limit]);
|
||
|
||
return result.rows.map((row: any) => this.mapEntityToSwipe(row));
|
||
}
|
||
|
||
// Получить статистику свайпов пользователя за день
|
||
async getDailySwipeStats(userId: string): Promise<{
|
||
likes: number;
|
||
superlikes: number;
|
||
passes: number;
|
||
total: number;
|
||
}> {
|
||
const today = new Date();
|
||
today.setHours(0, 0, 0, 0);
|
||
|
||
const result = await query(`
|
||
SELECT type, COUNT(*) as count
|
||
FROM swipes
|
||
WHERE user_id = $1 AND created_at >= $2
|
||
GROUP BY type
|
||
`, [userId, today]);
|
||
|
||
const stats = {
|
||
likes: 0,
|
||
superlikes: 0,
|
||
passes: 0,
|
||
total: 0
|
||
};
|
||
|
||
result.rows.forEach((row: any) => {
|
||
const count = parseInt(row.count);
|
||
stats.total += count;
|
||
|
||
switch (row.type) {
|
||
case 'like':
|
||
stats.likes = count;
|
||
break;
|
||
case 'superlike':
|
||
stats.superlikes = count;
|
||
break;
|
||
case 'pass':
|
||
stats.passes = count;
|
||
break;
|
||
}
|
||
});
|
||
|
||
return stats;
|
||
}
|
||
|
||
// Проверить лимиты свайпов
|
||
async checkSwipeLimits(userId: string): Promise<{
|
||
canLike: boolean;
|
||
canSuperLike: boolean;
|
||
likesLeft: number;
|
||
superLikesLeft: number;
|
||
}> {
|
||
const stats = await this.getDailySwipeStats(userId);
|
||
|
||
const likesPerDay = 100; // Из конфига
|
||
const superLikesPerDay = 1; // Из конфига
|
||
|
||
return {
|
||
canLike: stats.likes < likesPerDay,
|
||
canSuperLike: stats.superlikes < superLikesPerDay,
|
||
likesLeft: Math.max(0, likesPerDay - stats.likes),
|
||
superLikesLeft: Math.max(0, superLikesPerDay - stats.superlikes)
|
||
};
|
||
}
|
||
|
||
// Получить рекомендации для пользователя
|
||
async getRecommendations(userId: string, limit: number = 10): Promise<string[]> {
|
||
return this.profileService.findCompatibleProfiles(userId, limit)
|
||
.then(profiles => profiles.map(p => p.userId));
|
||
}
|
||
|
||
// Преобразование entity в модель Swipe
|
||
private mapEntityToSwipe(entity: any): Swipe {
|
||
return new Swipe({
|
||
id: entity.id,
|
||
userId: entity.user_id || entity.swiper_id,
|
||
targetUserId: entity.target_user_id || entity.swiped_id,
|
||
type: entity.type || 'pass',
|
||
timestamp: entity.created_at,
|
||
isMatch: entity.is_match
|
||
});
|
||
}
|
||
|
||
// Преобразование entity в модель Match
|
||
private mapEntityToMatch(entity: any): Match {
|
||
return new Match({
|
||
id: entity.id,
|
||
userId1: entity.user_id_1,
|
||
userId2: entity.user_id_2,
|
||
createdAt: entity.created_at,
|
||
lastMessageAt: entity.last_message_at,
|
||
isActive: entity.is_active === true,
|
||
isSuperMatch: false, // Определяется из swipes если нужно
|
||
unreadCount1: 0,
|
||
unreadCount2: 0
|
||
});
|
||
}
|
||
|
||
// Получить взаимные лайки (потенциальные матчи)
|
||
async getMutualLikes(userId: string): Promise<string[]> {
|
||
const result = await query(`
|
||
SELECT DISTINCT s1.target_user_id
|
||
FROM swipes s1
|
||
JOIN swipes s2 ON s1.user_id = s2.target_user_id AND s1.target_user_id = s2.user_id
|
||
WHERE s1.user_id = $1
|
||
AND s1.type IN ('like', 'superlike')
|
||
AND s2.type IN ('like', 'superlike')
|
||
AND NOT EXISTS (
|
||
SELECT 1 FROM matches m
|
||
WHERE (m.user_id_1 = s1.user_id AND m.user_id_2 = s1.target_user_id)
|
||
OR (m.user_id_1 = s1.target_user_id AND m.user_id_2 = s1.user_id)
|
||
)
|
||
`, [userId]);
|
||
|
||
return result.rows.map((row: any) => row.target_user_id);
|
||
}
|
||
|
||
// Получить следующего кандидата для просмотра
|
||
async getNextCandidate(telegramId: string, isNewUser: boolean = false): Promise<Profile | null> {
|
||
console.log(`[DEBUG] getNextCandidate вызван для telegramId=${telegramId}, isNewUser=${isNewUser}`);
|
||
|
||
// Сначала получаем профиль пользователя по telegramId
|
||
const userProfile = await this.profileService.getProfileByTelegramId(telegramId);
|
||
if (!userProfile) {
|
||
console.log(`[ERROR] Профиль пользователя с telegramId=${telegramId} не найден`);
|
||
throw new BotError('User profile not found', 'PROFILE_NOT_FOUND');
|
||
}
|
||
console.log(`[DEBUG] Найден профиль пользователя:`, JSON.stringify({
|
||
userId: userProfile.userId,
|
||
gender: userProfile.gender,
|
||
interestedIn: userProfile.interestedIn,
|
||
minAge: userProfile.searchPreferences?.minAge,
|
||
maxAge: userProfile.searchPreferences?.maxAge
|
||
}));
|
||
|
||
// Получаем UUID пользователя
|
||
const userId = userProfile.userId;
|
||
|
||
// Определяем, каким должен быть пол показываемых профилей
|
||
let targetGender: string;
|
||
if (userProfile.interestedIn === 'male' || userProfile.interestedIn === 'female') {
|
||
targetGender = userProfile.interestedIn;
|
||
} else {
|
||
// Если "both" или другое значение, показываем противоположный пол
|
||
targetGender = userProfile.gender === 'male' ? 'female' : 'male';
|
||
}
|
||
|
||
console.log(`[DEBUG] Определен целевой пол для поиска: ${targetGender}`);
|
||
|
||
// Получаем список просмотренных профилей из новой таблицы profile_views
|
||
// и добавляем также профили из свайпов для полной совместимости
|
||
console.log(`[DEBUG] Запрашиваем просмотренные и свайпнутые профили для userId=${userId}`);
|
||
const [viewedProfilesResult, swipedProfilesResult] = await Promise.all([
|
||
query(`
|
||
SELECT DISTINCT viewed_profile_id
|
||
FROM profile_views
|
||
WHERE viewer_id = $1
|
||
`, [userId]),
|
||
query(`
|
||
SELECT DISTINCT target_user_id
|
||
FROM swipes
|
||
WHERE user_id = $1
|
||
`, [userId])
|
||
]);
|
||
console.log(`[DEBUG] Найдено ${viewedProfilesResult.rows.length} просмотренных и ${swipedProfilesResult.rows.length} свайпнутых профилей`);
|
||
|
||
// Объединяем просмотренные и свайпнутые профили в один список
|
||
const viewedUserIds = [
|
||
...viewedProfilesResult.rows.map((row: any) => row.viewed_profile_id),
|
||
...swipedProfilesResult.rows.map((row: any) => row.target_user_id)
|
||
];
|
||
|
||
// Всегда добавляем самого пользователя в список исключений
|
||
viewedUserIds.push(userId);
|
||
|
||
// Удаляем дубликаты
|
||
const uniqueViewedIds = [...new Set(viewedUserIds)];
|
||
console.log(`[DEBUG] Всего ${uniqueViewedIds.length} уникальных исключаемых профилей`);
|
||
|
||
// Формируем параметры запроса
|
||
let params: any[] = [];
|
||
let excludeCondition: string = '';
|
||
|
||
// Для новых пользователей исключаем только себя
|
||
if (isNewUser || uniqueViewedIds.length <= 1) {
|
||
params = [userId];
|
||
excludeCondition = 'AND p.user_id != $1';
|
||
console.log(`[DEBUG] Режим нового пользователя: исключаем только самого себя`);
|
||
} else {
|
||
// Для остальных исключаем все просмотренные профили
|
||
params = [...uniqueViewedIds];
|
||
const placeholders = uniqueViewedIds.map((_: any, i: number) => `$${i + 1}`).join(', ');
|
||
excludeCondition = `AND p.user_id NOT IN (${placeholders})`;
|
||
console.log(`[DEBUG] Стандартный режим: исключаем ${uniqueViewedIds.length} профилей`);
|
||
}
|
||
|
||
// Выполним предварительный запрос для проверки наличия доступных анкет
|
||
const countQuery = `
|
||
SELECT COUNT(*) as count
|
||
FROM profiles p
|
||
JOIN users u ON p.user_id = u.id
|
||
WHERE p.is_visible = true
|
||
AND p.gender = '${targetGender}'
|
||
${excludeCondition}
|
||
`;
|
||
|
||
console.log(`[DEBUG] Проверка наличия подходящих анкет...`);
|
||
console.log(`[DEBUG] SQL запрос count: ${countQuery}`);
|
||
console.log(`[DEBUG] Параметры count: ${JSON.stringify(params)}`);
|
||
const countResult = await query(countQuery, params);
|
||
const availableProfilesCount = parseInt(countResult.rows[0]?.count || '0');
|
||
console.log(`[DEBUG] Найдено ${availableProfilesCount} доступных профилей`);
|
||
|
||
// Используем определенный ранее targetGender для поиска
|
||
console.log(`[DEBUG] Поиск кандидата для gender=${targetGender}, возраст: ${userProfile.searchPreferences.minAge}-${userProfile.searchPreferences.maxAge}`);
|
||
|
||
const candidateQuery = `
|
||
SELECT p.*, u.telegram_id, u.username, u.first_name, u.last_name
|
||
FROM profiles p
|
||
JOIN users u ON p.user_id = u.id
|
||
WHERE p.is_visible = true
|
||
AND p.gender = '${targetGender}'
|
||
AND p.age BETWEEN ${userProfile.searchPreferences.minAge} AND ${userProfile.searchPreferences.maxAge}
|
||
${excludeCondition}
|
||
ORDER BY RANDOM()
|
||
LIMIT 1
|
||
`;
|
||
console.log(`[DEBUG] SQL запрос: ${candidateQuery}`);
|
||
console.log(`[DEBUG] Параметры: ${JSON.stringify(params)}`);
|
||
|
||
const result = await query(candidateQuery, params);
|
||
console.log(`[DEBUG] Результаты запроса: найдено ${result.rows.length} профилей`);
|
||
|
||
if (result.rows.length === 0) {
|
||
console.log(`[DEBUG] Подходящие кандидаты не найдены`);
|
||
return null;
|
||
}
|
||
|
||
const candidateData = result.rows[0];
|
||
console.log(`[DEBUG] Найден подходящий кандидат: ${candidateData.name}, возраст: ${candidateData.age}`);
|
||
|
||
// Записываем просмотр профиля в новую таблицу profile_views
|
||
try {
|
||
const viewerTelegramId = telegramId;
|
||
const viewedTelegramId = candidateData.telegram_id.toString();
|
||
|
||
console.log(`[DEBUG] Записываем просмотр профиля: viewer=${viewerTelegramId}, viewed=${viewedTelegramId}`);
|
||
// Асинхронно записываем просмотр, но не ждем завершения
|
||
this.profileService.recordProfileView(viewerTelegramId, viewedTelegramId, 'browse')
|
||
.catch(err => console.error(`[ERROR] Ошибка записи просмотра профиля:`, err));
|
||
} catch (err) {
|
||
console.error(`[ERROR] Ошибка записи просмотра профиля:`, err);
|
||
}
|
||
|
||
// Используем ProfileService для правильного маппинга данных
|
||
const profile = this.profileService.mapEntityToProfile(candidateData);
|
||
console.log(`[DEBUG] Профиль преобразован и возвращается клиенту`);
|
||
return profile;
|
||
}
|
||
|
||
// VIP функция: поиск кандидатов по цели знакомства
|
||
async getCandidatesWithGoal(userProfile: Profile, targetGoal: string): Promise<Profile[]> {
|
||
const swipedUsersResult = await query(`
|
||
SELECT target_user_id
|
||
FROM swipes
|
||
WHERE user_id = $1
|
||
`, [userProfile.userId]);
|
||
|
||
const swipedUserIds = swipedUsersResult.rows.map((row: any) => row.target_user_id);
|
||
swipedUserIds.push(userProfile.userId); // Исключаем себя
|
||
|
||
let candidateQuery = `
|
||
SELECT DISTINCT p.*, u.telegram_id, u.username, u.first_name, u.last_name
|
||
FROM profiles p
|
||
JOIN users u ON p.user_id = u.id
|
||
WHERE p.is_visible = true
|
||
AND u.is_active = true
|
||
AND p.gender = $1
|
||
AND p.dating_goal = $2
|
||
AND p.user_id NOT IN (${swipedUserIds.map((_: any, i: number) => `$${i + 3}`).join(', ')})
|
||
ORDER BY p.created_at DESC
|
||
LIMIT 50
|
||
`;
|
||
|
||
const params = [userProfile.interestedIn, targetGoal, ...swipedUserIds];
|
||
const result = await query(candidateQuery, params);
|
||
|
||
return result.rows.map((row: any) => this.profileService.mapEntityToProfile(row));
|
||
}
|
||
} |