Skip to content

Staging#95

Merged
gkorland merged 31 commits intomainfrom
staging
Aug 21, 2025
Merged

Staging#95
gkorland merged 31 commits intomainfrom
staging

Conversation

@gkorland
Copy link
Contributor

@gkorland gkorland commented Aug 21, 2025

Summary by CodeRabbit

  • New Features

    • Landing demo now types SQL with a visible success badge.
    • Chat entry point at /chat with updated Google/GitHub sign-in flow.
    • Schema graph can be loaded on selection with clearer validation and errors.
  • Bug Fixes

    • Hardened static file handling (directory access returns 403).
  • Documentation

    • Guides updated for FastAPI/Uvicorn startup and developer workflow.
  • Chores

    • Backend migrated to FastAPI; dev tooling and dependencies upgraded; Python requirement raised to 3.12; Dependabot targets staging.

@vercel
Copy link

vercel bot commented Aug 21, 2025

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Preview Comments Updated (UTC)
queryweaver Ready Ready Preview Comment Aug 21, 2025 6:08pm

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Aug 21, 2025

Walkthrough

Migrates backend from Flask to FastAPI (ASGI), rewrites OAuth/auth flows and route handlers to FastAPI routers/middleware, replaces MySQL driver with PyMySQL, updates templates/static path resolutions, enhances landing UI and typing demo, modernizes frontend TypeScript imports and graph loading, updates dependencies and CI/Makefile, adjusts tests, and sets Dependabot target branch to staging.

Changes

Cohort / File(s) Summary
Backend framework migration (Flask → FastAPI)
api/app_factory.py, api/index.py, Makefile, Pipfile, .github/copilot-instructions.md
Replace Flask app factory and server startup with FastAPI, add ASGI middlewares (session, security, template globals), mount static files, switch dev/prod run to Uvicorn, update dependencies and docs to FastAPI stack.
Auth & OAuth rework
api/routes/auth.py, api/auth/oauth_handlers.py, api/auth/user_management.py
Replace Flask-Dance flows with AuthLib + FastAPI patterns: async APIRouter handlers, init_auth(app), OAuth clients in app.state, async validate/cache user, token refresh logic, and FastAPI-style error handling and session usage.
Routes migration & API models
api/routes/__init__.py, api/routes/database.py, api/routes/graphs.py
Convert blueprints → APIRouters, handlers → async, add Pydantic request models, use request.state.user_id, return JSONResponse/StreamingResponse and raise HTTPException for errors; streaming query/confirm endpoints added.
MySQL driver swap
api/loaders/mysql_loader.py, tests/test_mysql_loader.py
Swap mysql-connector-python → PyMySQL with DictCursor, update connection/cursor usage and exception handling; tests now patch pymysql.connect.
Templates & static path fixes + landing UI
app/templates/base.j2, app/templates/landing.j2, app/templates/components/*, app/public/css/landing.css, app/templates/components/scripts.j2
Change static URL param (filename→path) or use absolute /static paths, update landing markup/JS to type SQL into pre and show demo-success badge, add CSS for hero/demo/button styling and caret animation.
Frontend TypeScript updates
app/ts/app.ts, app/ts/modules/*
Remove .js extensions from imports, add loadAndShowGraph helper to fetch/validate graph data and render via showGraph; minor dynamic import adjustments.
Node devDependencies bump
app/package.json
Bump devDependencies (esbuild, typescript, eslint, @typescript-eslint/*) to newer versions.
Dependabot target branch
.github/dependabot.yml
Add target-branch: "staging" to pip (/) and npm (/app) update entries.
Tests & CI changes
tests/conftest.py, tests/e2e/pages/home_page.py, tests/e2e/test_api_endpoints.py, tests/test_simple_integration.py, .github/workflows/e2e-tests.yml
Use Uvicorn to start FastAPI in test fixtures with readiness probe, update E2E workflow to use Makefile targets and Node setup, adjust tests to expect 403 for /static/* and navigate to /chat where relevant.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  participant C as Client
  participant A as FastAPI App
  participant O as Authlib OAuth
  participant S as Session (Cookie)
  participant T as Templates

  C->>A: GET /
  A->>S: Check request.session.user_info
  alt Unauthenticated
    A->>T: Render landing.j2 (typing demo)
    T-->>C: Landing page
  else Authenticated
    A->>T: Render chat.j2
    T-->>C: Chat page
  end

  C->>A: GET /login/google
  A->>O: Build auth redirect (callback URL)
  O-->>C: Redirect to provider

  C->>A: GET /login/google/authorized?code=...
  A->>O: Exchange code for token & userinfo
  A->>S: Store token + user_info
  A-->>C: Redirect /chat
Loading
sequenceDiagram
  autonumber
  participant C as Client
  participant A as FastAPI (graphs_router)
  participant L as Loader (DB)
  participant AI as LLM/Agents

  C->>A: POST /graphs/:id/query (ChatRequest)
  A->>AI: Start relevancy & SQL gen (streamed)
  par Table discovery
    A->>L: Inspect schema (timeout)
    L-->>A: Tables/metadata
  end
  A->>AI: Generate SQL
  A-->>C: Stream JSON chunks (steps)
  alt Needs confirmation
    C->>A: POST /graphs/:id/confirm (ConfirmRequest)
    alt CONFIRM
      A->>L: Execute SQL
      L-->>A: Results
      A->>AI: Summarize
      A-->>C: Stream results + summary
    else Cancel
      A-->>C: Stream operation_cancelled
    end
  end
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Possibly related PRs

Suggested reviewers

  • galshubeli

Poem

Hop, hop! I swapped the flask for speed,
Fast lanes of FastAPI now lead.
OAuth carrots neatly stored,
Streaming queries never bored.
Graphs now dance, the schema sings—
On staging’s branch, Dependabot brings. 🥕🚀

Tip

🔌 Remote MCP (Model Context Protocol) integration is now available!

Pro plan users can now connect to remote MCP servers from the Integrations page. Connect with popular remote MCPs such as Notion and Linear to add more context to your reviews and chats.

✨ Finishing Touches
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch staging

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • 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.
  • 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 the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR/Issue comments)

Type @coderabbitai help to get the list of available commands.

Other keywords and placeholders

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

CodeRabbit Configuration File (.coderabbit.yaml)

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

Status, Documentation and Community

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

@github-actions
Copy link

github-actions bot commented Aug 21, 2025

Dependency Review

The following issues were found:

  • ✅ 0 vulnerable package(s)
  • ✅ 0 package(s) with incompatible licenses
  • ✅ 0 package(s) with invalid SPDX license definitions
  • ⚠️ 1 package(s) with unknown licenses.
  • ⚠️ 7 packages with OpenSSF Scorecard issues.

View full job summary

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: 2

🧹 Nitpick comments (7)
app/public/css/landing.css (1)

36-38: Use a themable success color and add reduced-motion fallback

  • Hard-coding #10B981 breaks theme consistency (you’re using CSS vars elsewhere). Prefer a theme var with a fallback.
  • Consider respecting user reduced-motion preferences for the typing/caret UI.

Apply:

-.demo-success{display:flex;align-items:center;gap:0.5rem;color:#10B981;font-size:0.9rem;font-weight:600}
+.demo-success{display:flex;align-items:center;gap:0.5rem;color:var(--success, #10B981);font-size:0.9rem;font-weight:600}

Additionally (outside changed lines), consider:

@media (prefers-reduced-motion: reduce){
  .demo-sql.typing::after{ animation: none; }
}
app/templates/landing.j2 (6)

29-40: Use semantics for the success badge and avoid inline styles

  • Replace inline style="display:none" toggling with the semantic hidden attribute or a CSS class.
  • Expose completion state to assistive tech via role="status" aria-live="polite".

Apply:

-      <div class="demo-sql-header">
+      <div class="demo-sql-header">
         <div class="demo-label">Generated SQL:</div>
-        <div class="demo-success" style="display:none;">
+        <div class="demo-success" role="status" aria-live="polite" hidden>
           <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
             <circle cx="8" cy="8" r="8" fill="#10B981"/>
             <path d="M5.5 8L7 9.5L10.5 6" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
           </svg>
           <span>Success</span>
         </div>
       </div>

Update JS toggles accordingly in the comments below.


66-66: Guard exists; consider caching nullability with a data-attribute for testability

You already guard successEl. If you want easier integration/UI tests, give the element a data-testid="demo-success"; no functional change, just improves E2E selectors.


78-78: Prefer attribute/class toggling to inline styles for success state

Use hidden to show/hide, which plays nicely with CSS and SSR, and is terser in JS.

Apply:

-          if(successEl) successEl.style.display = 'flex';
+          if(successEl) successEl.hidden = false;

83-85: Stop previous typing before early returns; toggle success via hidden

If sEl were ever missing, the early return would skip clearing an existing timer. Move the timer clear before the sEl null-guard and switch to hidden toggling.

Apply:

-        function typeSql(text){
-          if(!sEl) return;
-          // hide success while typing
-          if(successEl) successEl.style.display = 'none';
-          // stop any previous typing
-          if(typingTimer) {
-            clearInterval(typingTimer);
-            typingTimer = null;
-          }
+        function typeSql(text){
+          // stop any previous typing
+          if(typingTimer) {
+            clearInterval(typingTimer);
+            typingTimer = null;
+          }
+          if(!sEl) return;
+          // hide success while typing
+          if(successEl) successEl.hidden = true;

100-102: Show success using hidden = false; optionally delay to avoid blink on short texts

For very short SQL, the badge may flicker. A tiny delay can smooth this, but not required.

Apply:

-              // show success when typing completes
-              if(successEl) successEl.style.display = 'flex';
+              // show success when typing completes
+              if(successEl) successEl.hidden = false;
+              // Optional: small delay
+              // setTimeout(() => successEl && (successEl.hidden = false), 50);

116-119: Respect prefers-reduced-motion for the initial render

When users prefer reduced motion, render instantly instead of typing.

Apply:

-        // initial render: use typing animation for first example too
-        const firstExample = examples[0];
-        if(qEl) qEl.textContent = firstExample.q;
-        typeSql(firstExample.sql);
+        // initial render: prefer reduced motion
+        const firstExample = examples[0];
+        if(qEl) qEl.textContent = firstExample.q;
+        if (window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches) {
+          renderFull(0);
+        } else {
+          typeSql(firstExample.sql);
+        }

Optional: disable the “Try another question” button while typing to prevent rapid re-queues.

📜 Review details

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

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between b2e3459 and 9b2be20.

⛔ Files ignored due to path filters (1)
  • app/package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (4)
  • .github/dependabot.yml (1 hunks)
  • app/package.json (1 hunks)
  • app/public/css/landing.css (1 hunks)
  • app/templates/landing.j2 (5 hunks)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: e2e-tests
🔇 Additional comments (1)
app/public/css/landing.css (1)

42-42: Minor spacing tweak looks good

The reduced top margin on .demo-sql tightens the header/SQL grouping appropriately. No issues.

Comment on lines +10 to 16
target-branch: "staging"
schedule:
interval: "weekly"
- package-ecosystem: "npm"
directory: "/app"
target-branch: "staging"
schedule:
Copy link
Contributor

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

Targeting staging is sensible; verify branch protections and automations

Directing Dependabot PRs to staging is aligned with your flow. Ensure:

  • CI required checks are configured for staging.
  • Any auto-merge rules or Renovate-equivalent workflows are updated to act on staging.
  • Backports/merges from staging to main are scheduled to avoid security updates stalling on main.

Quick checks to confirm:


🏁 Script executed:

#!/bin/bash
# Ensure branch protection and CI for staging
gh api repos/:owner/:repo/branches/staging/protection || echo "No branch protection on staging"
rg -nP -C2 'on:\s*\n\s*push:.*branches:.*\[[^\]]*staging' -g '.github/workflows/*.yml' || echo "No workflows triggering on staging push"
rg -nP -C2 'on:\s*pull_request:.*branches:.*\[[^\]]*staging' -g '.github/workflows/*.yml' || echo "No PR workflows for staging"

Length of output: 982


🏁 Script executed:

#!/bin/bash
set -e

echo "Listing workflow directory contents:"
find .github/workflows -maxdepth 1 -type f

echo -e "\nFinding all YAML workflow files:"
find .github/workflows -type f \( -iname '*.yml' -o -iname '*.yaml' \)

echo -e "\nSearching for ‘staging’ references in workflow files:"
rg -n 'staging' .github/workflows || echo "No references to 'staging' found in workflows"

Length of output: 954


CI and Branch Protection for staging Are Missing – Action Required

Our scan shows:

  • The GitHub API query for branch protection on staging returned a 403, indicating it’s either unprotected or inaccessible by your integration.
  • A ripgrep search across all workflows in .github/workflows/ found no references to staging, so no CI runs or PR checks are configured for that branch.

Please address the following before merging Dependabot PRs into staging:

• Configure branch protection for staging (at minimum, require status checks).
• Update your workflows in .github/workflows/ to include both push and pull_request triggers on staging:

  • Add under on.push.branches: and on.pull_request.branches: the entry "staging".
    • Review any auto-merge or Renovate/Dependabot automations to ensure they target staging.
    • Put in place a clear process or scheduled job to backport security updates from staging into main so hotfixes don’t stall.

Once these are in place, your weekly Dependabot PRs to staging will be properly verified and merged—and any fixes will flow safely to main.

🤖 Prompt for AI Agents
.github/dependabot.yml lines 10-16: branch protection and CI for the staging
branch are missing; add/configure branch protection for "staging" (require
status checks and PR reviews) in the repo settings or via repo protection
config, update all workflows in .github/workflows/ to include "staging" under
both on.push.branches and on.pull_request.branches so CI runs for pushes and PRs
to staging, review Dependabot/auto-merge/Renovate settings to ensure their
target-branch is "staging" (not bypassing protections), and add a
documented/backport process or scheduled job to propagate security fixes from
staging into main.

Comment on lines +12 to +16
"esbuild": "^0.25.9",
"typescript": "^5.9.2",
"eslint": "^9.33.0",
"@typescript-eslint/parser": "^8.40.0",
"@typescript-eslint/eslint-plugin": "^8.40.0"
Copy link
Contributor

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

Major tooling upgrades (ESLint v9, TS v5.9, TSESLint v8): check config and engines before merging

  • ESLint 9 defaults to the flat config and drops legacy config loaders. If you're still on .eslintrc*, the lint script may no-op or fail silently (script already swallows failures via || true). Recommend adding an eslint.config.{js|mjs} at /app and removing || true once green.
  • @typescript-eslint v8 is aligned to ESLint 9 and TS ≥5.4; verify rule names haven’t changed and that parser/plugin versions match.
  • Consider pinning Node engine to avoid CI skew; these versions typically expect Node 18+ (prefer 20+).

Add a minimal flat config and Node engines:

{
   "name": "queryweaver-app",
   "version": "0.0.1",
   "private": true,
+  "engines": {
+    "node": ">=18.18.0"
+  },
   "scripts": {
     "build": "NODE_ENV=production esbuild ts/app.ts --bundle --outfile=./public/js/app.js --platform=browser --minify",
     "build:dev": "NODE_ENV=development esbuild ts/app.ts --bundle --outfile=./public/js/app.js --sourcemap --platform=browser",
     "watch": "esbuild ts/app.ts --bundle --outfile=./public/js/app.js --sourcemap --platform=browser --watch",
-    "lint": "eslint --ext .ts ./ts --max-warnings=0 || true"
+    "lint": "eslint --max-warnings=0"
   },

Example flat config at app/eslint.config.js:

// app/eslint.config.js
import tseslint from '@typescript-eslint/eslint-plugin'
import tsParser from '@typescript-eslint/parser'

export default [
  {
    files: ['ts/**/*.ts'],
    languageOptions: { parser: tsParser, parserOptions: { project: false } },
    plugins: { '@typescript-eslint': tseslint },
    rules: {
      '@typescript-eslint/no-unused-vars': ['warn', { argsIgnorePattern: '^_' }],
    },
  },
]

