Skip to content
Open
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .devcontainer/onCreateCommand.sh
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,4 @@ poetry install --no-interaction --no-ansi

git submodule update --init

invoke demo.pull
poetry run invoke demo.pull
2 changes: 1 addition & 1 deletion .devcontainer/postCreateCommand.sh
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@

git pull
git submodule update
invoke demo.start --wait
poetry run invoke demo.start --wait
8 changes: 4 additions & 4 deletions .devcontainer/updateContentCommand.sh
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
#!/bin/bash

export WEB_CONCURRENCY=2
invoke demo.start
poetry run invoke demo.start
sleep 120
docker logs infrahub-server-1
invoke demo.load-infra-schema
poetry run invoke demo.load-infra-schema
docker logs infrahub-server-1
sleep 90
docker logs infrahub-server-1
invoke demo.load-infra-data
invoke demo.stop
poetry run invoke demo.load-infra-data
poetry run invoke demo.stop
16 changes: 16 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,22 @@ This project uses [*towncrier*](https://towncrier.readthedocs.io/) and the chang

<!-- towncrier release notes start -->

## [Infrahub - v1.4.12](https://github.com/opsmill/infrahub/tree/infrahub-v1.4.12) - 2025-10-23

### Added

- - Schema Visualizer now displays `on_delete` settings for relationships
- Fixed display of common_parent settings in relationships.

([#7431](https://github.com/opsmill/infrahub/issues/7431))

### Fixed

- Loosen requirements for upsert mutations in the GraphQL schema so that required fields can be supplied by a template. ([#7398](https://github.com/opsmill/infrahub/issues/7398))
- Fix a bug that could cause duplicated attributes to be created when updating a generic schema with a new attribute. Includes a migration to fix any existing duplicated attributes created by this bug. ([#7407](https://github.com/opsmill/infrahub/issues/7407))
- Fix bug in logic to create an object from a template that would prevent existing objects in relationships of sub-templates from being correctly linked to the created object. ([#7430](https://github.com/opsmill/infrahub/issues/7430))
- The artifact count has been removed from the Proposed Changes list view.

## [Infrahub - v1.4.11](https://github.com/opsmill/infrahub/tree/infrahub-v1.4.11) - 2025-10-17

### Added
Expand Down
21 changes: 16 additions & 5 deletions backend/infrahub/core/node/create.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,14 +57,25 @@ async def extract_peer_data(

for rel in template_peer.get_schema().relationship_names:
rel_manager: RelationshipManager = getattr(template_peer, rel)
if (
rel_manager.schema.kind not in [RelationshipKind.COMPONENT, RelationshipKind.PARENT]
or rel_manager.schema.name not in obj_peer_schema.relationship_names
):

if rel_manager.schema.name not in obj_peer_schema.relationship_names:
continue

if list(await rel_manager.get_peers(db=db)) == [current_template.id]:
peers_map = await rel_manager.get_peers(db=db)
if rel_manager.schema.kind in [RelationshipKind.COMPONENT, RelationshipKind.PARENT] and list(
peers_map.keys()
) == [current_template.id]:
obj_peer_data[rel] = {"id": parent_obj.id}
continue

rel_peer_ids = []
for peer_id, peer_object in peers_map.items():
# deeper templates are handled in the next level of recursion
if peer_object.get_schema().is_template_schema:
continue
rel_peer_ids.append({"id": peer_id})

obj_peer_data[rel] = rel_peer_ids

return obj_peer_data

Expand Down
12 changes: 3 additions & 9 deletions backend/infrahub/graphql/manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -787,25 +787,19 @@ 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:
continue

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)

Expand Down
1 change: 1 addition & 0 deletions backend/tests/helpers/schema/tshirt.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
98 changes: 96 additions & 2 deletions backend/tests/unit/graphql/test_mutation_create.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from infrahub import config
from infrahub.core import registry
from infrahub.core.branch.models import Branch
from infrahub.core.constants import InfrahubKind, SchemaPathType
from infrahub.core.constants import InfrahubKind, RelationshipKind, SchemaPathType
from infrahub.core.initialization import create_branch
from infrahub.core.manager import NodeManager
from infrahub.core.migrations.schema.node_kind_update import NodeKindUpdateMigration
Expand All @@ -17,7 +17,7 @@
from infrahub.graphql.initialization import prepare_graphql_params
from tests.constants import TestKind
from tests.helpers.graphql import graphql
from tests.helpers.schema import DEVICE_SCHEMA
from tests.helpers.schema import CAR_SCHEMA, DEVICE_SCHEMA


async def test_create_simple_object(db: InfrahubDatabase, default_branch, car_person_schema):
Expand Down Expand Up @@ -1462,6 +1462,100 @@ async def test_create_with_object_template(
assert sfp.part_number.source_id is None


async def test_create_with_object_template_and_real_object(
db: InfrahubDatabase, default_branch: Branch, register_core_models_schema: SchemaBranch, branch: Branch
):
"""
Test that relationships on sub-templates will correctly link the created sub-object to an existing object on non-component relationships
"""
updated_car_schema = CAR_SCHEMA.duplicate()
manufacturer_schema = updated_car_schema.get(name=TestKind.MANUFACTURER)
manufacturer_schema.generate_template = True
cars_rel = manufacturer_schema.get_relationship(name="cars")
cars_rel.kind = RelationshipKind.COMPONENT
person_schema = updated_car_schema.get(name=TestKind.PERSON)
person_schema.generate_template = True
car_schema = updated_car_schema.get(name=TestKind.CAR)
car_schema.generate_template = True
manufacturer_rel = car_schema.get_relationship(name="manufacturer")
manufacturer_rel.kind = RelationshipKind.PARENT
registry.schema.register_schema(schema=updated_car_schema, branch=branch.name)

manufacturer_object = await Node.init(schema=TestKind.MANUFACTURER, db=db, branch=branch)
await manufacturer_object.new(db=db, name="Hark Motors")
await manufacturer_object.save(db=db)

person_object = await Node.init(schema=TestKind.PERSON, db=db, branch=branch)
await person_object.new(db=db, name="John", height=180)
await person_object.save(db=db)

car_object = await Node.init(schema=TestKind.CAR, db=db, branch=branch)
await car_object.new(db=db, name="Accord", manufacturer=manufacturer_object, owner=person_object, color="blurple")
await car_object.save(db=db)

manufacturer_template: Node = await Node.init(schema=f"Template{TestKind.MANUFACTURER}", db=db, branch=branch)
await manufacturer_template.new(db=db, template_name="m_template", customers=[person_object])
await manufacturer_template.save(db=db)

car_template_with_person_object = await Node.init(schema=f"Template{TestKind.CAR}", db=db, branch=branch)
await car_template_with_person_object.new(
db=db,
template_name="c_template",
name="Civic",
color="blurple",
manufacturer=manufacturer_template,
owner=person_object,
)
await car_template_with_person_object.save(db=db)

create_manufacturer_with_template_query = """
mutation CreateManufacturerWithTemplate($manufacturer_name: String!, $template_id: String!) {
TestingManufacturerCreate(data: {
name: {value: $manufacturer_name}
object_template: {id: $template_id}
}) {
ok
object {
id
}
}
}
"""
gql_params = await prepare_graphql_params(db=db, branch=branch)
result = await graphql(
schema=gql_params.schema,
source=create_manufacturer_with_template_query,
context_value=gql_params.context,
variable_values={"manufacturer_name": "Fresh Motors", "template_id": manufacturer_template.id},
)
assert not result.errors
new_manufacturer = await NodeManager.get_one(
db=db,
kind=TestKind.MANUFACTURER,
branch=branch,
id=result.data[f"{TestKind.MANUFACTURER}Create"]["object"]["id"],
)
assert new_manufacturer
assert new_manufacturer.name.value == "Fresh Motors"
customers_peers = await new_manufacturer.customers.get_peers(db=db)
assert len(customers_peers) == 1
customers_by_name = {person.name.value: person for person in customers_peers.values()}
# check non-template person
non_template_person = customers_by_name["John"]
assert non_template_person.id == person_object.id

cars_peers = await new_manufacturer.cars.get_peers(db=db)
assert len(cars_peers) == 1
cars_by_name = {car.name.value: car for car in cars_peers.values()}
# check car template with person object
car_template_with_person_object = cars_by_name["Civic"]
assert car_template_with_person_object.color.value == "blurple"
car_manufacturer = await car_template_with_person_object.manufacturer.get_peer(db=db)
assert car_manufacturer.id == new_manufacturer.id
car_owner = await car_template_with_person_object.owner.get_peer(db=db)
assert car_owner.id == person_object.id


async def test_create_without_object_template(
db: InfrahubDatabase, default_branch: Branch, register_core_models_schema: SchemaBranch, branch: Branch
):
Expand Down
81 changes: 80 additions & 1 deletion backend/tests/unit/graphql/test_mutation_upsert.py
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand Down Expand Up @@ -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"
1 change: 0 additions & 1 deletion changelog/+artifact.fixed.md

This file was deleted.

1 change: 0 additions & 1 deletion changelog/7407.fixed.md

This file was deleted.

2 changes: 0 additions & 2 deletions changelog/7431.added.md

This file was deleted.

35 changes: 35 additions & 0 deletions docs/docs/release-notes/infrahub/release-1_4_12.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
---
title: Release 1.4.12
---
<table>
<tbody>
<tr>
<th>Release Number</th>
<td>1.4.12</td>
</tr>
<tr>
<th>Release Date</th>
<td>October 23rd, 2025</td>
</tr>
<tr>
<th>Tag</th>
<td>[infrahub-v1.4.12](https://github.com/opsmill/infrahub/releases/tag/infrahub-v1.4.12)</td>
</tr>
</tbody>
</table>

<!-- vale off -->
### Added

- - Schema Visualizer now displays `on_delete` settings for relationships
- Fixed display of common_parent settings in relationships.

([#7431](https://github.com/opsmill/infrahub/issues/7431))

### Fixed

- Loosen requirements for upsert mutations in the GraphQL schema so that required fields can be supplied by a template. ([#7398](https://github.com/opsmill/infrahub/issues/7398))
- Fix a bug that could cause duplicated attributes to be created when updating a generic schema with a new attribute. Includes a migration to fix any existing duplicated attributes created by this bug. ([#7407](https://github.com/opsmill/infrahub/issues/7407))
- Fix bug in logic to create an object from a template that would prevent existing objects in relationships of sub-templates from being correctly linked to the created object. ([#7430](https://github.com/opsmill/infrahub/issues/7430))
- The artifact count has been removed from the Proposed Changes list view.
<!-- vale on -->
1 change: 1 addition & 0 deletions docs/sidebars.ts
Original file line number Diff line number Diff line change
Expand Up @@ -414,6 +414,7 @@ const sidebars: SidebarsConfig = {
slug: 'release-notes/infrahub',
},
items: [
'release-notes/infrahub/release-1_4_12',
'release-notes/infrahub/release-1_4_11',
'release-notes/infrahub/release-1_4_10',
'release-notes/infrahub/release-1_4_9',
Expand Down
Loading
Loading