Skip to content

Commit d86bd06

Browse files
jopemachineclaude
andcommitted
feat(BA-5815): add AppConfigPolicy GraphQL + REST v2 surface
GraphQL (`api/gql/app_config_policy/`): - Queries: `appConfigPolicy(configName)`, `appConfigPolicies(filter, orderBy, ...)`. - Bulk admin mutations (BEP-1052 §3): `adminBulkCreateAppConfigPolicies`, `adminBulkUpdateAppConfigPolicies`, `adminBulkPurgeAppConfigPolicies`. Single-item mutations are intentionally not exposed — Policy writes are bulk-only by design. - Types: `AppConfigPolicyGQL`, filter / order-by, `AdminAppConfigPolicyItemInputGQL`, bulk inputs / payloads, `AppConfigPolicyBulkErrorGQL`. REST v2 (`api/rest/v2/app_config_policy/`, mounted at `/v2/app-config-policies`): - `GET /{config_name}` — auth_required. - `POST /search` — auth_required. - `POST /bulk-create` / `bulk-update` / `bulk-purge` — superadmin_required. Adds `AppConfigPolicyConfigNamePathParam` to the shared path-params module and wires the handler into `rest/v2/tree.py`. Service / adapter foundation lands in BA-5814; this PR only wires the GQL + REST entry points on top. Resolves BA-5815. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent d21272d commit d86bd06

15 files changed

Lines changed: 608 additions & 0 deletions

