feat: delete volume

This commit is contained in:
Nicolas Meienberger
2025-08-31 21:49:49 +02:00
parent 37effcb4e3
commit eb2fbe8f75
22 changed files with 282 additions and 316 deletions

View File

@@ -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 = <TOptions extends Options>(
): [QueryKey<TOptions>[0]] => {
const params: QueryKey<TOptions>[0] = {
_id: id,
baseUrl:
options?.baseUrl ||
(options?.client ?? _heyApiClient).getConfig().baseUrl,
baseUrl: options?.baseUrl || (options?.client ?? _heyApiClient).getConfig().baseUrl,
} as QueryKey<TOptions>[0];
if (infinite) {
params._infinite = infinite;
@@ -54,8 +50,7 @@ const createQueryKey = <TOptions extends Options>(
return [params];
};
export const listVolumesQueryKey = (options?: Options<ListVolumesData>) =>
createQueryKey("listVolumes", options);
export const listVolumesQueryKey = (options?: Options<ListVolumesData>) => createQueryKey("listVolumes", options);
/**
* List all volumes
@@ -75,8 +70,7 @@ export const listVolumesOptions = (options?: Options<ListVolumesData>) => {
});
};
export const createVolumeQueryKey = (options?: Options<CreateVolumeData>) =>
createQueryKey("createVolume", options);
export const createVolumeQueryKey = (options?: Options<CreateVolumeData>) => createQueryKey("createVolume", options);
/**
* Create a new volume
@@ -101,16 +95,8 @@ export const createVolumeOptions = (options?: Options<CreateVolumeData>) => {
*/
export const createVolumeMutation = (
options?: Partial<Options<CreateVolumeData>>,
): UseMutationOptions<
CreateVolumeResponse,
DefaultError,
Options<CreateVolumeData>
> => {
const mutationOptions: UseMutationOptions<
CreateVolumeResponse,
DefaultError,
Options<CreateVolumeData>
> = {
): UseMutationOptions<CreateVolumeResponse, DefaultError, Options<CreateVolumeData>> => {
const mutationOptions: UseMutationOptions<CreateVolumeResponse, DefaultError, Options<CreateVolumeData>> = {
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<Options<DeleteVolumeData>>,
): UseMutationOptions<DeleteVolumeResponse, DefaultError, Options<DeleteVolumeData>> => {
const mutationOptions: UseMutationOptions<DeleteVolumeResponse, DefaultError, Options<DeleteVolumeData>> = {
mutationFn: async (localOptions) => {
const { data } = await deleteVolume({
...options,
...localOptions,
throwOnError: true,
});
return data;
},
};
return mutationOptions;
};

View File

@@ -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<T extends DefaultClientOptions = ClientOptions> =
(
override?: Config<DefaultClientOptions & T>,
) => Config<Required<DefaultClientOptions> & T>;
export type CreateClientConfig<T extends DefaultClientOptions = ClientOptions> = (
override?: Config<DefaultClientOptions & T>,
) => Config<Required<DefaultClientOptions> & T>;
export const client = createClient(
createConfig<ClientOptions>({

View File

@@ -26,12 +26,7 @@ export const createClient = (config: Config = {}): Client => {
return getConfig();
};
const interceptors = createInterceptors<
Request,
Response,
unknown,
ResolvedRequestOptions
>();
const interceptors = createInterceptors<Request, Response, unknown, ResolvedRequestOptions>();
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) {

View File

@@ -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<T extends ClientOptions = ClientOptions>
*
* @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<string, unknown>
? TData[keyof TData]
: TData;
data: TData extends Record<string, unknown> ? TData[keyof TData] : TData;
request: Request;
response: Response;
}
>
: Promise<
TResponseStyle extends "data"
?
| (TData extends Record<string, unknown>
? TData[keyof TData]
: TData)
| undefined
? (TData extends Record<string, unknown> ? TData[keyof TData] : TData) | undefined
: (
| {
data: TData extends Record<string, unknown>
? TData[keyof TData]
: TData;
data: TData extends Record<string, unknown> ? TData[keyof TData] : TData;
error: undefined;
}
| {
data: undefined;
error: TError extends Record<string, unknown>
? TError[keyof TError]
: TError;
error: TError extends Record<string, unknown> ? 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<TResponseStyle, ThrowOnError>,
"body" | "path" | "query" | "url"
> &
Omit<TData, "url">;
> = OmitKeys<RequestOptions<TResponseStyle, ThrowOnError>, "body" | "path" | "query" | "url"> & Omit<TData, "url">;
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<TResponseStyle, ThrowOnError>,
"body" | "headers" | "url"
> &
TData
? OmitKeys<RequestOptions<TResponseStyle, ThrowOnError>, "body" | "headers" | "url"> & TData
: OmitKeys<RequestOptions<TResponseStyle, ThrowOnError>, "body" | "url"> &
TData &
Pick<RequestOptions<TResponseStyle, ThrowOnError>, "headers">
: TData extends { headers?: any }
? OmitKeys<
RequestOptions<TResponseStyle, ThrowOnError>,
"headers" | "url"
> &
? OmitKeys<RequestOptions<TResponseStyle, ThrowOnError>, "headers" | "url"> &
TData &
Pick<RequestOptions<TResponseStyle, ThrowOnError>, "body">
: OmitKeys<RequestOptions<TResponseStyle, ThrowOnError>, "url"> & TData;

View File

@@ -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<string, unknown>;
@@ -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 = <T = unknown>({
allowReserved,
array,
object,
}: QuerySerializerOptions = {}) => {
export const createQuerySerializer = <T = unknown>({ allowReserved, array, object }: QuerySerializerOptions = {}) => {
const querySerializer = (queryParams: T) => {
const search: string[] = [];
if (queryParams && typeof queryParams === "object") {
@@ -152,9 +131,7 @@ export const createQuerySerializer = <T = unknown>({
/**
* Infers parseAs value from provided Content-Type header.
*/
export const getParseAs = (
contentType: string | null,
): Exclude<Config["parseAs"], "auto"> => {
export const getParseAs = (contentType: string | null): Exclude<Config["parseAs"], "auto"> => {
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<Required<Config>["headers"] | undefined>
): Headers => {
export const mergeHeaders = (...headers: Array<Required<Config>["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<Err, Res, Req, Options> = (
options: Options,
) => Err | Promise<Err>;
type ReqInterceptor<Req, Options> = (
request: Req,
options: Options,
) => Req | Promise<Req>;
type ReqInterceptor<Req, Options> = (request: Req, options: Options) => Req | Promise<Req>;
type ResInterceptor<Res, Req, Options> = (
response: Res,
request: Req,
options: Options,
) => Res | Promise<Res>;
type ResInterceptor<Res, Req, Options> = (response: Res, request: Req, options: Options) => Res | Promise<Res>;
class Interceptors<Interceptor> {
_fns: (Interceptor | null)[];
@@ -400,15 +353,9 @@ class Interceptors<Interceptor> {
// `createInterceptors()` response, meant for external use as it does not
// expose internals
export interface Middleware<Req, Res, Err, Options> {
error: Pick<
Interceptors<ErrInterceptor<Err, Res, Req, Options>>,
"eject" | "use"
>;
error: Pick<Interceptors<ErrInterceptor<Err, Res, Req, Options>>, "eject" | "use">;
request: Pick<Interceptors<ReqInterceptor<Req, Options>>, "eject" | "use">;
response: Pick<
Interceptors<ResInterceptor<Res, Req, Options>>,
"eject" | "use"
>;
response: Pick<Interceptors<ResInterceptor<Res, Req, Options>>, "eject" | "use">;
}
// do not add `Middleware` as return type so we can use _fns internally

View File

@@ -23,8 +23,7 @@ export const getAuthToken = async (
auth: Auth,
callback: ((auth: Auth) => Promise<AuthToken> | AuthToken) | AuthToken,
): Promise<string | undefined> => {
const token =
typeof callback === "function" ? await callback(auth) : callback;
const token = typeof callback === "function" ? await callback(auth) : callback;
if (!token) {
return;

View File

@@ -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, unknown>) => string;
@@ -16,11 +12,7 @@ export interface QuerySerializerOptions {
object?: SerializerOptions<ObjectStyle>;
}
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: <T extends Record<string, any> | Array<Record<string, any>>>(
body: T,
): FormData => {
bodySerializer: <T extends Record<string, any> | Array<Record<string, any>>>(body: T): FormData => {
const data = new FormData();
Object.entries(body).forEach(([key, value]) => {
@@ -65,15 +51,11 @@ export const formDataBodySerializer = {
export const jsonBodySerializer = {
bodySerializer: <T>(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: <T extends Record<string, any> | Array<Record<string, any>>>(
body: T,
): string => {
bodySerializer: <T extends Record<string, any> | Array<Record<string, any>>>(body: T): string => {
const data = new URLSearchParams();
Object.entries(body).forEach(([key, value]) => {

View File

@@ -83,10 +83,7 @@ const stripEmptySlots = (params: Params) => {
}
};
export const buildClientParams = (
args: ReadonlyArray<unknown>,
fields: FieldsConfig,
) => {
export const buildClientParams = (args: ReadonlyArray<unknown>, fields: FieldsConfig) => {
const params: Params = {
body: {},
headers: {},
@@ -123,19 +120,13 @@ export const buildClientParams = (
const name = field.map || key;
(params[field.in] as Record<string, unknown>)[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<string, unknown>)[
key.slice(prefix.length)
] = value;
(params[slot] as Record<string, unknown>)[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<string, unknown>)[key] = value;
break;

View File

@@ -1,8 +1,6 @@
// This file is auto-generated by @hey-api/openapi-ts
interface SerializeOptions<T>
extends SerializePrimitiveOptions,
SerializerOptions<T> {}
interface SerializeOptions<T> extends SerializePrimitiveOptions, SerializerOptions<T> {}
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;
};

View File

@@ -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<RequestFn = never, Config = unknown, MethodFn = never, BuildUrlFn = never> {
/**
* 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<string, string | number | boolean | (string | number | boolean)[] | null | undefined | unknown>;
/**
* 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> = [T] extends [never]
: false;
export type OmitNever<T extends Record<string, unknown>> = {
[K in keyof T as IsExactlyNeverOrNeverUndefined<T[K]> extends true
? never
: K]: T[K];
[K in keyof T as IsExactlyNeverOrNeverUndefined<T[K]> extends true ? never : K]: T[K];
};

View File

@@ -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<TData, ThrowOnError> & {
export type Options<TData extends TDataShape = TDataShape, ThrowOnError extends boolean = boolean> = 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 = <ThrowOnError extends boolean = false>(
options?: Options<ListVolumesData, ThrowOnError>,
) => {
return (options?.client ?? _heyApiClient).get<
ListVolumesResponses,
unknown,
ThrowOnError
>({
export const listVolumes = <ThrowOnError extends boolean = false>(options?: Options<ListVolumesData, ThrowOnError>) => {
return (options?.client ?? _heyApiClient).get<ListVolumesResponses, unknown, ThrowOnError>({
url: "/api/v1/volumes",
...options,
});
@@ -48,11 +44,7 @@ export const listVolumes = <ThrowOnError extends boolean = false>(
export const createVolume = <ThrowOnError extends boolean = false>(
options?: Options<CreateVolumeData, ThrowOnError>,
) => {
return (options?.client ?? _heyApiClient).post<
CreateVolumeResponses,
unknown,
ThrowOnError
>({
return (options?.client ?? _heyApiClient).post<CreateVolumeResponses, unknown, ThrowOnError>({
url: "/api/v1/volumes",
...options,
headers: {
@@ -61,3 +53,15 @@ export const createVolume = <ThrowOnError extends boolean = false>(
},
});
};
/**
* Delete a volume
*/
export const deleteVolume = <ThrowOnError extends boolean = false>(
options: Options<DeleteVolumeData, ThrowOnError>,
) => {
return (options.client ?? _heyApiClient).delete<DeleteVolumeResponses, unknown, ThrowOnError>({
url: "/api/v1/volumes/{name}",
...options,
});
};

View File

@@ -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 & {});

View File

@@ -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 (
<Sonner
theme={theme as ToasterProps["theme"]}
className="toaster group"
style={
{
"--normal-bg": "var(--popover)",
"--normal-text": "var(--popover-foreground)",
"--normal-border": "var(--border)",
} as React.CSSProperties
}
{...props}
/>
);
};
export { Toaster };

View File

@@ -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;
};

View File

@@ -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 }) {
<QueryClientProvider client={queryClient}>
<body className="h-screen">
{children}
<Toaster />
<ScrollRestoration />
<Scripts />
</body>
@@ -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;

View File

@@ -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) {
<TableCell>
<span className="flex items-center gap-2">
<span className="text-muted-foreground text-xs truncate bg-primary/10 rounded-md px-2 py-1">
{volume.mountpoint}
{volume.path}
</span>
<Copy size={10} />
</span>

View File

@@ -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"
}
}

View File

@@ -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<typeof volumeConfigSchema.inferOut>()
.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<typeof volumeConfigSchema.inferOut>().notNull(),
});
export type Volume = typeof volumesTable.$inferSelect;

View File

@@ -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")}` });
})

View File

@@ -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),
},
},
},
},
});

View File

@@ -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,
};

View File

@@ -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=="],