Files
adam-acs/app/models/device.ts
T
2026-06-01 15:49:09 +02:00

179 lines
6.3 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()
// Only return documents created by the ACS (must have an identifier field)
const safeFilter: Filter<any> = { ...filter, identifier: { $exists: true, $ne: null } }
const total = await collection.countDocuments(safeFilter)
const data = (await collection
.find(safeFilter, { 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(),
},
}
)
}
}