Files
poshmanager/server/models/hostGroupModel.js
2026-06-25 14:43:13 -05:00

186 lines
6.9 KiB
JavaScript

import { db, id, json, now, parseJson, visibleClause } from '../db.js';
import { hostGroupSchema } from '../forms/schemas.js';
import { camelHostGroup } from './mappers.js';
import { filterVisibleHostIds } from './hostModel.js';
function scopedGroupId(payload) {
return payload.visibility === 'group' ? payload.groupId || null : null;
}
function normalizeHostGroup(body) {
const parsed = hostGroupSchema.parse(body || {});
return {
...parsed,
groupId: scopedGroupId(parsed),
rules: parsed.mode === 'dynamic' ? parsed.rules : [],
hostIds: parsed.mode === 'manual' ? parsed.hostIds : []
};
}
export function listHostGroups(userId) {
return db.prepare(`
SELECT hg.*, g.name AS group_name,
COALESCE((SELECT COUNT(*) FROM host_group_members hgm WHERE hgm.host_group_id = hg.id), 0) AS member_count,
COALESCE((SELECT json_group_array(host_id) FROM host_group_members hgm WHERE hgm.host_group_id = hg.id), '[]') AS host_ids
FROM host_groups hg
LEFT JOIN groups g ON g.id = hg.group_id
WHERE ${visibleClause('hg')}
ORDER BY hg.name
`).all(userId, userId).map(camelHostGroup);
}
export function getVisibleHostGroup(groupId, userId) {
const row = db.prepare(`
SELECT hg.*, g.name AS group_name,
COALESCE((SELECT COUNT(*) FROM host_group_members hgm WHERE hgm.host_group_id = hg.id), 0) AS member_count,
COALESCE((SELECT json_group_array(host_id) FROM host_group_members hgm WHERE hgm.host_group_id = hg.id), '[]') AS host_ids
FROM host_groups hg
LEFT JOIN groups g ON g.id = hg.group_id
WHERE hg.id = ? AND ${visibleClause('hg')}
`).get(groupId, userId, userId);
return row ? camelHostGroup(row) : null;
}
export function createHostGroup(body, userId) {
const payload = normalizeHostGroup(body);
if (payload.mode === 'dynamic' && !payload.rules.length) throw new Error('Dynamic host groups require at least one rule.');
const groupId = id('hgrp');
db.prepare(`
INSERT INTO host_groups (
id, name, description, mode, match_mode, rules, visibility, owner_user_id,
group_id, created_by, created_at, updated_at
)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`).run(
groupId,
payload.name,
payload.description,
payload.mode,
payload.matchMode,
json(payload.rules),
payload.visibility,
userId,
payload.groupId,
userId,
now(),
now()
);
if (payload.mode === 'manual') replaceHostGroupMembers(groupId, filterVisibleHostIds(payload.hostIds, userId), 'manual');
else syncHostGroup(groupId);
return getVisibleHostGroup(groupId, userId);
}
export function updateHostGroup(groupId, body, userId) {
const existing = db.prepare(`SELECT * FROM host_groups WHERE id = ? AND ${visibleClause()}`).get(groupId, userId, userId);
if (!existing) return null;
const payload = normalizeHostGroup({
name: existing.name,
description: existing.description || '',
mode: existing.mode,
matchMode: existing.match_mode,
rules: parseJson(existing.rules, []),
visibility: existing.visibility,
groupId: existing.group_id,
...body
});
if (payload.mode === 'dynamic' && !payload.rules.length) throw new Error('Dynamic host groups require at least one rule.');
db.prepare(`
UPDATE host_groups
SET name = ?, description = ?, mode = ?, match_mode = ?, rules = ?,
visibility = ?, group_id = ?, updated_at = ?
WHERE id = ?
`).run(
payload.name,
payload.description,
payload.mode,
payload.matchMode,
json(payload.rules),
payload.visibility,
payload.groupId,
now(),
groupId
);
if (payload.mode === 'manual') replaceHostGroupMembers(groupId, filterVisibleHostIds(payload.hostIds, userId), 'manual');
else syncHostGroup(groupId);
return getVisibleHostGroup(groupId, userId);
}
export function deleteHostGroup(groupId, userId) {
db.prepare(`DELETE FROM host_groups WHERE id = ? AND ${visibleClause()}`).run(groupId, userId, userId);
}
export function filterVisibleHostGroupIds(groupIds = [], userId) {
if (!groupIds.length) return [];
const visible = new Set(
db.prepare(`SELECT id FROM host_groups WHERE ${visibleClause()}`).all(userId, userId).map((row) => row.id)
);
return groupIds.filter((groupId) => visible.has(groupId));
}
export function syncHostGroup(groupId) {
const group = db.prepare('SELECT * FROM host_groups WHERE id = ?').get(groupId);
if (!group || group.mode !== 'dynamic') return { groupId, matched: 0, skipped: true };
const ownerId = group.owner_user_id || group.created_by;
const hosts = ownerId
? db.prepare(`SELECT * FROM hosts WHERE ${visibleClause()} ORDER BY name`).all(ownerId, ownerId)
: db.prepare("SELECT * FROM hosts WHERE visibility = 'shared' ORDER BY name").all();
const rules = parseJson(group.rules, []);
const matched = hosts.filter((host) => hostMatchesRules(host, rules, group.match_mode)).map((host) => host.id);
replaceHostGroupMembers(groupId, matched, 'rule');
db.prepare('UPDATE host_groups SET last_synced_at = ?, updated_at = ? WHERE id = ?').run(now(), now(), groupId);
return { groupId, matched: matched.length, skipped: false };
}
export function syncDynamicHostGroups() {
const groups = db.prepare("SELECT id FROM host_groups WHERE mode = 'dynamic'").all();
const summary = { evaluated: 0, synced: 0, matched: 0, errors: 0 };
for (const group of groups) {
summary.evaluated += 1;
try {
const result = syncHostGroup(group.id);
if (!result.skipped) {
summary.synced += 1;
summary.matched += result.matched;
}
} catch {
summary.errors += 1;
}
}
return summary;
}
function replaceHostGroupMembers(groupId, hostIds, source) {
db.prepare('DELETE FROM host_group_members WHERE host_group_id = ?').run(groupId);
const stmt = db.prepare(`
INSERT OR IGNORE INTO host_group_members (host_group_id, host_id, source, created_at)
VALUES (?, ?, ?, ?)
`);
for (const hostId of [...new Set(hostIds)]) stmt.run(groupId, hostId, source, now());
}
export function hostMatchesRules(host, rules = [], matchMode = 'all') {
if (!rules.length) return false;
const results = rules.map((rule) => hostMatchesRule(host, rule));
return matchMode === 'any' ? results.some(Boolean) : results.every(Boolean);
}
function hostMatchesRule(host, rule) {
const values = hostRuleValues(host, rule.field);
const needle = String(rule.value || '').toLowerCase();
return values.some((value) => compareRuleValue(String(value || '').toLowerCase(), needle, rule.operator));
}
function hostRuleValues(host, field) {
if (field === 'tags') return parseJson(host.tags, []);
if (field === 'sourceType') return [host.source_type || 'manual'];
if (field === 'osFamily') return [host.os_family || ''];
return [host[field] || ''];
}
function compareRuleValue(value, needle, operator = 'contains') {
if (operator === 'equals') return value === needle;
if (operator === 'startsWith') return value.startsWith(needle);
if (operator === 'endsWith') return value.endsWith(needle);
return value.includes(needle);
}