191 lines
5.5 KiB
TypeScript

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,
});
}),
});