From 6171f32fe4f4e249018683629d5f9ae714ece6d1 Mon Sep 17 00:00:00 2001 From: jopemachine Date: Sat, 25 Apr 2026 14:31:22 +0900 Subject: [PATCH 1/8] feat(BA-5830): add AppConfigFragment + AppConfig REST v2 surface MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit REST endpoints for the Fragment domain and the merged AppConfig view (BEP-1052 §2 / §5). Service / adapter foundation lands in BA-5827 (Fragment bulk) and BA-5829 (merged-view actions); this PR only wires the REST entry points on top. REST v2 (`/v2/app-config-fragments`): - `GET /{scope_type}/{scope_id}/{name}` — auth_required. - `POST /{scope_type}/{scope_id}/search` — scope-bound search. - `POST /search` — superadmin cross-scope search. - `POST /bulk-create` / `bulk-update` / `bulk-purge` — superadmin only. - `POST /my/bulk-create` / `my/bulk-update` — self-service USER scope. REST v2 (`/v2/app-configs`, merged view): - `GET /my/{name}` / `POST /my/search` — auth_required. - `GET /{user_id}/{name}` / `POST /search` — superadmin only. Adds the path-param classes for the new URL segments and wires the handlers into `rest/v2/tree.py`. Resolves BA-5830. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../api/rest/v2/app_config/__init__.py | 0 .../manager/api/rest/v2/app_config/handler.py | 74 +++++++++++ .../api/rest/v2/app_config/registry.py | 36 +++++ .../rest/v2/app_config_fragment/__init__.py | 0 .../rest/v2/app_config_fragment/handler.py | 124 ++++++++++++++++++ .../rest/v2/app_config_fragment/registry.py | 48 +++++++ .../manager/api/rest/v2/path_params.py | 14 ++ src/ai/backend/manager/api/rest/v2/tree.py | 10 ++ 8 files changed, 306 insertions(+) create mode 100644 src/ai/backend/manager/api/rest/v2/app_config/__init__.py create mode 100644 src/ai/backend/manager/api/rest/v2/app_config/handler.py create mode 100644 src/ai/backend/manager/api/rest/v2/app_config/registry.py create mode 100644 src/ai/backend/manager/api/rest/v2/app_config_fragment/__init__.py create mode 100644 src/ai/backend/manager/api/rest/v2/app_config_fragment/handler.py create mode 100644 src/ai/backend/manager/api/rest/v2/app_config_fragment/registry.py diff --git a/src/ai/backend/manager/api/rest/v2/app_config/__init__.py b/src/ai/backend/manager/api/rest/v2/app_config/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/src/ai/backend/manager/api/rest/v2/app_config/handler.py b/src/ai/backend/manager/api/rest/v2/app_config/handler.py new file mode 100644 index 00000000000..f813a91b6a1 --- /dev/null +++ b/src/ai/backend/manager/api/rest/v2/app_config/handler.py @@ -0,0 +1,74 @@ +"""REST v2 handler for the AppConfig merged-view domain (BEP-1052 §5).""" + +from __future__ import annotations + +import logging +from http import HTTPStatus +from typing import TYPE_CHECKING, Final + +from ai.backend.common.api_handlers import APIResponse, BodyParam, PathParam +from ai.backend.common.dto.manager.v2.app_config.request import ( + GetUserAppConfigInput, + SearchAppConfigsInput, + SearchMyAppConfigsInput, +) +from ai.backend.logging import BraceStyleAdapter +from ai.backend.manager.api.rest.v2.path_params import ( + AppConfigMyNamePathParam, + AppConfigUserNamePathParam, +) + +if TYPE_CHECKING: + from ai.backend.manager.api.adapters.app_config import AppConfigAdapter + +log: Final = BraceStyleAdapter(logging.getLogger(__spec__.name)) + + +class V2AppConfigHandler: + """REST v2 handler for the merged-view `AppConfig` surface. + + Mounted at `/v2/app-configs/...`. The merged view is computed from + fragment rows joined against `app_config_policies` and is exposed + via its own `AppConfigAdapter`. + """ + + def __init__(self, *, adapter: AppConfigAdapter) -> None: + self._adapter = adapter + + # ── My (self-service) ──────────────────────────────────────── + + async def my_get( + self, + path: PathParam[AppConfigMyNamePathParam], + ) -> APIResponse: + """Read the caller's own merged AppConfig for `name`.""" + result = await self._adapter.my_app_config(path.parsed.name) + return APIResponse.build(status_code=HTTPStatus.OK, response_model=result) + + async def my_search( + self, + body: BodyParam[SearchMyAppConfigsInput], + ) -> APIResponse: + """Paginated merged-view search over the caller's own AppConfigs.""" + result = await self._adapter.my_search_app_configs(body.parsed) + return APIResponse.build(status_code=HTTPStatus.OK, response_model=result) + + # ── Admin ──────────────────────────────────────────────────── + + async def admin_get( + self, + path: PathParam[AppConfigUserNamePathParam], + ) -> APIResponse: + """Read a specific user's merged AppConfig (admin only).""" + result = await self._adapter.admin_get_user_app_config( + GetUserAppConfigInput(user_id=path.parsed.user_id, name=path.parsed.name) + ) + return APIResponse.build(status_code=HTTPStatus.OK, response_model=result) + + async def admin_search( + self, + body: BodyParam[SearchAppConfigsInput], + ) -> APIResponse: + """Cross-user merged-view search (admin only).""" + result = await self._adapter.admin_search_app_configs(body.parsed) + return APIResponse.build(status_code=HTTPStatus.OK, response_model=result) diff --git a/src/ai/backend/manager/api/rest/v2/app_config/registry.py b/src/ai/backend/manager/api/rest/v2/app_config/registry.py new file mode 100644 index 00000000000..b70d0ce48e6 --- /dev/null +++ b/src/ai/backend/manager/api/rest/v2/app_config/registry.py @@ -0,0 +1,36 @@ +"""Route registration for v2 AppConfig merged-view endpoints (BEP-1052 §5).""" + +from __future__ import annotations + +from typing import TYPE_CHECKING + +from ai.backend.manager.api.rest.middleware.auth import auth_required, superadmin_required +from ai.backend.manager.api.rest.routing import RouteRegistry + +from .handler import V2AppConfigHandler + +if TYPE_CHECKING: + from ai.backend.manager.api.rest.types import RouteDeps + + +def register_v2_app_config_routes( + handler: V2AppConfigHandler, + route_deps: RouteDeps, +) -> RouteRegistry: + """Register all v2 `/v2/app-configs/*` routes (BEP-1052 §4). + + Read-only surface — writes go through `/v2/app-config-fragments/...` + (§4). Self-service routes land under the `/my/...` prefix so the + adapter can pin `(USER, current_user)` internally; admin routes + allow targeting any user id. + """ + reg = RouteRegistry.create("app-configs", route_deps.cors_options) + + # Self-service + reg.add("GET", "/my/{name}", handler.my_get, middlewares=[auth_required]) + reg.add("POST", "/my/search", handler.my_search, middlewares=[auth_required]) + # Admin + reg.add("POST", "/search", handler.admin_search, middlewares=[superadmin_required]) + reg.add("GET", "/{user_id}/{name}", handler.admin_get, middlewares=[superadmin_required]) + + return reg diff --git a/src/ai/backend/manager/api/rest/v2/app_config_fragment/__init__.py b/src/ai/backend/manager/api/rest/v2/app_config_fragment/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/src/ai/backend/manager/api/rest/v2/app_config_fragment/handler.py b/src/ai/backend/manager/api/rest/v2/app_config_fragment/handler.py new file mode 100644 index 00000000000..4b3e7715b51 --- /dev/null +++ b/src/ai/backend/manager/api/rest/v2/app_config_fragment/handler.py @@ -0,0 +1,124 @@ +"""REST v2 handler for the app-config fragment domain (BEP-1052 §4). + +Writes are **bulk-only** per BEP §3 — the single-item create / update / +purge endpoints were removed in favour of `/bulk-create`, +`/bulk-update`, `/bulk-purge` (admin) and `/my/bulk-create`, +`/my/bulk-update` (self-service). +""" + +from __future__ import annotations + +import logging +from http import HTTPStatus +from typing import TYPE_CHECKING, Final + +from ai.backend.common.api_handlers import APIResponse, BodyParam, PathParam +from ai.backend.common.dto.manager.v2.app_config_fragment.request import ( + AdminBulkCreateAppConfigFragmentsInput, + AdminBulkPurgeAppConfigFragmentsInput, + AdminBulkUpdateAppConfigFragmentsInput, + AppConfigFragmentKeyInput, + BulkCreateMyAppConfigFragmentsInput, + BulkUpdateMyAppConfigFragmentsInput, + SearchAppConfigFragmentsInput, +) +from ai.backend.common.dto.manager.v2.app_config_fragment.types import AppConfigScopeType +from ai.backend.logging import BraceStyleAdapter +from ai.backend.manager.api.rest.v2.path_params import AppConfigFragmentScopePathParam +from ai.backend.manager.data.app_config_fragment.types import ( + AppConfigScopeType as DataAppConfigScopeType, +) + +if TYPE_CHECKING: + from ai.backend.manager.api.adapters.app_config_fragment import AppConfigFragmentAdapter + +log: Final = BraceStyleAdapter(logging.getLogger(__spec__.name)) + + +class V2AppConfigFragmentHandler: + """REST v2 handler for app-config fragment operations.""" + + def __init__(self, *, adapter: AppConfigFragmentAdapter) -> None: + self._adapter = adapter + + # ── Reads ──────────────────────────────────────────────────── + + async def get( + self, + body: BodyParam[AppConfigFragmentKeyInput], + ) -> APIResponse: + """Read a single fragment by natural key (any authenticated user).""" + result = await self._adapter.get(body.parsed) + return APIResponse.build(status_code=HTTPStatus.OK, response_model=result) + + async def scoped_search( + self, + path: PathParam[AppConfigFragmentScopePathParam], + body: BodyParam[SearchAppConfigFragmentsInput], + ) -> APIResponse: + """Scope-bound fragment search — caller is pinned to a specific + `(scope_type, scope_id)` pair via the URL path. + """ + result = await self._adapter.search( + scope_type=DataAppConfigScopeType(path.parsed.scope_type), + scope_id=path.parsed.scope_id, + input=body.parsed, + ) + return APIResponse.build(status_code=HTTPStatus.OK, response_model=result) + + async def admin_search( + self, + body: BodyParam[SearchAppConfigFragmentsInput], + ) -> APIResponse: + """Cross-scope admin search (admin only).""" + result = await self._adapter.admin_search(body.parsed) + return APIResponse.build(status_code=HTTPStatus.OK, response_model=result) + + # ── Admin bulk writes (BEP-1052 §3) ────────────────────────── + + async def admin_bulk_create( + self, + body: BodyParam[AdminBulkCreateAppConfigFragmentsInput], + ) -> APIResponse: + """Strict insert across any scope; per-item transactions (admin only).""" + result = await self._adapter.admin_bulk_create(body.parsed) + return APIResponse.build(status_code=HTTPStatus.OK, response_model=result) + + async def admin_bulk_update( + self, + body: BodyParam[AdminBulkUpdateAppConfigFragmentsInput], + ) -> APIResponse: + """Wholesale JSON replacement; per-item transactions (admin only).""" + result = await self._adapter.admin_bulk_update(body.parsed) + return APIResponse.build(status_code=HTTPStatus.OK, response_model=result) + + async def admin_bulk_purge( + self, + body: BodyParam[AdminBulkPurgeAppConfigFragmentsInput], + ) -> APIResponse: + """Cleanup-only deletion; absent keys are no-oped (admin only).""" + result = await self._adapter.admin_bulk_purge(body.parsed) + return APIResponse.build(status_code=HTTPStatus.OK, response_model=result) + + # ── Self-service bulk writes (BEP-1052 §3) ─────────────────── + + async def my_bulk_create( + self, + body: BodyParam[BulkCreateMyAppConfigFragmentsInput], + ) -> APIResponse: + """Self-service bulk create on the caller's `USER` row.""" + result = await self._adapter.my_bulk_create(body.parsed) + return APIResponse.build(status_code=HTTPStatus.OK, response_model=result) + + async def my_bulk_update( + self, + body: BodyParam[BulkUpdateMyAppConfigFragmentsInput], + ) -> APIResponse: + """Self-service bulk update on the caller's `USER` row.""" + result = await self._adapter.my_bulk_update(body.parsed) + return APIResponse.build(status_code=HTTPStatus.OK, response_model=result) + + +# ``AppConfigScopeType`` is imported for OpenAPI schema visibility of the +# string-form path parameter; keep the import alive. +_ = AppConfigScopeType diff --git a/src/ai/backend/manager/api/rest/v2/app_config_fragment/registry.py b/src/ai/backend/manager/api/rest/v2/app_config_fragment/registry.py new file mode 100644 index 00000000000..8e8ae67fff8 --- /dev/null +++ b/src/ai/backend/manager/api/rest/v2/app_config_fragment/registry.py @@ -0,0 +1,48 @@ +"""Route registration for v2 app-config fragment endpoints (BEP-1052 §4).""" + +from __future__ import annotations + +from typing import TYPE_CHECKING + +from ai.backend.manager.api.rest.middleware.auth import auth_required, superadmin_required +from ai.backend.manager.api.rest.routing import RouteRegistry + +from .handler import V2AppConfigFragmentHandler + +if TYPE_CHECKING: + from ai.backend.manager.api.rest.types import RouteDeps + + +def register_v2_app_config_fragment_routes( + handler: V2AppConfigFragmentHandler, + route_deps: RouteDeps, +) -> RouteRegistry: + """Register all v2 app-config fragment routes (BEP-1052 §4). + + - `POST /get` reads a single row via body (three-field natural key). + - Scoped search mounts at `/{scope_type}/{scope_id}/search`. + - Admin cross-scope search + bulk writes are admin-only. + - `/my/bulk-create` and `/my/bulk-update` are self-service writes + on the caller's `USER` row (no `/my/bulk-purge` — admin-only + cleanup per BEP-1052 §3). + """ + reg = RouteRegistry.create("app-config-fragments", route_deps.cors_options) + + # Reads + reg.add("POST", "/get", handler.get, middlewares=[auth_required]) + reg.add( + "POST", + "/{scope_type}/{scope_id}/search", + handler.scoped_search, + middlewares=[auth_required], + ) + reg.add("POST", "/search", handler.admin_search, middlewares=[superadmin_required]) + # Admin bulk writes (BEP-1052 §3 — bulk-only) + reg.add("POST", "/bulk-create", handler.admin_bulk_create, middlewares=[superadmin_required]) + reg.add("POST", "/bulk-update", handler.admin_bulk_update, middlewares=[superadmin_required]) + reg.add("POST", "/bulk-purge", handler.admin_bulk_purge, middlewares=[superadmin_required]) + # Self-service bulk writes + reg.add("POST", "/my/bulk-create", handler.my_bulk_create, middlewares=[auth_required]) + reg.add("POST", "/my/bulk-update", handler.my_bulk_update, middlewares=[auth_required]) + + return reg diff --git a/src/ai/backend/manager/api/rest/v2/path_params.py b/src/ai/backend/manager/api/rest/v2/path_params.py index 1b3ccbaff75..d9c0ed634ba 100644 --- a/src/ai/backend/manager/api/rest/v2/path_params.py +++ b/src/ai/backend/manager/api/rest/v2/path_params.py @@ -13,6 +13,20 @@ class AppConfigPolicyIdPathParam(BaseRequestModel): policy_id: UUID = Field(description="App-config policy row UUID") +class AppConfigFragmentScopePathParam(BaseRequestModel): + scope_type: str = Field(description="App-config scope type (public/domain/user/...).") + scope_id: str = Field(description="Scope id (domain name, user id, or `public`).") + + +class AppConfigMyNamePathParam(BaseRequestModel): + name: str = Field(description="Policy / config name.") + + +class AppConfigUserNamePathParam(BaseRequestModel): + user_id: UUID = Field(description="Target user's UUID (admin only).") + name: str = Field(description="Policy / config name.") + + class DomainNamePathParam(BaseRequestModel): domain_name: str = Field(description="Domain name") diff --git a/src/ai/backend/manager/api/rest/v2/tree.py b/src/ai/backend/manager/api/rest/v2/tree.py index b794551f77f..31af4a6d094 100644 --- a/src/ai/backend/manager/api/rest/v2/tree.py +++ b/src/ai/backend/manager/api/rest/v2/tree.py @@ -28,6 +28,10 @@ def build_v2_routes( # Lazy imports to avoid circular dependencies at module level from .agent.handler import V2AgentHandler from .agent.registry import register_v2_agent_routes + from .app_config.handler import V2AppConfigHandler + from .app_config.registry import register_v2_app_config_routes + from .app_config_fragment.handler import V2AppConfigFragmentHandler + from .app_config_fragment.registry import register_v2_app_config_fragment_routes from .app_config_policy.handler import V2AppConfigPolicyHandler from .app_config_policy.registry import register_v2_app_config_policy_routes from .artifact.handler import V2ArtifactHandler @@ -117,6 +121,8 @@ def build_v2_routes( # Build all handlers (each takes its individual adapter) agent_handler = V2AgentHandler(adapter=adapters.agent) + app_config_handler = V2AppConfigHandler(adapter=adapters.app_config) + app_config_fragment_handler = V2AppConfigFragmentHandler(adapter=adapters.app_config_fragment) app_config_policy_handler = V2AppConfigPolicyHandler(adapter=adapters.app_config_policy) artifact_handler = V2ArtifactHandler(adapter=adapters.artifact) artifact_registry_handler = V2ArtifactRegistryHandler(adapter=adapters.artifact_registry) @@ -174,6 +180,10 @@ def build_v2_routes( # Add all domain sub-registries v2_reg.add_subregistry(register_v2_agent_routes(agent_handler, route_deps)) + v2_reg.add_subregistry(register_v2_app_config_routes(app_config_handler, route_deps)) + v2_reg.add_subregistry( + register_v2_app_config_fragment_routes(app_config_fragment_handler, route_deps) + ) v2_reg.add_subregistry( register_v2_app_config_policy_routes(app_config_policy_handler, route_deps) ) From 4480b87794344ad48a88f107445a24c7fb1e6272 Mon Sep 17 00:00:00 2001 From: jopemachine Date: Sat, 25 Apr 2026 14:31:28 +0900 Subject: [PATCH 2/8] chore(BA-5830): add news fragment for #11286 --- changes/11286.feature.md | 1 + 1 file changed, 1 insertion(+) create mode 100644 changes/11286.feature.md diff --git a/changes/11286.feature.md b/changes/11286.feature.md new file mode 100644 index 00000000000..cfc79c37691 --- /dev/null +++ b/changes/11286.feature.md @@ -0,0 +1 @@ +Add `AppConfigFragment` REST v2 endpoints under `/v2/app-config-fragments` — `get` (body-carried natural key), scoped search `{scope_type}/{scope_id}/search`, admin cross-scope `search`, plus admin `create` / `update` / `purge` (BEP-1052 §4). From 4039bef467d1a7b50a16a45fcb2c00eb325c4339 Mon Sep 17 00:00:00 2001 From: jopemachine Date: Sat, 25 Apr 2026 17:20:28 +0900 Subject: [PATCH 3/8] fix(BA-5830): wire AppConfigAdapter into Fragment REST handler my_bulk After BA-5829 split the merged-view methods (`my_bulk_create` / `my_bulk_update`) into their own `AppConfigAdapter`, the Fragment REST handler still referenced them on `AppConfigFragmentAdapter` (404 at type-check time). The fragment routes own these URLs (the SDK already posts to `/v2/app-config-fragments/my/bulk-*`), so inject the `AppConfigAdapter` alongside the Fragment one and dispatch the my-bulk calls there. Also flips `V2AppConfigHandler` to take `AppConfigAdapter` instead of the now-stale Fragment adapter. --- .../rest/v2/app_config_fragment/handler.py | 20 +++++++++++++++---- src/ai/backend/manager/api/rest/v2/tree.py | 5 ++++- 2 files changed, 20 insertions(+), 5 deletions(-) diff --git a/src/ai/backend/manager/api/rest/v2/app_config_fragment/handler.py b/src/ai/backend/manager/api/rest/v2/app_config_fragment/handler.py index 4b3e7715b51..b260865c987 100644 --- a/src/ai/backend/manager/api/rest/v2/app_config_fragment/handler.py +++ b/src/ai/backend/manager/api/rest/v2/app_config_fragment/handler.py @@ -30,16 +30,28 @@ ) if TYPE_CHECKING: + from ai.backend.manager.api.adapters.app_config import AppConfigAdapter from ai.backend.manager.api.adapters.app_config_fragment import AppConfigFragmentAdapter log: Final = BraceStyleAdapter(logging.getLogger(__spec__.name)) class V2AppConfigFragmentHandler: - """REST v2 handler for app-config fragment operations.""" + """REST v2 handler for app-config fragment operations. - def __init__(self, *, adapter: AppConfigFragmentAdapter) -> None: + Self-service `my_bulk_*` writes return recomputed merged + `AppConfig` views, so they are dispatched to `AppConfigAdapter`; + everything else is the raw-row Fragment surface. + """ + + def __init__( + self, + *, + adapter: AppConfigFragmentAdapter, + app_config_adapter: AppConfigAdapter, + ) -> None: self._adapter = adapter + self._app_config_adapter = app_config_adapter # ── Reads ──────────────────────────────────────────────────── @@ -107,7 +119,7 @@ async def my_bulk_create( body: BodyParam[BulkCreateMyAppConfigFragmentsInput], ) -> APIResponse: """Self-service bulk create on the caller's `USER` row.""" - result = await self._adapter.my_bulk_create(body.parsed) + result = await self._app_config_adapter.my_bulk_create(body.parsed) return APIResponse.build(status_code=HTTPStatus.OK, response_model=result) async def my_bulk_update( @@ -115,7 +127,7 @@ async def my_bulk_update( body: BodyParam[BulkUpdateMyAppConfigFragmentsInput], ) -> APIResponse: """Self-service bulk update on the caller's `USER` row.""" - result = await self._adapter.my_bulk_update(body.parsed) + result = await self._app_config_adapter.my_bulk_update(body.parsed) return APIResponse.build(status_code=HTTPStatus.OK, response_model=result) diff --git a/src/ai/backend/manager/api/rest/v2/tree.py b/src/ai/backend/manager/api/rest/v2/tree.py index 31af4a6d094..8c0364ee9e0 100644 --- a/src/ai/backend/manager/api/rest/v2/tree.py +++ b/src/ai/backend/manager/api/rest/v2/tree.py @@ -122,7 +122,10 @@ def build_v2_routes( # Build all handlers (each takes its individual adapter) agent_handler = V2AgentHandler(adapter=adapters.agent) app_config_handler = V2AppConfigHandler(adapter=adapters.app_config) - app_config_fragment_handler = V2AppConfigFragmentHandler(adapter=adapters.app_config_fragment) + app_config_fragment_handler = V2AppConfigFragmentHandler( + adapter=adapters.app_config_fragment, + app_config_adapter=adapters.app_config, + ) app_config_policy_handler = V2AppConfigPolicyHandler(adapter=adapters.app_config_policy) artifact_handler = V2ArtifactHandler(adapter=adapters.artifact) artifact_registry_handler = V2ArtifactRegistryHandler(adapter=adapters.artifact_registry) From 541b2cb9d902f3a646254537836249275489e1a0 Mon Sep 17 00:00:00 2001 From: Gyubong Date: Sun, 26 Apr 2026 20:58:35 +0900 Subject: [PATCH 4/8] refactor(BA-5830): propagate my_bulk_* rename + final BEP cleanup MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Carry the BA-5836 / BA-5829 rename (`bulk_create_my` → `my_bulk_create`, `bulk_update_my` → `my_bulk_update`) through the REST v2 surface, the GQL schema, and the dumped GraphQL schema artefacts. Strip the remaining `BEP-1052 §X` references from the REST v2 handlers / registries, the legacy AppConfig drop migration, and the GraphQL schema dump comments. Only the news fragments retain the BEP wording (intentional). Co-Authored-By: Claude Opus 4.7 (1M context) --- .../common/dto/manager/v2/app_config/response.py | 4 ++-- .../api/gql/app_config/types/bulk_payloads.py | 4 ++-- .../manager/api/rest/v2/app_config/handler.py | 2 +- .../manager/api/rest/v2/app_config/registry.py | 4 ++-- .../api/rest/v2/app_config_fragment/handler.py | 14 +++++++------- .../api/rest/v2/app_config_fragment/registry.py | 8 ++++---- .../84d5c6daf8cc_drop_legacy_app_configs_table.py | 2 +- 7 files changed, 19 insertions(+), 19 deletions(-) diff --git a/src/ai/backend/common/dto/manager/v2/app_config/response.py b/src/ai/backend/common/dto/manager/v2/app_config/response.py index 7369aab242d..60cacfdb8ec 100644 --- a/src/ai/backend/common/dto/manager/v2/app_config/response.py +++ b/src/ai/backend/common/dto/manager/v2/app_config/response.py @@ -63,7 +63,7 @@ class SearchAppConfigsPayload(BaseResponseModel): class MyBulkCreateAppConfigFragmentsPayload(BaseResponseModel): - """Payload for `bulkCreateMyAppConfigFragments`. + """Payload for `myBulkCreateAppConfigFragments`. Each successfully created row produces a recomputed merged `AppConfigNode`; failures are collected per-item. @@ -76,7 +76,7 @@ class MyBulkCreateAppConfigFragmentsPayload(BaseResponseModel): class MyBulkUpdateAppConfigFragmentsPayload(BaseResponseModel): - """Payload for `bulkUpdateMyAppConfigFragments`.""" + """Payload for `myBulkUpdateAppConfigFragments`.""" updated: list[AppConfigNode] = Field( description="Recomputed merged AppConfig views for each updated USER fragment.", diff --git a/src/ai/backend/manager/api/gql/app_config/types/bulk_payloads.py b/src/ai/backend/manager/api/gql/app_config/types/bulk_payloads.py index f5e738bb818..408d8c09b8c 100644 --- a/src/ai/backend/manager/api/gql/app_config/types/bulk_payloads.py +++ b/src/ai/backend/manager/api/gql/app_config/types/bulk_payloads.py @@ -24,7 +24,7 @@ @gql_pydantic_type( BackendAIGQLMeta( added_version=NEXT_RELEASE_VERSION, - description="Payload for `bulkCreateMyAppConfigFragments` (recomputed views).", + description="Payload for `myBulkCreateAppConfigFragments` (recomputed views).", ), model=MyBulkCreatePayloadDTO, name="MyBulkCreateAppConfigFragmentsPayload", @@ -41,7 +41,7 @@ class MyBulkCreateAppConfigFragmentsPayloadGQL(PydanticOutputMixin[MyBulkCreateP @gql_pydantic_type( BackendAIGQLMeta( added_version=NEXT_RELEASE_VERSION, - description="Payload for `bulkUpdateMyAppConfigFragments` (recomputed views).", + description="Payload for `myBulkUpdateAppConfigFragments` (recomputed views).", ), model=MyBulkUpdatePayloadDTO, name="MyBulkUpdateAppConfigFragmentsPayload", diff --git a/src/ai/backend/manager/api/rest/v2/app_config/handler.py b/src/ai/backend/manager/api/rest/v2/app_config/handler.py index f813a91b6a1..777a5a568ff 100644 --- a/src/ai/backend/manager/api/rest/v2/app_config/handler.py +++ b/src/ai/backend/manager/api/rest/v2/app_config/handler.py @@ -1,4 +1,4 @@ -"""REST v2 handler for the AppConfig merged-view domain (BEP-1052 §5).""" +"""REST v2 handler for the AppConfig merged-view domain.""" from __future__ import annotations diff --git a/src/ai/backend/manager/api/rest/v2/app_config/registry.py b/src/ai/backend/manager/api/rest/v2/app_config/registry.py index b70d0ce48e6..62f2c0ec681 100644 --- a/src/ai/backend/manager/api/rest/v2/app_config/registry.py +++ b/src/ai/backend/manager/api/rest/v2/app_config/registry.py @@ -1,4 +1,4 @@ -"""Route registration for v2 AppConfig merged-view endpoints (BEP-1052 §5).""" +"""Route registration for v2 AppConfig merged-view endpoints.""" from __future__ import annotations @@ -17,7 +17,7 @@ def register_v2_app_config_routes( handler: V2AppConfigHandler, route_deps: RouteDeps, ) -> RouteRegistry: - """Register all v2 `/v2/app-configs/*` routes (BEP-1052 §4). + """Register all v2 `/v2/app-configs/*` routes. Read-only surface — writes go through `/v2/app-config-fragments/...` (§4). Self-service routes land under the `/my/...` prefix so the diff --git a/src/ai/backend/manager/api/rest/v2/app_config_fragment/handler.py b/src/ai/backend/manager/api/rest/v2/app_config_fragment/handler.py index b260865c987..0f4c42597de 100644 --- a/src/ai/backend/manager/api/rest/v2/app_config_fragment/handler.py +++ b/src/ai/backend/manager/api/rest/v2/app_config_fragment/handler.py @@ -1,4 +1,4 @@ -"""REST v2 handler for the app-config fragment domain (BEP-1052 §4). +"""REST v2 handler for the app-config fragment domain. Writes are **bulk-only** per BEP §3 — the single-item create / update / purge endpoints were removed in favour of `/bulk-create`, @@ -18,8 +18,8 @@ AdminBulkPurgeAppConfigFragmentsInput, AdminBulkUpdateAppConfigFragmentsInput, AppConfigFragmentKeyInput, - BulkCreateMyAppConfigFragmentsInput, - BulkUpdateMyAppConfigFragmentsInput, + MyBulkCreateAppConfigFragmentsInput, + MyBulkUpdateAppConfigFragmentsInput, SearchAppConfigFragmentsInput, ) from ai.backend.common.dto.manager.v2.app_config_fragment.types import AppConfigScopeType @@ -86,7 +86,7 @@ async def admin_search( result = await self._adapter.admin_search(body.parsed) return APIResponse.build(status_code=HTTPStatus.OK, response_model=result) - # ── Admin bulk writes (BEP-1052 §3) ────────────────────────── + # ── Admin bulk writes ────────────────────────── async def admin_bulk_create( self, @@ -112,11 +112,11 @@ async def admin_bulk_purge( result = await self._adapter.admin_bulk_purge(body.parsed) return APIResponse.build(status_code=HTTPStatus.OK, response_model=result) - # ── Self-service bulk writes (BEP-1052 §3) ─────────────────── + # ── Self-service bulk writes ─────────────────── async def my_bulk_create( self, - body: BodyParam[BulkCreateMyAppConfigFragmentsInput], + body: BodyParam[MyBulkCreateAppConfigFragmentsInput], ) -> APIResponse: """Self-service bulk create on the caller's `USER` row.""" result = await self._app_config_adapter.my_bulk_create(body.parsed) @@ -124,7 +124,7 @@ async def my_bulk_create( async def my_bulk_update( self, - body: BodyParam[BulkUpdateMyAppConfigFragmentsInput], + body: BodyParam[MyBulkUpdateAppConfigFragmentsInput], ) -> APIResponse: """Self-service bulk update on the caller's `USER` row.""" result = await self._app_config_adapter.my_bulk_update(body.parsed) diff --git a/src/ai/backend/manager/api/rest/v2/app_config_fragment/registry.py b/src/ai/backend/manager/api/rest/v2/app_config_fragment/registry.py index 8e8ae67fff8..ed242482ffe 100644 --- a/src/ai/backend/manager/api/rest/v2/app_config_fragment/registry.py +++ b/src/ai/backend/manager/api/rest/v2/app_config_fragment/registry.py @@ -1,4 +1,4 @@ -"""Route registration for v2 app-config fragment endpoints (BEP-1052 §4).""" +"""Route registration for v2 app-config fragment endpoints.""" from __future__ import annotations @@ -17,14 +17,14 @@ def register_v2_app_config_fragment_routes( handler: V2AppConfigFragmentHandler, route_deps: RouteDeps, ) -> RouteRegistry: - """Register all v2 app-config fragment routes (BEP-1052 §4). + """Register all v2 app-config fragment routes. - `POST /get` reads a single row via body (three-field natural key). - Scoped search mounts at `/{scope_type}/{scope_id}/search`. - Admin cross-scope search + bulk writes are admin-only. - `/my/bulk-create` and `/my/bulk-update` are self-service writes on the caller's `USER` row (no `/my/bulk-purge` — admin-only - cleanup per BEP-1052 §3). + cleanup ). """ reg = RouteRegistry.create("app-config-fragments", route_deps.cors_options) @@ -37,7 +37,7 @@ def register_v2_app_config_fragment_routes( middlewares=[auth_required], ) reg.add("POST", "/search", handler.admin_search, middlewares=[superadmin_required]) - # Admin bulk writes (BEP-1052 §3 — bulk-only) + # Admin bulk writes (bulk-only) reg.add("POST", "/bulk-create", handler.admin_bulk_create, middlewares=[superadmin_required]) reg.add("POST", "/bulk-update", handler.admin_bulk_update, middlewares=[superadmin_required]) reg.add("POST", "/bulk-purge", handler.admin_bulk_purge, middlewares=[superadmin_required]) diff --git a/src/ai/backend/manager/models/alembic/versions/84d5c6daf8cc_drop_legacy_app_configs_table.py b/src/ai/backend/manager/models/alembic/versions/84d5c6daf8cc_drop_legacy_app_configs_table.py index abaf59466da..28ff20b92c3 100644 --- a/src/ai/backend/manager/models/alembic/versions/84d5c6daf8cc_drop_legacy_app_configs_table.py +++ b/src/ai/backend/manager/models/alembic/versions/84d5c6daf8cc_drop_legacy_app_configs_table.py @@ -1,7 +1,7 @@ """drop legacy app_configs table Removes the predecessor `app_configs` table and its enum type as -preparation for BEP-1052 (Scoped App Config Redesign). The +preparation for the scoped app-config redesign. The replacement tables (`app_config_fragments`, `app_config_policies`) are introduced in a follow-up migration on top of this one — the new shape is incompatible with the old (different scope enum, From b88c09d2663a5e301d25a0e6a676c848094c9d8b Mon Sep 17 00:00:00 2001 From: Gyubong Date: Sun, 26 Apr 2026 21:56:00 +0900 Subject: [PATCH 5/8] chore(BA-5830): refresh schema dump after rebase reset my_bulk_* renames The `-X theirs` rebase onto BA-5829 reset the schema dump back to the older `BulkCreateMy*` / `bulkCreateMy*` field / type / mutation names. Re-apply the rename to the dump artefacts so the api-updated check sees a clean diff against the regenerated schema. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../graphql-reference/supergraph.graphql | 143 ++++++++---------- .../graphql-reference/v2-schema.graphql | 125 +++++++-------- 2 files changed, 114 insertions(+), 154 deletions(-) diff --git a/docs/manager/graphql-reference/supergraph.graphql b/docs/manager/graphql-reference/supergraph.graphql index b015d56e2ed..936ded93d1d 100644 --- a/docs/manager/graphql-reference/supergraph.graphql +++ b/docs/manager/graphql-reference/supergraph.graphql @@ -202,10 +202,8 @@ input AdminAppConfigFragmentItemInput config: JSON! } -""" -Added in UNRELEASED. Per-item input for admin bulk create — `config_name` + initial `scope_sources`. -""" -input AdminAppConfigPolicyCreateItemInput +"""Added in UNRELEASED. Per-item input for admin bulk create / update.""" +input AdminAppConfigPolicyItemInput @join__type(graph: STRAWBERRY) { """Unique, immutable policy name.""" @@ -215,19 +213,6 @@ input AdminAppConfigPolicyCreateItemInput scopeSources: [String!]! } -""" -Added in UNRELEASED. Per-item input for admin bulk update — target row id + new `scope_sources`. -""" -input AdminAppConfigPolicyUpdateItemInput - @join__type(graph: STRAWBERRY) -{ - """Policy row id.""" - id: UUID! - - """Ordered scope chain.""" - scopeSources: [String!]! -} - """Added in UNRELEASED. Admin bulk create input — items carry any scope.""" input AdminBulkCreateAppConfigFragmentInput @join__type(graph: STRAWBERRY) @@ -263,7 +248,7 @@ input AdminBulkCreateAppConfigPolicyInput @join__type(graph: STRAWBERRY) { """Policies to create.""" - items: [AdminAppConfigPolicyCreateItemInput!]! + items: [AdminAppConfigPolicyItemInput!]! } """ @@ -291,21 +276,21 @@ type AdminBulkPurgeAppConfigFragmentsPayload type AdminBulkPurgeAppConfigPoliciesPayload @join__type(graph: STRAWBERRY) { - """Ids of policies actually removed (absent ids no-oped).""" - purgedIds: [UUID!]! + """`config_name`s of policies actually removed (absent names no-oped).""" + purgedConfigNames: [String!]! """Per-item failures.""" failed: [AppConfigPolicyBulkError!]! } """ -Added in UNRELEASED. Admin bulk purge input for app-config policies (keyed on row id). +Added in UNRELEASED. Admin bulk purge input for app-config policies (keyed on `config_name`). """ input AdminBulkPurgeAppConfigPolicyInput @join__type(graph: STRAWBERRY) { - """Policy row ids to purge.""" - ids: [UUID!]! + """`config_name`s to purge.""" + configNames: [String!]! } """Added in UNRELEASED. Admin bulk update input.""" @@ -343,7 +328,7 @@ input AdminBulkUpdateAppConfigPolicyInput @join__type(graph: STRAWBERRY) { """Policies to update.""" - items: [AdminAppConfigPolicyUpdateItemInput!]! + items: [AdminAppConfigPolicyItemInput!]! } """Added in 26.4.2. Admin input for creating a keypair for a user.""" @@ -1078,12 +1063,6 @@ input AppConfigFilter """Filter by target user id (admin cross-user search only).""" userId: UUIDFilter = null - - """Filter by the oldest contributing fragment's creation timestamp.""" - createdAt: DateTimeFilter = null - - """Filter by the latest contributing fragment's update timestamp.""" - updatedAt: DateTimeFilter = null } """Added in UNRELEASED. Raw per-scope app-config fragment.""" @@ -1182,7 +1161,6 @@ enum AppConfigFragmentOrderField SCOPE_ID @join__enumValue(graph: STRAWBERRY) NAME @join__enumValue(graph: STRAWBERRY) CREATED_AT @join__enumValue(graph: STRAWBERRY) - UPDATED_AT @join__enumValue(graph: STRAWBERRY) } """Added in UNRELEASED. Specifies ordering for merged AppConfig results.""" @@ -1202,8 +1180,6 @@ enum AppConfigOrderField { USER_ID @join__enumValue(graph: STRAWBERRY) NAME @join__enumValue(graph: STRAWBERRY) - CREATED_AT @join__enumValue(graph: STRAWBERRY) - UPDATED_AT @join__enumValue(graph: STRAWBERRY) } """Added in UNRELEASED. Scoped app-config policy.""" @@ -1234,6 +1210,9 @@ type AppConfigPolicyBulkError """Original position in the input list.""" index: Int! + """`config_name` of the failed row.""" + configName: String! + """Reason for the failure.""" message: String! } @@ -2285,6 +2264,29 @@ type BulkAssignRolePayload failed: [BulkAssignRoleError!]! } +""" +Added in UNRELEASED. Self-service bulk create — scope is `USER` / `current_user`. +""" +input MyBulkCreateAppConfigFragmentInput + @join__type(graph: STRAWBERRY) +{ + """USER-scope rows to create.""" + items: [MyAppConfigFragmentItemInput!]! +} + +""" +Added in UNRELEASED. Payload for `myBulkCreateAppConfigFragments` (recomputed views). +""" +type MyBulkCreateAppConfigFragmentsPayload + @join__type(graph: STRAWBERRY) +{ + """Recomputed merged AppConfig views for each created USER fragment.""" + created: [AppConfig!]! + + """Per-item failures.""" + failed: [AppConfigFragmentBulkError!]! +} + """Added in 26.2.0. Payload for bulk user creation mutation.""" type BulkCreateUsersV2Payload @join__type(graph: STRAWBERRY) @@ -2437,6 +2439,29 @@ type BulkRevokeRolePayload failed: [BulkRevokeRoleError!]! } +""" +Added in UNRELEASED. Self-service bulk update — scope is `USER` / `current_user`. +""" +input MyBulkUpdateAppConfigFragmentInput + @join__type(graph: STRAWBERRY) +{ + """USER-scope rows to update.""" + items: [MyAppConfigFragmentItemInput!]! +} + +""" +Added in UNRELEASED. Payload for `myBulkUpdateAppConfigFragments` (recomputed views). +""" +type MyBulkUpdateAppConfigFragmentsPayload + @join__type(graph: STRAWBERRY) +{ + """Recomputed merged AppConfig views for each updated USER fragment.""" + updated: [AppConfig!]! + + """Per-item failures.""" + failed: [AppConfigFragmentBulkError!]! +} + """Added in 26.3.0. Payload for bulk user update mutation.""" type BulkUpdateUsersV2Payload @join__type(graph: STRAWBERRY) @@ -11287,7 +11312,7 @@ type Mutation adminBulkUpdateAppConfigPolicies(input: AdminBulkUpdateAppConfigPolicyInput!): AdminBulkUpdateAppConfigPoliciesPayload! @join__field(graph: STRAWBERRY) """ - Added in UNRELEASED. Hard-delete policies by row id; rows still referenced by fragments surface in `failed`. Admin only. + Added in UNRELEASED. Rejects items whose `config_name` still has referencing fragment rows. Admin only. """ adminBulkPurgeAppConfigPolicies(input: AdminBulkPurgeAppConfigPolicyInput!): AdminBulkPurgeAppConfigPoliciesPayload! @join__field(graph: STRAWBERRY) @@ -11546,52 +11571,6 @@ input MyAppConfigFragmentItemInput config: JSON! } -""" -Added in UNRELEASED. Self-service bulk create — scope is `USER` / `current_user`. -""" -input MyBulkCreateAppConfigFragmentInput - @join__type(graph: STRAWBERRY) -{ - """USER-scope rows to create.""" - items: [MyAppConfigFragmentItemInput!]! -} - -""" -Added in UNRELEASED. Payload for `bulkCreateMyAppConfigFragments` (recomputed views). -""" -type MyBulkCreateAppConfigFragmentsPayload - @join__type(graph: STRAWBERRY) -{ - """Recomputed merged AppConfig views for each created USER fragment.""" - created: [AppConfig!]! - - """Per-item failures.""" - failed: [AppConfigFragmentBulkError!]! -} - -""" -Added in UNRELEASED. Self-service bulk update — scope is `USER` / `current_user`. -""" -input MyBulkUpdateAppConfigFragmentInput - @join__type(graph: STRAWBERRY) -{ - """USER-scope rows to update.""" - items: [MyAppConfigFragmentItemInput!]! -} - -""" -Added in UNRELEASED. Payload for `bulkUpdateMyAppConfigFragments` (recomputed views). -""" -type MyBulkUpdateAppConfigFragmentsPayload - @join__type(graph: STRAWBERRY) -{ - """Recomputed merged AppConfig views for each updated USER fragment.""" - updated: [AppConfig!]! - - """Per-item failures.""" - failed: [AppConfigFragmentBulkError!]! -} - """ Added in 26.4.2. Query result returning the current client's IP address. """ @@ -13905,9 +13884,9 @@ type Query adminImageAliases(filter: ImageV2AliasFilter = null, orderBy: [ImageV2AliasOrderByGQL!] = null, before: String = null, after: String = null, first: Int = null, last: Int = null, limit: Int = null, offset: Int = null): ImageV2AliasConnection @join__field(graph: STRAWBERRY) """ - Added in UNRELEASED. Get a single app-config policy by row id. Available to any authenticated user. + Added in UNRELEASED. Get a single app-config policy by `config_name`. Available to any authenticated user. """ - appConfigPolicy(id: UUID!): AppConfigPolicy @join__field(graph: STRAWBERRY) + appConfigPolicy(configName: String!): AppConfigPolicy @join__field(graph: STRAWBERRY) """ Added in UNRELEASED. List app-config policies with filtering and pagination. Available to any authenticated user. diff --git a/docs/manager/graphql-reference/v2-schema.graphql b/docs/manager/graphql-reference/v2-schema.graphql index d73edf37a13..4d90fc246f1 100644 --- a/docs/manager/graphql-reference/v2-schema.graphql +++ b/docs/manager/graphql-reference/v2-schema.graphql @@ -154,10 +154,8 @@ input AdminAppConfigFragmentItemInput { config: JSON! } -""" -Added in UNRELEASED. Per-item input for admin bulk create — `config_name` + initial `scope_sources`. -""" -input AdminAppConfigPolicyCreateItemInput { +"""Added in UNRELEASED. Per-item input for admin bulk create / update.""" +input AdminAppConfigPolicyItemInput { """Unique, immutable policy name.""" configName: String! @@ -165,17 +163,6 @@ input AdminAppConfigPolicyCreateItemInput { scopeSources: [String!]! } -""" -Added in UNRELEASED. Per-item input for admin bulk update — target row id + new `scope_sources`. -""" -input AdminAppConfigPolicyUpdateItemInput { - """Policy row id.""" - id: UUID! - - """Ordered scope chain.""" - scopeSources: [String!]! -} - """Added in UNRELEASED. Admin bulk create input — items carry any scope.""" input AdminBulkCreateAppConfigFragmentInput { """Rows to create.""" @@ -203,7 +190,7 @@ type AdminBulkCreateAppConfigPoliciesPayload { """Added in UNRELEASED. Admin bulk create input for app-config policies.""" input AdminBulkCreateAppConfigPolicyInput { """Policies to create.""" - items: [AdminAppConfigPolicyCreateItemInput!]! + items: [AdminAppConfigPolicyItemInput!]! } """ @@ -225,19 +212,19 @@ type AdminBulkPurgeAppConfigFragmentsPayload { """Added in UNRELEASED. Payload for `adminBulkPurgeAppConfigPolicies`.""" type AdminBulkPurgeAppConfigPoliciesPayload { - """Ids of policies actually removed (absent ids no-oped).""" - purgedIds: [UUID!]! + """`config_name`s of policies actually removed (absent names no-oped).""" + purgedConfigNames: [String!]! """Per-item failures.""" failed: [AppConfigPolicyBulkError!]! } """ -Added in UNRELEASED. Admin bulk purge input for app-config policies (keyed on row id). +Added in UNRELEASED. Admin bulk purge input for app-config policies (keyed on `config_name`). """ input AdminBulkPurgeAppConfigPolicyInput { - """Policy row ids to purge.""" - ids: [UUID!]! + """`config_name`s to purge.""" + configNames: [String!]! } """Added in UNRELEASED. Admin bulk update input.""" @@ -267,7 +254,7 @@ type AdminBulkUpdateAppConfigPoliciesPayload { """Added in UNRELEASED. Admin bulk update input for app-config policies.""" input AdminBulkUpdateAppConfigPolicyInput { """Policies to update.""" - items: [AdminAppConfigPolicyUpdateItemInput!]! + items: [AdminAppConfigPolicyItemInput!]! } """Added in 26.4.2. Admin input for creating a keypair for a user.""" @@ -758,12 +745,6 @@ input AppConfigFilter { """Filter by target user id (admin cross-user search only).""" userId: UUIDFilter = null - - """Filter by the oldest contributing fragment's creation timestamp.""" - createdAt: DateTimeFilter = null - - """Filter by the latest contributing fragment's update timestamp.""" - updatedAt: DateTimeFilter = null } """Added in UNRELEASED. Raw per-scope app-config fragment.""" @@ -850,7 +831,6 @@ enum AppConfigFragmentOrderField { SCOPE_ID NAME CREATED_AT - UPDATED_AT } """Added in UNRELEASED. Specifies ordering for merged AppConfig results.""" @@ -866,8 +846,6 @@ input AppConfigOrderBy { enum AppConfigOrderField { USER_ID NAME - CREATED_AT - UPDATED_AT } """Added in UNRELEASED. Scoped app-config policy.""" @@ -893,6 +871,9 @@ type AppConfigPolicyBulkError { """Original position in the input list.""" index: Int! + """`config_name` of the failed row.""" + configName: String! + """Reason for the failure.""" message: String! } @@ -1620,6 +1601,25 @@ type BulkAssignRolePayload { failed: [BulkAssignRoleError!]! } +""" +Added in UNRELEASED. Self-service bulk create — scope is `USER` / `current_user`. +""" +input MyBulkCreateAppConfigFragmentInput { + """USER-scope rows to create.""" + items: [MyAppConfigFragmentItemInput!]! +} + +""" +Added in UNRELEASED. Payload for `myBulkCreateAppConfigFragments` (recomputed views). +""" +type MyBulkCreateAppConfigFragmentsPayload { + """Recomputed merged AppConfig views for each created USER fragment.""" + created: [AppConfig!]! + + """Per-item failures.""" + failed: [AppConfigFragmentBulkError!]! +} + """Added in 26.2.0. Error information for a failed user in bulk creation.""" type BulkCreateUserV2Error { """Original position in the input list.""" @@ -1744,6 +1744,25 @@ type BulkRevokeRolePayload { failed: [BulkRevokeRoleError!]! } +""" +Added in UNRELEASED. Self-service bulk update — scope is `USER` / `current_user`. +""" +input MyBulkUpdateAppConfigFragmentInput { + """USER-scope rows to update.""" + items: [MyAppConfigFragmentItemInput!]! +} + +""" +Added in UNRELEASED. Payload for `myBulkUpdateAppConfigFragments` (recomputed views). +""" +type MyBulkUpdateAppConfigFragmentsPayload { + """Recomputed merged AppConfig views for each updated USER fragment.""" + updated: [AppConfig!]! + + """Per-item failures.""" + failed: [AppConfigFragmentBulkError!]! +} + """Added in 26.3.0. Error information for a failed user in bulk update.""" type BulkUpdateUserV2Error { """UUID of the user that failed to update.""" @@ -7177,7 +7196,7 @@ type Mutation { adminBulkUpdateAppConfigPolicies(input: AdminBulkUpdateAppConfigPolicyInput!): AdminBulkUpdateAppConfigPoliciesPayload! """ - Added in UNRELEASED. Hard-delete policies by row id; rows still referenced by fragments surface in `failed`. Admin only. + Added in UNRELEASED. Rejects items whose `config_name` still has referencing fragment rows. Admin only. """ adminBulkPurgeAppConfigPolicies(input: AdminBulkPurgeAppConfigPolicyInput!): AdminBulkPurgeAppConfigPoliciesPayload! @@ -7434,44 +7453,6 @@ input MyAppConfigFragmentItemInput { config: JSON! } -""" -Added in UNRELEASED. Self-service bulk create — scope is `USER` / `current_user`. -""" -input MyBulkCreateAppConfigFragmentInput { - """USER-scope rows to create.""" - items: [MyAppConfigFragmentItemInput!]! -} - -""" -Added in UNRELEASED. Payload for `bulkCreateMyAppConfigFragments` (recomputed views). -""" -type MyBulkCreateAppConfigFragmentsPayload { - """Recomputed merged AppConfig views for each created USER fragment.""" - created: [AppConfig!]! - - """Per-item failures.""" - failed: [AppConfigFragmentBulkError!]! -} - -""" -Added in UNRELEASED. Self-service bulk update — scope is `USER` / `current_user`. -""" -input MyBulkUpdateAppConfigFragmentInput { - """USER-scope rows to update.""" - items: [MyAppConfigFragmentItemInput!]! -} - -""" -Added in UNRELEASED. Payload for `bulkUpdateMyAppConfigFragments` (recomputed views). -""" -type MyBulkUpdateAppConfigFragmentsPayload { - """Recomputed merged AppConfig views for each updated USER fragment.""" - updated: [AppConfig!]! - - """Per-item failures.""" - failed: [AppConfigFragmentBulkError!]! -} - """ Added in 26.4.2. Query result returning the current client's IP address. """ @@ -8998,9 +8979,9 @@ type Query { adminImageAliases(filter: ImageV2AliasFilter = null, orderBy: [ImageV2AliasOrderByGQL!] = null, before: String = null, after: String = null, first: Int = null, last: Int = null, limit: Int = null, offset: Int = null): ImageV2AliasConnection """ - Added in UNRELEASED. Get a single app-config policy by row id. Available to any authenticated user. + Added in UNRELEASED. Get a single app-config policy by `config_name`. Available to any authenticated user. """ - appConfigPolicy(id: UUID!): AppConfigPolicy + appConfigPolicy(configName: String!): AppConfigPolicy """ Added in UNRELEASED. List app-config policies with filtering and pagination. Available to any authenticated user. From 0a5c76d4f73404b31e24211ffdcbddff4736e946 Mon Sep 17 00:00:00 2001 From: Gyubong Date: Sun, 26 Apr 2026 13:10:28 +0000 Subject: [PATCH 6/8] chore: update api schema dump Co-authored-by: octodog --- .../graphql-reference/supergraph.graphql | 92 +++++++++---------- .../graphql-reference/v2-schema.graphql | 76 +++++++-------- 2 files changed, 84 insertions(+), 84 deletions(-) diff --git a/docs/manager/graphql-reference/supergraph.graphql b/docs/manager/graphql-reference/supergraph.graphql index 936ded93d1d..3a7a2640163 100644 --- a/docs/manager/graphql-reference/supergraph.graphql +++ b/docs/manager/graphql-reference/supergraph.graphql @@ -2264,29 +2264,6 @@ type BulkAssignRolePayload failed: [BulkAssignRoleError!]! } -""" -Added in UNRELEASED. Self-service bulk create — scope is `USER` / `current_user`. -""" -input MyBulkCreateAppConfigFragmentInput - @join__type(graph: STRAWBERRY) -{ - """USER-scope rows to create.""" - items: [MyAppConfigFragmentItemInput!]! -} - -""" -Added in UNRELEASED. Payload for `myBulkCreateAppConfigFragments` (recomputed views). -""" -type MyBulkCreateAppConfigFragmentsPayload - @join__type(graph: STRAWBERRY) -{ - """Recomputed merged AppConfig views for each created USER fragment.""" - created: [AppConfig!]! - - """Per-item failures.""" - failed: [AppConfigFragmentBulkError!]! -} - """Added in 26.2.0. Payload for bulk user creation mutation.""" type BulkCreateUsersV2Payload @join__type(graph: STRAWBERRY) @@ -2439,29 +2416,6 @@ type BulkRevokeRolePayload failed: [BulkRevokeRoleError!]! } -""" -Added in UNRELEASED. Self-service bulk update — scope is `USER` / `current_user`. -""" -input MyBulkUpdateAppConfigFragmentInput - @join__type(graph: STRAWBERRY) -{ - """USER-scope rows to update.""" - items: [MyAppConfigFragmentItemInput!]! -} - -""" -Added in UNRELEASED. Payload for `myBulkUpdateAppConfigFragments` (recomputed views). -""" -type MyBulkUpdateAppConfigFragmentsPayload - @join__type(graph: STRAWBERRY) -{ - """Recomputed merged AppConfig views for each updated USER fragment.""" - updated: [AppConfig!]! - - """Per-item failures.""" - failed: [AppConfigFragmentBulkError!]! -} - """Added in 26.3.0. Payload for bulk user update mutation.""" type BulkUpdateUsersV2Payload @join__type(graph: STRAWBERRY) @@ -11571,6 +11525,52 @@ input MyAppConfigFragmentItemInput config: JSON! } +""" +Added in UNRELEASED. Self-service bulk create — scope is `USER` / `current_user`. +""" +input MyBulkCreateAppConfigFragmentInput + @join__type(graph: STRAWBERRY) +{ + """USER-scope rows to create.""" + items: [MyAppConfigFragmentItemInput!]! +} + +""" +Added in UNRELEASED. Payload for `myBulkCreateAppConfigFragments` (recomputed views). +""" +type MyBulkCreateAppConfigFragmentsPayload + @join__type(graph: STRAWBERRY) +{ + """Recomputed merged AppConfig views for each created USER fragment.""" + created: [AppConfig!]! + + """Per-item failures.""" + failed: [AppConfigFragmentBulkError!]! +} + +""" +Added in UNRELEASED. Self-service bulk update — scope is `USER` / `current_user`. +""" +input MyBulkUpdateAppConfigFragmentInput + @join__type(graph: STRAWBERRY) +{ + """USER-scope rows to update.""" + items: [MyAppConfigFragmentItemInput!]! +} + +""" +Added in UNRELEASED. Payload for `myBulkUpdateAppConfigFragments` (recomputed views). +""" +type MyBulkUpdateAppConfigFragmentsPayload + @join__type(graph: STRAWBERRY) +{ + """Recomputed merged AppConfig views for each updated USER fragment.""" + updated: [AppConfig!]! + + """Per-item failures.""" + failed: [AppConfigFragmentBulkError!]! +} + """ Added in 26.4.2. Query result returning the current client's IP address. """ diff --git a/docs/manager/graphql-reference/v2-schema.graphql b/docs/manager/graphql-reference/v2-schema.graphql index 4d90fc246f1..b371196bf23 100644 --- a/docs/manager/graphql-reference/v2-schema.graphql +++ b/docs/manager/graphql-reference/v2-schema.graphql @@ -1601,25 +1601,6 @@ type BulkAssignRolePayload { failed: [BulkAssignRoleError!]! } -""" -Added in UNRELEASED. Self-service bulk create — scope is `USER` / `current_user`. -""" -input MyBulkCreateAppConfigFragmentInput { - """USER-scope rows to create.""" - items: [MyAppConfigFragmentItemInput!]! -} - -""" -Added in UNRELEASED. Payload for `myBulkCreateAppConfigFragments` (recomputed views). -""" -type MyBulkCreateAppConfigFragmentsPayload { - """Recomputed merged AppConfig views for each created USER fragment.""" - created: [AppConfig!]! - - """Per-item failures.""" - failed: [AppConfigFragmentBulkError!]! -} - """Added in 26.2.0. Error information for a failed user in bulk creation.""" type BulkCreateUserV2Error { """Original position in the input list.""" @@ -1744,25 +1725,6 @@ type BulkRevokeRolePayload { failed: [BulkRevokeRoleError!]! } -""" -Added in UNRELEASED. Self-service bulk update — scope is `USER` / `current_user`. -""" -input MyBulkUpdateAppConfigFragmentInput { - """USER-scope rows to update.""" - items: [MyAppConfigFragmentItemInput!]! -} - -""" -Added in UNRELEASED. Payload for `myBulkUpdateAppConfigFragments` (recomputed views). -""" -type MyBulkUpdateAppConfigFragmentsPayload { - """Recomputed merged AppConfig views for each updated USER fragment.""" - updated: [AppConfig!]! - - """Per-item failures.""" - failed: [AppConfigFragmentBulkError!]! -} - """Added in 26.3.0. Error information for a failed user in bulk update.""" type BulkUpdateUserV2Error { """UUID of the user that failed to update.""" @@ -7453,6 +7415,44 @@ input MyAppConfigFragmentItemInput { config: JSON! } +""" +Added in UNRELEASED. Self-service bulk create — scope is `USER` / `current_user`. +""" +input MyBulkCreateAppConfigFragmentInput { + """USER-scope rows to create.""" + items: [MyAppConfigFragmentItemInput!]! +} + +""" +Added in UNRELEASED. Payload for `myBulkCreateAppConfigFragments` (recomputed views). +""" +type MyBulkCreateAppConfigFragmentsPayload { + """Recomputed merged AppConfig views for each created USER fragment.""" + created: [AppConfig!]! + + """Per-item failures.""" + failed: [AppConfigFragmentBulkError!]! +} + +""" +Added in UNRELEASED. Self-service bulk update — scope is `USER` / `current_user`. +""" +input MyBulkUpdateAppConfigFragmentInput { + """USER-scope rows to update.""" + items: [MyAppConfigFragmentItemInput!]! +} + +""" +Added in UNRELEASED. Payload for `myBulkUpdateAppConfigFragments` (recomputed views). +""" +type MyBulkUpdateAppConfigFragmentsPayload { + """Recomputed merged AppConfig views for each updated USER fragment.""" + updated: [AppConfig!]! + + """Per-item failures.""" + failed: [AppConfigFragmentBulkError!]! +} + """ Added in 26.4.2. Query result returning the current client's IP address. """ From 34412b4a6bf572d2040f525a974a245b24cc83a2 Mon Sep 17 00:00:00 2001 From: Gyubong Date: Mon, 27 Apr 2026 11:00:43 +0900 Subject: [PATCH 7/8] chore(BA-5830): drop BEP-1052 ref from 11286 news fragment --- changes/11286.feature.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/changes/11286.feature.md b/changes/11286.feature.md index cfc79c37691..02abc6a842a 100644 --- a/changes/11286.feature.md +++ b/changes/11286.feature.md @@ -1 +1 @@ -Add `AppConfigFragment` REST v2 endpoints under `/v2/app-config-fragments` — `get` (body-carried natural key), scoped search `{scope_type}/{scope_id}/search`, admin cross-scope `search`, plus admin `create` / `update` / `purge` (BEP-1052 §4). +Add `AppConfigFragment` REST v2 endpoints under `/v2/app-config-fragments` — `get` (body-carried natural key), scoped search `{scope_type}/{scope_id}/search`, admin cross-scope `search`, plus admin `create` / `update` / `purge`. From 85d0da0a10fdfc6bf455fe8972cda64cdb9c9c30 Mon Sep 17 00:00:00 2001 From: Gyubong Date: Mon, 27 Apr 2026 11:44:16 +0900 Subject: [PATCH 8/8] chore(BA-5830): regenerate schema dump after BA-5815 propagation --- .../graphql-reference/supergraph.graphql | 51 +++++++++++++------ .../graphql-reference/v2-schema.graphql | 49 ++++++++++++------ 2 files changed, 70 insertions(+), 30 deletions(-) diff --git a/docs/manager/graphql-reference/supergraph.graphql b/docs/manager/graphql-reference/supergraph.graphql index 3a7a2640163..1841d9a0a3f 100644 --- a/docs/manager/graphql-reference/supergraph.graphql +++ b/docs/manager/graphql-reference/supergraph.graphql @@ -202,8 +202,10 @@ input AdminAppConfigFragmentItemInput config: JSON! } -"""Added in UNRELEASED. Per-item input for admin bulk create / update.""" -input AdminAppConfigPolicyItemInput +""" +Added in UNRELEASED. Per-item input for admin bulk create — `config_name` + initial `scope_sources`. +""" +input AdminAppConfigPolicyCreateItemInput @join__type(graph: STRAWBERRY) { """Unique, immutable policy name.""" @@ -213,6 +215,19 @@ input AdminAppConfigPolicyItemInput scopeSources: [String!]! } +""" +Added in UNRELEASED. Per-item input for admin bulk update — target row id + new `scope_sources`. +""" +input AdminAppConfigPolicyUpdateItemInput + @join__type(graph: STRAWBERRY) +{ + """Policy row id.""" + id: UUID! + + """Ordered scope chain.""" + scopeSources: [String!]! +} + """Added in UNRELEASED. Admin bulk create input — items carry any scope.""" input AdminBulkCreateAppConfigFragmentInput @join__type(graph: STRAWBERRY) @@ -248,7 +263,7 @@ input AdminBulkCreateAppConfigPolicyInput @join__type(graph: STRAWBERRY) { """Policies to create.""" - items: [AdminAppConfigPolicyItemInput!]! + items: [AdminAppConfigPolicyCreateItemInput!]! } """ @@ -276,21 +291,21 @@ type AdminBulkPurgeAppConfigFragmentsPayload type AdminBulkPurgeAppConfigPoliciesPayload @join__type(graph: STRAWBERRY) { - """`config_name`s of policies actually removed (absent names no-oped).""" - purgedConfigNames: [String!]! + """Ids of policies actually removed (absent ids no-oped).""" + purgedIds: [UUID!]! """Per-item failures.""" failed: [AppConfigPolicyBulkError!]! } """ -Added in UNRELEASED. Admin bulk purge input for app-config policies (keyed on `config_name`). +Added in UNRELEASED. Admin bulk purge input for app-config policies (keyed on row id). """ input AdminBulkPurgeAppConfigPolicyInput @join__type(graph: STRAWBERRY) { - """`config_name`s to purge.""" - configNames: [String!]! + """Policy row ids to purge.""" + ids: [UUID!]! } """Added in UNRELEASED. Admin bulk update input.""" @@ -328,7 +343,7 @@ input AdminBulkUpdateAppConfigPolicyInput @join__type(graph: STRAWBERRY) { """Policies to update.""" - items: [AdminAppConfigPolicyItemInput!]! + items: [AdminAppConfigPolicyUpdateItemInput!]! } """Added in 26.4.2. Admin input for creating a keypair for a user.""" @@ -1063,6 +1078,12 @@ input AppConfigFilter """Filter by target user id (admin cross-user search only).""" userId: UUIDFilter = null + + """Filter by the oldest contributing fragment's creation timestamp.""" + createdAt: DateTimeFilter = null + + """Filter by the latest contributing fragment's update timestamp.""" + updatedAt: DateTimeFilter = null } """Added in UNRELEASED. Raw per-scope app-config fragment.""" @@ -1161,6 +1182,7 @@ enum AppConfigFragmentOrderField SCOPE_ID @join__enumValue(graph: STRAWBERRY) NAME @join__enumValue(graph: STRAWBERRY) CREATED_AT @join__enumValue(graph: STRAWBERRY) + UPDATED_AT @join__enumValue(graph: STRAWBERRY) } """Added in UNRELEASED. Specifies ordering for merged AppConfig results.""" @@ -1180,6 +1202,8 @@ enum AppConfigOrderField { USER_ID @join__enumValue(graph: STRAWBERRY) NAME @join__enumValue(graph: STRAWBERRY) + CREATED_AT @join__enumValue(graph: STRAWBERRY) + UPDATED_AT @join__enumValue(graph: STRAWBERRY) } """Added in UNRELEASED. Scoped app-config policy.""" @@ -1210,9 +1234,6 @@ type AppConfigPolicyBulkError """Original position in the input list.""" index: Int! - """`config_name` of the failed row.""" - configName: String! - """Reason for the failure.""" message: String! } @@ -11266,7 +11287,7 @@ type Mutation adminBulkUpdateAppConfigPolicies(input: AdminBulkUpdateAppConfigPolicyInput!): AdminBulkUpdateAppConfigPoliciesPayload! @join__field(graph: STRAWBERRY) """ - Added in UNRELEASED. Rejects items whose `config_name` still has referencing fragment rows. Admin only. + Added in UNRELEASED. Hard-delete policies by row id; rows still referenced by fragments surface in `failed`. Admin only. """ adminBulkPurgeAppConfigPolicies(input: AdminBulkPurgeAppConfigPolicyInput!): AdminBulkPurgeAppConfigPoliciesPayload! @join__field(graph: STRAWBERRY) @@ -13884,9 +13905,9 @@ type Query adminImageAliases(filter: ImageV2AliasFilter = null, orderBy: [ImageV2AliasOrderByGQL!] = null, before: String = null, after: String = null, first: Int = null, last: Int = null, limit: Int = null, offset: Int = null): ImageV2AliasConnection @join__field(graph: STRAWBERRY) """ - Added in UNRELEASED. Get a single app-config policy by `config_name`. Available to any authenticated user. + Added in UNRELEASED. Get a single app-config policy by row id. Available to any authenticated user. """ - appConfigPolicy(configName: String!): AppConfigPolicy @join__field(graph: STRAWBERRY) + appConfigPolicy(id: UUID!): AppConfigPolicy @join__field(graph: STRAWBERRY) """ Added in UNRELEASED. List app-config policies with filtering and pagination. Available to any authenticated user. diff --git a/docs/manager/graphql-reference/v2-schema.graphql b/docs/manager/graphql-reference/v2-schema.graphql index b371196bf23..49147ce6b22 100644 --- a/docs/manager/graphql-reference/v2-schema.graphql +++ b/docs/manager/graphql-reference/v2-schema.graphql @@ -154,8 +154,10 @@ input AdminAppConfigFragmentItemInput { config: JSON! } -"""Added in UNRELEASED. Per-item input for admin bulk create / update.""" -input AdminAppConfigPolicyItemInput { +""" +Added in UNRELEASED. Per-item input for admin bulk create — `config_name` + initial `scope_sources`. +""" +input AdminAppConfigPolicyCreateItemInput { """Unique, immutable policy name.""" configName: String! @@ -163,6 +165,17 @@ input AdminAppConfigPolicyItemInput { scopeSources: [String!]! } +""" +Added in UNRELEASED. Per-item input for admin bulk update — target row id + new `scope_sources`. +""" +input AdminAppConfigPolicyUpdateItemInput { + """Policy row id.""" + id: UUID! + + """Ordered scope chain.""" + scopeSources: [String!]! +} + """Added in UNRELEASED. Admin bulk create input — items carry any scope.""" input AdminBulkCreateAppConfigFragmentInput { """Rows to create.""" @@ -190,7 +203,7 @@ type AdminBulkCreateAppConfigPoliciesPayload { """Added in UNRELEASED. Admin bulk create input for app-config policies.""" input AdminBulkCreateAppConfigPolicyInput { """Policies to create.""" - items: [AdminAppConfigPolicyItemInput!]! + items: [AdminAppConfigPolicyCreateItemInput!]! } """ @@ -212,19 +225,19 @@ type AdminBulkPurgeAppConfigFragmentsPayload { """Added in UNRELEASED. Payload for `adminBulkPurgeAppConfigPolicies`.""" type AdminBulkPurgeAppConfigPoliciesPayload { - """`config_name`s of policies actually removed (absent names no-oped).""" - purgedConfigNames: [String!]! + """Ids of policies actually removed (absent ids no-oped).""" + purgedIds: [UUID!]! """Per-item failures.""" failed: [AppConfigPolicyBulkError!]! } """ -Added in UNRELEASED. Admin bulk purge input for app-config policies (keyed on `config_name`). +Added in UNRELEASED. Admin bulk purge input for app-config policies (keyed on row id). """ input AdminBulkPurgeAppConfigPolicyInput { - """`config_name`s to purge.""" - configNames: [String!]! + """Policy row ids to purge.""" + ids: [UUID!]! } """Added in UNRELEASED. Admin bulk update input.""" @@ -254,7 +267,7 @@ type AdminBulkUpdateAppConfigPoliciesPayload { """Added in UNRELEASED. Admin bulk update input for app-config policies.""" input AdminBulkUpdateAppConfigPolicyInput { """Policies to update.""" - items: [AdminAppConfigPolicyItemInput!]! + items: [AdminAppConfigPolicyUpdateItemInput!]! } """Added in 26.4.2. Admin input for creating a keypair for a user.""" @@ -745,6 +758,12 @@ input AppConfigFilter { """Filter by target user id (admin cross-user search only).""" userId: UUIDFilter = null + + """Filter by the oldest contributing fragment's creation timestamp.""" + createdAt: DateTimeFilter = null + + """Filter by the latest contributing fragment's update timestamp.""" + updatedAt: DateTimeFilter = null } """Added in UNRELEASED. Raw per-scope app-config fragment.""" @@ -831,6 +850,7 @@ enum AppConfigFragmentOrderField { SCOPE_ID NAME CREATED_AT + UPDATED_AT } """Added in UNRELEASED. Specifies ordering for merged AppConfig results.""" @@ -846,6 +866,8 @@ input AppConfigOrderBy { enum AppConfigOrderField { USER_ID NAME + CREATED_AT + UPDATED_AT } """Added in UNRELEASED. Scoped app-config policy.""" @@ -871,9 +893,6 @@ type AppConfigPolicyBulkError { """Original position in the input list.""" index: Int! - """`config_name` of the failed row.""" - configName: String! - """Reason for the failure.""" message: String! } @@ -7158,7 +7177,7 @@ type Mutation { adminBulkUpdateAppConfigPolicies(input: AdminBulkUpdateAppConfigPolicyInput!): AdminBulkUpdateAppConfigPoliciesPayload! """ - Added in UNRELEASED. Rejects items whose `config_name` still has referencing fragment rows. Admin only. + Added in UNRELEASED. Hard-delete policies by row id; rows still referenced by fragments surface in `failed`. Admin only. """ adminBulkPurgeAppConfigPolicies(input: AdminBulkPurgeAppConfigPolicyInput!): AdminBulkPurgeAppConfigPoliciesPayload! @@ -8979,9 +8998,9 @@ type Query { adminImageAliases(filter: ImageV2AliasFilter = null, orderBy: [ImageV2AliasOrderByGQL!] = null, before: String = null, after: String = null, first: Int = null, last: Int = null, limit: Int = null, offset: Int = null): ImageV2AliasConnection """ - Added in UNRELEASED. Get a single app-config policy by `config_name`. Available to any authenticated user. + Added in UNRELEASED. Get a single app-config policy by row id. Available to any authenticated user. """ - appConfigPolicy(configName: String!): AppConfigPolicy + appConfigPolicy(id: UUID!): AppConfigPolicy """ Added in UNRELEASED. List app-config policies with filtering and pagination. Available to any authenticated user.