-
Couldn't load subscription status.
- Fork 11
chore: add organizer data structure for docker folders
#1540
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
e885d0e
7444bf5
a5b9c74
426612f
b54e257
c286126
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,230 @@ | ||
| /** | ||
| * @fileoverview Type-safe sequential validation processor | ||
| * | ||
| * This module provides a flexible validation system that allows you to chain multiple | ||
| * validation steps together in a type-safe manner. It supports both fail-fast and | ||
| * continue-on-error modes, with comprehensive error collection and reporting. | ||
| * | ||
| * Key features: | ||
| * - Type-safe validation pipeline creation | ||
| * - Sequential validation step execution | ||
| * - Configurable fail-fast behavior (global or per-step) | ||
| * - Comprehensive error collection with typed results | ||
| * - Helper functions for common validation result interpretations | ||
| * | ||
| * @example | ||
| * ```typescript | ||
| * const validator = createValidationProcessor({ | ||
| * steps: [ | ||
| * { | ||
| * name: 'required', | ||
| * validator: (input: string) => input.length > 0, | ||
| * isError: ResultInterpreters.booleanMeansSuccess | ||
| * }, | ||
| * { | ||
| * name: 'email', | ||
| * validator: (input: string) => /\S+@\S+\.\S+/.test(input), | ||
| * isError: ResultInterpreters.booleanMeansSuccess | ||
| * } | ||
| * ] | ||
| * }); | ||
| * | ||
| * const result = validator('user@example.com'); | ||
| * if (!result.isValid) { | ||
| * console.log('Validation errors:', result.errors); | ||
| * } | ||
| * ``` | ||
| */ | ||
|
|
||
| export type ValidationStepConfig<TInput, TResult, TName extends string = string> = { | ||
| name: TName; | ||
| validator: (input: TInput) => TResult; | ||
| isError: (result: TResult) => boolean; | ||
| alwaysFailFast?: boolean; | ||
| }; | ||
|
|
||
| export interface ValidationPipelineConfig { | ||
| failFast?: boolean; | ||
| } | ||
|
|
||
| export type ValidationPipelineDefinition< | ||
| TInput, | ||
| TSteps extends readonly ValidationStepConfig<TInput, any, string>[], | ||
| > = { | ||
| steps: TSteps; | ||
| }; | ||
|
|
||
| export type ExtractStepResults<TSteps extends readonly ValidationStepConfig<any, any, string>[]> = { | ||
| [K in TSteps[number]['name']]: Extract<TSteps[number], { name: K }> extends ValidationStepConfig< | ||
| any, | ||
| infer R, | ||
| K | ||
| > | ||
| ? R | ||
| : never; | ||
| }; | ||
|
|
||
| export type ValidationResult<TSteps extends readonly ValidationStepConfig<any, any, string>[]> = { | ||
| isValid: boolean; | ||
| errors: Partial<ExtractStepResults<TSteps>>; | ||
| }; | ||
|
|
||
| // Util: convert a union to an intersection | ||
| type UnionToIntersection<U> = (U extends any ? (arg: U) => void : never) extends (arg: infer I) => void | ||
| ? I | ||
| : never; | ||
|
|
||
| // Extract the *intersection* of all input types required by the steps. This guarantees that | ||
| // the resulting processor knows about every property that any individual step relies on. | ||
| // We purposely compute an intersection (not a union) so that all required fields are present. | ||
| type ExtractInputType<TSteps extends readonly ValidationStepConfig<any, any, string>[]> = | ||
| UnionToIntersection< | ||
| TSteps[number] extends ValidationStepConfig<infer TInput, any, string> ? TInput : never | ||
| >; | ||
|
|
||
| /** | ||
| * Creates a type-safe validation processor that executes a series of validation steps | ||
| * sequentially and collects errors from failed validations. | ||
| * | ||
| * This function returns a validation processor that can be called with input data | ||
| * and an optional configuration object. The processor will run each validation step | ||
| * in order, collecting any errors that occur. | ||
| * | ||
| * @template TSteps - A readonly array of validation step configurations that defines | ||
| * the validation pipeline. The type is constrained to ensure type safety | ||
| * across all steps and their results. | ||
| * | ||
| * @param definition - The validation pipeline definition | ||
| * @param definition.steps - An array of validation step configurations. Each step must have: | ||
| * - `name`: A unique string identifier for the step | ||
| * - `validator`: A function that takes input and returns a validation result | ||
| * - `isError`: A function that determines if the validation result represents an error | ||
| * - `alwaysFailFast`: Optional flag to always stop execution on this step's failure | ||
| * | ||
| * @returns A validation processor function that accepts: | ||
| * - `input`: The data to validate (type inferred from the first validation step) | ||
| * - `config`: Optional configuration object with: | ||
| * - `failFast`: If true, stops execution on first error (unless overridden by step config) | ||
| * | ||
| * @example Basic usage with string validation | ||
| * ```typescript | ||
| * const nameValidator = createValidationProcessor({ | ||
| * steps: [ | ||
| * { | ||
| * name: 'required', | ||
| * validator: (input: string) => input.trim().length > 0, | ||
| * isError: ResultInterpreters.booleanMeansSuccess | ||
| * }, | ||
| * { | ||
| * name: 'minLength', | ||
| * validator: (input: string) => input.length >= 2, | ||
| * isError: ResultInterpreters.booleanMeansSuccess | ||
| * }, | ||
| * { | ||
| * name: 'maxLength', | ||
| * validator: (input: string) => input.length <= 50, | ||
| * isError: ResultInterpreters.booleanMeansSuccess | ||
| * } | ||
| * ] | ||
| * }); | ||
| * | ||
| * const result = nameValidator('John'); | ||
| * // result.isValid: boolean | ||
| * // result.errors: { required?: boolean, minLength?: boolean, maxLength?: boolean } | ||
| * ``` | ||
| * | ||
| * @example Complex validation with custom error types | ||
| * ```typescript | ||
| * type ValidationError = { message: string; code: string }; | ||
| * | ||
| * const userValidator = createValidationProcessor({ | ||
| * steps: [ | ||
| * { | ||
| * name: 'email', | ||
| * validator: (user: { email: string }) => | ||
| * /\S+@\S+\.\S+/.test(user.email) | ||
| * ? null | ||
| * : { message: 'Invalid email format', code: 'INVALID_EMAIL' }, | ||
| * isError: (result): result is ValidationError => result !== null | ||
| * }, | ||
| * { | ||
| * name: 'age', | ||
| * validator: (user: { age: number }) => | ||
| * user.age >= 18 | ||
| * ? null | ||
| * : { message: 'Must be 18 or older', code: 'UNDERAGE' }, | ||
| * isError: (result): result is ValidationError => result !== null, | ||
| * alwaysFailFast: true // Stop immediately if age validation fails | ||
| * } | ||
| * ] | ||
| * }); | ||
| * ``` | ||
| * | ||
| * @example Using fail-fast mode | ||
| * ```typescript | ||
| * const result = validator(input, { failFast: true }); | ||
| * // Stops on first error, even if subsequent steps would also fail | ||
| * ``` | ||
| * | ||
| * @since 1.0.0 | ||
| */ | ||
| export function createValidationProcessor< | ||
| const TSteps extends readonly ValidationStepConfig<any, any, string>[], | ||
| >(definition: { steps: TSteps }) { | ||
| // Determine the base input type required by all steps (intersection). | ||
| type BaseInput = ExtractInputType<TSteps>; | ||
|
|
||
| // Helper: widen input type for object literals while keeping regular objects assignable. | ||
| type InputWithExtras = BaseInput extends object | ||
| ? BaseInput | (BaseInput & Record<string, unknown>) | ||
| : BaseInput; | ||
|
|
||
| return function processValidation( | ||
| input: InputWithExtras, | ||
| config: ValidationPipelineConfig = {} | ||
| ): ValidationResult<TSteps> { | ||
| const errors: Partial<ExtractStepResults<TSteps>> = {}; | ||
| let hasErrors = false; | ||
|
|
||
| for (const step of definition.steps) { | ||
| const result = step.validator(input as BaseInput); | ||
| const isError = step.isError(result); | ||
|
|
||
| if (isError) { | ||
| hasErrors = true; | ||
| (errors as any)[step.name] = result; | ||
|
|
||
| // Always fail fast for steps marked as such, or when global failFast is enabled | ||
| if (step.alwaysFailFast || config.failFast) { | ||
| break; | ||
| } | ||
| } | ||
| } | ||
|
|
||
| return { | ||
| isValid: !hasErrors, | ||
| errors, | ||
| }; | ||
| }; | ||
| } | ||
|
|
||
| /** Helper functions for common result interpretations */ | ||
| export const ResultInterpreters = { | ||
| /** For boolean results: true = success, false = error */ | ||
| booleanMeansSuccess: (result: boolean): boolean => !result, | ||
|
|
||
| /** For boolean results: false = success, true = error */ | ||
| booleanMeansFailure: (result: boolean): boolean => result, | ||
|
|
||
| /** For nullable results: null/undefined = success, anything else = error */ | ||
| nullableIsSuccess: <T>(result: T | null | undefined): boolean => result != null, | ||
|
|
||
| /** For array results: empty = success, non-empty = error */ | ||
| errorList: <T>(result: T[]): boolean => result.length > 0, | ||
|
|
||
| /** For custom predicate */ | ||
| custom: <T>(predicate: (result: T) => boolean) => predicate, | ||
|
|
||
| /** Interpreting the result of a validation processor */ | ||
| validationProcessor: (result: { isValid: boolean }) => !result.isValid, | ||
| } as const; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -118,7 +118,7 @@ export class AuthService { | |
| })) | ||
| ); | ||
|
|
||
| const { errors, errorOccured } = await batchProcess( | ||
| const { errors, errorOccurred: errorOccured } = await batchProcess( | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Suggestion for later - should we move to https://www.npmjs.com/package/p-map instead of the custom batchProcess - just thinking about developer maintainability and documentation as that library is essentially the same functionality but with a bit more control and options. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 💯 that looks perfect |
||
| permissionActions, | ||
| ({ resource, action }) => | ||
| this.authzService.addPermissionForUser(apiKeyId, resource, action) | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,44 @@ | ||
| import { Injectable } from '@nestjs/common'; | ||
| import { ConfigService } from '@nestjs/config'; | ||
|
|
||
| import { ConfigFilePersister } from '@unraid/shared/services/config-file.js'; | ||
| import { ValidationError } from 'class-validator'; | ||
|
|
||
| import { validateObject } from '@app/unraid-api/graph/resolvers/validation.utils.js'; | ||
| import { OrganizerV1 } from '@app/unraid-api/organizer/organizer.dto.js'; | ||
| import { validateOrganizerIntegrity } from '@app/unraid-api/organizer/organizer.validation.js'; | ||
|
|
||
| @Injectable() | ||
| export class DockerConfigService extends ConfigFilePersister<OrganizerV1> { | ||
| constructor(configService: ConfigService) { | ||
| super(configService); | ||
| } | ||
|
|
||
| configKey(): string { | ||
| return 'dockerOrganizer'; | ||
| } | ||
|
|
||
| fileName(): string { | ||
| return 'docker.organizer.json'; | ||
| } | ||
|
|
||
| defaultConfig(): OrganizerV1 { | ||
| return { | ||
| version: 1, | ||
| resources: {}, | ||
| views: {}, | ||
| }; | ||
| } | ||
|
|
||
| async validate(config: object): Promise<OrganizerV1> { | ||
| const organizer = await validateObject(OrganizerV1, config); | ||
| const { isValid, errors } = await validateOrganizerIntegrity(organizer); | ||
| if (!isValid) { | ||
| const error = new ValidationError(); | ||
| error.target = organizer; | ||
| error.contexts = errors; | ||
| throw error; | ||
| } | ||
| return organizer; | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, my only question here is should we have utilized a library to provide the type-safe validations, and then add the chaining logic ourselves? Seems like it could be a case of not-built-here but it may be 100% necessary
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Here's what AI said: 🛠️ TL;DR Recommendation
Use Zod or Valibot if you want standard schema-based validation.
Stick with your custom createValidationProcessor if:
You want step-named error reporting
You need typed per-step errors
You want fail-fast customization
You want full flexibility over validator logic
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ik it's kinda crazy that the zod's out there aren't great for validating trees/graphs. the more convincing benefit is the fact that we can unit test validation steps + pipelines individually as opposed to the zod
refinestyle.