Skip to content

Commit 05c11f1

Browse files
jopemachineclaude
andcommitted
feat(BA-5829): add AppConfigFragment + AppConfig GQL surface
Adds the GraphQL exposure for AppConfigFragment and the merged AppConfig view (BEP-1052 §2 / §5). Service / processor additions (only what the GQL layer needs that is not already in BA-5827): - Merged-view actions: get_user_app_config, search_user_app_configs, admin_search_app_configs (single-row + paginated reads of the computed AppConfig view). - Service methods + processor wiring + supported_actions for those three actions. Adapter additions: - Merged-view methods: get_user_app_config / my_app_configs / admin_search_app_configs / public_app_config_fragments. `my_*` pulls the user from current_user() inside the adapter. - Bulk-only `my` mutations: my_bulk_create / my_bulk_update — bulk admin variants are wired in BA-5827 already. DTO additions: - common/dto/manager/v2/app_config/* — AppConfigNode, GetUserAppConfigInput / Payload, SearchAppConfigsInput / Payload, SearchMyAppConfigsInput, BulkCreate/UpdateMyAppConfigFragmentsPayload (the my-bulk payloads live here because they carry AppConfigNode). GraphQL: - app_config package: AppConfigGQL, my/admin/public root resolvers. - app_config_fragment package: scope-bound + admin queries, bulk-only mutations (admin + my variants), AppConfigFragmentGQL + AppConfigScopeTypeGQL + filter/order/key inputs. - DataLoader: app_config_fragment_loader for N+1 batching. - schema.py: register the new root queries / mutations. Resolves BA-5829. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent b155ec8 commit 05c11f1

28 files changed

Lines changed: 1664 additions & 69 deletions

File tree

Lines changed: 29 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,39 @@
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+
BulkCreateMyAppConfigFragmentsPayload,
15+
BulkUpdateMyAppConfigFragmentsPayload,
16+
GetUserAppConfigPayload,
17+
SearchAppConfigsPayload,
18+
)
19+
from .types import (
20+
AppConfigOrderField,
21+
AppConfigScopeType,
22+
OrderDirection,
1723
)
1824

1925
__all__ = (
20-
"DeleteDomainConfigInput",
21-
"DeleteUserConfigInput",
22-
"UpsertDomainConfigInput",
23-
"UpsertUserConfigInput",
26+
"AppConfigFilter",
2427
"AppConfigNode",
25-
"DeleteDomainConfigPayload",
26-
"DeleteUserConfigPayload",
27-
"UpsertDomainConfigPayloadDTO",
28-
"UpsertUserConfigPayloadDTO",
28+
"AppConfigOrder",
29+
"AppConfigOrderField",
30+
"AppConfigScopeType",
31+
"BulkCreateMyAppConfigFragmentsPayload",
32+
"BulkUpdateMyAppConfigFragmentsPayload",
33+
"GetUserAppConfigInput",
34+
"GetUserAppConfigPayload",
35+
"OrderDirection",
36+
"SearchAppConfigsInput",
37+
"SearchAppConfigsPayload",
38+
"SearchMyAppConfigsInput",
2939
)
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: 54 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,49 +1,84 @@
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 (
14+
AppConfigFragmentBulkError,
15+
AppConfigFragmentNode,
16+
)
1217

1318
__all__ = (
1419
"AppConfigNode",
15-
"DeleteDomainConfigPayload",
16-
"DeleteUserConfigPayload",
17-
"UpsertDomainConfigPayloadDTO",
18-
"UpsertUserConfigPayloadDTO",
20+
"BulkCreateMyAppConfigFragmentsPayload",
21+
"BulkUpdateMyAppConfigFragmentsPayload",
22+
"GetUserAppConfigPayload",
23+
"SearchAppConfigsPayload",
1924
)
2025

2126

2227
class AppConfigNode(BaseResponseModel):
23-
"""Node model representing app configuration data."""
28+
"""Merged per-user AppConfig view (BEP-1052 §5).
29+
30+
`fragments` are ordered low → high merge priority (matching the
31+
policy's `scope_sources`). `config` is the deep-merged result,
32+
projected to `None` when every contributing fragment is empty
33+
(§3 null projection).
34+
"""
35+
36+
user_id: UUID = Field(description="Target user's UUID.")
37+
name: str = Field(description="Policy / config name.")
38+
fragments: list[AppConfigFragmentNode] = Field(
39+
description="Contributing fragments in merge order (low → high).",
40+
)
41+
config: dict[str, Any] | None = Field(
42+
default=None,
43+
description="Deep-merged configuration, or null when every fragment is empty.",
44+
)
2445

25-
extra_config: dict[str, Any] = Field(description="Additional configuration data.")
2646

47+
class GetUserAppConfigPayload(BaseResponseModel):
48+
"""Payload for reading a single merged AppConfig."""
2749

28-
class DeleteDomainConfigPayload(BaseResponseModel):
29-
"""Payload for domain-level app config deletion mutation result."""
50+
item: AppConfigNode | None = Field(
51+
default=None,
52+
description="Merged AppConfig, or null when no fragments exist for the user.",
53+
)
3054

31-
deleted: bool = Field(description="Whether the deletion was successful.")
3255

56+
class SearchAppConfigsPayload(BaseResponseModel):
57+
"""Payload for paginated merged-view AppConfig search."""
3358

34-
class DeleteUserConfigPayload(BaseResponseModel):
35-
"""Payload for user-level app config deletion mutation result."""
59+
items: list[AppConfigNode] = Field(description="AppConfigs matching the filter.")
60+
total_count: int = Field(description="Total number of AppConfigs matching the filter.")
61+
has_next_page: bool = Field(default=False, description="Whether there is a next page.")
62+
has_previous_page: bool = Field(default=False, description="Whether there is a previous page.")
3663

37-
deleted: bool = Field(description="Whether the deletion was successful.")
3864

65+
class BulkCreateMyAppConfigFragmentsPayload(BaseResponseModel):
66+
"""Payload for `bulkCreateMyAppConfigFragments`.
3967
40-
class UpsertDomainConfigPayloadDTO(BaseResponseModel):
41-
"""Payload returned after upserting domain-level app configuration."""
68+
Each successfully created row produces a recomputed merged
69+
`AppConfigNode`; failures are collected per-item (BEP-1052 §3).
70+
"""
4271

43-
app_config: AppConfigNode = Field(description="The resulting app configuration")
72+
created: list[AppConfigNode] = Field(
73+
description="Recomputed merged AppConfig views for each created USER fragment.",
74+
)
75+
failed: list[AppConfigFragmentBulkError] = Field(description="Per-item failures.")
4476

4577

46-
class UpsertUserConfigPayloadDTO(BaseResponseModel):
47-
"""Payload returned after upserting user-level app configuration."""
78+
class BulkUpdateMyAppConfigFragmentsPayload(BaseResponseModel):
79+
"""Payload for `bulkUpdateMyAppConfigFragments`."""
4880

49-
app_config: AppConfigNode = Field(description="The resulting app configuration")
81+
updated: list[AppConfigNode] = Field(
82+
description="Recomputed merged AppConfig views for each updated USER fragment.",
83+
)
84+
failed: list[AppConfigFragmentBulkError] = Field(description="Per-item failures.")
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)