Skip to content

refactor: adds proper logger and move templates into module#2563

Merged
stalniy merged 1 commit intomainfrom
refactor/templates2
Jan 26, 2026
Merged

refactor: adds proper logger and move templates into module#2563
stalniy merged 1 commit intomainfrom
refactor/templates2

Conversation

@stalniy
Copy link
Contributor

@stalniy stalniy commented Jan 23, 2026

Why

To improve observability for templates

What

  1. Replaced functional templates tests with unit ones
  2. Added logger to template services
  3. Cache templates forever after the first cache read
  4. Speeds up search for template by its id

Summary by CodeRabbit

  • Refactor

    • Restructured template services with improved dependency injection, logging, and concurrency for more reliable and performant template fetching and serving.
    • Template processing now produces merged category data with an index for stable template lookup.
  • Tests

    • Extensive new unit tests for template fetching, processing, and gallery behaviors.
  • Chores

    • Cleaned up legacy test fixtures and sample cache files.

✏️ Tip: You can customize this high-level summary in your review settings.

@stalniy stalniy requested a review from a team as a code owner January 23, 2026 13:12
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Jan 23, 2026

📝 Walkthrough

Walkthrough

Refactors template subsystem to inject Logger, fetch, and Octokit dependencies; replaces PAT-centric Octokit helpers; relocates and reimplements TemplateGalleryService with commit-SHA caching and filesystem fallback; adds in-flight promise reuse helper; changes merge result shape to include a template ID index; adds extensive unit tests and removes legacy functional mocks/tests.

Changes

