Files
kestrelos/server/utils/deviceUtils.js
Madison Grubb b7046dc0e6 initial commit
2026-02-10 23:32:26 -05:00

84 lines
3.5 KiB
JavaScript

import { sanitizeStreamUrl } from './feedUtils.js'
const DEVICE_TYPES = Object.freeze(['alpr', 'nvr', 'doorbell', 'feed', 'traffic', 'ip', 'drone'])
const SOURCE_TYPES = Object.freeze(['mjpeg', 'hls'])
/** @typedef {{ id: string, name: string, device_type: string, vendor: string | null, lat: number, lng: number, stream_url: string, source_type: string, config: string | null }} DeviceRow */
/**
* @param {string} s
* @returns {string} 'mjpeg' or 'hls'
*/
function normalizeSourceType(s) {
return SOURCE_TYPES.includes(s) ? s : 'mjpeg'
}
/**
* @param {unknown} row
* @returns {DeviceRow | null} Normalized device row or null if invalid
*/
export function rowToDevice(row) {
if (!row || typeof row !== 'object') return null
const r = /** @type {Record<string, unknown>} */ (row)
if (typeof r.id !== 'string' || typeof r.name !== 'string' || typeof r.device_type !== 'string') return null
if (typeof r.lat !== 'number' && typeof r.lat !== 'string') return null
if (typeof r.lng !== 'number' && typeof r.lng !== 'string') return null
const lat = Number(r.lat)
const lng = Number(r.lng)
if (!Number.isFinite(lat) || !Number.isFinite(lng)) return null
return {
id: r.id,
name: r.name,
device_type: r.device_type,
vendor: typeof r.vendor === 'string' ? r.vendor : null,
lat,
lng,
stream_url: typeof r.stream_url === 'string' ? r.stream_url : '',
source_type: normalizeSourceType(r.source_type),
config: typeof r.config === 'string' ? r.config : null,
}
}
/**
* Sanitize device for API response (safe stream URL, valid sourceType).
* @param {DeviceRow} device
* @returns {{ id: string, name: string, device_type: string, vendor: string | null, lat: number, lng: number, streamUrl: string, sourceType: string, config: string | null }} Sanitized device for API response
*/
export function sanitizeDeviceForResponse(device) {
return {
id: device.id,
name: device.name,
device_type: device.device_type,
vendor: device.vendor,
lat: device.lat,
lng: device.lng,
streamUrl: sanitizeStreamUrl(device.stream_url),
sourceType: normalizeSourceType(device.source_type),
config: device.config,
}
}
/**
* Validate and normalize device body for POST.
* @param {unknown} body
* @returns {{ name: string, device_type: string, vendor: string | null, lat: number, lng: number, stream_url: string, source_type: string, config: string | null }} Validated and normalized body fields
*/
export function validateDeviceBody(body) {
if (!body || typeof body !== 'object') throw createError({ statusCode: 400, message: 'body required' })
const b = /** @type {Record<string, unknown>} */ (body)
const name = typeof b.name === 'string' ? b.name.trim() || '' : ''
const deviceType = typeof b.device_type === 'string' && DEVICE_TYPES.includes(b.device_type) ? b.device_type : 'feed'
const vendor = typeof b.vendor === 'string' ? b.vendor.trim() || null : null
const lat = Number(b.lat)
const lng = Number(b.lng)
if (!Number.isFinite(lat) || !Number.isFinite(lng)) {
throw createError({ statusCode: 400, message: 'lat and lng required as finite numbers' })
}
const streamUrl = typeof b.stream_url === 'string' ? b.stream_url.trim() : ''
const sourceType = normalizeSourceType(b.source_type)
const config = typeof b.config === 'string' ? b.config : (b.config != null ? JSON.stringify(b.config) : null)
return { name, device_type: deviceType, vendor, lat, lng, stream_url: streamUrl, source_type: sourceType, config }
}
export { DEVICE_TYPES, SOURCE_TYPES }