File tree

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
"""AppConfigPolicy GraphQL API package."""
2+
3+
from .resolver import (
4+
admin_bulk_create_app_config_policies,
5+
admin_bulk_purge_app_config_policies,
6+
admin_bulk_update_app_config_policies,
7+
app_config_policies,
8+
app_config_policy,
9+
)
10+
from .types import (
11+
AppConfigPolicyFilterGQL,
12+
AppConfigPolicyGQL,
13+
AppConfigPolicyOrderByGQL,
14+
AppConfigPolicyOrderFieldGQL,
15+
)
16+
17+
__all__ = [
18+
# Queries
19+
"app_config_policy",
20+
"app_config_policies",
21+
# Bulk mutations (BEP-1052 §3 — bulk-only)
22+
"admin_bulk_create_app_config_policies",
23+
"admin_bulk_update_app_config_policies",
24+
"admin_bulk_purge_app_config_policies",
25+
# Types
26+
"AppConfigPolicyGQL",
27+
"AppConfigPolicyFilterGQL",
28+
"AppConfigPolicyOrderByGQL",
29+
"AppConfigPolicyOrderFieldGQL",
30+
]
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
from .mutation import (
2+
admin_bulk_create_app_config_policies,
3+
admin_bulk_purge_app_config_policies,
4+
admin_bulk_update_app_config_policies,
5+
)
6+
from .query import (
7+
app_config_policies,
8+
app_config_policy,
9+
)
10+
11+
__all__ = [
12+
"admin_bulk_create_app_config_policies",
13+
"admin_bulk_purge_app_config_policies",
14+
"admin_bulk_update_app_config_policies",
15+
"app_config_policies",
16+
"app_config_policy",
17+
]
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
"""AppConfigPolicy GQL mutation resolvers (bulk-only, BEP-1052 §3)."""
2+
3+
from __future__ import annotations
4+
5+
from strawberry import Info
6+
7+
from ai.backend.common.meta.meta import NEXT_RELEASE_VERSION
8+
from ai.backend.manager.api.gql.app_config_policy.types import (
9+
AdminBulkCreateAppConfigPoliciesPayloadGQL,
10+
AdminBulkCreateAppConfigPolicyInputGQL,
11+
AdminBulkPurgeAppConfigPoliciesPayloadGQL,
12+
AdminBulkPurgeAppConfigPolicyInputGQL,
13+
AdminBulkUpdateAppConfigPoliciesPayloadGQL,
14+
AdminBulkUpdateAppConfigPolicyInputGQL,
15+
)
16+
from ai.backend.manager.api.gql.decorators import (
17+
BackendAIGQLMeta,
18+
gql_mutation,
19+
)
20+
from ai.backend.manager.api.gql.types import StrawberryGQLContext
21+
from ai.backend.manager.api.gql.utils import check_admin_only
22+
23+
24+
@gql_mutation(
25+
BackendAIGQLMeta(
26+
added_version=NEXT_RELEASE_VERSION,
27+
description="Strict insert keyed on `configName` (admin only, per-item transaction).",
28+
)
29+
) # type: ignore[misc]
30+
async def admin_bulk_create_app_config_policies(
31+
info: Info[StrawberryGQLContext],
32+
input: AdminBulkCreateAppConfigPolicyInputGQL,
33+
) -> AdminBulkCreateAppConfigPoliciesPayloadGQL:
34+
check_admin_only()
35+
result = await info.context.adapters.app_config_policy.admin_bulk_create(input.to_pydantic())
36+
return AdminBulkCreateAppConfigPoliciesPayloadGQL.from_pydantic(result)
37+
38+
39+
@gql_mutation(
40+
BackendAIGQLMeta(
41+
added_version=NEXT_RELEASE_VERSION,
42+
description=(
43+
"Replace `scope_sources`; `config_name` is immutable (BEP-1052 §1). "
44+
"Admin only, per-item transaction."
45+
),
46+
)
47+
) # type: ignore[misc]
48+
async def admin_bulk_update_app_config_policies(
49+
info: Info[StrawberryGQLContext],
50+
input: AdminBulkUpdateAppConfigPolicyInputGQL,
51+
) -> AdminBulkUpdateAppConfigPoliciesPayloadGQL:
52+
check_admin_only()
53+
result = await info.context.adapters.app_config_policy.admin_bulk_update(input.to_pydantic())
54+
return AdminBulkUpdateAppConfigPoliciesPayloadGQL.from_pydantic(result)
55+
56+
57+
@gql_mutation(
58+
BackendAIGQLMeta(
59+
added_version=NEXT_RELEASE_VERSION,
60+
description=(
61+
"Rejects items whose `config_name` still has referencing fragment rows "
62+
"(BEP-1052 §1). Admin only."
63+
),
64+
)
65+
) # type: ignore[misc]
66+
async def admin_bulk_purge_app_config_policies(
67+
info: Info[StrawberryGQLContext],
68+
input: AdminBulkPurgeAppConfigPolicyInputGQL,
69+
) -> AdminBulkPurgeAppConfigPoliciesPayloadGQL:
70+
check_admin_only()
71+
result = await info.context.adapters.app_config_policy.admin_bulk_purge(input.to_pydantic())
72+
return AdminBulkPurgeAppConfigPoliciesPayloadGQL.from_pydantic(result)
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
"""AppConfigPolicy GQL query resolvers."""
2+
3+
from __future__ import annotations
4+
5+
from strawberry import Info
6+
7+
from ai.backend.common.dto.manager.v2.app_config_policy.request import (
8+
SearchAppConfigPoliciesInput,
9+
)
10+
from ai.backend.common.meta.meta import NEXT_RELEASE_VERSION
11+
from ai.backend.manager.api.gql.app_config_policy.types import (
12+
AppConfigPolicyFilterGQL,
13+
AppConfigPolicyGQL,
14+
AppConfigPolicyOrderByGQL,
15+
)
16+
from ai.backend.manager.api.gql.decorators import (
17+
BackendAIGQLMeta,
18+
gql_root_field,
19+
)
20+
from ai.backend.manager.api.gql.types import StrawberryGQLContext
21+
22+
23+
@gql_root_field(
24+
BackendAIGQLMeta(
25+
added_version=NEXT_RELEASE_VERSION,
26+
description=(
27+
"Get a single app-config policy by `config_name`. Available to any authenticated user."
28+
),
29+
)
30+
) # type: ignore[misc]
31+
async def app_config_policy(
32+
info: Info[StrawberryGQLContext],
33+
config_name: str,
34+
) -> AppConfigPolicyGQL | None:
35+
payload = await info.context.adapters.app_config_policy.get(config_name)
36+
if payload.item is None:
37+
return None
38+
return AppConfigPolicyGQL.from_pydantic(payload.item)
39+
40+
41+
@gql_root_field(
42+
BackendAIGQLMeta(
43+
added_version=NEXT_RELEASE_VERSION,
44+
description=(
45+
"List app-config policies with filtering and pagination. Available to any "
46+
"authenticated user."
47+
),
48+
)
49+
) # type: ignore[misc]
50+
async def app_config_policies(
51+
info: Info[StrawberryGQLContext],
52+
filter: AppConfigPolicyFilterGQL | None = None,
53+
order_by: list[AppConfigPolicyOrderByGQL] | None = None,
54+
first: int | None = None,
55+
after: str | None = None,
56+
last: int | None = None,
57+
before: str | None = None,
58+
limit: int | None = None,
59+
offset: int | None = None,
60+
) -> list[AppConfigPolicyGQL]:
61+
pydantic_filter = filter.to_pydantic() if filter else None
62+
pydantic_order = [o.to_pydantic() for o in order_by] if order_by else None
63+
64+
payload = await info.context.adapters.app_config_policy.search(
65+
SearchAppConfigPoliciesInput(
66+
filter=pydantic_filter,
67+
order=pydantic_order,
68+
first=first,
69+
after=after,
70+
last=last,
71+
before=before,
72+
limit=limit,
73+
offset=offset,
74+
)
75+
)
76+
return [AppConfigPolicyGQL.from_pydantic(node) for node in payload.items]
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
from .bulk_inputs import (
2+
AdminAppConfigPolicyItemInputGQL,
3+
AdminBulkCreateAppConfigPolicyInputGQL,
4+
AdminBulkPurgeAppConfigPolicyInputGQL,
5+
AdminBulkUpdateAppConfigPolicyInputGQL,
6+
)
7+
from .bulk_payloads import (
8+
AdminBulkCreateAppConfigPoliciesPayloadGQL,
9+
AdminBulkPurgeAppConfigPoliciesPayloadGQL,
10+
AdminBulkUpdateAppConfigPoliciesPayloadGQL,
11+
AppConfigPolicyBulkErrorGQL,
12+
)
13+
from .filters import (
14+
AppConfigPolicyFilterGQL,
15+
AppConfigPolicyOrderByGQL,
16+
AppConfigPolicyOrderFieldGQL,
17+
)
18+
from .node import AppConfigPolicyGQL
19+
20+
__all__ = [
21+
"AdminAppConfigPolicyItemInputGQL",
22+
"AdminBulkCreateAppConfigPoliciesPayloadGQL",
23+
"AdminBulkCreateAppConfigPolicyInputGQL",
24+
"AdminBulkPurgeAppConfigPoliciesPayloadGQL",
25+
"AdminBulkPurgeAppConfigPolicyInputGQL",
26+
"AdminBulkUpdateAppConfigPoliciesPayloadGQL",
27+
"AdminBulkUpdateAppConfigPolicyInputGQL",
28+
"AppConfigPolicyBulkErrorGQL",
29+
"AppConfigPolicyFilterGQL",
30+
"AppConfigPolicyGQL",
31+
"AppConfigPolicyOrderByGQL",
32+
"AppConfigPolicyOrderFieldGQL",
33+
]
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
"""AppConfigPolicy bulk-mutation GQL input types (BEP-1052 §3)."""
2+
3+
from __future__ import annotations
4+
5+
from ai.backend.common.dto.manager.v2.app_config_policy.request import (
6+
AdminAppConfigPolicyItemInput as AdminItemInputDTO,
7+
)
8+
from ai.backend.common.dto.manager.v2.app_config_policy.request import (
9+
AdminBulkCreateAppConfigPoliciesInput as AdminBulkCreateInputDTO,
10+
)
11+
from ai.backend.common.dto.manager.v2.app_config_policy.request import (
12+
AdminBulkPurgeAppConfigPoliciesInput as AdminBulkPurgeInputDTO,
13+
)
14+
from ai.backend.common.dto.manager.v2.app_config_policy.request import (
15+
AdminBulkUpdateAppConfigPoliciesInput as AdminBulkUpdateInputDTO,
16+
)
17+
from ai.backend.common.meta.meta import NEXT_RELEASE_VERSION
18+
from ai.backend.manager.api.gql.decorators import (
19+
BackendAIGQLMeta,
20+
gql_field,
21+
gql_pydantic_input,
22+
)
23+
from ai.backend.manager.api.gql.pydantic_compat import PydanticInputMixin
24+
25+
26+
@gql_pydantic_input(
27+
BackendAIGQLMeta(
28+
added_version=NEXT_RELEASE_VERSION,
29+
description="Per-item input for admin bulk create / update.",
30+
),
31+
name="AdminAppConfigPolicyItemInput",
32+
)
33+
class AdminAppConfigPolicyItemInputGQL(PydanticInputMixin[AdminItemInputDTO]):
34+
config_name: str = gql_field(description="Unique, immutable policy name.")
35+
scope_sources: list[str] = gql_field(description="Ordered scope chain.")
36+
37+
38+
@gql_pydantic_input(
39+
BackendAIGQLMeta(
40+
added_version=NEXT_RELEASE_VERSION,
41+
description="Admin bulk create input for app-config policies.",
42+
),
43+
name="AdminBulkCreateAppConfigPolicyInput",
44+
)
45+
class AdminBulkCreateAppConfigPolicyInputGQL(PydanticInputMixin[AdminBulkCreateInputDTO]):
46+
items: list[AdminAppConfigPolicyItemInputGQL] = gql_field(description="Policies to create.")
47+
48+
49+
@gql_pydantic_input(
50+
BackendAIGQLMeta(
51+
added_version=NEXT_RELEASE_VERSION,
52+
description="Admin bulk update input for app-config policies.",
53+
),
54+
name="AdminBulkUpdateAppConfigPolicyInput",
55+
)
56+
class AdminBulkUpdateAppConfigPolicyInputGQL(PydanticInputMixin[AdminBulkUpdateInputDTO]):
57+
items: list[AdminAppConfigPolicyItemInputGQL] = gql_field(description="Policies to update.")
58+
59+
60+
@gql_pydantic_input(
61+
BackendAIGQLMeta(
62+
added_version=NEXT_RELEASE_VERSION,
63+
description="Admin bulk purge input for app-config policies (keyed on `config_name`).",
64+
),
65+
name="AdminBulkPurgeAppConfigPolicyInput",
66+
)
67+
class AdminBulkPurgeAppConfigPolicyInputGQL(PydanticInputMixin[AdminBulkPurgeInputDTO]):
68+
config_names: list[str] = gql_field(description="`config_name`s to purge.")
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
"""AppConfigPolicy bulk-mutation GQL payload types (BEP-1052 §3)."""
2+
3+
from __future__ import annotations
4+
5+
from ai.backend.common.dto.manager.v2.app_config_policy.response import (
6+
AdminBulkCreateAppConfigPoliciesPayload as AdminBulkCreatePayloadDTO,
7+
)
8+
from ai.backend.common.dto.manager.v2.app_config_policy.response import (
9+
AdminBulkPurgeAppConfigPoliciesPayload as AdminBulkPurgePayloadDTO,
10+
)
11+
from ai.backend.common.dto.manager.v2.app_config_policy.response import (
12+
AdminBulkUpdateAppConfigPoliciesPayload as AdminBulkUpdatePayloadDTO,
13+
)
14+
from ai.backend.common.dto.manager.v2.app_config_policy.response import (
15+
AppConfigPolicyBulkError as AppConfigPolicyBulkErrorDTO,
16+
)
17+
from ai.backend.common.meta.meta import NEXT_RELEASE_VERSION
18+
from ai.backend.manager.api.gql.app_config_policy.types.node import AppConfigPolicyGQL
19+
from ai.backend.manager.api.gql.decorators import (
20+
BackendAIGQLMeta,
21+
gql_field,
22+
gql_pydantic_type,
23+
)
24+
from ai.backend.manager.api.gql.pydantic_compat import PydanticOutputMixin
25+
26+
27+
@gql_pydantic_type(
28+
BackendAIGQLMeta(
29+
added_version=NEXT_RELEASE_VERSION,
30+
description="Per-item failure info for bulk Policy mutations.",
31+
),
32+
model=AppConfigPolicyBulkErrorDTO,
33+
name="AppConfigPolicyBulkError",
34+
)
35+
class AppConfigPolicyBulkErrorGQL(PydanticOutputMixin[AppConfigPolicyBulkErrorDTO]):
36+
index: int = gql_field(description="Original position in the input list.")
37+
config_name: str = gql_field(description="`config_name` of the failed row.")
38+
message: str = gql_field(description="Reason for the failure.")
39+
40+
41+
@gql_pydantic_type(
42+
BackendAIGQLMeta(
43+
added_version=NEXT_RELEASE_VERSION,
44+
description="Payload for `adminBulkCreateAppConfigPolicies`.",
45+
),
46+
model=AdminBulkCreatePayloadDTO,
47+
name="AdminBulkCreateAppConfigPoliciesPayload",
48+
)
49+
class AdminBulkCreateAppConfigPoliciesPayloadGQL(PydanticOutputMixin[AdminBulkCreatePayloadDTO]):
50+
created: list[AppConfigPolicyGQL] = gql_field(description="Created policies.")
51+
failed: list[AppConfigPolicyBulkErrorGQL] = gql_field(description="Per-item failures.")
52+
53+
54+
@gql_pydantic_type(
55+
BackendAIGQLMeta(
56+
added_version=NEXT_RELEASE_VERSION,
57+
description="Payload for `adminBulkUpdateAppConfigPolicies`.",
58+
),
59+
model=AdminBulkUpdatePayloadDTO,
60+
name="AdminBulkUpdateAppConfigPoliciesPayload",
61+
)
62+
class AdminBulkUpdateAppConfigPoliciesPayloadGQL(PydanticOutputMixin[AdminBulkUpdatePayloadDTO]):
63+
updated: list[AppConfigPolicyGQL] = gql_field(description="Updated policies.")
64+
failed: list[AppConfigPolicyBulkErrorGQL] = gql_field(description="Per-item failures.")
65+
66+
67+
@gql_pydantic_type(
68+
BackendAIGQLMeta(
69+
added_version=NEXT_RELEASE_VERSION,
70+
description="Payload for `adminBulkPurgeAppConfigPolicies`.",
71+
),
72+
model=AdminBulkPurgePayloadDTO,
73+
name="AdminBulkPurgeAppConfigPoliciesPayload",
74+
)
75+
class AdminBulkPurgeAppConfigPoliciesPayloadGQL(PydanticOutputMixin[AdminBulkPurgePayloadDTO]):
76+
purged_config_names: list[str] = gql_field(
77+
description="`config_name`s of policies actually removed (absent names no-oped).",
78+
)
79+
failed: list[AppConfigPolicyBulkErrorGQL] = gql_field(description="Per-item failures.")

0 commit comments

Comments
 (0)