Messenger/packages/backend/src/modules/users/users.service.ts
Заид Омар Медхат | Zaid Omar Medhat f5b620111b redesign
2026-07-11 04:49:07 +05:00

93 lines
2.9 KiB
TypeScript

import type { User, PublicUser, UpdateMeBody, ProfileUpdateEvent } from '@altricade/core';
import { userChannel, EventType } from '@altricade/core';
import { HttpError } from '../../shared/http-error';
import type { Publisher } from '../../shared/publisher';
import type { UsersRepository } from './users.repository';
import { toUser, toPublicUser } from './users.mapper';
export interface UsersServiceDeps {
users: UsersRepository;
publish: Publisher;
}
export interface UsersService {
getMe(userId: string): Promise<User>;
getPublicProfile(username: string): Promise<PublicUser>;
updateProfile(userId: string, patch: UpdateMeBody): Promise<User>;
setAvatar(userId: string, avatarUrl: string): Promise<User>;
search(query: string, excludeUserId: string): Promise<PublicUser[]>;
heartbeat(userId: string): Promise<void>;
}
const SEARCH_LIMIT = 20;
// Escape LIKE wildcards so user input is matched literally.
const escapeLike = (value: string): string => value.replace(/[\\%_]/g, '\\$&');
export const createUsersService = ({ users, publish }: UsersServiceDeps): UsersService => ({
getMe: async (userId) => {
const row = await users.findById(userId);
if (row === undefined) {
throw new HttpError(404, 'not_found', 'User not found');
}
return toUser(row);
},
getPublicProfile: async (username) => {
const row = await users.findByUsername(username);
if (row === undefined) {
throw new HttpError(404, 'not_found', 'User not found');
}
return toPublicUser(row);
},
updateProfile: async (userId, patch) => {
let row;
try {
row = await users.updateProfile(userId, patch);
} catch (error) {
// Unique violation on the citext username column (23505) → taken.
if (typeof error === 'object' && error !== null && 'code' in error && error.code === '23505') {
throw new HttpError(409, 'username_taken', 'That username is already taken');
}
throw error;
}
if (row === undefined) {
throw new HttpError(404, 'not_found', 'User not found');
}
return toUser(row);
},
setAvatar: async (userId, avatarUrl) => {
const row = await users.setAvatar(userId, avatarUrl);
if (row === undefined) {
throw new HttpError(404, 'not_found', 'User not found');
}
const user = toUser(row);
const event: ProfileUpdateEvent = {
type: EventType.ProfileUpdate,
userId,
displayName: user.displayName,
avatarUrl: user.avatarUrl,
};
await publish(userChannel(userId), event);
return user;
},
search: async (query, excludeUserId) => {
const trimmed = query.trim();
if (trimmed.length === 0) {
return [];
}
const rows = await users.searchByPrefix(escapeLike(trimmed), excludeUserId, SEARCH_LIMIT);
return rows.map(toPublicUser);
},
heartbeat: (userId) => users.touchLastSeen(userId),
});
declare module 'fastify' {
interface FastifyInstance {
usersService: UsersService;
}
}