Some checks failed
Gitea Actions Demo / Explore-Gitea-Actions (push) Failing after 0s
245 lines
7.1 KiB
TypeScript
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,
|
|
});
|
|
}),
|
|
});
|