Skip to content

Commit 7459d81

Browse files
fregataaclaudelablup-octodog
authored
feat(BA-4485): Define GQL types for admin_entities query (#8974)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> Co-authored-by: octodog <mu001@lablup.com>
1 parent 3927e0c commit 7459d81

File tree

8 files changed

+238
-19
lines changed

8 files changed

+238
-19
lines changed

changes/8974.feature.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Define GQL types (`EntityRefGQL`, `EntityFilter`, `EntityOrderBy`, `EntityConnection`) and resolver stub for `admin_entities` query

docs/manager/graphql-reference/supergraph.graphql

Lines changed: 46 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4149,10 +4149,18 @@ type EntityConnection
41494149
type EntityEdge
41504150
@join__type(graph: STRAWBERRY)
41514151
{
4152-
node: EntityNode!
4152+
node: EntityRef!
41534153
cursor: String!
41544154
}
41554155

4156+
"""Added in 26.3.0. Filter for entity associations"""
4157+
input EntityFilter
4158+
@join__type(graph: STRAWBERRY)
4159+
{
4160+
entityType: EntityType = null
4161+
entityId: StringFilter = null
4162+
}
4163+
41564164
union EntityNode
41574165
@join__type(graph: STRAWBERRY)
41584166
@join__unionMember(graph: STRAWBERRY, member: "UserV2")
@@ -4172,6 +4180,41 @@ union EntityNode
41724180
@join__unionMember(graph: STRAWBERRY, member: "Role")
41734181
= UserV2 | ProjectV2 | DomainV2 | VirtualFolderNode | ImageV2 | ComputeSessionNode | Artifact | ArtifactRegistry | AppConfig | NotificationChannel | NotificationRule | ModelDeployment | ResourceGroup | ArtifactRevision | Role
41744182

4183+
"""Added in 26.3.0. Order by specification for entity associations"""
4184+
input EntityOrderBy
4185+
@join__type(graph: STRAWBERRY)
4186+
{
4187+
field: EntityOrderField!
4188+
direction: OrderDirection! = DESC
4189+
}
4190+
4191+
"""Added in 26.3.0. Entity ordering field"""
4192+
enum EntityOrderField
4193+
@join__type(graph: STRAWBERRY)
4194+
{
4195+
ENTITY_TYPE @join__enumValue(graph: STRAWBERRY)
4196+
REGISTERED_AT @join__enumValue(graph: STRAWBERRY)
4197+
}
4198+
4199+
"""
4200+
Added in 26.3.0. Entity reference from the association_scopes_entities table
4201+
"""
4202+
type EntityRef implements Node
4203+
@join__implements(graph: STRAWBERRY, interface: "Node")
4204+
@join__type(graph: STRAWBERRY)
4205+
{
4206+
"""The Globally Unique ID of this object"""
4207+
id: ID!
4208+
scopeType: EntityType!
4209+
scopeId: String!
4210+
entityType: EntityType!
4211+
entityId: String!
4212+
registeredAt: DateTime!
4213+
4214+
"""The resolved entity object."""
4215+
entity: EntityNode
4216+
}
4217+
41754218
"""
41764219
Added in 26.2.0. Common timestamp fields for entity lifecycle tracking. Reusable across different entity types.
41774220
"""
@@ -9247,9 +9290,9 @@ type Query
92479290
adminRoleAssignments(filter: RoleAssignmentFilter = null, before: String = null, after: String = null, first: Int = null, last: Int = null, limit: Int = null, offset: Int = null): RoleAssignmentConnection! @join__field(graph: STRAWBERRY)
92489291

92499292
"""
9250-
Added in 26.3.0. Search entities by type (admin only). Only entity_type selection and pagination are supported. Use each entity's dedicated query for detailed options.
9293+
Added in 26.3.0. Search entity associations (admin only). Optionally filter by entity_type and entity_id.
92519294
"""
9252-
adminEntities(entityType: EntityType!, before: String = null, after: String = null, first: Int = null, last: Int = null, limit: Int = null, offset: Int = null): EntityConnection! @join__field(graph: STRAWBERRY)
9295+
adminEntities(filter: EntityFilter = null, orderBy: [EntityOrderBy!] = null, before: String = null, after: String = null, first: Int = null, last: Int = null, limit: Int = null, offset: Int = null): EntityConnection! @join__field(graph: STRAWBERRY)
92539296

92549297
"""Added in 26.3.0. List available scope types."""
92559298
scopeTypes: [EntityType!]! @join__field(graph: STRAWBERRY)

docs/manager/graphql-reference/v2-schema.graphql

Lines changed: 37 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2266,12 +2266,46 @@ type EntityConnection {
22662266

22672267
"""Added in 26.3.0. Entity edge"""
22682268
type EntityEdge {
2269-
node: EntityNode!
2269+
node: EntityRef!
22702270
cursor: String!
22712271
}
22722272

2273+
"""Added in 26.3.0. Filter for entity associations"""
2274+
input EntityFilter {
2275+
entityType: EntityType = null
2276+
entityId: StringFilter = null
2277+
}
2278+
22732279
union EntityNode = UserV2 | ProjectV2 | DomainV2 | VirtualFolderNode | ImageV2 | ComputeSessionNode | Artifact | ArtifactRegistry | AppConfig | NotificationChannel | NotificationRule | ModelDeployment | ResourceGroup | ArtifactRevision | Role
22742280

2281+
"""Added in 26.3.0. Order by specification for entity associations"""
2282+
input EntityOrderBy {
2283+
field: EntityOrderField!
2284+
direction: OrderDirection! = DESC
2285+
}
2286+
2287+
"""Added in 26.3.0. Entity ordering field"""
2288+
enum EntityOrderField {
2289+
ENTITY_TYPE
2290+
REGISTERED_AT
2291+
}
2292+
2293+
"""
2294+
Added in 26.3.0. Entity reference from the association_scopes_entities table
2295+
"""
2296+
type EntityRef implements Node {
2297+
"""The Globally Unique ID of this object"""
2298+
id: ID!
2299+
scopeType: EntityType!
2300+
scopeId: String!
2301+
entityType: EntityType!
2302+
entityId: String!
2303+
registeredAt: DateTime!
2304+
2305+
"""The resolved entity object."""
2306+
entity: EntityNode
2307+
}
2308+
22752309
"""
22762310
Added in 26.2.0. Common timestamp fields for entity lifecycle tracking. Reusable across different entity types.
22772311
"""
@@ -4993,9 +5027,9 @@ type Query {
49935027
adminRoleAssignments(filter: RoleAssignmentFilter = null, before: String = null, after: String = null, first: Int = null, last: Int = null, limit: Int = null, offset: Int = null): RoleAssignmentConnection!
49945028

49955029
"""
4996-
Added in 26.3.0. Search entities by type (admin only). Only entity_type selection and pagination are supported. Use each entity's dedicated query for detailed options.
5030+
Added in 26.3.0. Search entity associations (admin only). Optionally filter by entity_type and entity_id.
49975031
"""
4998-
adminEntities(entityType: EntityType!, before: String = null, after: String = null, first: Int = null, last: Int = null, limit: Int = null, offset: Int = null): EntityConnection!
5032+
adminEntities(filter: EntityFilter = null, orderBy: [EntityOrderBy!] = null, before: String = null, after: String = null, first: Int = null, last: Int = null, limit: Int = null, offset: Int = null): EntityConnection!
49995033

50005034
"""Added in 26.3.0. List available scope types."""
50015035
scopeTypes: [EntityType!]!

src/ai/backend/manager/api/gql/rbac/__init__.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,9 @@
2222
CreatePermissionInput,
2323
CreateRoleInput,
2424
EntityConnection,
25+
EntityFilter,
26+
EntityOrderBy,
27+
EntityRefGQL,
2528
EntityTypeGQL,
2629
OperationTypeGQL,
2730
PermissionConnection,
@@ -51,13 +54,16 @@
5154
"RoleGQL",
5255
"PermissionGQL",
5356
"RoleAssignmentGQL",
57+
"EntityRefGQL",
5458
# Filters
5559
"RoleFilter",
5660
"PermissionFilter",
5761
"RoleAssignmentFilter",
62+
"EntityFilter",
5863
# OrderBy
5964
"RoleOrderBy",
6065
"PermissionOrderBy",
66+
"EntityOrderBy",
6167
# Inputs
6268
"CreateRoleInput",
6369
"UpdateRoleInput",

src/ai/backend/manager/api/gql/rbac/fetcher/entity.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,18 @@
44

55
from strawberry import Info
66

7-
from ai.backend.manager.api.gql.rbac.types import EntityConnection, EntityTypeGQL
7+
from ai.backend.manager.api.gql.rbac.types import (
8+
EntityConnection,
9+
EntityFilter,
10+
EntityOrderBy,
11+
)
812
from ai.backend.manager.api.gql.types import StrawberryGQLContext
913

1014

1115
async def fetch_entities(
1216
info: Info[StrawberryGQLContext],
13-
entity_type: EntityTypeGQL,
17+
filter: EntityFilter | None = None,
18+
order_by: list[EntityOrderBy] | None = None,
1419
before: str | None = None,
1520
after: str | None = None,
1621
first: int | None = None,

src/ai/backend/manager/api/gql/rbac/resolver/entity.py

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -5,28 +5,27 @@
55
import strawberry
66
from strawberry import Info
77

8-
from ai.backend.manager.api.gql.rbac.types import EntityConnection, EntityTypeGQL
8+
from ai.backend.manager.api.gql.rbac.types import (
9+
EntityConnection,
10+
EntityFilter,
11+
EntityOrderBy,
12+
)
913
from ai.backend.manager.api.gql.types import StrawberryGQLContext
1014

1115

1216
@strawberry.field(
13-
description="Added in 26.3.0. Search entities by type (admin only). Only entity_type selection and pagination are supported. Use each entity's dedicated query for detailed options."
17+
description="Added in 26.3.0. Search entity associations (admin only). Optionally filter by entity_type and entity_id."
1418
) # type: ignore[misc]
1519
async def admin_entities(
1620
info: Info[StrawberryGQLContext],
17-
entity_type: EntityTypeGQL,
21+
filter: EntityFilter | None = None,
22+
order_by: list[EntityOrderBy] | None = None,
1823
before: str | None = None,
1924
after: str | None = None,
2025
first: int | None = None,
2126
last: int | None = None,
2227
limit: int | None = None,
2328
offset: int | None = None,
2429
) -> EntityConnection:
25-
"""Search entities by type with pagination only.
26-
27-
Per-entity filtering and ordering are not provided because EntityNode
28-
is a union of 15+ types, making a common filter schema impractical.
29-
Use dedicated root queries (e.g., admin_users, admin_projects) for
30-
detailed options including filtering and ordering.
31-
"""
30+
"""Search entity associations with filtering, ordering, and pagination."""
3231
raise NotImplementedError

src/ai/backend/manager/api/gql/rbac/types/__init__.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,10 @@
33
from .entity import (
44
EntityConnection,
55
EntityEdge,
6+
EntityFilter,
7+
EntityOrderBy,
8+
EntityOrderField,
9+
EntityRefGQL,
610
)
711
from .entity_node import EntityNode
812
from .permission import (
@@ -50,17 +54,22 @@
5054
"RoleSourceGQL",
5155
"RoleStatusGQL",
5256
"RoleOrderField",
57+
# Entity enums
58+
"EntityOrderField",
5359
# Types
5460
"PermissionGQL",
5561
"RoleGQL",
5662
"RoleAssignmentGQL",
63+
"EntityRefGQL",
5764
# Filters
5865
"PermissionFilter",
5966
"RoleFilter",
6067
"RoleAssignmentFilter",
68+
"EntityFilter",
6169
# OrderBy
6270
"PermissionOrderBy",
6371
"RoleOrderBy",
72+
"EntityOrderBy",
6473
# Inputs
6574
"CreatePermissionInput",
6675
"DeletePermissionInput",

src/ai/backend/manager/api/gql/rbac/types/entity.py

Lines changed: 123 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,136 @@
22

33
from __future__ import annotations
44

5+
from collections.abc import Iterable
6+
from datetime import datetime
7+
from enum import StrEnum
8+
from typing import Self, override
9+
510
import strawberry
11+
from strawberry import ID, Info
12+
from strawberry.relay import Node, NodeID
613

14+
from ai.backend.manager.api.gql.base import OrderDirection, StringFilter
715
from ai.backend.manager.api.gql.rbac.types.entity_node import EntityNode
16+
from ai.backend.manager.api.gql.rbac.types.permission import EntityTypeGQL
17+
from ai.backend.manager.api.gql.types import GQLFilter, GQLOrderBy, StrawberryGQLContext
18+
from ai.backend.manager.data.permission.association_scopes_entities import (
19+
AssociationScopesEntitiesData,
20+
)
21+
from ai.backend.manager.repositories.base import QueryCondition, QueryOrder
22+
from ai.backend.manager.repositories.permission_controller.options import (
23+
EntityScopeConditions,
24+
EntityScopeOrders,
25+
)
26+
27+
# ==================== Enums ====================
28+
29+
30+
@strawberry.enum(description="Added in 26.3.0. Entity ordering field")
31+
class EntityOrderField(StrEnum):
32+
ENTITY_TYPE = "entity_type"
33+
REGISTERED_AT = "registered_at"
34+
35+
36+
# ==================== Node Types ====================
37+
38+
39+
@strawberry.type(
40+
name="EntityRef",
41+
description="Added in 26.3.0. Entity reference from the association_scopes_entities table",
42+
)
43+
class EntityRefGQL(Node):
44+
id: NodeID[str]
45+
scope_type: EntityTypeGQL
46+
scope_id: str
47+
entity_type: EntityTypeGQL
48+
entity_id: str
49+
registered_at: datetime
50+
51+
@strawberry.field( # type: ignore[misc]
52+
description="The resolved entity object."
53+
)
54+
async def entity(
55+
self,
56+
*,
57+
info: Info[StrawberryGQLContext],
58+
) -> EntityNode | None:
59+
raise NotImplementedError
60+
61+
@classmethod
62+
async def resolve_nodes( # type: ignore[override]
63+
cls,
64+
*,
65+
info: Info[StrawberryGQLContext],
66+
node_ids: Iterable[str],
67+
required: bool = False,
68+
) -> Iterable[Self | None]:
69+
raise NotImplementedError
70+
71+
@classmethod
72+
def from_dataclass(cls, data: AssociationScopesEntitiesData) -> Self:
73+
return cls(
74+
id=ID(str(data.id)),
75+
scope_type=EntityTypeGQL.from_scope_type(data.scope_id.scope_type),
76+
scope_id=data.scope_id.scope_id,
77+
entity_type=EntityTypeGQL.from_internal(data.object_id.entity_type),
78+
entity_id=data.object_id.entity_id,
79+
registered_at=data.registered_at,
80+
)
81+
82+
83+
# ==================== Filter Types ====================
84+
85+
86+
@strawberry.input(description="Added in 26.3.0. Filter for entity associations")
87+
class EntityFilter(GQLFilter):
88+
entity_type: EntityTypeGQL | None = None
89+
entity_id: StringFilter | None = None
90+
91+
@override
92+
def build_conditions(self) -> list[QueryCondition]:
93+
conditions: list[QueryCondition] = []
94+
95+
if self.entity_type is not None:
96+
conditions.append(EntityScopeConditions.by_entity_type(self.entity_type.to_internal()))
97+
98+
if self.entity_id is not None:
99+
condition = self.entity_id.build_query_condition(
100+
contains_factory=EntityScopeConditions.by_entity_id_contains,
101+
equals_factory=EntityScopeConditions.by_entity_id_equals,
102+
starts_with_factory=EntityScopeConditions.by_entity_id_starts_with,
103+
ends_with_factory=EntityScopeConditions.by_entity_id_ends_with,
104+
)
105+
if condition:
106+
conditions.append(condition)
107+
108+
return conditions
109+
110+
111+
# ==================== OrderBy Types ====================
112+
113+
114+
@strawberry.input(description="Added in 26.3.0. Order by specification for entity associations")
115+
class EntityOrderBy(GQLOrderBy):
116+
field: EntityOrderField
117+
direction: OrderDirection = OrderDirection.DESC
118+
119+
@override
120+
def to_query_order(self) -> QueryOrder:
121+
ascending = self.direction == OrderDirection.ASC
122+
match self.field:
123+
case EntityOrderField.ENTITY_TYPE:
124+
return EntityScopeOrders.entity_type(ascending)
125+
case EntityOrderField.REGISTERED_AT:
126+
return EntityScopeOrders.registered_at(ascending)
127+
128+
129+
# ==================== Connection Types ====================
8130

9131

10132
@strawberry.type(description="Added in 26.3.0. Entity edge")
11133
class EntityEdge:
12-
node: EntityNode
134+
node: EntityRefGQL
13135
cursor: str
14136

15137

0 commit comments

Comments
 (0)