Skip to content

Commit

Permalink
feat: Add support for stdin as input in CLI (#608)
Browse files Browse the repository at this point in the history
  • Loading branch information
orpheuslummis authored Jul 8, 2022
1 parent d5b3cbb commit e6b41d7
Show file tree
Hide file tree
Showing 4 changed files with 189 additions and 82 deletions.
46 changes: 46 additions & 0 deletions cli/cli.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
// Copyright 2022 Democratized Data Foundation
//
// Use of this software is governed by the Business Source License
// included in the file licenses/BSL.txt.
//
// As of the Change Date specified in that file, in accordance with
// the Business Source License, use of this software will be governed
// by the Apache License, Version 2.0, included in the file
// licenses/APL.txt.

/*
Package cli provides the command-line interface.
*/
package cli

import (
"bufio"
"bytes"
"encoding/json"
"fmt"
"os"
"strings"
)

func isStdinPipe() (bool, error) {
fileInfo, err := os.Stdin.Stat()
return fileInfo.Mode()&os.ModeCharDevice == 0, err
}

func readStdin() (string, error) {
var s strings.Builder
scanner := bufio.NewScanner(os.Stdin)
for scanner.Scan() {
s.Write(scanner.Bytes())
}
if err := scanner.Err(); err != nil {
return "", fmt.Errorf("reading standard input: %w", err)
}
return s.String(), nil
}

func indentJSON(b []byte) (string, error) {
var indentedJSON bytes.Buffer
err := json.Indent(&indentedJSON, b, "", " ")
return indentedJSON.String(), err
}
104 changes: 64 additions & 40 deletions cli/query.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,44 +11,75 @@
package cli

import (
"context"
"fmt"
"io"
"net/http"
"net/url"

httpapi "github.com/sourcenetwork/defradb/api/http"
"github.com/sourcenetwork/defradb/logging"
"github.com/spf13/cobra"

httpapi "github.com/sourcenetwork/defradb/api/http"
)

// queryCmd represents the query command
var queryCmd = &cobra.Command{
Use: "query",
Short: "Send a GraphQL query",
Long: `Use this command if you wish to send a formatted GraphQL
query to the database. It's advised to use a proper GraphQL client
to interact with the database, the reccomended approach is with a
local GraphiQL application (https://github.com/graphql/graphiql).
To learn more about the DefraDB GraphQL Query Language, you may use
the additional documentation found at: https://hackmd.io/@source/BksQY6Qfw.
`,
Run: func(cmd *cobra.Command, args []string) {
ctx := context.Background()

if len(args) != 1 {
log.Fatal(ctx, "Needs a single query argument")
Use: "query [query]",
Short: "Send a DefraDB GraphQL query",
Long: `Send a DefraDB GraphQL query to the database.
A query can be sent as a single argument. Example command:
defradb client query 'query { ... }'
Or it can be sent via stdin by using the '-' special syntax. Example command:
cat query.graphql | defradb client query -
A GraphQL client such as GraphiQL (https://github.com/graphql/graphiql) can be used to interact
with the database more conveniently.
To learn more about the DefraDB GraphQL Query Language, refer to https://docs.source.network.`,
RunE: func(cmd *cobra.Command, args []string) error {
var query string
inputIsPipe, err := isStdinPipe()
if err != nil {
return err
}

if len(args) > 1 {
return fmt.Errorf("too many arguments")
}
query := args[0]

if inputIsPipe && (len(args) == 0 || args[0] != "-") {
log.FeedbackInfo(
cmd.Context(),
"Run 'defradb client query -' to read from stdin. Example: 'cat my.graphql | defradb client query -').",
)
return nil
} else if len(args) == 0 {
err := cmd.Help()
if err != nil {
return fmt.Errorf("failed to print help: %w", err)
}
return nil
} else if args[0] == "-" {
stdin, err := readStdin()
if err != nil {
return fmt.Errorf("failed to read stdin: %w", err)
}
if len(stdin) == 0 {
return fmt.Errorf("no query in stdin provided")
} else {
query = stdin
}
} else {
query = args[0]
}

if query == "" {
log.Error(ctx, "Missing query")
return
return fmt.Errorf("query cannot be empty")
}

endpoint, err := httpapi.JoinPaths(cfg.API.AddressToURL(), httpapi.GraphQLPath)
if err != nil {
log.ErrorE(ctx, "Join paths failed", err)
return
return fmt.Errorf("joining paths failed: %w", err)
}

p := url.Values{}
Expand All @@ -57,37 +88,30 @@ the additional documentation found at: https://hackmd.io/@source/BksQY6Qfw.

res, err := http.Get(endpoint.String())
if err != nil {
log.ErrorE(ctx, "Request failed", err)
return
return fmt.Errorf("failed to send query: %w", err)
}

defer func() {
err = res.Body.Close()
if err != nil {
log.ErrorE(ctx, "Response body closing failed: ", err)
log.ErrorE(cmd.Context(), "response body closing failed: ", err)
}
}()

buf, err := io.ReadAll(res.Body)
if err != nil {
log.ErrorE(ctx, "Request failed", err)
return
return fmt.Errorf("failed to read response body: %w", err)
}

log.Info(ctx, "", logging.NewKV("Response", string(buf)))
indentedResult, err := indentJSON(buf)
if err != nil {
return fmt.Errorf("failed to pretty print result: %w", err)
}
log.FeedbackInfo(cmd.Context(), indentedResult)
return nil
},
}

func init() {
clientCmd.AddCommand(queryCmd)

// Here you will define your flags and configuration settings.

// Cobra supports Persistent Flags which will work for this command
// and all subcommands, e.g.:
// queryCmd.PersistentFlags().String("foo", "", "A help for foo")

// Cobra supports local flags which will only run when this command
// is called directly, e.g.:
// queryCmd.Flags().StringVar(&queryStr, "query", "", "Query to run on the database")
}
3 changes: 0 additions & 3 deletions cli/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,6 @@
// by the Apache License, Version 2.0, included in the file
// licenses/APL.txt.

/*
Package cli provides the command-line interface.
*/
package cli

import (
Expand Down
118 changes: 79 additions & 39 deletions cli/schema_add.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,74 +11,114 @@
package cli

import (
"bytes"
"context"
"fmt"
"io"
"net/http"
"os"
"strings"

httpapi "github.com/sourcenetwork/defradb/api/http"
"github.com/sourcenetwork/defradb/logging"
"github.com/spf13/cobra"
)

var (
schemaFile string
httpapi "github.com/sourcenetwork/defradb/api/http"
)

// addCmd represents the add command
var schemaFile string

var addCmd = &cobra.Command{
Use: "add",
Short: "Add a new schema type to a DefraDB instance",
Long: `Example Usage:
> defradb client schema add -f user.sdl`,
Run: func(cmd *cobra.Command, args []string) {
ctx := context.Background()

var schema []byte
if len(args) > 0 {
schema = []byte(strings.Join(args, "\n"))
} else if schemaFile != "" {
Use: "add [schema]",
Short: "Add a new schema type to DefraDB",
Long: `Add a new schema type to DefraDB.
Example: add as an argument string:
defradb client schema add 'type Foo { ... }'
Example: add from file:
defradb client schema add -f schema.graphql
Example: add from stdin:
cat schema.graphql | defradb client schema add -
To learn more about the DefraDB GraphQL Schema Language, refer to https://docs.source.network.`,
RunE: func(cmd *cobra.Command, args []string) error {
var schema string
inputIsPipe, err := isStdinPipe()
if err != nil {
return err
}

if len(args) > 1 {
return fmt.Errorf("too many arguments")
}

if schemaFile != "" {
buf, err := os.ReadFile(schemaFile)
cobra.CheckErr(err)
schema = buf
if err != nil {
return fmt.Errorf("failed to read schema file: %w", err)
}
schema = string(buf)
} else if inputIsPipe && (len(args) == 0 || args[0] != "-") {
log.FeedbackInfo(
cmd.Context(),
"Run 'defradb client schema add -' to read from stdin."+
" Example: 'cat schema.graphql | defradb client schema add -').",
)
return nil
} else if len(args) == 0 {
err := cmd.Help()
if err != nil {
return fmt.Errorf("failed to print help: %w", err)
}
return nil
} else if args[0] == "-" {
stdin, err := readStdin()
if err != nil {
return fmt.Errorf("failed to read stdin: %w", err)
}
if len(stdin) == 0 {
return fmt.Errorf("no schema in stdin provided")
} else {
schema = stdin
}
} else {
log.Fatal(ctx, "Missing schema")
schema = args[0]
}

if schema == "" {
return fmt.Errorf("empty schema provided")
}

endpoint, err := httpapi.JoinPaths(cfg.API.AddressToURL(), httpapi.SchemaLoadPath)
if err != nil {
log.ErrorE(ctx, "Join paths failed", err)
return
return fmt.Errorf("join paths failed: %w", err)
}

res, err := http.Post(endpoint.String(), "text", bytes.NewBuffer(schema))
cobra.CheckErr(err)
res, err := http.Post(endpoint.String(), "text", strings.NewReader(schema))
if err != nil {
return fmt.Errorf("failed to post schema: %w", err)
}

defer func() {
err = res.Body.Close()
if err != nil {
log.ErrorE(ctx, "Response body closing failed", err)
log.ErrorE(cmd.Context(), "response body closing failed", err)
}
}()

result, err := io.ReadAll(res.Body)
cobra.CheckErr(err)
log.Info(ctx, "", logging.NewKV("Result", string(result)))
if err != nil {
return fmt.Errorf("failed to read response body: %w", err)
}

indentedResult, err := indentJSON(result)
if err != nil {
return fmt.Errorf("failed to pretty print result: %w", err)
}
log.FeedbackInfo(cmd.Context(), indentedResult)
return nil
},
}

func init() {
schemaCmd.AddCommand(addCmd)

// Here you will define your flags and configuration settings.

// Cobra supports Persistent Flags which will work for this command
// and all subcommands, e.g.:
// addCmd.PersistentFlags().String("foo", "", "A help for foo")

// Cobra supports local flags which will only run when this command
// is called directly, e.g.:
addCmd.Flags().StringVarP(&schemaFile, "file", "f", "", "File to load a schema from")
addCmd.Flags().StringVarP(&schemaFile, "file", "f", "", "file to load a schema from")
}

0 comments on commit e6b41d7

Please sign in to comment.