Cohort / File(s) Summary
Caching Helper
apps/api/src/caching/helpers.ts
Added reusePendingPromise to memoize in-flight promises and dedupe concurrent calls (optional custom key).
Template Gallery (new)
apps/api/src/template/services/template-gallery/template-gallery.service.ts, apps/api/src/template/services/template-gallery/template-gallery.service.spec.ts
New TemplateGalleryService: orchestrates parallel repo fetches, commit-SHA keyed filesystem cache, last-working fallback, memoized public API; added comprehensive unit tests.
Template Gallery (deleted)
apps/api/src/services/external/templates/template-gallery.service.ts
Removed previous external TemplateGalleryService implementation (deleted file).
Template Fetcher
apps/api/src/template/services/template-fetcher/template-fetcher.service.ts, apps/api/src/template/services/template-fetcher/template-fetcher.service.spec.ts
Constructor now injects LoggerService, fetch, and Octokit; uses injected fetch; replaces console logs with logger events; added extensive unit tests for fetch flows.
GitHub Service
apps/api/src/services/external/githubService.ts
Removed getOctokit helper and Octokit import.
Template Processor
apps/api/src/template/services/template-processor/template-processor.service.ts, apps/api/src/template/services/template-processor/template-processor.service.spec.ts
mergeTemplateCategories now returns { categories, templatesIds } (new exported type MergedTemplateCategoriesResult); added specs covering merge and processing behavior.
Types
apps/api/src/template/types/template.ts
Category.templates made optional; FinalCategory adjusted to ensure templates: Template[].
Controller & Wiring
apps/api/src/template/controllers/template/template.controller.ts
Injects LoggerService; instantiates TemplateGalleryService with (logger, fsp, { ... }); response mapping updated to use result.categories.
Build Script
apps/api/scripts/buildAkashTemplatesCache.ts
Adapted imports/constructor usage to new TemplateGalleryService signature; adds templateSourceProcessingConcurrency option.
Tests & Mocks Removed
apps/api/test/functional/*.spec.ts, apps/api/test/mocks/templates/**
Removed legacy functional tests and many static mock fixtures (cache and git fixtures); replaced by focused unit tests above.

Sequence Diagram(s)

sequenceDiagram
    participant Controller as TemplateController
    participant Gallery as TemplateGalleryService
    participant Fetcher as TemplateFetcherService
    participant Processor as TemplateProcessorService
    participant GitHub as GitHub/Octokit
    participant FS as FileSystem

    Controller->>Gallery: getTemplateGallery()
    Gallery->>Gallery: check lastWorking cache
    opt not cached
        par fetch repos
            Gallery->>Fetcher: fetch repo A
            Fetcher->>GitHub: fetch latest commit SHA
            GitHub-->>Fetcher: sha
            Fetcher->>FS: read cache by sha
            alt cache hit
                FS-->>Fetcher: cached categories
            else
                Fetcher->>GitHub: list files / fetch content
                fetcher->>Processor: processTemplate(file, readme, deploy, guide?)
                Processor-->>Fetcher: processed templates
                Fetcher->>FS: write cache by sha
            end
            Fetcher-->>Gallery: categories
            Note over Fetcher: repeat for other repos (B, C)
        end
        Gallery->>Processor: mergeTemplateCategories(all categories)
        Processor-->>Gallery: { categories, templatesIds }
        Gallery->>Gallery: store lastWorking
    end
    Gallery-->>Controller: categories (and index)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested reviewers

  • ygrishajev

Poem

🐰 Hops of code, I weave and bind,

Logger, fetch, and octokit aligned.
Promises reused, caches kept neat,
Templates merged, indexes complete.
A carrot-coded cheer for this deployable feat!

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main changes: refactoring to add proper logging and restructure templates into a module.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings

Comment @coderabbitai help to get the list of available commands and usage tips.

@stalniy stalniy force-pushed the refactor/templates2 branch from 5f2fb64 to 987ad03 Compare January 23, 2026 13:15
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

🤖 Fix all issues with AI agents
In `@apps/api/src/caching/helpers.ts`:
- Around line 151-164: The function signature uses `any` in the generic
constraint which violates the guideline; update the signature to avoid `any` by
changing the constraint to `T extends (...args: unknown[]) => Promise<unknown>`
(so `Parameters<T>` and `ReturnType<T>` still work) and ensure any other
parameter types that used `any[]` are replaced with `unknown[]` (e.g., the
`getKey` callback already uses `(...args: Parameters<T>)`, so no change there).
This keeps the same runtime behavior while removing `any` from
`reusePendingPromise`.

In `@apps/api/src/template/services/template-gallery/template-gallery.service.ts`:
- Around line 12-17: The Options type currently marks githubPAT as optional but
getOctokit (used in TemplateGalleryService and related functions) throws if PAT
is missing; update the Options type to make githubPAT required (remove the ? on
githubPAT) or, alternatively, add explicit handling where getOctokit is called
(in methods like the TemplateGalleryService constructor or any function that
calls getOctokit) to throw a clear error or provide a fallback when githubPAT is
undefined; ensure references to githubPAT in functions that build the Octokit
client (getOctokit) are updated to reflect the non-optional type or guarded with
a clear runtime check and error message.

In
`@apps/api/src/template/services/template-processor/template-processor.service.spec.ts`:
- Around line 464-474: Update the setup helper so it accepts a single typed
options parameter (e.g., options?: { templateSource?: Partial<TemplateSource> })
instead of no args: construct the default TemplateSource inline, merge any
provided options.templateSource into those defaults, instantiate
TemplateProcessorService(), and return { service, templateSource } so existing
tests keep working; reference function name setup and types
TemplateProcessorService and TemplateSource when making the change.
🧹 Nitpick comments (2)
apps/api/src/template/services/template-gallery/template-gallery.service.spec.ts (1)

190-212: Align setup with test conventions and avoid private state patching.

  • The setup helper should accept a single parameter with an inline type definition to match repo test conventions.
  • Object.assign(service, { templateFetcher }) pokes at private state; consider constructor injection or a factory to keep tests from mutating internals. I can help draft a test-friendly constructor/factory if you want.
♻️ Suggested setup signature adjustment
-  function setup() {
+  function setup({ fsAccessError = new Error("File not found") }: { fsAccessError?: Error } = {}) {
     const logger = mock<LoggerService>();
     const fsMock = mock<FileSystemApi>({
-      access: jest.fn(() => Promise.reject(new Error("File not found")))
+      access: jest.fn(() => Promise.reject(fsAccessError))
     });

As per coding guidelines, setup should accept a single parameter with an inline type definition.

apps/api/src/template/services/template-fetcher/template-fetcher.service.spec.ts (1)

604-612: Update setup signature to match test conventions.

The setup helper should accept a single parameter with an inline type definition.

♻️ Suggested setup signature adjustment
-  function setup() {
+  function setup({ fetchImpl }: { fetchImpl?: typeof globalThis.fetch } = {}) {
     const templateProcessor = mock<TemplateProcessorService>();
     const logger = mock<LoggerService>();
     const fetchMock = jest.fn();
     const octokit = mockDeep<Octokit>();
 
-    const service = new TemplateFetcherService(templateProcessor, logger, fetchMock as typeof globalThis.fetch, octokit);
+    const service = new TemplateFetcherService(
+      templateProcessor,
+      logger,
+      (fetchImpl ?? fetchMock) as typeof globalThis.fetch,
+      octokit
+    );

As per coding guidelines, setup should accept a single parameter with an inline type definition.

@stalniy stalniy force-pushed the refactor/templates2 branch from 987ad03 to 2902011 Compare January 23, 2026 13:39
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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
apps/api/src/template/services/template-fetcher/template-fetcher.service.ts (1)

146-173: Replace Promise<any> with proper return type.

The return type Promise<any> on line 150 violates the coding guideline forbidding any. Based on the function logic, this should return Promise<Template | null>.

🛠️ Suggested fix
+import type { Template } from "../../types/template";
+
 private async processTemplateSource(
   templateSource: TemplateSource,
   directoryItems: GithubDirectoryItem[],
   options: { includeConfigJson?: boolean }
-): Promise<any> {
+): Promise<Template | null> {
🤖 Fix all issues with AI agents
In `@apps/api/src/template/services/template-gallery/template-gallery.service.ts`:
- Around line 125-134: The code assumes files[0] from glob() is the "latest"
cached version, but glob() is unordered; update the logic in
template-gallery.service.ts that computes latestCachedVersionSha (using files[0]
and logging event "TEMPLATES_FALLBACK_LATEST_COMMIT_SHA_FOUND") to be
deterministic: either sort the files array (e.g., alphabetically or by file
mtime) then pick the newest entry before slicing to derive
latestCachedVersionSha, or rename the variable/log to something like
cachedVersionSha and change the event/message to indicate "a cached version"
instead of "latest" so behavior and logs are accurate.
♻️ Duplicate comments (3)
apps/api/src/template/services/template-processor/template-processor.service.spec.ts (1)

464-474: Make setup accept a single typed options parameter.

Per coding guidelines, the setup function should accept a single parameter with an inline type definition to enable test customization while avoiding shared state.

🛠️ Suggested fix
-function setup() {
+function setup({ templateSourceOverrides }: { templateSourceOverrides?: Partial<TemplateSource> } = {}) {
   const service = new TemplateProcessorService();
   const templateSource: TemplateSource = {
     name: "Test Template",
     path: "templates/app",
     repoOwner: "test-owner",
     repoName: "test-repo",
-    repoVersion: "main"
+    repoVersion: "main",
+    ...templateSourceOverrides
   };
   return { service, templateSource };
 }
apps/api/src/caching/helpers.ts (1)

151-165: Avoid any in reusePendingPromise signature.

The codebase guideline forbids any. Use unknown[] instead to maintain type safety while preserving the generic inference.

🛠️ Suggested fix
-export function reusePendingPromise<T extends (...args: any[]) => Promise<unknown>>(fn: T, options?: { getKey?: (...args: Parameters<T>) => string }): T {
+export function reusePendingPromise<T extends (...args: unknown[]) => Promise<unknown>>(fn: T, options?: { getKey?: (...args: Parameters<T>) => string }): T {
apps/api/src/template/services/template-gallery/template-gallery.service.ts (1)

12-17: Align githubPAT type with runtime requirement.

getOctokit throws when PAT is missing, but Options.githubPAT is optional. Make it required or guard before constructing.

Also applies to: 167-170

Comment on lines +125 to +134
if (!files.length) throw error;

const latestCachedVersionSha = files[0].slice(path.normalize(cachePathPrefix).length + 1, -1 * ".json".length);
this.#logger.debug({
event: "TEMPLATES_FALLBACK_LATEST_COMMIT_SHA_FOUND",
message: "Trying to use the latest cached version",
repository,
commitSha: latestCachedVersionSha
});
return latestCachedVersionSha;
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

cat -n apps/api/src/template/services/template-gallery/template-gallery.service.ts | sed -n '110,150p'

Repository: akash-network/console

Length of output: 2059


🌐 Web query:

Node.js fs.promises.glob ordering guarantee deterministic

💡 Result:

fs.promises.glob() does not document any ordering guarantee for the paths it yields, so you should treat the output order as unspecified / not deterministic across OS/filesystems/runs. The iterator is ultimately driven by filesystem directory iteration, which Node explicitly says returns entries in “no particular order” as provided by the OS. [1]

If you need deterministic results, collect and sort yourself, e.g.:

import { glob } from "node:fs/promises";

const matches = await Array.fromAsync(glob("**/*.js"));
matches.sort((a, b) => a.localeCompare(b)); // or Intl.Collator for natural sort

