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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .jules/bolt.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,3 +60,6 @@ Optimized metric route processing to O(N) by creating a mapping of routes direct
## 2026-06-25 - Avoid Map allocations in frontend ERD loops and mutate asyncpg records in-place
**Learning:** The frontend `snapshotToGraph` iterates over thousands of columns to generate the graph, so repeated lookups and redundant collection assignments increase GC pressure. Backend snapshot column dictionaries are freshly instantiated for the payload, so `add_column_examples` can safely fill missing fields in place.
**Action:** Reuse existing collections while aggregating relational data, create `Map`/`Set` entries only on first use, and check for missing example fields before calling expensive inference helpers.
## 2026-07-05 - Optimize Node Search Garbage Collection
**Learning:** High-frequency React Flow hooks (like searching through large sets of nodes/columns) can cause severe garbage collection pressure and rendering hitches if they use array allocation methods (`.flatMap()`, `.join()`) inside render loops.
**Action:** Replace array mapping and spreading with direct string concatenation (`+`) for string building inside frequent loops or React `useMemo` hooks to significantly reduce allocations and avoid GC jank.
19 changes: 13 additions & 6 deletions backend/app/api/share.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
SchemaSnapshotData,
ShareLink,
)
from app.redact import redact_sensitive_schema_data
from app.spec.llm import (
LlmConfigurationError,
LlmProviderError,
Expand Down Expand Up @@ -124,7 +125,7 @@ async def get_shared_snapshot(
"status": snap.status,
"schema_filter": snap.schema_filter,
"error_message": snap.error_message,
"snapshot_json": data.snapshot_json if data else None,
"snapshot_json": redact_sensitive_schema_data(data.snapshot_json) if data else None,
}


Expand Down Expand Up @@ -154,7 +155,9 @@ async def export_shared_snapshot_sql(
data = await session.get(SchemaSnapshotData, schema_snapshot_uuid)
if data is None:
return "-- snapshot data not found\n"
return snapshot_json_to_sql(data.snapshot_json, target_dialect=dialect)

redacted_json = redact_sensitive_schema_data(data.snapshot_json)
return snapshot_json_to_sql(redacted_json, target_dialect=dialect)


@router.get(
Expand Down Expand Up @@ -183,9 +186,11 @@ async def export_shared_snapshot_reversing_spec(
data = await session.get(SchemaSnapshotData, schema_snapshot_uuid)
if data is None:
return "# DB Reversing Specification\n\nSnapshot data not found.\n"

redacted_json = redact_sensitive_schema_data(data.snapshot_json)
if mode == "llm-draft":
try:
return await generate_reversing_llm_draft(data.snapshot_json)
return await generate_reversing_llm_draft(redacted_json)
except LlmConfigurationError as exc:
raise HTTPException(
status_code=503, detail="LLM configuration error"
Expand All @@ -194,7 +199,7 @@ async def export_shared_snapshot_reversing_spec(
raise HTTPException(
status_code=502, detail="LLM provider request failed"
) from exc
return generate_reversing_spec(data.snapshot_json, mode=mode)
return generate_reversing_spec(redacted_json, mode=mode)


@router.get(
Expand Down Expand Up @@ -223,9 +228,11 @@ async def export_shared_snapshot_index_design(
data = await session.get(SchemaSnapshotData, schema_snapshot_uuid)
if data is None:
return "# ERD Index Design\n\nSnapshot data not found.\n"

redacted_json = redact_sensitive_schema_data(data.snapshot_json)
if mode == "llm-draft":
try:
return await generate_index_design_llm_draft(data.snapshot_json)
return await generate_index_design_llm_draft(redacted_json)
except LlmConfigurationError as exc:
raise HTTPException(
status_code=503, detail="LLM configuration error"
Expand All @@ -234,4 +241,4 @@ async def export_shared_snapshot_index_design(
raise HTTPException(
status_code=502, detail="LLM provider request failed"
) from exc
return generate_index_design_spec(data.snapshot_json, mode=mode)
return generate_index_design_spec(redacted_json, mode=mode)
24 changes: 24 additions & 0 deletions backend/app/redact.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import copy
from typing import Any, Dict


def redact_sensitive_schema_data(snapshot_json: Dict[str, Any] | None) -> Dict[str, Any]:
"""Redacts sensitive properties like comments or example values from snapshot data."""
if not snapshot_json:
return {}

redacted = copy.deepcopy(snapshot_json)

if "tables" in redacted and isinstance(redacted["tables"], list):
for table in redacted["tables"]:
if "comment" in table and table["comment"] is not None:
table["comment"] = "[REDACTED]"

if "columns" in table and isinstance(table["columns"], list):
for column in table["columns"]:
if "column_comment" in column and column["column_comment"] is not None:
column["column_comment"] = "[REDACTED]"
if "example_value" in column and column["example_value"] is not None:
column["example_value"] = "[REDACTED]"

return redacted
Empty file.
Empty file.
15 changes: 15 additions & 0 deletions backend/build/lib/app/api/auth_routes.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
from __future__ import annotations

from fastapi import APIRouter, Request

from app.auth import revoke_current_request_token

router = APIRouter(prefix="/api/auth", tags=["auth"])


@router.post("/logout")
async def logout(request: Request) -> dict[str, bool]:
"""Invalidate the current bearer token for this app process."""

await revoke_current_request_token(request)
return {"ok": True}
64 changes: 64 additions & 0 deletions backend/build/lib/app/api/connections.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
from __future__ import annotations

import datetime as dt
import uuid

from fastapi import APIRouter, Depends
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession

from app.auth import CurrentUser, get_current_user
from app.db import get_read_session, get_session
from app.models import DbConnection
from app.permissions import require_project_member
from app.schemas import ConnectionCreateIn, ConnectionOut
from app.security import encrypt_text
from app.sanitize import sanitize_for_storage

router = APIRouter(prefix="/api/connections", tags=["connections"])


@router.get("/by-project/{project_space_uuid}", response_model=list[ConnectionOut])
async def list_connections(
project_space_uuid: uuid.UUID,
user: CurrentUser = Depends(get_current_user),
session: AsyncSession = Depends(get_read_session),
) -> list[ConnectionOut]:
"""List DB connections for a project."""
await require_project_member(session, project_space_uuid, user.user_account_uuid)
rows = await session.execute(
select(DbConnection)
.where(DbConnection.project_space_uuid == project_space_uuid)
.order_by(DbConnection.created_at.desc())
)
cons = rows.scalars().all()
return [
ConnectionOut(db_connection_uuid=c.db_connection_uuid, conn_name=c.conn_name)
for c in cons
]


@router.post("/by-project/{project_space_uuid}", response_model=ConnectionOut)
async def create_connection(
project_space_uuid: uuid.UUID,
body: ConnectionCreateIn,
user: CurrentUser = Depends(get_current_user),
session: AsyncSession = Depends(get_session),
) -> ConnectionOut:
"""Create a DB connection for a project (encrypt DSN at rest)."""
await require_project_member(
session, project_space_uuid, user.user_account_uuid, minimum_role="editor"
)
encrypted = encrypt_text(str(sanitize_for_storage(body.dsn)))
c = DbConnection(
db_connection_uuid=uuid.uuid4(),
project_space_uuid=project_space_uuid,
conn_name=str(sanitize_for_storage(body.conn_name)),
dsn_ciphertext=encrypted.ciphertext,
dsn_nonce=encrypted.nonce,
created_at=dt.datetime.now(dt.timezone.utc),
updated_at=dt.datetime.now(dt.timezone.utc),
)
session.add(c)
await session.commit()
return ConnectionOut(db_connection_uuid=c.db_connection_uuid, conn_name=c.conn_name)
18 changes: 18 additions & 0 deletions backend/build/lib/app/api/me.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
from __future__ import annotations

from fastapi import APIRouter, Depends

from app.auth import CurrentUser, get_current_user
from app.schemas import MeOut

router = APIRouter(prefix="/api", tags=["me"])


@router.get("/me", response_model=MeOut)
async def get_me(user: CurrentUser = Depends(get_current_user)) -> MeOut:
"""Return the current user's identity."""
return MeOut(
user_account_uuid=user.user_account_uuid,
subject=user.subject,
display_name=user.display_name,
)
Loading