refactor: replace all explicit any types with proper TypeScript types

Backend resolvers: typed enrichArticle/enrichVideo/enrichModel with DB
and $inferSelect types, SQL<unknown>[] for conditions arrays, proper
enum casts for status/role fields, $inferInsert for .set() updates,
typed raw SQL result rows in gamification, ReplyLike interface for
ctx.reply in auth. Frontend: typed catch blocks with Error/interface
casts, isActiveLink param, adminGetUser response, tags filter callback.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-07 19:25:04 +01:00
parent 8313664d70
commit e236ced12a
30 changed files with 392 additions and 375 deletions

View File

@@ -1,10 +1,11 @@
import { builder } from "../builder"; import { builder } from "../builder";
import { ArticleType, ArticleListType, AdminArticleListType } from "../types/index"; import { ArticleType, ArticleListType, AdminArticleListType } from "../types/index";
import { articles, users } from "../../db/schema/index"; import { articles, users } from "../../db/schema/index";
import { eq, and, lte, desc, asc, ilike, or, count, arrayContains } from "drizzle-orm"; import { eq, and, lte, desc, asc, ilike, or, count, arrayContains, type SQL } from "drizzle-orm";
import { requireAdmin } from "../../lib/acl"; import { requireAdmin } from "../../lib/acl";
import type { DB } from "../../db/connection";
async function enrichArticle(db: any, article: any) { async function enrichArticle(db: DB, article: typeof articles.$inferSelect) {
let author = null; let author = null;
if (article.author) { if (article.author) {
const authorUser = await db const authorUser = await db
@@ -39,7 +40,7 @@ builder.queryField("articles", (t) =>
const pageSize = args.limit ?? 24; const pageSize = args.limit ?? 24;
const offset = args.offset ?? 0; const offset = args.offset ?? 0;
const conditions: any[] = [lte(articles.publish_date, new Date())]; const conditions: SQL<unknown>[] = [lte(articles.publish_date, new Date())];
if (args.featured !== null && args.featured !== undefined) { if (args.featured !== null && args.featured !== undefined) {
conditions.push(eq(articles.featured, args.featured)); conditions.push(eq(articles.featured, args.featured));
} }
@@ -50,28 +51,24 @@ builder.queryField("articles", (t) =>
or( or(
ilike(articles.title, `%${args.search}%`), ilike(articles.title, `%${args.search}%`),
ilike(articles.excerpt, `%${args.search}%`), ilike(articles.excerpt, `%${args.search}%`),
), ) as SQL<unknown>,
); );
} }
const orderArgs =
args.sortBy === "name"
? [asc(articles.title)]
: args.sortBy === "featured"
? [desc(articles.featured), desc(articles.publish_date)]
: [desc(articles.publish_date)];
const where = and(...conditions); const where = and(...conditions);
const baseQuery = ctx.db.select().from(articles).where(where);
const ordered =
args.sortBy === "name"
? baseQuery.orderBy(asc(articles.title))
: args.sortBy === "featured"
? baseQuery.orderBy(desc(articles.featured), desc(articles.publish_date))
: baseQuery.orderBy(desc(articles.publish_date));
const [articleList, totalRows] = await Promise.all([ const [articleList, totalRows] = await Promise.all([
(ctx.db.select().from(articles).where(where) as any) ordered.limit(pageSize).offset(offset),
.orderBy(...orderArgs)
.limit(pageSize)
.offset(offset),
ctx.db.select({ total: count() }).from(articles).where(where), ctx.db.select({ total: count() }).from(articles).where(where),
]); ]);
const items = await Promise.all( const items = await Promise.all(articleList.map((article) => enrichArticle(ctx.db, article)));
articleList.map((article: any) => enrichArticle(ctx.db, article)),
);
return { items, total: totalRows[0]?.total ?? 0 }; return { items, total: totalRows[0]?.total ?? 0 };
}, },
}), }),
@@ -130,13 +127,13 @@ builder.queryField("adminListArticles", (t) =>
const limit = args.limit ?? 50; const limit = args.limit ?? 50;
const offset = args.offset ?? 0; const offset = args.offset ?? 0;
const conditions: any[] = []; const conditions: SQL<unknown>[] = [];
if (args.search) { if (args.search) {
conditions.push( conditions.push(
or( or(
ilike(articles.title, `%${args.search}%`), ilike(articles.title, `%${args.search}%`),
ilike(articles.excerpt, `%${args.search}%`), ilike(articles.excerpt, `%${args.search}%`),
), ) as SQL<unknown>,
); );
} }
if (args.category) conditions.push(eq(articles.category, args.category)); if (args.category) conditions.push(eq(articles.category, args.category));
@@ -154,9 +151,7 @@ builder.queryField("adminListArticles", (t) =>
.offset(offset), .offset(offset),
ctx.db.select({ total: count() }).from(articles).where(where), ctx.db.select({ total: count() }).from(articles).where(where),
]); ]);
const items = await Promise.all( const items = await Promise.all(articleList.map((article) => enrichArticle(ctx.db, article)));
articleList.map((article: any) => enrichArticle(ctx.db, article)),
);
return { items, total: totalRows[0]?.total ?? 0 }; return { items, total: totalRows[0]?.total ?? 0 };
}, },
}), }),
@@ -232,7 +227,7 @@ builder.mutationField("updateArticle", (t) =>
const updated = await ctx.db const updated = await ctx.db
.update(articles) .update(articles)
.set(updates as any) .set(updates as Partial<typeof articles.$inferInsert>)
.where(eq(articles.id, args.id)) .where(eq(articles.id, args.id))
.returning(); .returning();
if (!updated[0]) return null; if (!updated[0]) return null;

View File

@@ -3,6 +3,10 @@ import { builder } from "../builder";
import { CurrentUserType } from "../types/index"; import { CurrentUserType } from "../types/index";
import { users } from "../../db/schema/index"; import { users } from "../../db/schema/index";
import { eq } from "drizzle-orm"; import { eq } from "drizzle-orm";
interface ReplyLike {
header?: (name: string, value: string) => void;
}
import { hash, verify as verifyArgon } from "../../lib/argon"; import { hash, verify as verifyArgon } from "../../lib/argon";
import { setSession, deleteSession } from "../../lib/auth"; import { setSession, deleteSession } from "../../lib/auth";
import { sendVerification, sendPasswordReset } from "../../lib/email"; import { sendVerification, sendPasswordReset } from "../../lib/email";
@@ -46,12 +50,7 @@ builder.mutationField("login", (t) =>
// Set session cookie // Set session cookie
const isProduction = process.env.NODE_ENV === "production"; const isProduction = process.env.NODE_ENV === "production";
const cookieValue = `session_token=${token}; HttpOnly; Path=/; SameSite=Strict; Max-Age=86400${isProduction ? "; Secure" : ""}`; const cookieValue = `session_token=${token}; HttpOnly; Path=/; SameSite=Strict; Max-Age=86400${isProduction ? "; Secure" : ""}`;
(ctx.reply as any).header?.("Set-Cookie", cookieValue); (ctx.reply as ReplyLike).header?.("Set-Cookie", cookieValue);
// For graphql-yoga response
if ((ctx as any).serverResponse) {
(ctx as any).serverResponse.setHeader("Set-Cookie", cookieValue);
}
return user[0]; return user[0];
}, },
@@ -76,7 +75,7 @@ builder.mutationField("logout", (t) =>
// Clear cookie // Clear cookie
const isProduction = process.env.NODE_ENV === "production"; const isProduction = process.env.NODE_ENV === "production";
const cookieValue = `session_token=; HttpOnly; Path=/; SameSite=Strict; Max-Age=0${isProduction ? "; Secure" : ""}`; const cookieValue = `session_token=; HttpOnly; Path=/; SameSite=Strict; Max-Age=0${isProduction ? "; Secure" : ""}`;
(ctx.reply as any).header?.("Set-Cookie", cookieValue); (ctx.reply as ReplyLike).header?.("Set-Cookie", cookieValue);
return true; return true;
}, },
}), }),