Run to confirm config presence and lintability:


🏁 Script executed:

#!/bin/bash
# Check for flat config and legacy configs
fd -H -a 'eslint.config.*' app || true
fd -H -a '.eslintrc*' . || true
# Try a no-file lint to ensure eslint resolves config
npx --yes eslint -v
npx --yes eslint app/ts --ext .ts --print-config app/ts/app.ts >/dev/null && echo "ESLint config OK"

Length of output: 257


🏁 Script executed:

#!/bin/bash
# Locate the package.json file
pkg=$(find . -maxdepth 2 -type f -name 'package.json' | head -n 1)
echo "Using package.json: $pkg"
jq '.scripts.lint, .engines' "$pkg"
# Check for flat ESLint config in root or subdirectories
fd -H -a 'eslint.config.*' . || true
# Check for any legacy .eslintrc* files
fd -H -a '.eslintrc*' . || true

Length of output: 371


Ensure ESLint v9 migration & Node engine pinning

It looks like you’re still on a legacy .eslintrc.cjs (found at the repo root) and haven’t yet added a flat config or pinned your Node version. Without these changes, your lint script will silently no-op (due to || true), and CI jobs may run on unexpected Node versions.

• Migrate to ESLint v9 flat config
– Create app/eslint.config.js (or .mjs) with at least a minimal rule set, then remove /home/jailuser/git/app/.eslintrc.cjs once migration is verified.
• Update the lint script in app/package.json
– Remove --ext .ts ./ts (flat config will pick up ts/ by default) and drop the || true so failures fail the CI.
• Pin your Node engine
– Add an engines field to app/package.json to ensure CI uses Node ≥18 (we recommend ≥20).
• Verify parser/plugin alignment
– Confirm that @typescript-eslint/parser and @typescript-eslint/eslint-plugin v8 options/rule names still match your existing config and TS 5.9 setup.

Suggested diff for app/package.json:

   "name": "queryweaver-app",
   "version": "0.0.1",
   "private": true,
+  "engines": {
+    "node": ">=18.18.0"
+  },
   "scripts": {
     "build": "NODE_ENV=production esbuild ts/app.ts --bundle --outfile=./public/js/app.js --platform=browser --minify",
     "build:dev": "NODE_ENV=development esbuild ts/app.ts --bundle --outfile=./public/js/app.js --sourcemap --platform=browser",
     "watch": "esbuild ts/app.ts --bundle --outfile=./public/js/app.js --sourcemap --platform=browser --watch",
-    "lint": "eslint --ext .ts ./ts --max-warnings=0 || true"
+    "lint": "eslint --max-warnings=0"
   },

And add at app/eslint.config.js:

import tsParser from '@typescript-eslint/parser'
import tsPlugin from '@typescript-eslint/eslint-plugin'

export default [
  {
    files: ['ts/**/*.ts'],
    languageOptions: { parser: tsParser, parserOptions: { project: false } },
    plugins: { '@typescript-eslint': tsPlugin },
    rules: {
      '@typescript-eslint/no-unused-vars': ['warn', { argsIgnorePattern: '^_' }],
    },
  },
]
🤖 Prompt for AI Agents
In app/package.json around lines 12-16, the project still relies on a legacy
.eslintrc.cjs and a lint script that silently succeeds; migrate to ESLint v9 by
adding a flat config file at app/eslint.config.js (or .mjs) with a minimal
ruleset that registers @typescript-eslint parser and plugin for ts/**/*.ts,
remove the repo root .eslintrc.cjs after verifying the flat config works, update
the package.json lint script to remove the explicit --ext .ts ./ts and drop the
trailing "|| true" so lint failures fail CI, add an "engines" field to
package.json to pin Node (e.g., ">=18" or ">=20"), and confirm that
@typescript-eslint/parser and @typescript-eslint/eslint-plugin v8 rule
names/options are compatible with TypeScript 5.9.

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

🧹 Nitpick comments (2)
app/templates/landing.j2 (2)

39-39: Announce SQL typing to screen readers

The SQL output changes dynamically; expose it as a live region so the typed text is announced without requiring focus.

-      <pre class="demo-sql"></pre>
+      <pre class="demo-sql" aria-live="polite" aria-atomic="true"></pre>

116-119: Respect reduced-motion preference on initial render

Offer an instant render path for users with “Reduce motion” enabled and keep typing for others. This also gives renderFull a clear purpose.

-        // initial render: use typing animation for first example too
-        const firstExample = examples[0];
-        if(qEl) qEl.textContent = firstExample.q;
-        typeSql(firstExample.sql);
+        // initial render: honor reduced-motion
+        const firstExample = examples[0];
+        if(qEl) qEl.textContent = firstExample.q;
+        const reduceMotion = window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches;
+        if (reduceMotion) {
+          renderFull(0);
+        } else {
+          typeSql(firstExample.sql);
+        }
📜 Review details

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

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 9b2be20 and 975a5dc.

📒 Files selected for processing (2)
  • app/public/css/landing.css (1 hunks)
  • app/templates/landing.j2 (6 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • app/public/css/landing.css
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: e2e-tests

<div class="hero-ctas">
<a href="{{ url_for('auth.chat') }}" class="btn-pill">Sign Up for Free</a>
<a href="{{ url_for('auth.login_google') }}" class="btn-ghost">Sign In</a>
<a href="{{ url_for('auth.chat') }}" class="btn-ghost">Sign In</a>
Copy link
Contributor

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

Confirm Sign In CTA route (auth.chat) is correct for returning users

Both CTAs now point to auth.chat. If auth.chat requires an authenticated session or leads to onboarding, returning users clicking “Sign In” may hit the wrong flow. Please verify there’s a dedicated login endpoint and wire this CTA to it.

Run this to locate available auth endpoints and usages:

If a dedicated login exists, consider:

-      <a href="{{ url_for('auth.chat') }}" class="btn-ghost">Sign In</a>
+      <a href="{{ url_for('auth.login') }}" class="btn-ghost">Sign In</a>

🏁 Script executed:

#!/bin/bash
# Inspect blueprint declarations and routes
rg -n -C3 -S "Blueprint\(['\"]auth['\"]" 
rg -n -C2 -S "def chat\(" 
rg -n -C2 -S "url_for\('auth\." 

# Look for canonical login routes we might prefer
rg -n -C2 -S "def login\(" 
rg -n -C2 -S "login_google" 

Length of output: 1310


Wire “Sign In” CTA to the dedicated login route
Currently both CTAs in app/templates/landing.j2 point at auth.chat (the chat UI, which in turn gates on authentication). We expose a login endpoint via auth.login_google (@auth_bp.route("/login")), so the “Sign In” button should target that directly to initiate the OAuth flow rather than sending unauthenticated users to /chat.

• File: app/templates/landing.j2 (line 19)

-      <a href="{{ url_for('auth.chat') }}" class="btn-ghost">Sign In</a>
+      <a href="{{ url_for('auth.login_google') }}" class="btn-ghost">Sign In</a>
🤖 Prompt for AI Agents
In app/templates/landing.j2 around line 19 the "Sign In" anchor targets
url_for('auth.chat') (the chat UI) instead of the dedicated login route; change
the href to url_for('auth.login_google') (the @auth_bp.route("/login") endpoint)
so the button initiates the OAuth/login flow directly for unauthenticated users.

Comment on lines +29 to +37
<div class="demo-sql-header">
<div class="demo-label">Generated SQL:</div>
<div class="demo-success" style="display:none;">
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="8" cy="8" r="8" fill="#10B981"/>
<path d="M5.5 8L7 9.5L10.5 6" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
<span>Success</span>
</div>
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Make the success badge accessible and avoid inline styles

Use semantic visibility with hidden and announce status changes to assistive tech via role="status" and aria-live. This also removes inline styles.

-      <div class="demo-sql-header">
+      <div class="demo-sql-header">
         <div class="demo-label">Generated SQL:</div>
-        <div class="demo-success" style="display:none;">
+        <div class="demo-success" hidden role="status" aria-live="polite" aria-atomic="true">
           <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
             <circle cx="8" cy="8" r="8" fill="#10B981"/>
             <path d="M5.5 8L7 9.5L10.5 6" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
           </svg>
           <span>Success</span>
         </div>
       </div>
📝 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
<div class="demo-sql-header">
<div class="demo-label">Generated SQL:</div>
<div class="demo-success" style="display:none;">
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="8" cy="8" r="8" fill="#10B981"/>
<path d="M5.5 8L7 9.5L10.5 6" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
<span>Success</span>
</div>
<div class="demo-sql-header">
<div class="demo-label">Generated SQL:</div>
<div class="demo-success" hidden role="status" aria-live="polite" aria-atomic="true">
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="8" cy="8" r="8" fill="#10B981"/>
<path d="M5.5 8L7 9.5L10.5 6" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
<span>Success</span>
</div>
</div>
🤖 Prompt for AI Agents
In app/templates/landing.j2 around lines 29 to 37, the success badge uses an
inline style to hide it and lacks ARIA roles; replace the inline
style="display:none;" with the semantic hidden attribute (or remove the
attribute to show), add role="status" and aria-live="polite" on the success
container so screen readers are notified when it becomes visible, and ensure the
SVG has aria-hidden="true" (or an accessible title) to avoid redundant
announcements; update any JS that toggles visibility to set/remove the hidden
attribute rather than manipulating inline styles.

let idx = 0;
const qEl = document.querySelector('.demo-question');
const sEl = document.querySelector('.demo-sql');
const successEl = document.querySelector('.demo-success');
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Replace inline display toggles with the hidden property and set aria-busy during typing

Using hidden is semantically correct and keeps styling in CSS. Toggling aria-busy helps AT know content is updating.

-        const successEl = document.querySelector('.demo-success');
+        const successEl = document.querySelector('.demo-success');

@@
-          if(successEl) successEl.style.display = 'flex';
+          if(successEl) successEl.hidden = false;

@@
-          // hide success while typing
-          if(successEl) successEl.style.display = 'none';
+          // hide success while typing
+          if(successEl) successEl.hidden = true;
+          // mark region busy for AT
+          sEl.setAttribute('aria-busy', 'true');

@@
-              // show success when typing completes
-              if(successEl) successEl.style.display = 'flex';
+              // show success when typing completes
+              if(successEl) successEl.hidden = false;
+              sEl.removeAttribute('aria-busy');

Also applies to: 79-79, 83-85, 100-101


return Response(stream_with_context(generate()), content_type="application/json")
return StreamingResponse(generate(), media_type="application/json")

Check warning

Code scanning / CodeQL

Information exposure through an exception Medium

Stack trace information
flows to this location and may be exposed to an external user.
Stack trace information
flows to this location and may be exposed to an external user.
Stack trace information
flows to this location and may be exposed to an external user.
Stack trace information
flows to this location and may be exposed to an external user.
Stack trace information
flows to this location and may be exposed to an external user.

Copilot Autofix

AI 6 months ago

To fix the problem, we need to ensure that detailed exception messages (including those from database drivers) are not sent to the user in API responses. Instead, we should log the detailed error on the server and return a generic error message to the user. Specifically:

  • In api/loaders/mysql_loader.py, update the load method so that when an exception occurs, the detailed error is logged, but the returned message is a generic one (e.g., "MySQL connection error" or "Error loading MySQL schema").
  • Similarly, in the refresh_graph_schema method, ensure that only generic error messages are returned to the user, while the detailed error is logged.
  • In api/routes/graphs.py, no changes are needed to the error handling logic, as it will now receive only generic error messages from the loader.

This approach preserves the current functionality (the API still indicates failure and success), but prevents sensitive information from being exposed to the user.


Suggested changeset 1
api/loaders/mysql_loader.py
Outside changed files

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/api/loaders/mysql_loader.py b/api/loaders/mysql_loader.py
--- a/api/loaders/mysql_loader.py
+++ b/api/loaders/mysql_loader.py
@@ -162,10 +162,11 @@
                          f"Found {len(entities)} tables.")
 
         except pymysql.MySQLError as e:
-            return False, f"MySQL connection error: {str(e)}"
+            logging.error("MySQL connection error: %s", str(e))
+            return False, "MySQL connection error"
         except Exception as e:
-            return False, f"Error loading MySQL schema: {str(e)}"
-
+            logging.error("Error loading MySQL schema: %s", str(e))
+            return False, "Error loading MySQL schema"
     @staticmethod
     def extract_tables_info(cursor, db_name: str) -> Dict[str, Any]:
         """
@@ -445,10 +445,7 @@
         except Exception as e:
             # Log the error and return failure
             logging.error("Error refreshing graph schema: %s", str(e))
-            error_msg = "Error refreshing graph schema"
-            logging.error(error_msg)
-            return False, error_msg
-
+            return False, "Error refreshing graph schema"
     @staticmethod
     def execute_sql_query(sql_query: str, db_url: str) -> List[Dict[str, Any]]:
         """
EOF
@@ -162,10 +162,11 @@
f"Found {len(entities)} tables.")

except pymysql.MySQLError as e:
return False, f"MySQL connection error: {str(e)}"
logging.error("MySQL connection error: %s", str(e))
return False, "MySQL connection error"
except Exception as e:
return False, f"Error loading MySQL schema: {str(e)}"

logging.error("Error loading MySQL schema: %s", str(e))
return False, "Error loading MySQL schema"
@staticmethod
def extract_tables_info(cursor, db_name: str) -> Dict[str, Any]:
"""
@@ -445,10 +445,7 @@
except Exception as e:
# Log the error and return failure
logging.error("Error refreshing graph schema: %s", str(e))
error_msg = "Error refreshing graph schema"
logging.error(error_msg)
return False, error_msg

return False, "Error refreshing graph schema"
@staticmethod
def execute_sql_query(sql_query: str, db_url: str) -> List[Dict[str, Any]]:
"""
Copilot is powered by AI and may make mistakes. Always verify output.
Unable to commit as this autofix suggestion is now outdated

return Response(stream_with_context(generate_confirmation()), content_type="application/json")
return StreamingResponse(generate_confirmation(), media_type="application/json")

Check warning

Code scanning / CodeQL

Information exposure through an exception Medium

Stack trace information
flows to this location and may be exposed to an external user.
Stack trace information
flows to this location and may be exposed to an external user.
Stack trace information
flows to this location and may be exposed to an external user.
Stack trace information
flows to this location and may be exposed to an external user.

Copilot Autofix

AI 6 months ago

