Skip to content

Commit 6c63a8d

Browse files
jopemachineclaude
andcommitted
feat(BA-5829): add AppConfig merged-view GQL (myAppConfigs, adminAppConfigs, publicAppConfigFragments)
Exposes the merged-view operations that BEP-1052 §5 / §3 require and which were only reachable from the repository layer until now: - `myAppConfigs(filter, orderBy, ...)` — caller's own merged AppConfig list (auth required). Adapter pins `(USER, current_user)` internally. - `adminAppConfigs(filter, orderBy, ...)` — cross-user admin search. Optional `filter.userId` pins to a single user; otherwise paginates across every user. - `publicAppConfigFragments(filter, orderBy, ...)` — raw `PUBLIC`-scope fragments, no-auth. Service / Processor / Adapter wiring: - `services/app_config_fragment/actions/{get_user_app_config, search_user_app_configs, admin_search_app_configs}.py` — 3 new actions + matching results. - `AppConfigFragmentService.get_user_app_config` / `search_user_app_configs` / `admin_search_app_configs` delegate to the existing repository merged-view methods. - `AppConfigFragmentProcessors` wraps each in an `ActionProcessor`. - `AppConfigFragmentAdapter.my_app_config` / `admin_get_user_app_config` / `my_search_app_configs` / `admin_search_app_configs` expose the v2 Pydantic DTOs. v2 DTO (`common/dto/manager/v2/app_config/`): replaces the stray legacy Upsert/Delete DTOs (unreferenced after the BA-5822 cleanup) with `AppConfigNode` + `GetUserAppConfigPayload` / `SearchAppConfigsPayload` + `SearchMyAppConfigsInput` / `SearchAppConfigsInput` / `GetUserAppConfigInput` + `AppConfigFilter` / `AppConfigOrder` / `AppConfigOrderField`. GQL types (`api/gql/app_config/`): `AppConfigGQL` (Pydantic-backed Node containing a `fragments: [AppConfigFragmentGQL!]!` list plus the deep-merged `config`), `AppConfigFilterGQL` / `AppConfigOrderByGQL` / `AppConfigOrderFieldGQL`. Three root resolvers registered in `schema.py`. REST surface lands in BA-5830 (#11286) alongside the existing fragment REST routes. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 33f0dd3 commit 6c63a8d

17 files changed

Lines changed: 774 additions & 71 deletions

File tree

Lines changed: 25 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,35 @@
11
"""
2-
App configuration DTOs v2 for Manager API.
2+
AppConfig (merged view) DTOs v2 for Manager API (BEP-1052 §5).
33
"""
44

