diff --git a/.cursor/rules/api-rules.mdc b/.cursor/rules/api-rules.mdc new file mode 100644 index 000000000..45e4becb9 --- /dev/null +++ b/.cursor/rules/api-rules.mdc @@ -0,0 +1,11 @@ +--- +description: +globs: api/* +alwaysApply: false +--- + +* pnpm ONLY +* always run scripts from api/package.json unless requested +* prefer adding new files to the nest repo located at api/src/unraid-api/ instead of the legacy code +* Test suite is VITEST, do not use jest +* Prefer to not mock simple dependencies \ No newline at end of file diff --git a/api/.env.development b/api/.env.development index 25172915b..ff9ec6fc9 100644 --- a/api/.env.development +++ b/api/.env.development @@ -9,6 +9,7 @@ PATHS_MY_SERVERS_CONFIG=./dev/Unraid.net/myservers.cfg # My servers config file PATHS_MY_SERVERS_FB=./dev/Unraid.net/fb_keepalive # My servers flashbackup timekeeper file PATHS_KEYFILE_BASE=./dev/Unraid.net # Keyfile location PATHS_MACHINE_ID=./dev/data/machine-id +PATHS_PARITY_CHECKS=./dev/states/parity-checks.log ENVIRONMENT="development" NODE_ENV="development" PORT="3001" diff --git a/api/.env.test b/api/.env.test index b374b3d24..427fadf8f 100644 --- a/api/.env.test +++ b/api/.env.test @@ -9,5 +9,6 @@ PATHS_MY_SERVERS_CONFIG=./dev/Unraid.net/myservers.cfg # My servers config file PATHS_MY_SERVERS_FB=./dev/Unraid.net/fb_keepalive # My servers flashbackup timekeeper file PATHS_KEYFILE_BASE=./dev/Unraid.net # Keyfile location PATHS_MACHINE_ID=./dev/data/machine-id +PATHS_PARITY_CHECKS=./dev/states/parity-checks.log PORT=5000 NODE_ENV="test" \ No newline at end of file diff --git a/api/codegen.ts b/api/codegen.ts index 2e51206df..152f520e9 100644 --- a/api/codegen.ts +++ b/api/codegen.ts @@ -1,18 +1,13 @@ import type { CodegenConfig } from '@graphql-codegen/cli'; - - -import { getAuthEnumTypeDefs } from './src/unraid-api/graph/utils/auth-enum.utils.js'; - - const config: CodegenConfig = { overwrite: true, emitLegacyCommonJSImports: false, verbose: true, config: { namingConvention: { - typeNames: './fix-array-type.cjs', - enumValues: 'change-case#upperCase', + enumValues: 'change-case-all#upperCase', + transformUnderscore: true, useTypeImports: true, }, scalars: { @@ -32,56 +27,6 @@ const config: CodegenConfig = { }, }, generates: { - './generated-schema.graphql': { - plugins: ['schema-ast'], - schema: [ - './src/graphql/types.ts', - './src/graphql/schema/types/**/*.graphql', - getAuthEnumTypeDefs(), - ], - }, - // Generate Types for the API Server - 'src/graphql/generated/api/types.ts': { - schema: [ - './src/graphql/types.ts', - './src/graphql/schema/types/**/*.graphql', - getAuthEnumTypeDefs(), - ], - plugins: [ - 'typescript', - 'typescript-resolvers', - { add: { content: '/* eslint-disable */\n/* @ts-nocheck */' } }, - ], - config: { - contextType: '@app/graphql/schema/utils.js#Context', - useIndexSignature: true, - }, - }, - // Generate Operations for any built-in API Server Operations (e.g., report.ts) - 'src/graphql/generated/api/operations.ts': { - documents: './src/graphql/client/api/*.ts', - schema: [ - './src/graphql/types.ts', - './src/graphql/schema/types/**/*.graphql', - getAuthEnumTypeDefs(), - ], - preset: 'import-types', - presetConfig: { - typesPath: '@app/graphql/generated/api/types.js', - }, - plugins: [ - 'typescript-validation-schema', - 'typescript-operations', - 'typed-document-node', - { add: { content: '/* eslint-disable */' } }, - ], - config: { - importFrom: '@app/graphql/generated/api/types.js', - strictScalars: true, - schema: 'zod', - withObjectType: true, - }, - }, // Generate Types for Mothership GraphQL Client 'src/graphql/generated/client/': { documents: './src/graphql/mothership/*.ts', @@ -120,4 +65,4 @@ const config: CodegenConfig = { }, }; -export default config; \ No newline at end of file +export default config; diff --git a/api/dev/Unraid.net/myservers.cfg b/api/dev/Unraid.net/myservers.cfg index 1c664dad2..42c67a36b 100644 --- a/api/dev/Unraid.net/myservers.cfg +++ b/api/dev/Unraid.net/myservers.cfg @@ -1,5 +1,5 @@ [api] -version="4.4.1" +version="4.6.6" extraOrigins="https://google.com,https://test.com" [local] sandbox="yes" diff --git a/api/fix-array-type.cjs b/api/fix-array-type.cjs deleted file mode 100644 index a5e8ef2df..000000000 --- a/api/fix-array-type.cjs +++ /dev/null @@ -1,18 +0,0 @@ -/** - * This function wraps constant case, that turns any string into CONSTANT_CASE - * However, this function has a bug that, if you pass _ to it it will return an empty - * string. This small module fixes that - * - * @param {string*} str - * @return {string} - */ -function FixArrayType(str) { - if (str === 'Array') { - return 'ArrayType'; - } - - // If result is an empty string, just return the original string - return str; -} - -module.exports = FixArrayType; diff --git a/api/generated-schema-new.graphql b/api/generated-schema-new.graphql new file mode 100644 index 000000000..8daa7f161 --- /dev/null +++ b/api/generated-schema-new.graphql @@ -0,0 +1,1563 @@ +# ------------------------------------------------------ +# THIS FILE WAS AUTOMATICALLY GENERATED (DO NOT MODIFY) +# ------------------------------------------------------ + +type ApiKeyResponse { + valid: Boolean! + error: String +} + +type MinigraphqlResponse { + status: MinigraphStatus! + timeout: Int + error: String +} + +enum MinigraphStatus { + PRE_INIT + CONNECTING + CONNECTED + PING_FAILURE + ERROR_RETRYING +} + +type CloudResponse { + status: String! + ip: String + error: String +} + +type RelayResponse { + status: String! + timeout: String + error: String +} + +type Cloud { + error: String + apiKey: ApiKeyResponse! + relay: RelayResponse + minigraphql: MinigraphqlResponse! + cloud: CloudResponse! + allowedOrigins: [String!]! +} + +type Capacity { + """Free capacity""" + free: String! + + """Used capacity""" + used: String! + + """Total capacity""" + total: String! +} + +type ArrayCapacity { + """Capacity in kilobytes""" + kilobytes: Capacity! + + """Capacity in number of disks""" + disks: Capacity! +} + +type ArrayDisk implements Node { + """Disk identifier, only set for present disks on the system""" + id: ID! + + """ + Array slot number. Parity1 is always 0 and Parity2 is always 29. Array slots will be 1 - 28. Cache slots are 30 - 53. Flash is 54. + """ + idx: Int! + name: String + device: String + + """(KB) Disk Size total""" + size: Long + status: ArrayDiskStatus + + """Is the disk a HDD or SSD.""" + rotational: Boolean + + """Disk temp - will be NaN if array is not started or DISK_NP""" + temp: Int + + """ + Count of I/O read requests sent to the device I/O drivers. These statistics may be cleared at any time. + """ + numReads: Long + + """ + Count of I/O writes requests sent to the device I/O drivers. These statistics may be cleared at any time. + """ + numWrites: Long + + """ + Number of unrecoverable errors reported by the device I/O drivers. Missing data due to unrecoverable array read errors is filled in on-the-fly using parity reconstruct (and we attempt to write this data back to the sector(s) which failed). Any unrecoverable write error results in disabling the disk. + """ + numErrors: Long + + """(KB) Total Size of the FS (Not present on Parity type drive)""" + fsSize: Long + + """(KB) Free Size on the FS (Not present on Parity type drive)""" + fsFree: Long + + """(KB) Used Size on the FS (Not present on Parity type drive)""" + fsUsed: Long + exportable: Boolean + + """Type of Disk - used to differentiate Cache / Flash / Array / Parity""" + type: ArrayDiskType! + + """(%) Disk space left to warn""" + warning: Int + + """(%) Disk space left for critical""" + critical: Int + + """File system type for the disk""" + fsType: String + + """User comment on disk""" + comment: String + + """File format (ex MBR: 4KiB-aligned)""" + format: String + + """ata | nvme | usb | (others)""" + transport: String + color: ArrayDiskFsColor +} + +interface Node { + id: ID! +} + +"""The `Long` scalar type represents 52-bit integers""" +scalar Long + +enum ArrayDiskStatus { + DISK_NP + DISK_OK + DISK_NP_MISSING + DISK_INVALID + DISK_WRONG + DISK_DSBL + DISK_NP_DSBL + DISK_DSBL_NEW + DISK_NEW +} + +enum ArrayDiskType { + DATA + PARITY + FLASH + CACHE +} + +enum ArrayDiskFsColor { + GREEN_ON + GREEN_BLINK + BLUE_ON + BLUE_BLINK + YELLOW_ON + YELLOW_BLINK + RED_ON + RED_OFF + GREY_OFF +} + +type UnraidArray implements Node { + id: ID! + + """Array state before this query/mutation""" + previousState: ArrayState + + """Array state after this query/mutation""" + pendingState: ArrayPendingState + + """Current array state""" + state: ArrayState! + + """Current array capacity""" + capacity: ArrayCapacity! + + """Current boot disk""" + boot: ArrayDisk + + """Parity disks in the current array""" + parities: [ArrayDisk!]! + + """Data disks in the current array""" + disks: [ArrayDisk!]! + + """Caches in the current array""" + caches: [ArrayDisk!]! +} + +enum ArrayState { + STARTED + STOPPED + NEW_ARRAY + RECON_DISK + DISABLE_DISK + SWAP_DSBL + INVALID_EXPANSION + PARITY_NOT_BIGGEST + TOO_MANY_MISSING_DISKS + NEW_DISK_TOO_SMALL + NO_DATA_DISKS +} + +enum ArrayPendingState { + STARTING + STOPPING + NO_DATA_DISKS + TOO_MANY_MISSING_DISKS +} + +type Share implements Node { + id: ID! + + """Display name""" + name: String + + """(KB) Free space""" + free: Long + + """(KB) Used Size""" + used: Long + + """(KB) Total size""" + size: Long + + """Disks that are included in this share""" + include: [String!] + + """Disks that are excluded from this share""" + exclude: [String!] + + """Is this share cached""" + cache: Boolean + + """Original name""" + nameOrig: String + + """User comment""" + comment: String + + """Allocator""" + allocator: String + + """Split level""" + splitLevel: String + + """Floor""" + floor: String + + """COW""" + cow: String + + """Color""" + color: String + + """LUKS status""" + luksStatus: String +} + +type RemoteAccess { + """The type of WAN access used for Remote Access""" + accessType: WAN_ACCESS_TYPE! + + """The type of port forwarding used for Remote Access""" + forwardType: WAN_FORWARD_TYPE + + """The port used for Remote Access""" + port: Int +} + +enum WAN_ACCESS_TYPE { + DYNAMIC + ALWAYS + DISABLED +} + +enum WAN_FORWARD_TYPE { + UPNP + STATIC +} + +type DynamicRemoteAccessStatus { + """The type of dynamic remote access that is enabled""" + enabledType: DynamicRemoteAccessType! + + """The type of dynamic remote access that is currently running""" + runningType: DynamicRemoteAccessType! + + """Any error message associated with the dynamic remote access""" + error: String +} + +enum DynamicRemoteAccessType { + STATIC + UPNP + DISABLED +} + +type ConnectSettingsValues { + """ + If true, the GraphQL sandbox is enabled and available at /graphql. If false, the GraphQL sandbox is disabled and only the production API will be available. + """ + sandbox: Boolean! + + """A list of origins allowed to interact with the API""" + extraOrigins: [String!]! + + """The type of WAN access used for Remote Access""" + accessType: WAN_ACCESS_TYPE! + + """The type of port forwarding used for Remote Access""" + forwardType: WAN_FORWARD_TYPE + + """The port used for Remote Access""" + port: Int + + """A list of Unique Unraid Account ID's""" + ssoUserIds: [String!]! +} + +type ConnectSettings implements Node { + """The unique identifier for the Connect settings""" + id: ID! + + """The data schema for the Connect settings""" + dataSchema: JSON! + + """The UI schema for the Connect settings""" + uiSchema: JSON! + + """The values for the Connect settings""" + values: ConnectSettingsValues! +} + +""" +The `JSON` scalar type represents JSON values as specified by [ECMA-404](http://www.ecma-international.org/publications/files/ECMA-ST/ECMA-404.pdf). +""" +scalar JSON @specifiedBy(url: "http://www.ecma-international.org/publications/files/ECMA-ST/ECMA-404.pdf") + +type Connect implements Node { + """The unique identifier for the Connect instance""" + id: ID! + + """The status of dynamic remote access""" + dynamicRemoteAccess: DynamicRemoteAccessStatus! + + """The settings for the Connect instance""" + settings: ConnectSettings! +} + +type AccessUrl { + type: URL_TYPE! + name: String + ipv4: URL + ipv6: URL +} + +enum URL_TYPE { + LAN + WIREGUARD + WAN + MDNS + OTHER + DEFAULT +} + +""" +A field whose value conforms to the standard URL format as specified in RFC3986: https://www.ietf.org/rfc/rfc3986.txt. +""" +scalar URL + +type Network implements Node { + id: ID! + accessUrls: [AccessUrl!] +} + +type ProfileModel { + userId: ID + username: String! + url: String! + avatar: String! +} + +type Server { + owner: ProfileModel! + guid: String! + apikey: String! + name: String! + status: ServerStatus! + wanip: String! + lanip: String! + localurl: String! + remoteurl: String! +} + +enum ServerStatus { + ONLINE + OFFLINE + NEVER_CONNECTED +} + +type DiskPartition { + """The name of the partition""" + name: String! + + """The filesystem type of the partition""" + fsType: DiskFsType! + + """The size of the partition in bytes""" + size: Float! +} + +"""The type of filesystem on the disk partition""" +enum DiskFsType { + XFS + BTRFS + VFAT + ZFS + EXT4 + NTFS +} + +type Disk { + """The unique identifier of the disk""" + id: String! + + """The device path of the disk (e.g. /dev/sdb)""" + device: String! + + """The type of disk (e.g. SSD, HDD)""" + type: String! + + """The model name of the disk""" + name: String! + + """The manufacturer of the disk""" + vendor: String! + + """The total size of the disk in bytes""" + size: Float! + + """The number of bytes per sector""" + bytesPerSector: Float! + + """The total number of cylinders on the disk""" + totalCylinders: Float! + + """The total number of heads on the disk""" + totalHeads: Float! + + """The total number of sectors on the disk""" + totalSectors: Float! + + """The total number of tracks on the disk""" + totalTracks: Float! + + """The number of tracks per cylinder""" + tracksPerCylinder: Float! + + """The number of sectors per track""" + sectorsPerTrack: Float! + + """The firmware revision of the disk""" + firmwareRevision: String! + + """The serial number of the disk""" + serialNum: String! + + """The interface type of the disk""" + interfaceType: DiskInterfaceType! + + """The SMART status of the disk""" + smartStatus: DiskSmartStatus! + + """The current temperature of the disk in Celsius""" + temperature: Float + + """The partitions on the disk""" + partitions: [DiskPartition!]! +} + +"""The type of interface the disk uses to connect to the system""" +enum DiskInterfaceType { + SAS + SATA + USB + PCIE + UNKNOWN +} + +""" +The SMART (Self-Monitoring, Analysis and Reporting Technology) status of the disk +""" +enum DiskSmartStatus { + OK + UNKNOWN +} + +type KeyFile { + location: String + contents: String +} + +type Registration { + guid: ID + type: registrationType + keyFile: KeyFile + state: RegistrationState + expiration: String + updateExpiration: String +} + +enum registrationType { + BASIC + PLUS + PRO + STARTER + UNLEASHED + LIFETIME + INVALID + TRIAL +} + +enum RegistrationState { + TRIAL + BASIC + PLUS + PRO + STARTER + UNLEASHED + LIFETIME + EEXPIRED + EGUID + EGUID1 + ETRIAL + ENOKEYFILE + ENOKEYFILE1 + ENOKEYFILE2 + ENOFLASH + ENOFLASH1 + ENOFLASH2 + ENOFLASH3 + ENOFLASH4 + ENOFLASH5 + ENOFLASH6 + ENOFLASH7 + EBLACKLISTED + EBLACKLISTED1 + EBLACKLISTED2 + ENOCONN +} + +type Vars implements Node { + id: ID! + + """Unraid version""" + version: String + maxArraysz: Int + maxCachesz: Int + + """Machine hostname""" + name: String + timeZone: String + comment: String + security: String + workgroup: String + domain: String + domainShort: String + hideDotFiles: Boolean + localMaster: Boolean + enableFruit: String + + """Should a NTP server be used for time sync?""" + useNtp: Boolean + + """NTP Server 1""" + ntpServer1: String + + """NTP Server 2""" + ntpServer2: String + + """NTP Server 3""" + ntpServer3: String + + """NTP Server 4""" + ntpServer4: String + domainLogin: String + sysModel: String + sysArraySlots: Int + sysCacheSlots: Int + sysFlashSlots: Int + useSsl: Boolean + + """Port for the webui via HTTP""" + port: Int + + """Port for the webui via HTTPS""" + portssl: Int + localTld: String + bindMgt: Boolean + + """Should telnet be enabled?""" + useTelnet: Boolean + porttelnet: Int + useSsh: Boolean + portssh: Int + startPage: String + startArray: Boolean + spindownDelay: String + queueDepth: String + spinupGroups: Boolean + defaultFormat: String + defaultFsType: String + shutdownTimeout: Int + luksKeyfile: String + pollAttributes: String + pollAttributesDefault: String + pollAttributesStatus: String + nrRequests: Int + nrRequestsDefault: Int + nrRequestsStatus: String + mdNumStripes: Int + mdNumStripesDefault: Int + mdNumStripesStatus: String + mdSyncWindow: Int + mdSyncWindowDefault: Int + mdSyncWindowStatus: String + mdSyncThresh: Int + mdSyncThreshDefault: Int + mdSyncThreshStatus: String + mdWriteMethod: Int + mdWriteMethodDefault: String + mdWriteMethodStatus: String + shareDisk: String + shareUser: String + shareUserInclude: String + shareUserExclude: String + shareSmbEnabled: Boolean + shareNfsEnabled: Boolean + shareAfpEnabled: Boolean + shareInitialOwner: String + shareInitialGroup: String + shareCacheEnabled: Boolean + shareCacheFloor: String + shareMoverSchedule: String + shareMoverLogging: Boolean + fuseRemember: String + fuseRememberDefault: String + fuseRememberStatus: String + fuseDirectio: String + fuseDirectioDefault: String + fuseDirectioStatus: String + shareAvahiEnabled: Boolean + shareAvahiSmbName: String + shareAvahiSmbModel: String + shareAvahiAfpName: String + shareAvahiAfpModel: String + safeMode: Boolean + startMode: String + configValid: Boolean + configError: ConfigErrorState + joinStatus: String + deviceCount: Int + flashGuid: String + flashProduct: String + flashVendor: String + regCheck: String + regFile: String + regGuid: String + regTy: registrationType + regState: RegistrationState + + """Registration owner""" + regTo: String + regTm: String + regTm2: String + regGen: String + sbName: String + sbVersion: String + sbUpdated: String + sbEvents: Int + sbState: String + sbClean: Boolean + sbSynced: Int + sbSyncErrs: Int + sbSynced2: Int + sbSyncExit: String + sbNumDisks: Int + mdColor: String + mdNumDisks: Int + mdNumDisabled: Int + mdNumInvalid: Int + mdNumMissing: Int + mdNumNew: Int + mdNumErased: Int + mdResync: Int + mdResyncCorr: String + mdResyncPos: String + mdResyncDb: String + mdResyncDt: String + mdResyncAction: String + mdResyncSize: Int + mdState: String + mdVersion: String + cacheNumDevices: Int + cacheSbNumDisks: Int + fsState: String + + """Human friendly string of array events happening""" + fsProgress: String + + """ + Percentage from 0 - 100 while upgrading a disk or swapping parity drives + """ + fsCopyPrcnt: Int + fsNumMounted: Int + fsNumUnmountable: Int + fsUnmountableMask: String + + """Total amount of user shares""" + shareCount: Int + + """Total amount shares with SMB enabled""" + shareSmbCount: Int + + """Total amount shares with NFS enabled""" + shareNfsCount: Int + + """Total amount shares with AFP enabled""" + shareAfpCount: Int + shareMoverActive: Boolean + csrfToken: String +} + +"""Possible error states for configuration""" +enum ConfigErrorState { + UNKNOWN_ERROR + INELIGIBLE + INVALID + NO_KEY_SERVER + WITHDRAWN +} + +type Permission { + resource: Resource! + actions: [String!]! +} + +"""Available resources for permissions""" +enum Resource { + API_KEY + ARRAY + CLOUD + CONFIG + CONNECT + CONNECT__REMOTE_ACCESS + CUSTOMIZATIONS + DASHBOARD + DISK + DISPLAY + DOCKER + FLASH + INFO + LOGS + ME + NETWORK + NOTIFICATIONS + ONLINE + OS + OWNER + PERMISSION + REGISTRATION + SERVERS + SERVICES + SHARE + VARS + VMS + WELCOME +} + +type ApiKey { + id: ID! + name: String! + description: String + roles: [Role!]! + createdAt: String! + permissions: [Permission!]! +} + +"""Available roles for API keys and users""" +enum Role { + ADMIN + CONNECT + GUEST +} + +type ApiKeyWithSecret { + id: ID! + name: String! + description: String + roles: [Role!]! + createdAt: String! + permissions: [Permission!]! + key: String! +} + +type ArrayMutations { + """Set array state""" + setState(input: ArrayStateInput!): UnraidArray! + + """Add new disk to array""" + addDiskToArray(input: ArrayDiskInput!): UnraidArray! + + """ + Remove existing disk from array. NOTE: The array must be stopped before running this otherwise it'll throw an error. + """ + removeDiskFromArray(input: ArrayDiskInput!): UnraidArray! + + """Mount a disk in the array""" + mountArrayDisk(id: String!): ArrayDisk! + + """Unmount a disk from the array""" + unmountArrayDisk(id: String!): ArrayDisk! + + """Clear statistics for a disk in the array""" + clearArrayDiskStatistics(id: String!): Boolean! +} + +input ArrayStateInput { + """Array state""" + desiredState: ArrayStateInputState! +} + +enum ArrayStateInputState { + START + STOP +} + +input ArrayDiskInput { + """Disk ID""" + id: ID! + + """The slot for the disk""" + slot: Int +} + +type DockerMutations { + """Start a container""" + start(id: String!): DockerContainer! + + """Stop a container""" + stop(id: String!): DockerContainer! +} + +type ParityCheck { + """Date of the parity check""" + date: DateTime + + """Duration of the parity check in seconds""" + duration: Int + + """Speed of the parity check, in MB/s""" + speed: String + + """Status of the parity check""" + status: String + + """Number of errors during the parity check""" + errors: Int + + """Progress percentage of the parity check""" + progress: Int + + """Whether corrections are being written to parity""" + correcting: Boolean + + """Whether the parity check is paused""" + paused: Boolean + + """Whether the parity check is running""" + running: Boolean +} + +""" +A date-time string at UTC, such as 2019-12-03T09:54:33Z, compliant with the date-time format. +""" +scalar DateTime + +type Config implements Node { + id: ID! + valid: Boolean + error: String +} + +type InfoApps implements Node { + id: ID! + + """How many docker containers are installed""" + installed: Int! + + """How many docker containers are running""" + started: Int! +} + +type Baseboard implements Node { + id: ID! + manufacturer: String! + model: String + version: String + serial: String + assetTag: String +} + +type InfoCpu implements Node { + id: ID! + manufacturer: String! + brand: String! + vendor: String! + family: String! + model: String! + stepping: Int! + revision: String! + voltage: String + speed: Float! + speedmin: Float! + speedmax: Float! + threads: Int! + cores: Int! + processors: Int! + socket: String! + cache: JSON! + flags: [String!]! +} + +type Gpu implements Node { + id: ID! + type: String! + typeid: String! + vendorname: String! + productid: String! + blacklisted: Boolean! + class: String! +} + +type Pci implements Node { + id: ID! + type: String + typeid: String + vendorname: String + vendorid: String + productname: String + productid: String + blacklisted: String + class: String +} + +type Usb { + id: ID! + name: String +} + +type Devices implements Node { + id: ID! + gpu: [Gpu!]! + pci: [Pci!]! + usb: [Usb!]! +} + +type Case implements Node { + id: ID! + icon: String + url: String + error: String + base64: String +} + +type Display implements Node { + id: ID! + case: Case + date: String + number: String + scale: Boolean + tabs: Boolean + users: String + resize: Boolean + wwn: Boolean + total: Boolean + usage: Boolean + banner: String + dashapps: String + theme: Theme + text: Boolean + unit: Temperature + warning: Int + critical: Int + hot: Int + max: Int + locale: String +} + +"""Display theme""" +enum Theme { + white +} + +"""Temperature unit (Celsius or Fahrenheit)""" +enum Temperature { + C + F +} + +type MemoryLayout { + size: Int! + bank: String + type: String + clockSpeed: Int + formFactor: String + manufacturer: String + partNum: String + serialNum: String + voltageConfigured: Int + voltageMin: Int + voltageMax: Int +} + +type InfoMemory implements Node { + id: ID! + max: Int! + total: Int! + free: Int! + used: Int! + active: Int! + available: Int! + buffcache: Int! + swaptotal: Int! + swapused: Int! + swapfree: Int! + layout: [MemoryLayout!]! +} + +type Os implements Node { + id: ID! + platform: String + distro: String + release: String + codename: String + kernel: String + arch: String + hostname: String + codepage: String + logofile: String + serial: String + build: String + uptime: String +} + +type System implements Node { + id: ID! + manufacturer: String + model: String + version: String + serial: String + uuid: String + sku: String +} + +type Versions implements Node { + id: ID! + kernel: String + openssl: String + systemOpenssl: String + systemOpensslLib: String + node: String + v8: String + npm: String + yarn: String + pm2: String + gulp: String + grunt: String + git: String + tsc: String + mysql: String + redis: String + mongodb: String + apache: String + nginx: String + php: String + docker: String + postfix: String + postgresql: String + perl: String + python: String + gcc: String + unraid: String +} + +type Info implements Node { + id: ID! + + """Count of docker containers""" + apps: InfoApps! + baseboard: Baseboard! + cpu: InfoCpu! + devices: Devices! + display: Display! + + """Machine ID""" + machineId: ID + memory: InfoMemory! + os: Os! + system: System! + time: DateTime! + versions: Versions! +} + +type ContainerPort { + ip: String + privatePort: Int! + publicPort: Int! + type: ContainerPortType! +} + +enum ContainerPortType { + TCP + UDP +} + +type ContainerHostConfig { + networkMode: String! +} + +type DockerContainer { + id: ID! + names: [String!]! + image: String! + imageId: String! + command: String! + created: Int! + ports: [ContainerPort!]! + + """Total size of all the files in the container""" + sizeRootFs: Int + labels: JSONObject + state: ContainerState! + status: String! + hostConfig: ContainerHostConfig + networkSettings: JSONObject + mounts: [JSONObject!] + autoStart: Boolean! +} + +""" +The `JSONObject` scalar type represents JSON objects as specified by [ECMA-404](http://www.ecma-international.org/publications/files/ECMA-ST/ECMA-404.pdf). +""" +scalar JSONObject + +enum ContainerState { + RUNNING + EXITED +} + +type DockerNetwork { + name: String! + id: ID! + created: String! + scope: String! + driver: String! + enableIPv6: Boolean! + ipam: JSONObject! + internal: Boolean! + attachable: Boolean! + ingress: Boolean! + configFrom: JSONObject! + configOnly: Boolean! + containers: JSONObject! + options: JSONObject! + labels: JSONObject! +} + +type Docker implements Node { + id: ID! + containers: [DockerContainer!]! + networks: [DockerNetwork!]! +} + +type Flash implements Node { + id: ID! + guid: String! + vendor: String! + product: String! +} + +type LogFile { + """Name of the log file""" + name: String! + + """Full path to the log file""" + path: String! + + """Size of the log file in bytes""" + size: Int! + + """Last modified timestamp""" + modifiedAt: DateTime! +} + +type LogFileContent { + """Path to the log file""" + path: String! + + """Content of the log file""" + content: String! + + """Total number of lines in the file""" + totalLines: Int! + + """Starting line number of the content (1-indexed)""" + startLine: Int +} + +type NotificationCounts { + info: Int! + warning: Int! + alert: Int! + total: Int! +} + +type NotificationOverview { + unread: NotificationCounts! + archive: NotificationCounts! +} + +type Notification { + id: ID! + + """Also known as 'event'""" + title: String! + subject: String! + description: String! + importance: NotificationImportance! + link: String + type: NotificationType! + + """ISO Timestamp for when the notification occurred""" + timestamp: String + formattedTimestamp: String +} + +enum NotificationImportance { + ALERT + INFO + WARNING +} + +enum NotificationType { + UNREAD + ARCHIVE +} + +type Notifications { + id: ID! + + """A cached overview of the notifications in the system & their severity.""" + overview: NotificationOverview! + list(filter: NotificationFilter!): [Notification!]! +} + +input NotificationFilter { + importance: NotificationImportance + type: NotificationType! + offset: Int! + limit: Int! +} + +type Owner { + username: String! + url: String! + avatar: String! +} + +type VmDomain { + uuid: ID! + + """A friendly name for the vm""" + name: String + + """Current domain vm state""" + state: VmState! +} + +"""The state of a virtual machine""" +enum VmState { + NOSTATE + RUNNING + IDLE + PAUSED + SHUTDOWN + SHUTOFF + CRASHED + PMSUSPENDED +} + +type Vms { + id: ID! + domains: [VmDomain!] +} + +type Uptime { + timestamp: String +} + +type Service implements Node { + id: ID! + name: String + online: Boolean + uptime: Uptime + version: String +} + +type UserAccount { + """A unique identifier for the user""" + id: ID! + + """The name of the user""" + name: String! + + """A description of the user""" + description: String! + + """The roles of the user""" + roles: [Role!]! + + """The permissions of the user""" + permissions: [Permission!] +} + +type Query { + apiKeys: [ApiKey!]! + apiKey(id: String!): ApiKey + cloud: Cloud! + config: Config! + display: Display! + flash: Flash! + info: Info! + logFiles: [LogFile!]! + logFile(path: String!, lines: Int, startLine: Int): LogFileContent! + me: UserAccount! + network: Network! + + """Get all notifications""" + notifications: Notifications! + online: Boolean! + owner: Owner! + registration: Registration + server: Server + servers: [Server!]! + services: [Service!]! + shares: [Share!]! + vars: Vars! + vms: Vms! + parityHistory: [ParityCheck!]! + array: UnraidArray! + connect: Connect! + remoteAccess: RemoteAccess! + extraAllowedOrigins: [String!]! + docker: Docker! + disks: [Disk!]! + disk(id: String!): Disk! +} + +type Mutation { + createApiKey(input: CreateApiKeyInput!): ApiKeyWithSecret! + addRoleForApiKey(input: AddRoleForApiKeyInput!): Boolean! + removeRoleFromApiKey(input: RemoveRoleFromApiKeyInput!): Boolean! + + """Creates a new notification record""" + createNotification(input: NotificationData!): Notification! + deleteNotification(id: String!, type: NotificationType!): NotificationOverview! + + """Deletes all archived notifications on server.""" + deleteArchivedNotifications: NotificationOverview! + + """Marks a notification as archived.""" + archiveNotification(id: String!): Notification! + archiveNotifications(ids: [String!]!): NotificationOverview! + archiveAll(importance: NotificationImportance): NotificationOverview! + + """Marks a notification as unread.""" + unreadNotification(id: String!): Notification! + unarchiveNotifications(ids: [String!]!): NotificationOverview! + unarchiveAll(importance: NotificationImportance): NotificationOverview! + + """Reads each notification to recompute & update the overview.""" + recalculateOverview: NotificationOverview! + array: ArrayMutations! + docker: DockerMutations! + + """Start a virtual machine""" + startVm(id: String!): Boolean! + + """Stop a virtual machine""" + stopVm(id: String!): Boolean! + + """Pause a virtual machine""" + pauseVm(id: String!): Boolean! + + """Resume a virtual machine""" + resumeVm(id: String!): Boolean! + + """Force stop a virtual machine""" + forceStopVm(id: String!): Boolean! + + """Reboot a virtual machine""" + rebootVm(id: String!): Boolean! + + """Reset a virtual machine""" + resetVm(id: String!): Boolean! + startParityCheck(correct: Boolean!): JSON! + pauseParityCheck: JSON! + resumeParityCheck: JSON! + cancelParityCheck: JSON! + updateApiSettings(input: ApiSettingsInput!): ConnectSettingsValues! + connectSignIn(input: ConnectSignInInput!): Boolean! + connectSignOut: Boolean! + setupRemoteAccess(input: SetupRemoteAccessInput!): Boolean! + setAdditionalAllowedOrigins(input: AllowedOriginInput!): [String!]! + enableDynamicRemoteAccess(input: EnableDynamicRemoteAccessInput!): Boolean! +} + +input CreateApiKeyInput { + name: String! + description: String + roles: [Role!] + permissions: [AddPermissionInput!] + + """ + This will replace the existing key if one already exists with the same name, otherwise returns the existing key + """ + overwrite: Boolean +} + +input AddPermissionInput { + resource: Resource! + actions: [String!]! +} + +input AddRoleForApiKeyInput { + apiKeyId: ID! + role: Role! +} + +input RemoveRoleFromApiKeyInput { + apiKeyId: ID! + role: Role! +} + +input NotificationData { + title: String! + subject: String! + description: String! + importance: NotificationImportance! + link: String +} + +input ApiSettingsInput { + """ + If true, the GraphQL sandbox will be enabled and available at /graphql. If false, the GraphQL sandbox will be disabled and only the production API will be available. + """ + sandbox: Boolean + + """A list of origins allowed to interact with the API""" + extraOrigins: [String!] + + """The type of WAN access to use for Remote Access""" + accessType: WAN_ACCESS_TYPE + + """The type of port forwarding to use for Remote Access""" + forwardType: WAN_FORWARD_TYPE + + """ + The port to use for Remote Access. Not required for UPNP forwardType. Required for STATIC forwardType. Ignored if accessType is DISABLED or forwardType is UPNP. + """ + port: Int + + """A list of Unique Unraid Account ID's""" + ssoUserIds: [String!] +} + +input ConnectSignInInput { + """The API key for authentication""" + apiKey: String! + + """The ID token for authentication""" + idToken: String + + """User information for the sign-in""" + userInfo: ConnectUserInfoInput + + """The access token for authentication""" + accessToken: String + + """The refresh token for authentication""" + refreshToken: String +} + +input ConnectUserInfoInput { + """The preferred username of the user""" + preferred_username: String! + + """The email address of the user""" + email: String! + + """The avatar URL of the user""" + avatar: String +} + +input SetupRemoteAccessInput { + """The type of WAN access to use for Remote Access""" + accessType: WAN_ACCESS_TYPE! + + """The type of port forwarding to use for Remote Access""" + forwardType: WAN_FORWARD_TYPE + + """ + The port to use for Remote Access. Not required for UPNP forwardType. Required for STATIC forwardType. Ignored if accessType is DISABLED or forwardType is UPNP. + """ + port: Int +} + +input AllowedOriginInput { + """A list of origins allowed to interact with the API""" + origins: [String!]! +} + +input EnableDynamicRemoteAccessInput { + """The URL for dynamic remote access""" + url: URL! + + """Whether to enable or disable dynamic remote access""" + enabled: Boolean! +} + +type Subscription { + displaySubscription: Display! + infoSubscription: Info! + logFile(path: String!): LogFileContent! + notificationAdded: Notification! + notificationsOverview: NotificationOverview! + ownerSubscription: Owner! + registrationSubscription: Registration! + serversSubscription: Server! + parityHistorySubscription: ParityCheck! + arraySubscription: UnraidArray! +} \ No newline at end of file diff --git a/api/generated-schema.graphql b/api/generated-schema.graphql index be62618b9..6fd788d0f 100644 --- a/api/generated-schema.graphql +++ b/api/generated-schema.graphql @@ -1,1199 +1,550 @@ -directive @auth(action: AuthActionVerb!, possession: AuthPossession!, resource: Resource!) on FIELD_DEFINITION - -type AccessUrl { - ipv4: URL - ipv6: URL - name: String - type: URL_TYPE! -} - -input AccessUrlInput { - ipv4: URL - ipv6: URL - name: String - type: URL_TYPE! -} - -input AddPermissionInput { - actions: [String!]! - resource: Resource! -} - -input AddRoleForApiKeyInput { - apiKeyId: ID! - role: Role! -} - -input AddRoleForUserInput { - role: Role! - userId: ID! -} - -input AllowedOriginInput { - origins: [String!]! -} - -type ApiKey { - createdAt: DateTime! - description: String - id: ID! - name: String! - permissions: [Permission!]! - roles: [Role!]! -} +# ------------------------------------------------------ +# THIS FILE WAS AUTOMATICALLY GENERATED (DO NOT MODIFY) +# ------------------------------------------------------ type ApiKeyResponse { - error: String valid: Boolean! + error: String } -type ApiKeyWithSecret { - createdAt: DateTime! - description: String - id: ID! - key: String! - name: String! - permissions: [Permission!]! - roles: [Role!]! +type MinigraphqlResponse { + status: MinigraphStatus! + timeout: Int + error: String } -""" -Input should be a subset of ApiSettings that can be updated. -Some field combinations may be required or disallowed. Please refer to each field for more information. -""" -input ApiSettingsInput { - """The type of WAN access to use for Remote Access.""" - accessType: WAN_ACCESS_TYPE - - """A list of origins allowed to interact with the API.""" - extraOrigins: [String!] - - """The type of port forwarding to use for Remote Access.""" - forwardType: WAN_FORWARD_TYPE - - """ - The port to use for Remote Access. Not required for UPNP forwardType. Required for STATIC forwardType. - Ignored if accessType is DISABLED or forwardType is UPNP. - """ - port: Port - - """ - If true, the GraphQL sandbox will be enabled and available at /graphql. - If false, the GraphQL sandbox will be disabled and only the production API will be available. - """ - sandbox: Boolean - - """A list of Unique Unraid Account ID's.""" - ssoUserIds: [String!] +enum MinigraphStatus { + PRE_INIT + CONNECTING + CONNECTED + PING_FAILURE + ERROR_RETRYING } -type Array implements Node { - """Current boot disk""" - boot: ArrayDisk +type CloudResponse { + status: String! + ip: String + error: String +} - """Caches in the current array""" - caches: [ArrayDisk!]! +type RelayResponse { + status: String! + timeout: String + error: String +} - """Current array capacity""" - capacity: ArrayCapacity! +type Cloud { + error: String + apiKey: ApiKeyResponse! + relay: RelayResponse + minigraphql: MinigraphqlResponse! + cloud: CloudResponse! + allowedOrigins: [String!]! +} - """Data disks in the current array""" - disks: [ArrayDisk!]! - id: ID! +type Capacity { + """Free capacity""" + free: String! - """Parity disks in the current array""" - parities: [ArrayDisk!]! + """Used capacity""" + used: String! - """Array state after this query/mutation""" - pendingState: ArrayPendingState - - """Array state before this query/mutation""" - previousState: ArrayState - - """Current array state""" - state: ArrayState! + """Total capacity""" + total: String! } type ArrayCapacity { - disks: Capacity! + """Capacity in kilobytes""" kilobytes: Capacity! + + """Capacity in number of disks""" + disks: Capacity! } -type ArrayDisk { - color: ArrayDiskFsColor - - """ User comment on disk """ - comment: String - - """ (%) Disk space left for critical """ - critical: Int - device: String - exportable: Boolean - - """ File format (ex MBR: 4KiB-aligned) """ - format: String - - """ (KB) Free Size on the FS (Not present on Parity type drive)""" - fsFree: Long - - """ (KB) Total Size of the FS (Not present on Parity type drive) """ - fsSize: Long - - """ File system type for the disk """ - fsType: String - - """ (KB) Used Size on the FS (Not present on Parity type drive)""" - fsUsed: Long - - """ Disk indentifier, only set for present disks on the system """ +type ArrayDisk implements Node { + """Disk identifier, only set for present disks on the system""" id: ID! - """ Array slot number. Parity1 is always 0 and Parity2 is always 29. Array slots will be 1 - 28. Cache slots are 30 - 53. Flash is 54. + """ + Array slot number. Parity1 is always 0 and Parity2 is always 29. Array slots will be 1 - 28. Cache slots are 30 - 53. Flash is 54. """ idx: Int! name: String + device: String - """ - Number of unrecoverable errors reported by the device I/O drivers. Missing data due to unrecoverable array read errors is filled in on-the-fly using parity reconstruct (and we attempt to write this data back to the sector(s) which failed). Any unrecoverable write error results in disabling the disk. - """ - numErrors: Long! + """(KB) Disk Size total""" + size: Long + status: ArrayDiskStatus + + """Is the disk a HDD or SSD.""" + rotational: Boolean + + """Disk temp - will be NaN if array is not started or DISK_NP""" + temp: Int """ Count of I/O read requests sent to the device I/O drivers. These statistics may be cleared at any time. """ - numReads: Long! + numReads: Long """ Count of I/O writes requests sent to the device I/O drivers. These statistics may be cleared at any time. """ - numWrites: Long! + numWrites: Long - """ Is the disk a HDD or SSD. """ - rotational: Boolean + """ + Number of unrecoverable errors reported by the device I/O drivers. Missing data due to unrecoverable array read errors is filled in on-the-fly using parity reconstruct (and we attempt to write this data back to the sector(s) which failed). Any unrecoverable write error results in disabling the disk. + """ + numErrors: Long - """ (KB) Disk Size total """ - size: Long! - status: ArrayDiskStatus + """(KB) Total Size of the FS (Not present on Parity type drive)""" + fsSize: Long - """ Disk temp - will be NaN if array is not started or DISK_NP """ - temp: Int + """(KB) Free Size on the FS (Not present on Parity type drive)""" + fsFree: Long - """ ata | nvme | usb | (others)""" - transport: String + """(KB) Used Size on the FS (Not present on Parity type drive)""" + fsUsed: Long + exportable: Boolean - """ Type of Disk - used to differentiate Cache / Flash / Array / Parity """ + """Type of Disk - used to differentiate Cache / Flash / Array / Parity""" type: ArrayDiskType! - """ (%) Disk space left to warn """ + """(%) Disk space left to warn""" warning: Int -} -enum ArrayDiskFsColor { - """New device, in standby mode (spun-down)""" - blue_blink - - """New device""" - blue_on - - """Device is in standby mode (spun-down)""" - green_blink - - """Normal operation, device is active""" - green_on - - """Device not present""" - grey_off - - """ - Device is missing (disabled) or contents emulated / Parity device is missing - """ - red_off - - """Device is disabled or contents emulated / Parity device is disabled""" - red_on - - """ - Device contents invalid or emulated / Parity is invalid, in standby mode (spun-down) - """ - yellow_blink - - """Device contents invalid or emulated / Parity is invalid""" - yellow_on -} - -input ArrayDiskInput { - """Disk ID""" - id: ID! - - """The slot for the disk""" - slot: Int -} - -enum ArrayDiskStatus { - """ disabled, old disk still present """ - DISK_DSBL - - """ disabled, new disk present """ - DISK_DSBL_NEW - - """ enabled, disk present, but not valid """ - DISK_INVALID - - """ new disk """ - DISK_NEW - - """ no disk present, no disk configured """ - DISK_NP - - """ disabled, no disk present """ - DISK_NP_DSBL - - """ enabled, but missing """ - DISK_NP_MISSING - - """ enabled, disk present, correct, valid """ - DISK_OK - - """ enablled, disk present, but not correct disk """ - DISK_WRONG -} - -enum ArrayDiskType { - """Cache disk""" - Cache - - """Data disk""" - Data - - """Flash disk""" - Flash - - """Parity disk""" - Parity -} - -type ArrayMutations { - """Add new disk to array""" - addDiskToArray(input: ArrayDiskInput): Array - clearArrayDiskStatistics(id: ID!): JSON - mountArrayDisk(id: ID!): Disk - - """ - Remove existing disk from array. NOTE: The array must be stopped before running this otherwise it'll throw an error. - """ - removeDiskFromArray(input: ArrayDiskInput): Array - - """Set array state""" - setState(input: ArrayStateInput): Array - unmountArrayDisk(id: ID!): Disk -} - -enum ArrayPendingState { - """Array has no data disks""" - no_data_disks - - """Array is starting""" - starting - - """Array is stopping""" - stopping - - """Array has too many missing data disks""" - too_many_missing_disks -} - -enum ArrayState { - """A disk is disabled in the array""" - DISABLE_DISK - - """Too many changes to array at the same time""" - INVALID_EXPANSION - - """Array has new disks""" - NEW_ARRAY - - """Array has new disks they're too small""" - NEW_DISK_TOO_SMALL - - """Array has no data disks""" - NO_DATA_DISKS - - """Parity isn't the biggest, can't start array""" - PARITY_NOT_BIGGEST - - """A disk is being reconstructed""" - RECON_DISK - - """Array is running""" - STARTED - - """Array has stopped""" - STOPPED - - """Array is disabled""" - SWAP_DSBL - - """Array has too many missing data disks""" - TOO_MANY_MISSING_DISKS -} - -input ArrayStateInput { - """Array state""" - desiredState: ArrayStateInputState! -} - -enum ArrayStateInputState { - """Start array""" - START - - """Stop array""" - STOP -} - -"""Available authentication action verbs""" -enum AuthActionVerb { - CREATE - DELETE - READ - UPDATE -} - -"""Available authentication possession types""" -enum AuthPossession { - ANY - OWN - OWN_ANY -} - -type Baseboard { - assetTag: String - manufacturer: String! - model: String - serial: String - version: String -} - -type Capacity { - free: String! - total: String! - used: String! -} - -type Case { - base64: String - error: String - icon: String - url: String -} - -type Cloud { - allowedOrigins: [String!]! - apiKey: ApiKeyResponse! - cloud: CloudResponse! - error: String - minigraphql: MinigraphqlResponse! - relay: RelayResponse -} - -type CloudResponse { - error: String - ip: String - status: String! -} - -type Config implements Node { - error: ConfigErrorState - id: ID! - valid: Boolean -} - -enum ConfigErrorState { - INELIGIBLE - INVALID - NO_KEY_SERVER - UNKNOWN_ERROR - WITHDRAWN -} - -type Connect implements Node { - dynamicRemoteAccess: DynamicRemoteAccessStatus! - id: ID! - settings: ConnectSettings! -} - -type ConnectSettings implements Node { - dataSchema: JSON! - id: ID! - uiSchema: JSON! - values: ConnectSettingsValues! -} - -"""Intersection type of ApiSettings and RemoteAccess""" -type ConnectSettingsValues { - """The type of WAN access used for Remote Access.""" - accessType: WAN_ACCESS_TYPE! - - """A list of origins allowed to interact with the API.""" - extraOrigins: [String!]! - - """The type of port forwarding used for Remote Access.""" - forwardType: WAN_FORWARD_TYPE - - """The port used for Remote Access.""" - port: Port - - """ - If true, the GraphQL sandbox is enabled and available at /graphql. - If false, the GraphQL sandbox is disabled and only the production API will be available. - """ - sandbox: Boolean! - - """A list of Unique Unraid Account ID's.""" - ssoUserIds: [String!]! -} - -input ConnectSignInInput { - accessToken: String - apiKey: String! - idToken: String - refreshToken: String - userInfo: ConnectUserInfoInput -} - -input ConnectUserInfoInput { - avatar: String - email: String! - preferred_username: String! -} - -type ContainerHostConfig { - networkMode: String! -} - -type ContainerMount { - destination: String! - driver: String! - mode: String! - name: String! - propagation: String! - rw: Boolean! - source: String! - type: String! -} - -type ContainerPort { - ip: String - privatePort: Int - publicPort: Int - type: ContainerPortType -} - -enum ContainerPortType { - TCP - UDP -} - -enum ContainerState { - EXITED - RUNNING -} - -input CreateApiKeyInput { - description: String - name: String! - - """ This will replace the existing key if one already exists with the same name, otherwise returns the existing key - """ - overwrite: Boolean - permissions: [AddPermissionInput!] - roles: [Role!] -} - -scalar DateTime - -type Devices { - gpu: [Gpu] - network: [Network] - pci: [Pci] - usb: [Usb] -} - -type Disk { - bytesPerSector: Long! - device: String! - firmwareRevision: String! - id: ID! - interfaceType: DiskInterfaceType! - name: String! - partitions: [DiskPartition!] - sectorsPerTrack: Long! - serialNum: String! - size: Long! - smartStatus: DiskSmartStatus! - temperature: Long - totalCylinders: Long! - totalHeads: Long! - totalSectors: Long! - totalTracks: Long! - tracksPerCylinder: Long! - type: String! - vendor: String! -} - -enum DiskFsType { - btrfs - ext4 - ntfs - vfat - xfs - zfs -} - -enum DiskInterfaceType { - PCIe - SAS - SATA - UNKNOWN - USB -} - -type DiskPartition { - fsType: DiskFsType! - name: String! - size: Long! -} - -enum DiskSmartStatus { - OK - UNKNOWN -} - -type Display { - banner: String - case: Case + """(%) Disk space left for critical""" critical: Int - dashapps: String - date: String - hot: Int - id: ID! - locale: String - max: Int - number: String - resize: Boolean - scale: Boolean - tabs: Boolean - text: Boolean - theme: Theme - total: Boolean - unit: Temperature - usage: Boolean - users: String - warning: Int - wwn: Boolean -} -type Docker implements Node { - containers: [DockerContainer!] - id: ID! - networks: [DockerNetwork!] -} + """File system type for the disk""" + fsType: String -type DockerContainer { - autoStart: Boolean! - command: String! - created: Int! - hostConfig: ContainerHostConfig - id: ID! - image: String! - imageId: String! - labels: JSON - mounts: [JSON] - names: [String!] - networkSettings: JSON - ports: [ContainerPort!]! + """User comment on disk""" + comment: String - """ (B) Total size of all the files in the container """ - sizeRootFs: Long - state: ContainerState! - status: String! -} + """File format (ex MBR: 4KiB-aligned)""" + format: String -type DockerMutations { - """ Start a container """ - start(id: ID!): DockerContainer! - - """ Stop a container """ - stop(id: ID!): DockerContainer! -} - -type DockerNetwork { - attachable: Boolean! - configFrom: JSON - configOnly: Boolean! - containers: JSON - created: String - driver: String - enableIPv6: Boolean! - id: ID - ingress: Boolean! - internal: Boolean! - ipam: JSON - labels: JSON - name: String - options: JSON - scope: String -} - -type DynamicRemoteAccessStatus { - enabledType: DynamicRemoteAccessType! - error: String - runningType: DynamicRemoteAccessType! -} - -enum DynamicRemoteAccessType { - DISABLED - STATIC - UPNP -} - -input EnableDynamicRemoteAccessInput { - enabled: Boolean! - url: AccessUrlInput! -} - -type Flash { - guid: String - product: String - vendor: String -} - -type Gpu { - blacklisted: Boolean! - class: String! - id: ID! - productid: String! - type: String! - typeid: String! - vendorname: String! -} - -enum Importance { - ALERT - INFO - WARNING -} - -type Info implements Node { - """Count of docker containers""" - apps: InfoApps - baseboard: Baseboard - cpu: InfoCpu - devices: Devices - display: Display - id: ID! - - """Machine ID""" - machineId: ID - memory: InfoMemory - os: Os - system: System - time: DateTime! - versions: Versions -} - -type InfoApps { - """How many docker containers are installed""" - installed: Int - - """How many docker containers are running""" - started: Int -} - -type InfoCpu { - brand: String! - cache: JSON! - cores: Int! - family: String! - flags: [String!] - manufacturer: String! - model: String! - processors: Long! - revision: String! - socket: String! - speed: Float! - speedmax: Float! - speedmin: Float! - stepping: Int! - threads: Int! - vendor: String! - voltage: String -} - -type InfoMemory { - active: Long! - available: Long! - buffcache: Long! - free: Long! - layout: [MemoryLayout!] - max: Long! - swapfree: Long! - swaptotal: Long! - swapused: Long! - total: Long! - used: Long! -} - -scalar JSON - -type KeyFile { - contents: String - location: String -} - -"""Represents a log file in the system""" -type LogFile { - """Last modified timestamp""" - modifiedAt: DateTime! - - """Name of the log file""" - name: String! - - """Full path to the log file""" - path: String! - - """Size of the log file in bytes""" - size: Int! -} - -"""Content of a log file""" -type LogFileContent { - """Content of the log file""" - content: String! - - """Path to the log file""" - path: String! - - """Starting line number of the content (1-indexed)""" - startLine: Int - - """Total number of lines in the file""" - totalLines: Int! -} - -scalar Long - -"""The current user""" -type Me implements UserAccount { - description: String! - id: ID! - name: String! - permissions: [Permission!] - roles: [Role!]! -} - -enum MemoryFormFactor { - DIMM -} - -type MemoryLayout { - bank: String - clockSpeed: Long - formFactor: MemoryFormFactor - manufacturer: String - partNum: String - serialNum: String - size: Long! - type: MemoryType - voltageConfigured: Long - voltageMax: Long - voltageMin: Long -} - -enum MemoryType { - DDR2 - DDR3 - DDR4 -} - -enum MinigraphStatus { - CONNECTED - CONNECTING - ERROR_RETRYING - PING_FAILURE - PRE_INIT -} - -type MinigraphqlResponse { - error: String - status: MinigraphStatus! - timeout: Int -} - -type Mount { - directory: String - name: String - permissions: String - type: String -} - -type Mutation { - addPermission(input: AddPermissionInput!): Boolean! - addRoleForApiKey(input: AddRoleForApiKeyInput!): Boolean! - addRoleForUser(input: AddRoleForUserInput!): Boolean! - - """Add a new user""" - addUser(input: addUserInput!): User - archiveAll(importance: Importance): NotificationOverview! - - """Marks a notification as archived.""" - archiveNotification(id: String!): Notification! - archiveNotifications(ids: [String!]): NotificationOverview! - array: ArrayMutations - - """Cancel parity check""" - cancelParityCheck: JSON - connectSignIn(input: ConnectSignInInput!): Boolean! - connectSignOut: Boolean! - createApiKey(input: CreateApiKeyInput!): ApiKeyWithSecret! - createNotification(input: NotificationData!): Notification! - - """Deletes all archived notifications on server.""" - deleteArchivedNotifications: NotificationOverview! - deleteNotification(id: String!, type: NotificationType!): NotificationOverview! - - """Delete a user""" - deleteUser(input: deleteUserInput!): User - docker: DockerMutations - enableDynamicRemoteAccess(input: EnableDynamicRemoteAccessInput!): Boolean! - login(password: String!, username: String!): String - - """Pause parity check""" - pauseParityCheck: JSON - reboot: String - - """Reads each notification to recompute & update the overview.""" - recalculateOverview: NotificationOverview! - removeRoleFromApiKey(input: RemoveRoleFromApiKeyInput!): Boolean! - - """Resume parity check""" - resumeParityCheck: JSON - setAdditionalAllowedOrigins(input: AllowedOriginInput!): [String!]! - setupRemoteAccess(input: SetupRemoteAccessInput!): Boolean! - shutdown: String - - """Start parity check""" - startParityCheck(correct: Boolean): JSON - unarchiveAll(importance: Importance): NotificationOverview! - unarchiveNotifications(ids: [String!]): NotificationOverview! - - """Marks a notification as unread.""" - unreadNotification(id: String!): Notification! - - """ - Update the API settings. - Some setting combinations may be required or disallowed. Please refer to each setting for more information. - """ - updateApiSettings(input: ApiSettingsInput!): ConnectSettingsValues! - - """Virtual machine mutations""" - vms: VmMutations -} - -type Network implements Node { - accessUrls: [AccessUrl!] - carrierChanges: String - duplex: String - id: ID! - iface: String - ifaceName: String - internal: String - ipv4: String - ipv6: String - mac: String - mtu: String - operstate: String - speed: String - type: String + """ata | nvme | usb | (others)""" + transport: String + color: ArrayDiskFsColor } interface Node { id: ID! } -type Notification implements Node { - description: String! - formattedTimestamp: String +"""The `Long` scalar type represents 52-bit integers""" +scalar Long + +enum ArrayDiskStatus { + DISK_NP + DISK_OK + DISK_NP_MISSING + DISK_INVALID + DISK_WRONG + DISK_DSBL + DISK_NP_DSBL + DISK_DSBL_NEW + DISK_NEW +} + +enum ArrayDiskType { + DATA + PARITY + FLASH + CACHE +} + +enum ArrayDiskFsColor { + GREEN_ON + GREEN_BLINK + BLUE_ON + BLUE_BLINK + YELLOW_ON + YELLOW_BLINK + RED_ON + RED_OFF + GREY_OFF +} + +type UnraidArray implements Node { id: ID! - importance: Importance! - link: String - subject: String! - """ISO Timestamp for when the notification occurred""" - timestamp: String + """Array state before this query/mutation""" + previousState: ArrayState - """Also known as 'event'""" - title: String! - type: NotificationType! + """Array state after this query/mutation""" + pendingState: ArrayPendingState + + """Current array state""" + state: ArrayState! + + """Current array capacity""" + capacity: ArrayCapacity! + + """Current boot disk""" + boot: ArrayDisk + + """Parity disks in the current array""" + parities: [ArrayDisk!]! + + """Data disks in the current array""" + disks: [ArrayDisk!]! + + """Caches in the current array""" + caches: [ArrayDisk!]! } -type NotificationCounts { - alert: Int! - info: Int! - total: Int! - warning: Int! +enum ArrayState { + STARTED + STOPPED + NEW_ARRAY + RECON_DISK + DISABLE_DISK + SWAP_DSBL + INVALID_EXPANSION + PARITY_NOT_BIGGEST + TOO_MANY_MISSING_DISKS + NEW_DISK_TOO_SMALL + NO_DATA_DISKS } -input NotificationData { - description: String! - importance: Importance! - link: String - subject: String! - title: String! +enum ArrayPendingState { + STARTING + STOPPING + NO_DATA_DISKS + TOO_MANY_MISSING_DISKS } -input NotificationFilter { - importance: Importance - limit: Int! - offset: Int! - type: NotificationType -} - -type NotificationOverview { - archive: NotificationCounts! - unread: NotificationCounts! -} - -enum NotificationType { - ARCHIVE - UNREAD -} - -type Notifications implements Node { +type Share implements Node { id: ID! - list(filter: NotificationFilter!): [Notification!]! - """A cached overview of the notifications in the system & their severity.""" - overview: NotificationOverview! + """Display name""" + name: String + + """(KB) Free space""" + free: Long + + """(KB) Used Size""" + used: Long + + """(KB) Total size""" + size: Long + + """Disks that are included in this share""" + include: [String!] + + """Disks that are excluded from this share""" + exclude: [String!] + + """Is this share cached""" + cache: Boolean + + """Original name""" + nameOrig: String + + """User comment""" + comment: String + + """Allocator""" + allocator: String + + """Split level""" + splitLevel: String + + """Floor""" + floor: String + + """COW""" + cow: String + + """Color""" + color: String + + """LUKS status""" + luksStatus: String } -type Os { - arch: String - build: String - codename: String - codepage: String - distro: String - hostname: String - kernel: String - logofile: String - platform: String - release: String - serial: String - uptime: DateTime +type AccessUrl { + type: URL_TYPE! + name: String + ipv4: URL + ipv6: URL } -type Owner { - avatar: String - url: String - username: String +enum URL_TYPE { + LAN + WIREGUARD + WAN + MDNS + OTHER + DEFAULT } -type ParityCheck { - date: String! - duration: Int! - errors: String! - speed: String! - status: String! +""" +A field whose value conforms to the standard URL format as specified in RFC3986: https://www.ietf.org/rfc/rfc3986.txt. +""" +scalar URL + +type RemoteAccess { + """The type of WAN access used for Remote Access""" + accessType: WAN_ACCESS_TYPE! + + """The type of port forwarding used for Remote Access""" + forwardType: WAN_FORWARD_TYPE + + """The port used for Remote Access""" + port: Int } -type Partition { - devlinks: String - devname: String - devpath: String - devtype: String - idAta: String - idAtaDownloadMicrocode: String - idAtaFeatureSetAam: String - idAtaFeatureSetAamCurrentValue: String - idAtaFeatureSetAamEnabled: String - idAtaFeatureSetAamVendorRecommendedValue: String - idAtaFeatureSetApm: String - idAtaFeatureSetApmCurrentValue: String - idAtaFeatureSetApmEnabled: String - idAtaFeatureSetHpa: String - idAtaFeatureSetHpaEnabled: String - idAtaFeatureSetPm: String - idAtaFeatureSetPmEnabled: String - idAtaFeatureSetPuis: String - idAtaFeatureSetPuisEnabled: String - idAtaFeatureSetSecurity: String - idAtaFeatureSetSecurityEnabled: String - idAtaFeatureSetSecurityEnhancedEraseUnitMin: String - idAtaFeatureSetSecurityEraseUnitMin: String - idAtaFeatureSetSmart: String - idAtaFeatureSetSmartEnabled: String - idAtaRotationRateRpm: String - idAtaSata: String - idAtaSataSignalRateGen1: String - idAtaSataSignalRateGen2: String - idAtaWriteCache: String - idAtaWriteCacheEnabled: String - idBus: String - idFsType: String - idFsUsage: String - idFsUuid: String - idFsUuidEnc: String - idModel: String - idModelEnc: String - idPartEntryDisk: String - idPartEntryNumber: String - idPartEntryOffset: String - idPartEntryScheme: String - idPartEntrySize: String - idPartEntryType: String - idPartTableType: String - idPath: String - idPathTag: String - idRevision: String - idSerial: String - idSerialShort: String - idType: String - idWwn: String - idWwnWithExtension: String - major: String - minor: String - partn: String - subsystem: String - usecInitialized: String +enum WAN_ACCESS_TYPE { + DYNAMIC + ALWAYS + DISABLED } -type Pci { - blacklisted: String - class: String +enum WAN_FORWARD_TYPE { + UPNP + STATIC +} + +type DynamicRemoteAccessStatus { + """The type of dynamic remote access that is enabled""" + enabledType: DynamicRemoteAccessType! + + """The type of dynamic remote access that is currently running""" + runningType: DynamicRemoteAccessType! + + """Any error message associated with the dynamic remote access""" + error: String +} + +enum DynamicRemoteAccessType { + STATIC + UPNP + DISABLED +} + +type ConnectSettingsValues { + """ + If true, the GraphQL sandbox is enabled and available at /graphql. If false, the GraphQL sandbox is disabled and only the production API will be available. + """ + sandbox: Boolean! + + """A list of origins allowed to interact with the API""" + extraOrigins: [String!]! + + """The type of WAN access used for Remote Access""" + accessType: WAN_ACCESS_TYPE! + + """The type of port forwarding used for Remote Access""" + forwardType: WAN_FORWARD_TYPE + + """The port used for Remote Access""" + port: Int + + """A list of Unique Unraid Account ID's""" + ssoUserIds: [String!]! +} + +type ConnectSettings implements Node { + """The unique identifier for the Connect settings""" id: ID! - productid: String - productname: String - type: String - typeid: String - vendorid: String - vendorname: String + + """The data schema for the Connect settings""" + dataSchema: JSON! + + """The UI schema for the Connect settings""" + uiSchema: JSON! + + """The values for the Connect settings""" + values: ConnectSettingsValues! } -type Permission { - actions: [String!]! - resource: Resource! +""" +The `JSON` scalar type represents JSON values as specified by [ECMA-404](http://www.ecma-international.org/publications/files/ECMA-ST/ECMA-404.pdf). +""" +scalar JSON @specifiedBy(url: "http://www.ecma-international.org/publications/files/ECMA-ST/ECMA-404.pdf") + +type Connect implements Node { + """The unique identifier for the Connect instance""" + id: ID! + + """The status of dynamic remote access""" + dynamicRemoteAccess: DynamicRemoteAccessStatus! + + """The settings for the Connect instance""" + settings: ConnectSettings! } -scalar Port +type Network implements Node { + id: ID! + accessUrls: [AccessUrl!] +} type ProfileModel { - avatar: String - url: String userId: ID - username: String + username: String! + url: String! + avatar: String! } -type Query { - apiKey(id: ID!): ApiKey - apiKeys: [ApiKey!]! +type Server { + owner: ProfileModel! + guid: String! + apikey: String! + name: String! + status: ServerStatus! + wanip: String! + lanip: String! + localurl: String! + remoteurl: String! +} - """ - An Unraid array consisting of 1 or 2 Parity disks and a number of Data disks. - """ - array: Array! - cloud: Cloud - config: Config! - connect: Connect! +enum ServerStatus { + ONLINE + OFFLINE + NEVER_CONNECTED +} - """Single disk""" - disk(id: ID!): Disk +type DiskPartition { + """The name of the partition""" + name: String! - """Mulitiple disks""" - disks: [Disk]! - display: Display - docker: Docker! + """The filesystem type of the partition""" + fsType: DiskFsType! - """Docker network""" - dockerNetwork(id: ID!): DockerNetwork! + """The size of the partition in bytes""" + size: Float! +} - """All Docker networks""" - dockerNetworks(all: Boolean): [DockerNetwork]! - extraAllowedOrigins: [String!]! - flash: Flash - info: Info +"""The type of filesystem on the disk partition""" +enum DiskFsType { + XFS + BTRFS + VFAT + ZFS + EXT4 + NTFS +} - """ - Get the content of a specific log file - @param path Path to the log file - @param lines Number of lines to read from the end of the file (default: 100) - @param startLine Optional starting line number (1-indexed) - """ - logFile(lines: Int, path: String!, startLine: Int): LogFileContent! +type Disk { + """The unique identifier of the disk""" + id: String! - """List all available log files""" - logFiles: [LogFile!]! + """The device path of the disk (e.g. /dev/sdb)""" + device: String! - """Current user account""" - me: Me - network: Network - notifications: Notifications! - online: Boolean - owner: Owner - parityHistory: [ParityCheck] - registration: Registration - remoteAccess: RemoteAccess! - server: Server - servers: [Server!]! - services: [Service!]! + """The type of disk (e.g. SSD, HDD)""" + type: String! - """Network Shares""" - shares: [Share] - unassignedDevices: [UnassignedDevice] + """The model name of the disk""" + name: String! - """User account""" - user(id: ID!): User + """The manufacturer of the disk""" + vendor: String! - """User accounts""" - users(input: usersInput): [User!]! - vars: Vars + """The total size of the disk in bytes""" + size: Float! - """Virtual machines""" - vms: Vms + """The number of bytes per sector""" + bytesPerSector: Float! + + """The total number of cylinders on the disk""" + totalCylinders: Float! + + """The total number of heads on the disk""" + totalHeads: Float! + + """The total number of sectors on the disk""" + totalSectors: Float! + + """The total number of tracks on the disk""" + totalTracks: Float! + + """The number of tracks per cylinder""" + tracksPerCylinder: Float! + + """The number of sectors per track""" + sectorsPerTrack: Float! + + """The firmware revision of the disk""" + firmwareRevision: String! + + """The serial number of the disk""" + serialNum: String! + + """The interface type of the disk""" + interfaceType: DiskInterfaceType! + + """The SMART status of the disk""" + smartStatus: DiskSmartStatus! + + """The current temperature of the disk in Celsius""" + temperature: Float + + """The partitions on the disk""" + partitions: [DiskPartition!]! +} + +"""The type of interface the disk uses to connect to the system""" +enum DiskInterfaceType { + SAS + SATA + USB + PCIE + UNKNOWN +} + +""" +The SMART (Self-Monitoring, Analysis and Reporting Technology) status of the disk +""" +enum DiskSmartStatus { + OK + UNKNOWN +} + +type KeyFile { + location: String + contents: String } type Registration { - expiration: String - guid: String + guid: ID + type: registrationType keyFile: KeyFile state: RegistrationState - type: registrationType + expiration: String updateExpiration: String } -enum RegistrationState { +enum registrationType { BASIC + PLUS + PRO + STARTER + UNLEASHED + LIFETIME + INVALID + TRIAL +} - """BLACKLISTED""" - EBLACKLISTED - - """BLACKLISTED""" - EBLACKLISTED1 - - """BLACKLISTED""" - EBLACKLISTED2 - - """Trial Expired""" +enum RegistrationState { + TRIAL + BASIC + PLUS + PRO + STARTER + UNLEASHED + LIFETIME EEXPIRED - - """GUID Error""" EGUID - - """Multiple License Keys Present""" EGUID1 - - """Trial Requires Internet Connection""" - ENOCONN - - """No Flash""" + ETRIAL + ENOKEYFILE + ENOKEYFILE1 + ENOKEYFILE2 ENOFLASH ENOFLASH1 ENOFLASH2 @@ -1202,41 +553,206 @@ enum RegistrationState { ENOFLASH5 ENOFLASH6 ENOFLASH7 - - """No Keyfile""" - ENOKEYFILE - - """No Keyfile""" - ENOKEYFILE1 - - """Missing key file""" - ENOKEYFILE2 - - """Invalid installation""" - ETRIAL - LIFETIME - PLUS - PRO - STARTER - TRIAL - UNLEASHED + EBLACKLISTED + EBLACKLISTED1 + EBLACKLISTED2 + ENOCONN } -type RelayResponse { - error: String - status: String! - timeout: String +type Vars implements Node { + id: ID! + + """Unraid version""" + version: String + maxArraysz: Int + maxCachesz: Int + + """Machine hostname""" + name: String + timeZone: String + comment: String + security: String + workgroup: String + domain: String + domainShort: String + hideDotFiles: Boolean + localMaster: Boolean + enableFruit: String + + """Should a NTP server be used for time sync?""" + useNtp: Boolean + + """NTP Server 1""" + ntpServer1: String + + """NTP Server 2""" + ntpServer2: String + + """NTP Server 3""" + ntpServer3: String + + """NTP Server 4""" + ntpServer4: String + domainLogin: String + sysModel: String + sysArraySlots: Int + sysCacheSlots: Int + sysFlashSlots: Int + useSsl: Boolean + + """Port for the webui via HTTP""" + port: Int + + """Port for the webui via HTTPS""" + portssl: Int + localTld: String + bindMgt: Boolean + + """Should telnet be enabled?""" + useTelnet: Boolean + porttelnet: Int + useSsh: Boolean + portssh: Int + startPage: String + startArray: Boolean + spindownDelay: String + queueDepth: String + spinupGroups: Boolean + defaultFormat: String + defaultFsType: String + shutdownTimeout: Int + luksKeyfile: String + pollAttributes: String + pollAttributesDefault: String + pollAttributesStatus: String + nrRequests: Int + nrRequestsDefault: Int + nrRequestsStatus: String + mdNumStripes: Int + mdNumStripesDefault: Int + mdNumStripesStatus: String + mdSyncWindow: Int + mdSyncWindowDefault: Int + mdSyncWindowStatus: String + mdSyncThresh: Int + mdSyncThreshDefault: Int + mdSyncThreshStatus: String + mdWriteMethod: Int + mdWriteMethodDefault: String + mdWriteMethodStatus: String + shareDisk: String + shareUser: String + shareUserInclude: String + shareUserExclude: String + shareSmbEnabled: Boolean + shareNfsEnabled: Boolean + shareAfpEnabled: Boolean + shareInitialOwner: String + shareInitialGroup: String + shareCacheEnabled: Boolean + shareCacheFloor: String + shareMoverSchedule: String + shareMoverLogging: Boolean + fuseRemember: String + fuseRememberDefault: String + fuseRememberStatus: String + fuseDirectio: String + fuseDirectioDefault: String + fuseDirectioStatus: String + shareAvahiEnabled: Boolean + shareAvahiSmbName: String + shareAvahiSmbModel: String + shareAvahiAfpName: String + shareAvahiAfpModel: String + safeMode: Boolean + startMode: String + configValid: Boolean + configError: ConfigErrorState + joinStatus: String + deviceCount: Int + flashGuid: String + flashProduct: String + flashVendor: String + regCheck: String + regFile: String + regGuid: String + regTy: registrationType + regState: RegistrationState + + """Registration owner""" + regTo: String + regTm: String + regTm2: String + regGen: String + sbName: String + sbVersion: String + sbUpdated: String + sbEvents: Int + sbState: String + sbClean: Boolean + sbSynced: Int + sbSyncErrs: Int + sbSynced2: Int + sbSyncExit: String + sbNumDisks: Int + mdColor: String + mdNumDisks: Int + mdNumDisabled: Int + mdNumInvalid: Int + mdNumMissing: Int + mdNumNew: Int + mdNumErased: Int + mdResync: Int + mdResyncCorr: String + mdResyncPos: String + mdResyncDb: String + mdResyncDt: String + mdResyncAction: String + mdResyncSize: Int + mdState: String + mdVersion: String + cacheNumDevices: Int + cacheSbNumDisks: Int + fsState: String + + """Human friendly string of array events happening""" + fsProgress: String + + """ + Percentage from 0 - 100 while upgrading a disk or swapping parity drives + """ + fsCopyPrcnt: Int + fsNumMounted: Int + fsNumUnmountable: Int + fsUnmountableMask: String + + """Total amount of user shares""" + shareCount: Int + + """Total amount shares with SMB enabled""" + shareSmbCount: Int + + """Total amount shares with NFS enabled""" + shareNfsCount: Int + + """Total amount shares with AFP enabled""" + shareAfpCount: Int + shareMoverActive: Boolean + csrfToken: String } -type RemoteAccess { - accessType: WAN_ACCESS_TYPE! - forwardType: WAN_FORWARD_TYPE - port: Port +"""Possible error states for configuration""" +enum ConfigErrorState { + UNKNOWN_ERROR + INELIGIBLE + INVALID + NO_KEY_SERVER + WITHDRAWN } -input RemoveRoleFromApiKeyInput { - apiKeyId: ID! - role: Role! +type Permission { + resource: Resource! + actions: [String!]! } """Available resources for permissions""" @@ -1271,6 +787,15 @@ enum Resource { WELCOME } +type ApiKey { + id: ID! + name: String! + description: String + roles: [Role!]! + createdAt: String! + permissions: [Permission!]! +} + """Available roles for API keys and users""" enum Role { ADMIN @@ -1278,22 +803,565 @@ enum Role { GUEST } -type Server { - apikey: String! - guid: String! - lanip: String! - localurl: String! +type ApiKeyWithSecret { + id: ID! name: String! - owner: ProfileModel! - remoteurl: String! - status: ServerStatus! - wanip: String! + description: String + roles: [Role!]! + createdAt: String! + permissions: [Permission!]! + key: String! } -enum ServerStatus { - never_connected - offline - online +type ArrayMutations { + """Set array state""" + setState(input: ArrayStateInput!): UnraidArray! + + """Add new disk to array""" + addDiskToArray(input: ArrayDiskInput!): UnraidArray! + + """ + Remove existing disk from array. NOTE: The array must be stopped before running this otherwise it'll throw an error. + """ + removeDiskFromArray(input: ArrayDiskInput!): UnraidArray! + + """Mount a disk in the array""" + mountArrayDisk(id: String!): ArrayDisk! + + """Unmount a disk from the array""" + unmountArrayDisk(id: String!): ArrayDisk! + + """Clear statistics for a disk in the array""" + clearArrayDiskStatistics(id: String!): Boolean! +} + +input ArrayStateInput { + """Array state""" + desiredState: ArrayStateInputState! +} + +enum ArrayStateInputState { + START + STOP +} + +input ArrayDiskInput { + """Disk ID""" + id: ID! + + """The slot for the disk""" + slot: Int +} + +type DockerMutations { + """Start a container""" + start(id: String!): DockerContainer! + + """Stop a container""" + stop(id: String!): DockerContainer! +} + +type VmMutations { + """Start a virtual machine""" + start(id: String!): Boolean! + + """Stop a virtual machine""" + stop(id: String!): Boolean! + + """Pause a virtual machine""" + pause(id: String!): Boolean! + + """Resume a virtual machine""" + resume(id: String!): Boolean! + + """Force stop a virtual machine""" + forceStop(id: String!): Boolean! + + """Reboot a virtual machine""" + reboot(id: String!): Boolean! + + """Reset a virtual machine""" + reset(id: String!): Boolean! +} + +""" +Parity check related mutations, WIP, response types and functionaliy will change +""" +type ParityCheckMutations { + """Start a parity check""" + start(correct: Boolean!): JSON! + + """Pause a parity check""" + pause: JSON! + + """Resume a parity check""" + resume: JSON! + + """Cancel a parity check""" + cancel: JSON! +} + +type ParityCheck { + """Date of the parity check""" + date: DateTime + + """Duration of the parity check in seconds""" + duration: Int + + """Speed of the parity check, in MB/s""" + speed: String + + """Status of the parity check""" + status: String + + """Number of errors during the parity check""" + errors: Int + + """Progress percentage of the parity check""" + progress: Int + + """Whether corrections are being written to parity""" + correcting: Boolean + + """Whether the parity check is paused""" + paused: Boolean + + """Whether the parity check is running""" + running: Boolean +} + +""" +A date-time string at UTC, such as 2019-12-03T09:54:33Z, compliant with the date-time format. +""" +scalar DateTime + +type Config implements Node { + id: ID! + valid: Boolean + error: String +} + +type InfoApps implements Node { + id: ID! + + """How many docker containers are installed""" + installed: Int! + + """How many docker containers are running""" + started: Int! +} + +type Baseboard implements Node { + id: ID! + manufacturer: String! + model: String + version: String + serial: String + assetTag: String +} + +type InfoCpu implements Node { + id: ID! + manufacturer: String! + brand: String! + vendor: String! + family: String! + model: String! + stepping: Int! + revision: String! + voltage: String + speed: Float! + speedmin: Float! + speedmax: Float! + threads: Int! + cores: Int! + processors: Int! + socket: String! + cache: JSON! + flags: [String!]! +} + +type Gpu implements Node { + id: ID! + type: String! + typeid: String! + vendorname: String! + productid: String! + blacklisted: Boolean! + class: String! +} + +type Pci implements Node { + id: ID! + type: String + typeid: String + vendorname: String + vendorid: String + productname: String + productid: String + blacklisted: String + class: String +} + +type Usb { + id: ID! + name: String +} + +type Devices implements Node { + id: ID! + gpu: [Gpu!]! + pci: [Pci!]! + usb: [Usb!]! +} + +type Case implements Node { + id: ID! + icon: String + url: String + error: String + base64: String +} + +type Display implements Node { + id: ID! + case: Case + date: String + number: String + scale: Boolean + tabs: Boolean + users: String + resize: Boolean + wwn: Boolean + total: Boolean + usage: Boolean + banner: String + dashapps: String + theme: Theme + text: Boolean + unit: Temperature + warning: Int + critical: Int + hot: Int + max: Int + locale: String +} + +"""Display theme""" +enum Theme { + white +} + +"""Temperature unit (Celsius or Fahrenheit)""" +enum Temperature { + C + F +} + +type MemoryLayout { + size: Int! + bank: String + type: String + clockSpeed: Int + formFactor: String + manufacturer: String + partNum: String + serialNum: String + voltageConfigured: Int + voltageMin: Int + voltageMax: Int +} + +type InfoMemory implements Node { + id: ID! + max: Int! + total: Int! + free: Int! + used: Int! + active: Int! + available: Int! + buffcache: Int! + swaptotal: Int! + swapused: Int! + swapfree: Int! + layout: [MemoryLayout!]! +} + +type Os implements Node { + id: ID! + platform: String + distro: String + release: String + codename: String + kernel: String + arch: String + hostname: String + codepage: String + logofile: String + serial: String + build: String + uptime: String +} + +type System implements Node { + id: ID! + manufacturer: String + model: String + version: String + serial: String + uuid: String + sku: String +} + +type Versions implements Node { + id: ID! + kernel: String + openssl: String + systemOpenssl: String + systemOpensslLib: String + node: String + v8: String + npm: String + yarn: String + pm2: String + gulp: String + grunt: String + git: String + tsc: String + mysql: String + redis: String + mongodb: String + apache: String + nginx: String + php: String + docker: String + postfix: String + postgresql: String + perl: String + python: String + gcc: String + unraid: String +} + +type Info implements Node { + id: ID! + + """Count of docker containers""" + apps: InfoApps! + baseboard: Baseboard! + cpu: InfoCpu! + devices: Devices! + display: Display! + + """Machine ID""" + machineId: ID + memory: InfoMemory! + os: Os! + system: System! + time: DateTime! + versions: Versions! +} + +type ContainerPort { + ip: String + privatePort: Port + publicPort: Port + type: ContainerPortType! +} + +""" +A field whose value is a valid TCP port within the range of 0 to 65535: https://en.wikipedia.org/wiki/Transmission_Control_Protocol#TCP_ports +""" +scalar Port + +enum ContainerPortType { + TCP + UDP +} + +type ContainerHostConfig { + networkMode: String! +} + +type DockerContainer { + id: ID! + names: [String!]! + image: String! + imageId: String! + command: String! + created: Int! + ports: [ContainerPort!]! + + """Total size of all the files in the container""" + sizeRootFs: Int + labels: JSONObject + state: ContainerState! + status: String! + hostConfig: ContainerHostConfig + networkSettings: JSONObject + mounts: [JSONObject!] + autoStart: Boolean! +} + +""" +The `JSONObject` scalar type represents JSON objects as specified by [ECMA-404](http://www.ecma-international.org/publications/files/ECMA-ST/ECMA-404.pdf). +""" +scalar JSONObject + +enum ContainerState { + RUNNING + EXITED +} + +type DockerNetwork { + name: String! + id: ID! + created: String! + scope: String! + driver: String! + enableIPv6: Boolean! + ipam: JSONObject! + internal: Boolean! + attachable: Boolean! + ingress: Boolean! + configFrom: JSONObject! + configOnly: Boolean! + containers: JSONObject! + options: JSONObject! + labels: JSONObject! +} + +type Docker implements Node { + id: ID! + containers: [DockerContainer!]! + networks: [DockerNetwork!]! +} + +type Flash implements Node { + id: ID! + guid: String! + vendor: String! + product: String! +} + +type LogFile { + """Name of the log file""" + name: String! + + """Full path to the log file""" + path: String! + + """Size of the log file in bytes""" + size: Int! + + """Last modified timestamp""" + modifiedAt: DateTime! +} + +type LogFileContent { + """Path to the log file""" + path: String! + + """Content of the log file""" + content: String! + + """Total number of lines in the file""" + totalLines: Int! + + """Starting line number of the content (1-indexed)""" + startLine: Int +} + +type NotificationCounts { + info: Int! + warning: Int! + alert: Int! + total: Int! +} + +type NotificationOverview { + unread: NotificationCounts! + archive: NotificationCounts! +} + +type Notification { + id: ID! + + """Also known as 'event'""" + title: String! + subject: String! + description: String! + importance: NotificationImportance! + link: String + type: NotificationType! + + """ISO Timestamp for when the notification occurred""" + timestamp: String + formattedTimestamp: String +} + +enum NotificationImportance { + ALERT + INFO + WARNING +} + +enum NotificationType { + UNREAD + ARCHIVE +} + +type Notifications { + id: ID! + + """A cached overview of the notifications in the system & their severity.""" + overview: NotificationOverview! + list(filter: NotificationFilter!): [Notification!]! +} + +input NotificationFilter { + importance: NotificationImportance + type: NotificationType! + offset: Int! + limit: Int! +} + +type Owner { + username: String! + url: String! + avatar: String! +} + +type VmDomain { + uuid: ID! + + """A friendly name for the vm""" + name: String + + """Current domain vm state""" + state: VmState! +} + +"""The state of a virtual machine""" +enum VmState { + NOSTATE + RUNNING + IDLE + PAUSED + SHUTDOWN + SHUTOFF + CRASHED + PMSUSPENDED +} + +type Vms { + id: ID! + domains: [VmDomain!] + domain: [VmDomain!] +} + +type Uptime { + timestamp: String } type Service implements Node { @@ -1304,496 +1372,222 @@ type Service implements Node { version: String } -input SetupRemoteAccessInput { - accessType: WAN_ACCESS_TYPE! - forwardType: WAN_FORWARD_TYPE - port: Port +type UserAccount { + """A unique identifier for the user""" + id: ID! + + """The name of the user""" + name: String! + + """A description of the user""" + description: String! + + """The roles of the user""" + roles: [Role!]! + + """The permissions of the user""" + permissions: [Permission!] } -"""Network Share""" -type Share { - allocator: String - cache: Boolean - color: String +type Query { + apiKeys: [ApiKey!]! + apiKey(id: String!): ApiKey + cloud: Cloud! + config: Config! + display: Display! + flash: Flash! + info: Info! + logFiles: [LogFile!]! + logFile(path: String!, lines: Int, startLine: Int): LogFileContent! + me: UserAccount! + network: Network! - """User comment""" - comment: String - cow: String + """Get all notifications""" + notifications: Notifications! + online: Boolean! + owner: Owner! + registration: Registration + server: Server + servers: [Server!]! + services: [Service!]! + shares: [Share!]! + vars: Vars! + vms: Vms! + parityHistory: [ParityCheck!]! + array: UnraidArray! + connect: Connect! + remoteAccess: RemoteAccess! + extraAllowedOrigins: [String!]! + docker: Docker! + disks: [Disk!]! + disk(id: String!): Disk! +} - """Disks that're excluded from this share""" - exclude: [String] - floor: String +type Mutation { + createApiKey(input: CreateApiKeyInput!): ApiKeyWithSecret! + addRoleForApiKey(input: AddRoleForApiKeyInput!): Boolean! + removeRoleFromApiKey(input: RemoveRoleFromApiKeyInput!): Boolean! - """(KB) Free space""" - free: Long + """Creates a new notification record""" + createNotification(input: NotificationData!): Notification! + deleteNotification(id: String!, type: NotificationType!): NotificationOverview! - """Disks that're included in this share""" - include: [String] - luksStatus: String + """Deletes all archived notifications on server.""" + deleteArchivedNotifications: NotificationOverview! - """Display name""" + """Marks a notification as archived.""" + archiveNotification(id: String!): Notification! + archiveNotifications(ids: [String!]!): NotificationOverview! + archiveAll(importance: NotificationImportance): NotificationOverview! + + """Marks a notification as unread.""" + unreadNotification(id: String!): Notification! + unarchiveNotifications(ids: [String!]!): NotificationOverview! + unarchiveAll(importance: NotificationImportance): NotificationOverview! + + """Reads each notification to recompute & update the overview.""" + recalculateOverview: NotificationOverview! + array: ArrayMutations! + docker: DockerMutations! + vm: VmMutations! + parityCheck: ParityCheckMutations! + updateApiSettings(input: ApiSettingsInput!): ConnectSettingsValues! + connectSignIn(input: ConnectSignInInput!): Boolean! + connectSignOut: Boolean! + setupRemoteAccess(input: SetupRemoteAccessInput!): Boolean! + setAdditionalAllowedOrigins(input: AllowedOriginInput!): [String!]! + enableDynamicRemoteAccess(input: EnableDynamicRemoteAccessInput!): Boolean! +} + +input CreateApiKeyInput { + name: String! + description: String + roles: [Role!] + permissions: [AddPermissionInput!] + + """ + This will replace the existing key if one already exists with the same name, otherwise returns the existing key + """ + overwrite: Boolean +} + +input AddPermissionInput { + resource: Resource! + actions: [String!]! +} + +input AddRoleForApiKeyInput { + apiKeyId: ID! + role: Role! +} + +input RemoveRoleFromApiKeyInput { + apiKeyId: ID! + role: Role! +} + +input NotificationData { + title: String! + subject: String! + description: String! + importance: NotificationImportance! + link: String +} + +input ApiSettingsInput { + """ + If true, the GraphQL sandbox will be enabled and available at /graphql. If false, the GraphQL sandbox will be disabled and only the production API will be available. + """ + sandbox: Boolean + + """A list of origins allowed to interact with the API""" + extraOrigins: [String!] + + """The type of WAN access to use for Remote Access""" + accessType: WAN_ACCESS_TYPE + + """The type of port forwarding to use for Remote Access""" + forwardType: WAN_FORWARD_TYPE + + """ + The port to use for Remote Access. Not required for UPNP forwardType. Required for STATIC forwardType. Ignored if accessType is DISABLED or forwardType is UPNP. + """ + port: Int + + """A list of Unique Unraid Account ID's""" + ssoUserIds: [String!] +} + +input ConnectSignInInput { + """The API key for authentication""" + apiKey: String! + + """The ID token for authentication""" + idToken: String + + """User information for the sign-in""" + userInfo: ConnectUserInfoInput + + """The access token for authentication""" + accessToken: String + + """The refresh token for authentication""" + refreshToken: String +} + +input ConnectUserInfoInput { + """The preferred username of the user""" + preferred_username: String! + + """The email address of the user""" + email: String! + + """The avatar URL of the user""" + avatar: String +} + +input SetupRemoteAccessInput { + """The type of WAN access to use for Remote Access""" + accessType: WAN_ACCESS_TYPE! + + """The type of port forwarding to use for Remote Access""" + forwardType: WAN_FORWARD_TYPE + + """ + The port to use for Remote Access. Not required for UPNP forwardType. Required for STATIC forwardType. Ignored if accessType is DISABLED or forwardType is UPNP. + """ + port: Int +} + +input AllowedOriginInput { + """A list of origins allowed to interact with the API""" + origins: [String!]! +} + +input EnableDynamicRemoteAccessInput { + """The AccessURL Input for dynamic remote access""" + url: AccessUrlInput! + + """Whether to enable or disable dynamic remote access""" + enabled: Boolean! +} + +input AccessUrlInput { + type: URL_TYPE! name: String - nameOrig: String - - """(KB) Total size""" - size: Long - splitLevel: String - - """(KB) Used Size""" - used: Long + ipv4: URL + ipv6: URL } type Subscription { - array: Array! - config: Config! - display: Display - dockerNetwork(id: ID!): DockerNetwork! - dockerNetworks: [DockerNetwork]! - flash: Flash! - info: Info! - - """ - Subscribe to changes in a log file - @param path Path to the log file - """ + displaySubscription: Display! + infoSubscription: Info! logFile(path: String!): LogFileContent! - me: Me notificationAdded: Notification! notificationsOverview: NotificationOverview! - online: Boolean! - owner: Owner! - parityHistory: ParityCheck! - ping: String! - registration: Registration! - server: Server - service(name: String!): [Service!] - share(id: ID!): Share! - shares: [Share!] - unassignedDevices: [UnassignedDevice!] - user(id: ID!): User! - users: [User]! - vars: Vars! - vms: Vms -} - -type System { - manufacturer: String - model: String - serial: String - sku: String - uuid: String - version: String -} - -enum Temperature { - C - F -} - -enum Theme { - white -} - -scalar URL - -enum URL_TYPE { - DEFAULT - LAN - MDNS - OTHER - WAN - WIREGUARD -} - -scalar UUID - -type UnassignedDevice { - devlinks: String - devname: String - devpath: String - devtype: String - idAta: String - idAtaDownloadMicrocode: String - idAtaFeatureSetAam: String - idAtaFeatureSetAamCurrentValue: String - idAtaFeatureSetAamEnabled: String - idAtaFeatureSetAamVendorRecommendedValue: String - idAtaFeatureSetApm: String - idAtaFeatureSetApmCurrentValue: String - idAtaFeatureSetApmEnabled: String - idAtaFeatureSetHpa: String - idAtaFeatureSetHpaEnabled: String - idAtaFeatureSetPm: String - idAtaFeatureSetPmEnabled: String - idAtaFeatureSetPuis: String - idAtaFeatureSetPuisEnabled: String - idAtaFeatureSetSecurity: String - idAtaFeatureSetSecurityEnabled: String - idAtaFeatureSetSecurityEnhancedEraseUnitMin: String - idAtaFeatureSetSecurityEraseUnitMin: String - idAtaFeatureSetSmart: String - idAtaFeatureSetSmartEnabled: String - idAtaRotationRateRpm: String - idAtaSata: String - idAtaSataSignalRateGen1: String - idAtaSataSignalRateGen2: String - idAtaWriteCache: String - idAtaWriteCacheEnabled: String - idBus: String - idModel: String - idModelEnc: String - idPartTableType: String - idPath: String - idPathTag: String - idRevision: String - idSerial: String - idSerialShort: String - idType: String - idWwn: String - idWwnWithExtension: String - major: String - minor: String - mount: Mount - mounted: Boolean - name: String - partitions: [Partition] - subsystem: String - temp: Int - usecInitialized: String -} - -type Uptime { - timestamp: String -} - -type Usb { - id: ID! - name: String -} - -"""A local user account""" -type User implements UserAccount { - description: String! - id: ID! - - """A unique name for the user""" - name: String! - - """If the account has a password set""" - password: Boolean - permissions: [Permission!] - roles: [Role!]! -} - -interface UserAccount { - description: String! - id: ID! - name: String! - permissions: [Permission!] - roles: [Role!]! -} - -type Vars implements Node { - bindMgt: Boolean - cacheNumDevices: Int - cacheSbNumDisks: Int - comment: String - configError: ConfigErrorState - configValid: Boolean - csrfToken: String - defaultFormat: String - defaultFsType: String - deviceCount: Int - domain: String - domainLogin: String - domainShort: String - enableFruit: String - flashGuid: String - flashProduct: String - flashVendor: String - - """ - Percentage from 0 - 100 while upgrading a disk or swapping parity drives - """ - fsCopyPrcnt: Int - fsNumMounted: Int - fsNumUnmountable: Int - - """Human friendly string of array events happening""" - fsProgress: String - fsState: String - fsUnmountableMask: String - fuseDirectio: String - fuseDirectioDefault: String - fuseDirectioStatus: String - fuseRemember: String - fuseRememberDefault: String - fuseRememberStatus: String - hideDotFiles: Boolean - id: ID! - joinStatus: String - localMaster: Boolean - localTld: String - luksKeyfile: String - maxArraysz: Int - maxCachesz: Int - mdColor: String - mdNumDisabled: Int - mdNumDisks: Int - mdNumErased: Int - mdNumInvalid: Int - mdNumMissing: Int - mdNumNew: Int - mdNumStripes: Int - mdNumStripesDefault: Int - mdNumStripesStatus: String - mdResync: Int - mdResyncAction: String - mdResyncCorr: String - mdResyncDb: String - mdResyncDt: String - mdResyncPos: String - mdResyncSize: Int - mdState: String - mdSyncThresh: Int - mdSyncThreshDefault: Int - mdSyncThreshStatus: String - mdSyncWindow: Int - mdSyncWindowDefault: Int - mdSyncWindowStatus: String - mdVersion: String - mdWriteMethod: Int - mdWriteMethodDefault: String - mdWriteMethodStatus: String - - """Machine hostname""" - name: String - nrRequests: Int - nrRequestsDefault: Int - nrRequestsStatus: String - - """NTP Server 1""" - ntpServer1: String - - """NTP Server 2""" - ntpServer2: String - - """NTP Server 3""" - ntpServer3: String - - """NTP Server 4""" - ntpServer4: String - pollAttributes: String - pollAttributesDefault: String - pollAttributesStatus: String - - """Port for the webui via HTTP""" - port: Int - portssh: Int - - """Port for the webui via HTTPS""" - portssl: Int - porttelnet: Int - queueDepth: String - regCheck: String - regFile: String - regGen: String - regGuid: String - regState: RegistrationState - regTm: String - regTm2: String - - """Registration owner""" - regTo: String - regTy: String - safeMode: Boolean - sbClean: Boolean - sbEvents: Int - sbName: String - sbNumDisks: Int - sbState: String - sbSyncErrs: Int - sbSyncExit: String - sbSynced: Int - sbSynced2: Int - sbUpdated: String - sbVersion: String - security: String - - """Total amount shares with AFP enabled""" - shareAfpCount: Int - shareAfpEnabled: Boolean - shareAvahiAfpModel: String - shareAvahiAfpName: String - shareAvahiEnabled: Boolean - shareAvahiSmbModel: String - shareAvahiSmbName: String - shareCacheEnabled: Boolean - shareCacheFloor: String - - """Total amount of user shares""" - shareCount: Int - shareDisk: String - shareInitialGroup: String - shareInitialOwner: String - shareMoverActive: Boolean - shareMoverLogging: Boolean - shareMoverSchedule: String - - """Total amount shares with NFS enabled""" - shareNfsCount: Int - shareNfsEnabled: Boolean - - """Total amount shares with SMB enabled""" - shareSmbCount: Int - shareSmbEnabled: Boolean - shareUser: String - shareUserExclude: String - shareUserInclude: String - shutdownTimeout: Int - spindownDelay: String - spinupGroups: Boolean - startArray: Boolean - startMode: String - startPage: String - sysArraySlots: Int - sysCacheSlots: Int - sysFlashSlots: Int - sysModel: String - timeZone: String - - """Should a NTP server be used for time sync?""" - useNtp: Boolean - useSsh: Boolean - useSsl: Boolean - - """Should telnet be enabled?""" - useTelnet: Boolean - - """Unraid version""" - version: String - workgroup: String -} - -type Versions { - apache: String - docker: String - gcc: String - git: String - grunt: String - gulp: String - kernel: String - mongodb: String - mysql: String - nginx: String - node: String - npm: String - openssl: String - perl: String - php: String - pm2: String - postfix: String - postgresql: String - python: String - redis: String - systemOpenssl: String - systemOpensslLib: String - tsc: String - unraid: String - v8: String - yarn: String -} - -"""A virtual machine""" -type VmDomain { - """A friendly name for the vm""" - name: String - - """Current domain vm state""" - state: VmState! - uuid: ID! -} - -type VmMutations { - """Force stop a virtual machine""" - forceStopVm(id: ID!): Boolean! - - """Pause a virtual machine""" - pauseVm(id: ID!): Boolean! - - """Reboot a virtual machine""" - rebootVm(id: ID!): Boolean! - - """Reset a virtual machine""" - resetVm(id: ID!): Boolean! - - """Resume a virtual machine""" - resumeVm(id: ID!): Boolean! - - """Start a virtual machine""" - startVm(id: ID!): Boolean! - - """Stop a virtual machine""" - stopVm(id: ID!): Boolean! -} - -enum VmState { - CRASHED - IDLE - NOSTATE - PAUSED - PMSUSPENDED - RUNNING - SHUTDOWN - SHUTOFF -} - -type Vms { - domain: [VmDomain!] - id: ID! -} - -enum WAN_ACCESS_TYPE { - ALWAYS - DISABLED - DYNAMIC -} - -enum WAN_FORWARD_TYPE { - STATIC - UPNP -} - -type Welcome { - message: String! -} - -input addUserInput { - description: String - name: String! - password: String! -} - -input deleteUserInput { - name: String! -} - -enum mdState { - STARTED - SWAP_DSBL -} - -enum registrationType { - BASIC - INVALID - LIFETIME - PLUS - PRO - STARTER - TRIAL - UNLEASHED -} - -input usersInput { - slim: Boolean + ownerSubscription: Owner! + registrationSubscription: Registration! + serversSubscription: Server! + parityHistorySubscription: ParityCheck! + arraySubscription: UnraidArray! } \ No newline at end of file diff --git a/api/legacy/README.md b/api/legacy/README.md new file mode 100644 index 000000000..8eeb56a47 --- /dev/null +++ b/api/legacy/README.md @@ -0,0 +1,3 @@ +# Legacy Assets + +This folder will store legacy types / functionality that may be useful but is not currently a part of the API diff --git a/api/legacy/generated-schema-legacy.graphql b/api/legacy/generated-schema-legacy.graphql new file mode 100644 index 000000000..0928c60b9 --- /dev/null +++ b/api/legacy/generated-schema-legacy.graphql @@ -0,0 +1,1365 @@ +type AccessUrl { + ipv4: URL + ipv6: URL + name: String + type: URL_TYPE! +} + +input AccessUrlInput { + ipv4: URL + ipv6: URL + name: String + type: URL_TYPE! +} + +input AllowedOriginInput { + origins: [String!]! +} + +type ApiKeyResponse { + error: String + valid: Boolean! +} + +""" +Input should be a subset of ApiSettings that can be updated. +Some field combinations may be required or disallowed. Please refer to each field for more information. +""" +input ApiSettingsInput { + """The type of WAN access to use for Remote Access.""" + accessType: WAN_ACCESS_TYPE + + """A list of origins allowed to interact with the API.""" + extraOrigins: [String!] + + """The type of port forwarding to use for Remote Access.""" + forwardType: WAN_FORWARD_TYPE + + """ + The port to use for Remote Access. Not required for UPNP forwardType. Required for STATIC forwardType. + Ignored if accessType is DISABLED or forwardType is UPNP. + """ + port: Port + + """ + If true, the GraphQL sandbox will be enabled and available at /graphql. + If false, the GraphQL sandbox will be disabled and only the production API will be available. + """ + sandbox: Boolean + + """A list of Unique Unraid Account ID's.""" + ssoUserIds: [String!] +} + +type Baseboard { + assetTag: String + manufacturer: String! + model: String + serial: String + version: String +} + +type Case { + base64: String + error: String + icon: String + url: String +} + +type Cloud { + allowedOrigins: [String!]! + apiKey: ApiKeyResponse! + cloud: CloudResponse! + error: String + minigraphql: MinigraphqlResponse! + relay: RelayResponse +} + +type CloudResponse { + error: String + ip: String + status: String! +} + +type Config implements Node { + error: ConfigErrorState + id: ID! + valid: Boolean +} + +enum ConfigErrorState { + INELIGIBLE + INVALID + NO_KEY_SERVER + UNKNOWN_ERROR + WITHDRAWN +} + +type Connect implements Node { + dynamicRemoteAccess: DynamicRemoteAccessStatus! + id: ID! + settings: ConnectSettings! +} + +type ConnectSettings implements Node { + dataSchema: JSON! + id: ID! + uiSchema: JSON! + values: ConnectSettingsValues! +} + +"""Intersection type of ApiSettings and RemoteAccess""" +type ConnectSettingsValues { + """The type of WAN access used for Remote Access.""" + accessType: WAN_ACCESS_TYPE! + + """A list of origins allowed to interact with the API.""" + extraOrigins: [String!]! + + """The type of port forwarding used for Remote Access.""" + forwardType: WAN_FORWARD_TYPE + + """The port used for Remote Access.""" + port: Port + + """ + If true, the GraphQL sandbox is enabled and available at /graphql. + If false, the GraphQL sandbox is disabled and only the production API will be available. + """ + sandbox: Boolean! + + """A list of Unique Unraid Account ID's.""" + ssoUserIds: [String!]! +} + +input ConnectSignInInput { + accessToken: String + apiKey: String! + idToken: String + refreshToken: String + userInfo: ConnectUserInfoInput +} + +input ConnectUserInfoInput { + avatar: String + email: String! + preferred_username: String! +} + +type ContainerHostConfig { + networkMode: String! +} + +type ContainerMount { + destination: String! + driver: String! + mode: String! + name: String! + propagation: String! + rw: Boolean! + source: String! + type: String! +} + +type ContainerPort { + ip: String + privatePort: Int + publicPort: Int + type: ContainerPortType +} + +enum ContainerPortType { + TCP + UDP +} + +enum ContainerState { + EXITED + RUNNING +} + +scalar DateTime + +type Devices { + gpu: [Gpu] + network: [Network] + pci: [Pci] + usb: [Usb] +} + +type Disk { + bytesPerSector: Long! + device: String! + firmwareRevision: String! + id: ID! + interfaceType: DiskInterfaceType! + name: String! + partitions: [DiskPartition!] + sectorsPerTrack: Long! + serialNum: String! + size: Long! + smartStatus: DiskSmartStatus! + temperature: Long + totalCylinders: Long! + totalHeads: Long! + totalSectors: Long! + totalTracks: Long! + tracksPerCylinder: Long! + type: String! + vendor: String! +} + +enum DiskFsType { + btrfs + ext4 + ntfs + vfat + xfs + zfs +} + +enum DiskInterfaceType { + PCIe + SAS + SATA + UNKNOWN + USB +} + +type DiskPartition { + fsType: DiskFsType! + name: String! + size: Long! +} + +enum DiskSmartStatus { + OK + UNKNOWN +} + +type Display { + banner: String + case: Case + critical: Int + dashapps: String + date: String + hot: Int + id: ID! + locale: String + max: Int + number: String + resize: Boolean + scale: Boolean + tabs: Boolean + text: Boolean + theme: Theme + total: Boolean + unit: Temperature + usage: Boolean + users: String + warning: Int + wwn: Boolean +} + +type Docker implements Node { + containers: [DockerContainer!] + id: ID! + networks: [DockerNetwork!] +} + +type DockerContainer { + autoStart: Boolean! + command: String! + created: Int! + hostConfig: ContainerHostConfig + id: ID! + image: String! + imageId: String! + labels: JSON + mounts: [JSON] + names: [String!] + networkSettings: JSON + ports: [ContainerPort!]! + + """ (B) Total size of all the files in the container """ + sizeRootFs: Long + state: ContainerState! + status: String! +} + +type DockerMutations { + """ Start a container """ + start(id: ID!): DockerContainer! + + """ Stop a container """ + stop(id: ID!): DockerContainer! +} + +type DockerNetwork { + attachable: Boolean! + configFrom: JSON + configOnly: Boolean! + containers: JSON + created: String + driver: String + enableIPv6: Boolean! + id: ID + ingress: Boolean! + internal: Boolean! + ipam: JSON + labels: JSON + name: String + options: JSON + scope: String +} + +type DynamicRemoteAccessStatus { + enabledType: DynamicRemoteAccessType! + error: String + runningType: DynamicRemoteAccessType! +} + +enum DynamicRemoteAccessType { + DISABLED + STATIC + UPNP +} + +input EnableDynamicRemoteAccessInput { + enabled: Boolean! + url: AccessUrlInput! +} + +type Flash { + guid: String + product: String + vendor: String +} + +type Gpu { + blacklisted: Boolean! + class: String! + id: ID! + productid: String! + type: String! + typeid: String! + vendorname: String! +} + +enum Importance { + ALERT + INFO + WARNING +} + +type Info implements Node { + """Count of docker containers""" + apps: InfoApps + baseboard: Baseboard + cpu: InfoCpu + devices: Devices + display: Display + id: ID! + + """Machine ID""" + machineId: ID + memory: InfoMemory + os: Os + system: System + time: DateTime! + versions: Versions +} + +type InfoApps { + """How many docker containers are installed""" + installed: Int + + """How many docker containers are running""" + started: Int +} + +type InfoCpu { + brand: String! + cache: JSON! + cores: Int! + family: String! + flags: [String!] + manufacturer: String! + model: String! + processors: Long! + revision: String! + socket: String! + speed: Float! + speedmax: Float! + speedmin: Float! + stepping: Int! + threads: Int! + vendor: String! + voltage: String +} + +type InfoMemory { + active: Long! + available: Long! + buffcache: Long! + free: Long! + layout: [MemoryLayout!] + max: Long! + swapfree: Long! + swaptotal: Long! + swapused: Long! + total: Long! + used: Long! +} + +scalar JSON + +type KeyFile { + contents: String + location: String +} + +"""Represents a log file in the system""" +type LogFile { + """Last modified timestamp""" + modifiedAt: DateTime! + + """Name of the log file""" + name: String! + + """Full path to the log file""" + path: String! + + """Size of the log file in bytes""" + size: Int! +} + +"""Content of a log file""" +type LogFileContent { + """Content of the log file""" + content: String! + + """Path to the log file""" + path: String! + + """Starting line number of the content (1-indexed)""" + startLine: Int + + """Total number of lines in the file""" + totalLines: Int! +} + +scalar Long + +enum MemoryFormFactor { + DIMM +} + +type MemoryLayout { + bank: String + clockSpeed: Long + formFactor: MemoryFormFactor + manufacturer: String + partNum: String + serialNum: String + size: Long! + type: MemoryType + voltageConfigured: Long + voltageMax: Long + voltageMin: Long +} + +enum MemoryType { + DDR2 + DDR3 + DDR4 +} + +enum MinigraphStatus { + CONNECTED + CONNECTING + ERROR_RETRYING + PING_FAILURE + PRE_INIT +} + +type MinigraphqlResponse { + error: String + status: MinigraphStatus! + timeout: Int +} + +type Mount { + directory: String + name: String + permissions: String + type: String +} + +type Mutation { + """Add a new user""" + addUser(input: addUserInput!): User + archiveAll(importance: Importance): NotificationOverview! + + """Marks a notification as archived.""" + archiveNotification(id: String!): Notification! + archiveNotifications(ids: [String!]): NotificationOverview! + connectSignIn(input: ConnectSignInInput!): Boolean! + connectSignOut: Boolean! + createNotification(input: NotificationData!): Notification! + + """Deletes all archived notifications on server.""" + deleteArchivedNotifications: NotificationOverview! + deleteNotification(id: String!, type: NotificationType!): NotificationOverview! + + """Delete a user""" + deleteUser(input: deleteUserInput!): User + docker: DockerMutations + enableDynamicRemoteAccess(input: EnableDynamicRemoteAccessInput!): Boolean! + login(password: String!, username: String!): String + reboot: String + + """Reads each notification to recompute & update the overview.""" + recalculateOverview: NotificationOverview! + setAdditionalAllowedOrigins(input: AllowedOriginInput!): [String!]! + setupRemoteAccess(input: SetupRemoteAccessInput!): Boolean! + shutdown: String + unarchiveAll(importance: Importance): NotificationOverview! + unarchiveNotifications(ids: [String!]): NotificationOverview! + + """Marks a notification as unread.""" + unreadNotification(id: String!): Notification! + + """ + Update the API settings. + Some setting combinations may be required or disallowed. Please refer to each setting for more information. + """ + updateApiSettings(input: ApiSettingsInput!): ConnectSettingsValues! + + """Virtual machine mutations""" + vms: VmMutations +} + +type Network implements Node { + accessUrls: [AccessUrl!] + carrierChanges: String + duplex: String + id: ID! + iface: String + ifaceName: String + internal: String + ipv4: String + ipv6: String + mac: String + mtu: String + operstate: String + speed: String + type: String +} + +interface Node { + id: ID! +} + +type Notification implements Node { + description: String! + formattedTimestamp: String + id: ID! + importance: Importance! + link: String + subject: String! + + """ISO Timestamp for when the notification occurred""" + timestamp: String + + """Also known as 'event'""" + title: String! + type: NotificationType! +} + +type NotificationCounts { + alert: Int! + info: Int! + total: Int! + warning: Int! +} + +input NotificationData { + description: String! + importance: Importance! + link: String + subject: String! + title: String! +} + +input NotificationFilter { + importance: Importance + limit: Int! + offset: Int! + type: NotificationType +} + +type NotificationOverview { + archive: NotificationCounts! + unread: NotificationCounts! +} + +enum NotificationType { + ARCHIVE + UNREAD +} + +type Notifications implements Node { + id: ID! + list(filter: NotificationFilter!): [Notification!]! + + """A cached overview of the notifications in the system & their severity.""" + overview: NotificationOverview! +} + +type Os { + arch: String + build: String + codename: String + codepage: String + distro: String + hostname: String + kernel: String + logofile: String + platform: String + release: String + serial: String + uptime: DateTime +} + +type Owner { + avatar: String + url: String + username: String +} + +type Partition { + devlinks: String + devname: String + devpath: String + devtype: String + idAta: String + idAtaDownloadMicrocode: String + idAtaFeatureSetAam: String + idAtaFeatureSetAamCurrentValue: String + idAtaFeatureSetAamEnabled: String + idAtaFeatureSetAamVendorRecommendedValue: String + idAtaFeatureSetApm: String + idAtaFeatureSetApmCurrentValue: String + idAtaFeatureSetApmEnabled: String + idAtaFeatureSetHpa: String + idAtaFeatureSetHpaEnabled: String + idAtaFeatureSetPm: String + idAtaFeatureSetPmEnabled: String + idAtaFeatureSetPuis: String + idAtaFeatureSetPuisEnabled: String + idAtaFeatureSetSecurity: String + idAtaFeatureSetSecurityEnabled: String + idAtaFeatureSetSecurityEnhancedEraseUnitMin: String + idAtaFeatureSetSecurityEraseUnitMin: String + idAtaFeatureSetSmart: String + idAtaFeatureSetSmartEnabled: String + idAtaRotationRateRpm: String + idAtaSata: String + idAtaSataSignalRateGen1: String + idAtaSataSignalRateGen2: String + idAtaWriteCache: String + idAtaWriteCacheEnabled: String + idBus: String + idFsType: String + idFsUsage: String + idFsUuid: String + idFsUuidEnc: String + idModel: String + idModelEnc: String + idPartEntryDisk: String + idPartEntryNumber: String + idPartEntryOffset: String + idPartEntryScheme: String + idPartEntrySize: String + idPartEntryType: String + idPartTableType: String + idPath: String + idPathTag: String + idRevision: String + idSerial: String + idSerialShort: String + idType: String + idWwn: String + idWwnWithExtension: String + major: String + minor: String + partn: String + subsystem: String + usecInitialized: String +} + +type Pci { + blacklisted: String + class: String + id: ID! + productid: String + productname: String + type: String + typeid: String + vendorid: String + vendorname: String +} + +scalar Port + +type ProfileModel { + avatar: String + url: String + userId: ID + username: String +} + +type Query { + cloud: Cloud + config: Config! + connect: Connect! + + """Single disk""" + disk(id: ID!): Disk + + """Mulitiple disks""" + disks: [Disk]! + display: Display + docker: Docker! + + """Docker network""" + dockerNetwork(id: ID!): DockerNetwork! + + """All Docker networks""" + dockerNetworks(all: Boolean): [DockerNetwork]! + extraAllowedOrigins: [String!]! + flash: Flash + info: Info + + """ + Get the content of a specific log file + @param path Path to the log file + @param lines Number of lines to read from the end of the file (default: 100) + @param startLine Optional starting line number (1-indexed) + """ + logFile(lines: Int, path: String!, startLine: Int): LogFileContent! + + """List all available log files""" + logFiles: [LogFile!]! + network: Network + notifications: Notifications! + online: Boolean + owner: Owner + registration: Registration + remoteAccess: RemoteAccess! + server: Server + servers: [Server!]! + services: [Service!]! + + """Network Shares""" + shares: [Share] + unassignedDevices: [UnassignedDevice] + + """User account""" + user(id: ID!): User + + """User accounts""" + users(input: usersInput): [User!]! + vars: Vars + + """Virtual machines""" + vms: Vms +} + +type Registration { + expiration: String + guid: String + keyFile: KeyFile + state: RegistrationState + type: registrationType + updateExpiration: String +} + +enum RegistrationState { + BASIC + + """BLACKLISTED""" + EBLACKLISTED + + """BLACKLISTED""" + EBLACKLISTED1 + + """BLACKLISTED""" + EBLACKLISTED2 + + """Trial Expired""" + EEXPIRED + + """GUID Error""" + EGUID + + """Multiple License Keys Present""" + EGUID1 + + """Trial Requires Internet Connection""" + ENOCONN + + """No Flash""" + ENOFLASH + ENOFLASH1 + ENOFLASH2 + ENOFLASH3 + ENOFLASH4 + ENOFLASH5 + ENOFLASH6 + ENOFLASH7 + + """No Keyfile""" + ENOKEYFILE + + """No Keyfile""" + ENOKEYFILE1 + + """Missing key file""" + ENOKEYFILE2 + + """Invalid installation""" + ETRIAL + LIFETIME + PLUS + PRO + STARTER + TRIAL + UNLEASHED +} + +type RelayResponse { + error: String + status: String! + timeout: String +} + +type RemoteAccess { + accessType: WAN_ACCESS_TYPE! + forwardType: WAN_FORWARD_TYPE + port: Port +} + +type Server { + apikey: String! + guid: String! + lanip: String! + localurl: String! + name: String! + owner: ProfileModel! + remoteurl: String! + status: ServerStatus! + wanip: String! +} + +enum ServerStatus { + never_connected + offline + online +} + +type Service implements Node { + id: ID! + name: String + online: Boolean + uptime: Uptime + version: String +} + +input SetupRemoteAccessInput { + accessType: WAN_ACCESS_TYPE! + forwardType: WAN_FORWARD_TYPE + port: Port +} + +"""Network Share""" +type Share { + allocator: String + cache: Boolean + color: String + + """User comment""" + comment: String + cow: String + + """Disks that're excluded from this share""" + exclude: [String] + floor: String + + """(KB) Free space""" + free: Long + + """Disks that're included in this share""" + include: [String] + luksStatus: String + + """Display name""" + name: String + nameOrig: String + + """(KB) Total size""" + size: Long + splitLevel: String + + """(KB) Used Size""" + used: Long +} + +type Subscription { + config: Config! + display: Display + dockerNetwork(id: ID!): DockerNetwork! + dockerNetworks: [DockerNetwork]! + flash: Flash! + info: Info! + + """ + Subscribe to changes in a log file + @param path Path to the log file + """ + logFile(path: String!): LogFileContent! + notificationAdded: Notification! + notificationsOverview: NotificationOverview! + online: Boolean! + owner: Owner! + ping: String! + registration: Registration! + server: Server + service(name: String!): [Service!] + share(id: ID!): Share! + shares: [Share!] + unassignedDevices: [UnassignedDevice!] + user(id: ID!): User! + users: [User]! + vars: Vars! + vms: Vms +} + +type System { + manufacturer: String + model: String + serial: String + sku: String + uuid: String + version: String +} + +enum Temperature { + C + F +} + +enum Theme { + white +} + +scalar URL + +enum URL_TYPE { + DEFAULT + LAN + MDNS + OTHER + WAN + WIREGUARD +} + +scalar UUID + +type UnassignedDevice { + devlinks: String + devname: String + devpath: String + devtype: String + idAta: String + idAtaDownloadMicrocode: String + idAtaFeatureSetAam: String + idAtaFeatureSetAamCurrentValue: String + idAtaFeatureSetAamEnabled: String + idAtaFeatureSetAamVendorRecommendedValue: String + idAtaFeatureSetApm: String + idAtaFeatureSetApmCurrentValue: String + idAtaFeatureSetApmEnabled: String + idAtaFeatureSetHpa: String + idAtaFeatureSetHpaEnabled: String + idAtaFeatureSetPm: String + idAtaFeatureSetPmEnabled: String + idAtaFeatureSetPuis: String + idAtaFeatureSetPuisEnabled: String + idAtaFeatureSetSecurity: String + idAtaFeatureSetSecurityEnabled: String + idAtaFeatureSetSecurityEnhancedEraseUnitMin: String + idAtaFeatureSetSecurityEraseUnitMin: String + idAtaFeatureSetSmart: String + idAtaFeatureSetSmartEnabled: String + idAtaRotationRateRpm: String + idAtaSata: String + idAtaSataSignalRateGen1: String + idAtaSataSignalRateGen2: String + idAtaWriteCache: String + idAtaWriteCacheEnabled: String + idBus: String + idModel: String + idModelEnc: String + idPartTableType: String + idPath: String + idPathTag: String + idRevision: String + idSerial: String + idSerialShort: String + idType: String + idWwn: String + idWwnWithExtension: String + major: String + minor: String + mount: Mount + mounted: Boolean + name: String + partitions: [Partition] + subsystem: String + temp: Int + usecInitialized: String +} + +type Uptime { + timestamp: String +} + +type Usb { + id: ID! + name: String +} + +"""A local user account""" +type User implements UserAccount { + description: String! + id: ID! + + """A unique name for the user""" + name: String! + + """If the account has a password set""" + password: Boolean +} + +interface UserAccount { + description: String! + id: ID! + name: String! +} + +type Vars implements Node { + bindMgt: Boolean + cacheNumDevices: Int + cacheSbNumDisks: Int + comment: String + configError: ConfigErrorState + configValid: Boolean + csrfToken: String + defaultFormat: String + defaultFsType: String + deviceCount: Int + domain: String + domainLogin: String + domainShort: String + enableFruit: String + flashGuid: String + flashProduct: String + flashVendor: String + + """ + Percentage from 0 - 100 while upgrading a disk or swapping parity drives + """ + fsCopyPrcnt: Int + fsNumMounted: Int + fsNumUnmountable: Int + + """Human friendly string of array events happening""" + fsProgress: String + fsState: String + fsUnmountableMask: String + fuseDirectio: String + fuseDirectioDefault: String + fuseDirectioStatus: String + fuseRemember: String + fuseRememberDefault: String + fuseRememberStatus: String + hideDotFiles: Boolean + id: ID! + joinStatus: String + localMaster: Boolean + localTld: String + luksKeyfile: String + maxArraysz: Int + maxCachesz: Int + mdColor: String + mdNumDisabled: Int + mdNumDisks: Int + mdNumErased: Int + mdNumInvalid: Int + mdNumMissing: Int + mdNumNew: Int + mdNumStripes: Int + mdNumStripesDefault: Int + mdNumStripesStatus: String + mdResync: Int + mdResyncAction: String + mdResyncCorr: String + mdResyncDb: String + mdResyncDt: String + mdResyncPos: String + mdResyncSize: Int + mdState: String + mdSyncThresh: Int + mdSyncThreshDefault: Int + mdSyncThreshStatus: String + mdSyncWindow: Int + mdSyncWindowDefault: Int + mdSyncWindowStatus: String + mdVersion: String + mdWriteMethod: Int + mdWriteMethodDefault: String + mdWriteMethodStatus: String + + """Machine hostname""" + name: String + nrRequests: Int + nrRequestsDefault: Int + nrRequestsStatus: String + + """NTP Server 1""" + ntpServer1: String + + """NTP Server 2""" + ntpServer2: String + + """NTP Server 3""" + ntpServer3: String + + """NTP Server 4""" + ntpServer4: String + pollAttributes: String + pollAttributesDefault: String + pollAttributesStatus: String + + """Port for the webui via HTTP""" + port: Int + portssh: Int + + """Port for the webui via HTTPS""" + portssl: Int + porttelnet: Int + queueDepth: String + regCheck: String + regFile: String + regGen: String + regGuid: String + regState: RegistrationState + regTm: String + regTm2: String + + """Registration owner""" + regTo: String + regTy: String + safeMode: Boolean + sbClean: Boolean + sbEvents: Int + sbName: String + sbNumDisks: Int + sbState: String + sbSyncErrs: Int + sbSyncExit: String + sbSynced: Int + sbSynced2: Int + sbUpdated: String + sbVersion: String + security: String + + """Total amount shares with AFP enabled""" + shareAfpCount: Int + shareAfpEnabled: Boolean + shareAvahiAfpModel: String + shareAvahiAfpName: String + shareAvahiEnabled: Boolean + shareAvahiSmbModel: String + shareAvahiSmbName: String + shareCacheEnabled: Boolean + shareCacheFloor: String + + """Total amount of user shares""" + shareCount: Int + shareDisk: String + shareInitialGroup: String + shareInitialOwner: String + shareMoverActive: Boolean + shareMoverLogging: Boolean + shareMoverSchedule: String + + """Total amount shares with NFS enabled""" + shareNfsCount: Int + shareNfsEnabled: Boolean + + """Total amount shares with SMB enabled""" + shareSmbCount: Int + shareSmbEnabled: Boolean + shareUser: String + shareUserExclude: String + shareUserInclude: String + shutdownTimeout: Int + spindownDelay: String + spinupGroups: Boolean + startArray: Boolean + startMode: String + startPage: String + sysArraySlots: Int + sysCacheSlots: Int + sysFlashSlots: Int + sysModel: String + timeZone: String + + """Should a NTP server be used for time sync?""" + useNtp: Boolean + useSsh: Boolean + useSsl: Boolean + + """Should telnet be enabled?""" + useTelnet: Boolean + + """Unraid version""" + version: String + workgroup: String +} + +type Versions { + apache: String + docker: String + gcc: String + git: String + grunt: String + gulp: String + kernel: String + mongodb: String + mysql: String + nginx: String + node: String + npm: String + openssl: String + perl: String + php: String + pm2: String + postfix: String + postgresql: String + python: String + redis: String + systemOpenssl: String + systemOpensslLib: String + tsc: String + unraid: String + v8: String + yarn: String +} + +"""A virtual machine""" +type VmDomain { + """A friendly name for the vm""" + name: String + + """Current domain vm state""" + state: VmState! + uuid: ID! +} + +type VmMutations { + """Force stop a virtual machine""" + forceStopVm(id: ID!): Boolean! + + """Pause a virtual machine""" + pauseVm(id: ID!): Boolean! + + """Reboot a virtual machine""" + rebootVm(id: ID!): Boolean! + + """Reset a virtual machine""" + resetVm(id: ID!): Boolean! + + """Resume a virtual machine""" + resumeVm(id: ID!): Boolean! + + """Start a virtual machine""" + startVm(id: ID!): Boolean! + + """Stop a virtual machine""" + stopVm(id: ID!): Boolean! +} + +enum VmState { + CRASHED + IDLE + NOSTATE + PAUSED + PMSUSPENDED + RUNNING + SHUTDOWN + SHUTOFF +} + +type Vms { + domain: [VmDomain!] + id: ID! +} + +enum WAN_ACCESS_TYPE { + ALWAYS + DISABLED + DYNAMIC +} + +enum WAN_FORWARD_TYPE { + STATIC + UPNP +} + +type Welcome { + message: String! +} + +input addUserInput { + description: String + name: String! + password: String! +} + +input deleteUserInput { + name: String! +} + +enum mdState { + STARTED + SWAP_DSBL +} + +enum registrationType { + BASIC + INVALID + LIFETIME + PLUS + PRO + STARTER + TRIAL + UNLEASHED +} + +input usersInput { + slim: Boolean +} \ No newline at end of file diff --git a/api/package.json b/api/package.json index ee0c80678..d78ad48fa 100644 --- a/api/package.json +++ b/api/package.json @@ -43,7 +43,9 @@ "container:start": "pnpm run container:stop && ./scripts/dc.sh run --rm --service-ports dev", "container:stop": "./scripts/dc.sh stop dev", "container:test": "./scripts/dc.sh run --rm builder pnpm run test", - "container:enter": "./scripts/dc.sh exec dev /bin/bash" + "container:enter": "./scripts/dc.sh exec dev /bin/bash", + "// Migration Scripts": "", + "migration:codefirst": "tsx ./src/unraid-api/graph/migration-script.ts" }, "bin": { "unraid-api": "dist/cli.js" @@ -81,6 +83,8 @@ "casbin": "^5.32.0", "change-case": "^5.4.4", "chokidar": "^4.0.1", + "class-transformer": "^0.5.1", + "class-validator": "^0.14.1", "cli-table": "^0.3.11", "command-exists": "^1.2.9", "convert": "^5.8.0", diff --git a/api/src/__test__/core/utils/shares/get-shares.test.ts b/api/src/__test__/core/utils/shares/get-shares.test.ts index 3131f61eb..97667e949 100644 --- a/api/src/__test__/core/utils/shares/get-shares.test.ts +++ b/api/src/__test__/core/utils/shares/get-shares.test.ts @@ -8,202 +8,211 @@ test('Returns both disk and user shares', async () => { await store.dispatch(loadStateFiles()); expect(getShares()).toMatchInlineSnapshot(` - { - "disks": [], - "users": [ - { - "allocator": "highwater", - "cachePool": "cache", - "color": "yellow-on", - "comment": "", - "cow": "auto", - "exclude": [], - "floor": "0", - "free": 9309372, - "include": [], - "luksStatus": "0", - "name": "appdata", - "nameOrig": "appdata", - "nfs": {}, - "size": 0, - "smb": {}, - "splitLevel": "", - "type": "user", - "used": 33619300, - }, - { - "allocator": "highwater", - "cachePool": "cache", - "color": "yellow-on", - "comment": "saved VM instances", - "cow": "auto", - "exclude": [], - "floor": "0", - "free": 9309372, - "include": [], - "luksStatus": "0", - "name": "domains", - "nameOrig": "domains", - "nfs": {}, - "size": 0, - "smb": {}, - "splitLevel": "1", - "type": "user", - "used": 33619300, - }, - { - "allocator": "highwater", - "cachePool": "cache", - "color": "yellow-on", - "comment": "ISO images", - "cow": "auto", - "exclude": [], - "floor": "0", - "free": 9309372, - "include": [], - "luksStatus": "0", - "name": "isos", - "nameOrig": "isos", - "nfs": {}, - "size": 0, - "smb": {}, - "splitLevel": "", - "type": "user", - "used": 33619300, - }, - { - "allocator": "highwater", - "cachePool": "cache", - "color": "yellow-on", - "comment": "system data", - "cow": "auto", - "exclude": [], - "floor": "0", - "free": 9309372, - "include": [], - "luksStatus": "0", - "name": "system", - "nameOrig": "system", - "nfs": {}, - "size": 0, - "smb": {}, - "splitLevel": "1", - "type": "user", - "used": 33619300, - }, - ], - } - `); + { + "disks": [], + "users": [ + { + "allocator": "highwater", + "cachePool": "cache", + "color": "yellow-on", + "comment": "", + "cow": "auto", + "exclude": [], + "floor": "0", + "free": 9309372, + "id": "appdata", + "include": [], + "luksStatus": "0", + "name": "appdata", + "nameOrig": "appdata", + "nfs": {}, + "size": 0, + "smb": {}, + "splitLevel": "", + "type": "user", + "used": 33619300, + }, + { + "allocator": "highwater", + "cachePool": "cache", + "color": "yellow-on", + "comment": "saved VM instances", + "cow": "auto", + "exclude": [], + "floor": "0", + "free": 9309372, + "id": "domains", + "include": [], + "luksStatus": "0", + "name": "domains", + "nameOrig": "domains", + "nfs": {}, + "size": 0, + "smb": {}, + "splitLevel": "1", + "type": "user", + "used": 33619300, + }, + { + "allocator": "highwater", + "cachePool": "cache", + "color": "yellow-on", + "comment": "ISO images", + "cow": "auto", + "exclude": [], + "floor": "0", + "free": 9309372, + "id": "isos", + "include": [], + "luksStatus": "0", + "name": "isos", + "nameOrig": "isos", + "nfs": {}, + "size": 0, + "smb": {}, + "splitLevel": "", + "type": "user", + "used": 33619300, + }, + { + "allocator": "highwater", + "cachePool": "cache", + "color": "yellow-on", + "comment": "system data", + "cow": "auto", + "exclude": [], + "floor": "0", + "free": 9309372, + "id": "system", + "include": [], + "luksStatus": "0", + "name": "system", + "nameOrig": "system", + "nfs": {}, + "size": 0, + "smb": {}, + "splitLevel": "1", + "type": "user", + "used": 33619300, + }, + ], + } + `); }); test('Returns shares by type', async () => { await store.dispatch(loadStateFiles()); expect(getShares('user')).toMatchInlineSnapshot(` - { - "allocator": "highwater", - "cachePool": "cache", - "color": "yellow-on", - "comment": "", - "cow": "auto", - "exclude": [], - "floor": "0", - "free": 9309372, - "include": [], - "luksStatus": "0", - "name": "appdata", - "nameOrig": "appdata", - "nfs": {}, - "size": 0, - "smb": {}, - "splitLevel": "", - "type": "user", - "used": 33619300, - } - `); + { + "allocator": "highwater", + "cachePool": "cache", + "color": "yellow-on", + "comment": "", + "cow": "auto", + "exclude": [], + "floor": "0", + "free": 9309372, + "id": "appdata", + "include": [], + "luksStatus": "0", + "name": "appdata", + "nameOrig": "appdata", + "nfs": {}, + "size": 0, + "smb": {}, + "splitLevel": "", + "type": "user", + "used": 33619300, + } + `); expect(getShares('users')).toMatchInlineSnapshot(` - [ - { - "allocator": "highwater", - "cachePool": "cache", - "color": "yellow-on", - "comment": "", - "cow": "auto", - "exclude": [], - "floor": "0", - "free": 9309372, - "include": [], - "luksStatus": "0", - "name": "appdata", - "nameOrig": "appdata", - "nfs": {}, - "size": 0, - "smb": {}, - "splitLevel": "", - "type": "user", - "used": 33619300, - }, - { - "allocator": "highwater", - "cachePool": "cache", - "color": "yellow-on", - "comment": "saved VM instances", - "cow": "auto", - "exclude": [], - "floor": "0", - "free": 9309372, - "include": [], - "luksStatus": "0", - "name": "domains", - "nameOrig": "domains", - "nfs": {}, - "size": 0, - "smb": {}, - "splitLevel": "1", - "type": "user", - "used": 33619300, - }, - { - "allocator": "highwater", - "cachePool": "cache", - "color": "yellow-on", - "comment": "ISO images", - "cow": "auto", - "exclude": [], - "floor": "0", - "free": 9309372, - "include": [], - "luksStatus": "0", - "name": "isos", - "nameOrig": "isos", - "nfs": {}, - "size": 0, - "smb": {}, - "splitLevel": "", - "type": "user", - "used": 33619300, - }, - { - "allocator": "highwater", - "cachePool": "cache", - "color": "yellow-on", - "comment": "system data", - "cow": "auto", - "exclude": [], - "floor": "0", - "free": 9309372, - "include": [], - "luksStatus": "0", - "name": "system", - "nameOrig": "system", - "nfs": {}, - "size": 0, - "smb": {}, - "splitLevel": "1", - "type": "user", - "used": 33619300, - }, - ] - `); + [ + { + "allocator": "highwater", + "cachePool": "cache", + "color": "yellow-on", + "comment": "", + "cow": "auto", + "exclude": [], + "floor": "0", + "free": 9309372, + "id": "appdata", + "include": [], + "luksStatus": "0", + "name": "appdata", + "nameOrig": "appdata", + "nfs": {}, + "size": 0, + "smb": {}, + "splitLevel": "", + "type": "user", + "used": 33619300, + }, + { + "allocator": "highwater", + "cachePool": "cache", + "color": "yellow-on", + "comment": "saved VM instances", + "cow": "auto", + "exclude": [], + "floor": "0", + "free": 9309372, + "id": "domains", + "include": [], + "luksStatus": "0", + "name": "domains", + "nameOrig": "domains", + "nfs": {}, + "size": 0, + "smb": {}, + "splitLevel": "1", + "type": "user", + "used": 33619300, + }, + { + "allocator": "highwater", + "cachePool": "cache", + "color": "yellow-on", + "comment": "ISO images", + "cow": "auto", + "exclude": [], + "floor": "0", + "free": 9309372, + "id": "isos", + "include": [], + "luksStatus": "0", + "name": "isos", + "nameOrig": "isos", + "nfs": {}, + "size": 0, + "smb": {}, + "splitLevel": "", + "type": "user", + "used": 33619300, + }, + { + "allocator": "highwater", + "cachePool": "cache", + "color": "yellow-on", + "comment": "system data", + "cow": "auto", + "exclude": [], + "floor": "0", + "free": 9309372, + "id": "system", + "include": [], + "luksStatus": "0", + "name": "system", + "nameOrig": "system", + "nfs": {}, + "size": 0, + "smb": {}, + "splitLevel": "1", + "type": "user", + "used": 33619300, + }, + ] + `); expect(getShares('disk')).toMatchInlineSnapshot('null'); expect(getShares('disks')).toMatchInlineSnapshot('[]'); }); @@ -211,27 +220,28 @@ test('Returns shares by type', async () => { test('Returns shares by name', async () => { await store.dispatch(loadStateFiles()); expect(getShares('user', { name: 'domains' })).toMatchInlineSnapshot(` - { - "allocator": "highwater", - "cachePool": "cache", - "color": "yellow-on", - "comment": "saved VM instances", - "cow": "auto", - "exclude": [], - "floor": "0", - "free": 9309372, - "include": [], - "luksStatus": "0", - "name": "domains", - "nameOrig": "domains", - "nfs": {}, - "size": 0, - "smb": {}, - "splitLevel": "1", - "type": "user", - "used": 33619300, - } - `); + { + "allocator": "highwater", + "cachePool": "cache", + "color": "yellow-on", + "comment": "saved VM instances", + "cow": "auto", + "exclude": [], + "floor": "0", + "free": 9309372, + "id": "domains", + "include": [], + "luksStatus": "0", + "name": "domains", + "nameOrig": "domains", + "nfs": {}, + "size": 0, + "smb": {}, + "splitLevel": "1", + "type": "user", + "used": 33619300, + } + `); expect(getShares('user', { name: 'non-existent-user-share' })).toMatchInlineSnapshot('null'); // @TODO: disk shares need to be added to the dev ini files expect(getShares('disk', { name: 'disk1' })).toMatchInlineSnapshot('null'); diff --git a/api/src/__test__/graphql/resolvers/subscription/network.test.ts b/api/src/__test__/graphql/resolvers/subscription/network.test.ts index 850a00b5e..877f42acf 100644 --- a/api/src/__test__/graphql/resolvers/subscription/network.test.ts +++ b/api/src/__test__/graphql/resolvers/subscription/network.test.ts @@ -2,7 +2,6 @@ import { expect, test, vi } from 'vitest'; import type { NginxUrlFields } from '@app/graphql/resolvers/subscription/network.js'; import { type Nginx } from '@app/core/types/states/nginx.js'; -import { URL_TYPE } from '@app/graphql/generated/client/graphql.js'; import { getServerIps, getUrlForField, @@ -11,6 +10,7 @@ import { import { store } from '@app/store/index.js'; import { loadConfigFile } from '@app/store/modules/config.js'; import { loadStateFiles } from '@app/store/modules/emhttp.js'; +import { URL_TYPE } from '@app/unraid-api/graph/resolvers/connect/connect.model.js'; test.each([ [{ httpPort: 80, httpsPort: 443, url: 'my-default-url.com' }], diff --git a/api/src/__test__/store/modules/config.test.ts b/api/src/__test__/store/modules/config.test.ts index 0bae3d7b9..c3fa36fdd 100644 --- a/api/src/__test__/store/modules/config.test.ts +++ b/api/src/__test__/store/modules/config.test.ts @@ -1,13 +1,17 @@ import { beforeEach, expect, test, vi } from 'vitest'; import { pubsub, PUBSUB_CHANNEL } from '@app/core/pubsub.js'; -import { MinigraphStatus, WAN_ACCESS_TYPE, WAN_FORWARD_TYPE } from '@app/graphql/generated/api/types.js'; import { GraphQLClient } from '@app/mothership/graphql-client.js'; import { stopPingTimeoutJobs } from '@app/mothership/jobs/ping-timeout-jobs.js'; import { setGraphqlConnectionStatus } from '@app/store/actions/set-minigraph-status.js'; import { setupRemoteAccessThunk } from '@app/store/actions/setup-remote-access.js'; import { store } from '@app/store/index.js'; import { MyServersConfigMemory } from '@app/types/my-servers-config.js'; +import { MinigraphStatus } from '@app/unraid-api/graph/resolvers/cloud/cloud.model.js'; +import { + WAN_ACCESS_TYPE, + WAN_FORWARD_TYPE, +} from '@app/unraid-api/graph/resolvers/connect/connect.model.js'; // Mock dependencies vi.mock('@app/core/pubsub.js', () => { @@ -145,6 +149,7 @@ test('loginUser updates state and publishes to pubsub', async () => { expect(pubsub.publish).toHaveBeenCalledWith(PUBSUB_CHANNEL.OWNER, { owner: { username: userInfo.username, + url: '', avatar: userInfo.avatar, }, }); diff --git a/api/src/__test__/store/modules/emhttp.test.ts b/api/src/__test__/store/modules/emhttp.test.ts index 2b6a2d003..a56c38e66 100644 --- a/api/src/__test__/store/modules/emhttp.test.ts +++ b/api/src/__test__/store/modules/emhttp.test.ts @@ -197,253 +197,257 @@ test('After init returns values from cfg file for all fields', async () => { } `); expect(disks).toMatchInlineSnapshot(` - [ - { - "comment": null, - "critical": null, - "device": "sdh", - "exportable": false, - "format": "GPT: 4KiB-aligned", - "fsFree": null, - "fsSize": null, - "fsType": null, - "fsUsed": null, - "id": "ST18000NM000J-2TV103_ZR585CPY", - "idx": 0, - "name": "parity", - "numErrors": 0, - "numReads": 0, - "numWrites": 0, - "rotational": true, - "size": 17578328012, - "status": "DISK_OK", - "temp": 25, - "transport": "ata", - "type": "Parity", - "warning": null, - }, - { - "comment": "Seagate Exos", - "critical": 75, - "device": "sdf", - "exportable": false, - "format": "GPT: 4KiB-aligned", - "fsFree": 13882739732, - "fsSize": 17998742753, - "fsType": "xfs", - "fsUsed": 4116003021, - "id": "ST18000NM000J-2TV103_ZR5B1W9X", - "idx": 1, - "name": "disk1", - "numErrors": 0, - "numReads": 0, - "numWrites": 0, - "rotational": true, - "size": 17578328012, - "status": "DISK_OK", - "temp": 30, - "transport": "ata", - "type": "Data", - "warning": 50, - }, - { - "comment": "", - "critical": null, - "device": "sdj", - "exportable": false, - "format": "GPT: 4KiB-aligned", - "fsFree": 93140746, - "fsSize": 11998001574, - "fsType": "xfs", - "fsUsed": 11904860828, - "id": "WDC_WD120EDAZ-11F3RA0_5PJRD45C", - "idx": 2, - "name": "disk2", - "numErrors": 0, - "numReads": 0, - "numWrites": 0, - "rotational": true, - "size": 11718885324, - "status": "DISK_OK", - "temp": 30, - "transport": "ata", - "type": "Data", - "warning": null, - }, - { - "comment": "", - "critical": null, - "device": "sde", - "exportable": false, - "format": "GPT: 4KiB-aligned", - "fsFree": 5519945093, - "fsSize": 11998001574, - "fsType": "xfs", - "fsUsed": 6478056481, - "id": "WDC_WD120EMAZ-11BLFA0_5PH8BTYD", - "idx": 3, - "name": "disk3", - "numErrors": 0, - "numReads": 0, - "numWrites": 0, - "rotational": true, - "size": 11718885324, - "status": "DISK_OK", - "temp": 30, - "transport": "ata", - "type": "Data", - "warning": null, - }, - { - "comment": "", - "critical": null, - "device": "sdi", - "exportable": false, - "format": "MBR: 4KiB-aligned", - "fsFree": 111810683, - "fsSize": 250059317, - "fsType": "btrfs", - "fsUsed": 137273827, - "id": "Samsung_SSD_850_EVO_250GB_S2R5NX0H643734Z", - "idx": 30, - "name": "cache", - "numErrors": 0, - "numReads": 0, - "numWrites": 0, - "rotational": false, - "size": 244198552, - "status": "DISK_OK", - "temp": 22, - "transport": "ata", - "type": "Cache", - "warning": null, - }, - { - "comment": null, - "critical": null, - "device": "nvme0n1", - "exportable": false, - "format": "MBR: 4KiB-aligned", - "fsFree": null, - "fsSize": null, - "fsType": null, - "fsUsed": null, - "id": "KINGSTON_SA2000M8250G_50026B7282669D9E", - "idx": 31, - "name": "cache2", - "numErrors": 0, - "numReads": 0, - "numWrites": 0, - "rotational": false, - "size": 244198552, - "status": "DISK_OK", - "temp": 27, - "transport": "nvme", - "type": "Cache", - "warning": null, - }, - { - "comment": "Unraid OS boot device", - "critical": null, - "device": "sda", - "exportable": true, - "format": "unknown", - "fsFree": 3191407, - "fsSize": 4042732, - "fsType": "vfat", - "fsUsed": 851325, - "id": "Cruzer", - "idx": 32, - "name": "flash", - "numErrors": 0, - "numReads": 0, - "numWrites": 0, - "rotational": true, - "size": 3956700, - "status": "DISK_OK", - "temp": null, - "transport": "usb", - "type": "Flash", - "warning": null, - }, - ] - `); + [ + { + "comment": null, + "critical": null, + "device": "sdh", + "exportable": false, + "format": "GPT: 4KiB-aligned", + "fsFree": null, + "fsSize": null, + "fsType": null, + "fsUsed": null, + "id": "ST18000NM000J-2TV103_ZR585CPY", + "idx": 0, + "name": "parity", + "numErrors": 0, + "numReads": 0, + "numWrites": 0, + "rotational": true, + "size": 17578328012, + "status": "DISK_OK", + "temp": 25, + "transport": "ata", + "type": "PARITY", + "warning": null, + }, + { + "comment": "Seagate Exos", + "critical": 75, + "device": "sdf", + "exportable": false, + "format": "GPT: 4KiB-aligned", + "fsFree": 13882739732, + "fsSize": 17998742753, + "fsType": "xfs", + "fsUsed": 4116003021, + "id": "ST18000NM000J-2TV103_ZR5B1W9X", + "idx": 1, + "name": "disk1", + "numErrors": 0, + "numReads": 0, + "numWrites": 0, + "rotational": true, + "size": 17578328012, + "status": "DISK_OK", + "temp": 30, + "transport": "ata", + "type": "DATA", + "warning": 50, + }, + { + "comment": "", + "critical": null, + "device": "sdj", + "exportable": false, + "format": "GPT: 4KiB-aligned", + "fsFree": 93140746, + "fsSize": 11998001574, + "fsType": "xfs", + "fsUsed": 11904860828, + "id": "WDC_WD120EDAZ-11F3RA0_5PJRD45C", + "idx": 2, + "name": "disk2", + "numErrors": 0, + "numReads": 0, + "numWrites": 0, + "rotational": true, + "size": 11718885324, + "status": "DISK_OK", + "temp": 30, + "transport": "ata", + "type": "DATA", + "warning": null, + }, + { + "comment": "", + "critical": null, + "device": "sde", + "exportable": false, + "format": "GPT: 4KiB-aligned", + "fsFree": 5519945093, + "fsSize": 11998001574, + "fsType": "xfs", + "fsUsed": 6478056481, + "id": "WDC_WD120EMAZ-11BLFA0_5PH8BTYD", + "idx": 3, + "name": "disk3", + "numErrors": 0, + "numReads": 0, + "numWrites": 0, + "rotational": true, + "size": 11718885324, + "status": "DISK_OK", + "temp": 30, + "transport": "ata", + "type": "DATA", + "warning": null, + }, + { + "comment": "", + "critical": null, + "device": "sdi", + "exportable": false, + "format": "MBR: 4KiB-aligned", + "fsFree": 111810683, + "fsSize": 250059317, + "fsType": "btrfs", + "fsUsed": 137273827, + "id": "Samsung_SSD_850_EVO_250GB_S2R5NX0H643734Z", + "idx": 30, + "name": "cache", + "numErrors": 0, + "numReads": 0, + "numWrites": 0, + "rotational": false, + "size": 244198552, + "status": "DISK_OK", + "temp": 22, + "transport": "ata", + "type": "CACHE", + "warning": null, + }, + { + "comment": null, + "critical": null, + "device": "nvme0n1", + "exportable": false, + "format": "MBR: 4KiB-aligned", + "fsFree": null, + "fsSize": null, + "fsType": null, + "fsUsed": null, + "id": "KINGSTON_SA2000M8250G_50026B7282669D9E", + "idx": 31, + "name": "cache2", + "numErrors": 0, + "numReads": 0, + "numWrites": 0, + "rotational": false, + "size": 244198552, + "status": "DISK_OK", + "temp": 27, + "transport": "nvme", + "type": "CACHE", + "warning": null, + }, + { + "comment": "Unraid OS boot device", + "critical": null, + "device": "sda", + "exportable": true, + "format": "unknown", + "fsFree": 3191407, + "fsSize": 4042732, + "fsType": "vfat", + "fsUsed": 851325, + "id": "Cruzer", + "idx": 32, + "name": "flash", + "numErrors": 0, + "numReads": 0, + "numWrites": 0, + "rotational": true, + "size": 3956700, + "status": "DISK_OK", + "temp": null, + "transport": "usb", + "type": "FLASH", + "warning": null, + }, + ] + `); expect(shares).toMatchInlineSnapshot(` - [ - { - "allocator": "highwater", - "cache": false, - "cachePool": "cache", - "color": "yellow-on", - "comment": "", - "cow": "auto", - "exclude": [], - "floor": "0", - "free": 9309372, - "include": [], - "luksStatus": "0", - "name": "appdata", - "nameOrig": "appdata", - "size": 0, - "splitLevel": "", - "used": 33619300, - }, - { - "allocator": "highwater", - "cache": false, - "cachePool": "cache", - "color": "yellow-on", - "comment": "saved VM instances", - "cow": "auto", - "exclude": [], - "floor": "0", - "free": 9309372, - "include": [], - "luksStatus": "0", - "name": "domains", - "nameOrig": "domains", - "size": 0, - "splitLevel": "1", - "used": 33619300, - }, - { - "allocator": "highwater", - "cache": true, - "cachePool": "cache", - "color": "yellow-on", - "comment": "ISO images", - "cow": "auto", - "exclude": [], - "floor": "0", - "free": 9309372, - "include": [], - "luksStatus": "0", - "name": "isos", - "nameOrig": "isos", - "size": 0, - "splitLevel": "", - "used": 33619300, - }, - { - "allocator": "highwater", - "cache": false, - "cachePool": "cache", - "color": "yellow-on", - "comment": "system data", - "cow": "auto", - "exclude": [], - "floor": "0", - "free": 9309372, - "include": [], - "luksStatus": "0", - "name": "system", - "nameOrig": "system", - "size": 0, - "splitLevel": "1", - "used": 33619300, - }, - ] - `); + [ + { + "allocator": "highwater", + "cache": false, + "cachePool": "cache", + "color": "yellow-on", + "comment": "", + "cow": "auto", + "exclude": [], + "floor": "0", + "free": 9309372, + "id": "appdata", + "include": [], + "luksStatus": "0", + "name": "appdata", + "nameOrig": "appdata", + "size": 0, + "splitLevel": "", + "used": 33619300, + }, + { + "allocator": "highwater", + "cache": false, + "cachePool": "cache", + "color": "yellow-on", + "comment": "saved VM instances", + "cow": "auto", + "exclude": [], + "floor": "0", + "free": 9309372, + "id": "domains", + "include": [], + "luksStatus": "0", + "name": "domains", + "nameOrig": "domains", + "size": 0, + "splitLevel": "1", + "used": 33619300, + }, + { + "allocator": "highwater", + "cache": true, + "cachePool": "cache", + "color": "yellow-on", + "comment": "ISO images", + "cow": "auto", + "exclude": [], + "floor": "0", + "free": 9309372, + "id": "isos", + "include": [], + "luksStatus": "0", + "name": "isos", + "nameOrig": "isos", + "size": 0, + "splitLevel": "", + "used": 33619300, + }, + { + "allocator": "highwater", + "cache": false, + "cachePool": "cache", + "color": "yellow-on", + "comment": "system data", + "cow": "auto", + "exclude": [], + "floor": "0", + "free": 9309372, + "id": "system", + "include": [], + "luksStatus": "0", + "name": "system", + "nameOrig": "system", + "size": 0, + "splitLevel": "1", + "used": 33619300, + }, + ] + `); expect(nfsShares).toMatchInlineSnapshot(` [ { @@ -957,7 +961,7 @@ test('After init returns values from cfg file for all fields', async () => { "configErrorState": "INELIGIBLE", "configValid": false, "csrfToken": "0000000000000000", - "defaultFsType": "xfs", + "defaultFsType": "XFS", "deviceCount": 4, "domain": "", "domainLogin": "Administrator", diff --git a/api/src/__test__/store/state-parsers/shares.test.ts b/api/src/__test__/store/state-parsers/shares.test.ts index a0769dc14..6435a72ed 100644 --- a/api/src/__test__/store/state-parsers/shares.test.ts +++ b/api/src/__test__/store/state-parsers/shares.test.ts @@ -15,79 +15,83 @@ test('Returns parsed state file', async () => { type: 'ini', }); expect(parse(stateFile)).toMatchInlineSnapshot(` - [ - { - "allocator": "highwater", - "cache": false, - "cachePool": "cache", - "color": "yellow-on", - "comment": "", - "cow": "auto", - "exclude": [], - "floor": "0", - "free": 9309372, - "include": [], - "luksStatus": "0", - "name": "appdata", - "nameOrig": "appdata", - "size": 0, - "splitLevel": "", - "used": 33619300, - }, - { - "allocator": "highwater", - "cache": false, - "cachePool": "cache", - "color": "yellow-on", - "comment": "saved VM instances", - "cow": "auto", - "exclude": [], - "floor": "0", - "free": 9309372, - "include": [], - "luksStatus": "0", - "name": "domains", - "nameOrig": "domains", - "size": 0, - "splitLevel": "1", - "used": 33619300, - }, - { - "allocator": "highwater", - "cache": true, - "cachePool": "cache", - "color": "yellow-on", - "comment": "ISO images", - "cow": "auto", - "exclude": [], - "floor": "0", - "free": 9309372, - "include": [], - "luksStatus": "0", - "name": "isos", - "nameOrig": "isos", - "size": 0, - "splitLevel": "", - "used": 33619300, - }, - { - "allocator": "highwater", - "cache": false, - "cachePool": "cache", - "color": "yellow-on", - "comment": "system data", - "cow": "auto", - "exclude": [], - "floor": "0", - "free": 9309372, - "include": [], - "luksStatus": "0", - "name": "system", - "nameOrig": "system", - "size": 0, - "splitLevel": "1", - "used": 33619300, - }, - ] - `); + [ + { + "allocator": "highwater", + "cache": false, + "cachePool": "cache", + "color": "yellow-on", + "comment": "", + "cow": "auto", + "exclude": [], + "floor": "0", + "free": 9309372, + "id": "appdata", + "include": [], + "luksStatus": "0", + "name": "appdata", + "nameOrig": "appdata", + "size": 0, + "splitLevel": "", + "used": 33619300, + }, + { + "allocator": "highwater", + "cache": false, + "cachePool": "cache", + "color": "yellow-on", + "comment": "saved VM instances", + "cow": "auto", + "exclude": [], + "floor": "0", + "free": 9309372, + "id": "domains", + "include": [], + "luksStatus": "0", + "name": "domains", + "nameOrig": "domains", + "size": 0, + "splitLevel": "1", + "used": 33619300, + }, + { + "allocator": "highwater", + "cache": true, + "cachePool": "cache", + "color": "yellow-on", + "comment": "ISO images", + "cow": "auto", + "exclude": [], + "floor": "0", + "free": 9309372, + "id": "isos", + "include": [], + "luksStatus": "0", + "name": "isos", + "nameOrig": "isos", + "size": 0, + "splitLevel": "", + "used": 33619300, + }, + { + "allocator": "highwater", + "cache": false, + "cachePool": "cache", + "color": "yellow-on", + "comment": "system data", + "cow": "auto", + "exclude": [], + "floor": "0", + "free": 9309372, + "id": "system", + "include": [], + "luksStatus": "0", + "name": "system", + "nameOrig": "system", + "size": 0, + "splitLevel": "1", + "used": 33619300, + }, + ] + `); }); diff --git a/api/src/__test__/store/state-parsers/slots.test.ts b/api/src/__test__/store/state-parsers/slots.test.ts index 3c1e1064d..312ce8b81 100644 --- a/api/src/__test__/store/state-parsers/slots.test.ts +++ b/api/src/__test__/store/state-parsers/slots.test.ts @@ -15,175 +15,175 @@ test('Returns parsed state file', async () => { type: 'ini', }); expect(parse(stateFile)).toMatchInlineSnapshot(` - [ - { - "comment": null, - "critical": null, - "device": "sdh", - "exportable": false, - "format": "GPT: 4KiB-aligned", - "fsFree": null, - "fsSize": null, - "fsType": null, - "fsUsed": null, - "id": "ST18000NM000J-2TV103_ZR585CPY", - "idx": 0, - "name": "parity", - "numErrors": 0, - "numReads": 0, - "numWrites": 0, - "rotational": true, - "size": 17578328012, - "status": "DISK_OK", - "temp": 25, - "transport": "ata", - "type": "Parity", - "warning": null, - }, - { - "comment": "Seagate Exos", - "critical": 75, - "device": "sdf", - "exportable": false, - "format": "GPT: 4KiB-aligned", - "fsFree": 13882739732, - "fsSize": 17998742753, - "fsType": "xfs", - "fsUsed": 4116003021, - "id": "ST18000NM000J-2TV103_ZR5B1W9X", - "idx": 1, - "name": "disk1", - "numErrors": 0, - "numReads": 0, - "numWrites": 0, - "rotational": true, - "size": 17578328012, - "status": "DISK_OK", - "temp": 30, - "transport": "ata", - "type": "Data", - "warning": 50, - }, - { - "comment": "", - "critical": null, - "device": "sdj", - "exportable": false, - "format": "GPT: 4KiB-aligned", - "fsFree": 93140746, - "fsSize": 11998001574, - "fsType": "xfs", - "fsUsed": 11904860828, - "id": "WDC_WD120EDAZ-11F3RA0_5PJRD45C", - "idx": 2, - "name": "disk2", - "numErrors": 0, - "numReads": 0, - "numWrites": 0, - "rotational": true, - "size": 11718885324, - "status": "DISK_OK", - "temp": 30, - "transport": "ata", - "type": "Data", - "warning": null, - }, - { - "comment": "", - "critical": null, - "device": "sde", - "exportable": false, - "format": "GPT: 4KiB-aligned", - "fsFree": 5519945093, - "fsSize": 11998001574, - "fsType": "xfs", - "fsUsed": 6478056481, - "id": "WDC_WD120EMAZ-11BLFA0_5PH8BTYD", - "idx": 3, - "name": "disk3", - "numErrors": 0, - "numReads": 0, - "numWrites": 0, - "rotational": true, - "size": 11718885324, - "status": "DISK_OK", - "temp": 30, - "transport": "ata", - "type": "Data", - "warning": null, - }, - { - "comment": "", - "critical": null, - "device": "sdi", - "exportable": false, - "format": "MBR: 4KiB-aligned", - "fsFree": 111810683, - "fsSize": 250059317, - "fsType": "btrfs", - "fsUsed": 137273827, - "id": "Samsung_SSD_850_EVO_250GB_S2R5NX0H643734Z", - "idx": 30, - "name": "cache", - "numErrors": 0, - "numReads": 0, - "numWrites": 0, - "rotational": false, - "size": 244198552, - "status": "DISK_OK", - "temp": 22, - "transport": "ata", - "type": "Cache", - "warning": null, - }, - { - "comment": null, - "critical": null, - "device": "nvme0n1", - "exportable": false, - "format": "MBR: 4KiB-aligned", - "fsFree": null, - "fsSize": null, - "fsType": null, - "fsUsed": null, - "id": "KINGSTON_SA2000M8250G_50026B7282669D9E", - "idx": 31, - "name": "cache2", - "numErrors": 0, - "numReads": 0, - "numWrites": 0, - "rotational": false, - "size": 244198552, - "status": "DISK_OK", - "temp": 27, - "transport": "nvme", - "type": "Cache", - "warning": null, - }, - { - "comment": "Unraid OS boot device", - "critical": null, - "device": "sda", - "exportable": true, - "format": "unknown", - "fsFree": 3191407, - "fsSize": 4042732, - "fsType": "vfat", - "fsUsed": 851325, - "id": "Cruzer", - "idx": 32, - "name": "flash", - "numErrors": 0, - "numReads": 0, - "numWrites": 0, - "rotational": true, - "size": 3956700, - "status": "DISK_OK", - "temp": null, - "transport": "usb", - "type": "Flash", - "warning": null, - }, - ] - `); + [ + { + "comment": null, + "critical": null, + "device": "sdh", + "exportable": false, + "format": "GPT: 4KiB-aligned", + "fsFree": null, + "fsSize": null, + "fsType": null, + "fsUsed": null, + "id": "ST18000NM000J-2TV103_ZR585CPY", + "idx": 0, + "name": "parity", + "numErrors": 0, + "numReads": 0, + "numWrites": 0, + "rotational": true, + "size": 17578328012, + "status": "DISK_OK", + "temp": 25, + "transport": "ata", + "type": "PARITY", + "warning": null, + }, + { + "comment": "Seagate Exos", + "critical": 75, + "device": "sdf", + "exportable": false, + "format": "GPT: 4KiB-aligned", + "fsFree": 13882739732, + "fsSize": 17998742753, + "fsType": "xfs", + "fsUsed": 4116003021, + "id": "ST18000NM000J-2TV103_ZR5B1W9X", + "idx": 1, + "name": "disk1", + "numErrors": 0, + "numReads": 0, + "numWrites": 0, + "rotational": true, + "size": 17578328012, + "status": "DISK_OK", + "temp": 30, + "transport": "ata", + "type": "DATA", + "warning": 50, + }, + { + "comment": "", + "critical": null, + "device": "sdj", + "exportable": false, + "format": "GPT: 4KiB-aligned", + "fsFree": 93140746, + "fsSize": 11998001574, + "fsType": "xfs", + "fsUsed": 11904860828, + "id": "WDC_WD120EDAZ-11F3RA0_5PJRD45C", + "idx": 2, + "name": "disk2", + "numErrors": 0, + "numReads": 0, + "numWrites": 0, + "rotational": true, + "size": 11718885324, + "status": "DISK_OK", + "temp": 30, + "transport": "ata", + "type": "DATA", + "warning": null, + }, + { + "comment": "", + "critical": null, + "device": "sde", + "exportable": false, + "format": "GPT: 4KiB-aligned", + "fsFree": 5519945093, + "fsSize": 11998001574, + "fsType": "xfs", + "fsUsed": 6478056481, + "id": "WDC_WD120EMAZ-11BLFA0_5PH8BTYD", + "idx": 3, + "name": "disk3", + "numErrors": 0, + "numReads": 0, + "numWrites": 0, + "rotational": true, + "size": 11718885324, + "status": "DISK_OK", + "temp": 30, + "transport": "ata", + "type": "DATA", + "warning": null, + }, + { + "comment": "", + "critical": null, + "device": "sdi", + "exportable": false, + "format": "MBR: 4KiB-aligned", + "fsFree": 111810683, + "fsSize": 250059317, + "fsType": "btrfs", + "fsUsed": 137273827, + "id": "Samsung_SSD_850_EVO_250GB_S2R5NX0H643734Z", + "idx": 30, + "name": "cache", + "numErrors": 0, + "numReads": 0, + "numWrites": 0, + "rotational": false, + "size": 244198552, + "status": "DISK_OK", + "temp": 22, + "transport": "ata", + "type": "CACHE", + "warning": null, + }, + { + "comment": null, + "critical": null, + "device": "nvme0n1", + "exportable": false, + "format": "MBR: 4KiB-aligned", + "fsFree": null, + "fsSize": null, + "fsType": null, + "fsUsed": null, + "id": "KINGSTON_SA2000M8250G_50026B7282669D9E", + "idx": 31, + "name": "cache2", + "numErrors": 0, + "numReads": 0, + "numWrites": 0, + "rotational": false, + "size": 244198552, + "status": "DISK_OK", + "temp": 27, + "transport": "nvme", + "type": "CACHE", + "warning": null, + }, + { + "comment": "Unraid OS boot device", + "critical": null, + "device": "sda", + "exportable": true, + "format": "unknown", + "fsFree": 3191407, + "fsSize": 4042732, + "fsType": "vfat", + "fsUsed": 851325, + "id": "Cruzer", + "idx": 32, + "name": "flash", + "numErrors": 0, + "numReads": 0, + "numWrites": 0, + "rotational": true, + "size": 3956700, + "status": "DISK_OK", + "temp": null, + "transport": "usb", + "type": "FLASH", + "warning": null, + }, + ] + `); }); diff --git a/api/src/__test__/store/state-parsers/var.test.ts b/api/src/__test__/store/state-parsers/var.test.ts index 98cf75099..b0a244faa 100644 --- a/api/src/__test__/store/state-parsers/var.test.ts +++ b/api/src/__test__/store/state-parsers/var.test.ts @@ -24,7 +24,7 @@ test('Returns parsed state file', async () => { "configErrorState": "INELIGIBLE", "configValid": false, "csrfToken": "0000000000000000", - "defaultFsType": "xfs", + "defaultFsType": "XFS", "deviceCount": 4, "domain": "", "domainLogin": "Administrator", diff --git a/api/src/core/modules/array/get-array-data.ts b/api/src/core/modules/array/get-array-data.ts index 20285aa0a..8b01d5b60 100644 --- a/api/src/core/modules/array/get-array-data.ts +++ b/api/src/core/modules/array/get-array-data.ts @@ -1,12 +1,16 @@ import { GraphQLError } from 'graphql'; import { sum } from 'lodash-es'; -import type { ArrayCapacity, ArrayType } from '@app/graphql/generated/api/types.js'; -import { ArrayDiskType } from '@app/graphql/generated/api/types.js'; import { store } from '@app/store/index.js'; import { FileLoadStatus } from '@app/store/types.js'; +import { + ArrayCapacity, + ArrayDiskType, + ArrayState, + UnraidArray, +} from '@app/unraid-api/graph/resolvers/array/array.model.js'; -export const getArrayData = (getState = store.getState): ArrayType => { +export const getArrayData = (getState = store.getState): UnraidArray => { // Var state isn't loaded const state = getState(); if ( @@ -51,7 +55,7 @@ export const getArrayData = (getState = store.getState): ArrayType => { return { id: 'array', - state: emhttp.var.mdState, + state: emhttp.var.mdState as ArrayState, capacity, boot, parities, diff --git a/api/src/core/modules/disks/id/get-disk.ts b/api/src/core/modules/disks/id/get-disk.ts index f8802847c..33c9731c9 100644 --- a/api/src/core/modules/disks/id/get-disk.ts +++ b/api/src/core/modules/disks/id/get-disk.ts @@ -1,6 +1,6 @@ import { AppError } from '@app/core/errors/app-error.js'; import { type CoreContext, type CoreResult } from '@app/core/types/index.js'; -import { Disk } from '@app/graphql/generated/api/types.js'; +import { ArrayDisk } from '@app/unraid-api/graph/resolvers/array/array.model.js'; interface Context extends CoreContext { params: { @@ -11,7 +11,7 @@ interface Context extends CoreContext { /** * Get a single disk. */ -export const getDisk = async (context: Context, Disks: Disk[]): Promise => { +export const getDisk = async (context: Context, Disks: ArrayDisk[]): Promise => { const { params } = context; const { id } = params; diff --git a/api/src/core/modules/get-parity-history.ts b/api/src/core/modules/get-parity-history.ts deleted file mode 100644 index e83306ee6..000000000 --- a/api/src/core/modules/get-parity-history.ts +++ /dev/null @@ -1,62 +0,0 @@ -import { promises as fs } from 'fs'; - -import Table from 'cli-table'; - -import { FileMissingError } from '@app/core/errors/file-missing-error.js'; -import { type CoreContext, type CoreResult } from '@app/core/types/index.js'; -import { ensurePermission } from '@app/core/utils/permissions/ensure-permission.js'; -import { getters } from '@app/store/index.js'; - -/** - * Get parity history. - * @returns All parity checks with their respective date, duration, speed, status and errors. - */ -export const getParityHistory = async (context: CoreContext): Promise => { - const { user } = context; - - // Bail if the user doesn't have permission - ensurePermission(user, { - resource: 'parity-history', - action: 'read', - possession: 'any', - }); - - const historyFilePath = getters.paths()['parity-checks']; - const history = await fs.readFile(historyFilePath).catch(() => { - throw new FileMissingError(historyFilePath); - }); - - // Convert checks into array of objects - const lines = history.toString().trim().split('\n').reverse(); - const parityChecks = lines.map((line) => { - const [date, duration, speed, status, errors = '0'] = line.split('|'); - return { - date, - duration: Number.parseInt(duration, 10), - speed, - status, - errors: Number.parseInt(errors, 10), - }; - }); - - // Create table for text output - const table = new Table({ - head: ['Date', 'Duration', 'Speed', 'Status', 'Errors'], - }); - // Update raw values with strings - parityChecks.forEach((check) => { - const array = Object.values({ - date: check.date, - speed: check.speed ? check.speed : 'Unavailable', - duration: check.duration >= 0 ? check.duration.toString() : 'Unavailable', - status: check.status === '-4' ? 'Cancelled' : 'OK', - errors: check.errors.toString(), - }); - table.push(array); - }); - - return { - text: table.toString(), - json: parityChecks, - }; -}; diff --git a/api/src/core/modules/index.ts b/api/src/core/modules/index.ts index 00167fc60..c8a028e12 100644 --- a/api/src/core/modules/index.ts +++ b/api/src/core/modules/index.ts @@ -10,7 +10,6 @@ export * from './add-share.js'; export * from './add-user.js'; export * from './get-apps.js'; export * from './get-devices.js'; -export * from './get-parity-history.js'; export * from './get-services.js'; export * from './get-users.js'; export * from './get-welcome.js'; diff --git a/api/src/core/pubsub.ts b/api/src/core/pubsub.ts index ea0cf320d..b1dbf54e3 100644 --- a/api/src/core/pubsub.ts +++ b/api/src/core/pubsub.ts @@ -19,6 +19,7 @@ export enum PUBSUB_CHANNEL { VMS = 'VMS', REGISTRATION = 'REGISTRATION', LOG_FILE = 'LOG_FILE', + PARITY = 'PARITY', } export const pubsub = new PubSub({ eventEmitter }); diff --git a/api/src/core/types/states/var.ts b/api/src/core/types/states/var.ts index 0137e0a2b..f76d66720 100644 --- a/api/src/core/types/states/var.ts +++ b/api/src/core/types/states/var.ts @@ -1,10 +1,10 @@ -import type { - ArrayState, - DiskFsType, +import { ArrayState } from '@app/unraid-api/graph/resolvers/array/array.model.js'; +import { DiskFsType } from '@app/unraid-api/graph/resolvers/disks/disks.model.js'; +import { RegistrationState, - registrationType, -} from '@app/graphql/generated/api/types.js'; -import { ConfigErrorState } from '@app/graphql/generated/api/types.js'; + RegistrationType, +} from '@app/unraid-api/graph/resolvers/registration/registration.model.js'; +import { ConfigErrorState } from '@app/unraid-api/graph/resolvers/vars/vars.model.js'; /** * Global vars @@ -128,7 +128,7 @@ export type Var = { /** Who the current Unraid key is registered to. */ regTo: string; /** Which type of key this is. */ - regTy: registrationType; + regTy: RegistrationType; /** Is the server currently in safe mode. */ safeMode: boolean; sbClean: boolean; diff --git a/api/src/core/utils/array/array-is-running.ts b/api/src/core/utils/array/array-is-running.ts index f94835dd5..f7af3bd84 100644 --- a/api/src/core/utils/array/array-is-running.ts +++ b/api/src/core/utils/array/array-is-running.ts @@ -1,5 +1,5 @@ -import { ArrayState } from '@app/graphql/generated/api/types.js'; import { getters } from '@app/store/index.js'; +import { ArrayState } from '@app/unraid-api/graph/resolvers/array/array.model.js'; /** * Is the array running? diff --git a/api/src/core/utils/shares/process-share.ts b/api/src/core/utils/shares/process-share.ts index f3596d1f8..7f47e4554 100644 --- a/api/src/core/utils/shares/process-share.ts +++ b/api/src/core/utils/shares/process-share.ts @@ -1,6 +1,6 @@ import type { DiskShare, Share, UserShare } from '@app/core/types/states/share.js'; -import type { ArrayDisk } from '@app/graphql/generated/api/types.js'; import { getters } from '@app/store/index.js'; +import { ArrayDisk } from '@app/unraid-api/graph/resolvers/array/array.model.js'; const processors = { user(share: Share) { diff --git a/api/src/core/utils/vms/index.ts b/api/src/core/utils/vms/index.ts index 884abbce7..e06ef4b60 100644 --- a/api/src/core/utils/vms/index.ts +++ b/api/src/core/utils/vms/index.ts @@ -2,4 +2,3 @@ export * from './filter-devices.js'; export * from './get-pci-devices.js'; -export * from './system-network-interfaces.js'; diff --git a/api/src/core/utils/vms/system-network-interfaces.ts b/api/src/core/utils/vms/system-network-interfaces.ts deleted file mode 100644 index 4d5bc2173..000000000 --- a/api/src/core/utils/vms/system-network-interfaces.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { networkInterfaces } from 'systeminformation'; - -export const systemNetworkInterfaces = networkInterfaces(); diff --git a/api/src/graphql/generated/api/operations.ts b/api/src/graphql/generated/api/operations.ts deleted file mode 100755 index b06ddec41..000000000 --- a/api/src/graphql/generated/api/operations.ts +++ /dev/null @@ -1,1475 +0,0 @@ -/* eslint-disable */ -import * as Types from '@app/graphql/generated/api/types.js'; - -import { z } from 'zod' -import { AccessUrl, AccessUrlInput, AddPermissionInput, AddRoleForApiKeyInput, AddRoleForUserInput, AllowedOriginInput, ApiKey, ApiKeyResponse, ApiKeyWithSecret, ApiSettingsInput, ArrayType, ArrayCapacity, ArrayDisk, ArrayDiskFsColor, ArrayDiskInput, ArrayDiskStatus, ArrayDiskType, ArrayMutations, ArrayMutationsaddDiskToArrayArgs, ArrayMutationsclearArrayDiskStatisticsArgs, ArrayMutationsmountArrayDiskArgs, ArrayMutationsremoveDiskFromArrayArgs, ArrayMutationssetStateArgs, ArrayMutationsunmountArrayDiskArgs, ArrayPendingState, ArrayState, ArrayStateInput, ArrayStateInputState, AuthActionVerb, AuthPossession, Baseboard, Capacity, Case, Cloud, CloudResponse, Config, ConfigErrorState, Connect, ConnectSettings, ConnectSettingsValues, ConnectSignInInput, ConnectUserInfoInput, ContainerHostConfig, ContainerMount, ContainerPort, ContainerPortType, ContainerState, CreateApiKeyInput, Devices, Disk, DiskFsType, DiskInterfaceType, DiskPartition, DiskSmartStatus, Display, Docker, DockerContainer, DockerMutations, DockerMutationsstartArgs, DockerMutationsstopArgs, DockerNetwork, DynamicRemoteAccessStatus, DynamicRemoteAccessType, EnableDynamicRemoteAccessInput, Flash, Gpu, Importance, Info, InfoApps, InfoCpu, InfoMemory, KeyFile, LogFile, LogFileContent, Me, MemoryFormFactor, MemoryLayout, MemoryType, MinigraphStatus, MinigraphqlResponse, Mount, Network, Node, Notification, NotificationCounts, NotificationData, NotificationFilter, NotificationOverview, NotificationType, Notifications, NotificationslistArgs, Os, Owner, ParityCheck, Partition, Pci, Permission, ProfileModel, Registration, RegistrationState, RelayResponse, RemoteAccess, RemoveRoleFromApiKeyInput, Resource, Role, Server, ServerStatus, Service, SetupRemoteAccessInput, Share, System, Temperature, Theme, URL_TYPE, UnassignedDevice, Uptime, Usb, User, UserAccount, Vars, Versions, VmDomain, VmMutations, VmMutationsforceStopVmArgs, VmMutationspauseVmArgs, VmMutationsrebootVmArgs, VmMutationsresetVmArgs, VmMutationsresumeVmArgs, VmMutationsstartVmArgs, VmMutationsstopVmArgs, VmState, Vms, WAN_ACCESS_TYPE, WAN_FORWARD_TYPE, Welcome, addUserInput, deleteUserInput, mdState, registrationType, usersInput } from '@app/graphql/generated/api/types.js' -import { TypedDocumentNode as DocumentNode } from '@graphql-typed-document-node/core'; - -type Properties = Required<{ - [K in keyof T]: z.ZodType; -}>; - -type definedNonNullAny = {}; - -export const isDefinedNonNullAny = (v: any): v is definedNonNullAny => v !== undefined && v !== null; - -export const definedNonNullAnySchema = z.any().refine((v) => isDefinedNonNullAny(v)); - -export const ArrayDiskFsColorSchema = z.nativeEnum(ArrayDiskFsColor); - -export const ArrayDiskStatusSchema = z.nativeEnum(ArrayDiskStatus); - -export const ArrayDiskTypeSchema = z.nativeEnum(ArrayDiskType); - -export const ArrayPendingStateSchema = z.nativeEnum(ArrayPendingState); - -export const ArrayStateSchema = z.nativeEnum(ArrayState); - -export const ArrayStateInputStateSchema = z.nativeEnum(ArrayStateInputState); - -export const AuthActionVerbSchema = z.nativeEnum(AuthActionVerb); - -export const AuthPossessionSchema = z.nativeEnum(AuthPossession); - -export const ConfigErrorStateSchema = z.nativeEnum(ConfigErrorState); - -export const ContainerPortTypeSchema = z.nativeEnum(ContainerPortType); - -export const ContainerStateSchema = z.nativeEnum(ContainerState); - -export const DiskFsTypeSchema = z.nativeEnum(DiskFsType); - -export const DiskInterfaceTypeSchema = z.nativeEnum(DiskInterfaceType); - -export const DiskSmartStatusSchema = z.nativeEnum(DiskSmartStatus); - -export const DynamicRemoteAccessTypeSchema = z.nativeEnum(DynamicRemoteAccessType); - -export const ImportanceSchema = z.nativeEnum(Importance); - -export const MemoryFormFactorSchema = z.nativeEnum(MemoryFormFactor); - -export const MemoryTypeSchema = z.nativeEnum(MemoryType); - -export const MinigraphStatusSchema = z.nativeEnum(MinigraphStatus); - -export const NotificationTypeSchema = z.nativeEnum(NotificationType); - -export const RegistrationStateSchema = z.nativeEnum(RegistrationState); - -export const ResourceSchema = z.nativeEnum(Resource); - -export const RoleSchema = z.nativeEnum(Role); - -export const ServerStatusSchema = z.nativeEnum(ServerStatus); - -export const TemperatureSchema = z.nativeEnum(Temperature); - -export const ThemeSchema = z.nativeEnum(Theme); - -export const URL_TYPESchema = z.nativeEnum(URL_TYPE); - -export const VmStateSchema = z.nativeEnum(VmState); - -export const WAN_ACCESS_TYPESchema = z.nativeEnum(WAN_ACCESS_TYPE); - -export const WAN_FORWARD_TYPESchema = z.nativeEnum(WAN_FORWARD_TYPE); - -export const mdStateSchema = z.nativeEnum(mdState); - -export const registrationTypeSchema = z.nativeEnum(registrationType); - -export function AccessUrlSchema(): z.ZodObject> { - return z.object({ - __typename: z.literal('AccessUrl').optional(), - ipv4: z.instanceof(URL).nullish(), - ipv6: z.instanceof(URL).nullish(), - name: z.string().nullish(), - type: URL_TYPESchema - }) -} - -export function AccessUrlInputSchema(): z.ZodObject> { - return z.object({ - ipv4: z.instanceof(URL).nullish(), - ipv6: z.instanceof(URL).nullish(), - name: z.string().nullish(), - type: URL_TYPESchema - }) -} - -export function AddPermissionInputSchema(): z.ZodObject> { - return z.object({ - actions: z.array(z.string()), - resource: ResourceSchema - }) -} - -export function AddRoleForApiKeyInputSchema(): z.ZodObject> { - return z.object({ - apiKeyId: z.string(), - role: RoleSchema - }) -} - -export function AddRoleForUserInputSchema(): z.ZodObject> { - return z.object({ - role: RoleSchema, - userId: z.string() - }) -} - -export function AllowedOriginInputSchema(): z.ZodObject> { - return z.object({ - origins: z.array(z.string()) - }) -} - -export function ApiKeySchema(): z.ZodObject> { - return z.object({ - __typename: z.literal('ApiKey').optional(), - createdAt: z.string(), - description: z.string().nullish(), - id: z.string(), - name: z.string(), - permissions: z.array(PermissionSchema()), - roles: z.array(RoleSchema) - }) -} - -export function ApiKeyResponseSchema(): z.ZodObject> { - return z.object({ - __typename: z.literal('ApiKeyResponse').optional(), - error: z.string().nullish(), - valid: z.boolean() - }) -} - -export function ApiKeyWithSecretSchema(): z.ZodObject> { - return z.object({ - __typename: z.literal('ApiKeyWithSecret').optional(), - createdAt: z.string(), - description: z.string().nullish(), - id: z.string(), - key: z.string(), - name: z.string(), - permissions: z.array(PermissionSchema()), - roles: z.array(RoleSchema) - }) -} - -export function ApiSettingsInputSchema(): z.ZodObject> { - return z.object({ - accessType: WAN_ACCESS_TYPESchema.nullish(), - extraOrigins: z.array(z.string()).nullish(), - forwardType: WAN_FORWARD_TYPESchema.nullish(), - port: z.number().nullish(), - sandbox: z.boolean().nullish(), - ssoUserIds: z.array(z.string()).nullish() - }) -} - -export function ArrayTypeSchema(): z.ZodObject> { - return z.object({ - __typename: z.literal('Array').optional(), - boot: ArrayDiskSchema().nullish(), - caches: z.array(ArrayDiskSchema()), - capacity: ArrayCapacitySchema(), - disks: z.array(ArrayDiskSchema()), - id: z.string(), - parities: z.array(ArrayDiskSchema()), - pendingState: ArrayPendingStateSchema.nullish(), - previousState: ArrayStateSchema.nullish(), - state: ArrayStateSchema - }) -} - -export function ArrayCapacitySchema(): z.ZodObject> { - return z.object({ - __typename: z.literal('ArrayCapacity').optional(), - disks: CapacitySchema(), - kilobytes: CapacitySchema() - }) -} - -export function ArrayDiskSchema(): z.ZodObject> { - return z.object({ - __typename: z.literal('ArrayDisk').optional(), - color: ArrayDiskFsColorSchema.nullish(), - comment: z.string().nullish(), - critical: z.number().nullish(), - device: z.string().nullish(), - exportable: z.boolean().nullish(), - format: z.string().nullish(), - fsFree: z.number().nullish(), - fsSize: z.number().nullish(), - fsType: z.string().nullish(), - fsUsed: z.number().nullish(), - id: z.string(), - idx: z.number(), - name: z.string().nullish(), - numErrors: z.number(), - numReads: z.number(), - numWrites: z.number(), - rotational: z.boolean().nullish(), - size: z.number(), - status: ArrayDiskStatusSchema.nullish(), - temp: z.number().nullish(), - transport: z.string().nullish(), - type: ArrayDiskTypeSchema, - warning: z.number().nullish() - }) -} - -export function ArrayDiskInputSchema(): z.ZodObject> { - return z.object({ - id: z.string(), - slot: z.number().nullish() - }) -} - -export function ArrayMutationsSchema(): z.ZodObject> { - return z.object({ - __typename: z.literal('ArrayMutations').optional(), - addDiskToArray: ArrayTypeSchema().nullish(), - clearArrayDiskStatistics: z.record(z.string(), z.any()).nullish(), - mountArrayDisk: DiskSchema().nullish(), - removeDiskFromArray: ArrayTypeSchema().nullish(), - setState: ArrayTypeSchema().nullish(), - unmountArrayDisk: DiskSchema().nullish() - }) -} - -export function ArrayMutationsaddDiskToArrayArgsSchema(): z.ZodObject> { - return z.object({ - input: z.lazy(() => ArrayDiskInputSchema().nullish()) - }) -} - -export function ArrayMutationsclearArrayDiskStatisticsArgsSchema(): z.ZodObject> { - return z.object({ - id: z.string() - }) -} - -export function ArrayMutationsmountArrayDiskArgsSchema(): z.ZodObject> { - return z.object({ - id: z.string() - }) -} - -export function ArrayMutationsremoveDiskFromArrayArgsSchema(): z.ZodObject> { - return z.object({ - input: z.lazy(() => ArrayDiskInputSchema().nullish()) - }) -} - -export function ArrayMutationssetStateArgsSchema(): z.ZodObject> { - return z.object({ - input: z.lazy(() => ArrayStateInputSchema().nullish()) - }) -} - -export function ArrayMutationsunmountArrayDiskArgsSchema(): z.ZodObject> { - return z.object({ - id: z.string() - }) -} - -export function ArrayStateInputSchema(): z.ZodObject> { - return z.object({ - desiredState: z.lazy(() => ArrayStateInputStateSchema) - }) -} - -export function BaseboardSchema(): z.ZodObject> { - return z.object({ - __typename: z.literal('Baseboard').optional(), - assetTag: z.string().nullish(), - manufacturer: z.string(), - model: z.string().nullish(), - serial: z.string().nullish(), - version: z.string().nullish() - }) -} - -export function CapacitySchema(): z.ZodObject> { - return z.object({ - __typename: z.literal('Capacity').optional(), - free: z.string(), - total: z.string(), - used: z.string() - }) -} - -export function CaseSchema(): z.ZodObject> { - return z.object({ - __typename: z.literal('Case').optional(), - base64: z.string().nullish(), - error: z.string().nullish(), - icon: z.string().nullish(), - url: z.string().nullish() - }) -} - -export function CloudSchema(): z.ZodObject> { - return z.object({ - __typename: z.literal('Cloud').optional(), - allowedOrigins: z.array(z.string()), - apiKey: ApiKeyResponseSchema(), - cloud: CloudResponseSchema(), - error: z.string().nullish(), - minigraphql: MinigraphqlResponseSchema(), - relay: RelayResponseSchema().nullish() - }) -} - -export function CloudResponseSchema(): z.ZodObject> { - return z.object({ - __typename: z.literal('CloudResponse').optional(), - error: z.string().nullish(), - ip: z.string().nullish(), - status: z.string() - }) -} - -export function ConfigSchema(): z.ZodObject> { - return z.object({ - __typename: z.literal('Config').optional(), - error: ConfigErrorStateSchema.nullish(), - id: z.string(), - valid: z.boolean().nullish() - }) -} - -export function ConnectSchema(): z.ZodObject> { - return z.object({ - __typename: z.literal('Connect').optional(), - dynamicRemoteAccess: DynamicRemoteAccessStatusSchema(), - id: z.string(), - settings: ConnectSettingsSchema() - }) -} - -export function ConnectSettingsSchema(): z.ZodObject> { - return z.object({ - __typename: z.literal('ConnectSettings').optional(), - dataSchema: z.record(z.string(), z.any()), - id: z.string(), - uiSchema: z.record(z.string(), z.any()), - values: ConnectSettingsValuesSchema() - }) -} - -export function ConnectSettingsValuesSchema(): z.ZodObject> { - return z.object({ - __typename: z.literal('ConnectSettingsValues').optional(), - accessType: WAN_ACCESS_TYPESchema, - extraOrigins: z.array(z.string()), - forwardType: WAN_FORWARD_TYPESchema.nullish(), - port: z.number().nullish(), - sandbox: z.boolean(), - ssoUserIds: z.array(z.string()) - }) -} - -export function ConnectSignInInputSchema(): z.ZodObject> { - return z.object({ - accessToken: z.string().nullish(), - apiKey: z.string(), - idToken: z.string().nullish(), - refreshToken: z.string().nullish(), - userInfo: z.lazy(() => ConnectUserInfoInputSchema().nullish()) - }) -} - -export function ConnectUserInfoInputSchema(): z.ZodObject> { - return z.object({ - avatar: z.string().nullish(), - email: z.string(), - preferred_username: z.string() - }) -} - -export function ContainerHostConfigSchema(): z.ZodObject> { - return z.object({ - __typename: z.literal('ContainerHostConfig').optional(), - networkMode: z.string() - }) -} - -export function ContainerMountSchema(): z.ZodObject> { - return z.object({ - __typename: z.literal('ContainerMount').optional(), - destination: z.string(), - driver: z.string(), - mode: z.string(), - name: z.string(), - propagation: z.string(), - rw: z.boolean(), - source: z.string(), - type: z.string() - }) -} - -export function ContainerPortSchema(): z.ZodObject> { - return z.object({ - __typename: z.literal('ContainerPort').optional(), - ip: z.string().nullish(), - privatePort: z.number().nullish(), - publicPort: z.number().nullish(), - type: ContainerPortTypeSchema.nullish() - }) -} - -export function CreateApiKeyInputSchema(): z.ZodObject> { - return z.object({ - description: z.string().nullish(), - name: z.string(), - overwrite: z.boolean().nullish(), - permissions: z.array(z.lazy(() => AddPermissionInputSchema())).nullish(), - roles: z.array(RoleSchema).nullish() - }) -} - -export function DevicesSchema(): z.ZodObject> { - return z.object({ - __typename: z.literal('Devices').optional(), - gpu: z.array(GpuSchema().nullable()).nullish(), - network: z.array(NetworkSchema().nullable()).nullish(), - pci: z.array(PciSchema().nullable()).nullish(), - usb: z.array(UsbSchema().nullable()).nullish() - }) -} - -export function DiskSchema(): z.ZodObject> { - return z.object({ - __typename: z.literal('Disk').optional(), - bytesPerSector: z.number(), - device: z.string(), - firmwareRevision: z.string(), - id: z.string(), - interfaceType: DiskInterfaceTypeSchema, - name: z.string(), - partitions: z.array(DiskPartitionSchema()).nullish(), - sectorsPerTrack: z.number(), - serialNum: z.string(), - size: z.number(), - smartStatus: DiskSmartStatusSchema, - temperature: z.number().nullish(), - totalCylinders: z.number(), - totalHeads: z.number(), - totalSectors: z.number(), - totalTracks: z.number(), - tracksPerCylinder: z.number(), - type: z.string(), - vendor: z.string() - }) -} - -export function DiskPartitionSchema(): z.ZodObject> { - return z.object({ - __typename: z.literal('DiskPartition').optional(), - fsType: DiskFsTypeSchema, - name: z.string(), - size: z.number() - }) -} - -export function DisplaySchema(): z.ZodObject> { - return z.object({ - __typename: z.literal('Display').optional(), - banner: z.string().nullish(), - case: CaseSchema().nullish(), - critical: z.number().nullish(), - dashapps: z.string().nullish(), - date: z.string().nullish(), - hot: z.number().nullish(), - id: z.string(), - locale: z.string().nullish(), - max: z.number().nullish(), - number: z.string().nullish(), - resize: z.boolean().nullish(), - scale: z.boolean().nullish(), - tabs: z.boolean().nullish(), - text: z.boolean().nullish(), - theme: ThemeSchema.nullish(), - total: z.boolean().nullish(), - unit: TemperatureSchema.nullish(), - usage: z.boolean().nullish(), - users: z.string().nullish(), - warning: z.number().nullish(), - wwn: z.boolean().nullish() - }) -} - -export function DockerSchema(): z.ZodObject> { - return z.object({ - __typename: z.literal('Docker').optional(), - containers: z.array(DockerContainerSchema()).nullish(), - id: z.string(), - networks: z.array(DockerNetworkSchema()).nullish() - }) -} - -export function DockerContainerSchema(): z.ZodObject> { - return z.object({ - __typename: z.literal('DockerContainer').optional(), - autoStart: z.boolean(), - command: z.string(), - created: z.number(), - hostConfig: ContainerHostConfigSchema().nullish(), - id: z.string(), - image: z.string(), - imageId: z.string(), - labels: z.record(z.string(), z.any()).nullish(), - mounts: z.array(z.record(z.string(), z.any()).nullable()).nullish(), - names: z.array(z.string()).nullish(), - networkSettings: z.record(z.string(), z.any()).nullish(), - ports: z.array(ContainerPortSchema()), - sizeRootFs: z.number().nullish(), - state: ContainerStateSchema, - status: z.string() - }) -} - -export function DockerMutationsSchema(): z.ZodObject> { - return z.object({ - __typename: z.literal('DockerMutations').optional(), - start: DockerContainerSchema(), - stop: DockerContainerSchema() - }) -} - -export function DockerMutationsstartArgsSchema(): z.ZodObject> { - return z.object({ - id: z.string() - }) -} - -export function DockerMutationsstopArgsSchema(): z.ZodObject> { - return z.object({ - id: z.string() - }) -} - -export function DockerNetworkSchema(): z.ZodObject> { - return z.object({ - __typename: z.literal('DockerNetwork').optional(), - attachable: z.boolean(), - configFrom: z.record(z.string(), z.any()).nullish(), - configOnly: z.boolean(), - containers: z.record(z.string(), z.any()).nullish(), - created: z.string().nullish(), - driver: z.string().nullish(), - enableIPv6: z.boolean(), - id: z.string().nullish(), - ingress: z.boolean(), - internal: z.boolean(), - ipam: z.record(z.string(), z.any()).nullish(), - labels: z.record(z.string(), z.any()).nullish(), - name: z.string().nullish(), - options: z.record(z.string(), z.any()).nullish(), - scope: z.string().nullish() - }) -} - -export function DynamicRemoteAccessStatusSchema(): z.ZodObject> { - return z.object({ - __typename: z.literal('DynamicRemoteAccessStatus').optional(), - enabledType: DynamicRemoteAccessTypeSchema, - error: z.string().nullish(), - runningType: DynamicRemoteAccessTypeSchema - }) -} - -export function EnableDynamicRemoteAccessInputSchema(): z.ZodObject> { - return z.object({ - enabled: z.boolean(), - url: z.lazy(() => AccessUrlInputSchema()) - }) -} - -export function FlashSchema(): z.ZodObject> { - return z.object({ - __typename: z.literal('Flash').optional(), - guid: z.string().nullish(), - product: z.string().nullish(), - vendor: z.string().nullish() - }) -} - -export function GpuSchema(): z.ZodObject> { - return z.object({ - __typename: z.literal('Gpu').optional(), - blacklisted: z.boolean(), - class: z.string(), - id: z.string(), - productid: z.string(), - type: z.string(), - typeid: z.string(), - vendorname: z.string() - }) -} - -export function InfoSchema(): z.ZodObject> { - return z.object({ - __typename: z.literal('Info').optional(), - apps: InfoAppsSchema().nullish(), - baseboard: BaseboardSchema().nullish(), - cpu: InfoCpuSchema().nullish(), - devices: DevicesSchema().nullish(), - display: DisplaySchema().nullish(), - id: z.string(), - machineId: z.string().nullish(), - memory: InfoMemorySchema().nullish(), - os: OsSchema().nullish(), - system: SystemSchema().nullish(), - time: z.string(), - versions: VersionsSchema().nullish() - }) -} - -export function InfoAppsSchema(): z.ZodObject> { - return z.object({ - __typename: z.literal('InfoApps').optional(), - installed: z.number().nullish(), - started: z.number().nullish() - }) -} - -export function InfoCpuSchema(): z.ZodObject> { - return z.object({ - __typename: z.literal('InfoCpu').optional(), - brand: z.string(), - cache: z.record(z.string(), z.any()), - cores: z.number(), - family: z.string(), - flags: z.array(z.string()).nullish(), - manufacturer: z.string(), - model: z.string(), - processors: z.number(), - revision: z.string(), - socket: z.string(), - speed: z.number(), - speedmax: z.number(), - speedmin: z.number(), - stepping: z.number(), - threads: z.number(), - vendor: z.string(), - voltage: z.string().nullish() - }) -} - -export function InfoMemorySchema(): z.ZodObject> { - return z.object({ - __typename: z.literal('InfoMemory').optional(), - active: z.number(), - available: z.number(), - buffcache: z.number(), - free: z.number(), - layout: z.array(MemoryLayoutSchema()).nullish(), - max: z.number(), - swapfree: z.number(), - swaptotal: z.number(), - swapused: z.number(), - total: z.number(), - used: z.number() - }) -} - -export function KeyFileSchema(): z.ZodObject> { - return z.object({ - __typename: z.literal('KeyFile').optional(), - contents: z.string().nullish(), - location: z.string().nullish() - }) -} - -export function LogFileSchema(): z.ZodObject> { - return z.object({ - __typename: z.literal('LogFile').optional(), - modifiedAt: z.string(), - name: z.string(), - path: z.string(), - size: z.number() - }) -} - -export function LogFileContentSchema(): z.ZodObject> { - return z.object({ - __typename: z.literal('LogFileContent').optional(), - content: z.string(), - path: z.string(), - startLine: z.number().nullish(), - totalLines: z.number() - }) -} - -export function MeSchema(): z.ZodObject> { - return z.object({ - __typename: z.literal('Me').optional(), - description: z.string(), - id: z.string(), - name: z.string(), - permissions: z.array(PermissionSchema()).nullish(), - roles: z.array(RoleSchema) - }) -} - -export function MemoryLayoutSchema(): z.ZodObject> { - return z.object({ - __typename: z.literal('MemoryLayout').optional(), - bank: z.string().nullish(), - clockSpeed: z.number().nullish(), - formFactor: MemoryFormFactorSchema.nullish(), - manufacturer: z.string().nullish(), - partNum: z.string().nullish(), - serialNum: z.string().nullish(), - size: z.number(), - type: MemoryTypeSchema.nullish(), - voltageConfigured: z.number().nullish(), - voltageMax: z.number().nullish(), - voltageMin: z.number().nullish() - }) -} - -export function MinigraphqlResponseSchema(): z.ZodObject> { - return z.object({ - __typename: z.literal('MinigraphqlResponse').optional(), - error: z.string().nullish(), - status: MinigraphStatusSchema, - timeout: z.number().nullish() - }) -} - -export function MountSchema(): z.ZodObject> { - return z.object({ - __typename: z.literal('Mount').optional(), - directory: z.string().nullish(), - name: z.string().nullish(), - permissions: z.string().nullish(), - type: z.string().nullish() - }) -} - -export function NetworkSchema(): z.ZodObject> { - return z.object({ - __typename: z.literal('Network').optional(), - accessUrls: z.array(AccessUrlSchema()).nullish(), - carrierChanges: z.string().nullish(), - duplex: z.string().nullish(), - id: z.string(), - iface: z.string().nullish(), - ifaceName: z.string().nullish(), - internal: z.string().nullish(), - ipv4: z.string().nullish(), - ipv6: z.string().nullish(), - mac: z.string().nullish(), - mtu: z.string().nullish(), - operstate: z.string().nullish(), - speed: z.string().nullish(), - type: z.string().nullish() - }) -} - -export function NodeSchema(): z.ZodObject> { - return z.object({ - id: z.string() - }) -} - -export function NotificationSchema(): z.ZodObject> { - return z.object({ - __typename: z.literal('Notification').optional(), - description: z.string(), - formattedTimestamp: z.string().nullish(), - id: z.string(), - importance: ImportanceSchema, - link: z.string().nullish(), - subject: z.string(), - timestamp: z.string().nullish(), - title: z.string(), - type: NotificationTypeSchema - }) -} - -export function NotificationCountsSchema(): z.ZodObject> { - return z.object({ - __typename: z.literal('NotificationCounts').optional(), - alert: z.number(), - info: z.number(), - total: z.number(), - warning: z.number() - }) -} - -export function NotificationDataSchema(): z.ZodObject> { - return z.object({ - description: z.string(), - importance: ImportanceSchema, - link: z.string().nullish(), - subject: z.string(), - title: z.string() - }) -} - -export function NotificationFilterSchema(): z.ZodObject> { - return z.object({ - importance: ImportanceSchema.nullish(), - limit: z.number(), - offset: z.number(), - type: NotificationTypeSchema.nullish() - }) -} - -export function NotificationOverviewSchema(): z.ZodObject> { - return z.object({ - __typename: z.literal('NotificationOverview').optional(), - archive: NotificationCountsSchema(), - unread: NotificationCountsSchema() - }) -} - -export function NotificationsSchema(): z.ZodObject> { - return z.object({ - __typename: z.literal('Notifications').optional(), - id: z.string(), - list: z.array(NotificationSchema()), - overview: NotificationOverviewSchema() - }) -} - -export function NotificationslistArgsSchema(): z.ZodObject> { - return z.object({ - filter: NotificationFilterSchema() - }) -} - -export function OsSchema(): z.ZodObject> { - return z.object({ - __typename: z.literal('Os').optional(), - arch: z.string().nullish(), - build: z.string().nullish(), - codename: z.string().nullish(), - codepage: z.string().nullish(), - distro: z.string().nullish(), - hostname: z.string().nullish(), - kernel: z.string().nullish(), - logofile: z.string().nullish(), - platform: z.string().nullish(), - release: z.string().nullish(), - serial: z.string().nullish(), - uptime: z.string().nullish() - }) -} - -export function OwnerSchema(): z.ZodObject> { - return z.object({ - __typename: z.literal('Owner').optional(), - avatar: z.string().nullish(), - url: z.string().nullish(), - username: z.string().nullish() - }) -} - -export function ParityCheckSchema(): z.ZodObject> { - return z.object({ - __typename: z.literal('ParityCheck').optional(), - date: z.string(), - duration: z.number(), - errors: z.string(), - speed: z.string(), - status: z.string() - }) -} - -export function PartitionSchema(): z.ZodObject> { - return z.object({ - __typename: z.literal('Partition').optional(), - devlinks: z.string().nullish(), - devname: z.string().nullish(), - devpath: z.string().nullish(), - devtype: z.string().nullish(), - idAta: z.string().nullish(), - idAtaDownloadMicrocode: z.string().nullish(), - idAtaFeatureSetAam: z.string().nullish(), - idAtaFeatureSetAamCurrentValue: z.string().nullish(), - idAtaFeatureSetAamEnabled: z.string().nullish(), - idAtaFeatureSetAamVendorRecommendedValue: z.string().nullish(), - idAtaFeatureSetApm: z.string().nullish(), - idAtaFeatureSetApmCurrentValue: z.string().nullish(), - idAtaFeatureSetApmEnabled: z.string().nullish(), - idAtaFeatureSetHpa: z.string().nullish(), - idAtaFeatureSetHpaEnabled: z.string().nullish(), - idAtaFeatureSetPm: z.string().nullish(), - idAtaFeatureSetPmEnabled: z.string().nullish(), - idAtaFeatureSetPuis: z.string().nullish(), - idAtaFeatureSetPuisEnabled: z.string().nullish(), - idAtaFeatureSetSecurity: z.string().nullish(), - idAtaFeatureSetSecurityEnabled: z.string().nullish(), - idAtaFeatureSetSecurityEnhancedEraseUnitMin: z.string().nullish(), - idAtaFeatureSetSecurityEraseUnitMin: z.string().nullish(), - idAtaFeatureSetSmart: z.string().nullish(), - idAtaFeatureSetSmartEnabled: z.string().nullish(), - idAtaRotationRateRpm: z.string().nullish(), - idAtaSata: z.string().nullish(), - idAtaSataSignalRateGen1: z.string().nullish(), - idAtaSataSignalRateGen2: z.string().nullish(), - idAtaWriteCache: z.string().nullish(), - idAtaWriteCacheEnabled: z.string().nullish(), - idBus: z.string().nullish(), - idFsType: z.string().nullish(), - idFsUsage: z.string().nullish(), - idFsUuid: z.string().nullish(), - idFsUuidEnc: z.string().nullish(), - idModel: z.string().nullish(), - idModelEnc: z.string().nullish(), - idPartEntryDisk: z.string().nullish(), - idPartEntryNumber: z.string().nullish(), - idPartEntryOffset: z.string().nullish(), - idPartEntryScheme: z.string().nullish(), - idPartEntrySize: z.string().nullish(), - idPartEntryType: z.string().nullish(), - idPartTableType: z.string().nullish(), - idPath: z.string().nullish(), - idPathTag: z.string().nullish(), - idRevision: z.string().nullish(), - idSerial: z.string().nullish(), - idSerialShort: z.string().nullish(), - idType: z.string().nullish(), - idWwn: z.string().nullish(), - idWwnWithExtension: z.string().nullish(), - major: z.string().nullish(), - minor: z.string().nullish(), - partn: z.string().nullish(), - subsystem: z.string().nullish(), - usecInitialized: z.string().nullish() - }) -} - -export function PciSchema(): z.ZodObject> { - return z.object({ - __typename: z.literal('Pci').optional(), - blacklisted: z.string().nullish(), - class: z.string().nullish(), - id: z.string(), - productid: z.string().nullish(), - productname: z.string().nullish(), - type: z.string().nullish(), - typeid: z.string().nullish(), - vendorid: z.string().nullish(), - vendorname: z.string().nullish() - }) -} - -export function PermissionSchema(): z.ZodObject> { - return z.object({ - __typename: z.literal('Permission').optional(), - actions: z.array(z.string()), - resource: ResourceSchema - }) -} - -export function ProfileModelSchema(): z.ZodObject> { - return z.object({ - __typename: z.literal('ProfileModel').optional(), - avatar: z.string().nullish(), - url: z.string().nullish(), - userId: z.string().nullish(), - username: z.string().nullish() - }) -} - -export function RegistrationSchema(): z.ZodObject> { - return z.object({ - __typename: z.literal('Registration').optional(), - expiration: z.string().nullish(), - guid: z.string().nullish(), - keyFile: KeyFileSchema().nullish(), - state: RegistrationStateSchema.nullish(), - type: registrationTypeSchema.nullish(), - updateExpiration: z.string().nullish() - }) -} - -export function RelayResponseSchema(): z.ZodObject> { - return z.object({ - __typename: z.literal('RelayResponse').optional(), - error: z.string().nullish(), - status: z.string(), - timeout: z.string().nullish() - }) -} - -export function RemoteAccessSchema(): z.ZodObject> { - return z.object({ - __typename: z.literal('RemoteAccess').optional(), - accessType: WAN_ACCESS_TYPESchema, - forwardType: WAN_FORWARD_TYPESchema.nullish(), - port: z.number().nullish() - }) -} - -export function RemoveRoleFromApiKeyInputSchema(): z.ZodObject> { - return z.object({ - apiKeyId: z.string(), - role: RoleSchema - }) -} - -export function ServerSchema(): z.ZodObject> { - return z.object({ - __typename: z.literal('Server').optional(), - apikey: z.string(), - guid: z.string(), - lanip: z.string(), - localurl: z.string(), - name: z.string(), - owner: ProfileModelSchema(), - remoteurl: z.string(), - status: ServerStatusSchema, - wanip: z.string() - }) -} - -export function ServiceSchema(): z.ZodObject> { - return z.object({ - __typename: z.literal('Service').optional(), - id: z.string(), - name: z.string().nullish(), - online: z.boolean().nullish(), - uptime: UptimeSchema().nullish(), - version: z.string().nullish() - }) -} - -export function SetupRemoteAccessInputSchema(): z.ZodObject> { - return z.object({ - accessType: WAN_ACCESS_TYPESchema, - forwardType: WAN_FORWARD_TYPESchema.nullish(), - port: z.number().nullish() - }) -} - -export function ShareSchema(): z.ZodObject> { - return z.object({ - __typename: z.literal('Share').optional(), - allocator: z.string().nullish(), - cache: z.boolean().nullish(), - color: z.string().nullish(), - comment: z.string().nullish(), - cow: z.string().nullish(), - exclude: z.array(z.string().nullable()).nullish(), - floor: z.string().nullish(), - free: z.number().nullish(), - include: z.array(z.string().nullable()).nullish(), - luksStatus: z.string().nullish(), - name: z.string().nullish(), - nameOrig: z.string().nullish(), - size: z.number().nullish(), - splitLevel: z.string().nullish(), - used: z.number().nullish() - }) -} - -export function SystemSchema(): z.ZodObject> { - return z.object({ - __typename: z.literal('System').optional(), - manufacturer: z.string().nullish(), - model: z.string().nullish(), - serial: z.string().nullish(), - sku: z.string().nullish(), - uuid: z.string().nullish(), - version: z.string().nullish() - }) -} - -export function UnassignedDeviceSchema(): z.ZodObject> { - return z.object({ - __typename: z.literal('UnassignedDevice').optional(), - devlinks: z.string().nullish(), - devname: z.string().nullish(), - devpath: z.string().nullish(), - devtype: z.string().nullish(), - idAta: z.string().nullish(), - idAtaDownloadMicrocode: z.string().nullish(), - idAtaFeatureSetAam: z.string().nullish(), - idAtaFeatureSetAamCurrentValue: z.string().nullish(), - idAtaFeatureSetAamEnabled: z.string().nullish(), - idAtaFeatureSetAamVendorRecommendedValue: z.string().nullish(), - idAtaFeatureSetApm: z.string().nullish(), - idAtaFeatureSetApmCurrentValue: z.string().nullish(), - idAtaFeatureSetApmEnabled: z.string().nullish(), - idAtaFeatureSetHpa: z.string().nullish(), - idAtaFeatureSetHpaEnabled: z.string().nullish(), - idAtaFeatureSetPm: z.string().nullish(), - idAtaFeatureSetPmEnabled: z.string().nullish(), - idAtaFeatureSetPuis: z.string().nullish(), - idAtaFeatureSetPuisEnabled: z.string().nullish(), - idAtaFeatureSetSecurity: z.string().nullish(), - idAtaFeatureSetSecurityEnabled: z.string().nullish(), - idAtaFeatureSetSecurityEnhancedEraseUnitMin: z.string().nullish(), - idAtaFeatureSetSecurityEraseUnitMin: z.string().nullish(), - idAtaFeatureSetSmart: z.string().nullish(), - idAtaFeatureSetSmartEnabled: z.string().nullish(), - idAtaRotationRateRpm: z.string().nullish(), - idAtaSata: z.string().nullish(), - idAtaSataSignalRateGen1: z.string().nullish(), - idAtaSataSignalRateGen2: z.string().nullish(), - idAtaWriteCache: z.string().nullish(), - idAtaWriteCacheEnabled: z.string().nullish(), - idBus: z.string().nullish(), - idModel: z.string().nullish(), - idModelEnc: z.string().nullish(), - idPartTableType: z.string().nullish(), - idPath: z.string().nullish(), - idPathTag: z.string().nullish(), - idRevision: z.string().nullish(), - idSerial: z.string().nullish(), - idSerialShort: z.string().nullish(), - idType: z.string().nullish(), - idWwn: z.string().nullish(), - idWwnWithExtension: z.string().nullish(), - major: z.string().nullish(), - minor: z.string().nullish(), - mount: MountSchema().nullish(), - mounted: z.boolean().nullish(), - name: z.string().nullish(), - partitions: z.array(PartitionSchema().nullable()).nullish(), - subsystem: z.string().nullish(), - temp: z.number().nullish(), - usecInitialized: z.string().nullish() - }) -} - -export function UptimeSchema(): z.ZodObject> { - return z.object({ - __typename: z.literal('Uptime').optional(), - timestamp: z.string().nullish() - }) -} - -export function UsbSchema(): z.ZodObject> { - return z.object({ - __typename: z.literal('Usb').optional(), - id: z.string(), - name: z.string().nullish() - }) -} - -export function UserSchema(): z.ZodObject> { - return z.object({ - __typename: z.literal('User').optional(), - description: z.string(), - id: z.string(), - name: z.string(), - password: z.boolean().nullish(), - permissions: z.array(PermissionSchema()).nullish(), - roles: z.array(RoleSchema) - }) -} - -export function UserAccountSchema(): z.ZodObject> { - return z.object({ - description: z.string(), - id: z.string(), - name: z.string(), - permissions: z.array(PermissionSchema()).nullish(), - roles: z.array(RoleSchema) - }) -} - -export function VarsSchema(): z.ZodObject> { - return z.object({ - __typename: z.literal('Vars').optional(), - bindMgt: z.boolean().nullish(), - cacheNumDevices: z.number().nullish(), - cacheSbNumDisks: z.number().nullish(), - comment: z.string().nullish(), - configError: ConfigErrorStateSchema.nullish(), - configValid: z.boolean().nullish(), - csrfToken: z.string().nullish(), - defaultFormat: z.string().nullish(), - defaultFsType: z.string().nullish(), - deviceCount: z.number().nullish(), - domain: z.string().nullish(), - domainLogin: z.string().nullish(), - domainShort: z.string().nullish(), - enableFruit: z.string().nullish(), - flashGuid: z.string().nullish(), - flashProduct: z.string().nullish(), - flashVendor: z.string().nullish(), - fsCopyPrcnt: z.number().nullish(), - fsNumMounted: z.number().nullish(), - fsNumUnmountable: z.number().nullish(), - fsProgress: z.string().nullish(), - fsState: z.string().nullish(), - fsUnmountableMask: z.string().nullish(), - fuseDirectio: z.string().nullish(), - fuseDirectioDefault: z.string().nullish(), - fuseDirectioStatus: z.string().nullish(), - fuseRemember: z.string().nullish(), - fuseRememberDefault: z.string().nullish(), - fuseRememberStatus: z.string().nullish(), - hideDotFiles: z.boolean().nullish(), - id: z.string(), - joinStatus: z.string().nullish(), - localMaster: z.boolean().nullish(), - localTld: z.string().nullish(), - luksKeyfile: z.string().nullish(), - maxArraysz: z.number().nullish(), - maxCachesz: z.number().nullish(), - mdColor: z.string().nullish(), - mdNumDisabled: z.number().nullish(), - mdNumDisks: z.number().nullish(), - mdNumErased: z.number().nullish(), - mdNumInvalid: z.number().nullish(), - mdNumMissing: z.number().nullish(), - mdNumNew: z.number().nullish(), - mdNumStripes: z.number().nullish(), - mdNumStripesDefault: z.number().nullish(), - mdNumStripesStatus: z.string().nullish(), - mdResync: z.number().nullish(), - mdResyncAction: z.string().nullish(), - mdResyncCorr: z.string().nullish(), - mdResyncDb: z.string().nullish(), - mdResyncDt: z.string().nullish(), - mdResyncPos: z.string().nullish(), - mdResyncSize: z.number().nullish(), - mdState: z.string().nullish(), - mdSyncThresh: z.number().nullish(), - mdSyncThreshDefault: z.number().nullish(), - mdSyncThreshStatus: z.string().nullish(), - mdSyncWindow: z.number().nullish(), - mdSyncWindowDefault: z.number().nullish(), - mdSyncWindowStatus: z.string().nullish(), - mdVersion: z.string().nullish(), - mdWriteMethod: z.number().nullish(), - mdWriteMethodDefault: z.string().nullish(), - mdWriteMethodStatus: z.string().nullish(), - name: z.string().nullish(), - nrRequests: z.number().nullish(), - nrRequestsDefault: z.number().nullish(), - nrRequestsStatus: z.string().nullish(), - ntpServer1: z.string().nullish(), - ntpServer2: z.string().nullish(), - ntpServer3: z.string().nullish(), - ntpServer4: z.string().nullish(), - pollAttributes: z.string().nullish(), - pollAttributesDefault: z.string().nullish(), - pollAttributesStatus: z.string().nullish(), - port: z.number().nullish(), - portssh: z.number().nullish(), - portssl: z.number().nullish(), - porttelnet: z.number().nullish(), - queueDepth: z.string().nullish(), - regCheck: z.string().nullish(), - regFile: z.string().nullish(), - regGen: z.string().nullish(), - regGuid: z.string().nullish(), - regState: RegistrationStateSchema.nullish(), - regTm: z.string().nullish(), - regTm2: z.string().nullish(), - regTo: z.string().nullish(), - regTy: z.string().nullish(), - safeMode: z.boolean().nullish(), - sbClean: z.boolean().nullish(), - sbEvents: z.number().nullish(), - sbName: z.string().nullish(), - sbNumDisks: z.number().nullish(), - sbState: z.string().nullish(), - sbSyncErrs: z.number().nullish(), - sbSyncExit: z.string().nullish(), - sbSynced: z.number().nullish(), - sbSynced2: z.number().nullish(), - sbUpdated: z.string().nullish(), - sbVersion: z.string().nullish(), - security: z.string().nullish(), - shareAfpCount: z.number().nullish(), - shareAfpEnabled: z.boolean().nullish(), - shareAvahiAfpModel: z.string().nullish(), - shareAvahiAfpName: z.string().nullish(), - shareAvahiEnabled: z.boolean().nullish(), - shareAvahiSmbModel: z.string().nullish(), - shareAvahiSmbName: z.string().nullish(), - shareCacheEnabled: z.boolean().nullish(), - shareCacheFloor: z.string().nullish(), - shareCount: z.number().nullish(), - shareDisk: z.string().nullish(), - shareInitialGroup: z.string().nullish(), - shareInitialOwner: z.string().nullish(), - shareMoverActive: z.boolean().nullish(), - shareMoverLogging: z.boolean().nullish(), - shareMoverSchedule: z.string().nullish(), - shareNfsCount: z.number().nullish(), - shareNfsEnabled: z.boolean().nullish(), - shareSmbCount: z.number().nullish(), - shareSmbEnabled: z.boolean().nullish(), - shareUser: z.string().nullish(), - shareUserExclude: z.string().nullish(), - shareUserInclude: z.string().nullish(), - shutdownTimeout: z.number().nullish(), - spindownDelay: z.string().nullish(), - spinupGroups: z.boolean().nullish(), - startArray: z.boolean().nullish(), - startMode: z.string().nullish(), - startPage: z.string().nullish(), - sysArraySlots: z.number().nullish(), - sysCacheSlots: z.number().nullish(), - sysFlashSlots: z.number().nullish(), - sysModel: z.string().nullish(), - timeZone: z.string().nullish(), - useNtp: z.boolean().nullish(), - useSsh: z.boolean().nullish(), - useSsl: z.boolean().nullish(), - useTelnet: z.boolean().nullish(), - version: z.string().nullish(), - workgroup: z.string().nullish() - }) -} - -export function VersionsSchema(): z.ZodObject> { - return z.object({ - __typename: z.literal('Versions').optional(), - apache: z.string().nullish(), - docker: z.string().nullish(), - gcc: z.string().nullish(), - git: z.string().nullish(), - grunt: z.string().nullish(), - gulp: z.string().nullish(), - kernel: z.string().nullish(), - mongodb: z.string().nullish(), - mysql: z.string().nullish(), - nginx: z.string().nullish(), - node: z.string().nullish(), - npm: z.string().nullish(), - openssl: z.string().nullish(), - perl: z.string().nullish(), - php: z.string().nullish(), - pm2: z.string().nullish(), - postfix: z.string().nullish(), - postgresql: z.string().nullish(), - python: z.string().nullish(), - redis: z.string().nullish(), - systemOpenssl: z.string().nullish(), - systemOpensslLib: z.string().nullish(), - tsc: z.string().nullish(), - unraid: z.string().nullish(), - v8: z.string().nullish(), - yarn: z.string().nullish() - }) -} - -export function VmDomainSchema(): z.ZodObject> { - return z.object({ - __typename: z.literal('VmDomain').optional(), - name: z.string().nullish(), - state: VmStateSchema, - uuid: z.string() - }) -} - -export function VmMutationsSchema(): z.ZodObject> { - return z.object({ - __typename: z.literal('VmMutations').optional(), - forceStopVm: z.boolean(), - pauseVm: z.boolean(), - rebootVm: z.boolean(), - resetVm: z.boolean(), - resumeVm: z.boolean(), - startVm: z.boolean(), - stopVm: z.boolean() - }) -} - -export function VmMutationsforceStopVmArgsSchema(): z.ZodObject> { - return z.object({ - id: z.string() - }) -} - -export function VmMutationspauseVmArgsSchema(): z.ZodObject> { - return z.object({ - id: z.string() - }) -} - -export function VmMutationsrebootVmArgsSchema(): z.ZodObject> { - return z.object({ - id: z.string() - }) -} - -export function VmMutationsresetVmArgsSchema(): z.ZodObject> { - return z.object({ - id: z.string() - }) -} - -export function VmMutationsresumeVmArgsSchema(): z.ZodObject> { - return z.object({ - id: z.string() - }) -} - -export function VmMutationsstartVmArgsSchema(): z.ZodObject> { - return z.object({ - id: z.string() - }) -} - -export function VmMutationsstopVmArgsSchema(): z.ZodObject> { - return z.object({ - id: z.string() - }) -} - -export function VmsSchema(): z.ZodObject> { - return z.object({ - __typename: z.literal('Vms').optional(), - domain: z.array(VmDomainSchema()).nullish(), - id: z.string() - }) -} - -export function WelcomeSchema(): z.ZodObject> { - return z.object({ - __typename: z.literal('Welcome').optional(), - message: z.string() - }) -} - -export function addUserInputSchema(): z.ZodObject> { - return z.object({ - description: z.string().nullish(), - name: z.string(), - password: z.string() - }) -} - -export function deleteUserInputSchema(): z.ZodObject> { - return z.object({ - name: z.string() - }) -} - -export function usersInputSchema(): z.ZodObject> { - return z.object({ - slim: z.boolean().nullish() - }) -} - -export type getCloudQueryVariables = Types.Exact<{ [key: string]: never; }>; - - -export type getCloudQuery = { __typename?: 'Query', cloud?: { __typename?: 'Cloud', error?: string | null, allowedOrigins: Array, apiKey: { __typename?: 'ApiKeyResponse', valid: boolean, error?: string | null }, minigraphql: { __typename?: 'MinigraphqlResponse', status: Types.MinigraphStatus, timeout?: number | null, error?: string | null }, cloud: { __typename?: 'CloudResponse', status: string, error?: string | null, ip?: string | null } } | null }; - -export type getServersQueryVariables = Types.Exact<{ [key: string]: never; }>; - - -export type getServersQuery = { __typename?: 'Query', servers: Array<{ __typename?: 'Server', name: string, guid: string, status: Types.ServerStatus, owner: { __typename?: 'ProfileModel', username?: string | null } }> }; - - -export const getCloudDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"getCloud"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"cloud"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"error"}},{"kind":"Field","name":{"kind":"Name","value":"apiKey"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"valid"}},{"kind":"Field","name":{"kind":"Name","value":"error"}}]}},{"kind":"Field","name":{"kind":"Name","value":"minigraphql"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"status"}},{"kind":"Field","name":{"kind":"Name","value":"timeout"}},{"kind":"Field","name":{"kind":"Name","value":"error"}}]}},{"kind":"Field","name":{"kind":"Name","value":"cloud"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"status"}},{"kind":"Field","name":{"kind":"Name","value":"error"}},{"kind":"Field","name":{"kind":"Name","value":"ip"}}]}},{"kind":"Field","name":{"kind":"Name","value":"allowedOrigins"}}]}}]}}]} as unknown as DocumentNode; -export const getServersDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"getServers"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"servers"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"guid"}},{"kind":"Field","name":{"kind":"Name","value":"status"}},{"kind":"Field","name":{"kind":"Name","value":"owner"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"username"}}]}}]}}]}}]} as unknown as DocumentNode; \ No newline at end of file diff --git a/api/src/graphql/generated/api/types.ts b/api/src/graphql/generated/api/types.ts deleted file mode 100644 index 46f506800..000000000 --- a/api/src/graphql/generated/api/types.ts +++ /dev/null @@ -1,3508 +0,0 @@ -/* eslint-disable */ -/* @ts-nocheck */ -import { GraphQLResolveInfo, GraphQLScalarType, GraphQLScalarTypeConfig } from 'graphql'; -import { Context } from '@app/graphql/schema/utils.js'; -export type Maybe = T | null; -export type InputMaybe = Maybe; -export type Exact = { [K in keyof T]: T[K] }; -export type MakeOptional = Omit & { [SubKey in K]?: Maybe }; -export type MakeMaybe = Omit & { [SubKey in K]: Maybe }; -export type MakeEmpty = { [_ in K]?: never }; -export type Incremental = T | { [P in keyof T]?: P extends ' $fragmentName' | '__typename' ? T[P] : never }; -export type RequireFields = Omit & { [P in K]-?: NonNullable }; -/** All built-in and custom scalars, mapped to their actual values */ -export type Scalars = { - ID: { input: string; output: string; } - String: { input: string; output: string; } - Boolean: { input: boolean; output: boolean; } - Int: { input: number; output: number; } - Float: { input: number; output: number; } - DateTime: { input: string; output: string; } - JSON: { input: Record; output: Record; } - Long: { input: number; output: number; } - Port: { input: number; output: number; } - URL: { input: URL; output: URL; } - UUID: { input: string; output: string; } -}; - -export type AccessUrl = { - __typename?: 'AccessUrl'; - ipv4?: Maybe; - ipv6?: Maybe; - name?: Maybe; - type: URL_TYPE; -}; - -export type AccessUrlInput = { - ipv4?: InputMaybe; - ipv6?: InputMaybe; - name?: InputMaybe; - type: URL_TYPE; -}; - -export type AddPermissionInput = { - actions: Array; - resource: Resource; -}; - -export type AddRoleForApiKeyInput = { - apiKeyId: Scalars['ID']['input']; - role: Role; -}; - -export type AddRoleForUserInput = { - role: Role; - userId: Scalars['ID']['input']; -}; - -export type AllowedOriginInput = { - origins: Array; -}; - -export type ApiKey = { - __typename?: 'ApiKey'; - createdAt: Scalars['DateTime']['output']; - description?: Maybe; - id: Scalars['ID']['output']; - name: Scalars['String']['output']; - permissions: Array; - roles: Array; -}; - -export type ApiKeyResponse = { - __typename?: 'ApiKeyResponse'; - error?: Maybe; - valid: Scalars['Boolean']['output']; -}; - -export type ApiKeyWithSecret = { - __typename?: 'ApiKeyWithSecret'; - createdAt: Scalars['DateTime']['output']; - description?: Maybe; - id: Scalars['ID']['output']; - key: Scalars['String']['output']; - name: Scalars['String']['output']; - permissions: Array; - roles: Array; -}; - -/** - * Input should be a subset of ApiSettings that can be updated. - * Some field combinations may be required or disallowed. Please refer to each field for more information. - */ -export type ApiSettingsInput = { - /** The type of WAN access to use for Remote Access. */ - accessType?: InputMaybe; - /** A list of origins allowed to interact with the API. */ - extraOrigins?: InputMaybe>; - /** The type of port forwarding to use for Remote Access. */ - forwardType?: InputMaybe; - /** - * The port to use for Remote Access. Not required for UPNP forwardType. Required for STATIC forwardType. - * Ignored if accessType is DISABLED or forwardType is UPNP. - */ - port?: InputMaybe; - /** - * If true, the GraphQL sandbox will be enabled and available at /graphql. - * If false, the GraphQL sandbox will be disabled and only the production API will be available. - */ - sandbox?: InputMaybe; - /** A list of Unique Unraid Account ID's. */ - ssoUserIds?: InputMaybe>; -}; - -export type ArrayType = Node & { - __typename?: 'Array'; - /** Current boot disk */ - boot?: Maybe; - /** Caches in the current array */ - caches: Array; - /** Current array capacity */ - capacity: ArrayCapacity; - /** Data disks in the current array */ - disks: Array; - id: Scalars['ID']['output']; - /** Parity disks in the current array */ - parities: Array; - /** Array state after this query/mutation */ - pendingState?: Maybe; - /** Array state before this query/mutation */ - previousState?: Maybe; - /** Current array state */ - state: ArrayState; -}; - -export type ArrayCapacity = { - __typename?: 'ArrayCapacity'; - disks: Capacity; - kilobytes: Capacity; -}; - -export type ArrayDisk = { - __typename?: 'ArrayDisk'; - color?: Maybe; - /** User comment on disk */ - comment?: Maybe; - /** (%) Disk space left for critical */ - critical?: Maybe; - device?: Maybe; - exportable?: Maybe; - /** File format (ex MBR: 4KiB-aligned) */ - format?: Maybe; - /** (KB) Free Size on the FS (Not present on Parity type drive) */ - fsFree?: Maybe; - /** (KB) Total Size of the FS (Not present on Parity type drive) */ - fsSize?: Maybe; - /** File system type for the disk */ - fsType?: Maybe; - /** (KB) Used Size on the FS (Not present on Parity type drive) */ - fsUsed?: Maybe; - /** Disk indentifier, only set for present disks on the system */ - id: Scalars['ID']['output']; - /** Array slot number. Parity1 is always 0 and Parity2 is always 29. Array slots will be 1 - 28. Cache slots are 30 - 53. Flash is 54. */ - idx: Scalars['Int']['output']; - name?: Maybe; - /** Number of unrecoverable errors reported by the device I/O drivers. Missing data due to unrecoverable array read errors is filled in on-the-fly using parity reconstruct (and we attempt to write this data back to the sector(s) which failed). Any unrecoverable write error results in disabling the disk. */ - numErrors: Scalars['Long']['output']; - /** Count of I/O read requests sent to the device I/O drivers. These statistics may be cleared at any time. */ - numReads: Scalars['Long']['output']; - /** Count of I/O writes requests sent to the device I/O drivers. These statistics may be cleared at any time. */ - numWrites: Scalars['Long']['output']; - /** Is the disk a HDD or SSD. */ - rotational?: Maybe; - /** (KB) Disk Size total */ - size: Scalars['Long']['output']; - status?: Maybe; - /** Disk temp - will be NaN if array is not started or DISK_NP */ - temp?: Maybe; - /** ata | nvme | usb | (others) */ - transport?: Maybe; - /** Type of Disk - used to differentiate Cache / Flash / Array / Parity */ - type: ArrayDiskType; - /** (%) Disk space left to warn */ - warning?: Maybe; -}; - -export enum ArrayDiskFsColor { - /** New device, in standby mode (spun-down) */ - BLUE_BLINK = 'blue_blink', - /** New device */ - BLUE_ON = 'blue_on', - /** Device is in standby mode (spun-down) */ - GREEN_BLINK = 'green_blink', - /** Normal operation, device is active */ - GREEN_ON = 'green_on', - /** Device not present */ - GREY_OFF = 'grey_off', - /** Device is missing (disabled) or contents emulated / Parity device is missing */ - RED_OFF = 'red_off', - /** Device is disabled or contents emulated / Parity device is disabled */ - RED_ON = 'red_on', - /** Device contents invalid or emulated / Parity is invalid, in standby mode (spun-down) */ - YELLOW_BLINK = 'yellow_blink', - /** Device contents invalid or emulated / Parity is invalid */ - YELLOW_ON = 'yellow_on' -} - -export type ArrayDiskInput = { - /** Disk ID */ - id: Scalars['ID']['input']; - /** The slot for the disk */ - slot?: InputMaybe; -}; - -export enum ArrayDiskStatus { - /** disabled, old disk still present */ - DISK_DSBL = 'DISK_DSBL', - /** disabled, new disk present */ - DISK_DSBL_NEW = 'DISK_DSBL_NEW', - /** enabled, disk present, but not valid */ - DISK_INVALID = 'DISK_INVALID', - /** new disk */ - DISK_NEW = 'DISK_NEW', - /** no disk present, no disk configured */ - DISK_NP = 'DISK_NP', - /** disabled, no disk present */ - DISK_NP_DSBL = 'DISK_NP_DSBL', - /** enabled, but missing */ - DISK_NP_MISSING = 'DISK_NP_MISSING', - /** enabled, disk present, correct, valid */ - DISK_OK = 'DISK_OK', - /** enablled, disk present, but not correct disk */ - DISK_WRONG = 'DISK_WRONG' -} - -export enum ArrayDiskType { - /** Cache disk */ - CACHE = 'Cache', - /** Data disk */ - DATA = 'Data', - /** Flash disk */ - FLASH = 'Flash', - /** Parity disk */ - PARITY = 'Parity' -} - -export type ArrayMutations = { - __typename?: 'ArrayMutations'; - /** Add new disk to array */ - addDiskToArray?: Maybe; - clearArrayDiskStatistics?: Maybe; - mountArrayDisk?: Maybe; - /** Remove existing disk from array. NOTE: The array must be stopped before running this otherwise it'll throw an error. */ - removeDiskFromArray?: Maybe; - /** Set array state */ - setState?: Maybe; - unmountArrayDisk?: Maybe; -}; - - -export type ArrayMutationsaddDiskToArrayArgs = { - input?: InputMaybe; -}; - - -export type ArrayMutationsclearArrayDiskStatisticsArgs = { - id: Scalars['ID']['input']; -}; - - -export type ArrayMutationsmountArrayDiskArgs = { - id: Scalars['ID']['input']; -}; - - -export type ArrayMutationsremoveDiskFromArrayArgs = { - input?: InputMaybe; -}; - - -export type ArrayMutationssetStateArgs = { - input?: InputMaybe; -}; - - -export type ArrayMutationsunmountArrayDiskArgs = { - id: Scalars['ID']['input']; -}; - -export enum ArrayPendingState { - /** Array has no data disks */ - NO_DATA_DISKS = 'no_data_disks', - /** Array is starting */ - STARTING = 'starting', - /** Array is stopping */ - STOPPING = 'stopping', - /** Array has too many missing data disks */ - TOO_MANY_MISSING_DISKS = 'too_many_missing_disks' -} - -export enum ArrayState { - /** A disk is disabled in the array */ - DISABLE_DISK = 'DISABLE_DISK', - /** Too many changes to array at the same time */ - INVALID_EXPANSION = 'INVALID_EXPANSION', - /** Array has new disks */ - NEW_ARRAY = 'NEW_ARRAY', - /** Array has new disks they're too small */ - NEW_DISK_TOO_SMALL = 'NEW_DISK_TOO_SMALL', - /** Array has no data disks */ - NO_DATA_DISKS = 'NO_DATA_DISKS', - /** Parity isn't the biggest, can't start array */ - PARITY_NOT_BIGGEST = 'PARITY_NOT_BIGGEST', - /** A disk is being reconstructed */ - RECON_DISK = 'RECON_DISK', - /** Array is running */ - STARTED = 'STARTED', - /** Array has stopped */ - STOPPED = 'STOPPED', - /** Array is disabled */ - SWAP_DSBL = 'SWAP_DSBL', - /** Array has too many missing data disks */ - TOO_MANY_MISSING_DISKS = 'TOO_MANY_MISSING_DISKS' -} - -export type ArrayStateInput = { - /** Array state */ - desiredState: ArrayStateInputState; -}; - -export enum ArrayStateInputState { - /** Start array */ - START = 'START', - /** Stop array */ - STOP = 'STOP' -} - -/** Available authentication action verbs */ -export enum AuthActionVerb { - CREATE = 'CREATE', - DELETE = 'DELETE', - READ = 'READ', - UPDATE = 'UPDATE' -} - -/** Available authentication possession types */ -export enum AuthPossession { - ANY = 'ANY', - OWN = 'OWN', - OWN_ANY = 'OWN_ANY' -} - -export type Baseboard = { - __typename?: 'Baseboard'; - assetTag?: Maybe; - manufacturer: Scalars['String']['output']; - model?: Maybe; - serial?: Maybe; - version?: Maybe; -}; - -export type Capacity = { - __typename?: 'Capacity'; - free: Scalars['String']['output']; - total: Scalars['String']['output']; - used: Scalars['String']['output']; -}; - -export type Case = { - __typename?: 'Case'; - base64?: Maybe; - error?: Maybe; - icon?: Maybe; - url?: Maybe; -}; - -export type Cloud = { - __typename?: 'Cloud'; - allowedOrigins: Array; - apiKey: ApiKeyResponse; - cloud: CloudResponse; - error?: Maybe; - minigraphql: MinigraphqlResponse; - relay?: Maybe; -}; - -export type CloudResponse = { - __typename?: 'CloudResponse'; - error?: Maybe; - ip?: Maybe; - status: Scalars['String']['output']; -}; - -export type Config = Node & { - __typename?: 'Config'; - error?: Maybe; - id: Scalars['ID']['output']; - valid?: Maybe; -}; - -export enum ConfigErrorState { - INELIGIBLE = 'INELIGIBLE', - INVALID = 'INVALID', - NO_KEY_SERVER = 'NO_KEY_SERVER', - UNKNOWN_ERROR = 'UNKNOWN_ERROR', - WITHDRAWN = 'WITHDRAWN' -} - -export type Connect = Node & { - __typename?: 'Connect'; - dynamicRemoteAccess: DynamicRemoteAccessStatus; - id: Scalars['ID']['output']; - settings: ConnectSettings; -}; - -export type ConnectSettings = Node & { - __typename?: 'ConnectSettings'; - dataSchema: Scalars['JSON']['output']; - id: Scalars['ID']['output']; - uiSchema: Scalars['JSON']['output']; - values: ConnectSettingsValues; -}; - -/** Intersection type of ApiSettings and RemoteAccess */ -export type ConnectSettingsValues = { - __typename?: 'ConnectSettingsValues'; - /** The type of WAN access used for Remote Access. */ - accessType: WAN_ACCESS_TYPE; - /** A list of origins allowed to interact with the API. */ - extraOrigins: Array; - /** The type of port forwarding used for Remote Access. */ - forwardType?: Maybe; - /** The port used for Remote Access. */ - port?: Maybe; - /** - * If true, the GraphQL sandbox is enabled and available at /graphql. - * If false, the GraphQL sandbox is disabled and only the production API will be available. - */ - sandbox: Scalars['Boolean']['output']; - /** A list of Unique Unraid Account ID's. */ - ssoUserIds: Array; -}; - -export type ConnectSignInInput = { - accessToken?: InputMaybe; - apiKey: Scalars['String']['input']; - idToken?: InputMaybe; - refreshToken?: InputMaybe; - userInfo?: InputMaybe; -}; - -export type ConnectUserInfoInput = { - avatar?: InputMaybe; - email: Scalars['String']['input']; - preferred_username: Scalars['String']['input']; -}; - -export type ContainerHostConfig = { - __typename?: 'ContainerHostConfig'; - networkMode: Scalars['String']['output']; -}; - -export type ContainerMount = { - __typename?: 'ContainerMount'; - destination: Scalars['String']['output']; - driver: Scalars['String']['output']; - mode: Scalars['String']['output']; - name: Scalars['String']['output']; - propagation: Scalars['String']['output']; - rw: Scalars['Boolean']['output']; - source: Scalars['String']['output']; - type: Scalars['String']['output']; -}; - -export type ContainerPort = { - __typename?: 'ContainerPort'; - ip?: Maybe; - privatePort?: Maybe; - publicPort?: Maybe; - type?: Maybe; -}; - -export enum ContainerPortType { - TCP = 'TCP', - UDP = 'UDP' -} - -export enum ContainerState { - EXITED = 'EXITED', - RUNNING = 'RUNNING' -} - -export type CreateApiKeyInput = { - description?: InputMaybe; - name: Scalars['String']['input']; - /** This will replace the existing key if one already exists with the same name, otherwise returns the existing key */ - overwrite?: InputMaybe; - permissions?: InputMaybe>; - roles?: InputMaybe>; -}; - -export type Devices = { - __typename?: 'Devices'; - gpu?: Maybe>>; - network?: Maybe>>; - pci?: Maybe>>; - usb?: Maybe>>; -}; - -export type Disk = { - __typename?: 'Disk'; - bytesPerSector: Scalars['Long']['output']; - device: Scalars['String']['output']; - firmwareRevision: Scalars['String']['output']; - id: Scalars['ID']['output']; - interfaceType: DiskInterfaceType; - name: Scalars['String']['output']; - partitions?: Maybe>; - sectorsPerTrack: Scalars['Long']['output']; - serialNum: Scalars['String']['output']; - size: Scalars['Long']['output']; - smartStatus: DiskSmartStatus; - temperature?: Maybe; - totalCylinders: Scalars['Long']['output']; - totalHeads: Scalars['Long']['output']; - totalSectors: Scalars['Long']['output']; - totalTracks: Scalars['Long']['output']; - tracksPerCylinder: Scalars['Long']['output']; - type: Scalars['String']['output']; - vendor: Scalars['String']['output']; -}; - -export enum DiskFsType { - BTRFS = 'btrfs', - EXT4 = 'ext4', - NTFS = 'ntfs', - VFAT = 'vfat', - XFS = 'xfs', - ZFS = 'zfs' -} - -export enum DiskInterfaceType { - PCIE = 'PCIe', - SAS = 'SAS', - SATA = 'SATA', - UNKNOWN = 'UNKNOWN', - USB = 'USB' -} - -export type DiskPartition = { - __typename?: 'DiskPartition'; - fsType: DiskFsType; - name: Scalars['String']['output']; - size: Scalars['Long']['output']; -}; - -export enum DiskSmartStatus { - OK = 'OK', - UNKNOWN = 'UNKNOWN' -} - -export type Display = { - __typename?: 'Display'; - banner?: Maybe; - case?: Maybe; - critical?: Maybe; - dashapps?: Maybe; - date?: Maybe; - hot?: Maybe; - id: Scalars['ID']['output']; - locale?: Maybe; - max?: Maybe; - number?: Maybe; - resize?: Maybe; - scale?: Maybe; - tabs?: Maybe; - text?: Maybe; - theme?: Maybe; - total?: Maybe; - unit?: Maybe; - usage?: Maybe; - users?: Maybe; - warning?: Maybe; - wwn?: Maybe; -}; - -export type Docker = Node & { - __typename?: 'Docker'; - containers?: Maybe>; - id: Scalars['ID']['output']; - networks?: Maybe>; -}; - -export type DockerContainer = { - __typename?: 'DockerContainer'; - autoStart: Scalars['Boolean']['output']; - command: Scalars['String']['output']; - created: Scalars['Int']['output']; - hostConfig?: Maybe; - id: Scalars['ID']['output']; - image: Scalars['String']['output']; - imageId: Scalars['String']['output']; - labels?: Maybe; - mounts?: Maybe>>; - names?: Maybe>; - networkSettings?: Maybe; - ports: Array; - /** (B) Total size of all the files in the container */ - sizeRootFs?: Maybe; - state: ContainerState; - status: Scalars['String']['output']; -}; - -export type DockerMutations = { - __typename?: 'DockerMutations'; - /** Start a container */ - start: DockerContainer; - /** Stop a container */ - stop: DockerContainer; -}; - - -export type DockerMutationsstartArgs = { - id: Scalars['ID']['input']; -}; - - -export type DockerMutationsstopArgs = { - id: Scalars['ID']['input']; -}; - -export type DockerNetwork = { - __typename?: 'DockerNetwork'; - attachable: Scalars['Boolean']['output']; - configFrom?: Maybe; - configOnly: Scalars['Boolean']['output']; - containers?: Maybe; - created?: Maybe; - driver?: Maybe; - enableIPv6: Scalars['Boolean']['output']; - id?: Maybe; - ingress: Scalars['Boolean']['output']; - internal: Scalars['Boolean']['output']; - ipam?: Maybe; - labels?: Maybe; - name?: Maybe; - options?: Maybe; - scope?: Maybe; -}; - -export type DynamicRemoteAccessStatus = { - __typename?: 'DynamicRemoteAccessStatus'; - enabledType: DynamicRemoteAccessType; - error?: Maybe; - runningType: DynamicRemoteAccessType; -}; - -export enum DynamicRemoteAccessType { - DISABLED = 'DISABLED', - STATIC = 'STATIC', - UPNP = 'UPNP' -} - -export type EnableDynamicRemoteAccessInput = { - enabled: Scalars['Boolean']['input']; - url: AccessUrlInput; -}; - -export type Flash = { - __typename?: 'Flash'; - guid?: Maybe; - product?: Maybe; - vendor?: Maybe; -}; - -export type Gpu = { - __typename?: 'Gpu'; - blacklisted: Scalars['Boolean']['output']; - class: Scalars['String']['output']; - id: Scalars['ID']['output']; - productid: Scalars['String']['output']; - type: Scalars['String']['output']; - typeid: Scalars['String']['output']; - vendorname: Scalars['String']['output']; -}; - -export enum Importance { - ALERT = 'ALERT', - INFO = 'INFO', - WARNING = 'WARNING' -} - -export type Info = Node & { - __typename?: 'Info'; - /** Count of docker containers */ - apps?: Maybe; - baseboard?: Maybe; - cpu?: Maybe; - devices?: Maybe; - display?: Maybe; - id: Scalars['ID']['output']; - /** Machine ID */ - machineId?: Maybe; - memory?: Maybe; - os?: Maybe; - system?: Maybe; - time: Scalars['DateTime']['output']; - versions?: Maybe; -}; - -export type InfoApps = { - __typename?: 'InfoApps'; - /** How many docker containers are installed */ - installed?: Maybe; - /** How many docker containers are running */ - started?: Maybe; -}; - -export type InfoCpu = { - __typename?: 'InfoCpu'; - brand: Scalars['String']['output']; - cache: Scalars['JSON']['output']; - cores: Scalars['Int']['output']; - family: Scalars['String']['output']; - flags?: Maybe>; - manufacturer: Scalars['String']['output']; - model: Scalars['String']['output']; - processors: Scalars['Long']['output']; - revision: Scalars['String']['output']; - socket: Scalars['String']['output']; - speed: Scalars['Float']['output']; - speedmax: Scalars['Float']['output']; - speedmin: Scalars['Float']['output']; - stepping: Scalars['Int']['output']; - threads: Scalars['Int']['output']; - vendor: Scalars['String']['output']; - voltage?: Maybe; -}; - -export type InfoMemory = { - __typename?: 'InfoMemory'; - active: Scalars['Long']['output']; - available: Scalars['Long']['output']; - buffcache: Scalars['Long']['output']; - free: Scalars['Long']['output']; - layout?: Maybe>; - max: Scalars['Long']['output']; - swapfree: Scalars['Long']['output']; - swaptotal: Scalars['Long']['output']; - swapused: Scalars['Long']['output']; - total: Scalars['Long']['output']; - used: Scalars['Long']['output']; -}; - -export type KeyFile = { - __typename?: 'KeyFile'; - contents?: Maybe; - location?: Maybe; -}; - -/** Represents a log file in the system */ -export type LogFile = { - __typename?: 'LogFile'; - /** Last modified timestamp */ - modifiedAt: Scalars['DateTime']['output']; - /** Name of the log file */ - name: Scalars['String']['output']; - /** Full path to the log file */ - path: Scalars['String']['output']; - /** Size of the log file in bytes */ - size: Scalars['Int']['output']; -}; - -/** Content of a log file */ -export type LogFileContent = { - __typename?: 'LogFileContent'; - /** Content of the log file */ - content: Scalars['String']['output']; - /** Path to the log file */ - path: Scalars['String']['output']; - /** Starting line number of the content (1-indexed) */ - startLine?: Maybe; - /** Total number of lines in the file */ - totalLines: Scalars['Int']['output']; -}; - -/** The current user */ -export type Me = UserAccount & { - __typename?: 'Me'; - description: Scalars['String']['output']; - id: Scalars['ID']['output']; - name: Scalars['String']['output']; - permissions?: Maybe>; - roles: Array; -}; - -export enum MemoryFormFactor { - DIMM = 'DIMM' -} - -export type MemoryLayout = { - __typename?: 'MemoryLayout'; - bank?: Maybe; - clockSpeed?: Maybe; - formFactor?: Maybe; - manufacturer?: Maybe; - partNum?: Maybe; - serialNum?: Maybe; - size: Scalars['Long']['output']; - type?: Maybe; - voltageConfigured?: Maybe; - voltageMax?: Maybe; - voltageMin?: Maybe; -}; - -export enum MemoryType { - DDR2 = 'DDR2', - DDR3 = 'DDR3', - DDR4 = 'DDR4' -} - -export enum MinigraphStatus { - CONNECTED = 'CONNECTED', - CONNECTING = 'CONNECTING', - ERROR_RETRYING = 'ERROR_RETRYING', - PING_FAILURE = 'PING_FAILURE', - PRE_INIT = 'PRE_INIT' -} - -export type MinigraphqlResponse = { - __typename?: 'MinigraphqlResponse'; - error?: Maybe; - status: MinigraphStatus; - timeout?: Maybe; -}; - -export type Mount = { - __typename?: 'Mount'; - directory?: Maybe; - name?: Maybe; - permissions?: Maybe; - type?: Maybe; -}; - -export type Mutation = { - __typename?: 'Mutation'; - addPermission: Scalars['Boolean']['output']; - addRoleForApiKey: Scalars['Boolean']['output']; - addRoleForUser: Scalars['Boolean']['output']; - /** Add a new user */ - addUser?: Maybe; - archiveAll: NotificationOverview; - /** Marks a notification as archived. */ - archiveNotification: Notification; - archiveNotifications: NotificationOverview; - array?: Maybe; - /** Cancel parity check */ - cancelParityCheck?: Maybe; - connectSignIn: Scalars['Boolean']['output']; - connectSignOut: Scalars['Boolean']['output']; - createApiKey: ApiKeyWithSecret; - createNotification: Notification; - /** Deletes all archived notifications on server. */ - deleteArchivedNotifications: NotificationOverview; - deleteNotification: NotificationOverview; - /** Delete a user */ - deleteUser?: Maybe; - docker?: Maybe; - enableDynamicRemoteAccess: Scalars['Boolean']['output']; - login?: Maybe; - /** Pause parity check */ - pauseParityCheck?: Maybe; - reboot?: Maybe; - /** Reads each notification to recompute & update the overview. */ - recalculateOverview: NotificationOverview; - removeRoleFromApiKey: Scalars['Boolean']['output']; - /** Resume parity check */ - resumeParityCheck?: Maybe; - setAdditionalAllowedOrigins: Array; - setupRemoteAccess: Scalars['Boolean']['output']; - shutdown?: Maybe; - /** Start parity check */ - startParityCheck?: Maybe; - unarchiveAll: NotificationOverview; - unarchiveNotifications: NotificationOverview; - /** Marks a notification as unread. */ - unreadNotification: Notification; - /** - * Update the API settings. - * Some setting combinations may be required or disallowed. Please refer to each setting for more information. - */ - updateApiSettings: ConnectSettingsValues; - /** Virtual machine mutations */ - vms?: Maybe; -}; - - -export type MutationaddPermissionArgs = { - input: AddPermissionInput; -}; - - -export type MutationaddRoleForApiKeyArgs = { - input: AddRoleForApiKeyInput; -}; - - -export type MutationaddRoleForUserArgs = { - input: AddRoleForUserInput; -}; - - -export type MutationaddUserArgs = { - input: addUserInput; -}; - - -export type MutationarchiveAllArgs = { - importance?: InputMaybe; -}; - - -export type MutationarchiveNotificationArgs = { - id: Scalars['String']['input']; -}; - - -export type MutationarchiveNotificationsArgs = { - ids?: InputMaybe>; -}; - - -export type MutationconnectSignInArgs = { - input: ConnectSignInInput; -}; - - -export type MutationcreateApiKeyArgs = { - input: CreateApiKeyInput; -}; - - -export type MutationcreateNotificationArgs = { - input: NotificationData; -}; - - -export type MutationdeleteNotificationArgs = { - id: Scalars['String']['input']; - type: NotificationType; -}; - - -export type MutationdeleteUserArgs = { - input: deleteUserInput; -}; - - -export type MutationenableDynamicRemoteAccessArgs = { - input: EnableDynamicRemoteAccessInput; -}; - - -export type MutationloginArgs = { - password: Scalars['String']['input']; - username: Scalars['String']['input']; -}; - - -export type MutationremoveRoleFromApiKeyArgs = { - input: RemoveRoleFromApiKeyInput; -}; - - -export type MutationsetAdditionalAllowedOriginsArgs = { - input: AllowedOriginInput; -}; - - -export type MutationsetupRemoteAccessArgs = { - input: SetupRemoteAccessInput; -}; - - -export type MutationstartParityCheckArgs = { - correct?: InputMaybe; -}; - - -export type MutationunarchiveAllArgs = { - importance?: InputMaybe; -}; - - -export type MutationunarchiveNotificationsArgs = { - ids?: InputMaybe>; -}; - - -export type MutationunreadNotificationArgs = { - id: Scalars['String']['input']; -}; - - -export type MutationupdateApiSettingsArgs = { - input: ApiSettingsInput; -}; - -export type Network = Node & { - __typename?: 'Network'; - accessUrls?: Maybe>; - carrierChanges?: Maybe; - duplex?: Maybe; - id: Scalars['ID']['output']; - iface?: Maybe; - ifaceName?: Maybe; - internal?: Maybe; - ipv4?: Maybe; - ipv6?: Maybe; - mac?: Maybe; - mtu?: Maybe; - operstate?: Maybe; - speed?: Maybe; - type?: Maybe; -}; - -export type Node = { - id: Scalars['ID']['output']; -}; - -export type Notification = Node & { - __typename?: 'Notification'; - description: Scalars['String']['output']; - formattedTimestamp?: Maybe; - id: Scalars['ID']['output']; - importance: Importance; - link?: Maybe; - subject: Scalars['String']['output']; - /** ISO Timestamp for when the notification occurred */ - timestamp?: Maybe; - /** Also known as 'event' */ - title: Scalars['String']['output']; - type: NotificationType; -}; - -export type NotificationCounts = { - __typename?: 'NotificationCounts'; - alert: Scalars['Int']['output']; - info: Scalars['Int']['output']; - total: Scalars['Int']['output']; - warning: Scalars['Int']['output']; -}; - -export type NotificationData = { - description: Scalars['String']['input']; - importance: Importance; - link?: InputMaybe; - subject: Scalars['String']['input']; - title: Scalars['String']['input']; -}; - -export type NotificationFilter = { - importance?: InputMaybe; - limit: Scalars['Int']['input']; - offset: Scalars['Int']['input']; - type?: InputMaybe; -}; - -export type NotificationOverview = { - __typename?: 'NotificationOverview'; - archive: NotificationCounts; - unread: NotificationCounts; -}; - -export enum NotificationType { - ARCHIVE = 'ARCHIVE', - UNREAD = 'UNREAD' -} - -export type Notifications = Node & { - __typename?: 'Notifications'; - id: Scalars['ID']['output']; - list: Array; - /** A cached overview of the notifications in the system & their severity. */ - overview: NotificationOverview; -}; - - -export type NotificationslistArgs = { - filter: NotificationFilter; -}; - -export type Os = { - __typename?: 'Os'; - arch?: Maybe; - build?: Maybe; - codename?: Maybe; - codepage?: Maybe; - distro?: Maybe; - hostname?: Maybe; - kernel?: Maybe; - logofile?: Maybe; - platform?: Maybe; - release?: Maybe; - serial?: Maybe; - uptime?: Maybe; -}; - -export type Owner = { - __typename?: 'Owner'; - avatar?: Maybe; - url?: Maybe; - username?: Maybe; -}; - -export type ParityCheck = { - __typename?: 'ParityCheck'; - date: Scalars['String']['output']; - duration: Scalars['Int']['output']; - errors: Scalars['String']['output']; - speed: Scalars['String']['output']; - status: Scalars['String']['output']; -}; - -export type Partition = { - __typename?: 'Partition'; - devlinks?: Maybe; - devname?: Maybe; - devpath?: Maybe; - devtype?: Maybe; - idAta?: Maybe; - idAtaDownloadMicrocode?: Maybe; - idAtaFeatureSetAam?: Maybe; - idAtaFeatureSetAamCurrentValue?: Maybe; - idAtaFeatureSetAamEnabled?: Maybe; - idAtaFeatureSetAamVendorRecommendedValue?: Maybe; - idAtaFeatureSetApm?: Maybe; - idAtaFeatureSetApmCurrentValue?: Maybe; - idAtaFeatureSetApmEnabled?: Maybe; - idAtaFeatureSetHpa?: Maybe; - idAtaFeatureSetHpaEnabled?: Maybe; - idAtaFeatureSetPm?: Maybe; - idAtaFeatureSetPmEnabled?: Maybe; - idAtaFeatureSetPuis?: Maybe; - idAtaFeatureSetPuisEnabled?: Maybe; - idAtaFeatureSetSecurity?: Maybe; - idAtaFeatureSetSecurityEnabled?: Maybe; - idAtaFeatureSetSecurityEnhancedEraseUnitMin?: Maybe; - idAtaFeatureSetSecurityEraseUnitMin?: Maybe; - idAtaFeatureSetSmart?: Maybe; - idAtaFeatureSetSmartEnabled?: Maybe; - idAtaRotationRateRpm?: Maybe; - idAtaSata?: Maybe; - idAtaSataSignalRateGen1?: Maybe; - idAtaSataSignalRateGen2?: Maybe; - idAtaWriteCache?: Maybe; - idAtaWriteCacheEnabled?: Maybe; - idBus?: Maybe; - idFsType?: Maybe; - idFsUsage?: Maybe; - idFsUuid?: Maybe; - idFsUuidEnc?: Maybe; - idModel?: Maybe; - idModelEnc?: Maybe; - idPartEntryDisk?: Maybe; - idPartEntryNumber?: Maybe; - idPartEntryOffset?: Maybe; - idPartEntryScheme?: Maybe; - idPartEntrySize?: Maybe; - idPartEntryType?: Maybe; - idPartTableType?: Maybe; - idPath?: Maybe; - idPathTag?: Maybe; - idRevision?: Maybe; - idSerial?: Maybe; - idSerialShort?: Maybe; - idType?: Maybe; - idWwn?: Maybe; - idWwnWithExtension?: Maybe; - major?: Maybe; - minor?: Maybe; - partn?: Maybe; - subsystem?: Maybe; - usecInitialized?: Maybe; -}; - -export type Pci = { - __typename?: 'Pci'; - blacklisted?: Maybe; - class?: Maybe; - id: Scalars['ID']['output']; - productid?: Maybe; - productname?: Maybe; - type?: Maybe; - typeid?: Maybe; - vendorid?: Maybe; - vendorname?: Maybe; -}; - -export type Permission = { - __typename?: 'Permission'; - actions: Array; - resource: Resource; -}; - -export type ProfileModel = { - __typename?: 'ProfileModel'; - avatar?: Maybe; - url?: Maybe; - userId?: Maybe; - username?: Maybe; -}; - -export type Query = { - __typename?: 'Query'; - apiKey?: Maybe; - apiKeys: Array; - /** An Unraid array consisting of 1 or 2 Parity disks and a number of Data disks. */ - array: ArrayType; - cloud?: Maybe; - config: Config; - connect: Connect; - /** Single disk */ - disk?: Maybe; - /** Mulitiple disks */ - disks: Array>; - display?: Maybe; - docker: Docker; - /** Docker network */ - dockerNetwork: DockerNetwork; - /** All Docker networks */ - dockerNetworks: Array>; - extraAllowedOrigins: Array; - flash?: Maybe; - info?: Maybe; - /** - * Get the content of a specific log file - * @param path Path to the log file - * @param lines Number of lines to read from the end of the file (default: 100) - * @param startLine Optional starting line number (1-indexed) - */ - logFile: LogFileContent; - /** List all available log files */ - logFiles: Array; - /** Current user account */ - me?: Maybe; - network?: Maybe; - notifications: Notifications; - online?: Maybe; - owner?: Maybe; - parityHistory?: Maybe>>; - registration?: Maybe; - remoteAccess: RemoteAccess; - server?: Maybe; - servers: Array; - services: Array; - /** Network Shares */ - shares?: Maybe>>; - unassignedDevices?: Maybe>>; - /** User account */ - user?: Maybe; - /** User accounts */ - users: Array; - vars?: Maybe; - /** Virtual machines */ - vms?: Maybe; -}; - - -export type QueryapiKeyArgs = { - id: Scalars['ID']['input']; -}; - - -export type QuerydiskArgs = { - id: Scalars['ID']['input']; -}; - - -export type QuerydockerNetworkArgs = { - id: Scalars['ID']['input']; -}; - - -export type QuerydockerNetworksArgs = { - all?: InputMaybe; -}; - - -export type QuerylogFileArgs = { - lines?: InputMaybe; - path: Scalars['String']['input']; - startLine?: InputMaybe; -}; - - -export type QueryuserArgs = { - id: Scalars['ID']['input']; -}; - - -export type QueryusersArgs = { - input?: InputMaybe; -}; - -export type Registration = { - __typename?: 'Registration'; - expiration?: Maybe; - guid?: Maybe; - keyFile?: Maybe; - state?: Maybe; - type?: Maybe; - updateExpiration?: Maybe; -}; - -export enum RegistrationState { - BASIC = 'BASIC', - /** BLACKLISTED */ - EBLACKLISTED = 'EBLACKLISTED', - /** BLACKLISTED */ - EBLACKLISTED1 = 'EBLACKLISTED1', - /** BLACKLISTED */ - EBLACKLISTED2 = 'EBLACKLISTED2', - /** Trial Expired */ - EEXPIRED = 'EEXPIRED', - /** GUID Error */ - EGUID = 'EGUID', - /** Multiple License Keys Present */ - EGUID1 = 'EGUID1', - /** Trial Requires Internet Connection */ - ENOCONN = 'ENOCONN', - /** No Flash */ - ENOFLASH = 'ENOFLASH', - ENOFLASH1 = 'ENOFLASH1', - ENOFLASH2 = 'ENOFLASH2', - ENOFLASH3 = 'ENOFLASH3', - ENOFLASH4 = 'ENOFLASH4', - ENOFLASH5 = 'ENOFLASH5', - ENOFLASH6 = 'ENOFLASH6', - ENOFLASH7 = 'ENOFLASH7', - /** No Keyfile */ - ENOKEYFILE = 'ENOKEYFILE', - /** No Keyfile */ - ENOKEYFILE1 = 'ENOKEYFILE1', - /** Missing key file */ - ENOKEYFILE2 = 'ENOKEYFILE2', - /** Invalid installation */ - ETRIAL = 'ETRIAL', - LIFETIME = 'LIFETIME', - PLUS = 'PLUS', - PRO = 'PRO', - STARTER = 'STARTER', - TRIAL = 'TRIAL', - UNLEASHED = 'UNLEASHED' -} - -export type RelayResponse = { - __typename?: 'RelayResponse'; - error?: Maybe; - status: Scalars['String']['output']; - timeout?: Maybe; -}; - -export type RemoteAccess = { - __typename?: 'RemoteAccess'; - accessType: WAN_ACCESS_TYPE; - forwardType?: Maybe; - port?: Maybe; -}; - -export type RemoveRoleFromApiKeyInput = { - apiKeyId: Scalars['ID']['input']; - role: Role; -}; - -/** Available resources for permissions */ -export enum Resource { - API_KEY = 'API_KEY', - ARRAY = 'ARRAY', - CLOUD = 'CLOUD', - CONFIG = 'CONFIG', - CONNECT = 'CONNECT', - CONNECT__REMOTE_ACCESS = 'CONNECT__REMOTE_ACCESS', - CUSTOMIZATIONS = 'CUSTOMIZATIONS', - DASHBOARD = 'DASHBOARD', - DISK = 'DISK', - DISPLAY = 'DISPLAY', - DOCKER = 'DOCKER', - FLASH = 'FLASH', - INFO = 'INFO', - LOGS = 'LOGS', - ME = 'ME', - NETWORK = 'NETWORK', - NOTIFICATIONS = 'NOTIFICATIONS', - ONLINE = 'ONLINE', - OS = 'OS', - OWNER = 'OWNER', - PERMISSION = 'PERMISSION', - REGISTRATION = 'REGISTRATION', - SERVERS = 'SERVERS', - SERVICES = 'SERVICES', - SHARE = 'SHARE', - VARS = 'VARS', - VMS = 'VMS', - WELCOME = 'WELCOME' -} - -/** Available roles for API keys and users */ -export enum Role { - ADMIN = 'ADMIN', - CONNECT = 'CONNECT', - GUEST = 'GUEST' -} - -export type Server = { - __typename?: 'Server'; - apikey: Scalars['String']['output']; - guid: Scalars['String']['output']; - lanip: Scalars['String']['output']; - localurl: Scalars['String']['output']; - name: Scalars['String']['output']; - owner: ProfileModel; - remoteurl: Scalars['String']['output']; - status: ServerStatus; - wanip: Scalars['String']['output']; -}; - -export enum ServerStatus { - NEVER_CONNECTED = 'never_connected', - OFFLINE = 'offline', - ONLINE = 'online' -} - -export type Service = Node & { - __typename?: 'Service'; - id: Scalars['ID']['output']; - name?: Maybe; - online?: Maybe; - uptime?: Maybe; - version?: Maybe; -}; - -export type SetupRemoteAccessInput = { - accessType: WAN_ACCESS_TYPE; - forwardType?: InputMaybe; - port?: InputMaybe; -}; - -/** Network Share */ -export type Share = { - __typename?: 'Share'; - allocator?: Maybe; - cache?: Maybe; - color?: Maybe; - /** User comment */ - comment?: Maybe; - cow?: Maybe; - /** Disks that're excluded from this share */ - exclude?: Maybe>>; - floor?: Maybe; - /** (KB) Free space */ - free?: Maybe; - /** Disks that're included in this share */ - include?: Maybe>>; - luksStatus?: Maybe; - /** Display name */ - name?: Maybe; - nameOrig?: Maybe; - /** (KB) Total size */ - size?: Maybe; - splitLevel?: Maybe; - /** (KB) Used Size */ - used?: Maybe; -}; - -export type Subscription = { - __typename?: 'Subscription'; - array: ArrayType; - config: Config; - display?: Maybe; - dockerNetwork: DockerNetwork; - dockerNetworks: Array>; - flash: Flash; - info: Info; - /** - * Subscribe to changes in a log file - * @param path Path to the log file - */ - logFile: LogFileContent; - me?: Maybe; - notificationAdded: Notification; - notificationsOverview: NotificationOverview; - online: Scalars['Boolean']['output']; - owner: Owner; - parityHistory: ParityCheck; - ping: Scalars['String']['output']; - registration: Registration; - server?: Maybe; - service?: Maybe>; - share: Share; - shares?: Maybe>; - unassignedDevices?: Maybe>; - user: User; - users: Array>; - vars: Vars; - vms?: Maybe; -}; - - -export type SubscriptiondockerNetworkArgs = { - id: Scalars['ID']['input']; -}; - - -export type SubscriptionlogFileArgs = { - path: Scalars['String']['input']; -}; - - -export type SubscriptionserviceArgs = { - name: Scalars['String']['input']; -}; - - -export type SubscriptionshareArgs = { - id: Scalars['ID']['input']; -}; - - -export type SubscriptionuserArgs = { - id: Scalars['ID']['input']; -}; - -export type System = { - __typename?: 'System'; - manufacturer?: Maybe; - model?: Maybe; - serial?: Maybe; - sku?: Maybe; - uuid?: Maybe; - version?: Maybe; -}; - -export enum Temperature { - C = 'C', - F = 'F' -} - -export enum Theme { - WHITE = 'white' -} - -export enum URL_TYPE { - DEFAULT = 'DEFAULT', - LAN = 'LAN', - MDNS = 'MDNS', - OTHER = 'OTHER', - WAN = 'WAN', - WIREGUARD = 'WIREGUARD' -} - -export type UnassignedDevice = { - __typename?: 'UnassignedDevice'; - devlinks?: Maybe; - devname?: Maybe; - devpath?: Maybe; - devtype?: Maybe; - idAta?: Maybe; - idAtaDownloadMicrocode?: Maybe; - idAtaFeatureSetAam?: Maybe; - idAtaFeatureSetAamCurrentValue?: Maybe; - idAtaFeatureSetAamEnabled?: Maybe; - idAtaFeatureSetAamVendorRecommendedValue?: Maybe; - idAtaFeatureSetApm?: Maybe; - idAtaFeatureSetApmCurrentValue?: Maybe; - idAtaFeatureSetApmEnabled?: Maybe; - idAtaFeatureSetHpa?: Maybe; - idAtaFeatureSetHpaEnabled?: Maybe; - idAtaFeatureSetPm?: Maybe; - idAtaFeatureSetPmEnabled?: Maybe; - idAtaFeatureSetPuis?: Maybe; - idAtaFeatureSetPuisEnabled?: Maybe; - idAtaFeatureSetSecurity?: Maybe; - idAtaFeatureSetSecurityEnabled?: Maybe; - idAtaFeatureSetSecurityEnhancedEraseUnitMin?: Maybe; - idAtaFeatureSetSecurityEraseUnitMin?: Maybe; - idAtaFeatureSetSmart?: Maybe; - idAtaFeatureSetSmartEnabled?: Maybe; - idAtaRotationRateRpm?: Maybe; - idAtaSata?: Maybe; - idAtaSataSignalRateGen1?: Maybe; - idAtaSataSignalRateGen2?: Maybe; - idAtaWriteCache?: Maybe; - idAtaWriteCacheEnabled?: Maybe; - idBus?: Maybe; - idModel?: Maybe; - idModelEnc?: Maybe; - idPartTableType?: Maybe; - idPath?: Maybe; - idPathTag?: Maybe; - idRevision?: Maybe; - idSerial?: Maybe; - idSerialShort?: Maybe; - idType?: Maybe; - idWwn?: Maybe; - idWwnWithExtension?: Maybe; - major?: Maybe; - minor?: Maybe; - mount?: Maybe; - mounted?: Maybe; - name?: Maybe; - partitions?: Maybe>>; - subsystem?: Maybe; - temp?: Maybe; - usecInitialized?: Maybe; -}; - -export type Uptime = { - __typename?: 'Uptime'; - timestamp?: Maybe; -}; - -export type Usb = { - __typename?: 'Usb'; - id: Scalars['ID']['output']; - name?: Maybe; -}; - -/** A local user account */ -export type User = UserAccount & { - __typename?: 'User'; - description: Scalars['String']['output']; - id: Scalars['ID']['output']; - /** A unique name for the user */ - name: Scalars['String']['output']; - /** If the account has a password set */ - password?: Maybe; - permissions?: Maybe>; - roles: Array; -}; - -export type UserAccount = { - description: Scalars['String']['output']; - id: Scalars['ID']['output']; - name: Scalars['String']['output']; - permissions?: Maybe>; - roles: Array; -}; - -export type Vars = Node & { - __typename?: 'Vars'; - bindMgt?: Maybe; - cacheNumDevices?: Maybe; - cacheSbNumDisks?: Maybe; - comment?: Maybe; - configError?: Maybe; - configValid?: Maybe; - csrfToken?: Maybe; - defaultFormat?: Maybe; - defaultFsType?: Maybe; - deviceCount?: Maybe; - domain?: Maybe; - domainLogin?: Maybe; - domainShort?: Maybe; - enableFruit?: Maybe; - flashGuid?: Maybe; - flashProduct?: Maybe; - flashVendor?: Maybe; - /** Percentage from 0 - 100 while upgrading a disk or swapping parity drives */ - fsCopyPrcnt?: Maybe; - fsNumMounted?: Maybe; - fsNumUnmountable?: Maybe; - /** Human friendly string of array events happening */ - fsProgress?: Maybe; - fsState?: Maybe; - fsUnmountableMask?: Maybe; - fuseDirectio?: Maybe; - fuseDirectioDefault?: Maybe; - fuseDirectioStatus?: Maybe; - fuseRemember?: Maybe; - fuseRememberDefault?: Maybe; - fuseRememberStatus?: Maybe; - hideDotFiles?: Maybe; - id: Scalars['ID']['output']; - joinStatus?: Maybe; - localMaster?: Maybe; - localTld?: Maybe; - luksKeyfile?: Maybe; - maxArraysz?: Maybe; - maxCachesz?: Maybe; - mdColor?: Maybe; - mdNumDisabled?: Maybe; - mdNumDisks?: Maybe; - mdNumErased?: Maybe; - mdNumInvalid?: Maybe; - mdNumMissing?: Maybe; - mdNumNew?: Maybe; - mdNumStripes?: Maybe; - mdNumStripesDefault?: Maybe; - mdNumStripesStatus?: Maybe; - mdResync?: Maybe; - mdResyncAction?: Maybe; - mdResyncCorr?: Maybe; - mdResyncDb?: Maybe; - mdResyncDt?: Maybe; - mdResyncPos?: Maybe; - mdResyncSize?: Maybe; - mdState?: Maybe; - mdSyncThresh?: Maybe; - mdSyncThreshDefault?: Maybe; - mdSyncThreshStatus?: Maybe; - mdSyncWindow?: Maybe; - mdSyncWindowDefault?: Maybe; - mdSyncWindowStatus?: Maybe; - mdVersion?: Maybe; - mdWriteMethod?: Maybe; - mdWriteMethodDefault?: Maybe; - mdWriteMethodStatus?: Maybe; - /** Machine hostname */ - name?: Maybe; - nrRequests?: Maybe; - nrRequestsDefault?: Maybe; - nrRequestsStatus?: Maybe; - /** NTP Server 1 */ - ntpServer1?: Maybe; - /** NTP Server 2 */ - ntpServer2?: Maybe; - /** NTP Server 3 */ - ntpServer3?: Maybe; - /** NTP Server 4 */ - ntpServer4?: Maybe; - pollAttributes?: Maybe; - pollAttributesDefault?: Maybe; - pollAttributesStatus?: Maybe; - /** Port for the webui via HTTP */ - port?: Maybe; - portssh?: Maybe; - /** Port for the webui via HTTPS */ - portssl?: Maybe; - porttelnet?: Maybe; - queueDepth?: Maybe; - regCheck?: Maybe; - regFile?: Maybe; - regGen?: Maybe; - regGuid?: Maybe; - regState?: Maybe; - regTm?: Maybe; - regTm2?: Maybe; - /** Registration owner */ - regTo?: Maybe; - regTy?: Maybe; - safeMode?: Maybe; - sbClean?: Maybe; - sbEvents?: Maybe; - sbName?: Maybe; - sbNumDisks?: Maybe; - sbState?: Maybe; - sbSyncErrs?: Maybe; - sbSyncExit?: Maybe; - sbSynced?: Maybe; - sbSynced2?: Maybe; - sbUpdated?: Maybe; - sbVersion?: Maybe; - security?: Maybe; - /** Total amount shares with AFP enabled */ - shareAfpCount?: Maybe; - shareAfpEnabled?: Maybe; - shareAvahiAfpModel?: Maybe; - shareAvahiAfpName?: Maybe; - shareAvahiEnabled?: Maybe; - shareAvahiSmbModel?: Maybe; - shareAvahiSmbName?: Maybe; - shareCacheEnabled?: Maybe; - shareCacheFloor?: Maybe; - /** Total amount of user shares */ - shareCount?: Maybe; - shareDisk?: Maybe; - shareInitialGroup?: Maybe; - shareInitialOwner?: Maybe; - shareMoverActive?: Maybe; - shareMoverLogging?: Maybe; - shareMoverSchedule?: Maybe; - /** Total amount shares with NFS enabled */ - shareNfsCount?: Maybe; - shareNfsEnabled?: Maybe; - /** Total amount shares with SMB enabled */ - shareSmbCount?: Maybe; - shareSmbEnabled?: Maybe; - shareUser?: Maybe; - shareUserExclude?: Maybe; - shareUserInclude?: Maybe; - shutdownTimeout?: Maybe; - spindownDelay?: Maybe; - spinupGroups?: Maybe; - startArray?: Maybe; - startMode?: Maybe; - startPage?: Maybe; - sysArraySlots?: Maybe; - sysCacheSlots?: Maybe; - sysFlashSlots?: Maybe; - sysModel?: Maybe; - timeZone?: Maybe; - /** Should a NTP server be used for time sync? */ - useNtp?: Maybe; - useSsh?: Maybe; - useSsl?: Maybe; - /** Should telnet be enabled? */ - useTelnet?: Maybe; - /** Unraid version */ - version?: Maybe; - workgroup?: Maybe; -}; - -export type Versions = { - __typename?: 'Versions'; - apache?: Maybe; - docker?: Maybe; - gcc?: Maybe; - git?: Maybe; - grunt?: Maybe; - gulp?: Maybe; - kernel?: Maybe; - mongodb?: Maybe; - mysql?: Maybe; - nginx?: Maybe; - node?: Maybe; - npm?: Maybe; - openssl?: Maybe; - perl?: Maybe; - php?: Maybe; - pm2?: Maybe; - postfix?: Maybe; - postgresql?: Maybe; - python?: Maybe; - redis?: Maybe; - systemOpenssl?: Maybe; - systemOpensslLib?: Maybe; - tsc?: Maybe; - unraid?: Maybe; - v8?: Maybe; - yarn?: Maybe; -}; - -/** A virtual machine */ -export type VmDomain = { - __typename?: 'VmDomain'; - /** A friendly name for the vm */ - name?: Maybe; - /** Current domain vm state */ - state: VmState; - uuid: Scalars['ID']['output']; -}; - -export type VmMutations = { - __typename?: 'VmMutations'; - /** Force stop a virtual machine */ - forceStopVm: Scalars['Boolean']['output']; - /** Pause a virtual machine */ - pauseVm: Scalars['Boolean']['output']; - /** Reboot a virtual machine */ - rebootVm: Scalars['Boolean']['output']; - /** Reset a virtual machine */ - resetVm: Scalars['Boolean']['output']; - /** Resume a virtual machine */ - resumeVm: Scalars['Boolean']['output']; - /** Start a virtual machine */ - startVm: Scalars['Boolean']['output']; - /** Stop a virtual machine */ - stopVm: Scalars['Boolean']['output']; -}; - - -export type VmMutationsforceStopVmArgs = { - id: Scalars['ID']['input']; -}; - - -export type VmMutationspauseVmArgs = { - id: Scalars['ID']['input']; -}; - - -export type VmMutationsrebootVmArgs = { - id: Scalars['ID']['input']; -}; - - -export type VmMutationsresetVmArgs = { - id: Scalars['ID']['input']; -}; - - -export type VmMutationsresumeVmArgs = { - id: Scalars['ID']['input']; -}; - - -export type VmMutationsstartVmArgs = { - id: Scalars['ID']['input']; -}; - - -export type VmMutationsstopVmArgs = { - id: Scalars['ID']['input']; -}; - -export enum VmState { - CRASHED = 'CRASHED', - IDLE = 'IDLE', - NOSTATE = 'NOSTATE', - PAUSED = 'PAUSED', - PMSUSPENDED = 'PMSUSPENDED', - RUNNING = 'RUNNING', - SHUTDOWN = 'SHUTDOWN', - SHUTOFF = 'SHUTOFF' -} - -export type Vms = { - __typename?: 'Vms'; - domain?: Maybe>; - id: Scalars['ID']['output']; -}; - -export enum WAN_ACCESS_TYPE { - ALWAYS = 'ALWAYS', - DISABLED = 'DISABLED', - DYNAMIC = 'DYNAMIC' -} - -export enum WAN_FORWARD_TYPE { - STATIC = 'STATIC', - UPNP = 'UPNP' -} - -export type Welcome = { - __typename?: 'Welcome'; - message: Scalars['String']['output']; -}; - -export type addUserInput = { - description?: InputMaybe; - name: Scalars['String']['input']; - password: Scalars['String']['input']; -}; - -export type deleteUserInput = { - name: Scalars['String']['input']; -}; - -export enum mdState { - STARTED = 'STARTED', - SWAP_DSBL = 'SWAP_DSBL' -} - -export enum registrationType { - BASIC = 'BASIC', - INVALID = 'INVALID', - LIFETIME = 'LIFETIME', - PLUS = 'PLUS', - PRO = 'PRO', - STARTER = 'STARTER', - TRIAL = 'TRIAL', - UNLEASHED = 'UNLEASHED' -} - -export type usersInput = { - slim?: InputMaybe; -}; - -export type WithIndex = TObject & Record; -export type ResolversObject = WithIndex; - -export type ResolverTypeWrapper = Promise | T; - - -export type ResolverWithResolve = { - resolve: ResolverFn; -}; -export type Resolver = ResolverFn | ResolverWithResolve; - -export type ResolverFn = ( - parent: TParent, - args: TArgs, - context: TContext, - info: GraphQLResolveInfo -) => Promise | TResult; - -export type SubscriptionSubscribeFn = ( - parent: TParent, - args: TArgs, - context: TContext, - info: GraphQLResolveInfo -) => AsyncIterable | Promise>; - -export type SubscriptionResolveFn = ( - parent: TParent, - args: TArgs, - context: TContext, - info: GraphQLResolveInfo -) => TResult | Promise; - -export interface SubscriptionSubscriberObject { - subscribe: SubscriptionSubscribeFn<{ [key in TKey]: TResult }, TParent, TContext, TArgs>; - resolve?: SubscriptionResolveFn; -} - -export interface SubscriptionResolverObject { - subscribe: SubscriptionSubscribeFn; - resolve: SubscriptionResolveFn; -} - -export type SubscriptionObject = - | SubscriptionSubscriberObject - | SubscriptionResolverObject; - -export type SubscriptionResolver = - | ((...args: any[]) => SubscriptionObject) - | SubscriptionObject; - -export type TypeResolveFn = ( - parent: TParent, - context: TContext, - info: GraphQLResolveInfo -) => Maybe | Promise>; - -export type IsTypeOfResolverFn = (obj: T, context: TContext, info: GraphQLResolveInfo) => boolean | Promise; - -export type NextResolverFn = () => Promise; - -export type DirectiveResolverFn = ( - next: NextResolverFn, - parent: TParent, - args: TArgs, - context: TContext, - info: GraphQLResolveInfo -) => TResult | Promise; - - -/** Mapping of interface types */ -export type ResolversInterfaceTypes<_RefType extends Record> = ResolversObject<{ - Node: ( ArrayType ) | ( Config ) | ( Connect ) | ( ConnectSettings ) | ( Docker ) | ( Info ) | ( Network ) | ( Notification ) | ( Notifications ) | ( Service ) | ( Vars ); - UserAccount: ( Me ) | ( User ); -}>; - -/** Mapping between all available schema types and the resolvers types */ -export type ResolversTypes = ResolversObject<{ - AccessUrl: ResolverTypeWrapper; - AccessUrlInput: AccessUrlInput; - AddPermissionInput: AddPermissionInput; - AddRoleForApiKeyInput: AddRoleForApiKeyInput; - AddRoleForUserInput: AddRoleForUserInput; - AllowedOriginInput: AllowedOriginInput; - ApiKey: ResolverTypeWrapper; - ApiKeyResponse: ResolverTypeWrapper; - ApiKeyWithSecret: ResolverTypeWrapper; - ApiSettingsInput: ApiSettingsInput; - Array: ResolverTypeWrapper; - ArrayCapacity: ResolverTypeWrapper; - ArrayDisk: ResolverTypeWrapper; - ArrayDiskFsColor: ArrayDiskFsColor; - ArrayDiskInput: ArrayDiskInput; - ArrayDiskStatus: ArrayDiskStatus; - ArrayDiskType: ArrayDiskType; - ArrayMutations: ResolverTypeWrapper; - ArrayPendingState: ArrayPendingState; - ArrayState: ArrayState; - ArrayStateInput: ArrayStateInput; - ArrayStateInputState: ArrayStateInputState; - AuthActionVerb: AuthActionVerb; - AuthPossession: AuthPossession; - Baseboard: ResolverTypeWrapper; - Boolean: ResolverTypeWrapper; - Capacity: ResolverTypeWrapper; - Case: ResolverTypeWrapper; - Cloud: ResolverTypeWrapper; - CloudResponse: ResolverTypeWrapper; - Config: ResolverTypeWrapper; - ConfigErrorState: ConfigErrorState; - Connect: ResolverTypeWrapper; - ConnectSettings: ResolverTypeWrapper; - ConnectSettingsValues: ResolverTypeWrapper; - ConnectSignInInput: ConnectSignInInput; - ConnectUserInfoInput: ConnectUserInfoInput; - ContainerHostConfig: ResolverTypeWrapper; - ContainerMount: ResolverTypeWrapper; - ContainerPort: ResolverTypeWrapper; - ContainerPortType: ContainerPortType; - ContainerState: ContainerState; - CreateApiKeyInput: CreateApiKeyInput; - DateTime: ResolverTypeWrapper; - Devices: ResolverTypeWrapper; - Disk: ResolverTypeWrapper; - DiskFsType: DiskFsType; - DiskInterfaceType: DiskInterfaceType; - DiskPartition: ResolverTypeWrapper; - DiskSmartStatus: DiskSmartStatus; - Display: ResolverTypeWrapper; - Docker: ResolverTypeWrapper; - DockerContainer: ResolverTypeWrapper; - DockerMutations: ResolverTypeWrapper; - DockerNetwork: ResolverTypeWrapper; - DynamicRemoteAccessStatus: ResolverTypeWrapper; - DynamicRemoteAccessType: DynamicRemoteAccessType; - EnableDynamicRemoteAccessInput: EnableDynamicRemoteAccessInput; - Flash: ResolverTypeWrapper; - Float: ResolverTypeWrapper; - Gpu: ResolverTypeWrapper; - ID: ResolverTypeWrapper; - Importance: Importance; - Info: ResolverTypeWrapper; - InfoApps: ResolverTypeWrapper; - InfoCpu: ResolverTypeWrapper; - InfoMemory: ResolverTypeWrapper; - Int: ResolverTypeWrapper; - JSON: ResolverTypeWrapper; - KeyFile: ResolverTypeWrapper; - LogFile: ResolverTypeWrapper; - LogFileContent: ResolverTypeWrapper; - Long: ResolverTypeWrapper; - Me: ResolverTypeWrapper; - MemoryFormFactor: MemoryFormFactor; - MemoryLayout: ResolverTypeWrapper; - MemoryType: MemoryType; - MinigraphStatus: MinigraphStatus; - MinigraphqlResponse: ResolverTypeWrapper; - Mount: ResolverTypeWrapper; - Mutation: ResolverTypeWrapper<{}>; - Network: ResolverTypeWrapper; - Node: ResolverTypeWrapper['Node']>; - Notification: ResolverTypeWrapper; - NotificationCounts: ResolverTypeWrapper; - NotificationData: NotificationData; - NotificationFilter: NotificationFilter; - NotificationOverview: ResolverTypeWrapper; - NotificationType: NotificationType; - Notifications: ResolverTypeWrapper; - Os: ResolverTypeWrapper; - Owner: ResolverTypeWrapper; - ParityCheck: ResolverTypeWrapper; - Partition: ResolverTypeWrapper; - Pci: ResolverTypeWrapper; - Permission: ResolverTypeWrapper; - Port: ResolverTypeWrapper; - ProfileModel: ResolverTypeWrapper; - Query: ResolverTypeWrapper<{}>; - Registration: ResolverTypeWrapper; - RegistrationState: RegistrationState; - RelayResponse: ResolverTypeWrapper; - RemoteAccess: ResolverTypeWrapper; - RemoveRoleFromApiKeyInput: RemoveRoleFromApiKeyInput; - Resource: Resource; - Role: Role; - Server: ResolverTypeWrapper; - ServerStatus: ServerStatus; - Service: ResolverTypeWrapper; - SetupRemoteAccessInput: SetupRemoteAccessInput; - Share: ResolverTypeWrapper; - String: ResolverTypeWrapper; - Subscription: ResolverTypeWrapper<{}>; - System: ResolverTypeWrapper; - Temperature: Temperature; - Theme: Theme; - URL: ResolverTypeWrapper; - URL_TYPE: URL_TYPE; - UUID: ResolverTypeWrapper; - UnassignedDevice: ResolverTypeWrapper; - Uptime: ResolverTypeWrapper; - Usb: ResolverTypeWrapper; - User: ResolverTypeWrapper; - UserAccount: ResolverTypeWrapper['UserAccount']>; - Vars: ResolverTypeWrapper; - Versions: ResolverTypeWrapper; - VmDomain: ResolverTypeWrapper; - VmMutations: ResolverTypeWrapper; - VmState: VmState; - Vms: ResolverTypeWrapper; - WAN_ACCESS_TYPE: WAN_ACCESS_TYPE; - WAN_FORWARD_TYPE: WAN_FORWARD_TYPE; - Welcome: ResolverTypeWrapper; - addUserInput: addUserInput; - deleteUserInput: deleteUserInput; - mdState: mdState; - registrationType: registrationType; - usersInput: usersInput; -}>; - -/** Mapping between all available schema types and the resolvers parents */ -export type ResolversParentTypes = ResolversObject<{ - AccessUrl: AccessUrl; - AccessUrlInput: AccessUrlInput; - AddPermissionInput: AddPermissionInput; - AddRoleForApiKeyInput: AddRoleForApiKeyInput; - AddRoleForUserInput: AddRoleForUserInput; - AllowedOriginInput: AllowedOriginInput; - ApiKey: ApiKey; - ApiKeyResponse: ApiKeyResponse; - ApiKeyWithSecret: ApiKeyWithSecret; - ApiSettingsInput: ApiSettingsInput; - Array: ArrayType; - ArrayCapacity: ArrayCapacity; - ArrayDisk: ArrayDisk; - ArrayDiskInput: ArrayDiskInput; - ArrayMutations: ArrayMutations; - ArrayStateInput: ArrayStateInput; - Baseboard: Baseboard; - Boolean: Scalars['Boolean']['output']; - Capacity: Capacity; - Case: Case; - Cloud: Cloud; - CloudResponse: CloudResponse; - Config: Config; - Connect: Connect; - ConnectSettings: ConnectSettings; - ConnectSettingsValues: ConnectSettingsValues; - ConnectSignInInput: ConnectSignInInput; - ConnectUserInfoInput: ConnectUserInfoInput; - ContainerHostConfig: ContainerHostConfig; - ContainerMount: ContainerMount; - ContainerPort: ContainerPort; - CreateApiKeyInput: CreateApiKeyInput; - DateTime: Scalars['DateTime']['output']; - Devices: Devices; - Disk: Disk; - DiskPartition: DiskPartition; - Display: Display; - Docker: Docker; - DockerContainer: DockerContainer; - DockerMutations: DockerMutations; - DockerNetwork: DockerNetwork; - DynamicRemoteAccessStatus: DynamicRemoteAccessStatus; - EnableDynamicRemoteAccessInput: EnableDynamicRemoteAccessInput; - Flash: Flash; - Float: Scalars['Float']['output']; - Gpu: Gpu; - ID: Scalars['ID']['output']; - Info: Info; - InfoApps: InfoApps; - InfoCpu: InfoCpu; - InfoMemory: InfoMemory; - Int: Scalars['Int']['output']; - JSON: Scalars['JSON']['output']; - KeyFile: KeyFile; - LogFile: LogFile; - LogFileContent: LogFileContent; - Long: Scalars['Long']['output']; - Me: Me; - MemoryLayout: MemoryLayout; - MinigraphqlResponse: MinigraphqlResponse; - Mount: Mount; - Mutation: {}; - Network: Network; - Node: ResolversInterfaceTypes['Node']; - Notification: Notification; - NotificationCounts: NotificationCounts; - NotificationData: NotificationData; - NotificationFilter: NotificationFilter; - NotificationOverview: NotificationOverview; - Notifications: Notifications; - Os: Os; - Owner: Owner; - ParityCheck: ParityCheck; - Partition: Partition; - Pci: Pci; - Permission: Permission; - Port: Scalars['Port']['output']; - ProfileModel: ProfileModel; - Query: {}; - Registration: Registration; - RelayResponse: RelayResponse; - RemoteAccess: RemoteAccess; - RemoveRoleFromApiKeyInput: RemoveRoleFromApiKeyInput; - Server: Server; - Service: Service; - SetupRemoteAccessInput: SetupRemoteAccessInput; - Share: Share; - String: Scalars['String']['output']; - Subscription: {}; - System: System; - URL: Scalars['URL']['output']; - UUID: Scalars['UUID']['output']; - UnassignedDevice: UnassignedDevice; - Uptime: Uptime; - Usb: Usb; - User: User; - UserAccount: ResolversInterfaceTypes['UserAccount']; - Vars: Vars; - Versions: Versions; - VmDomain: VmDomain; - VmMutations: VmMutations; - Vms: Vms; - Welcome: Welcome; - addUserInput: addUserInput; - deleteUserInput: deleteUserInput; - usersInput: usersInput; -}>; - -export type authDirectiveArgs = { - action: AuthActionVerb; - possession: AuthPossession; - resource: Resource; -}; - -export type authDirectiveResolver = DirectiveResolverFn; - -export type AccessUrlResolvers = ResolversObject<{ - ipv4?: Resolver, ParentType, ContextType>; - ipv6?: Resolver, ParentType, ContextType>; - name?: Resolver, ParentType, ContextType>; - type?: Resolver; - __isTypeOf?: IsTypeOfResolverFn; -}>; - -export type ApiKeyResolvers = ResolversObject<{ - createdAt?: Resolver; - description?: Resolver, ParentType, ContextType>; - id?: Resolver; - name?: Resolver; - permissions?: Resolver, ParentType, ContextType>; - roles?: Resolver, ParentType, ContextType>; - __isTypeOf?: IsTypeOfResolverFn; -}>; - -export type ApiKeyResponseResolvers = ResolversObject<{ - error?: Resolver, ParentType, ContextType>; - valid?: Resolver; - __isTypeOf?: IsTypeOfResolverFn; -}>; - -export type ApiKeyWithSecretResolvers = ResolversObject<{ - createdAt?: Resolver; - description?: Resolver, ParentType, ContextType>; - id?: Resolver; - key?: Resolver; - name?: Resolver; - permissions?: Resolver, ParentType, ContextType>; - roles?: Resolver, ParentType, ContextType>; - __isTypeOf?: IsTypeOfResolverFn; -}>; - -export type ArrayResolvers = ResolversObject<{ - boot?: Resolver, ParentType, ContextType>; - caches?: Resolver, ParentType, ContextType>; - capacity?: Resolver; - disks?: Resolver, ParentType, ContextType>; - id?: Resolver; - parities?: Resolver, ParentType, ContextType>; - pendingState?: Resolver, ParentType, ContextType>; - previousState?: Resolver, ParentType, ContextType>; - state?: Resolver; - __isTypeOf?: IsTypeOfResolverFn; -}>; - -export type ArrayCapacityResolvers = ResolversObject<{ - disks?: Resolver; - kilobytes?: Resolver; - __isTypeOf?: IsTypeOfResolverFn; -}>; - -export type ArrayDiskResolvers = ResolversObject<{ - color?: Resolver, ParentType, ContextType>; - comment?: Resolver, ParentType, ContextType>; - critical?: Resolver, ParentType, ContextType>; - device?: Resolver, ParentType, ContextType>; - exportable?: Resolver, ParentType, ContextType>; - format?: Resolver, ParentType, ContextType>; - fsFree?: Resolver, ParentType, ContextType>; - fsSize?: Resolver, ParentType, ContextType>; - fsType?: Resolver, ParentType, ContextType>; - fsUsed?: Resolver, ParentType, ContextType>; - id?: Resolver; - idx?: Resolver; - name?: Resolver, ParentType, ContextType>; - numErrors?: Resolver; - numReads?: Resolver; - numWrites?: Resolver; - rotational?: Resolver, ParentType, ContextType>; - size?: Resolver; - status?: Resolver, ParentType, ContextType>; - temp?: Resolver, ParentType, ContextType>; - transport?: Resolver, ParentType, ContextType>; - type?: Resolver; - warning?: Resolver, ParentType, ContextType>; - __isTypeOf?: IsTypeOfResolverFn; -}>; - -export type ArrayMutationsResolvers = ResolversObject<{ - addDiskToArray?: Resolver, ParentType, ContextType, Partial>; - clearArrayDiskStatistics?: Resolver, ParentType, ContextType, RequireFields>; - mountArrayDisk?: Resolver, ParentType, ContextType, RequireFields>; - removeDiskFromArray?: Resolver, ParentType, ContextType, Partial>; - setState?: Resolver, ParentType, ContextType, Partial>; - unmountArrayDisk?: Resolver, ParentType, ContextType, RequireFields>; - __isTypeOf?: IsTypeOfResolverFn; -}>; - -export type BaseboardResolvers = ResolversObject<{ - assetTag?: Resolver, ParentType, ContextType>; - manufacturer?: Resolver; - model?: Resolver, ParentType, ContextType>; - serial?: Resolver, ParentType, ContextType>; - version?: Resolver, ParentType, ContextType>; - __isTypeOf?: IsTypeOfResolverFn; -}>; - -export type CapacityResolvers = ResolversObject<{ - free?: Resolver; - total?: Resolver; - used?: Resolver; - __isTypeOf?: IsTypeOfResolverFn; -}>; - -export type CaseResolvers = ResolversObject<{ - base64?: Resolver, ParentType, ContextType>; - error?: Resolver, ParentType, ContextType>; - icon?: Resolver, ParentType, ContextType>; - url?: Resolver, ParentType, ContextType>; - __isTypeOf?: IsTypeOfResolverFn; -}>; - -export type CloudResolvers = ResolversObject<{ - allowedOrigins?: Resolver, ParentType, ContextType>; - apiKey?: Resolver; - cloud?: Resolver; - error?: Resolver, ParentType, ContextType>; - minigraphql?: Resolver; - relay?: Resolver, ParentType, ContextType>; - __isTypeOf?: IsTypeOfResolverFn; -}>; - -export type CloudResponseResolvers = ResolversObject<{ - error?: Resolver, ParentType, ContextType>; - ip?: Resolver, ParentType, ContextType>; - status?: Resolver; - __isTypeOf?: IsTypeOfResolverFn; -}>; - -export type ConfigResolvers = ResolversObject<{ - error?: Resolver, ParentType, ContextType>; - id?: Resolver; - valid?: Resolver, ParentType, ContextType>; - __isTypeOf?: IsTypeOfResolverFn; -}>; - -export type ConnectResolvers = ResolversObject<{ - dynamicRemoteAccess?: Resolver; - id?: Resolver; - settings?: Resolver; - __isTypeOf?: IsTypeOfResolverFn; -}>; - -export type ConnectSettingsResolvers = ResolversObject<{ - dataSchema?: Resolver; - id?: Resolver; - uiSchema?: Resolver; - values?: Resolver; - __isTypeOf?: IsTypeOfResolverFn; -}>; - -export type ConnectSettingsValuesResolvers = ResolversObject<{ - accessType?: Resolver; - extraOrigins?: Resolver, ParentType, ContextType>; - forwardType?: Resolver, ParentType, ContextType>; - port?: Resolver, ParentType, ContextType>; - sandbox?: Resolver; - ssoUserIds?: Resolver, ParentType, ContextType>; - __isTypeOf?: IsTypeOfResolverFn; -}>; - -export type ContainerHostConfigResolvers = ResolversObject<{ - networkMode?: Resolver; - __isTypeOf?: IsTypeOfResolverFn; -}>; - -export type ContainerMountResolvers = ResolversObject<{ - destination?: Resolver; - driver?: Resolver; - mode?: Resolver; - name?: Resolver; - propagation?: Resolver; - rw?: Resolver; - source?: Resolver; - type?: Resolver; - __isTypeOf?: IsTypeOfResolverFn; -}>; - -export type ContainerPortResolvers = ResolversObject<{ - ip?: Resolver, ParentType, ContextType>; - privatePort?: Resolver, ParentType, ContextType>; - publicPort?: Resolver, ParentType, ContextType>; - type?: Resolver, ParentType, ContextType>; - __isTypeOf?: IsTypeOfResolverFn; -}>; - -export interface DateTimeScalarConfig extends GraphQLScalarTypeConfig { - name: 'DateTime'; -} - -export type DevicesResolvers = ResolversObject<{ - gpu?: Resolver>>, ParentType, ContextType>; - network?: Resolver>>, ParentType, ContextType>; - pci?: Resolver>>, ParentType, ContextType>; - usb?: Resolver>>, ParentType, ContextType>; - __isTypeOf?: IsTypeOfResolverFn; -}>; - -export type DiskResolvers = ResolversObject<{ - bytesPerSector?: Resolver; - device?: Resolver; - firmwareRevision?: Resolver; - id?: Resolver; - interfaceType?: Resolver; - name?: Resolver; - partitions?: Resolver>, ParentType, ContextType>; - sectorsPerTrack?: Resolver; - serialNum?: Resolver; - size?: Resolver; - smartStatus?: Resolver; - temperature?: Resolver, ParentType, ContextType>; - totalCylinders?: Resolver; - totalHeads?: Resolver; - totalSectors?: Resolver; - totalTracks?: Resolver; - tracksPerCylinder?: Resolver; - type?: Resolver; - vendor?: Resolver; - __isTypeOf?: IsTypeOfResolverFn; -}>; - -export type DiskPartitionResolvers = ResolversObject<{ - fsType?: Resolver; - name?: Resolver; - size?: Resolver; - __isTypeOf?: IsTypeOfResolverFn; -}>; - -export type DisplayResolvers = ResolversObject<{ - banner?: Resolver, ParentType, ContextType>; - case?: Resolver, ParentType, ContextType>; - critical?: Resolver, ParentType, ContextType>; - dashapps?: Resolver, ParentType, ContextType>; - date?: Resolver, ParentType, ContextType>; - hot?: Resolver, ParentType, ContextType>; - id?: Resolver; - locale?: Resolver, ParentType, ContextType>; - max?: Resolver, ParentType, ContextType>; - number?: Resolver, ParentType, ContextType>; - resize?: Resolver, ParentType, ContextType>; - scale?: Resolver, ParentType, ContextType>; - tabs?: Resolver, ParentType, ContextType>; - text?: Resolver, ParentType, ContextType>; - theme?: Resolver, ParentType, ContextType>; - total?: Resolver, ParentType, ContextType>; - unit?: Resolver, ParentType, ContextType>; - usage?: Resolver, ParentType, ContextType>; - users?: Resolver, ParentType, ContextType>; - warning?: Resolver, ParentType, ContextType>; - wwn?: Resolver, ParentType, ContextType>; - __isTypeOf?: IsTypeOfResolverFn; -}>; - -export type DockerResolvers = ResolversObject<{ - containers?: Resolver>, ParentType, ContextType>; - id?: Resolver; - networks?: Resolver>, ParentType, ContextType>; - __isTypeOf?: IsTypeOfResolverFn; -}>; - -export type DockerContainerResolvers = ResolversObject<{ - autoStart?: Resolver; - command?: Resolver; - created?: Resolver; - hostConfig?: Resolver, ParentType, ContextType>; - id?: Resolver; - image?: Resolver; - imageId?: Resolver; - labels?: Resolver, ParentType, ContextType>; - mounts?: Resolver>>, ParentType, ContextType>; - names?: Resolver>, ParentType, ContextType>; - networkSettings?: Resolver, ParentType, ContextType>; - ports?: Resolver, ParentType, ContextType>; - sizeRootFs?: Resolver, ParentType, ContextType>; - state?: Resolver; - status?: Resolver; - __isTypeOf?: IsTypeOfResolverFn; -}>; - -export type DockerMutationsResolvers = ResolversObject<{ - start?: Resolver>; - stop?: Resolver>; - __isTypeOf?: IsTypeOfResolverFn; -}>; - -export type DockerNetworkResolvers = ResolversObject<{ - attachable?: Resolver; - configFrom?: Resolver, ParentType, ContextType>; - configOnly?: Resolver; - containers?: Resolver, ParentType, ContextType>; - created?: Resolver, ParentType, ContextType>; - driver?: Resolver, ParentType, ContextType>; - enableIPv6?: Resolver; - id?: Resolver, ParentType, ContextType>; - ingress?: Resolver; - internal?: Resolver; - ipam?: Resolver, ParentType, ContextType>; - labels?: Resolver, ParentType, ContextType>; - name?: Resolver, ParentType, ContextType>; - options?: Resolver, ParentType, ContextType>; - scope?: Resolver, ParentType, ContextType>; - __isTypeOf?: IsTypeOfResolverFn; -}>; - -export type DynamicRemoteAccessStatusResolvers = ResolversObject<{ - enabledType?: Resolver; - error?: Resolver, ParentType, ContextType>; - runningType?: Resolver; - __isTypeOf?: IsTypeOfResolverFn; -}>; - -export type FlashResolvers = ResolversObject<{ - guid?: Resolver, ParentType, ContextType>; - product?: Resolver, ParentType, ContextType>; - vendor?: Resolver, ParentType, ContextType>; - __isTypeOf?: IsTypeOfResolverFn; -}>; - -export type GpuResolvers = ResolversObject<{ - blacklisted?: Resolver; - class?: Resolver; - id?: Resolver; - productid?: Resolver; - type?: Resolver; - typeid?: Resolver; - vendorname?: Resolver; - __isTypeOf?: IsTypeOfResolverFn; -}>; - -export type InfoResolvers = ResolversObject<{ - apps?: Resolver, ParentType, ContextType>; - baseboard?: Resolver, ParentType, ContextType>; - cpu?: Resolver, ParentType, ContextType>; - devices?: Resolver, ParentType, ContextType>; - display?: Resolver, ParentType, ContextType>; - id?: Resolver; - machineId?: Resolver, ParentType, ContextType>; - memory?: Resolver, ParentType, ContextType>; - os?: Resolver, ParentType, ContextType>; - system?: Resolver, ParentType, ContextType>; - time?: Resolver; - versions?: Resolver, ParentType, ContextType>; - __isTypeOf?: IsTypeOfResolverFn; -}>; - -export type InfoAppsResolvers = ResolversObject<{ - installed?: Resolver, ParentType, ContextType>; - started?: Resolver, ParentType, ContextType>; - __isTypeOf?: IsTypeOfResolverFn; -}>; - -export type InfoCpuResolvers = ResolversObject<{ - brand?: Resolver; - cache?: Resolver; - cores?: Resolver; - family?: Resolver; - flags?: Resolver>, ParentType, ContextType>; - manufacturer?: Resolver; - model?: Resolver; - processors?: Resolver; - revision?: Resolver; - socket?: Resolver; - speed?: Resolver; - speedmax?: Resolver; - speedmin?: Resolver; - stepping?: Resolver; - threads?: Resolver; - vendor?: Resolver; - voltage?: Resolver, ParentType, ContextType>; - __isTypeOf?: IsTypeOfResolverFn; -}>; - -export type InfoMemoryResolvers = ResolversObject<{ - active?: Resolver; - available?: Resolver; - buffcache?: Resolver; - free?: Resolver; - layout?: Resolver>, ParentType, ContextType>; - max?: Resolver; - swapfree?: Resolver; - swaptotal?: Resolver; - swapused?: Resolver; - total?: Resolver; - used?: Resolver; - __isTypeOf?: IsTypeOfResolverFn; -}>; - -export interface JSONScalarConfig extends GraphQLScalarTypeConfig { - name: 'JSON'; -} - -export type KeyFileResolvers = ResolversObject<{ - contents?: Resolver, ParentType, ContextType>; - location?: Resolver, ParentType, ContextType>; - __isTypeOf?: IsTypeOfResolverFn; -}>; - -export type LogFileResolvers = ResolversObject<{ - modifiedAt?: Resolver; - name?: Resolver; - path?: Resolver; - size?: Resolver; - __isTypeOf?: IsTypeOfResolverFn; -}>; - -export type LogFileContentResolvers = ResolversObject<{ - content?: Resolver; - path?: Resolver; - startLine?: Resolver, ParentType, ContextType>; - totalLines?: Resolver; - __isTypeOf?: IsTypeOfResolverFn; -}>; - -export interface LongScalarConfig extends GraphQLScalarTypeConfig { - name: 'Long'; -} - -export type MeResolvers = ResolversObject<{ - description?: Resolver; - id?: Resolver; - name?: Resolver; - permissions?: Resolver>, ParentType, ContextType>; - roles?: Resolver, ParentType, ContextType>; - __isTypeOf?: IsTypeOfResolverFn; -}>; - -export type MemoryLayoutResolvers = ResolversObject<{ - bank?: Resolver, ParentType, ContextType>; - clockSpeed?: Resolver, ParentType, ContextType>; - formFactor?: Resolver, ParentType, ContextType>; - manufacturer?: Resolver, ParentType, ContextType>; - partNum?: Resolver, ParentType, ContextType>; - serialNum?: Resolver, ParentType, ContextType>; - size?: Resolver; - type?: Resolver, ParentType, ContextType>; - voltageConfigured?: Resolver, ParentType, ContextType>; - voltageMax?: Resolver, ParentType, ContextType>; - voltageMin?: Resolver, ParentType, ContextType>; - __isTypeOf?: IsTypeOfResolverFn; -}>; - -export type MinigraphqlResponseResolvers = ResolversObject<{ - error?: Resolver, ParentType, ContextType>; - status?: Resolver; - timeout?: Resolver, ParentType, ContextType>; - __isTypeOf?: IsTypeOfResolverFn; -}>; - -export type MountResolvers = ResolversObject<{ - directory?: Resolver, ParentType, ContextType>; - name?: Resolver, ParentType, ContextType>; - permissions?: Resolver, ParentType, ContextType>; - type?: Resolver, ParentType, ContextType>; - __isTypeOf?: IsTypeOfResolverFn; -}>; - -export type MutationResolvers = ResolversObject<{ - addPermission?: Resolver>; - addRoleForApiKey?: Resolver>; - addRoleForUser?: Resolver>; - addUser?: Resolver, ParentType, ContextType, RequireFields>; - archiveAll?: Resolver>; - archiveNotification?: Resolver>; - archiveNotifications?: Resolver>; - array?: Resolver, ParentType, ContextType>; - cancelParityCheck?: Resolver, ParentType, ContextType>; - connectSignIn?: Resolver>; - connectSignOut?: Resolver; - createApiKey?: Resolver>; - createNotification?: Resolver>; - deleteArchivedNotifications?: Resolver; - deleteNotification?: Resolver>; - deleteUser?: Resolver, ParentType, ContextType, RequireFields>; - docker?: Resolver, ParentType, ContextType>; - enableDynamicRemoteAccess?: Resolver>; - login?: Resolver, ParentType, ContextType, RequireFields>; - pauseParityCheck?: Resolver, ParentType, ContextType>; - reboot?: Resolver, ParentType, ContextType>; - recalculateOverview?: Resolver; - removeRoleFromApiKey?: Resolver>; - resumeParityCheck?: Resolver, ParentType, ContextType>; - setAdditionalAllowedOrigins?: Resolver, ParentType, ContextType, RequireFields>; - setupRemoteAccess?: Resolver>; - shutdown?: Resolver, ParentType, ContextType>; - startParityCheck?: Resolver, ParentType, ContextType, Partial>; - unarchiveAll?: Resolver>; - unarchiveNotifications?: Resolver>; - unreadNotification?: Resolver>; - updateApiSettings?: Resolver>; - vms?: Resolver, ParentType, ContextType>; -}>; - -export type NetworkResolvers = ResolversObject<{ - accessUrls?: Resolver>, ParentType, ContextType>; - carrierChanges?: Resolver, ParentType, ContextType>; - duplex?: Resolver, ParentType, ContextType>; - id?: Resolver; - iface?: Resolver, ParentType, ContextType>; - ifaceName?: Resolver, ParentType, ContextType>; - internal?: Resolver, ParentType, ContextType>; - ipv4?: Resolver, ParentType, ContextType>; - ipv6?: Resolver, ParentType, ContextType>; - mac?: Resolver, ParentType, ContextType>; - mtu?: Resolver, ParentType, ContextType>; - operstate?: Resolver, ParentType, ContextType>; - speed?: Resolver, ParentType, ContextType>; - type?: Resolver, ParentType, ContextType>; - __isTypeOf?: IsTypeOfResolverFn; -}>; - -export type NodeResolvers = ResolversObject<{ - __resolveType: TypeResolveFn<'Array' | 'Config' | 'Connect' | 'ConnectSettings' | 'Docker' | 'Info' | 'Network' | 'Notification' | 'Notifications' | 'Service' | 'Vars', ParentType, ContextType>; - id?: Resolver; -}>; - -export type NotificationResolvers = ResolversObject<{ - description?: Resolver; - formattedTimestamp?: Resolver, ParentType, ContextType>; - id?: Resolver; - importance?: Resolver; - link?: Resolver, ParentType, ContextType>; - subject?: Resolver; - timestamp?: Resolver, ParentType, ContextType>; - title?: Resolver; - type?: Resolver; - __isTypeOf?: IsTypeOfResolverFn; -}>; - -export type NotificationCountsResolvers = ResolversObject<{ - alert?: Resolver; - info?: Resolver; - total?: Resolver; - warning?: Resolver; - __isTypeOf?: IsTypeOfResolverFn; -}>; - -export type NotificationOverviewResolvers = ResolversObject<{ - archive?: Resolver; - unread?: Resolver; - __isTypeOf?: IsTypeOfResolverFn; -}>; - -export type NotificationsResolvers = ResolversObject<{ - id?: Resolver; - list?: Resolver, ParentType, ContextType, RequireFields>; - overview?: Resolver; - __isTypeOf?: IsTypeOfResolverFn; -}>; - -export type OsResolvers = ResolversObject<{ - arch?: Resolver, ParentType, ContextType>; - build?: Resolver, ParentType, ContextType>; - codename?: Resolver, ParentType, ContextType>; - codepage?: Resolver, ParentType, ContextType>; - distro?: Resolver, ParentType, ContextType>; - hostname?: Resolver, ParentType, ContextType>; - kernel?: Resolver, ParentType, ContextType>; - logofile?: Resolver, ParentType, ContextType>; - platform?: Resolver, ParentType, ContextType>; - release?: Resolver, ParentType, ContextType>; - serial?: Resolver, ParentType, ContextType>; - uptime?: Resolver, ParentType, ContextType>; - __isTypeOf?: IsTypeOfResolverFn; -}>; - -export type OwnerResolvers = ResolversObject<{ - avatar?: Resolver, ParentType, ContextType>; - url?: Resolver, ParentType, ContextType>; - username?: Resolver, ParentType, ContextType>; - __isTypeOf?: IsTypeOfResolverFn; -}>; - -export type ParityCheckResolvers = ResolversObject<{ - date?: Resolver; - duration?: Resolver; - errors?: Resolver; - speed?: Resolver; - status?: Resolver; - __isTypeOf?: IsTypeOfResolverFn; -}>; - -export type PartitionResolvers = ResolversObject<{ - devlinks?: Resolver, ParentType, ContextType>; - devname?: Resolver, ParentType, ContextType>; - devpath?: Resolver, ParentType, ContextType>; - devtype?: Resolver, ParentType, ContextType>; - idAta?: Resolver, ParentType, ContextType>; - idAtaDownloadMicrocode?: Resolver, ParentType, ContextType>; - idAtaFeatureSetAam?: Resolver, ParentType, ContextType>; - idAtaFeatureSetAamCurrentValue?: Resolver, ParentType, ContextType>; - idAtaFeatureSetAamEnabled?: Resolver, ParentType, ContextType>; - idAtaFeatureSetAamVendorRecommendedValue?: Resolver, ParentType, ContextType>; - idAtaFeatureSetApm?: Resolver, ParentType, ContextType>; - idAtaFeatureSetApmCurrentValue?: Resolver, ParentType, ContextType>; - idAtaFeatureSetApmEnabled?: Resolver, ParentType, ContextType>; - idAtaFeatureSetHpa?: Resolver, ParentType, ContextType>; - idAtaFeatureSetHpaEnabled?: Resolver, ParentType, ContextType>; - idAtaFeatureSetPm?: Resolver, ParentType, ContextType>; - idAtaFeatureSetPmEnabled?: Resolver, ParentType, ContextType>; - idAtaFeatureSetPuis?: Resolver, ParentType, ContextType>; - idAtaFeatureSetPuisEnabled?: Resolver, ParentType, ContextType>; - idAtaFeatureSetSecurity?: Resolver, ParentType, ContextType>; - idAtaFeatureSetSecurityEnabled?: Resolver, ParentType, ContextType>; - idAtaFeatureSetSecurityEnhancedEraseUnitMin?: Resolver, ParentType, ContextType>; - idAtaFeatureSetSecurityEraseUnitMin?: Resolver, ParentType, ContextType>; - idAtaFeatureSetSmart?: Resolver, ParentType, ContextType>; - idAtaFeatureSetSmartEnabled?: Resolver, ParentType, ContextType>; - idAtaRotationRateRpm?: Resolver, ParentType, ContextType>; - idAtaSata?: Resolver, ParentType, ContextType>; - idAtaSataSignalRateGen1?: Resolver, ParentType, ContextType>; - idAtaSataSignalRateGen2?: Resolver, ParentType, ContextType>; - idAtaWriteCache?: Resolver, ParentType, ContextType>; - idAtaWriteCacheEnabled?: Resolver, ParentType, ContextType>; - idBus?: Resolver, ParentType, ContextType>; - idFsType?: Resolver, ParentType, ContextType>; - idFsUsage?: Resolver, ParentType, ContextType>; - idFsUuid?: Resolver, ParentType, ContextType>; - idFsUuidEnc?: Resolver, ParentType, ContextType>; - idModel?: Resolver, ParentType, ContextType>; - idModelEnc?: Resolver, ParentType, ContextType>; - idPartEntryDisk?: Resolver, ParentType, ContextType>; - idPartEntryNumber?: Resolver, ParentType, ContextType>; - idPartEntryOffset?: Resolver, ParentType, ContextType>; - idPartEntryScheme?: Resolver, ParentType, ContextType>; - idPartEntrySize?: Resolver, ParentType, ContextType>; - idPartEntryType?: Resolver, ParentType, ContextType>; - idPartTableType?: Resolver, ParentType, ContextType>; - idPath?: Resolver, ParentType, ContextType>; - idPathTag?: Resolver, ParentType, ContextType>; - idRevision?: Resolver, ParentType, ContextType>; - idSerial?: Resolver, ParentType, ContextType>; - idSerialShort?: Resolver, ParentType, ContextType>; - idType?: Resolver, ParentType, ContextType>; - idWwn?: Resolver, ParentType, ContextType>; - idWwnWithExtension?: Resolver, ParentType, ContextType>; - major?: Resolver, ParentType, ContextType>; - minor?: Resolver, ParentType, ContextType>; - partn?: Resolver, ParentType, ContextType>; - subsystem?: Resolver, ParentType, ContextType>; - usecInitialized?: Resolver, ParentType, ContextType>; - __isTypeOf?: IsTypeOfResolverFn; -}>; - -export type PciResolvers = ResolversObject<{ - blacklisted?: Resolver, ParentType, ContextType>; - class?: Resolver, ParentType, ContextType>; - id?: Resolver; - productid?: Resolver, ParentType, ContextType>; - productname?: Resolver, ParentType, ContextType>; - type?: Resolver, ParentType, ContextType>; - typeid?: Resolver, ParentType, ContextType>; - vendorid?: Resolver, ParentType, ContextType>; - vendorname?: Resolver, ParentType, ContextType>; - __isTypeOf?: IsTypeOfResolverFn; -}>; - -export type PermissionResolvers = ResolversObject<{ - actions?: Resolver, ParentType, ContextType>; - resource?: Resolver; - __isTypeOf?: IsTypeOfResolverFn; -}>; - -export interface PortScalarConfig extends GraphQLScalarTypeConfig { - name: 'Port'; -} - -export type ProfileModelResolvers = ResolversObject<{ - avatar?: Resolver, ParentType, ContextType>; - url?: Resolver, ParentType, ContextType>; - userId?: Resolver, ParentType, ContextType>; - username?: Resolver, ParentType, ContextType>; - __isTypeOf?: IsTypeOfResolverFn; -}>; - -export type QueryResolvers = ResolversObject<{ - apiKey?: Resolver, ParentType, ContextType, RequireFields>; - apiKeys?: Resolver, ParentType, ContextType>; - array?: Resolver; - cloud?: Resolver, ParentType, ContextType>; - config?: Resolver; - connect?: Resolver; - disk?: Resolver, ParentType, ContextType, RequireFields>; - disks?: Resolver>, ParentType, ContextType>; - display?: Resolver, ParentType, ContextType>; - docker?: Resolver; - dockerNetwork?: Resolver>; - dockerNetworks?: Resolver>, ParentType, ContextType, Partial>; - extraAllowedOrigins?: Resolver, ParentType, ContextType>; - flash?: Resolver, ParentType, ContextType>; - info?: Resolver, ParentType, ContextType>; - logFile?: Resolver>; - logFiles?: Resolver, ParentType, ContextType>; - me?: Resolver, ParentType, ContextType>; - network?: Resolver, ParentType, ContextType>; - notifications?: Resolver; - online?: Resolver, ParentType, ContextType>; - owner?: Resolver, ParentType, ContextType>; - parityHistory?: Resolver>>, ParentType, ContextType>; - registration?: Resolver, ParentType, ContextType>; - remoteAccess?: Resolver; - server?: Resolver, ParentType, ContextType>; - servers?: Resolver, ParentType, ContextType>; - services?: Resolver, ParentType, ContextType>; - shares?: Resolver>>, ParentType, ContextType>; - unassignedDevices?: Resolver>>, ParentType, ContextType>; - user?: Resolver, ParentType, ContextType, RequireFields>; - users?: Resolver, ParentType, ContextType, Partial>; - vars?: Resolver, ParentType, ContextType>; - vms?: Resolver, ParentType, ContextType>; -}>; - -export type RegistrationResolvers = ResolversObject<{ - expiration?: Resolver, ParentType, ContextType>; - guid?: Resolver, ParentType, ContextType>; - keyFile?: Resolver, ParentType, ContextType>; - state?: Resolver, ParentType, ContextType>; - type?: Resolver, ParentType, ContextType>; - updateExpiration?: Resolver, ParentType, ContextType>; - __isTypeOf?: IsTypeOfResolverFn; -}>; - -export type RelayResponseResolvers = ResolversObject<{ - error?: Resolver, ParentType, ContextType>; - status?: Resolver; - timeout?: Resolver, ParentType, ContextType>; - __isTypeOf?: IsTypeOfResolverFn; -}>; - -export type RemoteAccessResolvers = ResolversObject<{ - accessType?: Resolver; - forwardType?: Resolver, ParentType, ContextType>; - port?: Resolver, ParentType, ContextType>; - __isTypeOf?: IsTypeOfResolverFn; -}>; - -export type ServerResolvers = ResolversObject<{ - apikey?: Resolver; - guid?: Resolver; - lanip?: Resolver; - localurl?: Resolver; - name?: Resolver; - owner?: Resolver; - remoteurl?: Resolver; - status?: Resolver; - wanip?: Resolver; - __isTypeOf?: IsTypeOfResolverFn; -}>; - -export type ServiceResolvers = ResolversObject<{ - id?: Resolver; - name?: Resolver, ParentType, ContextType>; - online?: Resolver, ParentType, ContextType>; - uptime?: Resolver, ParentType, ContextType>; - version?: Resolver, ParentType, ContextType>; - __isTypeOf?: IsTypeOfResolverFn; -}>; - -export type ShareResolvers = ResolversObject<{ - allocator?: Resolver, ParentType, ContextType>; - cache?: Resolver, ParentType, ContextType>; - color?: Resolver, ParentType, ContextType>; - comment?: Resolver, ParentType, ContextType>; - cow?: Resolver, ParentType, ContextType>; - exclude?: Resolver>>, ParentType, ContextType>; - floor?: Resolver, ParentType, ContextType>; - free?: Resolver, ParentType, ContextType>; - include?: Resolver>>, ParentType, ContextType>; - luksStatus?: Resolver, ParentType, ContextType>; - name?: Resolver, ParentType, ContextType>; - nameOrig?: Resolver, ParentType, ContextType>; - size?: Resolver, ParentType, ContextType>; - splitLevel?: Resolver, ParentType, ContextType>; - used?: Resolver, ParentType, ContextType>; - __isTypeOf?: IsTypeOfResolverFn; -}>; - -export type SubscriptionResolvers = ResolversObject<{ - array?: SubscriptionResolver; - config?: SubscriptionResolver; - display?: SubscriptionResolver, "display", ParentType, ContextType>; - dockerNetwork?: SubscriptionResolver>; - dockerNetworks?: SubscriptionResolver>, "dockerNetworks", ParentType, ContextType>; - flash?: SubscriptionResolver; - info?: SubscriptionResolver; - logFile?: SubscriptionResolver>; - me?: SubscriptionResolver, "me", ParentType, ContextType>; - notificationAdded?: SubscriptionResolver; - notificationsOverview?: SubscriptionResolver; - online?: SubscriptionResolver; - owner?: SubscriptionResolver; - parityHistory?: SubscriptionResolver; - ping?: SubscriptionResolver; - registration?: SubscriptionResolver; - server?: SubscriptionResolver, "server", ParentType, ContextType>; - service?: SubscriptionResolver>, "service", ParentType, ContextType, RequireFields>; - share?: SubscriptionResolver>; - shares?: SubscriptionResolver>, "shares", ParentType, ContextType>; - unassignedDevices?: SubscriptionResolver>, "unassignedDevices", ParentType, ContextType>; - user?: SubscriptionResolver>; - users?: SubscriptionResolver>, "users", ParentType, ContextType>; - vars?: SubscriptionResolver; - vms?: SubscriptionResolver, "vms", ParentType, ContextType>; -}>; - -export type SystemResolvers = ResolversObject<{ - manufacturer?: Resolver, ParentType, ContextType>; - model?: Resolver, ParentType, ContextType>; - serial?: Resolver, ParentType, ContextType>; - sku?: Resolver, ParentType, ContextType>; - uuid?: Resolver, ParentType, ContextType>; - version?: Resolver, ParentType, ContextType>; - __isTypeOf?: IsTypeOfResolverFn; -}>; - -export interface URLScalarConfig extends GraphQLScalarTypeConfig { - name: 'URL'; -} - -export interface UUIDScalarConfig extends GraphQLScalarTypeConfig { - name: 'UUID'; -} - -export type UnassignedDeviceResolvers = ResolversObject<{ - devlinks?: Resolver, ParentType, ContextType>; - devname?: Resolver, ParentType, ContextType>; - devpath?: Resolver, ParentType, ContextType>; - devtype?: Resolver, ParentType, ContextType>; - idAta?: Resolver, ParentType, ContextType>; - idAtaDownloadMicrocode?: Resolver, ParentType, ContextType>; - idAtaFeatureSetAam?: Resolver, ParentType, ContextType>; - idAtaFeatureSetAamCurrentValue?: Resolver, ParentType, ContextType>; - idAtaFeatureSetAamEnabled?: Resolver, ParentType, ContextType>; - idAtaFeatureSetAamVendorRecommendedValue?: Resolver, ParentType, ContextType>; - idAtaFeatureSetApm?: Resolver, ParentType, ContextType>; - idAtaFeatureSetApmCurrentValue?: Resolver, ParentType, ContextType>; - idAtaFeatureSetApmEnabled?: Resolver, ParentType, ContextType>; - idAtaFeatureSetHpa?: Resolver, ParentType, ContextType>; - idAtaFeatureSetHpaEnabled?: Resolver, ParentType, ContextType>; - idAtaFeatureSetPm?: Resolver, ParentType, ContextType>; - idAtaFeatureSetPmEnabled?: Resolver, ParentType, ContextType>; - idAtaFeatureSetPuis?: Resolver, ParentType, ContextType>; - idAtaFeatureSetPuisEnabled?: Resolver, ParentType, ContextType>; - idAtaFeatureSetSecurity?: Resolver, ParentType, ContextType>; - idAtaFeatureSetSecurityEnabled?: Resolver, ParentType, ContextType>; - idAtaFeatureSetSecurityEnhancedEraseUnitMin?: Resolver, ParentType, ContextType>; - idAtaFeatureSetSecurityEraseUnitMin?: Resolver, ParentType, ContextType>; - idAtaFeatureSetSmart?: Resolver, ParentType, ContextType>; - idAtaFeatureSetSmartEnabled?: Resolver, ParentType, ContextType>; - idAtaRotationRateRpm?: Resolver, ParentType, ContextType>; - idAtaSata?: Resolver, ParentType, ContextType>; - idAtaSataSignalRateGen1?: Resolver, ParentType, ContextType>; - idAtaSataSignalRateGen2?: Resolver, ParentType, ContextType>; - idAtaWriteCache?: Resolver, ParentType, ContextType>; - idAtaWriteCacheEnabled?: Resolver, ParentType, ContextType>; - idBus?: Resolver, ParentType, ContextType>; - idModel?: Resolver, ParentType, ContextType>; - idModelEnc?: Resolver, ParentType, ContextType>; - idPartTableType?: Resolver, ParentType, ContextType>; - idPath?: Resolver, ParentType, ContextType>; - idPathTag?: Resolver, ParentType, ContextType>; - idRevision?: Resolver, ParentType, ContextType>; - idSerial?: Resolver, ParentType, ContextType>; - idSerialShort?: Resolver, ParentType, ContextType>; - idType?: Resolver, ParentType, ContextType>; - idWwn?: Resolver, ParentType, ContextType>; - idWwnWithExtension?: Resolver, ParentType, ContextType>; - major?: Resolver, ParentType, ContextType>; - minor?: Resolver, ParentType, ContextType>; - mount?: Resolver, ParentType, ContextType>; - mounted?: Resolver, ParentType, ContextType>; - name?: Resolver, ParentType, ContextType>; - partitions?: Resolver>>, ParentType, ContextType>; - subsystem?: Resolver, ParentType, ContextType>; - temp?: Resolver, ParentType, ContextType>; - usecInitialized?: Resolver, ParentType, ContextType>; - __isTypeOf?: IsTypeOfResolverFn; -}>; - -export type UptimeResolvers = ResolversObject<{ - timestamp?: Resolver, ParentType, ContextType>; - __isTypeOf?: IsTypeOfResolverFn; -}>; - -export type UsbResolvers = ResolversObject<{ - id?: Resolver; - name?: Resolver, ParentType, ContextType>; - __isTypeOf?: IsTypeOfResolverFn; -}>; - -export type UserResolvers = ResolversObject<{ - description?: Resolver; - id?: Resolver; - name?: Resolver; - password?: Resolver, ParentType, ContextType>; - permissions?: Resolver>, ParentType, ContextType>; - roles?: Resolver, ParentType, ContextType>; - __isTypeOf?: IsTypeOfResolverFn; -}>; - -export type UserAccountResolvers = ResolversObject<{ - __resolveType: TypeResolveFn<'Me' | 'User', ParentType, ContextType>; - description?: Resolver; - id?: Resolver; - name?: Resolver; - permissions?: Resolver>, ParentType, ContextType>; - roles?: Resolver, ParentType, ContextType>; -}>; - -export type VarsResolvers = ResolversObject<{ - bindMgt?: Resolver, ParentType, ContextType>; - cacheNumDevices?: Resolver, ParentType, ContextType>; - cacheSbNumDisks?: Resolver, ParentType, ContextType>; - comment?: Resolver, ParentType, ContextType>; - configError?: Resolver, ParentType, ContextType>; - configValid?: Resolver, ParentType, ContextType>; - csrfToken?: Resolver, ParentType, ContextType>; - defaultFormat?: Resolver, ParentType, ContextType>; - defaultFsType?: Resolver, ParentType, ContextType>; - deviceCount?: Resolver, ParentType, ContextType>; - domain?: Resolver, ParentType, ContextType>; - domainLogin?: Resolver, ParentType, ContextType>; - domainShort?: Resolver, ParentType, ContextType>; - enableFruit?: Resolver, ParentType, ContextType>; - flashGuid?: Resolver, ParentType, ContextType>; - flashProduct?: Resolver, ParentType, ContextType>; - flashVendor?: Resolver, ParentType, ContextType>; - fsCopyPrcnt?: Resolver, ParentType, ContextType>; - fsNumMounted?: Resolver, ParentType, ContextType>; - fsNumUnmountable?: Resolver, ParentType, ContextType>; - fsProgress?: Resolver, ParentType, ContextType>; - fsState?: Resolver, ParentType, ContextType>; - fsUnmountableMask?: Resolver, ParentType, ContextType>; - fuseDirectio?: Resolver, ParentType, ContextType>; - fuseDirectioDefault?: Resolver, ParentType, ContextType>; - fuseDirectioStatus?: Resolver, ParentType, ContextType>; - fuseRemember?: Resolver, ParentType, ContextType>; - fuseRememberDefault?: Resolver, ParentType, ContextType>; - fuseRememberStatus?: Resolver, ParentType, ContextType>; - hideDotFiles?: Resolver, ParentType, ContextType>; - id?: Resolver; - joinStatus?: Resolver, ParentType, ContextType>; - localMaster?: Resolver, ParentType, ContextType>; - localTld?: Resolver, ParentType, ContextType>; - luksKeyfile?: Resolver, ParentType, ContextType>; - maxArraysz?: Resolver, ParentType, ContextType>; - maxCachesz?: Resolver, ParentType, ContextType>; - mdColor?: Resolver, ParentType, ContextType>; - mdNumDisabled?: Resolver, ParentType, ContextType>; - mdNumDisks?: Resolver, ParentType, ContextType>; - mdNumErased?: Resolver, ParentType, ContextType>; - mdNumInvalid?: Resolver, ParentType, ContextType>; - mdNumMissing?: Resolver, ParentType, ContextType>; - mdNumNew?: Resolver, ParentType, ContextType>; - mdNumStripes?: Resolver, ParentType, ContextType>; - mdNumStripesDefault?: Resolver, ParentType, ContextType>; - mdNumStripesStatus?: Resolver, ParentType, ContextType>; - mdResync?: Resolver, ParentType, ContextType>; - mdResyncAction?: Resolver, ParentType, ContextType>; - mdResyncCorr?: Resolver, ParentType, ContextType>; - mdResyncDb?: Resolver, ParentType, ContextType>; - mdResyncDt?: Resolver, ParentType, ContextType>; - mdResyncPos?: Resolver, ParentType, ContextType>; - mdResyncSize?: Resolver, ParentType, ContextType>; - mdState?: Resolver, ParentType, ContextType>; - mdSyncThresh?: Resolver, ParentType, ContextType>; - mdSyncThreshDefault?: Resolver, ParentType, ContextType>; - mdSyncThreshStatus?: Resolver, ParentType, ContextType>; - mdSyncWindow?: Resolver, ParentType, ContextType>; - mdSyncWindowDefault?: Resolver, ParentType, ContextType>; - mdSyncWindowStatus?: Resolver, ParentType, ContextType>; - mdVersion?: Resolver, ParentType, ContextType>; - mdWriteMethod?: Resolver, ParentType, ContextType>; - mdWriteMethodDefault?: Resolver, ParentType, ContextType>; - mdWriteMethodStatus?: Resolver, ParentType, ContextType>; - name?: Resolver, ParentType, ContextType>; - nrRequests?: Resolver, ParentType, ContextType>; - nrRequestsDefault?: Resolver, ParentType, ContextType>; - nrRequestsStatus?: Resolver, ParentType, ContextType>; - ntpServer1?: Resolver, ParentType, ContextType>; - ntpServer2?: Resolver, ParentType, ContextType>; - ntpServer3?: Resolver, ParentType, ContextType>; - ntpServer4?: Resolver, ParentType, ContextType>; - pollAttributes?: Resolver, ParentType, ContextType>; - pollAttributesDefault?: Resolver, ParentType, ContextType>; - pollAttributesStatus?: Resolver, ParentType, ContextType>; - port?: Resolver, ParentType, ContextType>; - portssh?: Resolver, ParentType, ContextType>; - portssl?: Resolver, ParentType, ContextType>; - porttelnet?: Resolver, ParentType, ContextType>; - queueDepth?: Resolver, ParentType, ContextType>; - regCheck?: Resolver, ParentType, ContextType>; - regFile?: Resolver, ParentType, ContextType>; - regGen?: Resolver, ParentType, ContextType>; - regGuid?: Resolver, ParentType, ContextType>; - regState?: Resolver, ParentType, ContextType>; - regTm?: Resolver, ParentType, ContextType>; - regTm2?: Resolver, ParentType, ContextType>; - regTo?: Resolver, ParentType, ContextType>; - regTy?: Resolver, ParentType, ContextType>; - safeMode?: Resolver, ParentType, ContextType>; - sbClean?: Resolver, ParentType, ContextType>; - sbEvents?: Resolver, ParentType, ContextType>; - sbName?: Resolver, ParentType, ContextType>; - sbNumDisks?: Resolver, ParentType, ContextType>; - sbState?: Resolver, ParentType, ContextType>; - sbSyncErrs?: Resolver, ParentType, ContextType>; - sbSyncExit?: Resolver, ParentType, ContextType>; - sbSynced?: Resolver, ParentType, ContextType>; - sbSynced2?: Resolver, ParentType, ContextType>; - sbUpdated?: Resolver, ParentType, ContextType>; - sbVersion?: Resolver, ParentType, ContextType>; - security?: Resolver, ParentType, ContextType>; - shareAfpCount?: Resolver, ParentType, ContextType>; - shareAfpEnabled?: Resolver, ParentType, ContextType>; - shareAvahiAfpModel?: Resolver, ParentType, ContextType>; - shareAvahiAfpName?: Resolver, ParentType, ContextType>; - shareAvahiEnabled?: Resolver, ParentType, ContextType>; - shareAvahiSmbModel?: Resolver, ParentType, ContextType>; - shareAvahiSmbName?: Resolver, ParentType, ContextType>; - shareCacheEnabled?: Resolver, ParentType, ContextType>; - shareCacheFloor?: Resolver, ParentType, ContextType>; - shareCount?: Resolver, ParentType, ContextType>; - shareDisk?: Resolver, ParentType, ContextType>; - shareInitialGroup?: Resolver, ParentType, ContextType>; - shareInitialOwner?: Resolver, ParentType, ContextType>; - shareMoverActive?: Resolver, ParentType, ContextType>; - shareMoverLogging?: Resolver, ParentType, ContextType>; - shareMoverSchedule?: Resolver, ParentType, ContextType>; - shareNfsCount?: Resolver, ParentType, ContextType>; - shareNfsEnabled?: Resolver, ParentType, ContextType>; - shareSmbCount?: Resolver, ParentType, ContextType>; - shareSmbEnabled?: Resolver, ParentType, ContextType>; - shareUser?: Resolver, ParentType, ContextType>; - shareUserExclude?: Resolver, ParentType, ContextType>; - shareUserInclude?: Resolver, ParentType, ContextType>; - shutdownTimeout?: Resolver, ParentType, ContextType>; - spindownDelay?: Resolver, ParentType, ContextType>; - spinupGroups?: Resolver, ParentType, ContextType>; - startArray?: Resolver, ParentType, ContextType>; - startMode?: Resolver, ParentType, ContextType>; - startPage?: Resolver, ParentType, ContextType>; - sysArraySlots?: Resolver, ParentType, ContextType>; - sysCacheSlots?: Resolver, ParentType, ContextType>; - sysFlashSlots?: Resolver, ParentType, ContextType>; - sysModel?: Resolver, ParentType, ContextType>; - timeZone?: Resolver, ParentType, ContextType>; - useNtp?: Resolver, ParentType, ContextType>; - useSsh?: Resolver, ParentType, ContextType>; - useSsl?: Resolver, ParentType, ContextType>; - useTelnet?: Resolver, ParentType, ContextType>; - version?: Resolver, ParentType, ContextType>; - workgroup?: Resolver, ParentType, ContextType>; - __isTypeOf?: IsTypeOfResolverFn; -}>; - -export type VersionsResolvers = ResolversObject<{ - apache?: Resolver, ParentType, ContextType>; - docker?: Resolver, ParentType, ContextType>; - gcc?: Resolver, ParentType, ContextType>; - git?: Resolver, ParentType, ContextType>; - grunt?: Resolver, ParentType, ContextType>; - gulp?: Resolver, ParentType, ContextType>; - kernel?: Resolver, ParentType, ContextType>; - mongodb?: Resolver, ParentType, ContextType>; - mysql?: Resolver, ParentType, ContextType>; - nginx?: Resolver, ParentType, ContextType>; - node?: Resolver, ParentType, ContextType>; - npm?: Resolver, ParentType, ContextType>; - openssl?: Resolver, ParentType, ContextType>; - perl?: Resolver, ParentType, ContextType>; - php?: Resolver, ParentType, ContextType>; - pm2?: Resolver, ParentType, ContextType>; - postfix?: Resolver, ParentType, ContextType>; - postgresql?: Resolver, ParentType, ContextType>; - python?: Resolver, ParentType, ContextType>; - redis?: Resolver, ParentType, ContextType>; - systemOpenssl?: Resolver, ParentType, ContextType>; - systemOpensslLib?: Resolver, ParentType, ContextType>; - tsc?: Resolver, ParentType, ContextType>; - unraid?: Resolver, ParentType, ContextType>; - v8?: Resolver, ParentType, ContextType>; - yarn?: Resolver, ParentType, ContextType>; - __isTypeOf?: IsTypeOfResolverFn; -}>; - -export type VmDomainResolvers = ResolversObject<{ - name?: Resolver, ParentType, ContextType>; - state?: Resolver; - uuid?: Resolver; - __isTypeOf?: IsTypeOfResolverFn; -}>; - -export type VmMutationsResolvers = ResolversObject<{ - forceStopVm?: Resolver>; - pauseVm?: Resolver>; - rebootVm?: Resolver>; - resetVm?: Resolver>; - resumeVm?: Resolver>; - startVm?: Resolver>; - stopVm?: Resolver>; - __isTypeOf?: IsTypeOfResolverFn; -}>; - -export type VmsResolvers = ResolversObject<{ - domain?: Resolver>, ParentType, ContextType>; - id?: Resolver; - __isTypeOf?: IsTypeOfResolverFn; -}>; - -export type WelcomeResolvers = ResolversObject<{ - message?: Resolver; - __isTypeOf?: IsTypeOfResolverFn; -}>; - -export type Resolvers = ResolversObject<{ - AccessUrl?: AccessUrlResolvers; - ApiKey?: ApiKeyResolvers; - ApiKeyResponse?: ApiKeyResponseResolvers; - ApiKeyWithSecret?: ApiKeyWithSecretResolvers; - Array?: ArrayResolvers; - ArrayCapacity?: ArrayCapacityResolvers; - ArrayDisk?: ArrayDiskResolvers; - ArrayMutations?: ArrayMutationsResolvers; - Baseboard?: BaseboardResolvers; - Capacity?: CapacityResolvers; - Case?: CaseResolvers; - Cloud?: CloudResolvers; - CloudResponse?: CloudResponseResolvers; - Config?: ConfigResolvers; - Connect?: ConnectResolvers; - ConnectSettings?: ConnectSettingsResolvers; - ConnectSettingsValues?: ConnectSettingsValuesResolvers; - ContainerHostConfig?: ContainerHostConfigResolvers; - ContainerMount?: ContainerMountResolvers; - ContainerPort?: ContainerPortResolvers; - DateTime?: GraphQLScalarType; - Devices?: DevicesResolvers; - Disk?: DiskResolvers; - DiskPartition?: DiskPartitionResolvers; - Display?: DisplayResolvers; - Docker?: DockerResolvers; - DockerContainer?: DockerContainerResolvers; - DockerMutations?: DockerMutationsResolvers; - DockerNetwork?: DockerNetworkResolvers; - DynamicRemoteAccessStatus?: DynamicRemoteAccessStatusResolvers; - Flash?: FlashResolvers; - Gpu?: GpuResolvers; - Info?: InfoResolvers; - InfoApps?: InfoAppsResolvers; - InfoCpu?: InfoCpuResolvers; - InfoMemory?: InfoMemoryResolvers; - JSON?: GraphQLScalarType; - KeyFile?: KeyFileResolvers; - LogFile?: LogFileResolvers; - LogFileContent?: LogFileContentResolvers; - Long?: GraphQLScalarType; - Me?: MeResolvers; - MemoryLayout?: MemoryLayoutResolvers; - MinigraphqlResponse?: MinigraphqlResponseResolvers; - Mount?: MountResolvers; - Mutation?: MutationResolvers; - Network?: NetworkResolvers; - Node?: NodeResolvers; - Notification?: NotificationResolvers; - NotificationCounts?: NotificationCountsResolvers; - NotificationOverview?: NotificationOverviewResolvers; - Notifications?: NotificationsResolvers; - Os?: OsResolvers; - Owner?: OwnerResolvers; - ParityCheck?: ParityCheckResolvers; - Partition?: PartitionResolvers; - Pci?: PciResolvers; - Permission?: PermissionResolvers; - Port?: GraphQLScalarType; - ProfileModel?: ProfileModelResolvers; - Query?: QueryResolvers; - Registration?: RegistrationResolvers; - RelayResponse?: RelayResponseResolvers; - RemoteAccess?: RemoteAccessResolvers; - Server?: ServerResolvers; - Service?: ServiceResolvers; - Share?: ShareResolvers; - Subscription?: SubscriptionResolvers; - System?: SystemResolvers; - URL?: GraphQLScalarType; - UUID?: GraphQLScalarType; - UnassignedDevice?: UnassignedDeviceResolvers; - Uptime?: UptimeResolvers; - Usb?: UsbResolvers; - User?: UserResolvers; - UserAccount?: UserAccountResolvers; - Vars?: VarsResolvers; - Versions?: VersionsResolvers; - VmDomain?: VmDomainResolvers; - VmMutations?: VmMutationsResolvers; - Vms?: VmsResolvers; - Welcome?: WelcomeResolvers; -}>; - -export type DirectiveResolvers = ResolversObject<{ - auth?: authDirectiveResolver; -}>; diff --git a/api/src/graphql/generated/client/gql.ts b/api/src/graphql/generated/client/gql.ts index 39e13b535..8c3476945 100644 --- a/api/src/graphql/generated/client/gql.ts +++ b/api/src/graphql/generated/client/gql.ts @@ -14,14 +14,14 @@ import type { TypedDocumentNode as DocumentNode } from '@graphql-typed-document- * Learn more about it here: https://the-guild.dev/graphql/codegen/plugins/presets/preset-client#reducing-bundle-size */ type Documents = { - "\n mutation sendRemoteGraphQLResponse($input: RemoteGraphQLServerInput!) {\n remoteGraphQLResponse(input: $input)\n }\n": typeof types.sendRemoteGraphQLResponseDocument, - "\n fragment RemoteGraphQLEventFragment on RemoteGraphQLEvent {\n remoteGraphQLEventData: data {\n type\n body\n sha256\n }\n }\n": typeof types.RemoteGraphQLEventFragmentFragmentDoc, - "\n subscription events {\n events {\n __typename\n ... on ClientConnectedEvent {\n connectedData: data {\n type\n version\n apiKey\n }\n connectedEvent: type\n }\n ... on ClientDisconnectedEvent {\n disconnectedData: data {\n type\n version\n apiKey\n }\n disconnectedEvent: type\n }\n ...RemoteGraphQLEventFragment\n }\n }\n": typeof types.eventsDocument, + "\n mutation sendRemoteGraphQLResponse($input: RemoteGraphQLServerInput!) {\n remoteGraphQLResponse(input: $input)\n }\n": typeof types.SendRemoteGraphQlResponseDocument, + "\n fragment RemoteGraphQLEventFragment on RemoteGraphQLEvent {\n remoteGraphQLEventData: data {\n type\n body\n sha256\n }\n }\n": typeof types.RemoteGraphQlEventFragmentFragmentDoc, + "\n subscription events {\n events {\n __typename\n ... on ClientConnectedEvent {\n connectedData: data {\n type\n version\n apiKey\n }\n connectedEvent: type\n }\n ... on ClientDisconnectedEvent {\n disconnectedData: data {\n type\n version\n apiKey\n }\n disconnectedEvent: type\n }\n ...RemoteGraphQLEventFragment\n }\n }\n": typeof types.EventsDocument, }; const documents: Documents = { - "\n mutation sendRemoteGraphQLResponse($input: RemoteGraphQLServerInput!) {\n remoteGraphQLResponse(input: $input)\n }\n": types.sendRemoteGraphQLResponseDocument, - "\n fragment RemoteGraphQLEventFragment on RemoteGraphQLEvent {\n remoteGraphQLEventData: data {\n type\n body\n sha256\n }\n }\n": types.RemoteGraphQLEventFragmentFragmentDoc, - "\n subscription events {\n events {\n __typename\n ... on ClientConnectedEvent {\n connectedData: data {\n type\n version\n apiKey\n }\n connectedEvent: type\n }\n ... on ClientDisconnectedEvent {\n disconnectedData: data {\n type\n version\n apiKey\n }\n disconnectedEvent: type\n }\n ...RemoteGraphQLEventFragment\n }\n }\n": types.eventsDocument, + "\n mutation sendRemoteGraphQLResponse($input: RemoteGraphQLServerInput!) {\n remoteGraphQLResponse(input: $input)\n }\n": types.SendRemoteGraphQlResponseDocument, + "\n fragment RemoteGraphQLEventFragment on RemoteGraphQLEvent {\n remoteGraphQLEventData: data {\n type\n body\n sha256\n }\n }\n": types.RemoteGraphQlEventFragmentFragmentDoc, + "\n subscription events {\n events {\n __typename\n ... on ClientConnectedEvent {\n connectedData: data {\n type\n version\n apiKey\n }\n connectedEvent: type\n }\n ... on ClientDisconnectedEvent {\n disconnectedData: data {\n type\n version\n apiKey\n }\n disconnectedEvent: type\n }\n ...RemoteGraphQLEventFragment\n }\n }\n": types.EventsDocument, }; /** diff --git a/api/src/graphql/generated/client/graphql.ts b/api/src/graphql/generated/client/graphql.ts index c7adc0682..5b8f535bb 100644 --- a/api/src/graphql/generated/client/graphql.ts +++ b/api/src/graphql/generated/client/graphql.ts @@ -35,14 +35,14 @@ export type AccessUrl = { ipv4?: Maybe; ipv6?: Maybe; name?: Maybe; - type: URL_TYPE; + type: UrlType; }; export type AccessUrlInput = { ipv4?: InputMaybe; ipv6?: InputMaybe; name?: InputMaybe; - type: URL_TYPE; + type: UrlType; }; export type ArrayCapacity = { @@ -305,7 +305,7 @@ export type DashboardVmsInput = { started: Scalars['Int']['input']; }; -export type Event = ClientConnectedEvent | ClientDisconnectedEvent | ClientPingEvent | RemoteAccessEvent | RemoteGraphQLEvent | UpdateEvent; +export type Event = ClientConnectedEvent | ClientDisconnectedEvent | ClientPingEvent | RemoteAccessEvent | RemoteGraphQlEvent | UpdateEvent; export enum EventType { CLIENT_CONNECTED_EVENT = 'CLIENT_CONNECTED_EVENT', @@ -373,32 +373,32 @@ export type Mutation = { }; -export type MutationremoteGraphQLResponseArgs = { - input: RemoteGraphQLServerInput; +export type MutationRemoteGraphQlResponseArgs = { + input: RemoteGraphQlServerInput; }; -export type MutationremoteMutationArgs = { - input: RemoteGraphQLClientInput; +export type MutationRemoteMutationArgs = { + input: RemoteGraphQlClientInput; }; -export type MutationremoteSessionArgs = { +export type MutationRemoteSessionArgs = { remoteAccess: RemoteAccessInput; }; -export type MutationsendNotificationArgs = { +export type MutationSendNotificationArgs = { notification: NotificationInput; }; -export type MutationupdateDashboardArgs = { +export type MutationUpdateDashboardArgs = { data: DashboardInput; }; -export type MutationupdateNetworkArgs = { +export type MutationUpdateNetworkArgs = { data: NetworkInput; }; @@ -474,17 +474,17 @@ export type Query = { }; -export type QuerydashboardArgs = { +export type QueryDashboardArgs = { id: Scalars['String']['input']; }; -export type QueryremoteQueryArgs = { - input: RemoteGraphQLClientInput; +export type QueryRemoteQueryArgs = { + input: RemoteGraphQlClientInput; }; -export type QueryserverStatusArgs = { +export type QueryServerStatusArgs = { apiKey: Scalars['String']['input']; }; @@ -557,7 +557,7 @@ export type RemoteAccessInput = { url?: InputMaybe; }; -export type RemoteGraphQLClientInput = { +export type RemoteGraphQlClientInput = { apiKey: Scalars['String']['input']; body: Scalars['String']['input']; /** Time in milliseconds to wait for a response from the remote server (defaults to 15000) */ @@ -566,34 +566,34 @@ export type RemoteGraphQLClientInput = { ttl?: InputMaybe; }; -export type RemoteGraphQLEvent = { +export type RemoteGraphQlEvent = { __typename?: 'RemoteGraphQLEvent'; - data: RemoteGraphQLEventData; + data: RemoteGraphQlEventData; type: EventType; }; -export type RemoteGraphQLEventData = { +export type RemoteGraphQlEventData = { __typename?: 'RemoteGraphQLEventData'; /** Contains mutation / subscription / query data in the form of body: JSON, variables: JSON */ body: Scalars['String']['output']; /** sha256 hash of the body */ sha256: Scalars['String']['output']; - type: RemoteGraphQLEventType; + type: RemoteGraphQlEventType; }; -export enum RemoteGraphQLEventType { +export enum RemoteGraphQlEventType { REMOTE_MUTATION_EVENT = 'REMOTE_MUTATION_EVENT', REMOTE_QUERY_EVENT = 'REMOTE_QUERY_EVENT', REMOTE_SUBSCRIPTION_EVENT = 'REMOTE_SUBSCRIPTION_EVENT', REMOTE_SUBSCRIPTION_EVENT_PING = 'REMOTE_SUBSCRIPTION_EVENT_PING' } -export type RemoteGraphQLServerInput = { +export type RemoteGraphQlServerInput = { /** Body - contains an object containing data: (GQL response data) or errors: (GQL Errors) */ body: Scalars['String']['input']; /** sha256 hash of the body */ sha256: Scalars['String']['input']; - type: RemoteGraphQLEventType; + type: RemoteGraphQlEventType; }; export type Server = { @@ -654,8 +654,8 @@ export type Subscription = { }; -export type SubscriptionremoteSubscriptionArgs = { - input: RemoteGraphQLClientInput; +export type SubscriptionRemoteSubscriptionArgs = { + input: RemoteGraphQlClientInput; }; export type TwoFactorLocal = { @@ -681,7 +681,7 @@ export type TwoFactorWithoutToken = { remote?: Maybe; }; -export enum URL_TYPE { +export enum UrlType { DEFAULT = 'DEFAULT', LAN = 'LAN', MDNS = 'MDNS', @@ -726,23 +726,23 @@ export type Vars = { regTy?: Maybe; }; -export type sendRemoteGraphQLResponseMutationVariables = Exact<{ - input: RemoteGraphQLServerInput; +export type SendRemoteGraphQlResponseMutationVariables = Exact<{ + input: RemoteGraphQlServerInput; }>; -export type sendRemoteGraphQLResponseMutation = { __typename?: 'Mutation', remoteGraphQLResponse: boolean }; +export type SendRemoteGraphQlResponseMutation = { __typename?: 'Mutation', remoteGraphQLResponse: boolean }; -export type RemoteGraphQLEventFragmentFragment = { __typename?: 'RemoteGraphQLEvent', remoteGraphQLEventData: { __typename?: 'RemoteGraphQLEventData', type: RemoteGraphQLEventType, body: string, sha256: string } } & { ' $fragmentName'?: 'RemoteGraphQLEventFragmentFragment' }; +export type RemoteGraphQlEventFragmentFragment = { __typename?: 'RemoteGraphQLEvent', remoteGraphQLEventData: { __typename?: 'RemoteGraphQLEventData', type: RemoteGraphQlEventType, body: string, sha256: string } } & { ' $fragmentName'?: 'RemoteGraphQlEventFragmentFragment' }; -export type eventsSubscriptionVariables = Exact<{ [key: string]: never; }>; +export type EventsSubscriptionVariables = Exact<{ [key: string]: never; }>; -export type eventsSubscription = { __typename?: 'Subscription', events?: Array<{ __typename: 'ClientConnectedEvent', connectedEvent: EventType, connectedData: { __typename?: 'ClientConnectionEventData', type: ClientType, version: string, apiKey: string } } | { __typename: 'ClientDisconnectedEvent', disconnectedEvent: EventType, disconnectedData: { __typename?: 'ClientConnectionEventData', type: ClientType, version: string, apiKey: string } } | { __typename: 'ClientPingEvent' } | { __typename: 'RemoteAccessEvent' } | ( +export type EventsSubscription = { __typename?: 'Subscription', events?: Array<{ __typename: 'ClientConnectedEvent', connectedEvent: EventType, connectedData: { __typename?: 'ClientConnectionEventData', type: ClientType, version: string, apiKey: string } } | { __typename: 'ClientDisconnectedEvent', disconnectedEvent: EventType, disconnectedData: { __typename?: 'ClientConnectionEventData', type: ClientType, version: string, apiKey: string } } | { __typename: 'ClientPingEvent' } | { __typename: 'RemoteAccessEvent' } | ( { __typename: 'RemoteGraphQLEvent' } - & { ' $fragmentRefs'?: { 'RemoteGraphQLEventFragmentFragment': RemoteGraphQLEventFragmentFragment } } + & { ' $fragmentRefs'?: { 'RemoteGraphQlEventFragmentFragment': RemoteGraphQlEventFragmentFragment } } ) | { __typename: 'UpdateEvent' }> | null }; -export const RemoteGraphQLEventFragmentFragmentDoc = {"kind":"Document","definitions":[{"kind":"FragmentDefinition","name":{"kind":"Name","value":"RemoteGraphQLEventFragment"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"RemoteGraphQLEvent"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","alias":{"kind":"Name","value":"remoteGraphQLEventData"},"name":{"kind":"Name","value":"data"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"type"}},{"kind":"Field","name":{"kind":"Name","value":"body"}},{"kind":"Field","name":{"kind":"Name","value":"sha256"}}]}}]}}]} as unknown as DocumentNode; -export const sendRemoteGraphQLResponseDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"sendRemoteGraphQLResponse"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"RemoteGraphQLServerInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"remoteGraphQLResponse"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}]}]}}]} as unknown as DocumentNode; -export const eventsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"subscription","name":{"kind":"Name","value":"events"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"events"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"__typename"}},{"kind":"InlineFragment","typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"ClientConnectedEvent"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","alias":{"kind":"Name","value":"connectedData"},"name":{"kind":"Name","value":"data"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"type"}},{"kind":"Field","name":{"kind":"Name","value":"version"}},{"kind":"Field","name":{"kind":"Name","value":"apiKey"}}]}},{"kind":"Field","alias":{"kind":"Name","value":"connectedEvent"},"name":{"kind":"Name","value":"type"}}]}},{"kind":"InlineFragment","typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"ClientDisconnectedEvent"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","alias":{"kind":"Name","value":"disconnectedData"},"name":{"kind":"Name","value":"data"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"type"}},{"kind":"Field","name":{"kind":"Name","value":"version"}},{"kind":"Field","name":{"kind":"Name","value":"apiKey"}}]}},{"kind":"Field","alias":{"kind":"Name","value":"disconnectedEvent"},"name":{"kind":"Name","value":"type"}}]}},{"kind":"FragmentSpread","name":{"kind":"Name","value":"RemoteGraphQLEventFragment"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"RemoteGraphQLEventFragment"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"RemoteGraphQLEvent"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","alias":{"kind":"Name","value":"remoteGraphQLEventData"},"name":{"kind":"Name","value":"data"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"type"}},{"kind":"Field","name":{"kind":"Name","value":"body"}},{"kind":"Field","name":{"kind":"Name","value":"sha256"}}]}}]}}]} as unknown as DocumentNode; \ No newline at end of file +export const RemoteGraphQlEventFragmentFragmentDoc = {"kind":"Document","definitions":[{"kind":"FragmentDefinition","name":{"kind":"Name","value":"RemoteGraphQLEventFragment"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"RemoteGraphQLEvent"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","alias":{"kind":"Name","value":"remoteGraphQLEventData"},"name":{"kind":"Name","value":"data"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"type"}},{"kind":"Field","name":{"kind":"Name","value":"body"}},{"kind":"Field","name":{"kind":"Name","value":"sha256"}}]}}]}}]} as unknown as DocumentNode; +export const SendRemoteGraphQlResponseDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"sendRemoteGraphQLResponse"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"RemoteGraphQLServerInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"remoteGraphQLResponse"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}]}]}}]} as unknown as DocumentNode; +export const EventsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"subscription","name":{"kind":"Name","value":"events"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"events"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"__typename"}},{"kind":"InlineFragment","typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"ClientConnectedEvent"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","alias":{"kind":"Name","value":"connectedData"},"name":{"kind":"Name","value":"data"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"type"}},{"kind":"Field","name":{"kind":"Name","value":"version"}},{"kind":"Field","name":{"kind":"Name","value":"apiKey"}}]}},{"kind":"Field","alias":{"kind":"Name","value":"connectedEvent"},"name":{"kind":"Name","value":"type"}}]}},{"kind":"InlineFragment","typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"ClientDisconnectedEvent"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","alias":{"kind":"Name","value":"disconnectedData"},"name":{"kind":"Name","value":"data"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"type"}},{"kind":"Field","name":{"kind":"Name","value":"version"}},{"kind":"Field","name":{"kind":"Name","value":"apiKey"}}]}},{"kind":"Field","alias":{"kind":"Name","value":"disconnectedEvent"},"name":{"kind":"Name","value":"type"}}]}},{"kind":"FragmentSpread","name":{"kind":"Name","value":"RemoteGraphQLEventFragment"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"RemoteGraphQLEventFragment"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"RemoteGraphQLEvent"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","alias":{"kind":"Name","value":"remoteGraphQLEventData"},"name":{"kind":"Name","value":"data"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"type"}},{"kind":"Field","name":{"kind":"Name","value":"body"}},{"kind":"Field","name":{"kind":"Name","value":"sha256"}}]}}]}}]} as unknown as DocumentNode; \ No newline at end of file diff --git a/api/src/graphql/generated/client/validators.ts b/api/src/graphql/generated/client/validators.ts index 3be98611a..3bf531585 100644 --- a/api/src/graphql/generated/client/validators.ts +++ b/api/src/graphql/generated/client/validators.ts @@ -1,6 +1,6 @@ /* eslint-disable */ import { z } from 'zod' -import { AccessUrlInput, ArrayCapacityBytesInput, ArrayCapacityInput, ClientType, ConfigErrorState, DashboardAppsInput, DashboardArrayInput, DashboardCaseInput, DashboardConfigInput, DashboardDisplayInput, DashboardInput, DashboardOsInput, DashboardServiceInput, DashboardServiceUptimeInput, DashboardTwoFactorInput, DashboardTwoFactorLocalInput, DashboardTwoFactorRemoteInput, DashboardVarsInput, DashboardVersionsInput, DashboardVmsInput, EventType, Importance, NetworkInput, NotificationInput, NotificationStatus, PingEventSource, RegistrationState, RemoteAccessEventActionType, RemoteAccessInput, RemoteGraphQLClientInput, RemoteGraphQLEventType, RemoteGraphQLServerInput, ServerStatus, URL_TYPE, UpdateType } from '@app/graphql/generated/client/graphql.js' +import { AccessUrlInput, ArrayCapacityBytesInput, ArrayCapacityInput, ClientType, ConfigErrorState, DashboardAppsInput, DashboardArrayInput, DashboardCaseInput, DashboardConfigInput, DashboardDisplayInput, DashboardInput, DashboardOsInput, DashboardServiceInput, DashboardServiceUptimeInput, DashboardTwoFactorInput, DashboardTwoFactorLocalInput, DashboardTwoFactorRemoteInput, DashboardVarsInput, DashboardVersionsInput, DashboardVmsInput, EventType, Importance, NetworkInput, NotificationInput, NotificationStatus, PingEventSource, RegistrationState, RemoteAccessEventActionType, RemoteAccessInput, RemoteGraphQlClientInput, RemoteGraphQlEventType, RemoteGraphQlServerInput, ServerStatus, UrlType, UpdateType } from '@app/graphql/generated/client/graphql.js' type Properties = Required<{ [K in keyof T]: z.ZodType; @@ -28,11 +28,11 @@ export const RegistrationStateSchema = z.nativeEnum(RegistrationState); export const RemoteAccessEventActionTypeSchema = z.nativeEnum(RemoteAccessEventActionType); -export const RemoteGraphQLEventTypeSchema = z.nativeEnum(RemoteGraphQLEventType); +export const RemoteGraphQlEventTypeSchema = z.nativeEnum(RemoteGraphQlEventType); export const ServerStatusSchema = z.nativeEnum(ServerStatus); -export const URL_TYPESchema = z.nativeEnum(URL_TYPE); +export const UrlTypeSchema = z.nativeEnum(UrlType); export const UpdateTypeSchema = z.nativeEnum(UpdateType); @@ -41,7 +41,7 @@ export function AccessUrlInputSchema(): z.ZodObject> ipv4: z.instanceof(URL).nullish(), ipv6: z.instanceof(URL).nullish(), name: z.string().nullish(), - type: URL_TYPESchema + type: UrlTypeSchema }) } @@ -198,7 +198,7 @@ export function RemoteAccessInputSchema(): z.ZodObject> { +export function RemoteGraphQlClientInputSchema(): z.ZodObject> { return z.object({ apiKey: z.string(), body: z.string(), @@ -207,10 +207,10 @@ export function RemoteGraphQLClientInputSchema(): z.ZodObject> { +export function RemoteGraphQlServerInputSchema(): z.ZodObject> { return z.object({ body: z.string(), sha256: z.string(), - type: RemoteGraphQLEventTypeSchema + type: RemoteGraphQlEventTypeSchema }) } diff --git a/api/src/graphql/resolvers/mutation/connect/connect-sign-in.ts b/api/src/graphql/resolvers/mutation/connect/connect-sign-in.ts deleted file mode 100644 index 2c06d76f8..000000000 --- a/api/src/graphql/resolvers/mutation/connect/connect-sign-in.ts +++ /dev/null @@ -1,58 +0,0 @@ -import { decodeJwt } from 'jose'; - -import type { ConnectSignInInput } from '@app/graphql/generated/api/types.js'; -import { getters, store } from '@app/store/index.js'; -import { loginUser } from '@app/store/modules/config.js'; -import { FileLoadStatus } from '@app/store/types.js'; -import { ApiKeyService } from '@app/unraid-api/auth/api-key.service.js'; - -export const connectSignIn = async (input: ConnectSignInInput): Promise => { - if (getters.emhttp().status === FileLoadStatus.LOADED) { - const userInfo = input.idToken ? decodeJwt(input.idToken) : (input.userInfo ?? null); - - if ( - !userInfo || - !userInfo.preferred_username || - !userInfo.email || - typeof userInfo.preferred_username !== 'string' || - typeof userInfo.email !== 'string' - ) { - throw new Error('Missing User Attributes'); - } - - try { - const { remote } = getters.config(); - const { localApiKey: localApiKeyFromConfig } = remote; - - let localApiKeyToUse = localApiKeyFromConfig; - - if (localApiKeyFromConfig == '') { - const apiKeyService = new ApiKeyService(); - // Create local API key - const localApiKey = await apiKeyService.createLocalConnectApiKey(); - - if (!localApiKey?.key) { - throw new Error('Failed to create local API key'); - } - - localApiKeyToUse = localApiKey.key; - } - - await store.dispatch( - loginUser({ - avatar: typeof userInfo.avatar === 'string' ? userInfo.avatar : '', - username: userInfo.preferred_username, - email: userInfo.email, - apikey: input.apiKey, - localApiKey: localApiKeyToUse, - }) - ); - - return true; - } catch (error) { - throw new Error(`Failed to login user: ${error}`); - } - } else { - return false; - } -}; diff --git a/api/src/graphql/resolvers/query/cloud/check-api.ts b/api/src/graphql/resolvers/query/cloud/check-api.ts index 05633a42b..e653d0a9a 100644 --- a/api/src/graphql/resolvers/query/cloud/check-api.ts +++ b/api/src/graphql/resolvers/query/cloud/check-api.ts @@ -1,5 +1,5 @@ import { logger } from '@app/core/log.js'; -import { type ApiKeyResponse } from '@app/graphql/generated/api/types.js'; +import { type ApiKeyResponse } from '@app/unraid-api/graph/resolvers/cloud/cloud.model.js'; export const checkApi = async (): Promise => { logger.trace('Cloud endpoint: Checking API'); diff --git a/api/src/graphql/resolvers/query/cloud/check-cloud.ts b/api/src/graphql/resolvers/query/cloud/check-cloud.ts index 9c6aa4b3d..04c2404d2 100644 --- a/api/src/graphql/resolvers/query/cloud/check-cloud.ts +++ b/api/src/graphql/resolvers/query/cloud/check-cloud.ts @@ -1,15 +1,14 @@ import { got } from 'got'; -import type { CloudResponse } from '@app/graphql/generated/api/types.js'; import { FIVE_DAYS_SECS, ONE_DAY_SECS } from '@app/consts.js'; import { logger } from '@app/core/log.js'; import { API_VERSION, MOTHERSHIP_GRAPHQL_LINK } from '@app/environment.js'; -import { MinigraphStatus } from '@app/graphql/generated/api/types.js'; import { checkDNS } from '@app/graphql/resolvers/query/cloud/check-dns.js'; import { checkMothershipAuthentication } from '@app/graphql/resolvers/query/cloud/check-mothership-authentication.js'; import { getCloudCache, getDnsCache } from '@app/store/getters/index.js'; import { getters, store } from '@app/store/index.js'; import { setCloudCheck, setDNSCheck } from '@app/store/modules/cache.js'; +import { CloudResponse, MinigraphStatus } from '@app/unraid-api/graph/resolvers/cloud/cloud.model.js'; const mothershipBaseUrl = new URL(MOTHERSHIP_GRAPHQL_LINK).origin; diff --git a/api/src/graphql/resolvers/query/cloud/check-minigraphql.ts b/api/src/graphql/resolvers/query/cloud/check-minigraphql.ts index 890514249..b72c3c89b 100644 --- a/api/src/graphql/resolvers/query/cloud/check-minigraphql.ts +++ b/api/src/graphql/resolvers/query/cloud/check-minigraphql.ts @@ -1,6 +1,6 @@ import { logger } from '@app/core/log.js'; -import { type MinigraphqlResponse } from '@app/graphql/generated/api/types.js'; import { getters } from '@app/store/index.js'; +import { MinigraphqlResponse } from '@app/unraid-api/graph/resolvers/cloud/cloud.model.js'; export const checkMinigraphql = (): MinigraphqlResponse => { logger.trace('Cloud endpoint: Checking mini-graphql'); diff --git a/api/src/graphql/resolvers/query/info.ts b/api/src/graphql/resolvers/query/info.ts index 412077463..725228946 100644 --- a/api/src/graphql/resolvers/query/info.ts +++ b/api/src/graphql/resolvers/query/info.ts @@ -19,20 +19,20 @@ import { sanitizeVendor } from '@app/core/utils/vms/domain/sanitize-vendor.js'; import { vmRegExps } from '@app/core/utils/vms/domain/vm-regexps.js'; import { filterDevices } from '@app/core/utils/vms/filter-devices.js'; import { getPciDevices } from '@app/core/utils/vms/get-pci-devices.js'; -import { - type Devices, - type Display, - type Gpu, - type InfoApps, - type InfoCpu, - type InfoMemory, - type Os as InfoOs, - type MemoryLayout, - type Temperature, - type Theme, - type Versions, -} from '@app/graphql/generated/api/types.js'; import { getters } from '@app/store/index.js'; +import { + Devices, + Display, + Gpu, + InfoApps, + InfoCpu, + InfoMemory, + Os as InfoOs, + MemoryLayout, + Temperature, + Theme, + Versions, +} from '@app/unraid-api/graph/resolvers/info/info.model.js'; export const generateApps = async (): Promise => { const installed = await docker @@ -43,13 +43,14 @@ export const generateApps = async (): Promise => { .listContainers() .catch(() => []) .then((containers) => containers.length); - return { installed, started }; + return { id: 'info/apps', installed, started }; }; export const generateOs = async (): Promise => { const os = await osInfo(); return { + id: 'info/os', ...os, hostname: getters.emhttp().var.name, uptime: bootTimestamp.toISOString(), @@ -63,6 +64,7 @@ export const generateCpu = async (): Promise => { .catch(() => []); return { + id: 'info/cpu', ...rest, cores: physicalCores, threads: cores, @@ -94,8 +96,8 @@ export const generateDisplay = async (): Promise => { } const { theme, unit, ...display } = state.display; return { - ...display, id: 'dynamix-config/display', + ...display, theme: theme as Theme, unit: unit as Temperature, scale: toBoolean(display.scale), @@ -118,6 +120,7 @@ export const generateVersions = async (): Promise => { const softwareVersions = await versions(); return { + id: 'info/versions', unraid, ...softwareVersions, }; @@ -165,6 +168,7 @@ export const generateMemory = async (): Promise => { } return { + id: 'info/memory', layout, max, ...info, @@ -410,10 +414,9 @@ export const generateDevices = async (): Promise => { }; return { + id: 'info/devices', // Scsi: await scsiDevices, gpu: await systemGPUDevices, - // Move this to interfaces - // network: await si.networkInterfaces(), pci: await systemPciDevices(), usb: await getSystemUSBDevices(), }; diff --git a/api/src/graphql/resolvers/subscription/network.ts b/api/src/graphql/resolvers/subscription/network.ts index bb8623d19..ac518fad8 100644 --- a/api/src/graphql/resolvers/subscription/network.ts +++ b/api/src/graphql/resolvers/subscription/network.ts @@ -1,11 +1,8 @@ -import type { AccessUrlInput } from '@app/graphql/generated/client/graphql.js'; import type { RootState } from '@app/store/index.js'; import { logger } from '@app/core/log.js'; import { type Nginx } from '@app/core/types/states/nginx.js'; -import { type AccessUrl } from '@app/graphql/generated/api/types.js'; -import { URL_TYPE } from '@app/graphql/generated/client/graphql.js'; -import { AccessUrlInputSchema } from '@app/graphql/generated/client/validators.js'; import { store } from '@app/store/index.js'; +import { AccessUrl, URL_TYPE } from '@app/unraid-api/graph/resolvers/connect/connect.model.js'; interface UrlForFieldInput { url: string; @@ -126,7 +123,7 @@ export const getServerIps = ( } const errors: Error[] = []; - const urls: AccessUrlInput[] = []; + const urls: AccessUrl[] = []; try { // Default URL @@ -232,16 +229,5 @@ export const getServerIps = ( } }); - const safeUrls = urls - .map((url) => AccessUrlInputSchema().safeParse(url)) - .reduce((acc, curr) => { - if (curr.success) { - acc.push(curr.data); - } else { - errors.push(curr.error); - } - return acc; - }, []); - - return { urls: safeUrls, errors }; + return { urls, errors }; }; diff --git a/api/src/graphql/resolvers/subscription/remote-graphql/remote-query.ts b/api/src/graphql/resolvers/subscription/remote-graphql/remote-query.ts index 8e72f17de..ee27890e0 100644 --- a/api/src/graphql/resolvers/subscription/remote-graphql/remote-query.ts +++ b/api/src/graphql/resolvers/subscription/remote-graphql/remote-query.ts @@ -1,14 +1,14 @@ -import type { RemoteGraphQLEventFragmentFragment } from '@app/graphql/generated/client/graphql.js'; +import type { RemoteGraphQlEventFragmentFragment } from '@app/graphql/generated/client/graphql.js'; import { remoteQueryLogger } from '@app/core/log.js'; import { getApiApolloClient } from '@app/graphql/client/api/get-api-client.js'; -import { RemoteGraphQLEventType } from '@app/graphql/generated/client/graphql.js'; +import { RemoteGraphQlEventType } from '@app/graphql/generated/client/graphql.js'; import { SEND_REMOTE_QUERY_RESPONSE } from '@app/graphql/mothership/mutations.js'; import { parseGraphQLQuery } from '@app/graphql/resolvers/subscription/remote-graphql/remote-graphql-helpers.js'; import { GraphQLClient } from '@app/mothership/graphql-client.js'; import { getters } from '@app/store/index.js'; export const executeRemoteGraphQLQuery = async ( - data: RemoteGraphQLEventFragmentFragment['remoteGraphQLEventData'] + data: RemoteGraphQlEventFragmentFragment['remoteGraphQLEventData'] ) => { remoteQueryLogger.debug({ query: data }, 'Executing remote query'); const client = GraphQLClient.getInstance(); @@ -44,7 +44,7 @@ export const executeRemoteGraphQLQuery = async ( input: { sha256: data.sha256, body: JSON.stringify({ data: localResult.data }), - type: RemoteGraphQLEventType.REMOTE_QUERY_EVENT, + type: RemoteGraphQlEventType.REMOTE_QUERY_EVENT, }, }, errorPolicy: 'none', @@ -57,7 +57,7 @@ export const executeRemoteGraphQLQuery = async ( input: { sha256: data.sha256, body: JSON.stringify({ errors: localResult.error }), - type: RemoteGraphQLEventType.REMOTE_QUERY_EVENT, + type: RemoteGraphQlEventType.REMOTE_QUERY_EVENT, }, }, }); @@ -70,7 +70,7 @@ export const executeRemoteGraphQLQuery = async ( input: { sha256: data.sha256, body: JSON.stringify({ errors: err }), - type: RemoteGraphQLEventType.REMOTE_QUERY_EVENT, + type: RemoteGraphQlEventType.REMOTE_QUERY_EVENT, }, }, }); diff --git a/api/src/graphql/resolvers/subscription/remote-graphql/remote-subscription.ts b/api/src/graphql/resolvers/subscription/remote-graphql/remote-subscription.ts index 9fd6a2c44..44055ed71 100644 --- a/api/src/graphql/resolvers/subscription/remote-graphql/remote-subscription.ts +++ b/api/src/graphql/resolvers/subscription/remote-graphql/remote-subscription.ts @@ -1,9 +1,9 @@ -import { type RemoteGraphQLEventFragmentFragment } from '@app/graphql/generated/client/graphql.js'; +import { type RemoteGraphQlEventFragmentFragment } from '@app/graphql/generated/client/graphql.js'; import { addRemoteSubscription } from '@app/store/actions/add-remote-subscription.js'; import { store } from '@app/store/index.js'; export const createRemoteSubscription = async ( - data: RemoteGraphQLEventFragmentFragment['remoteGraphQLEventData'] + data: RemoteGraphQlEventFragmentFragment['remoteGraphQLEventData'] ) => { await store.dispatch(addRemoteSubscription(data)); }; diff --git a/api/src/graphql/schema/types/api-key/api-key.graphql b/api/src/graphql/schema/types/api-key/api-key.graphql deleted file mode 100644 index b35f8c6b1..000000000 --- a/api/src/graphql/schema/types/api-key/api-key.graphql +++ /dev/null @@ -1,65 +0,0 @@ -type Permission { - resource: Resource! - actions: [String!]! -} - -type ApiKey { - id: ID! - name: String! - description: String - roles: [Role!]! - createdAt: DateTime! - permissions: [Permission!]! -} - -type ApiKeyWithSecret { - id: ID! - key: String! - name: String! - description: String - roles: [Role!]! - createdAt: DateTime! - permissions: [Permission!]! -} - -input CreateApiKeyInput { - name: String! - description: String - roles: [Role!] - permissions: [AddPermissionInput!] - """ This will replace the existing key if one already exists with the same name, otherwise returns the existing key """ - overwrite: Boolean -} - -input AddPermissionInput { - resource: Resource! - actions: [String!]! -} - -input AddRoleForUserInput { - userId: ID! - role: Role! -} - -input AddRoleForApiKeyInput { - apiKeyId: ID! - role: Role! -} - -input RemoveRoleFromApiKeyInput { - apiKeyId: ID! - role: Role! -} - -type Mutation { - createApiKey(input: CreateApiKeyInput!): ApiKeyWithSecret! - addPermission(input: AddPermissionInput!): Boolean! - addRoleForUser(input: AddRoleForUserInput!): Boolean! - addRoleForApiKey(input: AddRoleForApiKeyInput!): Boolean! - removeRoleFromApiKey(input: RemoveRoleFromApiKeyInput!): Boolean! -} - -type Query { - apiKeys: [ApiKey!]! - apiKey(id: ID!): ApiKey -} diff --git a/api/src/graphql/schema/types/api-key/roles.graphql b/api/src/graphql/schema/types/api-key/roles.graphql deleted file mode 100644 index 3177d7788..000000000 --- a/api/src/graphql/schema/types/api-key/roles.graphql +++ /dev/null @@ -1,42 +0,0 @@ -""" -Available resources for permissions -""" -enum Resource { - API_KEY - ARRAY - CLOUD - CONFIG - CONNECT - CONNECT__REMOTE_ACCESS - CUSTOMIZATIONS - DASHBOARD - DISK - DISPLAY - DOCKER - FLASH - INFO - LOGS - ME - NETWORK - NOTIFICATIONS - ONLINE - OS - OWNER - PERMISSION - REGISTRATION - SERVERS - SERVICES - SHARE - VARS - VMS - WELCOME -} - -""" -Available roles for API keys and users -""" -enum Role { - ADMIN - CONNECT - GUEST -} diff --git a/api/src/graphql/schema/types/array/array.graphql b/api/src/graphql/schema/types/array/array.graphql deleted file mode 100644 index 517be6a2f..000000000 --- a/api/src/graphql/schema/types/array/array.graphql +++ /dev/null @@ -1,214 +0,0 @@ -type Query { - """An Unraid array consisting of 1 or 2 Parity disks and a number of Data disks.""" - array: Array! -} - -enum ArrayStateInputState { - """Start array""" - START - """Stop array""" - STOP -} - -input ArrayStateInput { - """Array state""" - desiredState: ArrayStateInputState! -} - -type ArrayMutations { - """Set array state""" - setState(input: ArrayStateInput): Array - - """Add new disk to array""" - addDiskToArray(input: ArrayDiskInput): Array - """Remove existing disk from array. NOTE: The array must be stopped before running this otherwise it'll throw an error.""" - removeDiskFromArray(input: ArrayDiskInput): Array - - mountArrayDisk(id: ID!): Disk - unmountArrayDisk(id: ID!): Disk - - clearArrayDiskStatistics(id: ID!): JSON -} - -type Mutation { - array: ArrayMutations -} - -type Subscription { - array: Array! -} - -input ArrayDiskInput { - """Disk ID""" - id: ID! - """The slot for the disk""" - slot: Int -} - -type Array implements Node { - id: ID! - """Array state before this query/mutation""" - previousState: ArrayState - """Array state after this query/mutation""" - pendingState: ArrayPendingState - """Current array state""" - state: ArrayState! - """Current array capacity""" - capacity: ArrayCapacity! - """Current boot disk""" - boot: ArrayDisk - """Parity disks in the current array""" - parities: [ArrayDisk!]! - """Data disks in the current array""" - disks: [ArrayDisk!]! - """Caches in the current array""" - caches: [ArrayDisk!]! -} - -# /usr/src/linux-5.9.13-Unraid/drivers/md/md_unraid.c -enum ArrayState { - """Array is running""" - STARTED - """Array has stopped""" - STOPPED - """Array has new disks""" - NEW_ARRAY - """A disk is being reconstructed""" - RECON_DISK - """A disk is disabled in the array""" - DISABLE_DISK - """Array is disabled""" - SWAP_DSBL - """Too many changes to array at the same time""" - INVALID_EXPANSION - """Parity isn't the biggest, can't start array""" - PARITY_NOT_BIGGEST - """Array has too many missing data disks""" - TOO_MANY_MISSING_DISKS - """Array has new disks they're too small""" - NEW_DISK_TOO_SMALL - """Array has no data disks""" - NO_DATA_DISKS -} - -enum ArrayDiskStatus { - """ no disk present, no disk configured """ - DISK_NP - """ enabled, disk present, correct, valid """ - DISK_OK - """ enabled, but missing """ - DISK_NP_MISSING - """ enabled, disk present, but not valid """ - DISK_INVALID - """ enablled, disk present, but not correct disk """ - DISK_WRONG - """ disabled, old disk still present """ - DISK_DSBL - """ disabled, no disk present """ - DISK_NP_DSBL - """ disabled, new disk present """ - DISK_DSBL_NEW - """ new disk """ - DISK_NEW -} - -enum ArrayPendingState { - """Array is starting""" - starting - """Array is stopping""" - stopping - """Array has no data disks""" - no_data_disks - """Array has too many missing data disks""" - too_many_missing_disks -} - -type ArrayCapacity { - kilobytes: Capacity! - disks: Capacity! -} - -type Capacity { - free: String! - used: String! - total: String! -} - -type ArrayDisk { - """ Disk indentifier, only set for present disks on the system """ - id: ID! - """ Array slot number. Parity1 is always 0 and Parity2 is always 29. Array slots will be 1 - 28. Cache slots are 30 - 53. Flash is 54. """ - idx: Int! - name: String - device: String - """ (KB) Disk Size total """ - size: Long! - status: ArrayDiskStatus - """ Is the disk a HDD or SSD. """ - rotational: Boolean - """ Disk temp - will be NaN if array is not started or DISK_NP """ - temp: Int - """Count of I/O read requests sent to the device I/O drivers. These statistics may be cleared at any time.""" - numReads: Long! - """Count of I/O writes requests sent to the device I/O drivers. These statistics may be cleared at any time.""" - numWrites: Long! - """Number of unrecoverable errors reported by the device I/O drivers. Missing data due to unrecoverable array read errors is filled in on-the-fly using parity reconstruct (and we attempt to write this data back to the sector(s) which failed). Any unrecoverable write error results in disabling the disk.""" - numErrors: Long! - """ (KB) Total Size of the FS (Not present on Parity type drive) """ - fsSize: Long - """ (KB) Free Size on the FS (Not present on Parity type drive)""" - fsFree: Long - """ (KB) Used Size on the FS (Not present on Parity type drive)""" - fsUsed: Long - exportable: Boolean - """ Type of Disk - used to differentiate Cache / Flash / Array / Parity """ - type: ArrayDiskType! - """ (%) Disk space left to warn """ - warning: Int - """ (%) Disk space left for critical """ - critical: Int - """ File system type for the disk """ - fsType: String - """ User comment on disk """ - comment: String - """ File format (ex MBR: 4KiB-aligned) """ - format: String - """ ata | nvme | usb | (others)""" - transport: String - color: ArrayDiskFsColor -} - -# type ArrayParityDisk {} -# type ArrayCacheDisk {} - -enum ArrayDiskType { - """Data disk""" - Data - """Parity disk""" - Parity - """Flash disk""" - Flash - """Cache disk""" - Cache -} - -enum ArrayDiskFsColor { - """Normal operation, device is active""" - green_on - """Device is in standby mode (spun-down)""" - green_blink - """New device""" - blue_on - """New device, in standby mode (spun-down)""" - blue_blink - """Device contents invalid or emulated / Parity is invalid""" - yellow_on - """Device contents invalid or emulated / Parity is invalid, in standby mode (spun-down)""" - yellow_blink - """Device is disabled or contents emulated / Parity device is disabled""" - red_on - """Device is missing (disabled) or contents emulated / Parity device is missing""" - red_off - """Device not present""" - grey_off -} \ No newline at end of file diff --git a/api/src/graphql/schema/types/array/parity.graphql b/api/src/graphql/schema/types/array/parity.graphql deleted file mode 100644 index 4430c2a39..000000000 --- a/api/src/graphql/schema/types/array/parity.graphql +++ /dev/null @@ -1,26 +0,0 @@ -type Query { - parityHistory: [ParityCheck] -} - -type Mutation { - """Start parity check""" - startParityCheck(correct: Boolean): JSON - """Pause parity check""" - pauseParityCheck: JSON - """Resume parity check""" - resumeParityCheck: JSON - """Cancel parity check""" - cancelParityCheck: JSON -} - -type Subscription { - parityHistory: ParityCheck! -} - -type ParityCheck { - date: String! - duration: Int! - speed: String! - status: String! - errors: String! -} \ No newline at end of file diff --git a/api/src/graphql/schema/types/base.graphql b/api/src/graphql/schema/types/base.graphql deleted file mode 100644 index 5410016c5..000000000 --- a/api/src/graphql/schema/types/base.graphql +++ /dev/null @@ -1,35 +0,0 @@ -scalar JSON -scalar Long -scalar UUID -scalar DateTime -scalar Port -scalar URL - -directive @auth(action: AuthActionVerb!, resource: Resource!, possession: AuthPossession!) on FIELD_DEFINITION - -type Welcome { - message: String! -} - -type Query { - # This should always be available even for guest users - online: Boolean - info: Info -} - -type Mutation { - login(username: String!, password: String!): String - shutdown: String - reboot: String -} - -type Subscription { - ping: String! - info: Info! - online: Boolean! -} - -# An object with a Globally Unique ID: see https://graphql.org/learn/global-object-identification/ -interface Node { - id: ID! -} \ No newline at end of file diff --git a/api/src/graphql/schema/types/cloud/cloud.graphql b/api/src/graphql/schema/types/cloud/cloud.graphql deleted file mode 100644 index 261fbeae7..000000000 --- a/api/src/graphql/schema/types/cloud/cloud.graphql +++ /dev/null @@ -1,43 +0,0 @@ -type ApiKeyResponse { - valid: Boolean! - error: String -} - -enum MinigraphStatus { - PRE_INIT - CONNECTING - CONNECTED - PING_FAILURE - ERROR_RETRYING -} - -type MinigraphqlResponse { - status: MinigraphStatus! - timeout: Int - error: String -} - -type CloudResponse { - status: String! - ip: String - error: String -} - -type RelayResponse { - status: String! - timeout: String - error: String - } - -type Cloud { - error: String - apiKey: ApiKeyResponse! - relay: RelayResponse - minigraphql: MinigraphqlResponse! - cloud: CloudResponse! - allowedOrigins: [String!]! -} - -type Query { - cloud: Cloud -} diff --git a/api/src/graphql/schema/types/config/config.graphql b/api/src/graphql/schema/types/config/config.graphql deleted file mode 100644 index e5969dcec..000000000 --- a/api/src/graphql/schema/types/config/config.graphql +++ /dev/null @@ -1,13 +0,0 @@ -type Config implements Node { - id: ID! - valid: Boolean - error: ConfigErrorState -} - -type Query { - config: Config! -} - -type Subscription { - config: Config! -} diff --git a/api/src/graphql/schema/types/connect/connect.graphql b/api/src/graphql/schema/types/connect/connect.graphql deleted file mode 100644 index 1bba869d3..000000000 --- a/api/src/graphql/schema/types/connect/connect.graphql +++ /dev/null @@ -1,153 +0,0 @@ -input ConnectUserInfoInput { - preferred_username: String! - email: String! - avatar: String -} - -input ConnectSignInInput { - apiKey: String! - idToken: String - userInfo: ConnectUserInfoInput - accessToken: String - refreshToken: String -} - -input AllowedOriginInput { - origins: [String!]! -} - -enum WAN_ACCESS_TYPE { - DYNAMIC - ALWAYS - DISABLED -} - -enum WAN_FORWARD_TYPE { - UPNP - STATIC -} - -type RemoteAccess { - accessType: WAN_ACCESS_TYPE! - forwardType: WAN_FORWARD_TYPE - port: Port -} - -input SetupRemoteAccessInput { - accessType: WAN_ACCESS_TYPE! - forwardType: WAN_FORWARD_TYPE - port: Port -} - -input EnableDynamicRemoteAccessInput { - url: AccessUrlInput! - enabled: Boolean! -} - -enum DynamicRemoteAccessType { - STATIC - UPNP - DISABLED -} - -type DynamicRemoteAccessStatus { - enabledType: DynamicRemoteAccessType! - runningType: DynamicRemoteAccessType! - error: String -} - -""" -Intersection type of ApiSettings and RemoteAccess -""" -type ConnectSettingsValues { - """ - If true, the GraphQL sandbox is enabled and available at /graphql. - If false, the GraphQL sandbox is disabled and only the production API will be available. - """ - sandbox: Boolean! - """ - A list of origins allowed to interact with the API. - """ - extraOrigins: [String!]! - """ - The type of WAN access used for Remote Access. - """ - accessType: WAN_ACCESS_TYPE! - """ - The type of port forwarding used for Remote Access. - """ - forwardType: WAN_FORWARD_TYPE - """ - The port used for Remote Access. - """ - port: Port - """ - A list of Unique Unraid Account ID's. - """ - ssoUserIds: [String!]! -} - -""" -Input should be a subset of ApiSettings that can be updated. -Some field combinations may be required or disallowed. Please refer to each field for more information. -""" -input ApiSettingsInput { - """ - If true, the GraphQL sandbox will be enabled and available at /graphql. - If false, the GraphQL sandbox will be disabled and only the production API will be available. - """ - sandbox: Boolean - """ - A list of origins allowed to interact with the API. - """ - extraOrigins: [String!] - """ - The type of WAN access to use for Remote Access. - """ - accessType: WAN_ACCESS_TYPE - """ - The type of port forwarding to use for Remote Access. - """ - forwardType: WAN_FORWARD_TYPE - """ - The port to use for Remote Access. Not required for UPNP forwardType. Required for STATIC forwardType. - Ignored if accessType is DISABLED or forwardType is UPNP. - """ - port: Port - """ - A list of Unique Unraid Account ID's. - """ - ssoUserIds: [String!] -} - -type ConnectSettings implements Node { - id: ID! - dataSchema: JSON! - uiSchema: JSON! - values: ConnectSettingsValues! -} - -type Connect implements Node { - id: ID! - dynamicRemoteAccess: DynamicRemoteAccessStatus! - settings: ConnectSettings! -} - -type Query { - remoteAccess: RemoteAccess! - extraAllowedOrigins: [String!]! - connect: Connect! -} - -type Mutation { - connectSignIn(input: ConnectSignInInput!): Boolean! - connectSignOut: Boolean! - enableDynamicRemoteAccess(input: EnableDynamicRemoteAccessInput!): Boolean! - setAdditionalAllowedOrigins(input: AllowedOriginInput!): [String!]! - setupRemoteAccess(input: SetupRemoteAccessInput!): Boolean! - """ - Update the API settings. - Some setting combinations may be required or disallowed. Please refer to each setting for more information. - """ - updateApiSettings(input: ApiSettingsInput!): ConnectSettingsValues! -} diff --git a/api/src/graphql/schema/types/disks/disk.graphql b/api/src/graphql/schema/types/disks/disk.graphql deleted file mode 100644 index 2695cd0d0..000000000 --- a/api/src/graphql/schema/types/disks/disk.graphql +++ /dev/null @@ -1,70 +0,0 @@ -type Query { - """Single disk""" - disk(id: ID!): Disk - """Mulitiple disks""" - disks: [Disk]! -} - -type Disk { - id: ID! - # /dev/sdb - device: String! - # SSD - type: String! - # Samsung_SSD_860_QVO_1TB - name: String! - # Samsung - vendor: String! - # 1000204886016 - size: Long! - # -1 - bytesPerSector: Long! - # -1 - totalCylinders: Long! - # -1 - totalHeads: Long! - # -1 - totalSectors: Long! - # -1 - totalTracks: Long! - # -1 - tracksPerCylinder: Long! - # -1 - sectorsPerTrack: Long! - # 1B6Q - firmwareRevision: String! - # S4CZNF0M807232N - serialNum: String! - interfaceType: DiskInterfaceType! - smartStatus: DiskSmartStatus! - temperature: Long - partitions: [DiskPartition!] -} - -type DiskPartition { - name: String! - fsType: DiskFsType! - size: Long! -} - -enum DiskFsType { - xfs - btrfs - vfat - zfs - ext4 - ntfs -} - -enum DiskInterfaceType { - SAS - SATA - USB - PCIe - UNKNOWN -} - -enum DiskSmartStatus { - OK - UNKNOWN -} \ No newline at end of file diff --git a/api/src/graphql/schema/types/display/display.graphql b/api/src/graphql/schema/types/display/display.graphql deleted file mode 100644 index 83fd61389..000000000 --- a/api/src/graphql/schema/types/display/display.graphql +++ /dev/null @@ -1,19 +0,0 @@ -type Query { - display: Display -} - -type Subscription { - display: Display -} - -type Display { - id: ID! - case: Case -} - -type Case { - icon: String - url: String - error: String - base64: String -} \ No newline at end of file diff --git a/api/src/graphql/schema/types/docker/container.graphql b/api/src/graphql/schema/types/docker/container.graphql deleted file mode 100644 index 0c88b770a..000000000 --- a/api/src/graphql/schema/types/docker/container.graphql +++ /dev/null @@ -1,51 +0,0 @@ - -enum ContainerPortType { - TCP - UDP -} - -type ContainerPort { - ip: String - privatePort: Int - publicPort: Int - type: ContainerPortType -} - -enum ContainerState { - RUNNING - EXITED -} - -type ContainerHostConfig { - networkMode: String! -} - -type ContainerMount { - type: String! - name: String! - source: String! - destination: String! - driver: String! - mode: String! - rw: Boolean! - propagation: String! -} - -type DockerContainer { - id: ID! - names: [String!] - image: String! - imageId: String! - command: String! - created: Int! - ports: [ContainerPort!]! - """ (B) Total size of all the files in the container """ - sizeRootFs: Long - labels: JSON - state: ContainerState! - status: String! - hostConfig: ContainerHostConfig - networkSettings: JSON - mounts: [JSON] - autoStart: Boolean! -} \ No newline at end of file diff --git a/api/src/graphql/schema/types/docker/docker.graphql b/api/src/graphql/schema/types/docker/docker.graphql deleted file mode 100644 index 3dd1153bd..000000000 --- a/api/src/graphql/schema/types/docker/docker.graphql +++ /dev/null @@ -1,20 +0,0 @@ -type Docker implements Node { - id: ID! - containers: [DockerContainer!] - networks: [DockerNetwork!] -} - -type Query { - docker: Docker! -} - -type DockerMutations { - """ Stop a container """ - stop(id: ID!): DockerContainer! - """ Start a container """ - start(id: ID!): DockerContainer! -} - -type Mutation { - docker: DockerMutations -} diff --git a/api/src/graphql/schema/types/docker/network.graphql b/api/src/graphql/schema/types/docker/network.graphql deleted file mode 100644 index 5bb566cfd..000000000 --- a/api/src/graphql/schema/types/docker/network.graphql +++ /dev/null @@ -1,29 +0,0 @@ -type Query { - """Docker network""" - dockerNetwork(id: ID!): DockerNetwork! - """All Docker networks""" - dockerNetworks(all: Boolean): [DockerNetwork]! -} - -type Subscription { - dockerNetwork(id: ID!): DockerNetwork! - dockerNetworks: [DockerNetwork]! -} - -type DockerNetwork { - name: String - id: ID - created: String - scope: String - driver: String - enableIPv6: Boolean! - ipam: JSON - internal: Boolean! - attachable: Boolean! - ingress: Boolean! - configFrom: JSON - configOnly: Boolean! - containers: JSON - options: JSON - labels: JSON -} \ No newline at end of file diff --git a/api/src/graphql/schema/types/flash/flash.graphql b/api/src/graphql/schema/types/flash/flash.graphql deleted file mode 100644 index 674c7cc56..000000000 --- a/api/src/graphql/schema/types/flash/flash.graphql +++ /dev/null @@ -1,13 +0,0 @@ -type Query { - flash: Flash -} - -type Subscription { - flash: Flash! -} - -type Flash { - guid: String - vendor: String - product: String -} \ No newline at end of file diff --git a/api/src/graphql/schema/types/info/apps.graphql b/api/src/graphql/schema/types/info/apps.graphql deleted file mode 100644 index 6dd6675a5..000000000 --- a/api/src/graphql/schema/types/info/apps.graphql +++ /dev/null @@ -1,11 +0,0 @@ -type Info { - """Count of docker containers""" - apps: InfoApps -} - -type InfoApps { - """How many docker containers are installed""" - installed: Int - """How many docker containers are running""" - started: Int -} \ No newline at end of file diff --git a/api/src/graphql/schema/types/info/baseboard.graphql b/api/src/graphql/schema/types/info/baseboard.graphql deleted file mode 100644 index 79bc8fbec..000000000 --- a/api/src/graphql/schema/types/info/baseboard.graphql +++ /dev/null @@ -1,14 +0,0 @@ -type Info { - baseboard: Baseboard -} - -type Baseboard { - # Dell Inc. - manufacturer: String! - # 0MD99X - model: String - # A07 - version: String - serial: String - assetTag: String -} \ No newline at end of file diff --git a/api/src/graphql/schema/types/info/cpu.graphql b/api/src/graphql/schema/types/info/cpu.graphql deleted file mode 100644 index 71b97543b..000000000 --- a/api/src/graphql/schema/types/info/cpu.graphql +++ /dev/null @@ -1,39 +0,0 @@ -type Info { - cpu: InfoCpu -} - -type InfoCpu { - # 'Intel®' - manufacturer: String! - # 'Xeon® L5640' - brand: String! - # 'GenuineIntel' - vendor: String! - # '6' - family: String! - # '44' - model: String! - # '2' - stepping: Int! - # '' - revision: String! - # '' - voltage: String - # '2.27' - speed: Float! - # '1.60' - speedmin: Float! - # '2.26' - speedmax: Float! - # 12 - threads: Int! - # 6 - cores: Int! - # 1 - processors: Long! - # 'LGA1366' - socket: String! - # { l1d: 196608, l1i: 196608, l2: 1, l3: 12 } - cache: JSON! - flags: [String!] -} \ No newline at end of file diff --git a/api/src/graphql/schema/types/info/devices.graphql b/api/src/graphql/schema/types/info/devices.graphql deleted file mode 100644 index e6091a884..000000000 --- a/api/src/graphql/schema/types/info/devices.graphql +++ /dev/null @@ -1,52 +0,0 @@ -type Info { - devices: Devices -} - -type Devices { - gpu: [Gpu] - network: [Network] - pci: [Pci] - usb: [Usb] -} - -type Gpu { - id: ID! - type: String! - typeid: String! - vendorname: String! - productid: String! - blacklisted: Boolean! - class: String! -} - -type Network { - iface: String - ifaceName: String - ipv4: String - ipv6: String - mac: String - internal: String - operstate: String - type: String - duplex: String - mtu: String - speed: String - carrierChanges: String -} - -type Pci { - id: ID! - type: String - typeid: String - vendorname: String - vendorid: String - productname: String - productid: String - blacklisted: String - class: String -} - -type Usb { - id: ID! - name: String -} \ No newline at end of file diff --git a/api/src/graphql/schema/types/info/display.graphql b/api/src/graphql/schema/types/info/display.graphql deleted file mode 100644 index 6576008fc..000000000 --- a/api/src/graphql/schema/types/info/display.graphql +++ /dev/null @@ -1,34 +0,0 @@ -type Info { - display: Display -} - -type Display { - date: String - number: String - scale: Boolean - tabs: Boolean - users: String - resize: Boolean - wwn: Boolean - total: Boolean - usage: Boolean - banner: String - dashapps: String - theme: Theme - text: Boolean - unit: Temperature - warning: Int - critical: Int - hot: Int - max: Int - locale: String -} - -enum Temperature { - C - F -} - -enum Theme { - white -} \ No newline at end of file diff --git a/api/src/graphql/schema/types/info/info.graphql b/api/src/graphql/schema/types/info/info.graphql deleted file mode 100644 index b604b83d5..000000000 --- a/api/src/graphql/schema/types/info/info.graphql +++ /dev/null @@ -1,3 +0,0 @@ -type Info implements Node { - id: ID! -} diff --git a/api/src/graphql/schema/types/info/machine-id.graphql b/api/src/graphql/schema/types/info/machine-id.graphql deleted file mode 100644 index 80b700307..000000000 --- a/api/src/graphql/schema/types/info/machine-id.graphql +++ /dev/null @@ -1,4 +0,0 @@ -type Info { - """Machine ID""" - machineId: ID -} \ No newline at end of file diff --git a/api/src/graphql/schema/types/info/memory.graphql b/api/src/graphql/schema/types/info/memory.graphql deleted file mode 100644 index bec136dcd..000000000 --- a/api/src/graphql/schema/types/info/memory.graphql +++ /dev/null @@ -1,41 +0,0 @@ -type Info { - memory: InfoMemory -} - -type InfoMemory { - max: Long! - total: Long! - free: Long! - used: Long! - active: Long! - available: Long! - buffcache: Long! - swaptotal: Long! - swapused: Long! - swapfree: Long! - layout: [MemoryLayout!] -} - -type MemoryLayout { - size: Long! - bank: String - type: MemoryType - clockSpeed: Long - formFactor: MemoryFormFactor - manufacturer: String - partNum: String - serialNum: String - voltageConfigured: Long - voltageMin: Long - voltageMax: Long -} - -enum MemoryType { - DDR2 - DDR3 - DDR4 -} - -enum MemoryFormFactor { - DIMM -} \ No newline at end of file diff --git a/api/src/graphql/schema/types/info/os.graphql b/api/src/graphql/schema/types/info/os.graphql deleted file mode 100644 index 5bbdc1a2f..000000000 --- a/api/src/graphql/schema/types/info/os.graphql +++ /dev/null @@ -1,18 +0,0 @@ -type Info { - os: Os -} - -type Os { - platform: String - distro: String - release: String - codename: String - kernel: String - arch: String - hostname: String - codepage: String - logofile: String - serial: String - build: String - uptime: DateTime -} diff --git a/api/src/graphql/schema/types/info/system.graphql b/api/src/graphql/schema/types/info/system.graphql deleted file mode 100644 index 1028f5941..000000000 --- a/api/src/graphql/schema/types/info/system.graphql +++ /dev/null @@ -1,12 +0,0 @@ -type Info { - system: System -} - -type System { - manufacturer: String - model: String - version: String - serial: String - uuid: String - sku: String -} \ No newline at end of file diff --git a/api/src/graphql/schema/types/info/time.graphql b/api/src/graphql/schema/types/info/time.graphql deleted file mode 100644 index bcbae032b..000000000 --- a/api/src/graphql/schema/types/info/time.graphql +++ /dev/null @@ -1,3 +0,0 @@ -type Info { - time: DateTime! -} \ No newline at end of file diff --git a/api/src/graphql/schema/types/info/versions.graphql b/api/src/graphql/schema/types/info/versions.graphql deleted file mode 100644 index d48229242..000000000 --- a/api/src/graphql/schema/types/info/versions.graphql +++ /dev/null @@ -1,32 +0,0 @@ -type Info { - versions: Versions -} - -type Versions { - kernel: String - openssl: String - systemOpenssl: String - systemOpensslLib: String - node: String - v8: String - npm: String - yarn: String - pm2: String - gulp: String - grunt: String - git: String - tsc: String - mysql: String - redis: String - mongodb: String - apache: String - nginx: String - php: String - docker: String - postfix: String - postgresql: String - perl: String - python: String - gcc: String - unraid: String -} \ No newline at end of file diff --git a/api/src/graphql/schema/types/logs/logs.graphql b/api/src/graphql/schema/types/logs/logs.graphql deleted file mode 100644 index fe5cb4996..000000000 --- a/api/src/graphql/schema/types/logs/logs.graphql +++ /dev/null @@ -1,72 +0,0 @@ -type Query { - """ - List all available log files - """ - logFiles: [LogFile!]! - - """ - Get the content of a specific log file - @param path Path to the log file - @param lines Number of lines to read from the end of the file (default: 100) - @param startLine Optional starting line number (1-indexed) - """ - logFile(path: String!, lines: Int, startLine: Int): LogFileContent! -} - -type Subscription { - """ - Subscribe to changes in a log file - @param path Path to the log file - """ - logFile(path: String!): LogFileContent! -} - -""" -Represents a log file in the system -""" -type LogFile { - """ - Name of the log file - """ - name: String! - - """ - Full path to the log file - """ - path: String! - - """ - Size of the log file in bytes - """ - size: Int! - - """ - Last modified timestamp - """ - modifiedAt: DateTime! -} - -""" -Content of a log file -""" -type LogFileContent { - """ - Path to the log file - """ - path: String! - - """ - Content of the log file - """ - content: String! - - """ - Total number of lines in the file - """ - totalLines: Int! - - """ - Starting line number of the content (1-indexed) - """ - startLine: Int -} \ No newline at end of file diff --git a/api/src/graphql/schema/types/network/network.graphql b/api/src/graphql/schema/types/network/network.graphql deleted file mode 100644 index 0c86e4ea2..000000000 --- a/api/src/graphql/schema/types/network/network.graphql +++ /dev/null @@ -1,32 +0,0 @@ -enum URL_TYPE { - LAN - WIREGUARD - WAN - MDNS - OTHER - DEFAULT -} - - -input AccessUrlInput { - type: URL_TYPE! - name: String - ipv4: URL - ipv6: URL -} - -type AccessUrl { - type: URL_TYPE! - name: String - ipv4: URL - ipv6: URL -} - -type Query { - network: Network -} - -type Network implements Node { - id: ID! - accessUrls: [AccessUrl!] -} diff --git a/api/src/graphql/schema/types/notifications/notifications.graphql b/api/src/graphql/schema/types/notifications/notifications.graphql deleted file mode 100644 index 3fe20c1e1..000000000 --- a/api/src/graphql/schema/types/notifications/notifications.graphql +++ /dev/null @@ -1,98 +0,0 @@ -enum NotificationType { - UNREAD - ARCHIVE -} - -input NotificationFilter { - importance: Importance - type: NotificationType - offset: Int! - limit: Int! -} - -type Query { - notifications: Notifications! -} - -type Mutation { - createNotification(input: NotificationData!): Notification! - deleteNotification(id: String!, type: NotificationType!): NotificationOverview! - """ - Deletes all archived notifications on server. - """ - deleteArchivedNotifications: NotificationOverview! - """ - Marks a notification as archived. - """ - archiveNotification(id: String!): Notification! - """ - Marks a notification as unread. - """ - unreadNotification(id: String!): Notification! - archiveNotifications(ids: [String!]): NotificationOverview! - unarchiveNotifications(ids: [String!]): NotificationOverview! - archiveAll(importance: Importance): NotificationOverview! - unarchiveAll(importance: Importance): NotificationOverview! - """ - Reads each notification to recompute & update the overview. - """ - recalculateOverview: NotificationOverview! -} - -type Subscription { - notificationAdded: Notification! - notificationsOverview: NotificationOverview! -} - -enum Importance { - ALERT - INFO - WARNING -} - -type Notifications implements Node { - id: ID! - """ - A cached overview of the notifications in the system & their severity. - """ - overview: NotificationOverview! - list(filter: NotificationFilter!): [Notification!]! -} - -type Notification implements Node { - id: ID! - """ - Also known as 'event' - """ - title: String! - subject: String! - description: String! - importance: Importance! - link: String - type: NotificationType! - """ - ISO Timestamp for when the notification occurred - """ - timestamp: String - formattedTimestamp: String -} - -input NotificationData { - title: String! - subject: String! - description: String! - importance: Importance! - link: String -} - -type NotificationOverview { - unread: NotificationCounts! - archive: NotificationCounts! -} - -type NotificationCounts { - info: Int! - warning: Int! - alert: Int! - total: Int! -} diff --git a/api/src/graphql/schema/types/owner/owner.graphql b/api/src/graphql/schema/types/owner/owner.graphql deleted file mode 100644 index 62347087f..000000000 --- a/api/src/graphql/schema/types/owner/owner.graphql +++ /dev/null @@ -1,13 +0,0 @@ -type Query { - owner: Owner -} - -type Subscription { - owner: Owner! -} - -type Owner { - username: String - url: String - avatar: String -} \ No newline at end of file diff --git a/api/src/graphql/schema/types/registration/registration.graphql b/api/src/graphql/schema/types/registration/registration.graphql deleted file mode 100644 index 8b5648754..000000000 --- a/api/src/graphql/schema/types/registration/registration.graphql +++ /dev/null @@ -1,21 +0,0 @@ -type Query { - registration: Registration -} - -type Subscription { - registration: Registration! -} - -type KeyFile { - location: String - contents: String -} - -type Registration { - guid: String - type: registrationType - keyFile: KeyFile - state: RegistrationState - expiration: String - updateExpiration: String -} \ No newline at end of file diff --git a/api/src/graphql/schema/types/servers/server.graphql b/api/src/graphql/schema/types/servers/server.graphql deleted file mode 100644 index 574ff556d..000000000 --- a/api/src/graphql/schema/types/servers/server.graphql +++ /dev/null @@ -1,34 +0,0 @@ -type Query { - server: Server - servers: [Server!]! -} - -type Subscription { - server: Server -} - -enum ServerStatus { - online - offline - never_connected -} - - -type ProfileModel { - userId: ID - username: String - url: String - avatar: String -} - -type Server { - owner: ProfileModel! - guid: String! - apikey: String! - name: String! - status: ServerStatus! - wanip: String! - lanip: String! - localurl: String! - remoteurl: String! -} \ No newline at end of file diff --git a/api/src/graphql/schema/types/services/service.graphql b/api/src/graphql/schema/types/services/service.graphql deleted file mode 100644 index 8bd99f4f8..000000000 --- a/api/src/graphql/schema/types/services/service.graphql +++ /dev/null @@ -1,19 +0,0 @@ -type Subscription { - service(name: String!): [Service!] -} - -type Uptime { - timestamp: String -} - -type Service implements Node { - id: ID! - name: String - online: Boolean - uptime: Uptime - version: String -} - -type Query { - services: [Service!]! -} \ No newline at end of file diff --git a/api/src/graphql/schema/types/shares/share.graphql b/api/src/graphql/schema/types/shares/share.graphql deleted file mode 100644 index 244302a06..000000000 --- a/api/src/graphql/schema/types/shares/share.graphql +++ /dev/null @@ -1,35 +0,0 @@ -type Query { - """Network Shares""" - shares: [Share] -} - -type Subscription { - share(id: ID!): Share! - shares: [Share!] -} - -"""Network Share""" -type Share { - """Display name""" - name: String - """(KB) Free space""" - free: Long - """(KB) Used Size""" - used: Long - """(KB) Total size""" - size: Long - """Disks that're included in this share""" - include: [String] - """Disks that're excluded from this share""" - exclude: [String] - cache: Boolean - nameOrig: String - """User comment""" - comment: String - allocator: String - splitLevel: String - floor: String - cow: String - color: String - luksStatus: String -} \ No newline at end of file diff --git a/api/src/graphql/schema/types/unassigned-devices/mount.graphql b/api/src/graphql/schema/types/unassigned-devices/mount.graphql deleted file mode 100644 index b65155eec..000000000 --- a/api/src/graphql/schema/types/unassigned-devices/mount.graphql +++ /dev/null @@ -1,6 +0,0 @@ -type Mount { - name: String - directory: String - type: String - permissions: String -} \ No newline at end of file diff --git a/api/src/graphql/schema/types/unassigned-devices/partition.graphql b/api/src/graphql/schema/types/unassigned-devices/partition.graphql deleted file mode 100644 index fc5cb19ca..000000000 --- a/api/src/graphql/schema/types/unassigned-devices/partition.graphql +++ /dev/null @@ -1,60 +0,0 @@ -type Partition { - devlinks: String - devname: String - devpath: String - devtype: String - idAta: String - idAtaDownloadMicrocode: String - idAtaFeatureSetAam: String - idAtaFeatureSetAamCurrentValue: String - idAtaFeatureSetAamEnabled: String - idAtaFeatureSetAamVendorRecommendedValue: String - idAtaFeatureSetApm: String - idAtaFeatureSetApmCurrentValue: String - idAtaFeatureSetApmEnabled: String - idAtaFeatureSetHpa: String - idAtaFeatureSetHpaEnabled: String - idAtaFeatureSetPm: String - idAtaFeatureSetPmEnabled: String - idAtaFeatureSetPuis: String - idAtaFeatureSetPuisEnabled: String - idAtaFeatureSetSecurity: String - idAtaFeatureSetSecurityEnabled: String - idAtaFeatureSetSecurityEnhancedEraseUnitMin: String - idAtaFeatureSetSecurityEraseUnitMin: String - idAtaFeatureSetSmart: String - idAtaFeatureSetSmartEnabled: String - idAtaRotationRateRpm: String - idAtaSata: String - idAtaSataSignalRateGen1: String - idAtaSataSignalRateGen2: String - idAtaWriteCache: String - idAtaWriteCacheEnabled: String - idBus: String - idFsType: String - idFsUsage: String - idFsUuid: String - idFsUuidEnc: String - idModel: String - idModelEnc: String - idPartEntryDisk: String - idPartEntryNumber: String - idPartEntryOffset: String - idPartEntryScheme: String - idPartEntrySize: String - idPartEntryType: String - idPartTableType: String - idPath: String - idPathTag: String - idRevision: String - idSerial: String - idSerialShort: String - idType: String - idWwn: String - idWwnWithExtension: String - major: String - minor: String - partn: String - subsystem: String - usecInitialized: String -} \ No newline at end of file diff --git a/api/src/graphql/schema/types/unassigned-devices/unassigned-device.graphql b/api/src/graphql/schema/types/unassigned-devices/unassigned-device.graphql deleted file mode 100644 index b3d3f733c..000000000 --- a/api/src/graphql/schema/types/unassigned-devices/unassigned-device.graphql +++ /dev/null @@ -1,62 +0,0 @@ -type Query { - unassignedDevices: [UnassignedDevice] -} - -type Subscription { - unassignedDevices: [UnassignedDevice!] -} - -type UnassignedDevice { - devlinks: String - devname: String - devpath: String - devtype: String - idAta: String - idAtaDownloadMicrocode: String - idAtaFeatureSetAam: String - idAtaFeatureSetAamCurrentValue: String - idAtaFeatureSetAamEnabled: String - idAtaFeatureSetAamVendorRecommendedValue: String - idAtaFeatureSetApm: String - idAtaFeatureSetApmCurrentValue: String - idAtaFeatureSetApmEnabled: String - idAtaFeatureSetHpa: String - idAtaFeatureSetHpaEnabled: String - idAtaFeatureSetPm: String - idAtaFeatureSetPmEnabled: String - idAtaFeatureSetPuis: String - idAtaFeatureSetPuisEnabled: String - idAtaFeatureSetSecurity: String - idAtaFeatureSetSecurityEnabled: String - idAtaFeatureSetSecurityEnhancedEraseUnitMin: String - idAtaFeatureSetSecurityEraseUnitMin: String - idAtaFeatureSetSmart: String - idAtaFeatureSetSmartEnabled: String - idAtaRotationRateRpm: String - idAtaSata: String - idAtaSataSignalRateGen1: String - idAtaSataSignalRateGen2: String - idAtaWriteCache: String - idAtaWriteCacheEnabled: String - idBus: String - idModel: String - idModelEnc: String - idPartTableType: String - idPath: String - idPathTag: String - idRevision: String - idSerial: String - idSerialShort: String - idType: String - idWwn: String - idWwnWithExtension: String - major: String - minor: String - subsystem: String - usecInitialized: String - partitions: [Partition] - temp: Int - name: String - mounted: Boolean - mount: Mount -} \ No newline at end of file diff --git a/api/src/graphql/schema/types/users/me.graphql b/api/src/graphql/schema/types/users/me.graphql deleted file mode 100644 index 2aaacae62..000000000 --- a/api/src/graphql/schema/types/users/me.graphql +++ /dev/null @@ -1,21 +0,0 @@ -type Query { - """ - Current user account - """ - me: Me -} - -""" -The current user -""" -type Me implements UserAccount { - id: ID! - name: String! - description: String! - roles: [Role!]! - permissions: [Permission!] -} - -type Subscription { - me: Me -} diff --git a/api/src/graphql/schema/types/users/user.graphql b/api/src/graphql/schema/types/users/user.graphql deleted file mode 100644 index 606bcae25..000000000 --- a/api/src/graphql/schema/types/users/user.graphql +++ /dev/null @@ -1,66 +0,0 @@ -interface UserAccount { - id: ID! - name: String! - description: String! - roles: [Role!]! - permissions: [Permission!] -} - -input usersInput { - slim: Boolean -} - -type Query { - """ - User account - """ - user(id: ID!): User - """ - User accounts - """ - users(input: usersInput): [User!]! -} - -input addUserInput { - name: String! - password: String! - description: String -} - -input deleteUserInput { - name: String! -} - -type Mutation { - """ - Add a new user - """ - addUser(input: addUserInput!): User - """ - Delete a user - """ - deleteUser(input: deleteUserInput!): User -} - -type Subscription { - user(id: ID!): User! - users: [User]! -} - -""" -A local user account -""" -type User implements UserAccount { - id: ID! - """ - A unique name for the user - """ - name: String! - description: String! - roles: [Role!]! - """ - If the account has a password set - """ - password: Boolean - permissions: [Permission!] -} diff --git a/api/src/graphql/schema/types/vars/vars.graphql b/api/src/graphql/schema/types/vars/vars.graphql deleted file mode 100644 index e99c3fc17..000000000 --- a/api/src/graphql/schema/types/vars/vars.graphql +++ /dev/null @@ -1,293 +0,0 @@ -type Query { - vars: Vars -} - -type Subscription { - vars: Vars! -} - -enum ConfigErrorState { - UNKNOWN_ERROR - INELIGIBLE - INVALID - NO_KEY_SERVER - WITHDRAWN -} - -type Vars implements Node { - id: ID! - """ - Unraid version - """ - version: String - maxArraysz: Int - maxCachesz: Int - """ - Machine hostname - """ - name: String - timeZone: String - comment: String - security: String - workgroup: String - domain: String - domainShort: String - hideDotFiles: Boolean - localMaster: Boolean - enableFruit: String - """ - Should a NTP server be used for time sync? - """ - useNtp: Boolean - """ - NTP Server 1 - """ - ntpServer1: String - """ - NTP Server 2 - """ - ntpServer2: String - """ - NTP Server 3 - """ - ntpServer3: String - """ - NTP Server 4 - """ - ntpServer4: String - domainLogin: String - sysModel: String - sysArraySlots: Int - sysCacheSlots: Int - sysFlashSlots: Int - useSsl: Boolean - """ - Port for the webui via HTTP - """ - port: Int - """ - Port for the webui via HTTPS - """ - portssl: Int - localTld: String - bindMgt: Boolean - """ - Should telnet be enabled? - """ - useTelnet: Boolean - porttelnet: Int - useSsh: Boolean - portssh: Int - startPage: String - startArray: Boolean - spindownDelay: String - queueDepth: String - spinupGroups: Boolean - defaultFormat: String - defaultFsType: String - shutdownTimeout: Int - luksKeyfile: String - pollAttributes: String - pollAttributesDefault: String - pollAttributesStatus: String - nrRequests: Int - nrRequestsDefault: Int - nrRequestsStatus: String - mdNumStripes: Int - mdNumStripesDefault: Int - mdNumStripesStatus: String - mdSyncWindow: Int - mdSyncWindowDefault: Int - mdSyncWindowStatus: String - mdSyncThresh: Int - mdSyncThreshDefault: Int - mdSyncThreshStatus: String - mdWriteMethod: Int - mdWriteMethodDefault: String - mdWriteMethodStatus: String - shareDisk: String - shareUser: String - shareUserInclude: String - shareUserExclude: String - shareSmbEnabled: Boolean - shareNfsEnabled: Boolean - shareAfpEnabled: Boolean - shareInitialOwner: String - shareInitialGroup: String - shareCacheEnabled: Boolean - shareCacheFloor: String - shareMoverSchedule: String - shareMoverLogging: Boolean - fuseRemember: String - fuseRememberDefault: String - fuseRememberStatus: String - fuseDirectio: String - fuseDirectioDefault: String - fuseDirectioStatus: String - shareAvahiEnabled: Boolean - shareAvahiSmbName: String - shareAvahiSmbModel: String - shareAvahiAfpName: String - shareAvahiAfpModel: String - safeMode: Boolean - startMode: String - configValid: Boolean - configError: ConfigErrorState - joinStatus: String - deviceCount: Int - flashGuid: String - flashProduct: String - flashVendor: String - regCheck: String - regFile: String - regGuid: String - regTy: String - regState: RegistrationState - """ - Registration owner - """ - regTo: String - regTm: String - regTm2: String - regGen: String - sbName: String - sbVersion: String - sbUpdated: String - sbEvents: Int - sbState: String - sbClean: Boolean - sbSynced: Int - sbSyncErrs: Int - sbSynced2: Int - sbSyncExit: String - sbNumDisks: Int - mdColor: String - mdNumDisks: Int - mdNumDisabled: Int - mdNumInvalid: Int - mdNumMissing: Int - mdNumNew: Int - mdNumErased: Int - mdResync: Int - mdResyncCorr: String - mdResyncPos: String - mdResyncDb: String - mdResyncDt: String - mdResyncAction: String - mdResyncSize: Int - mdState: String - mdVersion: String - cacheNumDevices: Int - cacheSbNumDisks: Int - fsState: String - """ - Human friendly string of array events happening - """ - fsProgress: String - """ - Percentage from 0 - 100 while upgrading a disk or swapping parity drives - """ - fsCopyPrcnt: Int - fsNumMounted: Int - fsNumUnmountable: Int - fsUnmountableMask: String - """ - Total amount of user shares - """ - shareCount: Int - """ - Total amount shares with SMB enabled - """ - shareSmbCount: Int - """ - Total amount shares with NFS enabled - """ - shareNfsCount: Int - """ - Total amount shares with AFP enabled - """ - shareAfpCount: Int - shareMoverActive: Boolean - csrfToken: String -} - -enum mdState { - SWAP_DSBL - STARTED -} - -enum registrationType { - BASIC - PLUS - PRO - STARTER - UNLEASHED - LIFETIME - INVALID - TRIAL -} - -enum RegistrationState { - TRIAL - BASIC - PLUS - PRO - STARTER - UNLEASHED - LIFETIME - """ - Trial Expired - """ - EEXPIRED - """ - GUID Error - """ - EGUID - """ - Multiple License Keys Present - """ - EGUID1 - """ - Invalid installation - """ - ETRIAL - """ - No Keyfile - """ - ENOKEYFILE - """ - No Keyfile - """ - ENOKEYFILE1 - """ - Missing key file - """ - ENOKEYFILE2 - """ - No Flash - """ - ENOFLASH - ENOFLASH1 - ENOFLASH2 - ENOFLASH3 - ENOFLASH4 - ENOFLASH5 - ENOFLASH6 - ENOFLASH7 - """ - BLACKLISTED - """ - EBLACKLISTED - """ - BLACKLISTED - """ - EBLACKLISTED1 - """ - BLACKLISTED - """ - EBLACKLISTED2 - """ - Trial Requires Internet Connection - """ - ENOCONN -} diff --git a/api/src/graphql/schema/types/vms/domain.graphql b/api/src/graphql/schema/types/vms/domain.graphql deleted file mode 100644 index 7f5ff283c..000000000 --- a/api/src/graphql/schema/types/vms/domain.graphql +++ /dev/null @@ -1,56 +0,0 @@ -type Query { - """Virtual machines""" - vms: Vms -} - -type Mutation { - """Virtual machine mutations""" - vms: VmMutations -} - -type VmMutations { - """Start a virtual machine""" - startVm(id: ID!): Boolean! - """Stop a virtual machine""" - stopVm(id: ID!): Boolean! - """Pause a virtual machine""" - pauseVm(id: ID!): Boolean! - """Resume a virtual machine""" - resumeVm(id: ID!): Boolean! - """Force stop a virtual machine""" - forceStopVm(id: ID!): Boolean! - """Reboot a virtual machine""" - rebootVm(id: ID!): Boolean! - """Reset a virtual machine""" - resetVm(id: ID!): Boolean! -} - -type Subscription { - vms: Vms -} - -type Vms { - id: ID! - domain: [VmDomain!] -} - -# https://libvirt.org/manpages/virsh.html#list -enum VmState { - NOSTATE - RUNNING - IDLE - PAUSED - SHUTDOWN - SHUTOFF - CRASHED - PMSUSPENDED -} - -"""A virtual machine""" -type VmDomain { - uuid: ID! - """A friendly name for the vm""" - name: String - """Current domain vm state""" - state: VmState! -} diff --git a/api/src/graphql/schema/utils.ts b/api/src/graphql/schema/utils.ts index cee23d8e4..54e2b03c6 100644 --- a/api/src/graphql/schema/utils.ts +++ b/api/src/graphql/schema/utils.ts @@ -1,12 +1,11 @@ -import type { Server } from '@app/graphql/generated/client/graphql.js'; import { AppError } from '@app/core/errors/app-error.js'; import { graphqlLogger } from '@app/core/log.js'; import { pubsub } from '@app/core/pubsub.js'; import { type User } from '@app/core/types/states/user.js'; import { ensurePermission } from '@app/core/utils/permissions/ensure-permission.js'; -import { MinigraphStatus } from '@app/graphql/generated/api/types.js'; -import { ServerStatus } from '@app/graphql/generated/client/graphql.js'; import { store } from '@app/store/index.js'; +import { MinigraphStatus } from '@app/unraid-api/graph/resolvers/cloud/cloud.model.js'; +import { Server, ServerStatus } from '@app/unraid-api/graph/resolvers/servers/server.model.js'; export interface Context { user?: User; diff --git a/api/src/mothership/graphql-client.ts b/api/src/mothership/graphql-client.ts index 5b81a1fee..ec831a297 100644 --- a/api/src/mothership/graphql-client.ts +++ b/api/src/mothership/graphql-client.ts @@ -10,7 +10,6 @@ import { WebSocket } from 'ws'; import { FIVE_MINUTES_MS } from '@app/consts.js'; import { minigraphLogger } from '@app/core/log.js'; import { API_VERSION, MOTHERSHIP_GRAPHQL_LINK } from '@app/environment.js'; -import { MinigraphStatus } from '@app/graphql/generated/api/types.js'; import { buildDelayFunction } from '@app/mothership/utils/delay-function.js'; import { getMothershipConnectionParams, @@ -20,6 +19,7 @@ import { setGraphqlConnectionStatus } from '@app/store/actions/set-minigraph-sta import { getters, store } from '@app/store/index.js'; import { logoutUser } from '@app/store/modules/config.js'; import { receivedMothershipPing, setMothershipTimeout } from '@app/store/modules/minigraph.js'; +import { MinigraphStatus } from '@app/unraid-api/graph/resolvers/cloud/cloud.model.js'; const getWebsocketWithMothershipHeaders = () => { return class WebsocketWithMothershipHeaders extends WebSocket { diff --git a/api/src/mothership/jobs/ping-timeout-jobs.ts b/api/src/mothership/jobs/ping-timeout-jobs.ts index 8d7761097..907182784 100644 --- a/api/src/mothership/jobs/ping-timeout-jobs.ts +++ b/api/src/mothership/jobs/ping-timeout-jobs.ts @@ -2,12 +2,13 @@ import { CronJob } from 'cron'; import { KEEP_ALIVE_INTERVAL_MS, ONE_MINUTE_MS } from '@app/consts.js'; import { minigraphLogger, mothershipLogger, remoteAccessLogger } from '@app/core/log.js'; -import { DynamicRemoteAccessType, MinigraphStatus } from '@app/graphql/generated/api/types.js'; import { isAPIStateDataFullyLoaded } from '@app/mothership/graphql-client.js'; import { setGraphqlConnectionStatus } from '@app/store/actions/set-minigraph-status.js'; import { store } from '@app/store/index.js'; import { setRemoteAccessRunningType } from '@app/store/modules/dynamic-remote-access.js'; import { clearSubscription } from '@app/store/modules/remote-graphql.js'; +import { MinigraphStatus } from '@app/unraid-api/graph/resolvers/cloud/cloud.model.js'; +import { DynamicRemoteAccessType } from '@app/unraid-api/graph/resolvers/connect/connect.model.js'; class PingTimeoutJobs { private cronJob: CronJob; diff --git a/api/src/remoteAccess/handlers/remote-access-interface.ts b/api/src/remoteAccess/handlers/remote-access-interface.ts index 93fe95a4b..b3ce45e78 100644 --- a/api/src/remoteAccess/handlers/remote-access-interface.ts +++ b/api/src/remoteAccess/handlers/remote-access-interface.ts @@ -1,5 +1,5 @@ -import { type AccessUrl } from '@app/graphql/generated/api/types.js'; import { type AppDispatch, type RootState } from '@app/store/index.js'; +import { AccessUrl } from '@app/unraid-api/graph/resolvers/connect/connect.model.js'; export interface GenericRemoteAccess { beginRemoteAccess({ diff --git a/api/src/remoteAccess/handlers/static-remote-access.ts b/api/src/remoteAccess/handlers/static-remote-access.ts index 419dd53e9..79fd1eaf8 100644 --- a/api/src/remoteAccess/handlers/static-remote-access.ts +++ b/api/src/remoteAccess/handlers/static-remote-access.ts @@ -1,10 +1,13 @@ -import type { AccessUrl } from '@app/graphql/generated/api/types.js'; import { remoteAccessLogger } from '@app/core/log.js'; -import { DynamicRemoteAccessType, URL_TYPE } from '@app/graphql/generated/api/types.js'; import { getServerIps } from '@app/graphql/resolvers/subscription/network.js'; import { type GenericRemoteAccess } from '@app/remoteAccess/handlers/remote-access-interface.js'; import { setWanAccessAndReloadNginx } from '@app/store/actions/set-wan-access-with-reload.js'; import { type AppDispatch, type RootState } from '@app/store/index.js'; +import { + AccessUrl, + DynamicRemoteAccessType, + URL_TYPE, +} from '@app/unraid-api/graph/resolvers/connect/connect.model.js'; export class StaticRemoteAccess implements GenericRemoteAccess { public getRemoteAccessUrl({ getState }: { getState: () => RootState }): AccessUrl | null { diff --git a/api/src/remoteAccess/handlers/upnp-remote-access.ts b/api/src/remoteAccess/handlers/upnp-remote-access.ts index 33e7490bf..2bf3ec5c4 100644 --- a/api/src/remoteAccess/handlers/upnp-remote-access.ts +++ b/api/src/remoteAccess/handlers/upnp-remote-access.ts @@ -1,11 +1,14 @@ -import type { AccessUrl } from '@app/graphql/generated/api/types.js'; import { remoteAccessLogger } from '@app/core/log.js'; -import { DynamicRemoteAccessType, URL_TYPE } from '@app/graphql/generated/api/types.js'; import { getServerIps } from '@app/graphql/resolvers/subscription/network.js'; import { type GenericRemoteAccess } from '@app/remoteAccess/handlers/remote-access-interface.js'; import { setWanAccessAndReloadNginx } from '@app/store/actions/set-wan-access-with-reload.js'; import { type AppDispatch, type RootState } from '@app/store/index.js'; import { disableUpnp, enableUpnp } from '@app/store/modules/upnp.js'; +import { + AccessUrl, + DynamicRemoteAccessType, + URL_TYPE, +} from '@app/unraid-api/graph/resolvers/connect/connect.model.js'; export class UpnpRemoteAccess implements GenericRemoteAccess { async stopRemoteAccess({ dispatch }: { getState: () => RootState; dispatch: AppDispatch }) { @@ -16,7 +19,7 @@ export class UpnpRemoteAccess implements GenericRemoteAccess { public getRemoteAccessUrl({ getState }: { getState: () => RootState }): AccessUrl | null { const urlsForServer = getServerIps(getState()); - const url = urlsForServer.urls.find((url) => url.type === URL_TYPE.WAN); + const url = urlsForServer.urls.find((url) => url.type === URL_TYPE.WAN) ?? null; return url ?? null; } diff --git a/api/src/remoteAccess/remote-access-controller.ts b/api/src/remoteAccess/remote-access-controller.ts index c9a9b48de..0814580ff 100644 --- a/api/src/remoteAccess/remote-access-controller.ts +++ b/api/src/remoteAccess/remote-access-controller.ts @@ -1,8 +1,6 @@ -import type { AccessUrl } from '@app/graphql/generated/api/types.js'; import type { AppDispatch, RootState } from '@app/store/index.js'; import { remoteAccessLogger } from '@app/core/log.js'; import { UnraidLocalNotifier } from '@app/core/notifiers/unraid-local.js'; -import { DynamicRemoteAccessType } from '@app/graphql/generated/api/types.js'; import { type IRemoteAccessController } from '@app/remoteAccess/handlers/remote-access-interface.js'; import { StaticRemoteAccess } from '@app/remoteAccess/handlers/static-remote-access.js'; import { UpnpRemoteAccess } from '@app/remoteAccess/handlers/upnp-remote-access.js'; @@ -13,6 +11,10 @@ import { setDynamicRemoteAccessError, setRemoteAccessRunningType, } from '@app/store/modules/dynamic-remote-access.js'; +import { + AccessUrl, + DynamicRemoteAccessType, +} from '@app/unraid-api/graph/resolvers/connect/connect.model.js'; export class RemoteAccessController implements IRemoteAccessController { static _instance: RemoteAccessController | null = null; diff --git a/api/src/store/actions/add-remote-subscription.ts b/api/src/store/actions/add-remote-subscription.ts index 0e08e5e55..707eccb0b 100644 --- a/api/src/store/actions/add-remote-subscription.ts +++ b/api/src/store/actions/add-remote-subscription.ts @@ -1,9 +1,9 @@ import { createAsyncThunk } from '@reduxjs/toolkit'; -import type { RemoteGraphQLEventFragmentFragment } from '@app/graphql/generated/client/graphql.js'; +import type { RemoteGraphQlEventFragmentFragment } from '@app/graphql/generated/client/graphql.js'; import { remoteQueryLogger } from '@app/core/log.js'; import { getApiApolloClient } from '@app/graphql/client/api/get-api-client.js'; -import { RemoteGraphQLEventType } from '@app/graphql/generated/client/graphql.js'; +import { RemoteGraphQlEventType } from '@app/graphql/generated/client/graphql.js'; import { SEND_REMOTE_QUERY_RESPONSE } from '@app/graphql/mothership/mutations.js'; import { parseGraphQLQuery } from '@app/graphql/resolvers/subscription/remote-graphql/remote-graphql-helpers.js'; import { GraphQLClient } from '@app/mothership/graphql-client.js'; @@ -13,7 +13,7 @@ import { type SubscriptionWithSha256 } from '@app/store/types.js'; export const addRemoteSubscription = createAsyncThunk< SubscriptionWithSha256, - RemoteGraphQLEventFragmentFragment['remoteGraphQLEventData'], + RemoteGraphQlEventFragmentFragment['remoteGraphQLEventData'], { state: RootState; dispatch: AppDispatch } >('remoteGraphQL/addRemoteSubscription', async (data, { getState }) => { if (hasRemoteSubscription(data.sha256, getState())) { @@ -48,7 +48,7 @@ export const addRemoteSubscription = createAsyncThunk< input: { sha256: data.sha256, body: JSON.stringify({ data: val.data }), - type: RemoteGraphQLEventType.REMOTE_SUBSCRIPTION_EVENT, + type: RemoteGraphQlEventType.REMOTE_SUBSCRIPTION_EVENT, }, }, }); @@ -63,7 +63,7 @@ export const addRemoteSubscription = createAsyncThunk< input: { sha256: data.sha256, body: JSON.stringify({ errors: errorValue }), - type: RemoteGraphQLEventType.REMOTE_SUBSCRIPTION_EVENT, + type: RemoteGraphQlEventType.REMOTE_SUBSCRIPTION_EVENT, }, }, }); diff --git a/api/src/store/actions/handle-remote-graphql-event.ts b/api/src/store/actions/handle-remote-graphql-event.ts index 9a8e3e1f9..2931523b1 100644 --- a/api/src/store/actions/handle-remote-graphql-event.ts +++ b/api/src/store/actions/handle-remote-graphql-event.ts @@ -1,8 +1,8 @@ import { createAsyncThunk } from '@reduxjs/toolkit'; -import type { RemoteGraphQLEventFragmentFragment } from '@app/graphql/generated/client/graphql.js'; +import type { RemoteGraphQlEventFragmentFragment } from '@app/graphql/generated/client/graphql.js'; import { remoteQueryLogger } from '@app/core/log.js'; -import { RemoteGraphQLEventType } from '@app/graphql/generated/client/graphql.js'; +import { RemoteGraphQlEventType } from '@app/graphql/generated/client/graphql.js'; import { executeRemoteGraphQLQuery } from '@app/graphql/resolvers/subscription/remote-graphql/remote-query.js'; import { createRemoteSubscription } from '@app/graphql/resolvers/subscription/remote-graphql/remote-subscription.js'; import { type AppDispatch, type RootState } from '@app/store/index.js'; @@ -10,20 +10,20 @@ import { renewRemoteSubscription } from '@app/store/modules/remote-graphql.js'; export const handleRemoteGraphQLEvent = createAsyncThunk< void, - RemoteGraphQLEventFragmentFragment, + RemoteGraphQlEventFragmentFragment, { state: RootState; dispatch: AppDispatch } >('dynamicRemoteAccess/handleRemoteAccessEvent', async (event, { dispatch }) => { const data = event.remoteGraphQLEventData; switch (data.type) { - case RemoteGraphQLEventType.REMOTE_MUTATION_EVENT: + case RemoteGraphQlEventType.REMOTE_MUTATION_EVENT: break; - case RemoteGraphQLEventType.REMOTE_QUERY_EVENT: + case RemoteGraphQlEventType.REMOTE_QUERY_EVENT: remoteQueryLogger.debug('Responding to remote query event'); return await executeRemoteGraphQLQuery(event.remoteGraphQLEventData); - case RemoteGraphQLEventType.REMOTE_SUBSCRIPTION_EVENT: + case RemoteGraphQlEventType.REMOTE_SUBSCRIPTION_EVENT: remoteQueryLogger.debug('Responding to remote subscription event'); return await createRemoteSubscription(data); - case RemoteGraphQLEventType.REMOTE_SUBSCRIPTION_EVENT_PING: + case RemoteGraphQlEventType.REMOTE_SUBSCRIPTION_EVENT_PING: await dispatch(renewRemoteSubscription({ sha256: data.sha256 })); break; } diff --git a/api/src/store/actions/set-minigraph-status.ts b/api/src/store/actions/set-minigraph-status.ts index d7b78faf6..bb3bbdf6a 100644 --- a/api/src/store/actions/set-minigraph-status.ts +++ b/api/src/store/actions/set-minigraph-status.ts @@ -1,6 +1,6 @@ import { createAction } from '@reduxjs/toolkit'; -import { type MinigraphStatus } from '@app/graphql/generated/api/types.js'; +import { MinigraphStatus } from '@app/unraid-api/graph/resolvers/cloud/cloud.model.js'; export const setGraphqlConnectionStatus = createAction<{ status: MinigraphStatus; diff --git a/api/src/store/actions/setup-remote-access.ts b/api/src/store/actions/setup-remote-access.ts index c0066ad85..1c5613372 100644 --- a/api/src/store/actions/setup-remote-access.ts +++ b/api/src/store/actions/setup-remote-access.ts @@ -1,13 +1,13 @@ import { createAsyncThunk } from '@reduxjs/toolkit'; -import type { SetupRemoteAccessInput } from '@app/graphql/generated/api/types.js'; -import { - DynamicRemoteAccessType, - WAN_ACCESS_TYPE, - WAN_FORWARD_TYPE, -} from '@app/graphql/generated/api/types.js'; import { type AppDispatch, type RootState } from '@app/store/index.js'; import { type MyServersConfig } from '@app/types/my-servers-config.js'; +import { + DynamicRemoteAccessType, + SetupRemoteAccessInput, + WAN_ACCESS_TYPE, + WAN_FORWARD_TYPE, +} from '@app/unraid-api/graph/resolvers/connect/connect.model.js'; const getDynamicRemoteAccessType = ( accessType: WAN_ACCESS_TYPE, diff --git a/api/src/store/actions/shutdown-api-event.ts b/api/src/store/actions/shutdown-api-event.ts index 81ba4b83f..8e649741b 100644 --- a/api/src/store/actions/shutdown-api-event.ts +++ b/api/src/store/actions/shutdown-api-event.ts @@ -1,10 +1,11 @@ import { logDestination, logger } from '@app/core/log.js'; -import { DynamicRemoteAccessType, MinigraphStatus } from '@app/graphql/generated/api/types.js'; import { setGraphqlConnectionStatus } from '@app/store/actions/set-minigraph-status.js'; import { store } from '@app/store/index.js'; import { stopListeners } from '@app/store/listeners/stop-listeners.js'; import { setWanAccess } from '@app/store/modules/config.js'; import { writeConfigSync } from '@app/store/sync/config-disk-sync.js'; +import { MinigraphStatus } from '@app/unraid-api/graph/resolvers/cloud/cloud.model.js'; +import { DynamicRemoteAccessType } from '@app/unraid-api/graph/resolvers/connect/connect.model.js'; export const shutdownApiEvent = () => { logger.debug('Running shutdown'); diff --git a/api/src/store/getters/index.ts b/api/src/store/getters/index.ts index 42a24295c..cf69ab9cc 100644 --- a/api/src/store/getters/index.ts +++ b/api/src/store/getters/index.ts @@ -1,7 +1,7 @@ import type { DNSCheck } from '@app/store/types.js'; -import { type CloudResponse } from '@app/graphql/generated/api/types.js'; import { getters, store } from '@app/store/index.js'; import { CacheKeys } from '@app/store/types.js'; +import { type CloudResponse } from '@app/unraid-api/graph/resolvers/cloud/cloud.model.js'; export const getCloudCache = (): CloudResponse | undefined => { const { nodeCache } = getters.cache(); diff --git a/api/src/store/listeners/dynamic-remote-access-listener.ts b/api/src/store/listeners/dynamic-remote-access-listener.ts index da2047eac..084b66bbe 100644 --- a/api/src/store/listeners/dynamic-remote-access-listener.ts +++ b/api/src/store/listeners/dynamic-remote-access-listener.ts @@ -1,12 +1,12 @@ import { isAnyOf } from '@reduxjs/toolkit'; import { remoteAccessLogger } from '@app/core/log.js'; -import { DynamicRemoteAccessType } from '@app/graphql/generated/api/types.js'; import { RemoteAccessController } from '@app/remoteAccess/remote-access-controller.js'; import { type RootState } from '@app/store/index.js'; import { startAppListening } from '@app/store/listeners/listener-middleware.js'; import { loadConfigFile } from '@app/store/modules/config.js'; import { FileLoadStatus } from '@app/store/types.js'; +import { DynamicRemoteAccessType } from '@app/unraid-api/graph/resolvers/connect/connect.model.js'; const shouldDynamicRemoteAccessBeEnabled = (state: RootState | null): boolean => { if ( diff --git a/api/src/store/listeners/mothership-subscription-listener.ts b/api/src/store/listeners/mothership-subscription-listener.ts index 8a79b080e..f36b2bc0d 100644 --- a/api/src/store/listeners/mothership-subscription-listener.ts +++ b/api/src/store/listeners/mothership-subscription-listener.ts @@ -1,11 +1,11 @@ import { isEqual } from 'lodash-es'; import { minigraphLogger } from '@app/core/log.js'; -import { MinigraphStatus } from '@app/graphql/generated/api/types.js'; import { setupNewMothershipSubscription } from '@app/mothership/subscribe-to-mothership.js'; import { getMothershipConnectionParams } from '@app/mothership/utils/get-mothership-websocket-headers.js'; import { setGraphqlConnectionStatus } from '@app/store/actions/set-minigraph-status.js'; import { startAppListening } from '@app/store/listeners/listener-middleware.js'; +import { MinigraphStatus } from '@app/unraid-api/graph/resolvers/cloud/cloud.model.js'; export const enableMothershipJobsListener = () => startAppListening({ diff --git a/api/src/store/modules/cache.ts b/api/src/store/modules/cache.ts index d1e461094..126036790 100644 --- a/api/src/store/modules/cache.ts +++ b/api/src/store/modules/cache.ts @@ -4,8 +4,8 @@ import NodeCache from 'node-cache'; import type { DNSCheck } from '@app/store/types.js'; import { ONE_HOUR_SECS } from '@app/consts.js'; -import { type CloudResponse } from '@app/graphql/generated/api/types.js'; import { CacheKeys } from '@app/store/types.js'; +import { CloudResponse } from '@app/unraid-api/graph/resolvers/cloud/cloud.model.js'; const initialState: { nodeCache: NodeCache; diff --git a/api/src/store/modules/config.ts b/api/src/store/modules/config.ts index 741918eab..830daa125 100644 --- a/api/src/store/modules/config.ts +++ b/api/src/store/modules/config.ts @@ -6,19 +6,20 @@ import type { PayloadAction } from '@reduxjs/toolkit'; import { createAsyncThunk, createSlice, isAnyOf } from '@reduxjs/toolkit'; import { isEqual, merge } from 'lodash-es'; -import type { Owner } from '@app/graphql/generated/api/types.js'; import { logger } from '@app/core/log.js'; import { getWriteableConfig } from '@app/core/utils/files/config-file-normalizer.js'; import { safelySerializeObjectToIni } from '@app/core/utils/files/safe-ini-serializer.js'; import { parseConfig } from '@app/core/utils/misc/parse-config.js'; import { NODE_ENV } from '@app/environment.js'; -import { DynamicRemoteAccessType, MinigraphStatus } from '@app/graphql/generated/api/types.js'; import { setGraphqlConnectionStatus } from '@app/store/actions/set-minigraph-status.js'; import { setupRemoteAccessThunk } from '@app/store/actions/setup-remote-access.js'; import { type RootState } from '@app/store/index.js'; import { FileLoadStatus } from '@app/store/types.js'; import { RecursivePartial } from '@app/types/index.js'; import { type MyServersConfig, type MyServersConfigMemory } from '@app/types/my-servers-config.js'; +import { MinigraphStatus } from '@app/unraid-api/graph/resolvers/cloud/cloud.model.js'; +import { DynamicRemoteAccessType } from '@app/unraid-api/graph/resolvers/connect/connect.model.js'; +import { Owner } from '@app/unraid-api/graph/resolvers/owner/owner.model.js'; export type SliceState = { status: FileLoadStatus; @@ -68,6 +69,7 @@ export const loginUser = createAsyncThunk< const owner: Owner = { username: userInfo.username, avatar: userInfo.avatar, + url: '', }; await pubsub.publish(PUBSUB_CHANNEL.OWNER, { owner }); return userInfo; diff --git a/api/src/store/modules/dynamic-remote-access.ts b/api/src/store/modules/dynamic-remote-access.ts index 2e8a00078..9a557dac9 100644 --- a/api/src/store/modules/dynamic-remote-access.ts +++ b/api/src/store/modules/dynamic-remote-access.ts @@ -1,9 +1,12 @@ import type { PayloadAction } from '@reduxjs/toolkit'; import { createSlice } from '@reduxjs/toolkit'; -import type { AccessUrlInput } from '@app/graphql/generated/api/types.js'; import { remoteAccessLogger } from '@app/core/log.js'; -import { DynamicRemoteAccessType, URL_TYPE } from '@app/graphql/generated/api/types.js'; +import { + AccessUrlInput, + DynamicRemoteAccessType, + URL_TYPE, +} from '@app/unraid-api/graph/resolvers/connect/connect.model.js'; interface DynamicRemoteAccessState { runningType: DynamicRemoteAccessType; // Is Dynamic Remote Access actively running - shows type of access currently running diff --git a/api/src/store/modules/emhttp.ts b/api/src/store/modules/emhttp.ts index 35af546ba..130416e46 100644 --- a/api/src/store/modules/emhttp.ts +++ b/api/src/store/modules/emhttp.ts @@ -16,8 +16,8 @@ import { type SmbShares } from '@app/core/types/states/smb.js'; import { type Users } from '@app/core/types/states/user.js'; import { type Var } from '@app/core/types/states/var.js'; import { parseConfig } from '@app/core/utils/misc/parse-config.js'; -import { type ArrayDisk } from '@app/graphql/generated/api/types.js'; import { FileLoadStatus, StateFileKey } from '@app/store/types.js'; +import { ArrayDisk } from '@app/unraid-api/graph/resolvers/array/array.model.js'; export type SliceState = { status: FileLoadStatus; diff --git a/api/src/store/modules/minigraph.ts b/api/src/store/modules/minigraph.ts index 30209023e..1c8e6a04b 100644 --- a/api/src/store/modules/minigraph.ts +++ b/api/src/store/modules/minigraph.ts @@ -3,9 +3,9 @@ import { createSlice } from '@reduxjs/toolkit'; import { KEEP_ALIVE_INTERVAL_MS } from '@app/consts.js'; import { minigraphLogger } from '@app/core/log.js'; -import { MinigraphStatus } from '@app/graphql/generated/api/types.js'; import { setGraphqlConnectionStatus } from '@app/store/actions/set-minigraph-status.js'; import { loginUser, logoutUser } from '@app/store/modules/config.js'; +import { MinigraphStatus } from '@app/unraid-api/graph/resolvers/cloud/cloud.model.js'; export type MinigraphClientState = { status: MinigraphStatus; diff --git a/api/src/store/modules/paths.ts b/api/src/store/modules/paths.ts index 7e526e07a..3614fea1a 100644 --- a/api/src/store/modules/paths.ts +++ b/api/src/store/modules/paths.ts @@ -10,7 +10,9 @@ const initialState = { ), 'docker-autostart': '/var/lib/docker/unraid-autostart' as const, 'docker-socket': '/var/run/docker.sock' as const, - 'parity-checks': '/boot/config/parity-checks.log' as const, + 'parity-checks': resolvePath( + process.env.PATHS_PARITY_CHECKS ?? ('/boot/config/parity-checks.log' as const) + ), htpasswd: '/etc/nginx/htpasswd' as const, 'emhttpd-socket': '/var/run/emhttpd.socket' as const, states: resolvePath(process.env.PATHS_STATES ?? ('/usr/local/emhttp/state/' as const)), diff --git a/api/src/store/state-parsers/shares.ts b/api/src/store/state-parsers/shares.ts index 4dc7315aa..01cdc3b6e 100644 --- a/api/src/store/state-parsers/shares.ts +++ b/api/src/store/state-parsers/shares.ts @@ -1,6 +1,6 @@ import type { StateFileToIniParserMap } from '@app/store/types.js'; import { toNumberOrNullConvert } from '@app/core/utils/casting.js'; -import { type Share } from '@app/graphql/generated/api/types.js'; +import { Share } from '@app/unraid-api/graph/resolvers/array/array.model.js'; export type SharesIni = Array<{ name: string; @@ -16,7 +16,8 @@ export const parse: StateFileToIniParserMap['shares'] = (state) => Object.values(state).map((item) => { const { name, free, used, size, include, exclude, useCache, ...rest } = item; const share: Share = { - name: name ?? '', + id: name, + name: name ?? null, free: toNumberOrNullConvert(free, { startingUnit: 'KiB', diff --git a/api/src/store/state-parsers/slots.ts b/api/src/store/state-parsers/slots.ts index 76cd3be29..0eb1583f4 100644 --- a/api/src/store/state-parsers/slots.ts +++ b/api/src/store/state-parsers/slots.ts @@ -1,8 +1,11 @@ -import type { ArrayDisk } from '@app/graphql/generated/api/types.js'; import type { StateFileToIniParserMap } from '@app/store/types.js'; import { type IniEnabled, type IniNumberBoolean } from '@app/core/types/ini.js'; import { toBoolean, toNumber, toNumberOrNull, toNumberOrNullConvert } from '@app/core/utils/index.js'; -import { ArrayDiskStatus, ArrayDiskType } from '@app/graphql/generated/api/types.js'; +import { + ArrayDisk, + ArrayDiskStatus, + ArrayDiskType, +} from '@app/unraid-api/graph/resolvers/array/array.model.js'; type SlotStatus = 'DISK_OK'; type SlotFsStatus = 'Mounted'; diff --git a/api/src/store/state-parsers/var.ts b/api/src/store/state-parsers/var.ts index 4f961a09d..a0813d09b 100644 --- a/api/src/store/state-parsers/var.ts +++ b/api/src/store/state-parsers/var.ts @@ -1,13 +1,13 @@ import type { StateFileToIniParserMap } from '@app/store/types.js'; import { type IniStringBoolean, type IniStringBooleanOrAuto } from '@app/core/types/ini.js'; import { toNumber } from '@app/core/utils/index.js'; +import { ArrayState } from '@app/unraid-api/graph/resolvers/array/array.model.js'; +import { DiskFsType } from '@app/unraid-api/graph/resolvers/disks/disks.model.js'; import { - ArrayState, - ConfigErrorState, - DiskFsType, RegistrationState, - registrationType, -} from '@app/graphql/generated/api/types.js'; + RegistrationType, +} from '@app/unraid-api/graph/resolvers/registration/registration.model.js'; +import { ConfigErrorState } from '@app/unraid-api/graph/resolvers/vars/vars.model.js'; /** * Unraid registration check @@ -248,7 +248,7 @@ export const parse: StateFileToIniParserMap['var'] = (iniFile) => { portssl: toNumber(iniFile.portssl), porttelnet: toNumber(iniFile.porttelnet), regCheck: iniFile.regCheck === '' ? 'Valid' : 'Error', - regTy: registrationType[iniFile.regTy?.toUpperCase()] ?? registrationType.INVALID, + regTy: RegistrationType[iniFile.regTy?.toUpperCase()] ?? RegistrationType.INVALID, regExp: iniFile.regExp ?? null, // Make sure to use a || not a ?? as regCheck can be an empty string regState: diff --git a/api/src/store/types.ts b/api/src/store/types.ts index a2e9df0e7..b8abe2406 100644 --- a/api/src/store/types.ts +++ b/api/src/store/types.ts @@ -1,6 +1,5 @@ import type { Subscription } from 'zen-observable-ts'; -import type { ArrayDisk, Share } from '@app/graphql/generated/api/types.js'; import type { RootState } from '@app/store/index.js'; import { type Devices } from '@app/core/types/states/devices.js'; import { type Networks } from '@app/core/types/states/network.js'; @@ -9,7 +8,6 @@ import { type Nginx } from '@app/core/types/states/nginx.js'; import { type SmbShares } from '@app/core/types/states/smb.js'; import { type Users } from '@app/core/types/states/user.js'; import { type Var } from '@app/core/types/states/var.js'; -import { MinigraphStatus } from '@app/graphql/generated/api/types.js'; import { type DevicesIni } from '@app/store/state-parsers/devices.js'; import { type NetworkIni } from '@app/store/state-parsers/network.js'; import { type NfsSharesIni } from '@app/store/state-parsers/nfs.js'; @@ -19,6 +17,8 @@ import { type SlotsIni } from '@app/store/state-parsers/slots.js'; import { type SmbIni } from '@app/store/state-parsers/smb.js'; import { type UsersIni } from '@app/store/state-parsers/users.js'; import { type VarIni } from '@app/store/state-parsers/var.js'; +import { ArrayDisk, Share } from '@app/unraid-api/graph/resolvers/array/array.model.js'; +import { MinigraphStatus } from '@app/unraid-api/graph/resolvers/cloud/cloud.model.js'; export enum FileLoadStatus { UNLOADED = 'UNLOADED', diff --git a/api/src/types/my-servers-config.ts b/api/src/types/my-servers-config.ts index 0bff49aad..a4afcfa38 100644 --- a/api/src/types/my-servers-config.ts +++ b/api/src/types/my-servers-config.ts @@ -1,6 +1,7 @@ import { z } from 'zod'; -import { DynamicRemoteAccessType, MinigraphStatus } from '@app/graphql/generated/api/types.js'; +import { MinigraphStatus } from '@app/unraid-api/graph/resolvers/cloud/cloud.model.js'; +import { DynamicRemoteAccessType } from '@app/unraid-api/graph/resolvers/connect/connect.model.js'; // Define Zod schemas const ApiConfigSchema = z.object({ diff --git a/api/src/unraid-api/auth/api-key.service.spec.ts b/api/src/unraid-api/auth/api-key.service.spec.ts index 537f3387c..cc83fb494 100644 --- a/api/src/unraid-api/auth/api-key.service.spec.ts +++ b/api/src/unraid-api/auth/api-key.service.spec.ts @@ -3,17 +3,20 @@ import { readdir, readFile, writeFile } from 'fs/promises'; import { join } from 'path'; import { ensureDir, ensureDirSync } from 'fs-extra'; +import { AuthActionVerb } from 'nest-authz'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; -import { ZodError } from 'zod'; -import type { ApiKey, ApiKeyWithSecret } from '@app/graphql/generated/api/types.js'; import { environment } from '@app/environment.js'; -import { ApiKeySchema, ApiKeyWithSecretSchema } from '@app/graphql/generated/api/operations.js'; -import { AuthActionVerb, Resource, Role } from '@app/graphql/generated/api/types.js'; import { getters, store } from '@app/store/index.js'; import { updateUserConfig } from '@app/store/modules/config.js'; import { FileLoadStatus } from '@app/store/types.js'; import { ApiKeyService } from '@app/unraid-api/auth/api-key.service.js'; +import { + ApiKey, + ApiKeyWithSecret, + Permission, +} from '@app/unraid-api/graph/resolvers/api-key/api-key.model.js'; +import { Resource, Role } from '@app/unraid-api/graph/resolvers/base.model.js'; // Mock the store and its modules vi.mock('@app/store/index.js', () => ({ @@ -43,11 +46,6 @@ vi.mock('fs/promises', async () => ({ writeFile: vi.fn(), })); -vi.mock('@app/graphql/generated/api/operations.js', () => ({ - ApiKeyWithSecretSchema: vi.fn(), - ApiKeySchema: vi.fn(), -})); - vi.mock('fs-extra', () => ({ ensureDir: vi.fn(), ensureDirSync: vi.fn(), @@ -69,7 +67,12 @@ describe('ApiKeyService', () => { name: 'Test API Key', description: 'Test API Key Description', roles: [Role.GUEST], - permissions: [], + permissions: [ + { + resource: Resource.CONNECT, + actions: [AuthActionVerb.READ], + }, + ], createdAt: new Date().toISOString(), }; @@ -131,14 +134,6 @@ describe('ApiKeyService', () => { vi.mock('uuid', () => ({ v4: () => 'test-api-id', })); - - // Add default schema mocks - vi.mocked(ApiKeyWithSecretSchema).mockReturnValue({ - parse: vi.fn().mockImplementation((data) => data), - } as any); - vi.mocked(ApiKeySchema).mockReturnValue({ - parse: vi.fn().mockImplementation((data) => data), - } as any); }); afterEach(() => { @@ -297,15 +292,33 @@ describe('ApiKeyService', () => { ]); await apiKeyService.onModuleInit(); - vi.mocked(ApiKeySchema).mockReturnValue({ - parse: vi.fn().mockReturnValue(mockApiKey), - } as any); - const result = await apiKeyService.findAll(); - expect(result).toHaveLength(2); - expect(result[0]).toEqual(mockApiKey); - expect(result[1]).toEqual(mockApiKey); + + const expectedApiKey1 = { + ...mockApiKey, + id: 'test-api-id', + permissions: [ + { + resource: Resource.CONNECT, + actions: [AuthActionVerb.READ], + }, + ], + }; + + const expectedApiKey2 = { + ...mockApiKey, + id: 'second-id', + permissions: [ + { + resource: Resource.CONNECT, + actions: [AuthActionVerb.READ], + }, + ], + }; + + expect(result[0]).toEqual(expectedApiKey1); + expect(result[1]).toEqual(expectedApiKey2); }); it('should handle file read errors gracefully', async () => { @@ -319,10 +332,6 @@ describe('ApiKeyService', () => { vi.spyOn(apiKeyService, 'loadAllFromDisk').mockResolvedValue([mockApiKeyWithSecret]); await apiKeyService.onModuleInit(); - vi.mocked(ApiKeySchema).mockReturnValue({ - parse: vi.fn().mockReturnValue(mockApiKey), - } as any); - const result = await apiKeyService.findById(mockApiKeyWithSecret.id); expect(result).toEqual(mockApiKey); @@ -338,27 +347,6 @@ describe('ApiKeyService', () => { expect(result).toBeNull(); }); - - it('should throw error if schema validation fails', async () => { - vi.spyOn(apiKeyService, 'loadAllFromDisk').mockResolvedValue([mockApiKeyWithSecret]); - await apiKeyService.onModuleInit(); - - vi.mocked(ApiKeySchema).mockReturnValue({ - parse: vi.fn().mockImplementation(() => { - throw new ZodError([ - { - code: 'custom', - path: ['roles'], - message: 'Invalid role', - }, - ]); - }), - } as any); - - expect(() => apiKeyService.findById(mockApiKeyWithSecret.id)).toThrow( - 'Invalid API key structure' - ); - }); }); describe('findByIdWithSecret', () => { @@ -404,10 +392,6 @@ describe('ApiKeyService', () => { await apiKeyService.onModuleInit(); - vi.mocked(ApiKeyWithSecretSchema).mockReturnValue({ - parse: vi.fn().mockImplementation((data) => data), - } as any); - const result = await apiKeyService.findByKey(mockApiKeyWithSecret.key); expect(result).toEqual(mockApiKeyWithSecret); @@ -420,10 +404,6 @@ describe('ApiKeyService', () => { ]); await apiKeyService.onModuleInit(); - vi.mocked(ApiKeyWithSecretSchema).mockReturnValue({ - parse: vi.fn().mockImplementation((data) => data), - } as any); - const result = await apiKeyService.findByKey('non-existent-key'); expect(result).toBeNull(); @@ -442,10 +422,6 @@ describe('ApiKeyService', () => { describe('saveApiKey', () => { it('should save API key to file', async () => { - vi.mocked(ApiKeyWithSecretSchema).mockReturnValue({ - parse: vi.fn().mockReturnValue(mockApiKeyWithSecret), - } as any); - vi.mocked(writeFile).mockResolvedValue(undefined); await apiKeyService.saveApiKey(mockApiKeyWithSecret); @@ -469,10 +445,6 @@ describe('ApiKeyService', () => { }); it('should throw GraphQLError on write error', async () => { - vi.mocked(ApiKeyWithSecretSchema).mockReturnValue({ - parse: vi.fn().mockReturnValue(mockApiKeyWithSecret), - } as any); - vi.mocked(writeFile).mockRejectedValue(new Error('Write failed')); await expect(apiKeyService.saveApiKey(mockApiKeyWithSecret)).rejects.toThrow( @@ -481,18 +453,6 @@ describe('ApiKeyService', () => { }); it('should throw GraphQLError on invalid API key structure', async () => { - vi.mocked(ApiKeyWithSecretSchema).mockReturnValue({ - parse: vi.fn().mockImplementation(() => { - throw new ZodError([ - { - code: 'custom', - path: ['name'], - message: 'Name cannot be empty', - }, - ]); - }), - } as any); - const invalidApiKey = { ...mockApiKeyWithSecret, name: '', // Invalid: name cannot be empty @@ -503,26 +463,15 @@ describe('ApiKeyService', () => { ); }); - it('should throw GraphQLError when roles array is empty', async () => { - vi.mocked(ApiKeyWithSecretSchema).mockReturnValue({ - parse: vi.fn().mockImplementation(() => { - throw new ZodError([ - { - code: 'custom', - path: ['roles'], - message: 'Roles array cannot be empty', - }, - ]); - }), - } as any); - + it('should throw GraphQLError when roles and permissions array is empty', async () => { const invalidApiKey = { ...mockApiKeyWithSecret, - roles: [], // Invalid: roles cannot be empty + permissions: [], + roles: [], } as ApiKeyWithSecret; await expect(apiKeyService.saveApiKey(invalidApiKey)).rejects.toThrow( - 'Failed to save API key: Invalid data structure' + 'At least one of permissions or roles must be specified' ); }); }); @@ -533,9 +482,6 @@ describe('ApiKeyService', () => { vi.mocked(readdir).mockResolvedValue(mockFiles as any); vi.mocked(readFile).mockResolvedValue(JSON.stringify(mockApiKeyWithSecret)); - vi.mocked(ApiKeyWithSecretSchema).mockReturnValue({ - parse: vi.fn().mockReturnValue(mockApiKeyWithSecret), - } as any); const result = await apiKeyService.loadAllFromDisk(); @@ -553,14 +499,41 @@ describe('ApiKeyService', () => { expect.stringContaining('Failed to read API key directory') ); }); + + it('should ignore invalid API Key files when loading from disk', async () => { + vi.mocked(readdir).mockResolvedValue([ + 'key1.json', + 'badkey.json', + 'key2.json', + 'notakey.txt', + ] as any); + vi.mocked(readFile) + .mockResolvedValueOnce(JSON.stringify(mockApiKeyWithSecret)) + .mockResolvedValueOnce(JSON.stringify({ invalid: 'structure' })) + .mockResolvedValueOnce( + JSON.stringify({ ...mockApiKeyWithSecret, id: 'unique-id', key: 'unique-key' }) + ) + .mockResolvedValueOnce( + JSON.stringify({ ...mockApiKeyWithSecret, id: 'unique-id', key: 'unique-key' }) + ); + const result = await apiKeyService.loadAllFromDisk(); + expect(result).toHaveLength(2); + expect(result[0]).toEqual({ + ...mockApiKey, + id: 'test-api-id', + key: 'test-api-key', + }); + expect(result[1]).toEqual({ + ...mockApiKey, + id: 'unique-id', + key: 'unique-key', + }); + }); }); describe('loadApiKeyFile', () => { it('should load and parse a valid API key file', async () => { vi.mocked(readFile).mockResolvedValue(JSON.stringify(mockApiKeyWithSecret)); - vi.mocked(ApiKeyWithSecretSchema).mockReturnValue({ - parse: vi.fn().mockReturnValue(mockApiKeyWithSecret), - } as any); const result = await apiKeyService['loadApiKeyFile']('test.json'); @@ -589,24 +562,24 @@ describe('ApiKeyService', () => { it('should throw error on invalid API key structure', async () => { vi.mocked(readFile).mockResolvedValue(JSON.stringify({ invalid: 'structure' })); - vi.mocked(ApiKeyWithSecretSchema).mockReturnValue({ - parse: vi.fn().mockImplementation(() => { - throw new ZodError([ - { - code: 'custom', - path: [], - message: 'Invalid structure', - }, - ]); - }), - } as any); - await expect( - apiKeyService['loadApiKeyFile']('test.json') - ).rejects.toThrowErrorMatchingInlineSnapshot(`[Error: Invalid API key structure]`); + await expect(apiKeyService['loadApiKeyFile']('test.json')).rejects.toThrow( + 'Invalid API key structure' + ); expect(mockLogger.error).toHaveBeenCalledWith( - expect.stringContaining('Invalid API key structure in file test.json') + expect.stringContaining('Error validating API key file test.json') + ); + expect(mockLogger.error).toHaveBeenCalledWith( + expect.stringContaining('An instance of ApiKeyWithSecret has failed the validation') + ); + expect(mockLogger.error).toHaveBeenCalledWith(expect.stringContaining('property key')); + expect(mockLogger.error).toHaveBeenCalledWith(expect.stringContaining('property id')); + expect(mockLogger.error).toHaveBeenCalledWith(expect.stringContaining('property name')); + expect(mockLogger.error).toHaveBeenCalledWith(expect.stringContaining('property roles')); + expect(mockLogger.error).toHaveBeenCalledWith(expect.stringContaining('property createdAt')); + expect(mockLogger.error).toHaveBeenCalledWith( + expect.stringContaining('property permissions') ); }); }); diff --git a/api/src/unraid-api/auth/api-key.service.ts b/api/src/unraid-api/auth/api-key.service.ts index dbb5ff9a4..5b8f8a744 100644 --- a/api/src/unraid-api/auth/api-key.service.ts +++ b/api/src/unraid-api/auth/api-key.service.ts @@ -4,25 +4,24 @@ import { readdir, readFile, unlink, writeFile } from 'fs/promises'; import { join } from 'path'; import { watch } from 'chokidar'; +import { ValidationError } from 'class-validator'; import { ensureDirSync } from 'fs-extra'; import { GraphQLError } from 'graphql'; import { AuthActionVerb } from 'nest-authz'; import { v4 as uuidv4 } from 'uuid'; -import { ZodError } from 'zod'; -import type { Permission } from '@app/graphql/generated/api/types.js'; import { environment } from '@app/environment.js'; -import { ApiKeySchema, ApiKeyWithSecretSchema } from '@app/graphql/generated/api/operations.js'; +import { getters, store } from '@app/store/index.js'; +import { setLocalApiKey } from '@app/store/modules/config.js'; +import { FileLoadStatus } from '@app/store/types.js'; import { AddPermissionInput, ApiKey, ApiKeyWithSecret, - Resource, - Role, -} from '@app/graphql/generated/api/types.js'; -import { getters, store } from '@app/store/index.js'; -import { setLocalApiKey } from '@app/store/modules/config.js'; -import { FileLoadStatus } from '@app/store/types.js'; + Permission, +} from '@app/unraid-api/graph/resolvers/api-key/api-key.model.js'; +import { Resource, Role } from '@app/unraid-api/graph/resolvers/base.model.js'; +import { validateObject } from '@app/unraid-api/graph/resolvers/validation.utils.js'; import { batchProcess } from '@app/utils.js'; @Injectable() @@ -45,8 +44,18 @@ export class ApiKeyService implements OnModuleInit { } } - public findAll(): ApiKey[] { - return this.memoryApiKeys.map((key) => ApiKeySchema().parse(key)); + public convertApiKeyWithSecretToApiKey(key: ApiKeyWithSecret): ApiKey { + const { key: _, ...rest } = key; + return rest; + } + + public async findAll(): Promise { + return Promise.all( + this.memoryApiKeys.map(async (key) => { + const keyWithoutSecret = this.convertApiKeyWithSecretToApiKey(key); + return keyWithoutSecret; + }) + ); } private setupWatch() { @@ -211,24 +220,30 @@ export class ApiKeyService implements OnModuleInit { return apiKeys; } + /** + * Loads an API key file from the disk and validates it + * @param file The file to load + * @returns The API key with secret + */ private async loadApiKeyFile(file: string): Promise { try { const content = await readFile(join(this.basePath, file), 'utf8'); // First convert all the strings in roles and permissions to uppercase (this ensures that casing is never an issue) const parsedContent = JSON.parse(content); + if (parsedContent.roles) { parsedContent.roles = parsedContent.roles.map((role: string) => role.toUpperCase()); } - return ApiKeyWithSecretSchema().parse(parsedContent); + return await validateObject(ApiKeyWithSecret, parsedContent); } catch (error) { if (error instanceof SyntaxError) { this.logger.error(`Corrupted key file: ${file}`); throw new Error('Authentication system error: Corrupted key file'); } - if (error instanceof ZodError) { - this.logApiKeyZodError(file, error); + if (error instanceof ValidationError) { + this.logger.error(`Error validating API key file ${file}: ${error}`); throw new Error('Invalid API key structure'); } @@ -238,17 +253,17 @@ export class ApiKeyService implements OnModuleInit { } } - findById(id: string): ApiKey | null { + async findById(id: string): Promise { try { const key = this.findByField('id', id); if (key) { - return ApiKeySchema().parse(key); + return this.convertApiKeyWithSecretToApiKey(key); } return null; } catch (error) { - if (error instanceof ZodError) { - this.logApiKeyZodError(id, error); + if (error instanceof ValidationError) { + this.logApiKeyValidationError(id, error); throw new Error('Invalid API key structure'); } throw error; @@ -273,9 +288,9 @@ export class ApiKeyService implements OnModuleInit { return crypto.randomBytes(32).toString('hex'); } - private logApiKeyZodError(file: string, error: ZodError): void { + private logApiKeyValidationError(file: string, error: ValidationError): void { this.logger.error(`Invalid API key structure in file ${file}. - Errors: ${JSON.stringify(error.errors, null, 2)}`); + Errors: ${JSON.stringify(error.constraints, null, 2)}`); } public async createLocalConnectApiKey(): Promise { @@ -294,7 +309,10 @@ export class ApiKeyService implements OnModuleInit { public async saveApiKey(apiKey: ApiKeyWithSecret): Promise { try { - const validatedApiKey = ApiKeyWithSecretSchema().parse(apiKey); + const validatedApiKey = await validateObject(ApiKeyWithSecret, apiKey); + if (!validatedApiKey.permissions?.length && !validatedApiKey.roles?.length) { + throw new GraphQLError('At least one of permissions or roles must be specified'); + } const sortedApiKey = Object.keys(validatedApiKey) .sort() @@ -308,8 +326,8 @@ export class ApiKeyService implements OnModuleInit { JSON.stringify(sortedApiKey, null, 2) ); } catch (error: unknown) { - if (error instanceof ZodError) { - this.logApiKeyZodError(apiKey.id, error); + if (error instanceof ValidationError) { + this.logApiKeyValidationError(apiKey.id, error); throw new GraphQLError('Failed to save API key: Invalid data structure'); } else if (error instanceof Error) { throw new GraphQLError(`Failed to save API key: ${error.message}`); diff --git a/api/src/unraid-api/auth/auth.service.spec.ts b/api/src/unraid-api/auth/auth.service.spec.ts index 47b18a9c7..4d12ca095 100644 --- a/api/src/unraid-api/auth/auth.service.spec.ts +++ b/api/src/unraid-api/auth/auth.service.spec.ts @@ -4,11 +4,12 @@ import { newEnforcer } from 'casbin'; import { AuthActionVerb, AuthZService } from 'nest-authz'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; -import type { ApiKey, ApiKeyWithSecret, UserAccount } from '@app/graphql/generated/api/types.js'; -import { Resource, Role } from '@app/graphql/generated/api/types.js'; import { ApiKeyService } from '@app/unraid-api/auth/api-key.service.js'; import { AuthService } from '@app/unraid-api/auth/auth.service.js'; import { CookieService } from '@app/unraid-api/auth/cookie.service.js'; +import { ApiKey, ApiKeyWithSecret } from '@app/unraid-api/graph/resolvers/api-key/api-key.model.js'; +import { Resource, Role } from '@app/unraid-api/graph/resolvers/base.model.js'; +import { UserAccount } from '@app/unraid-api/graph/user/user.model.js'; import { FastifyRequest } from '@app/unraid-api/types/fastify.js'; describe('AuthService', () => { @@ -18,7 +19,6 @@ describe('AuthService', () => { let cookieService: CookieService; const mockApiKey: ApiKey = { - __typename: 'ApiKey', id: '10f356da-1e9e-43b8-9028-a26a645539a6', name: 'Test API Key', description: 'Test API Key Description', diff --git a/api/src/unraid-api/auth/auth.service.ts b/api/src/unraid-api/auth/auth.service.ts index 9d8882191..526c8f9e8 100644 --- a/api/src/unraid-api/auth/auth.service.ts +++ b/api/src/unraid-api/auth/auth.service.ts @@ -2,11 +2,12 @@ import { Injectable, Logger, UnauthorizedException } from '@nestjs/common'; import { AuthZService } from 'nest-authz'; -import type { Permission, UserAccount } from '@app/graphql/generated/api/types.js'; -import { Role } from '@app/graphql/generated/api/types.js'; import { getters } from '@app/store/index.js'; import { ApiKeyService } from '@app/unraid-api/auth/api-key.service.js'; import { CookieService } from '@app/unraid-api/auth/cookie.service.js'; +import { Permission } from '@app/unraid-api/graph/resolvers/api-key/api-key.model.js'; +import { Role } from '@app/unraid-api/graph/resolvers/base.model.js'; +import { UserAccount } from '@app/unraid-api/graph/user/user.model.js'; import { FastifyRequest } from '@app/unraid-api/types/fastify.js'; import { batchProcess, handleAuthError } from '@app/utils.js'; diff --git a/api/src/unraid-api/auth/casbin/policy.ts b/api/src/unraid-api/auth/casbin/policy.ts index 8aff35436..b0f7d88ca 100644 --- a/api/src/unraid-api/auth/casbin/policy.ts +++ b/api/src/unraid-api/auth/casbin/policy.ts @@ -1,6 +1,6 @@ import { AuthAction } from 'nest-authz'; -import { Resource, Role } from '@app/graphql/generated/api/types.js'; +import { Resource, Role } from '@app/unraid-api/graph/resolvers/base.model.js'; export const BASE_POLICY = ` # Admin permissions diff --git a/api/src/unraid-api/auth/header.strategy.ts b/api/src/unraid-api/auth/header.strategy.ts index 8b991e7c6..77cc897ad 100644 --- a/api/src/unraid-api/auth/header.strategy.ts +++ b/api/src/unraid-api/auth/header.strategy.ts @@ -3,8 +3,8 @@ import { PassportStrategy } from '@nestjs/passport'; import { Strategy } from 'passport-http-header-strategy'; -import { User } from '@app/graphql/generated/api/types.js'; import { AuthService } from '@app/unraid-api/auth/auth.service.js'; +import { UserAccount } from '@app/unraid-api/graph/user/user.model.js'; @Injectable() export class ServerHeaderStrategy extends PassportStrategy(Strategy, 'server-http-header') { @@ -18,7 +18,7 @@ export class ServerHeaderStrategy extends PassportStrategy(Strategy, 'server-htt }); } - async validate(req: any): Promise { + async validate(req: any): Promise { const request = req.req || req; const key = request.headers?.['x-api-key']; diff --git a/api/src/unraid-api/auth/user.decorator.ts b/api/src/unraid-api/auth/user.decorator.ts index 340fd9663..94990abdf 100644 --- a/api/src/unraid-api/auth/user.decorator.ts +++ b/api/src/unraid-api/auth/user.decorator.ts @@ -1,22 +1,14 @@ import { createParamDecorator, ExecutionContext } from '@nestjs/common'; import { GqlContextType, GqlExecutionContext } from '@nestjs/graphql'; -import { UserSchema } from '@app/graphql/generated/api/operations.js'; -import { UserAccount } from '@app/graphql/generated/api/types.js'; +import { UserAccount } from '@app/unraid-api/graph/user/user.model.js'; export const GraphqlUser = createParamDecorator( (data: null, context: ExecutionContext): UserAccount => { if (context.getType() === 'graphql') { const ctx = GqlExecutionContext.create(context); const user = ctx.getContext().req.user; - - const result = UserSchema().safeParse(user); - - if (!result.success) { - throw new Error('Invalid user account structure'); - } - - return result.data; + return user; } else { return context.switchToHttp().getRequest().user; } diff --git a/api/src/unraid-api/cli/apikey/add-api-key.questions.ts b/api/src/unraid-api/cli/apikey/add-api-key.questions.ts index 79508b8c5..b7a4c9243 100644 --- a/api/src/unraid-api/cli/apikey/add-api-key.questions.ts +++ b/api/src/unraid-api/cli/apikey/add-api-key.questions.ts @@ -1,9 +1,9 @@ import { ChoicesFor, Question, QuestionSet, WhenFor } from 'nest-commander'; -import type { Permission } from '@app/graphql/generated/api/types.js'; -import { Role } from '@app/graphql/generated/api/types.js'; import { ApiKeyService } from '@app/unraid-api/auth/api-key.service.js'; import { LogService } from '@app/unraid-api/cli/log.service.js'; +import { Permission } from '@app/unraid-api/graph/resolvers/api-key/api-key.model.js'; +import { Role } from '@app/unraid-api/graph/resolvers/base.model.js'; @QuestionSet({ name: 'add-api-key' }) export class AddApiKeyQuestionSet { diff --git a/api/src/unraid-api/cli/apikey/api-key.command.ts b/api/src/unraid-api/cli/apikey/api-key.command.ts index 340488c2a..12d1dae94 100644 --- a/api/src/unraid-api/cli/apikey/api-key.command.ts +++ b/api/src/unraid-api/cli/apikey/api-key.command.ts @@ -1,13 +1,13 @@ import { AuthActionVerb } from 'nest-authz'; import { Command, CommandRunner, InquirerService, Option } from 'nest-commander'; -import type { Permission } from '@app/graphql/generated/api/types.js'; import type { DeleteApiKeyAnswers } from '@app/unraid-api/cli/apikey/delete-api-key.questions.js'; -import { Resource, Role } from '@app/graphql/generated/api/types.js'; import { ApiKeyService } from '@app/unraid-api/auth/api-key.service.js'; import { AddApiKeyQuestionSet } from '@app/unraid-api/cli/apikey/add-api-key.questions.js'; import { DeleteApiKeyQuestionSet } from '@app/unraid-api/cli/apikey/delete-api-key.questions.js'; import { LogService } from '@app/unraid-api/cli/log.service.js'; +import { Permission } from '@app/unraid-api/graph/resolvers/api-key/api-key.model.js'; +import { Resource, Role } from '@app/unraid-api/graph/resolvers/base.model.js'; interface KeyOptions { name: string; @@ -101,7 +101,7 @@ ACTIONS: ${Object.values(AuthActionVerb).join(', ')}`, /** Prompt the user to select API keys to delete. Then, delete the selected keys. */ private async deleteKeys() { - const allKeys = this.apiKeyService.findAll(); + const allKeys = await this.apiKeyService.findAll(); if (allKeys.length === 0) { this.logger.log('No API keys found to delete'); return; diff --git a/api/src/unraid-api/cli/apikey/delete-api-key.questions.ts b/api/src/unraid-api/cli/apikey/delete-api-key.questions.ts index 74c1623fd..915eff368 100644 --- a/api/src/unraid-api/cli/apikey/delete-api-key.questions.ts +++ b/api/src/unraid-api/cli/apikey/delete-api-key.questions.ts @@ -27,7 +27,8 @@ export class DeleteApiKeyQuestionSet { @ChoicesFor({ name: 'selectedKeys' }) async getKeys() { - return this.apiKeyService.findAll().map((key) => ({ + const keys = await this.apiKeyService.findAll(); + return keys.map((key) => ({ name: `${key.name} (${key.description ?? ''}) [${key.id}]`, value: key.id, })); diff --git a/api/src/unraid-api/graph/README.md b/api/src/unraid-api/graph/README.md new file mode 100644 index 000000000..12a69ad18 --- /dev/null +++ b/api/src/unraid-api/graph/README.md @@ -0,0 +1,82 @@ +# GraphQL Schema Migration: Schema-First to Code-First + +This directory contains the GraphQL resolvers for the Unraid API. We are currently migrating from a schema-first approach to a code-first approach using NestJS decorators. + +## Migration Status + +We have started migrating the GraphQL schema from schema-first to code-first approach. The following resolvers have been migrated: + +- ✅ API Key Resolver + +The following resolvers still need to be migrated: + +- [ ] Docker Resolver +- [ ] Array Resolver +- [ ] Disks Resolver +- [ ] VMs Resolver +- [ ] Connect Resolver +- [ ] Display Resolver +- [ ] Info Resolver +- [ ] Owner Resolver +- [ ] Unassigned Devices Resolver +- [ ] Cloud Resolver +- [ ] Flash Resolver +- [ ] Config Resolver +- [ ] Vars Resolver +- [ ] Logs Resolver +- [ ] Users Resolver +- [ ] Notifications Resolver +- [ ] Network Resolver +- [ ] Registration Resolver +- [ ] Servers Resolver +- [ ] Services Resolver +- [ ] Shares Resolver + +## Migration Process + +For each resolver, we follow these steps: + +1. Create a model file (e.g., `resolver-name.model.ts`) +2. Define ObjectType classes for return types +3. Define InputType classes for input parameters +4. Update the resolver to use the new model classes +5. Update the resolver decorators to use the new model classes +6. Create a module file (e.g., `resolver-name.module.ts`) +7. Test the resolver to ensure it works correctly + +## Migration Tools + +We have created the following tools to help with the migration: + +- `migration-plan.md`: A detailed plan for migrating the GraphQL schema +- `migration-script.ts`: A script to help identify which resolvers need to be migrated + +## Example Migration + +See the API Key Resolver for an example of a migrated resolver: + +- `api-key.model.ts`: Contains the model classes for the API Key Resolver +- `api-key.resolver.ts`: Contains the resolver implementation using the model classes +- `api-key.module.ts`: Contains the module configuration for the API Key Resolver + +## Benefits of Code-First Approach + +The code-first approach offers several benefits: + +1. **Type Safety**: TypeScript types are used directly in the GraphQL schema +2. **Better IDE Support**: Better autocomplete and type checking +3. **Easier Refactoring**: Refactoring is easier as the types are defined in one place +4. **Better Documentation**: The schema is documented in the code +5. **Easier Testing**: Easier to test as the types are defined in the code + +## Next Steps + +1. Continue migrating the remaining resolvers +2. Update the GraphQL module configuration to use code-first approach +3. Remove the schema files once all resolvers are migrated + +## Resources + +- [NestJS GraphQL Documentation](https://docs.nestjs.com/graphql/quick-start) +- [GraphQL Code Generator](https://www.graphql-code-generator.com/) +- [TypeScript Documentation](https://www.typescriptlang.org/docs/) \ No newline at end of file diff --git a/api/src/unraid-api/graph/auth/auth.enums.ts b/api/src/unraid-api/graph/auth/auth.enums.ts new file mode 100644 index 000000000..5c9536e2e --- /dev/null +++ b/api/src/unraid-api/graph/auth/auth.enums.ts @@ -0,0 +1,52 @@ +import { DirectiveLocation, GraphQLDirective, GraphQLEnumType, GraphQLString } from 'graphql'; +import { AuthActionVerb, AuthPossession } from 'nest-authz'; + +// Create GraphQL enum types for auth action verbs and possessions +export const AuthActionVerbEnum = new GraphQLEnumType({ + name: 'AuthActionVerb', + description: 'Available authentication action verbs', + values: Object.entries(AuthActionVerb) + .filter(([key]) => isNaN(Number(key))) // Filter out numeric keys + .reduce( + (acc, [key]) => { + acc[key] = { value: key }; + return acc; + }, + {} as Record + ), +}); + +export const AuthPossessionEnum = new GraphQLEnumType({ + name: 'AuthPossession', + description: 'Available authentication possession types', + values: Object.entries(AuthPossession) + .filter(([key]) => isNaN(Number(key))) // Filter out numeric keys + .reduce( + (acc, [key]) => { + acc[key] = { value: key }; + return acc; + }, + {} as Record + ), +}); + +// Create the auth directive +export const AuthDirective = new GraphQLDirective({ + name: 'auth', + description: 'Directive to control access to fields based on authentication', + locations: [DirectiveLocation.FIELD_DEFINITION], + args: { + action: { + type: AuthActionVerbEnum, + description: 'The action verb required for access', + }, + resource: { + type: GraphQLString, + description: 'The resource required for access', + }, + possession: { + type: AuthPossessionEnum, + description: 'The possession type required for access', + }, + }, +}); diff --git a/api/src/unraid-api/graph/connect/connect.resolver.ts b/api/src/unraid-api/graph/connect/connect.resolver.ts deleted file mode 100644 index 7eb001c6f..000000000 --- a/api/src/unraid-api/graph/connect/connect.resolver.ts +++ /dev/null @@ -1,136 +0,0 @@ -import { Logger } from '@nestjs/common'; -import { Args, Mutation, Query, ResolveField, Resolver } from '@nestjs/graphql'; - -import { GraphQLError } from 'graphql'; -import { AuthActionVerb, AuthPossession, UsePermissions } from 'nest-authz'; - -import type { - ApiSettingsInput, - ConnectResolvers, - ConnectSettings, - DynamicRemoteAccessStatus, - EnableDynamicRemoteAccessInput, -} from '@app/graphql/generated/api/types.js'; -import { DynamicRemoteAccessType, Resource } from '@app/graphql/generated/api/types.js'; -import { RemoteAccessController } from '@app/remoteAccess/remote-access-controller.js'; -import { store } from '@app/store/index.js'; -import { setAllowedRemoteAccessUrl } from '@app/store/modules/dynamic-remote-access.js'; -import { ConnectSettingsService } from '@app/unraid-api/graph/connect/connect-settings.service.js'; -import { ConnectService } from '@app/unraid-api/graph/connect/connect.service.js'; - -@Resolver('Connect') -export class ConnectResolver implements ConnectResolvers { - protected logger = new Logger(ConnectResolver.name); - constructor( - private readonly connectSettingsService: ConnectSettingsService, - private readonly connectService: ConnectService - ) {} - - @Query('connect') - @UsePermissions({ - action: AuthActionVerb.READ, - resource: Resource.CONNECT, - possession: AuthPossession.ANY, - }) - public connect() { - return {}; - } - - @ResolveField() - public id() { - return 'connect'; - } - - @ResolveField() - public async settings(): Promise { - const { properties, elements } = await this.connectSettingsService.buildSettingsSchema(); - return { - id: 'connectSettingsForm', - dataSchema: { - type: 'object', - properties, - }, - uiSchema: { - type: 'VerticalLayout', - elements, - }, - values: await this.connectSettingsService.getCurrentSettings(), - }; - } - - @Mutation() - @UsePermissions({ - action: AuthActionVerb.UPDATE, - resource: Resource.CONFIG, - possession: AuthPossession.ANY, - }) - public async updateApiSettings(@Args('input') settings: ApiSettingsInput) { - this.logger.verbose(`Attempting to update API settings: ${JSON.stringify(settings, null, 2)}`); - const restartRequired = await this.connectSettingsService.syncSettings(settings); - const currentSettings = await this.connectSettingsService.getCurrentSettings(); - if (restartRequired) { - setTimeout(async () => { - // Send restart out of band to avoid blocking the return of this resolver - this.logger.log('Restarting API'); - await this.connectService.restartApi(); - }, 300); - } - return currentSettings; - } - - @ResolveField() - public dynamicRemoteAccess(): DynamicRemoteAccessStatus { - return { - runningType: store.getState().dynamicRemoteAccess.runningType, - enabledType: - store.getState().config.remote.dynamicRemoteAccessType ?? - DynamicRemoteAccessType.DISABLED, - error: store.getState().dynamicRemoteAccess.error, - }; - } - - @Mutation() - @UsePermissions({ - action: AuthActionVerb.UPDATE, - resource: Resource.CONNECT__REMOTE_ACCESS, - possession: AuthPossession.ANY, - }) - public async enableDynamicRemoteAccess( - @Args('input') dynamicRemoteAccessInput: EnableDynamicRemoteAccessInput - ): Promise { - // Start or extend dynamic remote access - const state = store.getState(); - - const { dynamicRemoteAccessType } = state.config.remote; - if (!dynamicRemoteAccessType || dynamicRemoteAccessType === DynamicRemoteAccessType.DISABLED) { - throw new GraphQLError('Dynamic Remote Access is not enabled.', { - extensions: { code: 'FORBIDDEN' }, - }); - } - - const controller = RemoteAccessController.instance; - - if (dynamicRemoteAccessInput.enabled === false) { - controller.stopRemoteAccess({ - getState: store.getState, - dispatch: store.dispatch, - }); - return true; - } else if (controller.getRunningRemoteAccessType() === DynamicRemoteAccessType.DISABLED) { - if (dynamicRemoteAccessInput.url) { - store.dispatch(setAllowedRemoteAccessUrl(dynamicRemoteAccessInput.url)); - } - controller.beginRemoteAccess({ - getState: store.getState, - dispatch: store.dispatch, - }); - } else { - controller.extendRemoteAccess({ - getState: store.getState, - dispatch: store.dispatch, - }); - } - - return true; - } -} diff --git a/api/src/unraid-api/graph/connect/connect.service.spec.ts b/api/src/unraid-api/graph/connect/connect.service.spec.ts deleted file mode 100644 index 03d239a83..000000000 --- a/api/src/unraid-api/graph/connect/connect.service.spec.ts +++ /dev/null @@ -1,22 +0,0 @@ -import type { TestingModule } from '@nestjs/testing'; -import { Test } from '@nestjs/testing'; - -import { beforeEach, describe, expect, it } from 'vitest'; - -import { ConnectService } from '@app/unraid-api/graph/connect/connect.service.js'; - -describe('ConnectService', () => { - let service: ConnectService; - - beforeEach(async () => { - const module: TestingModule = await Test.createTestingModule({ - providers: [ConnectService], - }).compile(); - - service = module.get(ConnectService); - }); - - it('should be defined', () => { - expect(service).toBeDefined(); - }); -}); diff --git a/api/src/unraid-api/graph/connect/connect.service.ts b/api/src/unraid-api/graph/connect/connect.service.ts deleted file mode 100644 index 36546d255..000000000 --- a/api/src/unraid-api/graph/connect/connect.service.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { Injectable, Logger } from '@nestjs/common'; - -import { execa } from 'execa'; - -@Injectable() -export class ConnectService { - private logger = new Logger(ConnectService.name); - async restartApi() { - try { - await execa('unraid-api', ['restart'], { shell: 'bash', stdio: 'ignore' }); - } catch (error) { - this.logger.error(error); - } - } -} diff --git a/api/src/unraid-api/graph/directives/auth.directive.ts b/api/src/unraid-api/graph/directives/auth.directive.ts index 056a0eca8..98686cdf9 100644 --- a/api/src/unraid-api/graph/directives/auth.directive.ts +++ b/api/src/unraid-api/graph/directives/auth.directive.ts @@ -4,7 +4,7 @@ import { getDirective, IResolvers, MapperKind, mapSchema } from '@graphql-tools/ import { GraphQLEnumType, GraphQLSchema } from 'graphql'; import { AuthActionVerb, AuthPossession, AuthZService, BatchApproval } from 'nest-authz'; -import { Resource } from '@app/graphql/generated/api/types.js'; +import { Resource } from '@app/unraid-api/graph/resolvers/base.model.js'; /** * @wip : This function does not correctly apply permission to every field. diff --git a/api/src/unraid-api/graph/graph.module.ts b/api/src/unraid-api/graph/graph.module.ts index 0283058e5..aeaa6f375 100644 --- a/api/src/unraid-api/graph/graph.module.ts +++ b/api/src/unraid-api/graph/graph.module.ts @@ -3,42 +3,31 @@ import { ApolloDriver } from '@nestjs/apollo'; import { Module } from '@nestjs/common'; import { GraphQLModule } from '@nestjs/graphql'; -import { NoUnusedVariablesRule, print } from 'graphql'; -import { - DateTimeResolver, - JSONResolver, - PortResolver, - URLResolver, - UUIDResolver, -} from 'graphql-scalars'; +import { NoUnusedVariablesRule } from 'graphql'; +import { JSONResolver, URLResolver } from 'graphql-scalars'; +import { ENVIRONMENT } from '@app/environment.js'; import { GraphQLLong } from '@app/graphql/resolvers/graphql-type-long.js'; -import { loadTypeDefs } from '@app/graphql/schema/loadTypesDefs.js'; import { getters } from '@app/store/index.js'; import { idPrefixPlugin } from '@app/unraid-api/graph/id-prefix-plugin.js'; import { ResolversModule } from '@app/unraid-api/graph/resolvers/resolvers.module.js'; import { sandboxPlugin } from '@app/unraid-api/graph/sandbox-plugin.js'; -import { getAuthEnumTypeDefs } from '@app/unraid-api/graph/utils/auth-enum.utils.js'; -import { PluginService } from '@app/unraid-api/plugin/plugin.service.js'; @Module({ imports: [ ResolversModule, GraphQLModule.forRootAsync({ driver: ApolloDriver, + imports: [], + inject: [], useFactory: async () => { - const pluginSchemas = await PluginService.getGraphQLSchemas(); - const authEnumTypeDefs = getAuthEnumTypeDefs(); - const typeDefs = print(await loadTypeDefs([...pluginSchemas, authEnumTypeDefs])); - const resolvers = { - DateTime: DateTimeResolver, - JSON: JSONResolver, - Long: GraphQLLong, - Port: PortResolver, - URL: URLResolver, - UUID: UUIDResolver, - }; return { + autoSchemaFile: + ENVIRONMENT === 'development' + ? { + path: './generated-schema.graphql', + } + : true, introspection: getters.config()?.local?.sandbox === 'yes', playground: false, context: async ({ req, connectionParams, extra }) => { @@ -54,13 +43,14 @@ import { PluginService } from '@app/unraid-api/plugin/plugin.service.js'; path: '/graphql', }, }, - typeDefs, - resolvers, - /** - * @todo : Once we've determined how to fix the transformResolvers function, uncomment this. - */ - // transformResolvers: (resolvers) => transformResolvers(resolvers, authZService), - // transformSchema: (schema) => authSchemaTransformer(schema), + resolvers: { + JSON: JSONResolver, + Long: GraphQLLong, + URL: URLResolver, + }, + buildSchemaOptions: { + dateScalarMode: 'isoDate', + }, validationRules: [NoUnusedVariablesRule], }; }, diff --git a/api/src/unraid-api/graph/migration-plan.md b/api/src/unraid-api/graph/migration-plan.md new file mode 100644 index 000000000..909338758 --- /dev/null +++ b/api/src/unraid-api/graph/migration-plan.md @@ -0,0 +1,121 @@ +# GraphQL Schema Migration Plan: Schema-First to Code-First + +## Overview + +This document outlines the plan to migrate the GraphQL schema from schema-first approach to code-first approach using NestJS decorators. + +## Migration Steps + +1. **Create Base Models** + - ✅ Create `base.model.ts` with common enums (Resource, Role) + - ✅ Register enums with `registerEnumType` + +2. **Migrate Each Resolver** + - ✅ API Key Resolver + - [ ] Docker Resolver + - [ ] Array Resolver + - [ ] Disks Resolver + - [ ] VMs Resolver + - [ ] Connect Resolver + - [ ] Display Resolver + - [ ] Info Resolver + - [ ] Owner Resolver + - [ ] Unassigned Devices Resolver + - [ ] Cloud Resolver + - [ ] Flash Resolver + - [ ] Config Resolver + - [ ] Vars Resolver + - [ ] Logs Resolver + - [ ] Users Resolver + - [ ] Notifications Resolver + - [ ] Network Resolver + - [ ] Registration Resolver + - [ ] Servers Resolver + - [ ] Services Resolver + - [ ] Shares Resolver + +3. **For Each Resolver**: + - Create a model file (e.g., `resolver-name.model.ts`) + - Define ObjectType classes for return types + - Define InputType classes for input parameters + - Update the resolver to use the new model classes + - Update the resolver decorators to use the new model classes + +4. **Update GraphQL Module Configuration** + - Remove schema loading from `loadTypesDefs.ts` + - Update the GraphQL module to use code-first approach + +5. **Remove Schema Files** + - Once all resolvers are migrated, remove the schema files + +## Example Migration Pattern + +For each resolver: + +1. Create model file: + ```typescript + // resolver-name.model.ts + import { Field, ID, InputType, ObjectType } from '@nestjs/graphql'; + + @ObjectType() + export class SomeType { + @Field(() => ID) + id!: string; + + // other fields + } + + @InputType() + export class SomeInput { + @Field() + name!: string; + + // other fields + } + ``` + +2. Update resolver: + ```typescript + // resolver-name.resolver.ts + import { Args, Mutation, Query, Resolver } from '@nestjs/graphql'; + import { SomeType, SomeInput } from './resolver-name.model'; + + @Resolver(() => SomeType) + export class SomeResolver { + @Query(() => [SomeType]) + async someQuery(): Promise { + // implementation + } + + @Mutation(() => SomeType) + async someMutation(@Args('input') input: SomeInput): Promise { + // implementation + } + } + ``` + +3. Create module file: + ```typescript + // resolver-name.module.ts + import { Module } from '@nestjs/common'; + import { SomeResolver } from './resolver-name.resolver'; + + @Module({ + providers: [SomeResolver], + exports: [SomeResolver], + }) + export class SomeModule {} + ``` + +## Testing + +After migrating each resolver: +1. Test the resolver to ensure it works correctly +2. Check for any type errors or runtime errors +3. Verify that the GraphQL schema is generated correctly + +## Rollback Plan + +If issues arise during migration: +1. Keep the schema files until the migration is complete and tested +2. If necessary, revert to schema-first approach by restoring the schema files and removing the code-first changes \ No newline at end of file diff --git a/api/src/unraid-api/graph/migration-script.ts b/api/src/unraid-api/graph/migration-script.ts new file mode 100644 index 000000000..960d49f97 --- /dev/null +++ b/api/src/unraid-api/graph/migration-script.ts @@ -0,0 +1,88 @@ +/** + * This script helps with the migration from schema-first to code-first approach. + * It identifies which resolvers need to be migrated and provides guidance on the migration process. + * + * To use this script: + * 1. Run it with Node.js: `node migration-script.js` + * 2. Follow the guidance provided by the script + */ + +import fs from 'fs'; +import path from 'path'; + +const __dirname = import.meta.dirname; +// Paths +const schemaDir = path.resolve(__dirname, '../../graphql/schema/types'); +const resolversDir = path.resolve(__dirname, './resolvers'); + +// Get all schema directories +const schemaDirs = fs + .readdirSync(schemaDir) + .filter((item) => fs.statSync(path.join(schemaDir, item)).isDirectory()) + .filter((item) => item !== 'array' && item !== 'disks'); // Exclude special cases + +// Get all resolver directories +const resolverDirs = fs + .readdirSync(resolversDir) + .filter((item) => fs.statSync(path.join(resolversDir, item)).isDirectory()); + +// Find resolvers that need to be migrated +const resolversToMigrate = schemaDirs.filter((dir) => { + const resolverDir = path.join(resolversDir, dir); + return ( + !fs.existsSync(resolverDir) || + !fs.readdirSync(resolverDir).some((file) => file.endsWith('.model.ts')) + ); +}); + +// Find resolvers that have already been migrated +const migratedResolvers = schemaDirs.filter((dir) => { + const resolverDir = path.join(resolversDir, dir); + return ( + fs.existsSync(resolverDir) && + fs.readdirSync(resolverDir).some((file) => file.endsWith('.model.ts')) + ); +}); + +// Print migration status +console.log('=== GraphQL Schema Migration Status ==='); +console.log(`Total schema directories: ${schemaDirs.length}`); +console.log(`Migrated resolvers: ${migratedResolvers.length}`); +console.log(`Resolvers to migrate: ${resolversToMigrate.length}`); + +// Print migrated resolvers +console.log('\n=== Migrated Resolvers ==='); +migratedResolvers.forEach((resolver) => { + console.log(`✅ ${resolver}`); +}); + +// Print resolvers to migrate +console.log('\n=== Resolvers to Migrate ==='); +resolversToMigrate.forEach((resolver) => { + console.log(`❌ ${resolver}`); +}); + +// Print migration guidance +console.log('\n=== Migration Guidance ==='); +console.log('For each resolver to migrate:'); +console.log('1. Create a model file (e.g., resolver-name.model.ts)'); +console.log('2. Define ObjectType classes for return types'); +console.log('3. Define InputType classes for input parameters'); +console.log('4. Update the resolver to use the new model classes'); +console.log('5. Update the resolver decorators to use the new model classes'); +console.log('6. Create a module file (e.g., resolver-name.module.ts)'); +console.log('7. Test the resolver to ensure it works correctly'); + +// Print example migration +console.log('\n=== Example Migration ==='); +console.log('See migration-plan.md for detailed examples'); + +// Print next steps +console.log('\n=== Next Steps ==='); +if (resolversToMigrate.length > 0) { + console.log(`Start migrating the ${resolversToMigrate.length} resolvers listed above`); +} else { + console.log('All resolvers have been migrated!'); + console.log('Next: Update the GraphQL module configuration to use code-first approach'); + console.log('Then: Remove the schema files'); +} diff --git a/api/src/unraid-api/graph/resolvers/api-key/api-key.model.ts b/api/src/unraid-api/graph/resolvers/api-key/api-key.model.ts new file mode 100644 index 000000000..2807ea9b4 --- /dev/null +++ b/api/src/unraid-api/graph/resolvers/api-key/api-key.model.ts @@ -0,0 +1,140 @@ +import { Field, ID, InputType, ObjectType } from '@nestjs/graphql'; + +import { Transform, Type } from 'class-transformer'; +import { + ArrayMinSize, + IsArray, + IsBoolean, + IsDate, + IsEnum, + IsNotEmpty, + IsOptional, + IsString, + ValidateIf, + ValidateNested, +} from 'class-validator'; + +import { Resource, Role } from '@app/unraid-api/graph/resolvers/base.model.js'; + +@ObjectType() +export class Permission { + @Field(() => Resource) + @IsEnum(Resource) + resource!: Resource; + + @Field(() => [String]) + @IsArray() + @IsString({ each: true }) + @ArrayMinSize(1) + actions!: string[]; +} + +@ObjectType() +export class ApiKey { + @Field(() => ID) + @IsString() + @IsNotEmpty() + id!: string; + + @Field() + @IsString() + @IsNotEmpty() + name!: string; + + @Field({ nullable: true }) + @IsString() + @IsOptional() + description?: string; + + @Field(() => [Role]) + @IsArray() + @IsEnum(Role, { each: true }) + roles!: Role[]; + + @Field() + @IsString() + @Transform(({ value }) => (value instanceof Date ? value.toISOString() : value)) + createdAt!: string; + + @Field(() => [Permission]) + @IsArray() + @ValidateNested({ each: true }) + @Type(() => Permission) + permissions!: Permission[]; +} + +@ObjectType() +export class ApiKeyWithSecret extends ApiKey { + @Field() + @IsString() + key!: string; +} + +@InputType() +export class AddPermissionInput { + @Field(() => Resource) + @IsEnum(Resource) + resource!: Resource; + + @Field(() => [String]) + @IsArray() + @IsString({ each: true }) + @ArrayMinSize(1) + actions!: string[]; +} + +@InputType() +export class CreateApiKeyInput { + @Field() + @IsString() + name!: string; + + @Field({ nullable: true }) + @IsString() + @IsOptional() + description?: string; + + @Field(() => [Role], { nullable: true }) + @IsArray() + @IsEnum(Role, { each: true }) + @IsOptional() + roles?: Role[]; + + @Field(() => [AddPermissionInput], { nullable: true }) + @IsArray() + @ValidateNested({ each: true }) + @Type(() => AddPermissionInput) + @IsOptional() + permissions?: AddPermissionInput[]; + + @Field({ + nullable: true, + description: + 'This will replace the existing key if one already exists with the same name, otherwise returns the existing key', + }) + @IsBoolean() + @IsOptional() + overwrite?: boolean; +} + +@InputType() +export class AddRoleForApiKeyInput { + @Field(() => ID) + @IsString() + apiKeyId!: string; + + @Field(() => Role) + @IsEnum(Role) + role!: Role; +} + +@InputType() +export class RemoveRoleFromApiKeyInput { + @Field(() => ID) + @IsString() + apiKeyId!: string; + + @Field(() => Role) + @IsEnum(Role) + role!: Role; +} diff --git a/api/src/unraid-api/graph/resolvers/api-key/api-key.module.ts b/api/src/unraid-api/graph/resolvers/api-key/api-key.module.ts new file mode 100644 index 000000000..3f26e3100 --- /dev/null +++ b/api/src/unraid-api/graph/resolvers/api-key/api-key.module.ts @@ -0,0 +1,11 @@ +import { Module } from '@nestjs/common'; + +import { ApiKeyService } from '@app/unraid-api/auth/api-key.service.js'; +import { AuthService } from '@app/unraid-api/auth/auth.service.js'; +import { ApiKeyResolver } from '@app/unraid-api/graph/resolvers/api-key/api-key.resolver.js'; + +@Module({ + providers: [ApiKeyResolver, ApiKeyService, AuthService], + exports: [ApiKeyResolver], +}) +export class ApiKeyModule {} diff --git a/api/src/unraid-api/graph/resolvers/api-key/api-key.resolver.spec.ts b/api/src/unraid-api/graph/resolvers/api-key/api-key.resolver.spec.ts index e709541ae..86b11715d 100644 --- a/api/src/unraid-api/graph/resolvers/api-key/api-key.resolver.spec.ts +++ b/api/src/unraid-api/graph/resolvers/api-key/api-key.resolver.spec.ts @@ -1,13 +1,13 @@ import { newEnforcer } from 'casbin'; -import { AuthActionVerb, AuthPossession, AuthZService } from 'nest-authz'; +import { AuthZService } from 'nest-authz'; import { beforeEach, describe, expect, it, vi } from 'vitest'; -import type { ApiKey } from '@app/graphql/generated/api/types.js'; -import { ApiKeyWithSecret, Resource, Role } from '@app/graphql/generated/api/types.js'; import { ApiKeyService } from '@app/unraid-api/auth/api-key.service.js'; import { AuthService } from '@app/unraid-api/auth/auth.service.js'; import { CookieService } from '@app/unraid-api/auth/cookie.service.js'; +import { ApiKey, ApiKeyWithSecret } from '@app/unraid-api/graph/resolvers/api-key/api-key.model.js'; import { ApiKeyResolver } from '@app/unraid-api/graph/resolvers/api-key/api-key.resolver.js'; +import { Role } from '@app/unraid-api/graph/resolvers/base.model.js'; describe('ApiKeyResolver', () => { let resolver: ApiKeyResolver; diff --git a/api/src/unraid-api/graph/resolvers/api-key/api-key.resolver.ts b/api/src/unraid-api/graph/resolvers/api-key/api-key.resolver.ts index 3570c3d8b..f71f5fb61 100644 --- a/api/src/unraid-api/graph/resolvers/api-key/api-key.resolver.ts +++ b/api/src/unraid-api/graph/resolvers/api-key/api-key.resolver.ts @@ -2,25 +2,26 @@ import { Args, Mutation, Query, Resolver } from '@nestjs/graphql'; import { AuthActionVerb, AuthPossession, UsePermissions } from 'nest-authz'; -import type { +import { ApiKeyService } from '@app/unraid-api/auth/api-key.service.js'; +import { AuthService } from '@app/unraid-api/auth/auth.service.js'; +import { AddRoleForApiKeyInput, ApiKey, ApiKeyWithSecret, CreateApiKeyInput, RemoveRoleFromApiKeyInput, -} from '@app/graphql/generated/api/types.js'; -import { Resource, Role } from '@app/graphql/generated/api/types.js'; -import { ApiKeyService } from '@app/unraid-api/auth/api-key.service.js'; -import { AuthService } from '@app/unraid-api/auth/auth.service.js'; +} from '@app/unraid-api/graph/resolvers/api-key/api-key.model.js'; +import { Resource, Role } from '@app/unraid-api/graph/resolvers/base.model.js'; +import { validateObject } from '@app/unraid-api/graph/resolvers/validation.utils.js'; -@Resolver('ApiKey') +@Resolver(() => ApiKey) export class ApiKeyResolver { constructor( private authService: AuthService, private apiKeyService: ApiKeyService ) {} - @Query() + @Query(() => [ApiKey]) @UsePermissions({ action: AuthActionVerb.READ, resource: Resource.API_KEY, @@ -30,7 +31,7 @@ export class ApiKeyResolver { return this.apiKeyService.findAll(); } - @Query() + @Query(() => ApiKey, { nullable: true }) @UsePermissions({ action: AuthActionVerb.READ, resource: Resource.API_KEY, @@ -40,7 +41,7 @@ export class ApiKeyResolver { return this.apiKeyService.findById(id); } - @Mutation() + @Mutation(() => ApiKeyWithSecret) @UsePermissions({ action: AuthActionVerb.CREATE, resource: Resource.API_KEY, @@ -48,8 +49,11 @@ export class ApiKeyResolver { }) async createApiKey( @Args('input') - input: CreateApiKeyInput + unvalidatedInput: CreateApiKeyInput ): Promise { + // Validate the input using class-validator + const input = await validateObject(CreateApiKeyInput, unvalidatedInput); + const apiKey = await this.apiKeyService.create({ name: input.name, description: input.description ?? undefined, @@ -63,7 +67,7 @@ export class ApiKeyResolver { return apiKey; } - @Mutation() + @Mutation(() => Boolean) @UsePermissions({ action: AuthActionVerb.UPDATE, resource: Resource.API_KEY, @@ -73,10 +77,13 @@ export class ApiKeyResolver { @Args('input') input: AddRoleForApiKeyInput ): Promise { - return this.authService.addRoleToApiKey(input.apiKeyId, Role[input.role]); + // Validate the input using class-validator + const validatedInput = await validateObject(AddRoleForApiKeyInput, input); + + return this.authService.addRoleToApiKey(validatedInput.apiKeyId, Role[validatedInput.role]); } - @Mutation() + @Mutation(() => Boolean) @UsePermissions({ action: AuthActionVerb.UPDATE, resource: Resource.API_KEY, @@ -86,6 +93,8 @@ export class ApiKeyResolver { @Args('input') input: RemoveRoleFromApiKeyInput ): Promise { - return this.authService.removeRoleFromApiKey(input.apiKeyId, Role[input.role]); + // Validate the input using class-validator + const validatedInput = await validateObject(RemoveRoleFromApiKeyInput, input); + return this.authService.removeRoleFromApiKey(validatedInput.apiKeyId, Role[validatedInput.role]); } } diff --git a/api/src/unraid-api/graph/resolvers/array/array.model.ts b/api/src/unraid-api/graph/resolvers/array/array.model.ts new file mode 100644 index 000000000..a61a40777 --- /dev/null +++ b/api/src/unraid-api/graph/resolvers/array/array.model.ts @@ -0,0 +1,312 @@ +import { Field, ID, InputType, Int, ObjectType, registerEnumType } from '@nestjs/graphql'; + +import { GraphQLLong } from '@app/graphql/resolvers/graphql-type-long.js'; +import { Node } from '@app/unraid-api/graph/resolvers/base.model.js'; + +@ObjectType() +export class Capacity { + @Field(() => String, { description: 'Free capacity' }) + free!: string; + + @Field(() => String, { description: 'Used capacity' }) + used!: string; + + @Field(() => String, { description: 'Total capacity' }) + total!: string; +} + +@ObjectType() +export class ArrayCapacity { + @Field(() => Capacity, { description: 'Capacity in kilobytes' }) + kilobytes!: Capacity; + + @Field(() => Capacity, { description: 'Capacity in number of disks' }) + disks!: Capacity; +} + +@ObjectType({ + implements: () => Node, +}) +export class ArrayDisk implements Node { + @Field(() => ID, { description: 'Disk identifier, only set for present disks on the system' }) + id!: string; + + @Field(() => Int, { + description: + 'Array slot number. Parity1 is always 0 and Parity2 is always 29. Array slots will be 1 - 28. Cache slots are 30 - 53. Flash is 54.', + }) + idx!: number; + + @Field(() => String, { nullable: true }) + name?: string; + + @Field(() => String, { nullable: true }) + device?: string; + + @Field(() => GraphQLLong, { description: '(KB) Disk Size total', nullable: true }) + size?: number | null; + + @Field(() => ArrayDiskStatus, { nullable: true }) + status?: ArrayDiskStatus; + + @Field(() => Boolean, { nullable: true, description: 'Is the disk a HDD or SSD.' }) + rotational?: boolean; + + @Field(() => Int, { + nullable: true, + description: 'Disk temp - will be NaN if array is not started or DISK_NP', + }) + temp?: number | null; + + @Field(() => GraphQLLong, { + nullable: true, + description: + 'Count of I/O read requests sent to the device I/O drivers. These statistics may be cleared at any time.', + }) + numReads?: number | null; + + @Field(() => GraphQLLong, { + nullable: true, + description: + 'Count of I/O writes requests sent to the device I/O drivers. These statistics may be cleared at any time.', + }) + numWrites?: number | null; + + @Field(() => GraphQLLong, { + nullable: true, + description: + 'Number of unrecoverable errors reported by the device I/O drivers. Missing data due to unrecoverable array read errors is filled in on-the-fly using parity reconstruct (and we attempt to write this data back to the sector(s) which failed). Any unrecoverable write error results in disabling the disk.', + }) + numErrors?: number | null; + + @Field(() => GraphQLLong, { + nullable: true, + description: '(KB) Total Size of the FS (Not present on Parity type drive)', + }) + fsSize?: number | null; + + @Field(() => GraphQLLong, { + nullable: true, + description: '(KB) Free Size on the FS (Not present on Parity type drive)', + }) + fsFree?: number | null; + + @Field(() => GraphQLLong, { + nullable: true, + description: '(KB) Used Size on the FS (Not present on Parity type drive)', + }) + fsUsed?: number | null; + + @Field(() => Boolean, { nullable: true }) + exportable?: boolean; + + @Field(() => ArrayDiskType, { + description: 'Type of Disk - used to differentiate Cache / Flash / Array / Parity', + }) + type!: ArrayDiskType; + + @Field(() => Int, { nullable: true, description: '(%) Disk space left to warn' }) + warning?: number | null; + + @Field(() => Int, { nullable: true, description: '(%) Disk space left for critical' }) + critical?: number | null; + + @Field(() => String, { nullable: true, description: 'File system type for the disk' }) + fsType?: string | null; + + @Field(() => String, { nullable: true, description: 'User comment on disk' }) + comment?: string | null; + + @Field(() => String, { nullable: true, description: 'File format (ex MBR: 4KiB-aligned)' }) + format?: string | null; + + @Field(() => String, { nullable: true, description: 'ata | nvme | usb | (others)' }) + transport?: string | null; + + @Field(() => ArrayDiskFsColor, { nullable: true }) + color?: ArrayDiskFsColor | null; +} + +@ObjectType({ + implements: () => Node, +}) +export class UnraidArray implements Node { + @Field(() => ID) + id!: string; + + @Field(() => ArrayState, { nullable: true, description: 'Array state before this query/mutation' }) + previousState?: ArrayState; + + @Field(() => ArrayPendingState, { + nullable: true, + description: 'Array state after this query/mutation', + }) + pendingState?: ArrayPendingState; + + @Field(() => ArrayState, { description: 'Current array state' }) + state!: ArrayState; + + @Field(() => ArrayCapacity, { description: 'Current array capacity' }) + capacity!: ArrayCapacity; + + @Field(() => ArrayDisk, { nullable: true, description: 'Current boot disk' }) + boot?: ArrayDisk; + + @Field(() => [ArrayDisk], { description: 'Parity disks in the current array' }) + parities!: ArrayDisk[]; + + @Field(() => [ArrayDisk], { description: 'Data disks in the current array' }) + disks!: ArrayDisk[]; + + @Field(() => [ArrayDisk], { description: 'Caches in the current array' }) + caches!: ArrayDisk[]; +} + +@InputType() +export class ArrayDiskInput { + @Field(() => ID, { description: 'Disk ID' }) + id!: string; + + @Field(() => Int, { nullable: true, description: 'The slot for the disk' }) + slot?: number; +} + +@InputType() +export class ArrayStateInput { + @Field(() => ArrayStateInputState, { description: 'Array state' }) + desiredState!: ArrayStateInputState; +} + +export enum ArrayStateInputState { + START = 'START', + STOP = 'STOP', +} + +registerEnumType(ArrayStateInputState, { + name: 'ArrayStateInputState', +}); + +export enum ArrayState { + STARTED = 'STARTED', + STOPPED = 'STOPPED', + NEW_ARRAY = 'NEW_ARRAY', + RECON_DISK = 'RECON_DISK', + DISABLE_DISK = 'DISABLE_DISK', + SWAP_DSBL = 'SWAP_DSBL', + INVALID_EXPANSION = 'INVALID_EXPANSION', + PARITY_NOT_BIGGEST = 'PARITY_NOT_BIGGEST', + TOO_MANY_MISSING_DISKS = 'TOO_MANY_MISSING_DISKS', + NEW_DISK_TOO_SMALL = 'NEW_DISK_TOO_SMALL', + NO_DATA_DISKS = 'NO_DATA_DISKS', +} + +registerEnumType(ArrayState, { + name: 'ArrayState', +}); + +export enum ArrayDiskStatus { + DISK_NP = 'DISK_NP', + DISK_OK = 'DISK_OK', + DISK_NP_MISSING = 'DISK_NP_MISSING', + DISK_INVALID = 'DISK_INVALID', + DISK_WRONG = 'DISK_WRONG', + DISK_DSBL = 'DISK_DSBL', + DISK_NP_DSBL = 'DISK_NP_DSBL', + DISK_DSBL_NEW = 'DISK_DSBL_NEW', + DISK_NEW = 'DISK_NEW', +} + +registerEnumType(ArrayDiskStatus, { + name: 'ArrayDiskStatus', +}); + +export enum ArrayPendingState { + STARTING = 'STARTING', + STOPPING = 'STOPPING', + NO_DATA_DISKS = 'NO_DATA_DISKS', + TOO_MANY_MISSING_DISKS = 'TOO_MANY_MISSING_DISKS', +} + +registerEnumType(ArrayPendingState, { + name: 'ArrayPendingState', +}); + +export enum ArrayDiskType { + DATA = 'DATA', + PARITY = 'PARITY', + FLASH = 'FLASH', + CACHE = 'CACHE', +} + +registerEnumType(ArrayDiskType, { + name: 'ArrayDiskType', +}); + +export enum ArrayDiskFsColor { + GREEN_ON = 'GREEN_ON', + GREEN_BLINK = 'GREEN_BLINK', + BLUE_ON = 'BLUE_ON', + BLUE_BLINK = 'BLUE_BLINK', + YELLOW_ON = 'YELLOW_ON', + YELLOW_BLINK = 'YELLOW_BLINK', + RED_ON = 'RED_ON', + RED_OFF = 'RED_OFF', + GREY_OFF = 'GREY_OFF', +} + +registerEnumType(ArrayDiskFsColor, { + name: 'ArrayDiskFsColor', +}); + +@ObjectType({ + implements: () => Node, +}) +export class Share implements Node { + @Field(() => ID) + id!: string; + + @Field(() => String, { description: 'Display name', nullable: true }) + name?: string | null; + + @Field(() => GraphQLLong, { description: '(KB) Free space', nullable: true }) + free?: number | null; + + @Field(() => GraphQLLong, { description: '(KB) Used Size', nullable: true }) + used?: number | null; + + @Field(() => GraphQLLong, { description: '(KB) Total size', nullable: true }) + size?: number | null; + + @Field(() => [String], { description: 'Disks that are included in this share', nullable: true }) + include?: string[] | null; + + @Field(() => [String], { description: 'Disks that are excluded from this share', nullable: true }) + exclude?: string[] | null; + + @Field(() => Boolean, { description: 'Is this share cached', nullable: true }) + cache?: boolean | null; + + @Field(() => String, { description: 'Original name', nullable: true }) + nameOrig?: string | null; + + @Field(() => String, { description: 'User comment', nullable: true }) + comment?: string | null; + + @Field(() => String, { description: 'Allocator', nullable: true }) + allocator?: string | null; + + @Field(() => String, { description: 'Split level', nullable: true }) + splitLevel?: string | null; + + @Field(() => String, { description: 'Floor', nullable: true }) + floor?: string | null; + + @Field(() => String, { description: 'COW', nullable: true }) + cow?: string | null; + + @Field(() => String, { description: 'Color', nullable: true }) + color?: string | null; + + @Field(() => String, { description: 'LUKS status', nullable: true }) + luksStatus?: string | null; +} diff --git a/api/src/unraid-api/graph/resolvers/array/array.module.ts b/api/src/unraid-api/graph/resolvers/array/array.module.ts new file mode 100644 index 000000000..66372d1c4 --- /dev/null +++ b/api/src/unraid-api/graph/resolvers/array/array.module.ts @@ -0,0 +1,21 @@ +import { Module } from '@nestjs/common'; + +import { ArrayMutationsResolver } from '@app/unraid-api/graph/resolvers/array/array.mutations.resolver.js'; +import { ArrayResolver } from '@app/unraid-api/graph/resolvers/array/array.resolver.js'; +import { ArrayService } from '@app/unraid-api/graph/resolvers/array/array.service.js'; +import { ParityCheckMutationsResolver } from '@app/unraid-api/graph/resolvers/array/parity.mutations.resolver.js'; +import { ParityResolver } from '@app/unraid-api/graph/resolvers/array/parity.resolver.js'; +import { ParityService } from '@app/unraid-api/graph/resolvers/array/parity.service.js'; + +@Module({ + imports: [], + providers: [ + ArrayService, + ParityService, + ArrayMutationsResolver, + ParityResolver, + ArrayResolver, + ParityCheckMutationsResolver, + ], +}) +export class ArrayModule {} diff --git a/api/src/unraid-api/graph/resolvers/array/array.mutations.resolver.ts b/api/src/unraid-api/graph/resolvers/array/array.mutations.resolver.ts index 2feb05177..dde7d1dda 100644 --- a/api/src/unraid-api/graph/resolvers/array/array.mutations.resolver.ts +++ b/api/src/unraid-api/graph/resolvers/array/array.mutations.resolver.ts @@ -1,72 +1,106 @@ -import { Args, ResolveField, Resolver } from '@nestjs/graphql'; +import { BadRequestException } from '@nestjs/common'; +import { Args, Mutation, ResolveField, Resolver } from '@nestjs/graphql'; import { AuthActionVerb, AuthPossession, UsePermissions } from 'nest-authz'; -import type { ArrayDiskInput, ArrayStateInput } from '@app/graphql/generated/api/types.js'; -import { Resource } from '@app/graphql/generated/api/types.js'; +import { + ArrayDisk, + ArrayDiskInput, + ArrayStateInput, + UnraidArray, +} from '@app/unraid-api/graph/resolvers/array/array.model.js'; import { ArrayService } from '@app/unraid-api/graph/resolvers/array/array.service.js'; +import { Resource } from '@app/unraid-api/graph/resolvers/base.model.js'; +import { ArrayMutations } from '@app/unraid-api/graph/resolvers/mutation/mutation.model.js'; -@Resolver('ArrayMutations') +/** + * Nested Resolvers for Mutations MUST use @ResolveField() instead of @Mutation() + */ +@Resolver(() => ArrayMutations) export class ArrayMutationsResolver { constructor(private readonly arrayService: ArrayService) {} - @ResolveField('setState') + @ResolveField(() => UnraidArray, { description: 'Set array state' }) @UsePermissions({ action: AuthActionVerb.UPDATE, resource: Resource.ARRAY, possession: AuthPossession.ANY, }) - public async setState(@Args('input') input: ArrayStateInput) { + public async setState(@Args('input') input: ArrayStateInput): Promise { return this.arrayService.updateArrayState(input); } - @ResolveField('addDiskToArray') + @ResolveField(() => UnraidArray, { description: 'Add new disk to array' }) @UsePermissions({ action: AuthActionVerb.UPDATE, resource: Resource.ARRAY, possession: AuthPossession.ANY, }) - public async addDiskToArray(@Args('input') input: ArrayDiskInput) { + public async addDiskToArray(@Args('input') input: ArrayDiskInput): Promise { return this.arrayService.addDiskToArray(input); } - @ResolveField('removeDiskFromArray') + @ResolveField(() => UnraidArray, { + description: + "Remove existing disk from array. NOTE: The array must be stopped before running this otherwise it'll throw an error.", + }) @UsePermissions({ action: AuthActionVerb.UPDATE, resource: Resource.ARRAY, possession: AuthPossession.ANY, }) - public async removeDiskFromArray(@Args('input') input: ArrayDiskInput) { + public async removeDiskFromArray(@Args('input') input: ArrayDiskInput): Promise { return this.arrayService.removeDiskFromArray(input); } - @ResolveField('mountArrayDisk') + @ResolveField(() => ArrayDisk, { description: 'Mount a disk in the array' }) @UsePermissions({ action: AuthActionVerb.UPDATE, resource: Resource.ARRAY, possession: AuthPossession.ANY, }) - public async mountArrayDisk(@Args('id') id: string) { - return this.arrayService.mountArrayDisk(id); + public async mountArrayDisk(@Args('id') id: string): Promise { + const array = await this.arrayService.mountArrayDisk(id); + const disk = + array.disks.find((disk) => disk.id === id) || + array.parities.find((disk) => disk.id === id) || + array.caches.find((disk) => disk.id === id); + + if (!disk) { + throw new BadRequestException(`Disk with id ${id} not found in array`); + } + + return disk; } - @ResolveField('unmountArrayDisk') + @ResolveField(() => ArrayDisk, { description: 'Unmount a disk from the array' }) @UsePermissions({ action: AuthActionVerb.UPDATE, resource: Resource.ARRAY, possession: AuthPossession.ANY, }) - public async unmountArrayDisk(@Args('id') id: string) { - return this.arrayService.unmountArrayDisk(id); + public async unmountArrayDisk(@Args('id') id: string): Promise { + const array = await this.arrayService.unmountArrayDisk(id); + const disk = + array.disks.find((disk) => disk.id === id) || + array.parities.find((disk) => disk.id === id) || + array.caches.find((disk) => disk.id === id); + + if (!disk) { + throw new BadRequestException(`Disk with id ${id} not found in array`); + } + + return disk; } - @ResolveField('clearArrayDiskStatistics') + @ResolveField(() => Boolean, { description: 'Clear statistics for a disk in the array' }) @UsePermissions({ action: AuthActionVerb.UPDATE, resource: Resource.ARRAY, possession: AuthPossession.ANY, }) - public async clearArrayDiskStatistics(@Args('id') id: string) { - return this.arrayService.clearArrayDiskStatistics(id); + public async clearArrayDiskStatistics(@Args('id') id: string): Promise { + await this.arrayService.clearArrayDiskStatistics(id); + return true; } } diff --git a/api/src/unraid-api/graph/resolvers/array/array.resolver.ts b/api/src/unraid-api/graph/resolvers/array/array.resolver.ts index 3b1c197ad..502184c79 100644 --- a/api/src/unraid-api/graph/resolvers/array/array.resolver.ts +++ b/api/src/unraid-api/graph/resolvers/array/array.resolver.ts @@ -4,25 +4,26 @@ import { AuthActionVerb, AuthPossession, UsePermissions } from 'nest-authz'; import { getArrayData } from '@app/core/modules/array/get-array-data.js'; import { createSubscription, PUBSUB_CHANNEL } from '@app/core/pubsub.js'; -import { Resource } from '@app/graphql/generated/api/types.js'; import { store } from '@app/store/index.js'; +import { UnraidArray } from '@app/unraid-api/graph/resolvers/array/array.model.js'; import { ArrayService } from '@app/unraid-api/graph/resolvers/array/array.service.js'; +import { Resource } from '@app/unraid-api/graph/resolvers/base.model.js'; @Resolver('Array') export class ArrayResolver { constructor(private readonly arrayService: ArrayService) {} - @Query() + @Query(() => UnraidArray) @UsePermissions({ action: AuthActionVerb.READ, resource: Resource.ARRAY, possession: AuthPossession.ANY, }) public async array() { - return getArrayData(store.getState); + return this.arrayService.getArrayData(); } - @Subscription('array') + @Subscription(() => UnraidArray) @UsePermissions({ action: AuthActionVerb.READ, resource: Resource.ARRAY, diff --git a/api/src/unraid-api/graph/resolvers/array/array.service.spec.ts b/api/src/unraid-api/graph/resolvers/array/array.service.spec.ts index 06a0901cd..197d04f39 100644 --- a/api/src/unraid-api/graph/resolvers/array/array.service.spec.ts +++ b/api/src/unraid-api/graph/resolvers/array/array.service.spec.ts @@ -3,11 +3,15 @@ import { Test } from '@nestjs/testing'; import { beforeEach, describe, expect, it, vi } from 'vitest'; -import type { ArrayDiskInput, ArrayStateInput } from '@app/graphql/generated/api/types.js'; import { getArrayData } from '@app/core/modules/array/get-array-data.js'; import { emcmd } from '@app/core/utils/clients/emcmd.js'; -import { ArrayState, ArrayStateInputState } from '@app/graphql/generated/api/types.js'; import { getters } from '@app/store/index.js'; +import { + ArrayDiskInput, + ArrayState, + ArrayStateInput, + ArrayStateInputState, +} from '@app/unraid-api/graph/resolvers/array/array.model.js'; import { ArrayService } from '@app/unraid-api/graph/resolvers/array/array.service.js'; vi.mock('@app/core/utils/clients/emcmd.js', () => ({ diff --git a/api/src/unraid-api/graph/resolvers/array/array.service.ts b/api/src/unraid-api/graph/resolvers/array/array.service.ts index 204c5f6b2..6d2539a90 100644 --- a/api/src/unraid-api/graph/resolvers/array/array.service.ts +++ b/api/src/unraid-api/graph/resolvers/array/array.service.ts @@ -3,17 +3,19 @@ import { BadRequestException, Injectable } from '@nestjs/common'; import { capitalCase, constantCase } from 'change-case'; import { GraphQLError } from 'graphql'; -import type { ArrayDiskInput, ArrayStateInput, ArrayType } from '@app/graphql/generated/api/types.js'; import { AppError } from '@app/core/errors/app-error.js'; import { ArrayRunningError } from '@app/core/errors/array-running-error.js'; -import { getArrayData } from '@app/core/modules/array/get-array-data.js'; +import { getArrayData as getArrayDataUtil } from '@app/core/modules/array/get-array-data.js'; import { emcmd } from '@app/core/utils/clients/emcmd.js'; import { arrayIsRunning as arrayIsRunningUtil } from '@app/core/utils/index.js'; import { + ArrayDiskInput, ArrayPendingState, ArrayState, + ArrayStateInput, ArrayStateInputState, -} from '@app/graphql/generated/api/types.js'; + UnraidArray, +} from '@app/unraid-api/graph/resolvers/array/array.model.js'; @Injectable() export class ArrayService { @@ -27,7 +29,11 @@ export class ArrayService { return arrayIsRunningUtil(); } - async updateArrayState({ desiredState }: ArrayStateInput): Promise { + public getArrayData() { + return getArrayDataUtil(); + } + + async updateArrayState({ desiredState }: ArrayStateInput): Promise { const startState = this.arrayIsRunning() ? ArrayState.STARTED : ArrayState.STOPPED; const pendingState = desiredState === ArrayStateInputState.STOP @@ -63,12 +69,12 @@ export class ArrayService { } // Get new array JSON - const array = getArrayData(); + const array = this.getArrayData(); return array; } - async addDiskToArray(input: ArrayDiskInput): Promise { + async addDiskToArray(input: ArrayDiskInput): Promise { if (this.arrayIsRunning()) { throw new ArrayRunningError(); } @@ -82,10 +88,10 @@ export class ArrayService { [`slotId.${slot}`]: diskId, }); - return getArrayData(); + return this.getArrayData(); } - async removeDiskFromArray(input: ArrayDiskInput): Promise { + async removeDiskFromArray(input: ArrayDiskInput): Promise { if (this.arrayIsRunning()) { throw new ArrayRunningError(); } @@ -99,10 +105,10 @@ export class ArrayService { [`slotId.${slotStr}`]: '', }); - return getArrayData(); + return this.getArrayData(); } - async mountArrayDisk(id: string): Promise { + async mountArrayDisk(id: string): Promise { if (!this.arrayIsRunning()) { throw new BadRequestException('Array must be running to mount disks'); } @@ -113,10 +119,10 @@ export class ArrayService { [`diskId.${id}`]: '1', }); - return getArrayData(); + return this.getArrayData(); } - async unmountArrayDisk(id: string): Promise { + async unmountArrayDisk(id: string): Promise { if (!this.arrayIsRunning()) { throw new BadRequestException('Array must be running to unmount disks'); } @@ -127,10 +133,10 @@ export class ArrayService { [`diskId.${id}`]: '1', }); - return getArrayData(); + return this.getArrayData(); } - async clearArrayDiskStatistics(id: string): Promise { + async clearArrayDiskStatistics(id: string): Promise { if (!this.arrayIsRunning()) { throw new BadRequestException('Array must be running to clear disk statistics'); } @@ -141,67 +147,6 @@ export class ArrayService { [`diskId.${id}`]: '1', }); - return getArrayData(); - } - - /** - * Updates the parity check state - * @param wantedState - The desired state for the parity check ('pause', 'resume', 'cancel', 'start') - * @param correct - Whether to write corrections to parity (only applicable for 'start' state) - * @returns The updated array data - */ - async updateParityCheck({ - wantedState, - correct, - }: { - wantedState: 'pause' | 'resume' | 'cancel' | 'start'; - correct: boolean; - }): Promise { - const { getters } = await import('@app/store/index.js'); - const running = getters.emhttp().var.mdResync !== 0; - const states = { - pause: { - cmdNoCheck: 'Pause', - }, - resume: { - cmdCheck: 'Resume', - }, - cancel: { - cmdNoCheck: 'Cancel', - }, - start: { - cmdCheck: 'Check', - }, - }; - - let allowedStates = Object.keys(states); - - // Only allow starting a check if there isn't already one running - if (running) { - // Remove 'start' from allowed states when a check is already running - allowedStates = allowedStates.filter((state) => state !== 'start'); - } - - // Only allow states from states object - if (!allowedStates.includes(wantedState)) { - throw new GraphQLError(`Invalid parity check state: ${wantedState}`); - } - - // Should we write correction to the parity during the check - const writeCorrectionsToParity = wantedState === 'start' && correct; - - try { - await emcmd({ - startState: 'STARTED', - ...states[wantedState], - ...(writeCorrectionsToParity ? { optionCorrect: 'correct' } : {}), - }); - } catch (error) { - throw new GraphQLError( - `Failed to update parity check: ${error instanceof Error ? error.message : 'Unknown error'}` - ); - } - - return getArrayData(); + return this.getArrayData(); } } diff --git a/api/src/unraid-api/graph/resolvers/array/parity.model.ts b/api/src/unraid-api/graph/resolvers/array/parity.model.ts new file mode 100644 index 000000000..07230351e --- /dev/null +++ b/api/src/unraid-api/graph/resolvers/array/parity.model.ts @@ -0,0 +1,34 @@ +import { Field, GraphQLISODateTime, Int, ObjectType } from '@nestjs/graphql'; + +@ObjectType() +export class ParityCheck { + @Field(() => GraphQLISODateTime, { nullable: true, description: 'Date of the parity check' }) + date?: Date; + + @Field(() => Int, { nullable: true, description: 'Duration of the parity check in seconds' }) + duration?: number; + + @Field(() => String, { nullable: true, description: 'Speed of the parity check, in MB/s' }) + speed?: string; + + @Field(() => String, { nullable: true, description: 'Status of the parity check' }) + status?: string; + + @Field(() => Int, { nullable: true, description: 'Number of errors during the parity check' }) + errors?: number; + + @Field(() => Int, { nullable: true, description: 'Progress percentage of the parity check' }) + progress?: number; + + @Field(() => Boolean, { + nullable: true, + description: 'Whether corrections are being written to parity', + }) + correcting?: boolean; + + @Field(() => Boolean, { nullable: true, description: 'Whether the parity check is paused' }) + paused?: boolean; + + @Field(() => Boolean, { nullable: true, description: 'Whether the parity check is running' }) + running?: boolean; +} diff --git a/api/src/unraid-api/graph/resolvers/array/parity.mutations.resolver.ts b/api/src/unraid-api/graph/resolvers/array/parity.mutations.resolver.ts new file mode 100644 index 000000000..bac72602d --- /dev/null +++ b/api/src/unraid-api/graph/resolvers/array/parity.mutations.resolver.ts @@ -0,0 +1,68 @@ +import { Args, Mutation, ResolveField, Resolver } from '@nestjs/graphql'; + +import { GraphQLJSON } from 'graphql-scalars'; +import { AuthActionVerb, AuthPossession, UsePermissions } from 'nest-authz'; + +import { ParityService } from '@app/unraid-api/graph/resolvers/array/parity.service.js'; +import { Resource } from '@app/unraid-api/graph/resolvers/base.model.js'; +import { ParityCheckMutations } from '@app/unraid-api/graph/resolvers/mutation/mutation.model.js'; + +/** + * Nested Resolvers for Mutations MUST use @ResolveField() instead of @Mutation() + */ +@Resolver(() => ParityCheckMutations) +export class ParityCheckMutationsResolver { + constructor(private readonly parityService: ParityService) {} + + @UsePermissions({ + action: AuthActionVerb.UPDATE, + resource: Resource.ARRAY, + possession: AuthPossession.ANY, + }) + @ResolveField(() => GraphQLJSON, { description: 'Start a parity check' }) + async start(@Args('correct') correct: boolean): Promise { + return this.parityService.updateParityCheck({ + wantedState: 'start', + correct, + }); + } + + @UsePermissions({ + action: AuthActionVerb.UPDATE, + resource: Resource.ARRAY, + possession: AuthPossession.ANY, + }) + @ResolveField(() => GraphQLJSON, { description: 'Pause a parity check' }) + async pause(): Promise { + return this.parityService.updateParityCheck({ + wantedState: 'pause', + correct: false, + }); + } + + @UsePermissions({ + action: AuthActionVerb.UPDATE, + resource: Resource.ARRAY, + possession: AuthPossession.ANY, + }) + @ResolveField(() => GraphQLJSON, { description: 'Resume a parity check' }) + async resume(): Promise { + return this.parityService.updateParityCheck({ + wantedState: 'resume', + correct: false, + }); + } + + @UsePermissions({ + action: AuthActionVerb.UPDATE, + resource: Resource.ARRAY, + possession: AuthPossession.ANY, + }) + @ResolveField(() => GraphQLJSON, { description: 'Cancel a parity check' }) + async cancel(): Promise { + return this.parityService.updateParityCheck({ + wantedState: 'cancel', + correct: false, + }); + } +} diff --git a/api/src/unraid-api/graph/resolvers/array/parity.resolver.ts b/api/src/unraid-api/graph/resolvers/array/parity.resolver.ts new file mode 100644 index 000000000..8386b39d9 --- /dev/null +++ b/api/src/unraid-api/graph/resolvers/array/parity.resolver.ts @@ -0,0 +1,41 @@ +import { Args, Mutation, Query, Resolver, Subscription } from '@nestjs/graphql'; + +import { GraphQLJSON } from 'graphql-scalars'; +import { PubSub } from 'graphql-subscriptions'; +import { AuthActionVerb, AuthPossession, UsePermissions } from 'nest-authz'; + +import { PUBSUB_CHANNEL } from '@app/core/pubsub.js'; +import { ArrayService } from '@app/unraid-api/graph/resolvers/array/array.service.js'; +import { ParityCheck } from '@app/unraid-api/graph/resolvers/array/parity.model.js'; +import { ParityService } from '@app/unraid-api/graph/resolvers/array/parity.service.js'; +import { Resource } from '@app/unraid-api/graph/resolvers/base.model.js'; + +const pubSub = new PubSub(); + +@Resolver(() => ParityCheck) +export class ParityResolver { + constructor( + private readonly arrayService: ArrayService, + private readonly parityService: ParityService + ) {} + + @UsePermissions({ + action: AuthActionVerb.READ, + resource: Resource.ARRAY, + possession: AuthPossession.ANY, + }) + @Query(() => [ParityCheck]) + async parityHistory(): Promise { + return await this.parityService.getParityHistory(); + } + + @UsePermissions({ + action: AuthActionVerb.READ, + resource: Resource.ARRAY, + possession: AuthPossession.ANY, + }) + @Subscription(() => ParityCheck) + parityHistorySubscription() { + return pubSub.asyncIterableIterator(PUBSUB_CHANNEL.PARITY); + } +} diff --git a/api/src/unraid-api/graph/resolvers/array/parity.service.ts b/api/src/unraid-api/graph/resolvers/array/parity.service.ts new file mode 100644 index 000000000..87ddaf7d2 --- /dev/null +++ b/api/src/unraid-api/graph/resolvers/array/parity.service.ts @@ -0,0 +1,95 @@ +import { Injectable } from '@nestjs/common'; +import { readFile } from 'fs/promises'; + +import { GraphQLError } from 'graphql'; + +import { emcmd } from '@app/core/utils/index.js'; +import { ParityCheck } from '@app/unraid-api/graph/resolvers/array/parity.model.js'; + +@Injectable() +export class ParityService { + constructor() {} + + async getParityHistory(): Promise { + const { getters } = await import('@app/store/index.js'); + + const historyFilePath = getters.paths()['parity-checks']; + const history = await readFile(historyFilePath).catch(() => { + throw new Error(`Parity history file not found: ${historyFilePath}`); + }); + + // Convert checks into array of objects + const lines = history.toString().trim().split('\n').reverse(); + return lines.map((line) => { + const [date, duration, speed, status, errors = '0'] = line.split('|'); + return { + date: new Date(date), + duration: Number.parseInt(duration, 10), + speed: speed ?? 'Unavailable', + status: status === '-4' ? 'Cancelled' : 'OK', + errors: Number.parseInt(errors, 10), + }; + }); + } + + /** + * Updates the parity check state + * @param wantedState - The desired state for the parity check ('pause', 'resume', 'cancel', 'start') + * @param correct - Whether to write corrections to parity (only applicable for 'start' state) + * @returns The updated array data + */ + async updateParityCheck({ + wantedState, + correct, + }: { + wantedState: 'pause' | 'resume' | 'cancel' | 'start'; + correct: boolean; + }): Promise { + const { getters } = await import('@app/store/index.js'); + const running = getters.emhttp().var.mdResync !== 0; + const states = { + pause: { + cmdNoCheck: 'Pause', + }, + resume: { + cmdCheck: 'Resume', + }, + cancel: { + cmdNoCheck: 'Cancel', + }, + start: { + cmdCheck: 'Check', + }, + }; + + let allowedStates = Object.keys(states); + + // Only allow starting a check if there isn't already one running + if (running) { + // Remove 'start' from allowed states when a check is already running + allowedStates = allowedStates.filter((state) => state !== 'start'); + } + + // Only allow states from states object + if (!allowedStates.includes(wantedState)) { + throw new GraphQLError(`Invalid parity check state: ${wantedState}`); + } + + // Should we write correction to the parity during the check + const writeCorrectionsToParity = wantedState === 'start' && correct; + + try { + await emcmd({ + startState: 'STARTED', + ...states[wantedState], + ...(writeCorrectionsToParity ? { optionCorrect: 'correct' } : {}), + }); + } catch (error) { + throw new GraphQLError( + `Failed to update parity check: ${error instanceof Error ? error.message : 'Unknown error'}` + ); + } + + return this.getParityHistory(); + } +} diff --git a/api/src/unraid-api/graph/resolvers/base.model.ts b/api/src/unraid-api/graph/resolvers/base.model.ts new file mode 100644 index 000000000..3872bb485 --- /dev/null +++ b/api/src/unraid-api/graph/resolvers/base.model.ts @@ -0,0 +1,55 @@ +import { Field, ID, InterfaceType, registerEnumType } from '@nestjs/graphql'; + +// Register enums +export enum Resource { + API_KEY = 'API_KEY', + ARRAY = 'ARRAY', + CLOUD = 'CLOUD', + CONFIG = 'CONFIG', + CONNECT = 'CONNECT', + CONNECT__REMOTE_ACCESS = 'CONNECT__REMOTE_ACCESS', + CUSTOMIZATIONS = 'CUSTOMIZATIONS', + DASHBOARD = 'DASHBOARD', + DISK = 'DISK', + DISPLAY = 'DISPLAY', + DOCKER = 'DOCKER', + FLASH = 'FLASH', + INFO = 'INFO', + LOGS = 'LOGS', + ME = 'ME', + NETWORK = 'NETWORK', + NOTIFICATIONS = 'NOTIFICATIONS', + ONLINE = 'ONLINE', + OS = 'OS', + OWNER = 'OWNER', + PERMISSION = 'PERMISSION', + REGISTRATION = 'REGISTRATION', + SERVERS = 'SERVERS', + SERVICES = 'SERVICES', + SHARE = 'SHARE', + VARS = 'VARS', + VMS = 'VMS', + WELCOME = 'WELCOME', +} + +export enum Role { + ADMIN = 'ADMIN', + CONNECT = 'CONNECT', + GUEST = 'GUEST', +} + +@InterfaceType() +export class Node { + @Field(() => ID) + id!: string; +} + +registerEnumType(Resource, { + name: 'Resource', + description: 'Available resources for permissions', +}); + +registerEnumType(Role, { + name: 'Role', + description: 'Available roles for API keys and users', +}); diff --git a/api/src/unraid-api/graph/resolvers/cloud/cloud.model.ts b/api/src/unraid-api/graph/resolvers/cloud/cloud.model.ts new file mode 100644 index 000000000..d0dea3f3c --- /dev/null +++ b/api/src/unraid-api/graph/resolvers/cloud/cloud.model.ts @@ -0,0 +1,79 @@ +import { Field, Int, ObjectType, registerEnumType } from '@nestjs/graphql'; + +export enum MinigraphStatus { + PRE_INIT = 'PRE_INIT', + CONNECTING = 'CONNECTING', + CONNECTED = 'CONNECTED', + PING_FAILURE = 'PING_FAILURE', + ERROR_RETRYING = 'ERROR_RETRYING', +} + +registerEnumType(MinigraphStatus, { + name: 'MinigraphStatus', +}); + +@ObjectType() +export class ApiKeyResponse { + @Field(() => Boolean) + valid!: boolean; + + @Field(() => String, { nullable: true }) + error?: string; +} + +@ObjectType() +export class MinigraphqlResponse { + @Field(() => MinigraphStatus) + status!: MinigraphStatus; + + @Field(() => Int, { nullable: true }) + timeout?: number | null; + + @Field(() => String, { nullable: true }) + error?: string | null; +} + +@ObjectType() +export class CloudResponse { + @Field(() => String) + status!: string; + + @Field(() => String, { nullable: true }) + ip?: string; + + @Field(() => String, { nullable: true }) + error?: string | null; +} + +@ObjectType() +export class RelayResponse { + @Field(() => String) + status!: string; + + @Field(() => String, { nullable: true }) + timeout?: string; + + @Field(() => String, { nullable: true }) + error?: string; +} + +@ObjectType() +export class Cloud { + @Field(() => String, { nullable: true }) + error?: string; + + @Field(() => ApiKeyResponse) + apiKey!: ApiKeyResponse; + + @Field(() => RelayResponse, { nullable: true }) + relay?: RelayResponse; + + @Field(() => MinigraphqlResponse) + minigraphql!: MinigraphqlResponse; + + @Field(() => CloudResponse) + cloud!: CloudResponse; + + @Field(() => [String]) + allowedOrigins!: string[]; +} diff --git a/api/src/unraid-api/graph/resolvers/cloud/cloud.resolver.ts b/api/src/unraid-api/graph/resolvers/cloud/cloud.resolver.ts index 6aa648e32..3bd175ef9 100644 --- a/api/src/unraid-api/graph/resolvers/cloud/cloud.resolver.ts +++ b/api/src/unraid-api/graph/resolvers/cloud/cloud.resolver.ts @@ -1,31 +1,17 @@ -import { Args, Mutation, Query, Resolver } from '@nestjs/graphql'; +import { Query, Resolver } from '@nestjs/graphql'; import { AuthActionVerb, AuthPossession, UsePermissions } from 'nest-authz'; -import type { - Cloud, - ConnectSignInInput, - RemoteAccess, - SetupRemoteAccessInput, -} from '@app/graphql/generated/api/types.js'; -import { getAllowedOrigins, getExtraOrigins } from '@app/common/allowed-origins.js'; -import { - DynamicRemoteAccessType, - Resource, - WAN_ACCESS_TYPE, - WAN_FORWARD_TYPE, -} from '@app/graphql/generated/api/types.js'; -import { connectSignIn } from '@app/graphql/resolvers/mutation/connect/connect-sign-in.js'; +import { getAllowedOrigins } from '@app/common/allowed-origins.js'; import { checkApi } from '@app/graphql/resolvers/query/cloud/check-api.js'; import { checkCloud } from '@app/graphql/resolvers/query/cloud/check-cloud.js'; import { checkMinigraphql } from '@app/graphql/resolvers/query/cloud/check-minigraphql.js'; -import { setupRemoteAccessThunk } from '@app/store/actions/setup-remote-access.js'; -import { getters, store } from '@app/store/index.js'; -import { logoutUser } from '@app/store/modules/config.js'; +import { Resource } from '@app/unraid-api/graph/resolvers/base.model.js'; +import { Cloud } from '@app/unraid-api/graph/resolvers/cloud/cloud.model.js'; -@Resolver('Cloud') +@Resolver(() => Cloud) export class CloudResolver { - @Query() + @Query(() => Cloud) @UsePermissions({ action: AuthActionVerb.READ, resource: Resource.CLOUD, @@ -40,7 +26,7 @@ export class CloudResolver { // Left in for UPC backwards compat. error: undefined, status: 'connected', - timeout: null, + timeout: undefined, }, apiKey, minigraphql, @@ -49,77 +35,7 @@ export class CloudResolver { error: `${apiKey.error ? `API KEY: ${apiKey.error}` : ''}${ cloud.error ? `NETWORK: ${cloud.error}` : '' - }${minigraphql.error ? `CLOUD: ${minigraphql.error}` : ''}` || null, + }${minigraphql.error ? `CLOUD: ${minigraphql.error}` : ''}` || undefined, }; } - - @Query() - @UsePermissions({ - action: AuthActionVerb.READ, - resource: Resource.CONNECT, - possession: AuthPossession.ANY, - }) - public async remoteAccess(): Promise { - const hasWanAccess = getters.config().remote.wanaccess === 'yes'; - const dynamicRemoteAccessSettings: RemoteAccess = { - accessType: hasWanAccess - ? getters.config().remote.dynamicRemoteAccessType !== DynamicRemoteAccessType.DISABLED - ? WAN_ACCESS_TYPE.DYNAMIC - : WAN_ACCESS_TYPE.ALWAYS - : WAN_ACCESS_TYPE.DISABLED, - forwardType: getters.config().remote.upnpEnabled - ? WAN_FORWARD_TYPE.UPNP - : WAN_FORWARD_TYPE.STATIC, - port: getters.config().remote.wanport ? Number(getters.config().remote.wanport) : null, - }; - - return dynamicRemoteAccessSettings; - } - - @Query() - @UsePermissions({ - action: AuthActionVerb.READ, - resource: Resource.CONNECT, - possession: AuthPossession.ANY, - }) - public async extraAllowedOrigins(): Promise> { - const extraOrigins = getExtraOrigins(); - - return extraOrigins; - } - - @Mutation() - @UsePermissions({ - action: AuthActionVerb.UPDATE, - resource: Resource.CONNECT, - possession: AuthPossession.ANY, - }) - public async connectSignIn(@Args('input') input: ConnectSignInInput): Promise { - /** - * @todo Move to service - */ - return await connectSignIn(input); - } - - @Mutation() - @UsePermissions({ - action: AuthActionVerb.UPDATE, - resource: Resource.CONNECT, - possession: AuthPossession.ANY, - }) - public async connectSignOut() { - await store.dispatch(logoutUser({ reason: 'Manual Sign Out Using API' })); - return true; - } - - @Mutation() - @UsePermissions({ - action: AuthActionVerb.UPDATE, - resource: Resource.CONNECT, - possession: AuthPossession.ANY, - }) - public async setupRemoteAccess(@Args('input') input: SetupRemoteAccessInput): Promise { - await store.dispatch(setupRemoteAccessThunk(input)).unwrap(); - return true; - } } diff --git a/api/src/unraid-api/graph/resolvers/config/config.model.ts b/api/src/unraid-api/graph/resolvers/config/config.model.ts new file mode 100644 index 000000000..789cb4fab --- /dev/null +++ b/api/src/unraid-api/graph/resolvers/config/config.model.ts @@ -0,0 +1,17 @@ +import { Field, ID, ObjectType } from '@nestjs/graphql'; + +import { Node } from '@app/unraid-api/graph/resolvers/base.model.js'; + +@ObjectType({ + implements: () => Node, +}) +export class Config implements Node { + @Field(() => ID) + id!: string; + + @Field(() => Boolean, { nullable: true }) + valid?: boolean | null; + + @Field(() => String, { nullable: true }) + error?: string | null; +} diff --git a/api/src/unraid-api/graph/resolvers/config/config.resolver.ts b/api/src/unraid-api/graph/resolvers/config/config.resolver.ts index 09264c54f..e9084d461 100644 --- a/api/src/unraid-api/graph/resolvers/config/config.resolver.ts +++ b/api/src/unraid-api/graph/resolvers/config/config.resolver.ts @@ -1,16 +1,14 @@ -import { Args, Mutation, Query, Resolver } from '@nestjs/graphql'; +import { Query, Resolver } from '@nestjs/graphql'; import { AuthActionVerb, AuthPossession, UsePermissions } from 'nest-authz'; -import type { AllowedOriginInput } from '@app/graphql/generated/api/types.js'; -import { getAllowedOrigins } from '@app/common/allowed-origins.js'; -import { Config, Resource } from '@app/graphql/generated/api/types.js'; -import { getters, store } from '@app/store/index.js'; -import { updateAllowedOrigins } from '@app/store/modules/config.js'; +import { getters } from '@app/store/index.js'; +import { Resource } from '@app/unraid-api/graph/resolvers/base.model.js'; +import { Config } from '@app/unraid-api/graph/resolvers/config/config.model.js'; -@Resolver('Config') +@Resolver(() => Config) export class ConfigResolver { - @Query() + @Query(() => Config) @UsePermissions({ action: AuthActionVerb.READ, resource: Resource.CONFIG, @@ -24,15 +22,4 @@ export class ConfigResolver { error: emhttp.var.configValid ? null : emhttp.var.configErrorState, }; } - - @Mutation('setAdditionalAllowedOrigins') - @UsePermissions({ - action: AuthActionVerb.UPDATE, - resource: Resource.CONFIG, - possession: AuthPossession.ANY, - }) - public async setAdditionalAllowedOrigins(@Args('input') input: AllowedOriginInput) { - await store.dispatch(updateAllowedOrigins(input.origins)); - return getAllowedOrigins(); - } } diff --git a/api/src/unraid-api/graph/resolvers/connect/connect-settings.resolver.ts b/api/src/unraid-api/graph/resolvers/connect/connect-settings.resolver.ts new file mode 100644 index 000000000..eefda05dc --- /dev/null +++ b/api/src/unraid-api/graph/resolvers/connect/connect-settings.resolver.ts @@ -0,0 +1,155 @@ +import { Logger } from '@nestjs/common'; +import { Args, ID, Mutation, Query, ResolveField, Resolver } from '@nestjs/graphql'; + +import { Layout } from '@jsonforms/core'; +import { GraphQLJSON, GraphQLJSONObject } from 'graphql-scalars'; +import { AuthActionVerb, AuthPossession, UsePermissions } from 'nest-authz'; + +import { getAllowedOrigins } from '@app/common/allowed-origins.js'; +import { setupRemoteAccessThunk } from '@app/store/actions/setup-remote-access.js'; +import { logoutUser, updateAllowedOrigins } from '@app/store/modules/config.js'; +import { Resource } from '@app/unraid-api/graph/resolvers/base.model.js'; +import { ConnectSettingsService } from '@app/unraid-api/graph/resolvers/connect/connect-settings.service.js'; +import { + AllowedOriginInput, + ApiSettingsInput, + ConnectSettings, + ConnectSettingsValues, + ConnectSignInInput, + EnableDynamicRemoteAccessInput, + RemoteAccess, + SetupRemoteAccessInput, +} from '@app/unraid-api/graph/resolvers/connect/connect.model.js'; +import { DataSlice } from '@app/unraid-api/types/json-forms.js'; + +@Resolver(() => ConnectSettings) +export class ConnectSettingsResolver { + private readonly logger = new Logger(ConnectSettingsResolver.name); + constructor(private readonly connectSettingsService: ConnectSettingsService) {} + + @ResolveField(() => ID) + public async id(): Promise { + return 'connectSettingsForm'; + } + + @ResolveField(() => GraphQLJSON) + public async dataSchema(): Promise<{ properties: DataSlice; type: 'object' }> { + const { properties } = await this.connectSettingsService.buildSettingsSchema(); + return { + type: 'object', + properties, + }; + } + + @ResolveField(() => GraphQLJSONObject) + public async uiSchema(): Promise { + const { elements } = await this.connectSettingsService.buildSettingsSchema(); + return { + type: 'VerticalLayout', + elements, + }; + } + @ResolveField(() => ConnectSettingsValues) + public async values(): Promise { + return await this.connectSettingsService.getCurrentSettings(); + } + + @Query(() => RemoteAccess) + @UsePermissions({ + action: AuthActionVerb.READ, + resource: Resource.CONNECT, + possession: AuthPossession.ANY, + }) + public async remoteAccess(): Promise { + return this.connectSettingsService.dynamicRemoteAccessSettings(); + } + + @Query(() => [String]) + @UsePermissions({ + action: AuthActionVerb.READ, + resource: Resource.CONNECT, + possession: AuthPossession.ANY, + }) + public async extraAllowedOrigins(): Promise> { + return this.connectSettingsService.extraAllowedOrigins(); + } + + @Mutation(() => ConnectSettingsValues) + @UsePermissions({ + action: AuthActionVerb.UPDATE, + resource: Resource.CONFIG, + possession: AuthPossession.ANY, + }) + public async updateApiSettings(@Args('input') settings: ApiSettingsInput) { + this.logger.verbose(`Attempting to update API settings: ${JSON.stringify(settings, null, 2)}`); + const restartRequired = await this.connectSettingsService.syncSettings(settings); + const currentSettings = await this.connectSettingsService.getCurrentSettings(); + if (restartRequired) { + setTimeout(async () => { + // Send restart out of band to avoid blocking the return of this resolver + this.logger.log('Restarting API'); + await this.connectSettingsService.restartApi(); + }, 300); + } + return currentSettings; + } + + @Mutation(() => Boolean) + @UsePermissions({ + action: AuthActionVerb.UPDATE, + resource: Resource.CONNECT, + possession: AuthPossession.ANY, + }) + public async connectSignIn(@Args('input') input: ConnectSignInInput): Promise { + return this.connectSettingsService.signIn(input); + } + + @Mutation(() => Boolean) + @UsePermissions({ + action: AuthActionVerb.UPDATE, + resource: Resource.CONNECT, + possession: AuthPossession.ANY, + }) + public async connectSignOut() { + const { store } = await import('@app/store/index.js'); + await store.dispatch(logoutUser({ reason: 'Manual Sign Out Using API' })); + return true; + } + + @Mutation(() => Boolean) + @UsePermissions({ + action: AuthActionVerb.UPDATE, + resource: Resource.CONNECT, + possession: AuthPossession.ANY, + }) + public async setupRemoteAccess(@Args('input') input: SetupRemoteAccessInput): Promise { + const { store } = await import('@app/store/index.js'); + await store.dispatch(setupRemoteAccessThunk(input)).unwrap(); + return true; + } + + @Mutation(() => [String]) + @UsePermissions({ + action: AuthActionVerb.UPDATE, + resource: Resource.CONFIG, + possession: AuthPossession.ANY, + }) + public async setAdditionalAllowedOrigins(@Args('input') input: AllowedOriginInput) { + const { store } = await import('@app/store/index.js'); + await store.dispatch(updateAllowedOrigins(input.origins)); + return getAllowedOrigins(); + } + + @Mutation(() => Boolean) + @UsePermissions({ + action: AuthActionVerb.UPDATE, + resource: Resource.CONNECT__REMOTE_ACCESS, + possession: AuthPossession.ANY, + }) + public async enableDynamicRemoteAccess( + @Args('input') dynamicRemoteAccessInput: EnableDynamicRemoteAccessInput + ): Promise { + console.log('enableDynamicRemoteAccess', dynamicRemoteAccessInput); + return this.connectSettingsService.enableDynamicRemoteAccess(dynamicRemoteAccessInput); + } +} diff --git a/api/src/unraid-api/graph/connect/connect-settings.service.ts b/api/src/unraid-api/graph/resolvers/connect/connect-settings.service.ts similarity index 74% rename from api/src/unraid-api/graph/connect/connect-settings.service.ts rename to api/src/unraid-api/graph/resolvers/connect/connect-settings.service.ts index 91355c992..618d1d40d 100644 --- a/api/src/unraid-api/graph/connect/connect-settings.service.ts +++ b/api/src/unraid-api/graph/resolvers/connect/connect-settings.service.ts @@ -1,29 +1,60 @@ -import { Injectable } from '@nestjs/common'; +import { Injectable, Logger } from '@nestjs/common'; import type { SchemaBasedCondition } from '@jsonforms/core'; import { RuleEffect } from '@jsonforms/core'; +import { execa } from 'execa'; import { GraphQLError } from 'graphql/error/GraphQLError.js'; +import { decodeJwt } from 'jose'; import type { ApiSettingsInput, ConnectSettingsValues, + ConnectSignInInput, + EnableDynamicRemoteAccessInput, RemoteAccess, SetupRemoteAccessInput, -} from '@app/graphql/generated/api/types.js'; +} from '@app/unraid-api/graph/resolvers/connect/connect.model.js'; import type { DataSlice, SettingSlice, UIElement } from '@app/unraid-api/types/json-forms.js'; +import { getExtraOrigins } from '@app/common/allowed-origins.js'; import { fileExistsSync } from '@app/core/utils/files/file-exists.js'; +import { setupRemoteAccessThunk } from '@app/store/actions/setup-remote-access.js'; +import { + loginUser, + setSsoUsers, + updateAllowedOrigins, + updateUserConfig, +} from '@app/store/modules/config.js'; +import { setAllowedRemoteAccessUrl } from '@app/store/modules/dynamic-remote-access.js'; +import { FileLoadStatus } from '@app/store/types.js'; +import { ApiKeyService } from '@app/unraid-api/auth/api-key.service.js'; import { DynamicRemoteAccessType, + URL_TYPE, WAN_ACCESS_TYPE, WAN_FORWARD_TYPE, -} from '@app/graphql/generated/api/types.js'; -import { setupRemoteAccessThunk } from '@app/store/actions/setup-remote-access.js'; -import { setSsoUsers, updateAllowedOrigins, updateUserConfig } from '@app/store/modules/config.js'; +} from '@app/unraid-api/graph/resolvers/connect/connect.model.js'; import { mergeSettingSlices } from '@app/unraid-api/types/json-forms.js'; import { csvStringToArray } from '@app/utils.js'; @Injectable() export class ConnectSettingsService { + constructor(private readonly apiKeyService: ApiKeyService) {} + + private readonly logger = new Logger(ConnectSettingsService.name); + + async restartApi() { + try { + await execa('unraid-api', ['restart'], { shell: 'bash', stdio: 'ignore' }); + } catch (error) { + this.logger.error(error); + } + } + + public async extraAllowedOrigins(): Promise> { + const extraOrigins = getExtraOrigins(); + return extraOrigins; + } + isConnectPluginInstalled(): boolean { // logic ported from webguid return ['/var/lib/pkgtools/packages/dynamix.unraid.net', '/usr/local/sbin/unraid-api'].every( @@ -31,6 +62,44 @@ export class ConnectSettingsService { ); } + public async enableDynamicRemoteAccess(input: EnableDynamicRemoteAccessInput): Promise { + const { store } = await import('@app/store/index.js'); + const { RemoteAccessController } = await import('@app/remoteAccess/remote-access-controller.js'); + // Start or extend dynamic remote access + const state = store.getState(); + + const { dynamicRemoteAccessType } = state.config.remote; + if (!dynamicRemoteAccessType || dynamicRemoteAccessType === DynamicRemoteAccessType.DISABLED) { + throw new GraphQLError('Dynamic Remote Access is not enabled.', { + extensions: { code: 'FORBIDDEN' }, + }); + } + + const controller = RemoteAccessController.instance; + + if (input.enabled === false) { + await controller.stopRemoteAccess({ + getState: store.getState, + dispatch: store.dispatch, + }); + return true; + } else if (controller.getRunningRemoteAccessType() === DynamicRemoteAccessType.DISABLED) { + if (input.url) { + store.dispatch(setAllowedRemoteAccessUrl(input.url)); + } + await controller.beginRemoteAccess({ + getState: store.getState, + dispatch: store.dispatch, + }); + } else { + controller.extendRemoteAccess({ + getState: store.getState, + dispatch: store.dispatch, + }); + } + return true; + } + async isSignedIn(): Promise { if (!this.isConnectPluginInstalled()) return false; const { getters } = await import('@app/store/index.js'); @@ -106,6 +175,62 @@ export class ConnectSettingsService { store.dispatch(updateAllowedOrigins(origins)); } + private async getOrCreateLocalApiKey() { + const { getters } = await import('@app/store/index.js'); + const { localApiKey: localApiKeyFromConfig } = getters.config().remote; + if (localApiKeyFromConfig === '') { + const localApiKey = await this.apiKeyService.createLocalConnectApiKey(); + if (!localApiKey?.key) { + throw new GraphQLError('Failed to create local API key', { + extensions: { code: 'INTERNAL_SERVER_ERROR' }, + }); + } + return localApiKey.key; + } + return localApiKeyFromConfig; + } + + async signIn(input: ConnectSignInInput) { + const { getters, store } = await import('@app/store/index.js'); + if (getters.emhttp().status === FileLoadStatus.LOADED) { + const userInfo = input.idToken ? decodeJwt(input.idToken) : (input.userInfo ?? null); + + if ( + !userInfo || + !userInfo.preferred_username || + !userInfo.email || + typeof userInfo.preferred_username !== 'string' || + typeof userInfo.email !== 'string' + ) { + throw new GraphQLError('Missing User Attributes', { + extensions: { code: 'BAD_REQUEST' }, + }); + } + + try { + const localApiKey = await this.getOrCreateLocalApiKey(); + + await store.dispatch( + loginUser({ + avatar: typeof userInfo.avatar === 'string' ? userInfo.avatar : '', + username: userInfo.preferred_username, + email: userInfo.email, + apikey: input.apiKey, + localApiKey, + }) + ); + + return true; + } catch (error) { + throw new GraphQLError(`Failed to login user: ${error}`, { + extensions: { code: 'INTERNAL_SERVER_ERROR' }, + }); + } + } else { + return false; + } + } + /** * Sets the sandbox mode and returns true if the mode was changed * @param sandboxEnabled - Whether to enable sandbox mode @@ -152,7 +277,7 @@ export class ConnectSettingsService { return true; } - private async dynamicRemoteAccessSettings(): Promise> { + public async dynamicRemoteAccessSettings(): Promise { const { getters } = await import('@app/store/index.js'); const hasWanAccess = getters.config().remote.wanaccess === 'yes'; return { diff --git a/api/src/unraid-api/graph/resolvers/connect/connect.model.ts b/api/src/unraid-api/graph/resolvers/connect/connect.model.ts new file mode 100644 index 000000000..2f68b1407 --- /dev/null +++ b/api/src/unraid-api/graph/resolvers/connect/connect.model.ts @@ -0,0 +1,375 @@ +import { Field, ID, InputType, Int, ObjectType, registerEnumType } from '@nestjs/graphql'; + +import { + ArrayMinSize, + IsArray, + IsBoolean, + IsEmail, + IsEnum, + IsNotEmpty, + IsObject, + IsOptional, + IsPort, + IsString, + MinLength, + ValidateNested, +} from 'class-validator'; +import { GraphQLJSON, GraphQLURL } from 'graphql-scalars'; + +import { Node } from '@app/unraid-api/graph/resolvers/base.model.js'; + +export enum WAN_ACCESS_TYPE { + DYNAMIC = 'DYNAMIC', + ALWAYS = 'ALWAYS', + DISABLED = 'DISABLED', +} + +export enum WAN_FORWARD_TYPE { + UPNP = 'UPNP', + STATIC = 'STATIC', +} + +export enum DynamicRemoteAccessType { + STATIC = 'STATIC', + UPNP = 'UPNP', + DISABLED = 'DISABLED', +} + +export enum URL_TYPE { + LAN = 'LAN', + WIREGUARD = 'WIREGUARD', + WAN = 'WAN', + MDNS = 'MDNS', + OTHER = 'OTHER', + DEFAULT = 'DEFAULT', +} + +registerEnumType(URL_TYPE, { + name: 'URL_TYPE', +}); + +registerEnumType(DynamicRemoteAccessType, { + name: 'DynamicRemoteAccessType', +}); + +registerEnumType(WAN_ACCESS_TYPE, { + name: 'WAN_ACCESS_TYPE', +}); + +registerEnumType(WAN_FORWARD_TYPE, { + name: 'WAN_FORWARD_TYPE', +}); + +@InputType() +export class AccessUrlInput { + @Field(() => URL_TYPE) + @IsEnum(URL_TYPE) + type!: URL_TYPE; + + @Field(() => String, { nullable: true }) + @IsOptional() + name?: string | null; + + @Field(() => GraphQLURL, { nullable: true }) + @IsOptional() + ipv4?: URL | null; + + @Field(() => GraphQLURL, { nullable: true }) + @IsOptional() + ipv6?: URL | null; +} + +/** + * This defines the LOCAL server Access URLs - these are sent to Connect if needed to share access routes + */ +@ObjectType() +export class AccessUrl { + @Field(() => URL_TYPE) + type!: URL_TYPE; + + @Field(() => String, { nullable: true }) + name?: string | null; + + @Field(() => GraphQLURL, { nullable: true }) + ipv4?: URL | null; + + @Field(() => GraphQLURL, { nullable: true }) + ipv6?: URL | null; +} + +@InputType() +export class ConnectUserInfoInput { + @Field(() => String, { description: 'The preferred username of the user' }) + @IsString() + @IsNotEmpty() + preferred_username!: string; + + @Field(() => String, { description: 'The email address of the user' }) + @IsEmail() + @IsNotEmpty() + email!: string; + + @Field(() => String, { nullable: true, description: 'The avatar URL of the user' }) + @IsString() + @IsOptional() + avatar?: string; +} + +@InputType() +export class ConnectSignInInput { + @Field(() => String, { description: 'The API key for authentication' }) + @IsString() + @IsNotEmpty() + @MinLength(5) + apiKey!: string; + + @Field(() => String, { nullable: true, description: 'The ID token for authentication' }) + @IsString() + @IsOptional() + idToken?: string; + + @Field(() => ConnectUserInfoInput, { + nullable: true, + description: 'User information for the sign-in', + }) + @ValidateNested() + @IsOptional() + userInfo?: ConnectUserInfoInput; + + @Field(() => String, { nullable: true, description: 'The access token for authentication' }) + @IsString() + @IsOptional() + accessToken?: string; + + @Field(() => String, { nullable: true, description: 'The refresh token for authentication' }) + @IsString() + @IsOptional() + refreshToken?: string; +} + +@InputType() +export class AllowedOriginInput { + @Field(() => [String], { description: 'A list of origins allowed to interact with the API' }) + @IsArray() + @ArrayMinSize(1) + @IsString({ each: true }) + origins!: string[]; +} + +@ObjectType() +export class RemoteAccess { + @Field(() => WAN_ACCESS_TYPE, { description: 'The type of WAN access used for Remote Access' }) + @IsEnum(WAN_ACCESS_TYPE) + accessType!: WAN_ACCESS_TYPE; + + @Field(() => WAN_FORWARD_TYPE, { + nullable: true, + description: 'The type of port forwarding used for Remote Access', + }) + @IsEnum(WAN_FORWARD_TYPE) + @IsOptional() + forwardType?: WAN_FORWARD_TYPE; + + @Field(() => Int, { nullable: true, description: 'The port used for Remote Access' }) + @IsPort() + @IsOptional() + port?: number | null; +} + +@InputType() +export class SetupRemoteAccessInput { + @Field(() => WAN_ACCESS_TYPE, { description: 'The type of WAN access to use for Remote Access' }) + @IsEnum(WAN_ACCESS_TYPE) + accessType!: WAN_ACCESS_TYPE; + + @Field(() => WAN_FORWARD_TYPE, { + nullable: true, + description: 'The type of port forwarding to use for Remote Access', + }) + @IsEnum(WAN_FORWARD_TYPE) + @IsOptional() + forwardType?: WAN_FORWARD_TYPE | null; + + @Field(() => Int, { + nullable: true, + description: + 'The port to use for Remote Access. Not required for UPNP forwardType. Required for STATIC forwardType. Ignored if accessType is DISABLED or forwardType is UPNP.', + }) + @IsPort() + @IsOptional() + port?: number | null; +} + +@InputType() +export class EnableDynamicRemoteAccessInput { + @Field(() => AccessUrlInput, { description: 'The AccessURL Input for dynamic remote access' }) + @ValidateNested() + url!: AccessUrlInput; + + @Field(() => Boolean, { description: 'Whether to enable or disable dynamic remote access' }) + @IsBoolean() + enabled!: boolean; +} + +@ObjectType() +export class DynamicRemoteAccessStatus { + @Field(() => DynamicRemoteAccessType, { + description: 'The type of dynamic remote access that is enabled', + }) + @IsEnum(DynamicRemoteAccessType) + enabledType!: DynamicRemoteAccessType; + + @Field(() => DynamicRemoteAccessType, { + description: 'The type of dynamic remote access that is currently running', + }) + @IsEnum(DynamicRemoteAccessType) + runningType!: DynamicRemoteAccessType; + + @Field(() => String, { + nullable: true, + description: 'Any error message associated with the dynamic remote access', + }) + @IsString() + @IsOptional() + error?: string; +} + +@ObjectType() +export class ConnectSettingsValues { + @Field(() => Boolean, { + description: + 'If true, the GraphQL sandbox is enabled and available at /graphql. If false, the GraphQL sandbox is disabled and only the production API will be available.', + }) + @IsBoolean() + sandbox!: boolean; + + @Field(() => [String], { description: 'A list of origins allowed to interact with the API' }) + @IsArray() + @IsString({ each: true }) + extraOrigins!: string[]; + + @Field(() => WAN_ACCESS_TYPE, { description: 'The type of WAN access used for Remote Access' }) + @IsEnum(WAN_ACCESS_TYPE) + accessType!: WAN_ACCESS_TYPE; + + @Field(() => WAN_FORWARD_TYPE, { + nullable: true, + description: 'The type of port forwarding used for Remote Access', + }) + @IsEnum(WAN_FORWARD_TYPE) + @IsOptional() + forwardType?: WAN_FORWARD_TYPE; + + @Field(() => Int, { nullable: true, description: 'The port used for Remote Access' }) + @IsPort() + @IsOptional() + port?: number | null; + + @Field(() => [String], { description: "A list of Unique Unraid Account ID's" }) + @IsArray() + @IsString({ each: true }) + ssoUserIds!: string[]; +} + +@InputType() +export class ApiSettingsInput { + @Field(() => Boolean, { + nullable: true, + description: + 'If true, the GraphQL sandbox will be enabled and available at /graphql. If false, the GraphQL sandbox will be disabled and only the production API will be available.', + }) + @IsBoolean() + @IsOptional() + sandbox?: boolean | null; + + @Field(() => [String], { + nullable: true, + description: 'A list of origins allowed to interact with the API', + }) + @IsArray() + @IsString({ each: true }) + @IsOptional() + extraOrigins?: string[] | null; + + @Field(() => WAN_ACCESS_TYPE, { + nullable: true, + description: 'The type of WAN access to use for Remote Access', + }) + @IsEnum(WAN_ACCESS_TYPE) + @IsOptional() + accessType?: WAN_ACCESS_TYPE | null; + + @Field(() => WAN_FORWARD_TYPE, { + nullable: true, + description: 'The type of port forwarding to use for Remote Access', + }) + @IsEnum(WAN_FORWARD_TYPE) + @IsOptional() + forwardType?: WAN_FORWARD_TYPE | null; + + @Field(() => Int, { + nullable: true, + description: + 'The port to use for Remote Access. Not required for UPNP forwardType. Required for STATIC forwardType. Ignored if accessType is DISABLED or forwardType is UPNP.', + }) + @IsPort() + @IsOptional() + port?: number | null; + + @Field(() => [String], { nullable: true, description: "A list of Unique Unraid Account ID's" }) + @IsArray() + @IsString({ each: true }) + @IsOptional() + ssoUserIds?: string[] | null; +} + +@ObjectType({ + implements: () => Node, +}) +export class ConnectSettings implements Node { + @Field(() => ID, { description: 'The unique identifier for the Connect settings' }) + @IsString() + @IsNotEmpty() + id!: string; + + @Field(() => GraphQLJSON, { description: 'The data schema for the Connect settings' }) + @IsObject() + dataSchema!: Record; + + @Field(() => GraphQLJSON, { description: 'The UI schema for the Connect settings' }) + @IsObject() + uiSchema!: Record; + + @Field(() => ConnectSettingsValues, { description: 'The values for the Connect settings' }) + @ValidateNested() + values!: ConnectSettingsValues; +} + +@ObjectType({ + implements: () => Node, +}) +export class Connect { + @Field(() => ID, { description: 'The unique identifier for the Connect instance' }) + @IsString() + @IsNotEmpty() + id!: string; + + @Field(() => DynamicRemoteAccessStatus, { description: 'The status of dynamic remote access' }) + @ValidateNested() + dynamicRemoteAccess?: DynamicRemoteAccessStatus; + + @Field(() => ConnectSettings, { description: 'The settings for the Connect instance' }) + @ValidateNested() + settings?: ConnectSettings; +} + +@ObjectType({ + implements: () => Node, +}) +export class Network implements Node { + @Field(() => ID) + id!: string; + + @Field(() => [AccessUrl], { nullable: true }) + accessUrls?: AccessUrl[]; +} diff --git a/api/src/unraid-api/graph/resolvers/connect/connect.module.ts b/api/src/unraid-api/graph/resolvers/connect/connect.module.ts new file mode 100644 index 000000000..86a499fbe --- /dev/null +++ b/api/src/unraid-api/graph/resolvers/connect/connect.module.ts @@ -0,0 +1,12 @@ +import { Module } from '@nestjs/common'; + +import { AuthModule } from '@app/unraid-api/auth/auth.module.js'; +import { ConnectSettingsResolver } from '@app/unraid-api/graph/resolvers/connect/connect-settings.resolver.js'; +import { ConnectSettingsService } from '@app/unraid-api/graph/resolvers/connect/connect-settings.service.js'; +import { ConnectResolver } from '@app/unraid-api/graph/resolvers/connect/connect.resolver.js'; + +@Module({ + imports: [AuthModule], + providers: [ConnectResolver, ConnectSettingsResolver, ConnectSettingsService], +}) +export class ConnectModule {} diff --git a/api/src/unraid-api/graph/resolvers/connect/connect.resolver.ts b/api/src/unraid-api/graph/resolvers/connect/connect.resolver.ts new file mode 100644 index 000000000..4a818f57c --- /dev/null +++ b/api/src/unraid-api/graph/resolvers/connect/connect.resolver.ts @@ -0,0 +1,51 @@ +import { Logger } from '@nestjs/common'; +import { Query, ResolveField, Resolver } from '@nestjs/graphql'; + +import { AuthActionVerb, AuthPossession, UsePermissions } from 'nest-authz'; + +import { store } from '@app/store/index.js'; +import { Resource } from '@app/unraid-api/graph/resolvers/base.model.js'; +import { + Connect, + ConnectSettings, + DynamicRemoteAccessStatus, + DynamicRemoteAccessType, +} from '@app/unraid-api/graph/resolvers/connect/connect.model.js'; + +@Resolver(() => Connect) +export class ConnectResolver { + protected logger = new Logger(ConnectResolver.name); + constructor() {} + + @Query(() => Connect) + @UsePermissions({ + action: AuthActionVerb.READ, + resource: Resource.CONNECT, + possession: AuthPossession.ANY, + }) + public connect(): Connect { + return { + id: 'connect', + }; + } + + @ResolveField(() => String) + public id() { + return 'connect'; + } + + @ResolveField(() => DynamicRemoteAccessStatus) + public dynamicRemoteAccess(): DynamicRemoteAccessStatus { + const state = store.getState(); + return { + runningType: state.dynamicRemoteAccess.runningType, + enabledType: state.config.remote.dynamicRemoteAccessType ?? DynamicRemoteAccessType.DISABLED, + error: state.dynamicRemoteAccess.error ?? undefined, + }; + } + + @ResolveField(() => ConnectSettings) + public async settings(): Promise { + return {} as ConnectSettings; + } +} diff --git a/api/src/unraid-api/graph/resolvers/disks/disks.model.ts b/api/src/unraid-api/graph/resolvers/disks/disks.model.ts new file mode 100644 index 000000000..558b786f0 --- /dev/null +++ b/api/src/unraid-api/graph/resolvers/disks/disks.model.ts @@ -0,0 +1,141 @@ +import { Field, ObjectType, registerEnumType } from '@nestjs/graphql'; + +import { Type } from 'class-transformer'; +import { IsArray, IsEnum, IsNumber, IsOptional, IsString, ValidateNested } from 'class-validator'; + +export enum DiskFsType { + XFS = 'XFS', + BTRFS = 'BTRFS', + VFAT = 'VFAT', + ZFS = 'ZFS', + EXT4 = 'EXT4', + NTFS = 'NTFS', +} + +registerEnumType(DiskFsType, { + name: 'DiskFsType', + description: 'The type of filesystem on the disk partition', +}); + +export enum DiskInterfaceType { + SAS = 'SAS', + SATA = 'SATA', + USB = 'USB', + PCIE = 'PCIE', + UNKNOWN = 'UNKNOWN', +} + +registerEnumType(DiskInterfaceType, { + name: 'DiskInterfaceType', + description: 'The type of interface the disk uses to connect to the system', +}); + +export enum DiskSmartStatus { + OK = 'OK', + UNKNOWN = 'UNKNOWN', +} + +registerEnumType(DiskSmartStatus, { + name: 'DiskSmartStatus', + description: 'The SMART (Self-Monitoring, Analysis and Reporting Technology) status of the disk', +}); + +@ObjectType() +export class DiskPartition { + @Field(() => String, { description: 'The name of the partition' }) + @IsString() + name!: string; + + @Field(() => DiskFsType, { description: 'The filesystem type of the partition' }) + @IsEnum(DiskFsType) + fsType!: DiskFsType; + + @Field(() => Number, { description: 'The size of the partition in bytes' }) + @IsNumber() + size!: number; +} + +@ObjectType() +export class Disk { + @Field(() => String, { description: 'The unique identifier of the disk' }) + @IsString() + id!: string; + + @Field(() => String, { description: 'The device path of the disk (e.g. /dev/sdb)' }) + @IsString() + device!: string; + + @Field(() => String, { description: 'The type of disk (e.g. SSD, HDD)' }) + @IsString() + type!: string; + + @Field(() => String, { description: 'The model name of the disk' }) + @IsString() + name!: string; + + @Field(() => String, { description: 'The manufacturer of the disk' }) + @IsString() + vendor!: string; + + @Field(() => Number, { description: 'The total size of the disk in bytes' }) + @IsNumber() + size!: number; + + @Field(() => Number, { description: 'The number of bytes per sector' }) + @IsNumber() + bytesPerSector!: number; + + @Field(() => Number, { description: 'The total number of cylinders on the disk' }) + @IsNumber() + totalCylinders!: number; + + @Field(() => Number, { description: 'The total number of heads on the disk' }) + @IsNumber() + totalHeads!: number; + + @Field(() => Number, { description: 'The total number of sectors on the disk' }) + @IsNumber() + totalSectors!: number; + + @Field(() => Number, { description: 'The total number of tracks on the disk' }) + @IsNumber() + totalTracks!: number; + + @Field(() => Number, { description: 'The number of tracks per cylinder' }) + @IsNumber() + tracksPerCylinder!: number; + + @Field(() => Number, { description: 'The number of sectors per track' }) + @IsNumber() + sectorsPerTrack!: number; + + @Field(() => String, { description: 'The firmware revision of the disk' }) + @IsString() + firmwareRevision!: string; + + @Field(() => String, { description: 'The serial number of the disk' }) + @IsString() + serialNum!: string; + + @Field(() => DiskInterfaceType, { description: 'The interface type of the disk' }) + @IsEnum(DiskInterfaceType) + interfaceType!: DiskInterfaceType; + + @Field(() => DiskSmartStatus, { description: 'The SMART status of the disk' }) + @IsEnum(DiskSmartStatus) + smartStatus!: DiskSmartStatus; + + @Field(() => Number, { + description: 'The current temperature of the disk in Celsius', + nullable: true, + }) + @IsOptional() + @IsNumber() + temperature?: number; + + @Field(() => [DiskPartition], { description: 'The partitions on the disk' }) + @IsArray() + @ValidateNested({ each: true }) + @Type(() => DiskPartition) + partitions!: DiskPartition[]; +} diff --git a/api/src/unraid-api/graph/resolvers/disks/disks.resolver.spec.ts b/api/src/unraid-api/graph/resolvers/disks/disks.resolver.spec.ts index aede3d6d3..609bcad70 100644 --- a/api/src/unraid-api/graph/resolvers/disks/disks.resolver.spec.ts +++ b/api/src/unraid-api/graph/resolvers/disks/disks.resolver.spec.ts @@ -3,8 +3,11 @@ import { Test } from '@nestjs/testing'; import { beforeEach, describe, expect, it, vi } from 'vitest'; -import type { Disk } from '@app/graphql/generated/api/types.js'; -import { DiskInterfaceType, DiskSmartStatus } from '@app/graphql/generated/api/types.js'; +import { + Disk, + DiskInterfaceType, + DiskSmartStatus, +} from '@app/unraid-api/graph/resolvers/disks/disks.model.js'; import { DisksResolver } from '@app/unraid-api/graph/resolvers/disks/disks.resolver.js'; import { DisksService } from '@app/unraid-api/graph/resolvers/disks/disks.service.js'; // Renamed from DiskService diff --git a/api/src/unraid-api/graph/resolvers/disks/disks.resolver.ts b/api/src/unraid-api/graph/resolvers/disks/disks.resolver.ts index 67a9c9246..e995cdee4 100644 --- a/api/src/unraid-api/graph/resolvers/disks/disks.resolver.ts +++ b/api/src/unraid-api/graph/resolvers/disks/disks.resolver.ts @@ -1,16 +1,16 @@ -import { Parent, Query, ResolveField, Resolver } from '@nestjs/graphql'; +import { Args, Int, Parent, Query, ResolveField, Resolver } from '@nestjs/graphql'; import { AuthActionVerb, AuthPossession, UsePermissions } from 'nest-authz'; -import type { Disk } from '@app/graphql/generated/api/types.js'; -import { Resource } from '@app/graphql/generated/api/types.js'; +import { Resource } from '@app/unraid-api/graph/resolvers/base.model.js'; +import { Disk } from '@app/unraid-api/graph/resolvers/disks/disks.model.js'; import { DisksService } from '@app/unraid-api/graph/resolvers/disks/disks.service.js'; -@Resolver('Disk') +@Resolver(() => Disk) export class DisksResolver { constructor(private readonly disksService: DisksService) {} - @Query('disks') + @Query(() => [Disk]) @UsePermissions({ action: AuthActionVerb.READ, resource: Resource.DISK, @@ -20,7 +20,17 @@ export class DisksResolver { return this.disksService.getDisks(); } - @ResolveField('temperature') + @Query(() => Disk) + @UsePermissions({ + action: AuthActionVerb.READ, + resource: Resource.DISK, + possession: AuthPossession.ANY, + }) + public async disk(@Args('id') id: string) { + return this.disksService.getDisk(id); + } + + @ResolveField(() => Int) public async temperature(@Parent() disk: Disk) { return this.disksService.getTemperature(disk.device); } diff --git a/api/src/unraid-api/graph/resolvers/disks/disks.service.spec.ts b/api/src/unraid-api/graph/resolvers/disks/disks.service.spec.ts index 7efbe605e..0763dc57a 100644 --- a/api/src/unraid-api/graph/resolvers/disks/disks.service.spec.ts +++ b/api/src/unraid-api/graph/resolvers/disks/disks.service.spec.ts @@ -6,8 +6,12 @@ import { blockDevices, diskLayout } from 'systeminformation'; // Vitest imports import { beforeEach, describe, expect, it, Mock, MockedFunction, vi } from 'vitest'; -import type { Disk } from '@app/graphql/generated/api/types.js'; -import { DiskFsType, DiskInterfaceType, DiskSmartStatus } from '@app/graphql/generated/api/types.js'; +import { + Disk, + DiskFsType, + DiskInterfaceType, + DiskSmartStatus, +} from '@app/unraid-api/graph/resolvers/disks/disks.model.js'; import { DisksService } from '@app/unraid-api/graph/resolvers/disks/disks.service.js'; import { batchProcess } from '@app/utils.js'; diff --git a/api/src/unraid-api/graph/resolvers/disks/disks.service.ts b/api/src/unraid-api/graph/resolvers/disks/disks.service.ts index 4908840d4..e931ba1d2 100644 --- a/api/src/unraid-api/graph/resolvers/disks/disks.service.ts +++ b/api/src/unraid-api/graph/resolvers/disks/disks.service.ts @@ -1,11 +1,15 @@ -import { Injectable } from '@nestjs/common'; +import { Injectable, NotFoundException } from '@nestjs/common'; import type { Systeminformation } from 'systeminformation'; import { execa } from 'execa'; import { blockDevices, diskLayout } from 'systeminformation'; -import type { Disk } from '@app/graphql/generated/api/types.js'; -import { DiskFsType, DiskInterfaceType, DiskSmartStatus } from '@app/graphql/generated/api/types.js'; +import { + Disk, + DiskFsType, + DiskInterfaceType, + DiskSmartStatus, +} from '@app/unraid-api/graph/resolvers/disks/disks.model.js'; import { batchProcess } from '@app/utils.js'; @Injectable() @@ -36,10 +40,19 @@ export class DisksService { } } + public async getDisk(id: string): Promise { + const disks = await this.getDisks(); + const disk = disks.find((d) => d.id === id); + if (!disk) { + throw new NotFoundException(`Disk with id ${id} not found`); + } + return disk; + } + private async parseDisk( disk: Systeminformation.DiskLayoutData, partitionsToParse: Systeminformation.BlockDevicesData[] - ): Promise { + ): Promise> { const partitions = partitionsToParse // Only get partitions from this disk .filter((partition) => partition.name.startsWith(disk.device.split('/dev/')[1])) diff --git a/api/src/unraid-api/graph/resolvers/display/display.resolver.ts b/api/src/unraid-api/graph/resolvers/display/display.resolver.ts index 95fc57986..f6771d85d 100644 --- a/api/src/unraid-api/graph/resolvers/display/display.resolver.ts +++ b/api/src/unraid-api/graph/resolvers/display/display.resolver.ts @@ -5,10 +5,10 @@ import { join } from 'node:path'; import { AuthActionVerb, AuthPossession, UsePermissions } from 'nest-authz'; -import type { Display } from '@app/graphql/generated/api/types.js'; import { createSubscription, PUBSUB_CHANNEL } from '@app/core/pubsub.js'; -import { Resource } from '@app/graphql/generated/api/types.js'; import { getters } from '@app/store/index.js'; +import { Resource } from '@app/unraid-api/graph/resolvers/base.model.js'; +import { Display } from '@app/unraid-api/graph/resolvers/info/info.model.js'; const states = { // Success @@ -58,9 +58,9 @@ const states = { }, }; -@Resolver('Display') +@Resolver(() => Display) export class DisplayResolver { - @Query() + @Query(() => Display) @UsePermissions({ action: AuthActionVerb.READ, resource: Resource.DISPLAY, @@ -110,7 +110,7 @@ export class DisplayResolver { }; } - @Subscription('display') + @Subscription(() => Display) @UsePermissions({ action: AuthActionVerb.READ, resource: Resource.DISPLAY, diff --git a/api/src/unraid-api/graph/resolvers/docker/docker.model.ts b/api/src/unraid-api/graph/resolvers/docker/docker.model.ts new file mode 100644 index 000000000..7bd2c2fe3 --- /dev/null +++ b/api/src/unraid-api/graph/resolvers/docker/docker.model.ts @@ -0,0 +1,182 @@ +import { Field, ID, Int, ObjectType, registerEnumType } from '@nestjs/graphql'; + +import { GraphQLPort } from 'graphql-scalars'; +import { GraphQLJSONObject } from 'graphql-type-json'; + +import { Node } from '@app/unraid-api/graph/resolvers/base.model.js'; + +export enum ContainerPortType { + TCP = 'TCP', + UDP = 'UDP', +} + +registerEnumType(ContainerPortType, { + name: 'ContainerPortType', +}); + +@ObjectType() +export class ContainerPort { + @Field(() => String, { nullable: true }) + ip?: string; + + @Field(() => GraphQLPort, { nullable: true }) + privatePort?: number; + + @Field(() => GraphQLPort, { nullable: true }) + publicPort?: number; + + @Field(() => ContainerPortType) + type!: ContainerPortType; +} + +export enum ContainerState { + RUNNING = 'RUNNING', + EXITED = 'EXITED', +} + +registerEnumType(ContainerState, { + name: 'ContainerState', +}); + +@ObjectType() +export class ContainerHostConfig { + @Field(() => String) + networkMode!: string; +} + +@ObjectType() +export class ContainerMount { + @Field(() => String) + type!: string; + + @Field(() => String) + name!: string; + + @Field(() => String) + source!: string; + + @Field(() => String) + destination!: string; + + @Field(() => String) + driver!: string; + + @Field(() => String) + mode!: string; + + @Field(() => Boolean) + rw!: boolean; + + @Field(() => String) + propagation!: string; +} + +@ObjectType() +export class DockerContainer { + @Field(() => ID) + id!: string; + + @Field(() => [String]) + names!: string[]; + + @Field(() => String) + image!: string; + + @Field(() => String) + imageId!: string; + + @Field(() => String) + command!: string; + + @Field(() => Int) + created!: number; + + @Field(() => [ContainerPort]) + ports!: ContainerPort[]; + + @Field(() => Int, { nullable: true, description: 'Total size of all the files in the container' }) + sizeRootFs?: number; + + @Field(() => GraphQLJSONObject, { nullable: true }) + labels?: Record; + + @Field(() => ContainerState) + state!: ContainerState; + + @Field(() => String) + status!: string; + + @Field(() => ContainerHostConfig, { nullable: true }) + hostConfig?: ContainerHostConfig; + + @Field(() => GraphQLJSONObject, { nullable: true }) + networkSettings?: Record; + + @Field(() => [GraphQLJSONObject], { nullable: true }) + mounts?: Record[]; + + @Field(() => Boolean) + autoStart!: boolean; +} + +@ObjectType() +export class DockerNetwork { + @Field(() => String) + name!: string; + + @Field(() => ID) + id!: string; + + @Field(() => String) + created!: string; + + @Field(() => String) + scope!: string; + + @Field(() => String) + driver!: string; + + @Field(() => Boolean) + enableIPv6!: boolean; + + @Field(() => GraphQLJSONObject) + ipam!: Record; + + @Field(() => Boolean) + internal!: boolean; + + @Field(() => Boolean) + attachable!: boolean; + + @Field(() => Boolean) + ingress!: boolean; + + @Field(() => GraphQLJSONObject) + configFrom!: Record; + + @Field(() => Boolean) + configOnly!: boolean; + + @Field(() => GraphQLJSONObject) + containers!: Record; + + @Field(() => GraphQLJSONObject) + options!: Record; + + @Field(() => GraphQLJSONObject) + labels!: Record; +} + +@ObjectType({ + implements: () => Node, +}) +export class Docker implements Node { + @Field(() => ID) + id!: string; + + @Field(() => [DockerContainer]) + containers!: DockerContainer[]; + + @Field(() => [DockerNetwork]) + networks!: DockerNetwork[]; +} diff --git a/api/src/unraid-api/graph/resolvers/docker/docker.mutations.resolver.spec.ts b/api/src/unraid-api/graph/resolvers/docker/docker.mutations.resolver.spec.ts index c83375c06..97e062604 100644 --- a/api/src/unraid-api/graph/resolvers/docker/docker.mutations.resolver.spec.ts +++ b/api/src/unraid-api/graph/resolvers/docker/docker.mutations.resolver.spec.ts @@ -3,8 +3,7 @@ import { Test } from '@nestjs/testing'; import { beforeEach, describe, expect, it, vi } from 'vitest'; -import type { DockerContainer } from '@app/graphql/generated/api/types.js'; -import { ContainerState } from '@app/graphql/generated/api/types.js'; +import { ContainerState, DockerContainer } from '@app/unraid-api/graph/resolvers/docker/docker.model.js'; import { DockerMutationsResolver } from '@app/unraid-api/graph/resolvers/docker/docker.mutations.resolver.js'; import { DockerService } from '@app/unraid-api/graph/resolvers/docker/docker.service.js'; @@ -45,6 +44,7 @@ describe('DockerMutationsResolver', () => { ports: [], state: ContainerState.RUNNING, status: 'Up 2 hours', + names: ['test-container'], }; vi.mocked(dockerService.start).mockResolvedValue(mockContainer); @@ -64,6 +64,7 @@ describe('DockerMutationsResolver', () => { ports: [], state: ContainerState.EXITED, status: 'Exited', + names: ['test-container'], }; vi.mocked(dockerService.stop).mockResolvedValue(mockContainer); diff --git a/api/src/unraid-api/graph/resolvers/docker/docker.mutations.resolver.ts b/api/src/unraid-api/graph/resolvers/docker/docker.mutations.resolver.ts index 2ddaba4a1..eb66e221c 100644 --- a/api/src/unraid-api/graph/resolvers/docker/docker.mutations.resolver.ts +++ b/api/src/unraid-api/graph/resolvers/docker/docker.mutations.resolver.ts @@ -2,14 +2,19 @@ import { Args, ResolveField, Resolver } from '@nestjs/graphql'; import { AuthActionVerb, AuthPossession, UsePermissions } from 'nest-authz'; -import { Resource } from '@app/graphql/generated/api/types.js'; +import { Resource } from '@app/unraid-api/graph/resolvers/base.model.js'; +import { DockerContainer } from '@app/unraid-api/graph/resolvers/docker/docker.model.js'; import { DockerService } from '@app/unraid-api/graph/resolvers/docker/docker.service.js'; +import { DockerMutations } from '@app/unraid-api/graph/resolvers/mutation/mutation.model.js'; -@Resolver('DockerMutations') +/** + * Nested Resolvers for Mutations MUST use @ResolveField() instead of @Mutation() + */ +@Resolver(() => DockerMutations) export class DockerMutationsResolver { constructor(private readonly dockerService: DockerService) {} - @ResolveField('start') + @ResolveField(() => DockerContainer, { description: 'Start a container' }) @UsePermissions({ action: AuthActionVerb.UPDATE, resource: Resource.DOCKER, @@ -19,7 +24,7 @@ export class DockerMutationsResolver { return this.dockerService.start(id); } - @ResolveField('stop') + @ResolveField(() => DockerContainer, { description: 'Stop a container' }) @UsePermissions({ action: AuthActionVerb.UPDATE, resource: Resource.DOCKER, diff --git a/api/src/unraid-api/graph/resolvers/docker/docker.resolver.spec.ts b/api/src/unraid-api/graph/resolvers/docker/docker.resolver.spec.ts index 8be518144..0884c485f 100644 --- a/api/src/unraid-api/graph/resolvers/docker/docker.resolver.spec.ts +++ b/api/src/unraid-api/graph/resolvers/docker/docker.resolver.spec.ts @@ -3,8 +3,7 @@ import { Test } from '@nestjs/testing'; import { beforeEach, describe, expect, it, vi } from 'vitest'; -import type { DockerContainer } from '@app/graphql/generated/api/types.js'; -import { ContainerState } from '@app/graphql/generated/api/types.js'; +import { ContainerState, DockerContainer } from '@app/unraid-api/graph/resolvers/docker/docker.model.js'; import { DockerResolver } from '@app/unraid-api/graph/resolvers/docker/docker.resolver.js'; import { DockerService } from '@app/unraid-api/graph/resolvers/docker/docker.service.js'; @@ -44,6 +43,7 @@ describe('DockerResolver', () => { id: '1', autoStart: false, command: 'test', + names: ['test-container'], created: 1234567890, image: 'test-image', imageId: 'test-image-id', @@ -55,6 +55,7 @@ describe('DockerResolver', () => { id: '2', autoStart: true, command: 'test2', + names: ['test-container2'], created: 1234567891, image: 'test-image2', imageId: 'test-image-id2', diff --git a/api/src/unraid-api/graph/resolvers/docker/docker.resolver.ts b/api/src/unraid-api/graph/resolvers/docker/docker.resolver.ts index 9c33c052a..7b4ff47fd 100644 --- a/api/src/unraid-api/graph/resolvers/docker/docker.resolver.ts +++ b/api/src/unraid-api/graph/resolvers/docker/docker.resolver.ts @@ -2,10 +2,15 @@ import { Query, ResolveField, Resolver } from '@nestjs/graphql'; import { AuthActionVerb, AuthPossession, UsePermissions } from 'nest-authz'; -import { Resource } from '@app/graphql/generated/api/types.js'; +import { Resource } from '@app/unraid-api/graph/resolvers/base.model.js'; +import { + Docker, + DockerContainer, + DockerNetwork, +} from '@app/unraid-api/graph/resolvers/docker/docker.model.js'; import { DockerService } from '@app/unraid-api/graph/resolvers/docker/docker.service.js'; -@Resolver('Docker') +@Resolver(() => Docker) export class DockerResolver { constructor(private readonly dockerService: DockerService) {} @@ -14,7 +19,7 @@ export class DockerResolver { resource: Resource.DOCKER, possession: AuthPossession.ANY, }) - @Query() + @Query(() => Docker) public docker() { return { id: 'docker', @@ -26,12 +31,17 @@ export class DockerResolver { resource: Resource.DOCKER, possession: AuthPossession.ANY, }) - @ResolveField() + @ResolveField(() => [DockerContainer]) public async containers() { return this.dockerService.getContainers({ useCache: false }); } - @ResolveField() + @UsePermissions({ + action: AuthActionVerb.READ, + resource: Resource.DOCKER, + possession: AuthPossession.ANY, + }) + @ResolveField(() => [DockerNetwork]) public async networks() { return this.dockerService.getNetworks({ useCache: false }); } diff --git a/api/src/unraid-api/graph/resolvers/docker/docker.service.spec.ts b/api/src/unraid-api/graph/resolvers/docker/docker.service.spec.ts index a872bc1da..2eb2b7099 100644 --- a/api/src/unraid-api/graph/resolvers/docker/docker.service.spec.ts +++ b/api/src/unraid-api/graph/resolvers/docker/docker.service.spec.ts @@ -4,7 +4,7 @@ import { Test } from '@nestjs/testing'; import Docker from 'dockerode'; import { beforeEach, describe, expect, it, vi } from 'vitest'; -import { ContainerState } from '@app/graphql/generated/api/types.js'; +import { ContainerState } from '@app/unraid-api/graph/resolvers/docker/docker.model.js'; import { DockerService } from '@app/unraid-api/graph/resolvers/docker/docker.service.js'; // Mock pubsub @@ -309,42 +309,44 @@ describe('DockerService', () => { const result = await service.getNetworks({ useCache: false }); - expect(result).toEqual([ + expect(result).toMatchInlineSnapshot(` + [ { - id: 'network1', - name: 'bridge', - created: '2023-01-01T00:00:00Z', - scope: 'local', - driver: 'bridge', - enableIpv6: false, - ipam: { - driver: 'default', - config: [ - { - subnet: '172.17.0.0/16', - gateway: '172.17.0.1', - }, - ], - }, - internal: false, - attachable: false, - ingress: false, - configFrom: { - network: '', - }, - configOnly: false, - containers: {}, - options: { - comDockerNetworkBridgeDefaultBridge: 'true', - comDockerNetworkBridgeEnableIcc: 'true', - comDockerNetworkBridgeEnableIpMasquerade: 'true', - comDockerNetworkBridgeHostBindingIpv4: '0.0.0.0', - comDockerNetworkBridgeName: 'docker0', - comDockerNetworkDriverMtu: '1500', - }, - labels: {}, + "attachable": false, + "configFrom": { + "Network": "", + }, + "configOnly": false, + "containers": {}, + "created": "2023-01-01T00:00:00Z", + "driver": "bridge", + "enableIPv6": false, + "id": "network1", + "ingress": false, + "internal": false, + "ipam": { + "Config": [ + { + "Gateway": "172.17.0.1", + "Subnet": "172.17.0.0/16", + }, + ], + "Driver": "default", + }, + "labels": {}, + "name": "bridge", + "options": { + "com.docker.network.bridge.default_bridge": "true", + "com.docker.network.bridge.enable_icc": "true", + "com.docker.network.bridge.enable_ip_masquerade": "true", + "com.docker.network.bridge.host_binding_ipv4": "0.0.0.0", + "com.docker.network.bridge.name": "docker0", + "com.docker.network.driver.mtu": "1500", + }, + "scope": "local", }, - ]); + ] + `); expect(mockListNetworks).toHaveBeenCalled(); }); diff --git a/api/src/unraid-api/graph/resolvers/docker/docker.service.ts b/api/src/unraid-api/graph/resolvers/docker/docker.service.ts index 170db88cd..be2431db1 100644 --- a/api/src/unraid-api/graph/resolvers/docker/docker.service.ts +++ b/api/src/unraid-api/graph/resolvers/docker/docker.service.ts @@ -5,12 +5,17 @@ import camelCaseKeys from 'camelcase-keys'; import Docker from 'dockerode'; import { debounce } from 'lodash-es'; -import type { ContainerPort, DockerContainer, DockerNetwork } from '@app/graphql/generated/api/types.js'; import { pubsub, PUBSUB_CHANNEL } from '@app/core/pubsub.js'; import { catchHandlers } from '@app/core/utils/misc/catch-handlers.js'; import { sleep } from '@app/core/utils/misc/sleep.js'; -import { ContainerPortType, ContainerState } from '@app/graphql/generated/api/types.js'; import { getters } from '@app/store/index.js'; +import { + ContainerPort, + ContainerPortType, + ContainerState, + DockerContainer, + DockerNetwork, +} from '@app/unraid-api/graph/resolvers/docker/docker.model.js'; interface ContainerListingOptions extends Docker.ContainerListOptions { useCache: boolean; @@ -77,34 +82,38 @@ export class DockerService implements OnModuleInit { }, 500); public transformContainer(container: Docker.ContainerInfo): DockerContainer { - return camelCaseKeys( - { - names: container.Names, - labels: container.Labels ?? {}, - sizeRootFs: undefined, - imageId: container.ImageID, - state: - typeof container.State === 'string' - ? (ContainerState[container.State.toUpperCase()] ?? ContainerState.EXITED) - : ContainerState.EXITED, - autoStart: this.autoStarts.includes(container.Names[0].split('/')[1]), - ports: container.Ports.map((port) => ({ - ...port, - type: ContainerPortType[port.Type.toUpperCase()], - })), - command: container.Command, - created: container.Created, - mounts: container.Mounts, - networkSettings: container.NetworkSettings, - hostConfig: { - networkMode: container.HostConfig.NetworkMode, - }, - id: container.Id, - image: container.Image, - status: container.Status, + const transformed: DockerContainer = { + id: container.Id, + names: container.Names, + image: container.Image, + imageId: container.ImageID, + command: container.Command, + created: container.Created, + ports: container.Ports.map((port) => ({ + ip: port.IP || '', + privatePort: port.PrivatePort, + publicPort: port.PublicPort, + type: + ContainerPortType[port.Type.toUpperCase() as keyof typeof ContainerPortType] || + ContainerPortType.TCP, + })), + sizeRootFs: undefined, + labels: container.Labels ?? {}, + state: + typeof container.State === 'string' + ? (ContainerState[container.State.toUpperCase() as keyof typeof ContainerState] ?? + ContainerState.EXITED) + : ContainerState.EXITED, + status: container.Status, + hostConfig: { + networkMode: container.HostConfig?.NetworkMode || '', }, - { deep: true } - ); + networkSettings: container.NetworkSettings, + mounts: container.Mounts, + autoStart: this.autoStarts.includes(container.Names[0].split('/')[1]), + }; + + return transformed; } public async getContainers( @@ -147,11 +156,27 @@ export class DockerService implements OnModuleInit { return this.client .listNetworks() .catch(catchHandlers.docker) - .then( - (networks = []) => - networks.map((object) => - camelCaseKeys(object as unknown as Record, { deep: true }) - ) as DockerNetwork[] + .then((networks = []) => + networks.map( + (network) => + ({ + name: network.Name || '', + id: network.Id || '', + created: network.Created || '', + scope: network.Scope || '', + driver: network.Driver || '', + enableIPv6: network.EnableIPv6 || false, + ipam: network.IPAM || {}, + internal: network.Internal || false, + attachable: network.Attachable || false, + ingress: network.Ingress || false, + configFrom: network.ConfigFrom || {}, + configOnly: network.ConfigOnly || false, + containers: network.Containers || {}, + options: network.Options || {}, + labels: network.Labels || {}, + }) as DockerNetwork + ) ); } diff --git a/api/src/unraid-api/graph/resolvers/flash/flash.model.ts b/api/src/unraid-api/graph/resolvers/flash/flash.model.ts new file mode 100644 index 000000000..eb3fae2c5 --- /dev/null +++ b/api/src/unraid-api/graph/resolvers/flash/flash.model.ts @@ -0,0 +1,20 @@ +import { Field, ID, ObjectType } from '@nestjs/graphql'; + +import { Node } from '@app/unraid-api/graph/resolvers/base.model.js'; + +@ObjectType({ + implements: () => Node, +}) +export class Flash implements Node { + @Field(() => ID) + id!: string; + + @Field(() => String) + guid!: string; + + @Field(() => String) + vendor!: string; + + @Field(() => String) + product!: string; +} diff --git a/api/src/unraid-api/graph/resolvers/flash/flash.resolver.ts b/api/src/unraid-api/graph/resolvers/flash/flash.resolver.ts index 65c3a48d9..c266524e3 100644 --- a/api/src/unraid-api/graph/resolvers/flash/flash.resolver.ts +++ b/api/src/unraid-api/graph/resolvers/flash/flash.resolver.ts @@ -2,12 +2,13 @@ import { Query, Resolver } from '@nestjs/graphql'; import { AuthActionVerb, AuthPossession, UsePermissions } from 'nest-authz'; -import { Resource } from '@app/graphql/generated/api/types.js'; import { getters } from '@app/store/index.js'; +import { Resource } from '@app/unraid-api/graph/resolvers/base.model.js'; +import { Flash } from '@app/unraid-api/graph/resolvers/flash/flash.model.js'; -@Resolver('Flash') +@Resolver(() => Flash) export class FlashResolver { - @Query() + @Query(() => Flash) @UsePermissions({ action: AuthActionVerb.READ, resource: Resource.FLASH, @@ -17,6 +18,7 @@ export class FlashResolver { const emhttp = getters.emhttp(); return { + id: 'flash', guid: emhttp.var.flashGuid, vendor: emhttp.var.flashVendor, product: emhttp.var.flashProduct, diff --git a/api/src/unraid-api/graph/resolvers/info/info.model.ts b/api/src/unraid-api/graph/resolvers/info/info.model.ts new file mode 100644 index 000000000..397c6f28f --- /dev/null +++ b/api/src/unraid-api/graph/resolvers/info/info.model.ts @@ -0,0 +1,615 @@ +import { + Field, + Float, + GraphQLISODateTime, + ID, + Int, + ObjectType, + registerEnumType, +} from '@nestjs/graphql'; + +import { GraphQLJSON } from 'graphql-scalars'; + +import { Node } from '@app/unraid-api/graph/resolvers/base.model.js'; + +export enum Temperature { + C = 'C', + F = 'F', +} + +export enum Theme { + white = 'white', +} + +registerEnumType(Temperature, { + name: 'Temperature', + description: 'Temperature unit (Celsius or Fahrenheit)', +}); + +registerEnumType(Theme, { + name: 'Theme', + description: 'Display theme', +}); + +@ObjectType({ + implements: () => Node, +}) +export class InfoApps implements Node { + @Field(() => ID) + id!: string; + + @Field(() => Int, { description: 'How many docker containers are installed' }) + installed!: number; + + @Field(() => Int, { description: 'How many docker containers are running' }) + started!: number; +} + +@ObjectType({ + implements: () => Node, +}) +export class Baseboard implements Node { + @Field(() => ID) + id!: string; + + @Field(() => String) + manufacturer!: string; + + @Field(() => String, { nullable: true }) + model?: string; + + @Field(() => String, { nullable: true }) + version?: string; + + @Field(() => String, { nullable: true }) + serial?: string; + + @Field(() => String, { nullable: true }) + assetTag?: string; +} + +@ObjectType({ + implements: () => Node, +}) +export class InfoCpu implements Node { + @Field(() => ID) + id!: string; + + @Field(() => String) + manufacturer!: string; + + @Field(() => String) + brand!: string; + + @Field(() => String) + vendor!: string; + + @Field(() => String) + family!: string; + + @Field(() => String) + model!: string; + + @Field(() => Int) + stepping!: number; + + @Field(() => String) + revision!: string; + + @Field(() => String, { nullable: true }) + voltage?: string; + + @Field(() => Float) + speed!: number; + + @Field(() => Float) + speedmin!: number; + + @Field(() => Float) + speedmax!: number; + + @Field(() => Int) + threads!: number; + + @Field(() => Int) + cores!: number; + + @Field(() => Int) + processors!: number; + + @Field(() => String) + socket!: string; + + @Field(() => GraphQLJSON) + cache!: Record; + + @Field(() => [String]) + flags!: string[]; +} + +@ObjectType({ + implements: () => Node, +}) +export class Gpu implements Node { + @Field(() => ID) + id!: string; + + @Field(() => String) + type!: string; + + @Field(() => String) + typeid!: string; + + @Field(() => String) + vendorname!: string; + + @Field(() => String) + productid!: string; + + @Field(() => Boolean) + blacklisted!: boolean; + + @Field(() => String) + class!: string; +} + +@ObjectType({ + implements: () => Node, +}) +export class Network implements Node { + @Field(() => ID) + id!: string; + + @Field(() => String, { nullable: true }) + iface?: string; + + @Field(() => String, { nullable: true }) + ifaceName?: string; + + @Field(() => String, { nullable: true }) + ipv4?: string; + + @Field(() => String, { nullable: true }) + ipv6?: string; + + @Field(() => String, { nullable: true }) + mac?: string; + + @Field(() => String, { nullable: true }) + internal?: string; + + @Field(() => String, { nullable: true }) + operstate?: string; + + @Field(() => String, { nullable: true }) + type?: string; + + @Field(() => String, { nullable: true }) + duplex?: string; + + @Field(() => String, { nullable: true }) + mtu?: string; + + @Field(() => String, { nullable: true }) + speed?: string; + + @Field(() => String, { nullable: true }) + carrierChanges?: string; +} + +@ObjectType({ + implements: () => Node, +}) +export class Pci implements Node { + @Field(() => ID) + id!: string; + + @Field(() => String, { nullable: true }) + type?: string; + + @Field(() => String, { nullable: true }) + typeid?: string; + + @Field(() => String, { nullable: true }) + vendorname?: string; + + @Field(() => String, { nullable: true }) + vendorid?: string; + + @Field(() => String, { nullable: true }) + productname?: string; + + @Field(() => String, { nullable: true }) + productid?: string; + + @Field(() => String, { nullable: true }) + blacklisted?: string; + + @Field(() => String, { nullable: true }) + class?: string; +} + +@ObjectType() +export class Usb { + @Field(() => ID) + id!: string; + + @Field(() => String, { nullable: true }) + name?: string; +} + +@ObjectType({ + implements: () => Node, +}) +export class Devices implements Node { + @Field(() => ID) + id!: string; + + @Field(() => [Gpu]) + gpu!: Gpu[]; + + @Field(() => [Pci]) + pci!: Pci[]; + + @Field(() => [Usb]) + usb!: Usb[]; +} + +@ObjectType({ + implements: () => Node, +}) +export class Case { + @Field(() => String, { nullable: true }) + icon?: string; + + @Field(() => String, { nullable: true }) + url?: string; + + @Field(() => String, { nullable: true }) + error?: string; + + @Field(() => String, { nullable: true }) + base64?: string; +} + +@ObjectType({ + implements: () => Node, +}) +export class Display implements Node { + @Field(() => ID, { nullable: false }) + id!: string; + + @Field(() => Case, { nullable: true }) + case?: Case; + + @Field(() => String, { nullable: true }) + date?: string; + + @Field(() => String, { nullable: true }) + number?: string; + + @Field(() => Boolean, { nullable: true }) + scale?: boolean; + + @Field(() => Boolean, { nullable: true }) + tabs?: boolean; + + @Field(() => String, { nullable: true }) + users?: string; + + @Field(() => Boolean, { nullable: true }) + resize?: boolean; + + @Field(() => Boolean, { nullable: true }) + wwn?: boolean; + + @Field(() => Boolean, { nullable: true }) + total?: boolean; + + @Field(() => Boolean, { nullable: true }) + usage?: boolean; + + @Field(() => String, { nullable: true }) + banner?: string; + + @Field(() => String, { nullable: true }) + dashapps?: string; + + @Field(() => Theme, { nullable: true }) + theme?: Theme; + + @Field(() => Boolean, { nullable: true }) + text?: boolean; + + @Field(() => Temperature, { nullable: true }) + unit?: Temperature; + + @Field(() => Int, { nullable: true }) + warning?: number; + + @Field(() => Int, { nullable: true }) + critical?: number; + + @Field(() => Int, { nullable: true }) + hot?: number; + + @Field(() => Int, { nullable: true }) + max?: number; + + @Field(() => String, { nullable: true }) + locale?: string; +} + +@ObjectType() +export class MemoryLayout { + @Field(() => Int) + size!: number; + + @Field(() => String, { nullable: true }) + bank?: string; + + @Field(() => String, { nullable: true }) + type?: string; + + @Field(() => Int, { nullable: true }) + clockSpeed?: number; + + @Field(() => String, { nullable: true }) + formFactor?: string; + + @Field(() => String, { nullable: true }) + manufacturer?: string; + + @Field(() => String, { nullable: true }) + partNum?: string; + + @Field(() => String, { nullable: true }) + serialNum?: string; + + @Field(() => Int, { nullable: true }) + voltageConfigured?: number; + + @Field(() => Int, { nullable: true }) + voltageMin?: number; + + @Field(() => Int, { nullable: true }) + voltageMax?: number; +} + +@ObjectType({ + implements: () => Node, +}) +export class InfoMemory implements Node { + @Field(() => ID) + id!: string; + + @Field(() => Int) + max!: number; + + @Field(() => Int) + total!: number; + + @Field(() => Int) + free!: number; + + @Field(() => Int) + used!: number; + + @Field(() => Int) + active!: number; + + @Field(() => Int) + available!: number; + + @Field(() => Int) + buffcache!: number; + + @Field(() => Int) + swaptotal!: number; + + @Field(() => Int) + swapused!: number; + + @Field(() => Int) + swapfree!: number; + + @Field(() => [MemoryLayout]) + layout!: MemoryLayout[]; +} + +@ObjectType({ + implements: () => Node, +}) +export class Os implements Node { + @Field(() => ID) + id!: string; + + @Field(() => String, { nullable: true }) + platform?: string; + + @Field(() => String, { nullable: true }) + distro?: string; + + @Field(() => String, { nullable: true }) + release?: string; + + @Field(() => String, { nullable: true }) + codename?: string; + + @Field(() => String, { nullable: true }) + kernel?: string; + + @Field(() => String, { nullable: true }) + arch?: string; + + @Field(() => String, { nullable: true }) + hostname?: string; + + @Field(() => String, { nullable: true }) + codepage?: string; + + @Field(() => String, { nullable: true }) + logofile?: string; + + @Field(() => String, { nullable: true }) + serial?: string; + + @Field(() => String, { nullable: true }) + build?: string; + + @Field(() => String, { nullable: true }) + uptime?: string; +} + +@ObjectType({ + implements: () => Node, +}) +export class System implements Node { + @Field(() => ID) + id!: string; + + @Field(() => String, { nullable: true }) + manufacturer?: string; + + @Field(() => String, { nullable: true }) + model?: string; + + @Field(() => String, { nullable: true }) + version?: string; + + @Field(() => String, { nullable: true }) + serial?: string; + + @Field(() => String, { nullable: true }) + uuid?: string; + + @Field(() => String, { nullable: true }) + sku?: string; +} + +@ObjectType({ + implements: () => Node, +}) +export class Versions implements Node { + @Field(() => ID) + id!: string; + + @Field(() => String, { nullable: true }) + kernel?: string; + + @Field(() => String, { nullable: true }) + openssl?: string; + + @Field(() => String, { nullable: true }) + systemOpenssl?: string; + + @Field(() => String, { nullable: true }) + systemOpensslLib?: string; + + @Field(() => String, { nullable: true }) + node?: string; + + @Field(() => String, { nullable: true }) + v8?: string; + + @Field(() => String, { nullable: true }) + npm?: string; + + @Field(() => String, { nullable: true }) + yarn?: string; + + @Field(() => String, { nullable: true }) + pm2?: string; + + @Field(() => String, { nullable: true }) + gulp?: string; + + @Field(() => String, { nullable: true }) + grunt?: string; + + @Field(() => String, { nullable: true }) + git?: string; + + @Field(() => String, { nullable: true }) + tsc?: string; + + @Field(() => String, { nullable: true }) + mysql?: string; + + @Field(() => String, { nullable: true }) + redis?: string; + + @Field(() => String, { nullable: true }) + mongodb?: string; + + @Field(() => String, { nullable: true }) + apache?: string; + + @Field(() => String, { nullable: true }) + nginx?: string; + + @Field(() => String, { nullable: true }) + php?: string; + + @Field(() => String, { nullable: true }) + docker?: string; + + @Field(() => String, { nullable: true }) + postfix?: string; + + @Field(() => String, { nullable: true }) + postgresql?: string; + + @Field(() => String, { nullable: true }) + perl?: string; + + @Field(() => String, { nullable: true }) + python?: string; + + @Field(() => String, { nullable: true }) + gcc?: string; + + @Field(() => String, { nullable: true }) + unraid?: string; +} + +@ObjectType({ + implements: () => Node, +}) +export class Info implements Node { + @Field(() => ID) + id!: string; + + @Field(() => InfoApps, { description: 'Count of docker containers' }) + apps!: InfoApps; + + @Field(() => Baseboard) + baseboard!: Baseboard; + + @Field(() => InfoCpu) + cpu!: InfoCpu; + + @Field(() => Devices) + devices!: Devices; + + @Field(() => Display) + display!: Display; + + @Field(() => ID, { description: 'Machine ID', nullable: true }) + machineId?: string; + + @Field(() => InfoMemory) + memory!: InfoMemory; + + @Field(() => Os) + os!: Os; + + @Field(() => System) + system!: System; + + @Field(() => GraphQLISODateTime) + time!: Date; + + @Field(() => Versions) + versions!: Versions; +} diff --git a/api/src/unraid-api/graph/resolvers/info/info.resolver.ts b/api/src/unraid-api/graph/resolvers/info/info.resolver.ts index f9dfae6e1..23ae2ec45 100644 --- a/api/src/unraid-api/graph/resolvers/info/info.resolver.ts +++ b/api/src/unraid-api/graph/resolvers/info/info.resolver.ts @@ -1,11 +1,10 @@ import { Query, ResolveField, Resolver, Subscription } from '@nestjs/graphql'; import { AuthActionVerb, AuthPossession, UsePermissions } from 'nest-authz'; -import { baseboard, system } from 'systeminformation'; +import { baseboard as getBaseboard, system as getSystem } from 'systeminformation'; import { createSubscription, PUBSUB_CHANNEL } from '@app/core/pubsub.js'; import { getMachineId } from '@app/core/utils/misc/get-machine-id.js'; -import { Resource } from '@app/graphql/generated/api/types.js'; import { generateApps, generateCpu, @@ -15,76 +14,109 @@ import { generateOs, generateVersions, } from '@app/graphql/resolvers/query/info.js'; +import { Resource } from '@app/unraid-api/graph/resolvers/base.model.js'; +import { + Baseboard, + Devices, + Display, + Info, + InfoApps, + InfoCpu, + InfoMemory, + Os, + System, + Versions, +} from '@app/unraid-api/graph/resolvers/info/info.model.js'; -@Resolver('Info') +@Resolver(() => Info) export class InfoResolver { - @Query() + @Query(() => Info) @UsePermissions({ action: AuthActionVerb.READ, resource: Resource.INFO, possession: AuthPossession.ANY, }) - public async info() { + public async info(): Promise { return { id: 'info', + time: new Date(), + apps: await this.apps(), + baseboard: await this.baseboard(), + cpu: await this.cpu(), + devices: await this.devices(), + display: await this.display(), + machineId: await this.machineId(), + memory: await this.memory(), + os: await this.os(), + system: await this.system(), + versions: await this.versions(), }; } - @ResolveField('time') - public async now() { - return new Date().toISOString(); + @ResolveField(() => Date) + public async time(): Promise { + return new Date(); } - @ResolveField('apps') - public async apps() { + @ResolveField(() => InfoApps) + public async apps(): Promise { return generateApps(); } - @ResolveField('baseboard') - public async baseboard() { - return baseboard(); + @ResolveField(() => Baseboard) + public async baseboard(): Promise { + const baseboard = await getBaseboard(); + return { + id: 'baseboard', + ...baseboard, + }; } - @ResolveField('cpu') - public async cpu() { + @ResolveField(() => InfoCpu) + public async cpu(): Promise { return generateCpu(); } - @ResolveField('devices') - public async devices() { + @ResolveField(() => Devices) + public async devices(): Promise { return generateDevices(); } - @ResolveField('display') - public async display() { + @ResolveField(() => Display) + public async display(): Promise { return generateDisplay(); } - @ResolveField('machineId') - public async machineId() { + @ResolveField(() => String, { nullable: true }) + public async machineId(): Promise { return getMachineId(); } - @ResolveField('memory') - public async memory() { + @ResolveField(() => InfoMemory) + public async memory(): Promise { return generateMemory(); } - @ResolveField('os') - public async os() { + @ResolveField(() => Os) + public async os(): Promise { return generateOs(); } - @ResolveField('system') - public async system() { - return system(); + @ResolveField(() => System) + public async system(): Promise { + const system = await getSystem(); + return { + id: 'system', + ...system, + }; } - @ResolveField('versions') - public async versions() { + + @ResolveField(() => Versions) + public async versions(): Promise { return generateVersions(); } - @Subscription('info') + @Subscription(() => Info) @UsePermissions({ action: AuthActionVerb.READ, resource: Resource.INFO, diff --git a/api/src/unraid-api/graph/resolvers/logs/logs.model.ts b/api/src/unraid-api/graph/resolvers/logs/logs.model.ts new file mode 100644 index 000000000..410e33d5c --- /dev/null +++ b/api/src/unraid-api/graph/resolvers/logs/logs.model.ts @@ -0,0 +1,46 @@ +import { Field, GraphQLISODateTime, InputType, Int, ObjectType } from '@nestjs/graphql'; + +@ObjectType() +export class LogFile { + @Field(() => String, { description: 'Name of the log file' }) + name!: string; + + @Field(() => String, { description: 'Full path to the log file' }) + path!: string; + + @Field(() => Int, { description: 'Size of the log file in bytes' }) + size!: number; + + @Field(() => GraphQLISODateTime, { description: 'Last modified timestamp' }) + modifiedAt!: Date; +} + +@ObjectType() +export class LogFileContent { + @Field(() => String, { description: 'Path to the log file' }) + path!: string; + + @Field(() => String, { description: 'Content of the log file' }) + content!: string; + + @Field(() => Int, { description: 'Total number of lines in the file' }) + totalLines!: number; + + @Field(() => Int, { nullable: true, description: 'Starting line number of the content (1-indexed)' }) + startLine?: number; +} + +@InputType() +export class LogFileInput { + @Field(() => String, { description: 'Path to the log file' }) + path!: string; + + @Field(() => Int, { + nullable: true, + description: 'Number of lines to read from the end of the file (default: 100)', + }) + lines?: number; + + @Field(() => Int, { nullable: true, description: 'Optional starting line number (1-indexed)' }) + startLine?: number; +} diff --git a/api/src/unraid-api/graph/resolvers/logs/logs.resolver.ts b/api/src/unraid-api/graph/resolvers/logs/logs.resolver.ts index d40654cd1..e9f200cc4 100644 --- a/api/src/unraid-api/graph/resolvers/logs/logs.resolver.ts +++ b/api/src/unraid-api/graph/resolvers/logs/logs.resolver.ts @@ -1,26 +1,27 @@ -import { Args, Query, Resolver, Subscription } from '@nestjs/graphql'; +import { Args, Int, Query, Resolver, Subscription } from '@nestjs/graphql'; import { AuthActionVerb, AuthPossession, UsePermissions } from 'nest-authz'; import { createSubscription, PUBSUB_CHANNEL } from '@app/core/pubsub.js'; -import { Resource } from '@app/graphql/generated/api/types.js'; +import { Resource } from '@app/unraid-api/graph/resolvers/base.model.js'; +import { LogFile, LogFileContent } from '@app/unraid-api/graph/resolvers/logs/logs.model.js'; import { LogsService } from '@app/unraid-api/graph/resolvers/logs/logs.service.js'; -@Resolver('Logs') +@Resolver(() => LogFile) export class LogsResolver { constructor(private readonly logsService: LogsService) {} - @Query() + @Query(() => [LogFile]) @UsePermissions({ action: AuthActionVerb.READ, resource: Resource.LOGS, possession: AuthPossession.ANY, }) - async logFiles() { + async logFiles(): Promise { return this.logsService.listLogFiles(); } - @Query() + @Query(() => LogFileContent) @UsePermissions({ action: AuthActionVerb.READ, resource: Resource.LOGS, @@ -28,13 +29,13 @@ export class LogsResolver { }) async logFile( @Args('path') path: string, - @Args('lines') lines?: number, - @Args('startLine') startLine?: number - ) { + @Args('lines', { nullable: true, type: () => Int }) lines?: number, + @Args('startLine', { nullable: true, type: () => Int }) startLine?: number + ): Promise { return this.logsService.getLogFileContent(path, lines, startLine); } - @Subscription('logFile') + @Subscription(() => LogFileContent, { name: 'logFile' }) @UsePermissions({ action: AuthActionVerb.READ, resource: Resource.LOGS, diff --git a/api/src/unraid-api/graph/resolvers/mutation/mutation.model.ts b/api/src/unraid-api/graph/resolvers/mutation/mutation.model.ts new file mode 100644 index 000000000..8cd4e1f5c --- /dev/null +++ b/api/src/unraid-api/graph/resolvers/mutation/mutation.model.ts @@ -0,0 +1,30 @@ +import { Field, ObjectType } from '@nestjs/graphql'; + +@ObjectType() +export class ArrayMutations {} + +@ObjectType() +export class DockerMutations {} + +@ObjectType() +export class VmMutations {} + +@ObjectType({ + description: 'Parity check related mutations, WIP, response types and functionaliy will change', +}) +export class ParityCheckMutations {} + +@ObjectType() +export class RootMutations { + @Field(() => ArrayMutations, { description: 'Array related mutations' }) + array: ArrayMutations = new ArrayMutations(); + + @Field(() => DockerMutations, { description: 'Docker related mutations' }) + docker: DockerMutations = new DockerMutations(); + + @Field(() => VmMutations, { description: 'VM related mutations' }) + vm: VmMutations = new VmMutations(); + + @Field(() => ParityCheckMutations, { description: 'Parity check related mutations' }) + parityCheck: ParityCheckMutations = new ParityCheckMutations(); +} diff --git a/api/src/unraid-api/graph/resolvers/mutation/mutation.resolver.ts b/api/src/unraid-api/graph/resolvers/mutation/mutation.resolver.ts index 954fb1847..29758be5c 100644 --- a/api/src/unraid-api/graph/resolvers/mutation/mutation.resolver.ts +++ b/api/src/unraid-api/graph/resolvers/mutation/mutation.resolver.ts @@ -1,18 +1,32 @@ -import { ResolveField, Resolver } from '@nestjs/graphql'; +import { Mutation, Resolver } from '@nestjs/graphql'; -@Resolver('Mutation') -export class MutationResolver { - @ResolveField() - public async array() { - return { - __typename: 'ArrayMutations', - }; +import { + ArrayMutations, + DockerMutations, + ParityCheckMutations, + RootMutations, + VmMutations, +} from '@app/unraid-api/graph/resolvers/mutation/mutation.model.js'; + +@Resolver(() => RootMutations) +export class RootMutationsResolver { + @Mutation(() => ArrayMutations, { name: 'array' }) + array(): ArrayMutations { + return new ArrayMutations(); // You can pass context/state here if needed } - @ResolveField() - public async docker() { - return { - __typename: 'DockerMutations', - }; + @Mutation(() => DockerMutations, { name: 'docker' }) + docker(): DockerMutations { + return new DockerMutations(); // You can pass context/state here if needed + } + + @Mutation(() => VmMutations, { name: 'vm' }) + vm(): VmMutations { + return new VmMutations(); // You can pass context/state here if needed + } + + @Mutation(() => ParityCheckMutations, { name: 'parityCheck' }) + parityCheck(): ParityCheckMutations { + return new ParityCheckMutations(); // You can pass context/state here if needed } } diff --git a/api/src/unraid-api/graph/network/network.resolver.spec.ts b/api/src/unraid-api/graph/resolvers/network/network.resolver.spec.ts similarity index 85% rename from api/src/unraid-api/graph/network/network.resolver.spec.ts rename to api/src/unraid-api/graph/resolvers/network/network.resolver.spec.ts index fb52f0df3..fe8aee9bd 100644 --- a/api/src/unraid-api/graph/network/network.resolver.spec.ts +++ b/api/src/unraid-api/graph/resolvers/network/network.resolver.spec.ts @@ -3,7 +3,7 @@ import { Test } from '@nestjs/testing'; import { beforeEach, describe, expect, it } from 'vitest'; -import { NetworkResolver } from '@app/unraid-api/graph/network/network.resolver.js'; +import { NetworkResolver } from '@app/unraid-api/graph/resolvers/network/network.resolver.js'; describe('NetworkResolver', () => { let resolver: NetworkResolver; diff --git a/api/src/unraid-api/graph/network/network.resolver.ts b/api/src/unraid-api/graph/resolvers/network/network.resolver.ts similarity index 59% rename from api/src/unraid-api/graph/network/network.resolver.ts rename to api/src/unraid-api/graph/resolvers/network/network.resolver.ts index 7252bc071..32593b2fd 100644 --- a/api/src/unraid-api/graph/network/network.resolver.ts +++ b/api/src/unraid-api/graph/resolvers/network/network.resolver.ts @@ -2,10 +2,11 @@ import { Query, ResolveField, Resolver } from '@nestjs/graphql'; import { AuthActionVerb, AuthPossession, UsePermissions } from 'nest-authz'; -import { AccessUrl, Network, Resource } from '@app/graphql/generated/api/types.js'; import { getServerIps } from '@app/graphql/resolvers/subscription/network.js'; +import { Resource } from '@app/unraid-api/graph/resolvers/base.model.js'; +import { AccessUrl, Network } from '@app/unraid-api/graph/resolvers/connect/connect.model.js'; -@Resolver('Network') +@Resolver(() => Network) export class NetworkResolver { constructor() {} @@ -14,16 +15,21 @@ export class NetworkResolver { resource: Resource.NETWORK, possession: AuthPossession.ANY, }) - @Query('network') + @Query(() => Network) public async network(): Promise { return { id: 'network', }; } - @ResolveField() + @ResolveField(() => [AccessUrl]) public async accessUrls(): Promise { const ips = await getServerIps(); - return ips.urls; + return ips.urls.map((url) => ({ + type: url.type, + name: url.name, + ipv4: url.ipv4, + ipv6: url.ipv6, + })); } } diff --git a/api/src/unraid-api/graph/resolvers/notifications/notifications.model.ts b/api/src/unraid-api/graph/resolvers/notifications/notifications.model.ts new file mode 100644 index 000000000..6bdb28b8a --- /dev/null +++ b/api/src/unraid-api/graph/resolvers/notifications/notifications.model.ts @@ -0,0 +1,176 @@ +import { Field, ID, InputType, Int, ObjectType, registerEnumType } from '@nestjs/graphql'; + +import { IsEnum, IsInt, IsNotEmpty, IsOptional, IsString, Min } from 'class-validator'; + +export enum NotificationType { + UNREAD = 'UNREAD', + ARCHIVE = 'ARCHIVE', +} + +export enum NotificationImportance { + ALERT = 'ALERT', + INFO = 'INFO', + WARNING = 'WARNING', +} + +// Register enums with GraphQL +registerEnumType(NotificationType, { + name: 'NotificationType', +}); + +registerEnumType(NotificationImportance, { + name: 'NotificationImportance', +}); + +@InputType('NotificationFilter') +export class NotificationFilter { + @Field(() => NotificationImportance, { nullable: true }) + @IsEnum(NotificationImportance) + @IsOptional() + importance?: NotificationImportance; + + @Field(() => NotificationType) + @IsEnum(NotificationType) + @IsNotEmpty() + type!: NotificationType; + + @Field(() => Int) + @IsInt() + @Min(0) + @IsNotEmpty() + offset!: number; + + @Field(() => Int) + @IsInt() + @Min(1) + @IsNotEmpty() + limit!: number; +} + +@InputType('NotificationData') +export class NotificationData { + @Field() + @IsString() + @IsNotEmpty() + title!: string; + + @Field() + @IsString() + @IsNotEmpty() + subject!: string; + + @Field() + @IsString() + @IsNotEmpty() + description!: string; + + @Field(() => NotificationImportance) + @IsEnum(NotificationImportance) + @IsNotEmpty() + importance!: NotificationImportance; + + @Field({ nullable: true }) + @IsString() + @IsOptional() + link?: string; +} + +@ObjectType('NotificationCounts') +export class NotificationCounts { + @Field(() => Int) + @IsInt() + @Min(0) + info!: number; + + @Field(() => Int) + @IsInt() + @Min(0) + warning!: number; + + @Field(() => Int) + @IsInt() + @Min(0) + alert!: number; + + @Field(() => Int) + @IsInt() + @Min(0) + total!: number; +} + +@ObjectType('NotificationOverview') +export class NotificationOverview { + @Field(() => NotificationCounts) + @IsNotEmpty() + unread!: NotificationCounts; + + @Field(() => NotificationCounts) + @IsNotEmpty() + archive!: NotificationCounts; +} + +@ObjectType('Notification') +export class Notification { + @Field(() => ID) + @IsString() + @IsNotEmpty() + id!: string; + + @Field({ description: "Also known as 'event'" }) + @IsString() + @IsNotEmpty() + title!: string; + + @Field() + @IsString() + @IsNotEmpty() + subject!: string; + + @Field() + @IsString() + @IsNotEmpty() + description!: string; + + @Field(() => NotificationImportance) + @IsEnum(NotificationImportance) + @IsNotEmpty() + importance!: NotificationImportance; + + @Field({ nullable: true }) + @IsString() + @IsOptional() + link?: string; + + @Field(() => NotificationType) + @IsEnum(NotificationType) + @IsNotEmpty() + type!: NotificationType; + + @Field({ nullable: true, description: 'ISO Timestamp for when the notification occurred' }) + @IsString() + @IsOptional() + timestamp?: string; + + @Field({ nullable: true }) + @IsString() + @IsOptional() + formattedTimestamp?: string; +} + +@ObjectType('Notifications') +export class Notifications { + @Field(() => ID) + @IsString() + @IsNotEmpty() + id!: string; + + @Field(() => NotificationOverview, { + description: 'A cached overview of the notifications in the system & their severity.', + }) + @IsNotEmpty() + overview!: NotificationOverview; + + @Field(() => [Notification]) + @IsNotEmpty() + list!: Notification[]; +} diff --git a/api/src/unraid-api/graph/resolvers/notifications/notifications.resolver.ts b/api/src/unraid-api/graph/resolvers/notifications/notifications.resolver.ts index 516c76215..d76ff3536 100644 --- a/api/src/unraid-api/graph/resolvers/notifications/notifications.resolver.ts +++ b/api/src/unraid-api/graph/resolvers/notifications/notifications.resolver.ts @@ -2,18 +2,21 @@ import { Args, Mutation, Query, ResolveField, Resolver, Subscription } from '@ne import { AuthActionVerb, AuthPossession, UsePermissions } from 'nest-authz'; -import type { - NotificationData, - NotificationFilter, - NotificationOverview, -} from '@app/graphql/generated/api/types.js'; import { AppError } from '@app/core/errors/app-error.js'; import { createSubscription, PUBSUB_CHANNEL } from '@app/core/pubsub.js'; -import { NotificationType, Resource } from '@app/graphql/generated/api/types.js'; -import { Importance } from '@app/graphql/generated/client/graphql.js'; +import { Resource } from '@app/unraid-api/graph/resolvers/base.model.js'; +import { + Notification, + NotificationData, + NotificationFilter, + NotificationImportance, + NotificationOverview, + Notifications, + NotificationType, +} from '@app/unraid-api/graph/resolvers/notifications/notifications.model.js'; import { NotificationsService } from '@app/unraid-api/graph/resolvers/notifications/notifications.service.js'; -@Resolver('Notifications') +@Resolver(() => Notifications) export class NotificationsResolver { constructor(readonly notificationsService: NotificationsService) {} @@ -21,28 +24,28 @@ export class NotificationsResolver { * Queries *=============================================**/ - @Query() + @Query(() => Notifications, { description: 'Get all notifications' }) @UsePermissions({ action: AuthActionVerb.READ, resource: Resource.NOTIFICATIONS, possession: AuthPossession.ANY, }) - public async notifications() { + public async notifications(): Promise { return { id: 'notifications', - }; + } as Notifications; } - @ResolveField() - public async overview() { + @ResolveField(() => NotificationOverview) + public async overview(): Promise { return this.notificationsService.getOverview(); } - @ResolveField() + @ResolveField(() => [Notification]) public async list( - @Args('filter') + @Args('filter', { type: () => NotificationFilter }) filters: NotificationFilter - ) { + ): Promise { return await this.notificationsService.getNotifications(filters); } @@ -50,69 +53,88 @@ export class NotificationsResolver { * Mutations *=============================================**/ - /** Creates a new notification record */ - @Mutation() + @Mutation(() => Notification, { description: 'Creates a new notification record' }) public createNotification( - @Args('input') + @Args('input', { type: () => NotificationData }) data: NotificationData - ) { + ): Promise { return this.notificationsService.createNotification(data); } - @Mutation() + @Mutation(() => NotificationOverview) public async deleteNotification( - @Args('id') + @Args('id', { type: () => String }) id: string, - @Args('type') + @Args('type', { type: () => NotificationType }) type: NotificationType - ) { + ): Promise { const { overview } = await this.notificationsService.deleteNotification({ id, type }); return overview; } - @Mutation() + @Mutation(() => NotificationOverview, { + description: 'Deletes all archived notifications on server.', + }) public async deleteArchivedNotifications(): Promise { return this.notificationsService.deleteNotifications(NotificationType.ARCHIVE); } - @Mutation() - public archiveNotification(@Args('id') id: string) { + @Mutation(() => Notification, { description: 'Marks a notification as archived.' }) + public archiveNotification( + @Args('id', { type: () => String }) + id: string + ): Promise { return this.notificationsService.archiveNotification({ id }); } - @Mutation() - public async archiveNotifications(@Args('ids') ids: string[]) { + @Mutation(() => NotificationOverview) + public async archiveNotifications( + @Args('ids', { type: () => [String] }) + ids: string[] + ): Promise { await this.notificationsService.archiveIds(ids); return this.notificationsService.getOverview(); } - @Mutation() - public async archiveAll(@Args('importance') importance?: Importance): Promise { + @Mutation(() => NotificationOverview) + public async archiveAll( + @Args('importance', { type: () => NotificationImportance, nullable: true }) + importance?: NotificationImportance + ): Promise { const { overview } = await this.notificationsService.archiveAll(importance); return overview; } - @Mutation() - public unreadNotification(@Args('id') id: string) { + @Mutation(() => Notification, { description: 'Marks a notification as unread.' }) + public unreadNotification( + @Args('id', { type: () => String }) + id: string + ): Promise { return this.notificationsService.markAsUnread({ id }); } - @Mutation() - public async unarchiveNotifications(@Args('ids') ids: string[]) { + @Mutation(() => NotificationOverview) + public async unarchiveNotifications( + @Args('ids', { type: () => [String] }) + ids: string[] + ): Promise { await this.notificationsService.unarchiveIds(ids); return this.notificationsService.getOverview(); } - @Mutation() + @Mutation(() => NotificationOverview) public async unarchiveAll( - @Args('importance') importance?: Importance + @Args('importance', { type: () => NotificationImportance, nullable: true }) + importance?: NotificationImportance ): Promise { const { overview } = await this.notificationsService.unarchiveAll(importance); return overview; } - @Mutation() - public async recalculateOverview() { + @Mutation(() => NotificationOverview, { + description: 'Reads each notification to recompute & update the overview.', + }) + public async recalculateOverview(): Promise { const { overview, error } = await this.notificationsService.recalculateOverview(); if (error) { throw new AppError('Failed to refresh overview', 500); @@ -124,7 +146,7 @@ export class NotificationsResolver { * Subscriptions *=============================================**/ - @Subscription() + @Subscription(() => Notification) @UsePermissions({ action: AuthActionVerb.READ, resource: Resource.NOTIFICATIONS, @@ -134,7 +156,7 @@ export class NotificationsResolver { return createSubscription(PUBSUB_CHANNEL.NOTIFICATION_ADDED); } - @Subscription() + @Subscription(() => NotificationOverview) @UsePermissions({ action: AuthActionVerb.READ, resource: Resource.NOTIFICATIONS, diff --git a/api/src/unraid-api/graph/resolvers/notifications/notifications.service.spec.ts b/api/src/unraid-api/graph/resolvers/notifications/notifications.service.spec.ts index 6263ea924..d147904ea 100644 --- a/api/src/unraid-api/graph/resolvers/notifications/notifications.service.spec.ts +++ b/api/src/unraid-api/graph/resolvers/notifications/notifications.service.spec.ts @@ -6,17 +6,18 @@ import { mkdir } from 'fs/promises'; import { execa } from 'execa'; import { afterEach, beforeAll, describe, expect, it, vi } from 'vitest'; -import type { +import { NotificationIni } from '@app/core/types/states/notification.js'; +import { Notification, NotificationCounts, NotificationData, NotificationFilter, + NotificationImportance, NotificationOverview, -} from '@app/graphql/generated/api/types.js'; -import { type NotificationIni } from '@app/core/types/states/notification.js'; -import { NotificationSchema } from '@app/graphql/generated/api/operations.js'; -import { Importance, NotificationType } from '@app/graphql/generated/api/types.js'; + NotificationType, +} from '@app/unraid-api/graph/resolvers/notifications/notifications.model.js'; import { NotificationsService } from '@app/unraid-api/graph/resolvers/notifications/notifications.service.js'; +import { validateObject } from '@app/unraid-api/graph/resolvers/validation.utils.js'; // defined outside `describe` so it's defined inside the `beforeAll` // needed to mock the dynamix import @@ -25,7 +26,7 @@ const basePath = '/tmp/test/notifications'; // we run sequentially here because this module's state depends on external, shared systems // rn, it's complicated to make the tests atomic & isolated describe.sequential('NotificationsService', () => { - const notificationImportance = Object.values(Importance); + const notificationImportance = Object.values(NotificationImportance); let service: NotificationsService; const testPaths = { basePath, @@ -77,7 +78,7 @@ describe.sequential('NotificationsService', () => { title = 'Test Notification', subject = 'Test Subject', description = 'Test Description', - importance = Importance.INFO, + importance = NotificationImportance.INFO, } = data; return service.createNotification({ title, subject, description, importance }); } @@ -103,7 +104,7 @@ describe.sequential('NotificationsService', () => { }; } - async function forEachImportance(action: (importance: Importance) => Promise) { + async function forEachImportance(action: (importance: NotificationImportance) => Promise) { for (const importance of notificationImportance) { await action(importance); } @@ -118,7 +119,7 @@ describe.sequential('NotificationsService', () => { // currently unused b/c of difficulty implementing NotificationOverview tests async function forAllTypesAndImportances( - action: (type: NotificationType, importance: Importance) => Promise + action: (type: NotificationType, importance: NotificationImportance) => Promise ) { await forEachType(async (type) => { await forEachImportance(async (importance) => { @@ -200,6 +201,7 @@ describe.sequential('NotificationsService', () => { const isISODate = (date: string) => new Date(date).toISOString() === date; const created = await createNotification(); const loaded = await findById(created.id); + expect(isISODate(created.timestamp ?? '')).toBeTruthy(); expect(isISODate(loaded?.timestamp ?? '')).toBeTruthy(); }); @@ -207,8 +209,8 @@ describe.sequential('NotificationsService', () => { it('generates gql-compatible notifications', async () => { const created = await createNotification(); const loaded = await findById(created.id); - const { success } = NotificationSchema().safeParse(loaded); - expect(success).toBeTruthy(); + const validated = await validateObject(Notification, loaded); + expect(validated).toEqual(loaded); }); /**======================================================================== @@ -220,7 +222,7 @@ describe.sequential('NotificationsService', () => { title: 'Test Notification', subject: 'Test Subject', description: 'Test Description', - importance: Importance.INFO, + importance: NotificationImportance.INFO, }; const notification = await createNotification(notificationData); @@ -255,15 +257,15 @@ describe.sequential('NotificationsService', () => { it.each(notificationImportance)('loadNotifications respects %s filter', async (importance) => { const notifications = await Promise.all([ - createNotification({ importance: Importance.ALERT }), - createNotification({ importance: Importance.ALERT }), - createNotification({ importance: Importance.ALERT }), - createNotification({ importance: Importance.INFO }), - createNotification({ importance: Importance.INFO }), - createNotification({ importance: Importance.INFO }), - createNotification({ importance: Importance.WARNING }), - createNotification({ importance: Importance.WARNING }), - createNotification({ importance: Importance.WARNING }), + createNotification({ importance: NotificationImportance.ALERT }), + createNotification({ importance: NotificationImportance.ALERT }), + createNotification({ importance: NotificationImportance.ALERT }), + createNotification({ importance: NotificationImportance.INFO }), + createNotification({ importance: NotificationImportance.INFO }), + createNotification({ importance: NotificationImportance.INFO }), + createNotification({ importance: NotificationImportance.WARNING }), + createNotification({ importance: NotificationImportance.WARNING }), + createNotification({ importance: NotificationImportance.WARNING }), ]); const { overview } = await service.recalculateOverview(); expect(notifications.length).toEqual(9); @@ -310,15 +312,15 @@ describe.sequential('NotificationsService', () => { it.each(notificationImportance)('can archiveAll & unarchiveAll %s', async (importance) => { const expectIn = makeExpectIn(expect); const notifications = await Promise.all([ - createNotification({ importance: Importance.ALERT }), - createNotification({ importance: Importance.ALERT }), - createNotification({ importance: Importance.ALERT }), - createNotification({ importance: Importance.INFO }), - createNotification({ importance: Importance.INFO }), - createNotification({ importance: Importance.INFO }), - createNotification({ importance: Importance.WARNING }), - createNotification({ importance: Importance.WARNING }), - createNotification({ importance: Importance.WARNING }), + createNotification({ importance: NotificationImportance.ALERT }), + createNotification({ importance: NotificationImportance.ALERT }), + createNotification({ importance: NotificationImportance.ALERT }), + createNotification({ importance: NotificationImportance.INFO }), + createNotification({ importance: NotificationImportance.INFO }), + createNotification({ importance: NotificationImportance.INFO }), + createNotification({ importance: NotificationImportance.WARNING }), + createNotification({ importance: NotificationImportance.WARNING }), + createNotification({ importance: NotificationImportance.WARNING }), ]); expect(notifications.length).toEqual(9); @@ -353,7 +355,10 @@ describe.sequential('NotificationsService', () => { // isn't just ignoring the filter, which would be possible if it only // contained the stuff it was supposed to unarchive. - const anotherImportance = importance === Importance.ALERT ? Importance.INFO : Importance.ALERT; + const anotherImportance = + importance === NotificationImportance.ALERT + ? NotificationImportance.INFO + : NotificationImportance.ALERT; await service.archiveAll(anotherImportance); await expectIn({ type: NotificationType.ARCHIVE }, 6); await expectIn({ type: NotificationType.UNREAD }, 3); diff --git a/api/src/unraid-api/graph/resolvers/notifications/notifications.service.ts b/api/src/unraid-api/graph/resolvers/notifications/notifications.service.ts index 7064b845a..6062e217b 100644 --- a/api/src/unraid-api/graph/resolvers/notifications/notifications.service.ts +++ b/api/src/unraid-api/graph/resolvers/notifications/notifications.service.ts @@ -10,22 +10,23 @@ import { emptyDir } from 'fs-extra'; import { encode as encodeIni } from 'ini'; import { v7 as uuidv7 } from 'uuid'; -import type { - Notification, - NotificationCounts, - NotificationData, - NotificationFilter, - NotificationOverview, -} from '@app/graphql/generated/api/types.js'; import { AppError } from '@app/core/errors/app-error.js'; import { pubsub, PUBSUB_CHANNEL } from '@app/core/pubsub.js'; import { NotificationIni } from '@app/core/types/states/notification.js'; import { fileExists } from '@app/core/utils/files/file-exists.js'; import { parseConfig } from '@app/core/utils/misc/parse-config.js'; import { CHOKIDAR_USEPOLLING } from '@app/environment.js'; -import { NotificationSchema } from '@app/graphql/generated/api/operations.js'; -import { Importance, NotificationType } from '@app/graphql/generated/api/types.js'; import { getters } from '@app/store/index.js'; +import { + Notification, + NotificationCounts, + NotificationData, + NotificationFilter, + NotificationImportance, + NotificationOverview, + NotificationType, +} from '@app/unraid-api/graph/resolvers/notifications/notifications.model.js'; +import { validateObject } from '@app/unraid-api/graph/resolvers/validation.utils.js'; import { SortFn } from '@app/unraid-api/types/util.js'; import { batchProcess, formatDatetime, isFulfilled, isRejected, unraidTimestamp } from '@app/utils.js'; @@ -140,12 +141,12 @@ export class NotificationsService { }); } - private increment(importance: Importance, collector: NotificationCounts) { + private increment(importance: NotificationImportance, collector: NotificationCounts) { collector[importance.toLowerCase()] += 1; collector['total'] += 1; } - private decrement(importance: Importance, collector: NotificationCounts) { + private decrement(importance: NotificationImportance, collector: NotificationCounts) { collector[importance.toLowerCase()] -= 1; collector['total'] -= 1; } @@ -462,7 +463,7 @@ export class NotificationsService { }; } - public async archiveAll(importance?: Importance) { + public async archiveAll(importance?: NotificationImportance) { const { UNREAD } = this.paths(); if (!importance) { @@ -483,7 +484,7 @@ export class NotificationsService { return { ...stats, overview: overviewSnapshot }; } - public async unarchiveAll(importance?: Importance) { + public async unarchiveAll(importance?: NotificationImportance) { const { ARCHIVE } = this.paths(); if (!importance) { @@ -655,7 +656,8 @@ export class NotificationsService { // The contents of the file, and therefore the notification, may not always be a valid notification. // so we parse it through the schema to make sure it is - return NotificationSchema().parse(notification); + const validatedNotification = await validateObject(Notification, notification); + return validatedNotification; } private getIdFromPath(path: string) { @@ -691,22 +693,26 @@ export class NotificationsService { }; } - private fileImportanceToGqlImportance(importance: NotificationIni['importance']): Importance { + private fileImportanceToGqlImportance( + importance: NotificationIni['importance'] + ): NotificationImportance { switch (importance) { case 'alert': - return Importance.ALERT; + return NotificationImportance.ALERT; case 'warning': - return Importance.WARNING; + return NotificationImportance.WARNING; default: - return Importance.INFO; + return NotificationImportance.INFO; } } - private gqlImportanceToFileImportance(importance: Importance): NotificationIni['importance'] { + private gqlImportanceToFileImportance( + importance: NotificationImportance + ): NotificationIni['importance'] { switch (importance) { - case Importance.ALERT: + case NotificationImportance.ALERT: return 'alert'; - case Importance.WARNING: + case NotificationImportance.WARNING: return 'warning'; default: return 'normal'; diff --git a/api/src/unraid-api/graph/resolvers/online/online.model.ts b/api/src/unraid-api/graph/resolvers/online/online.model.ts new file mode 100644 index 000000000..34d61bc18 --- /dev/null +++ b/api/src/unraid-api/graph/resolvers/online/online.model.ts @@ -0,0 +1,7 @@ +import { Field, ObjectType } from '@nestjs/graphql'; + +@ObjectType() +export class Online { + @Field(() => Boolean) + online: boolean = true; +} diff --git a/api/src/unraid-api/graph/resolvers/online/online.resolver.ts b/api/src/unraid-api/graph/resolvers/online/online.resolver.ts index 342499230..aa38a3501 100644 --- a/api/src/unraid-api/graph/resolvers/online/online.resolver.ts +++ b/api/src/unraid-api/graph/resolvers/online/online.resolver.ts @@ -1,12 +1,13 @@ import { Query, Resolver } from '@nestjs/graphql'; -import { AuthPossession, UsePermissions } from 'nest-authz'; +import { AuthActionVerb, AuthPossession, UsePermissions } from 'nest-authz'; -import { AuthActionVerb, Resource } from '@app/graphql/generated/api/types.js'; +import { Resource } from '@app/unraid-api/graph/resolvers/base.model.js'; +import { Online } from '@app/unraid-api/graph/resolvers/online/online.model.js'; -@Resolver('Online') +@Resolver(() => Online) export class OnlineResolver { - @Query() + @Query(() => Boolean) @UsePermissions({ action: AuthActionVerb.READ, resource: Resource.ONLINE, diff --git a/api/src/unraid-api/graph/resolvers/owner/owner.model.ts b/api/src/unraid-api/graph/resolvers/owner/owner.model.ts new file mode 100644 index 000000000..0d52c4ee3 --- /dev/null +++ b/api/src/unraid-api/graph/resolvers/owner/owner.model.ts @@ -0,0 +1,16 @@ +import { Field, ObjectType } from '@nestjs/graphql'; + +/** + * @todo Deprecate this type in favor of the UserAccount type + */ +@ObjectType() +export class Owner { + @Field(() => String) + username!: string; + + @Field(() => String) + url!: string; + + @Field(() => String) + avatar!: string; +} diff --git a/api/src/unraid-api/graph/resolvers/owner/owner.resolver.ts b/api/src/unraid-api/graph/resolvers/owner/owner.resolver.ts index f7febfed4..c8ad40def 100644 --- a/api/src/unraid-api/graph/resolvers/owner/owner.resolver.ts +++ b/api/src/unraid-api/graph/resolvers/owner/owner.resolver.ts @@ -3,12 +3,13 @@ import { Query, Resolver, Subscription } from '@nestjs/graphql'; import { AuthActionVerb, AuthPossession, UsePermissions } from 'nest-authz'; import { createSubscription, PUBSUB_CHANNEL } from '@app/core/pubsub.js'; -import { Resource } from '@app/graphql/generated/api/types.js'; import { getters } from '@app/store/index.js'; +import { Resource } from '@app/unraid-api/graph/resolvers/base.model.js'; +import { Owner } from '@app/unraid-api/graph/resolvers/owner/owner.model.js'; -@Resolver('Owner') +@Resolver(() => Owner) export class OwnerResolver { - @Query() + @Query(() => Owner) @UsePermissions({ action: AuthActionVerb.READ, resource: Resource.OWNER, @@ -31,7 +32,7 @@ export class OwnerResolver { }; } - @Subscription('owner') + @Subscription(() => Owner) @UsePermissions({ action: AuthActionVerb.READ, resource: Resource.OWNER, diff --git a/api/src/unraid-api/graph/resolvers/registration/registration.model.ts b/api/src/unraid-api/graph/resolvers/registration/registration.model.ts new file mode 100644 index 000000000..4b9f377cc --- /dev/null +++ b/api/src/unraid-api/graph/resolvers/registration/registration.model.ts @@ -0,0 +1,105 @@ +import { Field, ID, ObjectType, registerEnumType } from '@nestjs/graphql'; + +export enum RegistrationType { + BASIC = 'BASIC', + PLUS = 'PLUS', + PRO = 'PRO', + STARTER = 'STARTER', + UNLEASHED = 'UNLEASHED', + LIFETIME = 'LIFETIME', + INVALID = 'INVALID', + TRIAL = 'TRIAL', +} + +export enum RegistrationState { + /** Trial license */ + TRIAL = 'TRIAL', + /** Basic license */ + BASIC = 'BASIC', + /** Plus license */ + PLUS = 'PLUS', + /** Pro license */ + PRO = 'PRO', + /** Starter license */ + STARTER = 'STARTER', + /** Unleashed license */ + UNLEASHED = 'UNLEASHED', + /** Lifetime license */ + LIFETIME = 'LIFETIME', + /** Trial Expired */ + EEXPIRED = 'EEXPIRED', + /** GUID Error */ + EGUID = 'EGUID', + /** Multiple License Keys Present */ + EGUID1 = 'EGUID1', + /** Invalid installation */ + ETRIAL = 'ETRIAL', + /** No Keyfile */ + ENOKEYFILE = 'ENOKEYFILE', + /** No Keyfile */ + ENOKEYFILE1 = 'ENOKEYFILE1', + /** Missing key file */ + ENOKEYFILE2 = 'ENOKEYFILE2', + /** No Flash */ + ENOFLASH = 'ENOFLASH', + /** No Flash */ + ENOFLASH1 = 'ENOFLASH1', + /** No Flash */ + ENOFLASH2 = 'ENOFLASH2', + /** No Flash */ + ENOFLASH3 = 'ENOFLASH3', + /** No Flash */ + ENOFLASH4 = 'ENOFLASH4', + /** No Flash */ + ENOFLASH5 = 'ENOFLASH5', + /** No Flash */ + ENOFLASH6 = 'ENOFLASH6', + /** No Flash */ + ENOFLASH7 = 'ENOFLASH7', + /** BLACKLISTED */ + EBLACKLISTED = 'EBLACKLISTED', + /** BLACKLISTED */ + EBLACKLISTED1 = 'EBLACKLISTED1', + /** BLACKLISTED */ + EBLACKLISTED2 = 'EBLACKLISTED2', + /** Trial Requires Internet Connection */ + ENOCONN = 'ENOCONN', +} + +registerEnumType(RegistrationType, { + name: 'registrationType', +}); + +registerEnumType(RegistrationState, { + name: 'RegistrationState', +}); + +@ObjectType() +export class KeyFile { + @Field(() => String, { nullable: true }) + location?: string; + + @Field(() => String, { nullable: true }) + contents?: string; +} + +@ObjectType() +export class Registration { + @Field(() => ID, { nullable: true }) + guid?: string; + + @Field(() => RegistrationType, { nullable: true }) + type?: RegistrationType; + + @Field(() => KeyFile, { nullable: true }) + keyFile?: KeyFile; + + @Field(() => RegistrationState, { nullable: true }) + state?: RegistrationState; + + @Field(() => String, { nullable: true }) + expiration?: string; + + @Field(() => String, { nullable: true }) + updateExpiration?: string; +} diff --git a/api/src/unraid-api/graph/resolvers/registration/registration.resolver.ts b/api/src/unraid-api/graph/resolvers/registration/registration.resolver.ts index 5d817bd14..ff761d855 100644 --- a/api/src/unraid-api/graph/resolvers/registration/registration.resolver.ts +++ b/api/src/unraid-api/graph/resolvers/registration/registration.resolver.ts @@ -2,28 +2,31 @@ import { Query, Resolver, Subscription } from '@nestjs/graphql'; import { AuthActionVerb, AuthPossession, UsePermissions } from 'nest-authz'; -import type { Registration } from '@app/graphql/generated/api/types.js'; import { createSubscription, PUBSUB_CHANNEL } from '@app/core/pubsub.js'; import { getKeyFile } from '@app/core/utils/misc/get-key-file.js'; -import { registrationType, Resource } from '@app/graphql/generated/api/types.js'; import { getters } from '@app/store/index.js'; import { FileLoadStatus } from '@app/store/types.js'; +import { Resource } from '@app/unraid-api/graph/resolvers/base.model.js'; +import { + Registration, + RegistrationType, +} from '@app/unraid-api/graph/resolvers/registration/registration.model.js'; -@Resolver('Registration') +@Resolver(() => Registration) export class RegistrationResolver { - @Query() + @Query(() => Registration, { nullable: true }) @UsePermissions({ action: AuthActionVerb.READ, resource: Resource.REGISTRATION, possession: AuthPossession.ANY, }) - public async registration() { + public async registration(): Promise { const emhttp = getters.emhttp(); if (emhttp.status !== FileLoadStatus.LOADED || !emhttp.var?.regTy) { return null; } - const isTrial = emhttp.var.regTy === registrationType.TRIAL; + const isTrial = emhttp.var.regTy === RegistrationType.TRIAL; const isExpired = emhttp.var.regTy.includes('expired'); const registration: Registration = { @@ -32,16 +35,18 @@ export class RegistrationResolver { state: emhttp.var.regState, // Based on https://github.com/unraid/dynamix.unraid.net/blob/c565217fa8b2acf23943dc5c22a12d526cdf70a1/source/dynamix.unraid.net/usr/local/emhttp/plugins/dynamix.my.servers/include/state.php#L64 expiration: (1_000 * (isTrial || isExpired ? Number(emhttp.var.regTm2) : 0)).toString(), - updateExpiration: emhttp.var.regExp ? (Number(emhttp.var.regExp) * 1_000).toString() : null, + updateExpiration: emhttp.var.regExp + ? (Number(emhttp.var.regExp) * 1_000).toString() + : undefined, keyFile: { location: emhttp.var.regFile, - contents: await getKeyFile(), + contents: (await getKeyFile()) ?? undefined, }, }; return registration; } - @Subscription('registration') + @Subscription(() => Registration) @UsePermissions({ action: AuthActionVerb.READ, resource: Resource.REGISTRATION, diff --git a/api/src/unraid-api/graph/resolvers/resolvers.module.ts b/api/src/unraid-api/graph/resolvers/resolvers.module.ts index 48e664960..78f629608 100644 --- a/api/src/unraid-api/graph/resolvers/resolvers.module.ts +++ b/api/src/unraid-api/graph/resolvers/resolvers.module.ts @@ -1,16 +1,14 @@ import { Module } from '@nestjs/common'; import { AuthModule } from '@app/unraid-api/auth/auth.module.js'; -import { ConnectSettingsService } from '@app/unraid-api/graph/connect/connect-settings.service.js'; -import { ConnectResolver } from '@app/unraid-api/graph/connect/connect.resolver.js'; -import { ConnectService } from '@app/unraid-api/graph/connect/connect.service.js'; -import { NetworkResolver } from '@app/unraid-api/graph/network/network.resolver.js'; import { ApiKeyResolver } from '@app/unraid-api/graph/resolvers/api-key/api-key.resolver.js'; +import { ArrayModule } from '@app/unraid-api/graph/resolvers/array/array.module.js'; import { ArrayMutationsResolver } from '@app/unraid-api/graph/resolvers/array/array.mutations.resolver.js'; import { ArrayResolver } from '@app/unraid-api/graph/resolvers/array/array.resolver.js'; import { ArrayService } from '@app/unraid-api/graph/resolvers/array/array.service.js'; import { CloudResolver } from '@app/unraid-api/graph/resolvers/cloud/cloud.resolver.js'; import { ConfigResolver } from '@app/unraid-api/graph/resolvers/config/config.resolver.js'; +import { ConnectModule } from '@app/unraid-api/graph/resolvers/connect/connect.module.js'; import { DisksModule } from '@app/unraid-api/graph/resolvers/disks/disks.module.js'; import { DisplayResolver } from '@app/unraid-api/graph/resolvers/display/display.resolver.js'; import { DockerModule } from '@app/unraid-api/graph/resolvers/docker/docker.module.js'; @@ -18,8 +16,8 @@ import { FlashResolver } from '@app/unraid-api/graph/resolvers/flash/flash.resol import { InfoResolver } from '@app/unraid-api/graph/resolvers/info/info.resolver.js'; import { LogsResolver } from '@app/unraid-api/graph/resolvers/logs/logs.resolver.js'; import { LogsService } from '@app/unraid-api/graph/resolvers/logs/logs.service.js'; -import { MeResolver } from '@app/unraid-api/graph/resolvers/me/me.resolver.js'; -import { MutationResolver } from '@app/unraid-api/graph/resolvers/mutation/mutation.resolver.js'; +import { RootMutationsResolver } from '@app/unraid-api/graph/resolvers/mutation/mutation.resolver.js'; +import { NetworkResolver } from '@app/unraid-api/graph/resolvers/network/network.resolver.js'; import { NotificationsResolver } from '@app/unraid-api/graph/resolvers/notifications/notifications.resolver.js'; import { NotificationsService } from '@app/unraid-api/graph/resolvers/notifications/notifications.service.js'; import { OnlineResolver } from '@app/unraid-api/graph/resolvers/online/online.resolver.js'; @@ -32,32 +30,27 @@ import { VmsResolver } from '@app/unraid-api/graph/resolvers/vms/vms.resolver.js import { VmsService } from '@app/unraid-api/graph/resolvers/vms/vms.service.js'; import { ServicesResolver } from '@app/unraid-api/graph/services/services.resolver.js'; import { SharesResolver } from '@app/unraid-api/graph/shares/shares.resolver.js'; +import { MeResolver } from '@app/unraid-api/graph/user/user.resolver.js'; @Module({ - imports: [AuthModule, DockerModule, DisksModule], + imports: [ArrayModule, AuthModule, ConnectModule, DockerModule, DisksModule], providers: [ ApiKeyResolver, - ArrayMutationsResolver, - ArrayResolver, - ArrayService, CloudResolver, ConfigResolver, - ConnectResolver, - ConnectService, - ConnectSettingsService, DisplayResolver, FlashResolver, InfoResolver, LogsResolver, LogsService, MeResolver, - MutationResolver, NetworkResolver, NotificationsResolver, NotificationsService, OnlineResolver, OwnerResolver, RegistrationResolver, + RootMutationsResolver, ServerResolver, ServicesResolver, SharesResolver, diff --git a/api/src/unraid-api/graph/resolvers/servers/server.model.ts b/api/src/unraid-api/graph/resolvers/servers/server.model.ts new file mode 100644 index 000000000..c0406064f --- /dev/null +++ b/api/src/unraid-api/graph/resolvers/servers/server.model.ts @@ -0,0 +1,56 @@ +import { Field, ID, ObjectType, registerEnumType } from '@nestjs/graphql'; + +@ObjectType() +export class ProfileModel { + @Field(() => ID, { nullable: true }) + userId?: string; + + @Field() + username!: string; + + @Field() + url!: string; + + @Field() + avatar!: string; +} + +export enum ServerStatus { + ONLINE = 'ONLINE', + OFFLINE = 'OFFLINE', + NEVER_CONNECTED = 'NEVER_CONNECTED', +} + +registerEnumType(ServerStatus, { + name: 'ServerStatus', +}); + +@ObjectType() +export class Server { + @Field(() => ProfileModel) + owner!: ProfileModel; + + @Field() + guid!: string; + + @Field() + apikey!: string; + + @Field() + name!: string; + + @Field(() => ServerStatus) + status!: ServerStatus; + + @Field() + wanip!: string; + + @Field() + lanip!: string; + + @Field() + localurl!: string; + + @Field() + remoteurl!: string; +} diff --git a/api/src/unraid-api/graph/resolvers/servers/server.resolver.ts b/api/src/unraid-api/graph/resolvers/servers/server.resolver.ts index fcd35a2c5..c69f40b7c 100644 --- a/api/src/unraid-api/graph/resolvers/servers/server.resolver.ts +++ b/api/src/unraid-api/graph/resolvers/servers/server.resolver.ts @@ -3,34 +3,33 @@ import { Query, Resolver, Subscription } from '@nestjs/graphql'; import { AuthActionVerb, AuthPossession, UsePermissions } from 'nest-authz'; import { createSubscription, PUBSUB_CHANNEL } from '@app/core/pubsub.js'; -import { Resource } from '@app/graphql/generated/api/types.js'; -import { type Server } from '@app/graphql/generated/client/graphql.js'; import { getLocalServer } from '@app/graphql/schema/utils.js'; +import { Resource } from '@app/unraid-api/graph/resolvers/base.model.js'; +import { Server as ServerModel } from '@app/unraid-api/graph/resolvers/servers/server.model.js'; -@Resolver('Server') +@Resolver(() => ServerModel) export class ServerResolver { - @Query() + @Query(() => ServerModel, { nullable: true }) @UsePermissions({ action: AuthActionVerb.READ, resource: Resource.SERVERS, possession: AuthPossession.ANY, }) - public async server(): Promise { + public async server(): Promise { return getLocalServer()[0]; } - @Resolver('servers') - @Query() + @Query(() => [ServerModel]) @UsePermissions({ action: AuthActionVerb.READ, resource: Resource.SERVERS, possession: AuthPossession.ANY, }) - public async servers(): Promise { + public async servers(): Promise { return getLocalServer(); } - @Subscription('server') + @Subscription(() => ServerModel) @UsePermissions({ action: AuthActionVerb.READ, resource: Resource.SERVERS, diff --git a/api/src/unraid-api/graph/resolvers/validation.utils.ts b/api/src/unraid-api/graph/resolvers/validation.utils.ts new file mode 100644 index 000000000..c807ac51c --- /dev/null +++ b/api/src/unraid-api/graph/resolvers/validation.utils.ts @@ -0,0 +1,34 @@ +import 'reflect-metadata'; + +import { plainToClass } from 'class-transformer'; +import { validate, ValidationError } from 'class-validator'; + +/** + * Validates an object against a class using class-validator + * Automatically exposes all fields for transformation + * + * @param type The class to validate against + * @param object The object to validate + * @returns The validated and transformed object + * @throws ValidationError if validation fails + */ +export async function validateObject(type: new () => T, object: unknown): Promise { + const instance = plainToClass(type, object, { + enableImplicitConversion: true, + }); + + const errors = await validate(instance, { + whitelist: true, + validationError: { target: false, value: true }, + }); + + if (errors.length > 0) { + const singleError = new ValidationError(); + singleError.target = instance; + singleError.value = object; + singleError.children = errors; + throw singleError; + } + + return instance; +} diff --git a/api/src/unraid-api/graph/resolvers/vars/vars.model.ts b/api/src/unraid-api/graph/resolvers/vars/vars.model.ts new file mode 100644 index 000000000..c452da639 --- /dev/null +++ b/api/src/unraid-api/graph/resolvers/vars/vars.model.ts @@ -0,0 +1,467 @@ +import { Field, ID, Int, ObjectType, registerEnumType } from '@nestjs/graphql'; + +import { Node } from '@app/unraid-api/graph/resolvers/base.model.js'; +import { + RegistrationState, + RegistrationType, +} from '@app/unraid-api/graph/resolvers/registration/registration.model.js'; + +export enum ConfigErrorState { + UNKNOWN_ERROR = 'UNKNOWN_ERROR', + INELIGIBLE = 'INELIGIBLE', + INVALID = 'INVALID', + NO_KEY_SERVER = 'NO_KEY_SERVER', + WITHDRAWN = 'WITHDRAWN', +} + +registerEnumType(ConfigErrorState, { + name: 'ConfigErrorState', + description: 'Possible error states for configuration', +}); + +export enum MdState { + SWAP_DSBL = 'SWAP_DSBL', + STARTED = 'STARTED', +} + +registerEnumType(MdState, { + name: 'MdState', + description: 'Possible states for MD (Multiple Device)', +}); + +@ObjectType({ + implements: () => Node, +}) +export class Vars implements Node { + @Field(() => ID) + id!: string; + + @Field({ nullable: true, description: 'Unraid version' }) + version?: string; + + @Field(() => Int, { nullable: true }) + maxArraysz?: number; + + @Field(() => Int, { nullable: true }) + maxCachesz?: number; + + @Field({ nullable: true, description: 'Machine hostname' }) + name?: string; + + @Field({ nullable: true }) + timeZone?: string; + + @Field({ nullable: true }) + comment?: string; + + @Field({ nullable: true }) + security?: string; + + @Field({ nullable: true }) + workgroup?: string; + + @Field({ nullable: true }) + domain?: string; + + @Field({ nullable: true }) + domainShort?: string; + + @Field({ nullable: true }) + hideDotFiles?: boolean; + + @Field({ nullable: true }) + localMaster?: boolean; + + @Field({ nullable: true }) + enableFruit?: string; + + @Field({ nullable: true, description: 'Should a NTP server be used for time sync?' }) + useNtp?: boolean; + + @Field({ nullable: true, description: 'NTP Server 1' }) + ntpServer1?: string; + + @Field({ nullable: true, description: 'NTP Server 2' }) + ntpServer2?: string; + + @Field({ nullable: true, description: 'NTP Server 3' }) + ntpServer3?: string; + + @Field({ nullable: true, description: 'NTP Server 4' }) + ntpServer4?: string; + + @Field({ nullable: true }) + domainLogin?: string; + + @Field({ nullable: true }) + sysModel?: string; + + @Field(() => Int, { nullable: true }) + sysArraySlots?: number; + + @Field(() => Int, { nullable: true }) + sysCacheSlots?: number; + + @Field(() => Int, { nullable: true }) + sysFlashSlots?: number; + + @Field({ nullable: true }) + useSsl?: boolean; + + @Field(() => Int, { nullable: true, description: 'Port for the webui via HTTP' }) + port?: number; + + @Field(() => Int, { nullable: true, description: 'Port for the webui via HTTPS' }) + portssl?: number; + + @Field({ nullable: true }) + localTld?: string; + + @Field({ nullable: true }) + bindMgt?: boolean; + + @Field({ nullable: true, description: 'Should telnet be enabled?' }) + useTelnet?: boolean; + + @Field(() => Int, { nullable: true }) + porttelnet?: number; + + @Field({ nullable: true }) + useSsh?: boolean; + + @Field(() => Int, { nullable: true }) + portssh?: number; + + @Field({ nullable: true }) + startPage?: string; + + @Field({ nullable: true }) + startArray?: boolean; + + @Field({ nullable: true }) + spindownDelay?: string; + + @Field({ nullable: true }) + queueDepth?: string; + + @Field({ nullable: true }) + spinupGroups?: boolean; + + @Field({ nullable: true }) + defaultFormat?: string; + + @Field({ nullable: true }) + defaultFsType?: string; + + @Field(() => Int, { nullable: true }) + shutdownTimeout?: number; + + @Field({ nullable: true }) + luksKeyfile?: string; + + @Field({ nullable: true }) + pollAttributes?: string; + + @Field({ nullable: true }) + pollAttributesDefault?: string; + + @Field({ nullable: true }) + pollAttributesStatus?: string; + + @Field(() => Int, { nullable: true }) + nrRequests?: number; + + @Field(() => Int, { nullable: true }) + nrRequestsDefault?: number; + + @Field({ nullable: true }) + nrRequestsStatus?: string; + + @Field(() => Int, { nullable: true }) + mdNumStripes?: number; + + @Field(() => Int, { nullable: true }) + mdNumStripesDefault?: number; + + @Field({ nullable: true }) + mdNumStripesStatus?: string; + + @Field(() => Int, { nullable: true }) + mdSyncWindow?: number; + + @Field(() => Int, { nullable: true }) + mdSyncWindowDefault?: number; + + @Field({ nullable: true }) + mdSyncWindowStatus?: string; + + @Field(() => Int, { nullable: true }) + mdSyncThresh?: number; + + @Field(() => Int, { nullable: true }) + mdSyncThreshDefault?: number; + + @Field({ nullable: true }) + mdSyncThreshStatus?: string; + + @Field(() => Int, { nullable: true }) + mdWriteMethod?: number; + + @Field({ nullable: true }) + mdWriteMethodDefault?: string; + + @Field({ nullable: true }) + mdWriteMethodStatus?: string; + + @Field({ nullable: true }) + shareDisk?: string; + + @Field({ nullable: true }) + shareUser?: string; + + @Field({ nullable: true }) + shareUserInclude?: string; + + @Field({ nullable: true }) + shareUserExclude?: string; + + @Field({ nullable: true }) + shareSmbEnabled?: boolean; + + @Field({ nullable: true }) + shareNfsEnabled?: boolean; + + @Field({ nullable: true }) + shareAfpEnabled?: boolean; + + @Field({ nullable: true }) + shareInitialOwner?: string; + + @Field({ nullable: true }) + shareInitialGroup?: string; + + @Field({ nullable: true }) + shareCacheEnabled?: boolean; + + @Field({ nullable: true }) + shareCacheFloor?: string; + + @Field({ nullable: true }) + shareMoverSchedule?: string; + + @Field({ nullable: true }) + shareMoverLogging?: boolean; + + @Field({ nullable: true }) + fuseRemember?: string; + + @Field({ nullable: true }) + fuseRememberDefault?: string; + + @Field({ nullable: true }) + fuseRememberStatus?: string; + + @Field({ nullable: true }) + fuseDirectio?: string; + + @Field({ nullable: true }) + fuseDirectioDefault?: string; + + @Field({ nullable: true }) + fuseDirectioStatus?: string; + + @Field({ nullable: true }) + shareAvahiEnabled?: boolean; + + @Field({ nullable: true }) + shareAvahiSmbName?: string; + + @Field({ nullable: true }) + shareAvahiSmbModel?: string; + + @Field({ nullable: true }) + shareAvahiAfpName?: string; + + @Field({ nullable: true }) + shareAvahiAfpModel?: string; + + @Field({ nullable: true }) + safeMode?: boolean; + + @Field({ nullable: true }) + startMode?: string; + + @Field({ nullable: true }) + configValid?: boolean; + + @Field(() => ConfigErrorState, { nullable: true }) + configError?: ConfigErrorState; + + @Field({ nullable: true }) + joinStatus?: string; + + @Field(() => Int, { nullable: true }) + deviceCount?: number; + + @Field({ nullable: true }) + flashGuid?: string; + + @Field({ nullable: true }) + flashProduct?: string; + + @Field({ nullable: true }) + flashVendor?: string; + + @Field({ nullable: true }) + regCheck?: string; + + @Field({ nullable: true }) + regFile?: string; + + @Field({ nullable: true }) + regGuid?: string; + + @Field(() => RegistrationType, { nullable: true }) + regTy?: RegistrationType; + + @Field(() => RegistrationState, { nullable: true }) + regState?: RegistrationState; + + @Field({ nullable: true, description: 'Registration owner' }) + regTo?: string; + + @Field({ nullable: true }) + regTm?: string; + + @Field({ nullable: true }) + regTm2?: string; + + @Field({ nullable: true }) + regGen?: string; + + @Field({ nullable: true }) + sbName?: string; + + @Field({ nullable: true }) + sbVersion?: string; + + @Field({ nullable: true }) + sbUpdated?: string; + + @Field(() => Int, { nullable: true }) + sbEvents?: number; + + @Field({ nullable: true }) + sbState?: string; + + @Field({ nullable: true }) + sbClean?: boolean; + + @Field(() => Int, { nullable: true }) + sbSynced?: number; + + @Field(() => Int, { nullable: true }) + sbSyncErrs?: number; + + @Field(() => Int, { nullable: true }) + sbSynced2?: number; + + @Field({ nullable: true }) + sbSyncExit?: string; + + @Field(() => Int, { nullable: true }) + sbNumDisks?: number; + + @Field({ nullable: true }) + mdColor?: string; + + @Field(() => Int, { nullable: true }) + mdNumDisks?: number; + + @Field(() => Int, { nullable: true }) + mdNumDisabled?: number; + + @Field(() => Int, { nullable: true }) + mdNumInvalid?: number; + + @Field(() => Int, { nullable: true }) + mdNumMissing?: number; + + @Field(() => Int, { nullable: true }) + mdNumNew?: number; + + @Field(() => Int, { nullable: true }) + mdNumErased?: number; + + @Field(() => Int, { nullable: true }) + mdResync?: number; + + @Field({ nullable: true }) + mdResyncCorr?: string; + + @Field({ nullable: true }) + mdResyncPos?: string; + + @Field({ nullable: true }) + mdResyncDb?: string; + + @Field({ nullable: true }) + mdResyncDt?: string; + + @Field({ nullable: true }) + mdResyncAction?: string; + + @Field(() => Int, { nullable: true }) + mdResyncSize?: number; + + @Field({ nullable: true }) + mdState?: string; + + @Field({ nullable: true }) + mdVersion?: string; + + @Field(() => Int, { nullable: true }) + cacheNumDevices?: number; + + @Field(() => Int, { nullable: true }) + cacheSbNumDisks?: number; + + @Field({ nullable: true }) + fsState?: string; + + @Field({ nullable: true, description: 'Human friendly string of array events happening' }) + fsProgress?: string; + + @Field(() => Int, { + nullable: true, + description: 'Percentage from 0 - 100 while upgrading a disk or swapping parity drives', + }) + fsCopyPrcnt?: number; + + @Field(() => Int, { nullable: true }) + fsNumMounted?: number; + + @Field(() => Int, { nullable: true }) + fsNumUnmountable?: number; + + @Field({ nullable: true }) + fsUnmountableMask?: string; + + @Field(() => Int, { nullable: true, description: 'Total amount of user shares' }) + shareCount?: number; + + @Field(() => Int, { nullable: true, description: 'Total amount shares with SMB enabled' }) + shareSmbCount?: number; + + @Field(() => Int, { nullable: true, description: 'Total amount shares with NFS enabled' }) + shareNfsCount?: number; + + @Field(() => Int, { nullable: true, description: 'Total amount shares with AFP enabled' }) + shareAfpCount?: number; + + @Field({ nullable: true }) + shareMoverActive?: boolean; + + @Field({ nullable: true }) + csrfToken?: string; +} diff --git a/api/src/unraid-api/graph/resolvers/vars/vars.resolver.ts b/api/src/unraid-api/graph/resolvers/vars/vars.resolver.ts index 13715d32d..e1d328720 100644 --- a/api/src/unraid-api/graph/resolvers/vars/vars.resolver.ts +++ b/api/src/unraid-api/graph/resolvers/vars/vars.resolver.ts @@ -2,12 +2,13 @@ import { Query, Resolver } from '@nestjs/graphql'; import { AuthActionVerb, AuthPossession, UsePermissions } from 'nest-authz'; -import { Resource } from '@app/graphql/generated/api/types.js'; import { getters } from '@app/store/index.js'; +import { Resource } from '@app/unraid-api/graph/resolvers/base.model.js'; +import { Vars } from '@app/unraid-api/graph/resolvers/vars/vars.model.js'; -@Resolver('Vars') +@Resolver(() => Vars) export class VarsResolver { - @Query() + @Query(() => Vars) @UsePermissions({ action: AuthActionVerb.READ, resource: Resource.VARS, diff --git a/api/src/unraid-api/graph/resolvers/vms/vms.model.ts b/api/src/unraid-api/graph/resolvers/vms/vms.model.ts new file mode 100644 index 000000000..526404215 --- /dev/null +++ b/api/src/unraid-api/graph/resolvers/vms/vms.model.ts @@ -0,0 +1,42 @@ +import { Field, ID, InputType, ObjectType, registerEnumType } from '@nestjs/graphql'; + +// Register the VmState enum +export enum VmState { + NOSTATE = 'NOSTATE', + RUNNING = 'RUNNING', + IDLE = 'IDLE', + PAUSED = 'PAUSED', + SHUTDOWN = 'SHUTDOWN', + SHUTOFF = 'SHUTOFF', + CRASHED = 'CRASHED', + PMSUSPENDED = 'PMSUSPENDED', +} + +registerEnumType(VmState, { + name: 'VmState', + description: 'The state of a virtual machine', +}); + +@ObjectType() +export class VmDomain { + @Field(() => ID) + uuid!: string; + + @Field({ nullable: true, description: 'A friendly name for the vm' }) + name?: string; + + @Field(() => VmState, { description: 'Current domain vm state' }) + state!: VmState; +} + +@ObjectType() +export class Vms { + @Field(() => ID) + id!: string; + + @Field(() => [VmDomain], { nullable: true }) + domains?: VmDomain[]; + + @Field(() => [VmDomain], { nullable: true }) + domain?: VmDomain[]; +} diff --git a/api/src/unraid-api/graph/resolvers/vms/vms.mutations.resolver.spec.ts b/api/src/unraid-api/graph/resolvers/vms/vms.mutations.resolver.spec.ts index 1a1476d80..5a0081c6c 100644 --- a/api/src/unraid-api/graph/resolvers/vms/vms.mutations.resolver.spec.ts +++ b/api/src/unraid-api/graph/resolvers/vms/vms.mutations.resolver.spec.ts @@ -37,7 +37,7 @@ describe('VmMutationsResolver', () => { const vmId = 'test-vm-id'; vi.mocked(vmsService.startVm).mockResolvedValue(true); - const result = await resolver.startVm(vmId); + const result = await resolver.start(vmId); expect(result).toBe(true); expect(vmsService.startVm).toHaveBeenCalledWith(vmId); @@ -48,7 +48,7 @@ describe('VmMutationsResolver', () => { const error = new Error('Failed to start VM'); vi.mocked(vmsService.startVm).mockRejectedValue(error); - await expect(resolver.startVm(vmId)).rejects.toThrow('Failed to start VM'); + await expect(resolver.start(vmId)).rejects.toThrow('Failed to start VM'); }); }); @@ -57,7 +57,7 @@ describe('VmMutationsResolver', () => { const vmId = 'test-vm-id'; vi.mocked(vmsService.stopVm).mockResolvedValue(true); - const result = await resolver.stopVm(vmId); + const result = await resolver.stop(vmId); expect(result).toBe(true); expect(vmsService.stopVm).toHaveBeenCalledWith(vmId); @@ -68,7 +68,7 @@ describe('VmMutationsResolver', () => { const error = new Error('Failed to stop VM'); vi.mocked(vmsService.stopVm).mockRejectedValue(error); - await expect(resolver.stopVm(vmId)).rejects.toThrow('Failed to stop VM'); + await expect(resolver.stop(vmId)).rejects.toThrow('Failed to stop VM'); }); }); }); diff --git a/api/src/unraid-api/graph/resolvers/vms/vms.mutations.resolver.ts b/api/src/unraid-api/graph/resolvers/vms/vms.mutations.resolver.ts index 0c479f9d4..ba5a5d7ce 100644 --- a/api/src/unraid-api/graph/resolvers/vms/vms.mutations.resolver.ts +++ b/api/src/unraid-api/graph/resolvers/vms/vms.mutations.resolver.ts @@ -2,10 +2,14 @@ import { Args, ResolveField, Resolver } from '@nestjs/graphql'; import { AuthActionVerb, AuthPossession, UsePermissions } from 'nest-authz'; -import { Resource } from '@app/graphql/generated/api/types.js'; +import { Resource } from '@app/unraid-api/graph/resolvers/base.model.js'; +import { VmMutations } from '@app/unraid-api/graph/resolvers/mutation/mutation.model.js'; import { VmsService } from '@app/unraid-api/graph/resolvers/vms/vms.service.js'; -@Resolver('VmMutations') +/** + * Nested Resolvers for Mutations MUST use @ResolveField() instead of @Mutation() + */ +@Resolver(() => VmMutations) export class VmMutationsResolver { constructor(private readonly vmsService: VmsService) {} @@ -14,8 +18,8 @@ export class VmMutationsResolver { resource: Resource.VMS, possession: AuthPossession.ANY, }) - @ResolveField('startVm') - async startVm(@Args('id') id: string): Promise { + @ResolveField(() => Boolean, { description: 'Start a virtual machine' }) + async start(@Args('id') id: string): Promise { return this.vmsService.startVm(id); } @@ -24,8 +28,8 @@ export class VmMutationsResolver { resource: Resource.VMS, possession: AuthPossession.ANY, }) - @ResolveField('stopVm') - async stopVm(@Args('id') id: string): Promise { + @ResolveField(() => Boolean, { description: 'Stop a virtual machine' }) + async stop(@Args('id') id: string): Promise { return this.vmsService.stopVm(id); } @@ -34,8 +38,8 @@ export class VmMutationsResolver { resource: Resource.VMS, possession: AuthPossession.ANY, }) - @ResolveField('pauseVm') - async pauseVm(@Args('id') id: string): Promise { + @ResolveField(() => Boolean, { description: 'Pause a virtual machine' }) + async pause(@Args('id') id: string): Promise { return this.vmsService.pauseVm(id); } @@ -44,8 +48,8 @@ export class VmMutationsResolver { resource: Resource.VMS, possession: AuthPossession.ANY, }) - @ResolveField('resumeVm') - async resumeVm(@Args('id') id: string): Promise { + @ResolveField(() => Boolean, { description: 'Resume a virtual machine' }) + async resume(@Args('id') id: string): Promise { return this.vmsService.resumeVm(id); } @@ -54,8 +58,8 @@ export class VmMutationsResolver { resource: Resource.VMS, possession: AuthPossession.ANY, }) - @ResolveField('forceStopVm') - async forceStopVm(@Args('id') id: string): Promise { + @ResolveField(() => Boolean, { description: 'Force stop a virtual machine' }) + async forceStop(@Args('id') id: string): Promise { return this.vmsService.forceStopVm(id); } @@ -64,8 +68,8 @@ export class VmMutationsResolver { resource: Resource.VMS, possession: AuthPossession.ANY, }) - @ResolveField('rebootVm') - async rebootVm(@Args('id') id: string): Promise { + @ResolveField(() => Boolean, { description: 'Reboot a virtual machine' }) + async reboot(@Args('id') id: string): Promise { return this.vmsService.rebootVm(id); } @@ -74,8 +78,8 @@ export class VmMutationsResolver { resource: Resource.VMS, possession: AuthPossession.ANY, }) - @ResolveField('resetVm') - async resetVm(@Args('id') id: string): Promise { + @ResolveField(() => Boolean, { description: 'Reset a virtual machine' }) + async reset(@Args('id') id: string): Promise { return this.vmsService.resetVm(id); } } diff --git a/api/src/unraid-api/graph/resolvers/vms/vms.resolver.ts b/api/src/unraid-api/graph/resolvers/vms/vms.resolver.ts index 10ecd9436..fc9223f1d 100644 --- a/api/src/unraid-api/graph/resolvers/vms/vms.resolver.ts +++ b/api/src/unraid-api/graph/resolvers/vms/vms.resolver.ts @@ -2,15 +2,15 @@ import { Query, ResolveField, Resolver } from '@nestjs/graphql'; import { AuthActionVerb, AuthPossession, UsePermissions } from 'nest-authz'; -import type { VmDomain } from '@app/graphql/generated/api/types.js'; -import { Resource } from '@app/graphql/generated/api/types.js'; +import { Resource } from '@app/unraid-api/graph/resolvers/base.model.js'; +import { VmDomain, Vms } from '@app/unraid-api/graph/resolvers/vms/vms.model.js'; import { VmsService } from '@app/unraid-api/graph/resolvers/vms/vms.service.js'; -@Resolver('Vms') +@Resolver(() => Vms) export class VmsResolver { constructor(private readonly vmsService: VmsService) {} - @Query() + @Query(() => Vms) @UsePermissions({ action: AuthActionVerb.READ, resource: Resource.VMS, @@ -22,12 +22,22 @@ export class VmsResolver { }; } - @ResolveField('domain') + @ResolveField(() => [VmDomain]) + public async domains(): Promise> { + try { + return await this.vmsService.getDomains(); + } catch (error) { + throw new Error( + `Failed to retrieve VM domains: ${error instanceof Error ? error.message : 'Unknown error'}` + ); + } + } + + @ResolveField(() => [VmDomain]) public async domain(): Promise> { try { return await this.vmsService.getDomains(); } catch (error) { - // Consider using a proper logger here throw new Error( `Failed to retrieve VM domains: ${error instanceof Error ? error.message : 'Unknown error'}` ); diff --git a/api/src/unraid-api/graph/resolvers/vms/vms.service.spec.ts b/api/src/unraid-api/graph/resolvers/vms/vms.service.spec.ts index fb45d7030..73cd49463 100644 --- a/api/src/unraid-api/graph/resolvers/vms/vms.service.spec.ts +++ b/api/src/unraid-api/graph/resolvers/vms/vms.service.spec.ts @@ -8,7 +8,7 @@ import { join } from 'path'; import { ConnectListAllDomainsFlags, Hypervisor } from '@unraid/libvirt'; import { afterAll, beforeAll, beforeEach, describe, expect, it } from 'vitest'; -import { VmDomain } from '@app/graphql/generated/api/types.js'; +import { VmDomain } from '@app/unraid-api/graph/resolvers/vms/vms.model.js'; import { VmsService } from '@app/unraid-api/graph/resolvers/vms/vms.service.js'; const TEST_VM_NAME = 'test-integration-vm'; diff --git a/api/src/unraid-api/graph/resolvers/vms/vms.service.ts b/api/src/unraid-api/graph/resolvers/vms/vms.service.ts index ec2fbc792..5881d43b0 100644 --- a/api/src/unraid-api/graph/resolvers/vms/vms.service.ts +++ b/api/src/unraid-api/graph/resolvers/vms/vms.service.ts @@ -1,17 +1,17 @@ -import { Injectable, OnModuleInit } from '@nestjs/common'; +import { Injectable, Logger, OnModuleInit } from '@nestjs/common'; import { constants } from 'fs'; import { access } from 'fs/promises'; -import type { Domain, DomainInfo, Hypervisor as HypervisorClass } from '@unraid/libvirt'; +import type { Domain, Hypervisor as HypervisorClass } from '@unraid/libvirt'; import { ConnectListAllDomainsFlags, DomainState, Hypervisor } from '@unraid/libvirt'; import { GraphQLError } from 'graphql'; -import { libvirtLogger } from '@app/core/log.js'; -import { VmDomain, VmState } from '@app/graphql/generated/api/types.js'; import { getters } from '@app/store/index.js'; +import { VmDomain, VmState } from '@app/unraid-api/graph/resolvers/vms/vms.model.js'; @Injectable() export class VmsService implements OnModuleInit { + private readonly logger = new Logger(VmsService.name); private hypervisor: InstanceType | null = null; private isVmsAvailable: boolean = false; private uri: string; @@ -36,33 +36,33 @@ export class VmsService implements OnModuleInit { async onModuleInit() { try { - libvirtLogger.info(`Initializing VMs service with URI: ${this.uri}`); + this.logger.debug(`Initializing VMs service with URI: ${this.uri}`); await this.initializeHypervisor(); this.isVmsAvailable = true; - libvirtLogger.info(`VMs service initialized successfully with URI: ${this.uri}`); + this.logger.debug(`VMs service initialized successfully with URI: ${this.uri}`); } catch (error) { this.isVmsAvailable = false; - libvirtLogger.warn( + this.logger.warn( `VMs are not available: ${error instanceof Error ? error.message : 'Unknown error'}` ); } } private async initializeHypervisor(): Promise { - libvirtLogger.info('Checking if libvirt is running...'); + this.logger.debug('Checking if libvirt is running...'); const running = await this.isLibvirtRunning(); if (!running) { throw new Error('Libvirt is not running'); } - libvirtLogger.info('Libvirt is running, creating hypervisor instance...'); + this.logger.debug('Libvirt is running, creating hypervisor instance...'); this.hypervisor = new Hypervisor({ uri: this.uri }); try { - libvirtLogger.info('Attempting to connect to hypervisor...'); + this.logger.debug('Attempting to connect to hypervisor...'); await this.hypervisor.connectOpen(); - libvirtLogger.info('Successfully connected to hypervisor'); + this.logger.debug('Successfully connected to hypervisor'); } catch (error) { - libvirtLogger.error( + this.logger.error( `Failed starting VM hypervisor connection with "${(error as Error).message}"` ); throw error; @@ -79,11 +79,11 @@ export class VmsService implements OnModuleInit { } try { - libvirtLogger.info(`Looking up domain with UUID: ${uuid}`); + this.logger.debug(`Looking up domain with UUID: ${uuid}`); const domain = await this.hypervisor.domainLookupByUUIDString(uuid); - libvirtLogger.info(`Found domain, getting info...`); + this.logger.debug(`Found domain, getting info...`); const info = await domain.getInfo(); - libvirtLogger.info(`Current domain state: ${info.state}`); + this.logger.debug(`Current domain state: ${info.state}`); // Map VmState to DomainState for comparison const currentState = this.mapDomainStateToVmState(info.state); @@ -97,28 +97,28 @@ export class VmsService implements OnModuleInit { switch (targetState) { case VmState.RUNNING: if (currentState === VmState.SHUTOFF) { - libvirtLogger.info(`Starting domain...`); + this.logger.debug(`Starting domain...`); await domain.create(); } else if (currentState === VmState.PAUSED) { - libvirtLogger.info(`Resuming domain...`); + this.logger.debug(`Resuming domain...`); await domain.resume(); } break; case VmState.SHUTOFF: if (currentState === VmState.RUNNING || currentState === VmState.PAUSED) { - libvirtLogger.info(`Initiating graceful shutdown for domain...`); + this.logger.debug(`Initiating graceful shutdown for domain...`); await domain.shutdown(); const shutdownSuccess = await this.waitForDomainShutdown(domain); if (!shutdownSuccess) { - libvirtLogger.info('Graceful shutdown failed, forcing domain stop...'); + this.logger.debug('Graceful shutdown failed, forcing domain stop...'); await domain.destroy(); } } break; case VmState.PAUSED: if (currentState === VmState.RUNNING) { - libvirtLogger.info(`Pausing domain...`); + this.logger.debug(`Pausing domain...`); await domain.suspend(); } break; @@ -193,9 +193,9 @@ export class VmsService implements OnModuleInit { } try { - libvirtLogger.info(`Looking up domain with UUID: ${uuid}`); + this.logger.debug(`Looking up domain with UUID: ${uuid}`); const domain = await this.hypervisor.domainLookupByUUIDString(uuid); - libvirtLogger.info(`Found domain, force stopping...`); + this.logger.debug(`Found domain, force stopping...`); await domain.destroy(); return true; @@ -212,9 +212,9 @@ export class VmsService implements OnModuleInit { } try { - libvirtLogger.info(`Looking up domain with UUID: ${uuid}`); + this.logger.debug(`Looking up domain with UUID: ${uuid}`); const domain = await this.hypervisor.domainLookupByUUIDString(uuid); - libvirtLogger.info(`Found domain, rebooting...`); + this.logger.debug(`Found domain, rebooting...`); // First try graceful shutdown await domain.shutdown(); @@ -241,9 +241,9 @@ export class VmsService implements OnModuleInit { } try { - libvirtLogger.info(`Looking up domain with UUID: ${uuid}`); + this.logger.debug(`Looking up domain with UUID: ${uuid}`); const domain = await this.hypervisor.domainLookupByUUIDString(uuid); - libvirtLogger.info(`Found domain, resetting...`); + this.logger.debug(`Found domain, resetting...`); // Force stop the domain await domain.destroy(); @@ -268,19 +268,19 @@ export class VmsService implements OnModuleInit { try { const hypervisor = this.hypervisor; - libvirtLogger.info('Getting all domains...'); + this.logger.debug('Getting all domains...'); // Get both active and inactive domains const domains = await hypervisor.connectListAllDomains( ConnectListAllDomainsFlags.ACTIVE | ConnectListAllDomainsFlags.INACTIVE ); - libvirtLogger.info(`Found ${domains.length} domains`); + this.logger.debug(`Found ${domains.length} domains`); const resolvedDomains: Array = await Promise.all( domains.map(async (domain) => { const info = await domain.getInfo(); const name = await domain.getName(); const uuid = await domain.getUUIDString(); - libvirtLogger.info( + this.logger.debug( `Found domain: ${name} (${uuid}) with state ${DomainState[info.state]}` ); @@ -299,7 +299,7 @@ export class VmsService implements OnModuleInit { } catch (error: unknown) { // If we hit an error expect libvirt to be offline this.isVmsAvailable = false; - libvirtLogger.error( + this.logger.error( `Failed to get domains: ${error instanceof Error ? error.message : 'Unknown error'}` ); throw new GraphQLError( @@ -316,13 +316,13 @@ export class VmsService implements OnModuleInit { for (let i = 0; i < maxRetries; i++) { const currentInfo = await this.hypervisor.domainGetInfo(domain); if (currentInfo.state === DomainState.SHUTOFF) { - libvirtLogger.info('Domain shutdown completed successfully'); + this.logger.debug('Domain shutdown completed successfully'); return true; } - libvirtLogger.debug(`Waiting for domain shutdown... (attempt ${i + 1}/${maxRetries})`); + this.logger.debug(`Waiting for domain shutdown... (attempt ${i + 1}/${maxRetries})`); await new Promise((resolve) => setTimeout(resolve, 1000)); } - libvirtLogger.warn(`Domain shutdown timed out after ${maxRetries} attempts`); + this.logger.warn(`Domain shutdown timed out after ${maxRetries} attempts`); return false; } } diff --git a/api/src/unraid-api/graph/services/service.model.ts b/api/src/unraid-api/graph/services/service.model.ts new file mode 100644 index 000000000..1b502dbfd --- /dev/null +++ b/api/src/unraid-api/graph/services/service.model.ts @@ -0,0 +1,29 @@ +import { Field, ID, ObjectType } from '@nestjs/graphql'; + +import { Node } from '@app/unraid-api/graph/resolvers/base.model.js'; + +@ObjectType() +export class Uptime { + @Field(() => String, { nullable: true }) + timestamp?: string; +} + +@ObjectType({ + implements: () => Node, +}) +export class Service implements Node { + @Field(() => ID) + id!: string; + + @Field(() => String, { nullable: true }) + name?: string; + + @Field(() => Boolean, { nullable: true }) + online?: boolean; + + @Field(() => Uptime, { nullable: true }) + uptime?: Uptime; + + @Field(() => String, { nullable: true }) + version?: string; +} diff --git a/api/src/unraid-api/graph/services/services.resolver.ts b/api/src/unraid-api/graph/services/services.resolver.ts index 8ff91a3a0..0e02c4942 100644 --- a/api/src/unraid-api/graph/services/services.resolver.ts +++ b/api/src/unraid-api/graph/services/services.resolver.ts @@ -2,13 +2,14 @@ import { Query, Resolver } from '@nestjs/graphql'; import { AuthActionVerb, AuthPossession, UsePermissions } from 'nest-authz'; -import type { Service } from '@app/graphql/generated/api/types.js'; import { bootTimestamp } from '@app/common/dashboard/boot-timestamp.js'; import { API_VERSION } from '@app/environment.js'; -import { DynamicRemoteAccessType, Resource } from '@app/graphql/generated/api/types.js'; import { store } from '@app/store/index.js'; +import { Resource } from '@app/unraid-api/graph/resolvers/base.model.js'; +import { DynamicRemoteAccessType } from '@app/unraid-api/graph/resolvers/connect/connect.model.js'; +import { Service } from '@app/unraid-api/graph/services/service.model.js'; -@Resolver('Services') +@Resolver(() => Service) export class ServicesResolver { constructor() {} @@ -39,7 +40,7 @@ export class ServicesResolver { }; }; - @Query('services') + @Query(() => [Service]) @UsePermissions({ action: AuthActionVerb.READ, resource: Resource.SERVICES, diff --git a/api/src/unraid-api/graph/shares/shares.resolver.ts b/api/src/unraid-api/graph/shares/shares.resolver.ts index 367b22ffc..c03f5ddba 100644 --- a/api/src/unraid-api/graph/shares/shares.resolver.ts +++ b/api/src/unraid-api/graph/shares/shares.resolver.ts @@ -3,13 +3,14 @@ import { Query, Resolver } from '@nestjs/graphql'; import { AuthActionVerb, AuthPossession, UsePermissions } from 'nest-authz'; import { getShares } from '@app/core/utils/shares/get-shares.js'; -import { Resource } from '@app/graphql/generated/api/types.js'; +import { Share } from '@app/unraid-api/graph/resolvers/array/array.model.js'; +import { Resource } from '@app/unraid-api/graph/resolvers/base.model.js'; -@Resolver('Shares') +@Resolver(() => Share) export class SharesResolver { constructor() {} - @Query('shares') + @Query(() => [Share]) @UsePermissions({ action: AuthActionVerb.READ, resource: Resource.SHARE, diff --git a/api/src/unraid-api/graph/user/user.model.ts b/api/src/unraid-api/graph/user/user.model.ts new file mode 100644 index 000000000..a9d96bb8d --- /dev/null +++ b/api/src/unraid-api/graph/user/user.model.ts @@ -0,0 +1,22 @@ +import { Field, ID, ObjectType } from '@nestjs/graphql'; + +import { Permission } from '@app/unraid-api/graph/resolvers/api-key/api-key.model.js'; +import { Role } from '@app/unraid-api/graph/resolvers/base.model.js'; + +@ObjectType() +export class UserAccount { + @Field(() => ID, { description: 'A unique identifier for the user' }) + id!: string; + + @Field({ description: 'The name of the user' }) + name!: string; + + @Field({ description: 'A description of the user' }) + description!: string; + + @Field(() => [Role], { description: 'The roles of the user' }) + roles!: Role[]; + + @Field(() => [Permission], { description: 'The permissions of the user', nullable: true }) + permissions?: Permission[]; +} diff --git a/api/src/unraid-api/graph/resolvers/me/me.resolver.spec.ts b/api/src/unraid-api/graph/user/user.resolver.spec.ts similarity index 88% rename from api/src/unraid-api/graph/resolvers/me/me.resolver.spec.ts rename to api/src/unraid-api/graph/user/user.resolver.spec.ts index 457f2827a..5ed351fa2 100644 --- a/api/src/unraid-api/graph/resolvers/me/me.resolver.spec.ts +++ b/api/src/unraid-api/graph/user/user.resolver.spec.ts @@ -3,8 +3,9 @@ import { Test, TestingModule } from '@nestjs/testing'; import { AuthZService } from 'nest-authz'; import { beforeEach, describe, expect, it, vi } from 'vitest'; -import { Me, Resource, Role, UserAccount } from '@app/graphql/generated/api/types.js'; -import { MeResolver } from '@app/unraid-api/graph/resolvers/me/me.resolver.js'; +import { Resource, Role } from '@app/unraid-api/graph/resolvers/base.model.js'; +import { UserAccount } from '@app/unraid-api/graph/user/user.model.js'; +import { MeResolver } from '@app/unraid-api/graph/user/user.resolver.js'; describe('MeResolver', () => { let resolver: MeResolver; diff --git a/api/src/unraid-api/graph/resolvers/me/me.resolver.ts b/api/src/unraid-api/graph/user/user.resolver.ts similarity index 59% rename from api/src/unraid-api/graph/resolvers/me/me.resolver.ts rename to api/src/unraid-api/graph/user/user.resolver.ts index be7fc7aa3..3eeb22d1f 100644 --- a/api/src/unraid-api/graph/resolvers/me/me.resolver.ts +++ b/api/src/unraid-api/graph/user/user.resolver.ts @@ -2,21 +2,21 @@ import { Query, Resolver } from '@nestjs/graphql'; import { AuthActionVerb, AuthPossession, UsePermissions } from 'nest-authz'; -import type { UserAccount } from '@app/graphql/generated/api/types.js'; -import { Me, Resource } from '@app/graphql/generated/api/types.js'; import { GraphqlUser } from '@app/unraid-api/auth/user.decorator.js'; +import { Resource } from '@app/unraid-api/graph/resolvers/base.model.js'; +import { UserAccount } from '@app/unraid-api/graph/user/user.model.js'; -@Resolver('Me') +@Resolver(() => UserAccount) export class MeResolver { constructor() {} - @Query() + @Query(() => UserAccount) @UsePermissions({ action: AuthActionVerb.READ, resource: Resource.ME, possession: AuthPossession.ANY, }) - public async me(@GraphqlUser() user: UserAccount): Promise { + public async me(@GraphqlUser() user: UserAccount): Promise { return user; } } diff --git a/api/src/unraid-api/main.ts b/api/src/unraid-api/main.ts index 2c0312d1a..b5371246b 100644 --- a/api/src/unraid-api/main.ts +++ b/api/src/unraid-api/main.ts @@ -1,4 +1,5 @@ import type { NestFastifyApplication } from '@nestjs/platform-fastify'; +import { ValidationPipe } from '@nestjs/common'; import { NestFactory } from '@nestjs/core'; import { FastifyAdapter } from '@nestjs/platform-fastify/index.js'; @@ -20,6 +21,18 @@ export async function bootstrapNestServer(): Promise { ...(LOG_LEVEL !== 'TRACE' ? { logger: false } : {}), }); + // Enable validation globally + app.useGlobalPipes( + new ValidationPipe({ + transform: true, + whitelist: true, + forbidNonWhitelisted: true, + transformOptions: { + enableImplicitConversion: true, + }, + }) + ); + const server = app.getHttpAdapter().getInstance(); await server.register(fastifyCookie); diff --git a/api/src/unraid-api/rest/rest.controller.ts b/api/src/unraid-api/rest/rest.controller.ts index 444282d26..c2d87b1fc 100644 --- a/api/src/unraid-api/rest/rest.controller.ts +++ b/api/src/unraid-api/rest/rest.controller.ts @@ -3,8 +3,8 @@ import { Controller, Get, Logger, Param, Res } from '@nestjs/common'; import { AuthActionVerb, AuthPossession, UsePermissions } from 'nest-authz'; import type { FastifyReply } from '@app/unraid-api/types/fastify.js'; -import { Resource } from '@app/graphql/generated/api/types.js'; import { Public } from '@app/unraid-api/auth/public.decorator.js'; +import { Resource } from '@app/unraid-api/graph/resolvers/base.model.js'; import { RestService } from '@app/unraid-api/rest/rest.service.js'; @Controller() diff --git a/api/src/utils.ts b/api/src/utils.ts index bf08d28e1..492557379 100644 --- a/api/src/utils.ts +++ b/api/src/utils.ts @@ -1,11 +1,9 @@ import { BadRequestException, ExecutionContext, Logger, UnauthorizedException } from '@nestjs/common'; import { GqlExecutionContext } from '@nestjs/graphql'; -import { access, constants, copyFile, unlink } from 'node:fs/promises'; -import { dirname } from 'node:path'; import strftime from 'strftime'; -import { UserAccount } from '@app/graphql/generated/api/types.js'; +import { UserAccount } from '@app/unraid-api/graph/user/user.model.js'; import { FastifyRequest } from '@app/unraid-api/types/fastify.js'; export function notNull(value: T): value is NonNullable { diff --git a/api/vite.config.ts b/api/vite.config.ts index aeb954557..bba3f4766 100644 --- a/api/vite.config.ts +++ b/api/vite.config.ts @@ -143,6 +143,10 @@ export default defineConfig(({ mode }): ViteUserConfig => { }, }, test: { + browser: { + enabled: false, + }, + open: false, isolate: true, poolOptions: { threads: { diff --git a/generated-schema-new.graphql b/generated-schema-new.graphql new file mode 100644 index 000000000..8a0c87333 --- /dev/null +++ b/generated-schema-new.graphql @@ -0,0 +1,1528 @@ +# ------------------------------------------------------ +# THIS FILE WAS AUTOMATICALLY GENERATED (DO NOT MODIFY) +# ------------------------------------------------------ + +type ApiKeyResponse { + valid: Boolean! + error: String +} + +type MinigraphqlResponse { + status: MinigraphStatus! + timeout: Int + error: String +} + +enum MinigraphStatus { + PRE_INIT + CONNECTING + CONNECTED + PING_FAILURE + ERROR_RETRYING +} + +type CloudResponse { + status: String! + ip: String + error: String +} + +type RelayResponse { + status: String! + timeout: String + error: String +} + +type Cloud { + error: String + apiKey: ApiKeyResponse! + relay: RelayResponse + minigraphql: MinigraphqlResponse! + cloud: CloudResponse! + allowedOrigins: [String!]! +} + +type Capacity { + """Free capacity""" + free: String! + + """Used capacity""" + used: String! + + """Total capacity""" + total: String! +} + +type ArrayCapacity { + """Capacity in kilobytes""" + kilobytes: Capacity! + + """Capacity in number of disks""" + disks: Capacity! +} + +type ArrayDisk { + """Disk identifier, only set for present disks on the system""" + id: ID! + + """ + Array slot number. Parity1 is always 0 and Parity2 is always 29. Array slots will be 1 - 28. Cache slots are 30 - 53. Flash is 54. + """ + idx: Int! + name: String + device: String + + """(KB) Disk Size total""" + size: Float! + status: ArrayDiskStatus + + """Is the disk a HDD or SSD.""" + rotational: Boolean + + """Disk temp - will be NaN if array is not started or DISK_NP""" + temp: Int + + """ + Count of I/O read requests sent to the device I/O drivers. These statistics may be cleared at any time. + """ + numReads: Float! + + """ + Count of I/O writes requests sent to the device I/O drivers. These statistics may be cleared at any time. + """ + numWrites: Float! + + """ + Number of unrecoverable errors reported by the device I/O drivers. Missing data due to unrecoverable array read errors is filled in on-the-fly using parity reconstruct (and we attempt to write this data back to the sector(s) which failed). Any unrecoverable write error results in disabling the disk. + """ + numErrors: Float! + + """(KB) Total Size of the FS (Not present on Parity type drive)""" + fsSize: Float + + """(KB) Free Size on the FS (Not present on Parity type drive)""" + fsFree: Float + + """(KB) Used Size on the FS (Not present on Parity type drive)""" + fsUsed: Float + exportable: Boolean + + """Type of Disk - used to differentiate Cache / Flash / Array / Parity""" + type: ArrayDiskType! + + """(%) Disk space left to warn""" + warning: Int + + """(%) Disk space left for critical""" + critical: Int + + """File system type for the disk""" + fsType: String + + """User comment on disk""" + comment: String + + """File format (ex MBR: 4KiB-aligned)""" + format: String + + """ata | nvme | usb | (others)""" + transport: String + color: ArrayDiskFsColor +} + +enum ArrayDiskStatus { + DISK_NP + DISK_OK + DISK_NP_MISSING + DISK_INVALID + DISK_WRONG + DISK_DSBL + DISK_NP_DSBL + DISK_DSBL_NEW + DISK_NEW +} + +enum ArrayDiskType { + DATA + PARITY + FLASH + CACHE +} + +enum ArrayDiskFsColor { + GREEN_ON + GREEN_BLINK + BLUE_ON + BLUE_BLINK + YELLOW_ON + YELLOW_BLINK + RED_ON + RED_OFF + GREY_OFF +} + +type UnraidArray { + id: ID! + + """Array state before this query/mutation""" + previousState: ArrayState + + """Array state after this query/mutation""" + pendingState: ArrayPendingState + + """Current array state""" + state: ArrayState! + + """Current array capacity""" + capacity: ArrayCapacity! + + """Current boot disk""" + boot: ArrayDisk + + """Parity disks in the current array""" + parities: [ArrayDisk!]! + + """Data disks in the current array""" + disks: [ArrayDisk!]! + + """Caches in the current array""" + caches: [ArrayDisk!]! +} + +enum ArrayState { + STARTED + STOPPED + NEW_ARRAY + RECON_DISK + DISABLE_DISK + SWAP_DSBL + INVALID_EXPANSION + PARITY_NOT_BIGGEST + TOO_MANY_MISSING_DISKS + NEW_DISK_TOO_SMALL + NO_DATA_DISKS +} + +enum ArrayPendingState { + STARTING + STOPPING + NO_DATA_DISKS + TOO_MANY_MISSING_DISKS +} + +type Share { + id: ID! + + """Display name""" + name: String + + """(KB) Free space""" + free: Long + + """(KB) Used Size""" + used: Long + + """(KB) Total size""" + size: Long + + """Disks that are included in this share""" + include: [String!] + + """Disks that are excluded from this share""" + exclude: [String!] + + """Is this share cached""" + cache: Boolean + + """Original name""" + nameOrig: String + + """User comment""" + comment: String + + """Allocator""" + allocator: String + + """Split level""" + splitLevel: String + + """Floor""" + floor: String + + """COW""" + cow: String + + """Color""" + color: String + + """LUKS status""" + luksStatus: String +} + +"""The `Long` scalar type represents 52-bit integers""" +scalar Long + +type RemoteAccess { + """The type of WAN access used for Remote Access""" + accessType: WAN_ACCESS_TYPE! + + """The type of port forwarding used for Remote Access""" + forwardType: WAN_FORWARD_TYPE + + """The port used for Remote Access""" + port: Int +} + +enum WAN_ACCESS_TYPE { + DYNAMIC + ALWAYS + DISABLED +} + +enum WAN_FORWARD_TYPE { + UPNP + STATIC +} + +type DynamicRemoteAccessStatus { + """The type of dynamic remote access that is enabled""" + enabledType: DynamicRemoteAccessType! + + """The type of dynamic remote access that is currently running""" + runningType: DynamicRemoteAccessType! + + """Any error message associated with the dynamic remote access""" + error: String +} + +enum DynamicRemoteAccessType { + STATIC + UPNP + DISABLED +} + +type ConnectSettingsValues { + """ + If true, the GraphQL sandbox is enabled and available at /graphql. If false, the GraphQL sandbox is disabled and only the production API will be available. + """ + sandbox: Boolean! + + """A list of origins allowed to interact with the API""" + extraOrigins: [String!]! + + """The type of WAN access used for Remote Access""" + accessType: WAN_ACCESS_TYPE! + + """The type of port forwarding used for Remote Access""" + forwardType: WAN_FORWARD_TYPE + + """The port used for Remote Access""" + port: Int + + """A list of Unique Unraid Account ID's""" + ssoUserIds: [String!]! +} + +type ConnectSettings { + """The unique identifier for the Connect settings""" + id: ID! + + """The data schema for the Connect settings""" + dataSchema: JSON! + + """The UI schema for the Connect settings""" + uiSchema: JSON! + + """The values for the Connect settings""" + values: ConnectSettingsValues! +} + +""" +The `JSON` scalar type represents JSON values as specified by [ECMA-404](http://www.ecma-international.org/publications/files/ECMA-ST/ECMA-404.pdf). +""" +scalar JSON @specifiedBy(url: "http://www.ecma-international.org/publications/files/ECMA-ST/ECMA-404.pdf") + +type Connect { + """The unique identifier for the Connect instance""" + id: ID! + + """The status of dynamic remote access""" + dynamicRemoteAccess: DynamicRemoteAccessStatus! + + """The settings for the Connect instance""" + settings: ConnectSettings! +} + +type AccessUrl { + type: URL_TYPE! + name: String + ipv4: URL + ipv6: URL +} + +enum URL_TYPE { + LAN + WIREGUARD + WAN + MDNS + OTHER + DEFAULT +} + +""" +A field whose value conforms to the standard URL format as specified in RFC3986: https://www.ietf.org/rfc/rfc3986.txt. +""" +scalar URL + +type Network { + id: ID! + accessUrls: [AccessUrl!] +} + +type ProfileModel { + userId: ID + username: String! + url: String! + avatar: String! +} + +type Server { + owner: ProfileModel! + guid: String! + apikey: String! + name: String! + status: ServerStatus! + wanip: String! + lanip: String! + localurl: String! + remoteurl: String! +} + +enum ServerStatus { + ONLINE + OFFLINE + NEVER_CONNECTED +} + +type DiskPartition { + """The name of the partition""" + name: String! + + """The filesystem type of the partition""" + fsType: DiskFsType! + + """The size of the partition in bytes""" + size: Float! +} + +"""The type of filesystem on the disk partition""" +enum DiskFsType { + XFS + BTRFS + VFAT + ZFS + EXT4 + NTFS +} + +type Disk { + """The unique identifier of the disk""" + id: String! + + """The device path of the disk (e.g. /dev/sdb)""" + device: String! + + """The type of disk (e.g. SSD, HDD)""" + type: String! + + """The model name of the disk""" + name: String! + + """The manufacturer of the disk""" + vendor: String! + + """The total size of the disk in bytes""" + size: Float! + + """The number of bytes per sector""" + bytesPerSector: Float! + + """The total number of cylinders on the disk""" + totalCylinders: Float! + + """The total number of heads on the disk""" + totalHeads: Float! + + """The total number of sectors on the disk""" + totalSectors: Float! + + """The total number of tracks on the disk""" + totalTracks: Float! + + """The number of tracks per cylinder""" + tracksPerCylinder: Float! + + """The number of sectors per track""" + sectorsPerTrack: Float! + + """The firmware revision of the disk""" + firmwareRevision: String! + + """The serial number of the disk""" + serialNum: String! + + """The interface type of the disk""" + interfaceType: DiskInterfaceType! + + """The SMART status of the disk""" + smartStatus: DiskSmartStatus! + + """The current temperature of the disk in Celsius""" + temperature: Float + + """The partitions on the disk""" + partitions: [DiskPartition!]! +} + +"""The type of interface the disk uses to connect to the system""" +enum DiskInterfaceType { + SAS + SATA + USB + PCIE + UNKNOWN +} + +""" +The SMART (Self-Monitoring, Analysis and Reporting Technology) status of the disk +""" +enum DiskSmartStatus { + OK + UNKNOWN +} + +type KeyFile { + location: String + contents: String +} + +type Registration { + guid: ID + type: registrationType + keyFile: KeyFile + state: RegistrationState + expiration: String + updateExpiration: String +} + +enum registrationType { + BASIC + PLUS + PRO + STARTER + UNLEASHED + LIFETIME + INVALID + TRIAL +} + +enum RegistrationState { + TRIAL + BASIC + PLUS + PRO + STARTER + UNLEASHED + LIFETIME + EEXPIRED + EGUID + EGUID1 + ETRIAL + ENOKEYFILE + ENOKEYFILE1 + ENOKEYFILE2 + ENOFLASH + ENOFLASH1 + ENOFLASH2 + ENOFLASH3 + ENOFLASH4 + ENOFLASH5 + ENOFLASH6 + ENOFLASH7 + EBLACKLISTED + EBLACKLISTED1 + EBLACKLISTED2 + ENOCONN +} + +type Vars { + id: ID! + + """Unraid version""" + version: String + maxArraysz: Int + maxCachesz: Int + + """Machine hostname""" + name: String + timeZone: String + comment: String + security: String + workgroup: String + domain: String + domainShort: String + hideDotFiles: Boolean + localMaster: Boolean + enableFruit: String + + """Should a NTP server be used for time sync?""" + useNtp: Boolean + + """NTP Server 1""" + ntpServer1: String + + """NTP Server 2""" + ntpServer2: String + + """NTP Server 3""" + ntpServer3: String + + """NTP Server 4""" + ntpServer4: String + domainLogin: String + sysModel: String + sysArraySlots: Int + sysCacheSlots: Int + sysFlashSlots: Int + useSsl: Boolean + + """Port for the webui via HTTP""" + port: Int + + """Port for the webui via HTTPS""" + portssl: Int + localTld: String + bindMgt: Boolean + + """Should telnet be enabled?""" + useTelnet: Boolean + porttelnet: Int + useSsh: Boolean + portssh: Int + startPage: String + startArray: Boolean + spindownDelay: String + queueDepth: String + spinupGroups: Boolean + defaultFormat: String + defaultFsType: String + shutdownTimeout: Int + luksKeyfile: String + pollAttributes: String + pollAttributesDefault: String + pollAttributesStatus: String + nrRequests: Int + nrRequestsDefault: Int + nrRequestsStatus: String + mdNumStripes: Int + mdNumStripesDefault: Int + mdNumStripesStatus: String + mdSyncWindow: Int + mdSyncWindowDefault: Int + mdSyncWindowStatus: String + mdSyncThresh: Int + mdSyncThreshDefault: Int + mdSyncThreshStatus: String + mdWriteMethod: Int + mdWriteMethodDefault: String + mdWriteMethodStatus: String + shareDisk: String + shareUser: String + shareUserInclude: String + shareUserExclude: String + shareSmbEnabled: Boolean + shareNfsEnabled: Boolean + shareAfpEnabled: Boolean + shareInitialOwner: String + shareInitialGroup: String + shareCacheEnabled: Boolean + shareCacheFloor: String + shareMoverSchedule: String + shareMoverLogging: Boolean + fuseRemember: String + fuseRememberDefault: String + fuseRememberStatus: String + fuseDirectio: String + fuseDirectioDefault: String + fuseDirectioStatus: String + shareAvahiEnabled: Boolean + shareAvahiSmbName: String + shareAvahiSmbModel: String + shareAvahiAfpName: String + shareAvahiAfpModel: String + safeMode: Boolean + startMode: String + configValid: Boolean + configError: ConfigErrorState + joinStatus: String + deviceCount: Int + flashGuid: String + flashProduct: String + flashVendor: String + regCheck: String + regFile: String + regGuid: String + regTy: registrationType + regState: String + + """Registration owner""" + regTo: String + regTm: String + regTm2: String + regGen: String + sbName: String + sbVersion: String + sbUpdated: String + sbEvents: Int + sbState: String + sbClean: Boolean + sbSynced: Int + sbSyncErrs: Int + sbSynced2: Int + sbSyncExit: String + sbNumDisks: Int + mdColor: String + mdNumDisks: Int + mdNumDisabled: Int + mdNumInvalid: Int + mdNumMissing: Int + mdNumNew: Int + mdNumErased: Int + mdResync: Int + mdResyncCorr: String + mdResyncPos: String + mdResyncDb: String + mdResyncDt: String + mdResyncAction: String + mdResyncSize: Int + mdState: String + mdVersion: String + cacheNumDevices: Int + cacheSbNumDisks: Int + fsState: String + + """Human friendly string of array events happening""" + fsProgress: String + + """ + Percentage from 0 - 100 while upgrading a disk or swapping parity drives + """ + fsCopyPrcnt: Int + fsNumMounted: Int + fsNumUnmountable: Int + fsUnmountableMask: String + + """Total amount of user shares""" + shareCount: Int + + """Total amount shares with SMB enabled""" + shareSmbCount: Int + + """Total amount shares with NFS enabled""" + shareNfsCount: Int + + """Total amount shares with AFP enabled""" + shareAfpCount: Int + shareMoverActive: Boolean + csrfToken: String +} + +"""Possible error states for configuration""" +enum ConfigErrorState { + UNKNOWN_ERROR + INELIGIBLE + INVALID + NO_KEY_SERVER + WITHDRAWN +} + +type Permission { + resource: Resource! + actions: [String!]! +} + +"""Available resources for permissions""" +enum Resource { + API_KEY + ARRAY + CLOUD + CONFIG + CONNECT + CONNECT__REMOTE_ACCESS + CUSTOMIZATIONS + DASHBOARD + DISK + DISPLAY + DOCKER + FLASH + INFO + LOGS + ME + NETWORK + NOTIFICATIONS + ONLINE + OS + OWNER + PERMISSION + REGISTRATION + SERVERS + SERVICES + SHARE + VARS + VMS + WELCOME +} + +type ApiKey { + id: ID! + name: String! + description: String + roles: [Role!]! + createdAt: String! + permissions: [Permission!]! +} + +"""Available roles for API keys and users""" +enum Role { + ADMIN + CONNECT + GUEST +} + +type ApiKeyWithSecret { + id: ID! + name: String! + description: String + roles: [Role!]! + createdAt: String! + permissions: [Permission!]! + key: String! +} + +type ArrayMutations { + """Placeholder field to ensure the type is not empty""" + _: Boolean! +} + +type DockerMutations { + """Placeholder field to ensure the type is not empty""" + _: Boolean! + + """Start a container""" + start(id: String!): DockerContainer! + + """Stop a container""" + stop(id: String!): DockerContainer! +} + +type Config { + id: ID! + valid: Boolean + error: String +} + +type InfoApps { + id: ID! + + """How many docker containers are installed""" + installed: Int! + + """How many docker containers are running""" + started: Int! +} + +type Baseboard { + id: ID! + manufacturer: String! + model: String + version: String + serial: String + assetTag: String +} + +type InfoCpu { + id: ID! + manufacturer: String! + brand: String! + vendor: String! + family: String! + model: String! + stepping: Int! + revision: String! + voltage: String + speed: Float! + speedmin: Float! + speedmax: Float! + threads: Int! + cores: Int! + processors: Int! + socket: String! + cache: JSON! + flags: [String!]! +} + +type Gpu { + id: ID! + type: String! + typeid: String! + vendorname: String! + productid: String! + blacklisted: Boolean! + class: String! +} + +type Pci { + id: ID! + type: String + typeid: String + vendorname: String + vendorid: String + productname: String + productid: String + blacklisted: String + class: String +} + +type Usb { + id: ID! + name: String +} + +type Devices { + id: ID! + gpu: [Gpu!]! + pci: [Pci!]! + usb: [Usb!]! +} + +type Case { + icon: String + url: String + error: String + base64: String +} + +type Display { + id: ID! + case: Case + date: String + number: String + scale: Boolean + tabs: Boolean + users: String + resize: Boolean + wwn: Boolean + total: Boolean + usage: Boolean + banner: String + dashapps: String + theme: Theme + text: Boolean + unit: Temperature + warning: Int + critical: Int + hot: Int + max: Int + locale: String +} + +"""Display theme""" +enum Theme { + white +} + +"""Temperature unit (Celsius or Fahrenheit)""" +enum Temperature { + C + F +} + +type MemoryLayout { + size: Int! + bank: String + type: String + clockSpeed: Int + formFactor: String + manufacturer: String + partNum: String + serialNum: String + voltageConfigured: Int + voltageMin: Int + voltageMax: Int +} + +type InfoMemory { + id: ID! + max: Int! + total: Int! + free: Int! + used: Int! + active: Int! + available: Int! + buffcache: Int! + swaptotal: Int! + swapused: Int! + swapfree: Int! + layout: [MemoryLayout!]! +} + +type Os { + id: ID! + platform: String + distro: String + release: String + codename: String + kernel: String + arch: String + hostname: String + codepage: String + logofile: String + serial: String + build: String + uptime: String +} + +type System { + id: ID! + manufacturer: String + model: String + version: String + serial: String + uuid: String + sku: String +} + +type Versions { + id: ID! + kernel: String + openssl: String + systemOpenssl: String + systemOpensslLib: String + node: String + v8: String + npm: String + yarn: String + pm2: String + gulp: String + grunt: String + git: String + tsc: String + mysql: String + redis: String + mongodb: String + apache: String + nginx: String + php: String + docker: String + postfix: String + postgresql: String + perl: String + python: String + gcc: String + unraid: String +} + +type Info { + id: ID! + + """Count of docker containers""" + apps: InfoApps! + baseboard: Baseboard! + cpu: InfoCpu! + devices: Devices! + display: Display! + + """Machine ID""" + machineId: ID + memory: InfoMemory! + os: Os! + system: System! + time: DateTime! + versions: Versions! +} + +""" +A date-time string at UTC, such as 2019-12-03T09:54:33Z, compliant with the date-time format. +""" +scalar DateTime + +type ContainerPort { + ip: String + privatePort: Int! + publicPort: Int! + type: ContainerPortType! +} + +enum ContainerPortType { + TCP + UDP +} + +type ContainerHostConfig { + networkMode: String! +} + +type DockerContainer { + id: ID! + names: [String!]! + image: String! + imageId: String! + command: String! + created: Int! + ports: [ContainerPort!]! + + """Total size of all the files in the container""" + sizeRootFs: Int + labels: JSONObject + state: ContainerState! + status: String! + hostConfig: ContainerHostConfig + networkSettings: JSONObject + mounts: [JSONObject!] + autoStart: Boolean! +} + +""" +The `JSONObject` scalar type represents JSON objects as specified by [ECMA-404](http://www.ecma-international.org/publications/files/ECMA-ST/ECMA-404.pdf). +""" +scalar JSONObject + +enum ContainerState { + RUNNING + EXITED +} + +type DockerNetwork { + name: String! + id: ID! + created: String! + scope: String! + driver: String! + enableIPv6: Boolean! + ipam: JSONObject! + internal: Boolean! + attachable: Boolean! + ingress: Boolean! + configFrom: JSONObject! + configOnly: Boolean! + containers: JSONObject! + options: JSONObject! + labels: JSONObject! +} + +type Docker { + id: ID! + containers: [DockerContainer!]! + networks: [DockerNetwork!]! +} + +type Flash { + id: ID! + guid: String! + vendor: String! + product: String! +} + +type LogFile { + """Name of the log file""" + name: String! + + """Full path to the log file""" + path: String! + + """Size of the log file in bytes""" + size: Int! + + """Last modified timestamp""" + modifiedAt: DateTime! +} + +type LogFileContent { + """Path to the log file""" + path: String! + + """Content of the log file""" + content: String! + + """Total number of lines in the file""" + totalLines: Int! + + """Starting line number of the content (1-indexed)""" + startLine: Int +} + +type NotificationCounts { + info: Int! + warning: Int! + alert: Int! + total: Int! +} + +type NotificationOverview { + unread: NotificationCounts! + archive: NotificationCounts! +} + +type Notification { + id: ID! + + """Also known as 'event'""" + title: String! + subject: String! + description: String! + importance: NotificationImportance! + link: String + type: NotificationType! + + """ISO Timestamp for when the notification occurred""" + timestamp: String + formattedTimestamp: String +} + +enum NotificationImportance { + ALERT + INFO + WARNING +} + +enum NotificationType { + UNREAD + ARCHIVE +} + +type Notifications { + id: ID! + + """A cached overview of the notifications in the system & their severity.""" + overview: NotificationOverview! + list(filter: NotificationFilter!): [Notification!]! +} + +input NotificationFilter { + importance: NotificationImportance + type: NotificationType! + offset: Int! + limit: Int! +} + +type Owner { + username: String! + url: String! + avatar: String! +} + +type VmDomain { + uuid: ID! + + """A friendly name for the vm""" + name: String + + """Current domain vm state""" + state: VmState! +} + +"""The state of a virtual machine""" +enum VmState { + NOSTATE + RUNNING + IDLE + PAUSED + SHUTDOWN + SHUTOFF + CRASHED + PMSUSPENDED +} + +type Vms { + id: ID! + domain: [VmDomain!]! + domains: [VmDomain!]! +} + +type Uptime { + timestamp: String +} + +type Service { + id: ID! + name: String + online: Boolean + uptime: Uptime + version: String +} + +type UserAccount { + """A unique identifier for the user""" + id: ID! + + """The name of the user""" + name: String! + + """A description of the user""" + description: String! + + """The roles of the user""" + roles: [Role!]! + + """The permissions of the user""" + permissions: [Permission!] +} + +type Query { + apiKeys: [ApiKey!]! + apiKey(id: String!): ApiKey + array: UnraidArray! + cloud: Cloud! + config: Config! + connect: Connect! + remoteAccess: RemoteAccess! + extraAllowedOrigins: [String!]! + display: Display! + flash: Flash! + info: Info! + logFiles: [LogFile!]! + logFile(path: String!, lines: Float, startLine: Float): LogFileContent! + me: UserAccount! + network: Network! + + """Get all notifications""" + notifications: Notifications! + online: Boolean! + owner: Owner! + registration: Registration + server: Server + servers: [Server!]! + services: [Service!]! + shares: [Share!]! + vars: Vars! + vms: Vms! + docker: Docker! + disks: [Disk!]! + disk(id: String!): Disk! +} + +type Mutation { + createApiKey(input: CreateApiKeyInput!): ApiKeyWithSecret! + addRoleForApiKey(input: AddRoleForApiKeyInput!): Boolean! + removeRoleFromApiKey(input: RemoveRoleFromApiKeyInput!): Boolean! + + """Set array state""" + setState(input: ArrayStateInput!): UnraidArray! + + """Add new disk to array""" + addDiskToArray(input: ArrayDiskInput!): UnraidArray! + + """ + Remove existing disk from array. NOTE: The array must be stopped before running this otherwise it'll throw an error. + """ + removeDiskFromArray(input: ArrayDiskInput!): UnraidArray! + + """Mount a disk in the array""" + mountArrayDisk(id: String!): ArrayDisk! + + """Unmount a disk from the array""" + unmountArrayDisk(id: String!): ArrayDisk! + + """Clear statistics for a disk in the array""" + clearArrayDiskStatistics(id: String!): Boolean! + updateApiSettings(input: ApiSettingsInput!): ConnectSettings! + enableDynamicRemoteAccess(input: EnableDynamicRemoteAccessInput!): Boolean! + connectSignIn(input: ConnectSignInInput!): Boolean! + connectSignOut: Boolean! + setupRemoteAccess(input: SetupRemoteAccessInput!): Boolean! + setAdditionalAllowedOrigins(input: AllowedOriginInput!): [String!]! + + """Creates a new notification record""" + createNotification(input: NotificationData!): Notification! + deleteNotification(id: String!, type: NotificationType!): NotificationOverview! + + """Deletes all archived notifications on server.""" + deleteArchivedNotifications: NotificationOverview! + + """Marks a notification as archived.""" + archiveNotification(id: String!): Notification! + archiveNotifications(ids: [String!]!): NotificationOverview! + archiveAll(importance: NotificationImportance): NotificationOverview! + + """Marks a notification as unread.""" + unreadNotification(id: String!): Notification! + unarchiveNotifications(ids: [String!]!): NotificationOverview! + unarchiveAll(importance: NotificationImportance): NotificationOverview! + + """Reads each notification to recompute & update the overview.""" + recalculateOverview: NotificationOverview! + + """Start a virtual machine""" + startVm(id: String!): Boolean! + + """Stop a virtual machine""" + stopVm(id: String!): Boolean! + + """Pause a virtual machine""" + pauseVm(id: String!): Boolean! + + """Resume a virtual machine""" + resumeVm(id: String!): Boolean! + + """Force stop a virtual machine""" + forceStopVm(id: String!): Boolean! + + """Reboot a virtual machine""" + rebootVm(id: String!): Boolean! + + """Reset a virtual machine""" + resetVm(id: String!): Boolean! +} + +input CreateApiKeyInput { + name: String! + description: String + roles: [Role!] + permissions: [AddPermissionInput!] + + """ + This will replace the existing key if one already exists with the same name, otherwise returns the existing key + """ + overwrite: Boolean +} + +input AddPermissionInput { + resource: Resource! + actions: [String!]! +} + +input AddRoleForApiKeyInput { + apiKeyId: ID! + role: Role! +} + +input RemoveRoleFromApiKeyInput { + apiKeyId: ID! + role: Role! +} + +input ArrayStateInput { + """Array state""" + desiredState: ArrayStateInputState! = STOP +} + +enum ArrayStateInputState { + START + STOP +} + +input ArrayDiskInput { + """Disk ID""" + id: ID! = "" + + """The slot for the disk""" + slot: Int +} + +input ApiSettingsInput { + """ + If true, the GraphQL sandbox will be enabled and available at /graphql. If false, the GraphQL sandbox will be disabled and only the production API will be available. + """ + sandbox: Boolean + + """A list of origins allowed to interact with the API""" + extraOrigins: [String!] + + """The type of WAN access to use for Remote Access""" + accessType: WAN_ACCESS_TYPE + + """The type of port forwarding to use for Remote Access""" + forwardType: WAN_FORWARD_TYPE + + """ + The port to use for Remote Access. Not required for UPNP forwardType. Required for STATIC forwardType. Ignored if accessType is DISABLED or forwardType is UPNP. + """ + port: Int + + """A list of Unique Unraid Account ID's""" + ssoUserIds: [String!] +} + +input EnableDynamicRemoteAccessInput { + """The URL for dynamic remote access""" + url: URL! + + """Whether to enable or disable dynamic remote access""" + enabled: Boolean! +} + +input ConnectSignInInput { + """The API key for authentication""" + apiKey: String! + + """The ID token for authentication""" + idToken: String + + """User information for the sign-in""" + userInfo: ConnectUserInfoInput + + """The access token for authentication""" + accessToken: String + + """The refresh token for authentication""" + refreshToken: String +} + +input ConnectUserInfoInput { + """The preferred username of the user""" + preferred_username: String! + + """The email address of the user""" + email: String! + + """The avatar URL of the user""" + avatar: String +} + +input SetupRemoteAccessInput { + """The type of WAN access to use for Remote Access""" + accessType: WAN_ACCESS_TYPE! + + """The type of port forwarding to use for Remote Access""" + forwardType: WAN_FORWARD_TYPE + + """ + The port to use for Remote Access. Not required for UPNP forwardType. Required for STATIC forwardType. Ignored if accessType is DISABLED or forwardType is UPNP. + """ + port: Int +} + +input AllowedOriginInput { + """A list of origins allowed to interact with the API""" + origins: [String!]! +} + +input NotificationData { + title: String! + subject: String! + description: String! + importance: NotificationImportance! + link: String +} + +type Subscription { + arraySubscription: UnraidArray! + displaySubscription: Display! + infoSubscription: Info! + logFileSubscription(path: String!): LogFileContent! + notificationAdded: Notification! + notificationsOverview: NotificationOverview! + ownerSubscription: Owner! + registrationSubscription: Registration! + serversSubscription: Server! +} \ No newline at end of file diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4624b1c0d..e7105a643 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -58,31 +58,31 @@ importers: version: 3.5.1 '@nestjs/apollo': specifier: ^13.0.3 - version: 13.0.4(@apollo/server@4.11.3(graphql@16.10.0))(@as-integrations/fastify@2.1.1(@apollo/server@4.11.3(graphql@16.10.0))(fastify@5.2.1))(@nestjs/common@11.0.12(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/core@11.0.12(@nestjs/common@11.0.12(reflect-metadata@0.1.14)(rxjs@7.8.2))(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/graphql@13.0.4(@nestjs/common@11.0.12(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/core@11.0.12(@nestjs/common@11.0.12(reflect-metadata@0.1.14)(rxjs@7.8.2))(reflect-metadata@0.1.14)(rxjs@7.8.2))(graphql@16.10.0)(reflect-metadata@0.1.14)(ts-morph@24.0.0))(graphql@16.10.0) + version: 13.0.4(@apollo/server@4.11.3(graphql@16.10.0))(@as-integrations/fastify@2.1.1(@apollo/server@4.11.3(graphql@16.10.0))(fastify@5.2.1))(@nestjs/common@11.0.12(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/core@11.0.12(@nestjs/common@11.0.12(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.1.14)(rxjs@7.8.2))(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/graphql@13.0.4(@nestjs/common@11.0.12(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/core@11.0.12(@nestjs/common@11.0.12(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.1.14)(rxjs@7.8.2))(reflect-metadata@0.1.14)(rxjs@7.8.2))(class-transformer@0.5.1)(class-validator@0.14.1)(graphql@16.10.0)(reflect-metadata@0.1.14)(ts-morph@24.0.0))(graphql@16.10.0) '@nestjs/common': specifier: ^11.0.11 - version: 11.0.12(reflect-metadata@0.1.14)(rxjs@7.8.2) + version: 11.0.12(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.1.14)(rxjs@7.8.2) '@nestjs/config': specifier: ^4.0.2 - version: 4.0.2(@nestjs/common@11.0.12(reflect-metadata@0.1.14)(rxjs@7.8.2))(rxjs@7.8.2) + version: 4.0.2(@nestjs/common@11.0.12(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.1.14)(rxjs@7.8.2))(rxjs@7.8.2) '@nestjs/core': specifier: ^11.0.11 - version: 11.0.12(@nestjs/common@11.0.12(reflect-metadata@0.1.14)(rxjs@7.8.2))(reflect-metadata@0.1.14)(rxjs@7.8.2) + version: 11.0.12(@nestjs/common@11.0.12(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.1.14)(rxjs@7.8.2))(reflect-metadata@0.1.14)(rxjs@7.8.2) '@nestjs/graphql': specifier: ^13.0.3 - version: 13.0.4(@nestjs/common@11.0.12(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/core@11.0.12(@nestjs/common@11.0.12(reflect-metadata@0.1.14)(rxjs@7.8.2))(reflect-metadata@0.1.14)(rxjs@7.8.2))(graphql@16.10.0)(reflect-metadata@0.1.14)(ts-morph@24.0.0) + version: 13.0.4(@nestjs/common@11.0.12(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/core@11.0.12(@nestjs/common@11.0.12(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.1.14)(rxjs@7.8.2))(reflect-metadata@0.1.14)(rxjs@7.8.2))(class-transformer@0.5.1)(class-validator@0.14.1)(graphql@16.10.0)(reflect-metadata@0.1.14)(ts-morph@24.0.0) '@nestjs/passport': specifier: ^11.0.0 - version: 11.0.5(@nestjs/common@11.0.12(reflect-metadata@0.1.14)(rxjs@7.8.2))(passport@0.7.0) + version: 11.0.5(@nestjs/common@11.0.12(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.1.14)(rxjs@7.8.2))(passport@0.7.0) '@nestjs/platform-fastify': specifier: ^11.0.11 - version: 11.0.12(@nestjs/common@11.0.12(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/core@11.0.12(@nestjs/common@11.0.12(reflect-metadata@0.1.14)(rxjs@7.8.2))(reflect-metadata@0.1.14)(rxjs@7.8.2)) + version: 11.0.12(@nestjs/common@11.0.12(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/core@11.0.12(@nestjs/common@11.0.12(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.1.14)(rxjs@7.8.2))(reflect-metadata@0.1.14)(rxjs@7.8.2)) '@nestjs/schedule': specifier: ^5.0.0 - version: 5.0.1(@nestjs/common@11.0.12(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/core@11.0.12(@nestjs/common@11.0.12(reflect-metadata@0.1.14)(rxjs@7.8.2))(reflect-metadata@0.1.14)(rxjs@7.8.2)) + version: 5.0.1(@nestjs/common@11.0.12(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/core@11.0.12(@nestjs/common@11.0.12(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.1.14)(rxjs@7.8.2))(reflect-metadata@0.1.14)(rxjs@7.8.2)) '@nestjs/throttler': specifier: ^6.2.1 - version: 6.4.0(@nestjs/common@11.0.12(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/core@11.0.12(@nestjs/common@11.0.12(reflect-metadata@0.1.14)(rxjs@7.8.2))(reflect-metadata@0.1.14)(rxjs@7.8.2))(reflect-metadata@0.1.14) + version: 6.4.0(@nestjs/common@11.0.12(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/core@11.0.12(@nestjs/common@11.0.12(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.1.14)(rxjs@7.8.2))(reflect-metadata@0.1.14)(rxjs@7.8.2))(reflect-metadata@0.1.14) '@reduxjs/toolkit': specifier: ^2.3.0 version: 2.6.1(react@19.0.0) @@ -119,6 +119,12 @@ importers: chokidar: specifier: ^4.0.1 version: 4.0.3 + class-transformer: + specifier: ^0.5.1 + version: 0.5.1 + class-validator: + specifier: ^0.14.1 + version: 0.14.1 cli-table: specifier: ^0.3.11 version: 0.3.11 @@ -214,13 +220,13 @@ importers: version: 4.2.0 nest-authz: specifier: ^2.14.0 - version: 2.15.0(@nestjs/common@11.0.12(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/core@11.0.12(@nestjs/common@11.0.12(reflect-metadata@0.1.14)(rxjs@7.8.2))(reflect-metadata@0.1.14)(rxjs@7.8.2))(reflect-metadata@0.1.14)(rxjs@7.8.2) + version: 2.15.0(@nestjs/common@11.0.12(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/core@11.0.12(@nestjs/common@11.0.12(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.1.14)(rxjs@7.8.2))(reflect-metadata@0.1.14)(rxjs@7.8.2))(reflect-metadata@0.1.14)(rxjs@7.8.2) nest-commander: specifier: ^3.15.0 - version: 3.17.0(@nestjs/common@11.0.12(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/core@11.0.12(@nestjs/common@11.0.12(reflect-metadata@0.1.14)(rxjs@7.8.2))(reflect-metadata@0.1.14)(rxjs@7.8.2))(@types/inquirer@8.2.10)(typescript@5.8.2) + version: 3.17.0(@nestjs/common@11.0.12(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/core@11.0.12(@nestjs/common@11.0.12(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.1.14)(rxjs@7.8.2))(reflect-metadata@0.1.14)(rxjs@7.8.2))(@types/inquirer@8.2.10)(typescript@5.8.2) nestjs-pino: specifier: ^4.1.0 - version: 4.4.0(@nestjs/common@11.0.12(reflect-metadata@0.1.14)(rxjs@7.8.2))(pino-http@10.4.0)(pino@9.6.0)(rxjs@7.8.2) + version: 4.4.0(@nestjs/common@11.0.12(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.1.14)(rxjs@7.8.2))(pino-http@10.4.0)(pino@9.6.0)(rxjs@7.8.2) node-cache: specifier: ^5.1.2 version: 5.1.2 @@ -320,7 +326,7 @@ importers: version: 4.4.1(@vue/compiler-sfc@3.5.13)(prettier@3.5.3) '@nestjs/testing': specifier: ^11.0.11 - version: 11.0.12(@nestjs/common@11.0.12(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/core@11.0.12(@nestjs/common@11.0.12(reflect-metadata@0.1.14)(rxjs@7.8.2))(reflect-metadata@0.1.14)(rxjs@7.8.2)) + version: 11.0.12(@nestjs/common@11.0.12(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/core@11.0.12(@nestjs/common@11.0.12(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.1.14)(rxjs@7.8.2))(reflect-metadata@0.1.14)(rxjs@7.8.2)) '@originjs/vite-plugin-commonjs': specifier: ^1.0.3 version: 1.0.3 @@ -470,19 +476,19 @@ importers: devDependencies: '@nestjs/common': specifier: ^11.0.11 - version: 11.0.12(reflect-metadata@0.1.14)(rxjs@7.8.2) + version: 11.0.12(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.1.14)(rxjs@7.8.2) '@nestjs/config': specifier: ^4.0.2 - version: 4.0.2(@nestjs/common@11.0.12(reflect-metadata@0.1.14)(rxjs@7.8.2))(rxjs@7.8.2) + version: 4.0.2(@nestjs/common@11.0.12(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.1.14)(rxjs@7.8.2))(rxjs@7.8.2) '@nestjs/core': specifier: ^11.0.11 - version: 11.0.12(@nestjs/common@11.0.12(reflect-metadata@0.1.14)(rxjs@7.8.2))(reflect-metadata@0.1.14)(rxjs@7.8.2) + version: 11.0.12(@nestjs/common@11.0.12(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.1.14)(rxjs@7.8.2))(reflect-metadata@0.1.14)(rxjs@7.8.2) '@nestjs/graphql': specifier: ^13.0.3 - version: 13.0.4(@nestjs/common@11.0.12(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/core@11.0.12(@nestjs/common@11.0.12(reflect-metadata@0.1.14)(rxjs@7.8.2))(reflect-metadata@0.1.14)(rxjs@7.8.2))(graphql@16.10.0)(reflect-metadata@0.1.14)(ts-morph@24.0.0) + version: 13.0.4(@nestjs/common@11.0.12(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/core@11.0.12(@nestjs/common@11.0.12(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.1.14)(rxjs@7.8.2))(reflect-metadata@0.1.14)(rxjs@7.8.2))(class-transformer@0.5.1)(class-validator@0.14.1)(graphql@16.10.0)(reflect-metadata@0.1.14)(ts-morph@24.0.0) nest-authz: specifier: ^2.14.0 - version: 2.15.0(@nestjs/common@11.0.12(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/core@11.0.12(@nestjs/common@11.0.12(reflect-metadata@0.1.14)(rxjs@7.8.2))(reflect-metadata@0.1.14)(rxjs@7.8.2))(reflect-metadata@0.1.14)(rxjs@7.8.2) + version: 2.15.0(@nestjs/common@11.0.12(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/core@11.0.12(@nestjs/common@11.0.12(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.1.14)(rxjs@7.8.2))(reflect-metadata@0.1.14)(rxjs@7.8.2))(reflect-metadata@0.1.14)(rxjs@7.8.2) typescript: specifier: ^5.8.2 version: 5.8.2 @@ -491,19 +497,19 @@ importers: devDependencies: '@nestjs/common': specifier: ^11.0.11 - version: 11.0.12(reflect-metadata@0.1.14)(rxjs@7.8.2) + version: 11.0.12(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.1.14)(rxjs@7.8.2) '@nestjs/config': specifier: ^4.0.2 - version: 4.0.2(@nestjs/common@11.0.12(reflect-metadata@0.1.14)(rxjs@7.8.2))(rxjs@7.8.2) + version: 4.0.2(@nestjs/common@11.0.12(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.1.14)(rxjs@7.8.2))(rxjs@7.8.2) '@nestjs/core': specifier: ^11.0.11 - version: 11.0.12(@nestjs/common@11.0.12(reflect-metadata@0.1.14)(rxjs@7.8.2))(reflect-metadata@0.1.14)(rxjs@7.8.2) + version: 11.0.12(@nestjs/common@11.0.12(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.1.14)(rxjs@7.8.2))(reflect-metadata@0.1.14)(rxjs@7.8.2) '@nestjs/graphql': specifier: ^13.0.3 - version: 13.0.4(@nestjs/common@11.0.12(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/core@11.0.12(@nestjs/common@11.0.12(reflect-metadata@0.1.14)(rxjs@7.8.2))(reflect-metadata@0.1.14)(rxjs@7.8.2))(graphql@16.10.0)(reflect-metadata@0.1.14)(ts-morph@24.0.0) + version: 13.0.4(@nestjs/common@11.0.12(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/core@11.0.12(@nestjs/common@11.0.12(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.1.14)(rxjs@7.8.2))(reflect-metadata@0.1.14)(rxjs@7.8.2))(class-transformer@0.5.1)(class-validator@0.14.1)(graphql@16.10.0)(reflect-metadata@0.1.14)(ts-morph@24.0.0) nest-authz: specifier: ^2.14.0 - version: 2.15.0(@nestjs/common@11.0.12(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/core@11.0.12(@nestjs/common@11.0.12(reflect-metadata@0.1.14)(rxjs@7.8.2))(reflect-metadata@0.1.14)(rxjs@7.8.2))(reflect-metadata@0.1.14)(rxjs@7.8.2) + version: 2.15.0(@nestjs/common@11.0.12(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/core@11.0.12(@nestjs/common@11.0.12(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.1.14)(rxjs@7.8.2))(reflect-metadata@0.1.14)(rxjs@7.8.2))(reflect-metadata@0.1.14)(rxjs@7.8.2) typescript: specifier: ^5.8.2 version: 5.8.2 @@ -3860,6 +3866,9 @@ packages: '@types/uuid@9.0.8': resolution: {integrity: sha512-jg+97EGIcY9AGHJJRaaPVgetKDsrTgbRjQ5Msgjh/DQKEFl0DtyRr/VCOyD1T2R1MNeWPK/u7JoGhlDZnKBAfA==} + '@types/validator@13.12.3': + resolution: {integrity: sha512-2ipwZ2NydGQJImne+FhNdhgRM37e9lCev99KnqkbFHd94Xn/mErARWI1RSLem1QA19ch5kOhzIZd7e8CA2FI8g==} + '@types/web-bluetooth@0.0.21': resolution: {integrity: sha512-oIQLCGWtcFZy2JW77j9k8nHzAOpqMHLQejDA48XXMWH6tjCQHz5RCFz1bzsmROyL6PUm+LLnUiI4BCn221inxA==} @@ -3976,6 +3985,7 @@ packages: '@unraid/libvirt@2.1.0': resolution: {integrity: sha512-yF/sAzYukM+VpDdAebf0z0O2mE5mGOEAW5lIafFkYoMiu60zNkNmr5IoA9hWCMmQkBOUCam8RZGs9YNwRjMtMQ==} engines: {node: '>=14'} + cpu: [x64, arm64] os: [linux, darwin] '@unraid/shared-callbacks@1.0.1': @@ -5099,6 +5109,12 @@ packages: citty@0.1.6: resolution: {integrity: sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ==} + class-transformer@0.5.1: + resolution: {integrity: sha512-SQa1Ws6hUbfC98vKGxZH3KFY0Y1lm5Zm0SY8XX9zbK7FJCyVEac3ATW0RIpwzW+oOfmHE5PMPufDG9hCfoEOMw==} + + class-validator@0.14.1: + resolution: {integrity: sha512-2VEG9JICxIqTpoK1eMzZqaV+u/EiwEJkMGzTrZf6sU/fwsnOITVgYJ8yojSy6CaXtO9V0Cc6ZQZ8h8m4UBuLwQ==} + class-variance-authority@0.7.1: resolution: {integrity: sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==} @@ -7984,6 +8000,9 @@ packages: resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} engines: {node: '>= 0.8.0'} + libphonenumber-js@1.12.6: + resolution: {integrity: sha512-PJiS4ETaUfCOFLpmtKzAbqZQjCCKVu2OhTV4SVNNE7c2nu/dACvtCqj4L0i/KWNnIgRv7yrILvBj5Lonv5Ncxw==} + light-my-request@6.3.0: resolution: {integrity: sha512-bWTAPJmeWQH5suJNYwG0f5cs0p6ho9e6f1Ppoxv5qMosY+s9Ir2+ZLvvHcgA7VTDop4zl/NCHhOVVqU+kd++Ow==} @@ -12898,10 +12917,10 @@ snapshots: - '@vue/composition-api' - vue - '@golevelup/nestjs-discovery@4.0.3(@nestjs/common@11.0.12(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/core@11.0.12(@nestjs/common@11.0.12(reflect-metadata@0.1.14)(rxjs@7.8.2))(reflect-metadata@0.1.14)(rxjs@7.8.2))': + '@golevelup/nestjs-discovery@4.0.3(@nestjs/common@11.0.12(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/core@11.0.12(@nestjs/common@11.0.12(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.1.14)(rxjs@7.8.2))(reflect-metadata@0.1.14)(rxjs@7.8.2))': dependencies: - '@nestjs/common': 11.0.12(reflect-metadata@0.1.14)(rxjs@7.8.2) - '@nestjs/core': 11.0.12(@nestjs/common@11.0.12(reflect-metadata@0.1.14)(rxjs@7.8.2))(reflect-metadata@0.1.14)(rxjs@7.8.2) + '@nestjs/common': 11.0.12(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.1.14)(rxjs@7.8.2) + '@nestjs/core': 11.0.12(@nestjs/common@11.0.12(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.1.14)(rxjs@7.8.2))(reflect-metadata@0.1.14)(rxjs@7.8.2) lodash: 4.17.21 '@graphql-codegen/add@3.2.3(graphql@16.10.0)': @@ -13772,13 +13791,13 @@ snapshots: '@tybys/wasm-util': 0.9.0 optional: true - '@nestjs/apollo@13.0.4(@apollo/server@4.11.3(graphql@16.10.0))(@as-integrations/fastify@2.1.1(@apollo/server@4.11.3(graphql@16.10.0))(fastify@5.2.1))(@nestjs/common@11.0.12(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/core@11.0.12(@nestjs/common@11.0.12(reflect-metadata@0.1.14)(rxjs@7.8.2))(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/graphql@13.0.4(@nestjs/common@11.0.12(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/core@11.0.12(@nestjs/common@11.0.12(reflect-metadata@0.1.14)(rxjs@7.8.2))(reflect-metadata@0.1.14)(rxjs@7.8.2))(graphql@16.10.0)(reflect-metadata@0.1.14)(ts-morph@24.0.0))(graphql@16.10.0)': + '@nestjs/apollo@13.0.4(@apollo/server@4.11.3(graphql@16.10.0))(@as-integrations/fastify@2.1.1(@apollo/server@4.11.3(graphql@16.10.0))(fastify@5.2.1))(@nestjs/common@11.0.12(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/core@11.0.12(@nestjs/common@11.0.12(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.1.14)(rxjs@7.8.2))(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/graphql@13.0.4(@nestjs/common@11.0.12(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/core@11.0.12(@nestjs/common@11.0.12(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.1.14)(rxjs@7.8.2))(reflect-metadata@0.1.14)(rxjs@7.8.2))(class-transformer@0.5.1)(class-validator@0.14.1)(graphql@16.10.0)(reflect-metadata@0.1.14)(ts-morph@24.0.0))(graphql@16.10.0)': dependencies: '@apollo/server': 4.11.3(graphql@16.10.0) '@apollo/server-plugin-landing-page-graphql-playground': 4.0.1(@apollo/server@4.11.3(graphql@16.10.0)) - '@nestjs/common': 11.0.12(reflect-metadata@0.1.14)(rxjs@7.8.2) - '@nestjs/core': 11.0.12(@nestjs/common@11.0.12(reflect-metadata@0.1.14)(rxjs@7.8.2))(reflect-metadata@0.1.14)(rxjs@7.8.2) - '@nestjs/graphql': 13.0.4(@nestjs/common@11.0.12(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/core@11.0.12(@nestjs/common@11.0.12(reflect-metadata@0.1.14)(rxjs@7.8.2))(reflect-metadata@0.1.14)(rxjs@7.8.2))(graphql@16.10.0)(reflect-metadata@0.1.14)(ts-morph@24.0.0) + '@nestjs/common': 11.0.12(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.1.14)(rxjs@7.8.2) + '@nestjs/core': 11.0.12(@nestjs/common@11.0.12(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.1.14)(rxjs@7.8.2))(reflect-metadata@0.1.14)(rxjs@7.8.2) + '@nestjs/graphql': 13.0.4(@nestjs/common@11.0.12(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/core@11.0.12(@nestjs/common@11.0.12(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.1.14)(rxjs@7.8.2))(reflect-metadata@0.1.14)(rxjs@7.8.2))(class-transformer@0.5.1)(class-validator@0.14.1)(graphql@16.10.0)(reflect-metadata@0.1.14)(ts-morph@24.0.0) graphql: 16.10.0 iterall: 1.3.0 lodash.omit: 4.5.0 @@ -13786,25 +13805,28 @@ snapshots: optionalDependencies: '@as-integrations/fastify': 2.1.1(@apollo/server@4.11.3(graphql@16.10.0))(fastify@5.2.1) - '@nestjs/common@11.0.12(reflect-metadata@0.1.14)(rxjs@7.8.2)': + '@nestjs/common@11.0.12(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.1.14)(rxjs@7.8.2)': dependencies: iterare: 1.2.1 reflect-metadata: 0.1.14 rxjs: 7.8.2 tslib: 2.8.1 uid: 2.0.2 + optionalDependencies: + class-transformer: 0.5.1 + class-validator: 0.14.1 - '@nestjs/config@4.0.2(@nestjs/common@11.0.12(reflect-metadata@0.1.14)(rxjs@7.8.2))(rxjs@7.8.2)': + '@nestjs/config@4.0.2(@nestjs/common@11.0.12(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.1.14)(rxjs@7.8.2))(rxjs@7.8.2)': dependencies: - '@nestjs/common': 11.0.12(reflect-metadata@0.1.14)(rxjs@7.8.2) + '@nestjs/common': 11.0.12(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.1.14)(rxjs@7.8.2) dotenv: 16.4.7 dotenv-expand: 12.0.1 lodash: 4.17.21 rxjs: 7.8.2 - '@nestjs/core@11.0.12(@nestjs/common@11.0.12(reflect-metadata@0.1.14)(rxjs@7.8.2))(reflect-metadata@0.1.14)(rxjs@7.8.2)': + '@nestjs/core@11.0.12(@nestjs/common@11.0.12(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.1.14)(rxjs@7.8.2))(reflect-metadata@0.1.14)(rxjs@7.8.2)': dependencies: - '@nestjs/common': 11.0.12(reflect-metadata@0.1.14)(rxjs@7.8.2) + '@nestjs/common': 11.0.12(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.1.14)(rxjs@7.8.2) '@nuxt/opencollective': 0.4.1 fast-safe-stringify: 2.1.1 iterare: 1.2.1 @@ -13814,14 +13836,14 @@ snapshots: tslib: 2.8.1 uid: 2.0.2 - '@nestjs/graphql@13.0.4(@nestjs/common@11.0.12(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/core@11.0.12(@nestjs/common@11.0.12(reflect-metadata@0.1.14)(rxjs@7.8.2))(reflect-metadata@0.1.14)(rxjs@7.8.2))(graphql@16.10.0)(reflect-metadata@0.1.14)(ts-morph@24.0.0)': + '@nestjs/graphql@13.0.4(@nestjs/common@11.0.12(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/core@11.0.12(@nestjs/common@11.0.12(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.1.14)(rxjs@7.8.2))(reflect-metadata@0.1.14)(rxjs@7.8.2))(class-transformer@0.5.1)(class-validator@0.14.1)(graphql@16.10.0)(reflect-metadata@0.1.14)(ts-morph@24.0.0)': dependencies: '@graphql-tools/merge': 9.0.24(graphql@16.10.0) '@graphql-tools/schema': 10.0.23(graphql@16.10.0) '@graphql-tools/utils': 10.8.6(graphql@16.10.0) - '@nestjs/common': 11.0.12(reflect-metadata@0.1.14)(rxjs@7.8.2) - '@nestjs/core': 11.0.12(@nestjs/common@11.0.12(reflect-metadata@0.1.14)(rxjs@7.8.2))(reflect-metadata@0.1.14)(rxjs@7.8.2) - '@nestjs/mapped-types': 2.1.0(@nestjs/common@11.0.12(reflect-metadata@0.1.14)(rxjs@7.8.2))(reflect-metadata@0.1.14) + '@nestjs/common': 11.0.12(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.1.14)(rxjs@7.8.2) + '@nestjs/core': 11.0.12(@nestjs/common@11.0.12(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.1.14)(rxjs@7.8.2))(reflect-metadata@0.1.14)(rxjs@7.8.2) + '@nestjs/mapped-types': 2.1.0(@nestjs/common@11.0.12(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.1.14)(rxjs@7.8.2))(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.1.14) chokidar: 4.0.3 fast-glob: 3.3.3 graphql: 16.10.0 @@ -13834,6 +13856,8 @@ snapshots: tslib: 2.8.1 ws: 8.18.1 optionalDependencies: + class-transformer: 0.5.1 + class-validator: 0.14.1 ts-morph: 24.0.0 transitivePeerDependencies: - '@fastify/websocket' @@ -13841,45 +13865,48 @@ snapshots: - uWebSockets.js - utf-8-validate - '@nestjs/mapped-types@2.1.0(@nestjs/common@11.0.12(reflect-metadata@0.1.14)(rxjs@7.8.2))(reflect-metadata@0.1.14)': + '@nestjs/mapped-types@2.1.0(@nestjs/common@11.0.12(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.1.14)(rxjs@7.8.2))(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.1.14)': dependencies: - '@nestjs/common': 11.0.12(reflect-metadata@0.1.14)(rxjs@7.8.2) + '@nestjs/common': 11.0.12(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.1.14)(rxjs@7.8.2) reflect-metadata: 0.1.14 + optionalDependencies: + class-transformer: 0.5.1 + class-validator: 0.14.1 - '@nestjs/passport@11.0.5(@nestjs/common@11.0.12(reflect-metadata@0.1.14)(rxjs@7.8.2))(passport@0.7.0)': + '@nestjs/passport@11.0.5(@nestjs/common@11.0.12(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.1.14)(rxjs@7.8.2))(passport@0.7.0)': dependencies: - '@nestjs/common': 11.0.12(reflect-metadata@0.1.14)(rxjs@7.8.2) + '@nestjs/common': 11.0.12(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.1.14)(rxjs@7.8.2) passport: 0.7.0 - '@nestjs/platform-fastify@11.0.12(@nestjs/common@11.0.12(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/core@11.0.12(@nestjs/common@11.0.12(reflect-metadata@0.1.14)(rxjs@7.8.2))(reflect-metadata@0.1.14)(rxjs@7.8.2))': + '@nestjs/platform-fastify@11.0.12(@nestjs/common@11.0.12(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/core@11.0.12(@nestjs/common@11.0.12(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.1.14)(rxjs@7.8.2))(reflect-metadata@0.1.14)(rxjs@7.8.2))': dependencies: '@fastify/cors': 11.0.0 '@fastify/formbody': 8.0.2 '@fastify/middie': 9.0.3 - '@nestjs/common': 11.0.12(reflect-metadata@0.1.14)(rxjs@7.8.2) - '@nestjs/core': 11.0.12(@nestjs/common@11.0.12(reflect-metadata@0.1.14)(rxjs@7.8.2))(reflect-metadata@0.1.14)(rxjs@7.8.2) + '@nestjs/common': 11.0.12(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.1.14)(rxjs@7.8.2) + '@nestjs/core': 11.0.12(@nestjs/common@11.0.12(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.1.14)(rxjs@7.8.2))(reflect-metadata@0.1.14)(rxjs@7.8.2) fast-querystring: 1.1.2 fastify: 5.2.1 light-my-request: 6.6.0 path-to-regexp: 8.2.0 tslib: 2.8.1 - '@nestjs/schedule@5.0.1(@nestjs/common@11.0.12(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/core@11.0.12(@nestjs/common@11.0.12(reflect-metadata@0.1.14)(rxjs@7.8.2))(reflect-metadata@0.1.14)(rxjs@7.8.2))': + '@nestjs/schedule@5.0.1(@nestjs/common@11.0.12(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/core@11.0.12(@nestjs/common@11.0.12(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.1.14)(rxjs@7.8.2))(reflect-metadata@0.1.14)(rxjs@7.8.2))': dependencies: - '@nestjs/common': 11.0.12(reflect-metadata@0.1.14)(rxjs@7.8.2) - '@nestjs/core': 11.0.12(@nestjs/common@11.0.12(reflect-metadata@0.1.14)(rxjs@7.8.2))(reflect-metadata@0.1.14)(rxjs@7.8.2) + '@nestjs/common': 11.0.12(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.1.14)(rxjs@7.8.2) + '@nestjs/core': 11.0.12(@nestjs/common@11.0.12(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.1.14)(rxjs@7.8.2))(reflect-metadata@0.1.14)(rxjs@7.8.2) cron: 3.5.0 - '@nestjs/testing@11.0.12(@nestjs/common@11.0.12(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/core@11.0.12(@nestjs/common@11.0.12(reflect-metadata@0.1.14)(rxjs@7.8.2))(reflect-metadata@0.1.14)(rxjs@7.8.2))': + '@nestjs/testing@11.0.12(@nestjs/common@11.0.12(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/core@11.0.12(@nestjs/common@11.0.12(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.1.14)(rxjs@7.8.2))(reflect-metadata@0.1.14)(rxjs@7.8.2))': dependencies: - '@nestjs/common': 11.0.12(reflect-metadata@0.1.14)(rxjs@7.8.2) - '@nestjs/core': 11.0.12(@nestjs/common@11.0.12(reflect-metadata@0.1.14)(rxjs@7.8.2))(reflect-metadata@0.1.14)(rxjs@7.8.2) + '@nestjs/common': 11.0.12(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.1.14)(rxjs@7.8.2) + '@nestjs/core': 11.0.12(@nestjs/common@11.0.12(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.1.14)(rxjs@7.8.2))(reflect-metadata@0.1.14)(rxjs@7.8.2) tslib: 2.8.1 - '@nestjs/throttler@6.4.0(@nestjs/common@11.0.12(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/core@11.0.12(@nestjs/common@11.0.12(reflect-metadata@0.1.14)(rxjs@7.8.2))(reflect-metadata@0.1.14)(rxjs@7.8.2))(reflect-metadata@0.1.14)': + '@nestjs/throttler@6.4.0(@nestjs/common@11.0.12(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/core@11.0.12(@nestjs/common@11.0.12(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.1.14)(rxjs@7.8.2))(reflect-metadata@0.1.14)(rxjs@7.8.2))(reflect-metadata@0.1.14)': dependencies: - '@nestjs/common': 11.0.12(reflect-metadata@0.1.14)(rxjs@7.8.2) - '@nestjs/core': 11.0.12(@nestjs/common@11.0.12(reflect-metadata@0.1.14)(rxjs@7.8.2))(reflect-metadata@0.1.14)(rxjs@7.8.2) + '@nestjs/common': 11.0.12(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.1.14)(rxjs@7.8.2) + '@nestjs/core': 11.0.12(@nestjs/common@11.0.12(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.1.14)(rxjs@7.8.2))(reflect-metadata@0.1.14)(rxjs@7.8.2) reflect-metadata: 0.1.14 '@netlify/functions@3.0.4': @@ -15356,6 +15383,8 @@ snapshots: '@types/uuid@9.0.8': {} + '@types/validator@13.12.3': {} + '@types/web-bluetooth@0.0.21': {} '@types/ws@8.18.0': @@ -16893,6 +16922,14 @@ snapshots: dependencies: consola: 3.4.2 + class-transformer@0.5.1: {} + + class-validator@0.14.1: + dependencies: + '@types/validator': 13.12.3 + libphonenumber-js: 1.12.6 + validator: 13.12.0 + class-variance-authority@0.7.1: dependencies: clsx: 2.1.1 @@ -20128,6 +20165,8 @@ snapshots: prelude-ls: 1.2.1 type-check: 0.4.0 + libphonenumber-js@1.12.6: {} + light-my-request@6.3.0: dependencies: cookie: 1.0.2 @@ -20576,20 +20615,20 @@ snapshots: neo-async@2.6.2: {} - nest-authz@2.15.0(@nestjs/common@11.0.12(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/core@11.0.12(@nestjs/common@11.0.12(reflect-metadata@0.1.14)(rxjs@7.8.2))(reflect-metadata@0.1.14)(rxjs@7.8.2))(reflect-metadata@0.1.14)(rxjs@7.8.2): + nest-authz@2.15.0(@nestjs/common@11.0.12(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/core@11.0.12(@nestjs/common@11.0.12(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.1.14)(rxjs@7.8.2))(reflect-metadata@0.1.14)(rxjs@7.8.2))(reflect-metadata@0.1.14)(rxjs@7.8.2): dependencies: - '@nestjs/common': 11.0.12(reflect-metadata@0.1.14)(rxjs@7.8.2) - '@nestjs/core': 11.0.12(@nestjs/common@11.0.12(reflect-metadata@0.1.14)(rxjs@7.8.2))(reflect-metadata@0.1.14)(rxjs@7.8.2) + '@nestjs/common': 11.0.12(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.1.14)(rxjs@7.8.2) + '@nestjs/core': 11.0.12(@nestjs/common@11.0.12(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.1.14)(rxjs@7.8.2))(reflect-metadata@0.1.14)(rxjs@7.8.2) casbin: 5.38.0 reflect-metadata: 0.1.14 rxjs: 7.8.2 - nest-commander@3.17.0(@nestjs/common@11.0.12(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/core@11.0.12(@nestjs/common@11.0.12(reflect-metadata@0.1.14)(rxjs@7.8.2))(reflect-metadata@0.1.14)(rxjs@7.8.2))(@types/inquirer@8.2.10)(typescript@5.8.2): + nest-commander@3.17.0(@nestjs/common@11.0.12(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/core@11.0.12(@nestjs/common@11.0.12(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.1.14)(rxjs@7.8.2))(reflect-metadata@0.1.14)(rxjs@7.8.2))(@types/inquirer@8.2.10)(typescript@5.8.2): dependencies: '@fig/complete-commander': 3.2.0(commander@11.1.0) - '@golevelup/nestjs-discovery': 4.0.3(@nestjs/common@11.0.12(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/core@11.0.12(@nestjs/common@11.0.12(reflect-metadata@0.1.14)(rxjs@7.8.2))(reflect-metadata@0.1.14)(rxjs@7.8.2)) - '@nestjs/common': 11.0.12(reflect-metadata@0.1.14)(rxjs@7.8.2) - '@nestjs/core': 11.0.12(@nestjs/common@11.0.12(reflect-metadata@0.1.14)(rxjs@7.8.2))(reflect-metadata@0.1.14)(rxjs@7.8.2) + '@golevelup/nestjs-discovery': 4.0.3(@nestjs/common@11.0.12(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/core@11.0.12(@nestjs/common@11.0.12(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.1.14)(rxjs@7.8.2))(reflect-metadata@0.1.14)(rxjs@7.8.2)) + '@nestjs/common': 11.0.12(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.1.14)(rxjs@7.8.2) + '@nestjs/core': 11.0.12(@nestjs/common@11.0.12(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.1.14)(rxjs@7.8.2))(reflect-metadata@0.1.14)(rxjs@7.8.2) '@types/inquirer': 8.2.10 commander: 11.1.0 cosmiconfig: 8.3.6(typescript@5.8.2) @@ -20597,9 +20636,9 @@ snapshots: transitivePeerDependencies: - typescript - nestjs-pino@4.4.0(@nestjs/common@11.0.12(reflect-metadata@0.1.14)(rxjs@7.8.2))(pino-http@10.4.0)(pino@9.6.0)(rxjs@7.8.2): + nestjs-pino@4.4.0(@nestjs/common@11.0.12(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.1.14)(rxjs@7.8.2))(pino-http@10.4.0)(pino@9.6.0)(rxjs@7.8.2): dependencies: - '@nestjs/common': 11.0.12(reflect-metadata@0.1.14)(rxjs@7.8.2) + '@nestjs/common': 11.0.12(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.1.14)(rxjs@7.8.2) pino: 9.6.0 pino-http: 10.4.0 rxjs: 7.8.2 diff --git a/unraid-ui/eslint.config.ts b/unraid-ui/eslint.config.ts index 63f1bcf8a..fba74a205 100644 --- a/unraid-ui/eslint.config.ts +++ b/unraid-ui/eslint.config.ts @@ -1,4 +1,5 @@ import eslint from '@eslint/js'; +// @ts-ignore-error No Declaration For This Plugin import importPlugin from 'eslint-plugin-import'; import noRelativeImportPaths from 'eslint-plugin-no-relative-import-paths'; import prettier from 'eslint-plugin-prettier'; @@ -14,8 +15,8 @@ export default tseslint.config(eslint.configs.recommended, ...tseslint.configs.r ecmaVersion: 'latest', sourceType: 'module', ecmaFeatures: { - jsx: true - } + jsx: true, + }, }, globals: { browser: true, @@ -23,7 +24,7 @@ export default tseslint.config(eslint.configs.recommended, ...tseslint.configs.r document: true, es2022: true, HTMLElement: true, - } + }, }, plugins: { 'no-relative-import-paths': noRelativeImportPaths, @@ -72,10 +73,13 @@ export default tseslint.config(eslint.configs.recommended, ...tseslint.configs.r }, ], 'vue/no-undef-components': ['error'], - 'vue/no-unused-properties': ['error', { - groups: ['props'], - deepData: false, - }], + 'vue/no-unused-properties': [ + 'error', + { + groups: ['props'], + deepData: false, + }, + ], // Allow empty object types and any types in Vue component definitions '@typescript-eslint/no-explicit-any': [ 'error', diff --git a/web/codegen.ts b/web/codegen.ts index 1c2e8c660..8246e472b 100644 --- a/web/codegen.ts +++ b/web/codegen.ts @@ -1,12 +1,17 @@ import type { CodegenConfig } from '@graphql-codegen/cli'; + + + + const config: CodegenConfig = { overwrite: true, documents: ['./**/**/*.ts'], ignoreNoDocuments: false, config: { namingConvention: { - typeNames: './fix-array-type.js', + enumValues: 'change-case-all#upperCase', + transformUnderscore: true, }, scalars: { DateTime: 'string', @@ -35,4 +40,4 @@ const config: CodegenConfig = { }, }; -export default config; +export default config; \ No newline at end of file diff --git a/web/components/ConnectSettings/RemoteAccess.vue b/web/components/ConnectSettings/RemoteAccess.vue index 7bd0937ba..948cfba62 100644 --- a/web/components/ConnectSettings/RemoteAccess.vue +++ b/web/components/ConnectSettings/RemoteAccess.vue @@ -1,17 +1,17 @@ @@ -35,20 +35,20 @@ watch(accessType, (newVal) => {

Setup Remote Access

- + -