/** * Geofence Database Layer * CRUD operations for Geofence, GeofenceEvent, and GeofenceStatus */ import Database from 'better-sqlite3'; import path from 'path'; import type { Geofence, GeofenceEvent, GeofenceStatus, CreateGeofenceInput, CreateGeofenceEventInput, } from './types'; const dbPath = path.join(process.cwd(), 'data', 'database.sqlite'); function getDb() { return new Database(dbPath); } export const geofenceDb = { /** * Create a new geofence */ create(data: CreateGeofenceInput): Geofence { const db = getDb(); try { const stmt = db.prepare(` INSERT INTO Geofence ( id, name, description, shape_type, center_latitude, center_longitude, radius_meters, owner_id, device_id, color ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) `); stmt.run( data.id, data.name, data.description || null, 'circle', data.center_latitude, data.center_longitude, data.radius_meters, data.owner_id, data.device_id, data.color || '#3b82f6' ); return this.findById(data.id)!; } finally { db.close(); } }, /** * Find geofence by ID */ findById(id: string): Geofence | null { const db = getDb(); try { const stmt = db.prepare('SELECT * FROM Geofence WHERE id = ?'); return stmt.get(id) as Geofence | null; } finally { db.close(); } }, /** * Find all geofences for an owner */ findByOwner(ownerId: string): Geofence[] { const db = getDb(); try { const stmt = db.prepare('SELECT * FROM Geofence WHERE owner_id = ? ORDER BY created_at DESC'); return stmt.all(ownerId) as Geofence[]; } finally { db.close(); } }, /** * Find all active geofences for a specific device */ findActiveForDevice(deviceId: string): Geofence[] { const db = getDb(); try { const stmt = db.prepare(` SELECT * FROM Geofence WHERE device_id = ? AND is_active = 1 ORDER BY created_at DESC `); return stmt.all(deviceId) as Geofence[]; } finally { db.close(); } }, /** * Delete a geofence */ delete(id: string): boolean { const db = getDb(); try { const stmt = db.prepare('DELETE FROM Geofence WHERE id = ?'); const result = stmt.run(id); return result.changes > 0; } finally { db.close(); } }, /** * Get current status for device/geofence pair */ getStatus(deviceId: string, geofenceId: string): GeofenceStatus | null { const db = getDb(); try { const stmt = db.prepare(` SELECT * FROM GeofenceStatus WHERE device_id = ? AND geofence_id = ? `); return stmt.get(deviceId, geofenceId) as GeofenceStatus | null; } finally { db.close(); } }, /** * Update or create status for device/geofence pair */ updateStatus(deviceId: string, geofenceId: string, isInside: boolean): void { const db = getDb(); try { const now = new Date().toISOString(); const isInsideInt = isInside ? 1 : 0; // Try to update existing status const updateStmt = db.prepare(` UPDATE GeofenceStatus SET is_inside = ?, last_enter_time = CASE WHEN ? = 1 AND is_inside = 0 THEN ? ELSE last_enter_time END, last_exit_time = CASE WHEN ? = 0 AND is_inside = 1 THEN ? ELSE last_exit_time END, last_checked_at = ?, updated_at = ? WHERE device_id = ? AND geofence_id = ? `); const result = updateStmt.run( isInsideInt, isInsideInt, now, // for enter isInsideInt, now, // for exit now, now, deviceId, geofenceId ); // If no rows updated, insert new status if (result.changes === 0) { const insertStmt = db.prepare(` INSERT INTO GeofenceStatus ( device_id, geofence_id, is_inside, last_enter_time, last_exit_time, last_checked_at ) VALUES (?, ?, ?, ?, ?, ?) `); insertStmt.run( deviceId, geofenceId, isInsideInt, isInside ? now : null, !isInside ? now : null, now ); } } finally { db.close(); } }, /** * Create a new geofence event */ createEvent(event: CreateGeofenceEventInput): GeofenceEvent { const db = getDb(); try { const stmt = db.prepare(` INSERT INTO GeofenceEvent ( geofence_id, device_id, location_id, event_type, latitude, longitude, distance_from_center, timestamp ) VALUES (?, ?, ?, ?, ?, ?, ?, ?) `); const result = stmt.run( event.geofence_id, event.device_id, event.location_id, event.event_type, event.latitude, event.longitude, event.distance_from_center, event.timestamp ); // Return the created event const selectStmt = db.prepare('SELECT * FROM GeofenceEvent WHERE id = ?'); return selectStmt.get(result.lastInsertRowid) as GeofenceEvent; } finally { db.close(); } }, /** * Find events for a specific device or geofence */ findEvents(filters: { deviceId?: string; geofenceId?: string; limit?: number; }): GeofenceEvent[] { const db = getDb(); try { let query = 'SELECT * FROM GeofenceEvent WHERE 1=1'; const params: any[] = []; if (filters.deviceId) { query += ' AND device_id = ?'; params.push(filters.deviceId); } if (filters.geofenceId) { query += ' AND geofence_id = ?'; params.push(filters.geofenceId); } query += ' ORDER BY timestamp DESC'; if (filters.limit) { query += ' LIMIT ?'; params.push(filters.limit); } const stmt = db.prepare(query); return stmt.all(...params) as GeofenceEvent[]; } finally { db.close(); } }, /** * Mark notification as sent or failed */ markNotificationSent(eventId: number, success: boolean, error?: string): void { const db = getDb(); try { const stmt = db.prepare(` UPDATE GeofenceEvent SET notification_sent = ?, notification_error = ? WHERE id = ? `); stmt.run( success ? 1 : 2, // 1 = sent, 2 = failed error || null, eventId ); } finally { db.close(); } }, };