Skip to content

Conversation

pujitm
Copy link
Member

@pujitm pujitm commented Apr 10, 2025

Summary by CodeRabbit

  • New Features

    • Introduced a CLI tool to scaffold new Unraid API plugins with templates for configuration, persistence, and GraphQL resolvers.
    • Added plugin management commands for installing, removing, and listing plugins as peer dependencies.
    • Implemented robust configuration state management with validation, persistence, and error handling.
    • Added scheduled and debounced persistence for configuration changes.
    • Provided utilities for file existence checks and CSV string parsing.
    • Enhanced GraphQL schema with new queries and mutations for health checks and demo configuration.
    • Added environment variable support and ensured config directories are created automatically.
  • Improvements

    • Updated configuration and environment handling to support modular, persistent plugin configs.
    • Improved logging and error handling throughout CLI and service layers.
    • Refined dependency management for plugins, including support for bundled dependencies.
    • Added error handling to Docker service initialization to prevent unhandled exceptions.
  • Bug Fixes

    • Improved error handling during Docker service initialization to prevent unhandled exceptions.
  • Chores

    • Added and updated development dependencies and TypeScript configurations for better compatibility and type safety.
  • Refactor

    • Restructured internal modules to support dynamic plugin loading and configuration injection.
    • Removed deprecated plugin schema extension logic and related code.
  • Documentation

    • Updated and added configuration files and templates for easier plugin development and management.

Copy link
Contributor

coderabbitai bot commented Apr 10, 2025

Caution

Review failed

The pull request is closed.

Walkthrough

This update introduces a robust, extensible configuration state management system for Unraid API plugins, including disk persistence, validation, and migration from legacy formats. It adds new environment variables for configuration directories, ensures their existence at runtime, and provides utilities for config injection and persistence. The CLI gains a comprehensive plugin management command set, enabling installation, removal, and listing of plugins as peer dependencies, with support for bundled and unbundled modes. A new plugin generator CLI scaffolds plugin projects with standardized templates. The unraid-api-plugin-connect package is refactored for modular configuration, persistence, and GraphQL integration, and the codebase receives improvements in logging, error handling, and dependency management.

Changes