(For context: popular globbing implementations similarly warn that results may be returned in arbitrary order.) [2]

Sources: [1] [2]


"Latest cached version" selection isn't deterministic.

files[0] from glob() returns results in arbitrary order (no ordering guarantee), so the "latest" log message is misleading. Either sort files deterministically (e.g., alphabetically or by mtime) or rename the variable and message to reflect it's just "a cached version" rather than "the latest."

🤖 Prompt for AI Agents
In `@apps/api/src/template/services/template-gallery/template-gallery.service.ts`
around lines 125 - 134, The code assumes files[0] from glob() is the "latest"
cached version, but glob() is unordered; update the logic in
template-gallery.service.ts that computes latestCachedVersionSha (using files[0]
and logging event "TEMPLATES_FALLBACK_LATEST_COMMIT_SHA_FOUND") to be
deterministic: either sort the files array (e.g., alphabetically or by file
mtime) then pick the newest entry before slicing to derive
latestCachedVersionSha, or rename the variable/log to something like
cachedVersionSha and change the event/message to indicate "a cached version"
instead of "latest" so behavior and logs are accurate.

@codecov
Copy link

codecov bot commented Jan 23, 2026

Codecov Report

❌ Patch coverage is 92.72727% with 8 lines in your changes missing coverage. Please review.
✅ Project coverage is 50.55%. Comparing base (fa01b2c) to head (12e33bd).
⚠️ Report is 4 commits behind head on main.
✅ All tests successful. No failed tests found.

