From 6a784b18660064db027b34ee0446f348a0b7690f Mon Sep 17 00:00:00 2001 From: Cory O'Daniel Date: Wed, 6 Nov 2024 11:16:26 -0800 Subject: [PATCH] adding project list support --- cmd/project.go | 60 ++++++++++++ docs/helpdocs/project.md | 5 + docs/helpdocs/project/list.md | 3 + pkg/api/genqlient.graphql | 10 +- pkg/api/project.go | 25 +++++ pkg/api/project_test.go | 31 +++++++ pkg/api/zz_generated.go | 170 ++++++++++++++++++++++++++++++++++ 7 files changed, 301 insertions(+), 3 deletions(-) create mode 100644 cmd/project.go create mode 100644 docs/helpdocs/project.md create mode 100644 docs/helpdocs/project/list.md diff --git a/cmd/project.go b/cmd/project.go new file mode 100644 index 0000000..173c18d --- /dev/null +++ b/cmd/project.go @@ -0,0 +1,60 @@ +package cmd + +import ( + "fmt" + "os" + "text/tabwriter" + + "github.com/massdriver-cloud/mass/docs/helpdocs" + "github.com/massdriver-cloud/mass/pkg/api" + "github.com/massdriver-cloud/mass/pkg/config" + "github.com/spf13/cobra" +) + +var projCmdHelp = helpdocs.MustRender("project") +var projListCmdHelp = helpdocs.MustRender("project/list") +var projCmd = &cobra.Command{ + Use: "project", + Aliases: []string{"prj"}, + Short: "Manage Projects", + Long: projCmdHelp, +} + +var projListCmd = &cobra.Command{ + Use: `list`, + Short: "List projects", + Aliases: []string{"ls"}, + Long: projListCmdHelp, + RunE: runProjList, +} + +func init() { + rootCmd.AddCommand(projCmd) + projCmd.AddCommand(projListCmd) +} + +func runProjList(cmd *cobra.Command, args []string) error { + config, configErr := config.Get() + if configErr != nil { + return configErr + } + + client := api.NewClient(config.URL, config.APIKey) + + projects, err := api.ListProjects(client, config.OrgID) + + w := tabwriter.NewWriter(os.Stdout, 10, 1, 5, ' ', 0) + fmt.Fprintln(w, "ID\tNAME\tSLUG") + + for _, project := range *projects { + line := fmt.Sprintf("%s\t%s\t%s", project.ID, project.Name, project.Slug) + fmt.Fprintln(w, line) + } + + w.Flush() + + // TODO: present UI + // _, err := commands.DeployPackage(client, config.OrgID, name) + + return err +} diff --git a/docs/helpdocs/project.md b/docs/helpdocs/project.md new file mode 100644 index 0000000..606a552 --- /dev/null +++ b/docs/helpdocs/project.md @@ -0,0 +1,5 @@ +# Manage Projects + +[Projects](https://docs.massdriver.cloud/concepts/projects) act as permission and replication boundaries in Massdriver. + +A project can encompass many environments (permanent or ephemeral) and manages the parity across those environments. diff --git a/docs/helpdocs/project/list.md b/docs/helpdocs/project/list.md new file mode 100644 index 0000000..4bd03d4 --- /dev/null +++ b/docs/helpdocs/project/list.md @@ -0,0 +1,3 @@ +# List Projects + +Lists Massdriver projects. diff --git a/pkg/api/genqlient.graphql b/pkg/api/genqlient.graphql index 53a64d5..e7bc31f 100644 --- a/pkg/api/genqlient.graphql +++ b/pkg/api/genqlient.graphql @@ -8,11 +8,15 @@ query getArtifactsByType($organizationId: ID!, $artifactType: String!) { } } +query projects($organizationId: ID!){ + projects(organizationId: $organizationId){ + id, name, defaultParams, slug, description + } +} + query getProjectById($organizationId: ID!, $id: ID!) { project(organizationId: $organizationId, id: $id) { - id - defaultParams - slug + id, name, defaultParams, slug, description } } diff --git a/pkg/api/project.go b/pkg/api/project.go index c17d201..79f0945 100644 --- a/pkg/api/project.go +++ b/pkg/api/project.go @@ -10,7 +10,9 @@ import ( type Project struct { ID string + Name string Slug string + Description string DefaultParams map[string]interface{} } @@ -23,11 +25,34 @@ func GetProject(client graphql.Client, orgID string, idOrSlug string) (*Project, func (p *getProjectByIdProject) toProject() *Project { return &Project{ ID: p.Id, + Name: p.Name, Slug: p.Slug, + Description: p.Description, DefaultParams: p.DefaultParams, } } +func (p *projectsProjectsProject) toProject() Project { + return Project{ + ID: p.Id, + Slug: p.Slug, + Name: p.Name, + Description: p.Description, + DefaultParams: p.DefaultParams, + } +} + +func ListProjects(client graphql.Client, orgID string) (*[]Project, error) { + response, err := projects(context.Background(), client, orgID) + records := []Project{} + + for _, prj := range response.Projects { + records = append(records, prj.toProject()) + } + + return &records, err +} + func (p *Project) GetDefaultParams() map[string]PreviewPackage { packages := make(map[string]PreviewPackage) diff --git a/pkg/api/project_test.go b/pkg/api/project_test.go index b77d275..5408be5 100644 --- a/pkg/api/project_test.go +++ b/pkg/api/project_test.go @@ -34,3 +34,34 @@ func TestGetProject(t *testing.T) { t.Errorf("got %s, wanted %s", got, want) } } + +func TestListProjects(t *testing.T) { + client := gqlmock.NewClientWithSingleJSONResponse(map[string]interface{}{ + "data": map[string]interface{}{ + "projects": []map[string]interface{}{ + { + "id": "uuid1", + "name": "project1", + }, + { + "id": "uuid2", + "name": "project2", + }, + }, + }, + }) + + projects, err := api.ListProjects(client, "faux-org-id") + + if err != nil { + t.Fatal(err) + } + + got := len(*projects) + + want := 2 + + if got != want { + t.Errorf("got %d, wanted %d", got, want) + } +} diff --git a/pkg/api/zz_generated.go b/pkg/api/zz_generated.go index 52e0c0c..04d0002 100644 --- a/pkg/api/zz_generated.go +++ b/pkg/api/zz_generated.go @@ -514,6 +514,14 @@ func (v *__getProjectByIdInput) GetOrganizationId() string { return v.Organizati // GetId returns __getProjectByIdInput.Id, and is useful for accessing the field via an interface. func (v *__getProjectByIdInput) GetId() string { return v.Id } +// __projectsInput is used internally by genqlient +type __projectsInput struct { + OrganizationId string `json:"organizationId"` +} + +// GetOrganizationId returns __projectsInput.OrganizationId, and is useful for accessing the field via an interface. +func (v *__projectsInput) GetOrganizationId() string { return v.OrganizationId } + // configurePackageConfigurePackagePackagePayload includes the requested fields of the GraphQL type PackagePayload. type configurePackageConfigurePackagePackagePayload struct { // The object created/updated/deleted by the mutation. May be null if mutation failed. @@ -1232,19 +1240,27 @@ func (v *getPackageByNamingConventionResponse) GetGetPackageByNamingConvention() // getProjectByIdProject includes the requested fields of the GraphQL type Project. type getProjectByIdProject struct { Id string `json:"id"` + Name string `json:"name"` DefaultParams map[string]interface{} `json:"-"` Slug string `json:"slug"` + Description string `json:"description"` } // GetId returns getProjectByIdProject.Id, and is useful for accessing the field via an interface. func (v *getProjectByIdProject) GetId() string { return v.Id } +// GetName returns getProjectByIdProject.Name, and is useful for accessing the field via an interface. +func (v *getProjectByIdProject) GetName() string { return v.Name } + // GetDefaultParams returns getProjectByIdProject.DefaultParams, and is useful for accessing the field via an interface. func (v *getProjectByIdProject) GetDefaultParams() map[string]interface{} { return v.DefaultParams } // GetSlug returns getProjectByIdProject.Slug, and is useful for accessing the field via an interface. func (v *getProjectByIdProject) GetSlug() string { return v.Slug } +// GetDescription returns getProjectByIdProject.Description, and is useful for accessing the field via an interface. +func (v *getProjectByIdProject) GetDescription() string { return v.Description } + func (v *getProjectByIdProject) UnmarshalJSON(b []byte) error { if string(b) == "null" { @@ -1281,9 +1297,13 @@ func (v *getProjectByIdProject) UnmarshalJSON(b []byte) error { type __premarshalgetProjectByIdProject struct { Id string `json:"id"` + Name string `json:"name"` + DefaultParams json.RawMessage `json:"defaultParams"` Slug string `json:"slug"` + + Description string `json:"description"` } func (v *getProjectByIdProject) MarshalJSON() ([]byte, error) { @@ -1298,6 +1318,7 @@ func (v *getProjectByIdProject) __premarshalJSON() (*__premarshalgetProjectByIdP var retval __premarshalgetProjectByIdProject retval.Id = v.Id + retval.Name = v.Name { dst := &retval.DefaultParams @@ -1311,6 +1332,7 @@ func (v *getProjectByIdProject) __premarshalJSON() (*__premarshalgetProjectByIdP } } retval.Slug = v.Slug + retval.Description = v.Description return &retval, nil } @@ -1322,6 +1344,113 @@ type getProjectByIdResponse struct { // GetProject returns getProjectByIdResponse.Project, and is useful for accessing the field via an interface. func (v *getProjectByIdResponse) GetProject() getProjectByIdProject { return v.Project } +// projectsProjectsProject includes the requested fields of the GraphQL type Project. +type projectsProjectsProject struct { + Id string `json:"id"` + Name string `json:"name"` + DefaultParams map[string]interface{} `json:"-"` + Slug string `json:"slug"` + Description string `json:"description"` +} + +// GetId returns projectsProjectsProject.Id, and is useful for accessing the field via an interface. +func (v *projectsProjectsProject) GetId() string { return v.Id } + +// GetName returns projectsProjectsProject.Name, and is useful for accessing the field via an interface. +func (v *projectsProjectsProject) GetName() string { return v.Name } + +// GetDefaultParams returns projectsProjectsProject.DefaultParams, and is useful for accessing the field via an interface. +func (v *projectsProjectsProject) GetDefaultParams() map[string]interface{} { return v.DefaultParams } + +// GetSlug returns projectsProjectsProject.Slug, and is useful for accessing the field via an interface. +func (v *projectsProjectsProject) GetSlug() string { return v.Slug } + +// GetDescription returns projectsProjectsProject.Description, and is useful for accessing the field via an interface. +func (v *projectsProjectsProject) GetDescription() string { return v.Description } + +func (v *projectsProjectsProject) UnmarshalJSON(b []byte) error { + + if string(b) == "null" { + return nil + } + + var firstPass struct { + *projectsProjectsProject + DefaultParams json.RawMessage `json:"defaultParams"` + graphql.NoUnmarshalJSON + } + firstPass.projectsProjectsProject = v + + err := json.Unmarshal(b, &firstPass) + if err != nil { + return err + } + + { + dst := &v.DefaultParams + src := firstPass.DefaultParams + if len(src) != 0 && string(src) != "null" { + err = scalars.UnmarshalJSON( + src, dst) + if err != nil { + return fmt.Errorf( + "unable to unmarshal projectsProjectsProject.DefaultParams: %w", err) + } + } + } + return nil +} + +type __premarshalprojectsProjectsProject struct { + Id string `json:"id"` + + Name string `json:"name"` + + DefaultParams json.RawMessage `json:"defaultParams"` + + Slug string `json:"slug"` + + Description string `json:"description"` +} + +func (v *projectsProjectsProject) MarshalJSON() ([]byte, error) { + premarshaled, err := v.__premarshalJSON() + if err != nil { + return nil, err + } + return json.Marshal(premarshaled) +} + +func (v *projectsProjectsProject) __premarshalJSON() (*__premarshalprojectsProjectsProject, error) { + var retval __premarshalprojectsProjectsProject + + retval.Id = v.Id + retval.Name = v.Name + { + + dst := &retval.DefaultParams + src := v.DefaultParams + var err error + *dst, err = scalars.MarshalJSON( + &src) + if err != nil { + return nil, fmt.Errorf( + "unable to marshal projectsProjectsProject.DefaultParams: %w", err) + } + } + retval.Slug = v.Slug + retval.Description = v.Description + return &retval, nil +} + +// projectsResponse is returned by projects on success. +type projectsResponse struct { + Projects []projectsProjectsProject `json:"projects"` +} + +// GetProjects returns projectsResponse.Projects, and is useful for accessing the field via an interface. +func (v *projectsResponse) GetProjects() []projectsProjectsProject { return v.Projects } + // The query or mutation executed by configurePackage. const configurePackage_Operation = ` mutation configurePackage ($organizationId: ID!, $targetId: ID!, $manifestId: ID!, $params: JSON!) { @@ -1778,8 +1907,10 @@ const getProjectById_Operation = ` query getProjectById ($organizationId: ID!, $id: ID!) { project(organizationId: $organizationId, id: $id) { id + name defaultParams slug + description } } ` @@ -1811,3 +1942,42 @@ func getProjectById( return &data, err } + +// The query or mutation executed by projects. +const projects_Operation = ` +query projects ($organizationId: ID!) { + projects(organizationId: $organizationId) { + id + name + defaultParams + slug + description + } +} +` + +func projects( + ctx context.Context, + client graphql.Client, + organizationId string, +) (*projectsResponse, error) { + req := &graphql.Request{ + OpName: "projects", + Query: projects_Operation, + Variables: &__projectsInput{ + OrganizationId: organizationId, + }, + } + var err error + + var data projectsResponse + resp := &graphql.Response{Data: &data} + + err = client.MakeRequest( + ctx, + req, + resp, + ) + + return &data, err +}