commit 7bb538f44fe2e83c810af33436ca585d4be05b2c from: Oliver Lowe date: Sat Dec 20 05:32:26 2025 UTC gql: add it Need to deal with GraphQL without some bloated web app commit - cec1a3c7758f8fd6368f9066c7bb23580f2cd4d9 commit + 7bb538f44fe2e83c810af33436ca585d4be05b2c blob - /dev/null blob + 4c944815a4bb123f565845369d144f93ffccc233 (mode 644) --- /dev/null +++ man/gql.1 @@ -0,0 +1,43 @@ +.Dd +.Dt GQL 1 +.Sh NAME +.Nm gql +.Nd execute GraphQL queries +.Sh SYNOPSIS +.Nm +.Op Fl k Ar file +.Op Fl r Ar file +.Op Fl v Ar var=value +.Ar url +.Op Ar +.Sh DESCRIPTION +.Nm +executes the queries read from the named files +.Pq or from the standard input if none specified +to the GraphQL API served at +.Ar url . +The response is written to the standard output. +.Pp +The following flags are understood: +.Bl -tag -width indent +.It Fl k Ar file +Reads the API key from +.Pa file +to be be included in the Authorization field +of the HTTP request as a Bearer token. +.It Fl r Ar file +Loads the JSON-encoded variables object +from +.Pa file . +.It Fl v Ar var=value +Sets the named variable +.Ar var +to +.Ar value . +Any variables loaded from a file via the +.Fl r +flag will be discarded. +.El + +.Sh EXIT STATUS +.Ex blob - /dev/null blob + 064e8bc2904e37848f24e9a3a708901ad80c1ff0 (mode 644) --- /dev/null +++ src/gql/gql.go @@ -0,0 +1,107 @@ +package main + +import ( + "bytes" + "encoding/json" + "flag" + "fmt" + "io" + "log" + "net/http" + "os" + "strings" +) + +const usage string = "gql [-k file] [-r file] [-v var=value ] url [file]" + +// Firefox ESR (Extended Support Release). +const firefox = "Mozilla/5.0 (X11; Intel Linux x86_64; rv:140.0) Gecko/20100101 Firefox/140.0" + +type Query struct { + Query string `json:"query"` + Variables any `json:"variables,omitempty"` +} + +var tokenFlag = flag.String("k", "", "file containing api key") +var varFlag = flag.String("v", "", "variable in key=value format") +var rFlag = flag.String("r", "", "file containing variables in json format") + +var token string + +func init() { + flag.Parse() +} + +func main() { + if len(flag.Args()) == 0 { + fmt.Println("usage:", usage) + os.Exit(2) + } + + if *tokenFlag != "" { + b, err := os.ReadFile(*tokenFlag) + if err != nil { + log.Fatalf("read key file: %v", err) + } + token = string(bytes.TrimSpace(b)) + } + + var query Query + if *varFlag != "" { + k, v, ok := strings.Cut(*varFlag, "=") + if !ok { + log.Fatalf("missing = separator in variable spec %q", *varFlag) + } + query.Variables = map[string]string{k: v} + } else if *rFlag != "" { + f, err := os.Open(*rFlag) + if err != nil { + log.Fatalf("open variables file: %v", err) + } + defer f.Close() + if err := json.NewDecoder(f).Decode(&query.Variables); err != nil { + log.Fatalf("parse variables from %s: %v", *rFlag, err) + } + } + + url := flag.Arg(0) + + rd := os.Stdin + if len(flag.Args()) == 2 { + f, err := os.Open(flag.Arg(1)) + if err != nil { + log.Fatal(err) + } + defer f.Close() + rd = f + } + b, err := io.ReadAll(rd) + if err != nil { + log.Fatalf("read query: %v", err) + } + query.Query = string(bytes.TrimSpace(b)) + + buf := &bytes.Buffer{} + if err := json.NewEncoder(buf).Encode(&query); err != nil { + log.Fatalf("encode graphql query: %v", err) + } + println(buf.String()) + os.Exit(1) + + req, err := http.NewRequest(http.MethodPost, url, buf) + if err != nil { + log.Fatal(err) + } + if token != "" { + req.Header.Set("Authorization", "Bearer "+token) + } + req.Header.Set("User-Agent", firefox) + req.Header.Set("Content-Type", "application/json") + + resp, err := http.DefaultClient.Do(req) + if err != nil { + log.Fatal(err) + } + fmt.Fprintln(os.Stderr, resp.Status) + io.Copy(os.Stdout, resp.Body) +}