diff --git a/.changes/unreleased/component-type-category.yaml b/.changes/unreleased/component-type-category.yaml new file mode 100644 index 00000000..1f6c3145 --- /dev/null +++ b/.changes/unreleased/component-type-category.yaml @@ -0,0 +1,3 @@ +kind: Feature +body: Add `Category` to `ComponentTypeInput` and `ComponentType`, so component types can be filed under a catalog category (for example `infrastructure`) on create, update and read +time: 2026-09-16T07:00:00.000000000-05:00 diff --git a/component_test.go b/component_test.go index 3a0da9e9..30834a9c 100644 --- a/component_test.go +++ b/component_test.go @@ -12,6 +12,7 @@ func TestComponentTypeCreate(t *testing.T) { input := autopilot.Register[ol.ComponentTypeInput]("component_type_create_input", ol.ComponentTypeInput{ Alias: ol.RefOf("example"), + Category: ol.RefOf("infrastructure"), Name: ol.RefOf("Example"), Description: ol.RefOf("Example Description"), Properties: &[]ol.ComponentTypePropertyDefinitionInput{}, @@ -31,7 +32,7 @@ func TestComponentTypeCreate(t *testing.T) { testRequest := autopilot.NewTestRequest( `mutation ComponentTypeCreate($input:ComponentTypeInput!){componentTypeCreate(input:$input){componentType{{ template "component_type_graphql" }},errors{message,path}}}`, - `{"input": {"alias": "example", "name": "Example", "description": "Example Description", "properties": [], "ownerRelationship": {"managementRules": [{"operator": "EQUALS", "sourceProperty": "tag_key_eq:owner", "sourcePropertyBuiltin": true, "targetProperty": "name", "targetPropertyBuiltin": true, "targetType": "team"}]} }}`, + `{"input": {"alias": "example", "category": "infrastructure", "name": "Example", "description": "Example Description", "properties": [], "ownerRelationship": {"managementRules": [{"operator": "EQUALS", "sourceProperty": "tag_key_eq:owner", "sourcePropertyBuiltin": true, "targetProperty": "name", "targetPropertyBuiltin": true, "targetType": "team"}]} }}`, `{"data": {"componentTypeCreate": {"componentType": {{ template "component_type_1_response" }} }}}`, ) @@ -41,6 +42,32 @@ func TestComponentTypeCreate(t *testing.T) { // Assert autopilot.Ok(t, err) autopilot.Equals(t, id1, result.Id) + autopilot.Equals(t, "infrastructure", result.Category) +} + +// A component type input without a category must omit the field entirely, so that +// accounts where the backend does not expose `category` are unaffected. +func TestComponentTypeCreateWithoutCategory(t *testing.T) { + // Arrange + input := autopilot.Register[ol.ComponentTypeInput]("component_type_create_input_no_category", + ol.ComponentTypeInput{ + Alias: ol.RefOf("example"), + Name: ol.RefOf("Example"), + Properties: &[]ol.ComponentTypePropertyDefinitionInput{}, + }) + + testRequest := autopilot.NewTestRequest( + `mutation ComponentTypeCreate($input:ComponentTypeInput!){componentTypeCreate(input:$input){componentType{{ template "component_type_graphql" }},errors{message,path}}}`, + `{"input": {"alias": "example", "name": "Example", "properties": [] }}`, + `{"data": {"componentTypeCreate": {"componentType": {{ template "component_type_2_response" }} }}}`, + ) + + client := BestTestClient(t, "ComponentType/create_without_category", testRequest) + // Act + result, err := client.CreateComponentType(input) + // Assert + autopilot.Ok(t, err) + autopilot.Equals(t, id2, result.Id) } func TestComponentTypeGet(t *testing.T) { @@ -57,6 +84,7 @@ func TestComponentTypeGet(t *testing.T) { // Assert autopilot.Ok(t, err) autopilot.Equals(t, id1, result.Id) + autopilot.Equals(t, "infrastructure", result.Category) } func TestComponentTypeList(t *testing.T) { @@ -83,6 +111,9 @@ func TestComponentTypeList(t *testing.T) { autopilot.Equals(t, "Example1", result[0].Name) autopilot.Equals(t, "Example2", result[1].Name) autopilot.Equals(t, "Example3", result[2].Name) + autopilot.Equals(t, "infrastructure", result[0].Category) + autopilot.Equals(t, "default", result[1].Category) + autopilot.Equals(t, "default", result[2].Category) } func TestComponentTypeUpdate(t *testing.T) { @@ -90,6 +121,7 @@ func TestComponentTypeUpdate(t *testing.T) { input := autopilot.Register[ol.ComponentTypeInput]("component_type_update_input", ol.ComponentTypeInput{ Alias: ol.RefOf("example"), + Category: ol.RefOf("infrastructure"), Name: ol.RefOf("Example"), Description: ol.RefOf("Example Description"), Properties: &[]ol.ComponentTypePropertyDefinitionInput{}, @@ -109,7 +141,7 @@ func TestComponentTypeUpdate(t *testing.T) { testRequest := autopilot.NewTestRequest( `mutation ComponentTypeUpdate($input:ComponentTypeInput!$target:IdentifierInput!){componentTypeUpdate(componentType:$target,input:$input){componentType{{ template "component_type_graphql" }},errors{message,path}}}`, - `{"input": {"alias": "example", "name": "Example", "description": "Example Description", "properties": [], "ownerRelationship": {"managementRules": [{"operator": "EQUALS", "sourceProperty": "tag_key_eq:owner", "sourcePropertyBuiltin": true, "targetProperty": "name", "targetPropertyBuiltin": true, "targetType": "team"}]}}, "target": { {{ template "id1" }} }}`, + `{"input": {"alias": "example", "category": "infrastructure", "name": "Example", "description": "Example Description", "properties": [], "ownerRelationship": {"managementRules": [{"operator": "EQUALS", "sourceProperty": "tag_key_eq:owner", "sourcePropertyBuiltin": true, "targetProperty": "name", "targetPropertyBuiltin": true, "targetType": "team"}]}}, "target": { {{ template "id1" }} }}`, `{"data": {"componentTypeUpdate": {"componentType": {{ template "component_type_1_response" }} }}}`, ) @@ -119,6 +151,31 @@ func TestComponentTypeUpdate(t *testing.T) { // Assert autopilot.Ok(t, err) autopilot.Equals(t, id1, result.Id) + autopilot.Equals(t, "infrastructure", result.Category) +} + +// Patching only the category must leave the rest of the component type untouched, which +// is how existing types get adopted into the infrastructure catalog without a recreate. +func TestComponentTypeUpdateCategoryOnly(t *testing.T) { + // Arrange + input := autopilot.Register[ol.ComponentTypeInput]("component_type_update_category_only_input", + ol.ComponentTypeInput{ + Category: ol.RefOf("infrastructure"), + }) + + testRequest := autopilot.NewTestRequest( + `mutation ComponentTypeUpdate($input:ComponentTypeInput!$target:IdentifierInput!){componentTypeUpdate(componentType:$target,input:$input){componentType{{ template "component_type_graphql" }},errors{message,path}}}`, + `{"input": {"category": "infrastructure"}, "target": { {{ template "id1" }} }}`, + `{"data": {"componentTypeUpdate": {"componentType": {{ template "component_type_1_response" }} }}}`, + ) + + client := BestTestClient(t, "ComponentType/update_category_only", testRequest) + // Act + result, err := client.UpdateComponentType(string(id1), input) + // Assert + autopilot.Ok(t, err) + autopilot.Equals(t, id1, result.Id) + autopilot.Equals(t, "infrastructure", result.Category) } func TestComponentTypeDelete(t *testing.T) { diff --git a/input.go b/input.go index 66f9a9c6..0250a014 100644 --- a/input.go +++ b/input.go @@ -774,6 +774,7 @@ type ComponentTypeIconInput struct { // ComponentTypeInput Specifies the input fields used to create a component type type ComponentTypeInput struct { Alias *Nullable[string] `json:"alias,omitempty" yaml:"alias,omitempty" example:"example_value"` // The unique alias of the component type (Optional) + Category *Nullable[string] `json:"category,omitempty" yaml:"category,omitempty" example:"infrastructure"` // The catalog category the component type is filed under, e.g. `default` or `infrastructure` (Optional) Description *Nullable[string] `json:"description,omitempty" yaml:"description,omitempty" example:"example_value"` // The description of the component type (Optional) Icon *ComponentTypeIconInput `json:"icon,omitempty" yaml:"icon,omitempty"` // The icon associated with the component type (Optional) Name *Nullable[string] `json:"name,omitempty" yaml:"name,omitempty" example:"example_value"` // The unique name of the component type (Optional) diff --git a/object.go b/object.go index 4d79a30b..e12a11fb 100644 --- a/object.go +++ b/object.go @@ -150,6 +150,7 @@ type ComponentTypeId struct { // ComponentType Information about a particular component type type ComponentType struct { ComponentTypeId + Category string // The catalog category the component type is filed under, e.g. `default` or `infrastructure` (Optional) Description string // The description of the component type (Optional) Href string // The relative path to link to the component type (Required) Icon ComponentTypeIcon // The icon associated with the component type (Required) diff --git a/testdata/templates/component_type.tpl b/testdata/templates/component_type.tpl index 445cbefb..e4f42260 100644 --- a/testdata/templates/component_type.tpl +++ b/testdata/templates/component_type.tpl @@ -1,5 +1,5 @@ {{- define "component_type_graphql" }} -{id,aliases,description,href,icon{color,name},isDefault,name,ownerRelationship{managementRules{operator,sourceProperty,sourcePropertyBuiltin,targetCategory,targetProperty,targetPropertyBuiltin,targetType}},systemRelationship{managementRules{operator,sourceProperty,sourcePropertyBuiltin,targetCategory,targetProperty,targetPropertyBuiltin,targetType}},timestamps{createdAt,updatedAt}} +{id,aliases,category,description,href,icon{color,name},isDefault,name,ownerRelationship{managementRules{operator,sourceProperty,sourcePropertyBuiltin,targetCategory,targetProperty,targetPropertyBuiltin,targetType}},systemRelationship{managementRules{operator,sourceProperty,sourcePropertyBuiltin,targetCategory,targetProperty,targetPropertyBuiltin,targetType}},timestamps{createdAt,updatedAt}} {{end}} {{- define "component_type_1_response" }} { @@ -8,6 +8,7 @@ "example1" ], "name": "Example1", + "category": "infrastructure", "description": "Description", "href": "https://app.opslevel-staging.com/catalog/domains/platformdomain", "icon": { @@ -49,6 +50,7 @@ "example2" ], "name": "Example2", + "category": "default", "description": "Description", "href": "https://app.opslevel-staging.com/catalog/domains/platformdomain", "icon": { @@ -64,6 +66,7 @@ "example3" ], "name": "Example3", + "category": "default", "description": "Description", "href": "https://app.opslevel-staging.com/catalog/domains/platformdomain", "icon": {