feat: add admin tables for comments and recordings
All checks were successful
Build and Push Backend Image / build (push) Successful in 44s
Build and Push Frontend Image / build (push) Successful in 4m20s

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-07 11:29:48 +01:00
parent dfe49b5882
commit 754a236e51
12 changed files with 720 additions and 12 deletions

View File

@@ -33,7 +33,11 @@ export async function buildContext(ctx: YogaInitialContext & ServerContext): Pro
const session = await getSession(token); // also slides TTL
if (session) {
const dbInstance = ctx.db || db;
const [dbUser] = await dbInstance.select().from(users).where(eq(users.id, session.id)).limit(1);
const [dbUser] = await dbInstance
.select()
.from(users)
.where(eq(users.id, session.id))
.limit(1);
if (dbUser) {
currentUser = {
id: dbUser.id,

View File

@@ -105,11 +105,7 @@ builder.queryField("adminGetArticle", (t) =>
},
resolve: async (_root, args, ctx) => {
requireAdmin(ctx);
const article = await ctx.db
.select()
.from(articles)
.where(eq(articles.id, args.id))
.limit(1);
const article = await ctx.db.select().from(articles).where(eq(articles.id, args.id)).limit(1);
if (!article[0]) return null;
return enrichArticle(ctx.db, article[0]);
},

View File

@@ -1,10 +1,10 @@
import { GraphQLError } from "graphql";
import { builder } from "../builder";
import { CommentType } from "../types/index";
import { CommentType, AdminCommentListType } from "../types/index";
import { comments, users } from "../../db/schema/index";
import { eq, and, desc } from "drizzle-orm";
import { eq, and, desc, ilike, or, count } from "drizzle-orm";
import { awardPoints, checkAchievements } from "../../lib/gamification";
import { requireOwnerOrAdmin } from "../../lib/acl";
import { requireOwnerOrAdmin, requireAdmin } from "../../lib/acl";
builder.queryField("commentsForVideo", (t) =>
t.field({
@@ -96,3 +96,52 @@ builder.mutationField("deleteComment", (t) =>
},
}),
);
builder.queryField("adminListComments", (t) =>
t.field({
type: AdminCommentListType,
args: {
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 = args.search ? [ilike(comments.comment, `%${args.search}%`)] : [];
const where = conditions.length > 0 ? and(...conditions) : undefined;
const [commentList, totalRows] = await Promise.all([
ctx.db
.select()
.from(comments)
.where(where)
.orderBy(desc(comments.date_created))
.limit(limit)
.offset(offset),
ctx.db.select({ total: count() }).from(comments).where(where),
]);
const items = await Promise.all(
commentList.map(async (c: any) => {
const user = await ctx.db
.select({
id: users.id,
first_name: users.first_name,
last_name: users.last_name,
artist_name: users.artist_name,
avatar: users.avatar,
})
.from(users)
.where(eq(users.id, c.user_id))
.limit(1);
return { ...c, user: user[0] || null };
}),
);
return { items, total: totalRows[0]?.total ?? 0 };
},
}),
);

View File

@@ -1,10 +1,11 @@
import { GraphQLError } from "graphql";
import { builder } from "../builder";
import { RecordingType } from "../types/index";
import { recordings, recording_plays } from "../../db/schema/index";
import { eq, and, desc, ne } from "drizzle-orm";
import { RecordingType, AdminRecordingListType } from "../types/index";
import { recordings, recording_plays, users } from "../../db/schema/index";
import { eq, and, desc, ne, ilike, count } from "drizzle-orm";
import { slugify } from "../../lib/slugify";
import { awardPoints, checkAchievements } from "../../lib/gamification";
import { requireAdmin } from "../../lib/acl";
builder.queryField("recordings", (t) =>
t.field({
@@ -340,3 +341,52 @@ builder.mutationField("updateRecordingPlay", (t) =>
},
}),
);
builder.queryField("adminListRecordings", (t) =>
t.field({
type: AdminRecordingListType,
args: {
search: t.arg.string(),
status: 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: any[] = [];
if (args.search) conditions.push(ilike(recordings.title, `%${args.search}%`));
if (args.status) conditions.push(eq(recordings.status, args.status as any));
const where = conditions.length > 0 ? and(...conditions) : undefined;
const [rows, totalRows] = await Promise.all([
ctx.db
.select()
.from(recordings)
.where(where)
.orderBy(desc(recordings.date_created))
.limit(limit)
.offset(offset),
ctx.db.select({ total: count() }).from(recordings).where(where),
]);
return { items: rows, total: totalRows[0]?.total ?? 0 };
},
}),
);
builder.mutationField("adminDeleteRecording", (t) =>
t.field({
type: "Boolean",
args: {
id: t.arg.string({ required: true }),
},
resolve: async (_root, args, ctx) => {
requireAdmin(ctx);
await ctx.db.delete(recordings).where(eq(recordings.id, args.id));
return true;
},
}),
);

View File

@@ -374,6 +374,24 @@ export const AdminArticleListType = builder
}),
});
export const AdminCommentListType = builder
.objectRef<{ items: Comment[]; total: number }>("AdminCommentList")
.implement({
fields: (t) => ({
items: t.expose("items", { type: [CommentType] }),
total: t.exposeInt("total"),
}),
});
export const AdminRecordingListType = builder
.objectRef<{ items: Recording[]; total: number }>("AdminRecordingList")
.implement({
fields: (t) => ({
items: t.expose("items", { type: [RecordingType] }),
total: t.exposeInt("total"),
}),
});
export const AdminUserListType = builder
.objectRef<{ items: User[]; total: number }>("AdminUserList")
.implement({