To fix the problem, we need to ensure that any error messages returned from the loader methods (such as MySQLLoader.load and MySQLLoader.refresh_graph_schema) are not directly exposed to the user. Instead, only a generic error message should be sent in the API response, while the detailed error (including the original exception message) should be logged on the server.

Best approach:

  • In api/routes/graphs.py, within the confirm_destructive_operation route, when yielding an error message to the user (specifically in the "schema_refresh" block when refresh_success is False), replace the use of refresh_message with a generic error message.
  • Log the detailed refresh_message on the server using the logging module.
  • No changes are needed in the loader itself, as the loader should still return detailed messages for logging purposes.

Files/regions to change:

  • In api/routes/graphs.py, inside the generate_confirmation function of confirm_destructive_operation, update the error response in the "schema_refresh" block to not expose refresh_message to the user, and log it instead.

What is needed:

  • Use the logging module to log the detailed error.
  • Replace the user-facing error message with a generic one.

Suggested changeset 1
api/routes/graphs.py

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/api/routes/graphs.py b/api/routes/graphs.py
--- a/api/routes/graphs.py
+++ b/api/routes/graphs.py
@@ -573,11 +573,11 @@
                             }
                         ) + MESSAGE_DELIMITER
                     else:
+                        logging.error("Schema was modified but graph refresh failed: %s", refresh_message)
                         yield json.dumps(
                             {
                                 "type": "schema_refresh",
-                                "message": (f"⚠️ Schema was modified but graph refresh failed: "
-                                          f"{refresh_message}"),
+                                "message": "⚠️ Schema was modified but graph refresh failed due to an internal error.",
                                 "refresh_status": "failed"
                             }
                         ) + MESSAGE_DELIMITER
EOF
@@ -573,11 +573,11 @@
}
) + MESSAGE_DELIMITER
else:
logging.error("Schema was modified but graph refresh failed: %s", refresh_message)
yield json.dumps(
{
"type": "schema_refresh",
"message": (f"⚠️ Schema was modified but graph refresh failed: "
f"{refresh_message}"),
"message": "⚠️ Schema was modified but graph refresh failed due to an internal error.",
"refresh_status": "failed"
}
) + MESSAGE_DELIMITER
Copilot is powered by AI and may make mistakes. Always verify output.
Unable to commit as this autofix suggestion is now outdated
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: 10

Caution

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

⚠️ Outside diff range comments (1)
api/loaders/mysql_loader.py (1)

141-156: Ensure connection/cursor are closed on all failure paths (wrap with try/finally).

If any exception occurs after connecting (e.g., while extracting entities/relationships), the current code returns early and skips closing the connection/cursor. Wrap connection/cursor usage in try/finally or use context managers to guarantee cleanup.

Here’s a minimal refactor confined to this block:

-            conn = pymysql.connect(**conn_params)
-            cursor = conn.cursor(DictCursor)
+            conn = pymysql.connect(**conn_params)
+            try:
+                cursor = conn.cursor(DictCursor)
+                try:
                   # Get database name
                   db_name = conn_params['database']
                   # Get all table information
                   entities = MySQLLoader.extract_tables_info(cursor, db_name)
                   # Get all relationship information
                   relationships = MySQLLoader.extract_relationships(cursor, db_name)
-            # Close database connection
-            cursor.close()
-            conn.close()
+                finally:
+                    cursor.close()
+            finally:
+                conn.close()
♻️ Duplicate comments (3)
app/templates/landing.j2 (3)

19-19: Wire “Sign In” to the login route (not the chat UI).

This mirrors the earlier review: the “Sign In” CTA should initiate OAuth, not send unauthenticated users to /chat. Given your current route names, point it to google.login (or to a generic auth.login if you add that name on /login).

Apply:

-      <a href="{{ url_for('auth.chat') }}" class="btn-ghost">Sign In</a>
+      <a href="{{ url_for('google.login') }}" class="btn-ghost">Sign In</a>

29-37: Make the success badge accessible; avoid inline styles.

Replace display:none with the semantic hidden attribute and add ARIA. Also mark the SVG decorative.

Apply:

-        <div class="demo-success" style="display:none;">
-          <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
+        <div class="demo-success" hidden role="status" aria-live="polite" aria-atomic="true">
+          <svg width="16" height="16" viewBox="0 0 16 16" fill="none" aria-hidden="true" xmlns="http://www.w3.org/2000/svg">
             <circle cx="8" cy="8" r="8" fill="#10B981"/>
             <path d="M5.5 8L7 9.5L10.5 6" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
           </svg>
           <span>Success</span>
         </div>

65-65: Use the hidden property and aria-busy for better semantics instead of inline styles.

Toggle success visibility with hidden and indicate typing state with aria-busy for assistive tech.

Apply:

-        const successEl = document.querySelector('.demo-success');
+        const successEl = document.querySelector('.demo-success');

@@
-          if(successEl) successEl.style.display = 'flex';
+          if(successEl) successEl.hidden = false;

@@
-          // hide success while typing
-          if(successEl) successEl.style.display = 'none';
+          // hide success while typing
+          if(successEl) successEl.hidden = true;
+          sEl.setAttribute('aria-busy', 'true');

@@
-              // show success when typing completes
-              if(successEl) successEl.style.display = 'flex';
+              // show success when typing completes
+              if(successEl) successEl.hidden = false;
+              sEl.removeAttribute('aria-busy');

Also applies to: 78-79, 83-85, 100-101

🧹 Nitpick comments (35)
app/templates/components/chat_header.j2 (1)

3-3: Consistent migration to path= for static assets — good.

These changes match the FastAPI/Starlette static routing convention. Keep this consistent repo-wide. If you plan to serve under a non-root prefix (e.g., /app), this will continue to work, provided the router mount name remains static.

If you want to further harden UX:

  • Provide explicit dimensions on icons (reduces CLS).
  • Consider fetchpriority="low" on non-critical decorative icons.

No need to block this PR for these.

Also applies to: 23-23, 27-27

app/templates/components/scripts.j2 (1)

17-17: Prefer url_for over hard-coded “/static” path to respect mounts, prefixes, and CDN rewrites.

Hard-coding /static/js/app.js may break when the app is:

  • served behind a reverse proxy with a path prefix,
  • mounted under a sub-application path, or
  • using a CDN or hashed asset pipeline.

Use url_for('static', path='js/app.js') for consistency with other templates.

Apply:

-<script type="module" src="/static/js/app.js"></script>
+<script type="module" src="{{ url_for('static', path='js/app.js') }}"></script>
app/templates/components/login_modal.j2 (1)

7-7: Static icon path migration looks good.

This is correct for FastAPI/Starlette. No functional concerns.

If you want micro-optimizations:

  • Add decoding="async" on these small SVGs to avoid any decode-blocking on slower devices, though impact is minimal.

Also applies to: 10-10

Makefile (2)

66-66: Dev server switch to Uvicorn is appropriate. Consider parameterizing host/port.

Great for local dev with --reload. To improve flexibility across environments (containers, devboxes), parameterize host/port via variables.

Apply this change to use Make variables:

-pipenv run uvicorn api.index:app --host 127.0.0.1 --port 5000 --reload
+pipenv run uvicorn api.index:app --host $(HOST) --port $(PORT) --reload

And define defaults near the top of the Makefile:

# Defaults (override via: make run-dev HOST=0.0.0.0 PORT=8000)
HOST ?= 127.0.0.1
PORT ?= 5000

69-69: Enhance production Uvicorn invocation
I verified that the ASGI app is exposed via app = create_app() in api/index.py and that uvicorn is declared in Pipfile.lock. The existing Makefile target works, but for deployments behind a proxy and to leverage multiple workers, consider the following optional hardening:

Proposed diff (in your Makefile):

--- Makefile
+++ Makefile
@@ –1,3 +1,6 @@
+# Default server settings
+HOST    ?= 127.0.0.1
+PORT    ?= 5000
+WORKERS ?= 1
 run-dev:
-	pipenv run uvicorn api.index:app --host 127.0.0.1 --port 5000
+	pipenv run uvicorn api.index:app \
+		--host $(HOST) \
+		--port $(PORT) \
+		--proxy-headers \
+		--workers $(WORKERS)
+
+run: run-dev  ## alias for starting the development server
  • --proxy-headers ensures correct client IP and scheme when behind Nginx/Ingress.
  • --workers $(WORKERS) lets you scale to multiple processes for higher concurrency.
  • Using HOST, PORT, and WORKERS variables makes it easy to override in different environments.
  • Adding a run alias aligns with common “run” workflow guidelines.

These changes are purely optional but recommended for production readiness.

Pipfile.new (1)

10-10: Optional: use uvicorn[standard] for better prod defaults.

The “standard” extras enable uvloop, httptools, etc. Consider aligning with the main Pipfile which uses uvicorn without extras.

- uvicorn = {extras = ["standard"], version = "~=0.32.0"}
+ uvicorn = "~=0.32.0"  # or keep extras if you prefer
app/templates/base.j2 (1)

19-21: Optional: add SRI for third-party scripts or self-host to reduce supply-chain risk.

For https://unpkg.com/d3 and force-graph, either add integrity/crossorigin or serve pinned versions from your static bucket.

api/loaders/mysql_loader.py (1)

164-164: Surface PyMySQL errors while still cleaning up; consider logging the original error.

Catching pymysql.MySQLError is right, but pair it with the above cleanup changes. Also include the DB name/host in the log (excluding credentials) to aid debugging.

tests/test_mysql_loader.py (1)

115-135: Consider adding tests for execute_sql_query (SELECT and DML) and rollback on errors.

Given the driver switch and the fixed DictCursor usage, add focused tests that validate:

  • SELECT returns list[dict] with serialized values
  • INSERT/UPDATE/DELETE return the status payload and commit is called
  • Rollback and cleanup on MySQLError

I can add a small test module stubbing cursor.description/rowcount paths if you want.

Pipfile (2)

8-8: Optional: consider uvicorn[standard] for production performance.

Not required, but adopting extras=[standard] includes uvloop/httptools for a measurable boost in prod.

- uvicorn = "~=0.32.0"
+ uvicorn = {extras = ["standard"], version = "~=0.32.0"}

7-19: Don’t add a direct starlette pin here; let FastAPI manage it.

FastAPI’s own dependency range will ensure a compatible (and, after the update, patched) Starlette gets installed. (fastapi.tiangolo.com)

api/auth/user_management.py (3)

8-11: Remove unused imports (Ruff F401).

requests and JSONResponse are imported but never used.

Apply:

-import requests
-from fastapi.responses import JSONResponse
 from authlib.integrations.starlette_client import OAuth

239-249: GitHub email retrieval: use profile email first, fall back to /user/emails.

A direct call to /user/emails requires user:email scope and may fail. If the profile already carries a public email, prefer that before making the extra API call.

-                    # Get primary email
-                    email_resp = await oauth.github.get("/user/emails", token=github_token)
-                    email = None
-                    if email_resp.status_code == 200:
-                        for email_obj in email_resp.json():
-                            if email_obj.get("primary", False):
-                                email = email_obj.get("email")
-                                break
-                        if not email and email_resp.json():
-                            email = email_resp.json()[0].get("email")
+                    # Get email: prefer profile email, fall back to /user/emails
+                    email = github_user.get("email")
+                    if not email:
+                        email_resp = await oauth.github.get("/user/emails", token=github_token)
+                        if email_resp.status_code == 200:
+                            for email_obj in email_resp.json():
+                                if email_obj.get("primary", False):
+                                    email = email_obj.get("email")
+                                    break
+                            if not email and email_resp.json():
+                                email = email_resp.json()[0].get("email")

Also applies to: 256-265


312-320: Preserve original exception context when converting to HTTP 401 (B904).

Raising HTTPException “from e” keeps cause in logs while FastAPI still returns sanitized JSON to clients.

-        except Exception as e:
+        except Exception as e:
             logging.error("Unexpected error in token_required: %s", e)
             request.session.pop("user_info", None)
-            raise HTTPException(
+            raise HTTPException(
                 status_code=status.HTTP_401_UNAUTHORIZED,
-                detail="Unauthorized - Authentication error"
-            )
+                detail="Unauthorized - Authentication error"
+            ) from e
api/routes/graphs.py (4)

8-12: Trim unused imports to quiet linters and reduce noise.

Dict, Any, status, and Form aren’t used in this module.

-from typing import Dict, Any
+from typing import Optional, List
-
-from fastapi import APIRouter, Request, HTTPException, status, UploadFile, File, Form
+from fastapi import APIRouter, Request, HTTPException, UploadFile, File
 from fastapi.responses import JSONResponse, StreamingResponse
-from pydantic import BaseModel
+from pydantic import BaseModel, Field

34-38: Avoid mutable defaults in Pydantic models; use Field(default_factory=...).

Mutable defaults can be shared across instances. Safer to use default_factory.

-class ChatRequest(BaseModel):
-    chat: list
-    result: list = None
-    instructions: str = None
+class ChatRequest(BaseModel):
+    chat: List[str]
+    result: Optional[List[str]] = None
+    instructions: Optional[str] = None
@@
-class ConfirmRequest(BaseModel):
-    sql_query: str
-    confirmation: str = ""
-    chat: list = []
+class ConfirmRequest(BaseModel):
+    sql_query: str
+    confirmation: str = ""
+    chat: List[str] = Field(default_factory=list)

Also applies to: 40-44


61-67: Broaden DB URL detection to support common SQLAlchemy-style MySQL URLs.

Support mysql+pymysql:// and mysql+mysqlconnector:// in addition to mysql://.

-    elif db_url_lower.startswith('mysql://'):
+    elif (
+        db_url_lower.startswith('mysql://')
+        or db_url_lower.startswith('mysql+pymysql://')
+        or db_url_lower.startswith('mysql+mysqlconnector://')
+    ):
         return 'mysql', MySQLLoader

504-505: Consider a streaming-friendly media type.

You’re delimiting multiple JSON objects with a custom boundary. application/x-ndjson or text/event-stream typically plays nicer with streaming parsers. If the frontend expects application/json, ignore this.

Would you like me to switch both StreamingResponse media_type values to application/x-ndjson and update the client accordingly?

Also applies to: 620-621

api/index.py (1)

12-18: Make host/port configurable via environment variables.

Keeps local default behavior but simplifies containerized and cloud deploys.

-    uvicorn.run(
-        "api.index:app",
-        host="127.0.0.1",
-        port=5000,
-        reload=debug_mode,
-        log_level="info" if debug_mode else "warning"
-    )
+    host = os.environ.get("HOST", "127.0.0.1")
+    port = int(os.environ.get("PORT", "5000"))
+    uvicorn.run(
+        "api.index:app",
+        host=host,
+        port=port,
+        reload=debug_mode,
+        log_level="info" if debug_mode else "warning",
+    )
api/routes/__init__.py (1)

1-1: Add a module docstring to satisfy Pylint (C0114).

Minor polish; no functional impact.

-# Routes module for text2sql API
+"""Expose FastAPI routers for the text2sql API."""
api/auth/oauth_handlers.py (3)

4-7: Prune unused imports and fix linter warnings.

time and requests are unused.

-import time
-from typing import Dict, Any
-
-import requests
+from typing import Dict, Any

