feat(documents): added delete documents restoration (#61)

This commit is contained in:
Corentin THOMASSET
2025-01-12 20:41:13 +01:00
committed by GitHub
parent 5c875b3e6f
commit cad6ff4e51
14 changed files with 455 additions and 124 deletions

View File

@@ -1,4 +1,6 @@
import type { TooltipTriggerProps } from '@kobalte/core/tooltip';
import type { ColumnDef } from '@tanstack/solid-table';
import type { Document } from '../documents.types';
import { timeAgo } from '@/modules/shared/date/time-ago';
import { cn } from '@/modules/shared/style/cn';
import { Button } from '@/modules/ui/components/button';
@@ -7,28 +9,37 @@ import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@
import { Tooltip, TooltipContent, TooltipTrigger } from '@/modules/ui/components/tooltip';
import { formatBytes } from '@corentinth/chisels';
import { A } from '@solidjs/router';
import { createQuery, keepPreviousData } from '@tanstack/solid-query';
import { createSolidTable, flexRender, getCoreRowModel, getPaginationRowModel } from '@tanstack/solid-table';
import { type Component, createSignal, For, Match, Show, Switch } from 'solid-js';
import { type Accessor, type Component, For, Match, type Setter, Show, Switch } from 'solid-js';
import { getDocumentIcon } from '../document.models';
import { fetchOrganizationDocuments } from '../documents.services';
import { DocumentManagementDropdown } from './document-management-dropdown.component';
export const DocumentsPaginatedList: Component<{ organizationId: string }> = (props) => {
const [getPagination, setPagination] = createSignal({ pageIndex: 0, pageSize: 100 });
type Pagination = {
pageIndex: number;
pageSize: number;
};
const query = createQuery(() => ({
queryKey: ['organizations', props.organizationId, 'documents', getPagination()],
queryFn: () => fetchOrganizationDocuments({
organizationId: props.organizationId,
...getPagination(),
}),
placeholderData: keepPreviousData,
}));
export const createdAtColumn: ColumnDef<Document> = {
header: () => (<span class="hidden sm:block">Created at</span>),
accessorKey: 'createdAt',
cell: data => <div class="text-muted-foreground hidden sm:block" title={data.getValue<Date>().toLocaleString()}>{timeAgo({ date: data.getValue<Date>() })}</div>,
};
export const deletedAtColumn: ColumnDef<Document> = {
header: () => (<span class="hidden sm:block">Deleted at</span>),
accessorKey: 'deletedAt',
cell: data => <div class="text-muted-foreground hidden sm:block" title={data.getValue<Date>().toLocaleString()}>{timeAgo({ date: data.getValue<Date>() })}</div>,
};
export const DocumentsPaginatedList: Component<{
documents: Document[];
documentsCount: number;
getPagination: Accessor<Pagination>;
setPagination: Setter<Pagination>;
extraColumns?: ColumnDef<Document>[];
}> = (props) => {
const table = createSolidTable({
get data() {
return query.data?.documents ?? [];
return props.documents ?? [];
},
columns: [
{
@@ -41,7 +52,7 @@ export const DocumentsPaginatedList: Component<{ organizationId: string }> = (pr
<div class="flex-1 flex flex-col gap-1 truncate">
<A
href={`/organizations/${props.organizationId}/documents/${data.row.original.id}`}
href={`/organizations/${data.row.original.organizationId}/documents/${data.row.original.id}`}
class="font-bold truncate block hover:underline"
>
{data.row.original.name.split('.').shift()}
@@ -73,29 +84,32 @@ export const DocumentsPaginatedList: Component<{ organizationId: string }> = (pr
),
},
{
header: 'Created at',
accessorKey: 'createdAt',
cell: data => <div class="text-muted-foreground" title={data.getValue<Date>().toLocaleString()}>{timeAgo({ date: data.getValue<Date>() })}</div>,
},
{
header: 'Actions',
cell: data => (
<div class="flex items-center justify-end">
<DocumentManagementDropdown documentId={data.row.original.id} organizationId={props.organizationId} />
</div>
),
},
// {
// header: () => (<span class="hidden sm:block">Created at</span>),
// accessorKey: 'createdAt',
// cell: data => <div class="text-muted-foreground hidden sm:block" title={data.getValue<Date>().toLocaleString()}>{timeAgo({ date: data.getValue<Date>() })}</div>,
// },
// {
// header: () => (<span class="block text-right">Actions</span>),
// id: 'actions',
// cell: data => (
// <div class="flex items-center justify-end">
// <DocumentManagementDropdown documentId={data.row.original.id} organizationId={data.row.original.organizationId} />
// </div>
// ),
// },
...(props.extraColumns ?? []),
],
get rowCount() {
return query.data?.documentsCount;
return props.documentsCount;
},
getCoreRowModel: getCoreRowModel(),
getPaginationRowModel: getPaginationRowModel(),
onPaginationChange: setPagination,
onPaginationChange: props.setPagination,
state: {
get pagination() {
return getPagination();
return props.getPagination();
},
},
manualPagination: true,
@@ -105,16 +119,10 @@ export const DocumentsPaginatedList: Component<{ organizationId: string }> = (pr
return (
<div>
<Switch>
<Match when={query.data?.documentsCount === 0}>
<Match when={props.documentsCount === 0}>
<p>No documents found</p>
</Match>
<Match when={query.isError}>
<p>
Error:
{query.error?.message}
</p>
</Match>
<Match when={query.isSuccess}>
<Match when={props.documentsCount > 0}>
<Table>
<TableHeader>

View File

@@ -55,6 +55,37 @@ export async function fetchOrganizationDocuments({
};
}
export async function fetchOrganizationDeletedDocuments({
organizationId,
pageIndex,
pageSize,
}: {
organizationId: string;
pageIndex: number;
pageSize: number;
}) {
const {
documents,
documentsCount,
} = await apiClient<{ documents: Document[]; documentsCount: number }>({
method: 'GET',
path: `/api/organizations/${organizationId}/documents/deleted`,
query: {
pageIndex,
pageSize,
},
});
return {
documentsCount,
documents: documents.map(document => ({
...document,
createdAt: new Date(document.createdAt),
updatedAt: document.updatedAt ? new Date(document.updatedAt) : undefined,
})),
};
}
export async function deleteDocument({
documentId,
organizationId,
@@ -68,6 +99,19 @@ export async function deleteDocument({
});
}
export async function restoreDocument({
documentId,
organizationId,
}: {
documentId: string;
organizationId: string;
}) {
await apiClient({
method: 'POST',
path: `/api/organizations/${organizationId}/documents/${documentId}/restore`,
});
}
export async function fetchDocument({
documentId,
organizationId,

View File

@@ -6,4 +6,5 @@ export type Document = {
originalSize: number;
createdAt: Date;
updatedAt?: Date;
deletedAt?: Date;
};

View File

@@ -0,0 +1,93 @@
import type { Document } from '../documents.types';
import { timeAgo } from '@/modules/shared/date/time-ago';
import { queryClient } from '@/modules/shared/query/query-client';
import { Alert, AlertDescription } from '@/modules/ui/components/alert';
import { Button } from '@/modules/ui/components/button';
import { createToast } from '@/modules/ui/components/sonner';
import { useParams } from '@solidjs/router';
import { createQuery, keepPreviousData } from '@tanstack/solid-query';
import { type Component, createSignal, Show, Suspense } from 'solid-js';
import { DocumentsPaginatedList } from '../components/documents-list.component';
import { fetchOrganizationDeletedDocuments, restoreDocument } from '../documents.services';
export const DeletedDocumentsPage: Component = () => {
const [getPagination, setPagination] = createSignal({ pageIndex: 0, pageSize: 100 });
const params = useParams();
const query = createQuery(() => ({
queryKey: ['organizations', params.organizationId, 'documents', 'deleted', getPagination()],
queryFn: () => fetchOrganizationDeletedDocuments({
organizationId: params.organizationId,
...getPagination(),
}),
placeholderData: keepPreviousData,
}));
const restore = async ({ document }: { document: Document }) => {
await restoreDocument({
documentId: document.id,
organizationId: document.organizationId,
});
await queryClient.invalidateQueries({ queryKey: ['organizations', document.organizationId, 'documents'] });
createToast({ type: 'success', message: 'Document restored' });
};
return (
<div class="p-6 mt-4 pb-32">
<h1 class="text-2xl font-bold">Deleted documents</h1>
<Alert variant="muted" class="my-4 flex items-center gap-6 xl:gap-4">
<div class="i-tabler-info-circle size-10 xl:size-8 text-primary flex-shrink-0 hidden sm:block" />
<AlertDescription>
All deleted documents are stored in the trash bin for 30 days. Passing this delay, the documents will be permanently deleted, and you will not be able to restore them.
</AlertDescription>
</Alert>
<Suspense>
<Show when={query.data?.documents.length === 0}>
<div class="flex flex-col items-center justify-center gap-2 pt-24 mx-auto max-w-md text-center">
<div class="i-tabler-trash text-primary size-12" aria-hidden="true" />
<div class="text-xl font-medium">No deleted documents</div>
<div class="text-sm text-muted-foreground">
You have no deleted documents. Documents that are deleted will be moved to the trash bin for 30 days.
</div>
</div>
</Show>
<Show when={query.data && query.data?.documents.length > 0}>
<DocumentsPaginatedList
documents={query.data?.documents ?? []}
documentsCount={query.data?.documentsCount ?? 0}
getPagination={getPagination}
setPagination={setPagination}
extraColumns={[
{
id: 'deletion',
cell: data => (
<div class="text-muted-foreground hidden sm:block">
Deleted
{' '}
<span class="text-foreground font-bold" title={data.row.original.deletedAt?.toLocaleString()}>{timeAgo({ date: data.row.original.deletedAt! })}</span>
</div>
),
},
{
id: 'actions',
cell: data => (
<div class="flex items-center justify-end">
<Button variant="outline" size="sm" class="gap-2" onClick={() => restore({ document: data.row.original })}>
<div class="i-tabler-refresh size-4" />
Restore
</Button>
</div>
),
},
]}
/>
</Show>
</Suspense>
</div>
);
};

View File

@@ -1,15 +1,15 @@
import type { TooltipTriggerProps } from '@kobalte/core/tooltip';
import type { Component } from 'solid-js';
import type { Document } from '../documents.types';
import { timeAgo } from '@/modules/shared/date/time-ago';
import { cn } from '@/modules/shared/style/cn';
import { Tooltip, TooltipContent, TooltipTrigger } from '@/modules/ui/components/tooltip';
import { formatBytes } from '@corentinth/chisels';
import { A, useParams } from '@solidjs/router';
import { createQueries } from '@tanstack/solid-query';
import { createQueries, keepPreviousData } from '@tanstack/solid-query';
import { type Component, createSignal, Suspense } from 'solid-js';
import { DocumentManagementDropdown } from '../components/document-management-dropdown.component';
import { DocumentUploadArea } from '../components/document-upload-area.component';
import { DocumentsPaginatedList } from '../components/documents-paginated-list.component';
import { DocumentsPaginatedList } from '../components/documents-list.component';
import { getDocumentIcon } from '../document.models';
import { fetchOrganizationDocuments } from '../documents.services';
@@ -64,57 +64,64 @@ const DocumentCard: Component<{ document: Document; organizationId?: string }> =
export const DocumentsPage: Component = () => {
const params = useParams();
const [getPagination, setPagination] = createSignal({ pageIndex: 0, pageSize: 100 });
const query = createQueries(() => ({
queries: [
{
queryKey: ['organizations', params.organizationId, 'documents', { pageIndex: 0, pageSize: 100 }],
queryKey: ['organizations', params.organizationId, 'documents', getPagination()],
queryFn: () => fetchOrganizationDocuments({
organizationId: params.organizationId,
pageIndex: 0,
pageSize: 5,
...getPagination(),
}),
placeholderData: keepPreviousData,
},
],
}));
return (
<div class="p-6 mt-4 pb-32">
<Suspense>
{query[0].data?.documents?.length === 0
? (
<>
<h2 class="text-xl font-bold ">
No documents
</h2>
{query[0].data?.documents.length === 0
? (
<>
<h2 class="text-xl font-bold ">
No documents
</h2>
<p class="text-muted-foreground mt-1 mb-6">
There are no documents in this organization yet. Start by uploading some documents.
</p>
<p class="text-muted-foreground mt-1 mb-6">
There are no documents in this organization yet. Start by uploading some documents.
</p>
<DocumentUploadArea />
<DocumentUploadArea />
</>
)
: (
<>
<h2 class="text-lg font-semibold mb-4">
Latest imported documents
</h2>
<div class="grid gap-4 grid-cols-1 lg:grid-cols-3 xl:grid-cols-4">
{query[0].data?.documents.slice(0, 5).map(document => (
<DocumentCard document={document} />
))}
</div>
</>
)
: (
<>
<h2 class="text-lg font-semibold mb-4">
Latest imported documents
</h2>
<div class="grid gap-4 grid-cols-1 lg:grid-cols-3 xl:grid-cols-4">
{query[0].data?.documents.map(document => (
<DocumentCard document={document} />
))}
</div>
<h2 class="text-lg font-semibold mt-8 mb-2">
All documents
</h2>
<h2 class="text-lg font-semibold mt-8 mb-2">
All documents
</h2>
<DocumentsPaginatedList
documents={query[0].data?.documents ?? []}
documentsCount={query[0].data?.documentsCount ?? 0}
getPagination={getPagination}
setPagination={setPagination}
/>
</>
<DocumentsPaginatedList organizationId={params.organizationId} />
</>
)}
)}
</Suspense>
</div>
);
};

View File

@@ -0,0 +1,11 @@
import type { Component } from 'solid-js';
export const ComingSoonPage: Component = () => {
return (
<div class="flex flex-col items-center justify-center gap-2 pt-24">
<div class="i-tabler-alarm text-primary size-12" />
<div class="text-xl font-medium">Coming Soon</div>
<div class="text-sm text-muted-foreground">This feature is coming soon, please check back later.</div>
</div>
);
};

View File

@@ -0,0 +1,27 @@
import type { Component } from 'solid-js';
import { Button } from '@/modules/ui/components/button';
import { A } from '@solidjs/router';
export const NotFoundPage: Component = () => {
return (
<div class="h-screen flex flex-col items-center justify-center p-6">
<div class="flex items-center flex-row sm:gap-24">
<div class="max-w-350px">
<h1 class="text-xl mr-4 py-2">404 - Not Found</h1>
<p class="text-muted-foreground">
Sorry, the page you are looking for does seem to exist. Please check the URL and try again.
</p>
<Button as={A} href="/" class="mt-4" variant="default">
<div class="i-tabler-arrow-left mr-2"></div>
Go back to home
</Button>
</div>
<div class="hidden sm:block light:text-muted-foreground">
<div class="i-tabler-file-shredder text-200px"></div>
</div>
</div>
</div>
);
};

View File

@@ -14,6 +14,8 @@ export const alertVariants = cva(
variant: {
default: 'bg-background text-foreground',
destructive: 'border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive',
primary: 'bg-background text-foreground border-primary',
muted: 'bg-muted text-muted-foreground border-muted',
},
},
defaultVariants: {

View File

@@ -21,19 +21,35 @@ const OrganizationLayoutSideNav: Component = () => {
const getMainMenuItems = () => [
{
label: 'Documents',
icon: 'i-tabler-file-text',
label: 'Home',
icon: 'i-tabler-home',
href: `/organizations/${params.organizationId}`,
},
{
label: 'Deleted documents',
icon: 'i-tabler-trash',
href: `/organizations/${params.organizationId}/deleted`,
label: 'Documents',
icon: 'i-tabler-file-text',
href: `/organizations/${params.organizationId}/documents`,
},
{
label: 'Tags',
icon: 'i-tabler-tag',
href: `/organizations/${params.organizationId}/tags`,
},
{
label: 'Integrations',
icon: 'i-tabler-link',
href: `/organizations/${params.organizationId}/integrations`,
},
];
const getFooterMenuItems = () => [
{
label: 'Deleted documents',
icon: 'i-tabler-trash',
href: `/organizations/${params.organizationId}/deleted`,
},
{
label: 'Organization settings',
icon: 'i-tabler-settings',

View File

@@ -11,7 +11,7 @@ import { useThemeStore } from '@/modules/theme/theme.store';
import { Button } from '@/modules/ui/components/button';
import { A, useParams } from '@solidjs/router';
import { type Component, type ParentComponent, Show, Suspense } from 'solid-js';
import { type Component, type ComponentProps, type JSX, type ParentComponent, Suspense } from 'solid-js';
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from '../components/dropdown-menu';
import { Sheet, SheetContent, SheetTrigger } from '../components/sheet';
import { Tooltip, TooltipContent, TooltipTrigger } from '../components/tooltip';
@@ -21,29 +21,18 @@ type MenuItem = {
icon: string;
href?: string;
onClick?: () => void;
badge?: JSX.Element;
};
const MenuItemButton: Component<MenuItem> = (props) => {
return (
<>
<Show when={props.onClick}>
<Button class="block" onClick={props.onClick} variant="ghost">
<div class="flex items-center gap-2 dark:text-muted-foreground truncate">
<div class={cn(props.icon, 'size-5')}></div>
<div>{props.label}</div>
</div>
</Button>
</Show>
<Show when={!props.onClick}>
<Button class="block dark:text-muted-foreground" as={A} href={props.href!} variant="ghost" activeClass="bg-accent/50! text-accent-foreground! truncate" end>
<div class="flex items-center gap-2">
<div class={cn(props.icon, 'size-5')}></div>
<div class="truncate">{props.label}</div>
</div>
</Button>
</Show>
</>
<Button class="block" variant="ghost" {...(props.onClick ? { onClick: props.onClick } : { as: A, href: props.href, activeClass: 'bg-accent/50! text-accent-foreground! truncate', end: true } as ComponentProps<typeof Button>)}>
<div class="flex items-center gap-2 dark:text-muted-foreground truncate">
<div class={cn(props.icon, 'size-5')}></div>
<div>{props.label}</div>
{props.badge && <div class="ml-auto">{props.badge}</div>}
</div>
</Button>
);
};

View File

@@ -1,4 +1,4 @@
import { A, Navigate, type RouteDefinition, useParams } from '@solidjs/router';
import { Navigate, type RouteDefinition, useParams } from '@solidjs/router';
import { createQuery } from '@tanstack/solid-query';
import { Match, Show, Suspense, Switch } from 'solid-js';
import { createProtectedPage } from './modules/auth/middleware/protected-page.middleware';
@@ -6,6 +6,7 @@ import { ConfirmPage } from './modules/auth/pages/confirm.page';
import { GenericAuthPage } from './modules/auth/pages/generic-auth.page';
import { MagicLinkSentPage } from './modules/auth/pages/magic-link-sent.page';
import { PendingMagicLinkPage } from './modules/auth/pages/verify-magic-link.page';
import { DeletedDocumentsPage } from './modules/documents/pages/deleted-documents.page';
import { DocumentPage } from './modules/documents/pages/document.page';
import { DocumentsPage } from './modules/documents/pages/documents.page';
import { fetchOrganizations } from './modules/organizations/organizations.services';
@@ -13,7 +14,8 @@ import { CreateFirstOrganizationPage } from './modules/organizations/pages/creat
import { CreateOrganizationPage } from './modules/organizations/pages/create-organization.page';
import { OrganizationsSettingsPage } from './modules/organizations/pages/organizations-settings.page';
import { OrganizationsPage } from './modules/organizations/pages/organizations.page';
import { Button } from './modules/ui/components/button';
import { ComingSoonPage } from './modules/shared/pages/coming-soon.page';
import { NotFoundPage } from './modules/shared/pages/not-found.page';
import { OrganizationLayout } from './modules/ui/layouts/organization.layout';
import { CurrentUserProvider, useCurrentUser } from './modules/users/composables/useCurrentUser';
import { UserSettingsPage } from './modules/users/pages/user-settings.page';
@@ -84,6 +86,10 @@ export const routes: RouteDefinition[] = [
path: '/',
component: DocumentsPage,
},
{
path: '/documents',
component: DocumentsPage,
},
{
path: '/documents/:documentId',
component: DocumentPage,
@@ -92,6 +98,18 @@ export const routes: RouteDefinition[] = [
path: '/settings',
component: OrganizationsSettingsPage,
},
{
path: '/deleted',
component: DeletedDocumentsPage,
},
{
path: '/tags',
component: ComingSoonPage,
},
{
path: '/integrations',
component: ComingSoonPage,
},
],
},
{
@@ -132,26 +150,6 @@ export const routes: RouteDefinition[] = [
},
{
path: '*404',
component: () => (
<div class="h-screen flex flex-col items-center justify-center p-6">
<div class="flex items-center flex-row sm:gap-24">
<div class="max-w-350px">
<h1 class="text-xl mr-4 py-2">404 - Not Found</h1>
<p class="text-muted-foreground">
Sorry, the page you are looking for does seem to exist. Please check the URL and try again.
</p>
<Button as={A} href="/" class="mt-4" variant="default">
<div class="i-tabler-arrow-left mr-2"></div>
Go back to home
</Button>
</div>
<div class="hidden sm:block light:text-muted-foreground">
<div class="i-tabler-file-shredder text-200px"></div>
</div>
</div>
</div>
),
component: NotFoundPage,
},
];

View File

@@ -5,3 +5,9 @@ export const createDocumentNotFoundError = createErrorFactory({
code: 'document.not_found',
statusCode: 404,
});
export const createDocumentIsNotDeletedError = createErrorFactory({
message: 'Document is not deleted, cannot restore.',
code: 'document.not_deleted',
statusCode: 400,
});

View File

@@ -12,10 +12,13 @@ export function createDocumentsRepository({ db }: { db: Database }) {
{
saveOrganizationDocument,
getOrganizationDocuments,
getOrganizationDeletedDocuments,
getDocumentById,
softDeleteDocument,
getOrganizationDocumentsCount,
getOrganizationDeletedDocumentsCount,
searchOrganizationDocuments,
restoreDocument,
},
{ db },
);
@@ -30,7 +33,7 @@ async function saveOrganizationDocument({ db, ...documentToInsert }: { db: Datab
async function getOrganizationDocumentsCount({ organizationId, db }: { organizationId: string; db: Database }) {
const [{ documentsCount }] = await db
.select({
documentsCount: count(),
documentsCount: count(documentsTable.id),
})
.from(documentsTable)
.where(
@@ -43,6 +46,22 @@ async function getOrganizationDocumentsCount({ organizationId, db }: { organizat
return { documentsCount };
}
async function getOrganizationDeletedDocumentsCount({ organizationId, db }: { organizationId: string; db: Database }) {
const [{ documentsCount }] = await db
.select({
documentsCount: count(documentsTable.id),
})
.from(documentsTable)
.where(
and(
eq(documentsTable.organizationId, organizationId),
eq(documentsTable.isDeleted, true),
),
);
return { documentsCount };
}
async function getOrganizationDocuments({ organizationId, pageIndex, pageSize, db }: { organizationId: string; pageIndex: number; pageSize: number; db: Database }) {
const query = db
.select()
@@ -68,6 +87,31 @@ async function getOrganizationDocuments({ organizationId, pageIndex, pageSize, d
};
}
async function getOrganizationDeletedDocuments({ organizationId, pageIndex, pageSize, db }: { organizationId: string; pageIndex: number; pageSize: number; db: Database }) {
const query = db
.select()
.from(documentsTable)
.where(
and(
eq(documentsTable.organizationId, organizationId),
eq(documentsTable.isDeleted, true),
),
);
const documents = await withPagination(
query.$dynamic(),
{
orderByColumn: desc(documentsTable.deletedAt),
pageIndex,
pageSize,
},
);
return {
documents,
};
}
async function getDocumentById({ documentId, db }: { documentId: string; db: Database }) {
const [document] = await db
.select()
@@ -90,6 +134,17 @@ async function softDeleteDocument({ documentId, userId, db, now = new Date() }:
.where(eq(documentsTable.id, documentId));
}
async function restoreDocument({ documentId, db }: { documentId: string; db: Database }) {
await db
.update(documentsTable)
.set({
isDeleted: false,
deletedBy: null,
deletedAt: null,
})
.where(eq(documentsTable.id, documentId));
}
async function searchOrganizationDocuments({ organizationId, searchQuery, pageIndex, pageSize, db }: { organizationId: string; searchQuery: string; pageIndex: number; pageSize: number; db: Database }) {
// TODO: extract this logic to a tested function
// when searchquery is a single word, we append a wildcard to it to make it a prefix search

View File

@@ -9,6 +9,7 @@ import { createOrganizationsRepository } from '../organizations/organizations.re
import { ensureUserIsInOrganization } from '../organizations/organizations.usecases';
import { createError } from '../shared/errors/errors';
import { validateFormData, validateParams, validateQuery } from '../shared/validation/validation';
import { createDocumentIsNotDeletedError } from './documents.errors';
import { createDocumentsRepository } from './documents.repository';
import { createDocument, ensureDocumentExists, getDocumentOrThrow } from './documents.usecases';
import { createDocumentStorageService } from './storage/documents.storage.services';
@@ -17,6 +18,8 @@ export function registerDocumentsPrivateRoutes({ app }: { app: ServerInstance })
setupCreateDocumentRoute({ app });
setupGetDocumentsRoute({ app });
setupSearchDocumentsRoute({ app });
setupRestoreDocumentRoute({ app });
setupGetDeletedDocumentsRoute({ app });
setupGetDocumentRoute({ app });
setupDeleteDocumentRoute({ app });
setupGetDocumentFileRoute({ app });
@@ -132,6 +135,46 @@ function setupGetDocumentsRoute({ app }: { app: ServerInstance }) {
);
}
function setupGetDeletedDocumentsRoute({ app }: { app: ServerInstance }) {
app.get(
'/api/organizations/:organizationId/documents/deleted',
validateParams(z.object({
organizationId: z.string().regex(organizationIdRegex),
})),
validateQuery(
z.object({
pageIndex: z.coerce.number().min(0).int().optional().default(0),
pageSize: z.coerce.number().min(1).max(100).int().optional().default(100),
}),
),
async (context) => {
const { userId } = getAuthUserId({ context });
const { db } = getDb({ context });
const { organizationId } = context.req.valid('param');
const { pageIndex, pageSize } = context.req.valid('query');
const documentsRepository = createDocumentsRepository({ db });
const organizationsRepository = createOrganizationsRepository({ db });
await ensureUserIsInOrganization({ userId, organizationId, organizationsRepository });
const [
{ documents },
{ documentsCount },
] = await Promise.all([
documentsRepository.getOrganizationDeletedDocuments({ organizationId, pageIndex, pageSize }),
documentsRepository.getOrganizationDeletedDocumentsCount({ organizationId }),
]);
return context.json({
documents,
documentsCount,
});
},
);
}
function setupGetDocumentRoute({ app }: { app: ServerInstance }) {
app.get(
'/api/organizations/:organizationId/documents/:documentId',
@@ -187,6 +230,37 @@ function setupDeleteDocumentRoute({ app }: { app: ServerInstance }) {
);
}
function setupRestoreDocumentRoute({ app }: { app: ServerInstance }) {
app.post(
'/api/organizations/:organizationId/documents/:documentId/restore',
validateParams(z.object({
organizationId: z.string().regex(organizationIdRegex),
documentId: z.string(),
})),
async (context) => {
const { userId } = getAuthUserId({ context });
const { db } = getDb({ context });
const { organizationId, documentId } = context.req.valid('param');
const documentsRepository = createDocumentsRepository({ db });
const organizationsRepository = createOrganizationsRepository({ db });
await ensureUserIsInOrganization({ userId, organizationId, organizationsRepository });
const { document } = await getDocumentOrThrow({ documentId, documentsRepository });
if (!document.isDeleted) {
throw createDocumentIsNotDeletedError();
}
await documentsRepository.restoreDocument({ documentId });
return context.body(null, 204);
},
);
}
function setupGetDocumentFileRoute({ app }: { app: ServerInstance }) {
app.get(
'/api/organizations/:organizationId/documents/:documentId/file',