305 lines
9.7 KiB
TypeScript
305 lines
9.7 KiB
TypeScript
import { experimental_trpcMiddleware, TRPCError } from "@trpc/server";
|
|
import { and, desc, eq, inArray, notExists } from "drizzle-orm";
|
|
import { z } from "zod";
|
|
|
|
import { SqliteError } from "@lifetracker/db";
|
|
import { categories, colors } from "@lifetracker/db/schema";
|
|
import {
|
|
ZCategories,
|
|
zCategorySchema,
|
|
ZCreateCategories,
|
|
zCreateCategorySchema,
|
|
zGetCategoryResponseSchema,
|
|
zUpdateCategoryRequestSchema
|
|
} from "@lifetracker/shared/types/categories";
|
|
import type { Context } from "../index";
|
|
import { authedProcedure, router } from "../index";
|
|
import { zColorSchema } from "@lifetracker/shared/types/colors";
|
|
|
|
|
|
function conditionFromInput(input: { categoryCode: string }, userId: string) {
|
|
return and(eq(categories.code, input.categoryCode), eq(categories.userId, userId));
|
|
}
|
|
|
|
async function createCategory(
|
|
input: z.infer<typeof zCategorySchema>,
|
|
ctx: Context,
|
|
) {
|
|
|
|
return ctx.db.transaction(async (trx) => {
|
|
console.log("Creating a category", input);
|
|
try {
|
|
const result = await trx
|
|
.insert(categories)
|
|
.values({
|
|
name: input.name,
|
|
code: input.code,
|
|
description: input.description,
|
|
colorId: input.color.id,
|
|
userId: ctx.user!.id,
|
|
parentId: input.parentId ?? undefined
|
|
})
|
|
.returning({
|
|
id: categories.id,
|
|
name: categories.name,
|
|
code: categories.code,
|
|
description: categories.description,
|
|
colorId: categories.colorId
|
|
});
|
|
const { colorId, ...newCategory } = result[0];
|
|
|
|
return {
|
|
color: input.color,
|
|
...newCategory
|
|
};
|
|
|
|
} catch (e) {
|
|
console.log(e);
|
|
if (e instanceof SqliteError) {
|
|
if (e.code == "SQLITE_CONSTRAINT_UNIQUE") {
|
|
throw new TRPCError({
|
|
code: "BAD_REQUEST",
|
|
message: "There's already a category with this code",
|
|
});
|
|
}
|
|
}
|
|
throw new TRPCError({
|
|
code: "INTERNAL_SERVER_ERROR",
|
|
message: "Something went wrong",
|
|
});
|
|
}
|
|
});
|
|
}
|
|
|
|
export const categoriesAppRouter = router({
|
|
categoryStats: authedProcedure
|
|
.output(
|
|
z.record(
|
|
z.string(),
|
|
z.object({
|
|
numEntries: z.number(),
|
|
}),
|
|
),
|
|
)
|
|
.query(async ({ ctx }) => {
|
|
const [categoryIds] = await Promise.all([
|
|
ctx.db.select({ id: categories.id }).from(categories)
|
|
]);
|
|
|
|
const results: Record<
|
|
string,
|
|
{ numEntries: number }
|
|
> = {};
|
|
for (const category of categoryIds) {
|
|
results[category.id] = {
|
|
numEntries: 3330,
|
|
};
|
|
}
|
|
return results;
|
|
}),
|
|
list: authedProcedure
|
|
.output(
|
|
z.object({
|
|
categories: z.array(
|
|
z.object({
|
|
id: z.string(),
|
|
code: z.number(),
|
|
name: z.string(),
|
|
description: z.string().optional(),
|
|
color: zColorSchema,
|
|
}),
|
|
),
|
|
}),
|
|
)
|
|
.query(async ({ ctx }) => {
|
|
const dbCategories = await ctx.db
|
|
.select({
|
|
id: categories.id,
|
|
code: categories.code,
|
|
name: categories.name,
|
|
description: categories.description,
|
|
color: {
|
|
name: colors.name,
|
|
hexcode: colors.hexcode,
|
|
inverse: colors.inverse
|
|
}
|
|
})
|
|
.from(categories)
|
|
.leftJoin(colors, eq(categories.colorId, colors.id))
|
|
;
|
|
|
|
return {
|
|
categories: dbCategories.map(({ color, ...category }) => ({
|
|
...category,
|
|
color,
|
|
})),
|
|
};
|
|
}),
|
|
get: authedProcedure
|
|
.input(
|
|
z.object({
|
|
categoryCode: z.number(),
|
|
}),
|
|
)
|
|
.output(zCategorySchema)
|
|
.query(async ({ input, ctx }) => {
|
|
const res = await ctx.db
|
|
.select({
|
|
id: categories.id,
|
|
code: categories.code,
|
|
name: categories.name,
|
|
description: categories.description,
|
|
colorId: categories.colorId,
|
|
parentId: categories.parentId,
|
|
userId: categories.userId,
|
|
})
|
|
.from(categories)
|
|
.where(
|
|
and(
|
|
conditionFromInput(input, ctx.user.id),
|
|
eq(categories.userId, ctx.user.id),
|
|
),
|
|
);
|
|
|
|
if (res.length == 0) {
|
|
throw new TRPCError({ code: "NOT_FOUND" });
|
|
}
|
|
|
|
const numEntriesWithCategory = res.reduce<
|
|
Record<ZCategorizedByEnum, number>
|
|
>(
|
|
(acc, curr) => {
|
|
if (curr.categorizedBy) {
|
|
acc[curr.categorizedBy]++;
|
|
}
|
|
return acc;
|
|
},
|
|
{ ai: 0, human: 0 },
|
|
);
|
|
|
|
const [color] = await ctx.db.select().from(colors).where(
|
|
and(
|
|
and(eq(colors.id, res[0].colorId), eq(colors.userId, res[0].userId)),
|
|
eq(res[0].userId, ctx.user.id),
|
|
)
|
|
);
|
|
|
|
const categoryId: string = res[0].id;
|
|
|
|
const { id, ...categoryProps } = res[0];
|
|
const category: ZCategories = {
|
|
color: color,
|
|
...categoryProps
|
|
};
|
|
|
|
if (category.parentId == null) {
|
|
category.parentId = categoryId;
|
|
}
|
|
|
|
console.log(category);
|
|
|
|
return category;
|
|
}),
|
|
create: authedProcedure
|
|
.input(zCategorySchema)
|
|
.output(
|
|
z.object({
|
|
id: z.string(),
|
|
code: z.number(),
|
|
name: z.string(),
|
|
color: zColorSchema,
|
|
description: z.string().optional(),
|
|
}),
|
|
)
|
|
.mutation(async ({ input, ctx }) => {
|
|
return createCategory(input, ctx);
|
|
}),
|
|
update: authedProcedure
|
|
.input(zUpdateCategoryRequestSchema)
|
|
.output(
|
|
z.object({
|
|
id: z.string(),
|
|
name: z.string(),
|
|
userId: z.string(),
|
|
createdAt: z.date(),
|
|
}),
|
|
)
|
|
.mutation(async ({ input, ctx }) => {
|
|
try {
|
|
const res = await ctx.db
|
|
.update(bookmarkTags)
|
|
.set({
|
|
name: input.name,
|
|
})
|
|
.where(
|
|
and(
|
|
eq(bookmarkTags.id, input.tagId),
|
|
eq(bookmarkTags.userId, ctx.user.id),
|
|
),
|
|
)
|
|
.returning();
|
|
|
|
if (res.length == 0) {
|
|
throw new TRPCError({ code: "NOT_FOUND" });
|
|
}
|
|
|
|
try {
|
|
const affectedBookmarks = await ctx.db.query.tagsOnBookmarks.findMany(
|
|
{
|
|
where: eq(tagsOnBookmarks.tagId, input.tagId),
|
|
columns: {
|
|
bookmarkId: true,
|
|
},
|
|
},
|
|
);
|
|
await Promise.all(
|
|
affectedBookmarks
|
|
.map((b) => b.bookmarkId)
|
|
.map((id) => triggerSearchReindex(id)),
|
|
);
|
|
} catch (e) {
|
|
console.error("Something ELSE Went Wrong", e);
|
|
}
|
|
|
|
return res[0];
|
|
} catch (e) {
|
|
if (e instanceof SqliteError) {
|
|
if (e.code == "SQLITE_CONSTRAINT_UNIQUE") {
|
|
throw new TRPCError({
|
|
code: "BAD_REQUEST",
|
|
message:
|
|
"Label name already exists.",
|
|
});
|
|
}
|
|
}
|
|
throw e;
|
|
}
|
|
}),
|
|
delete: authedProcedure
|
|
.input(
|
|
z.object({
|
|
categoryCode: z.number(),
|
|
}),
|
|
)
|
|
.mutation(async ({ input, ctx }) => {
|
|
// const affectedBookmarks = await ctx.db
|
|
// .select({
|
|
// bookmarkId: tagsOnBookmarks.bookmarkId,
|
|
// })
|
|
// .from(tagsOnBookmarks)
|
|
// .where(
|
|
// and(
|
|
// eq(tagsOnBookmarks.tagId, input.tagId),
|
|
// // Tag ownership is checked in the ensureTagOwnership middleware
|
|
// // eq(bookmarkTags.userId, ctx.user.id),
|
|
// ),
|
|
// );
|
|
|
|
const res = await ctx.db
|
|
.delete(categories)
|
|
.where(conditionFromInput(input, ctx.user.id));
|
|
if (res.changes == 0) {
|
|
throw new TRPCError({ code: "NOT_FOUND" });
|
|
}
|
|
}),
|
|
}); |