Skip to content

Commit

Permalink
feat: delete space
Browse files Browse the repository at this point in the history
  • Loading branch information
nichenqin committed Aug 8, 2024
1 parent 7acd2d7 commit c8ddad2
Show file tree
Hide file tree
Showing 19 changed files with 282 additions and 72 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -144,8 +144,11 @@ CREATE TABLE `undb_space` (
`created_by` text NOT NULL,
`updated_at` text NOT NULL,
`updated_by` text NOT NULL,
`deleted_at` integer,
`deleted_by` text,
FOREIGN KEY (`created_by`) REFERENCES `undb_user`(`id`) ON UPDATE no action ON DELETE no action,
FOREIGN KEY (`updated_by`) REFERENCES `undb_user`(`id`) ON UPDATE no action ON DELETE no action
FOREIGN KEY (`updated_by`) REFERENCES `undb_user`(`id`) ON UPDATE no action ON DELETE no action,
FOREIGN KEY (`deleted_by`) REFERENCES `undb_user`(`id`) ON UPDATE no action ON DELETE no action
);
--> statement-breakpoint
CREATE TABLE `undb_space_member` (
Expand Down
29 changes: 28 additions & 1 deletion apps/backend/drizzle/meta/0000_snapshot.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"version": "6",
"dialect": "sqlite",
"id": "c3c2dc77-f394-4926-a0f1-11dfa1b810df",
"id": "608a3167-9943-4485-be34-ab091ebda8e1",
"prevId": "00000000-0000-0000-0000-000000000000",
"tables": {
"undb_api_token": {
Expand Down Expand Up @@ -1157,6 +1157,20 @@
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"deleted_at": {
"name": "deleted_at",
"type": "integer",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"deleted_by": {
"name": "deleted_by",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
}
},
"indexes": {
Expand Down Expand Up @@ -1194,6 +1208,19 @@
],
"onDelete": "no action",
"onUpdate": "no action"
},
"undb_space_deleted_by_undb_user_id_fk": {
"name": "undb_space_deleted_by_undb_user_id_fk",
"tableFrom": "undb_space",
"tableTo": "undb_user",
"columnsFrom": [
"deleted_by"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
Expand Down
4 changes: 2 additions & 2 deletions apps/backend/drizzle/meta/_journal.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@
{
"idx": 0,
"version": "6",
"when": 1722954750445,
"tag": "0000_stormy_red_wolf",
"when": 1723098293272,
"tag": "0000_lively_warstar",
"breakpoints": true
}
]
Expand Down
10 changes: 7 additions & 3 deletions apps/backend/src/db.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,18 @@ import { IsolationLevel } from "kysely"
export const withTransaction =
(qb: IQueryBuilder, level: IsolationLevel = "read committed") =>
<T = any>(callback: () => Promise<T>): Promise<T> => {
return new Promise((resolve) => {
return new Promise((resolve, reject) => {
return qb
.transaction()
.setIsolationLevel(level)
.execute(async (trx) => {
startTransaction(trx)
const result = await callback()
resolve(result)
try {
const result = await callback()
resolve(result)
} catch (error) {
reject(error)
}
})
})
}
92 changes: 61 additions & 31 deletions apps/backend/src/modules/space/space.module.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
import { singleton } from "@undb/di"
import { DeleteSpaceCommand } from "@undb/commands"
import { getCurrentUserId } from "@undb/context/server"
import { CommandBus } from "@undb/cqrs"
import { inject, singleton } from "@undb/di"
import { injectQueryBuilder, type IQueryBuilder } from "@undb/persistence"
import { injectSpaceService, type ISpaceService } from "@undb/space"
import Elysia, { t } from "elysia"
import { type Lucia } from "lucia"
import { withTransaction } from "../../db"
import { injectLucia } from "../auth/auth.provider"

@singleton()
Expand All @@ -11,43 +16,68 @@ export class SpaceModule {
private readonly lucia: Lucia,
@injectSpaceService()
private readonly spaceService: ISpaceService,
@inject(CommandBus)
private readonly commandBus: CommandBus,
@injectQueryBuilder()
private readonly qb: IQueryBuilder,
) {}
public route() {
return new Elysia().get(
"/api/spaces/:spaceId/goto",
async (ctx) => {
const { spaceId } = ctx.params
const space = (await this.spaceService.getSpace({ spaceId })).expect("Space not found")
return new Elysia()
.get(
"/api/spaces/:spaceId/goto",
async (ctx) => {
const { spaceId } = ctx.params
const space = (await this.spaceService.getSpace({ spaceId })).expect("Space not found")

const cookieHeader = ctx.request.headers.get("Cookie") ?? ""
const sessionId = this.lucia.readSessionCookie(cookieHeader)
const cookieHeader = ctx.request.headers.get("Cookie") ?? ""
const sessionId = this.lucia.readSessionCookie(cookieHeader)

if (!sessionId) {
return new Response("Unauthorized", { status: 401 })
}
if (!sessionId) {
return new Response("Unauthorized", { status: 401 })
}

const { session, user } = await this.lucia.validateSession(sessionId)
if (!user) {
const { session, user } = await this.lucia.validateSession(sessionId)
if (!user) {
return new Response(null, {
status: 401,
})
}
await this.lucia.invalidateUserSessions(user.id)
const updatedSession = await this.lucia.createSession(user.id, { space_id: space.id.value })
const sessionCookie = this.lucia.createSessionCookie(updatedSession.id)
return new Response(null, {
status: 401,
status: 302,
headers: {
Location: "/",
"Set-Cookie": sessionCookie.serialize(),
},
})
},
{
params: t.Object({
spaceId: t.String(),
}),
},
)
.delete("/api/space", async (ctx) => {
return withTransaction(this.qb)(async () => {
await this.commandBus.execute(new DeleteSpaceCommand({}))

const userId = getCurrentUserId()

await this.lucia.invalidateSession(userId)
const space = (await this.spaceService.getSpace({ userId })).expect("Space not found")

const updatedSession = await this.lucia.createSession(userId, { space_id: space.id.value })
const sessionCookie = this.lucia.createSessionCookie(updatedSession.id)
return new Response(null, {
status: 200,
headers: {
Location: "/",
"Set-Cookie": sessionCookie.serialize(),
},
})
}
await this.lucia.invalidateUserSessions(user.id)
const updatedSession = await this.lucia.createSession(user.id, { space_id: space.id.value })
const sessionCookie = this.lucia.createSessionCookie(updatedSession.id)
return new Response(null, {
status: 302,
headers: {
Location: "/",
"Set-Cookie": sessionCookie.serialize(),
},
})
},
{
params: t.Object({
spaceId: t.String(),
}),
},
)
})
}
}
65 changes: 61 additions & 4 deletions apps/frontend/src/lib/components/blocks/space/space-setting.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,13 @@
import { Input } from "$lib/components/ui/input/index.js"
import type { ISpaceDTO } from "@undb/space"
import { toast } from "svelte-sonner"
import { Button } from "$lib/components/ui/button"
import * as AlertDialog from "$lib/components/ui/alert-dialog/index.js"
export let space: ISpaceDTO
let deleteConfirm = ""
const form = superForm(
defaults(
{
Expand All @@ -24,13 +28,13 @@
dataType: "json",
validators: zodClient(updateSpaceCommand),
resetForm: false,
invalidateAll: true,
onUpdate(event) {
invalidateAll: false,
async onUpdate(event) {
if (!event.form.valid) {
return
}
$updateSpaceMutation.mutate(event.form.data)
await $updateSpaceMutation.mutateAsync(event.form.data)
},
},
)
Expand All @@ -43,9 +47,18 @@
})
const { form: formData, enhance } = form
const deleteSpaceMutation = createMutation({
mutationFn: () => fetch("/api/space", { method: "DELETE" }),
async onSuccess() {
toast.success("Space deleted successfully")
window.location.replace("/")
},
})
</script>

<section class="mx-auto">
<section class="mx-auto space-y-6">
<!-- Update space -->
<form method="POST" class="w-2/3 space-y-4" use:enhance>
<Form.Field {form} name="name">
<Form.Control let:attrs>
Expand All @@ -61,4 +74,48 @@
<!-- <SuperDebug data={$formData} /> -->
{/if}
</form>

<!-- Delete space -->
<div class="w-2/3 space-y-3 rounded-md border-2 border-red-500 p-4">
<p class="text-red-500">Danger Zone</p>
<div>Delete Space</div>

{#if space.isPersonal}
<p class="text-muted-foreground">You can not delete your personal space.</p>
{/if}

<AlertDialog.Root>
<AlertDialog.Trigger asChild let:builder>
<Button variant="destructive" builders={[builder]} disabled={space.isPersonal}>Delete Space</Button>
</AlertDialog.Trigger>
<AlertDialog.Content>
<AlertDialog.Header>
<AlertDialog.Title>Are you absolutely sure to delete space?</AlertDialog.Title>
<AlertDialog.Description>
This action cannot be undone. This will permanently delete your database state and remove your data from our
servers.
</AlertDialog.Description>
</AlertDialog.Header>

<p>Please type <span class="text-red-500">DELETE</span> to confirm.</p>
<Input bind:value={deleteConfirm} placeholder="DELETE" />

<AlertDialog.Footer>
<AlertDialog.Cancel>Cancel</AlertDialog.Cancel>
<AlertDialog.Action let:builder asChild>
<Button
variant="destructive"
builders={[builder]}
disabled={$deleteSpaceMutation.isPending || space.isPersonal || deleteConfirm !== "DELETE"}
on:click={async () => {
await $deleteSpaceMutation.mutateAsync()
}}
>
Delete Space
</Button>
</AlertDialog.Action>
</AlertDialog.Footer>
</AlertDialog.Content>
</AlertDialog.Root>
</div>
</section>
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@
<div
class={cn(
"group flex h-8 items-center justify-between pl-4 pr-2 transition-all",
active && !tableId && !viewId ? "hover:bg-primary/60 bg-primary/50 rounded-md" : "hover:bg-gray-100",
active && !tableId && !viewId ? "bg-primary/90 text-background rounded-md" : "hover:bg-gray-100",
)}
>
<a
Expand All @@ -82,7 +82,10 @@
</a>

<div
class="item-center text-muted-foreground flex justify-center gap-2 opacity-0 transition-all group-hover:opacity-100"
class={cn(
"item-center text-muted-foreground flex justify-center gap-2 opacity-0 transition-all group-hover:opacity-100",
active && "text-background",
)}
>
<button
class="h-full"
Expand All @@ -95,7 +98,11 @@
</button>
<Collapsible.Trigger class="h-full">
<ChevronRightIcon
class={cn("text-muted-foreground h-4 w-4 transition-all", open[base.id] && "rotate-90")}
class={cn(
"text-muted-foreground h-4 w-4 transition-all",
open[base.id] && "rotate-90",
active && "text-background",
)}
/>
</Collapsible.Trigger>
</div>
Expand All @@ -108,14 +115,14 @@
<div
class={cn(
"group flex h-8 cursor-pointer items-center justify-between rounded-md pl-8 pr-2 transition-all",
active && !viewId ? "hover:bg-primary/60 bg-primary/50" : "hover:bg-gray-100",
active && !viewId ? "bg-primary/90" : "hover:bg-gray-100",
)}
>
<a
href={`/t/${table.id}`}
class={cn(
"flex h-full flex-1 items-center font-light",
active && "text-primary text-background font-medium",
"text-primary flex h-full flex-1 items-center font-light",
active && !viewId && "text-background font-medium",
)}
>
<DatabaseIcon class="mr-2 h-4 w-4" />
Expand All @@ -130,11 +137,15 @@
}}
class={cn(
"flex h-5 w-5 items-center justify-center rounded-md hover:bg-gray-200",
active && !viewId && "hover:bg-blue-200",
active && !viewId && "hover:bg-primary",
)}
>
<ChevronRightIcon
class={cn("text-muted-foreground h-4 w-4 transition-all", open[table.id] && "rotate-90")}
class={cn(
"text-muted-foreground h-4 w-4 transition-all",
open[table.id] && "rotate-90",
active && "text-background",
)}
/>
</Collapsible.Trigger>
</div>
Expand All @@ -153,14 +164,14 @@
{@const active = view.id === viewId}
<div
class={cn(
"group flex h-8 items-center justify-between pl-14 pr-2 transition-all",
active ? "hover:bg-primary/60 bg-primary/50" : "hover:bg-gray-100",
"group flex h-8 items-center justify-between rounded-sm pl-14 pr-2 transition-all",
active ? "bg-primary/90" : "hover:bg-gray-100",
)}
>
<a
class={cn(
"flex h-full flex-1 items-center text-xs font-light",
active && "text-primary text-background font-medium",
active && "text-background font-medium",
)}
href={`/t/${table.id}/${view.id}`}
>
Expand Down
Loading

0 comments on commit c8ddad2

Please sign in to comment.