From 1102ae7f0ea095712c7c850a7db8ee0c7995072f Mon Sep 17 00:00:00 2001 From: solababs Date: Mon, 13 Oct 2025 21:47:57 +0100 Subject: [PATCH 01/16] WIP --- backend/infrahub/graphql/queries/__init__.py | 3 +- backend/infrahub/graphql/queries/branch.py | 41 +++++++++++++++++++- backend/infrahub/graphql/schema.py | 2 + backend/infrahub/graphql/types/__init__.py | 3 +- backend/infrahub/graphql/types/branch.py | 10 ++++- 5 files changed, 54 insertions(+), 5 deletions(-) diff --git a/backend/infrahub/graphql/queries/__init__.py b/backend/infrahub/graphql/queries/__init__.py index 1082887a40..e1da8ca9af 100644 --- a/backend/infrahub/graphql/queries/__init__.py +++ b/backend/infrahub/graphql/queries/__init__.py @@ -1,5 +1,5 @@ from .account import AccountPermissions, AccountToken -from .branch import BranchQueryList +from .branch import BranchQueryList, InfrahubBranchQueryList from .internal import InfrahubInfo from .ipam import ( DeprecatedIPAddressGetNextAvailable, @@ -20,6 +20,7 @@ "BranchQueryList", "DeprecatedIPAddressGetNextAvailable", "DeprecatedIPPrefixGetNextAvailable", + "InfrahubBranchQueryList", "InfrahubIPAddressGetNextAvailable", "InfrahubIPPrefixGetNextAvailable", "InfrahubInfo", diff --git a/backend/infrahub/graphql/queries/branch.py b/backend/infrahub/graphql/queries/branch.py index 4c1402fcbb..49dc3ee7c8 100644 --- a/backend/infrahub/graphql/queries/branch.py +++ b/backend/infrahub/graphql/queries/branch.py @@ -2,10 +2,10 @@ from typing import TYPE_CHECKING, Any -from graphene import ID, Field, List, NonNull, String +from graphene import ID, Field, Int, List, NonNull, String from infrahub.graphql.field_extractor import extract_graphql_fields -from infrahub.graphql.types import BranchType +from infrahub.graphql.types import BranchType, InfrahubBranchType if TYPE_CHECKING: from graphql import GraphQLResolveInfo @@ -28,3 +28,40 @@ async def branch_resolver( resolver=branch_resolver, required=True, ) + + +async def infrahub_branch_resolver( + root: dict, # noqa: ARG001 + info: GraphQLResolveInfo, + **kwargs: Any, +) -> InfrahubBranchType: + page = kwargs.pop("page", 1) + limit = kwargs.pop("limit", 100) + offset = (page - 1) * limit + + branches = await BranchType.get_list( + fields=BranchType.fields, graphql_context=info.context, limit=limit, offset=offset, **kwargs + ) + total_count = 100 # await BranchType.get_list_total_count(graphql_context=info.context, **kwargs) + + total_pages = (total_count + limit - 1) // limit + + return InfrahubBranchType( + total_count=total_count, + total_pages=total_pages, + current_page=page, + per_page=limit, + branches=[BranchType(**branch) for branch in branches], + ) + + +InfrahubBranchQueryList = Field( + InfrahubBranchType, + ids=List(ID), + name=String(), + page=Int(default_value=1), + limit=Int(default_value=100), + description="Retrieve paginated information about active branches.", + resolver=infrahub_branch_resolver, + required=True, +) diff --git a/backend/infrahub/graphql/schema.py b/backend/infrahub/graphql/schema.py index ec92dc9858..a0a6bac4d3 100644 --- a/backend/infrahub/graphql/schema.py +++ b/backend/infrahub/graphql/schema.py @@ -37,6 +37,7 @@ BranchQueryList, DeprecatedIPAddressGetNextAvailable, DeprecatedIPPrefixGetNextAvailable, + InfrahubBranchQueryList, InfrahubInfo, InfrahubIPAddressGetNextAvailable, InfrahubIPPrefixGetNextAvailable, @@ -63,6 +64,7 @@ class InfrahubBaseQuery(ObjectType): Relationship = Relationship + InfrahubBranch = InfrahubBranchQueryList InfrahubInfo = InfrahubInfo InfrahubStatus = InfrahubStatus diff --git a/backend/infrahub/graphql/types/__init__.py b/backend/infrahub/graphql/types/__init__.py index 45aabd62e3..78e32d1d99 100644 --- a/backend/infrahub/graphql/types/__init__.py +++ b/backend/infrahub/graphql/types/__init__.py @@ -21,7 +21,7 @@ StrAttributeType, TextAttributeType, ) -from .branch import BranchType +from .branch import BranchType, InfrahubBranchType from .interface import InfrahubInterface from .node import InfrahubObject from .permission import PaginatedObjectPermission @@ -41,6 +41,7 @@ "DropdownType", "IPHostType", "IPNetworkType", + "InfrahubBranchType", "InfrahubInterface", "InfrahubObject", "InfrahubObjectType", diff --git a/backend/infrahub/graphql/types/branch.py b/backend/infrahub/graphql/types/branch.py index 0413c452eb..300744d722 100644 --- a/backend/infrahub/graphql/types/branch.py +++ b/backend/infrahub/graphql/types/branch.py @@ -2,7 +2,7 @@ from typing import TYPE_CHECKING, Any -from graphene import Boolean, Field, String +from graphene import Boolean, Field, Int, List, NonNull, String from infrahub.core.branch import Branch from infrahub.core.constants import GLOBAL_BRANCH_NAME @@ -44,3 +44,11 @@ async def get_list( return [] return [await obj.to_graphql(fields=fields) for obj in objs if obj.name != GLOBAL_BRANCH_NAME] + + +class InfrahubBranchType(InfrahubObjectType): + total_count = Int() + total_pages = Int() + current_page = Int() + per_page = Int() + branches = List(of_type=NonNull(BranchType)) From c0b3b31363fda33daa2d88e6716e03998f0ad12c Mon Sep 17 00:00:00 2001 From: solababs Date: Thu, 16 Oct 2025 10:43:17 +0100 Subject: [PATCH 02/16] IFC-1886: Paginated branch graphql query --- backend/infrahub/graphql/queries/branch.py | 17 +--- backend/infrahub/graphql/types/branch.py | 4 +- .../tests/unit/graphql/queries/test_branch.py | 78 +++++++++++++++++++ 3 files changed, 81 insertions(+), 18 deletions(-) diff --git a/backend/infrahub/graphql/queries/branch.py b/backend/infrahub/graphql/queries/branch.py index b2f364cb63..e349eb37a7 100644 --- a/backend/infrahub/graphql/queries/branch.py +++ b/backend/infrahub/graphql/queries/branch.py @@ -38,28 +38,15 @@ async def infrahub_branch_resolver( page = kwargs.pop("page", 1) limit = kwargs.pop("limit", 100) offset = (page - 1) * limit - fields = {"id": None, "name": None} # extract_graphql_fields(info) + fields = {name: str(field.type) for name, field in BranchType._meta.fields.items()} branches = await BranchType.get_list( fields=fields, graphql_context=info.context, limit=limit, offset=offset, **kwargs ) - total_count = 100 # await BranchType.get_list_total_count(graphql_context=info.context, **kwargs) - - total_pages = (total_count + limit - 1) // limit - - # return InfrahubBranchType( - # total_count=total_count, - # total_pages=total_pages, - # current_page=page, - # per_page=limit, - # branches=[BranchType(**branch) for branch in branches], - # ) return { - "total_count": total_count, - "total_pages": total_pages, "current_page": page, - "per_page": limit, + "count_per_page": limit, "branches": branches, } diff --git a/backend/infrahub/graphql/types/branch.py b/backend/infrahub/graphql/types/branch.py index 300744d722..e060b7ff5f 100644 --- a/backend/infrahub/graphql/types/branch.py +++ b/backend/infrahub/graphql/types/branch.py @@ -47,8 +47,6 @@ async def get_list( class InfrahubBranchType(InfrahubObjectType): - total_count = Int() - total_pages = Int() current_page = Int() - per_page = Int() + count_per_page = Int() branches = List(of_type=NonNull(BranchType)) diff --git a/backend/tests/unit/graphql/queries/test_branch.py b/backend/tests/unit/graphql/queries/test_branch.py index 044c8b99a4..83138338a7 100644 --- a/backend/tests/unit/graphql/queries/test_branch.py +++ b/backend/tests/unit/graphql/queries/test_branch.py @@ -148,3 +148,81 @@ async def test_branch_query( assert id_response.data assert id_response.data["Branch"][0]["name"] == "branch3" assert len(id_response.data["Branch"]) == 1 + + async def test_paginated_branch_query( + self, + db: InfrahubDatabase, + default_branch: Branch, + register_core_models_schema, + session_admin, + client, + service, + ): + for i in range(10): + create_branch_query = """ + mutation { + BranchCreate(data: { name: "%s", description: "%s" }) { + ok + object { + id + name + } + } + } + """ % ( + f"sample-branch-{i}", + f"sample description {i}", + ) + + gql_params = await prepare_graphql_params( + db=db, + include_subscription=False, + branch=default_branch, + account_session=session_admin, + service=service, + ) + branch_result = await graphql( + schema=gql_params.schema, + source=create_branch_query, + context_value=gql_params.context, + root_value=None, + variable_values={}, + ) + assert branch_result.errors is None + assert branch_result.data + + query = """ + query { + InfrahubBranch(page: 2, limit: 5) { + branches { + id + name + description + origin_branch + branched_from + created_at + is_default + is_isolated + has_schema_changes + sync_with_git + } + current_page + count_per_page + } + } + """ + gql_params = await prepare_graphql_params( + db=db, include_subscription=False, branch=default_branch, service=service + ) + all_branches = await graphql( + schema=gql_params.schema, + source=query, + context_value=gql_params.context, + root_value=None, + variable_values={}, + ) + assert all_branches.errors is None + assert all_branches.data + assert len(all_branches.data["InfrahubBranch"]["branches"]) == 5 + assert all_branches.data["InfrahubBranch"]["current_page"] == 2 + assert all_branches.data["InfrahubBranch"]["count_per_page"] == 5 From 9a07e2346537c49f5caa0c4e5381b0df22058525 Mon Sep 17 00:00:00 2001 From: solababs Date: Mon, 20 Oct 2025 09:04:29 +0100 Subject: [PATCH 03/16] update repsonse format --- backend/infrahub/core/branch/models.py | 12 +++++ backend/infrahub/core/node/standard.py | 16 ++++++ backend/infrahub/core/query/__init__.py | 2 + backend/infrahub/core/query/standard_node.py | 2 + backend/infrahub/graphql/queries/branch.py | 23 ++++----- backend/infrahub/graphql/types/branch.py | 24 +++++++-- .../tests/unit/graphql/queries/test_branch.py | 50 ++++++++++++------- 7 files changed, 94 insertions(+), 35 deletions(-) diff --git a/backend/infrahub/core/branch/models.py b/backend/infrahub/core/branch/models.py index 9456e31e4b..272a625a6b 100644 --- a/backend/infrahub/core/branch/models.py +++ b/backend/infrahub/core/branch/models.py @@ -165,6 +165,18 @@ async def get_list( return branches + @classmethod + async def get_list_and_count( + cls, + db: InfrahubDatabase, + limit: int = 1000, + ids: list[str] | None = None, + name: str | None = None, + **kwargs: dict[str, Any], + ) -> tuple[list[Self], int]: + kwargs["raw_filter"] = f"n.status <> '{BranchStatus.DELETING.value}'" + return await super().get_list_and_count(db=db, limit=limit, ids=ids, name=name, **kwargs) + @classmethod def isinstance(cls, obj: Any) -> bool: return isinstance(obj, cls) diff --git a/backend/infrahub/core/node/standard.py b/backend/infrahub/core/node/standard.py index d1d05ca6a9..74f3790669 100644 --- a/backend/infrahub/core/node/standard.py +++ b/backend/infrahub/core/node/standard.py @@ -227,3 +227,19 @@ async def get_list( await query.execute(db=db) return [cls.from_db(result.get("n")) for result in query.get_results()] + + @classmethod + async def get_list_and_count( + cls, + db: InfrahubDatabase, + limit: int = 1000, + ids: list[str] | None = None, + name: str | None = None, + **kwargs: dict[str, Any], + ) -> tuple[list[Self], int]: + query: Query = await StandardNodeGetListQuery.init( + db=db, node_class=cls, ids=ids, node_name=name, limit=limit, **kwargs + ) + await query.execute(db=db) + + return [cls.from_db(result.get("n")) for result in query.get_results()], await query.count(db=db) diff --git a/backend/infrahub/core/query/__init__.py b/backend/infrahub/core/query/__init__.py index f98797a8f6..b295e6eae1 100644 --- a/backend/infrahub/core/query/__init__.py +++ b/backend/infrahub/core/query/__init__.py @@ -352,6 +352,7 @@ def __init__( offset: int | None = None, order_by: list[str] | None = None, branch_agnostic: bool = False, + **kwargs: dict[str, Any], ): if branch: self.branch = branch @@ -378,6 +379,7 @@ def __init__( self.has_errors: bool = False self.stats: QueryStats = QueryStats() + self.kwargs = kwargs def update_return_labels(self, value: str | list[str]) -> None: if isinstance(value, str) and value not in self.return_labels: diff --git a/backend/infrahub/core/query/standard_node.py b/backend/infrahub/core/query/standard_node.py index 1fc212edc9..a0a7d9ec79 100644 --- a/backend/infrahub/core/query/standard_node.py +++ b/backend/infrahub/core/query/standard_node.py @@ -150,6 +150,8 @@ async def query_init(self, db: InfrahubDatabase, **kwargs: Any) -> None: # noqa if self.node_name: filters.append("n.name = $name") self.params["name"] = self.node_name + if raw_filter := self.kwargs.get("raw_filter"): + filters.append(raw_filter) where = "" if filters: diff --git a/backend/infrahub/graphql/queries/branch.py b/backend/infrahub/graphql/queries/branch.py index e349eb37a7..c4ffd453b9 100644 --- a/backend/infrahub/graphql/queries/branch.py +++ b/backend/infrahub/graphql/queries/branch.py @@ -35,27 +35,24 @@ async def infrahub_branch_resolver( info: GraphQLResolveInfo, **kwargs: Any, ) -> dict[str, Any]: - page = kwargs.pop("page", 1) limit = kwargs.pop("limit", 100) - offset = (page - 1) * limit - fields = {name: str(field.type) for name, field in BranchType._meta.fields.items()} - - branches = await BranchType.get_list( - fields=fields, graphql_context=info.context, limit=limit, offset=offset, **kwargs + offset = kwargs.pop("offset", 0) + fields = extract_graphql_fields(info) + branches, count = await InfrahubBranchType.get_list_and_count( + fields=fields.get("edges", {}).get("node", {}), + graphql_context=info.context, + limit=limit, + offset=offset, + **kwargs, ) - - return { - "current_page": page, - "count_per_page": limit, - "branches": branches, - } + return {"count": count, "edges": {"node": branches}} InfrahubBranchQueryList = Field( InfrahubBranchType, ids=List(ID), name=String(), - page=Int(default_value=1), + offset=Int(default_value=0), limit=Int(default_value=100), description="Retrieve paginated information about active branches.", resolver=infrahub_branch_resolver, diff --git a/backend/infrahub/graphql/types/branch.py b/backend/infrahub/graphql/types/branch.py index e060b7ff5f..960066a2d6 100644 --- a/backend/infrahub/graphql/types/branch.py +++ b/backend/infrahub/graphql/types/branch.py @@ -46,7 +46,25 @@ async def get_list( return [await obj.to_graphql(fields=fields) for obj in objs if obj.name != GLOBAL_BRANCH_NAME] +class InfrahubBranchEdge(InfrahubObjectType): + node = Field(List(of_type=NonNull(BranchType), required=True), required=True) + + class InfrahubBranchType(InfrahubObjectType): - current_page = Int() - count_per_page = Int() - branches = List(of_type=NonNull(BranchType)) + count = Field(Int, required=True) + edges = Field(InfrahubBranchEdge, required=True) + + @classmethod + async def get_list_and_count( + cls, + fields: dict, + graphql_context: GraphqlContext, + **kwargs: Any, + ) -> tuple[list[dict[str, Any]], int]: + async with graphql_context.db.start_session(read_only=True) as db: + objs, count = await Branch.get_list_and_count(db=db, **kwargs) + + if not objs: + return [], 0 + + return [await obj.to_graphql(fields=fields) for obj in objs if obj.name != GLOBAL_BRANCH_NAME], count diff --git a/backend/tests/unit/graphql/queries/test_branch.py b/backend/tests/unit/graphql/queries/test_branch.py index 83138338a7..c41f573bdd 100644 --- a/backend/tests/unit/graphql/queries/test_branch.py +++ b/backend/tests/unit/graphql/queries/test_branch.py @@ -192,24 +192,17 @@ async def test_paginated_branch_query( assert branch_result.data query = """ - query { - InfrahubBranch(page: 2, limit: 5) { - branches { - id - name - description - origin_branch - branched_from - created_at - is_default - is_isolated - has_schema_changes - sync_with_git + query { + InfrahubBranch(offset: 2, limit: 5) { + count + edges { + node { + name + description + } + } } - current_page - count_per_page } - } """ gql_params = await prepare_graphql_params( db=db, include_subscription=False, branch=default_branch, service=service @@ -223,6 +216,25 @@ async def test_paginated_branch_query( ) assert all_branches.errors is None assert all_branches.data - assert len(all_branches.data["InfrahubBranch"]["branches"]) == 5 - assert all_branches.data["InfrahubBranch"]["current_page"] == 2 - assert all_branches.data["InfrahubBranch"]["count_per_page"] == 5 + assert all_branches.data["InfrahubBranch"]["count"] == 13 + + expected_branches = [ + { + "description": "Default Branch", + "name": "main", + }, + { + "description": "my description", + "name": "branch3", + }, + *[ + { + "description": f"sample description {i}", + "name": f"sample-branch-{i}", + } + for i in range(10) + ], + ] + assert all_branches.data["InfrahubBranch"]["edges"]["node"].sort( + key=operator.itemgetter("name") + ) == expected_branches.sort(key=operator.itemgetter("name")) From 41b18491f74e7583be34eb83bc1cec03b8e03695 Mon Sep 17 00:00:00 2001 From: solababs Date: Mon, 20 Oct 2025 09:20:58 +0100 Subject: [PATCH 04/16] fix mypy --- backend/infrahub/core/branch/models.py | 2 +- backend/infrahub/core/query/__init__.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/backend/infrahub/core/branch/models.py b/backend/infrahub/core/branch/models.py index 272a625a6b..475b21d571 100644 --- a/backend/infrahub/core/branch/models.py +++ b/backend/infrahub/core/branch/models.py @@ -172,7 +172,7 @@ async def get_list_and_count( limit: int = 1000, ids: list[str] | None = None, name: str | None = None, - **kwargs: dict[str, Any], + **kwargs: Any, ) -> tuple[list[Self], int]: kwargs["raw_filter"] = f"n.status <> '{BranchStatus.DELETING.value}'" return await super().get_list_and_count(db=db, limit=limit, ids=ids, name=name, **kwargs) diff --git a/backend/infrahub/core/query/__init__.py b/backend/infrahub/core/query/__init__.py index b295e6eae1..300a240e1d 100644 --- a/backend/infrahub/core/query/__init__.py +++ b/backend/infrahub/core/query/__init__.py @@ -352,7 +352,7 @@ def __init__( offset: int | None = None, order_by: list[str] | None = None, branch_agnostic: bool = False, - **kwargs: dict[str, Any], + **kwargs: Any, ): if branch: self.branch = branch From 9f1dc4ae2285663363cc9e092942855908d3a707 Mon Sep 17 00:00:00 2001 From: solababs Date: Tue, 21 Oct 2025 23:22:59 +0100 Subject: [PATCH 05/16] refactor branch list and count logic --- backend/infrahub/core/branch/models.py | 23 +++++++++++-------- backend/infrahub/core/node/standard.py | 16 ------------- backend/infrahub/core/query/__init__.py | 4 +--- backend/infrahub/core/query/branch.py | 6 +++++ backend/infrahub/core/query/standard_node.py | 5 ++-- backend/infrahub/graphql/queries/branch.py | 20 +++++++--------- backend/infrahub/graphql/types/branch.py | 16 +++---------- .../tests/unit/graphql/queries/test_branch.py | 18 ++++----------- schema/schema.graphql | 12 ++++++++++ 9 files changed, 51 insertions(+), 69 deletions(-) diff --git a/backend/infrahub/core/branch/models.py b/backend/infrahub/core/branch/models.py index 475b21d571..de1fae2c84 100644 --- a/backend/infrahub/core/branch/models.py +++ b/backend/infrahub/core/branch/models.py @@ -11,8 +11,9 @@ ) from infrahub.core.models import SchemaBranchHash # noqa: TC001 from infrahub.core.node.standard import StandardNode -from infrahub.core.query import QueryType +from infrahub.core.query import Query, QueryType from infrahub.core.query.branch import ( + BranchNodeGetListQuery, DeleteBranchRelationshipsQuery, GetAllBranchInternalRelationshipQuery, RebaseBranchDeleteRelationshipQuery, @@ -160,22 +161,26 @@ async def get_list( name: str | None = None, **kwargs: dict[str, Any], ) -> list[Self]: - branches = await super().get_list(db=db, limit=limit, ids=ids, name=name, **kwargs) - branches = [branch for branch in branches if branch.status != BranchStatus.DELETING] + query: Query = await BranchNodeGetListQuery.init( + db=db, node_class=cls, ids=ids, node_name=name, limit=limit, **kwargs + ) + await query.execute(db=db) - return branches + return [cls.from_db(result.get("n")) for result in query.get_results()] @classmethod - async def get_list_and_count( + async def get_list_count( cls, db: InfrahubDatabase, limit: int = 1000, ids: list[str] | None = None, name: str | None = None, - **kwargs: Any, - ) -> tuple[list[Self], int]: - kwargs["raw_filter"] = f"n.status <> '{BranchStatus.DELETING.value}'" - return await super().get_list_and_count(db=db, limit=limit, ids=ids, name=name, **kwargs) + **kwargs: dict[str, Any], + ) -> int: + query: Query = await BranchNodeGetListQuery.init( + db=db, node_class=cls, ids=ids, node_name=name, limit=limit, **kwargs + ) + return await query.count(db=db) @classmethod def isinstance(cls, obj: Any) -> bool: diff --git a/backend/infrahub/core/node/standard.py b/backend/infrahub/core/node/standard.py index 74f3790669..d1d05ca6a9 100644 --- a/backend/infrahub/core/node/standard.py +++ b/backend/infrahub/core/node/standard.py @@ -227,19 +227,3 @@ async def get_list( await query.execute(db=db) return [cls.from_db(result.get("n")) for result in query.get_results()] - - @classmethod - async def get_list_and_count( - cls, - db: InfrahubDatabase, - limit: int = 1000, - ids: list[str] | None = None, - name: str | None = None, - **kwargs: dict[str, Any], - ) -> tuple[list[Self], int]: - query: Query = await StandardNodeGetListQuery.init( - db=db, node_class=cls, ids=ids, node_name=name, limit=limit, **kwargs - ) - await query.execute(db=db) - - return [cls.from_db(result.get("n")) for result in query.get_results()], await query.count(db=db) diff --git a/backend/infrahub/core/query/__init__.py b/backend/infrahub/core/query/__init__.py index 300a240e1d..0f35eef52d 100644 --- a/backend/infrahub/core/query/__init__.py +++ b/backend/infrahub/core/query/__init__.py @@ -352,7 +352,6 @@ def __init__( offset: int | None = None, order_by: list[str] | None = None, branch_agnostic: bool = False, - **kwargs: Any, ): if branch: self.branch = branch @@ -379,7 +378,6 @@ def __init__( self.has_errors: bool = False self.stats: QueryStats = QueryStats() - self.kwargs = kwargs def update_return_labels(self, value: str | list[str]) -> None: if isinstance(value, str) and value not in self.return_labels: @@ -397,7 +395,7 @@ async def init( at: Timestamp | str | None = None, limit: int | None = None, offset: int | None = None, - **kwargs: Any, + **kwargs: dict[str, Any], ) -> Self: query = cls(branch=branch, at=at, limit=limit, offset=offset, **kwargs) diff --git a/backend/infrahub/core/query/branch.py b/backend/infrahub/core/query/branch.py index 37f08e617f..656d5c5f80 100644 --- a/backend/infrahub/core/query/branch.py +++ b/backend/infrahub/core/query/branch.py @@ -3,8 +3,10 @@ from typing import TYPE_CHECKING, Any from infrahub import config +from infrahub.core.branch.enums import BranchStatus from infrahub.core.constants import GLOBAL_BRANCH_NAME from infrahub.core.query import Query, QueryType +from infrahub.core.query.standard_node import StandardNodeGetListQuery if TYPE_CHECKING: from infrahub.database import InfrahubDatabase @@ -146,3 +148,7 @@ async def query_init(self, db: InfrahubDatabase, **kwargs: Any) -> None: # noqa self.add_to_query(query=query) self.params["ids"] = [db.to_database_id(id) for id in self.ids] + + +class BranchNodeGetListQuery(StandardNodeGetListQuery): + raw_filter = f"n.status <> '{BranchStatus.DELETING.value}'" diff --git a/backend/infrahub/core/query/standard_node.py b/backend/infrahub/core/query/standard_node.py index a0a7d9ec79..92ae1ec45e 100644 --- a/backend/infrahub/core/query/standard_node.py +++ b/backend/infrahub/core/query/standard_node.py @@ -132,6 +132,7 @@ async def query_init(self, db: InfrahubDatabase, **kwargs: Any) -> None: # noqa class StandardNodeGetListQuery(Query): name = "standard_node_list" type = QueryType.READ + raw_filter: str | None = None def __init__( self, node_class: StandardNode, ids: list[str] | None = None, node_name: str | None = None, **kwargs: Any @@ -150,8 +151,8 @@ async def query_init(self, db: InfrahubDatabase, **kwargs: Any) -> None: # noqa if self.node_name: filters.append("n.name = $name") self.params["name"] = self.node_name - if raw_filter := self.kwargs.get("raw_filter"): - filters.append(raw_filter) + if self.raw_filter: + filters.append(self.raw_filter) where = "" if filters: diff --git a/backend/infrahub/graphql/queries/branch.py b/backend/infrahub/graphql/queries/branch.py index c4ffd453b9..1b11a9c893 100644 --- a/backend/infrahub/graphql/queries/branch.py +++ b/backend/infrahub/graphql/queries/branch.py @@ -33,27 +33,23 @@ async def branch_resolver( async def infrahub_branch_resolver( root: dict, # noqa: ARG001 info: GraphQLResolveInfo, - **kwargs: Any, + limit: int | None = None, + offset: int | None = None, ) -> dict[str, Any]: - limit = kwargs.pop("limit", 100) - offset = kwargs.pop("offset", 0) fields = extract_graphql_fields(info) - branches, count = await InfrahubBranchType.get_list_and_count( - fields=fields.get("edges", {}).get("node", {}), - graphql_context=info.context, - limit=limit, - offset=offset, - **kwargs, + branches = await BranchType.get_list( + graphql_context=info.context, fields=fields.get("edges", {}).get("node", {}), limit=limit, offset=offset ) + count = await InfrahubBranchType.get_list_count(graphql_context=info.context, limit=limit, offset=offset) return {"count": count, "edges": {"node": branches}} InfrahubBranchQueryList = Field( InfrahubBranchType, - ids=List(ID), + ids=List(of_type=NonNull(ID)), name=String(), - offset=Int(default_value=0), - limit=Int(default_value=100), + offset=Int(), + limit=Int(), description="Retrieve paginated information about active branches.", resolver=infrahub_branch_resolver, required=True, diff --git a/backend/infrahub/graphql/types/branch.py b/backend/infrahub/graphql/types/branch.py index 960066a2d6..038f94239e 100644 --- a/backend/infrahub/graphql/types/branch.py +++ b/backend/infrahub/graphql/types/branch.py @@ -51,20 +51,10 @@ class InfrahubBranchEdge(InfrahubObjectType): class InfrahubBranchType(InfrahubObjectType): - count = Field(Int, required=True) + count = Field(Int, required=True, description="Total number of items") edges = Field(InfrahubBranchEdge, required=True) @classmethod - async def get_list_and_count( - cls, - fields: dict, - graphql_context: GraphqlContext, - **kwargs: Any, - ) -> tuple[list[dict[str, Any]], int]: + async def get_list_count(cls, graphql_context: GraphqlContext, **kwargs: Any) -> int: async with graphql_context.db.start_session(read_only=True) as db: - objs, count = await Branch.get_list_and_count(db=db, **kwargs) - - if not objs: - return [], 0 - - return [await obj.to_graphql(fields=fields) for obj in objs if obj.name != GLOBAL_BRANCH_NAME], count + return await Branch.get_list_count(db=db, **kwargs) diff --git a/backend/tests/unit/graphql/queries/test_branch.py b/backend/tests/unit/graphql/queries/test_branch.py index c41f573bdd..8205fcdb80 100644 --- a/backend/tests/unit/graphql/queries/test_branch.py +++ b/backend/tests/unit/graphql/queries/test_branch.py @@ -31,7 +31,6 @@ async def test_branch_query( gql_params = await prepare_graphql_params( db=db, - include_subscription=False, branch=default_branch, account_session=session_admin, service=service, @@ -61,9 +60,7 @@ async def test_branch_query( } } """ - gql_params = await prepare_graphql_params( - db=db, include_subscription=False, branch=default_branch, service=service - ) + gql_params = await prepare_graphql_params(db=db, branch=default_branch, service=service) all_branches = await graphql( schema=gql_params.schema, source=query, @@ -108,9 +105,7 @@ async def test_branch_query( } } """ % branch3["name"] - gql_params = await prepare_graphql_params( - db=db, include_subscription=False, branch=default_branch, service=service - ) + gql_params = await prepare_graphql_params(db=db, branch=default_branch, service=service) name_response = await graphql( schema=gql_params.schema, source=name_query, @@ -134,9 +129,7 @@ async def test_branch_query( """ % [branch3["id"]] id_query = id_query.replace("'", '"') - gql_params = await prepare_graphql_params( - db=db, include_subscription=False, branch=default_branch, service=service - ) + gql_params = await prepare_graphql_params(db=db, branch=default_branch, service=service) id_response = await graphql( schema=gql_params.schema, source=id_query, @@ -176,7 +169,6 @@ async def test_paginated_branch_query( gql_params = await prepare_graphql_params( db=db, - include_subscription=False, branch=default_branch, account_session=session_admin, service=service, @@ -204,9 +196,7 @@ async def test_paginated_branch_query( } } """ - gql_params = await prepare_graphql_params( - db=db, include_subscription=False, branch=default_branch, service=service - ) + gql_params = await prepare_graphql_params(db=db, branch=default_branch, service=service) all_branches = await graphql( schema=gql_params.schema, source=query, diff --git a/schema/schema.graphql b/schema/schema.graphql index 9d3638b89e..ba3418ce0e 100644 --- a/schema/schema.graphql +++ b/schema/schema.graphql @@ -6880,6 +6880,16 @@ input InfrahubAccountUpdateSelfInput { password: String } +type InfrahubBranchEdge { + node: [Branch!]! +} + +type InfrahubBranchType { + """Total number of items""" + count: Int! + edges: InfrahubBranchEdge! +} + input InfrahubComputedAttributeRecomputeInput { """Name of the computed attribute that must be recomputed""" attribute: String! @@ -10397,6 +10407,8 @@ type Query { """Retrieve fields mapping for converting object type""" FieldsMappingTypeConversion(source_kind: String, target_kind: String): FieldsMapping! InfrahubAccountToken(limit: Int, offset: Int): AccountTokenEdges! + """Retrieve paginated information about active branches.""" + InfrahubBranch(ids: [ID!], limit: Int, name: String, offset: Int): InfrahubBranchType! InfrahubEvent( """Filter the query to specific accounts""" account__ids: [String!] From 3a7ff90301daf2f9618853425b4b603325c28bec Mon Sep 17 00:00:00 2001 From: solababs Date: Tue, 21 Oct 2025 23:41:06 +0100 Subject: [PATCH 06/16] fix mypy --- backend/infrahub/core/branch/models.py | 9 +++++---- backend/infrahub/core/query/__init__.py | 2 +- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/backend/infrahub/core/branch/models.py b/backend/infrahub/core/branch/models.py index de1fae2c84..458ca7fd80 100644 --- a/backend/infrahub/core/branch/models.py +++ b/backend/infrahub/core/branch/models.py @@ -1,8 +1,9 @@ from __future__ import annotations import re -from typing import TYPE_CHECKING, Any, Optional, Self, Union +from typing import TYPE_CHECKING, Any, Optional, Self, Union, cast +from neo4j.graph import Node as Neo4jNode from pydantic import Field, field_validator from infrahub.core.branch.enums import BranchStatus @@ -159,14 +160,14 @@ async def get_list( limit: int = 1000, ids: list[str] | None = None, name: str | None = None, - **kwargs: dict[str, Any], + **kwargs: Any, ) -> list[Self]: query: Query = await BranchNodeGetListQuery.init( db=db, node_class=cls, ids=ids, node_name=name, limit=limit, **kwargs ) await query.execute(db=db) - return [cls.from_db(result.get("n")) for result in query.get_results()] + return [cls.from_db(node=cast(Neo4jNode, result.get("n"))) for result in query.get_results()] @classmethod async def get_list_count( @@ -175,7 +176,7 @@ async def get_list_count( limit: int = 1000, ids: list[str] | None = None, name: str | None = None, - **kwargs: dict[str, Any], + **kwargs: Any, ) -> int: query: Query = await BranchNodeGetListQuery.init( db=db, node_class=cls, ids=ids, node_name=name, limit=limit, **kwargs diff --git a/backend/infrahub/core/query/__init__.py b/backend/infrahub/core/query/__init__.py index 0f35eef52d..f98797a8f6 100644 --- a/backend/infrahub/core/query/__init__.py +++ b/backend/infrahub/core/query/__init__.py @@ -395,7 +395,7 @@ async def init( at: Timestamp | str | None = None, limit: int | None = None, offset: int | None = None, - **kwargs: dict[str, Any], + **kwargs: Any, ) -> Self: query = cls(branch=branch, at=at, limit=limit, offset=offset, **kwargs) From e659340acedd498b9f610dddfc38612045cfe164 Mon Sep 17 00:00:00 2001 From: solababs Date: Tue, 21 Oct 2025 23:44:16 +0100 Subject: [PATCH 07/16] remove unused limit and offset on get list count --- backend/infrahub/graphql/queries/branch.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/infrahub/graphql/queries/branch.py b/backend/infrahub/graphql/queries/branch.py index 1b11a9c893..b8dd220278 100644 --- a/backend/infrahub/graphql/queries/branch.py +++ b/backend/infrahub/graphql/queries/branch.py @@ -40,7 +40,7 @@ async def infrahub_branch_resolver( branches = await BranchType.get_list( graphql_context=info.context, fields=fields.get("edges", {}).get("node", {}), limit=limit, offset=offset ) - count = await InfrahubBranchType.get_list_count(graphql_context=info.context, limit=limit, offset=offset) + count = await InfrahubBranchType.get_list_count(graphql_context=info.context) return {"count": count, "edges": {"node": branches}} From 2ad4b2d31302243acd02ab4324bb321ca12c79ee Mon Sep 17 00:00:00 2001 From: solababs Date: Tue, 28 Oct 2025 10:51:53 +0100 Subject: [PATCH 08/16] conditionally resolve fields --- backend/infrahub/graphql/queries/branch.py | 16 +++++++++++----- backend/infrahub/graphql/types/branch.py | 14 ++++++++++---- .../tests/unit/graphql/queries/test_branch.py | 7 ++++--- 3 files changed, 25 insertions(+), 12 deletions(-) diff --git a/backend/infrahub/graphql/queries/branch.py b/backend/infrahub/graphql/queries/branch.py index b8dd220278..be90aa17b5 100644 --- a/backend/infrahub/graphql/queries/branch.py +++ b/backend/infrahub/graphql/queries/branch.py @@ -37,11 +37,17 @@ async def infrahub_branch_resolver( offset: int | None = None, ) -> dict[str, Any]: fields = extract_graphql_fields(info) - branches = await BranchType.get_list( - graphql_context=info.context, fields=fields.get("edges", {}).get("node", {}), limit=limit, offset=offset - ) - count = await InfrahubBranchType.get_list_count(graphql_context=info.context) - return {"count": count, "edges": {"node": branches}} + result = {} + if "edges" in fields: + result["edges"] = [ + {"node": branch} + for branch in await BranchType.get_list( + graphql_context=info.context, fields=fields.get("edges", {}).get("node", {}), limit=limit, offset=offset + ) + ] + if "count" in fields: + result["count"] = await InfrahubBranchType.get_list_count(graphql_context=info.context) + return result InfrahubBranchQueryList = Field( diff --git a/backend/infrahub/graphql/types/branch.py b/backend/infrahub/graphql/types/branch.py index 038f94239e..da193c915e 100644 --- a/backend/infrahub/graphql/types/branch.py +++ b/backend/infrahub/graphql/types/branch.py @@ -7,6 +7,7 @@ from infrahub.core.branch import Branch from infrahub.core.constants import GLOBAL_BRANCH_NAME +from ...exceptions import BranchNotFoundError from .standard_node import InfrahubObjectType if TYPE_CHECKING: @@ -47,14 +48,19 @@ async def get_list( class InfrahubBranchEdge(InfrahubObjectType): - node = Field(List(of_type=NonNull(BranchType), required=True), required=True) + node = Field(BranchType, required=True) class InfrahubBranchType(InfrahubObjectType): - count = Field(Int, required=True, description="Total number of items") - edges = Field(InfrahubBranchEdge, required=True) + count = Field(Int, description="Total number of items") + edges = Field(List(of_type=NonNull(InfrahubBranchEdge))) @classmethod async def get_list_count(cls, graphql_context: GraphqlContext, **kwargs: Any) -> int: async with graphql_context.db.start_session(read_only=True) as db: - return await Branch.get_list_count(db=db, **kwargs) + count = await Branch.get_list_count(db=db, **kwargs) + try: + await Branch.get_by_name(name=GLOBAL_BRANCH_NAME, db=db) + return count - 1 + except BranchNotFoundError: + return count diff --git a/backend/tests/unit/graphql/queries/test_branch.py b/backend/tests/unit/graphql/queries/test_branch.py index 8205fcdb80..b51fc5a8ac 100644 --- a/backend/tests/unit/graphql/queries/test_branch.py +++ b/backend/tests/unit/graphql/queries/test_branch.py @@ -206,7 +206,7 @@ async def test_paginated_branch_query( ) assert all_branches.errors is None assert all_branches.data - assert all_branches.data["InfrahubBranch"]["count"] == 13 + assert all_branches.data["InfrahubBranch"]["count"] == 12 # 10 created here + 1 created above + main branch expected_branches = [ { @@ -225,6 +225,7 @@ async def test_paginated_branch_query( for i in range(10) ], ] - assert all_branches.data["InfrahubBranch"]["edges"]["node"].sort( + all_branches_data_only = [branch.get("node") for branch in all_branches.data["InfrahubBranch"]["edges"]] + assert all_branches_data_only.sort(key=operator.itemgetter("name")) == expected_branches.sort( key=operator.itemgetter("name") - ) == expected_branches.sort(key=operator.itemgetter("name")) + ) From 32c72ad7e775eb9658c8d73c29a463bd36f77d73 Mon Sep 17 00:00:00 2001 From: solababs Date: Wed, 29 Oct 2025 05:59:14 +0100 Subject: [PATCH 09/16] fix mypy, update schema --- backend/infrahub/graphql/queries/branch.py | 2 +- backend/infrahub/graphql/types/branch.py | 2 +- schema/schema.graphql | 6 +++--- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/backend/infrahub/graphql/queries/branch.py b/backend/infrahub/graphql/queries/branch.py index be90aa17b5..429506c441 100644 --- a/backend/infrahub/graphql/queries/branch.py +++ b/backend/infrahub/graphql/queries/branch.py @@ -37,7 +37,7 @@ async def infrahub_branch_resolver( offset: int | None = None, ) -> dict[str, Any]: fields = extract_graphql_fields(info) - result = {} + result: dict[str, Any] = {} if "edges" in fields: result["edges"] = [ {"node": branch} diff --git a/backend/infrahub/graphql/types/branch.py b/backend/infrahub/graphql/types/branch.py index da193c915e..c42fd7211f 100644 --- a/backend/infrahub/graphql/types/branch.py +++ b/backend/infrahub/graphql/types/branch.py @@ -53,7 +53,7 @@ class InfrahubBranchEdge(InfrahubObjectType): class InfrahubBranchType(InfrahubObjectType): count = Field(Int, description="Total number of items") - edges = Field(List(of_type=NonNull(InfrahubBranchEdge))) + edges = Field(NonNull(List(of_type=NonNull(InfrahubBranchEdge)))) @classmethod async def get_list_count(cls, graphql_context: GraphqlContext, **kwargs: Any) -> int: diff --git a/schema/schema.graphql b/schema/schema.graphql index ba3418ce0e..04af48ab3d 100644 --- a/schema/schema.graphql +++ b/schema/schema.graphql @@ -6881,13 +6881,13 @@ input InfrahubAccountUpdateSelfInput { } type InfrahubBranchEdge { - node: [Branch!]! + node: Branch! } type InfrahubBranchType { """Total number of items""" - count: Int! - edges: InfrahubBranchEdge! + count: Int + edges: [InfrahubBranchEdge!]! } input InfrahubComputedAttributeRecomputeInput { From 84fac0a3e66d40fbf80c9f75a65a0f30ca58712f Mon Sep 17 00:00:00 2001 From: solababs Date: Wed, 29 Oct 2025 16:54:37 +0100 Subject: [PATCH 10/16] change response format --- backend/infrahub/graphql/queries/branch.py | 12 ++-- backend/infrahub/graphql/types/__init__.py | 3 +- backend/infrahub/graphql/types/branch.py | 57 ++++++++++++++++++- .../tests/unit/graphql/queries/test_branch.py | 24 ++++---- 4 files changed, 76 insertions(+), 20 deletions(-) diff --git a/backend/infrahub/graphql/queries/branch.py b/backend/infrahub/graphql/queries/branch.py index 429506c441..7e0e53e6b1 100644 --- a/backend/infrahub/graphql/queries/branch.py +++ b/backend/infrahub/graphql/queries/branch.py @@ -5,7 +5,7 @@ from graphene import ID, Field, Int, List, NonNull, String from infrahub.graphql.field_extractor import extract_graphql_fields -from infrahub.graphql.types import BranchType, InfrahubBranchType +from infrahub.graphql.types import BranchType, InfrahubBranch, InfrahubBranchType if TYPE_CHECKING: from graphql import GraphQLResolveInfo @@ -39,12 +39,10 @@ async def infrahub_branch_resolver( fields = extract_graphql_fields(info) result: dict[str, Any] = {} if "edges" in fields: - result["edges"] = [ - {"node": branch} - for branch in await BranchType.get_list( - graphql_context=info.context, fields=fields.get("edges", {}).get("node", {}), limit=limit, offset=offset - ) - ] + branches = await InfrahubBranch.get_list( + graphql_context=info.context, fields=fields.get("edges", {}).get("node", {}), limit=limit, offset=offset + ) + result["edges"] = [{"node": branch} for branch in branches] if "count" in fields: result["count"] = await InfrahubBranchType.get_list_count(graphql_context=info.context) return result diff --git a/backend/infrahub/graphql/types/__init__.py b/backend/infrahub/graphql/types/__init__.py index 78e32d1d99..63280557f6 100644 --- a/backend/infrahub/graphql/types/__init__.py +++ b/backend/infrahub/graphql/types/__init__.py @@ -21,7 +21,7 @@ StrAttributeType, TextAttributeType, ) -from .branch import BranchType, InfrahubBranchType +from .branch import BranchType, InfrahubBranch, InfrahubBranchType from .interface import InfrahubInterface from .node import InfrahubObject from .permission import PaginatedObjectPermission @@ -41,6 +41,7 @@ "DropdownType", "IPHostType", "IPNetworkType", + "InfrahubBranch", "InfrahubBranchType", "InfrahubInterface", "InfrahubObject", diff --git a/backend/infrahub/graphql/types/branch.py b/backend/infrahub/graphql/types/branch.py index c42fd7211f..668054c19c 100644 --- a/backend/infrahub/graphql/types/branch.py +++ b/backend/infrahub/graphql/types/branch.py @@ -31,6 +31,10 @@ class Meta: name = "Branch" model = Branch + @staticmethod + async def _map_fields_to_graphql(objs: list[Branch], fields: dict) -> list[dict[str, Any]]: + return [await obj.to_graphql(fields=fields) for obj in objs if obj.name != GLOBAL_BRANCH_NAME] + @classmethod async def get_list( cls, @@ -44,11 +48,60 @@ async def get_list( if not objs: return [] - return [await obj.to_graphql(fields=fields) for obj in objs if obj.name != GLOBAL_BRANCH_NAME] + return await cls._map_fields_to_graphql(objs=objs, fields=fields) + + +class RequiredStringValueField(InfrahubObjectType): + value = String(required=True) + + +class NonRequiredStringValueField(InfrahubObjectType): + value = String(required=False) + + +class NonRequiredBooleanValueField(InfrahubObjectType): + value = Boolean(required=False) + + +class InfrahubBranch(BranchType): + id = String(required=True) + created_at = String(required=False) + + name = Field(RequiredStringValueField, required=True) + description = Field(NonRequiredStringValueField, required=False) + origin_branch = Field(NonRequiredStringValueField, required=False) + branched_from = Field(NonRequiredStringValueField, required=False) + sync_with_git = Field(NonRequiredBooleanValueField, required=False) + is_default = Field(NonRequiredBooleanValueField, required=False) + is_isolated = Field( + NonRequiredBooleanValueField, required=False, deprecation_reason="non isolated mode is not supported anymore" + ) + has_schema_changes = Field(NonRequiredBooleanValueField, required=False) + + class Meta: + description = "InfrahubBranch" + name = "InfrahubBranch" + + @staticmethod + async def _map_fields_to_graphql(objs: list[Branch], fields: dict) -> list[dict[str, Any]]: + field_keys = fields.keys() + result: list[dict[str, Any]] = [] + for obj in objs: + if obj.name == GLOBAL_BRANCH_NAME: + continue + data = {} + for field in field_keys: + value = getattr(obj, field, None) + if isinstance(fields.get(field), dict): + data[field] = {"value": value} + else: + data[field] = value + result.append(data) + return result class InfrahubBranchEdge(InfrahubObjectType): - node = Field(BranchType, required=True) + node = Field(InfrahubBranch, required=True) class InfrahubBranchType(InfrahubObjectType): diff --git a/backend/tests/unit/graphql/queries/test_branch.py b/backend/tests/unit/graphql/queries/test_branch.py index b51fc5a8ac..7cdf26bd4b 100644 --- a/backend/tests/unit/graphql/queries/test_branch.py +++ b/backend/tests/unit/graphql/queries/test_branch.py @@ -189,8 +189,12 @@ async def test_paginated_branch_query( count edges { node { - name - description + name { + value + } + description { + value + } } } } @@ -210,22 +214,22 @@ async def test_paginated_branch_query( expected_branches = [ { - "description": "Default Branch", - "name": "main", + "description": {"value": "Default Branch"}, + "name": {"value": "main"}, }, { - "description": "my description", - "name": "branch3", + "description": {"value": "my description"}, + "name": {"value": "branch3"}, }, *[ { - "description": f"sample description {i}", - "name": f"sample-branch-{i}", + "description": {"value": f"sample description {i}"}, + "name": {"value": f"sample-branch-{i}"}, } for i in range(10) ], ] all_branches_data_only = [branch.get("node") for branch in all_branches.data["InfrahubBranch"]["edges"]] - assert all_branches_data_only.sort(key=operator.itemgetter("name")) == expected_branches.sort( - key=operator.itemgetter("name") + assert all_branches_data_only.sort(key=lambda x: x["name"]["value"]) == expected_branches.sort( + key=lambda x: x["name"]["value"] ) From 491ad04c8830614310a19c263907a43144a31fdb Mon Sep 17 00:00:00 2001 From: solababs Date: Wed, 29 Oct 2025 17:01:44 +0100 Subject: [PATCH 11/16] update status, add schema --- backend/infrahub/graphql/types/branch.py | 5 ++++ schema/schema.graphql | 33 +++++++++++++++++++++++- 2 files changed, 37 insertions(+), 1 deletion(-) diff --git a/backend/infrahub/graphql/types/branch.py b/backend/infrahub/graphql/types/branch.py index 13d3318996..6e575745a0 100644 --- a/backend/infrahub/graphql/types/branch.py +++ b/backend/infrahub/graphql/types/branch.py @@ -65,6 +65,10 @@ class NonRequiredBooleanValueField(InfrahubObjectType): value = Boolean(required=False) +class StatusField(InfrahubObjectType): + value = InfrahubBranchStatus(required=True) + + class InfrahubBranch(BranchType): id = String(required=True) created_at = String(required=False) @@ -73,6 +77,7 @@ class InfrahubBranch(BranchType): description = Field(NonRequiredStringValueField, required=False) origin_branch = Field(NonRequiredStringValueField, required=False) branched_from = Field(NonRequiredStringValueField, required=False) + status = Field(StatusField, required=True) sync_with_git = Field(NonRequiredBooleanValueField, required=False) is_default = Field(NonRequiredBooleanValueField, required=False) is_isolated = Field( diff --git a/schema/schema.graphql b/schema/schema.graphql index c1c227cb67..4888ceb6ef 100644 --- a/schema/schema.graphql +++ b/schema/schema.graphql @@ -7240,8 +7240,23 @@ input InfrahubAccountUpdateSelfInput { password: String } +"""InfrahubBranch""" +type InfrahubBranch { + branched_from: NonRequiredStringValueField + created_at: String + description: NonRequiredStringValueField + has_schema_changes: NonRequiredBooleanValueField + id: String! + is_default: NonRequiredBooleanValueField + is_isolated: NonRequiredBooleanValueField @deprecated(reason: "non isolated mode is not supported anymore") + name: RequiredStringValueField! + origin_branch: NonRequiredStringValueField + status: StatusField! + sync_with_git: NonRequiredBooleanValueField +} + type InfrahubBranchEdge { - node: Branch! + node: InfrahubBranch! } type InfrahubBranchType { @@ -9388,6 +9403,14 @@ type NodeMutatedEvent implements EventNodeInterface { relationships: [InfrahubMutatedRelationship!]! } +type NonRequiredBooleanValueField { + value: Boolean +} + +type NonRequiredStringValueField { + value: String +} + """Attribute of type Number""" type NumberAttribute implements AttributeInterface { id: String @@ -10912,6 +10935,10 @@ type Relationships { edges: [RelationshipNode!]! } +type RequiredStringValueField { + value: String! +} + type ResolveDiffConflict { ok: Boolean } @@ -11005,6 +11032,10 @@ type Status { workers: StatusWorkerEdges! } +type StatusField { + value: BranchStatus! +} + type StatusSummary { """Indicates if the schema hash is in sync on all active workers""" schema_hash_synced: Boolean! From 86dbefac4dc29e2019832bde10272c573c57f562 Mon Sep 17 00:00:00 2001 From: solababs Date: Wed, 29 Oct 2025 17:10:39 +0100 Subject: [PATCH 12/16] update schema --- .pre-commit-config.yaml | 1 + schema/schema.graphql | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 537c7969e6..53e6b32752 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -10,6 +10,7 @@ repos: - id: check-toml - id: check-yaml - id: end-of-file-fixer + exclude: ^schema/schema\.graphql$ - repo: https://github.com/astral-sh/ruff-pre-commit # Ruff version. diff --git a/schema/schema.graphql b/schema/schema.graphql index 4888ceb6ef..1b870e5b2c 100644 --- a/schema/schema.graphql +++ b/schema/schema.graphql @@ -11170,4 +11170,4 @@ type ValueType { directive @expand( """Exclude specific fields""" exclude: [String] -) on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT +) on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT \ No newline at end of file From 9eba0b835f1199173a43c1aa654c7b99956bc207 Mon Sep 17 00:00:00 2001 From: solababs Date: Wed, 29 Oct 2025 17:14:45 +0100 Subject: [PATCH 13/16] fix mypy --- backend/infrahub/graphql/types/branch.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/infrahub/graphql/types/branch.py b/backend/infrahub/graphql/types/branch.py index 6e575745a0..335f91dc1f 100644 --- a/backend/infrahub/graphql/types/branch.py +++ b/backend/infrahub/graphql/types/branch.py @@ -96,7 +96,7 @@ async def _map_fields_to_graphql(objs: list[Branch], fields: dict) -> list[dict[ for obj in objs: if obj.name == GLOBAL_BRANCH_NAME: continue - data = {} + data: dict[str, Any] = {} for field in field_keys: value = getattr(obj, field, None) if isinstance(fields.get(field), dict): From 8e4dca9e39a4d01d04dcd8dd67339ca71362c7eb Mon Sep 17 00:00:00 2001 From: solababs Date: Thu, 30 Oct 2025 17:15:33 +0100 Subject: [PATCH 14/16] use uuid for id --- backend/infrahub/graphql/types/branch.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/backend/infrahub/graphql/types/branch.py b/backend/infrahub/graphql/types/branch.py index 335f91dc1f..2ed74bb064 100644 --- a/backend/infrahub/graphql/types/branch.py +++ b/backend/infrahub/graphql/types/branch.py @@ -98,6 +98,9 @@ async def _map_fields_to_graphql(objs: list[Branch], fields: dict) -> list[dict[ continue data: dict[str, Any] = {} for field in field_keys: + if field == "id": + data["id"] = obj.uuid + continue value = getattr(obj, field, None) if isinstance(fields.get(field), dict): data[field] = {"value": value} From 089b015eb104672e511db3ce95c1b4f727355612 Mon Sep 17 00:00:00 2001 From: solababs Date: Fri, 31 Oct 2025 07:59:41 +0100 Subject: [PATCH 15/16] remove name and ids filter --- backend/infrahub/graphql/queries/branch.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/backend/infrahub/graphql/queries/branch.py b/backend/infrahub/graphql/queries/branch.py index 7e0e53e6b1..0b63f7ba27 100644 --- a/backend/infrahub/graphql/queries/branch.py +++ b/backend/infrahub/graphql/queries/branch.py @@ -50,8 +50,6 @@ async def infrahub_branch_resolver( InfrahubBranchQueryList = Field( InfrahubBranchType, - ids=List(of_type=NonNull(ID)), - name=String(), offset=Int(), limit=Int(), description="Retrieve paginated information about active branches.", From 1157cc46ba42a4b045880c682fa41282d3179b05 Mon Sep 17 00:00:00 2001 From: solababs Date: Fri, 31 Oct 2025 08:46:24 +0100 Subject: [PATCH 16/16] update graphql schema --- schema/schema.graphql | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/schema/schema.graphql b/schema/schema.graphql index 1b870e5b2c..5f5915d9d8 100644 --- a/schema/schema.graphql +++ b/schema/schema.graphql @@ -10787,7 +10787,7 @@ type Query { IPPrefixGetNextAvailable(prefix_id: String!, prefix_length: Int): IPPrefixGetNextAvailable! @deprecated(reason: "This query has been renamed to 'InfrahubIPPrefixGetNextAvailable'. It will be removed in the next version of Infrahub.") InfrahubAccountToken(limit: Int, offset: Int): AccountTokenEdges! """Retrieve paginated information about active branches.""" - InfrahubBranch(ids: [ID!], limit: Int, name: String, offset: Int): InfrahubBranchType! + InfrahubBranch(limit: Int, offset: Int): InfrahubBranchType! InfrahubEvent( """Filter the query to specific accounts""" account__ids: [String!]