Skip to content

Commit 0456aab

Browse files
Support custom properties on teams (#609)
* abstract team & service owners on property * feat: add CreateTeamPropertyDefinition Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat: add UpdateTeamPropertyDefinition Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat: add GetTeamPropertyDefinition Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat: add ListTeamPropertyDefinitions Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat: add AssignTeamPropertyDefinitions Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat: add Team.GetProperties with test * feat: add Team.GetProperty * feat: add Service.GetProperty, deprecate Client.GetProperty * chore: add changelog entries for team property definitions * chore: record breaking change for Property.Owner type * fix: set TotalCount correctly in GetProperties for Team and Service TotalCount is tagged graphql:"-" so it's never populated from the API. The recursive accumulation pattern (TotalCount += resp.TotalCount) always resulted in 0. Fix by setting TotalCount = len(Nodes) after all pages have been accumulated. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 193f7a0 commit 0456aab

11 files changed

Lines changed: 500 additions & 26 deletions
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
kind: Deprecated
2+
body: '`Client.GetProperty` only resolves service owners and will fail for team identifiers. Use `Service.GetProperty` or `Team.GetProperty` instead.'
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
kind: Removed
2+
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`).'
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
kind: Feature
2+
body: Add CRUD operations for team property definitions (`CreateTeamPropertyDefinition`, `UpdateTeamPropertyDefinition`, `GetTeamPropertyDefinition`, `ListTeamPropertyDefinitions`, `AssignTeamPropertyDefinitions`) and entity-scoped property lookup methods (`Team.GetProperty`, `Service.GetProperty`)

owner.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,3 +28,19 @@ func (entityOwnerService *EntityOwnerService) Aliases() []string {
2828
func (entityOwnerService *EntityOwnerService) Id() ID {
2929
return entityOwnerService.OnService.Id
3030
}
31+
32+
type PropertyOwner struct {
33+
Typename string `graphql:"__typename"`
34+
*TeamId `graphql:"... on Team"`
35+
*ServiceId `graphql:"... on Service"`
36+
}
37+
38+
func (o PropertyOwner) Id() ID {
39+
if o.ServiceId != nil {
40+
return o.ServiceId.Id
41+
}
42+
if o.TeamId != nil {
43+
return o.TeamId.Id
44+
}
45+
return ""
46+
}

payload.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -330,6 +330,12 @@ type TeamPropertyDefinitionPayload struct {
330330
BasePayload
331331
}
332332

333+
// TeamPropertyDefinitionsAssignPayload The return type for the teamPropertyDefinitionsAssign mutation
334+
type TeamPropertyDefinitionsAssignPayload struct {
335+
Properties TeamPropertyDefinitionConnection // The property definitions that were assigned (Optional)
336+
BasePayload
337+
}
338+
333339
// TeamUpdatePayload The return type of a `teamUpdate` mutation
334340
type TeamUpdatePayload struct {
335341
Team Team // A team belongs to your organization. Teams can own multiple services (Optional)

property.go

Lines changed: 106 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -25,12 +25,12 @@ type PropertyDefinitionId struct {
2525
type Property struct {
2626
Definition PropertyDefinitionId `graphql:"definition"`
2727
Locked bool `graphql:"locked"`
28-
Owner EntityOwnerService `graphql:"owner"`
28+
Owner PropertyOwner `graphql:"owner"`
2929
ValidationErrors []Error `graphql:"validationErrors"`
3030
Value *JsonString `graphql:"value"`
3131
}
3232

33-
type ServicePropertiesConnection struct {
33+
type PropertiesConnection struct {
3434
Nodes []Property
3535
PageInfo PageInfo
3636
TotalCount int `graphql:"-"`
@@ -111,6 +111,86 @@ func (client *Client) DeletePropertyDefinition(input string) error {
111111
return HandleErrors(err, m.Payload.Errors)
112112
}
113113

114+
func (client *Client) CreateTeamPropertyDefinition(input TeamPropertyDefinitionInput) (*TeamPropertyDefinition, error) {
115+
var m struct {
116+
Payload TeamPropertyDefinitionPayload `graphql:"teamPropertyDefinitionCreate(input: $input)"`
117+
}
118+
v := PayloadVariables{
119+
"input": input,
120+
}
121+
err := client.Mutate(&m, v, WithName("TeamPropertyDefinitionCreate"))
122+
return &m.Payload.Definition, HandleErrors(err, m.Payload.Errors)
123+
}
124+
125+
func (client *Client) UpdateTeamPropertyDefinition(identifier string, input TeamPropertyDefinitionInput) (*TeamPropertyDefinition, error) {
126+
var m struct {
127+
Payload TeamPropertyDefinitionPayload `graphql:"teamPropertyDefinitionUpdate(propertyDefinition: $propertyDefinition, input: $input)"`
128+
}
129+
v := PayloadVariables{
130+
"propertyDefinition": *NewIdentifier(identifier),
131+
"input": input,
132+
}
133+
err := client.Mutate(&m, v, WithName("TeamPropertyDefinitionUpdate"))
134+
return &m.Payload.Definition, HandleErrors(err, m.Payload.Errors)
135+
}
136+
137+
func (client *Client) GetTeamPropertyDefinition(identifier string) (*TeamPropertyDefinition, error) {
138+
var q struct {
139+
Account struct {
140+
Definition TeamPropertyDefinition `graphql:"teamPropertyDefinition(input: $input)"`
141+
}
142+
}
143+
v := PayloadVariables{
144+
"input": *NewIdentifier(identifier),
145+
}
146+
err := client.Query(&q, v, WithName("TeamPropertyDefinitionGet"))
147+
if q.Account.Definition.Id == "" {
148+
err = fmt.Errorf("TeamPropertyDefinition with ID or Alias matching '%s' not found", identifier)
149+
}
150+
return &q.Account.Definition, HandleErrors(err, nil)
151+
}
152+
153+
func (client *Client) ListTeamPropertyDefinitions(variables *PayloadVariables) (*TeamPropertyDefinitionConnection, error) {
154+
var q struct {
155+
Account struct {
156+
Definitions TeamPropertyDefinitionConnection `graphql:"teamPropertyDefinitions(after: $after, first: $first)"`
157+
}
158+
}
159+
if variables == nil {
160+
variables = client.InitialPageVariablesPointer()
161+
}
162+
if err := client.Query(&q, *variables, WithName("TeamPropertyDefinitionList")); err != nil {
163+
return nil, err
164+
}
165+
q.Account.Definitions.TotalCount = len(q.Account.Definitions.Nodes)
166+
if q.Account.Definitions.PageInfo.HasNextPage {
167+
(*variables)["after"] = q.Account.Definitions.PageInfo.End
168+
resp, err := client.ListTeamPropertyDefinitions(variables)
169+
if err != nil {
170+
return nil, err
171+
}
172+
q.Account.Definitions.Nodes = append(q.Account.Definitions.Nodes, resp.Nodes...)
173+
q.Account.Definitions.PageInfo = resp.PageInfo
174+
q.Account.Definitions.TotalCount += resp.TotalCount
175+
}
176+
return &q.Account.Definitions, nil
177+
}
178+
179+
func (client *Client) AssignTeamPropertyDefinitions(input TeamPropertyDefinitionsAssignInput) (*TeamPropertyDefinitionConnection, error) {
180+
var m struct {
181+
Payload TeamPropertyDefinitionsAssignPayload `graphql:"teamPropertyDefinitionsAssign(input: $input)"`
182+
}
183+
v := PayloadVariables{
184+
"input": input,
185+
}
186+
err := client.Mutate(&m, v, WithName("TeamPropertyDefinitionsAssign"))
187+
m.Payload.Properties.TotalCount = len(m.Payload.Properties.Nodes)
188+
return &m.Payload.Properties, HandleErrors(err, m.Payload.Errors)
189+
}
190+
191+
// Deprecated: Use [Service.GetProperty] or [Team.GetProperty] instead.
192+
// This method only resolves service owners. Passing a team identifier will
193+
// return an error from the API.
114194
func (client *Client) GetProperty(owner string, definition string) (*Property, error) {
115195
var q struct {
116196
Account struct {
@@ -125,6 +205,25 @@ func (client *Client) GetProperty(owner string, definition string) (*Property, e
125205
return &q.Account.Property, HandleErrors(err, nil)
126206
}
127207

208+
func (service *Service) GetProperty(client *Client, definition string) (*Property, error) {
209+
var q struct {
210+
Account struct {
211+
Service struct {
212+
Property Property `graphql:"property(definition: $definition)"`
213+
} `graphql:"service(id: $service)"`
214+
}
215+
}
216+
if service.Id == "" {
217+
return nil, fmt.Errorf("unable to get property, invalid Service id: '%s'", service.Id)
218+
}
219+
v := PayloadVariables{
220+
"service": service.Id,
221+
"definition": *NewIdentifier(definition),
222+
}
223+
err := client.Query(&q, v, WithName("ServicePropertyGet"))
224+
return &q.Account.Service.Property, HandleErrors(err, nil)
225+
}
226+
128227
func (client *Client) PropertyAssign(input PropertyInput) (*Property, error) {
129228
var m struct {
130229
Payload PropertyPayload `graphql:"propertyAssign(input: $input)"`
@@ -148,11 +247,11 @@ func (client *Client) PropertyUnassign(owner string, definition string) error {
148247
return HandleErrors(err, m.Payload.Errors)
149248
}
150249

151-
func (service *Service) GetProperties(client *Client, variables *PayloadVariables) (*ServicePropertiesConnection, error) {
250+
func (service *Service) GetProperties(client *Client, variables *PayloadVariables) (*PropertiesConnection, error) {
152251
var q struct {
153252
Account struct {
154253
Service struct {
155-
Properties ServicePropertiesConnection `graphql:"properties(after: $after, first: $first)"`
254+
Properties PropertiesConnection `graphql:"properties(after: $after, first: $first)"`
156255
} `graphql:"service(id: $service)"`
157256
}
158257
}
@@ -168,17 +267,16 @@ func (service *Service) GetProperties(client *Client, variables *PayloadVariable
168267
return nil, err
169268
}
170269
if service.Properties == nil {
171-
service.Properties = &ServicePropertiesConnection{}
270+
service.Properties = &PropertiesConnection{}
172271
}
173272
service.Properties.Nodes = append(service.Properties.Nodes, q.Account.Service.Properties.Nodes...)
174273
service.Properties.PageInfo = q.Account.Service.Properties.PageInfo
175274
if service.Properties.PageInfo.HasNextPage {
176275
(*variables)["after"] = service.Properties.PageInfo.End
177-
resp, err := service.GetProperties(client, variables)
178-
if err != nil {
276+
if _, err := service.GetProperties(client, variables); err != nil {
179277
return nil, err
180278
}
181-
service.Properties.TotalCount += resp.TotalCount
182279
}
280+
service.Properties.TotalCount = len(service.Properties.Nodes)
183281
return service.Properties, nil
184282
}

0 commit comments

Comments
 (0)