14-21: Type return values and split long signature to satisfy Pylint; mark unused args.

  • Add return type to setup_oauth_handlers.
  • Break long function signatures onto multiple lines.
  • Prefix unused parameters with _ to address Pylint’s unused-argument.
-def setup_oauth_handlers(app: FastAPI, oauth: OAuth):
+def setup_oauth_handlers(app: FastAPI, oauth: OAuth) -> None:
@@
-    async def handle_google_callback(request: Request, token: Dict[str, Any], user_info: Dict[str, Any]):
+    async def handle_google_callback(
+        _request: Request,
+        _token: Dict[str, Any],
+        user_info: Dict[str, Any],
+    ) -> bool:
@@
-    async def handle_github_callback(request: Request, token: Dict[str, Any], user_info: Dict[str, Any]):
+    async def handle_github_callback(
+        _request: Request,
+        _token: Dict[str, Any],
+        user_info: Dict[str, Any],
+    ) -> bool:

38-41: Narrow exception handling where feasible; at least preserve cause.

Catching broad Exception is sometimes necessary in auth flows, but prefer preserving context with from e so logs keep the root cause.

-        except Exception as e:
-            logging.error("Error handling Google OAuth callback: %s", e)
-            return False
+        except Exception as e:
+            logging.error("Error handling Google OAuth callback: %s", e)
+            return False  # consider re-raising or returning details internally

@@
-        except Exception as e:
-            logging.error("Error handling GitHub OAuth callback: %s", e)
-            return False
+        except Exception as e:
+            logging.error("Error handling GitHub OAuth callback: %s", e)
+            return False

If desired, we can instead re-raise using custom exceptions consumed by the caller.

Also applies to: 60-63

api/routes/database.py (2)

3-8: Remove unused imports; add run_in_threadpool for blocking calls.

Clean up unused types/status and prepare for the next comment’s threadpool usage.

Apply:

-from typing import Dict, Any
+from typing import Dict, Any
@@
-from fastapi import APIRouter, Request, HTTPException, status
+from fastapi import APIRouter, Request, HTTPException
 from fastapi.responses import JSONResponse
 from pydantic import BaseModel
+from starlette.concurrency import run_in_threadpool

Note: Dict/Any appear unused; if so, also remove them:

-from typing import Dict, Any
+# no typing imports needed

47-47: Preserve exception context when re-raising.

Chain exceptions to distinguish handler failures from original errors.

Apply:

-                raise HTTPException(status_code=500, detail="Failed to connect to PostgreSQL database")
+                raise HTTPException(status_code=500, detail="Failed to connect to PostgreSQL database") from e
@@
-                raise HTTPException(status_code=500, detail="Failed to connect to MySQL database")
+                raise HTTPException(status_code=500, detail="Failed to connect to MySQL database") from e
@@
-        raise HTTPException(status_code=500, detail="Internal server error")
+        raise HTTPException(status_code=500, detail="Internal server error") from e

Also applies to: 56-56, 73-73

api/app_factory.py (5)

11-11: Remove unused imports to satisfy Ruff and keep things tidy.

Jinja2Templates and setup_oauth_handlers aren’t used here.

Apply:

-from fastapi.templating import Jinja2Templates
@@
-from api.auth.oauth_handlers import setup_oauth_handlers

Also applies to: 15-15


45-46: Wrap long FastAPI constructor line to satisfy Pylint and readability.

Break the long description into a multiline call.

Apply:

-    app = FastAPI(title="QueryWeaver", description="Text2SQL with Graph-Powered Schema Understanding")
+    app = FastAPI(
+        title="QueryWeaver",
+        description="Text2SQL with Graph-Powered Schema Understanding",
+    )

54-61: Make session cookie ‘secure’ configurable.

Defaulting https_only=False is fine for dev, but allow enabling via env in staging/prod.

Apply:

-        https_only=False,  # allow http on localhost in development
+        https_only=os.getenv("SESSION_COOKIE_SECURE", "false").lower() in ("1", "true", "yes"),

Optionally also set same_site from env for stricter deployments.


31-39: Minor: avoid custom 400 in static middleware; let StaticFiles handle it.

Raising 400 for paths ending with “/” changes semantics; StaticFiles returns 404. Consider logging and returning the next handler to keep behavior consistent.


79-87: Prefer typed handlers over string matching for OAuth errors.

String checks for “token”/“oauth” are brittle. Register a handler for AuthlibBaseError in auth.py (or import here) and keep this catch-all minimal.

Example:

from authlib.common.errors import AuthlibBaseError

@app.exception_handler(AuthlibBaseError)
async def handle_authlib_error(request: Request, exc: AuthlibBaseError):
    logging.warning("OAuth error occurred: %s", exc)
    request.session.clear()
    return RedirectResponse(url="/", status_code=302)
api/routes/auth.py (5)

89-92: Name the generic login endpoint for consistency with template url_for usage.

Add a route name (e.g., auth.login) so templates can use url_for('auth.login'). This pairs with the landing “Sign In” CTA change.

Apply:

-@auth_router.get("/login", response_class=RedirectResponse)
+@auth_router.get("/login", name="auth.login", response_class=RedirectResponse)
 async def login_page(_: Request) -> RedirectResponse:
     return RedirectResponse(url="/login/google", status_code=status.HTTP_302_FOUND)

99-106: Break long warning strings to satisfy Pylint and readability.

These warnings exceed the 100-char limit.

Apply:

-        logging.warning(
-            "OAUTH_BASE_URL not set and base URL is 127.0.0.1; "
-            "if your Google OAuth app uses 'http://localhost:5000', "
-            "set OAUTH_BASE_URL=http://localhost:5000 to avoid redirect_uri mismatch."
-        )
+        logging.warning(
+            "OAUTH_BASE_URL not set and base URL is 127.0.0.1; "
+            "if your Google OAuth app uses 'http://localhost:5000', "
+            "set OAUTH_BASE_URL=http://localhost:5000 "
+            "to avoid redirect_uri mismatch."
+        )
@@
-        logging.warning(
-            "OAUTH_BASE_URL not set and base URL is 127.0.0.1; "
-            "if your GitHub OAuth app uses 'http://localhost:5000', "
-            "set OAUTH_BASE_URL=http://localhost:5000 to avoid redirect_uri mismatch."
-        )
+        logging.warning(
+            "OAUTH_BASE_URL not set and base URL is 127.0.0.1; "
+            "if your GitHub OAuth app uses 'http://localhost:5000', "
+            "set OAUTH_BASE_URL=http://localhost:5000 "
+            "to avoid redirect_uri mismatch."
+        )

Also applies to: 160-165


233-257: Send Google token in form-encoded body, not as query params, for revocation endpoint.

Per Google’s spec, token should be in application/x-www-form-urlencoded body. Using params may still work but is non-canonical.

Apply:

-                        resp = await client.post(
-                            "https://oauth2.googleapis.com/revoke",
-                            params={"token": token},
-                            headers={"content-type": "application/x-www-form-urlencoded"},
-                        )
+                        resp = await client.post(
+                            "https://oauth2.googleapis.com/revoke",
+                            data={"token": token},
+                            headers={"content-type": "application/x-www-form-urlencoded"},
+                        )

35-39: Break long list literal across lines to satisfy Pylint limit.

The pop-list on Line 37 likely triggers the 100-char rule.

Apply:

-    for key in ["user_info", "google_token", "github_token", "token_validated_at", "oauth_google_auth"]:
+    for key in [
+        "user_info",
+        "google_token",
+        "github_token",
+        "token_validated_at",
+        "oauth_google_auth",
+    ]:
         session.pop(key, None)

94-98: Unify route naming under an “auth.” namespace for consistency.

You’ve named chat as auth.chat, but login routes are google.login/github.login. Consider auth.login_google/auth.login_github (and auth.login as the generic), to keep template usage consistent.

If you adopt this, also update the landing CTA to url_for('auth.login') or url_for('auth.login_google').

Also applies to: 154-158

📜 Review details

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

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 975a5dc and 1f04885.

⛔ Files ignored due to path filters (1)
  • Pipfile.lock is excluded by !**/*.lock
📒 Files selected for processing (24)
  • Makefile (1 hunks)
  • Pipfile (1 hunks)
  • Pipfile.new (1 hunks)
  • api/app_factory.py (1 hunks)
  • api/auth/oauth_handlers.py (1 hunks)
  • api/auth/user_management.py (2 hunks)
  • api/index.py (1 hunks)
  • api/loaders/mysql_loader.py (5 hunks)
  • api/routes/__init__.py (1 hunks)
  • api/routes/auth.py (1 hunks)
  • api/routes/database.py (2 hunks)
  • api/routes/graphs.py (9 hunks)
  • app/templates/base.j2 (2 hunks)
  • app/templates/components/chat_header.j2 (2 hunks)
  • app/templates/components/chat_input.j2 (1 hunks)
  • app/templates/components/login_modal.j2 (1 hunks)
  • app/templates/components/scripts.j2 (1 hunks)
  • app/templates/landing.j2 (7 hunks)
  • app/ts/app.ts (2 hunks)
  • app/ts/modules/chat.ts (1 hunks)
  • app/ts/modules/graphs.ts (1 hunks)
  • app/ts/modules/messages.ts (1 hunks)
  • app/ts/modules/ui.ts (2 hunks)
  • tests/test_mysql_loader.py (2 hunks)
✅ Files skipped from review due to trivial changes (1)
  • app/ts/modules/ui.ts
🧰 Additional context used
📓 Path-based instructions (11)
api/index.py

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Keep the Flask application entry point at api/index.py

Files:

  • api/index.py
api/routes/**/*.py

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Place all additional Flask route handlers under api/routes/

Files:

  • api/routes/__init__.py
  • api/routes/graphs.py
  • api/routes/database.py
  • api/routes/auth.py
tests/test_*.py

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Name Python unit tests as tests/test_*.py

Files:

  • tests/test_mysql_loader.py
api/loaders/**/*.py

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Place data loading utilities under api/loaders/

Files:

  • api/loaders/mysql_loader.py
api/auth/**/*.py

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Place authentication modules under api/auth/

Files:

  • api/auth/oauth_handlers.py
  • api/auth/user_management.py
Makefile

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Expose developer workflows via Makefile targets (install, setup-dev, test, lint, run)

Files:

  • Makefile
api/routes/graphs.py

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Keep graph/database route handlers in api/routes/graphs.py

Files:

  • api/routes/graphs.py
api/routes/database.py

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Keep database management route handlers in api/routes/database.py

Files:

  • api/routes/database.py
Pipfile

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Manage Python dependencies with Pipenv using Pipfile

Files:

  • Pipfile
api/app_factory.py

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Implement the application factory (including OAuth setup) in api/app_factory.py

Files:

  • api/app_factory.py
api/routes/auth.py

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Keep authentication route handlers in api/routes/auth.py

Files:

  • api/routes/auth.py
🧠 Learnings (6)
📚 Learning: 2025-08-19T08:18:37.470Z
Learnt from: CR
PR: FalkorDB/QueryWeaver#0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-08-19T08:18:37.470Z
Learning: Applies to Pipfile : Manage Python dependencies with Pipenv using Pipfile

Applied to files:

  • Pipfile.new
📚 Learning: 2025-08-19T08:18:37.470Z
Learnt from: CR
PR: FalkorDB/QueryWeaver#0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-08-19T08:18:37.470Z
Learning: Applies to api/index.py : Keep the Flask application entry point at api/index.py

Applied to files:

  • api/index.py
📚 Learning: 2025-08-19T08:18:37.470Z
Learnt from: CR
PR: FalkorDB/QueryWeaver#0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-08-19T08:18:37.470Z
Learning: Applies to api/routes/graphs.py : Keep graph/database route handlers in api/routes/graphs.py

Applied to files:

  • api/routes/__init__.py
  • api/routes/graphs.py
📚 Learning: 2025-08-19T08:18:37.470Z
Learnt from: CR
PR: FalkorDB/QueryWeaver#0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-08-19T08:18:37.470Z
Learning: Applies to api/routes/database.py : Keep database management route handlers in api/routes/database.py

Applied to files:

  • api/routes/__init__.py
  • api/routes/database.py
📚 Learning: 2025-08-19T08:18:37.470Z
Learnt from: CR
PR: FalkorDB/QueryWeaver#0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-08-19T08:18:37.470Z
Learning: Applies to api/app_factory.py : Implement the application factory (including OAuth setup) in api/app_factory.py

Applied to files:

  • api/auth/oauth_handlers.py
  • api/app_factory.py
  • api/routes/auth.py
📚 Learning: 2025-08-19T08:18:37.470Z
Learnt from: CR
PR: FalkorDB/QueryWeaver#0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-08-19T08:18:37.470Z
Learning: Applies to api/routes/auth.py : Keep authentication route handlers in api/routes/auth.py

Applied to files:

  • api/routes/auth.py
🧬 Code graph analysis (5)
api/auth/oauth_handlers.py (1)
api/auth/user_management.py (1)
  • ensure_user_in_organizations (16-124)
api/routes/graphs.py (7)
api/auth/user_management.py (1)
  • token_required (280-322)
api/loaders/json_loader.py (1)
  • JSONLoader (23-71)
api/loaders/odata_loader.py (1)
  • ODataLoader (11-135)
api/loaders/csv_loader.py (1)
  • CSVLoader (13-233)
api/agents/relevancy_agent.py (1)
  • RelevancyAgent (57-90)
api/agents/analysis_agent.py (2)
  • AnalysisAgent (9-201)
  • get_analysis (14-49)
api/graph.py (2)
  • find (56-116)
  • get_db_description (38-53)
api/routes/database.py (3)
api/auth/user_management.py (1)
  • token_required (280-322)
api/loaders/postgres_loader.py (2)
  • PostgresLoader (18-491)
  • load (67-108)
api/loaders/mysql_loader.py (2)
  • MySQLLoader (20-535)
  • load (125-167)
api/app_factory.py (2)
api/auth/oauth_handlers.py (1)
  • setup_oauth_handlers (14-66)
api/routes/auth.py (1)
  • init_auth (273-307)
api/routes/auth.py (1)
api/auth/user_management.py (1)
  • validate_and_cache_user (162-278)
🪛 GitHub Actions: Pylint
api/routes/__init__.py

[warning] 1-1: C0114: Missing module docstring (missing-module-docstring)

api/auth/oauth_handlers.py

[warning] 16-16: pylint: Trailing whitespace (trailing-whitespace).


[warning] 19-19: pylint: Trailing whitespace (trailing-whitespace).


[warning] 20-20: pylint: Line too long (105/100) (line-too-long).


[warning] 42-42: pylint: Line too long (105/100) (line-too-long).


[warning] 63-63: pylint: Trailing whitespace (trailing-whitespace).


[warning] 38-38: pylint: Catching too general exception (broad-exception-caught).


[warning] 20-20: pylint: Unused argument 'request' (unused-argument).


[warning] 60-60: pylint: Catching too general exception (broad-exception-caught).


[warning] 42-42: pylint: Unused argument 'request' (unused-argument).


[warning] 42-42: pylint: Unused argument 'token' (unused-argument).


[warning] 4-4: pylint: Unused import time (unused-import).


