import { z } from "zod"; import { createTRPCRouter, protectedProcedure, publicProcedure, } from "@/server/api/trpc"; import { Article, articles } from "@/server/db/schema"; import { articleFilterSchema, articleSchema, createArticleSchema, } from "@/lib/validation/zod/article"; import { and, asc, count, desc, eq, gt, ilike, like, sql } from "drizzle-orm"; import { hasPermission, Role } from "@/lib/validation/permissions"; import { generateSlug } from "@/lib/utils"; const getArticleSorting = (sort: string) => { switch (sort) { case "newest": return desc(articles.createdAt); case "oldest": return asc(articles.createdAt); case "abc": return asc(articles.title); case "cba": return desc(articles.title); default: return desc(articles.createdAt); // Default to newest } }; export const articleRouter = createTRPCRouter({ // queries search: publicProcedure .input(z.object({ query: z.string() })) .query(async ({ ctx, input }) => { return (await ctx.db.query.articles.findMany({ where: like(articles.title, "%" + input.query + "%"), with: { category: true }, })) as Article[]; }), get: publicProcedure .input(z.object({ slug: z.string() })) .query(async ({ ctx, input }) => { return await ctx.db.query.articles.findFirst({ where: eq(articles.slug, input.slug), with: { category: true }, }); }), getByCursor: publicProcedure .input( z.object({ limit: z.number().optional(), cursor: z.string().optional(), filter: articleFilterSchema.optional(), }), ) .query(async ({ ctx, input }) => { const { cursor } = input!; const limit = input?.limit ?? 50; const cursorArg = cursor ? gt(articles.slug, cursor) : undefined; const queryFilterArg = input?.filter?.query?.length ? ilike(articles.title, "%" + input.filter.query + "%") : undefined; const categoryArg = input?.filter?.category ? eq(articles.categoryId, input.filter.category) : undefined; const orderBy = getArticleSorting(input?.filter?.sort ?? "newest"); const items = await ctx.db.query.articles.findMany({ where: and( cursorArg, categoryArg, queryFilterArg, eq(articles.published, true), ), limit: limit + 1, orderBy, columns: { title: true, slug: true, createdAt: true, }, }); let nextCursor: typeof cursor | undefined = undefined; if (items.length > limit) { const nextItem = items.pop(); nextCursor = nextItem!.slug; } return { items, nextCursor, previousCursor: cursor, }; }), getAll: publicProcedure .input( z .object({ categoryId: z.string().optional(), limit: z.number().optional(), }) .optional(), ) .query(async ({ ctx, input }) => { return await ctx.db.query.articles.findMany({ where: input?.categoryId ? eq(articles.categoryId, input.categoryId) : undefined, limit: input?.limit, columns: { title: true, slug: true, createdAt: true, }, }); }), getCount: publicProcedure .input(z.object({ categoryId: z.string() }).optional()) .query(async ({ ctx, input }) => { return ( await ctx.db .select({ count: count() }) .from(articles) .where( input?.categoryId ? eq(articles.categoryId, input.categoryId) : undefined, ) )[0]?.count; }), // mutations create: protectedProcedure .input(z.object({ article: createArticleSchema })) .mutation(async ({ ctx, input }) => { const isEditor = hasPermission(ctx.session.user.role, Role.EDITOR); if (!isEditor) { throw new Error("You are not allowed to create articles"); } const slug = generateSlug(input.article.title); return await ctx.db .insert(articles) .values({ ...input.article, slug }) .returning({ slug: articles.slug, }) .onConflictDoUpdate({ target: articles.slug, set: { slug: sql`${slug} || '-' || (SELECT COUNT(*) FROM ${articles} WHERE slug LIKE ${slug + "-%"})`, }, }); }), update: protectedProcedure .input(z.object({ article: articleSchema, articleId: z.string() })) .mutation(async ({ ctx, input }) => { const isEditor = hasPermission(ctx.session.user.role, Role.EDITOR); if (!isEditor) { throw new Error("You are not allowed to update articles"); } console.log( "Content before save", JSON.stringify(input.article.content), ); return await ctx.db .update(articles) .set(input.article) .where(eq(articles.id, input.articleId)) .returning({ slug: articles.slug, }); }), delete: protectedProcedure .input(z.object({ articleId: z.string() })) .mutation(async ({ ctx, input }) => { const isEditor = hasPermission(ctx.session.user.role, Role.EDITOR); if (!isEditor) { throw new Error("You are not allowed to delete articles"); } return await ctx.db .delete(articles) .where(eq(articles.id, input.articleId)) .returning({ id: articles.id, }); }), });