View File

@@ -20,7 +20,7 @@ builder.queryField("commentsForVideo", (t) =>
.orderBy(desc(comments.date_created)); .orderBy(desc(comments.date_created));
return Promise.all( return Promise.all(
commentList.map(async (c: any) => { commentList.map(async (c) => {
const user = await ctx.db const user = await ctx.db
.select({ .select({
id: users.id, id: users.id,
@@ -125,7 +125,7 @@ builder.queryField("adminListComments", (t) =>
]); ]);
const items = await Promise.all( const items = await Promise.all(
commentList.map(async (c: any) => { commentList.map(async (c) => {
const user = await ctx.db const user = await ctx.db
.select({ .select({
id: users.id, id: users.id,

View File

@@ -37,7 +37,7 @@ builder.queryField("leaderboard", (t) =>
.limit(limit) .limit(limit)
.offset(offset); .offset(offset);
return entries.map((e: any, i: number) => ({ ...e, rank: offset + i + 1 })); return entries.map((e, i) => ({ ...e, rank: offset + i + 1 }));
}, },
}), }),
); );
@@ -101,7 +101,7 @@ builder.queryField("userGamification", (t) =>
return { return {
stats: stats[0] ? { ...stats[0], rank } : null, stats: stats[0] ? { ...stats[0], rank } : null,
achievements: userAchievements.map((a: any) => ({ achievements: userAchievements.map((a) => ({
...a, ...a,
date_unlocked: a.date_unlocked!, date_unlocked: a.date_unlocked!,
})), })),

View File

@@ -1,9 +1,10 @@
import { builder } from "../builder"; import { builder } from "../builder";
import { ModelType, ModelListType } from "../types/index"; import { ModelType, ModelListType } from "../types/index";
import { users, user_photos, files } from "../../db/schema/index"; import { users, user_photos, files } from "../../db/schema/index";
import { eq, and, desc, asc, ilike, count, arrayContains } from "drizzle-orm"; import { eq, and, desc, asc, ilike, count, arrayContains, type SQL } from "drizzle-orm";
import type { DB } from "../../db/connection";
async function enrichModel(db: any, user: any) { async function enrichModel(db: DB, user: typeof users.$inferSelect) {
// Fetch photos // Fetch photos
const photoRows = await db const photoRows = await db
.select({ id: files.id, filename: files.filename }) .select({ id: files.id, filename: files.filename })
@@ -14,8 +15,8 @@ async function enrichModel(db: any, user: any) {
const seen = new Set<string>(); const seen = new Set<string>();
const photos = photoRows const photos = photoRows
.filter((p: any) => p.id && !seen.has(p.id) && seen.add(p.id)) .filter((p) => p.id && !seen.has(p.id) && seen.add(p.id))
.map((p: any) => ({ id: p.id, filename: p.filename })); .map((p) => ({ id: p.id, filename: p.filename }));
return { ...user, photos }; return { ...user, photos };
} }
@@ -35,7 +36,7 @@ builder.queryField("models", (t) =>
const pageSize = args.limit ?? 24; const pageSize = args.limit ?? 24;
const offset = args.offset ?? 0; const offset = args.offset ?? 0;
const conditions: any[] = [eq(users.role, "model")]; const conditions: SQL<unknown>[] = [eq(users.role, "model")];
if (args.search) conditions.push(ilike(users.artist_name, `%${args.search}%`)); if (args.search) conditions.push(ilike(users.artist_name, `%${args.search}%`));
if (args.tag) conditions.push(arrayContains(users.tags, [args.tag])); if (args.tag) conditions.push(arrayContains(users.tags, [args.tag]));
@@ -46,7 +47,7 @@ builder.queryField("models", (t) =>
ctx.db.select().from(users).where(where).orderBy(order).limit(pageSize).offset(offset), ctx.db.select().from(users).where(where).orderBy(order).limit(pageSize).offset(offset),
ctx.db.select({ total: count() }).from(users).where(where), ctx.db.select({ total: count() }).from(users).where(where),
]); ]);
const items = await Promise.all(modelList.map((m: any) => enrichModel(ctx.db, m))); const items = await Promise.all(modelList.map((m) => enrichModel(ctx.db, m)));
return { items, total: totalRows[0]?.total ?? 0 }; return { items, total: totalRows[0]?.total ?? 0 };
}, },
}), }),

View File

@@ -2,7 +2,7 @@ import { GraphQLError } from "graphql";
import { builder } from "../builder"; import { builder } from "../builder";
import { RecordingType, AdminRecordingListType } from "../types/index"; import { RecordingType, AdminRecordingListType } from "../types/index";
import { recordings, recording_plays } from "../../db/schema/index"; import { recordings, recording_plays } from "../../db/schema/index";
import { eq, and, desc, ilike, count } from "drizzle-orm"; import { eq, and, desc, ilike, count, type SQL } from "drizzle-orm";
import { slugify } from "../../lib/slugify"; import { slugify } from "../../lib/slugify";
import { awardPoints, checkAchievements } from "../../lib/gamification"; import { awardPoints, checkAchievements } from "../../lib/gamification";
import { requireAdmin } from "../../lib/acl"; import { requireAdmin } from "../../lib/acl";
@@ -21,7 +21,7 @@ builder.queryField("recordings", (t) =>
if (!ctx.currentUser) throw new GraphQLError("Unauthorized"); if (!ctx.currentUser) throw new GraphQLError("Unauthorized");
const conditions = [eq(recordings.user_id, ctx.currentUser.id)]; const conditions = [eq(recordings.user_id, ctx.currentUser.id)];
if (args.status) conditions.push(eq(recordings.status, args.status as any)); if (args.status) conditions.push(eq(recordings.status, args.status as "draft" | "published"));
if (args.linkedVideoId) conditions.push(eq(recordings.linked_video, args.linkedVideoId)); if (args.linkedVideoId) conditions.push(eq(recordings.linked_video, args.linkedVideoId));
const limit = args.limit || 50; const limit = args.limit || 50;
@@ -115,7 +115,7 @@ builder.mutationField("createRecording", (t) =>
user_id: ctx.currentUser.id, user_id: ctx.currentUser.id,
tags: args.tags || [], tags: args.tags || [],
linked_video: args.linkedVideoId || null, linked_video: args.linkedVideoId || null,
status: (args.status as any) || "draft", status: (args.status as "draft" | "published") || "draft",
public: false, public: false,
}) })
.returning(); .returning();
@@ -174,7 +174,7 @@ builder.mutationField("updateRecording", (t) =>
const updated = await ctx.db const updated = await ctx.db
.update(recordings) .update(recordings)
.set(updates as any) .set(updates as Partial<typeof recordings.$inferInsert>)
.where(eq(recordings.id, args.id)) .where(eq(recordings.id, args.id))
.returning(); .returning();
@@ -355,9 +355,9 @@ builder.queryField("adminListRecordings", (t) =>
const limit = args.limit ?? 50; const limit = args.limit ?? 50;
const offset = args.offset ?? 0; const offset = args.offset ?? 0;
const conditions: any[] = []; const conditions: SQL<unknown>[] = [];
if (args.search) conditions.push(ilike(recordings.title, `%${args.search}%`)); if (args.search) conditions.push(ilike(recordings.title, `%${args.search}%`));
if (args.status) conditions.push(eq(recordings.status, args.status as any)); if (args.status) conditions.push(eq(recordings.status, args.status as "draft" | "published"));
const where = conditions.length > 0 ? and(...conditions) : undefined; const where = conditions.length > 0 ? and(...conditions) : undefined;
const [rows, totalRows] = await Promise.all([ const [rows, totalRows] = await Promise.all([

View File

@@ -2,7 +2,7 @@ import { GraphQLError } from "graphql";
import { builder } from "../builder"; import { builder } from "../builder";
import { CurrentUserType, UserType, AdminUserListType, AdminUserDetailType } from "../types/index"; import { CurrentUserType, UserType, AdminUserListType, AdminUserDetailType } from "../types/index";
import { users, user_photos, files } from "../../db/schema/index"; import { users, user_photos, files } from "../../db/schema/index";
import { eq, ilike, or, count, and } from "drizzle-orm"; import { eq, ilike, or, count, and, asc, type SQL } from "drizzle-orm";
import { requireAdmin } from "../../lib/acl"; import { requireAdmin } from "../../lib/acl";
builder.queryField("me", (t) => builder.queryField("me", (t) =>
@@ -63,7 +63,7 @@ builder.mutationField("updateProfile", (t) =>
await ctx.db await ctx.db
.update(users) .update(users)
.set(updates as any) .set(updates as Partial<typeof users.$inferInsert>)
.where(eq(users.id, ctx.currentUser.id)); .where(eq(users.id, ctx.currentUser.id));
const updated = await ctx.db const updated = await ctx.db
@@ -93,27 +93,27 @@ builder.queryField("adminListUsers", (t) =>
const limit = args.limit ?? 50; const limit = args.limit ?? 50;
const offset = args.offset ?? 0; const offset = args.offset ?? 0;
let query = ctx.db.select().from(users); const conditions: SQL<unknown>[] = [];
let countQuery = ctx.db.select({ total: count() }).from(users);
const conditions: any[] = [];
if (args.role) { if (args.role) {
conditions.push(eq(users.role, args.role as any)); conditions.push(eq(users.role, args.role as "model" | "viewer" | "admin"));
} }
if (args.search) { if (args.search) {
const pattern = `%${args.search}%`; const pattern = `%${args.search}%`;
conditions.push(or(ilike(users.email, pattern), ilike(users.artist_name, pattern))); conditions.push(
} or(ilike(users.email, pattern), ilike(users.artist_name, pattern)) as SQL<unknown>,
);
if (conditions.length > 0) {
const where = conditions.length === 1 ? conditions[0] : and(...conditions);
query = (query as any).where(where);
countQuery = (countQuery as any).where(where);
} }
const where = conditions.length > 0 ? and(...conditions) : undefined;
const [items, totalRows] = await Promise.all([ const [items, totalRows] = await Promise.all([
(query as any).limit(limit).offset(offset), ctx.db
countQuery, .select()
.from(users)
.where(where)
.orderBy(asc(users.artist_name))
.limit(limit)
.offset(offset),
ctx.db.select({ total: count() }).from(users).where(where),
]); ]);
return { items, total: totalRows[0]?.total ?? 0 }; return { items, total: totalRows[0]?.total ?? 0 };
@@ -139,7 +139,8 @@ builder.mutationField("adminUpdateUser", (t) =>
requireAdmin(ctx); requireAdmin(ctx);
const updates: Record<string, unknown> = { date_updated: new Date() }; const updates: Record<string, unknown> = { date_updated: new Date() };
if (args.role !== undefined && args.role !== null) updates.role = args.role as any; if (args.role !== undefined && args.role !== null)
updates.role = args.role as "model" | "viewer" | "admin";
if (args.isAdmin !== undefined && args.isAdmin !== null) updates.is_admin = args.isAdmin; if (args.isAdmin !== undefined && args.isAdmin !== null) updates.is_admin = args.isAdmin;
if (args.firstName !== undefined && args.firstName !== null) if (args.firstName !== undefined && args.firstName !== null)
updates.first_name = args.firstName; updates.first_name = args.firstName;
@@ -151,7 +152,7 @@ builder.mutationField("adminUpdateUser", (t) =>
const updated = await ctx.db const updated = await ctx.db
.update(users) .update(users)
.set(updates as any) .set(updates as Partial<typeof users.$inferInsert>)
.where(eq(users.id, args.userId)) .where(eq(users.id, args.userId))
.returning(); .returning();
@@ -194,8 +195,8 @@ builder.queryField("adminGetUser", (t) =>
.orderBy(user_photos.sort); .orderBy(user_photos.sort);
const seen = new Set<string>(); const seen = new Set<string>();
const photos = photoRows const photos = photoRows
.filter((p: any) => p.id && !seen.has(p.id) && seen.add(p.id)) .filter((p) => p.id && !seen.has(p.id) && seen.add(p.id))
.map((p: any) => ({ id: p.id, filename: p.filename })); .map((p) => ({ id: p.id, filename: p.filename }));
return { ...user[0], photos }; return { ...user[0], photos };
}, },
}), }),

View File

@@ -28,10 +28,12 @@ import {
lt, lt,
gte, gte,
arrayContains, arrayContains,
type SQL,
} from "drizzle-orm"; } from "drizzle-orm";
import { requireAdmin } from "../../lib/acl"; import { requireAdmin } from "../../lib/acl";
import type { DB } from "../../db/connection";
async function enrichVideo(db: any, video: any) { async function enrichVideo(db: DB, video: typeof videos.$inferSelect) {
// Fetch models // Fetch models
const modelRows = await db const modelRows = await db
.select({ .select({
@@ -87,7 +89,7 @@ builder.queryField("videos", (t) =>
const pageSize = args.limit ?? 24; const pageSize = args.limit ?? 24;
const offset = args.offset ?? 0; const offset = args.offset ?? 0;
const conditions: any[] = [lte(videos.upload_date, new Date())]; const conditions: SQL<unknown>[] = [lte(videos.upload_date, new Date())];
if (!ctx.currentUser) conditions.push(eq(videos.premium, false)); if (!ctx.currentUser) conditions.push(eq(videos.premium, false));
if (args.featured !== null && args.featured !== undefined) { if (args.featured !== null && args.featured !== undefined) {
conditions.push(eq(videos.featured, args.featured)); conditions.push(eq(videos.featured, args.featured));
@@ -107,7 +109,7 @@ builder.queryField("videos", (t) =>
conditions.push( conditions.push(
inArray( inArray(
videos.id, videos.id,
videoIds.map((v: any) => v.video_id), videoIds.map((v) => v.video_id),
), ),
); );
} }
@@ -148,8 +150,8 @@ builder.queryField("videos", (t) =>
.leftJoin(files, eq(videos.movie, files.id)) .leftJoin(files, eq(videos.movie, files.id))
.where(fullWhere), .where(fullWhere),
]); ]);
const videoList = rows.map((r: any) => r.v || r); const videoList = rows.map((r) => r.v);
const items = await Promise.all(videoList.map((v: any) => enrichVideo(ctx.db, v))); const items = await Promise.all(videoList.map((v) => enrichVideo(ctx.db, v)));
return { items, total: totalRows[0]?.total ?? 0 }; return { items, total: totalRows[0]?.total ?? 0 };
} }
@@ -157,7 +159,7 @@ builder.queryField("videos", (t) =>
ctx.db.select().from(videos).where(where).orderBy(order).limit(pageSize).offset(offset), ctx.db.select().from(videos).where(where).orderBy(order).limit(pageSize).offset(offset),
ctx.db.select({ total: count() }).from(videos).where(where), ctx.db.select({ total: count() }).from(videos).where(where),
]); ]);
const items = await Promise.all(rows.map((v: any) => enrichVideo(ctx.db, v))); const items = await Promise.all(rows.map((v) => enrichVideo(ctx.db, v)));
return { items, total: totalRows[0]?.total ?? 0 }; return { items, total: totalRows[0]?.total ?? 0 };
}, },
}), }),
@@ -421,7 +423,7 @@ builder.queryField("analytics", (t) =>
}; };
} }
const videoIds = modelVideoIds.map((v: any) => v.video_id); const videoIds = modelVideoIds.map((v) => v.video_id);
const videoList = await ctx.db.select().from(videos).where(inArray(videos.id, videoIds)); const videoList = await ctx.db.select().from(videos).where(inArray(videos.id, videoIds));
const plays = await ctx.db const plays = await ctx.db
.select() .select()
@@ -435,14 +437,14 @@ builder.queryField("analytics", (t) =>
const totalLikes = videoList.reduce((sum, v) => sum + (v.likes_count || 0), 0); const totalLikes = videoList.reduce((sum, v) => sum + (v.likes_count || 0), 0);
const totalPlays = videoList.reduce((sum, v) => sum + (v.plays_count || 0), 0); const totalPlays = videoList.reduce((sum, v) => sum + (v.plays_count || 0), 0);
const playsByDate = plays.reduce((acc: any, play) => { const playsByDate = plays.reduce((acc: Record<string, number>, play) => {
const date = new Date(play.date_created).toISOString().split("T")[0]; const date = new Date(play.date_created).toISOString().split("T")[0];
if (!acc[date]) acc[date] = 0; if (!acc[date]) acc[date] = 0;
acc[date]++; acc[date]++;
return acc; return acc;
}, {}); }, {});
const likesByDate = likes.reduce((acc: any, like) => { const likesByDate = likes.reduce((acc: Record<string, number>, like) => {
const date = new Date(like.date_created).toISOString().split("T")[0]; const date = new Date(like.date_created).toISOString().split("T")[0];
if (!acc[date]) acc[date] = 0; if (!acc[date]) acc[date] = 0;
acc[date]++; acc[date]++;
@@ -499,7 +501,7 @@ builder.queryField("adminListVideos", (t) =>
const limit = args.limit ?? 50; const limit = args.limit ?? 50;
const offset = args.offset ?? 0; const offset = args.offset ?? 0;
const conditions: any[] = []; const conditions: SQL<unknown>[] = [];
if (args.search) conditions.push(ilike(videos.title, `%${args.search}%`)); if (args.search) conditions.push(ilike(videos.title, `%${args.search}%`));
if (args.premium !== null && args.premium !== undefined) if (args.premium !== null && args.premium !== undefined)
conditions.push(eq(videos.premium, args.premium)); conditions.push(eq(videos.premium, args.premium));
@@ -517,7 +519,7 @@ builder.queryField("adminListVideos", (t) =>
.offset(offset), .offset(offset),
ctx.db.select({ total: count() }).from(videos).where(where), ctx.db.select({ total: count() }).from(videos).where(where),
]); ]);
const items = await Promise.all(rows.map((v: any) => enrichVideo(ctx.db, v))); const items = await Promise.all(rows.map((v) => enrichVideo(ctx.db, v)));
return { items, total: totalRows[0]?.total ?? 0 }; return { items, total: totalRows[0]?.total ?? 0 };
}, },
}), }),
@@ -590,7 +592,7 @@ builder.mutationField("updateVideo", (t) =>
const updated = await ctx.db const updated = await ctx.db
.update(videos) .update(videos)
.set(updates as any) .set(updates as Partial<typeof videos.$inferInsert>)
.where(eq(videos.id, args.id)) .where(eq(videos.id, args.id))
.returning(); .returning();
if (!updated[0]) return null; if (!updated[0]) return null;

View File

@@ -47,7 +47,7 @@ export async function calculateWeightedScore(db: DB, userId: string): Promise<nu
FROM user_points FROM user_points
WHERE user_id = ${userId} WHERE user_id = ${userId}
`); `);
return parseFloat((result.rows[0] as any)?.weighted_score || "0"); return parseFloat((result.rows[0] as { weighted_score?: string })?.weighted_score || "0");
} }
export async function updateUserStats(db: DB, userId: string): Promise<void> { export async function updateUserStats(db: DB, userId: string): Promise<void> {
@@ -84,7 +84,7 @@ export async function updateUserStats(db: DB, userId: string): Promise<void> {
sql`, `, sql`, `,
)}) )})
`); `);
playbacksCount = parseInt((playbacksResult.rows[0] as any)?.count || "0"); playbacksCount = parseInt((playbacksResult.rows[0] as { count?: string })?.count || "0");
} else { } else {
const playbacksResult = await db const playbacksResult = await db
.select({ count: count() }) .select({ count: count() })
@@ -242,7 +242,7 @@ async function getAchievementProgress(
WHERE rp.user_id = ${userId} WHERE rp.user_id = ${userId}
AND r.user_id != ${userId} AND r.user_id != ${userId}
`); `);
return parseInt((result.rows[0] as any)?.count || "0"); return parseInt((result.rows[0] as { count?: string })?.count || "0");
} }
if (["completionist_10", "completionist_100"].includes(code)) { if (["completionist_10", "completionist_100"].includes(code)) {
@@ -293,7 +293,7 @@ async function getAchievementProgress(
WHERE rp.user_id = ${userId} AND r.user_id != ${userId} WHERE rp.user_id = ${userId} AND r.user_id != ${userId}
`); `);
const rc = recordingsResult[0]?.count || 0; const rc = recordingsResult[0]?.count || 0;
const pc = parseInt((playsResult.rows[0] as any)?.count || "0"); const pc = parseInt((playsResult.rows[0] as { count?: string })?.count || "0");
return rc >= 50 && pc >= 100 ? 1 : 0; return rc >= 50 && pc >= 100 ? 1 : 0;
} }

View File

@@ -38,7 +38,7 @@
isMobileMenuOpen = false; isMobileMenuOpen = false;
} }
function isActiveLink(link: any) { function isActiveLink(link: { name: string; href: string }) {
return ( return (
(page.url.pathname === "/" && link === navLinks[0]) || (page.url.pathname === "/" && link === navLinks[0]) ||
(page.url.pathname.startsWith(link.href) && link !== navLinks[0]) (page.url.pathname.startsWith(link.href) && link !== navLinks[0])

View File

@@ -20,7 +20,9 @@
{title} {title}
</h1> </h1>
{#if description} {#if description}
<p class="text-xl md:text-2xl text-muted-foreground mb-10 leading-relaxed max-w-4xl mx-auto"> <p
class="text-xl md:text-2xl text-muted-foreground mb-10 leading-relaxed max-w-4xl mx-auto"
>
{description} {description}
</p> </p>
{/if} {/if}

View File

@@ -26,7 +26,7 @@
return "text-green-400 bg-green-400/20"; return "text-green-400 bg-green-400/20";
case "draft": case "draft":
return "text-yellow-400 bg-yellow-400/20"; return "text-yellow-400 bg-yellow-400/20";
default: default:
return "text-gray-400 bg-gray-400/20"; return "text-gray-400 bg-gray-400/20";
} }
} }

View File

@@ -1245,7 +1245,9 @@ export async function adminGetUser(userId: string, token?: string) {
"adminGetUser", "adminGetUser",
async () => { async () => {
const client = token ? getAuthClient(token) : getGraphQLClient(); const client = token ? getAuthClient(token) : getGraphQLClient();
const data = await client.request<{ adminGetUser: any }>(ADMIN_GET_USER_QUERY, { userId }); const data = await client.request<{
adminGetUser: User & { photos: Array<{ id: string; filename: string }> };
}>(ADMIN_GET_USER_QUERY, { userId });
return data.adminGetUser; return data.adminGetUser;
}, },
{ userId }, { userId },

View File

@@ -75,7 +75,9 @@
class="w-24 h-24 rounded-full mx-auto object-cover ring-4 ring-primary/20 group-hover:ring-primary/40 transition-all bg-muted" class="w-24 h-24 rounded-full mx-auto object-cover ring-4 ring-primary/20 group-hover:ring-primary/40 transition-all bg-muted"
/> />
</div> </div>
<h3 class="font-semibold text-lg group-hover:text-primary transition-colors">{model.artist_name}</h3> <h3 class="font-semibold text-lg group-hover:text-primary transition-colors">
{model.artist_name}
</h3>
</CardContent> </CardContent>
</Card> </Card>
</a> </a>
@@ -110,7 +112,9 @@
class="absolute inset-0 bg-gradient-to-t from-black/60 to-transparent group-hover:scale-105 transition-transform duration-300" class="absolute inset-0 bg-gradient-to-t from-black/60 to-transparent group-hover:scale-105 transition-transform duration-300"
></div> ></div>
<div class="absolute bottom-2 left-2 text-white text-sm font-medium"> <div class="absolute bottom-2 left-2 text-white text-sm font-medium">
{#if video.movie_file?.duration}{formatVideoDuration(video.movie_file.duration)}{/if} {#if video.movie_file?.duration}{formatVideoDuration(
video.movie_file.duration,
)}{/if}
</div> </div>
<div <div
class="absolute inset-0 flex items-center justify-center opacity-0 group-hover:opacity-100 transition-opacity" class="absolute inset-0 flex items-center justify-center opacity-0 group-hover:opacity-100 transition-opacity"

View File

@@ -66,8 +66,8 @@
}); });
toast.success($_("admin.article_form.update_success")); toast.success($_("admin.article_form.update_success"));
goto("/admin/articles"); goto("/admin/articles");
} catch (e: any) { } catch (e) {
toast.error(e?.message ?? $_("admin.article_form.update_error")); toast.error((e instanceof Error ? e.message : null) ?? $_("admin.article_form.update_error"));
} finally { } finally {
saving = false; saving = false;
} }

View File

@@ -67,8 +67,8 @@
}); });
toast.success($_("admin.article_form.create_success")); toast.success($_("admin.article_form.create_success"));
goto("/admin/articles"); goto("/admin/articles");
} catch (e: any) { } catch (e) {
toast.error(e?.message ?? $_("admin.article_form.create_error")); toast.error((e instanceof Error ? e.message : null) ?? $_("admin.article_form.create_error"));
} finally { } finally {
saving = false; saving = false;
} }

View File

@@ -91,8 +91,8 @@
isAdmin, isAdmin,
}); });
toast.success($_("admin.user_edit.save_success")); toast.success($_("admin.user_edit.save_success"));
} catch (e: any) { } catch (e) {
toast.error(e?.message ?? $_("admin.user_edit.save_error")); toast.error((e instanceof Error ? e.message : null) ?? $_("admin.user_edit.save_error"));
} finally { } finally {
saving = false; saving = false;
} }

View File

@@ -77,8 +77,8 @@
await setVideoModels(data.video.id, selectedModelIds); await setVideoModels(data.video.id, selectedModelIds);
toast.success($_("admin.video_form.update_success")); toast.success($_("admin.video_form.update_success"));
goto("/admin/videos"); goto("/admin/videos");
} catch (e: any) { } catch (e) {
toast.error(e?.message ?? $_("admin.video_form.update_error")); toast.error((e instanceof Error ? e.message : null) ?? $_("admin.video_form.update_error"));
} finally { } finally {
saving = false; saving = false;
} }

