Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
16 changes: 16 additions & 0 deletions github/issue_pr_utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,7 @@ func appendIssueColumnIncludes(m *map[string]interface{}, cols []string) {
(*m)["includeIssueNodeId"] = githubv4.Boolean(slices.Contains(cols, "node_id"))
(*m)["includeIssueId"] = githubv4.Boolean(slices.Contains(cols, "id"))
(*m)["includeIssueIsReadByUser"] = githubv4.Boolean(slices.Contains(cols, "is_read_by_user"))
(*m)["includeIssueProjectItems"] = githubv4.Boolean(slices.Contains(cols, "project_items"))
}

func issueHydrateIsReadByUser(_ context.Context, _ *plugin.QueryData, h *plugin.HydrateData) (interface{}, error) {
Expand Down Expand Up @@ -472,6 +473,21 @@ func issueHydrateLabels(_ context.Context, _ *plugin.QueryData, h *plugin.Hydrat
return issue.Labels.Nodes, nil
}

func issueHydrateProjectItems(_ context.Context, _ *plugin.QueryData, h *plugin.HydrateData) (interface{}, error) {
issue, err := extractIssueFromHydrateItem(h)
if err != nil {
return nil, err
}
if len(issue.ProjectItems.Nodes) == 0 {
return nil, nil
}
nodeIds := make([]string, len(issue.ProjectItems.Nodes))
for i, item := range issue.ProjectItems.Nodes {
nodeIds[i] = item.Project.Id
}
return nodeIds, nil
}

func extractIssueCommentFromHydrateItem(h *plugin.HydrateData) (models.IssueComment, error) {
if issueComment, ok := h.Item.(models.IssueComment); ok {
return issueComment, nil
Expand Down
8 changes: 8 additions & 0 deletions github/models/issue.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,14 @@ type Issue struct {
TotalCount int
Nodes []BaseUser
} `graphql:"assignees(first: 10) @include(if:$includeIssueAssignees)" json:"assignees"`
ProjectItems struct {
TotalCount int
Nodes []struct {
Project struct {
Id string `graphql:"id" json:"id"`
} `json:"project"`
}
} `graphql:"projectItems(first: 100) @include(if:$includeIssueProjectItems)" json:"project_items"`
}

type IssueTemplate struct {
Expand Down
46 changes: 46 additions & 0 deletions github/models/project_v2.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package models

// ProjectV2Owner represents the owner of a project, which can be an Organization or User.
type ProjectV2Owner struct {
TypeName string `graphql:"type: __typename" json:"type"`
Organization struct {
Id int `graphql:"id: databaseId" json:"id"`
Login string `json:"login"`
} `graphql:"... on Organization" json:"organization,omitempty"`
User struct {
Id int `graphql:"id: databaseId" json:"id"`
Login string `json:"login"`
} `graphql:"... on User" json:"user,omitempty"`
}

// ProjectV2StatusUpdate represents a single status update on a project.
type ProjectV2StatusUpdate struct {
Id string `graphql:"id: fullDatabaseId" json:"id"`
NodeId string `graphql:"nodeId: id" json:"node_id"`
Status string `json:"status"`
Body string `json:"body"`
StartDate string `json:"start_date"`
TargetDate string `json:"target_date"`
CreatedAt NullableTime `json:"created_at"`
UpdatedAt NullableTime `json:"updated_at"`
Creator Actor `json:"creator"`
}

type ProjectV2 struct {
Id string `graphql:"id: fullDatabaseId @include(if:$includeId)" json:"id"`
NodeId string `graphql:"nodeId: id @include(if:$includeNodeId)" json:"node_id"`
Number int `json:"number"`
Owner ProjectV2Owner `graphql:"owner @include(if:$includeOwner)" json:"owner,omitempty"`
Creator Actor `graphql:"creator @include(if:$includeCreator)" json:"creator,omitempty"`
Title string `graphql:"title @include(if:$includeTitle)" json:"title"`
Description string `graphql:"description: shortDescription @include(if:$includeDescription)" json:"description"`
IsPublic bool `graphql:"public @include(if:$includeIsPublic)" json:"public"`
ClosedAt NullableTime `graphql:"closedAt @include(if:$includeClosedAt)" json:"closed_at"`
CreatedAt NullableTime `graphql:"createdAt @include(if:$includeCreatedAt)" json:"created_at"`
UpdatedAt NullableTime `graphql:"updatedAt @include(if:$includeUpdatedAt)" json:"updated_at"`
Closed bool `graphql:"closed @include(if:$includeState)" json:"closed"`
LatestStatusUpdate struct {
Nodes []ProjectV2StatusUpdate
} `graphql:"statusUpdates(last: 1) @include(if:$includeLatestStatusUpdate)" json:"latest_status_update"`
IsTemplate bool `graphql:"template @include(if:$includeIsTemplate)" json:"template"`
}
1 change: 1 addition & 0 deletions github/plugin.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ func Plugin(ctx context.Context) *plugin.Plugin {
"github_organization_collaborator": tableGitHubOrganizationCollaborator(),
"github_package": tableGitHubPackage(),
"github_package_version": tableGitHubPackageVersion(),
"github_project_v2": tableGitHubProjectV2(),
"github_pull_request": tableGitHubPullRequest(),
"github_pull_request_comment": tableGitHubPullRequestComment(),
"github_pull_request_review": tableGitHubPullRequestReview(),
Expand Down
146 changes: 146 additions & 0 deletions github/project_v2_utils.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
package github

import (
"context"
"fmt"
"slices"

"github.com/shurcooL/githubv4"
"github.com/turbot/steampipe-plugin-github/github/models"
"github.com/turbot/steampipe-plugin-sdk/v5/plugin"
)

func extractProjectV2FromHydrateItem(h *plugin.HydrateData) (models.ProjectV2, error) {
if project, ok := h.Item.(models.ProjectV2); ok {
return project, nil
} else {
return models.ProjectV2{}, fmt.Errorf("unable to parse hydrate item %v as a ProjectV2", h.Item)
}
}
func appendProjectV2ColumnIncludes(m *map[string]interface{}, cols []string) {
(*m)["includeId"] = githubv4.Boolean(slices.Contains(cols, "id"))
(*m)["includeNodeId"] = githubv4.Boolean(slices.Contains(cols, "node_id"))
(*m)["includeOwner"] = githubv4.Boolean(slices.Contains(cols, "owner"))
(*m)["includeCreator"] = githubv4.Boolean(slices.Contains(cols, "creator"))
(*m)["includeTitle"] = githubv4.Boolean(slices.Contains(cols, "title"))
(*m)["includeDescription"] = githubv4.Boolean(slices.Contains(cols, "description"))
(*m)["includeIsPublic"] = githubv4.Boolean(slices.Contains(cols, "is_public"))
(*m)["includeClosedAt"] = githubv4.Boolean(slices.Contains(cols, "closed_at"))
(*m)["includeCreatedAt"] = githubv4.Boolean(slices.Contains(cols, "created_at"))
(*m)["includeUpdatedAt"] = githubv4.Boolean(slices.Contains(cols, "updated_at"))
(*m)["includeState"] = githubv4.Boolean(slices.Contains(cols, "state"))
(*m)["includeLatestStatusUpdate"] = githubv4.Boolean(slices.Contains(cols, "latest_status_update"))
(*m)["includeIsTemplate"] = githubv4.Boolean(slices.Contains(cols, "is_template"))
}

func projectV2HydrateId(_ context.Context, _ *plugin.QueryData, h *plugin.HydrateData) (interface{}, error) {
project, err := extractProjectV2FromHydrateItem(h)
if err != nil {
return nil, err
}
return project.Id, nil
}

func projectV2HydrateNodeId(_ context.Context, _ *plugin.QueryData, h *plugin.HydrateData) (interface{}, error) {
project, err := extractProjectV2FromHydrateItem(h)
if err != nil {
return nil, err
}
return project.NodeId, nil
}

func projectV2HydrateOwner(_ context.Context, _ *plugin.QueryData, h *plugin.HydrateData) (interface{}, error) {
project, err := extractProjectV2FromHydrateItem(h)
if err != nil {
return nil, err
}
return project.Owner, nil
}

func projectV2HydrateCreator(_ context.Context, _ *plugin.QueryData, h *plugin.HydrateData) (interface{}, error) {
project, err := extractProjectV2FromHydrateItem(h)
if err != nil {
return nil, err
}
return project.Creator, nil
}

func projectV2HydrateTitle(_ context.Context, _ *plugin.QueryData, h *plugin.HydrateData) (interface{}, error) {
project, err := extractProjectV2FromHydrateItem(h)
if err != nil {
return nil, err
}
return project.Title, nil
}

func projectV2HydrateDescription(_ context.Context, _ *plugin.QueryData, h *plugin.HydrateData) (interface{}, error) {
project, err := extractProjectV2FromHydrateItem(h)
if err != nil {
return nil, err
}
return project.Description, nil
}

func projectV2HydrateIsPublic(_ context.Context, _ *plugin.QueryData, h *plugin.HydrateData) (interface{}, error) {
project, err := extractProjectV2FromHydrateItem(h)
if err != nil {
return nil, err
}
return project.IsPublic, nil
}

func projectV2HydrateClosedAt(_ context.Context, _ *plugin.QueryData, h *plugin.HydrateData) (interface{}, error) {
project, err := extractProjectV2FromHydrateItem(h)
if err != nil {
return nil, err
}
return project.ClosedAt, nil
}

func projectV2HydrateCreatedAt(_ context.Context, _ *plugin.QueryData, h *plugin.HydrateData) (interface{}, error) {
project, err := extractProjectV2FromHydrateItem(h)
if err != nil {
return nil, err
}
return project.CreatedAt, nil
}

func projectV2HydrateUpdatedAt(_ context.Context, _ *plugin.QueryData, h *plugin.HydrateData) (interface{}, error) {
project, err := extractProjectV2FromHydrateItem(h)
if err != nil {
return nil, err
}
return project.UpdatedAt, nil
}

// projectV2HydrateState derives the REST-compatible "open"/"closed" state string from the GraphQL boolean.
func projectV2HydrateState(_ context.Context, _ *plugin.QueryData, h *plugin.HydrateData) (interface{}, error) {
project, err := extractProjectV2FromHydrateItem(h)
if err != nil {
return nil, err
}
if project.Closed {
return "closed", nil
}
return "open", nil
}

// projectV2HydrateLatestStatusUpdate returns the most recent status update from the statusUpdates connection.
func projectV2HydrateLatestStatusUpdate(_ context.Context, _ *plugin.QueryData, h *plugin.HydrateData) (interface{}, error) {
project, err := extractProjectV2FromHydrateItem(h)
if err != nil {
return nil, err
}
if len(project.LatestStatusUpdate.Nodes) > 0 {
return project.LatestStatusUpdate.Nodes[0], nil
}
return nil, nil
}

func projectV2HydrateIsTemplate(_ context.Context, _ *plugin.QueryData, h *plugin.HydrateData) (interface{}, error) {
project, err := extractProjectV2FromHydrateItem(h)
if err != nil {
return nil, err
}
return project.IsTemplate, nil
}
1 change: 1 addition & 0 deletions github/table_github_issue.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ func sharedIssueColumns() []*plugin.Column {
{Name: "user_did_author", Type: proto.ColumnType_BOOL, Hydrate: issueHydrateUserDidAuthor, Transform: transform.FromValue(), Description: "If true, user authored the issue."},
{Name: "user_subscription", Type: proto.ColumnType_STRING, Hydrate: issueHydrateUserSubscription, Transform: transform.FromValue(), Description: "Subscription state of the user to the issue."},
{Name: "assignees", Type: proto.ColumnType_JSON, Hydrate: issueHydrateAssignees, Transform: transform.FromValue().NullIfZero(), Description: "A list of Users assigned to the issue."},
{Name: "project_items", Type: proto.ColumnType_JSON, Hydrate: issueHydrateProjectItems, Transform: transform.FromValue().NullIfZero(), Description: "A list of project node IDs (PVT_...) for ProjectV2 projects the issue belongs to."},

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
{Name: "project_items", Type: proto.ColumnType_JSON, Hydrate: issueHydrateProjectItems, Transform: transform.FromValue().NullIfZero(), Description: "A list of project node IDs (PVT_...) for ProjectV2 projects the issue belongs to."},
{Name: "projects_v2", Type: proto.ColumnType_JSON, Hydrate: issueHydrateProjectItems, Transform: transform.FromValue().NullIfZero(), Description: "A list of ProjectV2 projects associated with the issue."},

For this column, similar to assignees and labels, I think we should return more project data, including its scalar fields as part of the project object. So we can reuse the ProjectV2 model you already defined and include its scalar fields:

  • id
  • node_id
  • number
  • title
  • description
  • state
  • is_public
  • created_at
  • updated_at
  • closed_at

Notably, we should skip the pageable connections (items/fields/views), and @include-gate anything heavy like latest_status_update. Users can still join on node_id, but now get titles/state without one.

Can you please also add a projects_v2_total_count column with the description Count of ProjectV2 projects associated with the issue.? The model already fetches ProjectItems.TotalCount, so it's basically free, and it matches the existing projects_v2_total_count columns in the github_organization and github_team tables.

}
}

Expand Down
Loading