diff --git a/drizzle.config.ts b/drizzle.config.ts new file mode 100644 index 0000000..6f17dab --- /dev/null +++ b/drizzle.config.ts @@ -0,0 +1,7 @@ +import { defineConfig } from "drizzle-kit"; + +export default defineConfig({ + dialect: "sqlite", + schema: "./server/db/schema.ts", + out: "./server/db/migrations", +}); diff --git a/server/db/client.ts b/server/db/client.ts new file mode 100644 index 0000000..8031ead --- /dev/null +++ b/server/db/client.ts @@ -0,0 +1,15 @@ +// server/db/client.ts +import Database from "better-sqlite3"; +import { drizzle } from "drizzle-orm/better-sqlite3"; +import { mkdirSync } from "node:fs"; +import { dirname } from "node:path"; +import { env } from "../env.js"; +import * as schema from "./schema.js"; + +mkdirSync(dirname(env.dbPath), { recursive: true }); +const sqlite = new Database(env.dbPath); +sqlite.pragma("journal_mode = WAL"); +sqlite.pragma("foreign_keys = ON"); + +export const db = drizzle(sqlite, { schema }); +export { schema }; diff --git a/server/db/migrate.ts b/server/db/migrate.ts new file mode 100644 index 0000000..8834993 --- /dev/null +++ b/server/db/migrate.ts @@ -0,0 +1,7 @@ +// server/db/migrate.ts +import { migrate } from "drizzle-orm/better-sqlite3/migrator"; +import { db } from "./client.js"; + +export function runMigrations(): void { + migrate(db, { migrationsFolder: "./server/db/migrations" }); +} diff --git a/server/db/migrations/0000_brainy_dakota_north.sql b/server/db/migrations/0000_brainy_dakota_north.sql new file mode 100644 index 0000000..a7f1f58 --- /dev/null +++ b/server/db/migrations/0000_brainy_dakota_north.sql @@ -0,0 +1,38 @@ +CREATE TABLE `executions` ( + `id` text PRIMARY KEY NOT NULL, + `machine_id` text NOT NULL, + `action` text NOT NULL, + `mode` text DEFAULT 'manual' NOT NULL, + `started_at` text NOT NULL, + `finished_at` text, + `status` text NOT NULL, + `result_json` text, + `report_path` text, + `raw_log_path` text, + FOREIGN KEY (`machine_id`) REFERENCES `machines`(`id`) ON UPDATE no action ON DELETE cascade +); +--> statement-breakpoint +CREATE TABLE `machines` ( + `id` text PRIMARY KEY NOT NULL, + `name` text NOT NULL, + `hostname` text NOT NULL, + `port` integer DEFAULT 22 NOT NULL, + `os_family` text DEFAULT 'unknown' NOT NULL, + `username` text NOT NULL, + `enc_password` text NOT NULL, + `enc_sudo_password` text, + `apt_proxy_mode` text DEFAULT 'direct' NOT NULL, + `apt_proxy_url` text, + `status` text DEFAULT 'unknown' NOT NULL, + `last_checked_at` text, + `created_at` text NOT NULL +); +--> statement-breakpoint +CREATE TABLE `snapshots` ( + `id` text PRIMARY KEY NOT NULL, + `machine_id` text NOT NULL, + `checked_at` text NOT NULL, + `status` text NOT NULL, + `payload_json` text NOT NULL, + FOREIGN KEY (`machine_id`) REFERENCES `machines`(`id`) ON UPDATE no action ON DELETE cascade +); diff --git a/server/db/migrations/meta/0000_snapshot.json b/server/db/migrations/meta/0000_snapshot.json new file mode 100644 index 0000000..960b7c9 --- /dev/null +++ b/server/db/migrations/meta/0000_snapshot.json @@ -0,0 +1,277 @@ +{ + "version": "6", + "dialect": "sqlite", + "id": "6aec3f17-e17f-4e7c-950c-c11592a58541", + "prevId": "00000000-0000-0000-0000-000000000000", + "tables": { + "executions": { + "name": "executions", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "machine_id": { + "name": "machine_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "mode": { + "name": "mode", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'manual'" + }, + "started_at": { + "name": "started_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "finished_at": { + "name": "finished_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "result_json": { + "name": "result_json", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "report_path": { + "name": "report_path", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "raw_log_path": { + "name": "raw_log_path", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "executions_machine_id_machines_id_fk": { + "name": "executions_machine_id_machines_id_fk", + "tableFrom": "executions", + "tableTo": "machines", + "columnsFrom": [ + "machine_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "machines": { + "name": "machines", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "hostname": { + "name": "hostname", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "port": { + "name": "port", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 22 + }, + "os_family": { + "name": "os_family", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'unknown'" + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "enc_password": { + "name": "enc_password", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "enc_sudo_password": { + "name": "enc_sudo_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "apt_proxy_mode": { + "name": "apt_proxy_mode", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'direct'" + }, + "apt_proxy_url": { + "name": "apt_proxy_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'unknown'" + }, + "last_checked_at": { + "name": "last_checked_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "snapshots": { + "name": "snapshots", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "machine_id": { + "name": "machine_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "checked_at": { + "name": "checked_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "payload_json": { + "name": "payload_json", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "snapshots_machine_id_machines_id_fk": { + "name": "snapshots_machine_id_machines_id_fk", + "tableFrom": "snapshots", + "tableTo": "machines", + "columnsFrom": [ + "machine_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + } + }, + "views": {}, + "enums": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "indexes": {} + } +} \ No newline at end of file diff --git a/server/db/migrations/meta/_journal.json b/server/db/migrations/meta/_journal.json new file mode 100644 index 0000000..b16f80c --- /dev/null +++ b/server/db/migrations/meta/_journal.json @@ -0,0 +1,13 @@ +{ + "version": "7", + "dialect": "sqlite", + "entries": [ + { + "idx": 0, + "version": "6", + "when": 1780599514478, + "tag": "0000_brainy_dakota_north", + "breakpoints": true + } + ] +} \ No newline at end of file diff --git a/server/db/schema.ts b/server/db/schema.ts new file mode 100644 index 0000000..a5a432a --- /dev/null +++ b/server/db/schema.ts @@ -0,0 +1,39 @@ +// server/db/schema.ts +import { sqliteTable, text, integer } from "drizzle-orm/sqlite-core"; + +export const machines = sqliteTable("machines", { + id: text("id").primaryKey(), + name: text("name").notNull(), + hostname: text("hostname").notNull(), + port: integer("port").notNull().default(22), + osFamily: text("os_family").notNull().default("unknown"), + username: text("username").notNull(), + encPassword: text("enc_password").notNull(), + encSudoPassword: text("enc_sudo_password"), + aptProxyMode: text("apt_proxy_mode").notNull().default("direct"), + aptProxyUrl: text("apt_proxy_url"), + status: text("status").notNull().default("unknown"), + lastCheckedAt: text("last_checked_at"), + createdAt: text("created_at").notNull(), +}); + +export const snapshots = sqliteTable("snapshots", { + id: text("id").primaryKey(), + machineId: text("machine_id").notNull().references(() => machines.id, { onDelete: "cascade" }), + checkedAt: text("checked_at").notNull(), + status: text("status").notNull(), + payloadJson: text("payload_json").notNull(), +}); + +export const executions = sqliteTable("executions", { + id: text("id").primaryKey(), + machineId: text("machine_id").notNull().references(() => machines.id, { onDelete: "cascade" }), + action: text("action").notNull(), + mode: text("mode").notNull().default("manual"), + startedAt: text("started_at").notNull(), + finishedAt: text("finished_at"), + status: text("status").notNull(), + resultJson: text("result_json"), + reportPath: text("report_path"), + rawLogPath: text("raw_log_path"), +});