diff --git a/.changes/unreleased/deprecate-client-get-property.yaml b/.changes/unreleased/deprecate-client-get-property.yaml new file mode 100644 index 00000000..04262b48 --- /dev/null +++ b/.changes/unreleased/deprecate-client-get-property.yaml @@ -0,0 +1,2 @@ +kind: Deprecated +body: '`Client.GetProperty` only resolves service owners and will fail for team identifiers. Use `Service.GetProperty` or `Team.GetProperty` instead.' diff --git a/.changes/unreleased/property-owner-type-change.yaml b/.changes/unreleased/property-owner-type-change.yaml new file mode 100644 index 00000000..1bb82842 --- /dev/null +++ b/.changes/unreleased/property-owner-type-change.yaml @@ -0,0 +1,2 @@ +kind: Removed +body: '[Breaking change] `Property.Owner` type changed from `EntityOwnerService` to `PropertyOwner` to support both service and team owners. Direct field access (e.g. `property.Owner.Aliases`) must be updated to go through the embedded type (e.g. `property.Owner.ServiceId.Aliases`).' diff --git a/.changes/unreleased/team-property-definitions.yaml b/.changes/unreleased/team-property-definitions.yaml new file mode 100644 index 00000000..ea214d1d --- /dev/null +++ b/.changes/unreleased/team-property-definitions.yaml @@ -0,0 +1,2 @@ +kind: Feature +body: Add CRUD operations for team property definitions (`CreateTeamPropertyDefinition`, `UpdateTeamPropertyDefinition`, `GetTeamPropertyDefinition`, `ListTeamPropertyDefinitions`, `AssignTeamPropertyDefinitions`) and entity-scoped property lookup methods (`Team.GetProperty`, `Service.GetProperty`) diff --git a/owner.go b/owner.go index 050db146..2b48c261 100644 --- a/owner.go +++ b/owner.go @@ -28,3 +28,19 @@ func (entityOwnerService *EntityOwnerService) Aliases() []string { func (entityOwnerService *EntityOwnerService) Id() ID { return entityOwnerService.OnService.Id } + +type PropertyOwner struct { + Typename string `graphql:"__typename"` + *TeamId `graphql:"... on Team"` + *ServiceId `graphql:"... on Service"` +} + +func (o PropertyOwner) Id() ID { + if o.ServiceId != nil { + return o.ServiceId.Id + } + if o.TeamId != nil { + return o.TeamId.Id + } + return "" +} diff --git a/payload.go b/payload.go index 658d3c4a..e74b8408 100644 --- a/payload.go +++ b/payload.go @@ -330,6 +330,12 @@ type TeamPropertyDefinitionPayload struct { BasePayload } +// TeamPropertyDefinitionsAssignPayload The return type for the teamPropertyDefinitionsAssign mutation +type TeamPropertyDefinitionsAssignPayload struct { + Properties TeamPropertyDefinitionConnection // The property definitions that were assigned (Optional) + BasePayload +} + // TeamUpdatePayload The return type of a `teamUpdate` mutation type TeamUpdatePayload struct { Team Team // A team belongs to your organization. Teams can own multiple services (Optional) diff --git a/property.go b/property.go index eef61e15..24a2a2cb 100644 --- a/property.go +++ b/property.go @@ -25,12 +25,12 @@ type PropertyDefinitionId struct { type Property struct { Definition PropertyDefinitionId `graphql:"definition"` Locked bool `graphql:"locked"` - Owner EntityOwnerService `graphql:"owner"` + Owner PropertyOwner `graphql:"owner"` ValidationErrors []Error `graphql:"validationErrors"` Value *JsonString `graphql:"value"` } -type ServicePropertiesConnection struct { +type PropertiesConnection struct { Nodes []Property PageInfo PageInfo TotalCount int `graphql:"-"` @@ -111,6 +111,86 @@ func (client *Client) DeletePropertyDefinition(input string) error { return HandleErrors(err, m.Payload.Errors) } +func (client *Client) CreateTeamPropertyDefinition(input TeamPropertyDefinitionInput) (*TeamPropertyDefinition, error) { + var m struct { + Payload TeamPropertyDefinitionPayload `graphql:"teamPropertyDefinitionCreate(input: $input)"` + } + v := PayloadVariables{ + "input": input, + } + err := client.Mutate(&m, v, WithName("TeamPropertyDefinitionCreate")) + return &m.Payload.Definition, HandleErrors(err, m.Payload.Errors) +} + +func (client *Client) UpdateTeamPropertyDefinition(identifier string, input TeamPropertyDefinitionInput) (*TeamPropertyDefinition, error) { + var m struct { + Payload TeamPropertyDefinitionPayload `graphql:"teamPropertyDefinitionUpdate(propertyDefinition: $propertyDefinition, input: $input)"` + } + v := PayloadVariables{ + "propertyDefinition": *NewIdentifier(identifier), + "input": input, + } + err := client.Mutate(&m, v, WithName("TeamPropertyDefinitionUpdate")) + return &m.Payload.Definition, HandleErrors(err, m.Payload.Errors) +} + +func (client *Client) GetTeamPropertyDefinition(identifier string) (*TeamPropertyDefinition, error) { + var q struct { + Account struct { + Definition TeamPropertyDefinition `graphql:"teamPropertyDefinition(input: $input)"` + } + } + v := PayloadVariables{ + "input": *NewIdentifier(identifier), + } + err := client.Query(&q, v, WithName("TeamPropertyDefinitionGet")) + if q.Account.Definition.Id == "" { + err = fmt.Errorf("TeamPropertyDefinition with ID or Alias matching '%s' not found", identifier) + } + return &q.Account.Definition, HandleErrors(err, nil) +} + +func (client *Client) ListTeamPropertyDefinitions(variables *PayloadVariables) (*TeamPropertyDefinitionConnection, error) { + var q struct { + Account struct { + Definitions TeamPropertyDefinitionConnection `graphql:"teamPropertyDefinitions(after: $after, first: $first)"` + } + } + if variables == nil { + variables = client.InitialPageVariablesPointer() + } + if err := client.Query(&q, *variables, WithName("TeamPropertyDefinitionList")); err != nil { + return nil, err + } + q.Account.Definitions.TotalCount = len(q.Account.Definitions.Nodes) + if q.Account.Definitions.PageInfo.HasNextPage { + (*variables)["after"] = q.Account.Definitions.PageInfo.End + resp, err := client.ListTeamPropertyDefinitions(variables) + if err != nil { + return nil, err + } + q.Account.Definitions.Nodes = append(q.Account.Definitions.Nodes, resp.Nodes...) + q.Account.Definitions.PageInfo = resp.PageInfo + q.Account.Definitions.TotalCount += resp.TotalCount + } + return &q.Account.Definitions, nil +} + +func (client *Client) AssignTeamPropertyDefinitions(input TeamPropertyDefinitionsAssignInput) (*TeamPropertyDefinitionConnection, error) { + var m struct { + Payload TeamPropertyDefinitionsAssignPayload `graphql:"teamPropertyDefinitionsAssign(input: $input)"` + } + v := PayloadVariables{ + "input": input, + } + err := client.Mutate(&m, v, WithName("TeamPropertyDefinitionsAssign")) + m.Payload.Properties.TotalCount = len(m.Payload.Properties.Nodes) + return &m.Payload.Properties, HandleErrors(err, m.Payload.Errors) +} + +// Deprecated: Use [Service.GetProperty] or [Team.GetProperty] instead. +// This method only resolves service owners. Passing a team identifier will +// return an error from the API. func (client *Client) GetProperty(owner string, definition string) (*Property, error) { var q struct { Account struct { @@ -125,6 +205,25 @@ func (client *Client) GetProperty(owner string, definition string) (*Property, e return &q.Account.Property, HandleErrors(err, nil) } +func (service *Service) GetProperty(client *Client, definition string) (*Property, error) { + var q struct { + Account struct { + Service struct { + Property Property `graphql:"property(definition: $definition)"` + } `graphql:"service(id: $service)"` + } + } + if service.Id == "" { + return nil, fmt.Errorf("unable to get property, invalid Service id: '%s'", service.Id) + } + v := PayloadVariables{ + "service": service.Id, + "definition": *NewIdentifier(definition), + } + err := client.Query(&q, v, WithName("ServicePropertyGet")) + return &q.Account.Service.Property, HandleErrors(err, nil) +} + func (client *Client) PropertyAssign(input PropertyInput) (*Property, error) { var m struct { Payload PropertyPayload `graphql:"propertyAssign(input: $input)"` @@ -148,11 +247,11 @@ func (client *Client) PropertyUnassign(owner string, definition string) error { return HandleErrors(err, m.Payload.Errors) } -func (service *Service) GetProperties(client *Client, variables *PayloadVariables) (*ServicePropertiesConnection, error) { +func (service *Service) GetProperties(client *Client, variables *PayloadVariables) (*PropertiesConnection, error) { var q struct { Account struct { Service struct { - Properties ServicePropertiesConnection `graphql:"properties(after: $after, first: $first)"` + Properties PropertiesConnection `graphql:"properties(after: $after, first: $first)"` } `graphql:"service(id: $service)"` } } @@ -168,17 +267,16 @@ func (service *Service) GetProperties(client *Client, variables *PayloadVariable return nil, err } if service.Properties == nil { - service.Properties = &ServicePropertiesConnection{} + service.Properties = &PropertiesConnection{} } service.Properties.Nodes = append(service.Properties.Nodes, q.Account.Service.Properties.Nodes...) service.Properties.PageInfo = q.Account.Service.Properties.PageInfo if service.Properties.PageInfo.HasNextPage { (*variables)["after"] = service.Properties.PageInfo.End - resp, err := service.GetProperties(client, variables) - if err != nil { + if _, err := service.GetProperties(client, variables); err != nil { return nil, err } - service.Properties.TotalCount += resp.TotalCount } + service.Properties.TotalCount = len(service.Properties.Nodes) return service.Properties, nil } diff --git a/property_test.go b/property_test.go index 95eaf7ae..4c3d9b6f 100644 --- a/property_test.go +++ b/property_test.go @@ -195,9 +195,9 @@ func TestListPropertyDefinitions(t *testing.T) { func TestGetProperty(t *testing.T) { // Arrange testRequest := autopilot.NewTestRequest( - `query PropertyGet($definition:IdentifierInput!$owner:IdentifierInput!){account{property(owner: $owner, definition: $definition){definition{id,aliases},locked,owner{... on Service{id,aliases}},validationErrors{message,path},value}}}`, + `query PropertyGet($definition:IdentifierInput!$owner:IdentifierInput!){account{property(owner: $owner, definition: $definition){definition{id,aliases},locked,owner{__typename,... on Team{alias,id},... on Service{id,aliases}},validationErrors{message,path},value}}}`, `{"owner":{"alias":"monolith"},"definition":{"alias":"is_beta_feature"}}`, - `{"data":{"account":{"property":{"definition":{"id":"{{ template "id2_string" }}"},"locked":true,"owner":{"id":"{{ template "id1_string" }}"},"validationErrors":[],"value":"true"}}}}`, + `{"data":{"account":{"property":{"definition":{"id":"{{ template "id2_string" }}"},"locked":true,"owner":{"__typename":"Service","id":"{{ template "id1_string" }}","aliases":[]},"validationErrors":[],"value":"true"}}}}`, ) client := BestTestClient(t, "properties/property_get", testRequest) @@ -216,9 +216,9 @@ func TestGetProperty(t *testing.T) { func TestGetPropertyHasErrors(t *testing.T) { // Arrange testRequest := autopilot.NewTestRequest( - `query PropertyGet($definition:IdentifierInput!$owner:IdentifierInput!){account{property(owner: $owner, definition: $definition){definition{id,aliases},locked,owner{... on Service{id,aliases}},validationErrors{message,path},value}}}`, + `query PropertyGet($definition:IdentifierInput!$owner:IdentifierInput!){account{property(owner: $owner, definition: $definition){definition{id,aliases},locked,owner{__typename,... on Team{alias,id},... on Service{id,aliases}},validationErrors{message,path},value}}}`, `{"owner":{"alias":"monolith"},"definition":{"alias":"dropdown"}}`, - `{"data":{"account":{"property":{"definition":{"id":"{{ template "id2_string" }}"},"locked":false,"owner":{"id":"{{ template "id1_string" }}"},"validationErrors":[{"message":"vmessage1","path":["vmp1","vmp2"]},{"message":"vmessage2","path":["vmp3"]}],"value":"\"orange\""}}}}`, + `{"data":{"account":{"property":{"definition":{"id":"{{ template "id2_string" }}"},"locked":false,"owner":{"__typename":"Service","id":"{{ template "id1_string" }}","aliases":[]},"validationErrors":[{"message":"vmessage1","path":["vmp1","vmp2"]},{"message":"vmessage2","path":["vmp3"]}],"value":"\"orange\""}}}}`, ) client := BestTestClient(t, "properties/property_get_has_errors", testRequest) @@ -246,9 +246,9 @@ func TestGetPropertyHasErrors(t *testing.T) { func TestGetPropertyHasNullValue(t *testing.T) { // Arrange testRequest := autopilot.NewTestRequest( - `query PropertyGet($definition:IdentifierInput!$owner:IdentifierInput!){account{property(owner: $owner, definition: $definition){definition{id,aliases},locked,owner{... on Service{id,aliases}},validationErrors{message,path},value}}}`, + `query PropertyGet($definition:IdentifierInput!$owner:IdentifierInput!){account{property(owner: $owner, definition: $definition){definition{id,aliases},locked,owner{__typename,... on Team{alias,id},... on Service{id,aliases}},validationErrors{message,path},value}}}`, `{"owner":{"alias":"monolith"},"definition":{"alias":"is_beta_feature"}}`, - `{"data":{"account":{"property":{"definition":{"id":"{{ template "id2_string" }}"},"locked":true,"owner":{"id":"{{ template "id1_string" }}"},"validationErrors":[],"value":null}}}}`, + `{"data":{"account":{"property":{"definition":{"id":"{{ template "id2_string" }}"},"locked":true,"owner":{"__typename":"Service","id":"{{ template "id1_string" }}","aliases":[]},"validationErrors":[],"value":null}}}}`, ) client := BestTestClient(t, "properties/property_get_has_null_value", testRequest) @@ -272,9 +272,9 @@ func TestAssignProperty(t *testing.T) { Value: "true", } testRequest := autopilot.NewTestRequest( - `mutation PropertyAssign($input:PropertyInput!){propertyAssign(input: $input){property{definition{id,aliases},locked,owner{... on Service{id,aliases}},validationErrors{message,path},value},errors{message,path}}}`, + `mutation PropertyAssign($input:PropertyInput!){propertyAssign(input: $input){property{definition{id,aliases},locked,owner{__typename,... on Team{alias,id},... on Service{id,aliases}},validationErrors{message,path},value},errors{message,path}}}`, `{"input": {{ template "property_assign_input" }} }`, - `{"data":{"propertyAssign":{"property":{"definition":{"id":"{{ template "id2_string" }}"},"locked":true,"owner":{"id":"{{ template "id1_string" }}"},"validationErrors":[],"value":"true"},"errors":[]}}}`, + `{"data":{"propertyAssign":{"property":{"definition":{"id":"{{ template "id2_string" }}"},"locked":true,"owner":{"__typename":"Service","id":"{{ template "id1_string" }}","aliases":[]},"validationErrors":[],"value":"true"},"errors":[]}}}`, ) client := BestTestClient(t, "properties/property_assign", testRequest) @@ -306,16 +306,183 @@ func TestUnassignProperty(t *testing.T) { autopilot.Ok(t, err) } +func TestCreateTeamPropertyDefinition(t *testing.T) { + // Arrange + schema, schemaErr := ol.NewJSONSchema(schemaString2) + autopilot.Ok(t, schemaErr) + expectedDefinition := autopilot.Register("expected_team_property_definition", ol.TeamPropertyDefinition{ + Alias: "my_team_prop", + Id: id1, + Name: "my-team-prop", + Schema: *schema, + }) + input := autopilot.Register("team_property_definition_input", ol.TeamPropertyDefinitionInput{ + Alias: "my_team_prop", + Name: "my-team-prop", + Schema: *schema, + }) + testRequest := autopilot.NewTestRequest( + `mutation TeamPropertyDefinitionCreate($input:TeamPropertyDefinitionInput!){teamPropertyDefinitionCreate(input: $input){definition{alias,description,displaySubtype,displayType,id,lockedStatus,name,schema},errors{message,path}}}`, + `{"input": {{ template "team_property_definition_input" }} }`, + fmt.Sprintf(`{"data":{"teamPropertyDefinitionCreate":{"definition":{"alias":"my_team_prop","id":"%s","name":"my-team-prop","schema":%s},"errors":[]}}}`, id1, schemaString2), + ) + client := BestTestClient(t, "properties/team_definition_create", testRequest) + + // Act + result, err := client.CreateTeamPropertyDefinition(input) + + // Assert + autopilot.Ok(t, err) + autopilot.Equals(t, expectedDefinition, *result) + autopilot.Equals(t, expectedDefinition.Schema, result.Schema) +} + +func TestUpdateTeamPropertyDefinition(t *testing.T) { + // Arrange + schema, schemaErr := ol.NewJSONSchema(schemaString2) + autopilot.Ok(t, schemaErr) + expectedDefinition := autopilot.Register("expected_team_property_definition", ol.TeamPropertyDefinition{ + Alias: "my_team_prop", + Description: "updated description", + Id: id1, + Name: "my-team-prop", + Schema: *schema, + }) + input := autopilot.Register("team_property_definition_input", ol.TeamPropertyDefinitionInput{ + Alias: "my_team_prop", + Description: "updated description", + Name: "my-team-prop", + Schema: *schema, + }) + testRequest := autopilot.NewTestRequest( + `mutation TeamPropertyDefinitionUpdate($input:TeamPropertyDefinitionInput!$propertyDefinition:IdentifierInput!){teamPropertyDefinitionUpdate(propertyDefinition: $propertyDefinition, input: $input){definition{alias,description,displaySubtype,displayType,id,lockedStatus,name,schema},errors{message,path}}}`, + `{"propertyDefinition":{"alias":"my_team_prop"}, "input": {{ template "team_property_definition_input" }} }`, + fmt.Sprintf(`{"data":{"teamPropertyDefinitionUpdate":{"definition":{"alias":"my_team_prop","description":"updated description","id":"%s","name":"my-team-prop","schema":%s},"errors":[]}}}`, id1, schemaString2), + ) + client := BestTestClient(t, "properties/team_definition_update", testRequest) + + // Act + result, err := client.UpdateTeamPropertyDefinition("my_team_prop", input) + + // Assert + autopilot.Ok(t, err) + autopilot.Equals(t, expectedDefinition, *result) + autopilot.Equals(t, expectedDefinition.Schema, result.Schema) +} + +func TestGetTeamPropertyDefinition(t *testing.T) { + // Arrange + schema, schemaErr := ol.NewJSONSchema(schemaString2) + autopilot.Ok(t, schemaErr) + expectedDefinition := autopilot.Register("expected_team_property_definition", ol.TeamPropertyDefinition{ + Alias: "my_team_prop", + Id: id1, + Name: "my-team-prop", + Schema: *schema, + }) + testRequest := autopilot.NewTestRequest( + `query TeamPropertyDefinitionGet($input:IdentifierInput!){account{teamPropertyDefinition(input: $input){alias,description,displaySubtype,displayType,id,lockedStatus,name,schema}}}`, + `{"input":{"alias":"my_team_prop"}}`, + fmt.Sprintf(`{"data":{"account":{"teamPropertyDefinition":{"alias":"my_team_prop","id":"%s","name":"my-team-prop","schema":%s}}}}`, id1, schemaString2), + ) + client := BestTestClient(t, "properties/team_definition_get", testRequest) + + // Act + result, err := client.GetTeamPropertyDefinition("my_team_prop") + + // Assert + autopilot.Ok(t, err) + autopilot.Equals(t, expectedDefinition, *result) + autopilot.Equals(t, expectedDefinition.Schema, result.Schema) + autopilot.Equals(t, string(id1), string(result.Id)) +} + +func TestListTeamPropertyDefinitions(t *testing.T) { + // Arrange + schema, schemaErr := ol.NewJSONSchema(schemaString2) + autopilot.Ok(t, schemaErr) + expectedPage1 := autopilot.Register("team_property_definitions_page1", []ol.TeamPropertyDefinition{ + {Alias: "prop_a", Id: id1, Name: "prop-a", Schema: *schema}, + {Alias: "prop_b", Id: id2, Name: "prop-b", Schema: *schema}, + }) + expectedPage2 := autopilot.Register("team_property_definition_page2", ol.TeamPropertyDefinition{ + Alias: "prop_c", Id: id3, Name: "prop-c", Schema: *schema, + }) + testRequestOne := autopilot.NewTestRequest( + `query TeamPropertyDefinitionList($after:String!$first:Int!){account{teamPropertyDefinitions(after: $after, first: $first){nodes{alias,description,displaySubtype,displayType,id,lockedStatus,name,schema},{{ template "pagination_request" }}}}}`, + `{{ template "pagination_initial_query_variables" }}`, + fmt.Sprintf(`{"data":{"account":{"teamPropertyDefinitions":{"nodes":[{"alias":"prop_a","id":"%s","name":"prop-a","schema":%s},{"alias":"prop_b","id":"%s","name":"prop-b","schema":%s}],{{ template "pagination_initial_pageInfo_response" }}}}}}`, id1, schemaString2, id2, schemaString2), + ) + testRequestTwo := autopilot.NewTestRequest( + `query TeamPropertyDefinitionList($after:String!$first:Int!){account{teamPropertyDefinitions(after: $after, first: $first){nodes{alias,description,displaySubtype,displayType,id,lockedStatus,name,schema},{{ template "pagination_request" }}}}}`, + `{{ template "pagination_second_query_variables" }}`, + fmt.Sprintf(`{"data":{"account":{"teamPropertyDefinitions":{"nodes":[{"alias":"prop_c","id":"%s","name":"prop-c","schema":%s}],{{ template "pagination_second_pageInfo_response" }}}}}}`, id3, schemaString2), + ) + client := BestTestClient(t, "properties/team_definition_list", testRequestOne, testRequestTwo) + + // Act + result, err := client.ListTeamPropertyDefinitions(nil) + + // Assert + autopilot.Ok(t, err) + autopilot.Equals(t, 3, len(result.Nodes)) + autopilot.Equals(t, expectedPage1[0], result.Nodes[0]) + autopilot.Equals(t, expectedPage1[1], result.Nodes[1]) + autopilot.Equals(t, expectedPage2, result.Nodes[2]) + autopilot.Equals(t, expectedPage1[0].Schema, result.Nodes[0].Schema) + autopilot.Equals(t, 3, result.TotalCount) +} + +func TestGetServiceProperty(t *testing.T) { + // Arrange + serviceId := ol.ServiceId{ + Id: id1, + Aliases: []string{}, + } + service := ol.Service{ + ServiceId: serviceId, + } + owner := ol.PropertyOwner{ + Typename: "Service", + ServiceId: &serviceId, + } + value := ol.JsonString("true") + expectedProperty := ol.Property{ + Definition: ol.PropertyDefinitionId{Id: id2}, + Locked: true, + Owner: owner, + Value: &value, + } + testRequest := autopilot.NewTestRequest( + `query ServicePropertyGet($definition:IdentifierInput!$service:ID!){account{service(id: $service){property(definition: $definition){definition{id,aliases},locked,owner{__typename,... on Team{alias,id},... on Service{id,aliases}},validationErrors{message,path},value}}}}`, + `{"definition":{"alias":"is_beta_feature"},"service":"{{ template "id1_string" }}"}`, + `{"data":{"account":{"service":{"property":{"definition":{"id":"{{ template "id2_string" }}","aliases":[]},"locked":true,"owner":{"__typename":"Service","id":"{{ template "id1_string" }}","aliases":[]},"validationErrors":[],"value":"true"}}}}}`, + ) + client := BestTestClient(t, "properties/service_property_get", testRequest) + + // Act + result, err := service.GetProperty(client, "is_beta_feature") + + // Assert + autopilot.Ok(t, err) + autopilot.Equals(t, expectedProperty.Definition.Id, result.Definition.Id) + autopilot.Equals(t, expectedProperty.Locked, result.Locked) + autopilot.Equals(t, string(*expectedProperty.Value), string(*result.Value)) + autopilot.Equals(t, expectedProperty.Owner.Id(), result.Owner.Id()) +} + func TestGetServiceProperties(t *testing.T) { // Arrange serviceId := ol.ServiceId{ - Id: id1, + Id: id1, + Aliases: []string{}, } service := ol.Service{ ServiceId: serviceId, } - owner := ol.EntityOwnerService{ - OnService: serviceId, + owner := ol.PropertyOwner{ + Typename: "Service", + ServiceId: &serviceId, } value1 := ol.JsonString("true") value2 := ol.JsonString("false") @@ -352,12 +519,12 @@ func TestGetServiceProperties(t *testing.T) { }, }) testRequestOne := autopilot.NewTestRequest( - `query ServicePropertiesList($after:String!$first:Int!$service:ID!){account{service(id: $service){properties(after: $after, first: $first){nodes{definition{id,aliases},locked,owner{... on Service{id,aliases}},validationErrors{message,path},value},{{ template "pagination_request" }}}}}}`, + `query ServicePropertiesList($after:String!$first:Int!$service:ID!){account{service(id: $service){properties(after: $after, first: $first){nodes{definition{id,aliases},locked,owner{__typename,... on Team{alias,id},... on Service{id,aliases}},validationErrors{message,path},value},{{ template "pagination_request" }}}}}}`, `{ {{ template "first_page_variables" }}, "service": "{{ template "id1_string" }}" }`, `{"data":{"account":{"service":{"properties":{"nodes":[{{ template "service_properties_page_1" }}],{{ template "pagination_initial_pageInfo_response" }}}}}}}`, ) testRequestTwo := autopilot.NewTestRequest( - `query ServicePropertiesList($after:String!$first:Int!$service:ID!){account{service(id: $service){properties(after: $after, first: $first){nodes{definition{id,aliases},locked,owner{... on Service{id,aliases}},validationErrors{message,path},value},{{ template "pagination_request" }}}}}}`, + `query ServicePropertiesList($after:String!$first:Int!$service:ID!){account{service(id: $service){properties(after: $after, first: $first){nodes{definition{id,aliases},locked,owner{__typename,... on Team{alias,id},... on Service{id,aliases}},validationErrors{message,path},value},{{ template "pagination_request" }}}}}}`, `{ {{ template "second_page_variables" }}, "service": "{{ template "id1_string" }}" }`, `{"data":{"account":{"service":{"properties":{"nodes":[{{ template "service_properties_page_2" }}],{{ template "pagination_second_pageInfo_response" }}}}}}}`, ) @@ -378,3 +545,35 @@ func TestGetServiceProperties(t *testing.T) { autopilot.Equals(t, expectedPropsPageOne[1].Value, result[1].Value) autopilot.Equals(t, expectedPropsPageTwo[0].Value, result[2].Value) } + +func TestAssignTeamPropertyDefinitions(t *testing.T) { + // Arrange + schema, schemaErr := ol.NewJSONSchema(schemaString2) + autopilot.Ok(t, schemaErr) + input := autopilot.Register("team_property_definitions_assign_input", ol.TeamPropertyDefinitionsAssignInput{ + Properties: []ol.TeamPropertyDefinitionInput{ + {Alias: "prop_a", Name: "prop-a", Schema: *schema}, + {Alias: "prop_b", Name: "prop-b", Schema: *schema}, + }, + }) + expectedDefinitions := autopilot.Register("team_property_definitions_assigned", []ol.TeamPropertyDefinition{ + {Alias: "prop_a", Id: id1, Name: "prop-a", Schema: *schema}, + {Alias: "prop_b", Id: id2, Name: "prop-b", Schema: *schema}, + }) + testRequest := autopilot.NewTestRequest( + `mutation TeamPropertyDefinitionsAssign($input:TeamPropertyDefinitionsAssignInput!){teamPropertyDefinitionsAssign(input: $input){properties{nodes{alias,description,displaySubtype,displayType,id,lockedStatus,name,schema},pageInfo{hasNextPage,hasPreviousPage,startCursor,endCursor}},errors{message,path}}}`, + `{"input": {{ template "team_property_definitions_assign_input" }} }`, + fmt.Sprintf(`{"data":{"teamPropertyDefinitionsAssign":{"properties":{"nodes":[{"alias":"prop_a","id":"%s","name":"prop-a","schema":%s},{"alias":"prop_b","id":"%s","name":"prop-b","schema":%s}],"pageInfo":{"hasNextPage":false,"hasPreviousPage":false,"startCursor":"MQ","endCursor":"NA"}},"errors":[]}}}`, id1, schemaString2, id2, schemaString2), + ) + client := BestTestClient(t, "properties/team_definitions_assign", testRequest) + + // Act + result, err := client.AssignTeamPropertyDefinitions(input) + + // Assert + autopilot.Ok(t, err) + autopilot.Equals(t, 2, len(result.Nodes)) + autopilot.Equals(t, expectedDefinitions[0], result.Nodes[0]) + autopilot.Equals(t, expectedDefinitions[1], result.Nodes[1]) + autopilot.Equals(t, 2, result.TotalCount) +} diff --git a/service.go b/service.go index d3f1d114..e6da096a 100644 --- a/service.go +++ b/service.go @@ -43,8 +43,8 @@ type Service struct { Dependencies *ServiceDependenciesConnection `graphql:"-"` Dependents *ServiceDependentsConnection `graphql:"-"` - LastDeploy *Deploy `graphql:"-"` - Properties *ServicePropertiesConnection `graphql:"-"` + LastDeploy *Deploy `graphql:"-"` + Properties *PropertiesConnection `graphql:"-"` } // Returns unique identifiers created by OpsLevel, values in Aliases but not ManagedAliases diff --git a/team.go b/team.go index 148f016e..c9cf7d3c 100644 --- a/team.go +++ b/team.go @@ -26,6 +26,7 @@ type Team struct { ParentTeam TeamId Responsibilities string Tags *TagConnection + Properties *PropertiesConnection `graphql:"-" json:"-"` } // TeamIdConnection exists to prevent circular references on User because Team has a UserConnection @@ -171,6 +172,59 @@ func (team *Team) GetTags(client *Client, variables *PayloadVariables) (*TagConn return &q.Account.Team.Tags, nil } +func (team *Team) GetProperty(client *Client, definition string) (*Property, error) { + var q struct { + Account struct { + Team struct { + Property Property `graphql:"property(definition: $definition)"` + } `graphql:"team(id: $team)"` + } + } + if team.Id == "" { + return nil, fmt.Errorf("unable to get property, invalid Team id: '%s'", team.Id) + } + v := PayloadVariables{ + "team": team.Id, + "definition": *NewIdentifier(definition), + } + err := client.Query(&q, v, WithName("TeamPropertyGet")) + return &q.Account.Team.Property, HandleErrors(err, nil) +} + +func (team *Team) GetProperties(client *Client, variables *PayloadVariables) (*PropertiesConnection, error) { + var q struct { + Account struct { + Team struct { + Properties PropertiesConnection `graphql:"properties(after: $after, first: $first)"` + } `graphql:"team(id: $team)"` + } + } + + if team.Id == "" { + return nil, fmt.Errorf("unable to get properties, invalid Team id: '%s'", team.Id) + } + if variables == nil { + variables = client.InitialPageVariablesPointer() + } + (*variables)["team"] = team.Id + if err := client.Query(&q, *variables, WithName("TeamPropertiesList")); err != nil { + return nil, err + } + if team.Properties == nil { + team.Properties = &PropertiesConnection{} + } + team.Properties.Nodes = append(team.Properties.Nodes, q.Account.Team.Properties.Nodes...) + team.Properties.PageInfo = q.Account.Team.Properties.PageInfo + if team.Properties.PageInfo.HasNextPage { + (*variables)["after"] = team.Properties.PageInfo.End + if _, err := team.GetProperties(client, variables); err != nil { + return nil, err + } + } + team.Properties.TotalCount = len(team.Properties.Nodes) + return team.Properties, nil +} + func (team *Team) GetAliases() []string { return team.Aliases } diff --git a/team_test.go b/team_test.go index 975f88ac..eb1bbc09 100644 --- a/team_test.go +++ b/team_test.go @@ -7,6 +7,79 @@ import ( "github.com/rocktavious/autopilot/v2023" ) +func TestGetTeamProperty(t *testing.T) { + // Arrange + teamId := ol.TeamId{ + Alias: "example", + Id: id1, + } + team := ol.Team{ + TeamId: teamId, + } + value := ol.JsonString("true") + expectedProperty := ol.Property{ + Definition: ol.PropertyDefinitionId{Id: id2}, + Locked: false, + Owner: ol.PropertyOwner{Typename: "Team", TeamId: &teamId}, + Value: &value, + } + testRequest := autopilot.NewTestRequest( + `query TeamPropertyGet($definition:IdentifierInput!$team:ID!){account{team(id: $team){property(definition: $definition){definition{id,aliases},locked,owner{__typename,... on Team{alias,id},... on Service{id,aliases}},validationErrors{message,path},value}}}}`, + `{"definition":{"alias":"my-prop-alias"},"team":"{{ template "id1_string" }}"}`, + `{"data":{"account":{"team":{"property":{"definition":{"id":"{{ template "id2_string" }}","aliases":[]},"locked":false,"owner":{"__typename":"Team","alias":"{{ template "alias1" }}","id":"{{ template "id1_string" }}"},"validationErrors":[],"value":"true"}}}}}`, + ) + client := BestTestClient(t, "teams/team_property_get", testRequest) + + // Act + result, err := team.GetProperty(client, "my-prop-alias") + + // Assert + autopilot.Ok(t, err) + autopilot.Equals(t, expectedProperty.Definition.Id, result.Definition.Id) + autopilot.Equals(t, string(*expectedProperty.Value), string(*result.Value)) + autopilot.Equals(t, expectedProperty.Owner.Id(), result.Owner.Id()) +} + +func TestGetTeamProperties(t *testing.T) { + // Arrange + teamId := ol.TeamId{ + Alias: "example", + Id: id1, + } + team := ol.Team{ + TeamId: teamId, + } + owner := ol.PropertyOwner{ + Typename: "Team", + TeamId: &teamId, + } + value1 := ol.JsonString("true") + expectedProperties := autopilot.Register("team_properties", []ol.Property{ + { + Definition: ol.PropertyDefinitionId{Id: id2}, + Locked: false, + Owner: owner, + Value: &value1, + }, + }) + testRequest := autopilot.NewTestRequest( + `query TeamPropertiesList($after:String!$first:Int!$team:ID!){account{team(id: $team){properties(after: $after, first: $first){nodes{definition{id,aliases},locked,owner{__typename,... on Team{alias,id},... on Service{id,aliases}},validationErrors{message,path},value},{{ template "pagination_request" }}}}}}`, + `{ {{ template "first_page_variables" }}, "team": "{{ template "id1_string" }}" }`, + `{"data":{"account":{"team":{"properties":{"nodes":[{{ template "team_properties_page_1" }}],{{ template "no_pagination_response" }}}}}}}`, + ) + client := BestTestClient(t, "teams/team_properties", testRequest) + + // Act + result, err := team.GetProperties(client, nil) + + // Assert + autopilot.Ok(t, err) + autopilot.Equals(t, 1, len(result.Nodes)) + autopilot.Equals(t, expectedProperties[0].Definition.Id, result.Nodes[0].Definition.Id) + autopilot.Equals(t, expectedProperties[0].Owner.Id(), result.Nodes[0].Owner.Id()) + autopilot.Equals(t, string(*expectedProperties[0].Value), string(*result.Nodes[0].Value)) +} + // Probably should be a feature of autopilot func getTestRequestWithAlias() autopilot.TestRequest { return autopilot.NewTestRequest( diff --git a/testdata/templates/properties.tpl b/testdata/templates/properties.tpl index 7daf992e..f0bf7de5 100644 --- a/testdata/templates/properties.tpl +++ b/testdata/templates/properties.tpl @@ -1,5 +1,21 @@ {{- define "property_assign_input" }}{"owner":{"id":"{{ template "id1_string" }}"},"definition":{"id":"{{ template "id2_string" }}"},"value":"true"}{{ end }} +{{- define "team_properties_page_1" }} +{ + "definition": { + "id": "{{ template "id2_string" }}" + }, + "locked": false, + "owner": { + "__typename": "Team", + "alias": "{{ template "alias1" }}", + "id": "{{ template "id1_string" }}" + }, + "validationErrors": [], + "value": "true" +} +{{ end }} + {{- define "service_properties_page_1" }} { "definition": { @@ -7,7 +23,9 @@ }, "locked": true, "owner": { - "id": "{{ template "id1_string" }}" + "__typename": "Service", + "id": "{{ template "id1_string" }}", + "aliases": [] }, "validationErrors": [], "value": "true" @@ -18,7 +36,9 @@ }, "locked": false, "owner": { - "id": "{{ template "id1_string" }}" + "__typename": "Service", + "id": "{{ template "id1_string" }}", + "aliases": [] }, "validationErrors": [], "value": "false" @@ -32,7 +52,9 @@ }, "locked": true, "owner": { - "id": "{{ template "id1_string" }}" + "__typename": "Service", + "id": "{{ template "id1_string" }}", + "aliases": [] }, "validationErrors": [], "value": "\"Hello World!\""