From a06500a949e169951abfa4cf10752b53aa6712f1 Mon Sep 17 00:00:00 2001 From: Henry Date: Fri, 30 Aug 2024 22:28:07 +0100 Subject: [PATCH] feat: delete workspace --- src/components/Button.tsx | 7 +- src/components/NewWorkspaceForm.tsx | 86 +- src/server/api/routers/board.ts | 6 +- src/server/api/routers/card.ts | 6 +- src/server/api/routers/label.ts | 4 +- src/server/api/routers/list.ts | 4 +- src/server/api/routers/workspace.ts | 16 + .../migrations/0002_clever_robin_chapel.sql | 66 ++ .../db/migrations/meta/0002_snapshot.json | 1003 +++++++++++++++++ src/server/db/migrations/meta/_journal.json | 9 +- src/server/db/repository/board.repo.ts | 11 +- src/server/db/repository/card.repo.ts | 136 +-- src/server/db/repository/label.repo.ts | 2 +- src/server/db/repository/list.repo.ts | 68 +- src/server/db/repository/workspace.repo.ts | 12 + src/server/db/schema.ts | 414 +++---- .../DeleteWorkspaceConfirmation.tsx | 96 ++ src/views/settings/index.tsx | 27 +- supabase/seed.sql | 39 +- 19 files changed, 1647 insertions(+), 365 deletions(-) create mode 100644 src/server/db/migrations/0002_clever_robin_chapel.sql create mode 100644 src/server/db/migrations/meta/0002_snapshot.json create mode 100644 src/views/settings/components/DeleteWorkspaceConfirmation.tsx diff --git a/src/components/Button.tsx b/src/components/Button.tsx index 0c2531fd..521c1b40 100644 --- a/src/components/Button.tsx +++ b/src/components/Button.tsx @@ -1,7 +1,7 @@ import { twMerge } from "tailwind-merge"; interface ButtonProps extends React.ButtonHTMLAttributes { - variant?: "primary" | "secondary"; + variant?: "primary" | "secondary" | "danger"; isLoading?: boolean; } @@ -19,8 +19,11 @@ const Button = ({ "bg-light-1000 dark:bg-dark-1000 dark:text-dark-50", variant === "secondary" && "border-[1px] border-light-600 bg-light-50 dark:border-dark-600 dark:bg-dark-300 dark:text-dark-1000", + variant === "danger" && + "dark:text-red-1000 border-[1px] border-red-600 bg-red-50 dark:border-red-600 dark:bg-red-500", + props.disabled && "opacity-50", )} - disabled={isLoading} + disabled={isLoading ?? props.disabled} {...props} > diff --git a/src/components/NewWorkspaceForm.tsx b/src/components/NewWorkspaceForm.tsx index ef3d9d99..b4338ef6 100644 --- a/src/components/NewWorkspaceForm.tsx +++ b/src/components/NewWorkspaceForm.tsx @@ -1,9 +1,14 @@ import { useEffect } from "react"; import { api } from "~/utils/api"; -import { HiXMark } from "react-icons/hi2"; -import { useModal } from "~/providers/modal"; -import { useWorkspace } from "~/providers/workspace"; import { useForm } from "react-hook-form"; +import { HiXMark } from "react-icons/hi2"; + +import { useModal } from "~/providers/modal"; +import { usePopup } from "~/providers/popup"; +import { useWorkspace } from "~/providers/workspace"; + +import Button from "~/components/Button"; +import Input from "~/components/Input"; interface FormValues { name: string; @@ -11,20 +16,23 @@ interface FormValues { export function NewWorkspaceForm() { const { closeModal } = useModal(); + const { showPopup } = usePopup(); const { switchWorkspace } = useWorkspace(); const { register, handleSubmit } = useForm(); const createWorkspace = api.workspace.create.useMutation({ onSuccess: (values) => { - try { - if (values?.publicId && values.name) { - switchWorkspace({ publicId: values.publicId, name: values.name }); - closeModal(); - } - } catch (e) { - console.log(e); + if (values?.publicId && values.name) { + switchWorkspace({ publicId: values.publicId, name: values.name }); + closeModal(); } }, + onError: () => { + showPopup({ + header: "Unable to create workspace", + message: "Please try again later, or contact customer support.", + }); + }, }); useEffect(() => { @@ -40,40 +48,36 @@ export function NewWorkspaceForm() { }; return ( -
-
-

- New workspace -

- -
- -
- - -
+ +
+
+

+ New workspace +

- -
+ + +
+
+
+ +
+
+ ); } diff --git a/src/server/api/routers/board.ts b/src/server/api/routers/board.ts index 0f99d88d..edc232fd 100644 --- a/src/server/api/routers/board.ts +++ b/src/server/api/routers/board.ts @@ -114,20 +114,20 @@ export const boardRouter = createTRPCRouter({ const deletedAt = new Date().toISOString(); - await boardRepo.destroy(ctx.db, { + await boardRepo.softDelete(ctx.db, { boardId: board.id, deletedAt, deletedBy: userId, }); if (listIds.length) { - await listRepo.destroyAllByBoardId(ctx.db, { + await listRepo.softDeleteAllByBoardId(ctx.db, { boardId: board.id, deletedAt, deletedBy: userId, }); - await cardRepo.destroyAllByListIds(ctx.db, { + await cardRepo.softDeleteAllByListIds(ctx.db, { listIds, deletedAt, deletedBy: userId, diff --git a/src/server/api/routers/card.ts b/src/server/api/routers/card.ts index bdab654d..7f06abb0 100644 --- a/src/server/api/routers/card.ts +++ b/src/server/api/routers/card.ts @@ -149,7 +149,7 @@ export const cardRouter = createTRPCRouter({ ); if (existingLabel) { - await cardRepo.destroyCardLabelRelationship(ctx.db, cardLabelIds); + await cardRepo.hardDeleteCardLabelRelationship(ctx.db, cardLabelIds); return { newLabel: false }; } @@ -200,7 +200,7 @@ export const cardRouter = createTRPCRouter({ ); if (existingMember) { - await cardRepo.destroyCardMemberRelationship(ctx.db, cardMemberIds); + await cardRepo.hardDeleteCardMemberRelationship(ctx.db, cardMemberIds); return { newMember: false }; } @@ -272,7 +272,7 @@ export const cardRouter = createTRPCRouter({ const deletedAt = new Date().toISOString(); - await cardRepo.destroy(ctx.db, { + await cardRepo.softDelete(ctx.db, { cardId: card.id, deletedAt, deletedBy: userId, diff --git a/src/server/api/routers/label.ts b/src/server/api/routers/label.ts index f8085fa3..bb741c8e 100644 --- a/src/server/api/routers/label.ts +++ b/src/server/api/routers/label.ts @@ -81,9 +81,9 @@ export const labelRouter = createTRPCRouter({ code: "NOT_FOUND", }); - await cardRepo.destroyAllCardLabelRelationships(ctx.db, label.id); + await cardRepo.hardDeleteAllCardLabelRelationships(ctx.db, label.id); - await labelRepo.destroy(ctx.db, label.id); + await labelRepo.hardDelete(ctx.db, label.id); return { success: true }; }), diff --git a/src/server/api/routers/list.ts b/src/server/api/routers/list.ts index bbd54750..9b4b489d 100644 --- a/src/server/api/routers/list.ts +++ b/src/server/api/routers/list.ts @@ -98,13 +98,13 @@ export const listRouter = createTRPCRouter({ const deletedAt = new Date().toISOString(); - await listRepo.destroyById(ctx.db, { + await listRepo.softDeleteById(ctx.db, { listId: list.id, deletedAt, deletedBy: userId, }); - await cardRepo.destroyAllByListIds(ctx.db, { + await cardRepo.softDeleteAllByListIds(ctx.db, { listIds: [list.id], deletedAt, deletedBy: userId, diff --git a/src/server/api/routers/workspace.ts b/src/server/api/routers/workspace.ts index 49bd0f67..b5efdaeb 100644 --- a/src/server/api/routers/workspace.ts +++ b/src/server/api/routers/workspace.ts @@ -49,6 +49,22 @@ export const workspaceRouter = createTRPCRouter({ createdBy: userId, }); + if (!result?.publicId) + throw new TRPCError({ + message: `Unable to create workspace`, + code: "INTERNAL_SERVER_ERROR", + }); + return result; }), + delete: protectedProcedure + .input(z.object({ workspacePublicId: z.string().min(12) })) + .mutation(async ({ ctx, input }) => { + const { data } = await workspaceRepo.hardDelete( + ctx.db, + input.workspacePublicId, + ); + + return data; + }), }); diff --git a/src/server/db/migrations/0002_clever_robin_chapel.sql b/src/server/db/migrations/0002_clever_robin_chapel.sql new file mode 100644 index 00000000..eacb2d98 --- /dev/null +++ b/src/server/db/migrations/0002_clever_robin_chapel.sql @@ -0,0 +1,66 @@ +DROP TABLE IF EXISTS "account";--> statement-breakpoint +DROP TABLE IF EXISTS "session";--> statement-breakpoint +DROP TABLE IF EXISTS "verificationToken";--> statement-breakpoint +ALTER TABLE "board" DROP CONSTRAINT "board_workspaceId_workspace_id_fk"; +--> statement-breakpoint +ALTER TABLE "_card_workspace_members" DROP CONSTRAINT "_card_workspace_members_cardId_card_id_fk"; +--> statement-breakpoint +ALTER TABLE "card" DROP CONSTRAINT "card_listId_list_id_fk"; +--> statement-breakpoint +ALTER TABLE "_card_labels" DROP CONSTRAINT IF EXISTS "_card_labels_cardId_card_id_fk"; +--> statement-breakpoint +ALTER TABLE "_card_labels" DROP CONSTRAINT IF EXISTS "_card_labels_labelId_label_id_fk"; +--> statement-breakpoint +ALTER TABLE "label" DROP CONSTRAINT "label_boardId_board_id_fk"; +--> statement-breakpoint +ALTER TABLE "list" DROP CONSTRAINT "list_boardId_board_id_fk"; +--> statement-breakpoint +ALTER TABLE "workspace_members" DROP CONSTRAINT "workspace_members_workspaceId_workspace_id_fk"; +--> statement-breakpoint +DO $$ BEGIN + ALTER TABLE "board" ADD CONSTRAINT "board_workspaceId_workspace_id_fk" FOREIGN KEY ("workspaceId") REFERENCES "workspace"("id") ON DELETE cascade ON UPDATE no action; +EXCEPTION + WHEN duplicate_object THEN null; +END $$; +--> statement-breakpoint +DO $$ BEGIN + ALTER TABLE "_card_workspace_members" ADD CONSTRAINT "_card_workspace_members_workspaceMemberId_workspace_members_id_fk" FOREIGN KEY ("workspaceMemberId") REFERENCES "workspace_members"("id") ON DELETE CASCADE ON UPDATE NO ACTION; +EXCEPTION + WHEN duplicate_object THEN null; +END $$; +--> statement-breakpoint +DO $$ BEGIN + ALTER TABLE "card" ADD CONSTRAINT "card_listId_list_id_fk" FOREIGN KEY ("listId") REFERENCES "list"("id") ON DELETE cascade ON UPDATE no action; +EXCEPTION + WHEN duplicate_object THEN null; +END $$; +--> statement-breakpoint +DO $$ BEGIN + ALTER TABLE "_card_labels" ADD CONSTRAINT "_card_labels_cardId_card_id_fk" FOREIGN KEY ("cardId") REFERENCES "card"("id") ON DELETE CASCADE; +EXCEPTION + WHEN duplicate_object THEN null; +END $$; +--> statement-breakpoint +DO $$ BEGIN + ALTER TABLE "_card_labels" ADD CONSTRAINT "_card_labels_labelId_label_id_fk" FOREIGN KEY ("labelId") REFERENCES "label"("id") ON DELETE CASCADE; +EXCEPTION + WHEN duplicate_object THEN null; +END $$; +--> statement-breakpoint +DO $$ BEGIN + ALTER TABLE "label" ADD CONSTRAINT "label_boardId_board_id_fk" FOREIGN KEY ("boardId") REFERENCES "board"("id") ON DELETE cascade ON UPDATE no action; +EXCEPTION + WHEN duplicate_object THEN null; +END $$; +--> statement-breakpoint +DO $$ BEGIN + ALTER TABLE "list" ADD CONSTRAINT "list_boardId_board_id_fk" FOREIGN KEY ("boardId") REFERENCES "board"("id") ON DELETE cascade ON UPDATE no action; +EXCEPTION + WHEN duplicate_object THEN null; +END $$; +--> statement-breakpoint +DO $$ BEGIN + ALTER TABLE "workspace_members" ADD CONSTRAINT "workspace_members_workspaceId_workspace_id_fk" FOREIGN KEY ("workspaceId") REFERENCES "workspace"("id") ON DELETE cascade ON UPDATE no action; +EXCEPTION + WHEN duplicate_object THEN null; +END $$; diff --git a/src/server/db/migrations/meta/0002_snapshot.json b/src/server/db/migrations/meta/0002_snapshot.json new file mode 100644 index 00000000..e61be5c3 --- /dev/null +++ b/src/server/db/migrations/meta/0002_snapshot.json @@ -0,0 +1,1003 @@ +{ + "version": "5", + "dialect": "pg", + "id": "3cce2217-3ea4-4132-bf7c-1c028901693c", + "prevId": "9ebe8f2d-46c1-43e9-b5ca-4b05e3e37090", + "tables": { + "board": { + "name": "board", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "publicId": { + "name": "publicId", + "type": "varchar(12)", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "createdBy": { + "name": "createdBy", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "deletedAt": { + "name": "deletedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "deletedBy": { + "name": "deletedBy", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "importId": { + "name": "importId", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "workspaceId": { + "name": "workspaceId", + "type": "bigint", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "board_createdBy_user_id_fk": { + "name": "board_createdBy_user_id_fk", + "tableFrom": "board", + "tableTo": "user", + "columnsFrom": [ + "createdBy" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "board_deletedBy_user_id_fk": { + "name": "board_deletedBy_user_id_fk", + "tableFrom": "board", + "tableTo": "user", + "columnsFrom": [ + "deletedBy" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "board_importId_import_id_fk": { + "name": "board_importId_import_id_fk", + "tableFrom": "board", + "tableTo": "import", + "columnsFrom": [ + "importId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "board_workspaceId_workspace_id_fk": { + "name": "board_workspaceId_workspace_id_fk", + "tableFrom": "board", + "tableTo": "workspace", + "columnsFrom": [ + "workspaceId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "board_publicId_unique": { + "name": "board_publicId_unique", + "nullsNotDistinct": false, + "columns": [ + "publicId" + ] + } + } + }, + "_card_workspace_members": { + "name": "_card_workspace_members", + "schema": "", + "columns": { + "cardId": { + "name": "cardId", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "workspaceMemberId": { + "name": "workspaceMemberId", + "type": "bigint", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "_card_workspace_members_cardId_card_id_fk": { + "name": "_card_workspace_members_cardId_card_id_fk", + "tableFrom": "_card_workspace_members", + "tableTo": "card", + "columnsFrom": [ + "cardId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "_card_workspace_members_workspaceMemberId_workspace_members_id_fk": { + "name": "_card_workspace_members_workspaceMemberId_workspace_members_id_fk", + "tableFrom": "_card_workspace_members", + "tableTo": "workspace_members", + "columnsFrom": [ + "workspaceMemberId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "_card_workspace_members_cardId_workspaceMemberId": { + "name": "_card_workspace_members_cardId_workspaceMemberId", + "columns": [ + "cardId", + "workspaceMemberId" + ] + } + }, + "uniqueConstraints": {} + }, + "card": { + "name": "card", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "publicId": { + "name": "publicId", + "type": "varchar(12)", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "index": { + "name": "index", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "createdBy": { + "name": "createdBy", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "deletedAt": { + "name": "deletedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "deletedBy": { + "name": "deletedBy", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "listId": { + "name": "listId", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "importId": { + "name": "importId", + "type": "bigint", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "card_createdBy_user_id_fk": { + "name": "card_createdBy_user_id_fk", + "tableFrom": "card", + "tableTo": "user", + "columnsFrom": [ + "createdBy" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "card_deletedBy_user_id_fk": { + "name": "card_deletedBy_user_id_fk", + "tableFrom": "card", + "tableTo": "user", + "columnsFrom": [ + "deletedBy" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "card_listId_list_id_fk": { + "name": "card_listId_list_id_fk", + "tableFrom": "card", + "tableTo": "list", + "columnsFrom": [ + "listId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "card_importId_import_id_fk": { + "name": "card_importId_import_id_fk", + "tableFrom": "card", + "tableTo": "import", + "columnsFrom": [ + "importId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "card_publicId_unique": { + "name": "card_publicId_unique", + "nullsNotDistinct": false, + "columns": [ + "publicId" + ] + } + } + }, + "_card_labels": { + "name": "_card_labels", + "schema": "", + "columns": { + "cardId": { + "name": "cardId", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "labelId": { + "name": "labelId", + "type": "bigint", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "_card_labels_cardId_card_id_fk": { + "name": "_card_labels_cardId_card_id_fk", + "tableFrom": "_card_labels", + "tableTo": "card", + "columnsFrom": [ + "cardId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "_card_labels_labelId_label_id_fk": { + "name": "_card_labels_labelId_label_id_fk", + "tableFrom": "_card_labels", + "tableTo": "label", + "columnsFrom": [ + "labelId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "_card_labels_cardId_labelId": { + "name": "_card_labels_cardId_labelId", + "columns": [ + "cardId", + "labelId" + ] + } + }, + "uniqueConstraints": {} + }, + "import": { + "name": "import", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "publicId": { + "name": "publicId", + "type": "varchar(12)", + "primaryKey": false, + "notNull": true + }, + "source": { + "name": "source", + "type": "source", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "status", + "primaryKey": false, + "notNull": true + }, + "createdBy": { + "name": "createdBy", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "import_createdBy_user_id_fk": { + "name": "import_createdBy_user_id_fk", + "tableFrom": "import", + "tableTo": "user", + "columnsFrom": [ + "createdBy" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "import_publicId_unique": { + "name": "import_publicId_unique", + "nullsNotDistinct": false, + "columns": [ + "publicId" + ] + } + } + }, + "label": { + "name": "label", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "publicId": { + "name": "publicId", + "type": "varchar(12)", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "colourCode": { + "name": "colourCode", + "type": "varchar(12)", + "primaryKey": false, + "notNull": false + }, + "createdBy": { + "name": "createdBy", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "boardId": { + "name": "boardId", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "importId": { + "name": "importId", + "type": "bigint", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "label_createdBy_user_id_fk": { + "name": "label_createdBy_user_id_fk", + "tableFrom": "label", + "tableTo": "user", + "columnsFrom": [ + "createdBy" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "label_boardId_board_id_fk": { + "name": "label_boardId_board_id_fk", + "tableFrom": "label", + "tableTo": "board", + "columnsFrom": [ + "boardId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "label_importId_import_id_fk": { + "name": "label_importId_import_id_fk", + "tableFrom": "label", + "tableTo": "import", + "columnsFrom": [ + "importId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "label_publicId_unique": { + "name": "label_publicId_unique", + "nullsNotDistinct": false, + "columns": [ + "publicId" + ] + } + } + }, + "list": { + "name": "list", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "publicId": { + "name": "publicId", + "type": "varchar(12)", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "index": { + "name": "index", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "createdBy": { + "name": "createdBy", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "deletedAt": { + "name": "deletedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "deletedBy": { + "name": "deletedBy", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "boardId": { + "name": "boardId", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "importId": { + "name": "importId", + "type": "bigint", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "list_createdBy_user_id_fk": { + "name": "list_createdBy_user_id_fk", + "tableFrom": "list", + "tableTo": "user", + "columnsFrom": [ + "createdBy" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "list_deletedBy_user_id_fk": { + "name": "list_deletedBy_user_id_fk", + "tableFrom": "list", + "tableTo": "user", + "columnsFrom": [ + "deletedBy" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "list_boardId_board_id_fk": { + "name": "list_boardId_board_id_fk", + "tableFrom": "list", + "tableTo": "board", + "columnsFrom": [ + "boardId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "list_importId_import_id_fk": { + "name": "list_importId_import_id_fk", + "tableFrom": "list", + "tableTo": "import", + "columnsFrom": [ + "importId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "list_publicId_unique": { + "name": "list_publicId_unique", + "nullsNotDistinct": false, + "columns": [ + "publicId" + ] + } + } + }, + "user": { + "name": "user", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "email": { + "name": "email", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "emailVerified": { + "name": "emailVerified", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "image": { + "name": "image", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "user_email_unique": { + "name": "user_email_unique", + "nullsNotDistinct": false, + "columns": [ + "email" + ] + } + } + }, + "workspace_members": { + "name": "workspace_members", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "publicId": { + "name": "publicId", + "type": "varchar(12)", + "primaryKey": false, + "notNull": true + }, + "userId": { + "name": "userId", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "workspaceId": { + "name": "workspaceId", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "createdBy": { + "name": "createdBy", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "deletedAt": { + "name": "deletedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "role": { + "name": "role", + "type": "role", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "workspace_members_userId_user_id_fk": { + "name": "workspace_members_userId_user_id_fk", + "tableFrom": "workspace_members", + "tableTo": "user", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "workspace_members_workspaceId_workspace_id_fk": { + "name": "workspace_members_workspaceId_workspace_id_fk", + "tableFrom": "workspace_members", + "tableTo": "workspace", + "columnsFrom": [ + "workspaceId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "workspace_members_publicId_unique": { + "name": "workspace_members_publicId_unique", + "nullsNotDistinct": false, + "columns": [ + "publicId" + ] + } + } + }, + "workspace": { + "name": "workspace", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "publicId": { + "name": "publicId", + "type": "varchar(12)", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "createdBy": { + "name": "createdBy", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "deletedAt": { + "name": "deletedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "deletedBy": { + "name": "deletedBy", + "type": "uuid", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "workspace_createdBy_user_id_fk": { + "name": "workspace_createdBy_user_id_fk", + "tableFrom": "workspace", + "tableTo": "user", + "columnsFrom": [ + "createdBy" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "workspace_deletedBy_user_id_fk": { + "name": "workspace_deletedBy_user_id_fk", + "tableFrom": "workspace", + "tableTo": "user", + "columnsFrom": [ + "deletedBy" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "workspace_publicId_unique": { + "name": "workspace_publicId_unique", + "nullsNotDistinct": false, + "columns": [ + "publicId" + ] + }, + "workspace_slug_unique": { + "name": "workspace_slug_unique", + "nullsNotDistinct": false, + "columns": [ + "slug" + ] + } + } + } + }, + "enums": { + "source": { + "name": "source", + "values": { + "trello": "trello" + } + }, + "status": { + "name": "status", + "values": { + "started": "started", + "success": "success", + "failed": "failed" + } + }, + "role": { + "name": "role", + "values": { + "admin": "admin", + "member": "member", + "guest": "guest" + } + } + }, + "schemas": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + } +} \ No newline at end of file diff --git a/src/server/db/migrations/meta/_journal.json b/src/server/db/migrations/meta/_journal.json index db2ea567..e1ddc2ef 100644 --- a/src/server/db/migrations/meta/_journal.json +++ b/src/server/db/migrations/meta/_journal.json @@ -15,6 +15,13 @@ "when": 1713021051974, "tag": "0001_stale_mattie_franklin", "breakpoints": true + }, + { + "idx": 2, + "version": "5", + "when": 1724967733894, + "tag": "0002_clever_robin_chapel", + "breakpoints": true } ] -} \ No newline at end of file +} diff --git a/src/server/db/repository/board.repo.ts b/src/server/db/repository/board.repo.ts index 6ba484ee..cd754fd8 100644 --- a/src/server/db/repository/board.repo.ts +++ b/src/server/db/repository/board.repo.ts @@ -144,7 +144,7 @@ export const update = async ( return data; }; -export const destroy = async ( +export const softDelete = async ( db: SupabaseClient, args: { boardId: number; @@ -160,3 +160,12 @@ export const destroy = async ( return result; }; + +export const hardDelete = async ( + db: SupabaseClient, + workspaceId: number, +) => { + const result = db.from("board").delete().eq("workspaceId", workspaceId); + + return result; +}; diff --git a/src/server/db/repository/card.repo.ts b/src/server/db/repository/card.repo.ts index b57dff62..2e156cfe 100644 --- a/src/server/db/repository/card.repo.ts +++ b/src/server/db/repository/card.repo.ts @@ -74,39 +74,6 @@ export const update = async ( return data; }; -export const destroy = async ( - db: SupabaseClient, - args: { - cardId: number; - deletedAt: string; - deletedBy: string; - }, -) => { - const result = await db - .from("card") - .update({ deletedAt: args.deletedAt, deletedBy: args.deletedBy }) - .eq("id", args.cardId); - - return result; -}; - -export const destroyAllByListIds = async ( - db: SupabaseClient, - args: { - listIds: number[]; - deletedAt: string; - deletedBy: string; - }, -) => { - const result = await db - .from("card") - .update({ deletedAt: args.deletedAt, deletedBy: args.deletedBy }) - .in("listId", args.listIds) - .is("deletedAt", null); - - return result; -}; - export const getCardWithListByPublicId = async ( db: SupabaseClient, cardPublicId: string, @@ -168,28 +135,6 @@ export const bulkCreate = async ( return data; }; -export const destroyCardLabelRelationship = async ( - db: SupabaseClient, - args: { cardId: number; labelId: number }, -) => { - const result = await db - .from("_card_labels") - .delete() - .eq("cardId", args.cardId) - .eq("labelId", args.labelId); - - return result; -}; - -export const destroyAllCardLabelRelationships = async ( - db: SupabaseClient, - labelId: number, -) => { - const result = await db.from("_card_labels").delete().eq("labelId", labelId); - - return result; -}; - export const createCardLabelRelationship = async ( db: SupabaseClient, cardLabelRelationshipInput: { cardId: number; labelId: number }, @@ -217,19 +162,6 @@ export const getCardMemberRelationship = async ( return data; }; -export const destroyCardMemberRelationship = async ( - db: SupabaseClient, - args: { cardId: number; memberId: number }, -) => { - const result = await db - .from("_card_workspace_members") - .delete() - .eq("cardId", args.cardId) - .eq("workspaceMemberId", args.memberId); - - return result; -}; - export const createCardMemberRelationship = async ( db: SupabaseClient, cardMemberRelationshipInput: { cardId: number; memberId: number }, @@ -353,3 +285,71 @@ export const pushIndex = async ( return data; }; + +export const softDelete = async ( + db: SupabaseClient, + args: { + cardId: number; + deletedAt: string; + deletedBy: string; + }, +) => { + const result = await db + .from("card") + .update({ deletedAt: args.deletedAt, deletedBy: args.deletedBy }) + .eq("id", args.cardId); + + return result; +}; + +export const softDeleteAllByListIds = async ( + db: SupabaseClient, + args: { + listIds: number[]; + deletedAt: string; + deletedBy: string; + }, +) => { + const result = await db + .from("card") + .update({ deletedAt: args.deletedAt, deletedBy: args.deletedBy }) + .in("listId", args.listIds) + .is("deletedAt", null); + + return result; +}; + +export const hardDeleteCardMemberRelationship = async ( + db: SupabaseClient, + args: { cardId: number; memberId: number }, +) => { + const result = await db + .from("_card_workspace_members") + .delete() + .eq("cardId", args.cardId) + .eq("workspaceMemberId", args.memberId); + + return result; +}; + +export const hardDeleteCardLabelRelationship = async ( + db: SupabaseClient, + args: { cardId: number; labelId: number }, +) => { + const result = await db + .from("_card_labels") + .delete() + .eq("cardId", args.cardId) + .eq("labelId", args.labelId); + + return result; +}; + +export const hardDeleteAllCardLabelRelationships = async ( + db: SupabaseClient, + labelId: number, +) => { + const result = await db.from("_card_labels").delete().eq("labelId", labelId); + + return result; +}; diff --git a/src/server/db/repository/label.repo.ts b/src/server/db/repository/label.repo.ts index a839ce7b..b0d322f8 100644 --- a/src/server/db/repository/label.repo.ts +++ b/src/server/db/repository/label.repo.ts @@ -79,7 +79,7 @@ export const update = async ( return data; }; -export const destroy = async ( +export const hardDelete = async ( db: SupabaseClient, labelId: number, ) => { diff --git a/src/server/db/repository/list.repo.ts b/src/server/db/repository/list.repo.ts index 2fddea3a..ae21d19a 100644 --- a/src/server/db/repository/list.repo.ts +++ b/src/server/db/repository/list.repo.ts @@ -85,40 +85,6 @@ export const update = async ( return data; }; -export const destroyAllByBoardId = async ( - db: SupabaseClient, - args: { - boardId: number; - deletedAt: string; - deletedBy: string; - }, -) => { - const result = await db - .from("list") - .update({ deletedAt: args.deletedAt, deletedBy: args.deletedBy }) - .eq("boardId", args.boardId) - .is("deletedAt", null); - - return result; -}; - -export const destroyById = async ( - db: SupabaseClient, - args: { - listId: number; - deletedAt: string; - deletedBy: string; - }, -) => { - const result = await db - .from("list") - .update({ deletedAt: args.deletedAt, deletedBy: args.deletedBy }) - .eq("id", args.listId) - .is("deletedAt", null); - - return result; -}; - export const reorder = async ( db: SupabaseClient, args: { @@ -152,3 +118,37 @@ export const shiftIndex = async ( return data; }; + +export const softDeleteAllByBoardId = async ( + db: SupabaseClient, + args: { + boardId: number; + deletedAt: string; + deletedBy: string; + }, +) => { + const result = await db + .from("list") + .update({ deletedAt: args.deletedAt, deletedBy: args.deletedBy }) + .eq("boardId", args.boardId) + .is("deletedAt", null); + + return result; +}; + +export const softDeleteById = async ( + db: SupabaseClient, + args: { + listId: number; + deletedAt: string; + deletedBy: string; + }, +) => { + const result = await db + .from("list") + .update({ deletedAt: args.deletedAt, deletedBy: args.deletedBy }) + .eq("id", args.listId) + .is("deletedAt", null); + + return result; +}; diff --git a/src/server/db/repository/workspace.repo.ts b/src/server/db/repository/workspace.repo.ts index 7f9f3475..633e47f1 100644 --- a/src/server/db/repository/workspace.repo.ts +++ b/src/server/db/repository/workspace.repo.ts @@ -127,3 +127,15 @@ export const getAllMembersByPublicIds = async ( return data; }; + +export const hardDelete = async ( + db: SupabaseClient, + workspacePublicId: string, +) => { + const result = db + .from("workspace") + .delete() + .eq("publicId", workspacePublicId); + + return result; +}; diff --git a/src/server/db/schema.ts b/src/server/db/schema.ts index 3af3e6b3..be2396a0 100644 --- a/src/server/db/schema.ts +++ b/src/server/db/schema.ts @@ -12,217 +12,237 @@ import { bigint, } from "drizzle-orm/pg-core"; -export const importSourceEnum = pgEnum('source', ['trello']); -export const importStatusEnum = pgEnum('status', ['started', 'success', 'failed']); -export const memberRoleEnum = pgEnum('role', ['admin', 'member', 'guest']); +export const importSourceEnum = pgEnum("source", ["trello"]); +export const importStatusEnum = pgEnum("status", [ + "started", + "success", + "failed", +]); +export const memberRoleEnum = pgEnum("role", ["admin", "member", "guest"]); -export const boards = pgTable( - "board", - { - id: bigserial("id", { mode: "number" }).primaryKey(), - publicId: varchar("publicId", { length: 12 }).notNull().unique(), - name: varchar("name", { length: 255 }).notNull(), - createdBy: uuid("createdBy").notNull().references(() => users.id), - createdAt: timestamp("createdAt").defaultNow().notNull(), - updatedAt: timestamp("updatedAt"), - deletedAt: timestamp("deletedAt"), - deletedBy: uuid("deletedBy").references(() => users.id), - importId: bigint("importId", { mode: "number" }).references(() => imports.id), - workspaceId: bigint("workspaceId", { mode: "number" }).notNull().references(() => workspaces.id), - }, -); +export const boards = pgTable("board", { + id: bigserial("id", { mode: "number" }).primaryKey(), + publicId: varchar("publicId", { length: 12 }).notNull().unique(), + name: varchar("name", { length: 255 }).notNull(), + createdBy: uuid("createdBy") + .notNull() + .references(() => users.id), + createdAt: timestamp("createdAt").defaultNow().notNull(), + updatedAt: timestamp("updatedAt"), + deletedAt: timestamp("deletedAt"), + deletedBy: uuid("deletedBy").references(() => users.id), + importId: bigint("importId", { mode: "number" }).references(() => imports.id), + workspaceId: bigint("workspaceId", { mode: "number" }) + .notNull() + .references(() => workspaces.id, { onDelete: "cascade" }), +}); export const boardsRelations = relations(boards, ({ one, many }) => ({ - createdBy: one(users, { - fields: [boards.createdBy], - references: [users.id], - }), + createdBy: one(users, { + fields: [boards.createdBy], + references: [users.id], + }), lists: many(lists), labels: many(labels), deletedBy: one(users, { - fields: [boards.deletedBy], - references: [users.id], - }), + fields: [boards.deletedBy], + references: [users.id], + }), import: one(imports, { - fields: [boards.importId], - references: [imports.id], - }), + fields: [boards.importId], + references: [imports.id], + }), workspace: one(workspaces, { - fields: [boards.workspaceId], - references: [workspaces.id], - }), + fields: [boards.workspaceId], + references: [workspaces.id], + }), })); -export const imports = pgTable( - "import", - { - id: bigserial("id", { mode: "number" }).primaryKey(), - publicId: varchar("publicId", { length: 12 }).notNull().unique(), - source: importSourceEnum('source').notNull(), - status: importStatusEnum('status').notNull(), - createdBy: uuid("createdBy").notNull().references(() => users.id), - createdAt: timestamp("createdAt").defaultNow().notNull(), - }, -); +export const imports = pgTable("import", { + id: bigserial("id", { mode: "number" }).primaryKey(), + publicId: varchar("publicId", { length: 12 }).notNull().unique(), + source: importSourceEnum("source").notNull(), + status: importStatusEnum("status").notNull(), + createdBy: uuid("createdBy") + .notNull() + .references(() => users.id), + createdAt: timestamp("createdAt").defaultNow().notNull(), +}); export const importsRelations = relations(imports, ({ one, many }) => ({ - createdBy: one(users, { - fields: [imports.createdBy], - references: [users.id], - }), + createdBy: one(users, { + fields: [imports.createdBy], + references: [users.id], + }), boards: many(boards), cards: many(cards), lists: many(lists), - labels: many(labels) + labels: many(labels), })); -export const labels = pgTable( - "label", - { - id: bigserial("id", { mode: "number" }).primaryKey(), - publicId: varchar("publicId", { length: 12 }).notNull().unique(), - name: varchar("name", { length: 255 }).notNull(), - colourCode: varchar("colourCode", { length: 12 }), - createdBy: uuid("createdBy").notNull().references(() => users.id), - createdAt: timestamp("createdAt").defaultNow().notNull(), - updatedAt: timestamp("updatedAt"), - boardId: bigint("boardId", { mode: "number" }).notNull().references(() => boards.id), - importId: bigint("importId", { mode: "number" }).references(() => imports.id), - } -); +export const labels = pgTable("label", { + id: bigserial("id", { mode: "number" }).primaryKey(), + publicId: varchar("publicId", { length: 12 }).notNull().unique(), + name: varchar("name", { length: 255 }).notNull(), + colourCode: varchar("colourCode", { length: 12 }), + createdBy: uuid("createdBy") + .notNull() + .references(() => users.id), + createdAt: timestamp("createdAt").defaultNow().notNull(), + updatedAt: timestamp("updatedAt"), + boardId: bigint("boardId", { mode: "number" }) + .notNull() + .references(() => boards.id, { onDelete: "cascade" }), + importId: bigint("importId", { mode: "number" }).references(() => imports.id), +}); export const labelsRelations = relations(labels, ({ one, many }) => ({ - createdBy: one(users, { - fields: [labels.createdBy], - references: [users.id], - }), + createdBy: one(users, { + fields: [labels.createdBy], + references: [users.id], + }), board: one(boards, { - fields: [labels.boardId], - references: [boards.id], - }), + fields: [labels.boardId], + references: [boards.id], + }), cards: many(cardsToLabels), import: one(imports, { - fields: [labels.importId], - references: [imports.id], - }), + fields: [labels.importId], + references: [imports.id], + }), })); export const cardsToLabels = pgTable( "_card_labels", { - cardId: bigint("cardId", { mode: "number" }).notNull().references(() => cards.id), - labelId: bigint("labelId", { mode: "number" }).notNull().references(() => labels.id), - }, (t) => ({ + cardId: bigint("cardId", { mode: "number" }) + .notNull() + .references(() => cards.id), + labelId: bigint("labelId", { mode: "number" }) + .notNull() + .references(() => labels.id, { onDelete: "cascade" }), + }, + (t) => ({ pk: primaryKey(t.cardId, t.labelId), }), ); export const cardToLabelsRelations = relations(cardsToLabels, ({ one }) => ({ - card: one(cards, { - fields: [cardsToLabels.cardId], - references: [cards.id], - }), + card: one(cards, { + fields: [cardsToLabels.cardId], + references: [cards.id], + }), label: one(labels, { - fields: [cardsToLabels.labelId], - references: [labels.id], - }), + fields: [cardsToLabels.labelId], + references: [labels.id], + }), })); export const cardToWorkspaceMembers = pgTable( "_card_workspace_members", { - cardId: bigint("cardId", { mode: "number" }).notNull().references(() => cards.id), - workspaceMemberId: bigint("workspaceMemberId", { mode: "number" }).notNull().references(() => workspaceMembers.id), - }, (t) => ({ + cardId: bigint("cardId", { mode: "number" }) + .notNull() + .references(() => cards.id), + workspaceMemberId: bigint("workspaceMemberId", { mode: "number" }) + .notNull() + .references(() => workspaceMembers.id, { onDelete: "cascade" }), + }, + (t) => ({ pk: primaryKey(t.cardId, t.workspaceMemberId), }), ); -export const cardToWorkspaceMembersRelations = relations(cardToWorkspaceMembers, ({ one }) => ({ - card: one(cards, { - fields: [cardToWorkspaceMembers.cardId], - references: [cards.id], - }), - member: one(workspaceMembers, { - fields: [cardToWorkspaceMembers.workspaceMemberId], - references: [workspaceMembers.id], - }), -})); - -export const lists = pgTable( - "list", - { - id: bigserial("id", { mode: "number" }).primaryKey(), - publicId: varchar("publicId", { length: 12 }).notNull().unique(), - name: varchar("name", { length: 255 }).notNull(), - index: integer("index").notNull(), - createdBy: uuid("createdBy").notNull().references(() => users.id), - createdAt: timestamp("createdAt").defaultNow().notNull(), - updatedAt: timestamp("updatedAt"), - deletedAt: timestamp("deletedAt"), - deletedBy: uuid("deletedBy").references(() => users.id), - boardId: bigint("boardId", { mode: "number" }).notNull().references(() => boards.id), - importId: bigint("importId", { mode: "number" }).references(() => imports.id), - } +export const cardToWorkspaceMembersRelations = relations( + cardToWorkspaceMembers, + ({ one }) => ({ + card: one(cards, { + fields: [cardToWorkspaceMembers.cardId], + references: [cards.id], + }), + member: one(workspaceMembers, { + fields: [cardToWorkspaceMembers.workspaceMemberId], + references: [workspaceMembers.id], + }), + }), ); +export const lists = pgTable("list", { + id: bigserial("id", { mode: "number" }).primaryKey(), + publicId: varchar("publicId", { length: 12 }).notNull().unique(), + name: varchar("name", { length: 255 }).notNull(), + index: integer("index").notNull(), + createdBy: uuid("createdBy") + .notNull() + .references(() => users.id), + createdAt: timestamp("createdAt").defaultNow().notNull(), + updatedAt: timestamp("updatedAt"), + deletedAt: timestamp("deletedAt"), + deletedBy: uuid("deletedBy").references(() => users.id), + boardId: bigint("boardId", { mode: "number" }) + .notNull() + .references(() => boards.id, { onDelete: "cascade" }), + importId: bigint("importId", { mode: "number" }).references(() => imports.id), +}); + export const listsRelations = relations(lists, ({ one, many }) => ({ - createdBy: one(users, { - fields: [lists.createdBy], - references: [users.id], - }), + createdBy: one(users, { + fields: [lists.createdBy], + references: [users.id], + }), board: one(boards, { - fields: [lists.boardId], - references: [boards.id], - }), + fields: [lists.boardId], + references: [boards.id], + }), cards: many(cards), deletedBy: one(users, { - fields: [lists.deletedBy], - references: [users.id], - }), + fields: [lists.deletedBy], + references: [users.id], + }), import: one(imports, { - fields: [lists.importId], - references: [imports.id], - }), + fields: [lists.importId], + references: [imports.id], + }), })); -export const cards = pgTable( - "card", - { - id: bigserial("id", { mode: "number" }).primaryKey(), - publicId: varchar("publicId", { length: 12 }).notNull().unique(), - title: varchar("title", { length: 255 }).notNull(), - description: text("description"), - index: integer("index").notNull(), - createdBy: uuid("createdBy").notNull().references(() => users.id), - createdAt: timestamp("createdAt").defaultNow().notNull(), - updatedAt: timestamp("updatedAt"), - deletedAt: timestamp("deletedAt"), - deletedBy: uuid("deletedBy").references(() => users.id), - listId: bigint("listId", { mode: "number" }).notNull().references(() => lists.id), - importId: bigint("importId", { mode: "number" }).references(() => imports.id), - } -); +export const cards = pgTable("card", { + id: bigserial("id", { mode: "number" }).primaryKey(), + publicId: varchar("publicId", { length: 12 }).notNull().unique(), + title: varchar("title", { length: 255 }).notNull(), + description: text("description"), + index: integer("index").notNull(), + createdBy: uuid("createdBy") + .notNull() + .references(() => users.id), + createdAt: timestamp("createdAt").defaultNow().notNull(), + updatedAt: timestamp("updatedAt"), + deletedAt: timestamp("deletedAt"), + deletedBy: uuid("deletedBy").references(() => users.id), + listId: bigint("listId", { mode: "number" }) + .notNull() + .references(() => lists.id, { onDelete: "cascade" }), + importId: bigint("importId", { mode: "number" }).references(() => imports.id), +}); export const cardsRelations = relations(cards, ({ one, many }) => ({ - createdBy: one(users, { - fields: [cards.createdBy], - references: [users.id], - }), + createdBy: one(users, { + fields: [cards.createdBy], + references: [users.id], + }), list: one(lists, { - fields: [cards.listId], - references: [lists.id], - }), + fields: [cards.listId], + references: [lists.id], + }), deletedBy: one(users, { - fields: [cards.deletedBy], - references: [users.id], - }), + fields: [cards.deletedBy], + references: [users.id], + }), labels: many(cardsToLabels), members: many(cardToWorkspaceMembers), import: one(imports, { - fields: [cards.importId], - references: [imports.id], - }), + fields: [cards.importId], + references: [imports.id], + }), })); export const users = pgTable("user", { @@ -241,51 +261,55 @@ export const usersRelations = relations(users, ({ many }) => ({ workspaces: many(workspaces), })); -export const workspaces = pgTable( - "workspace", - { - id: bigserial("id", { mode: "number" }).primaryKey(), - publicId: varchar("publicId", { length: 12 }).notNull().unique(), - name: varchar("name", { length: 255 }).notNull(), - slug: varchar("slug", { length: 255 }).notNull().unique(), - createdBy: uuid("createdBy").notNull().references(() => users.id), - createdAt: timestamp("createdAt").defaultNow().notNull(), - updatedAt: timestamp("updatedAt"), - deletedAt: timestamp("deletedAt"), - deletedBy: uuid("deletedBy").references(() => users.id), - } -); +export const workspaces = pgTable("workspace", { + id: bigserial("id", { mode: "number" }).primaryKey(), + publicId: varchar("publicId", { length: 12 }).notNull().unique(), + name: varchar("name", { length: 255 }).notNull(), + slug: varchar("slug", { length: 255 }).notNull().unique(), + createdBy: uuid("createdBy") + .notNull() + .references(() => users.id), + createdAt: timestamp("createdAt").defaultNow().notNull(), + updatedAt: timestamp("updatedAt"), + deletedAt: timestamp("deletedAt"), + deletedBy: uuid("deletedBy").references(() => users.id), +}); export const workspaceRelations = relations(workspaces, ({ one, many }) => ({ user: one(users, { fields: [workspaces.createdBy], references: [users.id] }), - members: many(workspaceMembers) + members: many(workspaceMembers), })); -export const workspaceMembers = pgTable( - "workspace_members", - { - id: bigserial("id", { mode: "number" }).primaryKey(), - publicId: varchar("publicId", { length: 12 }).notNull().unique(), - userId: uuid("userId").notNull().references(() => users.id), - workspaceId: bigint("workspaceId", { mode: "number" }).notNull().references(() => workspaces.id), - createdBy: uuid("createdBy").notNull(), - createdAt: timestamp("createdAt").defaultNow().notNull(), - updatedAt: timestamp("updatedAt"), - deletedAt: timestamp("deletedAt"), - role: memberRoleEnum('role').notNull(), - }, +export const workspaceMembers = pgTable("workspace_members", { + id: bigserial("id", { mode: "number" }).primaryKey(), + publicId: varchar("publicId", { length: 12 }).notNull().unique(), + userId: uuid("userId") + .notNull() + .references(() => users.id), + workspaceId: bigint("workspaceId", { mode: "number" }) + .notNull() + .references(() => workspaces.id, { onDelete: "cascade" }), + createdBy: uuid("createdBy").notNull(), + createdAt: timestamp("createdAt").defaultNow().notNull(), + updatedAt: timestamp("updatedAt"), + deletedAt: timestamp("deletedAt"), + role: memberRoleEnum("role").notNull(), +}); + +export const usersToWorkspacesRelations = relations( + workspaceMembers, + ({ one }) => ({ + addedBy: one(users, { + fields: [workspaceMembers.createdBy], + references: [users.id], + }), + user: one(users, { + fields: [workspaceMembers.userId], + references: [users.id], + }), + workspace: one(workspaces, { + fields: [workspaceMembers.workspaceId], + references: [workspaces.id], + }), + }), ); - -export const usersToWorkspacesRelations = relations(workspaceMembers, ({ one }) => ({ - addedBy: one(users, { fields: [workspaceMembers.createdBy], references: [users.id] }), - user: one(users, { - fields: [workspaceMembers.userId], - references: [users.id], - }), - workspace: one(workspaces, { - fields: [workspaceMembers.workspaceId], - references: [workspaces.id], - }), -})); - - diff --git a/src/views/settings/components/DeleteWorkspaceConfirmation.tsx b/src/views/settings/components/DeleteWorkspaceConfirmation.tsx new file mode 100644 index 00000000..adb64a39 --- /dev/null +++ b/src/views/settings/components/DeleteWorkspaceConfirmation.tsx @@ -0,0 +1,96 @@ +import { useState } from "react"; +import { useRouter } from "next/navigation"; + +import { api } from "~/utils/api"; +import { useModal } from "~/providers/modal"; +import { usePopup } from "~/providers/popup"; +import { useWorkspace } from "~/providers/workspace"; + +import Button from "~/components/Button"; + +export function DeleteWorkspaceConfirmation() { + const { closeModal } = useModal(); + const { workspace, switchWorkspace, availableWorkspaces } = useWorkspace(); + const { showPopup } = usePopup(); + const router = useRouter(); + + const [isAcknowledgmentChecked, setIsAcknowledgmentChecked] = useState(false); + + const deleteWorkspaceMutation = api.workspace.delete.useMutation({ + onSuccess: () => { + closeModal(); + const filteredWorkspaces = availableWorkspaces.filter( + (ws) => ws.publicId !== workspace?.publicId, + ); + if (filteredWorkspaces.length > 0 && filteredWorkspaces[0]) { + switchWorkspace(filteredWorkspaces[0]); + } else { + router.push("/"); + } + }, + onError: () => { + closeModal(); + showPopup({ + header: "Error deleting workspace", + message: "Please try again later, or contact customer support.", + }); + }, + }); + + const handleDeleteWorkspace = () => { + deleteWorkspaceMutation.mutate({ + workspacePublicId: workspace?.publicId, + }); + }; + + return ( +
+
+

+ {`Are you sure you want to delete the workspace ${workspace?.name}?`} +

+

+ Keep in mind that this action is irreversible. +

+

+ This will result in the permanent deletion of all data associated with + this workspace. +

+
+
+
+ + setIsAcknowledgmentChecked(!isAcknowledgmentChecked) + } + /> +
+
+

+ I acknowledge that all of the workspace data will be permanently + deleted and want to proceed. +

+
+
+
+ + +
+
+ ); +} diff --git a/src/views/settings/index.tsx b/src/views/settings/index.tsx index 1cdd6be1..ca229c7a 100644 --- a/src/views/settings/index.tsx +++ b/src/views/settings/index.tsx @@ -1,10 +1,10 @@ import { useModal } from "~/providers/modal"; import Modal from "~/components/modal"; - -import { NewWorkspaceForm } from "~/components/NewWorkspaceForm"; +import Button from "~/components/Button"; +import { DeleteWorkspaceConfirmation } from "./components/DeleteWorkspaceConfirmation"; export default function SettingsPage() { - const { modalContentType } = useModal(); + const { modalContentType, openModal } = useModal(); return (
@@ -14,16 +14,23 @@ export default function SettingsPage() {
-
-
-
-
-
-
+
+

+ Delete workspace +

+

+ Once you delete your workspace, there is no going back. Please be + certain. +

+
- {modalContentType === "NEW_WORKSPACE" && } + {modalContentType === "DELETE_WORKSPACE" && ( + + )}
); diff --git a/supabase/seed.sql b/supabase/seed.sql index bd2a740c..726662f4 100644 --- a/supabase/seed.sql +++ b/supabase/seed.sql @@ -207,10 +207,25 @@ USING ( ) ); -CREATE POLICY "Allow access to user's workspaces" +CREATE POLICY "Allow viewing user's workspaces" ON public.workspace AS PERMISSIVE -FOR ALL +FOR SELECT +TO authenticated +USING ( + id IN ( + SELECT "workspaceId" + FROM workspace_members + WHERE "userId" = auth.uid() + ) + OR + "createdBy" = auth.uid() +); + +CREATE POLICY "Allow updating user's workspaces" +ON public.workspace +AS PERMISSIVE +FOR UPDATE TO authenticated USING ( id IN ( @@ -220,6 +235,26 @@ USING ( ) ); +CREATE POLICY "Allow deleting user's workspaces" +ON public.workspace +AS PERMISSIVE +FOR DELETE +TO authenticated +USING ( + id IN ( + SELECT "workspaceId" + FROM workspace_members + WHERE "userId" = auth.uid() + ) +); + +CREATE POLICY "Allow authenticated users to create workspaces" +ON public.workspace +AS PERMISSIVE +FOR INSERT +TO authenticated +USING (true); + CREATE POLICY "Allow access to user's own workspace membership" ON public.workspace_members AS PERMISSIVE