Repository: Hexastack/Hexabot
Pull request #883: feat: add extension module
Author: login: yassinedorbozgithub
Created: 2025-04-04T07:19:39Z | Merged: 2025-04-07T07:52:43Z

# Motivation
The main motivation is to delete automatically the unused settings (helper, channel)
 
Fixes #637

# Checklist:
- [x] I have performed a self-review of my own code
- [x] I have added unit tests that prove my fix is effective or that my feature works
- [x] New and existing unit tests pass locally with my changes


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

- **New Features**
  - Introduced an extension management component to enhance system performance by automatically cleaning up outdated settings during startup.
  - Improved extension configuration with refined type definitions and dynamic configuration entries.

- **Tests**
  - Added comprehensive tests to validate the new cleanup functionality, ensuring accurate removal of obsolete settings.

- **Chores**
  - Updated test fixtures with new dynamic settings entries for improved testing coverage.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

Linked issue #637: Blank settings when a helper extension gets removed
When you install an extension (helper, channel), and then remove it, the settings page shows a blank type. 

![Image](https://github.com/user-attachments/assets/6e16fef0-c96e-430a-8403-78259755f0f0)

The settings must be returned only when the extension is present.


AC: 
- The solution/implementation needs to be argumented (Pros / Cons Table)
- Leverage OO inheritance
- Unit tests
- Update Documentation
Linked issue comments: user: login: marrouchi
created_at: 2025-01-29T05:55:47Z
body: @yassine-sallemi assigned it to you.
author_association: CONTRIBUTOR

Diff:
diff --git a/api/src/app.module.ts b/api/src/app.module.ts
index 86c35c441..72465eb3c 100644
--- a/api/src/app.module.ts
+++ b/api/src/app.module.ts
@@ -35,6 +35,7 @@ import { ChannelModule } from './channel/channel.module';
 import { ChatModule } from './chat/chat.module';
 import { CmsModule } from './cms/cms.module';
 import { config } from './config';
+import { ExtensionModule } from './extension/extension.module';
 import extraModules from './extra';
 import { HelperModule } from './helper/helper.module';
 import { I18nModule } from './i18n/i18n.module';
@@ -152,6 +153,7 @@ const i18nOptions: I18nOptions = {
           max: config.cache.max,
         }),
     MigrationModule,
+    ExtensionModule,
     ...extraModules,
   ],
   controllers: [AppController],
