207 lines
5.8 KiB
TypeScript

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<Category, "slug" | "createdAt" | "name">;
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,
});
}),
});