dashboards: migrate from filesystem to postgresql
Replace JSON file storage with DB tables for dashboard layouts, screens, projects, and terminal defaults. Fresh drizzle migration with dashboardDefaults table and new columns on screens/projects. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
+65
-38
@@ -48,10 +48,8 @@ CREATE TABLE "user_integrations" (
|
|||||||
"id" serial PRIMARY KEY NOT NULL,
|
"id" serial PRIMARY KEY NOT NULL,
|
||||||
"user_id" integer NOT NULL,
|
"user_id" integer NOT NULL,
|
||||||
"provider" text NOT NULL,
|
"provider" text NOT NULL,
|
||||||
"access_token" text,
|
"server_integration_id" integer,
|
||||||
"refresh_token" text,
|
"config" jsonb DEFAULT '{}'::jsonb NOT NULL,
|
||||||
"expires_at" timestamp with time zone,
|
|
||||||
"profile" jsonb,
|
|
||||||
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
|
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
"updated_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")
|
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
|
"updated_at" timestamp with time zone DEFAULT now() NOT NULL
|
||||||
);
|
);
|
||||||
--> statement-breakpoint
|
--> statement-breakpoint
|
||||||
CREATE TABLE "projects" (
|
CREATE TABLE "dashboard_defaults" (
|
||||||
"id" serial PRIMARY KEY NOT NULL,
|
"id" serial PRIMARY KEY NOT NULL,
|
||||||
"user_id" integer NOT NULL,
|
"user_id" integer NOT NULL,
|
||||||
"slug" text NOT NULL,
|
"terminals" jsonb DEFAULT '{}'::jsonb NOT NULL,
|
||||||
"meta" jsonb DEFAULT '{}'::jsonb NOT NULL,
|
"host_terminals" 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,
|
|
||||||
"updated_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")
|
CONSTRAINT "dashboard_defaults_user_id_unique" UNIQUE("user_id")
|
||||||
);
|
);
|
||||||
--> statement-breakpoint
|
--> statement-breakpoint
|
||||||
CREATE TABLE "screens" (
|
CREATE TABLE "dashboards" (
|
||||||
"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" (
|
|
||||||
"id" text PRIMARY KEY NOT NULL,
|
"id" text PRIMARY KEY NOT NULL,
|
||||||
"user_id" integer NOT NULL,
|
"user_id" integer NOT NULL,
|
||||||
"name" text NOT NULL,
|
"name" text NOT NULL,
|
||||||
@@ -147,7 +133,31 @@ CREATE TABLE "workspaces" (
|
|||||||
"sort_order" integer DEFAULT 0 NOT NULL,
|
"sort_order" integer DEFAULT 0 NOT NULL,
|
||||||
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
|
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
"updated_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
|
--> statement-breakpoint
|
||||||
CREATE TABLE "extensions" (
|
CREATE TABLE "extensions" (
|
||||||
@@ -185,20 +195,6 @@ CREATE TABLE "processes" (
|
|||||||
CONSTRAINT "uq_processes_scope_user_dir" UNIQUE("scope","user_id","dir_name")
|
CONSTRAINT "uq_processes_scope_user_dir" UNIQUE("scope","user_id","dir_name")
|
||||||
);
|
);
|
||||||
--> statement-breakpoint
|
--> 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" (
|
CREATE TABLE "skills" (
|
||||||
"id" serial PRIMARY KEY NOT NULL,
|
"id" serial PRIMARY KEY NOT NULL,
|
||||||
"scope" text NOT NULL,
|
"scope" text NOT NULL,
|
||||||
@@ -296,18 +292,49 @@ CREATE TABLE "server_config" (
|
|||||||
"updated_at" timestamp with time zone DEFAULT now() NOT NULL
|
"updated_at" timestamp with time zone DEFAULT now() NOT NULL
|
||||||
);
|
);
|
||||||
--> statement-breakpoint
|
--> 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 "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 "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 "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_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_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 "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_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_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 "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 "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 "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 "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 "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
|
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 "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 "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 "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_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_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
|
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_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_scope" ON "processes" USING btree ("scope");--> statement-breakpoint
|
||||||
CREATE INDEX "idx_processes_user" ON "processes" USING btree ("user_id");--> 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_scope" ON "skills" USING btree ("scope");--> statement-breakpoint
|
||||||
CREATE INDEX "idx_skills_user" ON "skills" USING btree ("user_id");--> 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
|
CREATE INDEX "idx_tasks_scope" ON "tasks" USING btree ("scope");--> statement-breakpoint
|
||||||
@@ -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";
|
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"id": "65f7442c-6226-471f-b6b3-d559e820ab61",
|
"id": "7187e43e-021e-4623-8c74-8e5e7bc73926",
|
||||||
"prevId": "00000000-0000-0000-0000-000000000000",
|
"prevId": "00000000-0000-0000-0000-000000000000",
|
||||||
"version": "7",
|
"version": "7",
|
||||||
"dialect": "postgresql",
|
"dialect": "postgresql",
|
||||||
@@ -347,29 +347,18 @@
|
|||||||
"primaryKey": false,
|
"primaryKey": false,
|
||||||
"notNull": true
|
"notNull": true
|
||||||
},
|
},
|
||||||
"access_token": {
|
"server_integration_id": {
|
||||||
"name": "access_token",
|
"name": "server_integration_id",
|
||||||
"type": "text",
|
"type": "integer",
|
||||||
"primaryKey": false,
|
"primaryKey": false,
|
||||||
"notNull": false
|
"notNull": false
|
||||||
},
|
},
|
||||||
"refresh_token": {
|
"config": {
|
||||||
"name": "refresh_token",
|
"name": "config",
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
"expires_at": {
|
|
||||||
"name": "expires_at",
|
|
||||||
"type": "timestamp with time zone",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
"profile": {
|
|
||||||
"name": "profile",
|
|
||||||
"type": "jsonb",
|
"type": "jsonb",
|
||||||
"primaryKey": false,
|
"primaryKey": false,
|
||||||
"notNull": false
|
"notNull": true,
|
||||||
|
"default": "'{}'::jsonb"
|
||||||
},
|
},
|
||||||
"created_at": {
|
"created_at": {
|
||||||
"name": "created_at",
|
"name": "created_at",
|
||||||
@@ -400,6 +389,19 @@
|
|||||||
],
|
],
|
||||||
"onDelete": "cascade",
|
"onDelete": "cascade",
|
||||||
"onUpdate": "no action"
|
"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": {},
|
"compositePrimaryKeys": {},
|
||||||
@@ -929,6 +931,177 @@
|
|||||||
"checkConstraints": {},
|
"checkConstraints": {},
|
||||||
"isRLSEnabled": false
|
"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": {
|
"public.projects": {
|
||||||
"name": "projects",
|
"name": "projects",
|
||||||
"schema": "",
|
"schema": "",
|
||||||
@@ -972,6 +1145,13 @@
|
|||||||
"notNull": true,
|
"notNull": true,
|
||||||
"default": "'[]'::jsonb"
|
"default": "'[]'::jsonb"
|
||||||
},
|
},
|
||||||
|
"host_terminals": {
|
||||||
|
"name": "host_terminals",
|
||||||
|
"type": "jsonb",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "'{}'::jsonb"
|
||||||
|
},
|
||||||
"created_at": {
|
"created_at": {
|
||||||
"name": "created_at",
|
"name": "created_at",
|
||||||
"type": "timestamp with time zone",
|
"type": "timestamp with time zone",
|
||||||
@@ -1047,6 +1227,20 @@
|
|||||||
"notNull": true,
|
"notNull": true,
|
||||||
"default": "'[]'::jsonb"
|
"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": {
|
"updated_at": {
|
||||||
"name": "updated_at",
|
"name": "updated_at",
|
||||||
"type": "timestamp with time zone",
|
"type": "timestamp with time zone",
|
||||||
@@ -1086,109 +1280,6 @@
|
|||||||
"checkConstraints": {},
|
"checkConstraints": {},
|
||||||
"isRLSEnabled": false
|
"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": {
|
"public.extensions": {
|
||||||
"name": "extensions",
|
"name": "extensions",
|
||||||
"schema": "",
|
"schema": "",
|
||||||
@@ -1514,108 +1605,6 @@
|
|||||||
"checkConstraints": {},
|
"checkConstraints": {},
|
||||||
"isRLSEnabled": false
|
"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": {
|
"public.skills": {
|
||||||
"name": "skills",
|
"name": "skills",
|
||||||
"schema": "",
|
"schema": "",
|
||||||
@@ -2427,6 +2416,200 @@
|
|||||||
"policies": {},
|
"policies": {},
|
||||||
"checkConstraints": {},
|
"checkConstraints": {},
|
||||||
"isRLSEnabled": false
|
"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": {},
|
"enums": {},
|
||||||
|
|||||||
@@ -5,15 +5,8 @@
|
|||||||
{
|
{
|
||||||
"idx": 0,
|
"idx": 0,
|
||||||
"version": "7",
|
"version": "7",
|
||||||
"when": 1772121115932,
|
"when": 1772795711164,
|
||||||
"tag": "0000_clammy_meteorite",
|
"tag": "0000_futuristic_ink",
|
||||||
"breakpoints": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"idx": 1,
|
|
||||||
"version": "7",
|
|
||||||
"when": 1772265600000,
|
|
||||||
"tag": "0001_rename_workspaces_to_dashboards",
|
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -52,5 +52,17 @@ export {
|
|||||||
getAllSyncedAccounts,
|
getAllSyncedAccounts,
|
||||||
} from './queries/email-accounts';
|
} from './queries/email-accounts';
|
||||||
|
|
||||||
|
export {
|
||||||
|
getAllDashboardState,
|
||||||
|
upsertDashboard,
|
||||||
|
deleteDashboard,
|
||||||
|
upsertScreen,
|
||||||
|
deleteScreen,
|
||||||
|
upsertProject,
|
||||||
|
deleteProject,
|
||||||
|
getDefaults,
|
||||||
|
upsertDefaults,
|
||||||
|
} from './queries/dashboards';
|
||||||
|
|
||||||
export { db } from './db';
|
export { db } from './db';
|
||||||
export * as schema from './schema';
|
export * as schema from './schema';
|
||||||
|
|||||||
@@ -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<Record<string, unknown>> {
|
||||||
|
const result: Record<string, unknown> = {};
|
||||||
|
|
||||||
|
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<void> {
|
||||||
|
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<string, unknown> = { 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<void> {
|
||||||
|
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<void> {
|
||||||
|
const now = new Date();
|
||||||
|
const set: Record<string, unknown> = { 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<void> {
|
||||||
|
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<void> {
|
||||||
|
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<string, unknown> = { 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<void> {
|
||||||
|
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<void> {
|
||||||
|
const now = new Date();
|
||||||
|
const set: Record<string, unknown> = { 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,
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -1,40 +1,66 @@
|
|||||||
import { pgTable, serial, text, integer, timestamp, jsonb, unique } from 'drizzle-orm/pg-core';
|
import { pgTable, serial, text, integer, timestamp, jsonb, unique } from 'drizzle-orm/pg-core';
|
||||||
import { users } from './auth';
|
import { users } from './auth';
|
||||||
|
|
||||||
export const dashboards = pgTable('dashboards', {
|
export const dashboards = pgTable(
|
||||||
id: text('id').primaryKey(),
|
'dashboards',
|
||||||
userId: integer('user_id').notNull().references(() => users.id, { onDelete: 'cascade' }),
|
{
|
||||||
name: text('name').notNull(),
|
id: text('id').primaryKey(),
|
||||||
config: jsonb('config').notNull().default({}),
|
userId: integer('user_id')
|
||||||
layout: jsonb('layout').notNull().default([]),
|
.notNull()
|
||||||
terminals: jsonb('terminals').notNull().default([]),
|
.references(() => users.id, { onDelete: 'cascade' }),
|
||||||
hostTerminals: jsonb('host_terminals').notNull().default([]),
|
name: text('name').notNull(),
|
||||||
sortOrder: integer('sort_order').notNull().default(0),
|
config: jsonb('config').notNull().default({}),
|
||||||
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
|
layout: jsonb('layout').notNull().default([]),
|
||||||
updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
|
terminals: jsonb('terminals').notNull().default([]),
|
||||||
}, (table) => [
|
hostTerminals: jsonb('host_terminals').notNull().default([]),
|
||||||
unique('uq_dashboards_user_id').on(table.userId, table.id),
|
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', {
|
export const screens = pgTable(
|
||||||
id: serial('id').primaryKey(),
|
'screens',
|
||||||
userId: integer('user_id').notNull().references(() => users.id, { onDelete: 'cascade' }),
|
{
|
||||||
name: text('name').notNull(),
|
id: serial('id').primaryKey(),
|
||||||
layout: jsonb('layout').notNull().default([]),
|
userId: integer('user_id')
|
||||||
updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
|
.notNull()
|
||||||
}, (table) => [
|
.references(() => users.id, { onDelete: 'cascade' }),
|
||||||
unique('uq_screens_user_name').on(table.userId, table.name),
|
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(),
|
id: serial('id').primaryKey(),
|
||||||
userId: integer('user_id').notNull().references(() => users.id, { onDelete: 'cascade' }),
|
userId: integer('user_id')
|
||||||
slug: text('slug').notNull(),
|
.notNull()
|
||||||
meta: jsonb('meta').notNull().default({}),
|
.references(() => users.id, { onDelete: 'cascade' })
|
||||||
layout: jsonb('layout').notNull().default([]),
|
.unique(),
|
||||||
terminals: jsonb('terminals').notNull().default([]),
|
terminals: jsonb('terminals').notNull().default({}),
|
||||||
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
|
hostTerminals: jsonb('host_terminals').notNull().default({}),
|
||||||
updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
|
updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
|
||||||
}, (table) => [
|
});
|
||||||
unique('uq_projects_user_slug').on(table.userId, table.slug),
|
|
||||||
]);
|
|
||||||
|
|||||||
@@ -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 { join, resolve } from 'node:path';
|
||||||
import { readdirSync } from 'node:fs';
|
import { readdirSync } from 'node:fs';
|
||||||
import { createRouter } from '@@/create-router';
|
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');
|
const TEMPLATES_DIR = resolve(import.meta.dir, '../../../../seed/project-templates');
|
||||||
|
|
||||||
@@ -10,51 +20,96 @@ export const dashboardsRouter = createRouter();
|
|||||||
|
|
||||||
// GET /dashboards
|
// GET /dashboards
|
||||||
dashboardsRouter.get('/', async (ctx) => {
|
dashboardsRouter.get('/', async (ctx) => {
|
||||||
const email = ctx.get('user').email;
|
const userId = ctx.get('user').id;
|
||||||
|
const state = await getAllDashboardState(userId);
|
||||||
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);
|
|
||||||
return ctx.json(state);
|
return ctx.json(state);
|
||||||
});
|
});
|
||||||
|
|
||||||
// PATCH /dashboards
|
// PATCH /dashboards
|
||||||
dashboardsRouter.patch('/', async (ctx) => {
|
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<string, unknown>;
|
const body = ctx.get('body') as Record<string, unknown>;
|
||||||
const dirs = getDirs(email);
|
|
||||||
|
|
||||||
await mkdir(dirs.dashDir, { recursive: true });
|
|
||||||
|
|
||||||
for (const [key, value] of Object.entries(body)) {
|
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-(.+)$/);
|
const projMetaMatch = key.match(/^proj-meta-(.+)$/);
|
||||||
if (projMetaMatch) {
|
if (projMetaMatch) {
|
||||||
const slug = projMetaMatch[1]!;
|
const slug = projMetaMatch[1]!;
|
||||||
const projectDir = join(dirs.projDir, slug);
|
const projectDir = join(getUserProjectsDir(user.email), slug);
|
||||||
|
|
||||||
if (value === null) {
|
if (value === null) {
|
||||||
|
await deleteProject(userId, slug);
|
||||||
await rm(projectDir, { recursive: true, force: true });
|
await rm(projectDir, { recursive: true, force: true });
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const officerdevDir = join(projectDir, '.officerdev');
|
const meta = value as Record<string, unknown>;
|
||||||
const metaFile = join(officerdevDir, 'meta.json');
|
const isNew = !(await Bun.file(join(projectDir, '.officerdev', 'meta.json')).exists());
|
||||||
const isNew = !(await Bun.file(metaFile).exists());
|
await upsertProject(userId, slug, { meta: value });
|
||||||
await mkdir(officerdevDir, { recursive: true });
|
|
||||||
await writeJsonFile(metaFile, value);
|
|
||||||
|
|
||||||
if (isNew) {
|
if (isNew) {
|
||||||
const meta = value as Record<string, unknown>;
|
|
||||||
if (meta.projectType === 'app') {
|
if (meta.projectType === 'app') {
|
||||||
const templateDir = join(TEMPLATES_DIR, 'simple-app-template');
|
const templateDir = join(TEMPLATES_DIR, 'simple-app-template');
|
||||||
const entries = readdirSync(templateDir);
|
const entries = readdirSync(templateDir);
|
||||||
@@ -63,7 +118,9 @@ dashboardsRouter.patch('/', async (ctx) => {
|
|||||||
await cp(join(templateDir, entry), join(projectDir, entry), { recursive: true });
|
await cp(join(templateDir, entry), join(projectDir, entry), { recursive: true });
|
||||||
}
|
}
|
||||||
const pkgPath = join(projectDir, 'package.json');
|
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) {
|
if (pkg) {
|
||||||
pkg.name = slug;
|
pkg.name = slug;
|
||||||
await Bun.write(pkgPath, JSON.stringify(pkg, null, 2) + '\n');
|
await Bun.write(pkgPath, JSON.stringify(pkg, null, 2) + '\n');
|
||||||
@@ -77,26 +134,31 @@ dashboardsRouter.patch('/', async (ctx) => {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const mapping = resolveKey(dirs, key);
|
// proj-layout-{slug}
|
||||||
if (!mapping) continue;
|
const projLayoutMatch = key.match(/^proj-layout-(.+)$/);
|
||||||
|
if (projLayoutMatch) {
|
||||||
if (value === null) {
|
const slug = projLayoutMatch[1]!;
|
||||||
try {
|
await upsertProject(userId, slug, { layout: value });
|
||||||
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
|
|
||||||
}
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (mapping.dir) await mkdir(mapping.dir, { recursive: true });
|
// proj-terminals-{slug}
|
||||||
await writeJsonFile(mapping.file, value);
|
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);
|
return ctx.json(state);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,3 +1 @@
|
|||||||
export * from './dashboards';
|
export * from './dashboards';
|
||||||
export * from './types';
|
|
||||||
export * from './utils';
|
|
||||||
|
|||||||
@@ -1,3 +0,0 @@
|
|||||||
export type KeyMapping = { file: string; dir?: string };
|
|
||||||
|
|
||||||
export type ResolveDirs = { dashDir: string; screensDir: string; projDir: string };
|
|
||||||
@@ -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<unknown | null> {
|
|
||||||
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<string, unknown>;
|
|
||||||
try { state = (await file.json()) as Record<string, unknown>; } 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<string, unknown>) {
|
|
||||||
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<string, unknown>): Promise<unknown | null> {
|
|
||||||
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<string, unknown>) {
|
|
||||||
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<Record<string, unknown>> {
|
|
||||||
const result: Record<string, unknown> = {};
|
|
||||||
|
|
||||||
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),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -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 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 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 getNativeSkillsDir = () => join(SEED_PATH, 'skills');
|
||||||
|
|
||||||
export const getGlobalSkillsDir = () => join(DATA_PATH, 'skills');
|
export const getGlobalSkillsDir = () => join(DATA_PATH, 'skills');
|
||||||
|
|||||||
Reference in New Issue
Block a user