diff --git a/cli/cli.go b/cli/cli.go new file mode 100644 index 0000000000..0b2c2f08aa --- /dev/null +++ b/cli/cli.go @@ -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 +} diff --git a/cli/query.go b/cli/query.go index a9a76902d1..f5bcb71dc0 100644 --- a/cli/query.go +++ b/cli/query.go @@ -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{} @@ -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") } diff --git a/cli/root.go b/cli/root.go index b9d54c4af1..e5f9798f60 100644 --- a/cli/root.go +++ b/cli/root.go @@ -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 ( diff --git a/cli/schema_add.go b/cli/schema_add.go index 29bcce5b68..e5c0117e19 100644 --- a/cli/schema_add.go +++ b/cli/schema_add.go @@ -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") }