diff --git a/lib/api/controllers/collections/collectionId/deleteCollectionById.ts b/lib/api/controllers/collections/collectionId/deleteCollectionById.ts index 03870aa..98b1e17 100644 --- a/lib/api/controllers/collections/collectionId/deleteCollectionById.ts +++ b/lib/api/controllers/collections/collectionId/deleteCollectionById.ts @@ -37,6 +37,8 @@ export default async function deleteCollection( } const deletedCollection = await prisma.$transaction(async () => { + await deleteSubCollections(collectionId); + await prisma.usersAndCollections.deleteMany({ where: { collection: { @@ -53,7 +55,7 @@ export default async function deleteCollection( }, }); - removeFolder({ filePath: `archives/${collectionId}` }); + await removeFolder({ filePath: `archives/${collectionId}` }); return await prisma.collection.delete({ where: { @@ -64,3 +66,35 @@ export default async function deleteCollection( return { response: deletedCollection, status: 200 }; } + +async function deleteSubCollections(collectionId: number) { + const subCollections = await prisma.collection.findMany({ + where: { parentId: collectionId }, + }); + + for (const subCollection of subCollections) { + await deleteSubCollections(subCollection.id); + + await prisma.usersAndCollections.deleteMany({ + where: { + collection: { + id: subCollection.id, + }, + }, + }); + + await prisma.link.deleteMany({ + where: { + collection: { + id: subCollection.id, + }, + }, + }); + + await prisma.collection.delete({ + where: { id: subCollection.id }, + }); + + await removeFolder({ filePath: `archives/${subCollection.id}` }); + } +} diff --git a/lib/api/controllers/collections/collectionId/getCollectionById.ts b/lib/api/controllers/collections/collectionId/getCollectionById.ts new file mode 100644 index 0000000..e508399 --- /dev/null +++ b/lib/api/controllers/collections/collectionId/getCollectionById.ts @@ -0,0 +1,35 @@ +import { prisma } from "@/lib/api/db"; + +export default async function getCollectionById( + userId: number, + collectionId: number +) { + const collections = await prisma.collection.findFirst({ + where: { + id: collectionId, + OR: [ + { ownerId: userId }, + { members: { some: { user: { id: userId } } } }, + ], + }, + include: { + _count: { + select: { links: true }, + }, + subCollections: true, + members: { + include: { + user: { + select: { + username: true, + name: true, + image: true, + }, + }, + }, + }, + }, + }); + + return { response: collections, status: 200 }; +} diff --git a/lib/api/controllers/collections/collectionId/updateCollectionById.ts b/lib/api/controllers/collections/collectionId/updateCollectionById.ts index f257021..e300662 100644 --- a/lib/api/controllers/collections/collectionId/updateCollectionById.ts +++ b/lib/api/controllers/collections/collectionId/updateCollectionById.ts @@ -1,7 +1,6 @@ import { prisma } from "@/lib/api/db"; import { CollectionIncludingMembersAndLinkCount } from "@/types/global"; import getPermission from "@/lib/api/getPermission"; -import { Collection, UsersAndCollections } from "@prisma/client"; export default async function updateCollection( userId: number, @@ -19,6 +18,26 @@ export default async function updateCollection( if (!(collectionIsAccessible?.ownerId === userId)) return { response: "Collection is not accessible.", status: 401 }; + if (data.parentId) { + const findParentCollection = await prisma.collection.findUnique({ + where: { + id: data.parentId, + }, + select: { + ownerId: true, + }, + }); + + if ( + findParentCollection?.ownerId !== userId || + typeof data.parentId !== "number" + ) + return { + response: "You are not authorized to create a sub-collection here.", + status: 403, + }; + } + const updatedCollection = await prisma.$transaction(async () => { await prisma.usersAndCollections.deleteMany({ where: { @@ -38,6 +57,11 @@ export default async function updateCollection( description: data.description, color: data.color, isPublic: data.isPublic, + parent: { + connect: { + id: data.parentId || undefined, + }, + }, members: { create: data.members.map((e) => ({ user: { connect: { id: e.user.id || e.userId } }, diff --git a/lib/api/controllers/collections/postCollection.ts b/lib/api/controllers/collections/postCollection.ts index 86ca379..a69d016 100644 --- a/lib/api/controllers/collections/postCollection.ts +++ b/lib/api/controllers/collections/postCollection.ts @@ -12,6 +12,26 @@ export default async function postCollection( status: 400, }; + if (collection.parentId) { + const findParentCollection = await prisma.collection.findUnique({ + where: { + id: collection.parentId, + }, + select: { + ownerId: true, + }, + }); + + if ( + findParentCollection?.ownerId !== userId || + typeof collection.parentId !== "number" + ) + return { + response: "You are not authorized to create a sub-collection here.", + status: 403, + }; + } + const findCollection = await prisma.user.findUnique({ where: { id: userId, @@ -40,6 +60,13 @@ export default async function postCollection( name: collection.name.trim(), description: collection.description, color: collection.color, + parent: collection.parentId + ? { + connect: { + id: collection.parentId, + }, + } + : undefined, }, include: { _count: { diff --git a/pages/api/v1/collections/[id].ts b/pages/api/v1/collections/[id].ts index 4f8020c..637e10f 100644 --- a/pages/api/v1/collections/[id].ts +++ b/pages/api/v1/collections/[id].ts @@ -1,4 +1,5 @@ import type { NextApiRequest, NextApiResponse } from "next"; +import getCollectionById from "@/lib/api/controllers/collections/collectionId/getCollectionById"; import updateCollectionById from "@/lib/api/controllers/collections/collectionId/updateCollectionById"; import deleteCollectionById from "@/lib/api/controllers/collections/collectionId/deleteCollectionById"; import verifyUser from "@/lib/api/verifyUser"; @@ -10,18 +11,18 @@ export default async function collections( const user = await verifyUser({ req, res }); if (!user) return; - if (req.method === "PUT") { - const updated = await updateCollectionById( - user.id, - Number(req.query.id) as number, - req.body - ); + const collectionId = Number(req.query.id); + + if (req.method === "GET") { + const collections = await getCollectionById(user.id, collectionId); + return res + .status(collections.status) + .json({ response: collections.response }); + } else if (req.method === "PUT") { + const updated = await updateCollectionById(user.id, collectionId, req.body); return res.status(updated.status).json({ response: updated.response }); } else if (req.method === "DELETE") { - const deleted = await deleteCollectionById( - user.id, - Number(req.query.id) as number - ); + const deleted = await deleteCollectionById(user.id, collectionId); return res.status(deleted.status).json({ response: deleted.response }); } } diff --git a/prisma/migrations/20240125124457_added_subcollection_relations/migration.sql b/prisma/migrations/20240125124457_added_subcollection_relations/migration.sql new file mode 100644 index 0000000..9479956 --- /dev/null +++ b/prisma/migrations/20240125124457_added_subcollection_relations/migration.sql @@ -0,0 +1,5 @@ +-- AlterTable +ALTER TABLE "Collection" ADD COLUMN "parentId" INTEGER; + +-- AddForeignKey +ALTER TABLE "Collection" ADD CONSTRAINT "Collection_parentId_fkey" FOREIGN KEY ("parentId") REFERENCES "Collection"("id") ON DELETE SET NULL ON UPDATE CASCADE; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index aebc98f..23ff147 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -69,17 +69,20 @@ model VerificationToken { } model Collection { - id Int @id @default(autoincrement()) - name String - description String @default("") - color String @default("#0ea5e9") - isPublic Boolean @default(false) - owner User @relation(fields: [ownerId], references: [id]) - ownerId Int - members UsersAndCollections[] - links Link[] - createdAt DateTime @default(now()) - updatedAt DateTime @default(now()) @updatedAt + id Int @id @default(autoincrement()) + name String + description String @default("") + color String @default("#0ea5e9") + parentId Int? + parent Collection? @relation("SubCollections", fields: [parentId], references: [id]) + subCollections Collection[] @relation("SubCollections") + isPublic Boolean @default(false) + owner User @relation(fields: [ownerId], references: [id]) + ownerId Int + members UsersAndCollections[] + links Link[] + createdAt DateTime @default(now()) + updatedAt DateTime @default(now()) @updatedAt @@unique([name, ownerId]) } diff --git a/scripts/worker.ts b/scripts/worker.ts index de025ea..5cddcf1 100644 --- a/scripts/worker.ts +++ b/scripts/worker.ts @@ -1,4 +1,4 @@ -import 'dotenv/config'; +import "dotenv/config"; import { Collection, Link, User } from "@prisma/client"; import { prisma } from "../lib/api/db"; import archiveHandler from "../lib/api/archiveHandler"; diff --git a/store/collections.ts b/store/collections.ts index 5c78b52..466b652 100644 --- a/store/collections.ts +++ b/store/collections.ts @@ -78,7 +78,11 @@ const useCollectionStore = create()((set) => ({ if (response.ok) { set((state) => ({ - collections: state.collections.filter((e) => e.id !== collectionId), + collections: state.collections.filter( + (collection) => + collection.id !== collectionId && + collection.parentId !== collectionId + ), })); useTagStore.getState().setTags(); }