Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
34 changes: 21 additions & 13 deletions api/core/schema_loader.py
Original file line number Diff line number Diff line change
Expand Up @@ -142,23 +142,31 @@ async def generate():
return generate()


async def list_databases(user_id: str, general_prefix: Optional[str] = None) -> list[str]:
async def list_databases(
user_id: str, general_prefix: Optional[str] = None
) -> list[dict]:
"""
This route is used to list all the graphs (databases names) that are available in the database.
List all graphs (database names) available for the user.

Returns a list of dicts with keys ``id`` (the full FalkorDB graph name
used for API calls), ``name`` (the display name with namespace prefix
stripped) and ``is_demo`` (whether the graph is a shared demo graph).
Comment on lines +151 to +153
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Docstring for id does not match runtime behavior.

Line 151-153 says id is the full FalkorDB graph name for API calls, but Line 163 sets user graph id to the stripped display name. This can mislead callers and future maintainers.

Also applies to: 163-170

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

In `@api/core/schema_loader.py` around lines 151 - 153, The docstring says the
returned dict key "id" should be the full FalkorDB graph name for API calls, but
the implementation at the user-graph construction (lines around 163-170) sets
"id" to the stripped display name; change the runtime so the returned dict uses
the original full graph name for the "id" field while keeping the stripped value
in "name" and preserving "is_demo", i.e., when building each graph dict assign
"id" from the full/original graph name variable (not the stripped display name)
so runtime matches the docstring.

"""
user_graphs = await db.list_graphs()

# Only include graphs that start with user_id + '_', and strip the prefix
filtered_graphs = [
graph[len(f"{user_id}_") :]
for graph in user_graphs
if graph.startswith(f"{user_id}_")
]
result: list[dict] = []

# User-owned graphs: strip the user_id prefix for display
for graph in user_graphs:
if graph.startswith(f"{user_id}_"):
display_name = graph[len(f"{user_id}_"):]
result.append({"id": display_name, "name": display_name, "is_demo": False})

# Demo/shared graphs: strip the general prefix for display
if general_prefix:
demo_graphs = [
graph for graph in user_graphs if graph.startswith(general_prefix)
]
filtered_graphs = filtered_graphs + demo_graphs
for graph in user_graphs:
if graph.startswith(general_prefix):
display_name = graph[len(general_prefix):]
result.append({"id": graph, "name": display_name, "is_demo": True})

