mirror of
https://github.com/unraid/api.git
synced 2025-12-31 05:29:48 -06:00
feat: basic array controls (#1291)
<!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit - **New Features** - Expanded API operations for array management, including new mutations for state changes and disk handling. - Introduced new enumeration and input types for managing array states in the GraphQL schema. - Added a new resolver for handling array mutations in the GraphQL API. - **Chores** - Upgraded configuration version to 4.4.1 and refined connectivity status reporting. - **Refactor** - Streamlined request processing with improved error handling to provide clearer feedback on connectivity issues. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
[api]
|
||||
version="4.1.3"
|
||||
version="4.4.1"
|
||||
extraOrigins="https://google.com,https://test.com"
|
||||
[local]
|
||||
sandbox="yes"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
[api]
|
||||
version="4.1.3"
|
||||
version="4.4.1"
|
||||
extraOrigins="https://google.com,https://test.com"
|
||||
[local]
|
||||
sandbox="yes"
|
||||
@@ -20,5 +20,5 @@ dynamicRemoteAccessType="DISABLED"
|
||||
ssoSubIds=""
|
||||
allowedOrigins="/var/run/unraid-notifications.sock, /var/run/unraid-php.sock, /var/run/unraid-cli.sock, http://localhost:8080, https://localhost:4443, https://tower.local:4443, https://192.168.1.150:4443, https://tower:4443, https://192-168-1-150.thisisfourtyrandomcharacters012345678900.myunraid.net:4443, https://85-121-123-122.thisisfourtyrandomcharacters012345678900.myunraid.net:8443, https://10-252-0-1.hash.myunraid.net:4443, https://10-252-1-1.hash.myunraid.net:4443, https://10-253-3-1.hash.myunraid.net:4443, https://10-253-4-1.hash.myunraid.net:4443, https://10-253-5-1.hash.myunraid.net:4443, https://10-100-0-1.hash.myunraid.net:4443, https://10-100-0-2.hash.myunraid.net:4443, https://10-123-1-2.hash.myunraid.net:4443, https://221-123-121-112.hash.myunraid.net:4443, https://google.com, https://test.com, https://connect.myunraid.net, https://connect-staging.myunraid.net, https://dev-my.myunraid.net:4000, https://studio.apollographql.com"
|
||||
[connectionStatus]
|
||||
minigraph="PRE_INIT"
|
||||
minigraph="ERROR_RETRYING"
|
||||
upnpStatus=""
|
||||
|
||||
@@ -2,7 +2,6 @@ import { GraphQLError } from 'graphql';
|
||||
import { sum } from 'lodash-es';
|
||||
|
||||
import type { ArrayCapacity, ArrayType } from '@app/graphql/generated/api/types.js';
|
||||
import { getServerIdentifier } from '@app/core/utils/server-identifier.js';
|
||||
import { ArrayDiskType } from '@app/graphql/generated/api/types.js';
|
||||
import { store } from '@app/store/index.js';
|
||||
import { FileLoadStatus } from '@app/store/types.js';
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { got } from 'got';
|
||||
|
||||
import { AppError } from '@app/core/errors/app-error.js';
|
||||
import { logger } from '@app/core/log.js';
|
||||
import { type LooseObject } from '@app/core/types/index.js';
|
||||
import { catchHandlers } from '@app/core/utils/misc/catch-handlers.js';
|
||||
import { DRY_RUN } from '@app/environment.js';
|
||||
import { getters } from '@app/store/index.js';
|
||||
|
||||
@@ -27,10 +27,15 @@ export const emcmd = async (commands: LooseObject) => {
|
||||
// Ensure we only log on dry-run
|
||||
return;
|
||||
}
|
||||
// Untested, this code is unused right now so going to assume it's probably not working well anyway, swapped
|
||||
// to got to remove this request-promise dependency
|
||||
return got
|
||||
.get(url, { searchParams: { ...commands, csrf_token: csrfToken } })
|
||||
.catch(catchHandlers.emhttpd);
|
||||
// return request.get(url, options).catch(catchHandlers.emhttpd);
|
||||
.get(url, {
|
||||
enableUnixSockets: true,
|
||||
searchParams: { ...commands, csrf_token: csrfToken },
|
||||
})
|
||||
.catch((error: NodeJS.ErrnoException) => {
|
||||
if (error.code === 'ENOENT') {
|
||||
throw new AppError('emhttpd socket unavailable.');
|
||||
}
|
||||
throw error;
|
||||
});
|
||||
};
|
||||
|
||||
@@ -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, ArrayDiskStatus, ArrayDiskType, ArrayPendingState, ArrayState, 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, arrayDiskInput, 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, 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<{
|
||||
@@ -25,6 +25,8 @@ export const ArrayPendingStateSchema = z.nativeEnum(ArrayPendingState);
|
||||
|
||||
export const ArrayStateSchema = z.nativeEnum(ArrayState);
|
||||
|
||||
export const ArrayStateInputStateSchema = z.nativeEnum(ArrayStateInputState);
|
||||
|
||||
export const ConfigErrorStateSchema = z.nativeEnum(ConfigErrorState);
|
||||
|
||||
export const ContainerPortTypeSchema = z.nativeEnum(ContainerPortType);
|
||||
@@ -213,6 +215,67 @@ export function ArrayDiskSchema(): z.ZodObject<Properties<ArrayDisk>> {
|
||||
})
|
||||
}
|
||||
|
||||
export function ArrayDiskInputSchema(): z.ZodObject<Properties<ArrayDiskInput>> {
|
||||
return z.object({
|
||||
id: z.string(),
|
||||
slot: z.number().nullish()
|
||||
})
|
||||
}
|
||||
|
||||
export function ArrayMutationsSchema(): z.ZodObject<Properties<ArrayMutations>> {
|
||||
return z.object({
|
||||
__typename: z.literal('ArrayMutations').optional(),
|
||||
addDiskToArray: ArrayTypeSchema().nullish(),
|
||||
clearArrayDiskStatistics: z.record(z.string(), z.any()).nullish(),
|
||||
mountArrayDisk: DiskSchema().nullish(),
|
||||
removeDiskFromArray: ArrayTypeSchema().nullish(),
|
||||
setState: ArrayTypeSchema().nullish(),
|
||||
unmountArrayDisk: DiskSchema().nullish()
|
||||
})
|
||||
}
|
||||
|
||||
export function ArrayMutationsaddDiskToArrayArgsSchema(): z.ZodObject<Properties<ArrayMutationsaddDiskToArrayArgs>> {
|
||||
return z.object({
|
||||
input: z.lazy(() => ArrayDiskInputSchema().nullish())
|
||||
})
|
||||
}
|
||||
|
||||
export function ArrayMutationsclearArrayDiskStatisticsArgsSchema(): z.ZodObject<Properties<ArrayMutationsclearArrayDiskStatisticsArgs>> {
|
||||
return z.object({
|
||||
id: z.string()
|
||||
})
|
||||
}
|
||||
|
||||
export function ArrayMutationsmountArrayDiskArgsSchema(): z.ZodObject<Properties<ArrayMutationsmountArrayDiskArgs>> {
|
||||
return z.object({
|
||||
id: z.string()
|
||||
})
|
||||
}
|
||||
|
||||
export function ArrayMutationsremoveDiskFromArrayArgsSchema(): z.ZodObject<Properties<ArrayMutationsremoveDiskFromArrayArgs>> {
|
||||
return z.object({
|
||||
input: z.lazy(() => ArrayDiskInputSchema().nullish())
|
||||
})
|
||||
}
|
||||
|
||||
export function ArrayMutationssetStateArgsSchema(): z.ZodObject<Properties<ArrayMutationssetStateArgs>> {
|
||||
return z.object({
|
||||
input: z.lazy(() => ArrayStateInputSchema().nullish())
|
||||
})
|
||||
}
|
||||
|
||||
export function ArrayMutationsunmountArrayDiskArgsSchema(): z.ZodObject<Properties<ArrayMutationsunmountArrayDiskArgs>> {
|
||||
return z.object({
|
||||
id: z.string()
|
||||
})
|
||||
}
|
||||
|
||||
export function ArrayStateInputSchema(): z.ZodObject<Properties<ArrayStateInput>> {
|
||||
return z.object({
|
||||
desiredState: z.lazy(() => ArrayStateInputStateSchema)
|
||||
})
|
||||
}
|
||||
|
||||
export function BaseboardSchema(): z.ZodObject<Properties<Baseboard>> {
|
||||
return z.object({
|
||||
__typename: z.literal('Baseboard').optional(),
|
||||
@@ -1303,13 +1366,6 @@ export function addUserInputSchema(): z.ZodObject<Properties<addUserInput>> {
|
||||
})
|
||||
}
|
||||
|
||||
export function arrayDiskInputSchema(): z.ZodObject<Properties<arrayDiskInput>> {
|
||||
return z.object({
|
||||
id: z.string(),
|
||||
slot: z.number().nullish()
|
||||
})
|
||||
}
|
||||
|
||||
export function deleteUserInputSchema(): z.ZodObject<Properties<deleteUserInput>> {
|
||||
return z.object({
|
||||
name: z.string()
|
||||
|
||||
@@ -191,6 +191,13 @@ export enum ArrayDiskFsColor {
|
||||
YELLOW_ON = 'yellow_on'
|
||||
}
|
||||
|
||||
export type ArrayDiskInput = {
|
||||
/** Disk ID */
|
||||
id: Scalars['ID']['input'];
|
||||
/** The slot for the disk */
|
||||
slot?: InputMaybe<Scalars['Int']['input']>;
|
||||
};
|
||||
|
||||
export enum ArrayDiskStatus {
|
||||
/** disabled, old disk still present */
|
||||
DISK_DSBL = 'DISK_DSBL',
|
||||
@@ -223,6 +230,49 @@ export enum ArrayDiskType {
|
||||
PARITY = 'Parity'
|
||||
}
|
||||
|
||||
export type ArrayMutations = {
|
||||
__typename?: 'ArrayMutations';
|
||||
/** Add new disk to array */
|
||||
addDiskToArray?: Maybe<ArrayType>;
|
||||
clearArrayDiskStatistics?: Maybe<Scalars['JSON']['output']>;
|
||||
mountArrayDisk?: Maybe<Disk>;
|
||||
/** Remove existing disk from array. NOTE: The array must be stopped before running this otherwise it'll throw an error. */
|
||||
removeDiskFromArray?: Maybe<ArrayType>;
|
||||
/** Set array state */
|
||||
setState?: Maybe<ArrayType>;
|
||||
unmountArrayDisk?: Maybe<Disk>;
|
||||
};
|
||||
|
||||
|
||||
export type ArrayMutationsaddDiskToArrayArgs = {
|
||||
input?: InputMaybe<ArrayDiskInput>;
|
||||
};
|
||||
|
||||
|
||||
export type ArrayMutationsclearArrayDiskStatisticsArgs = {
|
||||
id: Scalars['ID']['input'];
|
||||
};
|
||||
|
||||
|
||||
export type ArrayMutationsmountArrayDiskArgs = {
|
||||
id: Scalars['ID']['input'];
|
||||
};
|
||||
|
||||
|
||||
export type ArrayMutationsremoveDiskFromArrayArgs = {
|
||||
input?: InputMaybe<ArrayDiskInput>;
|
||||
};
|
||||
|
||||
|
||||
export type ArrayMutationssetStateArgs = {
|
||||
input?: InputMaybe<ArrayStateInput>;
|
||||
};
|
||||
|
||||
|
||||
export type ArrayMutationsunmountArrayDiskArgs = {
|
||||
id: Scalars['ID']['input'];
|
||||
};
|
||||
|
||||
export enum ArrayPendingState {
|
||||
/** Array has no data disks */
|
||||
NO_DATA_DISKS = 'no_data_disks',
|
||||
@@ -259,6 +309,18 @@ export enum ArrayState {
|
||||
TOO_MANY_MISSING_DISKS = 'TOO_MANY_MISSING_DISKS'
|
||||
}
|
||||
|
||||
export type ArrayStateInput = {
|
||||
/** Array state */
|
||||
desiredState: ArrayStateInputState;
|
||||
};
|
||||
|
||||
export enum ArrayStateInputState {
|
||||
/** Start array */
|
||||
START = 'START',
|
||||
/** Stop array */
|
||||
STOP = 'STOP'
|
||||
}
|
||||
|
||||
export type Baseboard = {
|
||||
__typename?: 'Baseboard';
|
||||
assetTag?: Maybe<Scalars['String']['output']>;
|
||||
@@ -731,8 +793,6 @@ export type Mount = {
|
||||
|
||||
export type Mutation = {
|
||||
__typename?: 'Mutation';
|
||||
/** Add new disk to array */
|
||||
addDiskToArray?: Maybe<ArrayType>;
|
||||
addPermission: Scalars['Boolean']['output'];
|
||||
addRoleForApiKey: Scalars['Boolean']['output'];
|
||||
addRoleForUser: Scalars['Boolean']['output'];
|
||||
@@ -742,9 +802,9 @@ export type Mutation = {
|
||||
/** Marks a notification as archived. */
|
||||
archiveNotification: Notification;
|
||||
archiveNotifications: NotificationOverview;
|
||||
array?: Maybe<ArrayMutations>;
|
||||
/** Cancel parity check */
|
||||
cancelParityCheck?: Maybe<Scalars['JSON']['output']>;
|
||||
clearArrayDiskStatistics?: Maybe<Scalars['JSON']['output']>;
|
||||
connectSignIn: Scalars['Boolean']['output'];
|
||||
connectSignOut: Scalars['Boolean']['output'];
|
||||
createApiKey: ApiKeyWithSecret;
|
||||
@@ -756,29 +816,21 @@ export type Mutation = {
|
||||
deleteUser?: Maybe<User>;
|
||||
enableDynamicRemoteAccess: Scalars['Boolean']['output'];
|
||||
login?: Maybe<Scalars['String']['output']>;
|
||||
mountArrayDisk?: Maybe<Disk>;
|
||||
/** Pause parity check */
|
||||
pauseParityCheck?: Maybe<Scalars['JSON']['output']>;
|
||||
reboot?: Maybe<Scalars['String']['output']>;
|
||||
/** Reads each notification to recompute & update the overview. */
|
||||
recalculateOverview: NotificationOverview;
|
||||
/** Remove existing disk from array. NOTE: The array must be stopped before running this otherwise it'll throw an error. */
|
||||
removeDiskFromArray?: Maybe<ArrayType>;
|
||||
removeRoleFromApiKey: Scalars['Boolean']['output'];
|
||||
/** Resume parity check */
|
||||
resumeParityCheck?: Maybe<Scalars['JSON']['output']>;
|
||||
setAdditionalAllowedOrigins: Array<Scalars['String']['output']>;
|
||||
setupRemoteAccess: Scalars['Boolean']['output'];
|
||||
shutdown?: Maybe<Scalars['String']['output']>;
|
||||
/** Start array */
|
||||
startArray?: Maybe<ArrayType>;
|
||||
/** Start parity check */
|
||||
startParityCheck?: Maybe<Scalars['JSON']['output']>;
|
||||
/** Stop array */
|
||||
stopArray?: Maybe<ArrayType>;
|
||||
unarchiveAll: NotificationOverview;
|
||||
unarchiveNotifications: NotificationOverview;
|
||||
unmountArrayDisk?: Maybe<Disk>;
|
||||
/** Marks a notification as unread. */
|
||||
unreadNotification: Notification;
|
||||
/**
|
||||
@@ -789,11 +841,6 @@ export type Mutation = {
|
||||
};
|
||||
|
||||
|
||||
export type MutationaddDiskToArrayArgs = {
|
||||
input?: InputMaybe<arrayDiskInput>;
|
||||
};
|
||||
|
||||
|
||||
export type MutationaddPermissionArgs = {
|
||||
input: AddPermissionInput;
|
||||
};
|
||||
@@ -829,11 +876,6 @@ export type MutationarchiveNotificationsArgs = {
|
||||
};
|
||||
|
||||
|
||||
export type MutationclearArrayDiskStatisticsArgs = {
|
||||
id: Scalars['ID']['input'];
|
||||
};
|
||||
|
||||
|
||||
export type MutationconnectSignInArgs = {
|
||||
input: ConnectSignInInput;
|
||||
};
|
||||
@@ -871,16 +913,6 @@ export type MutationloginArgs = {
|
||||
};
|
||||
|
||||
|
||||
export type MutationmountArrayDiskArgs = {
|
||||
id: Scalars['ID']['input'];
|
||||
};
|
||||
|
||||
|
||||
export type MutationremoveDiskFromArrayArgs = {
|
||||
input?: InputMaybe<arrayDiskInput>;
|
||||
};
|
||||
|
||||
|
||||
export type MutationremoveRoleFromApiKeyArgs = {
|
||||
input: RemoveRoleFromApiKeyInput;
|
||||
};
|
||||
@@ -911,11 +943,6 @@ export type MutationunarchiveNotificationsArgs = {
|
||||
};
|
||||
|
||||
|
||||
export type MutationunmountArrayDiskArgs = {
|
||||
id: Scalars['ID']['input'];
|
||||
};
|
||||
|
||||
|
||||
export type MutationunreadNotificationArgs = {
|
||||
id: Scalars['String']['input'];
|
||||
};
|
||||
@@ -1823,13 +1850,6 @@ export type addUserInput = {
|
||||
password: Scalars['String']['input'];
|
||||
};
|
||||
|
||||
export type arrayDiskInput = {
|
||||
/** Disk ID */
|
||||
id: Scalars['ID']['input'];
|
||||
/** The slot for the disk */
|
||||
slot?: InputMaybe<Scalars['Int']['input']>;
|
||||
};
|
||||
|
||||
export type deleteUserInput = {
|
||||
name: Scalars['String']['input'];
|
||||
};
|
||||
@@ -1945,10 +1965,14 @@ export type ResolversTypes = ResolversObject<{
|
||||
ArrayCapacity: ResolverTypeWrapper<ArrayCapacity>;
|
||||
ArrayDisk: ResolverTypeWrapper<ArrayDisk>;
|
||||
ArrayDiskFsColor: ArrayDiskFsColor;
|
||||
ArrayDiskInput: ArrayDiskInput;
|
||||
ArrayDiskStatus: ArrayDiskStatus;
|
||||
ArrayDiskType: ArrayDiskType;
|
||||
ArrayMutations: ResolverTypeWrapper<ArrayMutations>;
|
||||
ArrayPendingState: ArrayPendingState;
|
||||
ArrayState: ArrayState;
|
||||
ArrayStateInput: ArrayStateInput;
|
||||
ArrayStateInputState: ArrayStateInputState;
|
||||
Baseboard: ResolverTypeWrapper<Baseboard>;
|
||||
Boolean: ResolverTypeWrapper<Scalars['Boolean']['output']>;
|
||||
Capacity: ResolverTypeWrapper<Capacity>;
|
||||
@@ -2057,7 +2081,6 @@ export type ResolversTypes = ResolversObject<{
|
||||
WAN_FORWARD_TYPE: WAN_FORWARD_TYPE;
|
||||
Welcome: ResolverTypeWrapper<Welcome>;
|
||||
addUserInput: addUserInput;
|
||||
arrayDiskInput: arrayDiskInput;
|
||||
deleteUserInput: deleteUserInput;
|
||||
mdState: mdState;
|
||||
registrationType: registrationType;
|
||||
@@ -2079,6 +2102,9 @@ export type ResolversParentTypes = ResolversObject<{
|
||||
Array: ArrayType;
|
||||
ArrayCapacity: ArrayCapacity;
|
||||
ArrayDisk: ArrayDisk;
|
||||
ArrayDiskInput: ArrayDiskInput;
|
||||
ArrayMutations: ArrayMutations;
|
||||
ArrayStateInput: ArrayStateInput;
|
||||
Baseboard: Baseboard;
|
||||
Boolean: Scalars['Boolean']['output'];
|
||||
Capacity: Capacity;
|
||||
@@ -2165,7 +2191,6 @@ export type ResolversParentTypes = ResolversObject<{
|
||||
Vms: Vms;
|
||||
Welcome: Welcome;
|
||||
addUserInput: addUserInput;
|
||||
arrayDiskInput: arrayDiskInput;
|
||||
deleteUserInput: deleteUserInput;
|
||||
usersInput: usersInput;
|
||||
}>;
|
||||
@@ -2250,6 +2275,16 @@ export type ArrayDiskResolvers<ContextType = Context, ParentType extends Resolve
|
||||
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
|
||||
}>;
|
||||
|
||||
export type ArrayMutationsResolvers<ContextType = Context, ParentType extends ResolversParentTypes['ArrayMutations'] = ResolversParentTypes['ArrayMutations']> = ResolversObject<{
|
||||
addDiskToArray?: Resolver<Maybe<ResolversTypes['Array']>, ParentType, ContextType, Partial<ArrayMutationsaddDiskToArrayArgs>>;
|
||||
clearArrayDiskStatistics?: Resolver<Maybe<ResolversTypes['JSON']>, ParentType, ContextType, RequireFields<ArrayMutationsclearArrayDiskStatisticsArgs, 'id'>>;
|
||||
mountArrayDisk?: Resolver<Maybe<ResolversTypes['Disk']>, ParentType, ContextType, RequireFields<ArrayMutationsmountArrayDiskArgs, 'id'>>;
|
||||
removeDiskFromArray?: Resolver<Maybe<ResolversTypes['Array']>, ParentType, ContextType, Partial<ArrayMutationsremoveDiskFromArrayArgs>>;
|
||||
setState?: Resolver<Maybe<ResolversTypes['Array']>, ParentType, ContextType, Partial<ArrayMutationssetStateArgs>>;
|
||||
unmountArrayDisk?: Resolver<Maybe<ResolversTypes['Disk']>, ParentType, ContextType, RequireFields<ArrayMutationsunmountArrayDiskArgs, 'id'>>;
|
||||
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
|
||||
}>;
|
||||
|
||||
export type BaseboardResolvers<ContextType = Context, ParentType extends ResolversParentTypes['Baseboard'] = ResolversParentTypes['Baseboard']> = ResolversObject<{
|
||||
assetTag?: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>;
|
||||
manufacturer?: Resolver<ResolversTypes['String'], ParentType, ContextType>;
|
||||
@@ -2612,7 +2647,6 @@ export type MountResolvers<ContextType = Context, ParentType extends ResolversPa
|
||||
}>;
|
||||
|
||||
export type MutationResolvers<ContextType = Context, ParentType extends ResolversParentTypes['Mutation'] = ResolversParentTypes['Mutation']> = ResolversObject<{
|
||||
addDiskToArray?: Resolver<Maybe<ResolversTypes['Array']>, ParentType, ContextType, Partial<MutationaddDiskToArrayArgs>>;
|
||||
addPermission?: Resolver<ResolversTypes['Boolean'], ParentType, ContextType, RequireFields<MutationaddPermissionArgs, 'input'>>;
|
||||
addRoleForApiKey?: Resolver<ResolversTypes['Boolean'], ParentType, ContextType, RequireFields<MutationaddRoleForApiKeyArgs, 'input'>>;
|
||||
addRoleForUser?: Resolver<ResolversTypes['Boolean'], ParentType, ContextType, RequireFields<MutationaddRoleForUserArgs, 'input'>>;
|
||||
@@ -2620,8 +2654,8 @@ export type MutationResolvers<ContextType = Context, ParentType extends Resolver
|
||||
archiveAll?: Resolver<ResolversTypes['NotificationOverview'], ParentType, ContextType, Partial<MutationarchiveAllArgs>>;
|
||||
archiveNotification?: Resolver<ResolversTypes['Notification'], ParentType, ContextType, RequireFields<MutationarchiveNotificationArgs, 'id'>>;
|
||||
archiveNotifications?: Resolver<ResolversTypes['NotificationOverview'], ParentType, ContextType, Partial<MutationarchiveNotificationsArgs>>;
|
||||
array?: Resolver<Maybe<ResolversTypes['ArrayMutations']>, ParentType, ContextType>;
|
||||
cancelParityCheck?: Resolver<Maybe<ResolversTypes['JSON']>, ParentType, ContextType>;
|
||||
clearArrayDiskStatistics?: Resolver<Maybe<ResolversTypes['JSON']>, ParentType, ContextType, RequireFields<MutationclearArrayDiskStatisticsArgs, 'id'>>;
|
||||
connectSignIn?: Resolver<ResolversTypes['Boolean'], ParentType, ContextType, RequireFields<MutationconnectSignInArgs, 'input'>>;
|
||||
connectSignOut?: Resolver<ResolversTypes['Boolean'], ParentType, ContextType>;
|
||||
createApiKey?: Resolver<ResolversTypes['ApiKeyWithSecret'], ParentType, ContextType, RequireFields<MutationcreateApiKeyArgs, 'input'>>;
|
||||
@@ -2631,22 +2665,17 @@ export type MutationResolvers<ContextType = Context, ParentType extends Resolver
|
||||
deleteUser?: Resolver<Maybe<ResolversTypes['User']>, ParentType, ContextType, RequireFields<MutationdeleteUserArgs, 'input'>>;
|
||||
enableDynamicRemoteAccess?: Resolver<ResolversTypes['Boolean'], ParentType, ContextType, RequireFields<MutationenableDynamicRemoteAccessArgs, 'input'>>;
|
||||
login?: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType, RequireFields<MutationloginArgs, 'password' | 'username'>>;
|
||||
mountArrayDisk?: Resolver<Maybe<ResolversTypes['Disk']>, ParentType, ContextType, RequireFields<MutationmountArrayDiskArgs, 'id'>>;
|
||||
pauseParityCheck?: Resolver<Maybe<ResolversTypes['JSON']>, ParentType, ContextType>;
|
||||
reboot?: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>;
|
||||
recalculateOverview?: Resolver<ResolversTypes['NotificationOverview'], ParentType, ContextType>;
|
||||
removeDiskFromArray?: Resolver<Maybe<ResolversTypes['Array']>, ParentType, ContextType, Partial<MutationremoveDiskFromArrayArgs>>;
|
||||
removeRoleFromApiKey?: Resolver<ResolversTypes['Boolean'], ParentType, ContextType, RequireFields<MutationremoveRoleFromApiKeyArgs, 'input'>>;
|
||||
resumeParityCheck?: Resolver<Maybe<ResolversTypes['JSON']>, ParentType, ContextType>;
|
||||
setAdditionalAllowedOrigins?: Resolver<Array<ResolversTypes['String']>, ParentType, ContextType, RequireFields<MutationsetAdditionalAllowedOriginsArgs, 'input'>>;
|
||||
setupRemoteAccess?: Resolver<ResolversTypes['Boolean'], ParentType, ContextType, RequireFields<MutationsetupRemoteAccessArgs, 'input'>>;
|
||||
shutdown?: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>;
|
||||
startArray?: Resolver<Maybe<ResolversTypes['Array']>, ParentType, ContextType>;
|
||||
startParityCheck?: Resolver<Maybe<ResolversTypes['JSON']>, ParentType, ContextType, Partial<MutationstartParityCheckArgs>>;
|
||||
stopArray?: Resolver<Maybe<ResolversTypes['Array']>, ParentType, ContextType>;
|
||||
unarchiveAll?: Resolver<ResolversTypes['NotificationOverview'], ParentType, ContextType, Partial<MutationunarchiveAllArgs>>;
|
||||
unarchiveNotifications?: Resolver<ResolversTypes['NotificationOverview'], ParentType, ContextType, Partial<MutationunarchiveNotificationsArgs>>;
|
||||
unmountArrayDisk?: Resolver<Maybe<ResolversTypes['Disk']>, ParentType, ContextType, RequireFields<MutationunmountArrayDiskArgs, 'id'>>;
|
||||
unreadNotification?: Resolver<ResolversTypes['Notification'], ParentType, ContextType, RequireFields<MutationunreadNotificationArgs, 'id'>>;
|
||||
updateApiSettings?: Resolver<ResolversTypes['ConnectSettingsValues'], ParentType, ContextType, RequireFields<MutationupdateApiSettingsArgs, 'input'>>;
|
||||
}>;
|
||||
@@ -3273,6 +3302,7 @@ export type Resolvers<ContextType = Context> = ResolversObject<{
|
||||
Array?: ArrayResolvers<ContextType>;
|
||||
ArrayCapacity?: ArrayCapacityResolvers<ContextType>;
|
||||
ArrayDisk?: ArrayDiskResolvers<ContextType>;
|
||||
ArrayMutations?: ArrayMutationsResolvers<ContextType>;
|
||||
Baseboard?: BaseboardResolvers<ContextType>;
|
||||
Capacity?: CapacityResolvers<ContextType>;
|
||||
Case?: CaseResolvers<ContextType>;
|
||||
|
||||
@@ -3,16 +3,26 @@ type Query {
|
||||
array: Array!
|
||||
}
|
||||
|
||||
type Mutation {
|
||||
enum ArrayStateInputState {
|
||||
"""Start array"""
|
||||
startArray: Array
|
||||
START
|
||||
"""Stop array"""
|
||||
stopArray: Array
|
||||
STOP
|
||||
}
|
||||
|
||||
input ArrayStateInput {
|
||||
"""Array state"""
|
||||
desiredState: ArrayStateInputState!
|
||||
}
|
||||
|
||||
type ArrayMutations {
|
||||
"""Set array state"""
|
||||
setState(input: ArrayStateInput): Array
|
||||
|
||||
"""Add new disk to array"""
|
||||
addDiskToArray(input: arrayDiskInput): Array
|
||||
addDiskToArray(input: ArrayDiskInput): Array
|
||||
"""Remove existing disk from array. NOTE: The array must be stopped before running this otherwise it'll throw an error."""
|
||||
removeDiskFromArray(input: arrayDiskInput): Array
|
||||
removeDiskFromArray(input: ArrayDiskInput): Array
|
||||
|
||||
mountArrayDisk(id: ID!): Disk
|
||||
unmountArrayDisk(id: ID!): Disk
|
||||
@@ -20,11 +30,15 @@ type Mutation {
|
||||
clearArrayDiskStatistics(id: ID!): JSON
|
||||
}
|
||||
|
||||
type Mutation {
|
||||
array: ArrayMutations
|
||||
}
|
||||
|
||||
type Subscription {
|
||||
array: Array!
|
||||
}
|
||||
|
||||
input arrayDiskInput {
|
||||
input ArrayDiskInput {
|
||||
"""Disk ID"""
|
||||
id: ID!
|
||||
"""The slot for the disk"""
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
import { Args, ResolveField, Resolver } from '@nestjs/graphql';
|
||||
|
||||
import { AuthActionVerb, AuthPossession, UsePermissions } from 'nest-authz';
|
||||
|
||||
import type { ArrayDiskInput, ArrayStateInput } from '@app/graphql/generated/api/types.js';
|
||||
import { Resource } from '@app/graphql/generated/api/types.js';
|
||||
import { ArrayService } from '@app/unraid-api/graph/resolvers/array/array.service.js';
|
||||
|
||||
@Resolver('ArrayMutations')
|
||||
export class ArrayMutationsResolver {
|
||||
constructor(private readonly arrayService: ArrayService) {}
|
||||
|
||||
@ResolveField('setState')
|
||||
@UsePermissions({
|
||||
action: AuthActionVerb.UPDATE,
|
||||
resource: Resource.ARRAY,
|
||||
possession: AuthPossession.ANY,
|
||||
})
|
||||
public async setState(@Args('input') input: ArrayStateInput) {
|
||||
return this.arrayService.updateArrayState(input);
|
||||
}
|
||||
|
||||
@ResolveField('addDiskToArray')
|
||||
@UsePermissions({
|
||||
action: AuthActionVerb.UPDATE,
|
||||
resource: Resource.ARRAY,
|
||||
possession: AuthPossession.ANY,
|
||||
})
|
||||
public async addDiskToArray(@Args('input') input: ArrayDiskInput) {
|
||||
return this.arrayService.addDiskToArray(input);
|
||||
}
|
||||
|
||||
@ResolveField('removeDiskFromArray')
|
||||
@UsePermissions({
|
||||
action: AuthActionVerb.UPDATE,
|
||||
resource: Resource.ARRAY,
|
||||
possession: AuthPossession.ANY,
|
||||
})
|
||||
public async removeDiskFromArray(@Args('input') input: ArrayDiskInput) {
|
||||
return this.arrayService.removeDiskFromArray(input);
|
||||
}
|
||||
|
||||
@ResolveField('mountArrayDisk')
|
||||
@UsePermissions({
|
||||
action: AuthActionVerb.UPDATE,
|
||||
resource: Resource.ARRAY,
|
||||
possession: AuthPossession.ANY,
|
||||
})
|
||||
public async mountArrayDisk(@Args('id') id: string) {
|
||||
return this.arrayService.mountArrayDisk(id);
|
||||
}
|
||||
|
||||
@ResolveField('unmountArrayDisk')
|
||||
@UsePermissions({
|
||||
action: AuthActionVerb.UPDATE,
|
||||
resource: Resource.ARRAY,
|
||||
possession: AuthPossession.ANY,
|
||||
})
|
||||
public async unmountArrayDisk(@Args('id') id: string) {
|
||||
return this.arrayService.unmountArrayDisk(id);
|
||||
}
|
||||
|
||||
@ResolveField('clearArrayDiskStatistics')
|
||||
@UsePermissions({
|
||||
action: AuthActionVerb.UPDATE,
|
||||
resource: Resource.ARRAY,
|
||||
possession: AuthPossession.ANY,
|
||||
})
|
||||
public async clearArrayDiskStatistics(@Args('id') id: string) {
|
||||
return this.arrayService.clearArrayDiskStatistics(id);
|
||||
}
|
||||
}
|
||||
@@ -1,19 +1,35 @@
|
||||
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 { ArrayResolver } from '@app/unraid-api/graph/resolvers/array/array.resolver.js';
|
||||
import { ArrayService } from '@app/unraid-api/graph/resolvers/array/array.service.js';
|
||||
|
||||
describe('ArrayResolver', () => {
|
||||
let resolver: ArrayResolver;
|
||||
let arrayService: ArrayService;
|
||||
|
||||
beforeEach(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [ArrayResolver],
|
||||
providers: [
|
||||
ArrayResolver,
|
||||
{
|
||||
provide: ArrayService,
|
||||
useValue: {
|
||||
updateArrayState: vi.fn(),
|
||||
addDiskToArray: vi.fn(),
|
||||
removeDiskFromArray: vi.fn(),
|
||||
mountArrayDisk: vi.fn(),
|
||||
unmountArrayDisk: vi.fn(),
|
||||
clearArrayDiskStatistics: vi.fn(),
|
||||
},
|
||||
},
|
||||
],
|
||||
}).compile();
|
||||
|
||||
resolver = module.get<ArrayResolver>(ArrayResolver);
|
||||
arrayService = module.get<ArrayService>(ArrayService);
|
||||
});
|
||||
|
||||
it('should be defined', () => {
|
||||
|
||||
@@ -6,9 +6,12 @@ import { getArrayData } from '@app/core/modules/array/get-array-data.js';
|
||||
import { createSubscription, PUBSUB_CHANNEL } from '@app/core/pubsub.js';
|
||||
import { Resource } from '@app/graphql/generated/api/types.js';
|
||||
import { store } from '@app/store/index.js';
|
||||
import { ArrayService } from '@app/unraid-api/graph/resolvers/array/array.service.js';
|
||||
|
||||
@Resolver('Array')
|
||||
export class ArrayResolver {
|
||||
constructor(private readonly arrayService: ArrayService) {}
|
||||
|
||||
@Query()
|
||||
@UsePermissions({
|
||||
action: AuthActionVerb.READ,
|
||||
|
||||
210
api/src/unraid-api/graph/resolvers/array/array.service.spec.ts
Normal file
210
api/src/unraid-api/graph/resolvers/array/array.service.spec.ts
Normal file
@@ -0,0 +1,210 @@
|
||||
import type { TestingModule } from '@nestjs/testing';
|
||||
import { Test } from '@nestjs/testing';
|
||||
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import type { ArrayDiskInput, ArrayStateInput } from '@app/graphql/generated/api/types.js';
|
||||
import { getArrayData } from '@app/core/modules/array/get-array-data.js';
|
||||
import { emcmd } from '@app/core/utils/clients/emcmd.js';
|
||||
import { ArrayState, ArrayStateInputState } from '@app/graphql/generated/api/types.js';
|
||||
import { getters } from '@app/store/index.js';
|
||||
import { ArrayService } from '@app/unraid-api/graph/resolvers/array/array.service.js';
|
||||
|
||||
vi.mock('@app/core/utils/clients/emcmd.js', () => ({
|
||||
emcmd: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('@app/store/index.js', () => ({
|
||||
getters: {
|
||||
emhttp: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock('@app/core/modules/array/get-array-data.js', () => ({
|
||||
getArrayData: vi.fn(),
|
||||
}));
|
||||
|
||||
describe('ArrayService', () => {
|
||||
let service: ArrayService;
|
||||
let mockArrayData: any;
|
||||
|
||||
beforeEach(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [ArrayService],
|
||||
}).compile();
|
||||
|
||||
service = module.get<ArrayService>(ArrayService);
|
||||
|
||||
// Mock getters.emhttp()
|
||||
vi.mocked(getters.emhttp).mockReturnValue({
|
||||
var: {
|
||||
mdState: ArrayState.STOPPED,
|
||||
},
|
||||
} as any);
|
||||
|
||||
// Mock getArrayData
|
||||
mockArrayData = {
|
||||
id: 'array',
|
||||
state: ArrayState.STOPPED,
|
||||
capacity: {
|
||||
kilobytes: {
|
||||
free: '1000',
|
||||
used: '1000',
|
||||
total: '2000',
|
||||
},
|
||||
disks: {
|
||||
free: '10',
|
||||
used: '5',
|
||||
total: '15',
|
||||
},
|
||||
},
|
||||
boot: null,
|
||||
parities: [],
|
||||
disks: [],
|
||||
caches: [],
|
||||
};
|
||||
vi.mocked(getArrayData).mockReturnValue(mockArrayData);
|
||||
});
|
||||
|
||||
it('should be defined', () => {
|
||||
expect(service).toBeDefined();
|
||||
});
|
||||
|
||||
it('should update array state', async () => {
|
||||
const input: ArrayStateInput = {
|
||||
desiredState: ArrayStateInputState.START,
|
||||
};
|
||||
const result = await service.updateArrayState(input);
|
||||
expect(result).toEqual(mockArrayData);
|
||||
expect(emcmd).toHaveBeenCalledWith({
|
||||
cmdStart: 'Start',
|
||||
startState: 'STOPPED',
|
||||
});
|
||||
});
|
||||
|
||||
it('should add disk to array', async () => {
|
||||
const input: ArrayDiskInput = {
|
||||
id: 'test-disk',
|
||||
slot: 1,
|
||||
};
|
||||
const result = await service.addDiskToArray(input);
|
||||
expect(result).toEqual(mockArrayData);
|
||||
expect(emcmd).toHaveBeenCalledWith({
|
||||
changeDevice: 'apply',
|
||||
'slotId.1': 'test-disk',
|
||||
});
|
||||
});
|
||||
|
||||
it('should remove disk from array', async () => {
|
||||
const input: ArrayDiskInput = {
|
||||
id: 'test-disk',
|
||||
slot: 1,
|
||||
};
|
||||
const result = await service.removeDiskFromArray(input);
|
||||
expect(result).toEqual(mockArrayData);
|
||||
expect(emcmd).toHaveBeenCalledWith({
|
||||
changeDevice: 'apply',
|
||||
'slotId.1': '',
|
||||
});
|
||||
});
|
||||
|
||||
it('should mount array disk', async () => {
|
||||
// Mock array as running
|
||||
vi.mocked(getters.emhttp).mockReturnValue({
|
||||
var: {
|
||||
mdState: ArrayState.STARTED,
|
||||
},
|
||||
} as any);
|
||||
|
||||
const result = await service.mountArrayDisk('test-disk');
|
||||
expect(result).toEqual(mockArrayData);
|
||||
expect(emcmd).toHaveBeenCalledWith({
|
||||
mount: 'apply',
|
||||
'diskId.test-disk': '1',
|
||||
});
|
||||
});
|
||||
|
||||
it('should unmount array disk', async () => {
|
||||
// Mock array as running
|
||||
vi.mocked(getters.emhttp).mockReturnValue({
|
||||
var: {
|
||||
mdState: ArrayState.STARTED,
|
||||
},
|
||||
} as any);
|
||||
|
||||
const result = await service.unmountArrayDisk('test-disk');
|
||||
expect(result).toEqual(mockArrayData);
|
||||
expect(emcmd).toHaveBeenCalledWith({
|
||||
unmount: 'apply',
|
||||
'diskId.test-disk': '1',
|
||||
});
|
||||
});
|
||||
|
||||
it('should clear array disk statistics', async () => {
|
||||
// Mock array as running
|
||||
vi.mocked(getters.emhttp).mockReturnValue({
|
||||
var: {
|
||||
mdState: ArrayState.STARTED,
|
||||
},
|
||||
} as any);
|
||||
|
||||
const result = await service.clearArrayDiskStatistics('test-disk');
|
||||
expect(result).toEqual(mockArrayData);
|
||||
expect(emcmd).toHaveBeenCalledWith({
|
||||
clearStats: 'apply',
|
||||
'diskId.test-disk': '1',
|
||||
});
|
||||
});
|
||||
|
||||
it('should throw error when array is running for add disk', async () => {
|
||||
// Mock array as running
|
||||
vi.mocked(getters.emhttp).mockReturnValue({
|
||||
var: {
|
||||
mdState: ArrayState.STARTED,
|
||||
},
|
||||
} as any);
|
||||
|
||||
const input: ArrayDiskInput = {
|
||||
id: 'test-disk',
|
||||
slot: 1,
|
||||
};
|
||||
await expect(service.addDiskToArray(input)).rejects.toThrow(
|
||||
'Array needs to be stopped before any changes can occur.'
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw error when array is running for remove disk', async () => {
|
||||
// Mock array as running
|
||||
vi.mocked(getters.emhttp).mockReturnValue({
|
||||
var: {
|
||||
mdState: ArrayState.STARTED,
|
||||
},
|
||||
} as any);
|
||||
|
||||
const input: ArrayDiskInput = {
|
||||
id: 'test-disk',
|
||||
slot: 1,
|
||||
};
|
||||
await expect(service.removeDiskFromArray(input)).rejects.toThrow(
|
||||
'Array needs to be stopped before any changes can occur.'
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw error when array is not running for mount disk', async () => {
|
||||
await expect(service.mountArrayDisk('test-disk')).rejects.toThrow(
|
||||
'Array must be running to mount disks'
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw error when array is not running for unmount disk', async () => {
|
||||
await expect(service.unmountArrayDisk('test-disk')).rejects.toThrow(
|
||||
'Array must be running to unmount disks'
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw error when array is not running for clear disk statistics', async () => {
|
||||
await expect(service.clearArrayDiskStatistics('test-disk')).rejects.toThrow(
|
||||
'Array must be running to clear disk statistics'
|
||||
);
|
||||
});
|
||||
});
|
||||
145
api/src/unraid-api/graph/resolvers/array/array.service.ts
Normal file
145
api/src/unraid-api/graph/resolvers/array/array.service.ts
Normal file
@@ -0,0 +1,145 @@
|
||||
import { BadRequestException, Injectable } from '@nestjs/common';
|
||||
|
||||
import { capitalCase, constantCase } from 'change-case';
|
||||
|
||||
import type { ArrayDiskInput, ArrayStateInput, ArrayType } from '@app/graphql/generated/api/types.js';
|
||||
import { AppError } from '@app/core/errors/app-error.js';
|
||||
import { ArrayRunningError } from '@app/core/errors/array-running-error.js';
|
||||
import { getArrayData } from '@app/core/modules/array/get-array-data.js';
|
||||
import { emcmd } from '@app/core/utils/clients/emcmd.js';
|
||||
import { arrayIsRunning as arrayIsRunningUtil } from '@app/core/utils/index.js';
|
||||
import {
|
||||
ArrayPendingState,
|
||||
ArrayState,
|
||||
ArrayStateInputState,
|
||||
} from '@app/graphql/generated/api/types.js';
|
||||
|
||||
@Injectable()
|
||||
export class ArrayService {
|
||||
private pendingState: ArrayPendingState | null = null;
|
||||
|
||||
/**
|
||||
* Is the array running?
|
||||
* @todo Refactor this to include this util in the service directly
|
||||
*/
|
||||
private arrayIsRunning() {
|
||||
return arrayIsRunningUtil();
|
||||
}
|
||||
|
||||
async updateArrayState({ desiredState }: ArrayStateInput): Promise<ArrayType> {
|
||||
const startState = this.arrayIsRunning() ? ArrayState.STARTED : ArrayState.STOPPED;
|
||||
const pendingState =
|
||||
desiredState === ArrayStateInputState.STOP
|
||||
? ArrayPendingState.STOPPING
|
||||
: ArrayPendingState.STARTING;
|
||||
|
||||
// Prevent this running multiple times at once
|
||||
if (this.pendingState) {
|
||||
throw new BadRequestException(
|
||||
new AppError(`Array state is still being updated. Changing to ${pendingState}`)
|
||||
);
|
||||
}
|
||||
|
||||
// Prevent starting/stopping array when it's already in the same state
|
||||
if (
|
||||
(this.arrayIsRunning() && desiredState === ArrayStateInputState.START) ||
|
||||
(!this.arrayIsRunning() && desiredState === ArrayStateInputState.STOP)
|
||||
) {
|
||||
throw new BadRequestException(new AppError(`The array is already ${startState}`));
|
||||
}
|
||||
|
||||
// Set lock then start/stop array
|
||||
this.pendingState = pendingState;
|
||||
const command = {
|
||||
[`cmd${capitalCase(desiredState)}`]: capitalCase(desiredState),
|
||||
startState: constantCase(startState),
|
||||
};
|
||||
|
||||
try {
|
||||
await emcmd(command);
|
||||
} finally {
|
||||
this.pendingState = null;
|
||||
}
|
||||
|
||||
// Get new array JSON
|
||||
const array = getArrayData();
|
||||
|
||||
return array;
|
||||
}
|
||||
|
||||
async addDiskToArray(input: ArrayDiskInput): Promise<ArrayType> {
|
||||
if (this.arrayIsRunning()) {
|
||||
throw new ArrayRunningError();
|
||||
}
|
||||
|
||||
const { id: diskId, slot: preferredSlot } = input;
|
||||
const slot = preferredSlot?.toString() ?? '';
|
||||
|
||||
// Add disk
|
||||
await emcmd({
|
||||
changeDevice: 'apply',
|
||||
[`slotId.${slot}`]: diskId,
|
||||
});
|
||||
|
||||
return getArrayData();
|
||||
}
|
||||
|
||||
async removeDiskFromArray(input: ArrayDiskInput): Promise<ArrayType> {
|
||||
if (this.arrayIsRunning()) {
|
||||
throw new ArrayRunningError();
|
||||
}
|
||||
|
||||
const { slot } = input;
|
||||
const slotStr = slot?.toString() ?? '';
|
||||
|
||||
// Remove disk
|
||||
await emcmd({
|
||||
changeDevice: 'apply',
|
||||
[`slotId.${slotStr}`]: '',
|
||||
});
|
||||
|
||||
return getArrayData();
|
||||
}
|
||||
|
||||
async mountArrayDisk(id: string): Promise<ArrayType> {
|
||||
if (!this.arrayIsRunning()) {
|
||||
throw new BadRequestException('Array must be running to mount disks');
|
||||
}
|
||||
|
||||
// Mount disk
|
||||
await emcmd({
|
||||
mount: 'apply',
|
||||
[`diskId.${id}`]: '1',
|
||||
});
|
||||
|
||||
return getArrayData();
|
||||
}
|
||||
|
||||
async unmountArrayDisk(id: string): Promise<ArrayType> {
|
||||
if (!this.arrayIsRunning()) {
|
||||
throw new BadRequestException('Array must be running to unmount disks');
|
||||
}
|
||||
|
||||
// Unmount disk
|
||||
await emcmd({
|
||||
unmount: 'apply',
|
||||
[`diskId.${id}`]: '1',
|
||||
});
|
||||
|
||||
return getArrayData();
|
||||
}
|
||||
|
||||
async clearArrayDiskStatistics(id: string): Promise<ArrayType> {
|
||||
if (!this.arrayIsRunning()) {
|
||||
throw new BadRequestException('Array must be running to clear disk statistics');
|
||||
}
|
||||
|
||||
// Clear disk statistics
|
||||
await emcmd({
|
||||
clearStats: 'apply',
|
||||
[`diskId.${id}`]: '1',
|
||||
});
|
||||
|
||||
return getArrayData();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import { ResolveField, Resolver } from '@nestjs/graphql';
|
||||
|
||||
@Resolver('Mutation')
|
||||
export class MutationResolver {
|
||||
@ResolveField()
|
||||
public async array() {
|
||||
return {
|
||||
__typename: 'ArrayMutations',
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,9 @@ import { Module } from '@nestjs/common';
|
||||
import { AuthModule } from '@app/unraid-api/auth/auth.module.js';
|
||||
import { ConnectSettingsService } from '@app/unraid-api/graph/connect/connect-settings.service.js';
|
||||
import { ApiKeyResolver } from '@app/unraid-api/graph/resolvers/api-key/api-key.resolver.js';
|
||||
import { ArrayMutationsResolver } from '@app/unraid-api/graph/resolvers/array/array.mutations.resolver.js';
|
||||
import { ArrayResolver } from '@app/unraid-api/graph/resolvers/array/array.resolver.js';
|
||||
import { ArrayService } from '@app/unraid-api/graph/resolvers/array/array.service.js';
|
||||
import { CloudResolver } from '@app/unraid-api/graph/resolvers/cloud/cloud.resolver.js';
|
||||
import { ConfigResolver } from '@app/unraid-api/graph/resolvers/config/config.resolver.js';
|
||||
import { DisksResolver } from '@app/unraid-api/graph/resolvers/disks/disks.resolver.js';
|
||||
@@ -14,6 +16,7 @@ import { InfoResolver } from '@app/unraid-api/graph/resolvers/info/info.resolver
|
||||
import { LogsResolver } from '@app/unraid-api/graph/resolvers/logs/logs.resolver.js';
|
||||
import { LogsService } from '@app/unraid-api/graph/resolvers/logs/logs.service.js';
|
||||
import { MeResolver } from '@app/unraid-api/graph/resolvers/me/me.resolver.js';
|
||||
import { MutationResolver } from '@app/unraid-api/graph/resolvers/mutation/mutation.resolver.js';
|
||||
import { NotificationsResolver } from '@app/unraid-api/graph/resolvers/notifications/notifications.resolver.js';
|
||||
import { NotificationsService } from '@app/unraid-api/graph/resolvers/notifications/notifications.service.js';
|
||||
import { OnlineResolver } from '@app/unraid-api/graph/resolvers/online/online.resolver.js';
|
||||
@@ -27,6 +30,8 @@ import { VmsResolver } from '@app/unraid-api/graph/resolvers/vms/vms.resolver.js
|
||||
imports: [AuthModule],
|
||||
providers: [
|
||||
ArrayResolver,
|
||||
ArrayMutationsResolver,
|
||||
ArrayService,
|
||||
ApiKeyResolver,
|
||||
CloudResolver,
|
||||
ConfigResolver,
|
||||
@@ -34,6 +39,7 @@ import { VmsResolver } from '@app/unraid-api/graph/resolvers/vms/vms.resolver.js
|
||||
DisplayResolver,
|
||||
DockerResolver,
|
||||
FlashResolver,
|
||||
MutationResolver,
|
||||
InfoResolver,
|
||||
NotificationsResolver,
|
||||
OnlineResolver,
|
||||
|
||||
Reference in New Issue
Block a user