diff --git a/.gitignore b/.gitignore index 043bfa26..809f71d2 100644 --- a/.gitignore +++ b/.gitignore @@ -43,3 +43,4 @@ qa-output .worktrees/ .turbo +.superset diff --git a/app/client/components/file-browsers/__tests__/snapshot-tree-browser.test.tsx b/app/client/components/file-browsers/__tests__/snapshot-tree-browser.test.tsx new file mode 100644 index 00000000..568461eb --- /dev/null +++ b/app/client/components/file-browsers/__tests__/snapshot-tree-browser.test.tsx @@ -0,0 +1,174 @@ +import type { ComponentProps } from "react"; +import { afterEach, describe, expect, test } from "bun:test"; +import { HttpResponse, http, server } from "~/test/msw/server"; +import { cleanup, render, screen, userEvent, waitFor, within } from "~/test/test-utils"; + +type SnapshotFilesRequest = { + shortId: string; + snapshotId: string; + path: string | null; + offset: string | null; + limit: string | null; +}; + +const snapshotFiles = { + files: [ + { name: "project", path: "/mnt/project", type: "dir" }, + { name: "a.txt", path: "/mnt/project/a.txt", type: "file" }, + ], +}; + +import { SnapshotTreeBrowser } from "../snapshot-tree-browser"; + +const mockListSnapshotFiles = () => { + const requests: SnapshotFilesRequest[] = []; + + server.use( + http.get("/api/v1/repositories/:shortId/snapshots/:snapshotId/files", ({ params, request }) => { + const url = new URL(request.url); + requests.push({ + shortId: String(params.shortId), + snapshotId: String(params.snapshotId), + path: url.searchParams.get("path"), + offset: url.searchParams.get("offset"), + limit: url.searchParams.get("limit"), + }); + + return HttpResponse.json(snapshotFiles); + }), + ); + + return requests; +}; + +const renderSnapshotTreeBrowser = (props: Partial> = {}) => { + return render( + , + ); +}; + +afterEach(() => { + cleanup(); +}); + +describe("SnapshotTreeBrowser", () => { + test("renders the query root folder when display base path is broader than query base path", async () => { + mockListSnapshotFiles(); + + renderSnapshotTreeBrowser(); + + expect(await screen.findByRole("button", { name: "project" })).toBeTruthy(); + }); + + test("shows selected folder state when full paths are provided from the parent", async () => { + mockListSnapshotFiles(); + + renderSnapshotTreeBrowser({ + withCheckboxes: true, + selectedPaths: new Set(["/mnt/project"]), + onSelectionChange: () => {}, + }); + + const row = await screen.findByRole("button", { name: "project" }); + const checkbox = within(row).getByRole("checkbox"); + + expect(checkbox.getAttribute("aria-checked")).toBe("true"); + }); + + test("returns the full snapshot path and kind when selecting a displayed folder", async () => { + mockListSnapshotFiles(); + + let selectedPaths: Set | undefined; + let selectedKind: "file" | "dir" | null = null; + + renderSnapshotTreeBrowser({ + withCheckboxes: true, + onSelectionChange: (paths) => { + selectedPaths = paths; + }, + onSingleSelectionKindChange: (kind) => { + selectedKind = kind; + }, + }); + + const row = await screen.findByRole("button", { name: "project" }); + const checkbox = within(row).getByRole("checkbox"); + + await userEvent.click(checkbox); + + expect(selectedPaths ? Array.from(selectedPaths) : []).toEqual(["/mnt/project"]); + expect(selectedKind === "dir").toBe(true); + }); + + test("uses the query base path for the initial request when display base path is broader", async () => { + const requests = mockListSnapshotFiles(); + + renderSnapshotTreeBrowser(); + + await waitFor(() => { + expect(requests[0]).toEqual({ + shortId: "repo-1", + snapshotId: "snap-1", + path: "/mnt/project", + offset: null, + limit: null, + }); + }); + }); + + test("prefetches using the query path when display and query roots differ", async () => { + const requests = mockListSnapshotFiles(); + + renderSnapshotTreeBrowser(); + + const row = await screen.findByRole("button", { name: "project" }); + const initialRequestCount = requests.length; + + await userEvent.hover(row); + + await waitFor(() => { + expect(requests.length).toBe(initialRequestCount + 1); + }); + + expect(requests.at(-1)).toEqual({ + shortId: "repo-1", + snapshotId: "snap-1", + path: "/mnt/project", + offset: "0", + limit: "500", + }); + }); + + test("expands using the query path when display and query roots differ", async () => { + const requests = mockListSnapshotFiles(); + + renderSnapshotTreeBrowser(); + + const row = await screen.findByRole("button", { name: "project" }); + const expandIcon = row.querySelector("svg"); + if (!expandIcon) { + throw new Error("Expected expand icon for folder row"); + } + + const initialRequestCount = requests.length; + await userEvent.click(expandIcon); + + await waitFor(() => { + expect(requests.length).toBeGreaterThan(initialRequestCount); + }); + + expect(requests.at(-1)).toEqual({ + shortId: "repo-1", + snapshotId: "snap-1", + path: "/mnt/project", + offset: "0", + limit: "500", + }); + }); +}); diff --git a/app/client/components/file-browsers/snapshot-tree-browser.tsx b/app/client/components/file-browsers/snapshot-tree-browser.tsx index 993b935f..f6879888 100644 --- a/app/client/components/file-browsers/snapshot-tree-browser.tsx +++ b/app/client/components/file-browsers/snapshot-tree-browser.tsx @@ -7,92 +7,91 @@ import { parseError } from "~/client/lib/errors"; import { normalizeAbsolutePath } from "@zerobyte/core/utils"; import { logger } from "~/client/lib/logger"; +function createPathPrefixFns(basePath: string) { + return { + strip(path: string) { + if (basePath === "/") return path; + if (path === basePath) return "/"; + if (path.startsWith(`${basePath}/`)) return path.slice(basePath.length); + return path; + }, + add(displayPath: string) { + if (basePath === "/") return displayPath; + if (displayPath === "/") return basePath; + return `${basePath}${displayPath}`; + }, + }; +} + type SnapshotTreeBrowserProps = FileBrowserUiProps & { repositoryId: string; snapshotId: string; - basePath?: string; + queryBasePath?: string; + displayBasePath?: string; pageSize?: number; enabled?: boolean; onSingleSelectionKindChange?: (kind: "file" | "dir" | null) => void; }; -export const SnapshotTreeBrowser = ({ - repositoryId, - snapshotId, - basePath = "/", - pageSize = 500, - enabled = true, - ...uiProps -}: SnapshotTreeBrowserProps) => { +export const SnapshotTreeBrowser = (props: SnapshotTreeBrowserProps) => { + const { + repositoryId, + snapshotId, + queryBasePath = "/", + displayBasePath, + pageSize = 500, + enabled = true, + ...uiProps + } = props; + const { selectedPaths, onSelectionChange, onSingleSelectionKindChange, ...fileBrowserUiProps } = uiProps; const queryClient = useQueryClient(); - const normalizedBasePath = normalizeAbsolutePath(basePath); + const normalizedQueryBasePath = normalizeAbsolutePath(queryBasePath); + const normalizedDisplayBasePath = normalizeAbsolutePath(displayBasePath ?? normalizedQueryBasePath); const { data, isLoading, error } = useQuery({ ...listSnapshotFilesOptions({ path: { shortId: repositoryId, snapshotId }, - query: { path: normalizedBasePath }, + query: { path: normalizedQueryBasePath }, }), enabled, }); - const stripBasePath = useCallback( - (path: string): string => { - if (normalizedBasePath === "/") return path; - if (path === normalizedBasePath) return "/"; - if (path.startsWith(`${normalizedBasePath}/`)) { - return path.slice(normalizedBasePath.length); - } - return path; - }, - [normalizedBasePath], - ); - - const addBasePath = useCallback( - (displayPath: string): string => { - if (normalizedBasePath === "/") return displayPath; - if (displayPath === "/") return normalizedBasePath; - return `${normalizedBasePath}${displayPath}`; - }, - [normalizedBasePath], - ); + const displayPathFns = useMemo(() => createPathPrefixFns(normalizedDisplayBasePath), [normalizedDisplayBasePath]); const displaySelectedPaths = useMemo(() => { if (!selectedPaths) return undefined; const displayPaths = new Set(); for (const fullPath of selectedPaths) { - displayPaths.add(stripBasePath(fullPath)); + displayPaths.add(displayPathFns.strip(fullPath)); } return displayPaths; - }, [selectedPaths, stripBasePath]); + }, [displayPathFns, selectedPaths]); const fileBrowser = useFileBrowser({ initialData: data, isLoading, - fetchFolder: async (path, offset = 0) => { + fetchFolder: async (displayPath, offset = 0) => { return await queryClient.ensureQueryData( listSnapshotFilesOptions({ path: { shortId: repositoryId, snapshotId }, - query: { path, offset: offset, limit: pageSize }, + query: { path: displayPath, offset: offset, limit: pageSize }, }), ); }, - prefetchFolder: (path) => { + prefetchFolder: (displayPath) => { void queryClient .prefetchQuery( listSnapshotFilesOptions({ path: { shortId: repositoryId, snapshotId }, - query: { path, offset: 0, limit: pageSize }, + query: { path: displayPath, offset: 0, limit: pageSize }, }), ) .catch((e) => logger.error(e)); }, - pathTransform: { - strip: stripBasePath, - add: addBasePath, - }, + pathTransform: displayPathFns, }); const displayPathKinds = useMemo(() => { @@ -109,7 +108,7 @@ export const SnapshotTreeBrowser = ({ const nextFullPaths = new Set(); for (const displayPath of nextDisplayPaths) { - nextFullPaths.add(addBasePath(displayPath)); + nextFullPaths.add(displayPathFns.add(displayPath)); } if (onSingleSelectionKindChange) { @@ -127,7 +126,7 @@ export const SnapshotTreeBrowser = ({ onSelectionChange(nextFullPaths); }, - [onSelectionChange, addBasePath, onSingleSelectionKindChange, displayPathKinds], + [displayPathFns, displayPathKinds, onSelectionChange, onSingleSelectionKindChange], ); return ( diff --git a/app/client/components/restore-form.tsx b/app/client/components/restore-form.tsx index 56d75322..467e3601 100644 --- a/app/client/components/restore-form.tsx +++ b/app/client/components/restore-form.tsx @@ -33,14 +33,15 @@ interface RestoreFormProps { repository: Repository; snapshotId: string; returnPath: string; - basePath?: string; + queryBasePath?: string; + displayBasePath?: string; } -export function RestoreForm({ repository, snapshotId, returnPath, basePath }: RestoreFormProps) { +export function RestoreForm({ repository, snapshotId, returnPath, queryBasePath, displayBasePath }: RestoreFormProps) { const navigate = useNavigate(); const { addEventListener } = useServerEvents(); - const volumeBasePath = basePath ?? "/"; + const snapshotBasePath = queryBasePath ?? "/"; const [restoreLocation, setRestoreLocation] = useState("original"); const [customTargetPath, setCustomTargetPath] = useState(""); @@ -346,7 +347,8 @@ export function RestoreForm({ repository, snapshotId, returnPath, basePath }: Re - new QueryClient({ - defaultOptions: { - queries: { - retry: false, - gcTime: Infinity, - }, - mutations: { - gcTime: Infinity, - }, - }, - }); - const ConnectionConsumer = ({ enabled = true }: { enabled?: boolean }) => { useServerEvents({ enabled }); return null; @@ -105,10 +91,11 @@ describe("useServerEvents", () => { queryClient.refetchQueries = refetchQueries as typeof queryClient.refetchQueries; render( - + <> - , + , + { queryClient }, ); expect(MockEventSource.instances).toHaveLength(1); @@ -132,19 +119,11 @@ describe("useServerEvents", () => { test("waits to subscribe until enabled", () => { const queryClient = createTestQueryClient(); - const view = render( - - - , - ); + const view = render(, { queryClient }); expect(MockEventSource.instances).toHaveLength(0); - view.rerender( - - - , - ); + view.rerender(); expect(MockEventSource.instances).toHaveLength(1); expect(MockEventSource.instances[0]?.url).toBe("/api/v1/events"); diff --git a/app/client/modules/auth/routes/__tests__/login.test.tsx b/app/client/modules/auth/routes/__tests__/login.test.tsx index 8a824990..eff96959 100644 --- a/app/client/modules/auth/routes/__tests__/login.test.tsx +++ b/app/client/modules/auth/routes/__tests__/login.test.tsx @@ -1,6 +1,5 @@ import { afterEach, describe, expect, mock, test } from "bun:test"; -import { cleanup, render, screen } from "@testing-library/react"; -import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; +import { cleanup, render, screen } from "~/test/test-utils"; await mock.module("@tanstack/react-router", () => ({ useNavigate: () => mock(() => {}), @@ -11,19 +10,6 @@ await mock.module("~/client/modules/sso/components/sso-login-section", () => ({ })); import { LoginPage } from "../login"; - -const createTestQueryClient = () => - new QueryClient({ - defaultOptions: { - queries: { - retry: false, - gcTime: Infinity, - }, - mutations: { - gcTime: Infinity, - }, - }, - }); const inviteOnlyMessage = "Access is invite-only. Ask an organization admin to send you an invitation before signing in with SSO."; @@ -33,23 +19,13 @@ afterEach(() => { describe("LoginPage", () => { test("shows an invite-only message when SSO returns INVITE_REQUIRED code", async () => { - const queryClient = createTestQueryClient(); - render( - - - , - ); + render(); expect(await screen.findByText(inviteOnlyMessage)).toBeTruthy(); }); test("shows account link required message when SSO returns ACCOUNT_LINK_REQUIRED code", async () => { - const queryClient = createTestQueryClient(); - render( - - - , - ); + render(); expect( await screen.findByText( @@ -59,12 +35,7 @@ describe("LoginPage", () => { }); test("shows banned message when SSO returns BANNED_USER code", async () => { - const queryClient = createTestQueryClient(); - render( - - - , - ); + render(); expect( await screen.findByText( @@ -74,34 +45,19 @@ describe("LoginPage", () => { }); test("shows email not verified message when SSO returns EMAIL_NOT_VERIFIED code", async () => { - const queryClient = createTestQueryClient(); - render( - - - , - ); + render(); expect(await screen.findByText("Your identity provider did not mark your email as verified.")).toBeTruthy(); }); test("shows generic SSO error message when SSO returns SSO_LOGIN_FAILED code", async () => { - const queryClient = createTestQueryClient(); - render( - - - , - ); + render(); expect(await screen.findByText("SSO authentication failed. Please try again.")).toBeTruthy(); }); test("does not show error message for invalid error codes", async () => { - const queryClient = createTestQueryClient(); - render( - - - , - ); + render(); expect(await screen.findByText("Login to your account")).toBeTruthy(); expect(screen.queryByText(inviteOnlyMessage)).toBeNull(); diff --git a/app/client/modules/backups/components/__tests__/snapshot-file-browser.test.tsx b/app/client/modules/backups/components/__tests__/snapshot-file-browser.test.tsx new file mode 100644 index 00000000..2fe14a0c --- /dev/null +++ b/app/client/modules/backups/components/__tests__/snapshot-file-browser.test.tsx @@ -0,0 +1,45 @@ +import type { ReactNode } from "react"; +import { afterEach, describe, expect, mock, test } from "bun:test"; +import { cleanup, render, screen } from "@testing-library/react"; +import { fromAny } from "@total-typescript/shoehorn"; + +await mock.module("@tanstack/react-router", () => ({ + Link: ({ children }: { children?: ReactNode }) => {children}, +})); + +await mock.module("~/client/components/file-browsers/snapshot-tree-browser", () => ({ + SnapshotTreeBrowser: ({ queryBasePath, displayBasePath }: { queryBasePath?: string; displayBasePath?: string }) => ( +
{`query:${queryBasePath ?? "missing"} display:${displayBasePath ?? "missing"}`}
+ ), +})); + +await mock.module("~/client/lib/datetime", () => ({ + useTimeFormat: () => ({ + formatDateTime: () => "2026-03-26 00:00", + }), +})); + +import { SnapshotFileBrowser } from "../snapshot-file-browser"; + +afterEach(() => { + cleanup(); +}); + +describe("SnapshotFileBrowser", () => { + test("uses the snapshot common ancestor as query root while keeping a broader display root", () => { + render( + , + ); + + expect(screen.getByText("query:/mnt/project/subdir display:/mnt")).toBeTruthy(); + }); +}); diff --git a/app/client/modules/backups/components/snapshot-file-browser.tsx b/app/client/modules/backups/components/snapshot-file-browser.tsx index bbe28f8b..60e10b19 100644 --- a/app/client/modules/backups/components/snapshot-file-browser.tsx +++ b/app/client/modules/backups/components/snapshot-file-browser.tsx @@ -1,5 +1,4 @@ import { RotateCcw, Trash2 } from "lucide-react"; -import { useQuery } from "@tanstack/react-query"; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "~/client/components/ui/card"; import { Button, buttonVariants } from "~/client/components/ui/button"; import type { Snapshot } from "~/client/lib/types"; @@ -7,14 +6,13 @@ import { useTimeFormat } from "~/client/lib/datetime"; import { cn } from "~/client/lib/utils"; import { Link } from "@tanstack/react-router"; import { SnapshotTreeBrowser } from "~/client/components/file-browsers/snapshot-tree-browser"; -import { getBackupScheduleOptions } from "~/client/api-client/@tanstack/react-query.gen"; -import { getVolumeMountPath } from "~/client/lib/volume-path"; +import { findCommonAncestor } from "@zerobyte/core/utils"; interface Props { snapshot: Snapshot; repositoryId: string; backupId?: string; - basePath?: string; + displayBasePath?: string; onDeleteSnapshot?: (snapshotId: string) => void; isDeletingSnapshot?: boolean; } @@ -28,43 +26,11 @@ const treeProps = { stateClassName: "flex-1 min-h-0", } as const; -interface ScheduleAwareTreeBrowserProps { - scheduleShortId: string; - repositoryId: string; - snapshotId: string; -} - -const ScheduleAwareTreeBrowser = ({ scheduleShortId, repositoryId, snapshotId }: ScheduleAwareTreeBrowserProps) => { - const { data: schedule, isPending } = useQuery({ - ...getBackupScheduleOptions({ path: { shortId: scheduleShortId } }), - retry: false, - }); - - if (isPending) { - return ; - } - - return ( - - ); -}; - -const TreeBrowserFallback = () => ( -
-

Loading volume info...

-
-); - export const SnapshotFileBrowser = (props: Props) => { - const { snapshot, repositoryId, backupId, basePath, onDeleteSnapshot, isDeletingSnapshot } = props; + const { snapshot, repositoryId, backupId, displayBasePath, onDeleteSnapshot, isDeletingSnapshot } = props; const { formatDateTime } = useTimeFormat(); - const scheduleShortId = !basePath ? backupId || snapshot.tags?.[0] : undefined; + const queryBasePath = findCommonAncestor(snapshot.paths); return (
@@ -110,27 +76,13 @@ export const SnapshotFileBrowser = (props: Props) => {
- {basePath ? ( - - ) : scheduleShortId ? ( - - ) : ( - - )} + diff --git a/app/client/modules/backups/routes/__tests__/backup-details.test.tsx b/app/client/modules/backups/routes/__tests__/backup-details.test.tsx new file mode 100644 index 00000000..7111f932 --- /dev/null +++ b/app/client/modules/backups/routes/__tests__/backup-details.test.tsx @@ -0,0 +1,107 @@ +import { afterEach, describe, expect, mock, test } from "bun:test"; +import { fromAny } from "@total-typescript/shoehorn"; +import { HttpResponse, http, server } from "~/test/msw/server"; +import { cleanup, render, screen } from "~/test/test-utils"; + +const schedule = { + shortId: "backup-1", + name: "Backup 1", + repositoryId: "repo-1", + repository: { shortId: "repo-1", name: "Repo 1" }, + volume: { + shortId: "vol-1", + name: "Volume 1", + config: { backend: "directory", path: "/mnt" }, + }, + enabled: true, + cronExpression: "0 0 * * *", + retentionPolicy: null, + includePaths: ["/project"], + includePatterns: [], + excludePatterns: [], + excludeIfPresent: [], + oneFileSystem: false, + customResticParams: [], +}; + +const snapshot = { + short_id: "snap-1", + paths: ["/mnt/project"], + tags: ["backup-1"], + time: "2026-03-26T00:00:00.000Z", + summary: {}, +}; + +await mock.module("@tanstack/react-router", () => ({ + useNavigate: () => mock(() => {}), + useSearch: () => ({}), +})); + +await mock.module("~/client/components/backup-summary-card", () => ({ + BackupSummaryCard: () => null, +})); + +await mock.module("~/client/modules/backups/components/schedule-summary", () => ({ + ScheduleSummary: () => null, +})); + +await mock.module("~/client/modules/backups/components/snapshot-file-browser", () => ({ + SnapshotFileBrowser: ({ displayBasePath }: { displayBasePath?: string }) => ( +
{displayBasePath ? `display-base-path:${displayBasePath}` : "display-base-path:missing"}
+ ), +})); + +await mock.module("~/client/modules/backups/components/snapshot-timeline", () => ({ + SnapshotTimeline: () => null, +})); + +await mock.module("~/client/modules/backups/components/schedule-notifications-config", () => ({ + ScheduleNotificationsConfig: () => null, +})); + +await mock.module("~/client/modules/backups/components/schedule-mirrors-config", () => ({ + ScheduleMirrorsConfig: () => null, +})); + +import { ScheduleDetailsPage } from "../backup-details"; + +const mockScheduleDetailsRequests = () => { + server.use( + http.get("/api/v1/backups/:shortId", () => { + return HttpResponse.json(schedule); + }), + http.get("/api/v1/repositories/:shortId/snapshots", () => { + return HttpResponse.json([snapshot]); + }), + ); +}; + +afterEach(() => { + cleanup(); +}); + +describe("ScheduleDetailsPage", () => { + test("shows the selected snapshot from the volume root on the backup details page", async () => { + mockScheduleDetailsRequests(); + + render( + , + { withSuspense: true }, + ); + + expect(await screen.findByText("display-base-path:/mnt")).toBeTruthy(); + }); +}); diff --git a/app/client/modules/backups/routes/backup-details.tsx b/app/client/modules/backups/routes/backup-details.tsx index a581a7b3..48b6bca6 100644 --- a/app/client/modules/backups/routes/backup-details.tsx +++ b/app/client/modules/backups/routes/backup-details.tsx @@ -34,6 +34,7 @@ import { ScheduleNotificationsConfig } from "../components/schedule-notification import { ScheduleMirrorsConfig } from "../components/schedule-mirrors-config"; import { BackupSummaryCard } from "~/client/components/backup-summary-card"; import { cn } from "~/client/lib/utils"; +import { getVolumeMountPath } from "~/client/lib/volume-path"; import type { BackupSchedule, NotificationDestination, @@ -303,6 +304,7 @@ export function ScheduleDetailsPage(props: Props) { snapshot={selectedSnapshot} repositoryId={schedule.repository.shortId} backupId={schedule.shortId} + displayBasePath={getVolumeMountPath(schedule.volume)} onDeleteSnapshot={handleDeleteSnapshot} isDeletingSnapshot={deleteSnapshot.isPending} /> diff --git a/app/client/modules/repositories/routes/restore-snapshot.tsx b/app/client/modules/repositories/routes/restore-snapshot.tsx index 2459aa21..24761b42 100644 --- a/app/client/modules/repositories/routes/restore-snapshot.tsx +++ b/app/client/modules/repositories/routes/restore-snapshot.tsx @@ -5,11 +5,20 @@ type Props = { repository: Repository; snapshotId: string; returnPath: string; - basePath?: string; + queryBasePath?: string; + displayBasePath?: string; }; export function RestoreSnapshotPage(props: Props) { - const { returnPath, snapshotId, repository, basePath } = props; + const { returnPath, snapshotId, repository, queryBasePath, displayBasePath } = props; - return ; + return ( + + ); } diff --git a/app/client/modules/repositories/routes/snapshot-details.tsx b/app/client/modules/repositories/routes/snapshot-details.tsx index 43a6f301..832a3db4 100644 --- a/app/client/modules/repositories/routes/snapshot-details.tsx +++ b/app/client/modules/repositories/routes/snapshot-details.tsx @@ -101,7 +101,7 @@ export function SnapshotDetailsPage({ repositoryId, snapshotId, initialSnapshot ) : ( diff --git a/app/routes/(dashboard)/backups/$backupId/$snapshotId.restore.tsx b/app/routes/(dashboard)/backups/$backupId/$snapshotId.restore.tsx index e9f80b4f..2082f56f 100644 --- a/app/routes/(dashboard)/backups/$backupId/$snapshotId.restore.tsx +++ b/app/routes/(dashboard)/backups/$backupId/$snapshotId.restore.tsx @@ -3,6 +3,7 @@ import { getBackupSchedule } from "~/client/api-client"; import { getRepositoryOptions, getSnapshotDetailsOptions } from "~/client/api-client/@tanstack/react-query.gen"; import { RestoreSnapshotPage } from "~/client/modules/repositories/routes/restore-snapshot"; import { getVolumeMountPath } from "~/client/lib/volume-path"; +import { findCommonAncestor } from "@zerobyte/core/utils"; export const Route = createFileRoute("/(dashboard)/backups/$backupId/$snapshotId/restore")({ component: RouteComponent, @@ -29,7 +30,8 @@ export const Route = createFileRoute("/(dashboard)/backups/$backupId/$snapshotId snapshot, repository, schedule: schedule.data, - basePath: getVolumeMountPath(schedule.data.volume), + queryBasePath: findCommonAncestor(snapshot.paths), + displayBasePath: getVolumeMountPath(schedule.data.volume), }; }, head: ({ params }) => ({ @@ -53,14 +55,15 @@ export const Route = createFileRoute("/(dashboard)/backups/$backupId/$snapshotId function RouteComponent() { const { backupId, snapshotId } = Route.useParams(); - const { repository, basePath } = Route.useLoaderData(); + const { repository, queryBasePath, displayBasePath } = Route.useLoaderData(); return ( ); } diff --git a/app/routes/(dashboard)/repositories/$repositoryId/$snapshotId/restore.tsx b/app/routes/(dashboard)/repositories/$repositoryId/$snapshotId/restore.tsx index cbf4f488..f5c298c1 100644 --- a/app/routes/(dashboard)/repositories/$repositoryId/$snapshotId/restore.tsx +++ b/app/routes/(dashboard)/repositories/$repositoryId/$snapshotId/restore.tsx @@ -3,6 +3,7 @@ import { getBackupSchedule } from "~/client/api-client"; import { getRepositoryOptions, getSnapshotDetailsOptions } from "~/client/api-client/@tanstack/react-query.gen"; import { RestoreSnapshotPage } from "~/client/modules/repositories/routes/restore-snapshot"; import { getVolumeMountPath } from "~/client/lib/volume-path"; +import { findCommonAncestor } from "@zerobyte/core/utils"; export const Route = createFileRoute("/(dashboard)/repositories/$repositoryId/$snapshotId/restore")({ component: RouteComponent, @@ -15,16 +16,16 @@ export const Route = createFileRoute("/(dashboard)/repositories/$repositoryId/$s context.queryClient.ensureQueryData({ ...getRepositoryOptions({ path: { shortId: params.repositoryId } }) }), ]); - let basePath: string | undefined; + let displayBasePath: string | undefined; const scheduleShortId = snapshot.tags?.[0]; if (scheduleShortId) { const scheduleRes = await getBackupSchedule({ path: { shortId: scheduleShortId } }); if (scheduleRes.data) { - basePath = getVolumeMountPath(scheduleRes.data.volume); + displayBasePath = getVolumeMountPath(scheduleRes.data.volume); } } - return { snapshot, repository, basePath }; + return { snapshot, repository, queryBasePath: findCommonAncestor(snapshot.paths), displayBasePath }; }, staticData: { breadcrumb: (match) => [ @@ -47,14 +48,15 @@ export const Route = createFileRoute("/(dashboard)/repositories/$repositoryId/$s function RouteComponent() { const { repositoryId, snapshotId } = Route.useParams(); - const { repository, basePath } = Route.useLoaderData(); + const { repository, queryBasePath, displayBasePath } = Route.useLoaderData(); return ( ); } diff --git a/app/test/msw/server.ts b/app/test/msw/server.ts new file mode 100644 index 00000000..5f2a3ff6 --- /dev/null +++ b/app/test/msw/server.ts @@ -0,0 +1,5 @@ +import { setupServer } from "msw/node"; + +export { HttpResponse, http } from "msw"; + +export const server = setupServer(); diff --git a/app/test/setup-client.ts b/app/test/setup-client.ts index a0faa585..258a72c5 100644 --- a/app/test/setup-client.ts +++ b/app/test/setup-client.ts @@ -1,4 +1,24 @@ import "./setup.ts"; import { GlobalRegistrator } from "@happy-dom/global-registrator"; +import { afterAll, afterEach, beforeAll } from "bun:test"; +import { client } from "~/client/api-client/client.gen"; +import { server } from "~/test/msw/server"; GlobalRegistrator.register({ url: "http://localhost:3000" }); + +client.setConfig({ + baseUrl: "http://localhost:3000", + credentials: "include", +}); + +beforeAll(() => { + server.listen({ onUnhandledRequest: "error" }); +}); + +afterEach(() => { + server.resetHandlers(); +}); + +afterAll(() => { + server.close(); +}); diff --git a/app/test/test-utils.tsx b/app/test/test-utils.tsx new file mode 100644 index 00000000..63e0537b --- /dev/null +++ b/app/test/test-utils.tsx @@ -0,0 +1,94 @@ +import { MutationCache, QueryClient, QueryClientProvider } from "@tanstack/react-query"; +import { + render as testingLibraryRender, + renderHook as testingLibraryRenderHook, + type RenderHookOptions, + type RenderOptions, +} from "@testing-library/react"; +import testingLibraryUserEvent from "@testing-library/user-event"; +import { Suspense, type ReactElement, type ReactNode } from "react"; +import { logger } from "~/client/lib/logger"; + +type TestProviderOptions = { + queryClient?: QueryClient; + withSuspense?: boolean; + suspenseFallback?: ReactNode; +}; + +type TestRenderOptions = Omit & TestProviderOptions; +type TestRenderHookOptions = Omit, "wrapper"> & TestProviderOptions; + +export const createTestQueryClient = () => { + let queryClient: QueryClient; + + queryClient = new QueryClient({ + defaultOptions: { + queries: { + retry: false, + gcTime: Infinity, + }, + mutations: { + gcTime: Infinity, + }, + }, + mutationCache: new MutationCache({ + onSuccess: () => { + void queryClient.invalidateQueries(); + }, + onError: (error) => { + logger.error("Mutation error:", error); + void queryClient.invalidateQueries(); + }, + }), + }); + + return queryClient; +}; + +const createWrapper = (options: TestProviderOptions = {}) => { + const { queryClient = createTestQueryClient(), withSuspense = false, suspenseFallback = null } = options; + + const Wrapper = ({ children }: { children: ReactNode }) => { + return ( + + {withSuspense ? {children} : children} + + ); + }; + + return { queryClient, Wrapper }; +}; + +const customRender = (ui: ReactElement, options: TestRenderOptions = {}) => { + const { queryClient, withSuspense, suspenseFallback, ...renderOptions } = options; + const wrapper = createWrapper({ queryClient, withSuspense, suspenseFallback }); + + return { + queryClient: wrapper.queryClient, + ...testingLibraryRender(ui, { + wrapper: wrapper.Wrapper, + ...renderOptions, + }), + }; +}; + +const customRenderHook = ( + callback: (initialProps: Props) => Result, + options: TestRenderHookOptions = {}, +) => { + const { queryClient, withSuspense, suspenseFallback, ...renderOptions } = options; + const wrapper = createWrapper({ queryClient, withSuspense, suspenseFallback }); + + return { + queryClient: wrapper.queryClient, + ...testingLibraryRenderHook(callback, { + wrapper: wrapper.Wrapper, + ...renderOptions, + }), + }; +}; + +export * from "@testing-library/react"; + +export const userEvent = testingLibraryUserEvent.setup(); +export { customRender as render, customRenderHook as renderHook }; diff --git a/bun.lock b/bun.lock index b2c5c426..4cc7eb9e 100644 --- a/bun.lock +++ b/bun.lock @@ -83,6 +83,7 @@ "@tailwindcss/vite": "^4.2.2", "@testing-library/dom": "^10.4.1", "@testing-library/react": "^16.3.2", + "@testing-library/user-event": "^14.6.1", "@total-typescript/shoehorn": "^0.1.2", "@types/babel__core": "^7.20.5", "@types/bun": "^1.3.11", @@ -97,6 +98,7 @@ "drizzle-kit": "^1.0.0-beta.16-ea816b6", "lefthook": "^2.1.4", "lightningcss": "^1.32.0", + "msw": "^2.12.14", "nitro": "^3.0.1-alpha.2", "oxfmt": "^0.41.0", "oxlint": "^1.56.0", @@ -357,9 +359,9 @@ "@inquirer/checkbox": ["@inquirer/checkbox@5.1.2", "", { "dependencies": { "@inquirer/ansi": "^2.0.4", "@inquirer/core": "^11.1.7", "@inquirer/figures": "^2.0.4", "@inquirer/type": "^4.0.4" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-PubpMPO2nJgMufkoB3P2wwxNXEMUXnBIKi/ACzDUYfaoPuM7gSTmuxJeMscoLVEsR4qqrCMf5p0SiYGWnVJ8kw=="], - "@inquirer/confirm": ["@inquirer/confirm@6.0.10", "", { "dependencies": { "@inquirer/core": "^11.1.7", "@inquirer/type": "^4.0.4" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-tiNyA73pgpQ0FQ7axqtoLUe4GDYjNCDcVsbgcA5anvwg2z6i+suEngLKKJrWKJolT//GFPZHwN30binDIHgSgQ=="], + "@inquirer/confirm": ["@inquirer/confirm@5.1.21", "", { "dependencies": { "@inquirer/core": "^10.3.2", "@inquirer/type": "^3.0.10" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-KR8edRkIsUayMXV+o3Gv+q4jlhENF9nMYUZs9PA2HzrXeHI8M5uDag70U7RJn9yyiMZSbtF5/UexBtAVtZGSbQ=="], - "@inquirer/core": ["@inquirer/core@11.1.7", "", { "dependencies": { "@inquirer/ansi": "^2.0.4", "@inquirer/figures": "^2.0.4", "@inquirer/type": "^4.0.4", "cli-width": "^4.1.0", "fast-wrap-ansi": "^0.2.0", "mute-stream": "^3.0.0", "signal-exit": "^4.1.0" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-1BiBNDk9btIwYIzNZpkikIHXWeNzNncJePPqwDyVMhXhD1ebqbpn1mKGctpoqAbzywZfdG0O4tvmsGIcOevAPQ=="], + "@inquirer/core": ["@inquirer/core@10.3.2", "", { "dependencies": { "@inquirer/ansi": "^1.0.2", "@inquirer/figures": "^1.0.15", "@inquirer/type": "^3.0.10", "cli-width": "^4.1.0", "mute-stream": "^2.0.0", "signal-exit": "^4.1.0", "wrap-ansi": "^6.2.0", "yoctocolors-cjs": "^2.1.3" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-43RTuEbfP8MbKzedNqBrlhhNKVwoK//vUFNW3Q3vZ88BLcrs4kYpGg+B2mm5p2K/HfygoCxuKwJJiv8PbGmE0A=="], "@inquirer/editor": ["@inquirer/editor@5.0.10", "", { "dependencies": { "@inquirer/core": "^11.1.7", "@inquirer/external-editor": "^2.0.4", "@inquirer/type": "^4.0.4" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-VJx4XyaKea7t8hEApTw5dxeIyMtWXre2OiyJcICCRZI4hkoHsMoCnl/KbUnJJExLbH9csLLHMVR144ZhFE1CwA=="], @@ -383,7 +385,7 @@ "@inquirer/select": ["@inquirer/select@5.1.2", "", { "dependencies": { "@inquirer/ansi": "^2.0.4", "@inquirer/core": "^11.1.7", "@inquirer/figures": "^2.0.4", "@inquirer/type": "^4.0.4" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-kTK8YIkHV+f02y7bWCh7E0u2/11lul5WepVTclr3UMBtBr05PgcZNWfMa7FY57ihpQFQH/spLMHTcr0rXy50tA=="], - "@inquirer/type": ["@inquirer/type@4.0.4", "", { "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-PamArxO3cFJZoOzspzo6cxVlLeIftyBsZw/S9bKY5DzxqJVZgjoj1oP8d0rskKtp7sZxBycsoer1g6UeJV1BBA=="], + "@inquirer/type": ["@inquirer/type@3.0.10", "", { "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-BvziSRxfz5Ov8ch0z/n3oijRSEcEsHnhggm4xFZe93DHcUCTlutlq9Ox4SVENAfcRD22UQq7T/atg9Wr3k09eA=="], "@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="], @@ -431,6 +433,8 @@ "@mrleebo/prisma-ast": ["@mrleebo/prisma-ast@0.13.1", "", { "dependencies": { "chevrotain": "^10.5.0", "lilconfig": "^2.1.0" } }, "sha512-XyroGQXcHrZdvmrGJvsA9KNeOOgGMg1Vg9OlheUsBOSKznLMDl+YChxbkboRHvtFYJEMRYmlV3uoo/njCw05iw=="], + "@mswjs/interceptors": ["@mswjs/interceptors@0.41.3", "", { "dependencies": { "@open-draft/deferred-promise": "^2.2.0", "@open-draft/logger": "^0.3.0", "@open-draft/until": "^2.0.0", "is-node-process": "^1.2.0", "outvariant": "^1.4.3", "strict-event-emitter": "^0.5.1" } }, "sha512-cXu86tF4VQVfwz8W1SPbhoRyHJkti6mjH/XJIxp40jhO4j2k1m4KYrEykxqWPkFF3vrK4rgQppBh//AwyGSXPA=="], + "@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.1", "", { "dependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1", "@tybys/wasm-util": "^0.10.1" } }, "sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A=="], "@neon-rs/load": ["@neon-rs/load@0.0.4", "", {}, "sha512-kTPhdZyTQxB+2wpiRcFWrDcejc4JI6tkPuS7UZCG4l6Zvc5kU/gGQ/ozvHTh1XR5tS+UlfAfGuPajjzQjCiHCw=="], @@ -447,6 +451,12 @@ "@oozcitak/util": ["@oozcitak/util@10.0.0", "", {}, "sha512-hAX0pT/73190NLqBPPWSdBVGtbY6VOhWYK3qqHqtXQ1gK7kS2yz4+ivsN07hpJ6I3aeMtKP6J6npsEKOAzuTLA=="], + "@open-draft/deferred-promise": ["@open-draft/deferred-promise@2.2.0", "", {}, "sha512-CecwLWx3rhxVQF6V4bAgPS5t+So2sTbPgAzafKkVizyi7tlwpcFpdFqq+wqF2OwNBmqFuu6tOyouTuxgpMfzmA=="], + + "@open-draft/logger": ["@open-draft/logger@0.3.0", "", { "dependencies": { "is-node-process": "^1.2.0", "outvariant": "^1.4.0" } }, "sha512-X2g45fzhxH238HKO4xbSr7+wBS8Fvw6ixhTDuvLd5mqh6bJJCFAPwU9mPDxbcrRtfxv4u5IHCEH77BmxvXmmxQ=="], + + "@open-draft/until": ["@open-draft/until@2.1.0", "", {}, "sha512-U69T3ItWHvLwGg5eJ0n3I62nWuE6ilHlmz7zM0npLBRvPRd7e6NYmg54vvRtP5mZG7kZqZCFVdsTWo7BPtBujg=="], + "@oxc-minify/binding-android-arm-eabi": ["@oxc-minify/binding-android-arm-eabi@0.110.0", "", { "os": "android", "cpu": "arm" }, "sha512-43fMTO8/5bMlqfOiNSZNKUzIqeLIYuB9Hr1Ohyf58B1wU11S2dPGibTXOGNaWsfgHy99eeZ1bSgeIHy/fEYqbw=="], "@oxc-minify/binding-android-arm64": ["@oxc-minify/binding-android-arm64@0.110.0", "", { "os": "android", "cpu": "arm64" }, "sha512-5oQrnn9eK/ccOp80PTrNj0Vq893NPNNRryjGpOIVsYNgWFuoGCfpnKg68oEFcN8bArizYAqw4nvgHljEnar69w=="], @@ -915,6 +925,8 @@ "@testing-library/react": ["@testing-library/react@16.3.2", "", { "dependencies": { "@babel/runtime": "^7.12.5" }, "peerDependencies": { "@testing-library/dom": "^10.0.0", "@types/react": "^18.0.0 || ^19.0.0", "@types/react-dom": "^18.0.0 || ^19.0.0", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-XU5/SytQM+ykqMnAnvB2umaJNIOsLF3PVv//1Ew4CTcpz0/BRyy/af40qqrt7SjKpDdT1saBMc42CUok5gaw+g=="], + "@testing-library/user-event": ["@testing-library/user-event@14.6.1", "", { "peerDependencies": { "@testing-library/dom": ">=7.21.4" } }, "sha512-vq7fv0rnt+QTXgPxr5Hjc210p6YKq2kmdziLgnsZGgLJ9e6VAShx1pACLuRjd/AS/sr7phAR58OIIpf0LlmQNw=="], + "@total-typescript/shoehorn": ["@total-typescript/shoehorn@0.1.2", "", {}, "sha512-p7nNZbOZIofpDNyP0u1BctFbjxD44Qc+oO5jufgQdFdGIXJLc33QRloJpq7k5T59CTgLWfQSUxsuqLcmeurYRw=="], "@turbo/darwin-64": ["@turbo/darwin-64@2.8.20", "", { "os": "darwin", "cpu": "x64" }, "sha512-FQ9EX1xMU5nbwjxXxM3yU88AQQ6Sqc6S44exPRroMcx9XZHqqppl5ymJF0Ig/z3nvQNwDmz1Gsnvxubo+nXWjQ=="], @@ -989,6 +1001,8 @@ "@types/semver": ["@types/semver@7.7.1", "", {}, "sha512-FmgJfu+MOcQ370SD0ev7EI8TlCAfKYU+B4m5T3yXc1CiRN94g/SZPtsCkk506aUDtlMnFZvasDwHHUcZUEaYuA=="], + "@types/statuses": ["@types/statuses@2.0.6", "", {}, "sha512-xMAgYwceFhRA2zY+XbEA7mxYbA093wdiW8Vu6gZPGWy9cmOyU9XesH1tNcEWsKFd5Vzrqx5T3D38PWx1FIIXkA=="], + "@types/unist": ["@types/unist@3.0.3", "", {}, "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q=="], "@types/use-sync-external-store": ["@types/use-sync-external-store@0.0.6", "", {}, "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg=="], @@ -1111,8 +1125,14 @@ "cli-width": ["cli-width@4.1.0", "", {}, "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ=="], + "cliui": ["cliui@8.0.1", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", "wrap-ansi": "^7.0.0" } }, "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ=="], + "clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="], + "color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], + + "color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], + "color-support": ["color-support@1.1.3", "", { "bin": { "color-support": "bin.js" } }, "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg=="], "comma-separated-tokens": ["comma-separated-tokens@2.0.3", "", {}, "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg=="], @@ -1127,6 +1147,8 @@ "convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="], + "cookie": ["cookie@1.1.1", "", {}, "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ=="], + "cookie-es": ["cookie-es@2.0.0", "", {}, "sha512-RAj4E421UYRgqokKUmotqAwuplYw15qtdXfY+hGzgCJ/MBjCVZcSoHK/kH9kocfjRjcDME7IiDWR/1WX1TM2Pg=="], "cron-parser": ["cron-parser@5.5.0", "", { "dependencies": { "luxon": "^3.7.1" } }, "sha512-oML4lKUXxizYswqmxuOCpgFS8BNUJpIu6k/2HVHyaL8Ynnf3wdf9tkns0yRdJLSIjkJ+b0DXHMZEHGpMwjnPww=="], @@ -1231,6 +1253,8 @@ "electron-to-chromium": ["electron-to-chromium@1.5.302", "", {}, "sha512-sM6HAN2LyK82IyPBpznDRqlTQAtuSaO+ShzFiWTvoMJLHyZ+Y39r8VMfHzwbU8MVBzQ4Wdn85+wlZl2TLGIlwg=="], + "emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], + "empathic": ["empathic@2.0.0", "", {}, "sha512-i6UzDscO/XfAcNYD75CfICkmfLedpyPDdozrLMmQc5ORaQcdMoc21OnlEylMIqI7U8eniKrPMxxtj8k0vhmJhA=="], "encoding-sniffer": ["encoding-sniffer@0.2.1", "", { "dependencies": { "iconv-lite": "^0.6.3", "whatwg-encoding": "^3.1.1" } }, "sha512-5gvq20T6vfpekVtqrYQsSCFZ1wEg5+wW0/QaZMWkFr6BqD3NfKs0rLCx4rrVlSWJeZb5NBJgVLswK/w2MWU+Gw=="], @@ -1291,6 +1315,8 @@ "gensync": ["gensync@1.0.0-beta.2", "", {}, "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg=="], + "get-caller-file": ["get-caller-file@2.0.5", "", {}, "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg=="], + "get-nonce": ["get-nonce@1.0.1", "", {}, "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q=="], "get-port-please": ["get-port-please@3.2.0", "", {}, "sha512-I9QVvBw5U/hw3RmWpYKRumUeaDgxTPd401x364rLmWBJcOQ753eov1eTgzDqRG9bqFIfDc7gfzcQEWrUri3o1A=="], @@ -1307,6 +1333,8 @@ "graphmatch": ["graphmatch@1.1.1", "", {}, "sha512-5ykVn/EXM1hF0XCaWh05VbYvEiOL2lY1kBxZtaYsyvjp7cmWOU1XsAdfQBwClraEofXDT197lFbXOEVMHpvQOg=="], + "graphql": ["graphql@16.13.2", "", {}, "sha512-5bJ+nf/UCpAjHM8i06fl7eLyVC9iuNAjm9qzkiu2ZGhM0VscSvS6WDPfAwkdkBuoXGM9FJSbKl6wylMwP9Ktig=="], + "h3": ["h3@2.0.1-rc.14", "", { "dependencies": { "rou3": "^0.7.12", "srvx": "^0.11.2" }, "peerDependencies": { "crossws": "^0.4.1" }, "optionalPeers": ["crossws"], "bin": { "h3": "bin/h3.mjs" } }, "sha512-163qbGmTr/9rqQRNuqMqtgXnOUAkE4KTdauiC9y0E5iG1I65kte9NyfWvZw5RTDMt6eY+DtyoNzrQ9wA2BfvGQ=="], "h3-v2": ["h3@2.0.1-rc.16", "", { "dependencies": { "rou3": "^0.8.0", "srvx": "^0.11.9" }, "peerDependencies": { "crossws": "^0.4.1" }, "optionalPeers": ["crossws"], "bin": { "h3": "bin/h3.mjs" } }, "sha512-h+pjvyujdo9way8qj6FUbhaQcHlR8FEq65EhTX9ViT5pK8aLj68uFl4hBkF+hsTJAH+H1END2Yv6hTIsabGfag=="], @@ -1317,6 +1345,8 @@ "hast-util-whitespace": ["hast-util-whitespace@3.0.0", "", { "dependencies": { "@types/hast": "^3.0.0" } }, "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw=="], + "headers-polyfill": ["headers-polyfill@4.0.3", "", {}, "sha512-IScLbePpkvO846sIwOtOTDjutRMWdXdJmXdMvk6gCBHxFO8d+QKOQedyZSxFTTFYRSmlgSTDtXqqq4pcenBXLQ=="], + "hono": ["hono@4.12.8", "", {}, "sha512-VJCEvtrezO1IAR+kqEYnxUOoStaQPGrCmX3j4wDTNOcD1uRPFpGlwQUIW8niPuvHXaTUxeOUl5MMDGrl+tmO9A=="], "hono-openapi": ["hono-openapi@1.3.0", "", { "peerDependencies": { "@hono/standard-validator": "^0.2.0", "@standard-community/standard-json": "^0.3.5", "@standard-community/standard-openapi": "^0.2.9", "@types/json-schema": "^7.0.15", "hono": "^4.8.3", "openapi-types": "^12.1.3" }, "optionalPeers": ["@hono/standard-validator", "hono"] }, "sha512-xDvCWpWEIv0weEmnl3EjRQzqbHIO8LnfzMuYOCmbuyE5aes6aXxLg4vM3ybnoZD5TiTUkA6PuRQPJs3R7WRBig=="], @@ -1361,6 +1391,8 @@ "is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="], + "is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="], + "is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="], "is-hexadecimal": ["is-hexadecimal@2.0.1", "", {}, "sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg=="], @@ -1369,6 +1401,8 @@ "is-inside-container": ["is-inside-container@1.0.0", "", { "dependencies": { "is-docker": "^3.0.0" }, "bin": { "is-inside-container": "cli.js" } }, "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA=="], + "is-node-process": ["is-node-process@1.2.0", "", {}, "sha512-Vg4o6/fqPxIjtxgUH5QLJhwZ7gW5diGCVlXpuUfELC62CuxM1iHcRe51f2W1FDy04Ai4KJkagKjx3XaqyfRKXw=="], + "is-number": ["is-number@7.0.0", "", {}, "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="], "is-plain-obj": ["is-plain-obj@4.1.0", "", {}, "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg=="], @@ -1589,7 +1623,9 @@ "mssql": ["mssql@11.0.1", "", { "dependencies": { "@tediousjs/connection-string": "^0.5.0", "commander": "^11.0.0", "debug": "^4.3.3", "rfdc": "^1.3.0", "tarn": "^3.0.2", "tedious": "^18.2.1" }, "bin": { "mssql": "bin/mssql" } }, "sha512-KlGNsugoT90enKlR8/G36H0kTxPthDhmtNUCwEHvgRza5Cjpjoj+P2X6eMpFUDN7pFrJZsKadL4x990G8RBE1w=="], - "mute-stream": ["mute-stream@3.0.0", "", {}, "sha512-dkEJPVvun4FryqBmZ5KhDo0K9iDXAwn08tMLDinNdRBNPcYEDiWYysLcc6k3mjTMlbP9KyylvRpd4wFtwrT9rw=="], + "msw": ["msw@2.12.14", "", { "dependencies": { "@inquirer/confirm": "^5.0.0", "@mswjs/interceptors": "^0.41.2", "@open-draft/deferred-promise": "^2.2.0", "@types/statuses": "^2.0.6", "cookie": "^1.0.2", "graphql": "^16.12.0", "headers-polyfill": "^4.0.2", "is-node-process": "^1.2.0", "outvariant": "^1.4.3", "path-to-regexp": "^6.3.0", "picocolors": "^1.1.1", "rettime": "^0.10.1", "statuses": "^2.0.2", "strict-event-emitter": "^0.5.1", "tough-cookie": "^6.0.0", "type-fest": "^5.2.0", "until-async": "^3.0.2", "yargs": "^17.7.2" }, "peerDependencies": { "typescript": ">= 4.8.x" }, "optionalPeers": ["typescript"], "bin": { "msw": "cli/index.js" } }, "sha512-4KXa4nVBIBjbDbd7vfQNuQ25eFxug0aropCQFoI0JdOBuJWamkT1yLVIWReFI8SiTRc+H1hKzaNk+cLk2N9rtQ=="], + + "mute-stream": ["mute-stream@2.0.0", "", {}, "sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA=="], "mysql2": ["mysql2@3.15.3", "", { "dependencies": { "aws-ssl-profiles": "^1.1.1", "denque": "^2.1.0", "generate-function": "^2.3.1", "iconv-lite": "^0.7.0", "long": "^5.2.1", "lru.min": "^1.0.0", "named-placeholders": "^1.1.3", "seq-queue": "^0.0.5", "sqlstring": "^2.3.2" } }, "sha512-FBrGau0IXmuqg4haEZRBfHNWB5mUARw6hNwPDXXGg0XzVJ50mr/9hb267lvpVMnhZ1FON3qNd4Xfcez1rbFwSg=="], @@ -1633,6 +1669,8 @@ "openapi-types": ["openapi-types@12.1.3", "", {}, "sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw=="], + "outvariant": ["outvariant@1.4.3", "", {}, "sha512-+Sl2UErvtsoajRDKCE5/dBz4DIvHXQQnAxtQTF04OJxY0+DyZXSo5P5Bb7XYWOh81syohlYL24hbDwxedPUJCA=="], + "oxc-minify": ["oxc-minify@0.110.0", "", { "optionalDependencies": { "@oxc-minify/binding-android-arm-eabi": "0.110.0", "@oxc-minify/binding-android-arm64": "0.110.0", "@oxc-minify/binding-darwin-arm64": "0.110.0", "@oxc-minify/binding-darwin-x64": "0.110.0", "@oxc-minify/binding-freebsd-x64": "0.110.0", "@oxc-minify/binding-linux-arm-gnueabihf": "0.110.0", "@oxc-minify/binding-linux-arm-musleabihf": "0.110.0", "@oxc-minify/binding-linux-arm64-gnu": "0.110.0", "@oxc-minify/binding-linux-arm64-musl": "0.110.0", "@oxc-minify/binding-linux-ppc64-gnu": "0.110.0", "@oxc-minify/binding-linux-riscv64-gnu": "0.110.0", "@oxc-minify/binding-linux-riscv64-musl": "0.110.0", "@oxc-minify/binding-linux-s390x-gnu": "0.110.0", "@oxc-minify/binding-linux-x64-gnu": "0.110.0", "@oxc-minify/binding-linux-x64-musl": "0.110.0", "@oxc-minify/binding-openharmony-arm64": "0.110.0", "@oxc-minify/binding-wasm32-wasi": "0.110.0", "@oxc-minify/binding-win32-arm64-msvc": "0.110.0", "@oxc-minify/binding-win32-ia32-msvc": "0.110.0", "@oxc-minify/binding-win32-x64-msvc": "0.110.0" } }, "sha512-KWGTzPo83QmGrXC4ml83PM9HDwUPtZFfasiclUvTV4i3/0j7xRRqINVkrL77CbQnoWura3CMxkRofjQKVDuhBw=="], "oxc-transform": ["oxc-transform@0.110.0", "", { "optionalDependencies": { "@oxc-transform/binding-android-arm-eabi": "0.110.0", "@oxc-transform/binding-android-arm64": "0.110.0", "@oxc-transform/binding-darwin-arm64": "0.110.0", "@oxc-transform/binding-darwin-x64": "0.110.0", "@oxc-transform/binding-freebsd-x64": "0.110.0", "@oxc-transform/binding-linux-arm-gnueabihf": "0.110.0", "@oxc-transform/binding-linux-arm-musleabihf": "0.110.0", "@oxc-transform/binding-linux-arm64-gnu": "0.110.0", "@oxc-transform/binding-linux-arm64-musl": "0.110.0", "@oxc-transform/binding-linux-ppc64-gnu": "0.110.0", "@oxc-transform/binding-linux-riscv64-gnu": "0.110.0", "@oxc-transform/binding-linux-riscv64-musl": "0.110.0", "@oxc-transform/binding-linux-s390x-gnu": "0.110.0", "@oxc-transform/binding-linux-x64-gnu": "0.110.0", "@oxc-transform/binding-linux-x64-musl": "0.110.0", "@oxc-transform/binding-openharmony-arm64": "0.110.0", "@oxc-transform/binding-wasm32-wasi": "0.110.0", "@oxc-transform/binding-win32-arm64-msvc": "0.110.0", "@oxc-transform/binding-win32-ia32-msvc": "0.110.0", "@oxc-transform/binding-win32-x64-msvc": "0.110.0" } }, "sha512-/fymQNzzUoKZweH0nC5yvbI2eR0yWYusT9TEKDYVgOgYrf9Qmdez9lUFyvxKR9ycx+PTHi/reIOzqf3wkShQsw=="], @@ -1655,6 +1693,8 @@ "path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="], + "path-to-regexp": ["path-to-regexp@6.3.0", "", {}, "sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ=="], + "pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="], "perfect-debounce": ["perfect-debounce@2.1.0", "", {}, "sha512-LjgdTytVFXeUgtHZr9WYViYSM/g8MkcTPYDlPa3cDqMirHjKiSZPYd6DoL7pK8AJQr+uWkQvCjHNdiMqsrJs+g=="], @@ -1743,12 +1783,16 @@ "remeda": ["remeda@2.33.4", "", {}, "sha512-ygHswjlc/opg2VrtiYvUOPLjxjtdKvjGz1/plDhkG66hjNjFr1xmfrs2ClNFo/E6TyUFiwYNh53bKV26oBoMGQ=="], + "require-directory": ["require-directory@2.1.1", "", {}, "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q=="], + "reselect": ["reselect@5.1.1", "", {}, "sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w=="], "resolve-pkg-maps": ["resolve-pkg-maps@1.0.0", "", {}, "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw=="], "retry": ["retry@0.12.0", "", {}, "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow=="], + "rettime": ["rettime@0.10.1", "", {}, "sha512-uyDrIlUEH37cinabq0AX4QbgV4HbFZ/gqoiunWQ1UqBtRvTTytwhNYjE++pO/MjPTZL5KQCf2bEoJ/BJNVQ5Kw=="], + "rfdc": ["rfdc@1.4.1", "", {}, "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA=="], "rolldown": ["rolldown@1.0.0-rc.9", "", { "dependencies": { "@oxc-project/types": "=0.115.0", "@rolldown/pluginutils": "1.0.0-rc.9" }, "optionalDependencies": { "@rolldown/binding-android-arm64": "1.0.0-rc.9", "@rolldown/binding-darwin-arm64": "1.0.0-rc.9", "@rolldown/binding-darwin-x64": "1.0.0-rc.9", "@rolldown/binding-freebsd-x64": "1.0.0-rc.9", "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.9", "@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.9", "@rolldown/binding-linux-arm64-musl": "1.0.0-rc.9", "@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.9", "@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.9", "@rolldown/binding-linux-x64-gnu": "1.0.0-rc.9", "@rolldown/binding-linux-x64-musl": "1.0.0-rc.9", "@rolldown/binding-openharmony-arm64": "1.0.0-rc.9", "@rolldown/binding-wasm32-wasi": "1.0.0-rc.9", "@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.9", "@rolldown/binding-win32-x64-msvc": "1.0.0-rc.9" }, "bin": { "rolldown": "bin/cli.mjs" } }, "sha512-9EbgWge7ZH+yqb4d2EnELAntgPTWbfL8ajiTW+SyhJEC4qhBbkCKbqFV4Ge4zmu5ziQuVbWxb/XwLZ+RIO7E8Q=="], @@ -1801,12 +1845,20 @@ "srvx": ["srvx@0.10.1", "", { "bin": { "srvx": "bin/srvx.mjs" } }, "sha512-A//xtfak4eESMWWydSRFUVvCTQbSwivnGCEf8YGPe2eHU0+Z6znfUTCPF0a7oV3sObSOcrXHlL6Bs9vVctfXdg=="], + "statuses": ["statuses@2.0.2", "", {}, "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw=="], + "std-env": ["std-env@3.10.0", "", {}, "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg=="], + "strict-event-emitter": ["strict-event-emitter@0.5.1", "", {}, "sha512-vMgjE/GGEPEFnhFub6pa4FmJBRBVOLpIII2hvCZ8Kzb7K0hlHo7mQv6xYrBvCL2LtAIBwFUK8wvuJgTVSQ5MFQ=="], + + "string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], + "string_decoder": ["string_decoder@1.3.0", "", { "dependencies": { "safe-buffer": "~5.2.0" } }, "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA=="], "stringify-entities": ["stringify-entities@4.0.4", "", { "dependencies": { "character-entities-html4": "^2.0.0", "character-entities-legacy": "^3.0.0" } }, "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg=="], + "strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], + "strnum": ["strnum@2.1.2", "", {}, "sha512-l63NF9y/cLROq/yqKXSLtcMeeyOfnSQlfMSlzFt/K73oIaD8DGaQWd7Z34X9GPiKqP5rbSh84Hl4bOlLcjiSrQ=="], "style-to-js": ["style-to-js@1.1.21", "", { "dependencies": { "style-to-object": "1.0.14" } }, "sha512-RjQetxJrrUJLQPHbLku6U/ocGtzyjbJMP9lCNK7Ag0CNh690nSH8woqWH9u16nMjYBAok+i7JO1NP2pOy8IsPQ=="], @@ -1843,6 +1895,8 @@ "to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="], + "tough-cookie": ["tough-cookie@6.0.1", "", { "dependencies": { "tldts": "^7.0.5" } }, "sha512-LktZQb3IeoUWB9lqR5EWTHgW/VTITCXg4D21M+lvybRVdylLrRMnqaIONLVb5mav8vM19m44HIcGq4qASeu2Qw=="], + "tr46": ["tr46@5.1.1", "", { "dependencies": { "punycode": "^2.3.1" } }, "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw=="], "trim-lines": ["trim-lines@3.0.1", "", {}, "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg=="], @@ -1885,6 +1939,8 @@ "unstorage": ["unstorage@2.0.0-alpha.5", "", { "peerDependencies": { "@azure/app-configuration": "^1.9.0", "@azure/cosmos": "^4.7.0", "@azure/data-tables": "^13.3.1", "@azure/identity": "^4.13.0", "@azure/keyvault-secrets": "^4.10.0", "@azure/storage-blob": "^12.29.1", "@capacitor/preferences": "^6.0.3 || ^7.0.0", "@deno/kv": ">=0.12.0", "@netlify/blobs": "^6.5.0 || ^7.0.0 || ^8.1.0 || ^9.0.0 || ^10.0.0", "@planetscale/database": "^1.19.0", "@upstash/redis": "^1.35.6", "@vercel/blob": ">=0.27.3", "@vercel/functions": "^2.2.12 || ^3.0.0", "@vercel/kv": "^1.0.1", "aws4fetch": "^1.0.20", "chokidar": "^4 || ^5", "db0": ">=0.3.4", "idb-keyval": "^6.2.2", "ioredis": "^5.8.2", "lru-cache": "^11.2.2", "mongodb": "^6 || ^7", "ofetch": "*", "uploadthing": "^7.7.4" }, "optionalPeers": ["@azure/app-configuration", "@azure/cosmos", "@azure/data-tables", "@azure/identity", "@azure/keyvault-secrets", "@azure/storage-blob", "@capacitor/preferences", "@deno/kv", "@netlify/blobs", "@planetscale/database", "@upstash/redis", "@vercel/blob", "@vercel/functions", "@vercel/kv", "aws4fetch", "chokidar", "db0", "idb-keyval", "ioredis", "lru-cache", "mongodb", "ofetch", "uploadthing"] }, "sha512-Sj8btci21Twnd6M+N+MHhjg3fVn6lAPElPmvFTe0Y/wR0WImErUdA1PzlAaUavHylJ7uDiFwlZDQKm0elG4b7g=="], + "until-async": ["until-async@3.0.2", "", {}, "sha512-IiSk4HlzAMqTUseHHe3VhIGyuFmN90zMTpD3Z3y8jeQbzLIq500MVM7Jq2vUAnTKAFPJrqwkzr6PoTcPhGcOiw=="], + "update-browserslist-db": ["update-browserslist-db@1.2.3", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w=="], "use-callback-ref": ["use-callback-ref@1.3.3", "", { "dependencies": { "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg=="], @@ -1929,6 +1985,8 @@ "which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], + "wrap-ansi": ["wrap-ansi@6.2.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA=="], + "ws": ["ws@8.19.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg=="], "wsl-utils": ["wsl-utils@0.3.1", "", { "dependencies": { "is-wsl": "^3.1.0", "powershell-utils": "^0.1.0" } }, "sha512-g/eziiSUNBSsdDJtCLB8bdYEUMj4jR7AGeUo96p/3dTafgjHhpF4RiCFPiRILwjQoDXx5MqkBr4fwWtR3Ky4Wg=="], @@ -1943,10 +2001,18 @@ "xpath": ["xpath@0.0.32", "", {}, "sha512-rxMJhSIoiO8vXcWvSifKqhvV96GjiD5wYb8/QHdoRyQvraTpp4IEv944nhGausZZ3u7dhQXteZuZbaqfpB7uYw=="], + "y18n": ["y18n@5.0.8", "", {}, "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA=="], + "yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="], "yaml": ["yaml@2.8.2", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A=="], + "yargs": ["yargs@17.7.2", "", { "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.3", "y18n": "^5.0.5", "yargs-parser": "^21.1.1" } }, "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w=="], + + "yargs-parser": ["yargs-parser@21.1.1", "", {}, "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw=="], + + "yoctocolors-cjs": ["yoctocolors-cjs@2.1.3", "", {}, "sha512-U/PBtDf35ff0D8X8D0jfdzHYEPFxAI7jJlxZXwCSez5M3190m+QobIfh+sWDWSHMCWWJN2AWamkegn6vr6YBTw=="], + "zeptomatch": ["zeptomatch@2.1.0", "", { "dependencies": { "grammex": "^3.1.11", "graphmatch": "^1.1.0" } }, "sha512-KiGErG2J0G82LSpniV0CtIzjlJ10E04j02VOudJsPyPwNZgGnRKQy7I1R7GMyg/QswnE4l7ohSGrQbQbjXPPDA=="], "zod": ["zod@4.3.6", "", {}, "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg=="], @@ -1975,6 +2041,48 @@ "@hey-api/shared/semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="], + "@inquirer/checkbox/@inquirer/core": ["@inquirer/core@11.1.7", "", { "dependencies": { "@inquirer/ansi": "^2.0.4", "@inquirer/figures": "^2.0.4", "@inquirer/type": "^4.0.4", "cli-width": "^4.1.0", "fast-wrap-ansi": "^0.2.0", "mute-stream": "^3.0.0", "signal-exit": "^4.1.0" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-1BiBNDk9btIwYIzNZpkikIHXWeNzNncJePPqwDyVMhXhD1ebqbpn1mKGctpoqAbzywZfdG0O4tvmsGIcOevAPQ=="], + + "@inquirer/checkbox/@inquirer/type": ["@inquirer/type@4.0.4", "", { "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-PamArxO3cFJZoOzspzo6cxVlLeIftyBsZw/S9bKY5DzxqJVZgjoj1oP8d0rskKtp7sZxBycsoer1g6UeJV1BBA=="], + + "@inquirer/core/@inquirer/ansi": ["@inquirer/ansi@1.0.2", "", {}, "sha512-S8qNSZiYzFd0wAcyG5AXCvUHC5Sr7xpZ9wZ2py9XR88jUz8wooStVx5M6dRzczbBWjic9NP7+rY0Xi7qqK/aMQ=="], + + "@inquirer/core/@inquirer/figures": ["@inquirer/figures@1.0.15", "", {}, "sha512-t2IEY+unGHOzAaVM5Xx6DEWKeXlDDcNPeDyUpsRc6CUhBfU3VQOEl+Vssh7VNp1dR8MdUJBWhuObjXCsVpjN5g=="], + + "@inquirer/editor/@inquirer/core": ["@inquirer/core@11.1.7", "", { "dependencies": { "@inquirer/ansi": "^2.0.4", "@inquirer/figures": "^2.0.4", "@inquirer/type": "^4.0.4", "cli-width": "^4.1.0", "fast-wrap-ansi": "^0.2.0", "mute-stream": "^3.0.0", "signal-exit": "^4.1.0" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-1BiBNDk9btIwYIzNZpkikIHXWeNzNncJePPqwDyVMhXhD1ebqbpn1mKGctpoqAbzywZfdG0O4tvmsGIcOevAPQ=="], + + "@inquirer/editor/@inquirer/type": ["@inquirer/type@4.0.4", "", { "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-PamArxO3cFJZoOzspzo6cxVlLeIftyBsZw/S9bKY5DzxqJVZgjoj1oP8d0rskKtp7sZxBycsoer1g6UeJV1BBA=="], + + "@inquirer/expand/@inquirer/core": ["@inquirer/core@11.1.7", "", { "dependencies": { "@inquirer/ansi": "^2.0.4", "@inquirer/figures": "^2.0.4", "@inquirer/type": "^4.0.4", "cli-width": "^4.1.0", "fast-wrap-ansi": "^0.2.0", "mute-stream": "^3.0.0", "signal-exit": "^4.1.0" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-1BiBNDk9btIwYIzNZpkikIHXWeNzNncJePPqwDyVMhXhD1ebqbpn1mKGctpoqAbzywZfdG0O4tvmsGIcOevAPQ=="], + + "@inquirer/expand/@inquirer/type": ["@inquirer/type@4.0.4", "", { "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-PamArxO3cFJZoOzspzo6cxVlLeIftyBsZw/S9bKY5DzxqJVZgjoj1oP8d0rskKtp7sZxBycsoer1g6UeJV1BBA=="], + + "@inquirer/input/@inquirer/core": ["@inquirer/core@11.1.7", "", { "dependencies": { "@inquirer/ansi": "^2.0.4", "@inquirer/figures": "^2.0.4", "@inquirer/type": "^4.0.4", "cli-width": "^4.1.0", "fast-wrap-ansi": "^0.2.0", "mute-stream": "^3.0.0", "signal-exit": "^4.1.0" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-1BiBNDk9btIwYIzNZpkikIHXWeNzNncJePPqwDyVMhXhD1ebqbpn1mKGctpoqAbzywZfdG0O4tvmsGIcOevAPQ=="], + + "@inquirer/input/@inquirer/type": ["@inquirer/type@4.0.4", "", { "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-PamArxO3cFJZoOzspzo6cxVlLeIftyBsZw/S9bKY5DzxqJVZgjoj1oP8d0rskKtp7sZxBycsoer1g6UeJV1BBA=="], + + "@inquirer/number/@inquirer/core": ["@inquirer/core@11.1.7", "", { "dependencies": { "@inquirer/ansi": "^2.0.4", "@inquirer/figures": "^2.0.4", "@inquirer/type": "^4.0.4", "cli-width": "^4.1.0", "fast-wrap-ansi": "^0.2.0", "mute-stream": "^3.0.0", "signal-exit": "^4.1.0" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-1BiBNDk9btIwYIzNZpkikIHXWeNzNncJePPqwDyVMhXhD1ebqbpn1mKGctpoqAbzywZfdG0O4tvmsGIcOevAPQ=="], + + "@inquirer/number/@inquirer/type": ["@inquirer/type@4.0.4", "", { "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-PamArxO3cFJZoOzspzo6cxVlLeIftyBsZw/S9bKY5DzxqJVZgjoj1oP8d0rskKtp7sZxBycsoer1g6UeJV1BBA=="], + + "@inquirer/password/@inquirer/core": ["@inquirer/core@11.1.7", "", { "dependencies": { "@inquirer/ansi": "^2.0.4", "@inquirer/figures": "^2.0.4", "@inquirer/type": "^4.0.4", "cli-width": "^4.1.0", "fast-wrap-ansi": "^0.2.0", "mute-stream": "^3.0.0", "signal-exit": "^4.1.0" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-1BiBNDk9btIwYIzNZpkikIHXWeNzNncJePPqwDyVMhXhD1ebqbpn1mKGctpoqAbzywZfdG0O4tvmsGIcOevAPQ=="], + + "@inquirer/password/@inquirer/type": ["@inquirer/type@4.0.4", "", { "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-PamArxO3cFJZoOzspzo6cxVlLeIftyBsZw/S9bKY5DzxqJVZgjoj1oP8d0rskKtp7sZxBycsoer1g6UeJV1BBA=="], + + "@inquirer/prompts/@inquirer/confirm": ["@inquirer/confirm@6.0.10", "", { "dependencies": { "@inquirer/core": "^11.1.7", "@inquirer/type": "^4.0.4" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-tiNyA73pgpQ0FQ7axqtoLUe4GDYjNCDcVsbgcA5anvwg2z6i+suEngLKKJrWKJolT//GFPZHwN30binDIHgSgQ=="], + + "@inquirer/rawlist/@inquirer/core": ["@inquirer/core@11.1.7", "", { "dependencies": { "@inquirer/ansi": "^2.0.4", "@inquirer/figures": "^2.0.4", "@inquirer/type": "^4.0.4", "cli-width": "^4.1.0", "fast-wrap-ansi": "^0.2.0", "mute-stream": "^3.0.0", "signal-exit": "^4.1.0" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-1BiBNDk9btIwYIzNZpkikIHXWeNzNncJePPqwDyVMhXhD1ebqbpn1mKGctpoqAbzywZfdG0O4tvmsGIcOevAPQ=="], + + "@inquirer/rawlist/@inquirer/type": ["@inquirer/type@4.0.4", "", { "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-PamArxO3cFJZoOzspzo6cxVlLeIftyBsZw/S9bKY5DzxqJVZgjoj1oP8d0rskKtp7sZxBycsoer1g6UeJV1BBA=="], + + "@inquirer/search/@inquirer/core": ["@inquirer/core@11.1.7", "", { "dependencies": { "@inquirer/ansi": "^2.0.4", "@inquirer/figures": "^2.0.4", "@inquirer/type": "^4.0.4", "cli-width": "^4.1.0", "fast-wrap-ansi": "^0.2.0", "mute-stream": "^3.0.0", "signal-exit": "^4.1.0" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-1BiBNDk9btIwYIzNZpkikIHXWeNzNncJePPqwDyVMhXhD1ebqbpn1mKGctpoqAbzywZfdG0O4tvmsGIcOevAPQ=="], + + "@inquirer/search/@inquirer/type": ["@inquirer/type@4.0.4", "", { "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-PamArxO3cFJZoOzspzo6cxVlLeIftyBsZw/S9bKY5DzxqJVZgjoj1oP8d0rskKtp7sZxBycsoer1g6UeJV1BBA=="], + + "@inquirer/select/@inquirer/core": ["@inquirer/core@11.1.7", "", { "dependencies": { "@inquirer/ansi": "^2.0.4", "@inquirer/figures": "^2.0.4", "@inquirer/type": "^4.0.4", "cli-width": "^4.1.0", "fast-wrap-ansi": "^0.2.0", "mute-stream": "^3.0.0", "signal-exit": "^4.1.0" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-1BiBNDk9btIwYIzNZpkikIHXWeNzNncJePPqwDyVMhXhD1ebqbpn1mKGctpoqAbzywZfdG0O4tvmsGIcOevAPQ=="], + + "@inquirer/select/@inquirer/type": ["@inquirer/type@4.0.4", "", { "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-PamArxO3cFJZoOzspzo6cxVlLeIftyBsZw/S9bKY5DzxqJVZgjoj1oP8d0rskKtp7sZxBycsoer1g6UeJV1BBA=="], + "@prisma/config/c12": ["c12@3.1.0", "", { "dependencies": { "chokidar": "^4.0.3", "confbox": "^0.2.2", "defu": "^6.1.4", "dotenv": "^16.6.1", "exsolve": "^1.0.7", "giget": "^2.0.0", "jiti": "^2.4.2", "ohash": "^2.0.11", "pathe": "^2.0.3", "perfect-debounce": "^1.0.0", "pkg-types": "^2.2.0", "rc9": "^2.1.2" }, "peerDependencies": { "magicast": "^0.3.5" }, "optionalPeers": ["magicast"] }, "sha512-uWoS8OU1MEIsOv8p/5a82c3H31LsWVR5qiyXVfBNOzfffjUWtPnhAb4BYI2uG2HfGmZmFjCtui5XNWaps+iFuw=="], "@prisma/dev/hono": ["hono@4.11.4", "", {}, "sha512-U7tt8JsyrxSRKspfhtLET79pU8K+tInj5QZXs1jSugO1Vq5dFj3kmZsRldo29mTBfcjDRVRXrEZ6LS63Cog9ZA=="], @@ -2049,6 +2157,8 @@ "cheerio/whatwg-mimetype": ["whatwg-mimetype@4.0.0", "", {}, "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg=="], + "cliui/wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="], + "cross-fetch/node-fetch": ["node-fetch@2.7.0", "", { "dependencies": { "whatwg-url": "^5.0.0" }, "peerDependencies": { "encoding": "^0.1.0" }, "optionalPeers": ["encoding"] }, "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A=="], "dom-serializer/entities": ["entities@4.5.0", "", {}, "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="], @@ -2085,14 +2195,40 @@ "tedious/@types/node": ["@types/node@25.3.0", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-4K3bqJpXpqfg2XKGK9bpDTc6xO/xoUP/RBWS7AtRMug6zZFaRekiLzjVtAoZMquxoAbzBvy5nxQ7veS5eYzf8A=="], + "tough-cookie/tldts": ["tldts@7.0.27", "", { "dependencies": { "tldts-core": "^7.0.27" }, "bin": { "tldts": "bin/cli.js" } }, "sha512-I4FZcVFcqCRuT0ph6dCDpPuO4Xgzvh+spkcTr1gK7peIvxWauoloVO0vuy1FQnijT63ss6AsHB6+OIM4aXHbPg=="], + "vite/rolldown": ["rolldown@1.0.0-rc.10", "", { "dependencies": { "@oxc-project/types": "=0.120.0", "@rolldown/pluginutils": "1.0.0-rc.10" }, "optionalDependencies": { "@rolldown/binding-android-arm64": "1.0.0-rc.10", "@rolldown/binding-darwin-arm64": "1.0.0-rc.10", "@rolldown/binding-darwin-x64": "1.0.0-rc.10", "@rolldown/binding-freebsd-x64": "1.0.0-rc.10", "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.10", "@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.10", "@rolldown/binding-linux-arm64-musl": "1.0.0-rc.10", "@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.10", "@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.10", "@rolldown/binding-linux-x64-gnu": "1.0.0-rc.10", "@rolldown/binding-linux-x64-musl": "1.0.0-rc.10", "@rolldown/binding-openharmony-arm64": "1.0.0-rc.10", "@rolldown/binding-wasm32-wasi": "1.0.0-rc.10", "@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.10", "@rolldown/binding-win32-x64-msvc": "1.0.0-rc.10" }, "bin": { "rolldown": "bin/cli.mjs" } }, "sha512-q7j6vvarRFmKpgJUT8HCAUljkgzEp4LAhPlJUvQhA5LA1SUL36s5QCysMutErzL3EbNOZOkoziSx9iZC4FddKA=="], "whatwg-encoding/iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="], + "wrap-ansi/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], + "xml-crypto/xpath": ["xpath@0.0.33", "", {}, "sha512-NNXnzrkDrAzalLhIUc01jO2mOzXGXh1JwPgkihcLLzw98c0WgYDmmjSh1Kl3wzaxSVWMuA+fe0WTWOBDWCBmNA=="], "@azure/identity/open/wsl-utils": ["wsl-utils@0.1.0", "", { "dependencies": { "is-wsl": "^3.1.0" } }, "sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw=="], + "@inquirer/checkbox/@inquirer/core/mute-stream": ["mute-stream@3.0.0", "", {}, "sha512-dkEJPVvun4FryqBmZ5KhDo0K9iDXAwn08tMLDinNdRBNPcYEDiWYysLcc6k3mjTMlbP9KyylvRpd4wFtwrT9rw=="], + + "@inquirer/editor/@inquirer/core/mute-stream": ["mute-stream@3.0.0", "", {}, "sha512-dkEJPVvun4FryqBmZ5KhDo0K9iDXAwn08tMLDinNdRBNPcYEDiWYysLcc6k3mjTMlbP9KyylvRpd4wFtwrT9rw=="], + + "@inquirer/expand/@inquirer/core/mute-stream": ["mute-stream@3.0.0", "", {}, "sha512-dkEJPVvun4FryqBmZ5KhDo0K9iDXAwn08tMLDinNdRBNPcYEDiWYysLcc6k3mjTMlbP9KyylvRpd4wFtwrT9rw=="], + + "@inquirer/input/@inquirer/core/mute-stream": ["mute-stream@3.0.0", "", {}, "sha512-dkEJPVvun4FryqBmZ5KhDo0K9iDXAwn08tMLDinNdRBNPcYEDiWYysLcc6k3mjTMlbP9KyylvRpd4wFtwrT9rw=="], + + "@inquirer/number/@inquirer/core/mute-stream": ["mute-stream@3.0.0", "", {}, "sha512-dkEJPVvun4FryqBmZ5KhDo0K9iDXAwn08tMLDinNdRBNPcYEDiWYysLcc6k3mjTMlbP9KyylvRpd4wFtwrT9rw=="], + + "@inquirer/password/@inquirer/core/mute-stream": ["mute-stream@3.0.0", "", {}, "sha512-dkEJPVvun4FryqBmZ5KhDo0K9iDXAwn08tMLDinNdRBNPcYEDiWYysLcc6k3mjTMlbP9KyylvRpd4wFtwrT9rw=="], + + "@inquirer/prompts/@inquirer/confirm/@inquirer/core": ["@inquirer/core@11.1.7", "", { "dependencies": { "@inquirer/ansi": "^2.0.4", "@inquirer/figures": "^2.0.4", "@inquirer/type": "^4.0.4", "cli-width": "^4.1.0", "fast-wrap-ansi": "^0.2.0", "mute-stream": "^3.0.0", "signal-exit": "^4.1.0" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-1BiBNDk9btIwYIzNZpkikIHXWeNzNncJePPqwDyVMhXhD1ebqbpn1mKGctpoqAbzywZfdG0O4tvmsGIcOevAPQ=="], + + "@inquirer/prompts/@inquirer/confirm/@inquirer/type": ["@inquirer/type@4.0.4", "", { "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-PamArxO3cFJZoOzspzo6cxVlLeIftyBsZw/S9bKY5DzxqJVZgjoj1oP8d0rskKtp7sZxBycsoer1g6UeJV1BBA=="], + + "@inquirer/rawlist/@inquirer/core/mute-stream": ["mute-stream@3.0.0", "", {}, "sha512-dkEJPVvun4FryqBmZ5KhDo0K9iDXAwn08tMLDinNdRBNPcYEDiWYysLcc6k3mjTMlbP9KyylvRpd4wFtwrT9rw=="], + + "@inquirer/search/@inquirer/core/mute-stream": ["mute-stream@3.0.0", "", {}, "sha512-dkEJPVvun4FryqBmZ5KhDo0K9iDXAwn08tMLDinNdRBNPcYEDiWYysLcc6k3mjTMlbP9KyylvRpd4wFtwrT9rw=="], + + "@inquirer/select/@inquirer/core/mute-stream": ["mute-stream@3.0.0", "", {}, "sha512-dkEJPVvun4FryqBmZ5KhDo0K9iDXAwn08tMLDinNdRBNPcYEDiWYysLcc6k3mjTMlbP9KyylvRpd4wFtwrT9rw=="], + "@prisma/config/c12/chokidar": ["chokidar@4.0.3", "", { "dependencies": { "readdirp": "^4.0.1" } }, "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA=="], "@prisma/config/c12/dotenv": ["dotenv@16.6.1", "", {}, "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow=="], @@ -2101,12 +2237,16 @@ "@tanstack/router-plugin/chokidar/readdirp": ["readdirp@3.6.0", "", { "dependencies": { "picomatch": "^2.2.1" } }, "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA=="], + "cliui/wrap-ansi/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], + "cross-fetch/node-fetch/whatwg-url": ["whatwg-url@5.0.0", "", { "dependencies": { "tr46": "~0.0.3", "webidl-conversions": "^3.0.0" } }, "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw=="], "mssql/tedious/@types/node": ["@types/node@25.3.0", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-4K3bqJpXpqfg2XKGK9bpDTc6xO/xoUP/RBWS7AtRMug6zZFaRekiLzjVtAoZMquxoAbzBvy5nxQ7veS5eYzf8A=="], "mssql/tedious/iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="], + "tough-cookie/tldts/tldts-core": ["tldts-core@7.0.27", "", {}, "sha512-YQ7uPjgWUibIK6DW5lrKujGwUKhLevU4hcGbP5O6TcIUb+oTjJYJVWPS4nZsIHrEEEG6myk/oqAJUEQmpZrHsg=="], + "vite/rolldown/@oxc-project/types": ["@oxc-project/types@0.120.0", "", {}, "sha512-k1YNu55DuvAip/MGE1FTsIuU3FUCn6v/ujG9V7Nq5Df/kX2CWb13hhwD0lmJGMGqE+bE1MXvv9SZVnMzEXlWcg=="], "vite/rolldown/@rolldown/binding-android-arm64": ["@rolldown/binding-android-arm64@1.0.0-rc.10", "", { "os": "android", "cpu": "arm64" }, "sha512-jOHxwXhxmFKuXztiu1ORieJeTbx5vrTkcOkkkn2d35726+iwhrY1w/+nYY/AGgF12thg33qC3R1LMBF5tHTZHg=="], @@ -2141,6 +2281,8 @@ "vite/rolldown/@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-rc.10", "", {}, "sha512-UkVDEFk1w3mveXeKgaTuYfKWtPbvgck1dT8TUG3bnccrH0XtLTuAyfCoks4Q/M5ZGToSVJTIQYCzy2g/atAOeg=="], + "@inquirer/prompts/@inquirer/confirm/@inquirer/core/mute-stream": ["mute-stream@3.0.0", "", {}, "sha512-dkEJPVvun4FryqBmZ5KhDo0K9iDXAwn08tMLDinNdRBNPcYEDiWYysLcc6k3mjTMlbP9KyylvRpd4wFtwrT9rw=="], + "@prisma/config/c12/chokidar/readdirp": ["readdirp@4.1.2", "", {}, "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg=="], "@tanstack/router-plugin/chokidar/readdirp/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], diff --git a/e2e/0002-backup-restore.spec.ts b/e2e/0002-backup-restore.spec.ts index be71ef20..efd01a4e 100644 --- a/e2e/0002-backup-restore.spec.ts +++ b/e2e/0002-backup-restore.spec.ts @@ -35,7 +35,7 @@ function getRunId(testInfo: TestInfo) { return `${testInfo.parallelIndex}-${testInfo.retry}-${randomUUID().slice(0, 8)}`; } -function getWorkerTestDataPath(runId: string) { +function getWorkerTestDataPath() { fs.mkdirSync(testDataPath, { recursive: true }); return testDataPath; } @@ -49,7 +49,7 @@ function getScenarioNames(runId: string): ScenarioNames { } function prepareTestFile(runId: string, fileName = "test.json"): string { - const runPath = path.join(getWorkerTestDataPath(runId), runId); + const runPath = path.join(getWorkerTestDataPath(), runId); fs.mkdirSync(runPath, { recursive: true }); const filePath = path.join(runPath, fileName); @@ -58,8 +58,8 @@ function prepareTestFile(runId: string, fileName = "test.json"): string { return filePath; } -async function createBackupScenario(page: Page, names: ScenarioNames, runId: string, options: ScenarioOptions = {}) { - getWorkerTestDataPath(runId); +async function createBackupScenario(page: Page, names: ScenarioNames, options: ScenarioOptions = {}) { + getWorkerTestDataPath(); const volumeNameInput = page.getByRole("textbox", { name: "Name" }); await expect(async () => { @@ -153,7 +153,7 @@ test("can backup & restore a file", async ({ page }, testInfo) => { await gotoAndWaitForAppReady(page, "/"); await expect(page).toHaveURL("/volumes"); - await createBackupScenario(page, names, runId); + await createBackupScenario(page, names); await page.getByRole("button", { name: "Backup now" }).click(); await expect(page.getByText("Backup started successfully")).toBeVisible(); @@ -177,7 +177,7 @@ test("can backup & restore a file", async ({ page }, testInfo) => { test("can restore a single selected file to a custom location", async ({ page }, testInfo) => { const runId = getRunId(testInfo); const names = getScenarioNames(runId); - const workerTestDataPath = getWorkerTestDataPath(runId); + const workerTestDataPath = getWorkerTestDataPath(); const fileName = `single-file-${runId}.json`; const filePath = prepareTestFile(runId, fileName); const restoreTargetPath = path.join(workerTestDataPath, fileName); @@ -188,7 +188,7 @@ test("can restore a single selected file to a custom location", async ({ page }, await gotoAndWaitForAppReady(page, "/"); await expect(page).toHaveURL("/volumes"); - await createBackupScenario(page, names, runId); + await createBackupScenario(page, names); await page.getByRole("button", { name: "Backup now" }).click(); await expect(page.getByText("Backup started successfully")).toBeVisible(); @@ -234,7 +234,7 @@ test("can re-tag a snapshot to another backup schedule", async ({ page }, testIn await gotoAndWaitForAppReady(page, "/"); await expect(page).toHaveURL("/volumes"); - await createBackupScenario(page, names, runId); + await createBackupScenario(page, names); await page.getByRole("button", { name: "Backup now" }).click(); await expect(page.getByText("Backup started successfully")).toBeVisible(); @@ -277,7 +277,7 @@ test("can delete a snapshot from the repository snapshots tab", async ({ page }, await gotoAndWaitForAppReady(page, "/"); await expect(page).toHaveURL("/volumes"); - await createBackupScenario(page, names, runId); + await createBackupScenario(page, names); await page.getByRole("button", { name: "Backup now" }).click(); await expect(page.getByText("Backup started successfully")).toBeVisible(); @@ -302,7 +302,7 @@ test("can delete a snapshot from the repository snapshots tab", async ({ page }, test("can download a selected snapshot directory as a tar archive", async ({ page }, testInfo) => { const runId = getRunId(testInfo); const names = getScenarioNames(runId); - const workerTestDataPath = getWorkerTestDataPath(runId); + const workerTestDataPath = getWorkerTestDataPath(); const fileName = `download-${runId}.json`; const filePath = prepareTestFile(runId, fileName); const downloadedPath = path.join(workerTestDataPath, `downloaded-${runId}.tar`); @@ -312,7 +312,7 @@ test("can download a selected snapshot directory as a tar archive", async ({ pag await gotoAndWaitForAppReady(page, "/"); await expect(page).toHaveURL("/volumes"); - await createBackupScenario(page, names, runId); + await createBackupScenario(page, names); await page.getByRole("button", { name: "Backup now" }).click(); await expect(page.getByText("Backup started successfully")).toBeVisible(); @@ -351,7 +351,7 @@ test("deleting a volume cascades and removes its backup schedule", async ({ page await gotoAndWaitForAppReady(page, "/"); await expect(page).toHaveURL("/volumes"); - await createBackupScenario(page, names, runId); + await createBackupScenario(page, names); await gotoAndWaitForAppReady(page, "/backups"); await page.getByText(names.backupName, { exact: true }).first().click(); @@ -378,7 +378,7 @@ test("deleting a volume cascades and removes its backup schedule", async ({ page test("backup respects include globs, exclusion patterns, and exclude-if-present", async ({ page }, testInfo) => { const runId = getRunId(testInfo); const names = getScenarioNames(runId); - const workerTestDataPath = getWorkerTestDataPath(runId); + const workerTestDataPath = getWorkerTestDataPath(); const keptDir = `kept-${runId}`; const secondKeptDir = `second-kept-${runId}`; @@ -433,7 +433,7 @@ test("backup respects include globs, exclusion patterns, and exclude-if-present" await gotoAndWaitForAppReady(page, "/"); await expect(page).toHaveURL("/volumes"); - await createBackupScenario(page, names, runId, { + await createBackupScenario(page, names, { includePatterns: [ `/${keptDir}`, `/${secondKeptDir}`, @@ -492,7 +492,7 @@ test("backup respects include globs, exclusion patterns, and exclude-if-present" test("backup can include a selected folder whose name contains brackets", async ({ page }, testInfo) => { const runId = getRunId(testInfo); const names = getScenarioNames(runId); - const workerTestDataPath = getWorkerTestDataPath(runId); + const workerTestDataPath = getWorkerTestDataPath(); const bracketDir = `movies [${runId}]`; const bracketPath = path.join(workerTestDataPath, bracketDir); const fileName = `inside-${runId}.txt`; @@ -503,7 +503,7 @@ test("backup can include a selected folder whose name contains brackets", async await gotoAndWaitForAppReady(page, "/"); await expect(page).toHaveURL("/volumes"); - await createBackupScenario(page, names, runId, { + await createBackupScenario(page, names, { selectedPaths: [`/${bracketDir}`], }); diff --git a/package.json b/package.json index 6f76b767..3167ac51 100644 --- a/package.json +++ b/package.json @@ -108,6 +108,7 @@ "@tailwindcss/vite": "^4.2.2", "@testing-library/dom": "^10.4.1", "@testing-library/react": "^16.3.2", + "@testing-library/user-event": "^14.6.1", "@total-typescript/shoehorn": "^0.1.2", "@types/babel__core": "^7.20.5", "@types/bun": "^1.3.11", @@ -122,6 +123,7 @@ "drizzle-kit": "^1.0.0-beta.16-ea816b6", "lefthook": "^2.1.4", "lightningcss": "^1.32.0", + "msw": "^2.12.14", "nitro": "^3.0.1-alpha.2", "oxfmt": "^0.41.0", "oxlint": "^1.56.0", diff --git a/tsconfig.json b/tsconfig.json index bfd4ee03..6956e49b 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,5 +1,5 @@ { - "include": ["**/*"], + "include": ["app/**/*"], "compilerOptions": { "lib": ["DOM", "DOM.Iterable", "ES2022"], "types": ["node", "vite/client"],