File(s) / Path(s) Change Summary
api/.env.development, api/.env.production, api/.env.staging, api/.env.test Added PATHS_CONFIG_MODULES environment variable for config directory paths.
api/src/environment.ts Refactored package.json path resolution; added PATHS_CONFIG_MODULES constant.
api/src/index.ts Ensured config modules directory exists at startup.
api/src/unraid-api/config/api-state.model.ts, api-state.register.ts, api-state.service.ts, config.injection.ts, config.interface.ts, config/persistence.helper.ts Introduced a generic, type-safe config state manager with persistence, validation, registry, and injection utilities.
api/src/unraid-api/cli/plugins/dependency.service.ts, plugin.command.ts, plugin.cli.module.ts Added CLI plugin management commands and dependency management service.
api/src/unraid-api/cli/cli.module.ts Registered new plugin command module in CLI.
api/src/unraid-api/cli/restart.command.ts, log.service.ts Refined method signatures and improved logging flexibility.
api/src/unraid-api/graph/graph.module.ts Integrated plugin module registration with GraphQL.
api/src/unraid-api/graph/resolvers/docker/docker.service.ts Added error handling to Docker service initialization.
api/src/unraid-api/plugin/plugin.interface.ts, plugin.module.ts, plugin.service.ts Simplified plugin schema interface; removed GraphQL schema extension logic; made plugin listing public.
api/src/unraid-api/app/app.module.ts Removed unused plugin module import.
api/dev/Unraid.net/myservers.cfg Updated [api] version from "4.4.1" to "4.6.6".
api/dev/configs/connect.json Added new connect configuration JSON file.
api/generated-schema.graphql Changed Docker field argument semantics; added getDemo and setDemo fields.
api/justfile Added sync-env recipe for syncing environment files to remote server.
api/package.json Added @types/lodash-es to development dependencies.
api/vite.config.ts Reformatted test setupFiles array for brevity.
api/src/__test__/core/utils/files/config-file-normalizer.test.ts Removed assignment of showT2Fa in test setup.
packages/unraid-api-plugin-connect/index.ts Removed old plugin module implementation.
packages/unraid-api-plugin-connect/package.json Added runtime and peer dependencies for config and validation libraries.
packages/unraid-api-plugin-connect/src/index.ts, config.demo.ts, config.entity.ts, config.persistence.ts, connect.resolver.ts, helpers/my-servers-config.ts, helpers/utils.ts Reimplemented plugin as a modular, persistent, and validated config provider with GraphQL integration and legacy migration.
packages/unraid-api-plugin-connect/tsconfig.json Updated module system and source file inclusions.
packages/unraid-api-plugin-generator/package.json, tsconfig.json, tsconfig.build.json, src/create-plugin.ts, src/index.ts, src/templates/* Added new CLI tool for scaffolding Unraid API plugins with typed config, persistence, and GraphQL resolver templates.

Changes (Grouped Table)

Area / Files Summary
Environment & Setup
api/.env.*, api/src/index.ts
Added config modules directory env vars and ensured directory existence at runtime.
Config State Management
api/src/unraid-api/config/*
Introduced generic, persistent, and injectable config state management utilities.
CLI Plugin Management
api/src/unraid-api/cli/plugins/*, cli.module.ts, restart.command.ts, log.service.ts
Added CLI commands for plugin install/remove/list, improved logging.
Plugin System & GraphQL
api/src/unraid-api/plugin/*, graph/graph.module.ts, app/app.module.ts
Simplified plugin schema interface, improved plugin lifecycle integration.
Unraid Connect Plugin
packages/unraid-api-plugin-connect/*
Refactored as modular, persistent config provider with GraphQL and migration support.
Plugin Generator Tool
packages/unraid-api-plugin-generator/*
Added CLI tool to scaffold plugin projects with config and resolver templates.
Other
api/dev/Unraid.net/myservers.cfg, api/dev/configs/connect.json, api/generated-schema.graphql, api/justfile, api/package.json, api/vite.config.ts, api/src/__test__/core/utils/files/config-file-normalizer.test.ts
Updated configs, schema, scripts, and tests.

Sequence Diagram(s)

sequenceDiagram
    participant CLI_User
    participant CLI
    participant DependencyService
    participant PluginService
    participant RestartCommand

    CLI_User->>CLI: plugin install <package> [--bundled]
    CLI->>DependencyService: addPeerDependency(package, bundled)
    DependencyService->>DependencyService: Update package.json / run npm install
    CLI->>DependencyService: rebuildVendorArchive() (if not bundled)
    CLI->>RestartCommand: run()
    RestartCommand->>CLI_User: Output restart status

    CLI_User->>CLI: plugin remove <package>
    CLI->>DependencyService: removePeerDependency(package)
    CLI->>RestartCommand: run()
    RestartCommand->>CLI_User: Output restart status

    CLI_User->>CLI: plugin list
    CLI->>PluginService: listPlugins()
    PluginService-->>CLI: List of plugins
    CLI->>CLI_User: Output plugin list
Loading
sequenceDiagram
    participant PluginModule
    participant ApiStateConfig
    participant ConfigPersistenceHelper
    participant Disk

    PluginModule->>ApiStateConfig: load()
    ApiStateConfig->>ConfigPersistenceHelper: parseConfig()
    ConfigPersistenceHelper->>Disk: Read config file
    Disk-->>ConfigPersistenceHelper: Return data or undefined
    ConfigPersistenceHelper-->>ApiStateConfig: Return config or default
    ApiStateConfig->>ConfigPersistenceHelper: persist(config)
    ConfigPersistenceHelper->>Disk: Write config if changed
    Disk-->>ConfigPersistenceHelper: Write result
Loading

Poem

🎉
A config state blooms, persistent and bright,
Plugins line up, managed just right.
CLI commands at your beck and call,
Scaffolding plugins—no hassle at all!
Old configs migrate, new ones persist,
With logs and checks, no change is missed.
Unraid grows stronger, in code we delight!
🚀


📜 Recent review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro (Legacy)

📥 Commits

Reviewing files that changed from the base of the PR and between 7053070 and fb2d03e.

📒 Files selected for processing (1)
  • api/src/unraid-api/config/config.interface.ts (1 hunks)

🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Generate unit testing code for this file.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai generate unit testing code for this file.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and generate unit testing code.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

@pujitm
Copy link
Member Author

pujitm commented Apr 10, 2025

@coderabbitai pause

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (9)
api/src/index.ts (2)

6-6: Fix import order according to linting rules

The static analysis is flagging an ordering issue with the imports.

-import { unlinkSync, mkdirSync } from 'fs';
+import { mkdirSync, unlinkSync } from 'fs';
🧰 Tools
🪛 GitHub Check: Build API

[failure] 6-6:
Replace unlinkSync,·mkdir with mkdirSync,·unlink

🪛 GitHub Check: Test API

[failure] 6-6:
Replace unlinkSync,·mkdir with mkdirSync,·unlink


17-17: Fix import order according to linting rules

The static analysis is flagging an ordering issue with the environment imports.

-import { environment, PORT, CONFIG_MODULES_HOME } from '@app/environment.js';
+import { CONFIG_MODULES_HOME, environment, PORT } from '@app/environment.js';
🧰 Tools
🪛 GitHub Check: Build API

[failure] 17-17:
Replace environment,·PORT,·CONFIG_MODULES_HOME with CONFIG_MODULES_HOME,·environment,·PORT

🪛 GitHub Check: Test API

[failure] 17-17:
Replace environment,·PORT,·CONFIG_MODULES_HOME with CONFIG_MODULES_HOME,·environment,·PORT

api/src/unraid-api/config/config.interface.ts (1)

1-9: Consider the static analysis suggestion about empty interface

While the empty ConfigFeatures interface serves as a container for type extensions, the static analyzer flags it as suspicious. Since it's meant to be extended, keeping it as an interface is justified, but you could add documentation to clarify this for static analyzers.

/**
 * Container record of config names to their types. Used for type completion on registered configs.
 * Config authors should redeclare/merge this interface with their config names as the keys 
 * and implementation models as the types.
+ *
+ * @remarks This is intentionally an empty interface to be extended by other modules.
 */
