Skip to content
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import './App.css'

import TestServices from "./components/TestServices";
function App() {

return (
<>
<h1>Hello, OrgExplorer!</h1>
<TestServices />
</>
)
}
Expand Down
60 changes: 60 additions & 0 deletions src/components/TestServices.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import React, { useState } from "react"
import { tokenService, githubService } from "../services"
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Unify token flow: remove redundant tokenService write or consume it in githubService.

tokenService.setToken(trimmedToken) is currently unused by githubService.fetchOrgReposWithCache(trimmedOrg, trimmedToken) (token is passed directly), which creates two parallel auth paths and weakens API clarity.

♻️ Minimal cleanup option
-import { tokenService, githubService } from "../services"
+import { githubService } from "../services"
@@
-tokenService.setToken(trimmedToken)
 // funtion which fetches org repos is called

Also applies to: 23-29

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/TestServices.tsx` at line 2, The code currently writes the
token with tokenService.setToken(trimmedToken) but then passes trimmedToken
directly into githubService.fetchOrgReposWithCache(trimmedOrg, trimmedToken),
creating two parallel auth flows; either remove the redundant
tokenService.setToken call or make githubService consume the tokenService
instead of accepting a token param. Fix by choosing one flow: (A) delete the
tokenService.setToken(trimmedToken) calls in TestServices.tsx and keep passing
the token into githubService.fetchOrgReposWithCache(trimmedOrg, trimmedToken);
or (B) stop passing trimmedToken to githubService.fetchOrgReposWithCache and
instead call tokenService.setToken(trimmedToken) then call
githubService.fetchOrgReposWithCache(trimmedOrg) (and update
fetchOrgReposWithCache to read token from tokenService if needed). Ensure
references to tokenService.setToken and
githubService.fetchOrgReposWithCache(trimmedOrg, trimmedToken) are updated
consistently across the file (lines around the existing calls).

// funtion to accept PAT and organisation name
export default function TestServices() {
const [tokenInput, setTokenInput] = useState("")
const [orgInput, setOrgInput] = useState("")

const testFlow = async () => {
if (!tokenInput) {
alert("Please enter GitHub token")
return
}

if (!orgInput) {
alert("Please enter organization name")
return
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
}

tokenService.setToken(tokenInput)
// funtion which fetches org repos is called
try {
const repos = await githubService.fetchOrgReposWithCache(
orgInput,
tokenInput
)

console.log("Repos count:", repos.length)
console.log("Repo names:")
repos.forEach(repo => console.log(repo.name))
Copy link
Copy Markdown

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:

# First, verify the pattern exists in the file
rg -nP 'forEach\s*\(\s*\w+\s*=>\s*console\.log\(' src/components/TestServices.tsx

Repository: AOSSIE-Org/OrgExplorer

Length of output: 120


🏁 Script executed:

# Also check the full context around line 33
sed -n '25,40p' src/components/TestServices.tsx

Repository: AOSSIE-Org/OrgExplorer

Length of output: 447


🏁 Script executed:

# Verify the file exists and check its basic properties
ls -la src/components/TestServices.tsx && head -35 src/components/TestServices.tsx | tail -10

Repository: AOSSIE-Org/OrgExplorer

Length of output: 416


Fix forEach callback to use block body instead of expression body.

Line 33 uses an expression-bodied callback in forEach, which returns an implicit value and triggers Biome's lint/suspicious/useIterableCallbackReturn rule. Use a block body with curly braces to clarify that no return value is intended.

Proposed fix
-      repos.forEach(repo => console.log(repo.name))
+      repos.forEach((repo) => {
+        console.log(repo.name)
+      })
📝 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.

Suggested change
repos.forEach(repo => console.log(repo.name))
repos.forEach((repo) => {
console.log(repo.name)
})
🧰 Tools
🪛 Biome (2.4.4)

[error] 33-33: This callback passed to forEach() iterable method should not return a value.

(lint/suspicious/useIterableCallbackReturn)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/TestServices.tsx` at line 33, The forEach callback currently
uses an expression body (repos.forEach(repo => console.log(repo.name))) which
can be mistaken for returning a value; change the callback to a block body so it
clearly returns nothing — update the repos.forEach callback to use curly braces
and a statement (e.g., in the same callback that references repo.name) and
ensure the console.log call ends with a semicolon, so the callback is an
explicit void-style block.


} catch (error) {
console.error("Error:", error)
alert("Failed to fetch repositories");

}
}
// input fileds to enter PAT and org name
return (
<div style={{ padding: "1rem" }}>
<input
type="password"
placeholder="Enter GitHub PAT"
value={tokenInput}
onChange={(e) => setTokenInput(e.target.value)}
/>

<input
type="text"
placeholder="Enter organization name"
value={orgInput}
onChange={(e) => setOrgInput(e.target.value)}
style={{ marginLeft: "10px" }}
/>

<button onClick={testFlow} style={{ marginLeft: "10px" }}>
Comment thread
swathi2006 marked this conversation as resolved.
Outdated
Test Services
Comment thread
swathi2006 marked this conversation as resolved.
Outdated
</button>
</div>
)
}
91 changes: 91 additions & 0 deletions src/services/cacheService.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@

