import { builder } from "../builder"; import { ArticleType, ArticleListType, AdminArticleListType } from "../types/index"; import { articles, users } from "../../db/schema/index"; import { eq, and, lte, desc, asc, ilike, or, count, arrayContains } from "drizzle-orm"; import { requireAdmin } from "../../lib/acl"; async function enrichArticle(db: any, article: any) { let author = null; if (article.author) { const authorUser = await db .select({ id: users.id, artist_name: users.artist_name, slug: users.slug, avatar: users.avatar, }) .from(users) .where(eq(users.id, article.author)) .limit(1); author = authorUser[0] || null; } return { ...article, author }; } builder.queryField("articles", (t) => t.field({ type: ArticleListType, args: { featured: t.arg.boolean(), limit: t.arg.int(), search: t.arg.string(), category: t.arg.string(), offset: t.arg.int(), sortBy: t.arg.string(), tag: t.arg.string(), }, resolve: async (_root, args, ctx) => { const pageSize = args.limit ?? 24; const offset = args.offset ?? 0; const conditions: any[] = [lte(articles.publish_date, new Date())]; if (args.featured !== null && args.featured !== undefined) { conditions.push(eq(articles.featured, args.featured)); } if (args.category) conditions.push(eq(articles.category, args.category)); if (args.tag) conditions.push(arrayContains(articles.tags, [args.tag])); if (args.search) { conditions.push( or( ilike(articles.title, `%${args.search}%`), ilike(articles.excerpt, `%${args.search}%`), ), ); } 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 [articleList, totalRows] = await Promise.all([ (ctx.db.select().from(articles).where(where) as any) .orderBy(...orderArgs) .limit(pageSize) .offset(offset), ctx.db.select({ total: count() }).from(articles).where(where), ]); const items = await Promise.all( articleList.map((article: any) => enrichArticle(ctx.db, article)), ); return { items, total: totalRows[0]?.total ?? 0 }; }, }), ); builder.queryField("article", (t) => t.field({ type: ArticleType, nullable: true, args: { slug: t.arg.string({ required: true }), }, resolve: async (_root, args, ctx) => { const article = await ctx.db .select() .from(articles) .where(and(eq(articles.slug, args.slug), lte(articles.publish_date, new Date()))) .limit(1); if (!article[0]) return null; return enrichArticle(ctx.db, article[0]); }, }), ); builder.queryField("adminGetArticle", (t) => t.field({ type: ArticleType, nullable: true, args: { id: t.arg.string({ required: true }), }, resolve: async (_root, args, ctx) => { requireAdmin(ctx); 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]); }, }), ); // ─── Admin queries & mutations ──────────────────────────────────────────────── builder.queryField("adminListArticles", (t) => t.field({ type: AdminArticleListType, args: { search: t.arg.string(), category: t.arg.string(), featured: t.arg.boolean(), 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( or( ilike(articles.title, `%${args.search}%`), ilike(articles.excerpt, `%${args.search}%`), ), ); } if (args.category) conditions.push(eq(articles.category, args.category)); if (args.featured !== null && args.featured !== undefined) conditions.push(eq(articles.featured, args.featured)); const where = conditions.length > 0 ? and(...conditions) : undefined; const [articleList, totalRows] = await Promise.all([ ctx.db .select() .from(articles) .where(where) .orderBy(desc(articles.publish_date)) .limit(limit) .offset(offset), ctx.db.select({ total: count() }).from(articles).where(where), ]); const items = await Promise.all( articleList.map((article: any) => enrichArticle(ctx.db, article)), ); return { items, total: totalRows[0]?.total ?? 0 }; }, }), ); builder.mutationField("createArticle", (t) => t.field({ type: ArticleType, args: { title: t.arg.string({ required: true }), slug: t.arg.string({ required: true }), excerpt: t.arg.string(), content: t.arg.string(), imageId: t.arg.string(), tags: t.arg.stringList(), category: t.arg.string(), featured: t.arg.boolean(), publishDate: t.arg.string(), }, resolve: async (_root, args, ctx) => { requireAdmin(ctx); const inserted = await ctx.db .insert(articles) .values({ title: args.title, slug: args.slug, excerpt: args.excerpt || null, content: args.content || null, image: args.imageId || null, tags: args.tags || [], category: args.category || null, featured: args.featured ?? false, publish_date: args.publishDate ? new Date(args.publishDate) : new Date(), author: ctx.currentUser!.id, }) .returning(); return enrichArticle(ctx.db, inserted[0]); }, }), ); builder.mutationField("updateArticle", (t) => t.field({ type: ArticleType, nullable: true, args: { id: t.arg.string({ required: true }), title: t.arg.string(), slug: t.arg.string(), excerpt: t.arg.string(), content: t.arg.string(), imageId: t.arg.string(), authorId: t.arg.string(), tags: t.arg.stringList(), category: t.arg.string(), featured: t.arg.boolean(), publishDate: t.arg.string(), }, resolve: async (_root, args, ctx) => { requireAdmin(ctx); const updates: Record = { date_updated: new Date() }; if (args.title !== undefined && args.title !== null) updates.title = args.title; if (args.slug !== undefined && args.slug !== null) updates.slug = args.slug; if (args.excerpt !== undefined) updates.excerpt = args.excerpt; if (args.content !== undefined) updates.content = args.content; if (args.imageId !== undefined) updates.image = args.imageId; if (args.authorId !== undefined) updates.author = args.authorId; if (args.tags !== undefined && args.tags !== null) updates.tags = args.tags; if (args.category !== undefined) updates.category = args.category; if (args.featured !== undefined && args.featured !== null) updates.featured = args.featured; if (args.publishDate !== undefined && args.publishDate !== null) updates.publish_date = new Date(args.publishDate); const updated = await ctx.db .update(articles) .set(updates as any) .where(eq(articles.id, args.id)) .returning(); if (!updated[0]) return null; return enrichArticle(ctx.db, updated[0]); }, }), ); builder.mutationField("deleteArticle", (t) => t.field({ type: "Boolean", args: { id: t.arg.string({ required: true }), }, resolve: async (_root, args, ctx) => { requireAdmin(ctx); await ctx.db.delete(articles).where(eq(articles.id, args.id)); return true; }, }), );