Skip to content

Commit 1dac6a6

Browse files
MKY508claude
andcommitted
feat: add schema layout persistence with multi-view support
- Add independent SQLite metadata database for layout storage - Implement layout CRUD API endpoints (create, read, update, delete, duplicate) - Add multi-view support with view switching dropdown - Add table search and filtering functionality - Add auto-save for node positions with debounce - Enlarge connection handles for easier edge creation - Store layouts separately from main database (read-only principle) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 398ffbb commit 1dac6a6

File tree

9 files changed

+1097
-50
lines changed

9 files changed

+1097
-50
lines changed

apps/api/app/api/v1/schema.py

Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,17 @@
1010
from app.api.deps import get_current_user
1111
from app.core import encryptor
1212
from app.db import get_db
13+
from app.db.metadata import LayoutRepository
1314
from app.db.tables import Connection, TableRelationship, User
1415
from app.models import (
1516
APIResponse,
1617
ColumnInfo,
1718
RelationshipSuggestion,
1819
SchemaInfo,
20+
SchemaLayoutCreate,
21+
SchemaLayoutListItem,
22+
SchemaLayoutResponse,
23+
SchemaLayoutUpdate,
1924
TableInfo,
2025
TableRelationshipBatchCreate,
2126
TableRelationshipCreate,
@@ -330,3 +335,176 @@ async def delete_relationship(
330335
await db.commit()
331336

332337
return APIResponse.ok(message="关系删除成功")
338+
339+
340+
# ===== 布局管理 API (使用独立 SQLite 元数据库) =====
341+
342+
343+
@router.get(
344+
"/{connection_id}/layouts",
345+
response_model=APIResponse[list[SchemaLayoutListItem]],
346+
)
347+
async def get_layouts(
348+
connection_id: UUID,
349+
current_user: User = Depends(get_current_user),
350+
db: AsyncSession = Depends(get_db),
351+
):
352+
"""获取所有布局列表"""
353+
await _get_connection(connection_id, current_user, db)
354+
355+
layouts = LayoutRepository.list_layouts(current_user.id, connection_id)
356+
357+
return APIResponse.ok(data=[SchemaLayoutListItem(**layout) for layout in layouts])
358+
359+
360+
@router.post(
361+
"/{connection_id}/layouts",
362+
response_model=APIResponse[SchemaLayoutResponse],
363+
)
364+
async def create_layout(
365+
connection_id: UUID,
366+
data: SchemaLayoutCreate,
367+
current_user: User = Depends(get_current_user),
368+
db: AsyncSession = Depends(get_db),
369+
):
370+
"""创建新布局"""
371+
await _get_connection(connection_id, current_user, db)
372+
373+
# 检查名称是否重复
374+
if LayoutRepository.layout_name_exists(current_user.id, connection_id, data.name):
375+
raise HTTPException(
376+
status_code=status.HTTP_400_BAD_REQUEST,
377+
detail=f"布局名称 '{data.name}' 已存在",
378+
)
379+
380+
layout = LayoutRepository.create_layout(
381+
user_id=current_user.id,
382+
connection_id=connection_id,
383+
name=data.name,
384+
is_default=data.is_default,
385+
layout_data=data.layout_data,
386+
visible_tables=data.visible_tables,
387+
)
388+
389+
return APIResponse.ok(
390+
data=SchemaLayoutResponse(**layout),
391+
message="布局创建成功",
392+
)
393+
394+
395+
@router.get(
396+
"/{connection_id}/layouts/{layout_id}",
397+
response_model=APIResponse[SchemaLayoutResponse],
398+
)
399+
async def get_layout(
400+
connection_id: UUID,
401+
layout_id: UUID,
402+
current_user: User = Depends(get_current_user),
403+
db: AsyncSession = Depends(get_db),
404+
):
405+
"""获取单个布局"""
406+
await _get_connection(connection_id, current_user, db)
407+
408+
layout = LayoutRepository.get_layout(layout_id, current_user.id)
409+
410+
if not layout:
411+
raise HTTPException(
412+
status_code=status.HTTP_404_NOT_FOUND,
413+
detail="布局不存在",
414+
)
415+
416+
return APIResponse.ok(data=SchemaLayoutResponse(**layout))
417+
418+
419+
@router.put(
420+
"/{connection_id}/layouts/{layout_id}",
421+
response_model=APIResponse[SchemaLayoutResponse],
422+
)
423+
async def update_layout(
424+
connection_id: UUID,
425+
layout_id: UUID,
426+
data: SchemaLayoutUpdate,
427+
current_user: User = Depends(get_current_user),
428+
db: AsyncSession = Depends(get_db),
429+
):
430+
"""更新布局"""
431+
await _get_connection(connection_id, current_user, db)
432+
433+
# 检查布局是否存在
434+
existing = LayoutRepository.get_layout(layout_id, current_user.id)
435+
if not existing:
436+
raise HTTPException(
437+
status_code=status.HTTP_404_NOT_FOUND,
438+
detail="布局不存在",
439+
)
440+
441+
# 检查名称是否重复
442+
if data.name and data.name != existing["name"]:
443+
if LayoutRepository.layout_name_exists(
444+
current_user.id, connection_id, data.name, layout_id
445+
):
446+
raise HTTPException(
447+
status_code=status.HTTP_400_BAD_REQUEST,
448+
detail=f"布局名称 '{data.name}' 已存在",
449+
)
450+
451+
layout = LayoutRepository.update_layout(
452+
layout_id=layout_id,
453+
user_id=current_user.id,
454+
connection_id=connection_id,
455+
**data.model_dump(exclude_none=True),
456+
)
457+
458+
return APIResponse.ok(
459+
data=SchemaLayoutResponse(**layout),
460+
message="布局更新成功",
461+
)
462+
463+
464+
@router.delete(
465+
"/{connection_id}/layouts/{layout_id}",
466+
response_model=APIResponse,
467+
)
468+
async def delete_layout(
469+
connection_id: UUID,
470+
layout_id: UUID,
471+
current_user: User = Depends(get_current_user),
472+
db: AsyncSession = Depends(get_db),
473+
):
474+
"""删除布局"""
475+
await _get_connection(connection_id, current_user, db)
476+
477+
if not LayoutRepository.delete_layout(layout_id, current_user.id):
478+
raise HTTPException(
479+
status_code=status.HTTP_404_NOT_FOUND,
480+
detail="布局不存在",
481+
)
482+
483+
return APIResponse.ok(message="布局删除成功")
484+
485+
486+
@router.post(
487+
"/{connection_id}/layouts/{layout_id}/duplicate",
488+
response_model=APIResponse[SchemaLayoutResponse],
489+
)
490+
async def duplicate_layout(
491+
connection_id: UUID,
492+
layout_id: UUID,
493+
current_user: User = Depends(get_current_user),
494+
db: AsyncSession = Depends(get_db),
495+
):
496+
"""复制布局"""
497+
await _get_connection(connection_id, current_user, db)
498+
499+
new_layout = LayoutRepository.duplicate_layout(layout_id, current_user.id)
500+
501+
if not new_layout:
502+
raise HTTPException(
503+
status_code=status.HTTP_404_NOT_FOUND,
504+
detail="布局不存在",
505+
)
506+
507+
return APIResponse.ok(
508+
data=SchemaLayoutResponse(**new_layout),
509+
message="布局复制成功",
510+
)

0 commit comments

Comments
 (0)