1.0.38
This commit is contained in:
177 files changed
+21182
-1042
No files matched your search
@@ -655,6 +655,11 @@ async function initializeDatabase() {
|
||||
if (verbose) logStep('Opening SQLite database...');
|
||||
rawClient = new Database(dbPath);
|
||||
|
||||
// Enforce foreign keys — better-sqlite3 defaults this OFF, which made every
|
||||
// ON DELETE CASCADE in the schema (backup_configs → environments/destinations,
|
||||
// etc.) dead code, leaving dangling rows + ghost schedules after a delete
|
||||
// (audit #12). Must be set per-connection, before any statements run.
|
||||
rawClient.pragma('foreign_keys = ON');
|
||||
// Enable WAL mode for better performance and concurrency
|
||||
rawClient.pragma('journal_mode = WAL');
|
||||
// Synchronous NORMAL is a good balance between safety and speed
|
||||
@@ -924,6 +929,8 @@ export const scheduleExecutions = schemaProxy.scheduleExecutions;
|
||||
export const stackEnvironmentVariables = schemaProxy.stackEnvironmentVariables;
|
||||
export const pendingContainerUpdates = schemaProxy.pendingContainerUpdates;
|
||||
export const apiTokens = schemaProxy.apiTokens;
|
||||
export const backupDestinations = schemaProxy.backupDestinations;
|
||||
export const backupConfigs = schemaProxy.backupConfigs;
|
||||
|
||||
// Re-export types from SQLite schema (they're compatible with PostgreSQL)
|
||||
export type {
|
||||
@@ -984,7 +991,11 @@ export type {
|
||||
PendingContainerUpdate,
|
||||
NewPendingContainerUpdate,
|
||||
ApiToken,
|
||||
NewApiToken
|
||||
NewApiToken,
|
||||
BackupDestination,
|
||||
NewBackupDestination,
|
||||
BackupConfig,
|
||||
NewBackupConfig
|
||||
} from './schema/index.js';
|
||||
|
||||
export { eq, and, or, desc, asc, like, sql, inArray, isNull, isNotNull } from 'drizzle-orm';
|
||||
|
||||
@@ -444,7 +444,7 @@ export const scheduleExecutions = sqliteTable('schedule_executions', {
|
||||
completedAt: text('completed_at'),
|
||||
duration: integer('duration'), // milliseconds
|
||||
// Result
|
||||
status: text('status').notNull(), // 'queued' | 'running' | 'success' | 'failed' | 'skipped'
|
||||
status: text('status').notNull(), // 'queued' | 'running' | 'success' | 'warning' | 'failed' | 'skipped'
|
||||
errorMessage: text('error_message'),
|
||||
// Details
|
||||
details: text('details'), // JSON with execution details
|
||||
@@ -470,6 +470,50 @@ export const pendingContainerUpdates = sqliteTable('pending_container_updates',
|
||||
envContainerUnique: unique().on(table.environmentId, table.containerId)
|
||||
}));
|
||||
|
||||
// =============================================================================
|
||||
// BACKUP DESTINATIONS TABLE
|
||||
// =============================================================================
|
||||
|
||||
export const backupDestinations = sqliteTable('backup_destinations', {
|
||||
id: integer('id').primaryKey({ autoIncrement: true }),
|
||||
name: text('name').notNull().unique(),
|
||||
repository: text('repository').notNull(), // restic repo URL/path
|
||||
password: text('password').notNull(), // AES-256-GCM encrypted
|
||||
envVars: text('env_vars'), // JSON: { key: value } — values encrypted
|
||||
flags: text('flags'), // extra restic CLI flags
|
||||
hostPath: text('host_path'), // bind-mount source for local repos
|
||||
policies: text('policies'), // JSON: { pruneSchedule, checkSchedule, autoUnlock, maxUnused }
|
||||
lastTestAt: text('last_test_at'),
|
||||
lastTestStatus: text('last_test_status'), // 'success' | 'failed'
|
||||
lastTestError: text('last_test_error'),
|
||||
createdAt: text('created_at').default(sql`CURRENT_TIMESTAMP`),
|
||||
updatedAt: text('updated_at').default(sql`CURRENT_TIMESTAMP`)
|
||||
});
|
||||
|
||||
// =============================================================================
|
||||
// BACKUP CONFIGS TABLE
|
||||
// =============================================================================
|
||||
|
||||
export const backupConfigs = sqliteTable('backup_configs', {
|
||||
id: integer('id').primaryKey({ autoIncrement: true }),
|
||||
type: text('type').notNull().default('container'), // 'container' | 'stack'
|
||||
targetName: text('target_name').notNull(),
|
||||
environmentId: integer('environment_id').references(() => environments.id, { onDelete: 'cascade' }),
|
||||
destinationId: integer('destination_id').notNull().references(() => backupDestinations.id, { onDelete: 'cascade' }),
|
||||
enabled: integer('enabled', { mode: 'boolean' }).default(true),
|
||||
allVolumes: integer('all_volumes', { mode: 'boolean' }).default(true),
|
||||
selectedVolumes: text('selected_volumes'), // JSON array of volume names
|
||||
stopBeforeBackup: integer('stop_before_backup', { mode: 'boolean' }).default(false),
|
||||
schedule: text('schedule'), // cron expression
|
||||
retention: text('retention'), // JSON: { keepLast?, keepDaily?, keepWeekly?, keepMonthly?, keepYearly? }
|
||||
options: text('options'), // JSON: { compression?, limitUpload?, limitDownload?, excludePatterns?, webhookSuccess?, webhookFailure? }
|
||||
tags: text('tags'), // JSON array of strings
|
||||
lastBackupAt: text('last_backup_at'),
|
||||
lastBackupStatus: text('last_backup_status'), // 'success' | 'failed'
|
||||
createdAt: text('created_at').default(sql`CURRENT_TIMESTAMP`),
|
||||
updatedAt: text('updated_at').default(sql`CURRENT_TIMESTAMP`)
|
||||
});
|
||||
|
||||
// =============================================================================
|
||||
// API TOKENS TABLE
|
||||
// =============================================================================
|
||||
@@ -608,3 +652,9 @@ export type NewPendingContainerUpdate = typeof pendingContainerUpdates.$inferIns
|
||||
|
||||
export type ApiToken = typeof apiTokens.$inferSelect;
|
||||
export type NewApiToken = typeof apiTokens.$inferInsert;
|
||||
|
||||
export type BackupDestination = typeof backupDestinations.$inferSelect;
|
||||
export type NewBackupDestination = typeof backupDestinations.$inferInsert;
|
||||
|
||||
export type BackupConfig = typeof backupConfigs.$inferSelect;
|
||||
export type NewBackupConfig = typeof backupConfigs.$inferInsert;
|
||||
@@ -447,7 +447,7 @@ export const scheduleExecutions = pgTable('schedule_executions', {
|
||||
completedAt: timestamp('completed_at', { mode: 'string' }),
|
||||
duration: integer('duration'), // milliseconds
|
||||
// Result
|
||||
status: text('status').notNull(), // 'queued' | 'running' | 'success' | 'failed' | 'skipped'
|
||||
status: text('status').notNull(), // 'queued' | 'running' | 'success' | 'warning' | 'failed' | 'skipped'
|
||||
errorMessage: text('error_message'),
|
||||
// Details
|
||||
details: text('details'), // JSON with execution details
|
||||
@@ -473,6 +473,50 @@ export const pendingContainerUpdates = pgTable('pending_container_updates', {
|
||||
envContainerUnique: unique().on(table.environmentId, table.containerId)
|
||||
}));
|
||||
|
||||
// =============================================================================
|
||||
// BACKUP DESTINATIONS TABLE
|
||||
// =============================================================================
|
||||
|
||||
export const backupDestinations = pgTable('backup_destinations', {
|
||||
id: serial('id').primaryKey(),
|
||||
name: text('name').notNull().unique(),
|
||||
repository: text('repository').notNull(),
|
||||
password: text('password').notNull(),
|
||||
envVars: text('env_vars'),
|
||||
flags: text('flags'),
|
||||
hostPath: text('host_path'),
|
||||
policies: text('policies'),
|
||||
lastTestAt: timestamp('last_test_at', { mode: 'string' }),
|
||||
lastTestStatus: text('last_test_status'),
|
||||
lastTestError: text('last_test_error'),
|
||||
createdAt: timestamp('created_at', { mode: 'string' }).defaultNow(),
|
||||
updatedAt: timestamp('updated_at', { mode: 'string' }).defaultNow()
|
||||
});
|
||||
|
||||
// =============================================================================
|
||||
// BACKUP CONFIGS TABLE
|
||||
// =============================================================================
|
||||
|
||||
export const backupConfigs = pgTable('backup_configs', {
|
||||
id: serial('id').primaryKey(),
|
||||
type: text('type').notNull().default('container'),
|
||||
targetName: text('target_name').notNull(),
|
||||
environmentId: integer('environment_id').references(() => environments.id, { onDelete: 'cascade' }),
|
||||
destinationId: integer('destination_id').notNull().references(() => backupDestinations.id, { onDelete: 'cascade' }),
|
||||
enabled: boolean('enabled').default(true),
|
||||
allVolumes: boolean('all_volumes').default(true),
|
||||
selectedVolumes: text('selected_volumes'),
|
||||
stopBeforeBackup: boolean('stop_before_backup').default(false),
|
||||
schedule: text('schedule'),
|
||||
retention: text('retention'),
|
||||
options: text('options'),
|
||||
tags: text('tags'),
|
||||
lastBackupAt: timestamp('last_backup_at', { mode: 'string' }),
|
||||
lastBackupStatus: text('last_backup_status'),
|
||||
createdAt: timestamp('created_at', { mode: 'string' }).defaultNow(),
|
||||
updatedAt: timestamp('updated_at', { mode: 'string' }).defaultNow()
|
||||
});
|
||||
|
||||
// =============================================================================
|
||||
// API TOKENS TABLE
|
||||
// =============================================================================
|
||||
|
||||
Reference in new issue
Block a user