export interface ConfigFeatures {};
🧰 Tools
🪛 Biome (1.9.4)

[error] 8-8: An empty interface is equivalent to {}.

Safe fix: Use a type alias instead.

(lint/suspicious/noEmptyInterface)

api/src/unraid-api/config/config.registry.ts (1)

14-30: Consider refactoring the static-only class

The static analysis tool flags this class for having only static members. While functionally correct, consider these alternatives:

  1. Use the class name instead of this in static methods
  2. Convert to a namespace or module-level object with exported functions

Option 1: Use class name instead of this:

export class ConfigRegistry {
    /** A map of config names to their implementation models. */
    private static configTypes = new Map<string, string>();

    static register(configName: string, configType: string) {
-        this.configTypes.set(configName, configType);
+        ConfigRegistry.configTypes.set(configName, configType);
    }

    static getConfigType(configName: string) {
-        return this.configTypes.get(configName);
+        return ConfigRegistry.configTypes.get(configName);
    }

    static getConfigToken(configName: string) {
        const configType = ConfigRegistry.getConfigType(configName) ?? '';
        return makeConfigToken(configName, configType);
    }
}

Option 2: Convert to module-level functions:

/** A map of config names to their implementation models. */
const configTypes = new Map<string, string>();

export function registerConfig(configName: string, configType: string) {
    configTypes.set(configName, configType);
}

export function getConfigType(configName: string) {
    return configTypes.get(configName);
}

export function getConfigToken(configName: string) {
    const configType = getConfigType(configName) ?? '';
    return makeConfigToken(configName, configType);
}
🧰 Tools
🪛 Biome (1.9.4)

[error] 14-30: Avoid classes that contain only static members.

Prefer using simple functions instead of classes with only static members.

(lint/complexity/noStaticOnlyClass)


[error] 19-19: Using this in a static context can be confusing.

this refers to the class.
Unsafe fix: Use the class name instead.

(lint/complexity/noThisInStatic)


[error] 23-23: Using this in a static context can be confusing.

this refers to the class.
Unsafe fix: Use the class name instead.

(lint/complexity/noThisInStatic)

api/src/unraid-api/config/api-state.service.ts (2)

4-5: Consider using absolute import paths.

Static analysis flags these relative imports. If your style guide requires absolute paths, consider updating these imports accordingly.

- import type { ApiStateConfig } from './api-state.model.js';
- import { makeConfigToken } from './config.registry.js';
+ import type { ApiStateConfig } from '@app/unraid-api/config/api-state.model.js';
+ import { makeConfigToken } from '@app/unraid-api/config/config.registry.js';
🧰 Tools
🪛 GitHub Check: Build API

[failure] 4-4:
import statements should have an absolute path

🪛 GitHub Check: Test API

[failure] 4-4:
import statements should have an absolute path


57-70: Consider handling multiple invocations of setup().

If this method is called more than once, multiple intervals could be registered for the same token, leading to unexpected behavior. An optional improvement is to guard against re-initializing.

setup() {
    if (this.schedulerRegistry.doesExists('interval', this.token)) {
+       this.logger.warn(`Persistence already set up for token ${this.token}. Skipping re-initialization.`);
        return;
    }
    ...
}
api/src/unraid-api/config/api-state.register.ts (2)

4-7: Consider using absolute import paths.

Your build pipeline reports warnings for relative paths. Converting them to absolute paths may align with your import style guidelines.

- import type { ApiStateConfigOptions } from './api-state.model.js';
- import type { ApiStateConfigPersistenceOptions } from './api-state.service.js';
- import { ApiStateConfig } from './api-state.model.js';
- import { ScheduledConfigPersistence } from './api-state.service.js';
+ import type { ApiStateConfigOptions } from '@app/unraid-api/config/api-state.model.js';
+ import type { ApiStateConfigPersistenceOptions } from '@app/unraid-api/config/api-state.service.js';
+ import { ApiStateConfig } from '@app/unraid-api/config/api-state.model.js';
+ import { ScheduledConfigPersistence } from '@app/unraid-api/config/api-state.service.js';
🧰 Tools
🪛 GitHub Check: Build API

[failure] 4-4:
import statements should have an absolute path


[failure] 5-5:
import statements should have an absolute path


[failure] 6-6:
import statements should have an absolute path


[failure] 7-7:
import statements should have an absolute path

🪛 GitHub Check: Test API

[failure] 4-4:
import statements should have an absolute path


[failure] 5-5:
import statements should have an absolute path


[failure] 6-6:
import statements should have an absolute path


[failure] 7-7:
import statements should have an absolute path


13-45: Consider refactoring to a plain function rather than a static-only class.

A lint rule warns about classes containing only static methods. If instance state is unnecessary, a simpler function can suffice. Alternatively, keep the class if you anticipate adding instance functionality later.