return filtered_graphs
return result
4 changes: 2 additions & 2 deletions app/src/components/chat/ChatInterface.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -522,8 +522,8 @@ const ChatInterface = ({
{/* Bottom Section with Suggestions and Input */}
<div className="border-t border-border bg-background">
<div className="p-6">
{/* Suggestion Cards - Only show for DEMO_CRM database */}
{(selectedGraph?.id === 'DEMO_CRM' || selectedGraph?.name === 'DEMO_CRM') && (
{/* Suggestion Cards - Only show for demo databases */}
{selectedGraph?.is_demo && (
Comment thread
galshubeli marked this conversation as resolved.
Comment thread
galshubeli marked this conversation as resolved.
<SuggestionCards
suggestions={suggestions}
onSelect={handleSuggestionSelect}
Expand Down
2 changes: 1 addition & 1 deletion app/src/components/schema/SchemaViewer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -287,7 +287,7 @@ const SchemaViewer = ({ isOpen, onClose, onWidthChange, sidebarWidth = 64 }: Sch
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText(
node.displayName[1],
schemaNode.name,
node.x || 0,
(node.y || 0) - nodeHeight / 2 + headerHeight / 2 + padding / 2
);
Expand Down
13 changes: 5 additions & 8 deletions app/src/pages/Index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -177,12 +177,9 @@ const Index = () => {
}
};

const handleDeleteGraph = async (graphId: string, graphName: string, event: React.MouseEvent) => {
const handleDeleteGraph = async (graphId: string, graphName: string, isDemo: boolean, event: React.MouseEvent) => {
event.stopPropagation(); // Prevent dropdown from closing/selecting

// Check if this is a demo database
const isDemo = graphId.startsWith('general_');


if (isRefreshingSchema) return;
// Show the delete confirmation modal
setDatabaseToDelete({ id: graphId, name: graphName, isDemo });
Expand Down Expand Up @@ -532,7 +529,7 @@ const Index = () => {
<p className="text-xs text-muted-foreground">Graph-Powered Text-to-SQL</p>
{selectedGraph ? (
<Badge variant="default" className="bg-green-600 hover:bg-green-700 text-xs px-2 py-0.5 flex-shrink-0">
{selectedGraph.name === 'DEMO_CRM' ? 'CRM' : selectedGraph.name}
{selectedGraph.name}
</Badge>
) : (
<Badge variant="secondary" className="bg-yellow-600 hover:bg-yellow-700 text-xs px-2 py-0.5 flex-shrink-0">
Expand Down Expand Up @@ -570,7 +567,7 @@ const Index = () => {
</DropdownMenuTrigger>
<DropdownMenuContent className="bg-card border-border text-foreground">
{graphs.map((graph) => {
const isDemo = graph.id.startsWith('general_');
const isDemo = graph.is_demo || false;
return (
<DropdownMenuItem
key={graph.id}
Expand All @@ -586,7 +583,7 @@ const Index = () => {
className={`h-6 w-6 p-0 opacity-0 group-hover:opacity-100 transition-opacity ${
isDemo || isRefreshingSchema || isChatProcessing ? 'cursor-not-allowed opacity-40' : 'hover:bg-red-600 hover:text-white'
}`}
onClick={(e) => { if (isDemo || isRefreshingSchema || isChatProcessing) return; handleDeleteGraph(graph.id, graph.name, e); }}
onClick={(e) => { if (isDemo || isRefreshingSchema || isChatProcessing) return; handleDeleteGraph(graph.id, graph.name, isDemo, e); }}
disabled={isDemo || isRefreshingSchema}
title={isDemo ? 'Demo databases cannot be deleted' : (isRefreshingSchema ? 'Refreshing schema...' : `Delete ${graph.name}`)}
data-testid={`delete-graph-btn-${graph.id}`}
Expand Down
14 changes: 6 additions & 8 deletions app/src/services/database.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,21 +37,19 @@ export class DatabaseService {
const data = await response.json();
console.log('Graphs data received:', data);

// Backend returns array of strings like ["northwind", "chinook"]
// Transform to Graph objects
// Backend returns array of objects with id, name, is_demo
const graphNames = data.graphs || data || [];

if (Array.isArray(graphNames) && graphNames.length > 0 && typeof graphNames[0] === 'string') {
// Transform string array to Graph objects
// Legacy fallback: transform string array to Graph objects
return graphNames.map((name: string) => ({
id: name,
name: name,
created_at: new Date().toISOString(),
updated_at: new Date().toISOString(),
is_demo: false,
}));
}
// If already objects, return as is

// Already objects from the backend
return graphNames;
Comment thread
galshubeli marked this conversation as resolved.
} catch (error) {
// Backend not available - return empty array for demo mode
Expand Down
5 changes: 3 additions & 2 deletions app/src/types/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,9 @@ export interface Graph {
id: string;
name: string;
description?: string;
created_at: string;
updated_at: string;
created_at?: string;
updated_at?: string;
is_demo: boolean;
table_count?: number;
schema?: any;
}
Expand Down
8 changes: 7 additions & 1 deletion e2e/logic/api/apiCalls.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import type {
LoginResponse,
SignupResponse,
LogoutResponse,
GraphListItem,
GraphsListResponse,
GraphDataResponse,
GraphUploadResponse,
Expand Down Expand Up @@ -156,7 +157,12 @@ export default class ApiCalls {
undefined,
this.defaultRequestContext
);
return await response.json();
const data = await response.json();
// Backend now returns objects with {id, name, is_demo}; extract ids for backward compat
if (Array.isArray(data) && data.length > 0 && typeof data[0] === 'object') {
return (data as GraphListItem[]).map((g) => g.id);
}
return data;
Comment on lines +160 to +165
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Ensure getGraphs() always returns an array shape.

Line 165 returns data as-is; if payload shape drifts (object wrapper, error body, etc.), this breaks the GraphsListResponse contract and can destabilize tests expecting array methods.

💡 Safer shape-guard
-      const data = await response.json();
-      // Backend now returns objects with {id, name, is_demo}; extract ids for backward compat
-      if (Array.isArray(data) && data.length > 0 && typeof data[0] === 'object') {
-        return (data as GraphListItem[]).map((g) => g.id);
-      }
-      return data;
+      const data: unknown = await response.json();
+      if (!Array.isArray(data)) {
+        throw new Error('Unexpected /graphs response shape');
+      }
+      if (data.length === 0) return [];
+      if (typeof data[0] === 'string') return data as GraphsListResponse;
+      if (typeof data[0] === 'object' && data[0] !== null) {
+        return (data as GraphListItem[]).map((g) => g.id);
+      }
+      throw new Error('Unexpected /graphs item type');
📝 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 data = await response.json();
// Backend now returns objects with {id, name, is_demo}; extract ids for backward compat
if (Array.isArray(data) && data.length > 0 && typeof data[0] === 'object') {
return (data as GraphListItem[]).map((g) => g.id);
}
return data;
const data: unknown = await response.json();
if (!Array.isArray(data)) {
throw new Error('Unexpected /graphs response shape');
}
if (data.length === 0) return [];
if (typeof data[0] === 'string') return data as GraphsListResponse;
if (typeof data[0] === 'object' && data[0] !== null) {
return (data as GraphListItem[]).map((g) => g.id);
}
throw new Error('Unexpected /graphs item type');
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@e2e/logic/api/apiCalls.ts` around lines 160 - 165, getGraphs must always
return an array shape; instead of returning data as-is, coerce/normalize the
parsed response into an array of ids. After parsing response.json() in
getGraphs, handle these cases: if Array -> map GraphListItem to id; else if
object with an "id" property -> return [data.id]; else if object with an "items"
array -> map items to ids; otherwise return an empty array. Update the branch
around the GraphListItem check to normalize all non-array payloads into a safe
array return so GraphsListResponse contract is preserved.

} catch (error) {
throw new Error(
`Failed to get graphs. \n Error: ${(error as Error).message}`
Expand Down
6 changes: 6 additions & 0 deletions e2e/logic/api/apiResponses.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,12 @@ export interface LogoutResponse {

// ==================== GRAPH/DATABASE RESPONSES ====================

export interface GraphListItem {
id: string;
name: string;
is_demo: boolean;
}

export type GraphsListResponse = string[];

export interface GraphColumn {
Expand Down
Loading