259 lines
8.6 KiB
TypeScript
259 lines
8.6 KiB
TypeScript
import { experimental_trpcMiddleware, TRPCError } from "@trpc/server";
|
|
import { and, asc, desc, eq, inArray, notExists } from "drizzle-orm";
|
|
import { z } from "zod";
|
|
|
|
import { DatabaseError } 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, categoryId?: string }, userId: string) {
|
|
const queryType = input.categoryCode ? eq(categories.code, input.categoryCode) : eq(categories.id, input.categoryId);
|
|
return and(queryType, eq(categories.userId, userId));
|
|
}
|
|
|
|
async function createCategory(
|
|
input: z.infer<typeof zCategorySchema>,
|
|
ctx: Context,
|
|
) {
|
|
|
|
return ctx.db.transaction(async (trx) => {
|
|
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) {
|
|
if (e instanceof DatabaseError) {
|
|
if (e.code == "23505") {
|
|
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({
|
|
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))
|
|
;
|
|
|
|
const categoryParents = dbCategories
|
|
.filter((category) => category.code == parseInt(category.code) && category.code < 11)
|
|
.sort((a, b) => a.code - b.code);
|
|
const sortedCategories = categoryParents.map((parent) => {
|
|
return dbCategories
|
|
.filter((child) => child.code.toString().startsWith(parent.code.toString()))
|
|
});
|
|
return {
|
|
categories: sortedCategories.flat(),
|
|
};
|
|
}),
|
|
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;
|
|
}
|
|
|
|
return category;
|
|
}),
|
|
create: authedProcedure
|
|
.input(zCreateCategorySchema)
|
|
.output(
|
|
z.object({
|
|
id: z.string(),
|
|
code: z.number(),
|
|
name: z.string(),
|
|
color: zColorSchema,
|
|
description: z.string().optional(),
|
|
}),
|
|
)
|
|
.mutation(async ({ input, ctx }) => {
|
|
const color = await ctx.db.select().from(colors).where(
|
|
and(
|
|
eq(colors.name, input.colorName),
|
|
eq(colors.userId, ctx.user.id),
|
|
)
|
|
);
|
|
return createCategory({ ...input, color: color[0] }, ctx);
|
|
}),
|
|
update: authedProcedure
|
|
.input(zUpdateCategoryRequestSchema)
|
|
.output(
|
|
z.object({
|
|
id: z.string(),
|
|
name: z.string(),
|
|
userId: z.string(),
|
|
createdAt: z.date(),
|
|
}),
|
|
)
|
|
.mutation(async ({ input, ctx }) => {
|
|
const colorIdQuery = await ctx.db.select({ id: colors.id }).from(colors).where(
|
|
and(
|
|
eq(colors.name, input.color.name),
|
|
eq(colors.userId, ctx.user.id),
|
|
)
|
|
);
|
|
const res = await ctx.db
|
|
.update(categories)
|
|
.set({
|
|
name: input.name,
|
|
description: input.description,
|
|
code: input.code,
|
|
colorId: colorIdQuery[0].id,
|
|
})
|
|
.where(
|
|
and(
|
|
conditionFromInput({ categoryId: input.categoryId }, ctx.user.id),
|
|
eq(categories.userId, ctx.user.id),
|
|
),
|
|
).returning();
|
|
return res[0];
|
|
}),
|
|
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" });
|
|
}
|
|
}),
|
|
}); |