Files
sexy/packages/backend/src/graphql/resolvers/articles.ts

256 lines
8.0 KiB
TypeScript
Raw Normal View History

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<string, unknown> = { 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;
},
}),
);