Files with missing lines Patch % Lines
...mplate/controllers/template/template.controller.ts 30.00% 7 Missing ⚠️
...vices/template-gallery/template-gallery.service.ts 98.48% 1 Missing ⚠️

❌ Your project status has failed because the head coverage (79.46%) is below the target coverage (80.00%). You can increase the head coverage or adjust the target coverage.

Additional details and impacted files
@@            Coverage Diff             @@
##             main    #2563      +/-   ##
==========================================
- Coverage   50.76%   50.55%   -0.22%     
==========================================
  Files        1069     1058      -11     
  Lines       29728    29401     -327     
  Branches     6583     6542      -41     
==========================================
- Hits        15091    14863     -228     
+ Misses      14286    14200      -86     
+ Partials      351      338      -13     
Flag Coverage Δ *Carryforward flag
api 79.46% <92.72%> (+0.19%) ⬆️
deploy-web 31.45% <ø> (ø) Carriedforward from fa01b2c
log-collector ?
notifications 87.94% <ø> (ø) Carriedforward from fa01b2c
provider-console 81.48% <ø> (ø) Carriedforward from fa01b2c
provider-proxy 84.35% <ø> (ø) Carriedforward from fa01b2c

*This pull request uses carry forward flags. Click here to find out more.

Files with missing lines Coverage Δ
apps/api/src/caching/helpers.ts 100.00% <100.00%> (ø)
...vices/template-fetcher/template-fetcher.service.ts 96.82% <100.00%> (ø)
...s/template-processor/template-processor.service.ts 100.00% <100.00%> (ø)
...vices/template-gallery/template-gallery.service.ts 98.48% <98.48%> (ø)
...mplate/controllers/template/template.controller.ts 36.00% <30.00%> (-64.00%) ⬇️

... and 14 files with indirect coverage changes

🚀 New features to boost your workflow:
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@stalniy stalniy force-pushed the refactor/templates2 branch from 2902011 to 70804f7 Compare January 23, 2026 14:12
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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
apps/api/src/template/services/template-fetcher/template-fetcher.service.ts (1)

146-172: Avoid any return type.

The method returns Promise<any> which violates the coding guideline forbidding any. Use the proper return type.

