feat: rebuild warm operations workbench
This commit is contained in:
@@ -0,0 +1,91 @@
|
||||
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),
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user