Skip to content

Commit 164002e

Browse files
Improve JSON output for postgres create and list commands (#483)
Cleans up JSON output for `render ea pg list` and `render ea pg create` GROW-2588 GitOrigin-RevId: 022645efcf365da80a39b3b7836bf2217cb140c7
1 parent 398142b commit 164002e

8 files changed

Lines changed: 110 additions & 72 deletions

File tree

cmd/pgcreate.go

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ import (
66

77
"github.com/spf13/cobra"
88

9-
"github.com/render-oss/cli/pkg/client"
109
"github.com/render-oss/cli/pkg/command"
1110
"github.com/render-oss/cli/pkg/dependencies"
1211
"github.com/render-oss/cli/pkg/postgres"
@@ -101,11 +100,16 @@ Examples:
101100
// this gate. command.NonInteractive returns (false, nil) only when the resolved
102101
// output format is still interactive, without calling loadData.
103102
nonInteractive, err := command.NonInteractive(cmd,
104-
func() (*client.PostgresDetail, error) {
105-
return deps.PostgresService().Create(cmd.Context(), input)
103+
func() (*postgres.CreateOut, error) {
104+
resolved, err := deps.PostgresService().Create(cmd.Context(), input)
105+
if err != nil {
106+
return nil, err
107+
}
108+
out := postgres.NewPostgresCreateOut(resolved)
109+
return &out, nil
106110
},
107-
func(pg *client.PostgresDetail) string {
108-
return pgCreateSuccessMessage(pg)
111+
func(out *postgres.CreateOut) string {
112+
return pgCreateSuccessMessage(out)
109113
},
110114
)
111115
if err != nil || nonInteractive {
@@ -125,9 +129,9 @@ Examples:
125129
return cmd
126130
}
127131

128-
func pgCreateSuccessMessage(pg *client.PostgresDetail) string {
132+
func pgCreateSuccessMessage(out *postgres.CreateOut) string {
129133
return fmt.Sprintf(
130134
"Created Postgres database\n\n%s\n",
131-
text.PostgresAPIDetail(pg),
135+
text.PostgresDetail(&out.Data),
132136
)
133137
}

cmd/pgcreate_test.go

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
package cmd
22

33
import (
4-
"encoding/json"
54
"testing"
65

76
"github.com/stretchr/testify/assert"
87
"github.com/stretchr/testify/require"
98

109
renderapi "github.com/render-oss/cli/internal/fakes/renderapi"
10+
"github.com/render-oss/cli/internal/testrequire"
1111
"github.com/render-oss/cli/pkg/client"
1212
pgclient "github.com/render-oss/cli/pkg/client/postgres"
1313
)
@@ -82,16 +82,26 @@ func TestPGCreate_AllFlags(t *testing.T) {
8282

8383
func TestPGCreate_OutputJSON_IsMachineReadable(t *testing.T) {
8484
server := renderapi.NewServer(t)
85+
project := server.CreateProject(
86+
renderapi.ProjectAttrs{Name: "My Project", OwnerId: pgActiveWorkspaceID},
87+
renderapi.EnvAttrs{Name: "production"},
88+
)
89+
8590
result, err := executePGCreate(t, server,
8691
"--name", "my-pg",
92+
"--project", "My Project",
8793
"--output", "json",
8894
)
8995
require.NoError(t, err)
9096

91-
var decoded map[string]any
92-
require.NoError(t, json.Unmarshal([]byte(result.Stdout), &decoded))
93-
assert.Equal(t, "my-pg", decoded["name"])
94-
assert.NotEmpty(t, decoded["id"])
97+
body := unmarshalPGJSONOutput(t, result.Stdout)
98+
data := testrequire.SubMap(t, body, "data")
99+
assert.Equal(t, "my-pg", data["name"])
100+
assert.NotEmpty(t, data["id"])
101+
assert.Equal(t, project.Project.Id, data["projectId"])
102+
assert.Equal(t, project.Env("production").Id, data["environmentId"])
103+
testrequire.SubSlice(t, data, "ipAllowList")
104+
testrequire.SubSlice(t, data, "readReplicas")
95105
}
96106

97107
// Verifies that --confirm in interactive output mode creates without launching the wizard.

cmd/pglist.go

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -52,9 +52,16 @@ resolved within that project.`,
5252
}
5353
input = pgtypes.NormalizeListInput(input)
5454

55-
_, err := command.NonInteractive(cmd, func() ([]*postgres.Model, error) {
56-
return deps.PostgresService().List(cmd.Context(), input)
57-
}, text.PostgresTable)
55+
_, err := command.NonInteractive(cmd, func() (*postgres.PostgresListOut, error) {
56+
models, err := deps.PostgresService().List(cmd.Context(), input)
57+
if err != nil {
58+
return nil, err
59+
}
60+
out := postgres.NewPostgresListOut(models)
61+
return &out, nil
62+
}, func(out *postgres.PostgresListOut) string {
63+
return text.PostgresTable(out.Data)
64+
})
5865
return err
5966
}
6067

cmd/pglist_test.go

Lines changed: 10 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
package cmd
22

33
import (
4-
"encoding/json"
54
"testing"
65

76
"github.com/stretchr/testify/assert"
87
"github.com/stretchr/testify/require"
98

109
renderapi "github.com/render-oss/cli/internal/fakes/renderapi"
10+
"github.com/render-oss/cli/internal/testrequire"
1111
"github.com/render-oss/cli/pkg/client"
1212
)
1313

@@ -46,13 +46,8 @@ func TestPGList_NoDatabases(t *testing.T) {
4646
result, err = harness.execute("--output", "json")
4747
require.NoError(t, err)
4848

49-
var body []struct {
50-
Postgres struct {
51-
ID string `json:"id"`
52-
} `json:"postgres"`
53-
}
54-
require.NoError(t, json.Unmarshal([]byte(result.Stdout), &body))
55-
assert.Empty(t, body)
49+
body := unmarshalPGJSONOutput(t, result.Stdout)
50+
assert.Empty(t, testrequire.SubSlice(t, body, "data"))
5651
}
5752

5853
func TestPGList_MultipleDatabases(t *testing.T) {
@@ -146,20 +141,17 @@ func TestPGList_JSONOutput(t *testing.T) {
146141
result, err := harness.execute("--output", "json")
147142
require.NoError(t, err)
148143

149-
var body []struct {
150-
Postgres struct {
151-
ID string `json:"id"`
152-
Name string `json:"name"`
153-
} `json:"postgres"`
154-
}
155-
require.NoError(t, json.Unmarshal([]byte(result.Stdout), &body))
156-
require.Len(t, body, 2)
144+
body := unmarshalPGJSONOutput(t, result.Stdout)
145+
data := testrequire.SubSlice(t, body, "data")
146+
require.Len(t, data, 2)
157147

158148
// Build an _un-ordered_ object to assert against
159149
// Order is just determined by our fake render api, so not meaningful
160150
got := map[string]string{}
161-
for _, item := range body {
162-
got[item.Postgres.ID] = item.Postgres.Name
151+
for _, item := range data {
152+
itemMap, ok := item.(map[string]any)
153+
require.True(t, ok)
154+
got[itemMap["id"].(string)] = itemMap["name"].(string)
163155
}
164156
assert.Equal(t, map[string]string{
165157
pg1.Id: "json-db-one",

pkg/postgres/service.go

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,7 @@ func (s *Service) ResumePostgres(ctx context.Context, id string) error {
126126
// Create applies defaults, resolves workspace/project/environment scope,
127127
// and calls the Postgres create endpoint. The non-interactive flag path
128128
// and (eventually) the interactive wizard both go through here.
129-
func (s *Service) Create(ctx context.Context, input pgtypes.CreatePostgresInput) (*client.PostgresDetail, error) {
129+
func (s *Service) Create(ctx context.Context, input pgtypes.CreatePostgresInput) (*ResolvedPostgres, error) {
130130
scope, err := s.resolver.ResolveScope(ctx, resolve.ScopeInput{
131131
WorkspaceIDOrName: input.WorkspaceIDOrName,
132132
ProjectIDOrName: input.ProjectIDOrName,
@@ -143,13 +143,28 @@ func (s *Service) Create(ctx context.Context, input pgtypes.CreatePostgresInput)
143143
return nil, err
144144
}
145145
}
146+
env := scope.Environment
147+
if env == nil && environmentID != nil {
148+
env, err = s.environmentRepo.GetEnvironment(ctx, *environmentID)
149+
if err != nil {
150+
return nil, err
151+
}
152+
}
146153

147154
body, err := BuildCreateRequest(buildRequestInput(input, scope.WorkspaceID, environmentID))
148155
if err != nil {
149156
return nil, err
150157
}
151158

152-
return s.repo.CreatePostgres(ctx, body)
159+
created, err := s.repo.CreatePostgres(ctx, body)
160+
if err != nil {
161+
return nil, err
162+
}
163+
return &ResolvedPostgres{
164+
Postgres: created,
165+
Project: scope.Project,
166+
Environment: env,
167+
}, nil
153168
}
154169

155170
// Update resolves the target Postgres database (by ID or name, optionally

pkg/postgres/service_test.go

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -136,11 +136,11 @@ func TestServiceCreate_UsesResolvedWorkspaceAndDefaults(t *testing.T) {
136136
created, err := harness.service.Create(context.Background(), pgtypes.CreatePostgresInput{})
137137
require.NoError(t, err)
138138

139-
assert.NotEmpty(t, created.Id)
140-
assert.NotEmpty(t, created.Name)
141-
assert.Equal(t, harness.workspaceID, created.Owner.Id)
142-
assert.Equal(t, "free", string(created.Plan))
143-
assert.Equal(t, "18", string(created.Version))
139+
assert.NotEmpty(t, created.Postgres.Id)
140+
assert.NotEmpty(t, created.Postgres.Name)
141+
assert.Equal(t, harness.workspaceID, created.Postgres.Owner.Id)
142+
assert.Equal(t, "free", string(created.Postgres.Plan))
143+
assert.Equal(t, "18", string(created.Postgres.Version))
144144
assert.Len(t, harness.server.Postgres.Instances, 1)
145145
}
146146

@@ -167,8 +167,12 @@ func TestServiceCreate_AutoSelectsSingleEnvironmentWhenOnlyProjectGiven(t *testi
167167
})
168168
require.NoError(t, err)
169169

170-
require.NotNil(t, created.EnvironmentId)
171-
assert.Equal(t, env.Id, *created.EnvironmentId)
170+
require.NotNil(t, created.Postgres.EnvironmentId)
171+
assert.Equal(t, env.Id, *created.Postgres.EnvironmentId)
172+
require.NotNil(t, created.Environment)
173+
assert.Equal(t, env.Id, created.Environment.Id)
174+
require.NotNil(t, created.Project)
175+
assert.Equal(t, proj.Id, created.Project.Id)
172176
}
173177

174178
func TestServiceDelete_DeletesByID(t *testing.T) {

pkg/text/postgres.go

Lines changed: 9 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -12,35 +12,26 @@ import (
1212
"github.com/render-oss/cli/pkg/postgres"
1313
)
1414

15-
func PostgresTable(v []*postgres.Model) string {
15+
func PostgresTable(v []postgres.PostgresListItemOut) string {
1616
t := newTable()
1717
t.AppendHeader(table.Row{"Name", "Project", "Environment", "Plan", "Region", "Status", "ID"})
1818
if len(v) == 0 {
1919
t.SetCaption("No Postgres databases found.")
2020
}
21-
for _, m := range v {
21+
for _, pg := range v {
2222
t.AppendRow(table.Row{
23-
m.Name(),
24-
m.ProjectName(),
25-
m.EnvironmentName(),
26-
string(m.Postgres.Plan),
27-
string(m.Postgres.Region),
28-
string(m.Postgres.Status),
29-
m.ID(),
23+
pg.Name,
24+
pg.ProjectName,
25+
pg.EnvironmentName,
26+
string(pg.Plan),
27+
string(pg.Region),
28+
string(pg.Status),
29+
pg.Id,
3030
})
3131
}
3232
return FormatString(t.Render())
3333
}
3434

35-
// PostgresAPIDetail formats a raw API Postgres detail for text output.
36-
//
37-
// TODO(GROW-2588): delete this once all Postgres commands render from
38-
// postgres.PostgresOut.
39-
func PostgresAPIDetail(pg *client.PostgresDetail) string {
40-
out := postgres.NewPostgresGetOut(&postgres.ResolvedPostgres{Postgres: pg})
41-
return PostgresDetail(&out.Data)
42-
}
43-
4435
// PostgresDetail formats a Postgres instance detail for text output.
4536
// Does NOT include an action prefix (e.g., "Created") — callers should prepend
4637
// their own action prefix in the formatText closure passed to command.NonInteractive.

0 commit comments

Comments
 (0)