[warning] 7-7: pylint: Unused import requests (unused-import).

api/app_factory.py

[warning] 38-38: C0303: Trailing whitespace (trailing-whitespace)


[warning] 45-45: C0301: Line too long (102/100) (line-too-long)

api/routes/auth.py

[warning] 37-37: C0301: Line too long (104/100) (line-too-long)

🪛 Ruff (0.12.2)
api/loaders/mysql_loader.py

516-516: Local variable e is assigned to but never used

Remove assignment to unused variable e

(F841)


522-522: try-except block with duplicate exception pymysql.MySQLError

(B025)

api/auth/oauth_handlers.py

7-7: requests imported but unused

Remove unused import: requests

(F401)

api/routes/graphs.py

8-8: typing.Dict imported but unused

Remove unused import

(F401)


8-8: typing.Any imported but unused

Remove unused import

(F401)


10-10: fastapi.status imported but unused

Remove unused import

(F401)


10-10: fastapi.Form imported but unused

Remove unused import

(F401)


194-194: Do not perform function call File in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable

(B008)


225-225: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling

(B904)

api/auth/user_management.py

8-8: requests imported but unused

Remove unused import: requests

(F401)


10-10: fastapi.responses.JSONResponse imported but unused

Remove unused import: fastapi.responses.JSONResponse

(F401)


317-320: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling

(B904)

api/routes/database.py

3-3: typing.Dict imported but unused

Remove unused import

(F401)


3-3: typing.Any imported but unused

Remove unused import

(F401)


5-5: fastapi.status imported but unused

Remove unused import: fastapi.status

(F401)


47-47: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling

(B904)


56-56: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling

(B904)


73-73: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling

(B904)

api/app_factory.py

11-11: fastapi.templating.Jinja2Templates imported but unused

Remove unused import: fastapi.templating.Jinja2Templates

(F401)


15-15: api.auth.oauth_handlers.setup_oauth_handlers imported but unused

Remove unused import: api.auth.oauth_handlers.setup_oauth_handlers

(F401)

🪛 GitHub Check: CodeQL
api/routes/graphs.py

[failure] 111-111: Log Injection
This log entry depends on a user-provided value.


[warning] 504-504: Information exposure through an exception
Stack trace information flows to this location and may be exposed to an external user.
Stack trace information flows to this location and may be exposed to an external user.
Stack trace information flows to this location and may be exposed to an external user.
Stack trace information flows to this location and may be exposed to an external user.
Stack trace information flows to this location and may be exposed to an external user.


[warning] 620-620: Information exposure through an exception
Stack trace information flows to this location and may be exposed to an external user.
Stack trace information flows to this location and may be exposed to an external user.
Stack trace information flows to this location and may be exposed to an external user.
Stack trace information flows to this location and may be exposed to an external user.


[failure] 661-661: Log Injection
This log entry depends on a user-provided value.

🪛 GitHub Actions: Dependency review
Pipfile

[error] 1-1: Dependency review detected vulnerable packages: Pipfile.lock contains starlette@0.46.2 with a moderate severity denial-of-service vulnerability (GHSA-2c2j-9gv5-cj73).

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: e2e-tests
🔇 Additional comments (22)
app/ts/modules/messages.ts (1)

5-5: LGTM! Import path modernization is consistent.

The import path has been updated from .js to extensionless format, aligning with modern TypeScript practices and maintaining consistency with other modules in the PR.

app/ts/modules/chat.ts (1)

5-6: LGTM! Import paths updated consistently.

Both import statements have been modernized to use extensionless paths, which is consistent with the broader frontend refactoring across the TypeScript modules in this PR.

app/ts/modules/graphs.ts (1)

5-6: LGTM! Import path modernization aligns with codebase standards.

The extensionless import paths follow modern TypeScript conventions and are consistent with the changes made across other modules in this PR.

app/ts/app.ts (4)

5-21: LGTM! Import path modernization is comprehensive and consistent.

All import statements have been updated to use extensionless paths, which aligns with modern TypeScript practices and maintains consistency across the entire frontend codebase refactor.


23-46: Well-structured graph loading function with proper error handling.

The loadAndShowGraph function demonstrates good practices:

  • Early return for invalid input
  • Proper HTTP status checking
  • Data shape validation before processing
  • Container cleanup before rendering
  • Comprehensive error handling with meaningful logging

The function appropriately handles edge cases and provides clear feedback through console logging.


64-70: Efficient async callback integration.

The schema button event listener properly integrates the new loadAndShowGraph function with async/await syntax, ensuring the graph data is loaded when the schema container is toggled open.


86-93: Smart conditional graph loading on selection change.

The graph select change handler efficiently loads graph data only when the schema container is currently open, avoiding unnecessary network requests and improving performance.

app/templates/components/chat_input.j2 (1)

6-6: StaticFiles url_for validated – ready to merge

  • Verified no legacy url_for('static', filename=…) patterns in app/templates remain.
  • Confirmed app.mount("/static", StaticFiles(directory=static_path), name="static") in api/app_factory.py:69.
  • Confirmed Jinja2Templates instantiation (in api/routes/auth.py:21) and use of templates.TemplateResponse (lines 44, 70, 79) guarantees url_for is exposed in the Jinja context.

Optional enhancement: add explicit width/height attributes to the <img> tag to reduce layout shift.

app/templates/base.j2 (1)

37-37: LGTM: correct Starlette-style url_for usage with path=.

The switch from filename= to path= is correct for Starlette/FastAPI static files.

api/loaders/mysql_loader.py (1)

10-12: Migration to PyMySQL + DictCursor looks good.

Imports are correct and reflect the driver switch.

tests/test_mysql_loader.py (1)

104-114: LGTM on switching the patch target to pymysql.connect.

This aligns tests with the new driver and keeps behavior intact.

Pipfile (2)

12-12: Good: pymysql is present here (consistent with loader/tests).

This aligns the runtime with the driver migration.


27-28: Python 3.12 is fine; verify transitive deps (e.g., playwright) work on CI runners for 3.12.

Minor note; nothing to change unless CI shows issues.

app/templates/landing.j2 (2)

6-6: Static asset URL looks correct for FastAPI StaticFiles.

Using url_for('static', path='css/landing.css') matches the Starlette/FastAPI StaticFiles mount. No action needed.


116-119: Nice UX touch: typing the first example for consistency.

The initial render now uses the same typing path as subsequent examples. Simple and cohesive.

api/routes/database.py (2)

20-23: Auth integration via token_required looks correct.

Using token_required to populate request.state.user_id aligns with the new auth flow and keeps the route lean.


44-45: Confirmed: Loader.load methods are static – no instantiation required

Verified via ripgrep that both PostgresLoader.load and MySQLLoader.load are decorated with @staticmethod, so calling them directly on the class in api/routes/database.py will work as written. No changes to instantiation or signatures are needed.

api/app_factory.py (1)

75-78: Routers are mounted in the app factory per guidelines.

Auth, graphs (with /graphs prefix), and database routers are included correctly.

api/routes/auth.py (4)

20-22: Templates wiring for FastAPI is correct.

Computing TEMPLATES_DIR and using Jinja2Templates is fine; resolves to app/templates.


40-51: Chat route looks good and returns consistent template context.

Renders chat.j2 with is_authenticated and user_info; pairs with home route behavior.


272-308: Auth initialization belongs here and is wired correctly.

init_auth(app) registers Google and GitHub providers and exposes oauth via app.state. Good separation from the app factory.


61-87: Home route behavior reads well.

Clearing auth session keys when unauthenticated and rendering landing.j2 vs chat.j2 is straightforward.

Comment on lines +202 to +219
if resp.status_code == 200:
google_user = resp.json()
# Validate required fields
if not google_user.get("id") or not google_user.get("email"):
logging.warning("Invalid Google user data received")
session.clear()
request.session.pop("google_token", None)
request.session.pop("user_info", None)
return None, False

# Normalize user info structure
# Normalize
user_info = {
"id": str(google_user.get("id")), # Ensure string type
"id": str(google_user.get("id")),
"name": google_user.get("name", ""),
"email": google_user.get("email"),
"picture": google_user.get("picture", ""),
"provider": "google"
"provider": "google",
}
session["user_info"] = user_info
session["token_validated_at"] = current_time
request.session["user_info"] = user_info
request.session["token_validated_at"] = current_time
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

Google userinfo: use sub (OIDC) fallback to avoid false auth failures.

Google’s OIDC userinfo commonly uses sub as the stable subject identifier; relying on id alone will intermittently fail and cause unnecessary 401s. Normalize to prefer sub and fallback to id. Also keep the rest of the logic unchanged.

-                if resp.status_code == 200:
-                    google_user = resp.json()
-                    if not google_user.get("id") or not google_user.get("email"):
+                if resp.status_code == 200:
+                    google_user = resp.json()
+                    google_id = google_user.get("sub") or google_user.get("id")
+                    if not google_id or not google_user.get("email"):
                         logging.warning("Invalid Google user data received")
                         request.session.pop("google_token", None)
                         request.session.pop("user_info", None)
                         return None, False

                     # Normalize
                     user_info = {
-                        "id": str(google_user.get("id")),
+                        "id": str(google_id),
                         "name": google_user.get("name", ""),
                         "email": google_user.get("email"),
                         "picture": google_user.get("picture", ""),
                         "provider": "google",
                     }

Also applies to: 212-217

🤖 Prompt for AI Agents
In api/auth/user_management.py around lines 202 to 219 (and also apply same
change to 212-217), the code currently checks google_user.get("id") for a stable
identifier which can be missing; change the validation and normalization to
prefer the OIDC "sub" claim and fall back to "id": compute a stable_id =
google_user.get("sub") or google_user.get("id"), ensure stable_id exists (and
convert to str), keep the existing email check, then set user_info["id"] =
str(stable_id) and preserve the rest of the user_info fields and session
assignments exactly as before.

Comment on lines +470 to 472
# Rollback in case of error
if 'conn' in locals():
conn.rollback()
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

Bug: PyMySQL doesn’t support cursor(dictionary=True); use DictCursor (or pass cursorclass at connect).

This line will raise TypeError. Switch to DictCursor.

-            conn = pymysql.connect(**conn_params)
-            cursor = conn.cursor(dictionary=True)
+            conn = pymysql.connect(**conn_params)
+            cursor = conn.cursor(DictCursor)

Optionally set it at connect-time:

conn = pymysql.connect(**conn_params, cursorclass=DictCursor)
cursor = conn.cursor()

For reference on DictCursor usage, see PyMySQL examples. (stackoverflow.com, devdungeon.com)

🧰 Tools
🪛 GitHub Check: CodeQL

[failure] 472-472: SQL query built from user-controlled sources
This SQL query depends on a user-provided value.

🤖 Prompt for AI Agents
In api/loaders/mysql_loader.py around lines 470 to 472, the code calls
conn.cursor(dictionary=True) which PyMySQL does not support and will raise a
TypeError; replace this by using pymysql.cursors.DictCursor (either by passing
cursorclass=DictCursor to pymysql.connect or by importing DictCursor and calling
conn.cursor(DictCursor)), and update imports accordingly so cursor.fetchall()
returns dict rows without runtime errors.

Comment on lines +516 to +522
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

⚠️ Potential issue

Duplicate exception blocks + swallowed error; unify handling and always close safely.

There are two consecutive except pymysql.MySQLError blocks; the first swallows the error and makes the second unreachable, and tools flagged it. Also guard cursor.close() when cursor may not exist. Refactor to a single handler that rolls back, closes resources, then raises a descriptive exception.

Apply this unified handling:

-        except pymysql.MySQLError as e:
-            # Rollback in case of error
-            if 'conn' in locals():
-                conn.rollback()
-                cursor.close()
-                conn.close()
-        except pymysql.MySQLError as e:
+        except pymysql.MySQLError as e:
             # Rollback in case of error
             if 'conn' in locals():
                 conn.rollback()
-                cursor.close()
-                conn.close()
+                if 'cursor' in locals():
+                    try:
+                        cursor.close()
+                    except Exception:
+                        pass
+                try:
+                    conn.close()
+                except Exception:
+                    pass
             raise Exception(f"MySQL query execution error: {str(e)}") from e

This addresses the duplicate-except warning and the unused variable warning reported by static analysis.

Also applies to: 522-529

🧰 Tools
🪛 Ruff (0.12.2)

516-516: Local variable e is assigned to but never used

Remove assignment to unused variable e

(F841)


522-522: try-except block with duplicate exception pymysql.MySQLError

(B025)

🤖 Prompt for AI Agents
In api/loaders/mysql_loader.py around lines 516-529, there are two consecutive
`except pymysql.MySQLError` blocks that cause the first to swallow the error and
make the second unreachable; replace them with a single `except
pymysql.MySQLError as e` handler that: 1) checks for existence and non-None of
`conn` and `cursor` (e.g., `if 'cursor' in locals() and cursor is not None:`)
before calling `cursor.close()` and similarly for `conn` before calling
`conn.rollback()` and `conn.close()`, 2) ensures resources are closed safely
even if one of them was never created, and 3) raises or re-raises a descriptive
exception that includes the original error (do not silently swallow it).

Comment on lines +44 to 45
success, result = PostgresLoader.load(request.state.user_id, url)
except (ValueError, ConnectionError) as e:
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Avoid blocking the event loop: run sync DB loaders in a threadpool.

psycopg2 and PyMySQL are synchronous. Calling them directly in an async route will block the event loop. Offload to a threadpool.

Apply:

-                success, result = PostgresLoader.load(request.state.user_id, url)
+                loader = PostgresLoader()
+                success, result = await run_in_threadpool(loader.load, request.state.user_id, url)
@@
-                success, result = MySQLLoader.load(request.state.user_id, url)
+                loader = MySQLLoader()
+                success, result = await run_in_threadpool(loader.load, request.state.user_id, url)

Also applies to: 53-54

🤖 Prompt for AI Agents
In api/routes/database.py around lines 44-45 (and similarly lines 53-54), the
code calls synchronous DB loaders (psycopg2/PyMySQL) directly inside an async
route which will block the event loop; change these calls to execute the loader
functions in a threadpool (e.g., use asyncio.to_thread or loop.run_in_executor)
and await the result so the synchronous PostgresLoader.load (and the other
loader call at 53-54) run off the event loop, propagate exceptions, and return
their values to the async handler.

Comment on lines 205 to +213
if content_type.startswith("application/json"):
data = request.get_json()
if not data or "database" not in data:
return jsonify({"error": "Invalid JSON data"}), 400

graph_id = g.user_id + "_" + data["database"]
success, result = JSONLoader.load(graph_id, data)

# # ✅ Handle XML Payload
# elif content_type.startswith("application/xml") or content_type.startswith("text/xml"):
# xml_data = request.data
# graph_id = ""
# success, result = ODataLoader.load(graph_id, xml_data)

# # ✅ Handle CSV Payload
# elif content_type.startswith("text/csv"):
# csv_data = request.data
# graph_id = ""
# success, result = CSVLoader.load(graph_id, csv_data)

# ✅ Handle File Upload (FormData with JSON/XML)
elif content_type.startswith("multipart/form-data"):
if "file" not in request.files:
return jsonify({"error": "No file uploaded"}), 400

file = request.files["file"]
if file.filename == "":
return jsonify({"error": "Empty file"}), 400
if data:
if not hasattr(data, 'database') or not data.database:
raise HTTPException(status_code=400, detail="Invalid JSON data")

graph_id = request.state.user_id + "_" + data.database
success, result = JSONLoader.load(graph_id, data.dict())

# ✅ Handle File Upload
elif file:
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Sanitize graph_id derived from payload/filenames and handle loader exceptions.

  • Graph IDs built from data.database or filenames are not sanitized, enabling unexpected characters and potential namespace collisions.
  • Calls to loaders aren’t guarded; e.g., current CSVLoader raises NotImplementedError, which will bubble up as a 500.

Introduce a small sanitizer and wrap loader calls to consistently return 400s on bad inputs.

Use this helper (add near the top of the module):

# helper placed near other utils
import os
import re

GRAPH_ID_MAX_LEN = 100

def sanitize_graph_name(raw: str) -> str:
    if not isinstance(raw, str):
        raw = str(raw)
    # allow alnum, underscore, dash; replace others with underscore
    cleaned = re.sub(r"[^A-Za-z0-9_-]+", "_", raw).strip("_")
    return cleaned[:GRAPH_ID_MAX_LEN]

Apply:

@@
-    if data:
-        if not hasattr(data, 'database') or not data.database:
+    if data:
+        if not hasattr(data, 'database') or not data.database:
             raise HTTPException(status_code=400, detail="Invalid JSON data")
 
-        graph_id = request.state.user_id + "_" + data.database
-        success, result = JSONLoader.load(graph_id, data.dict())
+        graph_id = f"{request.state.user_id}_{sanitize_graph_name(data.database)}"
+        try:
+            success, result = JSONLoader.load(graph_id, data.dict())
+        except Exception as e:
+            logging.error("JSON load failed for %s: %s", sanitize_log_input(graph_id), e)
+            raise HTTPException(status_code=400, detail="Failed to load JSON graph data") from e
@@
-    elif file:
+    elif file:
         content = await file.read()
         filename = file.filename
@@
-        if filename.endswith(".json"):
+        if filename.endswith(".json"):
             try:
                 data = json.loads(content.decode("utf-8"))
-                graph_id = request.state.user_id + "_" + data.get("database", "")
-                success, result = JSONLoader.load(graph_id, data)
+                dbname = data.get("database", "")
+                if not dbname:
+                    raise HTTPException(status_code=400, detail="Missing 'database' in JSON")
+                graph_id = f"{request.state.user_id}_{sanitize_graph_name(dbname)}"
+                try:
+                    success, result = JSONLoader.load(graph_id, data)
+                except Exception as e:
+                    logging.error("JSON file load failed for %s: %s", sanitize_log_input(graph_id), e)
+                    raise HTTPException(status_code=400, detail="Failed to load JSON file") from e
             except json.JSONDecodeError:
                 raise HTTPException(status_code=400, detail="Invalid JSON file")
@@
-        elif filename.endswith(".xml"):
-            xml_data = content.decode("utf-8")
-            graph_id = request.state.user_id + "_" + filename.replace(".xml", "")
-            success, result = ODataLoader.load(graph_id, xml_data)
+        elif filename.endswith(".xml"):
+            xml_data = content.decode("utf-8")
+            base = os.path.splitext(filename)[0]
+            graph_id = f"{request.state.user_id}_{sanitize_graph_name(base)}"
+            try:
+                success, result = ODataLoader.load(graph_id, xml_data)
+            except Exception as e:
+                logging.error("XML load failed for %s: %s", sanitize_log_input(graph_id), e)
+                raise HTTPException(status_code=400, detail="Failed to load XML file") from e
@@
-        elif filename.endswith(".csv"):
-            csv_data = content.decode("utf-8")
-            graph_id = request.state.user_id + "_" + filename.replace(".csv", "")
-            success, result = CSVLoader.load(graph_id, csv_data)
+        elif filename.endswith(".csv"):
+            csv_data = content.decode("utf-8")
+            base = os.path.splitext(filename)[0]
+            graph_id = f"{request.state.user_id}_{sanitize_graph_name(base)}"
+            try:
+                success, result = CSVLoader.load(graph_id, csv_data)
+            except Exception as e:
+                logging.error("CSV load failed for %s: %s", sanitize_log_input(graph_id), e)
+                raise HTTPException(status_code=400, detail="Failed to load CSV file") from e

Also applies to: 218-241

Comment on lines +7 to +8
<link rel="stylesheet" href="/static/css/main.css">
<link rel="shortcut icon" href="/static/favicon.ico">
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Use url_for('static', path=...) instead of absolute “/static” to respect SCRIPT_NAME/subpaths and CDN mounts.

Direct “/static/…” breaks when the app is served under a subpath, behind a proxy with path prefix, or with a different static mount. Starlette’s Jinja integration exposes url_for and expects the “path=” kwarg for static. Suggested change:

-    <link rel="stylesheet" href="/static/css/main.css">
-    <link rel="shortcut icon" href="/static/favicon.ico">
+    <link rel="stylesheet" href="{{ url_for('static', path='css/main.css') }}">
+    <link rel="shortcut icon" href="{{ url_for('static', path='favicon.ico') }}">
📝 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
<link rel="stylesheet" href="/static/css/main.css">
<link rel="shortcut icon" href="/static/favicon.ico">
<link rel="stylesheet" href="{{ url_for('static', path='css/main.css') }}">
<link rel="shortcut icon" href="{{ url_for('static', path='favicon.ico') }}">
🤖 Prompt for AI Agents
In app/templates/base.j2 around lines 7 to 8, the template uses hardcoded
“/static/…” URLs which break when the app is mounted under a subpath or uses a
CDN; replace those href/src values with Jinja url_for calls using the static
endpoint and the path= keyword (e.g. url_for('static', path='css/main.css') and
url_for('static', path='favicon.ico')) so links resolve correctly under
SCRIPT_NAME, proxies, or alternate static mounts.

Pipfile Outdated
Comment on lines 7 to 13
fastapi = "~=0.115.0"
uvicorn = "~=0.32.0"
litellm = "~=1.74.14"
falkordb = "~=1.2.0"
flask = "~=3.1.0"
psycopg2-binary = "~=2.9.9"
pymysql = "~=1.1.0"
authlib = "~=1.4.0"
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Remediate Starlette DoS advisory by bumping FastAPI so it can resolve a patched Starlette (>=0.47.2).

Your CI flagged starlette@0.46.2 (GHSA-2c2j-9gv5-cj73). The fix is in Starlette 0.47.2. FastAPI 0.115.x caps Starlette below 0.47.0 in earlier patches; FastAPI 0.116.1 broadens support to <0.48.0, allowing 0.47.2+. Update FastAPI and refresh the lock to pull a patched Starlette, and do not pin Starlette directly.

- fastapi = "~=0.115.0"
+ fastapi = "~=0.116.1"

Then regenerate the lock (so Starlette resolves to >=0.47.2). References: CVE/GHSA advisory (fixed in 0.47.2) and FastAPI release notes expanding Starlette range. (nvd.nist.gov, osv.dev, fastapi.tiangolo.com)

📝 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
fastapi = "~=0.115.0"
uvicorn = "~=0.32.0"
litellm = "~=1.74.14"
falkordb = "~=1.2.0"
flask = "~=3.1.0"
psycopg2-binary = "~=2.9.9"
pymysql = "~=1.1.0"
authlib = "~=1.4.0"
[[packages]]
fastapi = "~=0.116.1"
uvicorn = "~=0.32.0"
litellm = "~=1.74.14"
falkordb = "~=1.2.0"
psycopg2-binary = "~=2.9.9"
pymysql = "~=1.1.0"
authlib = "~=1.4.0"
🤖 Prompt for AI Agents
In Pipfile around lines 7 to 13, CI flagged Starlette v0.46.2
(GHSA-2c2j-9gv5-cj73) which is pulled by FastAPI; update FastAPI to "~=0.116.1"
so its dependency range allows Starlette >=0.47.2, do not pin Starlette
directly, then run pipenv lock (or your dependency manager) to regenerate the
lockfile so Starlette resolves to >=0.47.2 and the advisory is remediated.

Pipfile.new Outdated
Comment on lines 1 to 31
[[source]]
url = "https://pypi.org/simple"
verify_ssl = true
name = "pypi"

[packages]
litellm = {extras = ["bedrock"], version = "~=1.74.14"}
falkordb = "~=1.2.0"
fastapi = "~=0.115.0"
uvicorn = {extras = ["standard"], version = "~=0.32.0"}
jsonschema = "~=4.25.0"
tqdm = "~=4.67.1"
boto3 = "~=1.40.11"
psycopg2-binary = "~=2.9.9"
async-timeout = "~=4.0.3"
mysql-connector-python = "~=9.4.0"
authlib = "~=1.4.0"
itsdangerous = "~=2.2.0"
python-multipart = "~=0.0.10"
jinja2 = "~=3.1.4"
starlette = "~=0.40.0"

[dev-packages]
pytest = "~=8.4.1"
pylint = "~=3.3.4"
playwright = "~=1.47.0"
pytest-playwright = "~=0.7.0"
pytest-asyncio = "~=0.24.0"

[requires]
python_version = "3.12"
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

Please remove Pipfile.new; manage dependencies only via Pipfile (to avoid drift/conflicts).

Per the repo’s guidelines, use a single Pipfile. Keeping Pipfile.new alongside Pipfile will cause lockfile drift, CI disagreements, and reviewer confusion. Merge any intended changes into Pipfile and delete this file.

Would you like me to propose a consolidated Pipfile diff and a small script to regenerate Pipfile.lock?

🤖 Prompt for AI Agents
In Pipfile.new (lines 1-31) you added a duplicate dependency file; remove
Pipfile.new, merge any intended dependency changes into the canonical Pipfile
(update its [packages]/[dev-packages] sections to include the new entries and
versions), regenerate the lockfile with the project’s standard command (e.g.,
pipenv lock or the repo’s script) so Pipfile.lock matches Pipfile, and commit
only the updated Pipfile and lockfile; delete Pipfile.new from the branch to
prevent drift and CI confusion.

Pipfile.new Outdated
itsdangerous = "~=2.2.0"
python-multipart = "~=0.0.10"
jinja2 = "~=3.1.4"
starlette = "~=0.40.0"
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Avoid pinning Starlette directly; let FastAPI control the compatible Starlette range.

FastAPI manages Starlette compatibility; pinning can introduce conflicts. Moreover, your pipeline flagged a Starlette vulnerability (GHSA-2c2j-9gv5-cj73; fixed in Starlette 0.47.2). To remediate properly you’ll need to let FastAPI resolve a patched Starlette version and bump FastAPI accordingly (see Pipfile comment). Do not add an explicit starlette pin here. (fastapi.tiangolo.com, nvd.nist.gov)

- starlette = "~=0.40.0"
+ # Intentionally not pinned; FastAPI controls the supported range.
📝 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
starlette = "~=0.40.0"
# (in Pipfile.new, under [packages])
# Intentionally not pinned; FastAPI controls the supported range.
🤖 Prompt for AI Agents
In Pipfile.new around line 21, remove the explicit starlette = "~=0.40.0" entry
so FastAPI can control the compatible Starlette range; instead, bump the FastAPI
version in the Pipfile to a release that depends on Starlette >=0.47.2 (per the
Pipfile comment) and run pipenv lock / regenerate the lockfile to pick up the
patched Starlette; do not add any other direct Starlette pin or downgrade
FastAPI dependencies.

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 (2)
tests/e2e/test_api_endpoints.py (1)

26-26: Fix favicon assertion to match comment.

Comment says “404 is acceptable,” but assertion only allows 200. Tests will fail if favicon is absent.

-        assert response.status_code in [200]  # 404 is acceptable if no favicon
+        assert response.status_code in [200, 404]  # 404 is acceptable if no favicon
tests/conftest.py (1)

25-36: Readiness probe is too strict; accept common ready statuses and avoid redirect chains.

The app root might 302 to /chat or return 401/403/404 before auth/static are configured. Current check requires 200 and follows redirects, causing false negatives. Broaden acceptable statuses and disable redirects.

     # Wait for the app to start
     max_retries = 30
     for _ in range(max_retries):
         try:
-            response = requests.get("http://localhost:5000/", timeout=1)
-            if response.status_code == 200:
+            response = requests.get("http://localhost:5000/", timeout=1, allow_redirects=False)
+            if response.status_code in (200, 302, 401, 403, 404):
                 break
         except requests.exceptions.RequestException:
             time.sleep(1)
     else:
         process.terminate()
-        raise RuntimeError("FastAPI app failed to start")
+        raise RuntimeError("FastAPI app failed to start (health check did not return a ready status code)")
🧹 Nitpick comments (11)
.github/copilot-instructions.md (5)

81-89: Bind to 0.0.0.0 in production examples (containers/cloud) rather than localhost.

Using "localhost" limits exposure to the loopback interface and can break Docker/K8s/VM deployments. Recommend binding to 0.0.0.0 in the production command snippet.

 # Production mode
 make run-prod
-# OR manually: pipenv run uvicorn api.index:app --host "localhost" --port "5000"
+# OR manually: pipenv run uvicorn api.index:app --host "0.0.0.0" --port "5000"

128-131: Clarify env var purpose and remove unused FLASK_DEBUG from docs.

The app uses FLASK_SECRET_KEY (FastAPI sessions), but FLASK_DEBUG isn’t referenced in api/app_factory.py or tests. Keep the secret key, drop FLASK_DEBUG from the required list, and clarify the comment to “Required for sessions.”

-# REQUIRED for FastAPI to start
-FLASK_SECRET_KEY=your_super_secret_key_here
-FLASK_DEBUG=False
+# REQUIRED for sessions (FastAPI cookies)
+FLASK_SECRET_KEY=your_super_secret_key_here

149-153: Mirror the minimal .env guidance: remove FLASK_DEBUG here too.

Keep the secret key and DB settings; FLASK_DEBUG is unused.

 FLASK_SECRET_KEY=test-secret-key
-FLASK_DEBUG=False
 FALKORDB_HOST=localhost
 FALKORDB_PORT=6379

197-215: Add a language to fenced code block to satisfy markdownlint (MD040).

Mark tree/listing blocks as text for cleaner linting and editor highlighting.

-```
+```text
 api/                          # Main application package
 ├── index.py                 # FastAPI application entry point
 ...

---

`293-295`: **Avoid referencing specific line numbers in docs; it drifts quickly.**

Embedding “(lines 1-50 contain core configuration)” will go stale. Remove the parenthetical.



