migration to postgres

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-26 17:42:59 +00:00
co-authored by Claude Opus 4.6
parent 7f04ecd644
commit 500a70910e
54 changed files with 4016 additions and 264 deletions
@@ -0,0 +1,24 @@
import { readFileSync } from 'node:fs';
import { resolve } from 'node:path';
import { defineConfig } from 'drizzle-kit';
const envPath = resolve(__dirname, '../../../.env');
try {
const text = readFileSync(envPath, 'utf-8');
for (const line of text.split('\n')) {
const match = line.match(/^(\w+)=(.*)$/);
if (match) {
const [, key, val] = match;
if (!process.env[key!]) process.env[key!] = val!.replace(/^["']|["']$/g, '');
}
}
} catch {}
export default defineConfig({
schema: './src/schema/index.ts',
out: './migrations',
dialect: 'postgresql',
dbCredentials: {
url: process.env.POSTGRES_URL!,
},
});
@@ -0,0 +1,339 @@
CREATE TABLE "passkey_challenges" (
"id" serial PRIMARY KEY NOT NULL,
"user_id" integer NOT NULL,
"origin" text NOT NULL,
"challenge" text NOT NULL,
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
"expires_at" timestamp with time zone NOT NULL
);
--> statement-breakpoint
CREATE TABLE "passkeys" (
"id" serial PRIMARY KEY NOT NULL,
"user_id" integer NOT NULL,
"origin" text,
"credential_id" text,
"public_key" text,
"counter" integer DEFAULT 0 NOT NULL,
"created_at" timestamp with time zone DEFAULT now() NOT NULL
);
--> statement-breakpoint
CREATE TABLE "token_blacklist" (
"jti" text PRIMARY KEY NOT NULL,
"expires_at" timestamp with time zone NOT NULL
);
--> statement-breakpoint
CREATE TABLE "users" (
"id" serial PRIMARY KEY NOT NULL,
"email" text NOT NULL,
"password" text,
"role" text DEFAULT 'Member' NOT NULL,
"status" text DEFAULT 'Unverified' NOT NULL,
"name" text,
"username" text,
"avatar" text,
"password_changed_at" timestamp with time zone,
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
"updated_at" timestamp with time zone DEFAULT now() NOT NULL,
CONSTRAINT "users_email_unique" UNIQUE("email"),
CONSTRAINT "users_username_unique" UNIQUE("username")
);
--> statement-breakpoint
CREATE TABLE "dock_configs" (
"user_id" integer PRIMARY KEY NOT NULL,
"paths" jsonb DEFAULT '[]'::jsonb NOT NULL,
"updated_at" timestamp with time zone DEFAULT now() NOT NULL
);
--> statement-breakpoint
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,
"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")
);
--> statement-breakpoint
CREATE TABLE "user_settings" (
"user_id" integer PRIMARY KEY NOT NULL,
"settings" jsonb DEFAULT '{}'::jsonb NOT NULL,
"updated_at" timestamp with time zone DEFAULT now() NOT NULL
);
--> statement-breakpoint
CREATE TABLE "user_state" (
"user_id" integer PRIMARY KEY NOT NULL,
"state" jsonb DEFAULT '{}'::jsonb NOT NULL,
"updated_at" timestamp with time zone DEFAULT now() NOT NULL
);
--> statement-breakpoint
CREATE TABLE "chat_groups" (
"slug" text PRIMARY KEY NOT NULL,
"user_id" integer NOT NULL,
"name" text NOT NULL,
"description" text,
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
"updated_at" timestamp with time zone DEFAULT now() NOT NULL
);
--> statement-breakpoint
CREATE TABLE "chat_messages" (
"id" text PRIMARY KEY NOT NULL,
"session_id" text NOT NULL,
"role" text NOT NULL,
"text" text,
"model" text,
"tool_name" text,
"tool_input" jsonb,
"tool_call_id" text,
"output" text,
"is_error" boolean DEFAULT false,
"cost_input_tokens" integer,
"cost_output_tokens" integer,
"cost_total_usd" numeric(12, 6),
"sort_order" integer NOT NULL,
"created_at" timestamp with time zone DEFAULT now() NOT NULL
);
--> statement-breakpoint
CREATE TABLE "chat_sessions" (
"id" text PRIMARY KEY NOT NULL,
"user_id" integer NOT NULL,
"context" text DEFAULT 'chat' NOT NULL,
"context_id" text,
"title" text DEFAULT 'New Chat' NOT NULL,
"model" text,
"cwd" text,
"thinking" text,
"archived" boolean DEFAULT false NOT NULL,
"group_slug" text,
"message_count" integer DEFAULT 0 NOT NULL,
"cost_input_tokens" integer DEFAULT 0 NOT NULL,
"cost_output_tokens" integer DEFAULT 0 NOT NULL,
"cost_total_usd" numeric(12, 6) DEFAULT '0' NOT NULL,
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
"updated_at" timestamp with time zone DEFAULT now() NOT NULL
);
--> 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,
"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,
"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,
"user_id" integer NOT NULL,
"name" text NOT NULL,
"config" jsonb DEFAULT '{}'::jsonb NOT NULL,
"layout" jsonb DEFAULT '[]'::jsonb NOT NULL,
"terminals" jsonb DEFAULT '[]'::jsonb NOT NULL,
"host_terminals" jsonb DEFAULT '[]'::jsonb NOT NULL,
"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")
);
--> statement-breakpoint
CREATE TABLE "extensions" (
"id" serial PRIMARY KEY NOT NULL,
"scope" text NOT NULL,
"user_id" integer,
"dir_name" text NOT NULL,
"name" text NOT NULL,
"implementation" text,
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
"updated_at" timestamp with time zone DEFAULT now() NOT NULL,
CONSTRAINT "uq_extensions_scope_user_dir" UNIQUE("scope","user_id","dir_name")
);
--> statement-breakpoint
CREATE TABLE "item_chats" (
"id" text PRIMARY KEY NOT NULL,
"item_type" text NOT NULL,
"item_id" integer NOT NULL,
"messages" jsonb DEFAULT '[]'::jsonb NOT NULL,
"updated_at" timestamp with time zone DEFAULT now() NOT NULL,
CONSTRAINT "uq_item_chats_type_item" UNIQUE("item_type","item_id")
);
--> statement-breakpoint
CREATE TABLE "processes" (
"id" serial PRIMARY KEY NOT NULL,
"scope" text NOT NULL,
"user_id" integer,
"dir_name" text NOT NULL,
"name" text NOT NULL,
"description" text,
"body" text,
"version" integer DEFAULT 1 NOT NULL,
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
"updated_at" timestamp with time zone DEFAULT now() NOT NULL,
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,
"user_id" integer,
"dir_name" text NOT NULL,
"name" text NOT NULL,
"description" text,
"body" text,
"version" integer DEFAULT 1 NOT NULL,
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
"updated_at" timestamp with time zone DEFAULT now() NOT NULL,
CONSTRAINT "uq_skills_scope_user_dir" UNIQUE("scope","user_id","dir_name")
);
--> statement-breakpoint
CREATE TABLE "tasks" (
"id" serial PRIMARY KEY NOT NULL,
"scope" text NOT NULL,
"user_id" integer,
"dir_name" text NOT NULL,
"name" text NOT NULL,
"description" text,
"body" text,
"version" integer DEFAULT 1 NOT NULL,
"tags" jsonb,
"tools" jsonb,
"skills" jsonb,
"inputs" jsonb,
"outputs" jsonb,
"dependencies" jsonb,
"config" jsonb,
"trigger" jsonb,
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
"updated_at" timestamp with time zone DEFAULT now() NOT NULL,
CONSTRAINT "uq_tasks_scope_user_dir" UNIQUE("scope","user_id","dir_name")
);
--> statement-breakpoint
CREATE TABLE "tools" (
"id" serial PRIMARY KEY NOT NULL,
"scope" text NOT NULL,
"user_id" integer,
"dir_name" text NOT NULL,
"name" text NOT NULL,
"label" text,
"description" text,
"body" text,
"version" integer DEFAULT 1 NOT NULL,
"language" text,
"inputs" jsonb,
"implementation" text,
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
"updated_at" timestamp with time zone DEFAULT now() NOT NULL,
CONSTRAINT "uq_tools_scope_user_dir" UNIQUE("scope","user_id","dir_name")
);
--> statement-breakpoint
CREATE TABLE "queue_jobs" (
"id" text PRIMARY KEY NOT NULL,
"user_id" integer NOT NULL,
"lane" text NOT NULL,
"type" text NOT NULL,
"status" text DEFAULT 'queued' NOT NULL,
"current_step" integer DEFAULT 0 NOT NULL,
"steps" jsonb DEFAULT '[]'::jsonb NOT NULL,
"meta" jsonb,
"error" text,
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
"started_at" timestamp with time zone,
"completed_at" timestamp with time zone
);
--> statement-breakpoint
CREATE TABLE "task_logs" (
"id" serial PRIMARY KEY NOT NULL,
"user_id" integer NOT NULL,
"task_name" text NOT NULL,
"task_dir_name" text NOT NULL,
"entry_name" text NOT NULL,
"entry_type" text NOT NULL,
"provider" text NOT NULL,
"model" text NOT NULL,
"is_error" boolean DEFAULT false NOT NULL,
"messages" jsonb DEFAULT '[]'::jsonb NOT NULL,
"started_at" timestamp with time zone NOT NULL,
"completed_at" timestamp with time zone
);
--> statement-breakpoint
CREATE TABLE "terminal_containers" (
"user_id" integer PRIMARY KEY NOT NULL,
"docker_id" text NOT NULL,
"port" integer NOT NULL,
"updated_at" timestamp with time zone DEFAULT now() NOT NULL
);
--> statement-breakpoint
CREATE TABLE "server_config" (
"key" text PRIMARY KEY NOT NULL,
"value" jsonb NOT NULL,
"updated_at" timestamp with time zone DEFAULT now() NOT NULL
);
--> 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_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 "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
ALTER TABLE "tasks" ADD CONSTRAINT "tasks_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "tools" ADD CONSTRAINT "tools_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 "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
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
CREATE INDEX "idx_sessions_user_context" ON "chat_sessions" USING btree ("user_id","context");--> statement-breakpoint
CREATE INDEX "idx_sessions_user_created" ON "chat_sessions" USING btree ("user_id","created_at");--> statement-breakpoint
CREATE INDEX "idx_sessions_user_group" ON "chat_sessions" USING btree ("user_id","group_slug");--> statement-breakpoint
CREATE INDEX "idx_extensions_scope" ON "extensions" USING btree ("scope");--> statement-breakpoint
CREATE INDEX "idx_extensions_user" ON "extensions" USING btree ("user_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_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
CREATE INDEX "idx_tasks_user" ON "tasks" USING btree ("user_id");--> statement-breakpoint
CREATE INDEX "idx_tools_scope" ON "tools" USING btree ("scope");--> statement-breakpoint
CREATE INDEX "idx_tools_user" ON "tools" USING btree ("user_id");--> statement-breakpoint
CREATE INDEX "idx_queue_jobs_status_lane" ON "queue_jobs" USING btree ("status","lane");--> statement-breakpoint
CREATE INDEX "idx_queue_jobs_user" ON "queue_jobs" USING btree ("user_id");--> statement-breakpoint
CREATE INDEX "idx_task_logs_user_started" ON "task_logs" USING btree ("user_id","started_at");
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,13 @@
{
"version": "7",
"dialect": "postgresql",
"entries": [
{
"idx": 0,
"version": "7",
"when": 1772121115932,
"tag": "0000_clammy_meteorite",
"breakpoints": true
}
]
}
+9 -1
View File
@@ -5,7 +5,15 @@
"type": "module",
"exports": {
".": "./src/index.ts",
"./types": "./src/types.ts"
"./types": "./src/types.ts",
"./db": "./src/db.ts",
"./schema": "./src/schema/index.ts"
},
"scripts": {
"generate": "drizzle-kit generate --config=drizzle.config.ts",
"push": "drizzle-kit push --config=drizzle.config.ts",
"migrate": "drizzle-kit migrate --config=drizzle.config.ts",
"studio": "drizzle-kit studio --config=drizzle.config.ts"
},
"license": "MIT",
"dependencies": {
+12
View File
@@ -0,0 +1,12 @@
import { drizzle } from 'drizzle-orm/postgres-js';
import postgres from 'postgres';
import * as schema from './schema';
const { POSTGRES_URL } = process.env;
if (!POSTGRES_URL) {
throw new Error('POSTGRES_URL environment variable is required');
}
const client = postgres(POSTGRES_URL);
export const db = drizzle(client, { schema });
@@ -0,0 +1,112 @@
import { eq, and, lt, sql } from 'drizzle-orm';
import { db } from '../db';
import { users, passkeys, passkeyChallenges, tokenBlacklist } from '../schema';
import type { UserSelect, UserInsert, PasskeySelect, PasskeyInsert } from '../types';
// ── Users ──
export async function getUsers(): Promise<UserSelect[]> {
return db.select().from(users);
}
export async function getUserById(id: number): Promise<UserSelect | undefined> {
const [user] = await db.select().from(users).where(eq(users.id, id));
return user;
}
export async function getUserByEmail(email: string): Promise<UserSelect | undefined> {
const [user] = await db.select().from(users).where(eq(users.email, email));
return user;
}
export async function getUserCount(): Promise<number> {
const [result] = await db.select({ count: sql<number>`count(*)::int` }).from(users);
return result?.count ?? 0;
}
export async function createUser(data: UserInsert): Promise<UserSelect> {
const [user] = await db.insert(users).values(data).returning();
return user!;
}
export async function updateUser(id: number, data: Partial<Omit<UserSelect, 'id'>>): Promise<UserSelect | undefined> {
const [user] = await db.update(users).set({ ...data, updatedAt: new Date() }).where(eq(users.id, id)).returning();
return user;
}
export async function deleteUser(id: number): Promise<boolean> {
const result = await db.delete(users).where(eq(users.id, id)).returning({ id: users.id });
return result.length > 0;
}
// ── Passkeys ──
export async function getPasskeysByUserId(userId: number): Promise<PasskeySelect[]> {
return db.select().from(passkeys).where(eq(passkeys.userId, userId));
}
export async function getPasskeysByUserIdAndOrigin(userId: number, origin: string): Promise<PasskeySelect[]> {
return db.select().from(passkeys).where(and(eq(passkeys.userId, userId), eq(passkeys.origin, origin)));
}
export async function getPasskeyByCredentialId(userId: number, credentialId: string): Promise<PasskeySelect | undefined> {
const [passkey] = await db
.select()
.from(passkeys)
.where(and(eq(passkeys.userId, userId), eq(passkeys.credentialId, credentialId)));
return passkey;
}
export async function createPasskey(data: PasskeyInsert): Promise<PasskeySelect> {
const [passkey] = await db.insert(passkeys).values(data).returning();
return passkey!;
}
export async function updatePasskey(id: number, data: Partial<Omit<PasskeySelect, 'id'>>): Promise<PasskeySelect | undefined> {
const [passkey] = await db.update(passkeys).set(data).where(eq(passkeys.id, id)).returning();
return passkey;
}
// ── Passkey Challenges ──
export async function storeChallenge(userId: number, origin: string, challenge: string, ttlMs: number) {
const expiresAt = new Date(Date.now() + ttlMs);
// Upsert: delete existing challenge for this user+origin, then insert
await db.delete(passkeyChallenges).where(and(eq(passkeyChallenges.userId, userId), eq(passkeyChallenges.origin, origin)));
await db.insert(passkeyChallenges).values({ userId, origin, challenge, expiresAt });
}
export async function consumeChallenge(userId: number, origin: string): Promise<string | null> {
const now = new Date();
// Clean up expired challenges
await db.delete(passkeyChallenges).where(lt(passkeyChallenges.expiresAt, now));
// Find and delete the matching challenge
const [entry] = await db
.delete(passkeyChallenges)
.where(and(eq(passkeyChallenges.userId, userId), eq(passkeyChallenges.origin, origin)))
.returning();
if (!entry) return null;
if (entry.expiresAt < now) return null;
return entry.challenge;
}
// ── Token Blacklist ──
export async function blacklistToken(jti: string, expiresAt: number) {
// expiresAt comes as Unix seconds from JWT exp claim
const expiresDate = new Date(expiresAt * 1000);
await db.insert(tokenBlacklist).values({ jti, expiresAt: expiresDate }).onConflictDoNothing();
}
export async function isTokenBlacklisted(jti: string): Promise<boolean> {
const [entry] = await db.select({ jti: tokenBlacklist.jti }).from(tokenBlacklist).where(eq(tokenBlacklist.jti, jti));
return !!entry;
}
export async function cleanupExpiredTokens() {
await db.delete(tokenBlacklist).where(lt(tokenBlacklist.expiresAt, new Date()));
}
@@ -0,0 +1,73 @@
import { eq, and } from 'drizzle-orm';
import { db } from '../db';
import { serverIntegrations, userIntegrations } from '../schema';
import type { ServerIntegrationSelect, UserIntegrationSelect } from '../types';
// ── Server Integrations ──
export async function getServerIntegrations(): Promise<ServerIntegrationSelect[]> {
return db.select().from(serverIntegrations);
}
export async function getServerIntegration(provider: string): Promise<ServerIntegrationSelect | undefined> {
const [row] = await db.select().from(serverIntegrations).where(eq(serverIntegrations.provider, provider));
return row;
}
export async function upsertServerIntegration(provider: string, config: Record<string, unknown>, enabled = true): Promise<ServerIntegrationSelect> {
const [row] = await db
.insert(serverIntegrations)
.values({ provider, config, enabled, updatedAt: new Date() })
.onConflictDoUpdate({
target: serverIntegrations.provider,
set: { config, enabled, updatedAt: new Date() },
})
.returning();
return row!;
}
export async function deleteServerIntegration(provider: string): Promise<boolean> {
const result = await db.delete(serverIntegrations).where(eq(serverIntegrations.provider, provider)).returning({ id: serverIntegrations.id });
return result.length > 0;
}
// ── User Integrations ──
export async function getUserIntegrations(userId: number): Promise<UserIntegrationSelect[]> {
return db.select().from(userIntegrations).where(eq(userIntegrations.userId, userId));
}
export async function getUserIntegration(userId: number, provider: string): Promise<UserIntegrationSelect | undefined> {
const [row] = await db
.select()
.from(userIntegrations)
.where(and(eq(userIntegrations.userId, userId), eq(userIntegrations.provider, provider)));
return row;
}
type UpsertUserIntegrationParams = {
userId: number;
provider: string;
serverIntegrationId?: number | null;
config: Record<string, unknown>;
};
export async function upsertUserIntegration({ userId, provider, serverIntegrationId, config }: UpsertUserIntegrationParams): Promise<UserIntegrationSelect> {
const [row] = await db
.insert(userIntegrations)
.values({ userId, provider, serverIntegrationId: serverIntegrationId ?? null, config, updatedAt: new Date() })
.onConflictDoUpdate({
target: [userIntegrations.userId, userIntegrations.provider],
set: { config, serverIntegrationId: serverIntegrationId ?? null, updatedAt: new Date() },
})
.returning();
return row!;
}
export async function deleteUserIntegration(userId: number, provider: string): Promise<boolean> {
const result = await db
.delete(userIntegrations)
.where(and(eq(userIntegrations.userId, userId), eq(userIntegrations.provider, provider)))
.returning({ id: userIntegrations.id });
return result.length > 0;
}
@@ -0,0 +1,20 @@
import { eq } from 'drizzle-orm';
import { db } from '../db';
import { serverConfig } from '../schema';
const SETTINGS_KEY = 'server-settings';
export async function readServerSettings(): Promise<Record<string, unknown>> {
const [row] = await db.select().from(serverConfig).where(eq(serverConfig.key, SETTINGS_KEY));
return (row?.value as Record<string, unknown>) ?? {};
}
export async function writeServerSettings(settings: Record<string, unknown>): Promise<void> {
await db
.insert(serverConfig)
.values({ key: SETTINGS_KEY, value: settings, updatedAt: new Date() })
.onConflictDoUpdate({
target: serverConfig.key,
set: { value: settings, updatedAt: new Date() },
});
}
@@ -0,0 +1,150 @@
import { pgTable, serial, text, integer, timestamp, jsonb, index, unique } from 'drizzle-orm/pg-core';
import { users } from './auth';
// ── Shared columns pattern ──
// Each table has: id, scope, userId, dirName, name, description, body, version, timestamps
// Type-specific columns are added per table
// ── Tasks ──
// Complex frontmatter: inputs, outputs, dependencies, triggers, config, tags, tools, skills
export const tasks = pgTable('tasks', {
id: serial('id').primaryKey(),
scope: text('scope').notNull(),
userId: integer('user_id').references(() => users.id, { onDelete: 'cascade' }),
dirName: text('dir_name').notNull(),
name: text('name').notNull(),
description: text('description'),
body: text('body'),
version: integer('version').notNull().default(1),
tags: jsonb('tags').$type<string[]>(),
tools: jsonb('tools').$type<string[]>(),
skills: jsonb('skills').$type<string[]>(),
inputs: jsonb('inputs'),
outputs: jsonb('outputs'),
dependencies: jsonb('dependencies'),
config: jsonb('config'),
trigger: jsonb('trigger'),
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
}, (table) => [
unique('uq_tasks_scope_user_dir').on(table.scope, table.userId, table.dirName),
index('idx_tasks_scope').on(table.scope),
index('idx_tasks_user').on(table.userId),
]);
// ── Skills ──
// Minimal frontmatter: name, description only. Rich markdown body.
export const skills = pgTable('skills', {
id: serial('id').primaryKey(),
scope: text('scope').notNull(),
userId: integer('user_id').references(() => users.id, { onDelete: 'cascade' }),
dirName: text('dir_name').notNull(),
name: text('name').notNull(),
description: text('description'),
body: text('body'),
version: integer('version').notNull().default(1),
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
}, (table) => [
unique('uq_skills_scope_user_dir').on(table.scope, table.userId, table.dirName),
index('idx_skills_scope').on(table.scope),
index('idx_skills_user').on(table.userId),
]);
// ── Processes ──
// Same shape as skills. Represents documented workflows.
export const processes = pgTable('processes', {
id: serial('id').primaryKey(),
scope: text('scope').notNull(),
userId: integer('user_id').references(() => users.id, { onDelete: 'cascade' }),
dirName: text('dir_name').notNull(),
name: text('name').notNull(),
description: text('description'),
body: text('body'),
version: integer('version').notNull().default(1),
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
}, (table) => [
unique('uq_processes_scope_user_dir').on(table.scope, table.userId, table.dirName),
index('idx_processes_scope').on(table.scope),
index('idx_processes_user').on(table.userId),
]);
// ── Resources ──
// Has separate config (key-value for external service settings). No user scope.
export const resources = pgTable('resources', {
id: serial('id').primaryKey(),
scope: text('scope').notNull(),
dirName: text('dir_name').notNull(),
name: text('name').notNull(),
description: text('description'),
body: text('body'),
version: integer('version').notNull().default(1),
config: jsonb('config').$type<Record<string, string>>().notNull().default({}),
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
}, (table) => [
unique('uq_resources_scope_dir').on(table.scope, table.dirName),
index('idx_resources_scope').on(table.scope),
]);
// ── Tools ──
// Has implementation code, language, structured input params, label.
export const tools = pgTable('tools', {
id: serial('id').primaryKey(),
scope: text('scope').notNull(),
userId: integer('user_id').references(() => users.id, { onDelete: 'cascade' }),
dirName: text('dir_name').notNull(),
name: text('name').notNull(),
label: text('label'),
description: text('description'),
body: text('body'),
version: integer('version').notNull().default(1),
language: text('language'),
inputs: jsonb('inputs'),
implementation: text('implementation'),
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
}, (table) => [
unique('uq_tools_scope_user_dir').on(table.scope, table.userId, table.dirName),
index('idx_tools_scope').on(table.scope),
index('idx_tools_user').on(table.userId),
]);
// ── Extensions ──
// Code-only, no markdown, no chat. Just implementation.
export const extensions = pgTable('extensions', {
id: serial('id').primaryKey(),
scope: text('scope').notNull(),
userId: integer('user_id').references(() => users.id, { onDelete: 'cascade' }),
dirName: text('dir_name').notNull(),
name: text('name').notNull(),
implementation: text('implementation'),
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
}, (table) => [
unique('uq_extensions_scope_user_dir').on(table.scope, table.userId, table.dirName),
index('idx_extensions_scope').on(table.scope),
index('idx_extensions_user').on(table.userId),
]);
// ── Item Chats ──
// Chat history for tasks, skills, processes, resources, tools.
// Uses polymorphic reference (item_type + item_id) instead of per-table FKs.
export const itemChats = pgTable('item_chats', {
id: text('id').primaryKey(),
itemType: text('item_type').notNull(),
itemId: integer('item_id').notNull(),
messages: jsonb('messages').notNull().default([]),
updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
}, (table) => [
unique('uq_item_chats_type_item').on(table.itemType, table.itemId),
index('idx_item_chats_type_item').on(table.itemType, table.itemId),
]);
@@ -0,0 +1,41 @@
import { pgTable, serial, text, integer, timestamp, index } from 'drizzle-orm/pg-core';
export const users = pgTable('users', {
id: serial('id').primaryKey(),
email: text('email').notNull().unique(),
password: text('password'),
role: text('role', { enum: ['Member', 'Admin', 'Owner', 'Super Admin'] }).notNull().default('Member'),
status: text('status', { enum: ['Unverified', 'Active', 'Prospect', 'Invited', 'Blocked', 'Banned', 'Deleted'] }).notNull().default('Unverified'),
name: text('name'),
username: text('username').unique(),
avatar: text('avatar'),
passwordChangedAt: timestamp('password_changed_at', { withTimezone: true }),
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
});
export const passkeys = pgTable('passkeys', {
id: serial('id').primaryKey(),
userId: integer('user_id').notNull().references(() => users.id, { onDelete: 'cascade' }),
origin: text('origin'),
credentialId: text('credential_id'),
publicKey: text('public_key'),
counter: integer('counter').notNull().default(0),
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
});
export const passkeyChallenges = pgTable('passkey_challenges', {
id: serial('id').primaryKey(),
userId: integer('user_id').notNull().references(() => users.id, { onDelete: 'cascade' }),
origin: text('origin').notNull(),
challenge: text('challenge').notNull(),
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
expiresAt: timestamp('expires_at', { withTimezone: true }).notNull(),
});
export const tokenBlacklist = pgTable('token_blacklist', {
jti: text('jti').primaryKey(),
expiresAt: timestamp('expires_at', { withTimezone: true }).notNull(),
}, (table) => [
index('idx_token_blacklist_expires').on(table.expiresAt),
]);
@@ -0,0 +1,56 @@
import { pgTable, text, integer, boolean, timestamp, jsonb, index, numeric } from 'drizzle-orm/pg-core';
import { users } from './auth';
export const chatSessions = pgTable('chat_sessions', {
id: text('id').primaryKey(),
userId: integer('user_id').notNull().references(() => users.id, { onDelete: 'cascade' }),
context: text('context').notNull().default('chat'),
contextId: text('context_id'),
title: text('title').notNull().default('New Chat'),
model: text('model'),
cwd: text('cwd'),
thinking: text('thinking'),
archived: boolean('archived').notNull().default(false),
groupSlug: text('group_slug'),
messageCount: integer('message_count').notNull().default(0),
costInputTokens: integer('cost_input_tokens').notNull().default(0),
costOutputTokens: integer('cost_output_tokens').notNull().default(0),
costTotalUsd: numeric('cost_total_usd', { precision: 12, scale: 6 }).notNull().default('0'),
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
}, (table) => [
index('idx_sessions_user_context').on(table.userId, table.context),
index('idx_sessions_user_created').on(table.userId, table.createdAt),
index('idx_sessions_user_group').on(table.userId, table.groupSlug),
]);
export const chatMessages = pgTable('chat_messages', {
id: text('id').primaryKey(),
sessionId: text('session_id').notNull().references(() => chatSessions.id, { onDelete: 'cascade' }),
role: text('role').notNull(),
text: text('text'),
model: text('model'),
toolName: text('tool_name'),
toolInput: jsonb('tool_input'),
toolCallId: text('tool_call_id'),
output: text('output'),
isError: boolean('is_error').default(false),
costInputTokens: integer('cost_input_tokens'),
costOutputTokens: integer('cost_output_tokens'),
costTotalUsd: numeric('cost_total_usd', { precision: 12, scale: 6 }),
sortOrder: integer('sort_order').notNull(),
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
}, (table) => [
index('idx_messages_session_order').on(table.sessionId, table.sortOrder),
]);
export const chatGroups = pgTable('chat_groups', {
slug: text('slug').primaryKey(),
userId: integer('user_id').notNull().references(() => users.id, { onDelete: 'cascade' }),
name: text('name').notNull(),
description: text('description'),
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
}, (table) => [
index('idx_chat_groups_user').on(table.userId),
]);
@@ -0,0 +1,7 @@
export * from './auth';
export * from './user-data';
export * from './chat';
export * from './workspaces';
export * from './agent-items';
export * from './operations';
export * from './server';
@@ -0,0 +1,44 @@
import { pgTable, serial, text, integer, boolean, timestamp, jsonb, index } from 'drizzle-orm/pg-core';
import { users } from './auth';
export const taskLogs = pgTable('task_logs', {
id: serial('id').primaryKey(),
userId: integer('user_id').notNull().references(() => users.id, { onDelete: 'cascade' }),
taskName: text('task_name').notNull(),
taskDirName: text('task_dir_name').notNull(),
entryName: text('entry_name').notNull(),
entryType: text('entry_type').notNull(),
provider: text('provider').notNull(),
model: text('model').notNull(),
isError: boolean('is_error').notNull().default(false),
messages: jsonb('messages').notNull().default([]),
startedAt: timestamp('started_at', { withTimezone: true }).notNull(),
completedAt: timestamp('completed_at', { withTimezone: true }),
}, (table) => [
index('idx_task_logs_user_started').on(table.userId, table.startedAt),
]);
export const queueJobs = pgTable('queue_jobs', {
id: text('id').primaryKey(),
userId: integer('user_id').notNull().references(() => users.id, { onDelete: 'cascade' }),
lane: text('lane').notNull(),
type: text('type').notNull(),
status: text('status', { enum: ['queued', 'running', 'completed', 'failed', 'cancelled'] }).notNull().default('queued'),
currentStep: integer('current_step').notNull().default(0),
steps: jsonb('steps').notNull().default([]),
meta: jsonb('meta'),
error: text('error'),
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
startedAt: timestamp('started_at', { withTimezone: true }),
completedAt: timestamp('completed_at', { withTimezone: true }),
}, (table) => [
index('idx_queue_jobs_status_lane').on(table.status, table.lane),
index('idx_queue_jobs_user').on(table.userId),
]);
export const terminalContainers = pgTable('terminal_containers', {
userId: integer('user_id').primaryKey().references(() => users.id, { onDelete: 'cascade' }),
dockerId: text('docker_id').notNull(),
port: integer('port').notNull(),
updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
});
@@ -0,0 +1,16 @@
import { pgTable, serial, boolean, text, timestamp, jsonb } from 'drizzle-orm/pg-core';
export const serverConfig = pgTable('server_config', {
key: text('key').primaryKey(),
value: jsonb('value').notNull(),
updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
});
export const serverIntegrations = pgTable('server_integrations', {
id: serial('id').primaryKey(),
provider: text('provider').notNull().unique(),
enabled: boolean('enabled').notNull().default(true),
config: jsonb('config').notNull().default({}),
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
});
@@ -0,0 +1,33 @@
import { pgTable, serial, integer, text, timestamp, jsonb, unique } from 'drizzle-orm/pg-core';
import { users } from './auth';
import { serverIntegrations } from './server';
export const userSettings = pgTable('user_settings', {
userId: integer('user_id').primaryKey().references(() => users.id, { onDelete: 'cascade' }),
settings: jsonb('settings').notNull().default({}),
updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
});
export const userState = pgTable('user_state', {
userId: integer('user_id').primaryKey().references(() => users.id, { onDelete: 'cascade' }),
state: jsonb('state').notNull().default({}),
updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
});
export const userIntegrations = pgTable('user_integrations', {
id: serial('id').primaryKey(),
userId: integer('user_id').notNull().references(() => users.id, { onDelete: 'cascade' }),
provider: text('provider').notNull(),
serverIntegrationId: integer('server_integration_id').references(() => serverIntegrations.id, { onDelete: 'set null' }),
config: jsonb('config').notNull().default({}),
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
}, (table) => [
unique('uq_user_integrations_user_provider').on(table.userId, table.provider),
]);
export const dockConfigs = pgTable('dock_configs', {
userId: integer('user_id').primaryKey().references(() => users.id, { onDelete: 'cascade' }),
paths: jsonb('paths').notNull().default([]),
updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
});
@@ -0,0 +1,40 @@
import { pgTable, serial, text, integer, timestamp, jsonb, unique } from 'drizzle-orm/pg-core';
import { users } from './auth';
export const workspaces = pgTable('workspaces', {
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_workspaces_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 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([]),
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),
]);
+107 -43
View File
@@ -1,50 +1,114 @@
import type { USER_ROLES, USER_STATUSES } from 'definitions';
import type * as Schema from './schema';
// Auth
export type UserSelect = {
id: number;
email: string;
password: string | null;
role: (typeof USER_ROLES)[number] | null;
status: (typeof USER_STATUSES)[number] | null;
name: string | null;
username: string | null;
avatar: string | null;
passwordChangedAt: number | null;
};
export type UserInsert = {
email: string;
password?: string | null;
role?: (typeof USER_ROLES)[number] | null;
status?: (typeof USER_STATUSES)[number] | null;
name?: string | null;
username?: string | null;
avatar?: string | null;
passwordChangedAt?: number | null;
};
// ── Auth ──
export type UserSelect = typeof Schema.users.$inferSelect;
export type UserInsert = typeof Schema.users.$inferInsert;
export type User = UserSelect & {
passkeys: Passkey[];
};
export type PasskeySelect = {
id: number;
email: string;
origin: string | null;
credentialId: string | null;
publicKey: string | null;
counter: number;
};
export type PasskeyInsert = {
email: string;
origin?: string | null;
credentialId?: string | null;
publicKey?: string | null;
counter?: number;
passkeys: PasskeySelect[];
};
export type PasskeySelect = typeof Schema.passkeys.$inferSelect;
export type PasskeyInsert = typeof Schema.passkeys.$inferInsert;
export type Passkey = PasskeySelect & {
user: User;
user: UserSelect;
};
export type PasskeyChallengeSelect = typeof Schema.passkeyChallenges.$inferSelect;
export type PasskeyChallengeInsert = typeof Schema.passkeyChallenges.$inferInsert;
export type TokenBlacklistSelect = typeof Schema.tokenBlacklist.$inferSelect;
export type TokenBlacklistInsert = typeof Schema.tokenBlacklist.$inferInsert;
// ── User Data ──
export type UserSettingsSelect = typeof Schema.userSettings.$inferSelect;
export type UserSettingsInsert = typeof Schema.userSettings.$inferInsert;
export type UserStateSelect = typeof Schema.userState.$inferSelect;
export type UserStateInsert = typeof Schema.userState.$inferInsert;
export type UserIntegrationSelect = typeof Schema.userIntegrations.$inferSelect;
export type UserIntegrationInsert = typeof Schema.userIntegrations.$inferInsert;
export type DockConfigSelect = typeof Schema.dockConfigs.$inferSelect;
export type DockConfigInsert = typeof Schema.dockConfigs.$inferInsert;
// ── Chat ──
export type ChatSessionSelect = typeof Schema.chatSessions.$inferSelect;
export type ChatSessionInsert = typeof Schema.chatSessions.$inferInsert;
export type ChatSession = ChatSessionSelect & {
messages?: ChatMessageSelect[];
};
export type ChatMessageSelect = typeof Schema.chatMessages.$inferSelect;
export type ChatMessageInsert = typeof Schema.chatMessages.$inferInsert;
export type ChatGroupSelect = typeof Schema.chatGroups.$inferSelect;
export type ChatGroupInsert = typeof Schema.chatGroups.$inferInsert;
// ── Workspaces ──
export type WorkspaceSelect = typeof Schema.workspaces.$inferSelect;
export type WorkspaceInsert = typeof Schema.workspaces.$inferInsert;
export type ScreenSelect = typeof Schema.screens.$inferSelect;
export type ScreenInsert = typeof Schema.screens.$inferInsert;
export type ProjectSelect = typeof Schema.projects.$inferSelect;
export type ProjectInsert = typeof Schema.projects.$inferInsert;
// ── Tasks ──
export type TaskSelect = typeof Schema.tasks.$inferSelect;
export type TaskInsert = typeof Schema.tasks.$inferInsert;
// ── Skills ──
export type SkillSelect = typeof Schema.skills.$inferSelect;
export type SkillInsert = typeof Schema.skills.$inferInsert;
// ── Processes ──
export type ProcessSelect = typeof Schema.processes.$inferSelect;
export type ProcessInsert = typeof Schema.processes.$inferInsert;
// ── Resources ──
export type ResourceSelect = typeof Schema.resources.$inferSelect;
export type ResourceInsert = typeof Schema.resources.$inferInsert;
// ── Tools ──
export type ToolSelect = typeof Schema.tools.$inferSelect;
export type ToolInsert = typeof Schema.tools.$inferInsert;
// ── Extensions ──
export type ExtensionSelect = typeof Schema.extensions.$inferSelect;
export type ExtensionInsert = typeof Schema.extensions.$inferInsert;
// ── Item Chats ──
export type ItemChatSelect = typeof Schema.itemChats.$inferSelect;
export type ItemChatInsert = typeof Schema.itemChats.$inferInsert;
// ── Operations ──
export type TaskLogSelect = typeof Schema.taskLogs.$inferSelect;
export type TaskLogInsert = typeof Schema.taskLogs.$inferInsert;
export type QueueJobSelect = typeof Schema.queueJobs.$inferSelect;
export type QueueJobInsert = typeof Schema.queueJobs.$inferInsert;
export type TerminalContainerSelect = typeof Schema.terminalContainers.$inferSelect;
export type TerminalContainerInsert = typeof Schema.terminalContainers.$inferInsert;
// ── Server ──
export type ServerConfigSelect = typeof Schema.serverConfig.$inferSelect;
export type ServerConfigInsert = typeof Schema.serverConfig.$inferInsert;
export type ServerIntegrationSelect = typeof Schema.serverIntegrations.$inferSelect;
export type ServerIntegrationInsert = typeof Schema.serverIntegrations.$inferInsert;