feat: basic docker controls (#1292)

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

- **Chores**
- Updated configuration settings by resetting various parameters to
default values.
	- Enhanced module integration with new Docker components.
- **New Features**
	- Introduced GraphQL API endpoints to start and stop Docker containers.
	- Added a backend service to manage Docker container operations.
- **Tests**
- Expanded test suites covering Docker resolvers and service methods to
ensure reliable container management.
- **Refactor**
- Consolidated code imports and streamlined internal calls for clearer
structure.
	- Enhanced type safety for Docker container retrieval parameters.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
This commit is contained in:
Eli Bosley
2025-03-31 16:21:06 -04:00
committed by GitHub
parent 61fe6966ca
commit 12eddf894e
12 changed files with 402 additions and 11 deletions

View File

@@ -1,5 +1,5 @@
[api]
version="4.4.1"
version="4.1.3"
extraOrigins="https://google.com,https://test.com"
[local]
sandbox="yes"

View File

@@ -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<Array<DockerContainer>> => {
const dockerState = getters.docker();
if (useCache && dockerState.containers) {

View File

@@ -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<T> = Required<{
@@ -501,6 +501,7 @@ export function DockerSchema(): z.ZodObject<Properties<Docker>> {
__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<Properties<DockerContainer>
})
}
export function DockerMutationsSchema(): z.ZodObject<Properties<DockerMutations>> {
return z.object({
__typename: z.literal('DockerMutations').optional(),
startContainer: DockerContainerSchema(),
stopContainer: DockerContainerSchema()
})
}
export function DockerMutationsstartContainerArgsSchema(): z.ZodObject<Properties<DockerMutationsstartContainerArgs>> {
return z.object({
id: z.string()
})
}
export function DockerMutationsstopContainerArgsSchema(): z.ZodObject<Properties<DockerMutationsstopContainerArgs>> {
return z.object({
id: z.string()
})
}
export function DockerNetworkSchema(): z.ZodObject<Properties<DockerNetwork>> {
return z.object({
__typename: z.literal('DockerNetwork').optional(),

View File

@@ -555,6 +555,7 @@ export type Docker = Node & {
__typename?: 'Docker';
containers?: Maybe<Array<DockerContainer>>;
id: Scalars['ID']['output'];
mutations: DockerMutations;
networks?: Maybe<Array<DockerNetwork>>;
};
@@ -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<Display>;
Docker: ResolverTypeWrapper<Docker>;
DockerContainer: ResolverTypeWrapper<DockerContainer>;
DockerMutations: ResolverTypeWrapper<DockerMutations>;
DockerNetwork: ResolverTypeWrapper<DockerNetwork>;
DynamicRemoteAccessStatus: ResolverTypeWrapper<DynamicRemoteAccessStatus>;
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<ContextType = Context, ParentType extends Resolvers
export type DockerResolvers<ContextType = Context, ParentType extends ResolversParentTypes['Docker'] = ResolversParentTypes['Docker']> = ResolversObject<{
containers?: Resolver<Maybe<Array<ResolversTypes['DockerContainer']>>, ParentType, ContextType>;
id?: Resolver<ResolversTypes['ID'], ParentType, ContextType>;
mutations?: Resolver<ResolversTypes['DockerMutations'], ParentType, ContextType>;
networks?: Resolver<Maybe<Array<ResolversTypes['DockerNetwork']>>, ParentType, ContextType>;
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
}>;
@@ -2475,6 +2495,12 @@ export type DockerContainerResolvers<ContextType = Context, ParentType extends R
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
}>;
export type DockerMutationsResolvers<ContextType = Context, ParentType extends ResolversParentTypes['DockerMutations'] = ResolversParentTypes['DockerMutations']> = ResolversObject<{
startContainer?: Resolver<ResolversTypes['DockerContainer'], ParentType, ContextType, RequireFields<DockerMutationsstartContainerArgs, 'id'>>;
stopContainer?: Resolver<ResolversTypes['DockerContainer'], ParentType, ContextType, RequireFields<DockerMutationsstopContainerArgs, 'id'>>;
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
}>;
export type DockerNetworkResolvers<ContextType = Context, ParentType extends ResolversParentTypes['DockerNetwork'] = ResolversParentTypes['DockerNetwork']> = ResolversObject<{
attachable?: Resolver<ResolversTypes['Boolean'], ParentType, ContextType>;
configFrom?: Resolver<Maybe<ResolversTypes['JSON']>, ParentType, ContextType>;
@@ -3322,6 +3348,7 @@ export type Resolvers<ContextType = Context> = ResolversObject<{
Display?: DisplayResolvers<ContextType>;
Docker?: DockerResolvers<ContextType>;
DockerContainer?: DockerContainerResolvers<ContextType>;
DockerMutations?: DockerMutationsResolvers<ContextType>;
DockerNetwork?: DockerNetworkResolvers<ContextType>;
DynamicRemoteAccessStatus?: DynamicRemoteAccessStatusResolvers<ContextType>;
Flash?: FlashResolvers<ContextType>;

View File

@@ -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!
}

View File

@@ -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>(DockerMutationsResolver);
dockerService = module.get<DockerService>(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');
});
});

View File

@@ -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);
}
}

View File

@@ -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>(DockerResolver);
dockerService = module.get<DockerService>(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' });
});
});

View File

@@ -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',
};
}
}

View File

@@ -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>(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');
});
});

View File

@@ -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<DockerContainer[]> {
return getDockerContainers({ useCache });
}
public async startContainer(id: string): Promise<DockerContainer> {
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<DockerContainer> {
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;
}
}

View File

@@ -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,