From 586653ccc11f394630e29fdf3fa9250c0892669b Mon Sep 17 00:00:00 2001 From: Eli Bosley Date: Tue, 29 Apr 2025 12:22:18 -0400 Subject: [PATCH] chore: add a prefix scalar instead of prefix plugin (#1361) ## Summary by CodeRabbit - **New Features** - Introduced a new `PrefixedID` scalar type for all GraphQL IDs, ensuring unique identifiers across multiple servers by prefixing IDs with a server identifier. - Added detailed documentation for the `PrefixedID` scalar in the API schema. - Grouped VM and parity check mutations under dedicated fields for better organization. - Added new scalar `Port` and input type `AccessUrlInput` for improved type safety. - **Refactor** - Replaced all usages of standard `ID` or `String` with `PrefixedID` for IDs in queries, mutations, and models. - Consolidated ID handling by extending a common `Node` base class across models, removing redundant `id` declarations. - Updated GraphQL argument types and resolver signatures to explicitly use `PrefixedID`. - Updated GraphQL code generation to map `PrefixedID` to TypeScript `string`. - **Bug Fixes** - Enhanced test assertions to verify API key creation timestamps are strings. - Fixed internal property naming for registration ID. - **Chores** - Removed legacy ID prefix Apollo Server plugin in favor of the new scalar approach. - Cleaned up imports and unused fields related to ID handling. --------- Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- api/generated-schema.graphql | 166 ++++---- api/src/graphql/schema/utils.ts | 2 + .../unraid-api/auth/api-key.service.spec.ts | 2 + api/src/unraid-api/graph/graph.module.ts | 8 +- api/src/unraid-api/graph/id-prefix-plugin.ts | 67 --- .../graph/resolvers/api-key/api-key.model.ts | 20 +- .../resolvers/api-key/api-key.resolver.ts | 6 +- .../graph/resolvers/array/array.model.ts | 22 +- .../array/array.mutations.resolver.ts | 11 +- .../unraid-api/graph/resolvers/base.model.ts | 10 +- .../graph/resolvers/config/config.model.ts | 7 +- .../connect/connect-settings.resolver.ts | 5 +- .../graph/resolvers/connect/connect.model.ts | 21 +- .../graph/resolvers/disks/disks.model.ts | 11 +- .../graph/resolvers/disks/disks.resolver.ts | 3 +- .../graph/resolvers/docker/docker.model.ts | 19 +- .../docker/docker.mutations.resolver.ts | 5 +- .../graph/resolvers/flash/flash.model.ts | 7 +- .../graph/resolvers/flash/flash.resolver.ts | 1 - .../graph/resolvers/info/info.model.ts | 135 ++---- .../notifications/notifications.model.ts | 23 +- .../notifications/notifications.resolver.ts | 11 +- .../registration/registration.model.ts | 11 +- .../registration/registration.resolver.ts | 2 +- .../graph/resolvers/servers/server.model.ts | 11 +- .../graph/resolvers/vars/vars.model.ts | 5 +- .../graph/resolvers/vms/vms.model.ts | 33 +- .../resolvers/vms/vms.mutations.resolver.ts | 15 +- .../graph/resolvers/vms/vms.service.spec.ts | 6 +- .../graph/resolvers/vms/vms.service.ts | 3 +- .../graph/scalars}/graphql-type-long.ts | 0 .../graph/scalars/graphql-type-prefixed-id.ts | 107 +++++ .../graph/services/service.model.ts | 5 +- api/src/unraid-api/graph/user/user.model.ts | 11 +- web/codegen.ts | 1 + .../graphql/notification.query.ts | 4 +- web/composables/gql/gql.ts | 12 +- web/composables/gql/graphql.ts | 397 +++++++++++------- 38 files changed, 617 insertions(+), 568 deletions(-) delete mode 100644 api/src/unraid-api/graph/id-prefix-plugin.ts rename api/src/{graphql/resolvers => unraid-api/graph/scalars}/graphql-type-long.ts (100%) create mode 100644 api/src/unraid-api/graph/scalars/graphql-type-prefixed-id.ts diff --git a/api/generated-schema.graphql b/api/generated-schema.graphql index 4b750740a..111482fca 100644 --- a/api/generated-schema.graphql +++ b/api/generated-schema.graphql @@ -74,8 +74,7 @@ type ArrayCapacity { } type ArrayDisk implements Node { - """Disk identifier, only set for present disks on the system""" - id: ID! + id: PrefixedID! """ 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. @@ -143,7 +142,7 @@ type ArrayDisk implements Node { } interface Node { - id: ID! + id: PrefixedID! } """The `Long` scalar type represents 52-bit integers""" @@ -181,7 +180,7 @@ enum ArrayDiskFsColor { } type UnraidArray implements Node { - id: ID! + id: PrefixedID! """Current array state""" state: ArrayState! @@ -217,7 +216,7 @@ enum ArrayState { } type Share implements Node { - id: ID! + id: PrefixedID! """Display name""" name: String @@ -348,8 +347,7 @@ type ConnectSettingsValues { } type ConnectSettings implements Node { - """The unique identifier for the Connect settings""" - id: ID! + id: PrefixedID! """The data schema for the Connect settings""" dataSchema: JSON! @@ -367,8 +365,7 @@ The `JSON` scalar type represents JSON values as specified by [ECMA-404](http:// 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! + id: PrefixedID! """The status of dynamic remote access""" dynamicRemoteAccess: DynamicRemoteAccessStatus! @@ -378,18 +375,19 @@ type Connect implements Node { } type Network implements Node { - id: ID! + id: PrefixedID! accessUrls: [AccessUrl!] } -type ProfileModel { - userId: ID +type ProfileModel implements Node { + id: PrefixedID! username: String! url: String! avatar: String! } -type Server { +type Server implements Node { + id: PrefixedID! owner: ProfileModel! guid: String! apikey: String! @@ -428,9 +426,8 @@ enum DiskFsType { NTFS } -type Disk { - """The unique identifier of the disk""" - id: String! +type Disk implements Node { + id: PrefixedID! """The device path of the disk (e.g. /dev/sdb)""" device: String! @@ -509,8 +506,8 @@ type KeyFile { contents: String } -type Registration { - guid: ID +type Registration implements Node { + id: PrefixedID! type: registrationType keyFile: KeyFile state: RegistrationState @@ -559,7 +556,7 @@ enum RegistrationState { } type Vars implements Node { - id: ID! + id: PrefixedID! """Unraid version""" version: String @@ -786,8 +783,8 @@ enum Resource { WELCOME } -type ApiKey { - id: ID! +type ApiKey implements Node { + id: PrefixedID! name: String! description: String roles: [Role!]! @@ -802,8 +799,8 @@ enum Role { GUEST } -type ApiKeyWithSecret { - id: ID! +type ApiKeyWithSecret implements Node { + id: PrefixedID! name: String! description: String roles: [Role!]! @@ -825,13 +822,13 @@ type ArrayMutations { removeDiskFromArray(input: ArrayDiskInput!): UnraidArray! """Mount a disk in the array""" - mountArrayDisk(id: String!): ArrayDisk! + mountArrayDisk(id: PrefixedID!): ArrayDisk! """Unmount a disk from the array""" - unmountArrayDisk(id: String!): ArrayDisk! + unmountArrayDisk(id: PrefixedID!): ArrayDisk! """Clear statistics for a disk in the array""" - clearArrayDiskStatistics(id: String!): Boolean! + clearArrayDiskStatistics(id: PrefixedID!): Boolean! } input ArrayStateInput { @@ -846,7 +843,7 @@ enum ArrayStateInputState { input ArrayDiskInput { """Disk ID""" - id: ID! + id: PrefixedID! """The slot for the disk""" slot: Int @@ -854,33 +851,33 @@ input ArrayDiskInput { type DockerMutations { """Start a container""" - start(id: String!): DockerContainer! + start(id: PrefixedID!): DockerContainer! """Stop a container""" - stop(id: String!): DockerContainer! + stop(id: PrefixedID!): DockerContainer! } type VmMutations { """Start a virtual machine""" - start(id: String!): Boolean! + start(id: PrefixedID!): Boolean! """Stop a virtual machine""" - stop(id: String!): Boolean! + stop(id: PrefixedID!): Boolean! """Pause a virtual machine""" - pause(id: String!): Boolean! + pause(id: PrefixedID!): Boolean! """Resume a virtual machine""" - resume(id: String!): Boolean! + resume(id: PrefixedID!): Boolean! """Force stop a virtual machine""" - forceStop(id: String!): Boolean! + forceStop(id: PrefixedID!): Boolean! """Reboot a virtual machine""" - reboot(id: String!): Boolean! + reboot(id: PrefixedID!): Boolean! """Reset a virtual machine""" - reset(id: String!): Boolean! + reset(id: PrefixedID!): Boolean! } """ @@ -935,13 +932,13 @@ A date-time string at UTC, such as 2019-12-03T09:54:33Z, compliant with the date scalar DateTime type Config implements Node { - id: ID! + id: PrefixedID! valid: Boolean error: String } type InfoApps implements Node { - id: ID! + id: PrefixedID! """How many docker containers are installed""" installed: Int! @@ -951,7 +948,7 @@ type InfoApps implements Node { } type Baseboard implements Node { - id: ID! + id: PrefixedID! manufacturer: String! model: String version: String @@ -960,7 +957,7 @@ type Baseboard implements Node { } type InfoCpu implements Node { - id: ID! + id: PrefixedID! manufacturer: String! brand: String! vendor: String! @@ -981,7 +978,7 @@ type InfoCpu implements Node { } type Gpu implements Node { - id: ID! + id: PrefixedID! type: String! typeid: String! vendorname: String! @@ -991,7 +988,7 @@ type Gpu implements Node { } type Pci implements Node { - id: ID! + id: PrefixedID! type: String typeid: String vendorname: String @@ -1002,20 +999,20 @@ type Pci implements Node { class: String } -type Usb { - id: ID! +type Usb implements Node { + id: PrefixedID! name: String } type Devices implements Node { - id: ID! + id: PrefixedID! gpu: [Gpu!]! pci: [Pci!]! usb: [Usb!]! } type Case implements Node { - id: ID! + id: PrefixedID! icon: String url: String error: String @@ -1023,7 +1020,7 @@ type Case implements Node { } type Display implements Node { - id: ID! + id: PrefixedID! case: Case date: String number: String @@ -1057,7 +1054,8 @@ enum Temperature { F } -type MemoryLayout { +type MemoryLayout implements Node { + id: PrefixedID! size: Int! bank: String type: String @@ -1072,7 +1070,7 @@ type MemoryLayout { } type InfoMemory implements Node { - id: ID! + id: PrefixedID! max: Int! total: Int! free: Int! @@ -1087,7 +1085,7 @@ type InfoMemory implements Node { } type Os implements Node { - id: ID! + id: PrefixedID! platform: String distro: String release: String @@ -1103,7 +1101,7 @@ type Os implements Node { } type System implements Node { - id: ID! + id: PrefixedID! manufacturer: String model: String version: String @@ -1113,7 +1111,7 @@ type System implements Node { } type Versions implements Node { - id: ID! + id: PrefixedID! kernel: String openssl: String systemOpenssl: String @@ -1143,7 +1141,7 @@ type Versions implements Node { } type Info implements Node { - id: ID! + id: PrefixedID! """Count of docker containers""" apps: InfoApps! @@ -1153,7 +1151,7 @@ type Info implements Node { display: Display! """Machine ID""" - machineId: ID + machineId: PrefixedID memory: InfoMemory! os: Os! system: System! @@ -1182,8 +1180,8 @@ type ContainerHostConfig { networkMode: String! } -type DockerContainer { - id: ID! +type DockerContainer implements Node { + id: PrefixedID! names: [String!]! image: String! imageId: String! @@ -1212,9 +1210,9 @@ enum ContainerState { EXITED } -type DockerNetwork { +type DockerNetwork implements Node { + id: PrefixedID! name: String! - id: ID! created: String! scope: String! driver: String! @@ -1231,13 +1229,13 @@ type DockerNetwork { } type Docker implements Node { - id: ID! + id: PrefixedID! containers(skipCache: Boolean! = false): [DockerContainer!]! networks(skipCache: Boolean! = false): [DockerNetwork!]! } type Flash implements Node { - id: ID! + id: PrefixedID! guid: String! vendor: String! product: String! @@ -1283,8 +1281,8 @@ type NotificationOverview { archive: NotificationCounts! } -type Notification { - id: ID! +type Notification implements Node { + id: PrefixedID! """Also known as 'event'""" title: String! @@ -1310,8 +1308,8 @@ enum NotificationType { ARCHIVE } -type Notifications { - id: ID! +type Notifications implements Node { + id: PrefixedID! """A cached overview of the notifications in the system & their severity.""" overview: NotificationOverview! @@ -1331,14 +1329,18 @@ type Owner { avatar: String! } -type VmDomain { - uuid: ID! +type VmDomain implements Node { + """The unique identifier for the vm (uuid)""" + id: PrefixedID! """A friendly name for the vm""" name: String """Current domain vm state""" state: VmState! + + """The UUID of the vm""" + uuid: String @deprecated(reason: "Use id instead") } """The state of a virtual machine""" @@ -1353,8 +1355,8 @@ enum VmState { PMSUSPENDED } -type Vms { - id: ID! +type Vms implements Node { + id: PrefixedID! domains: [VmDomain!] domain: [VmDomain!] } @@ -1364,16 +1366,15 @@ type Uptime { } type Service implements Node { - id: ID! + id: PrefixedID! name: String online: Boolean uptime: Uptime version: String } -type UserAccount { - """A unique identifier for the user""" - id: ID! +type UserAccount implements Node { + id: PrefixedID! """The name of the user""" name: String! @@ -1388,9 +1389,12 @@ type UserAccount { permissions: [Permission!] } +"\n### Description:\n\nID scalar type that prefixes the underlying ID with the server identifier on output and strips it on input.\n\nWe use this scalar type to ensure that the ID is unique across all servers, allowing the same underlying resource ID to be used across different server instances.\n\n#### Input Behavior:\n\nWhen providing an ID as input (e.g., in arguments or input objects), the server identifier prefix (':') is optional.\n\n- If the prefix is present (e.g., '123:456'), it will be automatically stripped, and only the underlying ID ('456') will be used internally.\n- If the prefix is absent (e.g., '456'), the ID will be used as-is.\n\nThis makes it flexible for clients, as they don't strictly need to know or provide the server ID.\n\n#### Output Behavior:\n\nWhen an ID is returned in the response (output), it will *always* be prefixed with the current server's unique identifier (e.g., '123:456').\n\n#### Example:\n\nNote: The server identifier is '123' in this example.\n\n##### Input (Prefix Optional):\n```graphql\n# Both of these are valid inputs resolving to internal ID '456'\n{\n someQuery(id: \"123:456\") { ... }\n anotherQuery(id: \"456\") { ... }\n}\n```\n\n##### Output (Prefix Always Added):\n```graphql\n# Assuming internal ID is '456'\n{\n \"data\": {\n \"someResource\": {\n \"id\": \"123:456\" \n }\n }\n}\n```\n " +scalar PrefixedID + type Query { apiKeys: [ApiKey!]! - apiKey(id: String!): ApiKey + apiKey(id: PrefixedID!): ApiKey cloud: Cloud! config: Config! display: Display! @@ -1421,7 +1425,7 @@ type Query { extraAllowedOrigins: [String!]! docker: Docker! disks: [Disk!]! - disk(id: String!): Disk! + disk(id: PrefixedID!): Disk! health: String! getDemo: String! } @@ -1433,19 +1437,19 @@ type Mutation { """Creates a new notification record""" createNotification(input: NotificationData!): Notification! - deleteNotification(id: String!, type: NotificationType!): NotificationOverview! + deleteNotification(id: PrefixedID!, type: NotificationType!): NotificationOverview! """Deletes all archived notifications on server.""" deleteArchivedNotifications: NotificationOverview! """Marks a notification as archived.""" - archiveNotification(id: String!): Notification! - archiveNotifications(ids: [String!]!): NotificationOverview! + archiveNotification(id: PrefixedID!): Notification! + archiveNotifications(ids: [PrefixedID!]!): NotificationOverview! archiveAll(importance: NotificationImportance): NotificationOverview! """Marks a notification as unread.""" - unreadNotification(id: String!): Notification! - unarchiveNotifications(ids: [String!]!): NotificationOverview! + unreadNotification(id: PrefixedID!): Notification! + unarchiveNotifications(ids: [PrefixedID!]!): NotificationOverview! unarchiveAll(importance: NotificationImportance): NotificationOverview! """Reads each notification to recompute & update the overview.""" @@ -1481,12 +1485,12 @@ input AddPermissionInput { } input AddRoleForApiKeyInput { - apiKeyId: ID! + apiKeyId: PrefixedID! role: Role! } input RemoveRoleFromApiKeyInput { - apiKeyId: ID! + apiKeyId: PrefixedID! role: Role! } diff --git a/api/src/graphql/schema/utils.ts b/api/src/graphql/schema/utils.ts index 54e2b03c6..3f18f2e07 100644 --- a/api/src/graphql/schema/utils.ts +++ b/api/src/graphql/schema/utils.ts @@ -83,7 +83,9 @@ export const getLocalServer = (getState = store.getState): Array => { return [ { + id: 'local', owner: { + id: 'local', username: config.remote.username ?? 'root', url: '', avatar: '', 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 dac020e60..750b30da7 100644 --- a/api/src/unraid-api/auth/api-key.service.spec.ts +++ b/api/src/unraid-api/auth/api-key.service.spec.ts @@ -520,11 +520,13 @@ describe('ApiKeyService', () => { expect(result).toHaveLength(2); expect(result[0]).toEqual({ ...mockApiKey, + createdAt: expect.any(String), id: 'test-api-id', key: 'test-api-key', }); expect(result[1]).toEqual({ ...mockApiKey, + createdAt: expect.any(String), id: 'unique-id', key: 'unique-key', }); diff --git a/api/src/unraid-api/graph/graph.module.ts b/api/src/unraid-api/graph/graph.module.ts index cf47210aa..1eb87564d 100644 --- a/api/src/unraid-api/graph/graph.module.ts +++ b/api/src/unraid-api/graph/graph.module.ts @@ -7,15 +7,15 @@ 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 { getters } from '@app/store/index.js'; import { UsePermissionsDirective, usePermissionsSchemaTransformer, } from '@app/unraid-api/graph/directives/use-permissions.directive.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 { GraphQLLong } from '@app/unraid-api/graph/scalars/graphql-type-long.js'; +import { PrefixedID as PrefixedIDScalar } from '@app/unraid-api/graph/scalars/graphql-type-prefixed-id.js'; import { PluginModule } from '@app/unraid-api/plugin/plugin.module.js'; @Module({ @@ -42,7 +42,7 @@ import { PluginModule } from '@app/unraid-api/plugin/plugin.module.js'; extra, }; }, - plugins: [sandboxPlugin, idPrefixPlugin] as any[], + plugins: [sandboxPlugin] as any[], subscriptions: { 'graphql-ws': { path: '/graphql', @@ -63,7 +63,7 @@ import { PluginModule } from '@app/unraid-api/plugin/plugin.module.js'; }, }), ], - providers: [], + providers: [PrefixedIDScalar], exports: [GraphQLModule], }) export class GraphModule {} diff --git a/api/src/unraid-api/graph/id-prefix-plugin.ts b/api/src/unraid-api/graph/id-prefix-plugin.ts deleted file mode 100644 index 9c7ae36fd..000000000 --- a/api/src/unraid-api/graph/id-prefix-plugin.ts +++ /dev/null @@ -1,67 +0,0 @@ -import { type ApolloServerPlugin } from '@apollo/server'; - -import { getServerIdentifier } from '@app/core/utils/server-identifier.js'; -import { updateObject } from '@app/utils.js'; - -type ObjectModifier = (obj: object) => void; - -/** - * Returns a function that takes an object and updates any 'id' properties to - * include the given serverId as a prefix. - * - * e.g. If the object is { id: '1234' }, the returned function will update it to - * { id: ':1234' }. - * - * @param serverId - The server identifier to use as the prefix. - * @returns A function that takes an object and updates any 'id' properties with the given serverId. - */ -function prefixWithServerId(serverId: string): ObjectModifier { - return (currentObj) => { - if ('id' in currentObj && typeof currentObj.id === 'string') { - currentObj.id = `${serverId}:${currentObj.id}`; - } - }; -} - -/** - * Takes an object and removes any server prefix from the 'id' property. - * - * e.g. If the object is { id: ':1234' }, the returned function will update it to - * { id: '1234' }. - * - * @param current - The object to update. If it has an 'id' property that is a string and - * has a server prefix, the prefix is removed. - */ -const stripServerPrefixFromIds: ObjectModifier = (current) => { - if ('id' in current && typeof current.id === 'string') { - const parts = current.id.split(':'); - // if there are more or less than 2 parts to the split, - // assume there is no server prefix and don't touch it. - if (parts.length === 2) { - current.id = parts[1]; - } - } -}; - -export const idPrefixPlugin: ApolloServerPlugin = { - async requestDidStart(requestContext) { - if (requestContext.request.operationName === 'IntrospectionQuery') { - // Don't modify the introspection query - return; - } - // If ID is requested, return an ID field with an extra prefix - return { - async didResolveOperation({ request }) { - if (request.variables) { - updateObject(request.variables, stripServerPrefixFromIds); - } - }, - async willSendResponse({ response }) { - if (response.body.kind === 'single' && response.body.singleResult.data) { - const serverId = getServerIdentifier(); - updateObject(response.body.singleResult.data, prefixWithServerId(serverId)); - } - }, - }; - }, -}; 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 index 2807ea9b4..08a030d27 100644 --- 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 @@ -1,20 +1,19 @@ -import { Field, ID, InputType, ObjectType } from '@nestjs/graphql'; +import { Field, 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'; +import { Node, Resource, Role } from '@app/unraid-api/graph/resolvers/base.model.js'; +import { PrefixedID } from '@app/unraid-api/graph/scalars/graphql-type-prefixed-id.js'; @ObjectType() export class Permission { @@ -29,13 +28,8 @@ export class Permission { actions!: string[]; } -@ObjectType() -export class ApiKey { - @Field(() => ID) - @IsString() - @IsNotEmpty() - id!: string; - +@ObjectType({ implements: () => Node }) +export class ApiKey extends Node { @Field() @IsString() @IsNotEmpty() @@ -119,7 +113,7 @@ export class CreateApiKeyInput { @InputType() export class AddRoleForApiKeyInput { - @Field(() => ID) + @Field(() => PrefixedID) @IsString() apiKeyId!: string; @@ -130,7 +124,7 @@ export class AddRoleForApiKeyInput { @InputType() export class RemoveRoleFromApiKeyInput { - @Field(() => ID) + @Field(() => PrefixedID) @IsString() apiKeyId!: string; 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 685da705d..2fca74c41 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 @@ -16,6 +16,7 @@ import { } 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 { PrefixedID } from '@app/unraid-api/graph/scalars/graphql-type-prefixed-id.js'; @Resolver(() => ApiKey) export class ApiKeyResolver { @@ -40,7 +41,10 @@ export class ApiKeyResolver { resource: Resource.API_KEY, possession: AuthPossession.ANY, }) - async apiKey(@Args('id') id: string): Promise { + async apiKey( + @Args('id', { type: () => PrefixedID }) + id: string + ): Promise { return this.apiKeyService.findById(id); } diff --git a/api/src/unraid-api/graph/resolvers/array/array.model.ts b/api/src/unraid-api/graph/resolvers/array/array.model.ts index 8a8739b51..e68d33f0d 100644 --- a/api/src/unraid-api/graph/resolvers/array/array.model.ts +++ b/api/src/unraid-api/graph/resolvers/array/array.model.ts @@ -1,9 +1,10 @@ -import { Field, ID, InputType, Int, ObjectType, registerEnumType } from '@nestjs/graphql'; +import { Field, InputType, Int, ObjectType, registerEnumType } from '@nestjs/graphql'; import { IsEnum } from 'class-validator'; -import { GraphQLLong } from '@app/graphql/resolvers/graphql-type-long.js'; import { Node } from '@app/unraid-api/graph/resolvers/base.model.js'; +import { GraphQLLong } from '@app/unraid-api/graph/scalars/graphql-type-long.js'; +import { PrefixedID } from '@app/unraid-api/graph/scalars/graphql-type-prefixed-id.js'; @ObjectType() export class Capacity { @@ -29,10 +30,7 @@ export class ArrayCapacity { @ObjectType({ implements: () => Node, }) -export class ArrayDisk implements Node { - @Field(() => ID, { description: 'Disk identifier, only set for present disks on the system' }) - id!: string; - +export class ArrayDisk extends Node { @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.', @@ -132,10 +130,7 @@ export class ArrayDisk implements Node { @ObjectType({ implements: () => Node, }) -export class UnraidArray implements Node { - @Field(() => ID) - id!: string; - +export class UnraidArray extends Node { @Field(() => ArrayState, { description: 'Current array state' }) state!: ArrayState; @@ -157,7 +152,7 @@ export class UnraidArray implements Node { @InputType() export class ArrayDiskInput { - @Field(() => ID, { description: 'Disk ID' }) + @Field(() => PrefixedID, { description: 'Disk ID' }) id!: string; @Field(() => Int, { nullable: true, description: 'The slot for the disk' }) @@ -244,10 +239,7 @@ registerEnumType(ArrayDiskFsColor, { @ObjectType({ implements: () => Node, }) -export class Share implements Node { - @Field(() => ID) - id!: string; - +export class Share extends Node { @Field(() => String, { description: 'Display name', nullable: true }) name?: string | null; 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 919df86e9..8385c9591 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 @@ -15,6 +15,7 @@ import { 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'; +import { PrefixedID } from '@app/unraid-api/graph/scalars/graphql-type-prefixed-id.js'; /** * Nested Resolvers for Mutations MUST use @ResolveField() instead of @Mutation() @@ -62,7 +63,7 @@ export class ArrayMutationsResolver { resource: Resource.ARRAY, possession: AuthPossession.ANY, }) - public async mountArrayDisk(@Args('id') id: string): Promise { + public async mountArrayDisk(@Args('id', { type: () => PrefixedID }) id: string): Promise { const array = await this.arrayService.mountArrayDisk(id); const disk = array.disks.find((disk) => disk.id === id) || @@ -82,7 +83,9 @@ export class ArrayMutationsResolver { resource: Resource.ARRAY, possession: AuthPossession.ANY, }) - public async unmountArrayDisk(@Args('id') id: string): Promise { + public async unmountArrayDisk( + @Args('id', { type: () => PrefixedID }) id: string + ): Promise { const array = await this.arrayService.unmountArrayDisk(id); const disk = array.disks.find((disk) => disk.id === id) || @@ -102,7 +105,9 @@ export class ArrayMutationsResolver { resource: Resource.ARRAY, possession: AuthPossession.ANY, }) - public async clearArrayDiskStatistics(@Args('id') id: string): Promise { + public async clearArrayDiskStatistics( + @Args('id', { type: () => PrefixedID }) id: string + ): Promise { await this.arrayService.clearArrayDiskStatistics(id); return true; } diff --git a/api/src/unraid-api/graph/resolvers/base.model.ts b/api/src/unraid-api/graph/resolvers/base.model.ts index 3872bb485..3f9d93c86 100644 --- a/api/src/unraid-api/graph/resolvers/base.model.ts +++ b/api/src/unraid-api/graph/resolvers/base.model.ts @@ -1,4 +1,8 @@ -import { Field, ID, InterfaceType, registerEnumType } from '@nestjs/graphql'; +import { Field, InterfaceType, registerEnumType } from '@nestjs/graphql'; + +import { IsNotEmpty, IsString } from 'class-validator'; + +import { PrefixedID } from '@app/unraid-api/graph/scalars/graphql-type-prefixed-id.js'; // Register enums export enum Resource { @@ -40,7 +44,9 @@ export enum Role { @InterfaceType() export class Node { - @Field(() => ID) + @Field(() => PrefixedID) + @IsString() + @IsNotEmpty() id!: string; } diff --git a/api/src/unraid-api/graph/resolvers/config/config.model.ts b/api/src/unraid-api/graph/resolvers/config/config.model.ts index 789cb4fab..cd56d0ec0 100644 --- a/api/src/unraid-api/graph/resolvers/config/config.model.ts +++ b/api/src/unraid-api/graph/resolvers/config/config.model.ts @@ -1,14 +1,11 @@ -import { Field, ID, ObjectType } from '@nestjs/graphql'; +import { Field, 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; - +export class Config extends Node { @Field(() => Boolean, { nullable: true }) valid?: boolean | null; 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 index c0d6ef72e..163bf05dd 100644 --- a/api/src/unraid-api/graph/resolvers/connect/connect-settings.resolver.ts +++ b/api/src/unraid-api/graph/resolvers/connect/connect-settings.resolver.ts @@ -1,5 +1,5 @@ import { Logger } from '@nestjs/common'; -import { Args, ID, Mutation, Query, ResolveField, Resolver } from '@nestjs/graphql'; +import { Args, Mutation, Query, ResolveField, Resolver } from '@nestjs/graphql'; import { Layout } from '@jsonforms/core'; import { GraphQLJSON, GraphQLJSONObject } from 'graphql-scalars'; @@ -24,6 +24,7 @@ import { RemoteAccess, SetupRemoteAccessInput, } from '@app/unraid-api/graph/resolvers/connect/connect.model.js'; +import { PrefixedID } from '@app/unraid-api/graph/scalars/graphql-type-prefixed-id.js'; import { DataSlice } from '@app/unraid-api/types/json-forms.js'; @Resolver(() => ConnectSettings) @@ -31,7 +32,7 @@ export class ConnectSettingsResolver { private readonly logger = new Logger(ConnectSettingsResolver.name); constructor(private readonly connectSettingsService: ConnectSettingsService) {} - @ResolveField(() => ID) + @ResolveField(() => PrefixedID) public async id(): Promise { return 'connectSettingsForm'; } diff --git a/api/src/unraid-api/graph/resolvers/connect/connect.model.ts b/api/src/unraid-api/graph/resolvers/connect/connect.model.ts index 2f68b1407..91e77dd16 100644 --- a/api/src/unraid-api/graph/resolvers/connect/connect.model.ts +++ b/api/src/unraid-api/graph/resolvers/connect/connect.model.ts @@ -17,6 +17,7 @@ import { import { GraphQLJSON, GraphQLURL } from 'graphql-scalars'; import { Node } from '@app/unraid-api/graph/resolvers/base.model.js'; +import { PrefixedID } from '@app/unraid-api/graph/scalars/graphql-type-prefixed-id.js'; export enum WAN_ACCESS_TYPE { DYNAMIC = 'DYNAMIC', @@ -326,12 +327,7 @@ export class ApiSettingsInput { @ObjectType({ implements: () => Node, }) -export class ConnectSettings implements Node { - @Field(() => ID, { description: 'The unique identifier for the Connect settings' }) - @IsString() - @IsNotEmpty() - id!: string; - +export class ConnectSettings extends Node { @Field(() => GraphQLJSON, { description: 'The data schema for the Connect settings' }) @IsObject() dataSchema!: Record; @@ -348,12 +344,8 @@ export class ConnectSettings implements Node { @ObjectType({ implements: () => Node, }) -export class Connect { - @Field(() => ID, { description: 'The unique identifier for the Connect instance' }) - @IsString() - @IsNotEmpty() - id!: string; - +export class Connect extends Node { + @Field(() => DynamicRemoteAccessStatus, { description: 'The status of dynamic remote access' }) @Field(() => DynamicRemoteAccessStatus, { description: 'The status of dynamic remote access' }) @ValidateNested() dynamicRemoteAccess?: DynamicRemoteAccessStatus; @@ -366,10 +358,7 @@ export class Connect { @ObjectType({ implements: () => Node, }) -export class Network implements Node { - @Field(() => ID) - id!: string; - +export class Network extends Node { @Field(() => [AccessUrl], { nullable: true }) accessUrls?: AccessUrl[]; } diff --git a/api/src/unraid-api/graph/resolvers/disks/disks.model.ts b/api/src/unraid-api/graph/resolvers/disks/disks.model.ts index 558b786f0..cd8f6e6f0 100644 --- a/api/src/unraid-api/graph/resolvers/disks/disks.model.ts +++ b/api/src/unraid-api/graph/resolvers/disks/disks.model.ts @@ -3,6 +3,9 @@ import { Field, ObjectType, registerEnumType } from '@nestjs/graphql'; import { Type } from 'class-transformer'; import { IsArray, IsEnum, IsNumber, IsOptional, IsString, ValidateNested } from 'class-validator'; +import { Node } from '@app/unraid-api/graph/resolvers/base.model.js'; +import { PrefixedID } from '@app/unraid-api/graph/scalars/graphql-type-prefixed-id.js'; + export enum DiskFsType { XFS = 'XFS', BTRFS = 'BTRFS', @@ -55,12 +58,8 @@ export class DiskPartition { size!: number; } -@ObjectType() -export class Disk { - @Field(() => String, { description: 'The unique identifier of the disk' }) - @IsString() - id!: string; - +@ObjectType({ implements: () => Node }) +export class Disk extends Node { @Field(() => String, { description: 'The device path of the disk (e.g. /dev/sdb)' }) @IsString() device!: string; 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 bb864650b..2a94bea63 100644 --- a/api/src/unraid-api/graph/resolvers/disks/disks.resolver.ts +++ b/api/src/unraid-api/graph/resolvers/disks/disks.resolver.ts @@ -8,6 +8,7 @@ import { 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'; +import { PrefixedID } from '@app/unraid-api/graph/scalars/graphql-type-prefixed-id.js'; @Resolver(() => Disk) export class DisksResolver { @@ -29,7 +30,7 @@ export class DisksResolver { resource: Resource.DISK, possession: AuthPossession.ANY, }) - public async disk(@Args('id') id: string) { + public async disk(@Args('id', { type: () => PrefixedID }) id: string) { return this.disksService.getDisk(id); } diff --git a/api/src/unraid-api/graph/resolvers/docker/docker.model.ts b/api/src/unraid-api/graph/resolvers/docker/docker.model.ts index 7bd2c2fe3..b1efa7939 100644 --- a/api/src/unraid-api/graph/resolvers/docker/docker.model.ts +++ b/api/src/unraid-api/graph/resolvers/docker/docker.model.ts @@ -71,11 +71,8 @@ export class ContainerMount { propagation!: string; } -@ObjectType() -export class DockerContainer { - @Field(() => ID) - id!: string; - +@ObjectType({ implements: () => Node }) +export class DockerContainer extends Node { @Field(() => [String]) names!: string[]; @@ -119,14 +116,11 @@ export class DockerContainer { autoStart!: boolean; } -@ObjectType() -export class DockerNetwork { +@ObjectType({ implements: () => Node }) +export class DockerNetwork extends Node { @Field(() => String) name!: string; - @Field(() => ID) - id!: string; - @Field(() => String) created!: string; @@ -170,10 +164,7 @@ export class DockerNetwork { @ObjectType({ implements: () => Node, }) -export class Docker implements Node { - @Field(() => ID) - id!: string; - +export class Docker extends Node { @Field(() => [DockerContainer]) containers!: DockerContainer[]; 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 ebbfd36d9..ced0a9a28 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 @@ -9,6 +9,7 @@ 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'; +import { PrefixedID } from '@app/unraid-api/graph/scalars/graphql-type-prefixed-id.js'; /** * Nested Resolvers for Mutations MUST use @ResolveField() instead of @Mutation() @@ -23,7 +24,7 @@ export class DockerMutationsResolver { resource: Resource.DOCKER, possession: AuthPossession.ANY, }) - public async start(@Args('id') id: string) { + public async start(@Args('id', { type: () => PrefixedID }) id: string) { return this.dockerService.start(id); } @@ -33,7 +34,7 @@ export class DockerMutationsResolver { resource: Resource.DOCKER, possession: AuthPossession.ANY, }) - public async stop(@Args('id') id: string) { + public async stop(@Args('id', { type: () => PrefixedID }) id: string) { return this.dockerService.stop(id); } } diff --git a/api/src/unraid-api/graph/resolvers/flash/flash.model.ts b/api/src/unraid-api/graph/resolvers/flash/flash.model.ts index eb3fae2c5..b17f3f621 100644 --- a/api/src/unraid-api/graph/resolvers/flash/flash.model.ts +++ b/api/src/unraid-api/graph/resolvers/flash/flash.model.ts @@ -1,14 +1,11 @@ -import { Field, ID, ObjectType } from '@nestjs/graphql'; +import { Field, 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; - +export class Flash extends Node { @Field(() => String) guid!: 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 b686f044c..7110de369 100644 --- a/api/src/unraid-api/graph/resolvers/flash/flash.resolver.ts +++ b/api/src/unraid-api/graph/resolvers/flash/flash.resolver.ts @@ -22,7 +22,6 @@ export class FlashResolver { 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 index 397c6f28f..a581a70db 100644 --- a/api/src/unraid-api/graph/resolvers/info/info.model.ts +++ b/api/src/unraid-api/graph/resolvers/info/info.model.ts @@ -11,6 +11,7 @@ import { import { GraphQLJSON } from 'graphql-scalars'; import { Node } from '@app/unraid-api/graph/resolvers/base.model.js'; +import { PrefixedID } from '@app/unraid-api/graph/scalars/graphql-type-prefixed-id.js'; export enum Temperature { C = 'C', @@ -31,13 +32,8 @@ registerEnumType(Theme, { description: 'Display theme', }); -@ObjectType({ - implements: () => Node, -}) -export class InfoApps implements Node { - @Field(() => ID) - id!: string; - +@ObjectType({ implements: () => Node }) +export class InfoApps extends Node { @Field(() => Int, { description: 'How many docker containers are installed' }) installed!: number; @@ -45,13 +41,8 @@ export class InfoApps implements Node { started!: number; } -@ObjectType({ - implements: () => Node, -}) -export class Baseboard implements Node { - @Field(() => ID) - id!: string; - +@ObjectType({ implements: () => Node }) +export class Baseboard extends Node { @Field(() => String) manufacturer!: string; @@ -68,13 +59,8 @@ export class Baseboard implements Node { assetTag?: string; } -@ObjectType({ - implements: () => Node, -}) -export class InfoCpu implements Node { - @Field(() => ID) - id!: string; - +@ObjectType({ implements: () => Node }) +export class InfoCpu extends Node { @Field(() => String) manufacturer!: string; @@ -127,13 +113,8 @@ export class InfoCpu implements Node { flags!: string[]; } -@ObjectType({ - implements: () => Node, -}) -export class Gpu implements Node { - @Field(() => ID) - id!: string; - +@ObjectType({ implements: () => Node }) +export class Gpu extends Node { @Field(() => String) type!: string; @@ -153,13 +134,8 @@ export class Gpu implements Node { class!: string; } -@ObjectType({ - implements: () => Node, -}) -export class Network implements Node { - @Field(() => ID) - id!: string; - +@ObjectType({ implements: () => Node }) +export class Network extends Node { @Field(() => String, { nullable: true }) iface?: string; @@ -197,13 +173,8 @@ export class Network implements Node { carrierChanges?: string; } -@ObjectType({ - implements: () => Node, -}) -export class Pci implements Node { - @Field(() => ID) - id!: string; - +@ObjectType({ implements: () => Node }) +export class Pci extends Node { @Field(() => String, { nullable: true }) type?: string; @@ -229,22 +200,14 @@ export class Pci implements Node { class?: string; } -@ObjectType() -export class Usb { - @Field(() => ID) - id!: string; - +@ObjectType({ implements: () => Node }) +export class Usb extends Node { @Field(() => String, { nullable: true }) name?: string; } -@ObjectType({ - implements: () => Node, -}) -export class Devices implements Node { - @Field(() => ID) - id!: string; - +@ObjectType({ implements: () => Node }) +export class Devices extends Node { @Field(() => [Gpu]) gpu!: Gpu[]; @@ -255,9 +218,7 @@ export class Devices implements Node { usb!: Usb[]; } -@ObjectType({ - implements: () => Node, -}) +@ObjectType({ implements: () => Node }) export class Case { @Field(() => String, { nullable: true }) icon?: string; @@ -272,13 +233,8 @@ export class Case { base64?: string; } -@ObjectType({ - implements: () => Node, -}) -export class Display implements Node { - @Field(() => ID, { nullable: false }) - id!: string; - +@ObjectType({ implements: () => Node }) +export class Display extends Node { @Field(() => Case, { nullable: true }) case?: Case; @@ -340,8 +296,8 @@ export class Display implements Node { locale?: string; } -@ObjectType() -export class MemoryLayout { +@ObjectType({ implements: () => Node }) +export class MemoryLayout extends Node { @Field(() => Int) size!: number; @@ -376,13 +332,8 @@ export class MemoryLayout { voltageMax?: number; } -@ObjectType({ - implements: () => Node, -}) -export class InfoMemory implements Node { - @Field(() => ID) - id!: string; - +@ObjectType({ implements: () => Node }) +export class InfoMemory extends Node { @Field(() => Int) max!: number; @@ -417,13 +368,8 @@ export class InfoMemory implements Node { layout!: MemoryLayout[]; } -@ObjectType({ - implements: () => Node, -}) -export class Os implements Node { - @Field(() => ID) - id!: string; - +@ObjectType({ implements: () => Node }) +export class Os extends Node { @Field(() => String, { nullable: true }) platform?: string; @@ -461,13 +407,8 @@ export class Os implements Node { uptime?: string; } -@ObjectType({ - implements: () => Node, -}) -export class System implements Node { - @Field(() => ID) - id!: string; - +@ObjectType({ implements: () => Node }) +export class System extends Node { @Field(() => String, { nullable: true }) manufacturer?: string; @@ -487,13 +428,8 @@ export class System implements Node { sku?: string; } -@ObjectType({ - implements: () => Node, -}) -export class Versions implements Node { - @Field(() => ID) - id!: string; - +@ObjectType({ implements: () => Node }) +export class Versions extends Node { @Field(() => String, { nullable: true }) kernel?: string; @@ -573,13 +509,8 @@ export class Versions implements Node { unraid?: string; } -@ObjectType({ - implements: () => Node, -}) -export class Info implements Node { - @Field(() => ID) - id!: string; - +@ObjectType({ implements: () => Node }) +export class Info extends Node { @Field(() => InfoApps, { description: 'Count of docker containers' }) apps!: InfoApps; @@ -595,7 +526,7 @@ export class Info implements Node { @Field(() => Display) display!: Display; - @Field(() => ID, { description: 'Machine ID', nullable: true }) + @Field(() => PrefixedID, { description: 'Machine ID', nullable: true }) machineId?: string; @Field(() => InfoMemory) diff --git a/api/src/unraid-api/graph/resolvers/notifications/notifications.model.ts b/api/src/unraid-api/graph/resolvers/notifications/notifications.model.ts index 6bdb28b8a..8c5e0f472 100644 --- a/api/src/unraid-api/graph/resolvers/notifications/notifications.model.ts +++ b/api/src/unraid-api/graph/resolvers/notifications/notifications.model.ts @@ -1,7 +1,9 @@ -import { Field, ID, InputType, Int, ObjectType, registerEnumType } from '@nestjs/graphql'; +import { Field, InputType, Int, ObjectType, registerEnumType } from '@nestjs/graphql'; import { IsEnum, IsInt, IsNotEmpty, IsOptional, IsString, Min } from 'class-validator'; +import { Node } from '@app/unraid-api/graph/resolvers/base.model.js'; + export enum NotificationType { UNREAD = 'UNREAD', ARCHIVE = 'ARCHIVE', @@ -109,13 +111,9 @@ export class NotificationOverview { archive!: NotificationCounts; } -@ObjectType('Notification') -export class Notification { - @Field(() => ID) - @IsString() - @IsNotEmpty() - id!: string; - +@ObjectType({ implements: () => Node }) +export class Notification extends Node { + @Field({ description: "Also known as 'event'" }) @Field({ description: "Also known as 'event'" }) @IsString() @IsNotEmpty() @@ -157,13 +155,8 @@ export class Notification { formattedTimestamp?: string; } -@ObjectType('Notifications') -export class Notifications { - @Field(() => ID) - @IsString() - @IsNotEmpty() - id!: string; - +@ObjectType({ implements: () => Node }) +export class Notifications extends Node { @Field(() => NotificationOverview, { description: 'A cached overview of the notifications in the system & their severity.', }) 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 d9bf3f038..77c6c6918 100644 --- a/api/src/unraid-api/graph/resolvers/notifications/notifications.resolver.ts +++ b/api/src/unraid-api/graph/resolvers/notifications/notifications.resolver.ts @@ -18,6 +18,7 @@ import { NotificationType, } from '@app/unraid-api/graph/resolvers/notifications/notifications.model.js'; import { NotificationsService } from '@app/unraid-api/graph/resolvers/notifications/notifications.service.js'; +import { PrefixedID } from '@app/unraid-api/graph/scalars/graphql-type-prefixed-id.js'; @Resolver(() => Notifications) export class NotificationsResolver { @@ -66,7 +67,7 @@ export class NotificationsResolver { @Mutation(() => NotificationOverview) public async deleteNotification( - @Args('id', { type: () => String }) + @Args('id', { type: () => PrefixedID }) id: string, @Args('type', { type: () => NotificationType }) type: NotificationType @@ -84,7 +85,7 @@ export class NotificationsResolver { @Mutation(() => Notification, { description: 'Marks a notification as archived.' }) public archiveNotification( - @Args('id', { type: () => String }) + @Args('id', { type: () => PrefixedID }) id: string ): Promise { return this.notificationsService.archiveNotification({ id }); @@ -92,7 +93,7 @@ export class NotificationsResolver { @Mutation(() => NotificationOverview) public async archiveNotifications( - @Args('ids', { type: () => [String] }) + @Args('ids', { type: () => [PrefixedID] }) ids: string[] ): Promise { await this.notificationsService.archiveIds(ids); @@ -110,7 +111,7 @@ export class NotificationsResolver { @Mutation(() => Notification, { description: 'Marks a notification as unread.' }) public unreadNotification( - @Args('id', { type: () => String }) + @Args('id', { type: () => PrefixedID }) id: string ): Promise { return this.notificationsService.markAsUnread({ id }); @@ -118,7 +119,7 @@ export class NotificationsResolver { @Mutation(() => NotificationOverview) public async unarchiveNotifications( - @Args('ids', { type: () => [String] }) + @Args('ids', { type: () => [PrefixedID] }) ids: string[] ): Promise { await this.notificationsService.unarchiveIds(ids); diff --git a/api/src/unraid-api/graph/resolvers/registration/registration.model.ts b/api/src/unraid-api/graph/resolvers/registration/registration.model.ts index 4b9f377cc..d1ad05c3e 100644 --- a/api/src/unraid-api/graph/resolvers/registration/registration.model.ts +++ b/api/src/unraid-api/graph/resolvers/registration/registration.model.ts @@ -1,4 +1,6 @@ -import { Field, ID, ObjectType, registerEnumType } from '@nestjs/graphql'; +import { Field, ObjectType, registerEnumType } from '@nestjs/graphql'; + +import { Node } from '@app/unraid-api/graph/resolvers/base.model.js'; export enum RegistrationType { BASIC = 'BASIC', @@ -83,11 +85,8 @@ export class KeyFile { contents?: string; } -@ObjectType() -export class Registration { - @Field(() => ID, { nullable: true }) - guid?: string; - +@ObjectType({ implements: () => Node }) +export class Registration extends Node { @Field(() => RegistrationType, { nullable: true }) type?: RegistrationType; 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 f4fef52e7..ff0233ea1 100644 --- a/api/src/unraid-api/graph/resolvers/registration/registration.resolver.ts +++ b/api/src/unraid-api/graph/resolvers/registration/registration.resolver.ts @@ -33,7 +33,7 @@ export class RegistrationResolver { const isExpired = emhttp.var.regTy.includes('expired'); const registration: Registration = { - guid: emhttp.var.regGuid, + id: emhttp.var.regGuid, type: emhttp.var.regTy, 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 diff --git a/api/src/unraid-api/graph/resolvers/servers/server.model.ts b/api/src/unraid-api/graph/resolvers/servers/server.model.ts index c0406064f..a6c4e7f79 100644 --- a/api/src/unraid-api/graph/resolvers/servers/server.model.ts +++ b/api/src/unraid-api/graph/resolvers/servers/server.model.ts @@ -1,10 +1,9 @@ import { Field, ID, ObjectType, registerEnumType } from '@nestjs/graphql'; -@ObjectType() -export class ProfileModel { - @Field(() => ID, { nullable: true }) - userId?: string; +import { Node } from '@app/unraid-api/graph/resolvers/base.model.js'; +@ObjectType({ implements: () => Node }) +export class ProfileModel extends Node { @Field() username!: string; @@ -25,8 +24,8 @@ registerEnumType(ServerStatus, { name: 'ServerStatus', }); -@ObjectType() -export class Server { +@ObjectType({ implements: () => Node }) +export class Server extends Node { @Field(() => ProfileModel) owner!: ProfileModel; diff --git a/api/src/unraid-api/graph/resolvers/vars/vars.model.ts b/api/src/unraid-api/graph/resolvers/vars/vars.model.ts index c452da639..8bd581c00 100644 --- a/api/src/unraid-api/graph/resolvers/vars/vars.model.ts +++ b/api/src/unraid-api/graph/resolvers/vars/vars.model.ts @@ -32,10 +32,7 @@ registerEnumType(MdState, { @ObjectType({ implements: () => Node, }) -export class Vars implements Node { - @Field(() => ID) - id!: string; - +export class Vars extends Node { @Field({ nullable: true, description: 'Unraid version' }) version?: string; diff --git a/api/src/unraid-api/graph/resolvers/vms/vms.model.ts b/api/src/unraid-api/graph/resolvers/vms/vms.model.ts index 526404215..355d3998f 100644 --- a/api/src/unraid-api/graph/resolvers/vms/vms.model.ts +++ b/api/src/unraid-api/graph/resolvers/vms/vms.model.ts @@ -1,4 +1,9 @@ -import { Field, ID, InputType, ObjectType, registerEnumType } from '@nestjs/graphql'; +import { Field, InputType, ObjectType, registerEnumType } from '@nestjs/graphql'; + +import { IsEnum, IsNotEmpty, IsString } from 'class-validator'; + +import { Node } from '@app/unraid-api/graph/resolvers/base.model.js'; +import { PrefixedID } from '@app/unraid-api/graph/scalars/graphql-type-prefixed-id.js'; // Register the VmState enum export enum VmState { @@ -17,23 +22,31 @@ registerEnumType(VmState, { description: 'The state of a virtual machine', }); -@ObjectType() -export class VmDomain { - @Field(() => ID) - uuid!: string; +@ObjectType({ implements: () => Node }) +export class VmDomain implements Node { + @Field(() => PrefixedID, { description: 'The unique identifier for the vm (uuid)' }) + @IsString() + @IsNotEmpty() + id!: string; @Field({ nullable: true, description: 'A friendly name for the vm' }) + @IsString() name?: string; @Field(() => VmState, { description: 'Current domain vm state' }) + @IsEnum(VmState) state!: VmState; + + @Field(() => String, { + nullable: true, + description: 'The UUID of the vm', + deprecationReason: 'Use id instead', + }) + uuid?: string; } -@ObjectType() -export class Vms { - @Field(() => ID) - id!: string; - +@ObjectType({ implements: () => Node }) +export class Vms extends Node { @Field(() => [VmDomain], { nullable: true }) domains?: VmDomain[]; 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 3cb287d20..6b7843a41 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 @@ -8,6 +8,7 @@ import { 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'; +import { PrefixedID } from '@app/unraid-api/graph/scalars/graphql-type-prefixed-id.js'; /** * Nested Resolvers for Mutations MUST use @ResolveField() instead of @Mutation() @@ -22,7 +23,7 @@ export class VmMutationsResolver { possession: AuthPossession.ANY, }) @ResolveField(() => Boolean, { description: 'Start a virtual machine' }) - async start(@Args('id') id: string): Promise { + async start(@Args('id', { type: () => PrefixedID }) id: string): Promise { return this.vmsService.startVm(id); } @@ -32,7 +33,7 @@ export class VmMutationsResolver { possession: AuthPossession.ANY, }) @ResolveField(() => Boolean, { description: 'Stop a virtual machine' }) - async stop(@Args('id') id: string): Promise { + async stop(@Args('id', { type: () => PrefixedID }) id: string): Promise { return this.vmsService.stopVm(id); } @@ -42,7 +43,7 @@ export class VmMutationsResolver { possession: AuthPossession.ANY, }) @ResolveField(() => Boolean, { description: 'Pause a virtual machine' }) - async pause(@Args('id') id: string): Promise { + async pause(@Args('id', { type: () => PrefixedID }) id: string): Promise { return this.vmsService.pauseVm(id); } @@ -52,7 +53,7 @@ export class VmMutationsResolver { possession: AuthPossession.ANY, }) @ResolveField(() => Boolean, { description: 'Resume a virtual machine' }) - async resume(@Args('id') id: string): Promise { + async resume(@Args('id', { type: () => PrefixedID }) id: string): Promise { return this.vmsService.resumeVm(id); } @@ -62,7 +63,7 @@ export class VmMutationsResolver { possession: AuthPossession.ANY, }) @ResolveField(() => Boolean, { description: 'Force stop a virtual machine' }) - async forceStop(@Args('id') id: string): Promise { + async forceStop(@Args('id', { type: () => PrefixedID }) id: string): Promise { return this.vmsService.forceStopVm(id); } @@ -72,7 +73,7 @@ export class VmMutationsResolver { possession: AuthPossession.ANY, }) @ResolveField(() => Boolean, { description: 'Reboot a virtual machine' }) - async reboot(@Args('id') id: string): Promise { + async reboot(@Args('id', { type: () => PrefixedID }) id: string): Promise { return this.vmsService.rebootVm(id); } @@ -82,7 +83,7 @@ export class VmMutationsResolver { possession: AuthPossession.ANY, }) @ResolveField(() => Boolean, { description: 'Reset a virtual machine' }) - async reset(@Args('id') id: string): Promise { + async reset(@Args('id', { type: () => PrefixedID }) id: string): Promise { return this.vmsService.resetVm(id); } } 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 73cd49463..e6256b2a5 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 @@ -261,10 +261,10 @@ describe('VmsService', () => { it('should start and stop the test VM', async () => { expect(testVm).toBeDefined(); - expect(testVm?.uuid).toBeDefined(); + expect(testVm?.id).toBeDefined(); // Start the VM - const startResult = await service.startVm(testVm!.uuid); + const startResult = await service.startVm(testVm!.id); expect(startResult).toBe(true); // Wait for VM to start with a more targeted approach @@ -282,7 +282,7 @@ describe('VmsService', () => { expect(isRunning).toBe(true); // Stop the VM - const stopResult = await service.stopVm(testVm!.uuid); + const stopResult = await service.stopVm(testVm!.id); expect(stopResult).toBe(true); // Wait for VM to stop with a more targeted approach 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 35a1b1a2e..4f507ff51 100644 --- a/api/src/unraid-api/graph/resolvers/vms/vms.service.ts +++ b/api/src/unraid-api/graph/resolvers/vms/vms.service.ts @@ -360,8 +360,9 @@ export class VmsService implements OnModuleInit, OnModuleDestroy { const state = this.mapDomainStateToVmState(info.state); return { - name, + id: uuid, uuid, + name, state, }; }) diff --git a/api/src/graphql/resolvers/graphql-type-long.ts b/api/src/unraid-api/graph/scalars/graphql-type-long.ts similarity index 100% rename from api/src/graphql/resolvers/graphql-type-long.ts rename to api/src/unraid-api/graph/scalars/graphql-type-long.ts diff --git a/api/src/unraid-api/graph/scalars/graphql-type-prefixed-id.ts b/api/src/unraid-api/graph/scalars/graphql-type-prefixed-id.ts new file mode 100644 index 000000000..625938b9b --- /dev/null +++ b/api/src/unraid-api/graph/scalars/graphql-type-prefixed-id.ts @@ -0,0 +1,107 @@ +import { CustomScalar, Scalar } from '@nestjs/graphql'; + +import { Kind, ValueNode } from 'graphql'; + +import { getServerIdentifier } from '@app/core/utils/server-identifier.js'; + +@Scalar('PrefixedID', () => PrefixedID) +export class PrefixedID implements CustomScalar { + description: string = ` +### Description: + +ID scalar type that prefixes the underlying ID with the server identifier on output and strips it on input. + +We use this scalar type to ensure that the ID is unique across all servers, allowing the same underlying resource ID to be used across different server instances. + +#### Input Behavior: + +When providing an ID as input (e.g., in arguments or input objects), the server identifier prefix (':') is optional. + +- If the prefix is present (e.g., '123:456'), it will be automatically stripped, and only the underlying ID ('456') will be used internally. +- If the prefix is absent (e.g., '456'), the ID will be used as-is. + +This makes it flexible for clients, as they don't strictly need to know or provide the server ID. + +#### Output Behavior: + +When an ID is returned in the response (output), it will *always* be prefixed with the current server's unique identifier (e.g., '123:456'). + +#### Example: + +Note: The server identifier is '123' in this example. + +##### Input (Prefix Optional): +\`\`\`graphql +# Both of these are valid inputs resolving to internal ID '456' +{ + someQuery(id: "123:456") { ... } + anotherQuery(id: "456") { ... } +} +\`\`\` + +##### Output (Prefix Always Added): +\`\`\`graphql +# Assuming internal ID is '456' +{ + "data": { + "someResource": { + "id": "123:456" + } + } +} +\`\`\` + `; + + // For output: Add the prefix + serialize(value: unknown): string { + if (typeof value !== 'string') { + // Consider logging this error or handling it based on your specific needs + console.error(`PrefixedID cannot represent non-string value: ${value}`); + throw new Error(`PrefixedID cannot represent non-string value: ${value}`); + } + // Simple check to avoid double-prefixing if somehow already prefixed + // This might happen if data is fetched internally already prefixed + if (value.includes(':')) { + // Optionally log or verify if the prefix matches the current serverId + // const serverId = this.serverIdentifierService.getId(); + // if (!value.startsWith(`${serverId}:`)) { + // console.warn(`PrefixedID serialize: Value '${value}' already has a different prefix.`); + // } + return value; + } + const serverId = getServerIdentifier(); + return `${serverId}:${value}`; + } + + // For input variables: Remove the prefix + parseValue(value: unknown): string { + if (typeof value !== 'string') { + throw new Error(`PrefixedID cannot represent non-string value: ${value}`); + } + // Expecting ':' + const parts = value.split(':'); + if (parts.length === 2) { + return parts[1]; + } + // If it doesn't have the prefix format, assume it's an internal ID already. + // console.debug(`PrefixedID parseValue: Value '${value}' does not contain expected prefix.`); + return value; + } + + // For inline query arguments: Remove the prefix + parseLiteral(ast: ValueNode): string { + if (ast.kind !== Kind.STRING) { + // Handle or throw error for non-string literals if necessary + throw new Error( + `PrefixedID cannot represent non-string literal value: ${'value' in ast ? ast.value : null}` + ); + } + // Same logic as parseValue + const parts = ast.value.split(':'); + if (parts.length === 2) { + return parts[1]; + } + // console.debug(`PrefixedID parseLiteral: Value '${ast.value}' does not contain expected prefix.`); + return ast.value; + } +} diff --git a/api/src/unraid-api/graph/services/service.model.ts b/api/src/unraid-api/graph/services/service.model.ts index 1b502dbfd..c58b57eaf 100644 --- a/api/src/unraid-api/graph/services/service.model.ts +++ b/api/src/unraid-api/graph/services/service.model.ts @@ -11,10 +11,7 @@ export class Uptime { @ObjectType({ implements: () => Node, }) -export class Service implements Node { - @Field(() => ID) - id!: string; - +export class Service extends Node { @Field(() => String, { nullable: true }) name?: string; diff --git a/api/src/unraid-api/graph/user/user.model.ts b/api/src/unraid-api/graph/user/user.model.ts index a9d96bb8d..b215c9f04 100644 --- a/api/src/unraid-api/graph/user/user.model.ts +++ b/api/src/unraid-api/graph/user/user.model.ts @@ -1,13 +1,10 @@ -import { Field, ID, ObjectType } from '@nestjs/graphql'; +import { Field, 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; +import { Node, Role } from '@app/unraid-api/graph/resolvers/base.model.js'; +@ObjectType({ implements: () => Node }) +export class UserAccount extends Node { @Field({ description: 'The name of the user' }) name!: string; diff --git a/web/codegen.ts b/web/codegen.ts index 8246e472b..e6e547118 100644 --- a/web/codegen.ts +++ b/web/codegen.ts @@ -20,6 +20,7 @@ const config: CodegenConfig = { URL: 'URL', Port: 'number', UUID: 'string', + PrefixedID: 'string', }, }, generates: { diff --git a/web/components/Notifications/graphql/notification.query.ts b/web/components/Notifications/graphql/notification.query.ts index 8703125d0..f46781c0d 100644 --- a/web/components/Notifications/graphql/notification.query.ts +++ b/web/components/Notifications/graphql/notification.query.ts @@ -35,7 +35,7 @@ export const getNotifications = graphql(/* GraphQL */ ` `); export const archiveNotification = graphql(/* GraphQL */ ` - mutation ArchiveNotification($id: String!) { + mutation ArchiveNotification($id: PrefixedID!) { archiveNotification(id: $id) { ...NotificationFragment } @@ -59,7 +59,7 @@ export const archiveAllNotifications = graphql(/* GraphQL */ ` `); export const deleteNotification = graphql(/* GraphQL */ ` - mutation DeleteNotification($id: String!, $type: NotificationType!) { + mutation DeleteNotification($id: PrefixedID!, $type: NotificationType!) { deleteNotification(id: $id, type: $type) { archive { total diff --git a/web/composables/gql/gql.ts b/web/composables/gql/gql.ts index 61a7724c3..a9357e94d 100644 --- a/web/composables/gql/gql.ts +++ b/web/composables/gql/gql.ts @@ -22,9 +22,9 @@ type Documents = { "\n fragment NotificationFragment on Notification {\n id\n title\n subject\n description\n importance\n link\n type\n timestamp\n formattedTimestamp\n }\n": typeof types.NotificationFragmentFragmentDoc, "\n fragment NotificationCountFragment on NotificationCounts {\n total\n info\n warning\n alert\n }\n": typeof types.NotificationCountFragmentFragmentDoc, "\n query Notifications($filter: NotificationFilter!) {\n notifications {\n id\n list(filter: $filter) {\n ...NotificationFragment\n }\n }\n }\n": typeof types.NotificationsDocument, - "\n mutation ArchiveNotification($id: String!) {\n archiveNotification(id: $id) {\n ...NotificationFragment\n }\n }\n": typeof types.ArchiveNotificationDocument, + "\n mutation ArchiveNotification($id: PrefixedID!) {\n archiveNotification(id: $id) {\n ...NotificationFragment\n }\n }\n": typeof types.ArchiveNotificationDocument, "\n mutation ArchiveAllNotifications {\n archiveAll {\n unread {\n total\n }\n archive {\n info\n warning\n alert\n total\n }\n }\n }\n": typeof types.ArchiveAllNotificationsDocument, - "\n mutation DeleteNotification($id: String!, $type: NotificationType!) {\n deleteNotification(id: $id, type: $type) {\n archive {\n total\n }\n }\n }\n": typeof types.DeleteNotificationDocument, + "\n mutation DeleteNotification($id: PrefixedID!, $type: NotificationType!) {\n deleteNotification(id: $id, type: $type) {\n archive {\n total\n }\n }\n }\n": typeof types.DeleteNotificationDocument, "\n mutation DeleteAllNotifications {\n deleteArchivedNotifications {\n archive {\n total\n }\n unread {\n total\n }\n }\n }\n": typeof types.DeleteAllNotificationsDocument, "\n query Overview {\n notifications {\n id\n overview {\n unread {\n info\n warning\n alert\n total\n }\n archive {\n total\n }\n }\n }\n }\n": typeof types.OverviewDocument, "\n mutation RecomputeOverview {\n recalculateOverview {\n archive {\n ...NotificationCountFragment\n }\n unread {\n ...NotificationCountFragment\n }\n }\n }\n": typeof types.RecomputeOverviewDocument, @@ -48,9 +48,9 @@ const documents: Documents = { "\n fragment NotificationFragment on Notification {\n id\n title\n subject\n description\n importance\n link\n type\n timestamp\n formattedTimestamp\n }\n": types.NotificationFragmentFragmentDoc, "\n fragment NotificationCountFragment on NotificationCounts {\n total\n info\n warning\n alert\n }\n": types.NotificationCountFragmentFragmentDoc, "\n query Notifications($filter: NotificationFilter!) {\n notifications {\n id\n list(filter: $filter) {\n ...NotificationFragment\n }\n }\n }\n": types.NotificationsDocument, - "\n mutation ArchiveNotification($id: String!) {\n archiveNotification(id: $id) {\n ...NotificationFragment\n }\n }\n": types.ArchiveNotificationDocument, + "\n mutation ArchiveNotification($id: PrefixedID!) {\n archiveNotification(id: $id) {\n ...NotificationFragment\n }\n }\n": types.ArchiveNotificationDocument, "\n mutation ArchiveAllNotifications {\n archiveAll {\n unread {\n total\n }\n archive {\n info\n warning\n alert\n total\n }\n }\n }\n": types.ArchiveAllNotificationsDocument, - "\n mutation DeleteNotification($id: String!, $type: NotificationType!) {\n deleteNotification(id: $id, type: $type) {\n archive {\n total\n }\n }\n }\n": types.DeleteNotificationDocument, + "\n mutation DeleteNotification($id: PrefixedID!, $type: NotificationType!) {\n deleteNotification(id: $id, type: $type) {\n archive {\n total\n }\n }\n }\n": types.DeleteNotificationDocument, "\n mutation DeleteAllNotifications {\n deleteArchivedNotifications {\n archive {\n total\n }\n unread {\n total\n }\n }\n }\n": types.DeleteAllNotificationsDocument, "\n query Overview {\n notifications {\n id\n overview {\n unread {\n info\n warning\n alert\n total\n }\n archive {\n total\n }\n }\n }\n }\n": types.OverviewDocument, "\n mutation RecomputeOverview {\n recalculateOverview {\n archive {\n ...NotificationCountFragment\n }\n unread {\n ...NotificationCountFragment\n }\n }\n }\n": types.RecomputeOverviewDocument, @@ -115,7 +115,7 @@ export function graphql(source: "\n query Notifications($filter: NotificationFi /** * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. */ -export function graphql(source: "\n mutation ArchiveNotification($id: String!) {\n archiveNotification(id: $id) {\n ...NotificationFragment\n }\n }\n"): (typeof documents)["\n mutation ArchiveNotification($id: String!) {\n archiveNotification(id: $id) {\n ...NotificationFragment\n }\n }\n"]; +export function graphql(source: "\n mutation ArchiveNotification($id: PrefixedID!) {\n archiveNotification(id: $id) {\n ...NotificationFragment\n }\n }\n"): (typeof documents)["\n mutation ArchiveNotification($id: PrefixedID!) {\n archiveNotification(id: $id) {\n ...NotificationFragment\n }\n }\n"]; /** * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. */ @@ -123,7 +123,7 @@ export function graphql(source: "\n mutation ArchiveAllNotifications {\n arc /** * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. */ -export function graphql(source: "\n mutation DeleteNotification($id: String!, $type: NotificationType!) {\n deleteNotification(id: $id, type: $type) {\n archive {\n total\n }\n }\n }\n"): (typeof documents)["\n mutation DeleteNotification($id: String!, $type: NotificationType!) {\n deleteNotification(id: $id, type: $type) {\n archive {\n total\n }\n }\n }\n"]; +export function graphql(source: "\n mutation DeleteNotification($id: PrefixedID!, $type: NotificationType!) {\n deleteNotification(id: $id, type: $type) {\n archive {\n total\n }\n }\n }\n"): (typeof documents)["\n mutation DeleteNotification($id: PrefixedID!, $type: NotificationType!) {\n deleteNotification(id: $id, type: $type) {\n archive {\n total\n }\n }\n }\n"]; /** * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. */ diff --git a/web/composables/gql/graphql.ts b/web/composables/gql/graphql.ts index bc43e2ada..137d56c38 100644 --- a/web/composables/gql/graphql.ts +++ b/web/composables/gql/graphql.ts @@ -22,6 +22,56 @@ export type Scalars = { JSONObject: { input: any; output: any; } /** The `Long` scalar type represents 52-bit integers */ Long: { input: number; output: number; } + /** 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 */ + Port: { input: number; output: number; } + /** + * + * ### Description: + * + * ID scalar type that prefixes the underlying ID with the server identifier on output and strips it on input. + * + * We use this scalar type to ensure that the ID is unique across all servers, allowing the same underlying resource ID to be used across different server instances. + * + * #### Input Behavior: + * + * When providing an ID as input (e.g., in arguments or input objects), the server identifier prefix (':') is optional. + * + * - If the prefix is present (e.g., '123:456'), it will be automatically stripped, and only the underlying ID ('456') will be used internally. + * - If the prefix is absent (e.g., '456'), the ID will be used as-is. + * + * This makes it flexible for clients, as they don't strictly need to know or provide the server ID. + * + * #### Output Behavior: + * + * When an ID is returned in the response (output), it will *always* be prefixed with the current server's unique identifier (e.g., '123:456'). + * + * #### Example: + * + * Note: The server identifier is '123' in this example. + * + * ##### Input (Prefix Optional): + * ```graphql + * # Both of these are valid inputs resolving to internal ID '456' + * { + * someQuery(id: "123:456") { ... } + * anotherQuery(id: "456") { ... } + * } + * ``` + * + * ##### Output (Prefix Always Added): + * ```graphql + * # Assuming internal ID is '456' + * { + * "data": { + * "someResource": { + * "id": "123:456" + * } + * } + * } + * ``` + * + */ + PrefixedID: { input: string; output: string; } /** A field whose value conforms to the standard URL format as specified in RFC3986: https://www.ietf.org/rfc/rfc3986.txt. */ URL: { input: URL; output: URL; } }; @@ -34,13 +84,20 @@ export type AccessUrl = { type: UrlType; }; +export type AccessUrlInput = { + ipv4?: InputMaybe; + ipv6?: InputMaybe; + name?: InputMaybe; + type: UrlType; +}; + export type AddPermissionInput = { actions: Array; resource: Resource; }; export type AddRoleForApiKeyInput = { - apiKeyId: Scalars['ID']['input']; + apiKeyId: Scalars['PrefixedID']['input']; role: Role; }; @@ -49,11 +106,11 @@ export type AllowedOriginInput = { origins: Array; }; -export type ApiKey = { +export type ApiKey = Node & { __typename?: 'ApiKey'; createdAt: Scalars['String']['output']; description?: Maybe; - id: Scalars['ID']['output']; + id: Scalars['PrefixedID']['output']; name: Scalars['String']['output']; permissions: Array; roles: Array; @@ -65,11 +122,11 @@ export type ApiKeyResponse = { valid: Scalars['Boolean']['output']; }; -export type ApiKeyWithSecret = { +export type ApiKeyWithSecret = Node & { __typename?: 'ApiKeyWithSecret'; createdAt: Scalars['String']['output']; description?: Maybe; - id: Scalars['ID']['output']; + id: Scalars['PrefixedID']['output']; key: Scalars['String']['output']; name: Scalars['String']['output']; permissions: Array; @@ -118,8 +175,7 @@ export type ArrayDisk = Node & { fsType?: Maybe; /** (KB) Used Size on the FS (Not present on Parity type drive) */ fsUsed?: Maybe; - /** Disk identifier, only set for present disks on the system */ - id: Scalars['ID']['output']; + id: Scalars['PrefixedID']['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; @@ -158,7 +214,7 @@ export enum ArrayDiskFsColor { export type ArrayDiskInput = { /** Disk ID */ - id: Scalars['ID']['input']; + id: Scalars['PrefixedID']['input']; /** The slot for the disk */ slot?: InputMaybe; }; @@ -205,12 +261,12 @@ export type ArrayMutationsAddDiskToArrayArgs = { export type ArrayMutationsClearArrayDiskStatisticsArgs = { - id: Scalars['String']['input']; + id: Scalars['PrefixedID']['input']; }; export type ArrayMutationsMountArrayDiskArgs = { - id: Scalars['String']['input']; + id: Scalars['PrefixedID']['input']; }; @@ -225,16 +281,9 @@ export type ArrayMutationsSetStateArgs = { export type ArrayMutationsUnmountArrayDiskArgs = { - id: Scalars['String']['input']; + id: Scalars['PrefixedID']['input']; }; -export enum ArrayPendingState { - NO_DATA_DISKS = 'NO_DATA_DISKS', - STARTING = 'STARTING', - STOPPING = 'STOPPING', - TOO_MANY_MISSING_DISKS = 'TOO_MANY_MISSING_DISKS' -} - export enum ArrayState { DISABLE_DISK = 'DISABLE_DISK', INVALID_EXPANSION = 'INVALID_EXPANSION', @@ -259,10 +308,25 @@ export enum ArrayStateInputState { 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 = Node & { __typename?: 'Baseboard'; assetTag?: Maybe; - id: Scalars['ID']['output']; + id: Scalars['PrefixedID']['output']; manufacturer: Scalars['String']['output']; model?: Maybe; serial?: Maybe; @@ -284,7 +348,7 @@ export type Case = Node & { base64?: Maybe; error?: Maybe; icon?: Maybe; - id: Scalars['ID']['output']; + id: Scalars['PrefixedID']['output']; url?: Maybe; }; @@ -308,7 +372,7 @@ export type CloudResponse = { export type Config = Node & { __typename?: 'Config'; error?: Maybe; - id: Scalars['ID']['output']; + id: Scalars['PrefixedID']['output']; valid?: Maybe; }; @@ -325,8 +389,7 @@ export type Connect = Node & { __typename?: 'Connect'; /** The status of dynamic remote access */ dynamicRemoteAccess: DynamicRemoteAccessStatus; - /** The unique identifier for the Connect instance */ - id: Scalars['ID']['output']; + id: Scalars['PrefixedID']['output']; /** The settings for the Connect instance */ settings: ConnectSettings; }; @@ -335,8 +398,7 @@ export type ConnectSettings = Node & { __typename?: 'ConnectSettings'; /** The data schema for the Connect settings */ dataSchema: Scalars['JSON']['output']; - /** The unique identifier for the Connect settings */ - id: Scalars['ID']['output']; + id: Scalars['PrefixedID']['output']; /** The UI schema for the Connect settings */ uiSchema: Scalars['JSON']['output']; /** The values for the Connect settings */ @@ -389,8 +451,8 @@ export type ContainerHostConfig = { export type ContainerPort = { __typename?: 'ContainerPort'; ip?: Maybe; - privatePort: Scalars['Int']['output']; - publicPort: Scalars['Int']['output']; + privatePort?: Maybe; + publicPort?: Maybe; type: ContainerPortType; }; @@ -416,12 +478,12 @@ export type CreateApiKeyInput = { export type Devices = Node & { __typename?: 'Devices'; gpu: Array; - id: Scalars['ID']['output']; + id: Scalars['PrefixedID']['output']; pci: Array; usb: Array; }; -export type Disk = { +export type Disk = Node & { __typename?: 'Disk'; /** The number of bytes per sector */ bytesPerSector: Scalars['Float']['output']; @@ -429,8 +491,7 @@ export type Disk = { device: Scalars['String']['output']; /** The firmware revision of the disk */ firmwareRevision: Scalars['String']['output']; - /** The unique identifier of the disk */ - id: Scalars['String']['output']; + id: Scalars['PrefixedID']['output']; /** The interface type of the disk */ interfaceType: DiskInterfaceType; /** The model name of the disk */ @@ -506,7 +567,7 @@ export type Display = Node & { dashapps?: Maybe; date?: Maybe; hot?: Maybe; - id: Scalars['ID']['output']; + id: Scalars['PrefixedID']['output']; locale?: Maybe; max?: Maybe; number?: Maybe; @@ -526,17 +587,27 @@ export type Display = Node & { export type Docker = Node & { __typename?: 'Docker'; containers: Array; - id: Scalars['ID']['output']; + id: Scalars['PrefixedID']['output']; networks: Array; }; -export type DockerContainer = { + +export type DockerContainersArgs = { + skipCache?: Scalars['Boolean']['input']; +}; + + +export type DockerNetworksArgs = { + skipCache?: Scalars['Boolean']['input']; +}; + +export type DockerContainer = Node & { __typename?: 'DockerContainer'; autoStart: Scalars['Boolean']['output']; command: Scalars['String']['output']; created: Scalars['Int']['output']; hostConfig?: Maybe; - id: Scalars['ID']['output']; + id: Scalars['PrefixedID']['output']; image: Scalars['String']['output']; imageId: Scalars['String']['output']; labels?: Maybe; @@ -560,15 +631,15 @@ export type DockerMutations = { export type DockerMutationsStartArgs = { - id: Scalars['String']['input']; + id: Scalars['PrefixedID']['input']; }; export type DockerMutationsStopArgs = { - id: Scalars['String']['input']; + id: Scalars['PrefixedID']['input']; }; -export type DockerNetwork = { +export type DockerNetwork = Node & { __typename?: 'DockerNetwork'; attachable: Scalars['Boolean']['output']; configFrom: Scalars['JSONObject']['output']; @@ -577,7 +648,7 @@ export type DockerNetwork = { created: Scalars['String']['output']; driver: Scalars['String']['output']; enableIPv6: Scalars['Boolean']['output']; - id: Scalars['ID']['output']; + id: Scalars['PrefixedID']['output']; ingress: Scalars['Boolean']['output']; internal: Scalars['Boolean']['output']; ipam: Scalars['JSONObject']['output']; @@ -606,14 +677,14 @@ export enum DynamicRemoteAccessType { export type EnableDynamicRemoteAccessInput = { /** Whether to enable or disable dynamic remote access */ enabled: Scalars['Boolean']['input']; - /** The URL for dynamic remote access */ - url: Scalars['URL']['input']; + /** The AccessURL Input for dynamic remote access */ + url: AccessUrlInput; }; export type Flash = Node & { __typename?: 'Flash'; guid: Scalars['String']['output']; - id: Scalars['ID']['output']; + id: Scalars['PrefixedID']['output']; product: Scalars['String']['output']; vendor: Scalars['String']['output']; }; @@ -622,7 +693,7 @@ export type Gpu = Node & { __typename?: 'Gpu'; blacklisted: Scalars['Boolean']['output']; class: Scalars['String']['output']; - id: Scalars['ID']['output']; + id: Scalars['PrefixedID']['output']; productid: Scalars['String']['output']; type: Scalars['String']['output']; typeid: Scalars['String']['output']; @@ -637,9 +708,9 @@ export type Info = Node & { cpu: InfoCpu; devices: Devices; display: Display; - id: Scalars['ID']['output']; + id: Scalars['PrefixedID']['output']; /** Machine ID */ - machineId?: Maybe; + machineId?: Maybe; memory: InfoMemory; os: Os; system: System; @@ -649,7 +720,7 @@ export type Info = Node & { export type InfoApps = Node & { __typename?: 'InfoApps'; - id: Scalars['ID']['output']; + id: Scalars['PrefixedID']['output']; /** How many docker containers are installed */ installed: Scalars['Int']['output']; /** How many docker containers are running */ @@ -663,7 +734,7 @@ export type InfoCpu = Node & { cores: Scalars['Int']['output']; family: Scalars['String']['output']; flags: Array; - id: Scalars['ID']['output']; + id: Scalars['PrefixedID']['output']; manufacturer: Scalars['String']['output']; model: Scalars['String']['output']; processors: Scalars['Int']['output']; @@ -684,7 +755,7 @@ export type InfoMemory = Node & { available: Scalars['Int']['output']; buffcache: Scalars['Int']['output']; free: Scalars['Int']['output']; - id: Scalars['ID']['output']; + id: Scalars['PrefixedID']['output']; layout: Array; max: Scalars['Int']['output']; swapfree: Scalars['Int']['output']; @@ -724,11 +795,12 @@ export type LogFileContent = { totalLines: Scalars['Int']['output']; }; -export type MemoryLayout = { +export type MemoryLayout = Node & { __typename?: 'MemoryLayout'; bank?: Maybe; clockSpeed?: Maybe; formFactor?: Maybe; + id: Scalars['PrefixedID']['output']; manufacturer?: Maybe; partNum?: Maybe; serialNum?: Maybe; @@ -762,7 +834,6 @@ export type Mutation = { archiveNotification: Notification; archiveNotifications: NotificationOverview; array: ArrayMutations; - cancelParityCheck: Scalars['JSON']['output']; connectSignIn: Scalars['Boolean']['output']; connectSignOut: Scalars['Boolean']['output']; createApiKey: ApiKeyWithSecret; @@ -773,33 +844,19 @@ export type Mutation = { deleteNotification: NotificationOverview; docker: DockerMutations; enableDynamicRemoteAccess: Scalars['Boolean']['output']; - /** Force stop a virtual machine */ - forceStopVm: Scalars['Boolean']['output']; - pauseParityCheck: Scalars['JSON']['output']; - /** Pause a virtual machine */ - pauseVm: Scalars['Boolean']['output']; - /** Reboot a virtual machine */ - rebootVm: Scalars['Boolean']['output']; + parityCheck: ParityCheckMutations; /** Reads each notification to recompute & update the overview. */ recalculateOverview: NotificationOverview; removeRoleFromApiKey: Scalars['Boolean']['output']; - /** Reset a virtual machine */ - resetVm: Scalars['Boolean']['output']; - resumeParityCheck: Scalars['JSON']['output']; - /** Resume a virtual machine */ - resumeVm: Scalars['Boolean']['output']; setAdditionalAllowedOrigins: Array; + setDemo: Scalars['String']['output']; setupRemoteAccess: Scalars['Boolean']['output']; - startParityCheck: Scalars['JSON']['output']; - /** Start a virtual machine */ - startVm: Scalars['Boolean']['output']; - /** Stop a virtual machine */ - stopVm: Scalars['Boolean']['output']; unarchiveAll: NotificationOverview; unarchiveNotifications: NotificationOverview; /** Marks a notification as unread. */ unreadNotification: Notification; updateApiSettings: ConnectSettingsValues; + vm: VmMutations; }; @@ -814,12 +871,12 @@ export type MutationArchiveAllArgs = { export type MutationArchiveNotificationArgs = { - id: Scalars['String']['input']; + id: Scalars['PrefixedID']['input']; }; export type MutationArchiveNotificationsArgs = { - ids: Array; + ids: Array; }; @@ -839,7 +896,7 @@ export type MutationCreateNotificationArgs = { export type MutationDeleteNotificationArgs = { - id: Scalars['String']['input']; + id: Scalars['PrefixedID']['input']; type: NotificationType; }; @@ -849,36 +906,11 @@ export type MutationEnableDynamicRemoteAccessArgs = { }; -export type MutationForceStopVmArgs = { - id: Scalars['String']['input']; -}; - - -export type MutationPauseVmArgs = { - id: Scalars['String']['input']; -}; - - -export type MutationRebootVmArgs = { - id: Scalars['String']['input']; -}; - - export type MutationRemoveRoleFromApiKeyArgs = { input: RemoveRoleFromApiKeyInput; }; -export type MutationResetVmArgs = { - id: Scalars['String']['input']; -}; - - -export type MutationResumeVmArgs = { - id: Scalars['String']['input']; -}; - - export type MutationSetAdditionalAllowedOriginsArgs = { input: AllowedOriginInput; }; @@ -889,33 +921,18 @@ export type MutationSetupRemoteAccessArgs = { }; -export type MutationStartParityCheckArgs = { - correct: Scalars['Boolean']['input']; -}; - - -export type MutationStartVmArgs = { - id: Scalars['String']['input']; -}; - - -export type MutationStopVmArgs = { - id: Scalars['String']['input']; -}; - - export type MutationUnarchiveAllArgs = { importance?: InputMaybe; }; export type MutationUnarchiveNotificationsArgs = { - ids: Array; + ids: Array; }; export type MutationUnreadNotificationArgs = { - id: Scalars['String']['input']; + id: Scalars['PrefixedID']['input']; }; @@ -926,18 +943,18 @@ export type MutationUpdateApiSettingsArgs = { export type Network = Node & { __typename?: 'Network'; accessUrls?: Maybe>; - id: Scalars['ID']['output']; + id: Scalars['PrefixedID']['output']; }; export type Node = { - id: Scalars['ID']['output']; + id: Scalars['PrefixedID']['output']; }; -export type Notification = { +export type Notification = Node & { __typename?: 'Notification'; description: Scalars['String']['output']; formattedTimestamp?: Maybe; - id: Scalars['ID']['output']; + id: Scalars['PrefixedID']['output']; importance: NotificationImportance; link?: Maybe; subject: Scalars['String']['output']; @@ -988,9 +1005,9 @@ export enum NotificationType { UNREAD = 'UNREAD' } -export type Notifications = { +export type Notifications = Node & { __typename?: 'Notifications'; - id: Scalars['ID']['output']; + id: Scalars['PrefixedID']['output']; list: Array; /** A cached overview of the notifications in the system & their severity. */ overview: NotificationOverview; @@ -1009,7 +1026,7 @@ export type Os = Node & { codepage?: Maybe; distro?: Maybe; hostname?: Maybe; - id: Scalars['ID']['output']; + id: Scalars['PrefixedID']['output']; kernel?: Maybe; logofile?: Maybe; platform?: Maybe; @@ -1047,11 +1064,30 @@ export type ParityCheck = { status?: Maybe; }; +/** Parity check related mutations, WIP, response types and functionaliy will change */ +export type ParityCheckMutations = { + __typename?: 'ParityCheckMutations'; + /** Cancel a parity check */ + cancel: Scalars['JSON']['output']; + /** Pause a parity check */ + pause: Scalars['JSON']['output']; + /** Resume a parity check */ + resume: Scalars['JSON']['output']; + /** Start a parity check */ + start: Scalars['JSON']['output']; +}; + + +/** Parity check related mutations, WIP, response types and functionaliy will change */ +export type ParityCheckMutationsStartArgs = { + correct: Scalars['Boolean']['input']; +}; + export type Pci = Node & { __typename?: 'Pci'; blacklisted?: Maybe; class?: Maybe; - id: Scalars['ID']['output']; + id: Scalars['PrefixedID']['output']; productid?: Maybe; productname?: Maybe; type?: Maybe; @@ -1066,11 +1102,11 @@ export type Permission = { resource: Resource; }; -export type ProfileModel = { +export type ProfileModel = Node & { __typename?: 'ProfileModel'; avatar: Scalars['String']['output']; + id: Scalars['PrefixedID']['output']; url: Scalars['String']['output']; - userId?: Maybe; username: Scalars['String']['output']; }; @@ -1088,6 +1124,8 @@ export type Query = { docker: Docker; extraAllowedOrigins: Array; flash: Flash; + getDemo: Scalars['String']['output']; + health: Scalars['String']['output']; info: Info; logFile: LogFileContent; logFiles: Array; @@ -1105,17 +1143,18 @@ export type Query = { services: Array; shares: Array; vars: Vars; + /** Get information about all VMs on the system */ vms: Vms; }; export type QueryApiKeyArgs = { - id: Scalars['String']['input']; + id: Scalars['PrefixedID']['input']; }; export type QueryDiskArgs = { - id: Scalars['String']['input']; + id: Scalars['PrefixedID']['input']; }; @@ -1125,10 +1164,10 @@ export type QueryLogFileArgs = { startLine?: InputMaybe; }; -export type Registration = { +export type Registration = Node & { __typename?: 'Registration'; expiration?: Maybe; - guid?: Maybe; + id: Scalars['PrefixedID']['output']; keyFile?: Maybe; state?: Maybe; type?: Maybe; @@ -1182,7 +1221,7 @@ export type RemoteAccess = { }; export type RemoveRoleFromApiKeyInput = { - apiKeyId: Scalars['ID']['input']; + apiKeyId: Scalars['PrefixedID']['input']; role: Role; }; @@ -1225,10 +1264,11 @@ export enum Role { GUEST = 'GUEST' } -export type Server = { +export type Server = Node & { __typename?: 'Server'; apikey: Scalars['String']['output']; guid: Scalars['String']['output']; + id: Scalars['PrefixedID']['output']; lanip: Scalars['String']['output']; localurl: Scalars['String']['output']; name: Scalars['String']['output']; @@ -1246,7 +1286,7 @@ export enum ServerStatus { export type Service = Node & { __typename?: 'Service'; - id: Scalars['ID']['output']; + id: Scalars['PrefixedID']['output']; name?: Maybe; online?: Maybe; uptime?: Maybe; @@ -1280,7 +1320,7 @@ export type Share = Node & { floor?: Maybe; /** (KB) Free space */ free?: Maybe; - id: Scalars['ID']['output']; + id: Scalars['PrefixedID']['output']; /** Disks that are included in this share */ include?: Maybe>; /** LUKS status */ @@ -1318,7 +1358,7 @@ export type SubscriptionLogFileArgs = { export type System = Node & { __typename?: 'System'; - id: Scalars['ID']['output']; + id: Scalars['PrefixedID']['output']; manufacturer?: Maybe; model?: Maybe; serial?: Maybe; @@ -1357,13 +1397,9 @@ export type UnraidArray = Node & { capacity: ArrayCapacity; /** Data disks in the current array */ disks: Array; - id: Scalars['ID']['output']; + id: Scalars['PrefixedID']['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; }; @@ -1373,18 +1409,17 @@ export type Uptime = { timestamp?: Maybe; }; -export type Usb = { +export type Usb = Node & { __typename?: 'Usb'; - id: Scalars['ID']['output']; + id: Scalars['PrefixedID']['output']; name?: Maybe; }; -export type UserAccount = { +export type UserAccount = Node & { __typename?: 'UserAccount'; /** A description of the user */ description: Scalars['String']['output']; - /** A unique identifier for the user */ - id: Scalars['ID']['output']; + id: Scalars['PrefixedID']['output']; /** The name of the user */ name: Scalars['String']['output']; /** The permissions of the user */ @@ -1427,7 +1462,7 @@ export type Vars = Node & { fuseRememberDefault?: Maybe; fuseRememberStatus?: Maybe; hideDotFiles?: Maybe; - id: Scalars['ID']['output']; + id: Scalars['PrefixedID']['output']; joinStatus?: Maybe; localMaster?: Maybe; localTld?: Maybe; @@ -1565,7 +1600,7 @@ export type Versions = Node & { git?: Maybe; grunt?: Maybe; gulp?: Maybe; - id: Scalars['ID']['output']; + id: Scalars['PrefixedID']['output']; kernel?: Maybe; mongodb?: Maybe; mysql?: Maybe; @@ -1588,13 +1623,72 @@ export type Versions = Node & { yarn?: Maybe; }; -export type VmDomain = { +export type VmDomain = Node & { __typename?: 'VmDomain'; + /** The unique identifier for the vm (uuid) */ + id: Scalars['PrefixedID']['output']; /** A friendly name for the vm */ name?: Maybe; /** Current domain vm state */ state: VmState; - uuid: Scalars['ID']['output']; + /** + * The UUID of the vm + * @deprecated Use id instead + */ + uuid?: Maybe; +}; + +export type VmMutations = { + __typename?: 'VmMutations'; + /** Force stop a virtual machine */ + forceStop: Scalars['Boolean']['output']; + /** Pause a virtual machine */ + pause: Scalars['Boolean']['output']; + /** Reboot a virtual machine */ + reboot: Scalars['Boolean']['output']; + /** Reset a virtual machine */ + reset: Scalars['Boolean']['output']; + /** Resume a virtual machine */ + resume: Scalars['Boolean']['output']; + /** Start a virtual machine */ + start: Scalars['Boolean']['output']; + /** Stop a virtual machine */ + stop: Scalars['Boolean']['output']; +}; + + +export type VmMutationsForceStopArgs = { + id: Scalars['PrefixedID']['input']; +}; + + +export type VmMutationsPauseArgs = { + id: Scalars['PrefixedID']['input']; +}; + + +export type VmMutationsRebootArgs = { + id: Scalars['PrefixedID']['input']; +}; + + +export type VmMutationsResetArgs = { + id: Scalars['PrefixedID']['input']; +}; + + +export type VmMutationsResumeArgs = { + id: Scalars['PrefixedID']['input']; +}; + + +export type VmMutationsStartArgs = { + id: Scalars['PrefixedID']['input']; +}; + + +export type VmMutationsStopArgs = { + id: Scalars['PrefixedID']['input']; }; /** The state of a virtual machine */ @@ -1609,10 +1703,11 @@ export enum VmState { SHUTOFF = 'SHUTOFF' } -export type Vms = { +export type Vms = Node & { __typename?: 'Vms'; + domain?: Maybe>; domains?: Maybe>; - id: Scalars['ID']['output']; + id: Scalars['PrefixedID']['output']; }; export enum WanAccessType { @@ -1685,7 +1780,7 @@ export type NotificationsQuery = { __typename?: 'Query', notifications: { __type )> } }; export type ArchiveNotificationMutationVariables = Exact<{ - id: Scalars['String']['input']; + id: Scalars['PrefixedID']['input']; }>; @@ -1700,7 +1795,7 @@ export type ArchiveAllNotificationsMutationVariables = Exact<{ [key: string]: ne export type ArchiveAllNotificationsMutation = { __typename?: 'Mutation', archiveAll: { __typename?: 'NotificationOverview', unread: { __typename?: 'NotificationCounts', total: number }, archive: { __typename?: 'NotificationCounts', info: number, warning: number, alert: number, total: number } } }; export type DeleteNotificationMutationVariables = Exact<{ - id: Scalars['String']['input']; + id: Scalars['PrefixedID']['input']; type: NotificationType; }>; @@ -1802,9 +1897,9 @@ export const LogFilesDocument = {"kind":"Document","definitions":[{"kind":"Opera export const LogFileContentDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"LogFileContent"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"path"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"lines"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"Int"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"startLine"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"Int"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"logFile"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"path"},"value":{"kind":"Variable","name":{"kind":"Name","value":"path"}}},{"kind":"Argument","name":{"kind":"Name","value":"lines"},"value":{"kind":"Variable","name":{"kind":"Name","value":"lines"}}},{"kind":"Argument","name":{"kind":"Name","value":"startLine"},"value":{"kind":"Variable","name":{"kind":"Name","value":"startLine"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"path"}},{"kind":"Field","name":{"kind":"Name","value":"content"}},{"kind":"Field","name":{"kind":"Name","value":"totalLines"}},{"kind":"Field","name":{"kind":"Name","value":"startLine"}}]}}]}}]} as unknown as DocumentNode; export const LogFileSubscriptionDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"subscription","name":{"kind":"Name","value":"LogFileSubscription"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"path"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"logFile"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"path"},"value":{"kind":"Variable","name":{"kind":"Name","value":"path"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"path"}},{"kind":"Field","name":{"kind":"Name","value":"content"}},{"kind":"Field","name":{"kind":"Name","value":"totalLines"}}]}}]}}]} as unknown as DocumentNode; export const NotificationsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"Notifications"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"filter"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"NotificationFilter"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"notifications"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"list"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"filter"},"value":{"kind":"Variable","name":{"kind":"Name","value":"filter"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"NotificationFragment"}}]}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"NotificationFragment"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Notification"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"subject"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"importance"}},{"kind":"Field","name":{"kind":"Name","value":"link"}},{"kind":"Field","name":{"kind":"Name","value":"type"}},{"kind":"Field","name":{"kind":"Name","value":"timestamp"}},{"kind":"Field","name":{"kind":"Name","value":"formattedTimestamp"}}]}}]} as unknown as DocumentNode; -export const ArchiveNotificationDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"ArchiveNotification"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"id"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"archiveNotification"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"id"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"NotificationFragment"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"NotificationFragment"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Notification"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"subject"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"importance"}},{"kind":"Field","name":{"kind":"Name","value":"link"}},{"kind":"Field","name":{"kind":"Name","value":"type"}},{"kind":"Field","name":{"kind":"Name","value":"timestamp"}},{"kind":"Field","name":{"kind":"Name","value":"formattedTimestamp"}}]}}]} as unknown as DocumentNode; +export const ArchiveNotificationDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"ArchiveNotification"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"id"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"PrefixedID"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"archiveNotification"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"id"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"NotificationFragment"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"NotificationFragment"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Notification"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"subject"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"importance"}},{"kind":"Field","name":{"kind":"Name","value":"link"}},{"kind":"Field","name":{"kind":"Name","value":"type"}},{"kind":"Field","name":{"kind":"Name","value":"timestamp"}},{"kind":"Field","name":{"kind":"Name","value":"formattedTimestamp"}}]}}]} as unknown as DocumentNode; export const ArchiveAllNotificationsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"ArchiveAllNotifications"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"archiveAll"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"unread"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"total"}}]}},{"kind":"Field","name":{"kind":"Name","value":"archive"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"info"}},{"kind":"Field","name":{"kind":"Name","value":"warning"}},{"kind":"Field","name":{"kind":"Name","value":"alert"}},{"kind":"Field","name":{"kind":"Name","value":"total"}}]}}]}}]}}]} as unknown as DocumentNode; -export const DeleteNotificationDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"DeleteNotification"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"id"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"type"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"NotificationType"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"deleteNotification"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"id"}}},{"kind":"Argument","name":{"kind":"Name","value":"type"},"value":{"kind":"Variable","name":{"kind":"Name","value":"type"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"archive"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"total"}}]}}]}}]}}]} as unknown as DocumentNode; +export const DeleteNotificationDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"DeleteNotification"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"id"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"PrefixedID"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"type"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"NotificationType"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"deleteNotification"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"id"}}},{"kind":"Argument","name":{"kind":"Name","value":"type"},"value":{"kind":"Variable","name":{"kind":"Name","value":"type"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"archive"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"total"}}]}}]}}]}}]} as unknown as DocumentNode; export const DeleteAllNotificationsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"DeleteAllNotifications"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"deleteArchivedNotifications"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"archive"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"total"}}]}},{"kind":"Field","name":{"kind":"Name","value":"unread"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"total"}}]}}]}}]}}]} as unknown as DocumentNode; export const OverviewDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"Overview"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"notifications"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"overview"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"unread"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"info"}},{"kind":"Field","name":{"kind":"Name","value":"warning"}},{"kind":"Field","name":{"kind":"Name","value":"alert"}},{"kind":"Field","name":{"kind":"Name","value":"total"}}]}},{"kind":"Field","name":{"kind":"Name","value":"archive"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"total"}}]}}]}}]}}]}}]} as unknown as DocumentNode; export const RecomputeOverviewDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"RecomputeOverview"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"recalculateOverview"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"archive"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"NotificationCountFragment"}}]}},{"kind":"Field","name":{"kind":"Name","value":"unread"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"NotificationCountFragment"}}]}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"NotificationCountFragment"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"NotificationCounts"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"total"}},{"kind":"Field","name":{"kind":"Name","value":"info"}},{"kind":"Field","name":{"kind":"Name","value":"warning"}},{"kind":"Field","name":{"kind":"Name","value":"alert"}}]}}]} as unknown as DocumentNode;