diff --git a/apps/client/app/api-client/@tanstack/react-query.gen.ts b/apps/client/app/api-client/@tanstack/react-query.gen.ts index 07752c4..de34603 100644 --- a/apps/client/app/api-client/@tanstack/react-query.gen.ts +++ b/apps/client/app/api-client/@tanstack/react-query.gen.ts @@ -1,15 +1,13 @@ // This file is auto-generated by @hey-api/openapi-ts -import { type Options, listVolumes, createVolume } from "../sdk.gen"; -import { - queryOptions, - type UseMutationOptions, - type DefaultError, -} from "@tanstack/react-query"; +import { type Options, listVolumes, createVolume, deleteVolume } from "../sdk.gen"; +import { queryOptions, type UseMutationOptions, type DefaultError } from "@tanstack/react-query"; import type { ListVolumesData, CreateVolumeData, CreateVolumeResponse, + DeleteVolumeData, + DeleteVolumeResponse, } from "../types.gen"; import { client as _heyApiClient } from "../client.gen"; @@ -29,9 +27,7 @@ const createQueryKey = ( ): [QueryKey[0]] => { const params: QueryKey[0] = { _id: id, - baseUrl: - options?.baseUrl || - (options?.client ?? _heyApiClient).getConfig().baseUrl, + baseUrl: options?.baseUrl || (options?.client ?? _heyApiClient).getConfig().baseUrl, } as QueryKey[0]; if (infinite) { params._infinite = infinite; @@ -54,8 +50,7 @@ const createQueryKey = ( return [params]; }; -export const listVolumesQueryKey = (options?: Options) => - createQueryKey("listVolumes", options); +export const listVolumesQueryKey = (options?: Options) => createQueryKey("listVolumes", options); /** * List all volumes @@ -75,8 +70,7 @@ export const listVolumesOptions = (options?: Options) => { }); }; -export const createVolumeQueryKey = (options?: Options) => - createQueryKey("createVolume", options); +export const createVolumeQueryKey = (options?: Options) => createQueryKey("createVolume", options); /** * Create a new volume @@ -101,16 +95,8 @@ export const createVolumeOptions = (options?: Options) => { */ export const createVolumeMutation = ( options?: Partial>, -): UseMutationOptions< - CreateVolumeResponse, - DefaultError, - Options -> => { - const mutationOptions: UseMutationOptions< - CreateVolumeResponse, - DefaultError, - Options - > = { +): UseMutationOptions> => { + const mutationOptions: UseMutationOptions> = { mutationFn: async (localOptions) => { const { data } = await createVolume({ ...options, @@ -122,3 +108,22 @@ export const createVolumeMutation = ( }; return mutationOptions; }; + +/** + * Delete a volume + */ +export const deleteVolumeMutation = ( + options?: Partial>, +): UseMutationOptions> => { + const mutationOptions: UseMutationOptions> = { + mutationFn: async (localOptions) => { + const { data } = await deleteVolume({ + ...options, + ...localOptions, + throwOnError: true, + }); + return data; + }, + }; + return mutationOptions; +}; diff --git a/apps/client/app/api-client/client.gen.ts b/apps/client/app/api-client/client.gen.ts index 435abf1..88fa974 100644 --- a/apps/client/app/api-client/client.gen.ts +++ b/apps/client/app/api-client/client.gen.ts @@ -1,12 +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 Config, type ClientOptions as DefaultClientOptions, createClient, createConfig } from "./client"; /** * The `createClientConfig()` function will be called on client initialization @@ -16,10 +11,9 @@ import { * `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({ diff --git a/apps/client/app/api-client/client/client.gen.ts b/apps/client/app/api-client/client/client.gen.ts index d5b4473..dc40889 100644 --- a/apps/client/app/api-client/client/client.gen.ts +++ b/apps/client/app/api-client/client/client.gen.ts @@ -26,12 +26,7 @@ export const createClient = (config: Config = {}): Client => { return getConfig(); }; - const interceptors = createInterceptors< - Request, - Response, - unknown, - ResolvedRequestOptions - >(); + const interceptors = createInterceptors(); const request: Client["request"] = async (options) => { const opts = { @@ -94,10 +89,7 @@ export const createClient = (config: Config = {}): Client => { }; if (response.ok) { - if ( - response.status === 204 || - response.headers.get("Content-Length") === "0" - ) { + if (response.status === 204 || response.headers.get("Content-Length") === "0") { return opts.responseStyle === "data" ? {} : { @@ -107,9 +99,7 @@ export const createClient = (config: Config = {}): Client => { } const parseAs = - (opts.parseAs === "auto" - ? getParseAs(response.headers.get("Content-Type")) - : opts.parseAs) ?? "json"; + (opts.parseAs === "auto" ? getParseAs(response.headers.get("Content-Type")) : opts.parseAs) ?? "json"; let data: any; switch (parseAs) { diff --git a/apps/client/app/api-client/client/types.gen.ts b/apps/client/app/api-client/client/types.gen.ts index 772a8ed..605d90f 100644 --- a/apps/client/app/api-client/client/types.gen.ts +++ b/apps/client/app/api-client/client/types.gen.ts @@ -1,10 +1,7 @@ // This file is auto-generated by @hey-api/openapi-ts import type { Auth } from "../core/auth.gen"; -import type { - Client as CoreClient, - Config as CoreConfig, -} from "../core/types.gen"; +import type { Client as CoreClient, Config as CoreConfig } from "../core/types.gen"; import type { Middleware } from "./utils.gen"; export type ResponseStyle = "data" | "fields"; @@ -38,14 +35,7 @@ export interface Config * * @default 'auto' */ - parseAs?: - | "arrayBuffer" - | "auto" - | "blob" - | "formData" - | "json" - | "stream" - | "text"; + parseAs?: "arrayBuffer" | "auto" | "blob" | "formData" | "json" | "stream" | "text"; /** * Should we return only data or multiple fields (data, error, response, etc.)? * @@ -103,32 +93,22 @@ export type RequestResult< ? TData[keyof TData] : TData : { - data: TData extends Record - ? TData[keyof TData] - : TData; + data: TData extends Record ? TData[keyof TData] : TData; request: Request; response: Response; } > : Promise< TResponseStyle extends "data" - ? - | (TData extends Record - ? TData[keyof TData] - : TData) - | undefined + ? (TData extends Record ? TData[keyof TData] : TData) | undefined : ( | { - data: TData extends Record - ? TData[keyof TData] - : TData; + data: TData extends Record ? TData[keyof TData] : TData; error: undefined; } | { data: undefined; - error: TError extends Record - ? TError[keyof TError] - : TError; + error: TError extends Record ? TError[keyof TError] : TError; } ) & { request: Request; @@ -202,11 +182,7 @@ export type Options< TData extends TDataShape = TDataShape, ThrowOnError extends boolean = boolean, TResponseStyle extends ResponseStyle = "fields", -> = OmitKeys< - RequestOptions, - "body" | "path" | "query" | "url" -> & - Omit; +> = OmitKeys, "body" | "path" | "query" | "url"> & Omit; export type OptionsLegacyParser< TData = unknown, @@ -214,19 +190,12 @@ export type OptionsLegacyParser< TResponseStyle extends ResponseStyle = "fields", > = TData extends { body?: any } ? TData extends { headers?: any } - ? OmitKeys< - RequestOptions, - "body" | "headers" | "url" - > & - TData + ? OmitKeys, "body" | "headers" | "url"> & TData : OmitKeys, "body" | "url"> & TData & Pick, "headers"> : TData extends { headers?: any } - ? OmitKeys< - RequestOptions, - "headers" | "url" - > & + ? OmitKeys, "headers" | "url"> & TData & Pick, "body"> : OmitKeys, "url"> & TData; diff --git a/apps/client/app/api-client/client/utils.gen.ts b/apps/client/app/api-client/client/utils.gen.ts index d451840..3fe22ba 100644 --- a/apps/client/app/api-client/client/utils.gen.ts +++ b/apps/client/app/api-client/client/utils.gen.ts @@ -1,22 +1,10 @@ // 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 { QuerySerializer, QuerySerializerOptions } from "../core/bodySerializer.gen"; import { jsonBodySerializer } from "../core/bodySerializer.gen"; -import { - serializeArrayParam, - serializeObjectParam, - serializePrimitiveParam, -} from "../core/pathSerializer.gen"; -import type { - Client, - ClientOptions, - Config, - RequestOptions, -} from "./types.gen"; +import { serializeArrayParam, serializeObjectParam, serializePrimitiveParam } from "../core/pathSerializer.gen"; +import type { Client, ClientOptions, Config, RequestOptions } from "./types.gen"; interface PathSerializer { path: Record; @@ -58,10 +46,7 @@ const defaultPathSerializer = ({ path, url: _url }: PathSerializer) => { } if (Array.isArray(value)) { - url = url.replace( - match, - serializeArrayParam({ explode, name, style, value }), - ); + url = url.replace(match, serializeArrayParam({ explode, name, style, value })); continue; } @@ -90,20 +75,14 @@ const defaultPathSerializer = ({ path, url: _url }: PathSerializer) => { continue; } - const replaceValue = encodeURIComponent( - style === "label" ? `.${value as string}` : (value as string), - ); + 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 = ({ allowReserved, array, object }: QuerySerializerOptions = {}) => { const querySerializer = (queryParams: T) => { const search: string[] = []; if (queryParams && typeof queryParams === "object") { @@ -152,9 +131,7 @@ export const createQuerySerializer = ({ /** * Infers parseAs value from provided Content-Type header. */ -export const getParseAs = ( - contentType: string | null, -): Exclude => { +export const getParseAs = (contentType: string | null): Exclude => { if (!contentType) { // If no Content-Type header is provided, the best we can do is return the raw response body, // which is effectively the same as the 'stream' option. @@ -167,10 +144,7 @@ export const getParseAs = ( return; } - if ( - cleanContent.startsWith("application/json") || - cleanContent.endsWith("+json") - ) { + if (cleanContent.startsWith("application/json") || cleanContent.endsWith("+json")) { return "json"; } @@ -178,11 +152,7 @@ export const getParseAs = ( return "formData"; } - if ( - ["application/", "audio/", "image/", "video/"].some((type) => - cleanContent.startsWith(type), - ) - ) { + if (["application/", "audio/", "image/", "video/"].some((type) => cleanContent.startsWith(type))) { return "blob"; } @@ -202,11 +172,7 @@ const checkForExistence = ( if (!name) { return false; } - if ( - options.headers.has(name) || - options.query?.[name] || - options.headers.get("Cookie")?.includes(`${name}=`) - ) { + if (options.headers.has(name) || options.query?.[name] || options.headers.get("Cookie")?.includes(`${name}=`)) { return true; } return false; @@ -301,17 +267,14 @@ export const mergeConfigs = (a: Config, b: Config): Config => { return config; }; -export const mergeHeaders = ( - ...headers: Array["headers"] | undefined> -): Headers => { +export const mergeHeaders = (...headers: Array["headers"] | undefined>): Headers => { const mergedHeaders = new Headers(); for (const header of headers) { if (!header || typeof header !== "object") { continue; } - const iterator = - header instanceof Headers ? header.entries() : Object.entries(header); + const iterator = header instanceof Headers ? header.entries() : Object.entries(header); for (const [key, value] of iterator) { if (value === null) { @@ -323,10 +286,7 @@ export const mergeHeaders = ( } else if (value !== undefined) { // assume object headers are meant to be JSON stringified, i.e. their // content value in OpenAPI specification is 'application/json' - mergedHeaders.set( - key, - typeof value === "object" ? JSON.stringify(value) : (value as string), - ); + mergedHeaders.set(key, typeof value === "object" ? JSON.stringify(value) : (value as string)); } } } @@ -340,16 +300,9 @@ type ErrInterceptor = ( options: Options, ) => Err | Promise; -type ReqInterceptor = ( - request: Req, - options: Options, -) => Req | Promise; +type ReqInterceptor = (request: Req, options: Options) => Req | Promise; -type ResInterceptor = ( - response: Res, - request: Req, - options: Options, -) => Res | Promise; +type ResInterceptor = (response: Res, request: Req, options: Options) => Res | Promise; class Interceptors { _fns: (Interceptor | null)[]; @@ -400,15 +353,9 @@ class Interceptors { // `createInterceptors()` response, meant for external use as it does not // expose internals export interface Middleware { - error: Pick< - Interceptors>, - "eject" | "use" - >; + error: Pick>, "eject" | "use">; request: Pick>, "eject" | "use">; - response: Pick< - Interceptors>, - "eject" | "use" - >; + response: Pick>, "eject" | "use">; } // do not add `Middleware` as return type so we can use _fns internally diff --git a/apps/client/app/api-client/core/auth.gen.ts b/apps/client/app/api-client/core/auth.gen.ts index 48eb510..9d80405 100644 --- a/apps/client/app/api-client/core/auth.gen.ts +++ b/apps/client/app/api-client/core/auth.gen.ts @@ -23,8 +23,7 @@ export const getAuthToken = async ( auth: Auth, callback: ((auth: Auth) => Promise | AuthToken) | AuthToken, ): Promise => { - const token = - typeof callback === "function" ? await callback(auth) : callback; + const token = typeof callback === "function" ? await callback(auth) : callback; if (!token) { return; diff --git a/apps/client/app/api-client/core/bodySerializer.gen.ts b/apps/client/app/api-client/core/bodySerializer.gen.ts index a6902b1..256d42d 100644 --- a/apps/client/app/api-client/core/bodySerializer.gen.ts +++ b/apps/client/app/api-client/core/bodySerializer.gen.ts @@ -1,10 +1,6 @@ // This file is auto-generated by @hey-api/openapi-ts -import type { - ArrayStyle, - ObjectStyle, - SerializerOptions, -} from "./pathSerializer.gen"; +import type { ArrayStyle, ObjectStyle, SerializerOptions } from "./pathSerializer.gen"; export type QuerySerializer = (query: Record) => string; @@ -16,11 +12,7 @@ export interface QuerySerializerOptions { object?: SerializerOptions; } -const serializeFormDataPair = ( - data: FormData, - key: string, - value: unknown, -): void => { +const serializeFormDataPair = (data: FormData, key: string, value: unknown): void => { if (typeof value === "string" || value instanceof Blob) { data.append(key, value); } else if (value instanceof Date) { @@ -30,11 +22,7 @@ const serializeFormDataPair = ( } }; -const serializeUrlSearchParamsPair = ( - data: URLSearchParams, - key: string, - value: unknown, -): void => { +const serializeUrlSearchParamsPair = (data: URLSearchParams, key: string, value: unknown): void => { if (typeof value === "string") { data.append(key, value); } else { @@ -43,9 +31,7 @@ const serializeUrlSearchParamsPair = ( }; export const formDataBodySerializer = { - bodySerializer: | Array>>( - body: T, - ): FormData => { + bodySerializer: | Array>>(body: T): FormData => { const data = new FormData(); Object.entries(body).forEach(([key, value]) => { @@ -65,15 +51,11 @@ export const formDataBodySerializer = { export const jsonBodySerializer = { bodySerializer: (body: T): string => - JSON.stringify(body, (_key, value) => - typeof value === "bigint" ? value.toString() : value, - ), + JSON.stringify(body, (_key, value) => (typeof value === "bigint" ? value.toString() : value)), }; export const urlSearchParamsBodySerializer = { - bodySerializer: | Array>>( - body: T, - ): string => { + bodySerializer: | Array>>(body: T): string => { const data = new URLSearchParams(); Object.entries(body).forEach(([key, value]) => { diff --git a/apps/client/app/api-client/core/params.gen.ts b/apps/client/app/api-client/core/params.gen.ts index 81f7760..db9fb4f 100644 --- a/apps/client/app/api-client/core/params.gen.ts +++ b/apps/client/app/api-client/core/params.gen.ts @@ -83,10 +83,7 @@ const stripEmptySlots = (params: Params) => { } }; -export const buildClientParams = ( - args: ReadonlyArray, - fields: FieldsConfig, -) => { +export const buildClientParams = (args: ReadonlyArray, fields: FieldsConfig) => { const params: Params = { body: {}, headers: {}, @@ -123,19 +120,13 @@ export const buildClientParams = ( const name = field.map || key; (params[field.in] as Record)[name] = value; } else { - const extra = extraPrefixes.find(([prefix]) => - key.startsWith(prefix), - ); + const extra = extraPrefixes.find(([prefix]) => key.startsWith(prefix)); if (extra) { const [prefix, slot] = extra; - (params[slot] as Record)[ - key.slice(prefix.length) - ] = value; + (params[slot] as Record)[key.slice(prefix.length)] = value; } else { - for (const [slot, allowed] of Object.entries( - 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/apps/client/app/api-client/core/pathSerializer.gen.ts index 2fe930d..9e3cccd 100644 --- a/apps/client/app/api-client/core/pathSerializer.gen.ts +++ b/apps/client/app/api-client/core/pathSerializer.gen.ts @@ -1,8 +1,6 @@ // This file is auto-generated by @hey-api/openapi-ts -interface SerializeOptions - extends SerializePrimitiveOptions, - SerializerOptions {} +interface SerializeOptions extends SerializePrimitiveOptions, SerializerOptions {} interface SerializePrimitiveOptions { allowReserved?: boolean; @@ -76,9 +74,9 @@ export const serializeArrayParam = ({ value: unknown[]; }) => { if (!explode) { - const joinedValues = ( - allowReserved ? value : value.map((v) => encodeURIComponent(v as string)) - ).join(separatorArrayNoExplode(style)); + const joinedValues = (allowReserved ? value : value.map((v) => encodeURIComponent(v as string))).join( + separatorArrayNoExplode(style), + ); switch (style) { case "label": return `.${joinedValues}`; @@ -105,16 +103,10 @@ export const serializeArrayParam = ({ }); }) .join(separator); - return style === "label" || style === "matrix" - ? separator + joinedValues - : joinedValues; + return style === "label" || style === "matrix" ? separator + joinedValues : joinedValues; }; -export const serializePrimitiveParam = ({ - allowReserved, - name, - value, -}: SerializePrimitiveParam) => { +export const serializePrimitiveParam = ({ allowReserved, name, value }: SerializePrimitiveParam) => { if (value === undefined || value === null) { return ""; } @@ -146,11 +138,7 @@ export const serializeObjectParam = ({ if (style !== "deepObject" && !explode) { let values: string[] = []; Object.entries(value).forEach(([key, v]) => { - values = [ - ...values, - key, - allowReserved ? (v as string) : encodeURIComponent(v as string), - ]; + values = [...values, key, allowReserved ? (v as string) : encodeURIComponent(v as string)]; }); const joinedValues = values.join(","); switch (style) { @@ -175,7 +163,5 @@ export const serializeObjectParam = ({ }), ) .join(separator); - return style === "label" || style === "matrix" - ? separator + joinedValues - : joinedValues; + return style === "label" || style === "matrix" ? separator + joinedValues : joinedValues; }; diff --git a/apps/client/app/api-client/core/types.gen.ts b/apps/client/app/api-client/core/types.gen.ts index 0bed182..be24654 100644 --- a/apps/client/app/api-client/core/types.gen.ts +++ b/apps/client/app/api-client/core/types.gen.ts @@ -1,18 +1,9 @@ // This file is auto-generated by @hey-api/openapi-ts import type { Auth, AuthToken } from "./auth.gen"; -import type { - BodySerializer, - QuerySerializer, - QuerySerializerOptions, -} from "./bodySerializer.gen"; +import type { BodySerializer, QuerySerializer, QuerySerializerOptions } from "./bodySerializer.gen"; -export interface Client< - RequestFn = never, - Config = unknown, - MethodFn = never, - BuildUrlFn = never, -> { +export interface Client { /** * Returns the final request URL. */ @@ -50,31 +41,13 @@ export interface Config { */ headers?: | RequestInit["headers"] - | Record< - string, - | string - | number - | boolean - | (string | number | boolean)[] - | null - | undefined - | unknown - >; + | Record; /** * The request method. * * {@link https://developer.mozilla.org/docs/Web/API/fetch#method See more} */ - method?: - | "CONNECT" - | "DELETE" - | "GET" - | "HEAD" - | "OPTIONS" - | "PATCH" - | "POST" - | "PUT" - | "TRACE"; + method?: "CONNECT" | "DELETE" | "GET" | "HEAD" | "OPTIONS" | "PATCH" | "POST" | "PUT" | "TRACE"; /** * A function for serializing request query parameters. By default, arrays * will be exploded in form style, objects will be exploded in deepObject @@ -114,7 +87,5 @@ type IsExactlyNeverOrNeverUndefined = [T] extends [never] : false; export type OmitNever> = { - [K in keyof T as IsExactlyNeverOrNeverUndefined extends true - ? never - : K]: T[K]; + [K in keyof T as IsExactlyNeverOrNeverUndefined extends true ? never : K]: T[K]; }; diff --git a/apps/client/app/api-client/sdk.gen.ts b/apps/client/app/api-client/sdk.gen.ts index fa8c244..8538f4e 100644 --- a/apps/client/app/api-client/sdk.gen.ts +++ b/apps/client/app/api-client/sdk.gen.ts @@ -6,13 +6,15 @@ import type { ListVolumesResponses, CreateVolumeData, CreateVolumeResponses, + DeleteVolumeData, + DeleteVolumeResponses, } from "./types.gen"; import { client as _heyApiClient } from "./client.gen"; -export type Options< - TData extends TDataShape = TDataShape, - ThrowOnError extends boolean = boolean, -> = ClientOptions & { +export type Options = ClientOptions< + TData, + ThrowOnError +> & { /** * You can provide a client instance returned by `createClient()` instead of * individual options. This might be also useful if you want to implement a @@ -29,14 +31,8 @@ export type Options< /** * List all volumes */ -export const listVolumes = ( - options?: Options, -) => { - return (options?.client ?? _heyApiClient).get< - ListVolumesResponses, - unknown, - ThrowOnError - >({ +export const listVolumes = (options?: Options) => { + return (options?.client ?? _heyApiClient).get({ url: "/api/v1/volumes", ...options, }); @@ -48,11 +44,7 @@ export const listVolumes = ( export const createVolume = ( options?: Options, ) => { - return (options?.client ?? _heyApiClient).post< - CreateVolumeResponses, - unknown, - ThrowOnError - >({ + return (options?.client ?? _heyApiClient).post({ url: "/api/v1/volumes", ...options, headers: { @@ -61,3 +53,15 @@ export const createVolume = ( }, }); }; + +/** + * Delete a volume + */ +export const deleteVolume = ( + options: Options, +) => { + return (options.client ?? _heyApiClient).delete({ + url: "/api/v1/volumes/{name}", + ...options, + }); +}; diff --git a/apps/client/app/api-client/types.gen.ts b/apps/client/app/api-client/types.gen.ts index 79876ba..e7a9b0d 100644 --- a/apps/client/app/api-client/types.gen.ts +++ b/apps/client/app/api-client/types.gen.ts @@ -14,14 +14,13 @@ export type ListVolumesResponses = { 200: { volumes: Array<{ createdAt: number; - mountpoint: string; name: string; + path: string; }>; }; }; -export type ListVolumesResponse = - ListVolumesResponses[keyof ListVolumesResponses]; +export type ListVolumesResponse = ListVolumesResponses[keyof ListVolumesResponses]; export type CreateVolumeData = { body?: { @@ -51,14 +50,36 @@ export type CreateVolumeResponses = { * Volume created successfully */ 201: { - createdAt: number; - mountpoint: string; - name: string; + message: string; + volume: { + createdAt: number; + name: string; + path: string; + }; }; }; -export type CreateVolumeResponse = - CreateVolumeResponses[keyof CreateVolumeResponses]; +export type CreateVolumeResponse = CreateVolumeResponses[keyof CreateVolumeResponses]; + +export type DeleteVolumeData = { + body?: never; + path: { + name: string; + }; + query?: never; + url: "/api/v1/volumes/{name}"; +}; + +export type DeleteVolumeResponses = { + /** + * Volume deleted successfully + */ + 200: { + message: string; + }; +}; + +export type DeleteVolumeResponse = DeleteVolumeResponses[keyof DeleteVolumeResponses]; export type ClientOptions = { baseUrl: "http://localhost:3000" | (string & {}); diff --git a/apps/client/app/components/ui/sonner.tsx b/apps/client/app/components/ui/sonner.tsx new file mode 100644 index 0000000..ec702d9 --- /dev/null +++ b/apps/client/app/components/ui/sonner.tsx @@ -0,0 +1,23 @@ +import { useTheme } from "next-themes"; +import { Toaster as Sonner, type ToasterProps } from "sonner"; + +const Toaster = ({ ...props }: ToasterProps) => { + const { theme = "system" } = useTheme(); + + return ( + + ); +}; + +export { Toaster }; diff --git a/apps/client/app/lib/errors.ts b/apps/client/app/lib/errors.ts new file mode 100644 index 0000000..e21584e --- /dev/null +++ b/apps/client/app/lib/errors.ts @@ -0,0 +1,7 @@ +export const parseError = (error?: unknown) => { + if (error && typeof error === "object" && "message" in error) { + return { message: error.message as string }; + } + + return undefined; +}; diff --git a/apps/client/app/root.tsx b/apps/client/app/root.tsx index 882338e..c275a27 100644 --- a/apps/client/app/root.tsx +++ b/apps/client/app/root.tsx @@ -1,12 +1,6 @@ import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; -import { - isRouteErrorResponse, - Links, - Meta, - Outlet, - Scripts, - ScrollRestoration, -} from "react-router"; +import { isRouteErrorResponse, Links, Meta, Outlet, Scripts, ScrollRestoration } from "react-router"; +import { Toaster } from "~/components/ui/sonner"; import type { Route } from "./+types/root"; import "./app.css"; @@ -43,6 +37,7 @@ export function Layout({ children }: { children: React.ReactNode }) { {children} + @@ -62,10 +57,7 @@ export function ErrorBoundary({ error }: Route.ErrorBoundaryProps) { if (isRouteErrorResponse(error)) { message = error.status === 404 ? "404" : "Error"; - details = - error.status === 404 - ? "The requested page could not be found." - : error.statusText || details; + details = error.status === 404 ? "The requested page could not be found." : error.statusText || details; } else if (import.meta.env.DEV && error && error instanceof Error) { details = error.message; stack = error.stack; diff --git a/apps/client/app/routes/home.tsx b/apps/client/app/routes/home.tsx index 7617658..d435a6b 100644 --- a/apps/client/app/routes/home.tsx +++ b/apps/client/app/routes/home.tsx @@ -1,6 +1,7 @@ import { Copy, Folder } from "lucide-react"; import { useState } from "react"; -import { listVolumes } from "~/api-client"; +import { useFetcher } from "react-router"; +import { createVolume, deleteVolume, listVolumes } from "~/api-client"; import { CreateVolumeDialog } from "~/components/create-volume-dialog"; import { Button } from "~/components/ui/button"; import { Input } from "~/components/ui/input"; @@ -8,7 +9,8 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "~ import { Table, TableBody, TableCaption, TableCell, TableHead, TableHeader, TableRow } from "~/components/ui/table"; import { cn } from "~/lib/utils"; import type { Route } from "./+types/home"; -import { useFetcher } from "react-router"; +import { parseError } from "~/lib/errors"; +import { toast } from "sonner"; export function meta(_: Route.MetaArgs) { return [ @@ -25,17 +27,29 @@ export async function clientAction({ request }: Route.ClientActionArgs) { const { _action, ...rest } = Object.fromEntries(formData.entries()); if (_action === "delete") { - return { yolo: "swag", _action: "delete" as const }; - console.log("Delete action triggered", rest); - // Delete volume logic + const { error } = await deleteVolume({ path: { name: rest.name as string } }); + + if (error) { + toast.error("Failed to delete volume", { + description: parseError(error)?.message, + }); + } else { + toast.success("Volume deleted successfully"); + } + + return { error: parseError(error), _action: "delete" as const }; } if (_action === "create") { - console.log("Create action triggered", rest); - return { - error: "Volume with this name already exists.", - _action: "create" as const, - }; + const { error } = await createVolume({ body: { name: rest.name as string, config: { backend: "directory" } } }); + if (error) { + toast.error("Failed to create volume", { + description: parseError(error)?.message, + }); + } else { + toast.success("Volume created successfully"); + } + return { error: parseError(error), _action: "create" as const }; } } @@ -131,7 +145,7 @@ export default function Home({ loaderData, actionData }: Route.ComponentProps) { - {volume.mountpoint} + {volume.path} diff --git a/apps/client/package.json b/apps/client/package.json index 3b6e1ec..c867a41 100644 --- a/apps/client/package.json +++ b/apps/client/package.json @@ -9,8 +9,8 @@ "typecheck": "react-router typegen && tsc" }, "dependencies": { - "@ironmount/server": "workspace:*", "@hookform/resolvers": "^5.2.1", + "@ironmount/server": "workspace:*", "@radix-ui/react-alert-dialog": "^1.1.15", "@radix-ui/react-dialog": "^1.1.15", "@radix-ui/react-label": "^2.1.7", @@ -25,10 +25,12 @@ "clsx": "^2.1.1", "isbot": "^5.1.27", "lucide-react": "^0.539.0", + "next-themes": "^0.4.6", "react": "^19.1.0", "react-dom": "^19.1.0", "react-hook-form": "^7.62.0", "react-router": "^7.7.1", + "sonner": "^2.0.7", "tailwind-merge": "^3.3.1", "zod": "^4.0.17" }, @@ -45,4 +47,3 @@ "vite-tsconfig-paths": "^5.1.4" } } - diff --git a/apps/server/src/db/schema.ts b/apps/server/src/db/schema.ts index eecf32c..e2eafed 100644 --- a/apps/server/src/db/schema.ts +++ b/apps/server/src/db/schema.ts @@ -25,9 +25,7 @@ const directoryConfigSchema = type({ backend: "'directory'", }); -export const volumeConfigSchema = nfsConfigSchema - .or(smbConfigSchema) - .or(directoryConfigSchema); +export const volumeConfigSchema = nfsConfigSchema.or(smbConfigSchema).or(directoryConfigSchema); export type BackendConfig = typeof volumeConfigSchema.infer; @@ -36,11 +34,9 @@ export const volumesTable = sqliteTable("volumes_table", { name: text().notNull().unique(), path: text().notNull(), type: text().notNull(), - createdAt: int("created_at").notNull().default(sql`(current_timestamp)`), - updatedAt: int("updated_at").notNull().default(sql`(current_timestamp)`), - config: text("config", { mode: "json" }) - .$type() - .notNull(), + createdAt: int("created_at", { mode: "timestamp" }).notNull().default(sql`(unixepoch())`), + updatedAt: int("updated_at", { mode: "timestamp" }).notNull().default(sql`(unixepoch())`), + config: text("config", { mode: "json" }).$type().notNull(), }); export type Volume = typeof volumesTable.$inferSelect; diff --git a/apps/server/src/modules/volumes/volume.controller.ts b/apps/server/src/modules/volumes/volume.controller.ts index 272f18f..f0988eb 100644 --- a/apps/server/src/modules/volumes/volume.controller.ts +++ b/apps/server/src/modules/volumes/volume.controller.ts @@ -1,7 +1,13 @@ import { Hono } from "hono"; import { validator } from "hono-openapi/arktype"; import { handleServiceError } from "../../utils/errors"; -import { createVolumeBody, createVolumeDto, type ListVolumesResponseDto, listVolumesDto } from "./volume.dto"; +import { + createVolumeBody, + createVolumeDto, + deleteVolumeDto, + type ListVolumesResponseDto, + listVolumesDto, +} from "./volume.dto"; import { volumeService } from "./volume.service"; export const volumeController = new Hono() @@ -11,8 +17,8 @@ export const volumeController = new Hono() const response = { volumes: volumes.map((volume) => ({ name: volume.name, - mountpoint: volume.path, - createdAt: volume.createdAt, + path: volume.path, + createdAt: volume.createdAt.getTime(), })), } satisfies ListVolumesResponseDto; @@ -29,6 +35,17 @@ export const volumeController = new Hono() return c.json({ message: "Volume created", volume: res.volume }); }) + .delete("/:name", deleteVolumeDto, async (c) => { + const { name } = c.req.param(); + const res = await volumeService.deleteVolume(name); + + if (res.error) { + const { message, status } = handleServiceError(res.error); + return c.json(message, status); + } + + return c.json({ message: "Volume deleted" }); + }) .get("/:name", (c) => { return c.json({ message: `Details of volume ${c.req.param("name")}` }); }) diff --git a/apps/server/src/modules/volumes/volume.dto.ts b/apps/server/src/modules/volumes/volume.dto.ts index c3a0a74..e5514ef 100644 --- a/apps/server/src/modules/volumes/volume.dto.ts +++ b/apps/server/src/modules/volumes/volume.dto.ts @@ -9,7 +9,7 @@ import { volumeConfigSchema } from "../../db/schema"; export const listVolumesResponse = type({ volumes: type({ name: "string", - mountpoint: "string", + path: "string", createdAt: "number", }).array(), }); @@ -41,9 +41,12 @@ export const createVolumeBody = type({ }); export const createVolumeResponse = type({ - name: "string", - mountpoint: "string", - createdAt: "number", + message: "string", + volume: type({ + name: "string", + path: "string", + createdAt: "number", + }), }); export const createVolumeDto = describeRoute({ @@ -62,3 +65,27 @@ export const createVolumeDto = describeRoute({ }, }, }); + +/** + * Delete a volume + */ +export const deleteVolumeResponse = type({ + message: "string", +}); + +export const deleteVolumeDto = describeRoute({ + description: "Delete a volume", + operationId: "deleteVolume", + validateResponse: true, + tags: ["Volumes"], + responses: { + 200: { + description: "Volume deleted successfully", + content: { + "application/json": { + schema: resolver(deleteVolumeResponse), + }, + }, + }, + }, +}); diff --git a/apps/server/src/modules/volumes/volume.service.ts b/apps/server/src/modules/volumes/volume.service.ts index 0340899..ef29d0a 100644 --- a/apps/server/src/modules/volumes/volume.service.ts +++ b/apps/server/src/modules/volumes/volume.service.ts @@ -39,6 +39,29 @@ const createVolume = async (name: string, backendConfig: BackendConfig) => { return { volume: val[0], status: 201 }; }; +const deleteVolume = async (name: string) => { + try { + const volume = await db.query.volumesTable.findFirst({ + where: eq(volumesTable.name, name), + }); + + if (!volume) { + return { error: new NotFoundError("Volume not found") }; + } + + const backend = createVolumeBackend(volume); + await backend.unmount(); + await db.delete(volumesTable).where(eq(volumesTable.name, name)); + return { status: 200 }; + } catch (error) { + return { + error: new InternalServerError("Failed to delete volume", { + cause: error, + }), + }; + } +}; + const mountVolume = async (name: string) => { try { const volume = await db.query.volumesTable.findFirst({ @@ -64,4 +87,5 @@ export const volumeService = { listVolumes, createVolume, mountVolume, + deleteVolume, }; diff --git a/bun.lock b/bun.lock index b333ece..3796e49 100644 --- a/bun.lock +++ b/bun.lock @@ -27,10 +27,12 @@ "clsx": "^2.1.1", "isbot": "^5.1.27", "lucide-react": "^0.539.0", + "next-themes": "^0.4.6", "react": "^19.1.0", "react-dom": "^19.1.0", "react-hook-form": "^7.62.0", "react-router": "^7.7.1", + "sonner": "^2.0.7", "tailwind-merge": "^3.3.1", "zod": "^4.0.17", }, @@ -723,6 +725,8 @@ "neo-async": ["neo-async@2.6.2", "", {}, "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw=="], + "next-themes": ["next-themes@0.4.6", "", { "peerDependencies": { "react": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc", "react-dom": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc" } }, "sha512-pZvgD5L0IEvX5/9GWyHMf3m8BKiVQwsCMHfoFosXtXBMnaS0ZnIJ9ST4b4NqLVKDEm8QBxoNNGNaBv2JNF6XNA=="], + "node-fetch-native": ["node-fetch-native@1.6.7", "", {}, "sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q=="], "node-releases": ["node-releases@2.0.19", "", {}, "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw=="], @@ -851,6 +855,8 @@ "slugify": ["slugify@1.6.6", "", {}, "sha512-h+z7HKHYXj6wJU+AnS/+IH8Uh9fdcX1Lrhg1/VMdf9PwoBQXFcXiAdsy2tSK0P6gKwJLXp02r90ahUCqHk9rrw=="], + "sonner": ["sonner@2.0.7", "", { "peerDependencies": { "react": "^18.0.0 || ^19.0.0 || ^19.0.0-rc", "react-dom": "^18.0.0 || ^19.0.0 || ^19.0.0-rc" } }, "sha512-W6ZN4p58k8aDKA4XPcx2hpIQXBRAgyiWVkYhT7CvK6D3iAu7xjvVyhQHg2/iaKJZ1XVJ4r7XuwGL+WGEK37i9w=="], + "source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="], "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],