/** * Unraid GraphQL Client * Provides type-safe access to the Unraid API */ import axios, { AxiosInstance } from 'axios'; import type { Customization, Disk, Docker, Flash, Network, Notification, Plugin, Registration, Server, ServerVars, Service, Share, SystemInfo, UnraidApiResponse, UnraidArray, UpsDevice, VirtualMachine, } from './types'; import { ARRAY_QUERY, CUSTOMIZATION_QUERY, DASHBOARD_QUERY, DISK_QUERY, DISKS_QUERY, DOCKER_QUERY, FLASH_QUERY, INFO_QUERY, NETWORK_QUERY, NOTIFICATIONS_QUERY, PLUGINS_QUERY, REGISTRATION_QUERY, SERVER_QUERY, SERVICES_QUERY, SHARES_QUERY, UPS_DEVICES_QUERY, VARS_QUERY, VMS_QUERY, } from './queries'; import { ARRAY_SET_STATE_MUTATION, DOCKER_START_MUTATION, DOCKER_STOP_MUTATION, NOTIFICATION_ARCHIVE_MUTATION, NOTIFICATION_DELETE_MUTATION, PARITY_CHECK_CANCEL_MUTATION, PARITY_CHECK_PAUSE_MUTATION, PARITY_CHECK_RESUME_MUTATION, PARITY_CHECK_START_MUTATION, VM_FORCE_STOP_MUTATION, VM_PAUSE_MUTATION, VM_REBOOT_MUTATION, VM_RESUME_MUTATION, VM_START_MUTATION, VM_STOP_MUTATION, } from './queries/mutations'; export interface UnraidClientConfig { host: string; apiKey: string; useSsl?: boolean; port?: number; } export interface DashboardData { info: SystemInfo; vars: ServerVars; registration: Registration; array: UnraidArray; docker: Docker; vms: VirtualMachine[]; shares: Share[]; services: Service[]; notifications: Notification[]; } export class UnraidClient { private client: AxiosInstance; private config: UnraidClientConfig; constructor(config: UnraidClientConfig) { this.config = config; const protocol = config.useSsl ? 'https' : 'http'; const port = config.port || (config.useSsl ? 443 : 80); const baseURL = `${protocol}://${config.host}:${port}`; this.client = axios.create({ baseURL, headers: { 'Content-Type': 'application/json', 'x-api-key': config.apiKey, }, timeout: 30000, }); } /** * Execute a GraphQL query */ private async query(query: string, variables?: Record): Promise { const response = await this.client.post>('/graphql', { query, variables, }); if (response.data.errors?.length) { throw new Error(response.data.errors.map((e) => e.message).join(', ')); } return response.data.data; } /** * Execute a GraphQL mutation */ private async mutate(mutation: string, variables?: Record): Promise { return this.query(mutation, variables); } // ============================================================================ // SYSTEM QUERIES // ============================================================================ async getInfo(): Promise { const data = await this.query<{ info: SystemInfo }>(INFO_QUERY); return data.info; } async getVars(): Promise { const data = await this.query<{ vars: ServerVars }>(VARS_QUERY); return data.vars; } async getServer(): Promise { const data = await this.query<{ server: Server }>(SERVER_QUERY); return data.server; } async getRegistration(): Promise { const data = await this.query<{ registration: Registration }>(REGISTRATION_QUERY); return data.registration; } async getFlash(): Promise { const data = await this.query<{ flash: Flash }>(FLASH_QUERY); return data.flash; } // ============================================================================ // ARRAY QUERIES // ============================================================================ async getArray(): Promise { const data = await this.query<{ array: UnraidArray }>(ARRAY_QUERY); return data.array; } async getDisks(): Promise { const data = await this.query<{ disks: Disk[] }>(DISKS_QUERY); return data.disks; } async getDisk(id: string): Promise { const data = await this.query<{ disk: Disk }>(DISK_QUERY, { id }); return data.disk; } // ============================================================================ // DOCKER QUERIES // ============================================================================ async getDocker(): Promise { const data = await this.query<{ docker: Docker }>(DOCKER_QUERY); return data.docker; } // ============================================================================ // VM QUERIES // ============================================================================ async getVms(): Promise { const data = await this.query<{ vms: VirtualMachine[] }>(VMS_QUERY); return data.vms; } // ============================================================================ // SHARES QUERIES // ============================================================================ async getShares(): Promise { const data = await this.query<{ shares: Share[] }>(SHARES_QUERY); return data.shares; } // ============================================================================ // NOTIFICATIONS QUERIES // ============================================================================ async getNotifications(): Promise { const data = await this.query<{ notifications: Notification[] }>(NOTIFICATIONS_QUERY); return data.notifications; } // ============================================================================ // SERVICES QUERIES // ============================================================================ async getServices(): Promise { const data = await this.query<{ services: Service[] }>(SERVICES_QUERY); return data.services; } // ============================================================================ // NETWORK QUERIES // ============================================================================ async getNetwork(): Promise { const data = await this.query<{ network: Network }>(NETWORK_QUERY); return data.network; } // ============================================================================ // UPS QUERIES // ============================================================================ async getUpsDevices(): Promise { const data = await this.query<{ upsDevices: UpsDevice[] }>(UPS_DEVICES_QUERY); return data.upsDevices; } // ============================================================================ // PLUGINS QUERIES // ============================================================================ async getPlugins(): Promise { const data = await this.query<{ plugins: Plugin[] }>(PLUGINS_QUERY); return data.plugins; } // ============================================================================ // CUSTOMIZATION QUERIES // ============================================================================ async getCustomization(): Promise { const data = await this.query<{ customization: Customization }>(CUSTOMIZATION_QUERY); return data.customization; } // ============================================================================ // DASHBOARD (COMPOSITE QUERY) // ============================================================================ async getDashboard(): Promise { const data = await this.query(DASHBOARD_QUERY); return data; } // ============================================================================ // ARRAY MUTATIONS // ============================================================================ async startArray(): Promise<{ state: string }> { const data = await this.mutate<{ array: { setState: { state: string } } }>( ARRAY_SET_STATE_MUTATION, { state: 'start' } ); return data.array.setState; } async stopArray(): Promise<{ state: string }> { const data = await this.mutate<{ array: { setState: { state: string } } }>( ARRAY_SET_STATE_MUTATION, { state: 'stop' } ); return data.array.setState; } // ============================================================================ // PARITY CHECK MUTATIONS // ============================================================================ async startParityCheck(correct = false): Promise<{ running: boolean; progress: number }> { const data = await this.mutate<{ parityCheck: { start: { running: boolean; progress: number } }; }>(PARITY_CHECK_START_MUTATION, { correct }); return data.parityCheck.start; } async pauseParityCheck(): Promise<{ running: boolean; progress: number }> { const data = await this.mutate<{ parityCheck: { pause: { running: boolean; progress: number } }; }>(PARITY_CHECK_PAUSE_MUTATION); return data.parityCheck.pause; } async resumeParityCheck(): Promise<{ running: boolean; progress: number }> { const data = await this.mutate<{ parityCheck: { resume: { running: boolean; progress: number } }; }>(PARITY_CHECK_RESUME_MUTATION); return data.parityCheck.resume; } async cancelParityCheck(): Promise<{ running: boolean }> { const data = await this.mutate<{ parityCheck: { cancel: { running: boolean } } }>( PARITY_CHECK_CANCEL_MUTATION ); return data.parityCheck.cancel; } // ============================================================================ // DOCKER MUTATIONS // ============================================================================ async startContainer(id: string): Promise<{ id: string; state: string; status: string }> { const data = await this.mutate<{ docker: { start: { id: string; state: string; status: string } }; }>(DOCKER_START_MUTATION, { id }); return data.docker.start; } async stopContainer(id: string): Promise<{ id: string; state: string; status: string }> { const data = await this.mutate<{ docker: { stop: { id: string; state: string; status: string } }; }>(DOCKER_STOP_MUTATION, { id }); return data.docker.stop; } // ============================================================================ // VM MUTATIONS // ============================================================================ async startVm(id: string): Promise<{ id: string; state: string }> { const data = await this.mutate<{ vm: { start: { id: string; state: string } } }>( VM_START_MUTATION, { id } ); return data.vm.start; } async stopVm(id: string): Promise<{ id: string; state: string }> { const data = await this.mutate<{ vm: { stop: { id: string; state: string } } }>( VM_STOP_MUTATION, { id } ); return data.vm.stop; } async pauseVm(id: string): Promise<{ id: string; state: string }> { const data = await this.mutate<{ vm: { pause: { id: string; state: string } } }>( VM_PAUSE_MUTATION, { id } ); return data.vm.pause; } async resumeVm(id: string): Promise<{ id: string; state: string }> { const data = await this.mutate<{ vm: { resume: { id: string; state: string } } }>( VM_RESUME_MUTATION, { id } ); return data.vm.resume; } async forceStopVm(id: string): Promise<{ id: string; state: string }> { const data = await this.mutate<{ vm: { forceStop: { id: string; state: string } } }>( VM_FORCE_STOP_MUTATION, { id } ); return data.vm.forceStop; } async rebootVm(id: string): Promise<{ id: string; state: string }> { const data = await this.mutate<{ vm: { reboot: { id: string; state: string } } }>( VM_REBOOT_MUTATION, { id } ); return data.vm.reboot; } // ============================================================================ // NOTIFICATION MUTATIONS // ============================================================================ async deleteNotification(id: string): Promise { const data = await this.mutate<{ notification: { delete: boolean } }>( NOTIFICATION_DELETE_MUTATION, { id } ); return data.notification.delete; } async archiveNotification(id: string): Promise<{ id: string; archived: boolean }> { const data = await this.mutate<{ notification: { archive: { id: string; archived: boolean } }; }>(NOTIFICATION_ARCHIVE_MUTATION, { id }); return data.notification.archive; } // ============================================================================ // HEALTH CHECK // ============================================================================ async healthCheck(): Promise { try { await this.getVars(); return true; } catch { return false; } } } // ============================================================================ // SINGLETON INSTANCE // ============================================================================ let unraidClient: UnraidClient | null = null; export function getUnraidClient(): UnraidClient { if (!unraidClient) { const host = process.env.UNRAID_HOST; const apiKey = process.env.UNRAID_API_KEY; if (!host || !apiKey) { throw new Error('UNRAID_HOST and UNRAID_API_KEY environment variables are required'); } unraidClient = new UnraidClient({ host, apiKey, useSsl: process.env.UNRAID_USE_SSL === 'true', port: process.env.UNRAID_PORT ? parseInt(process.env.UNRAID_PORT, 10) : undefined, }); } return unraidClient; } export function createUnraidClient(config: UnraidClientConfig): UnraidClient { return new UnraidClient(config); }