From 12069276e904190db36afc8e13b6974e822ecfb8 Mon Sep 17 00:00:00 2001 From: Patrick Ogenstad Date: Tue, 21 Oct 2025 13:34:32 +0200 Subject: [PATCH] Loosen requirements for Upsert mutations (for templates) --- backend/infrahub/graphql/manager.py | 12 +-- backend/tests/helpers/schema/tshirt.py | 1 + .../unit/graphql/test_mutation_upsert.py | 81 ++++++++++++++++++- changelog/7398.fixed.md | 1 + 4 files changed, 85 insertions(+), 10 deletions(-) create mode 100644 changelog/7398.fixed.md diff --git a/backend/infrahub/graphql/manager.py b/backend/infrahub/graphql/manager.py index 0544a2b323..b19f6ae42d 100644 --- a/backend/infrahub/graphql/manager.py +++ b/backend/infrahub/graphql/manager.py @@ -787,10 +787,7 @@ class StatusUpsertInput(InputObjectType): attr_kind = get_attr_kind(schema, attr) attr_type = get_attribute_type(kind=attr_kind).get_graphql_update() - # A Field is not required if explicitly indicated or if a default value has been provided - required = not attr.optional if not attr.default_value else False - - attrs[attr.name] = graphene.InputField(attr_type, required=required, description=attr.description) + attrs[attr.name] = graphene.InputField(attr_type, description=attr.description) for rel in schema.relationships: if rel.internal_peer or rel.read_only: @@ -798,14 +795,11 @@ class StatusUpsertInput(InputObjectType): input_type = self._get_related_input_type(relationship=rel) - required = not rel.optional if rel.cardinality == RelationshipCardinality.ONE: - attrs[rel.name] = graphene.InputField(input_type, required=required, description=rel.description) + attrs[rel.name] = graphene.InputField(input_type, description=rel.description) elif rel.cardinality == RelationshipCardinality.MANY: - attrs[rel.name] = graphene.InputField( - graphene.List(input_type), required=required, description=rel.description - ) + attrs[rel.name] = graphene.InputField(graphene.List(input_type), description=rel.description) return type(f"{schema.kind}UpsertInput", (graphene.InputObjectType,), attrs) diff --git a/backend/tests/helpers/schema/tshirt.py b/backend/tests/helpers/schema/tshirt.py index 05fc2c1afc..3b184b685a 100644 --- a/backend/tests/helpers/schema/tshirt.py +++ b/backend/tests/helpers/schema/tshirt.py @@ -11,6 +11,7 @@ default_filter="name__value", display_labels=["name__value"], uniqueness_constraints=[["name__value"]], + generate_template=True, attributes=[ AttributeSchema(name="name", kind="Text"), AttributeSchema( diff --git a/backend/tests/unit/graphql/test_mutation_upsert.py b/backend/tests/unit/graphql/test_mutation_upsert.py index 0c56781100..6047af9750 100644 --- a/backend/tests/unit/graphql/test_mutation_upsert.py +++ b/backend/tests/unit/graphql/test_mutation_upsert.py @@ -12,7 +12,7 @@ from tests.adapters.event import MemoryInfrahubEvent from tests.constants import TestKind from tests.helpers.graphql import graphql -from tests.helpers.schema import TICKET +from tests.helpers.schema import COLOR, TICKET, TSHIRT from tests.node_creation import create_and_save @@ -673,3 +673,82 @@ async def test_upsert_node_on_branch_with_hfid_on_default(db: InfrahubDatabase, in result.errors[0].message ) assert f"Please rebase this branch to access {person.id} / TestPerson" in result.errors[0].message + + +async def test_upsert_with_required_relationship_from_template( + db: InfrahubDatabase, default_branch: Branch, register_core_models_schema: None +) -> None: + """Validate that we can use a template to populate required relationships in upsert mutations. + + Steps: + - Create a color node and a Tshirt template node. + - Try to upsert a Tshirt without specifying color or template (should fail). + - Upsert a Tshirt specifying the template (should succeed and apply the color from the template). + """ + registry.schema.register_schema(schema=SchemaRoot(nodes=[TSHIRT, COLOR]), branch=default_branch.name) + + # Create a color node + color_node = await Node.init(db=db, schema="TestingColor", branch=default_branch) + await color_node.new(db=db, name="Red", description="Bright Red Color") + await color_node.save(db=db) + + # Create a Tshirt template node with the color relationship set + template_node = await Node.init(db=db, schema="TemplateTestingTShirt", branch=default_branch) + await template_node.new(db=db, template_name="Basic Red Tshirt", color=color_node) + await template_node.save(db=db) + + # Try to upsert a TShirt without specifying color or template (should fail) + query_missing_required = """ + mutation { + TestingTShirtUpsert(data: {name: {value: "My Shirt"} }) { + ok + object { + id + name { value } + color { node { id name { value } } } + } + } + } + """ + gql_params = await prepare_graphql_params(db=db, include_subscription=False, branch=default_branch) + result_missing = await graphql( + schema=gql_params.schema, + source=query_missing_required, + context_value=gql_params.context, + root_value=None, + variable_values={}, + ) + assert result_missing.errors + assert "color is mandatory for TestingTShirt at color" in str(result_missing.errors) + + # Upsert a Tshirt specifying the template (should succeed and apply the color from the template) + query_with_template = """ + mutation UpsertTShirt($template_id: String!) { + TestingTShirtUpsert(data: { + name: {value: "My Tshirt"}, + object_template: {id: $template_id} + }) { + ok + object { + id + name { value } + color { node { id name { value } } } + } + } + } + """ + + result_with_template = await graphql( + schema=gql_params.schema, + source=query_with_template, + context_value=gql_params.context, + root_value=None, + variable_values={"template_id": template_node.id}, + ) + assert result_with_template.errors is None + assert result_with_template.data + assert result_with_template.data["TestingTShirtUpsert"]["ok"] is True + tshirt_obj = result_with_template.data["TestingTShirtUpsert"]["object"] + assert tshirt_obj["name"]["value"] == "My Tshirt" + assert tshirt_obj["color"]["node"]["id"] == color_node.id + assert tshirt_obj["color"]["node"]["name"]["value"] == "Red" diff --git a/changelog/7398.fixed.md b/changelog/7398.fixed.md new file mode 100644 index 0000000000..f6ca170334 --- /dev/null +++ b/changelog/7398.fixed.md @@ -0,0 +1 @@ +Loosen requirements for upsert mutations in the GraphQL schema so that required fields can be supplied by a template.