mr-shortman 1597d4f113
Some checks failed
Gitea Actions Demo / Explore-Gitea-Actions (push) Failing after 0s
fixed editor missing peaces
2025-03-15 23:48:44 +01:00

245 lines
7.1 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,
gte,
ilike,
like,
lte,
sql,
} from "drizzle-orm";
import { hasPermission, Role } from "@/lib/validation/permissions";
import { generateSlug } from "@/lib/utils";
import SuperJSON from "superjson";
type ArticleCursor = Pick<Article, "slug" | "createdAt" | "title">;
const getArticleSorting = (sort: string, cursor?: ArticleCursor) => {
// Default to newest
const baseCase = {
orderBy: [desc(articles.createdAt), asc(articles.slug)],
cursor: cursor ? lte(articles.createdAt, cursor.createdAt) : undefined,
};
switch (sort) {
case "newest":
return baseCase;
case "oldest":
return {
orderBy: [asc(articles.createdAt), asc(articles.slug)],
cursor: cursor ? gte(articles.createdAt, cursor.createdAt) : undefined,
};
case "abc":
return {
orderBy: [asc(articles.title), asc(articles.slug)],
cursor: cursor ? gte(articles.title, cursor.title) : undefined,
};
case "cba":
return {
orderBy: [desc(articles.title), asc(articles.slug)],
cursor: cursor ? lte(articles.title, cursor.title) : undefined,
};
default:
return baseCase;
}
};
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 }) => {
const user = ctx?.session?.user;
const isEditor = user ? hasPermission(user.role, Role.EDITOR) : false;
const publishedArg = !isEditor ? eq(articles.published, true) : undefined;
return (await ctx.db.query.articles.findFirst({
where: and(eq(articles.slug, input.slug), publishedArg),
with: { category: true },
})) as Article;
}),
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;
// Decode cursor if using it
let cursorObj: ArticleCursor | 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(articles.title, "%" + input.filter.query + "%")
: undefined;
const categoryArg = input?.filter?.category
? eq(articles.categoryId, input.filter.category)
: undefined;
const sortConfig = input?.filter?.sort ?? "newest";
const { orderBy, cursor: cursorArg } = getArticleSorting(
sortConfig,
cursorObj,
);
const user = ctx?.session?.user;
const isEditor = user ? hasPermission(user.role, Role.EDITOR) : false;
const publishedArg = !isEditor ? eq(articles.published, true) : undefined;
const items = await ctx.db.query.articles.findMany({
where: and(cursorArg, categoryArg, queryFilterArg, publishedArg),
limit: limit + 1,
orderBy,
columns: {
title: true,
slug: true,
createdAt: true,
published: isEditor ? true : false,
},
});
let nextCursor: string | undefined = undefined;
if (items.length > limit) {
const cursorItem = items.pop();
// Create a cursor object with the relevant fields for sorting
const cursorData: ArticleCursor = {
slug: cursorItem!.slug,
createdAt: cursorItem!.createdAt,
title: cursorItem!.title,
};
// Encode the cursor as base64
nextCursor = Buffer.from(SuperJSON.stringify(cursorData)).toString(
"base64",
);
}
return {
items,
nextCursor,
};
}),
getMany: publicProcedure
.input(
z
.object({
categoryId: z.string().optional(),
limit: z.number().optional(),
})
.optional(),
)
.query(async ({ ctx, input }) => {
const limit = input?.limit ?? 50;
return (await ctx.db.query.articles.findMany({
where: input?.categoryId
? eq(articles.categoryId, input.categoryId)
: undefined,
limit: limit,
columns: {
title: true,
slug: true,
createdAt: true,
},
})) as Array<Article>;
}),
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");
}
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,
});
}),
});