Skip to content

Commit eb9fc95

Browse files
fregataaclaude
andcommitted
refactor(BA-5737): fold project_id into CreateVFolderInProjectInput
Align with the TerminateSessionsInProjectInput precedent: the *InProject input DTO carries project_id as a field, and the adapter takes a single DTO (no separate project_id parameter). GQL mutation drops the standalone projectId argument; REST handler overrides body.project_id with the URL path segment so the URL remains authoritative. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 1a03403 commit eb9fc95

8 files changed

Lines changed: 25 additions & 14 deletions

File tree

src/ai/backend/client/cli/v2/vfolder/commands.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,7 @@ def project_create(
187187
from ai.backend.common.dto.manager.v2.vfolder.types import VFolderUsageMode
188188

189189
input_dto = CreateVFolderInProjectInput(
190+
project_id=project_id,
190191
name=name,
191192
usage_mode=VFolderUsageMode(usage_mode),
192193
host=host,
@@ -196,7 +197,7 @@ def project_create(
196197
async def _run() -> None:
197198
registry = await create_v2_registry(load_v2_config())
198199
try:
199-
result = await registry.vfolder.create_in_project(project_id, input_dto)
200+
result = await registry.vfolder.create_in_project(input_dto)
200201
print_result(result)
201202
finally:
202203
await registry.close()

src/ai/backend/client/v2/domains_v2/vfolder.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -81,13 +81,15 @@ async def create(self, request: CreateVFolderInput) -> CreateVFolderPayload:
8181

8282
async def create_in_project(
8383
self,
84-
project_id: UUID,
8584
request: CreateVFolderInProjectInput,
8685
) -> CreateVFolderPayload:
87-
"""Create a vfolder owned by a project."""
86+
"""Create a vfolder owned by a project.
87+
88+
The target project is read from ``request.project_id``.
89+
"""
8890
return await self._client.typed_request(
8991
"POST",
90-
f"{_PATH}/projects/{project_id}",
92+
f"{_PATH}/projects/{request.project_id}",
9193
request=request,
9294
response_model=CreateVFolderPayload,
9395
)

src/ai/backend/common/dto/manager/v2/vfolder/request.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -88,10 +88,13 @@ def strip_and_validate_name(cls, v: object) -> object:
8888
class CreateVFolderInProjectInput(BaseRequestModel):
8989
"""Input for creating a virtual folder within a project.
9090
91-
Unlike ``CreateVFolderInput``, ``project_id`` is supplied as a separate
92-
mutation/path argument so this model contains only the payload fields.
91+
The project the vfolder belongs to is identified by ``project_id``. In
92+
transport layers where the scope is expressed separately (REST path
93+
segment, GraphQL scope argument), that value is injected into this
94+
field before the DTO reaches the adapter.
9395
"""
9496

97+
project_id: UUID = Field(description="Project UUID that owns the vfolder")
9598
name: VFolderName = Field(description="VFolder name")
9699
host: str | None = Field(default=None, description="Storage host for the vfolder")
97100
usage_mode: VFolderUsageMode = Field(

src/ai/backend/manager/api/adapters/vfolder.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -369,10 +369,9 @@ async def create(self, input: CreateVFolderInput) -> CreateVFolderPayload:
369369

370370
async def create_in_project(
371371
self,
372-
project_id: UUID,
373372
input: CreateVFolderInProjectInput,
374373
) -> CreateVFolderPayload:
375-
"""Create a vfolder owned by ``project_id``.
374+
"""Create a vfolder owned by ``input.project_id``.
376375
377376
Uses ``CreateVFolderInProjectAction`` which is PROJECT-scoped so the
378377
caller must hold CREATE permission on the project.
@@ -381,7 +380,7 @@ async def create_in_project(
381380
if me is None:
382381
raise UnreachableError("User context is not available")
383382
action = CreateVFolderInProjectAction(
384-
project_id=project_id,
383+
project_id=input.project_id,
385384
user_id=me.user_id,
386385
domain_name=me.domain_name,
387386
name=input.name,

src/ai/backend/manager/api/gql/vfolder_v2/resolver/mutation.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -258,11 +258,10 @@ async def bulk_delete_vfolders_v2(
258258
) # type: ignore[misc]
259259
async def create_vfolder_in_project(
260260
info: Info[StrawberryGQLContext],
261-
project_id: UUID,
262261
input: CreateVFolderInProjectInputGQL,
263262
) -> CreateVFolderPayloadGQL:
264263
"""Create a new virtual folder scoped to a project."""
265-
payload = await info.context.adapters.vfolder.create_in_project(project_id, input.to_pydantic())
264+
payload = await info.context.adapters.vfolder.create_in_project(input.to_pydantic())
266265
return CreateVFolderPayloadGQL.from_pydantic(payload)
267266

268267

src/ai/backend/manager/api/gql/vfolder_v2/types/mutations.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,7 @@ class CreateVFolderInputGQL(PydanticInputMixin[CreateInputDTO]):
138138
name="CreateVFolderInProjectInput",
139139
)
140140
class CreateVFolderInProjectInputGQL(PydanticInputMixin[CreateInProjectInputDTO]):
141+
project_id: UUID = gql_field(description="Project UUID that owns the vfolder.")
141142
name: str = gql_field(description="VFolder name.")
142143
host: str | None = gql_field(default=None, description="Storage host for the vfolder.")
143144
usage_mode: VFolderUsageModeGQL = gql_field(

src/ai/backend/manager/api/rest/v2/vfolder/handler.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -112,8 +112,13 @@ async def project_create(
112112
path: PathParam[ProjectIdPathParam],
113113
body: BodyParam[CreateVFolderInProjectInput],
114114
) -> APIResponse:
115-
"""Create a vfolder owned by a project."""
116-
result = await self._adapter.create_in_project(path.parsed.project_id, body.parsed)
115+
"""Create a vfolder owned by a project.
116+
117+
The path segment ``{project_id}`` is authoritative; any ``project_id``
118+
value in the body is overridden to match the URL.
119+
"""
120+
dto = body.parsed.model_copy(update={"project_id": path.parsed.project_id})
121+
result = await self._adapter.create_in_project(dto)
117122
return APIResponse.build(status_code=HTTPStatus.CREATED, response_model=result)
118123

119124
async def deploy(

tests/component/vfolder_v2/test_vfolder_mutation.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -252,11 +252,12 @@ async def test_regular_user_denied(
252252
) -> None:
253253
"""Regular user without project CREATE permission is denied before service runs."""
254254
request = CreateVFolderInProjectInput(
255+
project_id=group_fixture,
255256
name=f"rbac-denied-{secrets.token_hex(4)}",
256257
host="local",
257258
usage_mode=VFolderUsageMode.GENERAL,
258259
permission=VFolderPermissionField.READ_WRITE,
259260
cloneable=False,
260261
)
261262
with pytest.raises(PermissionDeniedError):
262-
await user_v2_registry.vfolder.create_in_project(group_fixture, request)
263+
await user_v2_registry.vfolder.create_in_project(request)

0 commit comments

Comments
 (0)