Files
adam-acs/app/services/firmware_catalog_service.ts
T
Kevin L'Hereec f9e56c78b1 refactor: migrate all internal imports to package.json subpath aliases
- Add #domain, #protocol, #utils aliases to package.json imports map
- Replace all relative imports (./, ../, ../..) in app/ and
  database/mongo_migrations/ with the corresponding #alias imports,
  eliminating relative path traversal across the codebase
- Remove dead code: ConfigurationVersionModel.findByConfigurationIdentifier
  (unused, superseded by paginateVersions) and unused exception classes
  (NotFoundException, UnprocessableEntityException, DatabaseException)
  from app/exceptions/index.ts, keeping only BadRequestException
- Fix pre-existing syntax error in configuration_version.ts (mismatched
  parenthesis) that broke tsc
- Fix eqeqeq violation in configuration_service.ts and isNaN ->
  Number.isNaN in configuration_engine.ts / cwmp-1-0.ts / exchange_controller.ts
- Fix no-shadow violation in cwmp-1-0.ts EventStruct constructor
- Prettier formatting pass on touched files

Validated: tsc --noEmit clean, eslint app/ clean, full test suite green.
2026-07-22 11:45:14 +02:00

229 lines
6.8 KiB
TypeScript

import FirmwareEntryModel from '#models/firmware_entry'
import MigrationRouteModel from '#models/migration_route'
import { type FirmwareEntry, type MigrationRoute } from '#domain/firmware/firmware_catalog'
import { type MigrationStep } from '#domain/firmware/migration_step'
import { matchGlob } from '#utils/glob_match'
import { BadRequestException } from '#exceptions/index'
export interface CreateFirmwareEntryDto {
identifier: string
productClass: string
version: string
downloadUrl: string
checksum: string
checksumType: 'md5' | 'sha256'
fileSize: number
}
export interface CreateMigrationRouteDto {
identifier: string
productClass: string
sourceVersionPattern: string
targetVersion: string
downloadUrl: string
priority: number
}
export default class FirmwareCatalogService {
/**
* Validate that a string is a valid URL.
*/
private validateUrl(url: string, fieldName: string): void {
try {
new URL(url)
} catch {
throw new BadRequestException(`${fieldName} must be a valid URL`)
}
}
/**
* Validate that a number is a positive integer (> 0).
*/
private validatePositiveInteger(value: number, fieldName: string): void {
if (!Number.isInteger(value) || value <= 0) {
throw new BadRequestException(`${fieldName} must be a positive integer`)
}
}
/**
* Validate that a string is non-empty.
*/
private validateNonEmptyString(value: string, fieldName: string): void {
if (!value || value.trim().length === 0) {
throw new BadRequestException(`${fieldName} must be a non-empty string`)
}
}
/**
* Validate checksumType is 'md5' or 'sha256'.
*/
private validateChecksumType(value: string, fieldName: string): void {
if (value !== 'md5' && value !== 'sha256') {
throw new BadRequestException(`${fieldName} must be 'md5' or 'sha256'`)
}
}
/**
* Create a new firmware entry after validation.
*/
async createEntry(data: CreateFirmwareEntryDto): Promise<FirmwareEntry> {
this.validateNonEmptyString(data.identifier, 'identifier')
this.validateUrl(data.downloadUrl, 'downloadUrl')
this.validatePositiveInteger(data.fileSize, 'fileSize')
this.validateChecksumType(data.checksumType, 'checksumType')
return FirmwareEntryModel.createIfNotExists({
identifier: data.identifier,
productClass: data.productClass,
version: data.version,
downloadUrl: data.downloadUrl,
checksum: data.checksum,
checksumType: data.checksumType,
fileSize: data.fileSize,
})
}
/**
* Find firmware entries, optionally filtered by productClass.
*/
async findEntries(productClass?: string): Promise<FirmwareEntry[]> {
if (productClass) {
return FirmwareEntryModel.findByProductClass(productClass)
}
return FirmwareEntryModel.find({}) as unknown as Promise<FirmwareEntry[]>
}
/**
* Delete a firmware entry by identifier.
*/
async deleteEntry(identifier: string): Promise<void> {
const deleted = await FirmwareEntryModel.deleteByIdentifier(identifier)
if (!deleted) {
throw new BadRequestException(`Firmware entry '${identifier}' not found`)
}
}
/**
* Create a new migration route after validation.
*/
async createMigrationRoute(data: CreateMigrationRouteDto): Promise<MigrationRoute> {
this.validateNonEmptyString(data.identifier, 'identifier')
this.validateUrl(data.downloadUrl, 'downloadUrl')
this.validatePositiveInteger(data.priority, 'priority')
return MigrationRouteModel.createIfNotExists({
identifier: data.identifier,
productClass: data.productClass,
sourceVersionPattern: data.sourceVersionPattern,
targetVersion: data.targetVersion,
downloadUrl: data.downloadUrl,
priority: data.priority,
})
}
async findAllRoutes(): Promise<MigrationRoute[]> {
return MigrationRouteModel.find({}) as unknown as Promise<MigrationRoute[]>
}
async updateEntry(
identifier: string,
data: Partial<CreateFirmwareEntryDto>
): Promise<FirmwareEntry> {
const collection = FirmwareEntryModel.getCollection()
const result = await collection.findOneAndUpdate(
{ identifier },
{ $set: data },
{ returnDocument: 'after' }
)
if (!result) throw new BadRequestException(`Firmware entry '${identifier}' not found`)
return result as unknown as FirmwareEntry
}
/**
* Find migration routes for a productClass, sorted by priority descending.
*/
async findMigrationRoutes(productClass: string): Promise<MigrationRoute[]> {
return MigrationRouteModel.findByProductClass(productClass, { priority: -1 })
}
/**
* Delete a migration route by identifier.
*/
async deleteMigrationRoute(identifier: string): Promise<void> {
const deleted = await MigrationRouteModel.deleteByIdentifier(identifier)
if (!deleted) {
throw new BadRequestException(`Migration route '${identifier}' not found`)
}
}
/**
* Resolve the firmware upgrade path from currentVersion to targetVersion.
* Uses a greedy best-first algorithm based on route priority with cycle detection.
*
* @returns MigrationStep[] ordered from first to last, or [] if no path exists
*/
async resolveUpgradePath(
productClass: string,
currentVersion: string,
targetVersion: string
): Promise<MigrationStep[]> {
// Trivial case: already at target version
if (currentVersion === targetVersion) return []
// Load all routes for this productClass
const routes = await MigrationRouteModel.findByProductClass(productClass)
const path: MigrationStep[] = []
const visited = new Set<string>()
let version = currentVersion
visited.add(version)
while (version !== targetVersion) {
// Find all applicable routes from the current version
const applicableRoutes = routes
.filter((r) => matchGlob(r.sourceVersionPattern, version))
.sort((a, b) => b.priority - a.priority) // Highest priority first
// Take the best route (highest priority)
const bestRoute = applicableRoutes[0]
if (!bestRoute) {
// No route found → no path exists
return []
}
// Cycle detection
if (visited.has(bestRoute.targetVersion)) {
return []
}
// Add step to path
path.push({
fromVersion: version,
toVersion: bestRoute.targetVersion,
downloadUrl: bestRoute.downloadUrl,
fileSize: 0,
checksum: '',
})
// Advance
visited.add(bestRoute.targetVersion)
version = bestRoute.targetVersion
}
// Enrich each step with fileSize and checksum from firmware_entries
for (const step of path) {
const entry = await FirmwareEntryModel.findByProductClassAndVersion(
productClass,
step.toVersion
)
if (entry) {
step.fileSize = entry.fileSize
step.checksum = entry.checksum
}
}
return path
}
}