diff --git a/api/src/extension/cleanup.service.spec.ts b/api/src/extension/cleanup.service.spec.ts
new file mode 100644
index 000000000..f8e97ffbf
--- /dev/null
+++ b/api/src/extension/cleanup.service.spec.ts
@@ -0,0 +1,109 @@
+/*
+ * Copyright © 2025 Hexastack. All rights reserved.
+ *
+ * Licensed under the GNU Affero General Public License v3.0 (AGPLv3) with the following additional terms:
+ * 1. The name "Hexabot" is a trademark of Hexastack. You may not use this name in derivative works without express written permission.
+ * 2. All derivative works must include clear attribution to the original creator and software, Hexastack and Hexabot, in a prominent location (e.g., in the software's "About" section, documentation, and README file).
+ */
+
+import { CACHE_MANAGER } from '@nestjs/cache-manager';
+import { MongooseModule } from '@nestjs/mongoose';
+
+import { AttachmentRepository } from '@/attachment/repositories/attachment.repository';
+import { AttachmentModel } from '@/attachment/schemas/attachment.schema';
+import { AttachmentService } from '@/attachment/services/attachment.service';
+import { ChannelService } from '@/channel/channel.service';
+import { SubscriberRepository } from '@/chat/repositories/subscriber.repository';
+import { SubscriberModel } from '@/chat/schemas/subscriber.schema';
+import { SubscriberService } from '@/chat/services/subscriber.service';
+import LocalStorageHelper from '@/extensions/helpers/local-storage/index.helper';
+import { HelperService } from '@/helper/helper.service';
+import { LoggerService } from '@/logger/logger.service';
+import { SettingRepository } from '@/setting/repositories/setting.repository';
+import { Setting, SettingModel } from '@/setting/schemas/setting.schema';
+import { SettingSeeder } from '@/setting/seeds/setting.seed';
+import { SettingService } from '@/setting/services/setting.service';
+import { installSettingFixtures } from '@/utils/test/fixtures/setting';
+import {
+  closeInMongodConnection,
+  rootMongooseTestModule,
+} from '@/utils/test/test';
+import { buildTestingMocks } from '@/utils/test/utils';
+
+import { CleanupService } from './cleanup.service';
+import { TNamespace } from './types';
+
+describe('CleanupService', () => {
+  let initialSettings: Setting[];
+  let helperService: HelperService;
+  let cleanupService: CleanupService;
+  let settingService: SettingService;
+
+  beforeAll(async () => {
+    const { getMocks, resolveMocks } = await buildTestingMocks({
+      imports: [
+        rootMongooseTestModule(installSettingFixtures),
+        MongooseModule.forFeature([
+          SettingModel,
+          SubscriberModel,
+          AttachmentModel,
+        ]),
+      ],
+      providers: [
+        CleanupService,
+        HelperService,
+        SettingService,
+        SettingRepository,
+        {
+          provide: CACHE_MANAGER,
+          useValue: {
+            del: jest.fn(),
+            get: jest.fn(),
+            set: jest.fn(),
+          },
+        },
+        SettingSeeder,
+        SubscriberService,
+        SubscriberRepository,
+        AttachmentService,
+        AttachmentRepository,
+        ChannelService,
+      ],
+    });
+    [cleanupService, settingService, helperService] = await getMocks([
+      CleanupService,
+      SettingService,
+      HelperService,
+    ]);
+
+    const [loggerService] = await resolveMocks([LoggerService]);
+    initialSettings = await settingService.findAll();
+
+    helperService.register(
+      new LocalStorageHelper(settingService, helperService, loggerService),
+    );
+  });
+
+  afterAll(closeInMongodConnection);
+
+  afterEach(jest.clearAllMocks);
+
+  describe('delete', () => {
+    it('should delete all the unregistered settings with a group suffix `_channel` or/and `_helper`', async () => {
+      const registeredNamespaces = [
+        ...cleanupService.getChannelNamespaces(),
+        ...cleanupService.getHelperNamespaces(),
+      ];
+
+      await cleanupService.pruneExtensionSettings();
+      const cleanSettings = await settingService.findAll();
+      const filteredSettings = initialSettings.filter(
+        ({ group }) =>
+          !/_(channel|helper)$/.test(group) !==
+          registeredNamespaces.includes(group as TNamespace),
+      );
+
+      expect(cleanSettings).toEqualPayload(filteredSettings);
+    });
+  });
+});
diff --git a/api/src/extension/cleanup.service.ts b/api/src/extension/cleanup.service.ts
new file mode 100644
index 000000000..62335ab05
--- /dev/null
+++ b/api/src/extension/cleanup.service.ts
@@ -0,0 +1,86 @@
+/*
+ * Copyright © 2025 Hexastack. All rights reserved.
+ *
+ * Licensed under the GNU Affero General Public License v3.0 (AGPLv3) with the following additional terms:
+ * 1. The name "Hexabot" is a trademark of Hexastack. You may not use this name in derivative works without express written permission.
+ * 2. All derivative works must include clear attribution to the original creator and software, Hexastack and Hexabot, in a prominent location (e.g., in the software's "About" section, documentation, and README file).
+ */
+
+import { Injectable } from '@nestjs/common';
+
+import { ChannelService } from '@/channel/channel.service';
+import { HelperService } from '@/helper/helper.service';
+import { LoggerService } from '@/logger/logger.service';
+import { SettingService } from '@/setting/services/setting.service';
+import { DeleteResult } from '@/utils/generics/base-repository';
+
+import { TCriteria, TExtractExtension, TExtractNamespace } from './types';
+
+@Injectable()
+export class CleanupService {
+  constructor(
+    private readonly helperService: HelperService,
+    private readonly loggerService: LoggerService,
+    private readonly settingService: SettingService,
+    private readonly channelService: ChannelService,
+  ) {}
+
+  /**
+   * Deletes unused settings with the specified criteria.
+   *
+   * @param criteria - An array of criteria objects containing:
+   *                   - suffix: Regex pattern to match setting groups
+   *                   - namespaces: Array of namespaces to exclude from deletion
+   * @returns A promise that resolves to the result of the deletion operation.
+   */
+  private async deleteManyBySuffixAndNamespaces(
+    criteria: TCriteria[],
+  ): Promise<DeleteResult> {
+    return await this.settingService.deleteMany({
+      $or: criteria.map(({ suffix, namespaces }) => ({
+        group: { $regex: new RegExp(`${suffix}$`), $nin: namespaces },
+      })),
+    });
+  }
+
+  /**
+   * Retrieves a list of channel Namespaces.
+   *
+   * @returns An array of channel Namespaces.
+   */
+  public getChannelNamespaces(): TExtractNamespace<'channel'>[] {
+    return this.channelService
+      .getAll()
+      .map((channel) => channel.getNamespace<TExtractExtension<'channel'>>());
+  }
+
+  /**
+   * Retrieves a list of helper Namespaces.
+   *
+   * @returns An array of helper Namespaces.
+   */
+  public getHelperNamespaces(): TExtractNamespace<'helper'>[] {
+    return this.helperService
+      .getAll()
+      .map((helper) => helper.getNamespace<TExtractExtension<'helper'>>());
+  }
+
+  /**
+   * Prune extensions unused settings.
+   *
+   */
+  public async pruneExtensionSettings(): Promise<void> {
+    const channels = this.getChannelNamespaces();
+    const helpers = this.getHelperNamespaces();
+    const { deletedCount } = await this.deleteManyBySuffixAndNamespaces([
+      { suffix: '_channel', namespaces: channels },
+      { suffix: '_helper', namespaces: helpers },
+    ]);
+
+    if (deletedCount > 0) {
+      this.loggerService.log(
+        `${deletedCount} unused setting${deletedCount === 1 ? '' : 's'} ${deletedCount === 1 ? 'is' : 'are'} successfully deleted!`,
+      );
+    }
+  }
+}
diff --git a/api/src/extension/extension.module.ts b/api/src/extension/extension.module.ts
new file mode 100644
index 000000000..8d43c8ab3
--- /dev/null
+++ b/api/src/extension/extension.module.ts
@@ -0,0 +1,33 @@
+/*
+ * Copyright © 2025 Hexastack. All rights reserved.
+ *
+ * Licensed under the GNU Affero General Public License v3.0 (AGPLv3) with the following additional terms:
+ * 1. The name "Hexabot" is a trademark of Hexastack. You may not use this name in derivative works without express written permission.
+ * 2. All derivative works must include clear attribution to the original creator and software, Hexastack and Hexabot, in a prominent location (e.g., in the software's "About" section, documentation, and README file).
+ */
+
+import { Global, Module, OnApplicationBootstrap } from '@nestjs/common';
+
+import { LoggerService } from '@/logger/logger.service';
+
+import { CleanupService } from './cleanup.service';
+
+@Global()
+@Module({
+  providers: [CleanupService],
+  exports: [CleanupService],
+})
+export class ExtensionModule implements OnApplicationBootstrap {
+  constructor(
+    private readonly loggerService: LoggerService,
+    private readonly cleanupService: CleanupService,
+  ) {}
+
+  async onApplicationBootstrap() {
+    try {
+      await this.cleanupService.pruneExtensionSettings();
+    } catch (error) {
+      this.loggerService.error('Unable to delete unused settings', error);
+    }
+  }
+}
diff --git a/api/src/extension/types.ts b/api/src/extension/types.ts
new file mode 100644
index 000000000..2514c1c8d
--- /dev/null
+++ b/api/src/extension/types.ts
@@ -0,0 +1,41 @@
+/*
+ * Copyright © 2025 Hexastack. All rights reserved.
+ *
+ * Licensed under the GNU Affero General Public License v3.0 (AGPLv3) with the following additional terms:
+ * 1. The name "Hexabot" is a trademark of Hexastack. You may not use this name in derivative works without express written permission.
+ * 2. All derivative works must include clear attribution to the original creator and software, Hexastack and Hexabot, in a prominent location (e.g., in the software's "About" section, documentation, and README file).
+ */
+
+import { ExtensionName } from '@/utils/types/extension';
+
+type TExcludedExtension = 'plugin';
+
+type TExcludeSuffix<
+  T,
+  S extends string = '_',
+  Suffix extends string = `${S}${TExcludedExtension}`,
+> = T extends `${infer _Base}${Suffix}` ? never : T;
+
+export type TExtensionName = TExcludeSuffix<ExtensionName, '-'>;
+
+export type TExtension =
+  Extract<TExtensionName, `${string}-${string}`> extends `${string}-${infer S}`
+    ? `${S}`
+    : never;
+
+export type TNamespace = HyphenToUnderscore<TExtensionName>;
+
+export type TExtractNamespace<
+  T extends TExtension = TExtension,
+  M extends TExtensionName = TExtensionName,
+> = M extends `${string}${T}` ? HyphenToUnderscore<M> : never;
+
+export type TExtractExtension<
+  T extends TExtension = TExtension,
+  M extends TExtensionName = TExtensionName,
+> = M extends `${string}${T}` ? M : never;
+
+export type TCriteria = {
+  suffix: `_${TExtension}`;
+  namespaces: TNamespace[];
+};
diff --git a/api/src/utils/test/fixtures/setting.ts b/api/src/utils/test/fixtures/setting.ts
index 39ccfadb6..7e848aeeb 100644
--- a/api/src/utils/test/fixtures/setting.ts
+++ b/api/src/utils/test/fixtures/setting.ts
@@ -11,6 +11,7 @@ import mongoose from 'mongoose';
 import { SettingCreateDto } from '@/setting/dto/setting.dto';
 import { SettingModel } from '@/setting/schemas/setting.schema';
 import { SettingType } from '@/setting/schemas/types';
