diff --git a/api/dev/Unraid.net/myservers.cfg b/api/dev/Unraid.net/myservers.cfg index 1c664dad2..0618a5257 100644 --- a/api/dev/Unraid.net/myservers.cfg +++ b/api/dev/Unraid.net/myservers.cfg @@ -1,5 +1,5 @@ [api] -version="4.4.1" +version="4.1.3" extraOrigins="https://google.com,https://test.com" [local] sandbox="yes" diff --git a/api/src/core/modules/docker/get-docker-containers.ts b/api/src/core/modules/docker/get-docker-containers.ts index a5d4c5245..4c78056b6 100644 --- a/api/src/core/modules/docker/get-docker-containers.ts +++ b/api/src/core/modules/docker/get-docker-containers.ts @@ -1,7 +1,6 @@ import fs from 'fs'; import camelCaseKeys from 'camelcase-keys'; -import { ContainerInfo } from 'dockerode'; import type { ContainerPort, Docker, DockerContainer } from '@app/graphql/generated/api/types.js'; import { dockerLogger } from '@app/core/log.js'; @@ -11,13 +10,16 @@ import { ContainerPortType, ContainerState } from '@app/graphql/generated/api/ty import { getters, store } from '@app/store/index.js'; import { updateDockerState } from '@app/store/modules/docker.js'; +export interface ContainerListingOptions { + useCache?: boolean; +} + /** * Get all Docker containers. * @returns All the in/active Docker containers on the system. */ - export const getDockerContainers = async ( - { useCache } = { useCache: true } + { useCache }: ContainerListingOptions = { useCache: true } ): Promise> => { const dockerState = getters.docker(); if (useCache && dockerState.containers) { diff --git a/api/src/graphql/generated/api/operations.ts b/api/src/graphql/generated/api/operations.ts index cee791d49..0b733f36b 100755 --- a/api/src/graphql/generated/api/operations.ts +++ b/api/src/graphql/generated/api/operations.ts @@ -2,7 +2,7 @@ import * as Types from '@app/graphql/generated/api/types.js'; import { z } from 'zod' -import { AccessUrl, AccessUrlInput, AddPermissionInput, AddRoleForApiKeyInput, AddRoleForUserInput, AllowedOriginInput, ApiKey, ApiKeyResponse, ApiKeyWithSecret, ApiSettingsInput, ArrayType, ArrayCapacity, ArrayDisk, ArrayDiskFsColor, ArrayDiskInput, ArrayDiskStatus, ArrayDiskType, ArrayMutations, ArrayMutationsaddDiskToArrayArgs, ArrayMutationsclearArrayDiskStatisticsArgs, ArrayMutationsmountArrayDiskArgs, ArrayMutationsremoveDiskFromArrayArgs, ArrayMutationssetStateArgs, ArrayMutationsunmountArrayDiskArgs, ArrayPendingState, ArrayState, ArrayStateInput, ArrayStateInputState, Baseboard, Capacity, Case, Cloud, CloudResponse, Config, ConfigErrorState, Connect, ConnectSettings, ConnectSettingsValues, ConnectSignInInput, ConnectUserInfoInput, ContainerHostConfig, ContainerMount, ContainerPort, ContainerPortType, ContainerState, CreateApiKeyInput, Devices, Disk, DiskFsType, DiskInterfaceType, DiskPartition, DiskSmartStatus, Display, Docker, DockerContainer, DockerNetwork, DynamicRemoteAccessStatus, DynamicRemoteAccessType, EnableDynamicRemoteAccessInput, Flash, Gpu, Importance, Info, InfoApps, InfoCpu, InfoMemory, KeyFile, LogFile, LogFileContent, Me, MemoryFormFactor, MemoryLayout, MemoryType, MinigraphStatus, MinigraphqlResponse, Mount, Network, Node, Notification, NotificationCounts, NotificationData, NotificationFilter, NotificationOverview, NotificationType, Notifications, NotificationslistArgs, Os, Owner, ParityCheck, Partition, Pci, Permission, ProfileModel, Registration, RegistrationState, RelayResponse, RemoteAccess, RemoveRoleFromApiKeyInput, Resource, Role, Server, ServerStatus, Service, SetupRemoteAccessInput, Share, System, Temperature, Theme, URL_TYPE, UnassignedDevice, Uptime, Usb, User, UserAccount, Vars, Versions, VmDomain, VmState, Vms, WAN_ACCESS_TYPE, WAN_FORWARD_TYPE, Welcome, addUserInput, deleteUserInput, mdState, registrationType, usersInput } from '@app/graphql/generated/api/types.js' +import { AccessUrl, AccessUrlInput, AddPermissionInput, AddRoleForApiKeyInput, AddRoleForUserInput, AllowedOriginInput, ApiKey, ApiKeyResponse, ApiKeyWithSecret, ApiSettingsInput, ArrayType, ArrayCapacity, ArrayDisk, ArrayDiskFsColor, ArrayDiskInput, ArrayDiskStatus, ArrayDiskType, ArrayMutations, ArrayMutationsaddDiskToArrayArgs, ArrayMutationsclearArrayDiskStatisticsArgs, ArrayMutationsmountArrayDiskArgs, ArrayMutationsremoveDiskFromArrayArgs, ArrayMutationssetStateArgs, ArrayMutationsunmountArrayDiskArgs, ArrayPendingState, ArrayState, ArrayStateInput, ArrayStateInputState, Baseboard, Capacity, Case, Cloud, CloudResponse, Config, ConfigErrorState, Connect, ConnectSettings, ConnectSettingsValues, ConnectSignInInput, ConnectUserInfoInput, ContainerHostConfig, ContainerMount, ContainerPort, ContainerPortType, ContainerState, CreateApiKeyInput, Devices, Disk, DiskFsType, DiskInterfaceType, DiskPartition, DiskSmartStatus, Display, Docker, DockerContainer, DockerMutations, DockerMutationsstartContainerArgs, DockerMutationsstopContainerArgs, DockerNetwork, DynamicRemoteAccessStatus, DynamicRemoteAccessType, EnableDynamicRemoteAccessInput, Flash, Gpu, Importance, Info, InfoApps, InfoCpu, InfoMemory, KeyFile, LogFile, LogFileContent, Me, MemoryFormFactor, MemoryLayout, MemoryType, MinigraphStatus, MinigraphqlResponse, Mount, Network, Node, Notification, NotificationCounts, NotificationData, NotificationFilter, NotificationOverview, NotificationType, Notifications, NotificationslistArgs, Os, Owner, ParityCheck, Partition, Pci, Permission, ProfileModel, Registration, RegistrationState, RelayResponse, RemoteAccess, RemoveRoleFromApiKeyInput, Resource, Role, Server, ServerStatus, Service, SetupRemoteAccessInput, Share, System, Temperature, Theme, URL_TYPE, UnassignedDevice, Uptime, Usb, User, UserAccount, Vars, Versions, VmDomain, VmState, Vms, WAN_ACCESS_TYPE, WAN_FORWARD_TYPE, Welcome, addUserInput, deleteUserInput, mdState, registrationType, usersInput } from '@app/graphql/generated/api/types.js' import { TypedDocumentNode as DocumentNode } from '@graphql-typed-document-node/core'; type Properties = Required<{ @@ -501,6 +501,7 @@ export function DockerSchema(): z.ZodObject> { __typename: z.literal('Docker').optional(), containers: z.array(DockerContainerSchema()).nullish(), id: z.string(), + mutations: DockerMutationsSchema(), networks: z.array(DockerNetworkSchema()).nullish() }) } @@ -526,6 +527,26 @@ export function DockerContainerSchema(): z.ZodObject }) } +export function DockerMutationsSchema(): z.ZodObject> { + return z.object({ + __typename: z.literal('DockerMutations').optional(), + startContainer: DockerContainerSchema(), + stopContainer: DockerContainerSchema() + }) +} + +export function DockerMutationsstartContainerArgsSchema(): z.ZodObject> { + return z.object({ + id: z.string() + }) +} + +export function DockerMutationsstopContainerArgsSchema(): z.ZodObject> { + return z.object({ + id: z.string() + }) +} + export function DockerNetworkSchema(): z.ZodObject> { return z.object({ __typename: z.literal('DockerNetwork').optional(), diff --git a/api/src/graphql/generated/api/types.ts b/api/src/graphql/generated/api/types.ts index 21c142754..a12b6fde5 100644 --- a/api/src/graphql/generated/api/types.ts +++ b/api/src/graphql/generated/api/types.ts @@ -555,6 +555,7 @@ export type Docker = Node & { __typename?: 'Docker'; containers?: Maybe>; id: Scalars['ID']['output']; + mutations: DockerMutations; networks?: Maybe>; }; @@ -578,6 +579,22 @@ export type DockerContainer = { status: Scalars['String']['output']; }; +export type DockerMutations = { + __typename?: 'DockerMutations'; + startContainer: DockerContainer; + stopContainer: DockerContainer; +}; + + +export type DockerMutationsstartContainerArgs = { + id: Scalars['ID']['input']; +}; + + +export type DockerMutationsstopContainerArgs = { + id: Scalars['ID']['input']; +}; + export type DockerNetwork = { __typename?: 'DockerNetwork'; attachable: Scalars['Boolean']['output']; @@ -2002,6 +2019,7 @@ export type ResolversTypes = ResolversObject<{ Display: ResolverTypeWrapper; Docker: ResolverTypeWrapper; DockerContainer: ResolverTypeWrapper; + DockerMutations: ResolverTypeWrapper; DockerNetwork: ResolverTypeWrapper; DynamicRemoteAccessStatus: ResolverTypeWrapper; DynamicRemoteAccessType: DynamicRemoteAccessType; @@ -2128,6 +2146,7 @@ export type ResolversParentTypes = ResolversObject<{ Display: Display; Docker: Docker; DockerContainer: DockerContainer; + DockerMutations: DockerMutations; DockerNetwork: DockerNetwork; DynamicRemoteAccessStatus: DynamicRemoteAccessStatus; EnableDynamicRemoteAccessInput: EnableDynamicRemoteAccessInput; @@ -2452,6 +2471,7 @@ export type DisplayResolvers = ResolversObject<{ containers?: Resolver>, ParentType, ContextType>; id?: Resolver; + mutations?: Resolver; networks?: Resolver>, ParentType, ContextType>; __isTypeOf?: IsTypeOfResolverFn; }>; @@ -2475,6 +2495,12 @@ export type DockerContainerResolvers; }>; +export type DockerMutationsResolvers = ResolversObject<{ + startContainer?: Resolver>; + stopContainer?: Resolver>; + __isTypeOf?: IsTypeOfResolverFn; +}>; + export type DockerNetworkResolvers = ResolversObject<{ attachable?: Resolver; configFrom?: Resolver, ParentType, ContextType>; @@ -3322,6 +3348,7 @@ export type Resolvers = ResolversObject<{ Display?: DisplayResolvers; Docker?: DockerResolvers; DockerContainer?: DockerContainerResolvers; + DockerMutations?: DockerMutationsResolvers; DockerNetwork?: DockerNetworkResolvers; DynamicRemoteAccessStatus?: DynamicRemoteAccessStatusResolvers; Flash?: FlashResolvers; diff --git a/api/src/graphql/schema/types/docker/docker.graphql b/api/src/graphql/schema/types/docker/docker.graphql index f2109fb25..6f982a8ac 100644 --- a/api/src/graphql/schema/types/docker/docker.graphql +++ b/api/src/graphql/schema/types/docker/docker.graphql @@ -6,4 +6,13 @@ type Docker implements Node { type Query { docker: Docker! +} + +type DockerMutations { + startContainer(id: ID!): DockerContainer! + stopContainer(id: ID!): DockerContainer! +} + +extend type Docker { + mutations: DockerMutations! } \ No newline at end of file diff --git a/api/src/unraid-api/graph/resolvers/docker/docker.mutations.resolver.spec.ts b/api/src/unraid-api/graph/resolvers/docker/docker.mutations.resolver.spec.ts new file mode 100644 index 000000000..cf60a8bee --- /dev/null +++ b/api/src/unraid-api/graph/resolvers/docker/docker.mutations.resolver.spec.ts @@ -0,0 +1,74 @@ +import type { TestingModule } from '@nestjs/testing'; +import { Test } from '@nestjs/testing'; + +import { beforeEach, describe, expect, it, vi } from 'vitest'; + +import type { DockerContainer } from '@app/graphql/generated/api/types.js'; +import { ContainerState } from '@app/graphql/generated/api/types.js'; +import { DockerMutationsResolver } from '@app/unraid-api/graph/resolvers/docker/docker.mutations.resolver.js'; +import { DockerService } from '@app/unraid-api/graph/resolvers/docker/docker.service.js'; + +describe('DockerMutationsResolver', () => { + let resolver: DockerMutationsResolver; + let dockerService: DockerService; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ + DockerMutationsResolver, + { + provide: DockerService, + useValue: { + startContainer: vi.fn(), + stopContainer: vi.fn(), + }, + }, + ], + }).compile(); + + resolver = module.get(DockerMutationsResolver); + dockerService = module.get(DockerService); + }); + + it('should be defined', () => { + expect(resolver).toBeDefined(); + }); + + it('should start container', async () => { + const mockContainer: DockerContainer = { + id: '1', + autoStart: false, + command: 'test', + created: 1234567890, + image: 'test-image', + imageId: 'test-image-id', + ports: [], + state: ContainerState.RUNNING, + status: 'Up 2 hours', + }; + vi.mocked(dockerService.startContainer).mockResolvedValue(mockContainer); + + const result = await resolver.startContainer('1'); + expect(result).toEqual(mockContainer); + expect(dockerService.startContainer).toHaveBeenCalledWith('1'); + }); + + it('should stop container', async () => { + const mockContainer: DockerContainer = { + id: '1', + autoStart: false, + command: 'test', + created: 1234567890, + image: 'test-image', + imageId: 'test-image-id', + ports: [], + state: ContainerState.EXITED, + status: 'Exited', + }; + vi.mocked(dockerService.stopContainer).mockResolvedValue(mockContainer); + + const result = await resolver.stopContainer('1'); + expect(result).toEqual(mockContainer); + expect(dockerService.stopContainer).toHaveBeenCalledWith('1'); + }); +}); 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 new file mode 100644 index 000000000..1fbef2d24 --- /dev/null +++ b/api/src/unraid-api/graph/resolvers/docker/docker.mutations.resolver.ts @@ -0,0 +1,31 @@ +import { Args, ResolveField, Resolver } from '@nestjs/graphql'; + +import { AuthActionVerb, AuthPossession, UsePermissions } from 'nest-authz'; + +import { Resource } from '@app/graphql/generated/api/types.js'; +import { DockerService } from '@app/unraid-api/graph/resolvers/docker/docker.service.js'; + +@Resolver('DockerMutations') +export class DockerMutationsResolver { + constructor(private readonly dockerService: DockerService) {} + + @ResolveField('startContainer') + @UsePermissions({ + action: AuthActionVerb.UPDATE, + resource: Resource.DOCKER, + possession: AuthPossession.ANY, + }) + public async startContainer(@Args('id') id: string) { + return this.dockerService.startContainer(id); + } + + @ResolveField('stopContainer') + @UsePermissions({ + action: AuthActionVerb.UPDATE, + resource: Resource.DOCKER, + possession: AuthPossession.ANY, + }) + public async stopContainer(@Args('id') id: string) { + return this.dockerService.stopContainer(id); + } +} diff --git a/api/src/unraid-api/graph/resolvers/docker/docker.resolver.spec.ts b/api/src/unraid-api/graph/resolvers/docker/docker.resolver.spec.ts index 7d9df30d8..f4621ea8b 100644 --- a/api/src/unraid-api/graph/resolvers/docker/docker.resolver.spec.ts +++ b/api/src/unraid-api/graph/resolvers/docker/docker.resolver.spec.ts @@ -1,22 +1,77 @@ import type { TestingModule } from '@nestjs/testing'; import { Test } from '@nestjs/testing'; -import { beforeEach, describe, expect, it } from 'vitest'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; +import type { DockerContainer } from '@app/graphql/generated/api/types.js'; +import { ContainerState } from '@app/graphql/generated/api/types.js'; import { DockerResolver } from '@app/unraid-api/graph/resolvers/docker/docker.resolver.js'; +import { DockerService } from '@app/unraid-api/graph/resolvers/docker/docker.service.js'; describe('DockerResolver', () => { let resolver: DockerResolver; + let dockerService: DockerService; beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ - providers: [DockerResolver], + providers: [ + DockerResolver, + { + provide: DockerService, + useValue: { + getContainers: vi.fn(), + }, + }, + ], }).compile(); resolver = module.get(DockerResolver); + dockerService = module.get(DockerService); }); it('should be defined', () => { expect(resolver).toBeDefined(); }); + + it('should return docker object with id', () => { + const result = resolver.docker(); + expect(result).toEqual({ id: 'docker' }); + }); + + it('should return containers from service', async () => { + const mockContainers: DockerContainer[] = [ + { + id: '1', + autoStart: false, + command: 'test', + created: 1234567890, + image: 'test-image', + imageId: 'test-image-id', + ports: [], + state: ContainerState.EXITED, + status: 'Exited', + }, + { + id: '2', + autoStart: true, + command: 'test2', + created: 1234567891, + image: 'test-image2', + imageId: 'test-image-id2', + ports: [], + state: ContainerState.RUNNING, + status: 'Up 2 hours', + }, + ]; + vi.mocked(dockerService.getContainers).mockResolvedValue(mockContainers); + + const result = await resolver.containers(); + expect(result).toEqual(mockContainers); + expect(dockerService.getContainers).toHaveBeenCalledWith({ useCache: false }); + }); + + it('should return mutations object with id', () => { + const result = resolver.mutations(); + expect(result).toEqual({ id: 'docker-mutations' }); + }); }); diff --git a/api/src/unraid-api/graph/resolvers/docker/docker.resolver.ts b/api/src/unraid-api/graph/resolvers/docker/docker.resolver.ts index 3a347f73b..e2871f231 100644 --- a/api/src/unraid-api/graph/resolvers/docker/docker.resolver.ts +++ b/api/src/unraid-api/graph/resolvers/docker/docker.resolver.ts @@ -3,9 +3,12 @@ import { Query, ResolveField, Resolver } from '@nestjs/graphql'; import { AuthActionVerb, AuthPossession, UsePermissions } from 'nest-authz'; import { Resource } from '@app/graphql/generated/api/types.js'; +import { DockerService } from '@app/unraid-api/graph/resolvers/docker/docker.service.js'; @Resolver('Docker') export class DockerResolver { + constructor(private readonly dockerService: DockerService) {} + @UsePermissions({ action: AuthActionVerb.READ, resource: Resource.DOCKER, @@ -25,10 +28,13 @@ export class DockerResolver { }) @ResolveField() public async containers() { - const { getDockerContainers } = await import( - '@app/core/modules/docker/get-docker-containers.js' - ); + return this.dockerService.getContainers({ useCache: false }); + } - return getDockerContainers({ useCache: false }); + @ResolveField() + public mutations() { + return { + id: 'docker-mutations', + }; } } diff --git a/api/src/unraid-api/graph/resolvers/docker/docker.service.spec.ts b/api/src/unraid-api/graph/resolvers/docker/docker.service.spec.ts new file mode 100644 index 000000000..3c693cead --- /dev/null +++ b/api/src/unraid-api/graph/resolvers/docker/docker.service.spec.ts @@ -0,0 +1,125 @@ +import type { TestingModule } from '@nestjs/testing'; +import { Test } from '@nestjs/testing'; + +import { beforeEach, describe, expect, it, vi } from 'vitest'; + +import type { DockerContainer } from '@app/graphql/generated/api/types.js'; +import { getDockerContainers } from '@app/core/modules/docker/get-docker-containers.js'; +import { docker } from '@app/core/utils/clients/docker.js'; +import { ContainerState } from '@app/graphql/generated/api/types.js'; +import { DockerService } from '@app/unraid-api/graph/resolvers/docker/docker.service.js'; + +vi.mock('@app/core/utils/clients/docker.js', () => ({ + docker: { + getContainer: vi.fn(), + listContainers: vi.fn(), + }, +})); + +vi.mock('@app/core/modules/docker/get-docker-containers.js', () => ({ + getDockerContainers: vi.fn(), +})); + +describe('DockerService', () => { + let service: DockerService; + let mockContainer: any; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [DockerService], + }).compile(); + + service = module.get(DockerService); + + mockContainer = { + start: vi.fn(), + stop: vi.fn(), + }; + vi.mocked(docker.getContainer).mockReturnValue(mockContainer as any); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); + + it('should get containers', async () => { + const mockContainers: DockerContainer[] = [ + { + id: '1', + autoStart: false, + command: 'test', + created: 1234567890, + image: 'test-image', + imageId: 'test-image-id', + ports: [], + state: ContainerState.EXITED, + status: 'Exited', + }, + ]; + vi.mocked(getDockerContainers).mockResolvedValue(mockContainers); + + const result = await service.getContainers({ useCache: false }); + expect(result).toEqual(mockContainers); + expect(getDockerContainers).toHaveBeenCalledWith({ useCache: false }); + }); + + it('should start container', async () => { + const mockContainers: DockerContainer[] = [ + { + id: '1', + autoStart: false, + command: 'test', + created: 1234567890, + image: 'test-image', + imageId: 'test-image-id', + ports: [], + state: ContainerState.RUNNING, + status: 'Up 2 hours', + }, + ]; + vi.mocked(getDockerContainers).mockResolvedValue(mockContainers); + + const result = await service.startContainer('1'); + expect(result).toEqual(mockContainers[0]); + expect(docker.getContainer).toHaveBeenCalledWith('1'); + expect(mockContainer.start).toHaveBeenCalled(); + expect(getDockerContainers).toHaveBeenCalledWith({ useCache: false }); + }); + + it('should stop container', async () => { + const mockContainers: DockerContainer[] = [ + { + id: '1', + autoStart: false, + command: 'test', + created: 1234567890, + image: 'test-image', + imageId: 'test-image-id', + ports: [], + state: ContainerState.EXITED, + status: 'Exited', + }, + ]; + vi.mocked(getDockerContainers).mockResolvedValue(mockContainers); + + const result = await service.stopContainer('1'); + expect(result).toEqual(mockContainers[0]); + expect(docker.getContainer).toHaveBeenCalledWith('1'); + expect(mockContainer.stop).toHaveBeenCalled(); + expect(getDockerContainers).toHaveBeenCalledWith({ useCache: false }); + }); + + it('should throw error if container not found after start', async () => { + vi.mocked(getDockerContainers).mockResolvedValue([]); + + await expect(service.startContainer('1')).rejects.toThrow( + 'Container 1 not found after starting' + ); + }); + + it('should throw error if container not found after stop', async () => { + vi.mocked(getDockerContainers).mockResolvedValue([]); + + await expect(service.stopContainer('1')).rejects.toThrow('Container 1 not found after stopping'); + }); +}); diff --git a/api/src/unraid-api/graph/resolvers/docker/docker.service.ts b/api/src/unraid-api/graph/resolvers/docker/docker.service.ts new file mode 100644 index 000000000..5c5ec46ee --- /dev/null +++ b/api/src/unraid-api/graph/resolvers/docker/docker.service.ts @@ -0,0 +1,37 @@ +import { Injectable } from '@nestjs/common'; + +import type { DockerContainer } from '@app/graphql/generated/api/types.js'; +import { + ContainerListingOptions, + getDockerContainers, +} from '@app/core/modules/docker/get-docker-containers.js'; +import { docker } from '@app/core/utils/clients/docker.js'; + +@Injectable() +export class DockerService { + public async getContainers({ useCache }: ContainerListingOptions): Promise { + return getDockerContainers({ useCache }); + } + + public async startContainer(id: string): Promise { + const container = docker.getContainer(id); + await container.start(); + const containers = await this.getContainers({ useCache: false }); + const updatedContainer = containers.find((c) => c.id === id); + if (!updatedContainer) { + throw new Error(`Container ${id} not found after starting`); + } + return updatedContainer; + } + + public async stopContainer(id: string): Promise { + const container = docker.getContainer(id); + await container.stop(); + const containers = await this.getContainers({ useCache: false }); + const updatedContainer = containers.find((c) => c.id === id); + if (!updatedContainer) { + throw new Error(`Container ${id} not found after stopping`); + } + return updatedContainer; + } +} diff --git a/api/src/unraid-api/graph/resolvers/resolvers.module.ts b/api/src/unraid-api/graph/resolvers/resolvers.module.ts index 31f47ac19..b182c234e 100644 --- a/api/src/unraid-api/graph/resolvers/resolvers.module.ts +++ b/api/src/unraid-api/graph/resolvers/resolvers.module.ts @@ -10,7 +10,9 @@ import { CloudResolver } from '@app/unraid-api/graph/resolvers/cloud/cloud.resol import { ConfigResolver } from '@app/unraid-api/graph/resolvers/config/config.resolver.js'; import { DisksResolver } from '@app/unraid-api/graph/resolvers/disks/disks.resolver.js'; import { DisplayResolver } from '@app/unraid-api/graph/resolvers/display/display.resolver.js'; +import { DockerMutationsResolver } from '@app/unraid-api/graph/resolvers/docker/docker.mutations.resolver.js'; import { DockerResolver } from '@app/unraid-api/graph/resolvers/docker/docker.resolver.js'; +import { DockerService } from '@app/unraid-api/graph/resolvers/docker/docker.service.js'; import { FlashResolver } from '@app/unraid-api/graph/resolvers/flash/flash.resolver.js'; import { InfoResolver } from '@app/unraid-api/graph/resolvers/info/info.resolver.js'; import { LogsResolver } from '@app/unraid-api/graph/resolvers/logs/logs.resolver.js'; @@ -38,6 +40,8 @@ import { VmsResolver } from '@app/unraid-api/graph/resolvers/vms/vms.resolver.js DisksResolver, DisplayResolver, DockerResolver, + DockerMutationsResolver, + DockerService, FlashResolver, MutationResolver, InfoResolver,