-export class ApiStateConfigModule {
-    private static readonly logger = new Logger(ApiStateConfigModule.name);
-
-    static async register<ConfigType>(
-        options: ApiStateRegisterOptions<ConfigType>
-    ): Promise<DynamicModule> {
-        ...
-        return {
-            module: ApiStateConfigModule,
-            providers: [ConfigProvider, PersistenceProvider],
-            exports: [ConfigProvider],
-        };
-    }
-}
+
+export async function registerApiStateConfigModule<ConfigType>(
+  options: ApiStateRegisterOptions<ConfigType>
+): Promise<DynamicModule> {
+  const logger = new Logger('ApiStateConfigModule');
+  ...
+  return {
+    module: class {},
+    providers: [ConfigProvider, PersistenceProvider],
+    exports: [ConfigProvider],
+  };
+}
🧰 Tools
🪛 Biome (1.9.4)

[error] 13-45: Avoid classes that contain only static members.

Prefer using simple functions instead of classes with only static members.

(lint/complexity/noStaticOnlyClass)

api/src/unraid-api/config/api-state.model.ts (1)

10-10: Consider using absolute import paths.

Static analysis warns about these imports. Converting them to absolute references could satisfy your project’s build checks.

- import { ConfigRegistry } from './config.registry.js';
+ import { ConfigRegistry } from '@app/unraid-api/config/config.registry.js';
🧰 Tools
🪛 GitHub Check: Build API

[failure] 10-10:
import statements should have an absolute path

🪛 GitHub Check: Test API

[failure] 10-10:
import statements should have an absolute path

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 8bb9efc and 69e1b71.

