Skip to content

Commit 4b7cafc

Browse files
jopemachineclaude
andcommitted
feat(BA-5829): convert AppConfigFragment GQL mutations to bulk-only (BEP-1052 §3)
BEP-1052 §3 specifies every write as bulk; the single-item mutations previously exposed are replaced with the five bulk mutations below. Internal service / repository single-row methods remain (bulk actions loop over them with per-item transactions) so only the external GQL surface is touched in this commit. GQL mutations: - `adminBulkCreateAppConfigFragments(input)` — any scope - `adminBulkUpdateAppConfigFragments(input)` — any scope - `adminBulkPurgeAppConfigFragments(input)` — absent keys no-oped - `bulkCreateMyAppConfigFragments(input)` — USER + current_user, recomputed AppConfig - `bulkUpdateMyAppConfigFragments(input)` — USER + current_user, recomputed AppConfig Each item runs in its own transaction; failures collected per-item in `failed: [AppConfigFragmentBulkError!]!`. Service / Processor additions: - `data/app_config_fragment/bulk_types.py` — per-item / error dataclasses - `services/app_config_fragment/actions/admin_bulk_{create,update,purge}.py` + `bulk_{create,update}_my.py` — 5 actions + results - `AppConfigFragmentService.admin_bulk_create` / `admin_bulk_update` / `admin_bulk_purge` / `bulk_create_my` / `bulk_update_my` — loop over admin repo; per-item try / except; my-path recomputes merged view. - `AppConfigFragmentProcessors` wires the 5 new processors + `supported_actions()` extended. DTO additions (`common/dto/manager/v2/app_config_fragment/`): - `AdminAppConfigFragmentItemInput`, `AdminBulkCreate/Update/PurgeAppConfigFragmentsInput` - `MyAppConfigFragmentItemInput`, `BulkCreate/UpdateMyAppConfigFragmentsInput` - `AppConfigFragmentBulkError`, `PurgeAppConfigFragmentKey` - `AdminBulkCreate/Update/PurgeAppConfigFragmentsPayload` `BulkCreate/UpdateMyAppConfigFragmentsPayload` live in `common/dto/manager/v2/app_config/response.py` next to `AppConfigNode` to avoid a circular import (they carry recomputed `AppConfigNode`s). Adapter additions (`api/adapters/app_config_fragment.py`): - `admin_bulk_create` / `admin_bulk_update` / `admin_bulk_purge` / `my_bulk_create` / `my_bulk_update` — convert v2 DTO input → service bulk items → service result → v2 DTO payload. `my_bulk_*` resolve the current user via `current_user()`. GQL types added under `api/gql/app_config_fragment/types/`: - `bulk_inputs.py` — 7 input types - `bulk_payloads.py` — 5 payload types + `AppConfigFragmentBulkErrorGQL` + `PurgeAppConfigFragmentKeyGQL` Five new root mutations registered in `schema.py`; the three prior single-item mutations (`adminCreate/Update/PurgeAppConfigFragment`) are removed along with their resolvers. REST bulk surface lands next in BA-5830. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 2fd3c48 commit 4b7cafc

21 files changed

Lines changed: 1176 additions & 77 deletions

File tree

src/ai/backend/common/dto/manager/v2/app_config/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@
1111
)
1212
from .response import (
1313
AppConfigNode,
14+
BulkCreateMyAppConfigFragmentsPayload,
15+
BulkUpdateMyAppConfigFragmentsPayload,
1416
GetUserAppConfigPayload,
1517
SearchAppConfigsPayload,
1618
)
@@ -26,6 +28,8 @@
2628
"AppConfigOrder",
2729
"AppConfigOrderField",
2830
"AppConfigScopeType",
31+
"BulkCreateMyAppConfigFragmentsPayload",
32+
"BulkUpdateMyAppConfigFragmentsPayload",
2933
"GetUserAppConfigInput",
3034
"GetUserAppConfigPayload",
3135
"OrderDirection",

src/ai/backend/common/dto/manager/v2/app_config/response.py

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,15 @@
1010
from pydantic import Field
1111

