plain blame
import { experimental_trpcMiddleware, TRPCError } from "@trpc/server";
import { and, desc, eq, lt, lte, or } from "drizzle-orm";
import { z } from "zod";
import { highlights } from "@hoarder/db/schema";
import {
DEFAULT_NUM_HIGHLIGHTS_PER_PAGE,
zGetAllHighlightsResponseSchema,
zHighlightSchema,
zNewHighlightSchema,
zUpdateHighlightSchema,
} from "@hoarder/shared/types/highlights";
import { zCursorV2 } from "@hoarder/shared/types/pagination";
import { authedProcedure, Context, router } from "../index";
import { ensureBookmarkOwnership } from "./bookmarks";
const ensureHighlightOwnership = experimental_trpcMiddleware<{
ctx: Context;
input: { highlightId: string };
}>().create(async (opts) => {
const highlight = await opts.ctx.db.query.highlights.findFirst({
where: eq(highlights.id, opts.input.highlightId),
columns: {
userId: true,
},
});
if (!opts.ctx.user) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "User is not authorized",
});
}
if (!highlight) {
throw new TRPCError({
code: "NOT_FOUND",
message: "Bookmark not found",
});
}
if (highlight.userId != opts.ctx.user.id) {
throw new TRPCError({
code: "FORBIDDEN",
message: "User is not allowed to access resource",
});
}
return opts.next();
});
export const highlightsAppRouter = router({
create: authedProcedure
.input(zNewHighlightSchema)
.output(zHighlightSchema)
.use(ensureBookmarkOwnership)
.mutation(async ({ input, ctx }) => {
const [result] = await ctx.db
.insert(highlights)
.values({
bookmarkId: input.bookmarkId,
startOffset: input.startOffset,
endOffset: input.endOffset,
color: input.color,
text: input.text,
note: input.note,
userId: ctx.user.id,
})
.returning();
return result;
}),
getForBookmark: authedProcedure
.input(z.object({ bookmarkId: z.string() }))
.output(z.object({ highlights: z.array(zHighlightSchema) }))
.use(ensureBookmarkOwnership)
.query(async ({ input, ctx }) => {
const results = await ctx.db.query.highlights.findMany({
where: and(
eq(highlights.bookmarkId, input.bookmarkId),
eq(highlights.userId, ctx.user.id),
),
orderBy: [desc(highlights.createdAt), desc(highlights.id)],
});
return { highlights: results };
}),
get: authedProcedure
.input(z.object({ highlightId: z.string() }))
.output(zHighlightSchema)
.use(ensureHighlightOwnership)
.query(async ({ input, ctx }) => {
const result = await ctx.db.query.highlights.findFirst({
where: and(
eq(highlights.id, input.highlightId),
eq(highlights.userId, ctx.user.id),
),
});
if (!result) {
throw new TRPCError({ code: "NOT_FOUND" });
}
return result;
}),
getAll: authedProcedure
.input(
z.object({
cursor: zCursorV2.nullish(),
limit: z.number().optional().default(DEFAULT_NUM_HIGHLIGHTS_PER_PAGE),
}),
)
.output(zGetAllHighlightsResponseSchema)
.query(async ({ input, ctx }) => {
const results = await ctx.db.query.highlights.findMany({
where: and(
eq(highlights.userId, ctx.user.id),
input.cursor
? or(
lt(highlights.createdAt, input.cursor.createdAt),
and(
eq(highlights.createdAt, input.cursor.createdAt),
lte(highlights.id, input.cursor.id),
),
)
: undefined,
),
limit: input.limit + 1,
orderBy: [desc(highlights.createdAt), desc(highlights.id)],
});
let nextCursor: z.infer<typeof zCursorV2> | null = null;
if (results.length > input.limit) {
const nextItem = results.pop()!;
nextCursor = {
id: nextItem.id,
createdAt: nextItem.createdAt,
};
}
return {
highlights: results,
nextCursor,
};
}),
delete: authedProcedure
.input(z.object({ highlightId: z.string() }))
.output(zHighlightSchema)
.use(ensureHighlightOwnership)
.mutation(async ({ input, ctx }) => {
const result = await ctx.db
.delete(highlights)
.where(
and(
eq(highlights.id, input.highlightId),
eq(highlights.userId, ctx.user.id),
),
)
.returning();
if (result.length == 0) {
throw new TRPCError({
code: "NOT_FOUND",
});
}
return result[0];
}),
update: authedProcedure
.input(zUpdateHighlightSchema)
.output(zHighlightSchema)
.use(ensureHighlightOwnership)
.mutation(async ({ input, ctx }) => {
const result = await ctx.db
.update(highlights)
.set({
color: input.color,
})
.where(
and(
eq(highlights.id, input.highlightId),
eq(highlights.userId, ctx.user.id),
),
)
.returning();
if (result.length == 0) {
throw new TRPCError({
code: "NOT_FOUND",
});
}
return result[0];
}),
});