import type { ScheduledOperationType, ScheduleTargetSelector } from '@multi-simadmin/contracts'; import type { SqliteDatabase } from '../../infrastructure/database/database.js'; export interface ResolvedTarget { readonly id: string; readonly revision: number; } interface TargetRow { id: string; config_revision: number; } export interface OperationTargetResolution { readonly targets: readonly ResolvedTarget[]; readonly targetSnapshot: readonly string[]; readonly unavailableInstanceIds: readonly string[]; } const operationIds: Record = { 'restart-service': 'postServiceRestart', 'reboot-system': 'postSystemReboot', 'send-sms': 'postSmsSend', }; export function resolveTargets( db: SqliteDatabase, selector: ScheduleTargetSelector, ): readonly ResolvedTarget[] { if (selector.mode === 'fixed') { const placeholders = selector.instanceIds.map(() => '?').join(','); const rows = db .prepare( `SELECT id, config_revision FROM instances WHERE enabled = 1 AND id IN (${placeholders})`, ) .all(...selector.instanceIds) as TargetRow[]; const byId = new Map(rows.map((row) => [row.id, row])); return selector.instanceIds.flatMap((id) => { const row = byId.get(id); return row ? [{ id: row.id, revision: row.config_revision }] : []; }); } const placeholders = selector.tags.map(() => '?').join(','); const comparison = selector.match === 'all' ? '= ?' : '> 0'; const parameters: Array = [...selector.tags]; if (selector.match === 'all') parameters.push(selector.tags.length); const rows = db .prepare( `SELECT i.id, i.config_revision FROM instances i JOIN instance_tags t ON t.instance_id = i.id WHERE i.enabled = 1 AND t.tag IN (${placeholders}) GROUP BY i.id, i.config_revision HAVING COUNT(DISTINCT t.tag) ${comparison} ORDER BY i.id ASC`, ) .all(...parameters) as TargetRow[]; return rows.map((row) => ({ id: row.id, revision: row.config_revision })); } export function resolveOperationTargets( db: SqliteDatabase, selector: ScheduleTargetSelector, operationType: ScheduledOperationType, ): OperationTargetResolution { const candidates = resolveTargets(db, selector); if (candidates.length === 0) return { targets: [], targetSnapshot: [], unavailableInstanceIds: [] }; const placeholders = candidates.map(() => '?').join(','); const rows = db .prepare( `SELECT instance_id, state FROM capabilities WHERE operation_id = ? AND instance_id IN (${placeholders})`, ) .all(operationIds[operationType], ...candidates.map((target) => target.id)) as Array<{ instance_id: string; state: string; }>; const states = new Map(rows.map((row) => [row.instance_id, row.state])); const availableStates = new Set(['supported', 'auth-required', 'degraded']); return { targets: candidates.filter((target) => availableStates.has(states.get(target.id) ?? 'unknown')), targetSnapshot: candidates.map((target) => target.id), unavailableInstanceIds: candidates .filter((target) => !availableStates.has(states.get(target.id) ?? 'unknown')) .map((target) => target.id), }; }