diff --git a/.dockerignore b/.dockerignore index 7cac851..9980d98 100644 --- a/.dockerignore +++ b/.dockerignore @@ -12,12 +12,9 @@ !**/build.ts !**/components.json -!apps/**/src/** -!apps/**/drizzle/** -!apps/**/app/** -!apps/**/public/** - -!packages/**/src/** +!src/** +!app/** +!public/** # License files and attributions !LICENSE diff --git a/.gitignore b/.gitignore index 2bd0209..4addbfe 100644 --- a/.gitignore +++ b/.gitignore @@ -1,47 +1,11 @@ -# If you prefer the allow list template instead of the deny list, see community template: -# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore -# -# Binaries for programs and plugins -*.exe -*.exe~ -*.dll -*.so -*.dylib +.DS_Store +/node_modules/ -# Test binary, built with `go test -c` -*.test +# React Router +/.react-router/ +/build/ +/dist/ -# Code coverage profiles and other test artifacts -*.out -coverage.* -*.coverprofile -profile.cov - -# Dependency directories (remove the comment below to include it) -# vendor/ - -# Go workspace file -go.work -go.work.sum - -# env file .env - -# Editor/IDE -# .idea/ -# .vscode/ -ironmount -out/ -*.db -tmp/ - -node_modules/ -.env* - .turbo - -mutagen.yml.lock - -data/ - CLAUDE.md diff --git a/Dockerfile b/Dockerfile index 0a7ad18..32b23e4 100644 --- a/Dockerfile +++ b/Dockerfile @@ -45,15 +45,12 @@ WORKDIR /app COPY --from=deps /deps/restic /usr/local/bin/restic COPY --from=deps /deps/rclone /usr/local/bin/rclone COPY ./package.json ./bun.lock ./ -COPY ./packages/schemas/package.json ./packages/schemas/package.json -COPY ./apps/client/package.json ./apps/client/package.json -COPY ./apps/server/package.json ./apps/server/package.json RUN bun install --frozen-lockfile COPY . . -EXPOSE 3000 +EXPOSE 4096 CMD ["bun", "run", "dev"] @@ -65,13 +62,12 @@ FROM oven/bun:${BUN_VERSION} AS builder WORKDIR /app COPY ./package.json ./bun.lock ./ +RUN bun install --frozen-lockfile COPY ./packages/schemas/package.json ./packages/schemas/package.json COPY ./apps/client/package.json ./apps/client/package.json COPY ./apps/server/package.json ./apps/server/package.json -RUN bun install --frozen-lockfile - COPY . . RUN bun run build @@ -82,16 +78,19 @@ ENV NODE_ENV="production" WORKDIR /app +COPY --from=builder /app/package.json ./ +RUN bun install --production --frozen-lockfile + COPY --from=deps /deps/restic /usr/local/bin/restic COPY --from=deps /deps/rclone /usr/local/bin/rclone -COPY --from=builder /app/apps/server/dist ./ -COPY --from=builder /app/apps/server/drizzle ./assets/migrations -COPY --from=builder /app/apps/client/dist/client ./assets/frontend +COPY --from=builder /app/dist/client ./dist/client +COPY --from=builder /app/dist/server ./dist/server +COPY --from=builder /app/app/drizzle ./assets/migrations # Include third-party licenses and attribution COPY ./LICENSES ./LICENSES COPY ./NOTICES.md ./NOTICES.md COPY ./LICENSE ./LICENSE.md -CMD ["bun", "./index.js"] +CMD ["bun", "run", "start"] diff --git a/apps/client/app/app.css b/app/app.css similarity index 100% rename from apps/client/app/app.css rename to app/app.css diff --git a/apps/client/app/api-client/@tanstack/react-query.gen.ts b/app/client/api-client/@tanstack/react-query.gen.ts similarity index 66% rename from apps/client/app/api-client/@tanstack/react-query.gen.ts rename to app/client/api-client/@tanstack/react-query.gen.ts index 38a20d7..8767f0f 100644 --- a/apps/client/app/api-client/@tanstack/react-query.gen.ts +++ b/app/client/api-client/@tanstack/react-query.gen.ts @@ -1,109 +1,184 @@ // This file is auto-generated by @hey-api/openapi-ts +import { type DefaultError, queryOptions, type UseMutationOptions } from "@tanstack/react-query"; + +import { client } from "../client.gen"; import { - type Options, - register, + browseFilesystem, + changePassword, + createBackupSchedule, + createRepository, + createVolume, + deleteBackupSchedule, + deleteRepository, + deleteVolume, + doctorRepository, + downloadResticPassword, + getBackupSchedule, + getBackupScheduleForVolume, + getContainersUsingVolume, + getMe, + getRepository, + getSnapshotDetails, + getStatus, + getSystemInfo, + getVolume, + healthCheckVolume, + listBackupSchedules, + listFiles, + listRcloneRemotes, + listRepositories, + listSnapshotFiles, + listSnapshots, + listVolumes, login, logout, - getMe, - getStatus, - changePassword, - listVolumes, - createVolume, - testConnection, - deleteVolume, - getVolume, - updateVolume, - getContainersUsingVolume, mountVolume, - unmountVolume, - healthCheckVolume, - listFiles, - browseFilesystem, - listRepositories, - createRepository, - listRcloneRemotes, - deleteRepository, - getRepository, - listSnapshots, - getSnapshotDetails, - listSnapshotFiles, + type Options, + register, restoreSnapshot, - doctorRepository, - listBackupSchedules, - createBackupSchedule, - deleteBackupSchedule, - getBackupSchedule, - updateBackupSchedule, - getBackupScheduleForVolume, runBackupNow, stopBackup, - getSystemInfo, - downloadResticPassword, + testConnection, + unmountVolume, + updateBackupSchedule, + updateVolume, } from "../sdk.gen"; -import { queryOptions, type UseMutationOptions, type DefaultError } from "@tanstack/react-query"; import type { - RegisterData, - RegisterResponse, + BrowseFilesystemData, + BrowseFilesystemResponse, + ChangePasswordData, + ChangePasswordResponse, + CreateBackupScheduleData, + CreateBackupScheduleResponse, + CreateRepositoryData, + CreateRepositoryResponse, + CreateVolumeData, + CreateVolumeResponse, + DeleteBackupScheduleData, + DeleteBackupScheduleResponse, + DeleteRepositoryData, + DeleteRepositoryResponse, + DeleteVolumeData, + DeleteVolumeResponse, + DoctorRepositoryData, + DoctorRepositoryResponse, + DownloadResticPasswordData, + DownloadResticPasswordResponse, + GetBackupScheduleData, + GetBackupScheduleForVolumeData, + GetBackupScheduleForVolumeResponse, + GetBackupScheduleResponse, + GetContainersUsingVolumeData, + GetContainersUsingVolumeResponse, + GetMeData, + GetMeResponse, + GetRepositoryData, + GetRepositoryResponse, + GetSnapshotDetailsData, + GetSnapshotDetailsResponse, + GetStatusData, + GetStatusResponse, + GetSystemInfoData, + GetSystemInfoResponse, + GetVolumeData, + GetVolumeResponse, + HealthCheckVolumeData, + HealthCheckVolumeResponse, + ListBackupSchedulesData, + ListBackupSchedulesResponse, + ListFilesData, + ListFilesResponse, + ListRcloneRemotesData, + ListRcloneRemotesResponse, + ListRepositoriesData, + ListRepositoriesResponse, + ListSnapshotFilesData, + ListSnapshotFilesResponse, + ListSnapshotsData, + ListSnapshotsResponse, + ListVolumesData, + ListVolumesResponse, LoginData, LoginResponse, LogoutData, LogoutResponse, - GetMeData, - GetStatusData, - ChangePasswordData, - ChangePasswordResponse, - ListVolumesData, - CreateVolumeData, - CreateVolumeResponse, - TestConnectionData, - TestConnectionResponse, - DeleteVolumeData, - DeleteVolumeResponse, - GetVolumeData, - UpdateVolumeData, - UpdateVolumeResponse, - GetContainersUsingVolumeData, MountVolumeData, MountVolumeResponse, - UnmountVolumeData, - UnmountVolumeResponse, - HealthCheckVolumeData, - HealthCheckVolumeResponse, - ListFilesData, - BrowseFilesystemData, - ListRepositoriesData, - CreateRepositoryData, - CreateRepositoryResponse, - ListRcloneRemotesData, - DeleteRepositoryData, - DeleteRepositoryResponse, - GetRepositoryData, - ListSnapshotsData, - GetSnapshotDetailsData, - ListSnapshotFilesData, + RegisterData, + RegisterResponse, RestoreSnapshotData, RestoreSnapshotResponse, - DoctorRepositoryData, - DoctorRepositoryResponse, - ListBackupSchedulesData, - CreateBackupScheduleData, - CreateBackupScheduleResponse, - DeleteBackupScheduleData, - DeleteBackupScheduleResponse, - GetBackupScheduleData, - UpdateBackupScheduleData, - UpdateBackupScheduleResponse, - GetBackupScheduleForVolumeData, RunBackupNowData, RunBackupNowResponse, StopBackupData, StopBackupResponse, - GetSystemInfoData, - DownloadResticPasswordData, - DownloadResticPasswordResponse, + TestConnectionData, + TestConnectionResponse, + UnmountVolumeData, + UnmountVolumeResponse, + UpdateBackupScheduleData, + UpdateBackupScheduleResponse, + UpdateVolumeData, + UpdateVolumeResponse, } from "../types.gen"; -import { client as _heyApiClient } from "../client.gen"; + +/** + * Register a new user + */ +export const registerMutation = ( + options?: Partial>, +): UseMutationOptions> => { + const mutationOptions: UseMutationOptions> = { + mutationFn: async (fnOptions) => { + const { data } = await register({ + ...options, + ...fnOptions, + throwOnError: true, + }); + return data; + }, + }; + return mutationOptions; +}; + +/** + * Login with username and password + */ +export const loginMutation = ( + options?: Partial>, +): UseMutationOptions> => { + const mutationOptions: UseMutationOptions> = { + mutationFn: async (fnOptions) => { + const { data } = await login({ + ...options, + ...fnOptions, + throwOnError: true, + }); + return data; + }, + }; + return mutationOptions; +}; + +/** + * Logout current user + */ +export const logoutMutation = ( + options?: Partial>, +): UseMutationOptions> => { + const mutationOptions: UseMutationOptions> = { + mutationFn: async (fnOptions) => { + const { data } = await logout({ + ...options, + ...fnOptions, + throwOnError: true, + }); + return data; + }, + }; + return mutationOptions; +}; export type QueryKey = [ Pick & { @@ -121,7 +196,7 @@ const createQueryKey = ( ): [QueryKey[0]] => { const params: QueryKey[0] = { _id: id, - baseUrl: options?.baseUrl || (options?.client ?? _heyApiClient).getConfig().baseUrl, + baseUrl: options?.baseUrl || (options?.client ?? client).getConfig().baseUrl, } as QueryKey[0]; if (infinite) { params._infinite = infinite; @@ -144,130 +219,13 @@ const createQueryKey = ( return [params]; }; -export const registerQueryKey = (options?: Options) => createQueryKey("register", options); - -/** - * Register a new user - */ -export const registerOptions = (options?: Options) => { - return queryOptions({ - queryFn: async ({ queryKey, signal }) => { - const { data } = await register({ - ...options, - ...queryKey[0], - signal, - throwOnError: true, - }); - return data; - }, - queryKey: registerQueryKey(options), - }); -}; - -/** - * Register a new user - */ -export const registerMutation = ( - options?: Partial>, -): UseMutationOptions> => { - const mutationOptions: UseMutationOptions> = { - mutationFn: async (localOptions) => { - const { data } = await register({ - ...options, - ...localOptions, - throwOnError: true, - }); - return data; - }, - }; - return mutationOptions; -}; - -export const loginQueryKey = (options?: Options) => createQueryKey("login", options); - -/** - * Login with username and password - */ -export const loginOptions = (options?: Options) => { - return queryOptions({ - queryFn: async ({ queryKey, signal }) => { - const { data } = await login({ - ...options, - ...queryKey[0], - signal, - throwOnError: true, - }); - return data; - }, - queryKey: loginQueryKey(options), - }); -}; - -/** - * Login with username and password - */ -export const loginMutation = ( - options?: Partial>, -): UseMutationOptions> => { - const mutationOptions: UseMutationOptions> = { - mutationFn: async (localOptions) => { - const { data } = await login({ - ...options, - ...localOptions, - throwOnError: true, - }); - return data; - }, - }; - return mutationOptions; -}; - -export const logoutQueryKey = (options?: Options) => createQueryKey("logout", options); - -/** - * Logout current user - */ -export const logoutOptions = (options?: Options) => { - return queryOptions({ - queryFn: async ({ queryKey, signal }) => { - const { data } = await logout({ - ...options, - ...queryKey[0], - signal, - throwOnError: true, - }); - return data; - }, - queryKey: logoutQueryKey(options), - }); -}; - -/** - * Logout current user - */ -export const logoutMutation = ( - options?: Partial>, -): UseMutationOptions> => { - const mutationOptions: UseMutationOptions> = { - mutationFn: async (localOptions) => { - const { data } = await logout({ - ...options, - ...localOptions, - throwOnError: true, - }); - return data; - }, - }; - return mutationOptions; -}; - export const getMeQueryKey = (options?: Options) => createQueryKey("getMe", options); /** * Get current authenticated user */ -export const getMeOptions = (options?: Options) => { - return queryOptions({ +export const getMeOptions = (options?: Options) => + queryOptions>({ queryFn: async ({ queryKey, signal }) => { const { data } = await getMe({ ...options, @@ -279,15 +237,14 @@ export const getMeOptions = (options?: Options) => { }, queryKey: getMeQueryKey(options), }); -}; export const getStatusQueryKey = (options?: Options) => createQueryKey("getStatus", options); /** * Get authentication system status */ -export const getStatusOptions = (options?: Options) => { - return queryOptions({ +export const getStatusOptions = (options?: Options) => + queryOptions>({ queryFn: async ({ queryKey, signal }) => { const { data } = await getStatus({ ...options, @@ -299,28 +256,6 @@ export const getStatusOptions = (options?: Options) => { }, queryKey: getStatusQueryKey(options), }); -}; - -export const changePasswordQueryKey = (options?: Options) => - createQueryKey("changePassword", options); - -/** - * Change current user password - */ -export const changePasswordOptions = (options?: Options) => { - return queryOptions({ - queryFn: async ({ queryKey, signal }) => { - const { data } = await changePassword({ - ...options, - ...queryKey[0], - signal, - throwOnError: true, - }); - return data; - }, - queryKey: changePasswordQueryKey(options), - }); -}; /** * Change current user password @@ -329,10 +264,10 @@ export const changePasswordMutation = ( options?: Partial>, ): UseMutationOptions> => { const mutationOptions: UseMutationOptions> = { - mutationFn: async (localOptions) => { + mutationFn: async (fnOptions) => { const { data } = await changePassword({ ...options, - ...localOptions, + ...fnOptions, throwOnError: true, }); return data; @@ -346,8 +281,8 @@ export const listVolumesQueryKey = (options?: Options) => creat /** * List all volumes */ -export const listVolumesOptions = (options?: Options) => { - return queryOptions({ +export const listVolumesOptions = (options?: Options) => + queryOptions>({ queryFn: async ({ queryKey, signal }) => { const { data } = await listVolumes({ ...options, @@ -359,27 +294,6 @@ export const listVolumesOptions = (options?: Options) => { }, queryKey: listVolumesQueryKey(options), }); -}; - -export const createVolumeQueryKey = (options?: Options) => createQueryKey("createVolume", options); - -/** - * Create a new volume - */ -export const createVolumeOptions = (options?: Options) => { - return queryOptions({ - queryFn: async ({ queryKey, signal }) => { - const { data } = await createVolume({ - ...options, - ...queryKey[0], - signal, - throwOnError: true, - }); - return data; - }, - queryKey: createVolumeQueryKey(options), - }); -}; /** * Create a new volume @@ -388,10 +302,10 @@ export const createVolumeMutation = ( options?: Partial>, ): UseMutationOptions> => { const mutationOptions: UseMutationOptions> = { - mutationFn: async (localOptions) => { + mutationFn: async (fnOptions) => { const { data } = await createVolume({ ...options, - ...localOptions, + ...fnOptions, throwOnError: true, }); return data; @@ -400,27 +314,6 @@ export const createVolumeMutation = ( return mutationOptions; }; -export const testConnectionQueryKey = (options?: Options) => - createQueryKey("testConnection", options); - -/** - * Test connection to backend - */ -export const testConnectionOptions = (options?: Options) => { - return queryOptions({ - queryFn: async ({ queryKey, signal }) => { - const { data } = await testConnection({ - ...options, - ...queryKey[0], - signal, - throwOnError: true, - }); - return data; - }, - queryKey: testConnectionQueryKey(options), - }); -}; - /** * Test connection to backend */ @@ -428,10 +321,10 @@ export const testConnectionMutation = ( options?: Partial>, ): UseMutationOptions> => { const mutationOptions: UseMutationOptions> = { - mutationFn: async (localOptions) => { + mutationFn: async (fnOptions) => { const { data } = await testConnection({ ...options, - ...localOptions, + ...fnOptions, throwOnError: true, }); return data; @@ -447,10 +340,10 @@ export const deleteVolumeMutation = ( options?: Partial>, ): UseMutationOptions> => { const mutationOptions: UseMutationOptions> = { - mutationFn: async (localOptions) => { + mutationFn: async (fnOptions) => { const { data } = await deleteVolume({ ...options, - ...localOptions, + ...fnOptions, throwOnError: true, }); return data; @@ -464,8 +357,8 @@ export const getVolumeQueryKey = (options: Options) => createQuer /** * Get a volume by name */ -export const getVolumeOptions = (options: Options) => { - return queryOptions({ +export const getVolumeOptions = (options: Options) => + queryOptions>({ queryFn: async ({ queryKey, signal }) => { const { data } = await getVolume({ ...options, @@ -477,7 +370,6 @@ export const getVolumeOptions = (options: Options) => { }, queryKey: getVolumeQueryKey(options), }); -}; /** * Update a volume's configuration @@ -486,10 +378,10 @@ export const updateVolumeMutation = ( options?: Partial>, ): UseMutationOptions> => { const mutationOptions: UseMutationOptions> = { - mutationFn: async (localOptions) => { + mutationFn: async (fnOptions) => { const { data } = await updateVolume({ ...options, - ...localOptions, + ...fnOptions, throwOnError: true, }); return data; @@ -504,8 +396,13 @@ export const getContainersUsingVolumeQueryKey = (options: Options) => { - return queryOptions({ +export const getContainersUsingVolumeOptions = (options: Options) => + queryOptions< + GetContainersUsingVolumeResponse, + DefaultError, + GetContainersUsingVolumeResponse, + ReturnType + >({ queryFn: async ({ queryKey, signal }) => { const { data } = await getContainersUsingVolume({ ...options, @@ -517,27 +414,6 @@ export const getContainersUsingVolumeOptions = (options: Options) => createQueryKey("mountVolume", options); - -/** - * Mount a volume - */ -export const mountVolumeOptions = (options: Options) => { - return queryOptions({ - queryFn: async ({ queryKey, signal }) => { - const { data } = await mountVolume({ - ...options, - ...queryKey[0], - signal, - throwOnError: true, - }); - return data; - }, - queryKey: mountVolumeQueryKey(options), - }); -}; /** * Mount a volume @@ -546,10 +422,10 @@ export const mountVolumeMutation = ( options?: Partial>, ): UseMutationOptions> => { const mutationOptions: UseMutationOptions> = { - mutationFn: async (localOptions) => { + mutationFn: async (fnOptions) => { const { data } = await mountVolume({ ...options, - ...localOptions, + ...fnOptions, throwOnError: true, }); return data; @@ -558,26 +434,6 @@ export const mountVolumeMutation = ( return mutationOptions; }; -export const unmountVolumeQueryKey = (options: Options) => createQueryKey("unmountVolume", options); - -/** - * Unmount a volume - */ -export const unmountVolumeOptions = (options: Options) => { - return queryOptions({ - queryFn: async ({ queryKey, signal }) => { - const { data } = await unmountVolume({ - ...options, - ...queryKey[0], - signal, - throwOnError: true, - }); - return data; - }, - queryKey: unmountVolumeQueryKey(options), - }); -}; - /** * Unmount a volume */ @@ -585,10 +441,10 @@ export const unmountVolumeMutation = ( options?: Partial>, ): UseMutationOptions> => { const mutationOptions: UseMutationOptions> = { - mutationFn: async (localOptions) => { + mutationFn: async (fnOptions) => { const { data } = await unmountVolume({ ...options, - ...localOptions, + ...fnOptions, throwOnError: true, }); return data; @@ -597,27 +453,6 @@ export const unmountVolumeMutation = ( return mutationOptions; }; -export const healthCheckVolumeQueryKey = (options: Options) => - createQueryKey("healthCheckVolume", options); - -/** - * Perform a health check on a volume - */ -export const healthCheckVolumeOptions = (options: Options) => { - return queryOptions({ - queryFn: async ({ queryKey, signal }) => { - const { data } = await healthCheckVolume({ - ...options, - ...queryKey[0], - signal, - throwOnError: true, - }); - return data; - }, - queryKey: healthCheckVolumeQueryKey(options), - }); -}; - /** * Perform a health check on a volume */ @@ -625,10 +460,10 @@ export const healthCheckVolumeMutation = ( options?: Partial>, ): UseMutationOptions> => { const mutationOptions: UseMutationOptions> = { - mutationFn: async (localOptions) => { + mutationFn: async (fnOptions) => { const { data } = await healthCheckVolume({ ...options, - ...localOptions, + ...fnOptions, throwOnError: true, }); return data; @@ -642,8 +477,8 @@ export const listFilesQueryKey = (options: Options) => createQuer /** * List files in a volume directory */ -export const listFilesOptions = (options: Options) => { - return queryOptions({ +export const listFilesOptions = (options: Options) => + queryOptions>({ queryFn: async ({ queryKey, signal }) => { const { data } = await listFiles({ ...options, @@ -655,7 +490,6 @@ export const listFilesOptions = (options: Options) => { }, queryKey: listFilesQueryKey(options), }); -}; export const browseFilesystemQueryKey = (options?: Options) => createQueryKey("browseFilesystem", options); @@ -663,8 +497,13 @@ export const browseFilesystemQueryKey = (options?: Options /** * Browse directories on the host filesystem */ -export const browseFilesystemOptions = (options?: Options) => { - return queryOptions({ +export const browseFilesystemOptions = (options?: Options) => + queryOptions< + BrowseFilesystemResponse, + DefaultError, + BrowseFilesystemResponse, + ReturnType + >({ queryFn: async ({ queryKey, signal }) => { const { data } = await browseFilesystem({ ...options, @@ -676,7 +515,6 @@ export const browseFilesystemOptions = (options?: Options) }, queryKey: browseFilesystemQueryKey(options), }); -}; export const listRepositoriesQueryKey = (options?: Options) => createQueryKey("listRepositories", options); @@ -684,8 +522,13 @@ export const listRepositoriesQueryKey = (options?: Options /** * List all repositories */ -export const listRepositoriesOptions = (options?: Options) => { - return queryOptions({ +export const listRepositoriesOptions = (options?: Options) => + queryOptions< + ListRepositoriesResponse, + DefaultError, + ListRepositoriesResponse, + ReturnType + >({ queryFn: async ({ queryKey, signal }) => { const { data } = await listRepositories({ ...options, @@ -697,28 +540,6 @@ export const listRepositoriesOptions = (options?: Options) }, queryKey: listRepositoriesQueryKey(options), }); -}; - -export const createRepositoryQueryKey = (options?: Options) => - createQueryKey("createRepository", options); - -/** - * Create a new restic repository - */ -export const createRepositoryOptions = (options?: Options) => { - return queryOptions({ - queryFn: async ({ queryKey, signal }) => { - const { data } = await createRepository({ - ...options, - ...queryKey[0], - signal, - throwOnError: true, - }); - return data; - }, - queryKey: createRepositoryQueryKey(options), - }); -}; /** * Create a new restic repository @@ -727,10 +548,10 @@ export const createRepositoryMutation = ( options?: Partial>, ): UseMutationOptions> => { const mutationOptions: UseMutationOptions> = { - mutationFn: async (localOptions) => { + mutationFn: async (fnOptions) => { const { data } = await createRepository({ ...options, - ...localOptions, + ...fnOptions, throwOnError: true, }); return data; @@ -745,8 +566,13 @@ export const listRcloneRemotesQueryKey = (options?: Options) => { - return queryOptions({ +export const listRcloneRemotesOptions = (options?: Options) => + queryOptions< + ListRcloneRemotesResponse, + DefaultError, + ListRcloneRemotesResponse, + ReturnType + >({ queryFn: async ({ queryKey, signal }) => { const { data } = await listRcloneRemotes({ ...options, @@ -758,7 +584,6 @@ export const listRcloneRemotesOptions = (options?: Options>, ): UseMutationOptions> => { const mutationOptions: UseMutationOptions> = { - mutationFn: async (localOptions) => { + mutationFn: async (fnOptions) => { const { data } = await deleteRepository({ ...options, - ...localOptions, + ...fnOptions, throwOnError: true, }); return data; @@ -784,8 +609,8 @@ export const getRepositoryQueryKey = (options: Options) => cr /** * Get a single repository by name */ -export const getRepositoryOptions = (options: Options) => { - return queryOptions({ +export const getRepositoryOptions = (options: Options) => + queryOptions>({ queryFn: async ({ queryKey, signal }) => { const { data } = await getRepository({ ...options, @@ -797,15 +622,14 @@ export const getRepositoryOptions = (options: Options) => { }, queryKey: getRepositoryQueryKey(options), }); -}; export const listSnapshotsQueryKey = (options: Options) => createQueryKey("listSnapshots", options); /** * List all snapshots in a repository */ -export const listSnapshotsOptions = (options: Options) => { - return queryOptions({ +export const listSnapshotsOptions = (options: Options) => + queryOptions>({ queryFn: async ({ queryKey, signal }) => { const { data } = await listSnapshots({ ...options, @@ -817,7 +641,6 @@ export const listSnapshotsOptions = (options: Options) => { }, queryKey: listSnapshotsQueryKey(options), }); -}; export const getSnapshotDetailsQueryKey = (options: Options) => createQueryKey("getSnapshotDetails", options); @@ -825,8 +648,13 @@ export const getSnapshotDetailsQueryKey = (options: Options) => { - return queryOptions({ +export const getSnapshotDetailsOptions = (options: Options) => + queryOptions< + GetSnapshotDetailsResponse, + DefaultError, + GetSnapshotDetailsResponse, + ReturnType + >({ queryFn: async ({ queryKey, signal }) => { const { data } = await getSnapshotDetails({ ...options, @@ -838,7 +666,6 @@ export const getSnapshotDetailsOptions = (options: Options) => createQueryKey("listSnapshotFiles", options); @@ -846,8 +673,13 @@ export const listSnapshotFilesQueryKey = (options: Options) => { - return queryOptions({ +export const listSnapshotFilesOptions = (options: Options) => + queryOptions< + ListSnapshotFilesResponse, + DefaultError, + ListSnapshotFilesResponse, + ReturnType + >({ queryFn: async ({ queryKey, signal }) => { const { data } = await listSnapshotFiles({ ...options, @@ -859,28 +691,6 @@ export const listSnapshotFilesOptions = (options: Options }, queryKey: listSnapshotFilesQueryKey(options), }); -}; - -export const restoreSnapshotQueryKey = (options: Options) => - createQueryKey("restoreSnapshot", options); - -/** - * Restore a snapshot to a target path on the filesystem - */ -export const restoreSnapshotOptions = (options: Options) => { - return queryOptions({ - queryFn: async ({ queryKey, signal }) => { - const { data } = await restoreSnapshot({ - ...options, - ...queryKey[0], - signal, - throwOnError: true, - }); - return data; - }, - queryKey: restoreSnapshotQueryKey(options), - }); -}; /** * Restore a snapshot to a target path on the filesystem @@ -889,10 +699,10 @@ export const restoreSnapshotMutation = ( options?: Partial>, ): UseMutationOptions> => { const mutationOptions: UseMutationOptions> = { - mutationFn: async (localOptions) => { + mutationFn: async (fnOptions) => { const { data } = await restoreSnapshot({ ...options, - ...localOptions, + ...fnOptions, throwOnError: true, }); return data; @@ -901,27 +711,6 @@ export const restoreSnapshotMutation = ( return mutationOptions; }; -export const doctorRepositoryQueryKey = (options: Options) => - createQueryKey("doctorRepository", options); - -/** - * Run doctor operations on a repository to fix common issues (unlock, check, repair index). Use this when the repository is locked or has errors. - */ -export const doctorRepositoryOptions = (options: Options) => { - return queryOptions({ - queryFn: async ({ queryKey, signal }) => { - const { data } = await doctorRepository({ - ...options, - ...queryKey[0], - signal, - throwOnError: true, - }); - return data; - }, - queryKey: doctorRepositoryQueryKey(options), - }); -}; - /** * Run doctor operations on a repository to fix common issues (unlock, check, repair index). Use this when the repository is locked or has errors. */ @@ -929,10 +718,10 @@ export const doctorRepositoryMutation = ( options?: Partial>, ): UseMutationOptions> => { const mutationOptions: UseMutationOptions> = { - mutationFn: async (localOptions) => { + mutationFn: async (fnOptions) => { const { data } = await doctorRepository({ ...options, - ...localOptions, + ...fnOptions, throwOnError: true, }); return data; @@ -947,8 +736,13 @@ export const listBackupSchedulesQueryKey = (options?: Options) => { - return queryOptions({ +export const listBackupSchedulesOptions = (options?: Options) => + queryOptions< + ListBackupSchedulesResponse, + DefaultError, + ListBackupSchedulesResponse, + ReturnType + >({ queryFn: async ({ queryKey, signal }) => { const { data } = await listBackupSchedules({ ...options, @@ -960,28 +754,6 @@ export const listBackupSchedulesOptions = (options?: Options) => - createQueryKey("createBackupSchedule", options); - -/** - * Create a new backup schedule for a volume - */ -export const createBackupScheduleOptions = (options?: Options) => { - return queryOptions({ - queryFn: async ({ queryKey, signal }) => { - const { data } = await createBackupSchedule({ - ...options, - ...queryKey[0], - signal, - throwOnError: true, - }); - return data; - }, - queryKey: createBackupScheduleQueryKey(options), - }); -}; /** * Create a new backup schedule for a volume @@ -994,10 +766,10 @@ export const createBackupScheduleMutation = ( DefaultError, Options > = { - mutationFn: async (localOptions) => { + mutationFn: async (fnOptions) => { const { data } = await createBackupSchedule({ ...options, - ...localOptions, + ...fnOptions, throwOnError: true, }); return data; @@ -1017,10 +789,10 @@ export const deleteBackupScheduleMutation = ( DefaultError, Options > = { - mutationFn: async (localOptions) => { + mutationFn: async (fnOptions) => { const { data } = await deleteBackupSchedule({ ...options, - ...localOptions, + ...fnOptions, throwOnError: true, }); return data; @@ -1035,8 +807,13 @@ export const getBackupScheduleQueryKey = (options: Options) => { - return queryOptions({ +export const getBackupScheduleOptions = (options: Options) => + queryOptions< + GetBackupScheduleResponse, + DefaultError, + GetBackupScheduleResponse, + ReturnType + >({ queryFn: async ({ queryKey, signal }) => { const { data } = await getBackupSchedule({ ...options, @@ -1048,7 +825,6 @@ export const getBackupScheduleOptions = (options: Options }, queryKey: getBackupScheduleQueryKey(options), }); -}; /** * Update a backup schedule @@ -1061,10 +837,10 @@ export const updateBackupScheduleMutation = ( DefaultError, Options > = { - mutationFn: async (localOptions) => { + mutationFn: async (fnOptions) => { const { data } = await updateBackupSchedule({ ...options, - ...localOptions, + ...fnOptions, throwOnError: true, }); return data; @@ -1079,8 +855,13 @@ export const getBackupScheduleForVolumeQueryKey = (options: Options) => { - return queryOptions({ +export const getBackupScheduleForVolumeOptions = (options: Options) => + queryOptions< + GetBackupScheduleForVolumeResponse, + DefaultError, + GetBackupScheduleForVolumeResponse, + ReturnType + >({ queryFn: async ({ queryKey, signal }) => { const { data } = await getBackupScheduleForVolume({ ...options, @@ -1092,27 +873,6 @@ export const getBackupScheduleForVolumeOptions = (options: Options) => createQueryKey("runBackupNow", options); - -/** - * Trigger a backup immediately for a schedule - */ -export const runBackupNowOptions = (options: Options) => { - return queryOptions({ - queryFn: async ({ queryKey, signal }) => { - const { data } = await runBackupNow({ - ...options, - ...queryKey[0], - signal, - throwOnError: true, - }); - return data; - }, - queryKey: runBackupNowQueryKey(options), - }); -}; /** * Trigger a backup immediately for a schedule @@ -1121,10 +881,10 @@ export const runBackupNowMutation = ( options?: Partial>, ): UseMutationOptions> => { const mutationOptions: UseMutationOptions> = { - mutationFn: async (localOptions) => { + mutationFn: async (fnOptions) => { const { data } = await runBackupNow({ ...options, - ...localOptions, + ...fnOptions, throwOnError: true, }); return data; @@ -1133,26 +893,6 @@ export const runBackupNowMutation = ( return mutationOptions; }; -export const stopBackupQueryKey = (options: Options) => createQueryKey("stopBackup", options); - -/** - * Stop a backup that is currently in progress - */ -export const stopBackupOptions = (options: Options) => { - return queryOptions({ - queryFn: async ({ queryKey, signal }) => { - const { data } = await stopBackup({ - ...options, - ...queryKey[0], - signal, - throwOnError: true, - }); - return data; - }, - queryKey: stopBackupQueryKey(options), - }); -}; - /** * Stop a backup that is currently in progress */ @@ -1160,10 +900,10 @@ export const stopBackupMutation = ( options?: Partial>, ): UseMutationOptions> => { const mutationOptions: UseMutationOptions> = { - mutationFn: async (localOptions) => { + mutationFn: async (fnOptions) => { const { data } = await stopBackup({ ...options, - ...localOptions, + ...fnOptions, throwOnError: true, }); return data; @@ -1177,8 +917,8 @@ export const getSystemInfoQueryKey = (options?: Options) => c /** * Get system information including available capabilities */ -export const getSystemInfoOptions = (options?: Options) => { - return queryOptions({ +export const getSystemInfoOptions = (options?: Options) => + queryOptions>({ queryFn: async ({ queryKey, signal }) => { const { data } = await getSystemInfo({ ...options, @@ -1190,28 +930,6 @@ export const getSystemInfoOptions = (options?: Options) => { }, queryKey: getSystemInfoQueryKey(options), }); -}; - -export const downloadResticPasswordQueryKey = (options?: Options) => - createQueryKey("downloadResticPassword", options); - -/** - * Download the Restic password file for backup recovery. Requires password re-authentication. - */ -export const downloadResticPasswordOptions = (options?: Options) => { - return queryOptions({ - queryFn: async ({ queryKey, signal }) => { - const { data } = await downloadResticPassword({ - ...options, - ...queryKey[0], - signal, - throwOnError: true, - }); - return data; - }, - queryKey: downloadResticPasswordQueryKey(options), - }); -}; /** * Download the Restic password file for backup recovery. Requires password re-authentication. @@ -1224,10 +942,10 @@ export const downloadResticPasswordMutation = ( DefaultError, Options > = { - mutationFn: async (localOptions) => { + mutationFn: async (fnOptions) => { const { data } = await downloadResticPassword({ ...options, - ...localOptions, + ...fnOptions, throwOnError: true, }); return data; diff --git a/apps/client/app/api-client/client.gen.ts b/app/client/api-client/client.gen.ts similarity index 57% rename from apps/client/app/api-client/client.gen.ts rename to app/client/api-client/client.gen.ts index d30d8c1..2695317 100644 --- a/apps/client/app/api-client/client.gen.ts +++ b/app/client/api-client/client.gen.ts @@ -1,7 +1,7 @@ // This file is auto-generated by @hey-api/openapi-ts -import type { ClientOptions } from "./types.gen"; -import { type Config, type ClientOptions as DefaultClientOptions, createClient, createConfig } from "./client"; +import { type ClientOptions, type Config, createClient, createConfig } from "./client"; +import type { ClientOptions as ClientOptions2 } from "./types.gen"; /** * The `createClientConfig()` function will be called on client initialization @@ -11,12 +11,12 @@ import { type Config, type ClientOptions as DefaultClientOptions, createClient, * `setConfig()`. This is useful for example if you're using Next.js * to ensure your client always has the correct values. */ -export type CreateClientConfig = ( - override?: Config, -) => Config & T>; +export type CreateClientConfig = ( + override?: Config, +) => Config & T>; export const client = createClient( - createConfig({ + createConfig({ baseUrl: "http://192.168.2.42:4096", }), ); diff --git a/apps/client/app/api-client/client/client.gen.ts b/app/client/api-client/client/client.gen.ts similarity index 53% rename from apps/client/app/api-client/client/client.gen.ts rename to app/client/api-client/client/client.gen.ts index dc40889..e30d8ad 100644 --- a/apps/client/app/api-client/client/client.gen.ts +++ b/app/client/api-client/client/client.gen.ts @@ -1,6 +1,9 @@ // This file is auto-generated by @hey-api/openapi-ts -import type { Client, Config, ResolvedRequestOptions } from "./types.gen"; +import { createSseClient } from "../core/serverSentEvents.gen"; +import type { HttpMethod } from "../core/types.gen"; +import { getValidRequestBody } from "../core/utils.gen"; +import type { Client, Config, RequestOptions, ResolvedRequestOptions } from "./types.gen"; import { buildUrl, createConfig, @@ -28,7 +31,7 @@ export const createClient = (config: Config = {}): Client => { const interceptors = createInterceptors(); - const request: Client["request"] = async (options) => { + const beforeRequest = async (options: RequestOptions) => { const opts = { ..._config, ...options, @@ -48,25 +51,32 @@ export const createClient = (config: Config = {}): Client => { await opts.requestValidator(opts); } - if (opts.body && opts.bodySerializer) { + if (opts.body !== undefined && opts.bodySerializer) { opts.serializedBody = opts.bodySerializer(opts.body); } // remove Content-Type header if body is empty to avoid sending invalid requests - if (opts.serializedBody === undefined || opts.serializedBody === "") { + if (opts.body === undefined || opts.serializedBody === "") { opts.headers.delete("Content-Type"); } const url = buildUrl(opts); + + return { opts, url }; + }; + + const request: Client["request"] = async (options) => { + // @ts-expect-error + const { opts, url } = await beforeRequest(options); const requestInit: ReqInit = { redirect: "follow", ...opts, - body: opts.serializedBody, + body: getValidRequestBody(opts), }; let request = new Request(url, requestInit); - for (const fn of interceptors.request._fns) { + for (const fn of interceptors.request.fns) { if (fn) { request = await fn(request, opts); } @@ -75,9 +85,37 @@ export const createClient = (config: Config = {}): Client => { // fetch must be assigned here, otherwise it would throw the error: // TypeError: Failed to execute 'fetch' on 'Window': Illegal invocation const _fetch = opts.fetch!; - let response = await _fetch(request); + let response: Response; - for (const fn of interceptors.response._fns) { + try { + response = await _fetch(request); + } catch (error) { + // Handle fetch exceptions (AbortError, network errors, etc.) + let finalError = error; + + for (const fn of interceptors.error.fns) { + if (fn) { + finalError = (await fn(error, undefined as any, request, opts)) as unknown; + } + } + + finalError = finalError || ({} as unknown); + + if (opts.throwOnError) { + throw finalError; + } + + // Return error response + return opts.responseStyle === "data" + ? undefined + : { + error: finalError, + request, + response: undefined as any, + }; + } + + for (const fn of interceptors.response.fns) { if (fn) { response = await fn(response, request, opts); } @@ -89,18 +127,36 @@ export const createClient = (config: Config = {}): Client => { }; if (response.ok) { + const parseAs = + (opts.parseAs === "auto" ? getParseAs(response.headers.get("Content-Type")) : opts.parseAs) ?? "json"; + if (response.status === 204 || response.headers.get("Content-Length") === "0") { + let emptyData: any; + switch (parseAs) { + case "arrayBuffer": + case "blob": + case "text": + emptyData = await response[parseAs](); + break; + case "formData": + emptyData = new FormData(); + break; + case "stream": + emptyData = response.body; + break; + case "json": + default: + emptyData = {}; + break; + } return opts.responseStyle === "data" - ? {} + ? emptyData : { - data: {}, + data: emptyData, ...result, }; } - const parseAs = - (opts.parseAs === "auto" ? getParseAs(response.headers.get("Content-Type")) : opts.parseAs) ?? "json"; - let data: any; switch (parseAs) { case "arrayBuffer": @@ -149,7 +205,7 @@ export const createClient = (config: Config = {}): Client => { const error = jsonError ?? textError; let finalError = error; - for (const fn of interceptors.error._fns) { + for (const fn of interceptors.error.fns) { if (fn) { finalError = (await fn(error, response, request, opts)) as string; } @@ -170,20 +226,53 @@ export const createClient = (config: Config = {}): Client => { }; }; + const makeMethodFn = (method: Uppercase) => (options: RequestOptions) => request({ ...options, method }); + + const makeSseFn = (method: Uppercase) => async (options: RequestOptions) => { + const { opts, url } = await beforeRequest(options); + return createSseClient({ + ...opts, + body: opts.body as BodyInit | null | undefined, + headers: opts.headers as unknown as Record, + method, + onRequest: async (url, init) => { + let request = new Request(url, init); + for (const fn of interceptors.request.fns) { + if (fn) { + request = await fn(request, opts); + } + } + return request; + }, + url, + }); + }; + return { buildUrl, - connect: (options) => request({ ...options, method: "CONNECT" }), - delete: (options) => request({ ...options, method: "DELETE" }), - get: (options) => request({ ...options, method: "GET" }), + connect: makeMethodFn("CONNECT"), + delete: makeMethodFn("DELETE"), + get: makeMethodFn("GET"), getConfig, - head: (options) => request({ ...options, method: "HEAD" }), + head: makeMethodFn("HEAD"), interceptors, - options: (options) => request({ ...options, method: "OPTIONS" }), - patch: (options) => request({ ...options, method: "PATCH" }), - post: (options) => request({ ...options, method: "POST" }), - put: (options) => request({ ...options, method: "PUT" }), + options: makeMethodFn("OPTIONS"), + patch: makeMethodFn("PATCH"), + post: makeMethodFn("POST"), + put: makeMethodFn("PUT"), request, setConfig, - trace: (options) => request({ ...options, method: "TRACE" }), - }; + sse: { + connect: makeSseFn("CONNECT"), + delete: makeSseFn("DELETE"), + get: makeSseFn("GET"), + head: makeSseFn("HEAD"), + options: makeSseFn("OPTIONS"), + patch: makeSseFn("PATCH"), + post: makeSseFn("POST"), + put: makeSseFn("PUT"), + trace: makeSseFn("TRACE"), + }, + trace: makeMethodFn("TRACE"), + } as Client; }; diff --git a/apps/client/app/api-client/client/index.ts b/app/client/api-client/client/index.ts similarity index 89% rename from apps/client/app/api-client/client/index.ts rename to app/client/api-client/client/index.ts index 3d14a8e..99ab72d 100644 --- a/apps/client/app/api-client/client/index.ts +++ b/app/client/api-client/client/index.ts @@ -8,6 +8,7 @@ export { urlSearchParamsBodySerializer, } from "../core/bodySerializer.gen"; export { buildClientParams } from "../core/params.gen"; +export { serializeQueryKeyValue } from "../core/queryKeySerializer.gen"; export { createClient } from "./client.gen"; export type { Client, @@ -15,7 +16,6 @@ export type { Config, CreateClientConfig, Options, - OptionsLegacyParser, RequestOptions, RequestResult, ResolvedRequestOptions, diff --git a/apps/client/app/api-client/client/types.gen.ts b/app/client/api-client/client/types.gen.ts similarity index 80% rename from apps/client/app/api-client/client/types.gen.ts rename to app/client/api-client/client/types.gen.ts index 605d90f..71ed2eb 100644 --- a/apps/client/app/api-client/client/types.gen.ts +++ b/app/client/api-client/client/types.gen.ts @@ -1,6 +1,7 @@ // This file is auto-generated by @hey-api/openapi-ts import type { Auth } from "../core/auth.gen"; +import type { ServerSentEventsOptions, ServerSentEventsResult } from "../core/serverSentEvents.gen"; import type { Client as CoreClient, Config as CoreConfig } from "../core/types.gen"; import type { Middleware } from "./utils.gen"; @@ -19,7 +20,7 @@ export interface Config * * @default globalThis.fetch */ - fetch?: (request: Request) => ReturnType; + fetch?: typeof fetch; /** * Please don't use the Fetch client for Next.js applications. The `next` * options won't have any effect. @@ -51,13 +52,18 @@ export interface Config } export interface RequestOptions< + TData = unknown, TResponseStyle extends ResponseStyle = "fields", ThrowOnError extends boolean = boolean, Url extends string = string, > extends Config<{ - responseStyle: TResponseStyle; - throwOnError: ThrowOnError; - }> { + responseStyle: TResponseStyle; + throwOnError: ThrowOnError; + }>, + Pick< + ServerSentEventsOptions, + "onSseError" | "onSseEvent" | "sseDefaultRetryDelay" | "sseMaxRetryAttempts" | "sseMaxRetryDelay" + > { /** * Any body that you want to add to your request. * @@ -77,7 +83,7 @@ export interface ResolvedRequestOptions< TResponseStyle extends ResponseStyle = "fields", ThrowOnError extends boolean = boolean, Url extends string = string, -> extends RequestOptions { +> extends RequestOptions { serializedBody?: string; } @@ -128,17 +134,26 @@ type MethodFn = < ThrowOnError extends boolean = false, TResponseStyle extends ResponseStyle = "fields", >( - options: Omit, "method">, + options: Omit, "method">, ) => RequestResult; +type SseFn = < + TData = unknown, + TError = unknown, + ThrowOnError extends boolean = false, + TResponseStyle extends ResponseStyle = "fields", +>( + options: Omit, "method">, +) => Promise>; + type RequestFn = < TData = unknown, TError = unknown, ThrowOnError extends boolean = false, TResponseStyle extends ResponseStyle = "fields", >( - options: Omit, "method"> & - Pick>, "method">, + options: Omit, "method"> & + Pick>, "method">, ) => RequestResult; type BuildUrlFn = < @@ -149,10 +164,10 @@ type BuildUrlFn = < url: string; }, >( - options: Pick & Options, + options: TData & Options, ) => string; -export type Client = CoreClient & { +export type Client = CoreClient & { interceptors: Middleware; }; @@ -181,21 +196,7 @@ type OmitKeys = Pick>; export type Options< TData extends TDataShape = TDataShape, ThrowOnError extends boolean = boolean, + TResponse = unknown, TResponseStyle extends ResponseStyle = "fields", -> = OmitKeys, "body" | "path" | "query" | "url"> & Omit; - -export type OptionsLegacyParser< - TData = unknown, - ThrowOnError extends boolean = boolean, - TResponseStyle extends ResponseStyle = "fields", -> = TData extends { body?: any } - ? TData extends { headers?: any } - ? OmitKeys, "body" | "headers" | "url"> & TData - : OmitKeys, "body" | "url"> & - TData & - Pick, "headers"> - : TData extends { headers?: any } - ? OmitKeys, "headers" | "url"> & - TData & - Pick, "body"> - : OmitKeys, "url"> & TData; +> = OmitKeys, "body" | "path" | "query" | "url"> & + ([TData] extends [never] ? unknown : Omit); diff --git a/apps/client/app/api-client/client/utils.gen.ts b/app/client/api-client/client/utils.gen.ts similarity index 61% rename from apps/client/app/api-client/client/utils.gen.ts rename to app/client/api-client/client/utils.gen.ts index 3fe22ba..fb46044 100644 --- a/apps/client/app/api-client/client/utils.gen.ts +++ b/app/client/api-client/client/utils.gen.ts @@ -1,88 +1,13 @@ // This file is auto-generated by @hey-api/openapi-ts import { getAuthToken } from "../core/auth.gen"; -import type { QuerySerializer, QuerySerializerOptions } from "../core/bodySerializer.gen"; +import type { QuerySerializerOptions } from "../core/bodySerializer.gen"; import { jsonBodySerializer } from "../core/bodySerializer.gen"; import { serializeArrayParam, serializeObjectParam, serializePrimitiveParam } from "../core/pathSerializer.gen"; +import { getUrl } from "../core/utils.gen"; import type { Client, ClientOptions, Config, RequestOptions } from "./types.gen"; -interface PathSerializer { - path: Record; - url: string; -} - -const PATH_PARAM_RE = /\{[^{}]+\}/g; - -type ArrayStyle = "form" | "spaceDelimited" | "pipeDelimited"; -type MatrixStyle = "label" | "matrix" | "simple"; -type ArraySeparatorStyle = ArrayStyle | MatrixStyle; - -const defaultPathSerializer = ({ path, url: _url }: PathSerializer) => { - let url = _url; - const matches = _url.match(PATH_PARAM_RE); - if (matches) { - for (const match of matches) { - let explode = false; - let name = match.substring(1, match.length - 1); - let style: ArraySeparatorStyle = "simple"; - - if (name.endsWith("*")) { - explode = true; - name = name.substring(0, name.length - 1); - } - - if (name.startsWith(".")) { - name = name.substring(1); - style = "label"; - } else if (name.startsWith(";")) { - name = name.substring(1); - style = "matrix"; - } - - const value = path[name]; - - if (value === undefined || value === null) { - continue; - } - - if (Array.isArray(value)) { - url = url.replace(match, serializeArrayParam({ explode, name, style, value })); - continue; - } - - if (typeof value === "object") { - url = url.replace( - match, - serializeObjectParam({ - explode, - name, - style, - value: value as Record, - valueOnly: true, - }), - ); - continue; - } - - if (style === "matrix") { - url = url.replace( - match, - `;${serializePrimitiveParam({ - name, - value: value as string, - })}`, - ); - continue; - } - - const replaceValue = encodeURIComponent(style === "label" ? `.${value as string}` : (value as string)); - url = url.replace(match, replaceValue); - } - } - return url; -}; - -export const createQuerySerializer = ({ allowReserved, array, object }: QuerySerializerOptions = {}) => { +export const createQuerySerializer = ({ parameters = {}, ...args }: QuerySerializerOptions = {}) => { const querySerializer = (queryParams: T) => { const search: string[] = []; if (queryParams && typeof queryParams === "object") { @@ -93,29 +18,31 @@ export const createQuerySerializer = ({ allowReserved, array, objec continue; } + const options = parameters[name] || args; + if (Array.isArray(value)) { const serializedArray = serializeArrayParam({ - allowReserved, + allowReserved: options.allowReserved, explode: true, name, style: "form", value, - ...array, + ...options.array, }); if (serializedArray) search.push(serializedArray); } else if (typeof value === "object") { const serializedObject = serializeObjectParam({ - allowReserved, + allowReserved: options.allowReserved, explode: true, name, style: "deepObject", value: value as Record, - ...object, + ...options.object, }); if (serializedObject) search.push(serializedObject); } else { const serializedPrimitive = serializePrimitiveParam({ - allowReserved, + allowReserved: options.allowReserved, name, value: value as string, }); @@ -216,8 +143,8 @@ export const setAuthParams = async ({ } }; -export const buildUrl: Client["buildUrl"] = (options) => { - const url = getUrl({ +export const buildUrl: Client["buildUrl"] = (options) => + getUrl({ baseUrl: options.baseUrl as string, path: options.path, query: options.query, @@ -227,36 +154,6 @@ export const buildUrl: Client["buildUrl"] = (options) => { : createQuerySerializer(options.querySerializer), url: options.url, }); - return url; -}; - -export const getUrl = ({ - baseUrl, - path, - query, - querySerializer, - url: _url, -}: { - baseUrl?: string; - path?: Record; - query?: Record; - querySerializer: QuerySerializer; - url: string; -}) => { - const pathUrl = _url.startsWith("/") ? _url : `/${_url}`; - let url = (baseUrl ?? "") + pathUrl; - if (path) { - url = defaultPathSerializer({ path, url }); - } - let search = query ? querySerializer(query) : ""; - if (search.startsWith("?")) { - search = search.substring(1); - } - if (search) { - url += `?${search}`; - } - return url; -}; export const mergeConfigs = (a: Config, b: Config): Config => { const config = { ...a, ...b }; @@ -267,14 +164,22 @@ export const mergeConfigs = (a: Config, b: Config): Config => { return config; }; +const headersEntries = (headers: Headers): Array<[string, string]> => { + const entries: Array<[string, string]> = []; + headers.forEach((value, key) => { + entries.push([key, value]); + }); + return entries; +}; + export const mergeHeaders = (...headers: Array["headers"] | undefined>): Headers => { const mergedHeaders = new Headers(); for (const header of headers) { - if (!header || typeof header !== "object") { + if (!header) { continue; } - const iterator = header instanceof Headers ? header.entries() : Object.entries(header); + const iterator = header instanceof Headers ? headersEntries(header) : Object.entries(header); for (const [key, value] of iterator) { if (value === null) { @@ -305,61 +210,53 @@ type ReqInterceptor = (request: Req, options: Options) => Req | Pr type ResInterceptor = (response: Res, request: Req, options: Options) => Res | Promise; class Interceptors { - _fns: (Interceptor | null)[]; + fns: Array = []; - constructor() { - this._fns = []; + clear(): void { + this.fns = []; } - clear() { - this._fns = []; + eject(id: number | Interceptor): void { + const index = this.getInterceptorIndex(id); + if (this.fns[index]) { + this.fns[index] = null; + } + } + + exists(id: number | Interceptor): boolean { + const index = this.getInterceptorIndex(id); + return Boolean(this.fns[index]); } getInterceptorIndex(id: number | Interceptor): number { if (typeof id === "number") { - return this._fns[id] ? id : -1; - } else { - return this._fns.indexOf(id); + return this.fns[id] ? id : -1; } - } - exists(id: number | Interceptor) { - const index = this.getInterceptorIndex(id); - return !!this._fns[index]; + return this.fns.indexOf(id); } - eject(id: number | Interceptor) { + update(id: number | Interceptor, fn: Interceptor): number | Interceptor | false { const index = this.getInterceptorIndex(id); - if (this._fns[index]) { - this._fns[index] = null; - } - } - - update(id: number | Interceptor, fn: Interceptor) { - const index = this.getInterceptorIndex(id); - if (this._fns[index]) { - this._fns[index] = fn; + if (this.fns[index]) { + this.fns[index] = fn; return id; - } else { - return false; } + return false; } - use(fn: Interceptor) { - this._fns = [...this._fns, fn]; - return this._fns.length - 1; + use(fn: Interceptor): number { + this.fns.push(fn); + return this.fns.length - 1; } } -// `createInterceptors()` response, meant for external use as it does not -// expose internals export interface Middleware { - error: Pick>, "eject" | "use">; - request: Pick>, "eject" | "use">; - response: Pick>, "eject" | "use">; + error: Interceptors>; + request: Interceptors>; + response: Interceptors>; } -// do not add `Middleware` as return type so we can use _fns internally -export const createInterceptors = () => ({ +export const createInterceptors = (): Middleware => ({ error: new Interceptors>(), request: new Interceptors>(), response: new Interceptors>(), diff --git a/apps/client/app/api-client/core/auth.gen.ts b/app/client/api-client/core/auth.gen.ts similarity index 100% rename from apps/client/app/api-client/core/auth.gen.ts rename to app/client/api-client/core/auth.gen.ts diff --git a/apps/client/app/api-client/core/bodySerializer.gen.ts b/app/client/api-client/core/bodySerializer.gen.ts similarity index 82% rename from apps/client/app/api-client/core/bodySerializer.gen.ts rename to app/client/api-client/core/bodySerializer.gen.ts index 256d42d..10e7b3a 100644 --- a/apps/client/app/api-client/core/bodySerializer.gen.ts +++ b/app/client/api-client/core/bodySerializer.gen.ts @@ -6,11 +6,19 @@ export type QuerySerializer = (query: Record) => string; export type BodySerializer = (body: any) => any; -export interface QuerySerializerOptions { +type QuerySerializerOptionsObject = { allowReserved?: boolean; - array?: SerializerOptions; - object?: SerializerOptions; -} + array?: Partial>; + object?: Partial>; +}; + +export type QuerySerializerOptions = QuerySerializerOptionsObject & { + /** + * Per-parameter serialization overrides. When provided, these settings + * override the global array/object settings for specific parameter names. + */ + parameters?: Record; +}; const serializeFormDataPair = (data: FormData, key: string, value: unknown): void => { if (typeof value === "string" || value instanceof Blob) { diff --git a/apps/client/app/api-client/core/params.gen.ts b/app/client/api-client/core/params.gen.ts similarity index 78% rename from apps/client/app/api-client/core/params.gen.ts rename to app/client/api-client/core/params.gen.ts index db9fb4f..c1fb15c 100644 --- a/apps/client/app/api-client/core/params.gen.ts +++ b/app/client/api-client/core/params.gen.ts @@ -22,6 +22,17 @@ export type Field = */ key?: string; map?: string; + } + | { + /** + * Field name. This is the name we want the user to see and use. + */ + key: string; + /** + * Field mapped name. This is the name we want to use in the request. + * If `in` is omitted, `map` aliases `key` to the transport layer. + */ + map: Slot; }; export interface Fields { @@ -41,10 +52,14 @@ const extraPrefixes = Object.entries(extraPrefixesMap); type KeyMap = Map< string, - { - in: Slot; - map?: string; - } + | { + in: Slot; + map?: string; + } + | { + in?: never; + map: Slot; + } >; const buildKeyMap = (fields: FieldsConfig, map?: KeyMap): KeyMap => { @@ -60,6 +75,10 @@ const buildKeyMap = (fields: FieldsConfig, map?: KeyMap): KeyMap => { map: config.map, }); } + } else if ("key" in config) { + map.set(config.key, { + map: config.map, + }); } else if (config.args) { buildKeyMap(config.args, map); } @@ -108,7 +127,9 @@ export const buildClientParams = (args: ReadonlyArray, fields: FieldsCo if (config.key) { const field = map.get(config.key)!; const name = field.map || config.key; - (params[field.in] as Record)[name] = arg; + if (field.in) { + (params[field.in] as Record)[name] = arg; + } } else { params.body = arg; } @@ -117,16 +138,20 @@ export const buildClientParams = (args: ReadonlyArray, fields: FieldsCo const field = map.get(key); if (field) { - const name = field.map || key; - (params[field.in] as Record)[name] = value; + if (field.in) { + const name = field.map || key; + (params[field.in] as Record)[name] = value; + } else { + params[field.map] = value; + } } else { const extra = extraPrefixes.find(([prefix]) => key.startsWith(prefix)); if (extra) { const [prefix, slot] = extra; (params[slot] as Record)[key.slice(prefix.length)] = value; - } else { - for (const [slot, allowed] of Object.entries(config.allowExtra ?? {})) { + } else if ("allowExtra" in config && config.allowExtra) { + for (const [slot, allowed] of Object.entries(config.allowExtra)) { if (allowed) { (params[slot as Slot] as Record)[key] = value; break; diff --git a/apps/client/app/api-client/core/pathSerializer.gen.ts b/app/client/api-client/core/pathSerializer.gen.ts similarity index 100% rename from apps/client/app/api-client/core/pathSerializer.gen.ts rename to app/client/api-client/core/pathSerializer.gen.ts diff --git a/app/client/api-client/core/queryKeySerializer.gen.ts b/app/client/api-client/core/queryKeySerializer.gen.ts new file mode 100644 index 0000000..b3e8767 --- /dev/null +++ b/app/client/api-client/core/queryKeySerializer.gen.ts @@ -0,0 +1,111 @@ +// This file is auto-generated by @hey-api/openapi-ts + +/** + * JSON-friendly union that mirrors what Pinia Colada can hash. + */ +export type JsonValue = null | string | number | boolean | JsonValue[] | { [key: string]: JsonValue }; + +/** + * Replacer that converts non-JSON values (bigint, Date, etc.) to safe substitutes. + */ +export const queryKeyJsonReplacer = (_key: string, value: unknown) => { + if (value === undefined || typeof value === "function" || typeof value === "symbol") { + return undefined; + } + if (typeof value === "bigint") { + return value.toString(); + } + if (value instanceof Date) { + return value.toISOString(); + } + return value; +}; + +/** + * Safely stringifies a value and parses it back into a JsonValue. + */ +export const stringifyToJsonValue = (input: unknown): JsonValue | undefined => { + try { + const json = JSON.stringify(input, queryKeyJsonReplacer); + if (json === undefined) { + return undefined; + } + return JSON.parse(json) as JsonValue; + } catch { + return undefined; + } +}; + +/** + * Detects plain objects (including objects with a null prototype). + */ +const isPlainObject = (value: unknown): value is Record => { + if (value === null || typeof value !== "object") { + return false; + } + const prototype = Object.getPrototypeOf(value as object); + return prototype === Object.prototype || prototype === null; +}; + +/** + * Turns URLSearchParams into a sorted JSON object for deterministic keys. + */ +const serializeSearchParams = (params: URLSearchParams): JsonValue => { + const entries = Array.from(params.entries()).sort(([a], [b]) => a.localeCompare(b)); + const result: Record = {}; + + for (const [key, value] of entries) { + const existing = result[key]; + if (existing === undefined) { + result[key] = value; + continue; + } + + if (Array.isArray(existing)) { + (existing as string[]).push(value); + } else { + result[key] = [existing, value]; + } + } + + return result; +}; + +/** + * Normalizes any accepted value into a JSON-friendly shape for query keys. + */ +export const serializeQueryKeyValue = (value: unknown): JsonValue | undefined => { + if (value === null) { + return null; + } + + if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") { + return value; + } + + if (value === undefined || typeof value === "function" || typeof value === "symbol") { + return undefined; + } + + if (typeof value === "bigint") { + return value.toString(); + } + + if (value instanceof Date) { + return value.toISOString(); + } + + if (Array.isArray(value)) { + return stringifyToJsonValue(value); + } + + if (typeof URLSearchParams !== "undefined" && value instanceof URLSearchParams) { + return serializeSearchParams(value); + } + + if (isPlainObject(value)) { + return stringifyToJsonValue(value); + } + + return undefined; +}; diff --git a/app/client/api-client/core/serverSentEvents.gen.ts b/app/client/api-client/core/serverSentEvents.gen.ts new file mode 100644 index 0000000..8a27c45 --- /dev/null +++ b/app/client/api-client/core/serverSentEvents.gen.ts @@ -0,0 +1,237 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import type { Config } from "./types.gen"; + +export type ServerSentEventsOptions = Omit & + Pick & { + /** + * Fetch API implementation. You can use this option to provide a custom + * fetch instance. + * + * @default globalThis.fetch + */ + fetch?: typeof fetch; + /** + * Implementing clients can call request interceptors inside this hook. + */ + onRequest?: (url: string, init: RequestInit) => Promise; + /** + * Callback invoked when a network or parsing error occurs during streaming. + * + * This option applies only if the endpoint returns a stream of events. + * + * @param error The error that occurred. + */ + onSseError?: (error: unknown) => void; + /** + * Callback invoked when an event is streamed from the server. + * + * This option applies only if the endpoint returns a stream of events. + * + * @param event Event streamed from the server. + * @returns Nothing (void). + */ + onSseEvent?: (event: StreamEvent) => void; + serializedBody?: RequestInit["body"]; + /** + * Default retry delay in milliseconds. + * + * This option applies only if the endpoint returns a stream of events. + * + * @default 3000 + */ + sseDefaultRetryDelay?: number; + /** + * Maximum number of retry attempts before giving up. + */ + sseMaxRetryAttempts?: number; + /** + * Maximum retry delay in milliseconds. + * + * Applies only when exponential backoff is used. + * + * This option applies only if the endpoint returns a stream of events. + * + * @default 30000 + */ + sseMaxRetryDelay?: number; + /** + * Optional sleep function for retry backoff. + * + * Defaults to using `setTimeout`. + */ + sseSleepFn?: (ms: number) => Promise; + url: string; + }; + +export interface StreamEvent { + data: TData; + event?: string; + id?: string; + retry?: number; +} + +export type ServerSentEventsResult = { + stream: AsyncGenerator ? TData[keyof TData] : TData, TReturn, TNext>; +}; + +export const createSseClient = ({ + onRequest, + onSseError, + onSseEvent, + responseTransformer, + responseValidator, + sseDefaultRetryDelay, + sseMaxRetryAttempts, + sseMaxRetryDelay, + sseSleepFn, + url, + ...options +}: ServerSentEventsOptions): ServerSentEventsResult => { + let lastEventId: string | undefined; + + const sleep = sseSleepFn ?? ((ms: number) => new Promise((resolve) => setTimeout(resolve, ms))); + + const createStream = async function* () { + let retryDelay: number = sseDefaultRetryDelay ?? 3000; + let attempt = 0; + const signal = options.signal ?? new AbortController().signal; + + while (true) { + if (signal.aborted) break; + + attempt++; + + const headers = + options.headers instanceof Headers + ? options.headers + : new Headers(options.headers as Record | undefined); + + if (lastEventId !== undefined) { + headers.set("Last-Event-ID", lastEventId); + } + + try { + const requestInit: RequestInit = { + redirect: "follow", + ...options, + body: options.serializedBody, + headers, + signal, + }; + let request = new Request(url, requestInit); + if (onRequest) { + request = await onRequest(url, requestInit); + } + // fetch must be assigned here, otherwise it would throw the error: + // TypeError: Failed to execute 'fetch' on 'Window': Illegal invocation + const _fetch = options.fetch ?? globalThis.fetch; + const response = await _fetch(request); + + if (!response.ok) throw new Error(`SSE failed: ${response.status} ${response.statusText}`); + + if (!response.body) throw new Error("No body in SSE response"); + + const reader = response.body.pipeThrough(new TextDecoderStream()).getReader(); + + let buffer = ""; + + const abortHandler = () => { + try { + reader.cancel(); + } catch { + // noop + } + }; + + signal.addEventListener("abort", abortHandler); + + try { + while (true) { + const { done, value } = await reader.read(); + if (done) break; + buffer += value; + + const chunks = buffer.split("\n\n"); + buffer = chunks.pop() ?? ""; + + for (const chunk of chunks) { + const lines = chunk.split("\n"); + const dataLines: Array = []; + let eventName: string | undefined; + + for (const line of lines) { + if (line.startsWith("data:")) { + dataLines.push(line.replace(/^data:\s*/, "")); + } else if (line.startsWith("event:")) { + eventName = line.replace(/^event:\s*/, ""); + } else if (line.startsWith("id:")) { + lastEventId = line.replace(/^id:\s*/, ""); + } else if (line.startsWith("retry:")) { + const parsed = Number.parseInt(line.replace(/^retry:\s*/, ""), 10); + if (!Number.isNaN(parsed)) { + retryDelay = parsed; + } + } + } + + let data: unknown; + let parsedJson = false; + + if (dataLines.length) { + const rawData = dataLines.join("\n"); + try { + data = JSON.parse(rawData); + parsedJson = true; + } catch { + data = rawData; + } + } + + if (parsedJson) { + if (responseValidator) { + await responseValidator(data); + } + + if (responseTransformer) { + data = await responseTransformer(data); + } + } + + onSseEvent?.({ + data, + event: eventName, + id: lastEventId, + retry: retryDelay, + }); + + if (dataLines.length) { + yield data as any; + } + } + } + } finally { + signal.removeEventListener("abort", abortHandler); + reader.releaseLock(); + } + + break; // exit loop on normal completion + } catch (error) { + // connection failed or aborted; retry after delay + onSseError?.(error); + + if (sseMaxRetryAttempts !== undefined && attempt >= sseMaxRetryAttempts) { + break; // stop after firing error + } + + // exponential backoff: double retry each attempt, cap at 30s + const backoff = Math.min(retryDelay * 2 ** (attempt - 1), sseMaxRetryDelay ?? 30000); + await sleep(backoff); + } + } + }; + + const stream = createStream(); + + return { stream }; +}; diff --git a/apps/client/app/api-client/core/types.gen.ts b/app/client/api-client/core/types.gen.ts similarity index 87% rename from apps/client/app/api-client/core/types.gen.ts rename to app/client/api-client/core/types.gen.ts index be24654..62252e5 100644 --- a/apps/client/app/api-client/core/types.gen.ts +++ b/app/client/api-client/core/types.gen.ts @@ -3,24 +3,19 @@ import type { Auth, AuthToken } from "./auth.gen"; import type { BodySerializer, QuerySerializer, QuerySerializerOptions } from "./bodySerializer.gen"; -export interface Client { +export type HttpMethod = "connect" | "delete" | "get" | "head" | "options" | "patch" | "post" | "put" | "trace"; + +export type Client = { /** * Returns the final request URL. */ buildUrl: BuildUrlFn; - connect: MethodFn; - delete: MethodFn; - get: MethodFn; getConfig: () => Config; - head: MethodFn; - options: MethodFn; - patch: MethodFn; - post: MethodFn; - put: MethodFn; request: RequestFn; setConfig: (config: Config) => Config; - trace: MethodFn; -} +} & { + [K in HttpMethod]: MethodFn; +} & ([SseFn] extends [never] ? { sse?: never } : { sse: { [K in HttpMethod]: SseFn } }); export interface Config { /** @@ -47,7 +42,7 @@ export interface Config { * * {@link https://developer.mozilla.org/docs/Web/API/fetch#method See more} */ - method?: "CONNECT" | "DELETE" | "GET" | "HEAD" | "OPTIONS" | "PATCH" | "POST" | "PUT" | "TRACE"; + method?: Uppercase; /** * A function for serializing request query parameters. By default, arrays * will be exploded in form style, objects will be exploded in deepObject diff --git a/app/client/api-client/core/utils.gen.ts b/app/client/api-client/core/utils.gen.ts new file mode 100644 index 0000000..7e48839 --- /dev/null +++ b/app/client/api-client/core/utils.gen.ts @@ -0,0 +1,137 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import type { BodySerializer, QuerySerializer } from "./bodySerializer.gen"; +import { + type ArraySeparatorStyle, + serializeArrayParam, + serializeObjectParam, + serializePrimitiveParam, +} from "./pathSerializer.gen"; + +export interface PathSerializer { + path: Record; + url: string; +} + +export const PATH_PARAM_RE = /\{[^{}]+\}/g; + +export const defaultPathSerializer = ({ path, url: _url }: PathSerializer) => { + let url = _url; + const matches = _url.match(PATH_PARAM_RE); + if (matches) { + for (const match of matches) { + let explode = false; + let name = match.substring(1, match.length - 1); + let style: ArraySeparatorStyle = "simple"; + + if (name.endsWith("*")) { + explode = true; + name = name.substring(0, name.length - 1); + } + + if (name.startsWith(".")) { + name = name.substring(1); + style = "label"; + } else if (name.startsWith(";")) { + name = name.substring(1); + style = "matrix"; + } + + const value = path[name]; + + if (value === undefined || value === null) { + continue; + } + + if (Array.isArray(value)) { + url = url.replace(match, serializeArrayParam({ explode, name, style, value })); + continue; + } + + if (typeof value === "object") { + url = url.replace( + match, + serializeObjectParam({ + explode, + name, + style, + value: value as Record, + valueOnly: true, + }), + ); + continue; + } + + if (style === "matrix") { + url = url.replace( + match, + `;${serializePrimitiveParam({ + name, + value: value as string, + })}`, + ); + continue; + } + + const replaceValue = encodeURIComponent(style === "label" ? `.${value as string}` : (value as string)); + url = url.replace(match, replaceValue); + } + } + return url; +}; + +export const getUrl = ({ + baseUrl, + path, + query, + querySerializer, + url: _url, +}: { + baseUrl?: string; + path?: Record; + query?: Record; + querySerializer: QuerySerializer; + url: string; +}) => { + const pathUrl = _url.startsWith("/") ? _url : `/${_url}`; + let url = (baseUrl ?? "") + pathUrl; + if (path) { + url = defaultPathSerializer({ path, url }); + } + let search = query ? querySerializer(query) : ""; + if (search.startsWith("?")) { + search = search.substring(1); + } + if (search) { + url += `?${search}`; + } + return url; +}; + +export function getValidRequestBody(options: { + body?: unknown; + bodySerializer?: BodySerializer | null; + serializedBody?: unknown; +}) { + const hasBody = options.body !== undefined; + const isSerializedBody = hasBody && options.bodySerializer; + + if (isSerializedBody) { + if ("serializedBody" in options) { + const hasSerializedBody = options.serializedBody !== undefined && options.serializedBody !== ""; + + return hasSerializedBody ? options.serializedBody : null; + } + + // not all clients implement a serializedBody property (i.e. client-axios) + return options.body !== "" ? options.body : null; + } + + // plain/text body + if (hasBody) { + return options.body; + } + + // no body was provided + return undefined; +} diff --git a/apps/client/app/api-client/index.ts b/app/client/api-client/index.ts similarity index 69% rename from apps/client/app/api-client/index.ts rename to app/client/api-client/index.ts index da87079..7a71692 100644 --- a/apps/client/app/api-client/index.ts +++ b/app/client/api-client/index.ts @@ -1,3 +1,4 @@ // This file is auto-generated by @hey-api/openapi-ts -export * from "./types.gen"; + +export type * from "./types.gen"; export * from "./sdk.gen"; diff --git a/apps/client/app/api-client/sdk.gen.ts b/app/client/api-client/sdk.gen.ts similarity index 78% rename from apps/client/app/api-client/sdk.gen.ts rename to app/client/api-client/sdk.gen.ts index 824ca97..891623b 100644 --- a/apps/client/app/api-client/sdk.gen.ts +++ b/app/client/api-client/sdk.gen.ts @@ -1,92 +1,92 @@ // This file is auto-generated by @hey-api/openapi-ts -import type { Options as ClientOptions, TDataShape, Client } from "./client"; +import type { Client, Options as Options2, TDataShape } from "./client"; +import { client } from "./client.gen"; import type { - RegisterData, - RegisterResponses, + BrowseFilesystemData, + BrowseFilesystemResponses, + ChangePasswordData, + ChangePasswordResponses, + CreateBackupScheduleData, + CreateBackupScheduleResponses, + CreateRepositoryData, + CreateRepositoryResponses, + CreateVolumeData, + CreateVolumeResponses, + DeleteBackupScheduleData, + DeleteBackupScheduleResponses, + DeleteRepositoryData, + DeleteRepositoryResponses, + DeleteVolumeData, + DeleteVolumeResponses, + DoctorRepositoryData, + DoctorRepositoryResponses, + DownloadResticPasswordData, + DownloadResticPasswordResponses, + GetBackupScheduleData, + GetBackupScheduleForVolumeData, + GetBackupScheduleForVolumeResponses, + GetBackupScheduleResponses, + GetContainersUsingVolumeData, + GetContainersUsingVolumeErrors, + GetContainersUsingVolumeResponses, + GetMeData, + GetMeResponses, + GetRepositoryData, + GetRepositoryResponses, + GetSnapshotDetailsData, + GetSnapshotDetailsResponses, + GetStatusData, + GetStatusResponses, + GetSystemInfoData, + GetSystemInfoResponses, + GetVolumeData, + GetVolumeErrors, + GetVolumeResponses, + HealthCheckVolumeData, + HealthCheckVolumeErrors, + HealthCheckVolumeResponses, + ListBackupSchedulesData, + ListBackupSchedulesResponses, + ListFilesData, + ListFilesResponses, + ListRcloneRemotesData, + ListRcloneRemotesResponses, + ListRepositoriesData, + ListRepositoriesResponses, + ListSnapshotFilesData, + ListSnapshotFilesResponses, + ListSnapshotsData, + ListSnapshotsResponses, + ListVolumesData, + ListVolumesResponses, LoginData, LoginResponses, LogoutData, LogoutResponses, - GetMeData, - GetMeResponses, - GetStatusData, - GetStatusResponses, - ChangePasswordData, - ChangePasswordResponses, - ListVolumesData, - ListVolumesResponses, - CreateVolumeData, - CreateVolumeResponses, - TestConnectionData, - TestConnectionResponses, - DeleteVolumeData, - DeleteVolumeResponses, - GetVolumeData, - GetVolumeResponses, - GetVolumeErrors, - UpdateVolumeData, - UpdateVolumeResponses, - UpdateVolumeErrors, - GetContainersUsingVolumeData, - GetContainersUsingVolumeResponses, - GetContainersUsingVolumeErrors, MountVolumeData, MountVolumeResponses, - UnmountVolumeData, - UnmountVolumeResponses, - HealthCheckVolumeData, - HealthCheckVolumeResponses, - HealthCheckVolumeErrors, - ListFilesData, - ListFilesResponses, - BrowseFilesystemData, - BrowseFilesystemResponses, - ListRepositoriesData, - ListRepositoriesResponses, - CreateRepositoryData, - CreateRepositoryResponses, - ListRcloneRemotesData, - ListRcloneRemotesResponses, - DeleteRepositoryData, - DeleteRepositoryResponses, - GetRepositoryData, - GetRepositoryResponses, - ListSnapshotsData, - ListSnapshotsResponses, - GetSnapshotDetailsData, - GetSnapshotDetailsResponses, - ListSnapshotFilesData, - ListSnapshotFilesResponses, + RegisterData, + RegisterResponses, RestoreSnapshotData, RestoreSnapshotResponses, - DoctorRepositoryData, - DoctorRepositoryResponses, - ListBackupSchedulesData, - ListBackupSchedulesResponses, - CreateBackupScheduleData, - CreateBackupScheduleResponses, - DeleteBackupScheduleData, - DeleteBackupScheduleResponses, - GetBackupScheduleData, - GetBackupScheduleResponses, - UpdateBackupScheduleData, - UpdateBackupScheduleResponses, - GetBackupScheduleForVolumeData, - GetBackupScheduleForVolumeResponses, RunBackupNowData, RunBackupNowResponses, StopBackupData, - StopBackupResponses, StopBackupErrors, - GetSystemInfoData, - GetSystemInfoResponses, - DownloadResticPasswordData, - DownloadResticPasswordResponses, + StopBackupResponses, + TestConnectionData, + TestConnectionResponses, + UnmountVolumeData, + UnmountVolumeResponses, + UpdateBackupScheduleData, + UpdateBackupScheduleResponses, + UpdateVolumeData, + UpdateVolumeErrors, + UpdateVolumeResponses, } from "./types.gen"; -import { client as _heyApiClient } from "./client.gen"; -export type Options = ClientOptions< +export type Options = Options2< TData, ThrowOnError > & { @@ -107,7 +107,7 @@ export type Options(options?: Options) => { - return (options?.client ?? _heyApiClient).post({ + return (options?.client ?? client).post({ url: "/api/v1/auth/register", ...options, headers: { @@ -121,7 +121,7 @@ export const register = (options?: Options * Login with username and password */ export const login = (options?: Options) => { - return (options?.client ?? _heyApiClient).post({ + return (options?.client ?? client).post({ url: "/api/v1/auth/login", ...options, headers: { @@ -135,7 +135,7 @@ export const login = (options?: Options(options?: Options) => { - return (options?.client ?? _heyApiClient).post({ + return (options?.client ?? client).post({ url: "/api/v1/auth/logout", ...options, }); @@ -145,7 +145,7 @@ export const logout = (options?: Options(options?: Options) => { - return (options?.client ?? _heyApiClient).get({ + return (options?.client ?? client).get({ url: "/api/v1/auth/me", ...options, }); @@ -155,7 +155,7 @@ export const getMe = (options?: Options(options?: Options) => { - return (options?.client ?? _heyApiClient).get({ + return (options?.client ?? client).get({ url: "/api/v1/auth/status", ...options, }); @@ -167,7 +167,7 @@ export const getStatus = (options?: Option export const changePassword = ( options?: Options, ) => { - return (options?.client ?? _heyApiClient).post({ + return (options?.client ?? client).post({ url: "/api/v1/auth/change-password", ...options, headers: { @@ -181,7 +181,7 @@ export const changePassword = ( * List all volumes */ export const listVolumes = (options?: Options) => { - return (options?.client ?? _heyApiClient).get({ + return (options?.client ?? client).get({ url: "/api/v1/volumes", ...options, }); @@ -193,7 +193,7 @@ export const listVolumes = (options?: Opti export const createVolume = ( options?: Options, ) => { - return (options?.client ?? _heyApiClient).post({ + return (options?.client ?? client).post({ url: "/api/v1/volumes", ...options, headers: { @@ -209,7 +209,7 @@ export const createVolume = ( export const testConnection = ( options?: Options, ) => { - return (options?.client ?? _heyApiClient).post({ + return (options?.client ?? client).post({ url: "/api/v1/volumes/test-connection", ...options, headers: { @@ -225,7 +225,7 @@ export const testConnection = ( export const deleteVolume = ( options: Options, ) => { - return (options.client ?? _heyApiClient).delete({ + return (options.client ?? client).delete({ url: "/api/v1/volumes/{name}", ...options, }); @@ -235,7 +235,7 @@ export const deleteVolume = ( * Get a volume by name */ export const getVolume = (options: Options) => { - return (options.client ?? _heyApiClient).get({ + return (options.client ?? client).get({ url: "/api/v1/volumes/{name}", ...options, }); @@ -247,7 +247,7 @@ export const getVolume = (options: Options export const updateVolume = ( options: Options, ) => { - return (options.client ?? _heyApiClient).put({ + return (options.client ?? client).put({ url: "/api/v1/volumes/{name}", ...options, headers: { @@ -263,7 +263,7 @@ export const updateVolume = ( export const getContainersUsingVolume = ( options: Options, ) => { - return (options.client ?? _heyApiClient).get< + return (options.client ?? client).get< GetContainersUsingVolumeResponses, GetContainersUsingVolumeErrors, ThrowOnError @@ -277,7 +277,7 @@ export const getContainersUsingVolume = ( * Mount a volume */ export const mountVolume = (options: Options) => { - return (options.client ?? _heyApiClient).post({ + return (options.client ?? client).post({ url: "/api/v1/volumes/{name}/mount", ...options, }); @@ -289,7 +289,7 @@ export const mountVolume = (options: Optio export const unmountVolume = ( options: Options, ) => { - return (options.client ?? _heyApiClient).post({ + return (options.client ?? client).post({ url: "/api/v1/volumes/{name}/unmount", ...options, }); @@ -301,7 +301,7 @@ export const unmountVolume = ( export const healthCheckVolume = ( options: Options, ) => { - return (options.client ?? _heyApiClient).post({ + return (options.client ?? client).post({ url: "/api/v1/volumes/{name}/health-check", ...options, }); @@ -311,7 +311,7 @@ export const healthCheckVolume = ( * List files in a volume directory */ export const listFiles = (options: Options) => { - return (options.client ?? _heyApiClient).get({ + return (options.client ?? client).get({ url: "/api/v1/volumes/{name}/files", ...options, }); @@ -323,7 +323,7 @@ export const listFiles = (options: Options export const browseFilesystem = ( options?: Options, ) => { - return (options?.client ?? _heyApiClient).get({ + return (options?.client ?? client).get({ url: "/api/v1/volumes/filesystem/browse", ...options, }); @@ -335,7 +335,7 @@ export const browseFilesystem = ( export const listRepositories = ( options?: Options, ) => { - return (options?.client ?? _heyApiClient).get({ + return (options?.client ?? client).get({ url: "/api/v1/repositories", ...options, }); @@ -347,7 +347,7 @@ export const listRepositories = ( export const createRepository = ( options?: Options, ) => { - return (options?.client ?? _heyApiClient).post({ + return (options?.client ?? client).post({ url: "/api/v1/repositories", ...options, headers: { @@ -363,7 +363,7 @@ export const createRepository = ( export const listRcloneRemotes = ( options?: Options, ) => { - return (options?.client ?? _heyApiClient).get({ + return (options?.client ?? client).get({ url: "/api/v1/repositories/rclone-remotes", ...options, }); @@ -375,7 +375,7 @@ export const listRcloneRemotes = ( export const deleteRepository = ( options: Options, ) => { - return (options.client ?? _heyApiClient).delete({ + return (options.client ?? client).delete({ url: "/api/v1/repositories/{name}", ...options, }); @@ -387,7 +387,7 @@ export const deleteRepository = ( export const getRepository = ( options: Options, ) => { - return (options.client ?? _heyApiClient).get({ + return (options.client ?? client).get({ url: "/api/v1/repositories/{name}", ...options, }); @@ -399,7 +399,7 @@ export const getRepository = ( export const listSnapshots = ( options: Options, ) => { - return (options.client ?? _heyApiClient).get({ + return (options.client ?? client).get({ url: "/api/v1/repositories/{name}/snapshots", ...options, }); @@ -411,7 +411,7 @@ export const listSnapshots = ( export const getSnapshotDetails = ( options: Options, ) => { - return (options.client ?? _heyApiClient).get({ + return (options.client ?? client).get({ url: "/api/v1/repositories/{name}/snapshots/{snapshotId}", ...options, }); @@ -423,7 +423,7 @@ export const getSnapshotDetails = ( export const listSnapshotFiles = ( options: Options, ) => { - return (options.client ?? _heyApiClient).get({ + return (options.client ?? client).get({ url: "/api/v1/repositories/{name}/snapshots/{snapshotId}/files", ...options, }); @@ -435,7 +435,7 @@ export const listSnapshotFiles = ( export const restoreSnapshot = ( options: Options, ) => { - return (options.client ?? _heyApiClient).post({ + return (options.client ?? client).post({ url: "/api/v1/repositories/{name}/restore", ...options, headers: { @@ -451,7 +451,7 @@ export const restoreSnapshot = ( export const doctorRepository = ( options: Options, ) => { - return (options.client ?? _heyApiClient).post({ + return (options.client ?? client).post({ url: "/api/v1/repositories/{name}/doctor", ...options, }); @@ -463,7 +463,7 @@ export const doctorRepository = ( export const listBackupSchedules = ( options?: Options, ) => { - return (options?.client ?? _heyApiClient).get({ + return (options?.client ?? client).get({ url: "/api/v1/backups", ...options, }); @@ -475,7 +475,7 @@ export const listBackupSchedules = ( export const createBackupSchedule = ( options?: Options, ) => { - return (options?.client ?? _heyApiClient).post({ + return (options?.client ?? client).post({ url: "/api/v1/backups", ...options, headers: { @@ -491,7 +491,7 @@ export const createBackupSchedule = ( export const deleteBackupSchedule = ( options: Options, ) => { - return (options.client ?? _heyApiClient).delete({ + return (options.client ?? client).delete({ url: "/api/v1/backups/{scheduleId}", ...options, }); @@ -503,7 +503,7 @@ export const deleteBackupSchedule = ( export const getBackupSchedule = ( options: Options, ) => { - return (options.client ?? _heyApiClient).get({ + return (options.client ?? client).get({ url: "/api/v1/backups/{scheduleId}", ...options, }); @@ -515,7 +515,7 @@ export const getBackupSchedule = ( export const updateBackupSchedule = ( options: Options, ) => { - return (options.client ?? _heyApiClient).patch({ + return (options.client ?? client).patch({ url: "/api/v1/backups/{scheduleId}", ...options, headers: { @@ -531,7 +531,7 @@ export const updateBackupSchedule = ( export const getBackupScheduleForVolume = ( options: Options, ) => { - return (options.client ?? _heyApiClient).get({ + return (options.client ?? client).get({ url: "/api/v1/backups/volume/{volumeId}", ...options, }); @@ -543,7 +543,7 @@ export const getBackupScheduleForVolume = export const runBackupNow = ( options: Options, ) => { - return (options.client ?? _heyApiClient).post({ + return (options.client ?? client).post({ url: "/api/v1/backups/{scheduleId}/run", ...options, }); @@ -553,7 +553,7 @@ export const runBackupNow = ( * Stop a backup that is currently in progress */ export const stopBackup = (options: Options) => { - return (options.client ?? _heyApiClient).post({ + return (options.client ?? client).post({ url: "/api/v1/backups/{scheduleId}/stop", ...options, }); @@ -565,7 +565,7 @@ export const stopBackup = (options: Option export const getSystemInfo = ( options?: Options, ) => { - return (options?.client ?? _heyApiClient).get({ + return (options?.client ?? client).get({ url: "/api/v1/system/info", ...options, }); @@ -577,7 +577,7 @@ export const getSystemInfo = ( export const downloadResticPassword = ( options?: Options, ) => { - return (options?.client ?? _heyApiClient).post({ + return (options?.client ?? client).post({ url: "/api/v1/system/restic-password", ...options, headers: { diff --git a/apps/client/app/api-client/types.gen.ts b/app/client/api-client/types.gen.ts similarity index 100% rename from apps/client/app/api-client/types.gen.ts rename to app/client/api-client/types.gen.ts index 27a9215..2f5bca8 100644 --- a/apps/client/app/api-client/types.gen.ts +++ b/app/client/api-client/types.gen.ts @@ -1,5 +1,9 @@ // This file is auto-generated by @hey-api/openapi-ts +export type ClientOptions = { + baseUrl: "http://192.168.2.42:4096" | (string & {}); +}; + export type RegisterData = { body?: { password: string; @@ -1672,7 +1676,3 @@ export type DownloadResticPasswordResponses = { }; export type DownloadResticPasswordResponse = DownloadResticPasswordResponses[keyof DownloadResticPasswordResponses]; - -export type ClientOptions = { - baseUrl: "http://192.168.2.42:4096" | (string & {}); -}; diff --git a/apps/client/app/components/app-breadcrumb.tsx b/app/client/components/app-breadcrumb.tsx similarity index 90% rename from apps/client/app/components/app-breadcrumb.tsx rename to app/client/components/app-breadcrumb.tsx index ca215dc..5918651 100644 --- a/apps/client/app/components/app-breadcrumb.tsx +++ b/app/client/components/app-breadcrumb.tsx @@ -6,8 +6,8 @@ import { BreadcrumbList, BreadcrumbPage, BreadcrumbSeparator, -} from "~/components/ui/breadcrumb"; -import { useBreadcrumbs } from "~/lib/breadcrumbs"; +} from "~/client/components/ui/breadcrumb"; +import { useBreadcrumbs } from "~/client/lib/breadcrumbs"; export function AppBreadcrumb() { const breadcrumbs = useBreadcrumbs(); diff --git a/apps/client/app/components/app-sidebar.tsx b/app/client/components/app-sidebar.tsx similarity index 94% rename from apps/client/app/components/app-sidebar.tsx rename to app/client/components/app-sidebar.tsx index 78bc14c..9995ee1 100644 --- a/apps/client/app/components/app-sidebar.tsx +++ b/app/client/components/app-sidebar.tsx @@ -10,9 +10,9 @@ import { SidebarMenuButton, SidebarMenuItem, useSidebar, -} from "~/components/ui/sidebar"; -import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "~/components/ui/tooltip"; -import { cn } from "~/lib/utils"; +} from "~/client/components/ui/sidebar"; +import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "~/client/components/ui/tooltip"; +import { cn } from "~/client/lib/utils"; const items = [ { diff --git a/apps/client/app/components/auth-layout.tsx b/app/client/components/auth-layout.tsx similarity index 100% rename from apps/client/app/components/auth-layout.tsx rename to app/client/components/auth-layout.tsx diff --git a/apps/client/app/components/bytes-size.tsx b/app/client/components/bytes-size.tsx similarity index 100% rename from apps/client/app/components/bytes-size.tsx rename to app/client/components/bytes-size.tsx diff --git a/apps/client/app/components/create-repository-dialog.tsx b/app/client/components/create-repository-dialog.tsx similarity index 93% rename from apps/client/app/components/create-repository-dialog.tsx rename to app/client/components/create-repository-dialog.tsx index 88aa1dd..8a04e44 100644 --- a/apps/client/app/components/create-repository-dialog.tsx +++ b/app/client/components/create-repository-dialog.tsx @@ -2,12 +2,12 @@ import { useMutation } from "@tanstack/react-query"; import { Plus } from "lucide-react"; import { useId } from "react"; import { toast } from "sonner"; -import { createRepositoryMutation } from "~/api-client/@tanstack/react-query.gen"; -import { parseError } from "~/lib/errors"; +import { parseError } from "~/client/lib/errors"; import { CreateRepositoryForm } from "./create-repository-form"; import { Button } from "./ui/button"; import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle, DialogTrigger } from "./ui/dialog"; import { ScrollArea } from "./ui/scroll-area"; +import { createRepositoryMutation } from "../api-client/@tanstack/react-query.gen"; type Props = { open: boolean; diff --git a/apps/client/app/components/create-repository-form.tsx b/app/client/components/create-repository-form.tsx similarity index 95% rename from apps/client/app/components/create-repository-form.tsx rename to app/client/components/create-repository-form.tsx index d925ad3..120e0f6 100644 --- a/apps/client/app/components/create-repository-form.tsx +++ b/app/client/components/create-repository-form.tsx @@ -1,20 +1,20 @@ import { arktypeResolver } from "@hookform/resolvers/arktype"; -import { COMPRESSION_MODES, repositoryConfigSchema } from "@ironmount/schemas/restic"; import { type } from "arktype"; import { useEffect } from "react"; import { useForm } from "react-hook-form"; -import { cn, slugify } from "~/lib/utils"; +import { cn, slugify } from "~/client/lib/utils"; import { deepClean } from "~/utils/object"; import { Button } from "./ui/button"; import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from "./ui/form"; import { Input } from "./ui/input"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "./ui/select"; -import { listRcloneRemotesOptions } from "~/api-client/@tanstack/react-query.gen"; import { useQuery } from "@tanstack/react-query"; import { Alert, AlertDescription } from "./ui/alert"; import { ExternalLink } from "lucide-react"; import { Tooltip, TooltipContent, TooltipTrigger } from "./ui/tooltip"; -import { useSystemInfo } from "~/hooks/use-system-info"; +import { useSystemInfo } from "~/client/hooks/use-system-info"; +import { COMPRESSION_MODES, repositoryConfigSchema } from "~/schemas/restic"; +import { listRcloneRemotesOptions } from "../api-client/@tanstack/react-query.gen"; export const formSchema = type({ name: "2<=string<=32", @@ -61,17 +61,17 @@ export const CreateRepositoryForm = ({ const { watch } = form; const watchedBackend = watch("backend"); - const watchedName = watch("name"); const { data: rcloneRemotes, isLoading: isLoadingRemotes } = useQuery({ ...listRcloneRemotesOptions(), }); useEffect(() => { - if (watchedBackend && watchedBackend in defaultValuesForType) { - form.reset({ name: watchedName, ...defaultValuesForType[watchedBackend as keyof typeof defaultValuesForType] }); - } - }, [watchedBackend, watchedName, form]); + form.reset({ + name: form.getValues().name, + ...defaultValuesForType[watchedBackend as keyof typeof defaultValuesForType], + }); + }, [watchedBackend, form]); const { capabilities } = useSystemInfo(); diff --git a/apps/client/app/components/create-volume-dialog.tsx b/app/client/components/create-volume-dialog.tsx similarity index 92% rename from apps/client/app/components/create-volume-dialog.tsx rename to app/client/components/create-volume-dialog.tsx index e4d8880..6ad92ca 100644 --- a/apps/client/app/components/create-volume-dialog.tsx +++ b/app/client/components/create-volume-dialog.tsx @@ -2,12 +2,12 @@ import { useMutation } from "@tanstack/react-query"; import { Plus } from "lucide-react"; import { useId } from "react"; import { toast } from "sonner"; -import { createVolumeMutation } from "~/api-client/@tanstack/react-query.gen"; -import { parseError } from "~/lib/errors"; +import { parseError } from "~/client/lib/errors"; import { CreateVolumeForm } from "./create-volume-form"; import { Button } from "./ui/button"; import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle, DialogTrigger } from "./ui/dialog"; import { ScrollArea } from "./ui/scroll-area"; +import { createVolumeMutation } from "../api-client/@tanstack/react-query.gen"; type Props = { open: boolean; diff --git a/apps/client/app/components/create-volume-form.tsx b/app/client/components/create-volume-form.tsx similarity index 96% rename from apps/client/app/components/create-volume-form.tsx rename to app/client/components/create-volume-form.tsx index 961fdc5..33040c4 100644 --- a/apps/client/app/components/create-volume-form.tsx +++ b/app/client/components/create-volume-form.tsx @@ -1,18 +1,18 @@ import { arktypeResolver } from "@hookform/resolvers/arktype"; -import { volumeConfigSchema } from "@ironmount/schemas"; import { useMutation } from "@tanstack/react-query"; import { type } from "arktype"; import { CheckCircle, Loader2, XCircle } from "lucide-react"; import { useEffect, useState } from "react"; import { useForm } from "react-hook-form"; -import { testConnectionMutation } from "~/api-client/@tanstack/react-query.gen"; -import { cn, slugify } from "~/lib/utils"; +import { cn, slugify } from "~/client/lib/utils"; import { deepClean } from "~/utils/object"; import { DirectoryBrowser } from "./directory-browser"; import { Button } from "./ui/button"; import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from "./ui/form"; import { Input } from "./ui/input"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "./ui/select"; +import { volumeConfigSchema } from "~/schemas/volumes"; +import { testConnectionMutation } from "../api-client/@tanstack/react-query.gen"; export const formSchema = type({ name: "2<=string<=32", @@ -50,13 +50,15 @@ export const CreateVolumeForm = ({ onSubmit, mode = "create", initialValues, for const { watch, getValues } = form; const watchedBackend = watch("backend"); - const watchedName = watch("name"); useEffect(() => { if (mode === "create") { - form.reset({ name: watchedName, ...defaultValuesForType[watchedBackend as keyof typeof defaultValuesForType] }); + form.reset({ + name: form.getValues().name, + ...defaultValuesForType[watchedBackend as keyof typeof defaultValuesForType], + }); } - }, [watchedBackend, watchedName, form.reset, mode]); + }, [watchedBackend, form, mode]); const [testMessage, setTestMessage] = useState<{ success: boolean; message: string } | null>(null); @@ -141,19 +143,17 @@ export const CreateVolumeForm = ({ onSubmit, mode = "create", initialValues, for control={form.control} name="path" render={({ field }) => { - const [showBrowser, setShowBrowser] = useState(!field.value || field.value === "/"); - return ( Directory Path - {!showBrowser && field.value ? ( + {field.value ? (
Selected path:
{field.value}
-
diff --git a/apps/client/app/components/directory-browser.tsx b/app/client/components/directory-browser.tsx similarity index 97% rename from apps/client/app/components/directory-browser.tsx rename to app/client/components/directory-browser.tsx index 29650f9..dab1d4d 100644 --- a/apps/client/app/components/directory-browser.tsx +++ b/app/client/components/directory-browser.tsx @@ -1,8 +1,8 @@ import { useQuery, useQueryClient } from "@tanstack/react-query"; import { useCallback, useMemo, useState } from "react"; -import { browseFilesystemOptions } from "~/api-client/@tanstack/react-query.gen"; import { FileTree, type FileEntry } from "./file-tree"; import { ScrollArea } from "./ui/scroll-area"; +import { browseFilesystemOptions } from "../api-client/@tanstack/react-query.gen"; type Props = { onSelectPath: (path: string) => void; diff --git a/apps/client/app/components/empty-state.tsx b/app/client/components/empty-state.tsx similarity index 91% rename from apps/client/app/components/empty-state.tsx rename to app/client/components/empty-state.tsx index 4acf26f..41a89af 100644 --- a/apps/client/app/components/empty-state.tsx +++ b/app/client/components/empty-state.tsx @@ -17,7 +17,7 @@ export function EmptyState(props: EmptyStateProps) {
-
+
diff --git a/apps/client/app/components/file-tree.tsx b/app/client/components/file-tree.tsx similarity index 99% rename from apps/client/app/components/file-tree.tsx rename to app/client/components/file-tree.tsx index 88c45c1..f763662 100644 --- a/apps/client/app/components/file-tree.tsx +++ b/app/client/components/file-tree.tsx @@ -10,8 +10,8 @@ import { ChevronDown, ChevronRight, File as FileIcon, Folder as FolderIcon, FolderOpen, Loader2 } from "lucide-react"; import { memo, type ReactNode, useCallback, useEffect, useMemo, useState } from "react"; -import { cn } from "~/lib/utils"; -import { Checkbox } from "~/components/ui/checkbox"; +import { cn } from "~/client/lib/utils"; +import { Checkbox } from "~/client/components/ui/checkbox"; const NODE_PADDING_LEFT = 12; diff --git a/apps/client/app/components/grid-background.tsx b/app/client/components/grid-background.tsx similarity index 55% rename from apps/client/app/components/grid-background.tsx rename to app/client/components/grid-background.tsx index 945cedb..bac779e 100644 --- a/apps/client/app/components/grid-background.tsx +++ b/app/client/components/grid-background.tsx @@ -1,5 +1,5 @@ import type { ReactNode } from "react"; -import { cn } from "~/lib/utils"; +import { cn } from "~/client/lib/utils"; interface GridBackgroundProps { children: ReactNode; @@ -12,9 +12,9 @@ export function GridBackground({ children, className, containerClassName }: Grid
diff --git a/apps/client/app/components/layout.tsx b/app/client/components/layout.tsx similarity index 96% rename from apps/client/app/components/layout.tsx rename to app/client/components/layout.tsx index 70caebb..bc6df0e 100644 --- a/apps/client/app/components/layout.tsx +++ b/app/client/components/layout.tsx @@ -2,7 +2,6 @@ import { useMutation } from "@tanstack/react-query"; import { LifeBuoy } from "lucide-react"; import { Outlet, redirect, useNavigate } from "react-router"; import { toast } from "sonner"; -import { logoutMutation } from "~/api-client/@tanstack/react-query.gen"; import { appContext } from "~/context"; import { authMiddleware } from "~/middleware/auth"; import type { Route } from "./+types/layout"; @@ -11,6 +10,7 @@ import { GridBackground } from "./grid-background"; import { Button } from "./ui/button"; import { SidebarProvider, SidebarTrigger } from "./ui/sidebar"; import { AppSidebar } from "./app-sidebar"; +import { logoutMutation } from "../api-client/@tanstack/react-query.gen"; export const clientMiddleware = [authMiddleware]; @@ -42,7 +42,7 @@ export default function Layout({ loaderData }: Route.ComponentProps) {
-
+
diff --git a/apps/client/app/components/onoff.tsx b/app/client/components/onoff.tsx similarity index 95% rename from apps/client/app/components/onoff.tsx rename to app/client/components/onoff.tsx index a5b690d..7fe9508 100644 --- a/apps/client/app/components/onoff.tsx +++ b/app/client/components/onoff.tsx @@ -1,4 +1,4 @@ -import { cn } from "~/lib/utils"; +import { cn } from "~/client/lib/utils"; import { Switch } from "./ui/switch"; type Props = { diff --git a/apps/client/app/components/repository-icon.tsx b/app/client/components/repository-icon.tsx similarity index 87% rename from apps/client/app/components/repository-icon.tsx rename to app/client/components/repository-icon.tsx index 40d4152..aaf9d57 100644 --- a/apps/client/app/components/repository-icon.tsx +++ b/app/client/components/repository-icon.tsx @@ -1,5 +1,5 @@ -import type { RepositoryBackend } from "@ironmount/schemas/restic"; import { Database, HardDrive, Cloud } from "lucide-react"; +import type { RepositoryBackend } from "~/schemas/restic"; type Props = { backend: RepositoryBackend; diff --git a/apps/client/app/components/snapshots-table.tsx b/app/client/components/snapshots-table.tsx similarity index 93% rename from apps/client/app/components/snapshots-table.tsx rename to app/client/components/snapshots-table.tsx index 922ddde..e185290 100644 --- a/apps/client/app/components/snapshots-table.tsx +++ b/app/client/components/snapshots-table.tsx @@ -1,10 +1,10 @@ import { Calendar, Clock, Database, FolderTree, HardDrive } from "lucide-react"; import { useNavigate } from "react-router"; -import type { ListSnapshotsResponse } from "~/api-client/types.gen"; -import { ByteSize } from "~/components/bytes-size"; -import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "~/components/ui/table"; -import { Tooltip, TooltipContent, TooltipTrigger } from "~/components/ui/tooltip"; +import { ByteSize } from "~/client/components/bytes-size"; +import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "~/client/components/ui/table"; +import { Tooltip, TooltipContent, TooltipTrigger } from "~/client/components/ui/tooltip"; import { formatDuration } from "~/utils/utils"; +import type { ListSnapshotsResponse } from "../api-client"; type Snapshot = ListSnapshotsResponse[number]; diff --git a/apps/client/app/components/status-dot.tsx b/app/client/components/status-dot.tsx similarity index 82% rename from apps/client/app/components/status-dot.tsx rename to app/client/components/status-dot.tsx index 9206a33..5902c77 100644 --- a/apps/client/app/components/status-dot.tsx +++ b/app/client/components/status-dot.tsx @@ -1,5 +1,5 @@ -import type { VolumeStatus } from "~/lib/types"; -import { cn } from "~/lib/utils"; +import type { VolumeStatus } from "~/client/lib/types"; +import { cn } from "~/client/lib/utils"; import { Tooltip, TooltipContent, TooltipTrigger } from "./ui/tooltip"; export const StatusDot = ({ status }: { status: VolumeStatus }) => { @@ -38,10 +38,7 @@ export const StatusDot = ({ status }: { status: VolumeStatus }) => { )} /> )} - + diff --git a/apps/client/app/components/ui/alert-dialog.tsx b/app/client/components/ui/alert-dialog.tsx similarity index 97% rename from apps/client/app/components/ui/alert-dialog.tsx rename to app/client/components/ui/alert-dialog.tsx index 6bbf718..4105ce3 100644 --- a/apps/client/app/components/ui/alert-dialog.tsx +++ b/app/client/components/ui/alert-dialog.tsx @@ -1,7 +1,7 @@ import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog"; import type * as React from "react"; -import { buttonVariants } from "~/components/ui/button"; -import { cn } from "~/lib/utils"; +import { buttonVariants } from "~/client/components/ui/button"; +import { cn } from "~/client/lib/utils"; function AlertDialog({ ...props }: React.ComponentProps) { return ; diff --git a/apps/client/app/components/ui/alert.tsx b/app/client/components/ui/alert.tsx similarity index 97% rename from apps/client/app/components/ui/alert.tsx rename to app/client/components/ui/alert.tsx index b33dad7..1014ae7 100644 --- a/apps/client/app/components/ui/alert.tsx +++ b/app/client/components/ui/alert.tsx @@ -1,7 +1,7 @@ import * as React from "react"; import { cva, type VariantProps } from "class-variance-authority"; -import { cn } from "~/lib/utils"; +import { cn } from "~/client/lib/utils"; const alertVariants = cva( "relative w-full rounded-lg border px-4 py-3 text-sm [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground [&>svg~*]:pl-7", diff --git a/apps/client/app/components/ui/badge.tsx b/app/client/components/ui/badge.tsx similarity index 97% rename from apps/client/app/components/ui/badge.tsx rename to app/client/components/ui/badge.tsx index d752872..e98a53b 100644 --- a/apps/client/app/components/ui/badge.tsx +++ b/app/client/components/ui/badge.tsx @@ -2,7 +2,7 @@ import type * as React from "react"; import { Slot } from "@radix-ui/react-slot"; import { cva, type VariantProps } from "class-variance-authority"; -import { cn } from "~/lib/utils"; +import { cn } from "~/client/lib/utils"; const badgeVariants = cva( "inline-flex items-center justify-center rounded-full border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden", diff --git a/app/client/components/ui/breadcrumb.tsx b/app/client/components/ui/breadcrumb.tsx new file mode 100644 index 0000000..4d0d3ea --- /dev/null +++ b/app/client/components/ui/breadcrumb.tsx @@ -0,0 +1,92 @@ +import type * as React from "react"; +import { Slot } from "@radix-ui/react-slot"; +import { ChevronRight, MoreHorizontal } from "lucide-react"; + +import { cn } from "~/client/lib/utils"; + +function Breadcrumb({ ...props }: React.ComponentProps<"nav">) { + return