mirror of
https://github.com/unraid/api.git
synced 2025-12-31 13:39:52 -06:00
feat: add parityCheckStatus field to array query (#1611)
Responds with a ParityCheck:
```ts
type ParityCheck {
"""Date of the parity check"""
date: DateTime
"""Duration of the parity check in seconds"""
duration: Int
"""Speed of the parity check, in MB/s"""
speed: String
"""Status of the parity check"""
status: ParityCheckStatus!
"""Number of errors during the parity check"""
errors: Int
"""Progress percentage of the parity check"""
progress: Int
"""Whether corrections are being written to parity"""
correcting: Boolean
"""Whether the parity check is paused"""
paused: Boolean
"""Whether the parity check is running"""
running: Boolean
}
enum ParityCheckStatus {
NEVER_RUN = 'never_run',
RUNNING = 'running',
PAUSED = 'paused',
COMPLETED = 'completed',
CANCELLED = 'cancelled',
FAILED = 'failed',
}
```
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
- New Features
- Exposes a structured parity-check status for arrays with detailed
fields (status enum: NEVER_RUN, RUNNING, PAUSED, COMPLETED, CANCELLED,
FAILED), date, duration, speed, progress, errors, and running/paused
flags.
- Tests
- Adds comprehensive unit tests covering all parity-check states,
numeric edge cases, speed/date/duration/progress calculations, and
real-world scenarios.
- Refactor
- Safer numeric/date parsing and a new numeric-conversion helper; minor
formatting/cleanup in shared utilities.
<!-- 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:
@@ -14,6 +14,49 @@ directive @usePermissions(
|
|||||||
possession: AuthPossession
|
possession: AuthPossession
|
||||||
) on FIELD_DEFINITION
|
) on FIELD_DEFINITION
|
||||||
|
|
||||||
|
type ParityCheck {
|
||||||
|
"""Date of the parity check"""
|
||||||
|
date: DateTime
|
||||||
|
|
||||||
|
"""Duration of the parity check in seconds"""
|
||||||
|
duration: Int
|
||||||
|
|
||||||
|
"""Speed of the parity check, in MB/s"""
|
||||||
|
speed: String
|
||||||
|
|
||||||
|
"""Status of the parity check"""
|
||||||
|
status: ParityCheckStatus!
|
||||||
|
|
||||||
|
"""Number of errors during the parity check"""
|
||||||
|
errors: Int
|
||||||
|
|
||||||
|
"""Progress percentage of the parity check"""
|
||||||
|
progress: Int
|
||||||
|
|
||||||
|
"""Whether corrections are being written to parity"""
|
||||||
|
correcting: Boolean
|
||||||
|
|
||||||
|
"""Whether the parity check is paused"""
|
||||||
|
paused: Boolean
|
||||||
|
|
||||||
|
"""Whether the parity check is running"""
|
||||||
|
running: Boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
"""
|
||||||
|
A date-time string at UTC, such as 2019-12-03T09:54:33Z, compliant with the date-time format.
|
||||||
|
"""
|
||||||
|
scalar DateTime
|
||||||
|
|
||||||
|
enum ParityCheckStatus {
|
||||||
|
NEVER_RUN
|
||||||
|
RUNNING
|
||||||
|
PAUSED
|
||||||
|
COMPLETED
|
||||||
|
CANCELLED
|
||||||
|
FAILED
|
||||||
|
}
|
||||||
|
|
||||||
type Capacity {
|
type Capacity {
|
||||||
"""Free capacity"""
|
"""Free capacity"""
|
||||||
free: String!
|
free: String!
|
||||||
@@ -156,6 +199,9 @@ type UnraidArray implements Node {
|
|||||||
"""Parity disks in the current array"""
|
"""Parity disks in the current array"""
|
||||||
parities: [ArrayDisk!]!
|
parities: [ArrayDisk!]!
|
||||||
|
|
||||||
|
"""Current parity check status"""
|
||||||
|
parityCheckStatus: ParityCheck!
|
||||||
|
|
||||||
"""Data disks in the current array"""
|
"""Data disks in the current array"""
|
||||||
disks: [ArrayDisk!]!
|
disks: [ArrayDisk!]!
|
||||||
|
|
||||||
@@ -836,40 +882,6 @@ input DeleteRCloneRemoteInput {
|
|||||||
name: String!
|
name: String!
|
||||||
}
|
}
|
||||||
|
|
||||||
type ParityCheck {
|
|
||||||
"""Date of the parity check"""
|
|
||||||
date: DateTime
|
|
||||||
|
|
||||||
"""Duration of the parity check in seconds"""
|
|
||||||
duration: Int
|
|
||||||
|
|
||||||
"""Speed of the parity check, in MB/s"""
|
|
||||||
speed: String
|
|
||||||
|
|
||||||
"""Status of the parity check"""
|
|
||||||
status: String
|
|
||||||
|
|
||||||
"""Number of errors during the parity check"""
|
|
||||||
errors: Int
|
|
||||||
|
|
||||||
"""Progress percentage of the parity check"""
|
|
||||||
progress: Int
|
|
||||||
|
|
||||||
"""Whether corrections are being written to parity"""
|
|
||||||
correcting: Boolean
|
|
||||||
|
|
||||||
"""Whether the parity check is paused"""
|
|
||||||
paused: Boolean
|
|
||||||
|
|
||||||
"""Whether the parity check is running"""
|
|
||||||
running: Boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
"""
|
|
||||||
A date-time string at UTC, such as 2019-12-03T09:54:33Z, compliant with the date-time format.
|
|
||||||
"""
|
|
||||||
scalar DateTime
|
|
||||||
|
|
||||||
type Config implements Node {
|
type Config implements Node {
|
||||||
id: PrefixedID!
|
id: PrefixedID!
|
||||||
valid: Boolean
|
valid: Boolean
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { GraphQLError } from 'graphql';
|
import { GraphQLError } from 'graphql';
|
||||||
import { sum } from 'lodash-es';
|
import { sum } from 'lodash-es';
|
||||||
|
|
||||||
|
import { getParityCheckStatus } from '@app/core/modules/array/parity-check-status.js';
|
||||||
import { store } from '@app/store/index.js';
|
import { store } from '@app/store/index.js';
|
||||||
import { FileLoadStatus } from '@app/store/types.js';
|
import { FileLoadStatus } from '@app/store/types.js';
|
||||||
import {
|
import {
|
||||||
@@ -61,5 +62,6 @@ export const getArrayData = (getState = store.getState): UnraidArray => {
|
|||||||
parities,
|
parities,
|
||||||
disks,
|
disks,
|
||||||
caches,
|
caches,
|
||||||
|
parityCheckStatus: getParityCheckStatus(emhttp.var),
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
1080
api/src/core/modules/array/parity-check-status.test.ts
Normal file
1080
api/src/core/modules/array/parity-check-status.test.ts
Normal file
File diff suppressed because it is too large
Load Diff
72
api/src/core/modules/array/parity-check-status.ts
Normal file
72
api/src/core/modules/array/parity-check-status.ts
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
import { toNumberAlways } from '@unraid/shared/util/data.js';
|
||||||
|
|
||||||
|
import type { Var } from '@app/core/types/states/var.js';
|
||||||
|
import type { ParityCheck } from '@app/unraid-api/graph/resolvers/array/parity.model.js';
|
||||||
|
|
||||||
|
export enum ParityCheckStatus {
|
||||||
|
NEVER_RUN = 'never_run',
|
||||||
|
RUNNING = 'running',
|
||||||
|
PAUSED = 'paused',
|
||||||
|
COMPLETED = 'completed',
|
||||||
|
CANCELLED = 'cancelled',
|
||||||
|
FAILED = 'failed',
|
||||||
|
}
|
||||||
|
|
||||||
|
function calculateParitySpeed(deltaTime: number, deltaBlocks: number) {
|
||||||
|
if (deltaTime === 0 || deltaBlocks === 0) return 0;
|
||||||
|
const deltaBytes = deltaBlocks * 1024;
|
||||||
|
const speedMBps = deltaBytes / deltaTime / 1024 / 1024;
|
||||||
|
return Math.round(speedMBps);
|
||||||
|
}
|
||||||
|
|
||||||
|
type RelevantVarData = Pick<
|
||||||
|
Var,
|
||||||
|
| 'mdResyncPos'
|
||||||
|
| 'mdResyncDt'
|
||||||
|
| 'sbSyncExit'
|
||||||
|
| 'sbSynced'
|
||||||
|
| 'sbSynced2'
|
||||||
|
| 'mdResyncDb'
|
||||||
|
| 'mdResyncSize'
|
||||||
|
>;
|
||||||
|
|
||||||
|
function getStatusFromVarData(varData: RelevantVarData): ParityCheckStatus {
|
||||||
|
const { mdResyncPos, mdResyncDt, sbSyncExit, sbSynced, sbSynced2 } = varData;
|
||||||
|
const mdResyncDtNumber = toNumberAlways(mdResyncDt, 0);
|
||||||
|
const sbSyncExitNumber = toNumberAlways(sbSyncExit, 0);
|
||||||
|
|
||||||
|
switch (true) {
|
||||||
|
case mdResyncPos > 0:
|
||||||
|
return mdResyncDtNumber > 0 ? ParityCheckStatus.RUNNING : ParityCheckStatus.PAUSED;
|
||||||
|
case sbSynced === 0:
|
||||||
|
return ParityCheckStatus.NEVER_RUN;
|
||||||
|
case sbSyncExitNumber === -4:
|
||||||
|
return ParityCheckStatus.CANCELLED;
|
||||||
|
case sbSyncExitNumber !== 0:
|
||||||
|
return ParityCheckStatus.FAILED;
|
||||||
|
case sbSynced2 > 0:
|
||||||
|
return ParityCheckStatus.COMPLETED;
|
||||||
|
default:
|
||||||
|
return ParityCheckStatus.NEVER_RUN;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getParityCheckStatus(varData: RelevantVarData): ParityCheck {
|
||||||
|
const { sbSynced, sbSynced2, mdResyncDt, mdResyncDb, mdResyncPos, mdResyncSize } = varData;
|
||||||
|
const deltaTime = toNumberAlways(mdResyncDt, 0);
|
||||||
|
const deltaBlocks = toNumberAlways(mdResyncDb, 0);
|
||||||
|
|
||||||
|
// seconds since epoch (unix timestamp)
|
||||||
|
const now = sbSynced2 > 0 ? sbSynced2 : Date.now() / 1000;
|
||||||
|
return {
|
||||||
|
status: getStatusFromVarData(varData),
|
||||||
|
speed: String(calculateParitySpeed(deltaTime, deltaBlocks)),
|
||||||
|
date: sbSynced > 0 ? new Date(sbSynced * 1000) : undefined,
|
||||||
|
duration: sbSynced > 0 ? Math.round(now - sbSynced) : undefined,
|
||||||
|
// percentage as integer, clamped to [0, 100]
|
||||||
|
progress:
|
||||||
|
mdResyncSize <= 0
|
||||||
|
? 0
|
||||||
|
: Math.round(Math.min(100, Math.max(0, (mdResyncPos / mdResyncSize) * 100))),
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -68,11 +68,24 @@ export type Var = {
|
|||||||
mdNumStripes: number;
|
mdNumStripes: number;
|
||||||
mdNumStripesDefault: number;
|
mdNumStripesDefault: number;
|
||||||
mdNumStripesStatus: string;
|
mdNumStripesStatus: string;
|
||||||
|
/**
|
||||||
|
* Serves a dual purpose depending on context:
|
||||||
|
* - Total size of the operation (in sectors/blocks)
|
||||||
|
* - Running state indicator (0 = paused, >0 = running)
|
||||||
|
*/
|
||||||
mdResync: number;
|
mdResync: number;
|
||||||
mdResyncAction: string;
|
mdResyncAction: string;
|
||||||
mdResyncCorr: string;
|
mdResyncCorr: string;
|
||||||
mdResyncDb: string;
|
mdResyncDb: string;
|
||||||
|
/** Average time interval (delta time) in seconds of current parity operations */
|
||||||
mdResyncDt: string;
|
mdResyncDt: string;
|
||||||
|
/**
|
||||||
|
* Current position in the parity operation (in sectors/blocks).
|
||||||
|
* When mdResyncPos > 0, a parity operation is active.
|
||||||
|
* When mdResyncPos = 0, no parity operation is running.
|
||||||
|
*
|
||||||
|
* Used to calculate progress percentage.
|
||||||
|
*/
|
||||||
mdResyncPos: number;
|
mdResyncPos: number;
|
||||||
mdResyncSize: number;
|
mdResyncSize: number;
|
||||||
mdState: ArrayState;
|
mdState: ArrayState;
|
||||||
@@ -136,9 +149,36 @@ export type Var = {
|
|||||||
sbName: string;
|
sbName: string;
|
||||||
sbNumDisks: number;
|
sbNumDisks: number;
|
||||||
sbState: string;
|
sbState: string;
|
||||||
|
/**
|
||||||
|
* Unix timestamp when parity operation started.
|
||||||
|
* When sbSynced = 0, indicates no parity check has ever been run.
|
||||||
|
*
|
||||||
|
* Used to calculate elapsed time during active operations.
|
||||||
|
*/
|
||||||
sbSynced: number;
|
sbSynced: number;
|
||||||
sbSynced2: number;
|
sbSynced2: number;
|
||||||
|
/**
|
||||||
|
* Unix timestamp when parity operation completed (successfully or with errors).
|
||||||
|
* Used to display completion time in status messages.
|
||||||
|
*
|
||||||
|
* When sbSynced2 = 0, indicates operation started but not yet finished
|
||||||
|
*/
|
||||||
sbSyncErrs: number;
|
sbSyncErrs: number;
|
||||||
|
/**
|
||||||
|
* Exit status code that indicates how the last parity operation completed, following standard Unix conventions.
|
||||||
|
*
|
||||||
|
* sbSyncExit = 0 - Successful Completion
|
||||||
|
* - Parity operation completed normally without errors
|
||||||
|
* - Used to calculate speed and display success message
|
||||||
|
*
|
||||||
|
* sbSyncExit = -4 - Aborted/Cancelled
|
||||||
|
* - Operation was manually cancelled by user
|
||||||
|
* - Displays as "aborted" in the UI
|
||||||
|
*
|
||||||
|
* sbSyncExit ≠ 0 (other values) - Failed/Incomplete
|
||||||
|
* - Operation failed due to errors or other issues
|
||||||
|
* - Displays the numeric error code
|
||||||
|
*/
|
||||||
sbSyncExit: string;
|
sbSyncExit: string;
|
||||||
sbUpdated: string;
|
sbUpdated: string;
|
||||||
sbVersion: string;
|
sbVersion: string;
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ import { PrefixedID } from '@unraid/shared/prefixed-id-scalar.js';
|
|||||||
import { IsEnum, IsInt, IsOptional, IsString } from 'class-validator';
|
import { IsEnum, IsInt, IsOptional, IsString } from 'class-validator';
|
||||||
import { GraphQLBigInt } from 'graphql-scalars';
|
import { GraphQLBigInt } from 'graphql-scalars';
|
||||||
|
|
||||||
|
import { ParityCheck } from '@app/unraid-api/graph/resolvers/array/parity.model.js';
|
||||||
|
|
||||||
@ObjectType()
|
@ObjectType()
|
||||||
export class Capacity {
|
export class Capacity {
|
||||||
@Field(() => String, { description: 'Free capacity' })
|
@Field(() => String, { description: 'Free capacity' })
|
||||||
@@ -142,6 +144,9 @@ export class UnraidArray extends Node {
|
|||||||
@Field(() => [ArrayDisk], { description: 'Parity disks in the current array' })
|
@Field(() => [ArrayDisk], { description: 'Parity disks in the current array' })
|
||||||
parities!: ArrayDisk[];
|
parities!: ArrayDisk[];
|
||||||
|
|
||||||
|
@Field(() => ParityCheck, { description: 'Current parity check status' })
|
||||||
|
parityCheckStatus!: ParityCheck;
|
||||||
|
|
||||||
@Field(() => [ArrayDisk], { description: 'Data disks in the current array' })
|
@Field(() => [ArrayDisk], { description: 'Data disks in the current array' })
|
||||||
disks!: ArrayDisk[];
|
disks!: ArrayDisk[];
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|||||||
|
|
||||||
import { ArrayRunningError } from '@app/core/errors/array-running-error.js';
|
import { ArrayRunningError } from '@app/core/errors/array-running-error.js';
|
||||||
import { getArrayData as getArrayDataUtil } from '@app/core/modules/array/get-array-data.js';
|
import { getArrayData as getArrayDataUtil } from '@app/core/modules/array/get-array-data.js';
|
||||||
|
import { ParityCheckStatus } from '@app/core/modules/array/parity-check-status.js';
|
||||||
import { emcmd } from '@app/core/utils/clients/emcmd.js';
|
import { emcmd } from '@app/core/utils/clients/emcmd.js';
|
||||||
import {
|
import {
|
||||||
ArrayDiskInput,
|
ArrayDiskInput,
|
||||||
@@ -82,6 +83,13 @@ describe('ArrayService', () => {
|
|||||||
parities: [],
|
parities: [],
|
||||||
disks: [],
|
disks: [],
|
||||||
caches: [],
|
caches: [],
|
||||||
|
parityCheckStatus: {
|
||||||
|
status: ParityCheckStatus.NEVER_RUN,
|
||||||
|
progress: 0,
|
||||||
|
date: undefined,
|
||||||
|
duration: 0,
|
||||||
|
speed: '0',
|
||||||
|
},
|
||||||
};
|
};
|
||||||
mockGetArrayDataUtil.mockResolvedValue(mockArrayData);
|
mockGetArrayDataUtil.mockResolvedValue(mockArrayData);
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,10 @@
|
|||||||
import { Field, GraphQLISODateTime, Int, ObjectType } from '@nestjs/graphql';
|
import { Field, GraphQLISODateTime, Int, ObjectType, registerEnumType } from '@nestjs/graphql';
|
||||||
|
|
||||||
|
import { ParityCheckStatus } from '@app/core/modules/array/parity-check-status.js';
|
||||||
|
|
||||||
|
registerEnumType(ParityCheckStatus, {
|
||||||
|
name: 'ParityCheckStatus',
|
||||||
|
});
|
||||||
|
|
||||||
@ObjectType()
|
@ObjectType()
|
||||||
export class ParityCheck {
|
export class ParityCheck {
|
||||||
@@ -11,8 +17,8 @@ export class ParityCheck {
|
|||||||
@Field(() => String, { nullable: true, description: 'Speed of the parity check, in MB/s' })
|
@Field(() => String, { nullable: true, description: 'Speed of the parity check, in MB/s' })
|
||||||
speed?: string;
|
speed?: string;
|
||||||
|
|
||||||
@Field(() => String, { nullable: true, description: 'Status of the parity check' })
|
@Field(() => ParityCheckStatus, { description: 'Status of the parity check' })
|
||||||
status?: string;
|
status!: ParityCheckStatus;
|
||||||
|
|
||||||
@Field(() => Int, { nullable: true, description: 'Number of errors during the parity check' })
|
@Field(() => Int, { nullable: true, description: 'Number of errors during the parity check' })
|
||||||
errors?: number;
|
errors?: number;
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
import { readFile } from 'fs/promises';
|
import { readFile } from 'fs/promises';
|
||||||
|
|
||||||
|
import { toNumberAlways } from '@unraid/shared/util/data.js';
|
||||||
import { GraphQLError } from 'graphql';
|
import { GraphQLError } from 'graphql';
|
||||||
|
|
||||||
|
import { ParityCheckStatus } from '@app/core/modules/array/parity-check-status.js';
|
||||||
import { emcmd } from '@app/core/utils/index.js';
|
import { emcmd } from '@app/core/utils/index.js';
|
||||||
import { ParityCheck } from '@app/unraid-api/graph/resolvers/array/parity.model.js';
|
import { ParityCheck } from '@app/unraid-api/graph/resolvers/array/parity.model.js';
|
||||||
|
|
||||||
@@ -22,16 +24,30 @@ export class ParityService {
|
|||||||
const lines = history.toString().trim().split('\n').reverse();
|
const lines = history.toString().trim().split('\n').reverse();
|
||||||
return lines.map<ParityCheck>((line) => {
|
return lines.map<ParityCheck>((line) => {
|
||||||
const [date, duration, speed, status, errors = '0'] = line.split('|');
|
const [date, duration, speed, status, errors = '0'] = line.split('|');
|
||||||
|
const parsedDate = new Date(date);
|
||||||
|
const safeDate = Number.isNaN(parsedDate.getTime()) ? undefined : parsedDate;
|
||||||
|
const durationNumber = Number(duration);
|
||||||
|
const safeDuration = Number.isNaN(durationNumber) ? undefined : durationNumber;
|
||||||
return {
|
return {
|
||||||
date: new Date(date),
|
date: safeDate,
|
||||||
duration: Number.parseInt(duration, 10),
|
duration: safeDuration,
|
||||||
speed: speed ?? 'Unavailable',
|
speed: speed ?? 'Unavailable',
|
||||||
status: status === '-4' ? 'Cancelled' : 'OK',
|
// use http 422 (unprocessable entity) as fallback to differentiate from unix error codes
|
||||||
|
// when status is not a number.
|
||||||
|
status: this.statusCodeToStatusEnum(toNumberAlways(status, 422)),
|
||||||
errors: Number.parseInt(errors, 10),
|
errors: Number.parseInt(errors, 10),
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
statusCodeToStatusEnum(statusCode: number): ParityCheckStatus {
|
||||||
|
return statusCode === -4
|
||||||
|
? ParityCheckStatus.CANCELLED
|
||||||
|
: toNumberAlways(statusCode, 0) === 0
|
||||||
|
? ParityCheckStatus.COMPLETED
|
||||||
|
: ParityCheckStatus.FAILED;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Updates the parity check state
|
* Updates the parity check state
|
||||||
* @param wantedState - The desired state for the parity check ('pause', 'resume', 'cancel', 'start')
|
* @param wantedState - The desired state for the parity check ('pause', 'resume', 'cancel', 'start')
|
||||||
|
|||||||
@@ -16,15 +16,15 @@ import type { Get } from "type-fest";
|
|||||||
* @returns An array of strings
|
* @returns An array of strings
|
||||||
*/
|
*/
|
||||||
export function csvStringToArray(
|
export function csvStringToArray(
|
||||||
csvString?: string | null,
|
csvString?: string | null,
|
||||||
opts: { noEmpty?: boolean } = { noEmpty: true }
|
opts: { noEmpty?: boolean } = { noEmpty: true }
|
||||||
): string[] {
|
): string[] {
|
||||||
if (!csvString) return [];
|
if (!csvString) return [];
|
||||||
const result = csvString.split(',').map((item) => item.trim());
|
const result = csvString.split(",").map((item) => item.trim());
|
||||||
if (opts.noEmpty) {
|
if (opts.noEmpty) {
|
||||||
return result.filter((item) => item !== '');
|
return result.filter((item) => item !== "");
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -41,8 +41,23 @@ export function csvStringToArray(
|
|||||||
* @returns The nested value or undefined if the path is invalid
|
* @returns The nested value or undefined if the path is invalid
|
||||||
*/
|
*/
|
||||||
export function getNestedValue<TObj extends object, TPath extends string>(
|
export function getNestedValue<TObj extends object, TPath extends string>(
|
||||||
obj: TObj,
|
obj: TObj,
|
||||||
path: TPath
|
path: TPath
|
||||||
): Get<TObj, TPath> {
|
): Get<TObj, TPath> {
|
||||||
return path.split('.').reduce((acc, part) => acc?.[part], obj as any) as Get<TObj, TPath>;
|
return path.split(".").reduce((acc, part) => acc?.[part], obj as any) as Get<
|
||||||
|
TObj,
|
||||||
|
TPath
|
||||||
|
>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts a value to a number. If the value is NaN, returns the default value.
|
||||||
|
*
|
||||||
|
* @param value - The value to convert to a number
|
||||||
|
* @param defaultValue - The default value to return if the value is NaN. Default is 0.
|
||||||
|
* @returns The number value or the default value if the value is NaN
|
||||||
|
*/
|
||||||
|
export function toNumberAlways(value: unknown, defaultValue = 0): number {
|
||||||
|
const num = Number(value);
|
||||||
|
return Number.isNaN(num) ? defaultValue : num;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user