diff --git a/src/databases/officer_db/migrations/0000_clammy_meteorite.sql b/src/databases/officer_db/migrations/0000_futuristic_ink.sql similarity index 86% rename from src/databases/officer_db/migrations/0000_clammy_meteorite.sql rename to src/databases/officer_db/migrations/0000_futuristic_ink.sql index 23dae652..26c7419e 100644 --- a/src/databases/officer_db/migrations/0000_clammy_meteorite.sql +++ b/src/databases/officer_db/migrations/0000_futuristic_ink.sql @@ -48,10 +48,8 @@ CREATE TABLE "user_integrations" ( "id" serial PRIMARY KEY NOT NULL, "user_id" integer NOT NULL, "provider" text NOT NULL, - "access_token" text, - "refresh_token" text, - "expires_at" timestamp with time zone, - "profile" jsonb, + "server_integration_id" integer, + "config" jsonb DEFAULT '{}'::jsonb NOT NULL, "created_at" timestamp with time zone DEFAULT now() NOT NULL, "updated_at" timestamp with time zone DEFAULT now() NOT NULL, CONSTRAINT "uq_user_integrations_user_provider" UNIQUE("user_id","provider") @@ -115,28 +113,16 @@ CREATE TABLE "chat_sessions" ( "updated_at" timestamp with time zone DEFAULT now() NOT NULL ); --> statement-breakpoint -CREATE TABLE "projects" ( +CREATE TABLE "dashboard_defaults" ( "id" serial PRIMARY KEY NOT NULL, "user_id" integer NOT NULL, - "slug" text NOT NULL, - "meta" jsonb DEFAULT '{}'::jsonb NOT NULL, - "layout" jsonb DEFAULT '[]'::jsonb NOT NULL, - "terminals" jsonb DEFAULT '[]'::jsonb NOT NULL, - "created_at" timestamp with time zone DEFAULT now() NOT NULL, + "terminals" jsonb DEFAULT '{}'::jsonb NOT NULL, + "host_terminals" jsonb DEFAULT '{}'::jsonb NOT NULL, "updated_at" timestamp with time zone DEFAULT now() NOT NULL, - CONSTRAINT "uq_projects_user_slug" UNIQUE("user_id","slug") + CONSTRAINT "dashboard_defaults_user_id_unique" UNIQUE("user_id") ); --> statement-breakpoint -CREATE TABLE "screens" ( - "id" serial PRIMARY KEY NOT NULL, - "user_id" integer NOT NULL, - "name" text NOT NULL, - "layout" jsonb DEFAULT '[]'::jsonb NOT NULL, - "updated_at" timestamp with time zone DEFAULT now() NOT NULL, - CONSTRAINT "uq_screens_user_name" UNIQUE("user_id","name") -); ---> statement-breakpoint -CREATE TABLE "workspaces" ( +CREATE TABLE "dashboards" ( "id" text PRIMARY KEY NOT NULL, "user_id" integer NOT NULL, "name" text NOT NULL, @@ -147,7 +133,31 @@ CREATE TABLE "workspaces" ( "sort_order" integer DEFAULT 0 NOT NULL, "created_at" timestamp with time zone DEFAULT now() NOT NULL, "updated_at" timestamp with time zone DEFAULT now() NOT NULL, - CONSTRAINT "uq_workspaces_user_id" UNIQUE("user_id","id") + CONSTRAINT "uq_dashboards_user_id" UNIQUE("user_id","id") +); +--> statement-breakpoint +CREATE TABLE "projects" ( + "id" serial PRIMARY KEY NOT NULL, + "user_id" integer NOT NULL, + "slug" text NOT NULL, + "meta" jsonb DEFAULT '{}'::jsonb NOT NULL, + "layout" jsonb DEFAULT '[]'::jsonb NOT NULL, + "terminals" jsonb DEFAULT '[]'::jsonb NOT NULL, + "host_terminals" jsonb DEFAULT '{}'::jsonb NOT NULL, + "created_at" timestamp with time zone DEFAULT now() NOT NULL, + "updated_at" timestamp with time zone DEFAULT now() NOT NULL, + CONSTRAINT "uq_projects_user_slug" UNIQUE("user_id","slug") +); +--> statement-breakpoint +CREATE TABLE "screens" ( + "id" serial PRIMARY KEY NOT NULL, + "user_id" integer NOT NULL, + "name" text NOT NULL, + "layout" jsonb DEFAULT '[]'::jsonb NOT NULL, + "terminals" jsonb DEFAULT '{}'::jsonb NOT NULL, + "host_terminals" jsonb DEFAULT '{}'::jsonb NOT NULL, + "updated_at" timestamp with time zone DEFAULT now() NOT NULL, + CONSTRAINT "uq_screens_user_name" UNIQUE("user_id","name") ); --> statement-breakpoint CREATE TABLE "extensions" ( @@ -185,20 +195,6 @@ CREATE TABLE "processes" ( CONSTRAINT "uq_processes_scope_user_dir" UNIQUE("scope","user_id","dir_name") ); --> statement-breakpoint -CREATE TABLE "resources" ( - "id" serial PRIMARY KEY NOT NULL, - "scope" text NOT NULL, - "dir_name" text NOT NULL, - "name" text NOT NULL, - "description" text, - "body" text, - "version" integer DEFAULT 1 NOT NULL, - "config" jsonb DEFAULT '{}'::jsonb NOT NULL, - "created_at" timestamp with time zone DEFAULT now() NOT NULL, - "updated_at" timestamp with time zone DEFAULT now() NOT NULL, - CONSTRAINT "uq_resources_scope_dir" UNIQUE("scope","dir_name") -); ---> statement-breakpoint CREATE TABLE "skills" ( "id" serial PRIMARY KEY NOT NULL, "scope" text NOT NULL, @@ -296,18 +292,49 @@ CREATE TABLE "server_config" ( "updated_at" timestamp with time zone DEFAULT now() NOT NULL ); --> statement-breakpoint +CREATE TABLE "server_integrations" ( + "id" serial PRIMARY KEY NOT NULL, + "provider" text NOT NULL, + "enabled" boolean DEFAULT true NOT NULL, + "config" jsonb DEFAULT '{}'::jsonb NOT NULL, + "created_at" timestamp with time zone DEFAULT now() NOT NULL, + "updated_at" timestamp with time zone DEFAULT now() NOT NULL, + CONSTRAINT "server_integrations_provider_unique" UNIQUE("provider") +); +--> statement-breakpoint +CREATE TABLE "email_accounts" ( + "id" serial PRIMARY KEY NOT NULL, + "user_id" integer NOT NULL, + "provider" text NOT NULL, + "email" text NOT NULL, + "display_name" text, + "imap_host" text NOT NULL, + "imap_port" integer NOT NULL, + "imap_secure" boolean DEFAULT true NOT NULL, + "auth_type" text NOT NULL, + "credentials" jsonb DEFAULT '{}'::jsonb NOT NULL, + "enabled" boolean DEFAULT true NOT NULL, + "created_at" timestamp with time zone DEFAULT now() NOT NULL, + "status" text DEFAULT 'connected' NOT NULL, + "sync_meta" jsonb DEFAULT '{}'::jsonb NOT NULL, + "updated_at" timestamp with time zone DEFAULT now() NOT NULL, + CONSTRAINT "uq_email_accounts_user_email" UNIQUE("user_id","email") +); +--> statement-breakpoint ALTER TABLE "passkey_challenges" ADD CONSTRAINT "passkey_challenges_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint ALTER TABLE "passkeys" ADD CONSTRAINT "passkeys_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint ALTER TABLE "dock_configs" ADD CONSTRAINT "dock_configs_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint ALTER TABLE "user_integrations" ADD CONSTRAINT "user_integrations_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "user_integrations" ADD CONSTRAINT "user_integrations_server_integration_id_server_integrations_id_fk" FOREIGN KEY ("server_integration_id") REFERENCES "public"."server_integrations"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint ALTER TABLE "user_settings" ADD CONSTRAINT "user_settings_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint ALTER TABLE "user_state" ADD CONSTRAINT "user_state_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint ALTER TABLE "chat_groups" ADD CONSTRAINT "chat_groups_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint ALTER TABLE "chat_messages" ADD CONSTRAINT "chat_messages_session_id_chat_sessions_id_fk" FOREIGN KEY ("session_id") REFERENCES "public"."chat_sessions"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint ALTER TABLE "chat_sessions" ADD CONSTRAINT "chat_sessions_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "dashboard_defaults" ADD CONSTRAINT "dashboard_defaults_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "dashboards" ADD CONSTRAINT "dashboards_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint ALTER TABLE "projects" ADD CONSTRAINT "projects_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint ALTER TABLE "screens" ADD CONSTRAINT "screens_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint -ALTER TABLE "workspaces" ADD CONSTRAINT "workspaces_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint ALTER TABLE "extensions" ADD CONSTRAINT "extensions_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint ALTER TABLE "processes" ADD CONSTRAINT "processes_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint ALTER TABLE "skills" ADD CONSTRAINT "skills_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint @@ -316,6 +343,7 @@ ALTER TABLE "tools" ADD CONSTRAINT "tools_user_id_users_id_fk" FOREIGN KEY ("use ALTER TABLE "queue_jobs" ADD CONSTRAINT "queue_jobs_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint ALTER TABLE "task_logs" ADD CONSTRAINT "task_logs_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint ALTER TABLE "terminal_containers" ADD CONSTRAINT "terminal_containers_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "email_accounts" ADD CONSTRAINT "email_accounts_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint CREATE INDEX "idx_token_blacklist_expires" ON "token_blacklist" USING btree ("expires_at");--> statement-breakpoint CREATE INDEX "idx_chat_groups_user" ON "chat_groups" USING btree ("user_id");--> statement-breakpoint CREATE INDEX "idx_messages_session_order" ON "chat_messages" USING btree ("session_id","sort_order");--> statement-breakpoint @@ -327,7 +355,6 @@ CREATE INDEX "idx_extensions_user" ON "extensions" USING btree ("user_id");--> s CREATE INDEX "idx_item_chats_type_item" ON "item_chats" USING btree ("item_type","item_id");--> statement-breakpoint CREATE INDEX "idx_processes_scope" ON "processes" USING btree ("scope");--> statement-breakpoint CREATE INDEX "idx_processes_user" ON "processes" USING btree ("user_id");--> statement-breakpoint -CREATE INDEX "idx_resources_scope" ON "resources" USING btree ("scope");--> statement-breakpoint CREATE INDEX "idx_skills_scope" ON "skills" USING btree ("scope");--> statement-breakpoint CREATE INDEX "idx_skills_user" ON "skills" USING btree ("user_id");--> statement-breakpoint CREATE INDEX "idx_tasks_scope" ON "tasks" USING btree ("scope");--> statement-breakpoint diff --git a/src/databases/officer_db/migrations/0001_rename_workspaces_to_dashboards.sql b/src/databases/officer_db/migrations/0001_rename_workspaces_to_dashboards.sql deleted file mode 100644 index 6107e76e..00000000 --- a/src/databases/officer_db/migrations/0001_rename_workspaces_to_dashboards.sql +++ /dev/null @@ -1,4 +0,0 @@ -ALTER TABLE "workspaces" RENAME TO "dashboards";--> statement-breakpoint -ALTER TABLE "dashboards" DROP CONSTRAINT "workspaces_user_id_users_id_fk";--> statement-breakpoint -ALTER TABLE "dashboards" ADD CONSTRAINT "dashboards_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint -ALTER TABLE "dashboards" RENAME CONSTRAINT "uq_workspaces_user_id" TO "uq_dashboards_user_id"; diff --git a/src/databases/officer_db/migrations/meta/0000_snapshot.json b/src/databases/officer_db/migrations/meta/0000_snapshot.json index 2911c3fb..e07dcce1 100644 --- a/src/databases/officer_db/migrations/meta/0000_snapshot.json +++ b/src/databases/officer_db/migrations/meta/0000_snapshot.json @@ -1,5 +1,5 @@ { - "id": "65f7442c-6226-471f-b6b3-d559e820ab61", + "id": "7187e43e-021e-4623-8c74-8e5e7bc73926", "prevId": "00000000-0000-0000-0000-000000000000", "version": "7", "dialect": "postgresql", @@ -347,29 +347,18 @@ "primaryKey": false, "notNull": true }, - "access_token": { - "name": "access_token", - "type": "text", + "server_integration_id": { + "name": "server_integration_id", + "type": "integer", "primaryKey": false, "notNull": false }, - "refresh_token": { - "name": "refresh_token", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "expires_at": { - "name": "expires_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": false - }, - "profile": { - "name": "profile", + "config": { + "name": "config", "type": "jsonb", "primaryKey": false, - "notNull": false + "notNull": true, + "default": "'{}'::jsonb" }, "created_at": { "name": "created_at", @@ -400,6 +389,19 @@ ], "onDelete": "cascade", "onUpdate": "no action" + }, + "user_integrations_server_integration_id_server_integrations_id_fk": { + "name": "user_integrations_server_integration_id_server_integrations_id_fk", + "tableFrom": "user_integrations", + "tableTo": "server_integrations", + "columnsFrom": [ + "server_integration_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" } }, "compositePrimaryKeys": {}, @@ -929,6 +931,177 @@ "checkConstraints": {}, "isRLSEnabled": false }, + "public.dashboard_defaults": { + "name": "dashboard_defaults", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "terminals": { + "name": "terminals", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "host_terminals": { + "name": "host_terminals", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "dashboard_defaults_user_id_users_id_fk": { + "name": "dashboard_defaults_user_id_users_id_fk", + "tableFrom": "dashboard_defaults", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "dashboard_defaults_user_id_unique": { + "name": "dashboard_defaults_user_id_unique", + "nullsNotDistinct": false, + "columns": [ + "user_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.dashboards": { + "name": "dashboards", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "config": { + "name": "config", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "layout": { + "name": "layout", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "terminals": { + "name": "terminals", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "host_terminals": { + "name": "host_terminals", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "dashboards_user_id_users_id_fk": { + "name": "dashboards_user_id_users_id_fk", + "tableFrom": "dashboards", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "uq_dashboards_user_id": { + "name": "uq_dashboards_user_id", + "nullsNotDistinct": false, + "columns": [ + "user_id", + "id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, "public.projects": { "name": "projects", "schema": "", @@ -972,6 +1145,13 @@ "notNull": true, "default": "'[]'::jsonb" }, + "host_terminals": { + "name": "host_terminals", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, "created_at": { "name": "created_at", "type": "timestamp with time zone", @@ -1047,6 +1227,20 @@ "notNull": true, "default": "'[]'::jsonb" }, + "terminals": { + "name": "terminals", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "host_terminals": { + "name": "host_terminals", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, "updated_at": { "name": "updated_at", "type": "timestamp with time zone", @@ -1086,109 +1280,6 @@ "checkConstraints": {}, "isRLSEnabled": false }, - "public.workspaces": { - "name": "workspaces", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true - }, - "user_id": { - "name": "user_id", - "type": "integer", - "primaryKey": false, - "notNull": true - }, - "name": { - "name": "name", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "config": { - "name": "config", - "type": "jsonb", - "primaryKey": false, - "notNull": true, - "default": "'{}'::jsonb" - }, - "layout": { - "name": "layout", - "type": "jsonb", - "primaryKey": false, - "notNull": true, - "default": "'[]'::jsonb" - }, - "terminals": { - "name": "terminals", - "type": "jsonb", - "primaryKey": false, - "notNull": true, - "default": "'[]'::jsonb" - }, - "host_terminals": { - "name": "host_terminals", - "type": "jsonb", - "primaryKey": false, - "notNull": true, - "default": "'[]'::jsonb" - }, - "sort_order": { - "name": "sort_order", - "type": "integer", - "primaryKey": false, - "notNull": true, - "default": 0 - }, - "created_at": { - "name": "created_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true, - "default": "now()" - }, - "updated_at": { - "name": "updated_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true, - "default": "now()" - } - }, - "indexes": {}, - "foreignKeys": { - "workspaces_user_id_users_id_fk": { - "name": "workspaces_user_id_users_id_fk", - "tableFrom": "workspaces", - "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": { - "uq_workspaces_user_id": { - "name": "uq_workspaces_user_id", - "nullsNotDistinct": false, - "columns": [ - "user_id", - "id" - ] - } - }, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, "public.extensions": { "name": "extensions", "schema": "", @@ -1514,108 +1605,6 @@ "checkConstraints": {}, "isRLSEnabled": false }, - "public.resources": { - "name": "resources", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "serial", - "primaryKey": true, - "notNull": true - }, - "scope": { - "name": "scope", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "dir_name": { - "name": "dir_name", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "name": { - "name": "name", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "description": { - "name": "description", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "body": { - "name": "body", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "version": { - "name": "version", - "type": "integer", - "primaryKey": false, - "notNull": true, - "default": 1 - }, - "config": { - "name": "config", - "type": "jsonb", - "primaryKey": false, - "notNull": true, - "default": "'{}'::jsonb" - }, - "created_at": { - "name": "created_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true, - "default": "now()" - }, - "updated_at": { - "name": "updated_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true, - "default": "now()" - } - }, - "indexes": { - "idx_resources_scope": { - "name": "idx_resources_scope", - "columns": [ - { - "expression": "scope", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - } - }, - "foreignKeys": {}, - "compositePrimaryKeys": {}, - "uniqueConstraints": { - "uq_resources_scope_dir": { - "name": "uq_resources_scope_dir", - "nullsNotDistinct": false, - "columns": [ - "scope", - "dir_name" - ] - } - }, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, "public.skills": { "name": "skills", "schema": "", @@ -2427,6 +2416,200 @@ "policies": {}, "checkConstraints": {}, "isRLSEnabled": false + }, + "public.server_integrations": { + "name": "server_integrations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "config": { + "name": "config", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "server_integrations_provider_unique": { + "name": "server_integrations_provider_unique", + "nullsNotDistinct": false, + "columns": [ + "provider" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.email_accounts": { + "name": "email_accounts", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "imap_host": { + "name": "imap_host", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "imap_port": { + "name": "imap_port", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "imap_secure": { + "name": "imap_secure", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "credentials": { + "name": "credentials", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'connected'" + }, + "sync_meta": { + "name": "sync_meta", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "email_accounts_user_id_users_id_fk": { + "name": "email_accounts_user_id_users_id_fk", + "tableFrom": "email_accounts", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "uq_email_accounts_user_email": { + "name": "uq_email_accounts_user_email", + "nullsNotDistinct": false, + "columns": [ + "user_id", + "email" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false } }, "enums": {}, diff --git a/src/databases/officer_db/migrations/meta/_journal.json b/src/databases/officer_db/migrations/meta/_journal.json index 51dcb83c..53cf823f 100644 --- a/src/databases/officer_db/migrations/meta/_journal.json +++ b/src/databases/officer_db/migrations/meta/_journal.json @@ -5,15 +5,8 @@ { "idx": 0, "version": "7", - "when": 1772121115932, - "tag": "0000_clammy_meteorite", - "breakpoints": true - }, - { - "idx": 1, - "version": "7", - "when": 1772265600000, - "tag": "0001_rename_workspaces_to_dashboards", + "when": 1772795711164, + "tag": "0000_futuristic_ink", "breakpoints": true } ] diff --git a/src/databases/officer_db/src/index.ts b/src/databases/officer_db/src/index.ts index 865cef1f..47e459d9 100644 --- a/src/databases/officer_db/src/index.ts +++ b/src/databases/officer_db/src/index.ts @@ -52,5 +52,17 @@ export { getAllSyncedAccounts, } from './queries/email-accounts'; +export { + getAllDashboardState, + upsertDashboard, + deleteDashboard, + upsertScreen, + deleteScreen, + upsertProject, + deleteProject, + getDefaults, + upsertDefaults, +} from './queries/dashboards'; + export { db } from './db'; export * as schema from './schema'; diff --git a/src/databases/officer_db/src/queries/dashboards.ts b/src/databases/officer_db/src/queries/dashboards.ts new file mode 100644 index 00000000..f4581069 --- /dev/null +++ b/src/databases/officer_db/src/queries/dashboards.ts @@ -0,0 +1,219 @@ +import { eq, and } from 'drizzle-orm'; +import { db } from '../db'; +import { dashboards, screens, projects, dashboardDefaults } from '../schema'; + +// ── Full state read ── + +export async function getAllDashboardState(userId: number): Promise> { + const result: Record = {}; + + const [dashRows, screenRows, projectRows, defaultsRow] = await Promise.all([ + db.select().from(dashboards).where(eq(dashboards.userId, userId)), + db.select().from(screens).where(eq(screens.userId, userId)), + db.select().from(projects).where(eq(projects.userId, userId)), + db + .select() + .from(dashboardDefaults) + .where(eq(dashboardDefaults.userId, userId)) + .then((rows) => rows[0]), + ]); + + // Dashboards → workspaces array + per-dashboard keys + if (dashRows.length > 0) { + const sorted = [...dashRows].sort((a, b) => a.sortOrder - b.sortOrder); + result['workspaces'] = sorted.map((d) => ({ id: d.id, name: d.name, ...(d.config as object) })); + + for (const d of dashRows) { + result[`ws-layout-${d.id}`] = d.layout; + result[`ws-terminals-${d.id}`] = d.terminals; + result[`ws-host-terminals-${d.id}`] = d.hostTerminals; + } + } + + // Defaults + if (defaultsRow) { + result['ws-terminals-default'] = defaultsRow.terminals; + result['ws-host-terminals-default'] = defaultsRow.hostTerminals; + } + + // Screens + for (const s of screenRows) { + result[`screens/${s.name}`] = s.layout; + } + + // Projects + const projectList = projectRows.map((p) => ({ + ...(p.meta as object), + id: p.slug, + cwd: `/Projects/${p.slug}`, + })); + result['projects'] = projectList; + + for (const p of projectRows) { + result[`proj-layout-${p.slug}`] = p.layout; + result[`proj-terminals-${p.slug}`] = p.terminals; + result[`proj-host-terminals-${p.slug}`] = p.hostTerminals; + } + + return result; +} + +// ── Dashboard CRUD ── + +type UpsertDashboardData = { + name?: string; + config?: unknown; + layout?: unknown; + terminals?: unknown; + hostTerminals?: unknown; + sortOrder?: number; +}; + +export async function upsertDashboard(userId: number, id: string, data: UpsertDashboardData): Promise { + const now = new Date(); + const existing = await db + .select() + .from(dashboards) + .where(and(eq(dashboards.userId, userId), eq(dashboards.id, id))) + .then((rows) => rows[0]); + + if (existing) { + const set: Record = { updatedAt: now }; + if (data.name !== undefined) set.name = data.name; + if (data.config !== undefined) set.config = data.config; + if (data.layout !== undefined) set.layout = data.layout; + if (data.terminals !== undefined) set.terminals = data.terminals; + if (data.hostTerminals !== undefined) set.hostTerminals = data.hostTerminals; + if (data.sortOrder !== undefined) set.sortOrder = data.sortOrder; + await db.update(dashboards).set(set).where(eq(dashboards.id, id)); + } else { + await db.insert(dashboards).values({ + id, + userId, + name: data.name ?? id, + config: data.config ?? {}, + layout: data.layout ?? [], + terminals: data.terminals ?? [], + hostTerminals: data.hostTerminals ?? [], + sortOrder: data.sortOrder ?? 0, + createdAt: now, + updatedAt: now, + }); + } +} + +export async function deleteDashboard(userId: number, id: string): Promise { + await db.delete(dashboards).where(and(eq(dashboards.userId, userId), eq(dashboards.id, id))); +} + +// ── Screen CRUD ── + +type UpsertScreenData = { + layout?: unknown; + terminals?: unknown; + hostTerminals?: unknown; +}; + +export async function upsertScreen(userId: number, name: string, data: UpsertScreenData): Promise { + const now = new Date(); + const set: Record = { updatedAt: now }; + if (data.layout !== undefined) set.layout = data.layout; + if (data.terminals !== undefined) set.terminals = data.terminals; + if (data.hostTerminals !== undefined) set.hostTerminals = data.hostTerminals; + + await db + .insert(screens) + .values({ + userId, + name, + layout: (data.layout ?? []) as never, + terminals: (data.terminals ?? {}) as never, + hostTerminals: (data.hostTerminals ?? {}) as never, + updatedAt: now, + }) + .onConflictDoUpdate({ + target: [screens.userId, screens.name], + set, + }); +} + +export async function deleteScreen(userId: number, name: string): Promise { + await db.delete(screens).where(and(eq(screens.userId, userId), eq(screens.name, name))); +} + +// ── Project CRUD ── + +type UpsertProjectData = { + meta?: unknown; + layout?: unknown; + terminals?: unknown; + hostTerminals?: unknown; +}; + +export async function upsertProject(userId: number, slug: string, data: UpsertProjectData): Promise { + const now = new Date(); + const existing = await db + .select() + .from(projects) + .where(and(eq(projects.userId, userId), eq(projects.slug, slug))) + .then((rows) => rows[0]); + + if (existing) { + const set: Record = { updatedAt: now }; + if (data.meta !== undefined) set.meta = data.meta; + if (data.layout !== undefined) set.layout = data.layout; + if (data.terminals !== undefined) set.terminals = data.terminals; + if (data.hostTerminals !== undefined) set.hostTerminals = data.hostTerminals; + await db + .update(projects) + .set(set) + .where(and(eq(projects.userId, userId), eq(projects.slug, slug))); + } else { + await db.insert(projects).values({ + userId, + slug, + meta: (data.meta ?? {}) as never, + layout: (data.layout ?? []) as never, + terminals: (data.terminals ?? []) as never, + hostTerminals: (data.hostTerminals ?? {}) as never, + createdAt: now, + updatedAt: now, + }); + } +} + +export async function deleteProject(userId: number, slug: string): Promise { + await db.delete(projects).where(and(eq(projects.userId, userId), eq(projects.slug, slug))); +} + +// ── Defaults ── + +type UpsertDefaultsData = { + terminals?: unknown; + hostTerminals?: unknown; +}; + +export async function getDefaults(userId: number): Promise<{ terminals: unknown; hostTerminals: unknown }> { + const [row] = await db.select().from(dashboardDefaults).where(eq(dashboardDefaults.userId, userId)); + return { terminals: row?.terminals ?? {}, hostTerminals: row?.hostTerminals ?? {} }; +} + +export async function upsertDefaults(userId: number, data: UpsertDefaultsData): Promise { + const now = new Date(); + const set: Record = { updatedAt: now }; + if (data.terminals !== undefined) set.terminals = data.terminals; + if (data.hostTerminals !== undefined) set.hostTerminals = data.hostTerminals; + + await db + .insert(dashboardDefaults) + .values({ + userId, + terminals: (data.terminals ?? {}) as never, + hostTerminals: (data.hostTerminals ?? {}) as never, + updatedAt: now, + }) + .onConflictDoUpdate({ + target: dashboardDefaults.userId, + set, + }); +} diff --git a/src/databases/officer_db/src/schema/dashboards.ts b/src/databases/officer_db/src/schema/dashboards.ts index 981397a2..1f6127b3 100644 --- a/src/databases/officer_db/src/schema/dashboards.ts +++ b/src/databases/officer_db/src/schema/dashboards.ts @@ -1,40 +1,66 @@ import { pgTable, serial, text, integer, timestamp, jsonb, unique } from 'drizzle-orm/pg-core'; import { users } from './auth'; -export const dashboards = pgTable('dashboards', { - id: text('id').primaryKey(), - userId: integer('user_id').notNull().references(() => users.id, { onDelete: 'cascade' }), - name: text('name').notNull(), - config: jsonb('config').notNull().default({}), - layout: jsonb('layout').notNull().default([]), - terminals: jsonb('terminals').notNull().default([]), - hostTerminals: jsonb('host_terminals').notNull().default([]), - sortOrder: integer('sort_order').notNull().default(0), - createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(), - updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(), -}, (table) => [ - unique('uq_dashboards_user_id').on(table.userId, table.id), -]); +export const dashboards = pgTable( + 'dashboards', + { + id: text('id').primaryKey(), + userId: integer('user_id') + .notNull() + .references(() => users.id, { onDelete: 'cascade' }), + name: text('name').notNull(), + config: jsonb('config').notNull().default({}), + layout: jsonb('layout').notNull().default([]), + terminals: jsonb('terminals').notNull().default([]), + hostTerminals: jsonb('host_terminals').notNull().default([]), + sortOrder: integer('sort_order').notNull().default(0), + createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(), + updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(), + }, + (table) => [unique('uq_dashboards_user_id').on(table.userId, table.id)], +); -export const screens = pgTable('screens', { - id: serial('id').primaryKey(), - userId: integer('user_id').notNull().references(() => users.id, { onDelete: 'cascade' }), - name: text('name').notNull(), - layout: jsonb('layout').notNull().default([]), - updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(), -}, (table) => [ - unique('uq_screens_user_name').on(table.userId, table.name), -]); +export const screens = pgTable( + 'screens', + { + id: serial('id').primaryKey(), + userId: integer('user_id') + .notNull() + .references(() => users.id, { onDelete: 'cascade' }), + name: text('name').notNull(), + layout: jsonb('layout').notNull().default([]), + terminals: jsonb('terminals').notNull().default({}), + hostTerminals: jsonb('host_terminals').notNull().default({}), + updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(), + }, + (table) => [unique('uq_screens_user_name').on(table.userId, table.name)], +); -export const projects = pgTable('projects', { +export const projects = pgTable( + 'projects', + { + id: serial('id').primaryKey(), + userId: integer('user_id') + .notNull() + .references(() => users.id, { onDelete: 'cascade' }), + slug: text('slug').notNull(), + meta: jsonb('meta').notNull().default({}), + layout: jsonb('layout').notNull().default([]), + terminals: jsonb('terminals').notNull().default([]), + hostTerminals: jsonb('host_terminals').notNull().default({}), + createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(), + updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(), + }, + (table) => [unique('uq_projects_user_slug').on(table.userId, table.slug)], +); + +export const dashboardDefaults = pgTable('dashboard_defaults', { id: serial('id').primaryKey(), - userId: integer('user_id').notNull().references(() => users.id, { onDelete: 'cascade' }), - slug: text('slug').notNull(), - meta: jsonb('meta').notNull().default({}), - layout: jsonb('layout').notNull().default([]), - terminals: jsonb('terminals').notNull().default([]), - createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(), + userId: integer('user_id') + .notNull() + .references(() => users.id, { onDelete: 'cascade' }) + .unique(), + terminals: jsonb('terminals').notNull().default({}), + hostTerminals: jsonb('host_terminals').notNull().default({}), updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(), -}, (table) => [ - unique('uq_projects_user_slug').on(table.userId, table.slug), -]); +}); diff --git a/src/servers/api/dashboards/dashboards.ts b/src/servers/api/dashboards/dashboards.ts index af47a6e7..1dd47b12 100644 --- a/src/servers/api/dashboards/dashboards.ts +++ b/src/servers/api/dashboards/dashboards.ts @@ -1,8 +1,18 @@ -import { mkdir, readdir, rm, cp } from 'node:fs/promises'; +import { mkdir, rm, cp } from 'node:fs/promises'; import { join, resolve } from 'node:path'; import { readdirSync } from 'node:fs'; import { createRouter } from '@@/create-router'; -import { getDirs, migrateDashboardsDir, migrateFromState, migrateHomepageToScreens, readAllDashboardsState, resolveKey, writeJsonFile } from './utils'; +import { getUserProjectsDir } from '@@/data-path'; +import { + getAllDashboardState, + upsertDashboard, + deleteDashboard, + upsertScreen, + deleteScreen, + upsertProject, + deleteProject, + upsertDefaults, +} from 'officerdb'; const TEMPLATES_DIR = resolve(import.meta.dir, '../../../../seed/project-templates'); @@ -10,51 +20,96 @@ export const dashboardsRouter = createRouter(); // GET /dashboards dashboardsRouter.get('/', async (ctx) => { - const email = ctx.get('user').email; - - await migrateDashboardsDir(email); - - const dirs = getDirs(email); - - const dirFile = Bun.file(join(dirs.dashDir, 'index.json')); - if (!(await dirFile.exists())) { - await migrateFromState(email, dirs); - } - - await migrateHomepageToScreens(dirs, email); - - const state = await readAllDashboardsState(dirs); + const userId = ctx.get('user').id; + const state = await getAllDashboardState(userId); return ctx.json(state); }); // PATCH /dashboards dashboardsRouter.patch('/', async (ctx) => { - const email = ctx.get('user').email; + const user = ctx.get('user'); + const userId = user.id; const body = ctx.get('body') as Record; - const dirs = getDirs(email); - - await mkdir(dirs.dashDir, { recursive: true }); for (const [key, value] of Object.entries(body)) { - // Handle proj-meta-{slug}: create/update/delete project + // workspaces — array of dashboard definitions with ordering + if (key === 'workspaces') { + const workspaces = value as Array<{ id: string; name: string; [k: string]: unknown }>; + for (let i = 0; i < workspaces.length; i++) { + const ws = workspaces[i]!; + const { id, name, ...config } = ws; + await upsertDashboard(userId, id, { name, config, sortOrder: i }); + } + continue; + } + + // ws-layout-{id} + const wsLayoutMatch = key.match(/^ws-layout-(.+)$/); + if (wsLayoutMatch) { + const id = wsLayoutMatch[1]!; + if (value === null) { + await deleteDashboard(userId, id); + } else { + await upsertDashboard(userId, id, { layout: value }); + } + continue; + } + + // ws-terminals-default / ws-host-terminals-default + if (key === 'ws-terminals-default') { + await upsertDefaults(userId, { terminals: value }); + continue; + } + if (key === 'ws-host-terminals-default') { + await upsertDefaults(userId, { hostTerminals: value }); + continue; + } + + // ws-terminals-{id} + const wsTerminalsMatch = key.match(/^ws-terminals-(.+)$/); + if (wsTerminalsMatch) { + const id = wsTerminalsMatch[1]!; + await upsertDashboard(userId, id, { terminals: value }); + continue; + } + + // ws-host-terminals-{id} + const wsHostTerminalsMatch = key.match(/^ws-host-terminals-(.+)$/); + if (wsHostTerminalsMatch) { + const id = wsHostTerminalsMatch[1]!; + await upsertDashboard(userId, id, { hostTerminals: value }); + continue; + } + + // screens/{name} + const screensMatch = key.match(/^screens\/(.+)$/); + if (screensMatch) { + const name = screensMatch[1]!; + if (value === null) { + await deleteScreen(userId, name); + } else { + await upsertScreen(userId, name, { layout: value }); + } + continue; + } + + // proj-meta-{slug} — create/update/delete project const projMetaMatch = key.match(/^proj-meta-(.+)$/); if (projMetaMatch) { const slug = projMetaMatch[1]!; - const projectDir = join(dirs.projDir, slug); + const projectDir = join(getUserProjectsDir(user.email), slug); if (value === null) { + await deleteProject(userId, slug); await rm(projectDir, { recursive: true, force: true }); continue; } - const officerdevDir = join(projectDir, '.officerdev'); - const metaFile = join(officerdevDir, 'meta.json'); - const isNew = !(await Bun.file(metaFile).exists()); - await mkdir(officerdevDir, { recursive: true }); - await writeJsonFile(metaFile, value); + const meta = value as Record; + const isNew = !(await Bun.file(join(projectDir, '.officerdev', 'meta.json')).exists()); + await upsertProject(userId, slug, { meta: value }); if (isNew) { - const meta = value as Record; if (meta.projectType === 'app') { const templateDir = join(TEMPLATES_DIR, 'simple-app-template'); const entries = readdirSync(templateDir); @@ -63,7 +118,9 @@ dashboardsRouter.patch('/', async (ctx) => { await cp(join(templateDir, entry), join(projectDir, entry), { recursive: true }); } const pkgPath = join(projectDir, 'package.json'); - const pkg = await Bun.file(pkgPath).json().catch(() => null); + const pkg = await Bun.file(pkgPath) + .json() + .catch(() => null); if (pkg) { pkg.name = slug; await Bun.write(pkgPath, JSON.stringify(pkg, null, 2) + '\n'); @@ -77,26 +134,31 @@ dashboardsRouter.patch('/', async (ctx) => { continue; } - const mapping = resolveKey(dirs, key); - if (!mapping) continue; - - if (value === null) { - try { - await rm(mapping.file, { force: true }); - if (mapping.dir) { - const remaining = await readdir(mapping.dir); - if (remaining.length === 0) await rm(mapping.dir, { recursive: true, force: true }); - } - } catch { - // ignore - } + // proj-layout-{slug} + const projLayoutMatch = key.match(/^proj-layout-(.+)$/); + if (projLayoutMatch) { + const slug = projLayoutMatch[1]!; + await upsertProject(userId, slug, { layout: value }); continue; } - if (mapping.dir) await mkdir(mapping.dir, { recursive: true }); - await writeJsonFile(mapping.file, value); + // proj-terminals-{slug} + const projTerminalsMatch = key.match(/^proj-terminals-(.+)$/); + if (projTerminalsMatch) { + const slug = projTerminalsMatch[1]!; + await upsertProject(userId, slug, { terminals: value }); + continue; + } + + // proj-host-terminals-{slug} + const projHostTerminalsMatch = key.match(/^proj-host-terminals-(.+)$/); + if (projHostTerminalsMatch) { + const slug = projHostTerminalsMatch[1]!; + await upsertProject(userId, slug, { hostTerminals: value }); + continue; + } } - const state = await readAllDashboardsState(dirs); + const state = await getAllDashboardState(userId); return ctx.json(state); }); diff --git a/src/servers/api/dashboards/index.ts b/src/servers/api/dashboards/index.ts index 0732128f..954313fd 100644 --- a/src/servers/api/dashboards/index.ts +++ b/src/servers/api/dashboards/index.ts @@ -1,3 +1 @@ export * from './dashboards'; -export * from './types'; -export * from './utils'; diff --git a/src/servers/api/dashboards/types.ts b/src/servers/api/dashboards/types.ts deleted file mode 100644 index 722c0920..00000000 --- a/src/servers/api/dashboards/types.ts +++ /dev/null @@ -1,3 +0,0 @@ -export type KeyMapping = { file: string; dir?: string }; - -export type ResolveDirs = { dashDir: string; screensDir: string; projDir: string }; diff --git a/src/servers/api/dashboards/utils.ts b/src/servers/api/dashboards/utils.ts deleted file mode 100644 index 572c8e8c..00000000 --- a/src/servers/api/dashboards/utils.ts +++ /dev/null @@ -1,263 +0,0 @@ -import { mkdir, readdir, rename, rm } from 'node:fs/promises'; -import { join } from 'node:path'; -import { getUserDashboardsDir, getUserHomepageDashboardDir, getUserStateFile, getUserProjectsDir, DATA_PATH } from '@@/data-path'; -import type { KeyMapping, ResolveDirs } from './types'; - -const RESERVED_DIRS = new Set(['screens']); - -export function resolveKey(dirs: ResolveDirs, key: string): KeyMapping | null { - if (key === 'workspaces') return { file: join(dirs.dashDir, 'index.json') }; - if (key === 'ws-terminals-default') return { file: join(dirs.dashDir, 'default-terminals.json') }; - if (key === 'ws-host-terminals-default') return { file: join(dirs.dashDir, 'default-host-terminals.json') }; - - // screens/{name} → screens/{name}/layout.json - const screensMatch = key.match(/^screens\/(.+)$/); - if (screensMatch) { - const name = screensMatch[1]!; - const dir = join(dirs.screensDir, name); - return { file: join(dir, 'layout.json'), dir }; - } - - const layoutMatch = key.match(/^ws-layout-(.+)$/); - if (layoutMatch) { - const id = layoutMatch[1]!; - const dir = join(dirs.dashDir, id); - return { file: join(dir, 'layout.json'), dir }; - } - - const terminalsMatch = key.match(/^ws-terminals-(.+)$/); - if (terminalsMatch) { - const id = terminalsMatch[1]!; - const dir = join(dirs.dashDir, id); - return { file: join(dir, 'terminals.json'), dir }; - } - - const hostTerminalsMatch = key.match(/^ws-host-terminals-(.+)$/); - if (hostTerminalsMatch) { - const id = hostTerminalsMatch[1]!; - const dir = join(dirs.dashDir, id); - return { file: join(dir, 'host-terminals.json'), dir }; - } - - // Project keys — stored in {projDir}/{slug}/.officerdev/ - const projMetaMatch = key.match(/^proj-meta-(.+)$/); - if (projMetaMatch) { - const slug = projMetaMatch[1]!; - const dir = join(dirs.projDir, slug, '.officerdev'); - return { file: join(dir, 'meta.json'), dir }; - } - - const projLayoutMatch = key.match(/^proj-layout-(.+)$/); - if (projLayoutMatch) { - const slug = projLayoutMatch[1]!; - const dir = join(dirs.projDir, slug, '.officerdev'); - return { file: join(dir, 'layout.json'), dir }; - } - - const projTerminalsMatch = key.match(/^proj-terminals-(.+)$/); - if (projTerminalsMatch) { - const slug = projTerminalsMatch[1]!; - const dir = join(dirs.projDir, slug, '.officerdev'); - return { file: join(dir, 'terminals.json'), dir }; - } - - const projHostTerminalsMatch = key.match(/^proj-host-terminals-(.+)$/); - if (projHostTerminalsMatch) { - const slug = projHostTerminalsMatch[1]!; - const dir = join(dirs.projDir, slug, '.officerdev'); - return { file: join(dir, 'host-terminals.json'), dir }; - } - - return null; -} - -export async function readJsonFile(path: string): Promise { - try { - const file = Bun.file(path); - if (!(await file.exists())) return null; - return await file.json(); - } catch { - return null; - } -} - -export async function writeJsonFile(path: string, data: unknown) { - await Bun.write(path, JSON.stringify(data, null, 2)); -} - -export async function migrateFromState(email: string, dirs: ResolveDirs) { - const stateFile = getUserStateFile(email); - const file = Bun.file(stateFile); - if (!(await file.exists())) return; - - let state: Record; - try { state = (await file.json()) as Record; } catch { return; } - const wsKeys = Object.keys(state).filter( - (k) => k === 'workspaces' || k.startsWith('ws-layout-') || k.startsWith('ws-terminals-') || k.startsWith('ws-host-terminals-'), - ); - if (wsKeys.length === 0) return; - - await mkdir(dirs.dashDir, { recursive: true }); - - for (const key of wsKeys) { - const migratedKey = key === 'ws-layout-workspaces' ? 'screens/homepage' : key; - const mapping = resolveKey(dirs, migratedKey); - if (!mapping) continue; - if (mapping.dir) await mkdir(mapping.dir, { recursive: true }); - await writeJsonFile(mapping.file, state[key]); - } - - const cleaned = { ...state }; - for (const key of wsKeys) delete cleaned[key]; - await Bun.write(stateFile, JSON.stringify(cleaned, null, 2)); -} - -export async function migrateHomepageToScreens(dirs: ResolveDirs, email: string) { - const oldHomepageDir = getUserHomepageDashboardDir(email); - const layoutFile = join(oldHomepageDir, 'layout.json'); - if (!(await Bun.file(layoutFile).exists())) return; - - const targetDir = join(dirs.screensDir, 'homepage'); - await mkdir(targetDir, { recursive: true }); - - for (const name of ['layout.json', 'terminals.json', 'host-terminals.json']) { - const src = join(oldHomepageDir, name); - if (await Bun.file(src).exists()) { - await rename(src, join(targetDir, name)); - } - } - - const remaining = await readdir(oldHomepageDir); - if (remaining.length === 0) await rm(oldHomepageDir, { recursive: true, force: true }); - - const oldWsHomepageDir = join(dirs.dashDir, 'ws-homepage'); - const oldWsLayout = join(oldWsHomepageDir, 'layout.json'); - if (!(await Bun.file(oldWsLayout).exists())) return; - - for (const name of ['layout.json', 'terminals.json', 'host-terminals.json']) { - const src = join(oldWsHomepageDir, name); - const target = join(targetDir, name); - if (await Bun.file(src).exists() && !(await Bun.file(target).exists())) { - await rename(src, target); - } - } - - const wsRemaining = await readdir(oldWsHomepageDir); - if (wsRemaining.length === 0) await rm(oldWsHomepageDir, { recursive: true, force: true }); -} - -// Migrate on-disk directory from old 'workspaces' name to 'dashboards' -export async function migrateDashboardsDir(email: string) { - const oldDir = join(DATA_PATH, email, 'workspaces'); - const newDir = getUserDashboardsDir(email); - try { - const oldExists = await Bun.file(join(oldDir, 'index.json')).exists() || await readdir(oldDir).then(() => true).catch(() => false); - if (oldExists) { - const newExists = await readdir(newDir).then(() => true).catch(() => false); - if (!newExists) { - await rename(oldDir, newDir); - } - } - } catch { - // ignore — old dir doesn't exist - } -} - -async function readDashboardDir(dirPath: string, id: string, result: Record) { - const layout = await readJsonFile(join(dirPath, 'layout.json')); - if (layout !== null) result[`ws-layout-${id}`] = layout; - - const terminals = await readJsonFile(join(dirPath, 'terminals.json')); - if (terminals !== null) result[`ws-terminals-${id}`] = terminals; - - const hostTerminals = await readJsonFile(join(dirPath, 'host-terminals.json')); - if (hostTerminals !== null) result[`ws-host-terminals-${id}`] = hostTerminals; -} - -async function readProjectDir(projectPath: string, slug: string, result: Record): Promise { - const officerdevDir = join(projectPath, '.officerdev'); - const meta = await readJsonFile(join(officerdevDir, 'meta.json')); - if (!meta) return null; - - const layout = await readJsonFile(join(officerdevDir, 'layout.json')); - if (layout !== null) result[`proj-layout-${slug}`] = layout; - - const terminals = await readJsonFile(join(officerdevDir, 'terminals.json')); - if (terminals !== null) result[`proj-terminals-${slug}`] = terminals; - - const hostTerminals = await readJsonFile(join(officerdevDir, 'host-terminals.json')); - if (hostTerminals !== null) result[`proj-host-terminals-${slug}`] = hostTerminals; - - return { ...(meta as object), id: slug, cwd: `/Projects/${slug}` }; -} - -async function readScreensDir(screensDir: string, result: Record) { - let entries: import('node:fs').Dirent[] = []; - try { - entries = await readdir(screensDir, { withFileTypes: true }); - } catch { - return; - } - - for (const entry of entries) { - if (!entry.isDirectory()) continue; - const layout = await readJsonFile(join(screensDir, entry.name, 'layout.json')); - if (layout !== null) result[`screens/${entry.name}`] = layout; - } -} - -export async function readAllDashboardsState(dirs: ResolveDirs): Promise> { - const result: Record = {}; - - const indexData = await readJsonFile(join(dirs.dashDir, 'index.json')); - if (indexData !== null) result['workspaces'] = indexData; - - const defaultTerminals = await readJsonFile(join(dirs.dashDir, 'default-terminals.json')); - if (defaultTerminals !== null) result['ws-terminals-default'] = defaultTerminals; - - const defaultHostTerminals = await readJsonFile(join(dirs.dashDir, 'default-host-terminals.json')); - if (defaultHostTerminals !== null) result['ws-host-terminals-default'] = defaultHostTerminals; - - // Read screens - await readScreensDir(dirs.screensDir, result); - - // Read per-dashboard subdirs - let entries: import('node:fs').Dirent[] = []; - try { - entries = await readdir(dirs.dashDir, { withFileTypes: true }); - } catch { - return result; - } - - for (const entry of entries) { - if (!entry.isDirectory() || RESERVED_DIRS.has(entry.name)) continue; - await readDashboardDir(join(dirs.dashDir, entry.name), entry.name, result); - } - - // Read project data — scan directories containing .officerdev/meta.json - const projects: unknown[] = []; - let projEntries: import('node:fs').Dirent[] = []; - try { - projEntries = await readdir(dirs.projDir, { withFileTypes: true }); - } catch { - result['projects'] = projects; - return result; - } - - for (const entry of projEntries) { - if (!entry.isDirectory()) continue; - const meta = await readProjectDir(join(dirs.projDir, entry.name), entry.name, result); - if (meta) projects.push(meta); - } - result['projects'] = projects; - - return result; -} - -export function getDirs(email: string): ResolveDirs { - return { - dashDir: getUserDashboardsDir(email), - screensDir: join(getUserDashboardsDir(email), 'screens'), - projDir: getUserProjectsDir(email), - }; -} diff --git a/src/servers/data-path.ts b/src/servers/data-path.ts index eb9a77f3..a045972a 100644 --- a/src/servers/data-path.ts +++ b/src/servers/data-path.ts @@ -37,12 +37,8 @@ export const getUserStateDir = (email: string) => join(DATA_PATH, email, 'state' export const getUserStateFile = (email: string) => join(DATA_PATH, email, 'state', 'state.json'); -export const getUserDashboardsDir = (email: string) => join(DATA_PATH, email, 'dashboards'); - export const getUserProjectsDir = (email: string) => join(DATA_PATH, email, 'home', 'Projects'); -export const getUserHomepageDashboardDir = (email: string) => join(DATA_PATH, email, 'ws-homepage'); - export const getNativeSkillsDir = () => join(SEED_PATH, 'skills'); export const getGlobalSkillsDir = () => join(DATA_PATH, 'skills');