refactor: simplify dtos and improve type saftey in json returns

This commit is contained in:
Nicolas Meienberger
2025-10-29 18:28:00 +01:00
parent d1c1adaba7
commit b188a84af3
26 changed files with 667 additions and 751 deletions

View File

@@ -4,22 +4,23 @@ import {
createVolumeBody,
createVolumeDto,
deleteVolumeDto,
type GetVolumeResponseDto,
getContainersDto,
getVolumeDto,
healthCheckDto,
type ListContainersResponseDto,
type ListFilesResponseDto,
type ListVolumesResponseDto,
type ListVolumesDto,
listFilesDto,
listVolumesDto,
mountVolumeDto,
testConnectionBody,
testConnectionDto,
type UpdateVolumeResponseDto,
unmountVolumeDto,
updateVolumeBody,
updateVolumeDto,
type CreateVolumeDto,
type GetVolumeDto,
type ListContainersDto,
type UpdateVolumeDto,
type ListFilesDto,
} from "./volume.dto";
import { volumeService } from "./volume.service";
import { getVolumePath } from "./helpers";
@@ -32,19 +33,21 @@ export const volumeController = new Hono()
volumes: volumes.map((volume) => ({
path: getVolumePath(volume.name),
...volume,
updatedAt: volume.updatedAt.getTime(),
createdAt: volume.createdAt.getTime(),
lastHealthCheck: volume.lastHealthCheck.getTime(),
})),
} satisfies ListVolumesResponseDto;
};
return c.json(response, 200);
return c.json<ListVolumesDto>(response, 200);
})
.post("/", createVolumeDto, validator("json", createVolumeBody), async (c) => {
const body = c.req.valid("json");
const res = await volumeService.createVolume(body.name, body.config);
return c.json({ message: "Volume created", volume: res.volume }, 201);
const response = {
...res.volume,
path: getVolumePath(res.volume.name),
};
return c.json<CreateVolumeDto>(response, 201);
})
.post("/test-connection", testConnectionDto, validator("json", testConnectionBody), async (c) => {
const body = c.req.valid("json");
@@ -66,28 +69,21 @@ export const volumeController = new Hono()
volume: {
...res.volume,
path: getVolumePath(res.volume.name),
createdAt: res.volume.createdAt.getTime(),
updatedAt: res.volume.updatedAt.getTime(),
lastHealthCheck: res.volume.lastHealthCheck.getTime(),
},
statfs: {
total: res.statfs.total ?? 0,
used: res.statfs.used ?? 0,
free: res.statfs.free ?? 0,
},
} satisfies GetVolumeResponseDto;
};
return c.json(response, 200);
return c.json<GetVolumeDto>(response, 200);
})
.get("/:name/containers", getContainersDto, async (c) => {
const { name } = c.req.param();
const { containers } = await volumeService.getContainersUsingVolume(name);
const response = {
containers,
} satisfies ListContainersResponseDto;
return c.json(response, 200);
return c.json<ListContainersDto>(containers, 200);
})
.put("/:name", updateVolumeDto, validator("json", updateVolumeBody), async (c) => {
const { name } = c.req.param();
@@ -95,17 +91,11 @@ export const volumeController = new Hono()
const res = await volumeService.updateVolume(name, body);
const response = {
message: "Volume updated",
volume: {
...res.volume,
path: getVolumePath(res.volume.name),
createdAt: res.volume.createdAt.getTime(),
updatedAt: res.volume.updatedAt.getTime(),
lastHealthCheck: res.volume.lastHealthCheck.getTime(),
},
} satisfies UpdateVolumeResponseDto;
...res.volume,
path: getVolumePath(res.volume.name),
};
return c.json(response, 200);
return c.json<UpdateVolumeDto>(response, 200);
})
.post("/:name/mount", mountVolumeDto, async (c) => {
const { name } = c.req.param();
@@ -133,9 +123,9 @@ export const volumeController = new Hono()
const response = {
files: result.files,
path: result.path,
} satisfies ListFilesResponseDto;
};
c.header("Cache-Control", "public, max-age=10, stale-while-revalidate=60");
return c.json(response, 200);
return c.json<ListFilesDto>(response, 200);
});

View File

