feat: implement cascading delete for gateway and associated installed skills
This commit is contained in:
committed by
Abhimanyu Saharan
parent
577c0d2839
commit
da6cc2544b
@@ -14,6 +14,7 @@ from app.db import crud
|
|||||||
from app.db.pagination import paginate
|
from app.db.pagination import paginate
|
||||||
from app.db.session import get_session
|
from app.db.session import get_session
|
||||||
from app.models.agents import Agent
|
from app.models.agents import Agent
|
||||||
|
from app.models.gateway_installed_skills import GatewayInstalledSkill
|
||||||
from app.models.gateways import Gateway
|
from app.models.gateways import Gateway
|
||||||
from app.schemas.common import OkResponse
|
from app.schemas.common import OkResponse
|
||||||
from app.schemas.gateways import (
|
from app.schemas.gateways import (
|
||||||
@@ -175,6 +176,12 @@ async def delete_gateway(
|
|||||||
await service.clear_agent_foreign_keys(agent_id=agent.id)
|
await service.clear_agent_foreign_keys(agent_id=agent.id)
|
||||||
await session.delete(agent)
|
await session.delete(agent)
|
||||||
|
|
||||||
|
installed_skills = await GatewayInstalledSkill.objects.filter_by(
|
||||||
|
gateway_id=gateway.id,
|
||||||
|
).all(session)
|
||||||
|
for installed_skill in installed_skills:
|
||||||
|
await session.delete(installed_skill)
|
||||||
|
|
||||||
await session.delete(gateway)
|
await session.delete(gateway)
|
||||||
await session.commit()
|
await session.commit()
|
||||||
return OkResponse()
|
return OkResponse()
|
||||||
|
|||||||
@@ -73,6 +73,7 @@ def upgrade() -> None:
|
|||||||
sa.ForeignKeyConstraint(
|
sa.ForeignKeyConstraint(
|
||||||
["gateway_id"],
|
["gateway_id"],
|
||||||
["gateways.id"],
|
["gateways.id"],
|
||||||
|
ondelete="CASCADE",
|
||||||
),
|
),
|
||||||
sa.ForeignKeyConstraint(
|
sa.ForeignKeyConstraint(
|
||||||
["skill_id"],
|
["skill_id"],
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ from sqlmodel import SQLModel, col, select
|
|||||||
from sqlmodel.ext.asyncio.session import AsyncSession
|
from sqlmodel.ext.asyncio.session import AsyncSession
|
||||||
|
|
||||||
from app.api.deps import require_org_admin
|
from app.api.deps import require_org_admin
|
||||||
|
from app.api.gateways import router as gateways_router
|
||||||
from app.api.skills_marketplace import (
|
from app.api.skills_marketplace import (
|
||||||
PackSkillCandidate,
|
PackSkillCandidate,
|
||||||
_collect_pack_skills_from_repo,
|
_collect_pack_skills_from_repo,
|
||||||
@@ -44,6 +45,7 @@ def _build_test_app(
|
|||||||
) -> FastAPI:
|
) -> FastAPI:
|
||||||
app = FastAPI()
|
app = FastAPI()
|
||||||
api_v1 = APIRouter(prefix="/api/v1")
|
api_v1 = APIRouter(prefix="/api/v1")
|
||||||
|
api_v1.include_router(gateways_router)
|
||||||
api_v1.include_router(skills_marketplace_router)
|
api_v1.include_router(skills_marketplace_router)
|
||||||
app.include_router(api_v1)
|
app.include_router(api_v1)
|
||||||
|
|
||||||
@@ -171,6 +173,58 @@ async def test_install_skill_dispatches_instruction_and_persists_installation(
|
|||||||
await engine.dispose()
|
await engine.dispose()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_delete_gateway_removes_installed_skill_rows() -> None:
|
||||||
|
engine = await _make_engine()
|
||||||
|
session_maker = async_sessionmaker(
|
||||||
|
engine,
|
||||||
|
class_=AsyncSession,
|
||||||
|
expire_on_commit=False,
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
async with session_maker() as session:
|
||||||
|
organization, gateway = await _seed_base(session)
|
||||||
|
skill = MarketplaceSkill(
|
||||||
|
organization_id=organization.id,
|
||||||
|
name="Deploy Helper",
|
||||||
|
source_url="https://example.com/skills/deploy-helper.git",
|
||||||
|
)
|
||||||
|
session.add(skill)
|
||||||
|
await session.commit()
|
||||||
|
await session.refresh(skill)
|
||||||
|
session.add(
|
||||||
|
GatewayInstalledSkill(
|
||||||
|
gateway_id=gateway.id,
|
||||||
|
skill_id=skill.id,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
await session.commit()
|
||||||
|
|
||||||
|
app = _build_test_app(session_maker, organization=organization)
|
||||||
|
async with AsyncClient(
|
||||||
|
transport=ASGITransport(app=app),
|
||||||
|
base_url="http://testserver",
|
||||||
|
) as client:
|
||||||
|
response = await client.delete(f"/api/v1/gateways/{gateway.id}")
|
||||||
|
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert response.json() == {"ok": True}
|
||||||
|
|
||||||
|
async with session_maker() as session:
|
||||||
|
deleted_gateway = await session.get(Gateway, gateway.id)
|
||||||
|
assert deleted_gateway is None
|
||||||
|
remaining_installs = (
|
||||||
|
await session.exec(
|
||||||
|
select(GatewayInstalledSkill).where(
|
||||||
|
col(GatewayInstalledSkill.gateway_id) == gateway.id,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
).all()
|
||||||
|
assert remaining_installs == []
|
||||||
|
finally:
|
||||||
|
await engine.dispose()
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_list_marketplace_skills_marks_installed_cards() -> None:
|
async def test_list_marketplace_skills_marks_installed_cards() -> None:
|
||||||
engine = await _make_engine()
|
engine = await _make_engine()
|
||||||
|
|||||||
@@ -7,6 +7,6 @@ type EditMarketplaceSkillPageProps = {
|
|||||||
export default async function EditMarketplaceSkillPage({
|
export default async function EditMarketplaceSkillPage({
|
||||||
params,
|
params,
|
||||||
}: EditMarketplaceSkillPageProps) {
|
}: EditMarketplaceSkillPageProps) {
|
||||||
const { skillId } = await params;
|
await params;
|
||||||
redirect(`/skills/packs/${skillId}/edit`);
|
redirect("/skills/marketplace");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -80,9 +80,9 @@ const toPackLabel = (packUrl: string): string => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const toPackDetailHref = (packUrl: string): string => {
|
const toPacksHref = (packUrl: string): string => {
|
||||||
const params = new URLSearchParams({ source_url: packUrl });
|
const params = new URLSearchParams({ source_url: packUrl });
|
||||||
return `/skills/packs/detail?${params.toString()}`;
|
return `/skills/packs?${params.toString()}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function MarketplaceSkillsTable({
|
export function MarketplaceSkillsTable({
|
||||||
@@ -143,7 +143,7 @@ export function MarketplaceSkillsTable({
|
|||||||
const packUrl = toPackUrl(row.original.source_url);
|
const packUrl = toPackUrl(row.original.source_url);
|
||||||
return (
|
return (
|
||||||
<Link
|
<Link
|
||||||
href={toPackDetailHref(packUrl)}
|
href={toPacksHref(packUrl)}
|
||||||
className="inline-flex items-center gap-1 text-sm font-medium text-slate-700 hover:text-blue-600"
|
className="inline-flex items-center gap-1 text-sm font-medium text-slate-700 hover:text-blue-600"
|
||||||
>
|
>
|
||||||
{truncate(toPackLabel(packUrl), 40)}
|
{truncate(toPackLabel(packUrl), 40)}
|
||||||
|
|||||||
Reference in New Issue
Block a user