📒 Files selected for processing (12)
  • api/.env.development (1 hunks)
  • api/.env.production (1 hunks)
  • api/.env.staging (1 hunks)
  • api/.env.test (1 hunks)
  • api/src/environment.ts (1 hunks)
  • api/src/index.ts (3 hunks)
  • api/src/unraid-api/config/api-state.model.ts (1 hunks)
  • api/src/unraid-api/config/api-state.register.ts (1 hunks)
  • api/src/unraid-api/config/api-state.service.ts (1 hunks)
  • api/src/unraid-api/config/config.injection.ts (1 hunks)
  • api/src/unraid-api/config/config.interface.ts (1 hunks)
  • api/src/unraid-api/config/config.registry.ts (1 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (6)
api/src/unraid-api/config/config.injection.ts (2)
api/src/unraid-api/config/config.interface.ts (1)
  • ConfigFeatures (8-8)
api/src/unraid-api/config/config.registry.ts (1)
  • ConfigRegistry (14-30)
api/src/index.ts (1)
api/src/environment.ts (1)
  • CONFIG_MODULES_HOME (89-89)
api/src/unraid-api/config/api-state.register.ts (2)
api/src/unraid-api/config/api-state.model.ts (2)
  • ApiStateConfigOptions (12-17)
  • ApiStateConfig (19-103)
api/src/unraid-api/config/api-state.service.ts (2)
  • ApiStateConfigPersistenceOptions (7-16)
  • ScheduledConfigPersistence (18-74)
api/src/unraid-api/config/api-state.service.ts (2)
api/src/unraid-api/config/api-state.model.ts (1)
  • ApiStateConfig (19-103)
api/src/unraid-api/config/config.registry.ts (1)
  • makeConfigToken (10-12)
api/src/unraid-api/config/api-state.model.ts (2)
api/src/unraid-api/config/config.registry.ts (1)
  • ConfigRegistry (14-30)
api/src/environment.ts (1)
  • CONFIG_MODULES_HOME (89-89)
api/src/unraid-api/config/config.registry.ts (1)
api/src/unraid-api/config/api-state.service.ts (1)
  • configName (34-36)
🪛 GitHub Check: Build API
api/src/environment.ts

[failure] 89-89:
Insert


[failure] 89-89:
Newline required at end of file but not found

api/src/index.ts

[failure] 6-6:
Replace unlinkSync,·mkdir with mkdirSync,·unlink


[failure] 17-17:
Replace environment,·PORT,·CONFIG_MODULES_HOME with CONFIG_MODULES_HOME,·environment,·PORT

api/src/unraid-api/config/api-state.register.ts

[failure] 4-4:
import statements should have an absolute path


[failure] 5-5:
import statements should have an absolute path


[failure] 6-6:
import statements should have an absolute path


[failure] 7-7:
import statements should have an absolute path

api/src/unraid-api/config/api-state.service.ts

[failure] 4-4:
import statements should have an absolute path

api/src/unraid-api/config/api-state.model.ts

[failure] 10-10:
import statements should have an absolute path

🪛 GitHub Check: Test API
api/src/environment.ts

[failure] 89-89:
Insert


[failure] 89-89:
Newline required at end of file but not found

api/src/index.ts

[failure] 6-6:
Replace unlinkSync,·mkdir with mkdirSync,·unlink


[failure] 17-17:
Replace environment,·PORT,·CONFIG_MODULES_HOME with CONFIG_MODULES_HOME,·environment,·PORT

api/src/unraid-api/config/api-state.register.ts

[failure] 4-4:
import statements should have an absolute path


[failure] 5-5:
import statements should have an absolute path


[failure] 6-6:
import statements should have an absolute path


[failure] 7-7:
import statements should have an absolute path

api/src/unraid-api/config/api-state.service.ts

[failure] 4-4:
import statements should have an absolute path

api/src/unraid-api/config/api-state.model.ts

[failure] 10-10:
import statements should have an absolute path

🪛 Biome (1.9.4)
api/src/unraid-api/config/config.interface.ts

[error] 8-8: An empty interface is equivalent to {}.

Safe fix: Use a type alias instead.

(lint/suspicious/noEmptyInterface)

api/src/unraid-api/config/api-state.register.ts

[error] 13-45: Avoid classes that contain only static members.

Prefer using simple functions instead of classes with only static members.

(lint/complexity/noStaticOnlyClass)

api/src/unraid-api/config/config.registry.ts

[error] 14-30: Avoid classes that contain only static members.

Prefer using simple functions instead of classes with only static members.

(lint/complexity/noStaticOnlyClass)


[error] 19-19: Using this in a static context can be confusing.

this refers to the class.
Unsafe fix: Use the class name instead.

(lint/complexity/noThisInStatic)


[error] 23-23: Using this in a static context can be confusing.

this refers to the class.
Unsafe fix: Use the class name instead.

(lint/complexity/noThisInStatic)

⏰ Context from checks skipped due to timeout of 90000ms (2)
  • GitHub Check: Build Web App
  • GitHub Check: Cloudflare Pages
🔇 Additional comments (9)
api/.env.test (1)

12-12: Looks good - new config variable added consistently.

The CONFIG_MODULES_HOME variable has been added with a development-appropriate relative path that matches the pattern of other configuration paths in this file.

api/.env.development (1)

12-12: Looks good - new config variable added consistently.

The CONFIG_MODULES_HOME variable has been added with a development-appropriate relative path that matches the pattern of other configuration paths in this file.

api/.env.production (1)

5-5: Looks good - new config variable added with production path.

The CONFIG_MODULES_HOME variable has been added with an appropriate absolute path for the production environment.

api/.env.staging (1)

5-5: Looks good - new config variable added with staging path.

The CONFIG_MODULES_HOME variable has been added with an appropriate absolute path for the staging environment, matching the production path for environment parity.

api/src/unraid-api/config/config.injection.ts (1)

1-13: Well-designed custom decorator for configuration injection

The InjectConfig decorator provides a clean abstraction over NestJS's dependency injection system for configuration settings. Good use of generics to ensure type safety by constraining the feature parameter to keys of the ConfigFeatures interface.

api/src/index.ts (1)

47-48: Good practice ensuring the config directory exists

Creating the config modules directory at startup with recursive option is a good practice to prevent file operation errors later in the application lifecycle.

api/src/unraid-api/config/config.interface.ts (1)

11-20: Good interface design for configuration metadata

The ConfigMetadata interface provides a clear structure for configuration metadata with well-documented properties. The use of Zod for schema validation is a good practice for runtime type checking.

api/src/unraid-api/config/config.registry.ts (1)

10-12: Clean implementation of token creation

The makeConfigToken function is a simple and effective way to generate unique tokens for configurations.

api/src/unraid-api/config/api-state.model.ts (1)

19-29: Nice design for managing default config immutability and logging.

Registering the config before assigning a new logger ensures the token is always recognized, and the structuredClone usage effectively prevents accidental mutations to options.defaultConfig.

Copy link
Contributor

coderabbitai bot commented Apr 10, 2025

✅ Actions performed

Reviews paused.

@pujitm pujitm force-pushed the feat/config-module branch from 7c6525e to 86cd4b4 Compare April 14, 2025 13:39
@pujitm pujitm force-pushed the feat/config-module branch 2 times, most recently from 5267984 to c9d8377 Compare April 17, 2025 22:40
@pujitm pujitm force-pushed the feat/config-module branch from c9d8377 to db4f475 Compare April 17, 2025 22:44
@pujitm pujitm force-pushed the feat/config-module branch from 2b37bc4 to b64d4ea Compare April 18, 2025 14:20
@pujitm pujitm force-pushed the feat/config-module branch from b0fdd31 to aab0a48 Compare April 22, 2025 13:32
@pujitm pujitm requested a review from elibosley April 22, 2025 13:33
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Inline review comments failed to post. This is likely due to GitHub's limits when posting large numbers of comments. If you are seeing this consistently it is likely a permissions issue. Please check "Moderation" -> "Code review limits" under your organization settings.

Actionable comments posted: 3

♻️ Duplicate comments (2)
packages/unraid-api-plugin-connect/src/config.persistence.ts (1)

24-31: 🛠️ Refactor suggestion

Error handling needed for required environment variable

The configPath getter uses a non-null assertion (!) on PATHS_CONFIG_MODULES, but doesn't handle the case where it might be undefined.

get configPath() {
+  const configModulesPath = this.configService.get("PATHS_CONFIG_MODULES");
+  if (!configModulesPath) {
+    throw new Error("PATHS_CONFIG_MODULES environment variable is not defined");
+  }
  return path.join(
-    this.configService.get("PATHS_CONFIG_MODULES")!,
+    configModulesPath,
    "connect.json"
  );
}
api/src/unraid-api/config/api-state.model.ts (1)

100-104: 🛠️ Refactor suggestion

Consider persisting after config update

The update method modifies the config but doesn't persist it, requiring callers to remember to call persist() afterward. This could lead to lost changes.

-update(config: Partial<T>) {
+async update(config: Partial<T>) {
  const proposedConfig = this.options.parse({ ...this.config, ...config });
  this.config = proposedConfig;
-  return this;
+  await this.persist();
+  return this;
}
🧹 Nitpick comments (3)
packages/unraid-api-plugin-connect/src/config.persistence.ts (2)

93-95: Enhance validation with class-validator

The validate method currently only transforms the object using plainToInstance but doesn't perform actual validation with class-validator decorators.

private validate(config: object) {
-  return plainToInstance(MyServersConfig, config);
+  const configInstance = plainToInstance(MyServersConfig, config);
+  // Import and use validateSync from class-validator
+  // const errors = validateSync(configInstance);
+  // if (errors.length > 0) {
+  //   throw new ValidationError(`Config validation failed: ${errors.toString()}`);
+  // }
+  return configInstance;
}

64-86: Add comparison check to avoid unnecessary config updates

The persist method correctly checks if the config has changed before writing, but this comparison is vulnerable to errors or exceptions and doesn't provide a clean fallback.

async persist(config = this.configService.get<MyServersConfig>("connect")) {
  try {
+    // Extract this logic to a helper method for better readability
+    const isDifferent = await this.isConfigDifferent(config);
+    if (!isDifferent) {
+      this.logger.verbose(`Config is unchanged, skipping persistence`);
+      return false;
+    }
-    if (isEqual(config, await this.loadConfig())) {
-      this.logger.verbose(`Config is unchanged, skipping persistence`);
-      return false;
-    }
  } catch (error) {
    this.logger.error(`Error loading config (will overwrite file):`, error);
  }
  const data = JSON.stringify(config, null, 2);
  this.logger.verbose(`Persisting config to ${this.configPath}: ${data}`);
  try {
    await writeFile(this.configPath, data);
    this.logger.verbose(`Config persisted to ${this.configPath}`);
    return true;
  } catch (error) {
    this.logger.error(
      `Error persisting config to '${this.configPath}':`,
      error
    );
    return false;
  }
}

+// New helper method
+private async isConfigDifferent(config: MyServersConfig): Promise<boolean> {
+  try {
+    const diskConfig = await this.loadConfig();
+    return !isEqual(config, diskConfig);
+  } catch (error) {
+    // If we can't load the config, assume it's different
+    return true;
+  }
+}
api/src/unraid-api/config/api-state.model.ts (1)

71-77: Consider handling JSON parse errors explicitly

The parseConfig method doesn't explicitly handle JSON parsing errors, which could lead to confusing error messages.

async parseConfig(opts: { filePath?: string } = {}): Promise<T | undefined> {
  const { filePath = this.filePath } = opts;
  if (!(await fileExists(filePath))) return undefined;

-  const rawConfig = JSON.parse(await readFile(filePath, 'utf8'));
-  return this.options.parse(rawConfig);
+  try {
+    const fileContent = await readFile(filePath, 'utf8');
+    const rawConfig = JSON.parse(fileContent);
+    return this.options.parse(rawConfig);
+  } catch (error) {
+    if (error instanceof SyntaxError) {
+      throw new Error(`Config file at ${filePath} contains invalid JSON: ${error.message}`);
+    }
+    throw error;
+  }
}
🛑 Comments failed to post (3)
api/src/index.ts (1)

18-18: ⚠️ Potential issue

Reorder imports to match project style conventions

The order of PATHS_CONFIG_MODULES, environment in the imports should be reversed to match the project's style conventions, as flagged by static analysis tools.

-import { PATHS_CONFIG_MODULES, environment, PORT } from '@app/environment.js';
+import { environment, PATHS_CONFIG_MODULES, PORT } from '@app/environment.js';
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

import { environment, PATHS_CONFIG_MODULES, PORT } from '@app/environment.js';
🧰 Tools
🪛 GitHub Check: Build API

[failure] 18-18:
Replace PATHS_CONFIG_MODULES,·environment with environment,·PATHS_CONFIG_MODULES

🪛 GitHub Check: Test API

[failure] 18-18:
Replace PATHS_CONFIG_MODULES,·environment with environment,·PATHS_CONFIG_MODULES

packages/unraid-api-plugin-connect/src/config.persistence.ts (1)

131-134: 🛠️ Refactor suggestion

Use async file operations consistently

The loadConfig method uses synchronous file operations (existsSync, readFileSync) while other methods use asynchronous ones.

private async loadConfig(configFilePath = this.configPath) {
-  if (!existsSync(configFilePath)) throw new Error(`Config file does not exist at '${configFilePath}'`);
-  return this.validate(JSON.parse(readFileSync(configFilePath, "utf8")));
+  const exists = await fileExists(configFilePath);
+  if (!exists) throw new Error(`Config file does not exist at '${configFilePath}'`);
+  const content = await readFile(configFilePath, "utf8");
+  return this.validate(JSON.parse(content));
}

Committable suggestion skipped: line range outside the PR's diff.

api/src/unraid-api/config/api-state.model.ts (1)

45-47: 🛠️ Refactor suggestion

Add error handling for PATHS_CONFIG_MODULES environment variable

The code assumes that PATHS_CONFIG_MODULES is defined, but should handle the case when it's not.

get filePath() {
+  if (!PATHS_CONFIG_MODULES) {
+    throw new Error('PATHS_CONFIG_MODULES environment variable is not defined');
+  }
  return join(PATHS_CONFIG_MODULES, this.fileName);
}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

    get filePath() {
        if (!PATHS_CONFIG_MODULES) {
            throw new Error('PATHS_CONFIG_MODULES environment variable is not defined');
        }
        return join(PATHS_CONFIG_MODULES, this.fileName);
    }

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

♻️ Duplicate comments (2)
packages/unraid-api-plugin-connect/src/config.persistence.ts (2)

24-31: 🛠️ Refactor suggestion

Consider adding error handling for CONFIG_MODULES_HOME path

The configPath getter doesn't include runtime validation for when the environment variable might be undefined. This could lead to unexpected errors during execution.

get configPath() {
  // PATHS_CONFIG_MODULES is a required environment variable.
  // It is the directory where custom config files are stored.
+  const configModulesPath = this.configService.get("PATHS_CONFIG_MODULES");
+  if (!configModulesPath) {
+    throw new Error("PATHS_CONFIG_MODULES is not defined");
+  }
  return path.join(
-    this.configService.get("PATHS_CONFIG_MODULES")!,
+    configModulesPath,
    "connect.json"
  );
}

131-134: 🛠️ Refactor suggestion

Add error handling for JSON parsing

The loadConfig method doesn't handle JSON parsing errors, which could occur if the file content isn't valid JSON.

private async loadConfig(configFilePath = this.configPath) {
  if (!existsSync(configFilePath)) throw new Error(`Config file does not exist at '${configFilePath}'`);
-  return this.validate(JSON.parse(readFileSync(configFilePath, "utf8")));
+  try {
+    const fileContent = readFileSync(configFilePath, "utf8");
+    const parsedConfig = JSON.parse(fileContent);
+    return this.validate(parsedConfig);
+  } catch (error) {
+    throw new Error(`Failed to parse config file at '${configFilePath}': ${error.message}`);
+  }
}
🧹 Nitpick comments (1)
packages/unraid-api-plugin-connect/src/config.persistence.ts (1)

64-69: Improve error handling for config comparison

The method attempts to compare with a file that might not exist yet, relying on the catch block to handle that scenario. This approach works but could be made more explicit.

async persist(config = this.configService.get<MyServersConfig>("connect")) {
  try {
-    if (isEqual(config, await this.loadConfig())) {
-      this.logger.verbose(`Config is unchanged, skipping persistence`);
-      return false;
-    }
+    if (existsSync(this.configPath)) {
+      const diskConfig = await this.loadConfig();
+      if (isEqual(config, diskConfig)) {
+        this.logger.verbose(`Config is unchanged, skipping persistence`);
+        return false;
+      }
+    } else {
+      this.logger.verbose(`Config file doesn't exist yet, will create it`);
+    }
  } catch (error) {
    this.logger.error(`Error loading config (will overwrite file):`, error);
  }
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro (Legacy)

📥 Commits

Reviewing files that changed from the base of the PR and between b0fdd31 and aab0a48.

📒 Files selected for processing (9)
  • api/.env.development (2 hunks)
  • api/.env.production (1 hunks)
  • api/.env.staging (1 hunks)
  • api/.env.test (1 hunks)
  • api/src/environment.ts (2 hunks)
  • api/src/index.ts (3 hunks)
  • api/src/unraid-api/config/api-state.model.ts (1 hunks)
  • packages/unraid-api-plugin-connect/src/config.persistence.ts (1 hunks)
  • packages/unraid-api-plugin-generator/src/templates/config.persistence.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (8)
  • api/.env.production
  • api/.env.test
  • api/.env.development
  • api/.env.staging
  • api/src/environment.ts
  • api/src/index.ts
  • packages/unraid-api-plugin-generator/src/templates/config.persistence.ts
  • api/src/unraid-api/config/api-state.model.ts
⏰ Context from checks skipped due to timeout of 90000ms (2)
  • GitHub Check: Build and Deploy Plugin / Build and Deploy Plugin
  • GitHub Check: Cloudflare Pages

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

♻️ Duplicate comments (2)
packages/unraid-api-plugin-connect/src/config.persistence.ts (2)

25-32: 🛠️ Refactor suggestion

Add error handling for missing CONFIG_MODULES_HOME

The configPath getter uses a non-null assertion (!) which could lead to cryptic errors if the environment variable is undefined.

  get configPath() {
    // PATHS_CONFIG_MODULES is a required environment variable.
    // It is the directory where custom config files are stored.
+   const configModulesPath = this.configService.get("PATHS_CONFIG_MODULES");
+   if (!configModulesPath) {
+     throw new Error("PATHS_CONFIG_MODULES environment variable is not defined");
+   }
    return path.join(
-     this.configService.get("PATHS_CONFIG_MODULES")!,
+     configModulesPath,
      "connect.json"
    );
  }

139-142: 🛠️ Refactor suggestion

Add try-catch for JSON parsing

The loadConfig method doesn't handle JSON parsing errors, which could occur with malformed config files.

  private async loadConfig(configFilePath = this.configPath) {
    if (!existsSync(configFilePath)) throw new Error(`Config file does not exist at '${configFilePath}'`);
-   return this.validate(JSON.parse(readFileSync(configFilePath, "utf8")));
+   try {
+     const fileContent = readFileSync(configFilePath, "utf8");
+     const configData = JSON.parse(fileContent);
+     return this.validate(configData);
+   } catch (error) {
+     throw new Error(`Failed to parse config file at '${configFilePath}': ${error.message}`);
+   }
  }
🧹 Nitpick comments (3)
packages/unraid-api-plugin-connect/src/config.persistence.ts (3)

94-103: Use validateSync for synchronous validation

The validate method is marked as async but validateOrReject returns a Promise that might not need to be awaited in all contexts.

Consider using validateSync for synchronous contexts and keeping validateOrReject for async contexts:

- private async validate(config: object) {
+ private validate(config: object) {
    let instance: MyServersConfig;
    if (config instanceof MyServersConfig) {
      instance = config;
    } else {
      instance = plainToInstance(MyServersConfig, config, { enableImplicitConversion: true });
    }
-   await validateOrReject(instance);
+   validateSync(instance);
    return instance;
  }

+ private async validateAsync(config: object) {
+   const instance = this.validate(config);
+   await validateOrReject(instance);
+   return instance;
+ }

152-157: Add comment explaining the demo timestamp purpose

The demo property added in migrateLegacyConfig doesn't have documentation explaining its purpose.

Based on the prior feedback, it's best to document that this is for demonstration purposes only:

  private async migrateLegacyConfig() {
    const legacyConfig = await this.parseLegacyConfig();
    this.configService.set("connect", {
+     // This timestamp is for demonstration purposes only
+     // It keeps demo environments looking fresh with current dates
      demo: new Date().toISOString(),
      ...legacyConfig,
    });
  }

65-87: Extract disk config loading into helper method

The persist method has logic that could be extracted to improve readability and reusability.

  async persist(config = this.configService.get<MyServersConfig>("connect")) {
    try {
-     if (isEqual(config, await this.loadConfig())) {
+     if (await this.isConfigUnchanged(config)) {
        this.logger.verbose(`Config is unchanged, skipping persistence`);
        return false;
      }
    } catch (error) {
      this.logger.error(`Error loading config (will overwrite file):`, error);
    }
    
    const data = JSON.stringify(config, null, 2);
    this.logger.verbose(`Persisting config to ${this.configPath}: ${data}`);
    try {
      await writeFile(this.configPath, data);
      this.logger.verbose(`Config persisted to ${this.configPath}`);
      return true;
    } catch (error) {
      this.logger.error(
        `Error persisting config to '${this.configPath}':`,
        error
      );
      return false;
    }
  }
+
+ /**
+  * Checks if the provided config is equal to the one on disk
+  * @param config The config to compare with the on-disk version
+  * @returns True if configs are equal, false otherwise
+  */
+ private async isConfigUnchanged(config: MyServersConfig): Promise<boolean> {
+   try {
+     const diskConfig = await this.loadConfig();
+     return isEqual(config, diskConfig);
+   } catch (error) {
+     this.logger.debug(`Failed to compare configs: ${error.message}`);
+     return false; 
+   }
+ }
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro (Legacy)

📥 Commits

Reviewing files that changed from the base of the PR and between aab0a48 and 7b08589.

📒 Files selected for processing (1)
  • packages/unraid-api-plugin-connect/src/config.persistence.ts (1 hunks)
🧰 Additional context used
🧠 Learnings (1)
packages/unraid-api-plugin-connect/src/config.persistence.ts (1)
Learnt from: pujitm
PR: unraid/api#1352
File: packages/unraid-api-plugin-connect/src/config.persistence.ts:143-149
Timestamp: 2025-04-22T14:41:19.582Z
Learning: The `demo` property in the config.persistence.ts file's migrateLegacyConfig method is used for demonstration purposes, adding a current timestamp to keep demo environments looking fresh.
⏰ Context from checks skipped due to timeout of 90000ms (3)
  • GitHub Check: Build API
  • GitHub Check: Test API
  • GitHub Check: Cloudflare Pages

Copy link
Member

@elibosley elibosley left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One question.

@pujitm pujitm merged commit 026b0b3 into main Apr 22, 2025
5 of 7 checks passed
@pujitm pujitm deleted the feat/config-module branch April 22, 2025 17:30
Copy link
Contributor

This plugin has been deployed to Cloudflare R2 and is available for testing.
Download it at this URL:

https://preview.dl.unraid.net/unraid-api/tag/PR1352/dynamix.unraid.net.plg

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants