Skip to content

Add GraphQL metadata sort options#7904

Merged
ogenstad merged 1 commit into
developfrom
pog-graphql-sort-IFC-1941
Dec 18, 2025
Merged

Add GraphQL metadata sort options#7904
ogenstad merged 1 commit into
developfrom
pog-graphql-sort-IFC-1941

Conversation

@ogenstad

@ogenstad ogenstad commented Dec 16, 2025

Copy link
Copy Markdown
Contributor

Add sort options for metadata to GraphQL layer, fully implemented for the standard nodes (i.e. only InfrahubBranch) and just included the API bindings for the dynamically defined schema nodes.

Example query for InfrahubBranch:

        query {
            InfrahubBranch(order: {node_metadata: {updated_at: DESC}}) {
                edges {
                    node_metadata { updated_at }
                    node { name { value } description { value } }
                }
            }
        }

In this current implementation I've forbidden anyone from combining updated_at with created_at, specifically for these two fields I don't think it matters much as there won't be much overlap with anything that has the same timestamp. However it might come into play at some later time for other fields. It seems like the order of what the user requests (i.e. if created_at is defined before updated_at) is kept within the info object but not in the created models. At some later point we could analyze the actual query and retain the internal order, for now we raise an error if someone specifies both updated_at and created_at)

I've added the same "order" part to the other dynamically created options, but those doesn't currently do anything, they are just a dummy entry so that the frontend team can experiment with what it will look like for the other node types.

Summary by CodeRabbit

  • New Features
    • Clients can request ordered node/branch lists (by id, created_at, updated_at) with ASC/DESC via new GraphQL Order input and types. Ordering is validated (mutually exclusive timestamp fields).
  • Tests
    • Added GraphQL tests verifying ordering behavior (asc/desc) and invalid ordering combinations.

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai

coderabbitai Bot commented Dec 16, 2025

Copy link
Copy Markdown
Contributor

Walkthrough

Adds ordering end-to-end: new enums OrderDirection and OrderByField and a StandardNodeOrdering dataclass. StandardNode.get_list and Branch.get_list accept node_ordering and forward it to StandardNodeGetListQuery, which applies ordering by id, created_at, or updated_at in the chosen direction. GraphQL adds OrderInput/InfrahubNodeMetadataOrder and InfrahubOrderDirection; resolvers normalize input via OrderModel.from_input. Tests cover ascending/descending ordering and the mutual-exclusion validation for created_at vs updated_at.

Pre-merge checks

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 44.44% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Add GraphQL metadata sort options' clearly and concisely describes the primary change: introducing GraphQL sorting capabilities for metadata fields. It is specific, avoids vague terms, and accurately reflects the main objective of the changeset.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions github-actions Bot added the group/backend Issue related to the backend (API Server, Git Agent) label Dec 16, 2025
@ogenstad ogenstad force-pushed the pog-graphql-sort-IFC-1941 branch from e289668 to 05a5f4a Compare December 16, 2025 14:25
@codspeed-hq

codspeed-hq Bot commented Dec 16, 2025

Copy link
Copy Markdown

CodSpeed Performance Report

Merging #7904 will not alter performance

Comparing pog-graphql-sort-IFC-1941 (625612d) with develop (51eeea1)

Summary

✅ 12 untouched

@ogenstad ogenstad force-pushed the pog-graphql-sort-IFC-1941 branch 2 times, most recently from bcc5b8f to b4c6be3 Compare December 17, 2025 07:44
@ogenstad ogenstad force-pushed the pog-graphql-sort-IFC-1941 branch 4 times, most recently from f3abb27 to 3474e58 Compare December 17, 2025 14:18
@ogenstad ogenstad marked this pull request as ready for review December 17, 2025 16:16
@ogenstad ogenstad requested a review from a team as a code owner December 17, 2025 16:16

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (4)
backend/infrahub/graphql/types/metadata.py (1)

13-15: Add description for the disable field.

The disable field on line 14 lacks a description parameter, while the node_metadata field has one. For consistency and API discoverability, all GraphQL input fields should include descriptions explaining their purpose and behavior.

Apply this diff to add a description:

 class OrderInput(InputObjectType):
-    disable = Boolean(required=False)
+    disable = Boolean(required=False, description="Disable ordering to preserve insertion order")
     node_metadata = Field(InfrahubNodeMetadataOrder, required=False, description="Order settings for branch metadata")

(Note: Adjust the description text based on the actual intended behavior of the disable field.)

backend/tests/unit/graphql/queries/test_branch.py (1)

543-750: LGTM!

The ordering tests are comprehensive and well-structured:

  • created_at tests verify both ASC and DESC ordering with appropriate assertions
  • updated_at tests cleverly manipulate branch update times to control the ordering, then filter results to avoid test interference
  • All tests include clear docstrings and validate both timestamp order and expected node positions

Consider using pytest.mark.parametrize to reduce duplication between the four ordering tests:

@pytest.mark.parametrize(
    "field,direction,branch_prefix,expected_first",
    [
        ("created_at", "ASC", "alpha", "main"),
        ("created_at", "DESC", "delta", "zeta-branch"),
        # ... etc
    ]
)
async def test_order_by_metadata(self, db, default_branch, service, session_admin, field, direction, branch_prefix, expected_first):
    # Unified test implementation

However, the current explicit approach is clear and maintainable, so this refactoring is purely optional.

schema/schema.graphql (1)

9788-9798: Consider improving the enum docstring.

The docstring "An enumeration." is generic boilerplate and doesn't add value. A more descriptive docstring would improve API documentation.

Additionally, the field description says "Order settings for branch metadata" but the type InfrahubNodeMetadataOrder suggests this is for general node metadata ordering. Consider aligning the description with the actual scope of the functionality.

-"""An enumeration."""
+"""Specifies the sort direction for ordering query results."""
 enum OrderDirection {
   ASC
   DESC
 }

 input OrderInput {
   disable: Boolean
-  """Order settings for branch metadata"""
+  """Order settings for node metadata (e.g., created_at, updated_at)"""
   node_metadata: InfrahubNodeMetadataOrder = null
 }
backend/infrahub/core/branch/models.py (1)

176-196: Consider adding node_ordering to get_list_count for API consistency.

The get_list method now accepts node_ordering, but get_list_count does not. While ordering may not affect count results, the caller in branch.py (line 81) passes node_ordering to get_list_count. This parameter is currently absorbed by **kwargs and ignored, which could cause confusion about the API contract.

Since the PR notes that dynamic options are "non-functional placeholders," this may be intentional for now—but consider documenting or aligning the signatures for maintainability.

📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 5eca117 and 3474e58.

📒 Files selected for processing (13)
  • backend/infrahub/constants/enums.py (1 hunks)
  • backend/infrahub/core/branch/models.py (2 hunks)
  • backend/infrahub/core/node/standard.py (4 hunks)
  • backend/infrahub/core/query/standard_node.py (4 hunks)
  • backend/infrahub/graphql/manager.py (1 hunks)
  • backend/infrahub/graphql/models.py (1 hunks)
  • backend/infrahub/graphql/queries/branch.py (4 hunks)
  • backend/infrahub/graphql/resolvers/ipam.py (4 hunks)
  • backend/infrahub/graphql/resolvers/resolver.py (3 hunks)
  • backend/infrahub/graphql/types/enums.py (2 hunks)
  • backend/infrahub/graphql/types/metadata.py (1 hunks)
  • backend/tests/unit/graphql/queries/test_branch.py (1 hunks)
  • schema/schema.graphql (3 hunks)
🧰 Additional context used
📓 Path-based instructions (4)
**/*.py

📄 CodeRabbit inference engine (.github/instructions/python-docstring.instructions.md)

**/*.py: Always use triple quotes (""") for Python docstrings
Follow Google-style docstring format for Python docstrings
Include brief one-line description in Python docstrings when applicable
Include detailed description in Python docstrings when applicable
Include Args/Parameters section without typing in Python docstrings when applicable
Include Returns section in Python docstrings when applicable
Include Raises section in Python docstrings when applicable
Include Examples section in Python docstrings when applicable

**/*.py: Use type hints for all function parameters and return values in Python
Use Async whenever possible in Python
Use async def for asynchronous functions in Python
Use await for asynchronous calls in Python
Use Pydantic models for dataclasses in Python
Use ruff and mypy for type checking and code validation in Python

Use ruff and mypy to validate and lint Python files

Files:

  • backend/infrahub/core/query/standard_node.py
  • backend/infrahub/graphql/resolvers/resolver.py
  • backend/infrahub/constants/enums.py
  • backend/infrahub/core/node/standard.py
  • backend/infrahub/graphql/types/metadata.py
  • backend/infrahub/graphql/queries/branch.py
  • backend/infrahub/graphql/resolvers/ipam.py
  • backend/infrahub/graphql/models.py
  • backend/tests/unit/graphql/queries/test_branch.py
  • backend/infrahub/core/branch/models.py
  • backend/infrahub/graphql/manager.py
  • backend/infrahub/graphql/types/enums.py
backend/infrahub/**/*.py

📄 CodeRabbit inference engine (backend/AGENTS.md)

backend/infrahub/**/*.py: Use async/await for all I/O operations to maintain async-first architecture
Type hint all function parameters and returns in Python code
Use Pydantic models for defining data structures instead of plain dictionaries
Use Query class pattern (extending infrahub.core.query.Query) for all database operations instead of unparameterized Cypher queries
Use Google-style docstrings with Args, Returns, and Raises sections for all functions
Use snake_case for function and variable names
Use PascalCase for class names
Use UPPER_SNAKE_CASE for constant definitions
Do not use unparameterized Cypher queries; always use parameterized queries to prevent injection
Do not block the event loop with synchronous I/O operations

Files:

  • backend/infrahub/core/query/standard_node.py
  • backend/infrahub/graphql/resolvers/resolver.py
  • backend/infrahub/constants/enums.py
  • backend/infrahub/core/node/standard.py
  • backend/infrahub/graphql/types/metadata.py
  • backend/infrahub/graphql/queries/branch.py
  • backend/infrahub/graphql/resolvers/ipam.py
  • backend/infrahub/graphql/models.py
  • backend/infrahub/core/branch/models.py
  • backend/infrahub/graphql/manager.py
  • backend/infrahub/graphql/types/enums.py
backend/**/*.py

📄 CodeRabbit inference engine (AGENTS.md)

Use type hints for all Python code in the backend

Files:

  • backend/infrahub/core/query/standard_node.py
  • backend/infrahub/graphql/resolvers/resolver.py
  • backend/infrahub/constants/enums.py
  • backend/infrahub/core/node/standard.py
  • backend/infrahub/graphql/types/metadata.py
  • backend/infrahub/graphql/queries/branch.py
  • backend/infrahub/graphql/resolvers/ipam.py
  • backend/infrahub/graphql/models.py
  • backend/tests/unit/graphql/queries/test_branch.py
  • backend/infrahub/core/branch/models.py
  • backend/infrahub/graphql/manager.py
  • backend/infrahub/graphql/types/enums.py
backend/tests/**/*.py

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Run backend tests with pytest or via invoke tasks

Name test files as test_<module>.py and mirror source structure in tests directory

Files:

  • backend/tests/unit/graphql/queries/test_branch.py
🧠 Learnings (2)
📚 Learning: 2025-12-01T22:16:13.680Z
Learnt from: CR
Repo: opsmill/infrahub PR: 0
File: backend/AGENTS.md:0-0
Timestamp: 2025-12-01T22:16:13.680Z
Learning: Applies to backend/infrahub/**/*.py : Use Query class pattern (extending infrahub.core.query.Query) for all database operations instead of unparameterized Cypher queries

Applied to files:

  • backend/infrahub/core/query/standard_node.py
📚 Learning: 2025-10-30T12:43:45.663Z
Learnt from: ogenstad
Repo: opsmill/infrahub PR: 7447
File: schema/schema.graphql:324-329
Timestamp: 2025-10-30T12:43:45.663Z
Learning: In the GraphQL schema (schema/schema.graphql), the BranchStatus enum includes NEED_UPGRADE_REBASE. Branches with NEED_UPGRADE_REBASE status must support conflict resolution capabilities.

Applied to files:

  • backend/infrahub/graphql/queries/branch.py
🧬 Code graph analysis (9)
backend/infrahub/core/query/standard_node.py (2)
backend/infrahub/constants/enums.py (1)
  • OrderByField (9-12)
backend/infrahub/core/node/standard.py (2)
  • StandardNode (83-349)
  • StandardNodeOrdering (37-74)
backend/infrahub/graphql/resolvers/resolver.py (1)
backend/infrahub/graphql/models.py (2)
  • OrderModel (19-36)
  • from_input (25-32)
backend/infrahub/graphql/queries/branch.py (2)
backend/infrahub/core/node/standard.py (2)
  • StandardNodeOrdering (37-74)
  • from_order_input (42-74)
backend/infrahub/graphql/types/metadata.py (1)
  • OrderInput (13-15)
backend/infrahub/graphql/resolvers/ipam.py (1)
backend/infrahub/graphql/models.py (2)
  • OrderModel (19-36)
  • from_input (25-32)
backend/infrahub/graphql/models.py (2)
backend/infrahub/types.py (1)
  • Any (312-319)
backend/infrahub/constants/enums.py (1)
  • OrderDirection (4-6)
backend/tests/unit/graphql/queries/test_branch.py (2)
backend/infrahub/graphql/initialization.py (1)
  • prepare_graphql_params (89-136)
backend/infrahub/core/node/standard.py (1)
  • save (205-211)
backend/infrahub/core/branch/models.py (2)
backend/infrahub/core/node/standard.py (2)
  • StandardNode (83-349)
  • StandardNodeOrdering (37-74)
backend/infrahub/core/query/branch.py (1)
  • BranchNodeGetListQuery (153-160)
backend/infrahub/graphql/manager.py (1)
backend/infrahub/graphql/types/metadata.py (1)
  • OrderInput (13-15)
backend/infrahub/graphql/types/enums.py (1)
backend/infrahub/constants/enums.py (1)
  • OrderDirection (4-6)
🔇 Additional comments (23)
backend/infrahub/graphql/types/enums.py (1)

3-3: LGTM!

The import and enum alias follow the existing pattern in the file and correctly expose the OrderDirection enum to the GraphQL layer.

Also applies to: 18-18

backend/infrahub/core/query/standard_node.py (2)

3-3: LGTM!

The new imports properly support the ordering functionality with type-safe enum handling and exhaustiveness checking.

Also applies to: 5-5, 12-12


184-192: Verify ORDER BY direction parameterization approach.

The direction.value is directly interpolated into the Cypher query string (lines 188, 190). While this is safe because the value comes from the OrderDirection enum, the coding guidelines state: "Do not use unparameterized Cypher queries; always use parameterized queries to prevent injection."

Can the ORDER BY direction be parameterized in Cypher, or is string interpolation of enum values the accepted pattern for ORDER BY clauses in this codebase?

Based on learnings, the Query class pattern should be used for all database operations with parameterized queries.

backend/infrahub/graphql/types/metadata.py (1)

8-10: LGTM!

The InfrahubNodeMetadataOrder input type is well-structured with clear descriptions for both ordering fields.

backend/infrahub/constants/enums.py (1)

1-12: LGTM!

The enum definitions are clean and follow Python best practices. The string values are appropriately cased for their respective contexts.

backend/infrahub/graphql/manager.py (1)

24-24: LGTM!

Centralizing the OrderInput definition by importing it from metadata.py instead of defining it locally is good practice. This ensures consistency across the GraphQL layer.

Also applies to: 1001-1001

backend/infrahub/graphql/resolvers/resolver.py (1)

149-149: LGTM!

The refactoring properly separates GraphQL input handling (raw dict) from the internal model (OrderModel). The OrderModel.from_input method handles conversion and validation, which is a clean separation of concerns.

Also applies to: 159-160, 208-208

backend/tests/unit/graphql/queries/test_branch.py (2)

484-542: LGTM!

The test properly validates ascending ordering by created_at. It creates branches in a known sequence, queries with ordering, and verifies both timestamp ordering and expected positions.


752-778: LGTM!

The validation test properly ensures that specifying both created_at and updated_at raises an error, confirming the constraint is enforced as intended by the PR objectives.

schema/schema.graphql (2)

7428-7433: LGTM!

The input type is well-structured with clear docstrings. The optional nature of both fields with null defaults is appropriate, and the mutual exclusivity constraint (not allowing both created_at and updated_at together) is noted as being enforced at the resolver layer.


11137-11145: LGTM!

The order parameter is cleanly integrated into the InfrahubBranch query with a clear description. The optional nature of the parameter is appropriate for backward compatibility.

backend/infrahub/graphql/resolvers/ipam.py (2)

19-19: LGTM!

Import of OrderModel is correctly placed and aligns with the new ordering pattern.


312-312: LGTM!

The signature change from OrderModel | None to dict[str, Any] | None correctly reflects how GraphQL resolver arguments are received. The conversion via OrderModel.from_input(input_data=order) properly handles the dictionary-to-model transformation and validation, and the resulting order_model (which may be None) is correctly passed to NodeManager.query.

Also applies to: 325-325, 404-404

backend/infrahub/core/node/standard.py (1)

340-345: LGTM!

The node_ordering parameter is properly typed and defaulted. The inline default ensures a StandardNodeOrdering() is always passed to the query, avoiding None checks downstream.

backend/infrahub/graphql/queries/branch.py (4)

5-12: LGTM!

Imports are properly organized with Argument added for the GraphQL field definition, and both StandardNodeOrdering and OrderInput are correctly imported from their respective modules.


47-47: LGTM!

The ordering flow is well-implemented: the order parameter is properly typed, converted once via StandardNodeOrdering.from_order_input(order), and passed through to InfrahubBranch.get_list. This ensures validation happens early and the ordering is consistently applied.

Also applies to: 54-55, 72-72


102-106: LGTM!

The OrderInput argument is properly configured with required=False and includes a clear description for API consumers.


76-81: The node_ordering parameter is properly accepted and used in the method chain.

The concern is unfounded. StandardNodeGetListQuery.__init__ explicitly declares node_ordering: StandardNodeOrdering as a named parameter (not via **kwargs), stores it as self.node_ordering, and uses it in the query_init() method to determine the ORDER BY clause of the Cypher query. The parameter flows correctly through the entire call chain: InfrahubBranchType.get_list_countBranch.get_list_countBranchNodeGetListQueryStandardNodeGetListQuery, where it is properly handled.

Likely an incorrect or invalid review comment.

backend/infrahub/core/branch/models.py (2)

12-12: LGTM!

Import of StandardNodeOrdering alongside StandardNode is correctly placed.


159-169: LGTM!

The node_ordering parameter is properly integrated into get_list with correct type hints and default handling. The ordering is correctly propagated to BranchNodeGetListQuery.init.

backend/infrahub/graphql/models.py (3)

1-9: LGTM!

Imports are well-organized. Using TYPE_CHECKING for OrderDirection avoids a circular import while maintaining type safety through string annotations with from __future__ import annotations.


12-15: LGTM!

NodeMetaOrder dataclass cleanly captures the ordering options with appropriate optional fields.


34-36: Validation logic is correct.

The mutual exclusivity check for created_at and updated_at is properly implemented, aligning with the PR requirement that combining these fields should raise an error.

Comment thread backend/infrahub/core/node/standard.py Outdated
Comment on lines +36 to +74
@dataclass
class StandardNodeOrdering:
order_by: OrderByField = field(default=OrderByField.ID)
direction: OrderDirection = field(default=OrderDirection.ASC)

@classmethod
def from_order_input(cls, order: OrderInput | None = None) -> StandardNodeOrdering:
"""Create a StandardNodeOrdering from an OrderInput.

Args:
order: Optional ordering specification from GraphQL input.

Returns:
StandardNodeOrdering with the specified field and direction, or defaults to ID with no direction.

Raises:
ValidationError: If both created_at and updated_at are specified.
"""
if order is None:
return cls(order_by=OrderByField.ID)

node_metadata = getattr(order, "node_metadata", None)

if node_metadata is None:
return cls(order_by=OrderByField.ID)

created_at = getattr(node_metadata, "created_at", None)
updated_at = getattr(node_metadata, "updated_at", None)

if created_at and updated_at:
raise ValidationError("Only one of 'created_at' or 'updated_at' can be specified for ordering.")

if created_at:
return cls(order_by=OrderByField.CREATED_AT, direction=OrderDirection(created_at.value))

if updated_at:
return cls(order_by=OrderByField.UPDATED_AT, direction=OrderDirection(updated_at.value))

return cls(order_by=OrderByField.ID)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Docstring inaccuracy: direction defaults to ASC, not "no direction".

Line 49 states "defaults to ID with no direction" but the class default on line 39 is direction: OrderDirection = field(default=OrderDirection.ASC). Update the docstring to reflect the actual default behavior.

         Returns:
-            StandardNodeOrdering with the specified field and direction, or defaults to ID with no direction.
+            StandardNodeOrdering with the specified field and direction, or defaults to ID ascending.
🤖 Prompt for AI Agents
In backend/infrahub/core/node/standard.py around lines 36 to 74, the
from_order_input docstring incorrectly says the default is "ID with no
direction" but the class default sets direction to OrderDirection.ASC; update
the docstring so the Returns section accurately states the default is ID with
direction ASC (or "ascending") and adjust any related phrasing (e.g., remove "no
direction") to reflect that the ordering defaults to ID ascending.

Comment on lines +24 to +32
@classmethod
def from_input(cls, input_data: dict[str, Any] | None) -> OrderModel | None:
"""Convert the dictionary type input data from GraphQL into an OrderModel instance."""
if not input_data:
return None

order_model = cls(**input_data)
order_model.validate()
return order_model

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Nested node_metadata dict won't be deserialized to NodeMetaOrder.

The from_input method uses cls(**input_data) directly, but if input_data contains a nested dict for node_metadata (e.g., {"node_metadata": {"created_at": "ASC"}}), it will remain a dict rather than being converted to a NodeMetaOrder instance. This will cause validate() to fail when accessing self.node_metadata.created_at since dict objects don't have those attributes.

Consider deserializing the nested structure:

     @classmethod
     def from_input(cls, input_data: dict[str, Any] | None) -> OrderModel | None:
         """Convert the dictionary type input data from GraphQL into an OrderModel instance."""
         if not input_data:
             return None

-        order_model = cls(**input_data)
+        node_metadata = None
+        if meta_data := input_data.get("node_metadata"):
+            node_metadata = NodeMetaOrder(**meta_data)
+        order_model = cls(disable=input_data.get("disable"), node_metadata=node_metadata)
         order_model.validate()
         return order_model

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In backend/infrahub/graphql/models.py around lines 24-32, the from_input method
currently passes nested node_metadata dicts straight into cls(**input_data)
which leaves node_metadata as a plain dict and breaks validate(); detect if
input_data.get("node_metadata") is a dict and convert it to a NodeMetaOrder
instance before constructing the OrderModel (e.g., replace the dict with
NodeMetaOrder.from_input(...) or NodeMetaOrder(**...) depending on available
factory), then call cls(**input_data) and validate as before.

Comment thread backend/infrahub/core/node/standard.py Outdated
Comment thread backend/infrahub/core/node/standard.py Outdated
Comment on lines +57 to +63
node_metadata = getattr(order, "node_metadata", None)

if node_metadata is None:
return cls(order_by=OrderByField.ID)

created_at = getattr(node_metadata, "created_at", None)
updated_at = getattr(node_metadata, "updated_at", None)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do we really need to user getattr here?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unfortunately yes we do at this point. It's to make the type checker happy.

class OrderInput(InputObjectType):
    disable = Boolean(required=False)
    node_metadata = Field(InfrahubNodeMetadataOrder, required=False, description="Order settings for branch metadata")

The node_metadata attribute is defined as a Field and not what it actually is InfrahubNodeMetadataOrder. In some places we just ignore this. We might have to update these definitions in the future to have a cleaner internal API. I agree it's not pretty. :(

Comment thread backend/infrahub/core/node/standard.py Outdated
case OrderByField.UPDATED_AT:
self.order_by = [f"n.updated_at {self.node_ordering.direction.value}"]
case _:
assert_never(self.node_ordering.order_by)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

interesting. learned something new today

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's something that I noticed that they have in Rust and I thought it was a nice thing to check against so when looking for an alternative in Python I came across this. I asked Lucas to add it in one other place as well but I think it's something that we can probably use in more places moving forward.

self.order_by = [f"{db.get_id_function_name()}(n)"]
match self.node_ordering.order_by:
case OrderByField.ID:
self.order_by = [f"{db.get_id_function_name()}(n)"]

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ID usually means Node.uuid for us, not the database's elementId

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I will leave this as is for now since I didn't want to change the existing default behaviour. But we can backtrack later and switch this to the uuid instead. What do you think?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I missed it was already the default behavior
this is fine then

session_admin: AccountSession,
) -> None:
"""Test that branches can be ordered by created_at in descending order."""
branch_names = ["delta-branch", "epsilon-branch", "zeta-branch"]

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fancy

assert timestamps == sorted(timestamps, reverse=True), "Branches should be ordered by created_at descending"

# The last branch created should be first
assert edges[0]["node"]["name"]["value"] == "zeta-branch"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

might as well check that all three are here in the expected order

assert result.errors is None

# Update branches in a specific order: iota first, then theta, then eta
# This means eta will have the latest updated_at

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sometimes these comments feel like they are directed at an idiot

# The first should be main (default branch, created first)
assert edges[0]["node"]["name"]["value"] == "main"

async def test_order_by_created_at_descending(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

feels like this could be combined with the ascending version of the test

result_branch_names = [e["node"]["name"]["value"] for e in test_edges]
assert result_branch_names == update_order, "Updated branches should appear in update order (ascending)"

async def test_order_by_updated_at_descending(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

also feels like it can be combined

@ogenstad ogenstad force-pushed the pog-graphql-sort-IFC-1941 branch from 3474e58 to 342b8fd Compare December 18, 2025 12:27

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

♻️ Duplicate comments (3)
backend/infrahub/graphql/models.py (1)

24-32: Nested node_metadata dict won't be deserialized to NodeMetaOrder.

The from_input method uses cls(**input_data) directly, but if input_data contains {"node_metadata": {"created_at": "ASC"}}, the nested dict remains a dict rather than being converted to a NodeMetaOrder instance. This will cause validate() to fail when accessing self.node_metadata.created_at.

🔎 Suggested fix:
     @classmethod
     def from_input(cls, input_data: dict[str, Any] | None) -> OrderModel | None:
         """Convert the dictionary type input data from GraphQL into an OrderModel instance."""
         if not input_data:
             return None

+        node_metadata = None
+        if meta_data := input_data.get("node_metadata"):
+            node_metadata = NodeMetaOrder(**meta_data)
+        order_model = cls(disable=input_data.get("disable"), node_metadata=node_metadata)
-        order_model = cls(**input_data)
         order_model.validate()
         return order_model
backend/tests/unit/graphql/queries/test_branch.py (2)

541-601: Consider combining with the ascending test for reduced duplication.

The ascending and descending tests share similar structure and could be parameterized or combined into a single test that verifies both directions. This would reduce code duplication and maintenance burden.


670-740: LGTM! Proper test isolation maintained.

The test correctly creates, updates, and filters branches to verify descending order. The isolation pattern matches the ascending test.

Note: A previous reviewer suggested combining the ascending/descending tests, but the current separate structure provides clarity.

🧹 Nitpick comments (2)
backend/tests/unit/graphql/queries/test_branch.py (1)

741-767: LGTM! Validates the mutual exclusion constraint.

The test correctly verifies that specifying both created_at and updated_at raises an error. The error message check is appropriately flexible.

Optional: Make error validation more specific

If the error message format is stable, consider checking for a more specific message:

     assert result.errors is not None
     assert len(result.errors) > 0
-    assert "created_at" in str(result.errors[0]) or "updated_at" in str(result.errors[0])
+    error_message = str(result.errors[0]).lower()
+    assert "created_at" in error_message and "updated_at" in error_message, \
+        "Error should mention both conflicting fields"

This would ensure users get a clear error indicating the conflict between both fields.

schema/schema.graphql (1)

9788-9798: LGTM with minor documentation note.

The OrderDirection enum is well-defined. The OrderInput type correctly integrates the new node_metadata field.

One minor observation: the doc comment says "Order settings for branch metadata" but the field/type naming (node_metadata, InfrahubNodeMetadataOrder) suggests more general node metadata ordering. This is fine if branches are the only current use case, but consider updating the comment to "Order settings for node metadata" if this input will be reused for other node types per the PR objectives.

📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 3474e58 and 342b8fd.

📒 Files selected for processing (13)
  • backend/infrahub/constants/enums.py (1 hunks)
  • backend/infrahub/core/branch/models.py (2 hunks)
  • backend/infrahub/core/node/standard.py (3 hunks)
  • backend/infrahub/core/query/standard_node.py (4 hunks)
  • backend/infrahub/graphql/manager.py (1 hunks)
  • backend/infrahub/graphql/models.py (1 hunks)
  • backend/infrahub/graphql/queries/branch.py (4 hunks)
  • backend/infrahub/graphql/resolvers/ipam.py (4 hunks)
  • backend/infrahub/graphql/resolvers/resolver.py (3 hunks)
  • backend/infrahub/graphql/types/enums.py (2 hunks)
  • backend/infrahub/graphql/types/metadata.py (1 hunks)
  • backend/tests/unit/graphql/queries/test_branch.py (1 hunks)
  • schema/schema.graphql (3 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • backend/infrahub/graphql/types/enums.py
  • backend/infrahub/constants/enums.py
🧰 Additional context used
📓 Path-based instructions (4)
**/*.py

📄 CodeRabbit inference engine (.github/instructions/python-docstring.instructions.md)

**/*.py: Always use triple quotes (""") for Python docstrings
Follow Google-style docstring format for Python docstrings
Include brief one-line description in Python docstrings when applicable
Include detailed description in Python docstrings when applicable
Include Args/Parameters section without typing in Python docstrings when applicable
Include Returns section in Python docstrings when applicable
Include Raises section in Python docstrings when applicable
Include Examples section in Python docstrings when applicable

**/*.py: Use type hints for all function parameters and return values in Python
Use Async whenever possible in Python
Use async def for asynchronous functions in Python
Use await for asynchronous calls in Python
Use Pydantic models for dataclasses in Python
Use ruff and mypy for type checking and code validation in Python

Use ruff and mypy to validate and lint Python files

Files:

  • backend/tests/unit/graphql/queries/test_branch.py
  • backend/infrahub/graphql/queries/branch.py
  • backend/infrahub/graphql/resolvers/resolver.py
  • backend/infrahub/core/query/standard_node.py
  • backend/infrahub/graphql/manager.py
  • backend/infrahub/core/node/standard.py
  • backend/infrahub/graphql/models.py
  • backend/infrahub/graphql/types/metadata.py
  • backend/infrahub/graphql/resolvers/ipam.py
  • backend/infrahub/core/branch/models.py
backend/tests/**/*.py

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Run backend tests with pytest or via invoke tasks

Name test files as test_<module>.py and mirror source structure in tests directory

Files:

  • backend/tests/unit/graphql/queries/test_branch.py
backend/**/*.py

📄 CodeRabbit inference engine (AGENTS.md)

Use type hints for all Python code in the backend

Files:

  • backend/tests/unit/graphql/queries/test_branch.py
  • backend/infrahub/graphql/queries/branch.py
  • backend/infrahub/graphql/resolvers/resolver.py
  • backend/infrahub/core/query/standard_node.py
  • backend/infrahub/graphql/manager.py
  • backend/infrahub/core/node/standard.py
  • backend/infrahub/graphql/models.py
  • backend/infrahub/graphql/types/metadata.py
  • backend/infrahub/graphql/resolvers/ipam.py
  • backend/infrahub/core/branch/models.py
backend/infrahub/**/*.py

📄 CodeRabbit inference engine (backend/AGENTS.md)

backend/infrahub/**/*.py: Use async/await for all I/O operations to maintain async-first architecture
Type hint all function parameters and returns in Python code
Use Pydantic models for defining data structures instead of plain dictionaries
Use Query class pattern (extending infrahub.core.query.Query) for all database operations instead of unparameterized Cypher queries
Use Google-style docstrings with Args, Returns, and Raises sections for all functions
Use snake_case for function and variable names
Use PascalCase for class names
Use UPPER_SNAKE_CASE for constant definitions
Do not use unparameterized Cypher queries; always use parameterized queries to prevent injection
Do not block the event loop with synchronous I/O operations

Files:

  • backend/infrahub/graphql/queries/branch.py
  • backend/infrahub/graphql/resolvers/resolver.py
  • backend/infrahub/core/query/standard_node.py
  • backend/infrahub/graphql/manager.py
  • backend/infrahub/core/node/standard.py
  • backend/infrahub/graphql/models.py
  • backend/infrahub/graphql/types/metadata.py
  • backend/infrahub/graphql/resolvers/ipam.py
  • backend/infrahub/core/branch/models.py
🧠 Learnings (2)
📚 Learning: 2025-10-30T12:43:45.663Z
Learnt from: ogenstad
Repo: opsmill/infrahub PR: 7447
File: schema/schema.graphql:324-329
Timestamp: 2025-10-30T12:43:45.663Z
Learning: In the GraphQL schema (schema/schema.graphql), the BranchStatus enum includes NEED_UPGRADE_REBASE. Branches with NEED_UPGRADE_REBASE status must support conflict resolution capabilities.

Applied to files:

  • backend/infrahub/graphql/queries/branch.py
📚 Learning: 2025-12-01T22:16:13.680Z
Learnt from: CR
Repo: opsmill/infrahub PR: 0
File: backend/AGENTS.md:0-0
Timestamp: 2025-12-01T22:16:13.680Z
Learning: Applies to backend/infrahub/**/*.py : Use Query class pattern (extending infrahub.core.query.Query) for all database operations instead of unparameterized Cypher queries

Applied to files:

  • backend/infrahub/core/query/standard_node.py
  • backend/infrahub/core/node/standard.py
🧬 Code graph analysis (7)
backend/infrahub/graphql/resolvers/resolver.py (1)
backend/infrahub/graphql/models.py (2)
  • OrderModel (19-36)
  • from_input (25-32)
backend/infrahub/core/query/standard_node.py (2)
backend/infrahub/constants/enums.py (1)
  • OrderByField (9-12)
backend/infrahub/core/node/standard.py (2)
  • StandardNode (47-315)
  • StandardNodeOrdering (36-38)
backend/infrahub/graphql/manager.py (1)
backend/infrahub/graphql/types/metadata.py (1)
  • OrderInput (13-15)
backend/infrahub/core/node/standard.py (2)
backend/infrahub/constants/enums.py (2)
  • OrderByField (9-12)
  • OrderDirection (4-6)
backend/infrahub/core/query/standard_node.py (1)
  • StandardNodeGetListQuery (133-192)
backend/infrahub/graphql/models.py (1)
backend/infrahub/constants/enums.py (1)
  • OrderDirection (4-6)
backend/infrahub/graphql/resolvers/ipam.py (1)
backend/infrahub/graphql/models.py (2)
  • OrderModel (19-36)
  • from_input (25-32)
backend/infrahub/core/branch/models.py (1)
backend/infrahub/core/node/standard.py (2)
  • StandardNode (47-315)
  • StandardNodeOrdering (36-38)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (9)
  • GitHub Check: E2E-testing-version-upgrade / From 1.3.0
  • GitHub Check: E2E-testing-playwright
  • GitHub Check: backend-benchmark
  • GitHub Check: validate-generated-documentation
  • GitHub Check: backend-docker-integration
  • GitHub Check: backend-tests-functional
  • GitHub Check: backend-tests-integration
  • GitHub Check: backend-tests-unit
  • GitHub Check: Cloudflare Pages
🔇 Additional comments (18)
backend/infrahub/graphql/models.py (2)

12-15: LGTM!

The NodeMetaOrder dataclass is clean and correctly defines the optional ordering fields with proper type hints.


34-36: LGTM!

The validation logic correctly enforces mutual exclusivity between created_at and updated_at ordering, as specified in the PR objectives.

backend/infrahub/graphql/manager.py (2)

24-24: LGTM!

Good refactoring to import OrderInput from the centralized location in infrahub.graphql.types.metadata rather than defining it locally. This reduces duplication and maintains consistency across the GraphQL layer.


1001-1002: LGTM!

The OrderInput is correctly added to the filters dictionary, enabling ordering capabilities for all dynamically generated node queries.

backend/infrahub/graphql/types/metadata.py (1)

8-15: LGTM!

The InfrahubNodeMetadataOrder and OrderInput GraphQL input types are well-structured with appropriate field descriptions. The structure aligns correctly with the corresponding NodeMetaOrder and OrderModel dataclasses in the models layer.

backend/infrahub/graphql/resolvers/resolver.py (2)

149-160: LGTM!

The resolver correctly converts the raw dict input from GraphQL into an OrderModel instance using the factory method, then passes it downstream to NodeManager.query. This provides proper separation between GraphQL input parsing and domain model usage.


38-40: LGTM!

Direct instantiation of OrderModel(disable=True) is appropriate here since this is not coming from GraphQL input and explicitly disables ordering for single-result account lookups.

backend/infrahub/core/node/standard.py (2)

35-38: LGTM!

The StandardNodeOrdering dataclass is well-designed with sensible defaults (OrderByField.ID and OrderDirection.ASC). It provides a clean abstraction for ordering configuration that's consumed by the query layer.


299-312: LGTM!

The get_list method correctly accepts an optional node_ordering parameter and defaults to StandardNodeOrdering() when None is provided. This ensures the query layer always receives a valid ordering configuration. The implementation follows the Query class pattern as per coding guidelines.

backend/tests/unit/graphql/queries/test_branch.py (2)

484-540: LGTM! Well-structured test for ascending order.

The test correctly creates branches sequentially, verifies timestamp ordering, and checks the expected first result.


602-669: LGTM! Excellent test isolation.

This test properly creates its own branches, updates them in a controlled order, and filters results to only verify the test data. The filtering approach at lines 661-662 ensures isolation from other branches.

schema/schema.graphql (2)

7428-7433: LGTM!

The InfrahubNodeMetadataOrder input type is well-structured with appropriate documentation. The mutual exclusion constraint between created_at and updated_at (as noted in the PR) is correctly handled at the resolver level, since GraphQL schema cannot express such constraints natively.


11137-11145: LGTM!

The order parameter is correctly added to the InfrahubBranch query with appropriate documentation. The parameter is optional (as expected), and the integration with pagination parameters (limit, offset) follows standard GraphQL patterns.

backend/infrahub/core/branch/models.py (1)

12-12: LGTM! Ordering support properly threaded through Branch.get_list.

The changes correctly mirror the pattern established in StandardNode.get_list, including the import, parameter addition with proper type hints, defaulting logic, and threading through to the query layer.

Also applies to: 159-174

backend/infrahub/graphql/resolvers/ipam.py (1)

19-19: LGTM! Proper conversion from GraphQL input to OrderModel.

The changes correctly implement the pattern of accepting flexible dict input from GraphQL and converting it to a strongly-typed OrderModel via from_input. The runtime import of OrderModel is necessary for this conversion, and validation errors will propagate appropriately.

Also applies to: 312-312, 325-325, 404-404

backend/infrahub/graphql/queries/branch.py (2)

19-46: Well-documented conversion function with appropriate validation.

The function correctly converts GraphQL OrderInput to StandardNodeOrdering with proper validation, type handling, and complete Google-style docstring per coding guidelines.

Note: The mutual-exclusion validation for created_at and updated_at is also present in OrderModel.validate() (backend/infrahub/graphql/models.py). This duplication provides defense-in-depth, which is acceptable, though the two validation paths operate on different types (OrderInput vs OrderModel).


78-113: LGTM! Ordering properly wired through resolver and GraphQL field.

The changes correctly:

  • Accept order: OrderInput | None in the resolver
  • Convert it to StandardNodeOrdering via the helper function
  • Thread node_ordering through to both list and count operations
  • Expose the order argument in the GraphQL field definition with proper description

Passing node_ordering to get_list_count is acceptable for API consistency even if the count operation doesn't use ordering.

Also applies to: 133-137

backend/infrahub/core/query/standard_node.py (1)

141-151: LGTM! Ordering parameter properly stored on query instance.

The node_ordering parameter is correctly added as a required parameter (callers handle defaulting) and stored on the instance for use in query_init.

Comment on lines +184 to +192
match self.node_ordering.order_by:
case OrderByField.ID:
self.order_by = [f"{db.get_id_function_name()}(n)"]
case OrderByField.CREATED_AT:
self.order_by = [f"n.created_at {self.node_ordering.direction.value}"]
case OrderByField.UPDATED_AT:
self.order_by = [f"n.updated_at {self.node_ordering.direction.value}"]
case _:
assert_never(self.node_ordering.order_by)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Critical: ID ordering doesn't respect the direction parameter.

Line 186 constructs the order_by clause for OrderByField.ID but omits the direction:

self.order_by = [f"{db.get_id_function_name()}(n)"]

This means ID ordering will always use the database's default sort direction, ignoring self.node_ordering.direction. The CREATED_AT and UPDATED_AT cases correctly include the direction.

🔎 Apply this diff to include direction for ID ordering:
         match self.node_ordering.order_by:
             case OrderByField.ID:
-                self.order_by = [f"{db.get_id_function_name()}(n)"]
+                self.order_by = [f"{db.get_id_function_name()}(n) {self.node_ordering.direction.value}"]
             case OrderByField.CREATED_AT:
                 self.order_by = [f"n.created_at {self.node_ordering.direction.value}"]
             case OrderByField.UPDATED_AT:
                 self.order_by = [f"n.updated_at {self.node_ordering.direction.value}"]
🤖 Prompt for AI Agents
In backend/infrahub/core/query/standard_node.py around lines 184 to 192, the
OrderByField.ID branch builds the order_by clause without appending the sort
direction so ID ordering ignores self.node_ordering.direction; change that
branch to include the direction the same way as CREATED_AT/UPDATED_AT (e.g.,
append f" {self.node_ordering.direction.value}" after the id expression) so the
generated order_by respects ascending/descending; keep the existing pattern and
formatting used for the other cases.

Comment thread backend/tests/unit/graphql/queries/test_branch.py
@ogenstad ogenstad force-pushed the pog-graphql-sort-IFC-1941 branch from 342b8fd to 625612d Compare December 18, 2025 12:43

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

♻️ Duplicate comments (3)
backend/tests/unit/graphql/queries/test_branch.py (1)

597-600: Fix test isolation: remove assertion on branch from another test.

Line 600 asserts that edges[3] contains "gamma-branch", which is only created in test_order_by_created_at_ascending (line 492), creating a dependency on test execution order. This issue was previously flagged in past review comments.

🔎 Fix by filtering to test-created branches (pattern from updated_at tests):
+    # Filter to only branches created in this test
+    test_branch_names = set(branch_names)
+    test_edges = [e for e in edges if e["node"]["name"]["value"] in test_branch_names]
+
+    result_branch_names = [e["node"]["name"]["value"] for e in test_edges]
-    assert edges[0]["node"]["name"]["value"] == "zeta-branch"
-    assert edges[1]["node"]["name"]["value"] == "epsilon-branch"
-    assert edges[2]["node"]["name"]["value"] == "delta-branch"
-    assert edges[3]["node"]["name"]["value"] == "gamma-branch"
+    assert result_branch_names == ["zeta-branch", "epsilon-branch", "delta-branch"]
backend/infrahub/graphql/models.py (1)

24-32: Nested node_metadata dict won't be deserialized to NodeMetaOrder.

When input_data contains {"node_metadata": {"created_at": "ASC"}}, the cls(**input_data) call assigns the raw dict to node_metadata instead of a NodeMetaOrder instance. The validate() method will then fail when accessing self.node_metadata.created_at since dict objects don't have that attribute.

🔎 Apply this diff to properly deserialize the nested structure:
     @classmethod
     def from_input(cls, input_data: dict[str, Any] | None) -> OrderModel | None:
         """Convert the dictionary type input data from GraphQL into an OrderModel instance."""
         if not input_data:
             return None

-        order_model = cls(**input_data)
+        node_metadata = None
+        if meta_data := input_data.get("node_metadata"):
+            node_metadata = NodeMetaOrder(**meta_data)
+        order_model = cls(disable=input_data.get("disable"), node_metadata=node_metadata)
         order_model.validate()
         return order_model
backend/infrahub/core/query/standard_node.py (1)

184-192: ID ordering doesn't respect the direction parameter.

Line 186 omits the direction for OrderByField.ID ordering, while CREATED_AT and UPDATED_AT correctly include it. This means ID ordering always uses the database's default sort direction.

🔎 Apply this diff to include direction for ID ordering:
         match self.node_ordering.order_by:
             case OrderByField.ID:
-                self.order_by = [f"{db.get_id_function_name()}(n)"]
+                self.order_by = [f"{db.get_id_function_name()}(n) {self.node_ordering.direction.value}"]
             case OrderByField.CREATED_AT:
                 self.order_by = [f"n.created_at {self.node_ordering.direction.value}"]
             case OrderByField.UPDATED_AT:
                 self.order_by = [f"n.updated_at {self.node_ordering.direction.value}"]
🧹 Nitpick comments (6)
backend/infrahub/constants/enums.py (1)

1-12: Consider adding docstrings for clarity.

While the enum names and values are self-explanatory, adding brief docstrings would improve code documentation and align with the project's Google-style docstring guidelines.

🔎 Example docstrings:
 class OrderDirection(StrEnum):
+    """Specifies the direction for ordering query results."""
     ASC = "ASC"
     DESC = "DESC"


 class OrderByField(StrEnum):
+    """Specifies the field to use for ordering nodes."""
     ID = "id"
     CREATED_AT = "created_at"
     UPDATED_AT = "updated_at"
backend/infrahub/graphql/types/metadata.py (1)

8-15: Add class-level docstrings for the new InputObjectType classes.

Both InfrahubNodeMetadataOrder and OrderInput would benefit from docstrings explaining their purpose, per the coding guidelines for Python files.

🔎 Suggested docstrings:
 class InfrahubNodeMetadataOrder(InputObjectType):
+    """Input type for specifying ordering by node metadata fields.
+    
+    Allows ordering by created_at or updated_at timestamps.
+    Only one field should be specified at a time.
+    """
     created_at = Field(InfrahubOrderDirection, required=False, description="Order by creation timestamp")
     updated_at = Field(InfrahubOrderDirection, required=False, description="Order by updated timestamp")


 class OrderInput(InputObjectType):
+    """Input type for controlling result ordering in queries.
+    
+    Provides options to disable ordering or specify metadata-based ordering.
+    """
     disable = Boolean(required=False)
     node_metadata = Field(InfrahubNodeMetadataOrder, required=False, description="Order settings for branch metadata")

As per coding guidelines, use Google-style docstrings for Python classes.

backend/tests/unit/graphql/queries/test_branch.py (1)

484-540: Consider filtering to test-created branches for better isolation.

While the current assertion on line 539 (checking edges[0] is "main") is likely safe, the test would be more robust if it filtered results to only the branches created in this test, similar to the approach in test_order_by_updated_at_ascending (lines 660-661).

🔎 Example pattern from updated_at tests:
test_branch_names = set(branch_names)
test_edges = [e for e in edges if e["node"]["name"]["value"] in test_branch_names]
# Then assert on test_edges instead of edges
backend/infrahub/core/node/standard.py (1)

35-38: Add a docstring to the StandardNodeOrdering dataclass.

Per the coding guidelines, classes should have Google-style docstrings explaining their purpose. A docstring would clarify the role of this dataclass in configuring node query ordering.

🔎 Suggested docstring:
 @dataclass
 class StandardNodeOrdering:
+    """Configuration for ordering standard node queries.
+    
+    Attributes:
+        order_by: Field to order by (ID, CREATED_AT, or UPDATED_AT).
+        direction: Sort direction (ASC or DESC).
+    """
     order_by: OrderByField = field(default=OrderByField.ID)
     direction: OrderDirection = field(default=OrderDirection.ASC)

As per coding guidelines, use Google-style docstrings for Python classes.

schema/schema.graphql (1)

7428-7433: Consider documenting the mutual exclusion constraint.

According to the PR objectives, combining created_at with updated_at is forbidden and raises an error. Adding a doc comment on the input type explaining this constraint would help API consumers understand the expected usage.

+"""
+Order settings for node metadata timestamps.
+Note: Only one of created_at or updated_at can be specified at a time.
+"""
 input InfrahubNodeMetadataOrder {
   """Order by creation timestamp"""
   created_at: OrderDirection = null
   """Order by updated timestamp"""
   updated_at: OrderDirection = null
 }
backend/infrahub/graphql/models.py (1)

12-22: Consider using Pydantic models instead of dataclasses.

The coding guidelines specify "Use Pydantic models for dataclasses in Python". Using Pydantic would also provide automatic nested model deserialization, which would resolve the from_input issue.

📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 342b8fd and 625612d.

📒 Files selected for processing (13)
  • backend/infrahub/constants/enums.py (1 hunks)
  • backend/infrahub/core/branch/models.py (2 hunks)
  • backend/infrahub/core/node/standard.py (3 hunks)
  • backend/infrahub/core/query/standard_node.py (4 hunks)
  • backend/infrahub/graphql/manager.py (1 hunks)
  • backend/infrahub/graphql/models.py (1 hunks)
  • backend/infrahub/graphql/queries/branch.py (4 hunks)
  • backend/infrahub/graphql/resolvers/ipam.py (4 hunks)
  • backend/infrahub/graphql/resolvers/resolver.py (3 hunks)
  • backend/infrahub/graphql/types/enums.py (2 hunks)
  • backend/infrahub/graphql/types/metadata.py (1 hunks)
  • backend/tests/unit/graphql/queries/test_branch.py (1 hunks)
  • schema/schema.graphql (3 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • backend/infrahub/graphql/resolvers/resolver.py
  • backend/infrahub/graphql/types/enums.py
🧰 Additional context used
📓 Path-based instructions (4)
**/*.py

📄 CodeRabbit inference engine (.github/instructions/python-docstring.instructions.md)

**/*.py: Always use triple quotes (""") for Python docstrings
Follow Google-style docstring format for Python docstrings
Include brief one-line description in Python docstrings when applicable
Include detailed description in Python docstrings when applicable
Include Args/Parameters section without typing in Python docstrings when applicable
Include Returns section in Python docstrings when applicable
Include Raises section in Python docstrings when applicable
Include Examples section in Python docstrings when applicable

**/*.py: Use type hints for all function parameters and return values in Python
Use Async whenever possible in Python
Use async def for asynchronous functions in Python
Use await for asynchronous calls in Python
Use Pydantic models for dataclasses in Python
Use ruff and mypy for type checking and code validation in Python

Use ruff and mypy to validate and lint Python files

Files:

  • backend/infrahub/constants/enums.py
  • backend/infrahub/graphql/types/metadata.py
  • backend/infrahub/core/branch/models.py
  • backend/infrahub/core/node/standard.py
  • backend/infrahub/graphql/manager.py
  • backend/infrahub/core/query/standard_node.py
  • backend/infrahub/graphql/models.py
  • backend/infrahub/graphql/resolvers/ipam.py
  • backend/tests/unit/graphql/queries/test_branch.py
  • backend/infrahub/graphql/queries/branch.py
backend/infrahub/**/*.py

📄 CodeRabbit inference engine (backend/AGENTS.md)

backend/infrahub/**/*.py: Use async/await for all I/O operations to maintain async-first architecture
Type hint all function parameters and returns in Python code
Use Pydantic models for defining data structures instead of plain dictionaries
Use Query class pattern (extending infrahub.core.query.Query) for all database operations instead of unparameterized Cypher queries
Use Google-style docstrings with Args, Returns, and Raises sections for all functions
Use snake_case for function and variable names
Use PascalCase for class names
Use UPPER_SNAKE_CASE for constant definitions
Do not use unparameterized Cypher queries; always use parameterized queries to prevent injection
Do not block the event loop with synchronous I/O operations

Files:

  • backend/infrahub/constants/enums.py
  • backend/infrahub/graphql/types/metadata.py
  • backend/infrahub/core/branch/models.py
  • backend/infrahub/core/node/standard.py
  • backend/infrahub/graphql/manager.py
  • backend/infrahub/core/query/standard_node.py
  • backend/infrahub/graphql/models.py
  • backend/infrahub/graphql/resolvers/ipam.py
  • backend/infrahub/graphql/queries/branch.py
backend/**/*.py

📄 CodeRabbit inference engine (AGENTS.md)

Use type hints for all Python code in the backend

Files:

  • backend/infrahub/constants/enums.py
  • backend/infrahub/graphql/types/metadata.py
  • backend/infrahub/core/branch/models.py
  • backend/infrahub/core/node/standard.py
  • backend/infrahub/graphql/manager.py
  • backend/infrahub/core/query/standard_node.py
  • backend/infrahub/graphql/models.py
  • backend/infrahub/graphql/resolvers/ipam.py
  • backend/tests/unit/graphql/queries/test_branch.py
  • backend/infrahub/graphql/queries/branch.py
backend/tests/**/*.py

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Run backend tests with pytest or via invoke tasks

Name test files as test_<module>.py and mirror source structure in tests directory

Files:

  • backend/tests/unit/graphql/queries/test_branch.py
🧠 Learnings (2)
📚 Learning: 2025-12-01T22:16:13.680Z
Learnt from: CR
Repo: opsmill/infrahub PR: 0
File: backend/AGENTS.md:0-0
Timestamp: 2025-12-01T22:16:13.680Z
Learning: Applies to backend/infrahub/**/*.py : Use Query class pattern (extending infrahub.core.query.Query) for all database operations instead of unparameterized Cypher queries

Applied to files:

  • backend/infrahub/core/node/standard.py
  • backend/infrahub/core/query/standard_node.py
📚 Learning: 2025-10-30T12:43:45.663Z
Learnt from: ogenstad
Repo: opsmill/infrahub PR: 7447
File: schema/schema.graphql:324-329
Timestamp: 2025-10-30T12:43:45.663Z
Learning: In the GraphQL schema (schema/schema.graphql), the BranchStatus enum includes NEED_UPGRADE_REBASE. Branches with NEED_UPGRADE_REBASE status must support conflict resolution capabilities.

Applied to files:

  • backend/infrahub/graphql/queries/branch.py
🧬 Code graph analysis (8)
backend/infrahub/core/branch/models.py (2)
backend/infrahub/core/node/standard.py (2)
  • StandardNode (47-315)
  • StandardNodeOrdering (36-38)
backend/infrahub/core/query/branch.py (1)
  • BranchNodeGetListQuery (153-160)
backend/infrahub/core/node/standard.py (2)
backend/infrahub/constants/enums.py (2)
  • OrderByField (9-12)
  • OrderDirection (4-6)
backend/infrahub/core/query/standard_node.py (1)
  • StandardNodeGetListQuery (133-192)
backend/infrahub/graphql/manager.py (1)
backend/infrahub/graphql/types/metadata.py (1)
  • OrderInput (13-15)
backend/infrahub/core/query/standard_node.py (2)
backend/infrahub/constants/enums.py (1)
  • OrderByField (9-12)
backend/infrahub/core/node/standard.py (2)
  • StandardNode (47-315)
  • StandardNodeOrdering (36-38)
backend/infrahub/graphql/models.py (1)
backend/infrahub/constants/enums.py (1)
  • OrderDirection (4-6)
backend/infrahub/graphql/resolvers/ipam.py (1)
backend/infrahub/graphql/models.py (2)
  • OrderModel (19-36)
  • from_input (25-32)
backend/tests/unit/graphql/queries/test_branch.py (4)
backend/infrahub/core/branch/models.py (1)
  • Branch (31-569)
backend/tests/unit/conftest.py (1)
  • session_admin (2524-2526)
backend/infrahub/graphql/initialization.py (1)
  • prepare_graphql_params (89-136)
backend/tests/helpers/graphql.py (1)
  • graphql (26-61)
backend/infrahub/graphql/queries/branch.py (4)
backend/infrahub/constants/enums.py (2)
  • OrderByField (9-12)
  • OrderDirection (4-6)
backend/infrahub/core/node/standard.py (1)
  • StandardNodeOrdering (36-38)
backend/infrahub/graphql/field_extractor.py (1)
  • extract_graphql_fields (67-69)
backend/infrahub/graphql/types/metadata.py (1)
  • OrderInput (13-15)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (5)
  • GitHub Check: graphql-schema
  • GitHub Check: validate-generated-documentation
  • GitHub Check: backend-tests-integration
  • GitHub Check: json-schema
  • GitHub Check: Cloudflare Pages
🔇 Additional comments (14)
backend/infrahub/graphql/manager.py (1)

24-24: LGTM! Centralizing OrderInput definition.

Moving the OrderInput definition to a shared metadata module eliminates duplication and improves maintainability.

backend/tests/unit/graphql/queries/test_branch.py (3)

601-668: LGTM! Proper test isolation.

This test correctly filters results to only the branches created within the test (lines 660-661), avoiding dependencies on other tests. The approach of updating branches in a specific order and then verifying the ordering matches expectations is well-designed.


669-738: LGTM! Proper test isolation with descending order.

This test correctly filters results to only test-created branches (lines 729-730) and properly verifies descending order. The pattern matches the ascending test and maintains good isolation.


740-766: LGTM! Important validation test.

This test properly validates the mutual exclusion constraint between created_at and updated_at ordering fields, ensuring the API returns an appropriate error when both are specified.

backend/infrahub/core/node/standard.py (1)

306-311: LGTM! Clean ordering integration.

The addition of the node_ordering parameter with proper defaulting and propagation to the query layer is well-implemented. The pattern maintains backward compatibility while enabling ordering functionality.

schema/schema.graphql (2)

9788-9798: LGTM!

The OrderDirection enum and OrderInput extension are well-structured. The addition of node_metadata aligns with the PR objective to support metadata-based ordering.

Minor note: The docstring says "branch metadata" but the field is named node_metadata. If this ordering is intended to apply to other node types in the future (as mentioned in the PR), consider generalizing the docstring to "Order settings for node metadata" for consistency.


11137-11145: LGTM!

The addition of the order parameter to InfrahubBranch query is clean and follows the established pattern. The docstring clearly describes its purpose for ordering branch query results.

backend/infrahub/core/branch/models.py (1)

159-171: LGTM! Clean integration of ordering into Branch.get_list.

The optional node_ordering parameter with a sensible default follows the existing pattern and correctly propagates to BranchNodeGetListQuery. The defaulting logic at line 162 ensures a valid StandardNodeOrdering is always passed downstream.

backend/infrahub/graphql/queries/branch.py (3)

19-46: Well-structured ordering conversion with proper validation.

The function follows coding guidelines with a Google-style docstring and proper type hints. The mutual exclusion validation aligns with the PR objective of forbidding combined created_at/updated_at ordering.


133-137: LGTM! GraphQL order argument properly exposed.

The order argument is correctly defined with OrderInput type and appropriate description for the public API.


107-113: Remove this comment - node_ordering is explicitly used.

The node_ordering parameter is not silently ignored. It's explicitly defined in StandardNodeGetListQuery.__init__ (line 141 in backend/infrahub/core/query/standard_node.py) and actively used in the query_init method to set the ORDER BY clause based on the specified ordering field.

Likely an incorrect or invalid review comment.

backend/infrahub/core/query/standard_node.py (1)

138-153: LGTM! Clean integration of node_ordering parameter.

The node_ordering parameter is properly typed, stored on the instance, and the assert_never usage ensures exhaustive handling of ordering fields.

backend/infrahub/graphql/resolvers/ipam.py (1)

312-325: Pattern change looks correct, but depends on OrderModel.from_input fix.

The change from accepting OrderModel directly to accepting dict[str, Any] and converting via from_input is appropriate for handling raw GraphQL input. However, this will fail at runtime when node_metadata is provided until the nested deserialization issue in OrderModel.from_input is resolved.

backend/infrahub/graphql/models.py (1)

8-9: No runtime issues exist with this TYPE_CHECKING import.

With from __future__ import annotations at the top of the file, type hints are no longer interpreted at runtime, so there's no NameError. When from __future__ import annotations is used, imports only needed for type annotations can safely be placed inside if TYPE_CHECKING:.

Since the file already includes from __future__ import annotations on line 1, the OrderDirection import under TYPE_CHECKING is correct and follows best practices. The annotations in NodeMetaOrder are stored as strings and never evaluated by the dataclass at runtime, preventing any import errors.

Likely an incorrect or invalid review comment.

self.order_by = [f"{db.get_id_function_name()}(n)"]
match self.node_ordering.order_by:
case OrderByField.ID:
self.order_by = [f"{db.get_id_function_name()}(n)"]

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I missed it was already the default behavior
this is fine then

@ogenstad ogenstad merged commit fff49ae into develop Dec 18, 2025
66 of 68 checks passed
@ogenstad ogenstad deleted the pog-graphql-sort-IFC-1941 branch December 18, 2025 15:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

group/backend Issue related to the backend (API Server, Git Agent)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants