import { GraphQLError } from "graphql"; import { builder } from "../builder"; import { CurrentUserType, UserType, AdminUserListType, AdminUserDetailType } from "../types/index"; import { users, user_photos, files } from "../../db/schema/index"; import { eq, ilike, or, count, and, asc, type SQL } from "drizzle-orm"; import { requireAdmin } from "../../lib/acl"; builder.queryField("me", (t) => t.field({ type: CurrentUserType, nullable: true, resolve: async (_root, _args, ctx) => { if (!ctx.currentUser) return null; const user = await ctx.db .select() .from(users) .where(eq(users.id, ctx.currentUser.id)) .limit(1); return user[0] || null; }, }), ); builder.queryField("userProfile", (t) => t.field({ type: UserType, nullable: true, args: { id: t.arg.string({ required: true }), }, resolve: async (_root, args, ctx) => { const user = await ctx.db.select().from(users).where(eq(users.id, args.id)).limit(1); return user[0] || null; }, }), ); builder.mutationField("updateProfile", (t) => t.field({ type: CurrentUserType, nullable: true, args: { firstName: t.arg.string(), lastName: t.arg.string(), artistName: t.arg.string(), description: t.arg.string(), tags: t.arg.stringList(), avatar: t.arg.string(), }, resolve: async (_root, args, ctx) => { if (!ctx.currentUser) throw new GraphQLError("Unauthorized"); const updates: Record = { date_updated: new Date() }; if (args.firstName !== undefined && args.firstName !== null) updates.first_name = args.firstName; if (args.lastName !== undefined && args.lastName !== null) updates.last_name = args.lastName; if (args.artistName !== undefined && args.artistName !== null) updates.artist_name = args.artistName; if (args.description !== undefined && args.description !== null) updates.description = args.description; if (args.tags !== undefined && args.tags !== null) updates.tags = args.tags; if (args.avatar !== undefined) updates.avatar = args.avatar; await ctx.db .update(users) .set(updates as Partial) .where(eq(users.id, ctx.currentUser.id)); const updated = await ctx.db .select() .from(users) .where(eq(users.id, ctx.currentUser.id)) .limit(1); return updated[0] || null; }, }), ); // ─── Admin queries & mutations ──────────────────────────────────────────────── builder.queryField("adminListUsers", (t) => t.field({ type: AdminUserListType, args: { role: t.arg.string(), search: t.arg.string(), limit: t.arg.int(), offset: t.arg.int(), }, resolve: async (_root, args, ctx) => { requireAdmin(ctx); const limit = args.limit ?? 50; const offset = args.offset ?? 0; const conditions: SQL[] = []; if (args.role) { conditions.push(eq(users.role, args.role as "model" | "viewer" | "admin")); } if (args.search) { const pattern = `%${args.search}%`; conditions.push( or(ilike(users.email, pattern), ilike(users.artist_name, pattern)) as SQL, ); } const where = conditions.length > 0 ? and(...conditions) : undefined; const [items, totalRows] = await Promise.all([ ctx.db .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 }; }, }), ); builder.mutationField("adminUpdateUser", (t) => t.field({ type: UserType, nullable: true, args: { userId: t.arg.string({ required: true }), role: t.arg.string(), isAdmin: t.arg.boolean(), firstName: t.arg.string(), lastName: t.arg.string(), artistName: t.arg.string(), avatarId: t.arg.string(), bannerId: t.arg.string(), }, resolve: async (_root, args, ctx) => { requireAdmin(ctx); const updates: Record = { date_updated: new Date() }; 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.firstName !== undefined && args.firstName !== null) updates.first_name = args.firstName; if (args.lastName !== undefined && args.lastName !== null) updates.last_name = args.lastName; if (args.artistName !== undefined && args.artistName !== null) updates.artist_name = args.artistName; if (args.avatarId !== undefined && args.avatarId !== null) updates.avatar = args.avatarId; if (args.bannerId !== undefined && args.bannerId !== null) updates.banner = args.bannerId; const updated = await ctx.db .update(users) .set(updates as Partial) .where(eq(users.id, args.userId)) .returning(); return updated[0] || null; }, }), ); builder.mutationField("adminDeleteUser", (t) => t.field({ type: "Boolean", args: { userId: t.arg.string({ required: true }), }, resolve: async (_root, args, ctx) => { requireAdmin(ctx); if (args.userId === ctx.currentUser!.id) throw new GraphQLError("Cannot delete yourself"); await ctx.db.delete(users).where(eq(users.id, args.userId)); return true; }, }), ); builder.queryField("adminGetUser", (t) => t.field({ type: AdminUserDetailType, nullable: true, args: { userId: t.arg.string({ required: true }), }, resolve: async (_root, args, ctx) => { requireAdmin(ctx); const user = await ctx.db.select().from(users).where(eq(users.id, args.userId)).limit(1); if (!user[0]) return null; const photoRows = await ctx.db .select({ id: files.id, filename: files.filename }) .from(user_photos) .leftJoin(files, eq(user_photos.file_id, files.id)) .where(eq(user_photos.user_id, args.userId)) .orderBy(user_photos.sort); const seen = new Set(); const photos = photoRows .filter((p) => p.id !== null && !seen.has(p.id!) && seen.add(p.id!)) .map((p) => ({ id: p.id!, filename: p.filename! })); return { ...user[0], photos }; }, }), ); builder.mutationField("adminAddUserPhoto", (t) => t.field({ type: "Boolean", args: { userId: t.arg.string({ required: true }), fileId: t.arg.string({ required: true }), }, resolve: async (_root, args, ctx) => { requireAdmin(ctx); await ctx.db.insert(user_photos).values({ user_id: args.userId, file_id: args.fileId }); return true; }, }), ); builder.mutationField("adminRemoveUserPhoto", (t) => t.field({ type: "Boolean", args: { userId: t.arg.string({ required: true }), fileId: t.arg.string({ required: true }), }, resolve: async (_root, args, ctx) => { requireAdmin(ctx); await ctx.db .delete(user_photos) .where(and(eq(user_photos.user_id, args.userId), eq(user_photos.file_id, args.fileId))); return true; }, }), );