diff --git a/.gitignore b/.gitignore index 7c1aace..5255c62 100644 --- a/.gitignore +++ b/.gitignore @@ -25,4 +25,4 @@ # End of https://www.toptal.com/developers/gitignore/api/go -/dist \ No newline at end of file +/distsdt diff --git a/cmd/products/products.go b/cmd/products/products.go index 531cf32..e46c282 100644 --- a/cmd/products/products.go +++ b/cmd/products/products.go @@ -2,18 +2,16 @@ package products import ( "encoding/json" - "fmt" - "reflect" "strconv" "strings" "time" - shopify "github.com/bold-commerce/go-shopify/v3" "github.com/cheynewallace/tabby" "github.com/urfave/cli/v2" "github.com/ScreenStaring/shopify-dev-tools/cmd" + "github.com/ScreenStaring/shopify-dev-tools/gql" ) var Cmd cli.Command @@ -34,7 +32,76 @@ type listProductOptions struct { UpdatedAtMin time.Time `url:"updated_at_min,omitempty"` } -func printJSONL(products []shopify.Product) { +func buildQuery(options listProductOptions) string { + var queryType string + var args []string + var fields string + + // Determine fields to return + if len(options.Fields) > 0 { + fields = strings.Join(options.Fields, " ") + } else { + // Default fields that match the original REST API response + fields = "id title handle status vendor productType createdAt updatedAt" + } + + if len(options.Ids) > 0 { + // Query specific products by IDs + idStrings := make([]string, len(options.Ids)) + for i, id := range options.Ids { + idStrings[i] = fmt.Sprintf(`"gid://shopify/Product/%d"`, id) + } + args = append(args, fmt.Sprintf("ids: [%s]", strings.Join(idStrings, ", "))) + queryType = "products" + } else { + // Query products with filters + if options.Limit > 0 { + args = append(args, fmt.Sprintf("first: %d", options.Limit)) + } else { + args = append(args, "first: 10") // Default limit + } + + if options.Status != "" { + // Map REST API status values to GraphQL + switch strings.ToUpper(options.Status) { + case "ACTIVE": + args = append(args, `query: "status:active"`) + case "DRAFT": + args = append(args, `query: "status:draft"`) + case "ARCHIVED": + args = append(args, `query: "status:archived"`) + } + } + queryType = "products" + } + + argsStr := "" + if len(args) > 0 { + argsStr = fmt.Sprintf("(%s)", strings.Join(args, ", ")) + } + + if len(options.Ids) > 0 { + return fmt.Sprintf(`{ + %s%s { + nodes { + %s + } + } + }`, queryType, argsStr, fields) + } else { + return fmt.Sprintf(`{ + %s%s { + edges { + node { + %s + } + } + } + }`, queryType, argsStr, fields) + } +} + +func printJSONL(products []map[string]interface{}) { for _, product := range products { line, err := json.Marshal(product) if err != nil { @@ -59,7 +126,7 @@ func isFieldToPrint(field string, selectedFields []string) bool { return false } -func printFormatted(products []shopify.Product, fieldsToPrint []string) { +func printFormatted(products []map[string]interface{}, fieldsToPrint []string) { t := tabby.New() normalizedFieldsToPrint := []string{} @@ -68,18 +135,15 @@ func printFormatted(products []shopify.Product, fieldsToPrint []string) { } for _, product := range products { - s := reflect.ValueOf(&product).Elem() - - for i := 0; i < s.NumField(); i++ { - field := s.Type().Field(i).Name - normalizedField := normalizeField(field) + for key, value := range product { + normalizedField := normalizeField(key) if len(fieldsToPrint) > 0 { if isFieldToPrint(normalizedField, normalizedFieldsToPrint) { - t.AddLine(field, s.Field(i).Interface()) + t.AddLine(key, value) } } else { - t.AddLine(field, s.Field(i).Interface()) + t.AddLine(key, value) } } @@ -89,7 +153,6 @@ func printFormatted(products []shopify.Product, fieldsToPrint []string) { } func listProducts(c *cli.Context) error { - var products []shopify.Product var options listProductOptions if c.NArg() > 0 { @@ -116,11 +179,23 @@ func listProducts(c *cli.Context) error { options.Fields = strings.Split(c.String("fields"), ",") } - products, err := cmd.NewShopifyClient(c).Product.List(options) + // Create GraphQL client + shop := c.String("shop") + client := gql.NewClient(shop, cmd.LookupAccessToken(shop, c.String("access-token")), c.String("api-version")) + + // Build and execute GraphQL query + query := buildQuery(options) + result, err := client.Query(query) if err != nil { return fmt.Errorf("Cannot list products: %s", err) } + // Parse GraphQL response + products, err := parseProductsResponse(result, len(options.Ids) > 0) + if err != nil { + return fmt.Errorf("Cannot parse products response: %s", err) + } + if c.Bool("jsonl") { printJSONL(products) } else { @@ -130,6 +205,81 @@ func listProducts(c *cli.Context) error { return nil } +func parseProductsResponse(result map[string]interface{}, byIds bool) ([]map[string]interface{}, error) { + var products []map[string]interface{} + + // Check for GraphQL errors + if errors, ok := result["errors"]; ok { + return nil, fmt.Errorf("GraphQL errors: %v", errors) + } + + // Navigate to the products data + data, ok := result["data"].(map[string]interface{}) + if !ok { + return nil, fmt.Errorf("invalid response structure: missing data") + } + + productsData, ok := data["products"].(map[string]interface{}) + if !ok { + return nil, fmt.Errorf("invalid response structure: missing products") + } + + if byIds { + // For queries by ID, products are returned in nodes array + nodes, ok := productsData["nodes"].([]interface{}) + if !ok { + return nil, fmt.Errorf("invalid response structure: missing nodes") + } + + for _, node := range nodes { + if product, ok := node.(map[string]interface{}); ok { + // Convert GraphQL ID to numeric ID + if id, exists := product["id"]; exists { + if idStr, ok := id.(string); ok { + if numericId := extractNumericId(idStr); numericId != "" { + product["id"] = numericId + } + } + } + products = append(products, product) + } + } + } else { + // For paginated queries, products are returned in edges array + edges, ok := productsData["edges"].([]interface{}) + if !ok { + return nil, fmt.Errorf("invalid response structure: missing edges") + } + + for _, edge := range edges { + if edgeMap, ok := edge.(map[string]interface{}); ok { + if node, ok := edgeMap["node"].(map[string]interface{}); ok { + // Convert GraphQL ID to numeric ID + if id, exists := node["id"]; exists { + if idStr, ok := id.(string); ok { + if numericId := extractNumericId(idStr); numericId != "" { + node["id"] = numericId + } + } + } + products = append(products, node) + } + } + } + } + + return products, nil +} + +func extractNumericId(gid string) string { + // Extract numeric ID from GraphQL global ID like "gid://shopify/Product/123" + parts := strings.Split(gid, "/") + if len(parts) >= 4 && parts[0] == "gid:" && parts[1] == "" && parts[2] == "shopify" { + return parts[len(parts)-1] + } + return gid +} + func init() { productFlags := []cli.Flag{ // &cli.StringSliceFlag{ @@ -157,6 +307,11 @@ func init() { Aliases: []string{"j"}, Usage: "Output the products in JSONL format", }, + &cli.StringFlag{ + Name: "api-version", + Aliases: []string{"a"}, + Usage: "API version to use; default is a versionless call", + }, } Cmd = cli.Command{ diff --git a/go.mod b/go.mod index a778b96..309a67e 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,6 @@ module github.com/ScreenStaring/shopify-dev-tools go 1.15 require ( - github.com/bold-commerce/go-shopify v2.3.0+incompatible // indirect github.com/bold-commerce/go-shopify/v3 v3.14.0 github.com/cheynewallace/tabby v1.1.1 github.com/clbanning/mxj v1.8.4 @@ -11,6 +10,6 @@ require ( github.com/google/go-querystring v1.1.0 // indirect github.com/pkg/browser v0.0.0-20201207095918-0426ae3fba23 github.com/russross/blackfriday/v2 v2.1.0 // indirect - github.com/shopspring/decimal v1.3.1 // indirect + github.com/shopspring/decimal v1.3.1 github.com/urfave/cli/v2 v2.3.0 ) diff --git a/go.sum b/go.sum index b176e38..39fb8ff 100644 --- a/go.sum +++ b/go.sum @@ -1,11 +1,5 @@ github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= -github.com/bold-commerce/go-shopify v2.3.0+incompatible h1:AiedLiOoFWp7iVO8n6JJOM7IEdyU4nLAvUKNM7Hw8b4= -github.com/bold-commerce/go-shopify v2.3.0+incompatible/go.mod h1:R9OKSw+EViwy7MGrAxma3q+Vqq8kMyZu+OJhx/dcw6s= -github.com/bold-commerce/go-shopify/v3 v3.11.0 h1:3FXbtpUmhz91kswPxrIQWFneoxOTW+RWbmhp05bISjI= -github.com/bold-commerce/go-shopify/v3 v3.11.0/go.mod h1:MxKdd8wvTKrRLh19VLZsJwQy0Qw/8GO9TRol+6ErDxg= -github.com/bold-commerce/go-shopify/v3 v3.12.0 h1:zB65ikoXWKOfcQPZGX+1yg4gUDuMrndiqMW53rdrBrs= -github.com/bold-commerce/go-shopify/v3 v3.12.0/go.mod h1:MxKdd8wvTKrRLh19VLZsJwQy0Qw/8GO9TRol+6ErDxg= github.com/bold-commerce/go-shopify/v3 v3.14.0 h1:YHq1MegncCIOgXNVx4iuftYKe8VsCXn9QjO8O0ggubY= github.com/bold-commerce/go-shopify/v3 v3.14.0/go.mod h1:qOrEfYoy5RRO/PAq4vGyHW03NZmt2iX/fPGuaZwemtI= github.com/cheynewallace/tabby v1.1.1 h1:JvUR8waht4Y0S3JF17G6Vhyt+FRhnqVCkk8l4YrOU54= @@ -15,29 +9,26 @@ github.com/clbanning/mxj v1.8.4/go.mod h1:BVjHeAH+rl9rs6f+QIpeRl0tfu10SXn1pUSa5P github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= github.com/cpuguy83/go-md2man/v2 v2.0.0 h1:EoUDS0afbrsXAZ9YQ9jdu/mZ2sXgT1/2yyNng4PGlyM= github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/google/go-cmp v0.5.2 h1:X2ev0eStA3AbceY54o37/0PQ/UWqKEiiO2dKL5OPaFM= github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= -github.com/jarcoal/httpmock v1.0.4 h1:jp+dy/+nonJE4g4xbVtl9QdrUNbn6/3hDT5R4nDIZnA= -github.com/jarcoal/httpmock v1.0.4/go.mod h1:ATjnClrvW/3tijVmpL/va5Z3aAyGvqU3gCT8nX0Txik= +github.com/jarcoal/httpmock v1.3.0 h1:2RJ8GP0IIaWwcC9Fp2BmVi8Kog3v2Hn7VXM3fTd+nuc= github.com/jarcoal/httpmock v1.3.0/go.mod h1:3yb8rc4BI7TCBhFY8ng0gjuLKJNquuDNiPaZjnENuYg= +github.com/maxatome/go-testdeep v1.12.0 h1:Ql7Go8Tg0C1D/uMMX59LAoYK7LffeJQ6X2T04nTH68g= github.com/maxatome/go-testdeep v1.12.0/go.mod h1:lPZc/HAcJMP92l7yI6TRz1aZN5URwUBUAfUNvrclaNM= github.com/pkg/browser v0.0.0-20201207095918-0426ae3fba23 h1:dofHuld+js7eKSemxqTVIo8yRlpRw+H1SdpzZxWruBc= github.com/pkg/browser v0.0.0-20201207095918-0426ae3fba23/go.mod h1:N6UoU20jOqggOuDwUaBQpluzLNDqif3kq9z2wpdYEfQ= -github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/shopspring/decimal v0.0.0-20200105231215-408a2507e114/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= -github.com/shopspring/decimal v1.2.0 h1:abSATXmQEYyShuxI4/vyW3tV1MrKAJzCZ/0zLUXYbsQ= -github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= github.com/shopspring/decimal v1.3.1 h1:2Usl1nmF/WZucqkFZhnfFYxxxu8LG21F6nPQBE5gKV8= github.com/shopspring/decimal v1.3.1/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= -github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo= github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= github.com/urfave/cli/v2 v2.3.0 h1:qph92Y649prgesehzOrQjdWyxFOp/QVM+6imKHad91M= github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI= diff --git a/sdt b/sdt new file mode 100755 index 0000000..7f40ebe Binary files /dev/null and b/sdt differ