import { z } from "zod"; import { createTRPCRouter, protectedProcedure, publicProcedure, } from "@/server/api/trpc"; import { categories, Category } from "@/server/db/schema"; import { and, asc, count, desc, eq, gt, gte, ilike, like, lte, } from "drizzle-orm"; import { hasPermission, Role } from "@/lib/validation/permissions"; import { categoryFilterSchema, categorySchema, } from "@/lib/validation/zod/category"; import { generateSlug } from "@/lib/utils"; import SuperJSON from "superjson"; type CategoryCursor = Pick; const getCategorySorting = (sort: string, cursor?: CategoryCursor) => { // Default to newest const baseCase = { orderBy: [desc(categories.createdAt), asc(categories.slug)], cursor: cursor ? lte(categories.createdAt, cursor.createdAt) : undefined, }; switch (sort) { case "newest": return baseCase; case "oldest": return { orderBy: [asc(categories.createdAt), asc(categories.slug)], cursor: cursor ? gte(categories.createdAt, cursor.createdAt) : undefined, }; case "abc": return { orderBy: [asc(categories.name), asc(categories.slug)], cursor: cursor ? gte(categories.name, cursor.name) : undefined, }; case "cba": return { orderBy: [desc(categories.name), asc(categories.slug)], cursor: cursor ? lte(categories.name, cursor.name) : undefined, }; default: return baseCase; } }; export const categoryRouter = createTRPCRouter({ search: publicProcedure .input(z.object({ query: z.string() })) .query(async ({ ctx, input }) => { return await ctx.db.query.categories.findMany({ where: like(categories.name, "%" + input.query + "%"), }); }), get: publicProcedure .input(z.object({ slug: z.string(), with: z.any() })) .query(async ({ ctx, input }) => { return (await ctx.db.query.categories.findFirst({ where: eq(categories.slug, input.slug), with: input?.with, })) as Category; }), getAll: publicProcedure .input( z .object({ limit: z.number().optional(), }) .optional(), ) .query(async ({ ctx, input }) => { return await ctx.db.query.categories.findMany({ limit: input?.limit, }); }), getByCursor: publicProcedure .input( z.object({ limit: z.number().optional(), cursor: z.string().optional(), filter: categoryFilterSchema.optional(), }), ) .query(async ({ ctx, input }) => { const { cursor } = input!; const limit = input?.limit ?? 50; // Decode cursor if using it let cursorObj: CategoryCursor | undefined; if (cursor) { try { cursorObj = SuperJSON.parse(Buffer.from(cursor, "base64").toString()); } catch (e) { // Handle invalid cursor cursorObj = undefined; } } const queryFilterArg = input?.filter?.query?.length ? ilike(categories.name, "%" + input.filter.query + "%") : undefined; const sortConfig = input?.filter?.sort ?? "newest"; const { orderBy, cursor: cursorArg } = getCategorySorting( sortConfig, cursorObj, ); const items = (await ctx.db.query.categories.findMany({ where: and(cursorArg, queryFilterArg), limit: limit + 1, orderBy, columns: { name: true, slug: true, createdAt: true, }, })) as Category[]; let nextCursor: string | undefined = undefined; if (items.length > limit) { console.log("Configure next cursor"); const cursorItem = items.pop(); // Create a cursor object with the relevant fields for sorting const cursorData: CategoryCursor = { slug: cursorItem!.slug, createdAt: cursorItem!.createdAt, name: cursorItem!.name, }; // Encode the cursor as base64 nextCursor = Buffer.from(SuperJSON.stringify(cursorData)).toString( "base64", ); } return { items, nextCursor, }; }), getCount: publicProcedure.query(async ({ ctx }) => { return (await ctx.db.select({ count: count() }).from(categories))[0]?.count; }), create: protectedProcedure .input(z.object({ category: categorySchema })) .mutation(async ({ ctx, input }) => { const isEditor = hasPermission(ctx.session.user.role, Role.EDITOR); if (!isEditor) { throw new Error("You are not allowed to create categories"); } const slug = generateSlug(input.category.name); return await ctx.db .insert(categories) .values({ ...input.category, slug }) .returning({ slug: categories.slug, }); }), update: protectedProcedure .input(z.object({ category: categorySchema, categoryId: 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 categories"); } return await ctx.db .update(categories) .set(input.category) .where(eq(categories.id, input.categoryId)) .returning({ id: categories.id, }); }), delete: protectedProcedure .input(z.object({ categoryId: 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(categories) .where(eq(categories.id, input.categoryId)) .returning({ id: categories.id, }); }), });