Skip to content

Commit 7b4468d

Browse files
Add Key Value output contract for list (#477)
render ea kv list -o json returns `{ "data": [...] }` instead of just `[...]` Part of GROW-2587 GitOrigin-RevId: f84cb8636feae176df06db07d8277173cd523d27
1 parent cada579 commit 7b4468d

7 files changed

Lines changed: 118 additions & 53 deletions

File tree

cmd/kvlist.go

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

33
import (
4+
"context"
5+
46
"github.com/spf13/cobra"
57

68
"github.com/render-oss/cli/pkg/client"
@@ -12,7 +14,7 @@ import (
1214
kvtypes "github.com/render-oss/cli/pkg/types/keyvalue"
1315
)
1416

15-
func newKVListCmd(_ *dependencies.Dependencies) *cobra.Command {
17+
func newKVListCmd(deps *dependencies.Dependencies) *cobra.Command {
1618
cmd := &cobra.Command{
1719
Use: "list",
1820
Short: "List Key Value store instances",
@@ -53,23 +55,31 @@ resolved within that project.`,
5355
}
5456
input = kvtypes.NormalizeListInput(input)
5557

56-
_, err := command.NonInteractive(cmd, func() ([]*keyvalue.Model, error) {
58+
_, err := command.NonInteractive(cmd, func() (*keyvalue.KeyValueListOut, error) {
5759
params := &client.ListKeyValueParams{}
5860

59-
envIDs, ok, err := resolveListEnvIDs(cmd, input)
61+
envIDs, ok, err := resolveListEnvIDs(cmd.Context(), deps, input)
6062
if err != nil {
6163
return nil, err
6264
}
6365
if ok {
6466
if len(envIDs) == 0 {
65-
return nil, nil
67+
out := keyvalue.NewKeyValueListOut(nil)
68+
return &out, nil
6669
}
6770
envParam := client.EnvironmentIdParam(envIDs)
6871
params.EnvironmentId = &envParam
6972
}
7073

71-
return keyvalue.List(cmd.Context(), params)
72-
}, text.KeyValueTable)
74+
models, err := deps.KeyValueService().ListKeyValue(cmd.Context(), params)
75+
if err != nil {
76+
return nil, err
77+
}
78+
out := keyvalue.NewKeyValueListOut(models)
79+
return &out, nil
80+
}, func(out *keyvalue.KeyValueListOut) string {
81+
return text.KeyValueTable(out.Data)
82+
})
7383
return err
7484
}
7585

@@ -79,16 +89,12 @@ resolved within that project.`,
7989
// resolveListEnvIDs translates --project/--environment selectors into the
8090
// environment IDs to filter on. The second return value is true when the
8191
// caller should apply an environment filter; false means list workspace-wide.
82-
func resolveListEnvIDs(cmd *cobra.Command, input kvtypes.KeyValueListInput) ([]string, bool, error) {
92+
func resolveListEnvIDs(ctx context.Context, deps *dependencies.Dependencies, input kvtypes.KeyValueListInput) ([]string, bool, error) {
8393
if input.ProjectIDOrName == nil && input.EnvironmentIDOrName == nil {
8494
return nil, false, nil
8595
}
8696

87-
c, err := client.NewDefaultClient()
88-
if err != nil {
89-
return nil, false, err
90-
}
91-
scope, err := resolve.NewFromClient(c).ResolveScopeInActiveWorkspace(cmd.Context(), resolve.ActiveWorkspaceScopeInput{
97+
scope, err := deps.Resolver().ResolveScopeInActiveWorkspace(ctx, resolve.ActiveWorkspaceScopeInput{
9298
ProjectIDOrName: input.ProjectIDOrName,
9399
EnvironmentIDOrName: input.EnvironmentIDOrName,
94100
})

cmd/kvlist_test.go

Lines changed: 19 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -78,21 +78,30 @@ func TestKVList_FilterByEnvironmentName(t *testing.T) {
7878

7979
func TestKVList_JSONOutput(t *testing.T) {
8080
server := renderapi.NewServer(t)
81-
kv := seedKV(server, "json-cache")
81+
proj := server.CreateProject(
82+
renderapi.ProjectAttrs{Name: "My Project", OwnerId: kvTestWorkspaceID},
83+
renderapi.EnvAttrs{Name: "production"},
84+
)
85+
first := seedKVInEnv(server, "json-cache-one", proj.Env("production").Id)
86+
second := seedKVInEnv(server, "json-cache-two", proj.Env("production").Id)
8287

8388
result, err := executeKVList(t, server, "--output", "json")
8489
require.NoError(t, err)
8590

86-
var body []struct {
87-
KeyValue struct {
88-
ID string `json:"id"`
89-
Name string `json:"name"`
90-
} `json:"keyValue"`
91-
}
91+
var body map[string]any
9292
require.NoError(t, json.Unmarshal([]byte(result.Stdout), &body))
93-
require.Len(t, body, 1)
94-
assert.Equal(t, kv.Id, body[0].KeyValue.ID)
95-
assert.Equal(t, "json-cache", body[0].KeyValue.Name)
93+
data := requireSubSlice(t, body, "data")
94+
require.Len(t, data, 2)
95+
require.IsType(t, map[string]any{}, data[0])
96+
require.IsType(t, map[string]any{}, data[1])
97+
firstItem := data[0].(map[string]any)
98+
secondItem := data[1].(map[string]any)
99+
100+
assert.Equal(t, first.Id, firstItem["id"])
101+
assert.Equal(t, "json-cache-one", firstItem["name"])
102+
assert.Equal(t, second.Id, secondItem["id"])
103+
assert.Equal(t, "json-cache-two", secondItem["name"])
104+
assert.NotContains(t, firstItem, "keyValue")
96105
}
97106

98107
func TestKVList_DefaultOutput_TreatedAsText(t *testing.T) {

cmd/testhelpers_test.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,3 +30,12 @@ func requireSubMap(t *testing.T, body map[string]any, key string) map[string]any
3030
require.IsType(t, map[string]any{}, body[key], "expected %q to contain a map", key)
3131
return body[key].(map[string]any)
3232
}
33+
34+
// requireSubSlice returns the nested slice stored at key, failing the test if
35+
// the key is absent or the value is not a []any.
36+
func requireSubSlice(t *testing.T, body map[string]any, key string) []any {
37+
t.Helper()
38+
require.Contains(t, body, key, "expected %q", key)
39+
require.IsType(t, []any{}, body[key], "expected %q to contain a slice", key)
40+
return body[key].([]any)
41+
}

internal/fakes/renderapi/server.go

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -491,6 +491,8 @@ func NewServer(t *testing.T) *Server {
491491
name := r.URL.Query().Get("name")
492492
envIDs := queryListValues(r, "environmentId")
493493
result := make([]client.KeyValueWithCursor, 0, len(s.KV.Instances))
494+
// Preserve insertion order so list tests can make deterministic
495+
// assertions about multiple resources.
494496
for i, kv := range s.KV.Instances {
495497
if name != "" && kv.Name != name {
496498
continue
@@ -503,9 +505,18 @@ func NewServer(t *testing.T) *Server {
503505
result = append(result, client.KeyValueWithCursor{
504506
Cursor: client.Cursor(fmt.Sprintf("c%d", i)),
505507
KeyValue: client.KeyValue{
508+
CreatedAt: kv.CreatedAt,
509+
EnvironmentId: kv.EnvironmentId,
506510
Id: kv.Id,
511+
IpAllowList: kv.IpAllowList,
507512
Name: kv.Name,
508-
EnvironmentId: kv.EnvironmentId,
513+
Options: kv.Options,
514+
Owner: kv.Owner,
515+
Plan: kv.Plan,
516+
Region: kv.Region,
517+
Status: kv.Status,
518+
UpdatedAt: kv.UpdatedAt,
519+
Version: kv.Version,
509520
},
510521
})
511522
}

pkg/keyvalue/list.go

Lines changed: 0 additions & 21 deletions
This file was deleted.

pkg/keyvalue/output.go

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,10 @@ type KeyValueGetOut struct {
3333

3434
type KeyValueCreateOut = KeyValueGetOut
3535

36+
type KeyValueListOut struct {
37+
Data []KeyValueOut `json:"data"`
38+
}
39+
3640
type DeleteOut struct {
3741
Data KeyValueOut `json:"data"`
3842
Meta DeleteOutMeta `json:"meta"`
@@ -110,3 +114,50 @@ func NewKeyValueOut(resolved *ResolvedKeyValue) KeyValueOut {
110114
}
111115
return out
112116
}
117+
118+
func NewKeyValueListOut(models []*Model) KeyValueListOut {
119+
data := make([]KeyValueOut, 0, len(models))
120+
for _, model := range models {
121+
data = append(data, NewKeyValueOutFromModel(model))
122+
}
123+
return KeyValueListOut{Data: data}
124+
}
125+
126+
func NewKeyValueOutFromModel(model *Model) KeyValueOut {
127+
if model == nil || model.KeyValue == nil {
128+
return KeyValueOut{}
129+
}
130+
131+
kv := model.KeyValue
132+
out := KeyValueOut{
133+
ID: kv.Id,
134+
Name: kv.Name,
135+
Plan: kv.Plan,
136+
Region: kv.Region,
137+
Status: kv.Status,
138+
CreatedAt: kv.CreatedAt,
139+
UpdatedAt: kv.UpdatedAt,
140+
Version: kv.Version,
141+
OwnerID: kv.Owner.Id,
142+
OwnerType: kv.Owner.Type,
143+
IPAllowList: kv.IpAllowList,
144+
}
145+
if out.IPAllowList == nil {
146+
out.IPAllowList = []client.CidrBlockAndDescription{}
147+
}
148+
if kv.Options.MaxmemoryPolicy != nil {
149+
out.MaxmemoryPolicy = kv.Options.MaxmemoryPolicy
150+
}
151+
if kv.EnvironmentId != nil {
152+
out.EnvironmentID = kv.EnvironmentId
153+
}
154+
if model.Environment != nil {
155+
out.EnvironmentID = &model.Environment.Id
156+
out.EnvironmentName = model.Environment.Name
157+
}
158+
if model.Project != nil {
159+
out.ProjectID = &model.Project.Id
160+
out.ProjectName = model.Project.Name
161+
}
162+
return out
163+
}

pkg/text/keyvalue.go

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -10,18 +10,18 @@ import (
1010
"github.com/render-oss/cli/pkg/keyvalue"
1111
)
1212

13-
func KeyValueTable(v []*keyvalue.Model) string {
13+
func KeyValueTable(v []keyvalue.KeyValueOut) string {
1414
t := newTable()
1515
t.AppendHeader(table.Row{"Name", "Project", "Environment", "Plan", "Region", "Status", "ID"})
16-
for _, m := range v {
16+
for _, kv := range v {
1717
t.AppendRow(table.Row{
18-
m.Name(),
19-
m.ProjectName(),
20-
m.EnvironmentName(),
21-
string(m.KeyValue.Plan),
22-
string(m.KeyValue.Region),
23-
string(m.KeyValue.Status),
24-
m.ID(),
18+
kv.Name,
19+
kv.ProjectName,
20+
kv.EnvironmentName,
21+
string(kv.Plan),
22+
string(kv.Region),
23+
string(kv.Status),
24+
kv.ID,
2525
})
2626
}
2727
return FormatString(t.Render())

0 commit comments

Comments
 (0)