From 88078355e1198d5b8bccf111d73932177a9e6c3c Mon Sep 17 00:00:00 2001 From: debidong <1953531014@qq.com> Date: Thu, 14 May 2026 11:56:13 +0800 Subject: [PATCH 1/3] feat: add team CRUD SDK methods (GetTeamInfo, UpsertTeam, DeleteTeam) Add full team management support to the SDK: - TeamItem type with all 13 fields from /team/info endpoint - TeamGetInput, TeamUpsertInput, TeamUpsertOutput, TeamDeleteInput types - GetTeamInfo: parallel /team/info + /team/infos calls for full metadata with member names - UpsertTeam: create or update teams via /team/upsert - DeleteTeam: delete teams by ID, name, or ref_id via /team/delete - Enhanced ListTeamsInput with Limit, OrderBy, Asc, PersonID fields - OrderBy validation and limit capping (max 100) --- teams.go | 254 ++++++++++++++++++++++++++++++++++++++++++++++++++++++- types.go | 53 +++++++++++- 2 files changed, 302 insertions(+), 5 deletions(-) diff --git a/teams.go b/teams.go index c08a3eb..bb54e15 100644 --- a/teams.go +++ b/teams.go @@ -4,15 +4,21 @@ import ( "context" "fmt" "net/http" + + "golang.org/x/sync/errgroup" ) const defaultTeamsQueryLimit = 20 // ListTeamsInput contains parameters for listing teams type ListTeamsInput struct { - TeamIDs []int64 // Direct lookup by team IDs - Name string // Search by team name - Page int // Page number (default 1) + TeamIDs []int64 // Direct lookup by team IDs + Name string // Search by team name + Page int // Page number (default 1) + Limit int // Page size (max 100, default 20) + OrderBy string // Sort field: created_at, updated_at, team_name + Asc bool // Ascending sort order + PersonID int64 // Filter by member ID } // ListTeamsOutput contains the result of listing teams @@ -92,13 +98,32 @@ func (c *Client) ListTeams(ctx context.Context, input *ListTeamsInput) (*ListTea if page <= 0 { page = 1 } + limit := input.Limit + if limit <= 0 { + limit = defaultTeamsQueryLimit + } + if limit > 100 { + limit = 100 + } requestBody := map[string]any{ "p": page, - "limit": defaultTeamsQueryLimit, + "limit": limit, } if input.Name != "" { requestBody["query"] = input.Name } + switch input.OrderBy { + case "created_at", "updated_at", "team_name": + requestBody["orderby"] = input.OrderBy + requestBody["asc"] = input.Asc + case "": + // no ordering + default: + return nil, fmt.Errorf("invalid orderby value %q: must be one of created_at, updated_at, team_name", input.OrderBy) + } + if input.PersonID != 0 { + requestBody["person_id"] = input.PersonID + } resp, err := c.makeRequest(ctx, "POST", "/team/list", requestBody) if err != nil { @@ -160,3 +185,224 @@ func (c *Client) ListTeams(ctx context.Context, input *ListTeamsInput) (*ListTea Total: total, }, nil } + +// GetTeamInfo retrieves full team detail by ID, name, or ref_id. +// It calls /team/info for full metadata and /team/infos for member names in parallel. +func (c *Client) GetTeamInfo(ctx context.Context, input *TeamGetInput) (*TeamItem, error) { + infoBody := map[string]any{} + if input.TeamID != 0 { + infoBody["team_id"] = input.TeamID + } + if input.TeamName != "" { + infoBody["team_name"] = input.TeamName + } + if input.RefID != "" { + infoBody["ref_id"] = input.RefID + } + + var team *TeamItem + var members []TeamMember + + g, gctx := errgroup.WithContext(ctx) + + g.Go(func() error { + resp, err := c.makeRequest(gctx, "POST", "/team/info", infoBody) + if err != nil { + return fmt.Errorf("unable to get team info: %w", err) + } + defer func() { _ = resp.Body.Close() }() + + if resp.StatusCode != http.StatusOK { + return handleAPIError(c.logger, resp) + } + + var result struct { + Error *DutyError `json:"error,omitempty"` + Data *TeamItem `json:"data,omitempty"` + } + if err := parseResponse(c.logger, resp, &result); err != nil { + return err + } + if result.Error != nil { + return result.Error + } + if result.Data == nil { + return fmt.Errorf("team not found") + } + team = result.Data + return nil + }) + + // When looking up by ID, we can fire /team/infos in parallel for member names. + // For name/ref_id lookups we don't know the ID upfront, so we enrich after. + if input.TeamID != 0 { + g.Go(func() error { + resolved, err := c.fetchTeamMembers(gctx, input.TeamID) + if err != nil { + c.logger.Warn("failed to enrich team members", "error", err) + return nil + } + members = resolved + return nil + }) + } + + if err := g.Wait(); err != nil { + return nil, err + } + + // For name/ref_id lookups, enrich members sequentially using the team_id we got back. + if input.TeamID == 0 && team != nil && len(team.PersonIDs) > 0 { + resolved, err := c.fetchTeamMembers(ctx, team.TeamID) + if err != nil { + c.logger.Warn("failed to enrich team members", "error", err) + } else { + members = resolved + } + } + + if len(members) > 0 { + team.Members = members + } + + return team, nil +} + +// fetchTeamMembers retrieves member details for a team via /team/infos. +func (c *Client) fetchTeamMembers(ctx context.Context, teamID int64) ([]TeamMember, error) { + infosBody := map[string]any{"team_ids": []int64{teamID}} + resp, err := c.makeRequest(ctx, "POST", "/team/infos", infosBody) + if err != nil { + return nil, fmt.Errorf("unable to fetch team members: %w", err) + } + defer func() { _ = resp.Body.Close() }() + + if resp.StatusCode != http.StatusOK { + return nil, handleAPIError(c.logger, resp) + } + + var result struct { + Error *DutyError `json:"error,omitempty"` + Data *struct { + Items []struct { + Members []struct { + PersonID int64 `json:"person_id"` + PersonName string `json:"person_name"` + Email string `json:"email,omitempty"` + } `json:"members,omitempty"` + } `json:"items"` + } `json:"data,omitempty"` + } + if err := parseResponse(c.logger, resp, &result); err != nil { + return nil, err + } + if result.Error != nil { + return nil, result.Error + } + + var members []TeamMember + if result.Data != nil && len(result.Data.Items) > 0 { + for _, m := range result.Data.Items[0].Members { + members = append(members, TeamMember{ + PersonID: m.PersonID, + PersonName: m.PersonName, + Email: m.Email, + }) + } + } + return members, nil +} + +// UpsertTeam creates or updates a team. +func (c *Client) UpsertTeam(ctx context.Context, input *TeamUpsertInput) (*TeamUpsertOutput, error) { + requestBody := map[string]any{} + if input.TeamName != "" { + requestBody["team_name"] = input.TeamName + } + if input.TeamID != 0 { + requestBody["team_id"] = input.TeamID + } + if input.Description != "" { + requestBody["description"] = input.Description + } + if len(input.PersonIDs) > 0 { + requestBody["person_ids"] = input.PersonIDs + } + if len(input.Emails) > 0 { + requestBody["emails"] = input.Emails + } + if len(input.Phones) > 0 { + requestBody["phones"] = input.Phones + } + if input.CountryCode != "" { + requestBody["countryCode"] = input.CountryCode + } + if input.RefID != "" { + requestBody["ref_id"] = input.RefID + } + if input.ResetIfNameExist { + requestBody["reset_if_name_exist"] = true + } + + resp, err := c.makeRequest(ctx, "POST", "/team/upsert", requestBody) + if err != nil { + return nil, fmt.Errorf("unable to upsert team: %w", err) + } + defer func() { _ = resp.Body.Close() }() + + if resp.StatusCode != http.StatusOK { + return nil, handleAPIError(c.logger, resp) + } + + var result struct { + Error *DutyError `json:"error,omitempty"` + Data *TeamUpsertOutput `json:"data,omitempty"` + } + if err := parseResponse(c.logger, resp, &result); err != nil { + return nil, err + } + if result.Error != nil { + return nil, result.Error + } + if result.Data == nil { + return nil, fmt.Errorf("unexpected empty response from team upsert") + } + + return result.Data, nil +} + +// DeleteTeam permanently deletes a team by ID, name, or ref_id. +func (c *Client) DeleteTeam(ctx context.Context, input *TeamDeleteInput) error { + requestBody := map[string]any{} + if input.TeamID != 0 { + requestBody["team_id"] = input.TeamID + } + if input.TeamName != "" { + requestBody["team_name"] = input.TeamName + } + if input.RefID != "" { + requestBody["ref_id"] = input.RefID + } + + resp, err := c.makeRequest(ctx, "POST", "/team/delete", requestBody) + if err != nil { + return fmt.Errorf("unable to delete team: %w", err) + } + defer func() { _ = resp.Body.Close() }() + + if resp.StatusCode != http.StatusOK { + return handleAPIError(c.logger, resp) + } + + var result struct { + Error *DutyError `json:"error,omitempty"` + } + if err := parseResponse(c.logger, resp, &result); err != nil { + return err + } + if result.Error != nil { + return result.Error + } + + return nil +} diff --git a/types.go b/types.go index 87fa159..d208804 100644 --- a/types.go +++ b/types.go @@ -89,7 +89,7 @@ type ChannelInfo struct { CreatorName string `json:"creator_name,omitempty" toon:"creator_name,omitempty"` } -// TeamInfo represents team information +// TeamInfo represents team information (used by list/batch endpoints) type TeamInfo struct { TeamID int64 `json:"team_id" toon:"team_id"` TeamName string `json:"team_name" toon:"team_name"` @@ -103,6 +103,57 @@ type TeamMember struct { Email string `json:"email,omitempty" toon:"email,omitempty"` } +// TeamItem represents full team detail from /team/info +type TeamItem struct { + AccountID int64 `json:"account_id" toon:"account_id"` + TeamID int64 `json:"team_id" toon:"team_id"` + TeamName string `json:"team_name" toon:"team_name"` + Description string `json:"description" toon:"description"` + Status string `json:"status" toon:"status"` + CreatorID int64 `json:"creator_id" toon:"creator_id"` + CreatorName string `json:"creator_name" toon:"creator_name"` + UpdatedBy int64 `json:"updated_by" toon:"updated_by"` + UpdatedByName string `json:"updated_by_name" toon:"updated_by_name"` + CreatedAt int64 `json:"created_at" toon:"created_at"` + UpdatedAt int64 `json:"updated_at" toon:"updated_at"` + PersonIDs []int64 `json:"person_ids" toon:"person_ids"` + RefID string `json:"ref_id" toon:"ref_id"` + Members []TeamMember `json:"members,omitempty" toon:"members,omitempty"` +} + +// TeamGetInput contains parameters for getting a single team +type TeamGetInput struct { + TeamID int64 + TeamName string + RefID string +} + +// TeamUpsertInput contains parameters for creating or updating a team +type TeamUpsertInput struct { + TeamID int64 + TeamName string + Description string + PersonIDs []int64 + Emails []string + Phones []string + CountryCode string + RefID string + ResetIfNameExist bool +} + +// TeamUpsertOutput contains the result of creating or updating a team +type TeamUpsertOutput struct { + TeamID int64 `json:"team_id"` + TeamName string `json:"team_name"` +} + +// TeamDeleteInput contains parameters for deleting a team +type TeamDeleteInput struct { + TeamID int64 + TeamName string + RefID string +} + // FieldInfo represents custom field definition type FieldInfo struct { FieldID string `json:"field_id" toon:"field_id"` From d6be5849cb10911aa7e187be31a96a920b46b95a Mon Sep 17 00:00:00 2001 From: debidong <1953531014@qq.com> Date: Thu, 14 May 2026 12:08:22 +0800 Subject: [PATCH 2/3] fix: require team_name in UpsertTeam and validate upfront The API requires team_name on every upsert call, even for updates. Add explicit validation in the SDK to fail fast with a clear error. --- teams.go | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/teams.go b/teams.go index bb54e15..a7f0871 100644 --- a/teams.go +++ b/teams.go @@ -313,11 +313,13 @@ func (c *Client) fetchTeamMembers(ctx context.Context, teamID int64) ([]TeamMemb return members, nil } -// UpsertTeam creates or updates a team. +// UpsertTeam creates or updates a team. TeamName is required by the API. func (c *Client) UpsertTeam(ctx context.Context, input *TeamUpsertInput) (*TeamUpsertOutput, error) { - requestBody := map[string]any{} - if input.TeamName != "" { - requestBody["team_name"] = input.TeamName + if input.TeamName == "" { + return nil, fmt.Errorf("team_name is required") + } + requestBody := map[string]any{ + "team_name": input.TeamName, } if input.TeamID != 0 { requestBody["team_id"] = input.TeamID From a1922943dc7b51df7e59af349a1c23ae2dee9826 Mon Sep 17 00:00:00 2001 From: debidong <1953531014@qq.com> Date: Thu, 14 May 2026 16:59:36 +0800 Subject: [PATCH 3/3] fix: use /person/infos for team member name resolution The /team/infos endpoint returns person_ids but not member names/emails for member-level app keys. Switch to /person/infos which reliably returns person details regardless of app key type. Changes: - GetTeamInfo: replace parallel /team/infos call with sequential /person/infos enrichment after /team/info returns person_ids. Also enrich creator_name when the API returns it empty. - ListTeams: parse person_ids from API response (not members), then batch-resolve via new enrichTeamMembers helper. - TeamInfo: add PersonIDs field to carry raw IDs from API. - Remove fetchTeamMembers helper (no longer needed). - Update TestListTeamsByIDsPreservesMembers to mock /person/infos. --- client_test.go | 64 ++++++++------ teams.go | 233 ++++++++++++++++++++----------------------------- types.go | 7 +- 3 files changed, 134 insertions(+), 170 deletions(-) diff --git a/client_test.go b/client_test.go index 1b5af50..504dd53 100644 --- a/client_test.go +++ b/client_test.go @@ -123,39 +123,45 @@ func callListTeams(t *testing.T, c *Client) { func TestListTeamsByIDsPreservesMembers(t *testing.T) { cap := &capturedHeaders{} ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - if r.URL.Path != "/team/infos" { - t.Fatalf("unexpected path: %s", r.URL.Path) - } - - var body struct { - TeamIDs []int64 `json:"team_ids"` - } - if err := json.NewDecoder(r.Body).Decode(&body); err != nil { - t.Fatalf("decode request: %v", err) - } - if len(body.TeamIDs) != 2 || body.TeamIDs[0] != 101 || body.TeamIDs[1] != 202 { - t.Fatalf("unexpected team_ids payload: %#v", body.TeamIDs) - } - - cap.set(r.Header) w.Header().Set("Content-Type", "application/json") - _ = json.NewEncoder(w).Encode(map[string]any{ - "data": map[string]any{ - "items": []any{ - map[string]any{ - "team_id": 101, - "team_name": "alpha", - "members": []any{ - map[string]any{ - "person_id": 1, - "person_name": "Ada", - "email": "ada@example.com", - }, + switch r.URL.Path { + case "/team/infos": + var body struct { + TeamIDs []int64 `json:"team_ids"` + } + if err := json.NewDecoder(r.Body).Decode(&body); err != nil { + t.Fatalf("decode request: %v", err) + } + if len(body.TeamIDs) != 2 || body.TeamIDs[0] != 101 || body.TeamIDs[1] != 202 { + t.Fatalf("unexpected team_ids payload: %#v", body.TeamIDs) + } + cap.set(r.Header) + _ = json.NewEncoder(w).Encode(map[string]any{ + "data": map[string]any{ + "items": []any{ + map[string]any{ + "team_id": 101, + "team_name": "alpha", + "person_ids": []int64{1}, }, }, }, - }, - }) + }) + case "/person/infos": + _ = json.NewEncoder(w).Encode(map[string]any{ + "data": map[string]any{ + "items": []any{ + map[string]any{ + "person_id": 1, + "person_name": "Ada", + "email": "ada@example.com", + }, + }, + }, + }) + default: + t.Fatalf("unexpected path: %s", r.URL.Path) + } })) defer ts.Close() diff --git a/teams.go b/teams.go index a7f0871..d7a740e 100644 --- a/teams.go +++ b/teams.go @@ -4,8 +4,6 @@ import ( "context" "fmt" "net/http" - - "golang.org/x/sync/errgroup" ) const defaultTeamsQueryLimit = 20 @@ -49,13 +47,9 @@ func (c *Client) ListTeams(ctx context.Context, input *ListTeamsInput) (*ListTea Error *DutyError `json:"error,omitempty"` Data *struct { Items []struct { - TeamID int64 `json:"team_id"` - TeamName string `json:"team_name"` - Members []struct { - PersonID int64 `json:"person_id"` - PersonName string `json:"person_name"` - Email string `json:"email,omitempty"` - } `json:"members,omitempty"` + TeamID int64 `json:"team_id"` + TeamName string `json:"team_name"` + PersonIDs []int64 `json:"person_ids"` } `json:"items"` } `json:"data,omitempty"` } @@ -69,24 +63,16 @@ func (c *Client) ListTeams(ctx context.Context, input *ListTeamsInput) (*ListTea teams := []TeamInfo{} if result.Data != nil { for _, t := range result.Data.Items { - team := TeamInfo{ - TeamID: t.TeamID, - TeamName: t.TeamName, - } - if len(t.Members) > 0 { - team.Members = make([]TeamMember, 0, len(t.Members)) - for _, m := range t.Members { - team.Members = append(team.Members, TeamMember{ - PersonID: m.PersonID, - PersonName: m.PersonName, - Email: m.Email, - }) - } - } - teams = append(teams, team) + teams = append(teams, TeamInfo{ + TeamID: t.TeamID, + TeamName: t.TeamName, + PersonIDs: t.PersonIDs, + }) } } + c.enrichTeamMembers(ctx, teams) + return &ListTeamsOutput{ Teams: teams, Total: len(teams), @@ -139,13 +125,9 @@ func (c *Client) ListTeams(ctx context.Context, input *ListTeamsInput) (*ListTea Error *DutyError `json:"error,omitempty"` Data *struct { Items []struct { - TeamID int64 `json:"team_id"` - TeamName string `json:"team_name"` - Members []struct { - PersonID int64 `json:"person_id"` - PersonName string `json:"person_name"` - Email string `json:"email,omitempty"` - } `json:"members,omitempty"` + TeamID int64 `json:"team_id"` + TeamName string `json:"team_name"` + PersonIDs []int64 `json:"person_ids"` } `json:"items"` Total int `json:"total"` } `json:"data,omitempty"` @@ -161,33 +143,58 @@ func (c *Client) ListTeams(ctx context.Context, input *ListTeamsInput) (*ListTea total := 0 if result.Data != nil { for _, t := range result.Data.Items { - team := TeamInfo{ - TeamID: t.TeamID, - TeamName: t.TeamName, - } - if len(t.Members) > 0 { - team.Members = make([]TeamMember, 0, len(t.Members)) - for _, m := range t.Members { - team.Members = append(team.Members, TeamMember{ - PersonID: m.PersonID, - PersonName: m.PersonName, - Email: m.Email, - }) - } - } - teams = append(teams, team) + teams = append(teams, TeamInfo{ + TeamID: t.TeamID, + TeamName: t.TeamName, + PersonIDs: t.PersonIDs, + }) } total = result.Data.Total } + c.enrichTeamMembers(ctx, teams) + return &ListTeamsOutput{ Teams: teams, Total: total, }, nil } +// enrichTeamMembers resolves member names for a slice of teams via /person/infos. +func (c *Client) enrichTeamMembers(ctx context.Context, teams []TeamInfo) { + var allIDs []int64 + for _, t := range teams { + allIDs = append(allIDs, t.PersonIDs...) + } + if len(allIDs) == 0 { + return + } + + personMap, err := c.fetchPersonInfos(ctx, allIDs) + if err != nil { + c.logger.Warn("failed to enrich team members", "error", err) + return + } + + for i := range teams { + members := make([]TeamMember, 0, len(teams[i].PersonIDs)) + for _, pid := range teams[i].PersonIDs { + if p, ok := personMap[pid]; ok { + members = append(members, TeamMember{ + PersonID: p.PersonID, + PersonName: p.PersonName, + Email: p.Email, + }) + } else { + members = append(members, TeamMember{PersonID: pid}) + } + } + teams[i].Members = members + } +} + // GetTeamInfo retrieves full team detail by ID, name, or ref_id. -// It calls /team/info for full metadata and /team/infos for member names in parallel. +// It calls /team/info for metadata, then /person/infos to resolve member names. func (c *Client) GetTeamInfo(ctx context.Context, input *TeamGetInput) (*TeamItem, error) { infoBody := map[string]any{} if input.TeamID != 0 { @@ -200,80 +207,9 @@ func (c *Client) GetTeamInfo(ctx context.Context, input *TeamGetInput) (*TeamIte infoBody["ref_id"] = input.RefID } - var team *TeamItem - var members []TeamMember - - g, gctx := errgroup.WithContext(ctx) - - g.Go(func() error { - resp, err := c.makeRequest(gctx, "POST", "/team/info", infoBody) - if err != nil { - return fmt.Errorf("unable to get team info: %w", err) - } - defer func() { _ = resp.Body.Close() }() - - if resp.StatusCode != http.StatusOK { - return handleAPIError(c.logger, resp) - } - - var result struct { - Error *DutyError `json:"error,omitempty"` - Data *TeamItem `json:"data,omitempty"` - } - if err := parseResponse(c.logger, resp, &result); err != nil { - return err - } - if result.Error != nil { - return result.Error - } - if result.Data == nil { - return fmt.Errorf("team not found") - } - team = result.Data - return nil - }) - - // When looking up by ID, we can fire /team/infos in parallel for member names. - // For name/ref_id lookups we don't know the ID upfront, so we enrich after. - if input.TeamID != 0 { - g.Go(func() error { - resolved, err := c.fetchTeamMembers(gctx, input.TeamID) - if err != nil { - c.logger.Warn("failed to enrich team members", "error", err) - return nil - } - members = resolved - return nil - }) - } - - if err := g.Wait(); err != nil { - return nil, err - } - - // For name/ref_id lookups, enrich members sequentially using the team_id we got back. - if input.TeamID == 0 && team != nil && len(team.PersonIDs) > 0 { - resolved, err := c.fetchTeamMembers(ctx, team.TeamID) - if err != nil { - c.logger.Warn("failed to enrich team members", "error", err) - } else { - members = resolved - } - } - - if len(members) > 0 { - team.Members = members - } - - return team, nil -} - -// fetchTeamMembers retrieves member details for a team via /team/infos. -func (c *Client) fetchTeamMembers(ctx context.Context, teamID int64) ([]TeamMember, error) { - infosBody := map[string]any{"team_ids": []int64{teamID}} - resp, err := c.makeRequest(ctx, "POST", "/team/infos", infosBody) + resp, err := c.makeRequest(ctx, "POST", "/team/info", infoBody) if err != nil { - return nil, fmt.Errorf("unable to fetch team members: %w", err) + return nil, fmt.Errorf("unable to get team info: %w", err) } defer func() { _ = resp.Body.Close() }() @@ -283,15 +219,7 @@ func (c *Client) fetchTeamMembers(ctx context.Context, teamID int64) ([]TeamMemb var result struct { Error *DutyError `json:"error,omitempty"` - Data *struct { - Items []struct { - Members []struct { - PersonID int64 `json:"person_id"` - PersonName string `json:"person_name"` - Email string `json:"email,omitempty"` - } `json:"members,omitempty"` - } `json:"items"` - } `json:"data,omitempty"` + Data *TeamItem `json:"data,omitempty"` } if err := parseResponse(c.logger, resp, &result); err != nil { return nil, err @@ -299,18 +227,47 @@ func (c *Client) fetchTeamMembers(ctx context.Context, teamID int64) ([]TeamMemb if result.Error != nil { return nil, result.Error } + if result.Data == nil { + return nil, fmt.Errorf("team not found") + } - var members []TeamMember - if result.Data != nil && len(result.Data.Items) > 0 { - for _, m := range result.Data.Items[0].Members { - members = append(members, TeamMember{ - PersonID: m.PersonID, - PersonName: m.PersonName, - Email: m.Email, - }) + team := result.Data + + // Collect all person IDs that need enrichment: members + creator (if name missing). + enrichIDs := make([]int64, 0, len(team.PersonIDs)+1) + enrichIDs = append(enrichIDs, team.PersonIDs...) + if team.CreatorID != 0 && team.CreatorName == "" { + enrichIDs = append(enrichIDs, team.CreatorID) + } + + if len(enrichIDs) > 0 { + personMap, err := c.fetchPersonInfos(ctx, enrichIDs) + if err != nil { + c.logger.Warn("failed to enrich team members", "error", err) + } else { + members := make([]TeamMember, 0, len(team.PersonIDs)) + for _, pid := range team.PersonIDs { + if p, ok := personMap[pid]; ok { + members = append(members, TeamMember{ + PersonID: p.PersonID, + PersonName: p.PersonName, + Email: p.Email, + }) + } else { + members = append(members, TeamMember{PersonID: pid}) + } + } + team.Members = members + + if team.CreatorName == "" && team.CreatorID != 0 { + if p, ok := personMap[team.CreatorID]; ok { + team.CreatorName = p.PersonName + } + } } } - return members, nil + + return team, nil } // UpsertTeam creates or updates a team. TeamName is required by the API. diff --git a/types.go b/types.go index d208804..987a925 100644 --- a/types.go +++ b/types.go @@ -91,9 +91,10 @@ type ChannelInfo struct { // TeamInfo represents team information (used by list/batch endpoints) type TeamInfo struct { - TeamID int64 `json:"team_id" toon:"team_id"` - TeamName string `json:"team_name" toon:"team_name"` - Members []TeamMember `json:"members,omitempty" toon:"members,omitempty"` + TeamID int64 `json:"team_id" toon:"team_id"` + TeamName string `json:"team_name" toon:"team_name"` + PersonIDs []int64 `json:"person_ids,omitempty" toon:"person_ids,omitempty"` + Members []TeamMember `json:"members,omitempty" toon:"members,omitempty"` } // TeamMember represents a team member