```diff
-- `api/app_factory.py`: Application factory with OAuth setup (lines 1-50 contain core configuration)
+- `api/app_factory.py`: Application factory with OAuth setup
tests/e2e/test_api_endpoints.py (1)

29-31: Make static directory assertion resilient across environments.

With SecurityMiddleware, /static/css/ returns 403, but different servers/CDNs may return 404 or 405. Keep the intent (“no directory listing”) but allow common variants.

-        assert response.status_code in [403]  # Various acceptable responses
+        assert response.status_code in [403, 404, 405]  # No directory listing; status may vary by server
tests/e2e/pages/home_page.py (1)

19-23: Optional: Centralize route paths to reduce coupling.

Consider a small Routes/Paths helper (or fixture-provided base path) so page objects don’t hard-code “/chat”, easing future route changes.

api/app_factory.py (4)

28-41: Harden static path checks: decode and normalize to catch encoded traversal.

Checking for '../' misses percent-encoded traversal (..%2F) and path normalization edge cases. Decode and posix-normalize before checks. Starlette’s StaticFiles already mitigates traversal; this makes your explicit 403 path stricter and less brittle.

     async def dispatch(self, request: Request, call_next):
-        # Block directory access in static files
-        if request.url.path.startswith(self.STATIC_PREFIX):
-            # Remove /static/ prefix to get the actual path
-            filename = request.url.path[len(self.STATIC_PREFIX):]
-            # Basic security check for directory traversal
-            if '../' in filename or filename.endswith('/'):
-                return JSONResponse(
-                    status_code=403,
-                    content={"detail": "Forbidden"}
-                )
-
-        response = await call_next(request)
-        return response
+        # Block directory access in static files
+        if request.url.path.startswith(self.STATIC_PREFIX):
+            # Remove /static/ prefix and decode
+            raw = request.url.path[len(self.STATIC_PREFIX):]
+            filename = unquote(raw)
+            # Normalize to detect traversal reliably
+            normalized = posixpath.normpath("/" + filename).lstrip("/")
+            # Forbid directory listing and traversal
+            if normalized.endswith("/") or normalized.startswith("../") or "/../" in f"/{normalized}/":
+                return JSONResponse(status_code=403, content={"detail": "Forbidden"})
+        response = await call_next(request)
+        return response

Add these imports near the other imports:

from urllib.parse import unquote
import posixpath

54-62: Make session cookie “secure” configurable via env for production.

Hardcoding https_only=False is fine locally but weak in prod. Gate it by SESSION_COOKIE_SECURE=true.

-    app.add_middleware(
+    secure_cookie = os.getenv("SESSION_COOKIE_SECURE", "false").lower() == "true"
+    app.add_middleware(
         SessionMiddleware,
         secret_key=secret_key,
         session_cookie="qw_session",
         same_site="lax",  # allow top-level OAuth GET redirects to send cookies
-        https_only=False,  # allow http on localhost in development
+        https_only=secure_cookie,  # enable in prod with SESSION_COOKIE_SECURE=true
         max_age=60 * 60 * 24 * 14,  # 14 days - measured by seconds
     )

49-53: Naming nit: FLASK_SECRET_KEY under FastAPI may confuse new contributors.

Not blocking, but consider documenting the rationale (“legacy env var name kept for backward compatibility”) or accept either name (e.g., read SESSION_SECRET if present, else fallback to FLASK_SECRET_KEY).

Example reference change outside this hunk:

secret_key = os.getenv("SESSION_SECRET") or os.getenv("FLASK_SECRET_KEY")

97-101: Minor: Read GTM id once at startup to avoid per-request env lookups.

Cache GOOGLE_TAG_MANAGER_ID in app.state on startup and reuse.

Example change outside this hunk:

app.state.gtm_id = os.getenv("GOOGLE_TAG_MANAGER_ID")

@app.middleware("http")
async def add_template_globals(request: Request, call_next):
    request.state.google_tag_manager_id = app.state.gtm_id
    return await call_next(request)
📜 Review details

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

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 1f04885 and eeea600.

📒 Files selected for processing (6)
  • .github/copilot-instructions.md (7 hunks)
  • api/app_factory.py (1 hunks)
  • tests/conftest.py (3 hunks)
  • tests/e2e/pages/home_page.py (1 hunks)
  • tests/e2e/test_api_endpoints.py (1 hunks)
  • tests/test_simple_integration.py (1 hunks)
✅ Files skipped from review due to trivial changes (1)
  • tests/test_simple_integration.py
🧰 Additional context used
📓 Path-based instructions (4)
tests/e2e/test_*.py

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Place Playwright E2E tests as tests/e2e/test_*.py

Files:

  • tests/e2e/test_api_endpoints.py
tests/e2e/pages/**/*.py

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Organize E2E Page Object Model classes under tests/e2e/pages/

Files:

  • tests/e2e/pages/home_page.py
tests/conftest.py

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Keep pytest configuration and fixtures in tests/conftest.py

Files:

  • tests/conftest.py
api/app_factory.py

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Implement the application factory (including OAuth setup) in api/app_factory.py

Files:

  • api/app_factory.py
🧠 Learnings (6)
📚 Learning: 2025-08-19T08:18:37.470Z
Learnt from: CR
PR: FalkorDB/QueryWeaver#0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-08-19T08:18:37.470Z
Learning: Applies to .env : Create a local .env file (not committed) from .env.example for running the app

Applied to files:

  • .github/copilot-instructions.md
📚 Learning: 2025-08-19T08:18:37.470Z
Learnt from: CR
PR: FalkorDB/QueryWeaver#0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-08-19T08:18:37.470Z
Learning: Applies to .env.example : Maintain all required environment variables in .env.example as a template

Applied to files:

  • .github/copilot-instructions.md
📚 Learning: 2025-08-19T08:18:37.470Z
Learnt from: CR
PR: FalkorDB/QueryWeaver#0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-08-19T08:18:37.470Z
Learning: Applies to vercel.json : Maintain Vercel deployment configuration in vercel.json

Applied to files:

  • .github/copilot-instructions.md
📚 Learning: 2025-08-19T08:18:37.470Z
Learnt from: CR
PR: FalkorDB/QueryWeaver#0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-08-19T08:18:37.470Z
Learning: Applies to api/index.py : Keep the Flask application entry point at api/index.py

Applied to files:

  • .github/copilot-instructions.md
📚 Learning: 2025-08-19T08:18:37.470Z
Learnt from: CR
PR: FalkorDB/QueryWeaver#0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-08-19T08:18:37.470Z
Learning: Applies to api/app_factory.py : Implement the application factory (including OAuth setup) in api/app_factory.py

Applied to files:

  • .github/copilot-instructions.md
  • api/app_factory.py
📚 Learning: 2025-08-19T08:18:37.470Z
Learnt from: CR
PR: FalkorDB/QueryWeaver#0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-08-19T08:18:37.470Z
Learning: Applies to api/agents/**/*.py : Place AI agent implementations (analysis, relevancy, follow-up) under api/agents/

Applied to files:

  • .github/copilot-instructions.md
🧬 Code graph analysis (2)
tests/e2e/pages/home_page.py (1)
tests/e2e/pages/base_page.py (1)
  • navigate_to (13-16)
api/app_factory.py (1)
api/routes/auth.py (1)
  • init_auth (273-307)
🪛 LanguageTool
.github/copilot-instructions.md

[grammar] ~9-~9: There might be a mistake here.
Context: ...uth authentication. Key Technologies: - Backend: Python 3.12+, FastAPI 0.115.0...

(QB_NEW_EN)


[grammar] ~10-~10: There might be a mistake here.
Context: ...+, FalkorDB (Redis-based graph database) - AI/ML: LiteLLM with Azure OpenAI/OpenA...

(QB_NEW_EN)


[grammar] ~11-~11: There might be a mistake here.
Context: ...I integration for text-to-SQL generation - Testing: pytest for unit tests, Playwr...

(QB_NEW_EN)


[grammar] ~12-~12: There might be a mistake here.
Context: ...r unit tests, Playwright for E2E testing - Dependencies: pipenv for package manag...

(QB_NEW_EN)


[grammar] ~13-~13: There might be a mistake here.
Context: ...dencies**: pipenv for package management - Authentication: authlib with Google/Gi...

(QB_NEW_EN)


[grammar] ~14-~14: There might be a mistake here.
Context: ...tion**: authlib with Google/GitHub OAuth - Deployment: Docker support, Vercel con...

(QB_NEW_EN)


[grammar] ~237-~237: There might be a mistake here.
Context: ...PI ecosystem**: FastAPI, Authlib (OAuth) - Database: falkordb, psycopg2-binary (P...

(QB_NEW_EN)


[grammar] ~293-~293: There might be a mistake here.
Context: .../index.py: Main FastAPI app entry point - api/app_factory.py`: Application factory with OAuth setup (...

(QB_NEW_EN)

🪛 markdownlint-cli2 (0.17.2)
.github/copilot-instructions.md

197-197: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: e2e-tests
🔇 Additional comments (2)
.github/copilot-instructions.md (1)

236-242: No action needed: “psycopg2-binary” is indeed a project dependency

I’ve confirmed that psycopg2-binary is declared and used throughout the repo:

  • Declared in Pipfile (and Pipfile.new) at psycopg2-binary = "~=2.9.9"
  • Locked in Pipfile.lock
  • Referenced in documentation (docs/postgres_loader.md) and used in code (api/loaders/postgres_loader.py) and its tests

The Key Dependencies section correctly lists it, so it can remain as-is.

tests/e2e/pages/home_page.py (1)

21-21: LGTM: Home now navigates to /chat.

Matches the FastAPI routing surface and reduces root-redirect dependency.

Comment on lines +80 to +95
@app.exception_handler(Exception)
async def handle_oauth_error(request: Request, exc: Exception):
"""Handle OAuth-related errors gracefully"""
# Check if it's an OAuth-related error
if "token" in str(error).lower() or "oauth" in str(error).lower():
logging.warning("OAuth error occurred: %s", error)
session.clear()
return redirect(url_for("auth.home"))
if "token" in str(exc).lower() or "oauth" in str(exc).lower():
logging.warning("OAuth error occurred: %s", exc)
request.session.clear()
return RedirectResponse(url="/", status_code=302)

# If it's an HTTPException (like abort(403)), re-raise so Flask handles it properly
if isinstance(error, HTTPException):
return error
# If it's an HTTPException, re-raise so FastAPI handles it properly
if isinstance(exc, HTTPException):
raise exc

# For other errors, let them bubble up
raise error

@app.before_request
def block_static_directories():
if request.path.startswith('/static/'):
# Remove /static/ prefix to get the actual path
filename = secure_filename(request.path[8:])
# Normalize and ensure the path stays within static_folder
static_folder = os.path.abspath(app.static_folder)
file_path = os.path.normpath(os.path.join(static_folder, filename))
if not file_path.startswith(static_folder):
abort(400) # Bad request, attempted traversal
if os.path.isdir(file_path):
abort(405)

@app.context_processor
def inject_google_tag_manager():
"""Inject Google Tag Manager ID into template context."""
return {
'google_tag_manager_id': os.getenv("GOOGLE_TAG_MANAGER_ID")
}
raise exc

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

Exception handler can mask errors and risk recursion; delegate HTTPException and target OAuth explicitly.

Catching Exception broadly, then “raise exc” for HTTPException can lead to unclear control flow and potential recursion. Also, substring checks (“oauth”/“token”) are brittle. Use FastAPI’s default HTTPException handler and a targeted OAuthError handler.

-    @app.exception_handler(Exception)
-    async def handle_oauth_error(request: Request, exc: Exception):
-        """Handle OAuth-related errors gracefully"""
-        # Check if it's an OAuth-related error
-        if "token" in str(exc).lower() or "oauth" in str(exc).lower():
-            logging.warning("OAuth error occurred: %s", exc)
-            request.session.clear()
-            return RedirectResponse(url="/", status_code=302)
-
-        # If it's an HTTPException, re-raise so FastAPI handles it properly
-        if isinstance(exc, HTTPException):
-            raise exc
-
-        # For other errors, let them bubble up
-        raise exc
+    @app.exception_handler(HTTPException)
+    async def passthrough_http_exception(request: Request, exc: HTTPException):
+        # Delegate to FastAPI's default handler to avoid recursion
+        return await http_exception_handler(request, exc)
+
+    @app.exception_handler(Exception)
+    async def handle_oauth_error(request: Request, exc: Exception):
+        """Handle OAuth-related errors gracefully without masking unrelated errors."""
+        if isinstance(exc, OAuthError) or "oauth" in str(exc).lower() or "token" in str(exc).lower():
+            logging.warning("OAuth error occurred: %s", exc)
+            request.session.clear()
+            return RedirectResponse(url="/", status_code=302)
+        # Bubble up everything else
+        raise exc

Add these imports near the top of the file:

from fastapi.exception_handlers import http_exception_handler
from authlib.integrations.starlette_client import OAuthError
🤖 Prompt for AI Agents
In api/app_factory.py around lines 80 to 95, the current broad Exception handler
uses brittle substring checks and can mask or recurse on HTTPException; replace
it with a specific handler for OAuthError that logs the exception, clears the
session and returns a RedirectResponse, and remove the global Exception catch so
FastAPI’s default HTTPException handling is preserved by delegating
HTTPException to fastapi.exception_handlers.http_exception_handler; also add the
two imports from fastapi.exception_handlers and
authlib.integrations.starlette_client at the top of the file as suggested.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

♻️ Duplicate comments (1)
api/app_factory.py (1)

80-95: Replace broad Exception handler with targeted handlers to avoid recursion and brittle matching.

Catching Exception and re-raising HTTPException here will re-enter the same handler and can recurse; substring checks for "oauth"/"token" are brittle. Delegate HTTPException to FastAPI’s default handler and handle OAuthError explicitly.

Apply this diff:

-    @app.exception_handler(Exception)
-    async def handle_oauth_error(request: Request, exc: Exception):
-        """Handle OAuth-related errors gracefully"""
-        # Check if it's an OAuth-related error
-        if "token" in str(exc).lower() or "oauth" in str(exc).lower():
-            logging.warning("OAuth error occurred: %s", exc)
-            request.session.clear()
-            return RedirectResponse(url="/", status_code=302)
-
-        # If it's an HTTPException, re-raise so FastAPI handles it properly
-        if isinstance(exc, HTTPException):
-            raise exc
-
-        # For other errors, let them bubble up
-        raise exc
+    @app.exception_handler(HTTPException)
+    async def passthrough_http_exception(request: Request, exc: HTTPException):
+        # Delegate to FastAPI's default handler to avoid recursion
+        return await http_exception_handler(request, exc)
+
+    @app.exception_handler(OAuthError)
+    async def handle_oauth_error(request: Request, exc: OAuthError):
+        """Handle OAuth-related errors gracefully without masking unrelated errors."""
+        logging.warning("OAuth error occurred: %s", exc)
+        request.session.clear()
+        return RedirectResponse(url="/", status_code=302)

Add these imports near the top of the file (see separate comment on Lines 8-13).

+from fastapi.exception_handlers import http_exception_handler
+from authlib.integrations.starlette_client import OAuthError

Run to ensure no catch-all Exception handlers remain:

#!/bin/bash
rg -nP "@app\.exception_handler\(\s*Exception\s*\)" -C2 api/app_factory.py
🧹 Nitpick comments (4)
api/app_factory.py (4)

49-53: Use generic env var names for the session secret; improve warning.

“FLASK_SECRET_KEY” is legacy. Accept SESSION_SECRET and SECRET_KEY first and keep FLASK_SECRET_KEY as a fallback. Log only when we actually generate a random key.

-    secret_key = os.getenv("FLASK_SECRET_KEY")
-    if not secret_key:
-        secret_key = secrets.token_hex(32)
-    logging.warning("FLASK_SECRET_KEY not set, using generated key. Set this in production!")
+    secret_key = (
+        os.getenv("SESSION_SECRET")
+        or os.getenv("SECRET_KEY")
+        or os.getenv("FLASK_SECRET_KEY")
+    )
+    if not secret_key:
+        secret_key = secrets.token_hex(32)
+        logging.warning(
+            "No session secret set (SESSION_SECRET/SECRET_KEY/FLASK_SECRET_KEY); "
+            "generated a random key for development. Configure a persistent secret in production!"
+        )

55-62: Make cookie security configurable; default to non-secure for localhost only.

Allow overriding https_only via env so production can set secure cookies without code changes.

-    app.add_middleware(
+    secure_cookie = os.getenv("SESSION_COOKIE_SECURE", "false").lower() == "true"
+    app.add_middleware(
         SessionMiddleware,
         secret_key=secret_key,
         session_cookie="qw_session",
         same_site="lax",  # allow top-level OAuth GET redirects to send cookies
-        https_only=False,  # allow http on localhost in development
+        https_only=secure_cookie,  # set SESSION_COOKIE_SECURE=true in prod
         max_age=60 * 60 * 24 * 14,  # 14 days - measured by seconds
     )

68-71: Prefer pathlib for static path resolution; avoid string joins with '..'.

Pathlib improves readability and cross-platform safety.

-    static_path = os.path.join(os.path.dirname(__file__), "../app/public")
-    if os.path.exists(static_path):
-        app.mount("/static", StaticFiles(directory=static_path), name="static")
+    static_path = Path(__file__).resolve().parent.parent / "app" / "public"
+    if static_path.exists():
+        app.mount("/static", StaticFiles(directory=str(static_path)), name="static")

96-101: Avoid per-request env lookups; cache GTM ID on app.state.

Minor perf/readability tweak.

-    # Add template globals
+    # Add template globals
+    # Cache once on startup
+    app.state.google_tag_manager_id = os.getenv("GOOGLE_TAG_MANAGER_ID")
     @app.middleware("http")
     async def add_template_globals(request: Request, call_next):
-        request.state.google_tag_manager_id = os.getenv("GOOGLE_TAG_MANAGER_ID")
+        request.state.google_tag_manager_id = app.state.google_tag_manager_id
         response = await call_next(request)
         return response
📜 Review details

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

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between eeea600 and e59adbf.

📒 Files selected for processing (3)
  • Makefile (2 hunks)
  • api/app_factory.py (1 hunks)
  • tests/test_simple_integration.py (2 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • tests/test_simple_integration.py
  • Makefile
🧰 Additional context used
📓 Path-based instructions (1)
api/app_factory.py

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Implement the application factory (including OAuth setup) in api/app_factory.py

Files:

  • api/app_factory.py
🧠 Learnings (1)
📚 Learning: 2025-08-19T08:18:37.470Z
Learnt from: CR
PR: FalkorDB/QueryWeaver#0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-08-19T08:18:37.470Z
Learning: Applies to api/app_factory.py : Implement the application factory (including OAuth setup) in api/app_factory.py

Applied to files:

  • api/app_factory.py
🧬 Code graph analysis (1)
api/app_factory.py (1)
api/routes/auth.py (1)
  • init_auth (273-307)
🪛 GitHub Actions: Pylint
api/app_factory.py

[error] 46-46: Pylint: Line too long (C0301).


[error] 23-23: Pylint: Too few public methods (R0903).

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: e2e-tests
🔇 Additional comments (1)
api/app_factory.py (1)

72-78: Routers and OAuth initialization are wired correctly.

init_auth(app) and the include_router calls align with the intended FastAPI migration and meet the app-factory objective.

Comment on lines +8 to +13
from fastapi import FastAPI, Request, HTTPException
from fastapi.responses import RedirectResponse, JSONResponse
from fastapi.staticfiles import StaticFiles
from starlette.middleware.sessions import SessionMiddleware
from starlette.middleware.base import BaseHTTPMiddleware

Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Add missing imports for exception handling helpers.

Required for the targeted handlers suggested below.

 from fastapi import FastAPI, Request, HTTPException
 from fastapi.responses import RedirectResponse, JSONResponse
 from fastapi.staticfiles import StaticFiles
+from fastapi.exception_handlers import http_exception_handler
 from starlette.middleware.sessions import SessionMiddleware
 from starlette.middleware.base import BaseHTTPMiddleware
+from authlib.integrations.starlette_client import OAuthError
+from pathlib import Path

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

🤖 Prompt for AI Agents
In api/app_factory.py around lines 8 to 13, the module is missing exception
helper imports used by the handlers; add the appropriate imports (for example
RequestValidationError from fastapi.exceptions and Starlette's HTTPException
alias if needed) at the top of the file so the exception handlers referenced
later have the correct symbols available.

Comment on lines +23 to +41
"""Middleware for security checks including static file access"""

STATIC_PREFIX = '/static/'

async def dispatch(self, request: Request, call_next):
# Block directory access in static files
if request.url.path.startswith(self.STATIC_PREFIX):
# Remove /static/ prefix to get the actual path
filename = request.url.path[len(self.STATIC_PREFIX):]
# Basic security check for directory traversal
if not filename or '../' in filename or filename.endswith('/'):
return JSONResponse(
status_code=403,
content={"detail": "Forbidden"}
)

response = await call_next(request)
return response

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

Address Pylint R0903 and reconsider custom static guard.

  • R0903 “Too few public methods” on this lightweight middleware breaks the pipeline. Add an inline disable or convert to function-based middleware.
  • StaticFiles already mitigates directory traversal; duplicating checks here risks false positives (e.g., blocking /static/ with 403) and adds maintenance overhead.

Minimal pipeline fix:

-class SecurityMiddleware(BaseHTTPMiddleware):
+class SecurityMiddleware(BaseHTTPMiddleware):  # pylint: disable=R0903

Optional: remove this class and rely on StaticFiles’ built-in protections, or reimplement as a function middleware colocated near the static mount.

📝 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
class SecurityMiddleware(BaseHTTPMiddleware):
"""Middleware for security checks including static file access"""
STATIC_PREFIX = '/static/'
async def dispatch(self, request: Request, call_next):
# Block directory access in static files
if request.url.path.startswith(self.STATIC_PREFIX):
# Remove /static/ prefix to get the actual path
filename = request.url.path[len(self.STATIC_PREFIX):]
# Basic security check for directory traversal
if not filename or '../' in filename or filename.endswith('/'):
return JSONResponse(
status_code=403,
content={"detail": "Forbidden"}
)
response = await call_next(request)
return response
class SecurityMiddleware(BaseHTTPMiddleware): # pylint: disable=R0903
"""Middleware for security checks including static file access"""
STATIC_PREFIX = '/static/'
async def dispatch(self, request: Request, call_next):
# Block directory access in static files
if request.url.path.startswith(self.STATIC_PREFIX):
# Remove /static/ prefix to get the actual path
filename = request.url.path[len(self.STATIC_PREFIX):]
# Basic security check for directory traversal
if not filename or '../' in filename or filename.endswith('/'):
return JSONResponse(
status_code=403,
content={"detail": "Forbidden"}
)
response = await call_next(request)
return response
🧰 Tools
🪛 GitHub Actions: Pylint

[error] 23-23: Pylint: Too few public methods (R0903).

app.secret_key = secrets.token_hex(32)
"""Create and configure the FastAPI application."""
app = FastAPI(title="QueryWeaver", description="Text2SQL with Graph-Powered Schema Understanding")

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

Fix Pylint C0301: split long FastAPI constructor line.

This line is flagged and breaks the pipeline. Split into multiple lines.

-    app = FastAPI(title="QueryWeaver", description="Text2SQL with Graph-Powered Schema Understanding")
+    app = FastAPI(
+        title="QueryWeaver",
+        description="Text2SQL with Graph-Powered Schema Understanding",
+    )
📝 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
app = FastAPI(title="QueryWeaver", description="Text2SQL with Graph-Powered Schema Understanding")
app = FastAPI(
title="QueryWeaver",
description="Text2SQL with Graph-Powered Schema Understanding",
)
🧰 Tools
🪛 GitHub Actions: Pylint

[error] 46-46: Pylint: Line too long (C0301).

🤖 Prompt for AI Agents
In api/app_factory.py around line 46, the single long FastAPI(...) call exceeds
the line length (Pylint C0301); break the constructor into multiple lines by
putting each keyword argument on its own line (e.g., title=...,
description=...), align the closing parenthesis on its own line, and ensure
indentation matches project style so the call stays within the line-length
limit.

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

🧹 Nitpick comments (5)
.github/workflows/e2e-tests.yml (5)

40-44: Node.js setup: consider pinning strategy and enabling cache

  • Using a major-only version ('22') is okay, but CI reproducibility improves if you either pin a minor (e.g., 22.x) or track LTS automatically.
  • If the Makefile invokes npm/pnpm/yarn, enable actions/setup-node caching for faster runs.

Apply one of these diffs:

Option A (track LTS automatically; add npm cache):

-    - name: Set up Node.js
-      uses: actions/setup-node@v4
-      with:
-        node-version: '22'
+    - name: Set up Node.js
+      uses: actions/setup-node@v4
+      with:
+        node-version: 'lts/*'
+        cache: 'npm'

Option B (pin minor; add npm cache):

-        node-version: '22'
+        node-version: '22.x'
+        cache: 'npm'

If you use pnpm or yarn under the hood, switch cache: to 'pnpm' or 'yarn' accordingly.


45-48: Make vs. pipenv: drop unused pipenv setup if Makefile doesn’t use it

You install pipenv earlier (Lines 35–39) but the subsequent steps only call make targets. If make install handles Python deps without pipenv, consider removing the pipenv installation to simplify CI and cut time.

Outside the selected lines, you can remove the pipenv step:

-    - name: Install pipenv
-      run: |
-        python -m pip install --upgrade pip
-        pip install pipenv

If make install actually shells out to pipenv, keep it and please ignore this.


49-52: Dev setup via Makefile: good consolidation; consider caching Python deps

Consolidating environment setup behind make setup-dev is 👍. For speed, you can enable pip cache in setup-python (if you rely on requirements.txt/constraints).

Example (outside selected lines, augment setup-python step):

-    - name: Set up Python
-      uses: actions/setup-python@v5
-      with:
-        python-version: '3.12'
+    - name: Set up Python
+      uses: actions/setup-python@v5
+      with:
+        python-version: '3.12'
+        cache: 'pip'
+        cache-dependency-path: |
+          requirements*.txt
+          **/requirements*.txt

Adjust cache-dependency-path to whatever your Makefile consumes.


18-25: Avoid 'latest' tag for FalkorDB to keep CI deterministic

Pin the service image to a specific version or digest to prevent surprise breakages when 'latest' updates.

-        image: falkordb/falkordb:latest
+        image: falkordb/falkordb:3.9.0

Replace 3.9.0 with the version you support, or use a sha256 digest for full reproducibility.


61-67: Bound the wait loop to avoid hanging jobs

The wait loop retries indefinitely. Add a max timeout/attempts so failed startups don’t hang CI.

-        until docker exec "$(docker ps -q --filter ancestor=falkordb/falkordb:latest)" redis-cli ping; do
-          echo "Waiting for FalkorDB..."
-          sleep 2
-        done
+        SECS=0
+        until docker exec "$(docker ps -q --filter ancestor=falkordb/falkordb:latest)" redis-cli ping; do
+          echo "Waiting for FalkorDB..."
+          sleep 2
+          SECS=$((SECS+2))
+          if [ "$SECS" -ge 120 ]; then
+            echo "Timeout waiting for FalkorDB"
+            exit 1
+          fi
+        done

Optionally, consider docker compose with healthcheck and depends_on: condition: service_healthy for cleaner orchestration.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between cc71bd9 and 8d575df.

📒 Files selected for processing (2)
  • .github/workflows/e2e-tests.yml (2 hunks)
  • Makefile (2 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • Makefile
🧰 Additional context used
📓 Path-based instructions (1)
.github/workflows/e2e-tests.yml

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Use a dedicated E2E CI workflow in .github/workflows/e2e-tests.yml

Files:

  • .github/workflows/e2e-tests.yml
🧠 Learnings (4)
📚 Learning: 2025-08-19T08:18:37.470Z
Learnt from: CR
PR: FalkorDB/QueryWeaver#0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-08-19T08:18:37.470Z
Learning: Applies to .github/workflows/e2e-tests.yml : Use a dedicated E2E CI workflow in .github/workflows/e2e-tests.yml

Applied to files:

  • .github/workflows/e2e-tests.yml
📚 Learning: 2025-08-19T08:18:37.470Z
Learnt from: CR
PR: FalkorDB/QueryWeaver#0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-08-19T08:18:37.470Z
Learning: Applies to tests/e2e/test_*.py : Place Playwright E2E tests as tests/e2e/test_*.py

Applied to files:

  • .github/workflows/e2e-tests.yml
📚 Learning: 2025-08-19T08:18:37.470Z
Learnt from: CR
PR: FalkorDB/QueryWeaver#0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-08-19T08:18:37.470Z
Learning: Applies to tests/e2e/README.md : Document E2E testing procedures in tests/e2e/README.md

Applied to files:

  • .github/workflows/e2e-tests.yml
📚 Learning: 2025-08-19T08:18:37.470Z
Learnt from: CR
PR: FalkorDB/QueryWeaver#0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-08-19T08:18:37.470Z
Learning: Applies to setup_e2e_tests.sh : Provide and maintain the E2E setup script at setup_e2e_tests.sh

Applied to files:

  • .github/workflows/e2e-tests.yml
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: e2e-tests
🔇 Additional comments (3)
.github/workflows/e2e-tests.yml (3)

1-1: Compliant: Dedicated E2E workflow path used as requested

This workflow lives at .github/workflows/e2e-tests.yml, matching the guideline to keep E2E CI in a dedicated workflow file.


53-60: Env file still uses FLASK_ after FastAPI migration—confirm or rename*

The workflow echoes FLASK_SECRET_KEY and FLASK_DEBUG. Given the PR migrates to FastAPI, verify whether these names are still read by the app/tests or should be replaced (e.g., APP_SECRET_KEY, DEBUG, or FASTAPI_*).

If they’re obsolete, adjust:

-        echo "FLASK_SECRET_KEY=test-secret-key-for-ci" >> .env
-        echo "FLASK_DEBUG=False" >> .env
+        echo "APP_SECRET_KEY=test-secret-key-for-ci" >> .env
+        echo "DEBUG=False" >> .env

Only change if the application actually expects the new names.


68-72: LGTM: make test-e2e entry point

Good consolidation—delegating to Makefile keeps CI and local runs aligned. Ensure the target emits artifacts (e.g., JUnit XML) that match the upload steps on failure.

If not already, you can have the target write JUnit XML to a consistent path and upload it similarly to screenshots for easier debugging.

@gkorland gkorland merged commit 2d8b143 into main Aug 21, 2025
12 of 14 checks passed
This was referenced Aug 21, 2025
This was referenced Aug 29, 2025
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