🛠️ Suggested fix
   private async processTemplateSource(
     templateSource: TemplateSource,
     directoryItems: GithubDirectoryItem[],
     options: { includeConfigJson?: boolean }
-  ): Promise<any> {
+  ): Promise<Template | null> {
♻️ Duplicate comments (3)
apps/api/src/caching/helpers.ts (1)

151-165: Avoid any in reusePendingPromise signature.

The function uses any[] in the generic constraint, which violates the coding guideline forbidding any. Use unknown[] instead.

🛠️ Suggested fix
-export function reusePendingPromise<T extends (...args: any[]) => Promise<unknown>>(fn: T, options?: { getKey?: (...args: Parameters<T>) => string }): T {
+export function reusePendingPromise<T extends (...args: unknown[]) => Promise<unknown>>(fn: T, options?: { getKey?: (...args: Parameters<T>) => string }): T {
apps/api/src/template/services/template-gallery/template-gallery.service.ts (2)

12-17: Make githubPAT required to match runtime behavior.

The Options type marks githubPAT as optional, but getOctokit throws when it's missing. This creates a type-safety gap where TypeScript won't catch missing PAT at compile time.

🛠️ Suggested fix
 type Options = {
-  githubPAT?: string;
+  githubPAT: string;
   dataFolderPath: string;
   categoryProcessingConcurrency?: number;
   templateSourceProcessingConcurrency?: number;
 };

Also applies to: 167-170


125-134: Non-deterministic cache file selection.

files[0] from glob() returns results in arbitrary order, so the "latest cached version" selection is not deterministic. Sort files before selecting, or rename the variable/log message to reflect it's "a cached version" rather than "the latest."

🛠️ Suggested fix
       const files = await Array.fromAsync(this.#fs.glob(`${cachePathPrefix}-*.json`));
+      files.sort(); // Ensure deterministic selection
       this.#logger.debug({
         event: "UNABLE_TO_FETCH_LATEST_TEMPLATES_COMMIT_SHA",
-        message: "Trying to use the latest cached version",
+        message: "Trying to use a cached version",
🧹 Nitpick comments (2)
apps/api/src/template/services/template-gallery/template-gallery.service.ts (1)

31-42: Inconsistent dependency injection: global fetch used instead of injected dependency.

The service accepts an injected FileSystemApi for testability, but line 35 uses the global fetch directly. Consider injecting fetch through the constructor for consistency and easier mocking in tests.

🛠️ Suggested refactor
 type Options = {
   githubPAT?: string;
   dataFolderPath: string;
   categoryProcessingConcurrency?: number;
   templateSourceProcessingConcurrency?: number;
+  fetch?: typeof globalThis.fetch;
 };

-  constructor(logger: LoggerService, fs: FileSystemApi, options: Options) {
+  constructor(logger: LoggerService, fs: FileSystemApi, options: Options) {
+    const fetchFn = options.fetch ?? fetch;
     this.#logger = logger;
     this.#fs = fs;
     this.templateProcessor = new TemplateProcessorService();
-    this.templateFetcher = new TemplateFetcherService(this.templateProcessor, this.#logger, fetch, getOctokit(options.githubPAT), {
+    this.templateFetcher = new TemplateFetcherService(this.templateProcessor, this.#logger, fetchFn, getOctokit(options.githubPAT), {
apps/api/src/template/services/template-fetcher/template-fetcher.service.spec.ts (1)

604-663: Align setup helper with test conventions (signature + placement).

Guidelines require setup to accept a single inline-typed parameter and to be the last function in the root describe. Consider moving helper functions above or outside the describe block and adding an optional overrides parameter. As per coding guidelines, ...

♻️ Example adjustment (signature only)
-  function setup() {
+  function setup({ fetchImpl }: { fetchImpl?: typeof globalThis.fetch } = {}) {
     const templateProcessor = mock<TemplateProcessorService>();
     const logger = mock<LoggerService>();
-    const fetchMock = jest.fn();
+    const fetchMock = fetchImpl ?? jest.fn();
     const octokit = mockDeep<Octokit>();

     const service = new TemplateFetcherService(templateProcessor, logger, fetchMock as typeof globalThis.fetch, octokit);

     return { service, templateProcessor, logger, fetchMock, octokit };
   }

@stalniy stalniy force-pushed the refactor/templates2 branch from 70804f7 to 12e33bd Compare January 23, 2026 14:45
this.getTemplateById = reusePendingPromise(this.getTemplateById.bind(this), { getKey: id => id });
}

@Memoize({ ttlInSeconds: Infinity })
Copy link
Contributor

Choose a reason for hiding this comment

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

question: how does that work?

Copy link
Contributor Author

@stalniy stalniy Jan 24, 2026

Choose a reason for hiding this comment

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

the intention was that templates will be read from fs and then cached forever ( in @Memoize there is a check which checks whether cache expired, there is no bigger value then Infinity, so this cache will never expire).

But I see a potential issue with this. If somebody updates templates, and our api restarts it will try to fetch them in runtime. So, this change with ttl is not enough to prevent runtime rebuild

Copy link
Contributor Author

@stalniy stalniy Jan 26, 2026

Choose a reason for hiding this comment

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

I'll merge this as is and will adjust the logic in a separate PR, just so it will be smaller

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

Comments