5-
from ai.backend.common.dto.manager.v2.app_config.request import (
6-
DeleteDomainConfigInput,
7-
DeleteUserConfigInput,
8-
UpsertDomainConfigInput,
9-
UpsertUserConfigInput,
5+
from .request import (
6+
AppConfigFilter,
7+
AppConfigOrder,
8+
GetUserAppConfigInput,
9+
SearchAppConfigsInput,
10+
SearchMyAppConfigsInput,
1011
)
11-
from ai.backend.common.dto.manager.v2.app_config.response import (
12+
from .response import (
1213
AppConfigNode,
13-
DeleteDomainConfigPayload,
14-
DeleteUserConfigPayload,
15-
UpsertDomainConfigPayloadDTO,
16-
UpsertUserConfigPayloadDTO,
14+
GetUserAppConfigPayload,
15+
SearchAppConfigsPayload,
16+
)
17+
from .types import (
18+
AppConfigOrderField,
19+
AppConfigScopeType,
20+
OrderDirection,
1721
)
1822

1923
__all__ = (
20-
"DeleteDomainConfigInput",
21-
"DeleteUserConfigInput",
22-
"UpsertDomainConfigInput",
23-
"UpsertUserConfigInput",
24+
"AppConfigFilter",
2425
"AppConfigNode",
25-
"DeleteDomainConfigPayload",
26-
"DeleteUserConfigPayload",
27-
"UpsertDomainConfigPayloadDTO",
28-
"UpsertUserConfigPayloadDTO",
26+
"AppConfigOrder",
27+
"AppConfigOrderField",
28+
"AppConfigScopeType",
29+
"GetUserAppConfigInput",
30+
"GetUserAppConfigPayload",
31+
"OrderDirection",
32+
"SearchAppConfigsInput",
33+
"SearchAppConfigsPayload",
34+
"SearchMyAppConfigsInput",
2935
)
Lines changed: 46 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,55 +1,73 @@
11
"""
2-
Request DTOs for App Configuration v2.
2+
Request DTOs for AppConfig (merged view) DTO v2 (BEP-1052 §5).
33
"""
44

55
from __future__ import annotations
66

7-
from typing import Any
87
from uuid import UUID
98

109
from pydantic import Field
1110

1211
from ai.backend.common.api_handlers import BaseRequestModel
12+
from ai.backend.common.dto.manager.query import StringFilter, UUIDFilter
13+
14+
from .types import AppConfigOrderField, OrderDirection
1315

1416
__all__ = (
15-
"DeleteDomainConfigInput",
16-
"DeleteUserConfigInput",
17-
"UpsertDomainConfigInput",
18-
"UpsertUserConfigInput",
17+
"AppConfigFilter",
18+
"AppConfigOrder",
19+
"GetUserAppConfigInput",
20+
"SearchAppConfigsInput",
21+
"SearchMyAppConfigsInput",
1922
)
2023

2124

22-
class UpsertDomainConfigInput(BaseRequestModel):
23-
"""Input for creating or updating domain-level app configuration."""
25+
class GetUserAppConfigInput(BaseRequestModel):
26+
"""Input for reading a single merged AppConfig for a target user
27+
(admin path — the `my` variant resolves the user internally)."""
2428

25-
domain_name: str = Field(description="Domain name whose configuration will be upserted.")
26-
extra_config: dict[str, Any] = Field(
27-
description="Configuration data to store. Completely replaces existing configuration."
28-
)
29+
user_id: UUID = Field(description="Target user's UUID.")
30+
name: str = Field(description="Policy / config name.")
2931

3032

31-
class UpsertUserConfigInput(BaseRequestModel):
32-
"""Input for creating or updating user-level app configuration."""
33+
class AppConfigFilter(BaseRequestModel):
34+
"""Filter for AppConfig merged-view search."""
3335

34-
extra_config: dict[str, Any] = Field(
35-
description="Configuration data to store. Completely replaces existing configuration."
36-
)
37-
user_id: UUID | None = Field(
36+
name: StringFilter | None = Field(default=None, description="Filter by policy name.")
37+
user_id: UUIDFilter | None = Field(
3838
default=None,
39-
description="User ID whose configuration will be upserted. Defaults to current user.",
39+
description="Filter by target user id (admin cross-user search only).",
4040
)
4141

4242

43-
class DeleteDomainConfigInput(BaseRequestModel):
44-
"""Input for deleting domain-level app configuration."""
43+
class AppConfigOrder(BaseRequestModel):
44+
"""Order specification for AppConfig merged-view results."""
4545

46-
domain_name: str = Field(description="Domain name whose configuration will be deleted.")
46+
field: AppConfigOrderField = Field(description="Field to order by.")
47+
direction: OrderDirection = Field(default=OrderDirection.ASC, description="Order direction.")
4748

4849

49-
class DeleteUserConfigInput(BaseRequestModel):
50-
"""Input for deleting user-level app configuration."""
50+
class _AppConfigSearchInputBase(BaseRequestModel):
51+
filter: AppConfigFilter | None = Field(default=None, description="Filter conditions.")
52+
order: list[AppConfigOrder] | None = Field(default=None, description="Order specifications.")
53+
first: int | None = Field(default=None, ge=1, description="Number of items from the start.")
54+
after: str | None = Field(default=None, description="Cursor to paginate forward from.")
55+
last: int | None = Field(default=None, ge=1, description="Number of items from the end.")
56+
before: str | None = Field(default=None, description="Cursor to paginate backward from.")
57+
limit: int | None = Field(default=None, ge=1, le=1000, description="Maximum items to return.")
58+
offset: int | None = Field(default=None, ge=0, description="Number of items to skip.")
5159

52-
user_id: UUID | None = Field(
53-
default=None,
54-
description="User ID whose configuration will be deleted. Defaults to current user.",
55-
)
60+
61+
class SearchMyAppConfigsInput(_AppConfigSearchInputBase):
62+
"""Input for self-service merged-view search (`/v2/app-configs/my/search`).
63+
64+
The adapter pins the caller as the user scope; no `user_id` argument
65+
is accepted here (BEP-1052 §5 — `filter.userId` is ignored).
66+
"""
67+
68+
69+
class SearchAppConfigsInput(_AppConfigSearchInputBase):
70+
"""Input for admin cross-user merged-view search.
71+
72+
Optional `filter.user_id` pins the search to a single user.
73+
"""
Lines changed: 32 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,49 +1,57 @@
11
"""
2-
Response DTOs for app_config DTO v2.
2+
Response DTOs for AppConfig (merged view) DTO v2 (BEP-1052 §5).
33
"""
44

55
from __future__ import annotations
66

77
from typing import Any
8+
from uuid import UUID
89

910
from pydantic import Field
1011

1112
from ai.backend.common.api_handlers import BaseResponseModel
13+
from ai.backend.common.dto.manager.v2.app_config_fragment.response import AppConfigFragmentNode
1214

1315
__all__ = (
1416
"AppConfigNode",
15-
"DeleteDomainConfigPayload",
16-
"DeleteUserConfigPayload",
17-
"UpsertDomainConfigPayloadDTO",
18-
"UpsertUserConfigPayloadDTO",
17+
"GetUserAppConfigPayload",
18+
"SearchAppConfigsPayload",
1919
)
2020

2121

2222
class AppConfigNode(BaseResponseModel):
23-
"""Node model representing app configuration data."""
23+
"""Merged per-user AppConfig view (BEP-1052 §5).
2424
25-
extra_config: dict[str, Any] = Field(description="Additional configuration data.")
25+
`fragments` are ordered low → high merge priority (matching the
26+
policy's `scope_sources`). `config` is the deep-merged result,
27+
projected to `None` when every contributing fragment is empty
28+
(§3 null projection).
29+
"""
2630

31+
user_id: UUID = Field(description="Target user's UUID.")
32+
name: str = Field(description="Policy / config name.")
33+
fragments: list[AppConfigFragmentNode] = Field(
34+
description="Contributing fragments in merge order (low → high).",
35+
)
36+
config: dict[str, Any] | None = Field(
37+
default=None,
38+
description="Deep-merged configuration, or null when every fragment is empty.",
39+
)
2740

28-
class DeleteDomainConfigPayload(BaseResponseModel):
29-
"""Payload for domain-level app config deletion mutation result."""
3041

31-
deleted: bool = Field(description="Whether the deletion was successful.")
42+
class GetUserAppConfigPayload(BaseResponseModel):
43+
"""Payload for reading a single merged AppConfig."""
3244

45+
item: AppConfigNode | None = Field(
46+
default=None,
47+
description="Merged AppConfig, or null when no fragments exist for the user.",
48+
)
3349

34-
class DeleteUserConfigPayload(BaseResponseModel):
35-
"""Payload for user-level app config deletion mutation result."""
3650

37-
deleted: bool = Field(description="Whether the deletion was successful.")
51+
class SearchAppConfigsPayload(BaseResponseModel):
52+
"""Payload for paginated merged-view AppConfig search."""
3853

39-
40-
class UpsertDomainConfigPayloadDTO(BaseResponseModel):
41-
"""Payload returned after upserting domain-level app configuration."""
42-
43-
app_config: AppConfigNode = Field(description="The resulting app configuration")
44-
45-
46-
class UpsertUserConfigPayloadDTO(BaseResponseModel):
47-
"""Payload returned after upserting user-level app configuration."""
48-
49-
app_config: AppConfigNode = Field(description="The resulting app configuration")
54+
items: list[AppConfigNode] = Field(description="AppConfigs matching the filter.")
55+
total_count: int = Field(description="Total number of AppConfigs matching the filter.")
56+
has_next_page: bool = Field(default=False, description="Whether there is a next page.")
57+
has_previous_page: bool = Field(default=False, description="Whether there is a previous page.")
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
"""
2+
Common types for AppConfig (merged view) DTO v2 (BEP-1052 §5).
3+
"""
4+
5+
from __future__ import annotations
6+
7+
from enum import StrEnum
8+
9+
from ai.backend.common.dto.manager.v2.app_config_fragment.types import AppConfigScopeType
10+
from ai.backend.common.dto.manager.v2.common import OrderDirection
11+
12+
__all__ = (
13+
"AppConfigOrderField",
14+
"AppConfigScopeType",
15+
"OrderDirection",
16+
)
17+
18+
19+
class AppConfigOrderField(StrEnum):
20+
"""Fields available for ordering AppConfig merged-view results."""
21+
22+
USER_ID = "user_id"
23+
NAME = "name"

0 commit comments

Comments
 (0)