mirror of
https://github.com/colanode/colanode.git
synced 2025-12-16 19:57:46 +01:00
Implement workspace delete (#21)
This commit is contained in:
@@ -65,6 +65,7 @@ import { AccountMetadataSaveMutationHandler } from '@/main/mutations/accounts/ac
|
|||||||
import { AccountMetadataDeleteMutationHandler } from '@/main/mutations/accounts/account-metadata-delete';
|
import { AccountMetadataDeleteMutationHandler } from '@/main/mutations/accounts/account-metadata-delete';
|
||||||
import { EmailPasswordResetInitMutationHandler } from '@/main/mutations/accounts/email-password-reset-init';
|
import { EmailPasswordResetInitMutationHandler } from '@/main/mutations/accounts/email-password-reset-init';
|
||||||
import { EmailPasswordResetCompleteMutationHandler } from '@/main/mutations/accounts/email-password-reset-complete';
|
import { EmailPasswordResetCompleteMutationHandler } from '@/main/mutations/accounts/email-password-reset-complete';
|
||||||
|
import { WorkspaceDeleteMutationHandler } from '@/main/mutations/workspaces/workspace-delete';
|
||||||
import { MutationHandler } from '@/main/lib/types';
|
import { MutationHandler } from '@/main/lib/types';
|
||||||
import { MutationMap } from '@/shared/mutations';
|
import { MutationMap } from '@/shared/mutations';
|
||||||
|
|
||||||
@@ -141,4 +142,5 @@ export const mutationHandlerMap: MutationHandlerMap = {
|
|||||||
email_password_reset_init: new EmailPasswordResetInitMutationHandler(),
|
email_password_reset_init: new EmailPasswordResetInitMutationHandler(),
|
||||||
email_password_reset_complete:
|
email_password_reset_complete:
|
||||||
new EmailPasswordResetCompleteMutationHandler(),
|
new EmailPasswordResetCompleteMutationHandler(),
|
||||||
|
workspace_delete: new WorkspaceDeleteMutationHandler(),
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,50 @@
|
|||||||
|
import { WorkspaceOutput } from '@colanode/core';
|
||||||
|
|
||||||
|
import { appService } from '@/main/services/app-service';
|
||||||
|
import { MutationHandler } from '@/main/lib/types';
|
||||||
|
import { parseApiError } from '@/shared/lib/axios';
|
||||||
|
import { MutationError, MutationErrorCode } from '@/shared/mutations';
|
||||||
|
import {
|
||||||
|
WorkspaceDeleteMutationInput,
|
||||||
|
WorkspaceDeleteMutationOutput,
|
||||||
|
} from '@/shared/mutations/workspaces/workspace-delete';
|
||||||
|
|
||||||
|
export class WorkspaceDeleteMutationHandler
|
||||||
|
implements MutationHandler<WorkspaceDeleteMutationInput>
|
||||||
|
{
|
||||||
|
async handleMutation(
|
||||||
|
input: WorkspaceDeleteMutationInput
|
||||||
|
): Promise<WorkspaceDeleteMutationOutput> {
|
||||||
|
const accountService = appService.getAccount(input.accountId);
|
||||||
|
|
||||||
|
if (!accountService) {
|
||||||
|
throw new MutationError(
|
||||||
|
MutationErrorCode.AccountNotFound,
|
||||||
|
'Account not found or has been logged out.'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const workspaceService = accountService.getWorkspace(input.workspaceId);
|
||||||
|
if (!workspaceService) {
|
||||||
|
throw new MutationError(
|
||||||
|
MutationErrorCode.WorkspaceNotFound,
|
||||||
|
'Workspace not found.'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { data } = await accountService.client.delete<WorkspaceOutput>(
|
||||||
|
`/v1/workspaces/${input.workspaceId}`
|
||||||
|
);
|
||||||
|
|
||||||
|
await accountService.deleteWorkspace(data.id);
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: data.id,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
const apiError = parseApiError(error);
|
||||||
|
throw new MutationError(MutationErrorCode.ApiError, apiError.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -22,7 +22,14 @@ export class RadarDataGetQueryHandler
|
|||||||
_: RadarDataGetQueryInput,
|
_: RadarDataGetQueryInput,
|
||||||
___: RadarDataGetQueryOutput
|
___: RadarDataGetQueryOutput
|
||||||
): Promise<ChangeCheckResult<RadarDataGetQueryInput>> {
|
): Promise<ChangeCheckResult<RadarDataGetQueryInput>> {
|
||||||
if (event.type === 'radar_data_updated') {
|
const shouldUpdate =
|
||||||
|
event.type === 'radar_data_updated' ||
|
||||||
|
event.type === 'workspace_created' ||
|
||||||
|
event.type === 'workspace_deleted' ||
|
||||||
|
event.type === 'account_created' ||
|
||||||
|
event.type === 'account_deleted';
|
||||||
|
|
||||||
|
if (shouldUpdate) {
|
||||||
const data = this.getRadarData();
|
const data = this.getRadarData();
|
||||||
return {
|
return {
|
||||||
hasChanges: true,
|
hasChanges: true,
|
||||||
|
|||||||
@@ -47,6 +47,10 @@ export class ServerService {
|
|||||||
return this.server.domain;
|
return this.server.domain;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public get version() {
|
||||||
|
return this.server.version;
|
||||||
|
}
|
||||||
|
|
||||||
private async sync() {
|
private async sync() {
|
||||||
const config = await ServerService.fetchServerConfig(this.server.domain);
|
const config = await ServerService.fetchServerConfig(this.server.domain);
|
||||||
const existingState = this.state;
|
const existingState = this.state;
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { Account as AccountType } from '@/shared/types/accounts';
|
|||||||
import { useQuery } from '@/renderer/hooks/use-query';
|
import { useQuery } from '@/renderer/hooks/use-query';
|
||||||
import { WorkspaceCreate } from '@/renderer/components/workspaces/workspace-create';
|
import { WorkspaceCreate } from '@/renderer/components/workspaces/workspace-create';
|
||||||
import { Workspace } from '@/renderer/components/workspaces/workspace';
|
import { Workspace } from '@/renderer/components/workspaces/workspace';
|
||||||
|
import { ServerProvider } from '@/renderer/components/servers/server-provider';
|
||||||
|
|
||||||
interface AccountProps {
|
interface AccountProps {
|
||||||
account: AccountType;
|
account: AccountType;
|
||||||
@@ -56,45 +57,47 @@ export const Account = ({ account }: AccountProps) => {
|
|||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AccountContext.Provider
|
<ServerProvider domain={account.server}>
|
||||||
value={{
|
<AccountContext.Provider
|
||||||
...account,
|
value={{
|
||||||
openSettings: () => setOpenSettings(true),
|
...account,
|
||||||
openLogout: () => setOpenLogout(true),
|
openSettings: () => setOpenSettings(true),
|
||||||
openWorkspaceCreate: () => setOpenCreateWorkspace(true),
|
openLogout: () => setOpenLogout(true),
|
||||||
openWorkspace: (id) => {
|
openWorkspaceCreate: () => setOpenCreateWorkspace(true),
|
||||||
setOpenCreateWorkspace(false);
|
openWorkspace: (id) => {
|
||||||
window.colanode.executeMutation({
|
setOpenCreateWorkspace(false);
|
||||||
type: 'account_metadata_save',
|
window.colanode.executeMutation({
|
||||||
accountId: account.id,
|
type: 'account_metadata_save',
|
||||||
key: 'workspace',
|
accountId: account.id,
|
||||||
value: id,
|
key: 'workspace',
|
||||||
});
|
value: id,
|
||||||
},
|
});
|
||||||
}}
|
},
|
||||||
>
|
}}
|
||||||
{!openCreateWorkspace && workspace ? (
|
>
|
||||||
<Workspace workspace={workspace} />
|
{!openCreateWorkspace && workspace ? (
|
||||||
) : (
|
<Workspace workspace={workspace} />
|
||||||
<WorkspaceCreate
|
) : (
|
||||||
onSuccess={handleWorkspaceCreateSuccess}
|
<WorkspaceCreate
|
||||||
onCancel={handleWorkspaceCreateCancel}
|
onSuccess={handleWorkspaceCreateSuccess}
|
||||||
/>
|
onCancel={handleWorkspaceCreateCancel}
|
||||||
)}
|
/>
|
||||||
{openSettings && (
|
)}
|
||||||
<AccountSettingsDialog
|
{openSettings && (
|
||||||
open={true}
|
<AccountSettingsDialog
|
||||||
onOpenChange={() => setOpenSettings(false)}
|
open={true}
|
||||||
/>
|
onOpenChange={() => setOpenSettings(false)}
|
||||||
)}
|
/>
|
||||||
{openLogout && (
|
)}
|
||||||
<AccountLogout
|
{openLogout && (
|
||||||
onCancel={() => setOpenLogout(false)}
|
<AccountLogout
|
||||||
onLogout={() => {
|
onCancel={() => setOpenLogout(false)}
|
||||||
setOpenLogout(false);
|
onLogout={() => {
|
||||||
}}
|
setOpenLogout(false);
|
||||||
/>
|
}}
|
||||||
)}
|
/>
|
||||||
</AccountContext.Provider>
|
)}
|
||||||
|
</AccountContext.Provider>
|
||||||
|
</ServerProvider>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
import { BadgeAlert } from 'lucide-react';
|
||||||
|
|
||||||
|
interface ServerNotFoundProps {
|
||||||
|
domain: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ServerNotFound = ({ domain }: ServerNotFoundProps) => {
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col items-center justify-center h-full p-6 text-center">
|
||||||
|
<BadgeAlert className="size-12 mb-4" />
|
||||||
|
<h1 className="text-2xl font-semibold tracking-tight">
|
||||||
|
Server not found
|
||||||
|
</h1>
|
||||||
|
<p className="mt-2 text-sm font-medium text-muted-foreground">
|
||||||
|
The server {domain} does not exist. It may have been deleted from your
|
||||||
|
app or the data has been lost.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
import { ServerContext } from '@/renderer/contexts/server';
|
||||||
|
import { useQuery } from '@/renderer/hooks/use-query';
|
||||||
|
import { ServerNotFound } from '@/renderer/components/servers/server-not-found';
|
||||||
|
import { isFeatureSupported } from '@/shared/lib/features';
|
||||||
|
|
||||||
|
interface ServerProviderProps {
|
||||||
|
domain: string;
|
||||||
|
children: React.ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ServerProvider = ({ domain, children }: ServerProviderProps) => {
|
||||||
|
const { data, isPending } = useQuery({
|
||||||
|
type: 'server_list',
|
||||||
|
});
|
||||||
|
|
||||||
|
const server = data?.find((server) => server.domain === domain);
|
||||||
|
|
||||||
|
if (isPending) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!server) {
|
||||||
|
return <ServerNotFound domain={domain} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ServerContext.Provider
|
||||||
|
value={{
|
||||||
|
...server,
|
||||||
|
supports: (feature) => {
|
||||||
|
return isFeatureSupported(feature, server.version);
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</ServerContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,115 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import { Button } from '@/renderer/components/ui/button';
|
||||||
|
import { useWorkspace } from '@/renderer/contexts/workspace';
|
||||||
|
import { useMutation } from '@/renderer/hooks/use-mutation';
|
||||||
|
import { toast } from '@/renderer/hooks/use-toast';
|
||||||
|
import {
|
||||||
|
AlertDialog,
|
||||||
|
AlertDialogCancel,
|
||||||
|
AlertDialogContent,
|
||||||
|
AlertDialogDescription,
|
||||||
|
AlertDialogFooter,
|
||||||
|
AlertDialogHeader,
|
||||||
|
AlertDialogTitle,
|
||||||
|
} from '@/renderer/components/ui/alert-dialog';
|
||||||
|
import { Spinner } from '@/renderer/components/ui/spinner';
|
||||||
|
import { useServer } from '@/renderer/contexts/server';
|
||||||
|
|
||||||
|
interface WorkspaceDeleteProps {
|
||||||
|
onDeleted: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const WorkspaceDelete = ({ onDeleted }: WorkspaceDeleteProps) => {
|
||||||
|
const server = useServer();
|
||||||
|
const workspace = useWorkspace();
|
||||||
|
const { mutate, isPending } = useMutation();
|
||||||
|
|
||||||
|
const [showDeleteModal, setShowDeleteModal] = React.useState(false);
|
||||||
|
const isDeleteSupported = server.supports('workspace-delete');
|
||||||
|
|
||||||
|
if (!isDeleteSupported) {
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col gap-4">
|
||||||
|
<h3 className="font-heading mb-px text-2xl font-semibold tracking-tight">
|
||||||
|
Delete workspace
|
||||||
|
</h3>
|
||||||
|
<p>
|
||||||
|
This feature is not supported on the server this workspace is hosted
|
||||||
|
on. Please contact your administrator to upgrade the server.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col gap-4">
|
||||||
|
<h3 className="font-heading mb-px text-2xl font-semibold tracking-tight">
|
||||||
|
Delete workspace
|
||||||
|
</h3>
|
||||||
|
<p>Deleting a workspace is permanent and cannot be undone.</p>
|
||||||
|
<p>
|
||||||
|
All data associated with the workspace will be deleted, including users,
|
||||||
|
chats, messages, pages, channels, databases, records, files and more.
|
||||||
|
</p>
|
||||||
|
<div>
|
||||||
|
<Button
|
||||||
|
variant="destructive"
|
||||||
|
onClick={() => {
|
||||||
|
setShowDeleteModal(true);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Delete
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<AlertDialog open={showDeleteModal} onOpenChange={setShowDeleteModal}>
|
||||||
|
<AlertDialogContent>
|
||||||
|
<AlertDialogHeader>
|
||||||
|
<AlertDialogTitle>
|
||||||
|
Are you sure you want delete this workspace?
|
||||||
|
</AlertDialogTitle>
|
||||||
|
<AlertDialogDescription>
|
||||||
|
This action cannot be undone. This workspace will no longer be
|
||||||
|
accessible by you or other users that are part of it.
|
||||||
|
</AlertDialogDescription>
|
||||||
|
</AlertDialogHeader>
|
||||||
|
<AlertDialogFooter>
|
||||||
|
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||||
|
<Button
|
||||||
|
variant="destructive"
|
||||||
|
disabled={isPending}
|
||||||
|
onClick={() => {
|
||||||
|
mutate({
|
||||||
|
input: {
|
||||||
|
type: 'workspace_delete',
|
||||||
|
accountId: workspace.accountId,
|
||||||
|
workspaceId: workspace.id,
|
||||||
|
},
|
||||||
|
onSuccess() {
|
||||||
|
setShowDeleteModal(false);
|
||||||
|
onDeleted();
|
||||||
|
toast({
|
||||||
|
title: 'Workspace deleted',
|
||||||
|
description: 'Workspace was deleted successfully',
|
||||||
|
variant: 'default',
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onError(error) {
|
||||||
|
toast({
|
||||||
|
title: 'Failed to delete workspace',
|
||||||
|
description: error.message,
|
||||||
|
variant: 'destructive',
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{isPending && <Spinner className="mr-1" />}
|
||||||
|
Delete
|
||||||
|
</Button>
|
||||||
|
</AlertDialogFooter>
|
||||||
|
</AlertDialogContent>
|
||||||
|
</AlertDialog>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -23,6 +23,7 @@ import {
|
|||||||
import { WorkspaceUpdate } from '@/renderer/components/workspaces/workspace-update';
|
import { WorkspaceUpdate } from '@/renderer/components/workspaces/workspace-update';
|
||||||
import { WorkspaceUsers } from '@/renderer/components/workspaces/workspace-users';
|
import { WorkspaceUsers } from '@/renderer/components/workspaces/workspace-users';
|
||||||
import { useWorkspace } from '@/renderer/contexts/workspace';
|
import { useWorkspace } from '@/renderer/contexts/workspace';
|
||||||
|
import { WorkspaceDelete } from '@/renderer/components/workspaces/workspace-delete';
|
||||||
|
|
||||||
interface WorkspaceSettingsDialogProps {
|
interface WorkspaceSettingsDialogProps {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
@@ -98,6 +99,7 @@ export const WorkspaceSettingsDialog = ({
|
|||||||
<SidebarMenuButton
|
<SidebarMenuButton
|
||||||
isActive={tab === 'delete'}
|
isActive={tab === 'delete'}
|
||||||
onClick={() => setTab('delete')}
|
onClick={() => setTab('delete')}
|
||||||
|
disabled={workspace.role !== 'owner'}
|
||||||
>
|
>
|
||||||
<Trash2 className="mr-2 size-4" />
|
<Trash2 className="mr-2 size-4" />
|
||||||
<span>Delete</span>
|
<span>Delete</span>
|
||||||
@@ -113,7 +115,9 @@ export const WorkspaceSettingsDialog = ({
|
|||||||
{match(tab)
|
{match(tab)
|
||||||
.with('info', () => <WorkspaceUpdate />)
|
.with('info', () => <WorkspaceUpdate />)
|
||||||
.with('users', () => <WorkspaceUsers />)
|
.with('users', () => <WorkspaceUsers />)
|
||||||
.with('delete', () => <p>Coming soon.</p>)
|
.with('delete', () => (
|
||||||
|
<WorkspaceDelete onDeleted={() => onOpenChange(false)} />
|
||||||
|
))
|
||||||
.exhaustive()}
|
.exhaustive()}
|
||||||
</div>
|
</div>
|
||||||
</SidebarProvider>
|
</SidebarProvider>
|
||||||
|
|||||||
12
apps/desktop/src/renderer/contexts/server.ts
Normal file
12
apps/desktop/src/renderer/contexts/server.ts
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import { createContext, useContext } from 'react';
|
||||||
|
|
||||||
|
import { FeatureKey } from '@/shared/lib/features';
|
||||||
|
import { Server } from '@/shared/types/servers';
|
||||||
|
|
||||||
|
interface ServerContext extends Server {
|
||||||
|
supports(feature: FeatureKey): boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ServerContext = createContext<ServerContext>({} as ServerContext);
|
||||||
|
|
||||||
|
export const useServer = () => useContext(ServerContext);
|
||||||
28
apps/desktop/src/shared/lib/features.ts
Normal file
28
apps/desktop/src/shared/lib/features.ts
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
import semver from 'semver';
|
||||||
|
|
||||||
|
export const FeatureVersions = {
|
||||||
|
'workspace-delete': semver.parse('0.1.5'),
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export type FeatureKey = keyof typeof FeatureVersions;
|
||||||
|
|
||||||
|
export const isFeatureSupported = (
|
||||||
|
feature: FeatureKey,
|
||||||
|
version: string
|
||||||
|
): boolean => {
|
||||||
|
if (version === 'dev') {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const parsedVersion = semver.parse(version);
|
||||||
|
if (!parsedVersion) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const featureVersion = FeatureVersions[feature];
|
||||||
|
if (!featureVersion) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return semver.gte(featureVersion, parsedVersion);
|
||||||
|
};
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
export type WorkspaceDeleteMutationInput = {
|
||||||
|
type: 'workspace_delete';
|
||||||
|
accountId: string;
|
||||||
|
workspaceId: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type WorkspaceDeleteMutationOutput = {
|
||||||
|
id: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
declare module '@/shared/mutations' {
|
||||||
|
interface MutationMap {
|
||||||
|
workspace_delete: {
|
||||||
|
input: WorkspaceDeleteMutationInput;
|
||||||
|
output: WorkspaceDeleteMutationOutput;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,9 +1,14 @@
|
|||||||
import { FastifyPluginCallbackZod } from 'fastify-type-provider-zod';
|
import { FastifyPluginCallbackZod } from 'fastify-type-provider-zod';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
import { ApiErrorCode, apiErrorOutputSchema } from '@colanode/core';
|
import {
|
||||||
|
ApiErrorCode,
|
||||||
|
apiErrorOutputSchema,
|
||||||
|
workspaceOutputSchema,
|
||||||
|
} from '@colanode/core';
|
||||||
|
|
||||||
import { database } from '@/data/database';
|
import { database } from '@/data/database';
|
||||||
import { eventBus } from '@/lib/event-bus';
|
import { eventBus } from '@/lib/event-bus';
|
||||||
|
import { jobService } from '@/services/job-service';
|
||||||
|
|
||||||
export const workspaceDeleteRoute: FastifyPluginCallbackZod = (
|
export const workspaceDeleteRoute: FastifyPluginCallbackZod = (
|
||||||
instance,
|
instance,
|
||||||
@@ -12,13 +17,13 @@ export const workspaceDeleteRoute: FastifyPluginCallbackZod = (
|
|||||||
) => {
|
) => {
|
||||||
instance.route({
|
instance.route({
|
||||||
method: 'DELETE',
|
method: 'DELETE',
|
||||||
url: '/:workspaceId',
|
url: '/',
|
||||||
schema: {
|
schema: {
|
||||||
params: z.object({
|
params: z.object({
|
||||||
workspaceId: z.string(),
|
workspaceId: z.string(),
|
||||||
}),
|
}),
|
||||||
response: {
|
response: {
|
||||||
200: z.object({ id: z.string() }),
|
200: workspaceOutputSchema,
|
||||||
400: apiErrorOutputSchema,
|
400: apiErrorOutputSchema,
|
||||||
403: apiErrorOutputSchema,
|
403: apiErrorOutputSchema,
|
||||||
404: apiErrorOutputSchema,
|
404: apiErrorOutputSchema,
|
||||||
@@ -35,10 +40,34 @@ export const workspaceDeleteRoute: FastifyPluginCallbackZod = (
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
await database
|
const workspace = await database
|
||||||
.deleteFrom('workspaces')
|
.deleteFrom('workspaces')
|
||||||
|
.returningAll()
|
||||||
.where('id', '=', workspaceId)
|
.where('id', '=', workspaceId)
|
||||||
.execute();
|
.executeTakeFirst();
|
||||||
|
|
||||||
|
await jobService.addJob(
|
||||||
|
{
|
||||||
|
type: 'clean_workspace_data',
|
||||||
|
workspaceId: workspaceId,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
jobId: `clean_workspace_data_${workspaceId}`,
|
||||||
|
attempts: 5,
|
||||||
|
backoff: {
|
||||||
|
type: 'exponential',
|
||||||
|
delay: 1000,
|
||||||
|
},
|
||||||
|
delay: 1000,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!workspace) {
|
||||||
|
return reply.code(404).send({
|
||||||
|
code: ApiErrorCode.WorkspaceNotFound,
|
||||||
|
message: 'Workspace not found.',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
eventBus.publish({
|
eventBus.publish({
|
||||||
type: 'workspace_deleted',
|
type: 'workspace_deleted',
|
||||||
@@ -46,7 +75,17 @@ export const workspaceDeleteRoute: FastifyPluginCallbackZod = (
|
|||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: workspaceId,
|
id: workspace.id,
|
||||||
|
name: workspace.name,
|
||||||
|
description: workspace.description,
|
||||||
|
avatar: workspace.avatar,
|
||||||
|
user: {
|
||||||
|
id: request.user.id,
|
||||||
|
accountId: request.user.account_id,
|
||||||
|
role: request.user.role,
|
||||||
|
storageLimit: request.user.storage_limit,
|
||||||
|
maxFileSize: request.user.max_file_size,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ export const workspaceGetRoute: FastifyPluginCallbackZod = (
|
|||||||
) => {
|
) => {
|
||||||
instance.route({
|
instance.route({
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
url: '/:workspaceId',
|
url: '/',
|
||||||
schema: {
|
schema: {
|
||||||
params: z.object({
|
params: z.object({
|
||||||
workspaceId: z.string(),
|
workspaceId: z.string(),
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
import { JobHandler } from '@/types/jobs';
|
import { JobHandler } from '@/types/jobs';
|
||||||
|
import { database } from '@/data/database';
|
||||||
|
import { deleteFile } from '@/lib/files';
|
||||||
|
|
||||||
export type CleanWorkspaceDataInput = {
|
export type CleanWorkspaceDataInput = {
|
||||||
type: 'clean_workspace_data';
|
type: 'clean_workspace_data';
|
||||||
@@ -13,8 +15,137 @@ declare module '@/types/jobs' {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const BATCH_SIZE = 500;
|
||||||
|
|
||||||
export const cleanWorkspaceDataHandler: JobHandler<
|
export const cleanWorkspaceDataHandler: JobHandler<
|
||||||
CleanWorkspaceDataInput
|
CleanWorkspaceDataInput
|
||||||
> = async (input) => {
|
> = async (input) => {
|
||||||
console.log(input);
|
try {
|
||||||
|
await deleteWorkspaceUsers(input.workspaceId);
|
||||||
|
await deleteWorkspaceNodes(input.workspaceId);
|
||||||
|
await deleteWorkspaceUploads(input.workspaceId);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error cleaning workspace data:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const deleteWorkspaceUsers = async (workspaceId: string) => {
|
||||||
|
let hasMore = true;
|
||||||
|
|
||||||
|
while (hasMore) {
|
||||||
|
const result = await database
|
||||||
|
.deleteFrom('users')
|
||||||
|
.returning(['id'])
|
||||||
|
.where(
|
||||||
|
'id',
|
||||||
|
'in',
|
||||||
|
database
|
||||||
|
.selectFrom('users')
|
||||||
|
.select('id')
|
||||||
|
.where('workspace_id', '=', workspaceId)
|
||||||
|
.limit(BATCH_SIZE)
|
||||||
|
)
|
||||||
|
.execute();
|
||||||
|
|
||||||
|
if (result.length === 0) {
|
||||||
|
hasMore = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const deleteWorkspaceNodes = async (workspaceId: string) => {
|
||||||
|
let hasMore = true;
|
||||||
|
|
||||||
|
while (hasMore) {
|
||||||
|
const nodes = await database
|
||||||
|
.selectFrom('nodes')
|
||||||
|
.select('id')
|
||||||
|
.where('workspace_id', '=', workspaceId)
|
||||||
|
.limit(BATCH_SIZE)
|
||||||
|
.execute();
|
||||||
|
|
||||||
|
const nodeIds = nodes.map((node) => node.id);
|
||||||
|
if (nodeIds.length === 0) {
|
||||||
|
hasMore = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// delete node updates
|
||||||
|
await database
|
||||||
|
.deleteFrom('node_updates')
|
||||||
|
.where('node_id', 'in', nodeIds)
|
||||||
|
.execute();
|
||||||
|
|
||||||
|
await database
|
||||||
|
.deleteFrom('node_reactions')
|
||||||
|
.where('node_id', 'in', nodeIds)
|
||||||
|
.execute();
|
||||||
|
|
||||||
|
await database
|
||||||
|
.deleteFrom('node_interactions')
|
||||||
|
.where('node_id', 'in', nodeIds)
|
||||||
|
.execute();
|
||||||
|
|
||||||
|
await database
|
||||||
|
.deleteFrom('node_tombstones')
|
||||||
|
.where('id', 'in', nodeIds)
|
||||||
|
.execute();
|
||||||
|
|
||||||
|
await database
|
||||||
|
.deleteFrom('node_embeddings')
|
||||||
|
.where('node_id', 'in', nodeIds)
|
||||||
|
.execute();
|
||||||
|
|
||||||
|
await database
|
||||||
|
.deleteFrom('collaborations')
|
||||||
|
.where('node_id', 'in', nodeIds)
|
||||||
|
.execute();
|
||||||
|
|
||||||
|
await database
|
||||||
|
.deleteFrom('document_embeddings')
|
||||||
|
.where('document_id', 'in', nodeIds)
|
||||||
|
.execute();
|
||||||
|
|
||||||
|
await database
|
||||||
|
.deleteFrom('document_updates')
|
||||||
|
.where('document_id', 'in', nodeIds)
|
||||||
|
.execute();
|
||||||
|
|
||||||
|
await database
|
||||||
|
.deleteFrom('document_embeddings')
|
||||||
|
.where('document_id', 'in', nodeIds)
|
||||||
|
.execute();
|
||||||
|
|
||||||
|
await database.deleteFrom('nodes').where('id', 'in', nodeIds).execute();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const deleteWorkspaceUploads = async (workspaceId: string) => {
|
||||||
|
let hasMore = true;
|
||||||
|
|
||||||
|
while (hasMore) {
|
||||||
|
const uploads = await database
|
||||||
|
.selectFrom('uploads')
|
||||||
|
.select(['file_id', 'path'])
|
||||||
|
.where('workspace_id', '=', workspaceId)
|
||||||
|
.limit(BATCH_SIZE)
|
||||||
|
.execute();
|
||||||
|
|
||||||
|
if (uploads.length === 0) {
|
||||||
|
hasMore = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const upload of uploads) {
|
||||||
|
await deleteFile(upload.path);
|
||||||
|
}
|
||||||
|
|
||||||
|
const fileIds = uploads.map((upload) => upload.file_id);
|
||||||
|
await database
|
||||||
|
.deleteFrom('uploads')
|
||||||
|
.where('file_id', 'in', fileIds)
|
||||||
|
.execute();
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user