chore: re-order backend file structure

This commit is contained in:
Nicolas Meienberger
2025-08-31 17:32:11 +02:00
parent a16fc37b44
commit 23f47bbb6b
28 changed files with 240 additions and 1129 deletions

View File

@@ -12,7 +12,9 @@
"dotenv": "^17.2.1",
"drizzle-orm": "^0.44.4",
"hono": "^4.9.2",
"hono-openapi": "^0.4.8"
"hono-openapi": "^0.4.8",
"http-errors-enhanced": "^3.0.2",
"slugify": "^1.6.6"
},
"devDependencies": {
"@types/bun": "^1.2.20",

View File

@@ -1,31 +0,0 @@
import { type } from "arktype";
import { Hono } from "hono";
import {
listVolumesDescriptor,
listVolumesResponse,
} from "../descriptors/volume.descriptors";
export const volumeController = new Hono()
.get("/", listVolumesDescriptor, (c) => {
const res = listVolumesResponse({
volumes: [],
});
if (res instanceof type.errors) {
return c.json({ error: "Invalid response format" }, 500);
}
return c.json(res, 200);
})
.post("/", (c) => {
return c.json({ message: "Create a new volume" }, 201);
})
.get("/:name", (c) => {
return c.json({ message: `Details of volume ${c.req.param("name")}` });
})
.put("/:name", (c) => {
return c.json({ message: `Update volume ${c.req.param("name")}` });
})
.delete("/:name", (c) => {
return c.json({ message: `Delete volume ${c.req.param("name")}` });
});

View File

@@ -5,11 +5,13 @@ const envSchema = type({
NODE_ENV: type
.enumerated("development", "production", "test")
.default("development"),
DB_FILE_NAME: "string",
VOLUME_ROOT: "string",
}).pipe((s) => ({
__prod__: s.NODE_ENV === "production",
environment: s.NODE_ENV,
dbFileName: s.DB_FILE_NAME,
dbFileName: "/data/ironmount.db",
volumeRootHost: s.VOLUME_ROOT,
volumeRootContainer: "/mnt/volumes",
}));
const parseConfig = (env: unknown) => {

View File

@@ -2,12 +2,19 @@ import { type } from "arktype";
import { sql } from "drizzle-orm";
import { int, sqliteTable, text } from "drizzle-orm/sqlite-core";
const BACKEND_TYPES = {
nfs: "nfs",
smb: "smb",
directory: "directory",
};
export type BackendType = keyof typeof BACKEND_TYPES;
const nfsConfigSchema = type({
backend: "'nfs'",
server: "string",
exportPath: "string",
port: "number",
version: type.enumerated(["3", "4"]),
version: "string", // Shold be an enum: "3" | "4" | "4.1"
});
const smbConfigSchema = type({
@@ -18,10 +25,12 @@ const directoryConfigSchema = type({
backend: "'directory'",
});
const configSchema = nfsConfigSchema
export const volumeConfigSchema = nfsConfigSchema
.or(smbConfigSchema)
.or(directoryConfigSchema);
export type BackendConfig = typeof volumeConfigSchema.infer;
export const volumesTable = sqliteTable("volumes_table", {
id: int().primaryKey({ autoIncrement: true }),
name: text().notNull().unique(),
@@ -30,6 +39,6 @@ export const volumesTable = sqliteTable("volumes_table", {
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 configSchema.inferOut>()
.$type<typeof volumeConfigSchema.inferOut>()
.notNull(),
});

View File

@@ -1,23 +0,0 @@
import { Scalar } from "@scalar/hono-api-reference";
import type { Hono } from "hono";
import { openAPISpecs } from "hono-openapi";
export const generalDescriptor = (app: Hono) =>
openAPISpecs(app, {
documentation: {
info: {
title: "Ironmount API",
version: "1.0.0",
description: "API for managing Docker volumes",
},
servers: [
{ url: "http://localhost:3000", description: "Development Server" },
],
},
});
export const scalarDescriptor = Scalar({
title: "Ironmount API Docs",
pageTitle: "Ironmount API Docs",
url: "/api/v1/openapi.json",
});

View File

@@ -1,26 +0,0 @@
import { type } from "arktype";
import { describeRoute } from "hono-openapi";
import { resolver } from "hono-openapi/arktype";
export const listVolumesResponse = type({
volumes: type({
name: "string",
mountpoint: "string",
createdAt: "string",
}).array(),
});
export const listVolumesDescriptor = describeRoute({
description: "List all volumes",
tags: ["Volumes"],
responses: {
200: {
description: "A list of volumes",
content: {
"application/json": {
schema: resolver(listVolumesResponse),
},
},
},
},
});

View File

@@ -1,13 +1,31 @@
import * as fs from "node:fs/promises";
import { Scalar } from "@scalar/hono-api-reference";
import { Hono } from "hono";
import { logger } from "hono/logger";
import { driverController } from "./controllers/driver.controller";
import { volumeController } from "./controllers/volume.controller";
import { openAPISpecs } from "hono-openapi";
import { runDbMigrations } from "./db/db";
import {
generalDescriptor,
scalarDescriptor,
} from "./descriptors/general.descriptors";
import { driverController } from "./modules/driver/driver.controller";
import { volumeController } from "./modules/volumes/volume.controller";
export const generalDescriptor = (app: Hono) =>
openAPISpecs(app, {
documentation: {
info: {
title: "Ironmount API",
version: "1.0.0",
description: "API for managing Docker volumes",
},
servers: [
{ url: "http://localhost:3000", description: "Development Server" },
],
},
});
export const scalarDescriptor = Scalar({
title: "Ironmount API Docs",
pageTitle: "Ironmount API Docs",
url: "/api/v1/openapi.json",
});
const driver = new Hono().use(logger()).route("/", driverController);
const app = new Hono()

View File

@@ -0,0 +1,50 @@
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 { volumeService } from "./volume.service";
export const volumeController = new Hono()
.get("/", listVolumesDto, async (c) => {
const volumes = await volumeService.listVolumes();
const response = {
volumes: volumes.map((volume) => ({
name: volume.name,
mountpoint: volume.path,
createdAt: volume.createdAt,
})),
} satisfies ListVolumesResponseDto;
return c.json(response, 200);
})
.post(
"/",
createVolumeDto,
validator("json", createVolumeBody),
async (c) => {
const body = c.req.valid("json");
const res = await volumeService.createVolume(body.name, body.config);
if (res.error) {
const { message, status } = handleServiceError(res.error);
return c.json(message, status);
}
return c.json({ message: "Volume created", volume: res.volume });
},
)
.get("/:name", (c) => {
return c.json({ message: `Details of volume ${c.req.param("name")}` });
})
.put("/:name", (c) => {
return c.json({ message: `Update volume ${c.req.param("name")}` });
})
.delete("/:name", (c) => {
return c.json({ message: `Delete volume ${c.req.param("name")}` });
});

View File

@@ -0,0 +1,60 @@
import { type } from "arktype";
import { describeRoute } from "hono-openapi";
import { resolver } from "hono-openapi/arktype";
import { volumeConfigSchema } from "../../db/schema";
/**
* List all volumes
*/
export const listVolumesResponse = type({
volumes: type({
name: "string",
mountpoint: "string",
createdAt: "number",
}).array(),
});
export type ListVolumesResponseDto = typeof listVolumesResponse.infer;
export const listVolumesDto = describeRoute({
description: "List all volumes",
tags: ["Volumes"],
responses: {
200: {
description: "A list of volumes",
content: {
"application/json": {
schema: resolver(listVolumesResponse),
},
},
},
},
});
/**
* Create a new volume
*/
export const createVolumeBody = type({
name: "string",
config: volumeConfigSchema,
});
export const createVolumeResponse = type({
name: "string",
mountpoint: "string",
createdAt: "number",
});
export const createVolumeDto = describeRoute({
description: "Create a new volume",
tags: ["Volumes"],
responses: {
201: {
description: "Volume created successfully",
content: {
"application/json": {
schema: resolver(createVolumeResponse),
},
},
},
},
});

View File

@@ -0,0 +1,44 @@
import * as path from "node:path";
import { eq } from "drizzle-orm";
import { ConflictError } from "http-errors-enhanced";
import slugify from "slugify";
import { config } from "../../core/config";
import { db } from "../../db/db";
import { type BackendConfig, volumesTable } from "../../db/schema";
const listVolumes = async () => {
const volumes = await db.query.volumesTable.findMany({});
return volumes;
};
const createVolume = async (name: string, backendConfig: BackendConfig) => {
const slug = slugify(name, { lower: true, strict: true });
const existing = await db.query.volumesTable.findFirst({
where: eq(volumesTable.name, slug),
});
if (existing) {
return { error: new ConflictError("Volume already exists") };
}
const volumePathHost = path.join(config.volumeRootHost);
const val = await db
.insert(volumesTable)
.values({
name: slug,
config: backendConfig,
path: path.join(volumePathHost, slug),
type: "nfs",
})
.returning();
return { volume: val[0], status: 201 };
};
export const volumeService = {
listVolumes,
createVolume,
};

View File

@@ -0,0 +1,8 @@
import { ConflictError } from "http-errors-enhanced";
export const handleServiceError = (error: unknown) => {
if (error instanceof ConflictError) {
return { message: error.message, status: 409 as const };
}
return { message: "Internal Server Error", status: 500 as const };
};