import { desc, sql } from 'drizzle-orm'; import { type AnySQLiteColumn, check, index, integer, primaryKey, sqliteTable, text, unique, uniqueIndex, } from 'drizzle-orm/sqlite-core'; const timestamps = { createdAt: text('created_at').notNull(), updatedAt: text('updated_at').notNull(), }; export const instances = sqliteTable( 'instances', { id: text('id').primaryKey(), name: text('name').notNull(), baseUrl: text('base_url').notNull(), authMode: text('auth_mode').notNull().default('password'), enabled: integer('enabled', { mode: 'boolean' }).notNull().default(true), configRevision: integer('config_revision').notNull().default(1), ...timestamps, }, (table) => [ unique('instances_base_url_unique').on(table.baseUrl), check('instances_enabled_check', sql`${table.enabled} in (0, 1)`), check('instances_config_revision_check', sql`${table.configRevision} > 0`), ], ); export const instanceTags = sqliteTable( 'instance_tags', { instanceId: text('instance_id') .notNull() .references(() => instances.id, { onDelete: 'cascade' }), tag: text('tag').notNull(), createdAt: text('created_at').notNull(), }, (table) => [primaryKey({ columns: [table.instanceId, table.tag] })], ); export const capabilities = sqliteTable( 'capabilities', { instanceId: text('instance_id') .notNull() .references(() => instances.id, { onDelete: 'cascade' }), operationId: text('operation_id').notNull(), state: text('state').notNull(), upstreamVersion: text('upstream_version'), detailCode: text('detail_code'), observedAt: text('observed_at').notNull(), ...timestamps, }, (table) => [ primaryKey({ columns: [table.instanceId, table.operationId] }), check( 'capabilities_state_check', sql`${table.state} in ('supported', 'unsupported', 'auth-required', 'degraded', 'unknown')`, ), index('idx_capabilities_state').on(table.state), ], ); export const statusSnapshots = sqliteTable( 'status_snapshots', { id: text('id').primaryKey(), instanceId: text('instance_id') .notNull() .references(() => instances.id, { onDelete: 'cascade' }), category: text('category').notNull(), state: text('state').notNull(), payloadJson: text('payload_json').notNull(), observedAt: text('observed_at').notNull(), expiresAt: text('expires_at'), createdAt: text('created_at').notNull(), }, (table) => [ check( 'status_snapshots_state_check', sql`${table.state} in ('fresh', 'stale', 'expired', 'unknown')`, ), index('idx_status_snapshots_instance_observed_at').on(table.instanceId, desc(table.observedAt)), uniqueIndex('uq_status_snapshots_instance_category').on(table.instanceId, table.category), ], ); export const jobs = sqliteTable( 'jobs', { id: text('id').primaryKey(), parentJobId: text('parent_job_id').references((): AnySQLiteColumn => jobs.id, { onDelete: 'restrict', }), rootJobId: text('root_job_id').references((): AnySQLiteColumn => jobs.id, { onDelete: 'restrict', }), retryOfJobId: text('retry_of_job_id').references((): AnySQLiteColumn => jobs.id, { onDelete: 'restrict', }), operationId: text('operation_id').notNull(), riskLevel: text('risk_level').notNull(), status: text('status').notNull(), requestedBy: text('requested_by').notNull(), requestId: text('request_id').notNull(), parametersDigest: text('parameters_digest').notNull(), createdAt: text('created_at').notNull(), startedAt: text('started_at'), finishedAt: text('finished_at'), updatedAt: text('updated_at').notNull(), }, (table) => [ check('jobs_risk_level_check', sql`${table.riskLevel} in ('R0', 'R1', 'R2', 'R3')`), index('idx_jobs_status_created_at').on(table.status, desc(table.createdAt)), ], ); export const jobItems = sqliteTable( 'job_items', { id: text('id').primaryKey(), jobId: text('job_id') .notNull() .references(() => jobs.id, { onDelete: 'cascade' }), instanceId: text('instance_id').notNull(), attemptNumber: integer('attempt_number').notNull().default(1), status: text('status').notNull(), resultCode: text('result_code'), errorJson: text('error_json'), sourceJobItemId: text('source_job_item_id').references((): AnySQLiteColumn => jobItems.id, { onDelete: 'restrict', }), createdAt: text('created_at').notNull(), startedAt: text('started_at'), finishedAt: text('finished_at'), updatedAt: text('updated_at').notNull(), }, (table) => [ unique('job_items_job_instance_attempt_unique').on( table.jobId, table.instanceId, table.attemptNumber, ), check('job_items_attempt_number_check', sql`${table.attemptNumber} > 0`), index('idx_job_items_job_status').on(table.jobId, table.status), ], ); export const jobAttempts = sqliteTable( 'job_attempts', { id: text('id').primaryKey(), jobId: text('job_id') .notNull() .references(() => jobs.id, { onDelete: 'cascade' }), status: text('status').notNull(), startedAt: text('started_at').notNull(), finishedAt: text('finished_at'), createdAt: text('created_at').notNull(), }, (table) => [index('idx_job_attempts_job_started_at').on(table.jobId, table.startedAt)], ); export const operationPreparations = sqliteTable( 'operation_preparations', { id: text('id').primaryKey(), operationId: text('operation_id').notNull(), riskLevel: text('risk_level').notNull(), status: text('status').notNull(), targetInstanceId: text('target_instance_id').notNull(), targetRevision: integer('target_revision').notNull(), parameterSchemaId: text('parameter_schema_id').notNull(), parametersDigest: text('parameters_digest').notNull(), tokenDigest: text('token_digest').notNull(), requestedBy: text('requested_by').notNull(), requestId: text('request_id').notNull(), expiresAt: text('expires_at').notNull(), consumedAt: text('consumed_at'), ...timestamps, }, (table) => [ index('idx_operation_preparations_status_expires_at').on(table.status, table.expiresAt), ], ); export const auditEvents = sqliteTable( 'audit_events', { id: text('id').primaryKey(), instanceId: text('instance_id').references(() => instances.id, { onDelete: 'set null' }), jobId: text('job_id').references(() => jobs.id, { onDelete: 'set null' }), actor: text('actor').notNull(), operationId: text('operation_id').notNull(), riskLevel: text('risk_level').notNull(), requestId: text('request_id').notNull(), parametersSummaryJson: text('parameters_summary_json').notNull(), bodyDigest: text('body_digest'), resultCode: text('result_code').notNull(), durationMs: integer('duration_ms').notNull(), createdAt: text('created_at').notNull(), }, (table) => [ check('audit_events_risk_level_check', sql`${table.riskLevel} in ('R0', 'R1', 'R2', 'R3')`), check('audit_events_duration_ms_check', sql`${table.durationMs} >= 0`), index('idx_audit_events_created_at').on(desc(table.createdAt)), ], ); export const appSettings = sqliteTable('app_settings', { key: text('key').primaryKey(), valueJson: text('value_json').notNull(), ...timestamps, }); export const secretReferences = sqliteTable( 'secret_references', { id: text('id').primaryKey(), instanceId: text('instance_id').references(() => instances.id, { onDelete: 'cascade' }), purpose: text('purpose').notNull(), provider: text('provider').notNull(), externalReference: text('external_reference').notNull(), ...timestamps, }, (table) => [ unique('secret_references_provider_external_unique').on( table.provider, table.externalReference, ), uniqueIndex('uq_secret_references_instance_purpose') .on(table.instanceId, table.purpose) .where(sql`${table.instanceId} is not null`), ], ); export const secretCleanupTasks = sqliteTable( 'secret_cleanup_tasks', { reference: text('reference').primaryKey(), instanceId: text('instance_id').notNull(), purpose: text('purpose').notNull(), provider: text('provider').notNull(), generation: integer('generation').notNull().default(1), queuedAt: text('queued_at').notNull(), updatedAt: text('updated_at').notNull(), }, (table) => [index('idx_secret_cleanup_tasks_queued_at').on(table.queuedAt)], ); export const scheduledTasks = sqliteTable( 'scheduled_tasks', { id: text('id').primaryKey(), name: text('name').notNull(), operationType: text('operation_type').notNull(), enabled: integer('enabled', { mode: 'boolean' }).notNull().default(true), version: integer('version').notNull().default(1), cronExpression: text('cron_expression').notNull(), timezone: text('timezone').notNull(), targetSelectorJson: text('target_selector_json').notNull(), smsSecretReference: text('sms_secret_reference'), smsRecipientCount: integer('sms_recipient_count'), effectiveStartAt: text('effective_start_at'), effectiveEndAt: text('effective_end_at'), misfirePolicy: text('misfire_policy').notNull(), overlapPolicy: text('overlap_policy').notNull(), retryPolicyJson: text('retry_policy_json').notNull(), nextDueAt: text('next_due_at'), lastEvaluatedAt: text('last_evaluated_at'), createdBy: text('created_by').notNull(), updatedBy: text('updated_by').notNull(), createdAt: text('created_at').notNull(), updatedAt: text('updated_at').notNull(), deletedAt: text('deleted_at'), }, (table) => [ check('scheduled_tasks_enabled_check', sql`${table.enabled} in (0, 1)`), check('scheduled_tasks_version_check', sql`${table.version} > 0`), index('idx_scheduled_tasks_enabled_next_due').on(table.enabled, table.nextDueAt), ], ); export const scheduledRuns = sqliteTable( 'scheduled_runs', { id: text('id').primaryKey(), scheduledTaskId: text('scheduled_task_id') .notNull() .references(() => scheduledTasks.id, { onDelete: 'restrict' }), scheduleVersion: integer('schedule_version').notNull(), taskSnapshotJson: text('task_snapshot_json').notNull(), dueAt: text('due_at').notNull(), claimedAt: text('claimed_at').notNull(), startedAt: text('started_at'), finishedAt: text('finished_at'), targetSnapshotJson: text('target_snapshot_json').notNull(), outcome: text('outcome'), reason: text('reason'), jobIdsJson: text('job_ids_json').notNull().default('[]'), triggerSource: text('trigger_source').notNull(), attempt: integer('attempt').notNull().default(1), }, (table) => [ unique('scheduled_runs_occurrence_unique').on( table.scheduledTaskId, table.scheduleVersion, table.dueAt, table.triggerSource, ), index('idx_scheduled_runs_task_due').on(table.scheduledTaskId, desc(table.dueAt)), ], );