From 44938e8c664690a084c716a8dcab5f4880874336 Mon Sep 17 00:00:00 2001 From: Derock Date: Tue, 14 May 2024 22:01:22 -0400 Subject: [PATCH] wip: generation-style migration --- drizzle.config.ts | 1 + drizzle/0000_tidy_vermin.sql | 163 +++ drizzle/meta/0000_snapshot.json | 1152 +++++++++++++++++ drizzle/meta/_journal.json | 13 + package.json | 1 + src/server/api/middleware/project.ts | 2 +- src/server/api/routers/projects/index.ts | 37 +- src/server/api/routers/projects/project.ts | 20 +- .../api/routers/projects/service/index.ts | 106 +- .../api/routers/projects/service/update.ts | 3 +- src/server/db/schema/schema.ts | 4 + src/server/managers/Project.ts | 13 +- src/server/server.ts | 12 +- 13 files changed, 1428 insertions(+), 99 deletions(-) create mode 100644 drizzle/0000_tidy_vermin.sql create mode 100644 drizzle/meta/0000_snapshot.json create mode 100644 drizzle/meta/_journal.json diff --git a/drizzle.config.ts b/drizzle.config.ts index 511b765..4c519db 100644 --- a/drizzle.config.ts +++ b/drizzle.config.ts @@ -1,6 +1,7 @@ import type { Config } from "drizzle-kit"; export default { + out: "./drizzle", schema: "./src/server/db/schema/index.ts", driver: "better-sqlite", dbCredentials: { diff --git a/drizzle/0000_tidy_vermin.sql b/drizzle/0000_tidy_vermin.sql new file mode 100644 index 0000000..5fa0391 --- /dev/null +++ b/drizzle/0000_tidy_vermin.sql @@ -0,0 +1,163 @@ +CREATE TABLE `project_deployment` ( + `id` text PRIMARY KEY DEFAULT (uuid_generate_v7()) NOT NULL, + `project_id` text NOT NULL, + `deployed_at` integer DEFAULT CURRENT_TIMESTAMP NOT NULL, + `status` integer NOT NULL, + FOREIGN KEY (`project_id`) REFERENCES `projects`(`id`) ON UPDATE no action ON DELETE cascade +); +--> statement-breakpoint +CREATE TABLE `projects` ( + `id` text PRIMARY KEY DEFAULT (uuid_generate_v7()) NOT NULL, + `friendly_name` text NOT NULL, + `internal_name` text NOT NULL, + `created_at` integer DEFAULT CURRENT_TIMESTAMP NOT NULL, + `owner_id` text NOT NULL, + FOREIGN KEY (`owner_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE cascade +); +--> statement-breakpoint +CREATE TABLE `service` ( + `id` text PRIMARY KEY DEFAULT (uuid_generate_v7()) NOT NULL, + `name` text NOT NULL, + `project_id` text NOT NULL, + `latest_generation_id` text NOT NULL DEFERRABLE INITIALLY DEFERRED, + `redeploy_secret` text NOT NULL, + `deployed_generation_id` text, + `created_at` integer DEFAULT CURRENT_TIMESTAMP NOT NULL, + FOREIGN KEY (`project_id`) REFERENCES `projects`(`id`) ON UPDATE no action ON DELETE cascade, + FOREIGN KEY (`latest_generation_id`) REFERENCES `service_generation`(`id`) ON UPDATE no action ON DELETE no action, + FOREIGN KEY (`deployed_generation_id`) REFERENCES `service_generation`(`id`) ON UPDATE no action ON DELETE no action +); +--> statement-breakpoint +CREATE TABLE `service_deployment` ( + `id` text PRIMARY KEY DEFAULT (uuid_generate_v7()) NOT NULL, + `project_deployment_id` text NOT NULL, + `service_id` text NOT NULL, + `created_at` integer DEFAULT CURRENT_TIMESTAMP NOT NULL, + `deployed_by` text, + `build_logs` blob, + `status` integer NOT NULL, + FOREIGN KEY (`project_deployment_id`) REFERENCES `project_deployment`(`id`) ON UPDATE no action ON DELETE cascade, + FOREIGN KEY (`service_id`) REFERENCES `service_generation`(`id`) ON UPDATE no action ON DELETE cascade, + FOREIGN KEY (`deployed_by`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE no action +); +--> statement-breakpoint +CREATE TABLE `service_domain` ( + `id` text PRIMARY KEY DEFAULT (uuid_generate_v7()) NOT NULL, + `service_id` text NOT NULL, + `domain` text NOT NULL, + `internal_port` integer NOT NULL, + `https` integer DEFAULT false NOT NULL, + `force_ssl` integer DEFAULT false NOT NULL, + FOREIGN KEY (`service_id`) REFERENCES `service_generation`(`id`) ON UPDATE no action ON DELETE no action +); +--> statement-breakpoint +CREATE TABLE `service_generation` ( + `id` text PRIMARY KEY DEFAULT (uuid_generate_v7()) NOT NULL, + `service_id` text NOT NULL, + `deployment_id` text, + `source` integer NOT NULL, + `environment` text, + `docker_image` text, + `docker_registry_username` text, + `docker_registry_password` text, + `github_username` text, + `github_repository` text, + `github_branch` text, + `git_url` text, + `git_branch` text, + `build_method` integer DEFAULT 2 NOT NULL, + `build_path` text DEFAULT '/' NOT NULL, + `command` text, + `entrypoint` text, + `replicas` integer DEFAULT 1 NOT NULL, + `max_replicas_per_node` integer, + `deploy_mode` integer DEFAULT 1 NOT NULL, + `zero_downtime` integer DEFAULT false NOT NULL, + `max_cpu` real DEFAULT 0 NOT NULL, + `max_memory` text DEFAULT '0' NOT NULL, + `max_pids` integer DEFAULT false NOT NULL, + `restart` integer DEFAULT 2 NOT NULL, + `restart_delay` text DEFAULT '5s', + `restart_max_attempts` integer, + `healthcheck_enabled` integer DEFAULT false NOT NULL, + `healthcheck_command` text, + `healthcheck_interval` text DEFAULT '30s' NOT NULL, + `healthcheck_timeout` text DEFAULT '30s' NOT NULL, + `healthcheck_retries` integer DEFAULT 3 NOT NULL, + `healthcheck_start_period` text DEFAULT '0s' NOT NULL, + `logging_max_size` text DEFAULT '-1' NOT NULL, + `logging_max_files` integer DEFAULT 1 NOT NULL, + `created_at` integer DEFAULT CURRENT_TIMESTAMP NOT NULL, + FOREIGN KEY (`service_id`) REFERENCES `service`(`id`) ON UPDATE no action ON DELETE cascade, + FOREIGN KEY (`deployment_id`) REFERENCES `service_deployment`(`id`) ON UPDATE no action ON DELETE no action +); +--> statement-breakpoint +CREATE TABLE `service_port` ( + `id` text PRIMARY KEY DEFAULT (uuid_generate_v7()) NOT NULL, + `service_id` text NOT NULL, + `internal_port` integer NOT NULL, + `external_port` integer NOT NULL, + `port_type` integer NOT NULL, + `type` integer NOT NULL, + FOREIGN KEY (`service_id`) REFERENCES `service_generation`(`id`) ON UPDATE no action ON DELETE no action +); +--> statement-breakpoint +CREATE TABLE `service_sysctl` ( + `id` text PRIMARY KEY DEFAULT (uuid_generate_v7()) NOT NULL, + `service_id` text NOT NULL, + `key` text NOT NULL, + `value` text NOT NULL, + FOREIGN KEY (`service_id`) REFERENCES `service_generation`(`id`) ON UPDATE no action ON DELETE no action +); +--> statement-breakpoint +CREATE TABLE `service_ulimit` ( + `id` text PRIMARY KEY DEFAULT (uuid_generate_v7()) NOT NULL, + `service_id` text NOT NULL, + `name` text NOT NULL, + `soft` integer NOT NULL, + `hard` integer NOT NULL, + FOREIGN KEY (`service_id`) REFERENCES `service_generation`(`id`) ON UPDATE no action ON DELETE no action +); +--> statement-breakpoint +CREATE TABLE `service_volume` ( + `id` text PRIMARY KEY DEFAULT (uuid_generate_v7()) NOT NULL, + `service_id` text NOT NULL, + `source` text, + `target` text NOT NULL, + `type` text NOT NULL, + FOREIGN KEY (`service_id`) REFERENCES `service_generation`(`id`) ON UPDATE no action ON DELETE no action +); +--> statement-breakpoint +CREATE TABLE `session` ( + `token` text PRIMARY KEY NOT NULL, + `last_useragent` text, + `last_ip` text, + `last_accessed` integer, + `created_at` integer DEFAULT CURRENT_TIMESTAMP NOT NULL, + `id` text NOT NULL, + FOREIGN KEY (`id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE no action +); +--> statement-breakpoint +CREATE TABLE `system_stats` ( + `id` integer PRIMARY KEY DEFAULT CURRENT_TIMESTAMP NOT NULL, + `cpu_usage` integer, + `memory_usage` integer NOT NULL, + `disk_usage` integer NOT NULL, + `network_tx` integer NOT NULL, + `network_rx` integer NOT NULL +); +--> statement-breakpoint +CREATE TABLE `users` ( + `id` text PRIMARY KEY DEFAULT (uuid_generate_v7()) NOT NULL, + `username` text NOT NULL, + `password` text, + `mfa_token` blob +); +--> statement-breakpoint +CREATE INDEX `proj_deployment_idx` ON `project_deployment` (`id`,`project_id`);--> statement-breakpoint +CREATE UNIQUE INDEX `projects_internal_name_unique` ON `projects` (`internal_name`);--> statement-breakpoint +CREATE INDEX `name_project_idx` ON `service` (`name`,`project_id`);--> statement-breakpoint +CREATE UNIQUE INDEX `name_project_unq` ON `service` (`name`,`project_id`);--> statement-breakpoint +CREATE INDEX `proj_generation_idx` ON `service_generation` (`id`,`service_id`);--> statement-breakpoint +CREATE UNIQUE INDEX `users_username_unique` ON `users` (`username`);--> statement-breakpoint +CREATE INDEX `username_idx` ON `users` (`username`); diff --git a/drizzle/meta/0000_snapshot.json b/drizzle/meta/0000_snapshot.json new file mode 100644 index 0000000..7109a44 --- /dev/null +++ b/drizzle/meta/0000_snapshot.json @@ -0,0 +1,1152 @@ +{ + "version": "5", + "dialect": "sqlite", + "id": "f5624a74-60b3-4d72-90e0-67670f26b0cd", + "prevId": "00000000-0000-0000-0000-000000000000", + "tables": { + "project_deployment": { + "name": "project_deployment", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false, + "default": "(uuid_generate_v7())" + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "deployed_at": { + "name": "deployed_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "status": { + "name": "status", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "proj_deployment_idx": { + "name": "proj_deployment_idx", + "columns": [ + "id", + "project_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "project_deployment_project_id_projects_id_fk": { + "name": "project_deployment_project_id_projects_id_fk", + "tableFrom": "project_deployment", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "projects": { + "name": "projects", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false, + "default": "(uuid_generate_v7())" + }, + "friendly_name": { + "name": "friendly_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "internal_name": { + "name": "internal_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "owner_id": { + "name": "owner_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "projects_internal_name_unique": { + "name": "projects_internal_name_unique", + "columns": [ + "internal_name" + ], + "isUnique": true + } + }, + "foreignKeys": { + "projects_owner_id_users_id_fk": { + "name": "projects_owner_id_users_id_fk", + "tableFrom": "projects", + "tableTo": "users", + "columnsFrom": [ + "owner_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "service": { + "name": "service", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false, + "default": "(uuid_generate_v7())" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "latest_generation_id": { + "name": "latest_generation_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "redeploy_secret": { + "name": "redeploy_secret", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "deployed_generation_id": { + "name": "deployed_generation_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "name_project_idx": { + "name": "name_project_idx", + "columns": [ + "name", + "project_id" + ], + "isUnique": false + }, + "name_project_unq": { + "name": "name_project_unq", + "columns": [ + "name", + "project_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "service_project_id_projects_id_fk": { + "name": "service_project_id_projects_id_fk", + "tableFrom": "service", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "service_latest_generation_id_service_generation_id_fk": { + "name": "service_latest_generation_id_service_generation_id_fk", + "tableFrom": "service", + "tableTo": "service_generation", + "columnsFrom": [ + "latest_generation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "service_deployed_generation_id_service_generation_id_fk": { + "name": "service_deployed_generation_id_service_generation_id_fk", + "tableFrom": "service", + "tableTo": "service_generation", + "columnsFrom": [ + "deployed_generation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "service_deployment": { + "name": "service_deployment", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false, + "default": "(uuid_generate_v7())" + }, + "project_deployment_id": { + "name": "project_deployment_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "service_id": { + "name": "service_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "deployed_by": { + "name": "deployed_by", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "build_logs": { + "name": "build_logs", + "type": "blob", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "service_deployment_project_deployment_id_project_deployment_id_fk": { + "name": "service_deployment_project_deployment_id_project_deployment_id_fk", + "tableFrom": "service_deployment", + "tableTo": "project_deployment", + "columnsFrom": [ + "project_deployment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "service_deployment_service_id_service_generation_id_fk": { + "name": "service_deployment_service_id_service_generation_id_fk", + "tableFrom": "service_deployment", + "tableTo": "service_generation", + "columnsFrom": [ + "service_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "service_deployment_deployed_by_users_id_fk": { + "name": "service_deployment_deployed_by_users_id_fk", + "tableFrom": "service_deployment", + "tableTo": "users", + "columnsFrom": [ + "deployed_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "service_domain": { + "name": "service_domain", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false, + "default": "(uuid_generate_v7())" + }, + "service_id": { + "name": "service_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "domain": { + "name": "domain", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "internal_port": { + "name": "internal_port", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "https": { + "name": "https", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "force_ssl": { + "name": "force_ssl", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + } + }, + "indexes": {}, + "foreignKeys": { + "service_domain_service_id_service_generation_id_fk": { + "name": "service_domain_service_id_service_generation_id_fk", + "tableFrom": "service_domain", + "tableTo": "service_generation", + "columnsFrom": [ + "service_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "service_generation": { + "name": "service_generation", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false, + "default": "(uuid_generate_v7())" + }, + "service_id": { + "name": "service_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "deployment_id": { + "name": "deployment_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "source": { + "name": "source", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "environment": { + "name": "environment", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "docker_image": { + "name": "docker_image", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "docker_registry_username": { + "name": "docker_registry_username", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "docker_registry_password": { + "name": "docker_registry_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "github_username": { + "name": "github_username", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "github_repository": { + "name": "github_repository", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "github_branch": { + "name": "github_branch", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "git_url": { + "name": "git_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "git_branch": { + "name": "git_branch", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "build_method": { + "name": "build_method", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 2 + }, + "build_path": { + "name": "build_path", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'/'" + }, + "command": { + "name": "command", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "entrypoint": { + "name": "entrypoint", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "replicas": { + "name": "replicas", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 1 + }, + "max_replicas_per_node": { + "name": "max_replicas_per_node", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "deploy_mode": { + "name": "deploy_mode", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 1 + }, + "zero_downtime": { + "name": "zero_downtime", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "max_cpu": { + "name": "max_cpu", + "type": "real", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "max_memory": { + "name": "max_memory", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'0'" + }, + "max_pids": { + "name": "max_pids", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "restart": { + "name": "restart", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 2 + }, + "restart_delay": { + "name": "restart_delay", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'5s'" + }, + "restart_max_attempts": { + "name": "restart_max_attempts", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "healthcheck_enabled": { + "name": "healthcheck_enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "healthcheck_command": { + "name": "healthcheck_command", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "healthcheck_interval": { + "name": "healthcheck_interval", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'30s'" + }, + "healthcheck_timeout": { + "name": "healthcheck_timeout", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'30s'" + }, + "healthcheck_retries": { + "name": "healthcheck_retries", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 3 + }, + "healthcheck_start_period": { + "name": "healthcheck_start_period", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'0s'" + }, + "logging_max_size": { + "name": "logging_max_size", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'-1'" + }, + "logging_max_files": { + "name": "logging_max_files", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 1 + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "proj_generation_idx": { + "name": "proj_generation_idx", + "columns": [ + "id", + "service_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "service_generation_service_id_service_id_fk": { + "name": "service_generation_service_id_service_id_fk", + "tableFrom": "service_generation", + "tableTo": "service", + "columnsFrom": [ + "service_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "service_generation_deployment_id_service_deployment_id_fk": { + "name": "service_generation_deployment_id_service_deployment_id_fk", + "tableFrom": "service_generation", + "tableTo": "service_deployment", + "columnsFrom": [ + "deployment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "service_port": { + "name": "service_port", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false, + "default": "(uuid_generate_v7())" + }, + "service_id": { + "name": "service_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "internal_port": { + "name": "internal_port", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "external_port": { + "name": "external_port", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "port_type": { + "name": "port_type", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "service_port_service_id_service_generation_id_fk": { + "name": "service_port_service_id_service_generation_id_fk", + "tableFrom": "service_port", + "tableTo": "service_generation", + "columnsFrom": [ + "service_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "service_sysctl": { + "name": "service_sysctl", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false, + "default": "(uuid_generate_v7())" + }, + "service_id": { + "name": "service_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "service_sysctl_service_id_service_generation_id_fk": { + "name": "service_sysctl_service_id_service_generation_id_fk", + "tableFrom": "service_sysctl", + "tableTo": "service_generation", + "columnsFrom": [ + "service_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "service_ulimit": { + "name": "service_ulimit", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false, + "default": "(uuid_generate_v7())" + }, + "service_id": { + "name": "service_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "soft": { + "name": "soft", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "hard": { + "name": "hard", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "service_ulimit_service_id_service_generation_id_fk": { + "name": "service_ulimit_service_id_service_generation_id_fk", + "tableFrom": "service_ulimit", + "tableTo": "service_generation", + "columnsFrom": [ + "service_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "service_volume": { + "name": "service_volume", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false, + "default": "(uuid_generate_v7())" + }, + "service_id": { + "name": "service_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "target": { + "name": "target", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "service_volume_service_id_service_generation_id_fk": { + "name": "service_volume_service_id_service_generation_id_fk", + "tableFrom": "service_volume", + "tableTo": "service_generation", + "columnsFrom": [ + "service_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "session": { + "name": "session", + "columns": { + "token": { + "name": "token", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "last_useragent": { + "name": "last_useragent", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_ip": { + "name": "last_ip", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_accessed": { + "name": "last_accessed", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "id": { + "name": "id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "session_id_users_id_fk": { + "name": "session_id_users_id_fk", + "tableFrom": "session", + "tableTo": "users", + "columnsFrom": [ + "id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "system_stats": { + "name": "system_stats", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "cpu_usage": { + "name": "cpu_usage", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "memory_usage": { + "name": "memory_usage", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "disk_usage": { + "name": "disk_usage", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "network_tx": { + "name": "network_tx", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "network_rx": { + "name": "network_rx", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "users": { + "name": "users", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false, + "default": "(uuid_generate_v7())" + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "mfa_token": { + "name": "mfa_token", + "type": "blob", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "users_username_unique": { + "name": "users_username_unique", + "columns": [ + "username" + ], + "isUnique": true + }, + "username_idx": { + "name": "username_idx", + "columns": [ + "username" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + } + }, + "enums": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + } +} \ No newline at end of file diff --git a/drizzle/meta/_journal.json b/drizzle/meta/_journal.json new file mode 100644 index 0000000..132562d --- /dev/null +++ b/drizzle/meta/_journal.json @@ -0,0 +1,13 @@ +{ + "version": "5", + "dialect": "sqlite", + "entries": [ + { + "idx": 0, + "version": "5", + "when": 1715730366620, + "tag": "0000_tidy_vermin", + "breakpoints": true + } + ] +} \ No newline at end of file diff --git a/package.json b/package.json index 6502062..3cbce3e 100644 --- a/package.json +++ b/package.json @@ -9,6 +9,7 @@ "build:server": "tsup", "clean": "rm -rf .next dist", "db:push": "drizzle-kit push:sqlite", + "db:generate": "drizzle-kit generate:sqlite", "dev": "tsup --watch --onSuccess \"npm run dev:run\" --clean", "dev:run": "node --enable-source-maps dist/server.js", "lint": "next lint", diff --git a/src/server/api/middleware/project.ts b/src/server/api/middleware/project.ts index 00828b8..2cbcaeb 100644 --- a/src/server/api/middleware/project.ts +++ b/src/server/api/middleware/project.ts @@ -12,7 +12,7 @@ export type BasicProjectDetails = { export const projectMiddleware = experimental_standaloneMiddleware<{ ctx: { db: typeof db }; input: { projectId: string }; -}>().create(async ({ ctx, input, next }) => { +}>().create(async ({ input, next }) => { if (typeof input.projectId != "string") { throw new TRPCError({ code: "INTERNAL_SERVER_ERROR", diff --git a/src/server/api/routers/projects/index.ts b/src/server/api/routers/projects/index.ts index 348a64b..11548e2 100644 --- a/src/server/api/routers/projects/index.ts +++ b/src/server/api/routers/projects/index.ts @@ -1,7 +1,7 @@ import assert from "assert"; -import { eq } from "drizzle-orm"; import { z } from "zod"; -import { projects, serviceGeneration } from "~/server/db/schema"; +import { projects } from "~/server/db/schema"; +import ProjectManager from "~/server/managers/Project"; import { authenticatedProcedure, createTRPCRouter } from "../../trpc"; import { deployProject } from "./deploy"; import { getProject } from "./project"; @@ -21,28 +21,21 @@ export const projectRouter = createTRPCRouter({ .input(z.void()) .output(z.unknown()) .query(async ({ ctx }) => { - const userProjects = await ctx.db - .select({ - id: projects.id, - friendlyName: projects.friendlyName, - internalName: projects.internalName, - createdAt: projects.createdAt, - }) - .from(projects); - - return await Promise.all( - userProjects.map(async (project) => { - const projServices = await ctx.db - .select({ - id: serviceGeneration.id, - name: serviceGeneration.name, - }) - .from(serviceGeneration) - .where(eq(serviceGeneration.serviceId, project.id)); + const projects = await ProjectManager.listForUser( + ctx.session.data.userId, + ); + // we love the nested Promise.all's + // TODO: refactor + return Promise.all( + projects.map(async (project) => { return { - ...project, - services: projServices, + ...project.getData(), + services: await Promise.all( + (await project.getServices()).map((service) => + service.getDataWithGenerations(), + ), + ), }; }), ); diff --git a/src/server/api/routers/projects/project.ts b/src/server/api/routers/projects/project.ts index 18f71bd..7f50fcb 100644 --- a/src/server/api/routers/projects/project.ts +++ b/src/server/api/routers/projects/project.ts @@ -1,6 +1,4 @@ -import { eq } from "drizzle-orm"; import { z } from "zod"; -import { serviceGeneration } from "~/server/db/schema"; import { projectMiddleware } from "../../middleware/project"; import { authenticatedProcedure } from "../../trpc"; @@ -16,26 +14,22 @@ export const getProject = authenticatedProcedure .use(projectMiddleware) .output(z.unknown()) .query(async ({ ctx }) => { - const projServices = await ctx.db - .select({ - id: serviceGeneration.id, - name: serviceGeneration.name, - }) - .from(serviceGeneration) - .where(eq(serviceGeneration.serviceId, ctx.project.id)); + const projServices = await ctx.project.getServices(); // get docker stats const stats = await ctx.docker.listServices({ filters: { - label: [`com.docker.stack.namespace=${ctx.project.internalName}`], + label: [ + `com.docker.stack.namespace=${ctx.project.getData().internalName}`, + ], }, }); return { - ...ctx.project, + ...ctx.project.getData(), services: projServices.map((service) => ({ - ...service, - stats: stats.find((stat) => stat.Spec?.Name === service.name), + ...service.getData(), + stats: stats.find((stat) => stat.Spec?.Name === service.getData().name), })), }; }); diff --git a/src/server/api/routers/projects/service/index.ts b/src/server/api/routers/projects/service/index.ts index e609266..ecab058 100644 --- a/src/server/api/routers/projects/service/index.ts +++ b/src/server/api/routers/projects/service/index.ts @@ -1,5 +1,5 @@ import assert from "assert"; -import { eq } from "drizzle-orm"; +import { eq, sql } from "drizzle-orm"; import { randomBytes } from "node:crypto"; import { z } from "zod"; import { env } from "~/env"; @@ -14,6 +14,7 @@ import { updateServiceDomainsProcedure, updateServiceProcedure, } from "./update"; +import { ServiceSource } from "~/server/db/types"; export const serviceRouter = createTRPCRouter({ containers: getServiceContainers, @@ -33,27 +34,9 @@ export const serviceRouter = createTRPCRouter({ .use(projectMiddleware) .use(serviceMiddleware) .query(async ({ ctx }) => { - // const fullServiceData = await ctx.db.query.service.findFirst({ - // where: eq(serviceGeneration.id, ctx.service.getData().id), - // with: { - // domains: true, - // ports: true, - // volumes: true, - // sysctls: true, - // ulimits: true, - // }, - // }); - - // assert(fullServiceData); - - // return { - // ...fullServiceData, - // deployMode: DOCKER_DEPLOY_MODE_MAP[fullServiceData.deployMode], - // }; - return { ...ctx.service.getData(), - latestGeneration: ctx.service.getData().latestGeneration, + latestGeneration: await ctx.service.fetchFullLatestGeneration(), }; }), @@ -75,42 +58,61 @@ export const serviceRouter = createTRPCRouter({ .use(projectMiddleware) .mutation(async ({ ctx, input }) => { // create a generation for the service - // const [defaultGeneration] = await ctx.db - // .insert(serviceGeneration) - // .values({ - // : ctx.project.getData().id, + const trxResult = await ctx.db.transaction( + async (trx) => { + // create the service + const [data] = await trx + .insert(service) + .values({ + name: input.name, + projectId: ctx.project.getData().id, + latestGenerationId: "", + redeploySecret: randomBytes(env.REDEPLOY_SECRET_BYTES).toString( + "hex", + ), + }) + .returning({ + id: serviceGeneration.id, + }) + .execute() + .catch((err) => { + console.error(err); + throw err; + }); - // status: "pending", - // }) - // .returning({ - // id: serviceGeneration.id, - // }) - // .execute(); + assert(data?.id, "Expected service data to be returned"); - const [data] = await ctx.db - .insert(service) - .values({ - name: input.name, - projectId: ctx.project.getData().id, - latestGenerationId: "", - redeploySecret: randomBytes(env.REDEPLOY_SECRET_BYTES).toString( - "hex", - ), - // source: ServiceSource.Docker, - // dockerImage: "traefik/whoami", - }) - .returning({ - id: serviceGeneration.id, - }) - .execute() - .catch((err) => { - console.error(err); - throw err; - }); + // create initial generation + const [generation] = await trx + .insert(serviceGeneration) + .values({ + serviceId: data.id, + source: ServiceSource.Docker, + dockerImage: "traefik/whoami", + }) + .returning({ + id: serviceGeneration.id, + }); - assert(data?.id); + assert(generation?.id, "Expected generation data to be returned"); - return data.id; + // update the service with the generation id + await trx + .update(service) + .set({ + latestGenerationId: generation.id, + }) + .where(eq(service.id, data.id)) + .execute(); + + return data.id; + }, + { + behavior: "deferred", + }, + ); + + return trxResult; }), delete: authenticatedProcedure diff --git a/src/server/api/routers/projects/service/update.ts b/src/server/api/routers/projects/service/update.ts index 4ce16d4..125c9a2 100644 --- a/src/server/api/routers/projects/service/update.ts +++ b/src/server/api/routers/projects/service/update.ts @@ -73,6 +73,7 @@ export const updateServiceProcedure = authenticatedProcedure id: true, projectId: true, name: true, + serviceId: true, }) .partial(), ) @@ -95,7 +96,7 @@ export const updateServiceProcedure = authenticatedProcedure await ctx.db .update(serviceGeneration) .set(queryUpdate) - .where(eq(serviceGeneration.id, ctx.service.id)) + .where(eq(serviceGeneration.id, ctx.service.getData().latestGenerationId)) .execute(); return true; diff --git a/src/server/db/schema/schema.ts b/src/server/db/schema/schema.ts index 80e87a8..d93ec0a 100644 --- a/src/server/db/schema/schema.ts +++ b/src/server/db/schema/schema.ts @@ -140,10 +140,14 @@ export const service = sqliteTable( onDelete: "cascade", }), + // https://github.com/drizzle-team/drizzle-orm/issues/2252 + // Must manually add `DEFERRABLE INITIALLY DEFERRED` latestGenerationId: text("latest_generation_id") .notNull() .references(() => serviceGeneration.id), + // latestGenerationId: sql`service_generation_id REFERENCES service_generation(id) NOT NULL `, + redeploySecret: text("redeploy_secret").notNull(), deployedGenerationId: text("deployed_generation_id").references( () => serviceGeneration.id, diff --git a/src/server/managers/Project.ts b/src/server/managers/Project.ts index 4e314d8..e7524e5 100644 --- a/src/server/managers/Project.ts +++ b/src/server/managers/Project.ts @@ -32,6 +32,17 @@ export default class ProjectManager { return data ? new ProjectManager(data) : null; } + /** + * Lists all projects for a user. + */ + static async listForUser(userId: string) { + const data = await db.query.projects.findMany({ + where: eq(projects.ownerId, userId), + }); + + return data.map((data) => new ProjectManager(data)); + } + /** * Returns the project data. */ @@ -143,7 +154,7 @@ export default class ProjectManager { const composeStack = await buildDockerStackFile(allServiceData); return await deployOptions.docker.cli( - ["stack", "deploy", "--compose-file", "-", this.projectData.internalName], + ["stack", "deploy", "--compose-file", "-", this.projectData.internalName, deployOptions.force ? "--force-recreate" : ""], { stdin: JSON.stringify(composeStack), }, diff --git a/src/server/server.ts b/src/server/server.ts index 60085ae..db46a06 100644 --- a/src/server/server.ts +++ b/src/server/server.ts @@ -29,15 +29,9 @@ try { } // migrate the database -if (env.NODE_ENV === "production") { - logger.child({ module: "database" }).info("⚙️ Migrating database"); - migrate(db, { migrationsFolder: "./migrations" }); - logger.child({ module: "database" }).info("✅ Database migrated"); -} else { - logger - .child({ module: "database" }) - .info("Not running database migrations, use drizzle-kit push to migrate"); -} +logger.child({ module: "database" }).info("⚙️ Starting database migrations..."); +migrate(db, { migrationsFolder: "./drizzle" }); +logger.child({ module: "database" }).info("✅ Migrations finished!"); // start statistics void stats.start();