feat: add stats to profile response

Made-with: Cursor
This commit is contained in:
Anton
2026-03-04 14:17:54 +03:00
parent 6530e81402
commit 9da82c839f

View File

@@ -1,13 +1,30 @@
import { eq } from 'drizzle-orm'; import { eq } from 'drizzle-orm';
import type { NodePgDatabase } from 'drizzle-orm/node-postgres'; import type { NodePgDatabase } from 'drizzle-orm/node-postgres';
import type * as schema from '../../db/schema/index.js'; import type * as schema from '../../db/schema/index.js';
import { users } from '../../db/schema/index.js'; import { users, userStats } from '../../db/schema/index.js';
import { notFound, conflict, ERROR_CODES } from '../../utils/errors.js'; import { notFound, conflict, ERROR_CODES } from '../../utils/errors.js';
import type { User } from '../../db/schema/users.js'; import type { User } from '../../db/schema/users.js';
import type { SelfLevel } from '../../db/schema/index.js'; import type { SelfLevel } from '../../db/schema/index.js';
type Db = NodePgDatabase<typeof schema>; type Db = NodePgDatabase<typeof schema>;
export type UserStatItem = {
stack: string;
level: string;
totalQuestions: number;
correctAnswers: number;
testsTaken: number;
lastTestAt: string | null;
};
export type ProfileStats = {
byStack: UserStatItem[];
totalTestsTaken: number;
totalQuestions: number;
correctAnswers: number;
accuracy: number | null;
};
export type ProfileUpdateInput = { export type ProfileUpdateInput = {
nickname?: string; nickname?: string;
avatarUrl?: string | null; avatarUrl?: string | null;
@@ -25,6 +42,7 @@ export type PublicProfile = {
city: string | null; city: string | null;
selfLevel: string | null; selfLevel: string | null;
isPublic: boolean; isPublic: boolean;
stats: ProfileStats;
}; };
export type PrivateProfile = PublicProfile & { export type PrivateProfile = PublicProfile & {
@@ -34,7 +52,30 @@ export type PrivateProfile = PublicProfile & {
updatedAt: string; updatedAt: string;
}; };
function toPublicProfile(user: User): PublicProfile { async function getStatsForUser(db: Db, userId: string): Promise<ProfileStats> {
const rows = await db
.select()
.from(userStats)
.where(eq(userStats.userId, userId));
const byStack: UserStatItem[] = rows.map((r) => ({
stack: r.stack,
level: r.level,
totalQuestions: r.totalQuestions,
correctAnswers: r.correctAnswers,
testsTaken: r.testsTaken,
lastTestAt: r.lastTestAt?.toISOString() ?? null,
}));
const totalTestsTaken = rows.reduce((sum, r) => sum + r.testsTaken, 0);
const totalQuestions = rows.reduce((sum, r) => sum + r.totalQuestions, 0);
const correctAnswers = rows.reduce((sum, r) => sum + r.correctAnswers, 0);
const accuracy = totalQuestions > 0 ? correctAnswers / totalQuestions : null;
return { byStack, totalTestsTaken, totalQuestions, correctAnswers, accuracy };
}
function toPublicProfile(user: User, stats: ProfileStats): PublicProfile {
return { return {
id: user.id, id: user.id,
nickname: user.nickname, nickname: user.nickname,
@@ -43,12 +84,13 @@ function toPublicProfile(user: User): PublicProfile {
city: user.city, city: user.city,
selfLevel: user.selfLevel, selfLevel: user.selfLevel,
isPublic: user.isPublic, isPublic: user.isPublic,
stats,
}; };
} }
function toPrivateProfile(user: User): PrivateProfile { function toPrivateProfile(user: User, stats: ProfileStats): PrivateProfile {
return { return {
...toPublicProfile(user), ...toPublicProfile(user, stats),
email: user.email, email: user.email,
emailVerifiedAt: user.emailVerifiedAt?.toISOString() ?? null, emailVerifiedAt: user.emailVerifiedAt?.toISOString() ?? null,
createdAt: user.createdAt.toISOString(), createdAt: user.createdAt.toISOString(),
@@ -74,11 +116,11 @@ export class UserService {
} }
async getPrivateProfile(userId: string): Promise<PrivateProfile> { async getPrivateProfile(userId: string): Promise<PrivateProfile> {
const user = await this.getById(userId); const [user, stats] = await Promise.all([this.getById(userId), getStatsForUser(this.db, userId)]);
if (!user) { if (!user) {
throw notFound('User not found'); throw notFound('User not found');
} }
return toPrivateProfile(user); return toPrivateProfile(user, stats);
} }
async getPublicProfile(username: string): Promise<PublicProfile> { async getPublicProfile(username: string): Promise<PublicProfile> {
@@ -89,7 +131,8 @@ export class UserService {
if (!user.isPublic) { if (!user.isPublic) {
throw notFound('User not found'); throw notFound('User not found');
} }
return toPublicProfile(user); const stats = await getStatsForUser(this.db, user.id);
return toPublicProfile(user, stats);
} }
async updateProfile(userId: string, input: ProfileUpdateInput): Promise<PrivateProfile> { async updateProfile(userId: string, input: ProfileUpdateInput): Promise<PrivateProfile> {
@@ -126,6 +169,7 @@ export class UserService {
throw notFound('User not found'); throw notFound('User not found');
} }
return toPrivateProfile(updated); const stats = await getStatsForUser(this.db, userId);
return toPrivateProfile(updated, stats);
} }
} }