+import { getRandom } from '@/utils/helpers/safeRandom';
 
 export const settingFixtures: SettingCreateDto[] = [
   {
@@ -90,6 +91,41 @@ export const settingFixtures: SettingCreateDto[] = [
     type: SettingType.text,
     weight: 10,
   },
+  {
+    group: `${getRandom()}_channel`,
+    label: `${getRandom()}`,
+    value: '',
+    type: SettingType.text,
+    weight: 11,
+  },
+  {
+    group: `${getRandom()}_helper`,
+    label: `${getRandom()}`,
+    value: '',
+    type: SettingType.text,
+    weight: 12,
+  },
+  {
+    group: `${getRandom()}_channel`,
+    label: `${getRandom()}`,
+    value: '',
+    type: SettingType.text,
+    weight: 13,
+  },
+  {
+    group: `${getRandom()}_helper`,
+    label: `${getRandom()}`,
+    value: '',
+    type: SettingType.text,
+    weight: 14,
+  },
+  {
+    group: 'local_storage_helper',
+    label: 'default storage helper label',
+    value: 'local-storage-helper',
+    type: SettingType.text,
+    weight: 15,
+  },
 ];
 
 export const installSettingFixtures = async () => {


Reviews:
user: login: marrouchi
body: Looks good @yassinedorbozgithub thanks, left some comments. 👏 
state: CHANGES_REQUESTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-04T07:34:06Z
user: login: yassinedorbozgithub
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-04T07:38:27Z
user: login: IkbelTalebHssan
body: LGTM :heavy_check_mark: 
![image](https://github.com/user-attachments/assets/011e42ed-c9aa-4bfe-a520-4b48ef0195fb)
![image](https://github.com/user-attachments/assets/19db406f-7cf7-44b6-91f6-48411a6c79a7)

state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-04-04T07:59:31Z
user: login: marrouchi
state: CHANGES_REQUESTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-04T09:34:58Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 2**

<details>
<summary>🧹 Nitpick comments (10)</summary><blockquote>

<details>
<summary>api/src/extension/cleanup.service.spec.ts (2)</summary><blockquote>

`92-108`: **Simplify the test assertion logic for better readability.**

The filter condition using double negatives is difficult to understand at first glance:

```typescript
!/_(channel|helper)$/.test(group) !== registeredNamespaces.includes(group as TNamespace)
```

Consider simplifying this condition to make the test more maintainable.


```diff
const filteredSettings = initialSettings.filter(
  ({ group }) =>
-    !/_(channel|helper)$/.test(group) !==
-    registeredNamespaces.includes(group as TNamespace),
+    // Keep settings that don't have channel/helper suffix OR are registered
+    !(/_(channel|helper)$/.test(group) && !registeredNamespaces.includes(group as TNamespace)),
);
```

---

`94-97`: **Consider adding a test comment to explain the purpose of registeredNamespaces.**

Adding a comment here would help readers understand what namespaces are being checked against.


```diff
const registeredNamespaces = [
+  // Get all currently registered channel and helper namespaces that should be preserved
  ...cleanupService.getChannelNamespaces(),
  ...cleanupService.getHelperNamespaces(),
];
```

</blockquote></details>
<details>
<summary>api/src/extension/types.ts (5)</summary><blockquote>

`11-11`: **Consider using a union type for TExcludedExtension**

The type is currently hardcoded to a single string literal 'plugin'. For better future extensibility, consider defining it as a union type to easily support additional excluded extensions if needed.



```diff
-type TExcludedExtension = 'plugin';
+type TExcludedExtension = 'plugin' | 'other-excluded-extension-type';
```

---

`13-17`: **Add documentation for complex type utility**

This generic utility type is well implemented, but its purpose and usage might not be immediately clear to other developers. Consider adding a documentation comment explaining what it does and when it should be used.



```diff
+/**
+ * Excludes types that end with a specific suffix.
+ * 
+ * @template T - The type to check
+ * @template S - The separator string (defaults to '_')
+ * @template Suffix - The constructed suffix to exclude
+ */
type TExcludeSuffix<
  T,
  S extends string = '_',
  Suffix extends string = `${S}${TExcludedExtension}`,
> = T extends `${infer _Base}${Suffix}` ? never : T;
```

---

`21-24`: **Add documentation for TExtension type**

This complex conditional type extracts the second part of hyphenated extension names. Adding a documentation comment would improve readability and maintainability.



```diff
+/**
+ * Extracts the extension type from a hyphenated extension name.
+ * For example, from 'some-helper', it extracts 'helper'.
+ */
export type TExtension =
  Extract<TExtensionName, `${string}-${string}`> extends `${string}-${infer S}`
    ? `${S}`
    : never;
```

---

`28-31`: **Document generic extraction utility types**

These generic types are powerful but could benefit from documentation explaining their purpose and usage patterns.



```diff
+/**
+ * Extracts and converts namespaces from extension names based on the provided extension type.
+ * For example, with T='helper' and M='discord-helper', it returns 'discord_helper'.
+ * 
+ * @template T - The extension type to match against
+ * @template M - The extension name to extract from
+ */
export type TExtractNamespace<
  T extends TExtension = TExtension,
  M extends TExtensionName = TExtensionName,
> = M extends `${string}${T}` ? HyphenToUnderscore<M> : never;

+/**
+ * Extracts extension names that match the provided extension type.
+ * For example, with T='helper' and M='discord-helper', it returns 'discord-helper'.
+ * 
+ * @template T - The extension type to match against
+ * @template M - The extension name to extract from
+ */
export type TExtractExtension<
  T extends TExtension = TExtension,
  M extends TExtensionName = TExtensionName,
> = M extends `${string}${T}` ? M : never;
```


Also applies to: 33-36

---

`38-41`: **Document TCriteria type**

This type is used in the `deleteManyBySuffixAndNamespaces` method of the `CleanupService`. Adding documentation would improve clarity.



```diff
+/**
+ * Criteria for deleting settings based on suffix and namespaces.
+ * Used in the CleanupService to identify which settings should be deleted.
+ */
export type TCriteria = {
  suffix: `_${TExtension}`;
  namespaces: TNamespace[];
};
```

</blockquote></details>
<details>
<summary>api/src/extension/cleanup.service.ts (3)</summary><blockquote>

`28-36`: **Add error handling and documentation to deleteManyBySuffixAndNamespaces**

This method constructs a MongoDB query to delete settings that match specific criteria. While the implementation is good, it would benefit from documentation and error handling.



```diff
+  /**
+   * Deletes settings that match the given suffix but are not in the provided namespaces
+   * 
+   * @param criteria - Array of criteria objects containing suffix and namespaces
+   * @returns Promise with delete operation result
+   */
  private async deleteManyBySuffixAndNamespaces(
    criteria: TCriteria[],
  ): Promise<DeleteResult> {
-    return await this.settingService.deleteMany({
-      $or: criteria.map(({ suffix, namespaces }) => ({
-        group: { $regex: suffix, $nin: namespaces },
-      })),
-    });
+    try {
+      return await this.settingService.deleteMany({
+        $or: criteria.map(({ suffix, namespaces }) => ({
+          group: { $regex: suffix, $nin: namespaces },
+        })),
+      });
+    } catch (error) {
+      this.loggerService.error(`Failed to delete settings: ${error.message}`, error.stack);
+      throw error;
+    }
  }
```

---

`38-48`: **Add documentation and consider future extensibility for namespace methods**

Both methods are well implemented but would benefit from documentation. If more extension types are added in the future, consider refactoring to a generic method.



```diff
+  /**
+   * Retrieves namespaces for all registered channels
+   * 
+   * @returns Array of channel namespaces
+   */
  public getChannelNamespaces(): TExtractNamespace<'channel'>[] {
    return this.channelService
      .getAll()
      .map((channel) => channel.getNamespace<TExtractExtension<'channel'>>());
  }

+  /**
+   * Retrieves namespaces for all registered helpers
+   * 
+   * @returns Array of helper namespaces
+   */
  public getHelperNamespaces(): TExtractNamespace<'helper'>[] {
    return this.helperService
      .getAll()
      .map((helper) => helper.getNamespace<TExtractExtension<'helper'>>());
  }
```

If more extension types are added, consider a generic approach:

```typescript
/**
 * Generic method to get namespaces for a specific extension type
 * 
 * @template T - The extension type
 * @param type - The extension type string
 * @param service - The service to get entities from
 * @returns Array of namespaces for the specified extension type
 */
private getNamespaces<T extends TExtension>(
  type: T,
  service: { getAll(): any[] }
): TExtractNamespace<T>[] {
  return service
    .getAll()
    .map((entity) => entity.getNamespace<TExtractExtension<T>>());
}
```

---

`50-63`: **Add documentation and improve error handling in pruneExtensionSettings**

While the implementation is good, it would benefit from documentation and better error handling. The method currently doesn't handle potential errors from the dependency calls.



```diff
+  /**
+   * Prunes unused extension settings for channels and helpers
+   * 
+   * @returns Promise that resolves once the pruning is complete
+   */
  public async pruneExtensionSettings() {
+    try {
      const channels = this.getChannelNamespaces();
      const helpers = this.getHelperNamespaces();
      const { deletedCount } = await this.deleteManyBySuffixAndNamespaces([
        { suffix: '_channel', namespaces: channels },
        { suffix: '_helper', namespaces: helpers },
      ]);

      if (deletedCount > 0) {
        this.loggerService.log(
          `${deletedCount} unused setting${deletedCount === 1 ? '' : 's'} ${deletedCount === 1 ? 'is' : 'are'} successfully deleted!`,
        );
      }
+    } catch (error) {
+      this.loggerService.error(`Failed to prune extension settings: ${error.message}`, error.stack);
+      throw error;
+    }
  }
```

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

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

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 78df48c3986b830d69e538e613dc001e4f3e71f0 and d73e4dc8cbb0bdf8444243321156126e3b1d1df5.

</details>

<details>
<summary>📒 Files selected for processing (6)</summary>

* `api/src/app.module.ts` (2 hunks)
* `api/src/extension/cleanup.service.spec.ts` (1 hunks)
* `api/src/extension/cleanup.service.ts` (1 hunks)
* `api/src/extension/extension.module.ts` (1 hunks)
* `api/src/extension/types.ts` (1 hunks)
* `api/src/utils/test/fixtures/setting.ts` (2 hunks)

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>🧬 Code Definitions (3)</summary>

<details>
<summary>api/src/extension/extension.module.ts (1)</summary><blockquote>

<details>
<summary>api/src/app.module.ts (1)</summary>

* `Module` (64-166)

</details>

</blockquote></details>
<details>
<summary>api/src/extension/cleanup.service.spec.ts (1)</summary><blockquote>

<details>
<summary>api/src/extension/types.ts (1)</summary>

* `TNamespace` (26-26)

</details>

</blockquote></details>
<details>
<summary>api/src/extension/cleanup.service.ts (1)</summary><blockquote>

<details>
<summary>api/src/extension/types.ts (3)</summary>

* `TCriteria` (38-41)
* `TExtractNamespace` (28-31)
* `TExtractExtension` (33-36)

</details>

</blockquote></details>

</details><details>
<summary>🪛 Biome (1.9.4)</summary>

<details>
<summary>api/src/extension/cleanup.service.spec.ts</summary>

[error] 73-78: This array contains an empty slot.

Unsafe fix: Replace hole with undefined


(lint/suspicious/noSparseArray)

</details>

</details>

</details>

<details>
<summary>🔇 Additional comments (9)</summary><blockquote>

<details>
<summary>api/src/app.module.ts (2)</summary>

`38-38`: **LGTM: Import statement added correctly.**

The ExtensionModule is correctly imported from its relative path.

---

`156-156`: **LGTM: Module integration looks good.**

The ExtensionModule is properly integrated into the application's module structure, placed after MigrationModule and before the spread operator for extraModules.

</details>
<details>
<summary>api/src/extension/extension.module.ts (4)</summary>

`9-14`: **LGTM: Module imports and dependencies look good.**

Appropriate imports from NestJS and related services. The module implements OnApplicationBootstrap interface which will allow for running cleanup logic during application startup.

---

`15-19`: **Good use of @Global() decorator and proper module exports.**

The module is correctly marked as global, making the CleanupService available throughout the application. The service is both provided and exported.

---

`20-24`: **LGTM: Constructor properly injects dependencies.**

The LoggerService and CleanupService are correctly injected via constructor.

---

`26-32`: **LGTM: Bootstrap lifecycle method implementation looks good.**

The onApplicationBootstrap method correctly calls the pruneExtensionSettings method and has proper error handling with logging.

</details>
<details>
<summary>api/src/utils/test/fixtures/setting.ts (2)</summary>

`14-14`: **LGTM: Import added correctly.**

The getRandom function is properly imported for generating random values in test fixtures.

---

`94-128`: **LGTM: Test fixtures added correctly.**

The new test fixtures provide a good variety of test cases:
- Random channel settings
- Random helper settings
- A specific helper setting with predefined values

These will be useful for testing the cleanup functionality.

</details>
<details>
<summary>api/src/extension/cleanup.service.ts (1)</summary>

`19-64`: **Add compodoc documentation for each function**

As requested in a previous review, please add compodoc documentation for each function in this class to improve code readability and maintainability.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-04T13:40:37Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 0**

<details>
<summary>🧹 Nitpick comments (1)</summary><blockquote>

<details>
<summary>api/src/extension/cleanup.service.ts (1)</summary><blockquote>

`36-44`: **Consider anchoring the regex to prevent partial matches.**

Using `$regex: suffix` may potentially match strings that contain the suffix anywhere, rather than solely at the end of the group name. If your intent is to only match settings that end with `'_channel'` or `'_helper'`, consider anchoring it:

```diff
- group: { $regex: suffix, $nin: namespaces },
+ group: { $regex: new RegExp(`${suffix}$`), $nin: namespaces },
```

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

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

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between d73e4dc8cbb0bdf8444243321156126e3b1d1df5 and 563e337a6557bff9a5f487e9498a5dc1121c92d4.

</details>

<details>
<summary>📒 Files selected for processing (2)</summary>

* `api/src/extension/cleanup.service.spec.ts` (1 hunks)
* `api/src/extension/cleanup.service.ts` (1 hunks)

</details>

<details>
<summary>🚧 Files skipped from review as they are similar to previous changes (1)</summary>

* api/src/extension/cleanup.service.spec.ts

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>🧬 Code Definitions (1)</summary>

<details>
<summary>api/src/extension/cleanup.service.ts (1)</summary><blockquote>

<details>
<summary>api/src/extension/types.ts (3)</summary>

* `TCriteria` (38-41)
* `TExtractNamespace` (28-31)
* `TExtractExtension` (33-36)

</details>

</blockquote></details>

</details>

</details>

<details>
<summary>⏰ Context from checks skipped due to timeout of 90000ms (3)</summary>

* GitHub Check: Analyze (javascript-typescript)
* GitHub Check: Frontend Tests
* GitHub Check: API-Tests

</details>

<details>
<summary>🔇 Additional comments (5)</summary><blockquote>

<details>
<summary>api/src/extension/cleanup.service.ts (5)</summary>

`1-7`: **Header license block looks good.**

No issues found with the license text.

---

`21-26`: **Constructor dependency injection follows NestJS best practices.**

The injected services are properly declared and typed. This readily supports features like mocking in unit tests.

---

`51-55`: **Sufficient documentation and clear logic.**

The doc comment and usage of `channel.getNamespace<TExtractExtension<'channel'>>()` are straightforward. Good job.

---

`62-66`: **Helper namespace retrieval mirrors channel logic effectively.**

The retrieval process is consistent and well-documented.

---

`72-85`: **Efficient cleanup process with proper logging.**

The reuse of `deleteManyBySuffixAndNamespaces` for multiple suffix-namespace pairs is clean, and the singular/plural handling in the log message is a nice touch.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-04T16:40:37Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 0**

<details>
<summary>🧹 Nitpick comments (2)</summary><blockquote>

<details>
<summary>api/src/extension/cleanup.service.ts (2)</summary><blockquote>

`36-45`: **Remove redundant `await` keyword for the return statement.**  
When returning a promise in an async function, using `return await ...` is not strictly needed unless you have additional try/catch logic. It might be cleaner to simply return the promise directly:  

```diff
-  return await this.settingService.deleteMany({
+  return this.settingService.deleteMany({
     $or: criteria.map(({ suffix, namespaces }) => ({
       group: { $regex: new RegExp(`${suffix}$`), $nin: namespaces },
     })),
   });
```

---

`68-85`: **Consider adding explicit error handling.**  
While the current logic is sufficient for normal operations, you might want to handle potential errors from `this.deleteManyBySuffixAndNamespaces()`. For example, if an exception occurs, you can log it or take alternative actions. This prevents unhandled promise rejections and provides clearer feedback during the cleanup process.

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

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

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 563e337a6557bff9a5f487e9498a5dc1121c92d4 and b51f5e85fbf89aa0434be35f591db17450de9b7b.

</details>

<details>
<summary>📒 Files selected for processing (1)</summary>

* `api/src/extension/cleanup.service.ts` (1 hunks)

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>🧬 Code Definitions (1)</summary>

<details>
<summary>api/src/extension/cleanup.service.ts (1)</summary><blockquote>

<details>
<summary>api/src/extension/types.ts (3)</summary>

* `TCriteria` (38-41)
* `TExtractNamespace` (28-31)
* `TExtractExtension` (33-36)

</details>

</blockquote></details>

</details>

</details>

<details>
<summary>⏰ Context from checks skipped due to timeout of 90000ms (3)</summary>

* GitHub Check: API-Tests
* GitHub Check: Frontend Tests
* GitHub Check: Analyze (javascript-typescript)

</details>

<details>
<summary>🔇 Additional comments (4)</summary><blockquote>

<details>
<summary>api/src/extension/cleanup.service.ts (4)</summary>

`1-7`: **License header looks consistent.**  
No issues found here; it aligns well with the codebase’s licensing measures.

---

`28-35`: **Ensure compodoc-format tags are used in doc comments if required.**  
It appears you have detailed JSDoc comments here, which is great. However, based on past review feedback (“Let's add a compodoc documentation for each function.”), you might want to confirm that these doc comments include any mandatory compodoc directives (e.g., `@param`, `@returns`) for full compatibility.

---

`46-55`: **Double-check use of doc comment tags for channel-related parameters or returns.**  
In some workplaces, compodoc requires explicit `@param` or `@returns` tags in addition to the summary. Confirm that your documentation setup includes these tags to ensure they’re properly recognized.

---

`57-66`: **Confirm helper documentation meets compodoc format.**  
Similar to the channel documentation, please ensure that each parameter and return value is documented in a compodoc-friendly manner if your project requires it.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-05T18:37:56Z
user: login: marrouchi
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-04-07T07:51:34Z

Review comments:
diff_hunk: @@ -0,0 +1,18 @@
+/*
+ * Copyright © 2025 Hexastack. All rights reserved.
+ *
+ * Licensed under the GNU Affero General Public License v3.0 (AGPLv3) with the following additional terms:
+ * 1. The name "Hexabot" is a trademark of Hexastack. You may not use this name in derivative works without express written permission.
+ * 2. All derivative works must include clear attribution to the original creator and software, Hexastack and Hexabot, in a prominent location (e.g., in the software's "About" section, documentation, and README file).
+ */
+
+import { Global, Module } from '@nestjs/common';
+
+import { CleanupService } from './cleanup.service';
+
+@Global()
+@Module({
+  providers: [CleanupService],
+  exports: [CleanupService],
+})
+export class CleanupModule {}
path: api/src/cleanup/cleanup.module.ts
user: login: marrouchi
body: I would recommend naming the module : ExtensionModule . So that we can add other services such as the cleanup service.
created_at: 2025-04-04T07:22:45Z
author_association: CONTRIBUTOR
diff_hunk: @@ -161,4 +165,17 @@ const i18nOptions: I18nOptions = {
     AppService,
   ],
 })
-export class HexabotModule {}
+export class HexabotModule implements OnApplicationBootstrap {
+  constructor(
+    private readonly loggerService: LoggerService,
+    private readonly cleanupService: CleanupService,
+  ) {}
+
+  async onApplicationBootstrap() {
+    try {
+      await this.cleanupService.deleteUnusedSettings();
+    } catch (error) {
+      this.loggerService.error('Unable to delete unused settings', error);
+    }
+  }
path: api/src/app.module.ts
user: login: marrouchi
body: Let's put this function inside  the ExtensionModule (see the other comment)
created_at: 2025-04-04T07:23:38Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,62 @@
+/*
+ * Copyright © 2025 Hexastack. All rights reserved.
+ *
+ * Licensed under the GNU Affero General Public License v3.0 (AGPLv3) with the following additional terms:
+ * 1. The name "Hexabot" is a trademark of Hexastack. You may not use this name in derivative works without express written permission.
+ * 2. All derivative works must include clear attribution to the original creator and software, Hexastack and Hexabot, in a prominent location (e.g., in the software's "About" section, documentation, and README file).
+ */
+
+import { Injectable } from '@nestjs/common';
+
+import { ChannelService } from '@/channel/channel.service';
+import { HelperService } from '@/helper/helper.service';
+import { LoggerService } from '@/logger/logger.service';
+import { SettingService } from '@/setting/services/setting.service';
+import { DeleteResult } from '@/utils/generics/base-repository';
+
+import { TCriteria, TExtractExtension, TExtractNamespace } from './types';
+
+@Injectable()
+export class CleanupService {
+  constructor(
+    private readonly helperService: HelperService,
+    private readonly loggerService: LoggerService,
+    private readonly settingService: SettingService,
+    private readonly channelService: ChannelService,
+  ) {}
+
+  private async deleteMany(criteria: TCriteria[]): Promise<DeleteResult> {
path: api/src/cleanup/cleanup.service.ts
user: login: marrouchi
body: ```suggestion
  private async deleteManyBySuffixAndNamespaces(criteria: TCriteria[]): Promise<DeleteResult> {
```
I suggest this function should live inside ethe setting service.
created_at: 2025-04-04T07:27:46Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,62 @@
+/*
+ * Copyright © 2025 Hexastack. All rights reserved.
+ *
+ * Licensed under the GNU Affero General Public License v3.0 (AGPLv3) with the following additional terms:
+ * 1. The name "Hexabot" is a trademark of Hexastack. You may not use this name in derivative works without express written permission.
+ * 2. All derivative works must include clear attribution to the original creator and software, Hexastack and Hexabot, in a prominent location (e.g., in the software's "About" section, documentation, and README file).
+ */
+
+import { Injectable } from '@nestjs/common';
+
+import { ChannelService } from '@/channel/channel.service';
+import { HelperService } from '@/helper/helper.service';
+import { LoggerService } from '@/logger/logger.service';
+import { SettingService } from '@/setting/services/setting.service';
+import { DeleteResult } from '@/utils/generics/base-repository';
+
+import { TCriteria, TExtractExtension, TExtractNamespace } from './types';
+
+@Injectable()
+export class CleanupService {
+  constructor(
+    private readonly helperService: HelperService,
+    private readonly loggerService: LoggerService,
+    private readonly settingService: SettingService,
+    private readonly channelService: ChannelService,
+  ) {}
+
+  private async deleteMany(criteria: TCriteria[]): Promise<DeleteResult> {
+    return await this.settingService.deleteMany({
+      $or: criteria.map(({ suffix, namespaces }) => ({
+        group: { $regex: suffix, $nin: namespaces },
+      })),
+    });
+  }
+
+  public getChannelNamespaces(): TExtractNamespace<'channel'>[] {
+    return this.channelService
+      .getAll()
+      .map((channel) => channel.getNamespace<TExtractExtension<'channel'>>());
+  }
+
+  public getHelperNamespaces(): TExtractNamespace<'helper'>[] {
+    return this.helperService
+      .getAll()
+      .map((helper) => helper.getNamespace<TExtractExtension<'helper'>>());
+  }
+
+  public async deleteUnusedSettings() {
path: api/src/cleanup/cleanup.service.ts
user: login: marrouchi
body: ```suggestion
  public async pruneExtensionSettings() {
```
created_at: 2025-04-04T07:33:27Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,18 @@
+/*
+ * Copyright © 2025 Hexastack. All rights reserved.
+ *
+ * Licensed under the GNU Affero General Public License v3.0 (AGPLv3) with the following additional terms:
+ * 1. The name "Hexabot" is a trademark of Hexastack. You may not use this name in derivative works without express written permission.
+ * 2. All derivative works must include clear attribution to the original creator and software, Hexastack and Hexabot, in a prominent location (e.g., in the software's "About" section, documentation, and README file).
+ */
+
+import { Global, Module } from '@nestjs/common';
+
+import { CleanupService } from './cleanup.service';
+
+@Global()
+@Module({
+  providers: [CleanupService],
+  exports: [CleanupService],
+})
+export class CleanupModule {}
path: api/src/cleanup/cleanup.module.ts
user: login: yassinedorbozgithub
body: Yes 100% ExtensionModule name gives a better meaning about the role of the module, happy to apply the update :technologist: 
created_at: 2025-04-04T07:38:27Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,64 @@
+/*
+ * Copyright © 2025 Hexastack. All rights reserved.
+ *
+ * Licensed under the GNU Affero General Public License v3.0 (AGPLv3) with the following additional terms:
+ * 1. The name "Hexabot" is a trademark of Hexastack. You may not use this name in derivative works without express written permission.
+ * 2. All derivative works must include clear attribution to the original creator and software, Hexastack and Hexabot, in a prominent location (e.g., in the software's "About" section, documentation, and README file).
+ */
+
+import { Injectable } from '@nestjs/common';
+
+import { ChannelService } from '@/channel/channel.service';
+import { HelperService } from '@/helper/helper.service';
+import { LoggerService } from '@/logger/logger.service';
+import { SettingService } from '@/setting/services/setting.service';
+import { DeleteResult } from '@/utils/generics/base-repository';
+
+import { TCriteria, TExtractExtension, TExtractNamespace } from './types';
+
+@Injectable()
+export class CleanupService {
+  constructor(
+    private readonly helperService: HelperService,
+    private readonly loggerService: LoggerService,
+    private readonly settingService: SettingService,
+    private readonly channelService: ChannelService,
+  ) {}
+
+  private async deleteManyBySuffixAndNamespaces(
+    criteria: TCriteria[],
+  ): Promise<DeleteResult> {
+    return await this.settingService.deleteMany({
+      $or: criteria.map(({ suffix, namespaces }) => ({
+        group: { $regex: suffix, $nin: namespaces },
+      })),
+    });
+  }
+
+  public getChannelNamespaces(): TExtractNamespace<'channel'>[] {
+    return this.channelService
+      .getAll()
+      .map((channel) => channel.getNamespace<TExtractExtension<'channel'>>());
+  }
+
+  public getHelperNamespaces(): TExtractNamespace<'helper'>[] {
+    return this.helperService
+      .getAll()
+      .map((helper) => helper.getNamespace<TExtractExtension<'helper'>>());
+  }
+
+  public async pruneExtensionSettings() {
path: api/src/extension/cleanup.service.ts
user: login: marrouchi
body: Let's add a compodoc documentation for each function.
created_at: 2025-04-04T09:34:00Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,110 @@
+/*
+ * Copyright © 2025 Hexastack. All rights reserved.
+ *
+ * Licensed under the GNU Affero General Public License v3.0 (AGPLv3) with the following additional terms:
+ * 1. The name "Hexabot" is a trademark of Hexastack. You may not use this name in derivative works without express written permission.
+ * 2. All derivative works must include clear attribution to the original creator and software, Hexastack and Hexabot, in a prominent location (e.g., in the software's "About" section, documentation, and README file).
+ */
+
+import { CACHE_MANAGER } from '@nestjs/cache-manager';
+import { MongooseModule } from '@nestjs/mongoose';
+
+import { AttachmentRepository } from '@/attachment/repositories/attachment.repository';
+import { AttachmentModel } from '@/attachment/schemas/attachment.schema';
+import { AttachmentService } from '@/attachment/services/attachment.service';
+import { ChannelService } from '@/channel/channel.service';
+import { SubscriberRepository } from '@/chat/repositories/subscriber.repository';
+import { SubscriberModel } from '@/chat/schemas/subscriber.schema';
+import { SubscriberService } from '@/chat/services/subscriber.service';
+import LocalStorageHelper from '@/extensions/helpers/local-storage/index.helper';
+import { HelperService } from '@/helper/helper.service';
+import { LoggerService } from '@/logger/logger.service';
+import { SettingRepository } from '@/setting/repositories/setting.repository';
+import { Setting, SettingModel } from '@/setting/schemas/setting.schema';
+import { SettingSeeder } from '@/setting/seeds/setting.seed';
+import { SettingService } from '@/setting/services/setting.service';
+import { installSettingFixtures } from '@/utils/test/fixtures/setting';
+import {
+  closeInMongodConnection,
+  rootMongooseTestModule,
+} from '@/utils/test/test';
+import { buildTestingMocks } from '@/utils/test/utils';
+
+import { CleanupService } from './cleanup.service';
+import { TNamespace } from './types';
+
+describe('CleanupService', () => {
+  let initialSettings: Setting[];
+  let helperService: HelperService;
+  let cleanupService: CleanupService;
+  let settingService: SettingService;
+
+  beforeAll(async () => {
+    const { getMocks, resolveMocks } = await buildTestingMocks({
+      imports: [
+        rootMongooseTestModule(installSettingFixtures),
+        MongooseModule.forFeature([
+          SettingModel,
+          SubscriberModel,
+          AttachmentModel,
+        ]),
+      ],
+      providers: [
+        CleanupService,
+        HelperService,
+        SettingService,
+        SettingRepository,
+        {
+          provide: CACHE_MANAGER,
+          useValue: {
+            del: jest.fn(),
+            get: jest.fn(),
+            set: jest.fn(),
+          },
+        },
+        SettingSeeder,
+        SubscriberService,
+        SubscriberRepository,
+        AttachmentService,
+        AttachmentRepository,
+        ChannelService,
+      ],
+    });
+    [cleanupService, settingService, helperService] = await getMocks([
+      CleanupService,
+      SettingService,
+      HelperService,
+      ,
+    ]);
path: api/src/extension/cleanup.service.spec.ts
user: login: coderabbitai[bot]
body: _⚠️ Potential issue_

**Remove the empty array slot.**

There's an extra comma after `HelperService` which creates an empty slot in the array.


```diff
[cleanupService, settingService, helperService] = await getMocks([
  CleanupService,
  SettingService,
  HelperService,
-  ,
]);
```


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **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.

`````suggestion
    [cleanupService, settingService, helperService] = await getMocks([
      CleanupService,
      SettingService,
      HelperService,
    ]);
`````

</details>

<!-- suggestion_end -->

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 Biome (1.9.4)</summary>

[error] 73-78: This array contains an empty slot.

Unsafe fix: Replace hole with undefined


(lint/suspicious/noSparseArray)

</details>

</details>

<!-- This is an auto-generated comment by CodeRabbit -->

✅ Addressed in commits 7a30c36 to 563e337
created_at: 2025-04-04T13:40:36Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,41 @@
+/*
+ * Copyright © 2025 Hexastack. All rights reserved.
+ *
+ * Licensed under the GNU Affero General Public License v3.0 (AGPLv3) with the following additional terms:
+ * 1. The name "Hexabot" is a trademark of Hexastack. You may not use this name in derivative works without express written permission.
+ * 2. All derivative works must include clear attribution to the original creator and software, Hexastack and Hexabot, in a prominent location (e.g., in the software's "About" section, documentation, and README file).
+ */
+
+import { ExtensionName } from '@/utils/types/extension';
+
+type TExcludedExtension = 'plugin';
+
+type TExcludeSuffix<
+  T,
+  S extends string = '_',
+  Suffix extends string = `${S}${TExcludedExtension}`,
+> = T extends `${infer _Base}${Suffix}` ? never : T;
+
+export type TExtensionName = TExcludeSuffix<ExtensionName, '-'>;
+
+export type TExtension =
+  Extract<TExtensionName, `${string}-${string}`> extends `${string}-${infer S}`
+    ? `${S}`
+    : never;
+
+export type TNamespace = HyphenToUnderscore<TExtensionName>;
path: api/src/extension/types.ts
user: login: coderabbitai[bot]
body: _⚠️ Potential issue_

**Missing HyphenToUnderscore utility type**

The `HyphenToUnderscore` utility type is used but not defined in this file or imported. This will lead to compilation errors.



Ensure HyphenToUnderscore is either defined in this file or imported from another module:

```diff
import { ExtensionName } from '@/utils/types/extension';
+import { HyphenToUnderscore } from '@/utils/types/string-manipulation';
```

Or define it in this file:

```typescript
/**
 * Converts hyphenated strings to underscored format.
 * For example, 'some-helper' becomes 'some_helper'.
 */
type HyphenToUnderscore<T extends string> = T extends `${infer A}-${infer B}`
  ? `${A}_${HyphenToUnderscore<B>}`
  : T;
```

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-04-04T13:40:36Z
author_association: CONTRIBUTOR

PR comments:
user: login: marrouchi
created_at: 2025-04-04T13:37:53Z
body: @coderabbitai review
author_association: CONTRIBUTOR
user: login: coderabbitai[bot]
created_at: 2025-04-04T13:37:59Z
body: <!-- This is an auto-generated reply by CodeRabbit -->
<details>
<summary>:white_check_mark: Actions performed</summary>

Review triggered.

> Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

</details>
author_association: CONTRIBUTOR
user: login: coderabbitai[bot]
created_at: 2025-04-04T13:38:02Z
body: <!-- This is an auto-generated comment: summarize by coderabbit.ai -->
<!-- walkthrough_start -->

## Walkthrough

This pull request integrates a new extension module into the application by adding the `ExtensionModule` to the main application module. It introduces a `CleanupService` with methods to prune unregistered settings based on channel and helper namespaces, alongside corresponding type definitions. The PR also adds unit tests for the cleanup functionality and updates test fixtures to include new settings data. The extension module leverages the application bootstrap lifecycle, calling cleanup logic on startup.

## Changes

| Files                                                                                      | Change Summary                                                                                                                                                     |
|--------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `api/src/app.module.ts`                                                                    | Added import and inclusion of `ExtensionModule` in the module's imports array.                                                                                     |
| `api/src/extension/extension.module.ts`, `api/src/extension/cleanup.service.ts`, `api/src/extension/types.ts` | Introduced new extension components: `ExtensionModule` (with bootstrap logic), `CleanupService` (with deletion and namespace methods), and related type definitions. |
| `api/src/extension/cleanup.service.spec.ts`                                                | Added Jest unit tests for `CleanupService` to verify deletion logic for unregistered settings.                                                                     |
| `api/src/utils/test/fixtures/setting.ts`                                                   | Updated test fixtures with five new setting entries using the dynamic `getRandom()` function for channel and helper groups.                                          |

## Sequence Diagram(s)

```mermaid
sequenceDiagram
    participant App as Application
    participant EM as ExtensionModule
    participant CS as CleanupService
    participant Logger as LoggerService
    participant DB as Database

    App->>EM: onApplicationBootstrap()
    EM->>CS: pruneExtensionSettings()
    CS->>CS: getChannelNamespaces() & getHelperNamespaces()
    CS->>DB: Delete settings by suffix and namespaces
    DB-->>CS: Return deletion result
    CS->>Logger: Log number of deleted settings
    EM-->>App: Bootstrap complete
```

## Assessment against linked issues

| Objective                                | Addressed | Explanation                                                     |
|------------------------------------------|:---------:|-----------------------------------------------------------------|
| Argumentation (Pros/Cons Table)          |    ❌     | Missing documented pros/cons evaluation in the PR.              |
| Leverage OO inheritance                  |    ❓     | No explicit use of OO inheritance is evident in the implementation.  |
| Unit tests                               |    ✅     | Unit tests for the cleanup functionality are provided.          |
| Update Documentation                     |    ❌     | No updates to documentation are observed in this PR.            |

## Poem

> I'm a little rabbit with a code delight,  
> Hopping through changes both crisp and light.  
> New modules bloom in the springtime code,  
> With cleanup services on a nimble mode.  
> I celebrate with ASCII carrots and hops galore—  
> May our settings be neat forevermore!  
> 🐇🥕

<!-- walkthrough_end -->
<!-- internal state start -->


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

<!-- internal state end -->
<!-- finishing_touch_checkbox_start -->

<details>
<summary>✨ Finishing Touches</summary>

- [ ] <!-- {"checkboxId": "7962f53c-55bc-4827-bfbf-6a18da830691"} --> 📝 Generate Docstrings

</details>

<!-- finishing_touch_checkbox_end -->
<!-- tips_start -->

---



<details>
<summary>🪧 Tips</summary>

### Chat

There are 3 ways to chat with [CodeRabbit](https://coderabbit.ai?utm_source=oss&utm_medium=github&utm_campaign=Hexastack/Hexabot&utm_content=883):

- 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 generate docstrings` to [generate docstrings](https://docs.coderabbit.ai/finishing-touches/docstrings) for this PR.
- `@coderabbitai resolve` resolve all the CodeRabbit review comments.
- `@coderabbitai plan` to trigger planning for file edits and PR creation.
- `@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](https://docs.coderabbit.ai/guides/configure-coderabbit) 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](https://docs.coderabbit.ai) for detailed information on how to use CodeRabbit.
- Join our [Discord Community](http://discord.gg/coderabbit) to get help, request features, and share feedback.
- Follow us on [X/Twitter](https://twitter.com/coderabbitai) for updates and announcements.

</details>

<!-- tips_end -->
author_association: CONTRIBUTOR