Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add stdin support in CLI #608

Merged
merged 4 commits into from
Jul 8, 2022
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 43 additions & 0 deletions cli/cli.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
// 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

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

func stdinIsPipe() (bool, error) {
orpheuslummis marked this conversation as resolved.
Show resolved Hide resolved
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 := stdinIsPipe()
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")
}
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 := stdinIsPipe()
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")
}