@@ -24,7 +24,7 @@ export type VolumeDto = typeof volumeSchema.infer;
export const listVolumesResponse = type({
volumes: volumeSchema.array(),
});
export type ListVolumesResponseDto = typeof listVolumesResponse.infer;
export type ListVolumesDto = typeof listVolumesResponse.infer;
export const listVolumesDto = describeRoute({
description: "List all volumes",
@@ -50,13 +50,8 @@ export const createVolumeBody = type({
config: volumeConfigSchema,
});
export const createVolumeResponse = type({
message: "string",
volume: type({
name: "string",
path: "string",
}),
});
export const createVolumeResponse = volumeSchema;
export type CreateVolumeDto = typeof createVolumeResponse.infer;
export const createVolumeDto = describeRoute({
description: "Create a new volume",
@@ -80,6 +75,7 @@ export const createVolumeDto = describeRoute({
export const deleteVolumeResponse = type({
message: "string",
});
export type DeleteVolumeDto = typeof deleteVolumeResponse.infer;
export const deleteVolumeDto = describeRoute({
description: "Delete a volume",
@@ -108,7 +104,7 @@ const getVolumeResponse = type({
statfs: statfsSchema,
});
export type GetVolumeResponseDto = typeof getVolumeResponse.infer;
export type GetVolumeDto = typeof getVolumeResponse.infer;
/**
* Get a volume
*/
@@ -141,10 +137,8 @@ export const updateVolumeBody = type({
export type UpdateVolumeBody = typeof updateVolumeBody.infer;
export const updateVolumeResponse = type({
message: "string",
volume: volumeSchema,
});
export const updateVolumeResponse = volumeSchema;
export type UpdateVolumeDto = typeof updateVolumeResponse.infer;
export const updateVolumeDto = describeRoute({
description: "Update a volume's configuration",
@@ -165,8 +159,6 @@ export const updateVolumeDto = describeRoute({
},
});
export type UpdateVolumeResponseDto = typeof updateVolumeResponse.infer;
/**
* Test connection
*/
@@ -178,6 +170,7 @@ export const testConnectionResponse = type({
success: "boolean",
message: "string",
});
export type TestConnectionDto = typeof testConnectionResponse.infer;
export const testConnectionDto = describeRoute({
description: "Test connection to backend",
@@ -202,6 +195,7 @@ export const mountVolumeResponse = type({
error: "string?",
status: type.valueOf(BACKEND_STATUS),
});
export type MountVolumeDto = typeof mountVolumeResponse.infer;
export const mountVolumeDto = describeRoute({
description: "Mount a volume",
@@ -216,9 +210,6 @@ export const mountVolumeDto = describeRoute({
},
},
},
404: {
description: "Volume not found",
},
},
});
@@ -229,6 +220,7 @@ export const unmountVolumeResponse = type({
error: "string?",
status: type.valueOf(BACKEND_STATUS),
});
export type UnmountVolumeDto = typeof unmountVolumeResponse.infer;
export const unmountVolumeDto = describeRoute({
description: "Unmount a volume",
@@ -243,9 +235,6 @@ export const unmountVolumeDto = describeRoute({
},
},
},
404: {
description: "Volume not found",
},
},
});
@@ -253,6 +242,7 @@ export const healthCheckResponse = type({
error: "string?",
status: type.valueOf(BACKEND_STATUS),
});
export type HealthCheckDto = typeof healthCheckResponse.infer;
export const healthCheckDto = describeRoute({
description: "Perform a health check on a volume",
@@ -283,10 +273,8 @@ const containerSchema = type({
image: "string",
});
export const listContainersResponse = type({
containers: containerSchema.array(),
});
export type ListContainersResponseDto = typeof listContainersResponse.infer;
export const listContainersResponse = containerSchema.array();
export type ListContainersDto = typeof listContainersResponse.infer;
export const getContainersDto = describeRoute({
description: "Get containers using a volume by name",
@@ -322,7 +310,7 @@ export const listFilesResponse = type({
files: fileEntrySchema.array(),
path: "string",
});
export type ListFilesResponseDto = typeof listFilesResponse.infer;
export type ListFilesDto = typeof listFilesResponse.infer;
export const listFilesDto = describeRoute({
description: "List files in a volume directory",
@@ -348,8 +336,5 @@ export const listFilesDto = describeRoute({
},
},
},
404: {
description: "Volume not found",
},
},
});

View File

@@ -6,7 +6,6 @@ import Docker from "dockerode";
import { eq } from "drizzle-orm";
import { ConflictError, InternalServerError, NotFoundError } from "http-errors-enhanced";
import slugify from "slugify";
import { VOLUME_MOUNT_BASE } from "../../core/constants";
import { db } from "../../db/db";
import { volumesTable } from "../../db/schema";
import { toMessage } from "../../utils/errors";
@@ -51,7 +50,7 @@ const createVolume = async (name: string, backendConfig: BackendConfig) => {
await db
.update(volumesTable)
.set({ status, lastError: error ?? null, lastHealthCheck: new Date() })
.set({ status, lastError: error ?? null, lastHealthCheck: Date.now() })
.where(eq(volumesTable.name, slug));
return { volume: created, status: 201 };
@@ -85,7 +84,7 @@ const mountVolume = async (name: string) => {
await db
.update(volumesTable)
.set({ status, lastError: error ?? null, lastHealthCheck: new Date() })
.set({ status, lastError: error ?? null, lastHealthCheck: Date.now() })
.where(eq(volumesTable.name, name));
return { error, status };
@@ -149,7 +148,7 @@ const updateVolume = async (name: string, volumeData: UpdateVolumeBody) => {
config: volumeData.config,
type: volumeData.config?.backend,
autoRemount: volumeData.autoRemount,
updatedAt: new Date(),
updatedAt: Date.now(),
})
.where(eq(volumesTable.name, name))
.returning();
@@ -163,7 +162,7 @@ const updateVolume = async (name: string, volumeData: UpdateVolumeBody) => {
const { error, status } = await backend.mount();
await db
.update(volumesTable)
.set({ status, lastError: error ?? null, lastHealthCheck: new Date() })
.set({ status, lastError: error ?? null, lastHealthCheck: Date.now() })
.where(eq(volumesTable.name, name));
}
@@ -178,9 +177,9 @@ const testConnection = async (backendConfig: BackendConfig) => {
name: "test-connection",
path: tempDir,
config: backendConfig,
createdAt: new Date(),
updatedAt: new Date(),
lastHealthCheck: new Date(),
createdAt: Date.now(),
updatedAt: Date.now(),
lastHealthCheck: Date.now(),
type: backendConfig.backend,
status: "unmounted" as const,
lastError: null,
@@ -215,7 +214,7 @@ const checkHealth = async (name: string) => {
await db
.update(volumesTable)
.set({ lastHealthCheck: new Date(), status, lastError: error ?? null })
.set({ lastHealthCheck: Date.now(), status, lastError: error ?? null })
.where(eq(volumesTable.name, volume.name));
return { status, error };