Skip to content

Commit be8b9e4

Browse files
Allow mutations in the api command (#419)
1 parent 8a132d7 commit be8b9e4

3 files changed

Lines changed: 20 additions & 71 deletions

File tree

internal/cmd/api/README.md

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,16 @@
11
# `spacectl api`
22

3-
`spacectl api` lets you run ad-hoc read-only GraphQL queries against the Spacelift API using your existing authentication.
3+
`spacectl api` lets you run ad-hoc GraphQL operations against the Spacelift API using your existing authentication.
44

5-
Mutations are not supported. For write operations, use the dedicated `spacectl` subcommands or the [Spacelift Terraform Provider](https://github.com/spacelift-io/terraform-provider-spacelift).
5+
The provided document is sent unchanged to the API, so queries and mutations are both supported.
66

77
## Usage
88

9-
Basic queries (bare field selections are wrapped in `query { ... }` automatically):
9+
Basic queries:
1010

1111
```bash
12-
spacectl api 'stacks { id name state }'
13-
spacectl api 'workerPools { id name workers { id } }'
12+
spacectl api '{ stacks { id name state } }'
13+
spacectl api '{ workerPools { id name workers { id } } }'
1414
```
1515

1616
Full query syntax:
@@ -25,12 +25,20 @@ With variables:
2525
spacectl api --variables '{"id":"my-stack"}' 'query($id: ID!) { stack(id: $id) { id name } }'
2626
```
2727

28+
Mutations:
29+
30+
```bash
31+
spacectl api 'mutation { stackDelete(id: "my-stack") { id } }'
32+
spacectl api 'mutation DeleteStack($id: ID!) { stackDelete(id: $id) { id } }' --variables '{"id":"my-stack"}'
33+
```
34+
2835
From a file or stdin:
2936

3037
```bash
3138
spacectl api < query.graphql
3239
spacectl api --variables '{"id":"my-stack"}' < query.graphql
3340
cat query.graphql | spacectl api
41+
spacectl api < mutation.graphql
3442
```
3543

3644
## Output

internal/cmd/api/api.go

Lines changed: 6 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -44,8 +44,8 @@ func Command() cmd.Command {
4444
Command: &cli.Command{
4545
Before: authenticated.Ensure,
4646
Flags: []cli.Flag{flagVariables, flagRaw, flagSchema},
47-
Description: "Pass a read-only GraphQL query as a positional argument, or pipe via stdin.\nBare field selections are wrapped as query { ... } automatically.\nMutations and subscriptions are not supported. Read more: https://github.com/spacelift-io/spacectl/tree/main/internal/cmd/api/README.md",
48-
ArgsUsage: "[query]",
47+
Description: "Pass a GraphQL document as a positional argument, or pipe via stdin.\nThe provided document is sent unchanged to the Spacelift GraphQL API. Read more: https://github.com/spacelift-io/spacectl/tree/main/internal/cmd/api/README.md",
48+
ArgsUsage: "[document]",
4949
Action: run,
5050
},
5151
},
@@ -58,25 +58,16 @@ type apiRequest struct {
5858
Variables map[string]any `json:"variables,omitempty"`
5959
}
6060

61-
var errMutationsNotAllowed = errors.New("mutations are not supported by spacectl api")
62-
6361
func run(ctx context.Context, cliCmd *cli.Command) error {
6462
var query string
6563
if cliCmd.Bool(flagSchema.Name) {
6664
query = introspectionQuery
6765
} else {
6866
var err error
69-
query, err = resolveQuery(cliCmd)
67+
query, err = resolveDocument(cliCmd)
7068
if err != nil {
7169
return err
7270
}
73-
74-
// This is a hopefull check but it's enough for most cases.
75-
// We could in theory allow mutations, but spacectl is not a great tool for
76-
// that so we do not.
77-
if isMutation(query) {
78-
return errMutationsNotAllowed
79-
}
8071
}
8172

8273
variables, err := parseVariables(cliCmd.String(flagVariables.Name))
@@ -124,9 +115,9 @@ func run(ctx context.Context, cliCmd *cli.Command) error {
124115
return nil
125116
}
126117

127-
func resolveQuery(cliCmd *cli.Command) (string, error) {
118+
func resolveDocument(cliCmd *cli.Command) (string, error) {
128119
if args := strings.TrimSpace(strings.Join(cliCmd.Args().Slice(), " ")); args != "" {
129-
return normalizeQuery(args), nil
120+
return args, nil
130121
}
131122

132123
if !isatty.IsTerminal(os.Stdin.Fd()) {
@@ -139,7 +130,7 @@ func resolveQuery(cliCmd *cli.Command) (string, error) {
139130
}
140131
}
141132

142-
return "", errors.New("query required: pass as argument or pipe via stdin")
133+
return "", errors.New("document required: pass as argument or pipe via stdin")
143134
}
144135

145136
func parseVariables(raw string) (map[string]any, error) {
@@ -153,19 +144,6 @@ func parseVariables(raw string) (map[string]any, error) {
153144
return obj, nil
154145
}
155146

156-
func normalizeQuery(query string) string {
157-
lower := strings.ToLower(query)
158-
if strings.HasPrefix(lower, "query") || strings.HasPrefix(lower, "mutation") || strings.HasPrefix(lower, "subscription") || strings.HasPrefix(query, "{") {
159-
return query
160-
}
161-
return "query { " + query + " }"
162-
}
163-
164-
func isMutation(query string) bool {
165-
lower := strings.ToLower(strings.TrimSpace(query))
166-
return strings.HasPrefix(lower, "mutation")
167-
}
168-
169147
func graphqlErrors(body []byte) string {
170148
var resp struct {
171149
Errors []struct {

internal/cmd/api/api_test.go

Lines changed: 1 addition & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,6 @@
11
package api
22

3-
import (
4-
"testing"
5-
)
6-
7-
func TestNormalizeQuery(t *testing.T) {
8-
tests := []struct {
9-
in, want string
10-
}{
11-
{"{ viewer { id } }", "{ viewer { id } }"},
12-
{"query { viewer { id } }", "query { viewer { id } }"},
13-
{"mutation { deleteStack(id: \"x\") { id } }", "mutation { deleteStack(id: \"x\") { id } }"},
14-
{"subscription { runs { id } }", "subscription { runs { id } }"},
15-
{"stacks { id name }", "query { stacks { id name } }"},
16-
}
17-
for _, tc := range tests {
18-
if got := normalizeQuery(tc.in); got != tc.want {
19-
t.Errorf("normalizeQuery(%q) = %q, want %q", tc.in, got, tc.want)
20-
}
21-
}
22-
}
3+
import "testing"
234

245
func TestParseVariables(t *testing.T) {
256
if v, err := parseVariables(""); v != nil || err != nil {
@@ -52,24 +33,6 @@ func TestGraphqlErrors(t *testing.T) {
5233
}
5334
}
5435

55-
func TestIsMutation(t *testing.T) {
56-
tests := []struct {
57-
in string
58-
want bool
59-
}{
60-
{"mutation { deleteStack(id: \"x\") { id } }", true},
61-
{" Mutation { foo }", true},
62-
{"query { stacks { id } }", false},
63-
{"{ viewer { id } }", false},
64-
{"stacks { id }", false},
65-
}
66-
for _, tc := range tests {
67-
if got := isMutation(tc.in); got != tc.want {
68-
t.Errorf("isMutation(%q) = %v, want %v", tc.in, got, tc.want)
69-
}
70-
}
71-
}
72-
7336
func TestTruncate(t *testing.T) {
7437
if s := truncate([]byte("short"), 100); s != "short" {
7538
t.Errorf("got %q", s)

0 commit comments

Comments
 (0)