// export default cacheService;
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
import { saveToIDB, getFromIDB } from "./idbService"

/**
* Minimal GitHub Repo Type
*/
export interface GitHubRepo {
id: number
name: string
stargazers_count: number
forks_count: number
language: string | null
updated_at: string
}

/**
* Structured cache entry format
*/
type RepoCacheEntry = {
data: GitHubRepo[]
savedAt: number
}

/**
* Cache expiry time (10 minutes)
*/
const MAX_CACHE_AGE = 1000 * 60 * 10

const cacheService = {
// repos of an org are saved in cache
async saveRepos(org: string, data: GitHubRepo[] | RepoCacheEntry): Promise<void> {
const entry: RepoCacheEntry = Array.isArray(data)
? { data, savedAt: Date.now() }
: data

console.log(`Saving ${org} repos to IDB`)
await saveToIDB(org, entry)
},
// repos are fetched from cache if they are in cache already
async getRepos(org: string): Promise<GitHubRepo[] | null> {
const entry = await getFromIDB(org)

Comment on lines +41 to +43
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Treat cache read failures as cache misses to preserve API fallback.

If IndexedDB read throws, this currently propagates and prevents fetchOrgReposWithCache from hitting the network path.

Proposed fix
   async getRepos(org: string): Promise<GitHubRepo[] | null> {
-    const entry = await getFromIDB(org)
+    let entry: unknown
+    try {
+      entry = await getFromIDB(org)
+    } catch (error) {
+      console.warn(`Cache read failed for ${org}:`, error)
+      return null
+    }
 
     if (!entry) {
       console.log("No cache found")
       return null
📝 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.

Suggested change
async getRepos(org: string): Promise<GitHubRepo[] | null> {
const entry = await getFromIDB(org)
async getRepos(org: string): Promise<GitHubRepo[] | null> {
let entry: unknown
try {
entry = await getFromIDB(org)
} catch (error) {
console.warn(`Cache read failed for ${org}:`, error)
return null
}
if (!entry) {
console.log("No cache found")
return null
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/services/cacheService.ts` around lines 41 - 43, The getRepos function
currently lets IndexedDB read errors propagate and block the network fallback in
fetchOrgReposWithCache; modify getRepos to treat any failures from
getFromIDB(org) as cache misses by wrapping the await getFromIDB(org) call in a
try/catch (or catch the Promise) and returning null on error (optionally logging
the error), so fetchOrgReposWithCache can continue to the network path.

if (!entry) {
console.log("No cache found")
return null
}

console.log("Cache found")

// Handle old format (raw array)
if (Array.isArray(entry)) {

console.log("Detected old cache format, migrating...");
const migratedEntry: RepoCacheEntry = {
data: entry,
savedAt: Date.now()
}

cacheService.saveRepos(org, migratedEntry).catch(err =>
console.error("Migration failed:", err)
)

return entry
}

// Handle structured format
if (
typeof entry === "object" &&
entry !== null &&
"data" in entry &&
"savedAt" in entry
) {
const typedEntry = entry as RepoCacheEntry

// TTL CHECK
if (Date.now() - typedEntry.savedAt > MAX_CACHE_AGE) {
console.log("Cache expired")
return null
}

return typedEntry.data
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Harden runtime validation for structured cache entries.

Checking only key presence allows malformed values through (e.g., savedAt not numeric, data not array), which can cause downstream runtime errors.

Proposed fix
-    if (
-      typeof entry === "object" &&
-      entry !== null &&
-      "data" in entry &&
-      "savedAt" in entry
-    ) {
+    if (
+      typeof entry === "object" &&
+      entry !== null &&
+      "data" in entry &&
+      Array.isArray((entry as { data: unknown }).data) &&
+      "savedAt" in entry &&
+      typeof (entry as { savedAt: unknown }).savedAt === "number"
+    ) {
       const typedEntry = entry as RepoCacheEntry
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/services/cacheService.ts` around lines 68 - 83, The current runtime check
only ensures keys exist on the cache object but not their types; update the
validation in the block that casts to RepoCacheEntry so it also asserts
Number.isFinite(typedEntry.savedAt) and typeof typedEntry.savedAt === "number"
(and e.g. Array.isArray(typedEntry.data) or whatever concrete shape
RepoCacheEntry.data must be) and that savedAt is non-negative before performing
the TTL check against MAX_CACHE_AGE; if any of those type/shape checks fail, log
or silently return null instead of proceeding to use malformed values. Ensure
you touch the same block that references RepoCacheEntry, savedAt, data, and
MAX_CACHE_AGE.


console.warn(`Cache for ${org} is in an unrecognized format.`)
return null
}

}

export default cacheService
77 changes: 77 additions & 0 deletions src/services/githubService.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import cacheService from "./cacheService";
import type { GitHubRepo } from "./cacheService"
const GITHUB_API_URL = 'https://api.github.com';

const githubService = {


async fetchOrgRepos(org: string, token: string): Promise<GitHubRepo[]> {
try {
const url = `${GITHUB_API_URL}/orgs/${org}/repos`;
console.log("Fetching from:", url);

const response = await fetch(url, {
method: 'GET',
headers: {
Authorization: `Bearer ${token}`,
Accept: 'application/vnd.github+json',
}
});

if (!response.ok) {
if (response.status === 401) {
throw new Error("Invalid or expired GitHub token. Please check your PAT.");
}

if (response.status === 403) {
throw new Error("Rate limit exceeded. Please try again later.");
}

if (response.status === 404) {
throw new Error("Organization not found.");
}

throw new Error(
`GitHub API error: ${response.status} ${response.statusText}`
);
}

return await response.json();
Comment on lines +10 to +39
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Fetch all repository pages instead of only the first page.

GitHub org repos API is paginated; the current implementation can silently cache only the first 30 repos for larger organizations.

Proposed fix
-      const url = `${GITHUB_API_URL}/orgs/${org}/repos`;
-      console.log("Fetching from:", url);
-
-      const response = await fetch(url, {
-        method: 'GET',
-        headers: {
-          Authorization: `Bearer ${token}`,
-          Accept: 'application/vnd.github+json',
-        }
-      });
+      const repos: GitHubRepo[] = [];
+      let page = 1;
+
+      while (true) {
+        const url = `${GITHUB_API_URL}/orgs/${encodeURIComponent(org)}/repos?per_page=100&page=${page}`;
+        console.log("Fetching from:", url);
+
+        const response = await fetch(url, {
+          method: "GET",
+          headers: {
+            Authorization: `Bearer ${token}`,
+            Accept: "application/vnd.github+json",
+          },
+        });
 
-     if (!response.ok) {
-  if (response.status === 401) {
-    throw new Error("Invalid or expired GitHub token. Please check your PAT.");
-  }
+        if (!response.ok) {
+          if (response.status === 401) {
+            throw new Error("Invalid or expired GitHub token. Please check your PAT.");
+          }
+          if (response.status === 403) {
+            throw new Error("Rate limit exceeded. Please try again later.");
+          }
+          if (response.status === 404) {
+            throw new Error("Organization not found.");
+          }
+          throw new Error(`GitHub API error: ${response.status} ${response.statusText}`);
+        }
 
-  if (response.status === 403) {
-    throw new Error("Rate limit exceeded. Please try again later.");
-  }
+        const batch = (await response.json()) as GitHubRepo[];
+        repos.push(...batch);
 
-  if (response.status === 404) {
-    throw new Error("Organization not found.");
-  }
+        if (batch.length < 100) break;
+        page += 1;
+      }
 
-  throw new Error(
-    `GitHub API error: ${response.status} ${response.statusText}`
-  );
-}
-
-      return await response.json();
+      return repos;
📝 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.

Suggested change
const url = `${GITHUB_API_URL}/orgs/${org}/repos`;
console.log("Fetching from:", url);
const response = await fetch(url, {
method: 'GET',
headers: {
Authorization: `Bearer ${token}`,
Accept: 'application/vnd.github+json',
}
});
if (!response.ok) {
if (response.status === 401) {
throw new Error("Invalid or expired GitHub token. Please check your PAT.");
}
if (response.status === 403) {
throw new Error("Rate limit exceeded. Please try again later.");
}
if (response.status === 404) {
throw new Error("Organization not found.");
}
throw new Error(
`GitHub API error: ${response.status} ${response.statusText}`
);
}
return await response.json();
const repos: GitHubRepo[] = [];
let page = 1;
while (true) {
const url = `${GITHUB_API_URL}/orgs/${encodeURIComponent(org)}/repos?per_page=100&page=${page}`;
console.log("Fetching from:", url);
const response = await fetch(url, {
method: "GET",
headers: {
Authorization: `Bearer ${token}`,
Accept: "application/vnd.github+json",
},
});
if (!response.ok) {
if (response.status === 401) {
throw new Error("Invalid or expired GitHub token. Please check your PAT.");
}
if (response.status === 403) {
throw new Error("Rate limit exceeded. Please try again later.");
}
if (response.status === 404) {
throw new Error("Organization not found.");
}
throw new Error(`GitHub API error: ${response.status} ${response.statusText}`);
}
const batch = (await response.json()) as GitHubRepo[];
repos.push(...batch);
if (batch.length < 100) break;
page += 1;
}
return repos;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/services/githubService.ts` around lines 10 - 39, The current
implementation only returns the first page of org repos; modify the fetch logic
to paginate until all pages are collected by repeatedly requesting the next page
(use the same Authorization/Accept headers) and concatenating results before
returning; detect next page from the response.headers.get('link') (look for
rel="next") or by incrementing a page query param until an empty result, reuse
the existing error handling for each response, and return the aggregated array
instead of a single response.json() call (references: GITHUB_API_URL, fetch
call, response, response.headers.get('link')).


} catch (error) {
console.error(`Error fetching repositories for organization ${org}:`, error);
if (error instanceof Error) {
alert(error.message)
} else {
alert("Something went wrong")
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
throw error;

}
},

async fetchOrgReposWithCache(org: string, token: string): Promise<GitHubRepo[]> {

// Step 1: Check IDB cache
const cachedRepos = await cacheService.getRepos(org);

if (cachedRepos) {
console.log("Using cached repos");
return cachedRepos;
}

// Step 2: Fetch from GitHub
const repos = await this.fetchOrgRepos(org, token);

// Step 3: Save structured cache
await cacheService.saveRepos(org, {
data: repos,
savedAt: Date.now()
});
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated

return repos;
}

};

export default githubService;
76 changes: 76 additions & 0 deletions src/services/idbService.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
// Name of the IndexedDB database
const DB_NAME = "OrgExplorerDB";

// Name of the object store (similar to a table in SQL)
const STORE_NAME = "repos";

/**
* Opens (or creates) the IndexedDB database.
* Returns a Promise that resolves with the database instance.
*/
function openDB(): Promise<IDBDatabase> {
return new Promise((resolve, reject) => {
// Open database with version 1
const request = indexedDB.open(DB_NAME, 1);

/**
* This event runs only when:
* - Database is created for the first time
* - Version number is increased
*/
request.onupgradeneeded = () => {
const db = request.result;

// Create object store if it does not already exist
if (!db.objectStoreNames.contains(STORE_NAME)) {
db.createObjectStore(STORE_NAME);
}
};

// If database opens successfully, resolve the Promise
request.onsuccess = () => resolve(request.result);

// If an error occurs while opening, reject the Promise
request.onerror = () => reject(request.error);
});
}

/**
* Saves data into IndexedDB.
* @param key Unique identifier (e.g., organization name)
* @param value Data to store (e.g., repos array with metadata)
*/
export async function saveToIDB(key: string, value: any) {
const db = await openDB();

// Create a read-write transaction
const tx = db.transaction(STORE_NAME, "readwrite");

// Access the object store
const store = tx.objectStore(STORE_NAME);

// Insert or update value using the provided key
store.put(value, key);
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated

/**
* Retrieves data from IndexedDB using a key.
* @param key Unique identifier (e.g., organization name)
* @returns Stored value or null if not found
*/
export async function getFromIDB(key: string): Promise<any | null> {
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
const db = await openDB();

// Create a read-only transaction
const tx = db.transaction(STORE_NAME, "readonly");

// Access the object store
const store = tx.objectStore(STORE_NAME);

return new Promise((resolve) => {
const req = store.get(key);

// Resolve with stored result or null if not found
req.onsuccess = () => resolve(req.result || null);
});
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
}
15 changes: 15 additions & 0 deletions src/services/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import tokenService from "./tokenService";
import githubService from "./githubService";
import cacheService from "./cacheService";

const { fetchOrgRepos } = githubService;
const { saveRepos, getRepos } = cacheService;

export {
tokenService,
githubService,
cacheService,
fetchOrgRepos,
saveRepos,
getRepos
};
35 changes: 35 additions & 0 deletions src/services/tokenService.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/**
* Service for managing the GitHub Personal Access Token (PAT).
* Currently stores the token in memory for security and as a temporary measure.
*
*
*/

let _token: string | null = null;

const tokenService = {
/**
* Sets the GitHub PAT in memory.
* @param token
*/
setToken(token: string): void {
_token = token;
},

/**
* Gets the GitHub PAT from memory.
* @returns
*/
getToken(): string | null {
return _token;
},

/**
* Removes the GitHub PAT from memory.
*/
removeToken(): void {
_token = null;
}
};

export default tokenService;