mirror of
https://github.com/khoaliber/dockhand.git
synced 2026-03-03 21:19:06 +00:00
483 lines
22 KiB
TypeScript
483 lines
22 KiB
TypeScript
/**
|
|
* Drizzle ORM Schema for Dockhand - PostgreSQL Version
|
|
*
|
|
* This schema is used for PostgreSQL migrations and is a mirror of the SQLite schema
|
|
* with PostgreSQL-specific types and syntax.
|
|
*/
|
|
|
|
import {
|
|
pgTable,
|
|
text,
|
|
integer,
|
|
serial,
|
|
boolean,
|
|
doublePrecision,
|
|
bigint,
|
|
timestamp,
|
|
unique,
|
|
index
|
|
} from 'drizzle-orm/pg-core';
|
|
import { sql } from 'drizzle-orm';
|
|
|
|
// =============================================================================
|
|
// CORE TABLES
|
|
// =============================================================================
|
|
|
|
export const environments = pgTable('environments', {
|
|
id: serial('id').primaryKey(),
|
|
name: text('name').notNull().unique(),
|
|
host: text('host'),
|
|
port: integer('port').default(2375),
|
|
protocol: text('protocol').default('http'),
|
|
tlsCa: text('tls_ca'),
|
|
tlsCert: text('tls_cert'),
|
|
tlsKey: text('tls_key'),
|
|
tlsSkipVerify: boolean('tls_skip_verify').default(false),
|
|
icon: text('icon').default('globe'),
|
|
collectActivity: boolean('collect_activity').default(true),
|
|
collectMetrics: boolean('collect_metrics').default(true),
|
|
highlightChanges: boolean('highlight_changes').default(true),
|
|
labels: text('labels'), // JSON array of label strings for categorization
|
|
// Connection settings
|
|
connectionType: text('connection_type').default('socket'), // 'socket' | 'direct' | 'hawser-standard' | 'hawser-edge'
|
|
socketPath: text('socket_path').default('/var/run/docker.sock'), // Unix socket path for 'socket' connection type
|
|
hawserToken: text('hawser_token'), // Plain-text token for hawser-standard auth
|
|
hawserLastSeen: timestamp('hawser_last_seen', { mode: 'string' }),
|
|
hawserAgentId: text('hawser_agent_id'),
|
|
hawserAgentName: text('hawser_agent_name'),
|
|
hawserVersion: text('hawser_version'),
|
|
hawserCapabilities: text('hawser_capabilities'), // JSON array: ["compose", "exec", "metrics"]
|
|
createdAt: timestamp('created_at', { mode: 'string' }).defaultNow(),
|
|
updatedAt: timestamp('updated_at', { mode: 'string' }).defaultNow()
|
|
});
|
|
|
|
export const hawserTokens = pgTable('hawser_tokens', {
|
|
id: serial('id').primaryKey(),
|
|
token: text('token').notNull().unique(), // Hashed token
|
|
tokenPrefix: text('token_prefix').notNull(), // First 8 chars for identification
|
|
name: text('name').notNull(),
|
|
environmentId: integer('environment_id').references(() => environments.id, { onDelete: 'cascade' }),
|
|
isActive: boolean('is_active').default(true),
|
|
lastUsed: timestamp('last_used', { mode: 'string' }),
|
|
createdAt: timestamp('created_at', { mode: 'string' }).defaultNow(),
|
|
expiresAt: timestamp('expires_at', { mode: 'string' })
|
|
});
|
|
|
|
export const registries = pgTable('registries', {
|
|
id: serial('id').primaryKey(),
|
|
name: text('name').notNull().unique(),
|
|
url: text('url').notNull(),
|
|
username: text('username'),
|
|
password: text('password'),
|
|
isDefault: boolean('is_default').default(false),
|
|
createdAt: timestamp('created_at', { mode: 'string' }).defaultNow(),
|
|
updatedAt: timestamp('updated_at', { mode: 'string' }).defaultNow()
|
|
});
|
|
|
|
export const settings = pgTable('settings', {
|
|
key: text('key').primaryKey(),
|
|
value: text('value').notNull(),
|
|
updatedAt: timestamp('updated_at', { mode: 'string' }).defaultNow()
|
|
});
|
|
|
|
// =============================================================================
|
|
// EVENT TRACKING TABLES
|
|
// =============================================================================
|
|
|
|
export const stackEvents = pgTable('stack_events', {
|
|
id: serial('id').primaryKey(),
|
|
environmentId: integer('environment_id').references(() => environments.id, { onDelete: 'cascade' }),
|
|
stackName: text('stack_name').notNull(),
|
|
eventType: text('event_type').notNull(),
|
|
timestamp: timestamp('timestamp', { mode: 'string' }).defaultNow(),
|
|
metadata: text('metadata')
|
|
});
|
|
|
|
export const hostMetrics = pgTable('host_metrics', {
|
|
id: serial('id').primaryKey(),
|
|
environmentId: integer('environment_id').references(() => environments.id, { onDelete: 'cascade' }),
|
|
cpuPercent: doublePrecision('cpu_percent').notNull(),
|
|
memoryPercent: doublePrecision('memory_percent').notNull(),
|
|
memoryUsed: bigint('memory_used', { mode: 'number' }),
|
|
memoryTotal: bigint('memory_total', { mode: 'number' }),
|
|
timestamp: timestamp('timestamp', { mode: 'string' }).defaultNow()
|
|
}, (table) => ({
|
|
envTimestampIdx: index('host_metrics_env_timestamp_idx').on(table.environmentId, table.timestamp)
|
|
}));
|
|
|
|
// =============================================================================
|
|
// CONFIGURATION TABLES
|
|
// =============================================================================
|
|
|
|
export const configSets = pgTable('config_sets', {
|
|
id: serial('id').primaryKey(),
|
|
name: text('name').notNull().unique(),
|
|
description: text('description'),
|
|
envVars: text('env_vars'),
|
|
labels: text('labels'),
|
|
ports: text('ports'),
|
|
volumes: text('volumes'),
|
|
networkMode: text('network_mode').default('bridge'),
|
|
restartPolicy: text('restart_policy').default('no'),
|
|
createdAt: timestamp('created_at', { mode: 'string' }).defaultNow(),
|
|
updatedAt: timestamp('updated_at', { mode: 'string' }).defaultNow()
|
|
});
|
|
|
|
export const autoUpdateSettings = pgTable('auto_update_settings', {
|
|
id: serial('id').primaryKey(),
|
|
environmentId: integer('environment_id').references(() => environments.id),
|
|
containerName: text('container_name').notNull(),
|
|
enabled: boolean('enabled').default(false),
|
|
scheduleType: text('schedule_type').default('daily'),
|
|
cronExpression: text('cron_expression'),
|
|
vulnerabilityCriteria: text('vulnerability_criteria').default('never'), // 'never' | 'any' | 'critical_high' | 'critical' | 'more_than_current'
|
|
lastChecked: timestamp('last_checked', { mode: 'string' }),
|
|
lastUpdated: timestamp('last_updated', { mode: 'string' }),
|
|
createdAt: timestamp('created_at', { mode: 'string' }).defaultNow(),
|
|
updatedAt: timestamp('updated_at', { mode: 'string' }).defaultNow()
|
|
}, (table) => ({
|
|
envContainerUnique: unique().on(table.environmentId, table.containerName)
|
|
}));
|
|
|
|
export const notificationSettings = pgTable('notification_settings', {
|
|
id: serial('id').primaryKey(),
|
|
type: text('type').notNull(),
|
|
name: text('name').notNull(),
|
|
enabled: boolean('enabled').default(true),
|
|
config: text('config').notNull(),
|
|
eventTypes: text('event_types'),
|
|
createdAt: timestamp('created_at', { mode: 'string' }).defaultNow(),
|
|
updatedAt: timestamp('updated_at', { mode: 'string' }).defaultNow()
|
|
});
|
|
|
|
export const environmentNotifications = pgTable('environment_notifications', {
|
|
id: serial('id').primaryKey(),
|
|
environmentId: integer('environment_id').notNull().references(() => environments.id, { onDelete: 'cascade' }),
|
|
notificationId: integer('notification_id').notNull().references(() => notificationSettings.id, { onDelete: 'cascade' }),
|
|
enabled: boolean('enabled').default(true),
|
|
eventTypes: text('event_types'),
|
|
createdAt: timestamp('created_at', { mode: 'string' }).defaultNow(),
|
|
updatedAt: timestamp('updated_at', { mode: 'string' }).defaultNow()
|
|
}, (table) => ({
|
|
envNotifUnique: unique().on(table.environmentId, table.notificationId)
|
|
}));
|
|
|
|
// =============================================================================
|
|
// AUTHENTICATION TABLES
|
|
// =============================================================================
|
|
|
|
export const authSettings = pgTable('auth_settings', {
|
|
id: serial('id').primaryKey(),
|
|
authEnabled: boolean('auth_enabled').default(false),
|
|
defaultProvider: text('default_provider').default('local'),
|
|
sessionTimeout: integer('session_timeout').default(86400),
|
|
createdAt: timestamp('created_at', { mode: 'string' }).defaultNow(),
|
|
updatedAt: timestamp('updated_at', { mode: 'string' }).defaultNow()
|
|
});
|
|
|
|
export const users = pgTable('users', {
|
|
id: serial('id').primaryKey(),
|
|
username: text('username').notNull().unique(),
|
|
email: text('email'),
|
|
passwordHash: text('password_hash').notNull(),
|
|
displayName: text('display_name'),
|
|
avatar: text('avatar'),
|
|
authProvider: text('auth_provider').default('local'), // e.g., 'local', 'oidc:Keycloak', 'ldap:AD'
|
|
mfaEnabled: boolean('mfa_enabled').default(false),
|
|
mfaSecret: text('mfa_secret'),
|
|
isActive: boolean('is_active').default(true),
|
|
lastLogin: timestamp('last_login', { mode: 'string' }),
|
|
createdAt: timestamp('created_at', { mode: 'string' }).defaultNow(),
|
|
updatedAt: timestamp('updated_at', { mode: 'string' }).defaultNow()
|
|
});
|
|
|
|
export const sessions = pgTable('sessions', {
|
|
id: text('id').primaryKey(),
|
|
userId: integer('user_id').notNull().references(() => users.id, { onDelete: 'cascade' }),
|
|
provider: text('provider').notNull(),
|
|
expiresAt: timestamp('expires_at', { mode: 'string' }).notNull(),
|
|
createdAt: timestamp('created_at', { mode: 'string' }).defaultNow()
|
|
}, (table) => ({
|
|
userIdIdx: index('sessions_user_id_idx').on(table.userId),
|
|
expiresAtIdx: index('sessions_expires_at_idx').on(table.expiresAt)
|
|
}));
|
|
|
|
export const ldapConfig = pgTable('ldap_config', {
|
|
id: serial('id').primaryKey(),
|
|
name: text('name').notNull(),
|
|
enabled: boolean('enabled').default(false),
|
|
serverUrl: text('server_url').notNull(),
|
|
bindDn: text('bind_dn'),
|
|
bindPassword: text('bind_password'),
|
|
baseDn: text('base_dn').notNull(),
|
|
userFilter: text('user_filter').default('(uid={{username}})'),
|
|
usernameAttribute: text('username_attribute').default('uid'),
|
|
emailAttribute: text('email_attribute').default('mail'),
|
|
displayNameAttribute: text('display_name_attribute').default('cn'),
|
|
groupBaseDn: text('group_base_dn'),
|
|
groupFilter: text('group_filter'),
|
|
adminGroup: text('admin_group'),
|
|
roleMappings: text('role_mappings'), // JSON: [{ groupDn: string, roleId: number }]
|
|
tlsEnabled: boolean('tls_enabled').default(false),
|
|
tlsCa: text('tls_ca'),
|
|
createdAt: timestamp('created_at', { mode: 'string' }).defaultNow(),
|
|
updatedAt: timestamp('updated_at', { mode: 'string' }).defaultNow()
|
|
});
|
|
|
|
export const oidcConfig = pgTable('oidc_config', {
|
|
id: serial('id').primaryKey(),
|
|
name: text('name').notNull(),
|
|
enabled: boolean('enabled').default(false),
|
|
issuerUrl: text('issuer_url').notNull(),
|
|
clientId: text('client_id').notNull(),
|
|
clientSecret: text('client_secret').notNull(),
|
|
redirectUri: text('redirect_uri').notNull(),
|
|
scopes: text('scopes').default('openid profile email'),
|
|
usernameClaim: text('username_claim').default('preferred_username'),
|
|
emailClaim: text('email_claim').default('email'),
|
|
displayNameClaim: text('display_name_claim').default('name'),
|
|
adminClaim: text('admin_claim'),
|
|
adminValue: text('admin_value'),
|
|
roleMappingsClaim: text('role_mappings_claim').default('groups'),
|
|
roleMappings: text('role_mappings'),
|
|
createdAt: timestamp('created_at', { mode: 'string' }).defaultNow(),
|
|
updatedAt: timestamp('updated_at', { mode: 'string' }).defaultNow()
|
|
});
|
|
|
|
// =============================================================================
|
|
// ROLE-BASED ACCESS CONTROL TABLES
|
|
// =============================================================================
|
|
|
|
export const roles = pgTable('roles', {
|
|
id: serial('id').primaryKey(),
|
|
name: text('name').notNull().unique(),
|
|
description: text('description'),
|
|
isSystem: boolean('is_system').default(false),
|
|
permissions: text('permissions').notNull(),
|
|
environmentIds: text('environment_ids'), // JSON array of env IDs, null = all environments
|
|
createdAt: timestamp('created_at', { mode: 'string' }).defaultNow(),
|
|
updatedAt: timestamp('updated_at', { mode: 'string' }).defaultNow()
|
|
});
|
|
|
|
export const userRoles = pgTable('user_roles', {
|
|
id: serial('id').primaryKey(),
|
|
userId: integer('user_id').notNull().references(() => users.id, { onDelete: 'cascade' }),
|
|
roleId: integer('role_id').notNull().references(() => roles.id, { onDelete: 'cascade' }),
|
|
environmentId: integer('environment_id').references(() => environments.id, { onDelete: 'cascade' }),
|
|
createdAt: timestamp('created_at', { mode: 'string' }).defaultNow()
|
|
}, (table) => ({
|
|
userRoleEnvUnique: unique().on(table.userId, table.roleId, table.environmentId)
|
|
}));
|
|
|
|
// =============================================================================
|
|
// GIT INTEGRATION TABLES
|
|
// =============================================================================
|
|
|
|
export const gitCredentials = pgTable('git_credentials', {
|
|
id: serial('id').primaryKey(),
|
|
name: text('name').notNull().unique(),
|
|
authType: text('auth_type').notNull().default('none'),
|
|
username: text('username'),
|
|
password: text('password'),
|
|
sshPrivateKey: text('ssh_private_key'),
|
|
sshPassphrase: text('ssh_passphrase'),
|
|
createdAt: timestamp('created_at', { mode: 'string' }).defaultNow(),
|
|
updatedAt: timestamp('updated_at', { mode: 'string' }).defaultNow()
|
|
});
|
|
|
|
export const gitRepositories = pgTable('git_repositories', {
|
|
id: serial('id').primaryKey(),
|
|
name: text('name').notNull().unique(),
|
|
url: text('url').notNull(),
|
|
branch: text('branch').default('main'),
|
|
credentialId: integer('credential_id').references(() => gitCredentials.id, { onDelete: 'set null' }),
|
|
composePath: text('compose_path').default('docker-compose.yml'),
|
|
environmentId: integer('environment_id'),
|
|
autoUpdate: boolean('auto_update').default(false),
|
|
autoUpdateSchedule: text('auto_update_schedule').default('daily'),
|
|
autoUpdateCron: text('auto_update_cron').default('0 3 * * *'),
|
|
webhookEnabled: boolean('webhook_enabled').default(false),
|
|
webhookSecret: text('webhook_secret'),
|
|
lastSync: timestamp('last_sync', { mode: 'string' }),
|
|
lastCommit: text('last_commit'),
|
|
syncStatus: text('sync_status').default('pending'),
|
|
syncError: text('sync_error'),
|
|
createdAt: timestamp('created_at', { mode: 'string' }).defaultNow(),
|
|
updatedAt: timestamp('updated_at', { mode: 'string' }).defaultNow()
|
|
});
|
|
|
|
export const gitStacks = pgTable('git_stacks', {
|
|
id: serial('id').primaryKey(),
|
|
stackName: text('stack_name').notNull(),
|
|
environmentId: integer('environment_id').references(() => environments.id, { onDelete: 'cascade' }),
|
|
repositoryId: integer('repository_id').notNull().references(() => gitRepositories.id, { onDelete: 'cascade' }),
|
|
composePath: text('compose_path').default('docker-compose.yml'),
|
|
envFilePath: text('env_file_path'), // Path to .env file in repository (e.g., ".env", "config/.env.prod")
|
|
autoUpdate: boolean('auto_update').default(false),
|
|
autoUpdateSchedule: text('auto_update_schedule').default('daily'),
|
|
autoUpdateCron: text('auto_update_cron').default('0 3 * * *'),
|
|
webhookEnabled: boolean('webhook_enabled').default(false),
|
|
webhookSecret: text('webhook_secret'),
|
|
lastSync: timestamp('last_sync', { mode: 'string' }),
|
|
lastCommit: text('last_commit'),
|
|
syncStatus: text('sync_status').default('pending'),
|
|
syncError: text('sync_error'),
|
|
createdAt: timestamp('created_at', { mode: 'string' }).defaultNow(),
|
|
updatedAt: timestamp('updated_at', { mode: 'string' }).defaultNow()
|
|
}, (table) => ({
|
|
stackEnvUnique: unique().on(table.stackName, table.environmentId)
|
|
}));
|
|
|
|
export const stackSources = pgTable('stack_sources', {
|
|
id: serial('id').primaryKey(),
|
|
stackName: text('stack_name').notNull(),
|
|
environmentId: integer('environment_id').references(() => environments.id, { onDelete: 'cascade' }),
|
|
sourceType: text('source_type').notNull().default('internal'),
|
|
gitRepositoryId: integer('git_repository_id').references(() => gitRepositories.id, { onDelete: 'set null' }),
|
|
gitStackId: integer('git_stack_id').references(() => gitStacks.id, { onDelete: 'set null' }),
|
|
createdAt: timestamp('created_at', { mode: 'string' }).defaultNow(),
|
|
updatedAt: timestamp('updated_at', { mode: 'string' }).defaultNow()
|
|
}, (table) => ({
|
|
stackSourceEnvUnique: unique().on(table.stackName, table.environmentId)
|
|
}));
|
|
|
|
export const stackEnvironmentVariables = pgTable('stack_environment_variables', {
|
|
id: serial('id').primaryKey(),
|
|
stackName: text('stack_name').notNull(),
|
|
environmentId: integer('environment_id').references(() => environments.id, { onDelete: 'cascade' }),
|
|
key: text('key').notNull(),
|
|
value: text('value').notNull(),
|
|
isSecret: boolean('is_secret').default(false),
|
|
createdAt: timestamp('created_at', { mode: 'string' }).defaultNow(),
|
|
updatedAt: timestamp('updated_at', { mode: 'string' }).defaultNow()
|
|
}, (table) => ({
|
|
stackEnvVarUnique: unique().on(table.stackName, table.environmentId, table.key)
|
|
}));
|
|
|
|
// =============================================================================
|
|
// SECURITY TABLES
|
|
// =============================================================================
|
|
|
|
export const vulnerabilityScans = pgTable('vulnerability_scans', {
|
|
id: serial('id').primaryKey(),
|
|
environmentId: integer('environment_id').references(() => environments.id, { onDelete: 'cascade' }),
|
|
imageId: text('image_id').notNull(),
|
|
imageName: text('image_name').notNull(),
|
|
scanner: text('scanner').notNull(),
|
|
scannedAt: timestamp('scanned_at', { mode: 'string' }).notNull(),
|
|
scanDuration: integer('scan_duration'),
|
|
criticalCount: integer('critical_count').default(0),
|
|
highCount: integer('high_count').default(0),
|
|
mediumCount: integer('medium_count').default(0),
|
|
lowCount: integer('low_count').default(0),
|
|
negligibleCount: integer('negligible_count').default(0),
|
|
unknownCount: integer('unknown_count').default(0),
|
|
vulnerabilities: text('vulnerabilities'),
|
|
error: text('error'),
|
|
createdAt: timestamp('created_at', { mode: 'string' }).defaultNow()
|
|
}, (table) => ({
|
|
envImageIdx: index('vulnerability_scans_env_image_idx').on(table.environmentId, table.imageId)
|
|
}));
|
|
|
|
// =============================================================================
|
|
// AUDIT LOGGING TABLES
|
|
// =============================================================================
|
|
|
|
export const auditLogs = pgTable('audit_logs', {
|
|
id: serial('id').primaryKey(),
|
|
userId: integer('user_id').references(() => users.id, { onDelete: 'set null' }),
|
|
username: text('username').notNull(),
|
|
action: text('action').notNull(),
|
|
entityType: text('entity_type').notNull(),
|
|
entityId: text('entity_id'),
|
|
entityName: text('entity_name'),
|
|
environmentId: integer('environment_id').references(() => environments.id, { onDelete: 'set null' }),
|
|
description: text('description'),
|
|
details: text('details'),
|
|
ipAddress: text('ip_address'),
|
|
userAgent: text('user_agent'),
|
|
createdAt: timestamp('created_at', { mode: 'string' }).defaultNow()
|
|
}, (table) => ({
|
|
userIdIdx: index('audit_logs_user_id_idx').on(table.userId),
|
|
createdAtIdx: index('audit_logs_created_at_idx').on(table.createdAt)
|
|
}));
|
|
|
|
// =============================================================================
|
|
// CONTAINER ACTIVITY TABLES
|
|
// =============================================================================
|
|
|
|
export const containerEvents = pgTable('container_events', {
|
|
id: serial('id').primaryKey(),
|
|
environmentId: integer('environment_id').references(() => environments.id, { onDelete: 'cascade' }),
|
|
containerId: text('container_id').notNull(),
|
|
containerName: text('container_name'),
|
|
image: text('image'),
|
|
action: text('action').notNull(),
|
|
actorAttributes: text('actor_attributes'),
|
|
timestamp: timestamp('timestamp', { mode: 'string' }).notNull(),
|
|
createdAt: timestamp('created_at', { mode: 'string' }).defaultNow()
|
|
}, (table) => ({
|
|
envTimestampIdx: index('container_events_env_timestamp_idx').on(table.environmentId, table.timestamp)
|
|
}));
|
|
|
|
// =============================================================================
|
|
// SCHEDULE EXECUTION TABLES
|
|
// =============================================================================
|
|
|
|
export const scheduleExecutions = pgTable('schedule_executions', {
|
|
id: serial('id').primaryKey(),
|
|
// Link to the scheduled job
|
|
scheduleType: text('schedule_type').notNull(), // 'container_update' | 'git_stack_sync' | 'system_cleanup'
|
|
scheduleId: integer('schedule_id').notNull(), // ID in autoUpdateSettings or gitStacks, or 0 for system jobs
|
|
environmentId: integer('environment_id').references(() => environments.id, { onDelete: 'cascade' }),
|
|
// What ran
|
|
entityName: text('entity_name').notNull(), // container name or stack name
|
|
// When and how
|
|
triggeredBy: text('triggered_by').notNull(), // 'cron' | 'webhook' | 'manual'
|
|
triggeredAt: timestamp('triggered_at', { mode: 'string' }).notNull(),
|
|
startedAt: timestamp('started_at', { mode: 'string' }),
|
|
completedAt: timestamp('completed_at', { mode: 'string' }),
|
|
duration: integer('duration'), // milliseconds
|
|
// Result
|
|
status: text('status').notNull(), // 'queued' | 'running' | 'success' | 'failed' | 'skipped'
|
|
errorMessage: text('error_message'),
|
|
// Details
|
|
details: text('details'), // JSON with execution details
|
|
logs: text('logs'), // Execution logs/output
|
|
createdAt: timestamp('created_at', { mode: 'string' }).defaultNow()
|
|
}, (table) => ({
|
|
typeIdIdx: index('schedule_executions_type_id_idx').on(table.scheduleType, table.scheduleId)
|
|
}));
|
|
|
|
// =============================================================================
|
|
// PENDING CONTAINER UPDATES TABLE
|
|
// =============================================================================
|
|
|
|
export const pendingContainerUpdates = pgTable('pending_container_updates', {
|
|
id: serial('id').primaryKey(),
|
|
environmentId: integer('environment_id').notNull().references(() => environments.id, { onDelete: 'cascade' }),
|
|
containerId: text('container_id').notNull(),
|
|
containerName: text('container_name').notNull(),
|
|
currentImage: text('current_image').notNull(),
|
|
checkedAt: timestamp('checked_at', { mode: 'string' }).defaultNow(),
|
|
createdAt: timestamp('created_at', { mode: 'string' }).defaultNow()
|
|
}, (table) => ({
|
|
envContainerUnique: unique().on(table.environmentId, table.containerId)
|
|
}));
|
|
|
|
// =============================================================================
|
|
// USER PREFERENCES TABLE (unified key-value store)
|
|
// =============================================================================
|
|
|
|
export const userPreferences = pgTable('user_preferences', {
|
|
id: serial('id').primaryKey(),
|
|
userId: integer('user_id').references(() => users.id, { onDelete: 'cascade' }), // NULL = shared (free edition), set = per-user (enterprise)
|
|
environmentId: integer('environment_id').references(() => environments.id, { onDelete: 'cascade' }), // NULL for global prefs
|
|
key: text('key').notNull(), // e.g., 'dashboard_layout', 'logs_favorites'
|
|
value: text('value').notNull(), // JSON value
|
|
createdAt: timestamp('created_at', { mode: 'string' }).defaultNow(),
|
|
updatedAt: timestamp('updated_at', { mode: 'string' }).defaultNow()
|
|
}, (table) => [
|
|
unique().on(table.userId, table.environmentId, table.key)
|
|
]);
|