mirror of
https://github.com/khairul169/code-share.git
synced 2025-04-28 16:49:36 +07:00
224 lines
5.9 KiB
TypeScript
224 lines
5.9 KiB
TypeScript
import { and, asc, desc, eq, inArray, isNull, sql } from "drizzle-orm";
|
|
import db from "../db";
|
|
import { procedure, router } from "../api/trpc";
|
|
import { file, insertFileSchema, selectFileSchema } from "../db/schema/file";
|
|
import { z } from "zod";
|
|
import { TRPCError } from "@trpc/server";
|
|
import { getProjectById, hasPermission } from "./project";
|
|
|
|
const fileRouter = router({
|
|
getAll: procedure
|
|
.input(
|
|
z.object({
|
|
projectId: z.number(),
|
|
id: z.number().array().min(1).optional(),
|
|
isPinned: z.boolean().optional(),
|
|
})
|
|
)
|
|
.query(async ({ ctx, input: opt }) => {
|
|
const project = await getProjectById(opt.projectId);
|
|
|
|
if (!hasPermission(ctx, project, "r")) {
|
|
throw new TRPCError({ code: "FORBIDDEN" });
|
|
}
|
|
|
|
const files = await db.query.file.findMany({
|
|
where: and(
|
|
eq(file.projectId, opt.projectId),
|
|
isNull(file.deletedAt),
|
|
opt.id && opt.id.length > 0 ? inArray(file.id, opt.id) : undefined,
|
|
opt.isPinned ? eq(file.isPinned, true) : undefined
|
|
),
|
|
orderBy: [desc(file.isDirectory), asc(file.filename)],
|
|
columns: !file.isPinned ? { content: true } : undefined,
|
|
});
|
|
|
|
return files;
|
|
}),
|
|
|
|
getById: procedure.input(z.number()).query(async ({ ctx, input }) => {
|
|
const result = await db.query.file.findFirst({
|
|
where: and(eq(file.id, input), isNull(file.deletedAt)),
|
|
with: { project: { columns: { visibility: true, userId: true } } },
|
|
});
|
|
|
|
if (!result) {
|
|
return undefined;
|
|
}
|
|
|
|
if (!hasPermission(ctx, result?.project, "r")) {
|
|
throw new TRPCError({ code: "FORBIDDEN" });
|
|
}
|
|
|
|
return result;
|
|
}),
|
|
|
|
create: procedure
|
|
.input(
|
|
insertFileSchema.pick({
|
|
projectId: true,
|
|
parentId: true,
|
|
filename: true,
|
|
isDirectory: true,
|
|
})
|
|
)
|
|
.mutation(async ({ ctx, input }) => {
|
|
const project = await getProjectById(input.projectId);
|
|
|
|
if (!hasPermission(ctx, project, "w")) {
|
|
throw new TRPCError({ code: "FORBIDDEN" });
|
|
}
|
|
|
|
let basePath = "";
|
|
if (input.parentId) {
|
|
const parent = await db.query.file.findFirst({
|
|
where: and(eq(file.id, input.parentId), isNull(file.deletedAt)),
|
|
});
|
|
|
|
if (!parent?.isDirectory) {
|
|
throw new Error("Parent is not a directory!");
|
|
}
|
|
basePath = parent.path + "/";
|
|
}
|
|
|
|
const data: z.infer<typeof insertFileSchema> = {
|
|
projectId: project.id,
|
|
parentId: input.parentId,
|
|
path: basePath + input.filename,
|
|
filename: input.filename,
|
|
isDirectory: input.isDirectory,
|
|
};
|
|
|
|
const [result] = await db.insert(file).values(data).returning();
|
|
return result;
|
|
}),
|
|
|
|
update: procedure
|
|
.input(selectFileSchema.partial().required({ id: true, projectId: true }))
|
|
.mutation(async ({ ctx, input }) => {
|
|
const fileData = await db.query.file.findFirst({
|
|
where: and(
|
|
eq(file.projectId, input.projectId),
|
|
eq(file.id, input.id),
|
|
isNull(file.deletedAt)
|
|
),
|
|
with: { project: { columns: { visibility: true, userId: true } } },
|
|
});
|
|
|
|
if (!fileData) {
|
|
throw new TRPCError({ code: "NOT_FOUND" });
|
|
}
|
|
if (!hasPermission(ctx, fileData.project, "w")) {
|
|
throw new TRPCError({ code: "FORBIDDEN" });
|
|
}
|
|
|
|
return db.transaction(async (tx) => {
|
|
const data = { ...input };
|
|
|
|
// also rename path
|
|
if (data.filename) {
|
|
const path = fileData.path.split("/");
|
|
const idx = path.length - 1;
|
|
path[idx] = data.filename;
|
|
data.path = path.join("/");
|
|
|
|
// recursively update files path in directory
|
|
if (data.isDirectory) {
|
|
await renameFilesPath(fileData.id, idx, data.filename, tx);
|
|
}
|
|
}
|
|
|
|
const [result] = await tx
|
|
.update(file)
|
|
.set(data)
|
|
.where(and(eq(file.id, input.id), isNull(file.deletedAt)))
|
|
.returning();
|
|
|
|
return result;
|
|
});
|
|
}),
|
|
|
|
delete: procedure.input(z.number()).mutation(async ({ ctx, input }) => {
|
|
const fileData = await db.query.file.findFirst({
|
|
where: and(eq(file.id, input), isNull(file.deletedAt)),
|
|
with: { project: { columns: { visibility: true, userId: true } } },
|
|
});
|
|
if (!fileData) {
|
|
throw new TRPCError({ code: "NOT_FOUND" });
|
|
}
|
|
if (!hasPermission(ctx, fileData.project, "w")) {
|
|
throw new TRPCError({ code: "FORBIDDEN" });
|
|
}
|
|
|
|
return db.transaction(async (tx) => {
|
|
const [result] = await tx
|
|
.update(file)
|
|
.set({ deletedAt: sql`CURRENT_TIMESTAMP` })
|
|
.where(eq(file.id, fileData.id))
|
|
.returning();
|
|
|
|
if (fileData.isDirectory) {
|
|
await deleteChildrenFiles(fileData.id, tx);
|
|
}
|
|
|
|
return result;
|
|
});
|
|
}),
|
|
});
|
|
|
|
const renameFilesPath = async (
|
|
id: number,
|
|
idx: number,
|
|
pathname: string,
|
|
tx: typeof db
|
|
) => {
|
|
const files = await tx.query.file.findMany({
|
|
where: and(eq(file.parentId, id), isNull(file.deletedAt)),
|
|
});
|
|
|
|
if (!files.length) {
|
|
return;
|
|
}
|
|
|
|
for (const f of files) {
|
|
const path = f.path.split("/");
|
|
if (idx >= 0 && idx < path.length) {
|
|
path[idx] = pathname;
|
|
}
|
|
|
|
await tx
|
|
.update(file)
|
|
.set({ path: path.join("/") })
|
|
.where(and(eq(file.id, f.id), isNull(file.deletedAt)))
|
|
.execute();
|
|
|
|
if (f.isDirectory) {
|
|
await renameFilesPath(f.id, idx, pathname, tx);
|
|
}
|
|
}
|
|
};
|
|
|
|
const deleteChildrenFiles = async (id: number, tx: typeof db) => {
|
|
const files = await tx.query.file.findMany({
|
|
where: and(eq(file.parentId, id), isNull(file.deletedAt)),
|
|
});
|
|
|
|
if (!files.length) {
|
|
return;
|
|
}
|
|
|
|
for (const f of files) {
|
|
await tx
|
|
.update(file)
|
|
.set({ deletedAt: sql`CURRENT_TIMESTAMP` })
|
|
.where(eq(file.id, f.id))
|
|
.execute();
|
|
|
|
if (f.isDirectory) {
|
|
await deleteChildrenFiles(f.id, tx);
|
|
}
|
|
}
|
|
};
|
|
|
|
export default fileRouter;
|