1212
from ai.backend.common.api_handlers import BaseResponseModel
13-
from ai.backend.common.dto.manager.v2.app_config_fragment.response import AppConfigFragmentNode
13+
from ai.backend.common.dto.manager.v2.app_config_fragment.response import (
14+
AppConfigFragmentBulkError,
15+
AppConfigFragmentNode,
16+
)
1417

1518
__all__ = (
1619
"AppConfigNode",
20+
"BulkCreateMyAppConfigFragmentsPayload",
21+
"BulkUpdateMyAppConfigFragmentsPayload",
1722
"GetUserAppConfigPayload",
1823
"SearchAppConfigsPayload",
1924
)
@@ -55,3 +60,25 @@ class SearchAppConfigsPayload(BaseResponseModel):
5560
total_count: int = Field(description="Total number of AppConfigs matching the filter.")
5661
has_next_page: bool = Field(default=False, description="Whether there is a next page.")
5762
has_previous_page: bool = Field(default=False, description="Whether there is a previous page.")
63+
64+
65+
class BulkCreateMyAppConfigFragmentsPayload(BaseResponseModel):
66+
"""Payload for `bulkCreateMyAppConfigFragments`.
67+
68+
Each successfully created row produces a recomputed merged
69+
`AppConfigNode`; failures are collected per-item (BEP-1052 §3).
70+
"""
71+
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.")
76+
77+
78+
class BulkUpdateMyAppConfigFragmentsPayload(BaseResponseModel):
79+
"""Payload for `bulkUpdateMyAppConfigFragments`."""
80+
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.")

src/ai/backend/common/dto/manager/v2/app_config_fragment/__init__.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,30 @@
11
from .request import (
2+
AdminAppConfigFragmentItemInput,
3+
AdminBulkCreateAppConfigFragmentsInput,
4+
AdminBulkPurgeAppConfigFragmentsInput,
5+
AdminBulkUpdateAppConfigFragmentsInput,
26
AppConfigFragmentFilter,
37
AppConfigFragmentKeyInput,
48
AppConfigFragmentOrder,
9+
BulkCreateMyAppConfigFragmentsInput,
10+
BulkUpdateMyAppConfigFragmentsInput,
511
CreateAppConfigFragmentInput,
12+
MyAppConfigFragmentItemInput,
613
PurgeAppConfigFragmentInput,
714
SearchAppConfigFragmentsInput,
815
UpdateAppConfigFragmentInput,
916
)
1017
from .response import (
18+
AdminBulkCreateAppConfigFragmentsPayload,
19+
AdminBulkPurgeAppConfigFragmentsPayload,
20+
AdminBulkUpdateAppConfigFragmentsPayload,
21+
AppConfigFragmentBulkError,
1122
AppConfigFragmentNode,
1223
CreateAppConfigFragmentGQLPayload,
1324
CreateAppConfigFragmentPayload,
1425
GetAppConfigFragmentPayload,
1526
PurgeAppConfigFragmentGQLPayload,
27+
PurgeAppConfigFragmentKey,
1628
PurgeAppConfigFragmentPayload,
1729
SearchAppConfigFragmentsPayload,
1830
UpdateAppConfigFragmentGQLPayload,
@@ -25,19 +37,31 @@
2537
)
2638

