import { MySQLClient } from '@p42/p42modules' const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i export function isValidUuid(val) { return(typeof(val) === 'string' && UUID_RE.test(val)) } export class SimRepository { constructor(dbPool, databases, debug = false) { this.db = dbPool this.guiDb = databases.guiDatabase this.simDb = databases.simDatabase this.debug = debug } #qualify(db, table) { return(`\`${db}\`.${table}`) } #parseGpsValues(raw) { let v = raw if(v == null) return(null) if(typeof(v) === 'string') { try { v = JSON.parse(v) } catch { return(null) } } if(typeof(v) !== 'object') return(null) const position = v.position const speed = v.speed ?? v.vector if(!position || !speed) return(null) const axes = ['x', 'y', 'z'] for(const axis of axes) { if(typeof(position[axis]) !== 'number' || typeof(speed[axis]) !== 'number') return(null) } return({ position: { x: position.x, y: position.y, z: position.z }, vector: { x: speed.x, y: speed.y, z: speed.z }, }) } async validateSimulationAccess(userUuid, simulationUuid, keyframeId) { if(!isValidUuid(userUuid)) return({ ok: false, err: 'Invalid user UUID' }) if(!isValidUuid(simulationUuid)) return({ ok: false, err: 'Invalid simulation UUID' }) if(!isValidUuid(keyframeId)) return({ ok: false, err: 'Invalid keyframe ID' }) const rows = await MySQLClient.poolExecute(this.db, ` SELECT s.sim_id, BIN_TO_UUID(s.sim_uuid) AS sim_uuid, BIN_TO_UUID(s.sim_root_kf_uuid) AS sim_root_kf_uuid FROM ${this.#qualify(this.simDb, 'simulations')} s INNER JOIN ${this.#qualify(this.guiDb, 'simowners')} o ON o.own_sim_uuid = s.sim_uuid INNER JOIN ${this.#qualify(this.guiDb, 'users')} u ON o.own_usr_id = u.usr_id WHERE u.usr_uuid = ? AND s.sim_uuid = UUID_TO_BIN(?) `, [userUuid, simulationUuid]) if(!rows.length) return({ ok: false, err: 'Simulation not found or access denied' }) const sim = rows[0] if(sim.sim_root_kf_uuid !== keyframeId) { return({ ok: false, err: 'Keyframe does not match simulation root keyframe' }) } const kfRows = await MySQLClient.poolExecute(this.db, ` SELECT ekf_uuid FROM ${this.#qualify(this.simDb, 'edited_keyframes')} WHERE ekf_uuid = UUID_TO_BIN(?) `, [keyframeId]) if(!kfRows.length) return({ ok: false, err: 'Keyframe not found' }) return({ ok: true, sim }) } async validateSimulationOwner(userUuid, simulationUuid) { if(!isValidUuid(userUuid)) return({ ok: false, err: 'Invalid user UUID' }) if(!isValidUuid(simulationUuid)) return({ ok: false, err: 'Invalid simulation UUID' }) const rows = await MySQLClient.poolExecute(this.db, ` SELECT s.sim_id, BIN_TO_UUID(s.sim_uuid) AS sim_uuid, BIN_TO_UUID(s.sim_root_kf_uuid) AS sim_root_kf_uuid FROM ${this.#qualify(this.simDb, 'simulations')} s INNER JOIN ${this.#qualify(this.guiDb, 'simowners')} o ON o.own_sim_uuid = s.sim_uuid INNER JOIN ${this.#qualify(this.guiDb, 'users')} u ON o.own_usr_id = u.usr_id WHERE u.usr_uuid = ? AND s.sim_uuid = UUID_TO_BIN(?) `, [userUuid, simulationUuid]) if(!rows.length) return({ ok: false, err: 'Simulation not found or access denied' }) return({ ok: true, sim: rows[0] }) } async loadKeyframeAgents(keyframeId) { const rows = await MySQLClient.poolExecute(this.db, ` SELECT BIN_TO_UUID(ekfs_agent_id) AS agent_id, ekfs_gps_values FROM ${this.#qualify(this.simDb, 'edited_kf_store')} WHERE ekfs_ekf_uuid = UUID_TO_BIN(?) `, [keyframeId]) const agents = [] const errors = [] for(const row of rows) { const parsed = this.#parseGpsValues(row.ekfs_gps_values) if(!parsed) { errors.push(`Invalid GPS values for agent ${row.agent_id}`) continue } agents.push({ id: row.agent_id, position: parsed.position, vector: parsed.vector, }) } if(errors.length) return({ ok: false, err: errors.join('; '), agents: [] }) return({ ok: true, agents }) } }