- Add ComplianceStatus.PENDING and configVersion to DeviceComplianceInfo - Add ComplianceReport + ComplianceDeviceResult types to ConfigurationEngine - Expose evaluateSpecific() as public (used by compliance + dry-run) - Add complianceReport() method: filter matching devices, evaluate each - Fix matchesCriteria: skip parameterCriteria entries with null/empty name - Fix getDeviceParameterValue: fallback to plain object arrays (DeviceRecord) - Fix findDevice: support lookup by both identifier and deviceId.id - ConfigurationService.update: version is now auto-managed (no user input) - behavioral changes (criteria/params/policy/priority/…) → patch bump - bumpType field allows 'patch' | 'minor' | 'major' - cosmetic changes (name/tags) → no bump - Add sanitizeCriteria: filter null/empty parameterCriteria names silently - Add isBehavioralChange helper - Add DeviceModel.resetComplianceForConfiguration call on behavioral update - Add ConfigurationModel.distinctTags() + controller endpoint GET /tags - Add controller endpoint GET /configurations/:id/compliance - Add DeviceModel.getDistinctValues for inventory fields - Fix unit tests: mock DeviceModel, remove obsolete version-validation test - Add 14 new unit tests (engine: 5, service: 9) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
177 lines
6.1 KiB
TypeScript
177 lines
6.1 KiB
TypeScript
import { BaseModel } from '#database/base_model'
|
|
import { DeviceRecord } from '../domain/device/device_record.js'
|
|
import { DeviceId } from '../domain/device/device_id.js'
|
|
import { DeviceComplianceInfo } from '../domain/configuration/compliance.js'
|
|
import { Filter, Sort } from 'mongodb'
|
|
|
|
export default class DeviceModel extends BaseModel {
|
|
static collectionName = 'devices'
|
|
|
|
static async findByIdentifier(identifier: string): Promise<DeviceRecord | null> {
|
|
return this.findOne({ identifier })
|
|
}
|
|
|
|
static async findByDeviceId(deviceId: DeviceId): Promise<DeviceRecord | null> {
|
|
return this.findOne({
|
|
'deviceId.manufacturer': deviceId.manufacturer,
|
|
'deviceId.oui': deviceId.oui,
|
|
'deviceId.productClass': deviceId.productClass,
|
|
'deviceId.serialNumber': deviceId.serialNumber,
|
|
})
|
|
}
|
|
|
|
static async paginateDevices(
|
|
offset: number,
|
|
limit: number,
|
|
filter: Filter<any> = {}
|
|
): Promise<{ data: DeviceRecord[]; total: number }> {
|
|
const collection = this.getCollection()
|
|
const total = await collection.countDocuments(filter)
|
|
const data = (await collection
|
|
.find(filter, { projection: { _id: 0 } })
|
|
.skip(offset)
|
|
.limit(limit)
|
|
.sort({ updatedAt: -1 } as Sort)
|
|
.toArray()) as unknown as DeviceRecord[]
|
|
return { data, total }
|
|
}
|
|
|
|
static async createOrUpdate(device: Partial<DeviceRecord>): Promise<DeviceRecord> {
|
|
const collection = this.getCollection()
|
|
let filter: Filter<any> = {}
|
|
|
|
if (device.deviceId) {
|
|
filter = { deviceId: device.deviceId }
|
|
} else if (device.identifier) {
|
|
filter = { identifier: device.identifier }
|
|
} else {
|
|
throw new Error('Either identifier or deviceId must be provided')
|
|
}
|
|
|
|
const result = await collection.findOneAndUpdate(
|
|
filter,
|
|
{
|
|
$set: { ...device, updatedAt: new Date() },
|
|
$setOnInsert: { createdAt: new Date() },
|
|
},
|
|
{ upsert: true, returnDocument: 'after', projection: { _id: 0 } }
|
|
)
|
|
|
|
return result as unknown as DeviceRecord
|
|
}
|
|
|
|
static async updateCompliance(
|
|
identifier: string,
|
|
compliance: DeviceComplianceInfo
|
|
): Promise<void> {
|
|
await this.updateOne({ identifier }, { compliance })
|
|
}
|
|
|
|
static async updateProvisioningCode(
|
|
identifier: string,
|
|
provisioningCode: string
|
|
): Promise<void> {
|
|
await this.updateOne({ identifier }, { provisioningCode })
|
|
}
|
|
|
|
static async findByComplianceStatus(status: string): Promise<DeviceRecord[]> {
|
|
const collection = this.getCollection()
|
|
return collection
|
|
.find({ 'compliance.status': status })
|
|
.toArray() as unknown as Promise<DeviceRecord[]>
|
|
}
|
|
|
|
static async findByConfigurationIdentifier(configurationIdentifier: string): Promise<DeviceRecord[]> {
|
|
const collection = this.getCollection()
|
|
return collection
|
|
.find({ 'compliance.configurationIdentifier': configurationIdentifier })
|
|
.toArray() as unknown as Promise<DeviceRecord[]>
|
|
}
|
|
|
|
static async distinctCriteriaValues(): Promise<{
|
|
manufacturer: string[]
|
|
oui: string[]
|
|
productClass: string[]
|
|
serialNumber: string[]
|
|
modelName: string[]
|
|
hardwareVersion: string[]
|
|
softwareVersion: string[]
|
|
parameterNames: string[]
|
|
}> {
|
|
const collection = this.getCollection()
|
|
|
|
const distinctProperty = async (paths: string[]): Promise<string[]> => {
|
|
const rows = await collection
|
|
.aggregate([
|
|
{ $unwind: '$properties' },
|
|
{ $match: { 'properties.name': { $in: paths } } },
|
|
{ $group: { _id: '$properties.value' } },
|
|
])
|
|
.toArray()
|
|
return rows.map((r) => String(r._id)).filter(Boolean).sort()
|
|
}
|
|
|
|
const [manufacturer, oui, productClass, serialNumber, modelName, hardwareVersion, softwareVersion, parameterNames] =
|
|
await Promise.all([
|
|
collection.distinct('deviceId.manufacturer'),
|
|
collection.distinct('deviceId.oui'),
|
|
collection.distinct('deviceId.productClass'),
|
|
collection.distinct('deviceId.serialNumber'),
|
|
distinctProperty(['Device.DeviceInfo.ModelName', 'InternetGatewayDevice.DeviceInfo.ModelName']),
|
|
distinctProperty(['Device.DeviceInfo.HardwareVersion', 'InternetGatewayDevice.DeviceInfo.HardwareVersion']),
|
|
distinctProperty(['Device.DeviceInfo.SoftwareVersion', 'InternetGatewayDevice.DeviceInfo.SoftwareVersion']),
|
|
collection.distinct('properties.name'),
|
|
])
|
|
|
|
return {
|
|
manufacturer: (manufacturer as string[]).filter(Boolean).sort(),
|
|
oui: (oui as string[]).filter(Boolean).sort(),
|
|
productClass: (productClass as string[]).filter(Boolean).sort(),
|
|
serialNumber: (serialNumber as string[]).filter(Boolean).sort(),
|
|
modelName,
|
|
hardwareVersion,
|
|
softwareVersion,
|
|
parameterNames: (parameterNames as string[]).filter(Boolean).sort(),
|
|
}
|
|
}
|
|
|
|
static async countByComplianceStatus(
|
|
configurationIdentifier: string
|
|
): Promise<{ compliant: number; in_progress: number; failed: number; pending: number }> {
|
|
const collection = this.getCollection()
|
|
const results = await collection
|
|
.aggregate([
|
|
{ $match: { 'compliance.configurationIdentifier': configurationIdentifier } },
|
|
{ $group: { _id: '$compliance.status', count: { $sum: 1 } } },
|
|
])
|
|
.toArray()
|
|
|
|
const counts = { compliant: 0, in_progress: 0, failed: 0, pending: 0 }
|
|
for (const r of results) {
|
|
if (r._id === 'compliant') counts.compliant = r.count
|
|
else if (r._id === 'in_progress') counts.in_progress = r.count
|
|
else if (r._id === 'failed') counts.failed = r.count
|
|
else if (r._id === 'pending') counts.pending = r.count
|
|
}
|
|
return counts
|
|
}
|
|
|
|
/**
|
|
* Reset compliance status to 'pending' for all devices linked to a configuration.
|
|
* Called after a behavioral configuration update to force re-evaluation on next Inform.
|
|
*/
|
|
static async resetComplianceForConfiguration(configurationIdentifier: string): Promise<void> {
|
|
const collection = this.getCollection()
|
|
await collection.updateMany(
|
|
{ 'compliance.configurationIdentifier': configurationIdentifier },
|
|
{
|
|
$set: {
|
|
'compliance.status': 'pending',
|
|
'compliance.lastError': null,
|
|
updatedAt: new Date(),
|
|
},
|
|
}
|
|
)
|
|
}
|
|
}
|