2739
__all__ = (
40+
"AdminAppConfigFragmentItemInput",
41+
"AdminBulkCreateAppConfigFragmentsInput",
42+
"AdminBulkCreateAppConfigFragmentsPayload",
43+
"AdminBulkPurgeAppConfigFragmentsInput",
44+
"AdminBulkPurgeAppConfigFragmentsPayload",
45+
"AdminBulkUpdateAppConfigFragmentsInput",
46+
"AdminBulkUpdateAppConfigFragmentsPayload",
47+
"AppConfigFragmentBulkError",
2848
"AppConfigFragmentFilter",
2949
"AppConfigFragmentKeyInput",
3050
"AppConfigFragmentNode",
3151
"AppConfigFragmentOrder",
3252
"AppConfigFragmentOrderField",
3353
"AppConfigScopeType",
54+
"BulkCreateMyAppConfigFragmentsInput",
55+
"BulkUpdateMyAppConfigFragmentsInput",
3456
"CreateAppConfigFragmentGQLPayload",
3557
"CreateAppConfigFragmentInput",
3658
"CreateAppConfigFragmentPayload",
3759
"GetAppConfigFragmentPayload",
60+
"MyAppConfigFragmentItemInput",
3861
"OrderDirection",
3962
"PurgeAppConfigFragmentGQLPayload",
4063
"PurgeAppConfigFragmentInput",
64+
"PurgeAppConfigFragmentKey",
4165
"PurgeAppConfigFragmentPayload",
4266
"SearchAppConfigFragmentsInput",
4367
"SearchAppConfigFragmentsPayload",

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

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,17 @@
1414
from .types import AppConfigFragmentOrderField, AppConfigScopeType, OrderDirection
1515

1616
__all__ = (
17+
"AdminAppConfigFragmentItemInput",
18+
"AdminBulkCreateAppConfigFragmentsInput",
19+
"AdminBulkPurgeAppConfigFragmentsInput",
20+
"AdminBulkUpdateAppConfigFragmentsInput",
1721
"AppConfigFragmentFilter",
1822
"AppConfigFragmentKeyInput",
1923
"AppConfigFragmentOrder",
24+
"BulkCreateMyAppConfigFragmentsInput",
25+
"BulkUpdateMyAppConfigFragmentsInput",
2026
"CreateAppConfigFragmentInput",
27+
"MyAppConfigFragmentItemInput",
2128
"PurgeAppConfigFragmentInput",
2229
"SearchAppConfigFragmentsInput",
2330
"UpdateAppConfigFragmentInput",
@@ -77,6 +84,51 @@ class AppConfigFragmentOrder(BaseRequestModel):
7784
direction: OrderDirection = Field(default=OrderDirection.ASC, description="Order direction.")
7885

7986

87+
# ── Bulk mutation inputs (BEP-1052 §3) ──────────────────────────
88+
89+
90+
class AdminAppConfigFragmentItemInput(BaseRequestModel):
91+
"""Per-item input for admin bulk create / update (natural key + payload)."""
92+
93+
key: AppConfigFragmentKeyInput = Field(description="Natural-key identifier.")
94+
extra_config: dict[str, Any] = Field(
95+
default_factory=dict,
96+
description="Raw configuration payload (empty dict clears the row).",
97+
)
98+
99+
100+
class AdminBulkCreateAppConfigFragmentsInput(BaseRequestModel):
101+
items: list[AdminAppConfigFragmentItemInput] = Field(description="Rows to create.")
102+
103+
104+
class AdminBulkUpdateAppConfigFragmentsInput(BaseRequestModel):
105+
items: list[AdminAppConfigFragmentItemInput] = Field(description="Rows to update.")
106+
107+
108+
class AdminBulkPurgeAppConfigFragmentsInput(BaseRequestModel):
109+
keys: list[AppConfigFragmentKeyInput] = Field(description="Natural keys to purge.")
110+
111+
112+
class MyAppConfigFragmentItemInput(BaseRequestModel):
113+
"""Per-item input for self-service (`my`) bulk — `scope_type`
114+
/ `scope_id` are server-injected, so `name` is the only identifier.
115+
"""
116+
117+
name: str = Field(description="Policy name.")
118+
extra_config: dict[str, Any] = Field(
119+
default_factory=dict,
120+
description="Raw configuration payload (empty dict clears the row).",
121+
)
122+
123+
124+
class BulkCreateMyAppConfigFragmentsInput(BaseRequestModel):
125+
items: list[MyAppConfigFragmentItemInput] = Field(description="USER-scope rows to create.")
126+
127+
128+
class BulkUpdateMyAppConfigFragmentsInput(BaseRequestModel):
129+
items: list[MyAppConfigFragmentItemInput] = Field(description="USER-scope rows to update.")
130+
131+
80132
class SearchAppConfigFragmentsInput(BaseRequestModel):
81133
"""Input for searching fragments (raw rows) with filter / order / pagination.
82134

src/ai/backend/common/dto/manager/v2/app_config_fragment/response.py

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,16 @@
1515
from .types import AppConfigScopeType
1616

1717
__all__ = (
18+
"AdminBulkCreateAppConfigFragmentsPayload",
19+
"AdminBulkPurgeAppConfigFragmentsPayload",
20+
"AdminBulkUpdateAppConfigFragmentsPayload",
21+
"AppConfigFragmentBulkError",
1822
"AppConfigFragmentNode",
1923
"CreateAppConfigFragmentGQLPayload",
2024
"CreateAppConfigFragmentPayload",
2125
"GetAppConfigFragmentPayload",
2226
"PurgeAppConfigFragmentGQLPayload",
27+
"PurgeAppConfigFragmentKey",
2328
"PurgeAppConfigFragmentPayload",
2429
"SearchAppConfigFragmentsPayload",
2530
"UpdateAppConfigFragmentGQLPayload",
@@ -89,10 +94,66 @@ class PurgeAppConfigFragmentGQLPayload(BaseResponseModel):
8994
purged: bool = Field(description="Whether a row was actually removed.")
9095

9196

97+
# ── Bulk mutation payloads (BEP-1052 §3) ─────────────────────────
98+
99+
100+
class AppConfigFragmentBulkError(BaseResponseModel):
101+
"""Per-item failure information for bulk Fragment mutations."""
102+
103+
index: int = Field(description="Original position in the input list.")
104+
scope_type: AppConfigScopeType = Field(description="Scope type of the failed row.")
105+
scope_id: str = Field(description="Scope id of the failed row.")
106+
name: str = Field(description="Policy name of the failed row.")
107+
message: str = Field(description="Reason for the failure.")
108+
109+
110+
class PurgeAppConfigFragmentKey(BaseResponseModel):
111+
"""Natural-key identifier returned by bulk purge payloads."""
112+
113+
scope_type: AppConfigScopeType = Field(description="Scope type.")
114+
scope_id: str = Field(description="Scope id.")
115+
name: str = Field(description="Policy name.")
116+
117+
118+
class AdminBulkCreateAppConfigFragmentsPayload(BaseResponseModel):
119+
"""Payload for `adminBulkCreateAppConfigFragments`."""
120+
121+
created: list[AppConfigFragmentNode] = Field(description="Created fragments.")
122+
failed: list[AppConfigFragmentBulkError] = Field(description="Per-item failures.")
123+
124+
125+
class AdminBulkUpdateAppConfigFragmentsPayload(BaseResponseModel):
126+
"""Payload for `adminBulkUpdateAppConfigFragments`."""
127+
128+
updated: list[AppConfigFragmentNode] = Field(description="Updated fragments.")
129+
failed: list[AppConfigFragmentBulkError] = Field(description="Per-item failures.")
130+
131+
132+
class AdminBulkPurgeAppConfigFragmentsPayload(BaseResponseModel):
133+
"""Payload for `adminBulkPurgeAppConfigFragments`."""
134+
135+
purged: list[PurgeAppConfigFragmentKey] = Field(
136+
description="Keys of rows actually removed (absent keys are no-oped).",
137+
)
138+
failed: list[AppConfigFragmentBulkError] = Field(description="Per-item failures.")
139+
140+
141+
# Bulk `my` payloads return recomputed merged views (AppConfig). To
142+
# avoid a circular import we declare them in `app_config/response.py`
143+
# and re-import them here via a forward alias below.
144+
145+
92146
class SearchAppConfigFragmentsPayload(BaseResponseModel):
93147
"""Payload for paginated fragment search results."""
94148

95149
items: list[AppConfigFragmentNode] = Field(description="Fragments matching the filter.")
96150
total_count: int = Field(description="Total number of fragments matching the filter.")
97151
has_next_page: bool = Field(default=False, description="Whether there is a next page.")
98152
has_previous_page: bool = Field(default=False, description="Whether there is a previous page.")
153+
154+
155+
# `BulkCreateMyAppConfigFragmentsPayload` / `BulkUpdateMyAppConfigFragmentsPayload`
156+
# return recomputed merged `AppConfig` views — they live in
157+
# `common/dto/manager/v2/app_config/response.py` to keep `AppConfigNode`
158+
# as the single source of truth and avoid a circular import from this
159+
# module into `app_config/response.py`.

0 commit comments

Comments
 (0)