View File

@@ -89,8 +89,8 @@
} }
toast.success($_("admin.video_form.create_success")); toast.success($_("admin.video_form.create_success"));
goto("/admin/videos"); goto("/admin/videos");
} catch (e: any) { } catch (e) {
toast.error(e?.message ?? $_("admin.video_form.create_error")); toast.error((e instanceof Error ? e.message : null) ?? $_("admin.video_form.create_error"));
} finally { } finally {
saving = false; saving = false;
} }

View File

@@ -32,8 +32,9 @@
isLoading = true; isLoading = true;
await login(email, password); await login(email, password);
goto("/videos", { invalidateAll: true }); goto("/videos", { invalidateAll: true });
} catch (err: any) { } catch (err) {
const raw = err.response?.errors?.[0]?.message ?? err.message; const e = err as { response?: { errors?: Array<{ message: string }> }; message?: string };
const raw = e.response?.errors?.[0]?.message ?? e.message;
error = raw === "Invalid credentials" ? $_("auth.login.error_invalid_credentials") : raw; error = raw === "Invalid credentials" ? $_("auth.login.error_invalid_credentials") : raw;
isError = true; isError = true;
} finally { } finally {

View File

@@ -315,8 +315,8 @@
size="sm" size="sm"
disabled={data.page <= 1} disabled={data.page <= 1}
onclick={() => goToPage(data.page - 1)} onclick={() => goToPage(data.page - 1)}
class="border-primary/20 hover:bg-primary/10" class="border-primary/20 hover:bg-primary/10">{$_("common.previous")}</Button
>{$_("common.previous")}</Button> >
{#each pageNumbers() as p, i (i)} {#each pageNumbers() as p, i (i)}
{#if p === -1} {#if p === -1}
<span class="px-2 text-muted-foreground select-none"></span> <span class="px-2 text-muted-foreground select-none"></span>
@@ -327,8 +327,8 @@
onclick={() => goToPage(p)} onclick={() => goToPage(p)}
class={p === data.page class={p === data.page
? "bg-gradient-to-r from-primary to-accent min-w-9" ? "bg-gradient-to-r from-primary to-accent min-w-9"
: "border-primary/20 hover:bg-primary/10 min-w-9"} : "border-primary/20 hover:bg-primary/10 min-w-9"}>{p}</Button
>{p}</Button> >
{/if} {/if}
{/each} {/each}
<Button <Button
@@ -336,8 +336,8 @@
size="sm" size="sm"
disabled={data.page >= totalPages} disabled={data.page >= totalPages}
onclick={() => goToPage(data.page + 1)} onclick={() => goToPage(data.page + 1)}
class="border-primary/20 hover:bg-primary/10" class="border-primary/20 hover:bg-primary/10">{$_("common.next")}</Button
>{$_("common.next")}</Button> >
</div> </div>
<p class="text-sm text-muted-foreground"> <p class="text-sm text-muted-foreground">
{$_("common.total_results", { values: { total: data.total } })} {$_("common.total_results", { values: { total: data.total } })}

View File

@@ -153,10 +153,15 @@
<div class="flex-1"> <div class="flex-1">
<h3 class="font-semibold text-lg mb-1">About {author.artist_name}</h3> <h3 class="font-semibold text-lg mb-1">About {author.artist_name}</h3>
{#if author.description} {#if author.description}
<p class="text-sm text-muted-foreground mb-3 leading-relaxed">{author.description}</p> <p class="text-sm text-muted-foreground mb-3 leading-relaxed">
{author.description}
</p>
{/if} {/if}
{#if author.slug} {#if author.slug}
<a href="/models/{author.slug}" class="inline-flex items-center gap-1 text-sm text-primary hover:underline"> <a
href="/models/{author.slug}"
class="inline-flex items-center gap-1 text-sm text-primary hover:underline"
>
View profile View profile
<span class="icon-[ri--arrow-right-line] w-3.5 h-3.5"></span> <span class="icon-[ri--arrow-right-line] w-3.5 h-3.5"></span>
</a> </a>

View File

@@ -90,8 +90,9 @@
}); });
toast.success($_("me.settings.toast_update")); toast.success($_("me.settings.toast_update"));
invalidateAll(); invalidateAll();
} catch (err: any) { } catch (err) {
profileError = err.response?.errors?.[0]?.message ?? err.message; const e = err as { response?: { errors?: Array<{ message: string }> }; message?: string };
profileError = e.response?.errors?.[0]?.message ?? e.message ?? "Unknown error";
isProfileError = true; isProfileError = true;
} finally { } finally {
isProfileLoading = false; isProfileLoading = false;
@@ -114,8 +115,9 @@
toast.success($_("me.settings.toast_update")); toast.success($_("me.settings.toast_update"));
invalidateAll(); invalidateAll();
password = confirmPassword = ""; password = confirmPassword = "";
} catch (err: any) { } catch (err) {
securityError = err.response?.errors?.[0]?.message ?? err.message; const e = err as { response?: { errors?: Array<{ message: string }> }; message?: string };
securityError = e.response?.errors?.[0]?.message ?? e.message ?? "Unknown error";
isSecurityError = true; isSecurityError = true;
} finally { } finally {
isSecurityLoading = false; isSecurityLoading = false;
@@ -290,9 +292,7 @@
</div> </div>
</FileDropZone> </FileDropZone>
<div class="flex flex-col gap-1"> <div class="flex flex-col gap-1">
<p class="text-sm text-muted-foreground"> <p class="text-sm text-muted-foreground">JPG, PNG · max 2 MB</p>
JPG, PNG · max 2 MB
</p>
<p class="text-xs text-muted-foreground/70"> <p class="text-xs text-muted-foreground/70">
Click or drop to {avatar ? "change" : "upload"} Click or drop to {avatar ? "change" : "upload"}
</p> </p>

View File

@@ -180,7 +180,6 @@
<!-- <span>{model.videos} videos</span> --> <!-- <span>{model.videos} videos</span> -->
<!-- category not available --> <!-- category not available -->
</div> </div>
</CardContent> </CardContent>
</Card> </Card>
</a> </a>
@@ -205,8 +204,8 @@
size="sm" size="sm"
disabled={data.page <= 1} disabled={data.page <= 1}
onclick={() => goToPage(data.page - 1)} onclick={() => goToPage(data.page - 1)}
class="border-primary/20 hover:bg-primary/10" class="border-primary/20 hover:bg-primary/10">{$_("common.previous")}</Button
>{$_("common.previous")}</Button> >
{#each pageNumbers() as p, i (i)} {#each pageNumbers() as p, i (i)}
{#if p === -1} {#if p === -1}
<span class="px-2 text-muted-foreground select-none"></span> <span class="px-2 text-muted-foreground select-none"></span>
@@ -217,8 +216,8 @@
onclick={() => goToPage(p)} onclick={() => goToPage(p)}
class={p === data.page class={p === data.page
? "bg-gradient-to-r from-primary to-accent min-w-9" ? "bg-gradient-to-r from-primary to-accent min-w-9"
: "border-primary/20 hover:bg-primary/10 min-w-9"} : "border-primary/20 hover:bg-primary/10 min-w-9"}>{p}</Button
>{p}</Button> >
{/if} {/if}
{/each} {/each}
<Button <Button
@@ -226,8 +225,8 @@
size="sm" size="sm"
disabled={data.page >= totalPages} disabled={data.page >= totalPages}
onclick={() => goToPage(data.page + 1)} onclick={() => goToPage(data.page + 1)}
class="border-primary/20 hover:bg-primary/10" class="border-primary/20 hover:bg-primary/10">{$_("common.next")}</Button
>{$_("common.next")}</Button> >
</div> </div>
<p class="text-sm text-muted-foreground"> <p class="text-sm text-muted-foreground">
{$_("common.total_results", { values: { total: data.total } })} {$_("common.total_results", { values: { total: data.total } })}

View File

@@ -30,8 +30,9 @@
await requestPassword(email); await requestPassword(email);
toast.success($_("auth.password_request.toast_request", { values: { email } })); toast.success($_("auth.password_request.toast_request", { values: { email } }));
goto("/login"); goto("/login");
} catch (err: any) { } catch (err) {
error = err.response?.errors?.[0]?.message ?? err.message; const e = err as { response?: { errors?: Array<{ message: string }> }; message?: string };
error = e.response?.errors?.[0]?.message ?? e.message ?? "Unknown error";
isError = true; isError = true;
} finally { } finally {
isLoading = false; isLoading = false;

View File

@@ -39,8 +39,9 @@
await resetPassword(data.token, password); await resetPassword(data.token, password);
toast.success($_("auth.password_reset.toast_reset")); toast.success($_("auth.password_reset.toast_reset"));
goto("/login"); goto("/login");
} catch (err: any) { } catch (err) {
const raw = err.response?.errors?.[0]?.message ?? err.message; const e = err as { response?: { errors?: Array<{ message: string }> }; message?: string };
const raw = e.response?.errors?.[0]?.message ?? e.message;
const tokenErrors = ["Invalid or expired reset token", "Reset token expired"]; const tokenErrors = ["Invalid or expired reset token", "Reset token expired"];
error = tokenErrors.includes(raw) ? $_("auth.password_reset.error_invalid_token") : raw; error = tokenErrors.includes(raw) ? $_("auth.password_reset.error_invalid_token") : raw;
isError = true; isError = true;

View File

@@ -47,8 +47,9 @@
await register(email, password, firstName, lastName); await register(email, password, firstName, lastName);
toast.success($_("auth.signup.toast_register", { values: { email } })); toast.success($_("auth.signup.toast_register", { values: { email } }));
goto("/login"); goto("/login");
} catch (err: any) { } catch (err) {
const raw = err.response?.errors?.[0]?.message ?? err.message; const e = err as { response?: { errors?: Array<{ message: string }> }; message?: string };
const raw = e.response?.errors?.[0]?.message ?? e.message;
error = raw === "Email already registered" ? $_("auth.signup.error_email_taken") : raw; error = raw === "Email already registered" ? $_("auth.signup.error_email_taken") : raw;
isError = true; isError = true;
} finally { } finally {

View File

@@ -27,7 +27,7 @@
const filteredItems = $derived(() => { const filteredItems = $derived(() => {
return data.items return data.items
.filter((item: any) => { .filter((item) => {
const matchesSearch = const matchesSearch =
searchQuery === "" || item.title.toLowerCase().includes(searchQuery.toLowerCase()); searchQuery === "" || item.title.toLowerCase().includes(searchQuery.toLowerCase());
const matchesCategory = categoryFilter === "all" || item.category === categoryFilter; const matchesCategory = categoryFilter === "all" || item.category === categoryFilter;
@@ -72,7 +72,9 @@
/> />
</div> </div>
<Select type="single" bind:value={categoryFilter}> <Select type="single" bind:value={categoryFilter}>
<SelectTrigger class="w-full md:w-48 bg-background/50 border-primary/20 focus:border-primary"> <SelectTrigger
class="w-full md:w-48 bg-background/50 border-primary/20 focus:border-primary"
>
<span class="icon-[ri--filter-line] w-4 h-4 mr-2"></span> <span class="icon-[ri--filter-line] w-4 h-4 mr-2"></span>
{categoryFilter === "all" {categoryFilter === "all"
? $_("tags.categories.all") ? $_("tags.categories.all")

View File

@@ -161,7 +161,9 @@
<div <div
class="absolute bottom-3 left-3 bg-black/70 text-white text-sm px-2 py-1 rounded font-medium" class="absolute bottom-3 left-3 bg-black/70 text-white text-sm px-2 py-1 rounded font-medium"
> >
{#if video.movie_file?.duration}{formatVideoDuration(video.movie_file.duration)}{/if} {#if video.movie_file?.duration}{formatVideoDuration(
video.movie_file.duration,
)}{/if}
</div> </div>
<!-- Premium Badge --> <!-- Premium Badge -->
@@ -236,7 +238,6 @@
{video.category} {video.category}
</span> --> </span> -->
</div> </div>
</CardContent> </CardContent>
</Card> </Card>
</a> </a>
@@ -263,8 +264,8 @@
size="sm" size="sm"
disabled={data.page <= 1} disabled={data.page <= 1}
onclick={() => goToPage(data.page - 1)} onclick={() => goToPage(data.page - 1)}
class="border-primary/20 hover:bg-primary/10" class="border-primary/20 hover:bg-primary/10">{$_("common.previous")}</Button
>{$_("common.previous")}</Button> >
{#each pageNumbers() as p, i (i)} {#each pageNumbers() as p, i (i)}
{#if p === -1} {#if p === -1}
<span class="px-2 text-muted-foreground select-none"></span> <span class="px-2 text-muted-foreground select-none"></span>
@@ -275,8 +276,8 @@
onclick={() => goToPage(p)} onclick={() => goToPage(p)}
class={p === data.page class={p === data.page
? "bg-gradient-to-r from-primary to-accent min-w-9" ? "bg-gradient-to-r from-primary to-accent min-w-9"
: "border-primary/20 hover:bg-primary/10 min-w-9"} : "border-primary/20 hover:bg-primary/10 min-w-9"}>{p}</Button
>{p}</Button> >
{/if} {/if}
{/each} {/each}
<Button <Button
@@ -284,8 +285,8 @@
size="sm" size="sm"
disabled={data.page >= totalPages} disabled={data.page >= totalPages}
onclick={() => goToPage(data.page + 1)} onclick={() => goToPage(data.page + 1)}
class="border-primary/20 hover:bg-primary/10" class="border-primary/20 hover:bg-primary/10">{$_("common.next")}</Button
>{$_("common.next")}</Button> >
</div> </div>
<p class="text-sm text-muted-foreground"> <p class="text-sm text-muted-foreground">
{$_("common.total_results", { values: { total: data.total } })} {$_("common.total_results", { values: { total: data.total } })}

View File

@@ -58,8 +58,8 @@
isLiked = true; isLiked = true;
toast.success("Added to liked videos"); toast.success("Added to liked videos");
} }
} catch (error: any) { } catch (error) {
toast.error(error.message || "Failed to update like"); toast.error((error instanceof Error ? error.message : null) || "Failed to update like");
} finally { } finally {
isLikeLoading = false; isLikeLoading = false;
} }
@@ -86,8 +86,8 @@
invalidateAll(); invalidateAll();
newComment = ""; newComment = "";
showComments = true; showComments = true;
} catch (err: any) { } catch (err) {
commentError = err.message; commentError = err instanceof Error ? err.message : "Unknown error";
isCommentError = true; isCommentError = true;
} finally { } finally {
isCommentLoading = false; isCommentLoading = false;