fix: register export/import before parameterised ticket route
GET /api/tickets/export was being matched by GET /api/tickets/{ticket_id}
because FastAPI matches in registration order. Moved export/import routes
before the /{ticket_id} route so they are resolved first.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
9eef5fd195
commit
f354c0ea89
1 changed files with 120 additions and 122 deletions
242
corvid/router.py
242
corvid/router.py
|
|
@ -300,6 +300,126 @@ def make_router(api_key: str, get_db, require_user=None) -> APIRouter:
|
|||
await db.commit()
|
||||
return await _ticket_dict(await _get_ticket_or_404(db, t.id), db)
|
||||
|
||||
@router.get("/api/tickets/export")
|
||||
async def export_tickets(
|
||||
db: AsyncSession = Depends(get_db),
|
||||
_auth=Depends(require_ticket_auth),
|
||||
):
|
||||
"""Export all environments, tickets, and attachments as portable JSON."""
|
||||
env_rows = (await db.execute(select(Environment).order_by(Environment.id))).scalars().all()
|
||||
env_by_id = {e.id: e.prefix for e in env_rows}
|
||||
|
||||
ticket_rows = (await db.execute(
|
||||
select(Ticket).order_by(Ticket.id)
|
||||
)).scalars().all()
|
||||
|
||||
att_by_ticket: dict[int, list] = {}
|
||||
for t in ticket_rows:
|
||||
att_by_ticket[t.id] = [
|
||||
{
|
||||
'title': a.title,
|
||||
'content': a.content,
|
||||
'created_by': a.created_by,
|
||||
}
|
||||
for a in t.attachments
|
||||
]
|
||||
|
||||
return {
|
||||
'version': '1',
|
||||
'source': 'corvid',
|
||||
'exported_at': datetime.now(timezone.utc).isoformat(),
|
||||
'environments': [
|
||||
{
|
||||
'prefix': e.prefix,
|
||||
'name': e.name,
|
||||
'description': e.description,
|
||||
}
|
||||
for e in env_rows
|
||||
],
|
||||
'tickets': [
|
||||
{
|
||||
'_id': t.id,
|
||||
'title': t.title,
|
||||
'description': t.description,
|
||||
'status': t.status,
|
||||
'type': t.type,
|
||||
'user': t.user,
|
||||
'effort': t.effort,
|
||||
'_parent_id': t.parent_id,
|
||||
'environment_prefix': env_by_id.get(t.environment_id) if t.environment_id else None,
|
||||
'attachments': att_by_ticket.get(t.id, []),
|
||||
}
|
||||
for t in ticket_rows
|
||||
],
|
||||
}
|
||||
|
||||
@router.post("/api/tickets/import", status_code=201)
|
||||
async def import_tickets(
|
||||
body: dict,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
_auth=Depends(require_ticket_auth),
|
||||
):
|
||||
"""Import from a portable JSON export (additive — never overwrites existing records)."""
|
||||
if body.get('version') != '1':
|
||||
raise HTTPException(400, "Unsupported export version")
|
||||
|
||||
env_prefix_to_id: dict[str, int] = {}
|
||||
for e_data in body.get('environments', []):
|
||||
prefix = (e_data.get('prefix') or '').strip().upper()
|
||||
if not prefix:
|
||||
continue
|
||||
existing = (await db.execute(
|
||||
select(Environment).where(Environment.prefix == prefix)
|
||||
)).scalar_one_or_none()
|
||||
if existing:
|
||||
env_prefix_to_id[prefix] = existing.id
|
||||
else:
|
||||
new_env = Environment(
|
||||
prefix=prefix,
|
||||
name=e_data.get('name') or prefix,
|
||||
description=e_data.get('description'),
|
||||
)
|
||||
db.add(new_env)
|
||||
await db.flush()
|
||||
env_prefix_to_id[prefix] = new_env.id
|
||||
|
||||
old_to_new: dict[int, int] = {}
|
||||
for t_data in body.get('tickets', []):
|
||||
old_id = t_data.get('_id')
|
||||
env_prefix = t_data.get('environment_prefix')
|
||||
env_id = env_prefix_to_id.get(env_prefix) if env_prefix else None
|
||||
|
||||
old_parent = t_data.get('_parent_id')
|
||||
parent_id = old_to_new.get(int(old_parent)) if old_parent else None
|
||||
|
||||
t = Ticket(
|
||||
guid=str(uuid.uuid4()),
|
||||
title=t_data.get('title') or 'Imported Ticket',
|
||||
description=t_data.get('description'),
|
||||
status=t_data.get('status') or 'open',
|
||||
type=t_data.get('type') or 'feature',
|
||||
user=t_data.get('user') or 'kevin',
|
||||
effort=t_data.get('effort'),
|
||||
parent_id=parent_id,
|
||||
environment_id=env_id,
|
||||
)
|
||||
db.add(t)
|
||||
await db.flush()
|
||||
|
||||
if old_id is not None:
|
||||
old_to_new[int(old_id)] = t.id
|
||||
|
||||
for att in t_data.get('attachments', []):
|
||||
db.add(TicketAttachment(
|
||||
ticket_id=t.id,
|
||||
title=att.get('title') or 'Attachment',
|
||||
content=att.get('content') or '',
|
||||
created_by=att.get('created_by') or 'kevin',
|
||||
))
|
||||
|
||||
await db.commit()
|
||||
return {'ok': True, 'imported': len(old_to_new)}
|
||||
|
||||
@router.get("/api/tickets/{ticket_id}")
|
||||
async def get_ticket(
|
||||
ticket_id: int,
|
||||
|
|
@ -452,126 +572,4 @@ def make_router(api_key: str, get_db, require_user=None) -> APIRouter:
|
|||
await db.commit()
|
||||
return {"ok": True}
|
||||
|
||||
# ── Export / Import ───────────────────────────────────────────────────────
|
||||
|
||||
@router.get("/api/tickets/export")
|
||||
async def export_tickets(
|
||||
db: AsyncSession = Depends(get_db),
|
||||
_auth=Depends(require_ticket_auth),
|
||||
):
|
||||
"""Export all environments, tickets, and attachments as portable JSON."""
|
||||
env_rows = (await db.execute(select(Environment).order_by(Environment.id))).scalars().all()
|
||||
env_by_id = {e.id: e.prefix for e in env_rows}
|
||||
|
||||
ticket_rows = (await db.execute(
|
||||
select(Ticket).order_by(Ticket.id)
|
||||
)).scalars().all()
|
||||
|
||||
att_by_ticket: dict[int, list] = {}
|
||||
for t in ticket_rows:
|
||||
att_by_ticket[t.id] = [
|
||||
{
|
||||
'title': a.title,
|
||||
'content': a.content,
|
||||
'created_by': a.created_by,
|
||||
}
|
||||
for a in t.attachments
|
||||
]
|
||||
|
||||
return {
|
||||
'version': '1',
|
||||
'source': 'corvid',
|
||||
'exported_at': datetime.now(timezone.utc).isoformat(),
|
||||
'environments': [
|
||||
{
|
||||
'prefix': e.prefix,
|
||||
'name': e.name,
|
||||
'description': e.description,
|
||||
}
|
||||
for e in env_rows
|
||||
],
|
||||
'tickets': [
|
||||
{
|
||||
'_id': t.id,
|
||||
'title': t.title,
|
||||
'description': t.description,
|
||||
'status': t.status,
|
||||
'type': t.type,
|
||||
'user': t.user,
|
||||
'effort': t.effort,
|
||||
'_parent_id': t.parent_id,
|
||||
'environment_prefix': env_by_id.get(t.environment_id) if t.environment_id else None,
|
||||
'attachments': att_by_ticket.get(t.id, []),
|
||||
}
|
||||
for t in ticket_rows
|
||||
],
|
||||
}
|
||||
|
||||
@router.post("/api/tickets/import", status_code=201)
|
||||
async def import_tickets(
|
||||
body: dict,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
_auth=Depends(require_ticket_auth),
|
||||
):
|
||||
"""Import from a portable JSON export (additive — never overwrites existing records)."""
|
||||
if body.get('version') != '1':
|
||||
raise HTTPException(400, "Unsupported export version")
|
||||
|
||||
env_prefix_to_id: dict[str, int] = {}
|
||||
for e_data in body.get('environments', []):
|
||||
prefix = (e_data.get('prefix') or '').strip().upper()
|
||||
if not prefix:
|
||||
continue
|
||||
existing = (await db.execute(
|
||||
select(Environment).where(Environment.prefix == prefix)
|
||||
)).scalar_one_or_none()
|
||||
if existing:
|
||||
env_prefix_to_id[prefix] = existing.id
|
||||
else:
|
||||
new_env = Environment(
|
||||
prefix=prefix,
|
||||
name=e_data.get('name') or prefix,
|
||||
description=e_data.get('description'),
|
||||
)
|
||||
db.add(new_env)
|
||||
await db.flush()
|
||||
env_prefix_to_id[prefix] = new_env.id
|
||||
|
||||
old_to_new: dict[int, int] = {}
|
||||
for t_data in body.get('tickets', []):
|
||||
old_id = t_data.get('_id')
|
||||
env_prefix = t_data.get('environment_prefix')
|
||||
env_id = env_prefix_to_id.get(env_prefix) if env_prefix else None
|
||||
|
||||
old_parent = t_data.get('_parent_id')
|
||||
parent_id = old_to_new.get(int(old_parent)) if old_parent else None
|
||||
|
||||
t = Ticket(
|
||||
guid=str(uuid.uuid4()),
|
||||
title=t_data.get('title') or 'Imported Ticket',
|
||||
description=t_data.get('description'),
|
||||
status=t_data.get('status') or 'open',
|
||||
type=t_data.get('type') or 'feature',
|
||||
user=t_data.get('user') or 'kevin',
|
||||
effort=t_data.get('effort'),
|
||||
parent_id=parent_id,
|
||||
environment_id=env_id,
|
||||
)
|
||||
db.add(t)
|
||||
await db.flush()
|
||||
|
||||
if old_id is not None:
|
||||
old_to_new[int(old_id)] = t.id
|
||||
|
||||
for att in t_data.get('attachments', []):
|
||||
db.add(TicketAttachment(
|
||||
ticket_id=t.id,
|
||||
title=att.get('title') or 'Attachment',
|
||||
content=att.get('content') or '',
|
||||
created_by=att.get('created_by') or 'kevin',
|
||||
))
|
||||
|
||||
await db.commit()
|
||||
return {'ok': True, 'imported': len(old_to_new)}
|
||||
|
||||
return router
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue