Files
multi-simadmin/apps/api/src/application/automation/target-resolver.ts
T

92 lines
3.2 KiB
TypeScript

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<ScheduledOperationType, string> = {
'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<string | number> = [...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),
};
}