Initial
This commit is contained in:
185
server/models/hostGroupModel.js
Normal file
185
server/models/hostGroupModel.js
Normal file
@@ -0,0 +1,185 @@
|
||||
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);
|
||||
}
|
||||
Reference in New Issue
Block a user