From bb3451c0a334c15f06340cb12c7a051909588491 Mon Sep 17 00:00:00 2001 From: ameer2468 <33054370+ameer2468@users.noreply.github.com> Date: Mon, 3 Nov 2025 14:34:36 +0300 Subject: [PATCH 1/6] delete org support --- .../(org)/dashboard/_components/MobileTab.tsx | 20 ++-- .../web/app/(org)/dashboard/dashboard-data.ts | 16 ++- .../settings/organization/Organization.tsx | 4 +- .../components/CapSettingsCard.tsx | 2 +- .../organization/components/DeleteOrg.tsx | 40 +++++++ .../components/DeleteOrgDialog.tsx | 90 ++++++++++++++++ .../components/OrganizationIcon.tsx | 1 - apps/web/app/s/[videoId]/page.tsx | 4 +- .../database/migrations/meta/_journal.json | 101 ++++++++++-------- packages/database/schema.ts | 1 + packages/ui/src/components/Button.tsx | 2 +- .../src/Organisations/OrganisationsRpcs.ts | 9 ++ .../web-backend/src/Organisations/index.ts | 45 +++++++- packages/web-domain/src/Organisation.ts | 9 ++ 14 files changed, 275 insertions(+), 69 deletions(-) create mode 100644 apps/web/app/(org)/dashboard/settings/organization/components/DeleteOrg.tsx create mode 100644 apps/web/app/(org)/dashboard/settings/organization/components/DeleteOrgDialog.tsx diff --git a/apps/web/app/(org)/dashboard/_components/MobileTab.tsx b/apps/web/app/(org)/dashboard/_components/MobileTab.tsx index 7ff0c88474..706433a21b 100644 --- a/apps/web/app/(org)/dashboard/_components/MobileTab.tsx +++ b/apps/web/app/(org)/dashboard/_components/MobileTab.tsx @@ -43,11 +43,13 @@ const MobileTab = () => { } }); return ( -
- - {open && } - - +
+
+ + {open && } + + +
{Tabs.filter((i) => !i.ownerOnly || isOwner).map((tab) => ( @@ -73,7 +75,7 @@ const Orgs = ({
setOpen((p) => !p)} ref={containerRef} - className="flex gap-1.5 items-center p-2 rounded-full border bg-gray-3 border-gray-5" + className="flex gap-1.5 items-center flex-auto max-w-[224px] p-2 rounded-full border bg-gray-3 border-gray-5" > -

+

{activeOrg?.organization.name}

} className={ - "isolate absolute overscroll-contain bottom-14 p-2 space-y-1.5 w-full rounded-xl h-fit border bg-gray-3 max-h-[325px] custom-scroll max-w-[200px] border-gray-4" + "isolate absolute overscroll-contain bottom-14 p-2 space-y-1.5 flex-auto w-full rounded-xl h-fit border bg-gray-3 max-h-[325px] custom-scroll border-gray-4" } > {orgData?.map((organization) => { @@ -142,7 +144,7 @@ const OrgsMenu = ({ />

diff --git a/apps/web/app/(org)/dashboard/dashboard-data.ts b/apps/web/app/(org)/dashboard/dashboard-data.ts index b1e2f0a5bc..e2a45473a0 100644 --- a/apps/web/app/(org)/dashboard/dashboard-data.ts +++ b/apps/web/app/(org)/dashboard/dashboard-data.ts @@ -71,9 +71,12 @@ export async function getDashboardData(user: typeof userSelectProps) { ) .leftJoin(users, eq(organizationMembers.userId, users.id)) .where( - or( - eq(organizations.ownerId, user.id), - eq(organizationMembers.userId, user.id), + and( + or( + eq(organizations.ownerId, user.id), + eq(organizationMembers.userId, user.id), + ), + isNull(organizations.tombstoneAt), ), ); @@ -322,7 +325,12 @@ export async function getDashboardData(user: typeof userSelectProps) { organizationInvites, eq(organizations.id, organizationInvites.organizationId), ) - .where(eq(organizations.ownerId, organization.ownerId)), + .where( + and( + eq(organizations.ownerId, organization.ownerId), + isNull(organizations.tombstoneAt), + ), + ), ); const totalInvites = totalInvitesResult[0]?.value || 0; diff --git a/apps/web/app/(org)/dashboard/settings/organization/Organization.tsx b/apps/web/app/(org)/dashboard/settings/organization/Organization.tsx index f7a760e671..8d6fa89ee4 100644 --- a/apps/web/app/(org)/dashboard/settings/organization/Organization.tsx +++ b/apps/web/app/(org)/dashboard/settings/organization/Organization.tsx @@ -11,9 +11,9 @@ import { import { toast } from "sonner"; import { manageBilling } from "@/actions/organization/manage-billing"; import { useDashboardContext } from "@/app/(org)/dashboard/Contexts"; - import { BillingCard } from "./components/BillingCard"; import CapSettingsCard from "./components/CapSettingsCard"; +import DeleteOrg from "./components/DeleteOrg"; import { InviteDialog } from "./components/InviteDialog"; import { MembersCard } from "./components/MembersCard"; import { OrganizationDetailsCard } from "./components/OrganizationDetailsCard"; @@ -90,6 +90,8 @@ export const Organization = () => { showOwnerToast={showOwnerToast} handleManageBilling={() => handleManageBilling(setLoading)} /> + + ); }; diff --git a/apps/web/app/(org)/dashboard/settings/organization/components/CapSettingsCard.tsx b/apps/web/app/(org)/dashboard/settings/organization/components/CapSettingsCard.tsx index edb9410652..561df22df6 100644 --- a/apps/web/app/(org)/dashboard/settings/organization/components/CapSettingsCard.tsx +++ b/apps/web/app/(org)/dashboard/settings/organization/components/CapSettingsCard.tsx @@ -160,7 +160,7 @@ const CapSettingsCard = () => { {options.map((option) => (

{ + const [toggleDeleteDialog, setToggleDeleteDialog] = useState(false); + const { organizationData, user } = useDashboardContext(); + + return ( + <> + + + + Delete Organization + + Delete your organization and all associated data.{" "} + + + + + + ); +}; + +export default DeleteOrg; diff --git a/apps/web/app/(org)/dashboard/settings/organization/components/DeleteOrgDialog.tsx b/apps/web/app/(org)/dashboard/settings/organization/components/DeleteOrgDialog.tsx new file mode 100644 index 0000000000..0e51779a49 --- /dev/null +++ b/apps/web/app/(org)/dashboard/settings/organization/components/DeleteOrgDialog.tsx @@ -0,0 +1,90 @@ +import { + Button, + Dialog, + DialogContent, + DialogFooter, + DialogHeader, + DialogTitle, + Input, +} from "@cap/ui"; +import type { Organisation } from "@cap/web-domain"; +import { faTrashCan } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { Effect } from "effect"; +import { useRouter } from "next/navigation"; +import { startTransition, useId, useState } from "react"; +import { toast } from "sonner"; +import { useEffectMutation, useRpcClient } from "@/lib/EffectRuntime"; +import { useDashboardContext } from "../../../Contexts"; + +interface DeleteOrgDialogProps { + open: boolean; + onOpenChange: (open: boolean) => void; +} + +const DeleteOrgDialog = ({ open, onOpenChange }: DeleteOrgDialogProps) => { + const { activeOrganization, organizationData, user } = useDashboardContext(); + const [organizationName, setOrganizationName] = useState(""); + const rpc = useRpcClient(); + const router = useRouter(); + const deleteOrg = useEffectMutation({ + mutationFn: Effect.fn(function* () { + yield* rpc.OrganisationDelete({ + id: activeOrganization?.organization.id as Organisation.OrganisationId, + }); + }), + onSuccess: () => { + toast.success("Organization deleted successfully"); + onOpenChange(false); + startTransition(() => { + router.push("/dashboard/caps"); + router.refresh(); + }); + }, + onError: (error) => { + console.error(error); + toast.error("Failed to delete organization"); + }, + }); + + return ( + + + } + description="Removing your organization will delete all associated data, including videos, and cannot be undone." + > + Delete Organization + +
+ setOrganizationName(e.target.value)} + placeholder="Organization name" + /> +
+ + + + +
+
+ ); +}; + +export default DeleteOrgDialog; diff --git a/apps/web/app/(org)/dashboard/settings/organization/components/OrganizationIcon.tsx b/apps/web/app/(org)/dashboard/settings/organization/components/OrganizationIcon.tsx index 98dc6500eb..400320cafd 100644 --- a/apps/web/app/(org)/dashboard/settings/organization/components/OrganizationIcon.tsx +++ b/apps/web/app/(org)/dashboard/settings/organization/components/OrganizationIcon.tsx @@ -8,7 +8,6 @@ import { useId } from "react"; import { toast } from "sonner"; import { FileInput } from "@/components/FileInput"; import { useEffectMutation, useRpcClient } from "@/lib/EffectRuntime"; -import { withRpc } from "@/lib/Rpcs"; import { useDashboardContext } from "../../../Contexts"; export const OrganizationIcon = () => { diff --git a/apps/web/app/s/[videoId]/page.tsx b/apps/web/app/s/[videoId]/page.tsx index b9e06a99fb..8107277e59 100644 --- a/apps/web/app/s/[videoId]/page.tsx +++ b/apps/web/app/s/[videoId]/page.tsx @@ -29,7 +29,7 @@ import { Policy, type Video, } from "@cap/web-domain"; -import { eq, type InferSelectModel, sql } from "drizzle-orm"; +import { and, eq, type InferSelectModel, isNull, sql } from "drizzle-orm"; import { Effect, Option } from "effect"; import type { Metadata } from "next"; import { headers } from "next/headers"; @@ -301,7 +301,7 @@ export default async function ShareVideoPage(props: PageProps<"/s/[videoId]">) { .innerJoin(users, eq(videos.ownerId, users.id)) .leftJoin(videoUploads, eq(videos.id, videoUploads.videoId)) .leftJoin(organizations, eq(videos.orgId, organizations.id)) - .where(eq(videos.id, videoId)), + .where(and(eq(videos.id, videoId), isNull(organizations.tombstoneAt))), ).pipe(Policy.withPublicPolicy(videosPolicy.canView(videoId))); return Option.fromNullable(video); diff --git a/packages/database/migrations/meta/_journal.json b/packages/database/migrations/meta/_journal.json index 74667669f0..53a3adc364 100644 --- a/packages/database/migrations/meta/_journal.json +++ b/packages/database/migrations/meta/_journal.json @@ -1,48 +1,55 @@ { - "version": "5", - "dialect": "mysql", - "entries": [ - { - "idx": 0, - "version": "5", - "when": 1743020179593, - "tag": "0000_brown_sunfire", - "breakpoints": true - }, - { - "idx": 1, - "version": "5", - "when": 1749268354138, - "tag": "0001_white_young_avengers", - "breakpoints": true - }, - { - "idx": 2, - "version": "5", - "when": 1750935538683, - "tag": "0002_dusty_maginty", - "breakpoints": true - }, - { - "idx": 3, - "version": "5", - "when": 1761710697286, - "tag": "0003_thin_gressill", - "breakpoints": true - }, - { - "idx": 4, - "version": "5", - "when": 1761711378574, - "tag": "0004_video-org-id", - "breakpoints": true - }, - { - "idx": 5, - "version": "5", - "when": 1761711605408, - "tag": "0005_video-org-id-required", - "breakpoints": true - } - ] -} + "version": "5", + "dialect": "mysql", + "entries": [ + { + "idx": 0, + "version": "5", + "when": 1743020179593, + "tag": "0000_brown_sunfire", + "breakpoints": true + }, + { + "idx": 1, + "version": "5", + "when": 1749268354138, + "tag": "0001_white_young_avengers", + "breakpoints": true + }, + { + "idx": 2, + "version": "5", + "when": 1750935538683, + "tag": "0002_dusty_maginty", + "breakpoints": true + }, + { + "idx": 3, + "version": "5", + "when": 1761710697286, + "tag": "0003_thin_gressill", + "breakpoints": true + }, + { + "idx": 4, + "version": "5", + "when": 1761711378574, + "tag": "0004_video-org-id", + "breakpoints": true + }, + { + "idx": 5, + "version": "5", + "when": 1761711605408, + "tag": "0005_video-org-id-required", + "breakpoints": true + }, + { + "idx": 6, + "version": "5", + "when": 1762162057689, + "tag": "0006_past_carlie_cooper", + "breakpoints": true + } + ] +} \ No newline at end of file diff --git a/packages/database/schema.ts b/packages/database/schema.ts index 6a94501f2b..ab14efa62f 100644 --- a/packages/database/schema.ts +++ b/packages/database/schema.ts @@ -180,6 +180,7 @@ export const organizations = mysqlTable( name: varchar("name", { length: 255 }).notNull(), ownerId: nanoId("ownerId").notNull().$type(), metadata: json("metadata"), + tombstoneAt: timestamp("tombstoneAt"), allowedEmailDomain: varchar("allowedEmailDomain", { length: 255 }), customDomain: varchar("customDomain", { length: 255 }), domainVerified: timestamp("domainVerified"), diff --git a/packages/ui/src/components/Button.tsx b/packages/ui/src/components/Button.tsx index 3e64fc3c3b..bee04c830a 100644 --- a/packages/ui/src/components/Button.tsx +++ b/packages/ui/src/components/Button.tsx @@ -17,7 +17,7 @@ const buttonVariants = cva( "bg-gray-12 dark-button-shadow text-gray-1 disabled:bg-gray-6 disabled:text-gray-9", blue: "bg-blue-600 text-white disabled:border-gray-8 border border-blue-800 shadow-[0_1.50px_0_0_rgba(255,255,255,0.20)_inset] hover:bg-blue-700 disabled:bg-gray-7 disabled:text-gray-10", destructive: - "bg-red-500 text-white hover:bg-red-600 disabled:bg-red-200", + "bg-red-500 text-white border-transparent hover:bg-red-600 disabled:bg-gray-7 disabled:border-gray-8 border disabled:text-gray-10", outline: "border border-gray-4 hover:border-gray-5 hover:bg-gray-3 text-gray-12 disabled:bg-gray-8", white: diff --git a/packages/web-backend/src/Organisations/OrganisationsRpcs.ts b/packages/web-backend/src/Organisations/OrganisationsRpcs.ts index 330f74e641..a09d5e05f1 100644 --- a/packages/web-backend/src/Organisations/OrganisationsRpcs.ts +++ b/packages/web-backend/src/Organisations/OrganisationsRpcs.ts @@ -14,6 +14,15 @@ export const OrganisationsRpcsLive = Organisation.OrganisationRpcs.toLayer( S3Error: () => new InternalError({ type: "s3" }), }), ), + OrganisationDelete: (data) => + orgs + .deleteOrg(data.id) + .pipe( + Effect.catchTag( + "DatabaseError", + () => new InternalError({ type: "database" }), + ), + ), }; }), ); diff --git a/packages/web-backend/src/Organisations/index.ts b/packages/web-backend/src/Organisations/index.ts index 6022ec1a7b..5805182630 100644 --- a/packages/web-backend/src/Organisations/index.ts +++ b/packages/web-backend/src/Organisations/index.ts @@ -1,8 +1,7 @@ import * as Db from "@cap/database/schema"; -import { type ImageUpload, Organisation, Policy } from "@cap/web-domain"; +import { CurrentUser, Organisation, Policy } from "@cap/web-domain"; import * as Dz from "drizzle-orm"; import { Array, Effect, Option } from "effect"; - import { Database } from "../Database"; import { ImageUploads } from "../ImageUploads"; import { S3Buckets } from "../S3Buckets"; @@ -49,7 +48,47 @@ export class Organisations extends Effect.Service()( } }); - return { update }; + const deleteOrg = Effect.fn("Organisations.deleteOrg")(function* ( + id: Organisation.OrganisationId, + ) { + const user = yield* CurrentUser; + + //this is fake deleting for now + yield* db.use((db) => + db + .update(Db.organizations) + .set({ tombstoneAt: new Date() }) + .where(Dz.eq(Db.organizations.id, id)), + ); + + //set another org as active org + const [otherOrg] = yield* db.use((db) => + db + .select({ id: Db.organizations.id }) + .from(Db.organizations) + .where( + Dz.and( + Dz.ne(Db.organizations.id, id), + Dz.isNull(Db.organizations.tombstoneAt), + Dz.eq(Db.organizations.ownerId, user.id), + ), + ) + .orderBy(Dz.asc(Db.organizations.createdAt)) + .limit(1), + ); + if (otherOrg) { + yield* db.use((db) => + db + .update(Db.users) + .set({ + activeOrganizationId: otherOrg.id, + defaultOrgId: otherOrg.id, + }) + .where(Dz.eq(Db.users.id, user.id)), + ); + } + }); + return { update, deleteOrg }; }), dependencies: [ ImageUploads.Default, diff --git a/packages/web-domain/src/Organisation.ts b/packages/web-domain/src/Organisation.ts index 6818a69979..bc6dd0a185 100644 --- a/packages/web-domain/src/Organisation.ts +++ b/packages/web-domain/src/Organisation.ts @@ -23,6 +23,11 @@ export class Organisation extends Schema.Class("Organisation")({ name: Schema.String, }) {} +export const OrganisationDelete = Schema.Struct({ + id: OrganisationId, +}); +export type OrganisationDelete = Schema.Schema.Type; + export const OrganisationUpdate = Schema.Struct({ id: OrganisationId, image: Schema.optional(ImageUpdatePayload), @@ -34,4 +39,8 @@ export class OrganisationRpcs extends RpcGroup.make( payload: OrganisationUpdate, error: Schema.Union(InternalError, PolicyDeniedError, NotFoundError), }).middleware(RpcAuthMiddleware), + Rpc.make("OrganisationDelete", { + payload: OrganisationDelete, + error: Schema.Union(InternalError, PolicyDeniedError, NotFoundError), + }).middleware(RpcAuthMiddleware), ) {} From 00145f98a64dfc4c61c320721ee33ad2e95df7fd Mon Sep 17 00:00:00 2001 From: ameer2468 <33054370+ameer2468@users.noreply.github.com> Date: Mon, 3 Nov 2025 14:36:24 +0300 Subject: [PATCH 2/6] Update _journal.json --- .../database/migrations/meta/_journal.json | 108 +++++++++--------- 1 file changed, 54 insertions(+), 54 deletions(-) diff --git a/packages/database/migrations/meta/_journal.json b/packages/database/migrations/meta/_journal.json index 53a3adc364..223bf8feda 100644 --- a/packages/database/migrations/meta/_journal.json +++ b/packages/database/migrations/meta/_journal.json @@ -1,55 +1,55 @@ { - "version": "5", - "dialect": "mysql", - "entries": [ - { - "idx": 0, - "version": "5", - "when": 1743020179593, - "tag": "0000_brown_sunfire", - "breakpoints": true - }, - { - "idx": 1, - "version": "5", - "when": 1749268354138, - "tag": "0001_white_young_avengers", - "breakpoints": true - }, - { - "idx": 2, - "version": "5", - "when": 1750935538683, - "tag": "0002_dusty_maginty", - "breakpoints": true - }, - { - "idx": 3, - "version": "5", - "when": 1761710697286, - "tag": "0003_thin_gressill", - "breakpoints": true - }, - { - "idx": 4, - "version": "5", - "when": 1761711378574, - "tag": "0004_video-org-id", - "breakpoints": true - }, - { - "idx": 5, - "version": "5", - "when": 1761711605408, - "tag": "0005_video-org-id-required", - "breakpoints": true - }, - { - "idx": 6, - "version": "5", - "when": 1762162057689, - "tag": "0006_past_carlie_cooper", - "breakpoints": true - } - ] -} \ No newline at end of file + "version": "5", + "dialect": "mysql", + "entries": [ + { + "idx": 0, + "version": "5", + "when": 1743020179593, + "tag": "0000_brown_sunfire", + "breakpoints": true + }, + { + "idx": 1, + "version": "5", + "when": 1749268354138, + "tag": "0001_white_young_avengers", + "breakpoints": true + }, + { + "idx": 2, + "version": "5", + "when": 1750935538683, + "tag": "0002_dusty_maginty", + "breakpoints": true + }, + { + "idx": 3, + "version": "5", + "when": 1761710697286, + "tag": "0003_thin_gressill", + "breakpoints": true + }, + { + "idx": 4, + "version": "5", + "when": 1761711378574, + "tag": "0004_video-org-id", + "breakpoints": true + }, + { + "idx": 5, + "version": "5", + "when": 1761711605408, + "tag": "0005_video-org-id-required", + "breakpoints": true + }, + { + "idx": 6, + "version": "5", + "when": 1762162057689, + "tag": "0006_past_carlie_cooper", + "breakpoints": true + } + ] +} From c2edd0b55e366aa1c535e81768e36d29e732b0ac Mon Sep 17 00:00:00 2001 From: ameer2468 <33054370+ameer2468@users.noreply.github.com> Date: Mon, 3 Nov 2025 14:48:25 +0300 Subject: [PATCH 3/6] update api for desktop --- .../organization/components/DeleteOrgDialog.tsx | 8 +++++--- apps/web/app/api/desktop/[...route]/root.ts | 11 +++++++---- packages/web-backend/src/Organisations/index.ts | 2 ++ 3 files changed, 14 insertions(+), 7 deletions(-) diff --git a/apps/web/app/(org)/dashboard/settings/organization/components/DeleteOrgDialog.tsx b/apps/web/app/(org)/dashboard/settings/organization/components/DeleteOrgDialog.tsx index 0e51779a49..2c287bf361 100644 --- a/apps/web/app/(org)/dashboard/settings/organization/components/DeleteOrgDialog.tsx +++ b/apps/web/app/(org)/dashboard/settings/organization/components/DeleteOrgDialog.tsx @@ -23,14 +23,16 @@ interface DeleteOrgDialogProps { } const DeleteOrgDialog = ({ open, onOpenChange }: DeleteOrgDialogProps) => { - const { activeOrganization, organizationData, user } = useDashboardContext(); + const { activeOrganization, organizationData } = useDashboardContext(); const [organizationName, setOrganizationName] = useState(""); const rpc = useRpcClient(); + const inputId = useId(); const router = useRouter(); const deleteOrg = useEffectMutation({ mutationFn: Effect.fn(function* () { + if (!activeOrganization) return; yield* rpc.OrganisationDelete({ - id: activeOrganization?.organization.id as Organisation.OrganisationId, + id: activeOrganization.organization.id, }); }), onSuccess: () => { @@ -58,7 +60,7 @@ const DeleteOrgDialog = ({ open, onOpenChange }: DeleteOrgDialogProps) => {
setOrganizationName(e.target.value)} placeholder="Organization name" diff --git a/apps/web/app/api/desktop/[...route]/root.ts b/apps/web/app/api/desktop/[...route]/root.ts index 6d697d9c64..18294c8417 100644 --- a/apps/web/app/api/desktop/[...route]/root.ts +++ b/apps/web/app/api/desktop/[...route]/root.ts @@ -8,7 +8,7 @@ import { import { buildEnv, serverEnv } from "@cap/env"; import { stripe, userIsPro } from "@cap/utils"; import { zValidator } from "@hono/zod-validator"; -import { eq, or } from "drizzle-orm"; +import { and, eq, isNull, or } from "drizzle-orm"; import { Hono } from "hono"; import { PostHog } from "posthog-node"; import type Stripe from "stripe"; @@ -212,9 +212,12 @@ app.get("/organizations", withAuth, async (c) => { eq(organizations.id, organizationMembers.organizationId), ) .where( - or( - eq(organizations.ownerId, user.id), - eq(organizationMembers.userId, user.id), + and( + isNull(organizations.tombstoneAt), + or( + eq(organizations.ownerId, user.id), + eq(organizationMembers.userId, user.id), + ), ), ) .groupBy(organizations.id); diff --git a/packages/web-backend/src/Organisations/index.ts b/packages/web-backend/src/Organisations/index.ts index 5805182630..f85a738acc 100644 --- a/packages/web-backend/src/Organisations/index.ts +++ b/packages/web-backend/src/Organisations/index.ts @@ -53,6 +53,8 @@ export class Organisations extends Effect.Service()( ) { const user = yield* CurrentUser; + yield* Policy.withPolicy(policy.isOwner(id))(Effect.void); + //this is fake deleting for now yield* db.use((db) => db From aba39b15ea532a041ab3eeec517d3d2ae3983223 Mon Sep 17 00:00:00 2001 From: ameer2468 <33054370+ameer2468@users.noreply.github.com> Date: Mon, 3 Nov 2025 14:55:32 +0300 Subject: [PATCH 4/6] cleanup effect --- .../src/Organisations/OrganisationsRpcs.ts | 13 ++++------- .../web-backend/src/Organisations/index.ts | 23 ++++++++++++------- 2 files changed, 20 insertions(+), 16 deletions(-) diff --git a/packages/web-backend/src/Organisations/OrganisationsRpcs.ts b/packages/web-backend/src/Organisations/OrganisationsRpcs.ts index a09d5e05f1..611a60a883 100644 --- a/packages/web-backend/src/Organisations/OrganisationsRpcs.ts +++ b/packages/web-backend/src/Organisations/OrganisationsRpcs.ts @@ -15,14 +15,11 @@ export const OrganisationsRpcsLive = Organisation.OrganisationRpcs.toLayer( }), ), OrganisationDelete: (data) => - orgs - .deleteOrg(data.id) - .pipe( - Effect.catchTag( - "DatabaseError", - () => new InternalError({ type: "database" }), - ), - ), + orgs.deleteOrg(data.id).pipe( + Effect.catchTags({ + DatabaseError: () => new InternalError({ type: "database" }), + }), + ), }; }), ); diff --git a/packages/web-backend/src/Organisations/index.ts b/packages/web-backend/src/Organisations/index.ts index f85a738acc..97d994bae1 100644 --- a/packages/web-backend/src/Organisations/index.ts +++ b/packages/web-backend/src/Organisations/index.ts @@ -53,15 +53,22 @@ export class Organisations extends Effect.Service()( ) { const user = yield* CurrentUser; - yield* Policy.withPolicy(policy.isOwner(id))(Effect.void); - //this is fake deleting for now - yield* db.use((db) => - db - .update(Db.organizations) - .set({ tombstoneAt: new Date() }) - .where(Dz.eq(Db.organizations.id, id)), - ); + yield* db + .use((db) => + db + .update(Db.organizations) + .set({ tombstoneAt: new Date() }) + .where(Dz.eq(Db.organizations.id, id)), + ) + .pipe( + Effect.flatMap(Array.get(0)), + Effect.catchTag( + "NoSuchElementException", + () => new Organisation.NotFoundError(), + ), + Policy.withPolicy(policy.isOwner(id)), + ); //set another org as active org const [otherOrg] = yield* db.use((db) => From e79cd6c2f5c4ff5f4f8dba920364c1790fad293a Mon Sep 17 00:00:00 2001 From: ameer2468 <33054370+ameer2468@users.noreply.github.com> Date: Mon, 3 Nov 2025 14:59:28 +0300 Subject: [PATCH 5/6] Update _journal.json --- packages/database/migrations/meta/_journal.json | 7 ------- 1 file changed, 7 deletions(-) diff --git a/packages/database/migrations/meta/_journal.json b/packages/database/migrations/meta/_journal.json index 223bf8feda..74667669f0 100644 --- a/packages/database/migrations/meta/_journal.json +++ b/packages/database/migrations/meta/_journal.json @@ -43,13 +43,6 @@ "when": 1761711605408, "tag": "0005_video-org-id-required", "breakpoints": true - }, - { - "idx": 6, - "version": "5", - "when": 1762162057689, - "tag": "0006_past_carlie_cooper", - "breakpoints": true } ] } From daa99c11aab148f73a5ad7de9aad30507d204464 Mon Sep 17 00:00:00 2001 From: ameer2468 <33054370+ameer2468@users.noreply.github.com> Date: Mon, 3 Nov 2025 15:14:43 +0300 Subject: [PATCH 6/6] wrap in a transaction --- .../web-backend/src/Organisations/index.ts | 70 ++++++++----------- 1 file changed, 31 insertions(+), 39 deletions(-) diff --git a/packages/web-backend/src/Organisations/index.ts b/packages/web-backend/src/Organisations/index.ts index 97d994bae1..fb7490e484 100644 --- a/packages/web-backend/src/Organisations/index.ts +++ b/packages/web-backend/src/Organisations/index.ts @@ -53,49 +53,41 @@ export class Organisations extends Effect.Service()( ) { const user = yield* CurrentUser; - //this is fake deleting for now - yield* db - .use((db) => - db + yield* Policy.withPolicy(policy.isOwner(id))(Effect.void); + + // Perform tombstone, find other org, and update user in a single transaction + yield* db.use((db) => + db.transaction(async (tx) => { + await tx .update(Db.organizations) .set({ tombstoneAt: new Date() }) - .where(Dz.eq(Db.organizations.id, id)), - ) - .pipe( - Effect.flatMap(Array.get(0)), - Effect.catchTag( - "NoSuchElementException", - () => new Organisation.NotFoundError(), - ), - Policy.withPolicy(policy.isOwner(id)), - ); + .where(Dz.eq(Db.organizations.id, id)); + + // Find another active organization owned by the user + const [otherOrg] = await tx + .select({ id: Db.organizations.id }) + .from(Db.organizations) + .where( + Dz.and( + Dz.ne(Db.organizations.id, id), + Dz.isNull(Db.organizations.tombstoneAt), + Dz.eq(Db.organizations.ownerId, user.id), + ), + ) + .orderBy(Dz.asc(Db.organizations.createdAt)) + .limit(1); - //set another org as active org - const [otherOrg] = yield* db.use((db) => - db - .select({ id: Db.organizations.id }) - .from(Db.organizations) - .where( - Dz.and( - Dz.ne(Db.organizations.id, id), - Dz.isNull(Db.organizations.tombstoneAt), - Dz.eq(Db.organizations.ownerId, user.id), - ), - ) - .orderBy(Dz.asc(Db.organizations.createdAt)) - .limit(1), + if (otherOrg) { + await tx + .update(Db.users) + .set({ + activeOrganizationId: otherOrg.id, + defaultOrgId: otherOrg.id, + }) + .where(Dz.eq(Db.users.id, user.id)); + } + }), ); - if (otherOrg) { - yield* db.use((db) => - db - .update(Db.users) - .set({ - activeOrganizationId: otherOrg.id, - defaultOrgId: otherOrg.id, - }) - .where(Dz.eq(Db.users.id, user.id)), - ); - } }); return { update, deleteOrg }; }),